def res_random_setup(res): weight = '' results = [] chance = 100 seed = '' for prop in res: if prop.name == 'chance': # Allow ending with '%' sign chance = utils.conv_int( prop.value.rstrip('%'), chance, ) elif prop.name == 'weights': weight = prop.value elif prop.name == 'seed': seed = prop.value else: results.append(prop) if not results: return None # Invalid! weight = conditions.weighted_random(len(results), weight) # We also need to execute result setups on all child properties! for prop in results[:]: if prop.name == 'group': for sub_prop in prop.value[:]: Condition.setup_result(prop.value, sub_prop) else: Condition.setup_result(results, prop) return seed, chance, weight, results
def parse(cls, data): """Parse a voice line definition.""" selitem_data = get_selitem_data(data.info) chars = { char.strip() for char in data.info['characters', ''].split(',') if char.strip() } # For Cave Johnson voicelines, this indicates what skin to use on the # portrait. port_skin = utils.conv_int(data.info['caveSkin', None], None) config = get_config( data.info, data.zip_file, 'voice', pak_id=data.pak_id, prop_name='file', ) return cls( data.id, selitem_data, config, chars=chars, skin=port_skin, )
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_add_variant_setup(res): count = utils.conv_int(res['Number', ''], None) if count: return conditions.weighted_random( count, res['weights', ''], ) else: return None
def test_conv_int_fails_on_float(self): # Check that float values fail marker = object() for string, result in floats: if isinstance(string, str): # We don't want to check float-rounding self.assertIs( utils.conv_int(string, marker), marker, msg=string, )
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_vactube_setup(res): group = res['group', 'DEFAULT_GROUP'] if group not in VAC_CONFIGS: # Store our values in the CONFIGS dictionary config, inst_configs = VAC_CONFIGS[group] = {}, {} else: # Grab the already-filled values, and add to them config, inst_configs = VAC_CONFIGS[group] for block in res.find_all("Instance"): # Configuration info for each instance set.. conf = { # The three sizes of corner instance ('corner', 1): block['corner_small_inst', ''], ('corner', 2): block['corner_medium_inst', ''], ('corner', 3): block['corner_large_inst', ''], ('corner_temp', 1): block['temp_corner_small', ''], ('corner_temp', 2): block['temp_corner_medium', ''], ('corner_temp', 3): block['temp_corner_large', ''], # Straight instances connected to the next part 'straight': block['straight_inst', ''], # Supports attach to the 4 sides of the straight part, # if there's a brush there. 'support': block['support_inst', ''], 'is_tsection': utils.conv_bool(block['is_tsection', '0']), ('entry', 'wall'): block['entry_inst'], ('entry', 'floor'): block['entry_floor_inst'], ('entry', 'ceiling'): block['entry_ceil_inst'], 'exit': block['exit_inst'], } for prop in block.find_all("File"): try: size, file = prop.value.split(":", 1) except ValueError: size = 1 file = prop.value inst_configs[resolve_inst(file)[0]] = conf, utils.conv_int(size, 1) return group
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 flag_random(inst, res: Property): """Randomly is either true or false.""" if res.has_children(): chance = res['chance', '100'] seed = res['seed', ''] else: chance = res.value seed = '' # Allow ending with '%' sign chance = utils.conv_int(chance.rstrip('%'), 100) random.seed('random_chance_{}:{}_{}_{}'.format( seed, inst['targetname', ''], inst['origin'], inst['angles'], )) return random.randrange(100) < chance
def res_add_output_setup(res): output = res['output'] input_name = res['input'] inst_in = res['inst_out', ''] inst_out = res['inst_out', ''] targ = res['target'] only_once = utils.conv_bool(res['only_once', None]) times = 1 if only_once else utils.conv_int(res['times', None], -1) delay = utils.conv_float(res['delay', '0.0']) parm = res['parm', ''] return ( output, targ, input_name, parm, delay, times, inst_in, inst_out, )
def parse(cls, prop_block): """Create a condition from a Property block.""" flags = [] results = [] else_results = [] priority = 0 for prop in prop_block: if prop.name == 'result': results.extend(prop.value) # join multiple ones together elif prop.name == 'else': else_results.extend(prop.value) elif prop.name == 'priority': priority = utils.conv_int(prop.value, priority) else: flags.append(prop) return cls( flags=flags, results=results, else_results=else_results, priority=priority, )
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 res_add_overlay_inst(inst, res): """Add another instance on top of this one. Values: File: The filename. Fixup Style: The Fixup style for the instance. '0' (default) is Prefix, '1' is Suffix, and '2' is None. Copy_Fixup: If true, all the $replace values from the original instance will be copied over. move_outputs: If true, outputs will be moved to this instance. offset: The offset (relative to the base) that the instance will be placed. Can be set to '<piston_top>' and '<piston_bottom>' to offset based on the configuration of piston platform handles. angles: If set, overrides the base instance angles. This does not affect the offset property. fixup: Keyvalues in this block will be copied to the overlay entity. If the value starts with $, the variable will be copied over. If this is present, copy_fixup will be disabled """ angle = res["angles", inst["angles", "0 0 0"]] overlay_inst = vbsp.VMF.create_ent( classname="func_instance", targetname=inst["targetname", ""], file=resolve_inst(res["file", ""])[0], angles=angle, origin=inst["origin"], fixup_style=res["fixup_style", "0"], ) # Don't run if the fixup block exists.. if utils.conv_bool(res["copy_fixup", "1"]) and "fixup" not in res: # Copy the fixup values across from the original instance for fixup, value in inst.fixup.items(): overlay_inst.fixup[fixup] = value # Copy additional fixup values over for prop in res.find_key("Fixup", []): # type: Property if prop.value.startswith("$"): overlay_inst.fixup[prop.real_name] = inst.fixup[prop.value] else: overlay_inst.fixup[prop.real_name] = prop.value if utils.conv_bool(res["move_outputs", "0"]): overlay_inst.outputs = inst.outputs inst.outputs = [] if "offset" in res: folded_off = res["offset"].casefold() # Offset the overlay by the given distance # Some special placeholder values: if folded_off == "<piston_bottom>": offset = Vec(z=utils.conv_int(inst.fixup["$bottom_level"]) * 128) elif folded_off == "<piston_top>": offset = Vec(z=utils.conv_int(inst.fixup["$top_level"], 1) * 128) else: # Regular vector offset = Vec.from_str(res["offset"]) offset.rotate_by_str(inst["angles", "0 0 0"]) overlay_inst["origin"] = (offset + Vec.from_str(inst["origin"])).join(" ") return overlay_inst
def res_cust_fizzler(base_inst, res): """Customises the various components of a custom fizzler item. This should be executed on the base instance. Brush and MakeLaserField are ignored on laserfield barriers. Options: * ModelName: sets the targetname given to the model instances. * UniqueModel: If true, each model instance will get a suffix to allow unique targetnames. * Brush: A brush entity that will be generated (the original is deleted.) * Name is the instance name for the brush * Left/Right/Center/Short/Nodraw are the textures used * Keys are a block of keyvalues to be set. Targetname and Origin are auto-set. * Thickness will change the thickness of the fizzler if set. By default it is 2 units thick. * MakeLaserField generates a brush stretched across the whole area. * Name, keys and thickness are the same as the regular Brush. * Texture/Nodraw are the textures. * Width is the pixel width of the laser texture, used to scale it correctly. """ model_name = res['modelname', None] make_unique = utils.conv_bool(res['UniqueModel', '0']) fizz_name = base_inst['targetname', ''] # search for the model instances model_targetnames = ( fizz_name + '_modelStart', fizz_name + '_modelEnd', ) is_laser = False for inst in vbsp.VMF.by_class['func_instance']: if inst['targetname', ''] in model_targetnames: if inst.fixup['skin', '0'] == '2': is_laser = True if model_name is not None: if model_name == '': inst['targetname'] = base_inst['targetname'] else: inst['targetname'] = ( base_inst['targetname'] + '-' + model_name ) if make_unique: inst.make_unique() for key, value in base_inst.fixup.items(): inst.fixup[key] = value new_brush_config = list(res.find_all('brush')) if len(new_brush_config) == 0: return # No brush modifications if is_laser: # This is a laserfield! We can't edit those brushes! LOGGER.warning('CustFizzler excecuted on LaserField!') return for orig_brush in ( vbsp.VMF.by_class['trigger_portal_cleanser'] & vbsp.VMF.by_target[fizz_name + '_brush']): orig_brush.remove() for config in new_brush_config: new_brush = orig_brush.copy() vbsp.VMF.add_ent(new_brush) # Don't allow restyling it vbsp.IGNORED_BRUSH_ENTS.add(new_brush) new_brush.clear_keys() # Wipe the original keyvalues new_brush['origin'] = orig_brush['origin'] new_brush['targetname'] = ( fizz_name + '-' + config['name', 'brush'] ) # All ents must have a classname! new_brush['classname'] = 'trigger_portal_cleanser' conditions.set_ent_keys( new_brush, base_inst, config, ) laserfield_conf = config.find_key('MakeLaserField', None) if laserfield_conf.value is not None: # Resize the brush into a laserfield format, without # the 128*64 parts. If the brush is 128x128, we can # skip the resizing since it's already correct. laser_tex = laserfield_conf['texture', 'effects/laserplane'] nodraw_tex = laserfield_conf['nodraw', 'tools/toolsnodraw'] tex_width = utils.conv_int( laserfield_conf['texwidth', '512'], 512 ) is_short = False for side in new_brush.sides(): if side.mat.casefold() == 'effects/fizzler': is_short = True break if is_short: for side in new_brush.sides(): if side.mat.casefold() == 'effects/fizzler': side.mat = laser_tex side.uaxis.offset = 0 side.scale = 0.25 else: side.mat = nodraw_tex else: # The hard part - stretching the brush. convert_to_laserfield( new_brush, laser_tex, nodraw_tex, tex_width, ) else: # Just change the textures for side in new_brush.sides(): try: side.mat = config[ TEX_FIZZLER[side.mat.casefold()] ] except (KeyError, IndexError): # If we fail, just use the original textures pass widen_amount = utils.conv_float(config['thickness', '2'], 2.0) if widen_amount != 2: for brush in new_brush.solids: conditions.widen_fizz_brush( brush, thickness=widen_amount, )
def res_cutout_tile(inst, res): """Generate random quarter tiles, like in Destroyed or Retro maps. - "MarkerItem" is the instance to look for. - "TileSize" can be "2x2" or "4x4". - rotateMax is the amount of degrees to rotate squarebeam models. Materials: - "squarebeams" is the squarebeams variant to use. - "ceilingwalls" are the sides of the ceiling section. - "floorbase" is the texture under floor sections. - "tile_glue" is used on top of a thinner tile segment. - "clip" is the player_clip texture used over floor segments. (This allows customising the surfaceprop.) - "Floor4x4Black", "Ceil2x2White" and other combinations can be used to override the textures used. """ item = resolve_inst(res['markeritem']) INST_LOCS = {} # Map targetnames -> surface loc CEIL_IO = [] # Pairs of ceil inst corners to cut out. FLOOR_IO = [] # Pairs of floor inst corners to cut out. overlay_ids = {} # When we replace brushes, we need to fix any overlays # on that surface. MATS.clear() floor_edges = [] # Values to pass to add_floor_sides() at the end sign_loc = set(FORCE_LOCATIONS) # If any signage is present in the map, we need to force tiles to # appear at that location! for over in conditions.VMF.by_class['info_overlay']: if ( over['material'].casefold() in FORCE_TILE_MATS and # Only check floor/ceiling overlays over['basisnormal'] in ('0 0 1', '0 0 -1') ): loc = Vec.from_str(over['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) sign_loc.add(loc_min.as_tuple()) sign_loc.add(loc_max.as_tuple()) SETTINGS = { 'floor_chance': utils.conv_int( res['floorChance', '100'], 100), 'ceil_chance': utils.conv_int( res['ceilingChance', '100'], 100), 'floor_glue_chance': utils.conv_int( res['floorGlueChance', '0']), 'ceil_glue_chance': utils.conv_int( res['ceilingGlueChance', '0']), 'rotate_beams': int(utils.conv_float( res['rotateMax', '0']) * BEAM_ROT_PRECISION), 'beam_skin': res['squarebeamsSkin', '0'], 'base_is_disp': utils.conv_bool(res['dispBase', '0']), 'quad_floor': res['FloorSize', '4x4'].casefold() == '2x2', 'quad_ceil': res['CeilingSize', '4x4'].casefold() == '2x2', } for mat_prop in res['Materials', []]: MATS[mat_prop.name].append(mat_prop.value) if SETTINGS['base_is_disp']: # We want the normal brushes to become nodraw. MATS['floorbase_disp'] = MATS['floorbase'] MATS['floorbase'] = ['tools/toolsnodraw'] for key, default in TEX_DEFAULT: if key not in MATS: MATS[key] = [default] # Find our marker ents for inst in conditions.VMF.by_class['func_instance']: # type: VLib.Entity if inst['file'].casefold() not in item: continue targ = inst['targetname'] orient = Vec(0, 0, 1).rotate_by_str(inst['angles', '0 0 0']) # Check the orientation of the marker to figure out what to generate if orient == (0, 0, 1): io_list = FLOOR_IO else: io_list = CEIL_IO # Reuse orient to calculate where the solid face will be. loc = (orient * -64) + Vec.from_str(inst['origin']) INST_LOCS[targ] = loc for out in inst.output_targets(): io_list.append((targ, out)) if not inst.outputs and inst.fixup['$connectioncount'] == '0': # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 128x128 tile segment. io_list.append((targ, targ)) inst.remove() # Remove the instance itself from the map. for start_floor, end_floor in FLOOR_IO: if end_floor not in INST_LOCS: # Not a marker - remove this and the antline. for toggle in conditions.VMF.by_target[end_floor]: conditions.remove_ant_toggle(toggle) continue detail_ent = conditions.VMF.create_ent( classname='func_detail' ) box_min = Vec(INST_LOCS[start_floor]) box_min.min(INST_LOCS[end_floor]) box_max = Vec(INST_LOCS[start_floor]) box_max.max(INST_LOCS[end_floor]) if box_min.z != box_max.z: continue # They're not in the same level! z = box_min.z if SETTINGS['rotate_beams']: # We have to generate 1 model per 64x64 block to do rotation... gen_rotated_squarebeams( box_min - (64, 64, 0), box_max + (64, 64, -8), skin=SETTINGS['beam_skin'], max_rot=SETTINGS['rotate_beams'], ) else: # Make the squarebeams props, using big models if possible gen_squarebeams( box_min + (-64, -64, 0), box_max + (64, 64, -8), skin=SETTINGS['beam_skin'] ) # Add a player_clip brush across the whole area conditions.VMF.add_brush(conditions.VMF.make_prism( p1=box_min - (64, 64, FLOOR_DEPTH), p2=box_max + (64, 64, 0), mat=MATS['clip'][0], ).solid) # Add a noportal_volume covering the surface, in case there's # room for a portal. noportal_solid = conditions.VMF.make_prism( # Don't go all the way to the sides, so it doesn't affect wall # brushes. p1=box_min - (63, 63, 9), p2=box_max + (63, 63, 0), mat='tools/toolsinvisible', ).solid noportal_ent = conditions.VMF.create_ent( classname='func_noportal_volume', origin=box_min.join(' '), ) noportal_ent.solids.append(noportal_solid) if SETTINGS['base_is_disp']: # Use displacements for the base instead. make_alpha_base( box_min + (-64, -64, 0), box_max + (64, 64, 0), ) for x, y in utils.iter_grid( min_x=int(box_min.x), max_x=int(box_max.x)+1, min_y=int(box_min.y), max_y=int(box_max.y)+1, stride=128, ): convert_floor( Vec(x, y, z), overlay_ids, MATS, SETTINGS, sign_loc, detail_ent, ) # Mark borders we need to fill in, and the angle (for func_instance) # The wall is the face pointing inwards towards the bottom brush, # and the ceil is the ceiling of the block above the bordering grid # points. for x in range(int(box_min.x), int(box_max.x) + 1, 128): # North floor_edges.append(BorderPoints( wall=Vec(x, box_max.y + 64, z - 64), ceil=Vec_tuple(x, box_max.y + 128, z), rot=270, )) # South floor_edges.append(BorderPoints( wall=Vec(x, box_min.y - 64, z - 64), ceil=Vec_tuple(x, box_min.y - 128, z), rot=90, )) for y in range(int(box_min.y), int(box_max.y) + 1, 128): # East floor_edges.append(BorderPoints( wall=Vec(box_max.x + 64, y, z - 64), ceil=Vec_tuple(box_max.x + 128, y, z), rot=180, )) # West floor_edges.append(BorderPoints( wall=Vec(box_min.x - 64, y, z - 64), ceil=Vec_tuple(box_min.x - 128, y, z), rot=0, )) add_floor_sides(floor_edges) conditions.reallocate_overlays(overlay_ids) return True
def test_conv_int(self): for string, result in ints: self.assertEqual(utils.conv_int(string), result)
def test_conv_int_fails_on_str(self): for string in non_ints: self.assertEqual(utils.conv_int(string), 0) for default in def_vals: # Check all default values pass through unchanged self.assertIs(utils.conv_int(string, default), default)
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
def res_cust_fizzler(base_inst, res): """Modify a fizzler item to allow for custom brush ents.""" model_name = res['modelname', None] make_unique = utils.conv_bool(res['UniqueModel', '0']) fizz_name = base_inst['targetname', ''] # search for the model instances model_targetnames = ( fizz_name + '_modelStart', fizz_name + '_modelEnd', ) for inst in VMF.by_class['func_instance']: if inst['targetname', ''] in model_targetnames: if inst.fixup['skin', '0'] == '2': # This is a laserfield! We can't edit that! utils.con_log('CustFizzler excecuted on LaserField!') return if model_name is not None: if model_name == '': inst['targetname'] = base_inst['targetname'] else: inst['targetname'] = ( base_inst['targetname'] + '-' + model_name ) if make_unique: inst.make_unique() for key, value in base_inst.fixup.items(): inst.fixup[key] = value new_brush_config = list(res.find_all('brush')) if len(new_brush_config) == 0: return # No brush modifications for orig_brush in ( VMF.by_class['trigger_portal_cleanser'] & VMF.by_target[fizz_name + '_brush']): print(orig_brush) VMF.remove_ent(orig_brush) for config in new_brush_config: new_brush = orig_brush.copy() VMF.add_ent(new_brush) new_brush.clear_keys() # Wipe the original keyvalues new_brush['origin'] = orig_brush['origin'] new_brush['targetname'] = ( fizz_name + '-' + config['name', 'brush'] ) # All ents must have a classname! new_brush['classname'] = 'trigger_portal_cleanser' for prop in config['keys', []]: new_brush[prop.name] = prop.value laserfield_conf = config.find_key('MakeLaserField', None) if laserfield_conf.value is not None: # Resize the brush into a laserfield format, without # the 128*64 parts. If the brush is 128x128, we can # skip the resizing since it's already correct. laser_tex = laserfield_conf['texture', 'effects/laserplane'] nodraw_tex = laserfield_conf['nodraw', 'tools/toolsnodraw'] tex_width = utils.conv_int( laserfield_conf['texwidth', '512'], 512 ) is_short = False for side in new_brush.sides(): if side.mat.casefold() == 'effects/fizzler': is_short = True break if is_short: for side in new_brush.sides(): if side.mat.casefold() == 'effects/fizzler': side.mat = laser_tex uaxis = side.uaxis.split(" ") vaxis = side.vaxis.split(" ") # the format is like "[1 0 0 -393.4] 0.25" side.uaxis = ' '.join(uaxis[:3]) + ' 0] 0.25' side.vaxis = ' '.join(vaxis[:4]) + ' 0.25' else: side.mat = nodraw_tex else: # The hard part - stretching the brush. convert_to_laserfield( new_brush, laser_tex, nodraw_tex, tex_width, ) else: # Just change the textures for side in new_brush.sides(): try: side.mat = config[ vbsp.TEX_FIZZLER[side.mat.casefold()] ] except (KeyError, IndexError): # If we fail, just use the original textures pass
def res_cutout_tile(inst, res): """Generate random quarter tiles, like in Destroyed or Retro maps. - "MarkerItem" is the instance to look for. - "TileSize" can be "2x2" or "4x4". - rotateMax is the amount of degrees to rotate squarebeam models. Materials: - "squarebeams" is the squarebeams variant to use. - "ceilingwalls" are the sides of the ceiling section. - "floorbase" is the texture under floor sections. - "tile_glue" is used on top of a thinner tile segment. - "clip" is the player_clip texture used over floor segments. (This allows customising the surfaceprop.) - "Floor4x4Black", "Ceil2x2White" and other combinations can be used to override the textures used. """ item = resolve_inst(res['markeritem']) INST_LOCS = {} # Map targetnames -> surface loc CEIL_IO = [] # Pairs of ceil inst corners to cut out. FLOOR_IO = [] # Pairs of floor inst corners to cut out. overlay_ids = {} # When we replace brushes, we need to fix any overlays # on that surface. MATS.clear() floor_edges = [] # Values to pass to add_floor_sides() at the end sign_loc = set(FORCE_LOCATIONS) # If any signage is present in the map, we need to force tiles to # appear at that location! for over in conditions.VMF.by_class['info_overlay']: if ( over['material'].casefold() in FORCE_TILE_MATS and # Only check floor/ceiling overlays over['basisnormal'] in ('0 0 1', '0 0 -1') ): loc = Vec.from_str(over['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) sign_loc.add(loc_min.as_tuple()) sign_loc.add(loc_max.as_tuple()) SETTINGS = { 'floor_chance': utils.conv_int( res['floorChance', '100'], 100), 'ceil_chance': utils.conv_int( res['ceilingChance', '100'], 100), 'floor_glue_chance': utils.conv_int( res['floorGlueChance', '0']), 'ceil_glue_chance': utils.conv_int( res['ceilingGlueChance', '0']), 'rotate_beams': int(utils.conv_float( res['rotateMax', '0']) * BEAM_ROT_PRECISION), 'beam_skin': res['squarebeamsSkin', '0'], 'base_is_disp': utils.conv_bool(res['dispBase', '0']), 'quad_floor': res['FloorSize', '4x4'].casefold() == '2x2', 'quad_ceil': res['CeilingSize', '4x4'].casefold() == '2x2', } random.seed(vbsp.MAP_RAND_SEED + '_CUTOUT_TILE_NOISE') noise = SimplexNoise(period=4 * 40) # 4 tiles/block, 50 blocks max # We want to know the number of neighbouring tile cutouts before # placing tiles - blocks away from the sides generate fewer tiles. floor_neighbours = defaultdict(dict) # all_floors[z][x,y] = count for mat_prop in res['Materials', []]: MATS[mat_prop.name].append(mat_prop.value) if SETTINGS['base_is_disp']: # We want the normal brushes to become nodraw. MATS['floorbase_disp'] = MATS['floorbase'] MATS['floorbase'] = ['tools/toolsnodraw'] # Since this uses random data for initialisation, the alpha and # regular will use slightly different patterns. alpha_noise = SimplexNoise(period=4 * 50) else: alpha_noise = None for key, default in TEX_DEFAULT: if key not in MATS: MATS[key] = [default] # Find our marker ents for inst in conditions.VMF.by_class['func_instance']: # type: VLib.Entity if inst['file'].casefold() not in item: continue targ = inst['targetname'] orient = Vec(0, 0, 1).rotate_by_str(inst['angles', '0 0 0']) # Check the orientation of the marker to figure out what to generate if orient == (0, 0, 1): io_list = FLOOR_IO else: io_list = CEIL_IO # Reuse orient to calculate where the solid face will be. loc = (orient * -64) + Vec.from_str(inst['origin']) INST_LOCS[targ] = loc for out in inst.output_targets(): io_list.append((targ, out)) if not inst.outputs and inst.fixup['$connectioncount'] == '0': # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 128x128 tile segment. io_list.append((targ, targ)) inst.remove() # Remove the instance itself from the map. for start_floor, end_floor in FLOOR_IO: if end_floor not in INST_LOCS: # Not a marker - remove this and the antline. for toggle in conditions.VMF.by_target[end_floor]: conditions.remove_ant_toggle(toggle) continue box_min = Vec(INST_LOCS[start_floor]) box_min.min(INST_LOCS[end_floor]) box_max = Vec(INST_LOCS[start_floor]) box_max.max(INST_LOCS[end_floor]) if box_min.z != box_max.z: continue # They're not in the same level! z = box_min.z if SETTINGS['rotate_beams']: # We have to generate 1 model per 64x64 block to do rotation... gen_rotated_squarebeams( box_min - (64, 64, 0), box_max + (64, 64, -8), skin=SETTINGS['beam_skin'], max_rot=SETTINGS['rotate_beams'], ) else: # Make the squarebeams props, using big models if possible gen_squarebeams( box_min + (-64, -64, 0), box_max + (64, 64, -8), skin=SETTINGS['beam_skin'] ) # Add a player_clip brush across the whole area conditions.VMF.add_brush(conditions.VMF.make_prism( p1=box_min - (64, 64, FLOOR_DEPTH), p2=box_max + (64, 64, 0), mat=MATS['clip'][0], ).solid) # Add a noportal_volume covering the surface, in case there's # room for a portal. noportal_solid = conditions.VMF.make_prism( # Don't go all the way to the sides, so it doesn't affect wall # brushes. p1=box_min - (63, 63, 9), p2=box_max + (63, 63, 0), mat='tools/toolsinvisible', ).solid noportal_ent = conditions.VMF.create_ent( classname='func_noportal_volume', origin=box_min.join(' '), ) noportal_ent.solids.append(noportal_solid) if SETTINGS['base_is_disp']: # Use displacements for the base instead. make_alpha_base( box_min + (-64, -64, 0), box_max + (64, 64, 0), noise=alpha_noise, ) for x, y in utils.iter_grid( min_x=int(box_min.x), max_x=int(box_max.x) + 1, min_y=int(box_min.y), max_y=int(box_max.y) + 1, stride=128, ): # Build the set of all positions.. floor_neighbours[z][x, y] = -1 # Mark borders we need to fill in, and the angle (for func_instance) # The wall is the face pointing inwards towards the bottom brush, # and the ceil is the ceiling of the block above the bordering grid # points. for x in range(int(box_min.x), int(box_max.x) + 1, 128): # North floor_edges.append(BorderPoints( wall=Vec(x, box_max.y + 64, z - 64), ceil=Vec_tuple(x, box_max.y + 128, z), rot=270, )) # South floor_edges.append(BorderPoints( wall=Vec(x, box_min.y - 64, z - 64), ceil=Vec_tuple(x, box_min.y - 128, z), rot=90, )) for y in range(int(box_min.y), int(box_max.y) + 1, 128): # East floor_edges.append(BorderPoints( wall=Vec(box_max.x + 64, y, z - 64), ceil=Vec_tuple(box_max.x + 128, y, z), rot=180, )) # West floor_edges.append(BorderPoints( wall=Vec(box_min.x - 64, y, z - 64), ceil=Vec_tuple(box_min.x - 128, y, z), rot=0, )) # Now count boundries near tiles, then generate them. # Do it seperately for each z-level: for z, xy_dict in floor_neighbours.items(): # type: float, dict for x, y in xy_dict: # type: float, float # We want to count where there aren't any tiles xy_dict[x, y] = ( ((x - 128, y - 128) not in xy_dict) + ((x - 128, y + 128) not in xy_dict) + ((x + 128, y - 128) not in xy_dict) + ((x + 128, y + 128) not in xy_dict) + ((x - 128, y) not in xy_dict) + ((x + 128, y) not in xy_dict) + ((x, y - 128) not in xy_dict) + ((x, y + 128) not in xy_dict) ) max_x = max_y = 0 weights = {} # Now the counts are all correct, compute the weight to apply # for tiles. # Adding the neighbouring counts will make a 5x5 area needed to set # the center to 0. for (x, y), cur_count in xy_dict.items(): max_x = max(x, max_x) max_y = max(y, max_y) # Orthrogonal is worth 0.2, diagonal is worth 0.1. # Not-present tiles would be 8 - the maximum tile_count = ( 0.8 * cur_count + 0.1 * xy_dict.get((x - 128, y - 128), 8) + 0.1 * xy_dict.get((x - 128, y + 128), 8) + 0.1 * xy_dict.get((x + 128, y - 128), 8) + 0.1 * xy_dict.get((x + 128, y + 128), 8) + 0.2 * xy_dict.get((x - 128, y), 8) + 0.2 * xy_dict.get((x, y - 128), 8) + 0.2 * xy_dict.get((x, y + 128), 8) + 0.2 * xy_dict.get((x + 128, y), 8) ) # The number ranges from 0 (all tiles) to 12.8 (no tiles). # All tiles should still have a small chance to generate tiles. weights[x, y] = min((tile_count + 0.5) / 8, 1) # Share the detail entity among same-height tiles.. detail_ent = conditions.VMF.create_ent( classname='func_detail', ) for x, y in xy_dict: convert_floor( Vec(x, y, z), overlay_ids, MATS, SETTINGS, sign_loc, detail_ent, noise_weight=weights[x, y], noise_func=noise, ) add_floor_sides(floor_edges) conditions.reallocate_overlays(overlay_ids) return conditions.RES_EXHAUSTED
def res_add_variant_setup(res): count = utils.conv_int(res["Number", ""], None) if count: return conditions.weighted_random(count, res["weights", ""]) else: return None
def res_cust_fizzler(base_inst, res): """Modify a fizzler item to allow for custom brush ents.""" from vbsp import TEX_FIZZLER model_name = res["modelname", None] make_unique = utils.conv_bool(res["UniqueModel", "0"]) fizz_name = base_inst["targetname", ""] # search for the model instances model_targetnames = (fizz_name + "_modelStart", fizz_name + "_modelEnd") is_laser = False for inst in VMF.by_class["func_instance"]: if inst["targetname", ""] in model_targetnames: if inst.fixup["skin", "0"] == "2": is_laser = True if model_name is not None: if model_name == "": inst["targetname"] = base_inst["targetname"] else: inst["targetname"] = base_inst["targetname"] + "-" + model_name if make_unique: inst.make_unique() for key, value in base_inst.fixup.items(): inst.fixup[key] = value new_brush_config = list(res.find_all("brush")) if len(new_brush_config) == 0: return # No brush modifications if is_laser: # This is a laserfield! We can't edit those brushes! utils.con_log("CustFizzler excecuted on LaserField!") return for orig_brush in VMF.by_class["trigger_portal_cleanser"] & VMF.by_target[fizz_name + "_brush"]: print(orig_brush) VMF.remove_ent(orig_brush) for config in new_brush_config: new_brush = orig_brush.copy() VMF.add_ent(new_brush) new_brush.clear_keys() # Wipe the original keyvalues new_brush["origin"] = orig_brush["origin"] new_brush["targetname"] = fizz_name + "-" + config["name", "brush"] # All ents must have a classname! new_brush["classname"] = "trigger_portal_cleanser" for prop in config["keys", []]: new_brush[prop.name] = prop.value laserfield_conf = config.find_key("MakeLaserField", None) if laserfield_conf.value is not None: # Resize the brush into a laserfield format, without # the 128*64 parts. If the brush is 128x128, we can # skip the resizing since it's already correct. laser_tex = laserfield_conf["texture", "effects/laserplane"] nodraw_tex = laserfield_conf["nodraw", "tools/toolsnodraw"] tex_width = utils.conv_int(laserfield_conf["texwidth", "512"], 512) is_short = False for side in new_brush.sides(): if side.mat.casefold() == "effects/fizzler": is_short = True break if is_short: for side in new_brush.sides(): if side.mat.casefold() == "effects/fizzler": side.mat = laser_tex uaxis = side.uaxis.split(" ") vaxis = side.vaxis.split(" ") # the format is like "[1 0 0 -393.4] 0.25" side.uaxis = " ".join(uaxis[:3]) + " 0] 0.25" side.vaxis = " ".join(vaxis[:4]) + " 0.25" else: side.mat = nodraw_tex else: # The hard part - stretching the brush. convert_to_laserfield(new_brush, laser_tex, nodraw_tex, tex_width) else: # Just change the textures for side in new_brush.sides(): try: side.mat = config[TEX_FIZZLER[side.mat.casefold()]] except (KeyError, IndexError): # If we fail, just use the original textures pass
def add_quote(quote: Property, targetname, quote_loc, use_dings=False): """Add a quote to the map.""" LOGGER.info('Adding quote: {}', quote) only_once = False cc_emit_name = None added_ents = [] end_commands = [] # The OnUser1 outputs always play the quote (PlaySound/Start), so you can # mix ent types in the same pack. for prop in quote: name = prop.name.casefold() if name == 'file': added_ents.append(VMF.create_ent( classname='func_instance', targetname='', file=INST_PREFIX + prop.value, origin=quote_loc, fixup_style='2', # No fixup )) elif name == 'choreo': # If the property has children, the children are a set of sequential # voice lines. # If the name is set to '@glados_line', the ents will be named # ('@glados_line', 'glados_line_2', 'glados_line_3', ...) LOGGER.info('End-commands: {}', '\n'.join(map(str,end_commands))) if prop.has_children(): secondary_name = targetname.lstrip('@') + '_' # Evenly distribute the choreo ents across the width of the # voice-line room. off = Vec(y=120 / (len(prop) + 1)) start = quote_loc - (0, 60, 0) + off for ind, choreo_line in enumerate(prop, start=1): # type: int, Property is_first = (ind == 1) is_last = (ind == len(prop)) name = ( targetname if is_first else secondary_name + str(ind) ) choreo = add_choreo( choreo_line.value, targetname=name, loc=start + off * (ind - 1), use_dings=use_dings, is_first=is_first, is_last=is_last, only_once=only_once, ) # Add a IO command to start the next one. if not is_last: choreo.add_out(vmfLib.Output( 'OnCompletion', secondary_name + str(ind + 1), 'Start', delay=0.1, )) if is_first: # Ensure this works with cc_emit added_ents.append(choreo) if is_last: for out in end_commands: choreo.add_out(out.copy()) end_commands.clear() else: # Add a single choreo command. choreo = add_choreo( prop.value, targetname, quote_loc, use_dings=use_dings, only_once=only_once, ) added_ents.append(choreo) for out in end_commands: choreo.add_out(out.copy()) end_commands.clear() elif name == 'snd': snd = VMF.create_ent( classname='ambient_generic', spawnflags='49', # Infinite Range, Starts Silent targetname=targetname, origin=quote_loc, message=prop.value, health='10', # Volume ) snd.add_out( vmfLib.Output( 'OnUser1', targetname, 'PlaySound', only_once=only_once, ) ) added_ents.append(snd) elif name == 'bullseye': # Cave's voice lines require a special named bullseye to # work correctly. # Don't add the same one more than once. if prop.value not in ADDED_BULLSEYES: VMF.create_ent( classname='npc_bullseye', # Not solid, Take No Damage, Think outside PVS spawnflags='222224', targetname=prop.value, origin=quote_loc - (0, 0, 16), angles='0 0 0', ) ADDED_BULLSEYES.add(prop.value) elif name == 'cc_emit': # In Aperture Tag, this additional console command is used # to add the closed captions. # Store in a variable, so we can be sure to add the output # regardless of the property order. cc_emit_name = prop.value elif name == 'setstylevar': # Set this stylevar to True # This is useful so some styles can react to which line was # chosen. style_vars[prop.value.casefold()] = True elif name == 'packlist': vbsp.TO_PACK.add(prop.value.casefold()) elif name == 'pack': if prop.has_children(): vbsp.PACK_FILES.update( subprop.value for subprop in prop ) else: vbsp.PACK_FILES.add(prop.value) elif name == 'choreo_name': # Change the targetname used for subsequent entities targetname = prop.value elif name == 'onlyonce': only_once = utils.conv_bool(prop.value) elif name == 'endcommand': end_commands.append(vmfLib.Output( 'OnCompletion', prop['target'], prop['input'], prop['parm', ''], utils.conv_float(prop['delay']), only_once=utils.conv_bool(prop['only_once', None]), times=utils.conv_int(prop['times', None], -1), )) if cc_emit_name: for ent in added_ents: ent.add_out(vmfLib.Output( 'OnUser1', '@command', 'Command', param='cc_emit ' + cc_emit_name, ))