Mercurial > touhou
comparison pytouhou/game/game.pyx @ 447:78e1c3864e73
Make pytouhou.game.game an extension type.
author | Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> |
---|---|
date | Sat, 17 Aug 2013 06:29:53 +0200 |
parents | pytouhou/game/game.py@3a33ed7f3b85 |
children | 3bc37791f0a2 |
comparison
equal
deleted
inserted
replaced
446:3a33ed7f3b85 | 447:78e1c3864e73 |
---|---|
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 pytouhou.vm.msgrunner import MSGRunner | |
16 | |
17 from pytouhou.game.element cimport Element | |
18 from pytouhou.game.bullet cimport Bullet | |
19 from pytouhou.game.bullet import LAUNCHED, CANCELLED | |
20 from pytouhou.game.enemy cimport Enemy | |
21 from pytouhou.game.item cimport Item | |
22 from pytouhou.game.effect cimport Particle | |
23 from pytouhou.game.text import Text | |
24 from pytouhou.game.face import Face | |
25 | |
26 | |
27 cdef class Game: | |
28 def __init__(self, players, long stage, long rank, long difficulty, bullet_types, | |
29 laser_types, item_types, long nb_bullets_max=0, long width=384, | |
30 long height=448, prng=None, interface=None, double continues=0, | |
31 hints=None): | |
32 self.width, self.height = width, height | |
33 | |
34 self.nb_bullets_max = nb_bullets_max | |
35 self.bullet_types = bullet_types | |
36 self.laser_types = laser_types | |
37 self.item_types = item_types | |
38 | |
39 self.players = players | |
40 self.enemies = [] | |
41 self.effects = [] | |
42 self.bullets = [] | |
43 self.lasers = [] | |
44 self.cancelled_bullets = [] | |
45 self.players_bullets = [] | |
46 self.players_lasers = [None, None] | |
47 self.items = [] | |
48 self.labels = [] | |
49 self.faces = [None, None] | |
50 self.interface = interface | |
51 self.hints = hints | |
52 | |
53 self.continues = continues | |
54 self.stage = stage | |
55 self.rank = rank | |
56 self.difficulty = difficulty | |
57 self.difficulty_counter = 0 | |
58 self.difficulty_min = 12 if rank == 0 else 10 | |
59 self.difficulty_max = 20 if rank == 0 else 32 | |
60 self.boss = None | |
61 self.spellcard = None | |
62 self.time_stop = False | |
63 self.msg_runner = None | |
64 self.msg_wait = False | |
65 self.bonus_list = [0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, | |
66 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 2] | |
67 self.prng = prng | |
68 self.frame = 0 | |
69 self.sfx_player = None | |
70 | |
71 self.spellcard_effect = None | |
72 | |
73 # See 102h.exe@0x413220 if you think you're brave enough. | |
74 self.deaths_count = self.prng.rand_uint16() % 3 | |
75 self.next_bonus = self.prng.rand_uint16() % 8 | |
76 | |
77 self.last_keystate = 0 | |
78 | |
79 | |
80 def msg_sprites(self): | |
81 return [face for face in self.faces if face is not None] if self.msg_runner is not None and not self.msg_runner.ended else [] | |
82 | |
83 | |
84 def lasers_sprites(self): | |
85 return [laser for laser in self.players_lasers if laser is not None] | |
86 | |
87 | |
88 cpdef modify_difficulty(self, long diff): | |
89 self.difficulty_counter += diff | |
90 while self.difficulty_counter < 0: | |
91 self.difficulty -= 1 | |
92 self.difficulty_counter += 100 | |
93 while self.difficulty_counter >= 100: | |
94 self.difficulty += 1 | |
95 self.difficulty_counter -= 100 | |
96 if self.difficulty < self.difficulty_min: | |
97 self.difficulty = self.difficulty_min | |
98 elif self.difficulty > self.difficulty_max: | |
99 self.difficulty = self.difficulty_max | |
100 | |
101 | |
102 def enable_spellcard_effect(self): | |
103 self.spellcard_effect = Effect((-32., -16.), 0, | |
104 self.spellcard_effect_anm) #TODO: find why this offset is necessary. | |
105 self.spellcard_effect.sprite.allow_dest_offset = True #TODO: should be the role of anm’s 25th instruction. Investigate! | |
106 | |
107 | |
108 def disable_spellcard_effect(self): | |
109 self.spellcard_effect = None | |
110 | |
111 | |
112 cpdef drop_bonus(self, double x, double y, long _type, end_pos=None): | |
113 if _type > 6: | |
114 return | |
115 if len(self.items) >= self.nb_bullets_max: | |
116 return #TODO: check | |
117 item_type = self.item_types[_type] | |
118 self.items.append(Item((x, y), _type, item_type, self, end_pos=end_pos)) | |
119 | |
120 | |
121 cdef void autocollect(self, Player player): | |
122 cdef Item item | |
123 | |
124 for item in self.items: | |
125 item.autocollect(player) | |
126 | |
127 | |
128 cpdef cancel_bullets(self): | |
129 cdef Bullet bullet | |
130 #TODO: cdef Laser laser | |
131 | |
132 for bullet in self.bullets: | |
133 bullet.cancel() | |
134 for laser in self.lasers: | |
135 laser.cancel() | |
136 | |
137 | |
138 def change_bullets_into_star_items(self): | |
139 cdef Player player | |
140 cdef Bullet bullet | |
141 | |
142 player = self.players[0] #TODO | |
143 item_type = self.item_types[6] | |
144 self.items.extend(Item((bullet.x, bullet.y), 6, item_type, self, player=player) | |
145 for bullet in self.bullets) | |
146 for laser in self.lasers: | |
147 self.items.extend(Item(pos, 6, item_type, self, player=player) | |
148 for pos in laser.get_bullets_pos()) | |
149 laser.cancel() | |
150 self.bullets = [] | |
151 | |
152 | |
153 def change_bullets_into_bonus(self): | |
154 cdef Player player | |
155 cdef Bullet bullet | |
156 | |
157 player = self.players[0] #TODO | |
158 score = 0 | |
159 bonus = 2000 | |
160 for bullet in self.bullets: | |
161 self.new_label((bullet.x, bullet.y), str(bonus)) | |
162 score += bonus | |
163 bonus += 10 | |
164 self.bullets = [] | |
165 player.state.score += score | |
166 #TODO: display the final bonus score. | |
167 | |
168 | |
169 def kill_enemies(self): | |
170 cdef Enemy enemy | |
171 | |
172 for enemy in self.enemies: | |
173 if enemy.boss: | |
174 pass # Bosses are immune to 96 | |
175 elif enemy.touchable: | |
176 enemy.life = 0 | |
177 elif enemy.death_callback > 0: | |
178 #TODO: check | |
179 enemy.process.switch_to_sub(enemy.death_callback) | |
180 enemy.death_callback = -1 | |
181 | |
182 | |
183 def new_effect(self, pos, long anim, anm=None, long number=1): | |
184 number = min(number, self.nb_bullets_max - len(self.effects)) | |
185 for i in xrange(number): | |
186 self.effects.append(Effect(pos, anim, anm or self.etama[1])) | |
187 | |
188 | |
189 cpdef new_particle(self, pos, long anim, long amp, long number=1, bint reverse=False, long duration=24): | |
190 number = min(number, self.nb_bullets_max - len(self.effects)) | |
191 for i in xrange(number): | |
192 self.effects.append(Particle(pos, anim, self.etama[1], amp, self, reverse=reverse, duration=duration)) | |
193 | |
194 | |
195 def new_enemy(self, pos, life, instr_type, bonus_dropped, die_score): | |
196 enemy = Enemy(pos, life, instr_type, bonus_dropped, die_score, self.enm_anm, self) | |
197 self.enemies.append(enemy) | |
198 return enemy | |
199 | |
200 | |
201 def new_msg(self, sub): | |
202 self.msg_runner = MSGRunner(self.msg, sub, self) | |
203 self.msg_runner.run_iteration() | |
204 | |
205 | |
206 cpdef new_label(self, pos, str text): | |
207 label = Text(pos, self.interface.ascii_anm, text=text, xspacing=8, shift=48) | |
208 label.set_timeout(60, effect='move') | |
209 self.labels.append(label) | |
210 return label | |
211 | |
212 | |
213 def new_hint(self, hint): | |
214 pos = hint['Pos'] | |
215 #TODO: Scale | |
216 | |
217 pos = pos[0] + 192, pos[1] | |
218 label = Text(pos, self.interface.ascii_anm, text=hint['Text'], align=hint['Align']) | |
219 label.set_timeout(hint['Time']) | |
220 label.set_alpha(hint['Alpha']) | |
221 label.set_color(hint['Color'], text=False) | |
222 self.labels.append(label) | |
223 return label | |
224 | |
225 | |
226 def new_face(self, side, effect): | |
227 face = Face(self.msg_anm, effect, side) | |
228 self.faces[side] = face | |
229 return face | |
230 | |
231 | |
232 def run_iter(self, long keystate): | |
233 # 1. VMs. | |
234 for runner in self.ecl_runners: | |
235 runner.run_iter() | |
236 | |
237 # 2. Modify difficulty | |
238 if self.frame % (32*60) == (32*60): #TODO: check if that is really that frame. | |
239 self.modify_difficulty(+100) | |
240 | |
241 # 3. Filter out destroyed enemies | |
242 self.enemies = filter_removed(self.enemies) | |
243 self.effects = filter_removed(self.effects) | |
244 self.bullets = filter_removed(self.bullets) | |
245 self.cancelled_bullets = filter_removed(self.cancelled_bullets) | |
246 self.items = filter_removed(self.items) | |
247 | |
248 # 4. Let's play! | |
249 # In the original game, updates are done in prioritized functions called "chains" | |
250 # We have to mimic this functionnality to be replay-compatible with the official game. | |
251 | |
252 # Pri 6 is background | |
253 self.update_background() #TODO: Pri unknown | |
254 if self.msg_runner is not None: | |
255 self.update_msg(keystate) # Pri ? | |
256 keystate &= ~3 # Remove the ability to attack (keystates 1 and 2). | |
257 self.update_players(keystate) # Pri 7 | |
258 self.update_enemies() # Pri 9 | |
259 self.update_effects() # Pri 10 | |
260 self.update_bullets() # Pri 11 | |
261 for laser in self.lasers: #TODO: what priority is it? | |
262 laser.update() | |
263 self.interface.update() # Pri 12 | |
264 if self.hints: | |
265 self.update_hints() # Not from this game, so unknown. | |
266 for label in self.labels: #TODO: what priority is it? | |
267 label.update() | |
268 self.update_faces() # Pri XXX | |
269 | |
270 # 5. Clean up | |
271 self.cleanup() | |
272 | |
273 self.frame += 1 | |
274 | |
275 | |
276 cdef void update_background(self): | |
277 if self.time_stop: | |
278 return | |
279 if self.spellcard_effect is not None: | |
280 self.spellcard_effect.update() | |
281 #TODO: update the actual background here? | |
282 | |
283 | |
284 cdef void update_enemies(self): | |
285 cdef Enemy enemy | |
286 | |
287 for enemy in self.enemies: | |
288 enemy.update() | |
289 | |
290 | |
291 cdef void update_msg(self, long keystate) except *: | |
292 cdef long k | |
293 | |
294 if any([(keystate & k and not self.last_keystate & k) for k in (1, 256)]): | |
295 self.msg_runner.skip() | |
296 self.msg_runner.skipping = bool(keystate & 256) | |
297 self.last_keystate = keystate | |
298 self.msg_runner.run_iteration() | |
299 | |
300 | |
301 cdef void update_players(self, long keystate) except *: | |
302 cdef Bullet bullet | |
303 cdef Player player | |
304 | |
305 if self.time_stop: | |
306 return | |
307 | |
308 for bullet in self.players_bullets: | |
309 bullet.update() | |
310 | |
311 for player in self.players: | |
312 player.update(keystate) #TODO: differentiate keystates (multiplayer mode) | |
313 | |
314 #XXX: Why 78910? Is it really the right value? | |
315 player.state.effective_score = min(player.state.effective_score + 78910, | |
316 player.state.score) | |
317 #TODO: give extra lives to the player | |
318 | |
319 | |
320 cdef void update_effects(self): | |
321 cdef Element effect | |
322 | |
323 for effect in self.effects: | |
324 effect.update() | |
325 | |
326 | |
327 cdef void update_hints(self): | |
328 for hint in self.hints: | |
329 if hint['Count'] == self.frame and hint['Base'] == 'start': | |
330 self.new_hint(hint) | |
331 | |
332 | |
333 cdef void update_faces(self): | |
334 for face in self.faces: | |
335 if face: | |
336 face.update() | |
337 | |
338 | |
339 cdef void update_bullets(self): | |
340 cdef Player player | |
341 cdef Bullet bullet | |
342 cdef Item item | |
343 cdef double bhalf_width, bhalf_height | |
344 | |
345 if self.time_stop: | |
346 return | |
347 | |
348 for bullet in self.cancelled_bullets: | |
349 bullet.update() | |
350 | |
351 for bullet in self.bullets: | |
352 bullet.update() | |
353 | |
354 for laser in self.players_lasers: | |
355 if laser is not None: | |
356 laser.update() | |
357 | |
358 for item in self.items: | |
359 item.update() | |
360 | |
361 for player in self.players: | |
362 player_state = player.state | |
363 | |
364 if not player_state.touchable: | |
365 continue | |
366 | |
367 px, py = player_state.x, player_state.y | |
368 phalf_size = <double>player.sht.hitbox | |
369 px1, px2 = px - phalf_size, px + phalf_size | |
370 py1, py2 = py - phalf_size, py + phalf_size | |
371 | |
372 ghalf_size = <double>player.sht.graze_hitbox | |
373 gx1, gx2 = px - ghalf_size, px + ghalf_size | |
374 gy1, gy2 = py - ghalf_size, py + ghalf_size | |
375 | |
376 for laser in self.lasers: | |
377 if laser.check_collision((px, py)): | |
378 if player_state.invulnerable_time == 0: | |
379 player.collide() | |
380 elif laser.check_grazing((px, py)): | |
381 player_state.graze += 1 #TODO | |
382 player_state.score += 500 #TODO | |
383 player.play_sound('graze') | |
384 self.modify_difficulty(+6) #TODO | |
385 self.new_particle((px, py), 9, 192) #TODO | |
386 | |
387 for bullet in self.bullets: | |
388 if bullet.state != LAUNCHED: | |
389 continue | |
390 | |
391 bhalf_width, bhalf_height = bullet.hitbox | |
392 bx, by = bullet.x, bullet.y | |
393 bx1, bx2 = bx - bhalf_width, bx + bhalf_width | |
394 by1, by2 = by - bhalf_height, by + bhalf_height | |
395 | |
396 if not (bx2 < px1 or bx1 > px2 | |
397 or by2 < py1 or by1 > py2): | |
398 bullet.collide() | |
399 if player_state.invulnerable_time == 0: | |
400 player.collide() | |
401 | |
402 elif not bullet.grazed and not (bx2 < gx1 or bx1 > gx2 | |
403 or by2 < gy1 or by1 > gy2): | |
404 bullet.grazed = True | |
405 player_state.graze += 1 | |
406 player_state.score += 500 # found experimentally | |
407 player.play_sound('graze') | |
408 self.modify_difficulty(+6) | |
409 self.new_particle((px, py), 9, 192) #TODO: find the real size and range. | |
410 #TODO: display a static particle during one frame at | |
411 # 12 pixels of the player, in the axis of the “collision”. | |
412 | |
413 #TODO: is it the right place? | |
414 if py < 128 and player_state.power >= 128: #TODO: check py. | |
415 self.autocollect(player) | |
416 | |
417 ihalf_size = <double>player.sht.item_hitbox | |
418 for item in self.items: | |
419 bx, by = item.x, item.y | |
420 bx1, bx2 = bx - ihalf_size, bx + ihalf_size | |
421 by1, by2 = by - ihalf_size, by + ihalf_size | |
422 | |
423 if not (bx2 < px1 or bx1 > px2 | |
424 or by2 < py1 or by1 > py2): | |
425 item.on_collect(player) | |
426 | |
427 | |
428 cdef void cleanup(self): | |
429 cdef Enemy enemy | |
430 cdef Bullet bullet | |
431 cdef Item item | |
432 cdef long i | |
433 | |
434 # Filter out non-visible enemies | |
435 for enemy in self.enemies: | |
436 if enemy.is_visible(self.width, self.height): | |
437 enemy.was_visible = True | |
438 elif enemy.was_visible: | |
439 # Filter out-of-screen enemy | |
440 enemy.removed = True | |
441 | |
442 self.enemies = filter_removed(self.enemies) | |
443 | |
444 # Filter out-of-scren bullets | |
445 cancelled_bullets = [] | |
446 bullets = [] | |
447 players_bullets = [] | |
448 | |
449 for bullet in self.cancelled_bullets: | |
450 if bullet.state == CANCELLED and not bullet.removed: | |
451 cancelled_bullets.append(bullet) | |
452 | |
453 for bullet in self.bullets: | |
454 if not bullet.removed: | |
455 if bullet.state == CANCELLED: | |
456 cancelled_bullets.append(bullet) | |
457 else: | |
458 bullets.append(bullet) | |
459 | |
460 for bullet in self.players_bullets: | |
461 if not bullet.removed: | |
462 if bullet.state == CANCELLED: | |
463 cancelled_bullets.append(bullet) | |
464 else: | |
465 players_bullets.append(bullet) | |
466 | |
467 self.cancelled_bullets = cancelled_bullets | |
468 self.bullets = bullets | |
469 self.players_bullets = players_bullets | |
470 | |
471 # Filter “timed-out” lasers | |
472 for i, laser in enumerate(self.players_lasers): | |
473 if laser is not None and laser.removed: | |
474 self.players_lasers[i] = None | |
475 | |
476 self.lasers = filter_removed(self.lasers) | |
477 | |
478 # Filter out-of-scren items | |
479 items = [] | |
480 for item in self.items: | |
481 if item.y < self.height: | |
482 items.append(item) | |
483 else: | |
484 self.modify_difficulty(-3) | |
485 self.items = items | |
486 | |
487 self.effects = filter_removed(self.effects) | |
488 self.labels = filter_removed(self.labels) | |
489 | |
490 # Disable boss mode if it is dead/it has timeout | |
491 if self.boss and self.boss._enemy.removed: | |
492 self.boss = None | |
493 | |
494 cdef list filter_removed(list elements): | |
495 cdef Element element | |
496 | |
497 filtered = [] | |
498 for element in elements: | |
499 if not element.removed: | |
500 filtered.append(element) | |
501 return filtered |