PageRenderTime 76ms CodeModel.GetById 60ms app.highlight 12ms RepoModel.GetById 1ms app.codeStats 1ms

/robots/scene.py

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