smartness now required for keypads
[koperkapel.git] / koperkapel / scenes / level.py
index e26d11dbe5e1b8858be77d3d8ef8b2f2186afdba..ca85884b75a2ccb0e4cbd3aec0494faf5ee4fff1 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, WorldEvent, 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)
@@ -89,20 +113,23 @@ class GameLevelScene(BaseLevelScene):
             for generator in self._generators:
                 generator.unpause()
             return
             for generator in self._generators:
                 generator.unpause()
             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)
         self._items = self.actors.add_layer("items", level=9)
         self._generators = self.actors.add_layer("enemy generators", level=8)
         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=7)
+        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._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)
 
     def _init_items(self):
         return self._init_roaches(world)
 
     def _init_items(self):
@@ -115,8 +142,9 @@ class GameLevelScene(BaseLevelScene):
 
     def exit(self, world):
         for generator in self._generators:
 
     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)
+            # 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):
             generator.pause()
 
     def _init_generators(self):
@@ -143,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
@@ -160,20 +191,118 @@ 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:
         super().update(world, engine, dt)
         events = world.pop_events()
         for friend in self._friends:
@@ -182,11 +311,38 @@ class GameLevelScene(BaseLevelScene):
         for item in self._items:
             item.pos = self.calc_offset(
                 item.game_pos[0] * TILE_SIZE, item.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
 
         more = self._check_held_keys(dt)
         if more:
             events.extend(more)
         return events
 
+    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
     def _check_held_keys(self, dt):
         for key in self._held_keys:
             self._last_key_down += dt
@@ -204,17 +360,18 @@ class GameLevelScene(BaseLevelScene):
             (keys.RIGHT, (1, 0), -90),
         ):
             if key == k:
             (keys.RIGHT, (1, 0), -90),
         ):
             if key == k:
-                if self._angle == angle and self._last_key_down > self._key_rate:
+                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])
                     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)
+                        self._set_angle(angle, dp)
                         self._last_key_down = 0
                         return [MoveViewportEvent(offset)]
                 else:
                     # just turn
                         self._last_key_down = 0
                         return [MoveViewportEvent(offset)]
                 else:
                     # just turn
-                    self._set_angle(angle)
+                    self._set_angle(angle, dp)
 
     def _activate_key(self):
         x, y = self._player_pos
 
     def _activate_key(self):
         x, y = self._player_pos
@@ -229,7 +386,7 @@ class GameLevelScene(BaseLevelScene):
                 self._level_layer = 'floor'
                 self._mode = 'walk'
         elif self._level.is_keypad(x, y):
                 self._level_layer = 'floor'
                 self._mode = 'walk'
         elif self._level.is_keypad(x, y):
-            self._level.press_keypad(x, y, self._roaches)
+            self._level.press_keypad(x, y, self._stats.smart)
         elif self._level.friend_at(x, y):
             friend = self._level.friend_at(x, y)
             self._level.remove_friend(friend)
         elif self._level.friend_at(x, y):
             friend = self._level.friend_at(x, y)
             self._level.remove_friend(friend)
@@ -237,8 +394,10 @@ class GameLevelScene(BaseLevelScene):
             self._add_roach()
         elif self._level.item_at(x, y):
             item = self._level.item_at(x, y)
             self._add_roach()
         elif self._level.item_at(x, y):
             item = self._level.item_at(x, y)
-            self._level.remove_item(item)
-            self._items.remove(item)
+            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 [
         elif self._level.is_exit(x, y):
             next_level = self._level.get_exit_level()
             return [
@@ -248,7 +407,7 @@ class GameLevelScene(BaseLevelScene):
     def _fire_key(self, dt):
         if self._last_key_down > self._key_rate:
             self._last_key_down = 0
     def _fire_key(self, dt):
         if self._last_key_down > self._key_rate:
             self._last_key_down = 0
-            print('Boom')
+            self._fire_weapon()
 
     def _vehicle_management_key(self):
         from .roach_management import RoachesScene
 
     def _vehicle_management_key(self):
         from .roach_management import RoachesScene
@@ -257,9 +416,13 @@ 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):
+            firing = (keys.X in self._held_keys)
             self._held_keys.clear()
             self._held_keys.add(key)
             self._held_keys.clear()
             self._held_keys.add(key)
-            # We do this so pressing the key has an instant effect, and can then be held
+            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:
             self._last_key_down = self._key_rate + 0.01
             return self._movement_key(key, 0.01)
         elif key == keys.C: