view pytouhou/game/enemy.py @ 370:74471afbac37

Add a programmable pipeline renderer, and a --fixed-pipeline switch to use the old one.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Fri, 27 Jul 2012 18:43:48 +0200
parents 7dc012f631dc
children e0e284fcb288
line wrap: on
line source

# -*- encoding: utf-8 -*-
##
## Copyright (C) 2011 Thibaut Girka <thib@sitedethib.com>
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
## by the Free Software Foundation; version 3 only.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##


from pytouhou.utils.interpolator import Interpolator
from pytouhou.vm.anmrunner import ANMRunner
from pytouhou.game.sprite import Sprite
from pytouhou.game.bullet import Bullet
from pytouhou.game.laser import Laser
from pytouhou.game.effect import Effect
from math import cos, sin, atan2, pi
from pytouhou.game.bullet import LAUNCHED


class Enemy(object):
    def __init__(self, pos, life, _type, bonus_dropped, die_score, anm_wrapper, game):
        self._game = game
        self._anm_wrapper = anm_wrapper
        self._type = _type

        self.process = None
        self.sprite = None
        self.anmrunner = None
        self.removed = False
        self.visible = True
        self.was_visible = False
        self.bonus_dropped = bonus_dropped
        self.die_score = die_score

        self.frame = 0

        self.x, self.y, self.z = pos
        self.life = 1 if life < 0 else life
        self.touchable = True
        self.collidable = True
        self.damageable = True
        self.death_flags = 0
        self.boss = False
        self.difficulty_coeffs = (-.5, .5, 0, 0, 0, 0)
        self.extended_bullet_attributes = (0, 0, 0, 0, 0., 0., 0., 0.)
        self.current_laser_id = 0
        self.laser_by_id = {}
        self.bullet_attributes = None
        self.bullet_launch_offset = (0, 0)
        self.death_callback = -1
        self.boss_callback = -1
        self.low_life_callback = -1
        self.low_life_trigger = None
        self.timeout = -1
        self.timeout_callback = -1
        self.remaining_lives = 0

        self.automatic_orientation = False

        self.bullet_launch_interval = 0
        self.bullet_launch_timer = 0
        self.delay_attack = False

        self.death_anim = 0
        self.movement_dependant_sprites = None
        self.direction = None
        self.interpolator = None #TODO
        self.speed_interpolator = None
        self.update_mode = 0
        self.angle = 0.
        self.speed = 0.
        self.rotation_speed = 0.
        self.acceleration = 0.

        self.hitbox = (0, 0)
        self.hitbox_half_size = (0, 0)
        self.screen_box = None

        self.aux_anm = 8 * [None]


    def objects(self):
        return [anm for anm in self.aux_anm if anm]


    def play_sound(self, index):
        name = {
            5: 'power0',
            6: 'power1',
            7: 'tan00',
            8: 'tan01',
            9: 'tan02',
            14: 'cat00',
            16: 'lazer00',
            17: 'lazer01',
            18: 'enep01',
            22: 'tan00', #XXX
            24: 'tan02', #XXX
            25: 'kira00',
            26: 'kira01',
            27: 'kira02'
        }[index]
        self._game.enemy_sfx.play('%s.wav' % name)


    def set_bullet_attributes(self, type_, anim, sprite_idx_offset,
                              bullets_per_shot, number_of_shots, speed, speed2,
                              launch_angle, angle, flags):

        # Apply difficulty-specific modifiers
        speed_a, speed_b, nb_a, nb_b, shots_a, shots_b = self.difficulty_coeffs
        diff_coeff = self._game.difficulty / 32.

        speed += speed_a * (1. - diff_coeff) + speed_b * diff_coeff
        speed2 += (speed_a * (1. - diff_coeff) + speed_b * diff_coeff) / 2.
        bullets_per_shot += int(nb_a * (1. - diff_coeff) + nb_b * diff_coeff)
        number_of_shots += int(shots_a * (1. - diff_coeff) + shots_b * diff_coeff)

        self.bullet_attributes = (type_, anim, sprite_idx_offset, bullets_per_shot,
                                  number_of_shots, speed, speed2, launch_angle,
                                  angle, flags)
        if not self.delay_attack:
            self.fire()


    def set_bullet_launch_interval(self, value, start=0):
        # Apply difficulty-specific modifiers:
        #TODO: check every value possible! Look around 102h.exe@0x408720
        value -= value * (self._game.difficulty - 16) // 80

        self.bullet_launch_interval = value
        self.bullet_launch_timer = start % value if value else 0


    def fire(self, offset=None, bullet_attributes=None, launch_pos=None):
        (type_, type_idx, sprite_idx_offset, bullets_per_shot, number_of_shots,
         speed, speed2, launch_angle, angle, flags) = bullet_attributes or self.bullet_attributes

        bullet_type = self._game.bullet_types[type_idx]

        if not launch_pos:
            ox, oy = offset or self.bullet_launch_offset
            launch_pos = self.x + ox, self.y + oy

        if speed < 0.3 and speed != 0.0:
            speed = 0.3
        if speed2 < 0.3:
            speed2 = 0.3

        self.bullet_launch_timer = 0

        player = self.select_player()

        if type_ in (67, 69, 71):
            launch_angle += self.get_player_angle(player, launch_pos)
        if type_ == 71 and bullets_per_shot % 2 or type_ in (69, 70) and not bullets_per_shot % 2:
            launch_angle += pi / bullets_per_shot
        if type_ != 75:
            launch_angle -= angle * (bullets_per_shot - 1) / 2.

        bullets = self._game.bullets
        nb_bullets_max = self._game.nb_bullets_max

        for shot_nb in range(number_of_shots):
            shot_speed = speed if shot_nb == 0 else speed + (speed2 - speed) * float(shot_nb) / float(number_of_shots)
            bullet_angle = launch_angle
            if type_ in (69, 70, 71, 74):
                launch_angle += angle
            for bullet_nb in range(bullets_per_shot):
                if nb_bullets_max is not None and len(bullets) == nb_bullets_max:
                    break

                if type_ == 75: # 102h.exe@0x4138cf
                    bullet_angle = self._game.prng.rand_double() * (launch_angle - angle) + angle
                if type_ in (74, 75): # 102h.exe@0x4138cf
                    shot_speed = self._game.prng.rand_double() * (speed - speed2) + speed2
                bullets.append(Bullet(launch_pos, bullet_type, sprite_idx_offset,
                                      bullet_angle, shot_speed,
                                      self.extended_bullet_attributes,
                                      flags, player, self._game))

                if type_ in (69, 70, 71, 74):
                    bullet_angle += 2. * pi / bullets_per_shot
                else:
                    bullet_angle += angle


    def new_laser(self, variant, laser_type, sprite_idx_offset, angle, speed,
                  start_offset, end_offset, max_length, width,
                  start_duration, duration, end_duration,
                  grazing_delay, grazing_extra_duration, unknown,
                  offset=None):
        ox, oy = offset or self.bullet_launch_offset
        launch_pos = self.x + ox, self.y + oy
        if variant == 86:
            angle += self.get_player_angle(self.select_player(), launch_pos)
        laser = Laser(launch_pos, self._game.laser_types[laser_type],
                      sprite_idx_offset, angle, speed,
                      start_offset, end_offset, max_length, width,
                      start_duration, duration, end_duration, grazing_delay,
                      grazing_extra_duration, self._game)
        self._game.lasers.append(laser)
        self.laser_by_id[self.current_laser_id] = laser


    def select_player(self, players=None):
        return (players or self._game.players)[0] #TODO


    def get_player_angle(self, player=None, pos=None):
        player = player or self.select_player()
        x, y = pos or (self.x, self.y)
        return atan2(player.y - y, player.x - x)


    def set_anim(self, index):
        self.sprite = Sprite()
        self.anmrunner = ANMRunner(self._anm_wrapper, index, self.sprite)
        self.anmrunner.run_frame()


    def die_anim(self):
        anim = {0: 3, 1: 4, 2: 5}[self.death_anim % 256] # The TB is wanted, if index isn’t in these values the original game crashs.
        self._game.new_effect((self.x, self.y), anim)


    def drop_particles(self, number, color):
        if color == 0:
            if self._game.stage in [1, 2, 7]:
                color = 3
        for i in range(number):
            self._game.new_particle((self.x, self.y), color, 3., 256) #TODO: find the real size.


    def set_aux_anm(self, number, script):
        self.aux_anm[number] = Effect((self.x, self.y), script, self._anm_wrapper)


    def set_pos(self, x, y, z):
        self.x, self.y = x, y
        self.update_mode = 1
        self.interpolator = Interpolator((x, y))
        self.interpolator.set_interpolation_start(self._game.frame, (x, y))


    def move_to(self, duration, x, y, z, formula):
        frame = self._game.frame
        self.speed_interpolator = None
        self.update_mode = 1
        self.interpolator = Interpolator((self.x, self.y), formula)
        self.interpolator.set_interpolation_start(frame, (self.x, self.y))
        self.interpolator.set_interpolation_end(frame + duration - 1, (x, y))

        self.angle = atan2(y - self.y, x - self.x)


    def stop_in(self, duration, formula):
        frame = self._game.frame
        self.interpolator = None
        self.update_mode = 1
        self.speed_interpolator = Interpolator((self.speed,), formula)
        self.speed_interpolator.set_interpolation_start(frame, (self.speed,))
        self.speed_interpolator.set_interpolation_end(frame + duration - 1, (0.,))


    def is_visible(self, screen_width, screen_height):
        if self.sprite:
            tx, ty, tw, th = self.sprite.texcoords
            if self.sprite.corner_relative_placement:
                raise Exception #TODO
        else:
            tx, ty, tw, th = 0., 0., 0., 0.

        x, y = self.x, self.y
        max_x = tw / 2.
        max_y = th / 2.

        if (max_x < x - screen_width
            or max_x < -x
            or max_y < y - screen_height
            or max_y < -y):
            return False
        return True


    def check_collisions(self):
        # Check for collisions
        ex, ey = self.x, self.y
        ehalf_size_x, ehalf_size_y = self.hitbox_half_size
        ex1, ex2 = ex - ehalf_size_x, ex + ehalf_size_x
        ey1, ey2 = ey - ehalf_size_y, ey + ehalf_size_y

        damages = 0

        # Check for enemy-bullet collisions
        for bullet in self._game.players_bullets:
            if bullet.state != LAUNCHED:
                continue
            half_size = bullet.hitbox_half_size
            bx, by = bullet.x, bullet.y
            bx1, bx2 = bx - half_size[0], bx + half_size[0]
            by1, by2 = by - half_size[1], by + half_size[1]

            if not (bx2 < ex1 or bx1 > ex2
                    or by2 < ey1 or by1 > ey2):
                bullet.collide()
                if self.damageable:
                    damages += bullet.damage

        # Check for enemy-laser collisions
        for laser in self._game.players_lasers:
            if not laser:
                continue

            half_size = laser.hitbox_half_size
            lx, ly = laser.x, laser.y * 2.
            lx1, lx2 = lx - half_size[0], lx + half_size[0]

            if not (lx2 < ex1 or lx1 > ex2
                    or ly < ey1):
                if self.damageable:
                    damages += laser.damage
                self.drop_particles(1, 1) #TODO: don’t call each frame.

        # Check for enemy-player collisions
        ex1, ex2 = ex - ehalf_size_x * 2. / 3., ex + ehalf_size_x * 2. / 3.
        ey1, ey2 = ey - ehalf_size_y * 2. / 3., ey + ehalf_size_y * 2. / 3.
        if self.collidable:
            for player in self._game.players:
                px, py = player.x, player.y
                phalf_size = player.hitbox_half_size
                px1, px2 = px - phalf_size, px + phalf_size
                py1, py2 = py - phalf_size, py + phalf_size

                #TODO: box-box or point-in-box?
                if not (ex2 < px1 or ex1 > px2 or ey2 < py1 or ey1 > py2):
                    if not self.boss:
                        damages += 10
                    player.collide()

        # Adjust damages
        damages = min(70, damages)
        score = (damages // 5) * 10
        self._game.players[0].state.score += score #TODO: better distribution amongst the players.

        if self._game.spellcard:
            #TODO: there is a division by 3, somewhere... where is it?
            if damages <= 7:
                damages = 1 if damages else 0
            else:
                damages //= 7

        # Apply damages
        self.life -= damages


    def handle_callbacks(self):
        #TODO: implement missing callbacks and clean up!
        if self.life <= 0 and self.touchable:
            self.timeout = -1 #TODO: not really true, the timeout is frozen
            self.timeout_callback = -1
            death_flags = self.death_flags & 7

            self.die_anim()

            #TODO: verify if the score is added with all the different flags.
            self._game.players[0].state.score += self.die_score #TODO: better distribution amongst the players.

            #TODO: verify if that should really be there.
            if self.boss:
                self._game.change_bullets_into_bonus()

            if death_flags < 4:
                if self.bonus_dropped > -1:
                    self.drop_particles(7, 0)
                    self._game.drop_bonus(self.x, self.y, self.bonus_dropped)
                elif self.bonus_dropped == -1:
                    if self._game.deaths_count % 3 == 0:
                        self.drop_particles(10, 0)
                        self._game.drop_bonus(self.x, self.y, self._game.bonus_list[self._game.next_bonus])
                        self._game.next_bonus = (self._game.next_bonus + 1) % 32
                    else:
                        self.drop_particles(4, 0)
                    self._game.deaths_count += 1
                else:
                    self.drop_particles(4, 0)

                if death_flags == 0:
                    self.removed = True
                    return

                if death_flags == 1:
                    if self.boss:
                        self.boss = False #TODO: really?
                        self._game.boss = None
                    self.touchable = False
                elif death_flags == 2:
                    pass # Just that?
                elif death_flags == 3:
                    if self.boss:
                        self.boss = False #TODO: really?
                        self._game.boss = None
                    self.damageable = False
                    self.life = 1
                    self.death_flags = 0

            if death_flags != 0 and self.death_callback > -1:
                self.process.switch_to_sub(self.death_callback)
                self.death_callback = -1
        elif self.life <= self.low_life_trigger and self.low_life_callback > -1:
            self.process.switch_to_sub(self.low_life_callback)
            self.low_life_callback = -1
            self.low_life_trigger = -1
            self.timeout_callback = -1
        elif self.timeout != -1 and self.frame == self.timeout:
            self.frame = 0
            self.timeout = -1
            self._game.kill_enemies()
            self._game.cancel_bullets()

            if self.low_life_trigger > 0:
                self.life = self.low_life_trigger
                self.low_life_trigger = -1

            if self.timeout_callback > -1:
                self.process.switch_to_sub(self.timeout_callback)
                self.timeout_callback = -1
            #TODO: this is only done under certain (unknown) conditions!
            # but it shouldn't hurt anyway, as the only option left is to crash!
            elif self.death_callback > -1:
                self.life = 0 #TODO: do this next frame? Bypass self.touchable?
            else:
                raise Exception('What the hell, man!')


    def update(self):
        if self.process:
            self.process.run_iteration()

        x, y = self.x, self.y

        if self.update_mode == 1:
            speed = 0.0
            if self.interpolator:
                self.interpolator.update(self._game.frame)
                x, y = self.interpolator.values
            if self.speed_interpolator:
                self.speed_interpolator.update(self._game.frame)
                speed, = self.speed_interpolator.values
        else:
            speed = self.speed
            self.speed += self.acceleration
            self.angle += self.rotation_speed

        dx, dy = cos(self.angle) * speed, sin(self.angle) * speed
        if self._type & 2:
            x -= dx
        else:
            x += dx
        y += dy

        if self.movement_dependant_sprites:
            #TODO: is that really how it works? Almost.
            # Sprite determination is done only once per changement, and is
            # superseeded by ins_97.
            end_left, end_right, left, right = self.movement_dependant_sprites
            if x < self.x and self.direction != -1:
                self.set_anim(left)
                self.direction = -1
            elif x > self.x and self.direction != +1:
                self.set_anim(right)
                self.direction = +1
            elif x == self.x and self.direction is not None:
                self.set_anim({-1: end_left, +1: end_right}[self.direction])
                self.direction = None


        if self.screen_box:
            xmin, ymin, xmax, ymax = self.screen_box
            x = max(xmin, min(x, xmax))
            y = max(ymin, min(y, ymax))


        self.x, self.y = x, y

        #TODO
        if self.anmrunner and not self.anmrunner.run_frame():
            self.anmrunner = None

        if self.sprite and self.visible:
            if self.sprite.removed:
                self.sprite = None
            else:
                self.sprite.update_orientation(self.angle,
                                               self.automatic_orientation)


        if self.bullet_launch_interval != 0:
            self.bullet_launch_timer += 1
            if self.bullet_launch_timer == self.bullet_launch_interval:
                self.fire()

        # Check collisions
        if self.touchable:
            self.check_collisions()

        for anm in self.aux_anm:
            if anm:
                anm.x, anm.y = self.x, self.y
                anm.update()

        self.handle_callbacks()

        self.frame += 1