PageRenderTime 62ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/robots/scene.py

https://bitbucket.org/lordmauve/metalwork
Python | 308 lines | 193 code | 44 blank | 71 comment | 16 complexity | afe9a03705e76fe793b33a00b1cc6341 MD5 | raw file
  1. import re
  2. import os.path
  3. import math
  4. import pygame
  5. from pygame.locals import *
  6. from world import World
  7. ALT_INCREMENT = 35
  8. IMAGES_ROOT = 'assets/images'
  9. def load_sprite(fname):
  10. return pygame.image.load(os.path.join(IMAGES_ROOT, fname)).convert_alpha()
  11. class Actor(object):
  12. """An actor is an object with visual presence, but not necessarily physical
  13. presence, in the 3D world."""
  14. def __init__(self, x, y, alt=0, description=None):
  15. self.pos = x, y # actual position
  16. self.display_pos = None
  17. self.alt = alt
  18. self.display_alt = None
  19. self.description = description
  20. def play(self, name):
  21. """Play a named animation.
  22. If name is None, configure the base animation."""
  23. def position(self):
  24. return self.display_pos or self.pos
  25. def altitude(self):
  26. return self.display_alt or self.alt
  27. def draw_center(self, screen, sprite, x, y):
  28. w, h = sprite.get_size()
  29. screen.blit(sprite, (x - w/2, y - h/2))
  30. def tooltip(self):
  31. return re.sub(r'([a-z])([A-Z])', r'\1 \2', self.__class__.__name__)
  32. def is_pushable(self, direction):
  33. return False
  34. #class AnimatedSprite(object):
  35. # class Instance(object):
  36. # def __init__(self, a):
  37. # self.a = a
  38. # self.ftime = 0
  39. # self.currentframe = 0
  40. # self.finished = False
  41. #
  42. # def draw(self, screen, x, y):
  43. # framedelay = self.a.framedelay
  44. # frames = self.a.frames
  45. #
  46. # if self.finished:
  47. # return
  48. #
  49. # self.ftime += 1
  50. # if self.ftime == framedelay:
  51. # self.ftime = 0
  52. # self.currentframe += 1
  53. # if self.currentframe == len(frames):
  54. # if self.a.loop:
  55. # self.currentframe = 0
  56. # else:
  57. # self.currentframe = len(frames) - 1
  58. # self.finished = True
  59. #
  60. # dx, dy, f = frames[self.currentframe]
  61. # w, h = f.get_size()
  62. # x = x + dx - w/2
  63. # y = y + dy - h/2
  64. # screen.blit(f, (x, y))
  65. #
  66. # def is_finished(self):
  67. # return self.finished
  68. #
  69. # def __init__(self, frames, framedelay=3, loop=True):
  70. # self.framedelay = framedelay
  71. # self.frames = frames
  72. # self.loop = loop
  73. #
  74. # def new(self):
  75. # return AnimatedSprite.Instance(self)
  76. #
  77. # @staticmethod
  78. # def load(path, range, framedelay=3, loop=True):
  79. # fs = [(0, 0, load_sprite(path % i)) for i in range]
  80. # return AnimatedSprite(fs, framedelay, loop)
  81. class Viewport(object):
  82. TILE_SIZE = (124, 72)
  83. def __init__(self, w=640, h=480, x=0, y=0):
  84. self.x = x
  85. self.y = y
  86. self.w = w
  87. self.h = h
  88. def world_pos(self, tx, ty):
  89. """Center point of the square in the world"""
  90. tw, th = self.TILE_SIZE
  91. return int((tx - ty + 1) * (tw / 2 - 1) + 0.5), int((tx + ty + 1) * (th / 2 - 1) + 0.5)
  92. def screen_pos(self, t_x, t_y, alt=0):
  93. """Center of the square on the screen"""
  94. x, y = self.world_pos(t_x, t_y)
  95. return x - int(self.x), y - int(self.y) - alt * ALT_INCREMENT
  96. def translate(self, dx, dy):
  97. self.x -= dx
  98. self.y -= dy
  99. def translate_tiles(self, tx, ty):
  100. wx0, wy0 = self.world_pos(0, 0)
  101. wx, wy = self.world_pos(tx, ty)
  102. self.x -= wx - wx0
  103. self.y -= wy - wy0
  104. def move_to(self, tx, ty):
  105. self.x, self.y = self.world_pos(tx, ty)
  106. self.x -= self.w/2
  107. self.y -= self.h/2
  108. def tile_for_pos(self, sx, sy):
  109. tw, th = self.TILE_SIZE
  110. htw = tw / 2 - 1
  111. hth = th / 2 - 1
  112. sx = float(sx + self.x)
  113. sy = float(sy + self.y)
  114. tx = (sx / htw + sy / hth) / 2 - 1
  115. ty = sy / hth - tx - 1
  116. return int(math.floor(tx + 0.5)), int(math.floor(ty + 0.5))
  117. def screen_bounds(self, actor):
  118. r = actor.bounds()
  119. ax, ay = actor.position()
  120. x, y = self.screen_pos(ax, ay, actor.altitude())
  121. return pygame.Rect(r.left + x, r.top + y, r.width, r.height)
  122. class Scene(object):
  123. """The scene is a 2D representation of a 3D World.
  124. This class knows how to draw that world and handle mapping of input to world space. A scene
  125. also contains a list of actors which are displayed in the 3D world but are not physical objects.
  126. Icons such as move arrows, require this functionality.
  127. The scene also maintains a list of animations, playing in the world, which
  128. represent the transition between one world state and another.
  129. """
  130. TILE = 0
  131. ACTOR = 1
  132. def __init__(self, world, viewport):
  133. self.world = world
  134. self.viewport = viewport
  135. self.viewport.move_to(self.world.w/2, self.world.h/2)
  136. self.animations = []
  137. self.actions = []
  138. self.gamestate = None
  139. self.tilecache = None
  140. self.hovered = None
  141. self.load_actions()
  142. self.world.start()
  143. def load_actions(self):
  144. """Graphics for actions get pre-loaded automagically when the scene
  145. starts. We could try to do this by listing dependencies for objects,
  146. but then we would have to work around circular import problems."""
  147. from robots import actions
  148. for cls in actions.__dict__.values():
  149. try:
  150. is_act = issubclass(cls, actions.Action)
  151. except TypeError:
  152. continue
  153. else:
  154. if is_act:
  155. self.world.load(cls)
  156. def draw_order(self, x, y, alt, type):
  157. return math.ceil(x + y + 0.1 * type), alt, type, x + y
  158. def tiles(self):
  159. if self.tilecache:
  160. return self.tilecache
  161. ts = []
  162. for x, y, tile, draw_south, draw_east in self.world.get_tiles():
  163. if tile is not None:
  164. ts.append((self.draw_order(x, y, tile.alt, Scene.TILE), Scene.TILE, (x, y, tile.alt), tile, draw_south, draw_east))
  165. ts.sort()
  166. self.tilecache = ts
  167. return ts
  168. def actors(self):
  169. ts = []
  170. for a in self.world.actors + self.actions:
  171. ax, ay = a.position()
  172. ts.append((self.draw_order(ax, ay, a.altitude(), Scene.ACTOR), Scene.ACTOR, a))
  173. return ts
  174. def add(self, a):
  175. self.actions.append(a)
  176. self.world.load(a.__class__)
  177. a.play(None)
  178. def remove(self, a):
  179. self.actions.remove(a)
  180. def remove_all(self):
  181. self.actions = []
  182. def draw_tile(self, screen, t):
  183. p, t, draw_south, draw_east = t
  184. x, y, alt = p
  185. sx, sy = self.viewport.screen_pos(x, y, alt)
  186. screen.start(hash((id(t), x, y)))
  187. t.draw(screen, sx, sy, draw_south, draw_east, x, y)
  188. def draw_actor(self, screen, act):
  189. screen.start(id(act))
  190. a = act[0]
  191. x, y = a.position()
  192. sx, sy = self.viewport.screen_pos(x, y, a.altitude())
  193. a.draw(screen, sx, sy)
  194. def draw(self, scenegraph):
  195. """Draw the world."""
  196. scenegraph.fill(0)
  197. s = self.tiles() + self.actors()
  198. s.sort()
  199. for i in s:
  200. if i[1] == Scene.TILE:
  201. self.draw_tile(scenegraph, i[2:])
  202. elif i[1] == Scene.ACTOR:
  203. self.draw_actor(scenegraph, i[2:])
  204. def update(self):
  205. for anim in self.animations[:]:
  206. anim.update()
  207. if anim.is_finished():
  208. self.animations.remove(anim)
  209. def add_animation(self, a):
  210. self.animations.append(a)
  211. def animation_playing(self):
  212. return len(self.animations) > 0
  213. def actors_sorted(self, pos):
  214. """Actors sorted in hit order.
  215. The hit testing checks for actions first, as action graphics
  216. are generally smaller than objects, and so they are clickable
  217. "through" anything else."""
  218. from robots.actions import Action
  219. actors = [a for a in self.actors() if self.viewport.screen_bounds(a[-1]).collidepoint(pos)]
  220. actors.sort()
  221. actors.reverse()# detect clicks in reverse draw order
  222. actions = []
  223. objects = []
  224. for a in actors:
  225. actor = a[-1]
  226. if isinstance(actor, Action):
  227. actions.append(actor)
  228. else:
  229. objects.append(actor)
  230. return actions + objects
  231. def onclick(self, pos):
  232. if not self.gamestate:
  233. return
  234. if self.animation_playing():
  235. return
  236. for a in self.actors_sorted(pos):
  237. if self.gamestate.onclick(a, pos):
  238. break
  239. def update_hover(self, pos):
  240. if not self.gamestate:
  241. return
  242. if self.animation_playing():
  243. if self.hovered:
  244. self.gamestate.onunhover(self.hovered)
  245. self.hovered = None
  246. return
  247. for a in self.actors_sorted(pos):
  248. if self.hovered == a:
  249. return
  250. if self.gamestate.onhover(a):
  251. self.gamestate.onunhover(self.hovered)
  252. self.hovered = a
  253. return
  254. if self.hovered:
  255. self.gamestate.onunhover(self.hovered)
  256. self.hovered = None