# HG changeset patch # User M. George Hansen # Date 1305360755 25200 # Node ID 7a89ea5404b18b45f6935efe8c2b699ff49c94e0 Initial commit of parpg-core. diff -r 000000000000 -r 7a89ea5404b1 __init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/__init__.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,31 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +COPYRIGHT_HEADER = """\ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . +""" diff -r 000000000000 -r 7a89ea5404b1 application.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/application.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,230 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""This module contains the main Application class +and the basic Listener for PARPG """ + +import os +import sys + +from fife import fife +from fife.extensions import pychan +from fife.extensions.serializers.xmlanimation import XMLAnimationLoader +from fife.extensions.basicapplication import ApplicationBase + +from parpg import console +from parpg.font import PARPGFont +from parpg.gamemodel import GameModel +from parpg.mainmenuview import MainMenuView +from parpg.mainmenucontroller import MainMenuController +from parpg.common.listeners.event_listener import EventListener +from parpg.common.listeners.key_listener import KeyListener +from parpg.common.listeners.mouse_listener import MouseListener +from parpg.common.listeners.command_listener import CommandListener +from parpg.common.listeners.console_executor import ConsoleExecuter +from parpg.common.listeners.widget_listener import WidgetListener + +class KeyFilter(fife.IKeyFilter): + """ + This is the implementation of the fife.IKeyFilter class. + + Prevents any filtered keys from being consumed by guichan. + """ + def __init__(self, keys): + fife.IKeyFilter.__init__(self) + self._keys = keys + + def isFiltered(self, event): + """Checks if an key is filtered""" + return event.getKey().getValue() in self._keys + +class ApplicationListener(KeyListener, + MouseListener, + ConsoleExecuter, + CommandListener, + WidgetListener): + """Basic listener for PARPG""" + + def __init__(self, event_listener, engine, view, model): + """Initialize the instance. + @type engine: fife.engine + @param engine: ??? + @type view: viewbase.ViewBase + @param view: View that draws the current state + @type model: GameModel + @param model: The game model""" + + KeyListener.__init__(self, event_listener) + MouseListener.__init__(self, event_listener) + ConsoleExecuter.__init__(self, event_listener) + CommandListener.__init__(self, event_listener) + WidgetListener.__init__(self, event_listener) + self.engine = engine + self.view = view + self.model = model + keyfilter = KeyFilter([fife.Key.ESCAPE]) + keyfilter.__disown__() + + engine.getEventManager().setKeyFilter(keyfilter) + self.quit = False + self.about_window = None + self.console = console.Console(self) + + def quitGame(self): + """Forces a quit game on next cycle. + @return: None""" + self.quit = True + + def onConsoleCommand(self, command): + """ + Called on every console comand, delegates calls to the a console + object, implementing the callbacks + @type command: string + @param command: the command to run + @return: result + """ + return self.console.handleConsoleCommand(command) + + def onCommand(self, command): + """Enables the game to be closed via the 'X' button on the window frame + @type command: fife.Command + @param command: The command to read. + @return: None""" + if(command.getCommandType() == fife.CMD_QUIT_GAME): + self.quit = True + command.consume() + +class PARPGApplication(ApplicationBase): + """Main Application class + We use an MVC model model + self.gamesceneview is our view,self.model is our model + self.controller is the controller""" + + def __init__(self, setting): + """Initialise the instance. + @return: None""" + self._setting = setting + self.engine = fife.Engine() + self.loadSettings() + self.engine.init() + self._animationloader = XMLAnimationLoader(self.engine.getImagePool(), + self.engine.getVFS()) + self.engine.getAnimationPool().addResourceLoader(self._animationloader) + + pychan.init(self.engine, debug = True) + pychan.setupModalExecution(self.mainLoop,self.breakFromMainLoop) + + self.quitRequested = False + self.breakRequested = False + self.returnValues = [] + #self.engine.getModel(self) + self.model = GameModel(self.engine, setting) + self.model.readMapFiles() + self.model.readObjectDB() + self.model.getAgentImportFiles() + self.model.readAllAgents() + self.model.getDialogues() + self.view = MainMenuView(self.engine, self.model) + self.loadFonts() + self.event_listener = EventListener(self.engine) + self.controllers = [] + controller = MainMenuController(self.engine, self.view, self.model, + self) + #controller.initHud() + self.controllers.append(controller) + self.listener = ApplicationListener(self.event_listener, + self.engine, + self.view, + self.model) + #start_map = self._setting.fife.get("PARPG", "Map") + #self.model.changeMap(start_map) + + def loadFonts(self): + # add the fonts path to the system path to import font definitons + sys.path.insert(0, os.path.join(self._setting.system_path, + self._setting.fife.FontsPath)) + from oldtypewriter import fontdefs + + for fontdef in fontdefs: + pychan.internal.get_manager().addFont(PARPGFont(fontdef, + self._setting)) + + + def loadSettings(self): + """ + Load the settings from a python file and load them into the engine. + Called in the ApplicationBase constructor. + """ + + engineSetting = self.engine.getSettings() + engineSetting.setDefaultFontGlyphs(self._setting.fife.FontGlyphs) + engineSetting.setDefaultFontPath(os.path.join(self._setting.system_path, + self._setting.fife.FontsPath, + self._setting.fife.Font)) + engineSetting.setDefaultFontSize(self._setting.fife.DefaultFontSize) + engineSetting.setBitsPerPixel(self._setting.fife.BitsPerPixel) + engineSetting.setInitialVolume(self._setting.fife.InitialVolume) + engineSetting.setSDLRemoveFakeAlpha(self._setting.fife.SDLRemoveFakeAlpha) + engineSetting.setScreenWidth(self._setting.fife.ScreenWidth) + engineSetting.setScreenHeight(self._setting.fife.ScreenHeight) + engineSetting.setRenderBackend(self._setting.fife.RenderBackend) + engineSetting.setFullScreen(self._setting.fife.FullScreen) + engineSetting.setVideoDriver(self._setting.fife.VideoDriver) + engineSetting.setLightingModel(self._setting.fife.Lighting) + engineSetting.setColorKeyEnabled(self._setting.fife.ColorKeyEnabled) + + engineSetting.setColorKey(*[int(digit) + for digit in self._setting.fife.ColorKey]) + + engineSetting.setWindowTitle(self._setting.fife.WindowTitle) + engineSetting.setWindowIcon(os.path.join(self._setting.system_path, + self._setting.fife.IconsPath, + self._setting.fife.WindowIcon)) + + def createListener(self): + """ __init__ takes care of creating an event listener, so + basicapplication's createListener is harmful. Without + overriding it, the program quit's on esc press, rather than + invoking the main menu + """ + pass + + def pushController(self, controller): + """Adds a controller to the list to be the current active one.""" + self.controllers[-1].pause(True) + self.controllers.append(controller) + + def popController(self): + """Removes and returns the current active controller, unless its the last one""" + ret_controller = None + if self.controllers.count > 1: + ret_controller = self.controllers.pop() + self.controllers[-1].pause(False) + ret_controller.onStop() + return ret_controller + + def switchController(self, controller): + """Clears the controller list and adds a controller to be the current active one""" + for old_controller in self.controllers: + old_controller.onStop() + self.controllers = [] + self.controllers.append(controller) + + def _pump(self): + """Main game loop. + There are in fact 2 main loops, this one and the one in GameSceneView. + @return: None""" + if self.listener.quit: + self.breakRequested = True #pylint: disable-msg=C0103 + else: + for controller in self.controllers: + controller.pump() diff -r 000000000000 -r 7a89ea5404b1 charactercreationcontroller.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/charactercreationcontroller.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,394 @@ +# This file is part of PARPG. +# +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . +"""Provides the controller that defines the behaviour of the character creation + screen.""" + +import characterstatistics as char_stats +from serializers import XmlSerializer +from controllerbase import ControllerBase +from gamescenecontroller import GameSceneController +from gamesceneview import GameSceneView +from parpg.inventory import Inventory + +DEFAULT_STAT_VALUE = 50 + + +def getStatCost(offset): + """Gets and returns the cost to increase stat based on the offset""" + if offset < 0: + offset *= -1 + if offset < 22: + return 1 + elif offset < 29: + return 2 + elif offset < 32: + return 3 + elif offset < 35: + return 4 + elif offset < 36: + return 5 + elif offset < 38: + return 6 + elif offset < 39: + return 7 + elif offset < 40: + return 8 + elif offset < 41: + return 9 + else: + return 10 + +#TODO: Should be replaced with the real character class once its possible +class SimpleCharacter(object): + """This is a simple class that is used to store the data during the + character creation""" + + def __init__(self, name, gender, origin, age, picture, traits, + primary_stats, secondary_stats, inventory): + self.name = name + self.gender = gender + self.origin = origin + self.age = age + self.picture = picture + self.traits = traits + self.statistics = {} + for primary_stat in primary_stats: + short_name = primary_stat.short_name + self.statistics[short_name] = char_stats.PrimaryStatisticValue( + primary_stat, + self, + DEFAULT_STAT_VALUE) + long_name = primary_stat.long_name + self.statistics[long_name] = char_stats.PrimaryStatisticValue( + primary_stat, + self, + DEFAULT_STAT_VALUE) + for secondary_stat in secondary_stats: + name = secondary_stat.name + self.statistics[name] = char_stats.SecondaryStatisticValue( + secondary_stat, + self) + self.inventory = inventory + +class CharacterCreationController(ControllerBase): + """Controller defining the behaviour of the character creation screen.""" + + #TODO: Change to actual values + MAX_TRAITS = 3 + MIN_AGE = 16 + MAX_AGE = 40 + ORIGINS = {"None": None,} + GENDERS = ["Male", "Female",] + PICTURES = {"Male": ["None",], "Female": ["None",],} + TRAITS = {} + def __init__(self, engine, view, model, application): + """Construct a new L{CharacterCreationController} instance. + @param engine: Rendering engine used to display the associated view. + @type engine: L{fife.Engine} + @param view: View used to display the character creation screen. + @type view: L{ViewBase} + @param model: Model of the game state. + @type model: L{GameModel} + @param application: Application used to glue the various MVC + components together. + @type application: + L{fife.extensions.basicapplication.ApplicationBase}""" + ControllerBase.__init__(self, engine, view, model, application) + self.view.start_new_game_callback = self.startNewGame + self.view.cancel_new_game_callback = self.cancelNewGame + self.view.show() + #TODO: Maybe this should not be hardcoded + stream = file("character_scripts/primary_stats.xml") + prim_stats = XmlSerializer.deserialize(stream) + stream = file("character_scripts/secondary_stats.xml") + sec_stats = XmlSerializer.deserialize(stream) + self.char_data = SimpleCharacter("", + self.GENDERS[0], + self.ORIGINS.keys()[0], + 20, + self.PICTURES[self.GENDERS[0]][0], + [], + prim_stats, + sec_stats, + Inventory()) + self._stat_points = 200 + + + def startNewGame(self): + """Create the new character and start a new game. + @return: None""" + view = GameSceneView(self.engine, self.model) + controller = GameSceneController(self.engine, view, self.model, + self.application) + self.application.view = view + self.application.switchController(controller) + start_map = self.model.settings.parpg.Map + self.model.changeMap(start_map) + + def cancelNewGame(self): + """Exit the character creation view and return the to main menu. + @return: None""" + # KLUDGE Technomage 2010-12-24: This is to prevent a circular import + # but a better fix needs to be thought up. + from mainmenucontroller import MainMenuController + from mainmenuview import MainMenuView + view = MainMenuView(self.engine, self.model) + controller = MainMenuController(self.engine, view, self.model, + self.application) + self.application.view = view + self.application.switchController(controller) + + def onStop(self): + """Called when the controller is removed from the list. + @return: None""" + self.view.hide() + + @property + def name(self): + """Returns the name of the character. + @return: Name of the character""" + return self.char_data.name + + @property + def age(self): + """Returns the age of the character. + @return: Age of the character""" + return self.char_data.age + + @property + def gender(self): + """Returns the gender of the character. + @return: Gender of the character""" + return self.char_data.gender + + @property + def origin(self): + """Returns the origin of the character. + @return: Origin of the character""" + return self.char_data.origin + + @property + def picture(self): + """Returns the ID of the current picture of the character.""" + return self.char_data.picture + + def getStatPoints(self): + """Returns the remaining statistic points that can be distributed""" + return self._stat_points + + def increaseStatistic(self, statistic): + """Increases the given statistic by one. + @param statistic: Name of the statistic to increase + @type statistic: string""" + if self.canIncreaseStatistic(statistic): + cost = self.getStatisticIncreaseCost(statistic) + if cost <= self._stat_points: + self.char_data.statistics[statistic].value += 1 + self._stat_points -= cost + + def getStatisticIncreaseCost(self, statistic): + """Calculate and return the cost to increase the statistic + @param statistic: Name of the statistic to increase + @type statistic: string + @return cost to increase the statistic""" + cur_value = self.char_data.statistics[statistic].value + new_value = cur_value + 1 + offset = new_value - DEFAULT_STAT_VALUE + return getStatCost(offset) + + def canIncreaseStatistic(self, statistic): + """Checks whether the given statistic can be increased or not. + @param statistic: Name of the statistic to check + @type statistic: string + @return: True if the statistic can be increased, False if not.""" + stat = self.char_data.statistics[statistic].value + return stat < stat.statistic_type.maximum + + def decreaseStatistic(self, statistic): + """Decreases the given statistic by one. + @param statistic: Name of the statistic to decrease + @type statistic: string""" + if self.canDecreaseStatistic(statistic): + gain = self.getStatisticDecreaseGain(statistic) + self.char_data.statistics[statistic].value -= 1 + self._stat_points += gain + + def getStatisticDecreaseGain(self, statistic): + """Calculate and return the gain of decreasing the statistic + @param statistic: Name of the statistic to decrease + @type statistic: string + @return cost to decrease the statistic""" + cur_value = self.char_data.statistics[statistic].value + new_value = cur_value - 1 + offset = new_value - DEFAULT_STAT_VALUE + return getStatCost(offset) + + def canDecreaseStatistic(self, statistic): + """Checks whether the given statistic can be decreased or not. + @param statistic: Name of the statistic to check + @type statistic: string + @return: True if the statistic can be decreased, False if not.""" + stat = self.char_data.statistics[statistic].value + return stat > stat.statistic_type.minimum + + def getStatisticValue(self, statistic): + """Returns the value of the given statistic. + @param statistic: Name of the primary or secondary statistic + @type statistic: string + @return: Value of the given statistic""" + return self.char_data.statistics[statistic] + + def areAllStatisticsValid(self): + """Checks if all statistics are inside the minimum/maximum values + @return True if all statistics are valid False if not""" + for stat in self.char_data.statistics.items(): + if not (stat.value > stat.statistic_type.minumum and\ + stat.value < stat.statistic_type.maximum): + return False + return True + + def setName(self, name): + """Sets the name of the character to the given value. + @param name: New name + @type name: string""" + self.char_data.name = name + + def isNameValid(self, name): + """Checks whether the name is valid. + @param name: Name to check + @type name: string + @return: True if the name is valid, False if not""" + if name: + return True + return False + + def changeOrigin(self, origin): + """Changes the origin of the character to the given value. + @param origin: New origin + @type origin: string""" + if self.isOriginValid(origin): + self.char_data.origin = origin + #TODO: Make changes according to origin + + def isOriginValid(self, origin): + """Checks whether the origin is valid. + @param origin: Origin to check + @type origin: string + @return: True if the origin is valid, False if not""" + return origin in self.ORIGINS + + def changeGender(self, gender): + """Changes the gender of the character to the given value. + @param gender: New gender + @param gender: string""" + if self.isGenderValid(gender): + self.char_data.gender = gender + + def isGenderValid(self, gender): + """Checks whether the gender is valid. + @param gender: Gender to check + @type gender: string? + @return: True if the origin is valid, False if not""" + return gender in self.GENDERS + + def changeAge(self, age): + """Sets the age of the character to the given value. + @param age: New age + @type age: integer + """ + if self.isAgeValid(age): + self.char_data.age = age + + def isAgeValid(self, age): + """Checks whether the age is valid. + @param age: Age to check + @type age: integer + @return: True if the origin is valid, False if not""" + return age >= self.MIN_AGE and age <= self.MAX_AGE + + def setPicture(self, picture): + """Set picture of the character. + @param picture: ID of the new picture + @type picture: string""" + if self.isPictureValid(picture): + self.char_data.picture = picture + + def isPictureValid(self, picture): + """Checks whether the picture is valid. + @param picture: ID of the picture to check + @type picture: string + @return: True if the picture is valid, False if not""" + return picture in self.PICTURES[self.gender] + + def addTrait(self, trait): + """Adds a trait to the character. + @param trait: ID of the trait to add + @type trait: string""" + if self.canAddAnotherTrait() and self.isTraitValid(trait)\ + and not self.hasTrait(trait): + self.char_data.traits.append(trait) + + def canAddAnotherTrait(self): + """Checks whether another trait can be added. + @return: True if another trait can be added, False if not""" + return len(self.char_data.traits) < self.MAX_TRAITS + + def removeTrait(self, trait): + """Remove trait from character. + @param trait: ID of the trait to remove + @type trait: string""" + if self.hasTrait(trait): + self.char_data.traits.remove(trait) + + def hasTrait(self, trait): + """Checks whether the character has the trait. + @param trait: ID of the trait to check + @type trait: string + @return: True if the character has the trait, False if not""" + return trait in self.char_data.traits + + def isTraitValid(self, trait): + """Checks whether the trait is valid. + @param trait: ID of the trait to check + @type trait: string + @return: True if the trait is valid, False if not""" + return trait in self.TRAITS + + def areCurrentTraitsValid(self): + """Checks whether the characters traits are valid. + @return: True if the traits are valid, False if not""" + if len(self.char_data.traits) > self.MAX_TRAITS: + return False + for trait in self.char_data.traits: + if not self.isTraitValid(trait): + return False + return True + + def isCharacterValid(self): + """Checks whether the character as a whole is valid. + @return: True if the character is valid, False if not""" + #Single checks can be disabled by putting a "#" in front of them + if True\ + and self._stat_points >= 0\ + and self.areAllStatisticsValid() \ + and self.areCurrentTraitsValid() \ + and self.isNameValid(self.name)\ + and self.isPictureValid(self.picture)\ + and self.isAgeValid(self.age)\ + and self.isGenderValid(self.gender)\ + and self.isOriginValid(self.origin)\ + : + return True + return False diff -r 000000000000 -r 7a89ea5404b1 charactercreationview.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/charactercreationview.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,80 @@ +# This file is part of PARPG. +# +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . +"""Provides the view for displaying the character creation screen.""" + +import os + +from fife.extensions import pychan + +from viewbase import ViewBase + +class CharacterCreationView(ViewBase): + """View used to display the character creation screen. + @ivar background: Widget displayed as the background. + @type background: L{pychan.Widget} + @ivar start_new_game_callback: Callback attached to the startButton. + @type start_new_game_callback: callable + @ivar cancel_new_game_callback: Callback attached to the cancelButton. + @type cancel_new_game_callback: callable + @ivar character_screen: Widget used to display the character creation + screen. + @type character_screen: L{pychan.Widget}""" + + def __init__(self, engine, model, settings): + """Construct a new L{CharacterCreationView} instance. + @param engine: Rendering engine used to display the view. + @type engine: L{fife.Engine} + @param model: Model of the game state. + @type model: L{GameState}""" + ViewBase.__init__(self, engine, model) + self.settings = settings + gui_path = os.path.join(self.settings.system_path, + self.settings.parpg.GuiPath) + self.background = pychan.loadXML(os.path.join(gui_path, + 'main_menu_background.xml')) + screen_mode = self.engine.getRenderBackend().getCurrentScreenMode() + self.background.width = screen_mode.getWidth() + self.background.height = screen_mode.getHeight() + self.start_new_game_callback = None + self.cancel_new_game_callback = None + self.character_screen = pychan.loadXML(os.path.join(gui_path, + 'character_screen.xml')) + self.character_screen.adaptLayout() + character_screen_events = {} + character_screen_events['startButton'] = self.startNewGame + character_screen_events['cancelButton'] = self.cancelNewGame + self.character_screen.mapEvents(character_screen_events) + + def show(self): + """Display the view. + @return: None""" + self.background.show() + self.character_screen.show() + + def hide(self): + """Hide the view. + @return: None""" + self.background.hide() + self.character_screen.hide() + + def startNewGame(self): + """Callback tied to the startButton. + @return: None""" + self.start_new_game_callback() + + def cancelNewGame(self): + """Callback tied to the cancelButton. + @return: None""" + self.cancel_new_game_callback() diff -r 000000000000 -r 7a89ea5404b1 characterstatistics.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/characterstatistics.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,165 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Provides classes that define character stats and traits. +""" + +from abc import ABCMeta, abstractmethod +from weakref import ref as weakref + +from .serializers import SerializableRegistry + +class AbstractCharacterStatistic(object): + __metaclass__ = ABCMeta + + @abstractmethod + def __init__(self, description, minimum, maximum): + self.description = description + self.minimum = minimum + self.maximum = maximum + + +class PrimaryCharacterStatistic(AbstractCharacterStatistic): + def __init__(self, long_name, short_name, description, minimum=0, + maximum=100): + AbstractCharacterStatistic.__init__(self, description=description, + minimum=minimum, maximum=maximum) + self.long_name = long_name + self.short_name = short_name + +SerializableRegistry.registerClass( + 'PrimaryCharacterStatistic', + PrimaryCharacterStatistic, + init_args=[ + ('long_name', unicode), + ('short_name', unicode), + ('description', unicode), + ('minimum', int), + ('maximum', int), + ], +) + + +class SecondaryCharacterStatistic(AbstractCharacterStatistic): + def __init__(self, name, description, unit, mean, sd, stat_modifiers, + minimum=None, maximum=None): + AbstractCharacterStatistic.__init__(self, description=description, + minimum=minimum, maximum=maximum) + self.name = name + self.unit = unit + self.mean = mean + self.sd = sd + self.stat_modifiers = stat_modifiers + +SerializableRegistry.registerClass( + 'SecondaryCharacterStatistic', + SecondaryCharacterStatistic, + init_args=[ + ('name', unicode), + ('description', unicode), + ('unit', unicode), + ('mean', float), + ('sd', float), + ('stat_modifiers', dict), + ('minimum', float), + ('maximum', float), + ], +) + + +class AbstractStatisticValue(object): + __metaclass__ = ABCMeta + + @abstractmethod + def __init__(self, statistic_type, character): + self.statistic_type = statistic_type + self.character = weakref(character) + + +class PrimaryStatisticValue(AbstractStatisticValue): + def value(): + def fget(self): + return self._value + def fset(self, new_value): + assert 0 <= new_value <= 100 + self._value = new_value + + def __init__(self, statistic_type, character, value): + AbstractStatisticValue.__init__(self, statistic_type=statistic_type, + character=character) + self._value = None + self.value = value + + +class SecondaryStatisticValue(AbstractStatisticValue): + def normalized_value(): + def fget(self): + return self._normalized_value + def fset(self, new_value): + self._normalized_value = new_value + statistic_type = self.statistic_type + mean = statistic_type.mean + sd = statistic_type.sd + self._value = self.calculate_value(mean, sd, new_value) + return locals() + normalized_value = property(**normalized_value()) + + def value(): + def fget(self): + return self._value + def fset(self, new_value): + self._value = new_value + statistic_type = self.statistic_type + mean = statistic_type.mean + sd = statistic_type.sd + self._normalized_value = self.calculate_value(mean, sd, new_value) + return locals() + value = property(**value()) + + def __init__(self, statistic_type, character): + AbstractStatisticValue.__init__(self, statistic_type=statistic_type, + character=character) + mean = statistic_type.mean + sd = statistic_type.sd + normalized_value = self.derive_value(normalized=True) + self._normalized_value = normalized_value + self._value = self.calculate_value(mean, sd, normalized_value) + + def derive_value(self, normalized=True): + """ + Derive the current value + """ + statistic_type = self.statistic_type + stat_modifiers = statistic_type.stat_modifiers + character = self.character() + + value = sum( + character.statistics[name].value * modifier for name, modifier in + stat_modifiers.items() + ) + assert 0 <= value <= 100 + if not normalized: + mean = statistic_type.mean + sd = statistic_type.sd + value = self.calculate_value(mean, sd, value) + return value + + @staticmethod + def calculate_value(mean, sd, normalized_value): + value = sd * (normalized_value - 50) + mean + return value + + @staticmethod + def calculate_normalized_value(mean, sd, value): + normalized_value = ((value - mean) / sd) + 50 + return normalized_value diff -r 000000000000 -r 7a89ea5404b1 common/__init__.py diff -r 000000000000 -r 7a89ea5404b1 common/listeners/__init__.py diff -r 000000000000 -r 7a89ea5404b1 common/listeners/command_listener.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/listeners/command_listener.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +"""This module contains the CommandListener class for receiving command events""" + + +class CommandListener(object): + """Base class for listeners that receiving command events""" + + def __init__(self, event_listener): + self.event_listener = None + CommandListener.attach(self, event_listener) + + def attach(self, event_listener): + """Attaches the listener to the event""" + event_listener.addListener("Command", self) + self.event_listener = event_listener + + def detach(self): + """Detaches the listener from the event""" + self.event_listener.removeListener("Command", self) + self.event_listener = None + + def onCommand(self, command): + """Called when a command is executed""" + pass diff -r 000000000000 -r 7a89ea5404b1 common/listeners/console_executor.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/listeners/console_executor.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,44 @@ +#!/usr/bin/env python + +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +"""This module contains the ConsoleExecuter class that receives +console events""" + +class ConsoleExecuter(object): + """This class is a base class for listeners receiving console events""" + + def __init__(self, event_listener): + self.event_listener = None + ConsoleExecuter.attach(self, event_listener) + + def attach(self, event_listener): + """Attaches the listener to the event""" + event_listener.addListener("ConsoleCommand", self) + self.event_listener = event_listener + + def detach(self): + """Detaches the listener from the event""" + self.event_listener.removeListener("ConsoleCommand", self) + self.event_listener = None + + def onToolsClick(self): + """Called when the tools button has been clicked""" + pass + + def onConsoleCommand(self, command): + """Called when a console command is executed""" + pass \ No newline at end of file diff -r 000000000000 -r 7a89ea5404b1 common/listeners/event_listener.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/listeners/event_listener.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,142 @@ +#!/usr/bin/env python + +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +"""This module contains the EventListener that receives events and distributes +them to PARPG listeners""" + +from fife import fife +import logging + +logger = logging.getLogger('event_listener') + +class EventListener(fife.IKeyListener, + fife.ICommandListener, + fife.IMouseListener, + fife.ConsoleExecuter): + """Class that receives all events and distributes them to the listeners""" + def __init__(self, engine): + """Initialize the instance""" + self.event_manager = engine.getEventManager() + + fife.IKeyListener.__init__(self) + self.event_manager.addKeyListener(self) + fife.ICommandListener.__init__(self) + self.event_manager.addCommandListener(self) + fife.IMouseListener.__init__(self) + self.event_manager.addMouseListener(self) + fife.ConsoleExecuter.__init__(self) + engine.getGuiManager().getConsole().setConsoleExecuter(self) + + self.listeners = {"Mouse" : [], + "Key" : [], + "Command" : [], + "ConsoleCommand" : [], + "Widget" : []} + + def addListener(self, listener_type, listener): + """Adds a listener""" + if listener_type in self.listeners.iterkeys(): + if not listener in self.listeners[listener_type]: + self.listeners[listener_type].append(listener) + else: + logger.warning("Listener type " + "'{0}' not supported".format(listener_type)) + + def removeListener(self, listener_type, listener): + """Removes a listener""" + if listener_type in self.listeners.iterkeys(): + self.listeners[listener_type].remove(listener) + else: + logger.warning("Listener type " + "'{0}' not supported".format(listener_type)) + + def mousePressed(self, evt): + """Called when a mouse button is pressed""" + for listeners in self.listeners["Mouse"]: + listeners.mousePressed(evt) + + def mouseReleased(self, evt): + """Called when a mouse button is released""" + for listeners in self.listeners["Mouse"]: + listeners.mouseReleased(evt) + + def mouseEntered(self, evt): + """Called when a mouse enters a region""" + for listeners in self.listeners["Mouse"]: + listeners.mouseEntered(evt) + + def mouseExited(self, evt): + """Called when a mouse exits a region""" + for listeners in self.listeners["Mouse"]: + listeners.mouseExited(evt) + + def mouseClicked(self, evt): + """Called after a mouse button is pressed and released""" + for listeners in self.listeners["Mouse"]: + listeners.mouseClicked(evt) + + def mouseWheelMovedUp(self, evt): + """Called when the mouse wheel has been moved up""" + for listeners in self.listeners["Mouse"]: + listeners.mouseWheelMovedUp(evt) + + def mouseWheelMovedDown(self, evt): + """Called when the mouse wheel has been moved down""" + for listener in self.listeners["Mouse"]: + listener.mouseWheelMovedDown(evt) + + def mouseMoved(self, evt): + """Called when when the mouse has been moved""" + for listener in self.listeners["Mouse"]: + listener.mouseMoved(evt) + + def mouseDragged(self, evt): + """Called when dragging the mouse""" + for listener in self.listeners["Mouse"]: + listener.mouseDragged(evt) + + def keyPressed(self, evt): + """Called when a key is being pressed""" + for listener in self.listeners["Key"]: + listener.keyPressed(evt) + + def keyReleased(self, evt): + """Called when a key is being released""" + for listener in self.listeners["Key"]: + listener.keyReleased(evt) + + def onCommand(self, command): + """Called when a command is executed""" + for listener in self.listeners["Command"]: + listener.onCommand(command) + + def onToolsClick(self): + """Called when the tools button has been clicked""" + for listener in self.listeners["ConsoleCommand"]: + listener.onToolsClick() + + def onConsoleCommand(self, command): + """Called when a console command is executed""" + for listener in self.listeners["ConsoleCommand"]: + listener.onConsoleCommand(command) + + def onWidgetAction(self, evt): + """Called when a widget action is executed""" + for listener in self.listeners["Widget"]: + listener.onWidgetAction(evt) + + diff -r 000000000000 -r 7a89ea5404b1 common/listeners/key_listener.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/listeners/key_listener.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,42 @@ +#!/usr/bin/env python + +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +"""This module contains the KeyListener class for receiving key inputs""" + +class KeyListener(object): + """Base class for listeners receiving keyboard input""" + + def __init__(self, event_listener): + self.event_listener = None + KeyListener.attach(self, event_listener) + + def attach(self, event_listener): + """Attaches the listener to the event""" + event_listener.addListener("Key", self) + self.event_listener = event_listener + + def detach(self): + """Detaches the listener from the event""" + self.event_listener.removeListener("Key", self) + + def keyPressed(self, event): + """Called when a key is being pressed""" + pass + + def keyReleased(self, event): + """Called when a key is being released""" + pass \ No newline at end of file diff -r 000000000000 -r 7a89ea5404b1 common/listeners/mouse_listener.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/listeners/mouse_listener.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,71 @@ +#!/usr/bin/env python + +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +"""This module contains the MouseListener class for receiving mouse inputs""" + +class MouseListener(object): + """Base class for listeners receiving mouse input""" + + def __init__(self, event_listener): + self.event_listener = None + MouseListener.attach(self, event_listener) + + def attach(self, event_listener): + """Attaches the listener to the event""" + event_listener.addListener("Mouse", self) + self.event_listener = event_listener + + def detach(self): + """Detaches the listener from the event""" + self.event_listener.removeListener("Mouse", self) + self.event_listener = None + + def mousePressed(self, evt): + """Called when a mouse button is pressed""" + pass + + def mouseReleased(self, evt): + """Called when a mouse button is released""" + pass + + def mouseEntered(self, evt): + """Called when a mouse enters a region""" + pass + + def mouseExited(self, evt): + """Called when a mouse exits a region""" + pass + + def mouseClicked(self, evt): + """Called after a mouse button is pressed and released""" + pass + + def mouseWheelMovedUp(self, evt): + """Called when the mouse wheel has been moved up""" + pass + + def mouseWheelMovedDown(self, evt): + """Called when the mouse wheel has been moved down""" + pass + + def mouseMoved(self, evt): + """Called when when the mouse has been moved""" + pass + + def mouseDragged(self, evt): + """Called when dragging the mouse""" + pass diff -r 000000000000 -r 7a89ea5404b1 common/listeners/widget_listener.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/listeners/widget_listener.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,39 @@ +#!/usr/bin/env python + +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +"""This module contains the WidgetListener class for receiving widget events""" + +class WidgetListener(object): + """A Base class for listeners receiving widget events""" + + def __init__(self, event_listener): + self.event_listener = None + WidgetListener.attach(self, event_listener) + + def attach(self, event_listener): + """Attaches the listener to the event""" + event_listener.addListener("Widget", self) + self.event_listener = event_listener + + def detach(self): + """Detaches the listener from the event""" + self.event_listener.removeListener("Widget", self) + self.event_listener = None + + def onWidgetAction(self, evt): + """Called when a widget action is executed""" + pass \ No newline at end of file diff -r 000000000000 -r 7a89ea5404b1 common/ordereddict.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/ordereddict.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,127 @@ +# Copyright (c) 2009 Raymond Hettinger +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +from UserDict import DictMixin + +class OrderedDict(dict, DictMixin): + + def __init__(self, *args, **kwds): + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__end + except AttributeError: + self.clear() + self.update(*args, **kwds) + + def clear(self): + self.__end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.__map = {} # key --> [key, prev, next] + dict.clear(self) + + def __setitem__(self, key, value): + if key not in self: + end = self.__end + curr = end[1] + curr[2] = end[1] = self.__map[key] = [key, curr, end] + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + key, prev, next = self.__map.pop(key) + prev[2] = next + next[1] = prev + + def __iter__(self): + end = self.__end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.__end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def popitem(self, last=True): + if not self: + raise KeyError('dictionary is empty') + if last: + key = reversed(self).next() + else: + key = iter(self).next() + value = self.pop(key) + return key, value + + def __reduce__(self): + items = [[k, self[k]] for k in self] + tmp = self.__map, self.__end + del self.__map, self.__end + inst_dict = vars(self).copy() + self.__map, self.__end = tmp + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def keys(self): + return list(self) + + setdefault = DictMixin.setdefault + update = DictMixin.update + pop = DictMixin.pop + values = DictMixin.values + items = DictMixin.items + iterkeys = DictMixin.iterkeys + itervalues = DictMixin.itervalues + iteritems = DictMixin.iteritems + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + + def copy(self): + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + if isinstance(other, OrderedDict): + if len(self) != len(other): + return False + for p, q in zip(self.items(), other.items()): + if p != q: + return False + return True + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other diff -r 000000000000 -r 7a89ea5404b1 common/utils.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/utils.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,61 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Miscellaneous game functions + +import os, sys, fnmatch +from textwrap import dedent + +def addPaths (*paths): + """Adds a list of paths to sys.path. Paths are expected to use forward + slashes, for example '../../engine/extensions'. Slashes are converted + to the OS-specific equivalent. + @type paths: ??? + @param paths: Paths to files? + @return: None""" + for p in paths: + if not p in sys.path: + sys.path.append(os.path.sep.join(p.split('/'))) + +def parseBool(value): + """Parses a string to get a boolean value""" + if (value.isdigit()): + return bool(int(value)) + elif (value.isalpha): + return value.lower()[0] == "t" + return False + +def locateFiles(pattern, root=os.curdir): + """Locate all files matching supplied filename pattern in and below + supplied root directory.""" + for path, _, files in os.walk(os.path.abspath(root)): + for filename in fnmatch.filter(files, pattern): + yield os.path.join(path, filename) + +def dedent_chomp(string): + """Remove common leading whitespace and chomp each non-blank line.""" + dedented_string = dedent(string).strip() + lines = dedented_string.splitlines() + formatted_lines = [] + for index in range(len(lines)): + line = lines[index] + if index == len(lines) - 1: + # Don't do anything to the last line. + pass + elif not line or line.isspace(): + line = '\n\n' + else: + line = ''.join([line, ' ']) + formatted_lines.append(line) + result = ''.join(formatted_lines) + return result diff -r 000000000000 -r 7a89ea5404b1 console.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/console.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,204 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import re +import os +import sys +from StringIO import StringIO +import code + +class Console: + def __init__(self, app_listener): + """ + @type appListener: ApplicationListener + @param appListener: ApplicationListener object providing a link with + the Controller, the view and the GameModel""" + exit_help = "Terminate application" + grid_help = "Toggle grid display" + run_help = "Toggle player run/walk" + help_help = "Show this help string" + load_help = "Usage: load directory file" + python_help = "Run some python code" + quit_help = "Terminate application" + save_help = "Usage: save directory file" + pause_help = "Pause/Unpause the game" + + self.commands = [ + {"cmd":"exit" ,"callback":self.handleQuit ,"help": exit_help}, + {"cmd":"grid" ,"callback":self.handleGrid ,"help": grid_help}, + {"cmd":"help" ,"callback":self.handleHelp ,"help": help_help}, + {"cmd":"load" ,"callback":self.handleLoad ,"help": load_help}, + {"cmd":"pause" ,"callback":self.handlePause ,"help": pause_help}, + {"cmd":"python","callback":self.handlePython,"help": python_help}, + {"cmd":"run" ,"callback":self.handleRun ,"help": run_help}, + {"cmd":"save" ,"callback":self.handleSave ,"help": save_help}, + {"cmd":"quit" ,"callback":self.handleQuit ,"help": quit_help}, + ] + self.app_listener = app_listener + self.view = self.app_listener.view + self.model = self.app_listener.model + self.game_state = self.app_listener.view.model.game_state + self.console_locals = {"__name__":"__paprg_console__",\ + "__doc__": None,\ + "app_listener":self.app_listener,\ + "model":self.app_listener.model,\ + "view":self.app_listener.view,\ + "engine":self.app_listener.engine} + + self.console = code.InteractiveConsole(self.console_locals) + + def handleQuit(self, command): + """Implements the quit console command + @type command: string + @param command: The command to run + @return: The resultstring""" + self.app_listener.quitGame() + return "Terminating ..." + + def handlePause(self, command): + """Implements the pause console command + @type command: string + @param command: The command to run + @return: The resultstring""" + self.model.togglePause() + return "Game (un)paused" + + def handleGrid(self, command): + """Implements the grid console command + @type command: string + @param command: The command to run + @return: The resultstring""" + self.app_listener.view.active_map.toggleRenderer('GridRenderer') + return "Grid toggled" + + def handleRun(self, command): + """Toggles run/walk mode of the PC player + @type command: string + @param command: The command to run. + @return: The response""" + if self.app_listener.model.pc_run == 1: + self.app_listener.model.pc_run = 0 + return "PC is now walking" + else: + self.app_listener.model.pc_run = 1 + return "PC is now running" + + def handleHelp(self, command): + """Implements the help console command + @type command: string + @param command: The command to run + @return: The resultstring""" + res = "" + for cmd in self.commands: + res += "%10s: %s\n" % (cmd["cmd"], cmd["help"]) + return res + + def handlePython(self,command): + user_code = command[7:len(command)] + + codeOut = StringIO() + + #make stdout and stderr write to our file, not the terminal + sys.stdout = codeOut + sys.stderr = codeOut + + #Workaround it not being possible to enter a blank line in the console + if user_code == " ": + user_code = "" + + #Process the code + self.console.push(user_code) + if len(self.console.buffer) == 0: + output = codeOut.getvalue() + else: + output = "..." + + + #restore stdout and stderr + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ + + temp_output = output + output = "" + counter = 0 + + #Make the output fit in the console screen + for char in temp_output: + counter += 1 + if char == "\n": + counter = 0 + elif counter == 110: + output += "\n" + counter = 0 + output += char + + return output + + def handleLoad(self, command): + """Implements the load console command + @type command: string + @param command: The command to run + @return: The resultstring""" + result = None + load_regex = re.compile('^load') + l_matches = load_regex.match(command.lower()) + if (l_matches is not None): + end_load = l_matches.end() + try: + l_args = command.lower()[end_load + 1:].strip() + l_path, l_filename = l_args.split(' ') + self.app_listener.model.load_save = True + self.app_listener.model.savegame = os.path.join(l_path, l_filename) + result = "Load triggered" + + except Exception, l_error: + self.app_listener.engine.getGuiManager().getConsole().println('Error: ' + str(l_error)) + result="Failed to load file" + + return result + + def handleSave(self, command): + """Implements the save console command + @type command: string + @param command: The command to run + @return: The resultstring""" + save_regex = re.compile('^save') + s_matches = save_regex.match(command.lower()) + if (s_matches != None): + end_save = s_matches.end() + try: + s_args = command.lower()[end_save+1:].strip() + s_path, s_filename = s_args.split(' ') + self.app_listener.model.save(s_path, s_filename) + result = "Saved to file: " + s_path + s_filename + + except Exception, s_error: + self.app_listener.engine.getGuiManager().getConsole(). \ + println('Error: ' + str(s_error)) + result = "Failed to save file" + return result + + def handleConsoleCommand(self, command): + """Implements the console logic + @type command: string + @param command: The command to run + @return: The response string """ + result = None + for cmd in self.commands: + regex = re.compile('^' + cmd["cmd"]) + if regex.match(command.lower()): + result=cmd["callback"](command) + + if result is None: + result = self.handlePython("python " + command) + return result diff -r 000000000000 -r 7a89ea5404b1 controllerbase.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/controllerbase.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,99 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . +import os +from fife import fife + +from parpg.common.listeners.key_listener import KeyListener +from parpg.common.listeners.mouse_listener import MouseListener +from parpg.common.listeners.command_listener import CommandListener + +class ControllerBase(KeyListener, MouseListener, CommandListener): + """Base of Controllers""" + def __init__(self, + engine, + view, + model, + application): + ''' + Constructor + @param engine: Instance of the active fife engine + @type engine: fife.Engine + @param view: Instance of a GameSceneView + @param type: parpg.GameSceneView + @param model: The model that has the current gamestate + @type model: parpg.GameModel + @param application: The application that created this controller + @type application: parpg.PARPGApplication + @param settings: The current settings of the application + @type settings: fife.extensions.fife_settings.Setting + ''' + KeyListener.__init__(self, application.event_listener) + MouseListener.__init__(self, application.event_listener) + CommandListener.__init__(self, application.event_listener) + self.engine = engine + self.event_manager = engine.getEventManager() + self.view = view + self.model = model + self.application = application + + def pause(self, paused): + """Stops receiving events""" + if paused: + KeyListener.detach(self) + MouseListener.detach(self) + else: + KeyListener.attach(self, self.application.event_listener) + MouseListener.attach(self, self.application.event_listener) + + def setMouseCursor(self, image, dummy_image, mc_type="native"): + """Set the mouse cursor to an image. + @type image: string + @param image: The image you want to set the cursor to + @type dummy_image: string + @param dummy_image: ??? + @type type: string + @param type: ??? + @return: None""" + cursor = self.engine.getCursor() + cursor_type = fife.CURSOR_IMAGE + img_pool = self.engine.getImagePool() + if(mc_type == "target"): + target_cursor_id = img_pool.addResourceFromFile(image) + dummy_cursor_id = img_pool.addResourceFromFile(dummy_image) + cursor.set(cursor_type, dummy_cursor_id) + cursor.setDrag(cursor_type, target_cursor_id, -16, -16) + else: + cursor_type = fife.CURSOR_IMAGE + zero_cursor_id = img_pool.addResourceFromFile(image) + cursor.set(cursor_type, zero_cursor_id) + cursor.setDrag(cursor_type, zero_cursor_id) + + def resetMouseCursor(self): + """Reset cursor to default image. + @return: None""" + image = os.path.join(self.model.settings.system_path, + self.model.settings.parpg.GuiPath, + self.model.settings.parpg.CursorPath, + self.model.settings.parpg.CursorDefault) + self.setMouseCursor(image, image) + + def onStop(self): + """Called when the controller is removed from the list""" + pass + + def pump(self): + """This method gets called every frame""" + pass + diff -r 000000000000 -r 7a89ea5404b1 dialogue.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dialogue.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,192 @@ +# This file is part of PARPG. +# +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . +""" +Provides classes used to contain and organize dialogue data for use within +in-game dialogues between the player character and NPCs. +""" +try: + from collections import OrderedDict +except ImportError: + # Python version 2.4-2.6 doesn't have the OrderedDict + from parpg.common.ordereddict import OrderedDict + +class Dialogue(object): + """ + Represents a complete dialogue and acts as a container for the dialogue + data belonging to a particular NPC. + """ + __slots__ = ['npc_name', 'avatar_path', 'default_greeting', + 'greetings', 'sections'] + + def __init__(self, npc_name, avatar_path, default_greeting, greetings=None, + sections=None): + """ + Initialize a new L{Dialogue} instance. + + @param npc_name: name displayed for the NPC in the dialogue. + @type npc_name: basestring + @param avatar_path: path to the image that should be displayed as the + NPC's avatar. + @type avatar_path: basestring + @param default_greeting: section of dialogue that should be + displayed when the dialogue is first initiated and no other start + sections are available. + @type default_greeting: L{DialogueSection} + @param greetings: sections of dialogue defining the conditions + under which each should be displayed when the dialogue is first + initiated. + @type greetings: list of + L{RootDialogueSections} + @param sections: sections of dialogue that make up this + L{Dialogue} instance. + @type sections: list of L{DialogueSections} + """ + self.npc_name = npc_name + self.avatar_path = avatar_path + self.default_greeting = default_greeting + self.greetings = greetings if greetings is not None else [] + self.sections = OrderedDict() + all_sections = [default_greeting] + if (greetings is not None): + all_sections += greetings + if (sections is not None): + all_sections += sections + if (__debug__): + section_ids = [section.id for section in all_sections] + for section in all_sections: + # Sanity check: All DialogueResponses should have next_section_id + # attributes that refer to valid DialogueSections in the Dialogue. + if (__debug__): + for response in section.responses: + assert response.next_section_id in section_ids + \ + ['end', 'back'], ('"{0}" does not refer to a ' + 'DialogueSection in this Dialogue')\ + .format(response.next_section_id) + self.sections[section.id] = section + + def __str__(self): + """Return the string representation of a L{Dialogue} instance.""" + string_representation = 'Dialogue(npc_id={0.npc_name})'.format(self) + return string_representation + + +class DialogueNode(object): + """ + Abstract base class that represents a node or related group of attributes + within a Dialogue. + """ + def __init__(self, text, actions=None): + """ + Initialize a new L{DialogueNode} instance. + + @param text: textual content of the L{DialogueNode}. + @type text: basestring + @param actions: dialogue actions associated with the L{DialogueNode}. + @type actions: list of L{DialogueActions} + """ + self.text = text + self.actions = actions or [] + + +class DialogueSection(DialogueNode): + """DialogueNode that represents a distinct section of the dialogue.""" + __slots__ = ['id', 'text', 'responses', 'actions'] + + def __init__(self, id_, text, responses=None, actions=None): + """ + Initialize a new L{DialogueSection} instance. + + @param id_: named used to uniquely identify the L{DialogueSection} + within a L{Dialogue}. + @type id_: basestring + @param text: text displayed as the NPC's part of the L{Dialogue}. + @type text: basestring + @param responses: possible responses that the player can choose from. + @type responses: list of L{DialogueResponses} + @param actions: dialogue actions that should be executed when the + L{DialogueSection} is reached. + @type actions: list of L{DialogueActions} + """ + DialogueNode.__init__(self, text=text, actions=actions) + self.id = id_ + if (responses is not None): + self.responses = list(responses) + + +class DialogueGreeting(DialogueSection): + """ + Represents a root section of dialogue in a L{Dialogue} along with the + conditional statement used to determine the whether this section should be + displayed first upon dialogue initiation. + + @ivar id: Name used to uniquely identify the L{DialogueSection} to which + the L{DialogueRootSectionReference} points. + @type id: basestring + @ivar condition: Boolean Python expression used to determine if the + L{DialogueSection} referenced is a valid starting section. + @type condition: basestring + """ + __slots__ = ['id', 'condition', 'text', 'actions', 'responses'] + + def __init__(self, id_, condition, text, responses=None, actions=None): + """ + Initialize a new L{DialogueGreeting} instance. + + @param id_: named used to uniquely identify the L{DialogueSection} + within a L{Dialogue}. + @type id_: basestring + @param condition: Boolean Python expression used to determine if this + root dialogue section should be displayed. + @type condition: basestring + @param text: text displayed as the NPC's part of the L{Dialogue}. + @type text: basestring + @param responses: possible responses that the player can choose from. + @type responses: list of L{DialogueResponses} + @param actions: dialogue actions that should be executed when the + L{DialogueSection} is reached. + @type actions: list of L{DialogueActions} + """ + DialogueSection.__init__(self, id_=id_, text=text, responses=responses, + actions=actions) + self.condition = condition + + +class DialogueResponse(DialogueNode): + """ + L{DialogueNode} that represents one possible player response to a + particular L{DialogueSection}. + """ + __slots__ = ['text', 'actions', 'condition', 'next_section_id'] + + def __init__(self, text, next_section_id, actions=None, condition=None): + """ + Initialize a new L{DialogueResponse} instance. + + @param text: text displayed as the content of the player's response. + @type text: basestring + @param next_section_id: ID of the L{DialogueSection} that should be + jumped to if this response is chosen by the player. + @type next_section_id: basestring + @param actions: dialogue actions that should be executed if this + response is chosen by the player. + @type actions: list of L{DialogueActions} + @param condition: Python expression that when evaluated determines + whether the L{DialogueResponse} should be displayed to the player + as a valid response. + @type condition: basestring + """ + DialogueNode.__init__(self, text=text, actions=actions) + self.condition = condition + self.next_section_id = next_section_id diff -r 000000000000 -r 7a89ea5404b1 dialogueactions.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dialogueactions.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,373 @@ +# This file is part of PARPG. +# +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . +""" +Provides classes used to implement dialogue logic and allow dialogues to have +external effects on the game state. +""" +import logging + +logger = logging.getLogger('dialogueaction') + +class DialogueAction(object): + """ + Abstract base class for subclasses that represent dialogue actions embedded + within a DialogueSection or DialogueResponse. + + Subclasses must define the keyword class variable and implement both the + __init__ and __call__ methods. + + @cvar keyword: keyword used by the L{DialogueParser} to recognize the + L{DialogueAction} in serialized L{Dialogues}. + @type keyword: basestring + """ + logger = logging.getLogger('dialogueaction.DialogueAction') + registered_actions = {} + + @classmethod + def registerAction(cls, dialogue_action_type): + """ + Register a L{DialogueAction} subclass for easy reference. + + @param dialogue_action_type: dialogue action to register. + @type dialogue_action_type: L{DialogueAction} subclass + """ + cls.registered_actions[dialogue_action_type.keyword] = \ + dialogue_action_type + + def __init__(self, *args, **kwargs): + """ + Initialize a new L{DialogueAction} instance. + + @param args: positional arguments passed by the L{DialogueParser} after + reading a serialized L{Dialogue}. + @type args: list of objects + @param kwargs: keyword arguments passed by the L{DialogueParser} after + reading a serialized L{Dialogue}. + @type kwargs: dict of objects + """ + if (not hasattr(type(self), 'keyword')): + raise AttributeError('DialogueAction subclasses must define the ' + 'keyword class variable.') + self.arguments = (args, kwargs) + + def __call__(self, game_state): + """ + Execute the L{DialogueAction}. + + @param game_state: variables and functions that make up the current + game state. + @type game_state: dict of objects + """ + raise NotImplementedError('subclasses of DialogueAction must ' + 'override __call__') + + +class MeetAction(DialogueAction): + """ + L{DialogueAction} that adds an NPC to the list of NPCs known by the player. + """ + keyword = 'meet' + + def __init__(self, *args, **kwargs): + """ + Initialize a new L{MeetAction} instance. + + @param args: positional arguments. + @type args: list of objects + @param npc_id: identifier of the NPC that the player has met. + @type npc_id: basestring + @param kwargs: keyword arguments (not used). + @type kwargs: dict of objects + """ + DialogueAction.__init__(self, *args, **kwargs) + self.npc_id = args[0] + + def __call__(self, game_state): + """ + Add an NPC to the list of NPCs known by the player. + + @param game_state: variables and functions that make up the current + game state. + @type game_state: dict of objects + """ + npc_id = self.npc_id + # NOTE Technomage 2010-11-13: This print statement seems overly + # verbose, so I'm logging it as an INFO message instead. +# print("You've met {0}!".format(npc_id)) + self.logger.info("You've met {0}!".format(npc_id)) + game_state['pc'].meet(npc_id) +DialogueAction.registerAction(MeetAction) + + +class InventoryAction(DialogueAction): + """ + Abstract base class for L{DialogueActions} used to + manipulate the NPC's and the player's inventory. + """ + def __init__(self, *args, **kwargs): + """ + Initialize a new L{InventoryAction} instance. + + @param args: positional arguments. + @type args: list of objects + @param item_types: item types that should be manipulated. + @type item_types: list of basestrings + @param kwargs: keyword arguments. + @type kwargs: dict of objects + """ + DialogueAction.__init__(self, *args, **kwargs) + self.item_types = args + + +class TakeStuffAction(InventoryAction): + """ + L{InventoryAction} used to move items from the NPC's inventory to the + player's inventory. + """ + keyword = 'take_stuff' + + def __call__(self, game_state): + """ + Move items from the NPC's inventory to the player's inventory. + + @param game_state: variables and functions that make up the current + game state. + @type game_state: dict of objects + """ + item_types = self.item_types + for item_type in item_types: + item = game_state['npc'].inventory.findItem(item_type=item_type) + if (item): + game_state['npc'].give(item, game_state['pc']) + print("{0} gave you the {1}".format(game_state['npc'].name, + item_type)) + else: + print("{0} doesn't have the {1}".format(game_state['npc'].name, + item_type)) +DialogueAction.registerAction(TakeStuffAction) + + +class GiveStuffAction(InventoryAction): + """ + L{InventoryAction} used to move items from the player's inventory to the + NPC's inventory. + """ + keyword = 'give_stuff' + + def __call__(self, game_state): + """ + Move items from the player's inventory to the NPC's inventory. + + @param game_state: variables and functions that make up the current + game state. + @type game_state: dict of objects + """ + item_types = self.item_types + for item_type in item_types: + item = game_state['npc'].inventory.findItem(item_type = item_type) + if (item): + game_state['pc'].give(item, game_state['npc']) + print("You give the {0} to {1}".format(item_type, + game_state['npc'].name)) + else: + print("You don't have the {0}".format(item_type)) +DialogueAction.registerAction(GiveStuffAction) + + +class QuestAction(DialogueAction): + """ + Abstract base class for quest-related L{DialogueActions}. + """ + def __init__(self, *args, **kwargs): + """ + Initialize a new L{QuestAction} instance. + + @param args: positional arguments. + @type args: list of objects + @param quest_id: ID of the quest to manipulate. + @type quest_id: basestring + @param kwargs: keyword arguments (not used). + @type kwargs: dict of objects + """ + DialogueAction.__init__(self, *args, **kwargs) + self.quest_id = kwargs['quest'] if 'quest' in kwargs else args[0] + + +class StartQuestAction(QuestAction): + """L{QuestAction} used to activate a quest.""" + keyword = 'start_quest' + + def __call__(self, game_state): + """ + Activate a quest. + + @param game_state: variables and functions that make up the current + game state. + @type game_state: dict of objects + """ + quest_id = self.quest_id + print("You've picked up the \"{0}\" quest!".format(quest_id)) + game_state['quest'].activateQuest(quest_id) +DialogueAction.registerAction(StartQuestAction) + + +class CompleteQuestAction(QuestAction): + """ + L{QuestAction} used to mark a quest as successfully finished/completed. + """ + keyword = 'complete_quest' + + def __call__(self, game_state): + """ + Successfully complete a quest. + + @param game_state: variables and functions that make up the current + game state. + @type game_state: dict of objects + """ + quest_id = self.quest_id + print("You've finished the \"{0}\" quest".format(quest_id)) + game_state['quest'].finishQuest(quest_id) +DialogueAction.registerAction(CompleteQuestAction) + + +class FailQuestAction(QuestAction): + """L{QuestAction} used to fail an active quest.""" + keyword = 'fail_quest' + + def __call__(self, game_state): + """ + Fail an active quest. + + @param game_state: variables and functions that make up the current + game state. + @type game_state: dict of objects + """ + quest_id = self.quest_id + print("You've failed the \"{0}\" quest".format(quest_id)) + game_state['quest'].failQuest(quest_id) +DialogueAction.registerAction(FailQuestAction) + + +class RestartQuestAction(QuestAction): + """L{QuestAction} used to restart an active quest.""" + keyword = 'restart_quest' + + def __call__(self, game_state): + """ + Restart an active quest. + + @param game_state: variables and functions that make up the current + game state. + @type game_state: dict of objects + """ + quest_id = self.quest_id + print("You've restarted the \"{0}\" quest".format(quest_id)) + game_state['quest'].restartQuest(quest_id) +DialogueAction.registerAction(RestartQuestAction) + + +class QuestVariableAction(QuestAction): + """ + Base class for L{QuestActions} that modify quest + variables. + """ + def __init__(self, *args, **kwargs): + """ + Initialize a new L{QuestVariableAction} instance. + + @param args: positional arguments (not used). + @type args: list of objects + @param kwargs: keyword arguments. + @type kwargs: dict of objects + @keyword quest: ID of the quest whose variable should be modified. + @type quest: basestring + @keyword variable: name of the quest variable to modify. + @type variable: basestring + @keyword value: new value that should be used to modify the quest + variable. + @type value: object + """ + QuestAction.__init__(self, *args, **kwargs) + self.variable_name = kwargs['variable'] + self.value = kwargs['value'] + + +class IncreaseQuestVariableAction(QuestVariableAction): + """ + L{QuestVariableAction} used to increase the value of a quest variable by a + set amount. + """ + keyword = 'increase_quest_variable' + + def __call__(self, game_state): + """ + Increase a quest variable by a set amount. + + @param game_state: variables and functions that make up the current + game state. + @type game_state: dict of objects + """ + quest_id = self.quest_id + variable_name = self.variable_name + value = self.value + print('Increased {0} by {1}'.format(variable_name, value)) + game_state['quest'][quest_id].increaseValue(variable_name, value) +DialogueAction.registerAction(IncreaseQuestVariableAction) + + +class DecreaseQuestVariableAction(QuestVariableAction): + """ + L{QuestVariableAction} used to decrease the value of a quest variable by a + set amount. + """ + keyword = 'decrease_quest_variable' + + def __call__(self, game_state): + """ + Decrease a quest variable by a set amount. + + @param game_state: variables and functions that make up the current + game state. + @type game_state: dict of objects + """ + quest_id = self.quest_id + variable_name = self.variable_name + value = self.value + print('Decreased {0} by {1}'.format(variable_name, value)) + game_state['quest'][quest_id].decreaseValue(variable_name, value) +DialogueAction.registerAction(DecreaseQuestVariableAction) + + +class SetQuestVariableAction(QuestVariableAction): + """ + L{QuestVariableAction} used to set the value of a quest variable. + """ + keyword = 'set_quest_variable' + + def __call__(self, game_state): + """ + Set the value of a quest variable. + + @param game_state: variables and functions that make up the current + game state. + @type game_state: dict of objects + """ + quest_id = self.quest_id + variable_name = self.variable_name + value = self.value + print('Set {0} to {1}'.format(variable_name, value)) + game_state['quest'][quest_id].setValue(variable_name, value) +DialogueAction.registerAction(SetQuestVariableAction) diff -r 000000000000 -r 7a89ea5404b1 dialoguecontroller.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dialoguecontroller.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,59 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . +from controllerbase import ControllerBase + +class DialogueController(ControllerBase): + """Controller that takes over when a dialogue is started""" + def __init__(self, + engine, + view, + model, + application): + """ + Constructor + @param engine: Instance of the active fife engine + @type engine: fife.Engine + @param view: Instance of a GameSceneView + @param type: parpg.GameSceneView + @param model: The model that has the current gamestate + @type model: parpg.GameModel + @param application: The application that created this controller + @type application: parpg.PARPGApplication + @param settings: The current settings of the application + @type settings: fife.extensions.fife_settings.Setting + """ + super(DialogueController, self).__init__(engine, + view, + model, + application) + self.dialogue = None + self.view = view + + def startTalk(self, npc): + if npc.dialogue is not None: + self.model.active_map.centerCameraOnPlayer() + npc.talk(self.model.game_state.player_character) + self.dialogue = self.view.hud.showDialogue(npc) + self.dialogue.initiateDialogue() + self.model.pause(True) + self.view.hud.enabled = False + + + def pump(self): + if self.dialogue and not self.dialogue.active: + self.application.popController() + self.model.pause(False) + self.view.hud.enabled = True + diff -r 000000000000 -r 7a89ea5404b1 dialogueparsers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dialogueparsers.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,669 @@ +# This file is part of PARPG. +# +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . +""" +Contains classes for parsing and validating L{Dialogues} and other +dialogue-related data. + +@TODO Technomage 2010-11-13: Exception handling + validation needs work. + Currently YAML files are only crudely validated - the code assumes that + the file contains valid dialogue data, and if that assumption is + violated and causes the code to raise any TypeErrors, AttributeErrors or + ValueErrors the code then raises a DialogueFormatError with the + original (and mostly unhelpful) error message. +@TODO Technomage 2010-11-13: Support reading and writing unicode. +""" +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO +from collections import Sequence +try: + from collections import OrderedDict +except ImportError: + # Python version 2.4-2.6 doesn't have the OrderedDict + from parpg.common.ordereddict import OrderedDict +import re +import textwrap + +import yaml + +from parpg import COPYRIGHT_HEADER +from parpg.dialogue import (Dialogue, DialogueSection, DialogueResponse, + DialogueGreeting) +from parpg.dialogueactions import DialogueAction + +import logging +logger = logging.getLogger('dialogueparser') + +class DialogueFormatError(Exception): + """Exception thrown when the DialogueParser has encountered an error.""" + + +class AbstractDialogueParser(object): + """ + Abstract base class defining the interface for parsers responsible for + constructing a L{Dialogue} from its serialized representation. + """ + def load(self, stream): + """ + Parse a stream and attempt to construct a new L{Dialogue} instance from + its serialized representation. + + @param stream: open stream containing the serialized representation of + a Dialogue. + @type stream: BufferType + """ + raise NotImplementedError('AbstractDialogueParser subclasses must ' + 'override the load method.') + + def dump(self, dialogue, stream): + """ + Serialize a L{Dialogue} instance and dump it to an open stream. + + @param dialogue: dialogue to serialize. + @type dialogue: L{Dialogue} + @param stream: open stream into which the serialized L{Dialogue} should + be dumped. + @type stream: BufferType + """ + raise NotImplementedError('AbstractDialogueParser subclasses must ' + 'override the dump method.') + + def validate(self, stream): + """ + Parse a stream and verify that it contains a valid serialization of a + L{Dialogue instance}. + + @param stream: stream containing the serialized representation of a + L{Dialogue} + @type stream: BufferType + """ + raise NotImplementedError('AbstractDialogueParser subclasses must ' + 'override the validate method.') + + +class YamlDialogueParser(AbstractDialogueParser): + """ + L{AbstractDialogueParser} subclass responsible for parsing dialogues + serialized in YAML. + """ + logger = logging.getLogger('dialogueparser.OldYamlDialogueParser') + + def load(self, stream, loader_class=yaml.Loader): + """ + Parse a YAML stream and attempt to construct a new L{Dialogue} + instance. + + @param stream: stream containing the serialized YAML representation of + a L{Dialogue}. + @type stream: BufferType + @param loader_class: PyYAML loader class to use for reading the + serialization. + @type loader_class: yaml.BaseLoader subclass + """ + loader = loader_class(stream) + try: + dialogue = \ + self._constructDialogue(loader, loader.get_single_node()) + except (AssertionError,) as error: + raise DialogueFormatError(str(error)) + return dialogue + + def dump(self, dialogue, output_stream, dumper_class=yaml.Dumper): + """ + Serialize a L{Dialogue} instance as YAML and dump it to an open stream. + + @param dialogue: dialogue to serialize. + @type dialogue: L{Dialogue} + @param stream: open stream into which the serialized L{Dialogue} should + be dumped. + @type stream: BufferType + @param dumper_class: PyYAML dumper class to use for formatting the + serialization. + @type dumper_class: yaml.BaseDumper subclass + """ + intermediate_stream = StringIO() + # KLUDE Technomage 2010-11-16: The "width" argument seems to be broken, + # as it doesn't take into about current line indentation and fails + # to correctly wrap at word boundaries. + dumper = dumper_class(intermediate_stream, default_flow_style=False, + indent=4, width=99999, line_break='\n', + allow_unicode=True, explicit_start=True, + explicit_end=True, tags=False) + dialogue_node = self._representDialogue(dumper, dialogue) + dumper.open() + dumper.serialize(dialogue_node) + dumper.close() + file_contents = intermediate_stream.getvalue() + + file_contents = re.sub(r'(\n|\r|\r\n)(\s*)(GOTO: .*)', r'\1\2\3\1\2', + file_contents) + lines = file_contents.splitlines() + max_line_length = 76 # 79 - 3 chars for escaping newlines + for i in range(len(lines)): + line = lines[i] + match = re.match( + r'^(\s*(?:-\s+)?)(SAY|REPLY|CONDITION):\s+"(.*)"$', + line + ) + if (match and len(line) > max_line_length): + # Wrap long lines for readability. + initial_indent = len(match.group(1)) + subsequent_indent = initial_indent + 4 + text_wrapper = textwrap.TextWrapper( + max_line_length, + subsequent_indent=' ' * subsequent_indent, + break_long_words=False, + break_on_hyphens=False + ) + new_lines = text_wrapper.wrap(line) + new_lines = ( + new_lines[:1] + [re.sub(r'^(\s*) (.*)$', r'\1\ \2', l) + for l in new_lines[1:]] + ) + lines[i] = '\\\n'.join(new_lines) + + output_stream.write(COPYRIGHT_HEADER) + output_stream.write('\n'.join(lines)) + + + def _representDialogue(self, dumper, dialogue): + dialogue_node = dumper.represent_dict({}) + dialogue_dict = OrderedDict() + dialogue_dict['NPC_NAME'] = dialogue.npc_name + dialogue_dict['AVATAR_PATH'] = dialogue.avatar_path + dialogue_dict['DEFAULT_GREETING'] = \ + self._representDialogueSection(dumper, + dialogue.default_greeting) + # NOTE Technomage 2010-11-16: Dialogue stores its sections in an + # OrderedDict, so a round-trip load, dump, and load will preserve + # the order of DialogueSections. + if (len(dialogue.greetings) > 0): + greetings_list_node = dumper.represent_list([]) + greetings_list = greetings_list_node.value + for greeting in dialogue.greetings: + greeting_node = \ + self._representRootDialogueSection(dumper, greeting) + greetings_list.append(greeting_node) + dialogue_dict['GREETINGS'] = greetings_list_node + if (len(dialogue.setions) > 0): + sections_list_node = dumper.represent_list([]) + sections_list = sections_list_node.value + for section in dialogue.sections.values(): + section_node = self._representDialogueSection(dumper, section) + sections_list.append(section_node) + dialogue_dict['SECTIONS'] = sections_list_node + + for key, value in dialogue_dict.items(): + if (isinstance(key, yaml.Node)): + key_node = key + else: + key_node = dumper.represent_data(key) + if (isinstance(value, yaml.Node)): + value_node = value + else: + value_node = dumper.represent_data(value) + dialogue_node.value.append((key_node, value_node)) + return dialogue_node + + def _representRootDialogueSection(self, dumper, greeting): + greeting_node = dumper.represent_dict({}) + greeting_dict = OrderedDict() + greeting_dict['ID'] = greeting.id + greeting_dict['CONDITION'] = dumper.represent_scalar( + 'tag:yaml.org,2002:str', + greeting.condition, + style='"' + ) + for key, value in greeting_dict.items(): + if (isinstance(key, yaml.Node)): + key_node = key + else: + key_node = dumper.represent_data(key) + if (isinstance(value, yaml.Node)): + value_node = value + else: + value_node = dumper.represent_data(value) + greeting_node.value.append((key_node, value_node)) + return greeting_node + + def _representDialogueSection(self, dumper, dialogue_section): + section_node = dumper.represent_dict({}) + section_dict = OrderedDict() # OrderedDict is required to preserve + # the order of attributes. + section_dict['ID'] = dialogue_section.id + # KLUDGE Technomage 2010-11-16: Hard-coding the tag like this could be + # a problem when writing unicode. + section_dict['SAY'] = dumper.represent_scalar('tag:yaml.org,2002:str', + dialogue_section.text, + style='"') + actions_list_node = dumper.represent_list([]) + actions_list = actions_list_node.value + for action in dialogue_section.actions: + action_node = self._representDialogueAction(dumper, action) + actions_list.append(action_node) + if (actions_list): + section_dict['ACTIONS'] = actions_list_node + responses_list_node = dumper.represent_list([]) + responses_list = responses_list_node.value + for response in dialogue_section.responses: + response_node = self._representDialogueResponse(dumper, response) + responses_list.append(response_node) + section_dict['RESPONSES'] = responses_list_node + + for key, value in section_dict.items(): + if (isinstance(key, yaml.Node)): + key_node = key + else: + key_node = dumper.represent_data(key) + if (isinstance(value, yaml.Node)): + value_node = value + else: + value_node = dumper.represent_data(value) + section_node.value.append((key_node, value_node)) + return section_node + + def _representDialogueResponse(self, dumper, dialogue_response): + response_node = dumper.represent_dict({}) + response_dict = OrderedDict() + # KLUDGE Technomage 2010-11-16: Hard-coding the tag like this could be + # a problem when writing unicode. + response_dict['REPLY'] = dumper.represent_scalar( + 'tag:yaml.org,2002:str', + dialogue_response.text, + style='"') + if (dialogue_response.condition is not None): + response_dict['CONDITION'] = dumper.represent_scalar( + 'tag:yaml.org,2002:str', + dialogue_response.condition, + style='"' + ) + actions_list_node = dumper.represent_list([]) + actions_list = actions_list_node.value + for action in dialogue_response.actions: + action_node = self._representDialogueAction(dumper, action) + actions_list.append(action_node) + if (actions_list): + response_dict['ACTIONS'] = actions_list_node + response_dict['GOTO'] = dialogue_response.next_section_id + + for key, value in response_dict.items(): + if (isinstance(key, yaml.Node)): + key_node = key + else: + key_node = dumper.represent_data(key) + if (isinstance(value, yaml.Node)): + value_node = value + else: + value_node = dumper.represent_data(value) + response_node.value.append((key_node, value_node)) + return response_node + + def _representDialogueAction(self, dumper, dialogue_action): + action_node = dumper.represent_dict({}) + action_dict = OrderedDict() + args, kwargs = dialogue_action.arguments + if (args and not kwargs): + arguments = list(args) + elif (kwargs and not args): + arguments = kwargs + else: + arguments = [list(args), kwargs] + action_dict[dialogue_action.keyword] = arguments + + for key, value in action_dict.items(): + if (isinstance(key, yaml.Node)): + key_node = key + else: + key_node = dumper.represent_data(key) + if (isinstance(value, yaml.Node)): + value_node = value + else: + value_node = dumper.represent_data(value) + action_node.value.append((key_node, value_node)) + return action_node + + def _constructDialogue(self, loader, yaml_node): + npc_name = None + avatar_path = None + default_greeting = None + greetings = [] + sections = [] + + try: + for key_node, value_node in yaml_node.value: + key = key_node.value + if (key == u'NPC_NAME'): + npc_name = loader.construct_object(value_node) + elif (key == u'AVATAR_PATH'): + avatar_path = loader.construct_object(value_node) + elif (key == u'DEFAULT_GREETING'): + default_greeting = \ + self._constructDialogueSection(loader, value_node) + elif (key == u'GREETINGS'): + for greeting_node in value_node.value: + greeting = self._constructRootDialogueSection( + loader, + greeting_node + ) + greetings.append( + greeting + ) + elif (key == u'SECTIONS'): + for section_node in value_node.value: + dialogue_section = self._constructDialogueSection( + loader, + section_node + ) + sections.append(dialogue_section) + except (AttributeError, TypeError, ValueError) as e: + raise DialogueFormatError(e) + + dialogue = Dialogue(npc_name=npc_name, avatar_path=avatar_path, + default_greeting=default_greeting, + greetings=greetings, + sections=sections) + return dialogue + + def _constructRootDialogueSection(self, loader, greeting_node): + id = None + text = None + condition = None + responses = [] + actions = [] + greeting = None + + try: + for key_node, value_node in greeting_node.value: + key = key_node.value + if (key == u'ID'): + id = loader.construct_object(value_node) + elif (key == u'SAY'): + text = loader.construct_object(value_node) + elif (key == u'CONDITION'): + condition = loader.construct_object(value_node) + elif (key == u'RESPONSES'): + for response_node in value_node.value: + dialogue_response = self._constructDialogueResponse( + loader, + response_node + ) + responses.append(dialogue_response) + elif (key == u'ACTIONS'): + for action_node in value_node.value: + action = self._constructDialogueAction(loader, + action_node) + actions.append(action) + except (AttributeError, TypeError, ValueError) as e: + raise DialogueFormatError(e) + else: + greeting = DialogueSection(id=id, text=text, + condition=condition, + responses=responses, + actions=actions) + + return greeting + + def _constructDialogueSection(self, loader, section_node): + id_ = None + text = None + responses = [] + actions = [] + dialogue_section = None + + try: + for key_node, value_node in section_node.value: + key = key_node.value + if (key == u'ID'): + id_ = loader.construct_object(value_node) + elif (key == u'SAY'): + text = loader.construct_object(value_node) + elif (key == u'RESPONSES'): + for response_node in value_node.value: + dialogue_response = self._constructDialogueResponse( + loader, + response_node + ) + responses.append(dialogue_response) + elif (key == u'ACTIONS'): + for action_node in value_node.value: + action = self._constructDialogueAction(loader, + action_node) + actions.append(action) + except (AttributeError, TypeError, ValueError) as e: + raise DialogueFormatError(e) + else: + dialogue_section = DialogueSection(id_=id_, text=text, + responses=responses, + actions=actions) + + return dialogue_section + + def _constructDialogueResponse(self, loader, response_node): + text = None + next_section_id = None + actions = [] + condition = None + + try: + for key_node, value_node in response_node.value: + key = key_node.value + if (key == u'REPLY'): + text = loader.construct_object(value_node) + elif (key == u'ACTIONS'): + for action_node in value_node.value: + action = self._constructDialogueAction(loader, + action_node) + actions.append(action) + elif (key == u'CONDITION'): + condition = loader.construct_object(value_node) + elif (key == u'GOTO'): + next_section_id = loader.construct_object(value_node) + except (AttributeError, TypeError, ValueError) as e: + raise DialogueFormatError(e) + + dialogue_response = DialogueResponse(text=text, + next_section_id=next_section_id, + actions=actions, + condition=condition) + return dialogue_response + + def _constructDialogueAction(self, loader, action_node): + mapping = loader.construct_mapping(action_node, deep=True) + keyword, arguments = mapping.items()[0] + if (isinstance(arguments, dict)): + # Got a dictionary of keyword arguments. + args = () + kwargs = arguments + elif (not isinstance(arguments, Sequence) or + isinstance(arguments, basestring)): + # Got a single positional argument. + args = (arguments,) + kwargs = {} + elif (not len(arguments) == 2 or not isinstance(arguments[1], dict)): + # Got a list of positional arguments. + args = arguments + kwargs = {} + else: + self.logger.error( + '{0} is an invalid DialogueAction argument'.format(arguments) + ) + return None + + action_type = DialogueAction.registered_actions.get(keyword) + if (action_type is None): + self.logger.error( + 'no DialogueAction with keyword "{0}"'.format(keyword) + ) + dialogue_action = None + else: + dialogue_action = action_type(*args, **kwargs) + return dialogue_action + + +class OldYamlDialogueParser(YamlDialogueParser): + """ + L{YAMLDialogueParser} that can read and write dialogues in the old + Techdemo1 dialogue file format. + + @warning: This class is deprecated and likely to be removed in a future + version. + """ + logger = logging.getLogger('dialogueparser.OldYamlDialogueParser') + + def __init__(self): + self.response_actions = {} + + def load(self, stream): + dialogue = YamlDialogueParser.load(self, stream) + # Place all DialogueActions that were in DialogueSections into the + # DialogueResponse that led to the action's original section. + for section in dialogue.sections.values(): + for response in section.responses: + actions = self.response_actions.get(response.next_section_id) + if (actions is not None): + response.actions = actions + return dialogue + + def _constructDialogue(self, loader, yaml_node): + npc_name = None + avatar_path = None + start_section_id = None + sections = [] + + try: + for key_node, value_node in yaml_node.value: + key = key_node.value + if (key == u'NPC'): + npc_name = loader.construct_object(value_node) + elif (key == u'AVATAR'): + avatar_path = loader.construct_object(value_node) + elif (key == u'START'): + start_section_id = loader.construct_object(value_node) + elif (key == u'SECTIONS'): + for id_node, section_node in value_node.value: + dialogue_section = self._constructDialogueSection( + loader, + id_node, + section_node + ) + sections.append(dialogue_section) + except (AttributeError, TypeError, ValueError) as e: + raise DialogueFormatError(e) + + dialogue = Dialogue(npc_name=npc_name, avatar_path=avatar_path, + start_section_id=start_section_id, + sections=sections) + return dialogue + + def _constructDialogueSection(self, loader, id_node, section_node): + id = loader.construct_object(id_node) + text = None + responses = [] + actions = [] + dialogue_section = None + + try: + for node in section_node.value: + key_node, value_node = node.value[0] + key = key_node.value + if (key == u'say'): + text = loader.construct_object(value_node) + elif (key == u'meet'): + action = self._constructDialogueAction(loader, node) + actions.append(action) + elif (key in [u'start_quest', u'complete_quest', u'fail_quest', + u'restart_quest', u'set_value', + u'decrease_value', u'increase_value', + u'give_stuff', u'get_stuff']): + action = self._constructDialogueAction(loader, node) + if (id not in self.response_actions.keys()): + self.response_actions[id] = [] + self.response_actions[id].append(action) + elif (key == u'responses'): + for response_node in value_node.value: + dialogue_response = self._constructDialogueResponse( + loader, + response_node + ) + responses.append(dialogue_response) + except (AttributeError, TypeError, ValueError) as e: + raise DialogueFormatError(e) + else: + dialogue_section = DialogueSection(id=id, text=text, + responses=responses, + actions=actions) + + return dialogue_section + + def _constructDialogueResponse(self, loader, response_node): + text = None + next_section_id = None + actions = [] + condition = None + + try: + text = loader.construct_object(response_node.value[0]) + next_section_id = loader.construct_object(response_node.value[1]) + if (len(response_node.value) == 3): + condition = loader.construct_object(response_node.value[2]) + except (AttributeError, TypeError, ValueError) as e: + raise DialogueFormatError(e) + + dialogue_response = DialogueResponse(text=text, + next_section_id=next_section_id, + actions=actions, + condition=condition) + return dialogue_response + + def _constructDialogueAction(self, loader, action_node): + mapping = loader.construct_mapping(action_node, deep=True) + keyword, arguments = mapping.items()[0] + if (keyword == 'get_stuff'): + # Renamed keyword in new syntax. + keyword = 'take_stuff' + elif (keyword == 'set_value'): + keyword = 'set_quest_value' + elif (keyword == 'increase_value'): + keyword = 'increase_quest_value' + elif (keyword == 'decrease_value'): + keyword = 'decrease_quest_value' + if (isinstance(arguments, dict)): + # Got a dictionary of keyword arguments. + args = () + kwargs = arguments + elif (not isinstance(arguments, Sequence) or + isinstance(arguments, basestring)): + # Got a single positional argument. + args = (arguments,) + kwargs = {} + elif (not len(arguments) == 2 or not isinstance(arguments[1], dict)): + # Got a list of positional arguments. + args = arguments + kwargs = {} + else: + self.logger.error( + '{0} is an invalid DialogueAction argument'.format(arguments) + ) + return None + action_type = DialogueAction.registered_actions.get(keyword) + if (action_type is None): + self.logger.error( + 'no DialogueAction with keyword "{0}"'.format(keyword) + ) + dialogue_action = None + else: + dialogue_action = action_type(*args, **kwargs) + return dialogue_action diff -r 000000000000 -r 7a89ea5404b1 dialogueprocessor.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dialogueprocessor.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,378 @@ +# This file is part of PARPG. +# +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . +""" +Provides the core interface to the dialogue subsystem used to process player +L{Dialogues} with NPCs. +""" +import logging + +from parpg.common.utils import dedent_chomp + +if (__debug__): + from collections import Sequence, MutableMapping + from parpg.dialogue import Dialogue + +logger = logging.getLogger('dialogueprocessor') + +class DialogueProcessor(object): + """ + Primary interface to the dialogue subsystem used to initiate and process a + L{Dialogue} with an NPC. + + To begin a dialogue with an NPC a L{DialogueProcessor} must first be + instantiated with the dialogue data to process and a dictionary of Python + objects defining the game state for testing of response conditionals. The + L{initiateDialogue} must be called to initialized the L{DialogueProcessor}, + and once it is initialized processing of + L{DialogueSections} and + L{DialogueResponses} can be initiated via the + L{continueDialogue} and L{reply} class methods. + + The state of dialogue processing is stored via the + L{dialogue_section_stack} class attribute, which stores a list of + L{DialogueSections} that have been or are currently being + processed. Each time L{reply} is called with a L{DialogueResponse} its + next_section_id attribute is used to select a new L{DialogueSection} from + the L{dialogue}. The selected L{DialogueSection} is then pushed + onto the end of the L{dialogue_section_stack}, ready to be processed via + L{continueDialogue}. The exception to this rule occurs when L{reply} is + called with a L{DialogueResponse} whose next_section_id attribute is "end" + or "back". "end" terminates the dialogue as described below, while "back" + removes the last L{DialogueSection} on the L{dialogue_section_stack} + effectively going back to the previous section of dialogue. + + The L{DialogueProcessor} terminates dialogue processing once L{reply} is + called with a L{DialogueResponse} whose next_section_id == 'end'. + Processing can also be manually terminated by calling the L{endDialogue} + class method. + + @note: See the dialogue_demo.py script for a complete example of how the + L{DialogueProcessor} can be used. + + @ivar dialogue: dialogue data currently being processed. + @type dialogue: L{Dialogue} + @ivar dialogue_section_stack: sections of dialogue that have been or are + currently being processed. + @type dialogue_section_stack: list of L{DialogueSections} + @ivar game_state: objects defining the game state that should be made + available for testing L{DialogueResponse} conditionals. + @type game_state: dict of Python objects + @ivar in_dialogue: whether a dialogue has been initiated. + @type in_dialogue: Bool + + Usage: + >>> game_state = {'pc': player_character, 'quest': quest_engine} + >>> dialogue_processor = DialogueProcessor(dialogue, game_state) + >>> dialogue_processor.initiateDialogue() + >>> while dialogue_processor.in_dialogue: + ... valid_responses = dialogue_processor.continueDialogue() + ... response = choose_response(valid_responses) + ... dialogue_processor.reply(response) + """ + _logger = logging.getLogger('dialogueengine.DialogueProcessor') + + def dialogue(): + def fget(self): + return self._dialogue + + def fset(self, dialogue): + assert isinstance(dialogue, Dialogue), \ + '{0} does not implement Dialogue interface'.format(dialogue) + self._dialogue = dialogue + + return locals() + dialogue = property(**dialogue()) + + def dialogue_section_stack(): + def fget(self): + return self._dialogue_section_stack + + def fset(self, new_value): + assert isinstance(new_value, Sequence) and not \ + isinstance(new_value, basestring), \ + 'dialogue_section_stack must be a Sequence, not {0}'\ + .format(new_value) + self._dialogue_section_stack = new_value + + return locals() + dialogue_section_stack = property(**dialogue_section_stack()) + + def game_state(): + def fget(self): + return self._game_state + + def fset(self, new_value): + assert isinstance(new_value, MutableMapping),\ + 'game_state must be a MutableMapping, not {0}'\ + .format(new_value) + self._game_state = new_value + + return locals() + game_state = property(**game_state()) + + def in_dialogue(): + def fget(self): + return self._in_dialogue + + def fset(self, value): + assert isinstance(value, bool), '{0} is not a bool'.format(value) + self._in_dialogue = value + + return locals() + in_dialogue = property(**in_dialogue()) + + def __init__(self, dialogue, game_state): + """ + Initialize a new L{DialogueProcessor} instance. + + @param dialogue: dialogue data to process. + @type dialogue: L{Dialogue} + @param game_state: objects defining the game state that should be made + available for testing L{DialogueResponse} conditions. + @type game_state: dict of objects + """ + self._dialogue_section_stack = [] + self._dialogue = dialogue + self._game_state = game_state + self._in_dialogue = False + + def getDialogueGreeting(self): + """ + Evaluate the L{RootDialogueSections} conditions + and return the valid L{DialogueSection} which should be displayed + first. + + @return: Valid root dialogue section. + @rtype: L{DialogueSection} + + @raise: RuntimeError - evaluation of a DialogueGreeting condition fails + by raising an exception (e.g. due to a syntax error). + """ + dialogue = self.dialogue + dialogue_greeting = None + for greeting in dialogue.greetings: + try: + condition_met = eval(greeting.condition, self.game_state) + except Exception as exception: + error_message = dedent_chomp(''' + exception raised in DialogueGreeting {id} condition: + {exception} + ''').format(id=greeting.id, exception=exception) + self._logger.error(error_message) + if (condition_met): + dialogue_greeting = greeting + if (dialogue_greeting is None): + dialogue_greeting = dialogue.default_greeting + + return dialogue_greeting + + def initiateDialogue(self): + """ + Prepare the L{DialogueProcessor} to process the L{Dialogue} by pushing + the starting L{DialogueSection} onto the L{dialogue_section_stack}. + + @raise RuntimeError: Unable to determine the root L{DialogueSection} + defined by the L{Dialogue}. + """ + if (self.in_dialogue): + self.endDialogue() + dialogue_greeting = self.getDialogueGreeting() + self.dialogue_section_stack.append(dialogue_greeting) + self.in_dialogue = True + self._logger.info('initiated dialogue {0}'.format(self.dialogue)) + + def continueDialogue(self): + """ + Process the L{DialogueSection} at the top of the + L{dialogue_section_stack}, run any L{DialogueActions} + it contains and return a list of valid + L{DialogueResponses after evaluating any response + conditionals. + + @returns: valid responses. + @rtype: list of L{DialogueResponses} + + @raise RuntimeError: Any preconditions are not met. + + @precondition: dialogue has been initiated via L{initiateDialogue}. + """ + if (not self.in_dialogue): + error_message = dedent_chomp(''' + dialogue has not be initiated via initiateDialogue yet + ''') + raise RuntimeError(error_message) + current_dialogue_section = self.getCurrentDialogueSection() + self.runDialogueActions(current_dialogue_section) + valid_responses = self.getValidResponses(current_dialogue_section) + + return valid_responses + + def getCurrentDialogueSection(self): + """ + Return the L{DialogueSection} at the top of the + L{dialogue_section_stack}. + + @returns: section of dialogue currently being processed. + @rtype: L{DialogueSection} + + @raise RuntimeError: Any preconditions are not met. + + @precondition: dialogue has been initiated via L{initiateDialogue} and + L{dialogue_section_stack} contains at least one L{DialogueSection}. + """ + if (not self.in_dialogue): + error_message = dedent_chomp(''' + getCurrentDialogueSection called but the dialogue has not been + initiated yet + ''') + raise RuntimeError(error_message) + try: + current_dialogue_section = self.dialogue_section_stack[-1] + except IndexError: + error_message = dedent_chomp(''' + getCurrentDialogueSection called but no DialogueSections are in + the stack + ''') + raise RuntimeError(error_message) + + return current_dialogue_section + + def runDialogueActions(self, dialogue_node): + """ + Execute all L{DialogueActions} contained by a + L{DialogueSection} or L{DialogueResponse}. + + @param dialogue_node: section of dialogue or response containing the + L{DialogueActions} to execute. + @type dialogue_node: L{DialogueNode} + """ + self._logger.info('processing commands for {0}'.format(dialogue_node)) + for command in dialogue_node.actions: + try: + command(self.game_state) + except (Exception,) as error: + self._logger.error('failed to execute DialogueAction {0}: {1}' + .format(command.keyword, error)) + # TODO Technomage 2010-11-18: Undo previous actions when an + # action fails to execute. + else: + self._logger.debug('ran {0} with arguments {1}' + .format(getattr(type(command), '__name__'), + command.arguments)) + + def getValidResponses(self, dialogue_section): + """ + Evaluate all L{DialogueResponse} conditions for a L{DialogueSection} + and return a list of valid responses. + + @param dialogue_section: section of dialogue containing the + L{DialogueResponses} to process. + @type dialogue_section: L{DialogueSection} + + @return: responses whose conditions were met. + @rtype: list of L{DialogueResponses} + """ + valid_responses = [] + for dialogue_response in dialogue_section.responses: + condition = dialogue_response.condition + try: + condition_met = condition is None or \ + eval(condition, self.game_state) + except (Exception,) as exception: + error_message = dedent_chomp(''' + evaluation of condition {condition} for {response} failed + with error: {exception} + ''').format(condition=dialogue_response.condition, + response=dialogue_response, exception=exception) + self._logger.error(error_message) + else: + self._logger.debug( + 'condition "{0}" for {1} evaluated to {2}' + .format(dialogue_response.condition, dialogue_response, + condition_met) + ) + if (condition_met): + valid_responses.append(dialogue_response) + + return valid_responses + + def reply(self, dialogue_response): + """ + Reply with a L{DialogueResponse}, execute the + L{DialogueActions} it contains and push the next + L{DialogueSection} onto the L{dialogue_section_stack}. + + @param dialogue_response: response to reply with. + @type dialogue_response: L{DialogueReponse} + + @raise RuntimeError: Any precondition is not met. + + @precondition: L{initiateDialogue} must be called before this method + is used. + """ + if (not self.in_dialogue): + error_message = dedent_chomp(''' + reply cannot be called until the dialogue has been initiated + via initiateDialogue + ''') + raise RuntimeError(error_message) + self._logger.info('replied with {0}'.format(dialogue_response)) + # FIXME: Technomage 2010-12-11: What happens if runDialogueActions + # raises an error? + self.runDialogueActions(dialogue_response) + next_section_id = dialogue_response.next_section_id + if (next_section_id == 'back'): + if (len(self.dialogue_section_stack) == 1): + error_message = dedent_chomp(''' + attempted to run goto: back action but stack does not + contain a previous DialogueSection + ''') + raise RuntimeError(error_message) + else: + try: + self.dialogue_section_stack.pop() + except (IndexError,): + error_message = dedent_chomp(''' + attempted to run goto: back action but the stack was + empty + ''') + raise RuntimeError(error_message) + else: + self._logger.debug( + 'ran goto: back action, restored last DialogueSection' + ) + elif (next_section_id == 'end'): + self.endDialogue() + self._logger.debug('ran goto: end action, ended dialogue') + else: + try: + next_dialogue_section = \ + self.dialogue.sections[next_section_id] + except KeyError: + error_message = dedent_chomp(''' + {0} is not a recognized goto: action or DialogueSection + identifier + ''').format(next_section_id) + raise RuntimeError(error_message) + else: + self.dialogue_section_stack.append(next_dialogue_section) + + def endDialogue(self): + """ + End the current dialogue and clean up any resources in use by the + L{DialogueProcessor}. + """ + self.dialogue_section_stack = [] + self.in_dialogue = False diff -r 000000000000 -r 7a89ea5404b1 font.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/font.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,54 @@ +import os + +from fife.extensions.pychan.fonts import Font +from fife.extensions.pychan.internal import get_manager + +class PARPGFont(Font): + """ Font class for PARPG + This class behaves identical to PyChan's Font class except in + initialization. Ratherthan take a name and a get object, this class + takes a fontdef and settings object as explained below. This class is + necessary because the original Font class was too restrictive on how it + accepted objects + + @param fontdef: defines the font's name, size, type, and optionally + row spacing as well as glyph spacing. + @type fontdef: dictionary + + @param settings: settings object used to dynamically determine the + font's source location + @type settings: parpg.settings.Settings object + """ + def __init__(self, fontdef, settings): + self.font = None + self.name = fontdef['name'] + self.typename = fontdef['typename'] + + if self.typename == 'truetype': + self.filename = '{0}.ttf'.format(self.name.lower().split('_')[0]) + + self.source = os.path.join(settings.system_path, + settings.fife.FontsPath, + self.filename) + self.row_spacing = fontdef.get('row_spacing', 0) + self.glyph_spacing = fontdef.get('glyph_spacing', 0) + + if self.typename == 'truetype': + self.size = fontdef['size'] + self.antialias = fontdef['antialias'] + self.color = fontdef.get('color', [255, 255, 255]) + manager = get_manager().hook.engine.getGuiManager() + self.font = manager.createFont(self.source, self.size, '') + + if not self.font: + raise InitializationError('Could not load font ' + '{0}'.format(self.name)) + + self.font.setAntiAlias(self.antialias) + self.font.setColor(*self.color) + else: + raise InitializationError('Unsupported font type ' + '{0}'.format(self.typename)) + + self.font.setRowSpacing(self.row_spacing) + self.font.setGlyphSpacing(self.glyph_spacing) diff -r 000000000000 -r 7a89ea5404b1 gamemap.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gamemap.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,167 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +from fife import fife + +from fife.extensions.loaders import loadMapFile + +class GameMap(fife.MapChangeListener): + """Map class used to flag changes in the map""" + def __init__(self, engine, model): + # init mapchange listener + fife.MapChangeListener.__init__(self) + self.map = None + self.engine = engine + self.model = model + + # init map attributes + self.my_cam_id = None + self.cameras = {} + self.agent_layer = None + self.top_layer = None + self.fife_model = engine.getModel() + self.transitions = [] + self.cur_cam2_x = 0 + self.initial_cam2_x = 0 + self.cam2_scrolling_right = True + self.target_rotation = 0 + self.outline_renderer = None + + def reset(self): + """Reset the model to default settings. + @return: None""" + # We have to delete the map in Fife. + if self.map: + self.model.deleteObjects() + self.model.deleteMap(self.map) + self.transitions = [] + self.map = None + self.agent_layer = None + self.top_layer = None + # We have to clear the cameras in the view as well, or we can't reuse + # camera names like 'main' + #self.view.clearCameras() + self.initial_cam2_x = 0 + self.cam2_scrolling_right = True + #self.cameras = {} + self.cur_cam2_x = 0 + self.target_rotation = 0 + self.outline_renderer = None + + def makeActive(self): + """Makes this map the active one. + @return: None""" + self.cameras[self.my_cam_id].setEnabled(True) + + def load(self, filename): + """Load a map given the filename. + @type filename: String + @param filename: Name of map to load + @return: None""" + self.reset() + self.map = loadMapFile(filename, self.engine) + self.agent_layer = self.map.getLayer('ObjectLayer') + self.top_layer = self.map.getLayer('TopLayer') + + # it's possible there's no transition layer + size = len('TransitionLayer') + for layer in self.map.getLayers(): + # could be many layers, but hopefully no more than 3 + if(layer.getId()[:size] == 'TransitionLayer'): + self.transitions.append(self.map.getLayer(layer.getId())) + + """ Initialize the camera. + Note that if we have more than one camera in a map file + we will have to rework how self.my_cam_id works. To make sure + the proper camera is set as the 'main' camera. + At this point we also set the viewport to the current resolution.""" + for cam in self.map.getCameras(): + width = self.model.settings.fife.ScreenWidth + height = self.model.settings.fife.ScreenHeight + viewport = fife.Rect(0, 0, width, height) + cam.setViewPort(viewport) + self.my_cam_id = cam.getId() + self.cameras[self.my_cam_id] = cam + cam.resetRenderers() + + self.target_rotation = self.cameras[self.my_cam_id].getRotation() + + self.outline_renderer = fife.InstanceRenderer.\ + getInstance( + self.cameras[ + self.my_cam_id + ]) + + # set the render text + rend = fife.FloatingTextRenderer.getInstance(self.cameras[ + self.my_cam_id + ]) + text = self.engine.getGuiManager().\ + createFont('fonts/rpgfont.png', 0, \ + self.model.settings.fife.FontGlyphs) + rend.changeDefaultFont(text) + rend.activateAllLayers(self.map) + rend.setEnabled(True) + + # Activate the grid renderer on all layers + rend = self.cameras['map_camera'].getRenderer('GridRenderer') + rend.activateAllLayers(self.map) + + # Activate the grid renderer on all layers + rend = fife.CoordinateRenderer.getInstance(self.cameras[ + self.my_cam_id + ]) + rend.setColor(0, 0, 0) + rend.addActiveLayer(self.map.getLayer("GroundLayer")) + + # Make World aware that this is now the active map. + self.model.active_map = self + + def addPC(self): + """Add the player character to the map + @return: None""" + # Update gamestate.player_character + self.model.game_state.player_character.behaviour.onNewMap(self.agent_layer) + self.centerCameraOnPlayer() + + def toggleRenderer(self, r_name): + """Enable or disable a renderer. + @return: None""" + renderer = self.cameras[self.my_cam_id].getRenderer(str(r_name)) + renderer.setEnabled(not renderer.isEnabled()) + + def isPaused(self): + """Returns wheter the map is currentply paused or not""" + # Time multiplier is a float, never do equals on floats + return not self.map.getTimeMultiplier() >= 1.0 + + def pause(self, paused): + """ Pause/Unpause the game. + @return: nothing""" + if paused: + self.map.setTimeMultiplier(0.0) + if not paused and self.isPaused(): + self.map.setTimeMultiplier(1.0) + + def togglePause(self): + """ Toggle paused state. + @return: nothing""" + self.pause(not self.isPaused()) + + def centerCameraOnPlayer(self): + """Center the camera on the player""" + camera = self.cameras[self.my_cam_id] + player_agent = self.model.game_state.player_character.behaviour.agent + camera.setLocation(player_agent.getLocation()) diff -r 000000000000 -r 7a89ea5404b1 gamemodel.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gamemodel.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,775 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +# there should be NO references to FIFE here! +import sys +import os.path +import logging +from copy import deepcopy + +from fife import fife +from fife.extensions.serializers.xmlobject import XMLObjectLoader + +from gamestate import GameState +from objects import createObject +from objects.composed import CarryableItem, CarryableContainer +from gamemap import GameMap +from common.utils import locateFiles +from common.utils import parseBool +from inventory import Inventory +from parpg.dialogueparsers import YamlDialogueParser, DialogueFormatError + +try: + import xml.etree.cElementTree as ElementTree +except ImportError: + import xml.etree.ElementTree as ElementTree + +import yaml + +logger = logging.getLogger('gamemodel') + +class GameModel(object): + """GameModel holds the logic for the game. + Since some data (object position and so forth) is held in the + fife, and would be pointless to replicate, we hold a instance of + the fife view here. This also prevents us from just having a + function heavy controller.""" + ALL_AGENTS_KEY = "All" + MAX_ID_NUMBER = 1000 + + def __init__(self, engine, settings): + """Initialize the instance. + @param engine: A fife.Engine object + @type emgome: fife.Engine + @param setting: The applications settigns + @type setting: parpg.settings.Settings object + @return: None""" + self.settings = settings + + self.map_change = False + self.load_saver = False + self.savegame = None + quests_directory = os.path.join(self.settings.system_path, + self.settings.parpg.QuestsPath) + self.game_state = GameState(quests_dir=quests_directory) + #self.game_state.quest_engine = + #self.game_state.quest_engine.readQuests() + self.pc_run = 1 + self.target_position = None + self.target_map_name = None + self.object_db = {} + self.active_map = None + self.map_files = {} + self.agents = {} + self.agents[self.ALL_AGENTS_KEY] = {} + self.engine = engine + self.fife_model = engine.getModel() + + # set values from settings + maps_file = os.path.join(self.settings.system_path, + self.settings.parpg.MapsPath, + self.settings.parpg.MapsFile) + self.game_state.maps_file = maps_file + all_agents_file = os.path.join(self.settings.system_path, + self.settings.parpg.MapsPath, + self.settings.parpg.AllAgentsFile) + self.all_agents_file = all_agents_file + objects_dir = os.path.join(self.settings.system_path, + self.settings.parpg.ObjectsPath) + self.objects_directory = objects_dir + object_db_file = os.path.join(self.objects_directory, + self.settings.parpg.ObjectDatabaseFile) + self.object_db_file = object_db_file + dialogues_dir = os.path.join(self.settings.system_path, + self.settings.parpg.DialoguesPath) + self.dialogues_directory = dialogues_dir + self.dialogues = {} + self.agent_import_files = {} + self.obj_loader = XMLObjectLoader( + self.engine.getImagePool(), + self.engine.getAnimationPool(), + self.engine.getModel(), + self.engine.getVFS() + ) + + def checkAttributes(self, attributes): + """Checks for attributes that where not given in the map file + and fills them with values from the object database + @param attributes: attributes to check + @type attributes: Dictionary + @return: The modified attributes""" + if attributes.has_key("object_type"): + class_name = attributes.pop("object_type") + else: + class_name = attributes["type"] + if not attributes.has_key("type"): + attributes["type"] = class_name + if self.object_db.has_key(class_name): + db_attributes = deepcopy(self.object_db[class_name]) + for key in db_attributes.keys(): + if attributes.has_key(key): + attributes[key] = attributes[key] or db_attributes[key] + else: + attributes[key] = db_attributes[key] + return attributes + + def isIDUsed(self, ID): + if self.game_state.hasObject(ID): + return True + for namespace in self.agents: + if ID in self.agents[namespace]: + return True + return False + + def createUniqueID(self, ID): + if self.isIDUsed(ID): + id_number = 1 + while self.isIDUsed(ID + "_" + str(id_number)): + id_number += 1 + if id_number > self.MAX_ID_NUMBER: + raise ValueError( + "Number exceeds MAX_ID_NUMBER:" + str(self.MAX_ID_NUMBER)) + + ID = ID + "_" + str(id_number) + return ID + + def createContainerItems(self, container_objs): + """Create the items of a container from a dictionary + @param container_objs: Dictionary containing the items + @type container_objs: dict""" + items = [] + for container_obj in container_objs: + items.append(self.createContainerObject(container_obj)) + + return items + + def createContainerObject(self, attributes): + """Create an object that can be stored in + an container and return it + @param attributes: Dictionary of all object attributes + @type attributes: Dictionary + @return: The created object """ + # create the extra data + extra = {} + extra['controller'] = self + attributes = self.checkAttributes(attributes) + + info = {} + info.update(attributes) + info.update(extra) + ID = info.pop("id") if info.has_key("id") else info.pop("ID") + if not info.has_key("item_type"): + info["item_type"] = info["type"] + ID = self.createUniqueID(ID) + if info.has_key("attributes"): + attributes = info["attributes"] + if "Container" in attributes: + info["actions"]["Open"] = "" + if info.has_key("Items"): + inventory_objs = info["Items"] + info["items"] = self.createContainerItems(inventory_objs) + + new_item = CarryableContainer(ID = ID, **info) + else: + new_item = CarryableItem(ID = ID, **info) + else: + new_item = CarryableItem(ID = ID, **info) + self.game_state.addObject(None, new_item) + return new_item + + def createInventoryObject(self, container, attributes): + """Create an inventory object and place it into a container + @type container: base.Container + @param container: Container where the item is on + @type attributes: Dictionary + @param attributes: Dictionary of all object attributes + @return: None""" + index = attributes.pop("index") if attributes.has_key("index") else None + slot = attributes.pop("slot") if attributes.has_key("slot") else None + obj = self.createContainerObject(attributes) + #obj = createObject(attributes, extra) + if slot: + container.moveItemToSlot(obj, slot) + else: + container.placeItem(obj, index) + + def deleteObject(self, object_id): + """Removes an object from the game + @param object_id: ID of the object + @type object_id: str """ + del self.agents["All"][object_id] + self.game_state.deleteObject(object_id) + + def save(self, path, filename): + """Writes the saver to a file. + @type filename: string + @param filename: the name of the file to write to + @return: None""" + fname = '/'.join([path, filename]) + try: + save_file = open(fname, 'w') + except(IOError): + sys.stderr.write("Error: Can't create save game: " + fname + "\n") + return + save_state = {} + save_state["Agents"] = {} + for map_name in self.agents: + if map_name == self.ALL_AGENTS_KEY: + continue + agents_dict = {} + for agent in self.agents[map_name]: + agent_obj = self.game_state.getObjectById(agent, map_name) + agent_inst = self.game_state.maps[map_name].\ + agent_layer.getInstance(agent) + agent_dict = self.agents[map_name][agent] + agent_dict.update(agent_obj.getStateForSaving()) + agent_dict["Rotation"] = agent_inst.getRotation() + agents_dict[agent] = agent_dict + save_state["Agents"][map_name] = agents_dict + agents_dict = {} + for agent in self.agents["All"]: + map_name = self.agents["All"][agent]["Map"] + agent_dict = self.agents["All"][agent] + agent_obj = None + if agent == "PlayerCharacter": + agent_obj = self.game_state.player_character + else: + agent_obj = self.game_state.getObjectById(agent, map_name) + if agent_obj: + agent_inst = self.game_state.maps[map_name].\ + agent_layer.getInstance(agent) + agent_dict.update(agent_obj.getStateForSaving()) + agent_dict["Rotation"] = agent_inst.getRotation() + agent_dict["MapName"] = map_name + agents_dict[agent] = agent_dict + save_state["Agents"]["All"] = agents_dict + save_state["GameState"] = self.game_state.getStateForSaving() + yaml.dump(save_state, save_file) + + save_file.close() + + def load(self, path, filename): + """Loads a saver from a file. + @type filename: string + @param filename: the name of the file (including path) to load from + @return: None""" + fname = '/'.join([path, filename]) + + try: + load_file = open(fname, 'r') + except(IOError): + sys.stderr.write("Error: Can't find save game file\n") + return + self.deleteMaps() + self.clearAgents() + + save_state = yaml.load(load_file) + self.game_state.restoreFromState(save_state["GameState"]) + maps = save_state["Agents"] + for map_name in maps: + for agent_name in maps[map_name]: + agent = {agent_name:maps[map_name][agent_name]} + self.addAgent(map_name, agent) + + # Load the current map + if self.game_state.current_map_name: + self.loadMap(self.game_state.current_map_name) + load_file.close() + + + # Recreate all the behaviours. These can't be saved because FIFE + # objects cannot be pickled + + self.placeAgents() + self.placePC() + + # In most maps we'll create the PlayerCharacter Instance internally. + # In these cases we need a target position + + def teleport(self, agent, position): + """Called when a an agent is moved instantly to a new position. + The setting of position may wan to be created as its own method down the road. + @type position: String Tuple + @param position: X,Y coordinates passed from engine.changeMap + @return: fife.Location""" + logging.debug(position) + coord = fife.DoublePoint3D(float(position[0]), float(position[1]), 0) + location = fife.Location(self.active_map.agent_layer) + location.setMapCoordinates(coord) + agent.teleport(location) + + def getObjectAtCoords(self, coords): + """Get the object which is at the given coords + @type coords: fife.Screenpoint + @param coords: Coordinates where to check for an object + @rtype: fife.Object + @return: An object or None""" + instances = self.active_map.cameras[ + self.active_map.my_cam_id].\ + getMatchingInstances(coords, self.active_map.agent_layer) + # no object returns an empty tuple + if(instances != ()): + front_y = 0 + + + for obj in instances: + # check to see if this in our list at all + if(self.objectActive(obj.getId())): + # check if the object is on the foreground + obj_map_coords = \ + obj.getLocation().getMapCoordinates() + obj_screen_coords = self.active_map.\ + cameras[self.active_map.my_cam_id]\ + .toScreenCoordinates(obj_map_coords) + + if obj_screen_coords.y > front_y: + #Object on the foreground + front_y = obj_screen_coords.y + return obj + else: + return None + else: + return None + + def getCoords(self, click): + """Get the map location x, y coordinates from the screen coordinates + @type click: fife.ScreenPoint + @param click: Screen coordinates + @rtype: fife.Location + @return: The map coordinates""" + coord = self.active_map.cameras[self.active_map.my_cam_id].\ + toMapCoordinates(click, False) + coord.z = 0 + location = fife.Location(self.active_map.agent_layer) + location.setMapCoordinates(coord) + return location + + def pause(self, paused): + """ Pause/Unpause the game + @return: nothing""" + if self.active_map: + self.active_map.pause(paused) + + def togglePause(self): + """ Toggle paused state. + @return: nothing""" + self.active_map.togglePause() + + def isPaused(self): + """Returns wheter the game is paused or not""" + return self.active_map.isPaused() + + def readMapFiles(self): + """Read all a available map-files and store them""" + maps_data = file(self.game_state.maps_file) + self.map_files = yaml.load(maps_data)["Maps"] + + def addAgent(self, namespace, agent): + """Adds an agent to the agents dictionary + @param namespace: the namespace where the agent is to be added to + @type namespace: str + @param agent: The agent to be added + @type agent: dict """ + from fife.extensions.serializers.xml_loader_tools import loadImportFile + if not self.agents.has_key(namespace): + self.agents[namespace] = {} + + agent_values = agent.values()[0] + unique_agent_id = self.createUniqueID(agent.keys()[0]) + del agent[agent.keys()[0]] + agent[unique_agent_id] = agent_values + self.agents[namespace].update(agent) + object_model = "" + if agent_values.has_key("ObjectModel"): + object_model = agent_values["ObjectModel"] + elif agent_values["ObjectType"] == "MapItem": + object_data = self.object_db[agent_values["ItemType"]] + object_model = object_data["gfx"] if object_data.has_key("gfx") \ + else "generic_item" + else: + object_model = self.object_db[agent_values["ObjectType"]]["gfx"] + import_file = self.agent_import_files[object_model] + loadImportFile(self.obj_loader, import_file, self.engine) + + def readAgentsOfMap(self, map_name): + """Read the agents of the map + @param map_name: Name of the map + @type map_name: str """ + #Get the agents of the map + map_agents_file = self.map_files[map_name].\ + replace(".xml", "_agents.yaml") + agents_data = file(map_agents_file) + agents = yaml.load_all(agents_data) + for agent in agents: + if not agent == None: + self.addAgent(map_name, agent) + + def readAllAgents(self): + """Read the agents of the all_agents_file and store them""" + agents_data = file(self.all_agents_file) + agents = yaml.load_all(agents_data) + for agent in agents: + if not agent == None: + self.addAgent(self.ALL_AGENTS_KEY, agent) + + def getAgentsOfMap(self, map_name): + """Returns the agents that are on the given map + @param map_name: Name of the map + @type map_name: str + @return: A dictionary with the agents of the map""" + if not self.agents.has_key(map_name): + return {} + ret_dict = self.agents[map_name].copy() + for agent_name, agent_value in self.agents[self.ALL_AGENTS_KEY]\ + .iteritems(): + if agent_value["Map"] == map_name: + ret_dict[agent_name] = agent_value + return ret_dict + + def getAgentsOfActiveMap(self): + """Returns the agents that are on active map + @return: A dictionary with the agents of the map """ + return self.getAgentsOfMap(self.active_map.map.getId()) + + def clearAgents(self): + """Resets the agents dictionary""" + self.agents = {} + self.agents[self.ALL_AGENTS_KEY] = {} + + def loadMap(self, map_name): + """Load a new map. + @type map_name: string + @param map_name: Name of the map to load + @return: None""" + if not map_name in self.game_state.maps: + map_file = self.map_files[map_name] + new_map = GameMap(self.engine, self) + self.game_state.maps[map_name] = new_map + new_map.load(map_file) + + def createAgent(self, agent, inst_id): + object_type = agent["ObjectType"] + object_id = agent["ObjectModel"] \ + if agent.has_key("ObjectModel") \ + else None + if object_id == None: + if object_type == "MapItem": + object_data = self.object_db[agent["ItemType"]] + object_id = object_data["gfx"] if object_data.has_key("gfx") \ + else "generic_item" + else: + object_id = self.object_db[object_type]["gfx"] + map_obj = self.fife_model.getObject(str(object_id), "PARPG") + if not map_obj: + logging.warning("Object with inst_id={0}, ns=PARPG, " + "could not be found. " + "Omitting...".format(str(obj_id))) + + x_pos = agent["Position"][0] + y_pos = agent["Position"][1] + z_pos = agent["Position"][2] if len(agent["Position"]) == 3 \ + else -0.1 if object_type == "MapItem" \ + else 0.0 + stack_pos = agent["Stackposition"] if \ + agent.has_key("StackPosition") \ + else None + inst = self.active_map.agent_layer.\ + createInstance(map_obj, + fife.ExactModelCoordinate(x_pos, + y_pos, + z_pos), + inst_id) + inst.setId(inst_id) + + rotation = agent["Rotation"] + inst.setRotation(rotation) + + fife.InstanceVisual.create(inst) + if (stack_pos): + inst.get2dGfxVisual().setStackPosition(int(stack_pos)) + + if (map_obj.getAction('default')): + target = fife.Location(self.active_map.agent_layer) + inst.act('default', target, True) + + inst_dict = {} + inst_dict["id"] = inst_id + inst_dict["type"] = object_type + inst_dict["xpos"] = x_pos + inst_dict["ypos"] = y_pos + inst_dict["gfx"] = object_id + inst_dict["is_open"] = parseBool(agent["Open"]) \ + if agent.has_key("Open") \ + else False + inst_dict["locked"] = parseBool(agent["Locked"]) \ + if agent.has_key("Locked") \ + else False + inst_dict["name"] = agent["ViewName"] + inst_dict["real_name"] = agent["RealName"] \ + if agent.has_key("RealName") \ + else agent["ViewName"] + inst_dict["text"] = agent["Text"] \ + if agent.has_key("Text") \ + else None + if self.dialogues.has_key(inst_id): + inst_dict["dialogue"] = self.dialogues[inst_id] + inst_dict["target_map_name"] = agent["TargetMap"] \ + if agent.\ + has_key("TargetMap") \ + else None + inst_dict["target_x"] = agent["TargetPosition"][0] \ + if agent.\ + has_key("TargetPosition") \ + else None + inst_dict["target_y"] = agent["TargetPosition"][1] \ + if agent.\ + has_key("TargetPosition") \ + else None + if agent.has_key("Inventory"): + inventory = Inventory() + inventory_objs = agent["Inventory"] + for inventory_obj in inventory_objs: + self.createInventoryObject(inventory, + inventory_obj + ) + inst_dict["inventory"] = inventory + + if agent.has_key("Items"): + container_objs = agent["Items"] + items = self.createContainerItems(container_objs) + inst_dict["items"] = items + + if agent.has_key("ItemType"): + if not agent.has_key("item"): + item_data = {} + item_data["type"] = agent["ItemType"] + item_data["ID"] = inst_id + item_data = self.createContainerObject(item_data) + else: + item_data = agent["item"] + inst_dict["item"] = item_data + inst_dict["item_type"] = agent["ItemType"] + + self.createMapObject(self.active_map.agent_layer, inst_dict) + + def placeAgents(self): + """Places the current maps agents """ + if not self.active_map: + return + agents = self.getAgentsOfMap(self.game_state.current_map_name) + for agent in agents: + if agent == "PlayerCharacter": + continue + if self.active_map.agent_layer.getInstances(agent): + continue + self.createAgent(agents[agent], agent) + + def placePC(self): + """Places the PlayerCharacter on the map""" + agent = self.agents[self.ALL_AGENTS_KEY]["PlayerCharacter"] + inst_id = "PlayerCharacter" + self.createAgent(agent, inst_id) + + # create the PlayerCharacter agent + self.active_map.addPC() + self.game_state.player_character.start() + if agent.has_key("PeopleKnown"): + self.game_state.player_character.people_i_know = agent["PeopleKnown"] + + def changeMap(self, map_name, target_position = None): + """Registers for a map change on the next pump(). + @type map_name: String + @param map_name: Id of the map to teleport to + @type map_file: String + @param map_file: Filename of the map to teleport to + @type target_position: Tuple + @param target_position: Position of PlayerCharacter on target map. + @return None""" + # set the parameters for the map change if moving to a new map + if map_name != self.game_state.current_map_name: + self.target_map_name = map_name + self.target_position = target_position + # issue the map change + self.map_change = True + + def deleteMaps(self): + """Clear all currently loaded maps from FIFE as well as clear our + local map cache + @return: nothing""" + self.engine.getModel().deleteMaps() + self.engine.getModel().deleteObjects() + self.game_state.clearObjects() + self.game_state.maps = {} + + def setActiveMap(self, map_name): + """Sets the active map that is to be rendered. + @type map_name: String + @param map_name: The name of the map to load + @return: None""" + # Turn off the camera on the old map before we turn on the camera + # on the new map. + self.active_map.cameras[self.active_map.my_cam_id].setEnabled(False) + # Make the new map active. + self.active_map = self.game_state.maps[map_name] + self.active_map.makeActive() + self.game_state.current_map_name = map_name + + def createMapObject (self, layer, attributes): + """Create an object and add it to the current map. + @type layer: fife.Layer + @param layer: FIFE layer object exists in + @type attributes: Dictionary + @param attributes: Dictionary of all object attributes + @type instance: fife.Instance + @param instance: FIFE instance corresponding to the object + @return: None""" + # create the extra data + extra = {} + if layer is not None: + extra['agent_layer'] = layer + attributes = self.checkAttributes(attributes) + + obj = createObject(attributes, extra) + + if obj.trueAttr("PC"): + self.addPC(layer, obj) + else: + self.addObject(layer, obj) + + def addPC(self, layer, player_char): + """Add the PlayerCharacter to the map + @type layer: fife.Layer + @param layer: FIFE layer object exists in + @type player_char: PlayerCharacter + @param player_char: PlayerCharacter object + @type instance: fife.Instance + @param instance: FIFE instance of PlayerCharacter + @return: None""" + # For now we copy the PlayerCharacter, + # in the future we will need to copy + # PlayerCharacter specifics between the different PlayerCharacter's + self.game_state.player_character = player_char + self.game_state.player_character.setup() + self.game_state.player_character.behaviour.speed = self.settings.parpg.PCSpeed + + + def addObject(self, layer, obj): + """Adds an object to the map. + @type layer: fife.Layer + @param layer: FIFE layer object exists in + @type obj: GameObject + @param obj: corresponding object class + @type instance: fife.Instance + @param instance: FIFE instance of object + @return: None""" + ref = self.game_state.getObjectById(obj.ID, \ + self.game_state.current_map_name) + if ref is None: + # no, add it to the game state + self.game_state.addObject(self.game_state.current_map_name, obj) + else: + # yes, use the current game state data + obj.X = ref.X + obj.Y = ref.Y + obj.gfx = ref.gfx + + if obj.trueAttr("NPC"): + # create the agent + obj.setup() + obj.behaviour.speed = self.settings.parpg.PCSpeed - 1 + # create the PlayerCharacter agent + obj.start() + if obj.trueAttr("AnimatedContainer"): + # create the agent + obj.setup() + + def objectActive(self, ident): + """Given the objects ID, pass back the object if it is active, + False if it doesn't exist or not displayed + @type ident: string + @param ident: ID of object + @rtype: boolean + @return: Status of result (True/False)""" + for game_object in \ + self.game_state.getObjectsFromMap(self.game_state.current_map_name): + if (game_object.ID == ident): + # we found a match + return game_object + # no match + return False + + def movePlayer(self, position): + """Code called when the player should move to another location + @type position: fife.ScreenPoint + @param position: Screen position to move to + @return: None""" + if(self.pc_run == 1): + self.game_state.player_character.run(position) + else: + self.game_state.player_character.walk(position) + + def teleportAgent(self, agent, position): + """Code called when an agent should teleport to another location + @type position: fife.ScreenPoint + @param position: Screen position to teleport to + @return: None""" + agent.teleport(position) + self.agents[agent.ID]["Position"] = position + + def readObjectDB(self): + """Reads the Object Information Database from a file. """ + database_file = file(self.object_db_file, "r") + database = yaml.load_all(database_file) + for object_info in database: + self.object_db.update(object_info) + + def getAgentImportFiles(self): + """Searches the agents directory for import files """ + files = locateFiles("*.xml", self.objects_directory) + for xml_file in files: + xml_file = os.path.relpath(xml_file).replace("\\", "/") + try: + root = ElementTree.parse(xml_file).getroot() + if root.tag == "object": + self.agent_import_files[root.attrib["id"]] = xml_file + except SyntaxError as error: + assert(isinstance(error, SyntaxError)) + logging.critical("Error parsing file {0}: " + "{1}".format(xml_file, error.msg)) + sys.exit(1) + + def getDialogues(self): + """Searches the dialogue directory for dialogues """ + files = locateFiles("*.yaml", self.dialogues_directory) + dialogue_parser = YamlDialogueParser() + for dialogue_filepath in files: + dialogue_filepath = os.path.relpath(dialogue_filepath) \ + .replace("\\", "/") + # Note Technomage 2010-11-13: the new DialogueEngine uses its own + # parser now, YamlDialogueParser. +# dialogues = yaml.load_all(file(dialogue_file, "r")) + with file(dialogue_filepath, 'r') as dialogue_file: + try: + dialogue = dialogue_parser.load(dialogue_file) + except (DialogueFormatError,) as error: + logging.error('unable to load dialogue file {0}: {1}' + .format(dialogue_filepath, error)) + else: + self.dialogues[dialogue.npc_name] = dialogue + # Note Technomage 2010-11-13: the below code is used to load + # multiple dialogues from a single file. Is this functionality + # used/necessary? +# for dialogue in dialogues: +# self.dialogues[dialogue["NPC"]] = dialogue diff -r 000000000000 -r 7a89ea5404b1 gamescenecontroller.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gamescenecontroller.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,461 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . +"""This file contains the GameSceneController that handles input when the game + is exploring a scene""" + + +from datetime import datetime +import random +import glob +import os + +from fife import fife +from fife import extensions + +from controllerbase import ControllerBase +from parpg.gui.hud import Hud +from parpg.gui import drag_drop_data as data_drag +from objects.action import ChangeMapAction, ExamineAction, OpenBoxAction, \ + UnlockBoxAction, LockBoxAction, TalkAction, \ + PickUpAction, DropItemAction + +#For debugging/code analysis +if False: + from gamesceneview import GameSceneView + from gamemodel import GameModel + from parpg import PARPGApplication + +import logging + +logger = logging.getLogger('gamescenecontroller') + +class GameSceneController(ControllerBase): + ''' + This controller handles inputs when the game is in "scene" state. + "Scene" state is when the player can move around and interact + with objects. Like, talking to a npc or examining the contents of a box. + ''' + + + def __init__(self, engine, view, model, application): + ''' + Constructor + @param engine: Instance of the active fife engine + @type engine: fife.Engine + @param view: Instance of a GameSceneView + @param type: parpg.GameSceneView + @param model: The model that has the current gamestate + @type model: parpg.GameModel + @param application: The application that created this controller + @type application: parpg.PARPGApplication + @param settings: The current settings of the application + @type settings: fife.extensions.fife_settings.Setting + ''' + ControllerBase.__init__(self, + engine, + view, + model, + application) + #this can be helpful for IDEs code analysis + if False: + assert(isinstance(self.engine, fife.Engine)) + assert(isinstance(self.view, GameSceneView)) + assert(isinstance(self.view, GameModel)) + assert(isinstance(self.application, PARPGApplication)) + assert(isinstance(self.event_manager, fife.EventManager)) + + # Last saved mouse coords + self.action_number = 1 + + self.has_mouse_focus = True + self.last_mousecoords = None + self.mouse_callback = None + self.original_cursor_id = self.engine.getCursor().getId() + self.scroll_direction = [0, 0] + self.scroll_timer = extensions.fife_timer.Timer(100, + lambda: self.view.moveCamera \ + (self.scroll_direction)) + + #this is temporary until we can set the native cursor + self.resetMouseCursor() + self.paused = False + + if model.settings.fife.EnableSound: + if not self.view.sounds.music_init: + music_file = random.choice(glob.glob(os.path.join( + "music", + "*.ogg"))) + self.view.sounds.playMusic(music_file) + self.initHud() + + + def initHud(self): + """Initialize the hud member + @return: None""" + hud_callbacks = { + 'saveGame': self.saveGame, + 'loadGame': self.loadGame, + 'quitGame': self.quitGame, + } + self.view.hud = Hud(self, + self.model.settings, + hud_callbacks) + + def keyPressed(self, evt): + """Whenever a key is pressed, fife calls this routine. + @type evt: fife.event + @param evt: The event that fife caught + @return: None""" + key = evt.getKey() + key_val = key.getValue() + + if(key_val == key.Q): + # we need to quit the game + self.view.hud.quitGame() + if(key_val == key.T): + self.model.active_map.toggleRenderer('GridRenderer') + if(key_val == key.F1): + # display the help screen and pause the game + self.view.hud.displayHelp() + if(key_val == key.F5): + self.model.active_map.toggleRenderer('CoordinateRenderer') + if(key_val == key.F7): + # F7 saves a screenshot to screenshots directory + + settings = self.model.settings + screenshot_directory = os.path.join(settings.user_path, + settings.parpg.ScreenshotsPath) + # try to create the screenshots directory + try: + os.mkdir(screenshot_directory) + #TODO: distinguish between already existing permissions error + except OSError: + logger.warning("screenshot directory wasn't created.") + + screenshot_file = os.path.join(screenshot_directory, + 'screen-{0}.png'.format( + datetime.now().strftime( + '%Y-%m-%d-%H-%M-%S'))) + self.engine.getRenderBackend().captureScreen(screenshot_file) + logger.info("PARPG: Saved: {0}".format(screenshot_file)) + if(key_val == key.F10): + # F10 shows/hides the console + self.engine.getGuiManager().getConsole().toggleShowHide() + if(key_val == key.C): + # C opens and closes the character screen. + self.view.hud.toggleCharacterScreen() + if(key_val == key.I): + # I opens and closes the inventory + self.view.hud.toggleInventory() + if(key_val == key.A): + # A adds a test action to the action box + # The test actions will follow this format: Action 1, + # Action 2, etc. + self.view.hud.addAction("Action " + str(self.action_number)) + self.action_number += 1 + if(key_val == key.ESCAPE): + # Escape brings up the main menu + self.view.hud.displayMenu() + # Hide the quit menu + self.view.hud.quit_window.hide() + if(key_val == key.M): + self.view.sounds.toggleMusic() + if(key_val == key.PAUSE): + # Pause pause/unpause the game + self.model.togglePause() + self.pause(False) + if(key_val == key.SPACE): + self.model.active_map.centerCameraOnPlayer() + + def mouseReleased(self, evt): + """If a mouse button is released, fife calls this routine. + We want to wait until the button is released, because otherwise + pychan captures the release if a menu is opened. + @type evt: fife.event + @param evt: The event that fife caught + @return: None""" + self.view.hud.hideContextMenu() + scr_point = fife.ScreenPoint(evt.getX(), evt.getY()) + if(evt.getButton() == fife.MouseEvent.LEFT): + if(data_drag.dragging): + coord = self.model.getCoords(scr_point)\ + .getExactLayerCoordinates() + commands = ({"Command": "ResetMouseCursor"}, + {"Command": "StopDragging"}) + self.model.game_state.player_character.approach([coord.x, + coord.y], + DropItemAction(self, + data_drag.dragged_item, + commands)) + else: + self.model.movePlayer(self.model.getCoords(scr_point)) + elif(evt.getButton() == fife.MouseEvent.RIGHT): + # is there an object here? + tmp_active_map = self.model.active_map + instances = tmp_active_map.cameras[tmp_active_map.my_cam_id].\ + getMatchingInstances(scr_point, + tmp_active_map.agent_layer) + info = None + for inst in instances: + # check to see if this is an active item + if(self.model.objectActive(inst.getId())): + # yes, get the model + info = self.getItemActions(inst.getId()) + break + + # take the menu items returned by the engine or show a + # default menu if no items + data = info or \ + [["Walk", "Walk here", self.view.onWalk, + self.model.getCoords(scr_point)]] + # show the menu + self.view.hud.showContextMenu(data, (scr_point.x, scr_point.y)) + + + def updateMouse(self): + """Updates the mouse values""" + if self.paused: + return + cursor = self.engine.getCursor() + #this can be helpful for IDEs code analysis + if False: + assert(isinstance(cursor, fife.Cursor)) + self.last_mousecoords = fife.ScreenPoint(cursor.getX(), + cursor.getY()) + self.view.highlightFrontObject(self.last_mousecoords) + + #set the trigger area in pixles + pixle_edge = 20 + + mouse_x = self.last_mousecoords.x + screen_width = self.model.engine.getSettings().getScreenWidth() + mouse_y = self.last_mousecoords.y + screen_height = self.model.engine.getSettings().getScreenHeight() + + image = None + settings = self.model.settings + + + #edge logic + self.scroll_direction = [0, 0] + if self.has_mouse_focus: + direction = self.scroll_direction + #up + if mouse_y <= pixle_edge: + direction[0] += 1 + direction[1] -= 1 + image = os.path.join(settings.system_path, + settings.parpg.GuiPath, + settings.parpg.CursorPath, + settings.parpg.CursorUp) + + #right + if mouse_x >= screen_width - pixle_edge: + direction[0] += 1 + direction[1] += 1 + image = os.path.join(settings.system_path, + settings.parpg.GuiPath, + settings.parpg.CursorPath, + settings.parpg.CursorRight) + + #down + if mouse_y >= screen_height - pixle_edge: + direction[0] -= 1 + direction[1] += 1 + image = os.path.join(settings.system_path, + settings.parpg.GuiPath, + settings.parpg.CursorPath, + settings.parpg.CursorDown) + + #left + if mouse_x <= pixle_edge: + direction[0] -= 1 + direction[1] -= 1 + image = os.path.join(settings.system_path, + settings.parpg.GuiPath, + settings.parpg.CursorPath, + settings.parpg.CursorLeft) + + if image is not None and not data_drag.dragging: + self.setMouseCursor(image, image) + + + def handleCommands(self): + """Check if a command is to be executed + """ + if self.model.map_change: + self.pause(True) + if self.model.active_map: + player_char = self.model.game_state.player_character + self.model.game_state.player_character = None + pc_agent = self.model.agents[self.model.ALL_AGENTS_KEY]\ + ["PlayerCharacter"] + pc_agent.update(player_char.getStateForSaving()) + pc_agent["Map"] = self.model.target_map_name + pc_agent["Position"] = self.model.target_position + pc_agent["Inventory"] = \ + player_char.inventory.serializeInventory() + player_agent = self.model.active_map.\ + agent_layer.getInstance("PlayerCharacter") + self.model.active_map.agent_layer.deleteInstance(player_agent) + self.model.loadMap(self.model.target_map_name) + self.model.setActiveMap(self.model.target_map_name) + self.model.readAgentsOfMap(self.model.target_map_name) + self.model.placeAgents() + self.model.placePC() + self.model.map_change = False + # The PlayerCharacter has an inventory, and also some + # filling of the ready slots in the HUD. + # At this point we sync the contents of the ready slots + # with the contents of the inventory. + self.view.hud.inventory = None + self.view.hud.initializeInventory() + self.pause(False) + + def nullFunc(self, userdata): + """Sample callback for the context menus.""" + logger.info(userdata) + + def initTalk(self, npc_info): + """ Starts the PlayerCharacter talking to an NPC. """ + # TODO: work more on this when we get NPCData and HeroData straightened + # out + npc = self.model.game_state.getObjectById(npc_info.ID, + self.model.game_state.\ + current_map_name) + self.model.game_state.player_character.approach([npc.getLocation().\ + getLayerCoordinates().x, + npc.getLocation().\ + getLayerCoordinates().y], + TalkAction(self, npc)) + + def getItemActions(self, obj_id): + """Given the objects ID, return the text strings and callbacks. + @type obj_id: string + @param obj_id: ID of object + @rtype: list + @return: List of text and callbacks""" + actions = [] + # note: ALWAYS check NPC's first! + obj = self.model.game_state.\ + getObjectById(obj_id, + self.model.game_state.current_map_name) + + if obj is not None: + if obj.trueAttr("NPC"): + # keep it simple for now, None to be replaced by callbacks + actions.append(["Talk", "Talk", self.initTalk, obj]) + actions.append(["Attack", "Attack", self.nullFunc, obj]) + else: + actions.append(["Examine", "Examine", + self.model.game_state.\ + player_character.approach, + [obj.X, obj.Y], + ExamineAction(self, + obj_id, obj.name, + obj.text)]) + # is it a Door? + if obj.trueAttr("door"): + actions.append(["Change Map", "Change Map", + self.model.game_state.player_character.approach, + [obj.X, obj.Y], + ChangeMapAction(self, obj.target_map_name, + obj.target_pos)]) + # is it a container? + if obj.trueAttr("container"): + actions.append(["Open", "Open", + self.model.game_state.\ + player_character.approach, + [obj.X, obj.Y], + OpenBoxAction(self, obj)]) + actions.append(["Unlock", "Unlock", + self.model.game_state.\ + player_character.approach, + [obj.X, obj.Y], + UnlockBoxAction(self, obj)]) + actions.append(["Lock", "Lock", + self.model.game_state.\ + player_character.approach, + [obj.X, obj.Y], + LockBoxAction(self, obj)]) + # can you pick it up? + if obj.trueAttr("carryable"): + actions.append(["Pick Up", "Pick Up", + self.model.game_state.\ + player_character.approach, + [obj.X, obj.Y], + PickUpAction(self, obj)]) + + return actions + + def saveGame(self, *args, **kwargs): + """Saves the game state, delegates call to engine.Engine + @return: None""" + self.model.pause(False) + self.pause(False) + self.view.hud.enabled = True + self.model.save(*args, **kwargs) + + def loadGame(self, *args, **kwargs): + """Loads the game state, delegates call to engine.Engine + @return: None""" + # Remove all currently loaded maps so we can start fresh + self.model.pause(False) + self.pause(False) + self.view.hud.enabled = True + self.model.deleteMaps() + self.view.hud.inventory = None + + self.model.load(*args, **kwargs) + self.view.hud.initializeInventory() + + def quitGame(self): + """Quits the game + @return: None""" + self.application.listener.quitGame() + + def pause(self, paused): + """Pauses the controller""" + super(GameSceneController, self).pause(paused) + self.paused = paused + if paused: + self.scroll_timer.stop() + self.resetMouseCursor() + + def onCommand(self, command): + if(command.getCommandType() == fife.CMD_MOUSE_FOCUS_GAINED): + self.has_mouse_focus = True + elif(command.getCommandType() == fife.CMD_MOUSE_FOCUS_LOST): + self.has_mouse_focus = False + + def pump(self): + """Routine called during each frame. Our main loop is in ./run.py""" + # uncomment to instrument + # t0 = time.time() + if self.paused: + return + self.updateMouse() + if self.model.active_map: + self.view.highlightFrontObject(self.last_mousecoords) + self.view.refreshTopLayerTransparencies() + if self.scroll_direction != [0, 0]: + self.scroll_timer.start() + else: + self.scroll_timer.stop() + if not data_drag.dragging: + self.resetMouseCursor() + + self.handleCommands() + # print "%05f" % (time.time()-t0,) diff -r 000000000000 -r 7a89ea5404b1 gamesceneview.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gamesceneview.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,159 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +from sounds import SoundEngine +from viewbase import ViewBase +from fife import fife + +class GameSceneView(ViewBase): + """GameSceneView is responsible for drawing the scene""" + def __init__(self, engine, model): + """Constructor for GameSceneView + @param engine: A fife.Engine instance + @type engine: fife.Engine + @param model: a script.GameModel instance + @type model: script.GameModel + """ + super(GameSceneView, self).__init__(engine, model) + + # init the sound + self.sounds = SoundEngine(engine) + + self.hud = None + + # The current highlighted object + self.highlight_obj = None + + # faded objects in top layer + self.faded_objects = set() + + def displayObjectText(self, obj_id, text, time=1000): + """Display on screen the text of the object over the object. + @type obj_id: id of fife.instance + @param obj: id of object to draw over + @type text: String + @param text: text to display over object + @return: None""" + try: + if obj_id: + obj = self.model.active_map.agent_layer.getInstance(obj_id) + else: + obj = None + except RuntimeError as error: + if error.args[0].split(',')[0].strip() == "_[NotFound]_": + obj = None + else: + raise + if obj: + obj.say(str(text), time) + + def onWalk(self, click): + """Callback sample for the context menu.""" + self.hud.hideContainer() + self.model.game_state.player_character.run(click) + + def refreshTopLayerTransparencies(self): + """Fade or unfade TopLayer instances if the PlayerCharacter + is under them.""" + if not self.model.active_map: + return + + # get the PlayerCharacter's screen coordinates + camera = self.model.active_map.cameras[self.model.active_map.my_cam_id] + point = self.model.game_state.player_character.\ + behaviour.agent.getLocation() + scr_coords = camera.toScreenCoordinates(point.getMapCoordinates()) + + # find all instances on TopLayer that fall on those coordinates + instances = camera.getMatchingInstances(scr_coords, + self.model.active_map.top_layer) + instance_ids = [ instance.getId() for instance in instances ] + faded_objects = self.faded_objects + + # fade instances + for instance_id in instance_ids: + if instance_id not in faded_objects: + faded_objects.add(instance_id) + self.model.active_map.top_layer.getInstance(instance_id).\ + get2dGfxVisual().setTransparency(128) + + # unfade previously faded instances + for instance_id in faded_objects.copy(): + if instance_id not in instance_ids: + faded_objects.remove(instance_id) + self.model.active_map.top_layer.getInstance(instance_id).\ + get2dGfxVisual().setTransparency(0) + + + #def removeHighlight(self): + + + def highlightFrontObject(self, mouse_coords): + """Highlights the object that is at the + current mouse coordinates""" + if not self.model.active_map: + return + if mouse_coords: + front_obj = self.model.getObjectAtCoords(mouse_coords) + if front_obj != None: + if self.highlight_obj == None \ + or front_obj.getId() != \ + self.highlight_obj: + if self.model.game_state.hasObject(front_obj.getId()): + self.displayObjectText(self.highlight_obj, "") + self.model.active_map.outline_renderer.removeAllOutlines() + self.highlight_obj = front_obj.getId() + self.model.active_map.outline_renderer.addOutlined( + front_obj, + 0, + 137, 255, 2) + # get the text + item = self.model.objectActive(self.highlight_obj) + if item is not None: + self.displayObjectText(self.highlight_obj, + item.name) + else: + self.model.active_map.outline_renderer.removeAllOutlines() + self.highlight_obj = None + + + def moveCamera(self, direction): + """Move the camera in the given direction. + @type direction: list of two integers + @param direction: the two integers can be 1, -1, or 0 + @return: None """ + + if 'cameras' in dir(self.model.active_map): + cam = self.model.active_map.cameras[self.model.active_map.my_cam_id] + location = cam.getLocation() + position = location.getMapCoordinates() + + #how many pixls to move by each call + move_by = 1 + #create a new DoublePoint3D and add it to position DoublePoint3D + new_x, new_y = move_by * direction[0], move_by * direction[1] + + position_offset = fife.DoublePoint3D(int(new_x), int(new_y)) + position += position_offset + + #give location the new position + location.setMapCoordinates(position) + + #detach the camera from any objects + cam.detach() + #move the camera to the new location + cam.setLocation(location) + + diff -r 000000000000 -r 7a89ea5404b1 gamestate.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gamestate.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,124 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +from parpg.quest_engine import QuestEngine + +class GameState(object): + """This class holds the current state of the game.""" + def __init__(self, quests_dir = None): + self.player_character = None + self.quest_engine = QuestEngine(quests_dir) + self.quest_engine.readQuests() + self.objects = {} + self.object_ids = {} + self.current_map_name = None + self.maps = {} + + + def addObject(self, map_id, game_object): + """Adds an object to the objects and object_ids + dictionaries. + @param map_id: ID of the map the object is on. + If the object is in a container this has to be None + @param object: object to be added + @type object: GameObject + @type map_id: str or None + """ + object_id = game_object.ID + if not self.object_ids.has_key(object_id): + if map_id: + self.objects[map_id][object_id] = game_object + self.object_ids[object_id] = map_id + + def deleteObject(self, object_id): + """Removes an object from the dictionaries + @param object_id: ID of the object + @type object_id: str + """ + if self.hasObject(object_id): + map_id = self.getMapOfObject(object_id) + if map_id: + inst = self.maps[map_id].agent_layer.getInstance(object_id) + self.maps[map_id].agent_layer.deleteInstance(inst) + del self.objects[map_id][object_id] + del self.object_ids[object_id] + + + def getObjectsFromMap(self, map_id): + """Gets all objects that are currently on the given map. + @type map: String + @param map: The map name. + @returns: The list of objects on this map. Or an empty list""" + if map_id in self.objects: + return [i for i in self.objects[map_id].values() \ + if map_id in self.objects] + + return {} + + def hasObject(self, object_id): + """Check if an object with the given id is present + @param object_id: ID of the object + @type object_id: str + @return: True if there is an object False if not + """ + return self.object_ids.has_key(object_id) + + def getMapOfObject(self, object_id): + """Returns the map the object is on. + @param object_id: ID of the object + @type object_id: str + @return: Name of the map the object is on. + If there is no such object or the object is in a container None is returned + """ + if self.object_ids.has_key(object_id): + return self.object_ids[object_id] + return None + + def getObjectById(self, obj_id, map_id = None): + """Gets an object by its object id and map id + @type obj_id: String + @param obj_id: The id of the object. + @type map_id: String + @param map_id: It id of the map containing the object. + @returns: The object or None.""" + if not map_id: + map_id = self.getMapOfObject(obj_id) + if not map_id in self.objects: + self.objects[map_id] = {} + if obj_id in self.objects[map_id]: + return self.objects[map_id][obj_id] + + def clearObjects(self): + """Delete all objects from the state + """ + self.objects = {} + self.object_ids = {} + + def getStateForSaving(self): + """Prepares state for saving + @type state: dictionary + @param state: State of the object + """ + ret_dict = {} + ret_dict["CurrentMap"] = self.current_map_name + ret_dict["Quests"] = self.quest_engine.getStateForSaving() + return ret_dict + + def restoreFromState(self, state): + """Restores the state""" + self.current_map_name = state["CurrentMap"] + self.quest_engine.readQuests() + self.quest_engine.restoreFromState(state["Quests"]) + diff -r 000000000000 -r 7a89ea5404b1 gui/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gui/__init__.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,10 @@ +from fife.extensions import pychan +from .inventorygui import EquipmentSlot, InventoryGrid +from .spinners import Spinner, IntSpinner +from .tabwidget import TabWidget + +pychan.registerWidget(EquipmentSlot) +pychan.registerWidget(InventoryGrid) +pychan.registerWidget(Spinner) +pychan.registerWidget(IntSpinner) +pychan.registerWidget(TabWidget) \ No newline at end of file diff -r 000000000000 -r 7a89ea5404b1 gui/actionsbox.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gui/actionsbox.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,73 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +"""Widget for displaying actions""" + +from fife.extensions import pychan +from fife.extensions.pychan import ScrollArea +from fife.extensions.pychan import VBox + +class ActionsBox(ScrollArea): + def __init__(self, **kwargs): + ScrollArea.__init__(self, **kwargs) + self.ContentBox = VBox(name = "ActionsContentBox", is_focusable=False) + self.addChild(self.ContentBox) + + def refresh(self): + """Refresh the actions box so that it displays the contents of + self.actions_text + @return: None""" + self.adaptLayout() + self.vertical_scroll_amount = self.getVerticalMaxScroll() + + def addAction(self, action): + """Add an action to the actions box. + @type action: (unicode) string + @param action: The text that you want to display in the actions box + @return: None""" + + if not type(action) is unicode: + action = unicode(action) + action_label = pychan.widgets.Label(text = action, wrap_text = True) + action_label.max_width = self.ContentBox.width + self.ContentBox.addChild(action_label) + self.refresh() + + def addDialog(self, name, text): + """Add a dialog text to the actions box. Prints first the name and then, indented to the right, the text. + @type name: (unicode) string + @param action: The name of the character that spoke + @type text:: (unicode) string + @param text: The text that was said + @return: None""" + if not type(name) is unicode: + name = unicode(name) + if not type(text) is unicode: + text = unicode(text) + + + name_label = pychan.widgets.Label(text = name, wrap_text = True) + self.ContentBox.addChild(name_label) + text_box = pychan.widgets.HBox() + spacer = pychan.widgets.Label() + spacer.min_width = int(self.ContentBox.width * 0.05) + spacer.max_width = int(self.ContentBox.width * 0.05) + text_box.addChild(spacer) + text_label = pychan.widgets.Label(text = text, wrap_text = True) + text_label.max_width = int(self.ContentBox.width * 0.95) + text_box.addChild(text_label) + self.ContentBox.addChild(text_box) + self.refresh() + diff -r 000000000000 -r 7a89ea5404b1 gui/charactercreationview.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gui/charactercreationview.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,28 @@ +from fife.extensions import pychan +from fife.extensions.pychan.widgets import Label, HBox + +from parpg.gui.spinner import IntSpinner + +class CharacterCreationView(object): + def __init__(self, xml_script_path='gui/character_creation.xml'): + self.gui = pychan.loadXML(xml_script_path) + + def createStatisticList(self, statistics): + statistics_list = self.gui.findChild(name='statisticsList') + # Start with an empty list. + statistics_list.removeAllChildren() + for statistic in statistics: + name = statistic.long_name + hbox = HBox() + hbox.opaque = 0 + label = Label(text=name) + spinner = IntSpinner(lower_limit=0, upper_limit=100) + hbox.addChildren(label, spinner) + statistics_list.addChildren(hbox) + + def createTraitsList(self, traits): + pass + + def updateMessageArea(self, message): + message_area = self.gui.findChild(name='messageArea') + message_area.text = unicode(message) \ No newline at end of file diff -r 000000000000 -r 7a89ea5404b1 gui/containergui.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gui/containergui.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,139 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from fife.extensions.pychan.tools import callbackWithArguments as cbwa + +from parpg.gui.containergui_base import ContainerGUIBase +from parpg.gui import drag_drop_data as data_drag +from parpg.objects.base import Container + +class ContainerGUI(ContainerGUIBase): + def __init__(self, controller, title, container): + """A class to create a window showing the contents of a container. + @param controller: The current Controller + @type controller: Class derived from ControllerBase + @param title: The title of the window + @type title: string + @param container: A container to represent + @type container: Container + @return: None""" + super(ContainerGUI, self).__init__(controller, "gui/container_base.xml") + self.gui.findChild(name="topWindow").title = title + + self.empty_images = dict() + self.container = container + self.events_to_map = {} + self.buttons = ("Slot1", "Slot2", "Slot3", + "Slot4", "Slot5", "Slot6", + "Slot7", "Slot8", "Slot9") + + def updateImages(self): + for index, button in enumerate(self.buttons): + widget = self.gui.findChild(name=button) + widget.item = self.container.getItemAt(index) + self.updateImage(widget) + + def updateImage(self, button): + if (button.item == None): + image = self.empty_images[button.name] + else: + image = button.item.getInventoryThumbnail() + button.up_image = image + button.down_image = image + button.hover_image = image + + def dragObject(self, obj): + """Drag the selected object. + @type obj: string + @param obj: The name of the object within + the dictionary 'self.buttons' + @return: None""" + # get the widget from the gui with the name obj + drag_widget = self.gui.findChild(name = obj) + drag_item = drag_widget.item + # only drag if the widget is not empty + if (drag_item != None): + # get the item that the widget is 'storing' + data_drag.dragged_item = drag_widget.item + # get the up and down images of the widget + up_image = drag_widget.up_image + down_image = drag_widget.down_image + # set the mouse cursor to be the widget's image + self.controller.setMouseCursor(up_image.source, down_image.source) + data_drag.dragged_image = up_image.source + data_drag.dragging = True + data_drag.dragged_widget = drag_widget + data_drag.source_container = self.container + + self.container.takeItem(drag_widget.item) + + # after dragging the 'item', set the widgets' images + # so that it has it's default 'empty' images + drag_widget.item = None + self.updateImage(drag_widget) + + def dropObject(self, obj): + """Drops the object being dropped + @type obj: string + @param obj: The name of the object within + the dictionary 'self.buttons' + @return: None""" + try: + drop_widget = self.gui.findChild(name = obj) + drop_index = drop_widget.index + replace_item = drop_widget.item + + if data_drag.dragging: + container = self.container + drag_item = data_drag.dragged_item + #this will get the replacement item and the data for drag_drop if + ## there is an item all ready occupying the slot + if replace_item != None: + self.dragObject(obj) + container.placeItem(drag_item, drop_index) + + drop_widget.item = drag_item + self.updateImage(drop_widget) + #if there was no item the stop dragging and reset cursor + if replace_item == None: + data_drag.dragging = False + #reset the mouse cursor to the normal cursor + self.controller.resetMouseCursor() + except (Container.SlotBusy, Container.TooBig, Container.ItemSelf): + #Do we want to notify the player why the item can't be dropped? + pass + + def showContainer(self): + """Show the container + @return: None""" + # Prepare slots 1 through 9 + empty_image = "gui/inv_images/inv_backpack.png" + slot_count = 9 + for counter in range(1, slot_count+1): + slot_name = "Slot%i" % counter + self.empty_images[slot_name] = empty_image + widget = self.gui.findChild(name=slot_name) + widget.item = self.container.items.get(counter-1) + widget.index = counter-1 + self.updateImage(widget) + self.events_to_map[slot_name] = cbwa(self.dragDrop, slot_name) + self.events_to_map[slot_name + "/mouseReleased"] = \ + self.showContextMenu + + self.gui.mapEvents(self.events_to_map) + self.gui.show() + + def hideContainer(self): + """Hide the container + @return: None""" + if self.gui.isVisible(): + self.gui.hide() diff -r 000000000000 -r 7a89ea5404b1 gui/containergui_base.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gui/containergui_base.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,122 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from fife import fife +from fife.extensions import pychan + +from parpg.gui import drag_drop_data as data_drag +from parpg.objects.action import ACTIONS +from copy import deepcopy + +class ContainerGUIBase(object): + """ + Base class for windows that show the content of a container + """ + + + def __init__(self, controller, gui_file): + self.controller = controller + self.gui = pychan.loadXML(gui_file) + + def dragDrop(self, obj): + """Decide whether to drag or drop the image. + @type obj: string + @param obj: The name of the object within + the dictionary 'self.buttons' + @return: None""" + if(data_drag.dragging == True): + self.dropObject(obj) + elif(data_drag.dragging == False): + self.dragObject(obj) + + def dragObject(self, obj): + """Drag the selected object. + @type obj: string + @param obj: The name of the object within + the dictionary 'self.buttons' + @return: None""" + pass + + def dropObject(self, obj): + """Drops the object being dropped + @type obj: string + @param obj: The name of the object within + the dictionary 'self.buttons' + @return: None""" + pass + + + def createMenuItems(self, item, actions): + """Creates context menu items for all classes based on ContainerGUI""" + menu_actions = [] + for action_name in actions: + display_name = action_name + if action_name in ACTIONS: + param_dict = {} + param_dict["controller"] = self.controller + param_dict["commands"] = {} + if action_name == "Look": + param_dict["examine_name"] = item.name + param_dict["examine_desc"] = actions[action_name].\ + pop("text") + if action_name == "Read": + param_dict["text_name"] = item.name + param_dict["text"] = "" + if action_name == "Use": + param_dict["item"] = item + display_name = actions[action_name].pop("text") + if action_name == "Open": + param_dict["container"] = item + if action_name == "BrewBeer": + param_dict["pot"] = item + display_name = "Brew beer" + if actions[action_name]: + param_dict.update(actions[action_name]) + menu_actions.append([action_name, + display_name, + self.executeMenuItem, + ACTIONS[action_name]\ + (**param_dict)]) + return menu_actions + + def showContextMenu(self, event, widget): + """Decide whether to drag or drop the image. + @type obj: string + @param obj: The name of the object within + the dictionary 'self.buttons' + @return: None""" + if event.getButton() == event.RIGHT: + item = widget.item + if item and item.trueAttr("usable"): + actions = deepcopy(item.actions) + if not actions: + return + x_pos, y_pos = widget.getAbsolutePos() + x_pos += event.getX() + y_pos += event.getY() + menu_actions = self.createMenuItems(item, actions) + self.controller.view.hud.hideContextMenu() + self.controller.view.hud.showContextMenu(menu_actions, + (x_pos, + y_pos) + ) + + def executeMenuItem(self, action): + """Executes the items action + @param action: The action to run + @type action: Class derived from parpg.objects.action.Action + """ + action.execute() + + def updateImages(self): + pass diff -r 000000000000 -r 7a89ea5404b1 gui/dialogs.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gui/dialogs.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,32 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +import os + +from fife.extensions import pychan + +class RestartDialog(object): + def __init__(self, settings): + self.settings = settings + self.window = pychan.loadXML(os.path.join(self.settings.system_path, + self.settings.parpg.GuiPath, + 'restart_dialog.xml')) + self.window.mapEvents({'closeButton': self.hide}) + + def hide(self): + self.window.hide() + + def show(self): + self.window.show() diff -r 000000000000 -r 7a89ea5404b1 gui/dialoguegui.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gui/dialoguegui.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,165 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . +import logging + +from fife import fife +from fife.extensions import pychan +from fife.extensions.pychan import widgets + +from parpg.dialogueprocessor import DialogueProcessor + +logger = logging.getLogger('dialoguegui') + +class DialogueGUI(object): + """Window that handles the dialogues.""" + _logger = logging.getLogger('dialoguegui.DialogueGUI') + + def __init__(self, controller, npc, quest_engine, player_character): + self.active = False + self.controller = controller + self.dialogue_gui = pychan.loadXML("gui/dialogue.xml") + self.npc = npc + # TODO Technomage 2010-11-10: the QuestEngine should probably be + # a singleton-like object, which would avoid all of this instance + # handling. + self.quest_engine = quest_engine + self.player_character = player_character + + def initiateDialogue(self): + """Callback for starting a quest""" + self.active = True + stats_label = self.dialogue_gui.findChild(name='stats_label') + stats_label.text = u'Name: John Doe\nAn unnamed one' + events = { + 'end_button': self.handleEnd + } + self.dialogue_gui.mapEvents(events) + self.dialogue_gui.show() + self.setNpcName(self.npc.name) + self.setAvatarImage(self.npc.dialogue.avatar_path) + + game_state = {'npc': self.npc, 'pc': self.player_character, + 'quest': self.quest_engine} + try: + self.dialogue_processor = DialogueProcessor(self.npc.dialogue, + game_state) + self.dialogue_processor.initiateDialogue() + except (TypeError) as error: + self._logger.error(str(error)) + else: + self.continueDialogue() + + def setDialogueText(self, text): + """Set the displayed dialogue text. + @param text: text to display.""" + text = unicode(text) + speech = self.dialogue_gui.findChild(name='speech') + # to append text to npc speech box, uncomment the following line + #speech.text = speech.text + "\n-----\n" + unicode(say) + speech.text = text + self._logger.debug('set dialogue text to "{0}"'.format(text)) + + def continueDialogue(self): + """Display the dialogue text and responses for the current + L{DialogueSection}.""" + dialogue_processor = self.dialogue_processor + dialogue_text = dialogue_processor.getCurrentDialogueSection().text + self.setDialogueText(dialogue_text) + self.responses = dialogue_processor.continueDialogue() + self.setResponses(self.responses) + + def handleEntered(self, *args): + """Callback for when user hovers over response label.""" + pass + + def handleExited(self, *args): + """Callback for when user hovers out of response label.""" + pass + + def handleClicked(self, *args): + """Handle a response being clicked.""" + response_n = int(args[0].name.replace('response', '')) + response = self.responses[response_n] + dialogue_processor = self.dialogue_processor + dialogue_processor.reply(response) + if (not dialogue_processor.in_dialogue): + self.handleEnd() + else: + self.continueDialogue() + + def handleEnd(self): + """Handle the end of the conversation being reached, either from the + GUI or from within the conversation itself.""" + self.dialogue_gui.hide() + self.responses = [] + self.npc.behaviour.state = 1 + self.npc.behaviour.idle() + self.active = False + + def setNpcName(self, name): + """Set the NPC name to display on the dialogue GUI. + @param name: name of the NPC to set + @type name: basestring""" + name = unicode(name) + stats_label = self.dialogue_gui.findChild(name='stats_label') + try: + (first_name, desc) = name.split(" ", 1) + stats_label.text = u'Name: ' + first_name + "\n" + desc + except ValueError: + stats_label.text = u'Name: ' + name + + self.dialogue_gui.title = name + self._logger.debug('set NPC name to "{0}"'.format(name)) + + def setAvatarImage(self, image_path): + """Set the NPC avatar image to display on the dialogue GUI + @param image_path: filepath to the avatar image + @type image_path: basestring""" + avatar_image = self.dialogue_gui.findChild(name='npc_avatar') + avatar_image.image = image_path + + def setResponses(self, dialogue_responses): + """Creates the list of clickable response labels and sets their + respective on-click callbacks. + @param responses: list of L{DialogueResponses} from the + L{DialogueProcessor} + @type responses: list of L{DialogueResponses}""" + choices_list = self.dialogue_gui.findChild(name='choices_list') + choices_list.removeAllChildren() + for index, response in enumerate(dialogue_responses): + button = widgets.Label( + name="response{0}".format(index), + text=unicode(response.text), + hexpand="1", + min_size=(100,16), + max_size=(490,48), + position_technique='center:center' + ) + button.margins = (5, 5) + button.background_color = fife.Color(0, 0, 0) + button.color = fife.Color(0, 255, 0) + button.border_size = 0 + button.wrap_text = 1 + button.capture(lambda button=button: self.handleEntered(button), + event_name='mouseEntered') + button.capture(lambda button=button: self.handleExited(button), + event_name='mouseExited') + button.capture(lambda button=button: self.handleClicked(button), + event_name='mouseClicked') + choices_list.addChild(button) + self.dialogue_gui.adaptLayout(True) + self._logger.debug( + 'added {0} to response choice list'.format(response) + ) diff -r 000000000000 -r 7a89ea5404b1 gui/drag_drop_data.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gui/drag_drop_data.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,25 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +""" +This contains the data that tells the GUI whether something is being dragged, dropped etc. +It is in one place to allow communication between multiple windows +""" +dragging = False +dragged_image = None +dragged_type = None +dragged_item = None +dragged_widget = None +source_container = None diff -r 000000000000 -r 7a89ea5404b1 gui/filebrowser.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gui/filebrowser.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,159 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from fife.extensions import pychan +from fife.extensions.pychan import widgets + +import sys +import os +import logging + +logger = logging.getLogger('filebrowser') + +def u2s(string): + # TODO: cryptic function name + return string.encode(sys.getfilesystemencoding()) + +class FileBrowser(object): + """FileBrowser displays directory and file listings from the vfs. + The file_selected parameter is a callback invoked when a file selection + has been made; its signature must be file_selected(path,filename). If + select_dir is set, file_selected's filename parameter should be optional. + The save_file option provides a box for supplying a new filename that + doesn't exist yet. The select_dir option allows directories to be + selected as well as files.""" + def __init__(self, engine, settings, file_selected, gui_xml_path, + close_callback=None, save_file=False, select_dir=False, + extensions=('.dat',)): + self.engine = engine + self.settings = settings + print self.settings.parpg.SavesPath + self.file_selected = file_selected + + self._widget = None + self.save_file = save_file + self.select_dir = select_dir + self.close_callback = close_callback + self.gui_xml_path = gui_xml_path + + self.extensions = extensions + self.path = os.path.join(self.settings.user_path, + self.settings.parpg.SavesPath) + self.dir_list = [] + self.file_list = [] + + def close(self): + """Closes the browser""" + self._widget.hide() + if self.close_callback: + self.close_callback() + + def showBrowser(self): + """Shows the file dialog browser""" + if self._widget: + self._widget.show() + return + self._widget = pychan.loadXML(self.gui_xml_path) + self._widget.mapEvents({ + 'dirList' : self._setPath, + 'selectButton' : self._selectFile, + 'closeButton' : self.close + }) + self._setPath() + if self.save_file: + self._file_entry = widgets.TextField(name='saveField', text=u'') + self._widget.findChild(name="fileColumn").\ + addChild(self._file_entry) + self._widget.show() + + def _setPath(self): + """Path change callback.""" + selection = self._widget.collectData('dirList') + if not (selection < 0): + new_dir = u2s(self.dir_list[selection]) + lst = self.path.split('/') + if new_dir == '..' and lst[-1] != '..' and lst[-1] != '.': + lst.pop() + else: + lst.append(new_dir) + self.path = '/'.join(lst) + + def decodeList(list): + fs_encoding = sys.getfilesystemencoding() + if fs_encoding is None: fs_encoding = "ascii" + + new_list = [] + for i in list: + try: + new_list.append(unicode(i, fs_encoding)) + except: + new_list.append(unicode(i, fs_encoding, 'replace')) + logger.debug("WARNING: Could not decode item:\n" + "{0}".format(i)) + return new_list + + + + self.dir_list = [] + self.file_list = [] + + dir_list = ('..',) + filter(lambda d: not d.startswith('.'), \ + self.engine.getVFS().\ + listDirectories(self.path)) + file_list = filter(lambda f: f.split('.')[-1] in self.extensions, \ + self.engine.getVFS().listFiles(self.path)) + + self.dir_list = decodeList(dir_list) + self.file_list = decodeList(file_list) + self._widget.distributeInitialData({ + 'dirList' : self.dir_list, + 'fileList' : self.file_list + }) + + def _selectFile(self): + """ File selection callback. """ + self._widget.hide() + selection = self._widget.collectData('fileList') + + if self.save_file: + data = self._widget.collectData('saveField') + if data: + if (data.endswith(".dat")): + self.file_selected(self.path, \ + u2s(self._widget.collectData('saveField'))) + else: + self.file_selected(self.path, + u2s(self._widget.collectData('saveField')) + '.dat') + return + + + if selection >= 0 and selection < len(self.file_list): + self.file_selected(self.path, u2s(self.file_list[selection])) + return + + if self.select_dir: + self.file_selected(self.path) + return + + logger.error('no selection') + + def _warningMessage(self): + """Shows the warning message dialog when a file with a + faulty extension was selected.""" + window = widgets.Window(title="Warning") + text = "Please save the file as a .dat" + label = widgets.Label(text=text) + ok_button = widgets.Button(name="ok_button", text="Ok") + window.addChildren([label, ok_button]) + window.mapEvents({'ok_button':window.hide}) + window.show() diff -r 000000000000 -r 7a89ea5404b1 gui/hud.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gui/hud.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,505 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +import os +import logging + +from fife.extensions import pychan +from fife.extensions.pychan.tools import callbackWithArguments as cbwa + +from parpg.gui.filebrowser import FileBrowser +from parpg.gui.menus import ContextMenu, SettingsMenu +from parpg.gui import inventorygui +from parpg.gui.popups import ExaminePopup +from parpg.gui.containergui import ContainerGUI +from parpg.gui.dialoguegui import DialogueGUI +from parpg.gui import drag_drop_data as data_drag +from actionsbox import ActionsBox + +logger = logging.getLogger('hud') +class Hud(object): + """Main Hud class""" + def __init__(self, controller, settings, callbacks): + """Initialise the instance. + @type controller: Class derived from ControllerBase + @param controller: The current controller + @type settings: settings.Setting + @param settings: The settings + @type inv_model: dict + @type callbacks: dict + @param callbacks: a dict of callbacks + saveGame: called when the user clicks on Save + loadGame: called when the user clicks on Load + quitGame: called when the user clicks on Quit + @return: None""" + + # TODO: perhaps this should not be hard-coded here + pychan.registerWidget(ActionsBox) + self.hud = pychan.loadXML("gui/hud.xml") + self.controller = controller + self.engine = controller.engine + self.model = controller.model + self.settings = settings + self.inventory = None + self.character_screen = None + + self.save_game_callback = callbacks['saveGame'] + self.load_game_callback = callbacks['loadGame'] + self.quit_callback = callbacks['quitGame'] + + self.box_container = None + self.examine_box = None + self.context_menu = None + self.help_dialog = None + self.events_to_map = None + self.main_menu = None + self.menu_events = None + self.quit_window = None + self.bottom_panel = self.hud.findChild(name="mainHudWindow") + + self.actions_box = self.hud.findChild(name="actionsBox") + self.menu_displayed = False + self.inventory_storage = None + self.initializeHud() + self.initializeMainMenu() + self.initializeContextMenu() + self.initializeHelpMenu() + self.initializeEvents() + self.initializeQuitDialog() + self.initializeSettingsMenu() + + def _getEnabled(self): + """"Returns whether the gui widget is enabled or not""" + return self.hud.real_widget.isEnabled() + + def _setEnabled(self, enabled): + """"Sets whether the gui widget is enabled or not""" + self.hud.real_widget.setEnabled(enabled) + childs = self.hud.getNamedChildren() + for child_list in childs.itervalues(): + for child in child_list: + child.real_widget.setEnabled(enabled) + + enabled = property(_getEnabled, _setEnabled) + + def initializeHud(self): + """Initialize and show the main HUD + @return: None""" + self.events_to_map = {"menuButton":self.displayMenu, } + self.hud.mapEvents(self.events_to_map) + # set HUD size according to screen size + screen_width = self.engine.getSettings().getScreenWidth() + self.hud.findChild(name="mainHudWindow").size = (screen_width, 65) + self.hud.findChild(name="inventoryButton").position = \ + (screen_width-59, 7) + # add ready slots + ready1 = self.hud.findChild(name='hudReady1') + ready2 = self.hud.findChild(name='hudReady2') + ready3 = self.hud.findChild(name='hudReady3') + ready4 = self.hud.findChild(name='hudReady4') + + if (screen_width <=800) : + gap = 0 + else : + gap = 40 + ready1.position = (160+gap, 7) + ready2.position = (220+gap, 7) + ready3.position = (screen_width-180-gap, 7) + ready4.position = (screen_width-120-gap, 7) + self.actions_box.position = (280+gap, 5) + actions_width = screen_width - 470 - 2*gap + + self.actions_box.ContentBox.min_width = actions_width + self.actions_box.ContentBox.max_width = actions_width + + # and finally add an actions box + self.actions_box.min_size = (actions_width, 55) + self.actions_box.max_size = (actions_width, 55) + # now it should be OK to display it all + self.showHUD() + + def addAction(self, action): + """Add an action to the actions box. + @type action: (unicode) string + @param action: The text that you want to display in the actions box + @return: None""" + self.actions_box.addAction(action) + + def showHUD(self): + """Show the HUD. + @return: None""" + self.hud.show() + self.enabled = True + + def hideHUD(self): + """Hide the HUD. + @return: None""" + self.hud.hide() + self.enabled = False + + def initializeInventory(self): + """Initialize the inventory""" + if not self.inventory: + self.inventory = inventorygui.InventoryGUI(self.controller, + None, + None) +# inv_callbacks = { +# 'refreshReadyImages': self.refreshReadyImages, +# 'toggleInventoryButton': self.toggleInventoryButton, +# } +# self.inventory_storage = \ +# self.model.game_state.player_character.inventory +# if self.inventory == None: +# self.inventory = inventorygui.InventoryGUI(self.controller, +# self.inventory_storage, +# inv_callbacks) +# else: +# self.inventory.inventory_storage = self.inventory_storage +# self.refreshReadyImages() + + def initializeCharacterScreen(self): + """Initialize the character screen.""" + # TODO Technomage 2010-12-24: + if not self.character_screen: + self.character_screen = pychan.loadXML('gui/character_screen.xml') + + + def initializeContextMenu(self): + """Initialize the Context Menu + @return: None""" + self.context_menu = ContextMenu (self.engine, [], (0, 0)) + + def showContextMenu(self, data, pos): + """Display the Context Menu with model at pos + @type model: list + @param model: model to pass to context menu + @type pos: tuple + @param pos: tuple of x and y coordinates + @return: None""" + self.context_menu = ContextMenu(self.engine, data, pos) + self.context_menu.show() + + def hideContextMenu(self): + """Hides the context menu + @return: None""" + self.context_menu.hide() + + def initializeMainMenu(self): + """Initalize the main menu. + @return: None""" + self.main_menu = pychan.loadXML("gui/hud_pause_menu.xml") + #TODO: find more suitalbe place for onOptilonsPress implementation + self.menu_events = {"resumeButton": self.hideMenu, + "settingsButton": self.displaySettings, + "helpButton": self.displayHelp} + self.main_menu.mapEvents(self.menu_events) + + def displayMenu(self): + """Displays the main in-game menu. + @return: None""" + self.stopActions() + if (self.menu_displayed == False): + self.main_menu.show() + self.menu_displayed = True + self.model.pause(True) + self.controller.pause(True) + self.enabled = False + elif (self.menu_displayed == True): + self.hideMenu() + + def hideMenu(self): + """Hides the main in-game menu. + @return: None""" + self.main_menu.hide() + self.menu_displayed = False + self.model.pause(False) + self.controller.pause(False) + self.enabled = True + + def initializeSettingsMenu(self): + self.settings_menu = SettingsMenu(self.engine, self.settings) + + def displaySettings(self): + self.settings_menu.show() + + def initializeHelpMenu(self): + """Initialize the help menu + @return: None""" + self.help_dialog = pychan.loadXML("gui/help.xml") + help_events = {"closeButton":self.help_dialog.hide} + self.help_dialog.mapEvents(help_events) + main_help_text = u"Welcome to Post-Apocalyptic RPG or PARPG![br][br]"\ + "This game is still in development, so please expect for there to be "\ + "bugs and[br]feel free to tell us about them at "\ + "http://www.forums.parpg.net.[br]This game uses a "\ + "\"Point 'N' Click\" interface, which means that to move around,[br]"\ + "just click where you would like to go and your character will move "\ + "there.[br]PARPG also utilizes a context menu. To access this, just "\ + "right click anywhere[br]on the screen and a menu will come up. This "\ + "menu will change depending on[br]what you have clicked on, hence "\ + "it's name \"context menu\".[br][br]" + + k_text = u" Keybindings" + k_text += "[br] A : Add a test action to the actions display" + k_text += "[br] I : Toggle the inventory screen" + k_text += "[br] F7 : Take a screenshot" + k_text += "[br] (Saves to screenshots directory)" + k_text += "[br] F10 : Toggle console" + k_text += "[br] PAUSE : (Un)Pause the game" + k_text += "[br] Q : Quit the game" + self.help_dialog.distributeInitialData({ + "MainHelpText":main_help_text, + "KeybindText":k_text + }) + + def displayHelp(self): + """Display the help screen. + @return: None""" + self.help_dialog.show() + + def saveGame(self): + """ Called when the user wants to save the game. + @return: None""" + self.stopActions() + xml_path = os.path.join(self.settings.system_path, + self.settings.parpg.GuiPath, + 'savebrowser.xml') + save_browser = FileBrowser(self.engine, + self.settings, + self.save_game_callback, + xml_path, + self.loadsave_close, + save_file=True, + extensions=('.dat')) + save_browser.showBrowser() + self.controller.pause(True) + self.model.pause(True) + self.enabled = False + + def stopActions(self): + """This method stops/resets actions that are currently performed + like dragging an item. + This is done to be able to savely perform other actions that might + interfere with current running ones.""" + #Reset dragging - move item back to its old container + if data_drag.dragging: + data_drag.source_container.placeItem(data_drag.dragged_item) + data_drag.dragging = False + data_drag.dragged_item = None + if self.inventory: + self.inventory.closeInventory() + + def newGame(self): + """Called when user request to start a new game. + @return: None""" + self.stopActions() + logger.info('new game') + + def loadsave_close(self): + """Called when the load/save filebrowser was closed without a file selected""" + if not self.menu_displayed: + self.enabled = True + self.model.pause(False) + self.controller.pause(False) + + def loadGame(self): + """ Called when the user wants to load a game. + @return: None""" + self.stopActions() + xml_path = os.path.join(self.settings.system_path, + self.settings.parpg.GuiPath, + 'loadbrowser.xml') + load_browser = FileBrowser(self.engine, + self.settings, + self.load_game_callback, + xml_path, + close_callback = self.loadsave_close, + save_file=False, + extensions=('.dat')) + load_browser.showBrowser() + self.model.pause(True) + self.controller.pause(True) + self.enabled = False + + def initializeQuitDialog(self): + """Creates the quit confirmation dialog + @return: None""" + self.quit_window = pychan.widgets.Window(title=unicode("Quit?"), \ + min_size=(200,0)) + + hbox = pychan.widgets.HBox() + are_you_sure = "Are you sure you want to quit?" + label = pychan.widgets.Label(text=unicode(are_you_sure)) + yes_button = pychan.widgets.Button(name="yes_button", + text=unicode("Yes"), + min_size=(90,20), + max_size=(90,20)) + no_button = pychan.widgets.Button(name="no_button", + text=unicode("No"), + min_size=(90,20), + max_size=(90,20)) + + self.quit_window.addChild(label) + hbox.addChild(yes_button) + hbox.addChild(no_button) + self.quit_window.addChild(hbox) + + events_to_map = { "yes_button": self.quit_callback, + "no_button": self.quit_window.hide } + + self.quit_window.mapEvents(events_to_map) + + + def quitGame(self): + """Called when user requests to quit game. + @return: None""" + self.stopActions() + self.quit_window.show() + + def toggleInventoryButton(self): + """Manually toggles the inventory button. + @return: None""" + button = self.hud.findChild(name="inventoryButton") + if button.toggled == 0: + button.toggled = 1 + else: + button.toggled = 0 + + def toggleInventory(self, toggle_image=True): + """Displays the inventory screen + @return: None""" + if self.inventory == None: + self.initializeInventory() + self.inventory.toggleInventory(toggle_image) + + def toggleCharacterScreen(self): + if not self.character_screen: + self.initializeCharacterScreen() + if not self.character_screen.isVisible(): + self.character_screen.show() + else: + self.character_screen.hide() + + def refreshReadyImages(self): + """Make the Ready slot images on the HUD be the same as those + on the inventory + @return: None""" + for ready in range(1, 5): + button = self.hud.findChild(name=("hudReady%d" % ready)) + if self.inventory_storage == None : + origin = None + else: + origin = self.inventory_storage.getItemsInSlot('ready', ready-1) + if origin == None: + self.setImages(button, + self.inventory.slot_empty_images['ready']) + else: + self.setImages(button, origin.getInventoryThumbnail()) + + def setImages(self, widget, image): + """Set the up, down, and hover images of an Imagebutton. + @type widget: pychan.widget + @param widget: widget to set + @type image: string + @param image: image to use + @return: None""" + widget.up_image = image + widget.down_image = image + widget.hover_image = image + + def initializeEvents(self): + """Intialize Hud events + @return: None""" + events_to_map = {} + + # when we click the toggle button don't change the image + events_to_map["inventoryButton"] = cbwa(self.toggleInventory, False) + events_to_map["saveButton"] = self.saveGame + events_to_map["loadButton"] = self.loadGame + + hud_ready_buttons = ["hudReady1", "hudReady2", \ + "hudReady3", "hudReady4"] + + for item in hud_ready_buttons: + events_to_map[item] = cbwa(self.readyAction, item) + + self.hud.mapEvents(events_to_map) + + menu_events = {} + menu_events["newButton"] = self.newGame + menu_events["quitButton"] = self.quitGame + menu_events["saveButton"] = self.saveGame + menu_events["loadButton"] = self.loadGame + self.main_menu.mapEvents(menu_events) + + def readyAction(self, ready_button): + """ Called when the user selects a ready button from the HUD """ + text = "Used the item from %s" % ready_button + self.addAction(text) + + def createBoxGUI(self, title, container): + """Creates a window to display the contents of a box + @type title: string + @param title: The title for the window + @param items: The box to display + @return: A new ContainerGui""" + events = {'takeAllButton':self.hideContainer, + 'closeButton':self.hideContainer} + #hide previous container if any, to avoid orphaned dialogs + self.hideContainer() + + self.box_container = ContainerGUI(self.controller, + unicode(title), container) + self.box_container.gui.mapEvents(events) + self.box_container.showContainer() + return self.box_container + + def hideContainer(self): + """Hide the container box + @return: None""" + if self.box_container: + self.box_container.hideContainer() + #TODO: Move the close() call into OpenBoxAction(). This can be done + # after createBoxGUI becomes a blocking call or it's otherwise + # possible to wait till the box GUI is closed. + if self.box_container.container.trueAttr("openable"): + self.box_container.container.close() + self.box_container = None + + def createExamineBox(self, title, desc): + """Create an examine box. It displays some textual description of an + object + @type title: string + @param title: The title of the examine box + @type desc: string + @param desc: The main body of the examine box + @return: None""" + if self.examine_box is not None: + self.examine_box.closePopUp() + self.examine_box = ExaminePopup(self.engine, title, desc) + self.examine_box.showPopUp() + + def showDialogue(self, npc): + """Show the NPC dialogue window + @type npc: actors.NonPlayerCharacter + @param npc: the npc that we are having a dialogue with + @return: The dialogue""" + self.stopActions() + dialogue = DialogueGUI( + self.controller, + npc, + self.model.game_state.quest_engine, + self.model.game_state.player_character) + return dialogue diff -r 000000000000 -r 7a89ea5404b1 gui/inventorygui.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gui/inventorygui.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,365 @@ +#!/usr/bin/env python + +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +from fife.extensions.pychan.tools import callbackWithArguments as cbwa +from fife.extensions import pychan +from fife.extensions.pychan.attrs import UnicodeAttr + +from parpg.gui import drag_drop_data as data_drag +from parpg.objects.base import Container +from parpg.gui.containergui_base import ContainerGUIBase +from parpg.objects.action import ACTIONS + +import logging + +logger = logging.getLogger('action') + +class EquipmentSlot(pychan.VBox): + ATTRIBUTES = pychan.VBox.ATTRIBUTES + [UnicodeAttr('label_text')] + + def _setLabelText(self, text): + label = self.findChild() + label.text = unicode(text) + label.resizeToContent() + self.margins = ( + int((self.width - label.width) / 2.0), + int((self.height - label.height) / 2.0) + ) + + def _getLabelText(self): + label = self.findChild() + return label.text + + label_text = property(fget=_getLabelText, fset=_setLabelText) + + def __init__(self, label_text=u'equipment', min_size=(50, 50), + max_size=(50, 50), margins=None, + background_image="gui/inv_images/inv_background.png", + **kwargs): + pychan.VBox.__init__(self, min_size=min_size, max_size=max_size, + **kwargs) + self.background_image = background_image + label = pychan.Label(text=unicode(label_text)) + self.addChild(label) + self.label_text = label_text + self.adaptLayout() + if self.parent is not None: + self.beforeShow() + + +class InventoryGrid(pychan.VBox): + ATTRIBUTES = pychan.VBox.ATTRIBUTES + [pychan.attrs.PointAttr('grid_size')] + + def _setNColumns(self, n_columns): + n_rows = self.grid_size[1] + self.grid_size = (n_columns, n_rows) + + def _getNColumns(self): + n_columns = self.grid_size[0] + return n_columns + n_columns = property(fget=_getNColumns, fset=_setNColumns) + + def _setNRows(self, n_rows): + n_columns = self.grid_size[0] + self.grid_size = (n_columns, n_rows) + + def _getNRows(self): + n_rows = self.grid_size[1] + return n_rows + n_rows = property(fget=_getNRows, fset=_getNColumns) + + def _setGridSize(self, grid_size): + n_columns, n_rows = grid_size + self.removeAllChildren() + for row_n in range(n_rows): + row_size = (n_columns * 50, 50) + row = pychan.HBox(min_size=row_size, max_size=row_size, + padding=self.padding) + row.border_size = 1 + row.opaque = 0 + for column_n in range(n_columns): + slot = pychan.Icon(min_size=(50, 50), max_size=(50, 50)) + slot.border_size = 1 + row.addChild(slot) + self.addChild(row) + self.min_size = ((n_columns * 50) + 2, (n_rows * 50) + 2) + self.max_size = self.min_size + + def _getGridSize(self): + n_rows = len(self.children) + n_columns = len(self.children[0].children) + return (n_rows, n_columns) + grid_size = property(fget=_getGridSize, fset=_setGridSize) + + def __init__(self, grid_size=(2, 2), padding=0, **kwargs): + pychan.VBox.__init__(self, padding=padding, **kwargs) + self.opaque = 0 + self.grid_size = grid_size + self.border_size = 1 + + +class InventoryGUI(ContainerGUIBase): + def __init__(self, controller, inventory, callbacks): + super(InventoryGUI, self).__init__(controller, "gui/inventory.xml") + self.engine = controller.engine + self.inventory_shown = False + render_backend = self.engine.getRenderBackend() + screen_mode = render_backend.getCurrentScreenMode() + screen_width, screen_height = (screen_mode.getWidth(), + screen_mode.getHeight()) + widget_width, widget_height = self.gui.size + self.gui.position = ((screen_width - widget_width) / 2, + (screen_height - widget_height) / 2) + + def toggleInventory(self, toggleImage=True): + """Pause the game and enter the inventory screen, or close the + inventory screen and resume the game. + @type toggleImage: bool + @param toggleImage: + Call toggleInventoryCallback if True. Toggling via a + keypress requires that we toggle the Hud inventory image + explicitly. Clicking on the Hud inventory button toggles the + image implicitly, so we don't change it. + @return: None""" + if not self.inventory_shown: + self.showInventory() + self.inventory_shown = True + else: + self.closeInventory() + self.inventory_shown = False + + def showInventory(self): + self.gui.show() + + def closeInventory(self): + self.gui.hide() + + +class _InventoryGUI(ContainerGUIBase): + """Inventory GUI class""" + def __init__(self, controller, inventory, callbacks): + """Initialise the instance. + @param controller: Current Controller + @type controller: Class derived from ControllerBase + @type inventory: Inventory + @param inventory: An inventory object to be displayed and manipulated + @type callbacks: dict + @param callbacks: a dict of callbacks + refreshReadyImages: + Function that will make the ready slots on the HUD + reflect those within the inventory + toggleInventoryButton: + Function that will toggle the state of the inventory button + @return: None""" + super(InventoryGUI, self).__init__(controller, "gui/inventory.xml") + self.engine = controller.engine + self.readyCallback = callbacks['refreshReadyImages'] + self.toggleInventoryButtonCallback = callbacks['toggleInventoryButton'] + self.original_cursor_id = self.engine.getCursor().getId() + + self.inventory_shown = False + events_to_map = {} + self.inventory_storage = inventory + + # Buttons of inventory arranged by slots + + self.slot_buttons = {'head': ('Head',), 'chest': ('Body',), + 'left_arm': ('LeftHand',), + 'right_arm': ('RightHand',), + 'hips' : ('Belt',), 'left_leg': ('LeftFoot',), + 'right_leg': ('RightFoot',), + 'left_hand': ('LeftHeld',), + 'right_hand': ('RightHeld',), + 'backpack': ('A1', 'A2', 'A3', 'A4', 'A5', + 'B1', 'B2', 'B3', 'B4', 'B5', + 'C1', 'C2', 'C3', 'C4', 'C5', + 'D1', 'D2', 'D3', 'D4', 'D5'), + 'ready': ('Ready1', 'Ready2', 'Ready3', 'Ready4') + } + # the images that should be used for the buttons when they are "empty" + self.slot_empty_images = {'head':'gui/inv_images/inv_head.png', + 'chest':'gui/inv_images/inv_torso.png', + 'left_arm':'gui/inv_images/inv_lhand.png', + 'right_arm':'gui/inv_images/inv_rhand.png', + 'hips':'gui/inv_images/inv_belt.png', + 'left_leg':'gui/inv_images/inv_lfoot.png', + 'right_leg':'gui/inv_images/inv_rfoot.png', + 'left_hand':'gui/inv_images/inv_litem.png', + 'right_hand':'gui/inv_images/inv_ritem.png', + 'backpack':'gui/inv_images/inv_backpack.png', + 'ready':'gui/inv_images/inv_belt_pouches.png', + } + self.updateInventoryButtons() + + for slot in self.slot_buttons: + for _, button in enumerate(self.slot_buttons[slot]): + events_to_map[button] = cbwa(self.dragDrop, button) + events_to_map[button + "/mouseReleased"] = \ + self.showContextMenu + events_to_map['close_button'] = self.closeInventoryAndToggle + self.gui.mapEvents(events_to_map) + # TODO: Why the commented out code? + # self.resetMouseCursor() + + def updateImages(self): + self.updateInventoryButtons() + + def updateInventoryButtons (self): + for slot in self.slot_buttons: + for index, button in enumerate(self.slot_buttons[slot]): + widget = self.gui.findChild(name=button) + widget.slot = slot + widget.index = index + widget.item = self.inventory_storage.getItemsInSlot(widget.slot, + widget.index) + self.updateImage(widget) + + def updateImage(self, button): + if (button.item == None): + image = self.slot_empty_images[button.slot] + else: + image = button.item.getInventoryThumbnail() + button.up_image = image + button.down_image = image + button.hover_image = image + + def closeInventory(self): + """Close the inventory. + @return: None""" + self.gui.hide() + + def closeInventoryAndToggle(self): + """Close the inventory screen. + @return: None""" + self.closeInventory() + self.toggleInventoryButtonCallback() + self.inventory_shown = False + + def toggleInventory(self, toggleImage=True): + """Pause the game and enter the inventory screen, or close the + inventory screen and resume the game. + @type toggleImage: bool + @param toggleImage: + Call toggleInventoryCallback if True. Toggling via a + keypress requires that we toggle the Hud inventory image + explicitly. Clicking on the Hud inventory button toggles the + image implicitly, so we don't change it. + @return: None""" + if not self.inventory_shown: + self.showInventory() + self.inventory_shown = True + else: + self.closeInventory() + self.inventory_shown = False + + if toggleImage: + self.toggleInventoryButtonCallback() + + def showInventory(self): + """Show the inventory. + @return: None""" + self.updateInventoryButtons() + self.gui.show() + + def dragObject(self, obj): + """Drag the selected object. + @type obj: string + @param obj: The name of the object within + the dictionary 'self.buttons' + @return: None""" + # get the widget from the inventory with the name obj + drag_widget = self.gui.findChild(name = obj) + drag_item = drag_widget.item + # only drag if the widget is not empty + if (drag_item != None): + # get the item that the widget is 'storing' + data_drag.dragged_item = drag_widget.item + # get the up and down images of the widget + up_image = drag_widget.up_image + down_image = drag_widget.down_image + # set the mouse cursor to be the widget's image + self.controller.setMouseCursor(up_image.source,down_image.source) + data_drag.dragged_image = up_image.source + data_drag.dragging = True + data_drag.dragged_widget = drag_widget + data_drag.source_container = self.inventory_storage + + self.inventory_storage.takeItem(drag_widget.item) + # after dragging the 'item', set the widgets' images + # so that it has it's default 'empty' images + drag_widget.item = None + self.updateImage(drag_widget) + + + def dropObject(self, obj): + """Drops the object being dropped + @type obj: string + @param obj: The name of the object within + the dictionary 'self.buttons' + @return: None""" + drop_widget = self.gui.findChild(name = obj) + drop_slot, drop_index = drop_widget.slot, drop_widget.index + replace_item = None + try : + if data_drag.dragging: + inventory = self.inventory_storage + drag_item = data_drag.dragged_item + #this will get the replacement item and data for drag_drop if + ## there is an item All ready occupying the slot + if not inventory.isSlotEmpty(drop_slot, drop_index): + #get the item and then remove it from the inventory + replace_item = inventory.getItemsInSlot \ + (drop_slot, drop_index) + self.dragObject(obj) + self.inventory_storage.moveItemToSlot(drag_item, + drop_slot, + drop_index) + + if drop_widget.slot == 'ready': + self.readyCallback() + + if replace_item == None: + self.controller.resetMouseCursor() + data_drag.dragging = False + except Container.TooBig : + logger.warning("%s too big to fit " + "into %s" % (data_drag.dragged_item, + drop_widget.slot)) + except (Container.SlotBusy, Container.ItemSelf): + pass + self.updateInventoryButtons() + + def createMenuItems(self, item, actions): + """Creates context menu items for the InventoryGUI""" + menu_actions = super(InventoryGUI, self).createMenuItems(item, actions) + param_dict = {} + param_dict["controller"] = self.controller + param_dict["commands"] = {} + param_dict["item"] = item + param_dict["container_gui"] = self + menu_actions.append(["Drop", + "Drop", + self.executeMenuItem, + ACTIONS["DropFromInventory"](**param_dict)]) + return menu_actions + + def getImage(self, name): + """Return a current image from the inventory + @type name: string + @param name: name of image to get + @return: None""" + return self.gui.findChild(name = name) diff -r 000000000000 -r 7a89ea5404b1 gui/menus.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gui/menus.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,156 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +import os +import logging + +from fife.extensions import pychan +from dialogs import RestartDialog + +logger = logging.getLogger('menus') + +class ContextMenu(object): + def __init__(self, engine, menu_items, pos): + """@type engine: engine.Engine + @param engine: An instance of the class Engine from engine.py + @type menu_items: list + @param menu_items: A list of items containing the name and + text for the menu item and callback + i.e. [["menu", "Some text", Callback] + @type pos: (int, int) + @param pos: Screen position to use + @return: None""" + self.vbox = pychan.widgets.VBox(position=pos) + events_to_map = {} + for item in menu_items: + p = pychan.widgets.Button(name=item[0], text=unicode(item[1])) + self.vbox.addChild(p) + events_to_map [item[0]] = self.actionDecorator(*item[2:]) + self.vbox.mapEvents(events_to_map) + + def show(self): + """Shows the context menu""" + self.vbox.show() + def hide(self): + """Hides the context menu""" + self.vbox.hide() + + def actionDecorator (self,func, *args, **kwargs): + """This function is supposed to add some generic that should be + executed before and/or after an action is fired through the + context menu. + + @type func: Any callable + @param func: The original action function + @param args: Unpacked list of positional arguments + @param kwargs: Unpacked list of keyword arguments + @return: A wrapped version of func""" + def decoratedFunc (): + """ This is the actual wrapped version of func, that is returned. + It takes no external arguments, so it can safely be passed around + as a callback.""" + # some stuff that we do before the actual action is executed + self.hide() + # run the action function, and record the return value + ret_val = func (*args,**kwargs) + # we can eventually add some post-action code here, if needed (e.g. logging) + pass + # return the value, as if the original function was called + return ret_val + return decoratedFunc + +class SettingsMenu(object): + def __init__(self, engine, settings): + self.engine = engine + self.settings = settings + + width = self.settings.fife.ScreenWidth + height = self.settings.fife.ScreenHeight + + # available options + screen_modes = self.engine.getDeviceCaps().getSupportedScreenModes() + resolutions = list(set([(mode.getWidth(), mode.getHeight()) + for mode in screen_modes])) + self.resolutions = ["{0}x{1}".format(item[0], item[1]) + for item in sorted(resolutions)[1:]] + + self.render_backends = ['OpenGL', 'SDL'] + self.lighting_models = range(3) + + # selected options + self.resolution = "{0}x{1}".format(width, height) + self.render_backend = self.settings.fife.RenderBackend + self.lighting_model = self.settings.fife.Lighting + self.fullscreen = self.settings.fife.FullScreen + self.sound = self.settings.fife.EnableSound + + self.window = pychan.loadXML(os.path.join(self.settings.system_path, + self.settings.parpg.GuiPath, + 'settings_menu.xml')) + self.restart_dialog = RestartDialog(self.settings) + self.window.mapEvents({'okButton': self.save, + 'cancelButton': self.hide, + 'defaultButton': self.reset}) + self.initializeWidgets() + self.fillWidgets() + + def initializeWidgets(self): + initial_data = {'screen_resolution': self.resolutions, + 'render_backend': self.render_backends, + 'lighting_model': self.lighting_models} + + self.window.distributeInitialData(initial_data) + + + def fillWidgets(self): + resolution = self.resolutions.index(self.resolution) + backend = self.render_backends.index(self.render_backend) + lighting = self.lighting_models.index(self.lighting_model) + + self.window.distributeData({'screen_resolution': resolution, + 'render_backend': backend, + 'lighting_model': lighting, + 'enable_fullscreen': self.fullscreen, + 'enable_sound': self.sound}) + + def show(self): + self.window.show() + + def hide(self): + self.window.hide() + + def reset(self): + self.settings.read(self.settings.system_path) + self.fillWidgets() + + def save(self): + (resolution, backend, lighting, + fullscreen, sound) = self.window.collectData('screen_resolution', + 'render_backend', + 'lighting_model', + 'enable_fullscreen', + 'enable_sound') + + width, height = self.resolutions[resolution].split('x') + self.settings.fife.ScreenWidth = width + self.settings.fife.ScreenHeight = height + self.settings.fife.RenderBackend = self.render_backends[backend] + self.settings.fife.Lighting = self.lighting_models[lighting] + self.settings.fife.FullScreen = fullscreen + self.settings.fife.EnableSound = sound + self.settings.write() + + self.restart_dialog.show() + self.hide() diff -r 000000000000 -r 7a89ea5404b1 gui/popups.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gui/popups.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,60 @@ +#/usr/bin/python + +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +from fife.extensions import pychan + +class ExaminePopup(): + """Create a popup for when you click examine on an object""" + def __init__(self, engine, object_title, desc): + """Initialize the popup + @type engine: fife.Engine + @param engine: an instance of the fife engine + @type object_title: string + @param object_title: The title for the window, probably should just + be the name of the object + @type desc: string + @param desc: The description of the object + @return: None""" + self.engine = engine + + self.examine_window = pychan.widgets.\ + Window(title=unicode(object_title), + position_technique="center:center", + min_size=(175,175)) + + self.scroll = pychan.widgets.ScrollArea(name='scroll', size=(150,150)) + self.description = pychan.widgets.Label(name='descText', + text=unicode(desc), + wrap_text=True) + self.description.max_width = 170 + self.scroll.addChild(self.description) + self.examine_window.addChild(self.scroll) + + self.close_button = pychan.widgets.Button(name='closeButton', + text=unicode('Close')) + self.examine_window.addChild(self.close_button) + + self.examine_window.mapEvents({'closeButton':self.examine_window.hide}) + + def closePopUp(self): + # TODO: missing function information + if self.examine_window.isVisible(): + self.examine_window.hide() + + def showPopUp(self): + # TODO: missing function information + self.examine_window.show() diff -r 000000000000 -r 7a89ea5404b1 gui/spinners.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gui/spinners.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,234 @@ +from fife.extensions.pychan.widgets import (ImageButton, TextField, HBox, + Spacer) +from fife.extensions.pychan.attrs import Attr, IntAttr, BoolAttr + +class ListAttr(Attr): + def parse(self, value): + list_ = value.split(',') + return list_ + + +class Spinner(HBox): + ATTRIBUTES = HBox.ATTRIBUTES + [ + ListAttr('items'), + IntAttr('default_item_n'), + BoolAttr('circular'), + ] + + def default_item_n(): + def fget(self): + return self._default_item_n + + def fset(self, index): + if len(self.items) -1 >= index: + self._default_item_n = index + self.text_field.text = self.items[index] + else: + error_message = \ + 'default_item_n exceeds number of items in spinner' + raise ValueError(error_message) + + return locals() + default_item_n = property(**default_item_n()) + + def items(): + def fget(self): + return self._items + + def fset(self, items): + self._items = map(unicode, items) + if self.default_item_n > len(items) - 1: + self.default_item_n = 0 + self.text_field.text = self.items[self.default_item_n] if \ + len(self.items) > 0 else u'' + + return locals() + items = property(**items()) + + def background_color(): + def fget(self): + return self.text_field.background_color + + def fset(self, background_color): + self.text_field.background_color = background_color + + return locals() + background_color = property(**background_color()) + + def font(): + def fget(self): + return self.text_field.font + + def fset(self, font): + self.text_field.font = font + + return locals() + font = property(**font()) + + def background_color(): + def fget(self): + return self.text_field.background_color + + def fset(self, background_color): + self.text_field.background_color = background_color + + return locals() + background_color = property(**background_color()) + + def min_size(): + def fget(self): + return self._min_size + + def fset(self, min_size): + self._min_size = min_size + self.decrease_button.capture(self.previousItem) + increase_button_width, increase_button_height = \ + self.increase_button.size + decrease_button_width, decrease_button_height = \ + self.decrease_button.size + text_field_width = min_size[0] - (2 * self.padding) - \ + (increase_button_width + decrease_button_width) + self.text_field.min_width = text_field_width + self.text_field.max_width = text_field_width + self.text_field.min_height = min_size[1] + + return locals() + min_size = property(**min_size()) + + + def max_size(): + def fget(self): + return self._max_size + + def fset(self, max_size): + self._max_size = max_size + self.decrease_button.capture(self.previousItem) + increase_button_width, increase_button_height = \ + self.increase_button.size + decrease_button_width, decrease_button_height = \ + self.decrease_button.size + text_field_width = max_size[0] - (2 * self.padding) - \ + (increase_button_width + decrease_button_width) + self.text_field.max_width = text_field_width + self.text_field.max_height = max_size[1] + + return locals() + max_size = property(**max_size()) + + def __init__(self, items=None, default_item_n=0, circular=True, + min_size=(50, 14), max_size=(50, 14), font=None, background_color=None, **kwargs): + self._current_index = 0 + self._items = map(unicode, items) if items is not None else [] + self._default_item_n = default_item_n + self._min_size = min_size + self.circular = circular + padding = 1 + self.text_field = TextField(background_color=background_color) + self.decrease_button = ImageButton( + up_image='gui/buttons/left_arrow_up.png', + down_image='gui/buttons/left_arrow_down.png', + hover_image='gui/buttons/left_arrow_hover.png', + ) + # FIXME Technomage 2011-03-05: This is a hack to prevent the button + # from expanding width-wise and skewing the TextField orientation. + # Max size shouldn't be hard-coded like this though... + self.decrease_button.max_size = (12, 12) + self.decrease_button.capture(self.previousItem) + self.increase_button = ImageButton( + up_image='gui/buttons/right_arrow_up.png', + down_image='gui/buttons/right_arrow_down.png', + hover_image='gui/buttons/right_arrow_hover.png', + ) + self.increase_button.capture(self.nextItem) + increase_button_width, increase_button_height = \ + self.increase_button.size + decrease_button_width, decrease_button_height = \ + self.decrease_button.size + self.text_field = TextField(font=font) + text_field_width = min_size[0] - (2 * padding) - \ + (increase_button_width + decrease_button_width) + self.text_field.min_width = text_field_width + self.text_field.max_width = text_field_width + self.text_field.min_height = min_size[1] + self.text_field.text = self.items[default_item_n] if \ + len(self.items) > 0 else u'' + HBox.__init__(self, **kwargs) + self.opaque = 0 + self.padding = padding + self.margins = (0, 0) + self.addChildren(self.decrease_button, self.text_field, + self.increase_button) + + def nextItem(self, event, widget): + if self.circular: + if self._current_index < len(self.items) - 1: + self._current_index += 1 + else: + self._current_index = 0 + self.text_field.text = self.items[self._current_index] + elif self._current_index < len(self.items) - 1: + self._current_index += 1 + self.text_field.text = self.items[self._current_index] + + def previousItem(self, event, widget): + if self.circular: + if self._current_index > 0: + self._current_index -= 1 + else: + self._current_index = len(self.items) - 1 + self.text_field.text = self.items[self._current_index] + elif self._current_index > 0: + self._current_index -= 1 + self.text_field.text = self.items[self._current_index] + + +class IntSpinner(Spinner): + ATTRIBUTES = Spinner.ATTRIBUTES + [ + IntAttr('lower_limit'), + IntAttr('upper_limit'), + IntAttr('step_size'), + ] + + def lower_limit(): + def fget(self): + return self._lower_limit + + def fset(self, lower_limit): + self._lower_limit = lower_limit + integers = range(lower_limit, self.upper_limit + 1, self.step_size) + self.items = integers + + return locals() + lower_limit = property(**lower_limit()) + + def upper_limit(): + def fget(self): + return self._upper_limit + + def fset(self, upper_limit): + self._upper_limit = upper_limit + integers = range(self.lower_limit, upper_limit + 1, self.step_size) + self.items = integers + + return locals() + upper_limit = property(**upper_limit()) + + def step_size(): + def fget(self): + return self._step_size + + def fset(self, step_size): + self._step_size = step_size + integers = range(self.lower_limit, self.upper_limit + 1, step_size) + self.items = integers + + return locals() + step_size = property(**step_size()) + + def __init__(self, lower_limit=0, upper_limit=100, step_size=1, **kwargs): + self._lower_limit = lower_limit + self._upper_limit = upper_limit + self._step_size = step_size + integers = range(lower_limit, upper_limit + 1, step_size) + Spinner.__init__(self, items=integers, **kwargs) + diff -r 000000000000 -r 7a89ea5404b1 gui/tabwidget.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gui/tabwidget.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,105 @@ +from fife.extensions.pychan.widgets import VBox, HBox, ScrollArea, Button +from fife.extensions.pychan.tools import callbackWithArguments + +class TabWidget(VBox): + def min_size(): + def fget(self): + return self._min_size + + def fset(self, min_size): + self._min_size = min_size + self.view.min_size = min_size + self.adaptLayout() + + return locals() + min_size = property(**min_size()) + + def max_size(): + def fget(self): + return self._max_size + + def fset(self, max_size): + self._max_size = max_size + self.view.max_size = max_size + self.adaptLayout() + + return locals() + max_size = property(**max_size()) + + def opaque(): + def fget(self): + return self._getOpaque() + + def fset(self, opaque): + self._setOpaque(opaque) + self.view.opaque = opaque + base_color = self.view.base_color + base_red = base_color.r + base_green = base_color.g + base_blue = base_color.b + background_color = self.view.background_color + background_red = background_color.r + background_green = background_color.g + background_blue = background_color.b + alpha = 255 if opaque else 0 + self.view.base_color = (base_red, base_green, base_blue, alpha) + self.view.background_color = (background_red, background_green, + background_blue, alpha) + + return locals() + opaque = property(**opaque()) + + def border_size(): + def fget(self): + frame = self.findChild(name='frame') + return frame.border_size + + def fset(self, border_size): + frame = self.findChild(name='frame') + frame.border_size = border_size + + return locals() + border_color = property(**border_size()) + + def __init__(self, min_size=(0, 0), max_size=(9999, 9999), border_size=1, + **kwargs): + self._min_size = min_size + self._max_size = max_size + self.views = {} + tab_bar = HBox(name='tabBar') + tab_bar.min_size = (0, 20) + tab_bar.max_size = (9999, 20) + self.view = ScrollArea(name='view') + self.view.min_size = self._min_size + self.view.max_size = self._max_size + self.view.border_size = border_size + frame = VBox(name='frame') + frame.border_size = border_size + frame.opaque = 0 + frame.addChild(self.view) + VBox.__init__(self, **kwargs) + self.padding = 0 + VBox.addChild(self, tab_bar) + VBox.addChild(self, frame) + self.adaptLayout() + + def addTab(self, text): + text = unicode(text) + tab = Button(text=text) + tab_bar = self.findChild(name='tabBar') + tab_bar.addChild(tab) + tab.capture(callbackWithArguments(self.showView, text)) + self.adaptLayout() + + def addChild(self, child): + name = child.name or unicode(str(child)) + self.addTab(name) + self.views[name] = child + if len(self.views) == 1: + # Show the first view by default. + self.showView(name) + + def showView(self, name): + view = self.views[name] + self.view.content = view + self.adaptLayout() diff -r 000000000000 -r 7a89ea5404b1 inventory.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/inventory.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,177 @@ +# This file is part of PARPG. +# +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +from parpg.objects.base import Container +from parpg.objects.composed import SingleItemContainer as Slot + +import copy + +# TODO: many missing function definitions in this code + +class Inventory(Container): + """The class to represent inventory 'model': allow operations with + inventory contents, perform weight/bulk calculations, etc""" + def __init__(self, **kwargs): + """Initialise instance""" + Container.__init__(self, **kwargs) + self.items = {"head": Slot(), "neck": Slot(), + "shoulders": Slot(), "chest": Slot(), + "abdomen": Slot(), "left_arm": Slot(), + "right_arm": Slot(),"groin": Slot(), + "hips": Slot(), "left_leg": Slot(), + "right_leg": Slot(), "left_hand": Slot(), + "right_hand": Slot(), "ready": Container(), + "backpack": Container()} + for key, item in self.items.iteritems(): + item.name = key + kwargs = {} + kwargs["container"] = item + item.setScript("onPlaceItem", self.onChildPlaceItem, kwargs = kwargs) + self.item_lookup = {} + + def onChildPlaceItem(self, container): + for item in container.items.itervalues(): + self.item_lookup[item.ID] = container.name + + def placeItem(self, item, index=None): + self.items["backpack"].placeItem(item, index) + #self.item_lookup[item.ID] = "backpack" + + def takeItem(self, item): + if not item.ID in self.item_lookup: + raise ValueError ('I do not contain this item: %s' % item) + self.items[self.item_lookup[item.ID]].takeItem(item) + del self.item_lookup[item.ID] + + def removeItem(self, item): + if not item.ID in self.item_lookup: + raise ValueError ('I do not contain this item: %s' % item) + self.items[self.item_lookup[item.ID]].removeItem(item) + del self.item_lookup[item.ID] + + def replaceItem(self, old_item, new_item): + """Replaces the old item with the new one + @param old_item: Old item which is removed + @type old_item: Carryable + @param new_item: New item which is added + @type new_item: Carryable + """ + if not old_item.ID in self.item_lookup: + raise ValueError ('I do not contain this item: %s' % old_item) + self.items[self.item_lookup[old_item.ID]]\ + .replaceItem(old_item, new_item) + + def getWeight(self): + """Total weight of all items in container + container's own weight""" + return sum((item.weight for item in self.items.values())) + + def setWeightDummy(self, weight): + pass + + weight = property(getWeight, setWeightDummy, "Total weight of container") + + + def count(self, item_type = ""): + return sum(item.count(item_type) for item in self.items.values()) + + def takeOff(self, item): + return self.moveItemToSlot(item, "backpack") + + def moveItemToSlot(self,item,slot,index=None): + if not slot in self.items: + raise(ValueError("%s: No such slot" % slot)) + + if item.ID in self.item_lookup: + self.items[self.item_lookup[item.ID]].takeItem(item) + try: + self.items[slot].placeItem(item, index) + except Container.SlotBusy: + if index == None : + offending_item = self.items[slot].items[0] + else : + offending_item = self.items[slot].items[index] + self.items[slot].takeItem(offending_item) + self.items[slot].placeItem(item, index) + self.placeItem(offending_item) + self.item_lookup[item.ID] = slot + + def getItemsInSlot(self, slot, index=None): + if not slot in self.items: + raise(ValueError("%s: No such slot" % slot)) + if index != None: + return self.items[slot].items.get(index) + else: + return copy.copy(self.items[slot].items) + + def isSlotEmpty(self, slot, index=None): + if not slot in self.items: + raise(ValueError("%s: No such slot" % slot)) + if index == None: + return self.items[slot].count() == 0 + else: + return not index in self.items[slot].items + + + def has(self, item_ID): + return item_ID in self.item_lookup + + def findItemByID(self, ID): + if ID not in self.item_lookup: + return None + return self.items[self.item_lookup[ID]].findItemByID(ID) + + def findItem(self, **kwargs): + """Find an item in inventory by various attributes. All parameters + are optional. + @type name: String + @param name: Object name. If the name is non-unique, + first matching object is returned + @type kind: String + @param kind: One of the possible object kinds like "openable" or + "weapon" (see base.py) + @return: The item matching criteria or None if none was found""" + for slot in self.items: + item_found = self.items[slot].findItem(**kwargs) + if item_found != None: + return item_found + return None + + def __repr__(self): + return "[Inventory contents: " + \ + reduce((lambda a,b: str(a) + + ', ' + str(b)), + self.items.values()) + " ]" + + def serializeInventory(self): + """Returns the inventory items as a list""" + inventory = [] + inventory.extend(self.items["backpack"].serializeItems()) + for key, slot in self.items.iteritems(): + if key == "ready" or key == "backpack": + continue + elif len(slot.items) > 0: + item = slot.items[0] + item_dict = item.getStateForSaving() + item_dict["slot"] = key + item_dict["type"] = type(item).__name__ + inventory.append(item_dict) + return inventory + + def getStateForSaving(self): + """Returns state for saving + """ + state = {} + state["Inventory"] = self.serializeInventory() + return state diff -r 000000000000 -r 7a89ea5404b1 main.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/main.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,96 @@ +#!/usr/bin/env python2 +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +#TODO: Modularize this script +import sys +import logging + +from optparse import OptionParser + +usage = ('usage: %prog [options] settings_path [system_path user_path]\n\n' + 'The settings_path argument is mandatory and is the directory in \n' + 'which your system.cfg file is located. Optionally, you may \n' + 'specify where data files are located (system_path), and where \n' + 'the user settings and data files should be saved to (user_path)\n\n' + 'Example: python %prog .') + +parser = OptionParser(description='PARPG Launcher Script', usage=usage) +parser.add_option('-f', '--logfile', + help='Name of log file to save to') +parser.add_option('-l', '--loglevel', default='critical', + help='desired output level for log file') +parser.add_option('-m', '--module', + help='location of the parpg module') +opts, args = parser.parse_args() + +if not args: + parser.print_help() + sys.exit(1) + + +# initialize settings +if opts.module: + print('added ' + opts.module) + sys.path.insert(0, opts.module) + +from parpg.settings import Settings + +settings = Settings(*args) + +levels = {'debug': logging.DEBUG, + 'info': logging.INFO, + 'warning': logging.WARNING, + 'error': logging.ERROR, + 'critical': logging.CRITICAL} + +#TODO: setup formating +logging.basicConfig(filename=opts.logfile, level=levels[opts.loglevel]) +logger = logging.getLogger('parpg') + +try: + sys.path.insert(0, settings.parpg.FifePath) +except AttributeError: + logger.warning('[parpg] section has no FifePath option') + +try: + from fife import fife +except ImportError: + logger.critical("Could not import fife module. Please install fife or add " + "'FifePath' to the [parpg] section of your settings file") + sys.exit(1) + +from parpg.application import PARPGApplication +from parpg.common import utils + +# enable psyco if available and in settings file +try: + import psyco + psyco_available = True +except ImportError: + logger.warning('Psyco Acceleration unavailable') + psyco_available = False + +if settings.fife.UsePsyco: + if psyco_available: + psyco.full() + logger.info('Psyco Acceleration enabled') + else: + logger.warning('Please install psyco before attempting to use it' + 'Psyco Acceleration disabled') +else: + logger.info('Psycho Acceleration disabled') + +# run the game +app = PARPGApplication(settings) +app.run() diff -r 000000000000 -r 7a89ea5404b1 mainmenucontroller.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mainmenucontroller.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,95 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +import containers +import doors +import actors +import items +import sys + +OBJECT_MODULES = [containers, actors, doors, items] + +def getAllObjects (): + """Returns a dictionary with the names of the concrete game object classes + mapped to the classes themselves""" + result = {} + for module in OBJECT_MODULES: + for class_name in module.__all__: + result[class_name] = getattr (module, class_name) + return result + +def createObject(info, extra = None): + """Called when we need to get an actual object. + @type info: dict + @param info: stores information about the object we want to create + @type extra: dict + @param extra: stores additionally required attributes + @return: the object""" + # First, we try to get the type and ID, which every game_obj needs. + extra = extra or {} + try: + obj_type = info.pop('type') + ID = info.pop('id') + except KeyError: + sys.stderr.write("Error: Game object missing type or id.") + sys.exit(False) + + # add the extra info + for key, val in extra.items(): + info[key] = val + + # this is for testing purposes + return getAllObjects()[obj_type](ID, **info) diff -r 000000000000 -r 7a89ea5404b1 objects/action.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/objects/action.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,548 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +#exceptions + +import logging + +logger = logging.getLogger('action') + +from parpg.gui import drag_drop_data as data_drag + +class NoSuchQuestException(Exception): + """NoQuestException is used when there is no active quest with the id""" + pass + +#classes + +class Action(object): + """Base Action class, to define the structure""" + + + def __init__(self, controller, commands = None): + """Basic action constructor + @param controller: A reference to the GameSceneController. + @type controller: parpg.GameSceneController + @param commands: Special commands that are executed + @type commands: Dictionary + """ + self.commands = commands or () + self.controller = controller + self.model = controller.model + + def execute(self): + """To be overwritten""" + #Check if there are special commands and execute them + for command_data in self.commands: + command = command_data["Command"] + if command == "SetQuestVariable": + quest_id = command_data["ID"] + variable = command_data["Variable"] + value = command_data["Value"] + quest_engine = self.model.game_state.quest_engine + if quest_engine.hasQuest(quest_id): + quest_engine[quest_id].setValue(variable, value) + else: + raise NoSuchQuestException + elif command == "ResetMouseCursor": + self.controller.resetMouseCursor() + elif command == "StopDragging": + data_drag.dragging = False + +class ChangeMapAction(Action): + """A change map scheduled""" + def __init__(self, controller, target_map_name, target_pos, commands=None): + """Initiates a change of the position of the character + possibly flagging a new map to be loaded. + @param controller: A reference to the GameSceneController. + @type controller: parpg.GameSceneController + @param commands: Special commands that are executed + @type commands: Dictionary + @type view: class derived from parpg.ViewBase + @param view: The view + @type target_map_name: String + @param target_map_name: Target map id + @type target_pos: Tuple + @param target_pos: (X, Y) coordinates on the target map. + @return: None""" + super(ChangeMapAction, self).__init__(controller, commands) + self.view = controller.view + self.target_pos = target_pos + self.target_map_name = target_map_name + + def execute(self): + """Executes the map change.""" + self.model.changeMap(self.target_map_name, + self.target_pos) + super(ChangeMapAction, self).execute() + +class OpenAction(Action): + """Open a container""" + def __init__(self, controller, container, commands=None): + """ + @param controller: A reference to the GameSceneController. + @type controller: parpg.GameSceneController + @param commands: Special commands that are executed + @type commands: Dictionary + @type view: class derived from parpg.ViewBase + @param view: The view + @param container: A reference to the container + """ + super(OpenAction, self).__init__(controller, commands) + self.view = controller.view + self.container = container + def execute(self): + """Open the box.""" + self.view.hud.createBoxGUI(self.container.name, \ + self.container) + super(OpenAction, self).execute() + + +class OpenBoxAction(OpenAction): + """Open a box. Needs to be more generic, but will do for now.""" + def __init__(self, controller, container, commands = None): + """ + @param controller: A reference to the GameSceneController. + @type controller: parpg.GameSceneController + @param commands: Special commands that are executed + @type commands: Dictionary + @type view: class derived from parpg.ViewBase + @param view: The view + @param container: A reference to the container + """ + super(OpenBoxAction, self).__init__(controller, commands) + self.view = controller.view + self.container = container + + def execute(self): + """Open the box.""" + try: + self.container.open() + super(OpenBoxAction, self).execute() + + except ValueError: + self.view.hud.createExamineBox(self.container.name, \ + "The container is locked") + +class UnlockBoxAction(Action): + """Unlocks a box. Needs to be more generic, but will do for now.""" + def __init__(self, controller, container, commands = None): + """ + @param controller: A reference to the GameSceneController. + @type controller: parpg.GameSceneController + @param commands: Special commands that are executed + @type commands: Dictionary + @param container: A reference to the container + """ + super(UnlockBoxAction, self).__init__(controller, commands) + self.container = container + + def execute(self): + """Open the box.""" + self.container.unlock() + super(UnlockBoxAction, self).execute() + +class LockBoxAction(Action): + """Locks a box. Needs to be more generic, but will do for now.""" + def __init__(self, controller, container, commands = None): + """ + @param controller: A reference to the GameSceneController. + @type controller: parpg.GameSceneController + @param commands: Special commands that are executed + @type commands: Dictionary + @param container: A reference to the container + """ + super(LockBoxAction, self).__init__(controller, commands) + self.container = container + + def execute(self): + """Lock the box.""" + self.container.lock() + super(LockBoxAction, self).execute() + + +class ExamineAction(Action): + """Examine an object.""" + def __init__(self, controller, examine_id, examine_name, examine_desc=None, commands=None): + """ + @param controller: A reference to the GameSceneController. + @type controller: parpg.GameSceneController + @param examine_id: An object id + @type examine_id: integer + @param examine_name: An object name + @type examine_name: string + @param examine_desc: A description of the object that will be displayed. + @type examine_desc: string + @param commands: Special commands that are executed + @type commands: Dictionary + @type view: class derived from parpg.ViewBase + @param view: The view + + """ + super(ExamineAction, self).__init__(controller, commands) + self.view = controller.view + self.examine_id = examine_id + self.examine_name = examine_name + if examine_desc is not None: + self.examine_desc = examine_desc + else: + self.examine_desc = "No Description" + + def execute(self): + """Display the text.""" + action_text = self.examine_desc + self.view.hud.addAction(unicode(action_text)) + logger.debug(action_text) + #this code will cut the line up into smaller lines that will be displayed + place = 25 + while place <= len(action_text): + if action_text[place] == ' ': + action_text = action_text[:place] +'\n'+action_text[place:] + place += 26 #plus 1 character to offset the new line + else: place += 1 + self.view.displayObjectText(self.examine_id, unicode(action_text), time=3000) + +class ExamineItemAction(Action): + """Examine an item.""" + def __init__(self, controller, examine_name, examine_desc, commands = None): + """ + @param controller: A reference to the GameSceneController. + @type controller: parpg.GameSceneController + @param commands: Special commands that are executed + @type commands: Dictionary + @type view: class derived from parpg.ViewBase + @param view: The view + @type examine_name: String + @param examine_name: Name of the object to be examined. + @type examine_name: String + @param examine_name: Description of the object to be examined. + """ + super(ExamineItemAction, self).__init__(controller, commands) + self.view = controller.view + self.examine_name = examine_name + self.examine_desc = examine_desc + + def execute(self): + """Display the text.""" + action_text = unicode(self.examine_desc) + self.view.hud.addAction(action_text) + logger.debug(action_text) + +class ReadAction(Action): + """Read a text.""" + def __init__(self, controller, text_name, text, commands = None): + """ + @param controller: A reference to the GameSceneController. + @type controller: parpg.GameSceneController + @param commands: Special commands that are executed + @type commands: Dictionary + @param view: The view + @type view: class derived from parpg.ViewBase + @param text_name: Name of the object containing the text + @type text_name: String + @param text: Text to be displayied + @type text: String + """ + super(ReadAction, self).__init__(controller, commands) + self.view = controller.view + self.text_name = text_name + self.text = text + + def execute(self): + """Examine the box.""" + action_text = unicode('\n'.join(["You read " + self.text_name + ".", + self.text])) + self.view.hud.addAction(action_text) + logger.debug(action_text) + super(ReadAction, self).execute() + +class TalkAction(Action): + """An action to represent starting a dialogue""" + def __init__(self, controller, npc, commands = None): + """ + @param controller: A reference to the GameSceneController. + @type controller: parpg.GameSceneController + @param commands: Special commands that are executed + @type commands: Dictionary + @type view: class derived from parpg.ViewBase + @param view: The view + @type npc: NonPlayerCharacter + @param npc: NPC to interact with. + """ + super(TalkAction, self).__init__(controller, commands) + self.view = controller.view + self.npc = npc + + def execute(self): + """Talk with the NPC when close enough, otherwise move closer. + @return: None""" + from parpg.dialoguecontroller import DialogueController + + player_char = self.model.game_state.player_character + npc_coordinates = self.npc.getLocation().getLayerCoordinates() + pc_coordinates = player_char.behaviour.agent.\ + getLocation().getLayerCoordinates() + + distance_squared = (npc_coordinates.x - pc_coordinates.x) *\ + (npc_coordinates.x - pc_coordinates.x) +\ + (npc_coordinates.y - pc_coordinates.y) *\ + (npc_coordinates.y - pc_coordinates.y) + + # If we are too far away, we approach the NPC again + if distance_squared > 2: + player_char.approach([self.npc.getLocation(). + getLayerCoordinates().x, + self.npc.getLocation(). + getLayerCoordinates().y], + TalkAction(self.controller, + self.npc, self.commands)) + else: + player_char.behaviour.agent.act('stand', self.npc.getLocation()) + + if self.npc.dialogue is not None: + dialogue_controller = DialogueController(self.controller.engine, + self.view, + self.model, + self.controller.application) + self.controller.application.pushController(dialogue_controller) + dialogue_controller.startTalk(self.npc) + else: + self.npc.behaviour.agent.say("Leave me alone!", 1000) + + self.model.game_state.player_character.behaviour.idle() + self.model.game_state.player_character.nextAction = None + super(TalkAction, self).execute() + +class UseAction(Action): + """Action for carryable items. It executes special commands that can be only + used on carryable utens""" + + + def __init__(self, controller, item, commands = None): + """ + @param controller: A reference to the GameSceneController. + @type controller: parpg.GameSceneController + @param item: Item on which the action is called + @type item: CarryableItem + @param commands: Special commands that are executed + @type commands: Dictionary + """ + super(UseAction, self).__init__(controller, commands) + self.view = controller.view + self.item = item + + def execute(self): + #Check if there are special commands and execute them + for command_data in self.commands: + command = command_data["Command"] + if command == "ReplaceItem": + object_id = command_data["ID"] + object_type = command_data["ObjectType"] + container = self.item.in_container + inst_dict = {} + inst_dict["ID"] = object_id + inst_dict["object_type"] = object_type + new_item = self.model.createContainerObject(inst_dict) + container.replaceItem(self.item, new_item) + self.view.hud.inventory.updateInventoryButtons() + super(UseAction, self).execute() + +class PickUpAction(Action): + """Action for picking up items from a map""" + + def __init__(self, controller, map_item, commands = None): + super(PickUpAction, self).__init__(controller, commands) + self.map_item = map_item + self.view = controller.view + + def execute(self): + real_item = self.map_item.item + self.model.deleteObject(self.map_item.ID) + self.model.game_state.player_character.\ + inventory.placeItem(real_item) + self.view.hud.inventory.updateInventoryButtons() + super(PickUpAction, self).execute() + +class DropItemAction(Action): + """Action for dropping an items on a map""" + def __init__(self, controller, item, commands = None): + super(DropItemAction, self).__init__(controller, commands) + self.item = item + + def execute(self): + map_name = self.model.game_state.current_map_name + map_item_values = {} + map_item_values["ViewName"] = self.item.name + map_item_values["ObjectType"] = "MapItem" + map_item_values["ItemType"] = self.item.item_type + map_item_values["Map"] = map_name + coords = self.model.game_state.player_character.\ + getLocation().getExactLayerCoordinates() + map_item_values["Position"] = (coords.x, coords.y) + map_item_values["Rotation"] = 0 + map_item_values["item"] = self.item + agent = {} + agent[self.item.ID] = map_item_values + self.model.addAgent("All", agent) + self.model.placeAgents() + super(DropItemAction, self).execute() + +class DropItemFromContainerAction(DropItemAction): + """Action for dropping an items from the Inventory to a map""" + + def __init__(self, controller, item, container_gui, commands = None): + super(DropItemFromContainerAction, self).__init__(controller, item, commands) + self.container_gui = container_gui + + def execute(self): + super(DropItemFromContainerAction, self).execute() + self.item.in_container.takeItem(self.item) + self.container_gui.updateImages() + +class BrewBeerAction(Action): + """Action for brewing beer in a pot""" + def __init__(self, controller, pot, commands = None): + super(BrewBeerAction, self).__init__(controller, commands) + self.pot = pot + self.view = controller.view + + def execute(self): + """Brew the beer""" + has_water = False + has_yeast = False + has_fruit = False + has_wood = False + has_bottle = False + player_character = self.model.game_state.player_character + for item in self.pot.items.itervalues(): + if item.item_type == "Questionable water": + if has_water: + self.view.hud.addAction(unicode(\ + "Please put only 1 water in the pot")) + return + has_water = True + water_type = 1 + water = item + elif item.item_type == "Pure water": + if has_water: + self.view.hud.addAction(unicode(\ + "Please put only 1 water in the pot")) + return + has_water = True + water_type = 2 + water = item + elif item.item_type == "Grain": + if has_fruit: + self.view.hud.addAction(unicode(\ + "Please put only 1 fruit in the pot")) + return + has_fruit = True + fruit_type = 3 + fruit = item + elif item.item_type == "Wild potato": + if has_fruit: + self.view.hud.addAction(unicode(\ + "Please put only 1 fruit in the pot")) + return + has_fruit = True + fruit_type = 2 + fruit = item + elif item.item_type == "Rotten yam": + if has_fruit: + self.view.hud.addAction(unicode(\ + "Please put only 1 fruit in the pot")) + return + has_fruit = True + fruit_type = 1 + fruit = item + elif item.item_type == "Yeast": + if has_yeast: + self.view.hud.addAction(unicode(\ + "Please put only 1 yeast in the pot")) + return + has_yeast = True + yeast = item + else: + self.view.hud.addAction(unicode("Item " + item.name + \ + " is not needed for brewing beer")) + self.view.hud.addAction(unicode(\ + "Please put only ingredients for the beer in the pot.\ + Things like bottles and wood have to be in your inventory")) + return + wood = player_character.hasItem("Wood") + if wood: + has_wood = True + bottle = player_character.hasItem("Empty beer bottle") + if bottle: + has_bottle = True + if has_water and has_fruit and has_wood and has_bottle: + self.pot.removeItem(water) + self.pot.removeItem(fruit) + if has_yeast: + self.pot.removeItem(yeast) + player_character.inventory.removeItem(wood) + inst_dict = {} + inst_dict["ID"] = "Beer" + inst_dict["object_type"] = "Beer" + new_item = self.model.createContainerObject(inst_dict) + player_character.inventory.placeItem(new_item) + self.view.hud.inventory.updateInventoryButtons() + beer_quality = 0 + if water_type == 1: + if fruit_type == 1: + beer_quality = -1 + elif fruit_type == 2: + beer_quality = 2 + elif fruit_type == 3: + beer_quality = 3 + if water_type == 2: + if fruit_type == 1: + beer_quality = 1 + elif fruit_type == 2: + beer_quality = 3 + elif fruit_type == 3: + beer_quality = 4 + if beer_quality > 0 and has_yeast: + beer_quality += 1 + self.model.game_state.quest_engine.quests["beer"].\ + setValue("beer_quality", beer_quality) + else: + self.view.hud.addAction(unicode( + """For brewing beer you need at least: + In the pot: + Fruit (like grain, potato, yam) + Water + Optionally: + Good quality yeast. + Wild yeast will be used if none present. + In the inventory: + Wood + Empty bottle""")) + super(BrewBeerAction, self).execute() + +ACTIONS = {"ChangeMap":ChangeMapAction, + "Open":OpenAction, + "OpenBox":OpenBoxAction, + "Unlock":UnlockBoxAction, + "Lock":LockBoxAction, + "ExamineItem":ExamineItemAction, + "Examine":ExamineAction, + "Look":ExamineItemAction, + "Read":ReadAction, + "Talk":TalkAction, + "Use":UseAction, + "PickUp":PickUpAction, + "DropFromInventory":DropItemFromContainerAction, + "BrewBeer":BrewBeerAction} diff -r 000000000000 -r 7a89ea5404b1 objects/actors.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/objects/actors.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,414 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +from random import randrange + + +from fife import fife + +from base import GameObject, Living, Scriptable, CharStats +from composed import CarryableItem +from parpg.inventory import Inventory + +"""All actors go here. Concrete classes only.""" + +__all__ = ["PlayerCharacter", "NonPlayerCharacter", ] + +_AGENT_STATE_NONE, _AGENT_STATE_IDLE, _AGENT_STATE_APPROACH, _AGENT_STATE_RUN, _AGENT_STATE_WANDER, _AGENT_STATE_TALK = xrange(6) + +class ActorBehaviour (fife.InstanceActionListener): + """Fife agent listener""" + def __init__(self, layer): + fife.InstanceActionListener.__init__(self) + self.layer = layer + self.agent = None + self.state = None + self.speed = 0 + self.idle_counter = 1 + + def attachToLayer(self, agent_ID): + """Attaches to a certain layer + @type agent_ID: String + @param agent_ID: ID of the layer to attach to. + @return: None""" + self.agent = self.layer.getInstance(agent_ID) + self.agent.addActionListener(self) + self.state = _AGENT_STATE_NONE + + def getX(self): + """Get the NPC's x position on the map. + @rtype: integer" + @return: the x coordinate of the NPC's location""" + return self.agent.getLocation().getLayerCoordinates().x + + def getY(self): + """Get the NPC's y position on the map. + @rtype: integer + @return: the y coordinate of the NPC's location""" + return self.agent.getLocation().getLayerCoordinates().y + + def onNewMap(self, layer): + """Sets the agent onto the new layer.""" + if self.agent is not None: + self.agent.removeActionListener(self) + + self.agent = layer.getInstance(self.parent.ID) + self.agent.addActionListener(self) + self.state = _AGENT_STATE_NONE + self.idle_counter = 1 + + def idle(self): + """@return: None""" + self.state = _AGENT_STATE_IDLE + self.agent.act('stand', self.agent.getFacingLocation()) + + def onInstanceActionFinished(self, instance, action): + pass + +class PCBehaviour (ActorBehaviour): + def __init__(self, parent=None, layer=None): + super(PCBehaviour, self).__init__(layer) + self.parent = parent + self.idle_counter = 1 + self.speed = 0 + self.nextAction = None + self.agent = None + + def onInstanceActionFinished(self, instance, action): + """@type instance: ??? + @param instance: ??? + @type action: ??? + @param action: ??? + @return: None""" + # First we reset the next behavior + act = self.nextAction + self.nextAction = None + self.idle() + + if act: + act.execute() + + if(action.getId() != 'stand'): + self.idle_counter = 1 + else: + self.idle_counter += 1 + + +class NPCBehaviour(ActorBehaviour): + def __init__(self, Parent=None, Layer=None): + super(NPCBehaviour, self).__init__(Layer) + + self.parent = Parent + self.state = _AGENT_STATE_NONE + self.pc = None + self.target_loc = None + self.nextAction = None + + # hard code these for now + self.distRange = (2, 4) + # these are parameters to lower the rate of wandering + # wander rate is the number of "IDLEs" before a wander step + # this could be set for individual NPCs at load time + # or thrown out altogether. + self.wanderCounter = 0 + self.wanderRate = 9 + + def getTargetLocation(self): + """@rtype: fife.Location + @return: NPC's position""" + x = self.getX() + y = self.getY() + if self.state == _AGENT_STATE_WANDER: + """ Random Target Location """ + l = [0, 0] + for i in range(len(l)): + sign = randrange(0, 2) + dist = randrange(self.distRange[0], self.distRange[1]) + if sign == 0: + dist *= -1 + l[i] = dist + x += l[0] + y += l[1] + # Random walk is + # rl = randint(-1, 1);ud = randint(-1, 1);x += rl;y += ud + l = fife.Location(self.agent.getLocation()) + l.setLayerCoordinates(fife.ModelCoordinate(x, y)) + return l + + def onInstanceActionFinished(self, instance, action): + """What the NPC does when it has finished an action. + Called by the engine and required for InstanceActionListeners. + @type instance: fife.Instance + @param instance: self.agent (the NPC listener is listening for this + instance) + @type action: ??? + @param action: ??? + @return: None""" + if self.state == _AGENT_STATE_WANDER: + self.target_loc = self.getTargetLocation() + self.idle() + + + def idle(self): + """Controls the NPC when it is idling. Different actions + based on the NPC's state. + @return: None""" + if self.state == _AGENT_STATE_NONE: + self.state = _AGENT_STATE_IDLE + self.agent.act('stand', self.agent.getFacingLocation()) + elif self.state == _AGENT_STATE_IDLE: + if self.wanderCounter > self.wanderRate: + self.wanderCounter = 0 + self.state = _AGENT_STATE_WANDER + else: + self.wanderCounter += 1 + self.state = _AGENT_STATE_NONE + + self.target_loc = self.getTargetLocation() + self.agent.act('stand', self.agent.getFacingLocation()) + elif self.state == _AGENT_STATE_WANDER: + self.parent.wander(self.target_loc) + self.state = _AGENT_STATE_NONE + elif self.state == _AGENT_STATE_TALK: + self.agent.act('stand', self.pc.getLocation()) + +class CharacterBase(GameObject, CharStats, Living): + """Base class for Characters""" + def __init__(self, ID, agent_layer=None, inventory=None, text="", + primary_stats=None, secondary_stats=None, **kwargs): + GameObject.__init__(self, ID, text=text, **kwargs) + CharStats.__init__(self, **kwargs) + Living.__init__(self, **kwargs) + self.statistics = {} + if primary_stats is not None: + for primary_stat in primary_stats: + name = primary_stat.name + self.statistics[name] = primary_stat + if secondary_stats is not None: + for secondary_stat in primary_stats: + long_name = secondary_stat.long_name + self.statistics[long_name] = secondary_stat + short_name = secondary_stat.short_name + self.statistics[short_name] = secondary_stat + secondary_stat.attach(self) + self.behaviour = None + if inventory == None: + self.inventory = Inventory() + else: + self.inventory = inventory + self.state = _AGENT_STATE_NONE + self.layer_id = agent_layer.getId() + self.createBehaviour(agent_layer) + + def createBehaviour(self, layer): + """Creates the behaviour for this actor. + @return: None""" + pass + + def setup(self): + """@return: None""" + self.behaviour.attachToLayer(self.ID) + + def start(self): + """@return: None""" + self.behaviour.idle() + + def teleport(self, location): + """Teleports a Character instantly to the given location. + @type location: fife.Location + @param location: Target coordinates for Character. + @return: None""" + self.state = _AGENT_STATE_IDLE + self.behaviour.nextAction = None + self.behaviour.agent.setLocation(location) + + def give (self, item, actor): + """Gives the specified item to the different actor. Raises an exception if the item was invalid or not found + @type item: Carryable + @param item: The item object to give + @param actor: Person to give item to""" + if item == None: + raise ValueError("I don't have %s" % item.name) + self.inventory.takeItem(item) + actor.inventory.placeItem(item) + + def hasItem(self, item_type): + """Returns wether an item is present in the players inventory or not + @param item_type: ID of the item + @type item_type: str + @return: True when the item is present, False when not""" + return self.inventory.findItem(item_type=item_type) + + def itemCount(self, item_type=""): + """Returns number of all items or items specified by item_type + the player has. + @param item_type: ID of the item, can be empty + @type item_type: str + @return: Number of items""" + return self.inventory.count(item_type) + + def getLocation(self): + """Get the NPC's position as a fife.Location object. Basically a + wrapper. + @rtype: fife.Location + @return: the location of the NPC""" + return self.behaviour.agent.getLocation() + + def run(self, location): + """Makes the PC run to a certain location + @type location: fife.ScreenPoint + @param location: Screen position to run to. + @return: None""" + self.state = _AGENT_STATE_RUN + self.behaviour.nextAction = None + self.behaviour.agent.move('run', location, self.behaviour.speed + 1) + + def walk(self, location): + """Makes the PC walk to a certain location. + @type location: fife.ScreenPoint + @param location: Screen position to walk to. + @return: None""" + self.state = _AGENT_STATE_RUN + self.behaviour.nextAction = None + self.behaviour.agent.move('walk', location, self.behaviour.speed - 1) + + def getStateForSaving(self): + """Returns state for saving + """ + ret_dict = GameObject.getStateForSaving(self) + ret_dict["Inventory"] = self.inventory.serializeInventory() + return ret_dict + + def _getCoords(self): + """Get-er property function""" + return (self.getLocation().getMapCoordinates().x, + self.getLocation().getMapCoordinates().y) + + def _setCoords(self, coords): + """Set-er property function""" + map_coords = self.getLocation().getMapCoordinates() + map_coords.X, map_coords.Y = float(coords[0]), float (coords[1]) + self.teleport(map_coords) + + coords = property (_getCoords, _setCoords, + doc="Property allowing you to get and set the object's \ + coordinates via tuples") + +class PlayerCharacter (CharacterBase): + """PC class""" + def __init__ (self, ID, agent_layer=None, inventory=None, + text="Its you. Who would've thought that?", **kwargs): + if inventory == None: + inventory = Inventory() + inventory.placeItem(CarryableItem(ID=456, name="Dagger123")) + inventory.placeItem(CarryableItem(ID=555, name="Beer")) + inventory.placeItem(CarryableItem(ID=616, + name="Pamphlet", + image="/gui/inv_images/inv_pamphlet.png")) + CharacterBase.__init__(self, ID, agent_layer, inventory, text, **kwargs) + self.people_i_know = set() + self.attributes.append("PC") + + def getStateForSaving(self): + """Returns state for saving + """ + ret_dict = super(PlayerCharacter, self).getStateForSaving() + ret_dict["PeopleKnown"] = self.people_i_know + return ret_dict + + def meet(self, npc): + """Record that the PC has met a certain NPC + @type npc: str + @param npc: The NPC's name or id""" + if npc in self.people_i_know: + # we could raise an error here, but should probably be a warn + # raise RuntimeError("I already know %s" % npc) + return + self.people_i_know.add(npc) + + def met(self, npc): + """Indicate whether the PC has met this npc before + @type npc: str + @param npc: The NPC's name or id + @return: None""" + return npc in self.people_i_know + + def createBehaviour(self, layer): + """Creates the behaviour for this actor. + @return: None""" + self.behaviour = PCBehaviour(self, layer) + + def approach(self, location, action=None): + """Approaches a location and then perform an action (if set). + @type loc: fife.Location + @param loc: the location to approach + @type action: Action + @param action: The action to schedule for execution after the approach. + @return: None""" + self.state = _AGENT_STATE_APPROACH + self.behaviour.nextAction = action + boxLocation = tuple([int(float(i)) for i in location]) + l = fife.Location(self.behaviour.agent.getLocation()) + l.setLayerCoordinates(fife.ModelCoordinate(*boxLocation)) + self.behaviour.agent.move('run', l, self.behaviour.speed + 1) + +class NonPlayerCharacter(CharacterBase, Scriptable): + """NPC class""" + def __init__(self, ID, agent_layer=None, name='NPC', \ + text='A nonplayer character', inventory=None, + real_name='NPC', dialogue=None, **kwargs): + # init game object + CharacterBase.__init__(self, ID, agent_layer=agent_layer, + inventory=inventory, name=name, + real_name=real_name, text=text, **kwargs) + Scriptable.__init__(self, **kwargs) + + self.attributes.append("NPC") + self.dialogue = dialogue + + def prepareStateForSaving(self, state): + """Prepares state for saving + @type state: dictionary + @param state: State of the object + """ + CharacterBase.prepareStateForSaving(self, state) + del state["behaviour"] + + def getStateForSaving(self): + """Returns state for saving + """ + ret_dict = CharacterBase.getStateForSaving(self) + ret_dict["Lives"] = self.lives + ret_dict["State"] = self.behaviour.state + return ret_dict + + def createBehaviour(self, layer): + """Creates the behaviour for this actor. + @return None """ + self.behaviour = NPCBehaviour(self, layer) + + def wander(self, location): + """Nice slow movement for random walking. + @type location: fife.Location + @param location: Where the NPC will walk to. + @return: None""" + self.behaviour.agent.move('walk', location, self.behaviour.speed - 1) + + def talk(self, pc): + """Makes the NPC ready to talk to the PC + @return: None""" + self.behaviour.state = _AGENT_STATE_TALK + self.behaviour.pc = pc.behaviour.agent + self.behaviour.idle() diff -r 000000000000 -r 7a89ea5404b1 objects/base.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/objects/base.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,508 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +"""Containes classes defining the base properties of all interactable in-game + objects (such as Carryable, Openable, etc. These are generally independent + classes, which can be combined in almost any way and order. + + Some rules that should be followed when CREATING base property classes: + + 1. If you want to support some custom initialization arguments, + always define them as keyword ones. Only GameObject would use + positional arguments. + 2. In __init__() **ALWAYS** call the parent's __init__(**kwargs), preferably + *at the end* of your __init__() (makes it easier to follow) + 3. There should always be an attributes.append(x) call on __init__ + (where X is the name of the class) + + EXAMPLE: + + class Openable(object): + def __init__ (self, is_open = True, **kwargs): + self.attribbutes.append("openable") + self.is_open = is_open + super(Openable,self).__init__ (**kwargs) + + + Some rules are to be followed when USING the base classes to make composed + ones: + + 1. The first parent should always be the base GameObject class + 2. Base classes other than GameObject can be inherited in any order + 3. The __init__ functoin of the composed class should always invoke the + parent's __init__() *before* it starts customizing any variables. + + EXAMPLE: + + class TinCan (GameObject, Container, Scriptable, Destructable, Carryable): + def __init__ (self, *args, **kwargs): + super(TinCan,self).__init__ (*args, **kwargs) + self.name = 'Tin Can'""" + +class BaseObject(object): + """A base class that supports dynamic attributes functionality""" + def __init__ (self): + if not self.__dict__.has_key("attributes"): + self.attributes = [] + + def trueAttr(self, attr): + """Method that checks if the instance has an attribute""" + return attr in self.attributes + + def getStateForSaving(self): + """Returns state for saving + """ + state = {} + state["attributes"] = self.attributes + return state + +class DynamicObject (BaseObject): + """Class with basic attributes""" + def __init__ (self, name="Dynamic object", real_name=None, image=None, **kwargs): + """Initialise minimalistic set of data + @type name: String + @param name: Object display name + @type image: String or None + @param name: Filename of image to use in inventory""" + BaseObject.__init__(self) + self.name = name + self.real_name = real_name or name + self.image = image + + def prepareStateForSaving(self, state): + """Prepares state for saving + @type state: dictionary + @param state: State of the object + """ + pass + + def restoreState(self, state): + """Restores a state from a saved state + @type state: dictionary + @param state: Saved state + """ + self.__dict__.update(state) + + def __getstate__(self): + odict = self.__dict__.copy() + self.prepareStateForSaving(odict) + return odict + + def __setstate__(self, state): + self.restoreState(state) + + def getStateForSaving(self): + """Returns state for saving + """ + state = BaseObject.getStateForSaving(self) + state["Name"] = self.name + state["RealName"] = self.real_name + state["Image"] = self.image + return state + +class GameObject (DynamicObject): + """A base class to be inherited by all game objects. This must be the + first class (left to right) inherited by any game object.""" + def __init__ (self, ID, gfx = None, xpos = 0.0, ypos = 0.0, map_id = None, + blocking=True, name="Generic object", real_name="Generic object", text="Item description", + desc="Detailed description", **kwargs): + """Set the basic values that are shared by all game objects. + @type ID: String + @param ID: Unique object identifier. Must be present. + @type gfx: Dictionary + @param gfx: Dictionary with graphics for the different contexts + @type coords 2-item tuple + @param coords: Initial coordinates of the object. + @type map_id: String + @param map_id: Identifier of the map where the object is located + @type blocking: Boolean + @param blocking: Whether the object blocks character movement + @type name: String + @param name: The display name of this object (e.g. 'Dirty crate') + @type text: String + @param text: A longer description of the item + @type desc: String + @param desc: A long description of the item that is displayed when it is examined + """ + DynamicObject.__init__(self, name, real_name, **kwargs) + self.ID = ID + self.gfx = gfx or {} + self.X = xpos + self.Y = ypos + self.map_id = map_id + self.blocking = True + self.text = text + self.desc = desc + + def _getCoords(self): + """Get-er property function""" + return (self.X, self.Y) + + def _setCoords(self, coords): + """Set-er property function""" + self.X, self.Y = float(coords[0]), float (coords[1]) + + coords = property (_getCoords, _setCoords, + doc = "Property allowing you to get and set the object's \ + coordinates via tuples") + + def __repr__(self): + """A debugging string representation of the object""" + return "<%s:%s>" % (self.name, self.ID) + + def getStateForSaving(self): + """Returns state for saving + """ + state = super(GameObject, self).getStateForSaving() + state["ObjectModel"] = self.gfx + state["Text"] = self.text + state["Desc"] = self.desc + state["Position"] = list(self.coords) + return state + + +class Scriptable (BaseObject): + """Allows objects to have predefined parpg executed on certain events""" + def __init__ (self, parpg = None, **kwargs): + """Init operation for scriptable objects + @type parpg: Dictionary + @param parpg: Dictionary where the event strings are keys. The + values are 3-item tuples (function, positional_args, keyword_args)""" + BaseObject.__init__(self) + self.attributes.append("scriptable") + self.parpg = parpg or {} + + def runScript (self, event): + """Runs the script for the given event""" + if event in self.parpg and self.parpg[event]: + func, args, kwargs = self.parpg[event] + func (*args, **kwargs) + + def setScript (self, event, func, args = None , kwargs = None): + """Sets a script to be executed for the given event.""" + args = args or {} + kwargs = kwargs or {} + self.parpg[event] = (func, args, kwargs) + +class Openable(DynamicObject, Scriptable): + """Adds open() and .close() capabilities to game objects + The current state is tracked by the .is_open variable""" + def __init__(self, is_open = True, **kwargs): + """Init operation for openable objects + @type is_open: Boolean + @param is_open: Keyword boolean argument sets the initial state.""" + DynamicObject.__init__(self, **kwargs) + Scriptable.__init__(self, **kwargs) + self.attributes.append("openable") + self.is_open = is_open + + def open(self): + """Opens the object, and runs an 'onOpen' script, if present""" + self.is_open = True + try: + if self.trueAttr ('scriptable'): + self.runScript('onOpen') + except AttributeError : + pass + + def close(self): + """Opens the object, and runs an 'onClose' script, if present""" + self.is_open = False + try: + if self.trueAttr ('scriptable'): + self.runScript('onClose') + except AttributeError : + pass + +class Lockable (Openable): + """Allows objects to be locked""" + def __init__ (self, locked = False, is_open = True, **kwargs): + """Init operation for lockable objects + @type locked: Boolean + @param locked: Keyword boolen argument sets the initial locked state. + @type is_open: Boolean + @param is_open: Keyword boolean argument sets the initial open state. + It is ignored if locked is True -- locked objects + are always closed.""" + self.attributes.append("lockable") + self.locked = locked + if locked : + is_open = False + Openable.__init__( self, is_open, **kwargs ) + + def unlock (self): + """Handles unlocking functionality""" + self.locked = False + + def lock (self): + """Handles locking functionality""" + self.close() + self.locked = True + + def open (self, *args, **kwargs): + """Adds a check to see if the object is unlocked before running the + .open() function of the parent class""" + if self.locked: + raise ValueError ("Open failed: object locked") + super (Lockable, self).open(*args, **kwargs) + +class Carryable (DynamicObject): + """Allows objects to be stored in containers""" + def __init__ (self, weight=0.0, bulk=0.0, **kwargs): + DynamicObject.__init__(self, **kwargs) + self.attributes.append("carryable") + self.in_container = None + self.on_map = None + self.agent = None + self.weight = weight + self.bulk = bulk + + def getInventoryThumbnail(self): + """Returns the inventory thumbnail of the object""" + # TODO: Implement properly after the objects database is in place + if self.image == None: + return "gui/inv_images/inv_litem.png" + else: + return self.image + +class Container (DynamicObject, Scriptable): + """Gives objects the capability to hold other objects""" + class TooBig(Exception): + """Exception to be raised when the object is too big + to fit into container""" + pass + + class SlotBusy(Exception): + """Exception to be raised when the requested slot is occupied""" + pass + + class ItemSelf(Exception): + """Exception to be raised when trying to add the container as an item""" + pass + + def __init__ (self, capacity = 0, items = None, **kwargs): + DynamicObject.__init__(self, **kwargs) + Scriptable.__init__(self, **kwargs) + self.attributes.append("container") + self.items = {} + self.capacity = capacity + if items: + for item in items: + self.placeItem(item) + + def placeItem (self, item, index=None): + """Adds the provided carryable item to the inventory. + Runs an 'onStoreItem' script, if present""" + if item is self: + raise self.ItemSelf("Paradox: Can't contain myself") + if not item.trueAttr ('carryable'): + raise TypeError ('%s is not carryable!' % item) + if self.capacity and self.getContentsBulk()+item.bulk > self.capacity: + raise self.TooBig ('%s is too big to fit into %s' % (item, self)) + item.in_container = self + if index == None: + self._placeAtVacant(item) + else: + if index in self.items : + raise self.SlotBusy('Slot %d is busy in %s' % (index, + self.name)) + self.items[index] = item + + # Run any parpg associated with storing an item in the container + try: + if self.trueAttr ('scriptable'): + self.runScript('onPlaceItem') + except AttributeError : + pass + + def _placeAtVacant(self, item): + """Places an item at a vacant slot""" + vacant = None + for i in range(len(self.items)): + if i not in self.items : + vacant = i + if vacant == None : + vacant = len(self.items) + self.items[vacant] = item + + def takeItem (self, item): + """Takes the listed item out of the inventory. + Runs an 'onTakeItem' script""" + if not item in self.items.values(): + raise ValueError ('I do not contain this item: %s' % item) + del self.items[self.items.keys()[self.items.values().index(item)]] + + # Run any parpg associated with popping an item out of the container + try: + if self.trueAttr ('scriptable'): + self.runScript('onTakeItem') + except AttributeError : + pass + + def replaceItem(self, old_item, new_item): + """Replaces the old item with the new one + @param old_item: Old item which is removed + @type old_item: Carryable + @param new_item: New item which is added + @type new_item: Carryable + """ + old_index = self.indexOf(old_item.ID) + self.removeItem(old_item) + self.placeItem(new_item, old_index) + + def removeItem(self, item): + """Removes an item from the container, basically the same as 'takeItem' + but does run a different script. This should be used when an item is + destroyed rather than moved out. + Runs 'onRemoveItem' script + """ + if not item in self.items.values(): + raise ValueError ('I do not contain this item: %s' % item) + del self.items[self.items.keys()[self.items.values().index(item)]] + + # Run any parpg associated with popping an item out of the container + try: + if self.trueAttr ('scriptable'): + self.runScript('onRemoveItem') + except AttributeError : + pass + + def count (self, item_type = ""): + """Returns the number of items""" + if item_type: + ret_count = 0 + for index in self.items : + if self.items[index].item_type == item_type: + ret_count += 1 + return ret_count + return len(self.items) + + def getContentsBulk(self): + """Bulk of the container contents""" + return sum((item.bulk for item in self.items.values())) + + def getItemAt(self, index): + return self.items[index] + + def indexOf(self, ID): + """Returns the index of the item with the passed ID""" + for index in self.items : + if self.items[index].ID == ID: + return index + return None + + def findItemByID(self, ID): + """Returns the item with the passed ID""" + for i in self.items : + if self.items[i].ID == ID: + return self.items[i] + return None + + def findItemByItemType(self, item_type): + """Returns the item with the passed item_type""" + for index in self.items : + if self.items[index].item_type == item_type: + return self.items[index] + return None + + def findItem(self, **kwargs): + """Find an item in container by attributes. All params are optional. + @type name: String + @param name: If the name is non-unique, return first matching object + @type kind: String + @param kind: One of the possible object types + @return: The item matching criteria or None if none was found""" + for index in self.items : + if "name" in kwargs and self.items[index].name != kwargs["name"]: + continue + if "ID" in kwargs and self.items[index].ID != kwargs["ID"]: + continue + if "kind" in kwargs and not self.items[index].trueAttr(kwargs["kind"]): + continue + if "item_type" in kwargs and self.items[index].item_type != kwargs["item_type"]: + continue + return self.items[index] + return None + + def serializeItems(self): + """Returns the items as a list""" + items = [] + for index, item in self.items.iteritems(): + item_dict = item.getStateForSaving() + item_dict["index"] = index + item_dict["type"] = item.item_type + items.append(item_dict) + return items + + def getStateForSaving(self): + """Returns state for saving + """ + ret_state = DynamicObject.getStateForSaving(self) + ret_state["Items"] = self.serializeItems() + return ret_state + +class Living (BaseObject): + """Objects that 'live'""" + def __init__ (self, **kwargs): + BaseObject.__init__(self) + self.attributes.append("living") + self.lives = True + + def die(self): + """Kills the object""" + self.lives = False + +class CharStats (BaseObject): + """Provides the object with character statistics""" + def __init__ (self, **kwargs): + BaseObject.__init__(self) + self.attributes.append("charstats") + +class Wearable (BaseObject): + """Objects than can be weared""" + def __init__ (self, slots, **kwargs): + """Allows the object to be worn somewhere on the body (e.g. pants)""" + BaseObject.__init__(self) + self.attributes.append("wearable") + if isinstance(slots, tuple) : + self.slots = slots + else : + self.slots = (slots,) + +class Usable (BaseObject): + """Allows the object to be used in some way (e.g. a Zippo lighter + to make a fire)""" + def __init__ (self, actions = None, **kwargs): + BaseObject.__init__(self) + self.attributes.append("usable") + self.actions = actions or {} + +class Weapon (BaseObject): + """Allows the object to be used as a weapon""" + def __init__ (self, **kwargs): + BaseObject.__init__(self) + self.attributes.append("weapon") + +class Destructable (BaseObject): + """Allows the object to be destroyed""" + def __init__ (self, **kwargs): + BaseObject.__init__(self) + self.attributes.append("destructable") + +class Trapable (BaseObject): + """Provides trap slots to the object""" + def __init__ (self, **kwargs): + BaseObject.__init__(self) + self.attributes.append("trapable") diff -r 000000000000 -r 7a89ea5404b1 objects/composed.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/objects/composed.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,145 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +"""Composite game object classes are kept here""" + +from base import GameObject, Container, Lockable, \ + Scriptable, Trapable, Destructable, Carryable, \ + Usable + +class ImmovableContainer(GameObject, Container, Lockable, Scriptable, + Trapable, Destructable): + """Composite class that can be used for crates, chests, etc.""" + def __init__ (self, **kwargs): + GameObject .__init__(self, **kwargs) + Container .__init__(self, **kwargs) + Lockable .__init__(self, **kwargs) + Scriptable .__init__(self, **kwargs) + Trapable .__init__(self, **kwargs) + Destructable .__init__(self, **kwargs) + self.blocking = True + +class SingleItemContainer (Container) : + """Container that can only store a single item. + This class can represent single-item inventory slots""" + def __init__ (self, **kwargs): + Container.__init__(self, **kwargs) + + def placeItem(self,item, index=None): + if len(self.items) > 0 : + raise self.SlotBusy ('%s is already busy' % self) + Container.placeItem(self, item) + +class CarryableItem (GameObject, Carryable, Usable): + """Composite class that will be used for all carryable items""" + def __init__(self, item_type, **kwargs): + GameObject.__init__(self, **kwargs) + Carryable.__init__(self, **kwargs) + Usable.__init__(self, **kwargs) + self.item_type = item_type + + def prepareStateForSaving(self, state): + """Prepares state for saving + @type state: dictionary + @param state: State of the object + """ + super(CarryableItem, self).prepareStateForSaving(state) + if state.has_key("in_container"): + del state["in_container"] + if state.has_key("on_map"): + del state["on_map"] + if state.has_key("agent"): + del state["agent"] + + def getStateForSaving(self): + """Returns state for saving + @type state: dictionary + @param state: State of the object + """ + ret_dict = self.__dict__.copy() + self.prepareStateForSaving(ret_dict) + return ret_dict + +class CarryableContainer(Container, CarryableItem): + """Composite class that will be used for backpack, pouches, etc.""" + def __init__ (self, item_type, **kwargs): + Container.__init__(self, **kwargs) + CarryableItem.__init__(self, item_type, **kwargs) + self.own_bulk = 0 + self.own_weight = 0 + + def getWeight(self): + """Resulting weight of a container""" + return sum((item.weight for item in self.items.values()), + self.own_weight) + + def setWeight(self, weight): + """Set container's own weight. + For compatibility with inherited methods""" + self.own_weight = weight + + weight = property(getWeight, setWeight, "Total weight of container") + + def getBulk(self): + """Resulting bulk of container""" + return self.getContentsBulk()+self.own_bulk + + def setBulk(self, bulk): + """Set container's own bulk. For compatibility with inherited methods""" + self.own_bulk = bulk + + bulk = property(getBulk, setBulk, "Total bulk of container") + + def __repr__(self): + return "[%s" % self.name + str(reduce((lambda a, b: a + ', ' + \ + str(self.items[b])), self.items, "")) + " ]" + + def getStateForSaving(self): + """Returns state for saving + @type state: dictionary + @param state: State of the object + """ + state = Container.getStateForSaving(self) + if not state.has_key("attributes"): + state["attributes"] = [] + state["attributes"].append("Container") + state.update(CarryableItem.getStateForSaving(self)) + return state + +class CarryableSingleItemContainer (SingleItemContainer, CarryableContainer) : + """Container that can only store a single item. + This class can represent single-item inventory slots""" + def __init__ (self, item_type, **kwargs): + SingleItemContainer.__init__(self, **kwargs) + CarryableContainer.__init__(self, item_type, **kwargs) + +class Door(GameObject, Lockable, Scriptable, Trapable): + """Composite class that can be used to create doors on a map.""" + def __init__ (self, target_map_name = 'my-map', + target_x = 0.0, target_y = 0.0, **kwargs): + GameObject.__init__(self, **kwargs) + Lockable.__init__(self, **kwargs) + Scriptable.__init__(self, **kwargs) + Trapable.__init__(self, **kwargs) + self.attributes.append("door") + self.target_map_name = target_map_name + self.target_pos = (target_x, target_y) + self.blocking = True + + def getStateForSaving(self): + """Returns state for saving + """ + ret_dict = super(Door, self).getStateForSaving() + return ret_dict diff -r 000000000000 -r 7a89ea5404b1 objects/containers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/objects/containers.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,110 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +"""Containes classes defining concrete container game objects like crates, + barrels, chests, etc.""" + +__all__ = ["WoodenCrate", "Footlocker"] + +_AGENT_STATE_NONE, _AGENT_STATE_OPENED, _AGENT_STATE_CLOSED, \ +_AGENT_STATE_OPENING, _AGENT_STATE_CLOSING = xrange(5) + +from composed import ImmovableContainer +from fife import fife + +class WoodenCrate (ImmovableContainer): + def __init__(self, object_id, name = 'Wooden Crate', + text = 'A battered crate', gfx = 'crate', **kwargs): + ImmovableContainer.__init__(self, ID = object_id, name = name, + gfx = gfx, text = text, **kwargs) + +class ContainerBehaviour(fife.InstanceActionListener): + def __init__(self, parent = None, agent_layer = None): + fife.InstanceActionListener.__init__(self) + self.parent = parent + self.layer = agent_layer + self.state = _AGENT_STATE_CLOSED + self.agent = None + + def attachToLayer(self, agent_id): + """ Attaches to a certain layer + @type agent_id: String + @param agent_id: ID of the layer to attach to. + @return: None""" + self.agent = self.layer.getInstance(agent_id) + self.agent.addActionListener(self) + self.state = _AGENT_STATE_CLOSED + self.agent.act('closed', self.agent.getLocation()) + + def onInstanceActionFinished(self, instance, action): + """What the Actor does when it has finished an action. + Called by the engine and required for InstanceActionListeners. + @type instance: fife.Instance + @param instance: self.agent (the Actor listener is listening for this + instance) + @type action: ??? + @param action: ??? + @return: None""" + if self.state == _AGENT_STATE_OPENING: + self.agent.act('opened', self.agent.getFacingLocation(), True) + self.state = _AGENT_STATE_OPENED + if self.state == _AGENT_STATE_CLOSING: + self.agent.act('closed', self.agent.getFacingLocation(), True) + self.state = _AGENT_STATE_CLOSED + + def open (self): + if self.state != _AGENT_STATE_OPENED and self.state != \ + _AGENT_STATE_OPENING: + self.agent.act('open', self.agent.getLocation()) + self.state = _AGENT_STATE_OPENING + + def close(self): + if self.state != _AGENT_STATE_CLOSED and self.state != \ + _AGENT_STATE_CLOSING: + self.agent.act('close', self.agent.getLocation()) + self.state = _AGENT_STATE_CLOSING + +class Footlocker(ImmovableContainer): + def __init__ (self, object_id, agent_layer=None, name = 'Footlocker', + text = 'A Footlocker', gfx = 'lock_box_metal01', **kwargs): + ImmovableContainer.__init__(self, ID = object_id, name = name, + gfx = gfx, text = text, **kwargs) + self.behaviour = None + + self.attributes.append("AnimatedContainer") + self.createBehaviour(agent_layer) + + def prepareStateForSaving(self, state): + """Prepares state for saving + @type state: dictionary + @param state: State of the object + """ + ImmovableContainer.prepareStateForSaving(self, state) + del state["behaviour"] + + def createBehaviour(self, layer): + self.behaviour = ContainerBehaviour(self, layer) + + def setup(self): + """@return: None""" + self.behaviour.attachToLayer(self.ID) + + def open (self): + super (Footlocker, self).open() + self.behaviour.open() + + def close(self): + super (Footlocker, self).close() + self.behaviour.close() diff -r 000000000000 -r 7a89ea5404b1 objects/doors.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/objects/doors.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,29 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +"""Containes classes defining concrete door game objects.""" + +__all__ = ["ShantyDoor", ] + +from composed import Door + +class ShantyDoor(Door): + def __init__ (self, ID, name = 'Shanty Door', text = 'A door', + gfx = 'shanty-door', target_map_name = 'my-map', + target_x = 0.0, target_y = 0.0, **kwargs): + Door.__init__(self, ID = ID, name = name, text = text, gfx = gfx, + target_map_name = target_map_name, + target_x = target_x, + target_y = target_y, **kwargs) diff -r 000000000000 -r 7a89ea5404b1 objects/items.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/objects/items.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,26 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +__all__ = ["MapItem", ] + +from composed import CarryableItem + +class MapItem(CarryableItem): + """Item that is lying on a map""" + def __init__(self, ID, item_type, item, name = 'Item', text = 'An item', + gfx = 'item', **kwargs): + CarryableItem.__init__(self, ID = ID, item_type = item_type, name = name, + text = text, gfx = gfx, **kwargs) + self.item = item diff -r 000000000000 -r 7a89ea5404b1 quest_engine.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/quest_engine.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,259 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +import yaml +from parpg.common.utils import locateFiles +import os.path + +class Quest(object): + """Class that holds the information for a quest""" + def __init__(self, quest_id, quest_giver_id, quest_name, description, + variables): + self.quest_id = quest_id + self.quest_giver_id = quest_giver_id + self.quest_name = quest_name + self.description = description + self.quest_variables = variables + + def setValue(self, variable_name, value): + """Set the value of a quest variable + @param variable_name: the name of the variable to set + @param value: the value you want to assign to the variable + @return: True on success + @return: False when it failes""" + + if self.quest_variables.has_key(variable_name): + self.quest_variables[variable_name]["value"] = value + return True + else: + return False + + def getValue(self, variable_name): + """Get the value of a quest_variable + @param variable_name: the name of the variable to set + @return: the value of the quest_variable""" + if self.quest_variables.has_key(variable_name): + return self.quest_variables[variable_name]["value"] + else: + return False + + def getGoalValue(self, variable_name): + """Get the goal value of a quest_variable + @param variable_name: the name of the variable to set + @return: the goal value of the quest variable""" + if self.quest_variables.has_key(variable_name): + return self.quest_variables[variable_name]["goal_value"] + else: + return False + + def increaseValue(self, variable_name, value): + """Increase a variable by a specified value + @param variable_name: the name of the variable to set + @param value: the value you want to increase the variable with + @return: True on success + @return: False when it fails""" + if self.quest_variables.has_key(variable_name): + self.quest_variables[variable_name]["value"] += value + return True + else: + return False + + def decreaseValue(self, variable_name, value): + """Decrease a variable by a specified value + @param variable_name: the name of the variable to set + @param value: the value you want to decrease the variable with + @return: True on success + @return: False when it failes""" + if self.quest_variables.has_key(variable_name): + self.quest_variables[variable_name]["value"] -= value + return True + else: + return False + + def isGoalValue(self, variable_name): + """Check if the variable has reached it's goal value + @param variable_name: the name of the variable to check + @return: True when the variable has reached the goal value + @return: False when it has not reached the goal value""" + if self.quest_variables.has_key(variable_name): + return self.quest_variables[variable_name]["value"] == \ + self.quest_variables[variable_name]["goal_value"] + else: + return False + + def isEqualOrBiggerThanGoalValue(self, variable_name): + """Check if the variable is equil or bigger then it's goal value + @param variable_name: the name of the variable to set + @return: True when it has reached or exceeded the goal value + @return: False when it has not reached or exceeded the goal value """ + if variable_name in self.quest_variables: + return self.quest_variables[variable_name]["value"] >= \ + self.quest_variables[variable_name]["goal_value"] + else: + return False + + def restartQuest(self): + """Restarts the quest. This sets all values to the reset values, + if there is a reset value present """ + for variable in self.quest_variables.itervalues(): + if variable.has_key("reset_value"): + variable["value"] = variable["reset_value"] + +class QuestEngine(dict): + def __init__(self, quest_dir): + """Create a quest engine object""" + dict.__init__(self) + self.empty_quest = Quest(None, None, None, None, {}) + self.quests = {} + self.active_quests = [] + self.finished_quests = [] + self.failed_quests = [] + self.quest_dir = quest_dir + + def __str__(self): + return self.quests.__str__() + + def __getitem__(self, key): + try: + return self.quests.__getitem__(key) + except KeyError: + return self.empty_quest + + def items(self): + return self.quests.items() + + def values(self): + return self.quests.values() + + def keys(self): + return self.quests.keys() + + def readQuests(self): + """Reads in the quests in the quest directory""" + files = locateFiles("*.yaml", self.quest_dir) + self.quests = {} + self.active_quests = [] + self.finished_quests = [] + self.failed_quests = [] + for quest_file in files: + quest_file = os.path.relpath(quest_file).replace("\\", "/") + tree = yaml.load(open(quest_file)) + quest_properties = tree["QUEST_PROPERTIES"] + variable_defines = tree["DEFINES"] + + self.quests[quest_properties["quest_id"]] = \ + Quest(quest_properties["quest_id"], + quest_properties["quest_giver_id"], + quest_properties["quest_name"], + quest_properties["description"], + variable_defines) + + def activateQuest(self, quest_id): + """Add a quest to the quest log + @param quest: the quest id of the quest to add to the quest log + @return: True if succesfully added + @return: False if failed to add""" + + if quest_id in self.quests \ + and not (quest_id in self.active_quests \ + or quest_id in self.finished_quests): + self.active_quests.append(quest_id) + return True + return False + + def finishQuest(self, quest_id): + """Move a quest to the finished quests log + @param quest_id: The id of the quest you want to move + @return: True on success + @return: False when it failes""" + if quest_id in self.active_quests: + self.finished_quests.append(quest_id) + self.active_quests.remove(quest_id) + return True + return False + + def restartQuest(self, quest_id): + """Restart a quest + @param quest_id: ID of the quest you want to restart + @return: True on success + @return: False when it failes""" + if quest_id in self.active_quests: + self.quests[quest_id].restartQuest() + + def failQuest(self, quest_id): + """Set a quest to failed + @param quest_id: ID of the quest you want to fail + @return: True on success + @return: False when it failes""" + if quest_id in self.active_quests: + self.failed_quests.append(quest_id) + self.active_quests.remove(quest_id) + return True + return False + + def hasQuest(self, quest_id): + """Check whether a quest is present in the quest_list. + It doesn't matter which state the quest is, or even if its + started. + @param quest_id: ID of the quest you want to check + @return: True on when the quest is in the quest log + @return: False when it's not in the quest log""" + return quest_id in self.quests + + def hasActiveQuest(self, quest_id): + """Check whether a quest is in the quest log + @param quest_id: ID of the quest you want to check + @return: True on when the quest is in the quest log + @return: False when it's not in the quest log""" + return quest_id in self.active_quests + + def hasFinishedQuest(self, quest_id): + """Check whether a quest is in the finished quests log + @param quest_id: ID of the quest you want to check + @return: True on when the quest is in the finished quests log + @return: False when it's not in the finished quests log""" + return quest_id in self.finished_quests + + def hasFailedQuest(self, quest_id): + """Check whether a quest is in the failed quests log + @param quest_id: ID of the quest you want to check + @return: True on when the quest is in the failed quests log + @return: False when it's not in the failed quests log""" + return quest_id in self.failed_quests + + def getStateForSaving(self): + """Prepares state for saving + @type state: dictionary + @param state: State of the object""" + ret_dict = {} + variables_dict = ret_dict["Variables"] = {} + for quest in self.quests.itervalues(): + quest_dict = variables_dict[quest.quest_id] = {} + for variable, data in quest.quest_variables.iteritems(): + quest_dict[variable] = data["value"] + ret_dict["ActiveQuests"] = self.active_quests + ret_dict["FinishedQuests"] = self.finished_quests + ret_dict["FailedQuests"] = self.failed_quests + return ret_dict + + def restoreFromState(self, state): + """Restores the state""" + variables_dict = state["Variables"] + for quest_id, variables in variables_dict.iteritems(): + for variable, value in variables.iteritems(): + self.quests[quest_id].setValue(variable, value) + self.active_quests = state["ActiveQuests"] + self.finished_quests = state["FinishedQuests"] + self.failed_quests = state["FailedQuests"] diff -r 000000000000 -r 7a89ea5404b1 serializers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/serializers.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,156 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . +""" +Provides classes used to serialize and deserialize Python classes. +""" + +from abc import ABCMeta, abstractmethod +try: + from xml.etree import cElementTree as ElementTree +except ImportError: + from xml.etree import ElementTree +try: + from collections import OrderedDict +except ImportError: + from .common.ordereddict import OrderedDict + +from .common.utils import dedent_chomp + +import logging + +logger = logging.getLogger('serializers') + +class Serializable(object): + def __init__(self, class_, init_args=None, attributes=None): + self.class_ = class_ + if init_args is not None: + self.init_args = OrderedDict(init_args) + else: + self.init_args = OrderedDict() + if attributes is not None: + self.attributes = OrderedDict(attributes) + else: + self.attributes = OrderedDict() + + +class SerializableRegistry(object): + """ + Class holding the data used to serialize and deserialize a particular + Python object. + """ + registered_classes = {} + + @classmethod + def registerClass(cls, name, class_, init_args=None, attributes=None): + serializable = Serializable(class_, init_args, attributes) + cls.registered_classes[name] = serializable + + +class AbstractSerializer(object): + __metaclass__ = ABCMeta + + @abstractmethod + def serialize(self, object_, stream): + pass + + @abstractmethod + def deserialize(self, stream): + pass + + +class XmlSerializer(AbstractSerializer): + def serialize(self, statistic, stream): + pass + + @classmethod + def deserialize(cls, stream): + element_tree = ElementTree.parse(stream) + root_element = element_tree.getroot() + object_ = cls.construct_object(root_element) + return object_ + + @classmethod + def construct_object(cls, element): + element_name = element.tag + if element_name in SerializableRegistry.registered_classes.keys(): + object_ = cls.construct_registered_class(element) + elif len(element) > 0: + # Element contains subelements, so we'll treat it as an + # OrderedDict. + if element_name == 'list': + object_ = cls.construct_list(element) + else: + object_ = cls.construct_ordered_dict(element) + else: + object_ = cls.construct_primitive(element) + return object_ + + @classmethod + def construct_registered_class(cls, element): + element_name = element.tag + serializable = SerializableRegistry.registered_classes[element_name] + class_ = serializable.class_ + init_args = OrderedDict() + for subelement in element: + arg = cls.construct_object(subelement) + subelement_name = subelement.tag + init_args[subelement_name] = arg + try: + object_ = class_(**init_args) + except (TypeError, ValueError) as exception: + logger.error(init_args) + error_message = \ + 'unable to deserialize tag {0}: {1}'.format(element_name, + exception) + raise ValueError(error_message) + return object_ + + @classmethod + def construct_ordered_dict(cls, element): + object_ = OrderedDict() + for subelement in element: + child = cls.construct_object(subelement) + name = subelement.tag + object_[name] = child + return object_ + + @classmethod + def construct_list(cls, element): + object_ = [] + for subelement in element: + child = cls.construct_object(subelement) + object_.append(child) + return object_ + + @classmethod + def construct_primitive(cls, element): + text = element.text + # Interpret the element's text as unicode by default. + element_type = element.attrib.get('type', 'unicode') + if element_type == 'unicode': + formatted_text = dedent_chomp(text) + object_ = unicode(formatted_text) + elif element_type == 'str': + formatted_text = dedent_chomp(text) + object_ = str(formatted_text) + elif element_type == 'int': + object_ = int(text) + elif element_type == 'float': + object_ = float(text) + else: + error_message = '{0!r} is not a recognized primitive type' + error_message.format(element_type) + raise ValueError(error_message) + return object_ diff -r 000000000000 -r 7a89ea5404b1 settings.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/settings.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,477 @@ +#!/usr/bin/env python2 + +# Copyright (C) 2011 Edwin Marshall + +# This file is part of PARPG. +# +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +""" Provides a class used for reading and writing various configurable options + throughout the game + + This class produces an INI formated settings file as opposed to an XML + formatted one. The reason that python's built-in ConfigurationParser isn't + sufficient is because comments aren't preserved when writing a settings + file, the order in which the options are written isn't preserved, and the + interface used with this class is arguably more convenient that + ConfigParser's. + + Default Settings may be generated by envoking this module from the + command line: + python -m settings.py [system] [data_directory] + + where [system] is one of local, windows, or linux (mac coming soon), + and data_directory is the base path for the data files to be loaded. + + Both [system] and [data_directory] are option. If omitted, both + default to whichever what is reasonable based on the system settings.py + is run on +""" + +import os +import sys +import platform + +#TODO: add logging to replace print statements +class Section(object): + """ An object that represents a section in a settings file. + + Options can be added to a section by simply assigning a value to an + attribute: + section.foo = baz + would produce: + [section] + foo = baz + in the settings file. Options that do not exist on assignment + are created dynamcially. + + Values are automatically converted to the appropriate python type. + Options that begin and end with brackets([, ]) are converted to lists, + and options that are double-quoted (") are converted to strings. + Section also recognizes booleans regardless of case, in addition to the + literals 'yes' and 'no' of any case. Except in the case of + double-quoted strings, extra white-space is trimmed, so you need not + worry. For example: + foo = bar + is equivalent to : + foo = baz + """ + def __init__(self, name): + """ Initialize a new section. + + @param name: name of the section. In the INI file, sections are surrounded + by brackets ([name]) + @type name: string + """ + self.name = name + + def __setattr__(self, option, value): + """ Assign a value to an option, converting types when appropriate. + + @param option: name of the option to assign a value to. + @type option: string @param value: value to be assigned to the option. + @type value: int, float, string, boolean, or list + """ + value = str(value) + if value.startswith('[') and value.endswith(']'): + value = [item.strip() for item in value[1:-1].split(',')] + elif value.lower() == 'true' or value.lower() == 'yes': + value = True + elif value.lower() == 'false' or value.lower() == 'no': + value = False + elif value.isdigit(): + value = int(value) + else: + try: + value = float(value) + except ValueError: + # leave as string + pass + + self.__dict__[option] = value + + def __getattribute__(self, option): + """ Returns the option's value""" + # Remove leading and trailing quotes from strings that have them + return_value = object.__getattribute__(self, option) + try: + for key, value in return_value.iteritems(): + if (hasattr(value, 'split') and + value.startswith("\"") and value.endswith("\"")): + return_value[key] = value[1:-1] + except AttributeError: + pass + + return return_value + + @property + def options(self): + """ Returns a dictionary of existing options """ + options = self.__dict__ + # get rid of properties that aren't actually options + if options.has_key('name'): + options.pop('name') + + return options + +class Settings(object): + """ An object that represents a settings file, its sectons, + and the options defined within those sections. + """ + def __init__(self, settings_path='', system_path='', user_path='', suffix='.cfg'): + """ initializes a new settings object. If no paths are given, they are + guessed based on whatever platform the script was run on. + + Examples: + paths = ['/etc/parpg', '/home/user_name/.config/parpg'] + settings = Settings(*paths) + + paths = {'system': '/etc/parpg', + 'user': '/home/user_name/.config/parpg'} + settings = Settings(**paths) + + settings = Settings('.') + + settigns = Settings() + + @param system_path: Path to the system settings file. + @type system_path: string (must be a valid path) + + @param user_path: Path to the user settings file. Options that + are missing from this file are propogated + from the system settings file and saved on + request + @type user_path: string (must be a valid path) + + @param suffix: Suffix of the settings file that will be generated. + @type suffix: string + """ + if not suffix.startswith('.'): + suffix = '.' + suffix + + self.suffix = suffix + self.settings_file = '' + + + self.paths = {} + if not system_path and not user_path and not settings_path: + # use platform-specific values as paths + (self.paths['system'], self.paths['user'], + self.paths['settings']) = self.platform_paths() + else: + # convert supplied paths to absolute paths + abs_paths = [os.path.expanduser(path) + for path in [system_path, user_path, settings_path]] + (self.paths['system'], self.paths['user'], + self.paths['settings']) = abs_paths + + self.read() + + + def __getattr__(self, name): + """ Returns a Section object to be used for assignment, creating one + if it doesn't exist. + + @param name: name of section to be retrieved + @type name: string + """ + if name in ['get', 'set']: + raise AttributeError("{0} is deprecated. Please consult Settings' " + "documentation for information on how to " + "create/modify sections and their respective " + "options".format(name)) + else: + if not self.__dict__.has_key(name): + setattr(self, name, Section(name)) + + return getattr(self, name) + + def platform_paths(self, system=None): + if system is None: + system = platform.system().lower() + + if system == 'linux': + return (os.path.join(os.sep, 'usr', 'share', 'parpg'), + os.path.join(os.environ['XDG_CONFIG_HOME'], 'parpg'), + os.path.join(os.sep, 'etc', 'parpg')) + elif system == 'windows': + return (os.path.join(os.environ['PROGRAMFILES'], 'PARPG'), + os.path.join(os.environ['USERDATA'], 'PARPG'), + os.path.join(os.environ['PROGRAMFILES'], 'PARPG')) + else: + # TODO: determine values for Mac + return None + + def read(self, filenames=None): + """ Reads a settings file and populates the settings object + with its sections and options. Calling this method without + any arguments simply re-reads the previously defined filename + and paths + + @param filenames: name of files to be parsed. + @type path: string or list + """ + + if filenames is None: + filenames = [os.path.join(self.paths['settings'], + 'system{0}'.format(self.suffix)), + os.path.join(self.paths['user'], + 'user{0}'.format(self.suffix))] + elif hasattr(filenames, 'split'): + filenames = [filenames] + + for filename in filenames: + section = None + + try: + self.settings_file = open(filename, 'r').readlines() + except IOError as (errno, strerror): + if errno == 2: + if os.path.basename(filename).startswith('system'): + print ('{0} could not be found. Please supply a ' + 'different path or generate a system settings ' + 'file with:\n' + 'python2 -m parpg.settings').format(filename) + sys.exit(1) + else: + print 'Error No. {0}: {1} {2}'.format(errno, filename, strerror) + sys.exit(1) + + for line in self.settings_file: + if line.startswith('#') or line.strip() == '': + continue + elif line.startswith('[') and line.endswith(']\n'): + getattr(self, line[1:-2]) + section = line[1:-2] + else: + option, value = [item.strip() + for item in line.split('=', 1)] + setattr(getattr(self, section), option, value) + + def write(self, filename=None): + """ Writes a settings file based on the settings object's + sections and options + + @param filename: Name of file to save to. By default, this is + the user settings file. + @type path: string + """ + if filename is None: + filename = os.path.join(self.paths['user'], + 'user{0}'.format(self.suffix)) + + for section in self.sections: + if '[{0}]\n'.format(section) not in self.settings_file: + self.settings_file.append('\n[{0}]\n'.format(section)) + for option, value in getattr(self, section).options.iteritems(): + template = '{0} = {1}\n'.format(option, value) + self.settings_file.append(template) + else: + start_of_section = (self.settings_file + .index('[{0}]\n'.format(section)) + 1) + + for option, value in getattr(self, + section).options.iteritems(): + if hasattr(value, 'sort'): + value = '[{0}]'.format(', '.join(value)) + + new_option = False + template = '{0} = {1}\n'.format(option, value) + for index, line in enumerate(self.settings_file[:]): + if option in line: + new_option = False + if str(value) not in line: + self.settings_file[index] = template + + break + else: + new_option = True + if new_option: + while self.settings_file[start_of_section].startswith('#'): + start_of_section += 1 + + self.settings_file.insert(start_of_section, template) + + with open(filename, 'w') as out_stream: + for line in self.settings_file: + out_stream.write(line) + + @property + def sections(self): + """ Returns a list of existing sections""" + sections = self.__dict__.keys() + sections.pop(sections.index('settings_file')) + sections.pop(sections.index('paths')) + sections.pop(sections.index('suffix')) + + return sections + + @property + def system_path(self): + return self.paths['system'] + + @property + def user_path(self): + return self.paths['user'] + + @property + def settings_path(self): + return self.paths['settings'] + +DEFAULT_SETTINGS = """\ +[fife] +#------------------------------------------------------------------------------ +# Options marked with ? are untested/unknown + +# Game window's title (string) DO NOT EDIT! +WindowTitle = PARPG Techdemo 2 + +# Icon to use for the game window's border (filename) DO NOT EDIT! +WindowIcon = window_icon.png + +# Video driver to use. (?) +VideoDriver = "" + +# Backend to use for graphics (OpenGL|SDL) +RenderBackend = OpenGL + +# Run the game in fullscreen mode or not. (True|False) +FullScreen = False + +# Screen Resolution's width. Not used if FullScreen is set to False (800|1024|etc) +ScreenWidth = 1024 + +# Screen Resolution's height. Not used if FullScreen is set to False (600|768|etc) +ScreenHeight = 768 + +# Screen DPI? (?) +BitsPerPixel = 0 + +# ? (?) +SDLRemoveFakeAlpha = 1 + +# Subdirectory to load icons from (path) +IconsPath = icons + +# ? ([R, G, B]) +ColorKey = [250, 0, 250] + +# ? (True|False) +ColorKeyEnabled = False + +# Turn on sound effects and music (True|False) +EnableSound = True + +# Initial volume of sound effects and music (0.0-100.0?) +InitialVolume = 5.0 + +# Characters to use to render fonts. DO NOT EDIT! +FontGlyphs = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?-+/():;%&`'*#=[]\"" + +# Subdirectory to load fronts from (path) +FontsPath = fonts + +# Font to load when game starts +Font = oldtypewriter.ttf + +# Size of in-game fonts +DefaultFontSize = 12 + +# ? (?) +LogModules = [controller] + +# ? (?) +PychanDebug = False + +# use Psyco Acceperation (True|False) +UsePsyco = False + +# ? (?) +ProfilingOn = False + +# Lighting Model to use (0-2) +Lighting = 0 + +[parpg] +#------------------------------------------------------------------------------ + +# System subdirectory to load maps from (path) +MapsPath = maps + +# YAML file that contains the available maps (filename) +MapsFile = maps.yaml + +# Map to load when game starts (filename) +Map = Mall + +# ? (filename) +AllAgentsFile = all_agents.yaml + +# System subdirectory to load objects from (path) +ObjectsPath = objects + +# YAML file that contains the database of availabel objects (filename) +ObjectDatabaseFile = object_database.yaml + +# System subdirectory to load dialogues from (path) +DialoguesPath = dialogue + +# System subdirectory to load quests from (path) +QuestsPath = quests + +# User subdirectory to save screenshots to +ScreenshotsPath = screenshots + +# User subdirectory to save games to +SavesPath = saves + +# System subdirectory where gui files are loaded from (path) +GuiPath = gui + +# System subdirectory where cursors are loaded from (path) +CursorPath = cursors + +# File to use for default cursor (filename) +CursorDefault = cursor_plain.png + +# File to use for up cursor (filename) +CursorUp = cursor_up.png + +# File to use for right cursor (filename) +CursorRight = cursor_right.png + +# File to use for down cursor (filename) +CursorDown = cursor_down.png + +# File to use for left cursor (filename) +CursorLeft = cursor_left.png + +# Player walk speed (digit) +PCSpeed = 3\ +""" + +if __name__ == '__main__': + from optparse import OptionParser + + usage = "usage: %prog [options] system[, system, ...]" + parser = OptionParser(usage=usage) + + parser.add_option('-f', '--filename', default='system.cfg', + help='Filename of output configuration file') + + opts, args = parser.parse_args() + + with open(opts.filename, 'w') as f: + for line in DEFAULT_SETTINGS: + f.write(line) diff -r 000000000000 -r 7a89ea5404b1 sounds.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sounds.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,67 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +# sounds.py holds the object code to play sounds and sound effects + +class SoundEngine: + def __init__(self, fife_engine): + """Initialise the SoundEngine instance + @type fife_engine: fine.Engine + @param fife_engine: Instance of the Fife engine + @return: None""" + self.engine = fife_engine + self.sound_engine = self.engine.getSoundManager() + self.sound_engine.init() + # set up the sound + self.music = self.sound_engine.createEmitter() + self.music_on = False + self.music_init = False + + def playMusic(self, sfile=None): + """Play music, with the given file if passed + @type sfile: string + @param sfile: Filename to play + @return: None""" + if(sfile is not None): + # setup the new sound + sound = self.engine.getSoundClipPool().addResourceFromFile(sfile) + self.music.setSoundClip(sound) + self.music.setLooping(True) + self.music_init = True + self.music.play() + self.music_on = True + + def pauseMusic(self): + """Stops current playback + @return: None""" + if(self.music_init == True): + self.music.pause() + self.music_on = False + + def toggleMusic(self): + """Toggle status of music, either on or off + @return: None""" + if((self.music_on == False)and(self.music_init == True)): + self.playMusic() + else: + self.pauseMusic() + + def setVolume(self, volume): + """Set the volume of the music + @type volume: integer + @param volume: The volume wanted, 0 to 100 + @return: None""" + self.sound_engine.setVolume(0.01 * volume) + diff -r 000000000000 -r 7a89ea5404b1 viewbase.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/viewbase.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,26 @@ +# This file is part of PARPG. + +# PARPG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# PARPG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with PARPG. If not, see . + +class ViewBase(object): + """Base class for views""" + def __init__(self, engine, model): + """Constructor for engine + @param engine: A fife.Engine instance + @type engine: fife.Engine + @param model: a script.GameModel instance + @type model: script.GameModel + """ + self.engine = engine + self.model = model