# HG changeset patch # User Emmanuel Gil Peyrot # Date 1324214072 -3600 # Node ID 0595315d3880c212b3f935676888cfe94dfa3f88 # Parent 091301805ccedbd7ae19400db6ed8db43d146665 Fix SHT handling; change a few things to be closer to ZUN’s mind; and first stub of PCB support. diff --git a/pcb b/pcb new file mode 100755 --- /dev/null +++ b/pcb @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2011 Thibaut Girka +## +## 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. +## + +import argparse +import os + +import pyximport +pyximport.install() + +from pytouhou.resource.loader import Loader +from pytouhou.game.background import Background +from pytouhou.ui.gamerunner import GameRunner +from pytouhou.games.pcb import PCBGame +from pytouhou.game.player import PlayerState + + +def main(path, stage_num, rank, character, data): + resource_loader = Loader() + resource_loader.scan_archives(os.path.join(path, name) + for name in data) + game = PCBGame(resource_loader, [PlayerState(character=character)], stage_num, rank, 16) + + # Load stage data + stage = resource_loader.get_stage('stage%d.std' % stage_num) + + background_anm_wrapper = resource_loader.get_anm_wrapper(('stg%dbg.anm' % stage_num,)) + background = Background(stage, background_anm_wrapper) + + # Let's go! + print(stage.name) + + # Main loop + runner = GameRunner(resource_loader, game, background) + runner.start() + + +parser = argparse.ArgumentParser(description='Libre reimplementation of the Touhou 6 engine.') + +parser.add_argument('data', metavar='DAT', default=('Th07.dat'), nargs='*', help='Game’s .DAT data files') +parser.add_argument('-p', '--path', metavar='DIRECTORY', default='.', help='Game directory path.') +parser.add_argument('-s', '--stage', metavar='STAGE', type=int, required=True, help='Stage, 1 to 6, 7 (Extra) and 8 (Phantasm).') +parser.add_argument('-r', '--rank', metavar='RANK', type=int, default=0, help='Rank, from 0 (Easy, default) to 3 (Lunatic).') +parser.add_argument('-c', '--character', metavar='CHARACTER', type=int, default=0, help='Select the character to use, from 0 (ReimuA, default) to 5 (SakuyaB).') + +args = parser.parse_args() + +main(args.path, args.stage, args.rank, args.character, tuple(args.data)) diff --git a/pytouhou/formats/sht.py b/pytouhou/formats/sht.py --- a/pytouhou/formats/sht.py +++ b/pytouhou/formats/sht.py @@ -21,7 +21,7 @@ logger = get_logger(__name__) class Shot(object): def __init__(self): - self.interval = 0. + self.interval = 0 self.unknown1 = None self.pos = (0., 0.) self.hitbox = (0., 0.) @@ -41,12 +41,11 @@ class Shot(object): class SHT(object): def __init__(self): self.unknown1 = None - self.level_count = 9 self.bombs = 0. self.unknown2 = None self.hitbox = 0. self.graze_hitbox = 0. - self.autocollected_item_speed = 0. + self.autocollection_speed = 0. self.item_hitbox = 0. self.percentage_of_cherry_loss_on_die = 0. self.point_of_collection = 0 @@ -59,18 +58,20 @@ class SHT(object): @classmethod def read(cls, file): - (_, level_count, bombs, _, hitbox, graze_hitbox, - autocollected_item_speed, item_hitbox, percentage_of_cherry_loss_on_die, - point_of_collection, horizontal_vertical_speed, - horizontal_vertical_focused_speed, diagonal_speed, - diagonal_focused_speed) = unpack(' ex2 or by2 < ey1 or by1 > ey2): bullet.collide() - damages += bullet._bullet_type.damage + damages += bullet.damage self.drop_particles(1, 1) # Check for enemy-player collisions diff --git a/pytouhou/game/game.py b/pytouhou/game/game.py --- a/pytouhou/game/game.py +++ b/pytouhou/game/game.py @@ -216,8 +216,8 @@ class Game(object): for bullet in self.bullets: half_size = bullet.hitbox_half_size bx, by = bullet.x, bullet.y - bx1, bx2 = bx - half_size, bx + half_size - by1, by2 = by - half_size, by + half_size + bx1, bx2 = bx - half_size[0], bx + half_size[0] + by1, by2 = by - half_size[1], by + half_size[1] if not (bx2 < px1 or bx1 > px2 or by2 < py1 or by1 > py2): @@ -247,7 +247,7 @@ class Game(object): if not (bx2 < px1 or bx1 > px2 or by2 < py1 or by1 > py2): - item.on_collect(player.state) + item.on_collect(player) def cleanup(self): diff --git a/pytouhou/game/item.py b/pytouhou/game/item.py --- a/pytouhou/game/item.py +++ b/pytouhou/game/item.py @@ -19,7 +19,7 @@ from pytouhou.utils.interpolator import class Item(object): - def __init__(self, start_pos, _type, item_type, game, angle=pi/2, speed=8., player=None, end_pos=None): + def __init__(self, start_pos, _type, item_type, game, angle=pi/2, player=None, end_pos=None): self._game = game self._sprite = item_type.sprite self._removed = False @@ -29,14 +29,13 @@ class Item(object): self.hitbox_half_size = item_type.hitbox_size / 2. self.frame = 0 - - self.player = player - self.x, self.y = start_pos self.angle = angle - self.speed = speed - dx, dy = cos(angle) * speed, sin(angle) * speed - self.delta = dx, dy + + if player: + self.autocollect(player) + else: + self.player = None if not player: #TODO: find the formulae in the binary. @@ -53,10 +52,11 @@ class Item(object): def autocollect(self, player): self.player = player - self.speed = 8. + self.speed = player.sht.autocollection_speed if hasattr(player, 'sht') else 8. - def on_collect(self, player_state): + def on_collect(self, player): + player_state = player.state old_power = player_state.power if self._type == 0 or self._type == 2: # power or big power @@ -84,7 +84,8 @@ class Item(object): elif self._type == 1: # point player_state.points += 1 - if player_state.y < 128: #TODO: find the exact poc. + poc = player.sht.point_of_collection if hasattr(player, 'sht') else 128 #TODO: find the exact poc in EoSD. + if player_state.y < poc: score = 100000 self._game.modify_difficulty(+30) else: @@ -117,8 +118,6 @@ class Item(object): def update(self): - dx, dy = self.delta - if self.frame == 60: self.speed_interpolator = Interpolator((0.,), 60, (3.,), 180) @@ -134,7 +133,6 @@ class Item(object): 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.delta = dx, dy self.x += dx self.y += dy diff --git a/pytouhou/game/player.py b/pytouhou/game/player.py --- a/pytouhou/game/player.py +++ b/pytouhou/game/player.py @@ -20,9 +20,6 @@ from pytouhou.game.bullettype import Bul from math import pi -SQ2 = 2. ** 0.5 / 2. - - class PlayerState(object): def __init__(self, character=0, score=0, power=0, lives=0, bombs=0): self.character = character # ReimuA/ReimuB/MarisaA/MarisaB/... @@ -46,21 +43,20 @@ class PlayerState(object): class Player(object): - def __init__(self, state, game, anm_wrapper, speed=4., hitbox_size=2.5, graze_hitbox_size=42.): + def __init__(self, state, game, anm_wrapper, hitbox_size=2.5, graze_hitbox_size=42., speeds=None): self._sprite = None self._anmrunner = None self._game = game self.anm_wrapper = anm_wrapper - self.speed = speed - self.focused_speed = speed/2. + self.speeds = speeds self.hitbox_size = hitbox_size self.hitbox_half_size = self.hitbox_size / 2. self.graze_hitbox_size = graze_hitbox_size self.graze_hitbox_half_size = self.graze_hitbox_size / 2. - self.bullet_type = BulletType(anm_wrapper, 64, 96, 0, 0, 0, hitbox_size=4, damage=48) + self.bullet_type = BulletType(anm_wrapper, 64, 96, 0, 0, 0, hitbox_size=4) self.bullet_launch_interval = 5 self.bullet_speed = 12. self.bullet_launch_angle = -pi/2 @@ -113,16 +109,13 @@ class Player(object): def update(self, keystate): if self.death_time == 0 or self._game.frame - self.death_time > 60: + speed, diag_speed = self.speeds[2:] if self.state.focused else self.speeds[:2] try: - dx, dy = {16: (0.0, -1.0), 32: (0.0, 1.0), 64: (-1.0, 0.0), 128: (1.0, 0.0), - 16|64: (-SQ2, -SQ2), 16|128: (SQ2, -SQ2), - 32|64: (-SQ2, SQ2), 32|128: (SQ2, SQ2)}[keystate & (16|32|64|128)] + dx, dy = {16: (0.0, -speed), 32: (0.0, speed), 64: (-speed, 0.0), 128: (speed, 0.0), + 16|64: (-diag_speed, -diag_speed), 16|128: (diag_speed, -diag_speed), + 32|64: (-diag_speed, diag_speed), 32|128: (diag_speed, diag_speed)}[keystate & (16|32|64|128)] except KeyError: - speed = 0.0 dx, dy = 0.0, 0.0 - else: - speed = self.focused_speed if keystate & 4 else self.speed - dx, dy = dx * speed, dy * speed if dx < 0 and self.direction != -1: self.set_anim(1) diff --git a/pytouhou/games/eosd.py b/pytouhou/games/eosd.py --- a/pytouhou/games/eosd.py +++ b/pytouhou/games/eosd.py @@ -24,6 +24,9 @@ from pytouhou.game.orb import Orb from math import pi +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',)) @@ -58,8 +61,8 @@ class EoSDGame(Game): class EoSDPlayer(Player): - def __init__(self, state, game, anm_wrapper, speed=4., hitbox_size=2.5, graze_hitbox_size=42.): - Player.__init__(self, state, game, anm_wrapper, speed=speed) + def __init__(self, state, game, anm_wrapper, speeds=None, hitbox_size=2.5, graze_hitbox_size=42.): + Player.__init__(self, state, game, anm_wrapper, speeds=speeds) self.orbs = [Orb(self.anm_wrapper, 128, self.state, self.orb_fire), Orb(self.anm_wrapper, 129, self.state, self.orb_fire)] @@ -122,7 +125,7 @@ class Reimu(EoSDPlayer): anm_wrapper = resource_loader.get_anm_wrapper(('player00.anm',)) self.bullet_angle = pi/30 #TODO: check - EoSDPlayer.__init__(self, state, game, anm_wrapper, speed=4.) + EoSDPlayer.__init__(self, state, game, anm_wrapper, speeds=(4., 4. * SQ2, 2., 2. * SQ2)) def fire(self): @@ -149,7 +152,7 @@ class Reimu(EoSDPlayer): bullets.append(Bullet((self.x, self.y), self.bullet_type, 0, bullet_angle, self.bullet_speed, (0, 0, 0, 0, 0., 0., 0., 0.), - 0, self, self._game, player_bullet=True)) + 0, self, self._game, damage=48, player_bullet=True)) bullet_angle += self.bullet_angle for orb in self.orbs: @@ -161,7 +164,7 @@ class ReimuA(Reimu): def __init__(self, state, game, resource_loader): Reimu.__init__(self, state, game, resource_loader) - self.bulletA_type = BulletType(self.anm_wrapper, 65, 97, 0, 0, 0, hitbox_size=4, damage=14) #TODO: verify the hitbox. + self.bulletA_type = BulletType(self.anm_wrapper, 65, 97, 0, 0, 0, hitbox_size=4) #TODO: verify the hitbox, damage is 14. self.bulletA_speed = 12. @@ -180,7 +183,7 @@ class ReimuB(Reimu): def __init__(self, state, game, resource_loader): Reimu.__init__(self, state, game, resource_loader) - self.bulletB_type = BulletType(self.anm_wrapper, 66, 98, 0, 0, 0, hitbox_size=4, damage=12) #TODO: verify the hitbox. + self.bulletB_type = BulletType(self.anm_wrapper, 66, 98, 0, 0, 0, hitbox_size=4) #TODO: verify the hitbox. self.bulletB_speed = 22. @@ -194,7 +197,7 @@ class ReimuB(Reimu): bullets.append(Bullet((orb.x + offset_x, orb.y), self.bulletB_type, 0, self.bullet_launch_angle, self.bulletB_speed, (0, 0, 0, 0, 0., 0., 0., 0.), - 0, self, self._game, player_bullet=True)) + 0, self, self._game, damage=12, player_bullet=True)) def orb_fire(self, orb): if self.state.power < 8: @@ -240,7 +243,7 @@ class Marisa(EoSDPlayer): anm_wrapper = resource_loader.get_anm_wrapper(('player01.anm',)) self.bullet_angle = pi/40 #TODO: check - EoSDPlayer.__init__(self, state, game, anm_wrapper, speed=5.) + EoSDPlayer.__init__(self, state, game, anm_wrapper, speeds=(5., 5. * SQ2, 2.5, 2.5 * SQ2)) def fire(self): @@ -265,7 +268,7 @@ class Marisa(EoSDPlayer): bullets.append(Bullet((self.x, self.y), self.bullet_type, 0, bullet_angle, self.bullet_speed, (0, 0, 0, 0, 0., 0., 0., 0.), - 0, self, self._game, player_bullet=True)) + 0, self, self._game, damage=48, player_bullet=True)) bullet_angle += self.bullet_angle @@ -275,7 +278,7 @@ class MarisaA(Marisa): Marisa.__init__(self, state, game, resource_loader) #TODO: verify the hitbox and damages. - self.bulletA_types = [BulletType(self.anm_wrapper, 65, 0, 0, 0, 0, hitbox_size=4, damage=40), + self.bulletA_types = [BulletType(self.anm_wrapper, 65, 0, 0, 0, 0, hitbox_size=4), # damage is 40. BulletType(self.anm_wrapper, 66, 0, 0, 0, 0, hitbox_size=4), BulletType(self.anm_wrapper, 67, 0, 0, 0, 0, hitbox_size=4), BulletType(self.anm_wrapper, 68, 0, 0, 0, 0, hitbox_size=4)] diff --git a/pytouhou/games/pcb.py b/pytouhou/games/pcb.py new file mode 100644 --- /dev/null +++ b/pytouhou/games/pcb.py @@ -0,0 +1,142 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2011 Emmanuel Gil Peyrot +## +## 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.utils.interpolator import Interpolator + +from pytouhou.game.game import Game +from pytouhou.game.bullettype import BulletType +from pytouhou.game.itemtype import ItemType +from pytouhou.game.player import Player +from pytouhou.game.bullet import Bullet +from pytouhou.game.orb import Orb + +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)] + #TODO: hitbox + 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 = [] + for player in player_states: + players.append(PCBPlayer(player, self, resource_loader)) + + Game.__init__(self, resource_loader, players, stage, rank, difficulty, + bullet_types, item_types, nb_bullets_max=640, **kwargs) + + + +class PCBPlayer(Player): + def __init__(self, state, game, resource_loader, speed=4., hitbox_size=2.5, graze_hitbox_size=42.): + number = '%d%s' % (state.character // 2, 'b' if state.character % 2 else 'a') + self.sht = resource_loader.get_sht('ply0%s.sht' % number) + self.focused_sht = resource_loader.get_sht('ply0%ss.sht' % number) + anm_wrapper = resource_loader.get_anm_wrapper(('player0%d.anm' % (state.character // 2),)) + + Player.__init__(self, state, game, anm_wrapper, + speeds=(self.sht.horizontal_vertical_speed, + self.sht.diagonal_speed, + self.sht.horizontal_vertical_focused_speed, + self.sht.diagonal_focused_speed)) + + self.orbs = [Orb(self.anm_wrapper, 128, self.state, None), + Orb(self.anm_wrapper, 129, self.state, None)] + + self.orbs[0].offset_x = -24 + self.orbs[1].offset_x = 24 + + self.orb_dx_interpolator = None + self.orb_dy_interpolator = None + + + def start_focusing(self): + self.orb_dx_interpolator = Interpolator((24,), self._game.frame, + (8,), self._game.frame + 8, + lambda x: x ** 2) + self.orb_dy_interpolator = Interpolator((0,), self._game.frame, + (-32,), self._game.frame + 8) + self.state.focused = True + + + def stop_focusing(self): + self.orb_dx_interpolator = Interpolator((8,), self._game.frame, + (24,), self._game.frame + 8, + lambda x: x ** 2) + self.orb_dy_interpolator = Interpolator((-32,), self._game.frame, + (0,), self._game.frame + 8) + self.state.focused = False + + + def objects(self): + return self.orbs + + + def update(self, keystate): + Player.update(self, keystate) + + if self.death_time == 0 or self._game.frame - self.death_time > 60: + if self.orb_dx_interpolator: + self.orb_dx_interpolator.update(self._game.frame) + dx, = self.orb_dx_interpolator.values + self.orbs[0].offset_x = -dx + self.orbs[1].offset_x = dx + if self.orb_dy_interpolator: + self.orb_dy_interpolator.update(self._game.frame) + dy, = self.orb_dy_interpolator.values + self.orbs[0].offset_y = dy + self.orbs[1].offset_y = dy + + for orb in self.orbs: + orb.update() + + + def fire(self): + sht = self.focused_sht if self.state.focused else self.sht + power = min(power for power in sht.shots if self.state.power < power) + + bullets = self._game.players_bullets + nb_bullets_max = self._game.nb_bullets_max + + for shot in sht.shots[power]: + if self.fire_time % shot.interval == 0: + if nb_bullets_max is not None and len(bullets) == nb_bullets_max: + break + + origin = self.orbs[shot.orb - 1] if shot.orb else self + x = origin.x + shot.pos[0] + y = origin.y + shot.pos[1] + + bullets.append(Bullet((x, y), self.bullet_type, 0, + shot.angle, shot.speed, + (0, 0, 0, 0, 0., 0., 0., 0.), + 0, self, self._game, player_bullet=True, damage=shot.damage, hitbox=shot.hitbox)) diff --git a/pytouhou/resource/loader.py b/pytouhou/resource/loader.py --- a/pytouhou/resource/loader.py +++ b/pytouhou/resource/loader.py @@ -5,6 +5,7 @@ from pytouhou.formats.std import Stage from pytouhou.formats.ecl import ECL from pytouhou.formats.anm0 import Animations from pytouhou.formats.msg import MSG +from pytouhou.formats.sht import SHT from pytouhou.resource.anmwrapper import AnmWrapper @@ -44,6 +45,7 @@ class Loader(object): self.instanced_anms = {} self.instanced_stages = {} self.instanced_msgs = {} + self.instanced_shts = {} def scan_archives(self, paths): @@ -93,6 +95,13 @@ class Loader(object): return self.instanced_msgs[name] + def get_sht(self, name): + if name not in self.instanced_shts: + file = self.get_file(name) + self.instanced_shts[name] = SHT.read(file) #TODO: modular + return self.instanced_shts[name] + + def get_anm_wrapper(self, names): return AnmWrapper(self.get_anm(name) for name in names)