def startup(self, *items, **kwargs): """ Accepted Keyword Arguments: prompt: String used to let user know what value is being inputted (ie "Name?", "IP Address?") callback: Function to be called when dialog is confirmed. The value will be sent as only argument initial: Optional string to pre-fill the input box with. :param items: :param kwargs: :return: """ super(InputMenu, self).startup(*items, **kwargs) self.input_string = kwargs.get("initial", "") # area where the input will be shown self.text_area = TextArea(self.font, self.font_color, (96, 96, 96)) self.text_area.animated = False self.text_area.rect = Rect(tools.scale_sequence([90, 30, 80, 100])) self.text_area.text = self.input_string self.sprites.add(self.text_area) # prompt self.prompt = TextArea(self.font, self.font_color, (96, 96, 96)) self.prompt.animated = False self.prompt.rect = Rect(tools.scale_sequence([50, 20, 80, 100])) self.sprites.add(self.prompt) self.prompt.text = kwargs.get("prompt", "") self.callback = kwargs.get("callback") assert self.callback
def draw_hp_bars(self): """ Go through the HP bars and redraw them :returns: None """ for monster, hud in self.hud.items(): rect = Rect(0, 0, tools.scale(70), tools.scale(8)) rect.right = hud.image.get_width() - tools.scale(8) rect.top += tools.scale(12) self._hp_bars[monster].draw(hud.image, rect)
def draw_exp_bars(self): """ Go through the EXP bars and redraw them :rtype: None """ for monster, hud in self.hud.items(): if hud.player: rect = Rect(0, 0, tools.scale(70), tools.scale(6)) rect.right = hud.image.get_width() - tools.scale(8) rect.top += tools.scale(31) self._exp_bars[monster].draw(hud.image, rect)
def initialize_items(self): # position the monster portrait try: monster = local_session.player.monsters[self.selected_index] image = monster.sprites["front"] except IndexError: image = pygame.Surface((1, 1), pygame.SRCALPHA) # position and animate the monster portrait width, height = prepare.SCREEN_SIZE self.monster_portrait.rect = image.get_rect(centerx=width // 4, top=height // 12) self.sprites.add(self.monster_portrait) self.animations.empty() self.animate_monster_down() width = prepare.SCREEN_SIZE[0] // 2 height = prepare.SCREEN_SIZE[1] // (local_session.player.party_limit * 1.5) # make 6 slots for i in range(local_session.player.party_limit): rect = Rect(0, 0, width, height) surface = pygame.Surface(rect.size, pygame.SRCALPHA) item = MenuItem(surface, None, None, None) yield item self.refresh_menu_items()
def startup(self, **kwargs): super(MonsterMenuState, self).startup(**kwargs) # make a text area to show messages self.text_area = TextArea(self.font, self.font_color, (96, 96, 96)) self.text_area.rect = Rect(tools.scale_sequence([20, 80, 80, 100])) self.sprites.add(self.text_area, layer=100) # Set up the border images used for the monster slots self.monster_slot_border = {} self.monster_portrait = pygame.sprite.Sprite() self.hp_bar = HpBar() self.exp_bar = ExpBar() # load and scale the monster slot borders root = "gfx/ui/monster/" border_types = ["empty", "filled", "active"] for border_type in border_types: filename = root + border_type + "_monster_slot_border.png" border = graphics.load_and_scale(filename) filename = root + border_type + "_monster_slot_bg.png" background = graphics.load_image(filename) window = GraphicBox(border, background, None) self.monster_slot_border[border_type] = window # TODO: something better than this global, load_sprites stuff for monster in local_session.player.monsters: monster.load_sprites()
def animate_open(self): """ Animate the menu sliding in :return: """ self.state = "opening" # required # position the menu off screen. it will be slid into view with an animation right, height = prepare.SCREEN_SIZE # TODO: more robust API for sizing (kivy esque?) # this is highly irregular: # shrink to get the final width # record the width # turn off shrink, then adjust size self.shrink_to_items = True # force shrink of menu self.menu_items.expand = False # force shrink of items self.refresh_layout() # rearrange items width = self.rect.width # store the ideal width self.shrink_to_items = False # force menu to expand self.menu_items.expand = True # force menu to expand self.refresh_layout() # rearrange items self.rect = Rect(right, 0, width, height) # set new rect # animate the menu sliding in ani = self.animate(self.rect, x=right - width, duration=.50) ani.callback = lambda: setattr(self, "state", "normal") return ani
def strip_coords_from_sheet(sheet, coords, size): """Strip specific coordinates from a sprite sheet.""" frames = [] for coord in coords: location = (coord[0] * size[0], coord[1] * size[1]) frames.append(sheet.subsurface(Rect(location, size))) return frames
def show_combat_dialog(self): """ Create and show the area where battle messages are displayed """ # make the border and area at the bottom of the screen for messages x, y, w, h = self.client.screen.get_rect() rect = Rect(0, 0, w, h // 4) rect.bottomright = w, h border = graphics.load_and_scale(self.borders_filename) self.dialog_box = GraphicBox(border, None, self.background_color) self.dialog_box.rect = rect self.sprites.add(self.dialog_box, layer=100) # make a text area to show messages self.text_area = TextArea(self.font, self.font_color) self.text_area.rect = self.dialog_box.calc_inner_rect(self.dialog_box.rect) self.sprites.add(self.text_area, layer=100)
def test_snap_y_axis(self): rect = Rect(1, 16, 16, 30) grid_size = (16, 16) result = snap_rect(rect, grid_size) self.assertEqual(0, result.x) self.assertEqual(16, result.y) self.assertEqual(16, result.w) self.assertEqual(32, result.h)
def show_monster_action_menu(self, monster): """ Show the main window for choosing player actions :param monster: Monster to choose an action for :type monster: tuxemon.core.monster.Monster :returns: None """ message = T.format('combat_monster_choice', {"name": monster.name}) self.alert(message) x, y, w, h = self.client.screen.get_rect() rect = Rect(0, 0, w // 2.5, h // 4) rect.bottomright = w, h state = self.client.push_state("MainCombatMenuState", columns=2) state.monster = monster state.rect = rect
def strip_from_sheet(sheet, start, size, columns, rows=1): """Strips individual frames from a sprite sheet given a start location, sprite size, and number of columns and rows.""" frames = [] for j in range(rows): for i in range(columns): location = (start[0] + size[0] * i, start[1] + size[1] * j) frames.append(sheet.subsurface(Rect(location, size))) return frames
def _collision_box_to_pgrect(self, box): """Returns a Rect (in screen-coords) version of a collision box (in world-coords). """ # For readability x, y = self.get_pos_from_tilepos(box) tw, th = self.tile_size return Rect(x, y, tw, th)
class RelativeGroup(SpriteGroup): """ Drawing operations are relative to the group's rect """ rect = Rect(0, 0, 0, 0) def __init__(self, **kwargs): self.parent = kwargs.get('parent') super(RelativeGroup, self).__init__(**kwargs) def calc_bounding_rect(self): """A rect object that contains all sprites of this group """ rect = super(RelativeGroup, self).calc_bounding_rect() # return self.calc_absolute_rect(rect) return rect def calc_absolute_rect(self, rect): self.update_rect_from_parent() return rect.move(self.rect.topleft) def update_rect_from_parent(self): try: self.rect = self.parent() except TypeError: self.rect = Rect(self.parent.rect) def draw(self, surface): self.update_rect_from_parent() topleft = self.rect.topleft spritedict = self.spritedict surface_blit = surface.blit dirty = self.lostsprites self.lostsprites = [] dirty_append = dirty.append for s in self.sprites(): if s.image is None: continue if not getattr(s, 'visible', True): continue r = spritedict[s] newrect = surface_blit(s.image, s.rect.move(topleft)) if r: if newrect.colliderect(r): dirty_append(newrect.union(r)) else: dirty_append(newrect) dirty_append(r) else: dirty_append(newrect) spritedict[s] = newrect return dirty
def snap_rect(rect, grid_size): """ Align all vertices to the nearest point :param rect: :param grid_size: :return: """ left, top = snap_point(rect.topleft, grid_size) right, bottom = snap_point(rect.bottomright, grid_size) return Rect((left, top), (right - left, bottom - top))
def calc_bounding_rect(self): """A rect object that contains all sprites of this group """ sprites = self.sprites() if not sprites: return self.rect elif len(sprites) == 1: return Rect(sprites[0].rect) else: return sprites[0].rect.unionall([s.rect for s in sprites[1:]])
def __init__(self, font, font_color, bg=(192, 192, 192)): super().__init__() self.rect = Rect(0, 0, 0, 0) self.drawing_text = False self.font = font self.font_color = font_color self.font_bg = bg self._rendered_text = None self._text_rect = None self._image = None self._text = None
def initialize_items(self): empty_image = None rect = self.client.screen.get_rect() slot_rect = Rect(0, 0, rect.width * 0.80, rect.height // 6) for i in range(self.number_of_slots): # Check to see if a save exists for the current slot if os.path.exists(prepare.SAVE_PATH + str(i + 1) + ".save"): image = self.render_slot(slot_rect, i + 1) item = MenuItem(image, T.translate('menu_save'), None, None) self.add(item) else: if not empty_image: empty_image = self.render_empty_slot(slot_rect) item = MenuItem(empty_image, "SAVE", None, None) self.add(item)
def load(self): from tuxemon.core import prepare self.dpad["surface"] = graphics.load_and_scale("gfx/d-pad.png") self.dpad["position"] = (0, prepare.SCREEN_SIZE[1] - self.dpad["surface"].get_height()) # Create the collision rectangle objects for the dpad so we can see if we're pressing a button self.dpad["rect"] = {} self.dpad["rect"]["up"] = Rect( self.dpad["position"][0] + (self.dpad["surface"].get_width() / 3), self.dpad["position"][1], # Rectangle position_y self.dpad["surface"].get_width() / 3, # Rectangle size_x self.dpad["surface"].get_height() / 2) # Rectangle size_y self.dpad["rect"]["down"] = Rect( self.dpad["position"][0] + (self.dpad["surface"].get_width() / 3), self.dpad["position"][1] + (self.dpad["surface"].get_height() / 2), self.dpad["surface"].get_width() / 3, self.dpad["surface"].get_height() / 2) self.dpad["rect"]["left"] = Rect( self.dpad["position"][0], self.dpad["position"][1] + (self.dpad["surface"].get_height() / 3), self.dpad["surface"].get_width() / 2, self.dpad["surface"].get_height() / 3) self.dpad["rect"]["right"] = Rect( self.dpad["position"][0] + (self.dpad["surface"].get_width() / 2), self.dpad["position"][1] + (self.dpad["surface"].get_height() / 3), self.dpad["surface"].get_width() / 2, self.dpad["surface"].get_height() / 3) # Create the buttons self.a_button = {} self.a_button["surface"] = graphics.load_and_scale("gfx/a-button.png") self.a_button["position"] = ( prepare.SCREEN_SIZE[0] - int(self.a_button["surface"].get_width() * 1.0), (self.dpad["position"][1] + (self.dpad["surface"].get_height() / 2) - (self.a_button["surface"].get_height() / 2))) self.a_button["rect"] = Rect(self.a_button["position"][0], self.a_button["position"][1], self.a_button["surface"].get_width(), self.a_button["surface"].get_height()) self.b_button = {} self.b_button["surface"] = graphics.load_and_scale("gfx/b-button.png") self.b_button["position"] = ( prepare.SCREEN_SIZE[0] - int(self.b_button["surface"].get_width() * 2.1), (self.dpad["position"][1] + (self.dpad["surface"].get_height() / 2) - (self.b_button["surface"].get_height() / 2))) self.b_button["rect"] = Rect(self.b_button["position"][0], self.b_button["position"][1], self.b_button["surface"].get_width(), self.b_button["surface"].get_height())
def region_tiles(region, grid_size): """ Apply region properties to individual tiles Right now our collisions are defined in our tmx file as large regions that the player can't pass through. We need to convert these areas into individual tile coordinates that the player can't pass through. Loop through all of the collision objects in our tmx file. The region's bounding box will be snapped to the nearest tile coordinates. :param region: :param grid_size: :return: """ region_conditions = copy_dict_with_keys(region.properties, region_properties) rect = snap_rect( Rect(region.x, region.y, region.width, region.height), grid_size ) for tile_position in tiles_inside_rect(rect, grid_size): yield tile_position, extract_region_properties(region_conditions)
def test_snap_rect_result_is_rect(self): rect = Rect(1, 1, 14, 14) grid_size = (16, 16) result = snap_rect(rect, grid_size) self.assertIsInstance(result, Rect)
def test_correct_result(self): rect = Rect(0, 16, 32, 48) grid_size = (16, 16) expected = [(0, 1), (1, 1), (0, 2), (1, 2), (0, 3), (1, 3)] result = list(tiles_inside_rect(rect, grid_size)) self.assertEqual(expected, result)
def _npc_to_pgrect(self, npc): """Returns a Rect (in screen-coords) version of an NPC's bounding box. """ pos = self.get_pos_from_tilepos(npc.tile_pos) return Rect(pos, self.tile_size)
def __init__(self, npc_slug, sprite_name=None, combat_front=None, combat_back=None): super().__init__() # load initial data from the npc database npc_data = db.lookup(npc_slug, table="npc") self.slug = npc_slug # This is the NPC's name to be used in dialog self.name = T.translate(self.slug) # use 'animations' passed in # Hold on the the string so it can be sent over the network self.sprite_name = sprite_name self.combat_front = combat_front self.combat_back = combat_back if self.sprite_name is None: # Try to use the sprites defined in the JSON data self.sprite_name = npc_data["sprite_name"] if self.combat_front is None: self.combat_front = npc_data["combat_front"] if self.combat_back is None: self.combat_back = npc_data["combat_back"] # general self.behavior = "wander" # not used for now self.game_variables = {} # Tracks the game state self.interactions = [] # List of ways player can interact with the Npc self.isplayer = False # used for various tests, idk self.monsters = [ ] # This is a list of tuxemon the npc has. Do not modify directly self.inventory = {} # The Player's inventory. # Variables for long-term item and monster storage # Keeping these seperate so other code can safely # assume that all values are lists self.monster_boxes = dict() self.item_boxes = dict() # combat related self.ai = None # Whether or not this player has AI associated with it self.speed = 10 # To determine combat order (not related to movement!) self.moves = [] # list of techniques # pathfinding and waypoint related self.pathfinding = None self.path = [] self.final_move_dest = [ 0, 0 ] # Stores the final destination sent from a client # This is used to 'set back' when lost, and make movement robust. # If entity falls off of map due to a bug, it can be returned to this value. # When moving to a waypoint, this is used to detect if movement has overshot # the destination due to speed issues or framerate jitters. self.path_origin = None # movement related self.move_direction = None # Set this value to move the npc (see below) self.facing = "down" # Set this value to change the facing direction self.moverate = CONFIG.player_walkrate # walk by default self.ignore_collisions = False # What is "move_direction"? # Move direction allows other functions to move the npc in a controlled way. # To move the npc, change the value to one of four directions: left, right, up or down. # The npc will then move one tile in that direction until it is set to None. # TODO: move sprites into renderer so class can be used headless self.playerHeight = 0 self.playerWidth = 0 self.standing = {} # Standing animation frames self.sprite = {} # Moving animation frames self.moveConductor = pyganim.PygConductor() self.load_sprites() self.rect = Rect( self.tile_pos, (self.playerWidth, self.playerHeight)) # Collision rect
def update_rect_from_parent(self): try: self.rect = self.parent() except TypeError: self.rect = Rect(self.parent.rect)
class State(object): """ This is a prototype class for States. All states should inherit from it. No direct instances of this class should be created. Update must be overloaded in the child class. Overview of Methods: startup - Called when added to the state stack resume - Called each time state is updated for first time update - Called each frame while state is active process_event - Called when there is a new input event pause - Called when state is no longer active shutdown - Called before state is destroyed :ivar client: tuxemon.core.session.Client :cvar force_draw: If True, state will never be skipped in drawing phase :cvar rect: Area of the screen will be drawn on """ __metaclass__ = ABCMeta rect = Rect((0, 0), prepare.SCREEN_SIZE) transparent = False # ignore all background/borders force_draw = False # draw even if completely under another state def __init__(self, client): """ Do not override this unless there is a special need. All init for the State, loading of config, images, etc should be done in State.startup or State.resume, not here. :param tuxemon.core.client.Client client: State Manager / Game Client :returns: None """ self.client = client self.start_time = 0.0 self.current_time = 0.0 self.animations = pygame.sprite.Group() # only animations and tasks self.sprites = SpriteGroup() # all sprites that draw on the screen @property def name(self): return self.__class__.__name__ def load_sprite(self, filename, **kwargs): """ Load a sprite and add it to this state kwargs can be any value used by Rect, or layer :param filename: filename, relative to the resources folder :type filename: String :param kwargs: Keyword arguments to pass to the Rect constructor :returns: tuxemon.core.sprite.Sprite """ layer = kwargs.pop('layer', 0) sprite = graphics.load_sprite(filename, **kwargs) self.sprites.add(sprite, layer=layer) return sprite def animate(self, *targets, **kwargs): """ Animate something in this state Animations are processed even while state is inactive :param targets: targets of the Animation :type targets: any :param kwargs: Attributes and their final value :returns: tuxemon.core.animation.Animation """ ani = Animation(*targets, **kwargs) self.animations.add(ani) return ani def task(self, *args, **kwargs): """ Create a task for this state Tasks are processed even while state is inactive If you want to pass positional arguments, use functools.partial :param args: function to be called :param kwargs: kwargs passed to the function :returns: tuxemon.core.animation.Task """ task = Task(*args, **kwargs) self.animations.add(task) return task def remove_animations_of(self, target): """ Given and object, remove any animations that it is used with :param target: any :returns: None """ remove_animations_of(target, self.animations) def process_event(self, event): """ Handles player input events. This function is only called when the player provides input such as pressing a key or clicking the mouse. Since this is part of a chain of event handlers, the return value from this method becomes input for the next one. Returning None signifies that this method has dealt with an event and wants it exclusively. Return the event and others can use it as well. You should return None if you have handled input here. :type event: tuxemon.core.input.PlayerInput :rtype: Optional[core.input.PlayerInput] """ return event def update(self, time_delta): """ Time update function for state. Must be overloaded in children. :param time_delta: amount of time in fractional seconds since last update :type time_delta: Float :returns: None :rtype: None """ self.animations.update(time_delta) def draw(self, surface): """ Render the state to the surface passed. Must be overloaded in children Do not change the state of any game entities. Every draw should be the same for a given game time. Any game changes should be done during update. :param surface: Surface to be rendered onto :type surface: pygame.Surface :returns: None :rtype: None """ pass def startup(self, **kwargs): """ Called when scene is added to State Stack This will be called: * after state is pushed and before next update * just once during the life of a state Example uses: loading images, configuration, sounds. :param kwargs: Configuration options :returns: None :rtype: None """ pass def resume(self): """ Called before update when state is newly in focus This will be called: * before update after being pushed to the stack * before update after state has been paused After being called, state will begin to receive player input Could be called several times over lifetime of state Example uses: starting music, open menu, starting animations, timers, etc :returns: None :rtype: None """ pass def pause(self): """ Called when state is pushed back in the stack, allowed to pause This will be called: * after update when state is pushed back * before being shutdown After being called, state will no longer receive player input Could be called several times over lifetime of state Example uses: stopping music, sounds, fading out, making state graphics dim :returns: None :rtype: None """ pass def shutdown(self): """ Called when state is removed from stack and will be destroyed This will be called: * after update when state is popped Make sure to release any references to objects that may cause cyclical dependencies. :returns: None :rtype: None """ pass
def update_image(self): rect = Rect((0, 0), self._rect.size) surface = pygame.Surface(rect.size, pygame.SRCALPHA) self._original_image = surface self._image = surface self._draw(surface, rect)
class SpriteGroup(pygame.sprite.LayeredUpdates): """ Sane variation of a pygame sprite group Features: * Supports Layers * Supports Index / Slice * Supports skipping sprites without an image * Supports sprites with visible flag * Get bounding rect of all children Variations from standard group: * SpriteGroup.add no longer accepts a sequence, use SpriteGroup.extend """ _init_rect = Rect(0, 0, 0, 0) def __init__(self, *args, **kwargs): self._spritelayers = dict() self._spritelist = list() pygame.sprite.AbstractGroup.__init__(self) self._default_layer = kwargs.get('default_layer', 0) def __nonzero__(self): return bool(self._spritelist) def __getitem__(self, item): # patch in indexing / slicing support return self._spritelist.__getitem__(item) def draw(self, surface): spritedict = self.spritedict surface_blit = surface.blit dirty = self.lostsprites self.lostsprites = [] dirty_append = dirty.append for s in self.sprites(): if getattr(s, "image", None) is None: continue if not getattr(s, 'visible', True): continue if isinstance(s.image, PygAnimation): s.image.blit(surface, s.rect) continue r = spritedict[s] newrect = surface_blit(s.image, s.rect) if r: if newrect.colliderect(r): dirty_append(newrect.union(r)) else: dirty_append(newrect) dirty_append(r) else: dirty_append(newrect) spritedict[s] = newrect return dirty def extend(self, sprites, **kwargs): """ Add a sequence of sprites to the SpriteGroup :param sprites: Sequence (list, set, etc) :param kwargs: :returns: None """ if '_index' in kwargs.keys(): raise KeyError for index, sprite in enumerate(sprites): kwargs['_index'] = index self.add(sprite, **kwargs) def add(self, sprite, **kwargs): """ Add a sprite to group. do not pass a sequence or iterator LayeredUpdates.add(*sprites, **kwargs): return None If the sprite you add has an attribute _layer, then that layer will be used. If **kwarg contains 'layer', then the passed sprites will be added to that layer (overriding the sprite._layer attribute). If neither the sprite nor **kwarg has a 'layer', then the default layer is used to add the sprites. """ layer = kwargs.get('layer') if isinstance(sprite, pygame.sprite.Sprite): if not self.has_internal(sprite): self.add_internal(sprite, layer) sprite.add_internal(self) else: raise TypeError def calc_bounding_rect(self): """A rect object that contains all sprites of this group """ sprites = self.sprites() if not sprites: return self.rect elif len(sprites) == 1: return Rect(sprites[0].rect) else: return sprites[0].rect.unionall([s.rect for s in sprites[1:]])
def calc_menu_items_rect(self): width, height = self.rect.size left = width // 2.25 top = height // 12 width /= 2 return Rect(left, top, width, height - top * 2)
def scale_area(area): return Rect(tools.scale_sequence(area))
def get_rect(self): # Returns a Rect object for this animation object. # The top and left will be set to 0, 0, and the width and height # will be set to what is returned by getMaxSize(). maxWidth, maxHeight = self.getMaxSize() return Rect(0, 0, maxWidth, maxHeight)