def glass_item_setup(conf: dict, item_id, config_dict): """Build the config dictionary for a custom glass item.""" [base_inst] = resolve_inst('<{}:0>'.format(item_id)) conf.update({ 'frame_' + name: resolve_inst('<{}:bee2_frame_{}>'.format(item_id, name))[0] for name in ['edge', 'single', 'ubend', 'corner'] }) config_dict[base_inst.casefold()] = conf
def res_camera_setup(res: Property): return { 'cam_off': Vec.from_str(res['CamOff', '']), 'yaw_off': Vec.from_str(res['YawOff', '']), 'pitch_off': Vec.from_str(res['PitchOff', '']), 'io_inst': resolve_inst(res['IO_inst'])[0], 'yaw_inst': resolve_inst(res['yawInst', ''])[0], 'pitch_inst': resolve_inst(res['pitchInst', ''])[0], 'yaw_range': srctools.conv_int(res['YawRange', ''], 90), 'pitch_range': srctools.conv_int(res['YawRange', ''], 90), }
def res_add_global_inst(_, res): """Add one instance in a location. Once this is executed, it will be ignored thereafter. """ if res.value is not None: if (res['file'] not in GLOBAL_INSTANCES or utils.conv_bool(res['allow_multiple', '0'], True)): # By default we will skip adding the instance # if was already added - this is helpful for # items that add to original items, or to avoid # bugs. new_inst = VLib.Entity(VMF, keys={ "classname": "func_instance", "targetname": res['name', ''], "file": resolve_inst(res['file'])[0], "angles": res['angles', '0 0 0'], "origin": res['position', '0 0 -10000'], "fixup_style": res['fixup_style', '0'], }) GLOBAL_INSTANCES.append(res['file']) if new_inst['targetname'] == '': new_inst['targetname'] = "inst_" new_inst.make_unique() VMF.add_ent(new_inst) res.value = None # Disable this
def res_add_global_inst(_, res): """Add one instance in a location. Once this is executed, it will be ignored thereafter. """ if res.value is not None: if utils.conv_bool(res["allow_multiple", "0"]) or res["file"] not in GLOBAL_INSTANCES: # By default we will skip adding the instance # if was already added - this is helpful for # items that add to original items, or to avoid # bugs. new_inst = VLib.Entity( VMF, keys={ "classname": "func_instance", "targetname": res["name", ""], "file": resolve_inst(res["file"])[0], "angles": res["angles", "0 0 0"], "origin": res["position", "0 0 -10000"], "fixup_style": res["fixup_style", "0"], }, ) GLOBAL_INSTANCES.add(res["file"]) if new_inst["targetname"] == "": new_inst["targetname"] = "inst_" new_inst.make_unique() VMF.add_ent(new_inst) return True # Remove this result
def flag_has_inst(_, flag): """Checks if the given instance is present anywhere in the map.""" flags = resolve_inst(flag.value) return any( inst.casefold() in flags for inst in ALL_INST )
def flag_has_inst(_, flag): """Return true if the filename is present anywhere in the map.""" flags = resolve_inst(flag.value) return any( inst in flags for inst in ALL_INST )
def res_vactube_setup(res: Property): 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': srctools.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, srctools.conv_int( size, 1) return group
def res_find_potential_tag_fizzlers(vmf: VMF, inst: Entity): """We need to know which items are 'real' fizzlers. This is used for Aperture Tag paint fizzlers. """ if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']: return RES_EXHAUSTED if inst['file'].casefold() not in resolve_inst('<ITEM_BARRIER_HAZARD:0>'): return # The key list in the dict will be a set of all fizzler items! tag_fizzlers[inst['targetname']] = inst if tag_fizzler_locs: # Only loop through fizzlers once. return # Determine the origins by first finding the bounding box of the brushes, # then averaging. for fizz in vmf.by_class['trigger_portal_cleanser']: name = fizz['targetname'][:-6] # Strip off '_brush' bbox_min, bbox_max = fizz.get_bbox() if name in tag_fizzler_locs: orig_min, orig_max = tag_fizzler_locs[name] orig_min.min(bbox_min) orig_max.max(bbox_max) else: tag_fizzler_locs[name] = bbox_min, bbox_max for name, (s, l) in tag_fizzler_locs.items(): # Figure out how to compare for this brush. # If it's horizontal, signs should point to the center: if abs(s.z - l.z) == 2: tag_fizzler_locs[name] =( 'z', s.x + l.x / 2, s.y + l.y / 2, s.z + 1, ) continue # For the vertical directions, we want to compare based on the line segment. if abs(s.x - l.x) == 2: # Y direction tag_fizzler_locs[name] = ( 'y', s.y, l.y, s.x + 1, ) else: # Extends in X direction tag_fizzler_locs[name] = ( 'x', s.x, l.x, s.y + 1, )
def res_find_potential_tag_fizzlers(vmf: VMF, inst: Entity): """We need to know which items are 'real' fizzlers. This is used for Aperture Tag paint fizzlers. """ if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']: return RES_EXHAUSTED if inst['file'].casefold() not in resolve_inst('<ITEM_BARRIER_HAZARD:0>'): return # The key list in the dict will be a set of all fizzler items! tag_fizzlers[inst['targetname']] = inst if tag_fizzler_locs: # Only loop through fizzlers once. return # Determine the origins by first finding the bounding box of the brushes, # then averaging. for fizz in vmf.by_class['trigger_portal_cleanser']: name = fizz['targetname'][:-6] # Strip off '_brush' bbox_min, bbox_max = fizz.get_bbox() if name in tag_fizzler_locs: orig_min, orig_max = tag_fizzler_locs[name] orig_min.min(bbox_min) orig_max.max(bbox_max) else: tag_fizzler_locs[name] = bbox_min, bbox_max for name, (s, l) in tag_fizzler_locs.items(): # Figure out how to compare for this brush. # If it's horizontal, signs should point to the center: if abs(s.z - l.z) == 2: tag_fizzler_locs[name] = ( 'z', s.x + l.x / 2, s.y + l.y / 2, s.z + 1, ) continue # For the vertical directions, we want to compare based on the line segment. if abs(s.x - l.x) == 2: # Y direction tag_fizzler_locs[name] = ( 'y', s.y, l.y, s.x + 1, ) else: # Extends in X direction tag_fizzler_locs[name] = ( 'x', s.x, l.x, s.y + 1, )
def res_add_overlay_inst(inst, res): """Add another instance on top of this one.""" print('adding overlay', res['file']) VMF.create_ent( classname='func_instance', targetname=inst['targetname'], file=resolve_inst(res['file', ''])[0], angles=inst['angles'], origin=inst['origin'], fixup_style=res['fixup_style', '0'], )
def res_unst_scaffold_setup(res: Property): 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': srctools.conv_bool(block['isPiston', '0']), 'rotate_logic': srctools.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 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 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_add_overlay_inst(inst, res): """Add another instance on top of this one.""" print("adding overlay", res["file"]) overlay_inst = VMF.create_ent( classname="func_instance", targetname=inst["targetname", ""], file=resolve_inst(res["file", ""])[0], angles=inst["angles", "0 0 0"], origin=inst["origin"], fixup_style=res["fixup_style", "0"], ) if utils.conv_bool(res["copy_fixup", "1"]): # Copy the fixup values across from the original instance for fixup, value in inst.fixup.items(): overlay_inst.fixup[fixup] = value
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 find_indicator_panels(inst: Entity): """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 # type: Vec loc_max = (loc + (15, 15, 0)) // 32 * 32 # type: Vec loc_min += (16, 16, 0) loc_max += (16, 16, 0) FORCE_LOCATIONS.add(loc_min.as_tuple()) FORCE_LOCATIONS.add(loc_max.as_tuple())
def res_cust_output(inst, res): """Add an additional output to the instance with any values. Always points to the targeted item. """ over_name = '@' + inst['targetname'] + '_indicator' for toggle in VMF.by_class['func_instance']: if toggle.fixup['indicator_name', ''] == over_name: toggle_name = toggle['targetname'] break else: toggle_name = '' # we want to ignore the toggle instance, if it exists # Make this a set to ignore repeated targetnames targets = {o.target for o in inst.outputs if o.target != toggle_name} kill_signs = utils.conv_bool(res["remIndSign", '0'], False) dec_con_count = utils.conv_bool(res["decConCount", '0'], False) targ_conditions = list(res.find_all("targCondition")) pan_files = resolve_inst('[indPan]') if kill_signs or dec_con_count or targ_conditions: for con_inst in VMF.by_class['func_instance']: if con_inst['targetname'] in targets: if kill_signs and con_inst in pan_files: VMF.remove_ent(con_inst) if targ_conditions: for cond in targ_conditions: cond.value.test(con_inst) if dec_con_count and 'connectioncount' in con_inst.fixup: # decrease ConnectionCount on the ents, # so they can still process normal inputs try: val = int(con_inst.fixup['connectioncount']) con_inst.fixup['connectioncount'] = str(val-1) except ValueError: # skip if it's invalid utils.con_log( con_inst['targetname'] + ' has invalid ConnectionCount!' ) for targ in targets: for out in res.find_all('addOut'): add_output(inst, out, targ)
def mon_remove_bullseyes(inst: Entity): """Remove bullsyes used for cameras.""" if not BULLSYE_LOCS: return RES_EXHAUSTED if inst['file'].casefold() not in resolve_inst('<ITEM_CATAPULT_TARGET>'): return LOGGER.info('Bullseye {}', BULLSYE_LOCS) origin = Vec(0, 0, -64) origin.localise(Vec.from_str(inst['origin']), Vec.from_str(inst['angles'])) origin = origin.as_tuple() LOGGER.info('Pos: {} -> ', origin, BULLSYE_LOCS[origin]) if BULLSYE_LOCS[origin]: BULLSYE_LOCS[origin] -= 1 inst.remove()
def res_add_global_inst(res: Property): """Add one instance in a location. Options: allow_multiple: Allow multiple copies of this instance. If 0, the instance will not be added if it was already added. name: The targetname of the instance. IF blank, the instance will be given a name of the form 'inst_1234'. file: The filename for the instance. Angles: The orientation of the instance (defaults to '0 0 0'). Origin: The location of the instance (defaults to '0 0 -10000'). Fixup_style: The Fixup style for the instance. '0' (default) is Prefix, '1' is Suffix, and '2' is None. """ if res.value is not None: if srctools.conv_bool(res["allow_multiple", "0"]) or res["file"] not in GLOBAL_INSTANCES: # By default we will skip adding the instance # if was already added - this is helpful for # items that add to original items, or to avoid # bugs. new_inst = Entity( vbsp.VMF, keys={ "classname": "func_instance", "targetname": res["name", ""], "file": resolve_inst(res["file"])[0], "angles": res["angles", "0 0 0"], "origin": res["position", "0 0 -10000"], "fixup_style": res["fixup_style", "0"], }, ) GLOBAL_INSTANCES.add(res["file"]) if new_inst["targetname"] == "": new_inst["targetname"] = "inst_" new_inst.make_unique() vbsp.VMF.add_ent(new_inst) return RES_EXHAUSTED
def res_add_global_inst(res: Property): """Add one instance in a location. Options: allow_multiple: Allow multiple copies of this instance. If 0, the instance will not be added if it was already added. name: The targetname of the instance. IF blank, the instance will be given a name of the form 'inst_1234'. file: The filename for the instance. Angles: The orientation of the instance (defaults to '0 0 0'). Origin: The location of the instance (defaults to '0 0 -10000'). Fixup_style: The Fixup style for the instance. '0' (default) is Prefix, '1' is Suffix, and '2' is None. """ if res.value is not None: if ( srctools.conv_bool(res['allow_multiple', '0']) or res['file'] not in GLOBAL_INSTANCES): # By default we will skip adding the instance # if was already added - this is helpful for # items that add to original items, or to avoid # bugs. new_inst = Entity(vbsp.VMF, keys={ "classname": "func_instance", "targetname": res['name', ''], "file": resolve_inst(res['file'])[0], "angles": res['angles', '0 0 0'], "origin": res['position', '0 0 -10000'], "fixup_style": res['fixup_style', '0'], }) GLOBAL_INSTANCES.add(res['file']) if new_inst['targetname'] == '': new_inst['targetname'] = "inst_" new_inst.make_unique() vbsp.VMF.add_ent(new_inst) return RES_EXHAUSTED
def res_resizeable_trigger(res: Property): """Replace two markers with a trigger brush. This is run once to affect all of an item. Options: 'markerInst': <ITEM_ID:1,2> value referencing the marker instances, or a filename. 'markerItem': The item's ID 'previewVar': A stylevar which enables/disables the preview overlay. 'previewinst': An instance to place at the marker location in preview mode. This should contain checkmarks to display the value when testing. 'previewMat': If set, the material to use for an overlay func_brush. The brush will be parented to the trigger, so it vanishes once killed. It is also non-solid. 'previewScale': The scale for the func_brush materials. 'previewActivate', 'previewDeactivate': The 'instance:name;Input' value to turn the previewInst on and off. 'triggerActivate, triggerDeactivate': The outputs used when the trigger turns on or off. 'coopVar': The instance variable which enables detecting both Coop players. The trigger will be a trigger_playerteam. 'coopActivate, coopDeactivate': The outputs used when coopVar is enabled. These should be suitable for a logic_coop_manager. 'coopOnce': If true, kill the manager after it first activates. 'keys': A block of keyvalues for the trigger brush. Origin and targetname will be set automatically. 'localkeys': The same as above, except values will be changed to use instance-local names. """ marker = resolve_inst(res['markerInst']) markers = {} for inst in vbsp.VMF.by_class['func_instance']: if inst['file'].casefold() in marker: markers[inst['targetname']] = inst if not markers: # No markers in the map - abort return RES_EXHAUSTED trig_act = res['triggerActivate', 'OnStartTouchAll'] trig_deact = res['triggerDeactivate','OnEndTouchAll'] coop_var = res['coopVar', None] coop_act = res['coopActivate', 'OnChangeToAllTrue'] coop_deact = res['coopDeactivate', 'OnChangeToAnyFalse'] coop_only_once = res.bool('coopOnce') marker_connection = conditions.CONNECTIONS[res['markerItem'].casefold()] mark_act_name, mark_act_out = marker_connection.out_act mark_deact_name, mark_deact_out = marker_connection.out_deact del marker_connection preview_var = res['previewVar', ''].casefold() # Display preview overlays if it's preview mode, and the style var is true # or does not exist if vbsp.IS_PREVIEW and (not preview_var or vbsp.settings['style_vars'][preview_var]): preview_mat = res['previewMat', ''] preview_inst_file = res['previewInst', ''] pre_act_name, pre_act_inp = Output.parse_name( res['previewActivate', '']) pre_deact_name, pre_deact_inp = Output.parse_name( res['previewDeactivate', '']) preview_scale = srctools.conv_float(res['previewScale', '0.25'], 0.25) else: # Deactivate the preview_ options when publishing. preview_mat = preview_inst_file = '' pre_act_name = pre_deact_name = None pre_act_inp = pre_deact_inp = '' preview_scale = 0.25 # Now convert each brush # Use list() to freeze it, allowing us to delete from the dict for targ, inst in list(markers.items()): # type: str, VLib.Entity for out in inst.output_targets(): if out in markers: other = markers[out] # type: Entity del markers[out] # Don't let it get repeated break else: if inst.fixup['$connectioncount'] == '0': # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 1-block trigger. other = inst else: continue # It's a marker with an input, the other in the pair # will handle everything. for ent in {inst, other}: # Only do once if inst == other ent.remove() is_coop = vbsp.GAME_MODE == 'COOP' and ( inst.fixup.bool(coop_var) or other.fixup.bool(coop_var) ) bbox_min, bbox_max = Vec.bbox( Vec.from_str(inst['origin']), Vec.from_str(other['origin']) ) # Extend to the edge of the blocks. bbox_min -= 64 bbox_max += 64 out_ent = trig_ent = vbsp.VMF.create_ent( classname='trigger_multiple', # Default # Use the 1st instance's name - that way other inputs control the # trigger itself. targetname=targ, origin=inst['origin'], angles='0 0 0', ) trig_ent.solids = [ vbsp.VMF.make_prism( bbox_min, bbox_max, mat=const.Tools.TRIGGER, ).solid, ] # Use 'keys' and 'localkeys' blocks to set all the other keyvalues. conditions.set_ent_keys(trig_ent, inst, res) if is_coop: trig_ent['spawnflags'] = '1' # Clients trig_ent['classname'] = 'trigger_playerteam' out_ent_name = conditions.local_name(inst, 'man') out_ent = vbsp.VMF.create_ent( classname='logic_coop_manager', targetname=out_ent_name, origin=inst['origin'] ) if coop_only_once: # Kill all the ents when both players are present. out_ent.add_out( Output('OnChangeToAllTrue', out_ent_name, 'Kill'), Output('OnChangeToAllTrue', targ, 'Kill'), ) trig_ent.add_out( Output('OnStartTouchBluePlayer', out_ent_name, 'SetStateATrue'), Output('OnStartTouchOrangePlayer', out_ent_name, 'SetStateBTrue'), Output('OnEndTouchBluePlayer', out_ent_name, 'SetStateAFalse'), Output('OnEndTouchOrangePlayer', out_ent_name, 'SetStateBFalse'), ) act_out = coop_act deact_out = coop_deact else: act_out = trig_act deact_out = trig_deact if preview_mat: preview_brush = vbsp.VMF.create_ent( classname='func_brush', parentname=targ, origin=inst['origin'], Solidity='1', # Not solid drawinfastreflection='1', # Draw in goo.. # Disable shadows and lighting.. disableflashlight='1', disablereceiveshadows='1', disableshadowdepth='1', disableshadows='1', ) preview_brush.solids = [ # Make it slightly smaller, so it doesn't z-fight with surfaces. vbsp.VMF.make_prism( bbox_min + 0.5, bbox_max - 0.5, mat=preview_mat, ).solid, ] for face in preview_brush.sides(): face.scale = preview_scale if preview_inst_file: vbsp.VMF.create_ent( classname='func_instance', targetname=targ + '_preview', file=preview_inst_file, # Put it at the second marker, since that's usually # closest to antlines if present. origin=other['origin'], ) if pre_act_name and trig_act: out_ent.add_out(Output( trig_act, targ + '_preview', inst_in=pre_act_name, inp=pre_act_inp, )) if pre_deact_name and trig_deact: out_ent.add_out(Output( trig_deact, targ + '_preview', inst_in=pre_deact_name, inp=pre_deact_inp, )) # Now copy over the outputs from the markers, making it work. for out in inst.outputs + other.outputs: # Skip the output joining the two markers together. if out.target == other['targetname']: continue if out.inst_out == mark_act_name and out.output == mark_act_out: ent_out = act_out elif out.inst_out == mark_deact_name and out.output == mark_deact_out: ent_out = deact_out else: continue # Skip this output - it's somehow invalid for this item. if not ent_out: continue # Allow setting the output to "" to skip out_ent.add_out(Output( ent_out, out.target, inst_in=out.inst_in, inp=out.input, param=out.params, delay=out.delay, times=out.times, )) return RES_EXHAUSTED
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_cust_output(inst, res): """Add an additional output to the instance with any values. Always points to the targeted item. If DecConCount is 1, connections """ ( outputs, dec_con_count, targ_conditions, force_sign_type, (sign_act_name, sign_act_out), (sign_deact_name, sign_deact_out), ) = res.value over_name = '@' + inst['targetname'] + '_indicator' for toggle in vbsp.VMF.by_class['func_instance']: if toggle.fixup['indicator_name', ''] == over_name: toggle_name = toggle['targetname'] break else: toggle_name = '' # we want to ignore the toggle instance, if it exists # Build a mapping from names to targets. # This is also the set of all output items, plus indicators. targets = defaultdict(list) for out in inst.outputs: if out.target != toggle_name: targets[out.target].append(out) pan_files = resolve_inst('[indPan]') # These all require us to search through the instances. if force_sign_type or dec_con_count or targ_conditions: for con_inst in vbsp.VMF.by_class['func_instance']: # type: VLib.Entity if con_inst['targetname'] not in targets: # Not our instance continue # Is it an indicator panel, and should we be modding it? if force_sign_type is not None and con_inst['file'].casefold() in pan_files: # Remove the panel if force_sign_type == '': con_inst.remove() continue # Overwrite the signage instance, and then add the # appropriate outputs to control it. sign_id, sign_file_id = force_sign_type con_inst['file'] = resolve_inst(sign_file_id)[0] # First delete the original outputs: for out in targets[con_inst['targetname']]: inst.outputs.remove(out) inputs = CONNECTIONS[sign_id] act_name, act_inp = inputs.in_act deact_name, deact_inp = inputs.in_deact LOGGER.info( 'outputs: a="{}" d="{}"\n' 'inputs: a="{}" d="{}"'.format( (sign_act_name, sign_act_out), (sign_deact_name, sign_deact_out), inputs.in_act, inputs.in_deact ) ) if act_inp and sign_act_out: inst.add_out(VLib.Output( inst_out=sign_act_name, out=sign_act_out, inst_in=act_name, inp=act_inp, targ=con_inst['targetname'], )) if deact_inp and sign_deact_out: inst.add_out(VLib.Output( inst_out=sign_deact_name, out=sign_deact_out, inst_in=deact_name, inp=deact_inp, targ=con_inst['targetname'], )) if dec_con_count and 'connectioncount' in con_inst.fixup: # decrease ConnectionCount on the ents, # so they can still process normal inputs try: val = int(con_inst.fixup['connectioncount']) con_inst.fixup['connectioncount'] = str(val-1) except ValueError: # skip if it's invalid LOGGER.warning( con_inst['targetname'] + ' has invalid ConnectionCount!' ) if targ_conditions: for cond in targ_conditions: # type: Condition cond.test(con_inst) if outputs: for targ in targets: for out in outputs: conditions.add_output(inst, out, targ)
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 flag_file_equal(inst, flag): """Evaluates True if the instance matches the given file.""" return inst['file'].casefold() in resolve_inst(flag.value)
def make_static_pist_setup(res): return { name: resolve_inst(res[name, ""])[0] for name in ("bottom_1", "bottom_2", "bottom_3", "static_0", "static_1", "static_2", "static_3", "static_4") }
def res_track_plat(res: Property): """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. - vert_bottom_suffix: Add '_bottom' / '_vert_bottom' to the track at the bottom of vertical platforms. - 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']) # If invalid, [] = false so ''[0] = ''. single_plat_inst = (resolve_inst(res['single_plat', '']) or '')[0] 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 srctools.conv_bool(res['vert_suffix', '']): for inst in track_set: conditions.add_suffix(inst, '_vert') if srctools.conv_bool(res['plat_suffix', '']): conditions.add_suffix(plat_inst, '_vert') if srctools.conv_bool(res['vert_bottom_suffix', '']): # We want to find the bottom/top track which is facing the # same direction as the platform. track_dirs = { inst_top: Vec(-1, 0, 0), inst_bottom: Vec(1, 0, 0) } for inst in track_set: try: norm_off = track_dirs[inst['file'].casefold()] except KeyError: continue if norm_off.rotate_by_str(inst['angles']) == facing: conditions.add_suffix(inst, '_bottom') elif track_facing == 'HORIZ_MIRR': if srctools.conv_bool(res['horiz_suffix', '']): for inst in track_set: conditions.add_suffix(inst, '_horiz_mirrored') if srctools.conv_bool(res['plat_suffix', '']): conditions.add_suffix(plat_inst, '_horiz') else: # == 'HORIZ' if srctools.conv_bool(res['horiz_suffix', '']): for inst in track_set: conditions.add_suffix(inst, '_horiz') if srctools.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 flag_file_equal(inst, flag): return inst["file"].casefold() in resolve_inst(flag.value)
def res_make_catwalk(res: Property): """Speciallised result to generate catwalks from markers. Only runs once, and then quits the condition list. Instances: MarkerInst: The instance set in editoritems. Straight_128/256/512: Straight sections. Extends East Corner: A corner piece. Connects on N and W sides. TJunction; A T-piece. Connects on all but the East side. CrossJunction: A X-piece. Connects on all sides. End: An end piece. Connects on the East side. Stair: A stair. Starts East and goes Up and West. End_wall: Connects a West wall to a East catwalk. Support_Wall: A support extending from the East wall. Support_Ceil: A support extending from the ceiling. Support_Floor: A support extending from the floor. Support_Goo: A floor support, designed for goo pits. Single_Wall: A section connecting to an East wall. """ LOGGER.info("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', 'support_goo', '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 = {} # Find all our markers, so we can look them up by targetname. for inst in vbsp.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 # Snap the markers to the grid. If on glass it can become offset... origin = Vec.from_str(inst['origin']) origin = origin // 128 * 128 # type: Vec origin += 64 while origin.as_tuple() in conditions.GOO_LOCS: # The instance is in goo! Switch to floor orientation, and move # up until it's in air. inst['angles'] = '0 0 0' origin.z += 128 inst['origin'] = str(origin) if not markers: return RES_EXHAUSTED LOGGER.info('Connections: {}', connections) LOGGER.info('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 entities. # Find the associated overlays too. for del_inst in vbsp.VMF.by_target[conn.target]: conditions.remove_ant_toggle(del_inst) continue inst2 = markers[conn.target] LOGGER.debug('{} <-> {}', 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: LOGGER.warning('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 LOGGER.debug('Dist = {}, Vert = {}', dist, vert_dist) if (dist - 128) // 2 < abs(vert_dist): # The stairs are 2 long, 1 high. Check there's enough room # Subtract the last block though, since that's a corner. LOGGER.warning('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. normal = Vec(0, 0, 1).rotate_by_str(inst['angles']) new_type, inst['angles'] = utils.CONN_LOOKUP[tuple(dir_mask)] inst['file'] = instances[CATWALK_TYPES[new_type]] 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], # +E, -W y=dir_mask[0] - dir_mask[1], # +N, -S, z=0, ) 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): # If in goo, use different supports! origin = Vec.from_str(inst['origin']) origin.z -= 128 if origin.as_tuple() in conditions.GOO_LOCS: supp = instances['support_goo'] else: supp = instances['support_floor'] elif normal == (0, 0, -1): supp = instances['support_ceil'] else: supp = instances['support_wall'] if supp: vbsp.VMF.create_ent( classname='func_instance', origin=inst['origin'], angles=INST_ANGLE[normal.as_tuple()], file=supp, ) LOGGER.info('Finished catwalk generation!') return RES_EXHAUSTED
def res_add_overlay_inst(inst: Entity, res: Property): """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. '<piston_start>' will set it to the starting position, and '<piston_end>' will set it to the ending position. of piston platform handles. angles: If set, overrides the base instance angles. This does not affect the offset property. fixup/localfixup: 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 srctools.conv_bool(res['copy_fixup', '1']): if 'fixup' not in res and 'localfixup' not in res: # Copy the fixup values across from the original instance for fixup, value in inst.fixup.items(): overlay_inst.fixup[fixup] = value conditions.set_ent_keys(overlay_inst.fixup, inst, res, 'fixup') if res.bool('move_outputs', False): 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_start>': if srctools.conv_bool(inst.fixup['$start_up', '']): folded_off = '<piston_top>' else: folded_off = '<piston_bottom>' elif folded_off == '<piston_end>': if srctools.conv_bool(inst.fixup['$start_up', '']): folded_off = '<piston_bottom>' else: folded_off = '<piston_top>' if folded_off == '<piston_bottom>': offset = Vec( z=srctools.conv_int(inst.fixup['$bottom_level']) * 128, ) elif folded_off == '<piston_top>': offset = Vec( z=srctools.conv_int(inst.fixup['$top_level'], 1) * 128, ) else: # Regular vector offset = Vec.from_str(conditions.resolve_value(inst, 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_cutout_tile(res: Property): """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 # type: Vec loc_max = (loc + (15, 15, 0)) // 32 * 32 # type: Vec loc_min += (16, 16, 0) loc_max += (16, 16, 0) FORCE_LOCATIONS.add(loc_min.as_tuple()) FORCE_LOCATIONS.add(loc_max.as_tuple()) SETTINGS = { 'floor_chance': srctools.conv_int(res['floorChance', '100'], 100), 'ceil_chance': srctools.conv_int(res['ceilingChance', '100'], 100), 'floor_glue_chance': srctools.conv_int(res['floorGlueChance', '0']), 'ceil_glue_chance': srctools.conv_int(res['ceilingGlueChance', '0']), 'rotate_beams': int(srctools.conv_float(res['rotateMax', '0']) * BEAM_ROT_PRECISION), 'beam_skin': res['squarebeamsSkin', '0'], 'base_is_disp': srctools.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_cust_output(inst: Entity, res: Property): """Add an additional output to the instance with any values. Always points to the targeted item. If DecConCount is 1, connections """ ( outputs, dec_con_count, targ_conditions, force_sign_type, (sign_act_name, sign_act_out), (sign_deact_name, sign_deact_out), ) = res.value over_name = '@' + inst['targetname'] + '_indicator' for toggle in vbsp.VMF.by_class['func_instance']: if toggle.fixup['indicator_name', ''] == over_name: toggle_name = toggle['targetname'] break else: toggle_name = '' # we want to ignore the toggle instance, if it exists # Build a mapping from names to targets. # This is also the set of all output items, plus indicators. targets = defaultdict(list) for out in inst.outputs: if out.target != toggle_name: targets[out.target].append(out) pan_files = resolve_inst('[indPan]') # These all require us to search through the instances. if force_sign_type or dec_con_count or targ_conditions: for con_inst in vbsp.VMF.by_class['func_instance']: # type: Entity if con_inst['targetname'] not in targets: # Not our instance continue # Is it an indicator panel, and should we be modding it? if force_sign_type is not None and con_inst['file'].casefold( ) in pan_files: # Remove the panel if force_sign_type == '': con_inst.remove() continue # Overwrite the signage instance, and then add the # appropriate outputs to control it. sign_id, sign_file_id = force_sign_type con_inst['file'] = resolve_inst(sign_file_id)[0] # First delete the original outputs: for out in targets[con_inst['targetname']]: inst.outputs.remove(out) inputs = CONNECTIONS[sign_id] act_name, act_inp = inputs.in_act deact_name, deact_inp = inputs.in_deact LOGGER.info('outputs: a="{}" d="{}"\n' 'inputs: a="{}" d="{}"'.format( (sign_act_name, sign_act_out), (sign_deact_name, sign_deact_out), inputs.in_act, inputs.in_deact)) if act_inp and sign_act_out: inst.add_out( Output( inst_out=sign_act_name, out=sign_act_out, inst_in=act_name, inp=act_inp, targ=con_inst['targetname'], )) if deact_inp and sign_deact_out: inst.add_out( Output( inst_out=sign_deact_name, out=sign_deact_out, inst_in=deact_name, inp=deact_inp, targ=con_inst['targetname'], )) if dec_con_count and 'connectioncount' in con_inst.fixup: # decrease ConnectionCount on the ents, # so they can still process normal inputs try: val = int(con_inst.fixup['connectioncount']) con_inst.fixup['connectioncount'] = str(val - 1) except ValueError: # skip if it's invalid LOGGER.warning(con_inst['targetname'] + ' has invalid ConnectionCount!') if targ_conditions: for cond in targ_conditions: # type: Condition cond.test(con_inst) if outputs: for targ in targets: for out in outputs: conditions.add_output(inst, out, targ)
def flag_has_inst(flag: Property): """Checks if the given instance is present anywhere in the map.""" flags = resolve_inst(flag.value) return any(inst.casefold() in flags for inst in ALL_INST)
def res_make_tag_fizzler(vmf: VMF, inst: Entity, res: Property): """Add an Aperture Tag Paint Gun activation fizzler. These fizzlers are created via signs, and work very specially. MUST be priority -100 so it runs before fizzlers! """ import vbsp if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']: # Abort - TAG fizzlers shouldn't appear in any other game! inst.remove() return fizz_base = fizz_name = None # Look for the fizzler instance we want to replace for targetname in inst.output_targets(): if targetname in tag_fizzlers: fizz_name = targetname fizz_base = tag_fizzlers[targetname] del tag_fizzlers[targetname] # Don't let other signs mod this one! continue else: # It's an indicator toggle, remove it and the antline to clean up. LOGGER.warning('Toggle: {}', targetname) for ent in vmf.by_target[targetname]: remove_ant_toggle(ent) inst.outputs.clear() # Remove the outptuts now, they're not valid anyway. if fizz_base is None: # No fizzler - remove this sign inst.remove() return # The distance from origin the double signs are seperated by. sign_offset = res.int('signoffset', 16) sign_loc = ( # The actual location of the sign - on the wall Vec.from_str(inst['origin']) + Vec(0, 0, -64).rotate_by_str(inst['angles'])) # Now deal with the visual aspect: # Blue signs should be on top. blue_enabled = inst.fixup.bool('$start_enabled') oran_enabled = inst.fixup.bool('$start_reversed') # If True, single-color signs will also turn off the other color. # This also means we always show both signs. # If both are enabled or disabled, this has no effect. disable_other = (not inst.fixup.bool('$disable_autorespawn', True) and blue_enabled != oran_enabled) # Delete fixups now, they aren't useful. inst.fixup.clear() if not blue_enabled and not oran_enabled: # Hide the sign in this case! inst.remove() inst_angle = srctools.parse_vec_str(inst['angles']) inst_normal = Vec(0, 0, 1).rotate(*inst_angle) loc = Vec.from_str(inst['origin']) if disable_other or (blue_enabled and oran_enabled): inst['file'] = res['frame_double'] # On a wall, and pointing vertically if inst_normal.z == 0 and Vec(y=1).rotate(*inst_angle).z: # They're vertical, make sure blue's on top! blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset) oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset) # If orange is enabled, with two frames put that on top # instead since it's more important if disable_other and oran_enabled: blue_loc, oran_loc = oran_loc, blue_loc else: offset = Vec(0, sign_offset, 0).rotate(*inst_angle) blue_loc = loc + offset oran_loc = loc - offset else: inst['file'] = res['frame_single'] # They're always centered blue_loc = loc oran_loc = loc if inst_normal.z != 0: # If on floors/ceilings, rotate to point at the fizzler! sign_floor_loc = sign_loc.copy() sign_floor_loc.z = 0 # We don't care about z-positions. # Grab the data saved earlier in res_find_potential_tag_fizzlers() axis, side_min, side_max, normal = tag_fizzler_locs[fizz_name] # The Z-axis fizzler (horizontal) must be treated differently. if axis == 'z': # For z-axis, just compare to the center point. # The values are really x, y, z, not what they're named. sign_dir = sign_floor_loc - (side_min, side_max, normal) else: # For the other two, we compare to the line, # or compare to the closest side (in line with the fizz) other_axis = 'x' if axis == 'y' else 'y' if abs(sign_floor_loc[other_axis] - normal) < 32: # Compare to the closest side. Use ** to swap x/y arguments # appropriately. The closest side is the one with the # smallest magnitude. vmf.create_ent( classname='info_null', targetname=inst['targetname'] + '_min', origin=sign_floor_loc - Vec(**{ axis: side_min, other_axis: normal, }), ) vmf.create_ent( classname='info_null', targetname=inst['targetname'] + '_max', origin=sign_floor_loc - Vec(**{ axis: side_max, other_axis: normal, }), ) sign_dir = min( sign_floor_loc - Vec(**{ axis: side_min, other_axis: normal, }), sign_floor_loc - Vec(**{ axis: side_max, other_axis: normal, }), key=Vec.mag, ) else: # Align just based on whether we're in front or behind. sign_dir = Vec() sign_dir[other_axis] = sign_floor_loc[other_axis] - normal sign_angle = math.degrees(math.atan2(sign_dir.y, sign_dir.x)) # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals sign_angle = (sign_angle + 45) // 90 * 90 # Rotate to fit the instances - south is down sign_angle = int(sign_angle + 90) % 360 if inst_normal.z > 0: sign_angle = '0 {} 0'.format(sign_angle) elif inst_normal.z < 0: # Flip upside-down for ceilings sign_angle = '0 {} 180'.format(sign_angle) else: # On a wall, face upright sign_angle = PETI_INST_ANGLE[inst_normal.as_tuple()] # If disable_other, we show off signs. Otherwise we don't use that sign. blue_sign = 'blue_sign' if blue_enabled else 'blue_off_sign' if disable_other else None oran_sign = 'oran_sign' if oran_enabled else 'oran_off_sign' if disable_other else None if blue_sign: vmf.create_ent( classname='func_instance', file=res[blue_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=blue_loc.join(' '), ) if oran_sign: vmf.create_ent( classname='func_instance', file=res[oran_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=oran_loc.join(' '), ) # Now modify the fizzler... fizz_brushes = list(vmf.by_class['trigger_portal_cleanser'] & vmf.by_target[fizz_name + '_brush']) if 'base_inst' in res: fizz_base['file'] = resolve_inst(res['base_inst'])[0] fizz_base.outputs.clear() # Remove outputs, otherwise they break # branch_toggle entities # Subtract the sign from the list of connections, but don't go below # zero fizz_base.fixup['$connectioncount'] = str( max(0, srctools.conv_int(fizz_base.fixup['$connectioncount', ''], 0) - 1)) if 'model_inst' in res: model_inst = resolve_inst(res['model_inst'])[0] for mdl_inst in vmf.by_class['func_instance']: if mdl_inst['targetname', ''].startswith(fizz_name + '_model'): mdl_inst['file'] = model_inst # Find the direction the fizzler front/back points - z=floor fizz # Signs will associate with the given side! bbox_min, bbox_max = fizz_brushes[0].get_bbox() for axis, val in zip('xyz', bbox_max - bbox_min): if val == 2: fizz_axis = axis sign_center = (bbox_min[axis] + bbox_max[axis]) / 2 break else: # A fizzler that's not 128*x*2? raise Exception('Invalid fizzler brush ({})!'.format(fizz_name)) # Figure out what the sides will set values to... pos_blue = False pos_oran = False neg_blue = False neg_oran = False if sign_loc[fizz_axis] < sign_center: pos_blue = blue_enabled pos_oran = oran_enabled else: neg_blue = blue_enabled neg_oran = oran_enabled fizz_off_tex = { 'left': res['off_left'], 'center': res['off_center'], 'right': res['off_right'], 'short': res['off_short'], } fizz_on_tex = { 'left': res['on_left'], 'center': res['on_center'], 'right': res['on_right'], 'short': res['on_short'], } # If it activates the paint gun, use different textures if pos_blue or pos_oran: pos_tex = fizz_on_tex else: pos_tex = fizz_off_tex if neg_blue or neg_oran: neg_tex = fizz_on_tex else: neg_tex = fizz_off_tex if vbsp.GAME_MODE == 'COOP': # We need ATLAS-specific triggers pos_trig = vmf.create_ent(classname='trigger_playerteam', ) neg_trig = vmf.create_ent(classname='trigger_playerteam', ) output = 'OnStartTouchBluePlayer' else: pos_trig = vmf.create_ent(classname='trigger_multiple', ) neg_trig = vmf.create_ent( classname='trigger_multiple', spawnflags='1', ) output = 'OnStartTouch' pos_trig['origin'] = neg_trig['origin'] = fizz_base['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = fizz_name + '-trig_pos' neg_trig['targetname'] = fizz_name + '-trig_neg' pos_trig.outputs = [ Output( output, fizz_name + '-trig_neg', 'Enable', ), Output( output, fizz_name + '-trig_pos', 'Disable', ), ] neg_trig.outputs = [ Output( output, fizz_name + '-trig_pos', 'Enable', ), Output( output, fizz_name + '-trig_neg', 'Disable', ), ] voice_attr = vbsp.settings['has_attr'] if blue_enabled or disable_other: # If this is blue/oran only, don't affect the other color neg_trig.outputs.append( Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_blue), )) pos_trig.outputs.append( Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_blue), )) if blue_enabled: # Add voice attributes - we have the gun and gel! voice_attr['bluegelgun'] = True voice_attr['bluegel'] = True voice_attr['bouncegun'] = True voice_attr['bouncegel'] = True if oran_enabled or disable_other: neg_trig.outputs.append( Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_oran), )) pos_trig.outputs.append( Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_oran), )) if oran_enabled: voice_attr['orangegelgun'] = True voice_attr['orangegel'] = True voice_attr['speedgelgun'] = True voice_attr['speedgel'] = True if not oran_enabled and not blue_enabled: # If both are disabled, we must shutdown the gun when touching # either side - use neg_trig for that purpose! # We want to get rid of pos_trig to save ents vmf.remove_ent(pos_trig) neg_trig['targetname'] = fizz_name + '-trig' neg_trig.outputs.clear() neg_trig.add_out( Output(output, '@BlueIsEnabled', 'SetValue', param='0')) neg_trig.add_out( Output(output, '@OrangeIsEnabled', 'SetValue', param='0')) for fizz_brush in fizz_brushes: # portal_cleanser ent, not solid! # Modify fizzler textures bbox_min, bbox_max = fizz_brush.get_bbox() for side in fizz_brush.sides(): norm = side.normal() if norm[fizz_axis] == 0: # Not the front/back: force nodraw # Otherwise the top/bottom will have the odd stripes # which won't match the sides side.mat = 'tools/toolsnodraw' continue if norm[fizz_axis] == 1: side.mat = pos_tex[vbsp.TEX_FIZZLER[side.mat.casefold()]] else: side.mat = neg_tex[vbsp.TEX_FIZZLER[side.mat.casefold()]] # The fizzler shouldn't kill cubes fizz_brush['spawnflags'] = '1' fizz_brush.outputs.append( Output( 'OnStartTouch', '@shake_global', 'StartShake', )) fizz_brush.outputs.append( Output( 'OnStartTouch', '@shake_global_sound', 'PlaySound', )) # The triggers are 8 units thick, 24 from the center # (-1 because fizzlers are 2 thick on each side). neg_min, neg_max = Vec(bbox_min), Vec(bbox_max) neg_min[fizz_axis] -= 23 neg_max[fizz_axis] -= 17 pos_min, pos_max = Vec(bbox_min), Vec(bbox_max) pos_min[fizz_axis] += 17 pos_max[fizz_axis] += 23 if blue_enabled or oran_enabled: neg_trig.solids.append( vmf.make_prism( neg_min, neg_max, mat='tools/toolstrigger', ).solid, ) pos_trig.solids.append( vmf.make_prism( pos_min, pos_max, mat='tools/toolstrigger', ).solid, ) else: # If neither enabled, use one trigger neg_trig.solids.append( vmf.make_prism( neg_min, pos_max, mat='tools/toolstrigger', ).solid, )
def res_make_tag_fizzler(vmf: VMF, inst: Entity, res: Property): """Add an Aperture Tag Paint Gun activation fizzler. These fizzlers are created via signs, and work very specially. MUST be priority -100 so it runs before fizzlers! """ import vbsp if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']: # Abort - TAG fizzlers shouldn't appear in any other game! inst.remove() return fizz_base = fizz_name = None # Look for the fizzler instance we want to replace for targetname in inst.output_targets(): if targetname in tag_fizzlers: fizz_name = targetname fizz_base = tag_fizzlers[targetname] del tag_fizzlers[targetname] # Don't let other signs mod this one! continue else: # It's an indicator toggle, remove it and the antline to clean up. LOGGER.warning('Toggle: {}', targetname) for ent in vmf.by_target[targetname]: remove_ant_toggle(ent) inst.outputs.clear() # Remove the outptuts now, they're not valid anyway. if fizz_base is None: # No fizzler - remove this sign inst.remove() return # The distance from origin the double signs are seperated by. sign_offset = res.int('signoffset', 16) sign_loc = ( # The actual location of the sign - on the wall Vec.from_str(inst['origin']) + Vec(0, 0, -64).rotate_by_str(inst['angles']) ) # Now deal with the visual aspect: # Blue signs should be on top. blue_enabled = inst.fixup.bool('$start_enabled') oran_enabled = inst.fixup.bool('$start_reversed') # If True, single-color signs will also turn off the other color. # This also means we always show both signs. # If both are enabled or disabled, this has no effect. disable_other = ( not inst.fixup.bool('$disable_autorespawn', True) and blue_enabled != oran_enabled ) # Delete fixups now, they aren't useful. inst.fixup.clear() if not blue_enabled and not oran_enabled: # Hide the sign in this case! inst.remove() inst_angle = srctools.parse_vec_str(inst['angles']) inst_normal = Vec(0, 0, 1).rotate(*inst_angle) loc = Vec.from_str(inst['origin']) if disable_other or (blue_enabled and oran_enabled): inst['file'] = res['frame_double'] # On a wall, and pointing vertically if inst_normal.z == 0 and Vec(y=1).rotate(*inst_angle).z: # They're vertical, make sure blue's on top! blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset) oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset) # If orange is enabled, with two frames put that on top # instead since it's more important if disable_other and oran_enabled: blue_loc, oran_loc = oran_loc, blue_loc else: offset = Vec(0, sign_offset, 0).rotate(*inst_angle) blue_loc = loc + offset oran_loc = loc - offset else: inst['file'] = res['frame_single'] # They're always centered blue_loc = loc oran_loc = loc if inst_normal.z != 0: # If on floors/ceilings, rotate to point at the fizzler! sign_floor_loc = sign_loc.copy() sign_floor_loc.z = 0 # We don't care about z-positions. # Grab the data saved earlier in res_find_potential_tag_fizzlers() axis, side_min, side_max, normal = tag_fizzler_locs[fizz_name] # The Z-axis fizzler (horizontal) must be treated differently. if axis == 'z': # For z-axis, just compare to the center point. # The values are really x, y, z, not what they're named. sign_dir = sign_floor_loc - (side_min, side_max, normal) else: # For the other two, we compare to the line, # or compare to the closest side (in line with the fizz) other_axis = 'x' if axis == 'y' else 'y' if abs(sign_floor_loc[other_axis] - normal) < 32: # Compare to the closest side. Use ** to swap x/y arguments # appropriately. The closest side is the one with the # smallest magnitude. vmf.create_ent( classname='info_null', targetname=inst['targetname'] + '_min', origin=sign_floor_loc - Vec(**{ axis: side_min, other_axis: normal, }), ) vmf.create_ent( classname='info_null', targetname=inst['targetname'] + '_max', origin=sign_floor_loc - Vec(**{ axis: side_max, other_axis: normal, }), ) sign_dir = min( sign_floor_loc - Vec(**{ axis: side_min, other_axis: normal, }), sign_floor_loc - Vec(**{ axis: side_max, other_axis: normal, }), key=Vec.mag, ) else: # Align just based on whether we're in front or behind. sign_dir = Vec() sign_dir[other_axis] = sign_floor_loc[other_axis] - normal sign_angle = math.degrees( math.atan2(sign_dir.y, sign_dir.x) ) # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals sign_angle = (sign_angle + 45) // 90 * 90 # Rotate to fit the instances - south is down sign_angle = int(sign_angle + 90) % 360 if inst_normal.z > 0: sign_angle = '0 {} 0'.format(sign_angle) elif inst_normal.z < 0: # Flip upside-down for ceilings sign_angle = '0 {} 180'.format(sign_angle) else: # On a wall, face upright sign_angle = PETI_INST_ANGLE[inst_normal.as_tuple()] # If disable_other, we show off signs. Otherwise we don't use that sign. blue_sign = 'blue_sign' if blue_enabled else 'blue_off_sign' if disable_other else None oran_sign = 'oran_sign' if oran_enabled else 'oran_off_sign' if disable_other else None if blue_sign: vmf.create_ent( classname='func_instance', file=res[blue_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=blue_loc.join(' '), ) if oran_sign: vmf.create_ent( classname='func_instance', file=res[oran_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=oran_loc.join(' '), ) # Now modify the fizzler... fizz_brushes = list( vmf.by_class['trigger_portal_cleanser'] & vmf.by_target[fizz_name + '_brush'] ) if 'base_inst' in res: fizz_base['file'] = resolve_inst(res['base_inst'])[0] fizz_base.outputs.clear() # Remove outputs, otherwise they break # branch_toggle entities # Subtract the sign from the list of connections, but don't go below # zero fizz_base.fixup['$connectioncount'] = str(max( 0, srctools.conv_int(fizz_base.fixup['$connectioncount', ''], 0) - 1 )) if 'model_inst' in res: model_inst = resolve_inst(res['model_inst'])[0] for mdl_inst in vmf.by_class['func_instance']: if mdl_inst['targetname', ''].startswith(fizz_name + '_model'): mdl_inst['file'] = model_inst # Find the direction the fizzler front/back points - z=floor fizz # Signs will associate with the given side! bbox_min, bbox_max = fizz_brushes[0].get_bbox() for axis, val in zip('xyz', bbox_max-bbox_min): if val == 2: fizz_axis = axis sign_center = (bbox_min[axis] + bbox_max[axis]) / 2 break else: # A fizzler that's not 128*x*2? raise Exception('Invalid fizzler brush ({})!'.format(fizz_name)) # Figure out what the sides will set values to... pos_blue = False pos_oran = False neg_blue = False neg_oran = False if sign_loc[fizz_axis] < sign_center: pos_blue = blue_enabled pos_oran = oran_enabled else: neg_blue = blue_enabled neg_oran = oran_enabled fizz_off_tex = { 'left': res['off_left'], 'center': res['off_center'], 'right': res['off_right'], 'short': res['off_short'], } fizz_on_tex = { 'left': res['on_left'], 'center': res['on_center'], 'right': res['on_right'], 'short': res['on_short'], } # If it activates the paint gun, use different textures if pos_blue or pos_oran: pos_tex = fizz_on_tex else: pos_tex = fizz_off_tex if neg_blue or neg_oran: neg_tex = fizz_on_tex else: neg_tex = fizz_off_tex if vbsp.GAME_MODE == 'COOP': # We need ATLAS-specific triggers pos_trig = vmf.create_ent( classname='trigger_playerteam', ) neg_trig = vmf.create_ent( classname='trigger_playerteam', ) output = 'OnStartTouchBluePlayer' else: pos_trig = vmf.create_ent( classname='trigger_multiple', ) neg_trig = vmf.create_ent( classname='trigger_multiple', spawnflags='1', ) output = 'OnStartTouch' pos_trig['origin'] = neg_trig['origin'] = fizz_base['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = fizz_name + '-trig_pos' neg_trig['targetname'] = fizz_name + '-trig_neg' pos_trig.outputs = [ Output( output, fizz_name + '-trig_neg', 'Enable', ), Output( output, fizz_name + '-trig_pos', 'Disable', ), ] neg_trig.outputs = [ Output( output, fizz_name + '-trig_pos', 'Enable', ), Output( output, fizz_name + '-trig_neg', 'Disable', ), ] voice_attr = vbsp.settings['has_attr'] if blue_enabled or disable_other: # If this is blue/oran only, don't affect the other color neg_trig.outputs.append(Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_blue), )) pos_trig.outputs.append(Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_blue), )) if blue_enabled: # Add voice attributes - we have the gun and gel! voice_attr['bluegelgun'] = True voice_attr['bluegel'] = True voice_attr['bouncegun'] = True voice_attr['bouncegel'] = True if oran_enabled or disable_other: neg_trig.outputs.append(Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_oran), )) pos_trig.outputs.append(Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_oran), )) if oran_enabled: voice_attr['orangegelgun'] = True voice_attr['orangegel'] = True voice_attr['speedgelgun'] = True voice_attr['speedgel'] = True if not oran_enabled and not blue_enabled: # If both are disabled, we must shutdown the gun when touching # either side - use neg_trig for that purpose! # We want to get rid of pos_trig to save ents vmf.remove_ent(pos_trig) neg_trig['targetname'] = fizz_name + '-trig' neg_trig.outputs.clear() neg_trig.add_out(Output( output, '@BlueIsEnabled', 'SetValue', param='0' )) neg_trig.add_out(Output( output, '@OrangeIsEnabled', 'SetValue', param='0' )) for fizz_brush in fizz_brushes: # portal_cleanser ent, not solid! # Modify fizzler textures bbox_min, bbox_max = fizz_brush.get_bbox() for side in fizz_brush.sides(): norm = side.normal() if norm[fizz_axis] == 0: # Not the front/back: force nodraw # Otherwise the top/bottom will have the odd stripes # which won't match the sides side.mat = 'tools/toolsnodraw' continue if norm[fizz_axis] == 1: side.mat = pos_tex[ vbsp.TEX_FIZZLER[ side.mat.casefold() ] ] else: side.mat = neg_tex[ vbsp.TEX_FIZZLER[ side.mat.casefold() ] ] # The fizzler shouldn't kill cubes fizz_brush['spawnflags'] = '1' fizz_brush.outputs.append(Output( 'OnStartTouch', '@shake_global', 'StartShake', )) fizz_brush.outputs.append(Output( 'OnStartTouch', '@shake_global_sound', 'PlaySound', )) # The triggers are 8 units thick, 24 from the center # (-1 because fizzlers are 2 thick on each side). neg_min, neg_max = Vec(bbox_min), Vec(bbox_max) neg_min[fizz_axis] -= 23 neg_max[fizz_axis] -= 17 pos_min, pos_max = Vec(bbox_min), Vec(bbox_max) pos_min[fizz_axis] += 17 pos_max[fizz_axis] += 23 if blue_enabled or oran_enabled: neg_trig.solids.append( vmf.make_prism( neg_min, neg_max, mat='tools/toolstrigger', ).solid, ) pos_trig.solids.append( vmf.make_prism( pos_min, pos_max, mat='tools/toolstrigger', ).solid, ) else: # If neither enabled, use one trigger neg_trig.solids.append( vmf.make_prism( neg_min, pos_max, mat='tools/toolstrigger', ).solid, )
def res_change_instance(inst: Entity, res: Property): """Set the file to a value.""" inst['file'] = resolve_inst(res.value)[0]
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_reshape_fizzler(vmf: VMF, shape_inst: Entity, res: Property): """Convert a fizzler connected via the output to a new shape. This allows for different placing of fizzler items. Each `segment` parameter should be a `x y z;x y z` pair of positions that represent the ends of the fizzler. `up_axis` should be set to a normal vector pointing in the new 'upward' direction. `default` is the ID of a fizzler type which should be used if no outputs are fired. """ shape_name = shape_inst['targetname'] shape_item = connections.ITEMS.pop(shape_name) shape_angles = Vec.from_str(shape_inst['angles']) up_axis = res.vec('up_axis').rotate(*shape_angles) for conn in shape_item.outputs: fizz_item = conn.to_item try: fizz = fizzler.FIZZLERS[fizz_item.name] except KeyError: LOGGER.warning( 'Reshaping fizzler with non-fizzler output ({})! Ignoring!', fizz_item.name) continue fizz.emitters.clear() # Remove old positions. fizz.up_axis = up_axis break else: # No fizzler, so generate a default. # We create the fizzler instance, Fizzler object, and Item object # matching it. # This is hardcoded to use regular Emancipation Fields. base_inst = vmf.create_ent( targetname=shape_name, classname='func_instance', origin=shape_inst['origin'], file=resolve_inst('<ITEM_BARRIER_HAZARD:fizz_base>'), ) base_inst.fixup.update(shape_inst.fixup) fizz = fizzler.FIZZLERS[shape_name] = fizzler.Fizzler( fizzler.FIZZ_TYPES['VALVE_MATERIAL_EMANCIPATION_GRID'], up_axis, base_inst, [], ) fizz_item = connections.Item( base_inst, connections.ITEM_TYPES['item_barrier_hazard'], shape_item.ant_floor_style, shape_item.ant_wall_style, ) connections.ITEMS[shape_name] = fizz_item # Detach this connection and remove traces of it. for conn in list(shape_item.outputs): conn.remove() for coll in [ shape_item.antlines, shape_item.ind_panels, shape_item.shape_signs ]: for ent in coll: ent.remove() coll.clear() for inp in list(shape_item.inputs): inp.to_item = fizz_item fizz_base = fizz.base_inst fizz_base['origin'] = shape_inst['origin'] origin = Vec.from_str(shape_inst['origin']) for seg_prop in res.find_all('Segment'): vec1, vec2 = seg_prop.value.split(';') seg_min_max = Vec.bbox( Vec.from_str(vec1).rotate(*shape_angles) + origin, Vec.from_str(vec2).rotate(*shape_angles) + origin, ) fizz.emitters.append(seg_min_max)
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_reshape_fizzler(vmf: VMF, shape_inst: Entity, res: Property): """Convert a fizzler connected via the output to a new shape. This allows for different placing of fizzler items. * Each `segment` parameter should be a `x y z;x y z` pair of positions that represent the ends of the fizzler. * `up_axis` should be set to a normal vector pointing in the new 'upward' direction. * If none are connected, a regular fizzler will be synthesized. The following fixup vars will be set to allow the shape to match the fizzler: * `$uses_nodraw` will be 1 if the fizzler nodraws surfaces behind it. """ shape_name = shape_inst['targetname'] shape_item = connections.ITEMS.pop(shape_name) shape_angles = Vec.from_str(shape_inst['angles']) up_axis = res.vec('up_axis').rotate(*shape_angles) for conn in shape_item.outputs: fizz_item = conn.to_item try: fizz = fizzler.FIZZLERS[fizz_item.name] except KeyError: LOGGER.warning( 'Reshaping fizzler with non-fizzler output ({})! Ignoring!', fizz_item.name) continue fizz.emitters.clear() # Remove old positions. fizz.up_axis = up_axis fizz.base_inst['origin'] = shape_inst['origin'] fizz.base_inst['angles'] = shape_inst['angles'] break else: # No fizzler, so generate a default. # We create the fizzler instance, Fizzler object, and Item object # matching it. # This is hardcoded to use regular Emancipation Fields. base_inst = vmf.create_ent( targetname=shape_name, classname='func_instance', origin=shape_inst['origin'], angles=shape_inst['angles'], file=resolve_inst('<ITEM_BARRIER_HAZARD:fizz_base>'), ) base_inst.fixup.update(shape_inst.fixup) fizz = fizzler.FIZZLERS[shape_name] = fizzler.Fizzler( fizzler.FIZZ_TYPES['VALVE_MATERIAL_EMANCIPATION_GRID'], up_axis, base_inst, [], ) fizz_item = connections.Item( base_inst, connections.ITEM_TYPES['item_barrier_hazard'], shape_item.ant_floor_style, shape_item.ant_wall_style, ) connections.ITEMS[shape_name] = fizz_item # Detach this connection and remove traces of it. for conn in list(shape_item.outputs): conn.remove() # Transfer the inputs from us to the fizzler. for inp in list(shape_item.inputs): inp.to_item = fizz_item shape_item.delete_antlines() fizz_base = fizz.base_inst fizz_base['origin'] = shape_inst['origin'] origin = Vec.from_str(shape_inst['origin']) fizz.has_cust_position = True # Since the fizzler is moved elsewhere, it's the responsibility of # the new item to have holes. fizz.embedded = False # So tell it whether or not it needs to do so. shape_inst.fixup['$uses_nodraw'] = fizz.fizz_type.nodraw_behind for seg_prop in res.find_all('Segment'): vec1, vec2 = seg_prop.value.split(';') seg_min_max = Vec.bbox( Vec.from_str(vec1).rotate(*shape_angles) + origin, Vec.from_str(vec2).rotate(*shape_angles) + origin, ) fizz.emitters.append(seg_min_max)
def res_change_instance(inst, res): """Set the file to a value.""" inst["file"] = resolve_inst(res.value)[0]
def res_add_overlay_inst(inst: Entity, res: Property): """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. '<piston_start>' will set it to the starting position, and '<piston_end>' will set it to the ending position. of piston platform handles. angles: If set, overrides the base instance angles. This does not affect the offset property. fixup/localfixup: 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 srctools.conv_bool(res["copy_fixup", "1"]): if "fixup" not in res and "localfixup" not in res: # Copy the fixup values across from the original instance for fixup, value in inst.fixup.items(): overlay_inst.fixup[fixup] = value conditions.set_ent_keys(overlay_inst.fixup, inst, res, "fixup") if res.bool("move_outputs", False): 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_start>": if srctools.conv_bool(inst.fixup["$start_up", ""]): folded_off = "<piston_top>" else: folded_off = "<piston_bottom>" elif folded_off == "<piston_end>": if srctools.conv_bool(inst.fixup["$start_up", ""]): folded_off = "<piston_bottom>" else: folded_off = "<piston_top>" if folded_off == "<piston_bottom>": offset = Vec(z=srctools.conv_int(inst.fixup["$bottom_level"]) * 128) elif folded_off == "<piston_top>": offset = Vec(z=srctools.conv_int(inst.fixup["$top_level"], 1) * 128) else: # Regular vector offset = Vec.from_str(conditions.resolve_value(inst, 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 flag_file_equal(inst: Entity, flag: Property): """Evaluates True if the instance matches the given file.""" return inst['file'].casefold() in resolve_inst(flag.value)
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