The enemies are a threat (sort of)
[koperkapel.git] / koperkapel / scenes / level.py
index a800ba0d842d0f5c3d130b16acbef92f6c0aa958..54041baa7cc20e863e0c0457ba8a9cbe70a77b47 100644 (file)
@@ -1,17 +1,45 @@
 """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
+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 ..roaches import default_roaches
+from ..gamelib.items import clone_old_item, create_new_item
+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):
     """ Level scene. """
 
 
 
 class BaseLevelScene(Scene):
     """ Level scene. """
 
+    def __init__(self):
+        super().__init__()
+        self._level = None
+        self._stats = None
+
     def enter(self, world):
         self._level = levels.load(world.level.name)
         self._tiles = self._level.tiles
     def enter(self, world):
         self._level = levels.load(world.level.name)
         self._tiles = self._level.tiles
@@ -21,6 +49,14 @@ class BaseLevelScene(Scene):
         for layer in ['floor', 'tunnels']:
             self._surfaces[layer] = self._render(layer)
         self._overlay = self._surfaces['floor'].copy()
         for layer in ['floor', 'tunnels']:
             self._surfaces[layer] = self._render(layer)
         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)
+        for keypad in self._level.keypads:
+            self._keypads.add(keypad)
 
     def _render(self, layer):
         # We cache the rendered surface to avoid doing a large number
 
     def _render(self, layer):
         # We cache the rendered surface to avoid doing a large number
@@ -38,17 +74,29 @@ class BaseLevelScene(Scene):
                 surface.blit(tile[layer_key], pos)
         return surface.convert_alpha()
 
                 surface.blit(tile[layer_key], pos)
         return surface.convert_alpha()
 
-    def draw(self, screen, viewport):
+    def update(self, world, engine, dt):
+        """Fix the door and keypad positions"""
+        super().update(world, engine, dt)
+        for door in self._doors:
+            door.pos = self.calc_offset(
+                door.game_pos[0] * TILE_SIZE, door.game_pos[1] * TILE_SIZE)
+        for keypad in self._keypads:
+            keypad.pos = self.calc_offset(
+                keypad.game_pos[0] * TILE_SIZE, keypad.game_pos[1] * TILE_SIZE)
+
+    def draw(self, screen):
         screen.clear()
         # Viewport is the position of the screen relative to the
         # surface. We need the position of the surface relative to
         # the screen for the blit, so this conversion
         screen.clear()
         # Viewport is the position of the screen relative to the
         # surface. We need the position of the surface relative to
         # the screen for the blit, so this conversion
+        viewport = self.viewport
         screen.surface.blit(self._surfaces[self._level_layer], (0, 0),
                             area=(viewport[0], viewport[1], WIDTH, HEIGHT))
         if self._level_layer != 'floor':
         screen.surface.blit(self._surfaces[self._level_layer], (0, 0),
                             area=(viewport[0], viewport[1], WIDTH, HEIGHT))
         if self._level_layer != 'floor':
-            screen.surface.blit(self._overlay, (0, 0),
-                         area=(viewport[0], viewport[1], WIDTH, HEIGHT), 
-                         special_flags=pgl.BLEND_MULT)
+            screen.surface.blit(
+                self._overlay, (0, 0),
+                area=(viewport[0], viewport[1], WIDTH, HEIGHT),
+                special_flags=pgl.BLEND_MULT)
         self.actors.draw(screen)
 
     def on_key_down(self, key, mod, unicode):
         self.actors.draw(screen)
 
     def on_key_down(self, key, mod, unicode):
@@ -60,51 +108,336 @@ 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:
+            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)
         super().enter(world)
         self._roaches = self.actors.add_layer("roaches", level=10)
-        self._init_roaches(world.roaches)
+        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._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)
 
 
-    def _init_roaches(self, roaches):
+    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
         x, y = self._level.start_pos
-        for roach in roaches:
-            roach_actor = self._roaches.add(default_roaches.assemble(roach))
-            roach_actor.anchor = (0, 0)
-            roach_actor.pos = (x * TILE_SIZE, y * TILE_SIZE)
-        self._set_pos(x, y)
         self._level_layer = 'floor'
         self._level_layer = 'floor'
+        self._avatar = self._vehicle.get_avatar(world)
+        self._set_pos(x, y)
+        self._avatar.pos = (WIDTH // 2, HEIGHT // 2)
+        self._roaches.add(self._avatar)
+        # Fix viewport offset
+        return [
+            MoveViewportEvent((
+                x * TILE_SIZE - WIDTH // 2,
+                y * TILE_SIZE - HEIGHT // 2))]
 
     def _set_pos(self, x, y):
         self._player_pos = (x, y)
 
     def _set_pos(self, x, y):
         self._player_pos = (x, y)
+        # print('At ', (x, y))
 
 
-    def on_key_down(self, key, mod, unicode):
-        offset = None
+    def _can_move(self, x, y):
+        if self._mode == 'walk':
+            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)
+
+    def _set_angle(self, angle, dp):
+        self._angle = angle
+        self._angle_dp = dp
+        self._avatar.angle = angle
+
+    @defer_to_update
+    def _vehicle_changed(self, world):
+        self._roaches.remove(self._avatar)
+        self._vehicle = Vehicle.current(world)
+        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._angle_dp)
+
+    @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):
+        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):
+        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)
+        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
+
+    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
         x, y = self._player_pos
-        if key == keys.DOWN:
-            if self._level.can_walk(x, y + 1, self._level_layer):
-                self._set_pos(x, y + 1)
-                offset = (0, TILE_SIZE)
-        elif key == keys.UP:
-            if self._level.can_walk(x, y - 1, self._level_layer):
-                self._set_pos(x, y - 1)
-                offset = (0, -TILE_SIZE)
-        elif key == keys.LEFT:
-            if self._level.can_walk(x - 1, y, self._level_layer):
-                self._set_pos(x - 1, y)
-                offset = (-TILE_SIZE, 0)
-        elif key == keys.RIGHT:
-            if self._level.can_walk(x + 1, y, self._level_layer):
-                self._set_pos(x + 1, y)
-                offset = (TILE_SIZE, 0)
-        elif key == keys.S:
-            if self._level_layer == 'floor':
-                if self._level.can_walk(x, y, 'tunnels'):
+        for k, dp, angle in (
+            (keys.DOWN, (0, 1), 180),
+            (keys.UP, (0, -1), 0),
+            (keys.LEFT, (-1, 0), 90),
+            (keys.RIGHT, (1, 0), -90),
+        ):
+            if key == k:
+                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
+        if self._level.is_grate(x, y):
+            if (self._level_layer == 'floor' and
+                    self._level.can_crawl(x, y, 'floor')):
+                if self._level.can_crawl(x, y, 'tunnels'):
                     self._level_layer = 'tunnels'
                     self._level_layer = 'tunnels'
-            elif self._level.can_walk(x, y, 'floor'):
+                    self._mode = 'crawl'
+            elif self._level.can_crawl(x, y, 'floor'):
                 # Must be in the tunnels already
                 self._level_layer = 'floor'
                 # Must be in the tunnels already
                 self._level_layer = 'floor'
-            return
-        if offset:
-            return [MoveViewportEvent(offset)]
+                self._mode = 'walk'
+        elif self._level.is_keypad(x, y):
+            self._level.press_keypad(x, y, self._roaches)
+        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()
+        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, 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
+        return [ChangeSceneEvent(RoachesScene(level_scene=self))]
+
+    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)
+            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:
+            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)
         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