# HG changeset patch # User Jeremy Thurgood # Date 1328958618 -7200 # Node ID ded4324b236e10830791cef2d7f7e9abee3ba75e # Parent 33ce7ff757c353d1f6ce99a4fda5cff066a328db Moved stuff around, broke everything. diff -r 33ce7ff757c3 -r ded4324b236e gamelib/constants.py --- a/gamelib/constants.py Sat Feb 11 12:52:29 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,18 +0,0 @@ -# Useful constants -# copyright boomslang team (see COPYRIGHT file for details) - - -SCREEN = (800, 600) -FREQ = 44100 # same as audio CD -BITSIZE = -16 # unsigned 16 bit -CHANNELS = 2 # 1 == mono, 2 == stereo -BUFFER = 1024 # audio buffer size in no. of samples - -BUTTON_SIZE = 50 -SCENE_SIZE = (SCREEN[0], SCREEN[1] - BUTTON_SIZE) -# Animation frame rate -FRAME_RATE = 25 - -DEBUG = False - -ENTER, LEAVE = 1, 2 diff -r 33ce7ff757c3 -r ded4324b236e gamelib/cursor.py --- a/gamelib/cursor.py Sat Feb 11 12:52:29 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,116 +0,0 @@ -# cursor.py -# Copyright Boomslang team, 2010 (see COPYING File) -# Sprite Cursor - -from albow.resource import get_image -from albow.widget import Widget -from pygame.sprite import Sprite, RenderUpdates -from pygame.rect import Rect -import pygame -import pygame.color -import pygame.cursors -import pygame.mouse - -from gamelib.constants import SCENE_SIZE - - -class CursorSprite(Sprite): - "A Sprite that follows the Cursor" - - def __init__(self, filename, x=None, y=None): - Sprite.__init__(self) - self.filename = filename - self.pointer_x = x - self.pointer_y = y - self.highlighted = False - - def load(self): - if not hasattr(self, 'plain_image'): - self.plain_image = get_image('items', self.filename) - self.image = self.plain_image - self.rect = self.image.get_rect() - - if self.pointer_x is None: - self.pointer_x = self.rect.size[0] // 2 - if self.pointer_y is None: - self.pointer_y = self.rect.size[1] // 2 - - self.highlight = pygame.Surface(self.rect.size) - color = pygame.color.Color(255, 100, 100, 0) - self.highlight.fill(color) - - def update(self): - pos = pygame.mouse.get_pos() - self.rect.left = pos[0] - self.pointer_x - self.rect.top = pos[1] - self.pointer_y - - def set_highlight(self, enable): - if enable != self.highlighted: - self.load() - self.highlighted = enable - self.image = self.plain_image.copy() - if enable: - self.image.blit(self.highlight, self.highlight.get_rect(), - None, pygame.BLEND_MULT) - - -HAND = CursorSprite('hand.png', 12, 0) - - -class CursorWidget(Widget): - """Mix-in widget to ensure that mouse_move is propogated to parents""" - - cursor = HAND - _cursor_group = RenderUpdates() - _loaded_cursor = None - - def __init__(self, screen, *args, **kwargs): - Widget.__init__(self, *args, **kwargs) - self.screen = screen - - def enter_screen(self): - pygame.mouse.set_visible(0) - - def leave_screen(self): - pygame.mouse.set_visible(1) - - def draw_all(self, surface): - Widget.draw_all(self, surface) - self.draw_cursor(self.get_root().surface) - - def draw_cursor(self, surface): - self.set_cursor(self.screen.state.tool) - self.cursor.set_highlight(self.cursor_highlight()) - if self.cursor is not None: - self._cursor_group.update() - self._cursor_group.draw(surface) - - def mouse_delta(self, event): - self.invalidate() - - @classmethod - def set_cursor(cls, item): - if item is None or item.CURSOR is None: - cls.cursor = HAND - else: - cls.cursor = item.CURSOR - if cls.cursor != cls._loaded_cursor: - cls._loaded_cursor = cls.cursor - if cls.cursor is None: - pygame.mouse.set_visible(1) - cls._cursor_group.empty() - else: - pygame.mouse.set_visible(0) - cls.cursor.load() - cls._cursor_group.empty() - cls._cursor_group.add(cls.cursor) - - def cursor_highlight(self): - if not Rect((0, 0), SCENE_SIZE).collidepoint(pygame.mouse.get_pos()): - return False - if self.screen.state.highlight_override: - return True - current_thing = self.screen.state.current_thing - if current_thing: - return current_thing.is_interactive() - return False diff -r 33ce7ff757c3 -r ded4324b236e gamelib/data.py --- a/gamelib/data.py Sat Feb 11 12:52:29 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,25 +0,0 @@ -'''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, '..', 'Resources')) - - -def filepath(filename): - '''Determine the path to a file in the data directory. - ''' - return os.path.join(data_dir, filename) - - -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 -r 33ce7ff757c3 -r ded4324b236e gamelib/endscreen.py --- a/gamelib/endscreen.py Sat Feb 11 12:52:29 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,33 +0,0 @@ -# endscreen.py -# Copyright Boomslang team, 2010 (see COPYING File) -# Victory screen for the game - -from albow.screen import Screen -from albow.resource import get_image - -from gamelib.widgets import BoomImageButton - - -class EndImageButton(BoomImageButton): - - FOLDER = 'won' - - -class EndScreen(Screen): - def __init__(self, shell): - Screen.__init__(self, shell) - self.background = get_image('won', 'won.png') - self._menu_button = EndImageButton('menu.png', 26, 500, - action=self.main_menu) - self._quit_button = EndImageButton('quit.png', 250, 500, - action=shell.quit) - self.add(self._menu_button) - self.add(self._quit_button) - - def draw(self, surface): - surface.blit(self.background, (0, 0)) - self._menu_button.draw(surface) - self._quit_button.draw(surface) - - def main_menu(self): - self.shell.show_screen(self.shell.menu_screen) diff -r 33ce7ff757c3 -r ded4324b236e gamelib/gamescreen.py --- a/gamelib/gamescreen.py Sat Feb 11 12:52:29 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,261 +0,0 @@ -# gamescreen.py -# Copyright Boomslang team, 2010 (see COPYING File) -# Main menu for the game - -from albow.controls import Widget -from albow.layout import Row -from albow.palette_view import PaletteView -from albow.screen import Screen -from pygame import Rect, mouse -from pygame.color import Color - -from constants import SCREEN, BUTTON_SIZE, SCENE_SIZE, LEAVE -from cursor import CursorWidget -from state import initial_state, handle_result -from widgets import (MessageDialog, BoomButton, HandButton, PopupMenu, - PopupMenuButton) - - -class InventoryView(PaletteView): - - sel_color = Color("yellow") - sel_width = 2 - - def __init__(self, screen): - PaletteView.__init__(self, (BUTTON_SIZE, BUTTON_SIZE), 1, 6, - scrolling=True) - self.screen = screen - self.state = screen.state - self.state_widget = screen.state_widget - - def num_items(self): - return len(self.state.inventory) - - def draw_item(self, surface, item_no, rect): - item_image = self.state.inventory[item_no].get_inventory_image() - surface.blit(item_image, rect, None) - - def click_item(self, item_no, event): - item = self.state.inventory[item_no] - if self.item_is_selected(item_no): - self.unselect() - elif item.is_interactive(self.state.tool): - result = item.interact(self.state.tool) - handle_result(result, self.state_widget) - else: - self.state.set_tool(self.state.inventory[item_no]) - - def mouse_down(self, event): - if event.button != 1: - self.state.cancel_doodah(self.screen) - else: - PaletteView.mouse_down(self, event) - - def item_is_selected(self, item_no): - return self.state.tool is self.state.inventory[item_no] - - def unselect(self): - self.state.set_tool(None) - - -class StateWidget(Widget): - - def __init__(self, screen): - Widget.__init__(self, Rect(0, 0, SCENE_SIZE[0], SCENE_SIZE[1])) - self.screen = screen - self.state = screen.state - self.detail = DetailWindow(screen) - - def draw(self, surface): - if self.state.previous_scene and self.state.do_check == LEAVE: - # We still need to handle leave events, so still display the scene - self.state.previous_scene.draw(surface, self) - else: - self.state.current_scene.draw(surface, self) - - def mouse_down(self, event): - self.mouse_move(event) - if event.button != 1: # We have a right/middle click - self.state.cancel_doodah(self.screen) - elif self.subwidgets: - self.clear_detail() - self._mouse_move(event.pos) - else: - result = self.state.interact(event.pos) - handle_result(result, self) - - def animate(self): - if self.state.animate(): - # queue a redraw - self.invalidate() - # We do this here so we can get enter and leave events regardless - # of what happens - result = self.state.check_enter_leave(self.screen) - handle_result(result, self) - - def mouse_move(self, event): - self.state.highlight_override = False - if not self.subwidgets: - self._mouse_move(event.pos) - - def _mouse_move(self, pos): - self.state.highlight_override = False - self.state.current_scene.mouse_move(pos) - self.state.old_pos = pos - - def show_message(self, message, style=None): - # Display the message as a modal dialog - MessageDialog(self.screen, message, 60, style=style).present() - # queue a redraw to show updated state - self.invalidate() - # The cursor could have gone anywhere - if self.subwidgets: - self.subwidgets[0]._mouse_move(mouse.get_pos()) - else: - self._mouse_move(mouse.get_pos()) - - def show_detail(self, detail): - self.clear_detail() - detail_obj = self.state.set_current_detail(detail) - self.detail.set_image_rect(Rect((0, 0), detail_obj.get_detail_size())) - self.add_centered(self.detail) - self.state.do_enter_detail() - - def clear_detail(self): - """Hide the detail view""" - if self.state.current_detail is not None: - self.remove(self.detail) - self.state.do_leave_detail() - self.state.set_current_detail(None) - self._mouse_move(mouse.get_pos()) - - def end_game(self): - self.screen.running = False - self.screen.shell.show_screen(self.screen.shell.end_screen) - - -class DetailWindow(Widget): - def __init__(self, screen): - Widget.__init__(self) - self.image_rect = None - self.screen = screen - self.state = screen.state - self.border_width = 5 - self.border_color = (0, 0, 0) - # parent only gets set when we get added to the scene - self.close = BoomButton('Close', self.close_but, screen) - self.add(self.close) - - def close_but(self): - self.parent.clear_detail() - - def end_game(self): - self.parent.end_game() - - def set_image_rect(self, rect): - bw = self.border_width - self.image_rect = rect - self.image_rect.topleft = (bw, bw) - self.set_rect(rect.inflate(bw * 2, bw * 2)) - self.close.rect.midbottom = rect.midbottom - - def draw(self, surface): - scene_surface = self.get_root().surface.subsurface(self.parent.rect) - overlay = scene_surface.convert_alpha() - overlay.fill(Color(0, 0, 0, 191)) - scene_surface.blit(overlay, (0, 0)) - self.state.current_detail.draw( - surface.subsurface(self.image_rect), self) - - def mouse_down(self, event): - self.mouse_move(event) - if event.button != 1: # We have a right/middle click - self.state.cancel_doodah(self.screen) - else: - result = self.state.interact_detail( - self.global_to_local(event.pos)) - handle_result(result, self) - - def mouse_move(self, event): - self._mouse_move(event.pos) - - def _mouse_move(self, pos): - self.state.highlight_override = False - self.state.current_detail.mouse_move(self.global_to_local(pos)) - - def show_message(self, message, style=None): - self.parent.show_message(message, style) - self.invalidate() - - -class ToolBar(Row): - def __init__(self, items): - for item in items: - item.height = BUTTON_SIZE - Row.__init__(self, items, spacing=0, width=SCREEN[0]) - self.bg_color = (31, 31, 31) - - def draw(self, surface): - surface.fill(self.bg_color) - Row.draw(self, surface) - - -class GameScreen(Screen, CursorWidget): - def __init__(self, shell): - CursorWidget.__init__(self, self) - Screen.__init__(self, shell) - self.running = False - - def _clear_all(self): - for widget in self.subwidgets[:]: - self.remove(widget) - - def start_game(self): - self._clear_all() - self.state = initial_state() - self.state_widget = StateWidget(self) - self.add(self.state_widget) - - self.popup_menu = PopupMenu(self) - self.menubutton = PopupMenuButton('Menu', - action=self.popup_menu.show_menu) - - self.handbutton = HandButton(action=self.hand_pressed) - - self.inventory = InventoryView(self) - - self.toolbar = ToolBar([ - self.menubutton, - self.handbutton, - self.inventory, - ]) - self.toolbar.bottomleft = self.bottomleft - self.add(self.toolbar) - - self.running = True - - def enter_screen(self): - CursorWidget.enter_screen(self) - - def leave_screen(self): - CursorWidget.leave_screen(self) - - # Albow uses magic method names (command + '_cmd'). Yay. - # Albow's search order means they need to be defined here, not in - # PopMenu, which is annoying. - def hide_cmd(self): - # This option does nothing, but the method needs to exist for albow - return - - def main_menu_cmd(self): - self.shell.show_screen(self.shell.menu_screen) - - def quit_cmd(self): - self.shell.quit() - - def hand_pressed(self): - self.inventory.unselect() - - def begin_frame(self): - if self.running: - self.state_widget.animate() diff -r 33ce7ff757c3 -r ded4324b236e gamelib/main.py --- a/gamelib/main.py Sat Feb 11 12:52:29 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,77 +0,0 @@ -'''Game main module. - -Contains the entry point used by the run_game.py script. - -''' - -# Albow looks for stuff in os.path[0], which isn't always where it expects. -# The following horribleness fixes this. -import sys -import os.path -right_path = os.path.dirname(os.path.dirname(__file__)) -sys.path.insert(0, right_path) -from optparse import OptionParser - -import pygame -from pygame.locals import SWSURFACE -from albow.shell import Shell - -from menu import MenuScreen -from gamescreen import GameScreen -from endscreen import EndScreen -from constants import ( - SCREEN, FRAME_RATE, FREQ, BITSIZE, CHANNELS, BUFFER, DEBUG) -from sound import no_sound, disable_sound -import state -import data - - -def parse_args(args): - parser = OptionParser() - parser.add_option("--no-sound", action="store_false", default=True, - dest="sound", help="disable sound") - if DEBUG: - parser.add_option("--scene", type="str", default=None, - dest="scene", help="initial scene") - parser.add_option("--no-rects", action="store_false", default=True, - dest="rects", help="disable debugging rects") - opts, _ = parser.parse_args(args or []) - return opts - - -class MainShell(Shell): - def __init__(self, display): - Shell.__init__(self, display) - self.menu_screen = MenuScreen(self) - self.game_screen = GameScreen(self) - self.end_screen = EndScreen(self) - self.set_timer(FRAME_RATE) - self.show_screen(self.menu_screen) - - -def main(): - opts = parse_args(sys.argv) - pygame.display.init() - pygame.font.init() - if opts.sound: - try: - pygame.mixer.init(FREQ, BITSIZE, CHANNELS, BUFFER) - except pygame.error, exc: - no_sound(exc) - else: - # Ensure get_sound returns nothing, so everything else just works - disable_sound() - if DEBUG: - if opts.scene is not None: - # debug the specified scene - state.DEBUG_SCENE = opts.scene - state.DEBUG_RECTS = opts.rects - display = pygame.display.set_mode(SCREEN, SWSURFACE) - pygame.display.set_icon(pygame.image.load( - data.filepath('icons/suspended_sentence24x24.png'))) - pygame.display.set_caption("Suspended Sentence") - shell = MainShell(display) - try: - shell.run() - except KeyboardInterrupt: - pass diff -r 33ce7ff757c3 -r ded4324b236e gamelib/menu.py --- a/gamelib/menu.py Sat Feb 11 12:52:29 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,43 +0,0 @@ -# menu.py -# Copyright Boomslang team, 2010 (see COPYING File) -# Main menu for the game - -from albow.screen import Screen -from albow.resource import get_image - -from gamelib.widgets import BoomImageButton - - -class SplashButton(BoomImageButton): - - FOLDER = 'splash' - - -class MenuScreen(Screen): - def __init__(self, shell): - Screen.__init__(self, shell) - self._background = get_image('splash', 'splash.png') - self._start_button = SplashButton('play.png', 16, 523, self.start) - self._resume_button = SplashButton('resume.png', 256, 523, self.resume, - enable=self.check_running) - self._quit_button = SplashButton('quit.png', 580, 523, shell.quit) - self.add(self._start_button) - self.add(self._resume_button) - self.add(self._quit_button) - - def draw(self, surface): - surface.blit(self._background, (0, 0)) - self._start_button.draw(surface) - self._resume_button.draw(surface) - self._quit_button.draw(surface) - - def start(self): - self.shell.game_screen.start_game() - self.shell.show_screen(self.shell.game_screen) - - def check_running(self): - return self.shell.game_screen.running - - def resume(self): - if self.shell.game_screen.running: - self.shell.show_screen(self.shell.game_screen) diff -r 33ce7ff757c3 -r ded4324b236e gamelib/scenewidgets.py --- a/gamelib/scenewidgets.py Sat Feb 11 12:52:29 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,149 +0,0 @@ -"""Interactive elements within a Scene.""" - - -from pygame import Rect -from pygame.color import Color -from pygame.colordict import THECOLORS -from pygame.surface import Surface -from albow.resource import get_image - -from gamelib.state import Thing -from gamelib.constants import DEBUG -from gamelib.widgets import BoomLabel - - -class Interact(object): - - def __init__(self, image, rect, interact_rect): - self.image = image - self.rect = rect - self.interact_rect = interact_rect - - def set_thing(self, thing): - pass - - def draw(self, surface): - if self.image is not None: - surface.blit(self.image, self.rect, None) - - def animate(self): - return False - - -class InteractNoImage(Interact): - - def __init__(self, x, y, w, h): - super(InteractNoImage, self).__init__(None, None, Rect(x, y, w, h)) - - -class InteractText(Interact): - """Display box with text to interact with -- mostly for debugging.""" - - def __init__(self, x, y, text, bg_color=None): - if bg_color is None: - bg_color = (127, 127, 127) - label = BoomLabel(text) - label.set_margin(5) - label.border_width = 1 - label.border_color = (0, 0, 0) - label.bg_color = bg_color - label.fg_color = (0, 0, 0) - image = Surface(label.size) - rect = Rect((x, y), label.size) - label.draw_all(image) - super(InteractText, self).__init__(image, rect, rect) - - -class InteractRectUnion(Interact): - - def __init__(self, rect_list): - # pygame.rect.Rect.unionall should do this, but is broken - # in some pygame versions (including 1.8, it appears) - rect_list = [Rect(x) for x in rect_list] - union_rect = rect_list[0] - for rect in rect_list[1:]: - union_rect = union_rect.union(rect) - super(InteractRectUnion, self).__init__(None, None, union_rect) - self.interact_rect = rect_list - - -class InteractImage(Interact): - - def __init__(self, x, y, image_name): - super(InteractImage, self).__init__(None, None, None) - self._pos = (x, y) - self._image_name = image_name - - def set_thing(self, thing): - self.image = get_image(thing.folder, self._image_name) - self.rect = Rect(self._pos, self.image.get_size()) - self.interact_rect = self.rect - - -class InteractImageRect(InteractImage): - def __init__(self, x, y, image_name, r_x, r_y, r_w, r_h): - super(InteractImageRect, self).__init__(x, y, image_name) - self._r_pos = (r_x, r_y) - self._r_size = (r_w, r_h) - - def set_thing(self, thing): - super(InteractImageRect, self).set_thing(thing) - self.interact_rect = Rect(self._r_pos, self._r_size) - - -class InteractAnimated(Interact): - """Interactive with an animation rather than an image""" - - # FIXME: Assumes all images are the same size - # anim_seq - sequence of image names - # delay - number of frames to wait between changing images - - def __init__(self, x, y, anim_seq, delay): - self._pos = (x, y) - self._anim_pos = 0 - self._names = anim_seq - self._frame_count = 0 - self._anim_seq = None - self._delay = delay - - def set_thing(self, thing): - self._anim_seq = [get_image(thing.folder, x) for x in self._names] - self.image = self._anim_seq[0] - self.rect = Rect(self._pos, self.image.get_size()) - self.interact_rect = self.rect - - def animate(self): - if self._anim_seq: - self._frame_count += 1 - if self._frame_count > self._delay: - self._frame_count = 0 - self._anim_pos += 1 - if self._anim_pos >= len(self._anim_seq): - self._anim_pos = 0 - self.image = self._anim_seq[self._anim_pos] - # queue redraw - return True - return False - - -class GenericDescThing(Thing): - "Thing with an InteractiveUnionRect and a description" - - INITIAL = "description" - - def __init__(self, prefix, number, description, areas): - super(GenericDescThing, self).__init__() - self.description = description - self.name = '%s.%s' % (prefix, number) - self.interacts = { - 'description': InteractRectUnion(areas) - } - if DEBUG: - # Individual colors to make debugging easier - self._interact_hilight_color = Color(THECOLORS.keys()[number]) - - def get_description(self): - return self.description - - def is_interactive(self, tool=None): - return False diff -r 33ce7ff757c3 -r ded4324b236e gamelib/sound.py --- a/gamelib/sound.py Sat Feb 11 12:52:29 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,75 +0,0 @@ -# Sound management for Suspended Sentence - -# This re-implements some of the albow.resource code to -# a) work around an annoying bugs -# b) add some missing functionality (disable_sound) - -import os - -import pygame -from pygame.mixer import music -from albow.resource import _resource_path, dummy_sound -import albow.music - -sound_cache = {} - - -def get_sound(*names): - if sound_cache is None: - return dummy_sound - path = _resource_path("sounds", names) - sound = sound_cache.get(path) - if not sound: - if not os.path.isfile(path): - missing_sound("File does not exist", path) - return dummy_sound - try: - from pygame.mixer import Sound - except ImportError, e: - no_sound(e) - return dummy_sound - try: - sound = Sound(path) - except pygame.error, e: - missing_sound(e, path) - return dummy_sound - sound_cache[path] = sound - return sound - - -def no_sound(e): - global sound_cache - print "get_sound: %s" % e - print "get_sound: Sound not available, continuing without it" - sound_cache = None - albow.music.music_enabled = False - - -def disable_sound(): - global sound_cache - sound_cache = None - albow.music.music_enabled = False - - -def missing_sound(e, name): - print "albow.resource.get_sound: %s: %s" % (name, e) - - -def start_next_music(): - """Start playing the next item from the current playlist immediately.""" - if albow.music.music_enabled and albow.music.current_playlist: - next_music = albow.music.current_playlist.next() - if next_music: - #print "albow.music: loading", repr(next_music) - music.load(next_music) - music.play() - albow.music.next_change_delay = albow.music.change_delay - albow.music.current_music = next_music - - -def get_current_playlist(): - if albow.music.music_enabled and albow.music.current_playlist: - return albow.music.current_playlist - -# Monkey patch -albow.music.start_next_music = start_next_music diff -r 33ce7ff757c3 -r ded4324b236e gamelib/speech.py --- a/gamelib/speech.py Sat Feb 11 12:52:29 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,44 +0,0 @@ -# speech.py -# Copyright Boomslang team, 2010 (see COPYING File) -# Speech playing and cache - -import re - -from sound import get_sound - - -# cache of string -> sound object mappings -_SPEECH_CACHE = {} - -# characters not to allow in filenames -_REPLACE_RE = re.compile(r"[^a-z0-9-]+") - - -class SpeechError(RuntimeError): - pass - - -def get_filename(key, text): - """Simplify text to filename.""" - filename = "%s-%s" % (key, text) - filename = filename.lower() - filename = _REPLACE_RE.sub("_", filename) - filename = filename[:30] - filename = "%s.ogg" % filename - return filename - - -def get_speech(thing_name, text): - """Load a sound object from the cache.""" - key = (thing_name, text) - if key in _SPEECH_CACHE: - return _SPEECH_CACHE[key] - filename = get_filename(thing_name, text) - _SPEECH_CACHE[key] = sound = get_sound("speech", filename) - return sound - - -def say(thing_name, text): - """Play text as speech.""" - sound = get_speech(thing_name, text) - sound.play() diff -r 33ce7ff757c3 -r ded4324b236e gamelib/state.py --- a/gamelib/state.py Sat Feb 11 12:52:29 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,548 +0,0 @@ -"""Utilities and base classes for dealing with scenes.""" - -import copy - -from albow.resource import get_image -from albow.utils import frame_rect -from widgets import BoomLabel -from pygame.rect import Rect -from pygame.color import Color - -import constants -from scenes import SCENE_LIST, INITIAL_SCENE -from sound import get_sound - -# override the initial scene to for debugging -DEBUG_SCENE = None - -# whether to show debugging rects -DEBUG_RECTS = False - - -class Result(object): - """Result of interacting with a thing""" - - def __init__(self, message=None, soundfile=None, detail_view=None, - style=None, close_detail=False, end_game=False): - self.message = message - self.sound = None - if soundfile: - self.sound = get_sound(soundfile) - self.detail_view = detail_view - self.style = style - self.close_detail = close_detail - self.end_game = end_game - - def process(self, scene_widget): - """Helper function to do the right thing with a result object""" - if self.sound: - self.sound.play() - if self.message: - scene_widget.show_message(self.message, self.style) - if self.detail_view: - scene_widget.show_detail(self.detail_view) - if (self.close_detail - and hasattr(scene_widget, 'parent') - and hasattr(scene_widget.parent, 'clear_detail')): - scene_widget.parent.clear_detail() - if self.end_game: - scene_widget.end_game() - - -def handle_result(result, scene_widget): - """Handle dealing with result or result sequences""" - if result: - if hasattr(result, 'process'): - result.process(scene_widget) - else: - for res in result: - if res: - # List may contain None's - res.process(scene_widget) - - -def initial_state(): - """Load the initial state.""" - state = GameState() - for scene in SCENE_LIST: - state.load_scenes(scene) - initial_scene = INITIAL_SCENE if DEBUG_SCENE is None else DEBUG_SCENE - state.set_current_scene(initial_scene) - state.set_do_enter_leave() - return state - - -class GameState(object): - """Complete game state. - - Game state consists of: - - * items - * scenes - """ - - def __init__(self): - # map of scene name -> Scene object - self.scenes = {} - # map of detail view name -> DetailView object - self.detail_views = {} - # map of item name -> Item object - self.items = {} - # list of item objects in inventory - self.inventory = [] - # currently selected tool (item) - self.tool = None - # current scene - self.current_scene = None - # current detail view - self.current_detail = None - # scene we came from, for enter and leave processing - self.previous_scene = None - # scene transion helpers - self.do_check = None - self.old_pos = None - # current thing - self.current_thing = None - self.highlight_override = False - - def add_scene(self, scene): - self.scenes[scene.name] = scene - - def add_detail_view(self, detail_view): - self.detail_views[detail_view.name] = detail_view - - def add_item(self, item): - self.items[item.name] = item - item.set_state(self) - - def load_scenes(self, modname): - mod = __import__("gamelib.scenes.%s" % (modname,), fromlist=[modname]) - for scene_cls in mod.SCENES: - self.add_scene(scene_cls(self)) - if hasattr(mod, 'DETAIL_VIEWS'): - for scene_cls in mod.DETAIL_VIEWS: - self.add_detail_view(scene_cls(self)) - - def set_current_scene(self, name): - old_scene = self.current_scene - self.current_scene = self.scenes[name] - self.current_thing = None - if old_scene and old_scene != self.current_scene: - self.previous_scene = old_scene - self.set_do_enter_leave() - - def set_current_detail(self, name): - self.current_thing = None - if name is None: - self.current_detail = None - else: - self.current_detail = self.detail_views[name] - return self.current_detail - - def add_inventory_item(self, name): - self.inventory.append(self.items[name]) - - def is_in_inventory(self, name): - if name in self.items: - return self.items[name] in self.inventory - return False - - def remove_inventory_item(self, name): - self.inventory.remove(self.items[name]) - # Unselect tool if it's removed - if self.tool == self.items[name]: - self.set_tool(None) - - def replace_inventory_item(self, old_item_name, new_item_name): - """Try to replace an item in the inventory with a new one""" - try: - index = self.inventory.index(self.items[old_item_name]) - self.inventory[index] = self.items[new_item_name] - if self.tool == self.items[old_item_name]: - self.set_tool(self.items[new_item_name]) - except ValueError: - return False - return True - - def set_tool(self, item): - self.tool = item - - def interact(self, pos): - return self.current_scene.interact(self.tool, pos) - - def interact_detail(self, pos): - return self.current_detail.interact(self.tool, pos) - - def cancel_doodah(self, screen): - if self.tool: - self.set_tool(None) - elif self.current_detail: - screen.state_widget.clear_detail() - - def do_enter_detail(self): - if self.current_detail: - self.current_detail.enter() - - def do_leave_detail(self): - if self.current_detail: - self.current_detail.leave() - - def animate(self): - if not self.do_check: - return self.current_scene.animate() - - def check_enter_leave(self, screen): - if not self.do_check: - return None - if self.do_check == constants.LEAVE: - self.do_check = constants.ENTER - if self.previous_scene: - return self.previous_scene.leave() - return None - elif self.do_check == constants.ENTER: - self.do_check = None - # Fix descriptions, etc. - if self.old_pos: - self.current_scene.update_current_thing(self.old_pos) - return self.current_scene.enter() - raise RuntimeError('invalid do_check value %s' % self.do_check) - - def set_do_enter_leave(self): - """Flag that we need to run the enter loop""" - self.do_check = constants.LEAVE - - -class StatefulGizmo(object): - - # initial data (optional, defaults to none) - INITIAL_DATA = None - - def __init__(self): - self.data = {} - if 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 - - def get_data(self, key): - return self.data.get(key, None) - - -class Scene(StatefulGizmo): - """Base class for scenes.""" - - # sub-folder to look for resources in - FOLDER = None - - # name of background image resource - BACKGROUND = None - - # name of scene (optional, defaults to folder) - NAME = None - - # Offset of the background image - OFFSET = (0, 0) - - def __init__(self, state): - StatefulGizmo.__init__(self) - # scene name - self.name = self.NAME if self.NAME is not None else self.FOLDER - # link back to state object - self.state = state - # map of thing names -> Thing objects - self.things = {} - self._background = None - - def add_item(self, item): - self.state.add_item(item) - - def add_thing(self, thing): - self.things[thing.name] = thing - thing.set_scene(self) - - def remove_thing(self, thing): - del self.things[thing.name] - if thing is self.state.current_thing: - self.state.current_thing.leave() - self.state.current_thing = None - - def _get_description(self): - text = (self.state.current_thing and - self.state.current_thing.get_description()) - if text is None: - return None - label = BoomLabel(text) - label.set_margin(5) - label.border_width = 1 - label.border_color = (0, 0, 0) - label.bg_color = Color(210, 210, 210, 255) - label.fg_color = (0, 0, 0) - return label - - def draw_description(self, surface, screen): - description = self._get_description() - if description is not None: - w, h = description.size - sub = screen.get_root().surface.subsurface( - Rect(400 - w / 2, 5, w, h)) - description.draw_all(sub) - - def _cache_background(self): - if self.BACKGROUND and not self._background: - self._background = get_image(self.FOLDER, self.BACKGROUND) - - def draw_background(self, surface): - self._cache_background() - if self._background is not None: - surface.blit(self._background, self.OFFSET, None) - else: - surface.fill((200, 200, 200)) - - def draw_things(self, surface): - for thing in self.things.itervalues(): - thing.draw(surface) - - def draw(self, surface, screen): - self.draw_background(surface) - self.draw_things(surface) - self.draw_description(surface, screen) - - def interact(self, item, pos): - """Interact with a particular position. - - Item may be an item in the list of items or None for the hand. - - Returns a Result object to provide feedback to the player. - """ - if self.state.current_thing is not None: - return self.state.current_thing.interact(item) - - def animate(self): - """Animate all the things in the scene. - - Return true if any of them need to queue a redraw""" - result = False - for thing in self.things.itervalues(): - if thing.animate(): - result = True - return result - - def enter(self): - return None - - def leave(self): - return None - - def update_current_thing(self, pos): - if self.state.current_thing is not None: - if not self.state.current_thing.contains(pos): - self.state.current_thing.leave() - self.state.current_thing = None - for thing in self.things.itervalues(): - if thing.contains(pos): - thing.enter(self.state.tool) - self.state.current_thing = thing - break - - def mouse_move(self, pos): - """Call to check whether the cursor has entered / exited a thing. - - Item may be an item in the list of items or None for the hand. - """ - self.update_current_thing(pos) - - def get_detail_size(self): - self._cache_background() - return self._background.get_size() - - -class InteractiveMixin(object): - def is_interactive(self, tool=None): - return True - - def interact(self, tool): - if not self.is_interactive(tool): - return None - if tool is None: - return self.interact_without() - handler = getattr(self, 'interact_with_' + tool.tool_name, None) - inverse_handler = self.get_inverse_interact(tool) - if handler is not None: - return handler(tool) - elif inverse_handler is not None: - return inverse_handler(self) - else: - return self.interact_default(tool) - - def get_inverse_interact(self, tool): - return None - - def interact_without(self): - return self.interact_default(None) - - def interact_default(self, item=None): - return None - - -class Thing(StatefulGizmo, InteractiveMixin): - """Base class for things in a scene that you can interact with.""" - - # name of thing - NAME = None - - # sub-folder to look for resources in (defaults to scenes folder) - FOLDER = None - - # list of Interact objects - INTERACTS = {} - - # name first interact - INITIAL = None - - # Interact rectangle hi-light color (for debugging) - # (set to None to turn off) - _interact_hilight_color = Color('red') - - def __init__(self): - StatefulGizmo.__init__(self) - # name of the thing - self.name = self.NAME - # folder for resource (None is overridden by scene folder) - self.folder = self.FOLDER - # interacts - self.interacts = self.INTERACTS - # these are set by set_scene - self.scene = None - self.state = None - self.current_interact = None - self.rect = None - self.orig_rect = None - - def _fix_rect(self): - """Fix rects to compensate for scene offset""" - # Offset logic is to always work with copies, to avoid - # flying effects from multiple calls to _fix_rect - # See footwork in draw - if hasattr(self.rect, 'collidepoint'): - self.rect = self.rect.move(self.scene.OFFSET) - else: - self.rect = [x.move(self.scene.OFFSET) for x in self.rect] - - def set_scene(self, scene): - assert self.scene is None - self.scene = scene - if self.folder is None: - self.folder = scene.FOLDER - self.state = scene.state - for interact in self.interacts.itervalues(): - interact.set_thing(self) - self.set_interact(self.INITIAL) - - def set_interact(self, name): - self.current_interact = self.interacts[name] - self.rect = self.current_interact.interact_rect - if self.scene: - self._fix_rect() - assert self.rect is not None, name - - def contains(self, pos): - if hasattr(self.rect, 'collidepoint'): - return self.rect.collidepoint(pos) - else: - # FIXME: add sanity check - for rect in list(self.rect): - if rect.collidepoint(pos): - return True - return False - - def get_description(self): - return None - - def enter(self, item): - """Called when the cursor enters the Thing.""" - pass - - def leave(self): - """Called when the cursr leaves the Thing.""" - pass - - def animate(self): - return self.current_interact.animate() - - def draw(self, surface): - old_rect = self.current_interact.rect - if old_rect: - self.current_interact.rect = old_rect.move(self.scene.OFFSET) - self.current_interact.draw(surface) - self.current_interact.rect = old_rect - if DEBUG_RECTS and self._interact_hilight_color: - if hasattr(self.rect, 'collidepoint'): - frame_rect(surface, self._interact_hilight_color, - self.rect.inflate(1, 1), 1) - else: - for rect in self.rect: - frame_rect(surface, self._interact_hilight_color, - rect.inflate(1, 1), 1) - - -class Item(InteractiveMixin): - """Base class for inventory items.""" - - # image for inventory - INVENTORY_IMAGE = None - - # name of item - NAME = None - - # name for interactions (i.e. def interact_with_) - TOOL_NAME = None - - # set to instance of CursorSprite - CURSOR = None - - def __init__(self, name=None): - self.state = None - self.name = self.NAME - if name is not None: - self.name = name - self.tool_name = name - if self.TOOL_NAME is not None: - self.tool_name = self.TOOL_NAME - self.inventory_image = None - - def _cache_inventory_image(self): - if not self.inventory_image: - self.inventory_image = get_image('items', self.INVENTORY_IMAGE) - - def set_state(self, state): - assert self.state is None - self.state = state - - def get_inventory_image(self): - self._cache_inventory_image() - return self.inventory_image - - def get_inverse_interact(self, tool): - return getattr(tool, 'interact_with_' + self.tool_name, None) - - def is_interactive(self, tool=None): - if tool: - return True - return False - - -class CloneableItem(Item): - _counter = 0 - - @classmethod - def _get_new_id(cls): - cls._counter += 1 - return cls._counter - 1 - - def __init__(self, name=None): - super(CloneableItem, self).__init__(name) - my_count = self._get_new_id() - self.name = "%s.%s" % (self.name, my_count) diff -r 33ce7ff757c3 -r ded4324b236e gamelib/widgets.py --- a/gamelib/widgets.py Sat Feb 11 12:52:29 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,215 +0,0 @@ -# widgets.py -# Copyright Boomslang team, 2010 (see COPYING File) - -"""Custom Albow widgets""" - -import textwrap - -import albow.controls -import albow.menu -from albow.resource import get_font, get_image -from pygame.color import Color -from pygame.rect import Rect -from pygame.draw import lines as draw_lines -from pygame import mouse - -from constants import BUTTON_SIZE -from cursor import CursorWidget - - -class BoomLabel(albow.controls.Label): - - trim_line_top = 0 - - def __init__(self, text, width=None, **kwds): - albow.controls.Label.__init__(self, text, width, **kwds) - w, h = self.size - h -= self.trim_line_top * len(self.text.split('\n')) - self.size = (w, h) - - def set_margin(self, margin): - """Add a set_margin method that recalculates the label size""" - old_margin = self.margin - w, h = self.size - d = margin - old_margin - self.margin = margin - self.size = (w + 2 * d, h + 2 * d) - - def draw_all(self, surface): - bg_color = self.bg_color - self.bg_color = None - if bg_color is not None: - new_surface = surface.convert_alpha() - new_surface.fill(bg_color) - surface.blit(new_surface, surface.get_rect()) - albow.controls.Label.draw_all(self, surface) - self._draw_all_no_bg(surface) - self.bg_color = bg_color - - def _draw_all_no_bg(self, surface): - pass - - def draw_with(self, surface, fg, _bg=None): - m = self.margin - align = self.align - width = surface.get_width() - y = m - lines = self.text.split("\n") - font = self.font - dy = font.get_linesize() - self.trim_line_top - for line in lines: - image = font.render(line, True, fg) - r = image.get_rect() - image = image.subsurface(r.clip(r.move(0, self.trim_line_top))) - r.top = y - if align == 'l': - r.left = m - elif align == 'r': - r.right = width - m - else: - r.centerx = width // 2 - surface.blit(image, r) - y += dy - - -class BoomButton(BoomLabel): - - def __init__(self, text, action, screen): - super(BoomButton, self).__init__(text, font=get_font(20, 'Vera.ttf'), - margin=4) - self.bg_color = (0, 0, 0) - self._frame_color = Color(50, 50, 50) - self.action = action - self.screen = screen - - def mouse_down(self, event): - self.action() - self.screen.state_widget.mouse_move(event) - - def mouse_move(self, event): - self.screen.state.highlight_override = True - - def draw(self, surface): - super(BoomButton, self).draw(surface) - r = surface.get_rect() - w = 2 - top, bottom, left, right = r.top, r.bottom, r.left, r.right - draw_lines(surface, self._frame_color, False, [ - (left, bottom), (left, top), (right - w, top), (right - w, bottom) - ], w) - - -class MessageDialog(BoomLabel, CursorWidget): - - def __init__(self, screen, text, wrap_width, style=None, **kwds): - CursorWidget.__init__(self, screen) - self.set_style(style) - paras = text.split("\n\n") - text = "\n".join([textwrap.fill(para, wrap_width) for para in paras]) - BoomLabel.__init__(self, text, **kwds) - - def set_style(self, style): - self.set_margin(5) - self.border_width = 1 - self.border_color = (0, 0, 0) - self.bg_color = (127, 127, 127) - self.fg_color = (0, 0, 0) - if style == "JIM": - self.set(font=get_font(20, "Monospace.ttf")) - self.trim_line_top = 10 - self.bg_color = Color(255, 175, 127, 191) - self.fg_color = (0, 0, 0) - self.border_color = (127, 15, 0) - - def draw_all(self, surface): - root_surface = self.get_root().surface - overlay = root_surface.convert_alpha() - overlay.fill(Color(0, 0, 0, 191)) - root_surface.blit(overlay, (0, 0)) - BoomLabel.draw_all(self, surface) - - def _draw_all_no_bg(self, surface): - CursorWidget.draw_all(self, surface) - - def mouse_down(self, event): - self.dismiss() - self.screen.state_widget._mouse_move(mouse.get_pos()) - for widget in self.screen.state_widget.subwidgets: - widget._mouse_move(mouse.get_pos()) - - def cursor_highlight(self): - return False - - -class HandButton(albow.controls.Image): - """The fancy hand button for the widget""" - - def __init__(self, action): - # FIXME: Yes, please. - this_image = get_image('items', 'hand.png') - albow.controls.Image.__init__(self, image=this_image) - self.action = action - self.set_rect(Rect(0, 0, BUTTON_SIZE, BUTTON_SIZE)) - - def mouse_down(self, event): - self.action() - - -class PopupMenuButton(albow.controls.Button): - - def __init__(self, text, action): - albow.controls.Button.__init__(self, text, action) - - self.font = get_font(16, 'Vera.ttf') - self.set_rect(Rect(0, 0, BUTTON_SIZE, BUTTON_SIZE)) - self.margin = (BUTTON_SIZE - self.font.get_linesize()) / 2 - - -class PopupMenu(albow.menu.Menu, CursorWidget): - - def __init__(self, screen): - CursorWidget.__init__(self, screen) - self.screen = screen - self.shell = screen.shell - items = [ - ('Quit Game', 'quit'), - ('Exit to Main Menu', 'main_menu'), - ] - # albow.menu.Menu ignores title string - albow.menu.Menu.__init__(self, None, items) - self.font = get_font(16, 'Vera.ttf') - - def show_menu(self): - """Call present, with the correct position""" - item_height = self.font.get_linesize() - menu_top = 600 - (len(self.items) * item_height + BUTTON_SIZE) - item = self.present(self.shell, (0, menu_top)) - if item > -1: - # A menu item needs to be invoked - self.invoke_item(item) - - -class BoomImageButton(albow.controls.Image): - """The fancy image button for the screens""" - - FOLDER = None - - def __init__(self, filename, x, y, action, enable=None): - this_image = get_image(self.FOLDER, filename) - albow.controls.Image.__init__(self, image=this_image) - self.action = action - self.set_rect(Rect((x, y), this_image.get_size())) - self.enable = enable - - def draw(self, surface): - if self.is_enabled(): - surface.blit(self.get_image(), self.get_rect()) - - def mouse_down(self, event): - if self.is_enabled(): - self.action() - - def is_enabled(self): - if self.enable: - return self.enable() - return True diff -r 33ce7ff757c3 -r ded4324b236e pyntnclick/constants.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pyntnclick/constants.py Sat Feb 11 13:10:18 2012 +0200 @@ -0,0 +1,18 @@ +# Useful constants +# copyright boomslang team (see COPYRIGHT file for details) + + +SCREEN = (800, 600) +FREQ = 44100 # same as audio CD +BITSIZE = -16 # unsigned 16 bit +CHANNELS = 2 # 1 == mono, 2 == stereo +BUFFER = 1024 # audio buffer size in no. of samples + +BUTTON_SIZE = 50 +SCENE_SIZE = (SCREEN[0], SCREEN[1] - BUTTON_SIZE) +# Animation frame rate +FRAME_RATE = 25 + +DEBUG = False + +ENTER, LEAVE = 1, 2 diff -r 33ce7ff757c3 -r ded4324b236e pyntnclick/cursor.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pyntnclick/cursor.py Sat Feb 11 13:10:18 2012 +0200 @@ -0,0 +1,116 @@ +# cursor.py +# Copyright Boomslang team, 2010 (see COPYING File) +# Sprite Cursor + +from albow.resource import get_image +from albow.widget import Widget +from pygame.sprite import Sprite, RenderUpdates +from pygame.rect import Rect +import pygame +import pygame.color +import pygame.cursors +import pygame.mouse + +from gamelib.constants import SCENE_SIZE + + +class CursorSprite(Sprite): + "A Sprite that follows the Cursor" + + def __init__(self, filename, x=None, y=None): + Sprite.__init__(self) + self.filename = filename + self.pointer_x = x + self.pointer_y = y + self.highlighted = False + + def load(self): + if not hasattr(self, 'plain_image'): + self.plain_image = get_image('items', self.filename) + self.image = self.plain_image + self.rect = self.image.get_rect() + + if self.pointer_x is None: + self.pointer_x = self.rect.size[0] // 2 + if self.pointer_y is None: + self.pointer_y = self.rect.size[1] // 2 + + self.highlight = pygame.Surface(self.rect.size) + color = pygame.color.Color(255, 100, 100, 0) + self.highlight.fill(color) + + def update(self): + pos = pygame.mouse.get_pos() + self.rect.left = pos[0] - self.pointer_x + self.rect.top = pos[1] - self.pointer_y + + def set_highlight(self, enable): + if enable != self.highlighted: + self.load() + self.highlighted = enable + self.image = self.plain_image.copy() + if enable: + self.image.blit(self.highlight, self.highlight.get_rect(), + None, pygame.BLEND_MULT) + + +HAND = CursorSprite('hand.png', 12, 0) + + +class CursorWidget(Widget): + """Mix-in widget to ensure that mouse_move is propogated to parents""" + + cursor = HAND + _cursor_group = RenderUpdates() + _loaded_cursor = None + + def __init__(self, screen, *args, **kwargs): + Widget.__init__(self, *args, **kwargs) + self.screen = screen + + def enter_screen(self): + pygame.mouse.set_visible(0) + + def leave_screen(self): + pygame.mouse.set_visible(1) + + def draw_all(self, surface): + Widget.draw_all(self, surface) + self.draw_cursor(self.get_root().surface) + + def draw_cursor(self, surface): + self.set_cursor(self.screen.state.tool) + self.cursor.set_highlight(self.cursor_highlight()) + if self.cursor is not None: + self._cursor_group.update() + self._cursor_group.draw(surface) + + def mouse_delta(self, event): + self.invalidate() + + @classmethod + def set_cursor(cls, item): + if item is None or item.CURSOR is None: + cls.cursor = HAND + else: + cls.cursor = item.CURSOR + if cls.cursor != cls._loaded_cursor: + cls._loaded_cursor = cls.cursor + if cls.cursor is None: + pygame.mouse.set_visible(1) + cls._cursor_group.empty() + else: + pygame.mouse.set_visible(0) + cls.cursor.load() + cls._cursor_group.empty() + cls._cursor_group.add(cls.cursor) + + def cursor_highlight(self): + if not Rect((0, 0), SCENE_SIZE).collidepoint(pygame.mouse.get_pos()): + return False + if self.screen.state.highlight_override: + return True + current_thing = self.screen.state.current_thing + if current_thing: + return current_thing.is_interactive() + return False diff -r 33ce7ff757c3 -r ded4324b236e pyntnclick/data.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pyntnclick/data.py Sat Feb 11 13:10:18 2012 +0200 @@ -0,0 +1,25 @@ +'''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, '..', 'Resources')) + + +def filepath(filename): + '''Determine the path to a file in the data directory. + ''' + return os.path.join(data_dir, filename) + + +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 -r 33ce7ff757c3 -r ded4324b236e pyntnclick/endscreen.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pyntnclick/endscreen.py Sat Feb 11 13:10:18 2012 +0200 @@ -0,0 +1,33 @@ +# endscreen.py +# Copyright Boomslang team, 2010 (see COPYING File) +# Victory screen for the game + +from albow.screen import Screen +from albow.resource import get_image + +from gamelib.widgets import BoomImageButton + + +class EndImageButton(BoomImageButton): + + FOLDER = 'won' + + +class EndScreen(Screen): + def __init__(self, shell): + Screen.__init__(self, shell) + self.background = get_image('won', 'won.png') + self._menu_button = EndImageButton('menu.png', 26, 500, + action=self.main_menu) + self._quit_button = EndImageButton('quit.png', 250, 500, + action=shell.quit) + self.add(self._menu_button) + self.add(self._quit_button) + + def draw(self, surface): + surface.blit(self.background, (0, 0)) + self._menu_button.draw(surface) + self._quit_button.draw(surface) + + def main_menu(self): + self.shell.show_screen(self.shell.menu_screen) diff -r 33ce7ff757c3 -r ded4324b236e pyntnclick/gamescreen.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pyntnclick/gamescreen.py Sat Feb 11 13:10:18 2012 +0200 @@ -0,0 +1,261 @@ +# gamescreen.py +# Copyright Boomslang team, 2010 (see COPYING File) +# Main menu for the game + +from albow.controls import Widget +from albow.layout import Row +from albow.palette_view import PaletteView +from albow.screen import Screen +from pygame import Rect, mouse +from pygame.color import Color + +from constants import SCREEN, BUTTON_SIZE, SCENE_SIZE, LEAVE +from cursor import CursorWidget +from state import initial_state, handle_result +from widgets import (MessageDialog, BoomButton, HandButton, PopupMenu, + PopupMenuButton) + + +class InventoryView(PaletteView): + + sel_color = Color("yellow") + sel_width = 2 + + def __init__(self, screen): + PaletteView.__init__(self, (BUTTON_SIZE, BUTTON_SIZE), 1, 6, + scrolling=True) + self.screen = screen + self.state = screen.state + self.state_widget = screen.state_widget + + def num_items(self): + return len(self.state.inventory) + + def draw_item(self, surface, item_no, rect): + item_image = self.state.inventory[item_no].get_inventory_image() + surface.blit(item_image, rect, None) + + def click_item(self, item_no, event): + item = self.state.inventory[item_no] + if self.item_is_selected(item_no): + self.unselect() + elif item.is_interactive(self.state.tool): + result = item.interact(self.state.tool) + handle_result(result, self.state_widget) + else: + self.state.set_tool(self.state.inventory[item_no]) + + def mouse_down(self, event): + if event.button != 1: + self.state.cancel_doodah(self.screen) + else: + PaletteView.mouse_down(self, event) + + def item_is_selected(self, item_no): + return self.state.tool is self.state.inventory[item_no] + + def unselect(self): + self.state.set_tool(None) + + +class StateWidget(Widget): + + def __init__(self, screen): + Widget.__init__(self, Rect(0, 0, SCENE_SIZE[0], SCENE_SIZE[1])) + self.screen = screen + self.state = screen.state + self.detail = DetailWindow(screen) + + def draw(self, surface): + if self.state.previous_scene and self.state.do_check == LEAVE: + # We still need to handle leave events, so still display the scene + self.state.previous_scene.draw(surface, self) + else: + self.state.current_scene.draw(surface, self) + + def mouse_down(self, event): + self.mouse_move(event) + if event.button != 1: # We have a right/middle click + self.state.cancel_doodah(self.screen) + elif self.subwidgets: + self.clear_detail() + self._mouse_move(event.pos) + else: + result = self.state.interact(event.pos) + handle_result(result, self) + + def animate(self): + if self.state.animate(): + # queue a redraw + self.invalidate() + # We do this here so we can get enter and leave events regardless + # of what happens + result = self.state.check_enter_leave(self.screen) + handle_result(result, self) + + def mouse_move(self, event): + self.state.highlight_override = False + if not self.subwidgets: + self._mouse_move(event.pos) + + def _mouse_move(self, pos): + self.state.highlight_override = False + self.state.current_scene.mouse_move(pos) + self.state.old_pos = pos + + def show_message(self, message, style=None): + # Display the message as a modal dialog + MessageDialog(self.screen, message, 60, style=style).present() + # queue a redraw to show updated state + self.invalidate() + # The cursor could have gone anywhere + if self.subwidgets: + self.subwidgets[0]._mouse_move(mouse.get_pos()) + else: + self._mouse_move(mouse.get_pos()) + + def show_detail(self, detail): + self.clear_detail() + detail_obj = self.state.set_current_detail(detail) + self.detail.set_image_rect(Rect((0, 0), detail_obj.get_detail_size())) + self.add_centered(self.detail) + self.state.do_enter_detail() + + def clear_detail(self): + """Hide the detail view""" + if self.state.current_detail is not None: + self.remove(self.detail) + self.state.do_leave_detail() + self.state.set_current_detail(None) + self._mouse_move(mouse.get_pos()) + + def end_game(self): + self.screen.running = False + self.screen.shell.show_screen(self.screen.shell.end_screen) + + +class DetailWindow(Widget): + def __init__(self, screen): + Widget.__init__(self) + self.image_rect = None + self.screen = screen + self.state = screen.state + self.border_width = 5 + self.border_color = (0, 0, 0) + # parent only gets set when we get added to the scene + self.close = BoomButton('Close', self.close_but, screen) + self.add(self.close) + + def close_but(self): + self.parent.clear_detail() + + def end_game(self): + self.parent.end_game() + + def set_image_rect(self, rect): + bw = self.border_width + self.image_rect = rect + self.image_rect.topleft = (bw, bw) + self.set_rect(rect.inflate(bw * 2, bw * 2)) + self.close.rect.midbottom = rect.midbottom + + def draw(self, surface): + scene_surface = self.get_root().surface.subsurface(self.parent.rect) + overlay = scene_surface.convert_alpha() + overlay.fill(Color(0, 0, 0, 191)) + scene_surface.blit(overlay, (0, 0)) + self.state.current_detail.draw( + surface.subsurface(self.image_rect), self) + + def mouse_down(self, event): + self.mouse_move(event) + if event.button != 1: # We have a right/middle click + self.state.cancel_doodah(self.screen) + else: + result = self.state.interact_detail( + self.global_to_local(event.pos)) + handle_result(result, self) + + def mouse_move(self, event): + self._mouse_move(event.pos) + + def _mouse_move(self, pos): + self.state.highlight_override = False + self.state.current_detail.mouse_move(self.global_to_local(pos)) + + def show_message(self, message, style=None): + self.parent.show_message(message, style) + self.invalidate() + + +class ToolBar(Row): + def __init__(self, items): + for item in items: + item.height = BUTTON_SIZE + Row.__init__(self, items, spacing=0, width=SCREEN[0]) + self.bg_color = (31, 31, 31) + + def draw(self, surface): + surface.fill(self.bg_color) + Row.draw(self, surface) + + +class GameScreen(Screen, CursorWidget): + def __init__(self, shell): + CursorWidget.__init__(self, self) + Screen.__init__(self, shell) + self.running = False + + def _clear_all(self): + for widget in self.subwidgets[:]: + self.remove(widget) + + def start_game(self): + self._clear_all() + self.state = initial_state() + self.state_widget = StateWidget(self) + self.add(self.state_widget) + + self.popup_menu = PopupMenu(self) + self.menubutton = PopupMenuButton('Menu', + action=self.popup_menu.show_menu) + + self.handbutton = HandButton(action=self.hand_pressed) + + self.inventory = InventoryView(self) + + self.toolbar = ToolBar([ + self.menubutton, + self.handbutton, + self.inventory, + ]) + self.toolbar.bottomleft = self.bottomleft + self.add(self.toolbar) + + self.running = True + + def enter_screen(self): + CursorWidget.enter_screen(self) + + def leave_screen(self): + CursorWidget.leave_screen(self) + + # Albow uses magic method names (command + '_cmd'). Yay. + # Albow's search order means they need to be defined here, not in + # PopMenu, which is annoying. + def hide_cmd(self): + # This option does nothing, but the method needs to exist for albow + return + + def main_menu_cmd(self): + self.shell.show_screen(self.shell.menu_screen) + + def quit_cmd(self): + self.shell.quit() + + def hand_pressed(self): + self.inventory.unselect() + + def begin_frame(self): + if self.running: + self.state_widget.animate() diff -r 33ce7ff757c3 -r ded4324b236e pyntnclick/main.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pyntnclick/main.py Sat Feb 11 13:10:18 2012 +0200 @@ -0,0 +1,77 @@ +'''Game main module. + +Contains the entry point used by the run_game.py script. + +''' + +# Albow looks for stuff in os.path[0], which isn't always where it expects. +# The following horribleness fixes this. +import sys +import os.path +right_path = os.path.dirname(os.path.dirname(__file__)) +sys.path.insert(0, right_path) +from optparse import OptionParser + +import pygame +from pygame.locals import SWSURFACE +from albow.shell import Shell + +from menu import MenuScreen +from gamescreen import GameScreen +from endscreen import EndScreen +from constants import ( + SCREEN, FRAME_RATE, FREQ, BITSIZE, CHANNELS, BUFFER, DEBUG) +from sound import no_sound, disable_sound +import state +import data + + +def parse_args(args): + parser = OptionParser() + parser.add_option("--no-sound", action="store_false", default=True, + dest="sound", help="disable sound") + if DEBUG: + parser.add_option("--scene", type="str", default=None, + dest="scene", help="initial scene") + parser.add_option("--no-rects", action="store_false", default=True, + dest="rects", help="disable debugging rects") + opts, _ = parser.parse_args(args or []) + return opts + + +class MainShell(Shell): + def __init__(self, display): + Shell.__init__(self, display) + self.menu_screen = MenuScreen(self) + self.game_screen = GameScreen(self) + self.end_screen = EndScreen(self) + self.set_timer(FRAME_RATE) + self.show_screen(self.menu_screen) + + +def main(): + opts = parse_args(sys.argv) + pygame.display.init() + pygame.font.init() + if opts.sound: + try: + pygame.mixer.init(FREQ, BITSIZE, CHANNELS, BUFFER) + except pygame.error, exc: + no_sound(exc) + else: + # Ensure get_sound returns nothing, so everything else just works + disable_sound() + if DEBUG: + if opts.scene is not None: + # debug the specified scene + state.DEBUG_SCENE = opts.scene + state.DEBUG_RECTS = opts.rects + display = pygame.display.set_mode(SCREEN, SWSURFACE) + pygame.display.set_icon(pygame.image.load( + data.filepath('icons/suspended_sentence24x24.png'))) + pygame.display.set_caption("Suspended Sentence") + shell = MainShell(display) + try: + shell.run() + except KeyboardInterrupt: + pass diff -r 33ce7ff757c3 -r ded4324b236e pyntnclick/menu.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pyntnclick/menu.py Sat Feb 11 13:10:18 2012 +0200 @@ -0,0 +1,43 @@ +# menu.py +# Copyright Boomslang team, 2010 (see COPYING File) +# Main menu for the game + +from albow.screen import Screen +from albow.resource import get_image + +from gamelib.widgets import BoomImageButton + + +class SplashButton(BoomImageButton): + + FOLDER = 'splash' + + +class MenuScreen(Screen): + def __init__(self, shell): + Screen.__init__(self, shell) + self._background = get_image('splash', 'splash.png') + self._start_button = SplashButton('play.png', 16, 523, self.start) + self._resume_button = SplashButton('resume.png', 256, 523, self.resume, + enable=self.check_running) + self._quit_button = SplashButton('quit.png', 580, 523, shell.quit) + self.add(self._start_button) + self.add(self._resume_button) + self.add(self._quit_button) + + def draw(self, surface): + surface.blit(self._background, (0, 0)) + self._start_button.draw(surface) + self._resume_button.draw(surface) + self._quit_button.draw(surface) + + def start(self): + self.shell.game_screen.start_game() + self.shell.show_screen(self.shell.game_screen) + + def check_running(self): + return self.shell.game_screen.running + + def resume(self): + if self.shell.game_screen.running: + self.shell.show_screen(self.shell.game_screen) diff -r 33ce7ff757c3 -r ded4324b236e pyntnclick/scenewidgets.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pyntnclick/scenewidgets.py Sat Feb 11 13:10:18 2012 +0200 @@ -0,0 +1,149 @@ +"""Interactive elements within a Scene.""" + + +from pygame import Rect +from pygame.color import Color +from pygame.colordict import THECOLORS +from pygame.surface import Surface +from albow.resource import get_image + +from gamelib.state import Thing +from gamelib.constants import DEBUG +from gamelib.widgets import BoomLabel + + +class Interact(object): + + def __init__(self, image, rect, interact_rect): + self.image = image + self.rect = rect + self.interact_rect = interact_rect + + def set_thing(self, thing): + pass + + def draw(self, surface): + if self.image is not None: + surface.blit(self.image, self.rect, None) + + def animate(self): + return False + + +class InteractNoImage(Interact): + + def __init__(self, x, y, w, h): + super(InteractNoImage, self).__init__(None, None, Rect(x, y, w, h)) + + +class InteractText(Interact): + """Display box with text to interact with -- mostly for debugging.""" + + def __init__(self, x, y, text, bg_color=None): + if bg_color is None: + bg_color = (127, 127, 127) + label = BoomLabel(text) + label.set_margin(5) + label.border_width = 1 + label.border_color = (0, 0, 0) + label.bg_color = bg_color + label.fg_color = (0, 0, 0) + image = Surface(label.size) + rect = Rect((x, y), label.size) + label.draw_all(image) + super(InteractText, self).__init__(image, rect, rect) + + +class InteractRectUnion(Interact): + + def __init__(self, rect_list): + # pygame.rect.Rect.unionall should do this, but is broken + # in some pygame versions (including 1.8, it appears) + rect_list = [Rect(x) for x in rect_list] + union_rect = rect_list[0] + for rect in rect_list[1:]: + union_rect = union_rect.union(rect) + super(InteractRectUnion, self).__init__(None, None, union_rect) + self.interact_rect = rect_list + + +class InteractImage(Interact): + + def __init__(self, x, y, image_name): + super(InteractImage, self).__init__(None, None, None) + self._pos = (x, y) + self._image_name = image_name + + def set_thing(self, thing): + self.image = get_image(thing.folder, self._image_name) + self.rect = Rect(self._pos, self.image.get_size()) + self.interact_rect = self.rect + + +class InteractImageRect(InteractImage): + def __init__(self, x, y, image_name, r_x, r_y, r_w, r_h): + super(InteractImageRect, self).__init__(x, y, image_name) + self._r_pos = (r_x, r_y) + self._r_size = (r_w, r_h) + + def set_thing(self, thing): + super(InteractImageRect, self).set_thing(thing) + self.interact_rect = Rect(self._r_pos, self._r_size) + + +class InteractAnimated(Interact): + """Interactive with an animation rather than an image""" + + # FIXME: Assumes all images are the same size + # anim_seq - sequence of image names + # delay - number of frames to wait between changing images + + def __init__(self, x, y, anim_seq, delay): + self._pos = (x, y) + self._anim_pos = 0 + self._names = anim_seq + self._frame_count = 0 + self._anim_seq = None + self._delay = delay + + def set_thing(self, thing): + self._anim_seq = [get_image(thing.folder, x) for x in self._names] + self.image = self._anim_seq[0] + self.rect = Rect(self._pos, self.image.get_size()) + self.interact_rect = self.rect + + def animate(self): + if self._anim_seq: + self._frame_count += 1 + if self._frame_count > self._delay: + self._frame_count = 0 + self._anim_pos += 1 + if self._anim_pos >= len(self._anim_seq): + self._anim_pos = 0 + self.image = self._anim_seq[self._anim_pos] + # queue redraw + return True + return False + + +class GenericDescThing(Thing): + "Thing with an InteractiveUnionRect and a description" + + INITIAL = "description" + + def __init__(self, prefix, number, description, areas): + super(GenericDescThing, self).__init__() + self.description = description + self.name = '%s.%s' % (prefix, number) + self.interacts = { + 'description': InteractRectUnion(areas) + } + if DEBUG: + # Individual colors to make debugging easier + self._interact_hilight_color = Color(THECOLORS.keys()[number]) + + def get_description(self): + return self.description + + def is_interactive(self, tool=None): + return False diff -r 33ce7ff757c3 -r ded4324b236e pyntnclick/sound.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pyntnclick/sound.py Sat Feb 11 13:10:18 2012 +0200 @@ -0,0 +1,75 @@ +# Sound management for Suspended Sentence + +# This re-implements some of the albow.resource code to +# a) work around an annoying bugs +# b) add some missing functionality (disable_sound) + +import os + +import pygame +from pygame.mixer import music +from albow.resource import _resource_path, dummy_sound +import albow.music + +sound_cache = {} + + +def get_sound(*names): + if sound_cache is None: + return dummy_sound + path = _resource_path("sounds", names) + sound = sound_cache.get(path) + if not sound: + if not os.path.isfile(path): + missing_sound("File does not exist", path) + return dummy_sound + try: + from pygame.mixer import Sound + except ImportError, e: + no_sound(e) + return dummy_sound + try: + sound = Sound(path) + except pygame.error, e: + missing_sound(e, path) + return dummy_sound + sound_cache[path] = sound + return sound + + +def no_sound(e): + global sound_cache + print "get_sound: %s" % e + print "get_sound: Sound not available, continuing without it" + sound_cache = None + albow.music.music_enabled = False + + +def disable_sound(): + global sound_cache + sound_cache = None + albow.music.music_enabled = False + + +def missing_sound(e, name): + print "albow.resource.get_sound: %s: %s" % (name, e) + + +def start_next_music(): + """Start playing the next item from the current playlist immediately.""" + if albow.music.music_enabled and albow.music.current_playlist: + next_music = albow.music.current_playlist.next() + if next_music: + #print "albow.music: loading", repr(next_music) + music.load(next_music) + music.play() + albow.music.next_change_delay = albow.music.change_delay + albow.music.current_music = next_music + + +def get_current_playlist(): + if albow.music.music_enabled and albow.music.current_playlist: + return albow.music.current_playlist + +# Monkey patch +albow.music.start_next_music = start_next_music diff -r 33ce7ff757c3 -r ded4324b236e pyntnclick/speech.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pyntnclick/speech.py Sat Feb 11 13:10:18 2012 +0200 @@ -0,0 +1,44 @@ +# speech.py +# Copyright Boomslang team, 2010 (see COPYING File) +# Speech playing and cache + +import re + +from sound import get_sound + + +# cache of string -> sound object mappings +_SPEECH_CACHE = {} + +# characters not to allow in filenames +_REPLACE_RE = re.compile(r"[^a-z0-9-]+") + + +class SpeechError(RuntimeError): + pass + + +def get_filename(key, text): + """Simplify text to filename.""" + filename = "%s-%s" % (key, text) + filename = filename.lower() + filename = _REPLACE_RE.sub("_", filename) + filename = filename[:30] + filename = "%s.ogg" % filename + return filename + + +def get_speech(thing_name, text): + """Load a sound object from the cache.""" + key = (thing_name, text) + if key in _SPEECH_CACHE: + return _SPEECH_CACHE[key] + filename = get_filename(thing_name, text) + _SPEECH_CACHE[key] = sound = get_sound("speech", filename) + return sound + + +def say(thing_name, text): + """Play text as speech.""" + sound = get_speech(thing_name, text) + sound.play() diff -r 33ce7ff757c3 -r ded4324b236e pyntnclick/state.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pyntnclick/state.py Sat Feb 11 13:10:18 2012 +0200 @@ -0,0 +1,548 @@ +"""Utilities and base classes for dealing with scenes.""" + +import copy + +from albow.resource import get_image +from albow.utils import frame_rect +from widgets import BoomLabel +from pygame.rect import Rect +from pygame.color import Color + +import constants +from scenes import SCENE_LIST, INITIAL_SCENE +from sound import get_sound + +# override the initial scene to for debugging +DEBUG_SCENE = None + +# whether to show debugging rects +DEBUG_RECTS = False + + +class Result(object): + """Result of interacting with a thing""" + + def __init__(self, message=None, soundfile=None, detail_view=None, + style=None, close_detail=False, end_game=False): + self.message = message + self.sound = None + if soundfile: + self.sound = get_sound(soundfile) + self.detail_view = detail_view + self.style = style + self.close_detail = close_detail + self.end_game = end_game + + def process(self, scene_widget): + """Helper function to do the right thing with a result object""" + if self.sound: + self.sound.play() + if self.message: + scene_widget.show_message(self.message, self.style) + if self.detail_view: + scene_widget.show_detail(self.detail_view) + if (self.close_detail + and hasattr(scene_widget, 'parent') + and hasattr(scene_widget.parent, 'clear_detail')): + scene_widget.parent.clear_detail() + if self.end_game: + scene_widget.end_game() + + +def handle_result(result, scene_widget): + """Handle dealing with result or result sequences""" + if result: + if hasattr(result, 'process'): + result.process(scene_widget) + else: + for res in result: + if res: + # List may contain None's + res.process(scene_widget) + + +def initial_state(): + """Load the initial state.""" + state = GameState() + for scene in SCENE_LIST: + state.load_scenes(scene) + initial_scene = INITIAL_SCENE if DEBUG_SCENE is None else DEBUG_SCENE + state.set_current_scene(initial_scene) + state.set_do_enter_leave() + return state + + +class GameState(object): + """Complete game state. + + Game state consists of: + + * items + * scenes + """ + + def __init__(self): + # map of scene name -> Scene object + self.scenes = {} + # map of detail view name -> DetailView object + self.detail_views = {} + # map of item name -> Item object + self.items = {} + # list of item objects in inventory + self.inventory = [] + # currently selected tool (item) + self.tool = None + # current scene + self.current_scene = None + # current detail view + self.current_detail = None + # scene we came from, for enter and leave processing + self.previous_scene = None + # scene transion helpers + self.do_check = None + self.old_pos = None + # current thing + self.current_thing = None + self.highlight_override = False + + def add_scene(self, scene): + self.scenes[scene.name] = scene + + def add_detail_view(self, detail_view): + self.detail_views[detail_view.name] = detail_view + + def add_item(self, item): + self.items[item.name] = item + item.set_state(self) + + def load_scenes(self, modname): + mod = __import__("gamelib.scenes.%s" % (modname,), fromlist=[modname]) + for scene_cls in mod.SCENES: + self.add_scene(scene_cls(self)) + if hasattr(mod, 'DETAIL_VIEWS'): + for scene_cls in mod.DETAIL_VIEWS: + self.add_detail_view(scene_cls(self)) + + def set_current_scene(self, name): + old_scene = self.current_scene + self.current_scene = self.scenes[name] + self.current_thing = None + if old_scene and old_scene != self.current_scene: + self.previous_scene = old_scene + self.set_do_enter_leave() + + def set_current_detail(self, name): + self.current_thing = None + if name is None: + self.current_detail = None + else: + self.current_detail = self.detail_views[name] + return self.current_detail + + def add_inventory_item(self, name): + self.inventory.append(self.items[name]) + + def is_in_inventory(self, name): + if name in self.items: + return self.items[name] in self.inventory + return False + + def remove_inventory_item(self, name): + self.inventory.remove(self.items[name]) + # Unselect tool if it's removed + if self.tool == self.items[name]: + self.set_tool(None) + + def replace_inventory_item(self, old_item_name, new_item_name): + """Try to replace an item in the inventory with a new one""" + try: + index = self.inventory.index(self.items[old_item_name]) + self.inventory[index] = self.items[new_item_name] + if self.tool == self.items[old_item_name]: + self.set_tool(self.items[new_item_name]) + except ValueError: + return False + return True + + def set_tool(self, item): + self.tool = item + + def interact(self, pos): + return self.current_scene.interact(self.tool, pos) + + def interact_detail(self, pos): + return self.current_detail.interact(self.tool, pos) + + def cancel_doodah(self, screen): + if self.tool: + self.set_tool(None) + elif self.current_detail: + screen.state_widget.clear_detail() + + def do_enter_detail(self): + if self.current_detail: + self.current_detail.enter() + + def do_leave_detail(self): + if self.current_detail: + self.current_detail.leave() + + def animate(self): + if not self.do_check: + return self.current_scene.animate() + + def check_enter_leave(self, screen): + if not self.do_check: + return None + if self.do_check == constants.LEAVE: + self.do_check = constants.ENTER + if self.previous_scene: + return self.previous_scene.leave() + return None + elif self.do_check == constants.ENTER: + self.do_check = None + # Fix descriptions, etc. + if self.old_pos: + self.current_scene.update_current_thing(self.old_pos) + return self.current_scene.enter() + raise RuntimeError('invalid do_check value %s' % self.do_check) + + def set_do_enter_leave(self): + """Flag that we need to run the enter loop""" + self.do_check = constants.LEAVE + + +class StatefulGizmo(object): + + # initial data (optional, defaults to none) + INITIAL_DATA = None + + def __init__(self): + self.data = {} + if 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 + + def get_data(self, key): + return self.data.get(key, None) + + +class Scene(StatefulGizmo): + """Base class for scenes.""" + + # sub-folder to look for resources in + FOLDER = None + + # name of background image resource + BACKGROUND = None + + # name of scene (optional, defaults to folder) + NAME = None + + # Offset of the background image + OFFSET = (0, 0) + + def __init__(self, state): + StatefulGizmo.__init__(self) + # scene name + self.name = self.NAME if self.NAME is not None else self.FOLDER + # link back to state object + self.state = state + # map of thing names -> Thing objects + self.things = {} + self._background = None + + def add_item(self, item): + self.state.add_item(item) + + def add_thing(self, thing): + self.things[thing.name] = thing + thing.set_scene(self) + + def remove_thing(self, thing): + del self.things[thing.name] + if thing is self.state.current_thing: + self.state.current_thing.leave() + self.state.current_thing = None + + def _get_description(self): + text = (self.state.current_thing and + self.state.current_thing.get_description()) + if text is None: + return None + label = BoomLabel(text) + label.set_margin(5) + label.border_width = 1 + label.border_color = (0, 0, 0) + label.bg_color = Color(210, 210, 210, 255) + label.fg_color = (0, 0, 0) + return label + + def draw_description(self, surface, screen): + description = self._get_description() + if description is not None: + w, h = description.size + sub = screen.get_root().surface.subsurface( + Rect(400 - w / 2, 5, w, h)) + description.draw_all(sub) + + def _cache_background(self): + if self.BACKGROUND and not self._background: + self._background = get_image(self.FOLDER, self.BACKGROUND) + + def draw_background(self, surface): + self._cache_background() + if self._background is not None: + surface.blit(self._background, self.OFFSET, None) + else: + surface.fill((200, 200, 200)) + + def draw_things(self, surface): + for thing in self.things.itervalues(): + thing.draw(surface) + + def draw(self, surface, screen): + self.draw_background(surface) + self.draw_things(surface) + self.draw_description(surface, screen) + + def interact(self, item, pos): + """Interact with a particular position. + + Item may be an item in the list of items or None for the hand. + + Returns a Result object to provide feedback to the player. + """ + if self.state.current_thing is not None: + return self.state.current_thing.interact(item) + + def animate(self): + """Animate all the things in the scene. + + Return true if any of them need to queue a redraw""" + result = False + for thing in self.things.itervalues(): + if thing.animate(): + result = True + return result + + def enter(self): + return None + + def leave(self): + return None + + def update_current_thing(self, pos): + if self.state.current_thing is not None: + if not self.state.current_thing.contains(pos): + self.state.current_thing.leave() + self.state.current_thing = None + for thing in self.things.itervalues(): + if thing.contains(pos): + thing.enter(self.state.tool) + self.state.current_thing = thing + break + + def mouse_move(self, pos): + """Call to check whether the cursor has entered / exited a thing. + + Item may be an item in the list of items or None for the hand. + """ + self.update_current_thing(pos) + + def get_detail_size(self): + self._cache_background() + return self._background.get_size() + + +class InteractiveMixin(object): + def is_interactive(self, tool=None): + return True + + def interact(self, tool): + if not self.is_interactive(tool): + return None + if tool is None: + return self.interact_without() + handler = getattr(self, 'interact_with_' + tool.tool_name, None) + inverse_handler = self.get_inverse_interact(tool) + if handler is not None: + return handler(tool) + elif inverse_handler is not None: + return inverse_handler(self) + else: + return self.interact_default(tool) + + def get_inverse_interact(self, tool): + return None + + def interact_without(self): + return self.interact_default(None) + + def interact_default(self, item=None): + return None + + +class Thing(StatefulGizmo, InteractiveMixin): + """Base class for things in a scene that you can interact with.""" + + # name of thing + NAME = None + + # sub-folder to look for resources in (defaults to scenes folder) + FOLDER = None + + # list of Interact objects + INTERACTS = {} + + # name first interact + INITIAL = None + + # Interact rectangle hi-light color (for debugging) + # (set to None to turn off) + _interact_hilight_color = Color('red') + + def __init__(self): + StatefulGizmo.__init__(self) + # name of the thing + self.name = self.NAME + # folder for resource (None is overridden by scene folder) + self.folder = self.FOLDER + # interacts + self.interacts = self.INTERACTS + # these are set by set_scene + self.scene = None + self.state = None + self.current_interact = None + self.rect = None + self.orig_rect = None + + def _fix_rect(self): + """Fix rects to compensate for scene offset""" + # Offset logic is to always work with copies, to avoid + # flying effects from multiple calls to _fix_rect + # See footwork in draw + if hasattr(self.rect, 'collidepoint'): + self.rect = self.rect.move(self.scene.OFFSET) + else: + self.rect = [x.move(self.scene.OFFSET) for x in self.rect] + + def set_scene(self, scene): + assert self.scene is None + self.scene = scene + if self.folder is None: + self.folder = scene.FOLDER + self.state = scene.state + for interact in self.interacts.itervalues(): + interact.set_thing(self) + self.set_interact(self.INITIAL) + + def set_interact(self, name): + self.current_interact = self.interacts[name] + self.rect = self.current_interact.interact_rect + if self.scene: + self._fix_rect() + assert self.rect is not None, name + + def contains(self, pos): + if hasattr(self.rect, 'collidepoint'): + return self.rect.collidepoint(pos) + else: + # FIXME: add sanity check + for rect in list(self.rect): + if rect.collidepoint(pos): + return True + return False + + def get_description(self): + return None + + def enter(self, item): + """Called when the cursor enters the Thing.""" + pass + + def leave(self): + """Called when the cursr leaves the Thing.""" + pass + + def animate(self): + return self.current_interact.animate() + + def draw(self, surface): + old_rect = self.current_interact.rect + if old_rect: + self.current_interact.rect = old_rect.move(self.scene.OFFSET) + self.current_interact.draw(surface) + self.current_interact.rect = old_rect + if DEBUG_RECTS and self._interact_hilight_color: + if hasattr(self.rect, 'collidepoint'): + frame_rect(surface, self._interact_hilight_color, + self.rect.inflate(1, 1), 1) + else: + for rect in self.rect: + frame_rect(surface, self._interact_hilight_color, + rect.inflate(1, 1), 1) + + +class Item(InteractiveMixin): + """Base class for inventory items.""" + + # image for inventory + INVENTORY_IMAGE = None + + # name of item + NAME = None + + # name for interactions (i.e. def interact_with_) + TOOL_NAME = None + + # set to instance of CursorSprite + CURSOR = None + + def __init__(self, name=None): + self.state = None + self.name = self.NAME + if name is not None: + self.name = name + self.tool_name = name + if self.TOOL_NAME is not None: + self.tool_name = self.TOOL_NAME + self.inventory_image = None + + def _cache_inventory_image(self): + if not self.inventory_image: + self.inventory_image = get_image('items', self.INVENTORY_IMAGE) + + def set_state(self, state): + assert self.state is None + self.state = state + + def get_inventory_image(self): + self._cache_inventory_image() + return self.inventory_image + + def get_inverse_interact(self, tool): + return getattr(tool, 'interact_with_' + self.tool_name, None) + + def is_interactive(self, tool=None): + if tool: + return True + return False + + +class CloneableItem(Item): + _counter = 0 + + @classmethod + def _get_new_id(cls): + cls._counter += 1 + return cls._counter - 1 + + def __init__(self, name=None): + super(CloneableItem, self).__init__(name) + my_count = self._get_new_id() + self.name = "%s.%s" % (self.name, my_count) diff -r 33ce7ff757c3 -r ded4324b236e pyntnclick/version.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pyntnclick/version.py Sat Feb 11 13:10:18 2012 +0200 @@ -0,0 +1,82 @@ +"""Suspended Sentence Version Information""" + +VERSION = (1, 1, 0, 'alpha', 0) +BASE_VERSION_STR = '.'.join([str(x) for x in VERSION[:3]]) +VERSION_STR = { + 'final': BASE_VERSION_STR, + 'alpha': BASE_VERSION_STR + 'a' + str(VERSION[4]), + 'rc': BASE_VERSION_STR + 'rc' + str(VERSION[4]), +}[VERSION[3]] + +NAME = 'Suspended Sentence' +DESCRIPTION = 'Point-and-click adventure game written using Pygame.' + +PEOPLE = { + 'Simon': ('Simon Cross', 'hodgestar+rinkhals@gmail.com'), + 'Neil': ('Neil Muller', 'drnmuller+rinkhals@gmail.com'), + 'Adrianna': ('Adrianna Pinska', 'adrianna.pinska+rinkhals@gmail.com'), + 'Jeremy': ('Jeremy Thurgood', 'firxen+rinkhals@gmail.com'), + 'Stefano': ('Stefano Rivera', 'stefano@rivera.za.net'), +} + +AUTHORS = [ + PEOPLE['Simon'], + PEOPLE['Neil'], + PEOPLE['Adrianna'], + PEOPLE['Jeremy'], + PEOPLE['Stefano'], +] + +AUTHOR_NAME = AUTHORS[0][0] +AUTHOR_EMAIL = AUTHORS[0][1] + +MAINTAINERS = AUTHORS + +MAINTAINER_NAME = MAINTAINERS[0][0] +MAINTAINER_EMAIL = MAINTAINERS[0][1] + +ARTISTS = [ + PEOPLE['Adrianna'], +] + +DOCUMENTERS = [ + PEOPLE['Simon'], +] + +# SOURCEFORGE_URL = 'http://sourceforge.net/projects/XXXX/' +# PYPI_URL = 'http://pypi.python.org/pypi/XXXX/' + +LICENSE = 'MIT' +# LICENSE_TEXT = resource_string(__name__, 'COPYING') + +CLASSIFIERS = [ + 'Development Status :: 4 - Beta', + 'Environment :: MacOS X', + 'Environment :: Win32 (MS Windows)', + 'Environment :: X11 Applications', + 'Intended Audience :: End Users/Desktop', + 'License :: OSI Approved :: MIT License', + 'Natural Language :: English', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX', + 'Operating System :: MacOS :: MacOS X', + 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2.6', + 'Topic :: Games/Entertainment :: Role-Playing', +] + +PLATFORMS = [ + 'Linux', + 'Mac OS X', + 'Windows', +] + +INSTALL_REQUIRES = [ +] + +# Install these manually +NON_EGG_REQUIREMENTS = [ + 'setuptools', + 'pygame', + 'albow', +] diff -r 33ce7ff757c3 -r ded4324b236e pyntnclick/widgets.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pyntnclick/widgets.py Sat Feb 11 13:10:18 2012 +0200 @@ -0,0 +1,215 @@ +# widgets.py +# Copyright Boomslang team, 2010 (see COPYING File) + +"""Custom Albow widgets""" + +import textwrap + +import albow.controls +import albow.menu +from albow.resource import get_font, get_image +from pygame.color import Color +from pygame.rect import Rect +from pygame.draw import lines as draw_lines +from pygame import mouse + +from constants import BUTTON_SIZE +from cursor import CursorWidget + + +class BoomLabel(albow.controls.Label): + + trim_line_top = 0 + + def __init__(self, text, width=None, **kwds): + albow.controls.Label.__init__(self, text, width, **kwds) + w, h = self.size + h -= self.trim_line_top * len(self.text.split('\n')) + self.size = (w, h) + + def set_margin(self, margin): + """Add a set_margin method that recalculates the label size""" + old_margin = self.margin + w, h = self.size + d = margin - old_margin + self.margin = margin + self.size = (w + 2 * d, h + 2 * d) + + def draw_all(self, surface): + bg_color = self.bg_color + self.bg_color = None + if bg_color is not None: + new_surface = surface.convert_alpha() + new_surface.fill(bg_color) + surface.blit(new_surface, surface.get_rect()) + albow.controls.Label.draw_all(self, surface) + self._draw_all_no_bg(surface) + self.bg_color = bg_color + + def _draw_all_no_bg(self, surface): + pass + + def draw_with(self, surface, fg, _bg=None): + m = self.margin + align = self.align + width = surface.get_width() + y = m + lines = self.text.split("\n") + font = self.font + dy = font.get_linesize() - self.trim_line_top + for line in lines: + image = font.render(line, True, fg) + r = image.get_rect() + image = image.subsurface(r.clip(r.move(0, self.trim_line_top))) + r.top = y + if align == 'l': + r.left = m + elif align == 'r': + r.right = width - m + else: + r.centerx = width // 2 + surface.blit(image, r) + y += dy + + +class BoomButton(BoomLabel): + + def __init__(self, text, action, screen): + super(BoomButton, self).__init__(text, font=get_font(20, 'Vera.ttf'), + margin=4) + self.bg_color = (0, 0, 0) + self._frame_color = Color(50, 50, 50) + self.action = action + self.screen = screen + + def mouse_down(self, event): + self.action() + self.screen.state_widget.mouse_move(event) + + def mouse_move(self, event): + self.screen.state.highlight_override = True + + def draw(self, surface): + super(BoomButton, self).draw(surface) + r = surface.get_rect() + w = 2 + top, bottom, left, right = r.top, r.bottom, r.left, r.right + draw_lines(surface, self._frame_color, False, [ + (left, bottom), (left, top), (right - w, top), (right - w, bottom) + ], w) + + +class MessageDialog(BoomLabel, CursorWidget): + + def __init__(self, screen, text, wrap_width, style=None, **kwds): + CursorWidget.__init__(self, screen) + self.set_style(style) + paras = text.split("\n\n") + text = "\n".join([textwrap.fill(para, wrap_width) for para in paras]) + BoomLabel.__init__(self, text, **kwds) + + def set_style(self, style): + self.set_margin(5) + self.border_width = 1 + self.border_color = (0, 0, 0) + self.bg_color = (127, 127, 127) + self.fg_color = (0, 0, 0) + if style == "JIM": + self.set(font=get_font(20, "Monospace.ttf")) + self.trim_line_top = 10 + self.bg_color = Color(255, 175, 127, 191) + self.fg_color = (0, 0, 0) + self.border_color = (127, 15, 0) + + def draw_all(self, surface): + root_surface = self.get_root().surface + overlay = root_surface.convert_alpha() + overlay.fill(Color(0, 0, 0, 191)) + root_surface.blit(overlay, (0, 0)) + BoomLabel.draw_all(self, surface) + + def _draw_all_no_bg(self, surface): + CursorWidget.draw_all(self, surface) + + def mouse_down(self, event): + self.dismiss() + self.screen.state_widget._mouse_move(mouse.get_pos()) + for widget in self.screen.state_widget.subwidgets: + widget._mouse_move(mouse.get_pos()) + + def cursor_highlight(self): + return False + + +class HandButton(albow.controls.Image): + """The fancy hand button for the widget""" + + def __init__(self, action): + # FIXME: Yes, please. + this_image = get_image('items', 'hand.png') + albow.controls.Image.__init__(self, image=this_image) + self.action = action + self.set_rect(Rect(0, 0, BUTTON_SIZE, BUTTON_SIZE)) + + def mouse_down(self, event): + self.action() + + +class PopupMenuButton(albow.controls.Button): + + def __init__(self, text, action): + albow.controls.Button.__init__(self, text, action) + + self.font = get_font(16, 'Vera.ttf') + self.set_rect(Rect(0, 0, BUTTON_SIZE, BUTTON_SIZE)) + self.margin = (BUTTON_SIZE - self.font.get_linesize()) / 2 + + +class PopupMenu(albow.menu.Menu, CursorWidget): + + def __init__(self, screen): + CursorWidget.__init__(self, screen) + self.screen = screen + self.shell = screen.shell + items = [ + ('Quit Game', 'quit'), + ('Exit to Main Menu', 'main_menu'), + ] + # albow.menu.Menu ignores title string + albow.menu.Menu.__init__(self, None, items) + self.font = get_font(16, 'Vera.ttf') + + def show_menu(self): + """Call present, with the correct position""" + item_height = self.font.get_linesize() + menu_top = 600 - (len(self.items) * item_height + BUTTON_SIZE) + item = self.present(self.shell, (0, menu_top)) + if item > -1: + # A menu item needs to be invoked + self.invoke_item(item) + + +class BoomImageButton(albow.controls.Image): + """The fancy image button for the screens""" + + FOLDER = None + + def __init__(self, filename, x, y, action, enable=None): + this_image = get_image(self.FOLDER, filename) + albow.controls.Image.__init__(self, image=this_image) + self.action = action + self.set_rect(Rect((x, y), this_image.get_size())) + self.enable = enable + + def draw(self, surface): + if self.is_enabled(): + surface.blit(self.get_image(), self.get_rect()) + + def mouse_down(self, event): + if self.is_enabled(): + self.action() + + def is_enabled(self): + if self.enable: + return self.enable() + return True