Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

merge with new lib2d

  • Loading branch information...
commit 559d4197d1c21bc032d9f9585d5f87e5c0844a1a 1 parent d8e90e5
@bitcraft authored
Showing with 8,442 additions and 0 deletions.
  1. +405 −0 lib/ui.py
  2. BIN  lib2d/.DS_Store
  3. +45 −0 lib2d/README.md
  4. +192 −0 lib2d/animation.py
  5. +493 −0 lib2d/area.py
  6. +175 −0 lib2d/avatar.py
  7. +107 −0 lib2d/banner.py
  8. +222 −0 lib2d/bbox.py
  9. +69 −0 lib2d/buttons.py
  10. +367 −0 lib2d/context.py
  11. +166 −0 lib2d/cursor.py
  12. +198 −0 lib2d/draw.py
  13. +36 −0 lib2d/encodings.py
  14. +119 −0 lib2d/fov.py
  15. +193 −0 lib2d/fsa.py
  16. 0  lib2d/fsa/__init__.py
  17. +5 −0 lib2d/fsa/flags.py
  18. +245 −0 lib2d/fsa/fsa.py
  19. +20 −0 lib2d/game.py
  20. +145 −0 lib2d/gfx.py
  21. +180 −0 lib2d/ikguy.py
  22. +53 −0 lib2d/image.py
  23. +154 −0 lib2d/los.py
  24. BIN  lib2d/mouse/.DS_Store
  25. +1 −0  lib2d/mouse/__init__.py
  26. +1 −0  lib2d/mouse/tools/__init__.py
  27. +19 −0 lib2d/mouse/tools/mousetool.py
  28. +149 −0 lib2d/mouse/tools/pantool.py
  29. +1 −0  lib2d/net/__init__.py
  30. +51 −0 lib2d/net/communicate.py
  31. +11 −0 lib2d/net/errors.py
  32. +136 −0 lib2d/net/netbase.py
  33. +158 −0 lib2d/net/socket-2.py
  34. +36 −0 lib2d/net/tcp.py
  35. +349 −0 lib2d/objects.py
  36. +246 −0 lib2d/packets.py
  37. +260 −0 lib2d/playerinput.py
  38. +305 −0 lib2d/quadtree.py
  39. +252 −0 lib2d/rect.py
  40. +173 −0 lib2d/res.py
  41. BIN  lib2d/server/.DS_Store
  42. +36 −0 lib2d/server/udp.py
  43. +17 −0 lib2d/signals.py
  44. +30 −0 lib2d/sound.py
  45. +73 −0 lib2d/temporal.py
  46. +438 −0 lib2d/tilemap.py
  47. BIN  lib2d/ui/.DS_Store
  48. +4 −0 lib2d/ui/__init__.py
  49. +66 −0 lib2d/ui/element.py
  50. +29 −0 lib2d/ui/frame.py
  51. +198 −0 lib2d/ui/menu.py
  52. +65 −0 lib2d/ui/packer.py
  53. +334 −0 lib2d/ui/ui.py
  54. +38 −0 lib2d/utils.py
  55. +403 −0 lib2d/vec.py
  56. +21 −0 lib2d/zone.py
  57. +11 −0 pytmx/constants.py
  58. +618 −0 pytmx/pytmx.py
  59. +251 −0 pytmx/utils.py
  60. +1 −0  startserver.sh
  61. +3 −0  updatedeps.sh
  62. +69 −0 utilities/mk_border.py
View
405 lib/ui.py
@@ -0,0 +1,405 @@
+"""
+this module strives to NOT be a replacement for more fucntional gui toolkits.
+this is a bare-bones simple gui toolkit for mouse use only.
+"""
+
+class UIElement(object):
+ def __init__(self, parent):
+ self.parent = parent
+ self.enabled = False
+
+
+ def getUI(self):
+ parent = self.parent
+ while not isinstance(parent, UserInterface):
+ parent = parent.parent
+ return parent
+
+
+ def setParent(self, parent):
+ self.parent = parent
+
+
+ def handle_commandlist(self, cmdlist):
+ pass
+
+
+class Pane(object):
+ """
+ object capable of interacting with the mouse
+ """
+ pass
+
+
+class MouseTool(object):
+ toole_image = None
+ cursor_image = None
+
+
+ def onClick(self, pane, point, button):
+ pass
+
+
+ def onDrag(self, pane, point, button, origin):
+ pass
+
+
+ def onHover(self, pane, point):
+ pass
+
+
+class GraphicIcon(object):
+ """
+ Clickable Icon
+
+ TODO: cache the image so it isn't duplicated in memory
+ """
+
+ def __init__(self, filename, func, arg=[], kwarg={}, uses=1):
+ self.filename = filename
+ self.func = (func, arg, kwarg)
+ self.uses = uses
+ self.image = None
+ self.load()
+
+
+ def load(self):
+ if self.image == None:
+ self.image = res.loadImage(self.filename)
+ self.enabled = True
+
+ def unload(self):
+ self.image = None
+
+ def onClick(self, point, button, origin):
+ if self.uses > 0 and self.enabled:
+ self.func[0](*self.func[1], **self.func[2])
+ self.uses -= 1
+ if self.uses == 0:
+ self.unload()
+ self.func = None
+
+ def onDrag(self, point, button, origin):
+ pass
+
+ def onHover(self, point):
+ pass
+
+ def draw(self, surface, pos):
+ surface.blit(self.image, pos)
+
+
+class RoundMenu(UIElement):
+ """
+ menu that 'explodes' from a center point and presents a group of menu
+ options as a circle of GraphicIcon objects
+ """
+
+ def __init__(self, items):
+ self.items = []
+ for i in items:
+ i.load()
+ i.enabled = False
+
+
+ def open(self):
+ """ start the animation of the menu """
+ self.enabled = True
+ for i in self.items:
+ i.enabled = False
+
+ def draw(self, surface, rect):
+ for i, item in enumerate(self.items):
+ x = i*32
+ y = 10
+ item.draw(surface, (x,y))
+
+
+class PanTool(MouseTool, UIElement):
+ def __init__(self, parent):
+ MouseTool.__init__(self)
+ UIElement.__init__(self, parent)
+ self.drag_origin = None
+
+
+ def load(self):
+ self.tool_image = res.loadImage("pantool.png")
+
+
+ def onClick(self, pane, point, button):
+ self.drag_origin = None
+ m = testMenu()
+ self.getUI().addElement(m)
+ self.getUI().setRect(m, (pos, (32, 32)))
+ m.open()
+
+
+ def onDrag(self, pane, point, button, origin):
+ if isinstance(pane, ViewPort):
+ if self.drag_origin == None:
+ x, y = pane.rect.width / 2, pane.rect.height / 2
+ self.drag_origin = pane.camera.surfaceToWorld((x, y))
+
+ x, y, z = self.drag_origin
+ dy, dx = point[0] - origin[0], point[1] - origin[1]
+ pane.camera.center((x-dx, y-dy, z))
+
+
+class UserInterface(object):
+ pass
+
+
+class StandardUI(UserInterface):
+ """
+ Standard UI controls mouse interaction, drawing the maps, and UI
+ elements such as menus
+ """
+
+ height = 20
+ color = pygame.Color(196, 207, 214)
+ transparent = pygame.Color(1,2,3)
+
+ background = (109, 109, 109)
+ foreground = (0, 0, 0)
+
+
+ def __init__(self):
+ self.blank = True
+ self.elements = []
+
+
+ def addElement(self, other):
+ other.setParent(self)
+ self.elements.append(other)
+
+
+ def buildInterface(self, rect):
+ """
+ pass the rect of the screen surface and the interface will be
+ proportioned correctly.
+ """
+
+ self.msgFont = pygame.font.Font((res.fontPath("volter.ttf")), 9)
+ self.border = gui.GraphicBox("dialog2-h.png", hollow=True)
+ self.borderFilled = gui.GraphicBox("dialog2.png")
+ self.paneManager = None
+
+ x, y, w, h = rect
+ w = x+int((w*.30))
+ s = pygame.Surface((w, self.height))
+ s.fill(self.transparent)
+ s.set_colorkey(self.transparent, pygame.RLEACCEL)
+
+ pygame.draw.circle(s, (128,128,128), (self.height, 1), self.height)
+ pygame.draw.rect(s, (128, 128, 128), (self.height, 1, w, self.height))
+
+ pygame.draw.circle(s, self.color, (self.height+1, 0), self.height)
+ pygame.draw.rect(s, self.color, (self.height+1, 0, w-self.height, self.height))
+
+ self.buttonbar = s
+
+
+ def draw(self, surface):
+ print self.elements
+ for e in self.elements:
+ e.draw(surface)
+
+
+ if self.blank:
+ self.paneManager = PaneManager(self)
+ self.blank = False
+
+ x, y, w, h = surface.get_rect()
+ back_width = x+int((w*.70))
+ self.buildInterface((x, y, w, h))
+ surface.blit(self.buttonbar, (x+int(w*.70)+1,0))
+
+
+ def handle_commandlist(self, cmdlist):
+ for e in self.elements:
+ e.handle_commandlist(cmdlist)
+
+
+ def update(self, time):
+ [ i.update(time) for i in self.elements ]
+
+
+def testMenu():
+ def func():
+ pass
+
+ g = GraphicIcon("grasp.png", func)
+ m = RoundMenu([g, g, g, g])
+ return m
+
+
+class PaneManager(UIElement):
+ """
+ Handles panes and mouse tools
+ """
+
+ drag_sensitivity = 2
+
+
+ def __init__(self, parent):
+ UIElement.__init__(self, parent)
+ self.panes = []
+ self.areas = []
+ self.rect = None
+
+ self.tools = [ PanTool(self) ]
+
+ for tool in self.tools:
+ tool.load()
+
+ self.mouse_tool = self.tools[0]
+
+ #mouse hack
+ self.drag_origin = None
+ self.drag_vp = None
+
+
+ def addArea(self, area):
+ if area not in self.areas:
+ self.areas.append(area)
+ area.load()
+
+ # load the children
+ for child in area.getChildren():
+ child.load()
+
+ # load sounds from area
+ for filename in area.soundFiles:
+ SoundMan.loadSound(filename)
+
+ def _resize(self, rect):
+ """ resize the rects for the panes """
+
+ self.rect = pygame.Rect(rect)
+
+ if len(self.panes) == 1:
+ self.panes[0].setRect(rect)
+
+ elif len(self.panes) == 2:
+ w, h = self.rect.size
+ self.panes[0].setRect((0,0,w,h/2))
+ self.panes[1].setRect((0,h/2,w,h/2))
+
+ elif len(self.panes) == 3:
+ w = self.rect.width / 2
+ h = self.rect.height / 2
+ rect = self.rect.copy()
+ # WARNING!!!! 3 panes does not work
+
+ elif len(self.panes) == 4:
+ w = self.rect.width / 2
+ h = self.rect.height / 2
+ self.panes[0].setRect((0,0,w,h))
+ self.panes[1].setRect((w,0,w,h))
+ self.panes[2].setRect((0,h,w,h))
+ self.panes[3].setRect((w,h,w,h))
+
+
+ def new(self, area, follow=None):
+ if area not in self.areas:
+ self.addArea(area)
+
+ vp = ViewPort(area)
+ self.panes.append(vp)
+
+ # this will cause the rects to be recalculated next draw
+ self.rect = None
+
+
+ def draw(self, surface):
+ rect = surface.get_rect()
+ if not self.rect == rect:
+ self._resize(rect)
+
+ dirty = []
+ [ dirty.extend(pane.draw(surface, pane.rect)) for pane in self.panes ]
+
+ #for rect in self.paneManager.getRects():
+ # self.border.draw(surface, rect.inflate(6,6))
+
+ return dirty
+
+
+ def getRects(self):
+ """ return a list of rects that split the viewports """
+ return [ pane.rect for pane in self.panes ]
+
+
+ def update(self, time):
+ [ vp.update(time) for vp in self.panes ]
+ [ area.update(time) for area in self.areas ]
+
+
+ def findViewport(self, point):
+ for vp in self.panes:
+ if vp.rect.collidepoint(point):
+ return vp
+
+
+ # handles all mouse interaction
+ def handle_commandlist(self, cmdlist):
+ for cls, cmd, arg in cmdlist:
+ if cmd == CLICK1:
+ state, pos = arg
+ vp = self.findViewport(pos)
+ if vp:
+ pos = Vec2d(pos[0] - vp.rect.left, pos[1] - vp.rect.top)
+ if state == BUTTONDOWN:
+ self.drag_origin = pos
+ self.drag_vp = vp
+ self.mouse_tool.onClick(vp, pos, 1)
+
+ elif state == BUTTONHELD:
+ d = abs(sum(pos - self.drag_origin))
+ if vp == self.drag_vp and d > self.drag_sensitivity:
+ self.mouse_tool.onDrag(vp, pos, 1, self.drag_origin)
+
+ elif cmd == CLICK2:
+ pass
+ elif cmd == MOUSEPOS:
+ vp = self.findViewport(arg)
+ if vp:
+ pos = (arg[0] - vp.rect.left, arg[1] - vp.rect.top)
+ self.mouse_tool.onHover(vp, pos)
+
+
+class ViewPort(Pane):
+ """
+ the ViewPort is a Pane that draws a the area to the screen (or other
+ surface)
+ """
+
+ def __init__(self, area):
+ self.area = area
+ self.rect = None
+ self.camera = None
+
+
+ def setRect(self, rect):
+ self._resize(rect)
+
+
+ def _resize(self, rect):
+ self.rect = pygame.Rect(rect)
+ self.camera = LevelCamera(self.area, self.rect)
+
+
+ def draw(self, surface, rect):
+ if not self.rect == rect:
+ self._resize(rect)
+ self.camera.draw(surface, self.rect)
+ return self.rect
+
+ else:
+ return self.camera.draw(surface, self.rect)
+
+
+ def update(self, time):
+ self.camera.update(time)
+
+
View
BIN  lib2d/.DS_Store
Binary file not shown
View
45 lib2d/README.md
@@ -0,0 +1,45 @@
+Lib2d is a game engine that I have been developing to create PyWeek games.
+
+
+----- this is an experimental version for adventure games -----
+
+What it does:
+ State (context) management
+ Animated Sprites with multiple animations and automatic flipping
+ TMX level importing
+ Integrated Tiled Editing
+ Fast, efficient tilemap rendering with parallax support
+ Basic GUI features
+ Integrated Chipmunk dynamics (pymunk)
+ Advanced AI (pygoap)
+ Integrated animation and input handling (fsa.py)
+ Simplified save game support
+ Dialogs
+
+
+Game Structure Overview:
+ Map is designed in Tiled with special layer (controlset.tsx)
+ Game objects are created in world.py and assigned GUID control numbers
+ The 'world' data structure is pickleable and becomes the save game
+ When engine is started, the world data structure can be used to play
+
+
+Control:
+ Lib2d wraps all pygame events for player input so they can be remapped.
+ It also make keyboard and joystick controls interchangeable at runtime.
+ Using fsa.py, instead of coding the behavour of the controls, you can
+ use a finite state machine to define how the character changes states.
+
+Tilemap:
+ The tilemap uses a special surface that gets updated in the background
+ (or by another thread). It performs very well when scrolling. Large TMX
+ maps can be used since only the visible portions of the map are rendered.
+
+
+Physics:
+ The library uses pymunk and cannot work without it. The obvious benefits
+ are a fast physics system (not more colliding rects!) and it has good
+ integration with the TMX loader.
+
+ You can define your walls in tiles, and they will get loaded into the
+ Chipmunk engine automatically.
View
192 lib2d/animation.py
@@ -0,0 +1,192 @@
+from objects import GameObject
+from image import Image, ImageTile
+from sound import Sound
+import res
+
+from collections import namedtuple
+import math, itertools
+
+
+pi2 = math.pi * 2
+
+box = namedtuple('Box', 'width height')
+
+
+"""
+animations may be used by a class that does not need the images, but may need
+to know when an animation finishes, where it is at, etc (such as a server).
+"""
+
+def calcFrameSize(filename, frames, directions):
+ """
+ load the images for the animation and set the size of each frame
+ """
+
+ width, height = res.loadImage(filename, 0,0,1).get_size()
+ return (width / frames, height / directions)
+
+
+class Animation(GameObject):
+ """
+ Animation is a collection of frames with a few control variables and useful
+ methods to control it.
+
+ Animations can store multiple directions and are picklable.
+
+ each set of animation added will count as a seperate direction.
+ 1 animation = no rotations
+ 2 animations = left and right (or up and down)
+ 4 animations = left, right, up, down
+ 6 animations = up, down, nw, ne, sw, se (for hex maps)
+ and so on.
+
+ The animation loader expects the image to be in a specific format.
+ Loader only supports animations where each frame is the same size.
+
+ TODO: implement some sort of timing, rather than relying on frames
+ """
+
+ def __init__(self,name,image,frames,directions=1,timing=100,sound=None):
+ GameObject.__init__(self)
+
+ assert isinstance(image, Image)
+
+ #if sound:
+ # assert(sound, Sound)
+
+
+ self.name = name
+ self.image = image
+ self.images = None
+ self.directions = directions
+ self.frames = frames
+ self.timing = timing
+
+ # TODO: some checking to make sure inputs are the correct length
+ if isinstance(self.frames, int):
+ self.frames = tuple(range(0, self.frames))
+ else:
+ self.frames = tuple(self.frames)
+
+ self.real_frames = len(set(self.frames))
+
+ if isinstance(self.timing, int):
+ self.timing = tuple([self.timing] * len(self.frames))
+ else:
+ self.timing = tuple(self.timing)
+
+
+ def __iter__(self):
+ return itertools.izip(self.timing, self.frames)
+
+
+ def returnNew(self):
+ return self
+
+
+ def load(self, force=False):
+ """
+ load the images for use with pygame
+ returns a new Animation Object
+ """
+
+ if (self.images is not None) and (not force):
+ return
+
+ image = self.image.load()
+
+ iw, ih = image.get_size()
+ tw = iw / self.real_frames
+ th = ih / self.directions
+ self.images = [None] * (self.directions * self.real_frames)
+
+ d = 0
+ for y in range(0, ih, th):
+ #if d == ih/th: d = 0
+ for x in range(0, iw, tw):
+ try:
+ frame = image.subsurface((x, y, tw, th))
+ except ValueError as e:
+ msg = "Invalid tiles selected for image {}"
+ raise ValueError, msg.format(self.image.filename)
+ self.images[(x/tw)+d*self.real_frames] = frame
+ d += 1
+
+
+ def unload(self):
+ self.images = []
+
+
+ def getImage(self, number, direction=0):
+ """
+ return the frame by number with the correct image for the direction
+ direction should be expressed in radians
+ """
+
+ if self.images == []:
+ raise Exception, "Avatar hasn't loaded images yet"
+
+ if direction < 0:
+ direction = pi + (pi - abs(direction))
+ d = int(math.ceil(direction / pi2 * (self.directions - 1)))
+
+ try:
+ return self.images[number+d*self.real_frames]
+ except IndexError:
+ msg="{} cannot find image for animation ({}/{})"
+ raise IndexError, msg.format(self, number+d*self.real_frames,
+ len(self.images))
+
+
+ def __repr__(self):
+ return "<Animation %s: \"%s\">" % (id(self), self.name)
+
+
+class StaticAnimation(Animation):
+ """
+ Animation that only supports one frame
+ Immutable
+ """
+
+ def __init__(self, name, image):
+ GameObject.__init__(self)
+
+ try:
+ assert isinstance(image, Image) or isinstance(image, ImageTile)
+ except AssertionError:
+ print name, image
+ raise
+
+ self.image = image
+ self.name = name
+ self.frames = [0]
+ self.timing = [-1]
+
+
+ def load(self):
+ """
+ load the images for use with pygame
+ """
+
+ self.image = self.image.load()
+ print "loading, static", self, self.image
+
+ def returnNew(self):
+ return self
+
+
+ def unload(self):
+ self.image = None
+
+
+ def getImage(self, number, direction=0):
+ """
+ return the frame by number with the correct image for the direction
+ direction should be expressed in radians
+ """
+
+ if self.image is None:
+ raise Exception, "Avatar hasn't loaded images yet"
+
+ return self.image
+
View
493 lib2d/area.py
@@ -0,0 +1,493 @@
+import res
+from pathfinding.astar import Node
+from objects import GameObject
+from pygame import Rect
+from pathfinding import astar
+from lib2d.signals import *
+from lib2d.objects import AvatarObject
+from lib2d.zone import Zone
+import math
+
+import pymunk
+
+cardinalDirs = {"north": math.pi*1.5, "east": 0.0, "south": math.pi/2, "west": math.pi}
+
+
+
+class PathfindingSentinel(object):
+ """
+ this object watches a body move and will adjust the movement as needed
+ used to move a body when a path is set for it to move towards
+ """
+
+ def __init__(self, body, path):
+ self.body = body
+ self.path = path
+ self.dx = 0
+ self.dy = 0
+
+ def update(self, time):
+ if worldToTile(bbox.origin) == self.path[-1]:
+ pos = path.pop()
+ theta = math.atan2(self.destination[1], self.destination[0])
+ self.destination = self.position + self.destination
+ self.dx = self.speed * cos(theta)
+ self.dy = self.speed * sin(theta)
+
+ self.area.movePosition(self.body, (seldf.dx, self.dy, 0))
+
+
+class AbstractArea(GameObject):
+ pass
+
+
+class Sound(object):
+ """
+ Class that manages how sounds are played and emitted from the area
+ """
+
+ def __init__(self, filename, ttl):
+ self.filename = filename
+ self.ttl = ttl
+ self._done = 0
+ self.timer = 0
+
+ def update(self, time):
+ if self.timer >= self.ttl:
+ self._done = 1
+ else:
+ self.timer += time
+
+ @property
+ def done(self):
+ return self._done
+
+
+
+class AdventureMixin(object):
+ """
+ Mixin class that contains methods to translate world coordinates to screen
+ or surface coordinates.
+ The methods will translate coordinates of the tiled map
+
+ TODO: manipulate the tmx loader to swap the axis
+ """
+
+ def tileToWorld(self, (x, y, z)):
+ xx = int(x) * self.tmxdata.tileheight
+ yy = int(y) * self.tmxdata.tilewidth
+ return xx, yy, z
+
+
+ def pixelToWorld(self, (x, y)):
+ return Vec3d(y, x, 0)
+
+
+ def toRect(self, bbox):
+ # return a rect that represents the object on the xy plane
+ # currently this is used for geometry collision detection
+ return Rect((bbox.x, bbox.y, bbox.depth, bbox.width))
+
+
+ def worldToPixel(self, (x, y, z)):
+ return Vec2d((y, x))
+
+
+ def worldToTile(self, (x, y, z)):
+ xx = int(x) / self.tmxdata.tilewidth
+ yy = int(y) / self.tmxdata.tileheight
+ zz = 0
+ return xx, yy, zz
+
+
+ def setForce(self, body, (x, y, z)):
+ body.acc = Vec2d(x, y)
+
+
+class PlatformMixin(object):
+ """
+ Mixin class is suitable for platformer games
+ """
+
+ def defaultPosition(self):
+ return 0,0
+
+
+ def translate(self, (x, y, z)):
+ return y, z
+
+
+ def toRect(self, bbox):
+ # return a rect that represents the object on the zy plane
+ return Rect((bbox.y, bbox.z+bbox.height, bbox.width, bbox.height))
+
+
+ """
+ the underlying physics 'engine' is only capable of calculating 2 axises.
+ for playformer type games, we use the zy plane for calculations
+ """
+
+ def grounded(self, body):
+ try:
+ return self._grounded[body]
+ except:
+ return False
+
+
+ def applyForce(self, body, (x, y, z)):
+ body.acc += Vec2d(y, z)
+
+
+ def worldToPixel(self, (x, y)):
+ return (x*self.scaling, y*self.scaling)
+
+
+ def worldToTile(self, (x, y, z)):
+ xx = int(x) / self.tmxdata.tilewidth
+ yy = int(y) / self.tmxdata.tileheight
+ zz = 0
+ return xx, yy, zz
+
+
+"""
+ G R O U P S
+
+
+ 1: LEVEL GEOMETRY
+ 2: ZONES
+
+
+
+ T Y P E S
+
+ 1: THE PLAYER (AS SET BY THE LEVEL STATE)
+ 2: ZONES
+
+"""
+
+class PlatformArea(AbstractArea, PlatformMixin):
+ """
+ 2D environment for things to live in.
+ Includes basic pathfinding, collision detection, among other things.
+
+ Physics simulation is handled by pymunk/chipmunk 2d physics.
+
+ Bodies can exits in layers, just like maps. since the y values can
+ vary, when testing for collisions the y value will be truncated and tested
+ against the quadtree that is closest. if there is no quadtree, no
+ collision testing will be done.
+
+ There are a few hacks to be aware of:
+ bodies move in 3d space, but level geometry is 2d space
+ when using pygame rects, the y value maps to the z value in the area
+
+ a word on the coordinate system:
+ coordinates are 'right handed'
+ x axis moves toward viewer
+ y axis move left right
+ z axis is height
+
+ Expects to load a specially formatted TMX map created with Tiled.
+ Layers:
+ Control Tiles
+ Upper Partial Tiles
+ Lower Partial Tiles
+ Lower Full Tiles
+
+ Contains a very basic discrete collision system.
+
+ The control layer is where objects and boundries are placed. It will not
+ be rendered. Your map must not have any spaces that are open. Each space
+ must have a tile in it. Blank spaces will not be rendered properly and
+ will leave annoying trails on the screen.
+
+ The control layer must be created with the utility included with lib2d. It
+ contains metadata that lib2d can use to layout and position objects
+ correctly.
+
+ REWRITE: FUNCTIONS HERE SHOULD NOT CHANGE STATE
+
+ Handle mapping of physics bodies to game entities
+
+
+ NOTE: some of the code is specific for maps from the tmxloader
+ """
+
+ gravity = (0, 50)
+
+
+ def defaultSize(self):
+ # TODO: this cannot be hardcoded!
+ return (10, 8)
+
+
+ def __init__(self):
+ AbstractArea.__init__(self)
+ self.subscribers = []
+
+ self.exits = {}
+ self.messages = []
+ self.tmxdata = None
+ self.mappath = None
+ self.sounds = []
+ self.soundFiles = []
+ self.inUpdate = False
+ self.drawables = [] # HAAAAKCCCCKCK
+ self.changedAvatars = True #hack
+ self.time = 0
+ self.music_pos = 0
+ self._addQueue = []
+ self._removeQueue = []
+ self._addQueue = []
+
+ self.flashes = []
+ self.inUpdate = False
+ self._removeQueue = []
+
+ # temporary storage of physics stuff
+ self.temp_positions = {}
+
+ # internal physics stuff
+ self.geometry = {}
+ self.shapes = {}
+ self.bodies = {}
+ self.physicsgroup = None
+ self.extent = None # absolute boundaries of the area
+ self.scaling = 1.0 # MUST BE FLOAT
+
+
+ def load(self):
+ def toChipPoly(rect):
+ return (rect.topleft, rect.topright,
+ rect.bottomright, rect.bottomleft)
+
+
+ import pytmx
+
+ self.tmxdata = pytmx.tmxloader.load_pygame(
+ self.mappath, force_colorkey=(128,128,0))
+
+ # get sounds from tiles
+ for i, layer in enumerate(self.tmxdata.tilelayers):
+ props = self.tmxdata.getTilePropertiesByLayer(i)
+ for gid, tileProp in props:
+ for key, value in tileProp.items():
+ if key[4:].lower() == "sound":
+ self.soundFiles.append(value)
+
+ # get sounds from objects
+ for i in [ i for i in self.getChildren() if i.sounds ]:
+ self.soundFiles.extend(i.sounds)
+
+ self.space = pymunk.Space()
+ self.space.gravity = self.gravity
+
+ # transform the saved geometry into chipmunk geometry and add it
+ # bug: will not work with multiple layers
+ geometry = []
+ for layer, rects in self.geometry.items():
+ for rect in rects:
+ shape = pymunk.Poly(self.space.static_body, toChipPoly(rect))
+ shape.friction = 1.0
+ shape.group = 1
+ #shape.layers = layer
+ geometry.append(shape)
+
+ self.space.add(geometry)
+
+ # dont worry about setting the player group, that will be set by the
+ # levelstate
+ self.groups = 2
+
+ # just assume we have the correct types under us
+ for child in self._children:
+ if isinstance(child, AvatarObject):
+ if child.physics:
+ body = pymunk.Body(5, pymunk.inf)
+ body.position = self.temp_positions[child]
+ body.friction = 1.0
+ shape = pymunk.Poly.create_box(body, size=child.size[:2])
+ self.bodies[child] = body
+ self.shapes[child] = shape
+ self.space.add(body, shape)
+
+ else:
+ rect = Rect(self.temp_positions[child], child.size[:2])
+ shape = pymunk.Poly(self.space.static_body, toChipPoly(rect))
+ shape.friction = 1.0
+ self.shapes[child] = shape
+ self.space.add(shape)
+
+ elif isinstance(child, Zone):
+ points = toChipPoly(child.extent)
+ shape = pymunk.Poly(self.space.static_body, points)
+ shape.collision_type = 2
+ self.shapes[child] = shape
+ self.space.add(shape)
+
+
+ def unload(self):
+ self.bodies = {}
+ self.shapes = {}
+ self.physicsgroup = None
+ self.space = None
+
+
+ def add(self, child, pos=None):
+ AbstractArea.add(self, child)
+
+ # don't do anything with the physics engine here
+ # handle it in load(), where the area is prepped for use
+
+ if isinstance(child, AvatarObject):
+ if pos is None:
+ pos = self.defaultPosition()
+ else:
+ pos = self.translate(pos)
+
+ self.temp_positions[child] = pos
+ self.changedAvatars = True
+
+
+ def remove(self, entity):
+ if self.inUpdate:
+ self._removeQueue.append(entity)
+ return
+
+ AbstractArea.remove(self, entity)
+ del self.bodies[entity]
+ self.changedAvatars = True
+
+ # hack
+ try:
+ self.drawables.remove(entity)
+ except (ValueError, IndexError):
+ pass
+
+
+ def getBody(self, entity):
+ return self.bodies[entity]
+
+
+ def setLayerGeometry(self, layer, rects):
+ """
+ set the layer's geometry. expects a list of rects.
+ """
+
+ self.geometry[layer] = rects
+
+
+ def pathfind(self, start, destination):
+ """Pathfinding for the world. Destinations are 'snapped' to tiles.
+ """
+
+ def NodeFactory(pos):
+ x, y = pos[:2]
+ l = 0
+ return Node((x, y))
+
+ try:
+ if self.tmxdata.getTileGID(x, y, l) == 0:
+ node = Node((x, y))
+ else:
+ return None
+ except:
+ return None
+ else:
+ return node
+
+ start = self.worldToTile(start)
+ destination = self.worldToTile(destination)
+ path = astar.search(start, destination, NodeFactory)
+ return path
+
+
+ def emitText(self, text, pos=None, entity=None):
+ if pos==entity==None:
+ raise ValueError, "emitText requires a position or entity"
+
+ if entity:
+ pos = self.bodies[entity].bbox.center
+ emitText.send(sender=self, text=text, position=pos)
+ self.messages.append(text)
+
+
+ def emitSound(self, filename, pos=None, entity=None, ttl=350):
+ if pos==entity==None:
+ raise ValueError, "emitSound requires a position or entity"
+
+ self.sounds = [ s for s in self.sounds if not s.done ]
+ if filename not in [ s.filename for s in self.sounds ]:
+ self.sounds.append(Sound(filename, ttl))
+ if entity:
+ pos = self.bodies[entity].position
+ for sub in self.subscribers:
+ sub.emitSound(filename, pos)
+
+
+ def update(self, time):
+ self.inUpdate = True
+ self.time += time
+
+ [ sound.update(time) for sound in self.sounds ]
+
+ for entity, body in self.bodies.items():
+ grounding = {
+ 'normal' : pymunk.Vec2d.zero(),
+ 'penetration' : pymunk.Vec2d.zero(),
+ 'impulse' : pymunk.Vec2d.zero(),
+ 'position' : pymunk.Vec2d.zero(),
+ 'body' : None
+ }
+
+ def f(arbiter):
+ n = -arbiter.contacts[0].normal
+ if n.y > grounding['normal'].y:
+ grounding['normal'] = n
+ grounding['penetration'] = -arbiter.contacts[0].distance
+ grounding['body'] = arbiter.shapes[1].body
+ grounding['impulse'] = arbiter.total_impulse
+ grounding['position'] = arbiter.contacts[0].position
+ body.each_arbiter(f)
+ entity.avatar.update(time)
+
+ if grounding['body'] != None:
+ friction = -(body.velocity.y/0.05)/self.space.gravity.y
+
+ if grounding['body'] != None and abs(grounding['normal'].x/grounding['normal'].y) < friction:
+ entity.grounded = True
+ else:
+ entity.grounded = False
+
+ if entity.time_update:
+ entity.update(time)
+
+ self.space.step(1.0/60)
+
+ # awkward looping allowing objects to be added/removed during update
+ self.inUpdate = False
+ [ self.add(entity) for entity in self._addQueue ]
+ self._addQueue = []
+ [ self.remove(entity) for entity in self._removeQueue ]
+ self._removeQueue = []
+
+
+ def _sendBodyMove(self, body, caller, force=None):
+ position = body.bbox.origin
+ bodyAbsMove.send(sender=self, body=body, position=position, caller=caller, force=force)
+
+
+ # CLIENT API --------------
+
+
+ def subscribe(self, subscriber):
+ self.subscribers.append(subscriber)
+
+
+ def getSize(self, entity):
+ """ Return 3d size of the object """
+ return self.bodies[entity].bbox.size
+
+
+ def getBody(self, entity):
+ return self.bodies[entity]
View
175 lib2d/avatar.py
@@ -0,0 +1,175 @@
+"""
+Copyright 2010, 2011 Leif Theden
+
+
+This file is part of lib2d.
+
+lib2d 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.
+
+lib2d 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 lib2d. If not, see <http://www.gnu.org/licenses/>.
+"""
+
+
+from objects import GameObject
+import res, animation
+from pygame.transform import flip
+import itertools
+from pymunk import Vec2d
+
+
+class RenderGroup(object):
+ """
+ optional class from managing groups of avatars
+ """
+
+ pass
+
+
+class Avatar(GameObject):
+ """
+ Avatar is a sprite-like class that supports multiple animations, animation
+ controls, directions, is pickleable, and has lazy image loading.
+ update must be called occasionally for animations and rotations to work.
+ """
+
+ def __init__(self, animations, axis_offset=(0,0)):
+ GameObject.__init__(self)
+ self.axis_offset = Vec2d(axis_offset)
+
+ self.curImage = None # cached for drawing ops
+ self.curFrame = None # current frame number
+ self.curAnimation = None
+ self.animations = {}
+ self.looped = 0
+ self.timer = 0.0
+ self.ttl = 0
+ self.flip = 0
+ self.speed_mod = 1.0
+ self._prevAngle = None
+ self._changed = True
+ self.axis = Vec2d(0,0)
+
+ for animation in animations:
+ self.add(animation)
+ self.animations[animation.name] = animation
+
+ self.play(self.animations.keys()[0])
+
+
+ def _updateCache(self):
+ angle = 0
+ self.curImage = self.curAnimation.getImage(self.curFrame, angle)
+ self.axis = Vec2d(0, self.curImage.get_size()[1])
+ self.axis += self.axis_offset
+ if self.flip: self.curImage = flip(self.curImage, 1, 0)
+
+
+ @property
+ def image(self):
+ if self._changed:
+ self._updateCache()
+ return self.curImage
+
+
+ def unload(self):
+ self.curImage = None
+
+
+ def update(self, time):
+ """
+ call this as often as possible with a time. the units in the
+ animation files must match the units provided here. ie: milliseconds.
+ """
+
+ if self.ttl < 0:
+ return
+
+ self.timer += time
+
+ while (self.timer >= self.ttl):
+ self.timer -= self.ttl
+
+ try:
+ self.ttl, self.curFrame = next(self.iterator)
+ self.ttl *= self.speed_mod
+ except StopIteration:
+ if self.callback:
+ self.callback[0](*self.callback[1], **self.callback[2])
+
+ else:
+ self.changed = True
+
+ # needed to handle looping
+ if self.ttl < 0:
+ return
+
+ else:
+ self.changed = False
+
+
+ def isPlaying(self, name):
+ if isinstance(name, animation.Animation):
+ if name == self.curAnimation: return True
+ else:
+ if self.getAnimation(name) == self.curAnimation: return True
+ return False
+
+
+ def play(self, name=None, loop=-1, loop_frame=None, callback=None):
+ if isinstance(name, (animation.Animation, animation.StaticAnimation)):
+ if name == self.curAnimation: return
+ self.curAnimation = name
+ elif name is None:
+ self.play(self.animations.keys()[0])
+ else:
+ temp = self.getAnimation(name)
+ if temp == self.curAnimation: return
+ self.curAnimation = temp
+
+ self.callback = (callback, [], {})
+ self.looped = 0
+ self.timer = 0
+
+ if loop >= 0:
+ self.iterator = itertools.chain.from_iterable(
+ itertools.repeat(
+ tuple(iter(self.curAnimation)), loop + 1))
+
+ else:
+ if loop_frame:
+ self.iterator = itertools.chain(
+ iter(self.curAnimation),
+ itertools.cycle(((-1, loop_frame),))
+ )
+
+ else:
+ self.iterator = itertools.cycle(iter(self.curAnimation))
+
+ self.ttl, self.curFrame = next(self.iterator)
+ self.ttl *= self.speed_mod
+ self._changed = True
+
+
+ def getAnimation(self, name):
+ """
+ return the animation for this name.
+ """
+
+ try:
+ return self.animations[name]
+ except:
+ raise
+
+
+ def __str__(self):
+ return "<Avatar %s>" % id(self)
+
View
107 lib2d/banner.py
@@ -0,0 +1,107 @@
+"""
+Copyright 2010, 2011 Leif Theden
+
+This file is part of lib2d.
+
+lib2d 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.
+
+lib2d 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 lib2d. If not, see <http://www.gnu.org/licenses/>.
+"""
+
+import res
+import pygame
+
+
+pygame.font.init()
+
+
+"""
+simple functions wrap pygame's font module
+"""
+
+
+def loadFont(font, size):
+ if font is None:
+ return pygame.font.Font(res.defaultFont(), size)
+
+ elif isinstance(font, str):
+ return pygame.font.Font(res.fontPath(font), size)
+
+ elif isinstance(font, pygame.font.Font):
+ return font
+
+ else:
+ return font
+
+
+def TextBanner(text, font_name=None, size=12, color=[0,0,0], alpha=False, background=None):
+ font = loadFont(font_name, size)
+
+ if alpha == True:
+ return font.render(text, True, color).convert_alpha()
+ elif background is None:
+ return font.render(text, False, color).convert()
+ else:
+ image = font.render(text, True, color, bkg).convert()
+ image.set_colorkey(bkg)
+ return image
+
+
+def outlinedText(text, font_name, font_size, color=[0,0,0], alpha=False, background=None):
+ colorkey = (90,0,0)
+ border = 3
+ border_color = (0,0,0)
+
+ font = loadFont(font_name, font_size-1)
+
+ # render the font once to determine the size needed for our scratch surface
+ size = font.render(text, True, color).get_size()
+
+ # this is our scratch surface
+ if alpha:
+ s = pygame.Surface(size, pygame.SRCALPHA)
+
+ else:
+ s = pygame.Surface(size)
+ s.fill(colorkey)
+ s.set_colorkey(colorkey)
+
+ # choose the font for the banner. it must be smaller than the original size
+ inner_font = loadFont(font_name, font_size - 4)
+
+ # render the text for the border
+ border_image = inner_font.render(text, False, border_color)
+
+ # build a border for the text by blitting it in a circle
+ for x in xrange(border + 2):
+ for y in xrange(border + 2):
+ s.blit(border_image, (x, y))
+
+ # render the innner portion of the banner
+ text = inner_font.render(text, False, color)
+
+ # blit the text over the border
+ s.blit(text, (2,2))
+
+ return s
+
+
+
+def stretch2x(image):
+ w, h = image.get_size()
+ new_image = pygame.transform.scale(image, (int(w/2), h))
+ return pygame.transform.scale(new_image, (w, h)).convert(image)
+
+
+def retroOutlinedText(*arg, **kwarg):
+ return stretch2x(outlinedText(*arg, **kwarg))
+
View
222 lib2d/bbox.py
@@ -0,0 +1,222 @@
+import collections, itertools
+
+
+
+def intersect(a, b):
+ return (((a.back >= b.back and a.back < b.front) or
+ (b.back >= a.back and b.back < a.front)) and
+ ((a.left >= b.left and a.left < b.right) or
+ (b.left >= a.left and b.left < a.right)) and
+ ((a.bottom >= b.bottom and a.bottom < b.top) or
+ (b.bottom >= a.bottom and b.bottom < a.top)))
+
+# BUG: collisions on right side are not correct
+
+
+class BBox(list):
+ """
+ Rect-like class for defining area in 3d space.
+ subclassed from list to provide fast index access
+
+ Not hashable.
+
+ Many of the methods here have not been extensively tested, but most should
+ work as expected.
+ """
+
+ __slots__ = []
+
+
+ def copy(self):
+ return BBox(self)
+
+
+ def move(self, x, y, z):
+ self[0] += x
+ self[1] += y
+ self[2] += z
+
+
+ def inflate(self, x, y, z):
+ self[0] -= x / 2
+ self[1] -= y / 2
+ self[2] -= z / 2
+ self[3] += x
+ self[4] += y
+ self[5] += z
+
+
+ def scale(self, x, y, z):
+ self[0] *= x
+ self[1] *= y
+ self[2] *= z
+ self[3] *= x
+ self[4] *= y
+ self[5] *= z
+
+
+ def clamp(self):
+ raise NotImplementedError
+
+
+ def clip(self, other):
+ raise NotImplementedError
+
+
+ def union(self, other):
+ raise NotImplementedError
+
+
+ def unionall(self, *rects):
+ raise NotImplementedError
+
+
+ def fit(self):
+ raise NotImplementedError
+
+
+ def normalized(self):
+ """
+ return a normalized bbox of this one
+ """
+ x, y, z, d, w, h = self
+ if d < 0:
+ x += d
+ d = -d
+ if w < 0:
+ y += w
+ w = -w
+ if h < 0:
+ z += h
+ h = -h
+ return BBox(x, y, z, d, w, h)
+
+
+ def contains(self, other):
+ raise NotImplementedError
+ other = BBox(other)
+ # this is not correct!
+ return ((self[0] <= other.back) and
+ (self[1] <= other.top) and
+ (self[2] <= other.front) and
+ (self[0] + self[4] >= other.right) and
+ (self[1] + self[5] >= other.bottom) and
+ (self[2] + self[3] >= other.back) and
+ (self[0] + self[4] > other.left) and
+ (self[1] + self[5] > other.top) and
+ (self[2] + self[3] > other.front))
+
+
+ def collidepoint(self, (x, y, z)):
+ return (x >= self[0] and x < self[0] + self[3] and
+ y >= self[1] and y < self[1] + self[4] and
+ z >= self[2] and z < self[2] + self[5])
+
+
+ def collidebbox(self, other):
+ return intersect(self, BBox(other))
+
+
+ def collidelist(self, l):
+ for i, bbox in enumerate(l):
+ if intersect(self, bbox):
+ return i
+ return -1
+
+
+ def collidelistall(self, l):
+ return [ i for i, bbox in enumerate(l)
+ if intersect(self, bbox) ]
+
+
+ def collidedict(self):
+ raise NotImplementedError
+
+
+ def collidedictall(self):
+ raise NotImplementedError
+
+
+ @property
+ def back(self):
+ return self[0]
+
+
+ @property
+ def left(self):
+ return self[1]
+
+
+ @property
+ def bottom(self):
+ return self[2]
+
+
+ @property
+ def front(self):
+ return self[0] + self[3]
+
+
+ @property
+ def right(self):
+ return self[1] + self[4]
+
+
+ @property
+ def top(self):
+ return self[2] + self[5]
+
+
+ @property
+ def size(self):
+ return self[3], self[4], self[5]
+
+
+ @property
+ def origin(self):
+ return self[0], self[1], self[2]
+
+
+ @property
+ def bottomcenter(self):
+ return self[0]+self[3]/2, self[1]+self[4]/2, self[2]
+
+
+ @property
+ def topcenter(self):
+ return self[0]+self[3]/2, self[1]+self[4]/2, self[2]+self[5]
+
+
+ @property
+ def center(self):
+ return self[0]+self[3]/2, self[1]+self[4]/2, self[2]+self[5]/2
+
+
+ @property
+ def x(self):
+ return self[0]
+
+
+ @property
+ def y(self):
+ return self[1]
+
+
+ @property
+ def z(self):
+ return self[2]
+
+
+ @property
+ def depth(self):
+ return self[3]
+
+
+ @property
+ def width(self):
+ return self[4]
+
+
+ @property
+ def height(self):
+ return self[5]
View
69 lib2d/buttons.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+
+# Button Constants
+
+# movement
+BUTTON_LEFT = 64
+BUTTON_RIGHT = 128
+BUTTON_UP = 256
+BUTTON_DOWN = 512
+
+# for fighting style games
+BUTTON_PUNCH = 7 # don't assign controls to this -- for interal use
+BUTTON_LOW_PUNCH = 1
+BUTTON_MED_PUNCH = 2
+BUTTON_HI_PUNCH = 4
+
+BUTTON_KICK = 56 # don't assign controls to this -- for interal use
+BUTTON_LOW_KICK = 8
+BUTTON_MED_KICK = 16
+BUTTON_HI_KICK = 32
+
+BUTTON_GUARD = 2048
+
+# virutal
+STATE_VIRTUAL = 1024
+STATE_FINISHED = 1
+FALL_DAMAGE = 2048
+
+
+
+# misc
+BUTTON_NULL = 0 # virtual button to handle state changes w/hold buttons
+BUTTON_FORWARD = 4096
+BUTTON_BACK = 8192
+BUTTON_PAUSE = 1024
+BUTTON_MENU = 1024
+BUTTON_SELECT = 4096
+MOUSEPOS = 16384
+CLICK1 = 16
+CLICK2 = 32
+CLICK3 = 64
+CLICK4 = 128
+
+
+BUTTONUP = 1 # after being released
+BUTTONHELD = 2 # when button is down for more than one check
+BUTTONDOWN = 4 # will only be this value on first check
+
+P1_UP = 1
+P1_DOWN = 2
+P1_LEFT = 4
+P1_RIGHT = 8
+P1_ACTION1 = 16
+P1_ACTION2 = 32
+P1_ACTION3 = 64
+P1_ACTION4 = 128
+P1_X_RETURN = 256
+P1_Y_RETURN = 512
+
+
+P2_UP = 1
+P2_DOWN = 2
+P2_LEFT = 4
+P2_RIGHT = 8
+P2_ACTION1 = 16
+P2_ACTION2 = 32
+
+
+KEYNAMES = {1:"up", 2:"down", 4:"left", 8:"right", 16:"action0", 32:"action1"}
View
367 lib2d/context.py
@@ -0,0 +1,367 @@
+"""
+Copyright 2010, 2011 Leif Theden
+
+
+This file is part of lib2d.
+
+lib2d 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.
+
+lib2d 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 lib2d. If not, see <http://www.gnu.org/licenses/>.
+"""
+
+import gfx
+import pygame
+from lib2d.objects import GameObject
+from collections import deque
+from itertools import cycle, islice
+from pygame.locals import *
+
+
+"""
+player's input doesn't get checked every loop. it is checked every 15ms and
+then handled. this prevents the game logic from dealing with input too often
+and slowing down rendering.
+"""
+
+
+
+
+
+class Context(object):
+ """
+ Game states are a logical way to break up distinct portions
+ of a game.
+ """
+
+ def __init__(self, parent):
+ """
+ Called when object is instanced.
+
+ parent is a ref to the statedriver
+
+ Not a good idea to load large objects here since it is possible
+ that the state is simply instanced and placed in a queue.
+
+ Ideally, any initialization will be handled in activate() since
+ that is the point when assets will be required.
+ """
+
+ self.parent = parent
+ self.activated = False
+
+
+ def activate(self):
+ """
+ Called when focus is given to the state for the first time
+
+ *** When overriding this method, set activated to true ***
+ """
+
+ pass
+
+
+ def reactivate(self):
+ """
+ Called with focus is given to the state again
+ """
+
+ pass
+
+
+ def deactivate(self):
+ """
+ Called when focus is being lost
+ """
+
+ pass
+
+
+ def terminate(self):
+ """
+ Called when the state is no longer needed
+ The state will be lost after this is called
+ """
+
+ pass
+
+
+ def draw(self, surface):
+ """
+ Called when state can draw to the screen
+ """
+
+ pass
+
+
+ def handle_command(self, command):
+ """
+ Called when there is an input command to process
+ """
+
+ pass
+
+
+ def update(self, time):
+ pass
+
+
+ def done(self):
+ self.parent.done()
+
+
+def flush_cmds(cmds):
+ pass
+
+
+class StatePlaceholder(object):
+ """
+ holds a ref to a state
+
+ when found in the queue, will be instanced
+ """
+
+ def __init__(self, klass):
+ self.klass = klass
+
+ def activate(self):
+ pass
+
+ def deactivate(self):
+ pass
+
+
+class ContextDriver(object):
+ """
+ A state driver controls what is displayed and where input goes.
+
+ A state is a logical way to break up "modes" of use for a game.
+ For example, a title screen, options screen, normal play, pause,
+ etc.
+ """
+
+ def __init__(self, parent, target_fps=30):
+ self.parent = parent
+ self._stack = deque()
+ self.target_fps = target_fps
+ self.inputs = []
+
+ self.lameduck = None
+
+
+ if parent != None:
+ self.reload_screen()
+
+
+ def get_size(self):
+ """
+ Return the size of the surface that is being drawn on.
+
+ * This may differ from the size of the window or screen if the display
+ is set to scale.
+ """
+
+ return self.parent.get_screen().get_size()
+
+
+ def get_screen(self):
+ """
+ Return the surface that is being drawn to.
+
+ * This may not be the pygame display surface
+ """
+
+ return self.parent.get_screen()
+
+
+ def reload_screen(self):
+ """
+ Called when the display changes mode.
+ """
+
+ self._screen = self.parent.get_screen()
+
+
+ def done(self):
+ """
+ deactivate the current state and activate the next state, if any
+ """
+
+ if self.lameduck is None:
+ self.lameduck = self._stack.pop()
+
+
+ def getCurrentState(self):
+ try:
+ return self._stack[-1]
+ except:
+ return None
+
+
+ def start(self, state):
+ """
+ start a new state and hold the current state.
+
+ when the new state finishes, the previous one will continue
+ where it was left off.
+
+ idea: the old state could be pickled and stored to disk.
+ """
+
+ self._stack.append(state)
+ self.getCurrentState().activate()
+ self.getCurrentState().activated = True
+
+
+ def start_restart(self, state):
+ """
+ start a new state and hold the current state.
+
+ the current state will be terminated and a placeholder will be
+ placed on the stack. when the new state finishes, the previous
+ state will be re-instanced. this can be used to conserve memory.
+ """
+
+ prev = self.getCurrentState()
+ prev.deactivate()
+ self._stack.pop()
+ self._stack.append(StatePlaceholder(prev.__class__))
+ self.start(state)
+
+
+ def push(self, state):
+ self._stack.append(state)
+
+
+ def roundrobin(*iterables):
+ """
+ create a new schedule for concurrent states
+ roundrobin('ABC', 'D', 'EF') --> A D E B F C
+
+ Recipe credited to George Sakkis
+ """
+
+ pending = len(iterables)
+ nexts = cycle(iter(it).next for it in iterables)
+ while pending:
+ try:
+ for next in nexts:
+ yield next()
+ except StopIteration:
+ pending -= 1
+ nexts = cycle(islice(nexts, pending))
+
+
+ def run(self):
+ """
+ run the state driver.
+ """
+
+ # deref for speed
+ event_poll = pygame.event.poll
+ event_pump = pygame.event.pump
+ current_state = self.getCurrentState
+ clock = pygame.time.Clock()
+
+ # streamline event processing by filtering out stuff we won't use
+ allowed = [QUIT, KEYDOWN, KEYUP, \
+ MOUSEBUTTONDOWN, MOUSEBUTTONUP, MOUSEMOTION]
+
+ pygame.event.set_allowed(None)
+ pygame.event.set_allowed(allowed)
+
+ # set an event to update the game state
+ debug_output = pygame.USEREVENT
+ pygame.time.set_timer(debug_output, 2000)
+
+ # make sure our custom events will be triggered
+ pygame.event.set_allowed([debug_output])
+
+ currentState = current_state()
+ lastState = currentState
+
+ # this will loop until the end of the program
+ while currentState:
+
+ if self.lameduck:
+ self.lameduck = None
+ currentState = self._stack[-1]
+ if currentState.activated:
+ currentState.reactivate()
+ else:
+ currentState.activate()
+
+ elif currentState is not lastState:
+ if currentState.activated:
+ currentState.reactivate()
+ else:
+ currentState.activate()
+
+ lastState = currentState
+
+ time = clock.tick(self.target_fps)
+
+
+# =============================================================================
+# EVENT HANDLING ==============================================================
+
+ event = event_poll()
+ while event:
+
+ # we should quit
+ if event.type == QUIT:
+ currentState = None
+ break
+
+ # check each input for something interesting
+ for cmd in [ c.getCommand(event) for c in self.inputs ]:
+ if cmd is not None:
+ currentState.handle_command(cmd)
+
+ if event.type == debug_output:
+ print "current FPS: \t{0:.1f}".format(clock.get_fps())
+
+ # back out of this state, or send event to the state
+ elif event.type == KEYDOWN:
+ if event.key == K_ESCAPE:
+ currentState = None
+ break
+
+ event = event_poll()
+
+# =============================================================================
+# STATE UPDATING AND DRAWING HANDLING =========================================
+
+ if current_state() is currentState:
+
+ dirty = currentState.draw(self._screen)
+ gfx.update_display(dirty)
+ #gfx.update_display()
+
+ # looks awkward? because it is. forcibly give small updates
+ # to each object so we don't draw too often.
+
+ time = time / 5.0
+
+ currentState.update(time)
+ currentState = current_state()
+ if not currentState == lastState: continue
+ currentState.update(time)
+ currentState = current_state()
+ if not currentState == lastState: continue
+ currentState.update(time)
+ currentState = current_state()
+ if not currentState == lastState: continue
+ currentState.update(time)
+ currentState = current_state()
+ if not currentState == lastState: continue
+ currentState.update(time)
+ currentState = current_state()
View
166 lib2d/cursor.py
@@ -0,0 +1,166 @@
+"""
+Misc. software cursor.
+"""
+
+from pygame.locals import *
+from pygame.rect import Rect
+from pygame.transform import flip
+import pygame
+
+
+
+class Cursor(object):
+ """
+ Cursor base class that has a shadow and flips while moving
+ """
+ def __init__(self, image, hotspot=(0,0)):
+ """
+ surface = Global surface to draw on
+ cursor = surface of cursor (needs to be specified when enabled!)
+ hotspot = the hotspot for your cursor
+ """
+ self.enabled = 0
+ self.image = None
+ self.shadow = None
+ self.hotspot = hotspot
+ self.bg = None
+ self.offset = 0,0
+ self.old_pos = 0,0
+ self.direction = 0
+ self.do_flip = False
+
+ if image:
+ self.setImage(image, hotspot)
+
+
+ def enable(self):
+ """
+ Enable the GfxCursor (disable normal pygame cursor)
+ """
+ raise NotImplementedError
+
+
+ def disable(self):
+ """
+ Disable the GfxCursor (enable normal pygame cursor)
+ """
+ raise NotImplementedError
+
+
+ def make_shadow(self):
+ # generate an image for use as a shadow
+ # this is a kludge
+ self.shadow = self.image.copy()
+ colorkey = self.image.get_at((0,0))
+ self.shadow.set_colorkey(colorkey)
+ for x in xrange(self.image.get_rect().width):
+ for y in xrange(self.image.get_rect().height):
+ if not self.shadow.get_at((x, y)) == colorkey:
+ self.shadow.set_at((x, y), (0,0,0))
+ self.shadow.convert()
+ self.shadow.set_alpha(60)
+
+
+ def setImage(self, image, hotspot=(0,0)):
+ """
+ Set a new cursor surface
+ """
+ self.image = image
+ self.offset = hotspot
+ self.image.set_alpha(200)
+ self.make_shadow()
+ if self.direction == 1:
+ self.do_flip = True
+
+
+ def setHotspot(self,pos):
+ """
+ Set a new hotspot for the cursor
+ """
+ self.hide()
+ self.offset = pos
+
+
+ def draw(self, surface):
+ if self.enabled:
+ if self.do_flip:
+ self.image = flip(self.image, True, False)
+ if self.shadow is not None:
+ self.shadow = flip(self.shadow, True, False)
+ self.do_flip = False
+
+ pos=[self.old_pos[0]-self.offset[0],self.old_pos[1]-self.offset[1]]
+
+ if self.shadow is not None:
+ surface.blit(self.shadow,(pos[0]+2, pos[1]+2))
+ surface.blit(self.image,pos)
+
+
+ def setPos(self, pos):
+ if self.direction == 0:
+ if self.old_pos[0] > pos[0]:
+ if self.do_flip == False:
+ self.direction = 1
+ self.do_flip = True
+
+ else:
+ if self.old_pos[0] < pos[0]:
+ if self.do_flip == False:
+ self.direction = 0
+ self.do_flip = True
+
+ self.old_pos = pos[:]
+
+
+ def setFlip(self, flip):
+ """
+ The cursor can optionally flip left or right depending on the
+ direction it is moving. This is a neat-o effect for the hand cursor.
+ """
+ self.do_flip = bool(flip)
+
+
+class MouseCursor(Cursor):
+ """
+ Replaces the normal pygame mouse cursor with any bitmap cursor
+ """
+
+ def enable(self):
+ """
+ Enable the GfxCursor (disables normal pygame cursor)
+ """
+ if not self.image or self.enabled: return
+ pygame.mouse.set_visible(0)
+ self.enabled = 1
+
+
+ def disable(self):
+ """
+ Disable the GfxCursor (enables normal pygame cursor)
+ """
+ if self.enabled:
+ pygame.mouse.set_visible(1)
+ self.enabled = 0
+
+
+class KeyCursor(Cursor):
+ """
+ A cursor that can be controlled by keys
+ (not complete)
+ """
+
+ def enable(self):
+ """
+ Enable the GfxCursor
+ """
+ if not self.image or self.enabled: return
+ self.enabled = 1
+
+
+ def disable(self):
+ """
+ Disable the GfxCursor
+ """
+ if self.enabled:
+ self.enabled = 0
+
View
198 lib2d/draw.py
@@ -0,0 +1,198 @@
+"""
+Copyright 2009, 2010, 2011 Leif Theden
+
+This file is part of lib2d.
+
+lib2d 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.
+
+lib2d 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 lib2d. If not, see <http://www.gnu.org/licenses/>.
+"""
+
+import res
+from pygame import Surface, Rect, RLEACCEL
+import pygame
+from math import ceil
+from itertools import product
+
+
+
+class GraphicBox(object):
+ """
+ Generic class for drawing graphical boxes
+
+ load it, then draw it wherever needed
+ """
+
+ def __init__(self, image):
+ surface = image.load()
+ iw, self.th = surface.get_size()
+ self.tw = iw / 9
+ names = "nw ne sw se n e s w c".split()
+ tiles = [ surface.subsurface((i*self.tw, 0, self.tw, self.th))
+ for i in range(len(names)) ]
+
+ self.tiles = dict(zip(names, tiles))
+ self.tiles['c'] = self.tiles['c'].convert_alpha()
+
+ def draw(self, surface, rect, fill=False):
+ ox, oy, w, h = Rect(rect)
+
+ if fill:
+ if fill == True:
+ pass
+ elif isinstance(fill, int):
+ print "int"
+ self.tiles['c'].set_alpha(fill)
+
+ wmod = 0
+ hmod = 0
+
+ if float(w) % self.tw > 0:
+ wmod = self.tw
+
+ if float(h) / self.th> 0:
+ hmod = self.th
+
+ p = product(range(ox+4, ox+w-wmod, self.tw),
+ range(oy+4, oy+h-hmod, self.th))
+
+
+ [ surface.blit(self.tiles['c'], (x, y)) for x, y in p ]
+
+ # we were unable to fill it completly due to size restrictions
+ if wmod:
+ for y in range(oy+4, oy+h-hmod, self.th):
+ surface.blit(self.tiles['c'], (ox+w-self.tw-4, y))
+
+ if hmod:
+ for x in range(ox+4, ox+w-wmod, self.tw):
+ surface.blit(self.tiles['c'], (x, oy+h-self.th-4))
+
+ if hmod or wmod:
+ surface.blit(self.tiles['c'], (ox+w-self.tw-4, oy+h-self.th-4))
+
+
+ for x in range(self.tw+ox, w-self.tw+ox, self.tw):
+ surface.blit(self.tiles['n'], (x, oy))
+ surface.blit(self.tiles['s'], (x, h-self.th+oy))
+
+ for y in range(self.th+oy, h-self.th+oy, self.th):
+ surface.blit(self.tiles['w'], (w-self.tw+ox, y))
+ surface.blit(self.tiles['e'], (ox, y))
+
+ surface.blit(self.tiles['nw'], (ox, oy))
+ surface.blit(self.tiles['ne'], (w-self.tw+ox, oy))
+ surface.blit(self.tiles['se'], (ox, h-self.th+oy))
+ surface.blit(self.tiles['sw'], (w-self.tw+ox, h-self.th+oy))
+
+
+# draw some text into an area of a surface
+# automatically wraps words
+# returns any text that didn't get blitted
+# passing None as the surface is ok
+def drawText(surface, text, color, rect, font=None, aa=False, bkg=None):
+ rect = Rect(rect)
+ y = rect.top
+ lineSpacing = -2
+ maxWidth = 0
+ maxHeight = 0
+
+ if font is None:
+ fullpath = pygame.font.get_default_font()
+ font = pygame.font.Font(fullpath, 12)
+
+ # get the height of the font
+ fontHeight = font.size("Tg")[1]
+
+ # for very small fonts, turn off antialiasing
+ if fontHeight < 16:
+ aa=0
+ bkg=None
+
+
+ while text:
+ i = 1
+
+ # determine if the row of text will be outside our area
+ if y + fontHeight > rect.bottom:
+ break
+
+ # determine maximum width of line
+ while font.size(text[:i])[0] < rect.width and i < len(text):
+ if text[i] == "\n":
+ text = text[:i] + text[i+1:]
+ break
+ i += 1