Mercurial > touhou
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 |