def res_replace_instance(inst: VLib.Entity, res): """Replace an instance with another entity. 'keys' and 'localkeys' defines the new keyvalues used. 'targetname' and 'angles' are preset, and 'origin' will be used to offset the given amount from the current location. If 'keep_instance' is true, the instance entity will be kept instead of removed. """ import vbsp origin = Vec.from_str(inst['origin']) angles = inst['angles'] if not utils.conv_bool(res['keep_instance', '0'], False): inst.remove() # Do this first to free the ent ID, so the new ent has # the same one. # We copy to allow us to still acess the $fixups and other values. new_ent = inst.copy(des_id=inst.id) new_ent.clear_keys() # Ensure there's a classname, just in case. new_ent['classname'] = 'info_null' vbsp.VMF.add_ent(new_ent) conditions.set_ent_keys(new_ent, inst, res) origin += Vec.from_str(new_ent['origin']).rotate_by_str(angles) new_ent['origin'] = origin new_ent['angles'] = angles new_ent['targetname'] = inst['targetname']
def flag_angles(inst, flag): """Check that a instance is pointed in a direction.""" angle = inst["angles", "0 0 0"] if flag.has_children(): targ_angle = flag["direction", "0 0 0"] from_dir = flag["from_dir", "0 0 1"] if from_dir.casefold() in DIRECTIONS: from_dir = Vec(DIRECTIONS[from_dir.casefold()]) else: from_dir = Vec.from_str(from_dir, 0, 0, 1) allow_inverse = utils.conv_bool(flag["allow_inverse", "0"]) else: targ_angle = flag.value from_dir = Vec(0, 0, 1) allow_inverse = False if angle == targ_angle: return True # Check for exact match normal = DIRECTIONS.get(targ_angle.casefold(), None) if normal is None: return False # If it's not a special angle, # so it failed the exact match angle = Vec.from_str(angle, 0, 0, 0) inst_normal = from_dir.rotate(angle.x, angle.y, angle.z) if normal == "WALL": # Special case - it's not on the floor or ceiling return not (inst_normal == (0, 0, 1) or inst_normal == (0, 0, -1)) else: return inst_normal == normal or (allow_inverse and -inst_normal == normal)
def flag_brush_at_loc(inst, flag): """Checks to see if a wall is present at the given location. - Pos is the position of the brush, where `0 0 0` is the floor-position of the brush. - Dir is the normal the face is pointing. (0 0 -1) is 'up'. - Type defines the type the brush must be: - "Any" requires either a black or white brush. - "None" means that no brush must be present. - "White" requires a portalable surface. - "Black" requires a non-portalable surface. - SetVar defines an instvar which will be given a value of "black", "white" or "none" to allow the result to be reused. - If gridPos is true, the position will be snapped so it aligns with the 128 brushes (Useful with fizzler/light strip items). - RemoveBrush: If set to 1, the brush will be removed if found. Only do this to EmbedFace brushes, since it will remove the other sides as well. """ from conditions import VMF pos = Vec.from_str(flag['pos', '0 0 0']) pos.z -= 64 # Subtract so origin is the floor-position pos = pos.rotate_by_str(inst['angles', '0 0 0']) # Relative to the instance origin pos += Vec.from_str(inst['origin', '0 0 0']) norm = Vec.from_str(flag['dir', '0 0 -1']).rotate_by_str( inst['angles', '0 0 0'] ) if utils.conv_bool(flag['gridpos', '0']): for axis in 'xyz': # Don't realign things in the normal's axis - # those are already fine. if norm[axis] == 0: pos[axis] = pos[axis] // 128 * 128 + 64 result_var = flag['setVar', ''] should_remove = utils.conv_bool(flag['RemoveBrush', False], False) des_type = flag['type', 'any'].casefold() brush = SOLIDS.get(pos.as_tuple(), None) if brush is None or brush.normal != norm: br_type = 'none' else: br_type = str(brush.color) if should_remove: VMF.remove_brush( brush.solid, ) if result_var: inst.fixup[result_var] = br_type if des_type == 'any' and br_type != 'none': return True return des_type == br_type
def res_faith_mods(inst, res): """Modify the trigger_catrapult that is created for ItemFaithPlate items. """ # Get data about the trigger this instance uses for flinging fixup_var = res["instvar", ""] offset = utils.conv_int(res["raise_trig", "0"]) if offset: angle = Vec.from_str(inst["angles", "0 0 0"]) offset = Vec(0, 0, offset).rotate(angle.x, angle.y, angle.z) ":type offset Vec" for trig in VMF.by_class["trigger_catapult"]: if inst["targetname"] in trig["targetname"]: if offset: # Edit both the normal and the helper trigger trig["origin"] = (Vec.from_str(trig["origin"]) + offset).join(" ") for solid in trig.solids: solid.translate(offset) for out in trig.outputs: if out.inst_in == "animate_angled_relay": out.inst_in = res["angled_targ", "animate_angled_relay"] out.input = res["angled_in", "Trigger"] if fixup_var: inst.fixup[fixup_var] = "angled" break elif out.inst_in == "animate_straightup_relay": out.inst_in = res["straight_targ", "animate_straightup_relay"] out.input = res["straight_in", "Trigger"] if fixup_var: inst.fixup[fixup_var] = "straight" break
def res_fizzler_pair(begin_inst, res): """Modify the instance of a fizzler to link with its pair.""" orig_target = begin_inst['targetname'] if 'modelEnd' in orig_target: return # We only execute starting from the start side. orig_target = orig_target[:-11] # remove "_modelStart" end_name = orig_target + '_modelEnd' # What we search for # The name all these instances get pair_name = orig_target + '-model' + str(begin_inst.id) orig_file = begin_inst['file'] begin_file = res['StartInst', orig_file] end_file = res['EndInst', orig_file] mid_file = res['MidInst', ''] begin_inst['file'] = begin_file begin_inst['targetname'] = pair_name angles = Vec.from_str(begin_inst['angles']) # We round it to get rid of 0.00001 inprecision from the calculations. direction = round(Vec(0, 0, 1).rotate(angles.x, angles.y, angles.z)) ':type direction: utils.Vec' print(end_name, direction) begin_pos = Vec.from_str(begin_inst['origin']) axis_1, axis_2, main_axis = PAIR_AXES[direction.as_tuple()] for end_inst in VMF.by_class['func_instance']: if end_inst['targetname', ''] != end_name: # Only examine this barrier hazard's instances! continue end_pos = Vec.from_str(end_inst['origin']) if ( begin_pos[axis_1] == end_pos[axis_1] and begin_pos[axis_2] == end_pos[axis_2] ): length = int(end_pos[main_axis] - begin_pos[main_axis]) break else: utils.con_log('No matching pair for {}!!'.format(orig_target)) return end_inst['targetname'] = pair_name end_inst['file'] = end_file if mid_file != '': # Go 64 from each side, and always have at least 1 section # A 128 gap will have length = 0 for dis in range(0, abs(length) + 1, 128): new_pos = begin_pos + direction*dis VMF.create_ent( classname='func_instance', targetname=pair_name, angles=begin_inst['angles'], file=mid_file, origin=new_pos.join(' '), )
def join_markers(inst_a, inst_b, is_start=False): """Join two marker ents together with corners. This returns a list of solids used for the vphysics_motion trigger. """ origin_a = Vec.from_str(inst_a['ent']['origin']) origin_b = Vec.from_str(inst_b['ent']['origin']) norm_a = Vec(-1, 0, 0).rotate_by_str(inst_a['ent']['angles']) norm_b = Vec(-1, 0, 0).rotate_by_str(inst_b['ent']['angles']) config = inst_a['conf'] if norm_a == norm_b: # Either straight-line, or s-bend. dist = (origin_a - origin_b).mag() if origin_a + (norm_a * dist) == origin_b: make_straight( origin_a, norm_a, dist, config, is_start, ) # else: S-bend, we don't do the geometry for this.. return if norm_a == -norm_b: # U-shape bend.. make_ubend( origin_a, origin_b, norm_a, config, max_size=inst_a['size'], ) return try: corner_ang, flat_angle = CORNER_ANG[norm_a.as_tuple(), norm_b.as_tuple()] if origin_a[flat_angle] != origin_b[flat_angle]: # It needs to be flat in this angle! raise ValueError except ValueError: # The tubes need two corners to join together - abort for that. return else: make_bend( origin_a, origin_b, norm_a, norm_b, corner_ang, config, max_size=inst_a['size'], )
def res_fizzler_pair(begin_inst, res): """Modify the instance of a fizzler to link with its pair.""" orig_target = begin_inst["targetname"] if "modelEnd" in orig_target: return # We only execute starting from the start side. orig_target = orig_target[:-11] # remove "_modelStart" end_name = orig_target + "_modelEnd" # What we search for # The name all these instances get pair_name = orig_target + "-model" + str(begin_inst.id) orig_file = begin_inst["file"] begin_file = res["StartInst", orig_file] end_file = res["EndInst", orig_file] mid_file = res["MidInst", ""] begin_inst["file"] = begin_file begin_inst["targetname"] = pair_name angles = Vec.from_str(begin_inst["angles"]) # We round it to get rid of 0.00001 inprecision from the calculations. direction = Vec(0, 0, 1).rotate(angles.x, angles.y, angles.z) ":type direction: utils.Vec" begin_pos = Vec.from_str(begin_inst["origin"]) axis_1, axis_2, main_axis = PAIR_AXES[direction.as_tuple()] for end_inst in VMF.by_class["func_instance"]: if end_inst["targetname", ""] != end_name: # Only examine this barrier hazard's instances! continue end_pos = Vec.from_str(end_inst["origin"]) if begin_pos[axis_1] == end_pos[axis_1] and begin_pos[axis_2] == end_pos[axis_2]: length = int(end_pos[main_axis] - begin_pos[main_axis]) break else: utils.con_log("No matching pair for {}!!".format(orig_target)) return end_inst["targetname"] = pair_name end_inst["file"] = end_file if mid_file != "": # Go 64 from each side, and always have at least 1 section # A 128 gap will have length = 0 for dis in range(0, abs(length) + 1, 128): new_pos = begin_pos + direction * dis VMF.create_ent( classname="func_instance", targetname=pair_name, angles=begin_inst["angles"], file=mid_file, origin=new_pos.join(" "), )
def res_unst_scaffold_setup(res): group = res["group", "DEFAULT_GROUP"] if group not in SCAFFOLD_CONFIGS: # Store our values in the CONFIGS dictionary targ_inst, links = SCAFFOLD_CONFIGS[group] = {}, {} else: # Grab the already-filled values, and add to them targ_inst, links = SCAFFOLD_CONFIGS[group] for block in res.find_all("Instance"): conf = { # If set, adjusts the offset appropriately "is_piston": utils.conv_bool(block["isPiston", "0"]), "rotate_logic": utils.conv_bool(block["AlterAng", "1"], True), "off_floor": Vec.from_str(block["FloorOff", "0 0 0"]), "off_wall": Vec.from_str(block["WallOff", "0 0 0"]), "logic_start": block["startlogic", ""], "logic_end": block["endLogic", ""], "logic_mid": block["midLogic", ""], "logic_start_rev": block["StartLogicRev", None], "logic_end_rev": block["EndLogicRev", None], "logic_mid_rev": block["EndLogicRev", None], "inst_wall": block["wallInst", ""], "inst_floor": block["floorInst", ""], "inst_offset": block["offsetInst", None], # Specially rotated to face the next track! "inst_end": block["endInst", None], } for logic_type in ("logic_start", "logic_mid", "logic_end"): if conf[logic_type + "_rev"] is None: conf[logic_type + "_rev"] = conf[logic_type] for inst in resolve_inst(block["file"]): targ_inst[inst] = conf # We need to provide vars to link the tracks and beams. for block in res.find_all("LinkEnt"): # The name for this set of entities. # It must be a '@' name, or the name will be fixed-up incorrectly! loc_name = block["name"] if not loc_name.startswith("@"): loc_name = "@" + loc_name links[block["nameVar"]] = { "name": loc_name, # The next entity (not set in end logic) "next": block["nextVar"], # A '*' name to reference all the ents (set on the start logic) "all": block["allVar", None], } return group # We look up the group name to find the values.
def flag_goo_at_loc(inst, flag): """Check to see if a given location is submerged in goo. 0 0 0 is the origin of the instance, values are in 128 increments. """ pos = Vec.from_str(flag.value).rotate_by_str(inst['angles', '0 0 0']) pos *= 128 pos += Vec.from_str(inst['origin']) # Round to 128 units, then offset to the center pos = pos // 128 * 128 + 64 # type: Vec val = pos.as_tuple() in GOO_LOCS return val
def track_scan( tr_set, track_inst, start_track: VLib.Entity, middle_file: str, x_dir: int, ): """Build a set of track instances extending from a point. :param track_inst: A dictionary mapping origins to track instances :param start_track: The instance we start on :param middle_file: The file for the center track piece :param x_dir: The direction to look (-1 or 1) """ track = start_track move_dir = Vec(x_dir*128, 0, 0).rotate_by_str(track['angles']) while track: tr_set.add(track) next_pos = Vec.from_str(track['origin']) + move_dir track = track_inst.get(next_pos.as_tuple(), None) if track is None: return if track['file'].casefold() != middle_file: # If the next piece is an end section, add it then quit tr_set.add(track) return
def res_import_template_setup(res): temp_id = res['id'].casefold() face = Vec.from_str(res['face_pos', '0 0 -64']) norm = Vec.from_str(res['normal', '0 0 1']) replace_tex = defaultdict(list) for prop in res.find_key('replace', []): replace_tex[prop.name].append(prop.value) offset = Vec.from_str(res['offset', '0 0 0']) return ( temp_id, dict(replace_tex), face, norm, offset, )
def find_indicator_panels(inst): """We need to locate indicator panels, so they aren't overwritten. """ if inst['file'].casefold() not in resolve_inst('[indpan]'): return loc = Vec(0, 0, -64).rotate_by_str(inst['angles']) loc += Vec.from_str(inst['origin']) # Sometimes (light bridges etc) a sign will be halfway between # tiles, so in that case we need to force 2 tiles. loc_min = (loc - (15, 15, 0)) // 32 * 32 + (16, 16, 0) loc_max = (loc + (15, 15, 0)) // 32 * 32 + (16, 16, 0) FORCE_LOCATIONS.add(loc_min.as_tuple()) FORCE_LOCATIONS.add(loc_max.as_tuple())
def res_translate_inst(inst, res): """Translate the instance locally by the given amount. The special values <piston>, <piston_bottom> and <piston_top> can be used to offset it based on the starting position, bottom or top position of a piston platform. """ folded_val = res.value.casefold() if folded_val == '<piston>': folded_val = ( '<piston_top>' if utils.conv_bool(inst.fixup['$start_up']) else '<piston_bottom>' ) if folded_val == '<piston_top>': val = Vec(z=128 * utils.conv_int(inst.fixup['$top_level', '1'], 1)) elif folded_val == '<piston_bottom>': val = Vec(z=128 * utils.conv_int(inst.fixup['$bottom_level', '0'], 0)) else: val = Vec.from_str(res.value) offset = val.rotate_by_str(inst['angles']) inst['origin'] = (offset + Vec.from_str(inst['origin'])).join(' ')
def res_faith_mods(inst, res): """Modify the trigger_catrapult that is created for ItemFaithPlate items. Values: - raise_trig: Raise or lower the trigger_catapults by this amount. - angled_targ, angled_in: Instance entity and input for angled plates - straight_targ, straight_in: Instance entity and input for straight plates - instvar: A $replace value to set to either 'angled' or ' 'straight'. """ # Get data about the trigger this instance uses for flinging fixup_var = res['instvar', ''] offset = utils.conv_int(res['raise_trig', '0']) if offset: offset = Vec(0, 0, offset).rotate_by_str(inst['angles', '0 0 0']) ':type offset Vec' for trig in vbsp.VMF.by_class['trigger_catapult']: if inst['targetname'] in trig['targetname']: if offset: # Edit both the normal and the helper trigger trig['origin'] = ( Vec.from_str(trig['origin']) + offset ).join(' ') for solid in trig.solids: solid.translate(offset) # Inspect the outputs to determine the type. # We also change them if desired, since that's not possible # otherwise. for out in trig.outputs: if out.inst_in == 'animate_angled_relay': out.inst_in = res['angled_targ', 'animate_angled_relay'] out.input = res['angled_in', 'Trigger'] if fixup_var: inst.fixup[fixup_var] = 'angled' break # There's only one output we want to look for... elif out.inst_in == 'animate_straightup_relay': out.inst_in = res[ 'straight_targ', 'animate_straightup_relay' ] out.input = res['straight_in', 'Trigger'] if fixup_var: inst.fixup[fixup_var] = 'straight' break
def res_import_template_setup(res): temp_id = res['id'].casefold() force = res['force', ''].casefold().split() if 'white' in force: force_colour = MAT_TYPES.white elif 'black' in force: force_colour = MAT_TYPES.black elif 'invert' in force: force_colour = 'INVERT' else: force_colour = None if 'world' in force: force_type = TEMP_TYPES.world elif 'detail' in force: force_type = TEMP_TYPES.detail else: force_type = TEMP_TYPES.default for size in ('2x2', '4x4', 'wall', 'special'): if size in force: force_grid = size break else: force_grid = None replace_tex = defaultdict(list) for prop in res.find_key('replace', []): replace_tex[prop.name].append(prop.value) replace_brush = res['replaceBrush', None] if replace_brush: replace_brush_pos = Vec.from_str(replace_brush) replace_brush_pos.z -= 64 # 0 0 0 defaults to the floor. else: replace_brush_pos = None return ( temp_id, dict(replace_tex), force_colour, force_grid, force_type, replace_brush_pos, )
def flag_angles(inst, flag): """Check that a instance is pointed in a direction. The value should be either just the angle to check, or a block of options: - Angle: A unit vector (XYZ value) pointing in a direction, or some keywords: +z, -y, N/S/E/W, up/down, floor/ceiling, or walls - From_dir: The direction the unrotated instance is pointed in. This lets the flag check multiple directions - Allow_inverse: If true, this also returns True if the instance is pointed the opposite direction . """ angle = inst['angles', '0 0 0'] if flag.has_children(): targ_angle = flag['direction', '0 0 0'] from_dir = flag['from_dir', '0 0 1'] if from_dir.casefold() in DIRECTIONS: from_dir = Vec(DIRECTIONS[from_dir.casefold()]) else: from_dir = Vec.from_str(from_dir, 0, 0, 1) allow_inverse = utils.conv_bool(flag['allow_inverse', '0']) else: targ_angle = flag.value from_dir = Vec(0, 0, 1) allow_inverse = False if angle == targ_angle: return True # Check for exact match normal = DIRECTIONS.get(targ_angle.casefold(), None) if normal is None: return False # If it's not a special angle, # so it failed the exact match inst_normal = from_dir.rotate_by_str(angle) if normal == 'WALL': # Special case - it's not on the floor or ceiling return not (inst_normal == (0, 0, 1) or inst_normal == (0, 0, -1)) else: return inst_normal == normal or ( allow_inverse and -inst_normal == normal )
def make_corner(origin, angle, size, config): vbsp.VMF.create_ent( classname='func_instance', origin=origin, angles=angle, file=config['corner', size], ) temp = config['corner_temp', size] if temp: temp_solids = import_template( temp, origin=origin, angles=Vec.from_str(angle), force_type=TEMP_TYPES.world, ).world for solid in temp_solids: vbsp.VMF.remove_brush(solid) motion_trigger(*temp_solids)
def res_hollow_brush(inst, res): """Hollow out the attached brush, as if EmbeddedVoxel was set. This just removes the surface if it's already an embeddedVoxel. This allows multiple items to embed thinly in the same block without affecting each other. """ loc = Vec(0, 0, -64).rotate_by_str(inst['angles']) loc += Vec.from_str(inst['origin']) try: group = SOLIDS[loc.as_tuple()] except KeyError: LOGGER.warning('No brush for hollowing at ({})', loc) return # No brush here? conditions.hollow_block( group, remove_orig_face=utils.conv_bool(res['RemoveFace', False]) )
def make_vac_track(start, all_markers): """Create a vactube path section. """ start_normal = Vec(-1, 0, 0).rotate_by_str(start['ent']['angles']) # First create the start section.. start_logic = start['ent'].copy() vbsp.VMF.add_ent(start_logic) start_logic['file'] = start['conf']['entry', ( 'ceiling' if (start_normal.z > 0) else 'floor' if (start_normal.z < 0) else 'wall' )] end = start for inst, end in follow_vac_path(all_markers, start): join_markers(inst, end, inst is start) end_loc = Vec.from_str(end['ent']['origin']) end_norm = Vec(-1, 0, 0).rotate_by_str(end['ent']['angles']) # join_markers creates straight parts up-to the marker, but not at it's # location - create the last one. make_straight( end_loc, end_norm, 128, end['conf'], ) # If the end is placed in goo, don't add logic - it isn't visible, and # the object is on a one-way trip anyway. if end_loc.as_tuple() not in GOO_LOCS: end_logic = end['ent'].copy() vbsp.VMF.add_ent(end_logic) end_logic['file'] = end['conf']['exit']
def res_track_plat(_, res): """Logic specific to Track Platforms. This allows switching the instances used depending on if the track is horizontal or vertical and sets the track targetnames to a useful value. """ # Get the instances from editoritems (inst_bot_grate, inst_bottom, inst_middle, inst_top, inst_plat, inst_plat_oscil, inst_single) = resolve_inst( res["orig_item"] ) single_plat_inst = res["single_plat", ""] track_targets = res["track_name", ""] track_files = [inst_bottom, inst_middle, inst_top, inst_single] platforms = [inst_plat, inst_plat_oscil] # All the track_set in the map, indexed by origin track_instances = { Vec.from_str(inst["origin"]).as_tuple(): inst for inst in VMF.by_class["func_instance"] if inst["file"].casefold() in track_files } utils.con_log("Track instances:") utils.con_log("\n".join("{!s}: {}".format(k, v["file"]) for k, v in track_instances.items())) # Now we loop through all platforms in the map, and then locate their # track_set for plat_inst in VMF.by_class["func_instance"]: if plat_inst["file"].casefold() not in platforms: continue # Not a platform! utils.con_log('Modifying "' + plat_inst["targetname"] + '"!') plat_loc = Vec.from_str(plat_inst["origin"]) angles = Vec.from_str(plat_inst["angles"]) # The direction away from the wall/floor/ceil normal = Vec(0, 0, 1).rotate(angles.x, angles.y, angles.z) for tr_origin, first_track in track_instances.items(): if plat_loc == tr_origin: # Check direction if normal == Vec(0, 0, 1).rotate(*Vec.from_str(first_track["angles"])): break else: raise Exception('Platform "{}" has no track!'.format(plat_inst["targetname"])) track_type = first_track["file"].casefold() if track_type == inst_single: # Track is one block long, use a single-only instance and # remove track! plat_inst["file"] = single_plat_inst first_track.remove() continue # Next platform track_set = set() if track_type == inst_top or track_type == inst_middle: # search left track_scan(track_set, track_instances, first_track, middle_file=inst_middle, x_dir=-1) if track_type == inst_bottom or track_type == inst_middle: # search right track_scan(track_set, track_instances, first_track, middle_file=inst_middle, x_dir=+1) # Give every track a targetname matching the platform for ind, track in enumerate(track_set, start=1): if track_targets == "": track["targetname"] = plat_inst["targetname"] else: track["targetname"] = plat_inst["targetname"] + "-" + track_targets + str(ind) # Now figure out which way the track faces: # The direction horizontal track is offset side_dir = Vec(0, 1, 0).rotate(*Vec.from_str(first_track["angles"])) # The direction of the platform surface facing = Vec(-1, 0, 0).rotate(angles.x, angles.y, angles.z) if side_dir == facing: track_facing = "HORIZ" elif side_dir == -facing: track_facing = "HORIZ_MIRR" else: track_facing = "VERT" # Now add the suffixes if track_facing == "VERT": if utils.conv_bool(res["vert_suffix", ""]): for inst in track_set: add_suffix(inst, "_vert") if utils.conv_bool(res["plat_suffix", ""]): add_suffix(plat_inst, "_vert") elif track_facing == "HORIZ_MIRR": if utils.conv_bool(res["horiz_suffix", ""]): for inst in track_set: add_suffix(inst, "_horiz_mirrored") if utils.conv_bool(res["plat_suffix", ""]): add_suffix(plat_inst, "_horiz") else: # == 'HORIZ' if utils.conv_bool(res["horiz_suffix", ""]): for inst in track_set: add_suffix(inst, "_horiz") if utils.conv_bool(res["plat_suffix", ""]): add_suffix(plat_inst, "_horiz") return True # Only run once!
def res_make_catwalk(_, res): """Speciallised result to generate catwalks from markers. Only runs once, and then quits the condition list. """ utils.con_log("Starting catwalk generator...") marker = resolve_inst(res["markerInst"]) output_target = res["output_name", "MARKER"] instances = { name: resolve_inst(res[name, ""])[0] for name in ( "straight_128", "straight_256", "straight_512", "corner", "tjunction", "crossjunction", "end", "stair", "end_wall", "support_wall", "support_ceil", "support_floor", "single_wall", "markerInst", ) } # If there are no attachments remove a catwalk piece instances["NONE"] = "" if instances["end_wall"] == "": instances["end_wall"] = instances["end"] connections = {} # The directions this instance is connected by (NSEW) markers = {} for inst in VMF.by_class["func_instance"]: if inst["file"].casefold() not in marker: continue # [North, South, East, West ] connections[inst] = [False, False, False, False] markers[inst["targetname"]] = inst if not markers: return True # No catwalks! utils.con_log("Conn:", connections) utils.con_log("Markers:", markers) # First loop through all the markers, adding connecting sections for inst in markers.values(): for conn in inst.outputs: if conn.output != output_target or conn.input != output_target: # Indicator toggles or similar, delete these print("Removing ", conn.target) for del_inst in VMF.by_target[conn.target]: del_inst.remove() continue inst2 = markers[conn.target] print(inst["targetname"], "<->", inst2["targetname"]) origin1 = Vec.from_str(inst["origin"]) origin2 = Vec.from_str(inst2["origin"]) if origin1.x != origin2.x and origin1.y != origin2.y: utils.con_log("Instances not aligned!") continue y_dir = origin1.x == origin2.x # Which way the connection is if y_dir: dist = abs(origin1.y - origin2.y) else: dist = abs(origin1.x - origin2.x) vert_dist = origin1.z - origin2.z utils.con_log("Dist =", dist, ", Vert =", vert_dist) if dist // 2 < vert_dist: # The stairs are 2 long, 1 high. utils.con_log("Not enough room for stairs!") continue if dist > 128: # add straight sections in between place_catwalk_connections(instances, origin1, origin2) # Update the lists based on the directions that were set conn_lst1 = connections[inst] conn_lst2 = connections[inst2] if origin1.x < origin2.x: conn_lst1[2] = True # E conn_lst2[3] = True # W elif origin2.x < origin1.x: conn_lst1[3] = True # W conn_lst2[2] = True # E if origin1.y < origin2.y: conn_lst1[0] = True # N conn_lst2[1] = True # S elif origin2.y < origin1.y: conn_lst1[1] = True # S conn_lst2[0] = True # N inst.outputs.clear() # Remove the outputs now, they're useless for inst, dir_mask in connections.items(): # Set the marker instances based on the attached walkways. print(inst["targetname"], dir_mask) angle = Vec.from_str(inst["angles"], 0, 0, 0) new_type, inst["angles"] = utils.CONN_LOOKUP[tuple(dir_mask)] inst["file"] = instances[CATWALK_TYPES[new_type]] normal = Vec(0, 0, 1).rotate(angle.x, angle.y, angle.z) ":type normal: Vec" if new_type is utils.CONN_TYPES.side: # If the end piece is pointing at a wall, switch the instance. if normal.z == 0: # Treat booleans as ints to get the direction the connection is # in - True == 1, False == 0 conn_dir = Vec(x=dir_mask[2] - dir_mask[3], y=dir_mask[0] - dir_mask[1], z=0) # +E, -W # +N, -S, if normal == conn_dir: inst["file"] = instances["end_wall"] continue # We never have normal supports on end pieces elif new_type is utils.CONN_TYPES.none: # Unconnected catwalks on the wall switch to a special instance. # This lets players stand next to a portal surface on the wall. if normal.z == 0: inst["file"] = instances["single_wall"] inst["angles"] = INST_ANGLE[normal.as_tuple()] else: inst.remove() continue # These don't get supports otherwise # Add regular supports if normal == (0, 0, 1): supp = instances["support_floor"] elif normal == (0, 0, -1): supp = instances["support_ceil"] else: supp = instances["support_wall"] if supp: VMF.create_ent( classname="func_instance", origin=inst["origin"], angles=INST_ANGLE[normal.as_tuple()], file=supp ) utils.con_log("Finished catwalk generation!") return True # Don't run this again
def res_fix_rotation_axis(ent, res): """Generate a `func_rotating`, `func_door_rotating` or any similar entity. This uses the orientation of the instance to detemine the correct spawnflags to make it rotate in the correct direction. The brush will be 2x2x2 units large, and always set to be non-solid. - `Pos` and `name` are local to the instance, and will set the `origin` and `targetname` respectively. - `Keys` are any other keyvalues to be be set. - `Flags` sets additional spawnflags. Multiple values may be separated by '+', and will be added together. - `Classname` specifies which entity will be created, as well as which other values will be set to specify the correct orientation. - `AddOut` is used to add outputs to the generated entity. It takes the options `Output`, `Target`, `Input`, `Param` and `Delay`. If `Inst_targ` is defined, it will be used with the input to construct an instance proxy input. If `OnceOnly` is set, the output will be deleted when fired. Permitted entities: * `func_rotating` * `func_door_rotating` * `func_rot_button` * `func_platrot` """ des_axis = res['axis', 'z'].casefold() reverse = utils.conv_bool(res['reversed', '0']) door_type = res['classname', 'func_door_rotating'] # Extra stuff to apply to the flags (USE, toggle, etc) flags = sum(map( # Add together multiple values utils.conv_int, res['flags', '0'].split('+') )) name = res['name', ''] if not name.startswith('@'): # If a local name is given, add it to the instance targetname. # It the name given is '', set to the instance's name. # If it has an @, don't change it! name = ent['targetname', ''] + (('-' + name) if name else '') axis = Vec( x=int(des_axis == 'x'), y=int(des_axis == 'y'), z=int(des_axis == 'z'), ).rotate_by_str(ent['angles', '0 0 0']) pos = Vec.from_str( res['Pos', '0 0 0'] ).rotate_by_str(ent['angles', '0 0 0']) pos += Vec.from_str(ent['origin', '0 0 0']) door_ent = vbsp.VMF.create_ent( classname=door_type, targetname=name, origin=pos.join(' '), ) conditions.set_ent_keys(door_ent, ent, res) for output in res.find_all('AddOut'): door_ent.add_out(VLib.Output( out=output['Output', 'OnUse'], inp=output['Input', 'Use'], targ=output['Target', ''], inst_in=output['Inst_targ', None], param=output['Param', ''], delay=utils.conv_float(output['Delay', '']), times=( 1 if utils.conv_bool(output['OnceOnly', False]) else -1), )) # Generate brush door_ent.solids = [vbsp.VMF.make_prism(pos - 1, pos + 1).solid] if axis.x > 0 or axis.y > 0 or axis.z > 0: # If it points forward, we need to reverse the rotating door reverse = not reverse flag_values = FLAG_ROTATING[door_type] # Make the door always non-solid! flags |= flag_values.get('solid_flags', 0) # Add or remove flags as needed. # flags |= bit sets it to 1. # flags |= ~bit sets it to 0. if axis.x != 0: flags |= flag_values.get('x', 0) else: flags &= ~flag_values.get('x', 0) if axis.y != 0: flags |= flag_values.get('y', 0) else: flags &= ~flag_values.get('y', 0) if axis.z != 0: flags |= flag_values.get('z', 0) else: flags &= ~flag_values.get('z', 0) if door_type == 'momentary_rot_button': door_ent['startdirection'] = '1' if reverse else '-1' else: if reverse: flags |= flag_values.get('rev', 0) else: flags &= ~flag_values.get('rev', 0) door_ent['spawnflags'] = str(flags)
def res_import_template(inst, res): """Import a template VMF file, retexturing it to match orientatation. It will be placed overlapping the given instance. Options: - ID: The ID of the template to be inserted. - force: a space-seperated list of overrides. If 'white' or 'black' is present, the colour of tiles will be overriden. 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. """ ( temp_id, replace_tex, force_colour, force_grid, force_type, replace_brush_pos, ) = res.value if temp_id not in TEMPLATES: # The template map is read in after setup is performed, so # it must be checked here! # We don't want an error, just quit LOGGER.warning('"{}" not a valid template!', temp_id) return origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles', '0 0 0']) temp_data = conditions.import_template( temp_id, origin, angles, targetname=inst['targetname', ''], force_type=force_type, ) conditions.retexture_template( temp_data, origin, replace_tex, force_colour, force_grid, ) # 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: return pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z) pos += origin try: brush_group = SOLIDS[pos.as_tuple()] except KeyError: return vbsp.VMF.remove_brush(brush_group.solid) new_ids = [] all_brushes = temp_data.world if temp_data.detail is not None: for ent in temp_data.detail: all_brushes.extend(ent.solids) for brush in all_brushes: # type: VLib.Solid for face in brush.sides: # Only faces pointing the same way! if face.normal() == brush_group.normal: # Skip tool brushes (nodraw, player clips..) if face.mat.casefold().startswith('tools/'): continue new_ids.append(str(face.id)) if new_ids: conditions.reallocate_overlays({ str(brush_group.face.id): new_ids, })
def res_track_plat(_, res): """Logic specific to Track Platforms. This allows switching the instances used depending on if the track is horizontal or vertical and sets the track targetnames to a useful value. Values: - Orig_item: The "<ITEM_ID>" for the track platform, with angle brackets - Single_plat: An instance used for platform with 1 rail - Track_name: The name to give to the tracks. - Vert_suffix: Add suffixes to vertical tracks (_vert) - Horiz_suffix: Add suffixes to horizontal tracks (_horiz, _horiz_mirrored) - plat_suffix: Also add the above _vert or _horiz suffixes to the platform. - plat_var: If set, save the orientation to the given $fixup variable """ # Get the instances from editoritems ( inst_bot_grate, inst_bottom, inst_middle, inst_top, inst_plat, inst_plat_oscil, inst_single ) = resolve_inst(res['orig_item']) single_plat_inst = res['single_plat', ''] track_targets = res['track_name', ''] track_files = [inst_bottom, inst_middle, inst_top, inst_single] platforms = [inst_plat, inst_plat_oscil] # All the track_set in the map, indexed by origin track_instances = { Vec.from_str(inst['origin']).as_tuple(): inst for inst in vbsp.VMF.by_class['func_instance'] if inst['file'].casefold() in track_files } LOGGER.debug('Track instances:') LOGGER.debug('\n'.join( '{!s}: {}'.format(k, v['file']) for k, v in track_instances.items() )) if not track_instances: return RES_EXHAUSTED # Now we loop through all platforms in the map, and then locate their # track_set for plat_inst in vbsp.VMF.by_class['func_instance']: if plat_inst['file'].casefold() not in platforms: continue # Not a platform! LOGGER.debug('Modifying "' + plat_inst['targetname'] + '"!') plat_loc = Vec.from_str(plat_inst['origin']) # The direction away from the wall/floor/ceil normal = Vec(0, 0, 1).rotate_by_str( plat_inst['angles'] ) for tr_origin, first_track in track_instances.items(): if plat_loc == tr_origin: # Check direction if normal == Vec(0, 0, 1).rotate( *Vec.from_str(first_track['angles']) ): break else: raise Exception('Platform "{}" has no track!'.format( plat_inst['targetname'] )) track_type = first_track['file'].casefold() if track_type == inst_single: # Track is one block long, use a single-only instance and # remove track! plat_inst['file'] = single_plat_inst first_track.remove() continue # Next platform track_set = set() if track_type == inst_top or track_type == inst_middle: # search left track_scan( track_set, track_instances, first_track, middle_file=inst_middle, x_dir=-1, ) if track_type == inst_bottom or track_type == inst_middle: # search right track_scan( track_set, track_instances, first_track, middle_file=inst_middle, x_dir=+1, ) # Give every track a targetname matching the platform for ind, track in enumerate(track_set, start=1): if track_targets == '': track['targetname'] = plat_inst['targetname'] else: track['targetname'] = ( plat_inst['targetname'] + '-' + track_targets + str(ind) ) # Now figure out which way the track faces: # The direction horizontal track is offset side_dir = Vec(0, 1, 0).rotate_by_str(first_track['angles']) # The direction of the platform surface facing = Vec(-1, 0, 0).rotate_by_str(plat_inst['angles']) if side_dir == facing: track_facing = 'HORIZ' elif side_dir == -facing: track_facing = 'HORIZ_MIRR' else: track_facing = 'VERT' # Now add the suffixes if track_facing == 'VERT': if utils.conv_bool(res['vert_suffix', '']): for inst in track_set: conditions.add_suffix(inst, '_vert') if utils.conv_bool(res['plat_suffix', '']): conditions.add_suffix(plat_inst, '_vert') elif track_facing == 'HORIZ_MIRR': if utils.conv_bool(res['horiz_suffix', '']): for inst in track_set: conditions.add_suffix(inst, '_horiz_mirrored') if utils.conv_bool(res['plat_suffix', '']): conditions.add_suffix(plat_inst, '_horiz') else: # == 'HORIZ' if utils.conv_bool(res['horiz_suffix', '']): for inst in track_set: conditions.add_suffix(inst, '_horiz') if utils.conv_bool(res['plat_suffix', '']): conditions.add_suffix(plat_inst, '_horiz') plat_var = res['plat_var', ''] if plat_var != '': # Skip the '_mirrored' section if needed plat_inst.fixup[plat_var] = track_facing[:5].lower() return RES_EXHAUSTED # Don't re-run
def res_add_brush(inst, res): """Spawn in a brush at the indicated points. - point1 and point2 are locations local to the instance, with '0 0 0' as the floor-position. - type is either 'black' or 'white'. - detail should be set to True/False. If true the brush will be a func_detail instead of a world brush. The sides will be textured with 1x1, 2x2 or 4x4 wall, ceiling and floor textures as needed. """ import vbsp point1 = Vec.from_str(res['point1']) point2 = Vec.from_str(res['point2']) point1.z -= 64 # Offset to the location of the floor point2.z -= 64 # Rotate to match the instance point1.rotate_by_str(inst['angles']) point2.rotate_by_str(inst['angles']) origin = Vec.from_str(inst['origin']) point1 += origin # Then offset to the location of the instance point2 += origin tex_type = res['type', None] if tex_type not in ('white', 'black'): LOGGER.warning( 'AddBrush: "{}" is not a valid brush ' 'color! (white or black)', tex_type, ) tex_type = 'black' dim = point2 - point1 dim.max(-dim) # Figure out what grid size and scale is needed # Check the dimensions in two axes to figure out the largest # tile size that can fit in it. x_maxsize = min(dim.y, dim.z) y_maxsize = min(dim.x, dim.z) if x_maxsize <= 32: x_grid = '4x4' elif x_maxsize <= 64: x_grid = '2x2' else: x_grid = 'wall' if y_maxsize <= 32: y_grid = '4x4' elif y_maxsize <= 64: y_grid = '2x2' else: y_grid = 'wall' grid_offset = origin // 128 # type: Vec # All brushes in each grid have the same textures for each side. random.seed(grid_offset.join(' ') + '-partial_block') solids = vbsp.VMF.make_prism(point1, point2) ':type solids: VLib.PrismFace' # Ensure the faces aren't re-textured later vbsp.IGNORED_FACES.update(solids.solid.sides) solids.north.mat = vbsp.get_tex(tex_type + '.' + y_grid) solids.south.mat = vbsp.get_tex(tex_type + '.' + y_grid) solids.east.mat = vbsp.get_tex(tex_type + '.' + x_grid) solids.west.mat = vbsp.get_tex(tex_type + '.' + x_grid) solids.top.mat = vbsp.get_tex(tex_type + '.floor') solids.bottom.mat = vbsp.get_tex(tex_type + '.ceiling') if utils.conv_bool(res['detail', False], False): # Add the brush to a func_detail entity vbsp.VMF.create_ent( classname='func_detail' ).solids = [ solids.solid ] else: # Add to the world vbsp.VMF.add_brush(solids.solid)
def res_set_texture(inst, res): """Set the brush face at a location to a particular texture. pos is the position, relative to the instance (0 0 0 is the floor-surface). dir is the normal of the texture. If gridPos is true, the position will be snapped so it aligns with the 128 brushes (Useful with fizzler/light strip items). tex is the texture used. If tex begins and ends with '<>', certain textures will be used based on style: - If tex is '<special>', the brush will be given a special texture like angled and clear panels. - '<white>' and '<black>' will use the regular textures for the given color. - '<white-2x2>', '<white-4x4>', '<black-2x2>', '<black-4x4'> will use the given wall-sizes. If on floors or ceilings these always use 4x4. - '<2x2>' or '<4x4>' will force to the given wall-size, keeping color. - '<special-white>' and '<special-black>' will use a special texture of the given color. If tex begins and ends with '[]', it is an option in the 'Textures' list. These are composed of a group and texture, separated by '.'. 'white.wall' are the white wall textures; 'special.goo' is the goo texture. """ import vbsp pos = Vec.from_str(res['pos', '0 0 0']) pos.z -= 64 # Subtract so origin is the floor-position pos = pos.rotate_by_str(inst['angles', '0 0 0']) # Relative to the instance origin pos += Vec.from_str(inst['origin', '0 0 0']) norm = Vec.from_str(res['dir', '0 0 -1']).rotate_by_str( inst['angles', '0 0 0'] ) if utils.conv_bool(res['gridpos', '0']): for axis in 'xyz': # Don't realign things in the normal's axis - # those are already fine. if not norm[axis]: pos[axis] //= 128 pos[axis] *= 128 pos[axis] += 64 brush = SOLIDS.get(pos.as_tuple(), None) ':type brush: solidGroup' if not brush or brush.normal != norm: return tex = res['tex'] if tex.startswith('[') and tex.endswith(']'): brush.face.mat = vbsp.get_tex(tex[1:-1]) brush.face.mat = tex elif tex.startswith('<') and tex.endswith('>'): # Special texture names! tex = tex[1:-1].casefold() if tex == 'white': brush.face.mat = 'tile/white_wall_tile003a' elif tex == 'black': brush.face.mat = 'metal/black_wall_metal_002c' if tex == 'black' or tex == 'white': # For these two, run the regular logic to apply textures # correctly. vbsp.alter_mat( brush.face, vbsp.face_seed(brush.face), vbsp.get_bool_opt('tile_texture_lock', True), ) if tex == 'special': vbsp.set_special_mat(brush.face, str(brush.color)) elif tex == 'special-white': vbsp.set_special_mat(brush.face, 'white') return elif tex == 'special-black': vbsp.set_special_mat(brush.face, 'black') # Do <4x4>, <white-2x4>, etc color = str(brush.color) if tex.startswith('black') or tex.endswith('white'): # Override the color used for 2x2/4x4 brushes color = tex[:5] if tex.endswith('2x2') or tex.endswith('4x4'): # 4x4 and 2x2 instructions are ignored on floors and ceilings. orient = vbsp.get_face_orient(brush.face) if orient == vbsp.ORIENT.wall: brush.face.mat = vbsp.get_tex( color + '.' + tex[-3:] ) else: brush.face.mat = vbsp.get_tex( color + '.' + str(orient) ) else: brush.face.mat = tex # Don't allow this to get overwritten later. vbsp.IGNORED_FACES.add(brush.face)
def res_unst_scaffold(_, res): """The condition to generate Unstationary Scaffolds. This is executed once to modify all instances. """ # The instance types we're modifying if res.value not in SCAFFOLD_CONFIGS: # We've already executed this config group return RES_EXHAUSTED LOGGER.info("Running Scaffold Generator ({})...", res.value) TARG_INST, LINKS = SCAFFOLD_CONFIGS[res.value] del SCAFFOLD_CONFIGS[res.value] # Don't let this run twice instances = {} # Find all the instances we're wanting to change, and map them to # targetnames for ent in vbsp.VMF.by_class["func_instance"]: file = ent["file"].casefold() targ = ent["targetname"] if file not in TARG_INST: continue config = TARG_INST[file] next_inst = set(out.target for out in ent.outputs) # Destroy these outputs, they're useless now! ent.outputs.clear() instances[targ] = {"ent": ent, "conf": config, "next": next_inst, "prev": None} # Now link each instance to its in and outputs for targ, inst in instances.items(): scaff_targs = 0 for ent_targ in inst["next"]: if ent_targ in instances: instances[ent_targ]["prev"] = targ inst["next"] = ent_targ scaff_targs += 1 else: # If it's not a scaffold, it's probably an indicator_toggle. # We want to remove any them as well as the assoicated # antlines! for toggle in vbsp.VMF.by_target[ent_targ]: conditions.remove_ant_toggle(toggle) if scaff_targs > 1: raise Exception("A scaffold item has multiple destinations!") elif scaff_targs == 0: inst["next"] = None # End instance starting_inst = [] # We need to find the start instances, so we can set everything up for inst in instances.values(): if inst["prev"] is None and inst["next"] is None: # Static item! continue elif inst["prev"] is None: starting_inst.append(inst) # We need to make the link entities unique for each scaffold set, # otherwise the AllVar property won't work. group_counter = 0 # Set all the instances and properties for start_inst in starting_inst: group_counter += 1 ent = start_inst["ent"] for vals in LINKS.values(): if vals["all"] is not None: ent.fixup[vals["all"]] = SCAFF_PATTERN.format(name=vals["name"], group=group_counter, index="*") should_reverse = utils.conv_bool(ent.fixup["$start_reversed"]) # Now set each instance in the chain, including first and last for index, inst in enumerate(scaff_scan(instances, start_inst)): ent, conf = inst["ent"], inst["conf"] orient = "floor" if Vec(0, 0, 1).rotate_by_str(ent["angles"]) == (0, 0, 1) else "wall" # Find the offset used for the logic ents offset = (conf["off_" + orient]).copy() if conf["is_piston"]: # Adjust based on the piston position offset.z += 128 * utils.conv_int( ent.fixup["$top_level" if ent.fixup["$start_up"] == "1" else "$bottom_level"] ) offset.rotate_by_str(ent["angles"]) offset += Vec.from_str(ent["origin"]) if inst["prev"] is None: link_type = "start" elif inst["next"] is None: link_type = "end" else: link_type = "mid" if orient == "floor" and link_type != "mid" and conf["inst_end"] is not None: # Add an extra instance pointing in the direction # of the connected track. This would be the endcap # model. other_ent = instances[inst["next" if link_type == "start" else "prev"]]["ent"] other_pos = Vec.from_str(other_ent["origin"]) our_pos = Vec.from_str(ent["origin"]) link_dir = other_pos - our_pos link_ang = math.degrees(math.atan2(link_dir.y, link_dir.x)) # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals link_ang = (link_ang + 45) // 90 * 90 vbsp.VMF.create_ent( classname="func_instance", targetname=ent["targetname"], file=conf["inst_end"], origin=offset.join(" "), angles="0 {:.0f} 0".format(link_ang), ) # Don't place the offset instance, this replaces that! elif conf["inst_offset"] is not None: # Add an additional rotated entity at the offset. # This is useful for the piston item. vbsp.VMF.create_ent( classname="func_instance", targetname=ent["targetname"], file=conf["inst_offset"], origin=offset.join(" "), angles=ent["angles"], ) logic_inst = vbsp.VMF.create_ent( classname="func_instance", targetname=ent["targetname"], file=conf.get("logic_" + link_type + ("_rev" if should_reverse else ""), ""), origin=offset.join(" "), angles=("0 0 0" if conf["rotate_logic"] else ent["angles"]), ) for key, val in ent.fixup.items(): # Copy over fixup values logic_inst.fixup[key] = val # Add the link-values for linkVar, link in LINKS.items(): logic_inst.fixup[linkVar] = SCAFF_PATTERN.format(name=link["name"], group=group_counter, index=index) if inst["next"] is not None: logic_inst.fixup[link["next"]] = SCAFF_PATTERN.format( name=link["name"], group=group_counter, index=index + 1 ) new_file = conf.get("inst_" + orient, "") if new_file != "": ent["file"] = new_file LOGGER.info("Finished Scaffold generation!") return RES_EXHAUSTED
def add_voice( has_items: dict, style_vars_: dict, vmf_file: vmfLib.VMF, map_seed: str, use_priority=True, ): """Add a voice line to the map.""" global ALLOW_MID_VOICES, VMF, map_attr, style_vars LOGGER.info('Adding Voice Lines!') VMF = vmf_file map_attr = has_items style_vars = style_vars_ norm_config = ConfigFile('voice.cfg', root='bee2') mid_config = ConfigFile('mid_voice.cfg', root='bee2') quote_base = QUOTE_DATA['base', False] quote_loc = Vec.from_str(QUOTE_DATA['quote_loc', '-10000 0 0'], x=-10000) if quote_base: LOGGER.info('Adding Base instance!') VMF.create_ent( classname='func_instance', targetname='voice', file=INST_PREFIX + quote_base, angles='0 0 0', origin=quote_loc, fixup_style='0', ) # Always add a box around the lines - it may be needed for quoteEvents. VMF.add_brushes(VMF.make_hollow( quote_loc - 64, quote_loc + 64, thick=32, )) ALLOW_MID_VOICES = not style_vars.get('nomidvoices', False) mid_quotes = [] # Enable using the beep before and after choreo lines. allow_dings = utils.conv_bool(QUOTE_DATA['use_dings', '0']) if allow_dings: VMF.create_ent( classname='logic_choreographed_scene', targetname='@ding_on', origin=quote_loc + (-8, -16, 0), scenefile='scenes/npc/glados_manual/ding_on.vcd', busyactor="1", # Wait for actor to stop talking onplayerdeath='0', ) VMF.create_ent( classname='logic_choreographed_scene', targetname='@ding_off', origin=quote_loc + (8, -16, 0), scenefile='scenes/npc/glados_manual/ding_off.vcd', busyactor="1", # Wait for actor to stop talking onplayerdeath='0', ) # QuoteEvents allows specifiying an instance for particular items, # so a voice line can be played at a certain time. It's only active # in certain styles, but uses the default if not set. for event in QUOTE_DATA.find_all('QuoteEvents', 'Event'): event_id = event['id', ''].casefold() # We ignore the config if no result was executed. if event_id and event_id in QUOTE_EVENTS: # Instances from the voiceline config are in this subfolder, # but not the default item - that's set from the conditions QUOTE_EVENTS[event_id] = INST_PREFIX + event['file'] LOGGER.info('Quote events: {}', list(QUOTE_EVENTS.keys())) if has_responses(): LOGGER.info('Generating responses data..') with open(RESP_LOC, 'w') as f: generate_resp_script(f, allow_dings) else: LOGGER.info('No responses data..') try: os.remove(RESP_LOC) except FileNotFoundError: pass for ind, file in enumerate(QUOTE_EVENTS.values()): VMF.create_ent( classname='func_instance', targetname='voice_event_' + str(ind), file=file, angles='0 0 0', origin=quote_loc, fixup_style='0', ) # For each group, locate the voice lines. for group in itertools.chain( QUOTE_DATA.find_all('group'), QUOTE_DATA.find_all('midchamber'), ): quote_targetname = group['Choreo_Name', '@choreo'] use_dings = utils.conv_bool(group['use_dings', ''], allow_dings) possible_quotes = sorted( find_group_quotes( group, mid_quotes, use_dings, conf=mid_config if group.name == 'midchamber' else norm_config, mid_name=quote_targetname, ), key=sort_func, reverse=True, ) if possible_quotes: choreo_loc = Vec.from_str(group['choreo_loc', quote_loc]) if use_priority: chosen = possible_quotes[0].lines else: # Chose one of the quote blocks.. random.seed('{}-VOICE_QUOTE_{}'.format( map_seed, len(possible_quotes), )) chosen = random.choice(possible_quotes).lines # Join the IDs for # the voice lines to the map seed, # so each quote block will chose different lines. random.seed(map_seed + '-VOICE_LINE_' + '|'.join( prop['id', 'ID'] for prop in chosen )) # Add one of the associated quotes add_quote( random.choice(chosen), quote_targetname, choreo_loc, use_dings, ) if ADDED_BULLSEYES or utils.conv_bool(QUOTE_DATA['UseMicrophones', '']): # Add microphones that broadcast audio directly at players. # This ensures it is heard regardless of location. # This is used for Cave and core Wheatley. if vbsp.GAME_MODE == 'SP': VMF.create_ent( classname='env_microphone', targetname='player_speaker_sp', speakername='!player', maxRange='96', origin=quote_loc, ) else: VMF.create_ent( classname='env_microphone', targetname='player_speaker_blue', speakername='!player_blue', maxRange='96', origin=quote_loc, ) VMF.create_ent( classname='env_microphone', targetname='player_speaker_orange', speakername='!player_orange', maxRange='96', origin=quote_loc, ) LOGGER.info('{} Mid quotes', len(mid_quotes)) for mid_item, use_ding, mid_name in mid_quotes: # Add all the mid quotes add_quote(mid_item, mid_name, quote_loc, use_ding) LOGGER.info('Done!')
def res_fizzler_pair(begin_inst, res): """Modify the instance of a fizzler to link with its pair. Each pair will be given a name along the lines of "fizz_name-model1334". Values: - StartInst, EndInst: The instances used for each end - MidInst: An instance placed every 128 units between emitters. """ orig_target = begin_inst['targetname'] if 'modelEnd' in orig_target: return # We only execute starting from the start side. orig_target = orig_target[:-11] # remove "_modelStart" end_name = orig_target + '_modelEnd' # What we search for # The name all these instances get pair_name = orig_target + '-model' + str(begin_inst.id) orig_file = begin_inst['file'] begin_file = res['StartInst', orig_file] end_file = res['EndInst', orig_file] mid_file = res['MidInst', ''] begin_inst['file'] = begin_file begin_inst['targetname'] = pair_name direction = Vec(0, 0, 1).rotate_by_str(begin_inst['angles']) begin_pos = Vec.from_str(begin_inst['origin']) axis_1, axis_2, main_axis = PAIR_AXES[direction.as_tuple()] for end_inst in vbsp.VMF.by_class['func_instance']: if end_inst['targetname', ''] != end_name: # Only examine this barrier hazard's instances! continue end_pos = Vec.from_str(end_inst['origin']) if ( begin_pos[axis_1] == end_pos[axis_1] and begin_pos[axis_2] == end_pos[axis_2] ): length = int(end_pos[main_axis] - begin_pos[main_axis]) break else: LOGGER.warning('No matching pair for {}!!', orig_target) return end_inst['targetname'] = pair_name end_inst['file'] = end_file if mid_file != '': # Go 64 from each side, and always have at least 1 section # A 128 gap will have length = 0 for dis in range(0, abs(length) + 1, 128): new_pos = begin_pos + direction*dis vbsp.VMF.create_ent( classname='func_instance', targetname=pair_name, angles=begin_inst['angles'], file=mid_file, origin=new_pos.join(' '), )
def res_faith_mods(inst: VLib.Entity, res: Property): """Modify the trigger_catrapult that is created for ItemFaithPlate items. Values: - raise_trig: Raise or lower the trigger_catapults by this amount. - angled_targ, angled_in: Instance entity and input for angled plates - straight_targ, straight_in: Instance entity and input for straight plates - instvar: A $replace value to set to either 'angled' or ' 'straight'. - enabledVar: A $replace value which will be copied to the main trigger's Start Disabled value (and inverted). - trig_temp: An ID for a template brush to add. This will be offset by the trigger's position (in the case of the 'helper' trigger). """ # Get data about the trigger this instance uses for flinging fixup_var = res['instvar', ''] trig_enabled = res['enabledVar', None] trig_temp = res['trig_temp', ''] offset = utils.conv_int(res['raise_trig', '0']) if offset: offset = Vec(0, 0, offset).rotate_by_str(inst['angles', '0 0 0']) else: offset = Vec() if trig_enabled is not None: trig_enabled = utils.conv_bool(inst.fixup[trig_enabled]) else: trig_enabled = None for trig in vbsp.VMF.by_class['trigger_catapult']: if inst['targetname'] not in trig['targetname']: continue # Edit both the normal and the helper trigger.. trig_origin = trig['origin'] = Vec.from_str(trig['origin']) + offset if offset and not trig_temp: # No template, shift the current brushes. for solid in trig.solids: solid.translate(offset) elif trig_temp: trig.solids = conditions.import_template( temp_name=trig_temp, origin=trig_origin, angles=Vec.from_str(inst['angles']), force_type=conditions.TEMP_TYPES.world, ).world # Remove the trigger solids from worldspawn.. for solid in trig.solids: vbsp.VMF.remove_brush(solid) if trig_enabled is not None and 'helper' not in trig['targetname']: trig['startdisabled'] = utils.bool_as_int(not trig_enabled) # Inspect the outputs to determine the type. # We also change them if desired, since that's not possible # otherwise. for out in trig.outputs: if out.inst_in == 'animate_angled_relay': # Instead of an instance: output, use local names. # This allows us to strip the proxy, as well as use # overlay instances. out.inst_in = None out.target = conditions.local_name( inst, res['angled_targ', 'animate_angled_relay'] ) out.input = res['angled_in', 'Trigger'] if fixup_var: inst.fixup[fixup_var] = 'angled' break # There's only one output we want to look for... elif out.inst_in == 'animate_straightup_relay': out.inst_in = None out.target = conditions.local_name( inst, res[ 'straight_targ', 'animate_straightup_relay' ], ) out.input = res['straight_in', 'Trigger'] if fixup_var: inst.fixup[fixup_var] = 'straight' break