git.fiddlerwoaroof.com
Raw Blame History
import numpy as np
import libtcodpy as tc
import yaml
import libs.bresenham
import libs.cache

def squeeze(val, low, high):
	return min(max(val, low), high)

class TerrainGen(object):
	def __init__(self, map):
		self.width, self.height = map.dim
		self.terrain_registry = map.terrain_registry

	def generate(self):
		w,h = self.width, self.height
		terrains, probs = self.terrain_registry.get_terrain_types()
		map = np.random.choice(terrains, (w,h), p=probs)
		chars = np.zeros((w,h)).astype('int')
		fgcolors = np.zeros((w,h,3))
		bgcolors = np.zeros((w,h,3))

		for x, row in enumerate(map):
			for y, cell in enumerate(row):
				char, fgcolor, bgcolor = self.terrain_registry.get_display(cell)
				chars[x,y] = char
				fgcolors[x,y] = fgcolor
				bgcolors[x,y] = bgcolor
		return map, chars, fgcolors, bgcolors


class Map(object):
	def __init__(self, w, h, terrain_registry):
		self.pov = None
		self.povcache = libs.cache.Cache()

		self.width = w
		self.height = h
		self.terrain_registry = terrain_registry
		self.overlays = {} # (x,y): { set( object, ... ) }

		self.ids, self.map, self.fgcolors, self.bgcolors = TerrainGen(self).generate()

		self.fov = FovCache(self, self.terrain_registry)

	def add(self, object):
		self.overlays.setdefault(object.pos,[]).append(object)

	def check_and_execute_bump(self, object, x,y):
		if (x,y) in self.overlays:
			for other in self.overlays[x,y]:
				object.bump(other)

	def interact(self, a, bs):
		print 'interacting with:', bs
		for b in bs:
			a.interact(b)

	def remove(self, object):
		pos = object.pos
		if pos in self.overlays and object in self.overlays[pos]:
			self.overlays[pos].remove(object)

	def move(self, object, dx,dy, update_cb):
		ox,oy = object.pos

		if abs(dx) < 2 and abs(dy) < 2:
			self.overlays[object.pos].remove(object)

			collide_x, collide_y = None, None
			x = squeeze(ox+dx, 0, self.width-1)
			y = squeeze(oy+dy, 0, self.height-1)
			if not self.is_passable((x,y)):
				collide_x, collide_y = x,y
				x,y = ox,oy
			update_cb(x-ox, y-oy)

			if collide_x is not None:
				self.check_and_execute_bump(object, collide_x, collide_y)
			if object.pos in self.overlays:
				self.interact(object, self.overlays[object.pos])

			self.overlays.setdefault((x,y), []).append(object)
			self.update_overlay(ox,oy)
		else:
			# calculate the deltas for each step
			line = list(libs.bresenham.line(0,0, dx,dy, 0))
			line = [(bx-ax, by-ay) for (ax,ay), (bx,by) in zip(line,line[1:])]
			print line
			for x,y in line:
				if self.move(object, x,y, update_cb) != (x,y):
					break
			x,y = object.pos

		return x-ox, y-oy

	def update_overlay(self, x=None, y=None):
		if x is None or y is None: pass
		else:
			if (x,y) in self.overlays and self.overlays[x,y] == []:
				self.overlays.pop((x,y))

	def set_pov(self, pov):
		self.pov = pov

	def get_visible_objects(self):
		o,r = self.pov
		results = set()
		for x,y in self.overlays:
			if self.fov.is_visible(o,r, (x,y)):
				results.update(self.overlays[x,y])
		return results



	def get_rgb(self, colors, fg=True,slices=(slice(0),slice(0))):
		result = np.rollaxis(colors, 2)
		return [x.transpose() for x in result]

	def draw(self, con, tl=(0,0)):
		def mv(x,y, (lx,ty)):
			return x-lx, y-ty
		br = tl[0]+con.width, tl[1]+con.height
		slices = slice(tl[0], br[0]), slice(tl[1], br[1])

		fgcolors = self.fgcolors.astype('int')
		bgcolors = self.bgcolors.astype('int')
		color_mask = np.ones( (con.width, con.height, 3) )
		char_mask = np.ones( (con.width, con.height) ).astype('bool')

		self.pov = None
		if self.pov is not None:
			origin, radius = self.pov
			ox, oy = origin
			if ox+radius >= br[0] or oy+radius >= br[1]:
				xmod, ymod = 0,0
				if ox + radius >= br[0]:
					xmod = min(ox + radius - br[0], self.width - br[0])
				if oy + radius >= br[1]:
					ymod =  min(oy + radius - br[1], self.height - br[1])
				br = br[0] + xmod, br[1] + ymod
				tl = tl[0] + xmod, tl[1] + ymod
				slices = slice(tl[0], br[0]), slice(tl[1], br[1])
			def calc_mask():
				for x in range(tl[0], br[0]):
					for y in range(tl[1], br[1]):
						if not self.fov.is_visible(origin, radius, (x,y)):
							color_mask[x-tl[0], y-tl[1]] = (0.25, 0.5, 0.5)
							char_mask[x-tl[0], y-tl[1]] = False
				return color_mask, char_mask
			color_mask, char_mask = self.povcache.get( (origin,radius,tl), calc_mask )
			fgcolors = (fgcolors[slices] * color_mask).astype('int')
			bgcolors = (bgcolors[slices] * color_mask).astype('int')
		else:
			fgcolors = fgcolors[slices]
			bgcolors = bgcolors[slices]

		tc.console_fill_foreground(con.con, *self.get_rgb(fgcolors))
		tc.console_fill_background(con.con, *self.get_rgb(bgcolors, False))

		chars = np.copy(self.map[slices])
		for x,y in self.overlays:
			screen_x = x-tl[0]
			screen_y = y-tl[1]
			if (not (tl[0] <= x < br[0])) or (not (tl[1] <= y < br[1])): continue
			elif not char_mask[screen_x,screen_y]: continue
			if 0 <= screen_x < con.width and 0 <= screen_y < con.height:
				obj = self.overlays[x,y][-1]
				chars[screen_x,screen_y] = obj.char
				tc.console_set_char_background(con.con, screen_x,screen_y, tc.Color(*bgcolors[screen_x,screen_y]))
				tc.console_set_char_foreground(con.con, screen_x,screen_y, tc.Color(*obj.color))
		chars[np.logical_not(char_mask)] = ord(' ')
		tc.console_fill_char(con.con, chars.transpose())

	def coord_iter(self):
		return ( (x,y) for x in range(self.width) for y in range(self.height) )

	@property
	def dim(self):
		return self.width, self.height

	def is_passable(self, coord):
		if coord in self.overlays and any(x.blocks for x in self.overlays[coord]):
			return False
		else:
			return self.fov.is_passable(coord)


class FovCache(object):
	# TODO: get allow updates to base_map
	def __init__(self, map, terrain_registry):
		self.width, self.height = map.dim

		self.base_map = tc.map_new(*map.dim)

		for x,y in map.coord_iter():
			if (x,y) in map.overlays:
				object = map.overlays[x,y]
				pssble,trnsprnt = object.passable, object.transparent
			else:
				pssble,trnsprnt = terrain_registry.get_props(map.ids[x,y])
			tc.map_set_properties(self.base_map, x,y, trnsprnt,pssble)

		self.fovmaps = {}

	def get_fovmap(self, origin, radius):
		key = origin,radius

		if key in self.fovmaps: fovmap = self.fovmaps[key]
		else:
			fovmap = tc.map_new(self.width, self.height)
			tc.map_copy(self.base_map, fovmap)
			self.fovmaps[key] = fovmap

			x,y = origin
			tc.map_compute_fov(fovmap, x,y, radius, algo=tc.FOV_DIAMOND)

		return fovmap

	def is_visible(self, origin, radius, coord):
		fovmap = self.get_fovmap(origin, radius)
		return tc.map_is_in_fov(fovmap, *coord)

	def is_transparent(self, coord):
		return tc.map_is_transparent(self.base_map, *coord)
	def is_passable(self, coord):
		return tc.map_is_walkable(self.base_map, *coord)


class TerrainInfo(object):
	passable = False
	transparent = False
	char = ord(' ')
	fg = (255,255,255)
	bg = (0,0,0)
	prob = 1

	@classmethod
	def make_terrain(cls, name, char, passable, transparent,fg,bg, prob=1):
		if hasattr(char, 'upper'): char = ord(char)
		passable = bool(passable)
		transparent = bool(transparent)
		return type(name, (cls,), dict(char=char, passable=passable, transparent=transparent,fg=fg,bg=bg,prob=prob))

class TerrainRegistry(object):
	def __init__(self):
		self.id = 0
		self.registry = {}
		self.names = {}

	def get_terrain(self, id):
		ter = self.registry[id]
		return ter

	def get_props(self, id):
		ter = self.get_terrain(id)
		return ter.passable, ter.transparent

	def get_display(self, char):
		ter = self.get_terrain(char)
		return ter.char, ter.fg, ter.bg

	def get_terrain_types(self):
		types, probabilities = zip(*[(x, self.registry[x].prob) for x in self.registry])
		probs = [float(x) / sum(probabilities) for x in probabilities]
		return types, probs


	def register(self, ter):
		self.id += 1
		self.registry[self.id] = ter
		self.names[ter.__name__] = ter
		return ter

	def new_terrain(self, name, char,
		passable=False, transparent=False, fg=(255,255,255), bg=(0,0,0), prob=1
	):
		ter = TerrainInfo.make_terrain(name, char, passable, transparent, fg,bg, prob=prob)
		return self.register(ter)

	def load_from_file(self, fn, loader=yaml.safe_load):
		with open(fn) as f:
			values = loader(f)
		for name, terrain in values.viewitems():
			self.new_terrain(name, **terrain)