Mercurial > touhou
view pytouhou/game/enemy.pyx @ 792:11bc22bad1bf
python: Replace the image crate with png
We weren’t using any of its features anyway, so the png crate is exactly what
we need, without the many heavy dependencies of image.
https://github.com/image-rs/image-png/pull/670 will eventually make it even
faster to build.
| author | Link Mauve <linkmauve@linkmauve.fr> |
|---|---|
| date | Sat, 17 Jan 2026 22:22:25 +0100 |
| parents | a6af3ff86612 |
| children |
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 libc.math cimport cos, sin, atan2, M_PI as pi from pytouhou.vm import ANMRunner from pytouhou.game.sprite import Sprite from pytouhou.game.bullet cimport Bullet, LAUNCHED from pytouhou.game.laser cimport Laser, PlayerLaser from pytouhou.game.effect cimport Effect cdef class Callback: def __init__(self, function=None, args=()): self.function = function self.args = args def __nonzero__(self): return self.function is not None cpdef enable(self, function, tuple args): self.function = function self.args = args cpdef disable(self): self.function = None cpdef fire(self): if self.function is not None: self.function(*self.args) self.function = None cdef class Enemy(Element): def __init__(self, pos, long life, long _type, long bonus_dropped, long die_score, anms, Game 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.low_life_trigger = -1 self.timeout = -1 self.remaining_lives = 0 self.death_callback = Callback() self.boss_callback = Callback() self.low_life_callback = Callback() self.timeout_callback = Callback() 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, unsigned long bullets_per_shot, unsigned long number_of_shots, double speed, double speed2, launch_angle, angle, flags): cdef double speed_a, speed_b cdef long nb_a, nb_b, shots_a, shots_b # 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 += <long>(nb_a * (1. - diff_coeff) + nb_b * diff_coeff) number_of_shots += <long>(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 * (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, tuple launch_pos=None): cdef unsigned long type_, bullets_per_shot, number_of_shots cdef double speed, speed2, launch_angle, angle (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_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 cpdef new_laser(self, unsigned long variant, laser_type, sprite_idx_offset, double angle, speed, start_offset, end_offset, max_length, width, start_duration, duration, end_duration, grazing_delay, grazing_extra_duration, unknown, tuple offset=None): cdef double ox, oy if offset is None: offset = self.bullet_launch_offset ox, oy = offset launch_pos = self.x + ox, self.y + oy if variant == 86: player = self.select_player() angle += self.get_angle(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 Player select_player(self, list players=None): if players is None: players = self._game.players return min(players, key=self.select_player_key) cpdef double get_angle(self, Element target, tuple pos=None) except 42: cdef double x, y x, y = pos or (self.x, self.y) return atan2(target.y - y, target.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 bint die_anim(self) except True: 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 bint drop_particles(self, long number, long color) except True: 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. 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, double x, double y, double z): self.x, self.y = x, y self.update_mode = 1 self.interpolator = Interpolator((x, y), self._game.frame) cpdef move_to(self, unsigned long duration, double x, double y, double 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, unsigned long 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 set_boss(self, bint enable): if enable: self.boss = True self._game.boss = self self._game.interface.set_boss_life() else: self.boss = False self._game.boss = None cdef bint is_visible(self, long screen_width, long screen_height) except -1: if self.sprite is not None: if self.sprite.corner_relative_placement: raise Exception #TODO tw, th = self.sprite._texcoords[2], self.sprite._texcoords[3] 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 bint check_collisions(self) except True: cdef Bullet bullet cdef Player player cdef PlayerLaser laser cdef long damages cdef double half_size[2] cdef double 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] 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].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 nb_players = len(self._game.players) if nb_players > 1: if damages <= nb_players: damages = 1 if damages else 0 else: damages //= nb_players # Apply damages self.life -= damages cdef bint handle_callbacks(self) except True: #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.disable() 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].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 False 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: self.death_callback.fire() elif self.life <= self.low_life_trigger and self.low_life_callback: self.low_life_callback.fire() self.low_life_trigger = -1 self.timeout_callback.disable() 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: self.timeout_callback.fire() #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: self.life = 0 #TODO: do this next frame? Bypass self.touchable? else: raise Exception('What the hell, man!') cdef bint update(self) except True: cdef double x, y, speed if self.process is not None: 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 def select_player_key(self, player): return ((player.x - self.x) ** 2 + (player.y - self.y) ** 2, player.character)
