Beispiel #1
0
class Item(BaseSprite, metaclass=abc.ABCMeta):
    """An abstract base class for sprites that represent in-game items."""
    # Number of pixels up and down that item will bob.
    BOB_RANGE = 15
    BOB_SPEED = 0.2

    def __init__(self, x: float, y: float, image: str, sound: str,
                 groups: typing.Dict[str, pg.sprite.Group]):
        BaseSprite.__init__(self, image, groups, groups['all'],
                            groups['items'])
        self.rect.center = (x, y)
        self._sfx = sound
        self._spawn_pos = pg.math.Vector2(x, y)
        self._effect_timer = Timer()
        # Default duration is 0.
        self._duration = 0
        # Tween function maps integer steps to values between 0 and 1.
        self._tween = tween.easeInOutSine
        self._step = 0
        self._direction = 1

    @property
    def spawn_pos(self) -> pg.math.Vector2:
        return self._spawn_pos

    def update(self, dt: float) -> None:
        """Floating animation for an item that has spawned. Credits to Chris Bradfield from KidsCanCode."""
        # Shift bobbing y offset to bob about item's original center.
        offset = Item.BOB_RANGE * (self._tween(self._step / Item.BOB_RANGE) -
                                   0.5)
        self.rect.centery = self._spawn_pos.y + offset * self._direction
        self._step += Item.BOB_SPEED
        # Reverse bobbing direction when item returns to center.
        if self._step > Item.BOB_RANGE:
            self._step = 0
            self._direction *= -1

    def activate(self, sprite: pg.sprite.Sprite) -> None:
        """Applies the item's effect upon pickup and causes it to be stop being drawn."""
        self._apply_effect(sprite)
        self._effect_timer.restart()
        # Applies to items with non-zero duration.
        sfx_loader.play(self._sfx)
        # Make sure it doesn't get drawn anymore after the effect has been applied.
        super().kill()

    def effect_subsided(self) -> bool:
        """Checks if the item's effect should subside."""
        return self._effect_timer.elapsed() > self._duration

    @abc.abstractmethod
    def _apply_effect(self, sprite) -> None:
        """Effect that is applied on item as long as the timer has not subsided."""
        pass

    def remove_effect(self, sprite) -> None:
        """Causes an item with a non-zero duration to have its effect removed from a sprite at the end."""
        pass
Beispiel #2
0
class AIReloadState(AITurretCtrlState):
    """ State class for AITankCtrl for reloading the AI's turret."""
    _RELOAD_TIME = 10000

    def __init__(self, ai):
        AITurretCtrlState.__init__(self, ai)
        self._reload_timer = None

    def enter(self) -> None:
        """Initiates the reload timer for the AI's turret."""
        self._reload_timer = Timer()

    def update(self, dt: float) -> None:
        """Switches back to attack state once it's time to reload."""
        if self._reload_timer.elapsed() > AIReloadState._RELOAD_TIME:
            self._ai.turret.barrel.reload()
            self._ai.state = self._ai.attack_state
Beispiel #3
0
class Bullet(BaseSprite, MoveMixin):
    """Sprite class that models a Bullet object."""
    IMAGE_ROT = 90  # See sprite sheet.

    def __init__(self, x: float, y: float, angle: float, color: str,
                 category: str, owner,
                 all_groups: typing.Dict[str, pg.sprite.Group]):
        """Creates a bullet object, rotating it to face the correct direction."""
        self._layer = cfg.ITEM_LAYER
        BaseSprite.__init__(self, _IMAGES[category][color], all_groups,
                            all_groups['all'], all_groups['bullets'])
        MoveMixin.__init__(self, x, y)
        self.vel = pg.math.Vector2(_STATS[category]["speed"], 0).rotate(-angle)
        self._damage = _STATS[category]["damage"]
        self._lifetime = _STATS[category]["lifetime"]
        self._spawn_timer = Timer()
        self._owner = owner
        RotateMixin.rotate_image(self, self.image, angle - Bullet.IMAGE_ROT)

    @property
    def owner(self):
        """Returns the owner sprite that triggered the creation of this bullet, i.e., a Tank object.
        :return: A sprite that triggered the firing of this bullet.
        """
        return self._owner

    @classmethod
    def range(cls, category: str) -> float:
        """Returns the range that this bullet can travel before it vanishes."""
        return _STATS[category]["speed"] * (_STATS[category]["lifetime"] /
                                            1000)

    @property
    def damage(self) -> int:
        """Returns the damage that this bullet can cause upon collision."""
        return self._damage

    def update(self, dt) -> None:
        """Moves the bullet until it's time for it to disappear."""
        if self._spawn_timer.elapsed() > self._lifetime:
            self.kill()
        else:
            self.move(dt)
Beispiel #4
0
class MuzzleFlash(BaseSprite, RotateMixin):
    """Sprite that models the flash (or explosion) at the barrel's nozzle upon firing a bullet."""
    FLASH_DURATION = 25
    IMAGE = 'shotLarge.png'

    def __init__(self, x: float, y: float, rot: float, all_groups):
        """Aligns the MuzzleFlash so that it starts at the tip of the Barrel nozzle."""
        self._layer = cfg.EFFECTS_LAYER
        BaseSprite.__init__(self, MuzzleFlash.IMAGE, all_groups, all_groups['all'])
        RotateMixin.__init__(self)
        self.rect.center = (x, y)
        self.rot = rot
        self.rotate()
        self._spawn_timer = Timer()

    def update(self, dt: float) -> None:
        """Remove the flash from screen after a short duration."""
        if self._spawn_timer.elapsed() > MuzzleFlash.FLASH_DURATION:
            self.kill()
Beispiel #5
0
class Tank(BaseSprite, MoveNonlinearMixin, RotateMixin, DamageMixin):
    """Sprite class that models a Tank object."""
    KNOCK_BACK = 100

    _SPEED_CUTOFF = 100
    _TRACK_DELAY = 100

    BIG = "big"
    LARGE = "large"
    HUGE = "huge"

    def __init__(self, x: float, y: float, img: str,
                 all_groups: typing.Dict[str, pg.sprite.Group]):
        """Initializes the tank's sprite with no barrels to shoot from.

        :param x: x coordinate for centering the sprite's position.
        :param y: y coordinate for centering the sprite's position.
        :param img: filename for the sprite's tank image.
        :param all_groups: A dictionary of all of the game world's sprite groups.
        """
        self._layer = cfg.TANK_LAYER
        BaseSprite.__init__(self, img, all_groups, all_groups['all'],
                            all_groups['tanks'], all_groups['damageable'])
        MoveNonlinearMixin.__init__(self, x, y)
        RotateMixin.__init__(self)
        DamageMixin.__init__(self, self.hit_rect)
        self.rect.center = (x, y)
        self.MAX_ACCELERATION = 768
        self._barrels = []
        self._items = []
        self._track_timer = Timer()

    def update(self, dt: float) -> None:
        """Rotates, moves, and handles any active in-game items that have some effect.

        :param dt: Time elapsed since the tank's last update.
        :return: None
        """
        self.rotate(dt)
        self.move(dt)
        for item in self._items:
            if item.effect_subsided():
                item.remove_effect(self)
                self._items.remove(item)
        if self.vel.length_squared(
        ) > Tank._SPEED_CUTOFF and self._track_timer.elapsed(
        ) > Tank._TRACK_DELAY:
            self._spawn_tracks()

    @property
    def range(self) -> float:
        """The shooting distance of the tank, as given by the tank's barrels."""
        return self._barrels[0].range

    @property
    def color(self) -> str:
        """Returns a string representing the color of one of the tank's barrels."""
        return self._barrels[0].color

    def pickup(self, item) -> None:
        """Activates an item that this Tank object has picked up (collided with) and saves it.

        :param item: Item sprite that can be used to apply an effect on the Tank object.
        :return: None
        """
        item.activate(self)
        self._items.append(item)

    def equip_barrel(self, barrel: Barrel) -> None:
        """Equips a new barrel to this tank."""
        self._barrels.append(barrel)

    def _spawn_tracks(self) -> None:
        """Spawns track sprites as the tank object moves around the map."""
        Tracks(*self.pos, self.hit_rect.height, self.hit_rect.height, self.rot,
               self.all_groups)
        self._track_timer.restart()

    def rotate_barrel(self, aim_direction: float):
        """Rotates the all of the tank's barrels in a direction indicated by aim_direction."""
        for barrel in self._barrels:
            barrel.rot = aim_direction
            barrel.rotate()

    def ammo_count(self) -> int:
        """Returns the ammo count of the tank's barrels."""
        return self._barrels[0].ammo_count

    def fire(self) -> None:
        """Fires a bullet from the Tank's barrels."""
        for barrel in self._barrels:
            barrel.fire()

    def reload(self) -> None:
        """Reloads bullets for each of the bullets."""
        for barrel in self._barrels:
            barrel.reload()

    def kill(self) -> None:
        """Removes this sprite and its barrels from all sprite groups."""
        for barrel in self._barrels:
            barrel.kill()
        for item in self._items:
            item.kill()
        super().kill()

    @classmethod
    def color_tank(cls, x: float, y: float, color: str, category: str,
                   groups: typing.Dict[str, pg.sprite.Group]):
        """Factory method for creating Tank objects."""
        tank = cls(x, y, f"tankBody_{color}_outline.png", groups)
        offset = pg.math.Vector2(tank.hit_rect.height // 3, 0)
        barrel = Barrel.create_color_barrel(tank, offset, color.capitalize(),
                                            category, groups)
        tank.equip_barrel(barrel)
        return tank

    @classmethod
    def enemy(cls, x: float, y: float, size: str,
              groups: typing.Dict[str, pg.sprite.Group]) -> 'Tank':
        """Returns a enemy tank class depending on the size parameter."""
        if size == cls.BIG:
            return cls.big_tank(x, y, groups)
        elif size == cls.LARGE:
            return cls.large_tank(x, y, groups)
        elif size == cls.HUGE:
            return cls.huge_tank(x, y, groups)
        raise ValueError(f"Invalid size attribute: {size}")

    @classmethod
    def big_tank(cls, x: float, y: float,
                 groups: typing.Dict[str, pg.sprite.Group]) -> 'Tank':
        """Returns the a 'big' enemy tank."""
        tank = cls(x, y, "tankBody_bigRed.png", groups)
        for y_offset in (-10, 10):
            barrel = Barrel.create_special(tank,
                                           pg.math.Vector2(0, y_offset),
                                           "Dark",
                                           groups,
                                           special=1)
            tank.equip_barrel(barrel)
        return tank

    @classmethod
    def large_tank(cls, x: float, y: float,
                   groups: typing.Dict[str, pg.sprite.Group]) -> 'Tank':
        """Returns the a 'large' enemy tank."""
        tank = cls(x, y, "tankBody_darkLarge.png", groups)
        tank.MAX_ACCELERATION *= 0.9
        for y_offset in (-10, 10):
            barrel = Barrel.create_special(tank,
                                           pg.math.Vector2(0, y_offset),
                                           "Dark",
                                           groups,
                                           special=4)
            tank.equip_barrel(barrel)
        return tank

    @classmethod
    def huge_tank(cls, x: float, y: float,
                  groups: typing.Dict[str, pg.sprite.Group]) -> 'Tank':
        """Returns the a 'huge' enemy tank."""
        tank = cls(x, y, "tankBody_huge_outline.png", groups)
        tank.MAX_ACCELERATION *= 0.8
        for y_offset in (-10, 10):
            barrel = Barrel.create_special(tank,
                                           pg.math.Vector2(20, y_offset),
                                           "Dark",
                                           groups,
                                           special=4)
            tank.equip_barrel(barrel)
        barrel = Barrel.create_special(tank,
                                       pg.math.Vector2(-10, 0),
                                       "Dark",
                                       groups,
                                       special=1)
        tank.equip_barrel(barrel)
        return tank
Beispiel #6
0
class Level:
    """Class that creates, draws, and updates the game world, including the map and all sprites."""
    _ITEM_RESPAWN_TIME = 30000  # 1 minute.

    def __init__(self, level_file: str):
        """Creates a map and creates all of the sprites in it.

        :param level_file: Filename of level file to load from the configuration file's map folder.
        """
        # Create the tiled map surface.
        map_loader = TiledMapLoader(level_file)
        self.image = map_loader.make_map()
        self.rect = self.image.get_rect()
        self._groups = {
            'all': pg.sprite.LayeredUpdates(),
            'tanks': pg.sprite.Group(),
            'damageable': pg.sprite.Group(),
            'bullets': pg.sprite.Group(),
            'obstacles': pg.sprite.Group(),
            'items': pg.sprite.Group(),
            'item_boxes': pg.sprite.Group()
        }
        self._player = None
        self._camera = None
        self._ai_mobs = []
        self._item_spawn_positions = []
        self._item_spawn_timer = Timer()
        # Initialize all sprites in game world.
        self._init_sprites(map_loader.tiled_map.objects)

    def _init_sprites(self, objects: pytmx.TiledObjectGroup) -> None:
        """Initializes all of the pygame sprites in this level's map.

        :param objects: Iterator for accessing the properties of all game objects to be created.
        :return: None

        Expects to find a single 'player' and 'enemy_tank' object, and possible more than one
        of any other object. A sprite is created out of each object and added to the appropriate
        group. A boundary for the game world is also created to keep the sprites constrained.
        """
        game_objects = {}
        for t_obj in objects:
            # Expect single enemy tank and multiple of other objects.
            if t_obj.name == 'enemy_tank' or t_obj.name == "player":
                game_objects[t_obj.name] = t_obj
            else:
                game_objects.setdefault(t_obj.name, []).append(t_obj)

        # Create the player and world camera.
        p = game_objects.get('player')
        tank = Tank.color_tank(p.x, p.y, p.color, p.category,
                               self._groups)  # Make a tank factory.
        self._player = PlayerCtrl(tank)
        self._camera = Camera(self.rect.width, self.rect.height,
                              self._player.tank)

        # Spawn single enemy tank.
        t = game_objects.get('enemy_tank')
        tank = Tank.enemy(t.x, t.y, t.size,
                          self._groups)  # Make a tank factory.
        ai_patrol_points = game_objects.get('ai_patrol_point')
        ai_boss = AITankCtrl(tank, ai_patrol_points, self._player.tank)
        self._ai_mobs.append(ai_boss)

        # Spawn turrets.
        for t in game_objects.get('turret'):
            turret = Turret(t.x, t.y, t.category, t.special, self._groups)
            self._ai_mobs.append(
                AITurretCtrl(turret, ai_boss, self._player.tank))

        # Spawn obstacles that one can collide with.
        for tree in game_objects.get('small_tree'):
            Tree(tree.x, tree.y, self._groups)

        # Spawn items boxes that can be destroyed to get an item.
        for box in game_objects.get('box_spawn'):
            self._item_spawn_positions.append((box.x, box.y))
            ItemBox.spawn(box.x, box.y, self._groups)

        # Creates the boundaries of the game world.
        BoundaryWall(x=0,
                     y=0,
                     width=self.rect.width,
                     height=1,
                     all_groups=self._groups)  # Top
        BoundaryWall(x=0,
                     y=self.rect.height,
                     width=self.rect.width,
                     height=1,
                     all_groups=self._groups)  # Bottom
        BoundaryWall(x=0,
                     y=0,
                     width=1,
                     height=self.rect.height,
                     all_groups=self._groups)  # Left
        BoundaryWall(x=self.rect.width,
                     y=0,
                     width=1,
                     height=self.rect.height,
                     all_groups=self._groups)  # Right

    def _can_spawn_item(self) -> bool:
        """"Checks if a new item can be spawned."""
        return self._item_spawn_timer.elapsed() > Level._ITEM_RESPAWN_TIME and \
            len(self._groups['items']) + len(self._groups['item_boxes']) < len(self._item_spawn_positions)

    def is_player_alive(self) -> bool:
        """Checks if the player's tank has been defeated."""
        return self._player.tank.alive()

    def mob_count(self) -> int:
        """Checks if all the AI mobs have been defeated."""
        return len(self._ai_mobs)

    def process_inputs(self) -> None:
        """Handles keys and clicks that affect the game world."""
        self._player.handle_keys()
        # Convert mouse coordinates to world coordinates.
        mouse_x, mouse_y = pg.mouse.get_pos()
        mouse_world_pos = pg.math.Vector2(mouse_x + self._camera.rect.x,
                                          mouse_y + self._camera.rect.y)
        self._player.handle_mouse(mouse_world_pos)

    def update(self, dt: float) -> None:
        """Updates the game world's AI, sprites, camera, and resolves collisions.

        :param dt: time elapsed since the last update of the game world.
        :return: None
        """
        for ai in self._ai_mobs:
            ai.update(dt)
        self._groups['all'].update(dt)
        # Update list of ai mobs.
        self._camera.update()

        game_items_count = len(self._groups['items'])
        collision_handler.handle_collisions(self._groups)
        if game_items_count > 0 and len(
                self._groups['items']) < game_items_count:
            self._item_spawn_timer.restart()
        # See if it's time to spawn a new item.
        if self._can_spawn_item():
            available_positions = self._item_spawn_positions.copy()
            for x, y in self._item_spawn_positions:
                for sprite in self._groups['items']:
                    if sprite.spawn_pos.x == x and sprite.spawn_pos == y and (
                            x, y) in available_positions:
                        available_positions.remove((x, y))
                for sprite in self._groups['item_boxes']:
                    if sprite.rect.center == (x, y) and (
                            x, y) in available_positions:
                        available_positions.remove((x, y))
            if available_positions:
                x, y, = random.choice(available_positions)
                ItemBox.spawn(x, y, self._groups)

        # Filter out any AIs that have been defeated.
        self._ai_mobs = [ai for ai in self._ai_mobs if ai.sprite.alive()]

    def draw(self, screen: pg.Surface) -> None:
        """Draws every sprite in the game world, as well as heads-up display elements.

        :param screen: The screen surface that the world's elements will be drawn to.
        :return: None
        """
        # Draw the map.
        screen.blit(self.image, self._camera.apply(self.rect))
        # Draw all sprites.
        for sprite in self._groups['all']:
            screen.blit(sprite.image, self._camera.apply(sprite.rect))
            # pg.draw.rect(screen, (255, 255, 255), self._camera.apply(sprite.hit_rect), 1)

        # Draw HUD.
        for ai in self._ai_mobs:
            ai.sprite.draw_health(screen, self._camera)
        self._player.draw_hud(screen, self._camera)
Beispiel #7
0
class Barrel(BaseSprite, RotateMixin):
    """Sprite class that models a Barrel object."""
    _FIRE_SFX = 'shoot.wav'

    def __init__(self, tank, offset: pg.math.Vector2, image: str, color: str,
                 category: str, all_groups: typing.Dict[str, pg.sprite.Group]):
        """Fills up the Barrel's ammo and centers its position on its parent."""
        self._layer = cfg.BARREL_LAYER
        BaseSprite.__init__(self, image, all_groups, all_groups['all'])
        RotateMixin.__init__(self)
        # Bullet parameters.
        self._category = category
        self._color = color
        self._ammo_count = _STATS[self._category]["max_ammo"]

        # Parameters used for barrel position.
        self._parent = tank
        self.rect.center = tank.rect.center
        self._offset = offset

        self._fire_delay = _STATS[self._category]["fire_delay"]
        self._fire_timer = Timer()

    @property
    def color(self) -> str:
        """Returns a string representing the barrel's color."""
        return self._color

    @property
    def ammo_count(self) -> int:
        """Returns the current ammo count for this barrel."""
        return self._ammo_count

    @property
    def range(self) -> float:
        """Returns the fire range of the barrel."""
        return Bullet.range(self._category)

    @property
    def fire_delay(self) -> float:
        """Returns the number of milliseconds until barrel can fire again."""
        return self._fire_delay

    def update(self, dt: float) -> None:
        """Updates the barrel's position by centering on the parent's position (accounting for the offset)."""
        vec = self._offset.rotate(-self.rot)
        self.rect.centerx = self._parent.rect.centerx + vec.x
        self.rect.centery = self._parent.rect.centery + vec.y

    def fire(self) -> None:
        """Fires a Bullet if enough time has passed and if there's ammo."""
        if self._ammo_count > 0 and self._fire_timer.elapsed() > self._fire_delay:
            self._spawn_bullet()
            sfx_loader.play(Barrel._FIRE_SFX)
            self._fire_timer.restart()

    def _spawn_bullet(self) -> None:
        """Spawns a Bullet object from the Barrel's nozzle."""
        fire_pos = pg.math.Vector2(self.hit_rect.height, 0).rotate(-self.rot)
        fire_pos.xy += self.rect.center
        Bullet(fire_pos.x, fire_pos.y, self.rot, self._color, self._category, self._parent, self.all_groups)
        MuzzleFlash(*fire_pos, self.rot, self.all_groups)
        self._ammo_count -= 1

    def reload(self) -> None:
        """Reloads the Barrel to have maximum ammo"""
        self._ammo_count = _STATS[self._category]["max_ammo"]

    def kill(self) -> None:
        self._parent = None
        super().kill()

    @classmethod
    def create_color_barrel(cls, tank, offset: pg.math.Vector2, color: str, category: str,
                            groups: typing.Dict[str, pg.sprite.Group]) -> 'Barrel':
        """Creates a color barrel object."""
        return cls(tank, offset, f"tank{color.capitalize()}_barrel{cfg.CATEGORY[category]}.png", color, category, groups)

    @classmethod
    def create_special(cls, tank, offset: pg.math.Vector2, color: str,
                       all_groups: typing.Dict[str, pg.sprite.Group], special) -> 'Barrel':
        """Creates a special barrel object."""
        barrel = cls(tank, offset, f"specialBarrel{special}.png", color, "standard", all_groups)
        helpers.flip(barrel, orig_image=barrel.image, x_reflect=True, y_reflect=False)
        return barrel