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_ZOMBIE = 'PoleVaultingZombie' ZOMBONI = 'Zomboni' SNORKELZOMBIE = 'SnorkelZombie' BOOMDIE = 'BoomDie' # 对僵尸的攻击类型设置 ZOMBIE_DEAFULT_DAMAGE = ZOMBIE_HELMET_2_FIRST = 'helmet2First' # 优先攻击二类防具 ZOMBIE_COMMON_DAMAGE = 'commonDamage' # 优先攻击僵尸与一类防具的整体 ZOMBIE_RANGE_DAMAGE = 'rangeDamage' # 范围攻击,同时伤害二类防具与(僵尸与一类防具的整体) ZOMBIE_ASH_DAMAGE = 'ashDamage' # 灰烬植物攻击,直接伤害本体 ZOMBIE_WALLNUT_BOWLING_DANMAGE = 'wallnutBowlingDamage' # 坚果保龄球冲撞伤害 # 僵尸生命值设置 # 有关本体 NORMAL_HEALTH = 200 # 普通僵尸生命值 POLE_VAULTING_HEALTH = 333 ZOMBONI_HEALTH = 1280 # 冰车损坏点 ZOMBONI_DAMAGED1_HEALTH = 2 * ZOMBONI_HEALTH // 3 + 70 ZOMBONI_DAMAGED2_HEALTH = ZOMBONI_HEALTH // 3 + 70 # 掉头后僵尸的生命值 LOSTHEAD_HEALTH = 70 POLE_VAULTING_LOSTHEAD_HEALTH = 167 # 有关一类防具 CONEHEAD_HEALTH = 370 BUCKETHEAD_HEALTH = 1100 FOOTBALL_HELMET_HEALTH = 1400 # 有关二类防具 NEWSPAPER_HEALTH = 150 SCREEN_DOOR_HEALTH = 1100 # 僵尸行动信息 ATTACK_INTERVAL = 500 ZOMBIE_ATTACK_DAMAGE = 50 ZOMBIE_WALK_INTERVAL = 60 # 僵尸步行间隔 # 僵尸生成位置 ZOMBIE_START_X = SCREEN_WIDTH + 30 # 场宽度不一样,用于拟合 # 僵尸集体属性集合 # 僵尸生成信息字典:包含生成僵尸名称、僵尸级别、生成权重 CREATE_ZOMBIE_DICT = { # 生成僵尸:(级别, 权重) NORMAL_ZOMBIE: (1, 4000), FLAG_ZOMBIE: (1, 0), CONEHEAD_ZOMBIE: (2, 4000), BUCKETHEAD_ZOMBIE: (4, 3000), NEWSPAPER_ZOMBIE: (2, 1000), FOOTBALL_ZOMBIE: (7, 2000), DUCKY_TUBE_ZOMBIE: (1, 0), # 作为变种,不主动生成 CONEHEAD_DUCKY_TUBE_ZOMBIE: (2, 0), # 作为变种,不主动生成 BUCKETHEAD_DUCKY_TUBE_ZOMBIE: (4, 0), # 作为变种,不主动生成 SCREEN_DOOR_ZOMBIE: (4, 3500), POLE_VAULTING_ZOMBIE: (2, 2000), ZOMBONI: (7, 2000), SNORKELZOMBIE: (3, 2000), } # 记录陆生僵尸的水生变种 CONVERT_ZOMBIE_IN_POOL = { NORMAL_ZOMBIE: DUCKY_TUBE_ZOMBIE, CONEHEAD_ZOMBIE: CONEHEAD_DUCKY_TUBE_ZOMBIE, BUCKETHEAD_ZOMBIE: BUCKETHEAD_DUCKY_TUBE_ZOMBIE, } # 水上僵尸集合 WATER_ZOMBIE = { DUCKY_TUBE_ZOMBIE, CONEHEAD_DUCKY_TUBE_ZOMBIE, BUCKETHEAD_DUCKY_TUBE_ZOMBIE, SNORKELZOMBIE, } # 状态类型 IDLE = 'idle' FLY = 'fly' EXPLODE = 'explode' ATTACK = 'attack' ATTACKED = 'attacked' DIGEST = 'digest' WALK = 'walk' DIE = 'die' CRY = 'cry' FREEZE = 'freeze' SLEEP = 'sleep' # 关卡状态 CHOOSE = 'choose' PLAY = 'play' # 加载矩形碰撞范围 用于消除文件边框影响 # 植物 PLANT_RECT = { BULLET_PEA: {'x': 28, 'y': 0, 'width': 28, 'height': 34}, BULLET_PEA_ICE: {'x': 26, 'y': 0, 'width': 30, 'height': 34}, CHOMPER: {'x': 0, 'y': 0, 'width': 100, 'height': 114}, PUFFSHROOM: {'x': 0, 'y': 28, 'width': 35, 'height': 38}, f'{PUFFSHROOM}Sleep': {'x': 1, 'y': 0, 'width': 39, 'height': 65}, BULLET_MUSHROOM: {'x': 0, 'y': 1, 'width': 55, 'height': 21}, BULLET_SEASHROOM: {'x': 0, 'y': 1, 'width': 55, 'height': 21}, POTATOMINE: {'x': 0, 'y': 0, 'width': 75, 'height': 55}, SQUASH: {'x': 10, 'y': 140, 'width': 80, 'height': 86}, f'{SQUASH}Aim': {'x': 10, 'y': 140, 'width': 80, 'height': 86}, SPIKEWEED: {'x': 3, 'y': 0, 'width': 80, 'height': 35}, } # 僵尸 ZOMBIE_RECT = { NORMAL_ZOMBIE: {'x': 62, 'width': 90}, f'{NORMAL_ZOMBIE}Attack': {'x': 62, 'width': 90}, f'{NORMAL_ZOMBIE}LostHead': {'x': 62, 'width': 90}, f'{NORMAL_ZOMBIE}LostHeadAttack': {'x': 62, 'width': 90}, f'{NORMAL_ZOMBIE}Die': {'x': 0, 'width': 164}, BOOMDIE: {'x': 68, 'width': 80}, CONEHEAD_ZOMBIE: {'x': 80, 'width': 80}, f'{CONEHEAD_ZOMBIE}Attack': {'x': 79, 'width': 87}, BUCKETHEAD_ZOMBIE: {'x': 54, 'width': 90}, f'{BUCKETHEAD_ZOMBIE}Attack': {'x': 46, 'width': 90}, FLAG_ZOMBIE: {'x': 56, 'width': 110}, f'{FLAG_ZOMBIE}Attack': {'x': 60, 'width': 100}, f'{FLAG_ZOMBIE}LostHead': {'x': 55, 'width': 110}, f'{FLAG_ZOMBIE}LostHeadAttack': {'x': 55, 'width': 110}, NEWSPAPER_ZOMBIE: {'x': 48, 'width': 92}, f'{NEWSPAPER_ZOMBIE}Attack': {'x': 48, 'width': 92}, f'{NEWSPAPER_ZOMBIE}NoPaper': {'x': 40, 'width': 98}, f'{NEWSPAPER_ZOMBIE}NoPaperAttack': {'x': 48, 'width': 92}, f'{NEWSPAPER_ZOMBIE}LostHead': {'x': 44, 'width': 96}, f'{NEWSPAPER_ZOMBIE}LostHeadAttack': {'x': 48, 'width': 92}, f'{NEWSPAPER_ZOMBIE}Die': {'x': 0, 'width': 100}, f'{DUCKY_TUBE_ZOMBIE}Die': {'x': 55, 'width': 105}, f'{DUCKY_TUBE_ZOMBIE}LostHead': {'x': 55, 'width': 105}, SCREEN_DOOR_ZOMBIE: {'x': 41, 'width': 100}, f'{SCREEN_DOOR_ZOMBIE}Attack': {'x': 41, 'width': 100}, } # 这里还有懒得写代码的补加,用循环实现 for _part1 in ( DUCKY_TUBE_ZOMBIE, CONEHEAD_DUCKY_TUBE_ZOMBIE, BUCKETHEAD_DUCKY_TUBE_ZOMBIE, ): for _part2 in ('', 'Attack', 'Swim'): ZOMBIE_RECT[f'{_part1}{_part2}'] = {'x': 55, 'width': 105} # 音效 def _getSound(filename): return pg.mixer.Sound( os.path.join( os.path.dirname(os.path.dirname(__file__)), 'resources', 'sound', filename, ) ) # 所有音效的元组,用一波海象算子表达,免得要维护两个 SOUNDS = ( # 程序交互等 SOUND_TAPPING_CARD := _getSound('tap.ogg'), SOUND_HELP_SCREEN := _getSound('helpScreen.ogg'), # 植物 SOUND_FIREPEA_EXPLODE := _getSound('firepea.ogg'), SOUND_BULLET_EXPLODE := _getSound('bulletExplode.ogg'), SOUND_SHOOT := _getSound('shoot.ogg'), SOUND_SNOWPEA_SPARKLES := _getSound('snowPeaSparkles.ogg'), SOUND_BOMB := _getSound('bomb.ogg'), SOUND_BIGCHOMP := _getSound('bigchomp.ogg'), SOUND_PUFF := _getSound('puff.ogg'), SOUND_POTATOMINE := _getSound('potatomine.ogg'), SOUND_SQUASHING := _getSound('squashing.ogg'), SOUND_SQUASH_HMM := _getSound('squashHmm.ogg'), SOUND_PLANT_GROW := _getSound('plantGrow.ogg'), SOUND_MUSHROOM_WAKEUP := _getSound('mushroomWakeup.ogg'), SOUND_TANGLE_KELP_DRAG := _getSound('tangleKelpDrag.ogg'), SOUND_DOOMSHROOM := _getSound('doomshroom.ogg'), SOUND_GRAVEBUSTER_CHOMP := _getSound('gravebusterchomp.ogg'), SOUND_FUME := _getSound('fume.ogg'), # 僵尸 SOUND_ZOMBIE_ENTERING_WATER := _getSound('zombieEnteringWater.ogg'), SOUND_ZOMBIE_ATTACKING := _getSound('zombieAttack.ogg'), SOUND_FREEZE := _getSound('freeze.ogg'), SOUND_HYPNOED := _getSound('hypnoed.ogg'), SOUND_NEWSPAPER_RIP := _getSound('newspaperRip.ogg'), SOUND_NEWSPAPER_ZOMBIE_ANGRY := _getSound('newspaperZombieAngry.ogg'), SOUND_POLEVAULT_JUMP := _getSound('polevaultjump.ogg'), SOUND_ZOMBONI := _getSound('zomboni.ogg'), SOUND_ZOMBONI_EXPLOSION := _getSound('zomboniExplosion.ogg'), # 关卡中 SOUND_CAR_WALKING := _getSound('carWalking.ogg'), SOUND_ZOMBIE_COMING := _getSound('zombieComing.ogg'), SOUND_ZOMBIE_VOICE := _getSound('zombieVoice.ogg'), SOUND_HUGE_WAVE_APPROCHING := _getSound('hugeWaveApproching.ogg'), SOUND_BUTTON_CLICK := _getSound('buttonclick.ogg'), SOUND_COLLECT_SUN := _getSound('collectSun.ogg'), SOUND_CLICK_CARD := _getSound('clickCard.ogg'), SOUND_SHOVEL := _getSound('shovel.ogg'), SOUND_PLANT := _getSound('plant.ogg'), SOUND_BOWLING_IMPACT := _getSound('bowlingimpact.ogg'), SOUND_PLANT_DIE := _getSound('plantDie.ogg'), SOUND_EVILLAUGH := _getSound('evillaugh.ogg'), SOUND_LOSE := _getSound('lose.ogg'), SOUND_WIN := _getSound('win.ogg'), SOUND_SCREAM := _getSound('scream.ogg'), SOUND_CANNOT_CHOOSE_WARNING := _getSound('cannotChooseWarning.ogg'), SOUND_FINAL_FANFARE := _getSound('finalfanfare.ogg'), ) # 记录本地存储文件初始值 INIT_USERDATA = { LEVEL_NUM: 1, LITTLEGAME_NUM: 1, LEVEL_COMPLETIONS: 0, LITTLEGAME_COMPLETIONS: 0, GAME_RATE: 1, SOUND_VOLUME: 1, } # 无穷大常量 INF = float('inf') # python传递字符串性能较低,故在这里对inf声明一次,以后仅需调用即可,虽然真正的用处是可以自动补全( ================================================ FILE: source/state/__init__.py ================================================ ================================================ FILE: source/state/level.py ================================================ import logging import os import random import pygame as pg from .. import constants as c from .. import tool from ..component import map, menubar, plant, zombie logger = logging.getLogger('main') class Level(tool.State): def __init__(self): tool.State.__init__(self) def startup(self, current_time, persist): self.game_info = persist self.persist = self.game_info self.game_info[c.CURRENT_TIME] = current_time # 暂停状态 self.pause = False self.pause_time = 0 # 默认显然不用显示菜单 self.show_game_menu = False # 导入地图参数 self.loadMap() self.map = map.Map(self.map_data[c.BACKGROUND_TYPE]) self.map_x_len = self.map.width self.map_y_len = self.map.height self.setupBackground() self.initState() def loadMap(self): # 冒险模式 if self.game_info[c.GAME_MODE] == c.MODE_ADVENTURE: if 0 <= self.game_info[c.LEVEL_NUM] < map.TOTAL_LEVEL: self.map_data = map.LEVEL_MAP_DATA[self.game_info[c.LEVEL_NUM]] pg.display.set_caption( f'pypvz: 冒险模式 {self.map_data[c.GAME_TITLE]}' ) else: self.game_info[c.LEVEL_NUM] = 1 self.saveUserData() self.map_data = map.LEVEL_MAP_DATA[self.game_info[c.LEVEL_NUM]] pg.display.set_caption( f'pypvz: 冒险模式 {self.map_data[c.GAME_TITLE]}' ) logger.warning('关卡数设定错误!进入默认的第一关!\n') # 小游戏模式 elif self.game_info[c.GAME_MODE] == c.MODE_LITTLEGAME: if 0 <= self.game_info[c.LITTLEGAME_NUM] < map.TOTAL_LITTLE_GAME: self.map_data = map.LITTLE_GAME_MAP_DATA[ self.game_info[c.LITTLEGAME_NUM] ] pg.display.set_caption( f'pypvz: 玩玩小游戏 {self.map_data[c.GAME_TITLE]}' ) else: self.game_info[c.LITTLEGAME_NUM] = 1 self.saveUserData() self.map_data = map.LITTLE_GAME_MAP_DATA[ self.game_info[c.LITTLEGAME_NUM] ] pg.display.set_caption( f'pypvz: 冒险模式 {self.map_data[c.GAME_TITLE]}' ) logger.warning('关卡数设定错误!进入默认的第一关!\n') # 是否有铲子的信息:无铲子时为0,有铲子时为1,故直接赋值即可 self.has_shovel = self.map_data[c.SHOVEL] # 同时指定音乐 # 缺省音乐为进入的音乐,方便发现错误 self.bgm = 'intro.opus' if c.CHOOSEBAR_TYPE in self.map_data: # 指定了choosebar_type的传送带关 if ( self.map_data[c.CHOOSEBAR_TYPE] == c.CHOOSEBAR_BOWLING ): # 坚果保龄球 self.bgm = 'bowling.opus' elif self.map_data[c.CHOOSEBAR_TYPE] == c.CHOOSEBAR_MOVE: # 传送带 self.bgm = 'battle.opus' else: # 一般选卡关,非传送带 # 白天类 if ( self.map_data[c.BACKGROUND_TYPE] in c.BACKGROUND_DAY_LIKE_BACKGROUNDS ): self.bgm = 'dayLevel.opus' # 夜晚 elif self.map_data[c.BACKGROUND_TYPE] == c.BACKGROUND_NIGHT: self.bgm = 'nightLevel.opus' # 泳池 elif self.map_data[c.BACKGROUND_TYPE] == c.BACKGROUND_POOL: self.bgm = 'poolLevel.opus' # 浓雾 elif self.map_data[c.BACKGROUND_TYPE] == c.BACKGROUND_FOG: self.bgm = 'fogLevel.opus' def setupBackground(self): img_index = self.map_data[c.BACKGROUND_TYPE] self.background_type = img_index self.background = tool.GFX[c.BACKGROUND_NAME][img_index] self.bg_rect = self.background.get_rect() self.level = pg.Surface((self.bg_rect.w, self.bg_rect.h)).convert() self.viewport = tool.SCREEN.get_rect(bottom=self.bg_rect.bottom) self.viewport.x += c.BACKGROUND_OFFSET_X def setupGroups(self): self.sun_group = pg.sprite.Group() self.head_group = pg.sprite.Group() # 改用列表生成器直接生成内容,不再在这里使用for循环 self.plant_groups = [pg.sprite.Group() for i in range(self.map_y_len)] self.zombie_groups = [pg.sprite.Group() for i in range(self.map_y_len)] self.hypno_zombie_groups = [ pg.sprite.Group() for i in range(self.map_y_len) ] # 被魅惑的僵尸 self.bullet_groups = [pg.sprite.Group() for i in range(self.map_y_len)] # 按照规则生成每一波僵尸 # 将波刷新和一波中的僵尸生成分开 # useableZombie是指可用的僵尸种类的元组 # inevitableZombie指在本轮必然出现的僵尸,输入形式为字典: {波数1:(僵尸1, 僵尸2……), 波数2:(僵尸1, 僵尸2……)……} def createWaves( self, useable_zombies, num_flags, survival_rounds=0, inevitable_zombie_dict=None, ): waves = [] self.num_flags = num_flags # 权重值,c.CREATE_ZOMBIE_DICT[zombie][1]即为对应的权重 weights = [ c.CREATE_ZOMBIE_DICT[zombie][1] for zombie in useable_zombies ] # 按照原版pvz设计的僵尸容量函数,是从无尽解析的,但是普通关卡也可以遵循 for wave in range(1, 10 * num_flags + 1): zombie_volume = ( int(int((wave + survival_rounds * 20) * 0.8) / 2) + 1 ) zombie_list = [] # 大波僵尸情况 if wave % 10 == 0: # 容量增大至2.5倍 zombie_volume = int(zombie_volume * 2.5) # 先生成旗帜僵尸 zombie_list.append(c.FLAG_ZOMBIE) zombie_volume -= c.CREATE_ZOMBIE_DICT[c.FLAG_ZOMBIE][0] # 传送带模式应当增大僵尸容量 if self.bar_type != c.CHOOSEBAR_STATIC: zombie_volume += 2 if inevitable_zombie_dict and (wave in inevitable_zombie_dict): for new_zombie in inevitable_zombie_dict[wave]: zombie_list.append(new_zombie) zombie_volume -= c.CREATE_ZOMBIE_DICT[new_zombie][0] if zombie_volume < 0: logger.warning(f'第{wave}波中手动设置的僵尸级别总数超过上限!') # 防止因为僵尸最小等级过大,使得总容量无法完全利用,造成死循环的检查机制 min_cost = c.CREATE_ZOMBIE_DICT[ min(useable_zombies, key=lambda x: c.CREATE_ZOMBIE_DICT[x][0]) ][0] while (zombie_volume >= min_cost) and (len(zombie_list) < 50): new_zombie = random.choices(useable_zombies, weights)[0] # 普通僵尸、路障僵尸、铁桶僵尸有概率生成水中变种 if self.background_type in c.POOL_EQUIPPED_BACKGROUNDS: # 有泳池第一轮的第四波设定上生成水生僵尸 if survival_rounds == 0 and wave == 4: if new_zombie in c.CONVERT_ZOMBIE_IN_POOL: new_zombie = c.CONVERT_ZOMBIE_IN_POOL[new_zombie] elif survival_rounds > 0 or wave > 4: if random.randint(1, 3) == 1: # 1/3概率水上,暂时人为设定 if new_zombie in c.CONVERT_ZOMBIE_IN_POOL: new_zombie = c.CONVERT_ZOMBIE_IN_POOL[ new_zombie ] # 首先几轮不出水生僵尸 elif new_zombie in c.WATER_ZOMBIE: continue if c.CREATE_ZOMBIE_DICT[new_zombie][0] <= zombie_volume: zombie_list.append(new_zombie) zombie_volume -= c.CREATE_ZOMBIE_DICT[new_zombie][0] waves.append(zombie_list) self.waves = waves # 针对有泳池的关卡 # 表示尚未生成最后一波中从水里冒出来的僵尸 self.created_zombie_from_pool = False # 僵尸的刷新机制 def refreshWaves(self, current_time, survival_rounds=0): # 最后一波或者大于最后一波 # 如果在夜晚按需从墓碑生成僵尸 有泳池时从水中生成僵尸 # 否则直接return if self.wave_num >= self.map_data[c.NUM_FLAGS] * 10: if self.map_data[c.BACKGROUND_TYPE] == c.BACKGROUND_NIGHT: # 生长墓碑 if not self.new_grave_added: if current_time - self.wave_time > 100: # 墓碑最多有12个 if len(self.grave_set) < 12: unoccupied = [] occupied = [] # 毁灭菇坑与冰道应当特殊化 exception_objects = {c.HOLE, c.ICEFROZENPLOT} # 遍历能生成墓碑的区域 for map_y in range(0, 4): for map_x in range(4, 8): # 为空、为毁灭菇坑、为冰道时看作未被植物占据 if ( not self.map.map[map_y][map_x][ c.MAP_PLANT ] ) or ( all( (i in exception_objects) for i in self.map.map[map_y][ map_x ][c.MAP_PLANT] ) ): unoccupied.append((map_x, map_y)) # 已有墓碑的格子不应该放到任何列表中 elif ( c.GRAVE not in self.map.map[map_y][map_x][ c.MAP_PLANT ] ): occupied.append((map_x, map_y)) if unoccupied: target = unoccupied[ random.randint(0, len(unoccupied) - 1) ] map_x, map_y = target posX, posY = self.map.getMapGridPos( map_x, map_y ) self.plant_groups[map_y].add( plant.Grave(posX, posY) ) self.map.map[map_y][map_x][c.MAP_PLANT].add( c.GRAVE ) self.grave_set.add((map_x, map_y)) elif occupied: target = occupied[ random.randint(0, len(occupied) - 1) ] map_x, map_y = target posX, posY = self.map.getMapGridPos( map_x, map_y ) for i in self.plant_groups[map_y]: checkMapX, _ = self.map.getMapIndex( i.rect.centerx, i.rect.bottom ) if map_x == checkMapX: # 不杀死毁灭菇坑和冰道 if i.name not in exception_objects: i.health = 0 self.plant_groups[map_y].add( plant.Grave(posX, posY) ) self.map.map[map_y][map_x][c.MAP_PLANT].add( c.GRAVE ) self.grave_set.add((map_x, map_y)) self.new_grave_added = True # 从墓碑中生成僵尸 if not self.grave_zombie_created: if current_time - self.wave_time > 1500: for item in self.grave_set: item_x, item_y = self.map.getMapGridPos(*item) # 目前设定:1/2概率普通僵尸,1/2概率路障僵尸 if random.randint(0, 1): self.zombie_groups[item[1]].add( zombie.NormalZombie( item_x, item_y, self.head_group ) ) else: self.zombie_groups[item[1]].add( zombie.ConeHeadZombie( item_x, item_y, self.head_group ) ) self.grave_zombie_created = True elif ( self.map_data[c.BACKGROUND_TYPE] in c.POOL_EQUIPPED_BACKGROUNDS ): if not self.created_zombie_from_pool: if current_time - self.wave_time > 1500: for i in range(3): # 水中倒数四列内可以在此时产生僵尸。共产生3个 map_x, map_y = random.randint( 5, 8 ), random.randint(2, 3) item_x, item_y = self.map.getMapGridPos( map_x, map_y ) # 用随机数指定产生的僵尸类型 # 暂时设定为生成概率相同 zombie_type = random.randint(1, 3) if zombie_type == 1: self.zombie_groups[map_y].add( zombie.BucketHeadDuckyTubeZombie( item_x, item_y, self.head_group ) ) elif zombie_type == 2: self.zombie_groups[map_y].add( zombie.ConeHeadDuckyTubeZombie( item_x, item_y, self.head_group ) ) else: self.zombie_groups[map_y].add( zombie.DuckyTubeZombie( item_x, item_y, self.head_group ) ) self.created_zombie_from_pool = True return # 还未开始出现僵尸 if self.wave_num == 0: if self.wave_time == 0: # 表明刚刚开始游戏 self.wave_time = current_time else: if (survival_rounds == 0) and ( self.bar_type == c.CHOOSEBAR_STATIC ): # 首次选卡等待时间较长 if current_time - self.wave_time >= 18000: self.wave_num += 1 self.wave_time = current_time self.wave_zombies = self.waves[self.wave_num - 1] self.zombie_num = len(self.wave_zombies) c.SOUND_ZOMBIE_COMING.play() else: if current_time - self.wave_time >= 6000: self.wave_num += 1 self.wave_time = current_time self.wave_zombies = self.waves[self.wave_num - 1] self.zombie_num = len(self.wave_zombies) c.SOUND_ZOMBIE_COMING.play() return if self.wave_num % 10 != 9: if ( current_time - self.wave_time >= 25000 + random.randint(0, 6000) ) or ( self.bar_type == c.CHOOSEBAR_BOWLING and current_time - self.wave_time >= 12500 + random.randint(0, 3000) ): self.wave_num += 1 self.wave_time = current_time self.wave_zombies = self.waves[self.wave_num - 1] self.zombie_num = len(self.wave_zombies) c.SOUND_ZOMBIE_VOICE.play() else: if (current_time - self.wave_time >= 45000) or ( self.bar_type != c.CHOOSEBAR_STATIC and current_time - self.wave_time >= 25000 ): self.wave_num += 1 self.wave_time = current_time self.wave_zombies = self.waves[self.wave_num - 1] self.zombie_num = len(self.wave_zombies) # 一大波时播放音效 c.SOUND_HUGE_WAVE_APPROCHING.play() return elif (current_time - self.wave_time >= 43000) or ( self.bar_type != c.CHOOSEBAR_STATIC and current_time - self.wave_time >= 23000 ): self.show_hugewave_approching_time = current_time zombie_nums = 0 for i in range(self.map_y_len): zombie_nums += len(self.zombie_groups[i]) if ( self.zombie_num and (zombie_nums / self.zombie_num < random.uniform(0.15, 0.25)) and (current_time - self.wave_time > 4000) ): # 当僵尸所剩无几并且时间过了4000 ms以上时,改变时间记录,使得2000 ms后刷新僵尸(所以需要判断剩余时间是否大于2000 ms) if self.bar_type == c.CHOOSEBAR_STATIC: if current_time - 43000 < self.wave_time: # 判断剩余时间是否有2000 ms self.wave_time = current_time - 43000 # 即倒计时2000 ms else: if current_time - 23000 < self.wave_time: # 判断剩余时间是否有2000 ms self.wave_time = current_time - 23000 # 即倒计时2000 ms # 旧机制,目前仅用于调试 def setupZombies(self): def takeTime(element): return element[0] self.zombie_list = [] for data in self.map_data[c.ZOMBIE_LIST]: self.zombie_list.append( (data['time'], data['name'], data['map_y']) ) self.zombie_start_time = 0 self.zombie_list.sort(key=takeTime) def setupCars(self): self.cars = [] for i in range(self.map_y_len): y = self.map.getMapGridPos(0, i)[1] self.cars.append(plant.Car(-45, y + 20, i)) # 更新函数每帧被调用,将鼠标事件传入给状态处理函数 def update(self, surface, current_time, mouse_pos, mouse_click): self.current_time = self.game_info[c.CURRENT_TIME] = self.gameTime( current_time ) if self.state == c.CHOOSE: self.choose(mouse_pos, mouse_click) elif self.state == c.PLAY: self.play(mouse_pos, mouse_click) self.draw(surface) def gameTime(self, current_time): # 扣除暂停时间 if not self.pause: self.before_pause_time = current_time - self.pause_time else: self.pause_time = current_time - self.before_pause_time return self.before_pause_time def initBowlingMap(self): for x in range(3, self.map_x_len): for y in range(self.map_y_len): self.map.setMapGridType( x, y, c.MAP_UNAVAILABLE ) # 将坚果保龄球红线右侧设置为不可种植任何植物 def initState(self): if c.CHOOSEBAR_TYPE in self.map_data: self.bar_type = self.map_data[c.CHOOSEBAR_TYPE] else: self.bar_type = c.CHOOSEBAR_STATIC if self.bar_type == c.CHOOSEBAR_STATIC: self.initChoose() else: card_pool = menubar.getCardPool(self.map_data[c.CARD_POOL]) self.initPlay(card_pool) if self.bar_type == c.CHOOSEBAR_BOWLING: self.initBowlingMap() self.setupLittleMenu() def initChoose(self): self.state = c.CHOOSE self.panel = menubar.Panel( c.CARDS_TO_CHOOSE, self.map_data[c.INIT_SUN_NAME], self.background_type, ) # 播放选卡音乐 pg.mixer.music.stop() pg.mixer.music.load( os.path.join(c.PATH_MUSIC_DIR, 'chooseYourSeeds.opus') ) pg.mixer.music.play(-1, 0) pg.mixer.music.set_volume(self.game_info[c.SOUND_VOLUME]) def choose(self, mouse_pos, mouse_click): # 如果暂停 if self.show_game_menu: self.pauseAndCheckMenuOptions(mouse_pos, mouse_click) return elif mouse_pos and mouse_click[0]: self.panel.checkCardClick(mouse_pos) if self.panel.checkStartButtonClick(mouse_pos): self.initPlay(self.panel.getSelectedCards()) elif self.inArea(self.little_menu_rect, *mouse_pos): self.show_game_menu = True c.SOUND_BUTTON_CLICK.play() def initPlay(self, card_list): # 播放bgm pg.mixer.music.stop() pg.mixer.music.load(os.path.join(c.PATH_MUSIC_DIR, self.bgm)) pg.mixer.music.play(-1, 0) pg.mixer.music.set_volume(self.game_info[c.SOUND_VOLUME]) self.state = c.PLAY if self.bar_type == c.CHOOSEBAR_STATIC: self.menubar = menubar.MenuBar( card_list, self.map_data[c.INIT_SUN_NAME] ) else: self.menubar = menubar.MoveBar(card_list) # 是否拖住植物或者铲子 self.drag_plant = False self.drag_shovel = False self.hint_image = None self.hint_plant = False # 用种下植物的名称与位置元组判断是否需要刷新僵尸的攻击对象 # 种植植物后应当刷新僵尸的攻击对象,当然,默认初始时不用刷新 self.new_plant_and_positon = None if ( self.background_type in c.DAYTIME_BACKGROUNDS and self.bar_type == c.CHOOSEBAR_STATIC ): self.produce_sun = True self.fallen_sun = 0 # 已掉落的阳光 else: self.produce_sun = False self.sun_timer = self.current_time self.removeMouseImage() self.setupGroups() if self.map_data[c.SPAWN_ZOMBIES] == c.SPAWN_ZOMBIES_LIST: self.setupZombies() else: # 僵尸波数数据及僵尸生成数据 self.wave_num = 0 # 还未出现僵尸时定义为0 self.wave_time = 0 self.wave_zombies = [] self.zombie_num = 0 # 暂时没有生存模式,所以 survival_rounds = 0 if c.INEVITABLE_ZOMBIE_DICT in self.map_data: self.createWaves( useable_zombies=self.map_data[c.INCLUDED_ZOMBIES], num_flags=self.map_data[c.NUM_FLAGS], survival_rounds=0, inevitable_zombie_dict=self.map_data[ c.INEVITABLE_ZOMBIE_DICT ], ) else: self.createWaves( useable_zombies=self.map_data[c.INCLUDED_ZOMBIES], num_flags=self.map_data[c.NUM_FLAGS], survival_rounds=0, ) self.setupCars() # 地图有铲子才添加铲子 if self.has_shovel: # 导入小铲子 frame_rect = (0, 0, 71, 67) self.shovel = tool.get_image_alpha( tool.GFX[c.SHOVEL], *frame_rect, c.BLACK, 1.1 ) self.shovel_rect = self.shovel.get_rect() frame_rect = (0, 0, 77, 75) self.shovel_positon = (608, 1) self.shovel_box = tool.get_image_alpha( tool.GFX[c.SHOVEL_BOX], *frame_rect, c.BLACK, 1.1 ) self.shovel_box_rect = self.shovel_box.get_rect() self.shovel_rect.x = self.shovel_box_rect.x = self.shovel_positon[ 0 ] self.shovel_rect.y = self.shovel_box_rect.y = self.shovel_positon[ 1 ] self.setupLevelProgressBarImage() self.setupHugeWaveApprochingImage() self.show_hugewave_approching_time = -2000 # 防止设置为0时刚刚打开游戏就已经启动红字 if self.map_data[c.BACKGROUND_TYPE] == c.BACKGROUND_NIGHT: # 判断墓碑数量等级 # 0为无墓碑,1为少量墓碑,2为中等量墓碑,3为大量墓碑 if c.GRADE_GRAVES in self.map_data: grade_graves = self.map_data[c.GRADE_GRAVES] # 缺省为少量墓碑 else: grade_graves = 1 grave_volume = c.GRAVES_GRADE_INFO[grade_graves] self.grave_set = set() while len(self.grave_set) < grave_volume: map_x = random.randint(4, 8) # 注意是从0开始编号 map_y = random.randint(0, 4) self.grave_set.add((map_x, map_y)) if self.grave_set: for i in self.grave_set: map_x, map_y = i posX, posY = self.map.getMapGridPos(map_x, map_y) self.plant_groups[map_y].add(plant.Grave(posX, posY)) self.map.map[map_y][map_x][c.MAP_PLANT].add(c.GRAVE) self.grave_zombie_created = False self.new_grave_added = False # 小菜单 def setupLittleMenu(self): # 具体运行游戏必定有个小菜单, 导入菜单和选项 frame_rect = (0, 0, 108, 31) self.little_menu = tool.get_image_alpha( tool.GFX[c.LITTLE_MENU], *frame_rect, c.BLACK, 1.1 ) self.little_menu_rect = self.little_menu.get_rect() self.little_menu_rect.x = 690 self.little_menu_rect.y = 0 # 弹出的菜单框 frame_rect = (0, 0, 500, 500) self.big_menu = tool.get_image_alpha( tool.GFX[c.BIG_MENU], *frame_rect, c.BLACK, 1.1 ) self.big_menu_rect = self.big_menu.get_rect() self.big_menu_rect.x = 150 self.big_menu_rect.y = 0 # 返回按钮,用字体渲染实现,增强灵活性 # 建立一个按钮大小的surface对象 self.return_button = pg.Surface((376, 96)) self.return_button.set_colorkey(c.BLACK) # 避免多余区域显示成黑色 self.return_button_rect = self.return_button.get_rect() self.return_button_rect.x = 220 self.return_button_rect.y = 440 font = pg.font.Font(c.FONT_PATH, 40) font.bold = True text = font.render('返回游戏', True, c.YELLOWGREEN) text_rect = text.get_rect() text_rect.x = 105 text_rect.y = 18 self.return_button.blit(text, text_rect) # 重新开始按钮 frame_rect = (0, 0, 207, 45) self.restart_button = tool.get_image_alpha( tool.GFX[c.RESTART_BUTTON], *frame_rect, c.BLACK, 1.1 ) self.restart_button_rect = self.restart_button.get_rect() self.restart_button_rect.x = 295 self.restart_button_rect.y = 325 # 主菜单按钮 frame_rect = (0, 0, 206, 43) self.mainMenu_button = tool.get_image_alpha( tool.GFX[c.MAINMENU_BUTTON], *frame_rect, c.BLACK, 1.1 ) self.mainMenu_button_rect = self.mainMenu_button.get_rect() self.mainMenu_button_rect.x = 299 self.mainMenu_button_rect.y = 372 # 音量+、音量- frame_rect = (0, 0, 39, 41) font = pg.font.Font(c.FONT_PATH, 35) font.bold = True # 音量+ self.sound_volume_plus_button = tool.get_image_alpha( tool.GFX[c.SOUND_VOLUME_BUTTON], *frame_rect, c.BLACK ) sign = font.render('+', True, c.YELLOWGREEN) sign_rect = sign.get_rect() sign_rect.x = 8 sign_rect.y = -4 self.sound_volume_plus_button.blit(sign, sign_rect) self.sound_volume_plus_button_rect = ( self.sound_volume_plus_button.get_rect() ) self.sound_volume_plus_button_rect.x = 500 # 音量- self.sound_volume_minus_button = tool.get_image_alpha( tool.GFX[c.SOUND_VOLUME_BUTTON], *frame_rect, c.BLACK ) sign = font.render('-', True, c.YELLOWGREEN) sign_rect = sign.get_rect() sign_rect.x = 12 sign_rect.y = -8 self.sound_volume_minus_button.blit(sign, sign_rect) self.sound_volume_minus_button_rect = ( self.sound_volume_minus_button.get_rect() ) self.sound_volume_minus_button_rect.x = 450 # 音量+、-应当处于同一高度 self.sound_volume_minus_button_rect.y = ( self.sound_volume_plus_button_rect.y ) = 250 def pauseAndCheckMenuOptions(self, mouse_pos, mouse_click): # 设置暂停状态 self.pause = True # 暂停播放音乐 pg.mixer.music.pause() if mouse_click[0]: # 返回键 if self.inArea(self.return_button_rect, *mouse_pos): # 终止暂停,停止显示菜单 self.pause = False self.show_game_menu = False # 继续播放音乐 pg.mixer.music.unpause() # 播放点击音效 c.SOUND_BUTTON_CLICK.play() # 重新开始键 elif self.inArea(self.restart_button_rect, *mouse_pos): self.done = True self.next = c.LEVEL # 播放点击音效 c.SOUND_BUTTON_CLICK.play() # 主菜单键 elif self.inArea(self.mainMenu_button_rect, *mouse_pos): self.done = True self.next = c.MAIN_MENU self.persist = self.game_info self.persist[c.CURRENT_TIME] = 0 # 播放点击音效 c.SOUND_BUTTON_CLICK.play() # 音量+ elif self.inArea(self.sound_volume_plus_button_rect, *mouse_pos): self.game_info[c.SOUND_VOLUME] = round( min(self.game_info[c.SOUND_VOLUME] + 0.05, 1), 2 ) # 一般不会有人想把音乐和音效分开设置,故pg.mixer.Sound.set_volume()和pg.mixer.music.set_volume()需要一起用 pg.mixer.music.set_volume(self.game_info[c.SOUND_VOLUME]) for i in c.SOUNDS: i.set_volume(self.game_info[c.SOUND_VOLUME]) c.SOUND_BUTTON_CLICK.play() # 将音量信息存档 self.saveUserData() elif self.inArea(self.sound_volume_minus_button_rect, *mouse_pos): self.game_info[c.SOUND_VOLUME] = round( max(self.game_info[c.SOUND_VOLUME] - 0.05, 0), 2 ) # 一般不会有人想把音乐和音效分开设置,故pg.mixer.Sound.set_volume()和pg.mixer.music.set_volume()需要一起用 pg.mixer.music.set_volume(self.game_info[c.SOUND_VOLUME]) for i in c.SOUNDS: i.set_volume(self.game_info[c.SOUND_VOLUME]) c.SOUND_BUTTON_CLICK.play() # 将音量信息存档 self.saveUserData() # 一大波僵尸来袭图片显示 def setupHugeWaveApprochingImage(self): frame_rect = (0, 0, 492, 80) self.huge_wave_approching_image = tool.get_image_alpha( tool.GFX[c.HUGE_WAVE_APPROCHING], *frame_rect, c.BLACK, 1 ) self.huge_wave_approching_image_rect = ( self.huge_wave_approching_image.get_rect() ) self.huge_wave_approching_image_rect.x = 140 # 猜的 self.huge_wave_approching_image_rect.y = 250 # 猜的 # 关卡进程显示设置 def setupLevelProgressBarImage(self): # 注意:定位一律采用与主进度条的相对位置 # 主进度条 frame_rect = (0, 0, 158, 26) self.level_progress_bar_image = tool.get_image_alpha( tool.GFX[c.LEVEL_PROGRESS_BAR], *frame_rect, c.BLACK, 1 ) self.level_progress_bar_image_rect = ( self.level_progress_bar_image.get_rect() ) self.level_progress_bar_image_rect.x = 600 self.level_progress_bar_image_rect.y = 574 # 僵尸头 frame_rect = (0, 0, 23, 25) self.level_progress_zombie_head_image = tool.get_image_alpha( tool.GFX[c.LEVEL_PROGRESS_ZOMBIE_HEAD], *frame_rect, c.BLACK, 1 ) self.level_progress_zombie_head_image_rect = ( self.level_progress_zombie_head_image.get_rect() ) self.level_progress_zombie_head_image_rect.x = ( self.level_progress_bar_image_rect.x + 75 ) self.level_progress_zombie_head_image_rect.y = ( self.level_progress_bar_image_rect.y - 3 ) # 旗帜(这里只包括最后一面) frame_rect = (0, 0, 20, 18) self.level_progress_flag = tool.get_image_alpha( tool.GFX[c.LEVEL_PROGRESS_FLAG], *frame_rect, c.BLACK, 1 ) self.level_progress_flag_rect = self.level_progress_flag.get_rect() self.level_progress_flag_rect.x = ( self.level_progress_bar_image_rect.x - 78 ) self.level_progress_flag_rect.y = ( self.level_progress_bar_image_rect.y - 3 ) # 用小铲子移除植物 def shovelRemovePlant(self, mouse_pos): x, y = mouse_pos map_x, map_y = self.map.getMapIndex(x, y) if not self.map.isValid(map_x, map_y): return for i in self.plant_groups[map_y]: if ( x >= i.rect.x and x <= i.rect.right and y >= i.rect.y and y <= i.rect.bottom ): if i.name in c.NON_PLANT_OBJECTS: continue if i.name in c.SKIP_ZOMBIE_COLLISION_CHECK_WHEN_WORKING: if i.start_boom: continue # 优先移除花盆、睡莲上的植物而非花盆、睡莲本身 if len(self.map.map[map_y][map_x][c.MAP_PLANT]) >= 2: if c.LILYPAD in self.map.map[map_y][map_x][c.MAP_PLANT]: if i.name == c.LILYPAD: continue elif '花盆(未实现)' in self.map.map[map_y][map_x][c.MAP_PLANT]: if i.name == '花盆(未实现)': continue self.killPlant(i, shovel=True) # 使用后默认铲子复原 self.drag_shovel = not self.drag_shovel self.removeMouseImagePlus() return def play(self, mouse_pos, mouse_click): # 如果暂停 if self.show_game_menu: self.pauseAndCheckMenuOptions(mouse_pos, mouse_click) return if self.map_data[c.SPAWN_ZOMBIES] == c.SPAWN_ZOMBIES_LIST: # 旧僵尸生成方式 if self.zombie_start_time == 0: self.zombie_start_time = self.current_time elif len(self.zombie_list) > 0: data = self.zombie_list[0] # 因此要求僵尸列表按照时间顺序排列 # data内容排列:[0]:时间 [1]:名称 [2]:坐标 if data[0] <= (self.current_time - self.zombie_start_time): self.createZombie(data[1], data[2]) self.zombie_list.remove(data) else: # 新僵尸生成方式 self.refreshWaves(self.current_time) for i in self.wave_zombies: self.createZombie(i) else: self.wave_zombies = [] for i in range(self.map_y_len): self.bullet_groups[i].update(self.game_info) self.plant_groups[i].update(self.game_info) self.zombie_groups[i].update(self.game_info) self.hypno_zombie_groups[i].update(self.game_info) # 清除走出去的魅惑僵尸 for zombie in self.hypno_zombie_groups[i]: if zombie.rect.x > c.SCREEN_WIDTH: zombie.kill() self.head_group.update(self.game_info) self.sun_group.update(self.game_info) if self.produce_sun: # 原版阳光掉落机制:(已掉落阳光数*100 ms + 4250 ms) 与 9500 ms的最小值,再加 0 ~ 2750 ms 之间的一个数 if (self.current_time - self.sun_timer) > min( c.PRODUCE_SUN_INTERVAL + 100 * self.fallen_sun, 9500 ) + random.randint(0, 2750): self.sun_timer = self.current_time map_x, map_y = self.map.getRandomMapIndex() x, y = self.map.getMapGridPos(map_x, map_y) self.sun_group.add(plant.Sun(x, 0, x, y)) self.fallen_sun += 1 # 检查有没有捡到阳光 clicked_sun = False clicked_cards_or_map = False if ( not self.drag_plant and not self.drag_shovel and mouse_pos and mouse_click[0] ): for sun in self.sun_group: if sun.checkCollision(*mouse_pos): self.menubar.increaseSunValue(sun.sun_value) clicked_sun = True # 播放收集阳光的音效 c.SOUND_COLLECT_SUN.play() # 拖动植物或者铲子 if ( not self.drag_plant and mouse_pos and mouse_click[0] and not clicked_sun ): self.click_result = self.menubar.checkCardClick(mouse_pos) if self.click_result: self.setupMouseImage( self.click_result[0], self.click_result[1] ) self.click_result[1].clicked = True clicked_cards_or_map = True # 播放音效 c.SOUND_CLICK_CARD.play() elif self.drag_plant: if mouse_click[1]: self.removeMouseImage() clicked_cards_or_map = True self.click_result[1].clicked = False elif mouse_click[0]: if self.menubar.checkMenuBarClick(mouse_pos): self.click_result[1].clicked = False self.removeMouseImage() else: self.addPlant() elif mouse_pos is None: self.setupHintImage() elif self.drag_shovel: if mouse_click[1]: self.removeMouseImagePlus() # 检查是否点击菜单 if mouse_click[0] and (not clicked_sun) and (not clicked_cards_or_map): if self.inArea(self.little_menu_rect, *mouse_pos): # 暂停 显示菜单 self.show_game_menu = True # 播放点击音效 c.SOUND_BUTTON_CLICK.play() elif self.has_shovel: if self.inArea(self.shovel_box_rect, *mouse_pos): self.drag_shovel = not self.drag_shovel if not self.drag_shovel: self.removeMouseImagePlus() # 播放点击铲子的音效 c.SOUND_SHOVEL.play() elif self.drag_shovel: # 移出这地方的植物 self.shovelRemovePlant(mouse_pos) for car in self.cars: if car: car.update(self.game_info) self.menubar.update(self.current_time) # 检查碰撞 self.checkBulletCollisions() self.checkZombieCollisions() self.checkPlants() self.checkCarCollisions() self.checkGameState() def createZombie(self, name, map_y=None): # 有指定时按照指定生成,无指定时随机位置生成 # 0:白天 1:夜晚 2:泳池 3:浓雾 4:屋顶 5:月夜 6:坚果保龄球 if map_y == None: # 情况复杂:分水路和陆路,不能简单实现,需要另外加判断 # 0, 1, 4, 5路为陆路,2, 3路为水路 if self.map_data[c.BACKGROUND_TYPE] in c.POOL_EQUIPPED_BACKGROUNDS: if name in c.WATER_ZOMBIE: map_y = random.randint(2, 3) elif name == '这里应该换成气球僵尸的名字(最好写调用的变量名,最好不要直接写,保持风格统一)': map_y = random.randint(0, 5) else: # 陆生僵尸 map_y = random.randint(0, 3) if map_y >= 2: # 后两路的map_y应当+2 map_y += 2 elif self.map_data[c.BACKGROUND_TYPE] == c.BACKGROUND_SINGLE: map_y = 2 elif self.map_data[c.BACKGROUND_TYPE] == c.BACKGROUND_TRIPLE: map_y = random.randint(1, 3) else: map_y = random.randint(0, 4) if self.map_data[c.SPAWN_ZOMBIES] == c.SPAWN_ZOMBIES_AUTO: # 旗帜波出生点右移 if self.wave_num % 10: huge_wave_move = 0 else: huge_wave_move = 40 else: huge_wave_move = 0 x, y = self.map.getMapGridPos(0, map_y) # 新增的僵尸也需要在这里声明 match name: case c.NORMAL_ZOMBIE: self.zombie_groups[map_y].add( zombie.NormalZombie( c.ZOMBIE_START_X + random.randint(-20, 20) + huge_wave_move, y, self.head_group, ) ) case c.CONEHEAD_ZOMBIE: self.zombie_groups[map_y].add( zombie.ConeHeadZombie( c.ZOMBIE_START_X + random.randint(-20, 20) + huge_wave_move, y, self.head_group, ) ) case c.BUCKETHEAD_ZOMBIE: self.zombie_groups[map_y].add( zombie.BucketHeadZombie( c.ZOMBIE_START_X + random.randint(-20, 20) + huge_wave_move, y, self.head_group, ) ) case c.FLAG_ZOMBIE: self.zombie_groups[map_y].add( zombie.FlagZombie(c.ZOMBIE_START_X, y, self.head_group) ) case c.NEWSPAPER_ZOMBIE: self.zombie_groups[map_y].add( zombie.NewspaperZombie( c.ZOMBIE_START_X + random.randint(-20, 20) + huge_wave_move, y, self.head_group, ) ) case c.FOOTBALL_ZOMBIE: self.zombie_groups[map_y].add( zombie.FootballZombie( c.ZOMBIE_START_X + random.randint(-20, 20) + huge_wave_move, y, self.head_group, ) ) case c.DUCKY_TUBE_ZOMBIE: self.zombie_groups[map_y].add( zombie.DuckyTubeZombie( c.ZOMBIE_START_X + random.randint(-20, 20) + huge_wave_move, y, self.head_group, ) ) case c.CONEHEAD_DUCKY_TUBE_ZOMBIE: self.zombie_groups[map_y].add( zombie.ConeHeadDuckyTubeZombie( c.ZOMBIE_START_X + random.randint(-20, 20) + huge_wave_move, y, self.head_group, ) ) case c.BUCKETHEAD_DUCKY_TUBE_ZOMBIE: self.zombie_groups[map_y].add( zombie.BucketHeadDuckyTubeZombie( c.ZOMBIE_START_X + random.randint(-20, 20) + huge_wave_move, y, self.head_group, ) ) case c.SCREEN_DOOR_ZOMBIE: self.zombie_groups[map_y].add( zombie.ScreenDoorZombie( c.ZOMBIE_START_X + random.randint(-20, 20) + huge_wave_move, y, self.head_group, ) ) case c.POLE_VAULTING_ZOMBIE: # 本来撑杆跳生成位置不同,对齐左端可认为修正了一部分(看作移动了70),只需要相对修改即可 self.zombie_groups[map_y].add( zombie.PoleVaultingZombie( c.ZOMBIE_START_X + random.randint(0, 10) + huge_wave_move, y, self.head_group, ) ) case c.ZOMBONI: # 冰车僵尸生成位置不同 self.zombie_groups[map_y].add( zombie.Zomboni( c.ZOMBIE_START_X + random.randint(0, 10) + huge_wave_move, y, self.plant_groups[map_y], self.map, plant.IceFrozenPlot, ) ) case c.SNORKELZOMBIE: # 潜水僵尸生成位置不同 self.zombie_groups[map_y].add( zombie.SnorkelZombie( c.ZOMBIE_START_X + random.randint(0, 10) + huge_wave_move, y, self.head_group, ) ) # 能否种植物的判断: # 先判断位置是否合法 isValid(map_x, map_y) # 再判断位置是否可用 isMovable(map_x, map_y) def canSeedPlant(self, plant_name): x, y = pg.mouse.get_pos() return self.map.checkPlantToSeed(x, y, plant_name) # 种植物 def addPlant(self): pos = self.canSeedPlant(self.plant_name) if pos is None: return # 恢复植物卡片样式 self.click_result[1].clicked = False if self.hint_image is None: self.setupHintImage() x, y = self.hint_rect.centerx, self.hint_rect.bottom map_x, map_y = self.map.getMapIndex(x, y) # 新植物也需要在这里声明 match self.plant_name: case c.SUNFLOWER: new_plant = plant.SunFlower(x, y, self.sun_group) case c.PEASHOOTER: new_plant = plant.PeaShooter(x, y, self.bullet_groups[map_y]) case c.SNOWPEASHOOTER: new_plant = plant.SnowPeaShooter( x, y, self.bullet_groups[map_y] ) case c.WALLNUT: new_plant = plant.WallNut(x, y) case c.CHERRYBOMB: new_plant = plant.CherryBomb(x, y) case c.THREEPEASHOOTER: new_plant = plant.ThreePeaShooter( x, y, self.bullet_groups, map_y, self.map.background_type ) case c.REPEATERPEA: new_plant = plant.RepeaterPea(x, y, self.bullet_groups[map_y]) case c.CHOMPER: new_plant = plant.Chomper(x, y) case c.PUFFSHROOM: new_plant = plant.PuffShroom(x, y, self.bullet_groups[map_y]) case c.POTATOMINE: new_plant = plant.PotatoMine(x, y) case c.SQUASH: new_plant = plant.Squash( x, y, self.map.map[map_y][map_x][c.MAP_PLANT] ) case c.SPIKEWEED: new_plant = plant.Spikeweed(x, y) case c.JALAPENO: new_plant = plant.Jalapeno(x, y) case c.SCAREDYSHROOM: new_plant = plant.ScaredyShroom( x, y, self.bullet_groups[map_y] ) case c.SUNSHROOM: new_plant = plant.SunShroom(x, y, self.sun_group) case c.ICESHROOM: new_plant = plant.IceShroom(x, y) case c.HYPNOSHROOM: new_plant = plant.HypnoShroom(x, y) case c.WALLNUTBOWLING: new_plant = plant.WallNutBowling(x, y, map_y, self) case c.REDWALLNUTBOWLING: new_plant = plant.RedWallNutBowling(x, y) case c.LILYPAD: new_plant = plant.LilyPad(x, y) case c.TORCHWOOD: new_plant = plant.TorchWood(x, y, self.bullet_groups[map_y]) case c.STARFRUIT: new_plant = plant.StarFruit( x, y, self.bullet_groups[map_y], self ) case c.COFFEEBEAN: new_plant = plant.CoffeeBean( x, y, self.plant_groups[map_y], self.map.map[map_y][map_x], self.map, map_x, ) case c.SEASHROOM: new_plant = plant.SeaShroom(x, y, self.bullet_groups[map_y]) case c.TALLNUT: new_plant = plant.TallNut(x, y) case c.TANGLEKLEP: new_plant = plant.TangleKlep(x, y) case c.DOOMSHROOM: if self.map.grid_height_size == c.GRID_Y_SIZE: new_plant = plant.DoomShroom( x, y, self.map.map[map_y][map_x][c.MAP_PLANT], explode_y_range=2, ) else: new_plant = plant.DoomShroom( x, y, self.map.map[map_y][map_x][c.MAP_PLANT], explode_y_range=3, ) case c.GRAVEBUSTER: new_plant = plant.GraveBuster( x, y, self.plant_groups[map_y], self.map, map_x ) case c.FUMESHROOM: new_plant = plant.FumeShroom( x, y, self.bullet_groups[map_y], self.zombie_groups[map_y] ) case c.GARLIC: new_plant = plant.Garlic(x, y) case c.PUMPKINHEAD: new_plant = plant.PumpkinHead(x, y) case c.GIANTWALLNUT: new_plant = plant.GiantWallNut(x, y) if (new_plant.name in c.CAN_SLEEP_PLANTS) and ( self.background_type in c.DAYTIME_BACKGROUNDS ): new_plant.setSleep() mushroom_sleep = True else: mushroom_sleep = False self.plant_groups[map_y].add(new_plant) # 种植植物后应当刷新僵尸的攻击对象 # 用元组表示植物的名称和格子坐标 self.new_plant_and_positon = (new_plant.name, (map_x, map_y)) if self.bar_type == c.CHOOSEBAR_STATIC: self.menubar.decreaseSunValue(self.select_plant.sun_cost) self.menubar.setCardFrozenTime(self.plant_name) else: self.menubar.deleateCard(self.select_plant) if self.bar_type != c.CHOOSEBAR_BOWLING: # 坚果保龄球关卡无需考虑格子被占用的情况 self.map.addMapPlant( map_x, map_y, self.plant_name, sleep=mushroom_sleep ) self.removeMouseImage() # print(self.new_plant_and_positon) # 播放种植音效 c.SOUND_PLANT.play() def setupHintImage(self): pos = self.canSeedPlant(self.plant_name) if pos and self.mouse_image: if ( self.hint_image and pos[0] == self.hint_rect.x and pos[1] == self.hint_rect.y ): return width, height = self.mouse_rect.w, self.mouse_rect.h image = pg.Surface([width, height]) image.blit(self.mouse_image, (0, 0), (0, 0, width, height)) image.set_colorkey(c.BLACK) image.set_alpha(128) self.hint_image = image self.hint_rect = image.get_rect() # 花盆、睡莲图片应当下移一些 if self.plant_name in {c.LILYPAD, '花盆(未实现)', c.TANGLEKLEP}: self.hint_rect.centerx = pos[0] self.hint_rect.bottom = pos[1] + 25 else: self.hint_rect.centerx = pos[0] self.hint_rect.bottom = pos[1] self.hint_plant = True else: self.hint_plant = False def setupMouseImage(self, plant_name, select_plant, colorkey=c.BLACK): frame_list = tool.GFX[plant_name] if plant_name in c.PLANT_RECT: data = c.PLANT_RECT[plant_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 self.mouse_image = tool.get_image( frame_list[0], x, y, width, height, colorkey, 1 ) self.mouse_rect = self.mouse_image.get_rect() self.drag_plant = True self.plant_name = plant_name self.select_plant = select_plant def removeMouseImage(self): self.drag_plant = False self.mouse_image = None self.hint_image = None self.hint_plant = False # 移除小铲子 def removeMouseImagePlus(self): self.drag_shovel = False self.shovel_rect.x = self.shovel_positon[0] self.shovel_rect.y = self.shovel_positon[1] def checkBulletCollisions(self): for i in range(self.map_y_len): for bullet in self.bullet_groups[i]: if bullet.name == c.FUME: continue collided_func = pg.sprite.collide_mask if bullet.state == c.FLY: # 利用循环而非内建精灵组碰撞判断函数,处理更加灵活,可排除已死亡僵尸 for zombie in self.zombie_groups[i]: if (zombie.name == c.SNORKELZOMBIE) and ( zombie.frames == zombie.swim_frames ): continue if collided_func(zombie, bullet): if zombie.state != c.DIE: zombie.setDamage( bullet.damage, effect=bullet.effect, damage_type=bullet.damage_type, ) bullet.setExplode() # 火球有溅射伤害 if bullet.name == c.BULLET_FIREBALL: for rangeZombie in self.zombie_groups[i]: if abs( rangeZombie.rect.x - bullet.rect.x ) <= (c.GRID_X_SIZE // 2): rangeZombie.setDamage( c.BULLET_DAMAGE_FIREBALL_RANGE, effect=None, damage_type=c.ZOMBIE_DEAFULT_DAMAGE, ) break def checkZombieCollisions(self): for i in range(self.map_y_len): for zombie in self.zombie_groups[i]: if zombie.name == c.ZOMBONI: continue if zombie.name in {c.POLE_VAULTING_ZOMBIE} and ( not zombie.jumped ): collided_func = pg.sprite.collide_rect_ratio(0.6) else: collided_func = pg.sprite.collide_mask if zombie.state != c.WALK: # 非啃咬时不用刷新 if zombie.state != c.ATTACK: continue # 没有新的植物种下时不用刷新 if not self.new_plant_and_positon: continue # 被攻击对象是植物时才可能刷新 if zombie.prey_is_plant: # 新植物种在被攻击植物同一格时才可能刷新 if ( zombie.prey_map_x, zombie.prey_map_y, ) == self.new_plant_and_positon[1]: # 如果被攻击植物是睡莲和花盆,同一格种了植物必然刷新 # 如果被攻击植物不是睡莲和花盆,同一格种了南瓜头才刷新 if ( zombie.prey.name not in {c.LILYPAD, '花盆(未实现)'} ) and ( self.new_plant_and_positon[0] != c.PUMPKINHEAD ): continue else: continue else: continue if zombie.can_swim and (not zombie.swimming): continue # 以下代码为了实现各个功能,较为凌乱 attackable_common_plants = [] attackable_backup_plants = [] # 利用更加精细的循环判断啃咬优先顺序 for plant in self.plant_groups[i]: if collided_func(plant, zombie): # 优先攻击南瓜头 if plant.name == c.PUMPKINHEAD: target_plant = plant break # 衬底植物情形 elif plant.name in {c.LILYPAD, '花盆(未实现)'}: attackable_backup_plants.append(plant) # 一般植物情形 # 同时也忽略了不可啃食对象 elif ( plant.name not in c.CAN_SKIP_ZOMBIE_COLLISION_CHECK ): attackable_common_plants.append(plant) # 在生效状态下忽略啃食碰撞但其他状况下不能忽略的情形 elif ( plant.name in c.SKIP_ZOMBIE_COLLISION_CHECK_WHEN_WORKING ): if not plant.start_boom: attackable_common_plants.append(plant) else: if attackable_common_plants: # 默认为最右侧的一个植物 target_plant = max( attackable_common_plants, key=lambda i: i.rect.x ) map_x, map_y = self.map.getMapIndex( target_plant.rect.centerx, target_plant.rect.centery, ) if self.map.isValid(map_x, map_y): if ( c.PUMPKINHEAD in self.map.map[map_y][map_x][c.MAP_PLANT] ): for actual_target_plant in self.plant_groups[ i ]: # 检测同一格的其他植物 if self.map.getMapIndex( actual_target_plant.rect.centerx, actual_target_plant.rect.bottom, ) == (map_x, map_y): if ( actual_target_plant.name == c.PUMPKINHEAD ): target_plant = actual_target_plant break elif attackable_backup_plants: target_plant = max( attackable_backup_plants, key=lambda i: i.rect.x ) map_x, map_y = self.map.getMapIndex( target_plant.rect.centerx, target_plant.rect.centery, ) if len(self.map.map[map_y][map_x][c.MAP_PLANT]) >= 2: for actual_target_plant in self.plant_groups[i]: # 检测同一格的其他植物 if self.map.getMapIndex( actual_target_plant.rect.centerx, actual_target_plant.rect.bottom, ) == (map_x, map_y): if ( actual_target_plant.name == c.PUMPKINHEAD ): target_plant = actual_target_plant break elif actual_target_plant.name not in { c.LILYPAD, '花盆(未实现)', }: attackable_common_plants.append( actual_target_plant ) else: if attackable_common_plants: target_plant = attackable_common_plants[-1] else: target_plant = None if target_plant: ( zombie.prey_map_x, zombie.prey_map_y, ) = self.map.getMapIndex( target_plant.rect.centerx, target_plant.rect.centery ) # 撑杆跳的特殊情况 if zombie.name in {c.POLE_VAULTING_ZOMBIE} and ( not zombie.jumped ): if target_plant.name == c.GIANTWALLNUT: zombie.health = 0 c.SOUND_BOWLING_IMPACT.play() elif not zombie.jumping: zombie.jump_map_x = min( self.map_x_len - 1, zombie.prey_map_x ) zombie.jump_map_y = min( self.map_y_len - 1, zombie.prey_map_y ) jump_x = target_plant.rect.x - c.GRID_X_SIZE * 0.6 if ( c.TALLNUT in self.map.map[zombie.jump_map_y][ zombie.jump_map_x ][c.MAP_PLANT] ): zombie.setJump(False, jump_x) else: zombie.setJump(True, jump_x) else: if ( c.TALLNUT in self.map.map[zombie.jump_map_y][ zombie.jump_map_x ][c.MAP_PLANT] ): zombie.setJump(False, zombie.jump_x) else: zombie.setJump(True, zombie.jump_x) continue if target_plant.name == c.WALLNUTBOWLING: if target_plant.canHit(i): # target_plant.vel_y不为0,有纵向速度,表明已经发生过碰撞,对铁门秒杀(这里实现为忽略二类防具攻击) if ( target_plant.vel_y and zombie.name == c.SCREEN_DOOR_ZOMBIE ): zombie.setDamage( c.WALLNUT_BOWLING_DAMAGE, damage_type=c.ZOMBIE_COMMON_DAMAGE, ) else: zombie.setDamage( c.WALLNUT_BOWLING_DAMAGE, damage_type=c.ZOMBIE_WALLNUT_BOWLING_DANMAGE, ) target_plant.changeDirection(i) # 播放撞击音效 c.SOUND_BOWLING_IMPACT.play() elif target_plant.name == c.REDWALLNUTBOWLING: if target_plant.state == c.IDLE: target_plant.setAttack() elif target_plant.name == c.GIANTWALLNUT: zombie.health = 0 c.SOUND_BOWLING_IMPACT.play() elif zombie.target_y_change: # 大蒜作用正在生效的僵尸不进行传递 continue elif target_plant.name == c.GARLIC: zombie.setAttack(target_plant) # 向吃过大蒜的僵尸传入level zombie.level = self zombie.to_change_group = True zombie.map_y = i if i == 0: _move = 1 elif i == self.map_y_len - 1: _move = -1 else: _move = random.randint(0, 1) * 2 - 1 if ( self.map.map[i][0][c.MAP_PLOT_TYPE] != self.map.map[i + _move][0][c.MAP_PLOT_TYPE] ): _move = -(_move) zombie.target_map_y = i + _move zombie.target_y_change = ( _move * self.map.grid_height_size ) else: zombie.setAttack(target_plant) for hypno_zombie in self.hypno_zombie_groups[i]: if hypno_zombie.health <= 0: continue collided_func = pg.sprite.collide_mask zombie_list = pg.sprite.spritecollide( hypno_zombie, self.zombie_groups[i], False, collided_func ) for zombie in zombie_list: if zombie.state == c.DIE: continue # 正常僵尸攻击被魅惑的僵尸 if zombie.state == c.WALK: zombie.setAttack(hypno_zombie, False) # 被魅惑的僵尸攻击正常僵尸 if hypno_zombie.state == c.WALK: hypno_zombie.setAttack(zombie, False) else: self.new_plant_and_positon = None # 生效后需要解除刷新设置 def checkCarCollisions(self): for i in range(len(self.cars)): if self.cars[i]: for zombie in self.zombie_groups[i]: if ( zombie and zombie.state != c.DIE and (not zombie.losthead) and (pg.sprite.collide_mask(zombie, self.cars[i])) ): self.cars[i].setWalk() if ( pg.sprite.collide_mask(zombie, self.cars[i]) or self.cars[i].rect.x <= zombie.rect.right <= self.cars[i].rect.right ): zombie.health = 0 if self.cars[i].dead: self.cars[i] = None def boomZombies(self, x, map_y, y_range, x_range, effect=None): for i in range(self.map_y_len): if abs(i - map_y) > y_range: continue for zombie in self.zombie_groups[i]: if (abs(zombie.rect.centerx - x) <= x_range) or ( (zombie.rect.right - (x - x_range) > 20) or (zombie.rect.right - (x - x_range)) / zombie.rect.width > 0.2, ((x + x_range) - zombie.rect.left > 20) or ((x + x_range) - zombie.rect.left) / zombie.rect.width > 0.2, )[ zombie.rect.x > x ]: # 这代码不太好懂,后面是一个判断僵尸在左还是在右,前面是一个元组,[0]是在左边的情况,[1]是在右边的情况 if effect == c.BULLET_EFFECT_UNICE: zombie.ice_slow_ratio = 1 zombie.setDamage(1800, damage_type=c.ZOMBIE_ASH_DAMAGE) if zombie.health <= 0: zombie.setBoomDie() def freezeZombies(self, plant): # 播放冻结音效 c.SOUND_FREEZE.play() for i in range(self.map_y_len): for zombie in self.zombie_groups[i]: zombie.setFreeze(plant.trap_frames[0]) zombie.setDamage( 20, damage_type=c.ZOMBIE_RANGE_DAMAGE ) # 寒冰菇还有全场20的伤害 def killPlant(self, target_plant, shovel=False): x, y = target_plant.getPosition() map_x, map_y = self.map.getMapIndex(x, y) # 用铲子铲不用触发植物功能 if not shovel: if ( target_plant.name == c.HYPNOSHROOM and target_plant.state != c.SLEEP ): if target_plant.zombie_to_hypno: zombie = target_plant.zombie_to_hypno zombie.setHypno() self.zombie_groups[map_y].remove(zombie) self.hypno_zombie_groups[map_y].add(zombie) # 对于墓碑:移除存储在墓碑集合中的坐标 # 注意这里是在描述墓碑而非墓碑吞噬者 elif target_plant.name == c.GRAVE: self.grave_set.remove((map_x, map_y)) elif ( target_plant.name in { c.DOOMSHROOM, c.ICESHROOM, c.POTATOMINE, } ) and (target_plant.boomed): # 毁灭菇的情况:爆炸时为了防止蘑菇云被坑掩盖没有加入坑,这里毁灭菇死亡(即爆炸动画结束)后再加入 if target_plant.name == c.DOOMSHROOM: self.plant_groups[map_y].add( plant.Hole( target_plant.original_x, target_plant.original_y, self.map.map[map_y][map_x][c.MAP_PLOT_TYPE], ) ) elif target_plant.name not in c.PLANT_DIE_SOUND_EXCEPTIONS: # 触发植物死亡音效 c.SOUND_PLANT_DIE.play() else: # 用铲子移除植物时播放音效 c.SOUND_PLANT.play() # 整理地图信息 if self.bar_type != c.CHOOSEBAR_BOWLING: self.map.removeMapPlant(map_x, map_y, target_plant.name) # 将睡眠植物移除后更新睡眠状态 if target_plant.state == c.SLEEP: self.map.map[map_y][map_x][c.MAP_SLEEP] = False # 避免僵尸在用铲子移除植物后还在原位啃食 target_plant.health = 0 target_plant.kill() def checkPlant(self, target_plant, i): zombie_len = len(self.zombie_groups[i]) # 不用检查攻击状况的情况 if not target_plant.attack_check: pass elif target_plant.name == c.THREEPEASHOOTER: if target_plant.state == c.IDLE: if zombie_len > 0: target_plant.setAttack() elif (i - 1) >= 0 and len(self.zombie_groups[i - 1]) > 0: target_plant.setAttack() elif (i + 1) < self.map_y_len and len( self.zombie_groups[i + 1] ) > 0: target_plant.setAttack() elif target_plant.state == c.ATTACK: if zombie_len > 0: pass elif (i - 1) >= 0 and len(self.zombie_groups[i - 1]) > 0: pass elif (i + 1) < self.map_y_len and len( self.zombie_groups[i + 1] ) > 0: pass else: target_plant.setIdle() elif target_plant.name == c.CHOMPER: for zombie in self.zombie_groups[i]: if target_plant.canAttack(zombie): target_plant.setAttack(zombie, self.zombie_groups[i]) break elif target_plant.name == c.POTATOMINE: for zombie in self.zombie_groups[i]: if target_plant.canAttack(zombie): target_plant.setAttack() break if target_plant.start_boom and (not target_plant.boomed): for zombie in self.zombie_groups[i]: # 双判断:发生碰撞或在攻击范围内 if (pg.sprite.collide_mask(zombie, target_plant)) or ( abs(zombie.rect.centerx - target_plant.rect.centerx) <= target_plant.explode_x_range ): zombie.setDamage( 1800, damage_type=c.ZOMBIE_RANGE_DAMAGE ) target_plant.boomed = True elif target_plant.name == c.SQUASH: for zombie in self.zombie_groups[i]: if target_plant.canAttack(zombie): target_plant.setAttack(zombie, self.zombie_groups[i]) break elif target_plant.name == c.SPIKEWEED: can_attack = False for zombie in self.zombie_groups[i]: if target_plant.canAttack(zombie): can_attack = True break if target_plant.state == c.IDLE and can_attack: target_plant.setAttack(self.zombie_groups[i]) elif target_plant.state == c.ATTACK and not can_attack: target_plant.setIdle() elif target_plant.name == c.SCAREDYSHROOM: need_cry = False can_attack = False for zombie in self.zombie_groups[i]: if target_plant.needCry(zombie): need_cry = True break elif target_plant.canAttack(zombie): can_attack = True if need_cry: if target_plant.state != c.CRY: target_plant.setCry() elif can_attack: if target_plant.state != c.ATTACK: target_plant.setAttack() elif target_plant.state != c.IDLE: target_plant.setIdle() elif target_plant.name == c.STARFRUIT: can_attack = False for zombie_group in self.zombie_groups: # 遍历循环所有僵尸 for zombie in zombie_group: if target_plant.canAttack(zombie): can_attack = True break if target_plant.state == c.IDLE and can_attack: target_plant.setAttack() elif target_plant.state == c.ATTACK and not can_attack: target_plant.setIdle() elif target_plant.name == c.TANGLEKLEP: for zombie in self.zombie_groups[i]: if target_plant.canAttack(zombie): target_plant.setAttack(zombie, self.zombie_groups[i]) break # 灰烬植物与寒冰菇 elif target_plant.name in c.ASH_PLANTS_AND_ICESHROOM: if target_plant.start_boom and (not target_plant.boomed): # 这样分成两层是因为场上灰烬植物肯定少,一个一个判断代价高,先笼统判断灰烬即可 if target_plant.name in {c.REDWALLNUTBOWLING, c.CHERRYBOMB}: self.boomZombies( target_plant.rect.centerx, i, target_plant.explode_y_range, target_plant.explode_x_range, ) elif target_plant.name == c.DOOMSHROOM: x, y = target_plant.original_x, target_plant.original_y map_x, map_y = self.map.getMapIndex(x, y) self.boomZombies( target_plant.rect.centerx, i, target_plant.explode_y_range, target_plant.explode_x_range, ) for item in self.plant_groups[map_y]: checkMapX, _ = self.map.getMapIndex( item.rect.centerx, item.rect.bottom ) if map_x == checkMapX: item.health = 0 # 为了防止坑显示在蘑菇云前面,这里先不生成坑,仅填位置 self.map.map[map_y][map_x][c.MAP_PLANT].add(c.HOLE) elif target_plant.name == c.JALAPENO: self.boomZombies( target_plant.rect.centerx, i, target_plant.explode_y_range, target_plant.explode_x_range, effect=c.BULLET_EFFECT_UNICE, ) # 消除冰道 for item in self.plant_groups[i]: if item.name == c.ICEFROZENPLOT: item.health = 0 elif target_plant.name == c.ICESHROOM: self.freezeZombies(target_plant) target_plant.boomed = True else: can_attack = False if zombie_len > 0: for zombie in self.zombie_groups[i]: if target_plant.canAttack(zombie): can_attack = True break if target_plant.state == c.IDLE and can_attack: target_plant.setAttack() elif target_plant.state == c.ATTACK and (not can_attack): target_plant.setIdle() def checkPlants(self): for i in range(self.map_y_len): for plant in self.plant_groups[i]: if plant.state != c.SLEEP: self.checkPlant(plant, i) if plant.health <= 0: self.killPlant(plant) def checkVictory(self): if self.map_data[c.SPAWN_ZOMBIES] == c.SPAWN_ZOMBIES_LIST: if len(self.zombie_list) > 0: return False for i in range(self.map_y_len): if len(self.zombie_groups[i]) > 0: return False else: if self.wave_num < self.map_data[c.NUM_FLAGS] * 10: return False for i in range(self.map_y_len): if len(self.zombie_groups[i]) > 0: return False return True def checkLose(self): for i in range(self.map_y_len): for zombie in self.zombie_groups[i]: if ( zombie.rect.right < -20 and (not zombie.losthead) and (zombie.state != c.DIE) ): return True return False def checkGameState(self): if self.checkVictory(): if self.game_info[c.GAME_MODE] == c.MODE_ADVENTURE: self.game_info[c.LEVEL_NUM] += 1 if self.game_info[c.LEVEL_NUM] >= map.TOTAL_LEVEL: self.game_info[c.LEVEL_COMPLETIONS] += 1 self.game_info[c.LEVEL_NUM] = 1 self.next = c.AWARD_SCREEN # 播放大胜利音效 c.SOUND_FINAL_FANFARE.play() else: self.next = c.GAME_VICTORY # 播放胜利音效 c.SOUND_WIN.play() elif self.game_info[c.GAME_MODE] == c.MODE_LITTLEGAME: self.game_info[c.LITTLEGAME_NUM] += 1 if self.game_info[c.LITTLEGAME_NUM] >= map.TOTAL_LITTLE_GAME: self.game_info[c.LITTLEGAME_COMPLETIONS] += 1 self.game_info[c.LITTLEGAME_NUM] = 1 self.next = c.AWARD_SCREEN # 播放大胜利音效 c.SOUND_FINAL_FANFARE.play() else: self.next = c.GAME_VICTORY # 播放胜利音效 c.SOUND_WIN.play() self.done = True self.saveUserData() elif self.checkLose(): # 播放失败音效 c.SOUND_LOSE.play() c.SOUND_SCREAM.play() self.next = c.GAME_LOSE self.done = True def drawMouseShow(self, surface): if self.hint_plant: surface.blit(self.hint_image, self.hint_rect) x, y = pg.mouse.get_pos() self.mouse_rect.centerx = x self.mouse_rect.centery = y surface.blit(self.mouse_image, self.mouse_rect) def drawMouseShowPlus(self, surface): # 拖动铲子时的显示 x, y = pg.mouse.get_pos() self.shovel_rect.centerx = x self.shovel_rect.centery = y # 铲子接近植物时会高亮提示 map_x, map_y = self.map.getMapIndex(x, y) surface.blit(self.shovel, self.shovel_rect) if not self.map.isValid(map_x, map_y): return for i in self.plant_groups[map_y]: if ( x >= i.rect.x and x <= i.rect.right and y >= i.rect.y and y <= i.rect.bottom ): if i.name in c.NON_PLANT_OBJECTS: continue if i.name in c.SKIP_ZOMBIE_COLLISION_CHECK_WHEN_WORKING: if i.start_boom: continue # 优先选中睡莲、花盆上的植物 if len(self.map.map[map_y][map_x][c.MAP_PLANT]) >= 2: if c.LILYPAD in self.map.map[map_y][map_x][c.MAP_PLANT]: if i.name == c.LILYPAD: continue elif '花盆(未实现)' in self.map.map[map_y][map_x][c.MAP_PLANT]: if i.name == '花盆(未实现)': continue i.highlight_time = self.current_time return def drawZombieFreezeTrap(self, i, surface): for zombie in self.zombie_groups[i]: zombie.drawFreezeTrap(surface) def showLevelProgress(self, surface): # 画进度条框 surface.blit( self.level_progress_bar_image, self.level_progress_bar_image_rect ) # 按照当前波数生成僵尸头位置 self.level_progress_zombie_head_image_rect.x = ( self.level_progress_bar_image_rect.x - int((150 * self.wave_num) / (self.map_data[c.NUM_FLAGS] * 10)) + 145 ) # 常数为拟合值 self.level_progress_zombie_head_image_rect.y = ( self.level_progress_bar_image_rect.y - 3 ) # 常数为拟合值 # 填充的进度条信息 # 常数为拟合值 filled_bar_rect = ( self.level_progress_zombie_head_image_rect.x + 3, self.level_progress_bar_image_rect.y + 6, int((150 * self.wave_num) / (self.map_data[c.NUM_FLAGS] * 10)) + 5, 9, ) # 画填充的进度条 pg.draw.rect(surface, c.YELLOWGREEN, filled_bar_rect) # 画旗帜 for i in range(self.num_flags): self.level_progress_flag_rect.x = ( self.level_progress_bar_image_rect.x + int((150 * i) / self.num_flags) + 5 ) # 常数是猜的 # 当指示进度的僵尸头在旗帜左侧时升高旗帜 if ( self.level_progress_flag_rect.x - 7 >= self.level_progress_zombie_head_image_rect.x ): self.level_progress_flag_rect.y = ( self.level_progress_bar_image_rect.y - 15 ) # 常数是猜的 else: self.level_progress_flag_rect.y = ( self.level_progress_bar_image_rect.y - 3 ) # 常数是猜的 surface.blit( self.level_progress_flag, self.level_progress_flag_rect ) # 画僵尸头 surface.blit( self.level_progress_zombie_head_image, self.level_progress_zombie_head_image_rect, ) def showAllContentOfMenu(self, surface): # 绘制不可变内容 surface.blit(self.big_menu, self.big_menu_rect) surface.blit(self.return_button, self.return_button_rect) surface.blit(self.restart_button, self.restart_button_rect) surface.blit(self.mainMenu_button, self.mainMenu_button_rect) surface.blit( self.sound_volume_minus_button, self.sound_volume_minus_button_rect ) surface.blit( self.sound_volume_plus_button, self.sound_volume_plus_button_rect ) # 显示当前音量 # 由于音量可变,因此这一内容不能在一开始就结束加载,而应当不断刷新不断显示 font = pg.font.Font(c.FONT_PATH, 30) volume_tips = font.render( f'音量:{round(self.game_info[c.SOUND_VOLUME]*100):3}%', True, c.LIGHTGRAY, ) volume_tips_rect = volume_tips.get_rect() volume_tips_rect.x = 275 volume_tips_rect.y = 247 surface.blit(volume_tips, volume_tips_rect) def draw(self, surface): self.level.blit(self.background, self.viewport, self.viewport) surface.blit(self.level, (0, 0), self.viewport) if self.state == c.CHOOSE: self.panel.draw(surface) # 画小菜单 surface.blit(self.little_menu, self.little_menu_rect) if self.show_game_menu: self.showAllContentOfMenu(surface) # 以后可能需要插入一个预备的状态(预览显示僵尸、返回战场) elif self.state == c.PLAY: if self.has_shovel: # 画铲子 surface.blit(self.shovel_box, self.shovel_box_rect) surface.blit(self.shovel, self.shovel_rect) # 画小菜单 surface.blit(self.little_menu, self.little_menu_rect) self.menubar.draw(surface) for i in range(self.map_y_len): self.plant_groups[i].draw(surface) self.zombie_groups[i].draw(surface) self.hypno_zombie_groups[i].draw(surface) self.bullet_groups[i].draw(surface) self.drawZombieFreezeTrap(i, surface) if self.cars[i]: self.cars[i].draw(surface) self.head_group.draw(surface) self.sun_group.draw(surface) if self.drag_plant: self.drawMouseShow(surface) if self.has_shovel and self.drag_shovel: self.drawMouseShowPlus(surface) if self.show_game_menu: self.showAllContentOfMenu(surface) if self.map_data[c.SPAWN_ZOMBIES] == c.SPAWN_ZOMBIES_AUTO: self.showLevelProgress(surface) if ( self.current_time - self.show_hugewave_approching_time <= 2000 ): surface.blit( self.huge_wave_approching_image, self.huge_wave_approching_image_rect, ) ================================================ FILE: source/state/mainmenu.py ================================================ import os import pygame as pg from .. import constants as c from .. import tool class Menu(tool.State): def __init__(self): tool.State.__init__(self) def startup(self, current_time: int, persist): self.next = c.LEVEL self.persist = persist self.game_info = persist self.setupBackground() self.setupOptions() self.setupOptionMenu() self.setupSunflowerTrophy() pg.mixer.music.stop() pg.mixer.music.load(os.path.join(c.PATH_MUSIC_DIR, 'intro.opus')) pg.mixer.music.play(-1, 0) pg.display.set_caption(c.ORIGINAL_CAPTION) pg.mixer.music.set_volume(self.game_info[c.SOUND_VOLUME]) for i in c.SOUNDS: i.set_volume(self.game_info[c.SOUND_VOLUME]) def setupBackground(self): frame_rect = (80, 0, 800, 600) # 1、形参中加单星号,即f(*x)则表示x为元组,所有对x的操作都应将x视为元组类型进行。 # 2、双星号同上,区别是x视为字典。 # 3、在变量前加单星号表示将元组(列表、集合)拆分为单个元素。 # 4、双星号同上,区别是目标为字典,字典前加单星号的话可以得到“键”。 self.bg_image = tool.get_image( tool.GFX[c.MAIN_MENU_IMAGE], *frame_rect ) self.bg_rect = self.bg_image.get_rect() self.bg_rect.x = 0 self.bg_rect.y = 0 def setupOptions(self): # 冒险模式 frame_rect = (0, 0, 330, 144) # 写成列表生成器方便IDE识别与自动补全 self.adventure_frames = [ tool.get_image_alpha( tool.GFX[f'{c.OPTION_ADVENTURE}_{i}'], *frame_rect ) for i in range(2) ] self.adventure_image = self.adventure_frames[0] self.adventure_rect = self.adventure_image.get_rect() self.adventure_rect.x = 400 self.adventure_rect.y = 60 self.adventure_highlight_time = 0 # 小游戏 littleGame_frame_rect = (0, 7, 317, 135) self.littleGame_frames = [ tool.get_image_alpha( tool.GFX[f'{c.LITTLEGAME_BUTTON}_{i}'], *littleGame_frame_rect ) for i in range(2) ] self.littleGame_image = self.littleGame_frames[0] self.littleGame_rect = self.littleGame_image.get_rect() self.littleGame_rect.x = 397 self.littleGame_rect.y = 175 self.littleGame_highlight_time = 0 # 退出按钮 exit_frame_rect = (0, 0, 47, 27) self.exit_frames = [ tool.get_image_alpha( tool.GFX[f'{c.EXIT}_{i}'], *exit_frame_rect, scale=1.1 ) for i in range(2) ] self.exit_image = self.exit_frames[0] self.exit_rect = self.exit_image.get_rect() self.exit_rect.x = 730 self.exit_rect.y = 507 self.exit_highlight_time = 0 # 选项按钮 option_button_frame_rect = (0, 0, 81, 31) self.option_button_frames = [ tool.get_image_alpha( tool.GFX[f'{c.OPTION_BUTTON}_{i}'], *option_button_frame_rect ) for i in range(2) ] self.option_button_image = self.option_button_frames[0] self.option_button_rect = self.option_button_image.get_rect() self.option_button_rect.x = 560 self.option_button_rect.y = 490 self.option_button_highlight_time = 0 # 帮助菜单 help_frame_rect = (0, 0, 48, 22) self.help_frames = [ tool.get_image_alpha(tool.GFX[f'{c.HELP}_{i}'], *help_frame_rect) for i in range(2) ] self.help_image = self.help_frames[0] self.help_rect = self.help_image.get_rect() self.help_rect.x = 653 self.help_rect.y = 520 self.help_hilight_time = 0 # 计时器与点击信号记录器 self.adventure_start = 0 self.adventure_timer = 0 self.adventure_clicked = False self.option_button_clicked = False def checkHilight(self, x: int, y: int): # 高亮冒险模式按钮 if self.inArea(self.adventure_rect, x, y): self.adventure_highlight_time = self.current_time # 高亮小游戏按钮 elif self.inArea(self.littleGame_rect, x, y): self.littleGame_highlight_time = self.current_time # 高亮退出按钮 elif self.inArea(self.exit_rect, x, y): self.exit_highlight_time = self.current_time # 高亮选项按钮 elif self.inArea(self.option_button_rect, x, y): self.option_button_highlight_time = self.current_time # 高亮帮助按钮 elif self.inArea(self.help_rect, x, y): self.help_hilight_time = self.current_time # 处理按钮高亮情况 self.adventure_image = self.chooseHilightImage( self.adventure_highlight_time, self.adventure_frames ) self.exit_image = self.chooseHilightImage( self.exit_highlight_time, self.exit_frames ) self.option_button_image = self.chooseHilightImage( self.option_button_highlight_time, self.option_button_frames ) self.littleGame_image = self.chooseHilightImage( self.littleGame_highlight_time, self.littleGame_frames ) self.help_image = self.chooseHilightImage( self.help_hilight_time, self.help_frames ) def chooseHilightImage(self, hilightTime: int, frames): if (self.current_time - hilightTime) < 80: index = 1 else: index = 0 return frames[index] def respondAdventureClick(self): self.adventure_clicked = True self.adventure_timer = self.adventure_start = self.current_time self.persist[c.GAME_MODE] = c.MODE_ADVENTURE # 播放进入音效 pg.mixer.music.stop() c.SOUND_EVILLAUGH.play() c.SOUND_LOSE.play() # 按到小游戏 def respondLittleGameClick(self): self.done = True self.persist[c.GAME_MODE] = c.MODE_LITTLEGAME # 播放点击音效 c.SOUND_BUTTON_CLICK.play() # 点击到退出按钮,修改转态的done属性 def respondExitClick(self): self.done = True self.next = c.EXIT # 帮助按钮点击 def respondHelpClick(self): self.done = True self.next = c.HELP_SCREEN def setupOptionMenu(self): # 选项菜单框 frame_rect = (0, 0, 500, 500) self.big_menu = tool.get_image_alpha( tool.GFX[c.BIG_MENU], *frame_rect, c.BLACK, 1.1 ) self.big_menu_rect = self.big_menu.get_rect() self.big_menu_rect.x = 150 self.big_menu_rect.y = 0 # 返回按钮,用字体渲染实现,增强灵活性 # 建立一个按钮大小的surface对象 self.return_button = pg.Surface((376, 96)) self.return_button.set_colorkey(c.BLACK) # 避免多余区域显示成黑色 self.return_button_rect = self.return_button.get_rect() self.return_button_rect.x = 220 self.return_button_rect.y = 440 font = pg.font.Font(c.FONT_PATH, 40) font.bold = True text = font.render('返回游戏', True, c.YELLOWGREEN) text_rect = text.get_rect() text_rect.x = 105 text_rect.y = 18 self.return_button.blit(text, text_rect) # 音量+、音量- frame_rect = (0, 0, 39, 41) font = pg.font.Font(c.FONT_PATH, 35) font.bold = True # 音量+ self.sound_volume_plus_button = tool.get_image_alpha( tool.GFX[c.SOUND_VOLUME_BUTTON], *frame_rect, c.BLACK ) sign = font.render('+', True, c.YELLOWGREEN) sign_rect = sign.get_rect() sign_rect.x = 8 sign_rect.y = -4 self.sound_volume_plus_button.blit(sign, sign_rect) self.sound_volume_plus_button_rect = ( self.sound_volume_plus_button.get_rect() ) self.sound_volume_plus_button_rect.x = 500 # 音量- self.sound_volume_minus_button = tool.get_image_alpha( tool.GFX[c.SOUND_VOLUME_BUTTON], *frame_rect, c.BLACK ) sign = font.render('-', True, c.YELLOWGREEN) sign_rect = sign.get_rect() sign_rect.x = 12 sign_rect.y = -6 self.sound_volume_minus_button.blit(sign, sign_rect) self.sound_volume_minus_button_rect = ( self.sound_volume_minus_button.get_rect() ) self.sound_volume_minus_button_rect.x = 450 # 音量+、-应当处于同一高度 self.sound_volume_minus_button_rect.y = ( self.sound_volume_plus_button_rect.y ) = 250 def setupSunflowerTrophy(self): # 设置金银向日葵图片信息 if ( self.game_info[c.LEVEL_COMPLETIONS] or self.game_info[c.LITTLEGAME_COMPLETIONS] ): if ( self.game_info[c.LEVEL_COMPLETIONS] and self.game_info[c.LITTLEGAME_COMPLETIONS] ): frame_rect = (157, 0, 157, 269) else: frame_rect = (0, 0, 157, 269) self.sunflower_trophy = tool.get_image_alpha( tool.GFX[c.TROPHY_SUNFLOWER], *frame_rect, c.BLACK ) self.sunflower_trophy_rect = self.sunflower_trophy.get_rect() self.sunflower_trophy_rect.x = 0 self.sunflower_trophy_rect.y = 280 self.sunflower_trophy_show_info_time = 0 def checkSunflowerTrophyInfo(self, surface: pg.Surface, x: int, y: int): if self.inArea(self.sunflower_trophy_rect, x, y): self.sunflower_trophy_show_info_time = self.current_time if (self.current_time - self.sunflower_trophy_show_info_time) < 80: font = pg.font.Font(c.FONT_PATH, 14) if ( self.game_info[c.LEVEL_COMPLETIONS] and self.game_info[c.LITTLEGAME_COMPLETIONS] ): infoText = f'目前您一共完成了:冒险模式{self.game_info[c.LEVEL_COMPLETIONS]}轮,玩玩小游戏{self.game_info[c.LITTLEGAME_COMPLETIONS]}轮' elif self.game_info[c.LEVEL_COMPLETIONS]: infoText = f'目前您一共完成了:冒险模式{self.game_info[c.LEVEL_COMPLETIONS]}轮;完成其他所有游戏模式以获得金向日葵奖杯!' else: infoText = f'目前您一共完成了:玩玩小游戏{self.game_info[c.LITTLEGAME_COMPLETIONS]}轮;完成其他所有游戏模式以获得金向日葵奖杯!' infoImg = font.render(infoText, True, c.BLACK, c.LIGHTYELLOW) infoImg_rect = infoImg.get_rect() infoImg_rect.x = self.sunflower_trophy_rect.x infoImg_rect.y = self.sunflower_trophy_rect.bottom - 14 surface.blit(infoImg, infoImg_rect) def respondOptionButtonClick(self): self.option_button_clicked = True # 播放点击音效 c.SOUND_BUTTON_CLICK.play() def showCurrentVolumeImage(self, surface: pg.Surface): # 由于音量可变,因此这一内容不能在一开始就结束加载,而应当不断刷新不断显示 font = pg.font.Font(c.FONT_PATH, 30) volume_tips = font.render( f'音量:{round(self.game_info[c.SOUND_VOLUME]*100):3}%', True, c.LIGHTGRAY, ) volume_tips_rect = volume_tips.get_rect() volume_tips_rect.x = 275 volume_tips_rect.y = 247 surface.blit(volume_tips, volume_tips_rect) def update( self, surface: pg.Surface, current_time: int, mouse_pos: list, mouse_click, ): self.current_time = self.game_info[c.CURRENT_TIME] = current_time surface.blit(self.bg_image, self.bg_rect) surface.blit(self.adventure_image, self.adventure_rect) surface.blit(self.littleGame_image, self.littleGame_rect) surface.blit(self.exit_image, self.exit_rect) surface.blit(self.option_button_image, self.option_button_rect) surface.blit(self.help_image, self.help_rect) if ( self.game_info[c.LEVEL_COMPLETIONS] or self.game_info[c.LITTLEGAME_COMPLETIONS] ): surface.blit(self.sunflower_trophy, self.sunflower_trophy_rect) # 点到冒险模式后播放动画 if self.adventure_clicked: # 乱写一个不用信号标记的循环播放 QwQ if ((self.current_time - self.adventure_timer) // 150) % 2: self.adventure_image = self.adventure_frames[1] else: self.adventure_image = self.adventure_frames[0] if (self.current_time - self.adventure_start) > 3200: self.done = True # 点到选项按钮后显示菜单 elif self.option_button_clicked: surface.blit(self.big_menu, self.big_menu_rect) surface.blit(self.return_button, self.return_button_rect) surface.blit( self.sound_volume_plus_button, self.sound_volume_plus_button_rect, ) surface.blit( self.sound_volume_minus_button, self.sound_volume_minus_button_rect, ) self.showCurrentVolumeImage(surface) if mouse_pos: # 返回 if self.inArea(self.return_button_rect, *mouse_pos): self.option_button_clicked = False c.SOUND_BUTTON_CLICK.play() # 音量+ elif self.inArea( self.sound_volume_plus_button_rect, *mouse_pos ): self.game_info[c.SOUND_VOLUME] = round( min(self.game_info[c.SOUND_VOLUME] + 0.05, 1), 2 ) # 一般不会有人想把音乐和音效分开设置,故pg.mixer.Sound.set_volume()和pg.mixer.music.set_volume()需要一起用 pg.mixer.music.set_volume(self.game_info[c.SOUND_VOLUME]) for i in c.SOUNDS: i.set_volume(self.game_info[c.SOUND_VOLUME]) c.SOUND_BUTTON_CLICK.play() self.saveUserData() # 音量- elif self.inArea( self.sound_volume_minus_button_rect, *mouse_pos ): self.game_info[c.SOUND_VOLUME] = round( max(self.game_info[c.SOUND_VOLUME] - 0.05, 0), 2 ) # 一般不会有人想把音乐和音效分开设置,故pg.mixer.Sound.set_volume()和pg.mixer.music.set_volume()需要一起用 pg.mixer.music.set_volume(self.game_info[c.SOUND_VOLUME]) for i in c.SOUNDS: i.set_volume(self.game_info[c.SOUND_VOLUME]) c.SOUND_BUTTON_CLICK.play() self.saveUserData() # 没有点到前两者时常规行检测所有按钮的点击和高亮 else: # 先检查选项高亮预览 x, y = pg.mouse.get_pos() self.checkHilight(x, y) if ( self.game_info[c.LEVEL_COMPLETIONS] or self.game_info[c.LITTLEGAME_COMPLETIONS] ): self.checkSunflowerTrophyInfo(surface, x, y) if mouse_pos: if self.inArea(self.adventure_rect, *mouse_pos): self.respondAdventureClick() elif self.inArea(self.littleGame_rect, *mouse_pos): self.respondLittleGameClick() elif self.inArea(self.option_button_rect, *mouse_pos): self.respondOptionButtonClick() elif self.inArea(self.exit_rect, *mouse_pos): self.respondExitClick() elif self.inArea(self.help_rect, *mouse_pos): self.respondHelpClick() ================================================ FILE: source/state/screen.py ================================================ import os from abc import abstractmethod import pygame as pg from .. import constants as c from .. import tool class Screen(tool.State): def __init__(self): tool.State.__init__(self) @abstractmethod def startup(self, current_time, persist): pass def setupImage(self, name, frame_rect=(0, 0, 800, 600), color_key=c.BLACK): # 背景图本身 self.image = tool.get_image( tool.GFX[name], *frame_rect, colorkey=color_key ) self.rect = self.image.get_rect() self.rect.x = 0 self.rect.y = 0 # 按钮 frame_rect = (0, 0, 111, 26) ## 主菜单按钮 self.main_menu_button_image = tool.get_image_alpha( tool.GFX[c.UNIVERSAL_BUTTON], *frame_rect ) self.main_menu_button_image_rect = ( self.main_menu_button_image.get_rect() ) self.main_menu_button_image_rect.x = 620 ### 主菜单按钮上的文字 font = pg.font.Font(c.FONT_PATH, 18) main_menu_text = font.render('主菜单', True, c.NAVYBLUE) main_menu_text_rect = main_menu_text.get_rect() main_menu_text_rect.x = 29 ## 继续按钮 self.next_button_image = tool.get_image_alpha( tool.GFX[c.UNIVERSAL_BUTTON], *frame_rect ) self.next_button_image_rect = self.next_button_image.get_rect() self.next_button_image_rect.x = 70 ### 继续按钮上的文字 if name == c.GAME_VICTORY_IMAGE: next_text = font.render('下一关', True, c.NAVYBLUE) next_text_rect = next_text.get_rect() next_text_rect.x = 29 self.next_button_image_rect.y = ( self.main_menu_button_image_rect.y ) = 555 else: next_text = font.render('重新开始', True, c.NAVYBLUE) next_text_rect = next_text.get_rect() next_text_rect.x = 21 self.next_button_image_rect.y = ( self.main_menu_button_image_rect.y ) = 530 self.next_button_image.blit(next_text, next_text_rect) self.main_menu_button_image.blit(main_menu_text, main_menu_text_rect) self.image.blit(self.next_button_image, self.next_button_image_rect) self.image.blit( self.main_menu_button_image, self.main_menu_button_image_rect ) def update(self, surface, current_time, mouse_pos, mouse_click): surface.fill(c.WHITE) surface.blit(self.image, self.rect) if mouse_pos: # 点到继续 if self.inArea(self.next_button_image_rect, *mouse_pos): self.next = c.LEVEL self.done = True # 点到主菜单 elif self.inArea(self.main_menu_button_image_rect, *mouse_pos): self.next = c.MAIN_MENU self.done = True class GameVictoryScreen(Screen): def __init__(self): Screen.__init__(self) self.image_name = c.GAME_VICTORY_IMAGE def startup(self, current_time, persist): self.start_time = current_time self.persist = persist self.game_info = persist self.setupImage(self.image_name) pg.display.set_caption('pypvz: 战斗胜利!') pg.mixer.music.stop() pg.mixer.music.load(os.path.join(c.PATH_MUSIC_DIR, 'zenGarden.opus')) pg.mixer.music.play(-1, 0) pg.mixer.music.set_volume(self.game_info[c.SOUND_VOLUME]) class GameLoseScreen(Screen): def __init__(self): Screen.__init__(self) self.image_name = c.GAME_LOSE_IMAGE def startup(self, current_time, persist): self.start_time = current_time self.persist = persist self.game_info = persist self.setupImage(self.image_name, (-118, -40, 800, 600), c.WHITE) pg.display.set_caption('pypvz: 战斗失败!') # 停止播放原来关卡中的音乐 pg.mixer.music.stop() class AwardScreen(tool.State): def __init__(self): tool.State.__init__(self) def setupImage(self): # 主体 frame_rect = (0, 0, 800, 600) self.image = tool.get_image( tool.GFX[c.AWARD_SCREEN_IMAGE], *frame_rect ) self.rect = self.image.get_rect() self.rect.x = 0 self.rect.y = 0 # 文字 # 标题处文字 font = pg.font.Font(c.FONT_PATH, 37) title_text = font.render('您获得了新的战利品!', True, c.PARCHMENT_YELLOW) title_text_rect = title_text.get_rect() title_text_rect.x = 220 title_text_rect.y = 23 self.image.blit(title_text, title_text_rect) # 按钮 frame_rect = (0, 0, 111, 26) if self.show_only_one_option: ## 主菜单按钮 self.main_menu_button_image = tool.get_image_alpha( tool.GFX[c.UNIVERSAL_BUTTON], *frame_rect ) self.main_menu_button_image_rect = ( self.main_menu_button_image.get_rect() ) self.main_menu_button_image_rect.x = 343 self.main_menu_button_image_rect.y = 520 ### 主菜单按钮上的文字 font = pg.font.Font(c.FONT_PATH, 18) main_menu_text = font.render('主菜单', True, c.NAVYBLUE) main_menu_text_rect = main_menu_text.get_rect() main_menu_text_rect.x = 29 self.main_menu_button_image.blit( main_menu_text, main_menu_text_rect ) self.image.blit( self.main_menu_button_image, self.main_menu_button_image_rect ) # 绘制向日葵奖杯 if ( self.game_info[c.LEVEL_COMPLETIONS] and self.game_info[c.LITTLEGAME_COMPLETIONS] ): frame_rect = (157, 0, 157, 269) intro_title = '金向日葵奖杯' intro_content = '您已通过所有关卡,获得此奖励!' else: frame_rect = (0, 0, 157, 269) intro_title = '银向日葵奖杯' if self.game_info[c.LEVEL_COMPLETIONS]: intro_content = '您已完成冒险模式,获得此奖励!' else: intro_content = '您已完成玩玩小游戏,获得此奖励!' sunflower_trophy_image = tool.get_image_alpha( tool.GFX[c.TROPHY_SUNFLOWER], *frame_rect, scale=0.7 ) sunflower_trophy_rect = sunflower_trophy_image.get_rect() sunflower_trophy_rect.x = 348 sunflower_trophy_rect.y = 108 self.image.blit(sunflower_trophy_image, sunflower_trophy_rect) # 绘制介绍标题 font = pg.font.Font(c.FONT_PATH, 22) intro_title_img = font.render( intro_title, True, c.PARCHMENT_YELLOW ) intro_title_rect = intro_title_img.get_rect() intro_title_rect.x = 333 intro_title_rect.y = 305 self.image.blit(intro_title_img, intro_title_rect) # 绘制介绍内容 font = pg.font.Font(c.FONT_PATH, 15) intro_content_img = font.render(intro_content, True, c.NAVYBLUE) intro_content_rect = intro_content_img.get_rect() intro_content_rect.x = 290 intro_content_rect.y = 370 self.image.blit(intro_content_img, intro_content_rect) else: ## 继续按钮 self.next_button_image = tool.get_image_alpha( tool.GFX[c.UNIVERSAL_BUTTON], *frame_rect ) self.next_button_image_rect = self.next_button_image.get_rect() self.next_button_image_rect.x = 70 ### 继续按钮上的文字 font = pg.font.Font(c.FONT_PATH, 18) next_text = font.render('继续', True, c.NAVYBLUE) next_text_rect = next_text.get_rect() next_text_rect.x = 37 ## 主菜单按钮 self.main_menu_button_image = tool.get_image_alpha( tool.GFX[c.UNIVERSAL_BUTTON], *frame_rect ) self.main_menu_button_image_rect = ( self.main_menu_button_image.get_rect() ) self.main_menu_button_image_rect.x = 620 self.next_button_image_rect.y = ( self.main_menu_button_image_rect.y ) = 540 ### 主菜单按钮上的文字 main_menu_text = font.render('主菜单', True, c.NAVYBLUE) main_menu_text_rect = main_menu_text.get_rect() main_menu_text_rect.x = 29 self.next_button_image.blit(next_text, next_text_rect) self.main_menu_button_image.blit( main_menu_text, main_menu_text_rect ) self.image.blit( self.next_button_image, self.next_button_image_rect ) self.image.blit( self.main_menu_button_image, self.main_menu_button_image_rect ) def startup(self, current_time, persist): self.start_time = current_time self.persist = persist self.game_info = persist if (c.PASSED_ALL in self.game_info) and ( not self.game_info[c.PASSED_ALL] ): self.show_only_one_option = False else: self.show_only_one_option = True self.setupImage() pg.display.set_caption('pypvz: 您获得了新的战利品!') pg.mixer.music.stop() pg.mixer.music.load(os.path.join(c.PATH_MUSIC_DIR, 'zenGarden.opus')) pg.mixer.music.play(-1, 0) pg.mixer.music.set_volume(self.game_info[c.SOUND_VOLUME]) def update(self, surface, current_time, mouse_pos, mouse_click): surface.blit(self.image, self.rect) if mouse_pos: # 检查主菜单点击 if self.inArea(self.main_menu_button_image_rect, *mouse_pos): self.next = c.MAIN_MENU self.done = True elif not self.show_only_one_option: if self.inArea(self.next_button_image_rect, *mouse_pos): self.next = c.LEVEL self.done = True class HelpScreen(tool.State): def __init__(self): tool.State.__init__(self) def startup(self, current_time, persist): self.start_time = current_time self.persist = persist self.game_info = persist self.setupImage() pg.display.set_caption('pypvz: 帮助') pg.mixer.music.stop() c.SOUND_HELP_SCREEN.play() def setupImage(self): # 主体 frame_rect = (-100, -50, 800, 600) self.image = tool.get_image( tool.GFX[c.HELP_SCREEN_IMAGE], *frame_rect, colorkey=(0, 255, 255) ) self.rect = self.image.get_rect() self.rect.x = 0 self.rect.y = 0 # 主菜单按钮 frame_rect = (0, 0, 111, 26) self.main_menu_button_image = tool.get_image_alpha( tool.GFX[c.UNIVERSAL_BUTTON], *frame_rect ) self.main_menu_button_image_rect = ( self.main_menu_button_image.get_rect() ) self.main_menu_button_image_rect.x = 343 self.main_menu_button_image_rect.y = 500 ### 主菜单按钮上的文字 font = pg.font.Font(c.FONT_PATH, 18) main_menu_text = font.render('主菜单', True, c.NAVYBLUE) main_menu_text_rect = main_menu_text.get_rect() main_menu_text_rect.x = 29 self.main_menu_button_image.blit(main_menu_text, main_menu_text_rect) self.image.blit( self.main_menu_button_image, self.main_menu_button_image_rect ) def update(self, surface, current_time, mouse_pos, mouse_click): surface.fill(c.BLACK) surface.blit(self.image, self.rect) if mouse_pos: # 检查主菜单点击 if self.inArea(self.main_menu_button_image_rect, *mouse_pos): self.next = c.MAIN_MENU self.done = True ================================================ FILE: source/tool.py ================================================ import json import logging import os from abc import abstractmethod import pygame as pg from pygame.locals import * from . import constants as c logger = logging.getLogger('main') # 状态机 抽象基类 class State: def __init__(self): self.start_time = 0 self.current_time = 0 self.done = False # false 代表未做完 self.next = None # 表示这个状态退出后要转到的下一个状态 self.persist = {} # 在状态间转换时需要传递的数据 # 当从其他状态进入这个状态时,需要进行的初始化操作 @abstractmethod def startup(self, current_time: int, persist: dict): # 前面加了@abstractmethod表示抽象基类中必须要重新定义的method(method是对象和函数的结合) pass # 当从这个状态退出时,需要进行的清除操作 def cleanup(self): self.done = False return self.persist # 在这个状态运行时进行的更新操作 @abstractmethod def update(self, surface: pg.Surface, keys, current_time: int): # 前面加了@abstractmethod表示抽象基类中必须要重新定义的method pass # 工具:范围判断函数,用于判断点击 def inArea(self, rect: pg.Rect, x: int, y: int): if rect.x <= x <= rect.right and rect.y <= y <= rect.bottom: return True else: return False # 工具:用户数据保存函数 def saveUserData(self): with open(c.USERDATA_PATH, 'w', encoding='utf-8') as f: userdata = {} for i in self.game_info: if i in c.INIT_USERDATA: userdata[i] = self.game_info[i] data_to_save = json.dumps(userdata, sort_keys=True, indent=4) f.write(data_to_save) # 进行游戏控制 循环 事件响应 class Control: def __init__(self): self.screen = pg.display.get_surface() self.done = False self.clock = pg.time.Clock() # 创建一个对象来帮助跟踪时间 self.keys = pg.key.get_pressed() self.mouse_pos = None self.mouse_click = [ False, False, ] # value:[left mouse click, right mouse click] self.current_time = 0.0 self.state_dict = {} self.state_name = None self.state = None try: # 存在存档即导入 # 先自动修复读写权限(Python权限规则和Unix不一样,420表示unix的644,Windows自动忽略不支持项) os.chmod(c.USERDATA_PATH, 420) with open(c.USERDATA_PATH, encoding='utf-8') as f: userdata = json.load(f) except FileNotFoundError: self.setupUserData() except json.JSONDecodeError: logger.warning('用户存档解码错误!程序将新建初始存档!\n') self.setupUserData() else: # 没有引发异常才执行 self.game_info = {} # 导入数据,保证了可运行性,但是放弃了数据向后兼容性,即假如某些变量在以后改名,在导入时可能会被重置 need_to_rewrite = False for key in c.INIT_USERDATA: if key in userdata: self.game_info[key] = userdata[key] else: self.game_info[key] = c.INIT_USERDATA[key] need_to_rewrite = True if need_to_rewrite: with open(c.USERDATA_PATH, 'w', encoding='utf-8') as f: savedata = json.dumps( self.game_info, sort_keys=True, indent=4 ) f.write(savedata) # 存档内不包含即时游戏时间信息,需要新建 self.game_info[c.CURRENT_TIME] = 0 # 50为目前的基础帧率,乘以倍率即是游戏帧率 self.fps = 50 * self.game_info[c.GAME_RATE] def setupUserData(self): if not os.path.exists(os.path.dirname(c.USERDATA_PATH)): os.makedirs(os.path.dirname(c.USERDATA_PATH)) with open(c.USERDATA_PATH, 'w', encoding='utf-8') as f: savedata = json.dumps(c.INIT_USERDATA, sort_keys=True, indent=4) f.write(savedata) self.game_info = c.INIT_USERDATA.copy() # 内部全是不可变对象,浅拷贝即可 def setup_states(self, state_dict: dict, start_state): self.state_dict = state_dict self.state_name = start_state self.state = self.state_dict[self.state_name] self.state.startup(self.current_time, self.game_info) def update(self): # 自 pygame_init() 调用以来的毫秒数 * 游戏速度倍率,即游戏时间 self.current_time = pg.time.get_ticks() * self.game_info[c.GAME_RATE] if self.state.done: self.flip_state() self.state.update( self.screen, self.current_time, self.mouse_pos, self.mouse_click ) self.mouse_pos = None self.mouse_click[0] = False self.mouse_click[1] = False # 状态转移 def flip_state(self): if self.state.next == c.EXIT: pg.quit() os._exit(0) self.state_name = self.state.next persist = self.state.cleanup() self.state = self.state_dict[self.state_name] self.state.startup(self.current_time, persist) def event_loop(self): for event in pg.event.get(): if event.type == pg.QUIT: self.done = True elif event.type == pg.KEYDOWN: self.keys = pg.key.get_pressed() if event.key == pg.K_f: pg.display.set_mode( c.SCREEN_SIZE, pg.HWSURFACE | pg.FULLSCREEN ) elif event.key == pg.K_u: pg.display.set_mode(c.SCREEN_SIZE) elif event.type == pg.KEYUP: self.keys = pg.key.get_pressed() elif event.type == pg.MOUSEBUTTONDOWN: self.mouse_pos = pg.mouse.get_pos() ( self.mouse_click[0], _, self.mouse_click[1], ) = pg.mouse.get_pressed() # self.mouse_click[0]表示左键,self.mouse_click[1]表示右键 print( f'点击位置: ({self.mouse_pos[0]:3}, {self.mouse_pos[1]:3}) 左右键点击情况: {self.mouse_click}' ) def run(self): while not self.done: self.event_loop() self.update() pg.display.update() self.clock.tick(self.fps) def get_image( sheet: pg.Surface, x: int, y: int, width: int, height: int, colorkey: tuple[int] = c.BLACK, scale: int = 1, ) -> pg.Surface: # 不保留alpha通道的图片导入 image = pg.Surface([width, height]) rect = image.get_rect() image.blit(sheet, (0, 0), (x, y, width, height)) if colorkey: image.set_colorkey(colorkey) image = pg.transform.scale( image, (int(rect.width * scale), int(rect.height * scale)) ) return image def get_image_alpha( sheet: pg.Surface, x: int, y: int, width: int, height: int, colorkey: tuple[int] = c.BLACK, scale: int = 1, ) -> pg.Surface: # 保留alpha通道的图片导入 image = pg.Surface([width, height], SRCALPHA) rect = image.get_rect() image.blit(sheet, (0, 0), (x, y, width, height)) image.set_colorkey(colorkey) image = pg.transform.scale( image, (int(rect.width * scale), int(rect.height * scale)) ) return image def load_image_frames( directory: str, image_name: str, colorkey: tuple[int], accept: tuple[str] ) -> list[pg.Surface]: frame_list = [] tmp = {} # image_name is "Peashooter", pic name is "Peashooter_1", get the index 1 index_start = len(image_name) + 1 frame_num = 0 for pic in os.listdir(directory): name, ext = os.path.splitext(pic) if ext.lower() in accept: index = int(name[index_start:]) img = pg.image.load(os.path.join(directory, pic)) if img.get_alpha(): img = img.convert_alpha() else: img = img.convert() img.set_colorkey(colorkey) tmp[index] = img frame_num += 1 for i in range(frame_num): # 这里注意编号必须连续,否则会出错 frame_list.append(tmp[i]) return frame_list # colorkeys 是设置图像中的某个颜色值为透明,这里用来消除白边 def load_all_gfx( directory: str, colorkey: tuple[int] = c.WHITE, accept: tuple[str] = ('.png', '.jpg', '.bmp', '.gif', '.webp'), ) -> dict[str : pg.Surface]: graphics = {} for name1 in os.listdir(directory): # subfolders under the folder resources\graphics dir1 = os.path.join(directory, name1) if os.path.isdir(dir1): for name2 in os.listdir(dir1): dir2 = os.path.join(dir1, name2) if os.path.isdir(dir2): # e.g. subfolders under the folder resources\graphics\Zombies for name3 in os.listdir(dir2): dir3 = os.path.join(dir2, name3) # e.g. subfolders or pics under the folder resources\graphics\Zombies\ConeheadZombie if os.path.isdir(dir3): # e.g. it"s the folder resources\graphics\Zombies\ConeheadZombie\ConeheadZombieAttack image_name, _ = os.path.splitext(name3) graphics[image_name] = load_image_frames( dir3, image_name, colorkey, accept ) else: # e.g. pics under the folder resources\graphics\Plants\Peashooter image_name, _ = os.path.splitext(name2) graphics[image_name] = load_image_frames( dir2, image_name, colorkey, accept ) break else: # e.g. pics under the folder resources\graphics\Screen name, ext = os.path.splitext(name2) if ext.lower() in accept: img = pg.image.load(dir2) if img.get_alpha(): img = img.convert_alpha() else: img = img.convert() img.set_colorkey(colorkey) graphics[name] = img return graphics pg.display.set_caption(c.ORIGINAL_CAPTION) # 设置标题 SCREEN = pg.display.set_mode(c.SCREEN_SIZE, pg.SCALED) # 设置初始屏幕 pg.mixer.set_num_channels(255) # 设置可以同时播放的音频数量,默认为8,经常不够用 if os.path.exists( c.ORIGINAL_LOGO ): # 设置窗口图标,仅对非Nuitka时生效,Nuitka不需要包括额外的图标文件,自动跳过这一过程即可 pg.display.set_icon(pg.image.load(c.ORIGINAL_LOGO)) GFX = load_all_gfx(c.PATH_IMG_DIR)