changeset 511:93ddcac0b772

Merged in engine refactorings.
author Jeremy Thurgood <firxen@gmail.com>
date Sat, 04 Sep 2010 09:53:00 +0200
parents d274cc414178 (diff) c1f4f9149349 (current diff)
children b10dae40dc32
files gamelib/state.py
diffstat 10 files changed, 278 insertions(+), 79 deletions(-) [+]
line wrap: on
line diff
Binary file Resources/images/cryo/comp_info_detail.png has changed
Binary file Resources/images/cryo/comp_info_detail_fixed.png has changed
Binary file Resources/images/items/superconductor_broken.png has changed
Binary file Resources/images/items/superconductor_broken_cursor.png has changed
--- a/TODO	Sun Aug 29 20:38:03 2010 +0200
+++ b/TODO	Sat Sep 04 09:53:00 2010 +0200
@@ -9,11 +9,16 @@
   * Fix thing.enter being called multiple times without leaving
 
 Hints:
-  * More help with the starting puzzle
-  * Interacts with broken pipes & cryo/pipe / tins
+  * More help with the starting puzzle (another interactable cryo-chamber?)
+  * Interacts with broken pipes & cryo/pipe / tins / < 3 fixed pipe
   * Interacts with laser cutter & tins
   * Interacts with cameras
   * Interacts with cryo-resevoirs & empty bottle
+  * Interacts with cryo-puddles & cans.
+  * Interacts with fish bowl & engine room
+  * Make JIM messages more clearly from JIM.
+  * Make it clearer who JIM is (That he is separate from the ship computers)
+  * JIM Speaks after fixing engines & repairing environment?
 
 Game:
   * Keep track of total sentence.
--- a/gamelib/scenes/engine.py	Sun Aug 29 20:38:03 2010 +0200
+++ b/gamelib/scenes/engine.py	Sat Sep 04 09:53:00 2010 +0200
@@ -24,7 +24,6 @@
         super(Engine, self).__init__(state)
         self.add_item(CanOpener('canopener'))
         self.add_thing(CanOpenerThing())
-        self.add_item(BrokenSuperconductor('superconductor_broken'))
         self.add_thing(SuperconductorSocket())
         self.add_thing(PowerLines())
         self.add_thing(CryoContainers())
@@ -45,12 +44,6 @@
                 (549, 479, 60, 55),
             )
         ))
-        self.add_thing(GenericDescThing('engine.controlpanel', 2,
-            "A control panel. It seems dead.",
-            (
-                (513, 330, 58, 50),
-            )
-        ))
         self.add_thing(GenericDescThing('engine.superconductors', 4,
             "Superconductors. The engines must be power hogs.",
             (
@@ -60,7 +53,8 @@
             )
         ))
         self.add_thing(GenericDescThing('engine.floor_hole', 5,
-            "A gaping hole in the floor of the room. You're guessing that's why there's a vacuum in here.",
+            "A gaping hole in the floor of the room. "
+            "It is clearly irreparable.",
             (
                 (257, 493, 141, 55),
                 (301, 450, 95, 45),
@@ -119,7 +113,8 @@
             )
         ))
         self.add_thing(GenericDescThing('engine.exit_sign', 13,
-            "It's one of those glow-in-the-dark exit signs that you see everywhere.",
+            "It's one of those glow-in-the-dark signs needed to satisfy the "
+            "health and safety inspectors.",
             (
                 (681, 322, 80, 33),
             )
@@ -182,11 +177,6 @@
                       "the vacuum has kept it in perfect condition.")
 
 
-class BrokenSuperconductor(Item):
-    INVENTORY_IMAGE = 'superconductor_broken.png'
-    CURSOR = CursorSprite('superconductor_broken_cursor.png')
-
-
 class SuperconductorSocket(Thing):
     NAME = 'engine.superconductor'
 
@@ -221,8 +211,8 @@
         if self.get_data('present') and not self.get_data('working'):
             self.set_interact('removed')
             self.set_data('present', False)
-            self.state.add_inventory_item('superconductor_broken')
-            return Result("With leverage, the burned-out superconductor snaps out.")
+            return Result("With leverage, the burned-out superconductor snaps out. "
+                          "You discard it.")
 
     def interact_with_superconductor(self, item):
         if self.get_data('present'):
@@ -503,7 +493,10 @@
     NAME = "engine.computer_console"
 
     INTERACTS = {
-        'console': InteractNoImage(293, 287, 39, 36),
+        'console': InteractRectUnion((
+            (293, 287, 39, 36),
+            (513, 330, 58, 50),
+        )),
     }
 
     INITIAL = 'console'
--- a/gamelib/scenes/scene_widgets.py	Sun Aug 29 20:38:03 2010 +0200
+++ b/gamelib/scenes/scene_widgets.py	Sat Sep 04 09:53:00 2010 +0200
@@ -184,21 +184,31 @@
 
 class BaseCamera(Thing):
     "Base class for the camera puzzles"
- 
+
     INITIAL = 'online'
     INITIAL_DATA = {
          'state': 'online',
     }
- 
+
     def get_description(self):
-        return "A security camera watches over the room"
- 
+        status = self.state.scenes['bridge'].get_data('ai status')
+        if status == 'online':
+            return "A security camera watches over the room"
+        elif status == 'looping':
+            return "The security camera is currently offline but should be working soon"
+        else:
+            return "The security camera is powered down"
+
+    def is_interactive(self):
+        return self.state.scenes['bridge'].get_data('ai status') == 'online'
+
     def interact_with_escher_poster(self, item):
         # Order matters here, because of helper function
-        ai_response = make_jim_dialog("3D scene reconstruction failed. Critical error. Entering emergency shutdown.", self.state)
-        self.state.scenes['bridge'].set_data('ai status', 'looping')
-        return ai_response
- 
+        if self.state.scenes['bridge'].get_data('ai status') == 'online':
+            ai_response = make_jim_dialog("3D scene reconstruction failed. Critical error. Entering emergency shutdown.", self.state)
+            self.state.scenes['bridge'].set_data('ai status', 'looping')
+            return ai_response
+
     def animate(self):
         ai_status = self.state.scenes['bridge'].get_data('ai status')
         if ai_status != self.get_data('status'):
--- a/gamelib/state.py	Sun Aug 29 20:38:03 2010 +0200
+++ b/gamelib/state.py	Sat Sep 04 09:53:00 2010 +0200
@@ -1,5 +1,7 @@
 """Utilities and base classes for dealing with scenes."""
 
+import copy
+
 from albow.resource import get_image, get_font
 from albow.utils import frame_rect
 from widgets import BoomLabel
@@ -237,7 +239,9 @@
     def __init__(self):
         self.data = {}
         if self.INITIAL_DATA:
-            self.data.update(self.INITIAL_DATA)
+            # deep copy of INITIAL_DATA allows lists, sets and
+            # other mutable types to safely be used in INITIAL_DATA
+            self.data.update(copy.deepcopy(self.INITIAL_DATA))
 
     def set_data(self, key, value):
         self.data[key] = value
Binary file sources/art/cryo_comp_info_detail.xcf has changed
--- a/tools/rect_drawer.py	Sun Aug 29 20:38:03 2010 +0200
+++ b/tools/rect_drawer.py	Sat Sep 04 09:53:00 2010 +0200
@@ -10,16 +10,22 @@
 from albow.root import RootWidget
 from albow.utils import frame_rect
 from albow.widget import Widget
-from albow.controls import Button, Image
+from albow.controls import Button, Image, Label
 from albow.palette_view import PaletteView
 from albow.file_dialogs import request_old_filename
 from albow.resource import get_font
-from pygame.locals import SWSURFACE, K_LEFT, K_RIGHT, K_UP, K_DOWN
+from pygame.locals import SWSURFACE, K_LEFT, K_RIGHT, K_UP, K_DOWN, \
+        K_a, K_t, K_d, K_i, K_r, K_o, K_b, K_z, \
+        BLEND_RGBA_MIN, SRCALPHA
 import pygame
 from pygame.colordict import THECOLORS
 
 from gamelib import constants
 constants.DEBUG = True
+MENU_WIDTH = 200
+MENU_BUTTON_HEIGHT = 25
+ZOOM = 4
+ZOOM_STEP = 100
 
 from gamelib import state
 state.DEBUG_RECTS = True
@@ -39,7 +45,7 @@
 
     def __init__(self, app_image):
         self.image = app_image
-        super(AppPalette, self).__init__((35, 35), 5, 5, margin=2)
+        super(AppPalette, self).__init__((35, 35), 4, 5, margin=2)
         self.selection = 0
         self.image.rect_color = pygame.color.Color(self.colors[self.selection])
 
@@ -66,7 +72,7 @@
 
     def __init__(self, state):
         self.state = state
-        super(AppImage, self).__init__(pygame.rect.Rect(0, 0, 800, 600))
+        super(AppImage, self).__init__(pygame.rect.Rect(0, 0, constants.SCREEN[0], constants.SCREEN[1]))
         self.mode = 'draw'
         self.rects = []
         self.images = []
@@ -90,15 +96,23 @@
         self.draw_things = True
         self.draw_thing_rects = True
         self.draw_images = True
+        self.trans_images = False
         self.draw_toolbar = True
+        self.old_mouse_pos = None
+        self.zoom_display = False
+        self.draw_anim = False
+        self.zoom_offset = (600, 600)
         self.find_existing_intersects()
 
+    def _get_scene(self):
+        if self.state.current_detail:
+            return self.state.current_detail
+        else:
+            return self.state.current_scene
+
     def find_existing_intersects(self):
         """Parse the things in the scene for overlaps"""
-        if self.state.current_detail:
-            scene = self.state.current_detail
-        else:
-            scene = self.state.current_scene
+        scene = self._get_scene()
         # Pylint hates this function
         for thing in scene.things.itervalues():
             for interact_name in thing.interacts:
@@ -128,10 +142,7 @@
     def find_intersecting_rects(self, d):
         """Find if any rect collections intersect"""
         # I loath N^X brute search algorithm's, but whatever, hey
-        if self.state.current_detail:
-            scene = self.state.current_detail
-        else:
-            scene = self.state.current_scene
+        scene = self._get_scene()
         for (num, col) in enumerate(d):
             rect_list = d[col]
             for thing in scene.things.itervalues():
@@ -169,10 +180,7 @@
 
     def toggle_thing_rects(self):
         self.draw_thing_rects = not self.draw_thing_rects
-        if self.state.current_detail:
-            scene = self.state.current_detail
-        else:
-            scene = self.state.current_scene
+        scene = self._get_scene()
         for thing in scene.things.itervalues():
             if not self.draw_thing_rects:
                 if not hasattr(thing, 'old_colour'):
@@ -184,12 +192,22 @@
     def toggle_images(self):
         self.draw_images = not self.draw_images
 
+    def toggle_trans_images(self):
+        self.trans_images = not self.trans_images
+        self.invalidate()
+
     def toggle_rects(self):
         self.draw_rects = not self.draw_rects
 
     def toggle_toolbar(self):
         self.draw_toolbar = not self.draw_toolbar
 
+    def toggle_zoom(self):
+        self.zoom_display = not self.zoom_display
+
+    def toggle_anim(self):
+        self.draw_anim = not self.draw_anim
+
     def draw_mode(self):
         self.mode = 'draw'
 
@@ -198,7 +216,32 @@
         self.start_pos = None
         self.end_pos = None
 
+    def draw_sub_image(self, image, surface, cropped_rect):
+        """Tweaked image drawing to avoid albow's centring the image in the
+           subsurface"""
+        surf = pygame.surface.Surface((cropped_rect.w, cropped_rect.h), SRCALPHA).convert_alpha()
+        frame = surf.get_rect()
+        imsurf = image.get_image().convert_alpha()
+        r = imsurf.get_rect()
+        r.topleft = frame.topleft
+        if self.trans_images:
+            surf.fill(pygame.color.Color(255, 255, 255, 96))
+            surf.blit(imsurf, r, None, BLEND_RGBA_MIN)
+        else:
+            surf.blit(imsurf, r, None)
+        surface.blit(surf, cropped_rect)
+
     def draw(self, surface):
+        if self.zoom_display:
+            base_surface = surface.copy()
+            self.do_unzoomed_draw(base_surface)
+            zoomed = pygame.transform.scale(base_surface, (ZOOM * constants.SCREEN[0], ZOOM * constants.SCREEN[1]))
+            area = pygame.rect.Rect(self.zoom_offset[0], self.zoom_offset[1], self.zoom_offset[0] + constants.SCREEN[0], self.zoom_offset[1] + constants.SCREEN[1])
+            surface.blit(zoomed, (0, 0), area)
+        else:
+            self.do_unzoomed_draw(surface)
+
+    def do_unzoomed_draw(self, surface):
         if self.state.current_detail:
             if self.draw_things:
                 self.state.draw_detail(surface, None)
@@ -222,20 +265,18 @@
             for image in self.images:
                 if image.rect.colliderect(surface.get_rect()):
                     cropped_rect = image.rect.clip(surface.get_rect())
-                    sub = surface.subsurface(cropped_rect)
-                    image.draw(sub)
+                    self.draw_sub_image(image, surface, cropped_rect)
                 else:
                     print 'image outside surface', image
             if self.current_image and self.mode == 'image':
                 if self.current_image.rect.colliderect(surface.get_rect()):
                     cropped_rect = self.current_image.rect.clip(surface.get_rect())
-                    sub = surface.subsurface(cropped_rect)
-                    self.current_image.draw(sub)
+                    self.draw_sub_image(self.current_image, surface, cropped_rect)
         if self.draw_toolbar:
-            toolbar_rect = pygame.rect.Rect(0, 550, 800, 50)
-            tb_surf = surface.subsurface(0, 550, 800, 50).convert_alpha()
+            toolbar_rect = pygame.rect.Rect(0, constants.SCREEN[1] - constants.BUTTON_SIZE, constants.SCREEN[0], constants.BUTTON_SIZE)
+            tb_surf = surface.subsurface(0, constants.SCREEN[1] - constants.BUTTON_SIZE, constants.SCREEN[0], constants.BUTTON_SIZE).convert_alpha()
             tb_surf.fill(pygame.color.Color(127, 0, 0, 191))
-            surface.blit(tb_surf, (0, 550))
+            surface.blit(tb_surf, (0, constants.SCREEN[1] - constants.BUTTON_SIZE))
             # frame_rect(surface, (127, 0, 0), toolbar_rect, 2)
 
     def _make_dict(self):
@@ -271,7 +312,7 @@
             self.current_image = Image(image_data)
             self.place_image_menu.enabled = True
             # ensure we're off screen to start
-            self.current_image.rect = image_data.get_rect().move(1000, 600)
+            self.current_image.rect = image_data.get_rect().move(constants.SCREEN[0] + MENU_WIDTH, constants.SCREEN[1])
         except pygame.error, e:
             print 'Unable to load image %s' % e
 
@@ -279,28 +320,99 @@
         self.mode = 'image'
         self.start_pos = None
         self.end_pos = None
+        # So we do the right thing for off screen images
+        self.old_mouse_pos = None
 
-    def mouse_move(self, e):
+    def cycle_mode(self):
+        self.mode = 'cycle'
+
+    def _conv_pos(self, mouse_pos):
+        if self.zoom_display:
+            pos = ((mouse_pos[0] + self.zoom_offset[0]) / ZOOM, (mouse_pos[1] + self.zoom_offset[1]) / ZOOM)
+        else:
+            pos = mouse_pos
+        return pos
+
+    def _check_limits(self, offset):
+        if offset[0] < 0:
+            offset[0] = 0
+        if offset[1] < 0:
+            offset[1] = 0
+        if offset[0] > ZOOM * constants.SCREEN[0] - constants.SCREEN[0]:
+            offset[0] = ZOOM * constants.SCREEN[0] - constants.SCREEN[0]
+        if offset[1] > ZOOM * constants.SCREEN[1] - constants.SCREEN[1]:
+            offset[1] = ZOOM * constants.SCREEN[1] - constants.SCREEN[1]
+
+    def _make_zoom_offset(self, pos):
+        zoom_pos = (pos[0] * ZOOM, pos[1] * ZOOM)
+        offset = [zoom_pos[0] - constants.SCREEN[0] / 2,
+                zoom_pos[1] - constants.SCREEN[1] / 2]
+        self._check_limits(offset)
+        self.zoom_offset = tuple(offset)
+
+    def _move_zoom(self, x, y):
+        offset = list(self.zoom_offset)
+        offset[0] += ZOOM_STEP * x
+        offset[1] += ZOOM_STEP * y
+        self._check_limits(offset)
+        self.zoom_offset = tuple(offset)
+
+    def do_mouse_move(self, e):
+        pos = self._conv_pos(e.pos)
+        if not self.zoom_display:
+            # Construct zoom offset from mouse pos
+            self._make_zoom_offset(e.pos)
         if self.mode == 'image' and self.current_image:
-            self.current_image.rect.topleft = e.pos
+            if self.old_mouse_pos:
+                delta = (pos[0] - self.old_mouse_pos[0], pos[1] - self.old_mouse_pos[1])
+                self.current_image.rect.center = (self.current_image.rect.center[0] + delta[0], self.current_image.rect.center[1] + delta[1])
+            else:
+                self.current_image.rect.center = pos
             self.invalidate()
+            self.old_mouse_pos = pos
 
     def key_down(self, e):
         if self.mode == 'image' and self.current_image:
             # Move the image by 1 pixel
-            cur_pos = self.current_image.rect.topleft
+            cur_pos = self.current_image.rect.center
             if e.key == K_LEFT:
-                self.current_image.rect.topleft = (cur_pos[0] - 1, cur_pos[1])
+                self.current_image.rect.center = (cur_pos[0] - 1, cur_pos[1])
+            elif e.key == K_RIGHT:
+                self.current_image.rect.center = (cur_pos[0] + 1, cur_pos[1])
+            elif e.key == K_UP:
+                self.current_image.rect.center = (cur_pos[0], cur_pos[1] - 1)
+            elif e.key == K_DOWN:
+                self.current_image.rect.center = (cur_pos[0], cur_pos[1] + 1)
+        elif self.zoom_display:
+            if e.key == K_LEFT:
+                self._move_zoom(-1, 0)
             elif e.key == K_RIGHT:
-                self.current_image.rect.topleft = (cur_pos[0] + 1, cur_pos[1])
+                self._move_zoom(1, 0)
             elif e.key == K_UP:
-                self.current_image.rect.topleft = (cur_pos[0], cur_pos[1] - 1)
+                self._move_zoom(0, -1)
             elif e.key == K_DOWN:
-                self.current_image.rect.topleft = (cur_pos[0], cur_pos[1] + 1)
+                self._move_zoom(0, 1)
+
+        if e.key == K_o:
+            self.toggle_trans_images()
+        elif e.key == K_t:
+            self.toggle_things()
+        elif e.key == K_r:
+            self.toggle_thing_rects()
+        elif e.key == K_i:
+            self.toggle_images()
+        elif e.key == K_d:
+            self.toggle_rects()
+        elif e.key == K_b:
+            self.toggle_toolbar()
+        elif e.key == K_z:
+            self.toggle_zoom()
+        elif e.key == K_a:
+            self.toggle_anim()
 
     def mouse_down(self, e):
+        pos = self._conv_pos(e.pos)
         if self.mode == 'del':
-            pos = e.pos
             cand = None
             # Images are drawn above rectangles, so search those first
             for image in self.images:
@@ -318,13 +430,31 @@
             if cand:
                 self.rects.remove(cand)
                 self.invalidate()
+        elif self.mode == 'cycle':
+            scene = self._get_scene()
+            cand = None
+            for thing in scene.things.itervalues():
+                if thing.contains(pos):
+                    cand = thing
+                    break
+            if cand:
+                # Find current interacts in this thing
+                cur_interact = cand.current_interact
+                j = cand.interacts.values().index(cur_interact)
+                if j + 1< len(cand.interacts):
+                    next_name = cand.interacts.keys()[j+1]
+                else:
+                    next_name = cand.interacts.keys()[0]
+                if cand.interacts[next_name] != cur_interact:
+                    cand.set_interact(next_name)
         elif self.mode == 'draw':
-            self.start_pos = e.pos
-            self.end_pos = e.pos
+            self.start_pos = pos
+            self.end_pos = pos
         elif self.mode == 'image':
             if self.current_image:
                 self.images.append(self.current_image)
                 self.current_image = None
+                self.old_mouse_pos = None
                 self.invalidate()
             else:
                 cand = None
@@ -335,7 +465,8 @@
                 if cand:
                     self.images.remove(cand)
                     self.current_image = cand
-                    self.current_image.topleft = e.pos
+                    # We want to move relative to the current mouse pos, so
+                    self.old_mouse_pos = pos
                     self.invalidate()
 
     def mouse_up(self, e):
@@ -349,16 +480,37 @@
 
     def mouse_drag(self, e):
         if self.mode == 'draw':
-            self.end_pos = e.pos
+            self.end_pos = self._conv_pos(e.pos)
             self.invalidate()
 
+    def animate(self):
+        if self.draw_anim:
+            if self.state.animate():
+                self.invalidate()
+
+
+class ModeLabel(BoomLabel):
+
+    def __init__(self, app_image):
+        self.app_image = app_image
+        super(ModeLabel, self).__init__('Mode : ', 200,
+                font=get_font(15, 'VeraBd.ttf'),
+                fg_color = pygame.color.Color(128, 0, 255))
+        self.rect.move_ip(805, 0)
+
+    def draw_all(self, surface):
+        self.set_text('Mode : %s' % self.app_image.mode)
+        super(ModeLabel, self).draw_all(surface)
+
+
 def make_button(text, action, ypos):
-    button = Button(text, action=action)
+    button = Button(text, action=action, font=get_font(15, 'VeraBd.ttf'))
     button.align = 'l'
-    button.rect = pygame.rect.Rect(0, 0, 200, 35)
+    button.rect = pygame.rect.Rect(0, 0, MENU_WIDTH, MENU_BUTTON_HEIGHT)
     button.rect.move_ip(805, ypos)
     return button
 
+
 class RectApp(RootWidget):
     """Handle the app stuff for the rect drawer"""
 
@@ -366,38 +518,73 @@
         super(RectApp, self).__init__(display)
         self.image = AppImage(state)
         self.add(self.image)
-        draw = make_button('Draw Rect', self.image.draw_mode, 0)
+        mode_label = ModeLabel(self.image)
+        self.add(mode_label)
+        y = mode_label.get_rect().h
+        draw = make_button('Draw Rect', self.image.draw_mode, y)
         self.add(draw)
-        load_image = make_button("Load image", self.image.image_load, 35)
+        y += draw.get_rect().h
+        load_image = make_button("Load image", self.image.image_load, y)
         self.add(load_image)
-        add_image = make_button("Place/Move images", self.image.image_mode, 70)
+        y += load_image.get_rect().h
+        add_image = make_button("Place/Move images", self.image.image_mode, y)
         add_image.enabled = False
         self.add(add_image)
         self.image.place_image_menu = add_image
-        delete = make_button('Delete Objects', self.image.del_mode, 105)
+        y += add_image.get_rect().h
+        cycle = make_button("Cycle interacts", self.image.cycle_mode, y)
+        self.add(cycle)
+        y += cycle.get_rect().h
+        delete = make_button('Delete Objects', self.image.del_mode, y)
         self.add(delete)
+        y += delete.get_rect().h
         palette = AppPalette(self.image)
-        palette.rect.move_ip(810, 140)
+        palette.rect.move_ip(810, y)
         self.add(palette)
-        print_rects = make_button("Print objects", self.image.print_objs, 300)
+        y += palette.get_rect().h
+        print_rects = make_button("Print objects", self.image.print_objs, y)
         self.add(print_rects)
-        toggle_things = make_button("Toggle Things", self.image.toggle_things, 335)
+        y += print_rects.get_rect().h
+        toggle_things = make_button("Show Things (t)", self.image.toggle_things, y)
         self.add(toggle_things)
-        toggle_thing_rects = make_button("Toggle Thing Rects", self.image.toggle_thing_rects, 370)
+        y += toggle_things.get_rect().h
+        toggle_thing_rects = make_button("Show Thing Rects (r)", self.image.toggle_thing_rects, y)
         self.add(toggle_thing_rects)
-        toggle_images = make_button("Toggle Images", self.image.toggle_images, 405)
+        y += toggle_thing_rects.get_rect().h
+        toggle_images = make_button("Show Images (i)", self.image.toggle_images, y)
         self.add(toggle_images)
-        toggle_rects = make_button("Toggle Rects", self.image.toggle_rects, 440)
+        y += toggle_images.get_rect().h
+        trans_images = make_button("Opaque Images (o)", self.image.toggle_trans_images, y)
+        self.add(trans_images)
+        y += trans_images.get_rect().h
+        toggle_rects = make_button("Show Drawn Rects (d)", self.image.toggle_rects, y)
         self.add(toggle_rects)
-        toggle_toolbar = make_button("Toggle Toolbar", self.image.toggle_toolbar, 475)
+        y += toggle_rects.get_rect().h
+        toggle_toolbar = make_button("Show Toolbar (b)", self.image.toggle_toolbar, y)
         self.add(toggle_toolbar)
-        quit_but = make_button("Quit", self.quit, 565)
+        y += toggle_toolbar.get_rect().h
+        toggle_anim = make_button("Show Animations (a)", self.image.toggle_anim, y)
+        self.add(toggle_anim)
+        y += toggle_anim.get_rect().h
+        toggle_zoom = make_button("Zoom (z)", self.image.toggle_zoom, y)
+        self.add(toggle_zoom)
+        y += toggle_zoom.get_rect().h
+        quit_but = make_button("Quit", self.quit, 570)
         self.add(quit_but)
+        self.set_timer(constants.FRAME_RATE)
 
     def key_down(self, event):
         # Dispatch to image widget
         self.image.key_down(event)
 
+    def mouse_delta(self, event):
+        # We propogate mouse move from here to draw region, so images move
+        # off-screen
+        self.image.do_mouse_move(event)
+
+    def begin_frame(self):
+        self.image.animate()
+
 
 if __name__ == "__main__":
     # FIXME: should load an actual scene with current things, not just a
@@ -409,7 +596,7 @@
     pygame.font.init()
     # enable key repeating
     pygame.key.set_repeat(200, 100)
-    display = pygame.display.set_mode((1000, 600))
+    display = pygame.display.set_mode((constants.SCREEN[0] + MENU_WIDTH, constants.SCREEN[1]))
     state = state.initial_state()
     if len(sys.argv) < 3:
         try: