Mercurial > pyntnclick
view pyntnclick/state.py @ 803:bcc9277a23e6 pyntnclick
Refactor widget positioning API. Remove unused widgets
author | Stefano Rivera <stefano@rivera.za.net> |
---|---|
date | Sun, 27 Jan 2013 14:52:16 +0200 |
parents | bdaffaa8b6bf |
children | d99400fcd624 |
line wrap: on
line source
"""Utilities and base classes for dealing with scenes.""" import os import json import copy from widgets.text import LabelWidget from pygame.color import Color from pyntnclick.engine import ScreenEvent from pyntnclick.tools.utils import draw_rect_image class Result(object): """Result of interacting with a thing""" def __init__(self, message=None, soundfile=None, detail_view=None, widget=None, end_game=False): self.message = message self.soundfile = soundfile self.detail_view = detail_view self.widget = widget self.end_game = end_game def play_sound(self, screen): if self.soundfile: sound = screen.gd.sound.get_sound(self.soundfile) sound.play() def process(self, screen): """Helper function to do the right thing with a result object""" self.play_sound(screen) if self.widget: screen.queue_widget(self.widget) if self.message: screen.show_message(self.message) if self.detail_view: screen.game.show_detail(self.detail_view) if self.end_game: screen.end_game() class GameState(object): """This holds the serializable game state. Games wanting to do fancier stuff with the state should sub-class this and feed the subclass into GameDescription via the custom_data parameter.""" def __init__(self, state_dict=None): if state_dict is None: state_dict = {'inventories': {'main': []}, 'current_scene': None} self._game_state = copy.deepcopy(state_dict) def __getitem__(self, key): return self._game_state[key] def __contains__(self, key): return key in self._game_state def export_data(self): return copy.deepcopy(self._game_state) def get_all_gizmo_data(self, state_key): """Get all state for a gizmo - returns a dict""" return self[state_key] def get_data(self, state_key, data_key): """Get a single entry""" return self[state_key].get(data_key, None) def set_data(self, state_key, data_key, value): """Set a single value""" self[state_key][data_key] = value def initialize_state(self, state_key, initial_data): """Initialize a gizmo entry""" if state_key not in self._game_state: self._game_state[state_key] = {} if initial_data: # deep copy of INITIAL_DATA allows lists, dicts and other # mutable types to safely be used in INITIAL_DATA self._game_state[state_key].update(copy.deepcopy(initial_data)) def inventory(self, name='main'): return self['inventories'][name] def set_current_scene(self, scene_name): self._game_state['current_scene'] = scene_name @classmethod def get_save_fn(cls, save_dir, save_name): return os.path.join(save_dir, '%s.json' % (save_name,)) @classmethod def load_game(cls, save_dir, save_name): fn = cls.get_save_fn(save_dir, save_name) if os.access(fn, os.R_OK): f = open(fn, 'r') state = json.load(f) f.close() return state def save_game(self, save_dir, save_name): fn = self.get_save_fn(save_dir, save_name) if not os.path.isdir(save_dir): os.makedirs(save_dir) f = open(fn, 'w') json.dump(self.export_data(), f) f.close() class Game(object): """Complete game state. Game state consists of: * items * scenes """ def __init__(self, gd, game_state): # game description self.gd = gd # map of scene name -> Scene object self.scenes = {} # map of detail view name -> DetailView object self.detail_views = {} # map of item name -> Item object self.items = {} # list of item objects in inventory self.current_inventory = 'main' # currently selected tool (item) self.tool = None # Global game data self.data = game_state # debug rects self.debug_rects = False def get_current_scene(self): scene_name = self.data['current_scene'] if scene_name is not None: return self.scenes[scene_name] return None def inventory(self, name=None): if name is None: name = self.current_inventory return self.data.inventory(name) def set_custom_data(self, data_object): self.data = data_object def set_debug_rects(self, value=True): self.debug_rects = value def add_scene(self, scene): scene.set_game(self) self.scenes[scene.name] = scene def add_detail_view(self, detail_view): detail_view.set_game(self) self.detail_views[detail_view.name] = detail_view def add_item(self, item): item.set_game(self) self.items[item.name] = item def load_scenes(self, modname): mod = __import__('%s.%s' % (self.gd.SCENE_MODULE, modname), fromlist=[modname]) for scene_cls in mod.SCENES: scene = scene_cls(self) self.add_scene(scene) if hasattr(mod, 'DETAIL_VIEWS'): for scene_cls in mod.DETAIL_VIEWS: scene = scene_cls(self) self.add_detail_view(scene) def change_scene(self, name): ScreenEvent.post('game', 'change_scene', {'name': name, 'detail': False}) def show_detail(self, name): ScreenEvent.post('game', 'change_scene', {'name': name, 'detail': True}) def _update_inventory(self): ScreenEvent.post('game', 'inventory', None) def add_inventory_item(self, name): self.inventory().append(name) self._update_inventory() def is_in_inventory(self, name): return name in self.inventory() def remove_inventory_item(self, name): self.inventory().remove(name) # Unselect tool if it's removed if self.tool == self.items[name]: self.set_tool(None) self._update_inventory() def replace_inventory_item(self, old_item_name, new_item_name): """Try to replace an item in the inventory with a new one""" try: index = self.inventory().index(old_item_name) self.inventory()[index] = new_item_name if self.tool == self.items[old_item_name]: self.set_tool(self.items[new_item_name]) except ValueError: return False self._update_inventory() return True def set_tool(self, item): self.tool = item class GameDeveloperGizmo(object): """Base class for objects game developers see.""" def __init__(self): """Set """ self.game = None self.gd = None self.resource = None self.sound = None def set_game(self, game): self.game = game self.gd = game.gd self.resource = self.gd.resource self.sound = self.gd.sound self.set_state(self.game.data) self.setup() def set_state(self, state): """Hack to allow set_state() to be called before setup().""" pass def setup(self): """Game developers should override this to do their setup. It will be called after all the useful state functions have been set. """ pass class StatefulGizmo(GameDeveloperGizmo): # initial data (optional, defaults to none) INITIAL_DATA = None STATE_KEY = None def __init__(self): GameDeveloperGizmo.__init__(self) self.state_key = self.STATE_KEY self.state = None # set this with set_state if required def set_state(self, state): """Set the state object and initialize if needed""" self.state = state self.state.initialize_state(self.state_key, self.INITIAL_DATA) def set_data(self, key, value): if self.state: self.state.set_data(self.state_key, key, value) def get_data(self, key): if self.state: return self.state.get_data(self.state_key, key) class Scene(StatefulGizmo): """Base class for scenes.""" # sub-folder to look for resources in FOLDER = None # name of background image resource BACKGROUND = None # name of scene (optional, defaults to folder) NAME = None # Offset of the background image OFFSET = (0, 0) def __init__(self, state): StatefulGizmo.__init__(self) # scene name self.name = self.NAME if self.NAME is not None else self.FOLDER self.state_key = self.name # map of thing names -> Thing objects self.things = {} self.current_thing = None self._background = None def add_item(self, item): self.game.add_item(item) def add_thing(self, thing): thing.set_game(self.game) if not thing.should_add(): return self.things[thing.name] = thing thing.set_scene(self) def remove_thing(self, thing): del self.things[thing.name] if thing is self.current_thing: self.current_thing.leave() self.current_thing = None def _get_description(self, dest_rect): text = (self.current_thing and self.current_thing.get_description()) if text is None: return None label = LabelWidget((0, 10), self.gd, text) label.do_prepare() # TODO: Centre more cleanly label.rect.left += (dest_rect.width - label.rect.width) / 2 return label def draw_description(self, surface): description = self._get_description(surface.get_rect()) if description is not None: description.draw(surface) def _cache_background(self): if self.BACKGROUND and not self._background: self._background = self.resource.get_image( self.FOLDER, self.BACKGROUND) def draw_background(self, surface): self._cache_background() if self._background is not None: surface.blit(self._background, self.OFFSET, None) else: surface.fill((200, 200, 200)) def draw_things(self, surface): for thing in self.things.itervalues(): thing.draw(surface) def draw(self, surface): self.draw_background(surface) self.draw_things(surface) def interact(self, item, pos): """Interact with a particular position. Item may be an item in the list of items or None for the hand. Returns a Result object to provide feedback to the player. """ if self.current_thing is not None: return self.current_thing.interact(item) def animate(self): """Animate all the things in the scene. Return true if any of them need to queue a redraw""" result = False for thing in self.things.itervalues(): if thing.animate(): result = True return result def enter(self): return None def leave(self): return None def update_current_thing(self, pos): if self.current_thing is not None: if not self.current_thing.contains(pos): self.current_thing.leave() self.current_thing = None for thing in self.things.itervalues(): if thing.contains(pos): thing.enter(self.game.tool) self.current_thing = thing break def mouse_move(self, pos): """Call to check whether the cursor has entered / exited a thing. Item may be an item in the list of items or None for the hand. """ self.update_current_thing(pos) def get_detail_size(self): self._cache_background() return self._background.get_size() def get_image(self, *image_name_fragments, **kw): return self.resource.get_image(*image_name_fragments, **kw) def set_state(self, state): return super(Scene, self).set_state(state) class InteractiveMixin(object): def is_interactive(self, tool=None): return True def interact(self, tool): if not self.is_interactive(tool): return None if tool is None: return self.interact_without() handler = getattr(self, 'interact_with_' + tool.tool_name, None) inverse_handler = self.get_inverse_interact(tool) if handler is not None: return handler(tool) elif inverse_handler is not None: return inverse_handler(self) else: return self.interact_default(tool) def get_inverse_interact(self, tool): return None def interact_without(self): return self.interact_default(None) def interact_default(self, item=None): return None class Thing(StatefulGizmo, InteractiveMixin): """Base class for things in a scene that you can interact with.""" # name of thing NAME = None # sub-folder to look for resources in (defaults to scenes folder) FOLDER = None # list of Interact objects INTERACTS = {} # name first interact INITIAL = None # Interact rectangle hi-light color (for debugging) # (set to None to turn off) _interact_hilight_color = Color('red') def __init__(self): StatefulGizmo.__init__(self) # name of the thing self.name = self.NAME # folder for resource (None is overridden by scene folder) self.folder = self.FOLDER self.state_key = self.NAME # interacts self.interacts = self.INTERACTS # these are set by set_scene self.scene = None self.current_interact = None self.rect = None self.orig_rect = None def _fix_rect(self): """Fix rects to compensate for scene offset""" # Offset logic is to always work with copies, to avoid # flying effects from multiple calls to _fix_rect # See footwork in draw if hasattr(self.rect, 'collidepoint'): self.rect = self.rect.move(self.scene.OFFSET) else: self.rect = [x.move(self.scene.OFFSET) for x in self.rect] def should_add(self): return True def set_scene(self, scene): assert self.scene is None self.scene = scene if self.folder is None: self.folder = scene.FOLDER self.game = scene.game self.set_state(self.game.data) for interact in self.interacts.itervalues(): interact.set_thing(self) self.set_interact() def set_interact(self): return self._set_interact(self.select_interact()) def _set_interact(self, name): self.current_interact = self.interacts[name] self.rect = self.current_interact.interact_rect if self.scene: self._fix_rect() assert self.rect is not None, name def select_interact(self): return self.INITIAL def contains(self, pos): if hasattr(self.rect, 'collidepoint'): return self.rect.collidepoint(pos) else: for rect in list(self.rect): if rect.collidepoint(pos): return True return False def get_description(self): return None def enter(self, item): """Called when the cursor enters the Thing.""" pass def leave(self): """Called when the cursor leaves the Thing.""" pass def animate(self): return self.current_interact.animate() def draw(self, surface): old_rect = self.current_interact.rect if old_rect: self.current_interact.rect = old_rect.move(self.scene.OFFSET) self.current_interact.draw(surface) self.current_interact.rect = old_rect if self.game.debug_rects and self._interact_hilight_color: if hasattr(self.rect, 'collidepoint'): draw_rect_image(surface, self._interact_hilight_color, self.rect.inflate(1, 1), 1) else: for rect in self.rect: draw_rect_image(surface, self._interact_hilight_color, rect.inflate(1, 1), 1) class Item(GameDeveloperGizmo, InteractiveMixin): """Base class for inventory items.""" # image for inventory INVENTORY_IMAGE = None # name of item NAME = None # name for interactions (i.e. def interact_with_<TOOL_NAME>) TOOL_NAME = None # set to instance of CursorSprite CURSOR = None def __init__(self, name=None): GameDeveloperGizmo.__init__(self) self.name = self.NAME if name is not None: self.name = name self.tool_name = name if self.TOOL_NAME is not None: self.tool_name = self.TOOL_NAME self.inventory_image = None def _cache_inventory_image(self): if not self.inventory_image: self.inventory_image = self.resource.get_image( 'items', self.INVENTORY_IMAGE) def get_inventory_image(self): self._cache_inventory_image() return self.inventory_image def get_inverse_interact(self, tool): return getattr(tool, 'interact_with_' + self.tool_name, None) def is_interactive(self, tool=None): if tool: return True return False class CloneableItem(Item): _counter = 0 @classmethod def _get_new_id(cls): cls._counter += 1 return cls._counter - 1 def __init__(self, name=None): super(CloneableItem, self).__init__(name) my_count = self._get_new_id() self.name = "%s.%s" % (self.name, my_count)