import os import json import pygame as pg import random from .. import tool from .. import constants as c from ..component import map, plant, zombie, menubar 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.pauseTime = 0 # 默认显然不用显示菜单 self.showLittleMenu = False # 导入地图参数 # 采用了容错设计,如果没有导入成功就不执行 if self.loadMap(): # 表示导入成功 self.map = map.Map(self.map_data[c.BACKGROUND_TYPE]) self.map_y_len = self.map.height self.setupBackground() self.initState() def loadMap(self): if self.game_info[c.GAME_MODE] == c.MODE_LITTLEGAME: map_file = f'littleGame_{self.game_info[c.LITTLEGAME_NUM]}.json' # 设置标题 pg.display.set_caption(f"pypvz: 玩玩小游戏 第{self.game_info[c.LITTLEGAME_NUM]}关") elif self.game_info[c.GAME_MODE] == c.MODE_ADVENTURE: map_file = f'level_{self.game_info[c.LEVEL_NUM]}.json' # 设置标题 pg.display.set_caption(f"pypvz: 冒险模式 第{self.game_info[c.LEVEL_NUM]}关") file_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))),'resources' , 'data', 'map', map_file) # 最后一关之后应该结束了 try: with open(file_path) as f: self.map_data = json.load(f) except FileNotFoundError: if self.game_info[c.GAME_MODE] == c.MODE_ADVENTURE: print("成功通关冒险模式!") self.game_info[c.LEVEL_NUM] = 1 self.game_info[c.LEVEL_COMPLETIONS] += 1 elif self.game_info[c.GAME_MODE] == c.MODE_LITTLEGAME: print("成功通关玩玩小游戏!") self.game_info[c.LITTLEGAME_NUM] = 1 self.game_info[c.LITTLEGAME_COMPLETIONS] += 1 self.done = True self.next = c.MAIN_MENU with open(c.USERDATA_PATH, "w") as f: userdata = {} for i in self.game_info: if i in c.INIT_USERDATA: userdata[i] = self.game_info[i] savedata = json.dumps(userdata, sort_keys=True, indent=4) f.write(savedata) return # 是否有铲子的信息:无铲子时为0,有铲子时为1,故直接赋值即可 self.hasShovel = 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.CHOSSEBAR_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' # 表示成功加载地图 return True 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)] #zombies who are hypno after eating hypnoshroom 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, useableZombies, numFlags, survivalRounds=0, inevitableZombieDict=None): waves = [] self.numFlags = numFlags # 权重值 weights = [] for zombie in useableZombies: weights.append(c.CREATE_ZOMBIE_DICT[zombie][1]) # 按照原版pvz设计的僵尸容量函数,是从无尽解析的,但是普通关卡也可以遵循 for wave in range(1, 10 * numFlags + 1): volume = int(int((wave + survivalRounds*20)*0.8)/2) + 1 zombieList = [] # 大波僵尸情况 if wave % 10 == 0: # 容量增大至2.5倍 volume = int(volume*2.5) # 先生成旗帜僵尸 zombieList.append(c.FLAG_ZOMBIE) volume -= c.CREATE_ZOMBIE_DICT[c.FLAG_ZOMBIE][0] # 传送带模式应当增大僵尸容量 if (self.bar_type != c.CHOOSEBAR_STATIC): volume += 2 if inevitableZombieDict and (str(wave) in inevitableZombieDict): for newZombie in inevitableZombieDict[str(wave)]: zombieList.append(newZombie) volume -= c.CREATE_ZOMBIE_DICT[newZombie][0] if volume < 0: print(f'警告:第{wave}波中手动设置的僵尸级别总数超过上限!') # 防止因为僵尸最小等级过大,使得总容量无法完全利用,造成死循环的检查机制 minCost = c.CREATE_ZOMBIE_DICT[min(useableZombies, key=lambda x:c.CREATE_ZOMBIE_DICT[x][0])][0] while (volume >= minCost) and (len(zombieList) < 50): newZombie = random.choices(useableZombies, weights)[0] # 普通僵尸、路障僵尸、铁桶僵尸有概率生成水中变种 if self.background_type in c.POOL_EQUIPPED_BACKGROUNDS: # 有泳池第一轮的第四波设定上生成水生僵尸 if survivalRounds == 0 and wave == 4: if newZombie in c.CONVERT_ZOMBIE_IN_POOL: newZombie = c.CONVERT_ZOMBIE_IN_POOL[newZombie] elif survivalRounds > 0 or wave > 4: if random.randint(1, 3) == 1: # 1/3概率水上,暂时人为设定 if newZombie in c.CONVERT_ZOMBIE_IN_POOL: newZombie = c.CONVERT_ZOMBIE_IN_POOL[newZombie] # 首先几轮不出水生僵尸 elif newZombie in c.WATER_ZOMBIE: continue if c.CREATE_ZOMBIE_DICT[newZombie][0] <= volume: zombieList.append(newZombie) volume -= c.CREATE_ZOMBIE_DICT[newZombie][0] waves.append(zombieList) # print(wave, zombieList, len(zombieList)) self.waves = waves # 针对有泳池的关卡 # 表示尚未生成最后一波中从水里冒出来的僵尸 self.createdZombieFromPool = False # 僵尸的刷新机制 def refreshWaves(self, current_time, survivalRounds=0): # 最后一波或者大于最后一波 # 如果在夜晚按需从墓碑生成僵尸 # 否则直接return if self.waveNum >= self.map_data[c.NUM_FLAGS] * 10: if self.map_data[c.BACKGROUND_TYPE] == c.BACKGROUND_NIGHT: # 生长墓碑 if not self.graveInLevelAdded: if current_time - self.waveTime > 100: # 墓碑最多有12个 if len(self.graveSet) < 12: unoccupied = [] occupied = [] # 毁灭菇坑与冰道应当特殊化 exceptionObjects = {c.HOLE, c.ICEFROZENPLOT} # 遍历能生成墓碑的区域 for mapY in range(0, 4): for mapX in range(4, 8): # 为空、为毁灭菇坑、为冰道时看作未被植物占据 if ((not self.map.map[mapY][mapX][c.MAP_PLANT]) or (all((i in exceptionObjects) for i in self.map.map[mapY][mapX][c.MAP_PLANT]))): unoccupied.append((mapX, mapY)) # 已有墓碑的格子不应该放到任何列表中 elif c.GRAVE not in self.map.map[mapY][mapX][c.MAP_PLANT]: occupied.append((mapX, mapY)) if unoccupied: target = unoccupied[random.randint(0, len(unoccupied) - 1)] mapX, mapY = target posX, posY = self.map.getMapGridPos(mapX, mapY) self.plant_groups[mapY].add(plant.Grave(posX, posY)) self.map.map[mapY][mapX][c.MAP_PLANT].add(c.GRAVE) self.graveSet.add((mapX, mapY)) elif occupied: target = occupied[random.randint(0, len(occupied) - 1)] mapX, mapY = target posX, posY = self.map.getMapGridPos(mapX, mapY) for i in self.plant_groups[mapY]: checkMapX, _ = self.map.getMapIndex(i.rect.centerx, i.rect.bottom) if mapX == checkMapX: # 不杀死毁灭菇坑和冰道 if i.name not in exceptionObjects: i.health = 0 self.plant_groups[mapY].add(plant.Grave(posX, posY)) self.map.map[mapY][mapX][c.MAP_PLANT].add(c.GRAVE) self.graveSet.add((mapX, mapY)) self.graveInLevelAdded = True # 从墓碑中生成僵尸 if not self.graveZombieCreated: if current_time - self.waveTime > 1500: for item in self.graveSet: itemX, itemY = self.map.getMapGridPos(*item) # 目前设定:2/3概率普通僵尸,1/3概率路障僵尸 if random.randint(0, 2): self.zombie_groups[item[1]].add(zombie.NormalZombie(itemX, itemY, self.head_group)) else: self.zombie_groups[item[1]].add(zombie.ConeHeadZombie(itemX, itemY, self.head_group)) self.graveZombieCreated = True elif self.map_data[c.BACKGROUND_TYPE] in c.POOL_EQUIPPED_BACKGROUNDS: if not self.createdZombieFromPool: if current_time - self.waveTime > 1500: for i in range(3): # 水中倒数四列内可以在此时产生僵尸。共产生3个 mapX, mapY = random.randint(5, 8), random.randint(2, 3) itemX, itemY = self.map.getMapGridPos(mapX, mapY) # 用随机数指定产生的僵尸类型 # 带有权重 zombieType = random.randint(1, 6) if zombieType == 1: self.zombie_groups[mapY].add(zombie.BucketHeadDuckyTubeZombie(itemX, itemY, self.head_group)) elif zombieType <= 3: self.zombie_groups[mapY].add(zombie.ConeHeadDuckyTubeZombie(itemX, itemY, self.head_group)) else: self.zombie_groups[mapY].add(zombie.DuckyTubeZombie(itemX, itemY, self.head_group)) self.createdZombieFromPool = True return # 还未开始出现僵尸 if (self.waveNum == 0): if (self.waveTime == 0): # 表明刚刚开始游戏 self.waveTime = current_time else: if (survivalRounds == 0) and (self.bar_type == c.CHOOSEBAR_STATIC): # 首次选卡等待时间较长 if current_time - self.waveTime >= 18000: self.waveNum += 1 self.waveTime = current_time self.waveZombies = self.waves[self.waveNum - 1] self.numZombie = len(self.waveZombies) pg.mixer.Sound(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ,"resources", "sound", "zombieComing.ogg")).play() else: if (current_time - self.waveTime >= 6000): self.waveNum += 1 self.waveTime = current_time self.waveZombies = self.waves[self.waveNum - 1] self.numZombie = len(self.waveZombies) pg.mixer.Sound(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ,"resources", "sound", "zombieComing.ogg")).play() return if (self.waveNum % 10 != 9): if ((current_time - self.waveTime >= 25000 + random.randint(0, 6000)) or (self.bar_type != c.CHOOSEBAR_STATIC and current_time - self.waveTime >= 12500 + random.randint(0, 3000))): self.waveNum += 1 self.waveTime = current_time self.waveZombies = self.waves[self.waveNum - 1] self.numZombie = len(self.waveZombies) pg.mixer.Sound(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ,"resources", "sound", "zombieVoice.ogg")).play() else: if ((current_time - self.waveTime >= 45000) or (self.bar_type != c.CHOOSEBAR_STATIC and current_time - self.waveTime >= 25000)): self.waveNum += 1 self.waveTime = current_time self.waveZombies = self.waves[self.waveNum - 1] self.numZombie = len(self.waveZombies) # 一大波时播放音效 pg.mixer.Sound(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ,"resources", "sound", "hugeWaveApproching.ogg")).play() return elif ((current_time - self.waveTime >= 43000) or (self.bar_type != c.CHOOSEBAR_STATIC and current_time - self.waveTime >= 23000)): self.showHugeWaveApprochingTime = current_time numZombies = 0 for i in range(self.map_y_len): numZombies += len(self.zombie_groups[i]) if (numZombies / self.numZombie < random.uniform(0.15, 0.25)) and (current_time - self.waveTime > 4000): # 当僵尸所剩无几并且时间过了4000 ms以上时,改变时间记录,使得2000 ms后刷新僵尸(所以需要判断剩余时间是否大于2000 ms) if self.bar_type == c.CHOOSEBAR_STATIC: if current_time - 43000 < self.waveTime: # 判断剩余时间是否有2000 ms self.waveTime = current_time - 43000 # 即倒计时2000 ms else: if current_time - 23000 < self.waveTime: # 判断剩余时间是否有2000 ms self.waveTime = 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(-40, y+20, i)) # 更新函数每帧被调用,将鼠标事件传入给状态处理函数 def update(self, surface, current_time, mouse_pos, mouse_click): # 这些内容是将来增加通过界面后的容错设计,以保证直接通关时不会闪退 if self.done: return self.current_time = self.game_info[c.CURRENT_TIME] = self.pvzTime(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 pvzTime(self, current_time): # 扣除暂停时间 if not self.pause: self.beforePauseTime = current_time - self.pauseTime else: self.pauseTime = current_time - self.beforePauseTime return self.beforePauseTime def initBowlingMap(self): for x in range(3, self.map.width): for y in range(self.map.height): 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.CHOSSEBAR_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]) # 播放选卡音乐 pg.mixer.music.stop() pg.mixer.music.load(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ,"resources", "music", "chooseYourSeeds.opus")) pg.mixer.music.play(-1, 0) def choose(self, mouse_pos, mouse_click): # 如果暂停 if self.showLittleMenu: self.pauseAndCheckLittleMenuOptions(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.checkLittleMenuClick(mouse_pos): self.showLittleMenu = True pg.mixer.Sound(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ,"resources", "sound", "buttonclick.ogg")).play() def initPlay(self, card_list): # 播放bgm pg.mixer.music.stop() pg.mixer.music.load(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ,"resources", "music", self.bgm)) pg.mixer.music.play(-1, 0) 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.newPlantAndPositon = None if self.background_type in c.DAYTIME_BACKGROUNDS and self.bar_type == c.CHOOSEBAR_STATIC: self.produce_sun = True 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.waveNum = 0 # 还未出现僵尸时定义为0 self.waveTime = 0 self.waveZombies = [] self.numZombie = 0 # 暂时没有生存模式,所以 survivalRounds = 0 if c.INEVITABLE_ZOMBIE_DICT in self.map_data: self.createWaves( useableZombies=self.map_data[c.INCLUDED_ZOMBIES], numFlags=self.map_data[c.NUM_FLAGS], survivalRounds=0, inevitableZombieDict=self.map_data[c.INEVITABLE_ZOMBIE_DICT]) else: self.createWaves( useableZombies=self.map_data[c.INCLUDED_ZOMBIES], numFlags=self.map_data[c.NUM_FLAGS], survivalRounds=0) self.setupCars() # 地图有铲子才添加铲子 if self.hasShovel: # 导入小铲子 frame_rect = [0, 0, 71, 67] self.shovel = tool.get_image_menu(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_menu(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.showHugeWaveApprochingTime = -2000 # 防止设置为0时刚刚打开游戏就已经启动红字 if self.map_data[c.BACKGROUND_TYPE] == c.BACKGROUND_NIGHT: # 判断墓碑数量等级 # 0为无墓碑,1为少量墓碑,2为中等量墓碑,3为大量墓碑 if c.GRADE_GRAVES in self.map_data: gradeGraves = self.map_data[c.GRADE_GRAVES] # 缺省为少量墓碑 else: gradeGraves = 1 graveVolume = c.GRAVES_GRADE_INFO[gradeGraves] self.graveSet = set() while len(self.graveSet) < graveVolume: mapX = random.randint(4, 8) # 注意是从0开始编号 mapY = random.randint(0, 4) self.graveSet.add((mapX, mapY)) if self.graveSet: for i in self.graveSet: mapX, mapY = i posX, posY = self.map.getMapGridPos(mapX, mapY) self.plant_groups[mapY].add(plant.Grave(posX, posY)) self.map.map[mapY][mapX][c.MAP_PLANT].add(c.GRAVE) self.graveZombieCreated = False self.graveInLevelAdded = False # 小菜单 def setupLittleMenu(self): # 具体运行游戏必定有个小菜单, 导入菜单和选项 frame_rect = (0, 0, 108, 31) self.little_menu = tool.get_image_menu(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_menu(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 frame_rect = (0, 0, 342, 87) self.return_button = tool.get_image_menu(tool.GFX[c.RETURN_BUTTON], *frame_rect, c.BLACK, 1.1) self.return_button_rect = self.return_button.get_rect() self.return_button_rect.x = 220 self.return_button_rect.y = 440 frame_rect = (0, 0, 207, 45) self.restart_button = tool.get_image_menu(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_menu(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 def pauseAndCheckLittleMenuOptions(self, mouse_pos, mouse_click): # 设置暂停状态 self.pause = True # 暂停播放音乐 pg.mixer.music.pause() if mouse_click[0]: if self.checkReturnClick(mouse_pos): # 终止暂停,停止显示菜单 self.pause = False self.showLittleMenu = False # 继续播放音乐 pg.mixer.music.unpause() # 播放点击音效 pg.mixer.Sound(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ,"resources", "sound", "buttonclick.ogg")).play() elif self.checkRestartClick(mouse_pos): self.done = True self.next = c.LEVEL # 播放点击音效 pg.mixer.Sound(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ,"resources", "sound", "buttonclick.ogg")).play() elif self.checkMainMenuClick(mouse_pos): self.done = True self.next = c.MAIN_MENU self.persist = self.game_info self.persist[c.CURRENT_TIME] = 0 # 播放点击音效 pg.mixer.Sound(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ,"resources", "sound", "buttonclick.ogg")).play() # 一大波僵尸来袭图片显示 def setupHugeWaveApprochingImage(self): frame_rect = (0, 0, 492, 80) self.huge_wave_approching_image = tool.get_image_menu(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_menu(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 = 565 # 僵尸头 frame_rect = (0, 0, 23, 25) self.level_progress_zombie_head_image = tool.get_image_menu(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_menu(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 checkLittleMenuClick(self, mouse_pos): x, y = mouse_pos if (x >= self.little_menu_rect.x and x <= self.little_menu_rect.right and y >= self.little_menu_rect.y and y <= self.little_menu_rect.bottom): return True return False # 检查小菜单的返回有没有被点击 def checkReturnClick(self, mouse_pos): x, y = mouse_pos if (x >= self.return_button_rect.x and x <= self.return_button_rect.right and y >= self.return_button_rect.y and y <= self.return_button_rect.bottom): return True return False # 检查小菜单的重新开始有没有被点击 def checkRestartClick(self, mouse_pos): x, y = mouse_pos if (x >= self.restart_button_rect.x and x <= self.restart_button_rect.right and y >= self.restart_button_rect.y and y <= self.restart_button_rect.bottom): return True return False # 检查小菜单的主菜单有没有被点击 def checkMainMenuClick(self, mouse_pos): x, y = mouse_pos if (x >= self.mainMenu_button_rect.x and x <= self.mainMenu_button_rect.right and y >= self.mainMenu_button_rect.y and y <= self.mainMenu_button_rect.bottom): return True return False # 用小铲子移除植物 def shovelRemovePlant(self, mouse_pos): x, y = mouse_pos map_x, map_y = self.map.getMapIndex(x, y) 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 checkShovelClick(self, mouse_pos): x, y = mouse_pos if( self.hasShovel and x >= self.shovel_box_rect.x and x <= self.shovel_box_rect.right and y >= self.shovel_box_rect.y and y <= self.shovel_box_rect.bottom): return True return False def play(self, mouse_pos, mouse_click): # 原版阳光掉落机制需要 # 已掉落的阳光 self.fallenSun = 0 # 如果暂停 if self.showLittleMenu: self.pauseAndCheckLittleMenuOptions(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.waveZombies: self.createZombie(i) else: self.waveZombies = [] 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.fallenSun, 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.fallenSun += 1 # wcb 添加 # 检查有没有捡到阳光 clickedSun = False clickedCardsOrMap = 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[0], mouse_pos[1]): self.menubar.increaseSunValue(sun.sun_value) clickedSun = True # 播放收集阳光的音效 pg.mixer.Sound(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ,"resources", "sound", "collectSun.ogg")).play() # 拖动植物或者铲子 if not self.drag_plant and mouse_pos and mouse_click[0] and not clickedSun: self.clickResult = self.menubar.checkCardClick(mouse_pos) if self.clickResult: self.setupMouseImage(self.clickResult[0], self.clickResult[1]) self.clickResult[1].clicked = True clickedCardsOrMap = True # 播放音效 pg.mixer.Sound(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ,"resources", "sound", "clickCard.ogg")).play() elif self.drag_plant: if mouse_click[1]: self.removeMouseImage() clickedCardsOrMap = True self.clickResult[1].clicked = False elif mouse_click[0]: if self.menubar.checkMenuBarClick(mouse_pos): self.clickResult[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 clickedSun) and (not clickedCardsOrMap): if self.checkLittleMenuClick(mouse_pos): # 暂停 显示菜单 self.showLittleMenu = True # 播放点击音效 pg.mixer.Sound(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ,"resources", "sound", "buttonclick.ogg")).play() elif self.checkShovelClick(mouse_pos): self.drag_shovel = not self.drag_shovel if not self.drag_shovel: self.removeMouseImagePlus() # 播放点击铲子的音效 pg.mixer.Sound(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ,"resources", "sound", "shovel.ogg")).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.waveNum % 10: hugeWaveMove = 0 else: hugeWaveMove = 40 else: hugeWaveMove = 0 x, y = self.map.getMapGridPos(0, map_y) # 新增的僵尸也需要在这里声明 if name == c.NORMAL_ZOMBIE: self.zombie_groups[map_y].add(zombie.NormalZombie(c.ZOMBIE_START_X + random.randint(-20, 20) + hugeWaveMove, y, self.head_group)) elif name == c.CONEHEAD_ZOMBIE: self.zombie_groups[map_y].add(zombie.ConeHeadZombie(c.ZOMBIE_START_X + random.randint(-20, 20) + hugeWaveMove, y, self.head_group)) elif name == c.BUCKETHEAD_ZOMBIE: self.zombie_groups[map_y].add(zombie.BucketHeadZombie(c.ZOMBIE_START_X + random.randint(-20, 20) + hugeWaveMove, y, self.head_group)) elif name == c.FLAG_ZOMBIE: self.zombie_groups[map_y].add(zombie.FlagZombie(c.ZOMBIE_START_X, y, self.head_group)) elif name == c.NEWSPAPER_ZOMBIE: self.zombie_groups[map_y].add(zombie.NewspaperZombie(c.ZOMBIE_START_X + random.randint(-20, 20) + hugeWaveMove, y, self.head_group)) elif name == c.FOOTBALL_ZOMBIE: self.zombie_groups[map_y].add(zombie.FootballZombie(c.ZOMBIE_START_X + random.randint(-20, 20) + hugeWaveMove, y, self.head_group)) elif name == c.DUCKY_TUBE_ZOMBIE: self.zombie_groups[map_y].add(zombie.DuckyTubeZombie(c.ZOMBIE_START_X + random.randint(-20, 20) + hugeWaveMove, y, self.head_group)) elif name == c.CONEHEAD_DUCKY_TUBE_ZOMBIE: self.zombie_groups[map_y].add(zombie.ConeHeadDuckyTubeZombie(c.ZOMBIE_START_X + random.randint(-20, 20) + hugeWaveMove, y, self.head_group)) elif name == c.BUCKETHEAD_DUCKY_TUBE_ZOMBIE: self.zombie_groups[map_y].add(zombie.BucketHeadDuckyTubeZombie(c.ZOMBIE_START_X + random.randint(-20, 20) + hugeWaveMove, y, self.head_group)) elif name == c.SCREEN_DOOR_ZOMBIE: self.zombie_groups[map_y].add(zombie.ScreenDoorZombie(c.ZOMBIE_START_X + random.randint(-20, 20) + hugeWaveMove, y, self.head_group)) elif name == c.POLE_VAULTING_ZOMBIE: # 本来撑杆跳生成位置不同,对齐左端可认为修正了一部分(看作移动了70),只需要相对修改即可 self.zombie_groups[map_y].add(zombie.PoleVaultingZombie(c.ZOMBIE_START_X + random.randint(0, 10) + hugeWaveMove, y, self.head_group)) elif name == c.ZOMBONI: # 冰车僵尸生成位置不同 self.zombie_groups[map_y].add(zombie.Zomboni(c.ZOMBIE_START_X + random.randint(0, 10) + hugeWaveMove, y, self.plant_groups[map_y], self.map, plant.IceFrozenPlot)) elif name == c.SNORKELZOMBIE: # 潜水僵尸生成位置不同 self.zombie_groups[map_y].add(zombie.SnorkelZombie(c.ZOMBIE_START_X + random.randint(0, 10) + hugeWaveMove, y, self.head_group)) # 能否种植物的判断: # 先判断位置是否合法 isValid(map_x, map_y) # 再判断位置是否可用 isMovable(map_x, map_y) def canSeedPlant(self, plantName): x, y = pg.mouse.get_pos() return self.map.checkPlantToSeed(x, y, plantName) # 种植物 def addPlant(self): pos = self.canSeedPlant(self.plant_name) if pos is None: return # 恢复植物卡片样式 self.clickResult[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) # 新植物也需要在这里声明 if self.plant_name == c.SUNFLOWER: new_plant = plant.SunFlower(x, y, self.sun_group) elif self.plant_name == c.PEASHOOTER: new_plant = plant.PeaShooter(x, y, self.bullet_groups[map_y]) elif self.plant_name == c.SNOWPEASHOOTER: new_plant = plant.SnowPeaShooter(x, y, self.bullet_groups[map_y]) elif self.plant_name == c.WALLNUT: new_plant = plant.WallNut(x, y) elif self.plant_name == c.CHERRYBOMB: new_plant = plant.CherryBomb(x, y) elif self.plant_name == c.THREEPEASHOOTER: new_plant = plant.ThreePeaShooter(x, y, self.bullet_groups, map_y, self.map.background_type) elif self.plant_name == c.REPEATERPEA: new_plant = plant.RepeaterPea(x, y, self.bullet_groups[map_y]) elif self.plant_name == c.CHOMPER: new_plant = plant.Chomper(x, y) elif self.plant_name == c.PUFFSHROOM: new_plant = plant.PuffShroom(x, y, self.bullet_groups[map_y]) elif self.plant_name == c.POTATOMINE: new_plant = plant.PotatoMine(x, y) elif self.plant_name == c.SQUASH: new_plant = plant.Squash(x, y, self.map.map[map_y][map_x][c.MAP_PLANT]) elif self.plant_name == c.SPIKEWEED: new_plant = plant.Spikeweed(x, y) elif self.plant_name == c.JALAPENO: new_plant = plant.Jalapeno(x, y) elif self.plant_name == c.SCAREDYSHROOM: new_plant = plant.ScaredyShroom(x, y, self.bullet_groups[map_y]) elif self.plant_name == c.SUNSHROOM: new_plant = plant.SunShroom(x, y, self.sun_group) elif self.plant_name == c.ICESHROOM: new_plant = plant.IceShroom(x, y) elif self.plant_name == c.HYPNOSHROOM: new_plant = plant.HypnoShroom(x, y) elif self.plant_name == c.WALLNUTBOWLING: new_plant = plant.WallNutBowling(x, y, map_y, self) elif self.plant_name == c.REDWALLNUTBOWLING: new_plant = plant.RedWallNutBowling(x, y) elif self.plant_name == c.LILYPAD: new_plant = plant.LilyPad(x, y) elif self.plant_name == c.TORCHWOOD: new_plant = plant.TorchWood(x, y, self.bullet_groups[map_y]) elif self.plant_name == c.STARFRUIT: new_plant = plant.StarFruit(x, y, self.bullet_groups[map_y], self) elif self.plant_name == c.COFFEEBEAN: new_plant = plant.CoffeeBean(x, y, self.plant_groups[map_y], self.map.map[map_y][map_x], self.map, map_x) elif self.plant_name == c.SEASHROOM: new_plant = plant.SeaShroom(x, y, self.bullet_groups[map_y]) elif self.plant_name == c.TALLNUT: new_plant = plant.TallNut(x, y) elif self.plant_name == c.TANGLEKLEP: new_plant = plant.TangleKlep(x, y) elif self.plant_name == c.DOOMSHROOM: if self.map.gridHeightSize == 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) elif self.plant_name == c.GRAVEBUSTER: new_plant = plant.GraveBuster(x, y, self.plant_groups[map_y], self.map, map_x) elif self.plant_name == c.FUMESHROOM: new_plant = plant.FumeShroom(x, y, self.bullet_groups[map_y], self.zombie_groups[map_y]) elif self.plant_name == c.GARLIC: new_plant = plant.Garlic(x, y) elif self.plant_name == c.PUMPKINHEAD: new_plant = plant.PumpkinHead(x, y) if new_plant.can_sleep and self.background_type in c.DAYTIME_BACKGROUNDS: new_plant.setSleep() mushroomSleep = True else: mushroomSleep = False self.plant_groups[map_y].add(new_plant) # 种植植物后应当刷新僵尸的攻击对象 # 用元组表示植物的名称和格子坐标 self.newPlantAndPositon = (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.CHOSSEBAR_BOWLING: # 坚果保龄球关卡无需考虑格子被占用的情况 self.map.addMapPlant(map_x, map_y, self.plant_name, sleep=mushroomSleep) self.removeMouseImage() # print(self.newPlantAndPositon) # 播放种植音效 pg.mixer.Sound(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ,"resources", "sound", "plant.ogg")).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): frame_list = tool.GFX[plant_name] if plant_name in tool.PLANT_RECT: data = tool.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 if (plant_name in c.PLANT_COLOR_KEY_WHITE): color = c.WHITE else: color = c.BLACK self.mouse_image = tool.get_image(frame_list[0], x, y, width, height, color, 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, damageType=bullet.damageType) 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, damageType=c.ZOMBIE_DEAFULT_DAMAGE) break def checkZombieCollisions(self): for i in range(self.map_y_len): hypo_zombies = [] 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.newPlantAndPositon: continue # 被攻击对象是植物时才可能刷新 if zombie.prey_is_plant: # 新植物种在被攻击植物同一格时才可能刷新 if (zombie.preyMapX, zombie.preyMapY) == self.newPlantAndPositon[1]: # 如果被攻击植物是睡莲和花盆,同一格种了植物必然刷新 # 如果被攻击植物不是睡莲和花盆,同一格种了南瓜头才刷新 if ((zombie.prey.name not in {c.LILYPAD, "花盆(未实现)"}) and (self.newPlantAndPositon[0] != c.PUMPKINHEAD)): continue else: continue else: continue if zombie.canSwim and (not zombie.swimming): continue # 以下代码为了实现各个功能,较为凌乱 attackableCommonPlants = [] attackableBackupPlant = [] # 利用更加精细的循环判断啃咬优先顺序 for plant in self.plant_groups[i]: if collided_func(plant, zombie): # 优先攻击南瓜头 if plant.name == c.PUMPKINHEAD: targetPlant = plant break # 衬底植物情形 elif plant.name in {c.LILYPAD, "花盆(未实现)"}: attackableBackupPlant.append(plant) # 一般植物情形 # 同时也忽略了不可啃食对象 elif plant.name not in c.CAN_SKIP_ZOMBIE_COLLISION_CHECK: attackableCommonPlants.append(plant) # 在生效状态下忽略啃食碰撞但其他状况下不能忽略的情形 elif plant.name in c.SKIP_ZOMBIE_COLLISION_CHECK_WHEN_WORKING: if not plant.start_boom: attackableCommonPlants.append(plant) else: if attackableCommonPlants: # 默认为最右侧的一个植物 targetPlant = max(attackableCommonPlants, key=lambda i: i.rect.x) elif attackableBackupPlant: targetPlant = max(attackableBackupPlant, key=lambda i: i.rect.x) map_x, map_y = self.map.getMapIndex(targetPlant.rect.centerx, targetPlant.rect.centery) if len(self.map.map[map_y][map_x][c.MAP_PLANT]) >= 2: for actualTargetPlant in self.plant_groups[i]: # 检测同一格的其他植物 if self.map.getMapIndex(actualTargetPlant.rect.centerx, actualTargetPlant.rect.bottom) == (map_x, map_y): if actualTargetPlant.name == c.PUMPKINHEAD: targetPlant = actualTargetPlant break elif actualTargetPlant.name not in {c.LILYPAD, "花盆(未实现)"}: attackableCommonPlants.append(actualTargetPlant) else: if attackableCommonPlants: targetPlant = attackableCommonPlants[-1] else: targetPlant = None if targetPlant: zombie.preyMapX, zombie.preyMapY = self.map.getMapIndex(targetPlant.rect.centerx, targetPlant.rect.centery) # 撑杆跳的特殊情况 if zombie.name in {c.POLE_VAULTING_ZOMBIE} and (not zombie.jumped): if not zombie.jumping: zombie.jumpMap_x, zombie.jumpMap_y = min(c.GRID_X_LEN - 1, zombie.preyMapX), min(self.map_y_len - 1, zombie.preyMapY) jumpX = targetPlant.rect.x - c.GRID_X_SIZE * 0.6 if c.TALLNUT in self.map.map[zombie.jumpMap_y][zombie.jumpMap_x][c.MAP_PLANT]: zombie.setJump(False, jumpX) else: zombie.setJump(True, jumpX) else: if c.TALLNUT in self.map.map[zombie.jumpMap_y][zombie.jumpMap_x][c.MAP_PLANT]: zombie.setJump(False, zombie.jumpX) else: zombie.setJump(True, zombie.jumpX) continue if targetPlant.name == c.WALLNUTBOWLING: if targetPlant.canHit(i): # targetPlant.vel_y不为0,有纵向速度,表明已经发生过碰撞,对铁门秒杀(这里实现为忽略二类防具攻击) if targetPlant.vel_y and zombie.name == c.SCREEN_DOOR_ZOMBIE: zombie.setDamage(c.WALLNUT_BOWLING_DAMAGE, damageType=c.ZOMBIE_COMMON_DAMAGE) else: zombie.setDamage(c.WALLNUT_BOWLING_DAMAGE, damageType=c.ZOMBIE_WALLNUT_BOWLING_DANMAGE) targetPlant.changeDirection(i) # 播放撞击音效 pg.mixer.Sound(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ,"resources", "sound", "bowlingimpact.ogg")).play() elif targetPlant.name == c.REDWALLNUTBOWLING: if targetPlant.state == c.IDLE: targetPlant.setAttack() elif zombie.targetYChange: # 大蒜作用正在生效的僵尸不进行传递 continue elif targetPlant.name == c.GARLIC: zombie.setAttack(targetPlant) # 向吃过大蒜的僵尸传入level zombie.level = self zombie.toChangeGroup = True zombie.mapY = 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.targetMapY = i + _move zombie.targetYChange = _move * self.map.gridHeightSize else: zombie.setAttack(targetPlant) 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.newPlantAndPositon = 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 (zombie.rect.centerx <= 0): self.cars[i].setWalk() if zombie.rect.centerx <= self.cars[i].rect.x: zombie.health = 0 # zombie.kill() 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, damageType=c.ZOMBIE_ASH_DAMAGE) if zombie.health <= 0: zombie.setBoomDie() def freezeZombies(self, plant): # 播放冻结音效 pg.mixer.Sound(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ,"resources", "sound", "freeze.ogg")).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, damageType=c.ZOMBIE_RANGE_DAMAGE) # 寒冰菇还有全场20的伤害 def killPlant(self, targetPlant, shovel=False): x, y = targetPlant.getPosition() map_x, map_y = self.map.getMapIndex(x, y) # 用铲子铲不用触发植物功能 if not shovel: if targetPlant.name == c.HYPNOSHROOM and targetPlant.state != c.SLEEP: if targetPlant.zombie_to_hypno: zombie = targetPlant.zombie_to_hypno zombie.setHypno() self.zombie_groups[map_y].remove(zombie) self.hypno_zombie_groups[map_y].add(zombie) # 对于墓碑:移除存储在墓碑集合中的坐标 # 注意这里是在描述墓碑而非墓碑吞噬者 elif targetPlant.name == c.GRAVE: self.graveSet.remove((map_x, map_y)) elif ((targetPlant.name in { c.DOOMSHROOM, c.ICESHROOM, c.POTATOMINE, }) and (targetPlant.boomed)): # 毁灭菇的情况:爆炸时为了防止蘑菇云被坑掩盖没有加入坑,这里毁灭菇死亡(即爆炸动画结束)后再加入 if targetPlant.name == c.DOOMSHROOM: self.plant_groups[map_y].add(plant.Hole(targetPlant.originalX, targetPlant.originalY, self.map.map[map_y][map_x][c.MAP_PLOT_TYPE])) elif targetPlant.name not in c.PLANT_DIE_SOUND_EXCEPTIONS: # 触发植物死亡音效 pg.mixer.Sound(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ,"resources", "sound", "plantDie.ogg")).play() else: # 用铲子移除植物时播放音效 pg.mixer.Sound(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ,"resources", "sound", "plant.ogg")).play() # 整理地图信息 if self.bar_type != c.CHOSSEBAR_BOWLING: self.map.removeMapPlant(map_x, map_y, targetPlant.name) # 将睡眠植物移除后更新睡眠状态 if targetPlant.state == c.SLEEP: self.map.map[map_y][map_x][c.MAP_SLEEP] = False # 避免僵尸在用铲子移除植物后还在原位啃食 targetPlant.health = 0 targetPlant.kill() def checkPlant(self, targetPlant, i): zombie_len = len(self.zombie_groups[i]) # 不用检查攻击状况的情况 if targetPlant.name in c.PLANT_NON_CHECK_ATTACK_STATE: pass elif targetPlant.name == c.THREEPEASHOOTER: if targetPlant.state == c.IDLE: if zombie_len > 0: targetPlant.setAttack() elif (i-1) >= 0 and len(self.zombie_groups[i-1]) > 0: targetPlant.setAttack() elif (i+1) < self.map_y_len and len(self.zombie_groups[i+1]) > 0: targetPlant.setAttack() elif targetPlant.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: targetPlant.setIdle() elif targetPlant.name == c.CHOMPER: for zombie in self.zombie_groups[i]: if targetPlant.canAttack(zombie): targetPlant.setAttack(zombie, self.zombie_groups[i]) break elif targetPlant.name == c.POTATOMINE: for zombie in self.zombie_groups[i]: if targetPlant.canAttack(zombie): targetPlant.setAttack() break if targetPlant.start_boom and (not targetPlant.boomed): for zombie in self.zombie_groups[i]: # 双判断:发生碰撞或在攻击范围内 if ((pg.sprite.collide_mask(zombie, targetPlant)) or (abs(zombie.rect.centerx - targetPlant.rect.centerx) <= targetPlant.explode_x_range)): zombie.setDamage(1800, damageType=c.ZOMBIE_RANGE_DAMAGE) targetPlant.boomed = True elif targetPlant.name == c.SQUASH: for zombie in self.zombie_groups[i]: if targetPlant.canAttack(zombie): targetPlant.setAttack(zombie, self.zombie_groups[i]) break elif targetPlant.name == c.SPIKEWEED: can_attack = False for zombie in self.zombie_groups[i]: if targetPlant.canAttack(zombie): can_attack = True break if targetPlant.state == c.IDLE and can_attack: targetPlant.setAttack(self.zombie_groups[i]) elif targetPlant.state == c.ATTACK and not can_attack: targetPlant.setIdle() elif targetPlant.name == c.SCAREDYSHROOM: need_cry = False can_attack = False for zombie in self.zombie_groups[i]: if targetPlant.needCry(zombie): need_cry = True break elif targetPlant.canAttack(zombie): can_attack = True if need_cry: if targetPlant.state != c.CRY: targetPlant.setCry() elif can_attack: if targetPlant.state != c.ATTACK: targetPlant.setAttack() elif targetPlant.state != c.IDLE: targetPlant.setIdle() elif targetPlant.name == c.STARFRUIT: can_attack = False for zombie_group in self.zombie_groups: # 遍历循环所有僵尸 for zombie in zombie_group: if targetPlant.canAttack(zombie): can_attack = True break if targetPlant.state == c.IDLE and can_attack: targetPlant.setAttack() elif (targetPlant.state == c.ATTACK and not can_attack): targetPlant.setIdle() elif targetPlant.name == c.TANGLEKLEP: for zombie in self.zombie_groups[i]: if targetPlant.canAttack(zombie): targetPlant.setAttack(zombie, self.zombie_groups[i]) break # 灰烬植物与寒冰菇 elif targetPlant.name in c.ASH_PLANTS_AND_ICESHROOM: if targetPlant.start_boom and (not targetPlant.boomed): # 这样分成两层是因为场上灰烬植物肯定少,一个一个判断代价高,先笼统判断灰烬即可 if targetPlant.name in {c.REDWALLNUTBOWLING, c.CHERRYBOMB}: self.boomZombies(targetPlant.rect.centerx, i, targetPlant.explode_y_range, targetPlant.explode_x_range) elif (targetPlant.name == c.DOOMSHROOM): x, y = targetPlant.originalX, targetPlant.originalY map_x, map_y = self.map.getMapIndex(x, y) self.boomZombies(targetPlant.rect.centerx, i, targetPlant.explode_y_range, targetPlant.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 targetPlant.name == c.JALAPENO: self.boomZombies(targetPlant.rect.centerx, i, targetPlant.explode_y_range, targetPlant.explode_x_range, effect=c.BULLET_EFFECT_UNICE) # 消除冰道 for item in self.plant_groups[i]: if item.name == c.ICEFROZENPLOT: item.health = 0 elif targetPlant.name == c.ICESHROOM: self.freezeZombies(targetPlant) targetPlant.boomed = True else: can_attack = False if (zombie_len > 0): for zombie in self.zombie_groups[i]: if targetPlant.canAttack(zombie): can_attack = True break if targetPlant.state == c.IDLE and can_attack: targetPlant.setAttack() elif (targetPlant.state == c.ATTACK and (not can_attack)): targetPlant.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.waveNum < 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): print(zombie.rect.right, zombie.lostHead, zombie.state,zombie.name) return True return False def checkGameState(self): if self.checkVictory(): if self.game_info[c.GAME_MODE] == c.MODE_LITTLEGAME: self.game_info[c.LITTLEGAME_NUM] += 1 elif self.game_info[c.GAME_MODE] == c.MODE_ADVENTURE: self.game_info[c.LEVEL_NUM] += 1 self.next = c.GAME_VICTORY self.done = True with open(c.USERDATA_PATH, "w") as f: userdata = {} for i in self.game_info: if i in c.INIT_USERDATA: userdata[i] = self.game_info[i] savedata = json.dumps(userdata, sort_keys=True, indent=4) f.write(savedata) elif self.checkLose(): 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) 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.highlightTime = 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.waveNum) / (self.map_data[c.NUM_FLAGS] * 10)) + 145 # 常数为预计值 self.level_progress_zombie_head_image_rect.y = self.level_progress_bar_image_rect.y - 3 # 常数为预计值 # 填充的进度条信息 # 常数为预计值 filledBarRect = (self.level_progress_zombie_head_image_rect.x + 3, self.level_progress_bar_image_rect.y + 6, int((150 * self.waveNum) / (self.map_data[c.NUM_FLAGS] * 10)) + 5, 9) # 画填充的进度条 pg.draw.rect(surface, c.GREEN, filledBarRect) # 画旗帜 for i in range(self.numFlags): self.level_progress_flag_rect.x = self.level_progress_bar_image_rect.x + int((150*i)/self.numFlags) + 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 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.showLittleMenu: 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) # 以后可能需要插入一个预备的状态(预览显示僵尸、返回战场) elif self.state == c.PLAY: if self.hasShovel: # 画铲子 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.hasShovel and self.drag_shovel: self.drawMouseShowPlus(surface) if self.showLittleMenu: 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) if self.map_data[c.SPAWN_ZOMBIES] == c.SPAWN_ZOMBIES_AUTO: self.showLevelProgress(surface) if self.current_time - self.showHugeWaveApprochingTime <= 2000: surface.blit(self.huge_wave_approching_image, self.huge_wave_approching_image_rect)