[
  {
    "path": ".github/workflows/build-pr.yml",
    "content": "name: \"Build for PR\"\n\nconcurrency: \n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\non:\n  pull_request:\n\njobs:\n  windows:\n    runs-on: windows-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        python_version:\n            - \"3.12\"\n    name: Windows Python ${{ matrix.python_version }}\n    steps:\n      - uses: actions/checkout@v2\n\n      - uses: ilammy/msvc-dev-cmd@v1\n\n      - name: Use Python ${{ matrix.python_version }}\n        uses: actions/setup-python@v2\n        with:\n          python-version: ${{ matrix.python_version }}\n\n      - name: 🧳 Install dependencies\n        run: |\n          echo y | pip install --no-python-version-warning --disable-pip-version-check pyinstaller\n          echo y | pip install --no-python-version-warning --disable-pip-version-check nuitka\n          echo y | pip install --no-python-version-warning --disable-pip-version-check zstandard\n          echo y | pip install --no-python-version-warning --disable-pip-version-check pygame\n          echo y | pip install --no-python-version-warning --disable-pip-version-check ordered-set\n\n      # 使用pyinstaller构建\n      - name: Build pypvz with pyinstaller\n        run: |\n          pyinstaller -F pypvz.py `\n                  -n pypvz-with-python${{ matrix.python_version }}-pyinstaller-x64.exe `\n                  --distpath ./out `\n                  --noconsole `\n                  --add-data=\"resources;./resources\" `\n                  --add-data=\"pypvz-exec-logo.png;./pypvz-exec-logo.png\" `\n                  -i ./pypvz.ico\n\n      - name: Release the version built by pyinstaller\n        if: github.event.pull_request.head.repo.full_name == github.repository\n        uses: ncipollo/release-action@v1\n        with:\n          allowUpdates: true\n          tag: Dev.Version.Built.with.Pyinstaller\n          artifacts: ./out/*pyinstaller*.exe\n          token: ${{ secrets.GITHUB_TOKEN }}\n\n      # 使用Nuitka构建\n      - name: Show nuitka version\n        run: |\n          Get-ChildItem env:\n          python -m nuitka --version\n\n      - name: Build pypvz with Nuitka\n        run: |\n          echo y | python -m nuitka --standalone `\n                  --onefile `\n                  --show-progress `\n                  --show-memory `\n                  --output-dir=out `\n                  --windows-icon-from-ico=pypvz.ico `\n                  --include-data-file=c:\\hostedtoolcache\\windows\\python\\${{ matrix.python_version }}*\\x64\\lib\\site-packages\\pygame\\libogg-0.dll=libogg-0.dll `\n                  --include-data-file=c:\\hostedtoolcache\\windows\\python\\${{ matrix.python_version }}*\\x64\\lib\\site-packages\\pygame\\libopus-0.dll=libopus-0.dll `\n                  --include-data-file=c:\\hostedtoolcache\\windows\\python\\${{ matrix.python_version }}*\\x64\\lib\\site-packages\\pygame\\libopusfile-0.dll=libopusfile-0.dll `\n                  --include-data-file=c:\\hostedtoolcache\\windows\\python\\${{ matrix.python_version }}*\\x64\\lib\\site-packages\\pygame\\libjpeg-9.dll=libjpeg-9.dll `\n                  --include-data-dir=resources=resources `\n                  --windows-disable-console `\n                  -o pypvz-with-python${{ matrix.python_version }}-nuitka-windows-x64.exe `\n                  pypvz.py\n\n      - name: Release the version built by nuitka\n        if: github.event.pull_request.head.repo.full_name == github.repository\n        uses: ncipollo/release-action@v1\n        with:\n          allowUpdates: true\n          tag: Dev\n          artifacts: ./out/*nuitka*.exe\n          token: ${{ secrets.GITHUB_TOKEN }}\n\n\n  linux:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        python_version:\n            - \"3.12\"\n    name: Ubuntu Python ${{ matrix.python_version }}\n    steps:\n      - name: 🛎️ Checkout\n        uses: actions/checkout@v2\n        with:\n          fetch-depth: 0\n\n      - name: 🐍 Use Python ${{ matrix.python_version }}\n        uses: actions/setup-python@v2\n        with:\n          python-version: ${{ matrix.python_version }}\n\n      - name: 🧳 Install dependencies\n        run: |\n          sudo apt-get update\n          sudo apt-get install patchelf gdb ccache libfuse2 zstd tar\n          python -m pip install --no-python-version-warning --disable-pip-version-check zstandard appdirs ordered-set tqdm Jinja2\n          python -m pip install --no-python-version-warning --disable-pip-version-check nuitka\n          python -m pip install --no-python-version-warning --disable-pip-version-check pygame\n\n      # 使用Nuitka构建\n      - name: Show nuitka version\n        run: |\n          env\n          python -m nuitka --version\n\n      - name: Build pypvz with Nuitka\n        run: |\n          yes | python -m nuitka \\\n                --onefile \\\n                --standalone \\\n                --include-data-dir=resources=resources \\\n                --linux-onefile-icon=pypvz.png \\\n                --static-libpython=no \\\n                --remove-output \\\n                -o pypvz-with-python${{ matrix.python_version }}-linux-x86_64.bin \\\n                pypvz.py\n\n      - name: Release the version built by nuitka\n        if: github.event.pull_request.head.repo.full_name == github.repository\n        uses: ncipollo/release-action@v1\n        with:\n          allowUpdates: true\n          tag: Dev\n          artifacts: ./pypvz*-x86_64.*\n          token: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\n\nconcurrency: \n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\non:\n  push:\n    branches:\n      - master\n\njobs:\n  windows:\n    runs-on: windows-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        python_version:\n            - \"3.12\"\n    name: Windows Python ${{ matrix.python_version }}\n    steps:\n      - uses: actions/checkout@v2\n\n      - uses: ilammy/msvc-dev-cmd@v1\n\n      - name: Use Python ${{ matrix.python_version }}\n        uses: actions/setup-python@v2\n        with:\n          python-version: ${{ matrix.python_version }}\n\n      - name: 🧳 Install dependencies\n        run: |\n          echo y | pip install --no-python-version-warning --disable-pip-version-check pyinstaller\n          echo y | pip install --no-python-version-warning --disable-pip-version-check nuitka\n          echo y | pip install --no-python-version-warning --disable-pip-version-check zstandard\n          echo y | pip install --no-python-version-warning --disable-pip-version-check pygame\n          echo y | pip install --no-python-version-warning --disable-pip-version-check ordered-set\n\n      # 使用pyinstaller构建\n      - name: Build pypvz with pyinstaller\n        run: |\n          pyinstaller -F pypvz.py `\n                  -n pypvz-with-python${{ matrix.python_version }}-pyinstaller-x64.exe `\n                  --distpath ./out `\n                  --noconsole `\n                  --add-data=\"resources;./resources\" `\n                  --add-data=\"pypvz-exec-logo.png;./pypvz-exec-logo.png\" `\n                  -i ./pypvz.ico\n\n      - name: Release the version built by pyinstaller\n        uses: ncipollo/release-action@v1\n        with:\n          allowUpdates: true\n          tag: Current.Version.Built.with.Pyinstaller\n          artifacts: ./out/*pyinstaller*.exe\n          token: ${{ secrets.GITHUB_TOKEN }}\n\n      # 使用Nuitka构建\n      - name: Show nuitka version\n        run: |\n          Get-ChildItem env:\n          python -m nuitka --version\n\n      - name: Build pypvz with Nuitka\n        run: |\n          echo y | python -m nuitka --standalone `\n                  --onefile `\n                  --show-progress `\n                  --show-memory `\n                  --output-dir=out `\n                  --windows-icon-from-ico=pypvz.ico `\n                  --include-data-file=c:\\hostedtoolcache\\windows\\python\\${{ matrix.python_version }}*\\x64\\lib\\site-packages\\pygame\\libogg-0.dll=libogg-0.dll `\n                  --include-data-file=c:\\hostedtoolcache\\windows\\python\\${{ matrix.python_version }}*\\x64\\lib\\site-packages\\pygame\\libopus-0.dll=libopus-0.dll `\n                  --include-data-file=c:\\hostedtoolcache\\windows\\python\\${{ matrix.python_version }}*\\x64\\lib\\site-packages\\pygame\\libopusfile-0.dll=libopusfile-0.dll `\n                  --include-data-file=c:\\hostedtoolcache\\windows\\python\\${{ matrix.python_version }}*\\x64\\lib\\site-packages\\pygame\\libjpeg-9.dll=libjpeg-9.dll `\n                  --include-data-dir=resources=resources `\n                  --windows-disable-console `\n                  -o pypvz-with-python${{ matrix.python_version }}-nuitka-windows-x64.exe `\n                  pypvz.py\n\n\n      - name: Release the version built by nuitka\n        uses: ncipollo/release-action@v1\n        with:\n          allowUpdates: true\n          tag: Latest\n          artifacts: ./out/*nuitka*.exe\n          token: ${{ secrets.GITHUB_TOKEN }}\n\n  linux:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        python_version:\n            - \"3.12\"\n    name: Ubuntu Python ${{ matrix.python_version }}\n    steps:\n      - name: 🛎️ Checkout\n        uses: actions/checkout@v2\n        with:\n          fetch-depth: 0\n\n      - name: 🐍 Use Python ${{ matrix.python_version }}\n        uses: actions/setup-python@v2\n        with:\n          python-version: ${{ matrix.python_version }}\n\n      - name: 🧳 Install dependencies\n        run: |\n          sudo apt-get update\n          sudo apt-get install patchelf gdb ccache libfuse2 zstd tar\n          python -m pip install --no-python-version-warning --disable-pip-version-check zstandard appdirs ordered-set tqdm Jinja2\n          python -m pip install --no-python-version-warning --disable-pip-version-check nuitka\n          python -m pip install --no-python-version-warning --disable-pip-version-check pygame\n\n      # 使用Nuitka构建\n      - name: Show nuitka version\n        run: |\n          env\n          python -m nuitka --version\n\n      - name: Build pypvz with Nuitka\n        run: |\n          yes | python -m nuitka \\\n                --onefile \\\n                --standalone \\\n                --include-data-dir=resources=resources \\\n                --linux-onefile-icon=pypvz.png \\\n                --static-libpython=no \\\n                --remove-output \\\n                -o pypvz-with-python${{ matrix.python_version }}-linux-x86_64.bin \\\n                pypvz.py\n\n      - name: Release the version built by nuitka\n        uses: ncipollo/release-action@v1\n        with:\n          allowUpdates: true\n          tag: Latest\n          artifacts: ./pypvz*-x86_64.*\n          token: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# 忽略构建内容\nout/\ntest-build/\nrelease/\n# 忽略调试内容\n.vscode/\n# 忽略 Pycharm 项目文件\n.idea/\n__pycache__/\n*/__pycache__/\n# 忽略测试文件\ntest*.py\n# uv 管理的虚拟环境\n.venv\n"
  },
  {
    "path": ".python-version",
    "content": "3.12\n"
  },
  {
    "path": "README.md",
    "content": "## Python版植物大战僵尸\n\n植物大战僵尸游戏的Python实现，基于[marblexu的项目进行创作](https://github.com/marblexu/PythonPlantsVsZombies)，部分代码也整合自[callmebg的项目](https://github.com/callmebg/PythonPlantsVsZombies)\n\n**本项目为个人python语言学习的练习项目，仅供个人学习和研究使用，不得用于其他用途。如果这个游戏侵犯了版权，请联系我删除**\n\n* 已有的植物：向日葵，豌豆射手，坚果墙，寒冰射手，樱桃炸弹，双发射手，三线射手，大嘴花，小喷菇，土豆雷，地刺，胆小菇，倭瓜，火爆辣椒，阳光菇，寒冰菇，魅惑菇，火炬树桩，睡莲，杨桃，咖啡豆，海蘑菇，高坚果，缠绕水草，毁灭菇，墓碑吞噬者，大喷菇，大蒜，南瓜头\n* 已有的僵尸：普通僵尸，旗帜僵尸，路障僵尸，铁桶僵尸，读报僵尸，橄榄球僵尸，鸭子救生圈僵尸，铁门僵尸，撑杆跳僵尸，冰车僵尸，潜水僵尸\n* 使用JSON文件记录关卡信息数据\n  * 在0.8.18.0及以后直接用python记录关卡的不可变数据，JSON目前仅用于用户存档\n* 支持选择植物卡片\n* 支持白昼模式，夜晚模式，泳池模式，浓雾模式（暂时没有加入雾），传送带模式和坚果保龄球模式\n* 支持背景音乐播放\n  * 支持调节音量\n* 支持音效\n  * 支持与背景音乐一起调节音量\n* 支持全屏模式\n  * 按`F`键进入全屏模式，按`U`键恢复至窗口模式\n* 支持用小铲子移除植物\n* 支持分波生成僵尸\n* 支持“关卡进程”进度条显示\n* 夜晚模式支持墓碑以及从墓碑生成僵尸\n* 含有泳池的模式支持在最后一波时从泳池中自动冒出僵尸\n* 支持保存进度\n  * Windows下默认进度文件的保存路径为`~\\AppData\\Roaming\\pypvz\\userdata.json`\n  * 其他操作系统为`~/.config/pypvz/userdata.json`\n  * 存档为JSON文件，如果出现因存档损坏而造成程序无法启动，可以手动编辑修复或者删除该文件重试\n    * 0.8.12.0版本后理论上不可能因为存档损坏而无法启动，如果有，请在[issues](https://github.com/wszqkzqk/pypvz/issues)中报告bug\n      * 仍然有可能因为升级后变量名不同而丢失存档的进度信息，这种情况手动编辑恢复即可\n* 支持错误日志记录\n  * Windows下默认日志文件的保存路径为`~\\AppData\\Roaming\\pypvz\\run.log`\n  * 其他操作系统为`~/.config/pypvz/run.log`\n* 支持自定义游戏速度倍率\n  * 保存在游戏存档文件中，可以通过修改`game rate`值更改速度倍率\n* 游戏完成成就显示\n  * 任意一游戏模式全部完成显示银向日葵奖杯\n  * 所有模式全部完成显示金向日葵奖杯\n  * 光标移动到向日葵奖杯上是显示当前各个模式通关次数\n* 含有游戏帮助界面 QwQ\n\n## 环境安装\n\n建议使用 [uv](https://docs.astral.sh/uv/) 安装依赖：\n\n```bash\ngit clone https://github.com/wszqkzqk/pypvz.git\ncd pypvz\nuv sync\n```\n\n或者参考：\n\n* `Python3` （建议 >= 3.10，最好使用最新版）\n* `Python-Pygame` （建议 >= 2.0，最好使用最新版）\n\n## 开始游戏\n\n### 使用仓库源代码\n\n先克隆仓库内容，再运行`pypvz.py`：\n\n```shell\ngit clone https://github.com/wszqkzqk/pypvz.git\ncd pypvz\npython pypvz.py\n```\n\n### 使用Windows可执行文件\n\n下载`pypvz.exe`文件，双击运行即可\n- 可以在仓库的[`Releases`](https://github.com/wszqkzqk/pypvz/releases)页面中[下载最新版（点击跳转）](https://github.com/wszqkzqk/pypvz/releases/latest)（推荐）：\n  - 使用GCC编译\n  - 程序包含名称、版本等信息\n  - 得到的验证最多\n  - 并非每次提交都会更新，更新可能不及时\n- 也可以直接下载GitHub Workflow[自动利用Nuitka构建的版本（点击跳转）](https://github.com/wszqkzqk/pypvz/releases/tag/Latest)（推荐）：\n  - 使用MSVC编译\n  - 每次合并提交到主分支时更新\n  - 得到的验证较多\n  - 服务器构建，编译环境更纯粹，冗余更少，体积更小\n- 还可以下载GitHub Workflow[自动利用Pyinstaller构建的版本（点击跳转）](https://github.com/wszqkzqk/pypvz/releases/tag/Current.Version.Built.with.Pyinstaller)：\n  - 在程序闪退时有报错窗口弹出\n  - 程序性能较差，不推荐\n- 均仅支持64位操作系统\n- 不依赖python、pygame等外部环境，开箱即用\n\n### 使用Linux可执行文件\n\n由于Linux几乎都标配了Python环境，因此本程序不太重视Linux下可执行的单文件的维护，因此没有手动构建版，只能下载自动构建的软件包。可以在仓库的[`Releases`](https://github.com/wszqkzqk/pypvz/releases)页面中[下载最新版（点击跳转）](https://github.com/wszqkzqk/pypvz/releases/latest)。\n\n## 方法\n\n* 使用鼠标收集阳光,种植植物\n* 对于已经存在存档的用户，可以在`~\\AppData\\Roaming\\pypvz\\userdata.json`（Windows）或`~/.config/pypvz/userdata.json`（其他操作系统）中修改当前关卡：\n  * 冒险模式：\n    * 白昼模式——单行草皮：1\n    * 白昼模式——三行草皮：2\n    * 白昼模式：3~5\n    * 夜晚模式：6~8\n    * 泳池模式：9~11\n    * 浓雾模式（暂时没有雾）：12\n  * 小游戏模式：\n    * 坚果保龄球：1\n    * 传送带模式（白天）：2\n    * 传送带模式（黑夜）：3\n    * 传送带模式（泳池）：4\n    * 坚果保龄球(II)：5\n  * 目前暂时按照以上设定，未与原版相符\n* 可以通过修改存档JSON文件中的`game rate`值来调节游戏速度倍率\n\n## Windows单文件封装\n\n### 使用Nuitka进行构建\n\n编译依赖：\n- `Python3` （建议 >= 3.10，最好使用最新版）\n- `Python-Pygame` （建议 >= 2.0，最好使用最新版）\n- `Nuitka`\n- `MinGW-w64`（或其他C编译器）\n- `ccache`\n- `depends`\n- `python-zstandard`（可选）\n\n**在编译环境安装不全时，Nuitka可以自动安装MinGW-w64、ccache和depends**\n\n- 由于目前Nuitka打包尚存bug，无法自动封装`pygame`中用来解码音频的相关`.dll`文件，因此需要手动在编译命令中添加\n  - 对于`mp3`编码，需要添加`libmpg123-0.dll`\n  - 对于`vorbis`编码，需要添加`libogg-0.dll`，`libvorbis-0.dll`和`libvorbisfile-3.dll`\n  - 对于`opus`编码，需要添加`libogg-0.dll`，`libopus-0.dll`和`libopusfile-0.dll`\n- 以添加`opus`和`vorbis`编码的背景音乐支持为例，编译需执行以下命令：\n\n``` cmd\ngit clone https://github.com/wszqkzqk/pypvz.git\ncd pypvz\nnuitka --mingw64 --standalone `\n        --onefile `\n        --show-progress `\n        --show-memory `\n        --output-dir=release `\n        --windows-icon-from-ico=pypvz.ico `\n        --include-data-dir=resources=resources `\n        --include-data-file=C:\\Users\\17265\\AppData\\Local\\Programs\\Python\\Python310\\Lib\\site-packages\\pygame\\libogg-0.dll=libogg-0.dll `\n        --include-data-file=C:\\Users\\17265\\AppData\\Local\\Programs\\Python\\Python310\\Lib\\site-packages\\pygame\\libopus-0.dll=libopus-0.dll `\n        --include-data-file=C:\\Users\\17265\\AppData\\Local\\Programs\\Python\\Python310\\Lib\\site-packages\\pygame\\libopusfile-0.dll=libopusfile-0.dll `\n        --include-data-file=C:\\Users\\17265\\AppData\\Local\\Programs\\Python\\Python310\\Lib\\site-packages\\pygame\\libvorbisfile-3.dll=libvorbisfile-3.dll `\n        --include-data-file=C:\\Users\\17265\\AppData\\Local\\Programs\\Python\\Python310\\Lib\\site-packages\\pygame\\libvorbis-0.dll=libvorbis-0.dll `\n        --lto=yes `\n        --windows-disable-console `\n        --windows-product-name=pypvz `\n        --windows-company-name=wszqkzqk.dev `\n        --windows-file-description=\"pypvz\" `\n        --windows-product-version=0.8.2.0 `\n        pypvz.py\n```\n\n* 其中`C:\\Users\\17265\\AppData\\Local\\Programs\\Python\\Python310\\Lib\\site-packages\\pygame\\xxx.dll`应当替换为`xxx.dll`实际所在路径，`--output-dir=`后应当跟实际需要输出的路径，绝对路径或者相对路径均可\n* 由于仅复制了`opus`与`vorbis`的解码器，故要求所有背景音乐都要以opus或vorbis编码\n* `--windows-product-version=`表示版本号信息，所跟内容格式必须为`x.x.x.x`\n* 建议开启`--lto=yes`选项优化链接，如果编译失败可以关闭此选项\n\n可执行文件生成路径为`./release/pypvz.exe`\n\n如果只需要在本地生成编译文件测试，则只需要执行：\n\n``` cmd\nnuitka --mingw64 `\n    --follow-imports `\n    --show-progress `\n    --output-dir=test-build `\n    --windows-icon-from-ico=pypvz.ico `\n    --windows-product-name=pypvz `\n    --windows-company-name=wszqkzqk.dev `\n    --windows-file-description=pypvz `\n    --windows-disable-console `\n    --windows-product-version=0.8.2.0 `\n    pypvz.py\n```\n\n这样生成的程序只能在具有相同python环境的机器上运行\n\n### 使用pyinstaller进行构建\n\n- 由于pyinstaller构建的程序运行效率显著较nuitka构建的程序低下，并且程序体积也往往比nuitka构建的程序大，因此本项目并不推荐使用pyinstaller构建\n- 但是因为pyinstaller直接封装了所导入的库中的全部内容，使用pyinstaller构建时不需要手动添加媒体解码库\n- pyinstaller并没有涉及python源代码优化、C源代码生成以及C源代码编译链接过程，因此编译速度显著快于nuitka\n\n编译依赖：\n- `Python3` （建议 >= 3.10，最好使用最新版）\n- `Python-Pygame` （建议 >= 2.0，最好使用最新版）\n- `Pyinstaller`\n\n编译参考命令：\n``` cmd\npyinstaller -F pypvz.py `\n                  --distpath ./release `\n                  --noconsole `\n                  --add-data=\"resources;./resources\" `\n                  --add-data=\"pypvz-exec-logo.png;./pypvz-exec-logo.png\" `\n                  -i ./pypvz.ico\n```\n\n可执行文件生成路径为`./release/pypvz.exe`\n\n### 使用Github Workflow进行自动构建\n\n直接复制本项目下的`.github/workflows`下的文件，进行少许改动即可满足大多数需求\n\n## 已知bug\n\n以下问题囿于个人目前的能力与精力，没有修复：\n* 冷冻的僵尸未用蓝色滤镜标识\n  * 这个想不到很好的实现方法，可能会想一种替代方案\n* 魅惑的僵尸未用红色滤镜标识\n  * 这个可能会作为一种“特性”\n* 南瓜头显示不正常\n* 墓碑吞噬者吞噬墓碑过程中被吞噬的墓碑顶端不会消失\n\n**欢迎提供[Pull requests](https://github.com/wszqkzqk/pypvz/pulls)或修复方法建议，也欢迎在这里反馈新的bug()**\n\n## ~~画大饼~~计划（不保证实施）\n\n* 增加关卡进程进度条\n  * 该功能自0.5.4已实现\n* 增加保存数据文件以存储用户进度的功能\n  * 该功能自0.8.0.0已实现\n* 增加调整音量的功能\n  * `pg.mixer.music.set_volume()`\n  * 可以用`音量+`、`音量-`按钮实现\n  * 注意字体颜色渲染\n  * 该功能自0.8.14.0已实现\n* 关卡开始前增加预览界面\n* 增加解锁与选关功能\n  * 目前的设想与原版不同，在完成两轮冒险模式（初始冒险模式 + 戴夫选关的冒险模式）后可以自主选关~~（当然现在只是画饼）~~\n* 更改僵尸生成方式\n  * 使僵尸生成更随机化，由JSON记录改为随机数生成\n    * 该功能自0.5.0已经基本实现\n    * 使用原版设定，每面旗帜出10波僵尸，9个小波，1个大波\n    * 采用手机版设定，无尽模式没有红眼计数和变速设定，每波红眼权重为1000，平均分布\n  * 增加僵尸死亡后有概率掉落奖励的机制\n* 增加更多植物、僵尸类型与游戏功能、模式，尽量符合原版基本设计\n* 细分伤害种类\n  * 实体\n    * 一般子弹实体——普通伤害且无特殊效果\n      * 豌豆\n        * 已实现\n      * 孢子\n        * 已实现\n      * 星星\n        * 已实现\n      * 尖刺\n    * 特殊子弹实体——非普通伤害或有特殊效果\n      * 冰豌豆（减速）\n        * 已实现\n      * 火豌豆（2倍伤害、带有1x1溅射）\n        * 已实现\n    * 投掷\n      * 西瓜（4倍伤害，带有3x3溅射）\n      * 冰瓜（4倍伤害，带有3x3溅射伤害与减速）\n      * 玉米粒\n      * 黄油（2倍伤害，定格）\n      * 卷心菜（2倍伤害）\n    * 烟雾\n      * 线形范围烟雾\n        * 自0.7.10.0起已实现\n      * 圆形范围烟雾\n    * 碾压\n      * 倭瓜\n        * 已实现\n  * 爆炸\n    * 一般爆炸\n      * 樱桃炸弹、爆炸坚果与玉米加农炮炮弹\n        * 已实现\n      * 毁灭菇\n        * 自0.7.6.0已实现\n    * 火焰爆炸\n      * 火爆辣椒\n        * 已实现\n    * 非灰烬类爆炸\n      * 土豆雷\n        * 已实现\n  * 从地面刺伤\n    * 已实现\n  * 缠绕与拖拽\n    * 自0.7.5.0已实现\n    * 与原版有所区别，设定上秒杀任意僵尸\n  * 吞噬\n    * 已实现\n    * 与原版有所区别，设定上秒杀任意僵尸\n  * 特殊\n    * 魅惑\n      * 已实现\n    * 移除铁制防具\n    * 全场伤害与冰冻\n      * 已实现\n    * 撞击\n      * 坚果保龄球撞击\n        * 已实现\n      * 巨型坚果保龄球撞击\n    * 吹走\n* 增加部分音效\n  * 如爆炸、打击等\n  * 自0.6.9已部分实现\n* 增加关卡前的本关僵尸预览\n* 鼠标移动到植物上时显示部分信息，类似图鉴功能\n\n## 截屏\n\n![截屏1](/screenshots/screenshot-1.webp)\n![截屏2](/screenshots/screenshot-2.webp)\n![截屏3](/screenshots/screenshot-3.webp)\n![截屏4](/screenshots/screenshot-4.webp)\n![截屏5](/screenshots/screenshot-5.webp)\n![截屏6](/screenshots/screenshot-6.webp)\n![截屏7](/screenshots/screenshot-7.webp)\n![截屏8](/screenshots/screenshot-8.webp)\n![截屏9](/screenshots/screenshot-9.webp)\n![截屏10](/screenshots/screenshot-10.webp)\n![截屏11](/screenshots/screenshot-11.webp)\n![截屏12](/screenshots/screenshot-12.webp)\n![截屏13](/screenshots/screenshot-13.webp)\n![截屏14](/screenshots/screenshot-14.webp)\n![截屏15](/screenshots/screenshot-15.webp)\n![截屏16](/screenshots/screenshot-16.webp)\n![截屏17](/screenshots/screenshot-17.webp)\n![截屏18](/screenshots/screenshot-18.webp)\n![截屏19](/screenshots/screenshot-19.webp)\n![截屏20](/screenshots/screenshot-20.webp)\n![截屏21](/screenshots/screenshot-21.webp)\n![截屏22](/screenshots/screenshot-22.webp)\n![截屏23](/screenshots/screenshot-23.webp)\n\n## 关于日志与反馈\n\n对于闪退情况，Linux用户与Windows下的python源代码运行用户可以直接在终端中复制出崩溃日志进行反馈。\n\nWindows单文件封装版本无法通过终端显示日志，需要在日志文件中寻找崩溃原因\n* Windows默认日志文件路径为`~\\AppData\\Roaming\\pypvz\\run.log`\n* 其他操作系统为`~/.config/pypvz/run.log`，但一般可以在终端中显示时用终端中的输出即可\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"pypvz\"\nversion = \"0.1.0\"\ndescription = \"Add your description here\"\nreadme = \"README.md\"\nrequires-python = \">=3.12\"\ndependencies = [\n    \"blue>=0.9.1\",\n    \"pygame>=2.6.1\",\n    \"setuptools>=80.9.0\",\n    \"wheel>=0.45.1\",\n]\n"
  },
  {
    "path": "pypvz.py",
    "content": "#!/usr/bin/env python\nimport logging\nimport os\nimport traceback\nfrom logging.handlers import RotatingFileHandler\n\nimport pygame as pg\n\n# 由于在后续本地模块中存在对pygame的调用，在此处必须完成pygame的初始化\nos.environ[\n    'SDL_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR'\n] = '0'   # 设置临时环境变量以避免Linux下禁用x11合成器\npg.init()\n\nfrom source import constants as c\nfrom source import tool\nfrom source.state import level, mainmenu, screen\n\nif __name__ == '__main__':\n    # 日志设置\n    if not os.path.exists(os.path.dirname(c.USERLOG_PATH)):\n        os.makedirs(os.path.dirname(c.USERLOG_PATH))\n    logger = logging.getLogger('main')\n    formatter = logging.Formatter('%(asctime)s - %(levelname)s: %(message)s')\n    fileHandler = RotatingFileHandler(\n        c.USERLOG_PATH, 'a', 1_000_000, 0, 'utf-8'\n    )\n    # 设置日志文件权限，Unix为644，Windows为可读写；Python的os.chmod与Unix chmod相同，但要显式说明8进制\n    os.chmod(c.USERLOG_PATH, 0o644)\n    fileHandler.setFormatter(formatter)\n    streamHandler = logging.StreamHandler()\n    streamHandler.setFormatter(formatter)\n    logger.addHandler(fileHandler)\n    logger.addHandler(streamHandler)\n\n    try:\n        # 控制状态机运行\n        game = tool.Control()\n        state_dict = {\n            c.MAIN_MENU: mainmenu.Menu(),\n            c.GAME_VICTORY: screen.GameVictoryScreen(),\n            c.GAME_LOSE: screen.GameLoseScreen(),\n            c.LEVEL: level.Level(),\n            c.AWARD_SCREEN: screen.AwardScreen(),\n            c.HELP_SCREEN: screen.HelpScreen(),\n        }\n        game.setup_states(state_dict, c.MAIN_MENU)\n        game.run()\n    except:\n        print()   # 将日志输出与上文内容分隔开，增加可读性\n        logger.error(f'\\n{traceback.format_exc()}')\n"
  },
  {
    "path": "source/__init__.py",
    "content": ""
  },
  {
    "path": "source/component/__init__.py",
    "content": ""
  },
  {
    "path": "source/component/map.py",
    "content": "import random\n\nfrom .. import constants as c\n\n\n# 记录植物种植情况的地图管理工具\nclass Map:\n    def __init__(self, background_type: int):\n        self.background_type = background_type\n        # 注意：从0开始编号\n        if self.background_type in c.POOL_EQUIPPED_BACKGROUNDS:\n            self.width = c.GRID_POOL_X_LEN\n            self.height = c.GRID_POOL_Y_LEN\n            self.grid_height_size = c.GRID_POOL_Y_SIZE\n            self.map = [\n                [\n                    self.initMapGrid(c.MAP_WATER)\n                    if 2 <= y <= 3\n                    else self.initMapGrid(c.MAP_GRASS)\n                    for x in range(self.width)\n                ]\n                for y in range(self.height)\n            ]\n        elif self.background_type in c.ON_ROOF_BACKGROUNDS:\n            self.width = c.GRID_ROOF_X_LEN\n            self.height = c.GRID_ROOF_Y_LEN\n            self.grid_height_size = c.GRID_ROOF_Y_SIZE\n            self.map = [\n                [self.initMapGrid(c.MAP_TILE) for x in range(self.width)]\n                for y in range(self.height)\n            ]\n        elif self.background_type == c.BACKGROUND_SINGLE:\n            self.width = c.GRID_X_LEN\n            self.height = c.GRID_Y_LEN\n            self.grid_height_size = c.GRID_Y_SIZE\n            self.map = [\n                [\n                    self.initMapGrid(c.MAP_GRASS)\n                    if y == 2\n                    else self.initMapGrid(c.MAP_UNAVAILABLE)\n                    for x in range(self.width)\n                ]\n                for y in range(self.height)\n            ]\n        elif self.background_type == c.BACKGROUND_TRIPLE:\n            self.width = c.GRID_X_LEN\n            self.height = c.GRID_Y_LEN\n            self.grid_height_size = c.GRID_Y_SIZE\n            self.map = [\n                [\n                    self.initMapGrid(c.MAP_GRASS)\n                    if 1 <= y <= 3\n                    else self.initMapGrid(c.MAP_UNAVAILABLE)\n                    for x in range(self.width)\n                ]\n                for y in range(self.height)\n            ]\n        else:\n            self.width = c.GRID_X_LEN\n            self.height = c.GRID_Y_LEN\n            self.grid_height_size = c.GRID_Y_SIZE\n            self.map = [\n                [self.initMapGrid(c.MAP_GRASS) for x in range(self.width)]\n                for y in range(self.height)\n            ]\n\n    def isValid(self, map_x: int, map_y: int) -> bool:\n        if (0 <= map_x < self.width) and (0 <= map_y < self.height):\n            return True\n        return False\n\n    # 地图单元格状态\n    # 注意是可变对象，不能直接引用\n    # 由于同一格显然不可能种两个相同的植物，所以用集合\n    def initMapGrid(self, plot_type: str) -> set:\n        return {\n            c.MAP_PLANT: set(),\n            c.MAP_SLEEP: False,\n            c.MAP_PLOT_TYPE: plot_type,\n        }\n\n    # 判断位置是否可用\n    # 暂时没有写紫卡植物的判断方法\n    # 由于紫卡植物需要移除以前的植物，所以可用另外定义一个函数\n    def isAvailable(self, map_x: int, map_y: int, plant_name: str) -> bool:\n        # 咖啡豆和墓碑吞噬者的判别最为特殊\n        if plant_name == c.COFFEEBEAN:\n            if self.map[map_y][map_x][c.MAP_SLEEP] and (\n                plant_name not in self.map[map_y][map_x][c.MAP_PLANT]\n            ):\n                return True\n            else:\n                return False\n        if plant_name == c.GRAVEBUSTER:\n            if c.GRAVE in self.map[map_y][map_x][c.MAP_PLANT] and (\n                plant_name not in self.map[map_y][map_x][c.MAP_PLANT]\n            ):\n                return True\n            else:\n                return False\n        # 被非植物障碍占据的格子对于一般植物不可种植\n        if any(\n            (i in c.NON_PLANT_OBJECTS)\n            for i in self.map[map_y][map_x][c.MAP_PLANT]\n        ):\n            return False\n        if self.map[map_y][map_x][c.MAP_PLOT_TYPE] == c.MAP_GRASS:  # 草地\n            # 首先需要判断植物是否是水生植物，水生植物不能种植在陆地上\n            if plant_name not in c.WATER_PLANTS:\n                if not self.map[map_y][map_x][c.MAP_PLANT]:   # 没有植物肯定可以种植\n                    return True\n                elif all(\n                    (i in {'花盆（未实现）', c.PUMPKINHEAD})\n                    for i in self.map[map_y][map_x][c.MAP_PLANT]\n                ) and (\n                    plant_name not in self.map[map_y][map_x][c.MAP_PLANT]\n                ):   # 例外植物：集合中填花盆和南瓜头，只要这里没有这种植物就能种植\n                    return True\n                elif (plant_name == c.PUMPKINHEAD) and (\n                    c.PUMPKINHEAD not in self.map[map_y][map_x][c.MAP_PLANT]\n                ):   # 没有南瓜头就能种南瓜头\n                    return True\n                else:\n                    return False\n            else:\n                return False\n        elif self.map[map_y][map_x][c.MAP_PLOT_TYPE] == c.MAP_TILE:   # 屋顶\n            # 首先需要判断植物是否是水生植物，水生植物不能种植在陆地上\n            if plant_name not in c.WATER_PLANTS:\n                if '花盆（未实现）' in self.map[map_y][map_x][c.MAP_PLANT]:\n                    if all(\n                        (i in {'花盆（未实现）', c.PUMPKINHEAD})\n                        for i in self.map[map_y][map_x][c.MAP_PLANT]\n                    ) and (\n                        plant_name not in self.map[map_y][map_x][c.MAP_PLANT]\n                    ):   # 例外植物：集合中填花盆和南瓜头，只要这里没有这种植物就能种植\n                        if plant_name in {c.SPIKEWEED}:   # 不能在花盆上种植的植物\n                            return False\n                        else:\n                            return True\n                    elif (plant_name == c.PUMPKINHEAD) and (\n                        c.PUMPKINHEAD\n                        not in self.map[map_y][map_x][c.MAP_PLANT]\n                    ):    # 有花盆且没有南瓜头就能种南瓜头\n                        return True\n                    else:\n                        return False\n                elif plant_name == '花盆（未实现）':   # 这一格本来没有花盆而且新来的植物是花盆，可以种\n                    return True\n                else:\n                    return False\n            else:\n                return False\n        elif self.map[map_y][map_x][c.MAP_PLOT_TYPE] == c.MAP_WATER:   # 水里\n            if plant_name in c.WATER_PLANTS:   # 是水生植物\n                if not self.map[map_y][map_x][\n                    c.MAP_PLANT\n                ]:   # 只有无植物时才能在水里种植水生植物\n                    return True\n                else:\n                    return False\n            else:   # 非水生植物，依赖睡莲\n                if c.LILYPAD in self.map[map_y][map_x][c.MAP_PLANT]:\n                    if all(\n                        (i in {c.LILYPAD, c.PUMPKINHEAD})\n                        for i in self.map[map_y][map_x][c.MAP_PLANT]\n                    ) and (\n                        plant_name not in self.map[map_y][map_x][c.MAP_PLANT]\n                    ):\n                        if plant_name in {\n                            c.SPIKEWEED,\n                            c.POTATOMINE,\n                            '花盆（未实现）',\n                        }:   # 不能在睡莲上种植的植物\n                            return False\n                        else:\n                            return True\n                    elif (plant_name == c.PUMPKINHEAD) and (\n                        c.PUMPKINHEAD\n                        not in self.map[map_y][map_x][c.MAP_PLANT]\n                    ):   # 在睡莲上且没有南瓜头就能种南瓜头\n                        return True\n                    else:\n                        return False\n                else:\n                    return False\n        else:   # 不可种植区域\n            return False\n\n    def getMapIndex(self, x: int, y: int) -> tuple[int, int]:\n        if self.background_type in c.POOL_EQUIPPED_BACKGROUNDS:\n            x -= c.MAP_POOL_OFFSET_X\n            y -= c.MAP_POOL_OFFSET_Y\n            return (x // c.GRID_POOL_X_SIZE, y // c.GRID_POOL_Y_SIZE)\n        elif self.background_type in c.ON_ROOF_BACKGROUNDS:\n            x -= c.MAP_ROOF_OFFSET_X\n            y -= c.MAP_ROOF_OFFSET_X\n            grid_x = x // c.GRID_ROOF_X_SIZE\n            if grid_x >= 5:\n                grid_y = y // c.GRID_ROOF_Y_SIZE\n            else:\n                grid_y = (y - 20 * (6 - grid_x)) // 85\n            return (grid_x, grid_y)\n        else:\n            x -= c.MAP_OFFSET_X\n            y -= c.MAP_OFFSET_Y\n            return (x // c.GRID_X_SIZE, y // c.GRID_Y_SIZE)\n\n    def getMapGridPos(self, map_x: int, map_y: int) -> tuple[int, int]:\n        if self.background_type in c.POOL_EQUIPPED_BACKGROUNDS:\n            return (\n                map_x * c.GRID_POOL_X_SIZE\n                + c.GRID_POOL_X_SIZE // 2\n                + c.MAP_POOL_OFFSET_X,\n                map_y * c.GRID_POOL_Y_SIZE\n                + c.GRID_POOL_Y_SIZE // 5 * 3\n                + c.MAP_POOL_OFFSET_Y,\n            )\n        elif self.background_type in c.ON_ROOF_BACKGROUNDS:\n            return (\n                map_x * c.GRID_ROOF_X_SIZE\n                + c.GRID_ROOF_X_SIZE // 2\n                + c.MAP_ROOF_OFFSET_X,\n                map_y * c.GRID_ROOF_Y_SIZE\n                + 20 * max(0, (6 - map_y))\n                + c.GRID_ROOF_Y_SIZE // 5 * 3\n                + c.MAP_POOL_OFFSET_Y,\n            )\n        else:\n            return (\n                map_x * c.GRID_X_SIZE + c.GRID_X_SIZE // 2 + c.MAP_OFFSET_X,\n                map_y * c.GRID_Y_SIZE\n                + c.GRID_Y_SIZE // 5 * 3\n                + c.MAP_OFFSET_Y,\n            )\n\n    def setMapGridType(self, map_x: int, map_y: int, plot_type: str):\n        self.map[map_y][map_x][c.MAP_PLOT_TYPE] = plot_type\n\n    def addMapPlant(\n        self, map_x: int, map_y: int, plant_name: int, sleep: bool = False\n    ):\n        self.map[map_y][map_x][c.MAP_PLANT].add(plant_name)\n        self.map[map_y][map_x][c.MAP_SLEEP] = sleep\n\n    def removeMapPlant(self, map_x: int, map_y: int, plant_name: str):\n        self.map[map_y][map_x][c.MAP_PLANT].discard(plant_name)\n\n    def getRandomMapIndex(self) -> tuple[int, int]:\n        map_x = random.randint(0, self.width - 1)\n        map_y = random.randint(0, self.height - 1)\n        return (map_x, map_y)\n\n    def checkPlantToSeed(\n        self, x: int, y: int, plant_name: str\n    ) -> tuple[int, int]:\n        pos = None\n        map_x, map_y = self.getMapIndex(x, y)\n        if self.isValid(map_x, map_y) and self.isAvailable(\n            map_x, map_y, plant_name\n        ):\n            pos = self.getMapGridPos(map_x, map_y)\n        return pos\n\n\n# 保存具体关卡地图信息常数\n# 冒险模式地图\nLEVEL_MAP_DATA = (\n    # 第0关：测试模式地图\n    {\n        c.BACKGROUND_TYPE: 2,\n        c.GAME_TITLE: '隐藏测试关卡',\n        c.INIT_SUN_NAME: 5000,\n        c.SHOVEL: 1,\n        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_LIST,\n        c.ZOMBIE_LIST: (\n            {'time': 0, 'map_y': 5, 'name': 'Zomboni'},\n            {'time': 1000, 'map_y': 4, 'name': 'ScreenDoorZombie'},\n            {'time': 2000, 'map_y': 4, 'name': 'ScreenDoorZombie'},\n            {'time': 3100, 'map_y': 4, 'name': 'ScreenDoorZombie'},\n            {'time': 4500, 'map_y': 4, 'name': 'ScreenDoorZombie'},\n            {'time': 5000, 'map_y': 4, 'name': 'ScreenDoorZombie'},\n            {'time': 6000, 'map_y': 4, 'name': 'ScreenDoorZombie'},\n            {'time': 7000, 'map_y': 4, 'name': 'ScreenDoorZombie'},\n            {'time': 8000, 'map_y': 4, 'name': 'ScreenDoorZombie'},\n            {'time': 0, 'map_y': 1, 'name': 'NewspaperZombie'},\n            {'time': 0, 'map_y': 0, 'name': 'PoleVaultingZombie'},\n            {'time': 6000, 'map_y': 0, 'name': 'FootballZombie'},\n            {'time': 0, 'map_y': 3, 'name': 'ConeheadDuckyTubeZombie'},\n            {'time': 0, 'map_y': 2, 'name': 'SnorkelZombie'},\n            {'time': 90000, 'map_y': 2, 'name': 'ConeheadDuckyTubeZombie'},\n        ),\n    },\n    # 第1关：单行草皮\n    {\n        c.BACKGROUND_TYPE: 7,\n        c.GAME_TITLE: '白天 1-1',\n        c.INIT_SUN_NAME: 150,\n        c.SHOVEL: 1,\n        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,\n        c.INCLUDED_ZOMBIES: (c.NORMAL_ZOMBIE,),\n        c.NUM_FLAGS: 1,\n    },\n    # 第2关：三行草皮\n    {\n        c.BACKGROUND_TYPE: 8,\n        c.GAME_TITLE: '白天 1-2',\n        c.INIT_SUN_NAME: 50,\n        c.SHOVEL: 1,\n        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,\n        c.INCLUDED_ZOMBIES: (c.NORMAL_ZOMBIE,),\n        c.NUM_FLAGS: 1,\n    },\n    # 第3关\n    {\n        c.BACKGROUND_TYPE: 0,\n        c.GAME_TITLE: '白天 1-3',\n        c.INIT_SUN_NAME: 50,\n        c.SHOVEL: 1,\n        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,\n        c.INCLUDED_ZOMBIES: (c.NORMAL_ZOMBIE,),\n        c.NUM_FLAGS: 2,\n    },\n    # 第4关\n    {\n        c.BACKGROUND_TYPE: 0,\n        c.GAME_TITLE: '白天 1-4',\n        c.INIT_SUN_NAME: 50,\n        c.SHOVEL: 1,\n        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,\n        c.INCLUDED_ZOMBIES: (\n            c.NORMAL_ZOMBIE,\n            c.CONEHEAD_ZOMBIE,\n            c.POLE_VAULTING_ZOMBIE,\n        ),\n        c.NUM_FLAGS: 2,\n    },\n    # 第5关 目前白天最后一关\n    {\n        c.BACKGROUND_TYPE: 0,\n        c.GAME_TITLE: '白天 1-5',\n        c.INIT_SUN_NAME: 50,\n        c.SHOVEL: 1,\n        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,\n        c.INCLUDED_ZOMBIES: (\n            c.NORMAL_ZOMBIE,\n            c.CONEHEAD_ZOMBIE,\n            c.POLE_VAULTING_ZOMBIE,\n            c.BUCKETHEAD_ZOMBIE,\n        ),\n        c.NUM_FLAGS: 3,\n    },\n    # 第6关 目前夜晚第一关\n    {\n        c.BACKGROUND_TYPE: 1,\n        c.GAME_TITLE: '黑夜 2-1',\n        c.INIT_SUN_NAME: 50,\n        c.SHOVEL: 1,\n        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,\n        c.INCLUDED_ZOMBIES: (c.NORMAL_ZOMBIE, c.NEWSPAPER_ZOMBIE),\n        c.NUM_FLAGS: 2,\n    },\n    # 第7关\n    {\n        c.BACKGROUND_TYPE: 1,\n        c.GAME_TITLE: '黑夜 2-2',\n        c.INIT_SUN_NAME: 50,\n        c.SHOVEL: 1,\n        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,\n        c.INCLUDED_ZOMBIES: (\n            c.NORMAL_ZOMBIE,\n            c.SCREEN_DOOR_ZOMBIE,\n        ),\n        c.NUM_FLAGS: 2,\n        c.GRADE_GRAVES: 2,\n    },\n    # 第8关 目前为夜晚最后一关\n    {\n        c.BACKGROUND_TYPE: 1,\n        c.GAME_TITLE: '黑夜 2-3',\n        c.INIT_SUN_NAME: 50,\n        c.SHOVEL: 1,\n        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,\n        c.INCLUDED_ZOMBIES: (\n            c.NORMAL_ZOMBIE,\n            c.NEWSPAPER_ZOMBIE,\n            c.CONEHEAD_ZOMBIE,\n            c.BUCKETHEAD_ZOMBIE,\n            c.SCREEN_DOOR_ZOMBIE,\n            c.FOOTBALL_ZOMBIE,\n        ),\n        c.INEVITABLE_ZOMBIE_DICT: {  # 这里改用python实现了以后，键不再用字符串，改用数字\n            # 仍然要注意字典值是元组\n            10: (c.NEWSPAPER_ZOMBIE,),\n            20: (c.SCREEN_DOOR_ZOMBIE,),\n            30: (c.FOOTBALL_ZOMBIE,),\n        },\n        c.NUM_FLAGS: 3,\n        c.GRADE_GRAVES: 3,\n    },\n    # 第9关 目前为泳池模式第一关\n    {\n        c.BACKGROUND_TYPE: 2,\n        c.GAME_TITLE: '泳池 3-1',\n        c.INIT_SUN_NAME: 50,\n        c.SHOVEL: 1,\n        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,\n        c.INCLUDED_ZOMBIES: (\n            c.NORMAL_ZOMBIE,\n            c.BUCKETHEAD_ZOMBIE,\n            c.CONEHEAD_ZOMBIE,\n        ),\n        c.NUM_FLAGS: 2,\n    },\n    # 第10关\n    {\n        c.BACKGROUND_TYPE: 2,\n        c.GAME_TITLE: '泳池 3-2',\n        c.INIT_SUN_NAME: 50,\n        c.SHOVEL: 1,\n        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,\n        c.INCLUDED_ZOMBIES: (\n            c.NORMAL_ZOMBIE,\n            c.BUCKETHEAD_ZOMBIE,\n            c.CONEHEAD_ZOMBIE,\n            c.SNORKELZOMBIE,\n        ),\n        c.INEVITABLE_ZOMBIE_DICT: {30: (c.SNORKELZOMBIE,)},\n        c.NUM_FLAGS: 3,\n    },\n    # 第11关\n    {\n        c.BACKGROUND_TYPE: 2,\n        c.GAME_TITLE: '泳池 3-3',\n        c.INIT_SUN_NAME: 50,\n        c.SHOVEL: 1,\n        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,\n        c.INCLUDED_ZOMBIES: (c.NORMAL_ZOMBIE, c.ZOMBONI),\n        c.INEVITABLE_ZOMBIE_DICT: {30: (c.ZOMBONI,)},\n        c.NUM_FLAGS: 3,\n    },\n    # 第12关 目前为泳池最后一关\n    {\n        c.BACKGROUND_TYPE: 2,\n        c.GAME_TITLE: '泳池 3-4',\n        c.INIT_SUN_NAME: 50,\n        c.SHOVEL: 1,\n        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,\n        c.INCLUDED_ZOMBIES: (\n            c.NORMAL_ZOMBIE,\n            c.ZOMBONI,\n            c.BUCKETHEAD_ZOMBIE,\n            c.CONEHEAD_ZOMBIE,\n            c.SNORKELZOMBIE,\n        ),\n        c.INEVITABLE_ZOMBIE_DICT: {40: (c.ZOMBONI,)},\n        c.NUM_FLAGS: 4,\n    },\n    # 第13关 目前为浓雾第一关 尚未完善\n    {\n        c.BACKGROUND_TYPE: 3,\n        c.GAME_TITLE: '浓雾 4-1',\n        c.INIT_SUN_NAME: 50,\n        c.SHOVEL: 1,\n        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,\n        c.INCLUDED_ZOMBIES: (\n            c.NORMAL_ZOMBIE,\n            c.NEWSPAPER_ZOMBIE,\n            c.ZOMBONI,\n            c.FOOTBALL_ZOMBIE,\n            c.CONEHEAD_ZOMBIE,\n            c.BUCKETHEAD_ZOMBIE,\n        ),\n        c.NUM_FLAGS: 4,\n    },\n)\n\n\n# 玩玩小游戏地图\nLITTLE_GAME_MAP_DATA = (\n    # 第0关 测试\n    {\n        c.BACKGROUND_TYPE: 3,\n        c.GAME_TITLE: '隐藏测试关卡',\n        c.CHOOSEBAR_TYPE: c.CHOOSEBAR_MOVE,\n        c.SHOVEL: 1,\n        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,\n        c.INCLUDED_ZOMBIES: (\n            c.NORMAL_ZOMBIE,\n            c.NEWSPAPER_ZOMBIE,\n            c.ZOMBONI,\n            c.FOOTBALL_ZOMBIE,\n            c.CONEHEAD_ZOMBIE,\n            c.BUCKETHEAD_ZOMBIE,\n        ),\n        c.NUM_FLAGS: 4,\n        c.CARD_POOL: {\n            c.LILYPAD: 300,\n            c.STARFRUIT: 400,\n            c.PUMPKINHEAD: 100,\n            c.SEASHROOM: 100,\n            c.SPIKEWEED: 100,\n        },\n    },\n    # 第1关 坚果保龄球\n    {\n        c.BACKGROUND_TYPE: 6,\n        c.GAME_TITLE: '坚果保龄球',\n        c.CHOOSEBAR_TYPE: c.CHOOSEBAR_BOWLING,\n        c.SHOVEL: 0,\n        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,\n        c.INCLUDED_ZOMBIES: (\n            c.NORMAL_ZOMBIE,\n            c.CONEHEAD_ZOMBIE,\n            c.POLE_VAULTING_ZOMBIE,\n            c.BUCKETHEAD_ZOMBIE,\n        ),\n        c.NUM_FLAGS: 2,\n        c.CARD_POOL: {\n            c.WALLNUTBOWLING: 300,\n            c.REDWALLNUTBOWLING: 100,\n        },\n    },\n    # 第2关 白天 大决战\n    {\n        c.BACKGROUND_TYPE: 0,\n        c.GAME_TITLE: '大决战（白天）',\n        c.CHOOSEBAR_TYPE: c.CHOOSEBAR_MOVE,\n        c.SHOVEL: 1,\n        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,\n        c.INCLUDED_ZOMBIES: (\n            c.NORMAL_ZOMBIE,\n            c.CONEHEAD_ZOMBIE,\n            c.POLE_VAULTING_ZOMBIE,\n            c.BUCKETHEAD_ZOMBIE,\n        ),\n        c.NUM_FLAGS: 3,\n        c.CARD_POOL: {\n            c.PEASHOOTER: 200,\n            c.SNOWPEASHOOTER: 100,\n            c.WALLNUT: 100,\n            c.CHERRYBOMB: 100,\n            c.REPEATERPEA: 200,\n            c.CHOMPER: 100,\n            c.POTATOMINE: 100,\n        },\n    },\n    # 第3关 夜晚 大决战\n    {\n        c.BACKGROUND_TYPE: 1,\n        c.GAME_TITLE: '大决战（黑夜）',\n        c.CHOOSEBAR_TYPE: c.CHOOSEBAR_MOVE,\n        c.SHOVEL: 1,\n        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,\n        c.INCLUDED_ZOMBIES: (\n            c.NORMAL_ZOMBIE,\n            c.CONEHEAD_ZOMBIE,\n            c.FOOTBALL_ZOMBIE,\n            c.BUCKETHEAD_ZOMBIE,\n            c.NEWSPAPER_ZOMBIE,\n            c.SCREEN_DOOR_ZOMBIE,\n        ),\n        c.NUM_FLAGS: 3,\n        c.CARD_POOL: {\n            c.PUFFSHROOM: 100,\n            c.SCAREDYSHROOM: 100,\n            c.ICESHROOM: 70,\n            c.HYPNOSHROOM: 100,\n            c.DOOMSHROOM: 50,\n            c.GRAVEBUSTER: 100,\n            c.FUMESHROOM: 200,\n        },\n        c.GRADE_GRAVES: 3,\n    },\n    # 第4关 泳池 大决战\n    {\n        c.BACKGROUND_TYPE: 2,\n        c.GAME_TITLE: '大决战（泳池）',\n        c.CHOOSEBAR_TYPE: c.CHOOSEBAR_MOVE,\n        c.SHOVEL: 1,\n        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,\n        c.INCLUDED_ZOMBIES: (\n            c.NORMAL_ZOMBIE,\n            c.CONEHEAD_ZOMBIE,\n            c.SNORKELZOMBIE,\n            c.BUCKETHEAD_ZOMBIE,\n            c.ZOMBONI,\n        ),\n        c.NUM_FLAGS: 4,\n        c.CARD_POOL: {\n            c.LILYPAD: 300,\n            c.TORCHWOOD: 100,\n            c.TALLNUT: 100,\n            c.TANGLEKLEP: 100,\n            c.SPIKEWEED: 100,\n            c.SQUASH: 100,\n            c.JALAPENO: 50,\n            c.THREEPEASHOOTER: 400,\n        },\n    },\n    # 第5关 坚果保龄球2\n    {\n        c.BACKGROUND_TYPE: 6,\n        c.GAME_TITLE: '坚果保龄球(II)',\n        c.CHOOSEBAR_TYPE: c.CHOOSEBAR_BOWLING,\n        c.SHOVEL: 0,\n        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,\n        c.INCLUDED_ZOMBIES: (\n            c.NORMAL_ZOMBIE,\n            c.CONEHEAD_ZOMBIE,\n            c.POLE_VAULTING_ZOMBIE,\n            c.BUCKETHEAD_ZOMBIE,\n            c.NEWSPAPER_ZOMBIE,\n            c.SCREEN_DOOR_ZOMBIE,\n        ),\n        c.NUM_FLAGS: 3,\n        c.CARD_POOL: {\n            c.WALLNUTBOWLING: 500,\n            c.REDWALLNUTBOWLING: 100,\n            c.GIANTWALLNUT: 100,\n        },\n    },\n)\n\n# 总关卡数\nTOTAL_LEVEL = len(LEVEL_MAP_DATA)\nTOTAL_LITTLE_GAME = len(LITTLE_GAME_MAP_DATA)\n"
  },
  {
    "path": "source/component/menubar.py",
    "content": "import random\n\nimport pygame as pg\n\nfrom .. import constants as c\nfrom .. import tool\n\n\ndef getSunValueImage(sun_value):\n    # for pack, must include ttf\n    font = pg.font.Font(c.FONT_PATH, 14)\n    font.bold = True\n    width = 35\n    msg_image = font.render(str(sun_value), True, c.NAVYBLUE, c.LIGHTYELLOW)\n    msg_rect = msg_image.get_rect()\n    msg_w = msg_rect.width\n\n    image = pg.Surface((width, 17))\n    x = width - msg_w\n\n    image.fill(c.LIGHTYELLOW)\n    image.blit(msg_image, (x, 0), (0, 0, msg_rect.w, msg_rect.h))\n    image.set_colorkey(c.BLACK)\n    return image\n\n\ndef getCardPool(data):\n    card_pool = {\n        c.PLANT_CARD_INFO[c.PLANT_CARD_INDEX[card_name]]: data[card_name]\n        for card_name in data\n    }\n    return card_pool\n\n\nclass Card:\n    def __init__(\n        self, x: int, y: int, index: int, scale: float = 0.5, not_recommend=0\n    ):\n        self.info = c.PLANT_CARD_INFO[index]\n        self.loadFrame(self.info[c.CARD_INDEX], scale)\n        self.rect = self.orig_image.get_rect()\n        self.rect.x = x\n        self.rect.y = y\n        # 绘制植物阳光消耗大小\n        font = pg.font.Font(c.FONT_PATH, 12)\n        self.sun_cost_img = font.render(\n            str(self.info[c.SUN_INDEX]), True, c.BLACK\n        )\n        self.sun_cost_img_rect = self.sun_cost_img.get_rect()\n        sun_cost_img_x = 32 - self.sun_cost_img_rect.w\n        self.orig_image.blit(\n            self.sun_cost_img,\n            (\n                sun_cost_img_x,\n                52,\n                self.sun_cost_img_rect.w,\n                self.sun_cost_img_rect.h,\n            ),\n        )\n\n        self.index = index\n        self.sun_cost = self.info[c.SUN_INDEX]\n        self.frozen_time = self.info[c.FROZEN_TIME_INDEX]\n        self.frozen_timer = -self.frozen_time\n        self.refresh_timer = 0\n        self.select = True\n        self.clicked = False\n        self.not_recommend = not_recommend\n        if self.not_recommend:\n            self.orig_image.set_alpha(128)\n            self.image = pg.Surface((self.rect.w, self.rect.h))  # 黑底\n            self.image.blit(\n                self.orig_image, (0, 0), (0, 0, self.rect.w, self.rect.h)\n            )\n        else:\n            self.image = self.orig_image\n            self.image.set_alpha(255)\n\n    def loadFrame(self, name, scale):\n        frame = tool.GFX[name]\n        rect = frame.get_rect()\n        width, height = rect.w, rect.h\n\n        self.orig_image = tool.get_image(\n            frame, 0, 0, width, height, c.BLACK, scale\n        )\n        self.image = self.orig_image\n\n    def checkMouseClick(self, mouse_pos):\n        x, y = mouse_pos\n        if (\n            self.rect.x <= x <= self.rect.right\n            and self.rect.y <= y <= self.rect.bottom\n        ):\n            return True\n        return False\n\n    def canClick(self, sun_value, current_time):\n        if (\n            self.sun_cost <= sun_value\n            and (current_time - self.frozen_timer) > self.frozen_time\n        ):\n            return True\n        return False\n\n    def canSelect(self):\n        return self.select\n\n    def setSelect(self, can_select):\n        self.select = can_select\n        if can_select:\n            if self.not_recommend % 2:\n                self.orig_image.set_alpha(128)\n                self.image = pg.Surface((self.rect.w, self.rect.h))  # 黑底\n                self.image.blit(\n                    self.orig_image, (0, 0), (0, 0, self.rect.w, self.rect.h)\n                )\n            else:\n                self.image = self.orig_image\n                self.image.set_alpha(255)\n        else:\n            self.orig_image.set_alpha(64)\n            self.image = pg.Surface((self.rect.w, self.rect.h))  # 黑底\n            self.image.blit(\n                self.orig_image, (0, 0), (0, 0, self.rect.w, self.rect.h)\n            )\n\n    def setFrozenTime(self, current_time):\n        self.frozen_timer = current_time\n\n    def createShowImage(self, sun_value, current_time):\n        # 有关是否满足冷却与阳光条件的图片形式\n        time = current_time - self.frozen_timer\n        if time < self.frozen_time:   # cool down status\n            image = pg.Surface((self.rect.w, self.rect.h))  # 黑底\n            frozen_image = self.orig_image\n            frozen_image.set_alpha(128)\n            frozen_height = (\n                (self.frozen_time - time) / self.frozen_time\n            ) * self.rect.h\n\n            image.blit(\n                frozen_image, (0, 0), (0, 0, self.rect.w, frozen_height)\n            )\n            self.orig_image.set_alpha(192)\n            image.blit(\n                self.orig_image,\n                (0, frozen_height),\n                (0, frozen_height, self.rect.w, self.rect.h - frozen_height),\n            )\n        elif self.sun_cost > sun_value:   # disable status\n            image = pg.Surface((self.rect.w, self.rect.h))  # 黑底\n            self.orig_image.set_alpha(192)\n            image.blit(\n                self.orig_image, (0, 0), (0, 0, self.rect.w, self.rect.h)\n            )\n        elif self.clicked:\n            image = pg.Surface((self.rect.w, self.rect.h))  # 黑底\n            chosen_image = self.orig_image\n            chosen_image.set_alpha(128)\n\n            image.blit(chosen_image, (0, 0), (0, 0, self.rect.w, self.rect.h))\n        else:\n            image = self.orig_image\n            image.set_alpha(255)\n        return image\n\n    def update(self, sun_value, current_time):\n        if (current_time - self.refresh_timer) >= 250:\n            self.image = self.createShowImage(sun_value, current_time)\n            self.refresh_timer = current_time\n\n    def draw(self, surface):\n        surface.blit(self.image, self.rect)\n\n\n# 植物栏\nclass MenuBar:\n    def __init__(self, card_list, sun_value):\n        self.loadFrame(c.MENUBAR_BACKGROUND)\n        self.rect = self.image.get_rect()\n        self.rect.x = 0\n        self.rect.y = 0\n\n        self.sun_value = sun_value\n        self.card_offset_x = 26\n        self.setupCards(card_list)\n\n    def loadFrame(self, name):\n        frame = tool.GFX[name]\n        rect = frame.get_rect()\n        frame_rect = (rect.x, rect.y, rect.w, rect.h)\n\n        self.image = tool.get_image(tool.GFX[name], *frame_rect, c.WHITE, 1)\n\n    def update(self, current_time):\n        self.current_time = current_time\n        for card in self.card_list:\n            card.update(self.sun_value, self.current_time)\n\n    def createImage(self, x, y, num):\n        if num == 1:\n            return\n        img = self.image\n        rect = self.image.get_rect()\n        width = rect.w\n        height = rect.h\n        self.image = pg.Surface((width * num, height)).convert()\n        self.rect = self.image.get_rect()\n        self.rect.x = x\n        self.rect.y = y\n        for i in range(num):\n            x = i * width\n            self.image.blit(img, (x, 0))\n        self.image.set_colorkey(c.BLACK)\n\n    def setupCards(self, card_list):\n        self.card_list = []\n        x = self.card_offset_x\n        y = 8\n        for index in card_list:\n            x += c.BAR_CARD_X_INTERNAL\n            self.card_list.append(Card(x, y, index))\n\n    def checkCardClick(self, mouse_pos):\n        result = None\n        for card in self.card_list:\n            if card.checkMouseClick(mouse_pos):\n                if card.canClick(self.sun_value, self.current_time):\n                    result = (\n                        c.PLANT_CARD_INFO[card.index][c.PLANT_NAME_INDEX],\n                        card,\n                    )\n                else:\n                    # 播放无法使用该卡片的警告音\n                    c.SOUND_CANNOT_CHOOSE_WARNING.play()\n                break\n        return result\n\n    def checkMenuBarClick(self, mouse_pos):\n        x, y = mouse_pos\n        if (\n            self.rect.x <= x <= self.rect.right\n            and self.rect.y <= y <= self.rect.bottom\n        ):\n            return True\n        return False\n\n    def decreaseSunValue(self, value):\n        self.sun_value -= value\n\n    def increaseSunValue(self, value):\n        self.sun_value += value\n        if self.sun_value > 9990:\n            self.sun_value = 9990\n\n    def setCardFrozenTime(self, plant_name):\n        for card in self.card_list:\n            if c.PLANT_CARD_INFO[card.index][c.PLANT_NAME_INDEX] == plant_name:\n                card.setFrozenTime(self.current_time)\n                break\n\n    def drawSunValue(self):\n        self.value_image = getSunValueImage(self.sun_value)\n        self.value_rect = self.value_image.get_rect()\n        self.value_rect.x = 21\n        self.value_rect.y = self.rect.bottom - 24\n\n        self.image.blit(self.value_image, self.value_rect)\n\n    def draw(self, surface):\n        self.drawSunValue()\n        surface.blit(self.image, self.rect)\n        for card in self.card_list:\n            card.draw(surface)\n\n\n# 关卡模式选植物的界面\nclass Panel:\n    def __init__(self, card_list, sun_value, background_type=c.BACKGROUND_DAY):\n        self.loadImages(sun_value)\n        self.selected_cards = []\n        self.selected_num = 0\n        self.background_type = background_type\n        self.setupCards(card_list)\n\n    def loadFrame(self, name):\n        frame = tool.GFX[name]\n        rect = frame.get_rect()\n        frame_rect = (rect.x, rect.y, rect.w, rect.h)\n\n        return tool.get_image(tool.GFX[name], *frame_rect, c.WHITE, 1)\n\n    def loadImages(self, sun_value):\n        self.menu_image = self.loadFrame(c.MENUBAR_BACKGROUND)\n        self.menu_rect = self.menu_image.get_rect()\n        self.menu_rect.x = 0\n        self.menu_rect.y = 0\n\n        self.panel_image = self.loadFrame(c.PANEL_BACKGROUND)\n        self.panel_rect = self.panel_image.get_rect()\n        self.panel_rect.x = 0\n        self.panel_rect.y = c.PANEL_Y_START\n\n        self.value_image = getSunValueImage(sun_value)\n        self.value_rect = self.value_image.get_rect()\n        self.value_rect.x = 21\n        self.value_rect.y = self.menu_rect.bottom - 24\n\n        self.button_image = self.loadFrame(c.START_BUTTON)\n        self.button_rect = self.button_image.get_rect()\n        self.button_rect.x = 155\n        self.button_rect.y = 547\n\n    def setupCards(self, card_list):\n        self.card_list = []\n        x = c.PANEL_X_START - c.PANEL_X_INTERNAL\n        y = c.PANEL_Y_START + 38 - c.PANEL_Y_INTERNAL\n        for i, index in enumerate(card_list):\n            if i % 8 == 0:\n                x = c.PANEL_X_START - c.PANEL_X_INTERNAL\n                y += c.PANEL_Y_INTERNAL\n            x += c.PANEL_X_INTERNAL\n            plant_name = c.PLANT_CARD_INFO[index][c.PLANT_NAME_INDEX]\n            if (\n                plant_name in c.WATER_PLANTS\n                and self.background_type not in c.POOL_EQUIPPED_BACKGROUNDS\n            ):\n                not_recommend = c.REASON_OTHER\n            elif (\n                plant_name == c.GRAVEBUSTER\n                and self.background_type != c.BACKGROUND_NIGHT\n            ):\n                not_recommend = c.REASON_OTHER\n            elif (\n                plant_name in c.CAN_SLEEP_PLANTS\n                and self.background_type in c.DAYTIME_BACKGROUNDS\n            ):\n                not_recommend = c.REASON_WILL_SLEEP\n            elif (\n                plant_name == c.COFFEEBEAN\n                and self.background_type not in c.DAYTIME_BACKGROUNDS\n            ):\n                not_recommend = c.REASON_OTHER\n            # 还有屋顶场景，以及其他植物没有实现的植物没有写进来\n            else:\n                not_recommend = 0\n            self.card_list.append(Card(x, y, index, 0.5, not_recommend))\n\n    def checkCardClick(self, mouse_pos):\n        delete_card = None\n        for card in self.selected_cards:\n            if delete_card:   # when delete a card, move right cards to left\n                card.rect.x -= c.BAR_CARD_X_INTERNAL\n            elif card.checkMouseClick(mouse_pos):\n                self.deleteCard(card.index)\n                delete_card = card\n\n        if delete_card:\n            self.selected_cards.remove(delete_card)\n            self.selected_num -= 1\n            # 播放点击音效\n            c.SOUND_TAPPING_CARD.play()\n            if delete_card.info[c.PLANT_NAME_INDEX] == c.COFFEEBEAN:\n                for i in self.card_list:\n                    if i.not_recommend == c.REASON_SLEEP_BUT_COFFEE_BEAN:\n                        i.not_recommend = c.REASON_WILL_SLEEP\n                        i.orig_image.set_alpha(128)\n                        i.image = pg.Surface((i.rect.w, i.rect.h))  # 黑底\n                        i.image.blit(\n                            i.orig_image, (0, 0), (0, 0, i.rect.w, i.rect.h)\n                        )\n\n        if self.selected_num >= c.CARD_MAX_NUM:\n            return\n\n        for card in self.card_list:\n            if card.checkMouseClick(mouse_pos):\n                if card.canSelect():\n                    self.addCard(card)\n                    # 播放点击音效\n                    c.SOUND_TAPPING_CARD.play()\n                    if card.info[c.PLANT_NAME_INDEX] == c.COFFEEBEAN:\n                        for i in self.card_list:\n                            if i.not_recommend == c.REASON_WILL_SLEEP:\n                                i.not_recommend = (\n                                    c.REASON_SLEEP_BUT_COFFEE_BEAN\n                                )\n                                i.image = i.orig_image\n                                i.image.set_alpha(255)\n                break\n\n    def addCard(self, card: Card):\n        card.setSelect(False)\n        y = 8\n        x = 77 + self.selected_num * c.BAR_CARD_X_INTERNAL\n        self.selected_cards.append(Card(x, y, card.index))\n        self.selected_num += 1\n\n    def deleteCard(self, index):\n        self.card_list[index].setSelect(True)\n\n    def checkStartButtonClick(self, mouse_pos):\n        if self.selected_num < c.CARD_LIST_NUM:\n            return False\n\n        x, y = mouse_pos\n        if (\n            self.button_rect.x <= x <= self.button_rect.right\n            and self.button_rect.y <= y <= self.button_rect.bottom\n        ):\n            return True\n        return False\n\n    def getSelectedCards(self):\n        card_index_list = []\n        for card in self.selected_cards:\n            card_index_list.append(card.index)\n        return card_index_list\n\n    def draw(self, surface):\n        self.menu_image.blit(self.value_image, self.value_rect)\n        surface.blit(self.menu_image, self.menu_rect)\n        surface.blit(self.panel_image, self.panel_rect)\n        for card in self.card_list:\n            card.draw(surface)\n        for card in self.selected_cards:\n            card.draw(surface)\n\n        if self.selected_num >= c.CARD_LIST_NUM:\n            surface.blit(self.button_image, self.button_rect)\n\n\n# 传送带模式的卡片\nclass MoveCard:\n    def __init__(self, x, y, card_name, plant_name, scale=0.5):\n        self.loadFrame(card_name, scale)\n        self.rect = self.orig_image.get_rect()\n        self.rect.x = x\n        self.rect.y = y\n        self.rect.w = 1\n        self.clicked = False\n        self.image = self.createShowImage()\n\n        self.card_name = card_name\n        self.plant_name = plant_name\n        self.move_timer = 0\n        self.select = True\n\n    def loadFrame(self, name, scale):\n        frame = tool.GFX[name]\n        rect = frame.get_rect()\n        width, height = rect.w, rect.h\n\n        self.orig_image = tool.get_image(\n            frame, 0, 0, width, height, c.BLACK, scale\n        )\n        self.orig_rect = self.orig_image.get_rect()\n        self.image = self.orig_image\n\n    def checkMouseClick(self, mouse_pos):\n        x, y = mouse_pos\n        if (\n            self.rect.x <= x <= self.rect.right\n            and self.rect.y <= y <= self.rect.bottom\n        ):\n            return True\n        return False\n\n    def createShowImage(self):\n        # 新增卡片时显示图片\n        if self.rect.w < self.orig_rect.w:   # create a part card image\n            image = pg.Surface([self.rect.w, self.rect.h])\n            if self.clicked:\n                self.orig_image.set_alpha(128)\n            else:\n                self.orig_image.set_alpha(255)\n            image.blit(\n                self.orig_image, (0, 0), (0, 0, self.rect.w, self.rect.h)\n            )\n            self.rect.w += 1\n        else:\n            if self.clicked:\n                image = pg.Surface([self.rect.w, self.rect.h])  # 黑底\n                self.orig_image.set_alpha(128)\n\n                image.blit(\n                    self.orig_image, (0, 0), (0, 0, self.rect.w, self.rect.h)\n                )\n            else:\n                self.orig_image.set_alpha(255)\n                image = self.orig_image\n        return image\n\n    def update(self, left_x, current_time):\n        if self.move_timer == 0:\n            self.move_timer = current_time\n        elif (current_time - self.move_timer) >= c.CARD_MOVE_TIME:\n            if self.rect.x > left_x:\n                self.rect.x -= 1\n            self.image = self.createShowImage()\n            self.move_timer += c.CARD_MOVE_TIME\n\n    def draw(self, surface):\n        surface.blit(self.image, self.rect)\n\n\n# 传送带\nclass MoveBar:\n    def __init__(self, card_pool):\n        self.loadFrame(c.MOVEBAR_BACKGROUND)\n        self.rect = self.image.get_rect()\n        self.rect.x = 20\n        self.rect.y = 0\n\n        self.card_start_x = self.rect.x + 8\n        self.card_end_x = self.rect.right - 5\n        self.card_pool = card_pool\n        self.card_pool_name = tuple(self.card_pool.keys())\n        self.card_pool_weight = tuple(self.card_pool.values())\n        self.card_list = []\n        self.create_timer = -c.MOVEBAR_CARD_FRESH_TIME\n\n    def loadFrame(self, name):\n        frame = tool.GFX[name]\n        rect = frame.get_rect()\n        frame_rect = (rect.x, rect.y, rect.w, rect.h)\n\n        self.image = tool.get_image(tool.GFX[name], *frame_rect, c.WHITE, 1)\n\n    def createCard(self):\n        if (\n            len(self.card_list) > 0\n            and self.card_list[-1].rect.right > self.card_end_x\n        ):\n            return False\n        x = self.card_end_x\n        y = 6\n        selected_card = random.choices(\n            self.card_pool_name, self.card_pool_weight\n        )[0]\n        self.card_list.append(\n            MoveCard(\n                x,\n                y,\n                selected_card[c.CARD_INDEX],\n                selected_card[c.PLANT_NAME_INDEX],\n            )\n        )\n        return True\n\n    def update(self, current_time):\n        self.current_time = current_time\n        left_x = self.card_start_x\n        for card in self.card_list:\n            card.update(left_x, self.current_time)\n            left_x = card.rect.right + 1\n\n        if (self.current_time - self.create_timer) > c.MOVEBAR_CARD_FRESH_TIME:\n            if self.createCard():\n                self.create_timer = self.current_time\n\n    def checkCardClick(self, mouse_pos):\n        result = None\n        for index, card in enumerate(self.card_list):\n            if card.checkMouseClick(mouse_pos):\n                result = (card.plant_name, card)\n                break\n        return result\n\n    def checkMenuBarClick(self, mouse_pos):\n        x, y = mouse_pos\n        if (\n            self.rect.x <= x <= self.rect.right\n            and self.rect.y <= y <= self.rect.bottom\n        ):\n            return True\n        return False\n\n    def deleateCard(self, card):\n        self.card_list.remove(card)\n\n    def draw(self, surface):\n        surface.blit(self.image, self.rect)\n        for card in self.card_list:\n            card.draw(surface)\n"
  },
  {
    "path": "source/component/plant.py",
    "content": "import random\n\nimport pygame as pg\n\nfrom .. import constants as c\nfrom .. import tool\n\n\nclass Car(pg.sprite.Sprite):\n    def __init__(self, x: int, y: int, map_y: int):\n        pg.sprite.Sprite.__init__(self)\n\n        rect = tool.GFX[c.CAR].get_rect()\n        width, height = rect.w, rect.h\n        self.image = tool.get_image(tool.GFX[c.CAR], 0, 0, width, height)\n        self.mask = pg.mask.from_surface(self.image)\n        self.rect = self.image.get_rect()\n        self.rect.x = x\n        self.rect.bottom = y\n        self.map_y = map_y\n        self.state = c.IDLE\n        self.dead = False\n\n    def update(self, game_info: dict):\n        self.current_time = game_info[c.CURRENT_TIME]\n        if self.state == c.WALK:\n            self.rect.x += 5\n        if self.rect.x > c.SCREEN_WIDTH + 25:\n            self.dead = True\n\n    def setWalk(self):\n        if self.state == c.IDLE:\n            self.state = c.WALK\n            # 播放音效\n            c.SOUND_CAR_WALKING.play()\n\n    def draw(self, surface):\n        surface.blit(self.image, self.rect)\n\n\n# 豌豆及孢子类普通子弹\nclass Bullet(pg.sprite.Sprite):\n    def __init__(\n        self,\n        x: int,\n        start_y: int,\n        dest_y: int,\n        name: str,\n        damage: int,\n        effect: str = None,\n        passed_torchwood_x: int = None,\n        damage_type: str = c.ZOMBIE_DEAFULT_DAMAGE,\n    ):\n        pg.sprite.Sprite.__init__(self)\n\n        self.name = name\n        self.frames = []\n        self.frame_index = 0\n        self.load_images()\n        self.frame_num = len(self.frames)\n        self.image = self.frames[self.frame_index]\n        self.mask = pg.mask.from_surface(self.image)\n        self.rect = self.image.get_rect()\n        self.rect.x = x\n        self.rect.y = start_y\n        self.dest_y = dest_y\n        self.y_vel = 15 if (dest_y > start_y) else -15\n        self.x_vel = 10\n        self.damage = damage\n        self.damage_type = damage_type\n        self.effect = effect\n        self.state = c.FLY\n        self.current_time = 0\n        self.animate_timer = 0\n        self.animate_interval = 70\n        self.passed_torchwood_x = (\n            passed_torchwood_x  # 记录最近通过的火炬树横坐标，如果没有缺省为None\n        )\n\n    def loadFrames(self, frames, name):\n        frame_list = tool.GFX[name]\n        if name in c.PLANT_RECT:\n            data = c.PLANT_RECT[name]\n            x, y, width, height = (\n                data['x'],\n                data['y'],\n                data['width'],\n                data['height'],\n            )\n        else:\n            x, y = 0, 0\n            rect = frame_list[0].get_rect()\n            width, height = rect.w, rect.h\n\n        for frame in frame_list:\n            frames.append(tool.get_image(frame, x, y, width, height))\n\n    def load_images(self):\n        self.fly_frames = []\n        self.explode_frames = []\n\n        fly_name = self.name\n        if self.name in c.BULLET_INDEPENDENT_BOOM_IMG:\n            explode_name = f'{self.name}Explode'\n        else:\n            explode_name = 'PeaNormalExplode'\n\n        self.loadFrames(self.fly_frames, fly_name)\n        self.loadFrames(self.explode_frames, explode_name)\n\n        self.frames = self.fly_frames\n\n    def update(self, game_info):\n        self.current_time = game_info[c.CURRENT_TIME]\n        if self.state == c.FLY:\n            if self.rect.y != self.dest_y:\n                self.rect.y += self.y_vel\n                if self.y_vel * (self.dest_y - self.rect.y) < 0:\n                    self.rect.y = self.dest_y\n            self.rect.x += self.x_vel\n            if self.rect.x >= c.SCREEN_WIDTH + 20:\n                self.kill()\n        elif self.state == c.EXPLODE:\n            if (self.current_time - self.explode_timer) > 250:\n                self.kill()\n        if self.current_time - self.animate_timer >= self.animate_interval:\n            self.frame_index += 1\n            self.animate_timer = self.current_time\n            if self.frame_index >= self.frame_num:\n                self.frame_index = 0\n            self.image = self.frames[self.frame_index]\n\n    def setExplode(self):\n        self.state = c.EXPLODE\n        self.explode_timer = self.current_time\n        self.frames = self.explode_frames\n        self.frame_num = len(self.frames)\n        self.image = self.frames[0]\n        self.mask = pg.mask.from_surface(self.image)\n\n        # 播放子弹爆炸音效\n        if self.name == c.BULLET_FIREBALL:\n            c.SOUND_FIREPEA_EXPLODE.play()\n        else:\n            c.SOUND_BULLET_EXPLODE.play()\n\n    def draw(self, surface):\n        surface.blit(self.image, self.rect)\n\n\n# 大喷菇的烟雾\n# 仅有动画效果，不参与攻击运算\nclass Fume(pg.sprite.Sprite):\n    def __init__(self, x, y):\n        pg.sprite.Sprite.__init__(self)\n        self.name = c.FUME\n        self.timer = 0\n        self.frame_index = 0\n        self.load_images()\n        self.frame_num = len(self.frames)\n        self.image = self.frames[self.frame_index]\n        self.mask = pg.mask.from_surface(self.image)\n        self.rect = self.image.get_rect()\n        self.rect.x = x\n        self.rect.y = y\n\n    def load_images(self):\n        self.fly_frames = []\n\n        fly_name = self.name\n\n        self.loadFrames(self.fly_frames, fly_name)\n\n        self.frames = self.fly_frames\n\n    def draw(self, surface):\n        surface.blit(self.image, self.rect)\n\n    def update(self, game_info):\n        self.current_time = game_info[c.CURRENT_TIME]\n        if self.current_time - self.timer >= 100:\n            self.frame_index += 1\n            if self.frame_index >= self.frame_num:\n                self.frame_index = self.frame_num - 1\n                self.kill()\n            self.timer = self.current_time\n        self.image = self.frames[self.frame_index]\n\n    def loadFrames(self, frames, name):\n        frame_list = tool.GFX[name]\n        x, y = 0, 0\n        rect = frame_list[0].get_rect()\n        width, height = rect.w, rect.h\n\n        for frame in frame_list:\n            frames.append(tool.get_image(frame, x, y, width, height))\n\n\n# 杨桃的子弹\nclass StarBullet(Bullet):\n    def __init__(\n        self,\n        x,\n        start_y,\n        damage,\n        direction,\n        level,\n        damage_type=c.ZOMBIE_DEAFULT_DAMAGE,\n    ):    # direction指星星飞行方向\n        Bullet.__init__(\n            self,\n            x,\n            start_y,\n            start_y,\n            c.BULLET_STAR,\n            damage,\n            damage_type=damage_type,\n        )\n        self.level = level\n        self.map_y = self.level.map.getMapIndex(\n            self.rect.x, self.rect.centery\n        )[1]\n        self.direction = direction\n\n    def update(self, game_info):\n        self.current_time = game_info[c.CURRENT_TIME]\n        if self.state == c.FLY:\n            if self.direction == c.STAR_FORWARD_UP:\n                self.rect.x += 8\n                self.rect.y -= 6\n            elif self.direction == c.STAR_FORWARD_DOWN:\n                self.rect.x += 7\n                self.rect.y += 7\n            elif self.direction == c.STAR_UPWARD:\n                self.rect.y -= 10\n            elif self.direction == c.STAR_DOWNWARD:\n                self.rect.y += 10\n            else:\n                self.rect.x -= 10\n            self.handleMapYPosition()\n            if (\n                (self.rect.x > c.SCREEN_WIDTH + 20)\n                or (self.rect.right < -20)\n                or (self.rect.y > c.SCREEN_HEIGHT)\n                or (self.rect.y < 0)\n            ):\n                self.kill()\n        elif self.state == c.EXPLODE:\n            if (self.current_time - self.explode_timer) >= 250:\n                self.kill()\n\n    # 这里用的是坚果保龄球的代码改一下，实现子弹换行\n    def handleMapYPosition(self):\n        if self.direction == c.STAR_UPWARD:\n            map_y1 = self.level.map.getMapIndex(\n                self.rect.x, self.rect.centery + 40\n            )[1]\n        else:\n            map_y1 = self.level.map.getMapIndex(\n                self.rect.x, self.rect.centery + 20\n            )[1]\n        if (self.map_y != map_y1) and (\n            0 <= map_y1 <= self.level.map_y_len - 1\n        ):    # 换行\n            self.level.bullet_groups[self.map_y].remove(self)\n            self.level.bullet_groups[map_y1].add(self)\n            self.map_y = map_y1\n\n\nclass Plant(pg.sprite.Sprite):\n    def __init__(self, x, y, name, health, bullet_group, scale=1):\n        pg.sprite.Sprite.__init__(self)\n\n        self.frames = []\n        self.frame_index = 0\n        self.loadImages(name, scale)\n        self.frame_num = len(self.frames)\n        self.image = self.frames[self.frame_index]\n        self.mask = pg.mask.from_surface(self.image)\n        self.rect = self.image.get_rect()\n        self.rect.centerx = x\n        self.rect.bottom = y\n\n        self.name = name\n        self.health = health\n        self.state = c.IDLE\n        self.bullet_group = bullet_group\n        self.animate_timer = 0\n        self.animate_interval = 70  # 帧播放间隔\n        self.hit_timer = 0\n        # 被铲子指向时间\n        self.highlight_time = 0\n\n        self.attack_check = c.CHECK_ATTACK_ALWAYS\n\n    def loadFrames(self, frames, name, scale=1, color=c.BLACK):\n        frame_list = tool.GFX[name]\n        if name in c.PLANT_RECT:\n            data = c.PLANT_RECT[name]\n            x, y, width, height = (\n                data['x'],\n                data['y'],\n                data['width'],\n                data['height'],\n            )\n        else:\n            x, y = 0, 0\n            rect = frame_list[0].get_rect()\n            width, height = rect.w, rect.h\n\n        for frame in frame_list:\n            frames.append(\n                tool.get_image(frame, x, y, width, height, color, scale)\n            )\n\n    def loadImages(self, name, scale):\n        self.loadFrames(self.frames, name, scale)\n\n    def changeFrames(self, frames):\n        # change image frames and modify rect position\n        self.frames = frames\n        self.frame_num = len(self.frames)\n        self.frame_index = 0\n\n        bottom = self.rect.bottom\n        x = self.rect.x\n        self.image = self.frames[self.frame_index]\n        self.mask = pg.mask.from_surface(self.image)\n        self.rect = self.image.get_rect()\n        self.rect.bottom = bottom\n        self.rect.x = x\n\n    def update(self, game_info):\n        self.current_time = game_info[c.CURRENT_TIME]\n        self.handleState()\n        self.animation()\n\n    def handleState(self):\n        if self.state == c.IDLE:\n            self.idling()\n        elif self.state == c.ATTACK:\n            self.attacking()\n        elif self.state == c.DIGEST:\n            self.digest()\n\n    def idling(self):\n        pass\n\n    def attacking(self):\n        pass\n\n    def digest(self):\n        pass\n\n    def animation(self):\n        if (self.current_time - self.animate_timer) > self.animate_interval:\n            self.frame_index += 1\n            if self.frame_index >= self.frame_num:\n                self.frame_index = 0\n            self.animate_timer = self.current_time\n\n        self.image = self.frames[self.frame_index]\n        self.mask = pg.mask.from_surface(self.image)\n        if self.current_time - self.highlight_time < 100:\n            self.image.set_alpha(150)\n        elif (self.current_time - self.hit_timer) < 200:\n            self.image.set_alpha(192)\n        else:\n            self.image.set_alpha(255)\n\n    def canAttack(self, zombie):\n        if (zombie.name == c.SNORKELZOMBIE) and (\n            zombie.frames == zombie.swim_frames\n        ):\n            return False\n        if (\n            self.state != c.SLEEP\n            and zombie.state != c.DIE\n            and self.rect.x <= zombie.rect.right\n            and zombie.rect.x <= c.SCREEN_WIDTH - 24\n        ):\n            return True\n        return False\n\n    def setAttack(self):\n        self.state = c.ATTACK\n\n    def setIdle(self):\n        self.state = c.IDLE\n        self.is_attacked = False\n\n    def setSleep(self):\n        self.state = c.SLEEP\n        self.changeFrames(self.sleep_frames)\n\n    def setDamage(self, damage, zombie):\n        if not zombie.losthead:\n            self.health -= damage\n        self.hit_timer = self.current_time\n        if (\n            (self.name == c.HYPNOSHROOM)\n            and (self.state != c.SLEEP)\n            and (zombie.name not in {c.ZOMBONI, '投石车僵尸（未实现）', '加刚特尔（未实现）'})\n        ):\n            self.zombie_to_hypno = zombie\n\n    def getPosition(self):\n        return self.rect.centerx, self.rect.bottom\n\n\nclass Sun(Plant):\n    def __init__(self, x, y, dest_x, dest_y, is_big=True):\n        if is_big:\n            scale = 0.9\n            self.sun_value = c.SUN_VALUE\n        else:\n            scale = 0.6\n            self.sun_value = 15\n        Plant.__init__(self, x, y, c.SUN, 0, None, scale)\n        self.move_speed = 1\n        self.dest_x = dest_x\n        self.dest_y = dest_y\n        self.die_timer = 0\n\n    def handleState(self):\n        if self.rect.centerx != self.dest_x:\n            self.rect.centerx += (\n                self.move_speed\n                if self.rect.centerx < self.dest_x\n                else -self.move_speed\n            )\n        if self.rect.bottom != self.dest_y:\n            self.rect.bottom += (\n                self.move_speed\n                if self.rect.bottom < self.dest_y\n                else -self.move_speed\n            )\n\n        if (\n            self.rect.centerx == self.dest_x\n            and self.rect.bottom == self.dest_y\n        ):\n            if self.die_timer == 0:\n                self.die_timer = self.current_time\n            elif (self.current_time - self.die_timer) > c.SUN_LIVE_TIME:\n                self.state = c.DIE\n                self.kill()\n\n    def checkCollision(self, x, y):\n        if self.state == c.DIE:\n            return False\n        if (\n            x >= self.rect.x\n            and x <= self.rect.right\n            and y >= self.rect.y\n            and y <= self.rect.bottom\n        ):\n            self.state = c.DIE\n            self.kill()\n            return True\n        return False\n\n\nclass SunFlower(Plant):\n    def __init__(self, x, y, sun_group):\n        Plant.__init__(self, x, y, c.SUNFLOWER, c.PLANT_HEALTH, None)\n        self.sun_timer = 0\n        self.sun_group = sun_group\n        self.attack_check = c.CHECK_ATTACK_NEVER\n\n    def idling(self):\n        if self.sun_timer == 0:\n            self.sun_timer = self.current_time - (c.FLOWER_SUN_INTERVAL - 6000)\n        elif (self.current_time - self.sun_timer) > c.FLOWER_SUN_INTERVAL:\n            self.sun_group.add(\n                Sun(\n                    self.rect.centerx,\n                    self.rect.bottom,\n                    self.rect.right,\n                    self.rect.bottom + self.rect.h // 2,\n                )\n            )\n            self.sun_timer = self.current_time\n\n\nclass PeaShooter(Plant):\n    def __init__(self, x, y, bullet_group):\n        Plant.__init__(self, x, y, c.PEASHOOTER, c.PLANT_HEALTH, bullet_group)\n        self.shoot_timer = 0\n\n    def attacking(self):\n        if self.shoot_timer == 0:\n            self.shoot_timer = self.current_time - 700\n        elif (self.current_time - self.shoot_timer) >= 1400:\n            self.bullet_group.add(\n                Bullet(\n                    self.rect.right - 15,\n                    self.rect.y,\n                    self.rect.y,\n                    c.BULLET_PEA,\n                    c.BULLET_DAMAGE_NORMAL,\n                    effect=None,\n                )\n            )\n            self.shoot_timer = self.current_time\n            # 播放发射音效\n            c.SOUND_SHOOT.play()\n\n    def setAttack(self):\n        self.state = c.ATTACK\n        if self.shoot_timer != 0:\n            self.shoot_timer = self.current_time - 700\n\n\nclass RepeaterPea(Plant):\n    def __init__(self, x, y, bullet_group):\n        Plant.__init__(self, x, y, c.REPEATERPEA, c.PLANT_HEALTH, bullet_group)\n        self.shoot_timer = 0\n\n        # 是否发射第一颗\n        self.first_shot = False\n\n    def attacking(self):\n        if self.shoot_timer == 0:\n            self.shoot_timer = self.current_time - 700\n        elif self.current_time - self.shoot_timer >= 1400:\n            self.first_shot = True\n            self.bullet_group.add(\n                Bullet(\n                    self.rect.right - 15,\n                    self.rect.y,\n                    self.rect.y,\n                    c.BULLET_PEA,\n                    c.BULLET_DAMAGE_NORMAL,\n                    effect=None,\n                )\n            )\n            self.shoot_timer = self.current_time\n            # 播放发射音效\n            c.SOUND_SHOOT.play()\n        elif self.first_shot and (self.current_time - self.shoot_timer) > 100:\n            self.first_shot = False\n            self.bullet_group.add(\n                Bullet(\n                    self.rect.right - 15,\n                    self.rect.y,\n                    self.rect.y,\n                    c.BULLET_PEA,\n                    c.BULLET_DAMAGE_NORMAL,\n                    effect=None,\n                )\n            )\n            # 播放发射音效\n            c.SOUND_SHOOT.play()\n\n    def setAttack(self):\n        self.state = c.ATTACK\n        if self.shoot_timer != 0:\n            self.shoot_timer = self.current_time - 700\n\n\nclass ThreePeaShooter(Plant):\n    def __init__(self, x, y, bullet_groups, map_y, background_type):\n        Plant.__init__(self, x, y, c.THREEPEASHOOTER, c.PLANT_HEALTH, None)\n        self.shoot_timer = 0\n        self.map_y = map_y\n        self.bullet_groups = bullet_groups\n        self.background_type = background_type\n\n    def attacking(self):\n        if self.shoot_timer == 0:\n            self.shoot_timer = self.current_time - 700\n        if (self.current_time - self.shoot_timer) >= 1400:\n            offset_y = 9  # modify bullet in the same y position with bullets of other plants\n            for i in range(3):\n                tmp_y = self.map_y + (i - 1)\n                if self.background_type in c.POOL_EQUIPPED_BACKGROUNDS:\n                    if tmp_y < 0 or tmp_y >= c.GRID_POOL_Y_LEN:\n                        continue\n                else:\n                    if tmp_y < 0 or tmp_y >= c.GRID_Y_LEN:\n                        continue\n                if self.background_type in {\n                    c.BACKGROUND_POOL,\n                    c.BACKGROUND_FOG,\n                    c.BACKGROUND_ROOF,\n                    c.BACKGROUND_ROOFNIGHT,\n                }:\n                    dest_y = (\n                        self.rect.y + (i - 1) * c.GRID_POOL_Y_SIZE + offset_y\n                    )\n                else:\n                    dest_y = self.rect.y + (i - 1) * c.GRID_Y_SIZE + offset_y\n                self.bullet_groups[tmp_y].add(\n                    Bullet(\n                        self.rect.right - 15,\n                        self.rect.y,\n                        dest_y,\n                        c.BULLET_PEA,\n                        c.BULLET_DAMAGE_NORMAL,\n                        effect=None,\n                    )\n                )\n            self.shoot_timer = self.current_time\n            # 播放发射音效\n            c.SOUND_SHOOT.play()\n\n    def setAttack(self):\n        self.state = c.ATTACK\n        if self.shoot_timer != 0:\n            self.shoot_timer = self.current_time - 700\n\n\nclass SnowPeaShooter(Plant):\n    def __init__(self, x, y, bullet_group):\n        Plant.__init__(\n            self, x, y, c.SNOWPEASHOOTER, c.PLANT_HEALTH, bullet_group\n        )\n        self.shoot_timer = 0\n\n    def attacking(self):\n        if self.shoot_timer == 0:\n            self.shoot_timer = self.current_time - 700\n        elif (self.current_time - self.shoot_timer) >= 1400:\n            self.bullet_group.add(\n                Bullet(\n                    self.rect.right - 15,\n                    self.rect.y,\n                    self.rect.y,\n                    c.BULLET_PEA_ICE,\n                    c.BULLET_DAMAGE_NORMAL,\n                    effect=c.BULLET_EFFECT_ICE,\n                )\n            )\n            self.shoot_timer = self.current_time\n            # 播放发射音效\n            c.SOUND_SHOOT.play()\n            # 播放冰子弹音效\n            c.SOUND_SNOWPEA_SPARKLES.play()\n\n    def setAttack(self):\n        self.state = c.ATTACK\n        if self.shoot_timer != 0:\n            self.shoot_timer = self.current_time - 700\n\n\nclass WallNut(Plant):\n    def __init__(self, x, y):\n        Plant.__init__(self, x, y, c.WALLNUT, c.WALLNUT_HEALTH, None)\n        self.load_images()\n        self.cracked1 = False\n        self.cracked2 = False\n        self.attack_check = c.CHECK_ATTACK_NEVER\n\n    def load_images(self):\n        self.cracked1_frames = []\n        self.cracked2_frames = []\n\n        cracked1_frames_name = self.name + '_cracked1'\n        cracked2_frames_name = self.name + '_cracked2'\n\n        self.loadFrames(self.cracked1_frames, cracked1_frames_name)\n        self.loadFrames(self.cracked2_frames, cracked2_frames_name)\n\n    def idling(self):\n        if (not self.cracked1) and self.health <= c.WALLNUT_CRACKED1_HEALTH:\n            self.changeFrames(self.cracked1_frames)\n            self.cracked1 = True\n        elif (not self.cracked2) and self.health <= c.WALLNUT_CRACKED2_HEALTH:\n            self.changeFrames(self.cracked2_frames)\n            self.cracked2 = True\n\n\nclass CherryBomb(Plant):\n    def __init__(self, x, y):\n        Plant.__init__(self, x, y, c.CHERRYBOMB, c.INF, None)\n        self.state = c.ATTACK\n        self.start_boom = False\n        self.boomed = False\n        self.bomb_timer = 0\n        self.explode_y_range = 1\n        self.explode_x_range = c.GRID_X_SIZE * 1.5\n\n    def setBoom(self):\n        frame = tool.GFX[c.BOOM_IMAGE]\n        rect = frame.get_rect()\n        width, height = rect.w, rect.h\n\n        old_rect = self.rect\n        image = tool.get_image(frame, 0, 0, width, height, c.BLACK, 1)\n        self.image = image\n        self.mask = pg.mask.from_surface(self.image)\n        self.rect = image.get_rect()\n        self.rect.centerx = old_rect.centerx\n        self.rect.centery = old_rect.centery\n        self.start_boom = True\n\n    def animation(self):\n        if self.start_boom:\n            if self.bomb_timer == 0:\n                self.bomb_timer = self.current_time\n                # 播放爆炸音效\n                c.SOUND_BOMB.play()\n            elif (self.current_time - self.bomb_timer) > 500:\n                self.health = 0\n        else:\n            if (self.current_time - self.animate_timer) > 100:\n                self.frame_index += 1\n                if self.frame_index >= self.frame_num:\n                    self.setBoom()\n                    return\n                self.animate_timer = self.current_time\n\n            self.image = self.frames[self.frame_index]\n            self.mask = pg.mask.from_surface(self.image)\n\n        if self.current_time - self.highlight_time < 100:\n            self.image.set_alpha(150)\n        elif (self.current_time - self.hit_timer) < 200:\n            self.image.set_alpha(192)\n        else:\n            self.image.set_alpha(255)\n\n\nclass Chomper(Plant):\n    def __init__(self, x, y):\n        Plant.__init__(self, x, y, c.CHOMPER, c.PLANT_HEALTH, None)\n        self.animate_interval = 140\n        self.digest_timer = 0\n        self.digest_interval = 15000\n        self.attack_zombie = None\n        self.zombie_group = None\n        self.should_diggest = False\n\n    def loadImages(self, name, scale):\n        self.idle_frames = []\n        self.attack_frames = []\n        self.digest_frames = []\n        self.animate_interval = 100   # 本身动画播放较慢\n\n        idle_name = name\n        attack_name = name + 'Attack'\n        digest_name = name + 'Digest'\n\n        frame_list = [self.idle_frames, self.attack_frames, self.digest_frames]\n        name_list = [idle_name, attack_name, digest_name]\n        scale_list = [1, 1, 1]\n        # rect_list = [(0, 0, 100, 114), None, None]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name, scale_list[i])\n\n        self.frames = self.idle_frames\n\n    def canAttack(self, zombie):\n        if (zombie.name in {c.POLE_VAULTING_ZOMBIE}) and (not zombie.jumped):\n            return False\n        if (zombie.name == c.SNORKELZOMBIE) and (\n            zombie.frames == zombie.swim_frames\n        ):\n            return False\n        elif (\n            self.state == c.IDLE\n            and zombie.state != c.DIGEST\n            and self.rect.x <= zombie.rect.centerx\n            and (not zombie.losthead)\n            and (self.rect.x + c.GRID_X_SIZE * 2.7 >= zombie.rect.centerx)\n        ):\n            return True\n        return False\n\n    def setIdle(self):\n        self.state = c.IDLE\n        self.changeFrames(self.idle_frames)\n\n    def setAttack(self, zombie, zombie_group):\n        self.attack_zombie = zombie\n        self.zombie_group = zombie_group\n        self.state = c.ATTACK\n        self.changeFrames(self.attack_frames)\n\n    def setDigest(self):\n        self.state = c.DIGEST\n        self.changeFrames(self.digest_frames)\n\n    def attacking(self):\n        if self.frame_index == (self.frame_num - 3):\n            # 对活着的僵尸才需要吞下去消化\n            if self.attack_zombie.alive():\n                if not self.should_diggest:\n                    # 播放吞的音效 由于一帧在这个循环中执行了若干次，可能被设置播放若干次导致声音重叠，所以用if保护\n                    # 在尚未检测到需要消化时播放音效\n                    c.SOUND_BIGCHOMP.play()\n                    self.should_diggest = True\n                    self.attack_zombie.kill()\n        if (self.frame_index + 1) == self.frame_num:\n            if self.should_diggest:\n                self.setDigest()\n                self.should_diggest = False\n            else:\n                self.setIdle()\n\n    def digest(self):\n        if self.digest_timer == 0:\n            self.digest_timer = self.current_time\n        elif (self.current_time - self.digest_timer) > self.digest_interval:\n            self.digest_timer = 0\n            self.setIdle()\n\n\nclass PuffShroom(Plant):\n    def __init__(self, x, y, bullet_group):\n        Plant.__init__(self, x, y, c.PUFFSHROOM, c.PLANT_HEALTH, bullet_group)\n        self.shoot_timer = 0\n\n    def loadImages(self, name, scale):\n        self.idle_frames = []\n        self.sleep_frames = []\n\n        idle_name = name\n        sleep_name = name + 'Sleep'\n\n        frame_list = [self.idle_frames, self.sleep_frames]\n        name_list = [idle_name, sleep_name]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.idle_frames\n\n    def attacking(self):\n        if self.shoot_timer == 0:\n            self.shoot_timer = self.current_time - 700\n        elif (self.current_time - self.shoot_timer) >= 1400:\n            self.bullet_group.add(\n                Bullet(\n                    self.rect.right,\n                    self.rect.y + 10,\n                    self.rect.y + 10,\n                    c.BULLET_MUSHROOM,\n                    c.BULLET_DAMAGE_NORMAL,\n                    effect=None,\n                )\n            )\n            self.shoot_timer = self.current_time\n            # 播放音效\n            c.SOUND_PUFF.play()\n\n    def canAttack(self, zombie):\n        if (zombie.name == c.SNORKELZOMBIE) and (\n            zombie.frames == zombie.swim_frames\n        ):\n            return False\n        if (\n            self.rect.x <= zombie.rect.right\n            and (self.rect.x + c.GRID_X_SIZE * 4 >= zombie.rect.x)\n            and (zombie.rect.left <= c.SCREEN_WIDTH + 10)\n        ):\n            return True\n        return False\n\n    def setAttack(self):\n        self.state = c.ATTACK\n        if self.shoot_timer != 0:\n            self.shoot_timer = self.current_time - 700\n\n\nclass PotatoMine(Plant):\n    def __init__(self, x, y):\n        Plant.__init__(self, x, y, c.POTATOMINE, c.PLANT_HEALTH, None)\n        self.animate_interval = 300\n        self.is_init = True\n        self.init_timer = 0\n        self.bomb_timer = 0\n        self.explode_x_range = c.GRID_X_SIZE / 2\n        self.start_boom = False\n        self.boomed = False\n\n    def loadImages(self, name, scale):\n        self.init_frames = []\n        self.idle_frames = []\n        self.explode_frames = []\n\n        init_name = name + 'Init'\n        idle_name = name\n        explode_name = name + 'Explode'\n\n        frame_list = [self.init_frames, self.idle_frames, self.explode_frames]\n        name_list = [init_name, idle_name, explode_name]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.init_frames\n\n    def idling(self):\n        if self.is_init:\n            if self.init_timer == 0:\n                self.init_timer = self.current_time\n            elif (self.current_time - self.init_timer) > 15000:\n                self.changeFrames(self.idle_frames)\n                self.is_init = False\n\n    def canAttack(self, zombie):    # 土豆雷不可能遇上潜水僵尸\n        if zombie.name == c.POLE_VAULTING_ZOMBIE and (not zombie.jumped):\n            return False\n        # 这里碰撞应当比碰撞一般更容易，就设置成圆形或矩形模式，不宜采用mask\n        elif (\n            pg.sprite.collide_circle_ratio(0.7)(zombie, self)\n            and (not self.is_init)\n            and (not zombie.losthead)\n        ):\n            return True\n        return False\n\n    def attacking(self):\n        if self.bomb_timer == 0:\n            self.bomb_timer = self.current_time\n            # 播放音效\n            c.SOUND_POTATOMINE.play()\n            self.changeFrames(self.explode_frames)\n            self.start_boom = True\n        elif (self.current_time - self.bomb_timer) > 500:\n            self.health = 0\n\n\nclass Squash(Plant):\n    def __init__(self, x, y, map_plant_set):\n        Plant.__init__(self, x, y, c.SQUASH, c.PLANT_HEALTH, None)\n        self.orig_pos = (x, y)\n        self.aim_timer = 0\n        self.start_boom = False   # 和灰烬等植物统一变量名，在这里表示倭瓜是否跳起\n        self.map_plant_set = map_plant_set\n\n    def loadImages(self, name, scale):\n        self.idle_frames = []\n        self.aim_frames = []\n        self.attack_frames = []\n\n        idle_name = name\n        aim_name = name + 'Aim'\n        attack_name = name + 'Attack'\n\n        frame_list = [self.idle_frames, self.aim_frames, self.attack_frames]\n        name_list = [idle_name, aim_name, attack_name]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.idle_frames\n\n    def canAttack(self, zombie):\n        # 普通状态\n        if (\n            self.state == c.IDLE\n            and self.rect.x <= zombie.rect.right\n            and (self.rect.right + c.GRID_X_SIZE >= zombie.rect.x)\n        ):\n            return True\n        # 攻击状态\n        elif self.state == c.ATTACK:\n            if pg.sprite.collide_rect_ratio(0.5)(\n                zombie, self\n            ) or pg.sprite.collide_mask(zombie, self):\n                return True\n        return False\n\n    def setAttack(self, zombie, zombie_group):\n        self.attack_zombie = zombie\n        self.zombie_group = zombie_group\n        self.state = c.ATTACK\n        # 攻击状态下生命值无敌\n        self.health = c.INF\n\n    def attacking(self):\n        if self.start_boom:\n            if (self.frame_index + 1) == self.frame_num:\n                for zombie in self.zombie_group:\n                    if self.canAttack(zombie):\n                        zombie.setDamage(\n                            1800, damage_type=c.ZOMBIE_RANGE_DAMAGE\n                        )\n                self.health = 0   # 避免僵尸在原位啃食\n                self.map_plant_set.remove(c.SQUASH)\n                self.kill()\n                # 播放碾压音效\n                c.SOUND_SQUASHING.play()\n        elif self.aim_timer == 0:\n            # 锁定目标时播放音效\n            c.SOUND_SQUASH_HMM.play()\n            self.aim_timer = self.current_time\n            self.changeFrames(self.aim_frames)\n        elif (self.current_time - self.aim_timer) > 1000:\n            self.changeFrames(self.attack_frames)\n            self.rect.centerx = self.attack_zombie.rect.centerx\n            self.start_boom = True\n            self.animate_interval = 300\n\n    def getPosition(self):\n        return self.orig_pos\n\n\nclass Spikeweed(Plant):\n    def __init__(self, x, y):\n        Plant.__init__(\n            self, x, y, c.SPIKEWEED, c.PLANT_HEALTH, None, scale=0.9\n        )\n        self.animate_interval = 70\n        self.attack_timer = 0\n\n    def setIdle(self):\n        self.animate_interval = 70\n        self.state = c.IDLE\n\n    def canAttack(self, zombie):\n        # 地刺能不能扎的判据：\n        # 僵尸中心与地刺中心的距离或僵尸包括了地刺中心和右端（平衡得到合理的攻击范围,\"僵尸包括了地刺中心和右端\"是为以后巨人做准备）\n        # 暂时不能用碰撞判断，平衡性不好\n        if (-40 <= zombie.rect.centerx - self.rect.centerx <= 40) or (\n            zombie.rect.left <= self.rect.x <= zombie.rect.right\n            and zombie.rect.left <= self.rect.right <= zombie.rect.right\n        ):\n            return True\n        return False\n\n    def setAttack(self, zombie_group):\n        self.zombie_group = zombie_group\n        self.animate_interval = 35\n        self.state = c.ATTACK\n        if self.hit_timer != 0:\n            self.hit_timer = self.current_time - 500\n\n    def attacking(self):\n        if self.hit_timer == 0:\n            self.hit_timer = self.current_time - 500\n        elif (self.current_time - self.attack_timer) >= 700:\n            self.attack_timer = self.current_time\n            # 最后再来判断攻击是否要杀死自己\n            killSelf = False\n            for zombie in self.zombie_group:\n                if self.canAttack(zombie):\n                    # 有车的僵尸\n                    if zombie.name in {c.ZOMBONI}:\n                        zombie.health = zombie.losthead_health\n                        killSelf = True\n                    else:\n                        zombie.setDamage(\n                            20, damage_type=c.ZOMBIE_COMMON_DAMAGE\n                        )\n            if killSelf:\n                self.health = 0\n            # 播放攻击音效，同子弹打击\n            c.SOUND_BULLET_EXPLODE.play()\n\n\nclass Jalapeno(Plant):\n    def __init__(self, x, y):\n        Plant.__init__(self, x, y, c.JALAPENO, c.INF, None)\n        self.orig_pos = (x, y)\n        self.state = c.ATTACK\n        self.start_boom = False\n        self.boomed = False\n        self.explode_y_range = 0\n        self.explode_x_range = 500\n\n    def loadImages(self, name, scale):\n        self.explode_frames = []\n        explode_name = name + 'Explode'\n        self.loadFrames(self.explode_frames, explode_name)\n\n        self.loadFrames(self.frames, name)\n\n    def setExplode(self):\n        self.changeFrames(self.explode_frames)\n        self.animate_timer = self.current_time\n        self.rect.x = c.MAP_OFFSET_X\n        self.start_boom = True\n\n    def animation(self):\n        if self.start_boom:\n            if (self.current_time - self.animate_timer) > 100:\n                if self.frame_index == 1:\n                    # 播放爆炸音效\n                    c.SOUND_BOMB.play()\n                self.frame_index += 1\n                if self.frame_index >= self.frame_num:\n                    self.health = 0\n                    return\n                self.animate_timer = self.current_time\n        else:\n            if (self.current_time - self.animate_timer) > 100:\n                self.frame_index += 1\n                if self.frame_index >= self.frame_num:\n                    self.setExplode()\n                    return\n                self.animate_timer = self.current_time\n        self.image = self.frames[self.frame_index]\n        self.mask = pg.mask.from_surface(self.image)\n\n        if self.current_time - self.highlight_time < 100:\n            self.image.set_alpha(150)\n        elif (self.current_time - self.hit_timer) < 200:\n            self.image.set_alpha(192)\n        else:\n            self.image.set_alpha(255)\n\n    def getPosition(self):\n        return self.orig_pos\n\n\nclass ScaredyShroom(Plant):\n    def __init__(self, x, y, bullet_group):\n        Plant.__init__(\n            self, x, y, c.SCAREDYSHROOM, c.PLANT_HEALTH, bullet_group\n        )\n        self.shoot_timer = 0\n        self.cry_x_range = c.GRID_X_SIZE * 1.5\n\n    def loadImages(self, name, scale):\n        self.idle_frames = []\n        self.cry_frames = []\n        self.sleep_frames = []\n\n        idle_name = name\n        cry_name = name + 'Cry'\n        sleep_name = name + 'Sleep'\n\n        frame_list = [self.idle_frames, self.cry_frames, self.sleep_frames]\n        name_list = [idle_name, cry_name, sleep_name]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.idle_frames\n\n    def needCry(self, zombie):\n        if (\n            zombie.state != c.DIE\n            and abs(self.rect.x - zombie.rect.x) < self.cry_x_range\n        ):\n            return True\n        return False\n\n    def setCry(self):\n        self.state = c.CRY\n        self.changeFrames(self.cry_frames)\n\n    def setAttack(self):\n        self.state = c.ATTACK\n        self.changeFrames(self.idle_frames)\n        if self.shoot_timer != 0:\n            self.shoot_timer = self.current_time - 700\n\n    def setIdle(self):\n        self.state = c.IDLE\n        self.changeFrames(self.idle_frames)\n\n    def attacking(self):\n        if self.shoot_timer == 0:\n            self.shoot_timer = self.current_time - 700\n        elif (self.current_time - self.shoot_timer) >= 1400:\n            self.bullet_group.add(\n                Bullet(\n                    self.rect.right - 15,\n                    self.rect.y + 40,\n                    self.rect.y + 40,\n                    c.BULLET_MUSHROOM,\n                    c.BULLET_DAMAGE_NORMAL,\n                    effect=None,\n                )\n            )\n            self.shoot_timer = self.current_time\n            # 播放音效\n            c.SOUND_PUFF.play()\n\n\nclass SunShroom(Plant):\n    def __init__(self, x, y, sun_group):\n        Plant.__init__(self, x, y, c.SUNSHROOM, c.PLANT_HEALTH, None)\n        self.animate_interval = 140\n        self.sun_timer = 0\n        self.sun_group = sun_group\n        self.is_big = False\n        self.change_timer = 0\n\n    def loadImages(self, name, scale):\n        self.idle_frames = []\n        self.big_frames = []\n        self.sleep_frames = []\n\n        idle_name = name\n        big_name = name + 'Big'\n        sleep_name = name + 'Sleep'\n\n        frame_list = [self.idle_frames, self.big_frames, self.sleep_frames]\n        name_list = [idle_name, big_name, sleep_name]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.idle_frames\n\n    def idling(self):\n        if not self.is_big:\n            if self.change_timer == 0:\n                self.change_timer = self.current_time\n            elif (self.current_time - self.change_timer) > 100000:\n                self.changeFrames(self.big_frames)\n                self.is_big = True\n                # 播放长大音效\n                c.SOUND_PLANT_GROW.play()\n        if self.sun_timer == 0:\n            self.sun_timer = self.current_time - (c.FLOWER_SUN_INTERVAL - 6000)\n        elif (self.current_time - self.sun_timer) > c.FLOWER_SUN_INTERVAL:\n            self.sun_group.add(\n                Sun(\n                    self.rect.centerx,\n                    self.rect.bottom,\n                    self.rect.right,\n                    self.rect.bottom + self.rect.h // 2,\n                    self.is_big,\n                )\n            )\n            self.sun_timer = self.current_time\n\n\nclass IceShroom(Plant):\n    def __init__(self, x, y):\n        Plant.__init__(self, x, y, c.ICESHROOM, c.PLANT_HEALTH, None)\n        self.orig_pos = (x, y)\n        self.start_boom = False\n        self.boomed = False\n\n    def loadImages(self, name, scale):\n        self.idle_frames = []\n        self.snow_frames = []\n        self.sleep_frames = []\n        self.trap_frames = []\n\n        idle_name = name\n        snow_name = name + 'Snow'\n        sleep_name = name + 'Sleep'\n        trap_name = name + 'Trap'\n\n        frame_list = [\n            self.idle_frames,\n            self.snow_frames,\n            self.sleep_frames,\n            self.trap_frames,\n        ]\n        name_list = [idle_name, snow_name, sleep_name, trap_name]\n        scale_list = [1, 1.5, 1, 1]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name, scale_list[i])\n\n        self.frames = self.idle_frames\n\n    def setFreeze(self):\n        self.changeFrames(self.snow_frames)\n        self.animate_timer = self.current_time\n        self.rect.x = c.MAP_OFFSET_X\n        self.rect.y = c.MAP_OFFSET_Y\n        self.start_boom = True\n\n    def animation(self):\n        if self.start_boom:\n            if (self.current_time - self.animate_timer) > 500:\n                self.frame_index += 1\n                if self.frame_index >= self.frame_num:\n                    self.health = 0\n                    return\n                self.animate_timer = self.current_time\n        else:\n            if self.state != c.SLEEP:\n                self.health = c.INF\n            if (self.current_time - self.animate_timer) > 100:\n                self.frame_index += 1\n                if self.frame_index >= self.frame_num:\n                    if self.state == c.SLEEP:\n                        self.frame_index = 0\n                    else:\n                        self.setFreeze()\n                        return\n                self.animate_timer = self.current_time\n        self.image = self.frames[self.frame_index]\n        self.mask = pg.mask.from_surface(self.image)\n\n        if self.current_time - self.highlight_time < 100:\n            self.image.set_alpha(150)\n        elif (self.current_time - self.hit_timer) < 200:\n            self.image.set_alpha(192)\n        else:\n            self.image.set_alpha(255)\n\n    def getPosition(self):\n        return self.orig_pos\n\n\nclass HypnoShroom(Plant):\n    def __init__(self, x, y):\n        Plant.__init__(self, x, y, c.HYPNOSHROOM, c.PLANT_HEALTH, None)\n        self.animate_interval = 80\n        self.zombie_to_hypno = None\n        self.attack_check = c.CHECK_ATTACK_NEVER\n\n    def loadImages(self, name, scale):\n        self.idle_frames = []\n        self.sleep_frames = []\n\n        idle_name = name\n        sleep_name = name + 'Sleep'\n\n        frame_list = [self.idle_frames, self.sleep_frames]\n        name_list = [idle_name, sleep_name]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.idle_frames\n\n    def idling(self):\n        if self.health < c.PLANT_HEALTH and self.zombie_to_hypno:\n            self.health = 0\n\n\nclass WallNutBowling(Plant):\n    def __init__(self, x, y, map_y, level):\n        Plant.__init__(self, x, y, c.WALLNUTBOWLING, 1, None)\n        self.map_y = map_y\n        self.level = level\n        self.init_rect = self.rect.copy()\n        self.rotate_degree = 0\n        self.animate_interval = 200\n        self.move_timer = 0\n        self.move_interval = 70\n        self.vel_x = random.randint(12, 15)\n        self.vel_y = 0\n        self.disable_hit_y = -1\n        self.attack_check = c.CHECK_ATTACK_NEVER\n\n    def loadImages(self, name, scale):\n        self.loadFrames(self.frames, name, 1)\n\n    def idling(self):\n        if self.move_timer == 0:\n            self.move_timer = self.current_time\n        elif (self.current_time - self.move_timer) >= self.move_interval:\n            self.rotate_degree = (self.rotate_degree - 30) % 360\n            self.init_rect.x += self.vel_x\n            self.init_rect.y += self.vel_y\n            self.handleMapYPosition()\n            if self.shouldChangeDirection():\n                self.changeDirection(-1)\n            if self.init_rect.x > c.SCREEN_WIDTH + 25:\n                self.health = 0\n            self.move_timer += self.move_interval\n\n    def canHit(self, map_y):\n        if self.disable_hit_y == map_y:\n            return False\n        return True\n\n    def handleMapYPosition(self):\n        map_y1 = self.level.map.getMapIndex(\n            self.init_rect.x, self.init_rect.centery\n        )[1]\n        map_y2 = self.level.map.getMapIndex(\n            self.init_rect.x, self.init_rect.bottom\n        )[1]\n        if self.map_y != map_y1 and map_y1 == map_y2:\n            # wallnut bowls to another row, should modify which plant group it belongs to\n            self.level.plant_groups[self.map_y].remove(self)\n            self.level.plant_groups[map_y1].add(self)\n            self.map_y = map_y1\n\n    def shouldChangeDirection(self):\n        if self.init_rect.centery <= c.MAP_OFFSET_Y:\n            return True\n        elif self.init_rect.bottom + 20 >= c.SCREEN_HEIGHT:\n            return True\n        return False\n\n    def changeDirection(self, map_y):\n        if self.vel_y == 0:\n            if self.map_y == 0:\n                self.vel_y = self.vel_x\n            elif self.map_y == (c.GRID_Y_LEN - 1):  # 坚果保龄球显然没有泳池的6行情形\n                self.vel_y = -self.vel_x\n            else:\n                if random.randint(0, 1):\n                    self.vel_y = self.vel_x\n                else:\n                    self.vel_y = -self.vel_x\n        else:\n            self.vel_y = -self.vel_y\n\n        self.disable_hit_y = map_y\n\n    def animation(self):\n        image = self.frames[self.frame_index]\n        self.image = pg.transform.rotate(image, self.rotate_degree)\n        self.mask = pg.mask.from_surface(self.image)\n        # must keep the center postion of image when rotate\n        self.rect = self.image.get_rect(center=self.init_rect.center)\n\n\nclass RedWallNutBowling(Plant):\n    def __init__(self, x, y):\n        Plant.__init__(self, x, y, c.REDWALLNUTBOWLING, 1, None)\n        self.orig_y = y\n        self.explode_timer = 0\n        self.explode_y_range = 1\n        self.explode_x_range = c.GRID_X_SIZE * 1.5\n        self.init_rect = self.rect.copy()\n        self.rotate_degree = 0\n        self.animate_interval = 200\n        self.move_timer = 0\n        self.move_interval = 70\n        self.vel_x = random.randint(12, 15)\n        self.start_boom = False\n        self.boomed = False\n\n    def loadImages(self, name, scale):\n        self.idle_frames = []\n        self.loadFrames(self.idle_frames, name, 1)\n\n        frame = tool.GFX[c.BOOM_IMAGE]\n        rect = frame.get_rect()\n        image = tool.get_image(frame, 0, 0, rect.w, rect.h)\n        self.explode_frames = (image,)\n\n        self.frames = self.idle_frames\n\n    def idling(self):\n        if self.move_timer == 0:\n            self.move_timer = self.current_time\n        elif (self.current_time - self.move_timer) >= self.move_interval:\n            self.rotate_degree = (self.rotate_degree - 30) % 360\n            self.init_rect.x += self.vel_x\n            if self.init_rect.x > c.SCREEN_WIDTH + 25:\n                self.health = 0\n            self.move_timer += self.move_interval\n\n    def attacking(self):\n        if self.explode_timer == 0:\n            self.start_boom = True\n            self.explode_timer = self.current_time\n            self.changeFrames(self.explode_frames)\n            # 播放爆炸音效\n            c.SOUND_BOMB.play()\n        elif (self.current_time - self.explode_timer) > 500:\n            self.health = 0\n\n    def animation(self):\n        if (self.current_time - self.animate_timer) > self.animate_interval:\n            self.frame_index += 1\n            if self.frame_index >= self.frame_num:\n                self.frame_index = 0\n            self.animate_timer = self.current_time\n\n        image = self.frames[self.frame_index]\n        if self.state == c.IDLE:\n            self.image = pg.transform.rotate(image, self.rotate_degree)\n        else:\n            self.image = image\n        self.mask = pg.mask.from_surface(self.image)\n        # must keep the center postion of image when rotate\n        self.rect = self.image.get_rect(center=self.init_rect.center)\n\n    def getPosition(self):\n        return (self.rect.centerx, self.orig_y)\n\n\nclass LilyPad(Plant):\n    def __init__(self, x, y):\n        Plant.__init__(self, x, y, c.LILYPAD, c.PLANT_HEALTH, None)\n        self.attack_check = c.CHECK_ATTACK_NEVER\n\n\nclass TorchWood(Plant):\n    def __init__(self, x, y, bullet_group):\n        Plant.__init__(self, x, y, c.TORCHWOOD, c.PLANT_HEALTH, bullet_group)\n        self.attack_check = c.CHECK_ATTACK_NEVER\n\n    def idling(self):\n        for i in self.bullet_group:\n            if (\n                i.name == c.BULLET_PEA\n                and i.passed_torchwood_x != self.rect.centerx\n                and abs(i.rect.centerx - self.rect.centerx) <= 20\n            ):\n                self.bullet_group.add(\n                    Bullet(\n                        i.rect.x,\n                        i.rect.y,\n                        i.dest_y,\n                        c.BULLET_FIREBALL,\n                        c.BULLET_DAMAGE_FIREBALL_BODY,\n                        effect=c.BULLET_EFFECT_UNICE,\n                        passed_torchwood_x=self.rect.centerx,\n                    )\n                )\n                i.kill()\n            elif (\n                i.name == c.BULLET_PEA_ICE\n                and i.passed_torchwood_x != self.rect.centerx\n                and abs(i.rect.centerx - self.rect.centerx)\n            ):\n                self.bullet_group.add(\n                    Bullet(\n                        i.rect.x,\n                        i.rect.y,\n                        i.dest_y,\n                        c.BULLET_PEA,\n                        c.BULLET_DAMAGE_NORMAL,\n                        effect=None,\n                        passed_torchwood_x=self.rect.centerx,\n                    )\n                )\n                i.kill()\n\n\nclass StarFruit(Plant):\n    def __init__(self, x, y, bullet_group, level):\n        Plant.__init__(self, x, y, c.STARFRUIT, c.PLANT_HEALTH, bullet_group)\n        self.shoot_timer = 0\n        self.level = level\n        self.map_x, self.map_y = self.level.map.getMapIndex(x, y)\n\n    def canAttack(self, zombie):\n        if (zombie.name == c.SNORKELZOMBIE) and (\n            zombie.frames == zombie.swim_frames\n        ):\n            return False\n        if zombie.state != c.DIE:\n            zombie_map_y = self.level.map.getMapIndex(\n                zombie.rect.centerx, zombie.rect.bottom\n            )[1]\n            if (self.rect.x >= zombie.rect.x) and (\n                self.map_y == zombie_map_y\n            ):  # 对于同行且在杨桃后的僵尸\n                return True\n            # 斜向上，理想直线方程为：\n            # f(zombie.rect.x) = -0.75*(zombie.rect.x - (self.rect.right - 5)) + self.rect.y - 10\n            # 注意实际上为射线\n            elif (\n                -100\n                <= (\n                    zombie.rect.y\n                    - (\n                        -0.75 * (zombie.rect.x - (self.rect.right - 5))\n                        + self.rect.y\n                        - 10\n                    )\n                )\n                <= 70\n                and (zombie.rect.left <= c.SCREEN_WIDTH)\n                and (zombie.rect.x >= self.rect.x)\n            ):\n                return True\n            # 斜向下，理想直线方程为：f(zombie.rect.x) = zombie.rect.x + self.rect.y - self.rect.right - 15\n            # 注意实际上为射线\n            elif (\n                abs(\n                    zombie.rect.y\n                    - (zombie.rect.x + self.rect.y - self.rect.right - 15)\n                )\n                <= 70\n                and (zombie.rect.left <= c.SCREEN_WIDTH)\n                and (zombie.rect.x >= self.rect.x)\n            ):\n                return True\n            elif zombie.rect.left <= self.rect.x <= zombie.rect.right:\n                return True\n        return False\n\n    def attacking(self):\n        if self.shoot_timer == 0:\n            self.shoot_timer = self.current_time - 700\n        elif (self.current_time - self.shoot_timer) >= 1400:\n            # pypvz特有设定：向后打的杨桃子弹无视铁门与报纸防具\n            self.bullet_group.add(\n                StarBullet(\n                    self.rect.left - 10,\n                    self.rect.y + 15,\n                    c.BULLET_DAMAGE_NORMAL,\n                    c.STAR_BACKWARD,\n                    self.level,\n                    damage_type=c.ZOMBIE_COMMON_DAMAGE,\n                )\n            )\n            # 其他方向的杨桃子弹伤害效果与豌豆等同\n            self.bullet_group.add(\n                StarBullet(\n                    self.rect.centerx - 20,\n                    self.rect.bottom - self.rect.h - 15,\n                    c.BULLET_DAMAGE_NORMAL,\n                    c.STAR_UPWARD,\n                    self.level,\n                )\n            )\n            self.bullet_group.add(\n                StarBullet(\n                    self.rect.centerx - 20,\n                    self.rect.bottom - 5,\n                    c.BULLET_DAMAGE_NORMAL,\n                    c.STAR_DOWNWARD,\n                    self.level,\n                )\n            )\n            self.bullet_group.add(\n                StarBullet(\n                    self.rect.right - 5,\n                    self.rect.bottom - 20,\n                    c.BULLET_DAMAGE_NORMAL,\n                    c.STAR_FORWARD_DOWN,\n                    self.level,\n                )\n            )\n            self.bullet_group.add(\n                StarBullet(\n                    self.rect.right - 5,\n                    self.rect.y - 10,\n                    c.BULLET_DAMAGE_NORMAL,\n                    c.STAR_FORWARD_UP,\n                    self.level,\n                )\n            )\n            self.shoot_timer = self.current_time\n            # 播放发射音效\n            c.SOUND_SHOOT.play()\n\n    def setAttack(self):\n        self.state = c.ATTACK\n        if self.shoot_timer != 0:\n            self.shoot_timer = self.current_time - 700\n\n\nclass CoffeeBean(Plant):\n    def __init__(self, x, y, plant_group, map_content, map, map_x):\n        Plant.__init__(self, x, y, c.COFFEEBEAN, c.PLANT_HEALTH, None)\n        self.plant_group = plant_group\n        self.map_content = map_content\n        self.map = map\n        self.map_x = map_x\n        self.attack_check = c.CHECK_ATTACK_NEVER\n\n    def animation(self):\n        if (self.current_time - self.animate_timer) > self.animate_interval:\n            self.frame_index += 1\n\n            if self.frame_index >= self.frame_num:\n                self.map_content[c.MAP_SLEEP] = False\n                for plant in self.plant_group:\n                    if plant.name in c.CAN_SLEEP_PLANTS:\n                        if plant.state == c.SLEEP:\n                            plant_map_x, _ = self.map.getMapIndex(\n                                plant.rect.centerx, plant.rect.bottom\n                            )\n                            if plant_map_x == self.map_x:\n                                plant.state = c.IDLE\n                                plant.setIdle()\n                                plant.changeFrames(plant.idle_frames)\n                # 播放唤醒音效\n                c.SOUND_MUSHROOM_WAKEUP.play()\n                self.map_content[c.MAP_PLANT].remove(self.name)\n                self.kill()\n                self.frame_index = self.frame_num - 1\n\n            self.animate_timer = self.current_time\n\n        self.image = self.frames[self.frame_index]\n        self.mask = pg.mask.from_surface(self.image)\n        if self.current_time - self.highlight_time < 100:\n            self.image.set_alpha(150)\n        elif (self.current_time - self.hit_timer) < 200:\n            self.image.set_alpha(192)\n        else:\n            self.image.set_alpha(255)\n\n\nclass SeaShroom(Plant):\n    def __init__(self, x, y, bullet_group):\n        Plant.__init__(self, x, y, c.SEASHROOM, c.PLANT_HEALTH, bullet_group)\n        self.shoot_timer = 0\n\n    def loadImages(self, name, scale):\n        self.idle_frames = []\n        self.sleep_frames = []\n\n        idle_name = name\n        sleep_name = name + 'Sleep'\n\n        frame_list = [self.idle_frames, self.sleep_frames]\n        name_list = [idle_name, sleep_name]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.idle_frames\n\n    def attacking(self):\n        if self.shoot_timer == 0:\n            self.shoot_timer = self.current_time - 700\n        elif (self.current_time - self.shoot_timer) >= 1400:\n            self.bullet_group.add(\n                Bullet(\n                    self.rect.right,\n                    self.rect.y + 50,\n                    self.rect.y + 50,\n                    c.BULLET_SEASHROOM,\n                    c.BULLET_DAMAGE_NORMAL,\n                    effect=None,\n                )\n            )\n            self.shoot_timer = self.current_time\n            # 播放发射音效\n            c.SOUND_PUFF.play()\n\n    def canAttack(self, zombie):\n        if (zombie.name == c.SNORKELZOMBIE) and (\n            zombie.frames == zombie.swim_frames\n        ):\n            return False\n        if (\n            self.rect.x <= zombie.rect.right\n            and (self.rect.x + c.GRID_X_SIZE * 4 >= zombie.rect.x)\n            and (zombie.rect.left <= c.SCREEN_WIDTH + 10)\n        ):\n            return True\n        return False\n\n    def setAttack(self):\n        self.state = c.ATTACK\n        if self.shoot_timer != 0:\n            self.shoot_timer = self.current_time - 700\n\n\nclass TallNut(Plant):\n    def __init__(self, x, y):\n        Plant.__init__(self, x, y, c.TALLNUT, c.TALLNUT_HEALTH, None)\n        self.load_images()\n        self.cracked1 = False\n        self.cracked2 = False\n        self.attack_check = c.CHECK_ATTACK_NEVER\n\n    def load_images(self):\n        self.cracked1_frames = []\n        self.cracked2_frames = []\n\n        cracked1_frames_name = self.name + '_cracked1'\n        cracked2_frames_name = self.name + '_cracked2'\n\n        self.loadFrames(self.cracked1_frames, cracked1_frames_name)\n        self.loadFrames(self.cracked2_frames, cracked2_frames_name)\n\n    def idling(self):\n        if not self.cracked1 and self.health <= c.TALLNUT_CRACKED1_HEALTH:\n            self.changeFrames(self.cracked1_frames)\n            self.cracked1 = True\n        elif not self.cracked2 and self.health <= c.TALLNUT_CRACKED2_HEALTH:\n            self.changeFrames(self.cracked2_frames)\n            self.cracked2 = True\n\n\nclass TangleKlep(Plant):\n    def __init__(self, x, y):\n        Plant.__init__(self, x, y, c.TANGLEKLEP, c.PLANT_HEALTH, None)\n        self.load_images()\n        self.splashing = False\n\n    def load_images(self):\n        self.idle_frames = []\n        self.splash_frames = []\n\n        idle_name = self.name\n        splash_name = self.name + 'Splash'\n\n        frame_list = [self.idle_frames, self.splash_frames]\n        name_list = [idle_name, splash_name]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.idle_frames\n\n    def canAttack(self, zombie):\n        if zombie.state != c.DIE and (not zombie.losthead):\n            # 这里碰撞应当比碰撞一般更容易，就设置成圆形或矩形模式，不宜采用mask\n            if pg.sprite.collide_rect_ratio(1)(zombie, self):\n                return True\n        return False\n\n    def setAttack(self, zombie, zombie_group):\n        self.attack_zombie = zombie\n        self.zombie_group = zombie_group\n        self.state = c.ATTACK\n\n    def attacking(self):\n        if not self.splashing:\n            self.splashing = True\n            self.changeFrames(self.splash_frames)\n            self.attack_zombie.kill()\n            # 播放拖拽音效\n            c.SOUND_TANGLE_KELP_DRAG.play()\n        # 这里必须用elif排除尚未进入splash阶段，以免误触\n        elif (self.frame_index + 1) >= self.frame_num:\n            self.health = 0\n\n\n# 毁灭菇的处理办法：\n# 爆炸后留下的坑看作另一种形态的毁灭菇\n# 当存在这种形态的毁灭菇时不可以种植物\n# 坑形态的毁灭菇存在时不可种植物\n# 坑形态的毁灭菇同地刺一样不可以被啃食\n# 爆炸时杀死同一格的所有植物\nclass DoomShroom(Plant):\n    def __init__(self, x, y, map_plant_set, explode_y_range):\n        Plant.__init__(self, x, y, c.DOOMSHROOM, c.PLANT_HEALTH, None)\n        self.map_plant_set = map_plant_set\n        self.bomb_timer = 0\n        self.explode_y_range = explode_y_range\n        self.explode_x_range = 250\n        self.start_boom = False\n        self.boomed = False\n        self.original_x = x\n        self.original_y = y\n\n    def loadImages(self, name, scale):\n        self.idle_frames = []\n        self.sleep_frames = []\n        self.boom_frames = []\n\n        idle_name = name\n        sleep_name = name + 'Sleep'\n        boom_name = name + 'Boom'\n\n        frame_list = [self.idle_frames, self.sleep_frames, self.boom_frames]\n        name_list = [idle_name, sleep_name, boom_name]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.idle_frames\n\n    def setBoom(self):\n        self.changeFrames(self.boom_frames)\n        self.start_boom = True\n\n    def animation(self):\n        # 发生了爆炸\n        if self.start_boom:\n            if self.frame_index == 1:\n                self.rect.x -= 80\n                self.rect.y += 30\n                # 播放爆炸音效\n                c.SOUND_DOOMSHROOM.play()\n            if (\n                self.current_time - self.animate_timer\n            ) > self.animate_interval:\n                self.frame_index += 1\n            if self.frame_index >= self.frame_num:\n                self.health = 0\n                self.frame_index = self.frame_num - 1\n                self.map_plant_set.add(c.HOLE)\n        # 睡觉状态\n        elif self.state == c.SLEEP:\n            if (\n                self.current_time - self.animate_timer\n            ) > self.animate_interval:\n                self.frame_index += 1\n                if self.frame_index >= self.frame_num:\n                    self.frame_index = 0\n                self.animate_timer = self.current_time\n        # 正常状态\n        else:\n            self.health = c.INF\n            if (self.current_time - self.animate_timer) > 100:\n                self.frame_index += 1\n                if self.frame_index >= self.frame_num:\n                    self.setBoom()\n                    return\n                self.animate_timer = self.current_time\n        self.image = self.frames[self.frame_index]\n        self.mask = pg.mask.from_surface(self.image)\n\n        if self.current_time - self.highlight_time < 100:\n            self.image.set_alpha(150)\n        elif (self.current_time - self.hit_timer) < 200:\n            self.image.set_alpha(192)\n        else:\n            self.image.set_alpha(255)\n\n\n# 用于描述毁灭菇的坑\nclass Hole(Plant):\n    def __init__(self, x, y, plot_type):\n        # 指定区域类型这一句必须放在前面，否则加载图片判断将会失败\n        self.plot_type = plot_type\n        Plant.__init__(self, x, y, c.HOLE, c.INF, None)\n        self.timer = 0\n        self.shallow = False\n        self.attack_check = c.CHECK_ATTACK_NEVER\n\n    def loadImages(self, name, scale):\n        self.idle_frames = []\n        self.idle2_frames = []\n        self.water_frames = []\n        self.water2_frames = []\n        self.roof_frames = []\n        self.roof2_frames = []\n\n        idle_name = name\n        idle2_name = name + 'Shallow'\n        water_name = name + 'Water'\n        water2_name = name + 'WaterShallow'\n        roof_name = name + 'Roof'\n        roof2_name = name + 'RoofShallow'\n\n        frame_list = [\n            self.idle_frames,\n            self.idle2_frames,\n            self.water_frames,\n            self.water2_frames,\n            self.roof_frames,\n            self.roof2_frames,\n        ]\n        name_list = [\n            idle_name,\n            idle2_name,\n            water_name,\n            water2_name,\n            roof_name,\n            roof2_name,\n        ]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        if self.plot_type == c.MAP_TILE:\n            self.frames = self.roof_frames\n        elif self.plot_type == c.MAP_WATER:\n            self.frames = self.water_frames\n        else:\n            self.frames = self.idle_frames\n\n    def idling(self):\n        if self.timer == 0:\n            self.timer = self.current_time\n        elif (not self.shallow) and (self.current_time - self.timer >= 90000):\n            if self.plot_type == c.MAP_TILE:\n                self.frames = self.roof2_frames\n            elif self.plot_type == c.MAP_WATER:\n                self.frames = self.water2_frames\n            else:\n                self.frames = self.idle2_frames\n            self.shallow = True\n        elif self.current_time - self.timer >= 180000:\n            self.health = 0\n\n\nclass Grave(Plant):\n    def __init__(self, x, y):\n        Plant.__init__(self, x, y, c.GRAVE, c.INF, None)\n        self.frame_index = random.randint(0, self.frame_num - 1)\n        self.image = self.frames[self.frame_index]\n        self.mask = pg.mask.from_surface(self.image)\n        self.attack_check = c.CHECK_ATTACK_NEVER\n\n    def animation(self):\n        pass\n\n\nclass GraveBuster(Plant):\n    def __init__(self, x, y, plant_group, map, map_x):\n        Plant.__init__(self, x, y, c.GRAVEBUSTER, c.PLANT_HEALTH, None)\n        self.map = map\n        self.map_x = map_x\n        self.plant_group = plant_group\n        self.animate_interval = 100\n        self.attack_check = c.CHECK_ATTACK_NEVER\n        # 播放吞噬音效\n        c.SOUND_GRAVEBUSTER_CHOMP.play()\n\n    def animation(self):\n        if (self.current_time - self.animate_timer) > self.animate_interval:\n            self.frame_index += 1\n            if self.frame_index >= self.frame_num:\n                self.frame_index = self.frame_num - 1\n                for item in self.plant_group:\n                    if item.name == c.GRAVE:\n                        item_map_x, _ = self.map.getMapIndex(\n                            item.rect.centerx, item.rect.bottom\n                        )\n                        if item_map_x == self.map_x:\n                            item.health = 0\n                            self.health = 0\n            self.animate_timer = self.current_time\n\n        self.image = self.frames[self.frame_index]\n        self.mask = pg.mask.from_surface(self.image)\n\n        if self.current_time - self.highlight_time < 100:\n            self.image.set_alpha(150)\n        elif (self.current_time - self.hit_timer) < 200:\n            self.image.set_alpha(192)\n        else:\n            self.image.set_alpha(255)\n\n\nclass FumeShroom(Plant):\n    def __init__(self, x, y, bullet_group, zombie_group):\n        Plant.__init__(self, x, y, c.FUMESHROOM, c.PLANT_HEALTH, bullet_group)\n        self.shoot_timer = 0\n        self.show_attack_frames = True\n        self.zombie_group = zombie_group\n\n    def loadImages(self, name, scale):\n        self.idle_frames = []\n        self.sleep_frames = []\n        self.attack_frames = []\n\n        idle_name = name\n        sleep_name = name + 'Sleep'\n        attack_name = name + 'Attack'\n\n        frame_list = [self.idle_frames, self.sleep_frames, self.attack_frames]\n        name_list = [idle_name, sleep_name, attack_name]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.idle_frames\n\n    def canAttack(self, zombie):\n        if (zombie.name == c.SNORKELZOMBIE) and (\n            zombie.frames == zombie.swim_frames\n        ):\n            return False\n        if (\n            self.rect.x <= zombie.rect.right\n            and (self.rect.x + c.GRID_X_SIZE * 5 >= zombie.rect.x)\n            and (zombie.rect.left <= c.SCREEN_WIDTH + 10)\n        ):\n            return True\n        return False\n\n    def setAttack(self):\n        self.state = c.ATTACK\n        if self.shoot_timer != 0:\n            self.shoot_timer = self.current_time - 700\n\n    def attacking(self):\n        if self.shoot_timer == 0:\n            self.shoot_timer = self.current_time - 700\n        elif self.current_time - self.shoot_timer >= 1100:\n            if self.show_attack_frames:\n                self.show_attack_frames = False\n                self.changeFrames(self.attack_frames)\n\n        if self.current_time - self.shoot_timer >= 1400:\n            self.bullet_group.add(Fume(self.rect.right - 35, self.rect.y))\n            # 烟雾只是个动画，实际伤害由本身完成\n            for target_zombie in self.zombie_group:\n                if self.canAttack(target_zombie):\n                    target_zombie.setDamage(\n                        c.BULLET_DAMAGE_NORMAL,\n                        damage_type=c.ZOMBIE_RANGE_DAMAGE,\n                    )\n            self.shoot_timer = self.current_time\n            self.show_attack_frames = True\n            # 播放发射音效\n            c.SOUND_FUME.play()\n\n    def animation(self):\n        if (self.current_time - self.animate_timer) > self.animate_interval:\n            self.frame_index += 1\n            if self.frame_index >= self.frame_num:\n                if self.frames == self.attack_frames:\n                    self.changeFrames(self.idle_frames)\n                else:\n                    self.frame_index = 0\n            self.animate_timer = self.current_time\n\n        self.image = self.frames[self.frame_index]\n        self.mask = pg.mask.from_surface(self.image)\n\n        if self.current_time - self.highlight_time < 100:\n            self.image.set_alpha(150)\n        elif (self.current_time - self.hit_timer) < 200:\n            self.image.set_alpha(192)\n        else:\n            self.image.set_alpha(255)\n\n\nclass IceFrozenPlot(Plant):\n    def __init__(self, x, y):\n        Plant.__init__(self, x, y, c.ICEFROZENPLOT, c.INF, None)\n        self.timer = 0\n        self.attack_check = c.CHECK_ATTACK_NEVER\n\n    def idling(self):\n        if self.timer == 0:\n            self.timer = self.current_time\n        elif self.current_time - self.timer >= 30000:\n            self.health = 0\n\n\nclass Garlic(Plant):\n    def __init__(self, x, y):\n        Plant.__init__(self, x, y, c.GARLIC, c.GARLIC_HEALTH, None)\n        self.load_images()\n        self.cracked1 = False\n        self.cracked2 = False\n\n    def load_images(self):\n        self.cracked1_frames = []\n        self.cracked2_frames = []\n\n        cracked1_frames_name = self.name + '_cracked1'\n        cracked2_frames_name = self.name + '_cracked2'\n\n        self.loadFrames(self.cracked1_frames, cracked1_frames_name)\n        self.loadFrames(self.cracked2_frames, cracked2_frames_name)\n\n    def idling(self):\n        if (not self.cracked1) and self.health <= c.GARLIC_CRACKED1_HEALTH:\n            self.changeFrames(self.cracked1_frames)\n            self.cracked1 = True\n        elif (not self.cracked2) and self.health <= c.GARLIC_CRACKED2_HEALTH:\n            self.changeFrames(self.cracked2_frames)\n            self.cracked2 = True\n\n\nclass PumpkinHead(Plant):\n    def __init__(self, x, y):\n        Plant.__init__(self, x, y, c.PUMPKINHEAD, c.WALLNUT_HEALTH, None)\n        self.load_images()\n        self.cracked1 = False\n        self.cracked2 = False\n        self.animate_interval = 160\n        self.attack_check = c.CHECK_ATTACK_NEVER\n\n    def load_images(self):\n        self.cracked1_frames = []\n        self.cracked2_frames = []\n\n        cracked1_frames_name = self.name + '_cracked1'\n        cracked2_frames_name = self.name + '_cracked2'\n\n        self.loadFrames(self.cracked1_frames, cracked1_frames_name)\n        self.loadFrames(self.cracked2_frames, cracked2_frames_name)\n\n    def idling(self):\n        if not self.cracked1 and self.health <= c.WALLNUT_CRACKED1_HEALTH:\n            self.changeFrames(self.cracked1_frames)\n            self.cracked1 = True\n        elif not self.cracked2 and self.health <= c.WALLNUT_CRACKED2_HEALTH:\n            self.changeFrames(self.cracked2_frames)\n            self.cracked2 = True\n\n\nclass GiantWallNut(Plant):\n    def __init__(self, x, y):\n        Plant.__init__(self, x, y, c.GIANTWALLNUT, 1, None)\n        self.init_rect = self.rect.copy()\n        self.rotate_degree = 0\n        self.animate_interval = 200\n        self.move_timer = 0\n        self.move_interval = 70\n        self.vel_x = random.randint(15, 18)\n        self.attack_check = c.CHECK_ATTACK_NEVER\n\n    def idling(self):\n        if self.move_timer == 0:\n            self.move_timer = self.current_time\n        elif (self.current_time - self.move_timer) >= self.move_interval:\n            self.rotate_degree = (self.rotate_degree - 30) % 360\n            self.init_rect.x += self.vel_x\n            if self.init_rect.x > c.SCREEN_WIDTH:\n                self.health = 0\n            self.move_timer += self.move_interval\n\n    def animation(self):\n        image = self.frames[self.frame_index]\n        self.image = pg.transform.rotate(image, self.rotate_degree)\n        self.mask = pg.mask.from_surface(self.image)\n        # must keep the center postion of image when rotate\n        self.rect = self.image.get_rect(center=self.init_rect.center)\n"
  },
  {
    "path": "source/component/zombie.py",
    "content": "import random\n\nimport pygame as pg\n\nfrom .. import constants as c\nfrom .. import tool\n\n\nclass Zombie(pg.sprite.Sprite):\n    def __init__(\n        self,\n        x,\n        y,\n        name,\n        head_group=None,\n        helmet_health=0,\n        helmet_type2_health=0,\n        body_health=c.NORMAL_HEALTH,\n        losthead_health=c.LOSTHEAD_HEALTH,\n        damage=c.ZOMBIE_ATTACK_DAMAGE,\n        can_swim=False,\n    ):\n        pg.sprite.Sprite.__init__(self)\n\n        self.name = name\n        self.frames = []\n        self.frame_index = 0\n        self.loadImages()\n        self.frame_num = len(self.frames)\n\n        self.image = self.frames[self.frame_index]\n        self.rect = self.image.get_rect()\n        self.mask = pg.mask.from_surface(self.image)\n        self.rect.x = x\n        self.rect.bottom = y\n        # 大蒜换行移动像素值，< 0时向上，= 0时不变，> 0时向上\n        self.target_y_change = 0\n        self.original_y = y\n        self.to_change_group = False\n\n        self.helmet_health = helmet_health\n        self.helmet_type2_health = helmet_type2_health\n        self.health = body_health + losthead_health\n        self.losthead_health = losthead_health\n        self.damage = damage\n        self.dead = False\n        self.losthead = False\n        self.can_swim = can_swim\n        self.swimming = False\n        self.helmet = self.helmet_health > 0\n        self.helmet_type2 = self.helmet_type2_health > 0\n        self.head_group = head_group\n\n        self.walk_timer = 0\n        self.animate_timer = 0\n        self.attack_timer = 0\n        self.state = c.WALK\n        self.animate_interval = 150\n        self.walk_animate_interval = 180\n        self.attack_animate_interval = 100\n        self.losthead_animate_interval = 180\n        self.die_animate_interval = 50\n        self.boomDie_animate_interval = 100\n        self.ice_slow_ratio = 1\n        self.ice_slow_timer = 0\n        self.hit_timer = 0\n        self.speed = 1\n        self.freeze_timer = 0\n        self.losthead_timer = 0\n        self.is_hypno = False  # the zombie is hypo and attack other zombies when it ate a HypnoShroom\n\n    def loadFrames(self, frames, name, colorkey=c.BLACK):\n        frame_list = tool.GFX[name]\n        rect = frame_list[0].get_rect()\n        width, height = rect.w, rect.h\n        if name in c.ZOMBIE_RECT:\n            data = c.ZOMBIE_RECT[name]\n            x, width = data['x'], data['width']\n        else:\n            x = 0\n        for frame in frame_list:\n            frames.append(tool.get_image(frame, x, 0, width, height, colorkey))\n\n    def update(self, game_info):\n        self.current_time = game_info[c.CURRENT_TIME]\n        self.handleState()\n        self.updateIceSlow()\n        self.animation()\n\n    def handleState(self):\n        if self.state == c.WALK:\n            self.walking()\n        elif self.state == c.ATTACK:\n            self.attacking()\n        elif self.state == c.DIE:\n            self.dying()\n        elif self.state == c.FREEZE:\n            self.freezing()\n\n    # 濒死状态用函数\n    def checkToDie(self, framesKind):\n        if self.health <= 0:\n            self.setDie()\n            return True\n        elif self.health <= self.losthead_health:\n            if not self.losthead:\n                self.changeFrames(framesKind)\n                self.setLostHead()\n                return True\n            else:\n                self.health -= (self.current_time - self.losthead_timer) / 40\n                self.losthead_timer = self.current_time\n                return False\n        else:\n            return False\n\n    def walking(self):\n        if self.checkToDie(self.losthead_walk_frames):\n            return\n\n        # 能游泳的僵尸\n        if self.can_swim:\n            # 在水池范围内\n            # 在右侧岸左\n            if self.rect.right <= c.MAP_POOL_FRONT_X:\n                # 在左侧岸右，左侧岸位置为预估\n                if self.rect.right - 25 >= c.MAP_POOL_OFFSET_X:\n                    # 还未进入游泳状态\n                    if not self.swimming:\n                        self.swimming = True\n                        self.changeFrames(self.swim_frames)\n                        # 播放入水音效\n                        c.SOUND_ZOMBIE_ENTERING_WATER.play()\n                        # 同样没有兼容双防具\n                        if self.helmet:\n                            if self.helmet_health <= 0:\n                                self.helmet = False\n                            else:\n                                self.changeFrames(self.helmet_swim_frames)\n                        if self.helmet_type2:\n                            if self.helmet_type2_health <= 0:\n                                self.helmet_type2 = False\n                            else:\n                                self.changeFrames(self.helmet_swim_frames)\n                    # 已经进入游泳状态\n                    else:\n                        if self.helmet:\n                            if self.helmet_health <= 0:\n                                self.changeFrames(self.swim_frames)\n                                self.helmet = False\n                        if self.helmet_type2:\n                            if self.helmet_type2_health <= 0:\n                                self.changeFrames(self.swim_frames)\n                                self.helmet_type2 = False\n                # 水生僵尸已经接近家门口并且上岸\n                else:\n                    if self.swimming:\n                        self.changeFrames(self.walk_frames)\n                        self.swimming = False\n                        # 同样没有兼容双防具\n                        if self.helmet:\n                            if self.helmet_health <= 0:\n                                self.helmet = False\n                            else:\n                                self.changeFrames(self.helmet_walk_frames)\n                        if self.helmet_type2:\n                            if self.helmet_type2_health <= 0:\n                                self.helmet_type2 = False\n                            else:\n                                self.changeFrames(self.helmet_walk_frames)\n                    if self.helmet:\n                        if self.helmet_health <= 0:\n                            self.helmet = False\n                            self.changeFrames(self.walk_frames)\n                    if self.helmet_type2:\n                        if self.helmet_type2_health <= 0:\n                            self.helmet_type2 = False\n                            self.changeFrames(self.walk_frames)\n            elif (\n                self.is_hypno and self.rect.right > c.MAP_POOL_FRONT_X + 55\n            ):   # 常数拟合暂时缺乏检验\n                if self.swimming:\n                    self.changeFrames(self.walk_frames)\n                if self.helmet:\n                    if self.helmet_health <= 0:\n                        self.changeFrames(self.walk_frames)\n                        self.helmet = False\n                    elif self.swimming:   # 游泳状态需要改为步行\n                        self.changeFrames(self.helmet_walk_frames)\n                if self.helmet_type2:\n                    if self.helmet_type2_health <= 0:\n                        self.changeFrames(self.walk_frames)\n                        self.helmet_type2 = False\n                    elif self.swimming:   # 游泳状态需要改为步行\n                        self.changeFrames(self.helmet_walk_frames)\n                self.swimming = False\n            # 尚未进入水池\n            else:\n                if self.helmet_health <= 0 and self.helmet:\n                    self.changeFrames(self.walk_frames)\n                    self.helmet = False\n                if self.helmet_type2_health <= 0 and self.helmet_type2:\n                    self.changeFrames(self.walk_frames)\n                    self.helmet_type2 = False\n        # 不能游泳的一般僵尸\n        else:\n            if self.helmet_health <= 0 and self.helmet:\n                self.changeFrames(self.walk_frames)\n                self.helmet = False\n            if self.helmet_type2_health <= 0 and self.helmet_type2:\n                self.changeFrames(self.walk_frames)\n                self.helmet_type2 = False\n\n        if (self.current_time - self.walk_timer) > (\n            c.ZOMBIE_WALK_INTERVAL * self.getTimeRatio()\n        ):\n            self.handleGarlicYChange()\n            self.walk_timer = self.current_time\n            if self.is_hypno:\n                self.rect.x += 1\n            else:\n                self.rect.x -= 1\n\n    def handleGarlicYChange(self):\n        if self.target_y_change < 0:\n            if (\n                self.rect.bottom > self.original_y + self.target_y_change\n            ):  # 注意这里加的是负数\n                self.rect.bottom -= 3\n                # 过半时换行\n                if (self.to_change_group) and (\n                    self.rect.bottom\n                    >= self.original_y + 0.5 * self.target_y_change\n                ):\n                    self.level.zombie_groups[self.map_y].remove(self)\n                    self.level.zombie_groups[self.target_map_y].add(self)\n                    self.to_change_group = False\n            else:\n                self.rect.bottom = self.original_y + self.target_y_change\n                self.original_y = self.rect.bottom\n                self.target_y_change = 0\n        elif self.target_y_change > 0:\n            if (\n                self.rect.bottom < self.original_y + self.target_y_change\n            ):  # 注意这里加的是负数\n                self.rect.bottom += 3\n                # 过半时换行\n                if (self.to_change_group) and (\n                    self.rect.bottom\n                    <= self.original_y + 0.5 * self.target_y_change\n                ):\n                    self.level.zombie_groups[self.map_y].remove(self)\n                    self.level.zombie_groups[self.target_map_y].add(self)\n                    self.to_change_group = False\n            else:\n                self.rect.bottom = self.original_y + self.target_y_change\n                self.original_y = self.rect.bottom\n                self.target_y_change = 0\n\n    def attacking(self):\n        if self.checkToDie(self.losthead_attack_frames):\n            return\n\n        if self.helmet_health <= 0 and self.helmet:\n            self.changeFrames(self.attack_frames)\n            self.helmet = False\n        if self.helmet_type2_health <= 0 and self.helmet_type2:\n            self.changeFrames(self.attack_frames)\n            self.helmet_type2 = False\n            if self.name == c.NEWSPAPER_ZOMBIE:\n                self.speed = 2.65\n                self.walk_animate_interval = 300\n        if (\n            (self.current_time - self.attack_timer)\n            > (c.ATTACK_INTERVAL * self.getAttackTimeRatio())\n        ) and (not self.losthead):\n            if self.prey.health > 0:\n                if self.prey_is_plant:\n                    self.prey.setDamage(self.damage, self)\n                    if self.prey.name == c.GARLIC:\n                        self.setWalk()\n                else:\n                    self.prey.setDamage(self.damage)\n\n                # 播放啃咬音效\n                c.SOUND_ZOMBIE_ATTACKING.play()\n            self.attack_timer = self.current_time\n\n        if self.prey.health <= 0:\n            self.prey = None\n            self.setWalk()\n\n    def dying(self):\n        pass\n\n    def freezing(self):\n        if self.old_state == c.WALK:\n            if self.checkToDie(self.losthead_walk_frames):\n                return\n        else:\n            if self.checkToDie(self.losthead_attack_frames):\n                return\n\n        if (\n            self.current_time - self.freeze_timer\n        ) >= c.MIN_FREEZE_TIME + random.randint(0, 2000):\n            self.setWalk()\n            # 注意寒冰菇解冻后还有减速\n            self.ice_slow_timer = (\n                self.freeze_timer + 10000\n            )   # 每次冰冻冻结 + 减速时间为20 s，而减速有10 s计时，故这里+10 s\n            self.ice_slow_ratio = 2\n\n    def setLostHead(self):\n        self.losthead_timer = self.current_time\n        self.losthead = True\n        self.animate_interval = self.losthead_animate_interval\n        if self.head_group is not None:\n            self.head_group.add(\n                ZombieHead(self.rect.centerx, self.rect.bottom)\n            )\n\n    def changeFrames(self, frames):\n        \"\"\"change image frames and modify rect position\"\"\"\n        self.frames = frames\n        self.frame_num = len(self.frames)\n        self.frame_index = 0\n\n        bottom = self.rect.bottom\n        centerx = self.rect.centerx\n        self.image = self.frames[self.frame_index]\n        self.mask = pg.mask.from_surface(self.image)\n        self.rect = self.image.get_rect()\n        self.rect.bottom = bottom\n        self.rect.centerx = centerx\n\n    def animation(self):\n        if self.state == c.FREEZE:\n            self.image.set_alpha(192)\n            return\n\n        if (self.current_time - self.animate_timer) > (\n            self.animate_interval * self.getTimeRatio()\n        ):\n            self.frame_index += 1\n            if self.frame_index >= self.frame_num:\n                if self.state == c.DIE:\n                    self.kill()\n                    return\n                self.frame_index = 0\n            self.animate_timer = self.current_time\n\n        self.image = self.frames[self.frame_index]\n        if self.is_hypno:\n            self.image = pg.transform.flip(self.image, True, False)\n        self.mask = pg.mask.from_surface(self.image)\n        if (self.current_time - self.hit_timer) >= 200:\n            self.image.set_alpha(255)\n        else:\n            self.image.set_alpha(192)\n\n    def getTimeRatio(self):\n        return (\n            self.ice_slow_ratio / self.speed\n        )   # 目前的机制为：冰冻减速状态与自身速度共同决定行走的时间间隔\n\n    def getAttackTimeRatio(self):\n        return self.ice_slow_ratio  # 攻击速度只取决于冰冻状态\n\n    def setIceSlow(self):\n        # 在转入冰冻减速状态时播放冰冻音效\n        if self.ice_slow_ratio == 1:\n            c.SOUND_FREEZE.play()\n\n        # when get a ice bullet damage, slow the attack or walk speed of the zombie\n        self.ice_slow_timer = self.current_time\n        self.ice_slow_ratio = 2\n\n    def updateIceSlow(self):\n        if self.ice_slow_ratio > 1:\n            if (self.current_time - self.ice_slow_timer) > c.ICE_SLOW_TIME:\n                self.ice_slow_ratio = 1\n\n    def setDamage(\n        self, damage, effect=None, damage_type=c.ZOMBIE_COMMON_DAMAGE\n    ):\n        # 冰冻减速效果\n        if effect == c.BULLET_EFFECT_ICE:\n            if damage_type == c.ZOMBIE_DEAFULT_DAMAGE:   # 寒冰射手不能穿透二类防具进行减速\n                if not self.helmet_type2:\n                    self.setIceSlow()\n            else:\n                self.setIceSlow()\n        # 解冻\n        elif effect == c.BULLET_EFFECT_UNICE:\n            if damage_type == c.ZOMBIE_DEAFULT_DAMAGE:   # 寒冰射手不能穿透二类防具进行减速\n                if not self.helmet_type2:\n                    self.ice_slow_ratio = 1\n            else:\n                self.ice_slow_ratio = 1\n\n        if damage_type == c.ZOMBIE_DEAFULT_DAMAGE:   # 不穿透二类防具的攻击\n            # 从第二类防具开始逐级传递\n            if self.helmet_type2:\n                self.helmet_type2_health -= damage\n                if self.helmet_type2_health <= 0:\n                    if self.helmet:\n                        self.helmet_health += (\n                            self.helmet_type2_health\n                        )   # 注意self.helmet_type2_health已经带有正负\n                        self.helmet_type2_health = 0  # 注意合并后清零\n                        if self.helmet_health <= 0:\n                            self.health += self.helmet_health\n                            self.helmet_health = 0   # 注意合并后清零\n                    else:\n                        self.health += self.helmet_type2_health\n                        self.helmet_type2_health = 0\n            elif self.helmet:   # 不存在二类防具，但是存在一类防具\n                self.helmet_health -= damage\n                if self.helmet_health <= 0:\n                    self.health += self.helmet_health\n                    self.helmet_health = 0   # 注意合并后清零\n            else:   # 没有防具\n                self.health -= damage\n        elif damage_type == c.ZOMBIE_COMMON_DAMAGE:  # 无视二类防具，将攻击一类防具与本体视为整体的攻击\n            if self.helmet:   # 存在一类防具\n                self.helmet_health -= damage\n                if self.helmet_health <= 0:\n                    self.health += self.helmet_health\n                    self.helmet_health = 0   # 注意合并后清零\n            else:   # 没有一类防具\n                self.health -= damage\n        elif damage_type == c.ZOMBIE_RANGE_DAMAGE:\n            # 从第二类防具开始逐级传递\n            if self.helmet_type2:\n                self.helmet_type2_health -= damage\n                if self.helmet_type2_health <= 0:\n                    if self.helmet:\n                        self.helmet_health -= damage   # 注意范围伤害中这里还有一个攻击\n                        self.helmet_health += (\n                            self.helmet_type2_health\n                        )   # 注意self.helmet_type2_health已经带有正负\n                        self.helmet_type2_health = 0  # 注意合并后清零\n                        if self.helmet_health <= 0:\n                            self.health += self.helmet_health\n                            self.helmet_health = 0   # 注意合并后清零\n                    else:\n                        self.health -= damage   # 注意范围伤害中这里还有一个攻击\n                        self.health += self.helmet_type2_health\n                        self.helmet_type2_health = 0\n                else:\n                    if self.helmet:\n                        self.helmet_health -= damage\n                        if self.helmet_health <= 0:\n                            self.health += self.helmet_health\n                            self.helmet_health = 0   # 注意合并后清零\n                    else:\n                        self.health -= damage\n            elif self.helmet:   # 不存在二类防具，但是存在一类防具\n                self.helmet_health -= damage\n                if self.helmet_health <= 0:\n                    self.health += self.helmet_health\n                    self.helmet_health = 0   # 注意合并后清零\n            else:   # 没有防具\n                self.health -= damage\n        elif damage_type == c.ZOMBIE_ASH_DAMAGE:\n            self.health -= damage   # 无视任何防具\n        elif damage_type == c.ZOMBIE_WALLNUT_BOWLING_DANMAGE:\n            # 逻辑：对防具的多余伤害不传递\n            if self.helmet_type2:\n                # 对二类防具伤害较一般情况低，拟合铁门需要砸3次的设定\n                self.helmet_type2_health -= int(damage * 0.8)\n            elif self.helmet:   # 不存在二类防具，但是存在一类防具\n                self.helmet_health -= damage\n            else:   # 没有防具\n                self.health -= damage\n        else:\n            print('警告：植物攻击类型错误，现在默认进行类豌豆射手型攻击')\n            self.setDamage(\n                damage, effect=effect, damage_type=c.ZOMBIE_DEAFULT_DAMAGE\n            )\n\n        # 记录攻击时间\n        self.hit_timer = self.current_time\n\n    def setWalk(self):\n        self.state = c.WALK\n        self.animate_interval = self.walk_animate_interval\n\n        if self.helmet or self.helmet_type2:   # 这里暂时没有考虑同时有两种防具的僵尸\n            self.changeFrames(self.helmet_walk_frames)\n        elif self.losthead:\n            self.changeFrames(self.losthead_walk_frames)\n        else:\n            self.changeFrames(self.walk_frames)\n\n        if self.can_swim:\n            if self.rect.right <= c.MAP_POOL_FRONT_X:\n                self.swimming = True\n                self.changeFrames(self.swim_frames)\n                # 同样没有兼容双防具\n                if self.helmet:\n                    if self.helmet_health <= 0:\n                        self.changeFrames(self.swim_frames)\n                        self.helmet = False\n                    else:\n                        self.changeFrames(self.helmet_swim_frames)\n                if self.helmet_type2:\n                    if self.helmet_type2_health <= 0:\n                        self.changeFrames(self.swim_frames)\n                        self.helmet_type2 = False\n                    else:\n                        self.changeFrames(self.helmet_swim_frames)\n\n    def setAttack(self, prey, is_plant=True):\n        self.prey = prey  # prey can be plant or other zombies\n        self.prey_is_plant = is_plant\n        self.state = c.ATTACK\n        self.attack_timer = self.current_time\n        self.animate_interval = self.attack_animate_interval\n\n        if self.helmet or self.helmet_type2:   # 这里暂时没有考虑同时有两种防具的僵尸\n            self.changeFrames(self.helmet_attack_frames)\n        elif self.losthead:\n            self.changeFrames(self.losthead_attack_frames)\n        else:\n            self.changeFrames(self.attack_frames)\n\n    def setDie(self):\n        self.state = c.DIE\n        self.animate_interval = self.die_animate_interval\n        self.changeFrames(self.die_frames)\n\n    def setBoomDie(self):\n        self.health = 0\n        self.state = c.DIE\n        self.animate_interval = self.boomDie_animate_interval\n        self.changeFrames(self.boomdie_frames)\n\n    def setFreeze(self, ice_trap_image):\n        self.old_state = self.state\n        self.state = c.FREEZE\n        self.freeze_timer = self.current_time\n        self.ice_trap_image = ice_trap_image\n        self.ice_trap_rect = ice_trap_image.get_rect()\n        self.ice_trap_rect.centerx = self.rect.centerx\n        self.ice_trap_rect.bottom = self.rect.bottom\n\n    def drawFreezeTrap(self, surface):\n        if self.state == c.FREEZE:\n            surface.blit(self.ice_trap_image, self.ice_trap_rect)\n\n    def setHypno(self):\n        self.is_hypno = True\n        self.setWalk()\n        # 播放魅惑音效\n        c.SOUND_HYPNOED.play()\n\n\nclass ZombieHead(Zombie):\n    def __init__(self, x, y):\n        Zombie.__init__(self, x, y, c.ZOMBIE_HEAD, 0)\n        self.state = c.DIE\n\n    def loadImages(self):\n        self.die_frames = []\n        die_name = self.name\n        self.loadFrames(self.die_frames, die_name)\n        self.frames = self.die_frames\n\n    def setWalk(self):\n        self.animate_interval = 100\n\n\nclass NormalZombie(Zombie):\n    def __init__(self, x, y, head_group):\n        Zombie.__init__(self, x, y, c.NORMAL_ZOMBIE, head_group)\n\n    def loadImages(self):\n        self.walk_frames = []\n        self.attack_frames = []\n        self.losthead_walk_frames = []\n        self.losthead_attack_frames = []\n        self.die_frames = []\n        self.boomdie_frames = []\n\n        walk_name = self.name\n        attack_name = self.name + 'Attack'\n        losthead_walk_name = self.name + 'LostHead'\n        losthead_attack_name = self.name + 'LostHeadAttack'\n        die_name = self.name + 'Die'\n        boomdie_name = c.BOOMDIE\n\n        frame_list = [\n            self.walk_frames,\n            self.attack_frames,\n            self.losthead_walk_frames,\n            self.losthead_attack_frames,\n            self.die_frames,\n            self.boomdie_frames,\n        ]\n        name_list = [\n            walk_name,\n            attack_name,\n            losthead_walk_name,\n            losthead_attack_name,\n            die_name,\n            boomdie_name,\n        ]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.walk_frames\n\n\n# 路障僵尸\nclass ConeHeadZombie(Zombie):\n    def __init__(self, x, y, head_group):\n        Zombie.__init__(\n            self,\n            x,\n            y,\n            c.CONEHEAD_ZOMBIE,\n            head_group,\n            helmet_health=c.CONEHEAD_HEALTH,\n        )\n\n    def loadImages(self):\n        self.helmet_walk_frames = []\n        self.helmet_attack_frames = []\n        self.walk_frames = []\n        self.attack_frames = []\n        self.losthead_walk_frames = []\n        self.losthead_attack_frames = []\n        self.die_frames = []\n        self.boomdie_frames = []\n\n        helmet_walk_name = self.name\n        helmet_attack_name = self.name + 'Attack'\n        walk_name = c.NORMAL_ZOMBIE\n        attack_name = c.NORMAL_ZOMBIE + 'Attack'\n        losthead_walk_name = c.NORMAL_ZOMBIE + 'LostHead'\n        losthead_attack_name = c.NORMAL_ZOMBIE + 'LostHeadAttack'\n        die_name = c.NORMAL_ZOMBIE + 'Die'\n        boomdie_name = c.BOOMDIE\n\n        frame_list = [\n            self.helmet_walk_frames,\n            self.helmet_attack_frames,\n            self.walk_frames,\n            self.attack_frames,\n            self.losthead_walk_frames,\n            self.losthead_attack_frames,\n            self.die_frames,\n            self.boomdie_frames,\n        ]\n        name_list = [\n            helmet_walk_name,\n            helmet_attack_name,\n            walk_name,\n            attack_name,\n            losthead_walk_name,\n            losthead_attack_name,\n            die_name,\n            boomdie_name,\n        ]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.helmet_walk_frames\n\n\nclass BucketHeadZombie(Zombie):\n    def __init__(self, x, y, head_group):\n        Zombie.__init__(\n            self,\n            x,\n            y,\n            c.BUCKETHEAD_ZOMBIE,\n            head_group,\n            helmet_health=c.BUCKETHEAD_HEALTH,\n        )\n\n    def loadImages(self):\n        self.helmet_walk_frames = []\n        self.helmet_attack_frames = []\n        self.walk_frames = []\n        self.attack_frames = []\n        self.losthead_walk_frames = []\n        self.losthead_attack_frames = []\n        self.die_frames = []\n        self.boomdie_frames = []\n\n        helmet_walk_name = self.name\n        helmet_attack_name = self.name + 'Attack'\n        walk_name = c.NORMAL_ZOMBIE\n        attack_name = c.NORMAL_ZOMBIE + 'Attack'\n        losthead_walk_name = c.NORMAL_ZOMBIE + 'LostHead'\n        losthead_attack_name = c.NORMAL_ZOMBIE + 'LostHeadAttack'\n        die_name = c.NORMAL_ZOMBIE + 'Die'\n        boomdie_name = c.BOOMDIE\n\n        frame_list = [\n            self.helmet_walk_frames,\n            self.helmet_attack_frames,\n            self.walk_frames,\n            self.attack_frames,\n            self.losthead_walk_frames,\n            self.losthead_attack_frames,\n            self.die_frames,\n            self.boomdie_frames,\n        ]\n        name_list = [\n            helmet_walk_name,\n            helmet_attack_name,\n            walk_name,\n            attack_name,\n            losthead_walk_name,\n            losthead_attack_name,\n            die_name,\n            boomdie_name,\n        ]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.helmet_walk_frames\n\n\nclass FlagZombie(Zombie):\n    def __init__(self, x, y, head_group):\n        Zombie.__init__(self, x, y, c.FLAG_ZOMBIE, head_group)\n        self.speed = 1.25\n\n    def loadImages(self):\n        self.walk_frames = []\n        self.attack_frames = []\n        self.losthead_walk_frames = []\n        self.losthead_attack_frames = []\n        self.die_frames = []\n        self.boomdie_frames = []\n\n        walk_name = self.name\n        attack_name = self.name + 'Attack'\n        losthead_walk_name = self.name + 'LostHead'\n        losthead_attack_name = self.name + 'LostHeadAttack'\n        die_name = c.NORMAL_ZOMBIE + 'Die'\n        boomdie_name = c.BOOMDIE\n\n        frame_list = [\n            self.walk_frames,\n            self.attack_frames,\n            self.losthead_walk_frames,\n            self.losthead_attack_frames,\n            self.die_frames,\n            self.boomdie_frames,\n        ]\n        name_list = [\n            walk_name,\n            attack_name,\n            losthead_walk_name,\n            losthead_attack_name,\n            die_name,\n            boomdie_name,\n        ]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.walk_frames\n\n\nclass NewspaperZombie(Zombie):\n    def __init__(self, x, y, head_group):\n        Zombie.__init__(\n            self,\n            x,\n            y,\n            c.NEWSPAPER_ZOMBIE,\n            head_group,\n            helmet_type2_health=c.NEWSPAPER_HEALTH,\n        )\n        self.speed_up = False\n\n    def loadImages(self):\n        self.helmet_walk_frames = []\n        self.helmet_attack_frames = []\n        self.walk_frames = []\n        self.attack_frames = []\n        self.losthead_walk_frames = []\n        self.losthead_attack_frames = []\n        self.lostnewspaper_frames = []\n        self.die_frames = []\n        self.boomdie_frames = []\n\n        helmet_walk_name = self.name\n        helmet_attack_name = self.name + 'Attack'\n        walk_name = self.name + 'NoPaper'\n        attack_name = self.name + 'NoPaperAttack'\n        losthead_walk_name = self.name + 'LostHead'\n        losthead_attack_name = self.name + 'LostHeadAttack'\n        lostnewspaper_name = self.name + 'LostNewspaper'\n        die_name = self.name + 'Die'\n        boomdie_name = c.BOOMDIE\n\n        frame_list = [\n            self.helmet_walk_frames,\n            self.helmet_attack_frames,\n            self.walk_frames,\n            self.attack_frames,\n            self.losthead_walk_frames,\n            self.losthead_attack_frames,\n            self.lostnewspaper_frames,\n            self.die_frames,\n            self.boomdie_frames,\n        ]\n        name_list = [\n            helmet_walk_name,\n            helmet_attack_name,\n            walk_name,\n            attack_name,\n            losthead_walk_name,\n            losthead_attack_name,\n            lostnewspaper_name,\n            die_name,\n            boomdie_name,\n        ]\n\n        for i, name in enumerate(name_list):\n            if name in {c.BOOMDIE, lostnewspaper_name}:\n                color = c.BLACK\n            else:\n                color = c.WHITE\n            self.loadFrames(frame_list[i], name, color)\n\n        self.frames = self.helmet_walk_frames\n\n    def walking(self):\n        if self.checkToDie(self.losthead_walk_frames):\n            return\n\n        if self.helmet_type2_health <= 0 and self.helmet_type2:\n            self.changeFrames(self.lostnewspaper_frames)\n            self.helmet_type2 = False\n            # 触发报纸撕裂音效\n            c.SOUND_NEWSPAPER_RIP.play()\n        if (self.current_time - self.walk_timer) > (\n            c.ZOMBIE_WALK_INTERVAL * self.getTimeRatio()\n        ):\n            self.handleGarlicYChange()\n            self.walk_timer = self.current_time\n            if self.frames == self.lostnewspaper_frames:\n                pass\n            elif self.is_hypno:\n                self.rect.x += 1\n            else:\n                self.rect.x -= 1\n\n    def animation(self):\n        if self.state == c.FREEZE:\n            self.image.set_alpha(192)\n            return\n\n        if (self.current_time - self.animate_timer) > (\n            self.animate_interval * self.getTimeRatio()\n        ):\n            self.frame_index += 1\n            if self.frame_index >= self.frame_num:\n                if self.state == c.DIE:\n                    self.kill()\n                    return\n                elif self.frames == self.lostnewspaper_frames and (\n                    not self.speed_up\n                ):\n                    self.changeFrames(self.walk_frames)\n                    self.speed_up = True\n                    self.speed = 2.65\n                    self.walk_animate_interval = 300\n                    # 触发报纸僵尸暴走音效\n                    c.SOUND_NEWSPAPER_ZOMBIE_ANGRY.play()\n                    return\n                self.frame_index = 0\n            self.animate_timer = self.current_time\n\n        self.image = self.frames[self.frame_index]\n        if self.is_hypno:\n            self.image = pg.transform.flip(self.image, True, False)\n        self.mask = pg.mask.from_surface(self.image)\n        if (self.current_time - self.hit_timer) >= 200:\n            self.image.set_alpha(255)\n        else:\n            self.image.set_alpha(192)\n\n\nclass FootballZombie(Zombie):\n    def __init__(self, x, y, head_group):\n        Zombie.__init__(\n            self,\n            x,\n            y,\n            c.FOOTBALL_ZOMBIE,\n            head_group,\n            helmet_health=c.FOOTBALL_HELMET_HEALTH,\n        )\n        self.speed = 1.88\n        self.animate_interval = 50\n        self.walk_animate_interval = 50\n        self.attack_animate_interval = 60\n        self.losthead_animate_interval = 180\n        self.die_animate_interval = 150\n\n    def loadImages(self):\n        self.helmet_walk_frames = []\n        self.helmet_attack_frames = []\n        self.walk_frames = []\n        self.attack_frames = []\n        self.losthead_walk_frames = []\n        self.losthead_attack_frames = []\n        self.die_frames = []\n        self.boomdie_frames = []\n\n        helmet_walk_name = self.name\n        helmet_attack_name = self.name + 'Attack'\n        walk_name = self.name + 'LostHelmet'\n        attack_name = self.name + 'LostHelmetAttack'\n        losthead_walk_name = self.name + 'LostHead'\n        losthead_attack_name = self.name + 'LostHeadAttack'\n        die_name = self.name + 'Die'\n        boomdie_name = c.BOOMDIE\n\n        frame_list = [\n            self.helmet_walk_frames,\n            self.helmet_attack_frames,\n            self.walk_frames,\n            self.attack_frames,\n            self.losthead_walk_frames,\n            self.losthead_attack_frames,\n            self.die_frames,\n            self.boomdie_frames,\n        ]\n        name_list = [\n            helmet_walk_name,\n            helmet_attack_name,\n            walk_name,\n            attack_name,\n            losthead_walk_name,\n            losthead_attack_name,\n            die_name,\n            boomdie_name,\n        ]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.helmet_walk_frames\n\n\nclass DuckyTubeZombie(Zombie):\n    def __init__(self, x, y, head_group):\n        Zombie.__init__(\n            self, x, y, c.DUCKY_TUBE_ZOMBIE, head_group, can_swim=True\n        )\n\n    def loadImages(self):\n        self.walk_frames = []\n        self.swim_frames = []\n        self.attack_frames = []\n        self.losthead_walk_frames = []\n        self.losthead_attack_frames = []\n        self.die_frames = []\n        self.boomdie_frames = []\n\n        walk_name = self.name\n        swim_name = self.name + 'Swim'\n        attack_name = self.name + 'Attack'\n        losthead_walk_name = self.name + 'LostHead'\n        losthead_attack_name = self.name + 'LostHead'\n        die_name = self.name + 'Die'\n        boomdie_name = c.BOOMDIE\n\n        frame_list = [\n            self.walk_frames,\n            self.swim_frames,\n            self.attack_frames,\n            self.losthead_walk_frames,\n            self.losthead_attack_frames,\n            self.die_frames,\n            self.boomdie_frames,\n        ]\n        name_list = [\n            walk_name,\n            swim_name,\n            attack_name,\n            losthead_walk_name,\n            losthead_attack_name,\n            die_name,\n            boomdie_name,\n        ]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.walk_frames\n\n\nclass ConeHeadDuckyTubeZombie(Zombie):\n    def __init__(self, x, y, head_group):\n        Zombie.__init__(\n            self,\n            x,\n            y,\n            c.CONEHEAD_DUCKY_TUBE_ZOMBIE,\n            head_group,\n            helmet_health=c.CONEHEAD_HEALTH,\n            can_swim=True,\n        )\n\n    def loadImages(self):\n        self.helmet_walk_frames = []\n        self.walk_frames = []\n        self.helmet_swim_frames = []\n        self.swim_frames = []\n        self.helmet_attack_frames = []\n        self.attack_frames = []\n        self.losthead_walk_frames = []\n        self.losthead_attack_frames = []\n        self.die_frames = []\n        self.boomdie_frames = []\n\n        helmet_walk_name = self.name\n        helmet_swim_name = self.name + 'Swim'\n        helmet_attack_name = self.name + 'Attack'\n        walk_name = c.DUCKY_TUBE_ZOMBIE\n        swim_name = c.DUCKY_TUBE_ZOMBIE + 'Swim'\n        attack_name = c.DUCKY_TUBE_ZOMBIE + 'Attack'\n        losthead_walk_name = c.DUCKY_TUBE_ZOMBIE + 'LostHead'\n        losthead_attack_name = c.DUCKY_TUBE_ZOMBIE + 'LostHead'\n        die_name = c.DUCKY_TUBE_ZOMBIE + 'Die'\n        boomdie_name = c.BOOMDIE\n\n        frame_list = [\n            self.helmet_walk_frames,\n            self.helmet_swim_frames,\n            self.helmet_attack_frames,\n            self.walk_frames,\n            self.swim_frames,\n            self.attack_frames,\n            self.losthead_walk_frames,\n            self.losthead_attack_frames,\n            self.die_frames,\n            self.boomdie_frames,\n        ]\n        name_list = [\n            helmet_walk_name,\n            helmet_swim_name,\n            helmet_attack_name,\n            walk_name,\n            swim_name,\n            attack_name,\n            losthead_walk_name,\n            losthead_attack_name,\n            die_name,\n            boomdie_name,\n        ]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.helmet_walk_frames\n\n\nclass BucketHeadDuckyTubeZombie(Zombie):\n    def __init__(self, x, y, head_group):\n        Zombie.__init__(\n            self,\n            x,\n            y,\n            c.BUCKETHEAD_DUCKY_TUBE_ZOMBIE,\n            head_group,\n            helmet_health=c.BUCKETHEAD_HEALTH,\n            can_swim=True,\n        )\n\n    def loadImages(self):\n        self.helmet_walk_frames = []\n        self.walk_frames = []\n        self.helmet_swim_frames = []\n        self.swim_frames = []\n        self.helmet_attack_frames = []\n        self.attack_frames = []\n        self.losthead_walk_frames = []\n        self.losthead_attack_frames = []\n        self.die_frames = []\n        self.boomdie_frames = []\n\n        helmet_walk_name = self.name\n        helmet_swim_name = self.name + 'Swim'\n        helmet_attack_name = self.name + 'Attack'\n        walk_name = c.DUCKY_TUBE_ZOMBIE\n        swim_name = c.DUCKY_TUBE_ZOMBIE + 'Swim'\n        attack_name = c.DUCKY_TUBE_ZOMBIE + 'Attack'\n        losthead_walk_name = c.DUCKY_TUBE_ZOMBIE + 'LostHead'\n        losthead_attack_name = c.DUCKY_TUBE_ZOMBIE + 'LostHead'\n        die_name = c.DUCKY_TUBE_ZOMBIE + 'Die'\n        boomdie_name = c.BOOMDIE\n\n        frame_list = [\n            self.helmet_walk_frames,\n            self.helmet_swim_frames,\n            self.helmet_attack_frames,\n            self.walk_frames,\n            self.swim_frames,\n            self.attack_frames,\n            self.losthead_walk_frames,\n            self.losthead_attack_frames,\n            self.die_frames,\n            self.boomdie_frames,\n        ]\n        name_list = [\n            helmet_walk_name,\n            helmet_swim_name,\n            helmet_attack_name,\n            walk_name,\n            swim_name,\n            attack_name,\n            losthead_walk_name,\n            losthead_attack_name,\n            die_name,\n            boomdie_name,\n        ]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.helmet_walk_frames\n\n\nclass ScreenDoorZombie(Zombie):\n    def __init__(self, x, y, head_group):\n        Zombie.__init__(\n            self,\n            x,\n            y,\n            c.SCREEN_DOOR_ZOMBIE,\n            head_group,\n            helmet_type2_health=c.SCREEN_DOOR_HEALTH,\n        )\n\n    def loadImages(self):\n        self.helmet_walk_frames = []\n        self.helmet_attack_frames = []\n        self.walk_frames = []\n        self.attack_frames = []\n        self.losthead_walk_frames = []\n        self.losthead_attack_frames = []\n        self.die_frames = []\n        self.boomdie_frames = []\n\n        helmet_walk_name = self.name\n        helmet_attack_name = self.name + 'Attack'\n        walk_name = c.NORMAL_ZOMBIE\n        attack_name = c.NORMAL_ZOMBIE + 'Attack'\n        losthead_walk_name = c.NORMAL_ZOMBIE + 'LostHead'\n        losthead_attack_name = c.NORMAL_ZOMBIE + 'LostHeadAttack'\n        die_name = c.NORMAL_ZOMBIE + 'Die'\n        boomdie_name = c.BOOMDIE\n\n        frame_list = [\n            self.helmet_walk_frames,\n            self.helmet_attack_frames,\n            self.walk_frames,\n            self.attack_frames,\n            self.losthead_walk_frames,\n            self.losthead_attack_frames,\n            self.die_frames,\n            self.boomdie_frames,\n        ]\n        name_list = [\n            helmet_walk_name,\n            helmet_attack_name,\n            walk_name,\n            attack_name,\n            losthead_walk_name,\n            losthead_attack_name,\n            die_name,\n            boomdie_name,\n        ]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.helmet_walk_frames\n\n\nclass PoleVaultingZombie(Zombie):\n    def __init__(self, x, y, head_group):\n        Zombie.__init__(\n            self,\n            x,\n            y,\n            c.POLE_VAULTING_ZOMBIE,\n            head_group=head_group,\n            body_health=c.POLE_VAULTING_HEALTH,\n            losthead_health=c.POLE_VAULTING_LOSTHEAD_HEALTH,\n        )\n        self.speed = 1.88\n        self.jumped = False\n        self.jumping = False\n\n    def loadImages(self):\n        self.walk_frames = []\n        self.attack_frames = []\n        self.losthead_walk_frames = []\n        self.losthead_attack_frames = []\n        self.die_frames = []\n        self.boomdie_frames = []\n        self.walk_before_jump_frames = []\n        self.jump_frames = []\n\n        walk_name = self.name + 'WalkAfterJump'\n        attack_name = self.name + 'Attack'\n        losthead_walk_name = self.name + 'LostHead'\n        losthead_attack_name = self.name + 'LostHeadAttack'\n        die_name = self.name + 'Die'\n        boomdie_name = c.BOOMDIE\n        walk_before_jump_name = self.name\n        jump_name = self.name + 'Jump'\n\n        frame_list = [\n            self.walk_frames,\n            self.attack_frames,\n            self.losthead_walk_frames,\n            self.losthead_attack_frames,\n            self.die_frames,\n            self.boomdie_frames,\n            self.walk_before_jump_frames,\n            self.jump_frames,\n        ]\n        name_list = [\n            walk_name,\n            attack_name,\n            losthead_walk_name,\n            losthead_attack_name,\n            die_name,\n            boomdie_name,\n            walk_before_jump_name,\n            jump_name,\n        ]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.walk_before_jump_frames\n\n    def setJump(self, successfullyJumped, jump_x):\n        if not self.jumping:\n            self.jumping = True\n            self.changeFrames(self.jump_frames)\n            self.successfullyJumped = successfullyJumped\n            self.jump_x = jump_x\n            # 播放跳跃音效\n            c.SOUND_POLEVAULT_JUMP.play()\n\n    def animation(self):\n        if self.state == c.FREEZE:\n            self.image.set_alpha(192)\n            return\n\n        if (self.current_time - self.animate_timer) > (\n            self.animate_interval * self.getTimeRatio()\n        ):\n            self.frame_index += 1\n            if self.state == c.WALK:\n                if self.jumping and (not self.jumped):\n                    if self.successfullyJumped:\n                        self.rect.x -= 5\n                    else:\n                        self.rect.x -= 1\n            if self.frame_index >= self.frame_num:\n                if self.state == c.DIE:\n                    self.kill()\n                    return\n                self.frame_index = 0\n                if self.jumping and (not self.jumped):\n                    self.changeFrames(self.walk_frames)\n                    if self.successfullyJumped:\n                        self.rect.centerx = self.jump_x\n                    self.jumped = True\n                    self.speed = 1.04\n            self.animate_timer = self.current_time\n\n        self.image = self.frames[self.frame_index]\n        if self.is_hypno:\n            self.image = pg.transform.flip(self.image, True, False)\n        self.mask = pg.mask.from_surface(self.image)\n        if (self.current_time - self.hit_timer) >= 200:\n            self.image.set_alpha(255)\n        else:\n            self.image.set_alpha(192)\n\n    def setWalk(self):\n        self.state = c.WALK\n        self.animate_interval = self.walk_animate_interval\n        if self.jumped:\n            self.changeFrames(self.walk_frames)\n\n    def setFreeze(self, ice_trap_image):\n        # 起跳但是没有落地时不设置冰冻\n        if self.jumping and (not self.jumped):\n            self.ice_slow_timer = self.current_time\n            self.ice_slow_ratio = 2\n        else:\n            self.freeze_timer = self.current_time\n            self.old_state = self.state\n            self.state = c.FREEZE\n            self.ice_trap_image = ice_trap_image\n            self.ice_trap_rect = ice_trap_image.get_rect()\n            self.ice_trap_rect.centerx = self.rect.centerx\n            self.ice_trap_rect.bottom = self.rect.bottom\n\n\n# 注意：冰车僵尸移动变速\nclass Zomboni(Zombie):\n    def __init__(self, x, y, plant_group, map, IceFrozenPlot):\n        Zombie.__init__(self, x, y, c.ZOMBONI, body_health=c.ZOMBONI_HEALTH)\n        self.plant_group = plant_group\n        self.map = map\n        self.IceFrozenPlot = IceFrozenPlot\n        self.die_animate_interval = 70\n        self.boomDie_animate_interval = 150\n        # 播放冰车生成音效\n        c.SOUND_ZOMBONI.play()\n\n    def loadImages(self):\n        self.walk_frames = []\n        self.walk_damaged1_frames = []\n        self.walk_damaged2_frames = []\n        self.losthead_walk_frames = []\n        self.die_frames = []\n        self.boomdie_frames = []\n\n        walk_name = self.name\n        walk_damaged1_name = self.name + 'Damaged1'\n        walk_damaged2_name = self.name + 'Damaged2'\n        losthead_walk_name = self.name + 'Damaged2'\n        die_name = self.name + 'Die'\n        boomdie_name = self.name + 'BoomDie'\n\n        frame_list = [\n            self.walk_frames,\n            self.walk_damaged1_frames,\n            self.walk_damaged2_frames,\n            self.losthead_walk_frames,\n            self.die_frames,\n            self.boomdie_frames,\n        ]\n        name_list = [\n            walk_name,\n            walk_damaged1_name,\n            walk_damaged2_name,\n            losthead_walk_name,\n            die_name,\n            boomdie_name,\n        ]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.walk_frames\n\n    def updateIceSlow(self):\n        # 冰车僵尸不可冰冻\n        self.ice_slow_ratio = 1\n\n    def setFreeze(self, ice_trap_image):\n        pass\n\n    def walking(self):\n        if self.checkToDie(self.losthead_walk_frames):\n            return\n\n        if self.health <= c.ZOMBONI_DAMAGED2_HEALTH:\n            self.changeFrames(self.walk_damaged2_frames)\n        elif self.health <= c.ZOMBONI_DAMAGED1_HEALTH:\n            self.changeFrames(self.walk_damaged1_frames)\n\n        if (self.current_time - self.walk_timer) > (\n            c.ZOMBIE_WALK_INTERVAL * self.getTimeRatio()\n        ) and (not self.losthead):\n            self.walk_timer = self.current_time\n            if self.is_hypno:\n                self.rect.x += 1\n            else:\n                self.rect.x -= 1\n\n            # 行进时碾压\n            for plant in self.plant_group:\n                # 地刺和地刺王不用检验\n                if (plant.name not in {c.SPIKEWEED}) and (\n                    self.rect.centerx <= plant.rect.right <= self.rect.right\n                ):\n                    # 扣除生命值为可能的最大有限生命值\n                    plant.health -= 8000\n\n            # 造冰\n            map_x, map_y = self.map.getMapIndex(\n                self.rect.right - 40, self.rect.bottom\n            )\n            if 0 <= map_x < c.GRID_X_LEN:\n                if c.ICEFROZENPLOT not in self.map.map[map_y][map_x]:\n                    x, y = self.map.getMapGridPos(map_x, map_y)\n                    self.plant_group.add(self.IceFrozenPlot(x, y))\n                    self.map.map[map_y][map_x][c.MAP_PLANT].add(\n                        c.ICEFROZENPLOT\n                    )\n\n            self.speed = max(0.6, 1.5 - (c.GRID_X_LEN + 1 - map_x) * 0.225)\n\n    def setDie(self):\n        self.state = c.DIE\n        self.animate_interval = self.die_animate_interval\n        self.changeFrames(self.die_frames)\n        # 播放冰车爆炸音效\n        c.SOUND_ZOMBONI_EXPLOSION.play()\n\n\nclass SnorkelZombie(Zombie):\n    def __init__(self, x, y, head_group):\n        Zombie.__init__(self, x, y, c.SNORKELZOMBIE, can_swim=True)\n        self.speed = 1.6\n        self.walk_animate_interval = 50\n        self.canSetAttack = True\n\n    def loadImages(self):\n        self.walk_frames = []\n        self.swim_frames = []\n        self.attack_frames = []\n        self.jump_frames = []\n        self.float_frames = []\n        self.sink_frames = []\n        self.losthead_walk_frames = []\n        self.losthead_attack_frames = []\n        self.die_frames = []\n        self.boomdie_frames = []\n\n        walk_name = self.name\n        swim_name = self.name + 'Dive'\n        attack_name = self.name + 'Attack'\n        jump_name = self.name + 'Jump'\n        float_name = self.name + 'Float'\n        sink_name = self.name + 'Sink'\n        losthead_walk_name = self.name + 'LostHead'\n        losthead_attack_name = self.name + 'LostHeadAttack'\n        die_name = self.name + 'Die'\n        boomdie_name = c.BOOMDIE\n\n        frame_list = [\n            self.walk_frames,\n            self.swim_frames,\n            self.attack_frames,\n            self.jump_frames,\n            self.float_frames,\n            self.sink_frames,\n            self.losthead_walk_frames,\n            self.losthead_attack_frames,\n            self.die_frames,\n            self.boomdie_frames,\n        ]\n        name_list = [\n            walk_name,\n            swim_name,\n            attack_name,\n            jump_name,\n            float_name,\n            sink_name,\n            losthead_walk_name,\n            losthead_attack_name,\n            die_name,\n            boomdie_name,\n        ]\n\n        for i, name in enumerate(name_list):\n            self.loadFrames(frame_list[i], name)\n\n        self.frames = self.walk_frames\n\n    def walking(self):\n        if self.checkToDie(self.losthead_walk_frames):\n            return\n\n        # 在水池范围内\n        # 在右侧岸左\n        if self.rect.centerx <= c.MAP_POOL_FRONT_X - 25:\n            # 在左侧岸右，左侧岸位置为预估\n            if self.rect.right - 25 >= c.MAP_POOL_OFFSET_X:\n                # 还未进入游泳状态\n                if not self.swimming:\n                    self.swimming = True\n                    self.changeFrames(self.jump_frames)\n                    self.speed = 1.175\n                    # 播放入水音效\n                    c.SOUND_ZOMBIE_ENTERING_WATER.play()\n            # 已经接近家门口并且上岸\n            else:\n                if self.swimming:\n                    self.changeFrames(self.walk_frames)\n                    self.speed = 1.6\n                    self.swimming = False\n        # 被魅惑时走到岸上需要起立\n        elif self.is_hypno and (\n            self.rect.right > c.MAP_POOL_FRONT_X + 55\n        ):   # 常数拟合暂时缺乏检验\n            if self.swimming:\n                self.speed = 1.6\n                self.changeFrames(self.walk_frames)\n            self.swimming = False\n        if (self.current_time - self.walk_timer) > (\n            c.ZOMBIE_WALK_INTERVAL * self.getTimeRatio()\n        ):\n            self.handleGarlicYChange()\n            self.walk_timer = self.current_time\n            # 正在上浮或者下潜不用移动\n            if (self.frames == self.float_frames) or (\n                self.frames == self.sink_frames\n            ):\n                pass\n            elif self.is_hypno:\n                self.rect.x += 1\n            else:\n                self.rect.x -= 1\n\n    def animation(self):\n        if self.state == c.FREEZE:\n            self.image.set_alpha(192)\n            return\n\n        if (self.current_time - self.animate_timer) > (\n            self.animate_interval * self.getTimeRatio()\n        ):\n            self.frame_index += 1\n            if self.frame_index >= self.frame_num:\n                if self.state == c.DIE:\n                    self.kill()\n                    return\n                elif self.frames == self.jump_frames:\n                    self.changeFrames(self.swim_frames)\n                elif self.frames == self.sink_frames:\n                    self.changeFrames(self.swim_frames)\n                    # 还需要改回原来的可进入攻击状态的设定\n                    self.canSetAttack = True\n                elif self.frames == self.float_frames:\n                    self.state = c.ATTACK\n                    self.attack_timer = self.current_time\n                    self.changeFrames(self.attack_frames)\n                self.frame_index = 0\n            self.animate_timer = self.current_time\n\n        self.image = self.frames[self.frame_index]\n        if self.is_hypno:\n            self.image = pg.transform.flip(self.image, True, False)\n        self.mask = pg.mask.from_surface(self.image)\n\n        if (self.current_time - self.hit_timer) >= 200:\n            self.image.set_alpha(255)\n        else:\n            self.image.set_alpha(192)\n\n    # 注意潜水僵尸较为特殊：这里的setAttack并没有直接触发攻击状态，而是触发从水面浮起\n    def setAttack(self, prey, is_plant=True):\n        self.prey = prey  # prey can be plant or other zombies\n        self.prey_is_plant = is_plant\n        self.animate_interval = self.attack_animate_interval\n\n        if self.losthead:\n            self.changeFrames(self.losthead_attack_frames)\n        elif self.canSetAttack:\n            self.changeFrames(self.float_frames)\n            self.canSetAttack = False\n\n    def setWalk(self):\n        self.state = c.WALK\n        self.animate_interval = self.walk_animate_interval\n        self.swimming = True\n        self.changeFrames(self.sink_frames)\n"
  },
  {
    "path": "source/constants.py",
    "content": "import os\n\nimport pygame as pg\n\n# 用户数据及日志存储路径\nif os.name == 'nt':   # Windows系统存储路径\n    USERDATA_PATH = os.path.expandvars(\n        os.path.join('%APPDATA%', 'pypvz', 'userdata.json')\n    )\n    USERLOG_PATH = os.path.expandvars(\n        os.path.join('%APPDATA%', 'pypvz', 'run.log')\n    )\nelse:   # 非Windows系统存储路径\n    USERDATA_PATH = os.path.expanduser(\n        os.path.join('~', '.config', 'pypvz', 'userdata.json')\n    )\n    USERLOG_PATH = os.path.expanduser(\n        os.path.join('~', '.config', 'pypvz', 'run.log')\n    )\n\n# 游戏图片资源路径\nPATH_IMG_DIR = os.path.join(\n    os.path.dirname(os.path.dirname(__file__)), 'resources', 'graphics'\n)\n# 游戏音乐文件夹路径\nPATH_MUSIC_DIR = os.path.join(\n    os.path.dirname(os.path.dirname(__file__)), 'resources', 'music'\n)\n# 窗口图标\nORIGINAL_LOGO = os.path.join(\n    os.path.dirname(os.path.dirname(__file__)), 'pypvz-exec-logo.png'\n)\n# 字体路径\nFONT_PATH = os.path.join(\n    os.path.dirname(os.path.dirname(__file__)),\n    'resources',\n    'DroidSansFallback.ttf',\n)\n\n# 窗口标题\nORIGINAL_CAPTION = 'pypvz'\n\n# 游戏模式\nGAME_MODE = 'mode'\nMODE_ADVENTURE = 'adventure'\nMODE_LITTLEGAME = 'littleGame'\n\n# 窗口大小\nSCREEN_WIDTH = 800\nSCREEN_HEIGHT = 600\nSCREEN_SIZE = (SCREEN_WIDTH, SCREEN_HEIGHT)\n\n\n# 选卡数量\n# 最大数量\nCARD_MAX_NUM = 10   # 这里以后可以增加解锁功能，从最初的6格逐渐解锁到10格\n# 最小数量\nCARD_LIST_NUM = CARD_MAX_NUM\n\n# 方格数据\n# 一般\nGRID_X_LEN = 9\nGRID_Y_LEN = 5\nGRID_X_SIZE = 80\nGRID_Y_SIZE = 100\n# 带有泳池\nGRID_POOL_X_LEN = GRID_X_LEN\nGRID_POOL_Y_LEN = 6\nGRID_POOL_X_SIZE = GRID_X_SIZE\nGRID_POOL_Y_SIZE = 85\n# 屋顶\nGRID_ROOF_X_LEN = GRID_X_LEN\nGRID_ROOF_Y_LEN = GRID_Y_LEN\nGRID_ROOF_X_SIZE = GRID_X_SIZE\nGRID_ROOF_Y_SIZE = 85\n\n# 颜色\nWHITE = (255, 255, 255)\nNAVYBLUE = (60, 60, 100)\nSKY_BLUE = (39, 145, 251)\nBLACK = (0, 0, 0)\nLIGHTYELLOW = (234, 233, 171)\nRED = (255, 0, 0)\nPURPLE = (255, 0, 255)\nGOLD = (255, 215, 0)\nGREEN = (0, 255, 0)\nYELLOWGREEN = (55, 200, 0)\nLIGHTGRAY = (107, 108, 145)\nPARCHMENT_YELLOW = (207, 146, 83)\n\n# 退出游戏按钮\nEXIT = 'exit'\nHELP = 'help'\n# 游戏界面可选的菜单\nLITTLE_MENU = 'littleMenu'\nBIG_MENU = 'bigMenu'\nRESTART_BUTTON = 'restartButton'\nMAINMENU_BUTTON = 'mainMenuButton'\nLITTLEGAME_BUTTON = 'littleGameButton'\nOPTION_BUTTON = 'optionButton'\nSOUND_VOLUME_BUTTON = 'volumeButton'\nUNIVERSAL_BUTTON = 'universalButton'\n# 金银向日葵奖杯\nTROPHY_SUNFLOWER = 'sunflowerTrophy'\n# 小铲子\nSHOVEL = 'shovel'\nSHOVEL_BOX = 'shovelBox'\n# 一大波僵尸来袭图片\nHUGE_WAVE_APPROCHING = 'Approching'\n# 关卡进程图片\nLEVEL_PROGRESS_BAR = 'LevelProgressBar'\nLEVEL_PROGRESS_ZOMBIE_HEAD = 'LevelProgressZombieHead'\nLEVEL_PROGRESS_FLAG = 'LevelProgressFlag'\n\n\n# GAME INFO字典键值\nCURRENT_TIME = 'current time'\nPASSED_ALL = 'passed all'   # 已完成该模式下的所有游戏，应当显示向日葵奖杯获得界面\nLEVEL_NUM = 'level num'\nLITTLEGAME_NUM = 'littleGame num'\nLEVEL_COMPLETIONS = 'level completions'\nLITTLEGAME_COMPLETIONS = 'littleGame completions'\nGAME_RATE = 'game rate'\nSOUND_VOLUME = 'volume'\n\n# 整个游戏的状态\nMAIN_MENU = 'main menu'\nLOAD_SCREEN = 'load screen'\nGAME_LOSE = 'game lose'\nGAME_VICTORY = 'game victory'\nLEVEL = 'level'\nAWARD_SCREEN = 'award screen'\nHELP_SCREEN = 'help screen'\n\n# 界面图片文件名\nMAIN_MENU_IMAGE = 'MainMenu'\nOPTION_ADVENTURE = 'Adventure'\nGAME_LOSE_IMAGE = 'GameLose'\nGAME_VICTORY_IMAGE = 'GameVictory'\nAWARD_SCREEN_IMAGE = 'AwardScreen'\nHELP_SCREEN_IMAGE = 'HelpScreen'\n\n# 地图相关内容\nBACKGROUND_NAME = 'Background'\nBACKGROUND_TYPE = 'background_type'\nINIT_SUN_NAME = 'init_sun_value'\nZOMBIE_LIST = 'zombie_list'\nGAME_TITLE = 'title'\n\n# 地图类型\nBACKGROUND_DAY = 0\nBACKGROUND_NIGHT = 1\nBACKGROUND_POOL = 2\nBACKGROUND_FOG = 3\nBACKGROUND_ROOF = 4\nBACKGROUND_ROOFNIGHT = 5\nBACKGROUND_WALLNUTBOWLING = 6\nBACKGROUND_SINGLE = 7\nBACKGROUND_TRIPLE = 8\n\n# 地图类型集合\n# 白天场地（泛指蘑菇睡觉的场地）\nDAYTIME_BACKGROUNDS = {\n    BACKGROUND_DAY,\n    BACKGROUND_POOL,\n    BACKGROUND_ROOF,\n    BACKGROUND_WALLNUTBOWLING,\n    BACKGROUND_SINGLE,\n    BACKGROUND_TRIPLE,\n}\n\n# 带有泳池的场地\nPOOL_EQUIPPED_BACKGROUNDS = {\n    BACKGROUND_POOL,\n    BACKGROUND_FOG,\n}\n\n# 屋顶上的场地\nON_ROOF_BACKGROUNDS = {\n    BACKGROUND_ROOF,\n    BACKGROUND_ROOFNIGHT,\n}\n\n# BACKGROUND_DAY场地的变体\nBACKGROUND_DAY_LIKE_BACKGROUNDS = {\n    BACKGROUND_DAY,\n    BACKGROUND_SINGLE,\n    BACKGROUND_TRIPLE,\n}\n\n# 夜晚地图的墓碑数量等级\nGRADE_GRAVES = 'grade_graves'\n# 不同墓碑等级对应的信息，列表位置对应的是墓碑等级\nGRAVES_GRADE_INFO = (0, 4, 7, 11)\n\n# 僵尸生成方式\nSPAWN_ZOMBIES = 'spawn_zombies'\nSPAWN_ZOMBIES_AUTO = 1\nSPAWN_ZOMBIES_LIST = 0\nINCLUDED_ZOMBIES = 'included_zombies'\nNUM_FLAGS = 'num_flags'\nINEVITABLE_ZOMBIE_DICT = 'inevitable_zombie_list'\nSURVIVAL_ROUNDS = 'survival_rounds'\n\n# 地图单元格属性\nMAP_PLANT = 'plantnames'\nMAP_SLEEP = 'sleep'   # 有没有休眠的蘑菇，作是否能种植咖啡豆的判断\nMAP_PLOT_TYPE = 'plot_type'\n# 地图单元格区域类型\nMAP_GRASS = 'grass'\nMAP_WATER = 'water'\nMAP_TILE = 'tile'  # 指屋顶上的瓦片\nMAP_UNAVAILABLE = 'unavailable'   # 指完全不能种植物的地方，包括无草皮的荒地和坚果保龄球等红线右侧\n\n# 地图相关像素数据\nBACKGROUND_OFFSET_X = 220\nMAP_OFFSET_X = 35\nMAP_OFFSET_Y = 100\nMAP_POOL_OFFSET_X = 42\nMAP_POOL_OFFSET_Y = 115\nMAP_ROOF_OFFSET_X = 35  # 暂时还不清楚数据\nMAP_ROOF_OFFSET_Y = 105   # 暂时还不清楚数据\n\n# 泳池前端陆地部分\nMAP_POOL_FRONT_X = SCREEN_WIDTH - 15\n\n# 植物选择菜单栏、传送带菜单栏等类型设定\nCHOOSEBAR_TYPE = 'choosebar_type'\nCHOOSEBAR_STATIC = 0\nCHOOSEBAR_MOVE = 1\nCHOOSEBAR_BOWLING = 2\nMENUBAR_BACKGROUND = 'ChooserBackground'\nMOVEBAR_BACKGROUND = 'MoveBackground'\nPANEL_BACKGROUND = 'PanelBackground'\nSTART_BUTTON = 'StartButton'\nCARD_POOL = 'card_pool'\n\n# 关于植物栏的像素设置\nPANEL_Y_START = 87\nPANEL_X_START = 22\nPANEL_Y_INTERNAL = 69\nPANEL_X_INTERNAL = 53\nBAR_CARD_X_INTERNAL = 51\n\n# 植物卡片信息索引\nPLANT_NAME_INDEX = 0\nCARD_INDEX = 1\nSUN_INDEX = 2\nFROZEN_TIME_INDEX = 3\n\n# 传送带模式中的刷新间隔和移动速率\nMOVEBAR_CARD_FRESH_TIME = 6000\nCARD_MOVE_TIME = 60\n\n# 其他显示物\nCAR = 'car'\nSUN = 'Sun'\n\n# plant子类非植物对象（这里的是不包括阳光、子弹的拟植物对象）\nNON_PLANT_OBJECTS = {\n    HOLE := 'Hole',\n    ICEFROZENPLOT := 'IceFrozenPlot',\n    GRAVE := 'Grave',\n}\n\n# 植物相关信息\nPLANT_IMAGE_RECT = 'plant_image_rect'\nBOOM_IMAGE = 'Boom'\n\n# 植物卡片信息汇总（包括植物名称, 卡片名称, 阳光, 冷却时间）\nPLANT_CARD_INFO = (  # 元组 (植物名称, 卡片名称, 阳光, 冷却时间)\n    (\n        PEASHOOTER := 'Peashooter',\n        CARD_PEASHOOTER := 'card_peashooter',\n        100,\n        7500,\n    ),\n    (SUNFLOWER := 'SunFlower', CARD_SUNFLOWER := 'card_sunflower', 50, 7500),\n    (\n        CHERRYBOMB := 'CherryBomb',\n        CARD_CHERRYBOMB := 'card_cherrybomb',\n        150,\n        50000,\n    ),\n    (WALLNUT := 'WallNut', CARD_WALLNUT := 'card_wallnut', 50, 30000),\n    (\n        POTATOMINE := 'PotatoMine',\n        CARD_POTATOMINE := 'card_potatomine',\n        25,\n        30000,\n    ),\n    (\n        SNOWPEASHOOTER := 'SnowPea',\n        CARD_SNOWPEASHOOTER := 'card_snowpea',\n        175,\n        7500,\n    ),\n    (CHOMPER := 'Chomper', CARD_CHOMPER := 'card_chomper', 150, 7500),\n    (\n        REPEATERPEA := 'RepeaterPea',\n        CARD_REPEATERPEA := 'card_repeaterpea',\n        200,\n        7500,\n    ),\n    (\n        PUFFSHROOM := 'PuffShroom',\n        CARD_PUFFSHROOM := 'card_puffshroom',\n        0,\n        7500,\n    ),\n    (SUNSHROOM := 'SunShroom', CARD_SUNSHROOM := 'card_sunshroom', 25, 7500),\n    (\n        FUMESHROOM := 'FumeShroom',\n        CARD_FUMESHROOM := 'card_fumeshroom',\n        75,\n        7500,\n    ),\n    (\n        GRAVEBUSTER := 'GraveBuster',\n        CARD_GRAVEBUSTER := 'card_gravebuster',\n        75,\n        7500,\n    ),\n    (\n        HYPNOSHROOM := 'HypnoShroom',\n        CARD_HYPNOSHROOM := 'card_hypnoshroom',\n        75,\n        30000,\n    ),\n    (\n        SCAREDYSHROOM := 'ScaredyShroom',\n        CARD_SCAREDYSHROOM := 'card_scaredyshroom',\n        25,\n        7500,\n    ),\n    (ICESHROOM := 'IceShroom', CARD_ICESHROOM := 'card_iceshroom', 75, 50000),\n    (\n        DOOMSHROOM := 'DoomShroom',\n        CARD_DOOMSHROOM := 'card_doomshroom',\n        125,\n        50000,\n    ),\n    (LILYPAD := 'LilyPad', CARD_LILYPAD := 'card_lilypad', 25, 7500),\n    (SQUASH := 'Squash', CARD_SQUASH := 'card_squash', 50, 30000),\n    (\n        TANGLEKLEP := 'TangleKlep',\n        CARD_TANGLEKLEP := 'card_tangleklep',\n        25,\n        30000,\n    ),\n    (\n        THREEPEASHOOTER := 'Threepeater',\n        CARD_THREEPEASHOOTER := 'card_threepeashooter',\n        325,\n        7500,\n    ),\n    (JALAPENO := 'Jalapeno', CARD_JALAPENO := 'card_jalapeno', 125, 50000),\n    (SPIKEWEED := 'Spikeweed', CARD_SPIKEWEED := 'card_spikeweed', 100, 7500),\n    (TORCHWOOD := 'TorchWood', CARD_TORCHWOOD := 'card_torchwood', 175, 7500),\n    (TALLNUT := 'TallNut', CARD_TALLNUT := 'card_tallnut', 125, 30000),\n    (SEASHROOM := 'SeaShroom', CARD_SEASHROOM := 'card_seashroom', 0, 30000),\n    (STARFRUIT := 'StarFruit', CARD_STARFRUIT := 'card_starfruit', 125, 7500),\n    (\n        PUMPKINHEAD := 'PumpkinHead',\n        CARD_PUMPKINHEAD := 'card_pumpkinhead',\n        125,\n        30000,\n    ),\n    (\n        COFFEEBEAN := 'CoffeeBean',\n        CARD_COFFEEBEAN := 'card_coffeebean',\n        75,\n        7500,\n    ),\n    (GARLIC := 'Garlic', CARD_GARLIC := 'card_garlic', 50, 7500),\n    # 应当保证这3个在一般模式下不可选的特殊植物恒在最后\n    (WALLNUTBOWLING := 'WallNutBowling', CARD_WALLNUT := 'card_wallnut', 0, 0),\n    (\n        REDWALLNUTBOWLING := 'RedWallNutBowling',\n        CARD_REDWALLNUT := 'card_redwallnut',\n        0,\n        0,\n    ),\n    (\n        GIANTWALLNUT := 'GiantWallNut',\n        CARD_GIANTWALLNUT := 'card_giantwallnut',\n        0,\n        0,\n    ),\n)\n\n# 卡片中的植物名称与索引序号的对应关系，指定名称以得到索引值\nPLANT_CARD_INDEX = {\n    item[PLANT_NAME_INDEX]: index\n    for (index, item) in enumerate(PLANT_CARD_INFO)\n}\n\n# 指定了哪些卡可选（排除坚果保龄球特殊植物）\nCARDS_TO_CHOOSE = range(len(PLANT_CARD_INFO) - 3)\n\n\n# 植物集体属性集合\n# 也许以后有必要的可以重新加入到对象的属性中\n# 在生效时不用与僵尸进行碰撞检测的对象（即生效时不可发生被僵尸啃食的事件）\nSKIP_ZOMBIE_COLLISION_CHECK_WHEN_WORKING = {\n    # 注意爆炸坚果的触发也是啃食类碰撞，因此只能算作爆炸后不检测\n    SQUASH,\n    ICESHROOM,\n    REDWALLNUTBOWLING,\n    CHERRYBOMB,\n    JALAPENO,\n    DOOMSHROOM,\n    POTATOMINE,\n}\n\n# 所有可能不用与僵尸进行碰撞检测的对象\nCAN_SKIP_ZOMBIE_COLLISION_CHECK = (  # 这里运用了集合运算\n    # 注意这个外围的小括号是用来换行的\n    # 各个部分末！尾！千！万！不！能！加！逗！号！！！\n    # 生效时不检测的植物\n    SKIP_ZOMBIE_COLLISION_CHECK_WHEN_WORKING\n    |\n    # 非植物对象\n    NON_PLANT_OBJECTS\n    |\n    # 地刺类\n    {\n        SPIKEWEED,\n    }\n)\n\n# 死亡时不触发音效的对象\nPLANT_DIE_SOUND_EXCEPTIONS = {\n    WALLNUTBOWLING,\n    TANGLEKLEP,\n    ICEFROZENPLOT,\n    HOLE,\n    GRAVE,\n    JALAPENO,\n    REDWALLNUTBOWLING,\n    CHERRYBOMB,\n    GIANTWALLNUT,\n}\n\n# 直接水生植物\nWATER_PLANTS = {\n    LILYPAD,\n    SEASHROOM,\n    TANGLEKLEP,\n}\n\n# 攻击状态检查类型\nCHECK_ATTACK_NEVER = 0\nCHECK_ATTACK_ALWAYS = 1\n\n# 范围爆炸植物，即灰烬植物与寒冰菇\nASH_PLANTS_AND_ICESHROOM = {\n    REDWALLNUTBOWLING,\n    CHERRYBOMB,\n    JALAPENO,\n    DOOMSHROOM,\n    ICESHROOM,\n}\n\n# 白天要睡觉的植物\nCAN_SLEEP_PLANTS = {\n    PUFFSHROOM,\n    SUNSHROOM,\n    FUMESHROOM,\n    HYPNOSHROOM,\n    SCAREDYSHROOM,\n    ICESHROOM,\n    DOOMSHROOM,\n    SEASHROOM,\n}\n\n# 选卡不推荐选择理由\nREASON_WILL_SLEEP = 1\nREASON_SLEEP_BUT_COFFEE_BEAN = 2\nREASON_OTHER = 3\n\n# 植物生命值\nPLANT_HEALTH = 300\nWALLNUT_HEALTH = 4000\nWALLNUT_CRACKED1_HEALTH = WALLNUT_HEALTH // 3 * 2\nWALLNUT_CRACKED2_HEALTH = WALLNUT_HEALTH // 3\nTALLNUT_HEALTH = 8000\nTALLNUT_CRACKED1_HEALTH = TALLNUT_HEALTH // 3 * 2\nTALLNUT_CRACKED2_HEALTH = TALLNUT_HEALTH // 3\nGARLIC_HEALTH = 450\nGARLIC_CRACKED1_HEALTH = GARLIC_HEALTH // 3 * 2\nGARLIC_CRACKED2_HEALTH = GARLIC_HEALTH // 3\n# 坚果保龄球攻击伤害\nWALLNUT_BOWLING_DAMAGE = 550\n\n# 阳光生成属性\nPRODUCE_SUN_INTERVAL = 4250   # 基准\nFLOWER_SUN_INTERVAL = 24000\nSUN_LIVE_TIME = 10000\nSUN_VALUE = 25\n\n# 僵尸冷冻\nICE_SLOW_TIME = 10000\nMIN_FREEZE_TIME = 4000\nICETRAP = 'IceTrap'\n\n# 子弹信息\n# 子弹类型\nBULLET_PEA = 'PeaNormal'\nBULLET_PEA_ICE = 'PeaIce'\nBULLET_FIREBALL = 'Fireball'\nBULLET_MUSHROOM = 'BulletMushRoom'\nBULLET_SEASHROOM = 'BulletSeaShroom'\nFUME = 'Fume'\n# 子弹伤害\nBULLET_DAMAGE_NORMAL = 20\nBULLET_DAMAGE_FIREBALL_BODY = 27   # 这是火球本体的伤害，注意不是40，本体(27) + 溅射(13)才是40\nBULLET_DAMAGE_FIREBALL_RANGE = 13   # 原版溅射伤害会随着僵尸数量增多而减少，这里相当于做了一个增强\n# 子弹效果\nBULLET_EFFECT_ICE = 'ice'\nBULLET_EFFECT_UNICE = 'unice'\n\n# 特殊子弹\n# 杨桃子弹\n# 子弹名称\nBULLET_STAR = 'StarBullet'\n# 子弹方向\nSTAR_FORWARD_UP = 'forwardUp'   # 向前上方\nSTAR_FORWARD_DOWN = 'forwardDown'   # 向前下方\nSTAR_BACKWARD = 'backward'  # 向后\nSTAR_UPWARD = 'upward'  # 向上\nSTAR_DOWNWARD = 'downward'  # 向下\n\n# 有爆炸图片的子弹\nBULLET_INDEPENDENT_BOOM_IMG = {\n    BULLET_PEA,\n    BULLET_PEA_ICE,\n    BULLET_MUSHROOM,\n    BULLET_SEASHROOM,\n    BULLET_STAR,\n}\n\n# 僵尸信息\nZOMBIE_IMAGE_RECT = 'zombie_image_rect'\nZOMBIE_HEAD = 'ZombieHead'\nNORMAL_ZOMBIE = 'Zombie'\nCONEHEAD_ZOMBIE = 'ConeheadZombie'\nBUCKETHEAD_ZOMBIE = 'BucketheadZombie'\nFLAG_ZOMBIE = 'FlagZombie'\nNEWSPAPER_ZOMBIE = 'NewspaperZombie'\nFOOTBALL_ZOMBIE = 'FootballZombie'\nDUCKY_TUBE_ZOMBIE = 'DuckyTubeZombie'\nCONEHEAD_DUCKY_TUBE_ZOMBIE = 'ConeheadDuckyTubeZombie'\nBUCKETHEAD_DUCKY_TUBE_ZOMBIE = 'BucketheadDuckyTubeZombie'\nSCREEN_DOOR_ZOMBIE = 'ScreenDoorZombie'\nPOLE_VAULTING_ZOMBIE = 'PoleVaultingZombie'\nZOMBONI = 'Zomboni'\nSNORKELZOMBIE = 'SnorkelZombie'\n\nBOOMDIE = 'BoomDie'\n\n# 对僵尸的攻击类型设置\nZOMBIE_DEAFULT_DAMAGE = ZOMBIE_HELMET_2_FIRST = 'helmet2First'  # 优先攻击二类防具\nZOMBIE_COMMON_DAMAGE = 'commonDamage'   # 优先攻击僵尸与一类防具的整体\nZOMBIE_RANGE_DAMAGE = 'rangeDamage'   # 范围攻击，同时伤害二类防具与(僵尸与一类防具的整体)\nZOMBIE_ASH_DAMAGE = 'ashDamage'   # 灰烬植物攻击，直接伤害本体\nZOMBIE_WALLNUT_BOWLING_DANMAGE = 'wallnutBowlingDamage'   # 坚果保龄球冲撞伤害\n\n# 僵尸生命值设置\n# 有关本体\nNORMAL_HEALTH = 200   # 普通僵尸生命值\nPOLE_VAULTING_HEALTH = 333\nZOMBONI_HEALTH = 1280\n# 冰车损坏点\nZOMBONI_DAMAGED1_HEALTH = 2 * ZOMBONI_HEALTH // 3 + 70\nZOMBONI_DAMAGED2_HEALTH = ZOMBONI_HEALTH // 3 + 70\n# 掉头后僵尸的生命值\nLOSTHEAD_HEALTH = 70\nPOLE_VAULTING_LOSTHEAD_HEALTH = 167\n# 有关一类防具\nCONEHEAD_HEALTH = 370\nBUCKETHEAD_HEALTH = 1100\nFOOTBALL_HELMET_HEALTH = 1400\n# 有关二类防具\nNEWSPAPER_HEALTH = 150\nSCREEN_DOOR_HEALTH = 1100\n\n# 僵尸行动信息\nATTACK_INTERVAL = 500\nZOMBIE_ATTACK_DAMAGE = 50\nZOMBIE_WALK_INTERVAL = 60  # 僵尸步行间隔\n\n# 僵尸生成位置\nZOMBIE_START_X = SCREEN_WIDTH + 30  # 场宽度不一样，用于拟合\n\n\n# 僵尸集体属性集合\n# 僵尸生成信息字典：包含生成僵尸名称、僵尸级别、生成权重\nCREATE_ZOMBIE_DICT = {  # 生成僵尸:(级别, 权重)\n    NORMAL_ZOMBIE: (1, 4000),\n    FLAG_ZOMBIE: (1, 0),\n    CONEHEAD_ZOMBIE: (2, 4000),\n    BUCKETHEAD_ZOMBIE: (4, 3000),\n    NEWSPAPER_ZOMBIE: (2, 1000),\n    FOOTBALL_ZOMBIE: (7, 2000),\n    DUCKY_TUBE_ZOMBIE: (1, 0),  # 作为变种，不主动生成\n    CONEHEAD_DUCKY_TUBE_ZOMBIE: (2, 0),  # 作为变种，不主动生成\n    BUCKETHEAD_DUCKY_TUBE_ZOMBIE: (4, 0),  # 作为变种，不主动生成\n    SCREEN_DOOR_ZOMBIE: (4, 3500),\n    POLE_VAULTING_ZOMBIE: (2, 2000),\n    ZOMBONI: (7, 2000),\n    SNORKELZOMBIE: (3, 2000),\n}\n\n# 记录陆生僵尸的水生变种\nCONVERT_ZOMBIE_IN_POOL = {\n    NORMAL_ZOMBIE: DUCKY_TUBE_ZOMBIE,\n    CONEHEAD_ZOMBIE: CONEHEAD_DUCKY_TUBE_ZOMBIE,\n    BUCKETHEAD_ZOMBIE: BUCKETHEAD_DUCKY_TUBE_ZOMBIE,\n}\n\n# 水上僵尸集合\nWATER_ZOMBIE = {\n    DUCKY_TUBE_ZOMBIE,\n    CONEHEAD_DUCKY_TUBE_ZOMBIE,\n    BUCKETHEAD_DUCKY_TUBE_ZOMBIE,\n    SNORKELZOMBIE,\n}\n\n\n# 状态类型\nIDLE = 'idle'\nFLY = 'fly'\nEXPLODE = 'explode'\nATTACK = 'attack'\nATTACKED = 'attacked'\nDIGEST = 'digest'\nWALK = 'walk'\nDIE = 'die'\nCRY = 'cry'\nFREEZE = 'freeze'\nSLEEP = 'sleep'\n\n# 关卡状态\nCHOOSE = 'choose'\nPLAY = 'play'\n\n# 加载矩形碰撞范围 用于消除文件边框影响\n# 植物\nPLANT_RECT = {\n    BULLET_PEA: {'x': 28, 'y': 0, 'width': 28, 'height': 34},\n    BULLET_PEA_ICE: {'x': 26, 'y': 0, 'width': 30, 'height': 34},\n    CHOMPER: {'x': 0, 'y': 0, 'width': 100, 'height': 114},\n    PUFFSHROOM: {'x': 0, 'y': 28, 'width': 35, 'height': 38},\n    f'{PUFFSHROOM}Sleep': {'x': 1, 'y': 0, 'width': 39, 'height': 65},\n    BULLET_MUSHROOM: {'x': 0, 'y': 1, 'width': 55, 'height': 21},\n    BULLET_SEASHROOM: {'x': 0, 'y': 1, 'width': 55, 'height': 21},\n    POTATOMINE: {'x': 0, 'y': 0, 'width': 75, 'height': 55},\n    SQUASH: {'x': 10, 'y': 140, 'width': 80, 'height': 86},\n    f'{SQUASH}Aim': {'x': 10, 'y': 140, 'width': 80, 'height': 86},\n    SPIKEWEED: {'x': 3, 'y': 0, 'width': 80, 'height': 35},\n}\n# 僵尸\nZOMBIE_RECT = {\n    NORMAL_ZOMBIE: {'x': 62, 'width': 90},\n    f'{NORMAL_ZOMBIE}Attack': {'x': 62, 'width': 90},\n    f'{NORMAL_ZOMBIE}LostHead': {'x': 62, 'width': 90},\n    f'{NORMAL_ZOMBIE}LostHeadAttack': {'x': 62, 'width': 90},\n    f'{NORMAL_ZOMBIE}Die': {'x': 0, 'width': 164},\n    BOOMDIE: {'x': 68, 'width': 80},\n    CONEHEAD_ZOMBIE: {'x': 80, 'width': 80},\n    f'{CONEHEAD_ZOMBIE}Attack': {'x': 79, 'width': 87},\n    BUCKETHEAD_ZOMBIE: {'x': 54, 'width': 90},\n    f'{BUCKETHEAD_ZOMBIE}Attack': {'x': 46, 'width': 90},\n    FLAG_ZOMBIE: {'x': 56, 'width': 110},\n    f'{FLAG_ZOMBIE}Attack': {'x': 60, 'width': 100},\n    f'{FLAG_ZOMBIE}LostHead': {'x': 55, 'width': 110},\n    f'{FLAG_ZOMBIE}LostHeadAttack': {'x': 55, 'width': 110},\n    NEWSPAPER_ZOMBIE: {'x': 48, 'width': 92},\n    f'{NEWSPAPER_ZOMBIE}Attack': {'x': 48, 'width': 92},\n    f'{NEWSPAPER_ZOMBIE}NoPaper': {'x': 40, 'width': 98},\n    f'{NEWSPAPER_ZOMBIE}NoPaperAttack': {'x': 48, 'width': 92},\n    f'{NEWSPAPER_ZOMBIE}LostHead': {'x': 44, 'width': 96},\n    f'{NEWSPAPER_ZOMBIE}LostHeadAttack': {'x': 48, 'width': 92},\n    f'{NEWSPAPER_ZOMBIE}Die': {'x': 0, 'width': 100},\n    f'{DUCKY_TUBE_ZOMBIE}Die': {'x': 55, 'width': 105},\n    f'{DUCKY_TUBE_ZOMBIE}LostHead': {'x': 55, 'width': 105},\n    SCREEN_DOOR_ZOMBIE: {'x': 41, 'width': 100},\n    f'{SCREEN_DOOR_ZOMBIE}Attack': {'x': 41, 'width': 100},\n}   # 这里还有懒得写代码的补加，用循环实现\nfor _part1 in (\n    DUCKY_TUBE_ZOMBIE,\n    CONEHEAD_DUCKY_TUBE_ZOMBIE,\n    BUCKETHEAD_DUCKY_TUBE_ZOMBIE,\n):\n    for _part2 in ('', 'Attack', 'Swim'):\n        ZOMBIE_RECT[f'{_part1}{_part2}'] = {'x': 55, 'width': 105}\n\n\n# 音效\ndef _getSound(filename):\n    return pg.mixer.Sound(\n        os.path.join(\n            os.path.dirname(os.path.dirname(__file__)),\n            'resources',\n            'sound',\n            filename,\n        )\n    )\n\n\n# 所有音效的元组，用一波海象算子表达，免得要维护两个\nSOUNDS = (  # 程序交互等\n    SOUND_TAPPING_CARD := _getSound('tap.ogg'),\n    SOUND_HELP_SCREEN := _getSound('helpScreen.ogg'),\n    # 植物\n    SOUND_FIREPEA_EXPLODE := _getSound('firepea.ogg'),\n    SOUND_BULLET_EXPLODE := _getSound('bulletExplode.ogg'),\n    SOUND_SHOOT := _getSound('shoot.ogg'),\n    SOUND_SNOWPEA_SPARKLES := _getSound('snowPeaSparkles.ogg'),\n    SOUND_BOMB := _getSound('bomb.ogg'),\n    SOUND_BIGCHOMP := _getSound('bigchomp.ogg'),\n    SOUND_PUFF := _getSound('puff.ogg'),\n    SOUND_POTATOMINE := _getSound('potatomine.ogg'),\n    SOUND_SQUASHING := _getSound('squashing.ogg'),\n    SOUND_SQUASH_HMM := _getSound('squashHmm.ogg'),\n    SOUND_PLANT_GROW := _getSound('plantGrow.ogg'),\n    SOUND_MUSHROOM_WAKEUP := _getSound('mushroomWakeup.ogg'),\n    SOUND_TANGLE_KELP_DRAG := _getSound('tangleKelpDrag.ogg'),\n    SOUND_DOOMSHROOM := _getSound('doomshroom.ogg'),\n    SOUND_GRAVEBUSTER_CHOMP := _getSound('gravebusterchomp.ogg'),\n    SOUND_FUME := _getSound('fume.ogg'),\n    # 僵尸\n    SOUND_ZOMBIE_ENTERING_WATER := _getSound('zombieEnteringWater.ogg'),\n    SOUND_ZOMBIE_ATTACKING := _getSound('zombieAttack.ogg'),\n    SOUND_FREEZE := _getSound('freeze.ogg'),\n    SOUND_HYPNOED := _getSound('hypnoed.ogg'),\n    SOUND_NEWSPAPER_RIP := _getSound('newspaperRip.ogg'),\n    SOUND_NEWSPAPER_ZOMBIE_ANGRY := _getSound('newspaperZombieAngry.ogg'),\n    SOUND_POLEVAULT_JUMP := _getSound('polevaultjump.ogg'),\n    SOUND_ZOMBONI := _getSound('zomboni.ogg'),\n    SOUND_ZOMBONI_EXPLOSION := _getSound('zomboniExplosion.ogg'),\n    # 关卡中\n    SOUND_CAR_WALKING := _getSound('carWalking.ogg'),\n    SOUND_ZOMBIE_COMING := _getSound('zombieComing.ogg'),\n    SOUND_ZOMBIE_VOICE := _getSound('zombieVoice.ogg'),\n    SOUND_HUGE_WAVE_APPROCHING := _getSound('hugeWaveApproching.ogg'),\n    SOUND_BUTTON_CLICK := _getSound('buttonclick.ogg'),\n    SOUND_COLLECT_SUN := _getSound('collectSun.ogg'),\n    SOUND_CLICK_CARD := _getSound('clickCard.ogg'),\n    SOUND_SHOVEL := _getSound('shovel.ogg'),\n    SOUND_PLANT := _getSound('plant.ogg'),\n    SOUND_BOWLING_IMPACT := _getSound('bowlingimpact.ogg'),\n    SOUND_PLANT_DIE := _getSound('plantDie.ogg'),\n    SOUND_EVILLAUGH := _getSound('evillaugh.ogg'),\n    SOUND_LOSE := _getSound('lose.ogg'),\n    SOUND_WIN := _getSound('win.ogg'),\n    SOUND_SCREAM := _getSound('scream.ogg'),\n    SOUND_CANNOT_CHOOSE_WARNING := _getSound('cannotChooseWarning.ogg'),\n    SOUND_FINAL_FANFARE := _getSound('finalfanfare.ogg'),\n)\n\n# 记录本地存储文件初始值\nINIT_USERDATA = {\n    LEVEL_NUM: 1,\n    LITTLEGAME_NUM: 1,\n    LEVEL_COMPLETIONS: 0,\n    LITTLEGAME_COMPLETIONS: 0,\n    GAME_RATE: 1,\n    SOUND_VOLUME: 1,\n}\n\n# 无穷大常量\nINF = float('inf')  # python传递字符串性能较低，故在这里对inf声明一次，以后仅需调用即可，虽然真正的用处是可以自动补全（\n"
  },
  {
    "path": "source/state/__init__.py",
    "content": ""
  },
  {
    "path": "source/state/level.py",
    "content": "import logging\nimport os\nimport random\n\nimport pygame as pg\n\nfrom .. import constants as c\nfrom .. import tool\nfrom ..component import map, menubar, plant, zombie\n\nlogger = logging.getLogger('main')\n\n\nclass Level(tool.State):\n    def __init__(self):\n        tool.State.__init__(self)\n\n    def startup(self, current_time, persist):\n        self.game_info = persist\n        self.persist = self.game_info\n        self.game_info[c.CURRENT_TIME] = current_time\n\n        # 暂停状态\n        self.pause = False\n        self.pause_time = 0\n\n        # 默认显然不用显示菜单\n        self.show_game_menu = False\n\n        # 导入地图参数\n        self.loadMap()\n        self.map = map.Map(self.map_data[c.BACKGROUND_TYPE])\n        self.map_x_len = self.map.width\n        self.map_y_len = self.map.height\n        self.setupBackground()\n        self.initState()\n\n    def loadMap(self):\n        # 冒险模式\n        if self.game_info[c.GAME_MODE] == c.MODE_ADVENTURE:\n            if 0 <= self.game_info[c.LEVEL_NUM] < map.TOTAL_LEVEL:\n                self.map_data = map.LEVEL_MAP_DATA[self.game_info[c.LEVEL_NUM]]\n                pg.display.set_caption(\n                    f'pypvz: 冒险模式 {self.map_data[c.GAME_TITLE]}'\n                )\n            else:\n                self.game_info[c.LEVEL_NUM] = 1\n                self.saveUserData()\n                self.map_data = map.LEVEL_MAP_DATA[self.game_info[c.LEVEL_NUM]]\n                pg.display.set_caption(\n                    f'pypvz: 冒险模式 {self.map_data[c.GAME_TITLE]}'\n                )\n                logger.warning('关卡数设定错误！进入默认的第一关！\\n')\n        # 小游戏模式\n        elif self.game_info[c.GAME_MODE] == c.MODE_LITTLEGAME:\n            if 0 <= self.game_info[c.LITTLEGAME_NUM] < map.TOTAL_LITTLE_GAME:\n                self.map_data = map.LITTLE_GAME_MAP_DATA[\n                    self.game_info[c.LITTLEGAME_NUM]\n                ]\n                pg.display.set_caption(\n                    f'pypvz: 玩玩小游戏 {self.map_data[c.GAME_TITLE]}'\n                )\n            else:\n                self.game_info[c.LITTLEGAME_NUM] = 1\n                self.saveUserData()\n                self.map_data = map.LITTLE_GAME_MAP_DATA[\n                    self.game_info[c.LITTLEGAME_NUM]\n                ]\n                pg.display.set_caption(\n                    f'pypvz: 冒险模式 {self.map_data[c.GAME_TITLE]}'\n                )\n                logger.warning('关卡数设定错误！进入默认的第一关！\\n')\n        # 是否有铲子的信息：无铲子时为0，有铲子时为1，故直接赋值即可\n        self.has_shovel = self.map_data[c.SHOVEL]\n\n        # 同时指定音乐\n        # 缺省音乐为进入的音乐，方便发现错误\n        self.bgm = 'intro.opus'\n        if c.CHOOSEBAR_TYPE in self.map_data:  # 指定了choosebar_type的传送带关\n            if (\n                self.map_data[c.CHOOSEBAR_TYPE] == c.CHOOSEBAR_BOWLING\n            ):   # 坚果保龄球\n                self.bgm = 'bowling.opus'\n            elif self.map_data[c.CHOOSEBAR_TYPE] == c.CHOOSEBAR_MOVE:  # 传送带\n                self.bgm = 'battle.opus'\n        else:   # 一般选卡关，非传送带\n            # 白天类\n            if (\n                self.map_data[c.BACKGROUND_TYPE]\n                in c.BACKGROUND_DAY_LIKE_BACKGROUNDS\n            ):\n                self.bgm = 'dayLevel.opus'\n            # 夜晚\n            elif self.map_data[c.BACKGROUND_TYPE] == c.BACKGROUND_NIGHT:\n                self.bgm = 'nightLevel.opus'\n            # 泳池\n            elif self.map_data[c.BACKGROUND_TYPE] == c.BACKGROUND_POOL:\n                self.bgm = 'poolLevel.opus'\n            # 浓雾\n            elif self.map_data[c.BACKGROUND_TYPE] == c.BACKGROUND_FOG:\n                self.bgm = 'fogLevel.opus'\n\n    def setupBackground(self):\n        img_index = self.map_data[c.BACKGROUND_TYPE]\n        self.background_type = img_index\n        self.background = tool.GFX[c.BACKGROUND_NAME][img_index]\n        self.bg_rect = self.background.get_rect()\n\n        self.level = pg.Surface((self.bg_rect.w, self.bg_rect.h)).convert()\n        self.viewport = tool.SCREEN.get_rect(bottom=self.bg_rect.bottom)\n        self.viewport.x += c.BACKGROUND_OFFSET_X\n\n    def setupGroups(self):\n        self.sun_group = pg.sprite.Group()\n        self.head_group = pg.sprite.Group()\n\n        # 改用列表生成器直接生成内容，不再在这里使用for循环\n        self.plant_groups = [pg.sprite.Group() for i in range(self.map_y_len)]\n        self.zombie_groups = [pg.sprite.Group() for i in range(self.map_y_len)]\n        self.hypno_zombie_groups = [\n            pg.sprite.Group() for i in range(self.map_y_len)\n        ]   # 被魅惑的僵尸\n        self.bullet_groups = [pg.sprite.Group() for i in range(self.map_y_len)]\n\n    # 按照规则生成每一波僵尸\n    # 将波刷新和一波中的僵尸生成分开\n    # useableZombie是指可用的僵尸种类的元组\n    # inevitableZombie指在本轮必然出现的僵尸，输入形式为字典: {波数1:(僵尸1, 僵尸2……), 波数2:(僵尸1, 僵尸2……)……}\n    def createWaves(\n        self,\n        useable_zombies,\n        num_flags,\n        survival_rounds=0,\n        inevitable_zombie_dict=None,\n    ):\n\n        waves = []\n\n        self.num_flags = num_flags\n\n        # 权重值，c.CREATE_ZOMBIE_DICT[zombie][1]即为对应的权重\n        weights = [\n            c.CREATE_ZOMBIE_DICT[zombie][1] for zombie in useable_zombies\n        ]\n\n        # 按照原版pvz设计的僵尸容量函数，是从无尽解析的，但是普通关卡也可以遵循\n        for wave in range(1, 10 * num_flags + 1):\n            zombie_volume = (\n                int(int((wave + survival_rounds * 20) * 0.8) / 2) + 1\n            )\n            zombie_list = []\n\n            # 大波僵尸情况\n            if wave % 10 == 0:\n                # 容量增大至2.5倍\n                zombie_volume = int(zombie_volume * 2.5)\n                # 先生成旗帜僵尸\n                zombie_list.append(c.FLAG_ZOMBIE)\n                zombie_volume -= c.CREATE_ZOMBIE_DICT[c.FLAG_ZOMBIE][0]\n\n            # 传送带模式应当增大僵尸容量\n            if self.bar_type != c.CHOOSEBAR_STATIC:\n                zombie_volume += 2\n\n            if inevitable_zombie_dict and (wave in inevitable_zombie_dict):\n                for new_zombie in inevitable_zombie_dict[wave]:\n                    zombie_list.append(new_zombie)\n                    zombie_volume -= c.CREATE_ZOMBIE_DICT[new_zombie][0]\n                if zombie_volume < 0:\n                    logger.warning(f'第{wave}波中手动设置的僵尸级别总数超过上限！')\n\n            # 防止因为僵尸最小等级过大，使得总容量无法完全利用，造成死循环的检查机制\n            min_cost = c.CREATE_ZOMBIE_DICT[\n                min(useable_zombies, key=lambda x: c.CREATE_ZOMBIE_DICT[x][0])\n            ][0]\n\n            while (zombie_volume >= min_cost) and (len(zombie_list) < 50):\n                new_zombie = random.choices(useable_zombies, weights)[0]\n                # 普通僵尸、路障僵尸、铁桶僵尸有概率生成水中变种\n                if self.background_type in c.POOL_EQUIPPED_BACKGROUNDS:\n                    # 有泳池第一轮的第四波设定上生成水生僵尸\n                    if survival_rounds == 0 and wave == 4:\n                        if new_zombie in c.CONVERT_ZOMBIE_IN_POOL:\n                            new_zombie = c.CONVERT_ZOMBIE_IN_POOL[new_zombie]\n                    elif survival_rounds > 0 or wave > 4:\n                        if random.randint(1, 3) == 1:  # 1/3概率水上，暂时人为设定\n                            if new_zombie in c.CONVERT_ZOMBIE_IN_POOL:\n                                new_zombie = c.CONVERT_ZOMBIE_IN_POOL[\n                                    new_zombie\n                                ]\n                    # 首先几轮不出水生僵尸\n                    elif new_zombie in c.WATER_ZOMBIE:\n                        continue\n                if c.CREATE_ZOMBIE_DICT[new_zombie][0] <= zombie_volume:\n                    zombie_list.append(new_zombie)\n                    zombie_volume -= c.CREATE_ZOMBIE_DICT[new_zombie][0]\n            waves.append(zombie_list)\n\n        self.waves = waves\n\n        # 针对有泳池的关卡\n        # 表示尚未生成最后一波中从水里冒出来的僵尸\n        self.created_zombie_from_pool = False\n\n    # 僵尸的刷新机制\n    def refreshWaves(self, current_time, survival_rounds=0):\n        # 最后一波或者大于最后一波\n        # 如果在夜晚按需从墓碑生成僵尸 有泳池时从水中生成僵尸\n        # 否则直接return\n        if self.wave_num >= self.map_data[c.NUM_FLAGS] * 10:\n            if self.map_data[c.BACKGROUND_TYPE] == c.BACKGROUND_NIGHT:\n                # 生长墓碑\n                if not self.new_grave_added:\n                    if current_time - self.wave_time > 100:\n                        # 墓碑最多有12个\n                        if len(self.grave_set) < 12:\n                            unoccupied = []\n                            occupied = []\n                            # 毁灭菇坑与冰道应当特殊化\n                            exception_objects = {c.HOLE, c.ICEFROZENPLOT}\n                            # 遍历能生成墓碑的区域\n                            for map_y in range(0, 4):\n                                for map_x in range(4, 8):\n                                    # 为空、为毁灭菇坑、为冰道时看作未被植物占据\n                                    if (\n                                        not self.map.map[map_y][map_x][\n                                            c.MAP_PLANT\n                                        ]\n                                    ) or (\n                                        all(\n                                            (i in exception_objects)\n                                            for i in self.map.map[map_y][\n                                                map_x\n                                            ][c.MAP_PLANT]\n                                        )\n                                    ):\n                                        unoccupied.append((map_x, map_y))\n                                    # 已有墓碑的格子不应该放到任何列表中\n                                    elif (\n                                        c.GRAVE\n                                        not in self.map.map[map_y][map_x][\n                                            c.MAP_PLANT\n                                        ]\n                                    ):\n                                        occupied.append((map_x, map_y))\n                            if unoccupied:\n                                target = unoccupied[\n                                    random.randint(0, len(unoccupied) - 1)\n                                ]\n                                map_x, map_y = target\n                                posX, posY = self.map.getMapGridPos(\n                                    map_x, map_y\n                                )\n                                self.plant_groups[map_y].add(\n                                    plant.Grave(posX, posY)\n                                )\n                                self.map.map[map_y][map_x][c.MAP_PLANT].add(\n                                    c.GRAVE\n                                )\n                                self.grave_set.add((map_x, map_y))\n                            elif occupied:\n                                target = occupied[\n                                    random.randint(0, len(occupied) - 1)\n                                ]\n                                map_x, map_y = target\n                                posX, posY = self.map.getMapGridPos(\n                                    map_x, map_y\n                                )\n                                for i in self.plant_groups[map_y]:\n                                    checkMapX, _ = self.map.getMapIndex(\n                                        i.rect.centerx, i.rect.bottom\n                                    )\n                                    if map_x == checkMapX:\n                                        # 不杀死毁灭菇坑和冰道\n                                        if i.name not in exception_objects:\n                                            i.health = 0\n                                self.plant_groups[map_y].add(\n                                    plant.Grave(posX, posY)\n                                )\n                                self.map.map[map_y][map_x][c.MAP_PLANT].add(\n                                    c.GRAVE\n                                )\n                                self.grave_set.add((map_x, map_y))\n                            self.new_grave_added = True\n                # 从墓碑中生成僵尸\n                if not self.grave_zombie_created:\n                    if current_time - self.wave_time > 1500:\n                        for item in self.grave_set:\n                            item_x, item_y = self.map.getMapGridPos(*item)\n                            # 目前设定：1/2概率普通僵尸，1/2概率路障僵尸\n                            if random.randint(0, 1):\n                                self.zombie_groups[item[1]].add(\n                                    zombie.NormalZombie(\n                                        item_x, item_y, self.head_group\n                                    )\n                                )\n                            else:\n                                self.zombie_groups[item[1]].add(\n                                    zombie.ConeHeadZombie(\n                                        item_x, item_y, self.head_group\n                                    )\n                                )\n                        self.grave_zombie_created = True\n            elif (\n                self.map_data[c.BACKGROUND_TYPE] in c.POOL_EQUIPPED_BACKGROUNDS\n            ):\n                if not self.created_zombie_from_pool:\n                    if current_time - self.wave_time > 1500:\n                        for i in range(3):\n                            # 水中倒数四列内可以在此时产生僵尸。共产生3个\n                            map_x, map_y = random.randint(\n                                5, 8\n                            ), random.randint(2, 3)\n                            item_x, item_y = self.map.getMapGridPos(\n                                map_x, map_y\n                            )\n                            # 用随机数指定产生的僵尸类型\n                            # 暂时设定为生成概率相同\n                            zombie_type = random.randint(1, 3)\n                            if zombie_type == 1:\n                                self.zombie_groups[map_y].add(\n                                    zombie.BucketHeadDuckyTubeZombie(\n                                        item_x, item_y, self.head_group\n                                    )\n                                )\n                            elif zombie_type == 2:\n                                self.zombie_groups[map_y].add(\n                                    zombie.ConeHeadDuckyTubeZombie(\n                                        item_x, item_y, self.head_group\n                                    )\n                                )\n                            else:\n                                self.zombie_groups[map_y].add(\n                                    zombie.DuckyTubeZombie(\n                                        item_x, item_y, self.head_group\n                                    )\n                                )\n                        self.created_zombie_from_pool = True\n            return\n\n        # 还未开始出现僵尸\n        if self.wave_num == 0:\n            if self.wave_time == 0:    # 表明刚刚开始游戏\n                self.wave_time = current_time\n            else:\n                if (survival_rounds == 0) and (\n                    self.bar_type == c.CHOOSEBAR_STATIC\n                ):   # 首次选卡等待时间较长\n                    if current_time - self.wave_time >= 18000:\n                        self.wave_num += 1\n                        self.wave_time = current_time\n                        self.wave_zombies = self.waves[self.wave_num - 1]\n                        self.zombie_num = len(self.wave_zombies)\n                        c.SOUND_ZOMBIE_COMING.play()\n                else:\n                    if current_time - self.wave_time >= 6000:\n                        self.wave_num += 1\n                        self.wave_time = current_time\n                        self.wave_zombies = self.waves[self.wave_num - 1]\n                        self.zombie_num = len(self.wave_zombies)\n                        c.SOUND_ZOMBIE_COMING.play()\n            return\n        if self.wave_num % 10 != 9:\n            if (\n                current_time - self.wave_time\n                >= 25000 + random.randint(0, 6000)\n            ) or (\n                self.bar_type == c.CHOOSEBAR_BOWLING\n                and current_time - self.wave_time\n                >= 12500 + random.randint(0, 3000)\n            ):\n                self.wave_num += 1\n                self.wave_time = current_time\n                self.wave_zombies = self.waves[self.wave_num - 1]\n                self.zombie_num = len(self.wave_zombies)\n                c.SOUND_ZOMBIE_VOICE.play()\n        else:\n            if (current_time - self.wave_time >= 45000) or (\n                self.bar_type != c.CHOOSEBAR_STATIC\n                and current_time - self.wave_time >= 25000\n            ):\n                self.wave_num += 1\n                self.wave_time = current_time\n                self.wave_zombies = self.waves[self.wave_num - 1]\n                self.zombie_num = len(self.wave_zombies)\n                # 一大波时播放音效\n                c.SOUND_HUGE_WAVE_APPROCHING.play()\n                return\n            elif (current_time - self.wave_time >= 43000) or (\n                self.bar_type != c.CHOOSEBAR_STATIC\n                and current_time - self.wave_time >= 23000\n            ):\n                self.show_hugewave_approching_time = current_time\n\n        zombie_nums = 0\n        for i in range(self.map_y_len):\n            zombie_nums += len(self.zombie_groups[i])\n        if (\n            self.zombie_num\n            and (zombie_nums / self.zombie_num < random.uniform(0.15, 0.25))\n            and (current_time - self.wave_time > 4000)\n        ):\n            # 当僵尸所剩无几并且时间过了4000 ms以上时，改变时间记录，使得2000 ms后刷新僵尸（所以需要判断剩余时间是否大于2000 ms）\n            if self.bar_type == c.CHOOSEBAR_STATIC:\n                if current_time - 43000 < self.wave_time:    # 判断剩余时间是否有2000 ms\n                    self.wave_time = current_time - 43000    # 即倒计时2000 ms\n            else:\n                if current_time - 23000 < self.wave_time:    # 判断剩余时间是否有2000 ms\n                    self.wave_time = current_time - 23000    # 即倒计时2000 ms\n\n    # 旧机制，目前仅用于调试\n    def setupZombies(self):\n        def takeTime(element):\n            return element[0]\n\n        self.zombie_list = []\n        for data in self.map_data[c.ZOMBIE_LIST]:\n            self.zombie_list.append(\n                (data['time'], data['name'], data['map_y'])\n            )\n        self.zombie_start_time = 0\n        self.zombie_list.sort(key=takeTime)\n\n    def setupCars(self):\n        self.cars = []\n        for i in range(self.map_y_len):\n            y = self.map.getMapGridPos(0, i)[1]\n            self.cars.append(plant.Car(-45, y + 20, i))\n\n    # 更新函数每帧被调用，将鼠标事件传入给状态处理函数\n    def update(self, surface, current_time, mouse_pos, mouse_click):\n        self.current_time = self.game_info[c.CURRENT_TIME] = self.gameTime(\n            current_time\n        )\n        if self.state == c.CHOOSE:\n            self.choose(mouse_pos, mouse_click)\n        elif self.state == c.PLAY:\n            self.play(mouse_pos, mouse_click)\n\n        self.draw(surface)\n\n    def gameTime(self, current_time):\n        # 扣除暂停时间\n        if not self.pause:\n            self.before_pause_time = current_time - self.pause_time\n        else:\n            self.pause_time = current_time - self.before_pause_time\n        return self.before_pause_time\n\n    def initBowlingMap(self):\n        for x in range(3, self.map_x_len):\n            for y in range(self.map_y_len):\n                self.map.setMapGridType(\n                    x, y, c.MAP_UNAVAILABLE\n                )   # 将坚果保龄球红线右侧设置为不可种植任何植物\n\n    def initState(self):\n        if c.CHOOSEBAR_TYPE in self.map_data:\n            self.bar_type = self.map_data[c.CHOOSEBAR_TYPE]\n        else:\n            self.bar_type = c.CHOOSEBAR_STATIC\n\n        if self.bar_type == c.CHOOSEBAR_STATIC:\n            self.initChoose()\n        else:\n            card_pool = menubar.getCardPool(self.map_data[c.CARD_POOL])\n            self.initPlay(card_pool)\n            if self.bar_type == c.CHOOSEBAR_BOWLING:\n                self.initBowlingMap()\n\n        self.setupLittleMenu()\n\n    def initChoose(self):\n        self.state = c.CHOOSE\n        self.panel = menubar.Panel(\n            c.CARDS_TO_CHOOSE,\n            self.map_data[c.INIT_SUN_NAME],\n            self.background_type,\n        )\n\n        # 播放选卡音乐\n        pg.mixer.music.stop()\n        pg.mixer.music.load(\n            os.path.join(c.PATH_MUSIC_DIR, 'chooseYourSeeds.opus')\n        )\n        pg.mixer.music.play(-1, 0)\n        pg.mixer.music.set_volume(self.game_info[c.SOUND_VOLUME])\n\n    def choose(self, mouse_pos, mouse_click):\n        # 如果暂停\n        if self.show_game_menu:\n            self.pauseAndCheckMenuOptions(mouse_pos, mouse_click)\n            return\n\n        elif mouse_pos and mouse_click[0]:\n            self.panel.checkCardClick(mouse_pos)\n            if self.panel.checkStartButtonClick(mouse_pos):\n                self.initPlay(self.panel.getSelectedCards())\n            elif self.inArea(self.little_menu_rect, *mouse_pos):\n                self.show_game_menu = True\n                c.SOUND_BUTTON_CLICK.play()\n\n    def initPlay(self, card_list):\n\n        # 播放bgm\n        pg.mixer.music.stop()\n        pg.mixer.music.load(os.path.join(c.PATH_MUSIC_DIR, self.bgm))\n        pg.mixer.music.play(-1, 0)\n        pg.mixer.music.set_volume(self.game_info[c.SOUND_VOLUME])\n\n        self.state = c.PLAY\n        if self.bar_type == c.CHOOSEBAR_STATIC:\n            self.menubar = menubar.MenuBar(\n                card_list, self.map_data[c.INIT_SUN_NAME]\n            )\n        else:\n            self.menubar = menubar.MoveBar(card_list)\n\n        # 是否拖住植物或者铲子\n        self.drag_plant = False\n        self.drag_shovel = False\n\n        self.hint_image = None\n        self.hint_plant = False\n\n        # 用种下植物的名称与位置元组判断是否需要刷新僵尸的攻击对象\n        # 种植植物后应当刷新僵尸的攻击对象，当然，默认初始时不用刷新\n        self.new_plant_and_positon = None\n\n        if (\n            self.background_type in c.DAYTIME_BACKGROUNDS\n            and self.bar_type == c.CHOOSEBAR_STATIC\n        ):\n            self.produce_sun = True\n            self.fallen_sun = 0   # 已掉落的阳光\n        else:\n            self.produce_sun = False\n        self.sun_timer = self.current_time\n\n        self.removeMouseImage()\n        self.setupGroups()\n        if self.map_data[c.SPAWN_ZOMBIES] == c.SPAWN_ZOMBIES_LIST:\n            self.setupZombies()\n        else:\n            # 僵尸波数数据及僵尸生成数据\n            self.wave_num = 0   # 还未出现僵尸时定义为0\n            self.wave_time = 0\n            self.wave_zombies = []\n            self.zombie_num = 0\n\n            # 暂时没有生存模式，所以 survival_rounds = 0\n            if c.INEVITABLE_ZOMBIE_DICT in self.map_data:\n                self.createWaves(\n                    useable_zombies=self.map_data[c.INCLUDED_ZOMBIES],\n                    num_flags=self.map_data[c.NUM_FLAGS],\n                    survival_rounds=0,\n                    inevitable_zombie_dict=self.map_data[\n                        c.INEVITABLE_ZOMBIE_DICT\n                    ],\n                )\n            else:\n                self.createWaves(\n                    useable_zombies=self.map_data[c.INCLUDED_ZOMBIES],\n                    num_flags=self.map_data[c.NUM_FLAGS],\n                    survival_rounds=0,\n                )\n        self.setupCars()\n\n        # 地图有铲子才添加铲子\n        if self.has_shovel:\n            #  导入小铲子\n            frame_rect = (0, 0, 71, 67)\n            self.shovel = tool.get_image_alpha(\n                tool.GFX[c.SHOVEL], *frame_rect, c.BLACK, 1.1\n            )\n            self.shovel_rect = self.shovel.get_rect()\n            frame_rect = (0, 0, 77, 75)\n            self.shovel_positon = (608, 1)\n            self.shovel_box = tool.get_image_alpha(\n                tool.GFX[c.SHOVEL_BOX], *frame_rect, c.BLACK, 1.1\n            )\n            self.shovel_box_rect = self.shovel_box.get_rect()\n            self.shovel_rect.x = self.shovel_box_rect.x = self.shovel_positon[\n                0\n            ]\n            self.shovel_rect.y = self.shovel_box_rect.y = self.shovel_positon[\n                1\n            ]\n\n        self.setupLevelProgressBarImage()\n\n        self.setupHugeWaveApprochingImage()\n        self.show_hugewave_approching_time = -2000   # 防止设置为0时刚刚打开游戏就已经启动红字\n\n        if self.map_data[c.BACKGROUND_TYPE] == c.BACKGROUND_NIGHT:\n            # 判断墓碑数量等级\n            # 0为无墓碑，1为少量墓碑，2为中等量墓碑，3为大量墓碑\n            if c.GRADE_GRAVES in self.map_data:\n                grade_graves = self.map_data[c.GRADE_GRAVES]\n            # 缺省为少量墓碑\n            else:\n                grade_graves = 1\n\n            grave_volume = c.GRAVES_GRADE_INFO[grade_graves]\n            self.grave_set = set()\n            while len(self.grave_set) < grave_volume:\n                map_x = random.randint(4, 8)    # 注意是从0开始编号\n                map_y = random.randint(0, 4)\n                self.grave_set.add((map_x, map_y))\n            if self.grave_set:\n                for i in self.grave_set:\n                    map_x, map_y = i\n                    posX, posY = self.map.getMapGridPos(map_x, map_y)\n                    self.plant_groups[map_y].add(plant.Grave(posX, posY))\n                    self.map.map[map_y][map_x][c.MAP_PLANT].add(c.GRAVE)\n            self.grave_zombie_created = False\n            self.new_grave_added = False\n\n    # 小菜单\n    def setupLittleMenu(self):\n        # 具体运行游戏必定有个小菜单, 导入菜单和选项\n        frame_rect = (0, 0, 108, 31)\n        self.little_menu = tool.get_image_alpha(\n            tool.GFX[c.LITTLE_MENU], *frame_rect, c.BLACK, 1.1\n        )\n        self.little_menu_rect = self.little_menu.get_rect()\n        self.little_menu_rect.x = 690\n        self.little_menu_rect.y = 0\n\n        # 弹出的菜单框\n        frame_rect = (0, 0, 500, 500)\n        self.big_menu = tool.get_image_alpha(\n            tool.GFX[c.BIG_MENU], *frame_rect, c.BLACK, 1.1\n        )\n        self.big_menu_rect = self.big_menu.get_rect()\n        self.big_menu_rect.x = 150\n        self.big_menu_rect.y = 0\n\n        # 返回按钮，用字体渲染实现，增强灵活性\n        # 建立一个按钮大小的surface对象\n        self.return_button = pg.Surface((376, 96))\n        self.return_button.set_colorkey(c.BLACK)    # 避免多余区域显示成黑色\n        self.return_button_rect = self.return_button.get_rect()\n        self.return_button_rect.x = 220\n        self.return_button_rect.y = 440\n        font = pg.font.Font(c.FONT_PATH, 40)\n        font.bold = True\n        text = font.render('返回游戏', True, c.YELLOWGREEN)\n        text_rect = text.get_rect()\n        text_rect.x = 105\n        text_rect.y = 18\n        self.return_button.blit(text, text_rect)\n\n        # 重新开始按钮\n        frame_rect = (0, 0, 207, 45)\n        self.restart_button = tool.get_image_alpha(\n            tool.GFX[c.RESTART_BUTTON], *frame_rect, c.BLACK, 1.1\n        )\n        self.restart_button_rect = self.restart_button.get_rect()\n        self.restart_button_rect.x = 295\n        self.restart_button_rect.y = 325\n\n        # 主菜单按钮\n        frame_rect = (0, 0, 206, 43)\n        self.mainMenu_button = tool.get_image_alpha(\n            tool.GFX[c.MAINMENU_BUTTON], *frame_rect, c.BLACK, 1.1\n        )\n        self.mainMenu_button_rect = self.mainMenu_button.get_rect()\n        self.mainMenu_button_rect.x = 299\n        self.mainMenu_button_rect.y = 372\n\n        # 音量+、音量-\n        frame_rect = (0, 0, 39, 41)\n        font = pg.font.Font(c.FONT_PATH, 35)\n        font.bold = True\n        # 音量+\n        self.sound_volume_plus_button = tool.get_image_alpha(\n            tool.GFX[c.SOUND_VOLUME_BUTTON], *frame_rect, c.BLACK\n        )\n        sign = font.render('+', True, c.YELLOWGREEN)\n        sign_rect = sign.get_rect()\n        sign_rect.x = 8\n        sign_rect.y = -4\n        self.sound_volume_plus_button.blit(sign, sign_rect)\n        self.sound_volume_plus_button_rect = (\n            self.sound_volume_plus_button.get_rect()\n        )\n        self.sound_volume_plus_button_rect.x = 500\n        # 音量-\n        self.sound_volume_minus_button = tool.get_image_alpha(\n            tool.GFX[c.SOUND_VOLUME_BUTTON], *frame_rect, c.BLACK\n        )\n        sign = font.render('-', True, c.YELLOWGREEN)\n        sign_rect = sign.get_rect()\n        sign_rect.x = 12\n        sign_rect.y = -8\n        self.sound_volume_minus_button.blit(sign, sign_rect)\n        self.sound_volume_minus_button_rect = (\n            self.sound_volume_minus_button.get_rect()\n        )\n        self.sound_volume_minus_button_rect.x = 450\n        # 音量+、-应当处于同一高度\n        self.sound_volume_minus_button_rect.y = (\n            self.sound_volume_plus_button_rect.y\n        ) = 250\n\n    def pauseAndCheckMenuOptions(self, mouse_pos, mouse_click):\n        # 设置暂停状态\n        self.pause = True\n        # 暂停播放音乐\n        pg.mixer.music.pause()\n        if mouse_click[0]:\n            # 返回键\n            if self.inArea(self.return_button_rect, *mouse_pos):\n                # 终止暂停，停止显示菜单\n                self.pause = False\n                self.show_game_menu = False\n                # 继续播放音乐\n                pg.mixer.music.unpause()\n                # 播放点击音效\n                c.SOUND_BUTTON_CLICK.play()\n            # 重新开始键\n            elif self.inArea(self.restart_button_rect, *mouse_pos):\n                self.done = True\n                self.next = c.LEVEL\n                # 播放点击音效\n                c.SOUND_BUTTON_CLICK.play()\n            # 主菜单键\n            elif self.inArea(self.mainMenu_button_rect, *mouse_pos):\n                self.done = True\n                self.next = c.MAIN_MENU\n                self.persist = self.game_info\n                self.persist[c.CURRENT_TIME] = 0\n                # 播放点击音效\n                c.SOUND_BUTTON_CLICK.play()\n            # 音量+\n            elif self.inArea(self.sound_volume_plus_button_rect, *mouse_pos):\n                self.game_info[c.SOUND_VOLUME] = round(\n                    min(self.game_info[c.SOUND_VOLUME] + 0.05, 1), 2\n                )\n                # 一般不会有人想把音乐和音效分开设置，故pg.mixer.Sound.set_volume()和pg.mixer.music.set_volume()需要一起用\n                pg.mixer.music.set_volume(self.game_info[c.SOUND_VOLUME])\n                for i in c.SOUNDS:\n                    i.set_volume(self.game_info[c.SOUND_VOLUME])\n                c.SOUND_BUTTON_CLICK.play()\n                # 将音量信息存档\n                self.saveUserData()\n            elif self.inArea(self.sound_volume_minus_button_rect, *mouse_pos):\n                self.game_info[c.SOUND_VOLUME] = round(\n                    max(self.game_info[c.SOUND_VOLUME] - 0.05, 0), 2\n                )\n                # 一般不会有人想把音乐和音效分开设置，故pg.mixer.Sound.set_volume()和pg.mixer.music.set_volume()需要一起用\n                pg.mixer.music.set_volume(self.game_info[c.SOUND_VOLUME])\n                for i in c.SOUNDS:\n                    i.set_volume(self.game_info[c.SOUND_VOLUME])\n                c.SOUND_BUTTON_CLICK.play()\n                # 将音量信息存档\n                self.saveUserData()\n\n    # 一大波僵尸来袭图片显示\n    def setupHugeWaveApprochingImage(self):\n        frame_rect = (0, 0, 492, 80)\n        self.huge_wave_approching_image = tool.get_image_alpha(\n            tool.GFX[c.HUGE_WAVE_APPROCHING], *frame_rect, c.BLACK, 1\n        )\n        self.huge_wave_approching_image_rect = (\n            self.huge_wave_approching_image.get_rect()\n        )\n        self.huge_wave_approching_image_rect.x = 140    # 猜的\n        self.huge_wave_approching_image_rect.y = 250    # 猜的\n\n    # 关卡进程显示设置\n    def setupLevelProgressBarImage(self):\n        # 注意：定位一律采用与主进度条的相对位置\n\n        # 主进度条\n        frame_rect = (0, 0, 158, 26)\n        self.level_progress_bar_image = tool.get_image_alpha(\n            tool.GFX[c.LEVEL_PROGRESS_BAR], *frame_rect, c.BLACK, 1\n        )\n        self.level_progress_bar_image_rect = (\n            self.level_progress_bar_image.get_rect()\n        )\n        self.level_progress_bar_image_rect.x = 600\n        self.level_progress_bar_image_rect.y = 574\n\n        # 僵尸头\n        frame_rect = (0, 0, 23, 25)\n        self.level_progress_zombie_head_image = tool.get_image_alpha(\n            tool.GFX[c.LEVEL_PROGRESS_ZOMBIE_HEAD], *frame_rect, c.BLACK, 1\n        )\n        self.level_progress_zombie_head_image_rect = (\n            self.level_progress_zombie_head_image.get_rect()\n        )\n        self.level_progress_zombie_head_image_rect.x = (\n            self.level_progress_bar_image_rect.x + 75\n        )\n        self.level_progress_zombie_head_image_rect.y = (\n            self.level_progress_bar_image_rect.y - 3\n        )\n\n        # 旗帜（这里只包括最后一面）\n        frame_rect = (0, 0, 20, 18)\n        self.level_progress_flag = tool.get_image_alpha(\n            tool.GFX[c.LEVEL_PROGRESS_FLAG], *frame_rect, c.BLACK, 1\n        )\n        self.level_progress_flag_rect = self.level_progress_flag.get_rect()\n        self.level_progress_flag_rect.x = (\n            self.level_progress_bar_image_rect.x - 78\n        )\n        self.level_progress_flag_rect.y = (\n            self.level_progress_bar_image_rect.y - 3\n        )\n\n    # 用小铲子移除植物\n    def shovelRemovePlant(self, mouse_pos):\n        x, y = mouse_pos\n        map_x, map_y = self.map.getMapIndex(x, y)\n        if not self.map.isValid(map_x, map_y):\n            return\n        for i in self.plant_groups[map_y]:\n            if (\n                x >= i.rect.x\n                and x <= i.rect.right\n                and y >= i.rect.y\n                and y <= i.rect.bottom\n            ):\n                if i.name in c.NON_PLANT_OBJECTS:\n                    continue\n                if i.name in c.SKIP_ZOMBIE_COLLISION_CHECK_WHEN_WORKING:\n                    if i.start_boom:\n                        continue\n                # 优先移除花盆、睡莲上的植物而非花盆、睡莲本身\n                if len(self.map.map[map_y][map_x][c.MAP_PLANT]) >= 2:\n                    if c.LILYPAD in self.map.map[map_y][map_x][c.MAP_PLANT]:\n                        if i.name == c.LILYPAD:\n                            continue\n                    elif '花盆（未实现）' in self.map.map[map_y][map_x][c.MAP_PLANT]:\n                        if i.name == '花盆（未实现）':\n                            continue\n                self.killPlant(i, shovel=True)\n                # 使用后默认铲子复原\n                self.drag_shovel = not self.drag_shovel\n                self.removeMouseImagePlus()\n                return\n\n    def play(self, mouse_pos, mouse_click):\n        # 如果暂停\n        if self.show_game_menu:\n            self.pauseAndCheckMenuOptions(mouse_pos, mouse_click)\n            return\n\n        if self.map_data[c.SPAWN_ZOMBIES] == c.SPAWN_ZOMBIES_LIST:\n            # 旧僵尸生成方式\n            if self.zombie_start_time == 0:\n                self.zombie_start_time = self.current_time\n            elif len(self.zombie_list) > 0:\n                data = self.zombie_list[0]  # 因此要求僵尸列表按照时间顺序排列\n                # data内容排列：[0]:时间 [1]:名称 [2]:坐标\n                if data[0] <= (self.current_time - self.zombie_start_time):\n                    self.createZombie(data[1], data[2])\n                    self.zombie_list.remove(data)\n        else:\n            # 新僵尸生成方式\n            self.refreshWaves(self.current_time)\n            for i in self.wave_zombies:\n                self.createZombie(i)\n            else:\n                self.wave_zombies = []\n\n        for i in range(self.map_y_len):\n            self.bullet_groups[i].update(self.game_info)\n            self.plant_groups[i].update(self.game_info)\n            self.zombie_groups[i].update(self.game_info)\n            self.hypno_zombie_groups[i].update(self.game_info)\n            # 清除走出去的魅惑僵尸\n            for zombie in self.hypno_zombie_groups[i]:\n                if zombie.rect.x > c.SCREEN_WIDTH:\n                    zombie.kill()\n\n        self.head_group.update(self.game_info)\n        self.sun_group.update(self.game_info)\n\n        if self.produce_sun:\n            # 原版阳光掉落机制：(已掉落阳光数*100 ms + 4250 ms) 与 9500 ms的最小值，再加 0 ~ 2750 ms 之间的一个数\n            if (self.current_time - self.sun_timer) > min(\n                c.PRODUCE_SUN_INTERVAL + 100 * self.fallen_sun, 9500\n            ) + random.randint(0, 2750):\n                self.sun_timer = self.current_time\n                map_x, map_y = self.map.getRandomMapIndex()\n                x, y = self.map.getMapGridPos(map_x, map_y)\n                self.sun_group.add(plant.Sun(x, 0, x, y))\n                self.fallen_sun += 1\n\n        # 检查有没有捡到阳光\n        clicked_sun = False\n        clicked_cards_or_map = False\n        if (\n            not self.drag_plant\n            and not self.drag_shovel\n            and mouse_pos\n            and mouse_click[0]\n        ):\n            for sun in self.sun_group:\n                if sun.checkCollision(*mouse_pos):\n                    self.menubar.increaseSunValue(sun.sun_value)\n                    clicked_sun = True\n                    # 播放收集阳光的音效\n                    c.SOUND_COLLECT_SUN.play()\n\n        # 拖动植物或者铲子\n        if (\n            not self.drag_plant\n            and mouse_pos\n            and mouse_click[0]\n            and not clicked_sun\n        ):\n            self.click_result = self.menubar.checkCardClick(mouse_pos)\n            if self.click_result:\n                self.setupMouseImage(\n                    self.click_result[0], self.click_result[1]\n                )\n                self.click_result[1].clicked = True\n                clicked_cards_or_map = True\n                # 播放音效\n                c.SOUND_CLICK_CARD.play()\n        elif self.drag_plant:\n            if mouse_click[1]:\n                self.removeMouseImage()\n                clicked_cards_or_map = True\n                self.click_result[1].clicked = False\n            elif mouse_click[0]:\n                if self.menubar.checkMenuBarClick(mouse_pos):\n                    self.click_result[1].clicked = False\n                    self.removeMouseImage()\n                else:\n                    self.addPlant()\n            elif mouse_pos is None:\n                self.setupHintImage()\n        elif self.drag_shovel:\n            if mouse_click[1]:\n                self.removeMouseImagePlus()\n\n        # 检查是否点击菜单\n        if mouse_click[0] and (not clicked_sun) and (not clicked_cards_or_map):\n            if self.inArea(self.little_menu_rect, *mouse_pos):\n                # 暂停 显示菜单\n                self.show_game_menu = True\n                # 播放点击音效\n                c.SOUND_BUTTON_CLICK.play()\n            elif self.has_shovel:\n                if self.inArea(self.shovel_box_rect, *mouse_pos):\n                    self.drag_shovel = not self.drag_shovel\n                    if not self.drag_shovel:\n                        self.removeMouseImagePlus()\n                    # 播放点击铲子的音效\n                    c.SOUND_SHOVEL.play()\n                elif self.drag_shovel:\n                    # 移出这地方的植物\n                    self.shovelRemovePlant(mouse_pos)\n\n        for car in self.cars:\n            if car:\n                car.update(self.game_info)\n\n        self.menubar.update(self.current_time)\n\n        # 检查碰撞\n        self.checkBulletCollisions()\n        self.checkZombieCollisions()\n        self.checkPlants()\n        self.checkCarCollisions()\n        self.checkGameState()\n\n    def createZombie(self, name, map_y=None):\n        # 有指定时按照指定生成，无指定时随机位置生成\n        # 0:白天 1:夜晚 2:泳池 3:浓雾 4:屋顶 5:月夜 6:坚果保龄球\n        if map_y == None:\n            # 情况复杂：分水路和陆路，不能简单实现，需要另外加判断\n            # 0, 1, 4, 5路为陆路，2, 3路为水路\n            if self.map_data[c.BACKGROUND_TYPE] in c.POOL_EQUIPPED_BACKGROUNDS:\n                if name in c.WATER_ZOMBIE:\n                    map_y = random.randint(2, 3)\n                elif name == '这里应该换成气球僵尸的名字（最好写调用的变量名，最好不要直接写，保持风格统一）':\n                    map_y = random.randint(0, 5)\n                else:   # 陆生僵尸\n                    map_y = random.randint(0, 3)\n                    if map_y >= 2:   # 后两路的map_y应当+2\n                        map_y += 2\n            elif self.map_data[c.BACKGROUND_TYPE] == c.BACKGROUND_SINGLE:\n                map_y = 2\n            elif self.map_data[c.BACKGROUND_TYPE] == c.BACKGROUND_TRIPLE:\n                map_y = random.randint(1, 3)\n            else:\n                map_y = random.randint(0, 4)\n\n        if self.map_data[c.SPAWN_ZOMBIES] == c.SPAWN_ZOMBIES_AUTO:\n            # 旗帜波出生点右移\n            if self.wave_num % 10:\n                huge_wave_move = 0\n            else:\n                huge_wave_move = 40\n        else:\n            huge_wave_move = 0\n        x, y = self.map.getMapGridPos(0, map_y)\n\n        # 新增的僵尸也需要在这里声明\n        match name:\n            case c.NORMAL_ZOMBIE:\n                self.zombie_groups[map_y].add(\n                    zombie.NormalZombie(\n                        c.ZOMBIE_START_X\n                        + random.randint(-20, 20)\n                        + huge_wave_move,\n                        y,\n                        self.head_group,\n                    )\n                )\n            case c.CONEHEAD_ZOMBIE:\n                self.zombie_groups[map_y].add(\n                    zombie.ConeHeadZombie(\n                        c.ZOMBIE_START_X\n                        + random.randint(-20, 20)\n                        + huge_wave_move,\n                        y,\n                        self.head_group,\n                    )\n                )\n            case c.BUCKETHEAD_ZOMBIE:\n                self.zombie_groups[map_y].add(\n                    zombie.BucketHeadZombie(\n                        c.ZOMBIE_START_X\n                        + random.randint(-20, 20)\n                        + huge_wave_move,\n                        y,\n                        self.head_group,\n                    )\n                )\n            case c.FLAG_ZOMBIE:\n                self.zombie_groups[map_y].add(\n                    zombie.FlagZombie(c.ZOMBIE_START_X, y, self.head_group)\n                )\n            case c.NEWSPAPER_ZOMBIE:\n                self.zombie_groups[map_y].add(\n                    zombie.NewspaperZombie(\n                        c.ZOMBIE_START_X\n                        + random.randint(-20, 20)\n                        + huge_wave_move,\n                        y,\n                        self.head_group,\n                    )\n                )\n            case c.FOOTBALL_ZOMBIE:\n                self.zombie_groups[map_y].add(\n                    zombie.FootballZombie(\n                        c.ZOMBIE_START_X\n                        + random.randint(-20, 20)\n                        + huge_wave_move,\n                        y,\n                        self.head_group,\n                    )\n                )\n            case c.DUCKY_TUBE_ZOMBIE:\n                self.zombie_groups[map_y].add(\n                    zombie.DuckyTubeZombie(\n                        c.ZOMBIE_START_X\n                        + random.randint(-20, 20)\n                        + huge_wave_move,\n                        y,\n                        self.head_group,\n                    )\n                )\n            case c.CONEHEAD_DUCKY_TUBE_ZOMBIE:\n                self.zombie_groups[map_y].add(\n                    zombie.ConeHeadDuckyTubeZombie(\n                        c.ZOMBIE_START_X\n                        + random.randint(-20, 20)\n                        + huge_wave_move,\n                        y,\n                        self.head_group,\n                    )\n                )\n            case c.BUCKETHEAD_DUCKY_TUBE_ZOMBIE:\n                self.zombie_groups[map_y].add(\n                    zombie.BucketHeadDuckyTubeZombie(\n                        c.ZOMBIE_START_X\n                        + random.randint(-20, 20)\n                        + huge_wave_move,\n                        y,\n                        self.head_group,\n                    )\n                )\n            case c.SCREEN_DOOR_ZOMBIE:\n                self.zombie_groups[map_y].add(\n                    zombie.ScreenDoorZombie(\n                        c.ZOMBIE_START_X\n                        + random.randint(-20, 20)\n                        + huge_wave_move,\n                        y,\n                        self.head_group,\n                    )\n                )\n            case c.POLE_VAULTING_ZOMBIE:\n                # 本来撑杆跳生成位置不同，对齐左端可认为修正了一部分（看作移动了70），只需要相对修改即可\n                self.zombie_groups[map_y].add(\n                    zombie.PoleVaultingZombie(\n                        c.ZOMBIE_START_X\n                        + random.randint(0, 10)\n                        + huge_wave_move,\n                        y,\n                        self.head_group,\n                    )\n                )\n            case c.ZOMBONI:\n                # 冰车僵尸生成位置不同\n                self.zombie_groups[map_y].add(\n                    zombie.Zomboni(\n                        c.ZOMBIE_START_X\n                        + random.randint(0, 10)\n                        + huge_wave_move,\n                        y,\n                        self.plant_groups[map_y],\n                        self.map,\n                        plant.IceFrozenPlot,\n                    )\n                )\n            case c.SNORKELZOMBIE:\n                # 潜水僵尸生成位置不同\n                self.zombie_groups[map_y].add(\n                    zombie.SnorkelZombie(\n                        c.ZOMBIE_START_X\n                        + random.randint(0, 10)\n                        + huge_wave_move,\n                        y,\n                        self.head_group,\n                    )\n                )\n\n    # 能否种植物的判断：\n    # 先判断位置是否合法 isValid(map_x, map_y)\n    # 再判断位置是否可用 isMovable(map_x, map_y)\n    def canSeedPlant(self, plant_name):\n        x, y = pg.mouse.get_pos()\n        return self.map.checkPlantToSeed(x, y, plant_name)\n\n    # 种植物\n    def addPlant(self):\n        pos = self.canSeedPlant(self.plant_name)\n        if pos is None:\n            return\n\n        # 恢复植物卡片样式\n        self.click_result[1].clicked = False\n\n        if self.hint_image is None:\n            self.setupHintImage()\n        x, y = self.hint_rect.centerx, self.hint_rect.bottom\n        map_x, map_y = self.map.getMapIndex(x, y)\n\n        # 新植物也需要在这里声明\n        match self.plant_name:\n            case c.SUNFLOWER:\n                new_plant = plant.SunFlower(x, y, self.sun_group)\n            case c.PEASHOOTER:\n                new_plant = plant.PeaShooter(x, y, self.bullet_groups[map_y])\n            case c.SNOWPEASHOOTER:\n                new_plant = plant.SnowPeaShooter(\n                    x, y, self.bullet_groups[map_y]\n                )\n            case c.WALLNUT:\n                new_plant = plant.WallNut(x, y)\n            case c.CHERRYBOMB:\n                new_plant = plant.CherryBomb(x, y)\n            case c.THREEPEASHOOTER:\n                new_plant = plant.ThreePeaShooter(\n                    x, y, self.bullet_groups, map_y, self.map.background_type\n                )\n            case c.REPEATERPEA:\n                new_plant = plant.RepeaterPea(x, y, self.bullet_groups[map_y])\n            case c.CHOMPER:\n                new_plant = plant.Chomper(x, y)\n            case c.PUFFSHROOM:\n                new_plant = plant.PuffShroom(x, y, self.bullet_groups[map_y])\n            case c.POTATOMINE:\n                new_plant = plant.PotatoMine(x, y)\n            case c.SQUASH:\n                new_plant = plant.Squash(\n                    x, y, self.map.map[map_y][map_x][c.MAP_PLANT]\n                )\n            case c.SPIKEWEED:\n                new_plant = plant.Spikeweed(x, y)\n            case c.JALAPENO:\n                new_plant = plant.Jalapeno(x, y)\n            case c.SCAREDYSHROOM:\n                new_plant = plant.ScaredyShroom(\n                    x, y, self.bullet_groups[map_y]\n                )\n            case c.SUNSHROOM:\n                new_plant = plant.SunShroom(x, y, self.sun_group)\n            case c.ICESHROOM:\n                new_plant = plant.IceShroom(x, y)\n            case c.HYPNOSHROOM:\n                new_plant = plant.HypnoShroom(x, y)\n            case c.WALLNUTBOWLING:\n                new_plant = plant.WallNutBowling(x, y, map_y, self)\n            case c.REDWALLNUTBOWLING:\n                new_plant = plant.RedWallNutBowling(x, y)\n            case c.LILYPAD:\n                new_plant = plant.LilyPad(x, y)\n            case c.TORCHWOOD:\n                new_plant = plant.TorchWood(x, y, self.bullet_groups[map_y])\n            case c.STARFRUIT:\n                new_plant = plant.StarFruit(\n                    x, y, self.bullet_groups[map_y], self\n                )\n            case c.COFFEEBEAN:\n                new_plant = plant.CoffeeBean(\n                    x,\n                    y,\n                    self.plant_groups[map_y],\n                    self.map.map[map_y][map_x],\n                    self.map,\n                    map_x,\n                )\n            case c.SEASHROOM:\n                new_plant = plant.SeaShroom(x, y, self.bullet_groups[map_y])\n            case c.TALLNUT:\n                new_plant = plant.TallNut(x, y)\n            case c.TANGLEKLEP:\n                new_plant = plant.TangleKlep(x, y)\n            case c.DOOMSHROOM:\n                if self.map.grid_height_size == c.GRID_Y_SIZE:\n                    new_plant = plant.DoomShroom(\n                        x,\n                        y,\n                        self.map.map[map_y][map_x][c.MAP_PLANT],\n                        explode_y_range=2,\n                    )\n                else:\n                    new_plant = plant.DoomShroom(\n                        x,\n                        y,\n                        self.map.map[map_y][map_x][c.MAP_PLANT],\n                        explode_y_range=3,\n                    )\n            case c.GRAVEBUSTER:\n                new_plant = plant.GraveBuster(\n                    x, y, self.plant_groups[map_y], self.map, map_x\n                )\n            case c.FUMESHROOM:\n                new_plant = plant.FumeShroom(\n                    x, y, self.bullet_groups[map_y], self.zombie_groups[map_y]\n                )\n            case c.GARLIC:\n                new_plant = plant.Garlic(x, y)\n            case c.PUMPKINHEAD:\n                new_plant = plant.PumpkinHead(x, y)\n            case c.GIANTWALLNUT:\n                new_plant = plant.GiantWallNut(x, y)\n\n        if (new_plant.name in c.CAN_SLEEP_PLANTS) and (\n            self.background_type in c.DAYTIME_BACKGROUNDS\n        ):\n            new_plant.setSleep()\n            mushroom_sleep = True\n        else:\n            mushroom_sleep = False\n        self.plant_groups[map_y].add(new_plant)\n        # 种植植物后应当刷新僵尸的攻击对象\n        # 用元组表示植物的名称和格子坐标\n        self.new_plant_and_positon = (new_plant.name, (map_x, map_y))\n        if self.bar_type == c.CHOOSEBAR_STATIC:\n            self.menubar.decreaseSunValue(self.select_plant.sun_cost)\n            self.menubar.setCardFrozenTime(self.plant_name)\n        else:\n            self.menubar.deleateCard(self.select_plant)\n\n        if self.bar_type != c.CHOOSEBAR_BOWLING:    # 坚果保龄球关卡无需考虑格子被占用的情况\n            self.map.addMapPlant(\n                map_x, map_y, self.plant_name, sleep=mushroom_sleep\n            )\n        self.removeMouseImage()\n\n        # print(self.new_plant_and_positon)\n\n        # 播放种植音效\n        c.SOUND_PLANT.play()\n\n    def setupHintImage(self):\n        pos = self.canSeedPlant(self.plant_name)\n        if pos and self.mouse_image:\n            if (\n                self.hint_image\n                and pos[0] == self.hint_rect.x\n                and pos[1] == self.hint_rect.y\n            ):\n                return\n            width, height = self.mouse_rect.w, self.mouse_rect.h\n            image = pg.Surface([width, height])\n            image.blit(self.mouse_image, (0, 0), (0, 0, width, height))\n            image.set_colorkey(c.BLACK)\n            image.set_alpha(128)\n            self.hint_image = image\n            self.hint_rect = image.get_rect()\n            # 花盆、睡莲图片应当下移一些\n            if self.plant_name in {c.LILYPAD, '花盆（未实现）', c.TANGLEKLEP}:\n                self.hint_rect.centerx = pos[0]\n                self.hint_rect.bottom = pos[1] + 25\n            else:\n                self.hint_rect.centerx = pos[0]\n                self.hint_rect.bottom = pos[1]\n            self.hint_plant = True\n        else:\n            self.hint_plant = False\n\n    def setupMouseImage(self, plant_name, select_plant, colorkey=c.BLACK):\n        frame_list = tool.GFX[plant_name]\n        if plant_name in c.PLANT_RECT:\n            data = c.PLANT_RECT[plant_name]\n            x, y, width, height = (\n                data['x'],\n                data['y'],\n                data['width'],\n                data['height'],\n            )\n        else:\n            x, y = 0, 0\n            rect = frame_list[0].get_rect()\n            width, height = rect.w, rect.h\n\n        self.mouse_image = tool.get_image(\n            frame_list[0], x, y, width, height, colorkey, 1\n        )\n        self.mouse_rect = self.mouse_image.get_rect()\n        self.drag_plant = True\n        self.plant_name = plant_name\n        self.select_plant = select_plant\n\n    def removeMouseImage(self):\n        self.drag_plant = False\n        self.mouse_image = None\n        self.hint_image = None\n        self.hint_plant = False\n\n    # 移除小铲子\n    def removeMouseImagePlus(self):\n        self.drag_shovel = False\n        self.shovel_rect.x = self.shovel_positon[0]\n        self.shovel_rect.y = self.shovel_positon[1]\n\n    def checkBulletCollisions(self):\n        for i in range(self.map_y_len):\n            for bullet in self.bullet_groups[i]:\n                if bullet.name == c.FUME:\n                    continue\n                collided_func = pg.sprite.collide_mask\n                if bullet.state == c.FLY:\n                    # 利用循环而非内建精灵组碰撞判断函数，处理更加灵活，可排除已死亡僵尸\n                    for zombie in self.zombie_groups[i]:\n                        if (zombie.name == c.SNORKELZOMBIE) and (\n                            zombie.frames == zombie.swim_frames\n                        ):\n                            continue\n                        if collided_func(zombie, bullet):\n                            if zombie.state != c.DIE:\n                                zombie.setDamage(\n                                    bullet.damage,\n                                    effect=bullet.effect,\n                                    damage_type=bullet.damage_type,\n                                )\n                                bullet.setExplode()\n                                # 火球有溅射伤害\n                                if bullet.name == c.BULLET_FIREBALL:\n                                    for rangeZombie in self.zombie_groups[i]:\n                                        if abs(\n                                            rangeZombie.rect.x - bullet.rect.x\n                                        ) <= (c.GRID_X_SIZE // 2):\n                                            rangeZombie.setDamage(\n                                                c.BULLET_DAMAGE_FIREBALL_RANGE,\n                                                effect=None,\n                                                damage_type=c.ZOMBIE_DEAFULT_DAMAGE,\n                                            )\n                                break\n\n    def checkZombieCollisions(self):\n        for i in range(self.map_y_len):\n            for zombie in self.zombie_groups[i]:\n                if zombie.name == c.ZOMBONI:\n                    continue\n                if zombie.name in {c.POLE_VAULTING_ZOMBIE} and (\n                    not zombie.jumped\n                ):\n                    collided_func = pg.sprite.collide_rect_ratio(0.6)\n                else:\n                    collided_func = pg.sprite.collide_mask\n                if zombie.state != c.WALK:\n                    # 非啃咬时不用刷新\n                    if zombie.state != c.ATTACK:\n                        continue\n                    # 没有新的植物种下时不用刷新\n                    if not self.new_plant_and_positon:\n                        continue\n                    # 被攻击对象是植物时才可能刷新\n                    if zombie.prey_is_plant:\n                        # 新植物种在被攻击植物同一格时才可能刷新\n                        if (\n                            zombie.prey_map_x,\n                            zombie.prey_map_y,\n                        ) == self.new_plant_and_positon[1]:\n                            # 如果被攻击植物是睡莲和花盆，同一格种了植物必然刷新\n                            # 如果被攻击植物不是睡莲和花盆，同一格种了南瓜头才刷新\n                            if (\n                                zombie.prey.name not in {c.LILYPAD, '花盆（未实现）'}\n                            ) and (\n                                self.new_plant_and_positon[0] != c.PUMPKINHEAD\n                            ):\n                                continue\n                        else:\n                            continue\n                    else:\n                        continue\n                if zombie.can_swim and (not zombie.swimming):\n                    continue\n\n                # 以下代码为了实现各个功能，较为凌乱\n                attackable_common_plants = []\n                attackable_backup_plants = []\n                # 利用更加精细的循环判断啃咬优先顺序\n                for plant in self.plant_groups[i]:\n                    if collided_func(plant, zombie):\n                        # 优先攻击南瓜头\n                        if plant.name == c.PUMPKINHEAD:\n                            target_plant = plant\n                            break\n                        # 衬底植物情形\n                        elif plant.name in {c.LILYPAD, '花盆（未实现）'}:\n                            attackable_backup_plants.append(plant)\n                        # 一般植物情形\n                        # 同时也忽略了不可啃食对象\n                        elif (\n                            plant.name not in c.CAN_SKIP_ZOMBIE_COLLISION_CHECK\n                        ):\n                            attackable_common_plants.append(plant)\n                        # 在生效状态下忽略啃食碰撞但其他状况下不能忽略的情形\n                        elif (\n                            plant.name\n                            in c.SKIP_ZOMBIE_COLLISION_CHECK_WHEN_WORKING\n                        ):\n                            if not plant.start_boom:\n                                attackable_common_plants.append(plant)\n                else:\n                    if attackable_common_plants:\n                        # 默认为最右侧的一个植物\n                        target_plant = max(\n                            attackable_common_plants, key=lambda i: i.rect.x\n                        )\n                        map_x, map_y = self.map.getMapIndex(\n                            target_plant.rect.centerx,\n                            target_plant.rect.centery,\n                        )\n                        if self.map.isValid(map_x, map_y):\n                            if (\n                                c.PUMPKINHEAD\n                                in self.map.map[map_y][map_x][c.MAP_PLANT]\n                            ):\n                                for actual_target_plant in self.plant_groups[\n                                    i\n                                ]:\n                                    # 检测同一格的其他植物\n                                    if self.map.getMapIndex(\n                                        actual_target_plant.rect.centerx,\n                                        actual_target_plant.rect.bottom,\n                                    ) == (map_x, map_y):\n                                        if (\n                                            actual_target_plant.name\n                                            == c.PUMPKINHEAD\n                                        ):\n                                            target_plant = actual_target_plant\n                                            break\n                    elif attackable_backup_plants:\n                        target_plant = max(\n                            attackable_backup_plants, key=lambda i: i.rect.x\n                        )\n                        map_x, map_y = self.map.getMapIndex(\n                            target_plant.rect.centerx,\n                            target_plant.rect.centery,\n                        )\n                        if len(self.map.map[map_y][map_x][c.MAP_PLANT]) >= 2:\n                            for actual_target_plant in self.plant_groups[i]:\n                                # 检测同一格的其他植物\n                                if self.map.getMapIndex(\n                                    actual_target_plant.rect.centerx,\n                                    actual_target_plant.rect.bottom,\n                                ) == (map_x, map_y):\n                                    if (\n                                        actual_target_plant.name\n                                        == c.PUMPKINHEAD\n                                    ):\n                                        target_plant = actual_target_plant\n                                        break\n                                    elif actual_target_plant.name not in {\n                                        c.LILYPAD,\n                                        '花盆（未实现）',\n                                    }:\n                                        attackable_common_plants.append(\n                                            actual_target_plant\n                                        )\n                            else:\n                                if attackable_common_plants:\n                                    target_plant = attackable_common_plants[-1]\n                    else:\n                        target_plant = None\n\n                if target_plant:\n                    (\n                        zombie.prey_map_x,\n                        zombie.prey_map_y,\n                    ) = self.map.getMapIndex(\n                        target_plant.rect.centerx, target_plant.rect.centery\n                    )\n                    # 撑杆跳的特殊情况\n                    if zombie.name in {c.POLE_VAULTING_ZOMBIE} and (\n                        not zombie.jumped\n                    ):\n                        if target_plant.name == c.GIANTWALLNUT:\n                            zombie.health = 0\n                            c.SOUND_BOWLING_IMPACT.play()\n                        elif not zombie.jumping:\n                            zombie.jump_map_x = min(\n                                self.map_x_len - 1, zombie.prey_map_x\n                            )\n                            zombie.jump_map_y = min(\n                                self.map_y_len - 1, zombie.prey_map_y\n                            )\n                            jump_x = target_plant.rect.x - c.GRID_X_SIZE * 0.6\n                            if (\n                                c.TALLNUT\n                                in self.map.map[zombie.jump_map_y][\n                                    zombie.jump_map_x\n                                ][c.MAP_PLANT]\n                            ):\n                                zombie.setJump(False, jump_x)\n                            else:\n                                zombie.setJump(True, jump_x)\n                        else:\n                            if (\n                                c.TALLNUT\n                                in self.map.map[zombie.jump_map_y][\n                                    zombie.jump_map_x\n                                ][c.MAP_PLANT]\n                            ):\n                                zombie.setJump(False, zombie.jump_x)\n                            else:\n                                zombie.setJump(True, zombie.jump_x)\n                        continue\n\n                    if target_plant.name == c.WALLNUTBOWLING:\n                        if target_plant.canHit(i):\n                            # target_plant.vel_y不为0，有纵向速度，表明已经发生过碰撞，对铁门秒杀（这里实现为忽略二类防具攻击）\n                            if (\n                                target_plant.vel_y\n                                and zombie.name == c.SCREEN_DOOR_ZOMBIE\n                            ):\n                                zombie.setDamage(\n                                    c.WALLNUT_BOWLING_DAMAGE,\n                                    damage_type=c.ZOMBIE_COMMON_DAMAGE,\n                                )\n                            else:\n                                zombie.setDamage(\n                                    c.WALLNUT_BOWLING_DAMAGE,\n                                    damage_type=c.ZOMBIE_WALLNUT_BOWLING_DANMAGE,\n                                )\n                            target_plant.changeDirection(i)\n                            # 播放撞击音效\n                            c.SOUND_BOWLING_IMPACT.play()\n                    elif target_plant.name == c.REDWALLNUTBOWLING:\n                        if target_plant.state == c.IDLE:\n                            target_plant.setAttack()\n                    elif target_plant.name == c.GIANTWALLNUT:\n                        zombie.health = 0\n                        c.SOUND_BOWLING_IMPACT.play()\n                    elif zombie.target_y_change:\n                        # 大蒜作用正在生效的僵尸不进行传递\n                        continue\n                    elif target_plant.name == c.GARLIC:\n                        zombie.setAttack(target_plant)\n                        # 向吃过大蒜的僵尸传入level\n                        zombie.level = self\n                        zombie.to_change_group = True\n                        zombie.map_y = i\n                        if i == 0:\n                            _move = 1\n                        elif i == self.map_y_len - 1:\n                            _move = -1\n                        else:\n                            _move = random.randint(0, 1) * 2 - 1\n                            if (\n                                self.map.map[i][0][c.MAP_PLOT_TYPE]\n                                != self.map.map[i + _move][0][c.MAP_PLOT_TYPE]\n                            ):\n                                _move = -(_move)\n                        zombie.target_map_y = i + _move\n                        zombie.target_y_change = (\n                            _move * self.map.grid_height_size\n                        )\n                    else:\n                        zombie.setAttack(target_plant)\n\n            for hypno_zombie in self.hypno_zombie_groups[i]:\n                if hypno_zombie.health <= 0:\n                    continue\n                collided_func = pg.sprite.collide_mask\n                zombie_list = pg.sprite.spritecollide(\n                    hypno_zombie, self.zombie_groups[i], False, collided_func\n                )\n                for zombie in zombie_list:\n                    if zombie.state == c.DIE:\n                        continue\n                    # 正常僵尸攻击被魅惑的僵尸\n                    if zombie.state == c.WALK:\n                        zombie.setAttack(hypno_zombie, False)\n                    # 被魅惑的僵尸攻击正常僵尸\n                    if hypno_zombie.state == c.WALK:\n                        hypno_zombie.setAttack(zombie, False)\n\n        else:\n            self.new_plant_and_positon = None    # 生效后需要解除刷新设置\n\n    def checkCarCollisions(self):\n        for i in range(len(self.cars)):\n            if self.cars[i]:\n                for zombie in self.zombie_groups[i]:\n                    if (\n                        zombie\n                        and zombie.state != c.DIE\n                        and (not zombie.losthead)\n                        and (pg.sprite.collide_mask(zombie, self.cars[i]))\n                    ):\n                        self.cars[i].setWalk()\n                    if (\n                        pg.sprite.collide_mask(zombie, self.cars[i])\n                        or self.cars[i].rect.x\n                        <= zombie.rect.right\n                        <= self.cars[i].rect.right\n                    ):\n                        zombie.health = 0\n                if self.cars[i].dead:\n                    self.cars[i] = None\n\n    def boomZombies(self, x, map_y, y_range, x_range, effect=None):\n        for i in range(self.map_y_len):\n            if abs(i - map_y) > y_range:\n                continue\n            for zombie in self.zombie_groups[i]:\n                if (abs(zombie.rect.centerx - x) <= x_range) or (\n                    (zombie.rect.right - (x - x_range) > 20)\n                    or (zombie.rect.right - (x - x_range)) / zombie.rect.width\n                    > 0.2,\n                    ((x + x_range) - zombie.rect.left > 20)\n                    or ((x + x_range) - zombie.rect.left) / zombie.rect.width\n                    > 0.2,\n                )[\n                    zombie.rect.x > x\n                ]:  # 这代码不太好懂，后面是一个判断僵尸在左还是在右，前面是一个元组，[0]是在左边的情况，[1]是在右边的情况\n                    if effect == c.BULLET_EFFECT_UNICE:\n                        zombie.ice_slow_ratio = 1\n                    zombie.setDamage(1800, damage_type=c.ZOMBIE_ASH_DAMAGE)\n                    if zombie.health <= 0:\n                        zombie.setBoomDie()\n\n    def freezeZombies(self, plant):\n        # 播放冻结音效\n        c.SOUND_FREEZE.play()\n\n        for i in range(self.map_y_len):\n            for zombie in self.zombie_groups[i]:\n                zombie.setFreeze(plant.trap_frames[0])\n                zombie.setDamage(\n                    20, damage_type=c.ZOMBIE_RANGE_DAMAGE\n                )    # 寒冰菇还有全场20的伤害\n\n    def killPlant(self, target_plant, shovel=False):\n        x, y = target_plant.getPosition()\n        map_x, map_y = self.map.getMapIndex(x, y)\n\n        # 用铲子铲不用触发植物功能\n        if not shovel:\n            if (\n                target_plant.name == c.HYPNOSHROOM\n                and target_plant.state != c.SLEEP\n            ):\n                if target_plant.zombie_to_hypno:\n                    zombie = target_plant.zombie_to_hypno\n                    zombie.setHypno()\n                    self.zombie_groups[map_y].remove(zombie)\n                    self.hypno_zombie_groups[map_y].add(zombie)\n            # 对于墓碑：移除存储在墓碑集合中的坐标\n            # 注意这里是在描述墓碑而非墓碑吞噬者\n            elif target_plant.name == c.GRAVE:\n                self.grave_set.remove((map_x, map_y))\n            elif (\n                target_plant.name\n                in {\n                    c.DOOMSHROOM,\n                    c.ICESHROOM,\n                    c.POTATOMINE,\n                }\n            ) and (target_plant.boomed):\n                # 毁灭菇的情况：爆炸时为了防止蘑菇云被坑掩盖没有加入坑，这里毁灭菇死亡（即爆炸动画结束）后再加入\n                if target_plant.name == c.DOOMSHROOM:\n                    self.plant_groups[map_y].add(\n                        plant.Hole(\n                            target_plant.original_x,\n                            target_plant.original_y,\n                            self.map.map[map_y][map_x][c.MAP_PLOT_TYPE],\n                        )\n                    )\n            elif target_plant.name not in c.PLANT_DIE_SOUND_EXCEPTIONS:\n                # 触发植物死亡音效\n                c.SOUND_PLANT_DIE.play()\n        else:\n            # 用铲子移除植物时播放音效\n            c.SOUND_PLANT.play()\n\n        # 整理地图信息\n        if self.bar_type != c.CHOOSEBAR_BOWLING:\n            self.map.removeMapPlant(map_x, map_y, target_plant.name)\n        # 将睡眠植物移除后更新睡眠状态\n        if target_plant.state == c.SLEEP:\n            self.map.map[map_y][map_x][c.MAP_SLEEP] = False\n\n        # 避免僵尸在用铲子移除植物后还在原位啃食\n        target_plant.health = 0\n        target_plant.kill()\n\n    def checkPlant(self, target_plant, i):\n        zombie_len = len(self.zombie_groups[i])\n        # 不用检查攻击状况的情况\n        if not target_plant.attack_check:\n            pass\n        elif target_plant.name == c.THREEPEASHOOTER:\n            if target_plant.state == c.IDLE:\n                if zombie_len > 0:\n                    target_plant.setAttack()\n                elif (i - 1) >= 0 and len(self.zombie_groups[i - 1]) > 0:\n                    target_plant.setAttack()\n                elif (i + 1) < self.map_y_len and len(\n                    self.zombie_groups[i + 1]\n                ) > 0:\n                    target_plant.setAttack()\n            elif target_plant.state == c.ATTACK:\n                if zombie_len > 0:\n                    pass\n                elif (i - 1) >= 0 and len(self.zombie_groups[i - 1]) > 0:\n                    pass\n                elif (i + 1) < self.map_y_len and len(\n                    self.zombie_groups[i + 1]\n                ) > 0:\n                    pass\n                else:\n                    target_plant.setIdle()\n        elif target_plant.name == c.CHOMPER:\n            for zombie in self.zombie_groups[i]:\n                if target_plant.canAttack(zombie):\n                    target_plant.setAttack(zombie, self.zombie_groups[i])\n                    break\n        elif target_plant.name == c.POTATOMINE:\n            for zombie in self.zombie_groups[i]:\n                if target_plant.canAttack(zombie):\n                    target_plant.setAttack()\n                    break\n            if target_plant.start_boom and (not target_plant.boomed):\n                for zombie in self.zombie_groups[i]:\n                    # 双判断：发生碰撞或在攻击范围内\n                    if (pg.sprite.collide_mask(zombie, target_plant)) or (\n                        abs(zombie.rect.centerx - target_plant.rect.centerx)\n                        <= target_plant.explode_x_range\n                    ):\n                        zombie.setDamage(\n                            1800, damage_type=c.ZOMBIE_RANGE_DAMAGE\n                        )\n                target_plant.boomed = True\n        elif target_plant.name == c.SQUASH:\n            for zombie in self.zombie_groups[i]:\n                if target_plant.canAttack(zombie):\n                    target_plant.setAttack(zombie, self.zombie_groups[i])\n                    break\n        elif target_plant.name == c.SPIKEWEED:\n            can_attack = False\n            for zombie in self.zombie_groups[i]:\n                if target_plant.canAttack(zombie):\n                    can_attack = True\n                    break\n            if target_plant.state == c.IDLE and can_attack:\n                target_plant.setAttack(self.zombie_groups[i])\n            elif target_plant.state == c.ATTACK and not can_attack:\n                target_plant.setIdle()\n        elif target_plant.name == c.SCAREDYSHROOM:\n            need_cry = False\n            can_attack = False\n            for zombie in self.zombie_groups[i]:\n                if target_plant.needCry(zombie):\n                    need_cry = True\n                    break\n                elif target_plant.canAttack(zombie):\n                    can_attack = True\n            if need_cry:\n                if target_plant.state != c.CRY:\n                    target_plant.setCry()\n            elif can_attack:\n                if target_plant.state != c.ATTACK:\n                    target_plant.setAttack()\n            elif target_plant.state != c.IDLE:\n                target_plant.setIdle()\n        elif target_plant.name == c.STARFRUIT:\n            can_attack = False\n            for zombie_group in self.zombie_groups:   # 遍历循环所有僵尸\n                for zombie in zombie_group:\n                    if target_plant.canAttack(zombie):\n                        can_attack = True\n                        break\n            if target_plant.state == c.IDLE and can_attack:\n                target_plant.setAttack()\n            elif target_plant.state == c.ATTACK and not can_attack:\n                target_plant.setIdle()\n        elif target_plant.name == c.TANGLEKLEP:\n            for zombie in self.zombie_groups[i]:\n                if target_plant.canAttack(zombie):\n                    target_plant.setAttack(zombie, self.zombie_groups[i])\n                    break\n        # 灰烬植物与寒冰菇\n        elif target_plant.name in c.ASH_PLANTS_AND_ICESHROOM:\n            if target_plant.start_boom and (not target_plant.boomed):\n                # 这样分成两层是因为场上灰烬植物肯定少，一个一个判断代价高，先笼统判断灰烬即可\n                if target_plant.name in {c.REDWALLNUTBOWLING, c.CHERRYBOMB}:\n                    self.boomZombies(\n                        target_plant.rect.centerx,\n                        i,\n                        target_plant.explode_y_range,\n                        target_plant.explode_x_range,\n                    )\n                elif target_plant.name == c.DOOMSHROOM:\n                    x, y = target_plant.original_x, target_plant.original_y\n                    map_x, map_y = self.map.getMapIndex(x, y)\n                    self.boomZombies(\n                        target_plant.rect.centerx,\n                        i,\n                        target_plant.explode_y_range,\n                        target_plant.explode_x_range,\n                    )\n                    for item in self.plant_groups[map_y]:\n                        checkMapX, _ = self.map.getMapIndex(\n                            item.rect.centerx, item.rect.bottom\n                        )\n                        if map_x == checkMapX:\n                            item.health = 0\n                    # 为了防止坑显示在蘑菇云前面，这里先不生成坑，仅填位置\n                    self.map.map[map_y][map_x][c.MAP_PLANT].add(c.HOLE)\n                elif target_plant.name == c.JALAPENO:\n                    self.boomZombies(\n                        target_plant.rect.centerx,\n                        i,\n                        target_plant.explode_y_range,\n                        target_plant.explode_x_range,\n                        effect=c.BULLET_EFFECT_UNICE,\n                    )\n                    # 消除冰道\n                    for item in self.plant_groups[i]:\n                        if item.name == c.ICEFROZENPLOT:\n                            item.health = 0\n                elif target_plant.name == c.ICESHROOM:\n                    self.freezeZombies(target_plant)\n                target_plant.boomed = True\n        else:\n            can_attack = False\n            if zombie_len > 0:\n                for zombie in self.zombie_groups[i]:\n                    if target_plant.canAttack(zombie):\n                        can_attack = True\n                        break\n            if target_plant.state == c.IDLE and can_attack:\n                target_plant.setAttack()\n            elif target_plant.state == c.ATTACK and (not can_attack):\n                target_plant.setIdle()\n\n    def checkPlants(self):\n        for i in range(self.map_y_len):\n            for plant in self.plant_groups[i]:\n                if plant.state != c.SLEEP:\n                    self.checkPlant(plant, i)\n                if plant.health <= 0:\n                    self.killPlant(plant)\n\n    def checkVictory(self):\n        if self.map_data[c.SPAWN_ZOMBIES] == c.SPAWN_ZOMBIES_LIST:\n            if len(self.zombie_list) > 0:\n                return False\n            for i in range(self.map_y_len):\n                if len(self.zombie_groups[i]) > 0:\n                    return False\n        else:\n            if self.wave_num < self.map_data[c.NUM_FLAGS] * 10:\n                return False\n            for i in range(self.map_y_len):\n                if len(self.zombie_groups[i]) > 0:\n                    return False\n        return True\n\n    def checkLose(self):\n        for i in range(self.map_y_len):\n            for zombie in self.zombie_groups[i]:\n                if (\n                    zombie.rect.right < -20\n                    and (not zombie.losthead)\n                    and (zombie.state != c.DIE)\n                ):\n                    return True\n        return False\n\n    def checkGameState(self):\n        if self.checkVictory():\n            if self.game_info[c.GAME_MODE] == c.MODE_ADVENTURE:\n                self.game_info[c.LEVEL_NUM] += 1\n                if self.game_info[c.LEVEL_NUM] >= map.TOTAL_LEVEL:\n                    self.game_info[c.LEVEL_COMPLETIONS] += 1\n                    self.game_info[c.LEVEL_NUM] = 1\n                    self.next = c.AWARD_SCREEN\n                    # 播放大胜利音效\n                    c.SOUND_FINAL_FANFARE.play()\n                else:\n                    self.next = c.GAME_VICTORY\n                    # 播放胜利音效\n                    c.SOUND_WIN.play()\n            elif self.game_info[c.GAME_MODE] == c.MODE_LITTLEGAME:\n                self.game_info[c.LITTLEGAME_NUM] += 1\n                if self.game_info[c.LITTLEGAME_NUM] >= map.TOTAL_LITTLE_GAME:\n                    self.game_info[c.LITTLEGAME_COMPLETIONS] += 1\n                    self.game_info[c.LITTLEGAME_NUM] = 1\n                    self.next = c.AWARD_SCREEN\n                    # 播放大胜利音效\n                    c.SOUND_FINAL_FANFARE.play()\n                else:\n                    self.next = c.GAME_VICTORY\n                    # 播放胜利音效\n                    c.SOUND_WIN.play()\n            self.done = True\n            self.saveUserData()\n        elif self.checkLose():\n            # 播放失败音效\n            c.SOUND_LOSE.play()\n            c.SOUND_SCREAM.play()\n            self.next = c.GAME_LOSE\n            self.done = True\n\n    def drawMouseShow(self, surface):\n        if self.hint_plant:\n            surface.blit(self.hint_image, self.hint_rect)\n        x, y = pg.mouse.get_pos()\n        self.mouse_rect.centerx = x\n        self.mouse_rect.centery = y\n        surface.blit(self.mouse_image, self.mouse_rect)\n\n    def drawMouseShowPlus(self, surface):   # 拖动铲子时的显示\n        x, y = pg.mouse.get_pos()\n        self.shovel_rect.centerx = x\n        self.shovel_rect.centery = y\n        # 铲子接近植物时会高亮提示\n        map_x, map_y = self.map.getMapIndex(x, y)\n        surface.blit(self.shovel, self.shovel_rect)\n        if not self.map.isValid(map_x, map_y):\n            return\n        for i in self.plant_groups[map_y]:\n            if (\n                x >= i.rect.x\n                and x <= i.rect.right\n                and y >= i.rect.y\n                and y <= i.rect.bottom\n            ):\n                if i.name in c.NON_PLANT_OBJECTS:\n                    continue\n                if i.name in c.SKIP_ZOMBIE_COLLISION_CHECK_WHEN_WORKING:\n                    if i.start_boom:\n                        continue\n                # 优先选中睡莲、花盆上的植物\n                if len(self.map.map[map_y][map_x][c.MAP_PLANT]) >= 2:\n                    if c.LILYPAD in self.map.map[map_y][map_x][c.MAP_PLANT]:\n                        if i.name == c.LILYPAD:\n                            continue\n                    elif '花盆（未实现）' in self.map.map[map_y][map_x][c.MAP_PLANT]:\n                        if i.name == '花盆（未实现）':\n                            continue\n                i.highlight_time = self.current_time\n                return\n\n    def drawZombieFreezeTrap(self, i, surface):\n        for zombie in self.zombie_groups[i]:\n            zombie.drawFreezeTrap(surface)\n\n    def showLevelProgress(self, surface):\n        # 画进度条框\n        surface.blit(\n            self.level_progress_bar_image, self.level_progress_bar_image_rect\n        )\n\n        # 按照当前波数生成僵尸头位置\n        self.level_progress_zombie_head_image_rect.x = (\n            self.level_progress_bar_image_rect.x\n            - int((150 * self.wave_num) / (self.map_data[c.NUM_FLAGS] * 10))\n            + 145\n        )      # 常数为拟合值\n        self.level_progress_zombie_head_image_rect.y = (\n            self.level_progress_bar_image_rect.y - 3\n        )      # 常数为拟合值\n\n        # 填充的进度条信息\n        # 常数为拟合值\n        filled_bar_rect = (\n            self.level_progress_zombie_head_image_rect.x + 3,\n            self.level_progress_bar_image_rect.y + 6,\n            int((150 * self.wave_num) / (self.map_data[c.NUM_FLAGS] * 10)) + 5,\n            9,\n        )\n        # 画填充的进度条\n        pg.draw.rect(surface, c.YELLOWGREEN, filled_bar_rect)\n\n        # 画旗帜\n        for i in range(self.num_flags):\n            self.level_progress_flag_rect.x = (\n                self.level_progress_bar_image_rect.x\n                + int((150 * i) / self.num_flags)\n                + 5\n            )   # 常数是猜的\n            # 当指示进度的僵尸头在旗帜左侧时升高旗帜\n            if (\n                self.level_progress_flag_rect.x - 7\n                >= self.level_progress_zombie_head_image_rect.x\n            ):\n                self.level_progress_flag_rect.y = (\n                    self.level_progress_bar_image_rect.y - 15\n                )  # 常数是猜的\n            else:\n                self.level_progress_flag_rect.y = (\n                    self.level_progress_bar_image_rect.y - 3\n                )  # 常数是猜的\n            surface.blit(\n                self.level_progress_flag, self.level_progress_flag_rect\n            )\n\n        # 画僵尸头\n        surface.blit(\n            self.level_progress_zombie_head_image,\n            self.level_progress_zombie_head_image_rect,\n        )\n\n    def showAllContentOfMenu(self, surface):\n        # 绘制不可变内容\n        surface.blit(self.big_menu, self.big_menu_rect)\n        surface.blit(self.return_button, self.return_button_rect)\n        surface.blit(self.restart_button, self.restart_button_rect)\n        surface.blit(self.mainMenu_button, self.mainMenu_button_rect)\n        surface.blit(\n            self.sound_volume_minus_button, self.sound_volume_minus_button_rect\n        )\n        surface.blit(\n            self.sound_volume_plus_button, self.sound_volume_plus_button_rect\n        )\n\n        # 显示当前音量\n        # 由于音量可变，因此这一内容不能在一开始就结束加载，而应当不断刷新不断显示\n        font = pg.font.Font(c.FONT_PATH, 30)\n        volume_tips = font.render(\n            f'音量：{round(self.game_info[c.SOUND_VOLUME]*100):3}%',\n            True,\n            c.LIGHTGRAY,\n        )\n        volume_tips_rect = volume_tips.get_rect()\n        volume_tips_rect.x = 275\n        volume_tips_rect.y = 247\n        surface.blit(volume_tips, volume_tips_rect)\n\n    def draw(self, surface):\n        self.level.blit(self.background, self.viewport, self.viewport)\n        surface.blit(self.level, (0, 0), self.viewport)\n        if self.state == c.CHOOSE:\n            self.panel.draw(surface)\n            # 画小菜单\n            surface.blit(self.little_menu, self.little_menu_rect)\n            if self.show_game_menu:\n                self.showAllContentOfMenu(surface)\n        # 以后可能需要插入一个预备的状态（预览显示僵尸、返回战场）\n        elif self.state == c.PLAY:\n            if self.has_shovel:\n                # 画铲子\n                surface.blit(self.shovel_box, self.shovel_box_rect)\n                surface.blit(self.shovel, self.shovel_rect)\n            # 画小菜单\n            surface.blit(self.little_menu, self.little_menu_rect)\n\n            self.menubar.draw(surface)\n            for i in range(self.map_y_len):\n                self.plant_groups[i].draw(surface)\n                self.zombie_groups[i].draw(surface)\n                self.hypno_zombie_groups[i].draw(surface)\n                self.bullet_groups[i].draw(surface)\n                self.drawZombieFreezeTrap(i, surface)\n                if self.cars[i]:\n                    self.cars[i].draw(surface)\n            self.head_group.draw(surface)\n            self.sun_group.draw(surface)\n\n            if self.drag_plant:\n                self.drawMouseShow(surface)\n\n            if self.has_shovel and self.drag_shovel:\n                self.drawMouseShowPlus(surface)\n\n            if self.show_game_menu:\n                self.showAllContentOfMenu(surface)\n\n            if self.map_data[c.SPAWN_ZOMBIES] == c.SPAWN_ZOMBIES_AUTO:\n                self.showLevelProgress(surface)\n                if (\n                    self.current_time - self.show_hugewave_approching_time\n                    <= 2000\n                ):\n                    surface.blit(\n                        self.huge_wave_approching_image,\n                        self.huge_wave_approching_image_rect,\n                    )\n"
  },
  {
    "path": "source/state/mainmenu.py",
    "content": "import os\n\nimport pygame as pg\n\nfrom .. import constants as c\nfrom .. import tool\n\n\nclass Menu(tool.State):\n    def __init__(self):\n        tool.State.__init__(self)\n\n    def startup(self, current_time: int, persist):\n        self.next = c.LEVEL\n        self.persist = persist\n        self.game_info = persist\n        self.setupBackground()\n        self.setupOptions()\n        self.setupOptionMenu()\n        self.setupSunflowerTrophy()\n        pg.mixer.music.stop()\n        pg.mixer.music.load(os.path.join(c.PATH_MUSIC_DIR, 'intro.opus'))\n        pg.mixer.music.play(-1, 0)\n        pg.display.set_caption(c.ORIGINAL_CAPTION)\n        pg.mixer.music.set_volume(self.game_info[c.SOUND_VOLUME])\n        for i in c.SOUNDS:\n            i.set_volume(self.game_info[c.SOUND_VOLUME])\n\n    def setupBackground(self):\n        frame_rect = (80, 0, 800, 600)\n        # 1、形参中加单星号，即f(*x)则表示x为元组，所有对x的操作都应将x视为元组类型进行。\n        # 2、双星号同上，区别是x视为字典。\n        # 3、在变量前加单星号表示将元组（列表、集合）拆分为单个元素。\n        # 4、双星号同上，区别是目标为字典，字典前加单星号的话可以得到“键”。\n        self.bg_image = tool.get_image(\n            tool.GFX[c.MAIN_MENU_IMAGE], *frame_rect\n        )\n        self.bg_rect = self.bg_image.get_rect()\n        self.bg_rect.x = 0\n        self.bg_rect.y = 0\n\n    def setupOptions(self):\n        # 冒险模式\n        frame_rect = (0, 0, 330, 144)\n        # 写成列表生成器方便IDE识别与自动补全\n        self.adventure_frames = [\n            tool.get_image_alpha(\n                tool.GFX[f'{c.OPTION_ADVENTURE}_{i}'], *frame_rect\n            )\n            for i in range(2)\n        ]\n        self.adventure_image = self.adventure_frames[0]\n        self.adventure_rect = self.adventure_image.get_rect()\n        self.adventure_rect.x = 400\n        self.adventure_rect.y = 60\n        self.adventure_highlight_time = 0\n\n        # 小游戏\n        littleGame_frame_rect = (0, 7, 317, 135)\n        self.littleGame_frames = [\n            tool.get_image_alpha(\n                tool.GFX[f'{c.LITTLEGAME_BUTTON}_{i}'], *littleGame_frame_rect\n            )\n            for i in range(2)\n        ]\n        self.littleGame_image = self.littleGame_frames[0]\n        self.littleGame_rect = self.littleGame_image.get_rect()\n        self.littleGame_rect.x = 397\n        self.littleGame_rect.y = 175\n        self.littleGame_highlight_time = 0\n\n        # 退出按钮\n        exit_frame_rect = (0, 0, 47, 27)\n        self.exit_frames = [\n            tool.get_image_alpha(\n                tool.GFX[f'{c.EXIT}_{i}'], *exit_frame_rect, scale=1.1\n            )\n            for i in range(2)\n        ]\n        self.exit_image = self.exit_frames[0]\n        self.exit_rect = self.exit_image.get_rect()\n        self.exit_rect.x = 730\n        self.exit_rect.y = 507\n        self.exit_highlight_time = 0\n\n        # 选项按钮\n        option_button_frame_rect = (0, 0, 81, 31)\n        self.option_button_frames = [\n            tool.get_image_alpha(\n                tool.GFX[f'{c.OPTION_BUTTON}_{i}'], *option_button_frame_rect\n            )\n            for i in range(2)\n        ]\n        self.option_button_image = self.option_button_frames[0]\n        self.option_button_rect = self.option_button_image.get_rect()\n        self.option_button_rect.x = 560\n        self.option_button_rect.y = 490\n        self.option_button_highlight_time = 0\n\n        # 帮助菜单\n        help_frame_rect = (0, 0, 48, 22)\n        self.help_frames = [\n            tool.get_image_alpha(tool.GFX[f'{c.HELP}_{i}'], *help_frame_rect)\n            for i in range(2)\n        ]\n        self.help_image = self.help_frames[0]\n        self.help_rect = self.help_image.get_rect()\n        self.help_rect.x = 653\n        self.help_rect.y = 520\n        self.help_hilight_time = 0\n\n        # 计时器与点击信号记录器\n        self.adventure_start = 0\n        self.adventure_timer = 0\n        self.adventure_clicked = False\n        self.option_button_clicked = False\n\n    def checkHilight(self, x: int, y: int):\n        # 高亮冒险模式按钮\n        if self.inArea(self.adventure_rect, x, y):\n            self.adventure_highlight_time = self.current_time\n        # 高亮小游戏按钮\n        elif self.inArea(self.littleGame_rect, x, y):\n            self.littleGame_highlight_time = self.current_time\n        # 高亮退出按钮\n        elif self.inArea(self.exit_rect, x, y):\n            self.exit_highlight_time = self.current_time\n        # 高亮选项按钮\n        elif self.inArea(self.option_button_rect, x, y):\n            self.option_button_highlight_time = self.current_time\n        # 高亮帮助按钮\n        elif self.inArea(self.help_rect, x, y):\n            self.help_hilight_time = self.current_time\n\n        # 处理按钮高亮情况\n        self.adventure_image = self.chooseHilightImage(\n            self.adventure_highlight_time, self.adventure_frames\n        )\n        self.exit_image = self.chooseHilightImage(\n            self.exit_highlight_time, self.exit_frames\n        )\n        self.option_button_image = self.chooseHilightImage(\n            self.option_button_highlight_time, self.option_button_frames\n        )\n        self.littleGame_image = self.chooseHilightImage(\n            self.littleGame_highlight_time, self.littleGame_frames\n        )\n        self.help_image = self.chooseHilightImage(\n            self.help_hilight_time, self.help_frames\n        )\n\n    def chooseHilightImage(self, hilightTime: int, frames):\n        if (self.current_time - hilightTime) < 80:\n            index = 1\n        else:\n            index = 0\n        return frames[index]\n\n    def respondAdventureClick(self):\n        self.adventure_clicked = True\n        self.adventure_timer = self.adventure_start = self.current_time\n        self.persist[c.GAME_MODE] = c.MODE_ADVENTURE\n        # 播放进入音效\n        pg.mixer.music.stop()\n        c.SOUND_EVILLAUGH.play()\n        c.SOUND_LOSE.play()\n\n    # 按到小游戏\n    def respondLittleGameClick(self):\n        self.done = True\n        self.persist[c.GAME_MODE] = c.MODE_LITTLEGAME\n        # 播放点击音效\n        c.SOUND_BUTTON_CLICK.play()\n\n    # 点击到退出按钮，修改转态的done属性\n    def respondExitClick(self):\n        self.done = True\n        self.next = c.EXIT\n\n    # 帮助按钮点击\n    def respondHelpClick(self):\n        self.done = True\n        self.next = c.HELP_SCREEN\n\n    def setupOptionMenu(self):\n        # 选项菜单框\n        frame_rect = (0, 0, 500, 500)\n        self.big_menu = tool.get_image_alpha(\n            tool.GFX[c.BIG_MENU], *frame_rect, c.BLACK, 1.1\n        )\n        self.big_menu_rect = self.big_menu.get_rect()\n        self.big_menu_rect.x = 150\n        self.big_menu_rect.y = 0\n\n        # 返回按钮，用字体渲染实现，增强灵活性\n        # 建立一个按钮大小的surface对象\n        self.return_button = pg.Surface((376, 96))\n        self.return_button.set_colorkey(c.BLACK)    # 避免多余区域显示成黑色\n        self.return_button_rect = self.return_button.get_rect()\n        self.return_button_rect.x = 220\n        self.return_button_rect.y = 440\n        font = pg.font.Font(c.FONT_PATH, 40)\n        font.bold = True\n        text = font.render('返回游戏', True, c.YELLOWGREEN)\n        text_rect = text.get_rect()\n        text_rect.x = 105\n        text_rect.y = 18\n        self.return_button.blit(text, text_rect)\n\n        # 音量+、音量-\n        frame_rect = (0, 0, 39, 41)\n        font = pg.font.Font(c.FONT_PATH, 35)\n        font.bold = True\n        # 音量+\n        self.sound_volume_plus_button = tool.get_image_alpha(\n            tool.GFX[c.SOUND_VOLUME_BUTTON], *frame_rect, c.BLACK\n        )\n        sign = font.render('+', True, c.YELLOWGREEN)\n        sign_rect = sign.get_rect()\n        sign_rect.x = 8\n        sign_rect.y = -4\n        self.sound_volume_plus_button.blit(sign, sign_rect)\n        self.sound_volume_plus_button_rect = (\n            self.sound_volume_plus_button.get_rect()\n        )\n        self.sound_volume_plus_button_rect.x = 500\n        # 音量-\n        self.sound_volume_minus_button = tool.get_image_alpha(\n            tool.GFX[c.SOUND_VOLUME_BUTTON], *frame_rect, c.BLACK\n        )\n        sign = font.render('-', True, c.YELLOWGREEN)\n        sign_rect = sign.get_rect()\n        sign_rect.x = 12\n        sign_rect.y = -6\n        self.sound_volume_minus_button.blit(sign, sign_rect)\n        self.sound_volume_minus_button_rect = (\n            self.sound_volume_minus_button.get_rect()\n        )\n        self.sound_volume_minus_button_rect.x = 450\n        # 音量+、-应当处于同一高度\n        self.sound_volume_minus_button_rect.y = (\n            self.sound_volume_plus_button_rect.y\n        ) = 250\n\n    def setupSunflowerTrophy(self):\n        # 设置金银向日葵图片信息\n        if (\n            self.game_info[c.LEVEL_COMPLETIONS]\n            or self.game_info[c.LITTLEGAME_COMPLETIONS]\n        ):\n            if (\n                self.game_info[c.LEVEL_COMPLETIONS]\n                and self.game_info[c.LITTLEGAME_COMPLETIONS]\n            ):\n                frame_rect = (157, 0, 157, 269)\n            else:\n                frame_rect = (0, 0, 157, 269)\n            self.sunflower_trophy = tool.get_image_alpha(\n                tool.GFX[c.TROPHY_SUNFLOWER], *frame_rect, c.BLACK\n            )\n            self.sunflower_trophy_rect = self.sunflower_trophy.get_rect()\n            self.sunflower_trophy_rect.x = 0\n            self.sunflower_trophy_rect.y = 280\n            self.sunflower_trophy_show_info_time = 0\n\n    def checkSunflowerTrophyInfo(self, surface: pg.Surface, x: int, y: int):\n        if self.inArea(self.sunflower_trophy_rect, x, y):\n            self.sunflower_trophy_show_info_time = self.current_time\n        if (self.current_time - self.sunflower_trophy_show_info_time) < 80:\n            font = pg.font.Font(c.FONT_PATH, 14)\n            if (\n                self.game_info[c.LEVEL_COMPLETIONS]\n                and self.game_info[c.LITTLEGAME_COMPLETIONS]\n            ):\n                infoText = f'目前您一共完成了：冒险模式{self.game_info[c.LEVEL_COMPLETIONS]}轮，玩玩小游戏{self.game_info[c.LITTLEGAME_COMPLETIONS]}轮'\n            elif self.game_info[c.LEVEL_COMPLETIONS]:\n                infoText = f'目前您一共完成了：冒险模式{self.game_info[c.LEVEL_COMPLETIONS]}轮；完成其他所有游戏模式以获得金向日葵奖杯！'\n            else:\n                infoText = f'目前您一共完成了：玩玩小游戏{self.game_info[c.LITTLEGAME_COMPLETIONS]}轮；完成其他所有游戏模式以获得金向日葵奖杯！'\n            infoImg = font.render(infoText, True, c.BLACK, c.LIGHTYELLOW)\n            infoImg_rect = infoImg.get_rect()\n            infoImg_rect.x = self.sunflower_trophy_rect.x\n            infoImg_rect.y = self.sunflower_trophy_rect.bottom - 14\n            surface.blit(infoImg, infoImg_rect)\n\n    def respondOptionButtonClick(self):\n        self.option_button_clicked = True\n        # 播放点击音效\n        c.SOUND_BUTTON_CLICK.play()\n\n    def showCurrentVolumeImage(self, surface: pg.Surface):\n        # 由于音量可变，因此这一内容不能在一开始就结束加载，而应当不断刷新不断显示\n        font = pg.font.Font(c.FONT_PATH, 30)\n        volume_tips = font.render(\n            f'音量：{round(self.game_info[c.SOUND_VOLUME]*100):3}%',\n            True,\n            c.LIGHTGRAY,\n        )\n        volume_tips_rect = volume_tips.get_rect()\n        volume_tips_rect.x = 275\n        volume_tips_rect.y = 247\n        surface.blit(volume_tips, volume_tips_rect)\n\n    def update(\n        self,\n        surface: pg.Surface,\n        current_time: int,\n        mouse_pos: list,\n        mouse_click,\n    ):\n        self.current_time = self.game_info[c.CURRENT_TIME] = current_time\n\n        surface.blit(self.bg_image, self.bg_rect)\n        surface.blit(self.adventure_image, self.adventure_rect)\n        surface.blit(self.littleGame_image, self.littleGame_rect)\n        surface.blit(self.exit_image, self.exit_rect)\n        surface.blit(self.option_button_image, self.option_button_rect)\n        surface.blit(self.help_image, self.help_rect)\n        if (\n            self.game_info[c.LEVEL_COMPLETIONS]\n            or self.game_info[c.LITTLEGAME_COMPLETIONS]\n        ):\n            surface.blit(self.sunflower_trophy, self.sunflower_trophy_rect)\n\n        # 点到冒险模式后播放动画\n        if self.adventure_clicked:\n            # 乱写一个不用信号标记的循环播放 QwQ\n            if ((self.current_time - self.adventure_timer) // 150) % 2:\n                self.adventure_image = self.adventure_frames[1]\n            else:\n                self.adventure_image = self.adventure_frames[0]\n            if (self.current_time - self.adventure_start) > 3200:\n                self.done = True\n        # 点到选项按钮后显示菜单\n        elif self.option_button_clicked:\n            surface.blit(self.big_menu, self.big_menu_rect)\n            surface.blit(self.return_button, self.return_button_rect)\n            surface.blit(\n                self.sound_volume_plus_button,\n                self.sound_volume_plus_button_rect,\n            )\n            surface.blit(\n                self.sound_volume_minus_button,\n                self.sound_volume_minus_button_rect,\n            )\n            self.showCurrentVolumeImage(surface)\n            if mouse_pos:\n                # 返回\n                if self.inArea(self.return_button_rect, *mouse_pos):\n                    self.option_button_clicked = False\n                    c.SOUND_BUTTON_CLICK.play()\n                # 音量+\n                elif self.inArea(\n                    self.sound_volume_plus_button_rect, *mouse_pos\n                ):\n                    self.game_info[c.SOUND_VOLUME] = round(\n                        min(self.game_info[c.SOUND_VOLUME] + 0.05, 1), 2\n                    )\n                    # 一般不会有人想把音乐和音效分开设置，故pg.mixer.Sound.set_volume()和pg.mixer.music.set_volume()需要一起用\n                    pg.mixer.music.set_volume(self.game_info[c.SOUND_VOLUME])\n                    for i in c.SOUNDS:\n                        i.set_volume(self.game_info[c.SOUND_VOLUME])\n                    c.SOUND_BUTTON_CLICK.play()\n                    self.saveUserData()\n                # 音量-\n                elif self.inArea(\n                    self.sound_volume_minus_button_rect, *mouse_pos\n                ):\n                    self.game_info[c.SOUND_VOLUME] = round(\n                        max(self.game_info[c.SOUND_VOLUME] - 0.05, 0), 2\n                    )\n                    # 一般不会有人想把音乐和音效分开设置，故pg.mixer.Sound.set_volume()和pg.mixer.music.set_volume()需要一起用\n                    pg.mixer.music.set_volume(self.game_info[c.SOUND_VOLUME])\n                    for i in c.SOUNDS:\n                        i.set_volume(self.game_info[c.SOUND_VOLUME])\n                    c.SOUND_BUTTON_CLICK.play()\n                    self.saveUserData()\n        # 没有点到前两者时常规行检测所有按钮的点击和高亮\n        else:\n            # 先检查选项高亮预览\n            x, y = pg.mouse.get_pos()\n            self.checkHilight(x, y)\n            if (\n                self.game_info[c.LEVEL_COMPLETIONS]\n                or self.game_info[c.LITTLEGAME_COMPLETIONS]\n            ):\n                self.checkSunflowerTrophyInfo(surface, x, y)\n            if mouse_pos:\n                if self.inArea(self.adventure_rect, *mouse_pos):\n                    self.respondAdventureClick()\n                elif self.inArea(self.littleGame_rect, *mouse_pos):\n                    self.respondLittleGameClick()\n                elif self.inArea(self.option_button_rect, *mouse_pos):\n                    self.respondOptionButtonClick()\n                elif self.inArea(self.exit_rect, *mouse_pos):\n                    self.respondExitClick()\n                elif self.inArea(self.help_rect, *mouse_pos):\n                    self.respondHelpClick()\n"
  },
  {
    "path": "source/state/screen.py",
    "content": "import os\nfrom abc import abstractmethod\n\nimport pygame as pg\n\nfrom .. import constants as c\nfrom .. import tool\n\n\nclass Screen(tool.State):\n    def __init__(self):\n        tool.State.__init__(self)\n\n    @abstractmethod\n    def startup(self, current_time, persist):\n        pass\n\n    def setupImage(self, name, frame_rect=(0, 0, 800, 600), color_key=c.BLACK):\n        # 背景图本身\n        self.image = tool.get_image(\n            tool.GFX[name], *frame_rect, colorkey=color_key\n        )\n        self.rect = self.image.get_rect()\n        self.rect.x = 0\n        self.rect.y = 0\n\n        # 按钮\n        frame_rect = (0, 0, 111, 26)\n        ## 主菜单按钮\n        self.main_menu_button_image = tool.get_image_alpha(\n            tool.GFX[c.UNIVERSAL_BUTTON], *frame_rect\n        )\n        self.main_menu_button_image_rect = (\n            self.main_menu_button_image.get_rect()\n        )\n        self.main_menu_button_image_rect.x = 620\n        ### 主菜单按钮上的文字\n        font = pg.font.Font(c.FONT_PATH, 18)\n        main_menu_text = font.render('主菜单', True, c.NAVYBLUE)\n        main_menu_text_rect = main_menu_text.get_rect()\n        main_menu_text_rect.x = 29\n        ## 继续按钮\n        self.next_button_image = tool.get_image_alpha(\n            tool.GFX[c.UNIVERSAL_BUTTON], *frame_rect\n        )\n        self.next_button_image_rect = self.next_button_image.get_rect()\n        self.next_button_image_rect.x = 70\n        ### 继续按钮上的文字\n        if name == c.GAME_VICTORY_IMAGE:\n            next_text = font.render('下一关', True, c.NAVYBLUE)\n            next_text_rect = next_text.get_rect()\n            next_text_rect.x = 29\n            self.next_button_image_rect.y = (\n                self.main_menu_button_image_rect.y\n            ) = 555\n        else:\n            next_text = font.render('重新开始', True, c.NAVYBLUE)\n            next_text_rect = next_text.get_rect()\n            next_text_rect.x = 21\n            self.next_button_image_rect.y = (\n                self.main_menu_button_image_rect.y\n            ) = 530\n        self.next_button_image.blit(next_text, next_text_rect)\n        self.main_menu_button_image.blit(main_menu_text, main_menu_text_rect)\n        self.image.blit(self.next_button_image, self.next_button_image_rect)\n        self.image.blit(\n            self.main_menu_button_image, self.main_menu_button_image_rect\n        )\n\n    def update(self, surface, current_time, mouse_pos, mouse_click):\n        surface.fill(c.WHITE)\n        surface.blit(self.image, self.rect)\n        if mouse_pos:\n            # 点到继续\n            if self.inArea(self.next_button_image_rect, *mouse_pos):\n                self.next = c.LEVEL\n                self.done = True\n            # 点到主菜单\n            elif self.inArea(self.main_menu_button_image_rect, *mouse_pos):\n                self.next = c.MAIN_MENU\n                self.done = True\n\n\nclass GameVictoryScreen(Screen):\n    def __init__(self):\n        Screen.__init__(self)\n        self.image_name = c.GAME_VICTORY_IMAGE\n\n    def startup(self, current_time, persist):\n        self.start_time = current_time\n        self.persist = persist\n        self.game_info = persist\n        self.setupImage(self.image_name)\n        pg.display.set_caption('pypvz: 战斗胜利！')\n        pg.mixer.music.stop()\n        pg.mixer.music.load(os.path.join(c.PATH_MUSIC_DIR, 'zenGarden.opus'))\n        pg.mixer.music.play(-1, 0)\n        pg.mixer.music.set_volume(self.game_info[c.SOUND_VOLUME])\n\n\nclass GameLoseScreen(Screen):\n    def __init__(self):\n        Screen.__init__(self)\n        self.image_name = c.GAME_LOSE_IMAGE\n\n    def startup(self, current_time, persist):\n        self.start_time = current_time\n        self.persist = persist\n        self.game_info = persist\n        self.setupImage(self.image_name, (-118, -40, 800, 600), c.WHITE)\n        pg.display.set_caption('pypvz: 战斗失败！')\n        # 停止播放原来关卡中的音乐\n        pg.mixer.music.stop()\n\n\nclass AwardScreen(tool.State):\n    def __init__(self):\n        tool.State.__init__(self)\n\n    def setupImage(self):\n        # 主体\n        frame_rect = (0, 0, 800, 600)\n        self.image = tool.get_image(\n            tool.GFX[c.AWARD_SCREEN_IMAGE], *frame_rect\n        )\n        self.rect = self.image.get_rect()\n        self.rect.x = 0\n        self.rect.y = 0\n\n        # 文字\n        # 标题处文字\n        font = pg.font.Font(c.FONT_PATH, 37)\n        title_text = font.render('您获得了新的战利品！', True, c.PARCHMENT_YELLOW)\n        title_text_rect = title_text.get_rect()\n        title_text_rect.x = 220\n        title_text_rect.y = 23\n        self.image.blit(title_text, title_text_rect)\n\n        # 按钮\n        frame_rect = (0, 0, 111, 26)\n        if self.show_only_one_option:\n            ## 主菜单按钮\n            self.main_menu_button_image = tool.get_image_alpha(\n                tool.GFX[c.UNIVERSAL_BUTTON], *frame_rect\n            )\n            self.main_menu_button_image_rect = (\n                self.main_menu_button_image.get_rect()\n            )\n            self.main_menu_button_image_rect.x = 343\n            self.main_menu_button_image_rect.y = 520\n            ### 主菜单按钮上的文字\n            font = pg.font.Font(c.FONT_PATH, 18)\n            main_menu_text = font.render('主菜单', True, c.NAVYBLUE)\n            main_menu_text_rect = main_menu_text.get_rect()\n            main_menu_text_rect.x = 29\n            self.main_menu_button_image.blit(\n                main_menu_text, main_menu_text_rect\n            )\n            self.image.blit(\n                self.main_menu_button_image, self.main_menu_button_image_rect\n            )\n\n            # 绘制向日葵奖杯\n            if (\n                self.game_info[c.LEVEL_COMPLETIONS]\n                and self.game_info[c.LITTLEGAME_COMPLETIONS]\n            ):\n                frame_rect = (157, 0, 157, 269)\n                intro_title = '金向日葵奖杯'\n                intro_content = '您已通过所有关卡，获得此奖励！'\n            else:\n                frame_rect = (0, 0, 157, 269)\n                intro_title = '银向日葵奖杯'\n                if self.game_info[c.LEVEL_COMPLETIONS]:\n                    intro_content = '您已完成冒险模式，获得此奖励！'\n                else:\n                    intro_content = '您已完成玩玩小游戏，获得此奖励！'\n            sunflower_trophy_image = tool.get_image_alpha(\n                tool.GFX[c.TROPHY_SUNFLOWER], *frame_rect, scale=0.7\n            )\n            sunflower_trophy_rect = sunflower_trophy_image.get_rect()\n            sunflower_trophy_rect.x = 348\n            sunflower_trophy_rect.y = 108\n            self.image.blit(sunflower_trophy_image, sunflower_trophy_rect)\n\n            # 绘制介绍标题\n            font = pg.font.Font(c.FONT_PATH, 22)\n            intro_title_img = font.render(\n                intro_title, True, c.PARCHMENT_YELLOW\n            )\n            intro_title_rect = intro_title_img.get_rect()\n            intro_title_rect.x = 333\n            intro_title_rect.y = 305\n            self.image.blit(intro_title_img, intro_title_rect)\n\n            # 绘制介绍内容\n            font = pg.font.Font(c.FONT_PATH, 15)\n            intro_content_img = font.render(intro_content, True, c.NAVYBLUE)\n            intro_content_rect = intro_content_img.get_rect()\n            intro_content_rect.x = 290\n            intro_content_rect.y = 370\n            self.image.blit(intro_content_img, intro_content_rect)\n        else:\n            ## 继续按钮\n            self.next_button_image = tool.get_image_alpha(\n                tool.GFX[c.UNIVERSAL_BUTTON], *frame_rect\n            )\n            self.next_button_image_rect = self.next_button_image.get_rect()\n            self.next_button_image_rect.x = 70\n            ### 继续按钮上的文字\n            font = pg.font.Font(c.FONT_PATH, 18)\n            next_text = font.render('继续', True, c.NAVYBLUE)\n            next_text_rect = next_text.get_rect()\n            next_text_rect.x = 37\n            ## 主菜单按钮\n            self.main_menu_button_image = tool.get_image_alpha(\n                tool.GFX[c.UNIVERSAL_BUTTON], *frame_rect\n            )\n            self.main_menu_button_image_rect = (\n                self.main_menu_button_image.get_rect()\n            )\n            self.main_menu_button_image_rect.x = 620\n            self.next_button_image_rect.y = (\n                self.main_menu_button_image_rect.y\n            ) = 540\n            ### 主菜单按钮上的文字\n            main_menu_text = font.render('主菜单', True, c.NAVYBLUE)\n            main_menu_text_rect = main_menu_text.get_rect()\n            main_menu_text_rect.x = 29\n            self.next_button_image.blit(next_text, next_text_rect)\n            self.main_menu_button_image.blit(\n                main_menu_text, main_menu_text_rect\n            )\n            self.image.blit(\n                self.next_button_image, self.next_button_image_rect\n            )\n            self.image.blit(\n                self.main_menu_button_image, self.main_menu_button_image_rect\n            )\n\n    def startup(self, current_time, persist):\n        self.start_time = current_time\n        self.persist = persist\n        self.game_info = persist\n        if (c.PASSED_ALL in self.game_info) and (\n            not self.game_info[c.PASSED_ALL]\n        ):\n            self.show_only_one_option = False\n        else:\n            self.show_only_one_option = True\n        self.setupImage()\n        pg.display.set_caption('pypvz: 您获得了新的战利品！')\n        pg.mixer.music.stop()\n        pg.mixer.music.load(os.path.join(c.PATH_MUSIC_DIR, 'zenGarden.opus'))\n        pg.mixer.music.play(-1, 0)\n        pg.mixer.music.set_volume(self.game_info[c.SOUND_VOLUME])\n\n    def update(self, surface, current_time, mouse_pos, mouse_click):\n        surface.blit(self.image, self.rect)\n        if mouse_pos:\n            # 检查主菜单点击\n            if self.inArea(self.main_menu_button_image_rect, *mouse_pos):\n                self.next = c.MAIN_MENU\n                self.done = True\n            elif not self.show_only_one_option:\n                if self.inArea(self.next_button_image_rect, *mouse_pos):\n                    self.next = c.LEVEL\n                    self.done = True\n\n\nclass HelpScreen(tool.State):\n    def __init__(self):\n        tool.State.__init__(self)\n\n    def startup(self, current_time, persist):\n        self.start_time = current_time\n        self.persist = persist\n        self.game_info = persist\n        self.setupImage()\n        pg.display.set_caption('pypvz: 帮助')\n        pg.mixer.music.stop()\n        c.SOUND_HELP_SCREEN.play()\n\n    def setupImage(self):\n        # 主体\n        frame_rect = (-100, -50, 800, 600)\n        self.image = tool.get_image(\n            tool.GFX[c.HELP_SCREEN_IMAGE], *frame_rect, colorkey=(0, 255, 255)\n        )\n        self.rect = self.image.get_rect()\n        self.rect.x = 0\n        self.rect.y = 0\n\n        # 主菜单按钮\n        frame_rect = (0, 0, 111, 26)\n        self.main_menu_button_image = tool.get_image_alpha(\n            tool.GFX[c.UNIVERSAL_BUTTON], *frame_rect\n        )\n        self.main_menu_button_image_rect = (\n            self.main_menu_button_image.get_rect()\n        )\n        self.main_menu_button_image_rect.x = 343\n        self.main_menu_button_image_rect.y = 500\n        ### 主菜单按钮上的文字\n        font = pg.font.Font(c.FONT_PATH, 18)\n        main_menu_text = font.render('主菜单', True, c.NAVYBLUE)\n        main_menu_text_rect = main_menu_text.get_rect()\n        main_menu_text_rect.x = 29\n        self.main_menu_button_image.blit(main_menu_text, main_menu_text_rect)\n        self.image.blit(\n            self.main_menu_button_image, self.main_menu_button_image_rect\n        )\n\n    def update(self, surface, current_time, mouse_pos, mouse_click):\n        surface.fill(c.BLACK)\n        surface.blit(self.image, self.rect)\n        if mouse_pos:\n            # 检查主菜单点击\n            if self.inArea(self.main_menu_button_image_rect, *mouse_pos):\n                self.next = c.MAIN_MENU\n                self.done = True\n"
  },
  {
    "path": "source/tool.py",
    "content": "import json\nimport logging\nimport os\nfrom abc import abstractmethod\n\nimport pygame as pg\nfrom pygame.locals import *\n\nfrom . import constants as c\n\nlogger = logging.getLogger('main')\n\n# 状态机 抽象基类\nclass State:\n    def __init__(self):\n        self.start_time = 0\n        self.current_time = 0\n        self.done = False   # false 代表未做完\n        self.next = None    # 表示这个状态退出后要转到的下一个状态\n        self.persist = {}   # 在状态间转换时需要传递的数据\n\n    # 当从其他状态进入这个状态时，需要进行的初始化操作\n    @abstractmethod\n    def startup(self, current_time: int, persist: dict):\n        # 前面加了@abstractmethod表示抽象基类中必须要重新定义的method（method是对象和函数的结合）\n        pass\n\n    # 当从这个状态退出时，需要进行的清除操作\n    def cleanup(self):\n        self.done = False\n        return self.persist\n\n    # 在这个状态运行时进行的更新操作\n    @abstractmethod\n    def update(self, surface: pg.Surface, keys, current_time: int):\n        # 前面加了@abstractmethod表示抽象基类中必须要重新定义的method\n        pass\n\n    # 工具：范围判断函数，用于判断点击\n    def inArea(self, rect: pg.Rect, x: int, y: int):\n        if rect.x <= x <= rect.right and rect.y <= y <= rect.bottom:\n            return True\n        else:\n            return False\n\n    # 工具：用户数据保存函数\n    def saveUserData(self):\n        with open(c.USERDATA_PATH, 'w', encoding='utf-8') as f:\n            userdata = {}\n            for i in self.game_info:\n                if i in c.INIT_USERDATA:\n                    userdata[i] = self.game_info[i]\n            data_to_save = json.dumps(userdata, sort_keys=True, indent=4)\n            f.write(data_to_save)\n\n\n# 进行游戏控制 循环 事件响应\nclass Control:\n    def __init__(self):\n        self.screen = pg.display.get_surface()\n        self.done = False\n        self.clock = pg.time.Clock()    # 创建一个对象来帮助跟踪时间\n        self.keys = pg.key.get_pressed()\n        self.mouse_pos = None\n        self.mouse_click = [\n            False,\n            False,\n        ]  # value:[left mouse click, right mouse click]\n        self.current_time = 0.0\n        self.state_dict = {}\n        self.state_name = None\n        self.state = None\n        try:\n            # 存在存档即导入\n            # 先自动修复读写权限(Python权限规则和Unix不一样，420表示unix的644，Windows自动忽略不支持项)\n            os.chmod(c.USERDATA_PATH, 420)\n            with open(c.USERDATA_PATH, encoding='utf-8') as f:\n                userdata = json.load(f)\n        except FileNotFoundError:\n            self.setupUserData()\n        except json.JSONDecodeError:\n            logger.warning('用户存档解码错误！程序将新建初始存档！\\n')\n            self.setupUserData()\n        else:   # 没有引发异常才执行\n            self.game_info = {}\n            # 导入数据，保证了可运行性，但是放弃了数据向后兼容性，即假如某些变量在以后改名，在导入时可能会被重置\n            need_to_rewrite = False\n            for key in c.INIT_USERDATA:\n                if key in userdata:\n                    self.game_info[key] = userdata[key]\n                else:\n                    self.game_info[key] = c.INIT_USERDATA[key]\n                    need_to_rewrite = True\n            if need_to_rewrite:\n                with open(c.USERDATA_PATH, 'w', encoding='utf-8') as f:\n                    savedata = json.dumps(\n                        self.game_info, sort_keys=True, indent=4\n                    )\n                    f.write(savedata)\n        # 存档内不包含即时游戏时间信息，需要新建\n        self.game_info[c.CURRENT_TIME] = 0\n\n        # 50为目前的基础帧率，乘以倍率即是游戏帧率\n        self.fps = 50 * self.game_info[c.GAME_RATE]\n\n    def setupUserData(self):\n        if not os.path.exists(os.path.dirname(c.USERDATA_PATH)):\n            os.makedirs(os.path.dirname(c.USERDATA_PATH))\n        with open(c.USERDATA_PATH, 'w', encoding='utf-8') as f:\n            savedata = json.dumps(c.INIT_USERDATA, sort_keys=True, indent=4)\n            f.write(savedata)\n        self.game_info = c.INIT_USERDATA.copy()   # 内部全是不可变对象，浅拷贝即可\n\n    def setup_states(self, state_dict: dict, start_state):\n        self.state_dict = state_dict\n        self.state_name = start_state\n        self.state = self.state_dict[self.state_name]\n        self.state.startup(self.current_time, self.game_info)\n\n    def update(self):\n        # 自 pygame_init() 调用以来的毫秒数 * 游戏速度倍率，即游戏时间\n        self.current_time = pg.time.get_ticks() * self.game_info[c.GAME_RATE]\n\n        if self.state.done:\n            self.flip_state()\n\n        self.state.update(\n            self.screen, self.current_time, self.mouse_pos, self.mouse_click\n        )\n        self.mouse_pos = None\n        self.mouse_click[0] = False\n        self.mouse_click[1] = False\n\n    # 状态转移\n    def flip_state(self):\n        if self.state.next == c.EXIT:\n            pg.quit()\n            os._exit(0)\n        self.state_name = self.state.next\n        persist = self.state.cleanup()\n        self.state = self.state_dict[self.state_name]\n        self.state.startup(self.current_time, persist)\n\n    def event_loop(self):\n        for event in pg.event.get():\n            if event.type == pg.QUIT:\n                self.done = True\n            elif event.type == pg.KEYDOWN:\n                self.keys = pg.key.get_pressed()\n                if event.key == pg.K_f:\n                    pg.display.set_mode(\n                        c.SCREEN_SIZE, pg.HWSURFACE | pg.FULLSCREEN\n                    )\n                elif event.key == pg.K_u:\n                    pg.display.set_mode(c.SCREEN_SIZE)\n            elif event.type == pg.KEYUP:\n                self.keys = pg.key.get_pressed()\n            elif event.type == pg.MOUSEBUTTONDOWN:\n                self.mouse_pos = pg.mouse.get_pos()\n                (\n                    self.mouse_click[0],\n                    _,\n                    self.mouse_click[1],\n                ) = pg.mouse.get_pressed()\n                # self.mouse_click[0]表示左键，self.mouse_click[1]表示右键\n                print(\n                    f'点击位置: ({self.mouse_pos[0]:3}, {self.mouse_pos[1]:3}) 左右键点击情况: {self.mouse_click}'\n                )\n\n    def run(self):\n        while not self.done:\n            self.event_loop()\n            self.update()\n            pg.display.update()\n            self.clock.tick(self.fps)\n\n\ndef get_image(\n    sheet: pg.Surface,\n    x: int,\n    y: int,\n    width: int,\n    height: int,\n    colorkey: tuple[int] = c.BLACK,\n    scale: int = 1,\n) -> pg.Surface:\n    # 不保留alpha通道的图片导入\n    image = pg.Surface([width, height])\n    rect = image.get_rect()\n\n    image.blit(sheet, (0, 0), (x, y, width, height))\n    if colorkey:\n        image.set_colorkey(colorkey)\n    image = pg.transform.scale(\n        image, (int(rect.width * scale), int(rect.height * scale))\n    )\n    return image\n\n\ndef get_image_alpha(\n    sheet: pg.Surface,\n    x: int,\n    y: int,\n    width: int,\n    height: int,\n    colorkey: tuple[int] = c.BLACK,\n    scale: int = 1,\n) -> pg.Surface:\n    # 保留alpha通道的图片导入\n    image = pg.Surface([width, height], SRCALPHA)\n    rect = image.get_rect()\n\n    image.blit(sheet, (0, 0), (x, y, width, height))\n    image.set_colorkey(colorkey)\n    image = pg.transform.scale(\n        image, (int(rect.width * scale), int(rect.height * scale))\n    )\n    return image\n\n\ndef load_image_frames(\n    directory: str, image_name: str, colorkey: tuple[int], accept: tuple[str]\n) -> list[pg.Surface]:\n    frame_list = []\n    tmp = {}\n    # image_name is \"Peashooter\", pic name is \"Peashooter_1\", get the index 1\n    index_start = len(image_name) + 1\n    frame_num = 0\n    for pic in os.listdir(directory):\n        name, ext = os.path.splitext(pic)\n        if ext.lower() in accept:\n            index = int(name[index_start:])\n            img = pg.image.load(os.path.join(directory, pic))\n            if img.get_alpha():\n                img = img.convert_alpha()\n            else:\n                img = img.convert()\n                img.set_colorkey(colorkey)\n            tmp[index] = img\n            frame_num += 1\n\n    for i in range(frame_num):  # 这里注意编号必须连续，否则会出错\n        frame_list.append(tmp[i])\n    return frame_list\n\n\n# colorkeys 是设置图像中的某个颜色值为透明,这里用来消除白边\ndef load_all_gfx(\n    directory: str,\n    colorkey: tuple[int] = c.WHITE,\n    accept: tuple[str] = ('.png', '.jpg', '.bmp', '.gif', '.webp'),\n) -> dict[str : pg.Surface]:\n    graphics = {}\n    for name1 in os.listdir(directory):\n        # subfolders under the folder resources\\graphics\n        dir1 = os.path.join(directory, name1)\n        if os.path.isdir(dir1):\n            for name2 in os.listdir(dir1):\n                dir2 = os.path.join(dir1, name2)\n                if os.path.isdir(dir2):\n                    # e.g. subfolders under the folder resources\\graphics\\Zombies\n                    for name3 in os.listdir(dir2):\n                        dir3 = os.path.join(dir2, name3)\n                        # e.g. subfolders or pics under the folder resources\\graphics\\Zombies\\ConeheadZombie\n                        if os.path.isdir(dir3):\n                            # e.g. it\"s the folder resources\\graphics\\Zombies\\ConeheadZombie\\ConeheadZombieAttack\n                            image_name, _ = os.path.splitext(name3)\n                            graphics[image_name] = load_image_frames(\n                                dir3, image_name, colorkey, accept\n                            )\n                        else:\n                            # e.g. pics under the folder resources\\graphics\\Plants\\Peashooter\n                            image_name, _ = os.path.splitext(name2)\n                            graphics[image_name] = load_image_frames(\n                                dir2, image_name, colorkey, accept\n                            )\n                            break\n                else:\n                    # e.g. pics under the folder resources\\graphics\\Screen\n                    name, ext = os.path.splitext(name2)\n                    if ext.lower() in accept:\n                        img = pg.image.load(dir2)\n                        if img.get_alpha():\n                            img = img.convert_alpha()\n                        else:\n                            img = img.convert()\n                            img.set_colorkey(colorkey)\n                        graphics[name] = img\n    return graphics\n\n\npg.display.set_caption(c.ORIGINAL_CAPTION)  # 设置标题\nSCREEN = pg.display.set_mode(c.SCREEN_SIZE, pg.SCALED)   # 设置初始屏幕\npg.mixer.set_num_channels(255)  # 设置可以同时播放的音频数量，默认为8，经常不够用\nif os.path.exists(\n    c.ORIGINAL_LOGO\n):    # 设置窗口图标，仅对非Nuitka时生效，Nuitka不需要包括额外的图标文件，自动跳过这一过程即可\n    pg.display.set_icon(pg.image.load(c.ORIGINAL_LOGO))\n\nGFX = load_all_gfx(c.PATH_IMG_DIR)\n"
  }
]