def add_floor_sides(vmf: VMF, locs): """We need to replace nodraw textures around the outside of the holes. This requires looping through all faces, since these will have been nodrawed. """ added_locations = { barrier.wall.as_tuple(): False for barrier in locs } for face in vmf.iter_wfaces(world=True, detail=False): if face.mat != 'tools/toolsnodraw': continue loc = face.get_origin().as_tuple() if loc in added_locations: random.seed('floor_side_{}_{}_{}'.format(*loc)) face.mat = random.choice(MATS['squarebeams']) added_locations[loc] = True # Swap these to flip the texture diagonally, so the beam is at top face.uaxis, face.vaxis = face.vaxis, face.uaxis face.uaxis.offset = 48 vbsp.IGNORED_FACES.add(face) # Look for the ones without a texture - these are open to the void and # need to be sealed. The template chamfers the edges # to prevent showing void at outside corners. for wall_loc, ceil_loc, rot in locs: if added_locations[wall_loc.as_tuple()]: continue diag_loc = (wall_loc.x, wall_loc.y, wall_loc.z + 128) temp_data = template_brush.import_template( # If there's a wall surface directly above this point # or a ceiling brush in the next block over # we want to use a world brush to seal the leak. # Otherwise we use the detail version for inside the map. temp_name=( FLOOR_TEMP_SIDE_DETAIL if ceil_loc not in conditions.SOLIDS and diag_loc not in conditions.SOLIDS else FLOOR_TEMP_SIDE_WORLD ), origin=wall_loc, angles=Vec(0, rot, 0), ) template_brush.retexture_template( temp_data, wall_loc, # Switch to use the configured squarebeams texture replace_tex={ consts.Special.SQUAREBEAMS: random.choice( MATS['squarebeams'] ), } )
def add_floor_sides(vmf: VMF, locs): """We need to replace nodraw textures around the outside of the holes. This requires looping through all faces, since these will have been nodrawed. """ added_locations = {barrier.wall.as_tuple(): False for barrier in locs} for face in vmf.iter_wfaces(world=True, detail=False): if face.mat != 'tools/toolsnodraw': continue loc = face.get_origin().as_tuple() if loc in added_locations: random.seed('floor_side_{}_{}_{}'.format(*loc)) face.mat = random.choice(MATS['squarebeams']) added_locations[loc] = True # Swap these to flip the texture diagonally, so the beam is at top face.uaxis, face.vaxis = face.vaxis, face.uaxis face.uaxis.offset = 48 vbsp.IGNORED_FACES.add(face) # Look for the ones without a texture - these are open to the void and # need to be sealed. The template chamfers the edges # to prevent showing void at outside corners. for wall_loc, ceil_loc, rot in locs: if added_locations[wall_loc.as_tuple()]: continue diag_loc = (wall_loc.x, wall_loc.y, wall_loc.z + 128) temp_data = template_brush.import_template( # If there's a wall surface directly above this point # or a ceiling brush in the next block over # we want to use a world brush to seal the leak. # Otherwise we use the detail version for inside the map. temp_name=(FLOOR_TEMP_SIDE_DETAIL if ceil_loc not in conditions.SOLIDS and diag_loc not in conditions.SOLIDS else FLOOR_TEMP_SIDE_WORLD), origin=wall_loc, angles=Vec(0, rot, 0), ) template_brush.retexture_template( temp_data, wall_loc, # Switch to use the configured squarebeams texture replace_tex={ consts.Special.SQUAREBEAMS: random.choice(MATS['squarebeams']), })
def res_import_template(inst: Entity, res: Property): """Import a template VMF file, retexturing it to match orientation. It will be placed overlapping the given instance. Options: - ID: The ID of the template to be inserted. Add visgroups to additionally add after a colon, comma-seperated (temp_id:vis1,vis2) - force: a space-seperated list of overrides. If 'white' or 'black' is present, the colour of tiles will be overridden. If 'invert' is added, white/black tiles will be swapped. If a tile size ('2x2', '4x4', 'wall', 'special') is included, all tiles will be switched to that size (if not a floor/ceiling). If 'world' or 'detail' is present, the brush will be forced to that type. - replace: A block of template material -> replacement textures. This is case insensitive - any texture here will not be altered otherwise. - replaceBrush: The position of a brush to replace (0 0 0=the surface). This brush will be removed, and overlays will be fixed to use all faces with the same normal. Can alternately be a block: - Pos: The position to replace. - additionalIDs: Space-separated list of face IDs in the template to also fix for overlays. The surface should have close to a vertical normal, to prevent rescaling the overlay. - removeBrush: If true, the original brush will not be removed. - transferOverlay: Allow disabling transferring overlays to this template. The IDs will be removed instead. (This can be an instvar). - keys/localkeys: If set, a brush entity will instead be generated with these values. This overrides force world/detail. Specially-handled keys: - "origin", offset automatically. - "movedir" on func_movelinear - set a normal surrounded by <>, this gets replaced with angles. - colorVar: If this fixup var is set to 'white' or 'black', that colour will be forced. If the value is '<editor>', the colour will be chosen based on the color of the surface for ItemButtonFloor, funnels or entry/exit frames. - invertVar: If this fixup value is true, tile colour will be swapped to the opposite of the current force option. This applies after colorVar. - visgroup: Sets how visgrouped parts are handled. If 'none' (default), they are ignored. If 'choose', one is chosen. If a number, that is the percentage chance for each visgroup to be added. - visgroup_force_var: If set and True, visgroup is ignored and all groups are added. """ ( orig_temp_id, replace_tex, force_colour, force_grid, force_type, replace_brush_pos, rem_replace_brush, transfer_overlays, additional_replace_ids, invert_var, color_var, visgroup_func, visgroup_force_var, key_block, ) = res.value temp_id = conditions.resolve_value(inst, orig_temp_id) if srctools.conv_bool(conditions.resolve_value(inst, visgroup_force_var)): def visgroup_func(group): """Use all the groups.""" yield from group temp_name, visgroups = template_brush.parse_temp_name(temp_id) try: template = template_brush.get_template(temp_name) except template_brush.InvalidTemplateName: # The template map is read in after setup is performed, so # it must be checked here! # We don't want an error, just quit if temp_id != orig_temp_id: LOGGER.warning('{} -> "{}" is not a valid template!', orig_temp_id, temp_name) else: LOGGER.warning('"{}" is not a valid template!', temp_name) return if color_var.casefold() == '<editor>': # Check traits for the colour it should be. traits = instance_traits.get(inst) if 'white' in traits: force_colour = template_brush.MAT_TYPES.white elif 'black' in traits: force_colour = template_brush.MAT_TYPES.black else: LOGGER.warning( '"{}": Instance "{}" ' "isn't one with inherent color!", temp_id, inst['file'], ) elif color_var: color_val = conditions.resolve_value(inst, color_var).casefold() if color_val == 'white': force_colour = template_brush.MAT_TYPES.white elif color_val == 'black': force_colour = template_brush.MAT_TYPES.black # else: no color var if srctools.conv_bool(conditions.resolve_value(inst, invert_var)): force_colour = template_brush.TEMP_COLOUR_INVERT[force_colour] # else: False value, no invert. origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles', '0 0 0']) temp_data = template_brush.import_template( template, origin, angles, targetname=inst['targetname', ''], force_type=force_type, visgroup_choose=visgroup_func, add_to_map=True, additional_visgroups=visgroups, ) if key_block is not None: conditions.set_ent_keys(temp_data.detail, inst, key_block) br_origin = Vec.from_str(key_block.find_key('keys')['origin']) br_origin.localise(origin, angles) temp_data.detail['origin'] = br_origin move_dir = temp_data.detail['movedir', ''] if move_dir.startswith('<') and move_dir.endswith('>'): move_dir = Vec.from_str(move_dir).rotate(*angles) temp_data.detail['movedir'] = move_dir.to_angle() # Add it to the list of ignored brushes, so vbsp.change_brush() doesn't # modify it. vbsp.IGNORED_BRUSH_ENTS.add(temp_data.detail) try: # This is the original brush the template is replacing. We fix overlay # face IDs, so this brush is replaced by the faces in the template # pointing # the same way. if replace_brush_pos is None: raise KeyError # Not set, raise to jump out of the try block pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z) pos += origin brush_group = SOLIDS[pos.as_tuple()] except KeyError: # Not set or solid group doesn't exist, skip.. pass else: LOGGER.info('IDS: {}', additional_replace_ids | template.overlay_faces) conditions.steal_from_brush( temp_data, brush_group, rem_replace_brush, map(int, additional_replace_ids | template.overlay_faces), conv_bool(conditions.resolve_value(inst, transfer_overlays), True), ) template_brush.retexture_template( temp_data, origin, inst.fixup, replace_tex, force_colour, force_grid, )
def res_piston_plat(vmf: VMF, inst: Entity, res: Property) -> None: """Generates piston platforms with optimized logic.""" template: template_brush.Template visgroup_names: List[str] inst_filenames: Dict[str, str] has_dn_fizz: bool automatic_var: str color_var: str source_ent: str snd_start: str snd_loop: str snd_stop: str ( template, visgroup_names, inst_filenames, has_dn_fizz, automatic_var, color_var, source_ent, snd_start, snd_loop, snd_stop, ) = res.value min_pos = inst.fixup.int(FixupVars.PIST_BTM) max_pos = inst.fixup.int(FixupVars.PIST_TOP) start_up = inst.fixup.bool(FixupVars.PIST_IS_UP) # Allow doing variable lookups here. visgroup_names = [ conditions.resolve_value(inst, name) for name in visgroup_names ] if len(ITEMS[inst['targetname']].inputs) == 0: # No inputs. Check for the 'auto' var if applicable. if automatic_var and inst.fixup.bool(automatic_var): pass # The item is automatically moving, so we generate the dynamics. else: # It's static, we just make that and exit. position = max_pos if start_up else min_pos inst.fixup[FixupVars.PIST_BTM] = position inst.fixup[FixupVars.PIST_TOP] = position static_inst = inst.copy() vmf.add_ent(static_inst) static_inst['file'] = inst_filenames['fullstatic_' + str(position)] return init_script = 'SPAWN_UP <- {}'.format('true' if start_up else 'false') if snd_start and snd_stop: packing.pack_files(vmf, snd_start, snd_stop, file_type='sound') init_script += '; START_SND <- `{}`; STOP_SND <- `{}`'.format( snd_start, snd_stop) elif snd_start: packing.pack_files(vmf, snd_start, file_type='sound') init_script += '; START_SND <- `{}`'.format(snd_start) elif snd_stop: packing.pack_files(vmf, snd_stop, file_type='sound') init_script += '; STOP_SND <- `{}`'.format(snd_stop) script_ent = vmf.create_ent( classname='info_target', targetname=local_name(inst, 'script'), vscripts='BEE2/piston/common.nut', vscript_init_code=init_script, origin=inst['origin'], ) if has_dn_fizz: script_ent['thinkfunction'] = 'FizzThink' if start_up: st_pos, end_pos = max_pos, min_pos else: st_pos, end_pos = min_pos, max_pos script_ent.add_out( Output('OnUser1', '!self', 'RunScriptCode', 'moveto({})'.format(st_pos)), Output('OnUser2', '!self', 'RunScriptCode', 'moveto({})'.format(end_pos)), ) origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles']) off = Vec(z=128).rotate(*angles) move_ang = off.to_angle() # Index -> func_movelinear. pistons = {} # type: Dict[int, Entity] static_ent = vmf.create_ent('func_brush', origin=origin) for pist_ind in [1, 2, 3, 4]: pist_ent = inst.copy() vmf.add_ent(pist_ent) if pist_ind <= min_pos: # It's below the lowest position, so it can be static. pist_ent['file'] = inst_filenames['static_' + str(pist_ind)] pist_ent['origin'] = brush_pos = origin + pist_ind * off temp_targ = static_ent else: # It's a moving component. pist_ent['file'] = inst_filenames['dynamic_' + str(pist_ind)] if pist_ind > max_pos: # It's 'after' the highest position, so it never extends. # So simplify by merging those all. # That's before this so it'll have to exist. temp_targ = pistons[max_pos] if start_up: pist_ent['origin'] = brush_pos = origin + max_pos * off else: pist_ent['origin'] = brush_pos = origin + min_pos * off pist_ent.fixup['$parent'] = 'pist' + str(max_pos) else: # It's actually a moving piston. if start_up: brush_pos = origin + pist_ind * off else: brush_pos = origin + min_pos * off pist_ent['origin'] = brush_pos pist_ent.fixup['$parent'] = 'pist' + str(pist_ind) pistons[pist_ind] = temp_targ = vmf.create_ent( 'func_movelinear', targetname=local_name(pist_ent, 'pist' + str(pist_ind)), origin=brush_pos - off, movedir=move_ang, startposition=start_up, movedistance=128, speed=150, ) if pist_ind - 1 in pistons: pistons[pist_ind]['parentname'] = local_name( pist_ent, 'pist' + str(pist_ind - 1), ) if not pist_ent['file']: # No actual instance, remove. pist_ent.remove() temp_result = template_brush.import_template( template, brush_pos, angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, additional_visgroups={visgroup_names[pist_ind - 1]}, ) temp_targ.solids.extend(temp_result.world) template_brush.retexture_template( temp_result, origin, pist_ent.fixup, generator=GenCat.PANEL, ) # Associate any set panel with the same entity, if it's present. tile_pos = Vec(z=-128) tile_pos.localise(origin, angles) panel: Optional[Panel] = None try: tiledef = TILES[tile_pos.as_tuple(), off.norm().as_tuple()] except KeyError: pass else: for panel in tiledef.panels: if panel.same_item(inst): break else: # Checked all of them. panel = None if panel is not None: if panel.brush_ent in vmf.entities and not panel.brush_ent.solids: panel.brush_ent.remove() panel.brush_ent = pistons[max(pistons.keys())] panel.offset = st_pos * off if not static_ent.solids and (panel is None or panel.brush_ent is not static_ent): static_ent.remove() if snd_loop: script_ent['classname'] = 'ambient_generic' script_ent['message'] = snd_loop script_ent['health'] = 10 # Volume script_ent['pitch'] = '100' script_ent['spawnflags'] = 16 # Start silent, looped. script_ent['radius'] = 1024 if source_ent: # Parent is irrelevant for actual entity locations, but it # survives for the script to read. script_ent['SourceEntityName'] = script_ent[ 'parentname'] = local_name(inst, source_ent)
def convert_floor( vmf: VMF, loc: Vec, overlay_ids, mats, settings, signage_loc, detail, noise_weight, noise_func: SimplexNoise, ): """Cut out tiles at the specified location.""" # We pop it, so the face isn't detected by other logic - otherwise it'll # be retextured and whatnot, which we don't want. try: brush = conditions.SOLIDS.pop(loc.as_tuple()) except KeyError: return False # No tile here! if brush.normal == (0, 0, 1): # This is a pillar block - there isn't actually tiles here! # We need to generate a squarebeams brush to fill this gap. brush.face.mat = 'tools/toolsnodraw' # It won't be visible temp_data = template_brush.import_template( temp_name=FLOOR_TEMP_PILLAR, origin=loc, ) template_brush.retexture_template( temp_data, loc, # Switch to use the configured squarebeams texture replace_tex={ consts.Special.SQUAREBEAMS: random.choice(MATS['squarebeams']), }) return False # The new brush IDs overlays need to use # NOTE: strings, not ints! ant_locs = overlay_ids[str(brush.face.id)] = [] # Move the floor brush down and switch to the floorbase texture. for plane in brush.face.planes: plane.z -= FLOOR_DEPTH brush.face.mat = random.choice(mats['floorbase']) loc.x -= 64 loc.y -= 64 for x, y in utils.iter_grid(max_x=4, max_y=4): tile_loc = loc + (x * 32 + 16, y * 32 + 16, 0) if tile_loc.as_tuple() in signage_loc: # Force the tile to be present under signage.. should_make_tile = True rand = 100 # We don't need to check this again in future! signage_loc.remove(tile_loc.as_tuple()) else: # Create a number between 0-100 rand = 100 * get_noise(tile_loc // 32, noise_func) + 10 # Adjust based on the noise_weight value, so boundries have more tiles rand *= 0.1 + 0.9 * (1 - noise_weight) should_make_tile = rand < settings['floor_chance'] if random.randint(0, 7) == 0: # Sometimes there'll be random holes/extra tiles should_make_tile = not should_make_tile if should_make_tile: # Full tile tile = make_tile( vmf, p1=tile_loc - (16, 16, 0), p2=tile_loc + (16, 16, -2), top_mat=vbsp.get_tex(str(brush.color) + '.floor'), bottom_mat='tools/toolsnodraw', beam_mat=random.choice(mats['squarebeams']), ) detail.solids.append(tile.solid) ant_locs.append(str(tile.top.id)) elif rand < settings['floor_glue_chance']: # 'Glue' tile - this chance should be higher, making these appear # bordering the full tiles. tile = make_tile( vmf, p1=tile_loc - (16, 16, 1), p2=tile_loc + (16, 16, -2), top_mat=random.choice(mats['tile_glue']), bottom_mat='tools/toolsnodraw', beam_mat=random.choice(mats['squarebeams']), ) detail.solids.append(tile.solid) else: # No tile at this loc! pass return True
def res_piston_plat(vmf: VMF, inst: Entity, res: Property): """Generates piston platforms with optimized logic.""" ( template, visgroup_names, inst_filenames, automatic_var, color_var, source_ent, snd_start, snd_loop, snd_stop, ) = res.value # type: template_brush.Template, List[str], Dict[str, str], str, str, str, str, str, str min_pos = inst.fixup.int(FixupVars.PIST_BTM) max_pos = inst.fixup.int(FixupVars.PIST_TOP) start_up = inst.fixup.bool(FixupVars.PIST_IS_UP) # Allow doing variable lookups here. visgroup_names = [ conditions.resolve_value(inst, name) for name in visgroup_names ] if len(ITEMS[inst['targetname']].inputs) == 0: # No inputs. Check for the 'auto' var if applicable. if automatic_var and inst.fixup.bool(automatic_var): pass # The item is automatically moving, so we generate the dynamics. else: # It's static, we just make that and exit. position = max_pos if start_up else min_pos inst.fixup[FixupVars.PIST_BTM] = position inst.fixup[FixupVars.PIST_TOP] = position static_inst = inst.copy() vmf.add_ent(static_inst) static_inst['file'] = inst_filenames['fullstatic_' + str(position)] return init_script = 'SPAWN_UP <- {}'.format('true' if start_up else 'false') if snd_start and snd_stop: packing.pack_files(vmf, snd_start, snd_stop, file_type='sound') init_script += '; START_SND <- `{}`; STOP_SND <- `{}`'.format( snd_start, snd_stop) elif snd_start: packing.pack_files(vmf, snd_start, file_type='sound') init_script += '; START_SND <- `{}`'.format(snd_start) elif snd_stop: packing.pack_files(vmf, snd_stop, file_type='sound') init_script += '; STOP_SND <- `{}`'.format(snd_stop) script_ent = vmf.create_ent( classname='info_target', targetname=local_name(inst, 'script'), vscripts='BEE2/piston/common.nut', vscript_init_code=init_script, origin=inst['origin'], ) if start_up: st_pos, end_pos = max_pos, min_pos else: st_pos, end_pos = min_pos, max_pos script_ent.add_out( Output('OnUser1', '!self', 'RunScriptCode', 'moveto({})'.format(st_pos)), Output('OnUser2', '!self', 'RunScriptCode', 'moveto({})'.format(end_pos)), ) origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles']) off = Vec(z=128).rotate(*angles) move_ang = off.to_angle() # Index -> func_movelinear. pistons = {} # type: Dict[int, Entity] static_ent = vmf.create_ent('func_brush', origin=origin) color_var = conditions.resolve_value(inst, color_var).casefold() if color_var == 'white': top_color = template_brush.MAT_TYPES.white elif color_var == 'black': top_color = template_brush.MAT_TYPES.black else: top_color = None for pist_ind in range(1, 5): pist_ent = inst.copy() vmf.add_ent(pist_ent) if pist_ind <= min_pos: # It's below the lowest position, so it can be static. pist_ent['file'] = inst_filenames['static_' + str(pist_ind)] pist_ent['origin'] = brush_pos = origin + pist_ind * off temp_targ = static_ent else: # It's a moving component. pist_ent['file'] = inst_filenames['dynamic_' + str(pist_ind)] if pist_ind > max_pos: # It's 'after' the highest position, so it never extends. # So simplify by merging those all. # That's before this so it'll have to exist. temp_targ = pistons[max_pos] if start_up: pist_ent['origin'] = brush_pos = origin + max_pos * off else: pist_ent['origin'] = brush_pos = origin + min_pos * off pist_ent.fixup['$parent'] = 'pist' + str(max_pos) else: # It's actually a moving piston. if start_up: brush_pos = origin + pist_ind * off else: brush_pos = origin + min_pos * off pist_ent['origin'] = brush_pos pist_ent.fixup['$parent'] = 'pist' + str(pist_ind) pistons[pist_ind] = temp_targ = vmf.create_ent( 'func_movelinear', targetname=local_name(pist_ent, 'pist' + str(pist_ind)), origin=brush_pos - off, movedir=move_ang, startposition=start_up, movedistance=128, speed=150, ) if pist_ind - 1 in pistons: pistons[pist_ind]['parentname'] = local_name( pist_ent, 'pist' + str(pist_ind - 1), ) if not pist_ent['file']: # No actual instance, remove. pist_ent.remove() temp_result = template_brush.import_template( template, brush_pos, angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, additional_visgroups={visgroup_names[pist_ind - 1]}, ) temp_targ.solids.extend(temp_result.world) template_brush.retexture_template( temp_result, origin, pist_ent.fixup, force_colour=top_color, force_grid='special', no_clumping=True, ) if not static_ent.solids: static_ent.remove() if snd_loop: script_ent['classname'] = 'ambient_generic' script_ent['message'] = snd_loop script_ent['health'] = 10 # Volume script_ent['pitch'] = '100' script_ent['spawnflags'] = 16 # Start silent, looped. script_ent['radius'] = 1024 if source_ent: # Parent is irrelevant for actual entity locations, but it # survives for the script to read. script_ent['SourceEntityName'] = script_ent[ 'parentname'] = local_name(inst, source_ent)
def res_import_template(inst: Entity, res: Property): """Import a template VMF file, retexturing it to match orientation. It will be placed overlapping the given instance. Options: - `ID`: The ID of the template to be inserted. Add visgroups to additionally add after a colon, comma-seperated (`temp_id:vis1,vis2`). Either section, or the whole value can be a `$fixup`. - `force`: a space-seperated list of overrides. If 'white' or 'black' is present, the colour of tiles will be overridden. If `invert` is added, white/black tiles will be swapped. If a tile size (`2x2`, `4x4`, `wall`, `special`) is included, all tiles will be switched to that size (if not a floor/ceiling). If 'world' or 'detail' is present, the brush will be forced to that type. - `replace`: A block of template material -> replacement textures. This is case insensitive - any texture here will not be altered otherwise. If the material starts with a `#`, it is instead a face ID. - `replaceBrush`: The position of a brush to replace (`0 0 0`=the surface). This brush will be removed, and overlays will be fixed to use all faces with the same normal. Can alternately be a block: - `Pos`: The position to replace. - `additionalIDs`: Space-separated list of face IDs in the template to also fix for overlays. The surface should have close to a vertical normal, to prevent rescaling the overlay. - `removeBrush`: If true, the original brush will not be removed. - `transferOverlay`: Allow disabling transferring overlays to this template. The IDs will be removed instead. (This can be a `$fixup`). - `keys`/`localkeys`: If set, a brush entity will instead be generated with these values. This overrides force world/detail. Specially-handled keys: - `"origin"`, offset automatically. - `"movedir"` on func_movelinear - set a normal surrounded by `<>`, this gets replaced with angles. - `colorVar`: If this fixup var is set to `white` or `black`, that colour will be forced. If the value is `<editor>`, the colour will be chosen based on the color of the surface for ItemButtonFloor, funnels or entry/exit frames. - `invertVar`: If this fixup value is true, tile colour will be swapped to the opposite of the current force option. This applies after colorVar. - `visgroup`: Sets how visgrouped parts are handled. If `none` (default), they are ignored. If `choose`, one is chosen. If a number, that is the percentage chance for each visgroup to be added. - `visgroup_force_var`: If set and True, visgroup is ignored and all groups are added. - `outputs`: Add outputs to the brush ent. Syntax is like VMFs, and all names are local to the instance. """ ( orig_temp_id, replace_tex, force_colour, force_grid, force_type, replace_brush_pos, rem_replace_brush, transfer_overlays, additional_replace_ids, invert_var, color_var, visgroup_func, visgroup_force_var, key_block, outputs, ) = res.value if ':' in orig_temp_id: # Split, resolve each part, then recombine. temp_id, visgroup = orig_temp_id.split(':', 1) temp_id = ( conditions.resolve_value(inst, temp_id) + ':' + conditions.resolve_value(inst, visgroup) ) else: temp_id = conditions.resolve_value(inst, orig_temp_id) if srctools.conv_bool(conditions.resolve_value(inst, visgroup_force_var)): def visgroup_func(group): """Use all the groups.""" yield from group temp_name, visgroups = template_brush.parse_temp_name(temp_id) try: template = template_brush.get_template(temp_name) except template_brush.InvalidTemplateName: # If we did lookup, display both forms. if temp_id != orig_temp_id: LOGGER.warning( '{} -> "{}" is not a valid template!', orig_temp_id, temp_name ) else: LOGGER.warning( '"{}" is not a valid template!', temp_name ) # We don't want an error, just quit. return if color_var.casefold() == '<editor>': # Check traits for the colour it should be. traits = instance_traits.get(inst) if 'white' in traits: force_colour = template_brush.MAT_TYPES.white elif 'black' in traits: force_colour = template_brush.MAT_TYPES.black else: LOGGER.warning( '"{}": Instance "{}" ' "isn't one with inherent color!", temp_id, inst['file'], ) elif color_var: color_val = conditions.resolve_value(inst, color_var).casefold() if color_val == 'white': force_colour = template_brush.MAT_TYPES.white elif color_val == 'black': force_colour = template_brush.MAT_TYPES.black # else: no color var if srctools.conv_bool(conditions.resolve_value(inst, invert_var)): force_colour = template_brush.TEMP_COLOUR_INVERT[force_colour] # else: False value, no invert. origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles', '0 0 0']) temp_data = template_brush.import_template( template, origin, angles, targetname=inst['targetname', ''], force_type=force_type, visgroup_choose=visgroup_func, add_to_map=True, additional_visgroups=visgroups, ) if key_block is not None: conditions.set_ent_keys(temp_data.detail, inst, key_block) br_origin = Vec.from_str(key_block.find_key('keys')['origin']) br_origin.localise(origin, angles) temp_data.detail['origin'] = br_origin move_dir = temp_data.detail['movedir', ''] if move_dir.startswith('<') and move_dir.endswith('>'): move_dir = Vec.from_str(move_dir).rotate(*angles) temp_data.detail['movedir'] = move_dir.to_angle() for out in outputs: # type: Output out = out.copy() out.target = conditions.local_name(inst, out.target) temp_data.detail.add_out(out) # Add it to the list of ignored brushes, so vbsp.change_brush() doesn't # modify it. vbsp.IGNORED_BRUSH_ENTS.add(temp_data.detail) try: # This is the original brush the template is replacing. We fix overlay # face IDs, so this brush is replaced by the faces in the template # pointing # the same way. if replace_brush_pos is None: raise KeyError # Not set, raise to jump out of the try block pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z) pos += origin brush_group = SOLIDS[pos.as_tuple()] except KeyError: # Not set or solid group doesn't exist, skip.. pass else: LOGGER.info('IDS: {}', additional_replace_ids | template.overlay_faces) conditions.steal_from_brush( temp_data, brush_group, rem_replace_brush, map(int, additional_replace_ids | template.overlay_faces), conv_bool(conditions.resolve_value(inst, transfer_overlays), True), ) template_brush.retexture_template( temp_data, origin, inst.fixup, replace_tex, force_colour, force_grid, # Don't allow clumping if using custom keyvalues - then it won't be edited. no_clumping=key_block is not None, )
def convert_floor( vmf: VMF, loc: Vec, overlay_ids, mats, settings, signage_loc, detail, noise_weight, noise_func: SimplexNoise, ): """Cut out tiles at the specified location.""" # We pop it, so the face isn't detected by other logic - otherwise it'll # be retextured and whatnot, which we don't want. try: brush = conditions.SOLIDS.pop(loc.as_tuple()) except KeyError: return False # No tile here! if brush.normal == (0, 0, 1): # This is a pillar block - there isn't actually tiles here! # We need to generate a squarebeams brush to fill this gap. brush.face.mat = 'tools/toolsnodraw' # It won't be visible temp_data = template_brush.import_template( temp_name=FLOOR_TEMP_PILLAR, origin=loc, ) template_brush.retexture_template( temp_data, loc, # Switch to use the configured squarebeams texture replace_tex={ consts.Special.SQUAREBEAMS: random.choice( MATS['squarebeams'] ), } ) return False # The new brush IDs overlays need to use # NOTE: strings, not ints! ant_locs = overlay_ids[str(brush.face.id)] = [] # Move the floor brush down and switch to the floorbase texture. for plane in brush.face.planes: plane.z -= FLOOR_DEPTH brush.face.mat = random.choice(mats['floorbase']) loc.x -= 64 loc.y -= 64 for x, y in utils.iter_grid(max_x=4, max_y=4): tile_loc = loc + (x * 32 + 16, y * 32 + 16, 0) if tile_loc.as_tuple() in signage_loc: # Force the tile to be present under signage.. should_make_tile = True rand = 100 # We don't need to check this again in future! signage_loc.remove(tile_loc.as_tuple()) else: # Create a number between 0-100 rand = 100 * get_noise(tile_loc // 32, noise_func) + 10 # Adjust based on the noise_weight value, so boundries have more tiles rand *= 0.1 + 0.9 * (1 - noise_weight) should_make_tile = rand < settings['floor_chance'] if random.randint(0, 7) == 0: # Sometimes there'll be random holes/extra tiles should_make_tile = not should_make_tile if should_make_tile: # Full tile tile = make_tile( vmf, p1=tile_loc - (16, 16, 0), p2=tile_loc + (16, 16, -2), top_mat=vbsp.get_tex(str(brush.color) + '.floor'), bottom_mat='tools/toolsnodraw', beam_mat=random.choice(mats['squarebeams']), ) detail.solids.append(tile.solid) ant_locs.append(str(tile.top.id)) elif rand < settings['floor_glue_chance']: # 'Glue' tile - this chance should be higher, making these appear # bordering the full tiles. tile = make_tile( vmf, p1=tile_loc - (16, 16, 1), p2=tile_loc + (16, 16, -2), top_mat=random.choice(mats['tile_glue']), bottom_mat='tools/toolsnodraw', beam_mat=random.choice(mats['squarebeams']), ) detail.solids.append(tile.solid) else: # No tile at this loc! pass return True
def res_import_template(inst: Entity, res: Property): """Import a template VMF file, retexturing it to match orientation. It will be placed overlapping the given instance. If no block is used, only ID can be specified. Options: - `ID`: The ID of the template to be inserted. Add visgroups to additionally add after a colon, comma-seperated (`temp_id:vis1,vis2`). Either section, or the whole value can be a `$fixup`. - `force`: a space-seperated list of overrides. If 'white' or 'black' is present, the colour of tiles will be overridden. If `invert` is added, white/black tiles will be swapped. If a tile size (`2x2`, `4x4`, `wall`, `special`) is included, all tiles will be switched to that size (if not a floor/ceiling). If 'world' or 'detail' is present, the brush will be forced to that type. - `replace`: A block of template material -> replacement textures. This is case insensitive - any texture here will not be altered otherwise. If the material starts with a `#`, it is instead a list of face IDs separated by spaces. If the result evaluates to "", no change occurs. Both can be $fixups (parsed first). - `bindOverlay`: Bind overlays in this template to the given surface, and bind overlays on a surface to surfaces in this template. The value specifies the offset to the surface, where 0 0 0 is the floor position. It can also be a block of multiple positions. - `keys`/`localkeys`: If set, a brush entity will instead be generated with these values. This overrides force world/detail. Specially-handled keys: - `"origin"`, offset automatically. - `"movedir"` on func_movelinear - set a normal surrounded by `<>`, this gets replaced with angles. - `colorVar`: If this fixup var is set to `white` or `black`, that colour will be forced. If the value is `<editor>`, the colour will be chosen based on the color of the surface for ItemButtonFloor, funnels or entry/exit frames. - `invertVar`: If this fixup value is true, tile colour will be swapped to the opposite of the current force option. This applies after colorVar. - `visgroup`: Sets how visgrouped parts are handled. Several values are possible: - A property block: Each name should match a visgroup, and the value should be a block of flags that if true enables that group. - 'none' (default): All extra groups are ignored. - 'choose': One group is chosen randomly. - a number: The percentage chance for each visgroup to be added. - `visgroup_force_var`: If set and True, visgroup is ignored and all groups are added. - `pickerVars`: If this is set, the results of colorpickers can be read out of the template. The key is the name of the picker, the value is the fixup name to write to. The output is either 'white', 'black' or ''. - `outputs`: Add outputs to the brush ent. Syntax is like VMFs, and all names are local to the instance. - `senseOffset`: If set, colorpickers and tilesetters will be treated as being offset by this amount. """ ( orig_temp_id, replace_tex, force_colour, force_grid, force_type, surf_cat, bind_tile_pos, invert_var, color_var, visgroup_func, visgroup_force_var, visgroup_instvars, key_block, picker_vars, outputs, sense_offset, ) = res.value if ':' in orig_temp_id: # Split, resolve each part, then recombine. temp_id, visgroup = orig_temp_id.split(':', 1) temp_id = ( conditions.resolve_value(inst, temp_id) + ':' + conditions.resolve_value(inst, visgroup) ) else: temp_id = conditions.resolve_value(inst, orig_temp_id) if srctools.conv_bool(conditions.resolve_value(inst, visgroup_force_var)): def visgroup_func(group): """Use all the groups.""" yield from group # Special case - if blank, just do nothing silently. if not temp_id: return temp_name, visgroups = template_brush.parse_temp_name(temp_id) try: template = template_brush.get_template(temp_name) except template_brush.InvalidTemplateName: # If we did lookup, display both forms. if temp_id != orig_temp_id: LOGGER.warning( '{} -> "{}" is not a valid template!', orig_temp_id, temp_name ) else: LOGGER.warning( '"{}" is not a valid template!', temp_name ) # We don't want an error, just quit. return for vis_flag_block in visgroup_instvars: if all(conditions.check_flag(flag, inst) for flag in vis_flag_block): visgroups.add(vis_flag_block.real_name) if color_var.casefold() == '<editor>': # Check traits for the colour it should be. traits = instance_traits.get(inst) if 'white' in traits: force_colour = texturing.Portalable.white elif 'black' in traits: force_colour = texturing.Portalable.black else: LOGGER.warning( '"{}": Instance "{}" ' "isn't one with inherent color!", temp_id, inst['file'], ) elif color_var: color_val = conditions.resolve_value(inst, color_var).casefold() if color_val == 'white': force_colour = texturing.Portalable.white elif color_val == 'black': force_colour = texturing.Portalable.black # else: no color var if srctools.conv_bool(conditions.resolve_value(inst, invert_var)): force_colour = template_brush.TEMP_COLOUR_INVERT[force_colour] # else: False value, no invert. origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles', '0 0 0']) temp_data = template_brush.import_template( template, origin, angles, targetname=inst['targetname', ''], force_type=force_type, visgroup_choose=visgroup_func, add_to_map=True, additional_visgroups=visgroups, bind_tile_pos=bind_tile_pos, ) if key_block is not None: conditions.set_ent_keys(temp_data.detail, inst, key_block) br_origin = Vec.from_str(key_block.find_key('keys')['origin']) br_origin.localise(origin, angles) temp_data.detail['origin'] = br_origin move_dir = temp_data.detail['movedir', ''] if move_dir.startswith('<') and move_dir.endswith('>'): move_dir = Vec.from_str(move_dir).rotate(*angles) temp_data.detail['movedir'] = move_dir.to_angle() for out in outputs: # type: Output out = out.copy() out.target = conditions.local_name(inst, out.target) temp_data.detail.add_out(out) template_brush.retexture_template( temp_data, origin, inst.fixup, replace_tex, force_colour, force_grid, surf_cat, sense_offset, ) for picker_name, picker_var in picker_vars: picker_val = temp_data.picker_results.get( picker_name, None, ) # type: Optional[texturing.Portalable] if picker_val is not None: inst.fixup[picker_var] = picker_val.value else: inst.fixup[picker_var] = ''
def res_import_template(inst: Entity, res: Property): """Import a template VMF file, retexturing it to match orientation. It will be placed overlapping the given instance. Options: - ID: The ID of the template to be inserted. Add visgroups to additionally add after a colon, comma-seperated (temp_id:vis1,vis2) - force: a space-seperated list of overrides. If 'white' or 'black' is present, the colour of tiles will be overridden. If `invert` is added, white/black tiles will be swapped. If a tile size ('2x2', '4x4', 'wall', 'special') is included, all tiles will be switched to that size (if not a floor/ceiling). If 'world' or 'detail' is present, the brush will be forced to that type. - replace: A block of template material -> replacement textures. This is case insensitive - any texture here will not be altered otherwise. If the material starts with a '#', it is instead a face ID. - replaceBrush: The position of a brush to replace (0 0 0=the surface). This brush will be removed, and overlays will be fixed to use all faces with the same normal. Can alternately be a block: - Pos: The position to replace. - additionalIDs: Space-separated list of face IDs in the template to also fix for overlays. The surface should have close to a vertical normal, to prevent rescaling the overlay. - removeBrush: If true, the original brush will not be removed. - transferOverlay: Allow disabling transferring overlays to this template. The IDs will be removed instead. (This can be an instvar). - keys/localkeys: If set, a brush entity will instead be generated with these values. This overrides force world/detail. Specially-handled keys: - "origin", offset automatically. - "movedir" on func_movelinear - set a normal surrounded by <>, this gets replaced with angles. - colorVar: If this fixup var is set to `white` or `black`, that colour will be forced. If the value is `<editor>`, the colour will be chosen based on the color of the surface for ItemButtonFloor, funnels or entry/exit frames. - invertVar: If this fixup value is true, tile colour will be swapped to the opposite of the current force option. This applies after colorVar. - visgroup: Sets how visgrouped parts are handled. If 'none' (default), they are ignored. If 'choose', one is chosen. If a number, that is the percentage chance for each visgroup to be added. - visgroup_force_var: If set and True, visgroup is ignored and all groups are added. - outputs: Add outputs to the brush ent. Syntax is like VMFs, and all names are local to the instance. """ ( orig_temp_id, replace_tex, force_colour, force_grid, force_type, replace_brush_pos, rem_replace_brush, transfer_overlays, additional_replace_ids, invert_var, color_var, visgroup_func, visgroup_force_var, key_block, outputs, ) = res.value if ':' in orig_temp_id: # Split, resolve each part, then recombine. temp_id, visgroup = orig_temp_id.split(':', 1) temp_id = ( conditions.resolve_value(inst, temp_id) + ':' + conditions.resolve_value(inst, visgroup) ) else: temp_id = conditions.resolve_value(inst, orig_temp_id) if srctools.conv_bool(conditions.resolve_value(inst, visgroup_force_var)): def visgroup_func(group): """Use all the groups.""" yield from group temp_name, visgroups = template_brush.parse_temp_name(temp_id) try: template = template_brush.get_template(temp_name) except template_brush.InvalidTemplateName: # If we did lookup, display both forms. if temp_id != orig_temp_id: LOGGER.warning( '{} -> "{}" is not a valid template!', orig_temp_id, temp_name ) else: LOGGER.warning( '"{}" is not a valid template!', temp_name ) # We don't want an error, just quit. return if color_var.casefold() == '<editor>': # Check traits for the colour it should be. traits = instance_traits.get(inst) if 'white' in traits: force_colour = template_brush.MAT_TYPES.white elif 'black' in traits: force_colour = template_brush.MAT_TYPES.black else: LOGGER.warning( '"{}": Instance "{}" ' "isn't one with inherent color!", temp_id, inst['file'], ) elif color_var: color_val = conditions.resolve_value(inst, color_var).casefold() if color_val == 'white': force_colour = template_brush.MAT_TYPES.white elif color_val == 'black': force_colour = template_brush.MAT_TYPES.black # else: no color var if srctools.conv_bool(conditions.resolve_value(inst, invert_var)): force_colour = template_brush.TEMP_COLOUR_INVERT[force_colour] # else: False value, no invert. origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles', '0 0 0']) temp_data = template_brush.import_template( template, origin, angles, targetname=inst['targetname', ''], force_type=force_type, visgroup_choose=visgroup_func, add_to_map=True, additional_visgroups=visgroups, ) if key_block is not None: conditions.set_ent_keys(temp_data.detail, inst, key_block) br_origin = Vec.from_str(key_block.find_key('keys')['origin']) br_origin.localise(origin, angles) temp_data.detail['origin'] = br_origin move_dir = temp_data.detail['movedir', ''] if move_dir.startswith('<') and move_dir.endswith('>'): move_dir = Vec.from_str(move_dir).rotate(*angles) temp_data.detail['movedir'] = move_dir.to_angle() for out in outputs: # type: Output out = out.copy() out.target = conditions.local_name(inst, out.target) temp_data.detail.add_out(out) # Add it to the list of ignored brushes, so vbsp.change_brush() doesn't # modify it. vbsp.IGNORED_BRUSH_ENTS.add(temp_data.detail) try: # This is the original brush the template is replacing. We fix overlay # face IDs, so this brush is replaced by the faces in the template # pointing # the same way. if replace_brush_pos is None: raise KeyError # Not set, raise to jump out of the try block pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z) pos += origin brush_group = SOLIDS[pos.as_tuple()] except KeyError: # Not set or solid group doesn't exist, skip.. pass else: LOGGER.info('IDS: {}', additional_replace_ids | template.overlay_faces) conditions.steal_from_brush( temp_data, brush_group, rem_replace_brush, map(int, additional_replace_ids | template.overlay_faces), conv_bool(conditions.resolve_value(inst, transfer_overlays), True), ) template_brush.retexture_template( temp_data, origin, inst.fixup, replace_tex, force_colour, force_grid, # Don't allow clumping if using custom keyvalues - then it won't be edited. no_clumping=key_block is not None, )
def res_piston_plat(vmf: VMF, inst: Entity, res: Property): """Generates piston platforms with optimized logic.""" ( template, visgroup_names, inst_filenames, automatic_var, color_var, source_ent, snd_start, snd_loop, snd_stop, ) = res.value # type: template_brush.Template, List[str], Dict[str, str], str, str, str, str, str, str min_pos = inst.fixup.int(FixupVars.PIST_BTM) max_pos = inst.fixup.int(FixupVars.PIST_TOP) start_up = inst.fixup.bool(FixupVars.PIST_IS_UP) # Allow doing variable lookups here. visgroup_names = [ conditions.resolve_value(inst, name) for name in visgroup_names ] if len(ITEMS[inst['targetname']].inputs) == 0: # No inputs. Check for the 'auto' var if applicable. if automatic_var and inst.fixup.bool(automatic_var): pass # The item is automatically moving, so we generate the dynamics. else: # It's static, we just make that and exit. position = max_pos if start_up else min_pos inst.fixup[FixupVars.PIST_BTM] = position inst.fixup[FixupVars.PIST_TOP] = position static_inst = inst.copy() vmf.add_ent(static_inst) static_inst['file'] = inst_filenames['fullstatic_' + str(position)] return init_script = 'SPAWN_UP <- {}'.format('true' if start_up else 'false') if snd_start and snd_stop: packing.pack_files(vmf, snd_start, snd_stop, file_type='sound') init_script += '; START_SND <- `{}`; STOP_SND <- `{}`'.format(snd_start, snd_stop) elif snd_start: packing.pack_files(vmf, snd_start, file_type='sound') init_script += '; START_SND <- `{}`'.format(snd_start) elif snd_stop: packing.pack_files(vmf, snd_stop, file_type='sound') init_script += '; STOP_SND <- `{}`'.format(snd_stop) script_ent = vmf.create_ent( classname='info_target', targetname=local_name(inst, 'script'), vscripts='BEE2/piston/common.nut', vscript_init_code=init_script, origin=inst['origin'], ) if start_up: st_pos, end_pos = max_pos, min_pos else: st_pos, end_pos = min_pos, max_pos script_ent.add_out( Output('OnUser1', '!self', 'RunScriptCode', 'moveto({})'.format(st_pos)), Output('OnUser2', '!self', 'RunScriptCode', 'moveto({})'.format(end_pos)), ) origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles']) off = Vec(z=128).rotate(*angles) move_ang = off.to_angle() # Index -> func_movelinear. pistons = {} # type: Dict[int, Entity] static_ent = vmf.create_ent('func_brush', origin=origin) color_var = conditions.resolve_value(inst, color_var).casefold() if color_var == 'white': top_color = template_brush.MAT_TYPES.white elif color_var == 'black': top_color = template_brush.MAT_TYPES.black else: top_color = None for pist_ind in range(1, 5): pist_ent = inst.copy() vmf.add_ent(pist_ent) if pist_ind <= min_pos: # It's below the lowest position, so it can be static. pist_ent['file'] = inst_filenames['static_' + str(pist_ind)] pist_ent['origin'] = brush_pos = origin + pist_ind * off temp_targ = static_ent else: # It's a moving component. pist_ent['file'] = inst_filenames['dynamic_' + str(pist_ind)] if pist_ind > max_pos: # It's 'after' the highest position, so it never extends. # So simplify by merging those all. # That's before this so it'll have to exist. temp_targ = pistons[max_pos] if start_up: pist_ent['origin'] = brush_pos = origin + max_pos * off else: pist_ent['origin'] = brush_pos = origin + min_pos * off pist_ent.fixup['$parent'] = 'pist' + str(max_pos) else: # It's actually a moving piston. if start_up: brush_pos = origin + pist_ind * off else: brush_pos = origin + min_pos * off pist_ent['origin'] = brush_pos pist_ent.fixup['$parent'] = 'pist' + str(pist_ind) pistons[pist_ind] = temp_targ = vmf.create_ent( 'func_movelinear', targetname=local_name(pist_ent, 'pist' + str(pist_ind)), origin=brush_pos - off, movedir=move_ang, startposition=start_up, movedistance=128, speed=150, ) if pist_ind - 1 in pistons: pistons[pist_ind]['parentname'] = local_name( pist_ent, 'pist' + str(pist_ind - 1), ) if not pist_ent['file']: # No actual instance, remove. pist_ent.remove() temp_result = template_brush.import_template( template, brush_pos, angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, additional_visgroups={visgroup_names[pist_ind - 1]}, ) temp_targ.solids.extend(temp_result.world) template_brush.retexture_template( temp_result, origin, pist_ent.fixup, force_colour=top_color, force_grid='special', no_clumping=True, ) if not static_ent.solids: static_ent.remove() if snd_loop: script_ent['classname'] = 'ambient_generic' script_ent['message'] = snd_loop script_ent['health'] = 10 # Volume script_ent['pitch'] = '100' script_ent['spawnflags'] = 16 # Start silent, looped. script_ent['radius'] = 1024 if source_ent: # Parent is irrelevant for actual entity locations, but it # survives for the script to read. script_ent['SourceEntityName'] = script_ent['parentname'] = local_name(inst, source_ent)