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 move(self, object, dx,dy): self.overlays[object.pos].remove(object) collide_x, collide_y = None, None if abs(dx) < 2 and abs(dy) < 2: ox,oy = object.pos 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 else: ox,oy = object.pos tx,ty = ox+dx, oy+dy gx,gy = ox,oy for x,y in libs.bresenham.line(ox,oy, tx,ty, 1): if not self.is_passable((x,y)): collide_x, collide_y = x,y break else: gx,gy = x,y x,y = gx,gy if collide_x is not None: self.check_and_execute_bump(object, collide_x, collide_y) self.overlays.setdefault((x,y), []).append(object) self.update_overlay(ox,oy) 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[slices], 2) return [x.transpose() for x in result] def draw(self, con, tl=(0,0)): br = tl[0]+con.width, tl[1]+con.height 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') if self.pov is not None: origin, radius = self.pov 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,y] = (0.25, 0.5, 0.5) char_mask[x,y] = False return color_mask, char_mask color_mask, char_mask = self.povcache.get( (origin,radius), calc_mask ) fgcolors = (fgcolors * color_mask).astype('int') bgcolors = (bgcolors * color_mask).astype('int') slices = slice(tl[0], tl[0]+con.width), slice(tl[1], tl[1]+con.height) tc.console_fill_foreground(con.con, *self.get_rgb(fgcolors, slices=slices)) tc.console_fill_background(con.con, *self.get_rgb(bgcolors, False,slices=slices)) chars = np.copy(self.map[slices]) for x,y in self.overlays: screen_x = x-tl[0] screen_y = y-tl[1] if 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) 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)