changeset 122:174324a4da51

Add support for launch animations! (Warning: slow :()
author Thibaut Girka <thib@sitedethib.com>
date Sat, 10 Sep 2011 01:26:30 +0200
parents 1bc0ad774ed4
children d1c82d43bbf3
files data/ST/etama3.script eclviewer.py pytouhou/game/bullet.py pytouhou/game/bullettype.py pytouhou/game/game.py pytouhou/game/games.py pytouhou/game/sprite.py pytouhou/opengl/gamerenderer.py pytouhou/utils/interpolator.py pytouhou/vm/anmrunner.py
diffstat 10 files changed, 163 insertions(+), 98 deletions(-) [+]
line wrap: on
line diff
--- a/data/ST/etama3.script
+++ b/data/ST/etama3.script
@@ -180,7 +180,7 @@ Instruction: 0 0 1 88
 Instruction: 0 0 13
 Instruction: 0 0 15
 
-# ARDCODED: cancel anim for bullet type 0
+# HARDCODED: cancel anim for bullet type 0
 Script: 11
 Instruction: 0 0 1 56
 Instruction: 0 0 0
--- a/eclviewer.py
+++ b/eclviewer.py
@@ -19,7 +19,7 @@ import os
 from pytouhou.resource.loader import Loader
 from pytouhou.game.background import Background
 from pytouhou.opengl.gamerenderer import GameRenderer
-from pytouhou.game.game import Game
+from pytouhou.game.games import EoSDGame
 from pytouhou.game.player import Player
 
 
@@ -27,7 +27,7 @@ def main(path, stage_num):
     resource_loader = Loader()
     resource_loader.scan_archives(os.path.join(path, name)
                                     for name in ('CM.DAT', 'ST.DAT'))
-    game = Game(resource_loader, [Player()], stage_num, 3, 16)
+    game = EoSDGame(resource_loader, [Player()], stage_num, 3, 16)
 
     # Load stage data
     stage = resource_loader.get_stage('stage%d.std' % stage_num)
--- a/pytouhou/game/bullet.py
+++ b/pytouhou/game/bullet.py
@@ -20,19 +20,14 @@ from pytouhou.game.sprite import Sprite
 
 
 class Bullet(object):
-    __slots__ = ('x', 'y', 'angle', 'speed', 'frame', 'grazed',
-                 'flags', 'attributes', 'anim_idx', 'sprite_idx_offset', 'player',
-                 'speed_interpolator',
-                 '_game_state', '_sprite', '_anmrunner',
-                 '_removed', '_launched')
-
-    def __init__(self, pos, anim_idx, sprite_idx_offset,
+    def __init__(self, pos, type_idx, sprite_idx_offset,
                        angle, speed, attributes, flags, player, game_state):
         self._game_state = game_state
         self._sprite = None
         self._anmrunner = None
         self._removed = False
         self._launched = False
+        self._bullet_type = game_state.bullet_types[type_idx]
 
         self.speed_interpolator = None
         self.frame = 0
@@ -40,101 +35,111 @@ class Bullet(object):
 
         self.player = player
 
-        self.anim_idx = anim_idx
         self.sprite_idx_offset = sprite_idx_offset
 
-        #TODO
-        #if flags & (2|4|8):
-        #    if flags & 2: #TODO: Huh?!
-        #        index = 14
-        #    elif flags & 4:
-        #        index = 15
-        #    else:
-        #        index = 19
-        #    self._sprite = Sprite()
-        #    self._anmrunner = ANMRunner(self._game_state.resource_loader.get_anm_wrapper(('etama3.anm',)), #TODO
-        #                                index, self._sprite, 0) #TODO: offset
-        #    self._anmrunner.run_frame()
-
         self.flags = flags
         self.attributes = list(attributes)
 
         self.x, self.y = pos
         self.angle = angle
         self.speed = speed
+        dx, dy = cos(angle) * speed, sin(angle) * speed
+        self.delta = dx, dy
 
-        if flags & 1:
-            self.speed_interpolator = Interpolator((speed + 5.,))
-            self.speed_interpolator.set_interpolation_start(0, (speed + 5.,))
-            self.speed_interpolator.set_interpolation_end(16, (speed,))
-        if flags & 448:
-            self.speed_interpolator = Interpolator((speed,))
-            self.speed_interpolator.set_interpolation_start(0, (speed,))
-            self.speed_interpolator.set_interpolation_end(attributes[0] - 1, (0,)) # TODO: really -1? Without, the new speed isn’t set.
+        #TODO
+        if flags & 14:
+            bt = self._bullet_type
+            if flags & 2:
+                index = bt.launch_anim2_index
+                launch_mult = bt.launch_anim_penalties[0]
+            elif flags & 4:
+                index = bt.launch_anim4_index
+                launch_mult = bt.launch_anim_penalties[1]
+            else:
+                index = bt.launch_anim8_index
+                launch_mult = bt.launch_anim_penalties[2]
+            self.launch_delta = dx * launch_mult, dy * launch_mult
+            self._sprite = Sprite()
+            self._anmrunner = ANMRunner(bt.anm_wrapper,
+                                        index, self._sprite,
+                                        bt.launch_anim_offsets[sprite_idx_offset])
+            self._anmrunner.run_frame()
+        else:
+            self.launch()
 
-
-    def get_player_angle(self):
-        return atan2(self.player.y - self.y, self.player.x - self.x)
+        self._sprite.angle = angle
 
 
     def is_visible(self, screen_width, screen_height):
-        if self._sprite:
-            tx, ty, tw, th = self._sprite.texcoords
-            if self._sprite.corner_relative_placement:
-                raise Exception #TODO
-        else:
-            tx, ty, tw, th = 0., 0., 0., 0.
+        tx, ty, tw, th = self._sprite.texcoords
+        if self._sprite.corner_relative_placement:
+            raise Exception #TODO
 
         max_x = tw / 2.
         max_y = th / 2.
-        min_x = -max_x
-        min_y = -max_y
 
-        if any((min_x > screen_width - self.x,
+        if any((max_x < self.x - screen_width,
                 max_x < -self.x,
-                min_y > screen_height - self.y,
+                max_y < self.y - screen_height,
                 max_y < -self.y)):
             return False
         return True
 
 
-    def set_anim(self, anim_idx=None, sprite_idx_offset=None):
-        if anim_idx is not None:
-            self.anim_idx = anim_idx
-
+    def set_anim(self, sprite_idx_offset=None):
         if sprite_idx_offset is not None:
             self.sprite_idx_offset = sprite_idx_offset
 
+        bt = self._bullet_type
         self._sprite = Sprite()
-        anm_wrapper = self._game_state.resource_loader.get_anm_wrapper(('etama3.anm',)) #TODO
-        self._anmrunner = ANMRunner(anm_wrapper, self.anim_idx,
+        self._sprite.angle = self.angle
+        self._anmrunner = ANMRunner(bt.anm_wrapper, bt.anim_index,
                                     self._sprite, self.sprite_idx_offset)
+        self._anmrunner.run_frame()
+
+
+    def launch(self):
+        self._launched = True
+        self.update = self.update_full
+        self.set_anim()
+        if self.flags & 1:
+            self.speed_interpolator = Interpolator((self.speed + 5.,), 0,
+                                                   (self.speed,), 16)
 
 
     def update(self):
-        if not self._sprite or self._sprite._removed:
-            self._launched = True
-            self.set_anim()
+        dx, dy = self.launch_delta
+        self.x += dx
+        self.y += dy
+
+        self.frame += 1
+
+        if not self._anmrunner.run_frame():
+            self.launch()
 
+
+    def update_simple(self):
+        dx, dy = self.delta
+        self.x += dx
+        self.y += dy
+
+
+    def update_full(self):
         sprite = self._sprite
 
         if self._anmrunner is not None and not self._anmrunner.run_frame():
             self._anmrunner = None
-        if sprite.automatic_orientation and sprite.angle != self.angle:
-            sprite.angle = self.angle
-            sprite._changed = True
 
         #TODO: flags
         x, y = self.x, self.y
+        dx, dy = self.delta
 
         if self.flags & 16:
             frame, count = self.attributes[0:2]
             length, angle = self.attributes[4:6]
             angle = self.angle if angle < -900.0 else angle #TODO: is that right?
-            dx = cos(self.angle) * self.speed + cos(angle) * length
-            dy = sin(self.angle) * self.speed + sin(angle) * length
-            self.speed = (dx ** 2 + dy ** 2) ** 0.5
-            self.angle = atan2(dy, dx)
+            dx, dy = dx + cos(angle) * length, dy + sin(angle) * length
+            self.delta = dx, dy
             if self.frame == frame: #TODO: include last frame, or not?
                 if count > 0:
                     self.attributes[1] -= 1
@@ -147,7 +152,12 @@ class Bullet(object):
             acceleration, angular_speed = self.attributes[4:6]
             self.speed += acceleration
             self.angle += angular_speed
-            if self.frame != 0 and self.frame % frame == 0:
+            dx, dy = cos(self.angle) * self.speed, sin(self.angle) * self.speed
+            self.delta = dx, dy
+            sprite.angle = self.angle
+            if sprite.automatic_orientation:
+                sprite._changed = True
+            if self.frame % frame == 0:
                 if count > 0:
                     self.attributes[1] -= 1
                 else:
@@ -156,31 +166,38 @@ class Bullet(object):
             #TODO: check
             frame, count = self.attributes[0:2]
             angle, speed = self.attributes[4:6]
-            if self.frame != 0 and self.frame % frame == 0:
+            if self.frame % frame == 0:
                 count = count - 1
 
                 if self.flags & 64:
-                    self.angle = self.angle + angle
+                    self.angle += angle
                 elif self.flags & 128:
-                    self.angle = self.get_player_angle() + angle
+                    self.angle = atan2(self.player.y - y, self.player.x - x) + angle
                 elif self.flags & 256:
                     self.angle = angle
 
-                if count:
-                    self.speed_interpolator.set_interpolation_start(self.frame, (speed,))
-                    self.speed_interpolator.set_interpolation_end(self.frame + frame - 1, (0,)) # TODO: really -1? Without, the new speed isn’t set.
+                dx, dy = cos(self.angle) * speed, sin(self.angle) * speed
+                self.delta = dx, dy
+                sprite.angle = self.angle
+                if sprite.automatic_orientation:
+                    sprite._changed = True
+
+                if count > 0:
+                    self.speed_interpolator = Interpolator((speed,), self.frame,
+                                                           (0.,), self.frame + frame - 1)
                 else:
                     self.flags &= ~448
                     self.speed = speed
 
                 self.attributes[1] = count
         #TODO: other flags
+        elif not self.speed_interpolator and self._anmrunner is None:
+            self.update = self.update_simple
 
         if self.speed_interpolator:
             self.speed_interpolator.update(self.frame)
             self.speed, = self.speed_interpolator.values
 
-        dx, dy = cos(self.angle) * self.speed, sin(self.angle) * self.speed
         self.x, self.y = x + dx, y + dy
 
         self.frame += 1
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/bullettype.py
@@ -0,0 +1,14 @@
+class BulletType(object):
+    def __init__(self, anm_wrapper, anim_index, cancel_anim_index,
+                 launch_anim2_index, launch_anim4_index, launch_anim8_index,
+                 launch_anim_penalties=(0.5, 0.4, 1./3.),
+                 launch_anim_offsets=(0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 0)):
+        self.anm_wrapper = anm_wrapper
+        self.anim_index = anim_index
+        self.cancel_anim_index = cancel_anim_index
+        self.launch_anim2_index = launch_anim2_index
+        self.launch_anim4_index = launch_anim4_index
+        self.launch_anim8_index = launch_anim8_index
+        self.launch_anim_penalties = launch_anim_penalties
+        self.launch_anim_offsets = launch_anim_offsets
+
--- a/pytouhou/game/game.py
+++ b/pytouhou/game/game.py
@@ -22,10 +22,12 @@ from pytouhou.game.enemy import Enemy
 
 class GameState(object):
     __slots__ = ('resource_loader', 'bullets', 'players', 'rank', 'difficulty', 'frame',
-                 'stage', 'boss', 'prng')
-    def __init__(self, resource_loader, players, stage, rank, difficulty):
+                 'stage', 'boss', 'prng', 'bullet_types')
+    def __init__(self, resource_loader, players, stage, rank, difficulty, bullet_types):
         self.resource_loader = resource_loader
 
+        self.bullet_types = bullet_types
+
         self.bullets = []
 
         self.stage = stage
@@ -39,8 +41,8 @@ class GameState(object):
 
 
 class Game(object):
-    def __init__(self, resource_loader, players, stage, rank, difficulty):
-        self.game_state = GameState(resource_loader, players, stage, rank, difficulty)
+    def __init__(self, resource_loader, players, stage, rank, difficulty, bullet_types):
+        self.game_state = GameState(resource_loader, players, stage, rank, difficulty, bullet_types)
 
         self.enemies = []
 
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/games.py
@@ -0,0 +1,33 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2011 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 pytouhou.game.game import Game
+from pytouhou.game.bullettype import BulletType
+
+class EoSDGame(Game):
+    def __init__(self, resource_loader, players, stage, rank, difficulty):
+        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),
+                        BulletType(etama3, 1, 12, 17, 18, 19),
+                        BulletType(etama3, 2, 12, 17, 18, 19),
+                        BulletType(etama3, 3, 12, 17, 18, 19),
+                        BulletType(etama3, 4, 12, 17, 18, 19),
+                        BulletType(etama3, 5, 12, 17, 18, 19),
+                        BulletType(etama3, 6, 13, 20, 20, 20),
+                        BulletType(etama3, 7, 13, 20, 20, 20),
+                        BulletType(etama3, 8, 13, 20, 20, 20),
+                        BulletType(etama4, 0, 1, 2, 2, 2)]
+
+        Game.__init__(self, resource_loader, players, stage, rank, difficulty, bullet_types)
--- a/pytouhou/game/sprite.py
+++ b/pytouhou/game/sprite.py
@@ -61,23 +61,23 @@ class Sprite(object):
 
     def fade(self, duration, alpha, formula):
         if not self.fade_interpolator:
-            self.fade_interpolator = Interpolator((self.alpha,), formula)
-            self.fade_interpolator.set_interpolation_start(self.frame, (self.alpha,))
-            self.fade_interpolator.set_interpolation_end(self.frame + duration - 1, (alpha,))
+            self.fade_interpolator = Interpolator((self.alpha,), self.frame,
+                                                  (alpha,), self.frame + duration,
+                                                  formula)
 
 
     def scale_in(self, duration, sx, sy, formula):
         if not self.scale_interpolator:
-            self.scale_interpolator = Interpolator(self.rescale, formula)
-            self.scale_interpolator.set_interpolation_start(self.frame, self.rescale)
-            self.scale_interpolator.set_interpolation_end(self.frame + duration - 1, (sx, sy))
+            self.scale_interpolator = Interpolator(self.rescale, self.frame,
+                                                   (sx, sy), self.frame + duration,
+                                                   formula)
 
 
     def move_in(self, duration, x, y, z, formula):
         if not self.offset_interpolator:
-            self.offset_interpolator = Interpolator(self.dest_offset, formula)
-            self.offset_interpolator.set_interpolation_start(self.frame, self.dest_offset)
-            self.offset_interpolator.set_interpolation_end(self.frame + duration - 1, (x, y, z))
+            self.offset_interpolator = Interpolator(self.dest_offset, self.frame,
+                                                    (x, y, z), self.frame + duration,
+                                                    formula)
 
 
     def update_orientation(self, angle_base=0., force_rotation=False):
--- a/pytouhou/opengl/gamerenderer.py
+++ b/pytouhou/opengl/gamerenderer.py
@@ -55,7 +55,7 @@ class GameRenderer(pyglet.window.Window)
         glEnableClientState(GL_VERTEX_ARRAY)
         glEnableClientState(GL_TEXTURE_COORD_ARRAY)
 
-        pyglet.clock.schedule_interval(self.update, 1./120)
+        pyglet.clock.schedule_interval(self.update, 1./60)
         pyglet.app.run()
 
 
--- a/pytouhou/utils/interpolator.py
+++ b/pytouhou/utils/interpolator.py
@@ -14,12 +14,13 @@
 
 
 class Interpolator(object):
-    def __init__(self, values=(), formula=None):
+    __slots__ = ('values', 'start_values', 'end_values', 'start_frame', 'end_frame', '_frame', '_formula')
+    def __init__(self, values=(), start_frame=0, end_values=(), end_frame=0, formula=None):
         self.values = tuple(values)
         self.start_values = tuple(values)
-        self.end_values = tuple(values)
-        self.start_frame = 0
-        self.end_frame = 0
+        self.end_values = tuple(end_values)
+        self.start_frame = start_frame
+        self.end_frame = end_frame
         self._frame = 0
         self._formula = formula or (lambda x: x)
 
@@ -50,8 +51,8 @@ class Interpolator(object):
         self._frame = frame
         if frame >= self.end_frame - 1: #XXX: skip the last interpolation step
             # This bug is replicated from the original game
-            self.values = tuple(self.end_values)
-            self.start_values = tuple(self.end_values)
+            self.values = self.end_values
+            self.start_values = self.end_values
             self.start_frame = frame
         else:
             coeff = self._formula(float(frame - self.start_frame) / float(self.end_frame - self.start_frame))
--- a/pytouhou/vm/anmrunner.py
+++ b/pytouhou/vm/anmrunner.py
@@ -41,14 +41,13 @@ class ANMRunner(object):
 
 
     def run_frame(self):
-        if self._sprite._removed:
+        if not self._running:
             return False
 
+        sprite = self._sprite
+
         while self._running:
-            try:
-                frame, instr_type, args = self.script[self.instruction_pointer]
-            except IndexError:
-                return False
+            frame, opcode, args = self.script[self.instruction_pointer]
 
             if frame > self.frame:
                 break
@@ -57,16 +56,15 @@ class ANMRunner(object):
 
             if frame == self.frame:
                 try:
-                    callback = self._handlers[instr_type]
+                    callback = self._handlers[opcode]
                 except KeyError:
                     logger.warn('unhandled opcode %d (args: %r)', instr_type, args)
                 else:
                     callback(self, *args)
-                    self._sprite._changed = True
+                    sprite._changed = True
         self.frame += 1
 
         # Update sprite
-        sprite = self._sprite
         sprite.frame += 1
 
         if sprite.rotations_speed_3d != (0., 0., 0.):
@@ -102,7 +100,7 @@ class ANMRunner(object):
     @instruction(0)
     def remove(self):
         self._sprite._removed = True
-        self._running = True
+        self._running = False
 
 
     @instruction(1)