changeset 441:e8dc95a2a287

Make pytouhou.game.enemy an extension type.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Sat, 10 Aug 2013 19:59:17 +0200
parents b9d2db93972f
children 6b4c3e250bd6
files pytouhou/game/enemy.pxd pytouhou/game/enemy.py pytouhou/game/enemy.pyx pytouhou/vm/eclrunner.py
diffstat 4 files changed, 576 insertions(+), 527 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pytouhou/game/enemy.pxd	Sat Aug 10 19:59:17 2013 +0200
@@ -0,0 +1,40 @@
+from pytouhou.game.element cimport Element
+from pytouhou.utils.interpolator cimport Interpolator
+
+cdef class Enemy(Element):
+    cdef public double z, angle, speed, rotation_speed, acceleration
+    cdef public long _type, bonus_dropped, die_score, frame, life, death_flags, current_laser_id, death_callback, boss_callback, low_life_callback, low_life_trigger, timeout, timeout_callback, remaining_lives, bullet_launch_interval, bullet_launch_timer, death_anim, direction, update_mode
+    cdef public bint visible, was_visible, touchable, collidable, damageable, boss, automatic_orientation, delay_attack
+    cdef public tuple difficulty_coeffs, extended_bullet_attributes, bullet_attributes, bullet_launch_offset, movement_dependant_sprites, screen_box
+    cdef public dict laser_by_id
+    cdef public list aux_anm
+    cdef public Interpolator interpolator, speed_interpolator
+    cdef public object _game, _anms, process
+
+    cdef double[2] hitbox_half_size
+
+    cpdef play_sound(self, index)
+    cpdef set_hitbox(self, double width, double height)
+    cpdef set_bullet_attributes(self, type_, anim, sprite_idx_offset,
+                                bullets_per_shot, number_of_shots, speed, speed2,
+                                launch_angle, angle, flags)
+    cpdef set_bullet_launch_interval(self, long value, unsigned long start=*)
+    cpdef fire(self, offset=*, bullet_attributes=*, launch_pos=*)
+    cpdef 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=*)
+    cpdef select_player(self, players=*)
+    cpdef get_player_angle(self, player=*, pos=*)
+    cpdef set_anim(self, index)
+    cdef void die_anim(self)
+    cdef void drop_particles(self, long number, long color)
+    cpdef set_aux_anm(self, long number, long index)
+    cpdef set_pos(self, x, y, z)
+    cpdef move_to(self, duration, x, y, z, formula)
+    cpdef stop_in(self, duration, formula)
+    cpdef bint is_visible(self, long screen_width, long screen_height)
+    cdef void check_collisions(self)
+    cdef void handle_callbacks(self)
+    cpdef update(self)
--- a/pytouhou/game/enemy.py	Fri Aug 30 14:16:08 2013 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,525 +0,0 @@
-# -*- 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.element import Element
-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(Element):
-    def __init__(self, pos, life, _type, bonus_dropped, die_score, anms, game):
-        Element.__init__(self)
-
-        self._game = game
-        self._anms = anms
-        self._type = _type
-
-        self.process = None
-        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]
-
-
-    @property
-    def objects(self):
-        return [self] + [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.sfx_player.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):
-        entry = 0 if index in self._anms[0].scripts else 1
-        self.sprite = Sprite()
-        self.anmrunner = ANMRunner(self._anms[entry], index, self.sprite)
-
-
-    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)
-        self._game.sfx_player.play('enep00.wav')
-
-
-    def drop_particles(self, number, color):
-        if color == 0:
-            if self._game.stage in [1, 2, 7]:
-                color = 3
-        color += 9
-        for i in range(number):
-            self._game.new_particle((self.x, self.y), color, 256) #TODO: find the real size.
-
-
-    def set_aux_anm(self, number, index):
-        entry = 0 if index in self._anms[0].scripts else 1
-        self.aux_anm[number] = Effect((self.x, self.y), index, self._anms[entry])
-
-
-    def set_pos(self, x, y, z):
-        self.x, self.y = x, y
-        self.update_mode = 1
-        self.interpolator = Interpolator((x, y), self._game.frame)
-
-
-    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), frame,
-                                         (x, y), frame + duration - 1,
-                                         formula)
-
-        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,), frame,
-                                               (0.,), frame + duration - 1,
-                                               formula)
-
-
-    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
-            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()
-                damages += bullet.damage
-                self._game.sfx_player.play('damage00.wav')
-
-        # Check for enemy-laser collisions
-        for laser in self._game.players_lasers:
-            if not laser:
-                continue
-
-            half_size = laser.hitbox
-            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):
-                damages += laser.damage
-                self._game.sfx_player.play('damage00.wav')
-                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.sht.hitbox
-                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.damageable:
-            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
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pytouhou/game/enemy.pyx	Sat Aug 10 19:59:17 2013 +0200
@@ -0,0 +1,535 @@
+# -*- 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 libc.math cimport cos, sin, atan2, M_PI as pi
+
+from pytouhou.vm.anmrunner import ANMRunner
+from pytouhou.game.sprite import Sprite
+from pytouhou.game.bullet import Bullet, LAUNCHED
+from pytouhou.game.laser import Laser
+from pytouhou.game.effect import Effect
+
+
+cdef class Enemy(Element):
+    def __init__(self, pos, long life, long _type, long bonus_dropped, long die_score, anms, game):
+        Element.__init__(self)
+
+        self._game = game
+        self._anms = anms
+        self._type = _type
+
+        self.process = None
+        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 = -1
+        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 = 0
+        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_half_size[:] = [0, 0]
+        self.screen_box = None
+
+        self.aux_anm = 8 * [None]
+
+
+    property objects:
+        def __get__(self):
+            return [self] + [anm for anm in self.aux_anm if anm is not None]
+
+
+    cpdef 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.sfx_player.play('%s.wav' % name)
+
+
+    cpdef set_hitbox(self, double width, double height):
+        self.hitbox_half_size[:] = [width / 2, height / 2]
+
+
+    cpdef 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()
+
+
+    cpdef set_bullet_launch_interval(self, long value, unsigned long start=0):
+        # Apply difficulty-specific modifiers:
+        #TODO: check every value possible! Look around 102h.exe@0x408720
+        value -= value * (<long>self._game.difficulty - 16) // 80
+
+        self.bullet_launch_interval = value
+        self.bullet_launch_timer = start % value if value > 0 else 0
+
+
+    cpdef 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 launch_pos is None:
+            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 xrange(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 xrange(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
+
+
+    cpdef 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
+
+
+    cpdef select_player(self, players=None):
+        return (players or self._game.players)[0] #TODO
+
+
+    cpdef get_player_angle(self, player=None, pos=None):
+        player_state = (player or self.select_player()).state
+        x, y = pos or (self.x, self.y)
+        return atan2(player_state.y - y, player_state.x - x)
+
+
+    cpdef set_anim(self, index):
+        entry = 0 if index in self._anms[0].scripts else 1
+        self.sprite = Sprite()
+        self.anmrunner = ANMRunner(self._anms[entry], index, self.sprite)
+
+
+    cdef void 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)
+        self._game.sfx_player.play('enep00.wav')
+
+
+    cdef void drop_particles(self, long number, long color):
+        if color == 0:
+            if self._game.stage in [1, 2, 7]:
+                color = 3
+        color += 9
+        for i in xrange(number):
+            self._game.new_particle((self.x, self.y), color, 256) #TODO: find the real size.
+
+
+    cpdef set_aux_anm(self, long number, long index):
+        entry = 0 if index in self._anms[0].scripts else 1
+        self.aux_anm[number] = Effect((self.x, self.y), index, self._anms[entry])
+
+
+    cpdef set_pos(self, x, y, z):
+        self.x, self.y = x, y
+        self.update_mode = 1
+        self.interpolator = Interpolator((x, y), self._game.frame)
+
+
+    cpdef 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), frame,
+                                         (x, y), frame + duration - 1,
+                                         formula)
+
+        self.angle = atan2(y - self.y, x - self.x)
+
+
+    cpdef stop_in(self, duration, formula):
+        frame = self._game.frame
+        self.interpolator = None
+        self.update_mode = 1
+        self.speed_interpolator = Interpolator((self.speed,), frame,
+                                               (0.,), frame + duration - 1,
+                                               formula)
+
+
+    cpdef bint is_visible(self, long screen_width, long screen_height):
+        cdef double tw, th
+
+        if self.sprite is not None:
+            if self.sprite.corner_relative_placement:
+                raise Exception #TODO
+            _, _, tw, th = self.sprite.texcoords
+        else:
+            tw, th = 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
+
+
+    cdef void check_collisions(self):
+        cdef long damages
+        cdef double half_size[2], bx, by, lx, ly, px, py, phalf_size
+
+        # Check for collisions
+        ex, ey = self.x, self.y
+        ehalf_size_x = self.hitbox_half_size[0]
+        ehalf_size_y = self.hitbox_half_size[1]
+        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[0] = bullet.hitbox[0]
+            half_size[1] = bullet.hitbox[1]
+            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()
+                damages += bullet.damage
+                self._game.sfx_player.play('damage00.wav')
+
+        # Check for enemy-laser collisions
+        for laser in self._game.players_lasers:
+            if not laser:
+                continue
+
+            half_size[0] = laser.hitbox[0]
+            half_size[1] = laser.hitbox[1]
+            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):
+                damages += laser.damage
+                self._game.sfx_player.play('damage00.wav')
+                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.state.x, player.state.y
+                phalf_size = player.sht.hitbox
+                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.damageable:
+            if self._game.spellcard is not None:
+                #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
+
+
+    cdef void 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!')
+
+
+    cpdef update(self):
+        cdef double x, y, speed
+
+        if self.process:
+            self.process.run_iteration()
+
+        x, y = self.x, self.y
+
+        if self.update_mode == 1:
+            speed = 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 is not None:
+            #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 != 0:
+                self.set_anim({-1: end_left, +1: end_right}[self.direction])
+                self.direction = 0
+
+
+        if self.screen_box is not None:
+            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 is not None and not self.anmrunner.run_frame():
+            self.anmrunner = None
+
+        if self.sprite is not None 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 is not None:
+                anm.x, anm.y = self.x, self.y
+                anm.update()
+
+        self.handle_callbacks()
+
+        self.frame += 1
+
--- a/pytouhou/vm/eclrunner.py	Fri Aug 30 14:16:08 2013 +0200
+++ b/pytouhou/vm/eclrunner.py	Sat Aug 10 19:59:17 2013 +0200
@@ -815,8 +815,7 @@
 
     @instruction(103)
     def set_hitbox(self, width, height, depth):
-        self._enemy.hitbox = (width, height)
-        self._enemy.hitbox_half_size = (width / 2., height / 2.)
+        self._enemy.set_hitbox(width, height)
 
 
     @instruction(104)