From: Chip Black Date: Sun, 8 Jun 2008 09:16:20 +0000 (-0500) Subject: Initial commit from version 1e-99 X-Git-Url: http://git.bytex64.net/?a=commitdiff_plain;h=refs%2Fheads%2Fmaster;p=beatscape.git Initial commit from version 1e-99 --- 60b81d82978fcc83379e373d89c824987fb18760 diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..2b940a4 --- /dev/null +++ b/COPYING @@ -0,0 +1,347 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +The Free Software Foundation has exempted Bash from the requirement of +Paragraph 2c of the General Public License. This is to say, there is +no requirement for Bash to print a notice when it is started +interactively in the usual way. We made this exception because users +and standards expect shells not to print such messages. This +exception applies to any program that serves as a shell and that is +based primarily on Bash as opposed to other GNU software. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + Appendix: How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) 19yy + + 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; either version 2 of the License, or + (at your option) any later version. + + 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. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) 19yy name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..492a2a9 --- /dev/null +++ b/README.txt @@ -0,0 +1,73 @@ +beatscape - the red-headed stepchild of all vertical column rhythm games + +beatscape is a simulator for vertical column rhythm games. Right now, it +simulates 7-key beatmania IIDX (and, by induction, 5-key beatmania), but +plans to support the whole line from Pop'n Music to Keyboardmania. It +does *not* simulate any arrow games (DDR, ITG, Pump it Up, etc). If you +want that, go use StepMania. Right now it reads BMS/BME (pretty well) +and StepMania SM (poorly). + +beatscape requires: + pygame of a recent vintage (I'm using 1.7.1) + Python 2.4 + a little bit of a clue + +USAGE + +Add your songs to the 'songs' folder. There is no restriction on the +placement of files, other than that all files referenced by a keyfile +(WAV,BMP,etc) must be in the same folder as that keyfile. I like to +arrange my keyfiles logically by source, but you can do whatever you +want. + +beatscape does not automatically index your music files (yet), so you +will have to do this manually each time you add or remove songs by +running 'mksongcache.py'. + +beatscape accepts a command line argument specifying a keyfile to play. +This is handy if you're testing a keyfile and don't want to navigate the +menus. + +FILES + +config.py + This lists configuration variables for various aspects of the + game, such as timings and button assignments. Timings are set to + what I think is a reasonable approximation of the console + versions of IIDX, and button assignments should work with your + standard IIDX controller hooked up via a PSX -> USB adapter. + Other controllers and adapters will likely require tweaking. + +There is not (as yet) any per-user configuration or way to install +beatscape system-wide. Feel free to contribute. :) + +BUGS + +Oh, god, too many to mention, but these are extremely noticable: +* bpm-change problems on some songs (notably both era mixes) +* sometimes a song will just decide not to work after you've selected + it. If you select it again, it will probably work. +* A lot of undocumented features of the BMS/BME format are still + unhandled, and will likely cause errors or crashes. If anyone can + point me to a definitive BMS/BME reference, I'd be much obliged! +* Sometimes samples will just fail to play. I'm not yet sure if this is + a fault of my engine or of pygame. +* There's no way to adjust any game options, especially speed (without + hacking the source). +* It's visually abominable. + +LICENSE + +beatscape is released under the GPLv2. Details can be found in the COPYING file. + +OTHER + +beatscape might run on windows. I've only tested this a little bit, and it does +run, but the sound is usually lagged and choppy. + +CONTACT + +I can be contacted at bytex64@bytex64.net if you have any bug reports, +questions, fixes, or other contributions. + +Thanks, and have fun! diff --git a/accuracygraph.py b/accuracygraph.py new file mode 100644 index 0000000..0de687e --- /dev/null +++ b/accuracygraph.py @@ -0,0 +1,36 @@ +import pygame +from bmevent import * +from keyhandler import * + +class AccuracyGraph: + linecolor = (255,255,0) + tickcolor = (200,200,200) + accpoints = [-90,-60,-30,-15,0,15,30,60,90] + + def __init__(self,rect): + self.screen = pygame.display.get_surface() + self.disprect = rect + self.datapoints = [0] + self.ratio = rect.height / 100.0 + self.tickpoints = map(lambda y: (y * self.ratio) + (rect.height/2), self.accpoints) + self.n_hits = 0 + self.total = 0 + + def addpoint(self,point): + self.datapoints[0] = point + self.total += point + self.n_hits += 1 + return self.total / float(self.n_hits) + + def draw(self): + self.screen.fill((0,0,0), self.disprect) + for t in self.tickpoints: + y = self.disprect.top + t + pygame.draw.line(self.screen, self.tickcolor, + (self.disprect.left, y), (self.disprect.right-1, y)) + if abs(self.datapoints[0]) > 100: + return + y = self.disprect.height / 2 + self.disprect.top + (self.datapoints[0] * self.ratio) + pygame.draw.line(self.screen, self.linecolor, + (self.disprect.left, y), (self.disprect.right, y),2) + diff --git a/beatscape.py b/beatscape.py new file mode 100755 index 0000000..c2f84e0 --- /dev/null +++ b/beatscape.py @@ -0,0 +1,67 @@ +#!/usr/bin/python +# beatscape is the mother of all musical button games. + +import sys +import pygame +from keygraph import * +from keyfile import * +from bmevent import * +from chanman import * +from formats import * +from keyhandler import * +import game + +try: + import config +except ImportError: + print "Could not parse config.py. Maybe you forgot a semicolon? :-P" + exit(1) + +song = '' + +if len(sys.argv) > 1: + song = sys.argv[1] + +try: + import psyco + print "w00t! going psyco!" + psyco.full() +except ImportError: + pass + +pygame.mixer.pre_init(44100, -16, True, 1) +pygame.init() +opts = pygame.FULLSCREEN | pygame.DOUBLEBUF | pygame.HWSURFACE +pygame.display.set_mode((640,480), opts) +screen = pygame.display.get_surface() + +for n in range(0,pygame.joystick.get_count()): + pygame.joystick.Joystick(n).init(); + +if song: + # For now, assume IIDX. We'll make this smarter later. + game.playgame(gametypes['IIDX'],song) + exit(0) + +from mainmenu import * +from fileselect import * + +l = ['mainmenu'] + +while 1: + obj = None + + if l[0] == 'mainmenu': + obj = MainMenu() + elif l[0] == 'fileselect': + obj = FileSelect() + elif l[0] == 'play': + obj = game.Game(l[1]) + elif l[0] == 'quit': + sys.exit(0) + + l = obj.run() + # Flush events before switching to the next mode + pygame.event.clear() + +pygame.quit() diff --git a/bmevent.py b/bmevent.py new file mode 100644 index 0000000..b6cc994 --- /dev/null +++ b/bmevent.py @@ -0,0 +1,36 @@ +BME_BGM = 0x01 +BME_TEMPO = 0x02 +BME_BGA = 0x04 +BME_CHPOOR = 0x08 +BME_NOTE1 = 0x10 +BME_NOTE2 = 0x20 +BME_HIT = 0x40 +BME_STOP = 0x80 +BME_LONGMEASURE = 0x100 + +class BMEvent: + def __init__(self,beat,type,key=None,dataref=None): + self.beat = beat + self.type = type + self.key = key + self.dataref = dataref + + def __str__(self): + s = "BMEvent:\n" + s += " beat=" + str(self.beat) + "\n" + t = { BME_BGM: "BGM", + BME_TEMPO: "TEMPO", + BME_BGA: "BGA", + BME_CHPOOR: "CHANGE POOR", + BME_NOTE1: "NOTE P1", + BME_NOTE2: "NOTE P2" + }[self.type] + s += " type=" + t + "\n" + if self.key: + s += " key=" + str(self.key) + "\n" + if self.dataref: + try: + s += " dataref=" + hex(self.dataref) + "\n" + except TypeError: + s += " dataref=" + str(self.dataref) + "\n" + return s diff --git a/chanman.py b/chanman.py new file mode 100644 index 0000000..1323eac --- /dev/null +++ b/chanman.py @@ -0,0 +1,17 @@ +from bmevent import * +import pygame + +class ChannelManager: + keyfile = None + chanmap = None + + def __init__(self,keyfile): + self.keyfile = keyfile + self.chanmap = [pygame.mixer.Channel(n) for n in range(0,self.keyfile.numkeys)] + pygame.mixer.set_num_channels(64) + + def play(self,bme): + if bme.type & (BME_NOTE1 | BME_NOTE2 | BME_BGM | BME_HIT): + if bme.dataref > 0 and bme.dataref in self.keyfile.wavs: + self.keyfile.wavs[bme.dataref].play() + diff --git a/config.py b/config.py new file mode 100644 index 0000000..23f6dd4 --- /dev/null +++ b/config.py @@ -0,0 +1,36 @@ +# Configuration. Edit this file by hand, but beware, it is a python file +# and the game won't start if it does not parse correctly. + +# These are modules necessary for certain constants used here +from pygame.locals import * +from constants import * + +# Main config, for global options +librarypath = "/media/audio/BMS/library" +keytimeout = 250 # milliseconds + +# Game config, for various game types + +bm = { + 'format': 'IIDXFormat', + 'keymap': + {K_SPACE:0, K_z:1, K_s:2, K_x:3, K_d:4, K_c:5, K_f:6, K_v:7}, + 'jsbuttonmap': + { 3: 1, 6: 2, 2: 3, 7: 4, 1: 5, 4: 6 }, + 'jsaxismap': + { (0,-1.0): 7, (0,0.0): 7, (1,-1.0): 0, (1,0.0): 0, (1,1.0): 0 }, + 'timings': + #[(15,JUDGE_FGREAT), (35,JUDGE_GREAT), (50,JUDGE_GOOD), (80,JUDGE_BAD), (100,JUDGE_POOR)] + [(20,JUDGE_FGREAT), (42,JUDGE_GREAT), (70,JUDGE_GOOD), (250,JUDGE_BAD), (1000,JUDGE_POOR)] +} + +popn = { + 'format': 'PopnFormat', +} + +# game type mapping +gametypes = { + 'beatmania': bm, # For now + 'IIDX': bm, + 'Popn': popn +} diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..8d84fef --- /dev/null +++ b/constants.py @@ -0,0 +1,8 @@ +# Constants used everywhere + +JUDGE_FGREAT = 1 +JUDGE_GREAT = 2 +JUDGE_GOOD = 3 +JUDGE_BAD = 4 +JUDGE_POOR = 5 +JUDGE_NA = 6 diff --git a/event.py b/event.py new file mode 100644 index 0000000..09b233c --- /dev/null +++ b/event.py @@ -0,0 +1,49 @@ +import pygame +from pygame.locals import * +import config + +LEFT = 0 +RIGHT = 1 +UP = 2 +DOWN = 3 +OK = 4 +CANCEL = 5 +OPTION = 6 + +keymap = {K_UP: UP, K_DOWN: DOWN, K_LEFT: LEFT, K_RIGHT: RIGHT, K_RETURN: OK, K_ESCAPE: CANCEL} +jsamap = {(0,-1.0): OK, (1,-1.0): DOWN, (1,1.0): UP } +jsbmap = {3: OK, 6: CANCEL, 2: OK, 7: CANCEL, 1: OK, 4: CANCEL, 10: OPTION, 9: OK} + +def parseevent(e): + try: + if e.type == KEYDOWN: + return keymap[e.key] + elif e.type == JOYBUTTONDOWN: + return jsbmap[e.button] + elif e.type == JOYAXISMOTION: + if e.value != 0.0: + return jsamap[(e.axis,e.value)] + else: + return None + except KeyError: + return None + +last_action = None +last_action_time = 0 + +def getaction(): + global last_action, last_action_time + + e = pygame.event.poll() + if e.type != NOEVENT: + last_action = parseevent(e) + if last_action: + last_action_time = pygame.time.get_ticks() + return last_action + else: + if pygame.time.get_ticks() - last_action_time > config.keytimeout: + last_action_time = pygame.time.get_ticks() + # Only repeat for directionals + if last_action in (LEFT,RIGHT,UP,DOWN): + return last_action + return None diff --git a/fileselect.py b/fileselect.py new file mode 100644 index 0000000..a21997a --- /dev/null +++ b/fileselect.py @@ -0,0 +1,296 @@ +import pygame +import cPickle +import os, os.path, re, math + +from keyfile import * +from fx import * +import event + +fileroot = "songs" +songcache = 'songcache.pickle' +possave = 0 + +def mksongcache(): + songs = FileNode() + songs.crawl(fileroot) + f = open(songcache,'w') + cPickle.dump(songs,f) + f.close() + return songs + +filetile = pygame.image.load("gfx/filetile.png") + +class SelectWidget(FX): + color = (240,240,255) + basecolor = (200,0,0) + fadetime = 700 + size = (400,30) + + OFF = 0 + ON = 1 + FADING = 2 + + def __init__(self,text,location=(0,0)): + FX.__init__(self) + f = pygame.font.Font('font/Nano.ttf',16) + self.text = f.render(text,True,self.color) + self.surface = pygame.Surface(self.size).convert_alpha() + self.surface.fill( (0,0,0,0) ) + self.surface.blit(filetile,(0,0)) + self.surface.blit(self.text, (9,7)) + self.pointlist = [(10,0),(self.size[0]-2,0),(self.size[0]-2,self.size[1]-2),(0,self.size[1]-2),(0,10)] + self.state = self.OFF + self.location = location + + def off(self): + if self.state == self.ON: + self.state = self.FADING + self.fade_t = pygame.time.get_ticks() + + def on(self): + self.state = self.ON + + def draw(self,t): + self.screen.blit(self.surface,self.location) + color = self.basecolor + if self.state == self.ON: + color = self.color + if self.state == self.FADING: + dt = pygame.time.get_ticks() - self.fade_t + if dt < self.fadetime: + alpha = math.exp(-0.003*dt) + color = [(1.0-alpha) * self.basecolor[n] + alpha * self.color[n] for n in range(0,3)] + else: + self.state = self.OFF + pointlist = [(x[0] + self.location[0],x[1] + self.location[1]) for x in self.pointlist] + pygame.draw.lines(self.screen,color,True,pointlist,2) + +# This would look a lot less ugly if only there were something like +# perl's cmp and <=> operators. +# FUCK YOU PYTHON WHY DOES THIS HAVE TO BE SO COMPLICATED!?!?!?! +def WTFsort(a,b): + if a.type == a.DIRECTORY and b.type == b.DIRECTORY: + if a.name > b.name: + return 1 + elif a.name < b.name: + return -1 + else: + return 0 + elif a.type == a.DIRECTORY and b.type == b.FILE: + return 1 + elif a.type == a.FILE and b.type == b.DIRECTORY: + return -1 + else: + if a.info['player'] > b.info['player']: + return 1 + elif a.info['player'] < b.info['player']: + return -1 + else: + if not 'playlevel' in a.info: + return -1 + if not 'playlevel' in b.info: + return 1 + if a.info['playlevel'] > b.info['playlevel']: + return 1 + elif a.info['playlevel'] < b.info['playlevel']: + return -1 + else: + if not 'total' in a.info: + return -1 + if not 'total' in b.info: + return 1 + if a.info['total'] > b.info['total']: + return 1 + elif a.info['total'] < b.info['total']: + return -1 + else: + return 0 + +filematch = re.compile('\.(bms|bme|sm)$',re.I) + +class FileNode: + DIRECTORY = 0 + FILE = 1 + + def __init__(self): + self.children = [] + self.open = False + self.info = None + self.widget = None + + def crawl(self,path): + print "Crawling " + path + self.path = path + if os.path.isdir(path): + self.type = self.DIRECTORY + self.name = os.path.basename(path) + files = filter(lambda x: os.path.isdir(os.path.join(path,x)) or filematch.search(x), os.listdir(path)) + for f in files: + c = FileNode() + if c.crawl(os.path.join(path,f)): + self.children.append(c) + self.children.sort(WTFsort) + elif os.path.isfile(path): + self.type = self.FILE + self.info = kf_info(path) + self.name = self.info['title'] + else: + print path + " isn't a file or a directory. Ignoring." + return False + return True + + def update(self): + for c in self.children: + if not os.path.exists(c.path): + self.children.remove(c) + else: + c.update() + + def add(self,child): + self.children.append(child) + + def tolist(self,depth=0): + self.depth = depth + l = [self] + if self.open: + for c in self.children: + r = c.tolist(depth+1) + for i in r: + l.append(i) + return l + + def getwidget(self): + if not self.widget: + self.widget = SelectWidget(self.name) + return self.widget + + def nukewidgets(self): + self.widget = None + for c in self.children: + c.nukewidgets() + +class FileSelect: + + def __init__(self): + self.screen = pygame.display.get_surface() + try: + f = open(songcache,'r') + self.songs = cPickle.load(f) + f.close() + except IOError: + self.songs = mksongcache() + except cPickle.UnpicklingError: + self.songs = mksongcache() + + self.period = 428 + self.songlist = self.songs.tolist() + try: + self.position = self.songlist.index(self.songs.current) + except AttributeError: + self.position = 0 + except ValueError: + self.position = 0 + + self.fslabel = Text('file select','font/Nano.ttf',30,(330,30),(255,0,0)) + self.fadebottom = pygame.Surface((640,200)).convert_alpha() + for n in range(0,200): + alpha = int((1.0 - math.exp(-n/30.0)) * 255.0) + color = (0,0,0,alpha) + pygame.draw.line(self.fadebottom,color,(0,n),(640,n)) + self.labels = {} + self.olabels = {} + self.olabels['title'] = Text('title','font/Nano.ttf',12, (20,320),(255,130,130)) + self.labels['title'] = Text('','font/Nano.ttf',18, (30,330),(230,230,255)) + self.olabels['artist'] = Text('artist','font/Nano.ttf',12, (20,360),(255,130,130)) + self.labels['artist'] = Text('','font/Nano.ttf',16, (30,370),(230,230,255)) + self.olabels['genre'] = Text('genre','font/Nano.ttf',12, (20,390),(255,130,130)) + self.labels['genre'] = Text('','font/Nano.ttf',16, (30,400),(230,230,255)) + self.olabels['playlevel'] = Text('difficulty','font/Nano.ttf',12, (20,420),(255,130,130)) + self.labels['playlevel'] = Text('','font/Nano.ttf',16, (30,430),(230,230,255)) + self.olabels['bpm'] = Text('BPM','font/Nano.ttf',12, (20,450),(255,130,130)) + self.labels['bpm'] = Text('','font/Nano.ttf',16, (30,460),(230,230,255)) + self.calcsongwidgets() + + def draw(self,t): + self.screen.fill( (30,0,0) ) + l = len(self.songlist) + for nu in range(self.position-4,self.position+8): + n = nu % l + self.songlist[n].getwidget().draw(t) + + self.screen.blit(self.fadebottom,(0,280)) + for k in self.olabels.keys(): + self.olabels[k].draw(t) + for k in self.labels.keys(): + self.labels[k].draw(t) + self.fslabel.draw(t) + + def calcsongwidgets(self): + l = len(self.songlist) + for nu in range(self.position-4,self.position+8): + n = nu % l + w = self.songlist[n].getwidget() + w.location = (10 + self.songlist[n].depth * 20, 150 - (self.position - nu)*30) + if n == self.position: + w.on() + if self.songlist[n].type == FileNode.DIRECTORY: + for tag in ('artist','genre','playlevel','bpm'): + self.labels[tag].settext('') + self.labels['title'].settext(self.songlist[n].name) + else: + for tag in ('title','artist','genre','bpm'): + self.labels[tag].settext(self.songlist[n].info[tag]) + try: + playlevel = int(self.songlist[n].info['playlevel']) + self.labels['playlevel'].settext(("*" * playlevel) + "(" + str(playlevel) + ")") + except ValueError: + self.labels['playlevel'].settext(str(self.songlist[n].info['playlevel'])) + else: + w.off() + + def run(self): + global possave + decided = 0 + pygame.mixer.music.load('snd/0x01.ogg') + pygame.mixer.music.set_volume(0.75) + pygame.mixer.music.play(-1) + stab = pygame.mixer.Sound('snd/stab.ogg') + stab.set_volume(0.25) + start_t = pygame.time.get_ticks() + + while decided == 0: + t = pygame.time.get_ticks() - start_t + act = event.getaction() + while act: + if act == event.CANCEL: + return ['mainmenu'] + elif act == event.DOWN: + self.position += 1 + self.calcsongwidgets() + elif act == event.UP: + self.position -= 1 + self.calcsongwidgets() + elif act == event.OK: + if self.songlist[self.position].type == FileNode.DIRECTORY: + self.songlist[self.position].open = not self.songlist[self.position].open + self.songlist = self.songs.tolist() + self.calcsongwidgets() + else: + decided = self.position + act = event.getaction() + + self.position = self.position % len(self.songlist) + self.draw(t) + pygame.display.flip() + + pygame.mixer.music.fadeout(1500) + stab.play() + + self.songs.nukewidgets() + self.songs.current = self.songlist[self.position] + f = open(songcache,'w') + cPickle.dump(self.songs,f) + f.close() + + #possave = self.position + return ['play',self.songlist[decided].path] diff --git a/font/Nano.ttf b/font/Nano.ttf new file mode 100644 index 0000000..0c52860 Binary files /dev/null and b/font/Nano.ttf differ diff --git a/formats.py b/formats.py new file mode 100644 index 0000000..7b2d221 --- /dev/null +++ b/formats.py @@ -0,0 +1,114 @@ +import pygame +from keygraph import * +from keyhandler import * +from rotowidget import * +from fx import * +from constants import * + +red = (255,30,30) +offwhite = (245,245,245) +blue = (30,30,255) +green = (30,255,30) +black = (0,0,0) +darkgray = (20,20,20) +ggray = (200,255,200) + +class IIDXFormat: + keyfile = None + keystyle = [(1.75,red,black,ggray), (1,offwhite,darkgray,blue), + (0.8,blue,black,green), (1,offwhite,darkgray,blue), + (0.8,blue,black,green), (1,offwhite,darkgray,blue), + (0.8,blue,black,green), (1,offwhite,darkgray,blue)] + + def __init__(self,keyfile): + self.keyfile = keyfile + self.kg = (KeyGraph(keyfile,self.keystyle,1,pygame.Rect(0,0,192,400)), + KeyGraph(keyfile,self.keystyle,2,pygame.Rect(448,0,192,400)) ) + self.animpic = None + self.screen = pygame.display.get_surface() + self.roto = RotoWidget((30,430),25,1.0) + self.songlength = keyfile.length() + self.groove = 20.0 + self.groovetext = GlyphText((60,415)) + self.accuracy = 0 + self.nacc = 0 + + def setproperties(self,properties): + if properties.has_key('judgement'): + j = properties['judgement'] + if j == JUDGE_FGREAT: + self.groove += 1.0 + elif j == JUDGE_GREAT: + self.groove += 0.7 + elif j == JUDGE_GOOD: + self.groove += 0.2 + elif j == JUDGE_BAD: + self.groove -= 2.0 + elif j == JUDGE_POOR: + self.groove -= 6.0 + if self.groove > 100.0: + self.groove = 100.0 + elif self.groove < 0.0: + self.groove = 0.0 + if 'accuracy' in properties: + self.accuracy += properties['accuracy'] + self.nacc += 1 + print "accuracy:", self.accuracy / float(self.nacc) + self.kg[0].setproperties(properties) + + def draw(self, t, bmelist): + b = self.keyfile.eval_beatfunc(t) + for bme in bmelist: + if bme.type == BME_BGA and bme.dataref in self.keyfile.bmps: + self.animpic = self.keyfile.bmps[bme.dataref] + self.screen.fill( (20,20,20) ) + for k in self.kg: + k.t = t + k.draw() + if self.animpic: + self.screen.fill((0,0,0),(192,112,256,256)) + self.screen.blit(self.animpic,(192,112)) + self.roto.pulsev = self.groove + self.roto.arcv = (float(t) / self.songlength) * 100 + self.roto.draw(b) + self.groovetext.settext("%02d" % self.groove) + self.groovetext.draw(t) + pygame.display.update() + +lightgray = (230,230,230) +graybg = (30,30,30) +yellow = (230,230,0) +yellowbg = (30,30,0) +green = (30,255,30) +greenbg = (0,30,0) +blue = (0,0,230) +bluebg = (0,0,30) +redbg = (30,0,0) + +class PopnFormat: + keyfile = None + keystyle = [(1,lightgray,graybg), (0.7,yellow,yellowbg), + (1,green,greenbg), (0.7,blue,bluebg), (1,red,redbg), + (0.7,blue,bluebg), (1,green,greenbg), (0.7,yellow,yellowbg), + (1,lightgray,graybg)] + + def __init__(self,keyfile): + self.keyfile = keyfile + self.kg = KeyGraph(keyfile,self.keystyle,1,pygame.Rect(256,0,384,400)) + self.kg.bordercolor = black + + self.animpic = None + self.screen = pygame.display.get_surface() + + def draw(self, t, bmelist): + for bme in bmelist: + if bme.type == BME_BGA: + self.animpic = self.keyfile.bmps[bme.dataref] + self.screen.fill( (20,20,20) ) + self.kg.t = t + self.kg.draw() + if self.animpic: + self.screen.fill((0,0,0),(0,112,256,256)) + self.screen.blit(self.animpic,(0,112)) + pygame.display.update() + diff --git a/fx.py b/fx.py new file mode 100644 index 0000000..4b608d6 --- /dev/null +++ b/fx.py @@ -0,0 +1,174 @@ +import pygame +import math +import os, os.path + +ALIGN_LEFT = 0 +ALIGN_RIGHT = 1 + +# FX is the abstract base class for all "effects" objects. They have two +# defining features: +# +# 1) The FX base class sets up self.screen so that you can draw to the +# screen. It is then important to call FX.__init__(self) in your +# __init__ routine. +# 2) Each FX object has a draw(self,t) method that draws itself to the +# screen (possibly with appearance based on the current time). + +class FX: + def __init__(self): + self.screen = pygame.display.get_surface() + + def draw(self,t): + pass + +class Blank(FX): + def draw(self,t): + self.screen.fill((0,0,0)) + +class Image(FX): + align = ALIGN_LEFT + + def __init__(self,file,location=(0,0)): + FX.__init__(self) + self.surface = pygame.image.load(file).convert_alpha() + self.location = location + + def draw(self,t): + if self.align == ALIGN_RIGHT: + rl = (self.location[0] - self.surface.get_width(),self.location[1]) + self.screen.blit(self.surface,rl) + else: + self.screen.blit(self.surface,self.location) + +class Text(Image): + def __init__(self,text,font,size,location=(0,0),color=(255,255,255)): + FX.__init__(self) + self.font = pygame.font.Font(font,size) + self.location = location + self.color = color + self.settext(text) + + def settext(self,text): + f = self.font.render(text,True,(0,0,0)) + self.surface = pygame.surface.Surface((f.get_width()+2,f.get_height()+2)).convert_alpha() + self.surface.fill((0,0,0,0)) + self.surface.blit(f,(2,2)) + f = self.font.render(text,True,self.color) + self.surface.blit(f,(0,0)) + +textglyphs = {} + +class GlyphText(FX): + def __init__(self,location=(0,0),glyphdir='gfx/glyph'): + FX.__init__(self) + if not textglyphs: + l = os.listdir(glyphdir) + for g in l: + c = g.split('.')[0] + textglyphs[c] = pygame.image.load(os.path.join(glyphdir,g)).convert_alpha() + self.location = location + self.text = '' + + def settext(self,str): + self.text = str + + def draw(self,t): + (x,y) = self.location + for c in self.text: + if textglyphs.has_key(c): + self.screen.blit(textglyphs[c],(x,y)) + x += textglyphs[c].get_width() + +class PulseLine(FX): + r = -0.02 + + def __init__(self, pointlist, color, period, maxwidth = 8, minwidth = 1): + FX.__init__(self) + self.period = period + self.color = color + if maxwidth: + self.maxwidth = maxwidth + if minwidth: + self.minwidth = minwidth + self.pointlist = pointlist + self.location = (0,0) + + def draw(self,t): + w = (self.maxwidth - 1) * math.exp(self.r * (t % self.period)) + self.minwidth + pl = map(lambda x: (x[0] + self.location[0],x[1] + self.location[1]),self.pointlist) + pygame.draw.lines(self.screen,self.color, 0, pl, int(w)) + +class ColumnPulse(FX): + r = -0.03 + + def __init__(self,rect,color): + FX.__init__(self) + self.rect = rect + self.color = color + self.st = 0 + self.surf = [] + t = 0 + a = 1.0 + while a > 0.01: + s = pygame.Surface(self.rect.size).convert_alpha() + s.lock() + s.fill( (0,0,0,0) ) + for n in range(0,self.rect.height): + c = (self.color[0],self.color[1],self.color[2],float(n) / self.rect.height * a * 255.0) + w = (self.rect.width / 2) * (1.0 - a) + pygame.draw.line(s,c,(w,n),(self.rect.width-w,n)) + #s.fill(c,(0,n,self.rect.width,1)) + s.unlock() + self.surf.append(s) + t += 16 + a = math.exp(self.r * t) + + self.hit = 0 + + def down(self): + self.hit = 1 + + def up(self): + self.st = pygame.time.get_ticks() + self.hit = 0 + + def draw(self): + dt = pygame.time.get_ticks() - self.st + if self.hit: + self.screen.blit(self.surf[0],self.rect) + else: + n = dt / 16 + if n > len(self.surf) - 1: + return + self.screen.blit(self.surf[n],self.rect) + +class PlaneScroll(FX): + surface = None + + def __init__(self,file,direction,zoom=1.0,alpha=1.0): + FX.__init__(self) + ts = pygame.image.load(file) + if zoom != 1.0: + ts = pygame.transform.rotozoom(ts, 0.0, zoom) + self.surface = ts.convert() + del ts + if alpha < 1.0 and alpha >= 0.0: + self.surface.set_alpha(int(alpha * 255)) + self.direction = direction + + def draw(self,t): + w = self.surface.get_width() + h = self.surface.get_height() + location = self.surface.get_rect() + location.size = (640,480) + location.move_ip((t * self.direction[0]) % w, (t * self.direction[1]) % h) + self.screen.blit(self.surface,(0,0),location) + if location.right > w: + x = 640 - (location.right - w) + self.screen.blit(self.surface,(x,0),(0,location.top,640,480)) + if location.bottom > h: + y = 480 - (location.bottom - h) + self.screen.blit(self.surface,(0,y),(location.left,0,640,480)) + if location.bottom > h and location.right > w: + # x and y must be calculated from above two tests + self.screen.blit(self.surface,(x,y),(0,0,640,480)) diff --git a/game.py b/game.py new file mode 100644 index 0000000..9ee7961 --- /dev/null +++ b/game.py @@ -0,0 +1,132 @@ +import pygame +import thread + +from keygraph import * +from keyfile import * +from bmevent import * +from formats import * +from chanman import * +from keyhandler import * +import config + +events = [] +evlock = thread.allocate_lock() +evrun = 1 +t_orig = 0 + +def event_thread(): + global events,evlock,t_orig + while evrun: + e = pygame.event.wait() + t = pygame.time.get_ticks() - t_orig + evlock.acquire() + events.append((t,e)) + evlock.release() + +def event_thread_start(): + global evrun + evrun = 1 + thread.start_new_thread(event_thread,()) + +def event_thread_stop(): + global evrun + evrun = 0 + pygame.event.post(pygame.event.Event(pygame.NOEVENT,{})) + +def get_events(): + global events,evlock + evlock.acquire() + eold = events + events = [] + evlock.release() + return eold + +def playgame(gameconfig,song): + global t_orig + k = kf_load(song) + + dm = eval(gameconfig['format'] + '(k)'); + cm = ChannelManager(k); + kh = KeyHandler(gameconfig,k); + + # No background anims? No problem! + if not k.bmps: + print "IT'S PEANUT BUTTER JELLY TIME!" + for x in range(0,8): + k.bmps[x] = pygame.transform.scale(pygame.image.load("gfx/banana%d.png" % x).convert(), (256,256)) + lastbeat = k.bmelist[-1].beat + i = 0.0 + j = 0 + while i < lastbeat: + k.add(BMEvent(i,BME_BGA,None,j)) + j = (j+1) % 8 + i += 0.25 + k.sort() + + li = BMEListIter(k.bmelist) + reaper_li = BMEListIter(k.bmelist) + t_finish = k.length() + 5000 + + t_orig = pygame.time.get_ticks() + try: + if k.offset < 0: + t_orig -= int(k.offset * 1000) + except NameError: + pass + b = b_l = k.eval_beatfunc(t_orig) + + event_thread_start() + + animpic = None + while 1: + t = pygame.time.get_ticks() - t_orig + b = k.eval_beatfunc(t) + print t,b,b_l + db = b - b_l + b_l = b + li.goto(b) + bmelist = li.window(db) + dm.draw(t + 45, bmelist) + for bme in bmelist: + if bme.type == BME_BGM: + cm.play(bme); + if bme.type == BME_TEMPO: + bpm = bme.dataref + kh.setproperties({'bpm': bme.dataref}) + + for (t,e) in get_events(): + r = kh.handle(e,t) + if r: + (hitbme, properties) = r + if hitbme: + cm.play(hitbme) + dm.setproperties(properties) + + if e.type == pygame.KEYDOWN: + if e.key == pygame.K_ESCAPE: + event_thread_stop() + pygame.mixer.stop() + return False + elif e.key == pygame.K_PRINT: + pygame.image.save(screen,"screenshot.bmp") + + reaper_li.goto(b - 2.0) + bmelist = reaper_li.window(1.0, BME_NOTE1 | BME_NOTE2) + for bme in bmelist: + dm.setproperties({'judgement': JUDGE_POOR}) + bme.type = BME_HIT + + if t > t_finish: + event_thread_stop() + return True + +# Hmm. This is sort of braindead, isn't it? +class Game: + def __init__(self,song): + self.song = song + + def run(self): + print "Playing",self.song + # IIDX is hardcoded for now + playgame(config.gametypes['IIDX'], self.song) + return ['fileselect'] diff --git a/gfx/bad.png b/gfx/bad.png new file mode 100644 index 0000000..9d852f6 Binary files /dev/null and b/gfx/bad.png differ diff --git a/gfx/banana0.png b/gfx/banana0.png new file mode 100644 index 0000000..1fc979a Binary files /dev/null and b/gfx/banana0.png differ diff --git a/gfx/banana1.png b/gfx/banana1.png new file mode 100644 index 0000000..fef450e Binary files /dev/null and b/gfx/banana1.png differ diff --git a/gfx/banana2.png b/gfx/banana2.png new file mode 100644 index 0000000..00ec334 Binary files /dev/null and b/gfx/banana2.png differ diff --git a/gfx/banana3.png b/gfx/banana3.png new file mode 100644 index 0000000..59636d6 Binary files /dev/null and b/gfx/banana3.png differ diff --git a/gfx/banana4.png b/gfx/banana4.png new file mode 100644 index 0000000..94c407c Binary files /dev/null and b/gfx/banana4.png differ diff --git a/gfx/banana5.png b/gfx/banana5.png new file mode 100644 index 0000000..7ca5579 Binary files /dev/null and b/gfx/banana5.png differ diff --git a/gfx/banana6.png b/gfx/banana6.png new file mode 100644 index 0000000..de4c2dc Binary files /dev/null and b/gfx/banana6.png differ diff --git a/gfx/banana7.png b/gfx/banana7.png new file mode 100644 index 0000000..da45870 Binary files /dev/null and b/gfx/banana7.png differ diff --git a/gfx/chips_layer1.jpg b/gfx/chips_layer1.jpg new file mode 100644 index 0000000..198d5ef Binary files /dev/null and b/gfx/chips_layer1.jpg differ diff --git a/gfx/chips_layer2.jpg b/gfx/chips_layer2.jpg new file mode 100644 index 0000000..abcd3dd Binary files /dev/null and b/gfx/chips_layer2.jpg differ diff --git a/gfx/circle.png b/gfx/circle.png new file mode 100644 index 0000000..28b0d0a Binary files /dev/null and b/gfx/circle.png differ diff --git a/gfx/cross.png b/gfx/cross.png new file mode 100644 index 0000000..5c73448 Binary files /dev/null and b/gfx/cross.png differ diff --git a/gfx/dcircle.png b/gfx/dcircle.png new file mode 100644 index 0000000..70921b6 Binary files /dev/null and b/gfx/dcircle.png differ diff --git a/gfx/fgreat.png b/gfx/fgreat.png new file mode 100644 index 0000000..ca8e3a3 Binary files /dev/null and b/gfx/fgreat.png differ diff --git a/gfx/filetile.png b/gfx/filetile.png new file mode 100644 index 0000000..77e140c Binary files /dev/null and b/gfx/filetile.png differ diff --git a/gfx/glyph/0.png b/gfx/glyph/0.png new file mode 100644 index 0000000..993fe0d Binary files /dev/null and b/gfx/glyph/0.png differ diff --git a/gfx/glyph/1.png b/gfx/glyph/1.png new file mode 100644 index 0000000..40fb9da Binary files /dev/null and b/gfx/glyph/1.png differ diff --git a/gfx/glyph/2.png b/gfx/glyph/2.png new file mode 100644 index 0000000..e5be3d2 Binary files /dev/null and b/gfx/glyph/2.png differ diff --git a/gfx/glyph/3.png b/gfx/glyph/3.png new file mode 100644 index 0000000..6b32519 Binary files /dev/null and b/gfx/glyph/3.png differ diff --git a/gfx/glyph/4.png b/gfx/glyph/4.png new file mode 100644 index 0000000..e8fe6a4 Binary files /dev/null and b/gfx/glyph/4.png differ diff --git a/gfx/glyph/5.png b/gfx/glyph/5.png new file mode 100644 index 0000000..f3948b4 Binary files /dev/null and b/gfx/glyph/5.png differ diff --git a/gfx/glyph/6.png b/gfx/glyph/6.png new file mode 100644 index 0000000..fa5e1a1 Binary files /dev/null and b/gfx/glyph/6.png differ diff --git a/gfx/glyph/7.png b/gfx/glyph/7.png new file mode 100644 index 0000000..f515801 Binary files /dev/null and b/gfx/glyph/7.png differ diff --git a/gfx/glyph/8.png b/gfx/glyph/8.png new file mode 100644 index 0000000..56e877a Binary files /dev/null and b/gfx/glyph/8.png differ diff --git a/gfx/glyph/9.png b/gfx/glyph/9.png new file mode 100644 index 0000000..507f024 Binary files /dev/null and b/gfx/glyph/9.png differ diff --git a/gfx/good.png b/gfx/good.png new file mode 100644 index 0000000..f48ad0c Binary files /dev/null and b/gfx/good.png differ diff --git a/gfx/great.png b/gfx/great.png new file mode 100644 index 0000000..455899f Binary files /dev/null and b/gfx/great.png differ diff --git a/gfx/poor.png b/gfx/poor.png new file mode 100644 index 0000000..d453669 Binary files /dev/null and b/gfx/poor.png differ diff --git a/gfx/title.png b/gfx/title.png new file mode 100644 index 0000000..222d3cb Binary files /dev/null and b/gfx/title.png differ diff --git a/gfx/triangle.png b/gfx/triangle.png new file mode 100644 index 0000000..0df4d05 Binary files /dev/null and b/gfx/triangle.png differ diff --git a/keyfile.py b/keyfile.py new file mode 100644 index 0000000..3375e65 --- /dev/null +++ b/keyfile.py @@ -0,0 +1,214 @@ +import pygame +import imp +import os.path +from bmevent import * + +class KeyFile: + bmelist = None + numkeys = 0 + + # Loaded from a file + player = None + genre = None + title = None + artist = None + stagefile = None + playlevel = -1 + rank = 3 + stagefile = None + volwav = 1.0 + wavs = None + bmps = None + offset = 0 + + # Allows us to continuously map time to the current beat + beatfunc = None + + # Keymapping is a sequence of keycodes, mapping the index to the + # key + def __init__(self): + self.bmelist = [] + self.wavs = {} + self.bmps = {} + + def add(self,bme): + self.bmelist.append(bme) + + def remove(self,bme): + return self.bmelist.remove(bme) + + def sort(self): + def sortfun(a,b): + if a.beat == b.beat: + return 0 + elif a.beat > b.beat: + return 1 + elif a.beat < b.beat: + return -1 + self.bmelist.sort(sortfun) + + def dump(self): + for b in self.bmelist: + print str(b) + + # AWFUL DIRTY NO GOOD HACK + # (but does find the end of the song within 10ms) + def length(self): + if len(self.beatfunc) >= 2: + t = self.beatfunc[-2][0] + else: + t = 0 + last_beat = self.bmelist[-1].beat + b = self.eval_beatfunc(t) + while b < last_beat: + t+= 10 + b = self.eval_beatfunc(t) + return t + + # It's time to get func-y + def generate_beatfunc(self): + self.beatfunc = [] + bpms = filter(lambda x: x.type & (BME_TEMPO | BME_LONGMEASURE | BME_STOP), self.bmelist) + self.beatfunc = self.generate_beatfunc_r(bpms) + + def generate_beatfunc_r(self, bpms, ct=0): + if len(bpms) == 0: + return [(3600000.0,0)] + beat = bpms[0].beat + type = bpms[0].type + + if type == BME_TEMPO: + ms_per_beat = 60000.0 / bpms[0].dataref + func = lambda t: (t - ct) / ms_per_beat + beat + if len(bpms) == 1: + next = 4000 + else: + next = bpms[1].beat + duration = (next - beat) * ms_per_beat + self.lastbpm = bpms[0].dataref + elif type == BME_LONGMEASURE: + # Long measures only last one measure, so we do + # the slow measure just like a tempo change, + # then add a tempo change back at the end. + ms_per_beat = (60000.0 / self.lastbpm) * bpms[0].dataref + func = lambda t: (t - ct) / ms_per_beat + beat + + print "Last BPM:",self.lastbpm + duration = 4 * ms_per_beat + bpms.insert(1,BMEvent(beat+4,BME_TEMPO,0,self.lastbpm)) + elif type == BME_STOP: + func = lambda t: beat + duration = bpms[0].dataref + else: + raise Exception("WTF? Invalid type in generate_beatfunc_r") + l = self.generate_beatfunc_r(bpms[1:], ct + duration) + l.insert(0,(ct,func)) + return l + + def eval_beatfunc(self,t): + for n in range(0,len(self.beatfunc)-1): + if self.beatfunc[n][0] <= t and self.beatfunc[n+1][0] > t: + return self.beatfunc[n][1](t) + + def show_beatfunc(self,surface): + end = self.length() + xscale = end / surface.get_width() + yscale = self.bmelist[-1].beat / surface.get_height() + + for t in range(0,end,100): + beat = self.eval_beatfunc(t) + if beat: + pygame.draw.circle(surface,(255,255,255),(t/xscale,480 - beat / yscale),1) + else: + pygame.draw.line(surface,(255,0,0), (t/140.625,0), (t/140.625,480)) + + +class BMEListIter: + bmelist = None + + def __init__(self,bmelist): + self.bmelist = bmelist + self.b = 0.0 + self.i = 0 + + def goto(self,b): + l = len(self.bmelist) - 1 + self.b = b + + while self.i > 0 and b < self.bmelist[self.i].beat: + self.i -= 1 + while self.i < l and b > self.bmelist[self.i].beat: + self.i += 1 + + def window(self,db,type=None): + l = len(self.bmelist) + eb = self.b + db + ei = self.i + + while ei < l and eb >= self.bmelist[ei].beat: + ei += 1 + + if type: + return filter(lambda x: x.type & type, self.bmelist[self.i:ei]) + else: + return self.bmelist[self.i:ei] + + +screen = None +font = None + +loaders = [] +for x in ["BMloader","SMloader"]: + f = open("loaders/" + x + ".py") + loaders.append(imp.load_module(x,f,x + ".py",(".py",'r',imp.PY_SOURCE))) + #f.close() + +def vmessage(message): + fs = font.render(message, 0, (255,255,255),(0,0,0)) + screen.fill((0,0,0),(0,450,640,30)) + screen.blit(fs,(0,450)) + +def vstatus(type,arg): + if type == "STAGEFILE": + screen.blit(arg,(0,0)) + elif type == "WAV": + vmessage("Loaded WAV " + arg) + elif type == "BMP": + vmessage("Loaded BMP " + arg) + elif type == "TRACK": + vmessage("Parsing track " + str(arg)) + elif type == "ERROR": + vmessage("ERROR: " + arg) + pygame.display.flip() + +def likelihood(file): + likelihoods = map(lambda l: l.detect(file), loaders) + gl = 0.0 + gn = None + for n in range(0,len(likelihoods)): + if likelihoods[n] > gl: + gl = likelihoods[n] + gn = n + return gn + +def kf_load(file): + global screen,font + screen = pygame.display.get_surface() + font = pygame.font.SysFont("Helvetica Normal",30) + gn = likelihood(file) + if gn != None: + print "Load..." + kf = loaders[gn].load(file,vstatus) + kf.generate_beatfunc() + return kf + else: + return None + font = None + +def kf_info(file): + gn = likelihood(file) + d = loaders[gn].info(file) + d['loader.name'] = loaders[gn].name + d['loader.version'] = loaders[gn].version + + return d diff --git a/keygraph.py b/keygraph.py new file mode 100644 index 0000000..0468075 --- /dev/null +++ b/keygraph.py @@ -0,0 +1,189 @@ +import pygame +from bmevent import * +from keyhandler import * +from keyfile import BMEListIter +from fx import ColumnPulse +from constants import * + +red = (255,30,30) +offwhite = (245,245,245) +blue = (30,30,255) +black = (0,0,0) +darkgray = (20,20,20) + +class KeyGraph: + keyfile = None + t = 0 + b = 0 + velocity = 150.0 + li = None + bordercolor = (255,255,255) + judgemapping = {JUDGE_FGREAT: ["gfx/great.png","gfx/fgreat.png"], + JUDGE_GREAT: ["gfx/great.png"], + JUDGE_GOOD: ["gfx/good.png"], + JUDGE_BAD: ["gfx/bad.png"], + JUDGE_POOR: ["gfx/poor.png"], + JUDGE_NA: []} + colmarkmap = {JUDGE_FGREAT: "gfx/dcircle.png", + JUDGE_GREAT: "gfx/circle.png", + JUDGE_GOOD: "gfx/triangle.png", + JUDGE_BAD: "gfx/cross.png", + JUDGE_POOR: "gfx/cross.png", + JUDGE_NA: None} + numberfiles = ["gfx/glyph/0.png","gfx/glyph/1.png","gfx/glyph/2.png","gfx/glyph/3.png","gfx/glyph/4.png","gfx/glyph/5.png","gfx/glyph/6.png","gfx/glyph/7.png","gfx/glyph/8.png","gfx/glyph/9.png"] + + def __init__(self, kf, keystyle, player, disprect): + self.numkeys = len(keystyle) + self.keyfile = kf + self.screen = pygame.display.get_surface() + self.disprect = disprect + self.vheight = disprect.height - 5; + self.barrect = pygame.Rect(disprect.left, + disprect.bottom - 4, + disprect.width, + 5) + if player == 1: + self.notefilter = BME_NOTE1 + elif player == 2: + self.notefilter = BME_NOTE2 + else: + print "Invalid player given to keygraph: " + kf + exit(1) + self.t = 0 + self.b = 0 + self.li = BMEListIter(self.keyfile.bmelist) + self.keystyle = [] + + w = 0 + tw = sum(map(lambda x: x[0],keystyle)) + for x in keystyle: + self.keystyle.append((int(w),x[1],x[2])) + w += (x[0] / tw) * (disprect.width - 1) + self.keystyle.append((int(w),None,None)) + + # Load gfx + self.judgeimg = {} + for k in self.judgemapping.keys(): + self.judgeimg[k] = map(lambda y: pygame.image.load(y).convert_alpha(),self.judgemapping[k]) + self.colmarkimg = {} + for k in self.colmarkmap.keys(): + if self.colmarkmap[k]: + self.colmarkimg[k] = pygame.image.load(self.colmarkmap[k]).convert_alpha() + else: + self.colmarkimg[k] = None + self.colmarkimg[0] = None + self.numberimg = map(lambda y: pygame.image.load(y).convert_alpha(),self.numberfiles) + self.judgement = 0 + self.kjudgement = [0 for x in range(0,self.numkeys)] + self.judgetime = 0 + self.kjudgetime = [0 for x in range(0,self.numkeys)] + self.combo = 0 + + # Initialize keypulses (they are neat) + self.pulse = [] + for n in range(0,len(keystyle)): + x = self.keystyle[n][0] + 1 + y = self.disprect.top + 0.3 * self.disprect.height + w = self.keystyle[n+1][0] - self.keystyle[n][0] - 1 + h = 0.7 * self.disprect.height - 4 + self.pulse.append(ColumnPulse(pygame.Rect(x,y,w,h),keystyle[n][3])) + self.accuracy = 0 + self.key = 0 + + def setproperties(self,properties): + try: + ud = properties['ud'] + k = properties['key'] + if ud == 1: + self.pulse[k].down() + else: + self.pulse[k].up() + self.key = k + except KeyError: + pass + + try: + self.judgement = properties['judgement'] + self.kjudgement[self.key] = properties['judgement'] + self.judgetime = self.t + self.kjudgetime[self.key] = self.t + if self.judgement <= JUDGE_GOOD: + self.combo += 1 + else: + self.combo = 0 + except KeyError: + pass + + try: + self.accuracy = properties['accuracy'] + except KeyError: + pass + + def numimgs(self,n): + imgs = [] + while n: + imgs.append(self.numberimg[n % 10]) + n = n / 10 + imgs.reverse() + return imgs + + def draw(self): + self.b = self.keyfile.eval_beatfunc(self.t) + for n in range(0,self.numkeys): + x = self.disprect.left + self.keystyle[n][0] + w = self.keystyle[n+1][0] - self.keystyle[n][0] + self.screen.fill(self.keystyle[n][2], + (x,self.disprect.top,w,self.disprect.height)) + for n in range(0,self.numkeys+1): + pygame.draw.line(self.screen, self.bordercolor, + (self.disprect.left + self.keystyle[n][0], self.disprect.top), + (self.disprect.left + self.keystyle[n][0], self.disprect.bottom - 1) ) + + self.li.goto(self.b) + keylist = self.li.window(self.vheight / self.velocity, self.notefilter) + pygame.draw.rect(self.screen, (255,0,0), self.barrect) + distortion = 1.0 + for k in keylist: + y = self.disprect.top + self.vheight - int((k.beat - self.b) * (self.velocity / distortion)) + x = self.disprect.left + self.keystyle[k.key][0] + 1 + w = self.keystyle[k.key+1][0] - self.keystyle[k.key][0] - 1 + pygame.draw.rect(self.screen, self.keystyle[k.key][1], (x,y,w,5) ) + for n in self.pulse: + n.draw() + + if self.judgement: + if self.t - self.judgetime > 500: + self.judgement = 0 + elif len(self.judgeimg[self.judgement]) == 0: + pass + else: + z = (self.t / 60) % len(self.judgeimg[self.judgement]) + w = self.judgeimg[self.judgement][z].get_width() + x = self.disprect.left + (self.disprect.width / 2) - (w / 2) + y = self.disprect.top + self.disprect.height * 0.7 + if self.combo >= 2: + imgs = self.numimgs(self.combo) + x -= 2 + w += 4 + for i in imgs: + x -= i.get_width() / 2 + for i in imgs: + self.screen.blit(i,(x + w,y)) + w += i.get_width() + self.screen.blit(self.judgeimg[self.judgement][z],(x,y)) + # Draw accuracy bar + x = self.disprect.left + self.disprect.width / 2 + y += 50 + dx = self.accuracy + if dx < 0: + x += dx + dx = -dx + self.screen.fill((255,0,0), (x,y,dx,3)) + for i in range(0,self.numkeys): + if self.t - self.kjudgetime[i] > 300: + self.kjudgement[i] = 0 + else: + x = self.disprect.left + self.keystyle[i][0] + (self.keystyle[i+1][0] - self.keystyle[i][0] - 24) / 2 + y = self.disprect.bottom - 30 + if self.colmarkimg[self.kjudgement[i]]: + self.screen.blit(self.colmarkimg[self.kjudgement[i]],(x,y)) diff --git a/keyhandler.py b/keyhandler.py new file mode 100644 index 0000000..f06e344 --- /dev/null +++ b/keyhandler.py @@ -0,0 +1,100 @@ +from bmevent import * +from keyfile import * +from pygame.locals import * +from constants import * + +class KeyHandler: + keyfile = None + li = None + + keymap = None + jsbuttonmap = None + jsaxismap = None + timings = None + + def __init__(self,gameconfig,keyfile): + self.keyfile = keyfile + self.li = BMEListIter(keyfile.bmelist) + self.lastdataref = [0,0,0,0,0,0,0,0] + self.li.goto(0) + for b in self.li.window(1.0): + if b.type == BME_TEMPO: + self.bpm = b.dataref + self.adjust_window() + break + self.keymap = gameconfig['keymap'] + self.jsbuttonmap = gameconfig['jsbuttonmap'] + self.jsaxismap = gameconfig['jsaxismap'] + self.timings = gameconfig['timings'] + + def decodekey(self,event): + try: + if event.type == KEYDOWN: + return (self.keymap[event.key],1) + elif event.type == KEYUP: + return (self.keymap[event.key],0) + elif event.type == JOYBUTTONDOWN: + return (self.jsbuttonmap[event.button],1) + elif event.type == JOYBUTTONUP: + return (self.jsbuttonmap[event.button],0) + elif event.type == JOYAXISMOTION: + if event.value == 0.0: + return (self.jsaxismap[(event.axis,event.value)],0) + else: + return (self.jsaxismap[(event.axis,event.value)],1) + else: + return (None,-1) + except KeyError: + return (None,-1) + + def adjust_window(self): + self.ms_per_beat = 60000.0 / self.bpm + self.tstart = 200.0 / self.ms_per_beat + self.tlength = 2000.0 / self.ms_per_beat + + def setproperties(self,properties): + try: + self.bpm = properties['bpm'] + self.adjust_window() + except KeyError: + pass + + def handle(self,event,t): + b = self.keyfile.eval_beatfunc(t) + properties = {} + (k,ud) = self.decodekey(event) + if k == None: + return + properties['key'] = k + properties['ud'] = ud + if ud == 0: + return (None, properties) + self.li.goto(b - self.tstart) + bmelist = filter(lambda x: x.key == k, self.li.window(self.tlength,BME_NOTE1)) + if not bmelist: + properties['judgement'] = JUDGE_NA + return (BMEvent(b,BME_HIT,k,self.lastdataref[k]), properties) + + # Find the closest bme to our event + sd = self.tstart + cbme = None + for bme in bmelist: + d = abs(b - bme.beat) + if d < sd: + sd = d + cbme = bme + # Convert back to ms + sd = sd * self.ms_per_beat + + if cbme: + for bt in self.timings: + if sd < bt[0]: + properties['judgement'] = bt[1] + cbme.type = BME_HIT + break + self.lastdataref[k] = cbme.dataref + else: + properties['judgement'] = JUDGE_NA + return (BMEvent(b,BME_HIT,k,self.lastdataref[k]),properties) + properties['accuracy'] = (b - cbme.beat) * self.ms_per_beat + return (cbme, properties) diff --git a/loaders/BMloader.py b/loaders/BMloader.py new file mode 100644 index 0000000..591bd01 --- /dev/null +++ b/loaders/BMloader.py @@ -0,0 +1,175 @@ +import pygame +import os.path +import re +from bmevent import * +from keyfile import * +from util import * + +# Required information for a loader module. If this isn't here, the game +# will crash and burn and it will be ALL YOUR FAULT. +name = "BeMusic BMS/BME" +version = 0.0 + +def detect(file): + res = [re.compile("^#PLAYLEVEL"), re.compile("^#WAV"), re.compile("^#BMP")] + match = [0,0,0] + + f = open(file) + for line in f: + for n in range(0,len(res)): + if res[n].match(line): + match[n] = 1 + return sum(match) / float(len(match)) + +def info(file): + d = {} + f = open(file,'r') + r = re.compile('(WAV|BMP|\d{5}:)') + for line in f: + if len(line) == 0 or line[0] != "#": + continue + line = line[1:] + l = line.split(' ') + arg = '' + cmd = l[0] + if len(l) >= 2: + arg = ' '.join(l[1:]) + if not r.match(cmd): + d[cmd.lower()] = arg.strip() + return d + +def load(file,status=lambda x,y:None): + keymapping = {0:1, 1:2, 2:3, 3:4, 4:5, 5:0, 7:6, 8:7} + kf = KeyFile() + kf.numkeys = len(keymapping) + lastbpm = 0 + dir = os.path.dirname(file) + f = open(file,'r') + kf.track = -1 + for line in f: + line = line.strip() + if len(line) == 0 or line[0] != "#": + continue + line = line[1:] + l = line.split(' ') + arg = '' + cmd = l[0] + if len(l) >= 2: + arg = ' '.join(l[1:]) + if cmd == "PLAYER": + kf.player = arg + elif cmd == "GENRE": + kf.genre = arg + elif cmd == "TITLE": + kf.title = arg + elif cmd == "ARTIST": + kf.artist = arg + elif cmd == "BPM": + try: + lastbpm = float(arg) + kf.add(BMEvent(0, BME_TEMPO, None, lastbpm)) + except ValueError: + print "Invalid value for BPM" + #kf.ms_per_measure = 240000.0 / kf.bpm + elif cmd == "PLAYLEVEL": + try: + kf.playlevel = int(arg) + except ValueError: + print "Invalid value for PLAYLEVEL" + elif cmd == "RANK": + try: + kf.rank = int(arg) + except ValueError: + print "Invalid value for RANK" + elif cmd == "STAGEFILE": + # This will stay here for now. Eventually, this + # will go into the "status" routine or somewhere + # further up. + if arg: + kf.stagefile = loadBMP(os.path.join(dir,arg)) + status("STAGEFILE",kf.stagefile) + elif cmd == "VOLWAV": + kf.volwav = float(arg) / 100.0 + elif cmd[0:3] == "WAV": + slot = int(cmd[3:5],36) + if arg[-3:].lower() == 'mp3': + wav = loadMP3(os.path.join(dir,arg)) + else: + wav = loadWAV(os.path.join(dir,arg)) + if wav: + wav.set_volume(kf.volwav) + if wav: + kf.wavs[slot] = wav + status("WAV",arg) + else: + status("ERROR","Could not load " + arg) + pygame.time.wait(1500) + elif cmd[0:3] == "BMP": + slot = int(cmd[3:5],36) + bmp = loadBMP(os.path.join(dir,arg)) + if bmp: + kf.bmps[slot] = bmp + status("BMP",arg) + else: + status("ERROR","Could not load " + arg) + elif len(cmd) > 5 and cmd[5] == ":": + # Hmm. Should "track" really be "measure"? + track = int(cmd[0:3]) + channel = int(cmd[3:5]) + message = cmd[6:] + + if channel == 2: + # Channel 2 is a floating-point + # multiplier that changes the length of + # the measure. + kf.add(BMEvent(track*4, BME_LONGMEASURE, None, float(message))) + continue + if track != kf.track: + status("TRACK",track) + kf.track = track + + v = [int(message[n*2:n*2+2],36) for n in range(0,len(message)/2)] + l = float(len(v)) / 4.0 + + bme = None + if channel == 1: + for n in range(0,len(v)): + if v[n] == 0: + continue + bme = BMEvent(track*4 + n / l, BME_BGM, None, v[n]) + kf.add(bme) + if channel == 3: + v = [int(message[n*2:n*2+2],16) for n in range(0,len(message)/2)] + for n in range(0,len(v)): + # WTF is up with the low tempos? + if v[n] == 0 or v[n] < 30: + continue + print "new BPM: " + str(v[n]) + bme = BMEvent(track*4 + n / l, BME_TEMPO, None, v[n]) + kf.add(bme) + lastbpm = v[n] + if channel == 4: + for n in range(0,len(v)): + if v[n] == 0: + continue + bme = BMEvent(track*4 + n / l, BME_BGA, None, v[n]) + kf.add(bme) + elif (channel >= 11 and channel <= 19) or (channel >= 21 and channel <= 29): + if channel >= 21 and channel <= 29: + type = BME_NOTE2 + b = 21 + else: + type = BME_NOTE1 + b = 11 + for n in range(0,len(v)): + if v[n] == 0 or not (channel - b) in keymapping: + continue + bme = BMEvent(track*4 + n / l, type, keymapping[channel - b], v[n]) + kf.add(bme) + else: + print "Unknown command:",cmd + kf.sort() + f.close() + + return kf + diff --git a/loaders/SMloader.py b/loaders/SMloader.py new file mode 100644 index 0000000..1918aa6 --- /dev/null +++ b/loaders/SMloader.py @@ -0,0 +1,114 @@ +import pygame +import os.path +import re +from bmevent import * +from keyfile import * + +# Required information for a loader module. If this isn't here, the game +# will crash and burn and it will be ALL YOUR FAULT. +name = "StepMania" +version = 0.0 + +def detect(file): + res = [re.compile("^#SUBTITLE"), re.compile("^#BANNER"), + re.compile("^#BACKGROUND"),re.compile("^#NOTES")] + match = [0,0,0,0] + + f = open(file) + for line in f: + for n in range(0,len(res)): + if res[n].match(line): + match[n] = 1 + return sum(match) / float(len(match)) + +def info(file): + return {} + +def load(file,status=lambda x,y:None): + keymapping = {0:1, 1:2, 2:3, 3:4, 4:5, 5:0, 7:6, 8:7} + kf = KeyFile() + kf.numkeys = len(keymapping) + dir = os.path.dirname(file) + f = open(file,'r') + kf.track = -1 + matcher = re.compile("#([A-Z]+):(.*);",re.DOTALL) + kf.stoplist = [] + buf = '' + for line in f: + line = line.split('//')[0]; + line = line.strip() + if len(line) == 0: + continue + buf += line + m = matcher.match(buf) + if m: + buf = ''; + cmd = m.group(1) + arg = m.group(2).split(':') + else: + continue + + if cmd == "TITLE": + kf.title = arg[0] + elif cmd == "SUBTITLE": + kf.subtitle = arg[0] + elif cmd == "ARTIST": + kf.artist = arg[0] + elif cmd == "BPMS": + bpmlist = map(lambda x: x.split('='),arg[0].split(',')) + kf.bpm = bpmlist[0][1] + for b in bpmlist: + kf.add(BMEvent(float(b[0]), BME_TEMPO, None, float(b[1]))) + elif cmd == "STOPS": + if arg[0]: + stoplist = map(lambda x: x.split('='),arg[0].split(',')) + for s in stoplist: + print s[0] + kf.add(BMEvent(float(s[0]), BME_STOP, None, int(float(s[1])*1000))) + elif cmd == "BACKGROUND": + if arg[0]: + try: + kf.stagefile = pygame.image.load(os.path.join(dir,arg[0])) + kf.stagefile = kf.stagefile.convert() + status("STAGEFILE",kf.stagefile) + except pygame.error: + pass + elif cmd == "OFFSET": + kf.offset = float(arg[0]) + kf.add(BMEvent(int(kf.offset * 1000), BME_BGM, None, 255)) + elif cmd == "MUSIC": + print os.path.join(dir,arg[0]) + try: + pygame.mixer.music.load(os.path.join(dir,arg[0])) + except pygame.error: + pygame.mixer.music.load(os.path.join(dir,arg[0].lower())) + kf.wavs[255] = pygame.mixer.music + elif cmd == "NOTES": + if arg[0] == 'dance-single' and arg[2].lower() == 'challenge': + parse_notes(arg[5],kf) + else: + print "Unknown command:",cmd + kf.sort() + f.close() + + return kf + +# BUGS! This assumes a four-column chart. +def parse_notes(str,kf): + tracks = str.split(',') + track = 0 + + for t in tracks: + l = len(t) / 4 + for n in range(0,l): + for m in range(0,4): + k = t[n*4+m] + if k == 'M': # Freaking mines... + continue + k = int(k) + if k > 0: + beat = 4 * (track + (float(n)/l)) + # Add note filter here? + kf.add(BMEvent(beat, BME_NOTE1, m*2+1, 1)) + track += 1 + diff --git a/mainmenu.py b/mainmenu.py new file mode 100644 index 0000000..5fce8d2 --- /dev/null +++ b/mainmenu.py @@ -0,0 +1,130 @@ +import pygame +import thread +from fx import * +import event + +class MainMenu: + fx = None + fxlock = None + period = 428 + circuitlist = [[(590,0), (550,40), (200,40), (170,70), (170,310)], + [(570,0), (540,30), (190,30), (160,60), (20,60), (20,220), (50,250), (50,420), (90,460), (610,460)], + [(0,220), (40,260), (40,480)], + [(180,0), (180,20), (150,50), (20,50), (20,0)]] + # (label, message) + menuitems = [("play","fileselect"), ("quit","quit")] + subtitle = "holy shit parallax!!!11!1" + version = 1e-99 + + def __init__(self): + self.fxlock = thread.allocate_lock() + self.fx = [] + self.menufx = [] + self.position = 0 + self.bar = PulseLine([(0,0),(130,0)],(200,200,255),self.period,5) + thread.start_new_thread(self.fxloader,()) + + def fxloader(self): + self.fxlock.acquire() + self.fx.append(Blank()) + for l in self.circuitlist: + o = PulseLine(l,(255,30,30),self.period) + self.fx.append(o) + self.fxlock.release() + + o = Image('gfx/title.png',(200,40)) + self.fxlock.acquire() + self.fx.append(o) + self.fxlock.release() + + n = 0 + for l in self.menuitems: + o = Text(l[0],'font/Nano.ttf',18,(30,70 + 20*n), (255,30,30)) + self.fxlock.acquire() + self.fx.append(o) + self.menufx.append(o) + self.fxlock.release() + n += 1 + + o = Text(self.subtitle,'font/Nano.ttf',15,(600,100),(255,30,30)) + o.align = ALIGN_RIGHT + self.fxlock.acquire() + self.fx.append(o) + self.fxlock.release() + + o = Text("version " + str(self.version),'font/Nano.ttf',14,(610,440),(255,30,30)) + o.align = ALIGN_RIGHT + self.fxlock.acquire() + self.fx.append(o) + self.fxlock.release() + + o = PlaneScroll('gfx/chips_layer1.jpg',(-0.12,0.06),2.0,0.75) + self.fxlock.acquire() + self.fx.insert(1,o) + self.fxlock.release() + + o = PlaneScroll('gfx/chips_layer2.jpg',(-0.06,0.03)) + self.fxlock.acquire() + self.fx.remove(self.fx[0]) + self.fx.insert(0,o) + self.fxlock.release() + + def draw(self,t): + self.fxlock.acquire() + for f in self.fx: + f.draw(t) + self.fxlock.release() + self.bar.location = (30, 68 + 20*self.position) + self.bar.draw(t) + self.bar.location = (30, 68 + 20*self.position + 20) + self.bar.draw(t) + + def run(self): + screen = pygame.display.get_surface() + decided = 0 + pygame.mixer.music.load('snd/0x00.ogg') + pygame.mixer.music.set_volume(0.75) + pygame.mixer.music.play(-1) + stab = pygame.mixer.Sound('snd/stab.ogg') + stab.set_volume(0.25) + start_t = pygame.time.get_ticks() + + while decided == 0: + t = pygame.time.get_ticks() - start_t + for e in pygame.event.get(): + act = event.parseevent(e) + if act == event.CANCEL: + decided = 'quit' + elif act == event.DOWN: + self.position += 1 + elif act == event.UP: + self.position -= 1 + elif act == event.OK: + decided = self.menuitems[self.position][1] + + self.position = self.position % len(self.menuitems) + self.draw(t) + pygame.display.flip() + + pygame.mixer.music.fadeout(1500) + stab.play() + + start_t2 = pygame.time.get_ticks() + dt = 0 + blanksurf = pygame.surface.Surface((640,480)).convert() + blanksurf.fill( (0,0,0) ) + while dt < 1500: + dt = pygame.time.get_ticks() - start_t2 + if dt < 1000: + self.draw(pygame.time.get_ticks() - start_t) + blanksurf.set_alpha(int(dt / 1000.0 * 255.0)) + screen.blit(blanksurf,(0,0)) + else: + screen.fill( (0,0,0) ) + self.menufx[self.position].draw(0) + if dt >= 1000: + blanksurf.set_alpha(int((dt-1000) / 500.0 * 255.0)) + screen.blit(blanksurf,(0,0)) + pygame.display.flip() + + return [decided] diff --git a/mksongcache.py b/mksongcache.py new file mode 100755 index 0000000..03549c3 --- /dev/null +++ b/mksongcache.py @@ -0,0 +1,6 @@ +#!/usr/bin/python + +import fileselect + +fileselect.mksongcache() + diff --git a/rotowidget.py b/rotowidget.py new file mode 100644 index 0000000..6b9fef3 --- /dev/null +++ b/rotowidget.py @@ -0,0 +1,44 @@ +import pygame +from bmevent import * +from keyhandler import * +from math import exp,pi,sqrt + +# A rotowidget displays two axes of information in a rotary fashion. A +# central "pulser" is designed to show the score, and an arc around the +# edge shows the progress through the song. +class RotoWidget: + # Color format: (level, color) + # If the first item's level is greater than zero, the first + # color is black + pulsecolors = [(0,(180,0,0)), (80,(50,255,0))] + arccolors = [(0,(200,0,200))] + r = -0.2 + + def __init__(self,center,radius,period): + self.screen = pygame.display.get_surface() + self.center = center + self.radius = radius + self.period = period + self.pulsev = 0 + self.arcv = 0 + self.t = 0 + + def draw(self,t): + rad = self.radius * (self.pulsev / 100.0) + (0.1 * self.radius) * exp(self.r * (t % self.period)) + if rad > self.radius: + rad = self.radius + + pc = (0,0,0) + for n in range(len(self.pulsecolors)-1,-1,-1): + if self.pulsecolors[n][0] <= self.pulsev: + pc = self.pulsecolors[n][1] + break + pygame.draw.circle(self.screen,pc,self.center,rad,0) + + ac = (0,0,0) + for n in range(len(self.arccolors)-1,-1,-1): + if self.arccolors[n][0] <= self.arcv: + ac = self.arccolors[n][1] + break + pygame.draw.arc(self.screen,ac,(self.center[0]-self.radius, self.center[1]-self.radius, self.radius*2, self.radius*2),0,2*pi*(self.arcv / 100.0),3) + diff --git a/snd/0x00.ogg b/snd/0x00.ogg new file mode 100644 index 0000000..2b9ce02 Binary files /dev/null and b/snd/0x00.ogg differ diff --git a/snd/0x01.ogg b/snd/0x01.ogg new file mode 100644 index 0000000..9f7c3b3 Binary files /dev/null and b/snd/0x01.ogg differ diff --git a/snd/stab.ogg b/snd/stab.ogg new file mode 100644 index 0000000..3b814cc Binary files /dev/null and b/snd/stab.ogg differ diff --git a/songs/README.txt b/songs/README.txt new file mode 100644 index 0000000..d716355 --- /dev/null +++ b/songs/README.txt @@ -0,0 +1 @@ +Put your songs here. diff --git a/util.py b/util.py new file mode 100644 index 0000000..230a765 --- /dev/null +++ b/util.py @@ -0,0 +1,33 @@ +import pygame +import os.path + +import config + +def loadWAV(path,library=False): + try: + return pygame.mixer.Sound(path) + except pygame.error: + (dir,file) = os.path.split(path) + try: + return pygame.mixer.Sound(os.path.join(dir,file.lower())) + except pygame.error: + if library: + return None + else: + # Ok, try the system sample library... + return loadWAV(os.path.join(config.librarypath,file),True) + +def loadMP3(path): + pygame.mixer.music.load(path) + return pygame.mixer.music + +def loadBMP(path): + try: + return pygame.image.load(path).convert() + except pygame.error: + (dir,file) = os.path.split(path) + try: + return pygame.image.load(os.path.join(dir,file.lower())).convert() + except pygame.error: + return None +