植物大战僵尸 Steam 版键控指南

贴吧里的键控资料大部分都是针对第一版(即 1.0.0.1051 版)的,偶尔也有吧友问过 Steam 版该如何键控,但得到的答案都是【没法键控】。
但,实际上不是没法键控,而是大家都没去做而已。办法还是有的。
1. 为什么非 Steam 版不可
先说我为啥要做 Steam 版的键控框架。首先,我的 PvZ 是从 Steam 上买的,就是很朴素地想为童年补个票而已。当然,童年时我玩的是只有冒险模式 + 小游戏的 PvZ,生存无尽因为没有掌握布阵知识,胡搞瞎搞,最多撑到 20 flags 就没了。我入了 Steam 版后才真正开始玩无尽,现在五大场地的无尽都已经过了 400 flags 了。我的心血全在 Steam 版上,自然没有动力切换到原版重新开始。
另外 Steam 版修复了原版的上界之风 bug,同时还平衡了舞王,让伴舞不至于一出土就开始啃 5、6 列炮,相对于原版来说,对萌新更友好一点。这也是我留在 Steam 版的原因之一(虽然不是主要原因)。
2. 准备工作
这里我们要用到 @囧丫乙 大佬做好的框架:pvzscripts。这是一个用 Python 写的,作用于 1.0.0.1051 和 1.2.0.1065 版本的键控框架。我们在此基础上稍做修改,便可得到一个适配 Steam 版本的框架。地址:https://github.com/lmintlcx/pvzscripts

我们将其克隆下来。打开命令提示符,输入如下命令:
以上代码会将整个仓库克隆到你的电脑上的 E:\pvzscripts 目录里。当然这个目录名你可以自己任意指定,想克隆到别的地方也是可以的。以下为了方便说明,我全部假设你的仓库目录为 E:\pvzscripts。
然后,我们要对原始代码做一些修改。用 Visual Studio Code 打开 E:\pvzscripts 目录,然后打开 E:\pvzscripts\pvz\core.py 文件,找到 find_pvz 函数,做如下修改:

特别注意:这里之所以写 elif True,是因为我暂时没有找到检测 Steam 版本的方法,所以这里假设既不是 1.0.0.1051 又不是 1.2.0.1065 时就一定是 Steam 版。这只是让框架能跑起来的一个临时办法,是不严谨的。一旦后续找到了判断手段,我也会相应地更新判断条件。
接下来,找到 read_memory 函数,做如下修改:
如图所示,read_memory 函数一共有两处地方需要做这样的修改:

两条 critial 语句的作用是“让程序抛出异常并输出日志”,但在此之前我们必须要将内存锁给释放掉,否则程序无法正常结束。对于接下来的 write_memory 函数,也是同理:

找到 asm_code_inject_safely 函数,做出如下修改:

接下来对 E:\pvzscripts\pvz\extra.py 文件进行修改。其实本质都是在读内存,只不过不同版本间的内存地址不一样而已。所以我们需要改动的只是各种内存地址的数字而已。
Steam 版的大部分内存地址我是通过查阅 @囧丫乙 做的修改器 PvZ Toolkit 里的源码找到的(地址:https://github.com/lmintlcx/pvztoolkit)。还有少部分地址是通过各种百度、谷歌,以及自己瞎试找出来的。
由于不同版本的内存基址不一样,所以我们有必要用三个变量来存储一下当前运行中的版本的内存基址、二级偏移、三级偏移。首先我们打开 extra.py 文件,在 from .core import * 后面加上这样的代码:

然后我们开始对系统 API 进行修改。修改的内容大同小异,都是将原先写死的内存基址、二级偏移、三级偏移常量改写成变量。
game_ui、game_mode、game_scene 函数:

game_paused、mouse_in_game、mouse_have_something、game_clock 函数:

wave_init_countdown、wave_countdown、huge_wave_countdown、current_wave 函数:

下面的和修改出怪相关的代码,由于涉及到写内存,危险度较大,再加上我平时冲关的时候并不会去调整出怪,都是见招拆招,所以这部分我就没有适配了,判断到是 Steam 版时直接短路 return:




接下来是模仿者的点击坐标。实际使用时发现点击 (490, 550) ,即模仿者的中心点时,特别容易点到商店、图鉴甚至是僵尸身上,经常连续四次选卡都失败。改成 (470, 580) 即模仿者的左下角时,问题有了显著改善。因此把 IMITATER_X 改成 470,把 IMITATER_Y 改成 580。

继续,select_seed_by_crood 函数:

slots_exact_match、clear_slots 函数:

select_all_seeds 函数:

lets_rock 函数:

update_seeds_list 函数:

update_game_scene 函数:

update_cob_cannon_list 函数:

auto_collect 函数:

get_seeds_index、get_plants_croods 函数:

get_block_type 函数:

auto_fill_ice 函数:

下面这个读取阳光和寒冰菇价格的部分我改动的地方比较多。原代码里寒冰菇的价格是通过读取一个特定内存找到的,但是这片内存在 Steam 版里对应的地址是多少,我并没有找到。后来想了想,决定不读内存,转而把价格写死为 75。于是就改成了下面这个样子:

至此,我们还有最后一处地方要改动。我们需要在初始化框架的时候调用 extra 里的 set_addr 函数,以便正确设置内存基址、二级偏移、三级偏移。我们打开 __init__.py 文件,找到 on_start 函数,然后添加以下代码:

改动完成后,别忘了保存这些代码文件。然后,我们就可以愉快地编写 Steam 版的键控脚本啦!
3. 脚本中可以使用的函数
SelectCards,在选卡界面选择本轮要使用的卡片,参数为字符串数组,指定卡片时不是必须使用原名,也可以使用各种别名。对于模仿者,可以在原卡名字前加上“模仿者”、“复制”、“白”等前缀。必须在选卡时调用,其余时刻调用无效。举例:
UpdatePaoList,更新场上的炮列表。参数为玉米炮的后轮坐标数组。通常情况下无需调用此函数,导入 pvz 库时程序会自动扫描一遍场上的玉米炮。该函数一般在收尾阶段使用,使用指定的炮收尾,以防本次选卡结束后,下一次运行脚本时发炮顺序错乱。举例:
Prejudge,参数为一个相对时间和一个波数,令程序阻塞到指定波数的指定时间。举例:
该函数每波都需要调用一次。首波有 6 秒的准备时间,Prejudge 时间最早可以到 -599;旗帜波有 7.5 秒的准备时间(红字时间),Prejudge 时间最早可以到 -750;通常波则只有 2 秒的准备时间,Prejudge 时间最早到 -200。
但是这里要注意,Python 的精度没有 C/C++ 那么高,如果 Prejudge 的时间点过于极限,则有可能导致实际调用 Prejudge 时,需要到达的时间已经过去,导致程序抛出异常。所以这里建议使用 -195 等时间,留出一些误差余量。
Delay,参数为一个时间长度。令程序阻塞这么长时间。游戏暂停时,该计时也会跟着暂停。举例:
Until,参数为一个时间点,令程序阻塞到当前波的指定时间。实际效果跟 Prejudge 一样,区别在于 Prejudge 只能每波调用一次,而 Until 在同一波内不限制调用次数。举例:
Card,参数为卡片名称,及放置卡片的 (y, x) 坐标。在 (y, x) 处用指定的卡。举例:
Shovel,参数为一个或多个 (y, x) 坐标。对指定的坐标使用铲子。举例:
注意,当对应的格子有南瓜,且想要铲除的是南瓜时,需要将对应的 y 坐标向下偏移 0.1,以免铲到南瓜里的本体。
Pao,参数为一个或多个 (y, x) 坐标。向指定的位置发炮。由于 Steam 版修复了上界之风 bug,所以屋顶发炮仍然可以直接调用 Pao 函数,不需要像原版那样调用 RoofPao 函数。举例:
Skip,跳炮。基本用于收尾波,跳过一定数量的炮,以便使用指定的炮收尾。
AutoCollect,自动收集。参数为字符串数组,用于指定需要收集的掉落物品。可以选择阳光、幼苗、银币、金币、钻石(美中不足的是无法收集巧克力)。如不写,则默认全部收集。该函数需要在选卡完成后,逐波拆解前调用。举例:
IceSpots,自动存冰。第一个参数为 (y, x) 坐标数组,每一个坐标对应一个存冰位。程序会按照坐标先后顺序依次尝试存冰,所以需要把安全的位置写在前面,危险的位置写在后面。第二个参数为本轮的存冰数量,累计存冰达到该值时会退出存冰,后面即使寒冰菇的 CD 拖满了也不会继续存冰了。为了保证冰平衡,每次选卡后的存冰数量应该和点冰数量保持一致。举例:
Coffee,点冰。没有参数。程序会扫描 IceSpots 里指定的存冰位,然后反向点冰,先点危险的冰,后点安全的冰。如果你在 IceSpots 里指定了 (4, 6) 和 (3, 6) 两个存冰位,那么当你调用 Coffee() 点冰时,会先尝试点 (3, 6) 位置的冰,(3, 6) 处无冰时再尝试 (4, 6)。
注意:夜晚场景下寒冰菇无法像白天一样休息,请不要调用 IceSpots 和 Coffee 函数来存冰或点冰,请使用 Card 函数手动放置蓝冰、白冰卡片。
4. 示例程序——PE 神之六炮
PE 神之六炮的原阵如下:

这里我做了微调:

这样调整的原因是,核杀小偷时,如果小偷偷 5 列向日葵,那必须要用 8 列核弹才能消灭,不能用 9 列核弹;微调后,向日葵进入伞叶的保护范围,核弹可以放在 9 列了。
此阵使用 ch5 的双冰变奏运行,轨道为:I-PP | I-PP | PP (15, 15, 6)。由于年度舞王(相比于经典的 MJ 舞王)对前置炮的威胁大大降低,因此这里微调节奏,加速波延长至 7.25s 确保不漏撑杆,减速波提速至 14.5s 减少海豚对水路南瓜的啃食。
现在,我们在 E:\pvzscripts 仓库目录下新建一个 pe-god6p.py 的脚本文件,用于运行 PE 神之六炮。脚本代码如下:
首先,我定义了一个函数 ZombieCounts,它用于获得指定名字的僵尸在每一波中出现的数量,结果用一个长度为 21 的数组表示。其中第 0 格空置,第 1~20 格记录指定的僵尸在这一波里出现了多少只。举例,调用 ZombieCounts("红眼")[10] 可以得到第 10 波红眼的数量。
另外,我还定义了一个函数叫 AliveTiaoTiao,用于检测场上存活的跳跳。由于游戏机制问题,一对玉米炮炸 2、5 路时,1 路在空中的跳跳有概率躲过玉米炮的攻击(在空中的跳跳相当于 0 路跳跳,没有 z 轴,y 轴来凑)。若改为炸 1、5 路,则又会漏掉 3 路的僵尸。所以我这里写了一个额外的函数用于防御这些漏网之鱼。这个函数的代码参考了库里的示例程序【信息读取.py】中的代码,并修改了一些内存地址值。它的用途是,调用时检测场上是否有存活的 1 路跳跳,并返回存活的数量。
然后脚本就开始运行了。首先看第一行代码:
pao 这个量用于指示接下来要发几号炮。神之六炮场上有 6 门炮,按 0~5 编号。按照所在列数排序,0~1 号炮为水路底线的两门炮,2~3 号炮为水路位于向日葵右方的两门炮,4~5 号炮为岸上的两门炮。后续的代码里,每发射一对炮,就将该值 +2 并对 6 取余,以确保该指针始终指向下一次发射的炮的编号。
接下来的六行代码:
读取了冰车、白眼、红眼、小偷、铁桶、跳跳这些特殊僵尸每波各出现多少只,以便将来运行脚本时能对症下药。
然后是选卡、开启自动收集、开启自动存冰这三步曲:
第二行代码原先应该写成 AutoCollect() 的,但是这个框架里自带的自动收集 API 非常难用,时常会发生错误点击的事情。为了提高稳定性,这里改为写内存打开游戏内置的自动收集。向 0x004352f2 写入 0xeb 这一个字节即可。
重头戏来了,逐波拆解。直接一条 for 语句,进入 w = 1~20 的循环。注意 Python 里的区间是左闭右开的,所以用了 range(1, 21),意思是 1 ≤ w < 21。
如果是小波,则阻塞到 -195 的相对时间;如果是大波,则阻塞到 +5 的相对时间。内置的 Prejudge 函数有 bug:大波阻塞时,如果阻塞的时间小于 0,则有概率抛出【到 xxx 的相对时间已过去,当前相对时间 0】的异常,原因不明。由于该脚本里,大波不需要预判炸或预判冰,所以这里改为阻塞到 +5 的相对时间以对抗此 bug。
接下来有两个 if...elif...else 块,第一个块用来书写正常运阵时的操作,第二个块用来书写第 9、19 和 20 波的收尾操作。首先看第一个 if...elif...else 块:
这里我先列一个表格来表示一下每波执行的操作,以及波长:

可以看到,有三种不同类型的波:尾数是 1、2、5、8 的波为常规快波,尾数是 3、4、6、7、9 的波为常规慢波(其中尾数为 9 的波需要额外做中场拖尾处理),最后尾数为 0 的波是两个旗帜波,需要做一些特殊操作。
我们首先处理常规快波。由于每一波都有 2s 的准备时间,所以波长 7.25s 的快波需要确保 5.25s 时间炮要落地。而炮的飞行时间为 3.73s,这里取 0.25 的倍数,视为 3.75s。我们需要在 5.25 - 3.75s 的时刻发炮。
然后开始处理常规慢波。首先当然要炮落地点冰,激活预判冰。然后,和快波是类似的道理,慢波波长为 14.5s,因此在到达 12.5 - 3.75s 的时刻发一对炮。
慢波发完炮后需要执行一些特殊操作。水路有 6 个南瓜,其中靠右的 4 个会受到海豚及关底的珊瑚三人组的啃食。因此南瓜需要定期维护。这里我们选择在第 4、7、14、17 波依次补充 (4, 7)、(4, 6)、(3, 7)、(3, 6) 的南瓜,因此,要补充的南瓜的纵坐标 = 4 - 波次的十位数,横坐标 = 8 - 波次的个位数除以 3 的商。
最后是旗帜波。第 10 波无论是否有小偷都使用核弹(为了防止冰透支),第 20 波由于已经到关底,尽量正常发炮收尾,仅当有小偷出现时才使用核弹。咖啡豆唤醒核弹需花费 1.99s,取 2s。唤醒核弹后,核弹还需再花 1s 时间生效。而核弹波的波长是 7.5s,因此需要确保在本波相对时间 5.5s 时核弹生效。因此核弹和咖啡豆的种植时间为 5.5 - 3s,发炮时间则为 5.5 - 3.75s。第 10 波红眼的密度会变大,如果一对炮没有激活刷新,则需要在 (1, 9) 补种樱桃炸弹确保刷新。
常规运阵时要做的事就这么多。然后开始处理第 9、19、20 波的收尾。
首先是第 9、19 波的首尾。开始收尾前,我们要处理一种特殊情况——夹零。根据游戏设定,出现红字的时机是【第 9、19 波出生的僵尸(除伴舞外)均被消灭】,而不是【场上现存的僵尸均被消灭】。所以此时会出现一个诡异的情况:出怪里有红眼,第 8 波正常刷出了红眼,第 9 波却又没刷出红眼(白眼也没刷出),然后第 9 波一对炮下去,(第 8 波的)红眼还在场上,还没完成中场收尾呢,红字就直接跳出来了。这种现象在 PvZ 里的术语叫做【夹零】。对于键控脚本来说,如果没有夹零预案,则容易造成灾难性的后果。
如果你看到了类似于下面这样的画面,就说明第 9/19 波时出现了红眼夹零:

明明有红眼,但是节奏却被加快了。如果不对夹零做特殊处理,原先不会受到威胁的炮可能反而会受到威胁。

在本阵里,红眼夹零所带来的后果除了前置炮受到的威胁变大外,还有一点:如果场上同时有冰车,则会因为节奏突然加快导致冰道无法及时融化,进而影响第 10 波樱桃炸弹的使用。因此,我们对红眼夹零的额外处理主要有两点:一是烧冰道,二是出红字时仍然需要按照节奏发炮,而不能傻傻等下一波僵尸出来。
首先看烧冰道:
然后开始说怎么收尾,包括常规拖收尾以及夹零收尾。收尾波里,需要发射的炮数量跟当前出怪相关,如果是极速关(或红白夹零),一对炮足以刷新;如果是白眼关,则需要两对炮;而如果是红眼关,则三对炮后,还需要曾哥和冰瓜输出几下才能挂掉。所以这几波我们不能把发炮对数写死,而要不断读取战场状态动态决定接下来的行为。
为此,我导入了 pvz.extra 包里的 wave_countdown 和 huge_wave_countdown 函数,用于读取当前波次的倒计时和大波倒计时。根据游戏设定,常规波的僵尸被清掉一半左右时,倒计时会强制置为 200,以便 2s 后刷新下一波怪。第 9 和 19 波有点特殊,一定要本波的僵尸全部清除后才会强制重置倒计时。重置后倒计时会变为 200,然后当倒计时到达 4 后,转为大波倒计时。第 20 波则更特殊,只要有僵尸在场上,不论是不是本波的,都必须要清除才会重置倒计时。因此在第 9、19 波时,我们可以通过不断读取刷新倒计时来检查有没有触发红字。
大致的意思就是,i 从 23 开始倒计时到 0,每过 1s 就将 i 减去 1。期间不断读取刷新倒计时,若仍大于 4,说明当前波的僵尸还没有清理干净。特殊地,红眼夹零时,常规倒计时只是个障眼法,需要继续数到大波倒计时到达 300 以下。如果没有红眼,则 i 数到 0 时发一对炮;如果有红眼,则 i 数到 8 和 0 的时候各发一对炮。另外,如果本波有跳跳,则在 i 数到 7 时调用 AliveTiaoTiao() 函数检查有没有漏炸的 1 路跳跳。若有,则立刻在 (1, 2) 放置窝瓜清理漏网之鱼。
注意那句跳炮判定。这和第 20 波的终场收尾有关系,我会在终场收尾的部分再回过头来解释为什么这里要跳炮。现在请看第 20 波的收尾:
如果第 20 波有红眼,拖 15s 和 23s 后各发一对炮就 over,边路的红眼被炸 3 炮后就是个大号路障,放到阵型里面去给 2 列曾哥收掉。如果有白眼或铁桶,则至多拖 23s 后就必须消灭,不能将它们放入阵型内部(单曾收不掉)。而如果剩下的是普僵、路障等,则不做额外处理,这些僵尸即使放入阵型内部也是没有关系的,2 列单曾可以完收。
注意白眼铁桶的这个分支,我调用了 UpdatePaoList 这个函数更新了一下炮列表,确保收尾时用的是 (2, 5), (5, 5) 这一对岸路炮。如果用别的炮收尾,比如用 (3, 1), (4, 1) 这对水路炮,那么下次选卡时运行脚本必炸。因为脚本总是会以 (3, 1), (4, 1) 这一对水路炮起始,不会智能选择别的炮。
然后,回过头来解释一下之前埋的那个跳炮坑。由于这里必须要以 (2, 5), (5, 5) 这一对岸路炮来收尾,那么就不能再以这对炮首发了(只拖 23s 没有装填完毕)。而如果第 19 波以 (3, 4), (4, 4) 这一对水路炮收尾的话,本波就必然以 (2, 5), (5, 5) 首发。因此第 19 波收尾时判定,如果指针指到了 2,且没有红眼,那么说明即将以 (3, 4), (4, 4) 收尾,产生矛盾。此时强行跳过 2 门炮,解决矛盾。
写完后,我们在命令提示符中执行以下命令切换到 E:\pvzscripts 目录
然后打开 Steam 版 pvz,切换到泳池无尽模式,并执行
就可以看到脚本的演示效果了。这里放一个现成的录屏。请切换到 P2 观看。

5. 一个复杂点的例子——RE 底线四炮
首先,我们要打开游戏里的隐藏页面(Limbo Page)以便能进入屋顶无尽。请首先在 E:\pvzscripts 下新建一个 limbo.py 的 Python 脚本文件,并写下以下代码:
我们在命令提示符中执行以下命令切换到 E:\pvzscripts 目录
然后打开 Steam 版 pvz,在命令行里执行
就解锁了隐藏页面。此时我们回到游戏窗口,点击 SURVIVAL,然后我们惊喜地发现窗口的最下方多了一排【PAGE 0 | PAGE 1 | Limbo Page | PAGE 3】的小字:

这时候我们点击 Limbo Page,就能看到一堆隐藏的小游戏,以及剩余四个没有展示出来的无尽场地:

以上我们说明了怎么找到隐藏的无尽场地。那么,现在我们来看一个屋顶的极简炮阵:

屋顶没有矿工舞王,因为相比于草地,屋顶的瓦片显然不适合做这些事。与此对应地,屋顶的底线炮少了矿工这个极大的威胁,只剩投篮车一个威胁。
另外还有一点要注意,在英文原版里,受上界之风 bug 的影响,底线两炮没法全收五行僵尸,只能收掉其中四行,以及剩下一行的大体积僵尸。而年度版(Steam 版)修复了这个 bug,这也使得本阵成为一个年度专属阵。我们现在就要用我们这个适配了 Steam 版的键控框架给这个阵写一个键控脚本。
本阵只有四门炮,同时每行都会刷出的冰车极大地限制了樱桃、核弹等灰烬武器的使用,因此本阵选用 ch4* 双冰变奏的运阵节奏:I-PP | I-PP (19, 19)。本阵的最大威胁是投篮车,而在完美预判冰下,让投篮车不出手的极限波长为 19.29s。取 19s 波长既可略微提升对红眼的压制力,又不像 18s 波长那样有较大的冰透支压力,是综合性能最好的波长。
现在,我们在 E:\pvzscripts 仓库目录下新建一个 re-base4p.py 的脚本文件,用于运行 RE 底线四炮。脚本代码如下:
首先,ZombieCounts 函数的定义和 PE 神六里的那个定义一模一样,不再说明。
接下来的 MyPao 是我对 Pao 函数的二次封装,每发射一对炮,就记录该对炮上次发射的相对时间(由 GameClock() 函数获得的游戏内部时钟)。记录时间是为了在收尾阶段能够【等待炮装填完毕后立刻发射】,省去计算发射时间的过程。
SeedIsColding 和 DianCai 这两个函数是为第 20 波加临时伞弹走空降兵,以及拿花盆来垫红眼巨人服务的,这里就不详细解读了,感兴趣的读者可以尝试自己去解读。
现在开始运行脚本。首先前两行:
其中 pao 是一个指针,它的值为 0 时表示下一次发射的是左上角的这对炮;它的值是 1 时表示下一次发射的是左下角的这对炮。clk 数组则分别用于记录这两对炮前一次发射的时间。这两个量的值会在调用 MyPao 函数时被自动修改。接下来第三行:
game_over 这个量是用来跟 DianCai 函数进行通讯的,这里我们只需要知道第 20 波收尾完成后,这个值会变为 True 即可。
接下来的五行代码:
读取了冰车、白眼、红眼、小偷、铁桶这些特殊僵尸每波各出现多少只,以便将来运行脚本时能对症下药。
然后是选卡、开启自动收集、开启自动存冰这三步曲:
开始逐波拆解。直接一条 for 语句,进入 w = 1~20 的循环。
如果是小波,则阻塞到 -195 的相对时间;如果是大波,则阻塞到 +5 的相对时间。内置的 Prejudge 函数有 bug:大波阻塞时,如果阻塞的时间小于 0,则有概率抛出【到 xxx 的相对时间已过去,当前相对时间 0】的异常,原因不明。由于该脚本里,大波不需要预判炸或预判冰,所以这里改为阻塞到 +5 的相对时间以对抗此 bug。
接下来有两个 if...elif...else 块,第一个块用来书写正常运阵时的操作,第二个块用来书写第 20 波的收尾操作。首先看第一个 if...elif...else 块:
同样,列表格来说明每一波的操作及波长:

为了减少用冰压力,20 波怪里有 3 波是纯加速、不点冰的,依次是第 1 波 6s 核弹,第 2 波 7.45s 一对炮和第 10 波 6.7s 一对炮。
第 1 波核弹代奏,所有怪出生后 0.25s 在 (2, 9) 依次放置花盆、核弹、咖啡豆。白天的核弹需要由咖啡豆唤醒,0.25~2.24s 的时间里是脆弱的,可能会被走得快的梯子僵尸啃两口,但是没有关系。2.24s~3.24s 的时间里核弹开始蓄力,蓄力阶段免疫一切啃食伤害,直到 3.24s 时核弹爆炸。
第 2 波直接发一对炮。加速波篮球车最快于 5.47s 时出手,所以炮必须在 5.46s 或更早时落地。
这里需要注意,我们这次没有像 PE 神六那样把点冰操作 Coffee() 写在冰波分支的开头,而是写在上一波的末尾。因为前者相当于刷新后 1s 点冰,但是这样的点冰在 ch4 节奏下已经压制不住篮球车了(PE 神六的 ch5 节奏仍可以压制),我们需要进一步提早点冰时间,需要让寒冰菇在下一波怪刷新后 0.1s 内生效。这里我们需要牢牢记住一个定式:当你于 x-375 时刻发炮,让炮在 x-2 时刻落地时,若你在 x-90 时刻点冰,则可确保下一波怪刷新后 0.1s 内生效。
然后是常规冰波,也就是上半场的第 3~8 波,下半场的第 11~18 波:
由于预判冰已经在前一波炮落地前点过了,所以这里就不用在开头调用 Coffee() 了。直接阻塞到 1700-375 的相对时间发炮,落点的 x 坐标取 8.5(为了同时炸到走得快的冰车和走得慢的巨人)。再阻塞到 1700-90 的相对时间点下一波的预判冰。
如果当前波是第 12/14/16/18 波,则自动补坐标为 (1, 3) / (2, 3) / (4, 3) / (5,3) 的南瓜。
然后是收尾冰波:
到了这一步,我们要未雨绸缪,先在脑子里想好怎么完成收尾工作。我们来简单推演一下:
红眼需要 3 个灰烬 + 1 个路障的自然输出才能收掉,但是我们在这一波里只有两对炮可用。因此引入核弹:本波有红眼时,用两对炮 + 一个核弹收尾。
如果没有红眼,但有白眼或铁桶这样的硬家伙的话,是不是用两对炮就可以了呢?答案是否定的。第二对炮发出后出现 7.45s 的红字,然后第 10 波的波长是 6.7s,第 11 波的波长是 19s,这时候我们在第 9 波用掉的炮装填完毕了吗?7.45 + 6.7 + 19 = 33.15 < 34.75,离装填完毕还差 1.6s!因此第 9 波是万万不能用炮收尾的,即使是白眼,也只能用一对炮 + 一个核弹,这样的组合来收尾。
那么,如果有冰车造冰道,不让我放核弹怎么办?用辣椒烧它丫的!刷出怪后,如果检测到本波有冰车,且上波红眼、本波红眼、本波白眼、本波铁桶至少有其一的话,在 (3, 4) 放辣椒。这里直接借用空置的存冰位来放辣椒,就不用额外放花盆了。
接下来我们等待到当前炮装填完毕:
当前炮的上一次发射时间记录在 clk[pao] 这个全局变量里(由 MyPao 函数改写),那么下一次可发射的时间显然是 clk[pao] + 3475。这里取了 clk[pao] + 3480,增加稳定性。那么,我们用发射时间减去当前时间,就得到了仍需等待的时间:clk[pao] + 3480 - GameClock()。如果仍需等待的时间大于 0,那么就等待这么多时间让炮装填完毕。
到了发射的时候,我们需要根据出怪种类的不同,选择不同的炸点。如果本波有巨人,就炸 8.5 列,不用留其他僵尸,只留巨人拖尾即可;如果本波没有巨人,就炸 8.2 列,把跑得快、威胁大的僵尸(橄榄、冰车、小丑、投篮)炸了,留下一些路障、铁桶、撑杆啥的乌龟来拖尾。
注意这里 y 坐标选的是 1、4 而不是 2、4,因为收尾阶段要避免中路重叠火力。想象一下:第 9 波只有中路有一只白眼,然后你炸 2、4 路,白眼因为重叠火力被瞬杀了,直接出红字,你的炮都还没装填好,GG!
现在开始收尾。如果是第 19 波,则启动垫材线程,于 (2, 6)、(4, 6) 放临时伞弹走接下来的空降兵,再于 (1, 6)、(5, 6) 放裸花盆。
只有上波红眼、本波红眼、本波白眼、本波铁桶至少有其一的时候,才需要放核弹。其余情况就让冰瓜自然拍死就 OK 了。
倒计时数 19s,期间不停读取刷新倒计时。19s 后,若刷新倒计时仍大于 200,则(本波无红)补一枚核弹,或(本波有红)一对炮 + 一枚核弹。
到了中场旗帜波,需要考虑冰杀小偷的事。小偷落地时间为 375~394,停留 3s 后离开,离开时间为 675~694。显然小偷的可冰杀时间为最早 395,最晚 674。
中场时我选择了相对时间 97 发炮,相对时间 375 点冰。计算可知,炮于 97 + 373 = 470 时刻生效,2s 后于相对时间 670 刷出下一波怪。而 375 点冰时,冰于咖啡豆放下后 299 生效,冰生效时间为 375 + 299 = 674,也就是说,这个冰既充当了第 11 波预判冰的作用(刷新后 0.04s 生效),又充当了冰杀小偷的作用,一冰两用,效率非常高!
点冰后在 (3, 4) 补南瓜套那是顺带的。
关底,常规操作很简单,在 545 - 375 时刻发炮,如果有小偷则在 100 点冰,冰于 100 + 299 = 399 生效,略晚于最早可冰时间。同时补 (3, 5) 的南瓜。
第二个 if...elif 块写的是关底收尾逻辑。这里又分成了两个分支:有红收尾和无红收尾。
有红收尾:第一对炮发射后,等待 0.7s 发射第二对炮。如果第二对炮没有装填好,就等待装填好后再发射。
第三对炮则是固定用左下角那对来发射。如果本波没有冰杀小偷,则于第一对炮发射完毕后 34.8s 发射第三对炮;如果本波冰杀了小偷,为了缓解冰透支问题,改为第一对炮发射后 39.8s 发射第三对炮。
无红收尾:比较简单,第一对炮发射完毕后,等待 34.8s 发射左下角的那对炮。如果冰杀了小偷,多等 5s。
写完后,我们在命令提示符中执行以下命令切换到 E:\pvzscripts 目录
然后打开 Steam 版 pvz,切换到屋顶无尽模式,并执行
就可以看到脚本的演示效果了。这里放一个现成的录屏。请切换到 P3 观看。
