[whichwayisup] 29/34: Imported Upstream version 0.7.9

Markus Koschany apo at moszumanska.debian.org
Sat Jul 16 19:06:15 UTC 2016


This is an automated email from the git hooks/post-receive script.

apo pushed a commit to branch master
in repository whichwayisup.

commit da564811bb50846026dc2fcab9566b0e8aa7fbd7
Author: Markus Koschany <apo at debian.org>
Date:   Sat Jul 16 20:48:55 2016 +0200

    Imported Upstream version 0.7.9
---
 README.txt                                  | 156 +++++++++
 changelog.txt                               | 138 ++++++++
 cmd_here.bat                                |   1 +
 data/levels/A Piece of Cake.txt             |   1 +
 data/levels/Quest For The Keys.txt          |   7 +
 data/levels/The Other Side.txt              |   7 +
 data/levels/creating_levels.txt             |  62 ++++
 data/levels/empty_level.txt                 |  25 ++
 data/levels/w0-l0-old.txt                   |  77 +++++
 data/levels/w0-l0.txt                       |  77 +++++
 data/levels/w0-l1.txt                       |  53 +++
 data/levels/w0-l2.txt                       |  53 +++
 data/levels/w0-l3.txt                       |  44 +++
 data/levels/w0-l4.txt                       |  47 +++
 data/levels/w0-l5.txt                       |  59 ++++
 data/levels/w0-l6.txt                       |  57 ++++
 data/levels/w1-l0.txt                       |  51 +++
 data/levels/w1-l1.txt                       |  73 ++++
 data/levels/w1-l2.txt                       |  53 +++
 data/levels/w1-l3.txt                       |  47 +++
 data/levels/w1-l4.txt                       |  49 +++
 data/levels/w1-l5.txt                       |  62 ++++
 data/levels/w1-l6.txt                       |  44 +++
 data/levels/w2-l0.txt                       |  43 +++
 data/misc/Vera.ttf                          | Bin 0 -> 65932 bytes
 data/pictures/bg_lose.png                   | Bin 0 -> 1622 bytes
 data/pictures/bg_quit.png                   | Bin 0 -> 1289 bytes
 data/pictures/bg_victory.png                | Bin 0 -> 1664 bytes
 data/pictures/blob_dying.txt                |   5 +
 data/pictures/blob_dying_0.png              | Bin 0 -> 183 bytes
 data/pictures/blob_dying_1.png              | Bin 0 -> 186 bytes
 data/pictures/blob_dying_2.png              | Bin 0 -> 187 bytes
 data/pictures/blob_dying_3.png              | Bin 0 -> 169 bytes
 data/pictures/blob_falling_0.png            | Bin 0 -> 277 bytes
 data/pictures/blob_jumping_0.png            | Bin 0 -> 280 bytes
 data/pictures/blob_standing_0.png           | Bin 0 -> 266 bytes
 data/pictures/brown_background_static_0.png | Bin 0 -> 247767 bytes
 data/pictures/brown_bars_0.png              | Bin 0 -> 565 bytes
 data/pictures/brown_cake.txt                |   4 +
 data/pictures/brown_cake_0.png              | Bin 0 -> 699 bytes
 data/pictures/brown_cake_1.png              | Bin 0 -> 696 bytes
 data/pictures/brown_cake_2.png              | Bin 0 -> 697 bytes
 data/pictures/brown_cake_3.png              | Bin 0 -> 696 bytes
 data/pictures/brown_key_0.png               | Bin 0 -> 232 bytes
 data/pictures/brown_lever.txt               |   2 +
 data/pictures/brown_lever_0.png             | Bin 0 -> 897 bytes
 data/pictures/brown_lever_1.png             | Bin 0 -> 1437 bytes
 data/pictures/brown_lever_broken_0.png      | Bin 0 -> 853 bytes
 data/pictures/brown_other_pants_0.png       | Bin 0 -> 299 bytes
 data/pictures/brown_power_crystal_0.png     | Bin 0 -> 229 bytes
 data/pictures/brown_spikes_0.png            | Bin 0 -> 2097 bytes
 data/pictures/brown_wall_0.png              | Bin 0 -> 1456 bytes
 data/pictures/default_static.txt            |   1 +
 data/pictures/energy_dying.txt              |   3 +
 data/pictures/energy_dying_0.png            | Bin 0 -> 170 bytes
 data/pictures/energy_dying_1.png            | Bin 0 -> 150 bytes
 data/pictures/energy_flying_0.png           | Bin 0 -> 159 bytes
 data/pictures/example_anim.txt              |   7 +
 data/pictures/green_background_static_0.png | Bin 0 -> 242563 bytes
 data/pictures/green_lever.txt               |   2 +
 data/pictures/green_spikes_0.png            | Bin 0 -> 1775 bytes
 data/pictures/green_wall_0.png              | Bin 0 -> 2544 bytes
 data/pictures/grey_background_static_0.png  | Bin 0 -> 234816 bytes
 data/pictures/grey_spikes_0.png             | Bin 0 -> 1066 bytes
 data/pictures/grey_wall_0.png               | Bin 0 -> 2031 bytes
 data/pictures/guy.txt                       |   5 +
 data/pictures/guy_arrow_0.png               | Bin 0 -> 251 bytes
 data/pictures/guy_dying.txt                 |   2 +
 data/pictures/guy_dying_0.png               | Bin 0 -> 798 bytes
 data/pictures/guy_exit.txt                  |  12 +
 data/pictures/guy_exit_0.png                | Bin 0 -> 866 bytes
 data/pictures/guy_exit_1.png                | Bin 0 -> 851 bytes
 data/pictures/guy_exit_10.png               | Bin 0 -> 147 bytes
 data/pictures/guy_exit_2.png                | Bin 0 -> 819 bytes
 data/pictures/guy_exit_3.png                | Bin 0 -> 798 bytes
 data/pictures/guy_exit_4.png                | Bin 0 -> 713 bytes
 data/pictures/guy_exit_5.png                | Bin 0 -> 579 bytes
 data/pictures/guy_exit_6.png                | Bin 0 -> 514 bytes
 data/pictures/guy_exit_7.png                | Bin 0 -> 171 bytes
 data/pictures/guy_exit_8.png                | Bin 0 -> 180 bytes
 data/pictures/guy_exit_9.png                | Bin 0 -> 177 bytes
 data/pictures/guy_gone_0.png                | Bin 0 -> 147 bytes
 data/pictures/guy_shouting.txt              |   3 +
 data/pictures/guy_shouting_0.png            | Bin 0 -> 797 bytes
 data/pictures/guy_shouting_1.png            | Bin 0 -> 798 bytes
 data/pictures/guy_standing_0.png            | Bin 0 -> 799 bytes
 data/pictures/guy_walking.txt               |   3 +
 data/pictures/guy_walking_0.png             | Bin 0 -> 788 bytes
 data/pictures/guy_walking_1.png             | Bin 0 -> 786 bytes
 data/pictures/guy_walking_2.png             | Bin 0 -> 775 bytes
 data/pictures/health_bar_empty.png          | Bin 0 -> 163 bytes
 data/pictures/health_bar_fill.png           | Bin 0 -> 921 bytes
 data/pictures/key_p.png                     | Bin 0 -> 1164 bytes
 data/pictures/key_z.png                     | Bin 0 -> 584 bytes
 data/pictures/menu_bg.png                   | Bin 0 -> 443 bytes
 data/pictures/object_idle_0.png             | Bin 0 -> 234 bytes
 data/pictures/power_crystal.png             | Bin 0 -> 229 bytes
 data/pictures/spider_standing_0.png         | Bin 0 -> 270 bytes
 data/pictures/spider_walking.txt            |   4 +
 data/pictures/spider_walking_0.png          | Bin 0 -> 286 bytes
 data/pictures/spider_walking_1.png          | Bin 0 -> 270 bytes
 data/pictures/spider_walking_2.png          | Bin 0 -> 276 bytes
 data/pictures/spider_walking_3.png          | Bin 0 -> 270 bytes
 data/sounds/augh.ogg                        | Bin 0 -> 8448 bytes
 data/sounds/augh.txt                        |   1 +
 data/sounds/boing.ogg                       | Bin 0 -> 7173 bytes
 data/sounds/click.ogg                       | Bin 0 -> 5006 bytes
 data/sounds/coins.ogg                       | Bin 0 -> 9507 bytes
 data/sounds/fire.ogg                        | Bin 0 -> 11723 bytes
 data/sounds/kling.ogg                       | Bin 0 -> 9587 bytes
 data/sounds/nextlevel.ogg                   | Bin 0 -> 28667 bytes
 data/sounds/woosh.ogg                       | Bin 0 -> 26395 bytes
 lib/animation.py                            |  66 ++++
 lib/blob.py                                 |  75 ++++
 lib/data.py                                 |  36 ++
 lib/edit_utils.py                           |  41 +++
 lib/frame.py                                |  31 ++
 lib/game.py                                 | 507 ++++++++++++++++++++++++++++
 lib/item.py                                 |  56 +++
 lib/level.py                                | 355 +++++++++++++++++++
 lib/locals.py                               | 132 ++++++++
 lib/log.py                                  |  32 ++
 lib/main.py                                 | 169 ++++++++++
 lib/mainmenu.py                             | 104 ++++++
 lib/menu.py                                 | 135 ++++++++
 lib/object.py                               | 221 ++++++++++++
 lib/pack.bat                                |  24 ++
 lib/particle.py                             |  61 ++++
 lib/player.py                               | 145 ++++++++
 lib/projectile.py                           |  53 +++
 lib/scripted_event.py                       |  95 ++++++
 lib/setup.py                                |   4 +
 lib/sound.py                                |  42 +++
 lib/spider.py                               | 149 ++++++++
 lib/spikes.py                               |  18 +
 lib/tile.py                                 |  64 ++++
 lib/trigger.py                              |  12 +
 lib/util.py                                 | 305 +++++++++++++++++
 lib/variables.py                            |   5 +
 lib/visibleobject.py                        | 153 +++++++++
 lib/whichway.ico                            | Bin 0 -> 11694 bytes
 lib/world.py                                |  50 +++
 run_game.py                                 |  13 +
 143 files changed, 4493 insertions(+)

diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..4866f04
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,156 @@
+Which Way is Up? README
+=======================
+
+You're playing version Beta 0.7.9.
+
+
+
+CREDITS & CONTACT:
+
+Main developer: Olli Etuaho
+Home page: http://www.hectigo.net/
+E-mail: admin at hectigo.net
+
+Debian package maintained by: Miriam Ruiz
+Home page: http://www.miriamruiz.es/weblog/
+E-mail: webmistress at miriamruiz.es
+
+
+
+DEPENDENCIES:
+
+You might need to install some of these before running the game.
+The dependencies are for the Python source code version - they are not needed
+for running the Windows binary.
+
+  Python:     http://www.python.org/
+  PyGame:     http://www.pygame.org/
+
+
+
+RUNNING THE GAME:
+
+On Windows or Mac OS X, locate the "run_game.py" file and double-click it.
+If you're using the Windows binary, double-click on main.exe.
+
+Otherwise open a terminal / console and "cd" to the game directory and run:
+
+  python run_game.py
+
+
+
+HOW TO PLAY THE GAME:
+
+The game gives you some instructions as you start playing.
+
+Controls reference:
+
+KEYBOARD:
+
+Left/Right       Move
+Z or Up          Advance game dialogue
+                 Jump - the longer you hold down the button,
+                 the higher the character jumps
+Down             Interact with the environment, pick up objects
+P                Pause game
+
+JOYSTICK OR GAMEPAD WITH 2 OR MORE BUTTONS:
+
+Left/Right       Move
+Button 1         Advance game dialogue
+                 Jump - the longer you hold down the button,
+                 the higher the character jumps
+Button 2         Interact with the environment, pick up objects
+
+JOYSTICK OR GAMEPAD WITH LESS THAN 2 BUTTONS:
+
+Left/Right       Move
+Up               Advance game dialogue
+                 Jump - the longer you hold the pad or joystick,
+                 the higher the character jumps
+Down             Interact with the environment, pick up objects
+
+
+MENUS:
+
+ESC                    Main menu / Quit
+Up/Down                Navigate the menu
+Enter/Z/Button 1 or 2  Choose menu option
+
+
+Oh, and one more thing - if you don't find the game challenging enough as it
+is, try maximize your score by getting through it without losing any health
+at all. This could be damn hard at some points, but it's always possible!
+Improving speed records also provides some additional challenge.
+
+
+
+KNOWN BUGS/ISSUES:
+
+- Some minor collision detection related bugs and annoyances -
+  especially regarding corner-to-corner-collisions.
+- The player may get stuck if jumping over the top when the level is
+  about to rotate.
+
+
+
+SPEEDRUNNING:
+
+The game implements an accurate frame-based timer, which records the total
+time player uses to complete all the levels in a world. The current FPS is set
+at 24, so you get the time in seconds by dividing the final time in frames by
+24. Fading effects and scripted events with dialogue don't increase the timer,
+so the dialogue setting shouldn't considerably affect the final time. Of
+course, mid-level events may still interrupt the gameplay.
+
+At the moment the game is still in beta, so changes to the levels and gameplay
+are possible. Thus, records made with the current version might be better than
+what is possible with upcoming versions.
+
+
+
+ADDITIONAL INFORMATION:
+
+The game has a home page at
+http://hectigo.net/puskutraktori/whichwayisup/
+
+Information about creating your own levels can be found in
+data/levels/creating_levels.txt
+
+The game saves unlock data etc. to the user's home directory.
+
+In Linux, this is usually:
+~/.wwisup
+
+In Windows XP/2000, this is usually:
+C:\Documents and Settings\User\Application Data\Wwisup
+
+COMMAND LINE PARAMETERS:
+
+-l level_name           Start up from the level specified.
+-v                      Verbose mode - error messages appear in the console,
+                        not just the log file.
+-dev                    Developer mode. Enables editing the official levels
+                        and also activates verbose mode automatically.
+
+DEVELOPER KEYBOARD COMMANDS:
+
+F10                    Rotate level - chooses direction automatically.
+                       Only available in developer mode.
+
+
+
+LICENSE:
+
+All game code is licensed under the GPL 2.0.
+http://www.gnu.org/licenses/gpl.html
+
+All game content, sounds and graphics are licensed under
+Creative Commons 3.0 Attribution license.
+http://creativecommons.org/licenses/by/3.0/
+
+The included Bitstream Vera font is licenced separately.
+For more information, see http://www.gnome.org/fonts/
+
+
+
diff --git a/changelog.txt b/changelog.txt
new file mode 100644
index 0000000..0b27808
--- /dev/null
+++ b/changelog.txt
@@ -0,0 +1,138 @@
+==============================
+== POSSIBLE FUTURE FEATURES ==
+==============================
+
+New features for versions beyond 0.8.0, in order of priority:
+ -Add saving of replays and enable playback as a ghost during gameplay
+ -Add animation scripting
+ -Improve scripting in general
+ -Improve packaging and make an automated Windows installer
+ -Add random level generator
+ -Fix collision detection related minor bugs and annoyances
+ -Add umbrella?
+ -Add background animation
+ -Improve performance and clean up code
+
+
+
+======================
+== CURRENT PROGRESS ==
+======================
+
+Changes from Beta 0.7.0 to Beta 0.7.9:
+ -Added alternate control configuration for zero or one button joysticks
+ -Refactored menu code to enhance reusability
+ -Added fullscreen mode
+ -Added log message about using the Windows APPDATA directory instead of the UNIX HOME
+ -Moved the render_gui function from util.py to game.py
+ -Cleaned up code and added comments
+ -Added basic tile-based level editing capabilities
+ -Added helper lines to the background images to make rotating easier for the player
+ -Added developer feature: Objects remember their initial positions and objects are rendered there translucently while in developer mode
+ -Increased upward acceleration in air (while holding Z) to compensate for engine changes
+ -Fixed Guy's flipping position correction code and misdetected collisions right after flipping
+ -Added Guy's exit animation
+ -Improved event processing: Flip triggers now always have the correct flip direction
+
+To be done for Beta 0.8.0:
+ -Reorganize game menus and add a custom level menu?
+ -Add more levels and enemy types
+ -Add ingame credits
+ -Balance and polish existing levels
+
+
+
+===================
+== PAST PROGRESS ==
+===================
+
+Changes from Beta 0.6.5 to Beta 0.7.0:
+ -Added support for different worlds and all kinds of strings as level names
+ -Added world 2: The Other Side with levels 1-7
+ -Added Blob enemy type
+ -Added spider cannon moving animation
+ -Added themed object fallback to the default brown theme
+ -Added the power crystal
+ -Changed blood particle behaviour to make bleeding look better
+ -Added graphical health bar
+ -Fixed glitch: spider cannons don't fire if the player is in the other direction
+ -Fixed glitch: the "augh" sound now plays when the player dies
+ -Added game icon
+ -Added fading in and out effects
+ -Adjusted levels 6-7 of world 1
+ -Fixed one or zero button joystick crash
+ -Fixed no sound device crash
+ -Added feature: The level can now rotate counter-clockwise according to the lever location
+ -Added speedrun timer
+ -Improved the instructions given in the dialogue in the beginning
+ -Added error logging to a file and supressed printing errors to the console by default
+ -Added pause game functionality
+ -Added a transparent background for dialogue to make the text more readable
+ -Fixed bug: Player doesn't take damage when he hits spikes and solid ground at the same time with a high velocity any more
+
+Changes from Beta 0.6.0 to Beta 0.6.5:
+ -Fixed glitch: The player isn't able to trigger another switch when the playing field is going to rotate
+ -Fixed glitch: The player can't jump while dying
+ -Added feature: The game remembers the highest score of the player
+ -Changed scoring to lessen the effect of the player's health
+ -Changed the level file format to enable different tilesets
+ -Changed the level background to an animateable object (but didn't animate yet)
+ -Added support for easy addition of pickable items (no changes to the code required)
+ -Added level 7 and other pants
+ -Added scrolling to the menu
+ -Changed the order of level 2 and 3 to achieve more progressive difficulty, edited related dialogue
+ -Changed the (new) level 2 slightly
+ -Cleaned up and simplified the code by separating dynamic and static objects. This also improved performance.
+  Gameobject is now DynamicObject, which inherits the new class VisibleObject.
+  VisibleObjects handles only animations, flipping and rendering.
+
+Changes from Beta 0.5.5 to Beta 0.6.0:
+ -Improved dialogue
+ -Added feature: Spiders are now able to cling to the edges of the screen
+ -Added caching of animation frames to lessen memory requirements
+ -Added caching of level ground_check inquiries to improve performance
+ -Improved level 6
+ -Improved performance with faster spider and projectile collision detection
+ -Changed: Lowered framerate from 25 to 24 to make the game just slightly slower and easier
+ -Improved usability: The level the player last played is chosen in the menu after a death
+ -Cleaned up and commented some code
+ -Increased the size of the skip dialogue button image
+ -Added error handling if the game doesn't find the level specified
+
+Changes from Beta 0.5.0 to Beta 0.5.5:
+ -Added joystick/gamepad support
+ -Added lever animation
+ -Added levels 5 and 6
+ -Added score display after loss and victory
+ -Fixed projectiles leaking off the top of the screen
+ -Changed: Projectiles now stay on screen after flipping
+
+Changes from Alpha 0.3.0 to Beta 0.5.0:
+ -Made the player a bit smaller to make it easier to get through narrow passages while jumping
+ -Increased player health
+ -Added on screen game over messages and player hp changes on game over and victory
+
+Changes from Alpha 0.2.0 to Alpha 0.3.0:
+ -Fixed "woosh" sound (the sound that the flip levers make)
+ -Improved dialogue
+ -Adjusted scoring
+ -Adjusted player jump height when hitting spikes - it's possible to jump out of a spike pit two tiles deep now
+ -Added third and fourth levels
+ -Added documentation about creating levels
+ -Added -l command line parameter for starting from a different level (debugging)
+ -Fixed collision detection "jitter" bug when colliding with a wall on the left
+ -Fixed top attached spider bug
+ -Replaced jumping sound effect
+ -Changed: Player's health now stays the same over the levels
+ -Added unlocked levels saving
+ -Added main menu with dialogue and sound toggling
+
+Changes from Alpha 0.1.0 to Alpha 0.2.0:
+ -Added basic script & dialogue support
+ -Added dialogue to the first level
+ -Added code for level change & victory / loss to the main class
+ -Added second level
+ -Changed: Lowered the spikes so that they can be jumped over more easily
+
+Alpha 0.1.0:
+ -First available test build
\ No newline at end of file
diff --git a/cmd_here.bat b/cmd_here.bat
new file mode 100644
index 0000000..911dc38
--- /dev/null
+++ b/cmd_here.bat
@@ -0,0 +1 @@
+cmd
\ No newline at end of file
diff --git a/data/levels/A Piece of Cake.txt b/data/levels/A Piece of Cake.txt
new file mode 100644
index 0000000..5339624
--- /dev/null
+++ b/data/levels/A Piece of Cake.txt	
@@ -0,0 +1 @@
+level w2-l0
\ No newline at end of file
diff --git a/data/levels/Quest For The Keys.txt b/data/levels/Quest For The Keys.txt
new file mode 100644
index 0000000..f79d742
--- /dev/null
+++ b/data/levels/Quest For The Keys.txt	
@@ -0,0 +1,7 @@
+level w0-l0
+level w0-l1
+level w0-l2
+level w0-l3
+level w0-l4
+level w0-l5
+level w0-l6
\ No newline at end of file
diff --git a/data/levels/The Other Side.txt b/data/levels/The Other Side.txt
new file mode 100644
index 0000000..d7695ec
--- /dev/null
+++ b/data/levels/The Other Side.txt	
@@ -0,0 +1,7 @@
+level w1-l0
+level w1-l1
+level w1-l2
+level w1-l3
+level w1-l4
+level w1-l5
+level w1-l6
\ No newline at end of file
diff --git a/data/levels/creating_levels.txt b/data/levels/creating_levels.txt
new file mode 100644
index 0000000..0cb911b
--- /dev/null
+++ b/data/levels/creating_levels.txt
@@ -0,0 +1,62 @@
+=== INTRODUCTION ===
+
+Feel free to add your own levels! They can be played by starting the game
+with the parameter -l <level name>, where level name is the file name of your
+level without the .txt extension. The empty_level text file in this directory
+is a good starting point for your own levels. The game always starts from the
+bottom right corner of the level files. A brief description of the level format
+follows:
+
+=== THE TILESET ===
+The first row is usually used for choosing the tileset. This happens with the
+"set" keyword. The game currently has two tilesets, brown and green. Brown
+is the default.
+
+=== TILE DATA ===
+
+20 rows after the keyword "tiles" are for the tile data. Case sensitive.
+W                   - Wall tile.
+S                   - Spikes tile.
+B                   - Bars tile.
+Any other character - Empty space. Different tile types might be added in the
+                      future, but at least space and . are safe to use for
+                      empty space.
+
+=== OBJECTS ===
+
+Then comes the object and scripted event data. The official level files are
+good examples for adding these. There will probably be improvements to the
+scripting system later, but as of now, it's quite pritimitive and meant mainly
+for giving messages to the player and changing levels.
+
+The game currently supports 5 kinds of objects:
+player (always add this first)
+lever
+spider
+blob
+pickable items (key, other_pants, power_crystal)
+
+These all are followed by two common parameters - x and y in game tiles.
+
+spider has one extra parameter - the direction of the surface it's attached to.
+This can be LEFT, RIGHT, UP or DOWN. This parameters changes automatically
+when the level is rotated.
+
+lever has two extra values - the amount of times it can be activated and the
+thing that it does. Currently the only supported action is TRIGGER_FLIP, which
+rotates the level 90 degrees clockwise.
+
+=== SCRIPTED EVENTS ===
+
+Scripted events are started with a trigger row, which specifies the trigger
+type and how many times the event can be repeated.
+
+Trigger types:
+level_begin       Activated when the gameplay first begins
+flipped           Activated when the level has been rotated
+key               An item with the ID "key" has been picked up
+
+Implemented scripted event actions are:
+change_level         Changes the level
+dialogue             Prints a text message to the middle of the screen.
+player orientation   Makes the player face LEFT or RIGHT.
\ No newline at end of file
diff --git a/data/levels/empty_level.txt b/data/levels/empty_level.txt
new file mode 100644
index 0000000..bd4badd
--- /dev/null
+++ b/data/levels/empty_level.txt
@@ -0,0 +1,25 @@
+set brown
+
+tiles
+WWWWWWWWWWWWWWWWWWWW
+W                  W
+W                  W
+W                  W
+W                  W
+W                  W
+W                  W
+W                  W
+W                  W
+W                  W
+W                  W
+W                  W
+W                  W
+W                  W
+W                  W
+W                  W
+W                  W
+W                  W
+W                  W
+WWWWWWWWWWWWWWWWWWWW
+
+player 18.5 18.55
\ No newline at end of file
diff --git a/data/levels/w0-l0-old.txt b/data/levels/w0-l0-old.txt
new file mode 100644
index 0000000..61a5554
--- /dev/null
+++ b/data/levels/w0-l0-old.txt
@@ -0,0 +1,77 @@
+set brown
+
+tiles
+WWSSSWWWWWWWWWWWWWWW
+W        WW   W W  W
+W      W  W     W  W
+W W         WW     S
+W    W    WWW   W  W
+W                  W
+W       W        W W
+W   WW       WWW   W
+WW                 W
+W        W        WW
+W  WWW          W  W
+WW             WW  W
+W    W    WWW      W
+S                  W
+S  W   WW         WW
+S     WW W        WW
+S     WW  W        W
+S     W    WWWW    W
+S  W         B     B
+WWWWW   W WWWWWWWWWW
+
+player 15.5 18.55
+key 12.5 18.5
+lever 18.5 8.5 1 TRIGGER_FLIP
+lever 8.5 1.5 1 TRIGGER_FLIP
+lever 3.5 11.5 1 TRIGGER_FLIP
+lever 9.5 19.5 1 TRIGGER_FLIP
+spider 17.5 15 RIGHT
+spider 15 6.5 DOWN
+spider 2.5 8.5 LEFT
+
+trigger level_begin 1
+player animation sleep
+dialogue Zzz...
+player animation default
+dialogue ...uh, g'morning.
+dialogue Where am I?
+dialogue ...
+dialogue I guess I really need to lay off the booze.
+dialogue I remember something about a key... and some sort of an experiment.
+player orientation LEFT
+dialogue And it seems like my key has slipped behind those bars.
+dialogue Slippery bastard. If only I could reach it...
+dialogue ...
+dialogue Damn this 2D world. Platforming really isn't my thing.
+dialogue I'm more like a shooter guy.
+player orientation RIGHT
+dialogue But if I really have to, I guess I could try the lever up there.
+dialogue You there with the controls, just lay your hand on the arrow keys.
+dialogue I jump with the up arrow or Z. Hold it longer, and I'll jump higher.
+dialogue You can also use it to slow down falls.
+dialogue Collect stuff and pull levers with the down arrow. Got it now?
+dialogue Oh, and stay clear of dangers.
+dialogue Like, if there are sharp spikes on the floor, don't make me jump on them.
+dialogue I can take a few hits, but I'm not the kind of guy that likes pain.
+dialogue So just be careful. Please.
+end trigger
+
+trigger flipped 1
+dialogue Whoa!
+dialogue That was sort of unexpected.
+dialogue But I guess I can use it to my advantage.
+dialogue This lever seems to have broken, but I wonder what the next one up there does?
+end trigger
+
+trigger key 1
+dialogue Finally!
+dialogue ...
+dialogue Hey, what is this?
+dialogue This key doesn't fit that door after all. Now what?
+dialogue ...
+dialogue I guess I'll just have to go to the next level.
+change_level
+end trigger
\ No newline at end of file
diff --git a/data/levels/w0-l0.txt b/data/levels/w0-l0.txt
new file mode 100644
index 0000000..21a0c9e
--- /dev/null
+++ b/data/levels/w0-l0.txt
@@ -0,0 +1,77 @@
+set brown
+
+tiles
+WWSSSWWWWWWWWWWWWWWW
+W        WW   W W  W
+W      W  W     W  W
+W W         WW     S
+W    W    WWW   W  W
+W                  W
+W       W        W W
+W   WW       WWW   W
+WW                 W
+W        W        WW
+W  WWW          W  W
+WW             WW  W
+W    W    WWW      W
+S                  W
+S  W   WW         WW
+S     WW W        WW
+S     WW  W        W
+S     W    WWWW    W
+S  W         B     B
+WWWWW   W WWWWWWWWWW
+
+key 12.5 18.5
+lever 18.5 8.5 1 TRIGGER_FLIP
+lever 8.5 1.5 1 TRIGGER_FLIP
+lever 3.5 11.5 1 TRIGGER_FLIP
+lever 9.5 19.5 1 TRIGGER_FLIP
+spider 17.5 15.0 RIGHT
+spider 15.0 6.5 DOWN
+spider 2.5 8.5 LEFT
+player 15.5 18.55
+
+trigger level_begin 1
+player animation sleep
+dialogue Zzz...
+player animation default
+dialogue ...uh, g'morning.
+dialogue Where am I?
+dialogue ...
+dialogue I guess I really need to lay off the booze.
+dialogue I remember something about a key... and some sort of an experiment.
+player orientation LEFT
+dialogue And it seems like my key has slipped behind those bars.
+dialogue Slippery bastard. If only I could reach it...
+dialogue ...
+dialogue Damn this 2D world. Platforming really isn't my thing.
+dialogue I'm more like a shooter guy.
+player orientation RIGHT
+dialogue But if I really have to, I guess I could try the lever up there.
+dialogue You there with the controls, just lay your hand on the arrow keys.
+dialogue I jump with the up arrow or Z. Hold it longer, and I'll jump higher.
+dialogue You can also use it to slow down falls.
+dialogue Collect stuff and pull levers with the down arrow. Got it now?
+dialogue Oh, and stay clear of dangers.
+dialogue Like, if there are sharp spikes on the floor, don't make me jump on them.
+dialogue I can take a few hits, but I'm not the kind of guy that likes pain.
+dialogue So just be careful. Please.
+end trigger
+
+trigger flipped 1
+dialogue Whoa!
+dialogue That was sort of unexpected.
+dialogue But I guess I can use it to my advantage.
+dialogue This lever seems to have broken, but I wonder what the next one up there does?
+end trigger
+
+trigger key 1
+dialogue Finally!
+dialogue ...
+dialogue Hey, what is this?
+dialogue This key doesn't fit that door after all. Now what?
+dialogue ...
+dialogue I guess I'll just have to go to the next level.
+change_level
+end trigger
diff --git a/data/levels/w0-l1.txt b/data/levels/w0-l1.txt
new file mode 100644
index 0000000..4c94f2d
--- /dev/null
+++ b/data/levels/w0-l1.txt
@@ -0,0 +1,53 @@
+set brown
+
+tiles
+WSSSSSSSWWWWWWWWWWWW
+W       SWW    W   W
+W   WW   SW      W W
+W         S      S W
+WWW          W   S W
+W          WW    S W
+W  W             W W
+W                  W
+WW    S           WW
+W        WW        W
+W  W W WWWWW W     W
+W    W   WW     W  W
+S  SSS    W        W
+W             W    W
+W                W W
+S   W  WS  W       W
+S      WWS     W   W
+S       WW        WW
+S W  W   W         B
+WWWWWWWW WWWWWWWWWWW
+
+player 16.5 18.55
+key 8.5 19.5
+spider 11.5 12.5 LEFT
+spider 12.5 15.5 LEFT
+spider 18.5 10.5 RIGHT
+spider 18.5 1.5 UP
+spider 10.5 8.5 DOWN
+spider 1.5 2.5 LEFT
+lever 18.5 7.5 1 TRIGGER_FLIP
+lever 8.5 11.5 1 TRIGGER_FLIP
+lever 3.5 11.5 1 TRIGGER_FLIP
+
+trigger level_begin 1
+dialogue And here we are.
+end trigger
+
+trigger flipped 1
+dialogue Over the top, eh? I've got it.
+end trigger
+
+trigger key 1
+dialogue Finally!
+dialogue ...
+dialogue Hey, what the heck?
+dialogue This isn't the right key either.
+dialogue ...
+dialogue I sort of see a pattern here.
+change_level
+end trigger
\ No newline at end of file
diff --git a/data/levels/w0-l2.txt b/data/levels/w0-l2.txt
new file mode 100644
index 0000000..2ccc793
--- /dev/null
+++ b/data/levels/w0-l2.txt
@@ -0,0 +1,53 @@
+set brown
+
+tiles
+WWWWWWWSWWWWWWWWWWWW
+W   W W W        WWW
+W   W W W    W     W
+W          W W     W
+WW         W W     W
+W    W     W W     W
+W       W  W     W W
+W               WW W
+W     W        W W W
+WWWWS       SW W   W
+W W               WW
+W         W       WW
+W   W   W          W
+W           W     WW
+W     W           WW
+W              WW  W
+WW W W W W   W    WW
+WWWWWWWWWWW  WW   WW
+W            B     W
+WBWWWWWWWWWWWWWWWWWW
+
+player 18.5 18.55
+key 1.5 18.5
+lever 18.5 9.5 1 TRIGGER_FLIP
+lever 7.5 6.5 1 TRIGGER_FLIP
+lever 1.5 10.5 1 TRIGGER_FLIP
+spider 17.5 11 RIGHT
+spider 17.5 14 RIGHT
+spider 17.5 17 RIGHT
+spider 17.8 2.5 UP
+spider 16.5 1.5 UP
+spider 5.5 1.5 UP
+spider 2.5 1.5 UP
+spider 1.5 15.5 DOWN
+spider 3.5 15.5 DOWN
+spider 5.5 15.5 DOWN
+spider 7.5 15.5 DOWN
+spider 9.5 15.5 DOWN
+
+trigger level_begin 1
+dialogue Huzzah!
+end trigger
+
+trigger key 1
+player orientation LEFT
+dialogue Guess what?
+dialogue Yes, this is only level 3, and we've got more.
+dialogue So just get ready!
+change_level
+end trigger
\ No newline at end of file
diff --git a/data/levels/w0-l3.txt b/data/levels/w0-l3.txt
new file mode 100644
index 0000000..d98df07
--- /dev/null
+++ b/data/levels/w0-l3.txt
@@ -0,0 +1,44 @@
+set brown
+
+tiles
+WS        W   WWWWWW
+W  WWWWWW   W   W  S
+W   WSSWWWWWW      S
+WW  W  W    W      S
+B   W    W  W      S
+W  WW      WW      S
+W   W       W  W   W
+WW  W  WWWWWW      W
+W   W  W    W    S W
+W WWW        W  WW W
+W   W     W   W  W W
+W   W     W   WW W W
+W   W     WW  W    W
+WS   WWWWW    W W SW
+WS    W      WW    W
+W    WWWWWWW  WW   W
+W   W   W     W    W
+WS          WWW WS W
+WWW   W      B     B
+WWWWWWWWWWWWWWWWWWWW
+
+player 18.5 18.55
+lever 18.5 7.5 1 TRIGGER_FLIP
+lever 9.5 0.5 1 TRIGGER_FLIP
+lever 3.5 10.5 1 TRIGGER_FLIP
+lever 9.5 16.5 1 TRIGGER_FLIP
+lever 7.5 12.5 1 TRIGGER_FLIP
+lever 9.5 12.5 1 TRIGGER_FLIP
+spider 2.5 8.5 DOWN
+spider 11.5 8.5 UP
+spider 13.5 10.5 UP
+spider 11.5 6.5 DOWN
+spider 11.5 17.5 RIGHT
+spider 13.5 6.5 LEFT
+key 11.5 6.5
+
+trigger key 1
+player orientation LEFT
+dialogue Seriously, where have all the good keys gone?
+change_level
+end trigger
\ No newline at end of file
diff --git a/data/levels/w0-l4.txt b/data/levels/w0-l4.txt
new file mode 100644
index 0000000..e49c3be
--- /dev/null
+++ b/data/levels/w0-l4.txt
@@ -0,0 +1,47 @@
+set brown
+
+tiles
+WWWWWWWWWWWWWWWW...S
+W          W   W   S
+WW         S   S   S
+W          W S     S
+W   W        W   W S
+W     WWWWWWW      S
+W W   WW           S
+W     W    W   W   .
+W     W    S       .
+W                  W
+W                  W
+W   WW  WWS  WW    .
+  WW  WW      W    .
+  W    S           W
+  S  W             W
+  S  W WW  WWWWWWWWW
+     W W           W
+W    W W W WWWWW WWW
+WWWWWWWW           W
+W        W WWWWWWWSW
+
+player 10.5 19.55
+spider 8.5 18.5 LEFT
+spider 18.5 16.5 RIGHT
+spider 18.5 14 RIGHT
+spider 15.5 12 LEFT
+spider 18.5 10 RIGHT
+spider 2.5 17.5 DOWN
+spider 14.5 1.5 UP
+spider 7.5 1.5 UP
+spider 3 11.5 DOWN
+spider 5 10.5 DOWN
+spider 7 11.5 DOWN
+spider 9 10.5 DOWN
+lever 19.5 8.5 1 TRIGGER_FLIP
+lever 8.5 2.5 1 TRIGGER_FLIP
+lever 7.5 7.5 1 TRIGGER_FLIP
+key 1.5 19.5
+
+trigger key 1
+dialogue This isn't the right key either, but hey, at least I'm going forward.
+dialogue One way or another, I'm gonna get out of this labyrinth.
+change_level
+end trigger
\ No newline at end of file
diff --git a/data/levels/w0-l5.txt b/data/levels/w0-l5.txt
new file mode 100644
index 0000000..f016eb7
--- /dev/null
+++ b/data/levels/w0-l5.txt
@@ -0,0 +1,59 @@
+set brown
+
+tiles
+SSSSSS....S.........
+            W     W.
+       WW     W    .
+       S           .
+    WS            SW
+           SWW  W  .
+W          S W     .
+     W WWW W WWWWWW.
+    WW W           .
+     W WWWWW  W WS W
+W    W       W     .
+     W    W        .
+    WWS   W WWWW WWW
+    W   W W        .
+           WW  W   .
+   W   W   W       .
+ W   W     W    W  W
+S      WW  W       .
+  WWS W     W W    .
+W       WW         .
+
+player 15.5 19.55
+spider 12.5 17.5 LEFT
+spider 19.5 17.5 RIGHT
+spider 11.5 13.5 LEFT
+spider 19.5 13.5 RIGHT
+spider 19.5 10.5 RIGHT
+spider 8.5 8.5 LEFT
+spider 14.5 6.5 LEFT
+spider 13.5 1.5 LEFT
+spider 15.5 2.5 LEFT
+spider 9.5 2.5 LEFT
+spider 9 6.5 DOWN
+spider 4.5 7.5 DOWN
+spider 0.5 9.5 DOWN
+spider 4.5 11.5 DOWN
+spider 2.5 17.5 DOWN
+spider 4.5 14.5 UP
+spider 6.5 17.5 DOWN
+spider 8.5 14.5 UP
+lever 19.5 8.5 1 TRIGGER_FLIP
+lever 12.5 6.5 1 TRIGGER_FLIP
+lever 0.5 11.5 1 TRIGGER_FLIP
+key 1.5 19.5
+
+trigger level_begin 1
+dialogue Ok, let's get it on!
+end trigger
+
+trigger key 1
+player orientation LEFT
+dialogue Hey! Now I remember!
+dialogue I left my key in my other pants.
+dialogue Now, where the hell did I leave my other pants?
+change_level
+end trigger
\ No newline at end of file
diff --git a/data/levels/w0-l6.txt b/data/levels/w0-l6.txt
new file mode 100644
index 0000000..4a3fd66
--- /dev/null
+++ b/data/levels/w0-l6.txt
@@ -0,0 +1,57 @@
+set brown
+
+tiles
+W...................
+W   W   WW WWWWWWW .
+WW       W       W .
+W        W  W  W W .
+W    W   W    WW W .
+WW       WW      W .
+WW     WWW   W   W W
+    W      S    W  .
+   W      WWWWWW   W
+W                  W
+W       W      W   .
+     W     W W   W .
+W  W       W       .
+W  W WW            W
+W  W              WW
+W  W WW  W     W   W
+W  W  W            W
+W  W W W     W     W
+W  W W WW          W
+WW   W.............B
+
+player 18.5 19.55
+other_pants 6.5 17.5
+spider 15.5 16.5 UP
+spider 13.5 18.5 UP
+spider 11.5 13.5 UP
+spider 9.5 16.5 UP
+spider 17.5 14.5 RIGHT
+spider 18.5 9.5 RIGHT
+lever 15.5 7.5 1 TRIGGER_FLIP
+spider 12.5 2.5 UP
+spider 8.5 11.5 UP
+spider 19.5 5.5 DOWN
+lever 8.5 2.5 1 TRIGGER_FLIP
+spider 1.5 0.5 LEFT
+spider 1.5 4 LEFT
+spider 2.5 6 LEFT
+spider 1.5 9.5 LEFT
+lever 0.5 11.5 1 TRIGGER_FLIP
+spider 4.5 8.5 UP
+
+trigger level_begin 1
+player orientation LEFT
+dialogue Oh, I'm getting close!
+dialogue I can so smell my other pants in here!
+dialogue ...
+dialogue Actually, it kind of reminds me of doing the laundry more often,
+dialogue but I guess I have bigger problems at hand. Onwards!
+end trigger
+
+trigger other_pants 1
+dialogue Other pants! Yay! Now, let's get out of here!
+change_level
+end trigger
\ No newline at end of file
diff --git a/data/levels/w1-l0.txt b/data/levels/w1-l0.txt
new file mode 100644
index 0000000..8109ae9
--- /dev/null
+++ b/data/levels/w1-l0.txt
@@ -0,0 +1,51 @@
+set green
+
+tiles
+WWSSSSS  WWW........
+WS        W        .
+W         W        .
+W   W              .
+W           S      .
+WW          W   W  .
+W           S      S
+W    W           WWW
+W                  S
+WW            WW   .
+W                  .
+W                  W
+W                  W
+S  W       W      WW
+S                  W
+S        W         W
+S        W         W
+S   W    WWW       W
+S   W         WW   W
+S   WWWWSSSSSSWWSSWW
+
+player 18.5 18.55
+blob 15.5 17.65
+blob 11.5 16.65
+spider 18.5 12 RIGHT
+lever 15 8.5 1 TRIGGER_FLIP
+blob 8.5 2.5
+lever 8.5 1.5 1 TRIGGER_FLIP
+blob 1.6 6.5
+lever 1.5 10.5 1 TRIGGER_FLIP
+blob 1.6 10.5
+blob 4.5 13.5
+power_crystal 5.5 18.5
+
+trigger level_begin 1
+player orientation LEFT
+dialogue Well well, I found the seven keys and got the exit.
+dialogue But it seems like that's not enough.
+dialogue Now I'll probably have to find the seven power crystals too...
+dialogue ...or something equally inane.
+dialogue Looks like I have new friends too, better be careful.
+end trigger
+
+trigger power_crystal 1
+dialogue Whee! I got the power crystal!
+dialogue Who could have guessed?
+change_level
+end trigger
\ No newline at end of file
diff --git a/data/levels/w1-l1.txt b/data/levels/w1-l1.txt
new file mode 100644
index 0000000..72346fa
--- /dev/null
+++ b/data/levels/w1-l1.txt
@@ -0,0 +1,73 @@
+set green
+
+tiles
+WSSSSSSSWWWWW.W....W
+W       WW  W    W W
+W   W              W
+W           W  WWW W
+W  W      W    W   W
+W     W   W    W   W
+W         W   WW   W
+W    W   WWWWWWW  WW
+W        WS W     WW
+WWWW     WS W      S
+W S      W  W      S
+W           W      S
+W           W      S
+W WWWWWWWWWWWWW    S
+W W    WW          S
+W W    W   WSSSW   W
+W W    W           .
+W W W  W  WSSSSSW  W
+W   WW             B
+WWWWWWWSSSSSSSSSSWWW
+
+player 18.5 18.55
+lever 14.5 12.5 1 TRIGGER_FLIP
+lever 9.5 4.5 1 TRIGGER_FLIP
+lever 11.5 8.5 1 TRIGGER_FLIP
+lever 8.5 17.5 1 TRIGGER_FLIP
+spider 9.5 14.5 LEFT
+spider 19.5 16.5 RIGHT
+spider 17.5 4.5 UP
+spider 18.5 6.5 DOWN
+spider 15.5 0.5 UP
+spider 13.5 0.5 UP
+blob 9.5 6.5
+blob 11.5 3.6
+spider 1.5 10.5 UP
+spider 1.5 7.5 LEFT
+spider 6.5 7.5 LEFT
+blob 3.5 16.5
+spider 6.5 14.5 UP
+power_crystal 10.5 16.5
+
+
+trigger level_begin 1
+player orientation LEFT
+dialogue ...
+dialogue Ok, hard way or the easy but painful way?
+dialogue I'm counting on you, man!
+end trigger
+
+trigger power_crystal 1
+dialogue Another one, whoa!
+dialogue Let's boogie!
+player orientation LEFT
+wait
+wait
+player orientation RIGHT
+wait
+wait
+player orientation LEFT
+wait
+wait
+player orientation RIGHT
+wait
+wait
+player orientation LEFT
+wait
+wait
+dialogue Feel it, man, feel it!
+change_level
+end trigger
\ No newline at end of file
diff --git a/data/levels/w1-l2.txt b/data/levels/w1-l2.txt
new file mode 100644
index 0000000..0afcb73
--- /dev/null
+++ b/data/levels/w1-l2.txt
@@ -0,0 +1,53 @@
+set green
+
+tiles
+    WWWWWSSWWWWWWWWW
+ WW WW     W   W   W
+  W W      W   W   W
+W W   WWWW S W   W W
+  W  WSSSS   W   W W
+  WWWW    WWWWWWWW W
+       W        W  W
+ WW WWWW WWWW S W  S
+       W W    S    S
+  WW   W   W  S W  S
+  W  WWWW   WWW W  W
+      W   W W      W
+ WWS  WWWWW W     WW
+       S    WWWWWWWW
+   WWWWWWW SW      W
+WW W   W S  W W WW W
+W    W   WS W      W
+W    S  W     W SS W
+WW   S          WW W
+WWWWWWWWWWSSSWWWWWWW
+
+player 7.5 11.55
+lever 9.5 9.5 2 TRIGGER_FLIP
+lever 9.5 10.5 2 TRIGGER_FLIP
+lever 10.5 9.5 2 TRIGGER_FLIP
+lever 10.5 10.5 3 TRIGGER_FLIP
+
+lever 8.5 15.5 1 TRIGGER_FLIP
+lever 10.5 1.5 1 TRIGGER_FLIP
+lever 3.5 10.5 1 TRIGGER_FLIP
+power_crystal 18.5 18.5
+blob 18.5 11.65
+blob 16.5 3.5
+blob 12.5 3.5
+spider 0.5 5.5 LEFT
+spider 1.5 17 LEFT
+spider 6.5 9 RIGHT
+
+trigger level_begin 1
+dialogue Hey, this just can't be!
+dialogue What is it with all these levers?
+dialogue Does the designer seriously expect me to suddenly solve puzzles?
+dialogue Like I'm some sort of a puzzle-solver man? Is that it?
+dialogue Geez... might as well just suck it up and take the painful route.
+end trigger
+
+trigger power_crystal 1
+dialogue And it's three out of seven, babe!
+change_level
+end trigger
\ No newline at end of file
diff --git a/data/levels/w1-l3.txt b/data/levels/w1-l3.txt
new file mode 100644
index 0000000..2ccfa9b
--- /dev/null
+++ b/data/levels/w1-l3.txt
@@ -0,0 +1,47 @@
+set green
+
+tiles
+WWWWSS  WWWWWWWWWWWW
+W       W          W
+W      WW   WW S W W
+WWWS  WW  W  W   W S
+W W    W WWW W  WW S
+W W    W  W  WW    W
+W      W      S    W
+W SSSSWWW  WWWWWWW W
+W      W   W    W  W
+W W      WWW    W WW
+W        WWW  WWW  W
+WWWWW  W   WW  WWW W
+  WWWWWWWW WWW   W W
+W   S      WWWW    W
+W   W W   WWWWWW WWW
+S     W     W      W
+S     W W        W W
+S  W  W S  WWWSSWW W
+S          W       B
+WWWWWWWWWWWW WWWWWWW
+
+player 18.5 18.55
+
+lever 12.5 19.5 -1 TRIGGER_FLIP
+lever 18.5 7.5 -1 TRIGGER_FLIP
+lever 7.5 0.5 -1 TRIGGER_FLIP
+lever 0.5 12.5 -1 TRIGGER_FLIP
+power_crystal 14.5 9.5
+blob 13.5 11.65
+spider 1.5 18.5 DOWN
+spider 10.5 1.5 UP
+spider 6.5 4.5 RIGHT
+
+trigger level_begin 1
+dialogue Ok, well, now I'm in trouble.
+dialogue There seems to be no way out!
+player orientation LEFT
+dialogue Of course, I COULD try that lever over there.
+end trigger
+
+trigger power_crystal 1
+dialogue Oh, my precious!
+change_level
+end trigger
\ No newline at end of file
diff --git a/data/levels/w1-l4.txt b/data/levels/w1-l4.txt
new file mode 100644
index 0000000..824ed7c
--- /dev/null
+++ b/data/levels/w1-l4.txt
@@ -0,0 +1,49 @@
+set green
+
+tiles
+.........W.W.W.W.W.W
+.                  .
+.        W W W W W .
+.   WWWWWWWWWWWWWW .
+.  W         W   W .
+. W  WWWWWWW WWW W .
+.W  W          W W .
+W  W  WWWWWWWW W W .
+W W   W      W   W .
+W W WWW WWWW WWW W W
+W W   W W  W   W W .
+W   W W W  W W W W W
+W W W W WW W W   W .
+W W W W    WWWWW W W
+W W W WWWWWWWWWW   .
+W W W   S      WWS W
+W W W S   W  W W   W
+W W   WWWWWWWW W  SW
+W   W   W    W    WW
+W     W   W    SSSWW
+
+player 10.5 13.55
+
+lever 9.5 10.5 -1 TRIGGER_FLIP
+lever 7.5 8.5 -1 TRIGGER_FLIP
+lever 12.5 12.5 -1 TRIGGER_FLIP
+lever 12.5 4.5 -1 TRIGGER_FLIP
+lever 5.5 10.5 -1 TRIGGER_FLIP
+lever 1.5 7.5 -1 TRIGGER_FLIP
+lever 11.5 16.5 -1 TRIGGER_FLIP
+lever 11.5 19.5 -1 TRIGGER_FLIP
+lever 19.5 8.5 -1 TRIGGER_FLIP
+lever 8.5 0.5 -1 TRIGGER_FLIP
+power_crystal 13.5 15.5
+spider 3 19.5 DOWN
+spider 1.5 10 LEFT
+
+trigger level_begin 1
+dialogue So, I'm in the middle of a spiral structure.
+dialogue Doesn't look too complicated, really.
+end trigger
+
+trigger power_crystal 1
+dialogue Woosh.
+change_level
+end trigger
\ No newline at end of file
diff --git a/data/levels/w1-l5.txt b/data/levels/w1-l5.txt
new file mode 100644
index 0000000..7e7128c
--- /dev/null
+++ b/data/levels/w1-l5.txt
@@ -0,0 +1,62 @@
+set green
+
+tiles
+WWWWWWWWWWWWWWWWWWWW
+WWWWW    W    W    W
+W   S    S    S    .
+W W              W .
+W             W    S
+W  W     W         S
+W           W      S
+WW  WS          W  S
+S                  S
+S  WS      W       .
+S       W          .
+WWW  W  W     W    W
+S S     WW       S .
+    W WS           W
+      WS       W   W
+ W    W        W W W
+      W WW WW WW   W
+   W  W W      W WWW
+      W            W
+WWWWWWWWWWWWWWWWWWWW
+
+player 18.5 18.55
+blob 16.5 18.65
+blob 13.5 18.65
+blob 10.5 18.65
+blob 18.5 16.65
+spider 9.5 17.5 LEFT
+lever 7.5 18.5 -1 TRIGGER_FLIP
+lever 9.5 15.5 -1 TRIGGER_FLIP
+lever 9.5 11.5 1 TRIGGER_FLIP
+lever 7.5 11.5 1 TRIGGER_FLIP
+lever 1.5 12.5 1 TRIGGER_FLIP
+spider 12.5 9.5 LEFT
+spider 9.5 10.5 LEFT
+spider 18.5 1.5 UP
+spider 13.5 1.5 UP
+spider 8.5 1.5 UP
+spider 1.5 6.5 LEFT
+spider 1.5 18.5 DOWN
+power_crystal 5.5 13.5
+blob 2.35 15.5
+
+trigger level_begin 1
+player orientation LEFT
+dialogue ...
+dialogue I guess I should say something witty now, but I won't.
+dialogue Let the level speak for itself.
+dialogue I mean - you can hear it too, I know it...
+dialogue ...speaking to you, when nobody else is around...
+dialogue ...mocking you, making you feel like you're nothing...
+dialogue ...making your leet gamer skillz pale in the face of sadistic game design...
+dialogue ...
+dialogue Bah, nevermind. Let's just get on with it.
+end trigger
+
+trigger power_crystal 1
+dialogue And on to the final challenge!
+change_level
+end trigger
\ No newline at end of file
diff --git a/data/levels/w1-l6.txt b/data/levels/w1-l6.txt
new file mode 100644
index 0000000..eeb495a
--- /dev/null
+++ b/data/levels/w1-l6.txt
@@ -0,0 +1,44 @@
+set green
+
+tiles
+WWWWWWW WWWWWWWWWWWW
+W       W    W  W  .
+W     WWWW        W.
+WW                W.
+WW       S   S W  W.
+W            S    S.
+W  WW  W   S WWWW S.
+    S        W    S.
+WW  S WSS         W.
+W     W         W W.
+WWWWWWWSS         W.
+W        W WWSS SSW.
+W      W   W       .
+W    W       WW  W .
+W  W         W W W W
+WW          WW     W
+W            W  WWWW
+W         W  W     W
+W            W  W  W
+WWWWWWWWWWWWWWSSWWWW
+
+player 18.5 18.55
+blob 16.5 15.65
+blob 15.5 13.65
+lever 11.5 18.5 -1 TRIGGER_FLIP
+lever 7.5 9.5 -1 TRIGGER_FLIP
+lever 16.5 8.5 -1 TRIGGER_FLIP
+spider 9.5 18.5 DOWN
+spider 15.5 5.5 DOWN
+spider 10.5 2 LEFT
+lever 7.5 1.5 -1 TRIGGER_FLIP
+spider 1.5 1.5 LEFT
+spider 2.5 3.5 LEFT
+power_crystal 1.5 9.5
+blob 3.35 7.5
+
+trigger power_crystal 1
+dialogue And it's the final one! Hurrah!
+dialogue ...or whatever.
+change_level
+end trigger
\ No newline at end of file
diff --git a/data/levels/w2-l0.txt b/data/levels/w2-l0.txt
new file mode 100644
index 0000000..fd2ad68
--- /dev/null
+++ b/data/levels/w2-l0.txt
@@ -0,0 +1,43 @@
+set grey
+
+tiles
+WWWWWWWWWWWWW      W
+W   W   W     W    S
+            W      S
+  W   W            S
+        S S S      S
+W   S              S
+        W   W   W  S
+W W       W W      W
+W                  W
+W   W  W    S S W SW
+W                  W
+WWW   W S    W   W W
+ W    S            W
+ W    S  W   S S  SW
+ WWWS WW           .
+ W    S   W  W  W  .
+ W W  S            .
+ W W  WW   W S S W .
+                   .
+WWWWWWWWSSSSWWWSSSSW
+
+lever 17.5 10.5 -1 TRIGGER_FLIP
+lever 2.5 12.5 -1 TRIGGER_FLIP
+lever 7.5 1.5 -1 TRIGGER_FLIP
+spider 8.5 14.5 LEFT
+spider 0.5 3.5 LEFT
+spider 0.5 17.5 LEFT
+cake 0.5 17.5
+player 19.5 18.55
+
+trigger level_begin 1
+dialogue Damn, searching for power crystals has really made me hungry.
+dialogue I wonder if there is some cake nearby?
+end trigger
+
+trigger cake 1
+dialogue Mm, delicious cake.
+dialogue Just what I was looking for!
+change_level
+end trigger
\ No newline at end of file
diff --git a/data/misc/Vera.ttf b/data/misc/Vera.ttf
new file mode 100644
index 0000000..58cd6b5
Binary files /dev/null and b/data/misc/Vera.ttf differ
diff --git a/data/pictures/bg_lose.png b/data/pictures/bg_lose.png
new file mode 100644
index 0000000..2914790
Binary files /dev/null and b/data/pictures/bg_lose.png differ
diff --git a/data/pictures/bg_quit.png b/data/pictures/bg_quit.png
new file mode 100644
index 0000000..77a8eb2
Binary files /dev/null and b/data/pictures/bg_quit.png differ
diff --git a/data/pictures/bg_victory.png b/data/pictures/bg_victory.png
new file mode 100644
index 0000000..ef85cc7
Binary files /dev/null and b/data/pictures/bg_victory.png differ
diff --git a/data/pictures/blob_dying.txt b/data/pictures/blob_dying.txt
new file mode 100644
index 0000000..c7398df
--- /dev/null
+++ b/data/pictures/blob_dying.txt
@@ -0,0 +1,5 @@
+repeat_times 1
+frame 0 1
+frame 1 1
+frame 2 1
+frame 3 1
\ No newline at end of file
diff --git a/data/pictures/blob_dying_0.png b/data/pictures/blob_dying_0.png
new file mode 100644
index 0000000..4f94f3f
Binary files /dev/null and b/data/pictures/blob_dying_0.png differ
diff --git a/data/pictures/blob_dying_1.png b/data/pictures/blob_dying_1.png
new file mode 100644
index 0000000..0639082
Binary files /dev/null and b/data/pictures/blob_dying_1.png differ
diff --git a/data/pictures/blob_dying_2.png b/data/pictures/blob_dying_2.png
new file mode 100644
index 0000000..a5fa51e
Binary files /dev/null and b/data/pictures/blob_dying_2.png differ
diff --git a/data/pictures/blob_dying_3.png b/data/pictures/blob_dying_3.png
new file mode 100644
index 0000000..312f170
Binary files /dev/null and b/data/pictures/blob_dying_3.png differ
diff --git a/data/pictures/blob_falling_0.png b/data/pictures/blob_falling_0.png
new file mode 100644
index 0000000..ab238bd
Binary files /dev/null and b/data/pictures/blob_falling_0.png differ
diff --git a/data/pictures/blob_jumping_0.png b/data/pictures/blob_jumping_0.png
new file mode 100644
index 0000000..cdfec76
Binary files /dev/null and b/data/pictures/blob_jumping_0.png differ
diff --git a/data/pictures/blob_standing_0.png b/data/pictures/blob_standing_0.png
new file mode 100644
index 0000000..65bcf0d
Binary files /dev/null and b/data/pictures/blob_standing_0.png differ
diff --git a/data/pictures/brown_background_static_0.png b/data/pictures/brown_background_static_0.png
new file mode 100644
index 0000000..ab3c5f7
Binary files /dev/null and b/data/pictures/brown_background_static_0.png differ
diff --git a/data/pictures/brown_bars_0.png b/data/pictures/brown_bars_0.png
new file mode 100644
index 0000000..8a01ab3
Binary files /dev/null and b/data/pictures/brown_bars_0.png differ
diff --git a/data/pictures/brown_cake.txt b/data/pictures/brown_cake.txt
new file mode 100644
index 0000000..b4cc14a
--- /dev/null
+++ b/data/pictures/brown_cake.txt
@@ -0,0 +1,4 @@
+frame 0 3
+frame 1 3
+frame 2 3
+frame 3 3
\ No newline at end of file
diff --git a/data/pictures/brown_cake_0.png b/data/pictures/brown_cake_0.png
new file mode 100644
index 0000000..e93f186
Binary files /dev/null and b/data/pictures/brown_cake_0.png differ
diff --git a/data/pictures/brown_cake_1.png b/data/pictures/brown_cake_1.png
new file mode 100644
index 0000000..9215dd7
Binary files /dev/null and b/data/pictures/brown_cake_1.png differ
diff --git a/data/pictures/brown_cake_2.png b/data/pictures/brown_cake_2.png
new file mode 100644
index 0000000..8888a35
Binary files /dev/null and b/data/pictures/brown_cake_2.png differ
diff --git a/data/pictures/brown_cake_3.png b/data/pictures/brown_cake_3.png
new file mode 100644
index 0000000..9215dd7
Binary files /dev/null and b/data/pictures/brown_cake_3.png differ
diff --git a/data/pictures/brown_key_0.png b/data/pictures/brown_key_0.png
new file mode 100644
index 0000000..7a8fbba
Binary files /dev/null and b/data/pictures/brown_key_0.png differ
diff --git a/data/pictures/brown_lever.txt b/data/pictures/brown_lever.txt
new file mode 100644
index 0000000..9bcd4a9
--- /dev/null
+++ b/data/pictures/brown_lever.txt
@@ -0,0 +1,2 @@
+frame 0 20
+frame 1 20
\ No newline at end of file
diff --git a/data/pictures/brown_lever_0.png b/data/pictures/brown_lever_0.png
new file mode 100644
index 0000000..5602673
Binary files /dev/null and b/data/pictures/brown_lever_0.png differ
diff --git a/data/pictures/brown_lever_1.png b/data/pictures/brown_lever_1.png
new file mode 100644
index 0000000..8969dc5
Binary files /dev/null and b/data/pictures/brown_lever_1.png differ
diff --git a/data/pictures/brown_lever_broken_0.png b/data/pictures/brown_lever_broken_0.png
new file mode 100644
index 0000000..7398457
Binary files /dev/null and b/data/pictures/brown_lever_broken_0.png differ
diff --git a/data/pictures/brown_other_pants_0.png b/data/pictures/brown_other_pants_0.png
new file mode 100644
index 0000000..3c02abf
Binary files /dev/null and b/data/pictures/brown_other_pants_0.png differ
diff --git a/data/pictures/brown_power_crystal_0.png b/data/pictures/brown_power_crystal_0.png
new file mode 100644
index 0000000..1e24b1c
Binary files /dev/null and b/data/pictures/brown_power_crystal_0.png differ
diff --git a/data/pictures/brown_spikes_0.png b/data/pictures/brown_spikes_0.png
new file mode 100644
index 0000000..58d3c30
Binary files /dev/null and b/data/pictures/brown_spikes_0.png differ
diff --git a/data/pictures/brown_wall_0.png b/data/pictures/brown_wall_0.png
new file mode 100644
index 0000000..8b2ae61
Binary files /dev/null and b/data/pictures/brown_wall_0.png differ
diff --git a/data/pictures/default_static.txt b/data/pictures/default_static.txt
new file mode 100644
index 0000000..7282994
--- /dev/null
+++ b/data/pictures/default_static.txt
@@ -0,0 +1 @@
+frame 0 1
\ No newline at end of file
diff --git a/data/pictures/energy_dying.txt b/data/pictures/energy_dying.txt
new file mode 100644
index 0000000..fb5e222
--- /dev/null
+++ b/data/pictures/energy_dying.txt
@@ -0,0 +1,3 @@
+repeat_times 1
+frame 0 1
+frame 1 1
\ No newline at end of file
diff --git a/data/pictures/energy_dying_0.png b/data/pictures/energy_dying_0.png
new file mode 100644
index 0000000..b432fe7
Binary files /dev/null and b/data/pictures/energy_dying_0.png differ
diff --git a/data/pictures/energy_dying_1.png b/data/pictures/energy_dying_1.png
new file mode 100644
index 0000000..421bd48
Binary files /dev/null and b/data/pictures/energy_dying_1.png differ
diff --git a/data/pictures/energy_flying_0.png b/data/pictures/energy_flying_0.png
new file mode 100644
index 0000000..fbacd28
Binary files /dev/null and b/data/pictures/energy_flying_0.png differ
diff --git a/data/pictures/example_anim.txt b/data/pictures/example_anim.txt
new file mode 100644
index 0000000..6925014
--- /dev/null
+++ b/data/pictures/example_anim.txt
@@ -0,0 +1,7 @@
+# This is a list of animation frames.
+# There are two keywords: frame and repeat_times.
+# Repeat_times tells how many times the animation should be repeated. Default is -1, which means repeating infinitely.
+# Frame stands for a single animation frame. The second number after frame stands for the playing time of the animation frame in game engine frames. The parser doesn't currently care about the first number, but it can be used for indexing.
+frame 0 3
+frame 1 3
+frame 2 3
\ No newline at end of file
diff --git a/data/pictures/green_background_static_0.png b/data/pictures/green_background_static_0.png
new file mode 100644
index 0000000..128316f
Binary files /dev/null and b/data/pictures/green_background_static_0.png differ
diff --git a/data/pictures/green_lever.txt b/data/pictures/green_lever.txt
new file mode 100644
index 0000000..9bcd4a9
--- /dev/null
+++ b/data/pictures/green_lever.txt
@@ -0,0 +1,2 @@
+frame 0 20
+frame 1 20
\ No newline at end of file
diff --git a/data/pictures/green_spikes_0.png b/data/pictures/green_spikes_0.png
new file mode 100644
index 0000000..9124f8f
Binary files /dev/null and b/data/pictures/green_spikes_0.png differ
diff --git a/data/pictures/green_wall_0.png b/data/pictures/green_wall_0.png
new file mode 100644
index 0000000..6df72a1
Binary files /dev/null and b/data/pictures/green_wall_0.png differ
diff --git a/data/pictures/grey_background_static_0.png b/data/pictures/grey_background_static_0.png
new file mode 100644
index 0000000..39d79b9
Binary files /dev/null and b/data/pictures/grey_background_static_0.png differ
diff --git a/data/pictures/grey_spikes_0.png b/data/pictures/grey_spikes_0.png
new file mode 100644
index 0000000..4354f60
Binary files /dev/null and b/data/pictures/grey_spikes_0.png differ
diff --git a/data/pictures/grey_wall_0.png b/data/pictures/grey_wall_0.png
new file mode 100644
index 0000000..9df7928
Binary files /dev/null and b/data/pictures/grey_wall_0.png differ
diff --git a/data/pictures/guy.txt b/data/pictures/guy.txt
new file mode 100644
index 0000000..c8436db
--- /dev/null
+++ b/data/pictures/guy.txt
@@ -0,0 +1,5 @@
+default
+walking
+arrow
+shouting
+dying
\ No newline at end of file
diff --git a/data/pictures/guy_arrow_0.png b/data/pictures/guy_arrow_0.png
new file mode 100644
index 0000000..ecfc897
Binary files /dev/null and b/data/pictures/guy_arrow_0.png differ
diff --git a/data/pictures/guy_dying.txt b/data/pictures/guy_dying.txt
new file mode 100644
index 0000000..5a76f83
--- /dev/null
+++ b/data/pictures/guy_dying.txt
@@ -0,0 +1,2 @@
+repeat_times 1
+frame 0 30
\ No newline at end of file
diff --git a/data/pictures/guy_dying_0.png b/data/pictures/guy_dying_0.png
new file mode 100644
index 0000000..6461f00
Binary files /dev/null and b/data/pictures/guy_dying_0.png differ
diff --git a/data/pictures/guy_exit.txt b/data/pictures/guy_exit.txt
new file mode 100644
index 0000000..b05aeb0
--- /dev/null
+++ b/data/pictures/guy_exit.txt
@@ -0,0 +1,12 @@
+repeat_times 1
+frame 0 5
+frame 1 1
+frame 2 1
+frame 3 1
+frame 4 1
+frame 5 1
+frame 6 1
+frame 7 1
+frame 8 1
+frame 9 1
+frame 10 20
\ No newline at end of file
diff --git a/data/pictures/guy_exit_0.png b/data/pictures/guy_exit_0.png
new file mode 100644
index 0000000..2dc1b3e
Binary files /dev/null and b/data/pictures/guy_exit_0.png differ
diff --git a/data/pictures/guy_exit_1.png b/data/pictures/guy_exit_1.png
new file mode 100644
index 0000000..100f7a4
Binary files /dev/null and b/data/pictures/guy_exit_1.png differ
diff --git a/data/pictures/guy_exit_10.png b/data/pictures/guy_exit_10.png
new file mode 100644
index 0000000..791c69d
Binary files /dev/null and b/data/pictures/guy_exit_10.png differ
diff --git a/data/pictures/guy_exit_2.png b/data/pictures/guy_exit_2.png
new file mode 100644
index 0000000..64c3aed
Binary files /dev/null and b/data/pictures/guy_exit_2.png differ
diff --git a/data/pictures/guy_exit_3.png b/data/pictures/guy_exit_3.png
new file mode 100644
index 0000000..c5421ad
Binary files /dev/null and b/data/pictures/guy_exit_3.png differ
diff --git a/data/pictures/guy_exit_4.png b/data/pictures/guy_exit_4.png
new file mode 100644
index 0000000..f1783d4
Binary files /dev/null and b/data/pictures/guy_exit_4.png differ
diff --git a/data/pictures/guy_exit_5.png b/data/pictures/guy_exit_5.png
new file mode 100644
index 0000000..6dba4b4
Binary files /dev/null and b/data/pictures/guy_exit_5.png differ
diff --git a/data/pictures/guy_exit_6.png b/data/pictures/guy_exit_6.png
new file mode 100644
index 0000000..8f0c0bd
Binary files /dev/null and b/data/pictures/guy_exit_6.png differ
diff --git a/data/pictures/guy_exit_7.png b/data/pictures/guy_exit_7.png
new file mode 100644
index 0000000..67d562d
Binary files /dev/null and b/data/pictures/guy_exit_7.png differ
diff --git a/data/pictures/guy_exit_8.png b/data/pictures/guy_exit_8.png
new file mode 100644
index 0000000..cb0da89
Binary files /dev/null and b/data/pictures/guy_exit_8.png differ
diff --git a/data/pictures/guy_exit_9.png b/data/pictures/guy_exit_9.png
new file mode 100644
index 0000000..6e61ee1
Binary files /dev/null and b/data/pictures/guy_exit_9.png differ
diff --git a/data/pictures/guy_gone_0.png b/data/pictures/guy_gone_0.png
new file mode 100644
index 0000000..791c69d
Binary files /dev/null and b/data/pictures/guy_gone_0.png differ
diff --git a/data/pictures/guy_shouting.txt b/data/pictures/guy_shouting.txt
new file mode 100644
index 0000000..33c64ec
--- /dev/null
+++ b/data/pictures/guy_shouting.txt
@@ -0,0 +1,3 @@
+repeat_times 4
+frame 0 1
+frame 1 1
\ No newline at end of file
diff --git a/data/pictures/guy_shouting_0.png b/data/pictures/guy_shouting_0.png
new file mode 100644
index 0000000..995b185
Binary files /dev/null and b/data/pictures/guy_shouting_0.png differ
diff --git a/data/pictures/guy_shouting_1.png b/data/pictures/guy_shouting_1.png
new file mode 100644
index 0000000..7438cfb
Binary files /dev/null and b/data/pictures/guy_shouting_1.png differ
diff --git a/data/pictures/guy_standing_0.png b/data/pictures/guy_standing_0.png
new file mode 100644
index 0000000..443dab2
Binary files /dev/null and b/data/pictures/guy_standing_0.png differ
diff --git a/data/pictures/guy_walking.txt b/data/pictures/guy_walking.txt
new file mode 100644
index 0000000..d61f4ad
--- /dev/null
+++ b/data/pictures/guy_walking.txt
@@ -0,0 +1,3 @@
+frame 0 3
+frame 1 5
+frame 2 3
\ No newline at end of file
diff --git a/data/pictures/guy_walking_0.png b/data/pictures/guy_walking_0.png
new file mode 100644
index 0000000..a8ddd89
Binary files /dev/null and b/data/pictures/guy_walking_0.png differ
diff --git a/data/pictures/guy_walking_1.png b/data/pictures/guy_walking_1.png
new file mode 100644
index 0000000..cf527ac
Binary files /dev/null and b/data/pictures/guy_walking_1.png differ
diff --git a/data/pictures/guy_walking_2.png b/data/pictures/guy_walking_2.png
new file mode 100644
index 0000000..dbf9f9f
Binary files /dev/null and b/data/pictures/guy_walking_2.png differ
diff --git a/data/pictures/health_bar_empty.png b/data/pictures/health_bar_empty.png
new file mode 100644
index 0000000..3efb966
Binary files /dev/null and b/data/pictures/health_bar_empty.png differ
diff --git a/data/pictures/health_bar_fill.png b/data/pictures/health_bar_fill.png
new file mode 100644
index 0000000..85322d2
Binary files /dev/null and b/data/pictures/health_bar_fill.png differ
diff --git a/data/pictures/key_p.png b/data/pictures/key_p.png
new file mode 100644
index 0000000..1fee06e
Binary files /dev/null and b/data/pictures/key_p.png differ
diff --git a/data/pictures/key_z.png b/data/pictures/key_z.png
new file mode 100644
index 0000000..f6fd170
Binary files /dev/null and b/data/pictures/key_z.png differ
diff --git a/data/pictures/menu_bg.png b/data/pictures/menu_bg.png
new file mode 100644
index 0000000..a49eb35
Binary files /dev/null and b/data/pictures/menu_bg.png differ
diff --git a/data/pictures/object_idle_0.png b/data/pictures/object_idle_0.png
new file mode 100644
index 0000000..daffa23
Binary files /dev/null and b/data/pictures/object_idle_0.png differ
diff --git a/data/pictures/power_crystal.png b/data/pictures/power_crystal.png
new file mode 100644
index 0000000..1e24b1c
Binary files /dev/null and b/data/pictures/power_crystal.png differ
diff --git a/data/pictures/spider_standing_0.png b/data/pictures/spider_standing_0.png
new file mode 100644
index 0000000..efffc1b
Binary files /dev/null and b/data/pictures/spider_standing_0.png differ
diff --git a/data/pictures/spider_walking.txt b/data/pictures/spider_walking.txt
new file mode 100644
index 0000000..51ab226
--- /dev/null
+++ b/data/pictures/spider_walking.txt
@@ -0,0 +1,4 @@
+frame 0 2
+frame 1 1
+frame 2 2
+frame 3 1
\ No newline at end of file
diff --git a/data/pictures/spider_walking_0.png b/data/pictures/spider_walking_0.png
new file mode 100644
index 0000000..de46641
Binary files /dev/null and b/data/pictures/spider_walking_0.png differ
diff --git a/data/pictures/spider_walking_1.png b/data/pictures/spider_walking_1.png
new file mode 100644
index 0000000..efffc1b
Binary files /dev/null and b/data/pictures/spider_walking_1.png differ
diff --git a/data/pictures/spider_walking_2.png b/data/pictures/spider_walking_2.png
new file mode 100644
index 0000000..8545a6a
Binary files /dev/null and b/data/pictures/spider_walking_2.png differ
diff --git a/data/pictures/spider_walking_3.png b/data/pictures/spider_walking_3.png
new file mode 100644
index 0000000..efffc1b
Binary files /dev/null and b/data/pictures/spider_walking_3.png differ
diff --git a/data/sounds/augh.ogg b/data/sounds/augh.ogg
new file mode 100644
index 0000000..7d5ad86
Binary files /dev/null and b/data/sounds/augh.ogg differ
diff --git a/data/sounds/augh.txt b/data/sounds/augh.txt
new file mode 100644
index 0000000..3a02224
--- /dev/null
+++ b/data/sounds/augh.txt
@@ -0,0 +1 @@
+variations 3
\ No newline at end of file
diff --git a/data/sounds/boing.ogg b/data/sounds/boing.ogg
new file mode 100644
index 0000000..ef9f991
Binary files /dev/null and b/data/sounds/boing.ogg differ
diff --git a/data/sounds/click.ogg b/data/sounds/click.ogg
new file mode 100644
index 0000000..0eec051
Binary files /dev/null and b/data/sounds/click.ogg differ
diff --git a/data/sounds/coins.ogg b/data/sounds/coins.ogg
new file mode 100644
index 0000000..cef7044
Binary files /dev/null and b/data/sounds/coins.ogg differ
diff --git a/data/sounds/fire.ogg b/data/sounds/fire.ogg
new file mode 100644
index 0000000..d88e331
Binary files /dev/null and b/data/sounds/fire.ogg differ
diff --git a/data/sounds/kling.ogg b/data/sounds/kling.ogg
new file mode 100644
index 0000000..ac931f0
Binary files /dev/null and b/data/sounds/kling.ogg differ
diff --git a/data/sounds/nextlevel.ogg b/data/sounds/nextlevel.ogg
new file mode 100644
index 0000000..537dd81
Binary files /dev/null and b/data/sounds/nextlevel.ogg differ
diff --git a/data/sounds/woosh.ogg b/data/sounds/woosh.ogg
new file mode 100644
index 0000000..9cb0f71
Binary files /dev/null and b/data/sounds/woosh.ogg differ
diff --git a/lib/animation.py b/lib/animation.py
new file mode 100644
index 0000000..b02d658
--- /dev/null
+++ b/lib/animation.py
@@ -0,0 +1,66 @@
+import pygame
+import os
+
+from pygame.locals import *
+
+from locals import *
+
+import data
+
+from frame import Frame
+
+class Animation:
+
+  cached_frames = {}
+
+  def __init__(self, object, anim_name):
+    try:
+      conffile = open(data.animpath(object, anim_name))
+    except:
+      try:
+        conffile = open(data.animpath("brown", anim_name))
+      except:
+        conffile = open(data.animpath("default", "static"))
+    tiley = 0
+    values = []
+    self.frames = []
+    self.repeat_times = -1
+    self.cache_name = object + anim_name
+    for line in conffile.readlines():
+      if line.strip() != "":
+        values = line.split()
+        if values[0] == "repeat_times":
+          self.repeat_times = int(values[1])
+        if values[0] == "frame":
+          self.frames.append(Frame(object, anim_name, len(self.frames), int(values[2])))
+    self.reset()
+    return
+
+  def reset(self):
+    self.c = 0
+    self.i = 0
+    self.repeated = 0
+    self.finished = False
+    self.image = self.frames[self.i].get_image()
+    return
+
+
+  def update_and_get_image(self):
+    if (not self.finished):
+      self.c += 1
+      if (self.c > int(self.frames[self.i].get_time())):
+        self.c = 0
+        self.i += 1
+        if (self.i == len(self.frames)):
+          self.repeated += 1
+          if (self.repeated == self.repeat_times):
+            self.i -= 1
+            self.finished = True
+          else:
+            self.i = 0
+        if Animation.cached_frames.has_key(self.cache_name + str(self.i)):
+          self.image = Animation.cached_frames[self.cache_name + str(self.i)]
+        else:
+          self.image = (self.frames[self.i]).get_image()
+          Animation.cached_frames[self.cache_name + str(self.i)] = self.image
+    return self.image
\ No newline at end of file
diff --git a/lib/blob.py b/lib/blob.py
new file mode 100644
index 0000000..32dae27
--- /dev/null
+++ b/lib/blob.py
@@ -0,0 +1,75 @@
+'''Blob. Jumps when the player jumps, and damages the player and dies when it hits the player.'''
+
+import pygame
+import random
+
+from pygame.locals import *
+
+from locals import *
+
+import data
+
+from object import DynamicObject
+from sound import play_sound
+from animation import Animation
+from particle import Particle
+
+class Blob(DynamicObject):
+
+  def __init__(self, screen, x, y, player):
+    DynamicObject.__init__(self, screen, x, y, 10, True, True)
+    self.animations["default"] = Animation("blob", "standing")
+    self.animations["dying"] = Animation("blob", "dying")
+    self.animations["jumping"] = Animation("blob", "jumping")
+    self.animations["falling"] = Animation("blob", "falling")
+    self.image = self.animations[self.current_animation].update_and_get_image()
+    self.rect = self.image.get_rect()
+    self.rect.centerx = int(self.x)
+    self.rect.centery = int(self.y)
+    self.jump_queue = False
+    self.itemclass = "blob"
+    self.player = player
+    return
+
+  def update(self, level = None):
+    DynamicObject.update(self, level)
+
+    blood = []
+
+    if not self.active:
+      return blood
+
+    if self.on_ground:
+      if self.current_animation != "dying":
+        self.current_animation = "default"
+      if self.jump_queue:
+        self.true_jump()
+        count = 0
+        while (count < 5):
+          count += 1
+          blood.append(Particle(self.screen, 10, self.rect.centerx + random.uniform(-7, 7), self.rect.bottom, 0.0, -0.5, 0.3, level.dust_color, 4))
+    else:
+      self.dy = self.dy - BLOB_AIR_JUMP
+      if self.dy > 0 and self.current_animation == "jumping":
+        self.current_animation = "default"
+      if self.dy > 2 and self.current_animation == "default":
+        self.current_animation = "falling"
+      self.jump_queue = False
+
+    if self.current_animation != "dying" and self.rect.colliderect(self.player.rect):
+      self.die()
+      blood = self.player.take_damage(BLOB_DAMAGE)
+
+    return blood
+
+  def true_jump(self):
+    if self.on_ground and self.current_animation != "dying":
+      self.current_animation = "jumping"
+      self.jump_queue = False
+      self.dy = -BLOB_JUMP_ACC
+
+  def jump(self):
+    if self.active:
+      self.jump_queue = True
+    return
+    
diff --git a/lib/data.py b/lib/data.py
new file mode 100644
index 0000000..bf93ed5
--- /dev/null
+++ b/lib/data.py
@@ -0,0 +1,36 @@
+'''Simple data loader module.
+
+Loads data files from the "data" directory shipped with a game.
+
+Enhancing this to handle caching etc. is left as an exercise for the reader.
+'''
+
+import os
+
+data_py = os.path.abspath(os.path.dirname(__file__))
+data_dir = os.path.normpath(os.path.join(data_py, '..', 'data'))
+
+def filepath(filename):
+    '''Determine the path to a file in the data directory.
+    '''
+    return os.path.join(data_dir, filename)
+
+def picpath(object, animation, frame = None):
+    if (frame == None):
+      return os.path.join(data_dir, "pictures", object + "_" + animation + ".png")
+    else:
+      return os.path.join(data_dir, "pictures", object + "_" + animation + "_" + str(frame) + ".png")
+
+def animpath(object, animation):
+    return os.path.join(data_dir, "pictures", object + "_" + animation + ".txt")
+
+def levelpath(levelname):
+    return os.path.join(data_dir, "levels", levelname + ".txt")
+
+def load(filename, mode='rb'):
+    '''Open a file in the data directory.
+
+    "mode" is passed as the second arg to open().
+    '''
+    return open(os.path.join(data_dir, filename), mode)
+
diff --git a/lib/edit_utils.py b/lib/edit_utils.py
new file mode 100644
index 0000000..a3895ce
--- /dev/null
+++ b/lib/edit_utils.py
@@ -0,0 +1,41 @@
+import pygame
+
+from pygame.locals import *
+
+from locals import *
+
+from util import render_text
+from variables import Variables
+
+from level import Change
+
+class Edit_utils:
+
+  def __init__(self):
+    self.cursor = [0, 0]
+    return
+
+  def update(self, inputs):
+    if inputs.has_key("REMOVE_TILE"):
+      return Change("remove", self.cursor)
+    if inputs.has_key("ADD_TILE_WALL"):
+      return Change("W", self.cursor)
+    if inputs.has_key("ADD_TILE_SPIKES"):
+      return Change("S", self.cursor)
+    if inputs.has_key("ADD_TILE_BARS"):
+      return Change("B", self.cursor)
+    if inputs.has_key("SAVE_TILES"):
+      return Change("save", (0, 0))
+    if inputs.has_key("EDIT_RIGHT") and self.cursor[0] < (TILES_HOR - 1):
+      self.cursor[0] += 1
+    if inputs.has_key("EDIT_LEFT") and self.cursor[0] > 0:
+      self.cursor[0] -= 1
+    if inputs.has_key("EDIT_DOWN") and self.cursor[1] < (TILES_VER - 1):
+      self.cursor[1] += 1
+    if inputs.has_key("EDIT_UP") and self.cursor[1] > 0:
+      self.cursor[1] -= 1
+    return None
+
+  def render(self, screen):
+    pygame.draw.rect(screen, COLOR_GUI_EDIT_HILIGHT, pygame.Rect(self.cursor[0]*TILE_DIM, self.cursor[1]*TILE_DIM, TILE_DIM, TILE_DIM), 2)
+    return
diff --git a/lib/frame.py b/lib/frame.py
new file mode 100644
index 0000000..f451a72
--- /dev/null
+++ b/lib/frame.py
@@ -0,0 +1,31 @@
+import pygame
+import os
+
+from pygame.locals import *
+
+from locals import *
+
+from log import error_message
+
+import data
+
+class Frame:
+
+  def __init__(self, object, anim_name, frameno, frame_length):
+    try:
+      self.image = pygame.image.load(data.picpath(object, anim_name, frameno)).convert()
+    except:
+      try:
+        self.image = pygame.image.load(data.picpath("brown", anim_name, frameno)).convert() #Fallback to brown tileset
+      except:
+        self.image = pygame.image.load(data.picpath("object", "idle", 0)).convert() #Fallback to default object image
+        error_message("Object graphic missing: " + object + "_" +  anim_name + "_" + str(frameno))
+    self.frame_length = frame_length
+    self.image.set_colorkey((255,0,255))
+    return
+
+  def get_image(self):
+    return self.image
+
+  def get_time(self):
+    return self.frame_length
\ No newline at end of file
diff --git a/lib/game.py b/lib/game.py
new file mode 100644
index 0000000..f0afd84
--- /dev/null
+++ b/lib/game.py
@@ -0,0 +1,507 @@
+'''The main game module. One big event loop in the run function plus a few helper functions.'''
+
+import pygame
+from pygame.locals import *
+import os
+import random
+
+from locals import *
+
+from player import Player
+from spider import Spider
+from particle import Particle
+from level import Level
+from sound import play_sound
+from edit_utils import Edit_utils
+from util import *
+from variables import Variables
+from log import error_message
+from trigger import Trigger
+from visibleobject import flip_direction_from_position
+
+import data
+
+keys_released = {}
+
+def render_edit_utilities(screen):
+  return
+
+#This function renders the in-game GUI on the screen.
+def render_gui(screen, life, score, topleft):
+  score_image = render_text("Score: " + str(score) )
+  life_image = render_text("Life:")
+  #life_bar_bg_image = Util.cached_images["health_bar_empty"]
+  #life_bar_image = Util.cached_images["health_bar_fill"]
+  version_image = render_text("0.7.9")
+
+  rect = score_image.get_rect()
+  rect.left = topleft[0]
+  rect.top = topleft[1]
+  screen.blit(score_image, rect)
+
+  rect.left = topleft[0] + 26
+  rect.top = topleft[1] + 26
+  rect.width = 38
+  rect.height = 8
+  pygame.draw.rect(screen, COLOR_GUI_BG, rect)
+  if life > 0:
+    rect.left = topleft[0] + 27
+    rect.top = topleft[1] + 27
+    rect.width = life
+    rect.height = 6
+    pygame.draw.rect(screen, COLOR_BLOOD, rect)
+
+  rect = life_image.get_rect()
+  rect.left = topleft[0]
+  rect.top = topleft[1] + 20
+  screen.blit(life_image, rect)
+
+  rect = version_image.get_rect()
+  rect.right = SCREEN_WIDTH - 2
+  rect.bottom = SCREEN_HEIGHT - 2
+  screen.blit(version_image, rect)
+  return
+
+#This function parses inputs from the keyboard and returns them as an array
+def parse_inputs(joystick = None):
+  keys = pygame.key.get_pressed()
+  inputs = {}
+
+  if keys[K_LEFT]:
+    inputs["LEFT"] = True
+    if keys_released["K_LEFT"]:
+      inputs["EDIT_LEFT"] = True
+    keys_released["K_LEFT"] = False
+  else:
+    keys_released["K_LEFT"] = True
+
+  if keys[K_RIGHT]:
+    inputs["RIGHT"] = True
+    if keys_released["K_RIGHT"]:
+      inputs["EDIT_RIGHT"] = True
+    keys_released["K_RIGHT"] = False
+  else:
+    keys_released["K_RIGHT"] = True
+
+  if keys[K_DOWN]:
+    if keys_released["K_DOWN"]:
+      inputs["DOWN"] = True
+      inputs["EDIT_DOWN"] = True
+    keys_released["K_DOWN"] = False
+  else:
+    keys_released["K_DOWN"] = True
+
+  if keys[K_z]:
+    inputs["UP"] = True
+    if keys_released["K_z"]:
+      inputs["JUMP"] = True
+    keys_released["K_z"] = False
+  else:
+    keys_released["K_z"] = True
+
+  if keys[K_p]:
+    if keys_released["K_p"]:
+      inputs["PAUSE"] = True
+    keys_released["K_p"] = False
+  else:
+    keys_released["K_p"] = True
+
+  if keys[K_w]:
+    inputs["ADD_TILE_WALL"] = True
+
+  if keys[K_s]:
+    if keys_released["K_s"]:
+      inputs["ADD_TILE_SPIKES"] = True
+      if (keys[K_RCTRL] or keys[K_LCTRL]):
+        inputs["SAVE_TILES"] = True
+        del inputs["ADD_TILE_SPIKES"]
+    keys_released["K_s"] = False
+  else:
+    keys_released["K_s"] = True
+
+  if keys[K_b]:
+    inputs["ADD_TILE_BARS"] = True
+
+  if keys[K_d]:
+    inputs["REMOVE_TILE"] = True
+
+
+  if keys[K_UP]:
+    inputs["UP"] = True
+    if keys_released["K_UP"]:
+      inputs["JUMP"] = True
+      inputs["EDIT_UP"] = True
+    keys_released["K_UP"] = False
+  else:
+    keys_released["K_UP"] = True
+
+  if keys[K_F10]:
+    inputs["SPECIAL"] = True
+
+  if joystick != None:   # Parse joystick input
+
+    axis0 = joystick.get_axis(0)
+
+    if axis0 < -0.1:
+      inputs["LEFT"] = True
+      inputs["ANALOG"] = -axis0
+
+    if axis0 > 0.1:
+      inputs["RIGHT"] = True
+      inputs["ANALOG"] = axis0
+
+    if joystick.get_numbuttons() > 1:
+      if joystick.get_button(0):
+        inputs["UP"] = True
+        if keys_released["J_B0"]:
+          inputs["JUMP"] = True
+        keys_released["J_B0"] = False
+      else:
+        keys_released["J_B0"] = True
+
+      if joystick.get_button(1):
+        if keys_released["J_B1"]:
+          inputs["DOWN"] = True
+        keys_released["J_B1"] = False
+      else:
+        keys_released["J_B1"] = True
+    else:
+      axis1 = joystick.get_axis(1)
+
+      if axis1 < -0.1:
+        inputs["UP"] = True
+        if keys_released["J_A1U"]:
+          inputs["JUMP"] = True
+        keys_released["J_A1U"] = False
+      else:
+        keys_released["J_A1U"] = True
+
+      if axis1 > 0.1:
+        if keys_released["J_A1D"]:
+          inputs["DOWN"] = True
+        keys_released["J_A1D"] = False
+      else:
+        keys_released["J_A1D"] = True
+
+  return inputs
+
+
+def run(screen, level_name = "w0-l0", score_mod = 0, score = None, joystick = None):
+
+  if (Variables.vdict["devmode"]):
+    edit_utils = Edit_utils()
+
+  done = False
+  objects = []
+  particles = []
+
+  if score == None:
+    score = Score(0)
+
+  #try:
+  level = Level(screen, level_name)
+  #except:
+  #  error_message("Couldn't open level '" + level_name + "'")
+  #  return END_QUIT
+
+  objects = level.get_objects()
+  player = level.get_player()
+  objects.append(player)
+
+  player.life = score.life
+
+  clock = pygame.time.Clock()
+
+  end_trigger = END_NONE
+  scripted_event_on = False
+
+  #There's no music at the moment:
+  #pygame.mixer.music.load( data.filepath(os.path.join("music", "music.ogg")) )
+  #pygame.mixer.music.play(-1)
+
+  scripted_events = level.get_scripted_events()
+  current_scripted_event = None
+
+  scripted_event_trigger = TRIGGER_LEVEL_BEGIN
+
+  flip_wait = -1
+
+  keys_released["K_z"] = True
+  keys_released["K_p"] = True
+  keys_released["K_s"] = True
+  keys_released["K_DOWN"] = True
+  keys_released["K_LEFT"] = True
+  keys_released["K_RIGHT"] = True
+  keys_released["K_UP"] = True
+  keys_released["J_B0"] = True
+  keys_released["J_B1"] = True
+
+  fading = True
+  fade_target = FADE_STATE_NONE
+  Util.fade_state = FADE_STATE_BLACK
+
+  flip_trigger_position = (0, 0)
+
+  changing_level = False
+
+  paused = False
+
+  #Main game loop
+
+  while (end_trigger == END_NONE or fading):
+
+    # Pygame event and keyboard input processing
+    for event in pygame.event.get():
+      if event.type == QUIT:
+        end_trigger = END_HARD_QUIT
+      if (event.type == KEYDOWN and event.key == K_ESCAPE):
+        end_trigger = END_QUIT
+        if fading == False:
+          fading = True
+        fade_target = FADE_STATE_HALF
+
+    inputs = parse_inputs(joystick)
+
+    trigger = None
+
+    if scripted_event_on:
+      if inputs.has_key("JUMP") or inputs.has_key("DOWN"):
+        cleared = True
+
+    moved = False
+
+    add_time = False #The ingame time counter toggle - this is False when the player can't control the character
+
+    if not scripted_event_on and not level.flipping and not fading and not paused \
+    and player.current_animation != "dying" and player.current_animation != "exit":
+      #There isn't anything special going on: player can control the character
+      #Translates input to commands to the player object
+      add_time = True
+      if inputs.has_key("LEFT"):
+        player.move((-PLAYER_MAX_ACC, 0))
+        moved = True
+
+      if inputs.has_key("RIGHT"):
+        player.move((PLAYER_MAX_ACC, 0))
+        moved = True
+
+      if inputs.has_key("JUMP"):
+        if (player.on_ground):
+          count = 0
+          while (count < 5):
+            count += 1
+            particles.append(Particle(screen, 10, player.rect.centerx - player.dx / 4 + random.uniform(-3, 3), player.rect.bottom, -player.dx * 0.1, -0.5, 0.3, level.dust_color, 4))
+          player.jump()
+
+          #The blobs always try to jump when the player jumps
+
+          for o in objects:
+            if o.itemclass == "blob":
+              o.jump()
+
+      if inputs.has_key("UP") and not player.on_ground:
+        player.jump()
+
+      if inputs.has_key("DOWN"):
+        pick_up_item = level.pick_up(player.x, player.y)
+        if pick_up_item != None:
+          play_sound("coins")
+          player.inventory.append(pick_up_item)
+          scripted_event_trigger = pick_up_item.itemclass
+
+        #If the level is not flipping at the moment, the player can trigger stuff in the level
+        if flip_wait == -1:
+          trigger = level.trigger(player.x, player.y)
+
+      #Debug command for flipping:
+      if inputs.has_key("SPECIAL"):
+        trigger = Trigger(TRIGGER_FLIP, player.x, player.y)
+
+    if inputs.has_key("PAUSE") and player.current_animation != "dying":
+      paused = not paused
+
+    #Decelerates the player, if he doesn't press any movement keys or when he is dead and on the ground
+    if ((player.current_animation != "dying" and not moved) or (player.current_animation == "dying" and player.on_ground)) and not paused:
+      player.dec((PLAYER_MAX_ACC, 0))
+
+    if trigger != None and trigger.trigger_type == TRIGGER_FLIP:
+      if flip_wait == -1:
+        flip_wait = 0
+        flip_trigger_position = (trigger.x, trigger.y)
+        play_sound("woosh")
+
+    if flip_wait != -1 and not paused:
+      flip_wait += 1
+      if flip_wait > FLIP_DELAY:
+        flip_direction = flip_direction_from_position(flip_trigger_position)
+        flip_wait = -1
+        level.flip(flip_direction)
+        for o in objects:
+          o.flip(flip_direction)
+        for p in particles:
+          p.flip()
+
+    #Dust effect rising from the character's feet:
+
+    if (player.current_animation == "walking"):
+      particles.append(Particle(screen, 10, player.rect.centerx - player.dx / 2 + random.uniform(-2, 2), player.rect.bottom, -player.dx * 0.1, 0.1, 0.3, level.dust_color))
+
+    #Updating level and objects:
+
+    if scripted_event_trigger == None:
+      scripted_event_trigger = level.update()
+    else:
+      level.update()
+
+    #Objects are only updated when there's not a scripted event going on
+
+    normal_updating = not scripted_event_on and not fading and not paused
+
+    if changing_level:
+      player.update(level)
+    elif normal_updating:
+      for o in objects:
+        if o.dead and o.itemclass != "player":
+          objects.remove(o)
+          continue
+        new_particles = o.update(level)
+        if o.itemclass == "projectile":
+          if player.rect.collidepoint(o.x, o.y) and o.current_animation == "default":
+            new_particles = player.take_damage(o.damage)
+            o.die()
+        if type(new_particles) == list: #Sometimes the type of the return value is int (hackity hack)
+          if new_particles != None:
+            for p in new_particles:
+              particles.append(p)
+
+    if normal_updating or changing_level:
+      for p in particles:
+        p.update()
+        if p.dead:
+          particles.remove(p)
+
+    #Rendering level - background and tiles
+    level.render()
+
+    #Rendering objects and particles
+    for o in objects:
+      if o.itemclass == "player":
+        o.render(None, None, (fading or paused) )
+      else:
+        o.render(None, None, (scripted_event_on or fading or paused) )
+      #On special conditions the animations aren't updated. The player is updated on a scripted event, others are not.
+
+    for p in particles:
+      p.render()
+
+    #Rendering GUI on top of game graphics:
+    if (not paused) or (not Variables.vdict["devmode"]):
+      render_gui(screen, player.life, score.score, (5, 5))
+
+    # Scripted event triggering:
+
+    if scripted_event_trigger != None:
+      if player.on_ground:
+        for ev in scripted_events:
+          if ev.trigger_type == scripted_event_trigger:
+            scripted_event_on = True
+            current_scripted_event = ev
+            current_scripted_event_element = None
+            text = None
+            phase = 0
+            cleared = False        # Clearing dialog boxes
+            player.dy = 0
+            player.dx = 0
+            player.update()
+            scripted_event_trigger = None
+
+    # Scripted event processing:
+
+    if scripted_event_on and not fading and not paused:
+      if (current_scripted_event_element == None) or (current_scripted_event_element.finished):
+
+        current_scripted_event_element = current_scripted_event.next_element()
+
+        if current_scripted_event_element.event_type == "end":
+          scripted_event_on = False
+          current_scripted_event_element = None
+
+      else:
+
+        if not Variables.vdict["dialogue"]:  #Dialogue skipping
+          while (current_scripted_event_element.event_type == "dialogue" or current_scripted_event_element.event_type == "player"):
+            current_scripted_event_element.finished = True
+            current_scripted_event_element = current_scripted_event.next_element()
+            if current_scripted_event_element.event_type == "end":
+              current_scripted_event_element.finished = True
+
+        if current_scripted_event_element.event_type == "wait":
+          current_scripted_event_element.finished = True
+        elif current_scripted_event_element.event_type == "dialogue":
+          if text == None:
+            text = current_scripted_event_element.text
+            phase = 0
+          phase = render_text_dialogue(screen, text, phase)
+          if (phase == -1) and cleared:
+            current_scripted_event_element.finished = True
+            phase = 0
+            cleared = False
+            text = None
+          if cleared:
+            phase = -1
+            cleared = False
+        elif current_scripted_event_element.event_type == "player":
+          if current_scripted_event_element.text == "orientation":
+            player.orientation = current_scripted_event_element.orientation
+          current_scripted_event_element.finished = True
+        elif current_scripted_event_element.event_type == "change_level":
+          score.score += (5 + score_mod) * ((player.life + 4) / 5 + 12)
+          score.levels += 1
+          current_scripted_event_element.finished = True
+          if player.current_animation != "gone":
+            player.exit()
+
+    if player.current_animation == "exit":
+      changing_level = True
+    elif changing_level:
+      end_trigger = END_NEXT_LEVEL
+      fading = True
+      fade_target = FADE_STATE_BLACK
+
+
+    if player.dead:
+      end_trigger = END_LOSE
+      fading = True
+      fade_target = FADE_STATE_HALF
+
+    #And finally, rendering the pause button:
+
+    if paused:
+      if (Variables.vdict["devmode"]):
+        change = edit_utils.update(inputs)
+        level.change(change)
+        edit_utils.render(screen)
+      else:
+        render_text_dialogue(screen, "Game paused. Press P to continue.", -1, "p")
+
+    #Render fading on top of everything else:
+
+    if (fading or Util.fade_state != FADE_STATE_NONE):
+      if fade_to_black(screen, fade_target):
+        #Fading finished
+        fading = False
+
+    if(add_time):
+      score.time += 1
+
+    #Display, clock
+
+    pygame.display.flip()
+
+    clock.tick(FPS)
+
+  #Main game loop finished
+
+  score.life = player.life #To make the player's health stay the same to the next level
+
+  return end_trigger
diff --git a/lib/item.py b/lib/item.py
new file mode 100644
index 0000000..eb84e7d
--- /dev/null
+++ b/lib/item.py
@@ -0,0 +1,56 @@
+import pygame
+import os
+
+from pygame.locals import *
+
+from locals import *
+
+import data
+
+from visibleobject import VisibleObject
+from animation import Animation
+from trigger import Trigger
+from log import log_message
+
+class Item(VisibleObject):
+
+  def __init__(self, screen, x = None, y = None, set = "brown", itemclass = "key", max_activations = 1, trigger_type = None):
+    VisibleObject.__init__(self, screen, x, y)
+    self.animations["default"] = Animation(set, itemclass)
+
+    try:
+      self.animations["broken"] = Animation(set, itemclass + "_broken")
+    except:
+      self.animations["broken"] = self.animations["default"]
+
+    self.image = self.animations[self.current_animation].update_and_get_image()
+    self.rect = self.image.get_rect()
+    self.itemclass = itemclass
+    self.activated_times = 0
+    self.max_activations = max_activations
+    self.trigger = None
+    if trigger_type != None:
+      self.trigger = Trigger(trigger_type, x, y)
+    else:
+      self.pickable = True
+    return
+
+  def activate(self):
+    if self.itemclass == "lever":
+      self.activated_times += 1
+      if (self.activated_times <= self.max_activations) or (self.max_activations == -1):
+        if (self.activated_times == self.max_activations):
+          self.current_animation = "broken"
+        self.trigger.x = self.x
+        self.trigger.y = self.y
+        return self.trigger
+
+    return None
+
+  def to_str(self, verbose = True):
+    string = VisibleObject.to_str(self, False)
+    if self.trigger != None:
+      string += " " + str(self.max_activations) + " " + self.trigger.trigger_type
+    if verbose:
+      log_message("Obj converted to string: " + string)
+    return string
\ No newline at end of file
diff --git a/lib/level.py b/lib/level.py
new file mode 100644
index 0000000..3b07296
--- /dev/null
+++ b/lib/level.py
@@ -0,0 +1,355 @@
+import pygame
+import os
+import codecs
+
+from pygame.locals import *
+
+from locals import *
+
+import data
+from util import dir_from_str
+
+from log import error_message, log_message
+
+from tile import Tile
+from spikes import Spikes
+from item import Item
+from player import Player
+from spider import Spider
+from blob import Blob
+from scripted_event import Scripted_event
+from animation import Animation
+from trigger import Trigger
+from visibleobject import tile_coords_to_screen_coords
+
+class Change:
+
+  def __init__(self, tile_change, coords):
+    self.tile_change = tile_change
+    self.coords = coords
+    return
+
+class Level:
+
+  def __init__(self, screen, level_name = "w0-l0"):
+    self.screen = screen
+    self.image = None
+    self.flipping = False
+    self.flipcounter = 0
+    self.set = "brown"  #The default tileset, can be changed through level configuration
+
+    self.tiles = []
+    self.objects = []
+
+    self.scripted_events = []
+
+    self.cached_ground_check = {}
+
+    self.dust_color = COLOR_DUST["brown"]
+
+    self.level_name = level_name
+
+    self.orientation = 0
+
+    conffile = codecs.open(data.levelpath(self.level_name), "r", "utf_8")
+
+    tiley = 0
+    values = []
+
+    trigger = False
+    current_event = None
+
+    parse_tiles = False
+
+    for line in conffile:
+
+      if parse_tiles:
+        if tiley < FULL_TILES_VER:
+          tilex = 0
+          while tilex < FULL_TILES_VER:
+            if (line[tilex] == "W") or (line[tilex] == "B") or (line[tilex] == "S"):
+              self.add_tile(line[tilex], (tilex, tiley))
+            tilex += 1
+          tiley += 1
+          continue
+        else:
+          parse_tiles = False
+          continue
+
+      elif line.strip() != "":
+          values = line.split()
+
+          #Parsing special commands
+
+          if trigger:
+            if values[0] == "end" and values[1] == "trigger":
+              trigger = False
+            else:
+              current_event.add_element(line)
+            continue
+          elif values[0] == "trigger":
+            trigger = True
+            current_event = Scripted_event(values[1], int(values[2]))
+            self.scripted_events.append(current_event)
+            continue
+          elif values[0] == "tiles":
+            parse_tiles = True
+            continue
+          elif values[0] == "set":
+            self.set = values[1]
+            continue
+
+          #Parsing objects
+          x, y = tile_coords_to_screen_coords(values[1], values[2])
+          if values[0] == "player":
+            self.player = Player(self.screen, x, y)
+            continue
+          elif values[0] == "spider":
+            self.objects.append(Spider( self.screen, x, y, dir_from_str(values[3]) ))
+            continue
+          elif values[0] == "blob":
+            self.objects.append(Blob(self.screen, x, y, self.player))
+            continue
+          elif values[0] == "lever":
+            trigger_type = TRIGGER_FLIP
+            if values[4] == "TRIGGER_FLIP":
+              trigger_type = TRIGGER_FLIP
+            self.objects.append( Item(self.screen, x, y, self.set, values[0], int(values[3]), trigger_type) )
+            continue
+          else:
+            try:
+              self.objects.append(Item(self.screen, x, y, self.set, values[0]))
+            except:
+              error_message("Couldn't add object '" + values[0] + "'")
+            continue
+
+    self.dust_color = COLOR_DUST[self.set]
+
+    self.bg_animations = {}
+    self.bg_animations["default"] = Animation(self.set + "_background", "static")
+    self.current_animation = "default"
+    self.rect = (self.bg_animations[self.current_animation].update_and_get_image()).get_rect()
+    self.rect.centerx = SCREEN_WIDTH / 2
+    self.rect.centery = SCREEN_HEIGHT / 2
+
+    self.reset_active_tiles()
+    return
+
+  def update(self):
+    return_trigger = None
+    if self.flipping:
+      self.flipcounter += 1
+      if self.flipcounter > FLIP_FRAMES:
+        self.flipcounter = 0
+        self.flipping = False
+        self.reset_active_tiles()
+        return_trigger = TRIGGER_FLIPPED
+        self.image = None
+      for t in self.tiles:
+        t.update()
+    return return_trigger
+
+  def reset_active_tiles(self):
+    self.active_tiles = []
+    for t in self.tiles:
+      if (t.x > 0 and t.y > 0):
+        self.active_tiles.append(t)
+    return
+
+  def get_objects(self):
+    return self.objects
+
+  def get_player(self):
+    return self.player
+
+  def get_scripted_events(self):
+    return self.scripted_events
+
+  #Renders the background and the tiles
+  def render(self):
+    if self.flipping or self.image == None or self.edited:
+      self.image = pygame.Surface((self.rect.width, self.rect.height))
+      bg = self.bg_animations[self.current_animation].update_and_get_image()
+      self.image.blit(bg, self.rect)
+      for t in self.tiles:
+        t.render(self.image)
+      self.edited = False
+
+    #Blits the cached background
+    self.screen.blit(self.image, self.rect)
+    return
+
+  #Starts the flipping of the level
+  def flip(self, flip_direction = CLOCKWISE):
+    if self.flipping:
+      return
+    else:
+      self.cached_ground_check = {}
+      self.flipping = True
+      if (flip_direction == CLOCKWISE):
+        self.orientation += 1
+      if (flip_direction == COUNTER_CLOCKWISE):
+        self.orientation -= 1
+      for t in self.tiles:
+        t.flip(flip_direction)
+      return
+
+  #Triggers an object in the position specified
+  def trigger(self, x, y):
+    for o in self.objects:
+      if o.rect.collidepoint(x, y):
+        if o.itemclass == "lever":
+          trigg = o.activate()
+          if trigg != None:
+            return trigg
+    return None
+
+
+  #Gives an object from the level (also removes it from the level)
+  def pick_up(self, x, y):
+    for o in self.objects:
+      if o.rect.collidepoint(x, y):
+        if o.pickable:
+          self.objects.remove(o)
+          return o
+    return None
+
+
+  #Checks the point for solid ground
+  def ground_check(self, x, y):
+    if self.cached_ground_check.has_key(str(x) + "_" +  str(y)):
+      return self.cached_ground_check[str(x) + "_" +  str(y)]
+    else:
+      if x > SCREEN_WIDTH or y > SCREEN_HEIGHT or x < 0 or y < 0:
+        return True
+      for t in self.active_tiles:
+        if t.rect.collidepoint(x, y):
+          self.cached_ground_check[str(x) + "_" +  str(y)] = True
+          return True
+      self.cached_ground_check[str(x) + "_" +  str(y)] = False
+      return False
+
+  #This functions tests (approximately) if a rect collides with another and from which direction.
+  #It's one of the most performance-heavy functions in the game, and thus should be optimized.
+  #indexing: right left bottom top
+  def collide(self, rect, dy, dx, topcollision = True):
+    collision = [None, None, None, None, 0]
+    for t in self.active_tiles:
+      if not t.is_aligned():
+        #Sometimes collisions were misdetected just after the level was flipped,
+        #so this is an extra check to avoid that.
+        #Should look into the order things are done when flipping finishes
+        #to fix the problem properly
+        continue
+      if t.rect.collidepoint(rect.right + 1, rect.centery - dy) and dx > 0:
+        collision[RIGHT] = t.rect.left
+      if (t.rect.collidepoint(rect.right + 1, rect.bottom - dy - 1) or t.rect.collidepoint(rect.right + 1, rect.top - dy + 1)) and dx > 0:
+        collision[RIGHT] = t.rect.left
+      if t.rect.collidepoint(rect.left - 1, rect.centery - dy) and dx < 0:
+        collision[LEFT] = t.rect.right
+      if (t.rect.collidepoint(rect.left - 1, rect.bottom - dy - 1) or t.rect.collidepoint(rect.left - 1, rect.top - dy + 1)) and dx < 0:
+        collision[LEFT] = t.rect.right
+      if (t.rect.collidepoint(rect.centerx - dx, rect.bottom + 1) or t.rect.collidepoint(rect.right - dx - 1, rect.bottom + 1) or t.rect.collidepoint(rect.left - dx + 1, rect.bottom + 1)) and dy > 0:
+        if (t.itemclass == "spikes"):
+          if (collision[DOWN] == None):
+            collision[DAMAGE] = 5
+            collision[DOWN] = t.rect.top
+        else:
+          collision[DOWN] = t.rect.top
+          if collision[DAMAGE] > 0:
+            collision[DAMAGE] = 0
+      if t.rect.collidepoint(rect.centerx - dx, rect.top - 1) and dy < 0:
+        collision[UP] = t.rect.bottom
+      if (t.rect.collidepoint(rect.right - dx - 1, rect.top - 1) or t.rect.collidepoint(rect.left - dx + 1, rect.top - 1)) and dy < 0:
+        collision[UP] = t.rect.bottom
+
+    return collision
+
+
+  def change(self, change):
+    """Apply a change to the level data according to a Change class object."""
+    if change == None:
+      return
+
+    log_message("Made change " + change.tile_change + " to coords " + str(change.coords[0]) + ", " + str(change.coords[1]))
+
+    if (change.tile_change == "remove"):
+      self.remove_tile(change.coords)
+
+    elif (change.tile_change == "save"):
+      self.save()
+
+    elif (change.tile_change == "W") or (change.tile_change == "B") or (change.tile_change == "S"):
+      self.remove_tile(change.coords)
+      change.coords = (change.coords[0] + FULL_TILES_HOR - TILES_HOR, change.coords[1] + FULL_TILES_VER - TILES_VER)
+      self.add_tile(change.tile_change, change.coords)
+      self.reset_active_tiles()
+
+    return
+
+
+  #Uses to_str to convert the level to string form and then saves the level
+  #to a file, overwrites the old level file.
+  def save(self):
+    conffile = codecs.open(data.levelpath(self.level_name), "w", "utf_8")
+    string = self.to_str()
+    log_message('Level data to save:')
+    log_message(string)
+    log_message('Saving level to ' + data.levelpath(self.level_name))
+    conffile.write(string)
+    conffile.close()
+    log_message('Level saved.')
+    return
+
+
+  #Converts the level to a string with all the original level data
+  def to_str(self):
+    string = "set " + self.set + "\n\n"
+
+    string += "tiles" + "\n"
+
+    tilemap = [[] for i in range(FULL_TILES_VER)]
+    for row in tilemap:
+      for i in range(FULL_TILES_HOR):
+        row.append(' ')
+
+    for t in self.tiles:
+      tilemap[t.tiley][t.tilex] = t.tileclass[0].upper()
+
+    for row in tilemap:
+      for t in row:
+        string += t
+      string += "\n"
+    string += "\n"
+
+    for o in self.objects:
+      string += o.to_str(False) + "\n"
+
+    for s in self.scripted_events:
+      string += s.to_str() + "\n"
+    return string
+
+
+  def remove_tile(self, coords):
+    """Remove a tile from the level with coordinates relative to the corner of the area currently visible."""
+    for t in self.active_tiles:
+      if t.rect.collidepoint(coords[0]*TILE_DIM + TILE_DIM / 2, coords[1]*TILE_DIM + TILE_DIM / 2):
+        self.active_tiles.remove(t)
+        self.tiles.remove(t)
+        self.edited = True
+    return
+
+
+  def add_tile(self, tile_type, coords):
+    """Add a tile to the level with absolute coordinates in the current rotation state."""
+    new_tile = None
+    if tile_type == "W":
+      new_tile = Tile(self.screen, coords[0], coords[1], self.set)
+    elif tile_type == "B":
+      new_tile = Tile(self.screen, coords[0], coords[1], self.set, "bars")
+    elif tile_type == "S":
+      new_tile = Spikes(self.screen, coords[0], coords[1], self.set)
+    if new_tile != None:
+      self.tiles.append(new_tile)
+    self.edited = True
+    return
\ No newline at end of file
diff --git a/lib/locals.py b/lib/locals.py
new file mode 100644
index 0000000..e343a42
--- /dev/null
+++ b/lib/locals.py
@@ -0,0 +1,132 @@
+#This file contains constant values used throughout the game code.
+
+GAME_NAME_SHORT = "wwisup"
+
+SCREEN_WIDTH = 520
+SCREEN_HEIGHT = 520
+
+PLAY_AREA_WIDTH = 520
+PLAY_AREA_HEIGHT = 520
+
+FULL_TILES_HOR = 20
+FULL_TILES_VER = 20
+
+TILES_HOR = 13
+TILES_VER = 13
+
+TILE_DIM = 40
+
+PLAY_AREA_CENTER_X = (-FULL_TILES_HOR / 2 + TILES_HOR) * TILE_DIM
+PLAY_AREA_CENTER_Y = (-FULL_TILES_VER / 2 + TILES_VER) * TILE_DIM
+
+GRAVITY = 1.0
+GRAVITY_PARTICLE = 0.5
+
+PLAYER_JUMP_ACC = GRAVITY * 10.0
+
+PLAYER_AIR_JUMP = GRAVITY * 0.55
+
+FPS = 24
+
+#The time used flipping the level, in frames, and time from lever activation to the flipping, also in frames.
+FLIP_FRAMES = 30
+FLIP_DELAY = 15
+
+#In pixels per second or pixels per second^2
+PLAYER_MAX_SPEED = 6.0
+PLAYER_MAX_ACC = 4.0
+
+PLAYER_ACC_AIR_MULTIPLIER = 0.18
+
+PLAYER_COLLISION_ADJUST = 11 #In pixels
+PLAYER_LIFE = 36 #Hit points
+
+#Spikes offset for rect placement - the spikes are smaller than the rest of tiles:
+SPIKES_VER_OFFSET = 4
+
+#In frames
+SPIDER_FIRE_DELAY = 30
+
+SPIDER_TOO_WIDE = 7
+SPIDER_PROJECTILE_SPEED = 5
+SPIDER_DAMAGE = 5
+
+BLOB_JUMP_ACC = PLAYER_JUMP_ACC * 1.05
+BLOB_AIR_JUMP = GRAVITY * 0.5
+
+BLOB_DAMAGE = 5
+
+# Indexing for collision, including the "damage", which means the index of collision damage...
+# Also used for indexing player input.
+# This could be done better with a dictionary or just bare strings, I suppose.
+RIGHT = 0
+LEFT = 2
+DOWN = 1
+UP = 3
+JUMP = DAMAGE = 4
+SPECIAL = STAY = 5
+PAUSE = 6
+ANALOG = 7
+
+CLOCKWISE = 1
+COUNTER_CLOCKWISE = -1
+
+#Colors
+COLOR_DUST = {}
+COLOR_DUST["brown"] = (220, 200, 170)
+COLOR_DUST["green"] = (190, 185, 175)
+COLOR_DUST["grey"] = (190, 190, 185)
+COLOR_BLOOD = (200, 40, 10)
+COLOR_GUI_DARK = (120, 120, 120)
+COLOR_GUI = (230, 230, 230)
+COLOR_GUI_HILIGHT = (255, 255, 255)
+COLOR_GUI_BG = (0, 0, 0)
+
+GUI_MENU_TOP = 160
+
+#Editor colors:
+COLOR_GUI_EDIT_HILIGHT = (200, 0, 0)
+
+#Trigger indexes
+TRIGGER_FLIP = "TRIGGER_FLIP"
+TRIGGER_PICKUP = "TRIGGER_PICKUP"
+TRIGGER_TEXT = "TRIGGER_TEXT"
+TRIGGER_FLIPPED = "flipped"
+TRIGGER_LEVEL_BEGIN = "level_begin"
+
+#For the GUI
+FONT_SIZE = 12
+
+#Game ending types
+END_NONE = 0
+END_LOSE = 1
+END_WIN = 2
+END_NEXT_LEVEL = 3
+END_QUIT = 4
+END_HARD_QUIT = 5
+END_MENU = 6
+
+TOTAL_LEVELS = 7
+
+MENU_QUIT = -5
+MENU_SOUND = -4
+MENU_DIALOGUE = -3
+MENU_FULLSCREEN = -2
+MENU_WORLD = -1
+
+MENU_OFFSET = 5
+
+MENU_MAX_VISIBLE = 7
+
+FADE_IN = -0.7
+FADE_NONE = 0
+FADE_OUT = 0.7
+FADE_STATE_BLACK = 255
+FADE_STATE_HALF = 128
+FADE_STATE_NONE = 0
+
+#World names must also correspond to the text file names in the levels directory (.txt is added)
+WORLDS = ["Quest For The Keys", "The Other Side", "A Piece of Cake"]
+
+#How many old log lines are kept in the log file when the log is updated
+MAX_OLD_LOG_LINES = 50
\ No newline at end of file
diff --git a/lib/log.py b/lib/log.py
new file mode 100644
index 0000000..377700c
--- /dev/null
+++ b/lib/log.py
@@ -0,0 +1,32 @@
+"""A logging module - error messages using the error_message function
+will only be displayed on screen if the verbose setting is on, and written
+to the log variable by default. The log doesn't need to be initialized
+to be used, but you must call the write_log function in util.py on exit if you
+want the log to be saved."""
+
+from variables import Variables
+
+def error_message(string):
+    """Add a message specified as an error to the message log."""
+    log_message("Error: " + string)
+    return
+
+def log_message(string):
+    """Add a message to the message log, which can be written on disk later."""
+
+    #Multiple messages of the same type aren't added to the log:
+    if Variables.vdict.has_key("last_log_message"):
+      if string == Variables.vdict["last_log_message"]:
+        return
+        
+    if Variables.vdict['verbose']:
+        print(string)        
+
+    Variables.vdict["last_log_message"] = string
+
+    if Variables.vdict.has_key("log"):
+        Variables.vdict["log"] = string + "\n" + Variables.vdict["log"]
+    else:
+        Variables.vdict["log"] = string
+
+    return
\ No newline at end of file
diff --git a/lib/main.py b/lib/main.py
new file mode 100644
index 0000000..cfe9046
--- /dev/null
+++ b/lib/main.py
@@ -0,0 +1,169 @@
+'''Game main module.
+
+Contains the entry point used by the run_game.py script.
+The actual gameplay code is in game.py.
+'''
+
+import pygame
+import os
+import sys
+
+from pygame.locals import *
+
+from locals import *
+
+import data
+import game
+
+from util import Score, parse_config, write_config, write_log, apply_fullscreen_setting
+from variables import Variables
+from log import error_message
+from mainmenu import Mainmenu
+from world import World
+
+from sound import play_sound
+
+def main():
+
+    #Parsing level from parameters and parsing main config:
+
+    level_name = None
+    world_index = 0
+    world = World(WORLDS[world_index])
+
+    user_supplied_level = False
+
+    parse_config()
+
+    getlevel = False
+    
+    Variables.vdict["devmode"] = False
+
+    if len(sys.argv) > 1:
+        for arg in sys.argv:
+          if getlevel:
+            try:
+              level_name = arg
+              user_supplied_level = True
+              end_trigger = END_NEXT_LEVEL
+              menu_choice = MENU_QUIT
+            except:
+              error_message("Incorrect command line parameters")
+              level_name = None
+          elif arg == "-l":
+            getlevel = True
+          elif arg == "-dev":
+            Variables.vdict["devmode"] = True
+            Variables.vdict["verbose"] = True            
+          elif arg == "-v":
+            Variables.vdict["verbose"] = True
+
+    #Initializing pygame and screen
+
+    pygame.init()
+    screen = pygame.display.set_mode((SCREEN_WIDTH,SCREEN_HEIGHT))
+    caption = "Which way is up?"
+    if (Variables.vdict["devmode"]):
+      caption = caption + " - developer mode"
+    pygame.display.set_caption(caption)
+
+    apply_fullscreen_setting(screen)
+
+    if (pygame.joystick.get_count() > 0):
+      joystick = pygame.joystick.Joystick(0)
+      joystick.init()
+    else:
+      joystick = None
+
+    score = Score(0)
+
+    done = False
+
+    if not user_supplied_level:
+      if (Variables.vdict["unlocked" + WORLDS[0]] == 0): # Nothing unlocked, go straight to the game
+        end_trigger = END_NEXT_LEVEL
+        menu_choice = MENU_QUIT
+        level_name = world.get_level()
+      else:                                      # Go to the menu first
+        end_trigger = END_MENU
+        menu_choice = 0
+
+    bgscreen = None
+
+    #Menu and level changing loop, actual game code is in game.py:
+
+    while not done:
+      if end_trigger == END_NEXT_LEVEL:
+        if user_supplied_level:
+          end_trigger = game.run(screen, level_name, world.index, score, joystick)
+          if end_trigger == END_NEXT_LEVEL:
+            user_supplied_level = False
+            end_trigger = END_WIN
+        else:
+          end_trigger = game.run(screen, level_name, world.index, score, joystick)
+          if end_trigger == END_NEXT_LEVEL:
+            if world.is_next_level():
+              level_name = world.get_level()
+            else:
+              end_trigger = END_WIN
+          elif end_trigger == END_QUIT:
+            display_bg("quit", screen)
+            end_trigger = END_MENU
+            bgscreen = screen.copy()
+      if end_trigger == END_LOSE:
+        display_bg("lose", screen)
+        end_trigger = END_MENU
+        menu_choice = world.index - 1
+        bgscreen = screen.copy()
+      elif end_trigger == END_WIN:
+        display_bg("victory", screen)
+        end_trigger = END_MENU
+        menu_choice = 0
+        bgscreen = screen.copy()
+      elif end_trigger == END_QUIT or end_trigger == END_HARD_QUIT:
+        done = True
+      elif end_trigger == END_MENU:
+        prev_score = score.score
+        prev_time = score.time
+        prev_levels = score.levels
+        score = Score(0)
+        if prev_score != 0:
+          menu = Mainmenu(screen, prev_score, world, bgscreen, prev_time, prev_levels)
+        else:
+          menu = Mainmenu(screen, None, world, bgscreen)
+        menu_choice = menu.run(menu_choice)
+        if menu_choice == MENU_QUIT:
+          end_trigger = END_QUIT
+        elif menu_choice == MENU_SOUND:
+          Variables.vdict["sound"] = not Variables.vdict["sound"]
+          end_trigger = END_MENU
+        elif menu_choice == MENU_DIALOGUE:
+          Variables.vdict["dialogue"] = not Variables.vdict["dialogue"]
+          end_trigger = END_MENU
+        elif menu_choice == MENU_FULLSCREEN:
+          Variables.vdict["fullscreen"] = not Variables.vdict["fullscreen"]
+          end_trigger = END_MENU
+          apply_fullscreen_setting(screen)
+        elif menu_choice == MENU_WORLD:
+          world_index += 1
+          if world_index >= len(WORLDS):
+            world_index = 0
+          world = World(WORLDS[world_index])
+          end_trigger = END_MENU
+        else:
+          level_name = world.get_level(menu_choice)
+          end_trigger = END_NEXT_LEVEL
+
+    write_config()
+    write_log()
+
+    return
+
+def display_bg(key, screen):
+  bg_image = pygame.image.load(data.picpath("bg", key))
+  rect = bg_image.get_rect()
+  screen.blit(bg_image, rect)
+  return
+
+if __name__ == "__main__":
+  main()
diff --git a/lib/mainmenu.py b/lib/mainmenu.py
new file mode 100644
index 0000000..ef2ef7e
--- /dev/null
+++ b/lib/mainmenu.py
@@ -0,0 +1,104 @@
+import pygame
+import os
+
+from pygame.locals import *
+
+from locals import *
+
+import data
+
+from util import Util, Score, render_text, bool_to_str
+from variables import Variables
+from level import Level
+from menu import Menu
+
+class Mainmenu:
+
+  def __init__(self, screen, score = None, world = None, bgscreen = None, time = None, levels = None):
+    if bgscreen == None:
+      self.bgscreen = Util.blackscreen.copy()
+    else:
+      self.bgscreen = bgscreen.copy()
+    self.screen = screen
+    self.score = score
+    self.time = time
+    self.levels = levels
+    self.world = world
+    return
+
+  def run(self, menu_choice = 0):
+    done = False
+
+    #Static menu part
+
+    menu_items = ["Quit", "Sound: " + bool_to_str(Variables.vdict["sound"]), "Dialogue: " + bool_to_str(Variables.vdict["dialogue"]), "Fullscreen mode: " + bool_to_str(Variables.vdict["fullscreen"]), "Choose world: " + str(self.world.number)]
+
+    #Adds levels to the menu
+
+    count = 0
+
+    while (count <= Variables.vdict["unlocked" + self.world.name] and count < self.world.level_count):
+      menu_items.append("Play level " + str(count + 1))
+      count += 1
+
+    #Hi score and best time text on the bgscreen
+
+    if self.score != None:
+      score_text = "Your final score: %s" % str(self.score)
+      if self.levels == self.world.level_count:
+        time_text = "Your final time: %s frames" % str(self.time)
+      else:
+        time_text = "Didn't pass all levels"
+
+      if self.score > Variables.vdict["hiscore" + self.world.name]:
+        score_text += " - NEW HIGH SCORE!"
+        Variables.vdict["hiscore" + self.world.name] = self.score
+      else:
+        score_text += " - High score: %s" % Variables.vdict["hiscore" + self.world.name]
+
+      if (self.time < Variables.vdict["besttime" + self.world.name] or Variables.vdict["besttime" + self.world.name] == 0) and (self.levels == self.world.level_count):
+        time_text += " - NEW BEST TIME!"
+        Variables.vdict["besttime" + self.world.name] = self.time
+      elif Variables.vdict["besttime" + self.world.name] == 0:
+        time_text += " - Best time: no best time"
+      else:
+        time_text += " - Best time: %s frames" % Variables.vdict["besttime" + self.world.name]
+    else:
+      score_text = "High score: %s" % Variables.vdict["hiscore" + self.world.name]
+      if Variables.vdict["besttime" + self.world.name] == 0:
+        time_text = "Best time: no best time"
+      else:
+        time_text = "Best time: %s frames" % Variables.vdict["besttime" + self.world.name]
+
+
+    menu_image = render_text("World " + str(self.world.number) + ": " + self.world.name, COLOR_GUI)
+    rect = menu_image.get_rect()
+    rect.centerx = SCREEN_WIDTH / 2
+    rect.top = GUI_MENU_TOP - 75
+    self.bgscreen.blit(menu_image, rect)
+
+    menu_image = render_text(score_text, COLOR_GUI)
+    rect = menu_image.get_rect()
+    rect.centerx = SCREEN_WIDTH / 2
+    rect.top = GUI_MENU_TOP - 50
+    self.bgscreen.blit(menu_image, rect)
+
+    menu_image = render_text(time_text, COLOR_GUI)
+    rect = menu_image.get_rect()
+    rect.centerx = SCREEN_WIDTH / 2
+    rect.top = GUI_MENU_TOP - 30
+    self.bgscreen.blit(menu_image, rect)
+    
+    #Uses the menu class for the actual selection functionality
+
+    menu = Menu(self.screen, menu_items, self.bgscreen, "Which way is up?")
+
+    menu_choice = menu.run(menu_choice + MENU_OFFSET)
+
+    #Quit (-3) gets special treatment, because it's returned as a constant when the player presses ESC
+    #If offset would be applied to it, it would turn up -6
+
+    if not (menu_choice == MENU_QUIT):
+      menu_choice = menu_choice - MENU_OFFSET
+
+    return menu_choice
diff --git a/lib/menu.py b/lib/menu.py
new file mode 100644
index 0000000..8fb66f8
--- /dev/null
+++ b/lib/menu.py
@@ -0,0 +1,135 @@
+# A generic, keyboard- or joystick-controlled menu class.
+# Draws a list of strings on the screen, and the user can select one of them.
+#
+# Key configuration is hard-coded:
+# Up/down keys or the main vertical axis on the joystick change the selection
+# Return, Z, Space or joystick keys 1 or 2 make the selection.
+# Esc quits the menu.
+#
+# To use the module, initialize it with the following parameters:
+# screen - standard pygame surface the menu is drawn on
+# menu_items - a list of strings in the menu
+#
+# Optional initialization parameters:
+# bgscreen - the menu background covering the entire screen, a pygame surface
+# heading_text - a heading for the menu
+#
+# If the bgscreen isn't specified, the class uses a black screen instead.
+#
+# After initialization, use the run function. If you want some of the items
+# pre-selected, you can supply it with the index of the selection in the
+# menu_items list
+#
+# The run() function returns the index of the selection OR constant MENU_QUIT,
+# if the user presses Esc while in the menu.
+#
+# Menu placement on the screen is controlled by constants in the locals.py.
+# The use of the module also requires play_sound from the sound module,
+# Util class and render_text from the util module
+# and the Variables class from the variables module.
+
+import pygame
+import os
+
+from pygame.locals import *
+
+from locals import *
+
+import data
+
+from util import Util, render_text
+from variables import Variables
+
+from sound import play_sound
+
+class Menu:
+
+  def __init__(self, screen, menu_items, bgscreen = None, heading_text = None):
+    self.bgscreen = bgscreen
+    if self.bgscreen == None:
+      self.bgscreen = Util.blackscreen
+    self.screen = screen
+    self.menu_items = menu_items
+    self.heading_text = heading_text
+    return
+
+  def run(self, menu_choice = 0):
+    done = False
+
+    clock = pygame.time.Clock()
+
+    self.screen.blit(self.bgscreen, (0, 0))      #Renders the menu background, usually the faded out game display
+                                                 #Or a black screen
+    #Menu loop
+
+    while not done:
+
+      # Pygame event and keyboard input processing
+      for event in pygame.event.get():
+        if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
+          menu_choice = MENU_QUIT
+          done = True
+        elif (event.type == KEYDOWN and event.key == K_DOWN) or (event.type == JOYAXISMOTION and event.axis == 1 and event.value > 0.7):
+          if menu_choice + 1 < len(self.menu_items):
+            menu_choice += 1
+            play_sound("click")
+        elif (event.type == KEYDOWN and event.key == K_UP) or (event.type == JOYAXISMOTION and event.axis == 1 and event.value < -0.7):
+          if menu_choice > 0:
+            menu_choice -= 1
+            play_sound("click")
+        elif (event.type == KEYDOWN and (event.key == K_z or event.key == K_SPACE or event.key == K_RETURN)) or (event.type == JOYBUTTONDOWN and (event.button == 0 or event.button == 1)):
+          done = True
+
+      #Menu rendering
+
+      #Menu offset value centers the menu when the maximum amount of choices is not visible
+
+      if len(self.menu_items) < MENU_MAX_VISIBLE:
+        menu_offset = -(len(self.menu_items) - 5) * 10
+      else:
+        menu_offset = -(MENU_MAX_VISIBLE - 5) * 10
+
+      menu_bg = pygame.image.load(data.picpath("menu", "bg")).convert_alpha()
+      rect = menu_bg.get_rect()
+      rect.centerx = SCREEN_WIDTH / 2
+      rect.top = GUI_MENU_TOP
+      self.screen.blit(menu_bg, rect)
+
+      if self.heading_text != None:
+        menu_head = render_text(self.heading_text)
+        rect = menu_head.get_rect()
+        rect.centerx = SCREEN_WIDTH / 2
+        rect.top = GUI_MENU_TOP + 50 + menu_offset
+        self.screen.blit(menu_head, rect)
+
+      #If the menu choice is greater than the second last menu item on screen,
+      #the menu must be scrolled:
+      if menu_choice > (MENU_MAX_VISIBLE - 1):
+        current_menu_index = menu_choice - MENU_MAX_VISIBLE
+        if (menu_choice + 1) < len(self.menu_items):
+          current_menu_index += 1
+      else:
+        current_menu_index = 0
+
+      menu_visible = 0
+
+      while (not (menu_visible > MENU_MAX_VISIBLE or (current_menu_index) == len(self.menu_items))):
+        m = self.menu_items[current_menu_index]
+        if (menu_choice == current_menu_index):
+          menu_image = render_text(m, COLOR_GUI_HILIGHT, COLOR_GUI_DARK)
+        else:
+          menu_image = render_text(m, COLOR_GUI)
+        rect = menu_image.get_rect()
+        rect.centerx = SCREEN_WIDTH / 2
+        rect.top = GUI_MENU_TOP + 60 + (menu_visible + 1) * 20 + menu_offset
+        self.screen.blit(menu_image, rect)
+        current_menu_index += 1
+        menu_visible += 1
+
+      #Display, clock
+
+      pygame.display.flip()
+
+      clock.tick(FPS)
+
+    return menu_choice
\ No newline at end of file
diff --git a/lib/object.py b/lib/object.py
new file mode 100644
index 0000000..caeeb1c
--- /dev/null
+++ b/lib/object.py
@@ -0,0 +1,221 @@
+'''A game object class for almost everything - changed from Gameobject to DynamicObject after PyWeek.
+Might still need some cleaning up.'''
+
+import pygame
+import os
+import random
+
+from math import *
+
+from pygame.locals import *
+
+from locals import *
+
+import data
+
+from variables import Variables
+from particle import Particle
+from visibleobject import VisibleObject, screen_coords_to_tile_coords
+
+from sound import play_sound
+from log import log_message
+
+class DynamicObject(VisibleObject):
+
+  #The last parameter "colliding" might be one of the stupidest hacks ever,
+  #and has to do with objects moving while not having normal collision detection.
+  #(Spider cannons use the parameter).
+  #This probably should be reworked some time.
+  def __init__(self, screen, x, y, life = -1, gravity = False, colliding = False):
+    VisibleObject.__init__(self, screen, x, y)
+    self.dx = 0.0
+    self.dy = 0.0
+    self.initial_x = x
+    self.initial_y = y
+    self.gravity = gravity
+    self.colliding = colliding
+    self.active = (self.x + self.rect.width / 2 > 0) and (self.y + self.rect.height / 2 > 0)
+
+    self.on_ground = False
+
+    self.life = life
+    self.destructable = True
+    if (self.life == -1):
+      self.destructable = False
+
+    return
+
+  def acc(self, direction):
+    self.dx += direction[0]
+    self.dy += direction[1]
+    return
+
+  def dec(self, direction):
+    if abs(self.dx) < direction[0]:
+      self.dx = 0
+    else:
+      if self.dx > 0:
+        self.dx -= direction[0]
+      else:
+        self.dx += direction[0]
+
+    if abs(self.dy) < direction[1]:
+      self.dy = 0
+    else:
+      if self.dy > 0:
+        self.dy -= direction[1]
+      else:
+        self.dy += direction[1]
+    return
+
+  #A big-ass function for handling pretty much everything -
+  #flipping, moving, collision detection, taking damage from spikes.
+  #returns a list of particles for implementation of blood effects.
+  def update(self, level = None):
+
+    VisibleObject.update(self)
+
+    if self.flip_finished and self.itemclass != "player":
+      self.active = (self.x + self.rect.width / 2 > 0) and (self.y + self.rect.height / 2 > 0)
+
+    if self.flipping:
+      return
+
+    if not self.active:
+      return
+
+    if self.gravity:
+      self.dy += GRAVITY
+
+    self.x += self.dx
+    self.y += self.dy
+
+    if not self.colliding or level == None:
+      return
+
+    collision_type = self.check_collisions(level)
+
+    return collision_type
+
+  def flip(self, flip_direction = CLOCKWISE):
+    """Make the object flip with the level to either direction"""
+    if VisibleObject.flip(self, flip_direction):
+      if flip_direction == CLOCKWISE:
+        self.initial_x, self.initial_y = -self.initial_y + PLAY_AREA_WIDTH / TILES_HOR * (TILES_HOR*2 - FULL_TILES_HOR), self.initial_x
+      else:
+        self.initial_x, self.initial_y = self.initial_y, -self.initial_x + PLAY_AREA_WIDTH / TILES_HOR * (TILES_HOR*2 - FULL_TILES_HOR)
+    return
+
+  def check_collisions(self, level):
+    """Check for collisions and also change the object's position accordingly.
+    If there isn't a collision, it returns -1, if there is one, it returns 0,
+    And if the collision caused damage, it returns the amount of damage.
+    The function also updates the object's self.on_ground variable."""
+
+    collision_type = -1
+
+    self.on_ground = False
+
+    if self.x < 0 + self.rect.width / 2:
+      self.x = 0 + self.rect.width  / 2
+      self.dx = 0
+      collision_type = 0
+
+    if self.x > PLAY_AREA_WIDTH - self.rect.width  / 2:
+      self.x = PLAY_AREA_WIDTH - self.rect.width  / 2
+      self.dx = 0
+      collision_type = 0
+
+    # The commented block is the collision code for the upper edge of the screen.
+    # The spiders and projectiles might need this, but they use simplified
+    # collision detection for better performance anyway.
+    '''if self.y < 0 + self.rect.height / 2:
+      self.y = 0 + self.rect.height  / 2
+      self.dy = 0'''
+
+    if self.y > PLAY_AREA_HEIGHT - self.rect.height / 2:
+      self.y = PLAY_AREA_HEIGHT - self.rect.height  / 2
+      self.dy = 0
+      self.on_ground = True
+      collision_type = 0
+
+    if (level != None):
+      self.rect.centerx = int(self.x)
+      self.rect.centery = int(self.y)
+      if self.itemclass == "player":
+        self.rect.top += PLAYER_COLLISION_ADJUST
+        self.rect.height -= PLAYER_COLLISION_ADJUST
+        level_collision = level.collide(self.rect, self.dy, self.dx, True)
+        self.rect.height += PLAYER_COLLISION_ADJUST
+        self.rect.top -= PLAYER_COLLISION_ADJUST
+      else:
+        level_collision = level.collide(self.rect, self.dy, self.dx, True)
+      if (level_collision[RIGHT] != None):
+        self.x = level_collision[RIGHT] - float(self.rect.width) / 2.0 - 1.0
+        self.dx = 0
+        collision_type = 0
+      if (level_collision[LEFT] != None):
+        self.x = level_collision[LEFT] + float(self.rect.width) / 2.0 + 1.0
+        self.dx = 0
+        collision_type = 0
+      if (level_collision[DOWN] != None):
+        self.y = level_collision[DOWN] - float(self.rect.height) / 2.0 - 1.0
+        self.dy = 0
+        self.on_ground = True
+        collision_type = 0
+      if (level_collision[UP] != None):
+        if self.itemclass == "player":
+          self.y = level_collision[UP] + float(self.rect.height) / 2.0 + 1.0 - PLAYER_COLLISION_ADJUST
+        else:
+          self.y = level_collision[UP] + float(self.rect.height) / 2.0 + 1.0
+        self.dy = 0
+        collision_type = 0
+
+    if (level_collision[DAMAGE] > 0):
+      collision_type = level_collision[DAMAGE]
+
+    return collision_type
+
+  def get_orientation(self):
+    """Get the direction the object is facing"""
+    if (self.dx < 0):
+      orientation = LEFT
+    if (self.dx > 0):
+      orientation = RIGHT
+    try:
+      return orientation
+    except:
+      return self.orientation
+
+  def render(self, surface = None, center = None, static_render = False):
+    VisibleObject.render(self, surface, center, static_render)
+    if Variables.vdict["devmode"] and not self.flipping:
+      VisibleObject.render(self, surface, (self.initial_x, self.initial_y), static_render, 100)
+    return
+    
+  def to_str(self, verbose = True):
+    string = self.itemclass
+    tx, ty = screen_coords_to_tile_coords(self.initial_x, self.initial_y)
+    string += " " + str(tx) + " " + str(ty)
+    if verbose:
+      log_message("Obj converted to string: " + string)
+    return string
+
+  def take_damage(self, amount, x = None, y = None):
+    """Make the object take the specified amount of damage.
+    Returns a list of particles for blood effects"""
+    blood = []
+    if self.destructable:
+      if (x == None):
+        x = self.x
+        y = self.y
+      self.life -= amount
+      count = 0
+      if self.current_animation != "dying":
+        while (count < amount):
+          blood.append(Particle(self.screen, 15, x + random.uniform(-3, 3), y + random.uniform(-3, 3), self.dx, self.dy, 0.3, COLOR_BLOOD, 4, True))
+          count += 1
+      if self.life < 1:
+        self.life = 0
+        self.die()
+    return blood
\ No newline at end of file
diff --git a/lib/pack.bat b/lib/pack.bat
new file mode 100644
index 0000000..aa5e91c
--- /dev/null
+++ b/lib/pack.bat
@@ -0,0 +1,24 @@
+python setup.py py2exe -b 1
+mkdir ..\..\whichwayisup_build_bin\
+move dist\* ..\..\whichwayisup_build_bin\
+cd ..
+cd ..
+copy "whichwayisup\README.txt" whichwayisup_build_bin\
+copy "whichwayisup\changelog.txt" whichwayisup_build_bin\
+mkdir whichwayisup_build_bin\data\
+mkdir whichwayisup_build_bin\data\pictures\
+mkdir whichwayisup_build_bin\data\sounds\
+mkdir whichwayisup_build_bin\data\music\
+mkdir whichwayisup_build_bin\data\misc\
+mkdir whichwayisup_build_bin\data\levels\
+copy whichwayisup\data\pictures\* whichwayisup_build_bin\data\pictures\
+copy whichwayisup\data\sounds\* whichwayisup_build_bin\data\sounds\
+copy whichwayisup\data\music\* whichwayisup_build_bin\data\music\
+copy whichwayisup\data\misc\* whichwayisup_build_bin\data\misc\
+copy whichwayisup\data\levels\* whichwayisup_build_bin\data\levels\
+cd whichwayisup
+cd lib
+del /Q *.pyc
+del /Q build\*
+rmdir /S /Q build
+rmdir /S /Q dist
diff --git a/lib/particle.py b/lib/particle.py
new file mode 100644
index 0000000..e59b932
--- /dev/null
+++ b/lib/particle.py
@@ -0,0 +1,61 @@
+# A more simplified game object class to make special effects with less performance cost
+
+import pygame
+import os
+import random
+from math import *
+
+from pygame.locals import *
+
+from locals import *
+
+import data
+
+class Particle:
+  def __init__(self, screen, life = 30, x = None, y = None, dx = None, dy = None, random_move = 0, color = COLOR_DUST, radius = 3, gravity = False):
+    self.screen = screen
+    self.init_life = life
+    self.life = life
+    self.dead = False
+    self.color = color
+    self.init_radius = radius
+    self.radius = radius
+    self.random_move = random_move
+    self.x = x
+    self.y = y
+    self.dx = dx
+    self.dy = dy
+    self.radius = radius
+    self.gravity = gravity
+    if (self.x == None):
+      self.x = SCREEN_WIDTH / 2
+    if (self.y == None):
+      self.y = SCREEN_HEIGHT / 2
+    if (self.dx == None):
+      self.dx = 0.0
+    if (self.dy == None):
+      self.dy = 0.0
+    return
+
+
+  def update(self):
+    self.radius = (float(self.life) / float(self.init_life)) * self.init_radius
+    self.x += self.dx
+    self.y += self.dy
+    if self.gravity:
+      self.dy = self.dy + GRAVITY_PARTICLE
+    self.dx += self.random_move * random.uniform(-0.5, 0.5)
+    self.dy += self.random_move * random.uniform(-0.5, 0.5)
+    self.life -= 1
+    if self.life < 0:
+      self.dead = True
+    return
+
+  def render(self, drawsurface = None):
+    pygame.draw.circle(self.screen, self.color, (int(self.x), int(self.y)), int(self.radius))
+    return
+
+  def flip(self):
+    self.life = -1
+    self.dead = True
+    return
\ No newline at end of file
diff --git a/lib/player.py b/lib/player.py
new file mode 100644
index 0000000..e7b333d
--- /dev/null
+++ b/lib/player.py
@@ -0,0 +1,145 @@
+"""Player module. The guy with the stylish clothes and lots of
+different animations. Moves and jumps, takes damage."""
+
+import pygame
+import os
+
+from pygame.locals import *
+
+from locals import *
+
+import data
+
+from object import DynamicObject
+from sound import play_sound
+from animation import Animation
+from log import log_message
+
+class Player(DynamicObject):
+
+  def __init__(self, screen, x = None, y = None):
+    DynamicObject.__init__(self, screen, x, y, PLAYER_LIFE, True, True)
+    #Changing some of the values from DynamicObject, the animations should probably actually be parsed from a file:
+    self.animations["default"] = Animation("guy", "standing")
+    self.animations["walking"] = Animation("guy", "walking")
+    self.animations["arrow"] = Animation("guy", "arrow")
+    self.animations["dying"] = Animation("guy", "dying")
+    self.animations["shouting"] = Animation("guy", "shouting")
+    self.animations["jumping"] = Animation("guy", "standing")
+    self.animations["exit"] = Animation("guy", "exit")
+    self.animations["gone"] = Animation("guy", "gone")
+    self.image = self.animations[self.current_animation].update_and_get_image()
+    self.rect = self.image.get_rect()
+    self.itemclass = "player"
+
+    #Variables spesific to this class:
+    self.inventory = []
+    self.umbrella_on = False
+    return
+
+  def move(self, direction):
+    if self.current_animation == "dying":
+      return
+    if not self.on_ground:
+      direction = (direction[0] * PLAYER_ACC_AIR_MULTIPLIER, direction[1])
+
+    if direction[0] > 0 and self.dx < PLAYER_MAX_SPEED:
+      self.acc(direction)
+      if self.dx > PLAYER_MAX_SPEED:
+        self.dx = PLAYER_MAX_SPEED
+    if direction[0] < 0 and self.dx > -PLAYER_MAX_SPEED:
+      self.acc(direction)
+      if self.dx < -PLAYER_MAX_SPEED:
+        self.dx = -PLAYER_MAX_SPEED
+    return
+
+  def update(self, level = None):
+
+    #Automatic animation selection:
+    if self.animations[self.current_animation].finished:
+      if self.current_animation == "dying":
+        pass
+      elif self.current_animation == "exit":
+        self.current_animation = "gone"
+      else:
+        #Special animation has finished, falling back to automatic selection
+        self.animations[self.current_animation].reset()
+        self.current_animation = "default"
+    if self.on_ground:
+      if self.current_animation == "jumping":
+        self.current_animation = "default"
+      if self.dx != 0 and self.current_animation == "default":
+        self.current_animation = "walking"
+      if (self.dx == 0) and self.current_animation == "walking" :
+        self.current_animation = "default"
+    elif self.current_animation == "default" or self.current_animation == "walking":
+      self.current_animation = "jumping"
+
+    collision_type = DynamicObject.update(self, level)
+
+    blood = []
+
+    if collision_type > 0:
+      blood = self.take_damage(collision_type)
+      if self.current_animation != "dying":
+        self.dy -= collision_type*PLAYER_JUMP_ACC / 4.5
+    return blood
+
+  def dec(self, direction):
+    if not self.on_ground:
+      direction = (direction[0] * PLAYER_ACC_AIR_MULTIPLIER, direction[1])
+    DynamicObject.dec(self, direction)
+    return
+
+  def render(self, surface = None, topleft = None, static_render = False):
+    self.rect.centerx = int(self.x)
+    self.rect.centery = int(self.y)
+    if self.rect.bottom > 0:
+      DynamicObject.render(self, surface, topleft, static_render)
+    else:
+      self.arrowimage = self.animations["arrow"].update_and_get_image()
+      self.arrowrect = self.arrowimage.get_rect()
+      self.arrowrect.centerx = int(self.x)
+      self.arrowrect.top = 5
+      self.screen.blit(self.arrowimage, self.arrowrect)
+    if self.umbrella_on:
+      self.umbrella_on = False # This should be set again before next render by the jump function
+    return
+
+  def jump(self):
+    if (self.on_ground):
+      self.dy = -PLAYER_JUMP_ACC
+      play_sound("boing", 0.5)
+    else:
+      self.dy -= PLAYER_AIR_JUMP
+      self.umbrella_on = True
+    return
+
+  def flip(self, flip_direction = CLOCKWISE):
+    #Position correction - Guy's collision shape isn't an exact square,
+    #so this is needed to avoid unwanted collisions after flipping
+    self.y += 2
+    xmod = 20 - self.x % TILE_DIM
+    if abs(xmod) < 6:
+      #Guy hasn't crossed over to another (empty) tile, but isn't centered
+      #- a chance of a post-flip collision
+      self.x += xmod
+
+    DynamicObject.flip(self, flip_direction)
+    if self.current_animation == "arrow":
+      self.current_animation = "default"
+    return
+
+  def take_damage(self, amount, x = None, y = None):
+    last_life = self.life
+    blood = DynamicObject.take_damage(self, amount, x, y)
+    if self.current_animation != "dying":
+      self.current_animation = "shouting"
+      play_sound("augh")
+    elif last_life > 0:
+      play_sound("augh")
+    return blood
+    
+  def exit(self):
+    self.current_animation = "exit"
+    return
\ No newline at end of file
diff --git a/lib/projectile.py b/lib/projectile.py
new file mode 100644
index 0000000..4a44eb0
--- /dev/null
+++ b/lib/projectile.py
@@ -0,0 +1,53 @@
+'''A straight-flying projectile capable of damaging the player.'''
+
+import pygame
+import os
+
+from pygame.locals import *
+
+from locals import *
+
+import data
+
+from object import DynamicObject
+from sound import play_sound
+from animation import Animation
+
+class Projectile(DynamicObject):
+
+  def __init__(self, screen, x, y, dx, dy, damage = 5, set = "energy"):
+    DynamicObject.__init__(self, screen, x, y, -1, False, False)
+    self.animations["default"] = Animation(set, "flying")
+    self.animations["dying"] = Animation(set, "dying")
+    self.image = self.animations[self.current_animation].update_and_get_image()
+    self.rect = self.image.get_rect()
+    self.dx = dx
+    self.dy = dy
+    self.saveddx =  None
+    self.damage = damage
+    self.itemclass = "projectile"
+    return
+
+  def update(self, level = None):
+    DynamicObject.update(self, level)
+
+    if self.dx == 0 and self.dy == 0 and self.saveddx != None: #Restores values saved on flipping
+      self.dx = self.saveddx
+      self.dy = self.saveddy
+      self.saveddx = None
+
+    if level.ground_check(self.x - 1, self.y - 1) or level.ground_check(self.x + 1, self.y + 1): #Simplified collision detection
+      self.die()
+      self.dx = 0
+      self.dy = 0
+    return
+
+  def flip(self, flip_direction = CLOCKWISE):
+    if flip_direction == CLOCKWISE:
+      self.saveddx = -self.dy
+      self.saveddy = self.dx
+    else:
+      self.saveddx = self.dy
+      self.saveddy = -self.dx
+    DynamicObject.flip(self, flip_direction)
+    return
\ No newline at end of file
diff --git a/lib/scripted_event.py b/lib/scripted_event.py
new file mode 100644
index 0000000..cbb65eb
--- /dev/null
+++ b/lib/scripted_event.py
@@ -0,0 +1,95 @@
+import pygame
+import os
+
+from pygame.locals import *
+
+from locals import *
+
+import data
+
+from util import dir_from_str
+
+from log import log_message
+
+from variables import Variables
+
+class Scripted_event_element:
+  def __init__(self, event_type, text = "", orientation = RIGHT, animation = ""):
+    self.event_type = event_type
+    self.finished = False
+    self.text = text
+    self.orientation = orientation
+    self.animation = animation
+    return
+
+  def to_str(self):
+    if self.event_type == "dialogue":
+      return self.event_type + " " + self.text
+    if self.event_type == "player" and self.text == "orientation":
+      string = self.event_type + " " + self.text + " "
+      if self.orientation == RIGHT:
+        string += "RIGHT"
+      elif self.orientation == LEFT:
+        string += "LEFT"
+      elif self.orientation == DOWN:
+        string += "DOWN"
+      elif self.orientation == UP:
+        string += "UP"
+      return string
+    if self.event_type == "player" and self.text == "animation":
+      return self.event_type + " " + self.text + " " + self.animation
+    else:
+      return self.event_type
+
+class Scripted_event:
+  def __init__(self, trigger_type, times = 1):
+    self.trigger_type = trigger_type
+    self.elements = []
+    self.counter = -1
+    self.last_dir = RIGHT
+    self.repeated = 0
+    self.times = times
+    return
+
+  def add_element(self, text):
+    values  = text.split(" ", 1)
+    etype = values[0]
+    etype = etype.strip()
+    if etype == "dialogue":
+      element = Scripted_event_element(etype, values[1].strip())
+    else:
+      if etype == "player":
+        values = values[1].split()
+        if values[0] == "orientation":
+          self.last_dir = dir_from_str(values[1])
+          element = Scripted_event_element(etype, values[0], self.last_dir)
+        if values[0] == "animation":
+          element = Scripted_event_element(etype, values[0], self.last_dir, values[1])
+      else:
+        element = Scripted_event_element(etype)
+    self.elements.append(element)
+    return
+
+  def next_element(self):
+    if self.repeated == self.times:
+      #The event has repeated enough times
+      return Scripted_event_element("end")
+
+    #Returning one element
+    self.counter += 1
+
+    if self.counter < len(self.elements):
+      return self.elements[self.counter]
+
+    else:
+      #Event finished
+      self.repeated += 1
+      self.counter = -1
+      return Scripted_event_element("end")
+
+  def to_str(self):
+    string = "\ntrigger " + self.trigger_type + " " + str(self.times) + "\n"
+    for el in self.elements:
+      string += el.to_str() + "\n"
+    string += "end trigger"
+    return string
\ No newline at end of file
diff --git a/lib/setup.py b/lib/setup.py
new file mode 100644
index 0000000..77f665e
--- /dev/null
+++ b/lib/setup.py
@@ -0,0 +1,4 @@
+from distutils.core import setup
+import py2exe
+
+setup(console=['main.py'],zipfile=None, windows = [{ 'script': "main.py", 'icon_resources': [(0x0004,'whichway.ico')]}])
diff --git a/lib/sound.py b/lib/sound.py
new file mode 100644
index 0000000..d6923b8
--- /dev/null
+++ b/lib/sound.py
@@ -0,0 +1,42 @@
+'''A very simple sound module. Handles opening .ogg sound files,
+playing sounds at different volumes and caching.
+
+Use the play_sound function. It fetches and caches .ogg sound files
+from the "sounds" subdirectory of the data directory and plays them.
+Prints error messages if the sound file is not found or the sound
+can't be played. Doesn't throw exceptions.
+
+Usage of the module requires the Variables class from the variables module
+and the data module.'''
+
+import pygame
+from pygame.locals import *
+import os
+
+import data
+
+from variables import Variables
+
+from log import error_message
+
+sounds = {}
+
+def play_sound(sound_id, volume = 1.0):
+  if not Variables.vdict["sound"]:
+    return
+  snd = None
+  if (not sounds.has_key(sound_id)):
+    try:
+      sound_path = data.filepath(os.path.join("sounds", sound_id + ".ogg"))
+      snd = sounds[sound_id] = pygame.mixer.Sound(sound_path)
+    except:
+      error_message("No sound device available or sound file not found: " + sound_id + ".ogg")
+      return
+  else:
+    snd = sounds[sound_id]
+  try:
+    snd.set_volume(volume)
+    snd.play()
+  except:
+    error_message("Could not play sound")
+  return
diff --git a/lib/spider.py b/lib/spider.py
new file mode 100644
index 0000000..1da1030
--- /dev/null
+++ b/lib/spider.py
@@ -0,0 +1,149 @@
+'''One of the enemies, the spider, which climbs along walls and shoots at the player.'''
+
+import pygame
+
+from pygame.locals import *
+
+from locals import *
+
+import data
+
+from object import DynamicObject
+from sound import play_sound
+from animation import Animation
+from projectile import Projectile
+
+from util import cycle_clockwise, cycle_counter_clockwise, get_direction, str_from_dir
+from sound import play_sound
+from log import log_message
+
+class Spider(DynamicObject):
+
+  def __init__(self, screen, x = None, y = None, attached = RIGHT):
+    DynamicObject.__init__(self, screen, x, y, 10, False, False)
+    self.animations["default"] = Animation("spider", "standing")
+    self.animations["walking"] = Animation("spider", "walking")
+    self.image = self.animations[self.current_animation].update_and_get_image()
+    self.rect = self.image.get_rect()
+    self.itemclass = "spider"
+
+    self.attached = attached
+    self.move_target = STAY
+    self.fire_delay = 0
+
+    return
+
+  def get_orientation(self):
+    return self.attached
+
+  def update(self, level = None):
+    DynamicObject.update(self, level)
+
+    if self.x < 0 or self.y < 0 or self.flipping:
+      return
+
+    if self.attached == RIGHT or self.attached == LEFT:
+      self.top_collide_point = (self.rect.centerx, self.rect.top + SPIDER_TOO_WIDE - 2)
+      self.bottom_collide_point = (self.rect.centerx, self.rect.bottom - SPIDER_TOO_WIDE + 2)
+      if self.attached == RIGHT:
+        self.top_leg_attach_point = (self.rect.right + 2, self.rect.top + SPIDER_TOO_WIDE)
+        self.bottom_leg_attach_point = (self.rect.right + 2, self.rect.bottom - SPIDER_TOO_WIDE)
+      else:
+        self.top_leg_attach_point = (self.rect.left - 2, self.rect.top + SPIDER_TOO_WIDE)
+        self.bottom_leg_attach_point = (self.rect.left - 2, self.rect.bottom - SPIDER_TOO_WIDE)
+    else:
+      self.top_collide_point = (self.rect.left + SPIDER_TOO_WIDE - 2, self.rect.centery)
+      self.bottom_collide_point = (self.rect.right - SPIDER_TOO_WIDE + 2, self.rect.centery)
+      if self.attached == DOWN:
+        self.top_leg_attach_point = (self.rect.left + SPIDER_TOO_WIDE, self.rect.bottom + 2)
+        self.bottom_leg_attach_point = (self.rect.right - SPIDER_TOO_WIDE, self.rect.bottom + 2)
+      else:
+        self.top_leg_attach_point = (self.rect.left + SPIDER_TOO_WIDE, self.rect.top - 2)
+        self.bottom_leg_attach_point = (self.rect.right - SPIDER_TOO_WIDE, self.rect.top - 2)
+
+    fire = True
+
+    self.move_target = STAY
+    if self.attached == RIGHT or self.attached == LEFT:
+      if level.player.rect.top > (self.y - 2):
+        fire = False
+        if not level.ground_check(self.bottom_collide_point[0], self.bottom_collide_point[1]):
+          self.move_target = DOWN
+      if level.player.rect.bottom < (self.y + 2):
+        fire = False
+        if not level.ground_check(self.top_collide_point[0], self.top_collide_point[1]):
+          self.move_target = UP
+    else:
+      if level.player.rect.left > (self.x - 2):
+        fire = False
+        if not level.ground_check(self.bottom_collide_point[0], self.bottom_collide_point[1]):
+          self.move_target = RIGHT
+      if level.player.rect.right < (self.x + 2):
+        fire = False
+        if not level.ground_check(self.top_collide_point[0], self.top_collide_point[1]):
+          self.move_target = LEFT
+
+    if not self.gravity:
+      if self.fire_delay > 0:
+        self.fire_delay -= 1
+      self.dy = 0
+      self.dx = 0
+      if self.move_target == UP:
+        if (level.ground_check(self.top_leg_attach_point[0], self.top_leg_attach_point[1] - 1)):
+          self.dy = -1
+      elif self.move_target == DOWN:
+        if (level.ground_check(self.bottom_leg_attach_point[0], self.bottom_leg_attach_point[1] + 1)):
+          self.dy = 1
+      elif self.move_target == LEFT:
+        if (level.ground_check(self.top_leg_attach_point[0] - 1, self.top_leg_attach_point[1])):
+          self.dx = -1
+      elif self.move_target == RIGHT:
+        if (level.ground_check(self.bottom_leg_attach_point[0] + 1, self.bottom_leg_attach_point[1])):
+          self.dx = 1
+      elif fire and not level.player.dead:
+        if (self.attached == RIGHT) and (level.player.x > self.x + 20):
+          fire = False
+        if (self.attached == LEFT) and (level.player.x < self.x - 20):
+          fire = False
+        if (self.attached == UP) and (level.player.y < self.y - 20):
+          fire = False
+        if (self.attached == DOWN) and (level.player.y > self.y + 20):
+          fire = False
+        if fire:
+          self.fire(level)
+
+    if self.animations[self.current_animation].finished and self.current_animation != "dying":
+      self.animations[self.current_animation].reset()
+      self.current_animation = "default"
+    if self.dx != 0 or self.dy != 0 and self.current_animation == "default":
+      self.current_animation = "walking"
+    if self.dx == 0 and self.dy == 0 and self.current_animation == "walking":
+      self.current_animation = "default"
+
+    return
+
+  def flip(self, flip_direction = CLOCKWISE):
+    if flip_direction == CLOCKWISE:
+      self.attached = cycle_clockwise(self.attached)
+    else:
+      self.attached = cycle_counter_clockwise(self.attached)
+    DynamicObject.flip(self, flip_direction)
+    return
+
+  def fire(self, level):
+    if self.fire_delay == 0:
+      play_sound("fire")
+      self.fire_delay = SPIDER_FIRE_DELAY
+      fire_direction = get_direction(self.attached)
+      level.objects.append(Projectile(self.screen, self.x, self.y, fire_direction[0]*-SPIDER_PROJECTILE_SPEED, fire_direction[1]*-SPIDER_PROJECTILE_SPEED, SPIDER_DAMAGE, "energy"))
+    return
+    
+  def to_str(self, verbose = True):
+    string = DynamicObject.to_str(self, False)
+    try:
+      string += " " + str_from_dir(self.attached)
+    except:
+      pass
+    if verbose:
+      log_message("Obj converted to string: " + string)
+    return string
\ No newline at end of file
diff --git a/lib/spikes.py b/lib/spikes.py
new file mode 100644
index 0000000..96a83a7
--- /dev/null
+++ b/lib/spikes.py
@@ -0,0 +1,18 @@
+import pygame
+import os
+
+from pygame.locals import *
+
+from locals import *
+
+import data
+
+from tile import Tile
+from animation import Animation
+
+class Spikes(Tile):
+  def __init__(self, screen, tilex, tiley, set = "brown"):
+    Tile.__init__(self, screen, tilex, tiley, set, "spikes")
+    self.itemclass = "spikes"
+    self.realign()
+    return
diff --git a/lib/tile.py b/lib/tile.py
new file mode 100644
index 0000000..65bd238
--- /dev/null
+++ b/lib/tile.py
@@ -0,0 +1,64 @@
+import pygame
+import os
+
+from pygame.locals import *
+
+from locals import *
+
+import data
+
+from visibleobject import VisibleObject
+from animation import Animation
+
+from log import log_message
+
+class Tile(VisibleObject):
+  def __init__(self, screen, tilex, tiley, set = "brown", tileclass = "wall"):
+    x = (tilex - (FULL_TILES_HOR - TILES_HOR) + 0.5) * TILE_DIM
+    y = (tiley - (FULL_TILES_VER - TILES_VER) + 0.5) * TILE_DIM
+    VisibleObject.__init__(self, screen, x, y)
+    self.animations["default"] = Animation(set, tileclass)
+    self.image = self.animations[self.current_animation].update_and_get_image()
+    self.rect = self.image.get_rect()
+    self.tilex = tilex
+    self.tiley = tiley
+    self.tileclass = tileclass
+    self.aligned = True
+    return
+
+  def update(self, level = None):
+    VisibleObject.update(self)
+    if not self.flipping:
+      self.realign()
+    return
+
+  def flip(self, flip_direction = CLOCKWISE):
+    VisibleObject.flip(self, flip_direction)
+    if flip_direction == CLOCKWISE:
+      tempx = self.tilex
+      self.tilex = FULL_TILES_VER - self.tiley - 1
+      self.tiley = tempx
+    else:
+      tempy = self.tiley
+      self.tiley = FULL_TILES_HOR - self.tilex - 1
+      self.tilex = tempy
+
+
+  def realign(self):
+    self.rect.centerx = self.x
+    self.rect.centery = self.y
+    self.x = round((float(self.rect.right)/float(TILE_DIM)), 0)*TILE_DIM - self.rect.width / 2
+    self.y = round((float(self.rect.bottom)/float(TILE_DIM)), 0)*TILE_DIM - self.rect.height / 2
+    if self.rect.height % 2 == 1:
+       self.y -= 1
+    if self.rect.width % 2 == 1:
+       self.x -= 1
+    self.rect.centerx = self.x
+    self.rect.centery = self.y
+    return
+
+  def is_aligned(self):
+    aligned = self.rect.right % TILE_DIM == 0 and self.rect.bottom % TILE_DIM == 0
+    if not aligned:
+      log_message("tilepos " + str(self.rect.right) + " " + str(self.rect.bottom))
+    return aligned
\ No newline at end of file
diff --git a/lib/trigger.py b/lib/trigger.py
new file mode 100644
index 0000000..1f8e49d
--- /dev/null
+++ b/lib/trigger.py
@@ -0,0 +1,12 @@
+'''A class for storing triggers affecting gameplay.
+Flip triggers and also level editing triggers in the future.'''
+
+class Trigger:
+
+    def __init__(self, trigger_type, x, y, tilex = None, tiley = None):
+        self.trigger_type = trigger_type
+        self.x = x
+        self.y = y
+        self.tilex = tilex
+        self.tiley = tiley
+        return
diff --git a/lib/util.py b/lib/util.py
new file mode 100644
index 0000000..6989474
--- /dev/null
+++ b/lib/util.py
@@ -0,0 +1,305 @@
+''' This module contains generic utility and helper functions.
+The comments give a basic idea of each function's purpose.
+This module is mostly included in Skellington Plus.'''
+
+import pygame
+from pygame.locals import *
+import os
+import codecs
+import datetime
+
+from locals import *
+
+import data
+
+from sound import play_sound
+
+from variables import Variables
+
+from log import error_message, log_message
+
+class Score:
+  def __init__(self, score, life = PLAYER_LIFE, time = 0, levels = 0):
+    self.score = score
+    self.life = life
+    self.time = time
+    self.levels = levels
+    return
+
+class Util:
+
+  pygame.font.init()
+  smallfont = pygame.font.Font(data.filepath(os.path.join("misc", "Vera.ttf")), FONT_SIZE)
+  cached_text_images = {}
+  cached_images = {}
+  cached_images["key_z"] = pygame.image.load(data.picpath("key", "z"))
+  cached_images["key_p"] = pygame.image.load(data.picpath("key", "p"))
+  cached_images["health_bar_fill"] = pygame.image.load(data.picpath("health_bar", "fill"))
+  cached_images["health_bar_empty"] = pygame.image.load(data.picpath("health_bar", "empty"))
+  fade_state = FADE_STATE_BLACK
+  blackscreen = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
+
+'''This function returns a path for saving config in the user's home directory.
+It's compatible with both UNIX-likes (environment variable HOME)
+and Windows (environment variable APPDATA).
+Prints error messages if unsuccesful, and reverts to a "saves" directory
+under the data directory instead.'''
+def get_config_path():
+  path_name = ""
+  try:
+    path_name = os.path.join(os.environ["HOME"], "." + GAME_NAME_SHORT)
+  except:
+    error_message("Couldn't find environment variable HOME, reverting to APPDATA. This is normal under Windows.")
+    try:
+      path_name = os.path.join(os.environ["APPDATA"], GAME_NAME_SHORT)
+    except:
+      error_message("Couldn't get environment variable for home directory, using data directory instead.")
+      path_name = data.filepath("saves")
+  if not os.path.exists(path_name):
+    os.mkdir(path_name)
+  return path_name
+
+'''This parses a config file stored in the location given by get_config_path().
+The parsed values are stored in the Variables class of the variables module.'''
+def parse_config():
+  for world in WORLDS:
+    Variables.vdict["unlocked" + world] = 0
+    Variables.vdict["hiscore" + world] = 0
+    Variables.vdict["besttime" + world] = 0
+  Variables.vdict["sound"] = True
+  Variables.vdict["dialogue"] = True
+  Variables.vdict["verbose"] = False
+  Variables.vdict["fullscreen"] = False
+  file_path = os.path.join(get_config_path(), "config.txt")
+  try:
+    conffile = codecs.open(file_path, "r", "utf_8")
+    for line in conffile:
+      if line.strip() != "":
+        values = line.split("\t")
+
+        if values[0] == "unlocked":
+          try:
+            Variables.vdict["unlocked" + values[1]] = int(values[2])
+          except:
+            Variables.vdict["unlocked" + WORLDS[0]] = int(values[1])  #Old style config file compatibility
+
+        elif values[0] == "hiscore":
+          try:
+            Variables.vdict["hiscore" + values[1]] = int(values[2])
+          except:
+            Variables.vdict["hiscore" + WORLDS[0]] = int(values[1])  #Old style config file compatibility
+
+        elif values[0] == "besttime":
+          Variables.vdict["besttime" + values[1]] = int(values[2])
+
+        elif values[0] == "sound":
+          Variables.vdict["sound"] = str_to_bool(values[1])
+
+        elif values[0] == "dialogue":
+          Variables.vdict["dialogue"] = str_to_bool(values[1])
+
+        elif values[0] == "fullscreen":
+          Variables.vdict["fullscreen"] = str_to_bool(values[1])
+
+  except:
+    if write_config():
+      log_message("Created configuration file to " + file_path)
+  return
+
+'''Writes the config stored in the Variables class to the configuration file.
+Prints an error message if unsuccesful.'''
+def write_config():
+  file_path = os.path.join(get_config_path(), "config.txt")
+  try:
+    conffile = codecs.open(file_path, "w", "utf_8")
+    for world in WORLDS:
+      print >> conffile, "unlocked\t%(world)s\t%(unlocked)s" % {"world": world, "unlocked": Variables.vdict["unlocked" + world]}
+      print >> conffile, "hiscore\t%(world)s\t%(hiscore)s" % {"world": world, "hiscore": Variables.vdict["hiscore" + world]}
+      print >> conffile, "besttime\t%(world)s\t%(besttime)s" % {"world": world, "besttime": Variables.vdict["besttime" + world]}
+    print >> conffile, "sound\t%s" % bool_to_str(Variables.vdict["sound"])
+    print >> conffile, "dialogue\t%s" % bool_to_str(Variables.vdict["dialogue"])
+    print >> conffile, "fullscreen\t%s" % bool_to_str(Variables.vdict["fullscreen"])
+  except:
+    error_message("Could not write configuration file to " + file_path)
+    return False
+  return True
+  
+#Writes the message log to disk.
+def write_log():
+  file_path = os.path.join(get_config_path(), "log.txt")
+  old_log = ""
+  if os.path.exists(file_path):
+    conffile = open(file_path)
+    count = 0
+    for line in conffile:
+      old_log = old_log + line
+      count += 1
+      if count > MAX_OLD_LOG_LINES:
+        break
+  if Variables.vdict.has_key("log"):
+    try:
+      conffile = codecs.open(file_path, "w", "utf_8")
+      print >> conffile, "Log updated " + str(datetime.date.today())
+      print >> conffile, Variables.vdict["log"]
+      print >> conffile, ""
+      print >> conffile, old_log
+    except:
+      error_message("Could not write log file to " + file_path)
+      return False
+  return True
+
+def str_to_bool(string):
+  string = string.strip()
+  return (string == "true" or string == "True" or string == "1" or string == "on")
+
+def bool_to_str(bool):
+  if bool:
+    return "on"
+  else:
+    return "off"
+
+''' This function renders fancy-looking text and handles caching of text images
+Returns a pygame surface containing the rendered text
+The text is rendered with slight edges to make it look more readable on a
+colorful background.
+
+The constant colors can be found from locals.py.
+'''
+def render_text(string, color = COLOR_GUI, bgcolor = COLOR_GUI_BG):
+  if Util.cached_text_images.has_key(string + str(color) + str(bgcolor)):
+    final_image = Util.cached_text_images[string + str(color) + str(bgcolor)]
+  else:
+    text_image_bg = Util.smallfont.render(string, True, bgcolor)
+    text_image_fg = Util.smallfont.render(string, True, color)
+    rect = text_image_bg.get_rect()
+    final_image = pygame.Surface((rect.width + 2, rect.height + 2)).convert_alpha()
+
+    final_image.fill((0,0,0,0))
+
+    final_image.blit(text_image_bg, rect)
+    final_image.blit(text_image_bg, (1,0))
+    final_image.blit(text_image_bg, (2,0))
+
+    final_image.blit(text_image_bg, (0,2))
+    final_image.blit(text_image_bg, (1,2))
+    final_image.blit(text_image_bg, (2,2))
+
+    final_image.blit(text_image_fg, (1,1))
+    Util.cached_text_images[string + str(color) + str(bgcolor)] = final_image
+  return final_image
+
+'''This function renders text partially.
+For fancy dialogue display.
+The phase value is the amount of characters shown.
+-1 phase means that the whole string is visible.
+'''
+def render_text_dialogue(screen, string, phase, key = "z"):
+  if phase == -1:
+    phase = len(string)
+
+  rendered_string = string[0:phase]
+  string_image = render_text(rendered_string)
+  string_rect = string_image.get_rect()
+  string_rect.centerx = SCREEN_WIDTH / 2
+  string_rect.centery = SCREEN_HEIGHT / 2
+
+  if key == "p":
+    skip_image = Util.cached_images["key_p"]
+  else:
+    skip_image = Util.cached_images["key_z"]
+
+  skip_rect = skip_image.get_rect()
+  skip_rect.centerx = SCREEN_WIDTH / 2
+  skip_rect.top = string_rect.bottom + 5
+
+  bg_rect = pygame.Rect(string_rect.left - 10, string_rect.top - 5, string_rect.width + 20, string_rect.height + skip_rect.height + 15)
+  bg_image = pygame.Surface((bg_rect.width, bg_rect.height))  
+  bg_image.set_alpha(FADE_STATE_HALF)
+
+  screen.blit(bg_image, bg_rect)
+
+  screen.blit(string_image, string_rect)
+
+  screen.blit(skip_image, skip_rect)
+
+  if phase < len(string):
+    phase += 1
+    play_sound("click")
+  else:
+    return -1
+
+  return phase
+
+def cycle_clockwise(orientation):
+  orientation += 1
+  if orientation > 3:
+    orientation = 0
+  return orientation
+
+def cycle_counter_clockwise(orientation):
+  orientation -= 1
+  if orientation < 0:
+    orientation = 3
+  return orientation
+
+def get_direction(orientation):
+  if orientation == RIGHT:
+    return (1, 0)
+  if orientation == LEFT:
+    return (-1, 0)
+  if orientation == UP:
+    return (0, -1)
+  if orientation == DOWN:
+    return (0, 1)
+  return (0, 0)
+
+def dir_from_str(string):
+  if string == "LEFT":
+    return LEFT
+  if string == "UP":
+    return UP
+  if string == "DOWN":
+    return DOWN
+  return RIGHT
+  
+def str_from_dir(direction):
+  if direction == LEFT:
+    return "LEFT"
+  if direction == UP:
+    return "UP"    
+  if direction == DOWN:
+    return "DOWN"
+  return "RIGHT"
+
+'''This function fades the screen to black.
+Both fade-in and fade-out.
+Returns true if the fading has finished.
+fade_target should be an integer (0-255)
+* 255 = FADE_STATE_BLACK : The display is all black
+* 0 = FADE_STATE_NONE : The display is not faded at all'''
+def fade_to_black(screen, fade_target):
+  if Util.fade_state > fade_target:
+    Util.fade_state += int(255 / (FPS * FADE_IN))
+    if Util.fade_state < fade_target:
+      Util.fade_state = fade_target
+  if Util.fade_state < fade_target:
+    Util.fade_state -= -int(255 / (FPS * FADE_OUT))
+    if Util.fade_state > fade_target:
+      Util.fade_state = fade_target
+  if Util.fade_state > FADE_STATE_NONE:
+    Util.blackscreen.set_alpha(Util.fade_state)
+    screen.blit(Util.blackscreen, screen.get_rect())
+  return (Util.fade_state == fade_target)
+
+'''Applies the fullscreen setting stored in variables to the screen.
+screen is a pygame standard surface.
+Contents of the screen should not be affected.'''
+def apply_fullscreen_setting(screen):
+  mode = 0
+  if Variables.vdict["fullscreen"]:
+    mode ^= FULLSCREEN
+  tmp = pygame.Surface((SCREEN_WIDTH,SCREEN_HEIGHT)).convert()
+  tmp.blit(screen,(0,0))
+  screen = pygame.display.set_mode((SCREEN_WIDTH,SCREEN_HEIGHT),mode)
+  screen.blit(tmp,(0,0))
+  pygame.display.flip()
\ No newline at end of file
diff --git a/lib/variables.py b/lib/variables.py
new file mode 100644
index 0000000..52a7d0d
--- /dev/null
+++ b/lib/variables.py
@@ -0,0 +1,5 @@
+'''A very, very simple module for storing config.
+Separated from the util module to avoid import loops.'''
+
+class Variables:
+  vdict = {}
\ No newline at end of file
diff --git a/lib/visibleobject.py b/lib/visibleobject.py
new file mode 100644
index 0000000..9570431
--- /dev/null
+++ b/lib/visibleobject.py
@@ -0,0 +1,153 @@
+'''This class governs all visible, animateable game objects from ground tiles to the player.
+The functionality is extended with the DynamicObject class.'''
+import pygame
+import os
+import random
+
+from math import *
+
+from pygame.locals import *
+
+from locals import *
+
+import data
+
+from animation import Animation
+from log import log_message
+
+class VisibleObject:
+
+  def __init__(self, screen, x = None, y = None):
+    self.screen = screen
+    self.animations = {}
+    self.animations["default"] = Animation("object", "idle")
+    self.current_animation = "default"
+    self.image = self.animations[self.current_animation].update_and_get_image()
+    self.rect = self.image.get_rect()
+    self.x = x
+    self.y = y
+    if (self.x == None):
+      self.x = SCREEN_WIDTH / 2
+    if (self.y == None):
+      self.y = SCREEN_HEIGHT / 2
+
+    self.flipping = False
+    self.flipcounter = 0
+    self.flip_init_angle = 0
+    self.flip_finished = False
+    self.flip_direction = CLOCKWISE #The object will move clockwise
+
+    self.orientation = RIGHT
+    self.itemclass = "not_item"
+
+    self.pickable = False
+
+    self.dead = False
+
+    return
+
+  def update(self, level = None):
+    self.flip_finished = False
+
+    if self.animations[self.current_animation].finished and self.current_animation == "dying":
+      self.dead = True
+
+    if self.flipping:
+
+      if self.flipcounter == 0:
+        rela_x = self.x - PLAY_AREA_CENTER_X
+        rela_y = self.y - PLAY_AREA_CENTER_Y
+        self.rad = sqrt(rela_x**2 + rela_y**2)
+        self.flip_init_angle = atan2(rela_y, rela_x)
+
+      self.flipcounter += 1
+      self.flip_angle = self.flipcounter * (pi * 0.5 / (FLIP_FRAMES + 1)) * self.flip_direction
+      self.angle = self.flip_angle + self.flip_init_angle
+      self.x = PLAY_AREA_CENTER_X + cos(self.angle) * self.rad
+      self.y = PLAY_AREA_CENTER_Y + sin(self.angle) * self.rad
+
+      if self.flipcounter > FLIP_FRAMES:
+        self.flipcounter = 0
+        self.flipping = False
+        self.dx = 0
+        self.dy = 0
+        self.flip_finished = True
+    return
+
+  def render(self, surface = None, center = None, static_render = False, alpha = 255):
+    """Render the object - also flips or rotates it visually according to the orientation."""
+    if (not static_render) or (self.image == None):
+      self.image = self.animations[self.current_animation].update_and_get_image()
+    if center == None:
+      self.rect.centerx = int(self.x)
+      self.rect.centery = int(self.y)
+    else:
+      self.rect.centerx = center[0]
+      self.rect.centery = center[1]
+
+    self.orientation = self.get_orientation()
+
+    drawsurface = self.screen
+    if surface != None:
+      drawsurface = surface
+
+    image = self.image
+
+    if self.orientation == LEFT:
+      image = pygame.transform.flip(image, True, False)
+    elif self.orientation == UP:
+      image = pygame.transform.rotate(image, 90)
+    elif self.orientation == DOWN:
+      image = pygame.transform.rotate(image, -90)
+
+    image.set_alpha(alpha)
+
+    drawsurface.blit(image, self.rect)
+
+    if center != None:
+      self.rect.centerx = int(self.x)
+      self.rect.centery = int(self.y)
+    return
+
+  def get_orientation(self):
+    return RIGHT
+
+  def flip(self, flip_direction = CLOCKWISE):
+    """Make the object flip with the level to either direction"""
+    if not self.flipping:
+      self.flipping = True
+      self.flip_direction = flip_direction
+      return True
+    return False
+
+  def die(self):
+    """Make the object die - if the object has a death animation, it will be played first."""
+    if self.animations.has_key("dying"):
+      self.current_animation = "dying"
+    else:
+      self.dead = True
+    return
+    
+  def to_str(self, verbose = True):
+    string = self.itemclass
+    tx, ty = screen_coords_to_tile_coords(self.x, self.y)
+    string += " " + str(tx) + " " + str(ty)
+    if verbose:
+      log_message("Obj converted to string: " + string)
+    return string
+
+def flip_direction_from_position(flip_trigger_position):
+  flip_direction = CLOCKWISE
+  if flip_trigger_position[1] > flip_trigger_position[0] and flip_trigger_position[1] > 240:
+    flip_direction = COUNTER_CLOCKWISE
+  return flip_direction
+
+def tile_coords_to_screen_coords(x, y):
+  x = (float(x) - (FULL_TILES_HOR - TILES_HOR)) * TILE_DIM
+  y = (float(y) - (FULL_TILES_VER - TILES_VER))* TILE_DIM
+  return (x, y)
+
+def screen_coords_to_tile_coords(x, y):
+  x = float(x)/float(TILE_DIM) + float(FULL_TILES_HOR - TILES_HOR)
+  y = float(y)/float(TILE_DIM) + float(FULL_TILES_VER - TILES_VER)
+  return (x, y)
\ No newline at end of file
diff --git a/lib/whichway.ico b/lib/whichway.ico
new file mode 100644
index 0000000..841956e
Binary files /dev/null and b/lib/whichway.ico differ
diff --git a/lib/world.py b/lib/world.py
new file mode 100644
index 0000000..9450a6e
--- /dev/null
+++ b/lib/world.py
@@ -0,0 +1,50 @@
+from variables import Variables
+
+from locals import *
+
+import data
+
+class World:
+
+  def __init__(self, world_name = WORLDS[0]):
+    self.index = 0
+    self.levels = []
+    self.name = world_name
+    self.number = 1
+    count = 0
+    for w in WORLDS:
+      count += 1
+      if self.name == w:
+        self.number = count
+
+    #Parsing config:
+    conffile = open(data.levelpath(world_name))
+    for line in conffile:
+      if line.strip() != "":
+        values = line.split()
+        if values[0] == "level":
+          self.levels.append(values[1])
+
+    self.level_count = len(self.levels)
+    return
+
+  def is_next_level(self):
+    if self.index < len(self.levels):
+      return True
+    else:
+      return False
+
+  def get_level(self, index = None):
+    level = ""
+
+    if index != None:
+      self.index = index
+    level = self.levels[self.index]
+
+    #Unlocking the next level of this world
+    if Variables.vdict["unlocked" + self.name] < self.index:
+      Variables.vdict["unlocked" + self.name] = self.index
+
+    self.index += 1
+
+    return level
\ No newline at end of file
diff --git a/run_game.py b/run_game.py
new file mode 100644
index 0000000..7b2077a
--- /dev/null
+++ b/run_game.py
@@ -0,0 +1,13 @@
+#! /usr/bin/env python
+
+import sys
+import os
+try:
+    libdir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'lib'))
+    sys.path.insert(0, libdir)
+except:
+    # probably running inside py2exe which doesn't set __file__
+    pass
+
+import main
+main.main()

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-games/whichwayisup.git



More information about the Pkg-games-commits mailing list