comparison pytouhou/game/enemy.pyx @ 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 pytouhou/game/enemy.py@b9d2db93972f
children cae83b963695
comparison
equal deleted inserted replaced
440:b9d2db93972f 441:e8dc95a2a287
1 # -*- encoding: utf-8 -*-
2 ##
3 ## Copyright (C) 2011 Thibaut Girka <thib@sitedethib.com>
4 ##
5 ## This program is free software; you can redistribute it and/or modify
6 ## it under the terms of the GNU General Public License as published
7 ## by the Free Software Foundation; version 3 only.
8 ##
9 ## This program is distributed in the hope that it will be useful,
10 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
11 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 ## GNU General Public License for more details.
13 ##
14
15 from libc.math cimport cos, sin, atan2, M_PI as pi
16
17 from pytouhou.vm.anmrunner import ANMRunner
18 from pytouhou.game.sprite import Sprite
19 from pytouhou.game.bullet import Bullet, LAUNCHED
20 from pytouhou.game.laser import Laser
21 from pytouhou.game.effect import Effect
22
23
24 cdef class Enemy(Element):
25 def __init__(self, pos, long life, long _type, long bonus_dropped, long die_score, anms, game):
26 Element.__init__(self)
27
28 self._game = game
29 self._anms = anms
30 self._type = _type
31
32 self.process = None
33 self.visible = True
34 self.was_visible = False
35 self.bonus_dropped = bonus_dropped
36 self.die_score = die_score
37
38 self.frame = 0
39
40 self.x, self.y, self.z = pos
41 self.life = 1 if life < 0 else life
42 self.touchable = True
43 self.collidable = True
44 self.damageable = True
45 self.death_flags = 0
46 self.boss = False
47 self.difficulty_coeffs = (-.5, .5, 0, 0, 0, 0)
48 self.extended_bullet_attributes = (0, 0, 0, 0, 0., 0., 0., 0.)
49 self.current_laser_id = 0
50 self.laser_by_id = {}
51 self.bullet_attributes = None
52 self.bullet_launch_offset = (0, 0)
53 self.death_callback = -1
54 self.boss_callback = -1
55 self.low_life_callback = -1
56 self.low_life_trigger = -1
57 self.timeout = -1
58 self.timeout_callback = -1
59 self.remaining_lives = 0
60
61 self.automatic_orientation = False
62
63 self.bullet_launch_interval = 0
64 self.bullet_launch_timer = 0
65 self.delay_attack = False
66
67 self.death_anim = 0
68 self.movement_dependant_sprites = None
69 self.direction = 0
70 self.interpolator = None #TODO
71 self.speed_interpolator = None
72 self.update_mode = 0
73 self.angle = 0.
74 self.speed = 0.
75 self.rotation_speed = 0.
76 self.acceleration = 0.
77
78 self.hitbox_half_size[:] = [0, 0]
79 self.screen_box = None
80
81 self.aux_anm = 8 * [None]
82
83
84 property objects:
85 def __get__(self):
86 return [self] + [anm for anm in self.aux_anm if anm is not None]
87
88
89 cpdef play_sound(self, index):
90 name = {
91 5: 'power0',
92 6: 'power1',
93 7: 'tan00',
94 8: 'tan01',
95 9: 'tan02',
96 14: 'cat00',
97 16: 'lazer00',
98 17: 'lazer01',
99 18: 'enep01',
100 22: 'tan00', #XXX
101 24: 'tan02', #XXX
102 25: 'kira00',
103 26: 'kira01',
104 27: 'kira02'
105 }[index]
106 self._game.sfx_player.play('%s.wav' % name)
107
108
109 cpdef set_hitbox(self, double width, double height):
110 self.hitbox_half_size[:] = [width / 2, height / 2]
111
112
113 cpdef set_bullet_attributes(self, type_, anim, sprite_idx_offset,
114 bullets_per_shot, number_of_shots, speed, speed2,
115 launch_angle, angle, flags):
116
117 # Apply difficulty-specific modifiers
118 speed_a, speed_b, nb_a, nb_b, shots_a, shots_b = self.difficulty_coeffs
119 diff_coeff = self._game.difficulty / 32.
120
121 speed += speed_a * (1. - diff_coeff) + speed_b * diff_coeff
122 speed2 += (speed_a * (1. - diff_coeff) + speed_b * diff_coeff) / 2.
123 bullets_per_shot += int(nb_a * (1. - diff_coeff) + nb_b * diff_coeff)
124 number_of_shots += int(shots_a * (1. - diff_coeff) + shots_b * diff_coeff)
125
126 self.bullet_attributes = (type_, anim, sprite_idx_offset, bullets_per_shot,
127 number_of_shots, speed, speed2, launch_angle,
128 angle, flags)
129 if not self.delay_attack:
130 self.fire()
131
132
133 cpdef set_bullet_launch_interval(self, long value, unsigned long start=0):
134 # Apply difficulty-specific modifiers:
135 #TODO: check every value possible! Look around 102h.exe@0x408720
136 value -= value * (<long>self._game.difficulty - 16) // 80
137
138 self.bullet_launch_interval = value
139 self.bullet_launch_timer = start % value if value > 0 else 0
140
141
142 cpdef fire(self, offset=None, bullet_attributes=None, launch_pos=None):
143 (type_, type_idx, sprite_idx_offset, bullets_per_shot, number_of_shots,
144 speed, speed2, launch_angle, angle, flags) = bullet_attributes or self.bullet_attributes
145
146 bullet_type = self._game.bullet_types[type_idx]
147
148 if launch_pos is None:
149 ox, oy = offset or self.bullet_launch_offset
150 launch_pos = self.x + ox, self.y + oy
151
152 if speed < 0.3 and speed != 0.0:
153 speed = 0.3
154 if speed2 < 0.3:
155 speed2 = 0.3
156
157 self.bullet_launch_timer = 0
158
159 player = self.select_player()
160
161 if type_ in (67, 69, 71):
162 launch_angle += self.get_player_angle(player, launch_pos)
163 if type_ == 71 and bullets_per_shot % 2 or type_ in (69, 70) and not bullets_per_shot % 2:
164 launch_angle += pi / bullets_per_shot
165 if type_ != 75:
166 launch_angle -= angle * (bullets_per_shot - 1) / 2.
167
168 bullets = self._game.bullets
169 nb_bullets_max = self._game.nb_bullets_max
170
171 for shot_nb in xrange(number_of_shots):
172 shot_speed = speed if shot_nb == 0 else speed + (speed2 - speed) * float(shot_nb) / float(number_of_shots)
173 bullet_angle = launch_angle
174 if type_ in (69, 70, 71, 74):
175 launch_angle += angle
176 for bullet_nb in xrange(bullets_per_shot):
177 if nb_bullets_max is not None and len(bullets) == nb_bullets_max:
178 break
179
180 if type_ == 75: # 102h.exe@0x4138cf
181 bullet_angle = self._game.prng.rand_double() * (launch_angle - angle) + angle
182 if type_ in (74, 75): # 102h.exe@0x4138cf
183 shot_speed = self._game.prng.rand_double() * (speed - speed2) + speed2
184 bullets.append(Bullet(launch_pos, bullet_type, sprite_idx_offset,
185 bullet_angle, shot_speed,
186 self.extended_bullet_attributes,
187 flags, player, self._game))
188
189 if type_ in (69, 70, 71, 74):
190 bullet_angle += 2. * pi / bullets_per_shot
191 else:
192 bullet_angle += angle
193
194
195 cpdef new_laser(self, variant, laser_type, sprite_idx_offset, angle, speed,
196 start_offset, end_offset, max_length, width,
197 start_duration, duration, end_duration,
198 grazing_delay, grazing_extra_duration, unknown,
199 offset=None):
200 ox, oy = offset or self.bullet_launch_offset
201 launch_pos = self.x + ox, self.y + oy
202 if variant == 86:
203 angle += self.get_player_angle(self.select_player(), launch_pos)
204 laser = Laser(launch_pos, self._game.laser_types[laser_type],
205 sprite_idx_offset, angle, speed,
206 start_offset, end_offset, max_length, width,
207 start_duration, duration, end_duration, grazing_delay,
208 grazing_extra_duration, self._game)
209 self._game.lasers.append(laser)
210 self.laser_by_id[self.current_laser_id] = laser
211
212
213 cpdef select_player(self, players=None):
214 return (players or self._game.players)[0] #TODO
215
216
217 cpdef get_player_angle(self, player=None, pos=None):
218 player_state = (player or self.select_player()).state
219 x, y = pos or (self.x, self.y)
220 return atan2(player_state.y - y, player_state.x - x)
221
222
223 cpdef set_anim(self, index):
224 entry = 0 if index in self._anms[0].scripts else 1
225 self.sprite = Sprite()
226 self.anmrunner = ANMRunner(self._anms[entry], index, self.sprite)
227
228
229 cdef void die_anim(self):
230 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.
231 self._game.new_effect((self.x, self.y), anim)
232 self._game.sfx_player.play('enep00.wav')
233
234
235 cdef void drop_particles(self, long number, long color):
236 if color == 0:
237 if self._game.stage in [1, 2, 7]:
238 color = 3
239 color += 9
240 for i in xrange(number):
241 self._game.new_particle((self.x, self.y), color, 256) #TODO: find the real size.
242
243
244 cpdef set_aux_anm(self, long number, long index):
245 entry = 0 if index in self._anms[0].scripts else 1
246 self.aux_anm[number] = Effect((self.x, self.y), index, self._anms[entry])
247
248
249 cpdef set_pos(self, x, y, z):
250 self.x, self.y = x, y
251 self.update_mode = 1
252 self.interpolator = Interpolator((x, y), self._game.frame)
253
254
255 cpdef move_to(self, duration, x, y, z, formula):
256 frame = self._game.frame
257 self.speed_interpolator = None
258 self.update_mode = 1
259 self.interpolator = Interpolator((self.x, self.y), frame,
260 (x, y), frame + duration - 1,
261 formula)
262
263 self.angle = atan2(y - self.y, x - self.x)
264
265
266 cpdef stop_in(self, duration, formula):
267 frame = self._game.frame
268 self.interpolator = None
269 self.update_mode = 1
270 self.speed_interpolator = Interpolator((self.speed,), frame,
271 (0.,), frame + duration - 1,
272 formula)
273
274
275 cpdef bint is_visible(self, long screen_width, long screen_height):
276 cdef double tw, th
277
278 if self.sprite is not None:
279 if self.sprite.corner_relative_placement:
280 raise Exception #TODO
281 _, _, tw, th = self.sprite.texcoords
282 else:
283 tw, th = 0, 0
284
285 x, y = self.x, self.y
286 max_x = tw / 2
287 max_y = th / 2
288
289 if (max_x < x - screen_width
290 or max_x < -x
291 or max_y < y - screen_height
292 or max_y < -y):
293 return False
294 return True
295
296
297 cdef void check_collisions(self):
298 cdef long damages
299 cdef double half_size[2], bx, by, lx, ly, px, py, phalf_size
300
301 # Check for collisions
302 ex, ey = self.x, self.y
303 ehalf_size_x = self.hitbox_half_size[0]
304 ehalf_size_y = self.hitbox_half_size[1]
305 ex1, ex2 = ex - ehalf_size_x, ex + ehalf_size_x
306 ey1, ey2 = ey - ehalf_size_y, ey + ehalf_size_y
307
308 damages = 0
309
310 # Check for enemy-bullet collisions
311 for bullet in self._game.players_bullets:
312 if bullet.state != LAUNCHED:
313 continue
314 half_size[0] = bullet.hitbox[0]
315 half_size[1] = bullet.hitbox[1]
316 bx, by = bullet.x, bullet.y
317 bx1, bx2 = bx - half_size[0], bx + half_size[0]
318 by1, by2 = by - half_size[1], by + half_size[1]
319
320 if not (bx2 < ex1 or bx1 > ex2
321 or by2 < ey1 or by1 > ey2):
322 bullet.collide()
323 damages += bullet.damage
324 self._game.sfx_player.play('damage00.wav')
325
326 # Check for enemy-laser collisions
327 for laser in self._game.players_lasers:
328 if not laser:
329 continue
330
331 half_size[0] = laser.hitbox[0]
332 half_size[1] = laser.hitbox[1]
333 lx, ly = laser.x, laser.y * 2.
334 lx1, lx2 = lx - half_size[0], lx + half_size[0]
335
336 if not (lx2 < ex1 or lx1 > ex2
337 or ly < ey1):
338 damages += laser.damage
339 self._game.sfx_player.play('damage00.wav')
340 self.drop_particles(1, 1) #TODO: don’t call each frame.
341
342 # Check for enemy-player collisions
343 ex1, ex2 = ex - ehalf_size_x * 2. / 3., ex + ehalf_size_x * 2. / 3.
344 ey1, ey2 = ey - ehalf_size_y * 2. / 3., ey + ehalf_size_y * 2. / 3.
345 if self.collidable:
346 for player in self._game.players:
347 px, py = player.state.x, player.state.y
348 phalf_size = player.sht.hitbox
349 px1, px2 = px - phalf_size, px + phalf_size
350 py1, py2 = py - phalf_size, py + phalf_size
351
352 #TODO: box-box or point-in-box?
353 if not (ex2 < px1 or ex1 > px2 or ey2 < py1 or ey1 > py2):
354 if not self.boss:
355 damages += 10
356 player.collide()
357
358 # Adjust damages
359 damages = min(70, damages)
360 score = (damages // 5) * 10
361 self._game.players[0].state.score += score #TODO: better distribution amongst the players.
362
363 if self.damageable:
364 if self._game.spellcard is not None:
365 #TODO: there is a division by 3, somewhere... where is it?
366 if damages <= 7:
367 damages = 1 if damages else 0
368 else:
369 damages //= 7
370
371 # Apply damages
372 self.life -= damages
373
374
375 cdef void handle_callbacks(self):
376 #TODO: implement missing callbacks and clean up!
377 if self.life <= 0 and self.touchable:
378 self.timeout = -1 #TODO: not really true, the timeout is frozen
379 self.timeout_callback = -1
380 death_flags = self.death_flags & 7
381
382 self.die_anim()
383
384 #TODO: verify if the score is added with all the different flags.
385 self._game.players[0].state.score += self.die_score #TODO: better distribution amongst the players.
386
387 #TODO: verify if that should really be there.
388 if self.boss:
389 self._game.change_bullets_into_bonus()
390
391 if death_flags < 4:
392 if self.bonus_dropped > -1:
393 self.drop_particles(7, 0)
394 self._game.drop_bonus(self.x, self.y, self.bonus_dropped)
395 elif self.bonus_dropped == -1:
396 if self._game.deaths_count % 3 == 0:
397 self.drop_particles(10, 0)
398 self._game.drop_bonus(self.x, self.y, self._game.bonus_list[self._game.next_bonus])
399 self._game.next_bonus = (self._game.next_bonus + 1) % 32
400 else:
401 self.drop_particles(4, 0)
402 self._game.deaths_count += 1
403 else:
404 self.drop_particles(4, 0)
405
406 if death_flags == 0:
407 self.removed = True
408 return
409
410 if death_flags == 1:
411 if self.boss:
412 self.boss = False #TODO: really?
413 self._game.boss = None
414 self.touchable = False
415 elif death_flags == 2:
416 pass # Just that?
417 elif death_flags == 3:
418 if self.boss:
419 self.boss = False #TODO: really?
420 self._game.boss = None
421 self.damageable = False
422 self.life = 1
423 self.death_flags = 0
424
425 if death_flags != 0 and self.death_callback > -1:
426 self.process.switch_to_sub(self.death_callback)
427 self.death_callback = -1
428 elif self.life <= self.low_life_trigger and self.low_life_callback > -1:
429 self.process.switch_to_sub(self.low_life_callback)
430 self.low_life_callback = -1
431 self.low_life_trigger = -1
432 self.timeout_callback = -1
433 elif self.timeout != -1 and self.frame == self.timeout:
434 self.frame = 0
435 self.timeout = -1
436 self._game.kill_enemies()
437 self._game.cancel_bullets()
438
439 if self.low_life_trigger > 0:
440 self.life = self.low_life_trigger
441 self.low_life_trigger = -1
442
443 if self.timeout_callback > -1:
444 self.process.switch_to_sub(self.timeout_callback)
445 self.timeout_callback = -1
446 #TODO: this is only done under certain (unknown) conditions!
447 # but it shouldn't hurt anyway, as the only option left is to crash!
448 elif self.death_callback > -1:
449 self.life = 0 #TODO: do this next frame? Bypass self.touchable?
450 else:
451 raise Exception('What the hell, man!')
452
453
454 cpdef update(self):
455 cdef double x, y, speed
456
457 if self.process:
458 self.process.run_iteration()
459
460 x, y = self.x, self.y
461
462 if self.update_mode == 1:
463 speed = 0.
464 if self.interpolator:
465 self.interpolator.update(self._game.frame)
466 x, y = self.interpolator.values
467 if self.speed_interpolator:
468 self.speed_interpolator.update(self._game.frame)
469 speed, = self.speed_interpolator.values
470 else:
471 speed = self.speed
472 self.speed += self.acceleration
473 self.angle += self.rotation_speed
474
475 dx, dy = cos(self.angle) * speed, sin(self.angle) * speed
476 if self._type & 2:
477 x -= dx
478 else:
479 x += dx
480 y += dy
481
482 if self.movement_dependant_sprites is not None:
483 #TODO: is that really how it works? Almost.
484 # Sprite determination is done only once per changement, and is
485 # superseeded by ins_97.
486 end_left, end_right, left, right = self.movement_dependant_sprites
487 if x < self.x and self.direction != -1:
488 self.set_anim(left)
489 self.direction = -1
490 elif x > self.x and self.direction != +1:
491 self.set_anim(right)
492 self.direction = +1
493 elif x == self.x and self.direction != 0:
494 self.set_anim({-1: end_left, +1: end_right}[self.direction])
495 self.direction = 0
496
497
498 if self.screen_box is not None:
499 xmin, ymin, xmax, ymax = self.screen_box
500 x = max(xmin, min(x, xmax))
501 y = max(ymin, min(y, ymax))
502
503
504 self.x, self.y = x, y
505
506 #TODO
507 if self.anmrunner is not None and not self.anmrunner.run_frame():
508 self.anmrunner = None
509
510 if self.sprite is not None and self.visible:
511 if self.sprite.removed:
512 self.sprite = None
513 else:
514 self.sprite.update_orientation(self.angle,
515 self.automatic_orientation)
516
517
518 if self.bullet_launch_interval != 0:
519 self.bullet_launch_timer += 1
520 if self.bullet_launch_timer == self.bullet_launch_interval:
521 self.fire()
522
523 # Check collisions
524 if self.touchable:
525 self.check_collisions()
526
527 for anm in self.aux_anm:
528 if anm is not None:
529 anm.x, anm.y = self.x, self.y
530 anm.update()
531
532 self.handle_callbacks()
533
534 self.frame += 1
535