changeset 274:f037bca24f2d

Partially implement lasers. “Launch animations”/“energy circles” are missing, aswell as collision and grazing.
author Thibaut Girka <thib@sitedethib.com>
date Sun, 05 Feb 2012 23:41:55 +0100
parents 595b227886b1
children 4b0570bf5847
files pytouhou/formats/ecl.py pytouhou/game/enemy.py pytouhou/game/game.py pytouhou/game/laser.py pytouhou/game/lasertype.py pytouhou/game/player.py pytouhou/games/eosd.py pytouhou/games/pcb.py pytouhou/ui/gamerenderer.pyx pytouhou/ui/sprite.pyx pytouhou/vm/eclrunner.py
diffstat 11 files changed, 283 insertions(+), 48 deletions(-) [+]
line wrap: on
line diff
--- a/pytouhou/formats/ecl.py
+++ b/pytouhou/formats/ecl.py
@@ -108,7 +108,7 @@ class ECL(object):
                      86: ('hhffffffiiiiii', 'laser2'),
                      87: ('i', 'set_upcoming_id'),
                      88: ('if','alter_laser_angle'),
-                     90: ('iiii', 'translate_laser'),
+                     90: ('ifff', 'reposition_laser'),
                      92: ('i', 'cancel_laser'),
                      93: ('hhs', 'set_spellcard'),
                      94: ('', 'end_spellcard'),
--- a/pytouhou/game/enemy.py
+++ b/pytouhou/game/enemy.py
@@ -17,6 +17,7 @@ from pytouhou.utils.interpolator import 
 from pytouhou.vm.anmrunner import ANMRunner
 from pytouhou.game.sprite import Sprite
 from pytouhou.game.bullet import Bullet
+from pytouhou.game.laser import Laser
 from pytouhou.game.effect import Effect
 from math import cos, sin, atan2, pi
 
@@ -46,6 +47,8 @@ class Enemy(object):
         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.death_callback = -1
@@ -161,6 +164,22 @@ class Enemy(object):
                 bullet_angle += angle
 
 
+    def new_laser(self, variant, laser_type, sprite_idx_offset, angle, speed,
+                  start_offset, end_offset, max_length, width,
+                  start_duration, duration, end_duration,
+                  grazing_delay, grazing_extra_duration, unknown):
+        launch_pos = self.x, self.y
+        if variant == 86:
+            angle += self.get_player_angle(self.select_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
+
+
     def select_player(self, players=None):
         return (players or self._game.players)[0] #TODO
 
--- a/pytouhou/game/game.py
+++ b/pytouhou/game/game.py
@@ -27,7 +27,7 @@ from pytouhou.game.effect import Particl
 
 class Game(object):
     def __init__(self, resource_loader, players, stage, rank, difficulty,
-                 bullet_types, item_types,
+                 bullet_types, laser_types, item_types,
                  nb_bullets_max=None, width=384, height=448, prng=None):
         self.resource_loader = resource_loader
 
@@ -35,12 +35,14 @@ class Game(object):
 
         self.nb_bullets_max = nb_bullets_max
         self.bullet_types = bullet_types
+        self.laser_types = laser_types
         self.item_types = item_types
 
         self.players = players
         self.enemies = []
         self.effects = []
         self.bullets = []
+        self.lasers = []
         self.cancelled_bullets = []
         self.players_bullets = []
         self.items = []
@@ -113,7 +115,12 @@ class Game(object):
     def change_bullets_into_star_items(self):
         player = self.players[0] #TODO
         item_type = self.item_types[6]
-        self.items.extend(Item((bullet.x, bullet.y), 6, item_type, self, player=player) for bullet in self.bullets)
+        self.items.extend(Item((bullet.x, bullet.y), 6, item_type, self, player=player)
+                            for bullet in self.bullets)
+        for laser in self.lasers:
+            self.items.extend(Item(pos, 6, item_type, self, player=player)
+                                for pos in laser.get_bullets_pos())
+            laser.cancel()
         self.bullets = []
 
 
@@ -155,6 +162,8 @@ class Game(object):
         self.update_enemies() # Pri 9
         self.update_effects() # Pri 10
         self.update_bullets() # Pri 11
+        for laser in self.lasers: #TODO: what priority is it?
+            laser.update()
         # Pri 12 is HUD
 
         # 4. Cleaning
@@ -273,6 +282,9 @@ class Game(object):
         self.cancelled_bullets = [bullet for bullet in self.cancelled_bullets
                             if not bullet._removed]
 
+        # Filter “timed-out” lasers
+        self.lasers = [laser for laser in self.lasers if not laser._removed]
+
         # Filter out-of-scren items
         items = []
         for item in self.items:
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/laser.py
@@ -0,0 +1,121 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2012 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 math import cos, sin, pi
+
+from pytouhou.vm.anmrunner import ANMRunner
+from pytouhou.game.sprite import Sprite
+
+
+STARTING, STARTED, STOPPING = range(3)
+
+
+class Laser(object):
+    def __init__(self, pos, laser_type, sprite_idx_offset,
+                       angle, speed, start_offset, end_offset, max_length, width,
+                       start_duration, duration, stop_duration,
+                       grazing_delay, grazing_extra_duration,
+                       game):
+        self._game = game
+        #TODO: aux sprite
+        self._sprite = None
+        self._anmrunner = None
+        self._removed = False
+        self._laser_type = laser_type
+        self.state = STARTING
+
+        #TODO: hitbox
+
+        self.frame = 0
+        self.start_duration = start_duration
+        self.duration = duration
+        self.stop_duration = stop_duration
+        self.grazing_delay = grazing_delay
+        self.grazing_extra_duration = grazing_extra_duration
+
+        self.sprite_idx_offset = sprite_idx_offset
+        self.x, self.y = pos
+        self.angle = angle
+        self.speed = speed
+        self.start_offset = start_offset
+        self.end_offset = end_offset
+        self.max_length = max_length
+        self.width = width
+
+        self.set_anim()
+
+
+    def set_anim(self, sprite_idx_offset=None):
+        if sprite_idx_offset is not None:
+            self.sprite_idx_offset = sprite_idx_offset
+
+        lt = self._laser_type
+        self._sprite = Sprite()
+        self._sprite.angle = self.angle
+        self._anmrunner = ANMRunner(lt.anm_wrapper, lt.anim_index,
+                                    self._sprite, self.sprite_idx_offset)
+        self._anmrunner.run_frame()
+
+
+    def get_bullets_pos(self):
+        #TODO: check
+        offset = self.start_offset
+        length = min(self.end_offset - self.start_offset, self.max_length)
+        dx, dy = cos(self.angle), sin(self.angle)
+        while 0 <= offset - self.start_offset <= length:
+            yield (self.x + offset * dx, self.y + offset * dy)
+            offset += 48.
+
+
+    def cancel(self):
+        if self.state != STOPPING:
+            self.frame = 0
+            self.state = STOPPING
+
+
+    def update(self):
+        if self._anmrunner is not None and not self._anmrunner.run_frame():
+            self._anmrunner = None
+
+        self.end_offset += self.speed
+
+        length = min(self.end_offset - self.start_offset, self.max_length) # TODO
+        if self.state == STARTING:
+            if self.frame == self.start_duration:
+                self.frame = 0
+                self.state = STARTED
+            else:
+                width = self.width * float(self.frame) / self.start_duration #TODO
+        if self.state == STARTED:
+            width = self.width #TODO
+            if self.frame == self.duration:
+                self.frame = 0
+                self.state = STOPPING
+        if self.state == STOPPING:
+            if self.frame == self.stop_duration:
+                width = 0.
+                self._removed = True
+            else:
+                width = self.width * (1. - float(self.frame) / self.stop_duration) #TODO
+
+        self._sprite.allow_dest_offset = True
+        self._sprite.dest_offset = (0., self.end_offset - length / 2., 0.)
+        self._sprite.width_override = width or 0.01 #TODO
+        self._sprite.height_override = length or 0.01 #TODO
+
+        self._sprite.update_orientation(pi/2. - self.angle, True)
+        self._sprite._changed = True #TODO
+
+        self.frame += 1
+
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/lasertype.py
@@ -0,0 +1,4 @@
+class LaserType(object):
+    def __init__(self, anm_wrapper, anim_index):
+        self.anm_wrapper = anm_wrapper
+        self.anim_index = anim_index
--- a/pytouhou/game/player.py
+++ b/pytouhou/game/player.py
@@ -239,6 +239,8 @@ class Player(object):
             if time > 30:
                 for bullet in self._game.bullets:
                     bullet.cancel()
+                for laser in self._game.lasers:
+                    laser.cancel()
 
             if time > 90: # start the bullet hell again
                 self.death_time = 0
--- a/pytouhou/games/eosd.py
+++ b/pytouhou/games/eosd.py
@@ -16,6 +16,7 @@ from pytouhou.utils.interpolator import 
 
 from pytouhou.game.game import Game
 from pytouhou.game.bullettype import BulletType
+from pytouhou.game.lasertype import LaserType
 from pytouhou.game.itemtype import ItemType
 from pytouhou.game.player import Player
 from pytouhou.game.orb import Orb
@@ -27,33 +28,43 @@ SQ2 = 2. ** 0.5 / 2.
 
 
 class EoSDGame(Game):
-    def __init__(self, resource_loader, player_states, stage, rank, difficulty, **kwargs):
-        etama3 = resource_loader.get_anm_wrapper(('etama3.anm',))
-        etama4 = resource_loader.get_anm_wrapper(('etama4.anm',))
-        bullet_types = [BulletType(etama3, 0, 11, 14, 15, 16, hitbox_size=4),
-                        BulletType(etama3, 1, 12, 17, 18, 19, hitbox_size=6),
-                        BulletType(etama3, 2, 12, 17, 18, 19, hitbox_size=4),
-                        BulletType(etama3, 3, 12, 17, 18, 19, hitbox_size=6),
-                        BulletType(etama3, 4, 12, 17, 18, 19, hitbox_size=5),
-                        BulletType(etama3, 5, 12, 17, 18, 19, hitbox_size=4),
-                        BulletType(etama3, 6, 13, 20, 20, 20, hitbox_size=16),
-                        BulletType(etama3, 7, 13, 20, 20, 20, hitbox_size=11),
-                        BulletType(etama3, 8, 13, 20, 20, 20, hitbox_size=9),
-                        BulletType(etama4, 0, 1, 2, 2, 2, hitbox_size=32)]
+    def __init__(self, resource_loader, player_states, stage, rank, difficulty,
+                 bullet_types=None, laser_types=None, item_types=None,
+                 nb_bullets_max=640, width=384, height=448, prng=None):
 
-        item_types = [ItemType(etama3, 0, 7), #Power
-                      ItemType(etama3, 1, 8), #Point
-                      ItemType(etama3, 2, 9), #Big power
-                      ItemType(etama3, 3, 10), #Bomb
-                      ItemType(etama3, 4, 11), #Full power
-                      ItemType(etama3, 5, 12), #1up
-                      ItemType(etama3, 6, 13)] #Star
+        if not bullet_types:
+            etama3 = resource_loader.get_anm_wrapper(('etama3.anm',))
+            etama4 = resource_loader.get_anm_wrapper(('etama4.anm',))
+            bullet_types = [BulletType(etama3, 0, 11, 14, 15, 16, hitbox_size=4),
+                            BulletType(etama3, 1, 12, 17, 18, 19, hitbox_size=6),
+                            BulletType(etama3, 2, 12, 17, 18, 19, hitbox_size=4),
+                            BulletType(etama3, 3, 12, 17, 18, 19, hitbox_size=6),
+                            BulletType(etama3, 4, 12, 17, 18, 19, hitbox_size=5),
+                            BulletType(etama3, 5, 12, 17, 18, 19, hitbox_size=4),
+                            BulletType(etama3, 6, 13, 20, 20, 20, hitbox_size=16),
+                            BulletType(etama3, 7, 13, 20, 20, 20, hitbox_size=11),
+                            BulletType(etama3, 8, 13, 20, 20, 20, hitbox_size=9),
+                            BulletType(etama4, 0, 1, 2, 2, 2, hitbox_size=32)]
+
+        if not laser_types:
+            laser_types = [LaserType(etama3, 9),
+                           LaserType(etama3, 10)]
+
+        if not item_types:
+            item_types = [ItemType(etama3, 0, 7), #Power
+                          ItemType(etama3, 1, 8), #Point
+                          ItemType(etama3, 2, 9), #Big power
+                          ItemType(etama3, 3, 10), #Bomb
+                          ItemType(etama3, 4, 11), #Full power
+                          ItemType(etama3, 5, 12), #1up
+                          ItemType(etama3, 6, 13)] #Star
 
         characters = resource_loader.get_eosd_characters()
         players = [EoSDPlayer(state, self, resource_loader, characters[state.character]) for state in player_states]
 
         Game.__init__(self, resource_loader, players, stage, rank, difficulty,
-                      bullet_types, item_types, nb_bullets_max=640, **kwargs)
+                      bullet_types, laser_types, item_types, nb_bullets_max,
+                      width, height, prng)
 
 
 
--- a/pytouhou/games/pcb.py
+++ b/pytouhou/games/pcb.py
@@ -16,6 +16,7 @@ from pytouhou.utils.interpolator import 
 
 from pytouhou.game.game import Game
 from pytouhou.game.bullettype import BulletType
+from pytouhou.game.bullettype import LaserType
 from pytouhou.game.itemtype import ItemType
 from pytouhou.game.player import Player
 from pytouhou.game.orb import Orb
@@ -24,32 +25,40 @@ from math import pi
 
 
 class PCBGame(Game):
-    def __init__(self, resource_loader, player_states, stage, rank, difficulty, **kwargs):
-        etama3 = resource_loader.get_anm_wrapper(('etama3.anm',))
-        etama4 = resource_loader.get_anm_wrapper(('etama4.anm',))
-        bullet_types = [BulletType(etama3, 0, 11, 14, 15, 16, hitbox_size=4),
-                        BulletType(etama3, 1, 12, 17, 18, 19, hitbox_size=6),
-                        BulletType(etama3, 2, 12, 17, 18, 19, hitbox_size=4),
-                        BulletType(etama3, 3, 12, 17, 18, 19, hitbox_size=6),
-                        BulletType(etama3, 4, 12, 17, 18, 19, hitbox_size=5),
-                        BulletType(etama3, 5, 12, 17, 18, 19, hitbox_size=4),
-                        BulletType(etama3, 6, 13, 20, 20, 20, hitbox_size=16),
-                        BulletType(etama3, 7, 13, 20, 20, 20, hitbox_size=11),
-                        BulletType(etama3, 8, 13, 20, 20, 20, hitbox_size=9),
-                        BulletType(etama4, 0, 1, 2, 2, 2, hitbox_size=32)]
+    def __init__(self, resource_loader, player_states, stage, rank, difficulty,
+                 bullet_types=None, laser_types=None, item_types=None,
+                 nb_bullets_max=640, width=384, height=448, prng=None):
+        if not bullet_types:
+            etama3 = resource_loader.get_anm_wrapper(('etama3.anm',))
+            etama4 = resource_loader.get_anm_wrapper(('etama4.anm',))
+            bullet_types = [BulletType(etama3, 0, 11, 14, 15, 16, hitbox_size=4),
+                            BulletType(etama3, 1, 12, 17, 18, 19, hitbox_size=6),
+                            BulletType(etama3, 2, 12, 17, 18, 19, hitbox_size=4),
+                            BulletType(etama3, 3, 12, 17, 18, 19, hitbox_size=6),
+                            BulletType(etama3, 4, 12, 17, 18, 19, hitbox_size=5),
+                            BulletType(etama3, 5, 12, 17, 18, 19, hitbox_size=4),
+                            BulletType(etama3, 6, 13, 20, 20, 20, hitbox_size=16),
+                            BulletType(etama3, 7, 13, 20, 20, 20, hitbox_size=11),
+                            BulletType(etama3, 8, 13, 20, 20, 20, hitbox_size=9),
+                            BulletType(etama4, 0, 1, 2, 2, 2, hitbox_size=32)]
 
-        item_types = [ItemType(etama3, 0, 7), #Power
-                      ItemType(etama3, 1, 8), #Point
-                      ItemType(etama3, 2, 9), #Big power
-                      ItemType(etama3, 3, 10), #Bomb
-                      ItemType(etama3, 4, 11), #Full power
-                      ItemType(etama3, 5, 12), #1up
-                      ItemType(etama3, 6, 13)] #Star
+        if not laser_types:
+            laser_types = [] #TODO
+
+        if not item_types:
+            item_types = [ItemType(etama3, 0, 7), #Power
+                          ItemType(etama3, 1, 8), #Point
+                          ItemType(etama3, 2, 9), #Big power
+                          ItemType(etama3, 3, 10), #Bomb
+                          ItemType(etama3, 4, 11), #Full power
+                          ItemType(etama3, 5, 12), #1up
+                          ItemType(etama3, 6, 13)] #Star
 
         players = [PCBPlayer(state, self, resource_loader) for state in player_states]
 
         Game.__init__(self, resource_loader, players, stage, rank, difficulty,
-                      bullet_types, item_types, nb_bullets_max=640, **kwargs)
+                      bullet_types, laser_types, item_types, nb_bullets_max,
+                      width, height, prng)
 
 
 
--- a/pytouhou/ui/gamerenderer.pyx
+++ b/pytouhou/ui/gamerenderer.pyx
@@ -86,7 +86,7 @@ cdef class GameRenderer(Renderer):
             self.render_elements(chain(game.players_bullets,
                                        game.players,
                                        *(player.objects() for player in game.players)))
-            self.render_elements(chain(game.bullets, game.cancelled_bullets, game.items))
+            self.render_elements(chain(game.bullets, game.lasers, game.cancelled_bullets, game.items))
             #TODO: display item indicators
             glEnable(GL_FOG)
 
--- a/pytouhou/ui/sprite.pyx
+++ b/pytouhou/ui/sprite.pyx
@@ -38,6 +38,8 @@ cpdef object get_sprite_rendering_data(o
     if sprite.mirrored:
         vertmat.flip()
 
+    if sprite.allow_dest_offset:
+        vertmat.translate(sprite.dest_offset[0], sprite.dest_offset[1], sprite.dest_offset[2])
     rx, ry, rz = sprite.rotations_3d
     if sprite.automatic_orientation:
         rz += pi/2. - sprite.angle
@@ -51,8 +53,6 @@ cpdef object get_sprite_rendering_data(o
             vertmat.rotate_y(ry)
         if rz:
             vertmat.rotate_z(-rz) #TODO: minus, really?
-    if sprite.allow_dest_offset:
-        vertmat.translate(sprite.dest_offset[0], sprite.dest_offset[1], sprite.dest_offset[2])
     if sprite.corner_relative_placement: # Reposition
         vertmat.translate(width / 2., height / 2., 0.)
 
--- a/pytouhou/vm/eclrunner.py
+++ b/pytouhou/vm/eclrunner.py
@@ -723,6 +723,63 @@ class ECLRunner(object):
         self._game.change_bullets_into_star_items()
 
 
+    @instruction(85)
+    def new_laser(self, laser_type, sprite_idx_offset, angle, speed,
+                  start_offset, end_offset, max_length, width,
+                  start_duration, duration, end_duration,
+                  grazing_delay, grazing_extra_duration, unknown):
+        self._enemy.new_laser(85, laser_type, sprite_idx_offset, self._getval(angle), speed,
+                              start_offset, end_offset, max_length, width,
+                              start_duration, duration, end_duration,
+                              grazing_delay, grazing_extra_duration, unknown)
+
+
+    @instruction(86)
+    def new_laser_towards_player(self, laser_type, sprite_idx_offset, angle, speed,
+                                 start_offset, end_offset, max_length, width,
+                                 start_duration, duration, end_duration,
+                                 grazing_delay, grazing_extra_duration, unknown):
+        self._enemy.new_laser(86, laser_type, sprite_idx_offset, self._getval(angle), speed,
+                              start_offset, end_offset, max_length, width,
+                              start_duration, duration, end_duration,
+                              grazing_delay, grazing_extra_duration, unknown)
+
+
+    @instruction(87)
+    def set_upcoming_laser_id(self, laser_id):
+        self._enemy.current_laser_id = laser_id
+
+
+    @instruction(88)
+    def alter_laser_angle(self, laser_id, delta):
+        try:
+            laser = self._enemy.laser_by_id[laser_id]
+        except KeyError:
+            pass #TODO
+        else:
+            laser.angle += self._getval(delta)
+
+
+    @instruction(90)
+    def reposition_laser(self, laser_id, ox, oy, oz):
+        try:
+            laser = self._enemy.laser_by_id[laser_id]
+        except KeyError:
+            pass #TODO
+        else:
+            laser.x, laser.y = self._enemy.x + ox, self._enemy.y + oy
+
+
+    @instruction(92)
+    def cancel_laser(self, laser_id):
+        try:
+            laser = self._enemy.laser_by_id[laser_id]
+        except KeyError:
+            pass #TODO
+        else:
+            laser.cancel()
+
+
     @instruction(93)
     def set_spellcard(self, unknown, number, name):
         #TODO: display it on the game.