Mercurial > touhou
view pytouhou/game/enemy.py @ 357:26f78fc7acea
Fix Koakuma
author | Thibaut Girka <thib@sitedethib.com> |
---|---|
date | Sun, 08 Jul 2012 01:20:47 +0200 |
parents | 89ee09453906 |
children | 488c094ed51d |
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.timeout = -1 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.process.switch_to_sub(self.death_callback) self.timeout_callback = -1 self.death_callback = -1 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