def add_floor_sides(locs): """We need to replace nodraw textures around the outside of the holes. This requires looping through all faces, since these will have been nodrawed. """ added_locations = { barrier.wall.as_tuple(): False for barrier in locs } for face in conditions.VMF.iter_wfaces(world=True, detail=False): if face.mat != 'tools/toolsnodraw': continue loc = face.get_origin().as_tuple() if loc in added_locations: random.seed('floor_side_{}_{}_{}'.format(*loc)) face.mat = random.choice(MATS['squarebeams']) added_locations[loc] = True # Swap these to flip the texture diagonally, so the beam is at top face.uaxis, face.vaxis = face.vaxis, face.uaxis face.uaxis.offset = 48 vbsp.IGNORED_FACES.add(face) # Look for the ones without a texture - these are open to the void and # need to be sealed. The template chamfers the edges # to prevent showing void at outside corners. for wall_loc, ceil_loc, rot in locs: if added_locations[wall_loc.as_tuple()]: continue diag_loc = (wall_loc.x, wall_loc.y, wall_loc.z + 128) temp_data = conditions.import_template( # If there's a wall surface directly above this point # or a ceiling brush in the next block over # we want to use a world brush to seal the leak. # Otherwise we use the detail version for inside the map. temp_name=( FLOOR_TEMP_SIDE_DETAIL if ceil_loc not in conditions.SOLIDS and diag_loc not in conditions.SOLIDS else FLOOR_TEMP_SIDE_WORLD ), origin=wall_loc, angles=Vec(0, rot, 0), ) conditions.retexture_template( temp_data, wall_loc, # Switch to use the configured squarebeams texture replace_tex={ 'anim_wp/framework/squarebeams': random.choice( MATS['squarebeams'] ), } )
def add_floor_sides(locs): """We need to replace nodraw textures around the outside of the holes. This requires looping through all faces, since these will have been nodrawed. """ added_locations = {barrier.wall.as_tuple(): False for barrier in locs} for face in conditions.VMF.iter_wfaces(world=True, detail=False): if face.mat != 'tools/toolsnodraw': continue loc = face.get_origin().as_tuple() if loc in added_locations: random.seed('floor_side_{}_{}_{}'.format(*loc)) face.mat = random.choice(MATS['squarebeams']) added_locations[loc] = True # Swap these to flip the texture diagonally, so the beam is at top face.uaxis, face.vaxis = face.vaxis, face.uaxis face.uaxis.offset = 48 vbsp.IGNORED_FACES.add(face) # Look for the ones without a texture - these are open to the void and # need to be sealed. The template chamfers the edges # to prevent showing void at outside corners. for wall_loc, ceil_loc, rot in locs: if added_locations[wall_loc.as_tuple()]: continue diag_loc = (wall_loc.x, wall_loc.y, wall_loc.z + 128) temp_data = conditions.import_template( # If there's a wall surface directly above this point # or a ceiling brush in the next block over # we want to use a world brush to seal the leak. # Otherwise we use the detail version for inside the map. temp_name=(FLOOR_TEMP_SIDE_DETAIL if ceil_loc not in conditions.SOLIDS and diag_loc not in conditions.SOLIDS else FLOOR_TEMP_SIDE_WORLD), origin=wall_loc, angles=Vec(0, rot, 0), ) conditions.retexture_template( temp_data, wall_loc, # Switch to use the configured squarebeams texture replace_tex={ consts.Special.SQUAREBEAMS: random.choice(MATS['squarebeams']), })
def make_corner(origin, angle, size, config): vbsp.VMF.create_ent( classname='func_instance', origin=origin, angles=angle, file=config['corner', size], ) temp = config['corner_temp', size] if temp: temp_solids = import_template( temp, origin=origin, angles=Vec.from_str(angle), force_type=TEMP_TYPES.world, ).world for solid in temp_solids: vbsp.VMF.remove_brush(solid) motion_trigger(*temp_solids)
def res_import_template(inst, res): """Import a template VMF file, retexturing it to match orientatation. It will be placed overlapping the given instance. Options: - ID: The ID of the template to be inserted. - force: a space-seperated list of overrides. If 'white' or 'black' is present, the colour of tiles will be overriden. If a tile size ('2x2', '4x4', 'wall', 'special') is included, all tiles will be switched to that size (if not a floor/ceiling). If 'world' or 'detail' is present, the brush will be forced to that type. - replace: A block of template material -> replacement textures. This is case insensitive - any texture here will not be altered otherwise. - replaceBrush: The position of a brush to replace (0 0 0=the surface). This brush will be removed, and overlays will be fixed to use all faces with the same normal. """ ( temp_id, replace_tex, force_colour, force_grid, force_type, replace_brush_pos, ) = res.value if temp_id not in TEMPLATES: # The template map is read in after setup is performed, so # it must be checked here! # We don't want an error, just quit LOGGER.warning('"{}" not a valid template!', temp_id) return origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles', '0 0 0']) temp_data = conditions.import_template( temp_id, origin, angles, targetname=inst['targetname', ''], force_type=force_type, ) conditions.retexture_template( temp_data, origin, replace_tex, force_colour, force_grid, ) # This is the original brush the template is replacing. We fix overlay # face IDs, so this brush is replaced by the faces in the template pointing # the same way. if replace_brush_pos is None: return pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z) pos += origin try: brush_group = SOLIDS[pos.as_tuple()] except KeyError: return vbsp.VMF.remove_brush(brush_group.solid) new_ids = [] all_brushes = temp_data.world if temp_data.detail is not None: for ent in temp_data.detail: all_brushes.extend(ent.solids) for brush in all_brushes: # type: VLib.Solid for face in brush.sides: # Only faces pointing the same way! if face.normal() == brush_group.normal: # Skip tool brushes (nodraw, player clips..) if face.mat.casefold().startswith('tools/'): continue new_ids.append(str(face.id)) if new_ids: conditions.reallocate_overlays({ str(brush_group.face.id): new_ids, })
def res_faith_mods(inst: VLib.Entity, res: Property): """Modify the trigger_catrapult that is created for ItemFaithPlate items. Values: - raise_trig: Raise or lower the trigger_catapults by this amount. - angled_targ, angled_in: Instance entity and input for angled plates - straight_targ, straight_in: Instance entity and input for straight plates - instvar: A $replace value to set to either 'angled' or ' 'straight'. - enabledVar: A $replace value which will be copied to the main trigger's Start Disabled value (and inverted). - trig_temp: An ID for a template brush to add. This will be offset by the trigger's position (in the case of the 'helper' trigger). """ # Get data about the trigger this instance uses for flinging fixup_var = res['instvar', ''] trig_enabled = res['enabledVar', None] trig_temp = res['trig_temp', ''] offset = utils.conv_int(res['raise_trig', '0']) if offset: offset = Vec(0, 0, offset).rotate_by_str(inst['angles', '0 0 0']) else: offset = Vec() if trig_enabled is not None: trig_enabled = utils.conv_bool(inst.fixup[trig_enabled]) else: trig_enabled = None for trig in vbsp.VMF.by_class['trigger_catapult']: if inst['targetname'] not in trig['targetname']: continue # Edit both the normal and the helper trigger.. trig_origin = trig['origin'] = Vec.from_str(trig['origin']) + offset if offset and not trig_temp: # No template, shift the current brushes. for solid in trig.solids: solid.translate(offset) elif trig_temp: trig.solids = conditions.import_template( temp_name=trig_temp, origin=trig_origin, angles=Vec.from_str(inst['angles']), force_type=conditions.TEMP_TYPES.world, ).world # Remove the trigger solids from worldspawn.. for solid in trig.solids: vbsp.VMF.remove_brush(solid) if trig_enabled is not None and 'helper' not in trig['targetname']: trig['startdisabled'] = utils.bool_as_int(not trig_enabled) # Inspect the outputs to determine the type. # We also change them if desired, since that's not possible # otherwise. for out in trig.outputs: if out.inst_in == 'animate_angled_relay': # Instead of an instance: output, use local names. # This allows us to strip the proxy, as well as use # overlay instances. out.inst_in = None out.target = conditions.local_name( inst, res['angled_targ', 'animate_angled_relay'] ) out.input = res['angled_in', 'Trigger'] if fixup_var: inst.fixup[fixup_var] = 'angled' break # There's only one output we want to look for... elif out.inst_in == 'animate_straightup_relay': out.inst_in = None out.target = conditions.local_name( inst, res[ 'straight_targ', 'animate_straightup_relay' ], ) out.input = res['straight_in', 'Trigger'] if fixup_var: inst.fixup[fixup_var] = 'straight' break
def res_import_template(inst: Entity, res: Property): """Import a template VMF file, retexturing it to match orientatation. It will be placed overlapping the given instance. Options: - ID: The ID of the template to be inserted. Add visgroups to additionally add after a colon, comma-seperated (temp_id:vis1,vis2) - force: a space-seperated list of overrides. If 'white' or 'black' is present, the colour of tiles will be overriden. If 'invert' is added, white/black tiles will be swapped. If a tile size ('2x2', '4x4', 'wall', 'special') is included, all tiles will be switched to that size (if not a floor/ceiling). If 'world' or 'detail' is present, the brush will be forced to that type. - replace: A block of template material -> replacement textures. This is case insensitive - any texture here will not be altered otherwise. - replaceBrush: The position of a brush to replace (0 0 0=the surface). This brush will be removed, and overlays will be fixed to use all faces with the same normal. Can alternately be a block: - Pos: The position to replace. - additionalIDs: Space-separated list of face IDs in the template to also fix for overlays. The surface should have close to a vertical normal, to prevent rescaling the overlay. - removeBrush: If true, the original brush will not be removed. - transferOverlay: Allow disabling transferring overlays to this template. The IDs will be removed instead. (This can be an instvar). - keys/localkeys: If set, a brush entity will instead be generated with these values. This overrides force world/detail. Specially-handled keys: - "origin", offset automatically. - "movedir" on func_movelinear - set a normal surrounded by <>, this gets replaced with angles. - invertVar or colorVar: If this fixup value is true, tile colour will be swapped to the opposite of the current force option. If it is set to 'white' or 'black', that colour will be forced instead. - visgroup: Sets how vigsrouped parts are handled. If 'none' (default), they are ignored. If 'choose', one is chosen. If a number, that is the percentage chance for each visgroup to be added. """ ( orig_temp_id, replace_tex, force_colour, force_grid, force_type, replace_brush_pos, rem_replace_brush, transfer_overlays, additional_replace_ids, invert_var, visgroup_func, key_block, ) = res.value temp_id = conditions.resolve_value(inst, orig_temp_id) temp_name, vis = conditions.parse_temp_name(temp_id) if temp_name not in TEMPLATES: # The template map is read in after setup is performed, so # it must be checked here! # We don't want an error, just quit if temp_id != orig_temp_id: LOGGER.warning('{} -> "{}" is not a valid template!', orig_temp_id, temp_name) else: LOGGER.warning('"{}" is not a valid template!', temp_name) return if invert_var != '': invert_val = inst.fixup[invert_var].casefold() if invert_val == 'white': force_colour = conditions.MAT_TYPES.white elif invert_val == 'black': force_colour = conditions.MAT_TYPES.black elif srctools.conv_bool(invert_val): force_colour = conditions.TEMP_COLOUR_INVERT[force_colour] origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles', '0 0 0']) temp_data = conditions.import_template( temp_id, origin, angles, targetname=inst['targetname', ''], force_type=force_type, visgroup_choose=visgroup_func, ) if key_block is not None: conditions.set_ent_keys(temp_data.detail, inst, key_block) br_origin = Vec.from_str(key_block.find_key('keys')['origin']) br_origin.localise(origin, angles) temp_data.detail['origin'] = br_origin move_dir = temp_data.detail['movedir', ''] if move_dir.startswith('<') and move_dir.endswith('>'): move_dir = Vec.from_str(move_dir).rotate(*angles) temp_data.detail['movedir'] = move_dir.to_angle() # Add it to the list of ignored brushes, so vbsp.change_brush() doesn't # modify it. vbsp.IGNORED_BRUSH_ENTS.add(temp_data.detail) try: # This is the original brush the template is replacing. We fix overlay # face IDs, so this brush is replaced by the faces in the template # pointing # the same way. if replace_brush_pos is None: raise KeyError # Not set, raise to jump out of the try block pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z) pos += origin brush_group = SOLIDS[pos.as_tuple()] except KeyError: # Not set or solid group doesn't exist, skip.. pass else: conditions.steal_from_brush( temp_data, brush_group, rem_replace_brush, additional_replace_ids, conv_bool(conditions.resolve_value(inst, transfer_overlays), True), ) conditions.retexture_template( temp_data, origin, inst.fixup, replace_tex, force_colour, force_grid, )
def res_conveyor_belt(inst: Entity, res: Property): """Create a conveyor belt. Options: SegmentInst: Generated at each square. ('track' is the name of the path.) TrackTeleport: Set the track points so they teleport trains to the start. Speed: The fixup or number for the train speed. MotionTrig: If set, a trigger_multiple will be spawned that EnableMotions weighted cubes. The value is the name of the relevant filter. EndOutput: Adds an output to the last track. The value is the same as outputs in VMFs. RotateSegments: If true (default), force segments to face in the direction of movement RailTemplate: A template for the railings. This is made into a non-solid func_brush, combining all sections. """ move_dist = srctools.conv_int(inst.fixup['$travel_distance']) if move_dist <= 2: # There isn't room for a catwalk, so don't bother. inst.remove() return move_dir = Vec(1, 0, 0).rotate_by_str(inst.fixup['$travel_direction']) move_dir.rotate_by_str(inst['angles']) start_offset = srctools.conv_float(inst.fixup['$starting_position'], 0) teleport_to_start = res.bool('TrackTeleport', True) segment_inst_file = res['SegmentInst', ''] rail_template = res['RailTemplate', None] vmf = inst.map if segment_inst_file: segment_inst_file = conditions.resolve_inst(segment_inst_file)[0] track_speed = res['speed', None] start_pos = Vec.from_str(inst['origin']) end_pos = start_pos + move_dist * move_dir if start_offset > 0: # If an oscillating platform, move to the closest side.. offset = start_offset * move_dir # The instance is placed this far along, so move back to the end. start_pos -= offset end_pos -= offset if start_offset > 0.5: # Swap the direction of movement.. start_pos, end_pos = end_pos, start_pos inst['origin'] = start_pos # Find the angle which generates an instance pointing in the direction # of movement, with the same normal. norm = Vec(z=1).rotate_by_str(inst['angles']) for roll in range(0, 360, 90): angles = move_dir.to_angle(roll) if Vec(z=1).rotate(*angles) == norm: break else: raise ValueError("Can't find angles to give a" ' z={} and x={}!'.format(norm, move_dir)) if res.bool('rotateSegments', True): inst['angles'] = angles else: angles = Vec.from_str(inst['angles']) # Add the EnableMotion trigger_multiple seen in platform items. # This wakes up cubes when it starts moving. motion_filter = res['motionTrig', None] # Disable on walls, or if the conveyor can't be turned on. if norm != (0, 0, 1) or inst.fixup['$connectioncount'] == '0': motion_filter = None track_name = conditions.local_name(inst, 'segment_{}') rail_temp_solids = [] last_track = None # Place beams at the top, so they don't appear inside wall sections. beam_start = start_pos + 48 * norm # type: Vec beam_end = end_pos + 48 * norm # type: Vec for index, pos in enumerate(beam_start.iter_line(beam_end, stride=128), start=1): track = vmf.create_ent( classname='path_track', targetname=track_name.format(index) + '-track', origin=pos, spawnflags=0, orientationtype=0, # Don't rotate ) if track_speed is not None: track['speed'] = track_speed if last_track: last_track['target'] = track['targetname'] if index == 1 and teleport_to_start: track['spawnflags'] = 16 # Teleport here.. last_track = track # Don't place at the last point - it doesn't teleport correctly, # and would be one too many. if segment_inst_file and pos != end_pos: seg_inst = vmf.create_ent( classname='func_instance', targetname=track_name.format(index), file=segment_inst_file, origin=pos, angles=angles, ) seg_inst.fixup.update(inst.fixup) if rail_template: temp = conditions.import_template( rail_template, pos, angles, force_type=conditions.TEMP_TYPES.world, add_to_map=False, ) rail_temp_solids.extend(temp.world) if rail_temp_solids: vmf.create_ent( classname='func_brush', origin=beam_start, spawnflags=1, # Ignore +USE solidity=1, # Not solid vrad_brush_cast_shadows=1, drawinfastreflection=1, ).solids = rail_temp_solids if teleport_to_start: # Link back to the first track.. last_track['target'] = track_name.format(1) + '-track' # Generate an env_beam pointing from the start to the end of the track. beam_keys = res.find_key('BeamKeys', []) if beam_keys.value: beam = vmf.create_ent(classname='env_beam') # 3 offsets - x = distance from walls, y = side, z = height beam_off = beam_keys.vec('origin', 0, 63, 56) for prop in beam_keys: beam[prop.real_name] = prop.value # Localise the targetname so it can be triggered.. beam['LightningStart'] = beam['targetname'] = conditions.local_name( inst, beam['targetname', 'beam']) del beam['LightningEnd'] beam['origin'] = start_pos + Vec( -beam_off.x, beam_off.y, beam_off.z, ).rotate(*angles) beam['TargetPoint'] = end_pos + Vec( +beam_off.x, beam_off.y, beam_off.z, ).rotate(*angles) # Allow adding outputs to the last path_track. for prop in res.find_all('EndOutput'): output = Output.parse(prop) output.output = 'OnPass' output.inst_out = None output.comma_sep = False output.target = conditions.local_name(inst, output.target) last_track.add_out(output) if motion_filter is not None: motion_trig = vmf.create_ent( classname='trigger_multiple', targetname=conditions.local_name(inst, 'enable_motion_trig'), origin=start_pos, filtername=motion_filter, startDisabled=1, wait=0.1, ) motion_trig.add_out( Output('OnStartTouch', '!activator', 'ExitDisabledState')) # Match the size of the original... motion_trig.solids.append( vmf.make_prism( start_pos + Vec(72, -56, 58).rotate(*angles), end_pos + Vec(-72, 56, 144).rotate(*angles), mat='tools/toolstrigger', ).solid) if res.bool('NoPortalFloor'): # Block portals on the floor.. floor_noportal = vmf.create_ent( classname='func_noportal_volume', origin=beam_start, ) floor_noportal.solids.append( vmf.make_prism( start_pos + Vec(-60, -60, -66).rotate(*angles), end_pos + Vec(60, 60, -60).rotate(*angles), mat='tools/toolsinvisible', ).solid) # A brush covering under the platform. base_trig = vmf.make_prism( start_pos + Vec(-64, -64, 48).rotate(*angles), end_pos + Vec(64, 64, 56).rotate(*angles), mat='tools/toolsinvisible', ).solid vmf.add_brush(base_trig) # Make a paint_cleanser under the belt.. if res.bool('PaintFizzler'): pfizz = vmf.create_ent( classname='trigger_paint_cleanser', origin=start_pos, ) pfizz.solids.append(base_trig.copy()) for face in pfizz.sides(): face.mat = 'tools/toolstrigger'
def res_conveyor_belt(inst: Entity, res: Property): """Create a conveyor belt. Options: SegmentInst: Generated at each square. ('track' is the name of the path.) TrackTeleport: Set the track points so they teleport trains to the start. Speed: The fixup or number for the train speed. MotionTrig: If set, a trigger_multiple will be spawned that EnableMotions weighted cubes. The value is the name of the relevant filter. EndOutput: Adds an output to the last track. The value is the same as outputs in VMFs. RotateSegments: If true (default), force segments to face in the direction of movement RailTemplate: A template for the railings. This is made into a non-solid func_brush, combining all sections. """ move_dist = srctools.conv_int(inst.fixup['$travel_distance']) if move_dist <= 2: # There isn't room for a catwalk, so don't bother. inst.remove() return move_dir = Vec(1, 0, 0).rotate_by_str(inst.fixup['$travel_direction']) move_dir.rotate_by_str(inst['angles']) start_offset = srctools.conv_float(inst.fixup['$starting_position'], 0) teleport_to_start = res.bool('TrackTeleport', True) segment_inst_file = res['SegmentInst', ''] rail_template = res['RailTemplate', None] vmf = inst.map if segment_inst_file: segment_inst_file = conditions.resolve_inst(segment_inst_file)[0] track_speed = res['speed', None] start_pos = Vec.from_str(inst['origin']) end_pos = start_pos + move_dist * move_dir if start_offset > 0: # If an oscillating platform, move to the closest side.. offset = start_offset * move_dir # The instance is placed this far along, so move back to the end. start_pos -= offset end_pos -= offset if start_offset > 0.5: # Swap the direction of movement.. start_pos, end_pos = end_pos, start_pos inst['origin'] = start_pos # Find the angle which generates an instance pointing in the direction # of movement, with the same normal. norm = Vec(z=1).rotate_by_str(inst['angles']) for roll in range(0, 360, 90): angles = move_dir.to_angle(roll) if Vec(z=1).rotate(*angles) == norm: break else: raise ValueError( "Can't find angles to give a" ' z={} and x={}!'.format(norm, move_dir) ) if res.bool('rotateSegments', True): inst['angles'] = angles else: angles = Vec.from_str(inst['angles']) # Add the EnableMotion trigger_multiple seen in platform items. # This wakes up cubes when it starts moving. motion_filter = res['motionTrig', None] # Disable on walls, or if the conveyor can't be turned on. if norm != (0, 0, 1) or inst.fixup['$connectioncount'] == '0': motion_filter = None track_name = conditions.local_name(inst, 'segment_{}') rail_temp_solids = [] last_track = None # Place beams at the top, so they don't appear inside wall sections. beam_start = start_pos + 48 * norm # type: Vec beam_end = end_pos + 48 * norm # type: Vec for index, pos in enumerate(beam_start.iter_line(beam_end, stride=128), start=1): track = vmf.create_ent( classname='path_track', targetname=track_name.format(index) + '-track', origin=pos, spawnflags=0, orientationtype=0, # Don't rotate ) if track_speed is not None: track['speed'] = track_speed if last_track: last_track['target'] = track['targetname'] if index == 1 and teleport_to_start: track['spawnflags'] = 16 # Teleport here.. last_track = track # Don't place at the last point - it doesn't teleport correctly, # and would be one too many. if segment_inst_file and pos != end_pos: seg_inst = vmf.create_ent( classname='func_instance', targetname=track_name.format(index), file=segment_inst_file, origin=pos, angles=angles, ) seg_inst.fixup.update(inst.fixup) if rail_template: temp = conditions.import_template( rail_template, pos, angles, force_type=conditions.TEMP_TYPES.world, add_to_map=False, ) rail_temp_solids.extend(temp.world) if rail_temp_solids: vmf.create_ent( classname='func_brush', origin=beam_start, spawnflags=1, # Ignore +USE solidity=1, # Not solid vrad_brush_cast_shadows=1, drawinfastreflection=1, ).solids = rail_temp_solids if teleport_to_start: # Link back to the first track.. last_track['target'] = track_name.format(1) + '-track' # Generate an env_beam pointing from the start to the end of the track. beam_keys = res.find_key('BeamKeys', []) if beam_keys.value: beam = vmf.create_ent(classname='env_beam') # 3 offsets - x = distance from walls, y = side, z = height beam_off = beam_keys.vec('origin', 0, 63, 56) for prop in beam_keys: beam[prop.real_name] = prop.value # Localise the targetname so it can be triggered.. beam['LightningStart'] = beam['targetname'] = conditions.local_name( inst, beam['targetname', 'beam'] ) del beam['LightningEnd'] beam['origin'] = start_pos + Vec( -beam_off.x, beam_off.y, beam_off.z, ).rotate(*angles) beam['TargetPoint'] = end_pos + Vec( +beam_off.x, beam_off.y, beam_off.z, ).rotate(*angles) # Allow adding outputs to the last path_track. for prop in res.find_all('EndOutput'): output = Output.parse(prop) output.output = 'OnPass' output.inst_out = None output.comma_sep = False output.target = conditions.local_name(inst, output.target) last_track.add_out(output) if motion_filter is not None: motion_trig = vmf.create_ent( classname='trigger_multiple', targetname=conditions.local_name(inst, 'enable_motion_trig'), origin=start_pos, filtername=motion_filter, startDisabled=1, wait=0.1, ) motion_trig.add_out(Output('OnStartTouch', '!activator', 'ExitDisabledState')) # Match the size of the original... motion_trig.solids.append(vmf.make_prism( start_pos + Vec(72, -56, 58).rotate(*angles), end_pos + Vec(-72, 56, 144).rotate(*angles), mat='tools/toolstrigger', ).solid) if res.bool('NoPortalFloor'): # Block portals on the floor.. floor_noportal = vmf.create_ent( classname='func_noportal_volume', origin=beam_start, ) floor_noportal.solids.append(vmf.make_prism( start_pos + Vec(-60, -60, -66).rotate(*angles), end_pos + Vec(60, 60, -60).rotate(*angles), mat='tools/toolsinvisible', ).solid) # A brush covering under the platform. base_trig = vmf.make_prism( start_pos + Vec(-64, -64, 48).rotate(*angles), end_pos + Vec(64, 64, 56).rotate(*angles), mat='tools/toolsinvisible', ).solid vmf.add_brush(base_trig) # Make a paint_cleanser under the belt.. if res.bool('PaintFizzler'): pfizz = vmf.create_ent( classname='trigger_paint_cleanser', origin=start_pos, ) pfizz.solids.append(base_trig.copy()) for face in pfizz.sides(): face.mat = 'tools/toolstrigger'
def res_insert_overlay(inst: Entity, res: Property): """Use a template to insert one or more overlays on a surface. Options: - ID: The template ID. Brushes will be ignored. - Replace: old -> new material replacements - Face_pos: The offset of the brush face. - Normal: The direction of the brush face. - Offset: An offset to move the overlays by. """ ( temp_id, replace, face, norm, offset, ) = res.value if temp_id[:1] == '$': temp_id = inst.fixup[temp_id] origin = Vec.from_str(inst['origin']) # type: Vec angles = Vec.from_str(inst['angles', '0 0 0']) face_pos = Vec(face).rotate(*angles) face_pos += origin normal = Vec(norm).rotate(*angles) # Don't make offset change the face_pos value.. origin += offset.copy().rotate_by_str(inst['angles', '0 0 0']) for axis, norm in enumerate(normal): # Align to the center of the block grid. The normal direction is # already correct. if norm == 0: face_pos[axis] = face_pos[axis] // 128 * 128 + 64 try: face_id = SOLIDS[face_pos.as_tuple()].face.id except KeyError: LOGGER.warning( 'Overlay brush position is not valid: {}', face_pos, ) return temp = conditions.import_template( temp_id, origin, angles, targetname=inst['targetname', ''], force_type=TEMP_TYPES.detail, ) for over in temp.overlay: # type: VLib.Entity random.seed('TEMP_OVERLAY_' + over['basisorigin']) mat = random.choice( replace.get( over['material'], (over['material'], ), )) if mat[:1] == '$': mat = inst.fixup[mat] if mat.startswith('<') or mat.endswith('>'): # Lookup in the style data. import vbsp LOGGER.info('Tex: {}', vbsp.settings['textures'].keys()) mat = vbsp.get_tex(mat[1:-1]) over['material'] = mat over['sides'] = str(face_id) # Wipe the brushes from the map. if temp.detail is not None: temp.detail.remove() LOGGER.info( 'Overlay template "{}" could set keep_brushes=0.', temp_id, )
def convert_floor( loc: Vec, overlay_ids, mats, settings, signage_loc, detail, noise_weight, noise_func: SimplexNoise, ): """Cut out tiles at the specified location.""" try: brush = conditions.SOLIDS[loc.as_tuple()] except KeyError: return False # No tile here! if brush.normal == (0, 0, 1): # This is a pillar block - there isn't actually tiles here! # We need to generate a squarebeams brush to fill this gap. brush.face.mat = 'tools/toolsnodraw' # It won't be visible temp_data = conditions.import_template( temp_name=FLOOR_TEMP_PILLAR, origin=loc, ) conditions.retexture_template( temp_data, loc, # Switch to use the configured squarebeams texture replace_tex={ consts.Special.SQUAREBEAMS: random.choice(MATS['squarebeams']), }) return False # The new brush IDs overlays need to use # NOTE: strings, not ints! ant_locs = overlay_ids[str(brush.face.id)] = [] # Move the floor brush down and switch to the floorbase texture. for plane in brush.face.planes: plane.z -= FLOOR_DEPTH brush.face.mat = random.choice(mats['floorbase']) loc.x -= 64 loc.y -= 64 for x, y in utils.iter_grid(max_x=4, max_y=4): tile_loc = loc + (x * 32 + 16, y * 32 + 16, 0) if tile_loc.as_tuple() in signage_loc: # Force the tile to be present under signage.. should_make_tile = True rand = 100 # We don't need to check this again in future! signage_loc.remove(tile_loc.as_tuple()) else: # Create a number between 0-100 rand = 100 * get_noise(tile_loc // 32, noise_func) + 10 # Adjust based on the noise_weight value, so boundries have more tiles rand *= 0.1 + 0.9 * (1 - noise_weight) should_make_tile = rand < settings['floor_chance'] if random.randint(0, 7) == 0: # Sometimes there'll be random holes/extra tiles should_make_tile = not should_make_tile if should_make_tile: # Full tile tile = make_tile( p1=tile_loc - (16, 16, 0), p2=tile_loc + (16, 16, -2), top_mat=vbsp.get_tex(str(brush.color) + '.floor'), bottom_mat='tools/toolsnodraw', beam_mat=random.choice(mats['squarebeams']), ) detail.solids.append(tile.solid) ant_locs.append(str(tile.top.id)) elif rand < settings['floor_glue_chance']: # 'Glue' tile - this chance should be higher, making these appear # bordering the full tiles. tile = make_tile( p1=tile_loc - (16, 16, 1), p2=tile_loc + (16, 16, -2), top_mat=random.choice(mats['tile_glue']), bottom_mat='tools/toolsnodraw', beam_mat=random.choice(mats['squarebeams']), ) detail.solids.append(tile.solid) else: # No tile at this loc! pass return True
def res_insert_overlay(inst: VLib.Entity, res: Property): """Use a template to insert one or more overlays on a surface. Options: - ID: The template ID. Brushes will be ignored. - Replace: old -> new material replacements - Face_pos: The offset of the brush face. - Normal: The direction of the brush face. - Offset: An offset to move the overlays by. """ ( temp_id, replace, face, norm, offset, ) = res.value if temp_id[:1] == '$': temp_id = inst.fixup[temp_id] origin = Vec.from_str(inst['origin']) # type: Vec angles = Vec.from_str(inst['angles', '0 0 0']) face_pos = Vec(face).rotate(*angles) face_pos += origin normal = Vec(norm).rotate(*angles) # Don't make offset change the face_pos value.. origin += offset.copy().rotate_by_str( inst['angles', '0 0 0'] ) for axis, norm in enumerate(normal): # Align to the center of the block grid. The normal direction is # already correct. if norm == 0: face_pos[axis] = face_pos[axis] // 128 * 128 + 64 try: face_id = SOLIDS[face_pos.as_tuple()].face.id except KeyError: LOGGER.warning( 'Overlay brush position is not valid: {}', face_pos, ) return temp = conditions.import_template( temp_id, origin, angles, targetname=inst['targetname', ''], force_type=TEMP_TYPES.detail, ) for over in temp.overlay: # type: VLib.Entity random.seed('TEMP_OVERLAY_' + over['basisorigin']) mat = random.choice(replace.get( over['material'], (over['material'], ), )) if mat[:1] == '$': mat = inst.fixup[mat] over['material'] = mat over['sides'] = str(face_id) # Wipe the brushes from the map. if temp.detail is not None: temp.detail.remove() LOGGER.info( 'Overlay template "{}" could set keep_brushes=0.', temp_id, )
def res_import_template(inst, res): """Import a template VMF file, retexturing it to match orientatation. It will be placed overlapping the given instance. Options: - ID: The ID of the template to be inserted. - force: a space-seperated list of overrides. If 'white' or 'black' is present, the colour of tiles will be overriden. If 'invert' is added, white/black tiles will be swapped. If a tile size ('2x2', '4x4', 'wall', 'special') is included, all tiles will be switched to that size (if not a floor/ceiling). If 'world' or 'detail' is present, the brush will be forced to that type. - replace: A block of template material -> replacement textures. This is case insensitive - any texture here will not be altered otherwise. - replaceBrush: The position of a brush to replace (0 0 0=the surface). This brush will be removed, and overlays will be fixed to use all faces with the same normal. - keys/localkeys: If set, a brush entity will instead be generated with these values. This overrides force world/detail. The origin is set automatically. - invertVar: If this fixup value is true, tile colour will be swapped to the opposite of the current force option. If it is set to 'white' or 'black', that colour will be forced instead. """ ( temp_id, replace_tex, force_colour, force_grid, force_type, replace_brush_pos, invert_var, key_block, ) = res.value if temp_id not in TEMPLATES: # The template map is read in after setup is performed, so # it must be checked here! # We don't want an error, just quit LOGGER.warning('"{}" not a valid template!', temp_id) return if invert_var != '': invert_val = inst.fixup[invert_var].casefold() if invert_val == 'white': force_colour = conditions.MAT_TYPES.white elif invert_val == 'black': force_colour = conditions.MAT_TYPES.black elif utils.conv_bool(invert_val): force_colour = conditions.TEMP_COLOUR_INVERT[force_colour] origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles', '0 0 0']) temp_data = conditions.import_template( temp_id, origin, angles, targetname=inst['targetname', ''], force_type=force_type, ) conditions.retexture_template( temp_data, origin, replace_tex, force_colour, force_grid, ) if key_block is not None: conditions.set_ent_keys(temp_data.detail, inst, key_block) br_origin = Vec.from_str(key_block.find_key('keys')['origin']) br_origin.localise(origin, angles) temp_data.detail['origin'] = br_origin # Add it to the list of ignored brushes, so vbsp.change_brush() doesn't # modify it. vbsp.IGNORED_BRUSH_ENTS.add(temp_data.detail) # This is the original brush the template is replacing. We fix overlay # face IDs, so this brush is replaced by the faces in the template pointing # the same way. if replace_brush_pos is None: return pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z) pos += origin try: brush_group = SOLIDS[pos.as_tuple()] except KeyError: return vbsp.VMF.remove_brush(brush_group.solid) new_ids = [] all_brushes = temp_data.world # Overlays can't be applied to entities (other than func_detail). if temp_data.detail is not None and key_block is None: all_brushes.extend(temp_data.detail.solids) for brush in all_brushes: # type: VLib.Solid for face in brush.sides: # Only faces pointing the same way! if face.normal() == brush_group.normal: # Skip tool brushes (nodraw, player clips..) if face.mat.casefold().startswith('tools/'): continue new_ids.append(str(face.id)) if new_ids: conditions.reallocate_overlays({ str(brush_group.face.id): new_ids, })
def res_faith_mods(inst: Entity, res: Property): """Modify the trigger_catrapult that is created for ItemFaithPlate items. Values: - raise_trig: Raise or lower the trigger_catapults by this amount. - angled_targ, angled_in: Instance entity and input for angled plates - straight_targ, straight_in: Instance entity and input for straight plates - instvar: A $replace value to set to either 'angled' or ' 'straight'. - enabledVar: A $replace value which will be copied to the main trigger's Start Disabled value (and inverted). - trig_temp: An ID for a template brush to add. This will be offset by the trigger's position (in the case of the 'helper' trigger). """ # Get data about the trigger this instance uses for flinging fixup_var = res['instvar', ''] trig_enabled = res['enabledVar', None] trig_temp = res['trig_temp', ''] offset = srctools.conv_int(res['raise_trig', '0']) if offset: offset = Vec(0, 0, offset).rotate_by_str(inst['angles', '0 0 0']) else: offset = Vec() if trig_enabled is not None: trig_enabled = srctools.conv_bool(inst.fixup[trig_enabled]) else: trig_enabled = None for trig in vbsp.VMF.by_class['trigger_catapult']: if inst['targetname'] not in trig['targetname']: continue # Edit both the normal and the helper trigger.. trig_origin = trig['origin'] = Vec.from_str(trig['origin']) + offset if offset and not trig_temp: # No template, shift the current brushes. for solid in trig.solids: solid.translate(offset) elif trig_temp: trig.solids = conditions.import_template( temp_name=trig_temp, origin=trig_origin, angles=Vec.from_str(inst['angles']), force_type=conditions.TEMP_TYPES.world, ).world # Remove the trigger solids from worldspawn.. for solid in trig.solids: vbsp.VMF.remove_brush(solid) if trig_enabled is not None and 'helper' not in trig['targetname']: trig['startdisabled'] = srctools.bool_as_int(not trig_enabled) # Inspect the outputs to determine the type. # We also change them if desired, since that's not possible # otherwise. for out in trig.outputs: if out.inst_in == 'animate_angled_relay': # Instead of an instance: output, use local names. # This allows us to strip the proxy, as well as use # overlay instances. out.inst_in = None out.target = conditions.local_name( inst, res['angled_targ', 'animate_angled_relay']) out.input = res['angled_in', 'Trigger'] if fixup_var: inst.fixup[fixup_var] = 'angled' break # There's only one output we want to look for... elif out.inst_in == 'animate_straightup_relay': out.inst_in = None out.target = conditions.local_name( inst, res['straight_targ', 'animate_straightup_relay'], ) out.input = res['straight_in', 'Trigger'] if fixup_var: inst.fixup[fixup_var] = 'straight' break
def convert_floor( loc, overlay_ids, mats, settings, signage_loc, detail, ): """Cut out tiles at the specified location.""" try: brush = conditions.SOLIDS[loc.as_tuple()] except KeyError: return False # No tile here! if brush.normal == (0, 0, 1): # This is a pillar block - there isn't actually tiles here! # We need to generate a squarebeams brush to fill this gap. brush.face.mat = 'tools/toolsnodraw' # It won't be visible temp_data = conditions.import_template( temp_name=FLOOR_TEMP_PILLAR, origin=loc, ) conditions.retexture_template( temp_data, loc, # Switch to use the configured squarebeams texture replace_tex={ 'anim_wp/framework/squarebeams': random.choice( MATS['squarebeams'] ), } ) return False # The new brush IDs overlays need to use # NOTE: strings, not ints! ant_locs = overlay_ids[str(brush.face.id)] = [] # Move the floor brush down and switch to the floorbase texture. for plane in brush.face.planes: plane.z -= FLOOR_DEPTH brush.face.mat = random.choice(mats['floorbase']) loc.x -= 64 loc.y -= 64 random.seed('cutout_tile' + loc.join(' ')) tile_map = [ (random.randint(0, 100) < settings['floor_chance']) for _ in range(16) ] for x, y in utils.iter_grid(max_x=4, max_y=4): tile_loc = loc + (x*32 + 16, y*32 + 16, 0) if tile_loc.as_tuple() in signage_loc: should_make_tile = True # We don't need to check this again in future! signage_loc.remove(tile_loc.as_tuple()) else: should_make_tile = tile_map[x*4 + y] if should_make_tile: # Full tile tile = make_tile( p1=tile_loc - (16, 16, 0), p2=tile_loc + (16, 16, -2), top_mat=vbsp.get_tex(str(brush.color) + '.floor'), bottom_mat='tools/toolsnodraw', beam_mat=random.choice(mats['squarebeams']), ) detail.solids.append(tile.solid) ant_locs.append(str(tile.top.id)) elif random.randint(0, 100) < settings['floor_glue_chance']: # 'Glue' tile tile = make_tile( p1=tile_loc - (16, 16, 1), p2=tile_loc + (16, 16, -2), top_mat=random.choice(mats['tile_glue']), bottom_mat='tools/toolsnodraw', beam_mat=random.choice(mats['squarebeams']), ) detail.solids.append(tile.solid) else: # No tile at this loc! pass return True
def convert_floor( loc: Vec, overlay_ids, mats, settings, signage_loc, detail, noise_weight, noise_func: SimplexNoise, ): """Cut out tiles at the specified location.""" try: brush = conditions.SOLIDS[loc.as_tuple()] except KeyError: return False # No tile here! if brush.normal == (0, 0, 1): # This is a pillar block - there isn't actually tiles here! # We need to generate a squarebeams brush to fill this gap. brush.face.mat = 'tools/toolsnodraw' # It won't be visible temp_data = conditions.import_template( temp_name=FLOOR_TEMP_PILLAR, origin=loc, ) conditions.retexture_template( temp_data, loc, # Switch to use the configured squarebeams texture replace_tex={ 'anim_wp/framework/squarebeams': random.choice( MATS['squarebeams'] ), } ) return False # The new brush IDs overlays need to use # NOTE: strings, not ints! ant_locs = overlay_ids[str(brush.face.id)] = [] # Move the floor brush down and switch to the floorbase texture. for plane in brush.face.planes: plane.z -= FLOOR_DEPTH brush.face.mat = random.choice(mats['floorbase']) loc.x -= 64 loc.y -= 64 for x, y in utils.iter_grid(max_x=4, max_y=4): tile_loc = loc + (x * 32 + 16, y * 32 + 16, 0) if tile_loc.as_tuple() in signage_loc: # Force the tile to be present under signage.. should_make_tile = True rand = 100 # We don't need to check this again in future! signage_loc.remove(tile_loc.as_tuple()) else: # Create a number between 0-100 rand = 100 * get_noise(tile_loc // 32, noise_func) + 10 # Adjust based on the noise_weight value, so boundries have more tiles rand *= 0.1 + 0.9 * (1 - noise_weight) should_make_tile = rand < settings['floor_chance'] if random.randint(0, 7) == 0: # Sometimes there'll be random holes/extra tiles should_make_tile = not should_make_tile if should_make_tile: # Full tile tile = make_tile( p1=tile_loc - (16, 16, 0), p2=tile_loc + (16, 16, -2), top_mat=vbsp.get_tex(str(brush.color) + '.floor'), bottom_mat='tools/toolsnodraw', beam_mat=random.choice(mats['squarebeams']), ) detail.solids.append(tile.solid) ant_locs.append(str(tile.top.id)) elif rand < settings['floor_glue_chance']: # 'Glue' tile - this chance should be higher, making these appear # bordering the full tiles. tile = make_tile( p1=tile_loc - (16, 16, 1), p2=tile_loc + (16, 16, -2), top_mat=random.choice(mats['tile_glue']), bottom_mat='tools/toolsnodraw', beam_mat=random.choice(mats['squarebeams']), ) detail.solids.append(tile.solid) else: # No tile at this loc! pass return True