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