The enemies are a threat (sort of)
[koperkapel.git] / koperkapel / scenes / level.py
index a17de55d70ece5aa8a37e2bf88891580eadf728c..54041baa7cc20e863e0c0457ba8a9cbe70a77b47 100644 (file)
@@ -1,13 +1,35 @@
 """Render a level"""
 
 """Render a level"""
 
+import random
 from pgzero.constants import keys
 from pygame import Surface
 import pygame.locals as pgl
 from ..loaders.levelloader import levels
 from pgzero.constants import keys
 from pygame import Surface
 import pygame.locals as pgl
 from ..loaders.levelloader import levels
-from .base import Scene, ChangeSceneEvent, MoveViewportEvent, defer_to_update
+from .base import (
+    Scene, ChangeSceneEvent, MoveViewportEvent, WorldEvent, defer_to_update)
 from ..constants import TILE_SIZE, WIDTH, HEIGHT
 from ..constants import TILE_SIZE, WIDTH, HEIGHT
+from ..gamelib.items import clone_old_item, create_new_item
 from ..roaches import build_roach
 from ..vehicles.base import Vehicle
 from ..roaches import build_roach
 from ..vehicles.base import Vehicle
+from ..weapons import weapon_by_name
+
+
+class PlayerStats:
+    def __init__(self, world):
+        roaches = world.roaches
+        self.health = sum(r.health for r in roaches)
+        self.smart = self._count_attr("smart", roaches)
+        self.fast = self._count_attr("fast", roaches)
+        self.strong = self._count_attr("strong", roaches)
+
+    def __str__(self):
+        return "<PlayerStats health=%d smart=%d fast=%d strong=%d>" % (
+            self.health, self.smart, self.fast, self.strong)
+
+    def _count_attr(self, attr, roaches):
+        attrs = [r[attr] for r in roaches]
+        attrs = [a for a in attrs if a]
+        return len(attrs)
 
 
 class BaseLevelScene(Scene):
 
 
 class BaseLevelScene(Scene):
@@ -16,6 +38,7 @@ class BaseLevelScene(Scene):
     def __init__(self):
         super().__init__()
         self._level = None
     def __init__(self):
         super().__init__()
         self._level = None
+        self._stats = None
 
     def enter(self, world):
         self._level = levels.load(world.level.name)
 
     def enter(self, world):
         self._level = levels.load(world.level.name)
@@ -28,6 +51,7 @@ class BaseLevelScene(Scene):
         self._overlay = self._surfaces['floor'].copy()
         self._doors = self.actors.add_layer("doors", level=9)
         self._keypads = self.actors.add_layer("keypads", level=8)
         self._overlay = self._surfaces['floor'].copy()
         self._doors = self.actors.add_layer("doors", level=9)
         self._keypads = self.actors.add_layer("keypads", level=8)
+        self._bullets = self.actors.add_layer("bullets", level=10)
         # These are already Actors
         for door in self._level.doors:
             self._doors.add(door)
         # These are already Actors
         for door in self._level.doors:
             self._doors.add(door)
@@ -84,16 +108,50 @@ class BaseLevelScene(Scene):
 class GameLevelScene(BaseLevelScene):
 
     def enter(self, world):
 class GameLevelScene(BaseLevelScene):
 
     def enter(self, world):
+        self._held_keys = set()
         if self._level is not None:
         if self._level is not None:
+            for generator in self._generators:
+                generator.unpause()
             return
             return
+        self._update_player_stats(world)
         super().enter(world)
         self._roaches = self.actors.add_layer("roaches", level=10)
         self._friends = self.actors.add_layer("friendly roaches", level=9)
         super().enter(world)
         self._roaches = self.actors.add_layer("roaches", level=10)
         self._friends = self.actors.add_layer("friendly roaches", level=9)
+        self._items = self.actors.add_layer("items", level=9)
+        self._generators = self.actors.add_layer("enemy generators", level=8)
+        self._enemies = self.actors.add_layer("enemies", level=11)
         self._vehicle = Vehicle.current(world)
         self._mode = 'walk'
         self._vehicle = Vehicle.current(world)
         self._mode = 'walk'
-        self._angle = 0
+        self._angle = 0  # up
+        self._angle_dp = (0, -1)  # up
+        self._init_items()
+        self._init_friendly_roaches()
+        self._init_generators()
+        self._key_rate = 0.2
+        self._last_key_down = 0
+        self._last_dmg = 0
         return self._init_roaches(world)
 
         return self._init_roaches(world)
 
+    def _init_items(self):
+        for item in self._level.items:
+            self._items.add(item)
+
+    def _init_friendly_roaches(self):
+        for friend in self._level.friends:
+            self._friends.add(friend)
+
+    def exit(self, world):
+        for generator in self._generators:
+            # We don't want these running while we're on other levels, but we
+            # don't want to delete them here either (because of the vehicle
+            # management view)
+            generator.pause()
+
+    def _init_generators(self):
+        for generator in self._level.enemy_generators:
+            self._generators.add(generator)
+            generator.unpause()
+
     def _init_roaches(self, world):
         x, y = self._level.start_pos
         self._level_layer = 'floor'
     def _init_roaches(self, world):
         x, y = self._level.start_pos
         self._level_layer = 'floor'
@@ -101,8 +159,6 @@ class GameLevelScene(BaseLevelScene):
         self._set_pos(x, y)
         self._avatar.pos = (WIDTH // 2, HEIGHT // 2)
         self._roaches.add(self._avatar)
         self._set_pos(x, y)
         self._avatar.pos = (WIDTH // 2, HEIGHT // 2)
         self._roaches.add(self._avatar)
-        for friend in self._level.friends:
-            self._friends.add(friend)
         # Fix viewport offset
         return [
             MoveViewportEvent((
         # Fix viewport offset
         return [
             MoveViewportEvent((
@@ -115,14 +171,17 @@ class GameLevelScene(BaseLevelScene):
 
     def _can_move(self, x, y):
         if self._mode == 'walk':
 
     def _can_move(self, x, y):
         if self._mode == 'walk':
-            return self._level.can_walk(x, y, self._level_layer)
+            if not self._level.enemy_at(x, y):
+                return self._level.can_walk(x, y, self._level_layer)
+            return False
         elif self._mode == 'fly':
             return self._level.can_fly(x, y, self._level_layer)
         elif self._mode == 'crawl':
             return self._level.can_crawl(x, y, self._level_layer)
 
         elif self._mode == 'fly':
             return self._level.can_fly(x, y, self._level_layer)
         elif self._mode == 'crawl':
             return self._level.can_crawl(x, y, self._level_layer)
 
-    def _set_angle(self, angle):
+    def _set_angle(self, angle, dp):
         self._angle = angle
         self._angle = angle
+        self._angle_dp = dp
         self._avatar.angle = angle
 
     @defer_to_update
         self._avatar.angle = angle
 
     @defer_to_update
@@ -132,28 +191,167 @@ class GameLevelScene(BaseLevelScene):
         self._avatar = self._vehicle.get_avatar(world)
         self._avatar.pos = (WIDTH // 2, HEIGHT // 2)
         self._roaches.add(self._avatar)
         self._avatar = self._vehicle.get_avatar(world)
         self._avatar.pos = (WIDTH // 2, HEIGHT // 2)
         self._roaches.add(self._avatar)
-        self._set_angle(self._angle)
+        self._set_angle(self._angle, self._angle_dp)
 
     @defer_to_update
     def _add_roach(self, world):
         world.roaches.append(build_roach(world))
         self._vehicle_changed()
 
 
     @defer_to_update
     def _add_roach(self, world):
         world.roaches.append(build_roach(world))
         self._vehicle_changed()
 
+    @defer_to_update
+    def _damage_player(self, world):
+        if not world.roaches:
+            # Skip out if we're already dead
+            return
+        roach = random.choice(world.roaches)
+        roach.health -= self._last_dmg
+        self._last_dmg = 0
+        if roach.health < 0:
+            index = [x.name for x in world.roaches].index(roach.name)
+            world.roaches.pop(index)
+            # We can't check for empty, because updates will be processed later
+            if len(world.roaches) > 1:
+                self._vehicle_changed()
+
+    @defer_to_update
+    def _gain_item(self, world, item):
+        if item.item_type == "serum":
+            world.serums.append(item.item_data["serum"])
+        elif item.item_type == "weapon":
+            old_weapon = world.weapons.current
+            world.weapons.current = item.item_data["weapon"]
+            if old_weapon != "spit":
+                clone = clone_old_item(item, weapon=old_weapon)
+                self._level.items.append(clone)
+                self._items.add(clone)
+        self._vehicle_changed()
+
+    def _hit_enemy(self, enemy, weapon):
+        enemy.health -= weapon.damage
+        if enemy.health <= 0:
+            self._level.remove_enemy(enemy)
+            self._enemies.remove(enemy)
+
+    def _fire_bullet(self, bullet, pos, dp, angle):
+        if len(self._bullets) >= 10:
+            return
+        bullet.game_pos = pos
+        bullet.game_dp = dp
+        bullet.dt = 0
+        bullet.level_layer = self._level_layer
+        bullet.angle = angle
+        self._bullets.add(bullet)
+
+    def _check_for_bullet_hits(self):
+        for bullet in list(self._bullets):
+            for enemy in list(self._enemies):
+                if enemy.collidepoint(bullet.pos):
+                    self._hit_enemy(enemy, bullet.weapon)
+                    self._bullets.remove(bullet)
+
+    def _update_bullet(self, bullet, dt):
+        bullet.dt += dt
+        if bullet.dt > 0.1:
+            bullet.dt = 0
+            bullet.game_pos = pos = (
+                bullet.game_pos[0] + bullet.game_dp[0],
+                bullet.game_pos[1] + bullet.game_dp[1])
+            if not self._level.can_bullet(pos[0], pos[1], bullet.level_layer):
+                self._bullets.remove(bullet)
+
+    @defer_to_update
+    def _fire_weapon(self, world):
+        weapon = weapon_by_name(world.weapons.current)
+        weapon.play_sound()
+        if weapon.bullet_range > 0:
+            self._fire_bullet(
+                weapon.assemble_bullet(), self._player_pos, self._angle_dp,
+                self._angle)
+        else:
+            # melee
+            pos, dp = self._player_pos, self._angle_dp
+            pos = (pos[0] + dp[0], pos[1] + dp[1])
+            enemy = self._level.get_enemy(pos[0], pos[1])
+            if enemy:
+                self._hit_enemy(enemy, weapon)
+
     @defer_to_update
     def _change_vehicle(self, world):
     @defer_to_update
     def _change_vehicle(self, world):
-        vehicle = Vehicle.random()
-        world.vehicles.current = vehicle
+        x, y = self._player_pos
+
+        old_vehicle = world.vehicles.current
+
+        item = self._level.item_at(x, y)
+
+        if item and item.item_type == "vehicle":
+            world.vehicles.current = item.item_data["vehicle"]
+            self._level.remove_item(item)
+            self._items.remove(item)
+        else:
+            world.vehicles.current = "walking"
+
+        if old_vehicle != "walking":
+            dropped_vehicle = create_new_item(
+                "vehicle", (x, y), vehicle=old_vehicle)
+            self._level.items.append(dropped_vehicle)
+            self._items.add(dropped_vehicle)
+
         self._vehicle_changed()
 
     def update(self, world, engine, dt):
         self._vehicle_changed()
 
     def update(self, world, engine, dt):
+        if not world.roaches:
+            # Catch death here
+            from .menu import MenuScene
+            return [ChangeSceneEvent(MenuScene())]
         super().update(world, engine, dt)
         events = world.pop_events()
         for friend in self._friends:
             friend.pos = self.calc_offset(
                 friend.game_pos[0] * TILE_SIZE, friend.game_pos[1] * TILE_SIZE)
         super().update(world, engine, dt)
         events = world.pop_events()
         for friend in self._friends:
             friend.pos = self.calc_offset(
                 friend.game_pos[0] * TILE_SIZE, friend.game_pos[1] * TILE_SIZE)
+        for item in self._items:
+            item.pos = self.calc_offset(
+                item.game_pos[0] * TILE_SIZE, item.game_pos[1] * TILE_SIZE)
+        self._check_enemies(dt)
+        for enemy in self._enemies:
+            enemy.pos = self.calc_offset(
+                enemy.game_pos[0] * TILE_SIZE, enemy.game_pos[1] * TILE_SIZE)
+        for bullet in list(self._bullets):
+            self._update_bullet(bullet, dt)
+            bullet.pos = self.calc_offset(
+                bullet.game_pos[0] * TILE_SIZE + (TILE_SIZE // 2),
+                bullet.game_pos[1] * TILE_SIZE + (TILE_SIZE // 2))
+        self._check_for_bullet_hits()
+        self._update_player_stats(world)
+        more = self._check_held_keys(dt)
+        if more:
+            events.extend(more)
         return events
 
         return events
 
-    def _movement_key(self, key):
+    def _update_player_stats(self, world):
+        self._stats = PlayerStats(world)
+
+    def _check_enemies(self, dt):
+        if len(self._level.enemies) != len(self._enemies):
+            # New nemy has spawned
+            for enemy in self._level.enemies:
+                if enemy not in self._enemies:
+                    self._enemies.add(enemy)
+        for enemy in self._enemies:
+            dmg = enemy.attack(self._player_pos, self._level_layer, dt)
+            if dmg is not None:
+                self._last_dmg += dmg
+        if self._last_dmg:
+            self._damage_player()
+
+    def _check_held_keys(self, dt):
+        for key in self._held_keys:
+            self._last_key_down += dt
+            if key in (keys.DOWN, keys.UP, keys.LEFT, keys.RIGHT):
+                return self._movement_key(key, dt)
+            elif key == keys.X:
+                return self._fire_key(dt)
+
+    def _movement_key(self, key, dt):
         x, y = self._player_pos
         for k, dp, angle in (
             (keys.DOWN, (0, 1), 180),
         x, y = self._player_pos
         for k, dp, angle in (
             (keys.DOWN, (0, 1), 180),
@@ -162,12 +360,18 @@ class GameLevelScene(BaseLevelScene):
             (keys.RIGHT, (1, 0), -90),
         ):
             if key == k:
             (keys.RIGHT, (1, 0), -90),
         ):
             if key == k:
-                nx, ny = x + dp[0], y + dp[1]
-                if self._can_move(nx, ny):
-                    self._set_pos(nx, ny)
-                    offset = (TILE_SIZE * dp[0], TILE_SIZE * dp[1])
-                    self._set_angle(angle)
-                    return [MoveViewportEvent(offset)]
+                if (self._angle == angle and
+                        self._last_key_down > self._key_rate):
+                    nx, ny = x + dp[0], y + dp[1]
+                    if self._can_move(nx, ny):
+                        self._set_pos(nx, ny)
+                        offset = (TILE_SIZE * dp[0], TILE_SIZE * dp[1])
+                        self._set_angle(angle, dp)
+                        self._last_key_down = 0
+                        return [MoveViewportEvent(offset)]
+                else:
+                    # just turn
+                    self._set_angle(angle, dp)
 
     def _activate_key(self):
         x, y = self._player_pos
 
     def _activate_key(self):
         x, y = self._player_pos
@@ -183,13 +387,27 @@ class GameLevelScene(BaseLevelScene):
                 self._mode = 'walk'
         elif self._level.is_keypad(x, y):
             self._level.press_keypad(x, y, self._roaches)
                 self._mode = 'walk'
         elif self._level.is_keypad(x, y):
             self._level.press_keypad(x, y, self._roaches)
-        elif self._level.is_on_friend(x, y):
-            friend = self._level.remove_friend(x, y)
+        elif self._level.friend_at(x, y):
+            friend = self._level.friend_at(x, y)
+            self._level.remove_friend(friend)
             self._friends.remove(friend)
             self._add_roach()
             self._friends.remove(friend)
             self._add_roach()
+        elif self._level.item_at(x, y):
+            item = self._level.item_at(x, y)
+            if item.item_type != "vehicle":
+                self._level.remove_item(item)
+                self._items.remove(item)
+                self._gain_item(item)
+        elif self._level.is_exit(x, y):
+            next_level = self._level.get_exit_level()
+            return [
+                WorldEvent('set', {'level.name': next_level}),
+                ChangeSceneEvent(GameLevelScene())]
 
 
-    def _fire_key(self):
-        print('Boom')
+    def _fire_key(self, dt):
+        if self._last_key_down > self._key_rate:
+            self._last_key_down = 0
+            self._fire_weapon()
 
     def _vehicle_management_key(self):
         from .roach_management import RoachesScene
 
     def _vehicle_management_key(self):
         from .roach_management import RoachesScene
@@ -198,13 +416,28 @@ class GameLevelScene(BaseLevelScene):
     def on_key_down(self, key, mod, unicode):
         x, y = self._player_pos
         if key in (keys.DOWN, keys.UP, keys.LEFT, keys.RIGHT):
     def on_key_down(self, key, mod, unicode):
         x, y = self._player_pos
         if key in (keys.DOWN, keys.UP, keys.LEFT, keys.RIGHT):
-            return self._movement_key(key)
+            firing = (keys.X in self._held_keys)
+            self._held_keys.clear()
+            self._held_keys.add(key)
+            if firing:
+                self._held_keys.add(keys.X)
+            # We do this so pressing the key has an instant effect, and can
+            # then be held
+            self._last_key_down = self._key_rate + 0.01
+            return self._movement_key(key, 0.01)
         elif key == keys.C:
             return self._activate_key()
         elif key == keys.X:
         elif key == keys.C:
             return self._activate_key()
         elif key == keys.X:
-            return self._fire_key()
+            self._held_keys.clear()
+            self._held_keys.add(key)
+            self._last_key_down = self._key_rate + 0.01
+            return self._fire_key(0.01)
         elif key == keys.V:
             return self._change_vehicle()
         elif key == keys.Z:
             return self._vehicle_management_key()
         return super(GameLevelScene, self).on_key_down(key, mod, unicode)
         elif key == keys.V:
             return self._change_vehicle()
         elif key == keys.Z:
             return self._vehicle_management_key()
         return super(GameLevelScene, self).on_key_down(key, mod, unicode)
+
+    def on_key_up(self, key, mode):
+        self._held_keys.discard(key)
+        self._last_key_down = 0