class Zombie(BaseClass): """Create a Zombie Params: x: x coordinate of zombie y: y coordinate of zombie""" instances = set() # set of all zombies with open(Options.mappath) as file: spawn_tiles = [ i for i, x in enumerate(file.read().replace("\n", "")) if x == "Z" ] imgs = tuple( scale(pygame.image.load("assets/Images/Zombies/zombie{}.png".format( i))) for i in range(1, 5)) speed_tuple = _get_vel_list() logging.debug("zombie speeds: %s", speed_tuple) health_func_tuple = (lambda h: h, lambda h: h / 2, lambda h: h * 1.2, lambda h: h * 4) new_round_song = pygame.mixer.Sound( "assets/Audio/Other/new_round_short.ogg") new_round_song.set_volume(Options.volume) new_round_song_length = new_round_song.get_length() base_health = 100 attack_range = Tile.length * 1.3 # Max distance from zombie for an attack, in pixels level = 1 init_round = 0 if Options.no_zombies else 10 # How many zombies spawn on round 1 left_round = init_round PickUp.zombie_init_round = init_round cool_down, play_song = False, True spawn_interval = Options.fps * 2 # Frames between spawns, decreses exponentially AStarThread = None def __init__(self, x, y): self.direction = math.pi type_ = randint(0, 3) self.type = type_ self.org_img = Zombie.imgs[type_] self.img = self.org_img self.speed = Zombie.speed_tuple[type_] self.health_func = Zombie.health_func_tuple[type_] self.health = self.health_func(Zombie.base_health) self.org_health = self.health self.angle_to_vel = { 0: (self.speed, 0), math.pi / 2: (0, -self.speed), math.pi: (-self.speed, 0), math.pi * 3 / 2: (0, self.speed) } self.vel = Vector(0, 0) super().__init__(x, y) Zombie.instances.add(self) self.path = [] self.last_angle = 0. self.path_colour = Colours.random(s=(0.5, 1)) logging.debug("speed: %s type. %s", self.speed, self.type) def set_target(self, next_tile): self.to = next_tile.pos angle = angle_between(*self.pos, *next_tile.pos) assert angle % (math.pi / 2) == 0., "angle: %s to: %s pos: %s mod %s" % ( angle, self.to, self.pos, angle % math.pi / 2) self.vel = Vector(*self.angle_to_vel[angle]) @classmethod def update(cls, screen, survivor): del_zmbs = set() for zmb in cls.instances: if zmb.health <= 0.: Drop.spawn(zmb.pos) del_zmbs.add(zmb) stats["Zombies Killed"] += 1 continue screen.blit(zmb.img, zmb.pos) zmb.health_bar(surface=screen) # Health bar with rounded edges if Options.debug: for tile in zmb.path: pygame.draw.circle(screen, zmb.path_colour, tile.get_centre(), Tile.length // 3) zmb_to_survivor_dist = (survivor.pos - zmb.pos).magnitude() if zmb_to_survivor_dist <= cls.attack_range: survivor.health -= 0.4 if zmb.to is not None: # if the zombie is not next to the player angle = angle_between(*zmb.pos, *(zmb.to + zmb.vel)) if zmb.pos == zmb.to: # If the zombie is directly on a tile zmb.to = None # Trigger A-Star, not run between tiles for performance else: if "freeze" not in Drop.actives: zmb.pos += zmb.vel if zmb.direction != angle: # New direction, frame after a turn zmb.rotate(angle) if zmb.to is None and zmb_to_survivor_dist > Tile.length: AStar(zmb, survivor).solve() cls.instances -= del_zmbs @classmethod def spawn(cls, screen, totalframes, survivor): """Spawning and rounds""" if Options.no_zombies: return if cls.left_round and not cls.cool_down: if totalframes % cls.spawn_interval == 0: cls.left_round -= 1 if cls.play_song: # filenanes are numbered in hexadecimal file_nr = format(randint(0, 19), "x") sound = pygame.mixer.Sound( "assets/Audio/Spawn/zmb_spawn{}.wav".format(file_nr)) sound.set_volume(Options.volume) sound.play() cls.play_song = not cls.play_song # Loop True and False, only play every other spawn further_than_ = partial(further_than, survivor=survivor, min_dist=150) valid_tiles = list(filter(further_than_, cls.spawn_tiles)) if not valid_tiles: valid_tiles.extend(cls.spawn_tiles) spawn_idx = choice(valid_tiles) spawn_node = Tile.instances[spawn_idx] cls(*spawn_node.pos) logging.debug( "spawn_idx: %s, spawn_node: %s, valid: %s, survivor: %s", spawn_idx, spawn_node.pos, valid_tiles, survivor.pos) elif not (cls.instances or cls.cool_down): # Round is over, start cooldown cls.cool_down = int(totalframes + Options.fps * cls.new_round_song_length) cls.new_round_song.play() pygame.mixer.music.pause() cooldown_time = cls.cool_down - totalframes cls.cooldown_counter = NextRoundCountdown( cls.new_round_song_length * Options.fps) logging.debug( "Round over, start cooldown; cooldown: %s frames, %s sec", cooldown_time, cooldown_time // Options.fps) elif totalframes == cls.cool_down: # Cooldown is over, start round pygame.mixer.music.unpause() cls.cool_down = False cls.base_health *= 1.16 cls.init_round = 10 + cls.level * 3 cls.left_round = cls.init_round cls.level += 1 cls.spawn_interval //= 1.14 PickUp.zombie_init_round = cls.init_round PickUp.init_round = cls.level // 2 + 3 PickUp.left_round = PickUp.init_round logging.debug( "level: %s, base health: %s, Zombies: %s, Pick-Ups: %s, Interval: %s", cls.level, cls.base_health, cls.init_round, PickUp.init_round, cls.spawn_interval) else: if cls.cool_down: cls.cooldown_counter.update(screen) def rotate(self, new_dir): """Rotate self.img, set self.direction to new_dir :param new_dir: The angle to rotate self clockwise from the x-axis in radians""" self.img = rotated(self.org_img, new_dir) self.direction = new_dir def health_bar(self, surface): """Draw a health bar with rounded egdes above the zombie""" colour = 170, 0, 0 rect = pygame.Rect(*(self.pos - (0, 12)), self.width * self.health / self.org_health, self.height / 6) zeroed_rect = rect.copy() zeroed_rect.topleft = 0, 0 image = pygame.Surface(rect.size).convert_alpha() image.fill((0, 0, 0, 0)) corners = zeroed_rect.inflate(-6, -6) for attribute in ("topleft", "topright", "bottomleft", "bottomright"): pygame.draw.circle(image, colour, getattr(corners, attribute), 3) image.fill(colour, zeroed_rect.inflate(-6, 0)) image.fill(colour, zeroed_rect.inflate(0, -6)) surface.blit(image, rect)
class Survivor(BaseClass): """Create the survivor Params: x: the x coordinate of the survivor y: the y coordinate of the survivor""" guns = (scale(pygame.image.load("assets/Images/Weapons/pistol2.png"), Tile.size.scale(1 / 2, 1 / 4)), scale(pygame.image.load("assets/Images/Weapons/shotgun.png"), Tile.size.scale(1 / 2, 1 / 4)), scale(pygame.image.load("assets/Images/Weapons/automatic2.png"), Tile.size.scale(1 / 2, 1 / 4)), scale(pygame.image.load("assets/Images/Weapons/sniper.png"), Tile.size.scale(1 / 2, 1 / 4))) imgs = { d: scale( pygame.image.load( "assets/Images/Players/player_{0}_{1}.png".format( Options.gender, d))) for d in "nsew" } def __init__(self, x, y): self.current_gun = 0 self.direction = pi * 3 / 1 / 2 self.img = Survivor.imgs[dir2chr[self.direction]] super().__init__(x, y) self.health = 100 << 10 * Options.debug # 100 if not debug, 10*2**10 if debug self.vel = Vector(0, 0) self.init_ammo_count = 100, 50, 150, 50 self.ammo_count = list(self.init_ammo_count) def movement(self): """If survivor is between two tiles, or self.to hasn"t been updated: if survivor is on tile: Set self.to to None if survivor is between tile: Add self.vel to self.pos (Move)""" if self.to is not None: if self.pos == self.to: self.to = None else: self.pos += self.vel def draw(self, screen): """Draw survivor and survivor"s gun""" screen.blit(self.img, self.pos.as_ints()) w = self.width h, q = w >> 1, w >> 2 # fractions of width for placing gun_img gun_pos = { pi: (-q, h), 0: (h + q, h), pi * 3 / 2: (h, w), pi / 2: (h, -h) } gun_img = Survivor.guns[self.current_gun] gun_img_rotated = rotated(gun_img, self.direction) screen.blit(gun_img_rotated, self.pos + gun_pos[self.direction]) def rotate(self, new_dir): """If new_dir isn"t self.direction, update self.img to new_dir :param new_dir: direction of player in radians""" if self.direction != new_dir: self.img = Survivor.imgs[dir2chr[new_dir]] self.direction = new_dir @property def ammo(self): """Returns the ammo of the survivor"s current gun >>> a = Survivor(0, 0) >>> a.current_gun = 0 >>> a.ammo 100 >>> a.current_gun = 2 >>> a.ammo 150""" return self.ammo_count[self.current_gun] @ammo.setter def ammo(self, value): """Set the ammo of survivors current_gun to value""" self.ammo_count[self.current_gun] = value def set_target(self, next_tile): """Set the tile to which the survivor will be moving to next_tile""" self.to = next_tile.pos logging.debug("self.pos: %s, self.to: %s", self.to, self.pos)
class PickUp(BaseClass): """Creates a PickUp Params: x: x coordinate of the PickUp y: y coordinate of the PickUp spawn_tile: The index of the tile on which the PickUp is type_: A float between 0 and 1. If it is over 2/3 the PickUp is ammo else health Example: >>> tile = Tile.instances[PickUp.spawn_tiles[0]] >>> a = PickUp(*tile.pos, PickUp.spawn_tiles[0], type_=0.9) >>> a.type "health" TODO: Add more pick ups""" with open(Options.mappath) as file: spawn_tiles = [ i for i, x in enumerate(file.read().replace("\n", "")) if x == "P" ] init_round, left_round = 4, 4 zombie_init_round = None images = { "ammo": scale(pygame.image.load("assets/Images/PickUps/ammo.png")), "health": scale(pygame.image.load("assets/Images/PickUps/health.png")) } sounds = { "ammo": pygame.mixer.Sound("assets/Audio/PickUp/ammo_short.ogg"), "health": pygame.mixer.Sound("assets/Audio/PickUp/health.ogg") } sounds["ammo"].set_volume(Options.volume) sounds["health"].set_volume(Options.volume) instances = set() def __init__(self, x, y, spawn_tile, type_): super().__init__(x, y) PickUp.instances.add(self) self.incr = randint(20, 35) self.spawn_tile = spawn_tile self.type = "ammo" if type_ < 2 / 3 else "health" PickUp.spawn_tiles.remove(spawn_tile) @classmethod def spawn(cls, survivor): _further_than = partial(further_than, survivor=survivor, min_dist=150) pos_spawn_tiles = list(filter(_further_than, cls.spawn_tiles)) if not pos_spawn_tiles: # If no pick-up spawn is far enough away if not cls.spawn_tiles: # If all pick-up spawns are occupied, don"t spawn return pos_spawn_tiles.extend(cls.spawn_tiles) cls.left_round -= 1 type_ = random() spawn_tile = choice(pos_spawn_tiles) spawn_node = Tile.instances[spawn_tile] cls(*spawn_node.pos, spawn_tile, type_) @classmethod def update(cls, screen, survivor, total_frames): if cls.left_round: try: if total_frames % ((Options.fps * cls.zombie_init_round * 2) // cls.init_round) == 0: cls.spawn(survivor) except ZeroDivisionError: if total_frames % Options.fps * 10 == 0: cls.spawn(survivor) del_pick_up = set() for pick_up in cls.instances: screen.blit(cls.images[pick_up.type], pick_up.pos.as_ints()) if collide(*pick_up.pos, *pick_up._size, *survivor.pos, *survivor._size): setattr(survivor, pick_up.type, getattr(survivor, pick_up.type) + pick_up.incr) cls.sounds[pick_up.type].play() cls.spawn_tiles.append(pick_up.spawn_tile) del_pick_up.add(pick_up) del pick_up cls.instances -= del_pick_up
class Bullet(BaseClass): """Creates a Bullet Params: pos: A Vector with a x and y attribute vel: The constant velocity of the bullet. A Vector with a x and y velocity type_: The type of bullet. An int between 0 and 3""" width, height = 7, 9 instances = set() images = (scale(pygame.image.load("assets/Images/Bullets/pistol_b.png"), Tile.size.scale(1 / 3, 1 / 5)), scale(pygame.image.load("assets/Images/Bullets/shotgun_b2.png"), Tile.size.scale(1 / 3, 1 / 5)), scale(pygame.image.load("assets/Images/Bullets/automatic_b.png"), Tile.size.scale(1 / 3, 1 / 5)), scale(pygame.image.load("assets/Images/Bullets/sniper_b2.png"), Tile.size.scale(1 / 3, 1 / 5))) sounds = (pygame.mixer.Sound("assets/Audio/Gunshots/pistol.wav"), pygame.mixer.Sound("assets/Audio/Gunshots/shotgun2.wav"), pygame.mixer.Sound("assets/Audio/Gunshots/automatic.wav"), pygame.mixer.Sound("assets/Audio/Gunshots/sniper.wav")) for sound in sounds: sound.set_volume(Options.volume / 2) dmg_func = (lambda d: max( (-0.00442 * d**2 - 1.4273 * d + 1433.3) / Tile.length, 12), lambda d: ( (432000 * math.e**((-1) / 20 * d) + 720) / (100 * math.e**( (-1) / 20 * d) + 1)) / Tile.length, lambda d: max( (-2 * d + 1080) / Tile.length, 10), lambda d: 36 / Tile.length * (40 * math.log(d + 40) - 100) / 1.1) min_bullet_dist = (Tile.length * 2, Tile.length * 3, Tile.length, Tile.length * 8) last_bullet = None new_keys = (0, -1), (0, 1), (-1, 0), (1, 0) vel2img = dict(zip(new_keys, new_dir_func.values())) # Exchange the keys of new_dir_func with new_keys def __init__(self, pos, vel, type_, survivor): if survivor.ammo_count[type_] <= 0: return # Don"t create bullet if there is no ammo try: dist = (Bullet.last_bullet[1] - Bullet.last_bullet[0]).magnitude() if Bullet.min_bullet_dist[type_] >= dist: return # Don"t create bullet if last_bullet is too close except TypeError: # If Bulle.last_bullet hasn"t been updated yet. 1st bullet dist = None Bullet.sounds[type_].play() survivor.ammo_count[type_] -= 1 self.type = type_ self.orgpos = pos self.vel = vel self.dmg_drop = 1 self.hits = set() Bullet.instances.add(self) stats["Bullets Fired"] += 1 self.vel_as_signs = vel.signs() # Eg. (-3, 0) -> (-1, 0) self.img = Bullet.vel2img[self.vel_as_signs](Bullet.images[type_]) super().__init__(*pos, Bullet.width, Bullet.height) Bullet.last_bullet = pos, pos.copy(), vel logging.debug( "pos: %s, vel: %s, type: %s, dist: %s, last_bullet: %s, sign: %s" + "survivor pos: %s", pos, vel, type_, dist, Bullet.last_bullet, self.vel_as_signs, survivor.pos) def offscreen(self): return (self.pos.x < 0 or self.pos.y < 0 or self.pos.x + self.width > Options.width or self.pos.y + self.height > Options.height) def calc_dmg(self): dist = (self.orgpos - self.pos).magnitude() dmg = Bullet.dmg_func[self.type](dist) dmg *= self.dmg_drop dmg *= 4 if "quad" in Drop.actives else 1 return dmg @classmethod def update(cls, screen): try: cls.last_bullet[1] += cls.last_bullet[2] except TypeError: # If no bullets has been fired last_bullet is None pass del_bullets = set() for bullet in cls.instances: bullet.pos += bullet.vel screen.blit(bullet.img, bullet.pos) if get_number(bullet.pos + bullet.vel) in Tile.solid_nums and "trans" not in Drop.actives \ or bullet.offscreen(): del_bullets.add(bullet) del bullet continue for zombie in Zombie.instances - bullet.hits: if collide(*bullet.pos, *bullet._size, *zombie.pos, *zombie._size): dmg = bullet.calc_dmg() assert dmg > 0 zombie.health -= dmg logging.debug( "type %s, dmg %s, dmg_drop %s, zh %s, 4xd: %s", bullet.type, dmg, bullet.dmg_drop, zombie.health, "quad" in Drop.actives) bullet.dmg_drop /= 1.1 if not bullet.hits: stats["Bullets Hit"] += 1 bullet.hits.add(zombie) cls.instances -= del_bullets
class Drop(BaseClass): """Drop power ups similar to power ups in call of duty Current power ups are double damage for 5 seconds and max ammo and walking through walls The order of the drops in all lists is: 0th max_ammo, 1st quad damage, 2nd freeze, 3rd through_walls params: --------------- pos: The tile on which the power is type_: An integer indicating the type of the drop as its index in Drop.effects TODO: Add more types of power ups""" instances = set() load_img = lambda s: scale( pygame.image.load("assets/Images/Drops/%s.png" % s)) imgs = (load_img("max_ammo"), load_img("quad_damage"), load_img("freeze"), load_img("through_walls")) load_sound = lambda s: pygame.mixer.Sound("assets/Audio/Drop/%s.ogg" % s) sounds = (load_sound("max_ammo"), load_sound("quad_damage"), load_sound("freeze"), load_sound("through_walls")) for sound in sounds: sound.set_volume(Options.volume) effects = full_ammo, quad_dmg, freeze, through_walls actives = {} def __init__(self, pos, type_): self.type_ = type_ self.countdown = Options.fps * 5 Drop.instances.add(self) super().__init__(*pos) @classmethod def spawn(cls, pos): if random.random() < 2 / 3 if Options.debug else 14 / 15: return type_ = random.randint(0, len(cls.effects) - 1) cls(pos, type_) @classmethod def update(cls, screen, survivor): del_drops = set() for drop in cls.instances: drop.countdown -= 1 if drop.countdown == 0: del_drops.add(drop) continue screen.blit(cls.imgs[drop.type_], drop.pos) if collide(*drop.pos, *drop._size, *survivor.pos, *survivor._size): cls.effects[drop.type_](survivor) cls.sounds[drop.type_].play() del_drops.add(drop) continue cls.instances -= del_drops for power_up, value in tuple(cls.actives.items()): cls.actives[power_up] -= 1 if value == 0: if power_up == "trans": new_tile = survivor.get_tile().closest_open_tile() survivor.pos = new_tile.pos.copy() survivor.to = None del cls.actives[power_up]