Full Code of wszqkzqk/pypvz for AI

master c88a018209ca cached
72 files
321.0 KB
86.1k tokens
467 symbols
1 requests
Download .txt
Showing preview only (357K chars total). Download the full file or copy to clipboard to get everything.
Repository: wszqkzqk/pypvz
Branch: master
Commit: c88a018209ca
Files: 72
Total size: 321.0 KB

Directory structure:
gitextract_dzufbwce/

├── .github/
│   └── workflows/
│       ├── build-pr.yml
│       └── build.yml
├── .gitignore
├── .python-version
├── README.md
├── pyproject.toml
├── pypvz.py
├── resources/
│   ├── music/
│   │   ├── battle.opus
│   │   ├── bowling.opus
│   │   ├── chooseYourSeeds.opus
│   │   ├── dayLevel.opus
│   │   ├── fogLevel.opus
│   │   ├── intro.opus
│   │   ├── nightLevel.opus
│   │   ├── poolLevel.opus
│   │   └── zenGarden.opus
│   └── sound/
│       ├── bigchomp.ogg
│       ├── bomb.ogg
│       ├── bowlingimpact.ogg
│       ├── bulletExplode.ogg
│       ├── buttonclick.ogg
│       ├── cannotChooseWarning.ogg
│       ├── carWalking.ogg
│       ├── clickCard.ogg
│       ├── collectSun.ogg
│       ├── doomshroom.ogg
│       ├── evillaugh.ogg
│       ├── finalfanfare.ogg
│       ├── firepea.ogg
│       ├── freeze.ogg
│       ├── fume.ogg
│       ├── gravebusterchomp.ogg
│       ├── helpScreen.ogg
│       ├── hugeWaveApproching.ogg
│       ├── hypnoed.ogg
│       ├── lose.ogg
│       ├── mushroomWakeup.ogg
│       ├── newspaperRip.ogg
│       ├── newspaperZombieAngry.ogg
│       ├── plant.ogg
│       ├── plantDie.ogg
│       ├── plantGrow.ogg
│       ├── polevaultjump.ogg
│       ├── potatomine.ogg
│       ├── puff.ogg
│       ├── scream.ogg
│       ├── shoot.ogg
│       ├── shovel.ogg
│       ├── snowPeaSparkles.ogg
│       ├── squashHmm.ogg
│       ├── squashing.ogg
│       ├── tangleKelpDrag.ogg
│       ├── tap.ogg
│       ├── win.ogg
│       ├── zombieAttack.ogg
│       ├── zombieComing.ogg
│       ├── zombieEnteringWater.ogg
│       ├── zombieVoice.ogg
│       ├── zomboni.ogg
│       └── zomboniExplosion.ogg
└── source/
    ├── __init__.py
    ├── component/
    │   ├── __init__.py
    │   ├── map.py
    │   ├── menubar.py
    │   ├── plant.py
    │   └── zombie.py
    ├── constants.py
    ├── state/
    │   ├── __init__.py
    │   ├── level.py
    │   ├── mainmenu.py
    │   └── screen.py
    └── tool.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/build-pr.yml
================================================
name: "Build for PR"

concurrency: 
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

on:
  pull_request:

jobs:
  windows:
    runs-on: windows-latest
    strategy:
      fail-fast: false
      matrix:
        python_version:
            - "3.12"
    name: Windows Python ${{ matrix.python_version }}
    steps:
      - uses: actions/checkout@v2

      - uses: ilammy/msvc-dev-cmd@v1

      - name: Use Python ${{ matrix.python_version }}
        uses: actions/setup-python@v2
        with:
          python-version: ${{ matrix.python_version }}

      - name: 🧳 Install dependencies
        run: |
          echo y | pip install --no-python-version-warning --disable-pip-version-check pyinstaller
          echo y | pip install --no-python-version-warning --disable-pip-version-check nuitka
          echo y | pip install --no-python-version-warning --disable-pip-version-check zstandard
          echo y | pip install --no-python-version-warning --disable-pip-version-check pygame
          echo y | pip install --no-python-version-warning --disable-pip-version-check ordered-set

      # 使用pyinstaller构建
      - name: Build pypvz with pyinstaller
        run: |
          pyinstaller -F pypvz.py `
                  -n pypvz-with-python${{ matrix.python_version }}-pyinstaller-x64.exe `
                  --distpath ./out `
                  --noconsole `
                  --add-data="resources;./resources" `
                  --add-data="pypvz-exec-logo.png;./pypvz-exec-logo.png" `
                  -i ./pypvz.ico

      - name: Release the version built by pyinstaller
        if: github.event.pull_request.head.repo.full_name == github.repository
        uses: ncipollo/release-action@v1
        with:
          allowUpdates: true
          tag: Dev.Version.Built.with.Pyinstaller
          artifacts: ./out/*pyinstaller*.exe
          token: ${{ secrets.GITHUB_TOKEN }}

      # 使用Nuitka构建
      - name: Show nuitka version
        run: |
          Get-ChildItem env:
          python -m nuitka --version

      - name: Build pypvz with Nuitka
        run: |
          echo y | python -m nuitka --standalone `
                  --onefile `
                  --show-progress `
                  --show-memory `
                  --output-dir=out `
                  --windows-icon-from-ico=pypvz.ico `
                  --include-data-file=c:\hostedtoolcache\windows\python\${{ matrix.python_version }}*\x64\lib\site-packages\pygame\libogg-0.dll=libogg-0.dll `
                  --include-data-file=c:\hostedtoolcache\windows\python\${{ matrix.python_version }}*\x64\lib\site-packages\pygame\libopus-0.dll=libopus-0.dll `
                  --include-data-file=c:\hostedtoolcache\windows\python\${{ matrix.python_version }}*\x64\lib\site-packages\pygame\libopusfile-0.dll=libopusfile-0.dll `
                  --include-data-file=c:\hostedtoolcache\windows\python\${{ matrix.python_version }}*\x64\lib\site-packages\pygame\libjpeg-9.dll=libjpeg-9.dll `
                  --include-data-dir=resources=resources `
                  --windows-disable-console `
                  -o pypvz-with-python${{ matrix.python_version }}-nuitka-windows-x64.exe `
                  pypvz.py

      - name: Release the version built by nuitka
        if: github.event.pull_request.head.repo.full_name == github.repository
        uses: ncipollo/release-action@v1
        with:
          allowUpdates: true
          tag: Dev
          artifacts: ./out/*nuitka*.exe
          token: ${{ secrets.GITHUB_TOKEN }}


  linux:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python_version:
            - "3.12"
    name: Ubuntu Python ${{ matrix.python_version }}
    steps:
      - name: 🛎️ Checkout
        uses: actions/checkout@v2
        with:
          fetch-depth: 0

      - name: 🐍 Use Python ${{ matrix.python_version }}
        uses: actions/setup-python@v2
        with:
          python-version: ${{ matrix.python_version }}

      - name: 🧳 Install dependencies
        run: |
          sudo apt-get update
          sudo apt-get install patchelf gdb ccache libfuse2 zstd tar
          python -m pip install --no-python-version-warning --disable-pip-version-check zstandard appdirs ordered-set tqdm Jinja2
          python -m pip install --no-python-version-warning --disable-pip-version-check nuitka
          python -m pip install --no-python-version-warning --disable-pip-version-check pygame

      # 使用Nuitka构建
      - name: Show nuitka version
        run: |
          env
          python -m nuitka --version

      - name: Build pypvz with Nuitka
        run: |
          yes | python -m nuitka \
                --onefile \
                --standalone \
                --include-data-dir=resources=resources \
                --linux-onefile-icon=pypvz.png \
                --static-libpython=no \
                --remove-output \
                -o pypvz-with-python${{ matrix.python_version }}-linux-x86_64.bin \
                pypvz.py

      - name: Release the version built by nuitka
        if: github.event.pull_request.head.repo.full_name == github.repository
        uses: ncipollo/release-action@v1
        with:
          allowUpdates: true
          tag: Dev
          artifacts: ./pypvz*-x86_64.*
          token: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/build.yml
================================================
name: Build

concurrency: 
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

on:
  push:
    branches:
      - master

jobs:
  windows:
    runs-on: windows-latest
    strategy:
      fail-fast: false
      matrix:
        python_version:
            - "3.12"
    name: Windows Python ${{ matrix.python_version }}
    steps:
      - uses: actions/checkout@v2

      - uses: ilammy/msvc-dev-cmd@v1

      - name: Use Python ${{ matrix.python_version }}
        uses: actions/setup-python@v2
        with:
          python-version: ${{ matrix.python_version }}

      - name: 🧳 Install dependencies
        run: |
          echo y | pip install --no-python-version-warning --disable-pip-version-check pyinstaller
          echo y | pip install --no-python-version-warning --disable-pip-version-check nuitka
          echo y | pip install --no-python-version-warning --disable-pip-version-check zstandard
          echo y | pip install --no-python-version-warning --disable-pip-version-check pygame
          echo y | pip install --no-python-version-warning --disable-pip-version-check ordered-set

      # 使用pyinstaller构建
      - name: Build pypvz with pyinstaller
        run: |
          pyinstaller -F pypvz.py `
                  -n pypvz-with-python${{ matrix.python_version }}-pyinstaller-x64.exe `
                  --distpath ./out `
                  --noconsole `
                  --add-data="resources;./resources" `
                  --add-data="pypvz-exec-logo.png;./pypvz-exec-logo.png" `
                  -i ./pypvz.ico

      - name: Release the version built by pyinstaller
        uses: ncipollo/release-action@v1
        with:
          allowUpdates: true
          tag: Current.Version.Built.with.Pyinstaller
          artifacts: ./out/*pyinstaller*.exe
          token: ${{ secrets.GITHUB_TOKEN }}

      # 使用Nuitka构建
      - name: Show nuitka version
        run: |
          Get-ChildItem env:
          python -m nuitka --version

      - name: Build pypvz with Nuitka
        run: |
          echo y | python -m nuitka --standalone `
                  --onefile `
                  --show-progress `
                  --show-memory `
                  --output-dir=out `
                  --windows-icon-from-ico=pypvz.ico `
                  --include-data-file=c:\hostedtoolcache\windows\python\${{ matrix.python_version }}*\x64\lib\site-packages\pygame\libogg-0.dll=libogg-0.dll `
                  --include-data-file=c:\hostedtoolcache\windows\python\${{ matrix.python_version }}*\x64\lib\site-packages\pygame\libopus-0.dll=libopus-0.dll `
                  --include-data-file=c:\hostedtoolcache\windows\python\${{ matrix.python_version }}*\x64\lib\site-packages\pygame\libopusfile-0.dll=libopusfile-0.dll `
                  --include-data-file=c:\hostedtoolcache\windows\python\${{ matrix.python_version }}*\x64\lib\site-packages\pygame\libjpeg-9.dll=libjpeg-9.dll `
                  --include-data-dir=resources=resources `
                  --windows-disable-console `
                  -o pypvz-with-python${{ matrix.python_version }}-nuitka-windows-x64.exe `
                  pypvz.py


      - name: Release the version built by nuitka
        uses: ncipollo/release-action@v1
        with:
          allowUpdates: true
          tag: Latest
          artifacts: ./out/*nuitka*.exe
          token: ${{ secrets.GITHUB_TOKEN }}

  linux:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python_version:
            - "3.12"
    name: Ubuntu Python ${{ matrix.python_version }}
    steps:
      - name: 🛎️ Checkout
        uses: actions/checkout@v2
        with:
          fetch-depth: 0

      - name: 🐍 Use Python ${{ matrix.python_version }}
        uses: actions/setup-python@v2
        with:
          python-version: ${{ matrix.python_version }}

      - name: 🧳 Install dependencies
        run: |
          sudo apt-get update
          sudo apt-get install patchelf gdb ccache libfuse2 zstd tar
          python -m pip install --no-python-version-warning --disable-pip-version-check zstandard appdirs ordered-set tqdm Jinja2
          python -m pip install --no-python-version-warning --disable-pip-version-check nuitka
          python -m pip install --no-python-version-warning --disable-pip-version-check pygame

      # 使用Nuitka构建
      - name: Show nuitka version
        run: |
          env
          python -m nuitka --version

      - name: Build pypvz with Nuitka
        run: |
          yes | python -m nuitka \
                --onefile \
                --standalone \
                --include-data-dir=resources=resources \
                --linux-onefile-icon=pypvz.png \
                --static-libpython=no \
                --remove-output \
                -o pypvz-with-python${{ matrix.python_version }}-linux-x86_64.bin \
                pypvz.py

      - name: Release the version built by nuitka
        uses: ncipollo/release-action@v1
        with:
          allowUpdates: true
          tag: Latest
          artifacts: ./pypvz*-x86_64.*
          token: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
# 忽略构建内容
out/
test-build/
release/
# 忽略调试内容
.vscode/
# 忽略 Pycharm 项目文件
.idea/
__pycache__/
*/__pycache__/
# 忽略测试文件
test*.py
# uv 管理的虚拟环境
.venv


================================================
FILE: .python-version
================================================
3.12


================================================
FILE: README.md
================================================
## Python版植物大战僵尸

植物大战僵尸游戏的Python实现,基于[marblexu的项目进行创作](https://github.com/marblexu/PythonPlantsVsZombies),部分代码也整合自[callmebg的项目](https://github.com/callmebg/PythonPlantsVsZombies)

**本项目为个人python语言学习的练习项目,仅供个人学习和研究使用,不得用于其他用途。如果这个游戏侵犯了版权,请联系我删除**

* 已有的植物:向日葵,豌豆射手,坚果墙,寒冰射手,樱桃炸弹,双发射手,三线射手,大嘴花,小喷菇,土豆雷,地刺,胆小菇,倭瓜,火爆辣椒,阳光菇,寒冰菇,魅惑菇,火炬树桩,睡莲,杨桃,咖啡豆,海蘑菇,高坚果,缠绕水草,毁灭菇,墓碑吞噬者,大喷菇,大蒜,南瓜头
* 已有的僵尸:普通僵尸,旗帜僵尸,路障僵尸,铁桶僵尸,读报僵尸,橄榄球僵尸,鸭子救生圈僵尸,铁门僵尸,撑杆跳僵尸,冰车僵尸,潜水僵尸
* 使用JSON文件记录关卡信息数据
  * 在0.8.18.0及以后直接用python记录关卡的不可变数据,JSON目前仅用于用户存档
* 支持选择植物卡片
* 支持白昼模式,夜晚模式,泳池模式,浓雾模式(暂时没有加入雾),传送带模式和坚果保龄球模式
* 支持背景音乐播放
  * 支持调节音量
* 支持音效
  * 支持与背景音乐一起调节音量
* 支持全屏模式
  * 按`F`键进入全屏模式,按`U`键恢复至窗口模式
* 支持用小铲子移除植物
* 支持分波生成僵尸
* 支持“关卡进程”进度条显示
* 夜晚模式支持墓碑以及从墓碑生成僵尸
* 含有泳池的模式支持在最后一波时从泳池中自动冒出僵尸
* 支持保存进度
  * Windows下默认进度文件的保存路径为`~\AppData\Roaming\pypvz\userdata.json`
  * 其他操作系统为`~/.config/pypvz/userdata.json`
  * 存档为JSON文件,如果出现因存档损坏而造成程序无法启动,可以手动编辑修复或者删除该文件重试
    * 0.8.12.0版本后理论上不可能因为存档损坏而无法启动,如果有,请在[issues](https://github.com/wszqkzqk/pypvz/issues)中报告bug
      * 仍然有可能因为升级后变量名不同而丢失存档的进度信息,这种情况手动编辑恢复即可
* 支持错误日志记录
  * Windows下默认日志文件的保存路径为`~\AppData\Roaming\pypvz\run.log`
  * 其他操作系统为`~/.config/pypvz/run.log`
* 支持自定义游戏速度倍率
  * 保存在游戏存档文件中,可以通过修改`game rate`值更改速度倍率
* 游戏完成成就显示
  * 任意一游戏模式全部完成显示银向日葵奖杯
  * 所有模式全部完成显示金向日葵奖杯
  * 光标移动到向日葵奖杯上是显示当前各个模式通关次数
* 含有游戏帮助界面 QwQ

## 环境安装

建议使用 [uv](https://docs.astral.sh/uv/) 安装依赖:

```bash
git clone https://github.com/wszqkzqk/pypvz.git
cd pypvz
uv sync
```

或者参考:

* `Python3` (建议 >= 3.10,最好使用最新版)
* `Python-Pygame` (建议 >= 2.0,最好使用最新版)

## 开始游戏

### 使用仓库源代码

先克隆仓库内容,再运行`pypvz.py`:

```shell
git clone https://github.com/wszqkzqk/pypvz.git
cd pypvz
python pypvz.py
```

### 使用Windows可执行文件

下载`pypvz.exe`文件,双击运行即可
- 可以在仓库的[`Releases`](https://github.com/wszqkzqk/pypvz/releases)页面中[下载最新版(点击跳转)](https://github.com/wszqkzqk/pypvz/releases/latest)(推荐):
  - 使用GCC编译
  - 程序包含名称、版本等信息
  - 得到的验证最多
  - 并非每次提交都会更新,更新可能不及时
- 也可以直接下载GitHub Workflow[自动利用Nuitka构建的版本(点击跳转)](https://github.com/wszqkzqk/pypvz/releases/tag/Latest)(推荐):
  - 使用MSVC编译
  - 每次合并提交到主分支时更新
  - 得到的验证较多
  - 服务器构建,编译环境更纯粹,冗余更少,体积更小
- 还可以下载GitHub Workflow[自动利用Pyinstaller构建的版本(点击跳转)](https://github.com/wszqkzqk/pypvz/releases/tag/Current.Version.Built.with.Pyinstaller):
  - 在程序闪退时有报错窗口弹出
  - 程序性能较差,不推荐
- 均仅支持64位操作系统
- 不依赖python、pygame等外部环境,开箱即用

### 使用Linux可执行文件

由于Linux几乎都标配了Python环境,因此本程序不太重视Linux下可执行的单文件的维护,因此没有手动构建版,只能下载自动构建的软件包。可以在仓库的[`Releases`](https://github.com/wszqkzqk/pypvz/releases)页面中[下载最新版(点击跳转)](https://github.com/wszqkzqk/pypvz/releases/latest)。

## 方法

* 使用鼠标收集阳光,种植植物
* 对于已经存在存档的用户,可以在`~\AppData\Roaming\pypvz\userdata.json`(Windows)或`~/.config/pypvz/userdata.json`(其他操作系统)中修改当前关卡:
  * 冒险模式:
    * 白昼模式——单行草皮:1
    * 白昼模式——三行草皮:2
    * 白昼模式:3~5
    * 夜晚模式:6~8
    * 泳池模式:9~11
    * 浓雾模式(暂时没有雾):12
  * 小游戏模式:
    * 坚果保龄球:1
    * 传送带模式(白天):2
    * 传送带模式(黑夜):3
    * 传送带模式(泳池):4
    * 坚果保龄球(II):5
  * 目前暂时按照以上设定,未与原版相符
* 可以通过修改存档JSON文件中的`game rate`值来调节游戏速度倍率

## Windows单文件封装

### 使用Nuitka进行构建

编译依赖:
- `Python3` (建议 >= 3.10,最好使用最新版)
- `Python-Pygame` (建议 >= 2.0,最好使用最新版)
- `Nuitka`
- `MinGW-w64`(或其他C编译器)
- `ccache`
- `depends`
- `python-zstandard`(可选)

**在编译环境安装不全时,Nuitka可以自动安装MinGW-w64、ccache和depends**

- 由于目前Nuitka打包尚存bug,无法自动封装`pygame`中用来解码音频的相关`.dll`文件,因此需要手动在编译命令中添加
  - 对于`mp3`编码,需要添加`libmpg123-0.dll`
  - 对于`vorbis`编码,需要添加`libogg-0.dll`,`libvorbis-0.dll`和`libvorbisfile-3.dll`
  - 对于`opus`编码,需要添加`libogg-0.dll`,`libopus-0.dll`和`libopusfile-0.dll`
- 以添加`opus`和`vorbis`编码的背景音乐支持为例,编译需执行以下命令:

``` cmd
git clone https://github.com/wszqkzqk/pypvz.git
cd pypvz
nuitka --mingw64 --standalone `
        --onefile `
        --show-progress `
        --show-memory `
        --output-dir=release `
        --windows-icon-from-ico=pypvz.ico `
        --include-data-dir=resources=resources `
        --include-data-file=C:\Users\17265\AppData\Local\Programs\Python\Python310\Lib\site-packages\pygame\libogg-0.dll=libogg-0.dll `
        --include-data-file=C:\Users\17265\AppData\Local\Programs\Python\Python310\Lib\site-packages\pygame\libopus-0.dll=libopus-0.dll `
        --include-data-file=C:\Users\17265\AppData\Local\Programs\Python\Python310\Lib\site-packages\pygame\libopusfile-0.dll=libopusfile-0.dll `
        --include-data-file=C:\Users\17265\AppData\Local\Programs\Python\Python310\Lib\site-packages\pygame\libvorbisfile-3.dll=libvorbisfile-3.dll `
        --include-data-file=C:\Users\17265\AppData\Local\Programs\Python\Python310\Lib\site-packages\pygame\libvorbis-0.dll=libvorbis-0.dll `
        --lto=yes `
        --windows-disable-console `
        --windows-product-name=pypvz `
        --windows-company-name=wszqkzqk.dev `
        --windows-file-description="pypvz" `
        --windows-product-version=0.8.2.0 `
        pypvz.py
```

* 其中`C:\Users\17265\AppData\Local\Programs\Python\Python310\Lib\site-packages\pygame\xxx.dll`应当替换为`xxx.dll`实际所在路径,`--output-dir=`后应当跟实际需要输出的路径,绝对路径或者相对路径均可
* 由于仅复制了`opus`与`vorbis`的解码器,故要求所有背景音乐都要以opus或vorbis编码
* `--windows-product-version=`表示版本号信息,所跟内容格式必须为`x.x.x.x`
* 建议开启`--lto=yes`选项优化链接,如果编译失败可以关闭此选项

可执行文件生成路径为`./release/pypvz.exe`

如果只需要在本地生成编译文件测试,则只需要执行:

``` cmd
nuitka --mingw64 `
    --follow-imports `
    --show-progress `
    --output-dir=test-build `
    --windows-icon-from-ico=pypvz.ico `
    --windows-product-name=pypvz `
    --windows-company-name=wszqkzqk.dev `
    --windows-file-description=pypvz `
    --windows-disable-console `
    --windows-product-version=0.8.2.0 `
    pypvz.py
```

这样生成的程序只能在具有相同python环境的机器上运行

### 使用pyinstaller进行构建

- 由于pyinstaller构建的程序运行效率显著较nuitka构建的程序低下,并且程序体积也往往比nuitka构建的程序大,因此本项目并不推荐使用pyinstaller构建
- 但是因为pyinstaller直接封装了所导入的库中的全部内容,使用pyinstaller构建时不需要手动添加媒体解码库
- pyinstaller并没有涉及python源代码优化、C源代码生成以及C源代码编译链接过程,因此编译速度显著快于nuitka

编译依赖:
- `Python3` (建议 >= 3.10,最好使用最新版)
- `Python-Pygame` (建议 >= 2.0,最好使用最新版)
- `Pyinstaller`

编译参考命令:
``` cmd
pyinstaller -F pypvz.py `
                  --distpath ./release `
                  --noconsole `
                  --add-data="resources;./resources" `
                  --add-data="pypvz-exec-logo.png;./pypvz-exec-logo.png" `
                  -i ./pypvz.ico
```

可执行文件生成路径为`./release/pypvz.exe`

### 使用Github Workflow进行自动构建

直接复制本项目下的`.github/workflows`下的文件,进行少许改动即可满足大多数需求

## 已知bug

以下问题囿于个人目前的能力与精力,没有修复:
* 冷冻的僵尸未用蓝色滤镜标识
  * 这个想不到很好的实现方法,可能会想一种替代方案
* 魅惑的僵尸未用红色滤镜标识
  * 这个可能会作为一种“特性”
* 南瓜头显示不正常
* 墓碑吞噬者吞噬墓碑过程中被吞噬的墓碑顶端不会消失

**欢迎提供[Pull requests](https://github.com/wszqkzqk/pypvz/pulls)或修复方法建议,也欢迎在这里反馈新的bug()**

## ~~画大饼~~计划(不保证实施)

* 增加关卡进程进度条
  * 该功能自0.5.4已实现
* 增加保存数据文件以存储用户进度的功能
  * 该功能自0.8.0.0已实现
* 增加调整音量的功能
  * `pg.mixer.music.set_volume()`
  * 可以用`音量+`、`音量-`按钮实现
  * 注意字体颜色渲染
  * 该功能自0.8.14.0已实现
* 关卡开始前增加预览界面
* 增加解锁与选关功能
  * 目前的设想与原版不同,在完成两轮冒险模式(初始冒险模式 + 戴夫选关的冒险模式)后可以自主选关~~(当然现在只是画饼)~~
* 更改僵尸生成方式
  * 使僵尸生成更随机化,由JSON记录改为随机数生成
    * 该功能自0.5.0已经基本实现
    * 使用原版设定,每面旗帜出10波僵尸,9个小波,1个大波
    * 采用手机版设定,无尽模式没有红眼计数和变速设定,每波红眼权重为1000,平均分布
  * 增加僵尸死亡后有概率掉落奖励的机制
* 增加更多植物、僵尸类型与游戏功能、模式,尽量符合原版基本设计
* 细分伤害种类
  * 实体
    * 一般子弹实体——普通伤害且无特殊效果
      * 豌豆
        * 已实现
      * 孢子
        * 已实现
      * 星星
        * 已实现
      * 尖刺
    * 特殊子弹实体——非普通伤害或有特殊效果
      * 冰豌豆(减速)
        * 已实现
      * 火豌豆(2倍伤害、带有1x1溅射)
        * 已实现
    * 投掷
      * 西瓜(4倍伤害,带有3x3溅射)
      * 冰瓜(4倍伤害,带有3x3溅射伤害与减速)
      * 玉米粒
      * 黄油(2倍伤害,定格)
      * 卷心菜(2倍伤害)
    * 烟雾
      * 线形范围烟雾
        * 自0.7.10.0起已实现
      * 圆形范围烟雾
    * 碾压
      * 倭瓜
        * 已实现
  * 爆炸
    * 一般爆炸
      * 樱桃炸弹、爆炸坚果与玉米加农炮炮弹
        * 已实现
      * 毁灭菇
        * 自0.7.6.0已实现
    * 火焰爆炸
      * 火爆辣椒
        * 已实现
    * 非灰烬类爆炸
      * 土豆雷
        * 已实现
  * 从地面刺伤
    * 已实现
  * 缠绕与拖拽
    * 自0.7.5.0已实现
    * 与原版有所区别,设定上秒杀任意僵尸
  * 吞噬
    * 已实现
    * 与原版有所区别,设定上秒杀任意僵尸
  * 特殊
    * 魅惑
      * 已实现
    * 移除铁制防具
    * 全场伤害与冰冻
      * 已实现
    * 撞击
      * 坚果保龄球撞击
        * 已实现
      * 巨型坚果保龄球撞击
    * 吹走
* 增加部分音效
  * 如爆炸、打击等
  * 自0.6.9已部分实现
* 增加关卡前的本关僵尸预览
* 鼠标移动到植物上时显示部分信息,类似图鉴功能

## 截屏

![截屏1](/screenshots/screenshot-1.webp)
![截屏2](/screenshots/screenshot-2.webp)
![截屏3](/screenshots/screenshot-3.webp)
![截屏4](/screenshots/screenshot-4.webp)
![截屏5](/screenshots/screenshot-5.webp)
![截屏6](/screenshots/screenshot-6.webp)
![截屏7](/screenshots/screenshot-7.webp)
![截屏8](/screenshots/screenshot-8.webp)
![截屏9](/screenshots/screenshot-9.webp)
![截屏10](/screenshots/screenshot-10.webp)
![截屏11](/screenshots/screenshot-11.webp)
![截屏12](/screenshots/screenshot-12.webp)
![截屏13](/screenshots/screenshot-13.webp)
![截屏14](/screenshots/screenshot-14.webp)
![截屏15](/screenshots/screenshot-15.webp)
![截屏16](/screenshots/screenshot-16.webp)
![截屏17](/screenshots/screenshot-17.webp)
![截屏18](/screenshots/screenshot-18.webp)
![截屏19](/screenshots/screenshot-19.webp)
![截屏20](/screenshots/screenshot-20.webp)
![截屏21](/screenshots/screenshot-21.webp)
![截屏22](/screenshots/screenshot-22.webp)
![截屏23](/screenshots/screenshot-23.webp)

## 关于日志与反馈

对于闪退情况,Linux用户与Windows下的python源代码运行用户可以直接在终端中复制出崩溃日志进行反馈。

Windows单文件封装版本无法通过终端显示日志,需要在日志文件中寻找崩溃原因
* Windows默认日志文件路径为`~\AppData\Roaming\pypvz\run.log`
* 其他操作系统为`~/.config/pypvz/run.log`,但一般可以在终端中显示时用终端中的输出即可


================================================
FILE: pyproject.toml
================================================
[project]
name = "pypvz"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "blue>=0.9.1",
    "pygame>=2.6.1",
    "setuptools>=80.9.0",
    "wheel>=0.45.1",
]


================================================
FILE: pypvz.py
================================================
#!/usr/bin/env python
import logging
import os
import traceback
from logging.handlers import RotatingFileHandler

import pygame as pg

# 由于在后续本地模块中存在对pygame的调用,在此处必须完成pygame的初始化
os.environ[
    'SDL_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR'
] = '0'   # 设置临时环境变量以避免Linux下禁用x11合成器
pg.init()

from source import constants as c
from source import tool
from source.state import level, mainmenu, screen

if __name__ == '__main__':
    # 日志设置
    if not os.path.exists(os.path.dirname(c.USERLOG_PATH)):
        os.makedirs(os.path.dirname(c.USERLOG_PATH))
    logger = logging.getLogger('main')
    formatter = logging.Formatter('%(asctime)s - %(levelname)s: %(message)s')
    fileHandler = RotatingFileHandler(
        c.USERLOG_PATH, 'a', 1_000_000, 0, 'utf-8'
    )
    # 设置日志文件权限,Unix为644,Windows为可读写;Python的os.chmod与Unix chmod相同,但要显式说明8进制
    os.chmod(c.USERLOG_PATH, 0o644)
    fileHandler.setFormatter(formatter)
    streamHandler = logging.StreamHandler()
    streamHandler.setFormatter(formatter)
    logger.addHandler(fileHandler)
    logger.addHandler(streamHandler)

    try:
        # 控制状态机运行
        game = tool.Control()
        state_dict = {
            c.MAIN_MENU: mainmenu.Menu(),
            c.GAME_VICTORY: screen.GameVictoryScreen(),
            c.GAME_LOSE: screen.GameLoseScreen(),
            c.LEVEL: level.Level(),
            c.AWARD_SCREEN: screen.AwardScreen(),
            c.HELP_SCREEN: screen.HelpScreen(),
        }
        game.setup_states(state_dict, c.MAIN_MENU)
        game.run()
    except:
        print()   # 将日志输出与上文内容分隔开,增加可读性
        logger.error(f'\n{traceback.format_exc()}')


================================================
FILE: source/__init__.py
================================================


================================================
FILE: source/component/__init__.py
================================================


================================================
FILE: source/component/map.py
================================================
import random

from .. import constants as c


# 记录植物种植情况的地图管理工具
class Map:
    def __init__(self, background_type: int):
        self.background_type = background_type
        # 注意:从0开始编号
        if self.background_type in c.POOL_EQUIPPED_BACKGROUNDS:
            self.width = c.GRID_POOL_X_LEN
            self.height = c.GRID_POOL_Y_LEN
            self.grid_height_size = c.GRID_POOL_Y_SIZE
            self.map = [
                [
                    self.initMapGrid(c.MAP_WATER)
                    if 2 <= y <= 3
                    else self.initMapGrid(c.MAP_GRASS)
                    for x in range(self.width)
                ]
                for y in range(self.height)
            ]
        elif self.background_type in c.ON_ROOF_BACKGROUNDS:
            self.width = c.GRID_ROOF_X_LEN
            self.height = c.GRID_ROOF_Y_LEN
            self.grid_height_size = c.GRID_ROOF_Y_SIZE
            self.map = [
                [self.initMapGrid(c.MAP_TILE) for x in range(self.width)]
                for y in range(self.height)
            ]
        elif self.background_type == c.BACKGROUND_SINGLE:
            self.width = c.GRID_X_LEN
            self.height = c.GRID_Y_LEN
            self.grid_height_size = c.GRID_Y_SIZE
            self.map = [
                [
                    self.initMapGrid(c.MAP_GRASS)
                    if y == 2
                    else self.initMapGrid(c.MAP_UNAVAILABLE)
                    for x in range(self.width)
                ]
                for y in range(self.height)
            ]
        elif self.background_type == c.BACKGROUND_TRIPLE:
            self.width = c.GRID_X_LEN
            self.height = c.GRID_Y_LEN
            self.grid_height_size = c.GRID_Y_SIZE
            self.map = [
                [
                    self.initMapGrid(c.MAP_GRASS)
                    if 1 <= y <= 3
                    else self.initMapGrid(c.MAP_UNAVAILABLE)
                    for x in range(self.width)
                ]
                for y in range(self.height)
            ]
        else:
            self.width = c.GRID_X_LEN
            self.height = c.GRID_Y_LEN
            self.grid_height_size = c.GRID_Y_SIZE
            self.map = [
                [self.initMapGrid(c.MAP_GRASS) for x in range(self.width)]
                for y in range(self.height)
            ]

    def isValid(self, map_x: int, map_y: int) -> bool:
        if (0 <= map_x < self.width) and (0 <= map_y < self.height):
            return True
        return False

    # 地图单元格状态
    # 注意是可变对象,不能直接引用
    # 由于同一格显然不可能种两个相同的植物,所以用集合
    def initMapGrid(self, plot_type: str) -> set:
        return {
            c.MAP_PLANT: set(),
            c.MAP_SLEEP: False,
            c.MAP_PLOT_TYPE: plot_type,
        }

    # 判断位置是否可用
    # 暂时没有写紫卡植物的判断方法
    # 由于紫卡植物需要移除以前的植物,所以可用另外定义一个函数
    def isAvailable(self, map_x: int, map_y: int, plant_name: str) -> bool:
        # 咖啡豆和墓碑吞噬者的判别最为特殊
        if plant_name == c.COFFEEBEAN:
            if self.map[map_y][map_x][c.MAP_SLEEP] and (
                plant_name not in self.map[map_y][map_x][c.MAP_PLANT]
            ):
                return True
            else:
                return False
        if plant_name == c.GRAVEBUSTER:
            if c.GRAVE in self.map[map_y][map_x][c.MAP_PLANT] and (
                plant_name not in self.map[map_y][map_x][c.MAP_PLANT]
            ):
                return True
            else:
                return False
        # 被非植物障碍占据的格子对于一般植物不可种植
        if any(
            (i in c.NON_PLANT_OBJECTS)
            for i in self.map[map_y][map_x][c.MAP_PLANT]
        ):
            return False
        if self.map[map_y][map_x][c.MAP_PLOT_TYPE] == c.MAP_GRASS:  # 草地
            # 首先需要判断植物是否是水生植物,水生植物不能种植在陆地上
            if plant_name not in c.WATER_PLANTS:
                if not self.map[map_y][map_x][c.MAP_PLANT]:   # 没有植物肯定可以种植
                    return True
                elif all(
                    (i in {'花盆(未实现)', c.PUMPKINHEAD})
                    for i in self.map[map_y][map_x][c.MAP_PLANT]
                ) and (
                    plant_name not in self.map[map_y][map_x][c.MAP_PLANT]
                ):   # 例外植物:集合中填花盆和南瓜头,只要这里没有这种植物就能种植
                    return True
                elif (plant_name == c.PUMPKINHEAD) and (
                    c.PUMPKINHEAD not in self.map[map_y][map_x][c.MAP_PLANT]
                ):   # 没有南瓜头就能种南瓜头
                    return True
                else:
                    return False
            else:
                return False
        elif self.map[map_y][map_x][c.MAP_PLOT_TYPE] == c.MAP_TILE:   # 屋顶
            # 首先需要判断植物是否是水生植物,水生植物不能种植在陆地上
            if plant_name not in c.WATER_PLANTS:
                if '花盆(未实现)' in self.map[map_y][map_x][c.MAP_PLANT]:
                    if all(
                        (i in {'花盆(未实现)', c.PUMPKINHEAD})
                        for i in self.map[map_y][map_x][c.MAP_PLANT]
                    ) and (
                        plant_name not in self.map[map_y][map_x][c.MAP_PLANT]
                    ):   # 例外植物:集合中填花盆和南瓜头,只要这里没有这种植物就能种植
                        if plant_name in {c.SPIKEWEED}:   # 不能在花盆上种植的植物
                            return False
                        else:
                            return True
                    elif (plant_name == c.PUMPKINHEAD) and (
                        c.PUMPKINHEAD
                        not in self.map[map_y][map_x][c.MAP_PLANT]
                    ):    # 有花盆且没有南瓜头就能种南瓜头
                        return True
                    else:
                        return False
                elif plant_name == '花盆(未实现)':   # 这一格本来没有花盆而且新来的植物是花盆,可以种
                    return True
                else:
                    return False
            else:
                return False
        elif self.map[map_y][map_x][c.MAP_PLOT_TYPE] == c.MAP_WATER:   # 水里
            if plant_name in c.WATER_PLANTS:   # 是水生植物
                if not self.map[map_y][map_x][
                    c.MAP_PLANT
                ]:   # 只有无植物时才能在水里种植水生植物
                    return True
                else:
                    return False
            else:   # 非水生植物,依赖睡莲
                if c.LILYPAD in self.map[map_y][map_x][c.MAP_PLANT]:
                    if all(
                        (i in {c.LILYPAD, c.PUMPKINHEAD})
                        for i in self.map[map_y][map_x][c.MAP_PLANT]
                    ) and (
                        plant_name not in self.map[map_y][map_x][c.MAP_PLANT]
                    ):
                        if plant_name in {
                            c.SPIKEWEED,
                            c.POTATOMINE,
                            '花盆(未实现)',
                        }:   # 不能在睡莲上种植的植物
                            return False
                        else:
                            return True
                    elif (plant_name == c.PUMPKINHEAD) and (
                        c.PUMPKINHEAD
                        not in self.map[map_y][map_x][c.MAP_PLANT]
                    ):   # 在睡莲上且没有南瓜头就能种南瓜头
                        return True
                    else:
                        return False
                else:
                    return False
        else:   # 不可种植区域
            return False

    def getMapIndex(self, x: int, y: int) -> tuple[int, int]:
        if self.background_type in c.POOL_EQUIPPED_BACKGROUNDS:
            x -= c.MAP_POOL_OFFSET_X
            y -= c.MAP_POOL_OFFSET_Y
            return (x // c.GRID_POOL_X_SIZE, y // c.GRID_POOL_Y_SIZE)
        elif self.background_type in c.ON_ROOF_BACKGROUNDS:
            x -= c.MAP_ROOF_OFFSET_X
            y -= c.MAP_ROOF_OFFSET_X
            grid_x = x // c.GRID_ROOF_X_SIZE
            if grid_x >= 5:
                grid_y = y // c.GRID_ROOF_Y_SIZE
            else:
                grid_y = (y - 20 * (6 - grid_x)) // 85
            return (grid_x, grid_y)
        else:
            x -= c.MAP_OFFSET_X
            y -= c.MAP_OFFSET_Y
            return (x // c.GRID_X_SIZE, y // c.GRID_Y_SIZE)

    def getMapGridPos(self, map_x: int, map_y: int) -> tuple[int, int]:
        if self.background_type in c.POOL_EQUIPPED_BACKGROUNDS:
            return (
                map_x * c.GRID_POOL_X_SIZE
                + c.GRID_POOL_X_SIZE // 2
                + c.MAP_POOL_OFFSET_X,
                map_y * c.GRID_POOL_Y_SIZE
                + c.GRID_POOL_Y_SIZE // 5 * 3
                + c.MAP_POOL_OFFSET_Y,
            )
        elif self.background_type in c.ON_ROOF_BACKGROUNDS:
            return (
                map_x * c.GRID_ROOF_X_SIZE
                + c.GRID_ROOF_X_SIZE // 2
                + c.MAP_ROOF_OFFSET_X,
                map_y * c.GRID_ROOF_Y_SIZE
                + 20 * max(0, (6 - map_y))
                + c.GRID_ROOF_Y_SIZE // 5 * 3
                + c.MAP_POOL_OFFSET_Y,
            )
        else:
            return (
                map_x * c.GRID_X_SIZE + c.GRID_X_SIZE // 2 + c.MAP_OFFSET_X,
                map_y * c.GRID_Y_SIZE
                + c.GRID_Y_SIZE // 5 * 3
                + c.MAP_OFFSET_Y,
            )

    def setMapGridType(self, map_x: int, map_y: int, plot_type: str):
        self.map[map_y][map_x][c.MAP_PLOT_TYPE] = plot_type

    def addMapPlant(
        self, map_x: int, map_y: int, plant_name: int, sleep: bool = False
    ):
        self.map[map_y][map_x][c.MAP_PLANT].add(plant_name)
        self.map[map_y][map_x][c.MAP_SLEEP] = sleep

    def removeMapPlant(self, map_x: int, map_y: int, plant_name: str):
        self.map[map_y][map_x][c.MAP_PLANT].discard(plant_name)

    def getRandomMapIndex(self) -> tuple[int, int]:
        map_x = random.randint(0, self.width - 1)
        map_y = random.randint(0, self.height - 1)
        return (map_x, map_y)

    def checkPlantToSeed(
        self, x: int, y: int, plant_name: str
    ) -> tuple[int, int]:
        pos = None
        map_x, map_y = self.getMapIndex(x, y)
        if self.isValid(map_x, map_y) and self.isAvailable(
            map_x, map_y, plant_name
        ):
            pos = self.getMapGridPos(map_x, map_y)
        return pos


# 保存具体关卡地图信息常数
# 冒险模式地图
LEVEL_MAP_DATA = (
    # 第0关:测试模式地图
    {
        c.BACKGROUND_TYPE: 2,
        c.GAME_TITLE: '隐藏测试关卡',
        c.INIT_SUN_NAME: 5000,
        c.SHOVEL: 1,
        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_LIST,
        c.ZOMBIE_LIST: (
            {'time': 0, 'map_y': 5, 'name': 'Zomboni'},
            {'time': 1000, 'map_y': 4, 'name': 'ScreenDoorZombie'},
            {'time': 2000, 'map_y': 4, 'name': 'ScreenDoorZombie'},
            {'time': 3100, 'map_y': 4, 'name': 'ScreenDoorZombie'},
            {'time': 4500, 'map_y': 4, 'name': 'ScreenDoorZombie'},
            {'time': 5000, 'map_y': 4, 'name': 'ScreenDoorZombie'},
            {'time': 6000, 'map_y': 4, 'name': 'ScreenDoorZombie'},
            {'time': 7000, 'map_y': 4, 'name': 'ScreenDoorZombie'},
            {'time': 8000, 'map_y': 4, 'name': 'ScreenDoorZombie'},
            {'time': 0, 'map_y': 1, 'name': 'NewspaperZombie'},
            {'time': 0, 'map_y': 0, 'name': 'PoleVaultingZombie'},
            {'time': 6000, 'map_y': 0, 'name': 'FootballZombie'},
            {'time': 0, 'map_y': 3, 'name': 'ConeheadDuckyTubeZombie'},
            {'time': 0, 'map_y': 2, 'name': 'SnorkelZombie'},
            {'time': 90000, 'map_y': 2, 'name': 'ConeheadDuckyTubeZombie'},
        ),
    },
    # 第1关:单行草皮
    {
        c.BACKGROUND_TYPE: 7,
        c.GAME_TITLE: '白天 1-1',
        c.INIT_SUN_NAME: 150,
        c.SHOVEL: 1,
        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,
        c.INCLUDED_ZOMBIES: (c.NORMAL_ZOMBIE,),
        c.NUM_FLAGS: 1,
    },
    # 第2关:三行草皮
    {
        c.BACKGROUND_TYPE: 8,
        c.GAME_TITLE: '白天 1-2',
        c.INIT_SUN_NAME: 50,
        c.SHOVEL: 1,
        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,
        c.INCLUDED_ZOMBIES: (c.NORMAL_ZOMBIE,),
        c.NUM_FLAGS: 1,
    },
    # 第3关
    {
        c.BACKGROUND_TYPE: 0,
        c.GAME_TITLE: '白天 1-3',
        c.INIT_SUN_NAME: 50,
        c.SHOVEL: 1,
        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,
        c.INCLUDED_ZOMBIES: (c.NORMAL_ZOMBIE,),
        c.NUM_FLAGS: 2,
    },
    # 第4关
    {
        c.BACKGROUND_TYPE: 0,
        c.GAME_TITLE: '白天 1-4',
        c.INIT_SUN_NAME: 50,
        c.SHOVEL: 1,
        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,
        c.INCLUDED_ZOMBIES: (
            c.NORMAL_ZOMBIE,
            c.CONEHEAD_ZOMBIE,
            c.POLE_VAULTING_ZOMBIE,
        ),
        c.NUM_FLAGS: 2,
    },
    # 第5关 目前白天最后一关
    {
        c.BACKGROUND_TYPE: 0,
        c.GAME_TITLE: '白天 1-5',
        c.INIT_SUN_NAME: 50,
        c.SHOVEL: 1,
        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,
        c.INCLUDED_ZOMBIES: (
            c.NORMAL_ZOMBIE,
            c.CONEHEAD_ZOMBIE,
            c.POLE_VAULTING_ZOMBIE,
            c.BUCKETHEAD_ZOMBIE,
        ),
        c.NUM_FLAGS: 3,
    },
    # 第6关 目前夜晚第一关
    {
        c.BACKGROUND_TYPE: 1,
        c.GAME_TITLE: '黑夜 2-1',
        c.INIT_SUN_NAME: 50,
        c.SHOVEL: 1,
        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,
        c.INCLUDED_ZOMBIES: (c.NORMAL_ZOMBIE, c.NEWSPAPER_ZOMBIE),
        c.NUM_FLAGS: 2,
    },
    # 第7关
    {
        c.BACKGROUND_TYPE: 1,
        c.GAME_TITLE: '黑夜 2-2',
        c.INIT_SUN_NAME: 50,
        c.SHOVEL: 1,
        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,
        c.INCLUDED_ZOMBIES: (
            c.NORMAL_ZOMBIE,
            c.SCREEN_DOOR_ZOMBIE,
        ),
        c.NUM_FLAGS: 2,
        c.GRADE_GRAVES: 2,
    },
    # 第8关 目前为夜晚最后一关
    {
        c.BACKGROUND_TYPE: 1,
        c.GAME_TITLE: '黑夜 2-3',
        c.INIT_SUN_NAME: 50,
        c.SHOVEL: 1,
        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,
        c.INCLUDED_ZOMBIES: (
            c.NORMAL_ZOMBIE,
            c.NEWSPAPER_ZOMBIE,
            c.CONEHEAD_ZOMBIE,
            c.BUCKETHEAD_ZOMBIE,
            c.SCREEN_DOOR_ZOMBIE,
            c.FOOTBALL_ZOMBIE,
        ),
        c.INEVITABLE_ZOMBIE_DICT: {  # 这里改用python实现了以后,键不再用字符串,改用数字
            # 仍然要注意字典值是元组
            10: (c.NEWSPAPER_ZOMBIE,),
            20: (c.SCREEN_DOOR_ZOMBIE,),
            30: (c.FOOTBALL_ZOMBIE,),
        },
        c.NUM_FLAGS: 3,
        c.GRADE_GRAVES: 3,
    },
    # 第9关 目前为泳池模式第一关
    {
        c.BACKGROUND_TYPE: 2,
        c.GAME_TITLE: '泳池 3-1',
        c.INIT_SUN_NAME: 50,
        c.SHOVEL: 1,
        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,
        c.INCLUDED_ZOMBIES: (
            c.NORMAL_ZOMBIE,
            c.BUCKETHEAD_ZOMBIE,
            c.CONEHEAD_ZOMBIE,
        ),
        c.NUM_FLAGS: 2,
    },
    # 第10关
    {
        c.BACKGROUND_TYPE: 2,
        c.GAME_TITLE: '泳池 3-2',
        c.INIT_SUN_NAME: 50,
        c.SHOVEL: 1,
        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,
        c.INCLUDED_ZOMBIES: (
            c.NORMAL_ZOMBIE,
            c.BUCKETHEAD_ZOMBIE,
            c.CONEHEAD_ZOMBIE,
            c.SNORKELZOMBIE,
        ),
        c.INEVITABLE_ZOMBIE_DICT: {30: (c.SNORKELZOMBIE,)},
        c.NUM_FLAGS: 3,
    },
    # 第11关
    {
        c.BACKGROUND_TYPE: 2,
        c.GAME_TITLE: '泳池 3-3',
        c.INIT_SUN_NAME: 50,
        c.SHOVEL: 1,
        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,
        c.INCLUDED_ZOMBIES: (c.NORMAL_ZOMBIE, c.ZOMBONI),
        c.INEVITABLE_ZOMBIE_DICT: {30: (c.ZOMBONI,)},
        c.NUM_FLAGS: 3,
    },
    # 第12关 目前为泳池最后一关
    {
        c.BACKGROUND_TYPE: 2,
        c.GAME_TITLE: '泳池 3-4',
        c.INIT_SUN_NAME: 50,
        c.SHOVEL: 1,
        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,
        c.INCLUDED_ZOMBIES: (
            c.NORMAL_ZOMBIE,
            c.ZOMBONI,
            c.BUCKETHEAD_ZOMBIE,
            c.CONEHEAD_ZOMBIE,
            c.SNORKELZOMBIE,
        ),
        c.INEVITABLE_ZOMBIE_DICT: {40: (c.ZOMBONI,)},
        c.NUM_FLAGS: 4,
    },
    # 第13关 目前为浓雾第一关 尚未完善
    {
        c.BACKGROUND_TYPE: 3,
        c.GAME_TITLE: '浓雾 4-1',
        c.INIT_SUN_NAME: 50,
        c.SHOVEL: 1,
        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,
        c.INCLUDED_ZOMBIES: (
            c.NORMAL_ZOMBIE,
            c.NEWSPAPER_ZOMBIE,
            c.ZOMBONI,
            c.FOOTBALL_ZOMBIE,
            c.CONEHEAD_ZOMBIE,
            c.BUCKETHEAD_ZOMBIE,
        ),
        c.NUM_FLAGS: 4,
    },
)


# 玩玩小游戏地图
LITTLE_GAME_MAP_DATA = (
    # 第0关 测试
    {
        c.BACKGROUND_TYPE: 3,
        c.GAME_TITLE: '隐藏测试关卡',
        c.CHOOSEBAR_TYPE: c.CHOOSEBAR_MOVE,
        c.SHOVEL: 1,
        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,
        c.INCLUDED_ZOMBIES: (
            c.NORMAL_ZOMBIE,
            c.NEWSPAPER_ZOMBIE,
            c.ZOMBONI,
            c.FOOTBALL_ZOMBIE,
            c.CONEHEAD_ZOMBIE,
            c.BUCKETHEAD_ZOMBIE,
        ),
        c.NUM_FLAGS: 4,
        c.CARD_POOL: {
            c.LILYPAD: 300,
            c.STARFRUIT: 400,
            c.PUMPKINHEAD: 100,
            c.SEASHROOM: 100,
            c.SPIKEWEED: 100,
        },
    },
    # 第1关 坚果保龄球
    {
        c.BACKGROUND_TYPE: 6,
        c.GAME_TITLE: '坚果保龄球',
        c.CHOOSEBAR_TYPE: c.CHOOSEBAR_BOWLING,
        c.SHOVEL: 0,
        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,
        c.INCLUDED_ZOMBIES: (
            c.NORMAL_ZOMBIE,
            c.CONEHEAD_ZOMBIE,
            c.POLE_VAULTING_ZOMBIE,
            c.BUCKETHEAD_ZOMBIE,
        ),
        c.NUM_FLAGS: 2,
        c.CARD_POOL: {
            c.WALLNUTBOWLING: 300,
            c.REDWALLNUTBOWLING: 100,
        },
    },
    # 第2关 白天 大决战
    {
        c.BACKGROUND_TYPE: 0,
        c.GAME_TITLE: '大决战(白天)',
        c.CHOOSEBAR_TYPE: c.CHOOSEBAR_MOVE,
        c.SHOVEL: 1,
        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,
        c.INCLUDED_ZOMBIES: (
            c.NORMAL_ZOMBIE,
            c.CONEHEAD_ZOMBIE,
            c.POLE_VAULTING_ZOMBIE,
            c.BUCKETHEAD_ZOMBIE,
        ),
        c.NUM_FLAGS: 3,
        c.CARD_POOL: {
            c.PEASHOOTER: 200,
            c.SNOWPEASHOOTER: 100,
            c.WALLNUT: 100,
            c.CHERRYBOMB: 100,
            c.REPEATERPEA: 200,
            c.CHOMPER: 100,
            c.POTATOMINE: 100,
        },
    },
    # 第3关 夜晚 大决战
    {
        c.BACKGROUND_TYPE: 1,
        c.GAME_TITLE: '大决战(黑夜)',
        c.CHOOSEBAR_TYPE: c.CHOOSEBAR_MOVE,
        c.SHOVEL: 1,
        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,
        c.INCLUDED_ZOMBIES: (
            c.NORMAL_ZOMBIE,
            c.CONEHEAD_ZOMBIE,
            c.FOOTBALL_ZOMBIE,
            c.BUCKETHEAD_ZOMBIE,
            c.NEWSPAPER_ZOMBIE,
            c.SCREEN_DOOR_ZOMBIE,
        ),
        c.NUM_FLAGS: 3,
        c.CARD_POOL: {
            c.PUFFSHROOM: 100,
            c.SCAREDYSHROOM: 100,
            c.ICESHROOM: 70,
            c.HYPNOSHROOM: 100,
            c.DOOMSHROOM: 50,
            c.GRAVEBUSTER: 100,
            c.FUMESHROOM: 200,
        },
        c.GRADE_GRAVES: 3,
    },
    # 第4关 泳池 大决战
    {
        c.BACKGROUND_TYPE: 2,
        c.GAME_TITLE: '大决战(泳池)',
        c.CHOOSEBAR_TYPE: c.CHOOSEBAR_MOVE,
        c.SHOVEL: 1,
        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,
        c.INCLUDED_ZOMBIES: (
            c.NORMAL_ZOMBIE,
            c.CONEHEAD_ZOMBIE,
            c.SNORKELZOMBIE,
            c.BUCKETHEAD_ZOMBIE,
            c.ZOMBONI,
        ),
        c.NUM_FLAGS: 4,
        c.CARD_POOL: {
            c.LILYPAD: 300,
            c.TORCHWOOD: 100,
            c.TALLNUT: 100,
            c.TANGLEKLEP: 100,
            c.SPIKEWEED: 100,
            c.SQUASH: 100,
            c.JALAPENO: 50,
            c.THREEPEASHOOTER: 400,
        },
    },
    # 第5关 坚果保龄球2
    {
        c.BACKGROUND_TYPE: 6,
        c.GAME_TITLE: '坚果保龄球(II)',
        c.CHOOSEBAR_TYPE: c.CHOOSEBAR_BOWLING,
        c.SHOVEL: 0,
        c.SPAWN_ZOMBIES: c.SPAWN_ZOMBIES_AUTO,
        c.INCLUDED_ZOMBIES: (
            c.NORMAL_ZOMBIE,
            c.CONEHEAD_ZOMBIE,
            c.POLE_VAULTING_ZOMBIE,
            c.BUCKETHEAD_ZOMBIE,
            c.NEWSPAPER_ZOMBIE,
            c.SCREEN_DOOR_ZOMBIE,
        ),
        c.NUM_FLAGS: 3,
        c.CARD_POOL: {
            c.WALLNUTBOWLING: 500,
            c.REDWALLNUTBOWLING: 100,
            c.GIANTWALLNUT: 100,
        },
    },
)

# 总关卡数
TOTAL_LEVEL = len(LEVEL_MAP_DATA)
TOTAL_LITTLE_GAME = len(LITTLE_GAME_MAP_DATA)


================================================
FILE: source/component/menubar.py
================================================
import random

import pygame as pg

from .. import constants as c
from .. import tool


def getSunValueImage(sun_value):
    # for pack, must include ttf
    font = pg.font.Font(c.FONT_PATH, 14)
    font.bold = True
    width = 35
    msg_image = font.render(str(sun_value), True, c.NAVYBLUE, c.LIGHTYELLOW)
    msg_rect = msg_image.get_rect()
    msg_w = msg_rect.width

    image = pg.Surface((width, 17))
    x = width - msg_w

    image.fill(c.LIGHTYELLOW)
    image.blit(msg_image, (x, 0), (0, 0, msg_rect.w, msg_rect.h))
    image.set_colorkey(c.BLACK)
    return image


def getCardPool(data):
    card_pool = {
        c.PLANT_CARD_INFO[c.PLANT_CARD_INDEX[card_name]]: data[card_name]
        for card_name in data
    }
    return card_pool


class Card:
    def __init__(
        self, x: int, y: int, index: int, scale: float = 0.5, not_recommend=0
    ):
        self.info = c.PLANT_CARD_INFO[index]
        self.loadFrame(self.info[c.CARD_INDEX], scale)
        self.rect = self.orig_image.get_rect()
        self.rect.x = x
        self.rect.y = y
        # 绘制植物阳光消耗大小
        font = pg.font.Font(c.FONT_PATH, 12)
        self.sun_cost_img = font.render(
            str(self.info[c.SUN_INDEX]), True, c.BLACK
        )
        self.sun_cost_img_rect = self.sun_cost_img.get_rect()
        sun_cost_img_x = 32 - self.sun_cost_img_rect.w
        self.orig_image.blit(
            self.sun_cost_img,
            (
                sun_cost_img_x,
                52,
                self.sun_cost_img_rect.w,
                self.sun_cost_img_rect.h,
            ),
        )

        self.index = index
        self.sun_cost = self.info[c.SUN_INDEX]
        self.frozen_time = self.info[c.FROZEN_TIME_INDEX]
        self.frozen_timer = -self.frozen_time
        self.refresh_timer = 0
        self.select = True
        self.clicked = False
        self.not_recommend = not_recommend
        if self.not_recommend:
            self.orig_image.set_alpha(128)
            self.image = pg.Surface((self.rect.w, self.rect.h))  # 黑底
            self.image.blit(
                self.orig_image, (0, 0), (0, 0, self.rect.w, self.rect.h)
            )
        else:
            self.image = self.orig_image
            self.image.set_alpha(255)

    def loadFrame(self, name, scale):
        frame = tool.GFX[name]
        rect = frame.get_rect()
        width, height = rect.w, rect.h

        self.orig_image = tool.get_image(
            frame, 0, 0, width, height, c.BLACK, scale
        )
        self.image = self.orig_image

    def checkMouseClick(self, mouse_pos):
        x, y = mouse_pos
        if (
            self.rect.x <= x <= self.rect.right
            and self.rect.y <= y <= self.rect.bottom
        ):
            return True
        return False

    def canClick(self, sun_value, current_time):
        if (
            self.sun_cost <= sun_value
            and (current_time - self.frozen_timer) > self.frozen_time
        ):
            return True
        return False

    def canSelect(self):
        return self.select

    def setSelect(self, can_select):
        self.select = can_select
        if can_select:
            if self.not_recommend % 2:
                self.orig_image.set_alpha(128)
                self.image = pg.Surface((self.rect.w, self.rect.h))  # 黑底
                self.image.blit(
                    self.orig_image, (0, 0), (0, 0, self.rect.w, self.rect.h)
                )
            else:
                self.image = self.orig_image
                self.image.set_alpha(255)
        else:
            self.orig_image.set_alpha(64)
            self.image = pg.Surface((self.rect.w, self.rect.h))  # 黑底
            self.image.blit(
                self.orig_image, (0, 0), (0, 0, self.rect.w, self.rect.h)
            )

    def setFrozenTime(self, current_time):
        self.frozen_timer = current_time

    def createShowImage(self, sun_value, current_time):
        # 有关是否满足冷却与阳光条件的图片形式
        time = current_time - self.frozen_timer
        if time < self.frozen_time:   # cool down status
            image = pg.Surface((self.rect.w, self.rect.h))  # 黑底
            frozen_image = self.orig_image
            frozen_image.set_alpha(128)
            frozen_height = (
                (self.frozen_time - time) / self.frozen_time
            ) * self.rect.h

            image.blit(
                frozen_image, (0, 0), (0, 0, self.rect.w, frozen_height)
            )
            self.orig_image.set_alpha(192)
            image.blit(
                self.orig_image,
                (0, frozen_height),
                (0, frozen_height, self.rect.w, self.rect.h - frozen_height),
            )
        elif self.sun_cost > sun_value:   # disable status
            image = pg.Surface((self.rect.w, self.rect.h))  # 黑底
            self.orig_image.set_alpha(192)
            image.blit(
                self.orig_image, (0, 0), (0, 0, self.rect.w, self.rect.h)
            )
        elif self.clicked:
            image = pg.Surface((self.rect.w, self.rect.h))  # 黑底
            chosen_image = self.orig_image
            chosen_image.set_alpha(128)

            image.blit(chosen_image, (0, 0), (0, 0, self.rect.w, self.rect.h))
        else:
            image = self.orig_image
            image.set_alpha(255)
        return image

    def update(self, sun_value, current_time):
        if (current_time - self.refresh_timer) >= 250:
            self.image = self.createShowImage(sun_value, current_time)
            self.refresh_timer = current_time

    def draw(self, surface):
        surface.blit(self.image, self.rect)


# 植物栏
class MenuBar:
    def __init__(self, card_list, sun_value):
        self.loadFrame(c.MENUBAR_BACKGROUND)
        self.rect = self.image.get_rect()
        self.rect.x = 0
        self.rect.y = 0

        self.sun_value = sun_value
        self.card_offset_x = 26
        self.setupCards(card_list)

    def loadFrame(self, name):
        frame = tool.GFX[name]
        rect = frame.get_rect()
        frame_rect = (rect.x, rect.y, rect.w, rect.h)

        self.image = tool.get_image(tool.GFX[name], *frame_rect, c.WHITE, 1)

    def update(self, current_time):
        self.current_time = current_time
        for card in self.card_list:
            card.update(self.sun_value, self.current_time)

    def createImage(self, x, y, num):
        if num == 1:
            return
        img = self.image
        rect = self.image.get_rect()
        width = rect.w
        height = rect.h
        self.image = pg.Surface((width * num, height)).convert()
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        for i in range(num):
            x = i * width
            self.image.blit(img, (x, 0))
        self.image.set_colorkey(c.BLACK)

    def setupCards(self, card_list):
        self.card_list = []
        x = self.card_offset_x
        y = 8
        for index in card_list:
            x += c.BAR_CARD_X_INTERNAL
            self.card_list.append(Card(x, y, index))

    def checkCardClick(self, mouse_pos):
        result = None
        for card in self.card_list:
            if card.checkMouseClick(mouse_pos):
                if card.canClick(self.sun_value, self.current_time):
                    result = (
                        c.PLANT_CARD_INFO[card.index][c.PLANT_NAME_INDEX],
                        card,
                    )
                else:
                    # 播放无法使用该卡片的警告音
                    c.SOUND_CANNOT_CHOOSE_WARNING.play()
                break
        return result

    def checkMenuBarClick(self, mouse_pos):
        x, y = mouse_pos
        if (
            self.rect.x <= x <= self.rect.right
            and self.rect.y <= y <= self.rect.bottom
        ):
            return True
        return False

    def decreaseSunValue(self, value):
        self.sun_value -= value

    def increaseSunValue(self, value):
        self.sun_value += value
        if self.sun_value > 9990:
            self.sun_value = 9990

    def setCardFrozenTime(self, plant_name):
        for card in self.card_list:
            if c.PLANT_CARD_INFO[card.index][c.PLANT_NAME_INDEX] == plant_name:
                card.setFrozenTime(self.current_time)
                break

    def drawSunValue(self):
        self.value_image = getSunValueImage(self.sun_value)
        self.value_rect = self.value_image.get_rect()
        self.value_rect.x = 21
        self.value_rect.y = self.rect.bottom - 24

        self.image.blit(self.value_image, self.value_rect)

    def draw(self, surface):
        self.drawSunValue()
        surface.blit(self.image, self.rect)
        for card in self.card_list:
            card.draw(surface)


# 关卡模式选植物的界面
class Panel:
    def __init__(self, card_list, sun_value, background_type=c.BACKGROUND_DAY):
        self.loadImages(sun_value)
        self.selected_cards = []
        self.selected_num = 0
        self.background_type = background_type
        self.setupCards(card_list)

    def loadFrame(self, name):
        frame = tool.GFX[name]
        rect = frame.get_rect()
        frame_rect = (rect.x, rect.y, rect.w, rect.h)

        return tool.get_image(tool.GFX[name], *frame_rect, c.WHITE, 1)

    def loadImages(self, sun_value):
        self.menu_image = self.loadFrame(c.MENUBAR_BACKGROUND)
        self.menu_rect = self.menu_image.get_rect()
        self.menu_rect.x = 0
        self.menu_rect.y = 0

        self.panel_image = self.loadFrame(c.PANEL_BACKGROUND)
        self.panel_rect = self.panel_image.get_rect()
        self.panel_rect.x = 0
        self.panel_rect.y = c.PANEL_Y_START

        self.value_image = getSunValueImage(sun_value)
        self.value_rect = self.value_image.get_rect()
        self.value_rect.x = 21
        self.value_rect.y = self.menu_rect.bottom - 24

        self.button_image = self.loadFrame(c.START_BUTTON)
        self.button_rect = self.button_image.get_rect()
        self.button_rect.x = 155
        self.button_rect.y = 547

    def setupCards(self, card_list):
        self.card_list = []
        x = c.PANEL_X_START - c.PANEL_X_INTERNAL
        y = c.PANEL_Y_START + 38 - c.PANEL_Y_INTERNAL
        for i, index in enumerate(card_list):
            if i % 8 == 0:
                x = c.PANEL_X_START - c.PANEL_X_INTERNAL
                y += c.PANEL_Y_INTERNAL
            x += c.PANEL_X_INTERNAL
            plant_name = c.PLANT_CARD_INFO[index][c.PLANT_NAME_INDEX]
            if (
                plant_name in c.WATER_PLANTS
                and self.background_type not in c.POOL_EQUIPPED_BACKGROUNDS
            ):
                not_recommend = c.REASON_OTHER
            elif (
                plant_name == c.GRAVEBUSTER
                and self.background_type != c.BACKGROUND_NIGHT
            ):
                not_recommend = c.REASON_OTHER
            elif (
                plant_name in c.CAN_SLEEP_PLANTS
                and self.background_type in c.DAYTIME_BACKGROUNDS
            ):
                not_recommend = c.REASON_WILL_SLEEP
            elif (
                plant_name == c.COFFEEBEAN
                and self.background_type not in c.DAYTIME_BACKGROUNDS
            ):
                not_recommend = c.REASON_OTHER
            # 还有屋顶场景,以及其他植物没有实现的植物没有写进来
            else:
                not_recommend = 0
            self.card_list.append(Card(x, y, index, 0.5, not_recommend))

    def checkCardClick(self, mouse_pos):
        delete_card = None
        for card in self.selected_cards:
            if delete_card:   # when delete a card, move right cards to left
                card.rect.x -= c.BAR_CARD_X_INTERNAL
            elif card.checkMouseClick(mouse_pos):
                self.deleteCard(card.index)
                delete_card = card

        if delete_card:
            self.selected_cards.remove(delete_card)
            self.selected_num -= 1
            # 播放点击音效
            c.SOUND_TAPPING_CARD.play()
            if delete_card.info[c.PLANT_NAME_INDEX] == c.COFFEEBEAN:
                for i in self.card_list:
                    if i.not_recommend == c.REASON_SLEEP_BUT_COFFEE_BEAN:
                        i.not_recommend = c.REASON_WILL_SLEEP
                        i.orig_image.set_alpha(128)
                        i.image = pg.Surface((i.rect.w, i.rect.h))  # 黑底
                        i.image.blit(
                            i.orig_image, (0, 0), (0, 0, i.rect.w, i.rect.h)
                        )

        if self.selected_num >= c.CARD_MAX_NUM:
            return

        for card in self.card_list:
            if card.checkMouseClick(mouse_pos):
                if card.canSelect():
                    self.addCard(card)
                    # 播放点击音效
                    c.SOUND_TAPPING_CARD.play()
                    if card.info[c.PLANT_NAME_INDEX] == c.COFFEEBEAN:
                        for i in self.card_list:
                            if i.not_recommend == c.REASON_WILL_SLEEP:
                                i.not_recommend = (
                                    c.REASON_SLEEP_BUT_COFFEE_BEAN
                                )
                                i.image = i.orig_image
                                i.image.set_alpha(255)
                break

    def addCard(self, card: Card):
        card.setSelect(False)
        y = 8
        x = 77 + self.selected_num * c.BAR_CARD_X_INTERNAL
        self.selected_cards.append(Card(x, y, card.index))
        self.selected_num += 1

    def deleteCard(self, index):
        self.card_list[index].setSelect(True)

    def checkStartButtonClick(self, mouse_pos):
        if self.selected_num < c.CARD_LIST_NUM:
            return False

        x, y = mouse_pos
        if (
            self.button_rect.x <= x <= self.button_rect.right
            and self.button_rect.y <= y <= self.button_rect.bottom
        ):
            return True
        return False

    def getSelectedCards(self):
        card_index_list = []
        for card in self.selected_cards:
            card_index_list.append(card.index)
        return card_index_list

    def draw(self, surface):
        self.menu_image.blit(self.value_image, self.value_rect)
        surface.blit(self.menu_image, self.menu_rect)
        surface.blit(self.panel_image, self.panel_rect)
        for card in self.card_list:
            card.draw(surface)
        for card in self.selected_cards:
            card.draw(surface)

        if self.selected_num >= c.CARD_LIST_NUM:
            surface.blit(self.button_image, self.button_rect)


# 传送带模式的卡片
class MoveCard:
    def __init__(self, x, y, card_name, plant_name, scale=0.5):
        self.loadFrame(card_name, scale)
        self.rect = self.orig_image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.rect.w = 1
        self.clicked = False
        self.image = self.createShowImage()

        self.card_name = card_name
        self.plant_name = plant_name
        self.move_timer = 0
        self.select = True

    def loadFrame(self, name, scale):
        frame = tool.GFX[name]
        rect = frame.get_rect()
        width, height = rect.w, rect.h

        self.orig_image = tool.get_image(
            frame, 0, 0, width, height, c.BLACK, scale
        )
        self.orig_rect = self.orig_image.get_rect()
        self.image = self.orig_image

    def checkMouseClick(self, mouse_pos):
        x, y = mouse_pos
        if (
            self.rect.x <= x <= self.rect.right
            and self.rect.y <= y <= self.rect.bottom
        ):
            return True
        return False

    def createShowImage(self):
        # 新增卡片时显示图片
        if self.rect.w < self.orig_rect.w:   # create a part card image
            image = pg.Surface([self.rect.w, self.rect.h])
            if self.clicked:
                self.orig_image.set_alpha(128)
            else:
                self.orig_image.set_alpha(255)
            image.blit(
                self.orig_image, (0, 0), (0, 0, self.rect.w, self.rect.h)
            )
            self.rect.w += 1
        else:
            if self.clicked:
                image = pg.Surface([self.rect.w, self.rect.h])  # 黑底
                self.orig_image.set_alpha(128)

                image.blit(
                    self.orig_image, (0, 0), (0, 0, self.rect.w, self.rect.h)
                )
            else:
                self.orig_image.set_alpha(255)
                image = self.orig_image
        return image

    def update(self, left_x, current_time):
        if self.move_timer == 0:
            self.move_timer = current_time
        elif (current_time - self.move_timer) >= c.CARD_MOVE_TIME:
            if self.rect.x > left_x:
                self.rect.x -= 1
            self.image = self.createShowImage()
            self.move_timer += c.CARD_MOVE_TIME

    def draw(self, surface):
        surface.blit(self.image, self.rect)


# 传送带
class MoveBar:
    def __init__(self, card_pool):
        self.loadFrame(c.MOVEBAR_BACKGROUND)
        self.rect = self.image.get_rect()
        self.rect.x = 20
        self.rect.y = 0

        self.card_start_x = self.rect.x + 8
        self.card_end_x = self.rect.right - 5
        self.card_pool = card_pool
        self.card_pool_name = tuple(self.card_pool.keys())
        self.card_pool_weight = tuple(self.card_pool.values())
        self.card_list = []
        self.create_timer = -c.MOVEBAR_CARD_FRESH_TIME

    def loadFrame(self, name):
        frame = tool.GFX[name]
        rect = frame.get_rect()
        frame_rect = (rect.x, rect.y, rect.w, rect.h)

        self.image = tool.get_image(tool.GFX[name], *frame_rect, c.WHITE, 1)

    def createCard(self):
        if (
            len(self.card_list) > 0
            and self.card_list[-1].rect.right > self.card_end_x
        ):
            return False
        x = self.card_end_x
        y = 6
        selected_card = random.choices(
            self.card_pool_name, self.card_pool_weight
        )[0]
        self.card_list.append(
            MoveCard(
                x,
                y,
                selected_card[c.CARD_INDEX],
                selected_card[c.PLANT_NAME_INDEX],
            )
        )
        return True

    def update(self, current_time):
        self.current_time = current_time
        left_x = self.card_start_x
        for card in self.card_list:
            card.update(left_x, self.current_time)
            left_x = card.rect.right + 1

        if (self.current_time - self.create_timer) > c.MOVEBAR_CARD_FRESH_TIME:
            if self.createCard():
                self.create_timer = self.current_time

    def checkCardClick(self, mouse_pos):
        result = None
        for index, card in enumerate(self.card_list):
            if card.checkMouseClick(mouse_pos):
                result = (card.plant_name, card)
                break
        return result

    def checkMenuBarClick(self, mouse_pos):
        x, y = mouse_pos
        if (
            self.rect.x <= x <= self.rect.right
            and self.rect.y <= y <= self.rect.bottom
        ):
            return True
        return False

    def deleateCard(self, card):
        self.card_list.remove(card)

    def draw(self, surface):
        surface.blit(self.image, self.rect)
        for card in self.card_list:
            card.draw(surface)


================================================
FILE: source/component/plant.py
================================================
import random

import pygame as pg

from .. import constants as c
from .. import tool


class Car(pg.sprite.Sprite):
    def __init__(self, x: int, y: int, map_y: int):
        pg.sprite.Sprite.__init__(self)

        rect = tool.GFX[c.CAR].get_rect()
        width, height = rect.w, rect.h
        self.image = tool.get_image(tool.GFX[c.CAR], 0, 0, width, height)
        self.mask = pg.mask.from_surface(self.image)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.bottom = y
        self.map_y = map_y
        self.state = c.IDLE
        self.dead = False

    def update(self, game_info: dict):
        self.current_time = game_info[c.CURRENT_TIME]
        if self.state == c.WALK:
            self.rect.x += 5
        if self.rect.x > c.SCREEN_WIDTH + 25:
            self.dead = True

    def setWalk(self):
        if self.state == c.IDLE:
            self.state = c.WALK
            # 播放音效
            c.SOUND_CAR_WALKING.play()

    def draw(self, surface):
        surface.blit(self.image, self.rect)


# 豌豆及孢子类普通子弹
class Bullet(pg.sprite.Sprite):
    def __init__(
        self,
        x: int,
        start_y: int,
        dest_y: int,
        name: str,
        damage: int,
        effect: str = None,
        passed_torchwood_x: int = None,
        damage_type: str = c.ZOMBIE_DEAFULT_DAMAGE,
    ):
        pg.sprite.Sprite.__init__(self)

        self.name = name
        self.frames = []
        self.frame_index = 0
        self.load_images()
        self.frame_num = len(self.frames)
        self.image = self.frames[self.frame_index]
        self.mask = pg.mask.from_surface(self.image)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = start_y
        self.dest_y = dest_y
        self.y_vel = 15 if (dest_y > start_y) else -15
        self.x_vel = 10
        self.damage = damage
        self.damage_type = damage_type
        self.effect = effect
        self.state = c.FLY
        self.current_time = 0
        self.animate_timer = 0
        self.animate_interval = 70
        self.passed_torchwood_x = (
            passed_torchwood_x  # 记录最近通过的火炬树横坐标,如果没有缺省为None
        )

    def loadFrames(self, frames, name):
        frame_list = tool.GFX[name]
        if name in c.PLANT_RECT:
            data = c.PLANT_RECT[name]
            x, y, width, height = (
                data['x'],
                data['y'],
                data['width'],
                data['height'],
            )
        else:
            x, y = 0, 0
            rect = frame_list[0].get_rect()
            width, height = rect.w, rect.h

        for frame in frame_list:
            frames.append(tool.get_image(frame, x, y, width, height))

    def load_images(self):
        self.fly_frames = []
        self.explode_frames = []

        fly_name = self.name
        if self.name in c.BULLET_INDEPENDENT_BOOM_IMG:
            explode_name = f'{self.name}Explode'
        else:
            explode_name = 'PeaNormalExplode'

        self.loadFrames(self.fly_frames, fly_name)
        self.loadFrames(self.explode_frames, explode_name)

        self.frames = self.fly_frames

    def update(self, game_info):
        self.current_time = game_info[c.CURRENT_TIME]
        if self.state == c.FLY:
            if self.rect.y != self.dest_y:
                self.rect.y += self.y_vel
                if self.y_vel * (self.dest_y - self.rect.y) < 0:
                    self.rect.y = self.dest_y
            self.rect.x += self.x_vel
            if self.rect.x >= c.SCREEN_WIDTH + 20:
                self.kill()
        elif self.state == c.EXPLODE:
            if (self.current_time - self.explode_timer) > 250:
                self.kill()
        if self.current_time - self.animate_timer >= self.animate_interval:
            self.frame_index += 1
            self.animate_timer = self.current_time
            if self.frame_index >= self.frame_num:
                self.frame_index = 0
            self.image = self.frames[self.frame_index]

    def setExplode(self):
        self.state = c.EXPLODE
        self.explode_timer = self.current_time
        self.frames = self.explode_frames
        self.frame_num = len(self.frames)
        self.image = self.frames[0]
        self.mask = pg.mask.from_surface(self.image)

        # 播放子弹爆炸音效
        if self.name == c.BULLET_FIREBALL:
            c.SOUND_FIREPEA_EXPLODE.play()
        else:
            c.SOUND_BULLET_EXPLODE.play()

    def draw(self, surface):
        surface.blit(self.image, self.rect)


# 大喷菇的烟雾
# 仅有动画效果,不参与攻击运算
class Fume(pg.sprite.Sprite):
    def __init__(self, x, y):
        pg.sprite.Sprite.__init__(self)
        self.name = c.FUME
        self.timer = 0
        self.frame_index = 0
        self.load_images()
        self.frame_num = len(self.frames)
        self.image = self.frames[self.frame_index]
        self.mask = pg.mask.from_surface(self.image)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y

    def load_images(self):
        self.fly_frames = []

        fly_name = self.name

        self.loadFrames(self.fly_frames, fly_name)

        self.frames = self.fly_frames

    def draw(self, surface):
        surface.blit(self.image, self.rect)

    def update(self, game_info):
        self.current_time = game_info[c.CURRENT_TIME]
        if self.current_time - self.timer >= 100:
            self.frame_index += 1
            if self.frame_index >= self.frame_num:
                self.frame_index = self.frame_num - 1
                self.kill()
            self.timer = self.current_time
        self.image = self.frames[self.frame_index]

    def loadFrames(self, frames, name):
        frame_list = tool.GFX[name]
        x, y = 0, 0
        rect = frame_list[0].get_rect()
        width, height = rect.w, rect.h

        for frame in frame_list:
            frames.append(tool.get_image(frame, x, y, width, height))


# 杨桃的子弹
class StarBullet(Bullet):
    def __init__(
        self,
        x,
        start_y,
        damage,
        direction,
        level,
        damage_type=c.ZOMBIE_DEAFULT_DAMAGE,
    ):    # direction指星星飞行方向
        Bullet.__init__(
            self,
            x,
            start_y,
            start_y,
            c.BULLET_STAR,
            damage,
            damage_type=damage_type,
        )
        self.level = level
        self.map_y = self.level.map.getMapIndex(
            self.rect.x, self.rect.centery
        )[1]
        self.direction = direction

    def update(self, game_info):
        self.current_time = game_info[c.CURRENT_TIME]
        if self.state == c.FLY:
            if self.direction == c.STAR_FORWARD_UP:
                self.rect.x += 8
                self.rect.y -= 6
            elif self.direction == c.STAR_FORWARD_DOWN:
                self.rect.x += 7
                self.rect.y += 7
            elif self.direction == c.STAR_UPWARD:
                self.rect.y -= 10
            elif self.direction == c.STAR_DOWNWARD:
                self.rect.y += 10
            else:
                self.rect.x -= 10
            self.handleMapYPosition()
            if (
                (self.rect.x > c.SCREEN_WIDTH + 20)
                or (self.rect.right < -20)
                or (self.rect.y > c.SCREEN_HEIGHT)
                or (self.rect.y < 0)
            ):
                self.kill()
        elif self.state == c.EXPLODE:
            if (self.current_time - self.explode_timer) >= 250:
                self.kill()

    # 这里用的是坚果保龄球的代码改一下,实现子弹换行
    def handleMapYPosition(self):
        if self.direction == c.STAR_UPWARD:
            map_y1 = self.level.map.getMapIndex(
                self.rect.x, self.rect.centery + 40
            )[1]
        else:
            map_y1 = self.level.map.getMapIndex(
                self.rect.x, self.rect.centery + 20
            )[1]
        if (self.map_y != map_y1) and (
            0 <= map_y1 <= self.level.map_y_len - 1
        ):    # 换行
            self.level.bullet_groups[self.map_y].remove(self)
            self.level.bullet_groups[map_y1].add(self)
            self.map_y = map_y1


class Plant(pg.sprite.Sprite):
    def __init__(self, x, y, name, health, bullet_group, scale=1):
        pg.sprite.Sprite.__init__(self)

        self.frames = []
        self.frame_index = 0
        self.loadImages(name, scale)
        self.frame_num = len(self.frames)
        self.image = self.frames[self.frame_index]
        self.mask = pg.mask.from_surface(self.image)
        self.rect = self.image.get_rect()
        self.rect.centerx = x
        self.rect.bottom = y

        self.name = name
        self.health = health
        self.state = c.IDLE
        self.bullet_group = bullet_group
        self.animate_timer = 0
        self.animate_interval = 70  # 帧播放间隔
        self.hit_timer = 0
        # 被铲子指向时间
        self.highlight_time = 0

        self.attack_check = c.CHECK_ATTACK_ALWAYS

    def loadFrames(self, frames, name, scale=1, color=c.BLACK):
        frame_list = tool.GFX[name]
        if name in c.PLANT_RECT:
            data = c.PLANT_RECT[name]
            x, y, width, height = (
                data['x'],
                data['y'],
                data['width'],
                data['height'],
            )
        else:
            x, y = 0, 0
            rect = frame_list[0].get_rect()
            width, height = rect.w, rect.h

        for frame in frame_list:
            frames.append(
                tool.get_image(frame, x, y, width, height, color, scale)
            )

    def loadImages(self, name, scale):
        self.loadFrames(self.frames, name, scale)

    def changeFrames(self, frames):
        # change image frames and modify rect position
        self.frames = frames
        self.frame_num = len(self.frames)
        self.frame_index = 0

        bottom = self.rect.bottom
        x = self.rect.x
        self.image = self.frames[self.frame_index]
        self.mask = pg.mask.from_surface(self.image)
        self.rect = self.image.get_rect()
        self.rect.bottom = bottom
        self.rect.x = x

    def update(self, game_info):
        self.current_time = game_info[c.CURRENT_TIME]
        self.handleState()
        self.animation()

    def handleState(self):
        if self.state == c.IDLE:
            self.idling()
        elif self.state == c.ATTACK:
            self.attacking()
        elif self.state == c.DIGEST:
            self.digest()

    def idling(self):
        pass

    def attacking(self):
        pass

    def digest(self):
        pass

    def animation(self):
        if (self.current_time - self.animate_timer) > self.animate_interval:
            self.frame_index += 1
            if self.frame_index >= self.frame_num:
                self.frame_index = 0
            self.animate_timer = self.current_time

        self.image = self.frames[self.frame_index]
        self.mask = pg.mask.from_surface(self.image)
        if self.current_time - self.highlight_time < 100:
            self.image.set_alpha(150)
        elif (self.current_time - self.hit_timer) < 200:
            self.image.set_alpha(192)
        else:
            self.image.set_alpha(255)

    def canAttack(self, zombie):
        if (zombie.name == c.SNORKELZOMBIE) and (
            zombie.frames == zombie.swim_frames
        ):
            return False
        if (
            self.state != c.SLEEP
            and zombie.state != c.DIE
            and self.rect.x <= zombie.rect.right
            and zombie.rect.x <= c.SCREEN_WIDTH - 24
        ):
            return True
        return False

    def setAttack(self):
        self.state = c.ATTACK

    def setIdle(self):
        self.state = c.IDLE
        self.is_attacked = False

    def setSleep(self):
        self.state = c.SLEEP
        self.changeFrames(self.sleep_frames)

    def setDamage(self, damage, zombie):
        if not zombie.losthead:
            self.health -= damage
        self.hit_timer = self.current_time
        if (
            (self.name == c.HYPNOSHROOM)
            and (self.state != c.SLEEP)
            and (zombie.name not in {c.ZOMBONI, '投石车僵尸(未实现)', '加刚特尔(未实现)'})
        ):
            self.zombie_to_hypno = zombie

    def getPosition(self):
        return self.rect.centerx, self.rect.bottom


class Sun(Plant):
    def __init__(self, x, y, dest_x, dest_y, is_big=True):
        if is_big:
            scale = 0.9
            self.sun_value = c.SUN_VALUE
        else:
            scale = 0.6
            self.sun_value = 15
        Plant.__init__(self, x, y, c.SUN, 0, None, scale)
        self.move_speed = 1
        self.dest_x = dest_x
        self.dest_y = dest_y
        self.die_timer = 0

    def handleState(self):
        if self.rect.centerx != self.dest_x:
            self.rect.centerx += (
                self.move_speed
                if self.rect.centerx < self.dest_x
                else -self.move_speed
            )
        if self.rect.bottom != self.dest_y:
            self.rect.bottom += (
                self.move_speed
                if self.rect.bottom < self.dest_y
                else -self.move_speed
            )

        if (
            self.rect.centerx == self.dest_x
            and self.rect.bottom == self.dest_y
        ):
            if self.die_timer == 0:
                self.die_timer = self.current_time
            elif (self.current_time - self.die_timer) > c.SUN_LIVE_TIME:
                self.state = c.DIE
                self.kill()

    def checkCollision(self, x, y):
        if self.state == c.DIE:
            return False
        if (
            x >= self.rect.x
            and x <= self.rect.right
            and y >= self.rect.y
            and y <= self.rect.bottom
        ):
            self.state = c.DIE
            self.kill()
            return True
        return False


class SunFlower(Plant):
    def __init__(self, x, y, sun_group):
        Plant.__init__(self, x, y, c.SUNFLOWER, c.PLANT_HEALTH, None)
        self.sun_timer = 0
        self.sun_group = sun_group
        self.attack_check = c.CHECK_ATTACK_NEVER

    def idling(self):
        if self.sun_timer == 0:
            self.sun_timer = self.current_time - (c.FLOWER_SUN_INTERVAL - 6000)
        elif (self.current_time - self.sun_timer) > c.FLOWER_SUN_INTERVAL:
            self.sun_group.add(
                Sun(
                    self.rect.centerx,
                    self.rect.bottom,
                    self.rect.right,
                    self.rect.bottom + self.rect.h // 2,
                )
            )
            self.sun_timer = self.current_time


class PeaShooter(Plant):
    def __init__(self, x, y, bullet_group):
        Plant.__init__(self, x, y, c.PEASHOOTER, c.PLANT_HEALTH, bullet_group)
        self.shoot_timer = 0

    def attacking(self):
        if self.shoot_timer == 0:
            self.shoot_timer = self.current_time - 700
        elif (self.current_time - self.shoot_timer) >= 1400:
            self.bullet_group.add(
                Bullet(
                    self.rect.right - 15,
                    self.rect.y,
                    self.rect.y,
                    c.BULLET_PEA,
                    c.BULLET_DAMAGE_NORMAL,
                    effect=None,
                )
            )
            self.shoot_timer = self.current_time
            # 播放发射音效
            c.SOUND_SHOOT.play()

    def setAttack(self):
        self.state = c.ATTACK
        if self.shoot_timer != 0:
            self.shoot_timer = self.current_time - 700


class RepeaterPea(Plant):
    def __init__(self, x, y, bullet_group):
        Plant.__init__(self, x, y, c.REPEATERPEA, c.PLANT_HEALTH, bullet_group)
        self.shoot_timer = 0

        # 是否发射第一颗
        self.first_shot = False

    def attacking(self):
        if self.shoot_timer == 0:
            self.shoot_timer = self.current_time - 700
        elif self.current_time - self.shoot_timer >= 1400:
            self.first_shot = True
            self.bullet_group.add(
                Bullet(
                    self.rect.right - 15,
                    self.rect.y,
                    self.rect.y,
                    c.BULLET_PEA,
                    c.BULLET_DAMAGE_NORMAL,
                    effect=None,
                )
            )
            self.shoot_timer = self.current_time
            # 播放发射音效
            c.SOUND_SHOOT.play()
        elif self.first_shot and (self.current_time - self.shoot_timer) > 100:
            self.first_shot = False
            self.bullet_group.add(
                Bullet(
                    self.rect.right - 15,
                    self.rect.y,
                    self.rect.y,
                    c.BULLET_PEA,
                    c.BULLET_DAMAGE_NORMAL,
                    effect=None,
                )
            )
            # 播放发射音效
            c.SOUND_SHOOT.play()

    def setAttack(self):
        self.state = c.ATTACK
        if self.shoot_timer != 0:
            self.shoot_timer = self.current_time - 700


class ThreePeaShooter(Plant):
    def __init__(self, x, y, bullet_groups, map_y, background_type):
        Plant.__init__(self, x, y, c.THREEPEASHOOTER, c.PLANT_HEALTH, None)
        self.shoot_timer = 0
        self.map_y = map_y
        self.bullet_groups = bullet_groups
        self.background_type = background_type

    def attacking(self):
        if self.shoot_timer == 0:
            self.shoot_timer = self.current_time - 700
        if (self.current_time - self.shoot_timer) >= 1400:
            offset_y = 9  # modify bullet in the same y position with bullets of other plants
            for i in range(3):
                tmp_y = self.map_y + (i - 1)
                if self.background_type in c.POOL_EQUIPPED_BACKGROUNDS:
                    if tmp_y < 0 or tmp_y >= c.GRID_POOL_Y_LEN:
                        continue
                else:
                    if tmp_y < 0 or tmp_y >= c.GRID_Y_LEN:
                        continue
                if self.background_type in {
                    c.BACKGROUND_POOL,
                    c.BACKGROUND_FOG,
                    c.BACKGROUND_ROOF,
                    c.BACKGROUND_ROOFNIGHT,
                }:
                    dest_y = (
                        self.rect.y + (i - 1) * c.GRID_POOL_Y_SIZE + offset_y
                    )
                else:
                    dest_y = self.rect.y + (i - 1) * c.GRID_Y_SIZE + offset_y
                self.bullet_groups[tmp_y].add(
                    Bullet(
                        self.rect.right - 15,
                        self.rect.y,
                        dest_y,
                        c.BULLET_PEA,
                        c.BULLET_DAMAGE_NORMAL,
                        effect=None,
                    )
                )
            self.shoot_timer = self.current_time
            # 播放发射音效
            c.SOUND_SHOOT.play()

    def setAttack(self):
        self.state = c.ATTACK
        if self.shoot_timer != 0:
            self.shoot_timer = self.current_time - 700


class SnowPeaShooter(Plant):
    def __init__(self, x, y, bullet_group):
        Plant.__init__(
            self, x, y, c.SNOWPEASHOOTER, c.PLANT_HEALTH, bullet_group
        )
        self.shoot_timer = 0

    def attacking(self):
        if self.shoot_timer == 0:
            self.shoot_timer = self.current_time - 700
        elif (self.current_time - self.shoot_timer) >= 1400:
            self.bullet_group.add(
                Bullet(
                    self.rect.right - 15,
                    self.rect.y,
                    self.rect.y,
                    c.BULLET_PEA_ICE,
                    c.BULLET_DAMAGE_NORMAL,
                    effect=c.BULLET_EFFECT_ICE,
                )
            )
            self.shoot_timer = self.current_time
            # 播放发射音效
            c.SOUND_SHOOT.play()
            # 播放冰子弹音效
            c.SOUND_SNOWPEA_SPARKLES.play()

    def setAttack(self):
        self.state = c.ATTACK
        if self.shoot_timer != 0:
            self.shoot_timer = self.current_time - 700


class WallNut(Plant):
    def __init__(self, x, y):
        Plant.__init__(self, x, y, c.WALLNUT, c.WALLNUT_HEALTH, None)
        self.load_images()
        self.cracked1 = False
        self.cracked2 = False
        self.attack_check = c.CHECK_ATTACK_NEVER

    def load_images(self):
        self.cracked1_frames = []
        self.cracked2_frames = []

        cracked1_frames_name = self.name + '_cracked1'
        cracked2_frames_name = self.name + '_cracked2'

        self.loadFrames(self.cracked1_frames, cracked1_frames_name)
        self.loadFrames(self.cracked2_frames, cracked2_frames_name)

    def idling(self):
        if (not self.cracked1) and self.health <= c.WALLNUT_CRACKED1_HEALTH:
            self.changeFrames(self.cracked1_frames)
            self.cracked1 = True
        elif (not self.cracked2) and self.health <= c.WALLNUT_CRACKED2_HEALTH:
            self.changeFrames(self.cracked2_frames)
            self.cracked2 = True


class CherryBomb(Plant):
    def __init__(self, x, y):
        Plant.__init__(self, x, y, c.CHERRYBOMB, c.INF, None)
        self.state = c.ATTACK
        self.start_boom = False
        self.boomed = False
        self.bomb_timer = 0
        self.explode_y_range = 1
        self.explode_x_range = c.GRID_X_SIZE * 1.5

    def setBoom(self):
        frame = tool.GFX[c.BOOM_IMAGE]
        rect = frame.get_rect()
        width, height = rect.w, rect.h

        old_rect = self.rect
        image = tool.get_image(frame, 0, 0, width, height, c.BLACK, 1)
        self.image = image
        self.mask = pg.mask.from_surface(self.image)
        self.rect = image.get_rect()
        self.rect.centerx = old_rect.centerx
        self.rect.centery = old_rect.centery
        self.start_boom = True

    def animation(self):
        if self.start_boom:
            if self.bomb_timer == 0:
                self.bomb_timer = self.current_time
                # 播放爆炸音效
                c.SOUND_BOMB.play()
            elif (self.current_time - self.bomb_timer) > 500:
                self.health = 0
        else:
            if (self.current_time - self.animate_timer) > 100:
                self.frame_index += 1
                if self.frame_index >= self.frame_num:
                    self.setBoom()
                    return
                self.animate_timer = self.current_time

            self.image = self.frames[self.frame_index]
            self.mask = pg.mask.from_surface(self.image)

        if self.current_time - self.highlight_time < 100:
            self.image.set_alpha(150)
        elif (self.current_time - self.hit_timer) < 200:
            self.image.set_alpha(192)
        else:
            self.image.set_alpha(255)


class Chomper(Plant):
    def __init__(self, x, y):
        Plant.__init__(self, x, y, c.CHOMPER, c.PLANT_HEALTH, None)
        self.animate_interval = 140
        self.digest_timer = 0
        self.digest_interval = 15000
        self.attack_zombie = None
        self.zombie_group = None
        self.should_diggest = False

    def loadImages(self, name, scale):
        self.idle_frames = []
        self.attack_frames = []
        self.digest_frames = []
        self.animate_interval = 100   # 本身动画播放较慢

        idle_name = name
        attack_name = name + 'Attack'
        digest_name = name + 'Digest'

        frame_list = [self.idle_frames, self.attack_frames, self.digest_frames]
        name_list = [idle_name, attack_name, digest_name]
        scale_list = [1, 1, 1]
        # rect_list = [(0, 0, 100, 114), None, None]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name, scale_list[i])

        self.frames = self.idle_frames

    def canAttack(self, zombie):
        if (zombie.name in {c.POLE_VAULTING_ZOMBIE}) and (not zombie.jumped):
            return False
        if (zombie.name == c.SNORKELZOMBIE) and (
            zombie.frames == zombie.swim_frames
        ):
            return False
        elif (
            self.state == c.IDLE
            and zombie.state != c.DIGEST
            and self.rect.x <= zombie.rect.centerx
            and (not zombie.losthead)
            and (self.rect.x + c.GRID_X_SIZE * 2.7 >= zombie.rect.centerx)
        ):
            return True
        return False

    def setIdle(self):
        self.state = c.IDLE
        self.changeFrames(self.idle_frames)

    def setAttack(self, zombie, zombie_group):
        self.attack_zombie = zombie
        self.zombie_group = zombie_group
        self.state = c.ATTACK
        self.changeFrames(self.attack_frames)

    def setDigest(self):
        self.state = c.DIGEST
        self.changeFrames(self.digest_frames)

    def attacking(self):
        if self.frame_index == (self.frame_num - 3):
            # 对活着的僵尸才需要吞下去消化
            if self.attack_zombie.alive():
                if not self.should_diggest:
                    # 播放吞的音效 由于一帧在这个循环中执行了若干次,可能被设置播放若干次导致声音重叠,所以用if保护
                    # 在尚未检测到需要消化时播放音效
                    c.SOUND_BIGCHOMP.play()
                    self.should_diggest = True
                    self.attack_zombie.kill()
        if (self.frame_index + 1) == self.frame_num:
            if self.should_diggest:
                self.setDigest()
                self.should_diggest = False
            else:
                self.setIdle()

    def digest(self):
        if self.digest_timer == 0:
            self.digest_timer = self.current_time
        elif (self.current_time - self.digest_timer) > self.digest_interval:
            self.digest_timer = 0
            self.setIdle()


class PuffShroom(Plant):
    def __init__(self, x, y, bullet_group):
        Plant.__init__(self, x, y, c.PUFFSHROOM, c.PLANT_HEALTH, bullet_group)
        self.shoot_timer = 0

    def loadImages(self, name, scale):
        self.idle_frames = []
        self.sleep_frames = []

        idle_name = name
        sleep_name = name + 'Sleep'

        frame_list = [self.idle_frames, self.sleep_frames]
        name_list = [idle_name, sleep_name]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.idle_frames

    def attacking(self):
        if self.shoot_timer == 0:
            self.shoot_timer = self.current_time - 700
        elif (self.current_time - self.shoot_timer) >= 1400:
            self.bullet_group.add(
                Bullet(
                    self.rect.right,
                    self.rect.y + 10,
                    self.rect.y + 10,
                    c.BULLET_MUSHROOM,
                    c.BULLET_DAMAGE_NORMAL,
                    effect=None,
                )
            )
            self.shoot_timer = self.current_time
            # 播放音效
            c.SOUND_PUFF.play()

    def canAttack(self, zombie):
        if (zombie.name == c.SNORKELZOMBIE) and (
            zombie.frames == zombie.swim_frames
        ):
            return False
        if (
            self.rect.x <= zombie.rect.right
            and (self.rect.x + c.GRID_X_SIZE * 4 >= zombie.rect.x)
            and (zombie.rect.left <= c.SCREEN_WIDTH + 10)
        ):
            return True
        return False

    def setAttack(self):
        self.state = c.ATTACK
        if self.shoot_timer != 0:
            self.shoot_timer = self.current_time - 700


class PotatoMine(Plant):
    def __init__(self, x, y):
        Plant.__init__(self, x, y, c.POTATOMINE, c.PLANT_HEALTH, None)
        self.animate_interval = 300
        self.is_init = True
        self.init_timer = 0
        self.bomb_timer = 0
        self.explode_x_range = c.GRID_X_SIZE / 2
        self.start_boom = False
        self.boomed = False

    def loadImages(self, name, scale):
        self.init_frames = []
        self.idle_frames = []
        self.explode_frames = []

        init_name = name + 'Init'
        idle_name = name
        explode_name = name + 'Explode'

        frame_list = [self.init_frames, self.idle_frames, self.explode_frames]
        name_list = [init_name, idle_name, explode_name]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.init_frames

    def idling(self):
        if self.is_init:
            if self.init_timer == 0:
                self.init_timer = self.current_time
            elif (self.current_time - self.init_timer) > 15000:
                self.changeFrames(self.idle_frames)
                self.is_init = False

    def canAttack(self, zombie):    # 土豆雷不可能遇上潜水僵尸
        if zombie.name == c.POLE_VAULTING_ZOMBIE and (not zombie.jumped):
            return False
        # 这里碰撞应当比碰撞一般更容易,就设置成圆形或矩形模式,不宜采用mask
        elif (
            pg.sprite.collide_circle_ratio(0.7)(zombie, self)
            and (not self.is_init)
            and (not zombie.losthead)
        ):
            return True
        return False

    def attacking(self):
        if self.bomb_timer == 0:
            self.bomb_timer = self.current_time
            # 播放音效
            c.SOUND_POTATOMINE.play()
            self.changeFrames(self.explode_frames)
            self.start_boom = True
        elif (self.current_time - self.bomb_timer) > 500:
            self.health = 0


class Squash(Plant):
    def __init__(self, x, y, map_plant_set):
        Plant.__init__(self, x, y, c.SQUASH, c.PLANT_HEALTH, None)
        self.orig_pos = (x, y)
        self.aim_timer = 0
        self.start_boom = False   # 和灰烬等植物统一变量名,在这里表示倭瓜是否跳起
        self.map_plant_set = map_plant_set

    def loadImages(self, name, scale):
        self.idle_frames = []
        self.aim_frames = []
        self.attack_frames = []

        idle_name = name
        aim_name = name + 'Aim'
        attack_name = name + 'Attack'

        frame_list = [self.idle_frames, self.aim_frames, self.attack_frames]
        name_list = [idle_name, aim_name, attack_name]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.idle_frames

    def canAttack(self, zombie):
        # 普通状态
        if (
            self.state == c.IDLE
            and self.rect.x <= zombie.rect.right
            and (self.rect.right + c.GRID_X_SIZE >= zombie.rect.x)
        ):
            return True
        # 攻击状态
        elif self.state == c.ATTACK:
            if pg.sprite.collide_rect_ratio(0.5)(
                zombie, self
            ) or pg.sprite.collide_mask(zombie, self):
                return True
        return False

    def setAttack(self, zombie, zombie_group):
        self.attack_zombie = zombie
        self.zombie_group = zombie_group
        self.state = c.ATTACK
        # 攻击状态下生命值无敌
        self.health = c.INF

    def attacking(self):
        if self.start_boom:
            if (self.frame_index + 1) == self.frame_num:
                for zombie in self.zombie_group:
                    if self.canAttack(zombie):
                        zombie.setDamage(
                            1800, damage_type=c.ZOMBIE_RANGE_DAMAGE
                        )
                self.health = 0   # 避免僵尸在原位啃食
                self.map_plant_set.remove(c.SQUASH)
                self.kill()
                # 播放碾压音效
                c.SOUND_SQUASHING.play()
        elif self.aim_timer == 0:
            # 锁定目标时播放音效
            c.SOUND_SQUASH_HMM.play()
            self.aim_timer = self.current_time
            self.changeFrames(self.aim_frames)
        elif (self.current_time - self.aim_timer) > 1000:
            self.changeFrames(self.attack_frames)
            self.rect.centerx = self.attack_zombie.rect.centerx
            self.start_boom = True
            self.animate_interval = 300

    def getPosition(self):
        return self.orig_pos


class Spikeweed(Plant):
    def __init__(self, x, y):
        Plant.__init__(
            self, x, y, c.SPIKEWEED, c.PLANT_HEALTH, None, scale=0.9
        )
        self.animate_interval = 70
        self.attack_timer = 0

    def setIdle(self):
        self.animate_interval = 70
        self.state = c.IDLE

    def canAttack(self, zombie):
        # 地刺能不能扎的判据:
        # 僵尸中心与地刺中心的距离或僵尸包括了地刺中心和右端(平衡得到合理的攻击范围,"僵尸包括了地刺中心和右端"是为以后巨人做准备)
        # 暂时不能用碰撞判断,平衡性不好
        if (-40 <= zombie.rect.centerx - self.rect.centerx <= 40) or (
            zombie.rect.left <= self.rect.x <= zombie.rect.right
            and zombie.rect.left <= self.rect.right <= zombie.rect.right
        ):
            return True
        return False

    def setAttack(self, zombie_group):
        self.zombie_group = zombie_group
        self.animate_interval = 35
        self.state = c.ATTACK
        if self.hit_timer != 0:
            self.hit_timer = self.current_time - 500

    def attacking(self):
        if self.hit_timer == 0:
            self.hit_timer = self.current_time - 500
        elif (self.current_time - self.attack_timer) >= 700:
            self.attack_timer = self.current_time
            # 最后再来判断攻击是否要杀死自己
            killSelf = False
            for zombie in self.zombie_group:
                if self.canAttack(zombie):
                    # 有车的僵尸
                    if zombie.name in {c.ZOMBONI}:
                        zombie.health = zombie.losthead_health
                        killSelf = True
                    else:
                        zombie.setDamage(
                            20, damage_type=c.ZOMBIE_COMMON_DAMAGE
                        )
            if killSelf:
                self.health = 0
            # 播放攻击音效,同子弹打击
            c.SOUND_BULLET_EXPLODE.play()


class Jalapeno(Plant):
    def __init__(self, x, y):
        Plant.__init__(self, x, y, c.JALAPENO, c.INF, None)
        self.orig_pos = (x, y)
        self.state = c.ATTACK
        self.start_boom = False
        self.boomed = False
        self.explode_y_range = 0
        self.explode_x_range = 500

    def loadImages(self, name, scale):
        self.explode_frames = []
        explode_name = name + 'Explode'
        self.loadFrames(self.explode_frames, explode_name)

        self.loadFrames(self.frames, name)

    def setExplode(self):
        self.changeFrames(self.explode_frames)
        self.animate_timer = self.current_time
        self.rect.x = c.MAP_OFFSET_X
        self.start_boom = True

    def animation(self):
        if self.start_boom:
            if (self.current_time - self.animate_timer) > 100:
                if self.frame_index == 1:
                    # 播放爆炸音效
                    c.SOUND_BOMB.play()
                self.frame_index += 1
                if self.frame_index >= self.frame_num:
                    self.health = 0
                    return
                self.animate_timer = self.current_time
        else:
            if (self.current_time - self.animate_timer) > 100:
                self.frame_index += 1
                if self.frame_index >= self.frame_num:
                    self.setExplode()
                    return
                self.animate_timer = self.current_time
        self.image = self.frames[self.frame_index]
        self.mask = pg.mask.from_surface(self.image)

        if self.current_time - self.highlight_time < 100:
            self.image.set_alpha(150)
        elif (self.current_time - self.hit_timer) < 200:
            self.image.set_alpha(192)
        else:
            self.image.set_alpha(255)

    def getPosition(self):
        return self.orig_pos


class ScaredyShroom(Plant):
    def __init__(self, x, y, bullet_group):
        Plant.__init__(
            self, x, y, c.SCAREDYSHROOM, c.PLANT_HEALTH, bullet_group
        )
        self.shoot_timer = 0
        self.cry_x_range = c.GRID_X_SIZE * 1.5

    def loadImages(self, name, scale):
        self.idle_frames = []
        self.cry_frames = []
        self.sleep_frames = []

        idle_name = name
        cry_name = name + 'Cry'
        sleep_name = name + 'Sleep'

        frame_list = [self.idle_frames, self.cry_frames, self.sleep_frames]
        name_list = [idle_name, cry_name, sleep_name]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.idle_frames

    def needCry(self, zombie):
        if (
            zombie.state != c.DIE
            and abs(self.rect.x - zombie.rect.x) < self.cry_x_range
        ):
            return True
        return False

    def setCry(self):
        self.state = c.CRY
        self.changeFrames(self.cry_frames)

    def setAttack(self):
        self.state = c.ATTACK
        self.changeFrames(self.idle_frames)
        if self.shoot_timer != 0:
            self.shoot_timer = self.current_time - 700

    def setIdle(self):
        self.state = c.IDLE
        self.changeFrames(self.idle_frames)

    def attacking(self):
        if self.shoot_timer == 0:
            self.shoot_timer = self.current_time - 700
        elif (self.current_time - self.shoot_timer) >= 1400:
            self.bullet_group.add(
                Bullet(
                    self.rect.right - 15,
                    self.rect.y + 40,
                    self.rect.y + 40,
                    c.BULLET_MUSHROOM,
                    c.BULLET_DAMAGE_NORMAL,
                    effect=None,
                )
            )
            self.shoot_timer = self.current_time
            # 播放音效
            c.SOUND_PUFF.play()


class SunShroom(Plant):
    def __init__(self, x, y, sun_group):
        Plant.__init__(self, x, y, c.SUNSHROOM, c.PLANT_HEALTH, None)
        self.animate_interval = 140
        self.sun_timer = 0
        self.sun_group = sun_group
        self.is_big = False
        self.change_timer = 0

    def loadImages(self, name, scale):
        self.idle_frames = []
        self.big_frames = []
        self.sleep_frames = []

        idle_name = name
        big_name = name + 'Big'
        sleep_name = name + 'Sleep'

        frame_list = [self.idle_frames, self.big_frames, self.sleep_frames]
        name_list = [idle_name, big_name, sleep_name]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.idle_frames

    def idling(self):
        if not self.is_big:
            if self.change_timer == 0:
                self.change_timer = self.current_time
            elif (self.current_time - self.change_timer) > 100000:
                self.changeFrames(self.big_frames)
                self.is_big = True
                # 播放长大音效
                c.SOUND_PLANT_GROW.play()
        if self.sun_timer == 0:
            self.sun_timer = self.current_time - (c.FLOWER_SUN_INTERVAL - 6000)
        elif (self.current_time - self.sun_timer) > c.FLOWER_SUN_INTERVAL:
            self.sun_group.add(
                Sun(
                    self.rect.centerx,
                    self.rect.bottom,
                    self.rect.right,
                    self.rect.bottom + self.rect.h // 2,
                    self.is_big,
                )
            )
            self.sun_timer = self.current_time


class IceShroom(Plant):
    def __init__(self, x, y):
        Plant.__init__(self, x, y, c.ICESHROOM, c.PLANT_HEALTH, None)
        self.orig_pos = (x, y)
        self.start_boom = False
        self.boomed = False

    def loadImages(self, name, scale):
        self.idle_frames = []
        self.snow_frames = []
        self.sleep_frames = []
        self.trap_frames = []

        idle_name = name
        snow_name = name + 'Snow'
        sleep_name = name + 'Sleep'
        trap_name = name + 'Trap'

        frame_list = [
            self.idle_frames,
            self.snow_frames,
            self.sleep_frames,
            self.trap_frames,
        ]
        name_list = [idle_name, snow_name, sleep_name, trap_name]
        scale_list = [1, 1.5, 1, 1]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name, scale_list[i])

        self.frames = self.idle_frames

    def setFreeze(self):
        self.changeFrames(self.snow_frames)
        self.animate_timer = self.current_time
        self.rect.x = c.MAP_OFFSET_X
        self.rect.y = c.MAP_OFFSET_Y
        self.start_boom = True

    def animation(self):
        if self.start_boom:
            if (self.current_time - self.animate_timer) > 500:
                self.frame_index += 1
                if self.frame_index >= self.frame_num:
                    self.health = 0
                    return
                self.animate_timer = self.current_time
        else:
            if self.state != c.SLEEP:
                self.health = c.INF
            if (self.current_time - self.animate_timer) > 100:
                self.frame_index += 1
                if self.frame_index >= self.frame_num:
                    if self.state == c.SLEEP:
                        self.frame_index = 0
                    else:
                        self.setFreeze()
                        return
                self.animate_timer = self.current_time
        self.image = self.frames[self.frame_index]
        self.mask = pg.mask.from_surface(self.image)

        if self.current_time - self.highlight_time < 100:
            self.image.set_alpha(150)
        elif (self.current_time - self.hit_timer) < 200:
            self.image.set_alpha(192)
        else:
            self.image.set_alpha(255)

    def getPosition(self):
        return self.orig_pos


class HypnoShroom(Plant):
    def __init__(self, x, y):
        Plant.__init__(self, x, y, c.HYPNOSHROOM, c.PLANT_HEALTH, None)
        self.animate_interval = 80
        self.zombie_to_hypno = None
        self.attack_check = c.CHECK_ATTACK_NEVER

    def loadImages(self, name, scale):
        self.idle_frames = []
        self.sleep_frames = []

        idle_name = name
        sleep_name = name + 'Sleep'

        frame_list = [self.idle_frames, self.sleep_frames]
        name_list = [idle_name, sleep_name]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.idle_frames

    def idling(self):
        if self.health < c.PLANT_HEALTH and self.zombie_to_hypno:
            self.health = 0


class WallNutBowling(Plant):
    def __init__(self, x, y, map_y, level):
        Plant.__init__(self, x, y, c.WALLNUTBOWLING, 1, None)
        self.map_y = map_y
        self.level = level
        self.init_rect = self.rect.copy()
        self.rotate_degree = 0
        self.animate_interval = 200
        self.move_timer = 0
        self.move_interval = 70
        self.vel_x = random.randint(12, 15)
        self.vel_y = 0
        self.disable_hit_y = -1
        self.attack_check = c.CHECK_ATTACK_NEVER

    def loadImages(self, name, scale):
        self.loadFrames(self.frames, name, 1)

    def idling(self):
        if self.move_timer == 0:
            self.move_timer = self.current_time
        elif (self.current_time - self.move_timer) >= self.move_interval:
            self.rotate_degree = (self.rotate_degree - 30) % 360
            self.init_rect.x += self.vel_x
            self.init_rect.y += self.vel_y
            self.handleMapYPosition()
            if self.shouldChangeDirection():
                self.changeDirection(-1)
            if self.init_rect.x > c.SCREEN_WIDTH + 25:
                self.health = 0
            self.move_timer += self.move_interval

    def canHit(self, map_y):
        if self.disable_hit_y == map_y:
            return False
        return True

    def handleMapYPosition(self):
        map_y1 = self.level.map.getMapIndex(
            self.init_rect.x, self.init_rect.centery
        )[1]
        map_y2 = self.level.map.getMapIndex(
            self.init_rect.x, self.init_rect.bottom
        )[1]
        if self.map_y != map_y1 and map_y1 == map_y2:
            # wallnut bowls to another row, should modify which plant group it belongs to
            self.level.plant_groups[self.map_y].remove(self)
            self.level.plant_groups[map_y1].add(self)
            self.map_y = map_y1

    def shouldChangeDirection(self):
        if self.init_rect.centery <= c.MAP_OFFSET_Y:
            return True
        elif self.init_rect.bottom + 20 >= c.SCREEN_HEIGHT:
            return True
        return False

    def changeDirection(self, map_y):
        if self.vel_y == 0:
            if self.map_y == 0:
                self.vel_y = self.vel_x
            elif self.map_y == (c.GRID_Y_LEN - 1):  # 坚果保龄球显然没有泳池的6行情形
                self.vel_y = -self.vel_x
            else:
                if random.randint(0, 1):
                    self.vel_y = self.vel_x
                else:
                    self.vel_y = -self.vel_x
        else:
            self.vel_y = -self.vel_y

        self.disable_hit_y = map_y

    def animation(self):
        image = self.frames[self.frame_index]
        self.image = pg.transform.rotate(image, self.rotate_degree)
        self.mask = pg.mask.from_surface(self.image)
        # must keep the center postion of image when rotate
        self.rect = self.image.get_rect(center=self.init_rect.center)


class RedWallNutBowling(Plant):
    def __init__(self, x, y):
        Plant.__init__(self, x, y, c.REDWALLNUTBOWLING, 1, None)
        self.orig_y = y
        self.explode_timer = 0
        self.explode_y_range = 1
        self.explode_x_range = c.GRID_X_SIZE * 1.5
        self.init_rect = self.rect.copy()
        self.rotate_degree = 0
        self.animate_interval = 200
        self.move_timer = 0
        self.move_interval = 70
        self.vel_x = random.randint(12, 15)
        self.start_boom = False
        self.boomed = False

    def loadImages(self, name, scale):
        self.idle_frames = []
        self.loadFrames(self.idle_frames, name, 1)

        frame = tool.GFX[c.BOOM_IMAGE]
        rect = frame.get_rect()
        image = tool.get_image(frame, 0, 0, rect.w, rect.h)
        self.explode_frames = (image,)

        self.frames = self.idle_frames

    def idling(self):
        if self.move_timer == 0:
            self.move_timer = self.current_time
        elif (self.current_time - self.move_timer) >= self.move_interval:
            self.rotate_degree = (self.rotate_degree - 30) % 360
            self.init_rect.x += self.vel_x
            if self.init_rect.x > c.SCREEN_WIDTH + 25:
                self.health = 0
            self.move_timer += self.move_interval

    def attacking(self):
        if self.explode_timer == 0:
            self.start_boom = True
            self.explode_timer = self.current_time
            self.changeFrames(self.explode_frames)
            # 播放爆炸音效
            c.SOUND_BOMB.play()
        elif (self.current_time - self.explode_timer) > 500:
            self.health = 0

    def animation(self):
        if (self.current_time - self.animate_timer) > self.animate_interval:
            self.frame_index += 1
            if self.frame_index >= self.frame_num:
                self.frame_index = 0
            self.animate_timer = self.current_time

        image = self.frames[self.frame_index]
        if self.state == c.IDLE:
            self.image = pg.transform.rotate(image, self.rotate_degree)
        else:
            self.image = image
        self.mask = pg.mask.from_surface(self.image)
        # must keep the center postion of image when rotate
        self.rect = self.image.get_rect(center=self.init_rect.center)

    def getPosition(self):
        return (self.rect.centerx, self.orig_y)


class LilyPad(Plant):
    def __init__(self, x, y):
        Plant.__init__(self, x, y, c.LILYPAD, c.PLANT_HEALTH, None)
        self.attack_check = c.CHECK_ATTACK_NEVER


class TorchWood(Plant):
    def __init__(self, x, y, bullet_group):
        Plant.__init__(self, x, y, c.TORCHWOOD, c.PLANT_HEALTH, bullet_group)
        self.attack_check = c.CHECK_ATTACK_NEVER

    def idling(self):
        for i in self.bullet_group:
            if (
                i.name == c.BULLET_PEA
                and i.passed_torchwood_x != self.rect.centerx
                and abs(i.rect.centerx - self.rect.centerx) <= 20
            ):
                self.bullet_group.add(
                    Bullet(
                        i.rect.x,
                        i.rect.y,
                        i.dest_y,
                        c.BULLET_FIREBALL,
                        c.BULLET_DAMAGE_FIREBALL_BODY,
                        effect=c.BULLET_EFFECT_UNICE,
                        passed_torchwood_x=self.rect.centerx,
                    )
                )
                i.kill()
            elif (
                i.name == c.BULLET_PEA_ICE
                and i.passed_torchwood_x != self.rect.centerx
                and abs(i.rect.centerx - self.rect.centerx)
            ):
                self.bullet_group.add(
                    Bullet(
                        i.rect.x,
                        i.rect.y,
                        i.dest_y,
                        c.BULLET_PEA,
                        c.BULLET_DAMAGE_NORMAL,
                        effect=None,
                        passed_torchwood_x=self.rect.centerx,
                    )
                )
                i.kill()


class StarFruit(Plant):
    def __init__(self, x, y, bullet_group, level):
        Plant.__init__(self, x, y, c.STARFRUIT, c.PLANT_HEALTH, bullet_group)
        self.shoot_timer = 0
        self.level = level
        self.map_x, self.map_y = self.level.map.getMapIndex(x, y)

    def canAttack(self, zombie):
        if (zombie.name == c.SNORKELZOMBIE) and (
            zombie.frames == zombie.swim_frames
        ):
            return False
        if zombie.state != c.DIE:
            zombie_map_y = self.level.map.getMapIndex(
                zombie.rect.centerx, zombie.rect.bottom
            )[1]
            if (self.rect.x >= zombie.rect.x) and (
                self.map_y == zombie_map_y
            ):  # 对于同行且在杨桃后的僵尸
                return True
            # 斜向上,理想直线方程为:
            # f(zombie.rect.x) = -0.75*(zombie.rect.x - (self.rect.right - 5)) + self.rect.y - 10
            # 注意实际上为射线
            elif (
                -100
                <= (
                    zombie.rect.y
                    - (
                        -0.75 * (zombie.rect.x - (self.rect.right - 5))
                        + self.rect.y
                        - 10
                    )
                )
                <= 70
                and (zombie.rect.left <= c.SCREEN_WIDTH)
                and (zombie.rect.x >= self.rect.x)
            ):
                return True
            # 斜向下,理想直线方程为:f(zombie.rect.x) = zombie.rect.x + self.rect.y - self.rect.right - 15
            # 注意实际上为射线
            elif (
                abs(
                    zombie.rect.y
                    - (zombie.rect.x + self.rect.y - self.rect.right - 15)
                )
                <= 70
                and (zombie.rect.left <= c.SCREEN_WIDTH)
                and (zombie.rect.x >= self.rect.x)
            ):
                return True
            elif zombie.rect.left <= self.rect.x <= zombie.rect.right:
                return True
        return False

    def attacking(self):
        if self.shoot_timer == 0:
            self.shoot_timer = self.current_time - 700
        elif (self.current_time - self.shoot_timer) >= 1400:
            # pypvz特有设定:向后打的杨桃子弹无视铁门与报纸防具
            self.bullet_group.add(
                StarBullet(
                    self.rect.left - 10,
                    self.rect.y + 15,
                    c.BULLET_DAMAGE_NORMAL,
                    c.STAR_BACKWARD,
                    self.level,
                    damage_type=c.ZOMBIE_COMMON_DAMAGE,
                )
            )
            # 其他方向的杨桃子弹伤害效果与豌豆等同
            self.bullet_group.add(
                StarBullet(
                    self.rect.centerx - 20,
                    self.rect.bottom - self.rect.h - 15,
                    c.BULLET_DAMAGE_NORMAL,
                    c.STAR_UPWARD,
                    self.level,
                )
            )
            self.bullet_group.add(
                StarBullet(
                    self.rect.centerx - 20,
                    self.rect.bottom - 5,
                    c.BULLET_DAMAGE_NORMAL,
                    c.STAR_DOWNWARD,
                    self.level,
                )
            )
            self.bullet_group.add(
                StarBullet(
                    self.rect.right - 5,
                    self.rect.bottom - 20,
                    c.BULLET_DAMAGE_NORMAL,
                    c.STAR_FORWARD_DOWN,
                    self.level,
                )
            )
            self.bullet_group.add(
                StarBullet(
                    self.rect.right - 5,
                    self.rect.y - 10,
                    c.BULLET_DAMAGE_NORMAL,
                    c.STAR_FORWARD_UP,
                    self.level,
                )
            )
            self.shoot_timer = self.current_time
            # 播放发射音效
            c.SOUND_SHOOT.play()

    def setAttack(self):
        self.state = c.ATTACK
        if self.shoot_timer != 0:
            self.shoot_timer = self.current_time - 700


class CoffeeBean(Plant):
    def __init__(self, x, y, plant_group, map_content, map, map_x):
        Plant.__init__(self, x, y, c.COFFEEBEAN, c.PLANT_HEALTH, None)
        self.plant_group = plant_group
        self.map_content = map_content
        self.map = map
        self.map_x = map_x
        self.attack_check = c.CHECK_ATTACK_NEVER

    def animation(self):
        if (self.current_time - self.animate_timer) > self.animate_interval:
            self.frame_index += 1

            if self.frame_index >= self.frame_num:
                self.map_content[c.MAP_SLEEP] = False
                for plant in self.plant_group:
                    if plant.name in c.CAN_SLEEP_PLANTS:
                        if plant.state == c.SLEEP:
                            plant_map_x, _ = self.map.getMapIndex(
                                plant.rect.centerx, plant.rect.bottom
                            )
                            if plant_map_x == self.map_x:
                                plant.state = c.IDLE
                                plant.setIdle()
                                plant.changeFrames(plant.idle_frames)
                # 播放唤醒音效
                c.SOUND_MUSHROOM_WAKEUP.play()
                self.map_content[c.MAP_PLANT].remove(self.name)
                self.kill()
                self.frame_index = self.frame_num - 1

            self.animate_timer = self.current_time

        self.image = self.frames[self.frame_index]
        self.mask = pg.mask.from_surface(self.image)
        if self.current_time - self.highlight_time < 100:
            self.image.set_alpha(150)
        elif (self.current_time - self.hit_timer) < 200:
            self.image.set_alpha(192)
        else:
            self.image.set_alpha(255)


class SeaShroom(Plant):
    def __init__(self, x, y, bullet_group):
        Plant.__init__(self, x, y, c.SEASHROOM, c.PLANT_HEALTH, bullet_group)
        self.shoot_timer = 0

    def loadImages(self, name, scale):
        self.idle_frames = []
        self.sleep_frames = []

        idle_name = name
        sleep_name = name + 'Sleep'

        frame_list = [self.idle_frames, self.sleep_frames]
        name_list = [idle_name, sleep_name]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.idle_frames

    def attacking(self):
        if self.shoot_timer == 0:
            self.shoot_timer = self.current_time - 700
        elif (self.current_time - self.shoot_timer) >= 1400:
            self.bullet_group.add(
                Bullet(
                    self.rect.right,
                    self.rect.y + 50,
                    self.rect.y + 50,
                    c.BULLET_SEASHROOM,
                    c.BULLET_DAMAGE_NORMAL,
                    effect=None,
                )
            )
            self.shoot_timer = self.current_time
            # 播放发射音效
            c.SOUND_PUFF.play()

    def canAttack(self, zombie):
        if (zombie.name == c.SNORKELZOMBIE) and (
            zombie.frames == zombie.swim_frames
        ):
            return False
        if (
            self.rect.x <= zombie.rect.right
            and (self.rect.x + c.GRID_X_SIZE * 4 >= zombie.rect.x)
            and (zombie.rect.left <= c.SCREEN_WIDTH + 10)
        ):
            return True
        return False

    def setAttack(self):
        self.state = c.ATTACK
        if self.shoot_timer != 0:
            self.shoot_timer = self.current_time - 700


class TallNut(Plant):
    def __init__(self, x, y):
        Plant.__init__(self, x, y, c.TALLNUT, c.TALLNUT_HEALTH, None)
        self.load_images()
        self.cracked1 = False
        self.cracked2 = False
        self.attack_check = c.CHECK_ATTACK_NEVER

    def load_images(self):
        self.cracked1_frames = []
        self.cracked2_frames = []

        cracked1_frames_name = self.name + '_cracked1'
        cracked2_frames_name = self.name + '_cracked2'

        self.loadFrames(self.cracked1_frames, cracked1_frames_name)
        self.loadFrames(self.cracked2_frames, cracked2_frames_name)

    def idling(self):
        if not self.cracked1 and self.health <= c.TALLNUT_CRACKED1_HEALTH:
            self.changeFrames(self.cracked1_frames)
            self.cracked1 = True
        elif not self.cracked2 and self.health <= c.TALLNUT_CRACKED2_HEALTH:
            self.changeFrames(self.cracked2_frames)
            self.cracked2 = True


class TangleKlep(Plant):
    def __init__(self, x, y):
        Plant.__init__(self, x, y, c.TANGLEKLEP, c.PLANT_HEALTH, None)
        self.load_images()
        self.splashing = False

    def load_images(self):
        self.idle_frames = []
        self.splash_frames = []

        idle_name = self.name
        splash_name = self.name + 'Splash'

        frame_list = [self.idle_frames, self.splash_frames]
        name_list = [idle_name, splash_name]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.idle_frames

    def canAttack(self, zombie):
        if zombie.state != c.DIE and (not zombie.losthead):
            # 这里碰撞应当比碰撞一般更容易,就设置成圆形或矩形模式,不宜采用mask
            if pg.sprite.collide_rect_ratio(1)(zombie, self):
                return True
        return False

    def setAttack(self, zombie, zombie_group):
        self.attack_zombie = zombie
        self.zombie_group = zombie_group
        self.state = c.ATTACK

    def attacking(self):
        if not self.splashing:
            self.splashing = True
            self.changeFrames(self.splash_frames)
            self.attack_zombie.kill()
            # 播放拖拽音效
            c.SOUND_TANGLE_KELP_DRAG.play()
        # 这里必须用elif排除尚未进入splash阶段,以免误触
        elif (self.frame_index + 1) >= self.frame_num:
            self.health = 0


# 毁灭菇的处理办法:
# 爆炸后留下的坑看作另一种形态的毁灭菇
# 当存在这种形态的毁灭菇时不可以种植物
# 坑形态的毁灭菇存在时不可种植物
# 坑形态的毁灭菇同地刺一样不可以被啃食
# 爆炸时杀死同一格的所有植物
class DoomShroom(Plant):
    def __init__(self, x, y, map_plant_set, explode_y_range):
        Plant.__init__(self, x, y, c.DOOMSHROOM, c.PLANT_HEALTH, None)
        self.map_plant_set = map_plant_set
        self.bomb_timer = 0
        self.explode_y_range = explode_y_range
        self.explode_x_range = 250
        self.start_boom = False
        self.boomed = False
        self.original_x = x
        self.original_y = y

    def loadImages(self, name, scale):
        self.idle_frames = []
        self.sleep_frames = []
        self.boom_frames = []

        idle_name = name
        sleep_name = name + 'Sleep'
        boom_name = name + 'Boom'

        frame_list = [self.idle_frames, self.sleep_frames, self.boom_frames]
        name_list = [idle_name, sleep_name, boom_name]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.idle_frames

    def setBoom(self):
        self.changeFrames(self.boom_frames)
        self.start_boom = True

    def animation(self):
        # 发生了爆炸
        if self.start_boom:
            if self.frame_index == 1:
                self.rect.x -= 80
                self.rect.y += 30
                # 播放爆炸音效
                c.SOUND_DOOMSHROOM.play()
            if (
                self.current_time - self.animate_timer
            ) > self.animate_interval:
                self.frame_index += 1
            if self.frame_index >= self.frame_num:
                self.health = 0
                self.frame_index = self.frame_num - 1
                self.map_plant_set.add(c.HOLE)
        # 睡觉状态
        elif self.state == c.SLEEP:
            if (
                self.current_time - self.animate_timer
            ) > self.animate_interval:
                self.frame_index += 1
                if self.frame_index >= self.frame_num:
                    self.frame_index = 0
                self.animate_timer = self.current_time
        # 正常状态
        else:
            self.health = c.INF
            if (self.current_time - self.animate_timer) > 100:
                self.frame_index += 1
                if self.frame_index >= self.frame_num:
                    self.setBoom()
                    return
                self.animate_timer = self.current_time
        self.image = self.frames[self.frame_index]
        self.mask = pg.mask.from_surface(self.image)

        if self.current_time - self.highlight_time < 100:
            self.image.set_alpha(150)
        elif (self.current_time - self.hit_timer) < 200:
            self.image.set_alpha(192)
        else:
            self.image.set_alpha(255)


# 用于描述毁灭菇的坑
class Hole(Plant):
    def __init__(self, x, y, plot_type):
        # 指定区域类型这一句必须放在前面,否则加载图片判断将会失败
        self.plot_type = plot_type
        Plant.__init__(self, x, y, c.HOLE, c.INF, None)
        self.timer = 0
        self.shallow = False
        self.attack_check = c.CHECK_ATTACK_NEVER

    def loadImages(self, name, scale):
        self.idle_frames = []
        self.idle2_frames = []
        self.water_frames = []
        self.water2_frames = []
        self.roof_frames = []
        self.roof2_frames = []

        idle_name = name
        idle2_name = name + 'Shallow'
        water_name = name + 'Water'
        water2_name = name + 'WaterShallow'
        roof_name = name + 'Roof'
        roof2_name = name + 'RoofShallow'

        frame_list = [
            self.idle_frames,
            self.idle2_frames,
            self.water_frames,
            self.water2_frames,
            self.roof_frames,
            self.roof2_frames,
        ]
        name_list = [
            idle_name,
            idle2_name,
            water_name,
            water2_name,
            roof_name,
            roof2_name,
        ]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        if self.plot_type == c.MAP_TILE:
            self.frames = self.roof_frames
        elif self.plot_type == c.MAP_WATER:
            self.frames = self.water_frames
        else:
            self.frames = self.idle_frames

    def idling(self):
        if self.timer == 0:
            self.timer = self.current_time
        elif (not self.shallow) and (self.current_time - self.timer >= 90000):
            if self.plot_type == c.MAP_TILE:
                self.frames = self.roof2_frames
            elif self.plot_type == c.MAP_WATER:
                self.frames = self.water2_frames
            else:
                self.frames = self.idle2_frames
            self.shallow = True
        elif self.current_time - self.timer >= 180000:
            self.health = 0


class Grave(Plant):
    def __init__(self, x, y):
        Plant.__init__(self, x, y, c.GRAVE, c.INF, None)
        self.frame_index = random.randint(0, self.frame_num - 1)
        self.image = self.frames[self.frame_index]
        self.mask = pg.mask.from_surface(self.image)
        self.attack_check = c.CHECK_ATTACK_NEVER

    def animation(self):
        pass


class GraveBuster(Plant):
    def __init__(self, x, y, plant_group, map, map_x):
        Plant.__init__(self, x, y, c.GRAVEBUSTER, c.PLANT_HEALTH, None)
        self.map = map
        self.map_x = map_x
        self.plant_group = plant_group
        self.animate_interval = 100
        self.attack_check = c.CHECK_ATTACK_NEVER
        # 播放吞噬音效
        c.SOUND_GRAVEBUSTER_CHOMP.play()

    def animation(self):
        if (self.current_time - self.animate_timer) > self.animate_interval:
            self.frame_index += 1
            if self.frame_index >= self.frame_num:
                self.frame_index = self.frame_num - 1
                for item in self.plant_group:
                    if item.name == c.GRAVE:
                        item_map_x, _ = self.map.getMapIndex(
                            item.rect.centerx, item.rect.bottom
                        )
                        if item_map_x == self.map_x:
                            item.health = 0
                            self.health = 0
            self.animate_timer = self.current_time

        self.image = self.frames[self.frame_index]
        self.mask = pg.mask.from_surface(self.image)

        if self.current_time - self.highlight_time < 100:
            self.image.set_alpha(150)
        elif (self.current_time - self.hit_timer) < 200:
            self.image.set_alpha(192)
        else:
            self.image.set_alpha(255)


class FumeShroom(Plant):
    def __init__(self, x, y, bullet_group, zombie_group):
        Plant.__init__(self, x, y, c.FUMESHROOM, c.PLANT_HEALTH, bullet_group)
        self.shoot_timer = 0
        self.show_attack_frames = True
        self.zombie_group = zombie_group

    def loadImages(self, name, scale):
        self.idle_frames = []
        self.sleep_frames = []
        self.attack_frames = []

        idle_name = name
        sleep_name = name + 'Sleep'
        attack_name = name + 'Attack'

        frame_list = [self.idle_frames, self.sleep_frames, self.attack_frames]
        name_list = [idle_name, sleep_name, attack_name]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.idle_frames

    def canAttack(self, zombie):
        if (zombie.name == c.SNORKELZOMBIE) and (
            zombie.frames == zombie.swim_frames
        ):
            return False
        if (
            self.rect.x <= zombie.rect.right
            and (self.rect.x + c.GRID_X_SIZE * 5 >= zombie.rect.x)
            and (zombie.rect.left <= c.SCREEN_WIDTH + 10)
        ):
            return True
        return False

    def setAttack(self):
        self.state = c.ATTACK
        if self.shoot_timer != 0:
            self.shoot_timer = self.current_time - 700

    def attacking(self):
        if self.shoot_timer == 0:
            self.shoot_timer = self.current_time - 700
        elif self.current_time - self.shoot_timer >= 1100:
            if self.show_attack_frames:
                self.show_attack_frames = False
                self.changeFrames(self.attack_frames)

        if self.current_time - self.shoot_timer >= 1400:
            self.bullet_group.add(Fume(self.rect.right - 35, self.rect.y))
            # 烟雾只是个动画,实际伤害由本身完成
            for target_zombie in self.zombie_group:
                if self.canAttack(target_zombie):
                    target_zombie.setDamage(
                        c.BULLET_DAMAGE_NORMAL,
                        damage_type=c.ZOMBIE_RANGE_DAMAGE,
                    )
            self.shoot_timer = self.current_time
            self.show_attack_frames = True
            # 播放发射音效
            c.SOUND_FUME.play()

    def animation(self):
        if (self.current_time - self.animate_timer) > self.animate_interval:
            self.frame_index += 1
            if self.frame_index >= self.frame_num:
                if self.frames == self.attack_frames:
                    self.changeFrames(self.idle_frames)
                else:
                    self.frame_index = 0
            self.animate_timer = self.current_time

        self.image = self.frames[self.frame_index]
        self.mask = pg.mask.from_surface(self.image)

        if self.current_time - self.highlight_time < 100:
            self.image.set_alpha(150)
        elif (self.current_time - self.hit_timer) < 200:
            self.image.set_alpha(192)
        else:
            self.image.set_alpha(255)


class IceFrozenPlot(Plant):
    def __init__(self, x, y):
        Plant.__init__(self, x, y, c.ICEFROZENPLOT, c.INF, None)
        self.timer = 0
        self.attack_check = c.CHECK_ATTACK_NEVER

    def idling(self):
        if self.timer == 0:
            self.timer = self.current_time
        elif self.current_time - self.timer >= 30000:
            self.health = 0


class Garlic(Plant):
    def __init__(self, x, y):
        Plant.__init__(self, x, y, c.GARLIC, c.GARLIC_HEALTH, None)
        self.load_images()
        self.cracked1 = False
        self.cracked2 = False

    def load_images(self):
        self.cracked1_frames = []
        self.cracked2_frames = []

        cracked1_frames_name = self.name + '_cracked1'
        cracked2_frames_name = self.name + '_cracked2'

        self.loadFrames(self.cracked1_frames, cracked1_frames_name)
        self.loadFrames(self.cracked2_frames, cracked2_frames_name)

    def idling(self):
        if (not self.cracked1) and self.health <= c.GARLIC_CRACKED1_HEALTH:
            self.changeFrames(self.cracked1_frames)
            self.cracked1 = True
        elif (not self.cracked2) and self.health <= c.GARLIC_CRACKED2_HEALTH:
            self.changeFrames(self.cracked2_frames)
            self.cracked2 = True


class PumpkinHead(Plant):
    def __init__(self, x, y):
        Plant.__init__(self, x, y, c.PUMPKINHEAD, c.WALLNUT_HEALTH, None)
        self.load_images()
        self.cracked1 = False
        self.cracked2 = False
        self.animate_interval = 160
        self.attack_check = c.CHECK_ATTACK_NEVER

    def load_images(self):
        self.cracked1_frames = []
        self.cracked2_frames = []

        cracked1_frames_name = self.name + '_cracked1'
        cracked2_frames_name = self.name + '_cracked2'

        self.loadFrames(self.cracked1_frames, cracked1_frames_name)
        self.loadFrames(self.cracked2_frames, cracked2_frames_name)

    def idling(self):
        if not self.cracked1 and self.health <= c.WALLNUT_CRACKED1_HEALTH:
            self.changeFrames(self.cracked1_frames)
            self.cracked1 = True
        elif not self.cracked2 and self.health <= c.WALLNUT_CRACKED2_HEALTH:
            self.changeFrames(self.cracked2_frames)
            self.cracked2 = True


class GiantWallNut(Plant):
    def __init__(self, x, y):
        Plant.__init__(self, x, y, c.GIANTWALLNUT, 1, None)
        self.init_rect = self.rect.copy()
        self.rotate_degree = 0
        self.animate_interval = 200
        self.move_timer = 0
        self.move_interval = 70
        self.vel_x = random.randint(15, 18)
        self.attack_check = c.CHECK_ATTACK_NEVER

    def idling(self):
        if self.move_timer == 0:
            self.move_timer = self.current_time
        elif (self.current_time - self.move_timer) >= self.move_interval:
            self.rotate_degree = (self.rotate_degree - 30) % 360
            self.init_rect.x += self.vel_x
            if self.init_rect.x > c.SCREEN_WIDTH:
                self.health = 0
            self.move_timer += self.move_interval

    def animation(self):
        image = self.frames[self.frame_index]
        self.image = pg.transform.rotate(image, self.rotate_degree)
        self.mask = pg.mask.from_surface(self.image)
        # must keep the center postion of image when rotate
        self.rect = self.image.get_rect(center=self.init_rect.center)


================================================
FILE: source/component/zombie.py
================================================
import random

import pygame as pg

from .. import constants as c
from .. import tool


class Zombie(pg.sprite.Sprite):
    def __init__(
        self,
        x,
        y,
        name,
        head_group=None,
        helmet_health=0,
        helmet_type2_health=0,
        body_health=c.NORMAL_HEALTH,
        losthead_health=c.LOSTHEAD_HEALTH,
        damage=c.ZOMBIE_ATTACK_DAMAGE,
        can_swim=False,
    ):
        pg.sprite.Sprite.__init__(self)

        self.name = name
        self.frames = []
        self.frame_index = 0
        self.loadImages()
        self.frame_num = len(self.frames)

        self.image = self.frames[self.frame_index]
        self.rect = self.image.get_rect()
        self.mask = pg.mask.from_surface(self.image)
        self.rect.x = x
        self.rect.bottom = y
        # 大蒜换行移动像素值,< 0时向上,= 0时不变,> 0时向上
        self.target_y_change = 0
        self.original_y = y
        self.to_change_group = False

        self.helmet_health = helmet_health
        self.helmet_type2_health = helmet_type2_health
        self.health = body_health + losthead_health
        self.losthead_health = losthead_health
        self.damage = damage
        self.dead = False
        self.losthead = False
        self.can_swim = can_swim
        self.swimming = False
        self.helmet = self.helmet_health > 0
        self.helmet_type2 = self.helmet_type2_health > 0
        self.head_group = head_group

        self.walk_timer = 0
        self.animate_timer = 0
        self.attack_timer = 0
        self.state = c.WALK
        self.animate_interval = 150
        self.walk_animate_interval = 180
        self.attack_animate_interval = 100
        self.losthead_animate_interval = 180
        self.die_animate_interval = 50
        self.boomDie_animate_interval = 100
        self.ice_slow_ratio = 1
        self.ice_slow_timer = 0
        self.hit_timer = 0
        self.speed = 1
        self.freeze_timer = 0
        self.losthead_timer = 0
        self.is_hypno = False  # the zombie is hypo and attack other zombies when it ate a HypnoShroom

    def loadFrames(self, frames, name, colorkey=c.BLACK):
        frame_list = tool.GFX[name]
        rect = frame_list[0].get_rect()
        width, height = rect.w, rect.h
        if name in c.ZOMBIE_RECT:
            data = c.ZOMBIE_RECT[name]
            x, width = data['x'], data['width']
        else:
            x = 0
        for frame in frame_list:
            frames.append(tool.get_image(frame, x, 0, width, height, colorkey))

    def update(self, game_info):
        self.current_time = game_info[c.CURRENT_TIME]
        self.handleState()
        self.updateIceSlow()
        self.animation()

    def handleState(self):
        if self.state == c.WALK:
            self.walking()
        elif self.state == c.ATTACK:
            self.attacking()
        elif self.state == c.DIE:
            self.dying()
        elif self.state == c.FREEZE:
            self.freezing()

    # 濒死状态用函数
    def checkToDie(self, framesKind):
        if self.health <= 0:
            self.setDie()
            return True
        elif self.health <= self.losthead_health:
            if not self.losthead:
                self.changeFrames(framesKind)
                self.setLostHead()
                return True
            else:
                self.health -= (self.current_time - self.losthead_timer) / 40
                self.losthead_timer = self.current_time
                return False
        else:
            return False

    def walking(self):
        if self.checkToDie(self.losthead_walk_frames):
            return

        # 能游泳的僵尸
        if self.can_swim:
            # 在水池范围内
            # 在右侧岸左
            if self.rect.right <= c.MAP_POOL_FRONT_X:
                # 在左侧岸右,左侧岸位置为预估
                if self.rect.right - 25 >= c.MAP_POOL_OFFSET_X:
                    # 还未进入游泳状态
                    if not self.swimming:
                        self.swimming = True
                        self.changeFrames(self.swim_frames)
                        # 播放入水音效
                        c.SOUND_ZOMBIE_ENTERING_WATER.play()
                        # 同样没有兼容双防具
                        if self.helmet:
                            if self.helmet_health <= 0:
                                self.helmet = False
                            else:
                                self.changeFrames(self.helmet_swim_frames)
                        if self.helmet_type2:
                            if self.helmet_type2_health <= 0:
                                self.helmet_type2 = False
                            else:
                                self.changeFrames(self.helmet_swim_frames)
                    # 已经进入游泳状态
                    else:
                        if self.helmet:
                            if self.helmet_health <= 0:
                                self.changeFrames(self.swim_frames)
                                self.helmet = False
                        if self.helmet_type2:
                            if self.helmet_type2_health <= 0:
                                self.changeFrames(self.swim_frames)
                                self.helmet_type2 = False
                # 水生僵尸已经接近家门口并且上岸
                else:
                    if self.swimming:
                        self.changeFrames(self.walk_frames)
                        self.swimming = False
                        # 同样没有兼容双防具
                        if self.helmet:
                            if self.helmet_health <= 0:
                                self.helmet = False
                            else:
                                self.changeFrames(self.helmet_walk_frames)
                        if self.helmet_type2:
                            if self.helmet_type2_health <= 0:
                                self.helmet_type2 = False
                            else:
                                self.changeFrames(self.helmet_walk_frames)
                    if self.helmet:
                        if self.helmet_health <= 0:
                            self.helmet = False
                            self.changeFrames(self.walk_frames)
                    if self.helmet_type2:
                        if self.helmet_type2_health <= 0:
                            self.helmet_type2 = False
                            self.changeFrames(self.walk_frames)
            elif (
                self.is_hypno and self.rect.right > c.MAP_POOL_FRONT_X + 55
            ):   # 常数拟合暂时缺乏检验
                if self.swimming:
                    self.changeFrames(self.walk_frames)
                if self.helmet:
                    if self.helmet_health <= 0:
                        self.changeFrames(self.walk_frames)
                        self.helmet = False
                    elif self.swimming:   # 游泳状态需要改为步行
                        self.changeFrames(self.helmet_walk_frames)
                if self.helmet_type2:
                    if self.helmet_type2_health <= 0:
                        self.changeFrames(self.walk_frames)
                        self.helmet_type2 = False
                    elif self.swimming:   # 游泳状态需要改为步行
                        self.changeFrames(self.helmet_walk_frames)
                self.swimming = False
            # 尚未进入水池
            else:
                if self.helmet_health <= 0 and self.helmet:
                    self.changeFrames(self.walk_frames)
                    self.helmet = False
                if self.helmet_type2_health <= 0 and self.helmet_type2:
                    self.changeFrames(self.walk_frames)
                    self.helmet_type2 = False
        # 不能游泳的一般僵尸
        else:
            if self.helmet_health <= 0 and self.helmet:
                self.changeFrames(self.walk_frames)
                self.helmet = False
            if self.helmet_type2_health <= 0 and self.helmet_type2:
                self.changeFrames(self.walk_frames)
                self.helmet_type2 = False

        if (self.current_time - self.walk_timer) > (
            c.ZOMBIE_WALK_INTERVAL * self.getTimeRatio()
        ):
            self.handleGarlicYChange()
            self.walk_timer = self.current_time
            if self.is_hypno:
                self.rect.x += 1
            else:
                self.rect.x -= 1

    def handleGarlicYChange(self):
        if self.target_y_change < 0:
            if (
                self.rect.bottom > self.original_y + self.target_y_change
            ):  # 注意这里加的是负数
                self.rect.bottom -= 3
                # 过半时换行
                if (self.to_change_group) and (
                    self.rect.bottom
                    >= self.original_y + 0.5 * self.target_y_change
                ):
                    self.level.zombie_groups[self.map_y].remove(self)
                    self.level.zombie_groups[self.target_map_y].add(self)
                    self.to_change_group = False
            else:
                self.rect.bottom = self.original_y + self.target_y_change
                self.original_y = self.rect.bottom
                self.target_y_change = 0
        elif self.target_y_change > 0:
            if (
                self.rect.bottom < self.original_y + self.target_y_change
            ):  # 注意这里加的是负数
                self.rect.bottom += 3
                # 过半时换行
                if (self.to_change_group) and (
                    self.rect.bottom
                    <= self.original_y + 0.5 * self.target_y_change
                ):
                    self.level.zombie_groups[self.map_y].remove(self)
                    self.level.zombie_groups[self.target_map_y].add(self)
                    self.to_change_group = False
            else:
                self.rect.bottom = self.original_y + self.target_y_change
                self.original_y = self.rect.bottom
                self.target_y_change = 0

    def attacking(self):
        if self.checkToDie(self.losthead_attack_frames):
            return

        if self.helmet_health <= 0 and self.helmet:
            self.changeFrames(self.attack_frames)
            self.helmet = False
        if self.helmet_type2_health <= 0 and self.helmet_type2:
            self.changeFrames(self.attack_frames)
            self.helmet_type2 = False
            if self.name == c.NEWSPAPER_ZOMBIE:
                self.speed = 2.65
                self.walk_animate_interval = 300
        if (
            (self.current_time - self.attack_timer)
            > (c.ATTACK_INTERVAL * self.getAttackTimeRatio())
        ) and (not self.losthead):
            if self.prey.health > 0:
                if self.prey_is_plant:
                    self.prey.setDamage(self.damage, self)
                    if self.prey.name == c.GARLIC:
                        self.setWalk()
                else:
                    self.prey.setDamage(self.damage)

                # 播放啃咬音效
                c.SOUND_ZOMBIE_ATTACKING.play()
            self.attack_timer = self.current_time

        if self.prey.health <= 0:
            self.prey = None
            self.setWalk()

    def dying(self):
        pass

    def freezing(self):
        if self.old_state == c.WALK:
            if self.checkToDie(self.losthead_walk_frames):
                return
        else:
            if self.checkToDie(self.losthead_attack_frames):
                return

        if (
            self.current_time - self.freeze_timer
        ) >= c.MIN_FREEZE_TIME + random.randint(0, 2000):
            self.setWalk()
            # 注意寒冰菇解冻后还有减速
            self.ice_slow_timer = (
                self.freeze_timer + 10000
            )   # 每次冰冻冻结 + 减速时间为20 s,而减速有10 s计时,故这里+10 s
            self.ice_slow_ratio = 2

    def setLostHead(self):
        self.losthead_timer = self.current_time
        self.losthead = True
        self.animate_interval = self.losthead_animate_interval
        if self.head_group is not None:
            self.head_group.add(
                ZombieHead(self.rect.centerx, self.rect.bottom)
            )

    def changeFrames(self, frames):
        """change image frames and modify rect position"""
        self.frames = frames
        self.frame_num = len(self.frames)
        self.frame_index = 0

        bottom = self.rect.bottom
        centerx = self.rect.centerx
        self.image = self.frames[self.frame_index]
        self.mask = pg.mask.from_surface(self.image)
        self.rect = self.image.get_rect()
        self.rect.bottom = bottom
        self.rect.centerx = centerx

    def animation(self):
        if self.state == c.FREEZE:
            self.image.set_alpha(192)
            return

        if (self.current_time - self.animate_timer) > (
            self.animate_interval * self.getTimeRatio()
        ):
            self.frame_index += 1
            if self.frame_index >= self.frame_num:
                if self.state == c.DIE:
                    self.kill()
                    return
                self.frame_index = 0
            self.animate_timer = self.current_time

        self.image = self.frames[self.frame_index]
        if self.is_hypno:
            self.image = pg.transform.flip(self.image, True, False)
        self.mask = pg.mask.from_surface(self.image)
        if (self.current_time - self.hit_timer) >= 200:
            self.image.set_alpha(255)
        else:
            self.image.set_alpha(192)

    def getTimeRatio(self):
        return (
            self.ice_slow_ratio / self.speed
        )   # 目前的机制为:冰冻减速状态与自身速度共同决定行走的时间间隔

    def getAttackTimeRatio(self):
        return self.ice_slow_ratio  # 攻击速度只取决于冰冻状态

    def setIceSlow(self):
        # 在转入冰冻减速状态时播放冰冻音效
        if self.ice_slow_ratio == 1:
            c.SOUND_FREEZE.play()

        # when get a ice bullet damage, slow the attack or walk speed of the zombie
        self.ice_slow_timer = self.current_time
        self.ice_slow_ratio = 2

    def updateIceSlow(self):
        if self.ice_slow_ratio > 1:
            if (self.current_time - self.ice_slow_timer) > c.ICE_SLOW_TIME:
                self.ice_slow_ratio = 1

    def setDamage(
        self, damage, effect=None, damage_type=c.ZOMBIE_COMMON_DAMAGE
    ):
        # 冰冻减速效果
        if effect == c.BULLET_EFFECT_ICE:
            if damage_type == c.ZOMBIE_DEAFULT_DAMAGE:   # 寒冰射手不能穿透二类防具进行减速
                if not self.helmet_type2:
                    self.setIceSlow()
            else:
                self.setIceSlow()
        # 解冻
        elif effect == c.BULLET_EFFECT_UNICE:
            if damage_type == c.ZOMBIE_DEAFULT_DAMAGE:   # 寒冰射手不能穿透二类防具进行减速
                if not self.helmet_type2:
                    self.ice_slow_ratio = 1
            else:
                self.ice_slow_ratio = 1

        if damage_type == c.ZOMBIE_DEAFULT_DAMAGE:   # 不穿透二类防具的攻击
            # 从第二类防具开始逐级传递
            if self.helmet_type2:
                self.helmet_type2_health -= damage
                if self.helmet_type2_health <= 0:
                    if self.helmet:
                        self.helmet_health += (
                            self.helmet_type2_health
                        )   # 注意self.helmet_type2_health已经带有正负
                        self.helmet_type2_health = 0  # 注意合并后清零
                        if self.helmet_health <= 0:
                            self.health += self.helmet_health
                            self.helmet_health = 0   # 注意合并后清零
                    else:
                        self.health += self.helmet_type2_health
                        self.helmet_type2_health = 0
            elif self.helmet:   # 不存在二类防具,但是存在一类防具
                self.helmet_health -= damage
                if self.helmet_health <= 0:
                    self.health += self.helmet_health
                    self.helmet_health = 0   # 注意合并后清零
            else:   # 没有防具
                self.health -= damage
        elif damage_type == c.ZOMBIE_COMMON_DAMAGE:  # 无视二类防具,将攻击一类防具与本体视为整体的攻击
            if self.helmet:   # 存在一类防具
                self.helmet_health -= damage
                if self.helmet_health <= 0:
                    self.health += self.helmet_health
                    self.helmet_health = 0   # 注意合并后清零
            else:   # 没有一类防具
                self.health -= damage
        elif damage_type == c.ZOMBIE_RANGE_DAMAGE:
            # 从第二类防具开始逐级传递
            if self.helmet_type2:
                self.helmet_type2_health -= damage
                if self.helmet_type2_health <= 0:
                    if self.helmet:
                        self.helmet_health -= damage   # 注意范围伤害中这里还有一个攻击
                        self.helmet_health += (
                            self.helmet_type2_health
                        )   # 注意self.helmet_type2_health已经带有正负
                        self.helmet_type2_health = 0  # 注意合并后清零
                        if self.helmet_health <= 0:
                            self.health += self.helmet_health
                            self.helmet_health = 0   # 注意合并后清零
                    else:
                        self.health -= damage   # 注意范围伤害中这里还有一个攻击
                        self.health += self.helmet_type2_health
                        self.helmet_type2_health = 0
                else:
                    if self.helmet:
                        self.helmet_health -= damage
                        if self.helmet_health <= 0:
                            self.health += self.helmet_health
                            self.helmet_health = 0   # 注意合并后清零
                    else:
                        self.health -= damage
            elif self.helmet:   # 不存在二类防具,但是存在一类防具
                self.helmet_health -= damage
                if self.helmet_health <= 0:
                    self.health += self.helmet_health
                    self.helmet_health = 0   # 注意合并后清零
            else:   # 没有防具
                self.health -= damage
        elif damage_type == c.ZOMBIE_ASH_DAMAGE:
            self.health -= damage   # 无视任何防具
        elif damage_type == c.ZOMBIE_WALLNUT_BOWLING_DANMAGE:
            # 逻辑:对防具的多余伤害不传递
            if self.helmet_type2:
                # 对二类防具伤害较一般情况低,拟合铁门需要砸3次的设定
                self.helmet_type2_health -= int(damage * 0.8)
            elif self.helmet:   # 不存在二类防具,但是存在一类防具
                self.helmet_health -= damage
            else:   # 没有防具
                self.health -= damage
        else:
            print('警告:植物攻击类型错误,现在默认进行类豌豆射手型攻击')
            self.setDamage(
                damage, effect=effect, damage_type=c.ZOMBIE_DEAFULT_DAMAGE
            )

        # 记录攻击时间
        self.hit_timer = self.current_time

    def setWalk(self):
        self.state = c.WALK
        self.animate_interval = self.walk_animate_interval

        if self.helmet or self.helmet_type2:   # 这里暂时没有考虑同时有两种防具的僵尸
            self.changeFrames(self.helmet_walk_frames)
        elif self.losthead:
            self.changeFrames(self.losthead_walk_frames)
        else:
            self.changeFrames(self.walk_frames)

        if self.can_swim:
            if self.rect.right <= c.MAP_POOL_FRONT_X:
                self.swimming = True
                self.changeFrames(self.swim_frames)
                # 同样没有兼容双防具
                if self.helmet:
                    if self.helmet_health <= 0:
                        self.changeFrames(self.swim_frames)
                        self.helmet = False
                    else:
                        self.changeFrames(self.helmet_swim_frames)
                if self.helmet_type2:
                    if self.helmet_type2_health <= 0:
                        self.changeFrames(self.swim_frames)
                        self.helmet_type2 = False
                    else:
                        self.changeFrames(self.helmet_swim_frames)

    def setAttack(self, prey, is_plant=True):
        self.prey = prey  # prey can be plant or other zombies
        self.prey_is_plant = is_plant
        self.state = c.ATTACK
        self.attack_timer = self.current_time
        self.animate_interval = self.attack_animate_interval

        if self.helmet or self.helmet_type2:   # 这里暂时没有考虑同时有两种防具的僵尸
            self.changeFrames(self.helmet_attack_frames)
        elif self.losthead:
            self.changeFrames(self.losthead_attack_frames)
        else:
            self.changeFrames(self.attack_frames)

    def setDie(self):
        self.state = c.DIE
        self.animate_interval = self.die_animate_interval
        self.changeFrames(self.die_frames)

    def setBoomDie(self):
        self.health = 0
        self.state = c.DIE
        self.animate_interval = self.boomDie_animate_interval
        self.changeFrames(self.boomdie_frames)

    def setFreeze(self, ice_trap_image):
        self.old_state = self.state
        self.state = c.FREEZE
        self.freeze_timer = self.current_time
        self.ice_trap_image = ice_trap_image
        self.ice_trap_rect = ice_trap_image.get_rect()
        self.ice_trap_rect.centerx = self.rect.centerx
        self.ice_trap_rect.bottom = self.rect.bottom

    def drawFreezeTrap(self, surface):
        if self.state == c.FREEZE:
            surface.blit(self.ice_trap_image, self.ice_trap_rect)

    def setHypno(self):
        self.is_hypno = True
        self.setWalk()
        # 播放魅惑音效
        c.SOUND_HYPNOED.play()


class ZombieHead(Zombie):
    def __init__(self, x, y):
        Zombie.__init__(self, x, y, c.ZOMBIE_HEAD, 0)
        self.state = c.DIE

    def loadImages(self):
        self.die_frames = []
        die_name = self.name
        self.loadFrames(self.die_frames, die_name)
        self.frames = self.die_frames

    def setWalk(self):
        self.animate_interval = 100


class NormalZombie(Zombie):
    def __init__(self, x, y, head_group):
        Zombie.__init__(self, x, y, c.NORMAL_ZOMBIE, head_group)

    def loadImages(self):
        self.walk_frames = []
        self.attack_frames = []
        self.losthead_walk_frames = []
        self.losthead_attack_frames = []
        self.die_frames = []
        self.boomdie_frames = []

        walk_name = self.name
        attack_name = self.name + 'Attack'
        losthead_walk_name = self.name + 'LostHead'
        losthead_attack_name = self.name + 'LostHeadAttack'
        die_name = self.name + 'Die'
        boomdie_name = c.BOOMDIE

        frame_list = [
            self.walk_frames,
            self.attack_frames,
            self.losthead_walk_frames,
            self.losthead_attack_frames,
            self.die_frames,
            self.boomdie_frames,
        ]
        name_list = [
            walk_name,
            attack_name,
            losthead_walk_name,
            losthead_attack_name,
            die_name,
            boomdie_name,
        ]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.walk_frames


# 路障僵尸
class ConeHeadZombie(Zombie):
    def __init__(self, x, y, head_group):
        Zombie.__init__(
            self,
            x,
            y,
            c.CONEHEAD_ZOMBIE,
            head_group,
            helmet_health=c.CONEHEAD_HEALTH,
        )

    def loadImages(self):
        self.helmet_walk_frames = []
        self.helmet_attack_frames = []
        self.walk_frames = []
        self.attack_frames = []
        self.losthead_walk_frames = []
        self.losthead_attack_frames = []
        self.die_frames = []
        self.boomdie_frames = []

        helmet_walk_name = self.name
        helmet_attack_name = self.name + 'Attack'
        walk_name = c.NORMAL_ZOMBIE
        attack_name = c.NORMAL_ZOMBIE + 'Attack'
        losthead_walk_name = c.NORMAL_ZOMBIE + 'LostHead'
        losthead_attack_name = c.NORMAL_ZOMBIE + 'LostHeadAttack'
        die_name = c.NORMAL_ZOMBIE + 'Die'
        boomdie_name = c.BOOMDIE

        frame_list = [
            self.helmet_walk_frames,
            self.helmet_attack_frames,
            self.walk_frames,
            self.attack_frames,
            self.losthead_walk_frames,
            self.losthead_attack_frames,
            self.die_frames,
            self.boomdie_frames,
        ]
        name_list = [
            helmet_walk_name,
            helmet_attack_name,
            walk_name,
            attack_name,
            losthead_walk_name,
            losthead_attack_name,
            die_name,
            boomdie_name,
        ]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.helmet_walk_frames


class BucketHeadZombie(Zombie):
    def __init__(self, x, y, head_group):
        Zombie.__init__(
            self,
            x,
            y,
            c.BUCKETHEAD_ZOMBIE,
            head_group,
            helmet_health=c.BUCKETHEAD_HEALTH,
        )

    def loadImages(self):
        self.helmet_walk_frames = []
        self.helmet_attack_frames = []
        self.walk_frames = []
        self.attack_frames = []
        self.losthead_walk_frames = []
        self.losthead_attack_frames = []
        self.die_frames = []
        self.boomdie_frames = []

        helmet_walk_name = self.name
        helmet_attack_name = self.name + 'Attack'
        walk_name = c.NORMAL_ZOMBIE
        attack_name = c.NORMAL_ZOMBIE + 'Attack'
        losthead_walk_name = c.NORMAL_ZOMBIE + 'LostHead'
        losthead_attack_name = c.NORMAL_ZOMBIE + 'LostHeadAttack'
        die_name = c.NORMAL_ZOMBIE + 'Die'
        boomdie_name = c.BOOMDIE

        frame_list = [
            self.helmet_walk_frames,
            self.helmet_attack_frames,
            self.walk_frames,
            self.attack_frames,
            self.losthead_walk_frames,
            self.losthead_attack_frames,
            self.die_frames,
            self.boomdie_frames,
        ]
        name_list = [
            helmet_walk_name,
            helmet_attack_name,
            walk_name,
            attack_name,
            losthead_walk_name,
            losthead_attack_name,
            die_name,
            boomdie_name,
        ]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.helmet_walk_frames


class FlagZombie(Zombie):
    def __init__(self, x, y, head_group):
        Zombie.__init__(self, x, y, c.FLAG_ZOMBIE, head_group)
        self.speed = 1.25

    def loadImages(self):
        self.walk_frames = []
        self.attack_frames = []
        self.losthead_walk_frames = []
        self.losthead_attack_frames = []
        self.die_frames = []
        self.boomdie_frames = []

        walk_name = self.name
        attack_name = self.name + 'Attack'
        losthead_walk_name = self.name + 'LostHead'
        losthead_attack_name = self.name + 'LostHeadAttack'
        die_name = c.NORMAL_ZOMBIE + 'Die'
        boomdie_name = c.BOOMDIE

        frame_list = [
            self.walk_frames,
            self.attack_frames,
            self.losthead_walk_frames,
            self.losthead_attack_frames,
            self.die_frames,
            self.boomdie_frames,
        ]
        name_list = [
            walk_name,
            attack_name,
            losthead_walk_name,
            losthead_attack_name,
            die_name,
            boomdie_name,
        ]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.walk_frames


class NewspaperZombie(Zombie):
    def __init__(self, x, y, head_group):
        Zombie.__init__(
            self,
            x,
            y,
            c.NEWSPAPER_ZOMBIE,
            head_group,
            helmet_type2_health=c.NEWSPAPER_HEALTH,
        )
        self.speed_up = False

    def loadImages(self):
        self.helmet_walk_frames = []
        self.helmet_attack_frames = []
        self.walk_frames = []
        self.attack_frames = []
        self.losthead_walk_frames = []
        self.losthead_attack_frames = []
        self.lostnewspaper_frames = []
        self.die_frames = []
        self.boomdie_frames = []

        helmet_walk_name = self.name
        helmet_attack_name = self.name + 'Attack'
        walk_name = self.name + 'NoPaper'
        attack_name = self.name + 'NoPaperAttack'
        losthead_walk_name = self.name + 'LostHead'
        losthead_attack_name = self.name + 'LostHeadAttack'
        lostnewspaper_name = self.name + 'LostNewspaper'
        die_name = self.name + 'Die'
        boomdie_name = c.BOOMDIE

        frame_list = [
            self.helmet_walk_frames,
            self.helmet_attack_frames,
            self.walk_frames,
            self.attack_frames,
            self.losthead_walk_frames,
            self.losthead_attack_frames,
            self.lostnewspaper_frames,
            self.die_frames,
            self.boomdie_frames,
        ]
        name_list = [
            helmet_walk_name,
            helmet_attack_name,
            walk_name,
            attack_name,
            losthead_walk_name,
            losthead_attack_name,
            lostnewspaper_name,
            die_name,
            boomdie_name,
        ]

        for i, name in enumerate(name_list):
            if name in {c.BOOMDIE, lostnewspaper_name}:
                color = c.BLACK
            else:
                color = c.WHITE
            self.loadFrames(frame_list[i], name, color)

        self.frames = self.helmet_walk_frames

    def walking(self):
        if self.checkToDie(self.losthead_walk_frames):
            return

        if self.helmet_type2_health <= 0 and self.helmet_type2:
            self.changeFrames(self.lostnewspaper_frames)
            self.helmet_type2 = False
            # 触发报纸撕裂音效
            c.SOUND_NEWSPAPER_RIP.play()
        if (self.current_time - self.walk_timer) > (
            c.ZOMBIE_WALK_INTERVAL * self.getTimeRatio()
        ):
            self.handleGarlicYChange()
            self.walk_timer = self.current_time
            if self.frames == self.lostnewspaper_frames:
                pass
            elif self.is_hypno:
                self.rect.x += 1
            else:
                self.rect.x -= 1

    def animation(self):
        if self.state == c.FREEZE:
            self.image.set_alpha(192)
            return

        if (self.current_time - self.animate_timer) > (
            self.animate_interval * self.getTimeRatio()
        ):
            self.frame_index += 1
            if self.frame_index >= self.frame_num:
                if self.state == c.DIE:
                    self.kill()
                    return
                elif self.frames == self.lostnewspaper_frames and (
                    not self.speed_up
                ):
                    self.changeFrames(self.walk_frames)
                    self.speed_up = True
                    self.speed = 2.65
                    self.walk_animate_interval = 300
                    # 触发报纸僵尸暴走音效
                    c.SOUND_NEWSPAPER_ZOMBIE_ANGRY.play()
                    return
                self.frame_index = 0
            self.animate_timer = self.current_time

        self.image = self.frames[self.frame_index]
        if self.is_hypno:
            self.image = pg.transform.flip(self.image, True, False)
        self.mask = pg.mask.from_surface(self.image)
        if (self.current_time - self.hit_timer) >= 200:
            self.image.set_alpha(255)
        else:
            self.image.set_alpha(192)


class FootballZombie(Zombie):
    def __init__(self, x, y, head_group):
        Zombie.__init__(
            self,
            x,
            y,
            c.FOOTBALL_ZOMBIE,
            head_group,
            helmet_health=c.FOOTBALL_HELMET_HEALTH,
        )
        self.speed = 1.88
        self.animate_interval = 50
        self.walk_animate_interval = 50
        self.attack_animate_interval = 60
        self.losthead_animate_interval = 180
        self.die_animate_interval = 150

    def loadImages(self):
        self.helmet_walk_frames = []
        self.helmet_attack_frames = []
        self.walk_frames = []
        self.attack_frames = []
        self.losthead_walk_frames = []
        self.losthead_attack_frames = []
        self.die_frames = []
        self.boomdie_frames = []

        helmet_walk_name = self.name
        helmet_attack_name = self.name + 'Attack'
        walk_name = self.name + 'LostHelmet'
        attack_name = self.name + 'LostHelmetAttack'
        losthead_walk_name = self.name + 'LostHead'
        losthead_attack_name = self.name + 'LostHeadAttack'
        die_name = self.name + 'Die'
        boomdie_name = c.BOOMDIE

        frame_list = [
            self.helmet_walk_frames,
            self.helmet_attack_frames,
            self.walk_frames,
            self.attack_frames,
            self.losthead_walk_frames,
            self.losthead_attack_frames,
            self.die_frames,
            self.boomdie_frames,
        ]
        name_list = [
            helmet_walk_name,
            helmet_attack_name,
            walk_name,
            attack_name,
            losthead_walk_name,
            losthead_attack_name,
            die_name,
            boomdie_name,
        ]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.helmet_walk_frames


class DuckyTubeZombie(Zombie):
    def __init__(self, x, y, head_group):
        Zombie.__init__(
            self, x, y, c.DUCKY_TUBE_ZOMBIE, head_group, can_swim=True
        )

    def loadImages(self):
        self.walk_frames = []
        self.swim_frames = []
        self.attack_frames = []
        self.losthead_walk_frames = []
        self.losthead_attack_frames = []
        self.die_frames = []
        self.boomdie_frames = []

        walk_name = self.name
        swim_name = self.name + 'Swim'
        attack_name = self.name + 'Attack'
        losthead_walk_name = self.name + 'LostHead'
        losthead_attack_name = self.name + 'LostHead'
        die_name = self.name + 'Die'
        boomdie_name = c.BOOMDIE

        frame_list = [
            self.walk_frames,
            self.swim_frames,
            self.attack_frames,
            self.losthead_walk_frames,
            self.losthead_attack_frames,
            self.die_frames,
            self.boomdie_frames,
        ]
        name_list = [
            walk_name,
            swim_name,
            attack_name,
            losthead_walk_name,
            losthead_attack_name,
            die_name,
            boomdie_name,
        ]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.walk_frames


class ConeHeadDuckyTubeZombie(Zombie):
    def __init__(self, x, y, head_group):
        Zombie.__init__(
            self,
            x,
            y,
            c.CONEHEAD_DUCKY_TUBE_ZOMBIE,
            head_group,
            helmet_health=c.CONEHEAD_HEALTH,
            can_swim=True,
        )

    def loadImages(self):
        self.helmet_walk_frames = []
        self.walk_frames = []
        self.helmet_swim_frames = []
        self.swim_frames = []
        self.helmet_attack_frames = []
        self.attack_frames = []
        self.losthead_walk_frames = []
        self.losthead_attack_frames = []
        self.die_frames = []
        self.boomdie_frames = []

        helmet_walk_name = self.name
        helmet_swim_name = self.name + 'Swim'
        helmet_attack_name = self.name + 'Attack'
        walk_name = c.DUCKY_TUBE_ZOMBIE
        swim_name = c.DUCKY_TUBE_ZOMBIE + 'Swim'
        attack_name = c.DUCKY_TUBE_ZOMBIE + 'Attack'
        losthead_walk_name = c.DUCKY_TUBE_ZOMBIE + 'LostHead'
        losthead_attack_name = c.DUCKY_TUBE_ZOMBIE + 'LostHead'
        die_name = c.DUCKY_TUBE_ZOMBIE + 'Die'
        boomdie_name = c.BOOMDIE

        frame_list = [
            self.helmet_walk_frames,
            self.helmet_swim_frames,
            self.helmet_attack_frames,
            self.walk_frames,
            self.swim_frames,
            self.attack_frames,
            self.losthead_walk_frames,
            self.losthead_attack_frames,
            self.die_frames,
            self.boomdie_frames,
        ]
        name_list = [
            helmet_walk_name,
            helmet_swim_name,
            helmet_attack_name,
            walk_name,
            swim_name,
            attack_name,
            losthead_walk_name,
            losthead_attack_name,
            die_name,
            boomdie_name,
        ]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.helmet_walk_frames


class BucketHeadDuckyTubeZombie(Zombie):
    def __init__(self, x, y, head_group):
        Zombie.__init__(
            self,
            x,
            y,
            c.BUCKETHEAD_DUCKY_TUBE_ZOMBIE,
            head_group,
            helmet_health=c.BUCKETHEAD_HEALTH,
            can_swim=True,
        )

    def loadImages(self):
        self.helmet_walk_frames = []
        self.walk_frames = []
        self.helmet_swim_frames = []
        self.swim_frames = []
        self.helmet_attack_frames = []
        self.attack_frames = []
        self.losthead_walk_frames = []
        self.losthead_attack_frames = []
        self.die_frames = []
        self.boomdie_frames = []

        helmet_walk_name = self.name
        helmet_swim_name = self.name + 'Swim'
        helmet_attack_name = self.name + 'Attack'
        walk_name = c.DUCKY_TUBE_ZOMBIE
        swim_name = c.DUCKY_TUBE_ZOMBIE + 'Swim'
        attack_name = c.DUCKY_TUBE_ZOMBIE + 'Attack'
        losthead_walk_name = c.DUCKY_TUBE_ZOMBIE + 'LostHead'
        losthead_attack_name = c.DUCKY_TUBE_ZOMBIE + 'LostHead'
        die_name = c.DUCKY_TUBE_ZOMBIE + 'Die'
        boomdie_name = c.BOOMDIE

        frame_list = [
            self.helmet_walk_frames,
            self.helmet_swim_frames,
            self.helmet_attack_frames,
            self.walk_frames,
            self.swim_frames,
            self.attack_frames,
            self.losthead_walk_frames,
            self.losthead_attack_frames,
            self.die_frames,
            self.boomdie_frames,
        ]
        name_list = [
            helmet_walk_name,
            helmet_swim_name,
            helmet_attack_name,
            walk_name,
            swim_name,
            attack_name,
            losthead_walk_name,
            losthead_attack_name,
            die_name,
            boomdie_name,
        ]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.helmet_walk_frames


class ScreenDoorZombie(Zombie):
    def __init__(self, x, y, head_group):
        Zombie.__init__(
            self,
            x,
            y,
            c.SCREEN_DOOR_ZOMBIE,
            head_group,
            helmet_type2_health=c.SCREEN_DOOR_HEALTH,
        )

    def loadImages(self):
        self.helmet_walk_frames = []
        self.helmet_attack_frames = []
        self.walk_frames = []
        self.attack_frames = []
        self.losthead_walk_frames = []
        self.losthead_attack_frames = []
        self.die_frames = []
        self.boomdie_frames = []

        helmet_walk_name = self.name
        helmet_attack_name = self.name + 'Attack'
        walk_name = c.NORMAL_ZOMBIE
        attack_name = c.NORMAL_ZOMBIE + 'Attack'
        losthead_walk_name = c.NORMAL_ZOMBIE + 'LostHead'
        losthead_attack_name = c.NORMAL_ZOMBIE + 'LostHeadAttack'
        die_name = c.NORMAL_ZOMBIE + 'Die'
        boomdie_name = c.BOOMDIE

        frame_list = [
            self.helmet_walk_frames,
            self.helmet_attack_frames,
            self.walk_frames,
            self.attack_frames,
            self.losthead_walk_frames,
            self.losthead_attack_frames,
            self.die_frames,
            self.boomdie_frames,
        ]
        name_list = [
            helmet_walk_name,
            helmet_attack_name,
            walk_name,
            attack_name,
            losthead_walk_name,
            losthead_attack_name,
            die_name,
            boomdie_name,
        ]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.helmet_walk_frames


class PoleVaultingZombie(Zombie):
    def __init__(self, x, y, head_group):
        Zombie.__init__(
            self,
            x,
            y,
            c.POLE_VAULTING_ZOMBIE,
            head_group=head_group,
            body_health=c.POLE_VAULTING_HEALTH,
            losthead_health=c.POLE_VAULTING_LOSTHEAD_HEALTH,
        )
        self.speed = 1.88
        self.jumped = False
        self.jumping = False

    def loadImages(self):
        self.walk_frames = []
        self.attack_frames = []
        self.losthead_walk_frames = []
        self.losthead_attack_frames = []
        self.die_frames = []
        self.boomdie_frames = []
        self.walk_before_jump_frames = []
        self.jump_frames = []

        walk_name = self.name + 'WalkAfterJump'
        attack_name = self.name + 'Attack'
        losthead_walk_name = self.name + 'LostHead'
        losthead_attack_name = self.name + 'LostHeadAttack'
        die_name = self.name + 'Die'
        boomdie_name = c.BOOMDIE
        walk_before_jump_name = self.name
        jump_name = self.name + 'Jump'

        frame_list = [
            self.walk_frames,
            self.attack_frames,
            self.losthead_walk_frames,
            self.losthead_attack_frames,
            self.die_frames,
            self.boomdie_frames,
            self.walk_before_jump_frames,
            self.jump_frames,
        ]
        name_list = [
            walk_name,
            attack_name,
            losthead_walk_name,
            losthead_attack_name,
            die_name,
            boomdie_name,
            walk_before_jump_name,
            jump_name,
        ]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.walk_before_jump_frames

    def setJump(self, successfullyJumped, jump_x):
        if not self.jumping:
            self.jumping = True
            self.changeFrames(self.jump_frames)
            self.successfullyJumped = successfullyJumped
            self.jump_x = jump_x
            # 播放跳跃音效
            c.SOUND_POLEVAULT_JUMP.play()

    def animation(self):
        if self.state == c.FREEZE:
            self.image.set_alpha(192)
            return

        if (self.current_time - self.animate_timer) > (
            self.animate_interval * self.getTimeRatio()
        ):
            self.frame_index += 1
            if self.state == c.WALK:
                if self.jumping and (not self.jumped):
                    if self.successfullyJumped:
                        self.rect.x -= 5
                    else:
                        self.rect.x -= 1
            if self.frame_index >= self.frame_num:
                if self.state == c.DIE:
                    self.kill()
                    return
                self.frame_index = 0
                if self.jumping and (not self.jumped):
                    self.changeFrames(self.walk_frames)
                    if self.successfullyJumped:
                        self.rect.centerx = self.jump_x
                    self.jumped = True
                    self.speed = 1.04
            self.animate_timer = self.current_time

        self.image = self.frames[self.frame_index]
        if self.is_hypno:
            self.image = pg.transform.flip(self.image, True, False)
        self.mask = pg.mask.from_surface(self.image)
        if (self.current_time - self.hit_timer) >= 200:
            self.image.set_alpha(255)
        else:
            self.image.set_alpha(192)

    def setWalk(self):
        self.state = c.WALK
        self.animate_interval = self.walk_animate_interval
        if self.jumped:
            self.changeFrames(self.walk_frames)

    def setFreeze(self, ice_trap_image):
        # 起跳但是没有落地时不设置冰冻
        if self.jumping and (not self.jumped):
            self.ice_slow_timer = self.current_time
            self.ice_slow_ratio = 2
        else:
            self.freeze_timer = self.current_time
            self.old_state = self.state
            self.state = c.FREEZE
            self.ice_trap_image = ice_trap_image
            self.ice_trap_rect = ice_trap_image.get_rect()
            self.ice_trap_rect.centerx = self.rect.centerx
            self.ice_trap_rect.bottom = self.rect.bottom


# 注意:冰车僵尸移动变速
class Zomboni(Zombie):
    def __init__(self, x, y, plant_group, map, IceFrozenPlot):
        Zombie.__init__(self, x, y, c.ZOMBONI, body_health=c.ZOMBONI_HEALTH)
        self.plant_group = plant_group
        self.map = map
        self.IceFrozenPlot = IceFrozenPlot
        self.die_animate_interval = 70
        self.boomDie_animate_interval = 150
        # 播放冰车生成音效
        c.SOUND_ZOMBONI.play()

    def loadImages(self):
        self.walk_frames = []
        self.walk_damaged1_frames = []
        self.walk_damaged2_frames = []
        self.losthead_walk_frames = []
        self.die_frames = []
        self.boomdie_frames = []

        walk_name = self.name
        walk_damaged1_name = self.name + 'Damaged1'
        walk_damaged2_name = self.name + 'Damaged2'
        losthead_walk_name = self.name + 'Damaged2'
        die_name = self.name + 'Die'
        boomdie_name = self.name + 'BoomDie'

        frame_list = [
            self.walk_frames,
            self.walk_damaged1_frames,
            self.walk_damaged2_frames,
            self.losthead_walk_frames,
            self.die_frames,
            self.boomdie_frames,
        ]
        name_list = [
            walk_name,
            walk_damaged1_name,
            walk_damaged2_name,
            losthead_walk_name,
            die_name,
            boomdie_name,
        ]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.walk_frames

    def updateIceSlow(self):
        # 冰车僵尸不可冰冻
        self.ice_slow_ratio = 1

    def setFreeze(self, ice_trap_image):
        pass

    def walking(self):
        if self.checkToDie(self.losthead_walk_frames):
            return

        if self.health <= c.ZOMBONI_DAMAGED2_HEALTH:
            self.changeFrames(self.walk_damaged2_frames)
        elif self.health <= c.ZOMBONI_DAMAGED1_HEALTH:
            self.changeFrames(self.walk_damaged1_frames)

        if (self.current_time - self.walk_timer) > (
            c.ZOMBIE_WALK_INTERVAL * self.getTimeRatio()
        ) and (not self.losthead):
            self.walk_timer = self.current_time
            if self.is_hypno:
                self.rect.x += 1
            else:
                self.rect.x -= 1

            # 行进时碾压
            for plant in self.plant_group:
                # 地刺和地刺王不用检验
                if (plant.name not in {c.SPIKEWEED}) and (
                    self.rect.centerx <= plant.rect.right <= self.rect.right
                ):
                    # 扣除生命值为可能的最大有限生命值
                    plant.health -= 8000

            # 造冰
            map_x, map_y = self.map.getMapIndex(
                self.rect.right - 40, self.rect.bottom
            )
            if 0 <= map_x < c.GRID_X_LEN:
                if c.ICEFROZENPLOT not in self.map.map[map_y][map_x]:
                    x, y = self.map.getMapGridPos(map_x, map_y)
                    self.plant_group.add(self.IceFrozenPlot(x, y))
                    self.map.map[map_y][map_x][c.MAP_PLANT].add(
                        c.ICEFROZENPLOT
                    )

            self.speed = max(0.6, 1.5 - (c.GRID_X_LEN + 1 - map_x) * 0.225)

    def setDie(self):
        self.state = c.DIE
        self.animate_interval = self.die_animate_interval
        self.changeFrames(self.die_frames)
        # 播放冰车爆炸音效
        c.SOUND_ZOMBONI_EXPLOSION.play()


class SnorkelZombie(Zombie):
    def __init__(self, x, y, head_group):
        Zombie.__init__(self, x, y, c.SNORKELZOMBIE, can_swim=True)
        self.speed = 1.6
        self.walk_animate_interval = 50
        self.canSetAttack = True

    def loadImages(self):
        self.walk_frames = []
        self.swim_frames = []
        self.attack_frames = []
        self.jump_frames = []
        self.float_frames = []
        self.sink_frames = []
        self.losthead_walk_frames = []
        self.losthead_attack_frames = []
        self.die_frames = []
        self.boomdie_frames = []

        walk_name = self.name
        swim_name = self.name + 'Dive'
        attack_name = self.name + 'Attack'
        jump_name = self.name + 'Jump'
        float_name = self.name + 'Float'
        sink_name = self.name + 'Sink'
        losthead_walk_name = self.name + 'LostHead'
        losthead_attack_name = self.name + 'LostHeadAttack'
        die_name = self.name + 'Die'
        boomdie_name = c.BOOMDIE

        frame_list = [
            self.walk_frames,
            self.swim_frames,
            self.attack_frames,
            self.jump_frames,
            self.float_frames,
            self.sink_frames,
            self.losthead_walk_frames,
            self.losthead_attack_frames,
            self.die_frames,
            self.boomdie_frames,
        ]
        name_list = [
            walk_name,
            swim_name,
            attack_name,
            jump_name,
            float_name,
            sink_name,
            losthead_walk_name,
            losthead_attack_name,
            die_name,
            boomdie_name,
        ]

        for i, name in enumerate(name_list):
            self.loadFrames(frame_list[i], name)

        self.frames = self.walk_frames

    def walking(self):
        if self.checkToDie(self.losthead_walk_frames):
            return

        # 在水池范围内
        # 在右侧岸左
        if self.rect.centerx <= c.MAP_POOL_FRONT_X - 25:
            # 在左侧岸右,左侧岸位置为预估
            if self.rect.right - 25 >= c.MAP_POOL_OFFSET_X:
                # 还未进入游泳状态
                if not self.swimming:
                    self.swimming = True
                    self.changeFrames(self.jump_frames)
                    self.speed = 1.175
                    # 播放入水音效
                    c.SOUND_ZOMBIE_ENTERING_WATER.play()
            # 已经接近家门口并且上岸
            else:
                if self.swimming:
                    self.changeFrames(self.walk_frames)
                    self.speed = 1.6
                    self.swimming = False
        # 被魅惑时走到岸上需要起立
        elif self.is_hypno and (
            self.rect.right > c.MAP_POOL_FRONT_X + 55
        ):   # 常数拟合暂时缺乏检验
            if self.swimming:
                self.speed = 1.6
                self.changeFrames(self.walk_frames)
            self.swimming = False
        if (self.current_time - self.walk_timer) > (
            c.ZOMBIE_WALK_INTERVAL * self.getTimeRatio()
        ):
            self.handleGarlicYChange()
            self.walk_timer = self.current_time
            # 正在上浮或者下潜不用移动
            if (self.frames == self.float_frames) or (
                self.frames == self.sink_frames
            ):
                pass
            elif self.is_hypno:
                self.rect.x += 1
            else:
                self.rect.x -= 1

    def animation(self):
        if self.state == c.FREEZE:
            self.image.set_alpha(192)
            return

        if (self.current_time - self.animate_timer) > (
            self.animate_interval * self.getTimeRatio()
        ):
            self.frame_index += 1
            if self.frame_index >= self.frame_num:
                if self.state == c.DIE:
                    self.kill()
                    return
                elif self.frames == self.jump_frames:
                    self.changeFrames(self.swim_frames)
                elif self.frames == self.sink_frames:
                    self.changeFrames(self.swim_frames)
                    # 还需要改回原来的可进入攻击状态的设定
                    self.canSetAttack = True
                elif self.frames == self.float_frames:
                    self.state = c.ATTACK
                    self.attack_timer = self.current_time
                    self.changeFrames(self.attack_frames)
                self.frame_index = 0
            self.animate_timer = self.current_time

        self.image = self.frames[self.frame_index]
        if self.is_hypno:
            self.image = pg.transform.flip(self.image, True, False)
        self.mask = pg.mask.from_surface(self.image)

        if (self.current_time - self.hit_timer) >= 200:
            self.image.set_alpha(255)
        else:
            self.image.set_alpha(192)

    # 注意潜水僵尸较为特殊:这里的setAttack并没有直接触发攻击状态,而是触发从水面浮起
    def setAttack(self, prey, is_plant=True):
        self.prey = prey  # prey can be plant or other zombies
        self.prey_is_plant = is_plant
        self.animate_interval = self.attack_animate_interval

        if self.losthead:
            self.changeFrames(self.losthead_attack_frames)
        elif self.canSetAttack:
            self.changeFrames(self.float_frames)
            self.canSetAttack = False

    def setWalk(self):
        self.state = c.WALK
        self.animate_interval = self.walk_animate_interval
        self.swimming = True
        self.changeFrames(self.sink_frames)


================================================
FILE: source/constants.py
================================================
import os

import pygame as pg

# 用户数据及日志存储路径
if os.name == 'nt':   # Windows系统存储路径
    USERDATA_PATH = os.path.expandvars(
        os.path.join('%APPDATA%', 'pypvz', 'userdata.json')
    )
    USERLOG_PATH = os.path.expandvars(
        os.path.join('%APPDATA%', 'pypvz', 'run.log')
    )
else:   # 非Windows系统存储路径
    USERDATA_PATH = os.path.expanduser(
        os.path.join('~', '.config', 'pypvz', 'userdata.json')
    )
    USERLOG_PATH = os.path.expanduser(
        os.path.join('~', '.config', 'pypvz', 'run.log')
    )

# 游戏图片资源路径
PATH_IMG_DIR = os.path.join(
    os.path.dirname(os.path.dirname(__file__)), 'resources', 'graphics'
)
# 游戏音乐文件夹路径
PATH_MUSIC_DIR = os.path.join(
    os.path.dirname(os.path.dirname(__file__)), 'resources', 'music'
)
# 窗口图标
ORIGINAL_LOGO = os.path.join(
    os.path.dirname(os.path.dirname(__file__)), 'pypvz-exec-logo.png'
)
# 字体路径
FONT_PATH = os.path.join(
    os.path.dirname(os.path.dirname(__file__)),
    'resources',
    'DroidSansFallback.ttf',
)

# 窗口标题
ORIGINAL_CAPTION = 'pypvz'

# 游戏模式
GAME_MODE = 'mode'
MODE_ADVENTURE = 'adventure'
MODE_LITTLEGAME = 'littleGame'

# 窗口大小
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
SCREEN_SIZE = (SCREEN_WIDTH, SCREEN_HEIGHT)


# 选卡数量
# 最大数量
CARD_MAX_NUM = 10   # 这里以后可以增加解锁功能,从最初的6格逐渐解锁到10格
# 最小数量
CARD_LIST_NUM = CARD_MAX_NUM

# 方格数据
# 一般
GRID_X_LEN = 9
GRID_Y_LEN = 5
GRID_X_SIZE = 80
GRID_Y_SIZE = 100
# 带有泳池
GRID_POOL_X_LEN = GRID_X_LEN
GRID_POOL_Y_LEN = 6
GRID_POOL_X_SIZE = GRID_X_SIZE
GRID_POOL_Y_SIZE = 85
# 屋顶
GRID_ROOF_X_LEN = GRID_X_LEN
GRID_ROOF_Y_LEN = GRID_Y_LEN
GRID_ROOF_X_SIZE = GRID_X_SIZE
GRID_ROOF_Y_SIZE = 85

# 颜色
WHITE = (255, 255, 255)
NAVYBLUE = (60, 60, 100)
SKY_BLUE = (39, 145, 251)
BLACK = (0, 0, 0)
LIGHTYELLOW = (234, 233, 171)
RED = (255, 0, 0)
PURPLE = (255, 0, 255)
GOLD = (255, 215, 0)
GREEN = (0, 255, 0)
YELLOWGREEN = (55, 200, 0)
LIGHTGRAY = (107, 108, 145)
PARCHMENT_YELLOW = (207, 146, 83)

# 退出游戏按钮
EXIT = 'exit'
HELP = 'help'
# 游戏界面可选的菜单
LITTLE_MENU = 'littleMenu'
BIG_MENU = 'bigMenu'
RESTART_BUTTON = 'restartButton'
MAINMENU_BUTTON = 'mainMenuButton'
LITTLEGAME_BUTTON = 'littleGameButton'
OPTION_BUTTON = 'optionButton'
SOUND_VOLUME_BUTTON = 'volumeButton'
UNIVERSAL_BUTTON = 'universalButton'
# 金银向日葵奖杯
TROPHY_SUNFLOWER = 'sunflowerTrophy'
# 小铲子
SHOVEL = 'shovel'
SHOVEL_BOX = 'shovelBox'
# 一大波僵尸来袭图片
HUGE_WAVE_APPROCHING = 'Approching'
# 关卡进程图片
LEVEL_PROGRESS_BAR = 'LevelProgressBar'
LEVEL_PROGRESS_ZOMBIE_HEAD = 'LevelProgressZombieHead'
LEVEL_PROGRESS_FLAG = 'LevelProgressFlag'


# GAME INFO字典键值
CURRENT_TIME = 'current time'
PASSED_ALL = 'passed all'   # 已完成该模式下的所有游戏,应当显示向日葵奖杯获得界面
LEVEL_NUM = 'level num'
LITTLEGAME_NUM = 'littleGame num'
LEVEL_COMPLETIONS = 'level completions'
LITTLEGAME_COMPLETIONS = 'littleGame completions'
GAME_RATE = 'game rate'
SOUND_VOLUME = 'volume'

# 整个游戏的状态
MAIN_MENU = 'main menu'
LOAD_SCREEN = 'load screen'
GAME_LOSE = 'game lose'
GAME_VICTORY = 'game victory'
LEVEL = 'level'
AWARD_SCREEN = 'award screen'
HELP_SCREEN = 'help screen'

# 界面图片文件名
MAIN_MENU_IMAGE = 'MainMenu'
OPTION_ADVENTURE = 'Adventure'
GAME_LOSE_IMAGE = 'GameLose'
GAME_VICTORY_IMAGE = 'GameVictory'
AWARD_SCREEN_IMAGE = 'AwardScreen'
HELP_SCREEN_IMAGE = 'HelpScreen'

# 地图相关内容
BACKGROUND_NAME = 'Background'
BACKGROUND_TYPE = 'background_type'
INIT_SUN_NAME = 'init_sun_value'
ZOMBIE_LIST = 'zombie_list'
GAME_TITLE = 'title'

# 地图类型
BACKGROUND_DAY = 0
BACKGROUND_NIGHT = 1
BACKGROUND_POOL = 2
BACKGROUND_FOG = 3
BACKGROUND_ROOF = 4
BACKGROUND_ROOFNIGHT = 5
BACKGROUND_WALLNUTBOWLING = 6
BACKGROUND_SINGLE = 7
BACKGROUND_TRIPLE = 8

# 地图类型集合
# 白天场地(泛指蘑菇睡觉的场地)
DAYTIME_BACKGROUNDS = {
    BACKGROUND_DAY,
    BACKGROUND_POOL,
    BACKGROUND_ROOF,
    BACKGROUND_WALLNUTBOWLING,
    BACKGROUND_SINGLE,
    BACKGROUND_TRIPLE,
}

# 带有泳池的场地
POOL_EQUIPPED_BACKGROUNDS = {
    BACKGROUND_POOL,
    BACKGROUND_FOG,
}

# 屋顶上的场地
ON_ROOF_BACKGROUNDS = {
    BACKGROUND_ROOF,
    BACKGROUND_ROOFNIGHT,
}

# BACKGROUND_DAY场地的变体
BACKGROUND_DAY_LIKE_BACKGROUNDS = {
    BACKGROUND_DAY,
    BACKGROUND_SINGLE,
    BACKGROUND_TRIPLE,
}

# 夜晚地图的墓碑数量等级
GRADE_GRAVES = 'grade_graves'
# 不同墓碑等级对应的信息,列表位置对应的是墓碑等级
GRAVES_GRADE_INFO = (0, 4, 7, 11)

# 僵尸生成方式
SPAWN_ZOMBIES = 'spawn_zombies'
SPAWN_ZOMBIES_AUTO = 1
SPAWN_ZOMBIES_LIST = 0
INCLUDED_ZOMBIES = 'included_zombies'
NUM_FLAGS = 'num_flags'
INEVITABLE_ZOMBIE_DICT = 'inevitable_zombie_list'
SURVIVAL_ROUNDS = 'survival_rounds'

# 地图单元格属性
MAP_PLANT = 'plantnames'
MAP_SLEEP = 'sleep'   # 有没有休眠的蘑菇,作是否能种植咖啡豆的判断
MAP_PLOT_TYPE = 'plot_type'
# 地图单元格区域类型
MAP_GRASS = 'grass'
MAP_WATER = 'water'
MAP_TILE = 'tile'  # 指屋顶上的瓦片
MAP_UNAVAILABLE = 'unavailable'   # 指完全不能种植物的地方,包括无草皮的荒地和坚果保龄球等红线右侧

# 地图相关像素数据
BACKGROUND_OFFSET_X = 220
MAP_OFFSET_X = 35
MAP_OFFSET_Y = 100
MAP_POOL_OFFSET_X = 42
MAP_POOL_OFFSET_Y = 115
MAP_ROOF_OFFSET_X = 35  # 暂时还不清楚数据
MAP_ROOF_OFFSET_Y = 105   # 暂时还不清楚数据

# 泳池前端陆地部分
MAP_POOL_FRONT_X = SCREEN_WIDTH - 15

# 植物选择菜单栏、传送带菜单栏等类型设定
CHOOSEBAR_TYPE = 'choosebar_type'
CHOOSEBAR_STATIC = 0
CHOOSEBAR_MOVE = 1
CHOOSEBAR_BOWLING = 2
MENUBAR_BACKGROUND = 'ChooserBackground'
MOVEBAR_BACKGROUND = 'MoveBackground'
PANEL_BACKGROUND = 'PanelBackground'
START_BUTTON = 'StartButton'
CARD_POOL = 'card_pool'

# 关于植物栏的像素设置
PANEL_Y_START = 87
PANEL_X_START = 22
PANEL_Y_INTERNAL = 69
PANEL_X_INTERNAL = 53
BAR_CARD_X_INTERNAL = 51

# 植物卡片信息索引
PLANT_NAME_INDEX = 0
CARD_INDEX = 1
SUN_INDEX = 2
FROZEN_TIME_INDEX = 3

# 传送带模式中的刷新间隔和移动速率
MOVEBAR_CARD_FRESH_TIME = 6000
CARD_MOVE_TIME = 60

# 其他显示物
CAR = 'car'
SUN = 'Sun'

# plant子类非植物对象(这里的是不包括阳光、子弹的拟植物对象)
NON_PLANT_OBJECTS = {
    HOLE := 'Hole',
    ICEFROZENPLOT := 'IceFrozenPlot',
    GRAVE := 'Grave',
}

# 植物相关信息
PLANT_IMAGE_RECT = 'plant_image_rect'
BOOM_IMAGE = 'Boom'

# 植物卡片信息汇总(包括植物名称, 卡片名称, 阳光, 冷却时间)
PLANT_CARD_INFO = (  # 元组 (植物名称, 卡片名称, 阳光, 冷却时间)
    (
        PEASHOOTER := 'Peashooter',
        CARD_PEASHOOTER := 'card_peashooter',
        100,
        7500,
    ),
    (SUNFLOWER := 'SunFlower', CARD_SUNFLOWER := 'card_sunflower', 50, 7500),
    (
        CHERRYBOMB := 'CherryBomb',
        CARD_CHERRYBOMB := 'card_cherrybomb',
        150,
        50000,
    ),
    (WALLNUT := 'WallNut', CARD_WALLNUT := 'card_wallnut', 50, 30000),
    (
        POTATOMINE := 'PotatoMine',
        CARD_POTATOMINE := 'card_potatomine',
        25,
        30000,
    ),
    (
        SNOWPEASHOOTER := 'SnowPea',
        CARD_SNOWPEASHOOTER := 'card_snowpea',
        175,
        7500,
    ),
    (CHOMPER := 'Chomper', CARD_CHOMPER := 'card_chomper', 150, 7500),
    (
        REPEATERPEA := 'RepeaterPea',
        CARD_REPEATERPEA := 'card_repeaterpea',
        200,
        7500,
    ),
    (
        PUFFSHROOM := 'PuffShroom',
        CARD_PUFFSHROOM := 'card_puffshroom',
        0,
        7500,
    ),
    (SUNSHROOM := 'SunShroom', CARD_SUNSHROOM := 'card_sunshroom', 25, 7500),
    (
        FUMESHROOM := 'FumeShroom',
        CARD_FUMESHROOM := 'card_fumeshroom',
        75,
        7500,
    ),
    (
        GRAVEBUSTER := 'GraveBuster',
        CARD_GRAVEBUSTER := 'card_gravebuster',
        75,
        7500,
    ),
    (
        HYPNOSHROOM := 'HypnoShroom',
        CARD_HYPNOSHROOM := 'card_hypnoshroom',
        75,
        30000,
    ),
    (
        SCAREDYSHROOM := 'ScaredyShroom',
        CARD_SCAREDYSHROOM := 'card_scaredyshroom',
        25,
        7500,
    ),
    (ICESHROOM := 'IceShroom', CARD_ICESHROOM := 'card_iceshroom', 75, 50000),
    (
        DOOMSHROOM := 'DoomShroom',
        CARD_DOOMSHROOM := 'card_doomshroom',
        125,
        50000,
    ),
    (LILYPAD := 'LilyPad', CARD_LILYPAD := 'card_lilypad', 25, 7500),
    (SQUASH := 'Squash', CARD_SQUASH := 'card_squash', 50, 30000),
    (
        TANGLEKLEP := 'TangleKlep',
        CARD_TANGLEKLEP := 'card_tangleklep',
        25,
        30000,
    ),
    (
        THREEPEASHOOTER := 'Threepeater',
        CARD_THREEPEASHOOTER := 'card_threepeashooter',
        325,
        7500,
    ),
    (JALAPENO := 'Jalapeno', CARD_JALAPENO := 'card_jalapeno', 125, 50000),
    (SPIKEWEED := 'Spikeweed', CARD_SPIKEWEED := 'card_spikeweed', 100, 7500),
    (TORCHWOOD := 'TorchWood', CARD_TORCHWOOD := 'card_torchwood', 175, 7500),
    (TALLNUT := 'TallNut', CARD_TALLNUT := 'card_tallnut', 125, 30000),
    (SEASHROOM := 'SeaShroom', CARD_SEASHROOM := 'card_seashroom', 0, 30000),
    (STARFRUIT := 'StarFruit', CARD_STARFRUIT := 'card_starfruit', 125, 7500),
    (
        PUMPKINHEAD := 'PumpkinHead',
        CARD_PUMPKINHEAD := 'card_pumpkinhead',
        125,
        30000,
    ),
    (
        COFFEEBEAN := 'CoffeeBean',
        CARD_COFFEEBEAN := 'card_coffeebean',
        75,
        7500,
    ),
    (GARLIC := 'Garlic', CARD_GARLIC := 'card_garlic', 50, 7500),
    # 应当保证这3个在一般模式下不可选的特殊植物恒在最后
    (WALLNUTBOWLING := 'WallNutBowling', CARD_WALLNUT := 'card_wallnut', 0, 0),
    (
        REDWALLNUTBOWLING := 'RedWallNutBowling',
        CARD_REDWALLNUT := 'card_redwallnut',
        0,
        0,
    ),
    (
        GIANTWALLNUT := 'GiantWallNut',
        CARD_GIANTWALLNUT := 'card_giantwallnut',
        0,
        0,
    ),
)

# 卡片中的植物名称与索引序号的对应关系,指定名称以得到索引值
PLANT_CARD_INDEX = {
    item[PLANT_NAME_INDEX]: index
    for (index, item) in enumerate(PLANT_CARD_INFO)
}

# 指定了哪些卡可选(排除坚果保龄球特殊植物)
CARDS_TO_CHOOSE = range(len(PLANT_CARD_INFO) - 3)


# 植物集体属性集合
# 也许以后有必要的可以重新加入到对象的属性中
# 在生效时不用与僵尸进行碰撞检测的对象(即生效时不可发生被僵尸啃食的事件)
SKIP_ZOMBIE_COLLISION_CHECK_WHEN_WORKING = {
    # 注意爆炸坚果的触发也是啃食类碰撞,因此只能算作爆炸后不检测
    SQUASH,
    ICESHROOM,
    REDWALLNUTBOWLING,
    CHERRYBOMB,
    JALAPENO,
    DOOMSHROOM,
    POTATOMINE,
}

# 所有可能不用与僵尸进行碰撞检测的对象
CAN_SKIP_ZOMBIE_COLLISION_CHECK = (  # 这里运用了集合运算
    # 注意这个外围的小括号是用来换行的
    # 各个部分末!尾!千!万!不!能!加!逗!号!!!
    # 生效时不检测的植物
    SKIP_ZOMBIE_COLLISION_CHECK_WHEN_WORKING
    |
    # 非植物对象
    NON_PLANT_OBJECTS
    |
    # 地刺类
    {
        SPIKEWEED,
    }
)

# 死亡时不触发音效的对象
PLANT_DIE_SOUND_EXCEPTIONS = {
    WALLNUTBOWLING,
    TANGLEKLEP,
    ICEFROZENPLOT,
    HOLE,
    GRAVE,
    JALAPENO,
    REDWALLNUTBOWLING,
    CHERRYBOMB,
    GIANTWALLNUT,
}

# 直接水生植物
WATER_PLANTS = {
    LILYPAD,
    SEASHROOM,
    TANGLEKLEP,
}

# 攻击状态检查类型
CHECK_ATTACK_NEVER = 0
CHECK_ATTACK_ALWAYS = 1

# 范围爆炸植物,即灰烬植物与寒冰菇
ASH_PLANTS_AND_ICESHROOM = {
    REDWALLNUTBOWLING,
    CHERRYBOMB,
    JALAPENO,
    DOOMSHROOM,
    ICESHROOM,
}

# 白天要睡觉的植物
CAN_SLEEP_PLANTS = {
    PUFFSHROOM,
    SUNSHROOM,
    FUMESHROOM,
    HYPNOSHROOM,
    SCAREDYSHROOM,
    ICESHROOM,
    DOOMSHROOM,
    SEASHROOM,
}

# 选卡不推荐选择理由
REASON_WILL_SLEEP = 1
REASON_SLEEP_BUT_COFFEE_BEAN = 2
REASON_OTHER = 3

# 植物生命值
PLANT_HEALTH = 300
WALLNUT_HEALTH = 4000
WALLNUT_CRACKED1_HEALTH = WALLNUT_HEALTH // 3 * 2
WALLNUT_CRACKED2_HEALTH = WALLNUT_HEALTH // 3
TALLNUT_HEALTH = 8000
TALLNUT_CRACKED1_HEALTH = TALLNUT_HEALTH // 3 * 2
TALLNUT_CRACKED2_HEALTH = TALLNUT_HEALTH // 3
GARLIC_HEALTH = 450
GARLIC_CRACKED1_HEALTH = GARLIC_HEALTH // 3 * 2
GARLIC_CRACKED2_HEALTH = GARLIC_HEALTH // 3
# 坚果保龄球攻击伤害
WALLNUT_BOWLING_DAMAGE = 550

# 阳光生成属性
PRODUCE_SUN_INTERVAL = 4250   # 基准
FLOWER_SUN_INTERVAL = 24000
SUN_LIVE_TIME = 10000
SUN_VALUE = 25

# 僵尸冷冻
ICE_SLOW_TIME = 10000
MIN_FREEZE_TIME = 4000
ICETRAP = 'IceTrap'

# 子弹信息
# 子弹类型
BULLET_PEA = 'PeaNormal'
BULLET_PEA_ICE = 'PeaIce'
BULLET_FIREBALL = 'Fireball'
BULLET_MUSHROOM = 'BulletMushRoom'
BULLET_SEASHROOM = 'BulletSeaShroom'
FUME = 'Fume'
# 子弹伤害
BULLET_DAMAGE_NORMAL = 20
BULLET_DAMAGE_FIREBALL_BODY = 27   # 这是火球本体的伤害,注意不是40,本体(27) + 溅射(13)才是40
BULLET_DAMAGE_FIREBALL_RANGE = 13   # 原版溅射伤害会随着僵尸数量增多而减少,这里相当于做了一个增强
# 子弹效果
BULLET_EFFECT_ICE = 'ice'
BULLET_EFFECT_UNICE = 'unice'

# 特殊子弹
# 杨桃子弹
# 子弹名称
BULLET_STAR = 'StarBullet'
# 子弹方向
STAR_FORWARD_UP = 'forwardUp'   # 向前上方
STAR_FORWARD_DOWN = 'forwardDown'   # 向前下方
STAR_BACKWARD = 'backward'  # 向后
STAR_UPWARD = 'upward'  # 向上
STAR_DOWNWARD = 'downward'  # 向下

# 有爆炸图片的子弹
BULLET_INDEPENDENT_BOOM_IMG = {
    BULLET_PEA,
    BULLET_PEA_ICE,
    BULLET_MUSHROOM,
    BULLET_SEASHROOM,
    BULLET_STAR,
}

# 僵尸信息
ZOMBIE_IMAGE_RECT = 'zombie_image_rect'
ZOMBIE_HEAD = 'ZombieHead'
NORMAL_ZOMBIE = 'Zombie'
CONEHEAD_ZOMBIE = 'ConeheadZombie'
BUCKETHEAD_ZOMBIE = 'BucketheadZombie'
FLAG_ZOMBIE = 'FlagZombie'
NEWSPAPER_ZOMBIE = 'NewspaperZombie'
FOOTBALL_ZOMBIE = 'FootballZombie'
DUCKY_TUBE_ZOMBIE = 'DuckyTubeZombie'
CONEHEAD_DUCKY_TUBE_ZOMBIE = 'ConeheadDuckyTubeZombie'
BUCKETHEAD_DUCKY_TUBE_ZOMBIE = 'BucketheadDuckyTubeZombie'
SCREEN_DOOR_ZOMBIE = 'ScreenDoorZombie'
POLE_VAULTING
Download .txt
gitextract_dzufbwce/

├── .github/
│   └── workflows/
│       ├── build-pr.yml
│       └── build.yml
├── .gitignore
├── .python-version
├── README.md
├── pyproject.toml
├── pypvz.py
├── resources/
│   ├── music/
│   │   ├── battle.opus
│   │   ├── bowling.opus
│   │   ├── chooseYourSeeds.opus
│   │   ├── dayLevel.opus
│   │   ├── fogLevel.opus
│   │   ├── intro.opus
│   │   ├── nightLevel.opus
│   │   ├── poolLevel.opus
│   │   └── zenGarden.opus
│   └── sound/
│       ├── bigchomp.ogg
│       ├── bomb.ogg
│       ├── bowlingimpact.ogg
│       ├── bulletExplode.ogg
│       ├── buttonclick.ogg
│       ├── cannotChooseWarning.ogg
│       ├── carWalking.ogg
│       ├── clickCard.ogg
│       ├── collectSun.ogg
│       ├── doomshroom.ogg
│       ├── evillaugh.ogg
│       ├── finalfanfare.ogg
│       ├── firepea.ogg
│       ├── freeze.ogg
│       ├── fume.ogg
│       ├── gravebusterchomp.ogg
│       ├── helpScreen.ogg
│       ├── hugeWaveApproching.ogg
│       ├── hypnoed.ogg
│       ├── lose.ogg
│       ├── mushroomWakeup.ogg
│       ├── newspaperRip.ogg
│       ├── newspaperZombieAngry.ogg
│       ├── plant.ogg
│       ├── plantDie.ogg
│       ├── plantGrow.ogg
│       ├── polevaultjump.ogg
│       ├── potatomine.ogg
│       ├── puff.ogg
│       ├── scream.ogg
│       ├── shoot.ogg
│       ├── shovel.ogg
│       ├── snowPeaSparkles.ogg
│       ├── squashHmm.ogg
│       ├── squashing.ogg
│       ├── tangleKelpDrag.ogg
│       ├── tap.ogg
│       ├── win.ogg
│       ├── zombieAttack.ogg
│       ├── zombieComing.ogg
│       ├── zombieEnteringWater.ogg
│       ├── zombieVoice.ogg
│       ├── zomboni.ogg
│       └── zomboniExplosion.ogg
└── source/
    ├── __init__.py
    ├── component/
    │   ├── __init__.py
    │   ├── map.py
    │   ├── menubar.py
    │   ├── plant.py
    │   └── zombie.py
    ├── constants.py
    ├── state/
    │   ├── __init__.py
    │   ├── level.py
    │   ├── mainmenu.py
    │   └── screen.py
    └── tool.py
Download .txt
SYMBOL INDEX (467 symbols across 9 files)

FILE: source/component/map.py
  class Map (line 7) | class Map:
    method __init__ (line 8) | def __init__(self, background_type: int):
    method isValid (line 67) | def isValid(self, map_x: int, map_y: int) -> bool:
    method initMapGrid (line 75) | def initMapGrid(self, plot_type: str) -> set:
    method isAvailable (line 85) | def isAvailable(self, map_x: int, map_y: int, plant_name: str) -> bool:
    method getMapIndex (line 190) | def getMapIndex(self, x: int, y: int) -> tuple[int, int]:
    method getMapGridPos (line 209) | def getMapGridPos(self, map_x: int, map_y: int) -> tuple[int, int]:
    method setMapGridType (line 237) | def setMapGridType(self, map_x: int, map_y: int, plot_type: str):
    method addMapPlant (line 240) | def addMapPlant(
    method removeMapPlant (line 246) | def removeMapPlant(self, map_x: int, map_y: int, plant_name: str):
    method getRandomMapIndex (line 249) | def getRandomMapIndex(self) -> tuple[int, int]:
    method checkPlantToSeed (line 254) | def checkPlantToSeed(

FILE: source/component/menubar.py
  function getSunValueImage (line 9) | def getSunValueImage(sun_value):
  function getCardPool (line 27) | def getCardPool(data):
  class Card (line 35) | class Card:
    method __init__ (line 36) | def __init__(
    method loadFrame (line 79) | def loadFrame(self, name, scale):
    method checkMouseClick (line 89) | def checkMouseClick(self, mouse_pos):
    method canClick (line 98) | def canClick(self, sun_value, current_time):
    method canSelect (line 106) | def canSelect(self):
    method setSelect (line 109) | def setSelect(self, can_select):
    method setFrozenTime (line 128) | def setFrozenTime(self, current_time):
    method createShowImage (line 131) | def createShowImage(self, sun_value, current_time):
    method update (line 168) | def update(self, sun_value, current_time):
    method draw (line 173) | def draw(self, surface):
  class MenuBar (line 178) | class MenuBar:
    method __init__ (line 179) | def __init__(self, card_list, sun_value):
    method loadFrame (line 189) | def loadFrame(self, name):
    method update (line 196) | def update(self, current_time):
    method createImage (line 201) | def createImage(self, x, y, num):
    method setupCards (line 217) | def setupCards(self, card_list):
    method checkCardClick (line 225) | def checkCardClick(self, mouse_pos):
    method checkMenuBarClick (line 240) | def checkMenuBarClick(self, mouse_pos):
    method decreaseSunValue (line 249) | def decreaseSunValue(self, value):
    method increaseSunValue (line 252) | def increaseSunValue(self, value):
    method setCardFrozenTime (line 257) | def setCardFrozenTime(self, plant_name):
    method drawSunValue (line 263) | def drawSunValue(self):
    method draw (line 271) | def draw(self, surface):
  class Panel (line 279) | class Panel:
    method __init__ (line 280) | def __init__(self, card_list, sun_value, background_type=c.BACKGROUND_...
    method loadFrame (line 287) | def loadFrame(self, name):
    method loadImages (line 294) | def loadImages(self, sun_value):
    method setupCards (line 315) | def setupCards(self, card_list):
    method checkCardClick (line 350) | def checkCardClick(self, mouse_pos):
    method addCard (line 393) | def addCard(self, card: Card):
    method deleteCard (line 400) | def deleteCard(self, index):
    method checkStartButtonClick (line 403) | def checkStartButtonClick(self, mouse_pos):
    method getSelectedCards (line 415) | def getSelectedCards(self):
    method draw (line 421) | def draw(self, surface):
  class MoveCard (line 435) | class MoveCard:
    method __init__ (line 436) | def __init__(self, x, y, card_name, plant_name, scale=0.5):
    method loadFrame (line 450) | def loadFrame(self, name, scale):
    method checkMouseClick (line 461) | def checkMouseClick(self, mouse_pos):
    method createShowImage (line 470) | def createShowImage(self):
    method update (line 495) | def update(self, left_x, current_time):
    method draw (line 504) | def draw(self, surface):
  class MoveBar (line 509) | class MoveBar:
    method __init__ (line 510) | def __init__(self, card_pool):
    method loadFrame (line 524) | def loadFrame(self, name):
    method createCard (line 531) | def createCard(self):
    method update (line 552) | def update(self, current_time):
    method checkCardClick (line 563) | def checkCardClick(self, mouse_pos):
    method checkMenuBarClick (line 571) | def checkMenuBarClick(self, mouse_pos):
    method deleateCard (line 580) | def deleateCard(self, card):
    method draw (line 583) | def draw(self, surface):

FILE: source/component/plant.py
  class Car (line 9) | class Car(pg.sprite.Sprite):
    method __init__ (line 10) | def __init__(self, x: int, y: int, map_y: int):
    method update (line 24) | def update(self, game_info: dict):
    method setWalk (line 31) | def setWalk(self):
    method draw (line 37) | def draw(self, surface):
  class Bullet (line 42) | class Bullet(pg.sprite.Sprite):
    method __init__ (line 43) | def __init__(
    method loadFrames (line 80) | def loadFrames(self, frames, name):
    method load_images (line 98) | def load_images(self):
    method update (line 113) | def update(self, game_info):
    method setExplode (line 133) | def setExplode(self):
    method draw (line 147) | def draw(self, surface):
  class Fume (line 153) | class Fume(pg.sprite.Sprite):
    method __init__ (line 154) | def __init__(self, x, y):
    method load_images (line 167) | def load_images(self):
    method draw (line 176) | def draw(self, surface):
    method update (line 179) | def update(self, game_info):
    method loadFrames (line 189) | def loadFrames(self, frames, name):
  class StarBullet (line 200) | class StarBullet(Bullet):
    method __init__ (line 201) | def __init__(
    method update (line 225) | def update(self, game_info):
    method handleMapYPosition (line 253) | def handleMapYPosition(self):
  class Plant (line 270) | class Plant(pg.sprite.Sprite):
    method __init__ (line 271) | def __init__(self, x, y, name, health, bullet_group, scale=1):
    method loadFrames (line 296) | def loadFrames(self, frames, name, scale=1, color=c.BLACK):
    method loadImages (line 316) | def loadImages(self, name, scale):
    method changeFrames (line 319) | def changeFrames(self, frames):
    method update (line 333) | def update(self, game_info):
    method handleState (line 338) | def handleState(self):
    method idling (line 346) | def idling(self):
    method attacking (line 349) | def attacking(self):
    method digest (line 352) | def digest(self):
    method animation (line 355) | def animation(self):
    method canAttack (line 371) | def canAttack(self, zombie):
    method setAttack (line 385) | def setAttack(self):
    method setIdle (line 388) | def setIdle(self):
    method setSleep (line 392) | def setSleep(self):
    method setDamage (line 396) | def setDamage(self, damage, zombie):
    method getPosition (line 407) | def getPosition(self):
  class Sun (line 411) | class Sun(Plant):
    method __init__ (line 412) | def __init__(self, x, y, dest_x, dest_y, is_big=True):
    method handleState (line 425) | def handleState(self):
    method checkCollision (line 449) | def checkCollision(self, x, y):
  class SunFlower (line 464) | class SunFlower(Plant):
    method __init__ (line 465) | def __init__(self, x, y, sun_group):
    method idling (line 471) | def idling(self):
  class PeaShooter (line 486) | class PeaShooter(Plant):
    method __init__ (line 487) | def __init__(self, x, y, bullet_group):
    method attacking (line 491) | def attacking(self):
    method setAttack (line 509) | def setAttack(self):
  class RepeaterPea (line 515) | class RepeaterPea(Plant):
    method __init__ (line 516) | def __init__(self, x, y, bullet_group):
    method attacking (line 523) | def attacking(self):
    method setAttack (line 556) | def setAttack(self):
  class ThreePeaShooter (line 562) | class ThreePeaShooter(Plant):
    method __init__ (line 563) | def __init__(self, x, y, bullet_groups, map_y, background_type):
    method attacking (line 570) | def attacking(self):
    method setAttack (line 608) | def setAttack(self):
  class SnowPeaShooter (line 614) | class SnowPeaShooter(Plant):
    method __init__ (line 615) | def __init__(self, x, y, bullet_group):
    method attacking (line 621) | def attacking(self):
    method setAttack (line 641) | def setAttack(self):
  class WallNut (line 647) | class WallNut(Plant):
    method __init__ (line 648) | def __init__(self, x, y):
    method load_images (line 655) | def load_images(self):
    method idling (line 665) | def idling(self):
  class CherryBomb (line 674) | class CherryBomb(Plant):
    method __init__ (line 675) | def __init__(self, x, y):
    method setBoom (line 684) | def setBoom(self):
    method animation (line 698) | def animation(self):
  class Chomper (line 725) | class Chomper(Plant):
    method __init__ (line 726) | def __init__(self, x, y):
    method loadImages (line 735) | def loadImages(self, name, scale):
    method canAttack (line 755) | def canAttack(self, zombie):
    method setIdle (line 772) | def setIdle(self):
    method setAttack (line 776) | def setAttack(self, zombie, zombie_group):
    method setDigest (line 782) | def setDigest(self):
    method attacking (line 786) | def attacking(self):
    method digest (line 803) | def digest(self):
  class PuffShroom (line 811) | class PuffShroom(Plant):
    method __init__ (line 812) | def __init__(self, x, y, bullet_group):
    method loadImages (line 816) | def loadImages(self, name, scale):
    method attacking (line 831) | def attacking(self):
    method canAttack (line 849) | def canAttack(self, zombie):
    method setAttack (line 862) | def setAttack(self):
  class PotatoMine (line 868) | class PotatoMine(Plant):
    method __init__ (line 869) | def __init__(self, x, y):
    method loadImages (line 879) | def loadImages(self, name, scale):
    method idling (line 896) | def idling(self):
    method canAttack (line 904) | def canAttack(self, zombie):    # 土豆雷不可能遇上潜水僵尸
    method attacking (line 916) | def attacking(self):
  class Squash (line 927) | class Squash(Plant):
    method __init__ (line 928) | def __init__(self, x, y, map_plant_set):
    method loadImages (line 935) | def loadImages(self, name, scale):
    method canAttack (line 952) | def canAttack(self, zombie):
    method setAttack (line 968) | def setAttack(self, zombie, zombie_group):
    method attacking (line 975) | def attacking(self):
    method getPosition (line 999) | def getPosition(self):
  class Spikeweed (line 1003) | class Spikeweed(Plant):
    method __init__ (line 1004) | def __init__(self, x, y):
    method setIdle (line 1011) | def setIdle(self):
    method canAttack (line 1015) | def canAttack(self, zombie):
    method setAttack (line 1026) | def setAttack(self, zombie_group):
    method attacking (line 1033) | def attacking(self):
  class Jalapeno (line 1056) | class Jalapeno(Plant):
    method __init__ (line 1057) | def __init__(self, x, y):
    method loadImages (line 1066) | def loadImages(self, name, scale):
    method setExplode (line 1073) | def setExplode(self):
    method animation (line 1079) | def animation(self):
    method getPosition (line 1107) | def getPosition(self):
  class ScaredyShroom (line 1111) | class ScaredyShroom(Plant):
    method __init__ (line 1112) | def __init__(self, x, y, bullet_group):
    method loadImages (line 1119) | def loadImages(self, name, scale):
    method needCry (line 1136) | def needCry(self, zombie):
    method setCry (line 1144) | def setCry(self):
    method setAttack (line 1148) | def setAttack(self):
    method setIdle (line 1154) | def setIdle(self):
    method attacking (line 1158) | def attacking(self):
  class SunShroom (line 1177) | class SunShroom(Plant):
    method __init__ (line 1178) | def __init__(self, x, y, sun_group):
    method loadImages (line 1186) | def loadImages(self, name, scale):
    method idling (line 1203) | def idling(self):
  class IceShroom (line 1227) | class IceShroom(Plant):
    method __init__ (line 1228) | def __init__(self, x, y):
    method loadImages (line 1234) | def loadImages(self, name, scale):
    method setFreeze (line 1259) | def setFreeze(self):
    method animation (line 1266) | def animation(self):
    method getPosition (line 1296) | def getPosition(self):
  class HypnoShroom (line 1300) | class HypnoShroom(Plant):
    method __init__ (line 1301) | def __init__(self, x, y):
    method loadImages (line 1307) | def loadImages(self, name, scale):
    method idling (line 1322) | def idling(self):
  class WallNutBowling (line 1327) | class WallNutBowling(Plant):
    method __init__ (line 1328) | def __init__(self, x, y, map_y, level):
    method loadImages (line 1342) | def loadImages(self, name, scale):
    method idling (line 1345) | def idling(self):
    method canHit (line 1359) | def canHit(self, map_y):
    method handleMapYPosition (line 1364) | def handleMapYPosition(self):
    method shouldChangeDirection (line 1377) | def shouldChangeDirection(self):
    method changeDirection (line 1384) | def changeDirection(self, map_y):
    method animation (line 1400) | def animation(self):
  class RedWallNutBowling (line 1408) | class RedWallNutBowling(Plant):
    method __init__ (line 1409) | def __init__(self, x, y):
    method loadImages (line 1424) | def loadImages(self, name, scale):
    method idling (line 1435) | def idling(self):
    method attacking (line 1445) | def attacking(self):
    method animation (line 1455) | def animation(self):
    method getPosition (line 1471) | def getPosition(self):
  class LilyPad (line 1475) | class LilyPad(Plant):
    method __init__ (line 1476) | def __init__(self, x, y):
  class TorchWood (line 1481) | class TorchWood(Plant):
    method __init__ (line 1482) | def __init__(self, x, y, bullet_group):
    method idling (line 1486) | def idling(self):
  class StarFruit (line 1524) | class StarFruit(Plant):
    method __init__ (line 1525) | def __init__(self, x, y, bullet_group, level):
    method canAttack (line 1531) | def canAttack(self, zombie):
    method attacking (line 1578) | def attacking(self):
    method setAttack (line 1634) | def setAttack(self):
  class CoffeeBean (line 1640) | class CoffeeBean(Plant):
    method __init__ (line 1641) | def __init__(self, x, y, plant_group, map_content, map, map_x):
    method animation (line 1649) | def animation(self):
  class SeaShroom (line 1683) | class SeaShroom(Plant):
    method __init__ (line 1684) | def __init__(self, x, y, bullet_group):
    method loadImages (line 1688) | def loadImages(self, name, scale):
    method attacking (line 1703) | def attacking(self):
    method canAttack (line 1721) | def canAttack(self, zombie):
    method setAttack (line 1734) | def setAttack(self):
  class TallNut (line 1740) | class TallNut(Plant):
    method __init__ (line 1741) | def __init__(self, x, y):
    method load_images (line 1748) | def load_images(self):
    method idling (line 1758) | def idling(self):
  class TangleKlep (line 1767) | class TangleKlep(Plant):
    method __init__ (line 1768) | def __init__(self, x, y):
    method load_images (line 1773) | def load_images(self):
    method canAttack (line 1788) | def canAttack(self, zombie):
    method setAttack (line 1795) | def setAttack(self, zombie, zombie_group):
    method attacking (line 1800) | def attacking(self):
  class DoomShroom (line 1818) | class DoomShroom(Plant):
    method __init__ (line 1819) | def __init__(self, x, y, map_plant_set, explode_y_range):
    method loadImages (line 1830) | def loadImages(self, name, scale):
    method setBoom (line 1847) | def setBoom(self):
    method animation (line 1851) | def animation(self):
  class Hole (line 1897) | class Hole(Plant):
    method __init__ (line 1898) | def __init__(self, x, y, plot_type):
    method loadImages (line 1906) | def loadImages(self, name, scale):
    method idling (line 1948) | def idling(self):
  class Grave (line 1963) | class Grave(Plant):
    method __init__ (line 1964) | def __init__(self, x, y):
    method animation (line 1971) | def animation(self):
  class GraveBuster (line 1975) | class GraveBuster(Plant):
    method __init__ (line 1976) | def __init__(self, x, y, plant_group, map, map_x):
    method animation (line 1986) | def animation(self):
  class FumeShroom (line 2012) | class FumeShroom(Plant):
    method __init__ (line 2013) | def __init__(self, x, y, bullet_group, zombie_group):
    method loadImages (line 2019) | def loadImages(self, name, scale):
    method canAttack (line 2036) | def canAttack(self, zombie):
    method setAttack (line 2049) | def setAttack(self):
    method attacking (line 2054) | def attacking(self):
    method animation (line 2076) | def animation(self):
  class IceFrozenPlot (line 2097) | class IceFrozenPlot(Plant):
    method __init__ (line 2098) | def __init__(self, x, y):
    method idling (line 2103) | def idling(self):
  class Garlic (line 2110) | class Garlic(Plant):
    method __init__ (line 2111) | def __init__(self, x, y):
    method load_images (line 2117) | def load_images(self):
    method idling (line 2127) | def idling(self):
  class PumpkinHead (line 2136) | class PumpkinHead(Plant):
    method __init__ (line 2137) | def __init__(self, x, y):
    method load_images (line 2145) | def load_images(self):
    method idling (line 2155) | def idling(self):
  class GiantWallNut (line 2164) | class GiantWallNut(Plant):
    method __init__ (line 2165) | def __init__(self, x, y):
    method idling (line 2175) | def idling(self):
    method animation (line 2185) | def animation(self):

FILE: source/component/zombie.py
  class Zombie (line 9) | class Zombie(pg.sprite.Sprite):
    method __init__ (line 10) | def __init__(
    method loadFrames (line 72) | def loadFrames(self, frames, name, colorkey=c.BLACK):
    method update (line 84) | def update(self, game_info):
    method handleState (line 90) | def handleState(self):
    method checkToDie (line 101) | def checkToDie(self, framesKind):
    method walking (line 117) | def walking(self):
    method handleGarlicYChange (line 224) | def handleGarlicYChange(self):
    method attacking (line 260) | def attacking(self):
    method dying (line 293) | def dying(self):
    method freezing (line 296) | def freezing(self):
    method setLostHead (line 314) | def setLostHead(self):
    method changeFrames (line 323) | def changeFrames(self, frames):
    method animation (line 337) | def animation(self):
    method getTimeRatio (line 362) | def getTimeRatio(self):
    method getAttackTimeRatio (line 367) | def getAttackTimeRatio(self):
    method setIceSlow (line 370) | def setIceSlow(self):
    method updateIceSlow (line 379) | def updateIceSlow(self):
    method setDamage (line 384) | def setDamage(
    method setWalk (line 486) | def setWalk(self):
    method setAttack (line 515) | def setAttack(self, prey, is_plant=True):
    method setDie (line 529) | def setDie(self):
    method setBoomDie (line 534) | def setBoomDie(self):
    method setFreeze (line 540) | def setFreeze(self, ice_trap_image):
    method drawFreezeTrap (line 549) | def drawFreezeTrap(self, surface):
    method setHypno (line 553) | def setHypno(self):
  class ZombieHead (line 560) | class ZombieHead(Zombie):
    method __init__ (line 561) | def __init__(self, x, y):
    method loadImages (line 565) | def loadImages(self):
    method setWalk (line 571) | def setWalk(self):
  class NormalZombie (line 575) | class NormalZombie(Zombie):
    method __init__ (line 576) | def __init__(self, x, y, head_group):
    method loadImages (line 579) | def loadImages(self):
  class ConeHeadZombie (line 618) | class ConeHeadZombie(Zombie):
    method __init__ (line 619) | def __init__(self, x, y, head_group):
    method loadImages (line 629) | def loadImages(self):
  class BucketHeadZombie (line 675) | class BucketHeadZombie(Zombie):
    method __init__ (line 676) | def __init__(self, x, y, head_group):
    method loadImages (line 686) | def loadImages(self):
  class FlagZombie (line 732) | class FlagZombie(Zombie):
    method __init__ (line 733) | def __init__(self, x, y, head_group):
    method loadImages (line 737) | def loadImages(self):
  class NewspaperZombie (line 775) | class NewspaperZombie(Zombie):
    method __init__ (line 776) | def __init__(self, x, y, head_group):
    method loadImages (line 787) | def loadImages(self):
    method walking (line 840) | def walking(self):
    method animation (line 861) | def animation(self):
  class FootballZombie (line 897) | class FootballZombie(Zombie):
    method __init__ (line 898) | def __init__(self, x, y, head_group):
    method loadImages (line 914) | def loadImages(self):
  class DuckyTubeZombie (line 960) | class DuckyTubeZombie(Zombie):
    method __init__ (line 961) | def __init__(self, x, y, head_group):
    method loadImages (line 966) | def loadImages(self):
  class ConeHeadDuckyTubeZombie (line 1008) | class ConeHeadDuckyTubeZombie(Zombie):
    method __init__ (line 1009) | def __init__(self, x, y, head_group):
    method loadImages (line 1020) | def loadImages(self):
  class BucketHeadDuckyTubeZombie (line 1074) | class BucketHeadDuckyTubeZombie(Zombie):
    method __init__ (line 1075) | def __init__(self, x, y, head_group):
    method loadImages (line 1086) | def loadImages(self):
  class ScreenDoorZombie (line 1140) | class ScreenDoorZombie(Zombie):
    method __init__ (line 1141) | def __init__(self, x, y, head_group):
    method loadImages (line 1151) | def loadImages(self):
  class PoleVaultingZombie (line 1197) | class PoleVaultingZombie(Zombie):
    method __init__ (line 1198) | def __init__(self, x, y, head_group):
    method loadImages (line 1212) | def loadImages(self):
    method setJump (line 1257) | def setJump(self, successfullyJumped, jump_x):
    method animation (line 1266) | def animation(self):
    method setWalk (line 1303) | def setWalk(self):
    method setFreeze (line 1309) | def setFreeze(self, ice_trap_image):
  class Zomboni (line 1325) | class Zomboni(Zombie):
    method __init__ (line 1326) | def __init__(self, x, y, plant_group, map, IceFrozenPlot):
    method loadImages (line 1336) | def loadImages(self):
    method updateIceSlow (line 1373) | def updateIceSlow(self):
    method setFreeze (line 1377) | def setFreeze(self, ice_trap_image):
    method walking (line 1380) | def walking(self):
    method setDie (line 1421) | def setDie(self):
  class SnorkelZombie (line 1429) | class SnorkelZombie(Zombie):
    method __init__ (line 1430) | def __init__(self, x, y, head_group):
    method loadImages (line 1436) | def loadImages(self):
    method walking (line 1489) | def walking(self):
    method animation (line 1534) | def animation(self):
    method setAttack (line 1571) | def setAttack(self, prey, is_plant=True):
    method setWalk (line 1582) | def setWalk(self):

FILE: source/constants.py
  function _getSound (line 701) | def _getSound(filename):

FILE: source/state/level.py
  class Level (line 14) | class Level(tool.State):
    method __init__ (line 15) | def __init__(self):
    method startup (line 18) | def startup(self, current_time, persist):
    method loadMap (line 38) | def loadMap(self):
    method setupBackground (line 103) | def setupBackground(self):
    method setupGroups (line 113) | def setupGroups(self):
    method createWaves (line 129) | def createWaves(
    method refreshWaves (line 206) | def refreshWaves(self, current_time, survival_rounds=0):
    method setupZombies (line 412) | def setupZombies(self):
    method setupCars (line 424) | def setupCars(self):
    method update (line 431) | def update(self, surface, current_time, mouse_pos, mouse_click):
    method gameTime (line 442) | def gameTime(self, current_time):
    method initBowlingMap (line 450) | def initBowlingMap(self):
    method initState (line 457) | def initState(self):
    method initChoose (line 473) | def initChoose(self):
    method choose (line 489) | def choose(self, mouse_pos, mouse_click):
    method initPlay (line 503) | def initPlay(self, card_list):
    method setupLittleMenu (line 620) | def setupLittleMenu(self):
    method pauseAndCheckMenuOptions (line 707) | def pauseAndCheckMenuOptions(self, mouse_pos, mouse_click):
    method setupHugeWaveApprochingImage (line 761) | def setupHugeWaveApprochingImage(self):
    method setupLevelProgressBarImage (line 773) | def setupLevelProgressBarImage(self):
    method shovelRemovePlant (line 816) | def shovelRemovePlant(self, mouse_pos):
    method play (line 847) | def play(self, mouse_pos, mouse_click):
    method createZombie (line 975) | def createZombie(self, name, map_y=None):
    method canSeedPlant (line 1142) | def canSeedPlant(self, plant_name):
    method addPlant (line 1147) | def addPlant(self):
    method setupHintImage (line 1289) | def setupHintImage(self):
    method setupMouseImage (line 1316) | def setupMouseImage(self, plant_name, select_plant, colorkey=c.BLACK):
    method removeMouseImage (line 1339) | def removeMouseImage(self):
    method removeMouseImagePlus (line 1346) | def removeMouseImagePlus(self):
    method checkBulletCollisions (line 1351) | def checkBulletCollisions(self):
    method checkZombieCollisions (line 1385) | def checkZombieCollisions(self):
    method checkCarCollisions (line 1629) | def checkCarCollisions(self):
    method boomZombies (line 1650) | def boomZombies(self, x, map_y, y_range, x_range, effect=None):
    method freezeZombies (line 1671) | def freezeZombies(self, plant):
    method killPlant (line 1682) | def killPlant(self, target_plant, shovel=False):
    method checkPlant (line 1736) | def checkPlant(self, target_plant, i):
    method checkPlants (line 1886) | def checkPlants(self):
    method checkVictory (line 1894) | def checkVictory(self):
    method checkLose (line 1909) | def checkLose(self):
    method checkGameState (line 1920) | def checkGameState(self):
    method drawMouseShow (line 1955) | def drawMouseShow(self, surface):
    method drawMouseShowPlus (line 1963) | def drawMouseShowPlus(self, surface):   # 拖动铲子时的显示
    method drawZombieFreezeTrap (line 1995) | def drawZombieFreezeTrap(self, i, surface):
    method showLevelProgress (line 1999) | def showLevelProgress(self, surface):
    method showAllContentOfMenu (line 2055) | def showAllContentOfMenu(self, surface):
    method draw (line 2081) | def draw(self, surface):

FILE: source/state/mainmenu.py
  class Menu (line 9) | class Menu(tool.State):
    method __init__ (line 10) | def __init__(self):
    method startup (line 13) | def startup(self, current_time: int, persist):
    method setupBackground (line 29) | def setupBackground(self):
    method setupOptions (line 42) | def setupOptions(self):
    method checkHilight (line 118) | def checkHilight(self, x: int, y: int):
    method chooseHilightImage (line 152) | def chooseHilightImage(self, hilightTime: int, frames):
    method respondAdventureClick (line 159) | def respondAdventureClick(self):
    method respondLittleGameClick (line 169) | def respondLittleGameClick(self):
    method respondExitClick (line 176) | def respondExitClick(self):
    method respondHelpClick (line 181) | def respondHelpClick(self):
    method setupOptionMenu (line 185) | def setupOptionMenu(self):
    method setupSunflowerTrophy (line 245) | def setupSunflowerTrophy(self):
    method checkSunflowerTrophyInfo (line 266) | def checkSunflowerTrophyInfo(self, surface: pg.Surface, x: int, y: int):
    method respondOptionButtonClick (line 286) | def respondOptionButtonClick(self):
    method showCurrentVolumeImage (line 291) | def showCurrentVolumeImage(self, surface: pg.Surface):
    method update (line 304) | def update(

FILE: source/state/screen.py
  class Screen (line 10) | class Screen(tool.State):
    method __init__ (line 11) | def __init__(self):
    method startup (line 15) | def startup(self, current_time, persist):
    method setupImage (line 18) | def setupImage(self, name, frame_rect=(0, 0, 800, 600), color_key=c.BL...
    method update (line 70) | def update(self, surface, current_time, mouse_pos, mouse_click):
  class GameVictoryScreen (line 84) | class GameVictoryScreen(Screen):
    method __init__ (line 85) | def __init__(self):
    method startup (line 89) | def startup(self, current_time, persist):
  class GameLoseScreen (line 101) | class GameLoseScreen(Screen):
    method __init__ (line 102) | def __init__(self):
    method startup (line 106) | def startup(self, current_time, persist):
  class AwardScreen (line 116) | class AwardScreen(tool.State):
    method __init__ (line 117) | def __init__(self):
    method setupImage (line 120) | def setupImage(self):
    method startup (line 241) | def startup(self, current_time, persist):
    method update (line 258) | def update(self, surface, current_time, mouse_pos, mouse_click):
  class HelpScreen (line 271) | class HelpScreen(tool.State):
    method __init__ (line 272) | def __init__(self):
    method startup (line 275) | def startup(self, current_time, persist):
    method setupImage (line 284) | def setupImage(self):
    method update (line 314) | def update(self, surface, current_time, mouse_pos, mouse_click):

FILE: source/tool.py
  class State (line 14) | class State:
    method __init__ (line 15) | def __init__(self):
    method startup (line 24) | def startup(self, current_time: int, persist: dict):
    method cleanup (line 29) | def cleanup(self):
    method update (line 35) | def update(self, surface: pg.Surface, keys, current_time: int):
    method inArea (line 40) | def inArea(self, rect: pg.Rect, x: int, y: int):
    method saveUserData (line 47) | def saveUserData(self):
  class Control (line 58) | class Control:
    method __init__ (line 59) | def __init__(self):
    method setupUserData (line 106) | def setupUserData(self):
    method setup_states (line 114) | def setup_states(self, state_dict: dict, start_state):
    method update (line 120) | def update(self):
    method flip_state (line 135) | def flip_state(self):
    method event_loop (line 144) | def event_loop(self):
    method run (line 170) | def run(self):
  function get_image (line 178) | def get_image(
  function get_image_alpha (line 200) | def get_image_alpha(
  function load_image_frames (line 221) | def load_image_frames(
  function load_all_gfx (line 248) | def load_all_gfx(
Condensed preview — 72 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (362K chars).
[
  {
    "path": ".github/workflows/build-pr.yml",
    "chars": 5340,
    "preview": "name: \"Build for PR\"\n\nconcurrency: \n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\non:\n  "
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 5125,
    "preview": "name: Build\n\nconcurrency: \n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\non:\n  push:\n   "
  },
  {
    "path": ".gitignore",
    "chars": 143,
    "preview": "# 忽略构建内容\nout/\ntest-build/\nrelease/\n# 忽略调试内容\n.vscode/\n# 忽略 Pycharm 项目文件\n.idea/\n__pycache__/\n*/__pycache__/\n# 忽略测试文件\ntest*"
  },
  {
    "path": ".python-version",
    "chars": 5,
    "preview": "3.12\n"
  },
  {
    "path": "README.md",
    "chars": 9006,
    "preview": "## Python版植物大战僵尸\n\n植物大战僵尸游戏的Python实现,基于[marblexu的项目进行创作](https://github.com/marblexu/PythonPlantsVsZombies),部分代码也整合自[call"
  },
  {
    "path": "pyproject.toml",
    "chars": 239,
    "preview": "[project]\nname = \"pypvz\"\nversion = \"0.1.0\"\ndescription = \"Add your description here\"\nreadme = \"README.md\"\nrequires-pytho"
  },
  {
    "path": "pypvz.py",
    "chars": 1612,
    "preview": "#!/usr/bin/env python\nimport logging\nimport os\nimport traceback\nfrom logging.handlers import RotatingFileHandler\n\nimport"
  },
  {
    "path": "source/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "source/component/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "source/component/map.py",
    "chars": 20139,
    "preview": "import random\n\nfrom .. import constants as c\n\n\n# 记录植物种植情况的地图管理工具\nclass Map:\n    def __init__(self, background_type: int)"
  },
  {
    "path": "source/component/menubar.py",
    "chars": 19270,
    "preview": "import random\n\nimport pygame as pg\n\nfrom .. import constants as c\nfrom .. import tool\n\n\ndef getSunValueImage(sun_value):"
  },
  {
    "path": "source/component/plant.py",
    "chars": 71730,
    "preview": "import random\n\nimport pygame as pg\n\nfrom .. import constants as c\nfrom .. import tool\n\n\nclass Car(pg.sprite.Sprite):\n   "
  },
  {
    "path": "source/component/zombie.py",
    "chars": 53192,
    "preview": "import random\n\nimport pygame as pg\n\nfrom .. import constants as c\nfrom .. import tool\n\n\nclass Zombie(pg.sprite.Sprite):\n"
  },
  {
    "path": "source/constants.py",
    "chars": 19843,
    "preview": "import os\n\nimport pygame as pg\n\n# 用户数据及日志存储路径\nif os.name == 'nt':   # Windows系统存储路径\n    USERDATA_PATH = os.path.expandva"
  },
  {
    "path": "source/state/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "source/state/level.py",
    "chars": 86077,
    "preview": "import logging\nimport os\nimport random\n\nimport pygame as pg\n\nfrom .. import constants as c\nfrom .. import tool\nfrom ..co"
  },
  {
    "path": "source/state/mainmenu.py",
    "chars": 15073,
    "preview": "import os\n\nimport pygame as pg\n\nfrom .. import constants as c\nfrom .. import tool\n\n\nclass Menu(tool.State):\n    def __in"
  },
  {
    "path": "source/state/screen.py",
    "chars": 11663,
    "preview": "import os\nfrom abc import abstractmethod\n\nimport pygame as pg\n\nfrom .. import constants as c\nfrom .. import tool\n\n\nclass"
  },
  {
    "path": "source/tool.py",
    "chars": 10253,
    "preview": "import json\nimport logging\nimport os\nfrom abc import abstractmethod\n\nimport pygame as pg\nfrom pygame.locals import *\n\nfr"
  }
]

// ... and 53 more files (download for full content)

About this extraction

This page contains the full source code of the wszqkzqk/pypvz GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 72 files (321.0 KB), approximately 86.1k tokens, and a symbol index with 467 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!