def fix_single_straight( seg: Segment, over_name: str, join_points: Dict[Tuple[str, float, float, float], Segment], overlay_joins: Dict[Segment, Set[Segment]], ) -> None: """Figure out the correct rotation for 1-long straight antlines.""" # Check the U and V axis, to see if there's another antline on both # sides. If there is that's the correct orientation. axis_u, axis_v = Vec.INV_AXIS[seg.normal.axis()] center = seg.start.copy() for off in [ Vec.with_axes(axis_u, -8), Vec.with_axes(axis_u, +8), Vec.with_axes(axis_v, -8), Vec.with_axes(axis_v, +8), ]: pos = center + off try: neigh = join_points[over_name, pos.x, pos.y, pos.z] except KeyError: continue overlay_joins[seg].add(neigh) overlay_joins[neigh].add(seg) off_min = center - abs(off) off_max = center + abs(off) # If corners are on both opposite sides, we can be fairly confident # that's the correct orientation. If we don't have that (end of trail), # settle for one side. if seg.start == seg.end: # No points found. This is our best guess. seg.start = off_min seg.end = off_max elif seg.start != off_min or seg.end != off_max: # The other side is also present. Only override if we are on both # sides. opposite = center - off if (over_name, opposite.x, opposite.y, opposite.z) in join_points: seg.start = off_min seg.end = off_max # Else: Both equal, we're fine. if seg.start == seg.end: raise ValueError('Cannot determine orientation ' 'for 1-wide straight ' 'antline at ({})!'.format(seg.start))
def _calc_fizz_angles() -> None: """Generate FIZZ_ANGLES.""" it = itertools.product('xyz', (-1, 1), 'xyz', (-1, 1)) for norm_axis, norm_mag, roll_axis, roll_mag in it: if norm_axis == roll_axis: # They can't both be the same... continue norm = Vec.with_axes(norm_axis, norm_mag) roll = Vec.with_axes(roll_axis, roll_mag) # Norm is Z, roll is X, we want y. angle = roll.to_angle_roll(norm) up_dir = norm.cross(roll) FIZZ_ANGLES[norm.as_tuple(), up_dir.as_tuple()] = angle
def test_hole_spot(origin: Vec, normal: Vec, hole_type: HoleType): """Check if the given position is valid for holes. We need to check that it's actually placed on glass/grating, and that all the parts are the same. Otherwise it'd collide with the borders. """ try: center_type = BARRIERS[origin.as_tuple(), normal.as_tuple()] except KeyError: return False if hole_type is HoleType.SMALL: return True u, v = Vec.INV_AXIS[normal.axis()] # The corners don't matter, but all 4 neighbours must be there. for u_off, v_off in [ (-128, 0), (0, -128), (128, 0), (0, 128), ]: pos = origin + Vec.with_axes(u, u_off, v, v_off) try: off_type = BARRIERS[pos.as_tuple(), normal.as_tuple()] except KeyError: # No side LOGGER.warning('No offset barrier at {}, {}', pos, normal) return False if off_type is not center_type: # Different type. LOGGER.warning('Wrong barrier type at {}, {}', pos, normal) return False return True
def _fill_norm_rotations() -> Dict[ Tuple[Tuple[float, float, float], Tuple[float, float, float]], Tuple[float, float, float] ]: """Given a norm->norm rotation, return the angles producing that.""" rotations = {} for norm_ax in 'xyz': for norm_mag in [-1, +1]: norm = Vec.with_axes(norm_ax, norm_mag) for angle_ax in 'xyz': for angle_mag in (-90, 90): angle = Vec.with_axes(angle_ax, angle_mag) new_norm = norm.copy().rotate(*angle) if new_norm != norm: rotations[tuple(norm), tuple(new_norm)] = angle.as_tuple() # Assign a null rotation as well. rotations[tuple(norm), tuple(norm)] = (0.0, 0.0, 0.0) rotations[tuple(norm), tuple(-norm)] = (0.0, 0.0, 0.0) return rotations
def beam_hole_split(axis: str, min_pos: Vec, max_pos: Vec): """Break up floor beams to fit around holes.""" # Go along the shape. For each point, check if a hole is present, # and split at that. # Our positions are centered, but we return ones at the ends. # Inset in 4 units from each end to not overlap with the frames. start_pos = min_pos - Vec.with_axes(axis, 60) if HOLES: hole_size_large = vbsp_options.get(float, 'glass_hole_size_large') / 2 hole_size_small = vbsp_options.get(float, 'glass_hole_size_small') / 2 # Extract normal from the z-axis. grid_height = min_pos.z // 128 * 128 + 64 if grid_height < min_pos.z: normal = (0, 0, 1) else: normal = (0, 0, -1) import vbsp for pos in min_pos.iter_line(max_pos, 128): try: hole_type = HOLES[(pos.x, pos.y, grid_height), normal] except KeyError: continue else: if hole_type is HoleType.SMALL: size = hole_size_small elif hole_type is HoleType.LARGE: size = hole_size_large else: raise AssertionError(hole_type) yield start_pos, pos - Vec.with_axes(axis, size) start_pos = pos + Vec.with_axes(axis, size) # Last segment, or all if no holes. yield start_pos, max_pos + Vec.with_axes(axis, 60)
def _fill_norm_rotations() -> Dict[Tuple[Tuple[float, float, float], Tuple[ float, float, float]], Matrix, ]: """Given a norm->norm rotation, return the angles producing that.""" rotations = {} for norm_ax in 'xyz': for norm_mag in [-1, +1]: norm = Vec.with_axes(norm_ax, norm_mag) for angle_ax in ('pitch', 'yaw', 'roll'): for angle_mag in (-90, 90): angle = Matrix.from_angle( Angle.with_axes(angle_ax, angle_mag)) new_norm = norm @ angle if new_norm != norm: rotations[norm.as_tuple(), new_norm.as_tuple()] = angle # Assign a null rotation as well. rotations[norm.as_tuple(), norm.as_tuple()] = Matrix() rotations[norm.as_tuple(), (-norm).as_tuple()] = Matrix() return rotations
def test_hole_spot(origin: Vec, normal: Vec, hole_type: HoleType): """Check if the given position is valid for holes. We need to check that it's actually placed on glass/grating, and that all the parts are the same. Otherwise it'd collide with the borders. """ try: center_type = BARRIERS[origin.as_tuple(), normal.as_tuple()] except KeyError: LOGGER.warning('No center barrier at {}, {}', origin, normal) return False if hole_type is HoleType.SMALL: return True u, v = Vec.INV_AXIS[normal.axis()] # The corners don't matter, but all 4 neighbours must be there. for u_off, v_off in [ (-128, 0), (0, -128), (128, 0), (0, 128), ]: pos = origin + Vec.with_axes(u, u_off, v, v_off) try: off_type = BARRIERS[pos.as_tuple(), normal.as_tuple()] except KeyError: # No side LOGGER.warning('No offset barrier at {}, {}', pos, normal) return False if off_type is not center_type: # Different type. LOGGER.warning('Wrong barrier type at {}, {}', pos, normal) return False return True
def add_glass_floorbeams(vmf: VMF, temp_name: str): """Add beams to separate large glass panels. The texture is assumed to match plasticwall004a's shape. """ template = template_brush.get_template(temp_name) temp_world, temp_detail, temp_over = template.visgrouped() try: [beam_template] = temp_world + temp_detail # type: Solid except ValueError: raise ValueError('Bad Glass Floorbeam template!') # Grab the 'end' side, which we move around. for side in beam_template.sides: if side.normal() == (-1, 0, 0): beam_end_face = side break else: raise ValueError('Not aligned to world...') separation = vbsp_options.get(int, 'glass_floorbeam_sep') + 1 separation *= 128 # First we want to find all the groups of contiguous glass sections. # This is a mapping from some glass piece to its group list. groups = {} for (origin, normal), barr_type in BARRIERS.items(): # Grating doesn't use it. if barr_type is not BarrierType.GLASS: continue normal = Vec(normal) if not normal.z: # Not walls. continue pos = Vec(origin) + normal * 62 groups[pos.as_tuple()] = [pos] # Loop over every pos and check in the +x/y directions for another glass # piece. If there, merge the two lists and set every pos in the group to # point to the new list. # Once done, every unique list = a group. for pos_tup in groups.keys(): pos = Vec(pos_tup) for off in ((128, 0, 0), (0, 128, 0)): neighbour = (pos + off).as_tuple() if neighbour in groups: our_group = groups[pos_tup] neigh_group = groups[neighbour] if our_group is neigh_group: continue # Now merge the two lists. We then need to update all dict locs # to point to the new list. if len(neigh_group) > len(our_group): small_group, large_group = our_group, neigh_group else: small_group, large_group = neigh_group, our_group large_group.extend(small_group) for pos in small_group: groups[pos.as_tuple()] = large_group # Remove duplicates objects by using the ID as key.. groups = list({ id(group): group for group in groups.values() }.values()) # Side -> u, v or None for group in groups: bbox_min, bbox_max = Vec.bbox(group) dimensions = bbox_max - bbox_min LOGGER.info('Size = {}', dimensions) # Our beams align to the smallest axis. if dimensions.y > dimensions.x: beam_ax = 'x' side_ax = 'y' rot = Vec(0, 0, 0) else: beam_ax = 'y' side_ax = 'x' rot = Vec(0, 90, 0) # Build min, max tuples for each axis in the other direction. # This tells us where the beams will be. beams = {} # type: Dict[int, Tuple[int, int]] # Add 128 so the first pos isn't a beam. offset = bbox_min[side_ax] + 128 for pos in group: side_off = pos[side_ax] beam_off = pos[beam_ax] # Skip over non-'sep' positions.. if (side_off - offset) % separation != 0: continue try: min_pos, max_pos = beams[side_off] except KeyError: beams[side_off] = beam_off, beam_off else: beams[side_off] = min(min_pos, beam_off), max(max_pos, beam_off) detail = vmf.create_ent('func_detail') for side_off, (min_off, max_off) in beams.items(): for min_pos, max_pos in beam_hole_split( beam_ax, Vec.with_axes(side_ax, side_off, beam_ax, min_off, 'z', bbox_min), Vec.with_axes(side_ax, side_off, beam_ax, max_off, 'z', bbox_min), ): if min_pos[beam_ax] >= max_pos[beam_ax]: raise ValueError(min_pos, max_pos, beam_ax) # Make the beam. # Grab the end face and snap to the length we want. beam_end_off = max_pos[beam_ax] - min_pos[beam_ax] assert beam_end_off > 0, beam_end_off for plane in beam_end_face.planes: plane.x = beam_end_off new_beam = beam_template.copy(vmf_file=vmf) new_beam.localise(min_pos, rot) detail.solids.append(new_beam)
def make_barriers(vmf: VMF, get_tex: Callable[[str], str]): """Make barrier entities. get_tex is vbsp.get_tex.""" glass_temp = template_brush.get_scaling_template( vbsp_options.get(str, "glass_template") ) grate_temp = template_brush.get_scaling_template( vbsp_options.get(str, "grating_template") ) # Avoid error without this package. if HOLES: # Grab the template solids we need. hole_temp = template_brush.get_template( vbsp_options.get(str, 'glass_hole_temp') ) hole_world, hole_detail, _ = hole_temp.visgrouped({'small'}) hole_temp_small = hole_world + hole_detail hole_world, hole_detail, _ = hole_temp.visgrouped({'large'}) hole_temp_large = hole_world + hole_detail hole_world, hole_detail, _ = hole_temp.visgrouped({'large_corner'}) hole_temp_corner = hole_world + hole_detail else: hole_temp_small = hole_temp_large = hole_temp_corner = None floorbeam_temp = vbsp_options.get(str, 'glass_floorbeam_temp') if vbsp_options.get_itemconf('BEE_PELLET:PelletGrating', False): # Merge together these existing filters in global_pti_ents vmf.create_ent( origin=vbsp_options.get(Vec, 'global_pti_ents_loc'), targetname='@grating_filter', classname='filter_multi', filtertype=0, negated=0, filter01='@not_pellet', filter02='@not_paint_bomb', ) else: # Just skip paint bombs. vmf.create_ent( origin=vbsp_options.get(Vec, 'global_pti_ents_loc'), targetname='@grating_filter', classname='filter_activator_class', negated=1, filterclass='prop_paint_bomb', ) # Group the positions by planes in each orientation. # This makes them 2D grids which we can optimise. # (normal_dist, positive_axis, type) -> [(x, y)] slices = defaultdict(set) # type: Dict[Tuple[Tuple[float, float, float], bool, BarrierType], Set[Tuple[float, float]]] # We have this on the 32-grid so we can cut squares for holes. for (origin, normal), barr_type in BARRIERS.items(): origin = Vec(origin) normal = Vec(normal) norm_axis = normal.axis() u, v = origin.other_axes(norm_axis) norm_pos = Vec.with_axes(norm_axis, origin) slice_plane = slices[ norm_pos.as_tuple(), # distance from origin to this plane. normal[norm_axis] > 0, barr_type, ] for u_off in [-48, -16, 16, 48]: for v_off in [-48, -16, 16, 48]: slice_plane.add(( (u + u_off) // 32, (v + v_off) // 32, )) # Remove pane sections where the holes are. We then generate those with # templates for slanted parts. for (origin, normal), hole_type in HOLES.items(): barr_type = BARRIERS[origin, normal] origin = Vec(origin) normal = Vec(normal) norm_axis = normal.axis() u, v = origin.other_axes(norm_axis) norm_pos = Vec.with_axes(norm_axis, origin) slice_plane = slices[ norm_pos.as_tuple(), normal[norm_axis] > 0, barr_type, ] if hole_type is HoleType.LARGE: offsets = (-80, -48, -16, 16, 48, 80) hole_temp = hole_temp_large.copy() else: offsets = (-16, 16) hole_temp = hole_temp_small.copy() for u_off in offsets: for v_off in offsets: # Skip the corners on large holes. # Those aren't actually used, so skip them. That way # we can have them diagonally or without glass in the corner. if u_off in (-80, 80) and v_off in (-80, 80): continue slice_plane.discard(( (u + u_off) // 32, (v + v_off) // 32, )) # Now generate the curved brushwork. if barr_type is BarrierType.GLASS: front_temp = glass_temp front_mat = get_tex('special.glass') elif barr_type is BarrierType.GRATING: front_temp = grate_temp front_mat = get_tex('special.grating') else: raise NotImplementedError angles = normal.to_angle(0) # Angle corresponding to the brush, for the corners. angle_list = [angles] * len(hole_temp) # This is a tricky bit. Two large templates would collide # diagonally, # so chop off the corners, then put them back only if there's not # one diagonally. if hole_type is HoleType.LARGE: for roll in (0, 90, 180, 270): corn_angles = angles.copy() corn_angles.z = roll hole_off = origin + Vec(y=128, z=128).rotate(*corn_angles) diag_type = HOLES.get( (hole_off.as_tuple(), normal.as_tuple()), None, ) if diag_type is not HoleType.LARGE: hole_temp += hole_temp_corner angle_list += [corn_angles] * len(hole_temp_corner) def solid_pane_func(off1, off2, mat): """Given the two thicknesses, produce the curved hole from the template.""" off_min = min(off1, off2) off_max = max(off1, off2) new_brushes = [ brush.copy(vmf_file=vmf) for brush in hole_temp ] for brush, br_angles in zip(new_brushes, angle_list): for face in brush.sides: face.mat = mat f_norm = face.normal() if f_norm.x == 1: face.translate(Vec(x=4 - off_max)) # face.mat = 'min' elif f_norm.x == -1: face.translate(Vec(x=-4 - off_min)) # face.mat = 'max' face.localise(origin, br_angles) return new_brushes make_glass_grating( vmf, origin, normal, barr_type, front_temp, front_mat, solid_pane_func, ) for (plane_pos, is_pos, barr_type), pos_slice in slices.items(): plane_pos = Vec(plane_pos) norm_axis = plane_pos.axis() normal = Vec.with_axes(norm_axis, 1 if is_pos else -1) if barr_type is BarrierType.GLASS: front_temp = glass_temp front_mat = get_tex('special.glass') elif barr_type is BarrierType.GRATING: front_temp = grate_temp front_mat = get_tex('special.grating') else: raise NotImplementedError u_axis, v_axis = Vec.INV_AXIS[norm_axis] for min_u, min_v, max_u, max_v in grid_optimise(dict.fromkeys(pos_slice, True)): # These are two points in the origin plane, at the borders. pos_min = Vec.with_axes( norm_axis, plane_pos, u_axis, min_u * 32, v_axis, min_v * 32, ) pos_max = Vec.with_axes( norm_axis, plane_pos, u_axis, max_u * 32 + 32, v_axis, max_v * 32 + 32, ) def solid_pane_func(pos1, pos2, mat): """Make the solid brush.""" return [vmf.make_prism( pos_min + normal * (64.0 - pos1), pos_max + normal * (64.0 - pos2), mat=mat, ).solid] make_glass_grating( vmf, (pos_min + pos_max)/2, normal, barr_type, front_temp, front_mat, solid_pane_func, ) if floorbeam_temp: LOGGER.info('Adding Glass floor beams...') add_glass_floorbeams(vmf, floorbeam_temp) LOGGER.info('Done!')
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. This must be before -250 so it runs before fizzlers and connections. """ ( sign_offset, fizz_conn_conf, inst_frame_double, inst_frame_single, blue_sign_on, blue_sign_off, oran_sign_on, oran_sign_off, ) = res.value # type: int, Optional[connections.Config], str, str, str, str, str, str import vbsp if options.get(str, 'game_id') != utils.STEAM_IDS['TAG']: # Abort - TAG fizzlers shouldn't appear in any other game! inst.remove() return fizzler = None fizzler_item = None # Look for the fizzler instance we want to replace. sign_item = connections.ITEMS[inst['targetname']] for conn in list(sign_item.outputs): if conn.to_item.name in FIZZLERS: if fizzler is None: fizzler = FIZZLERS[conn.to_item.name] fizzler_item = conn.to_item else: raise ValueError('Multiple fizzlers attached to a sign!') conn.remove() # Regardless, remove the useless output. sign_item.delete_antlines() if fizzler is None: # No fizzler - remove this sign inst.remove() return if fizzler.fizz_type.id == TAG_FIZZ_ID: LOGGER.warning('Two tag signs attached to one fizzler...') inst.remove() return # Swap to the special Tag Fizzler type. fizzler.fizz_type = FIZZ_TYPES[TAG_FIZZ_ID] # And also swap the connection's type. if fizz_conn_conf is not None: fizzler_item.config = fizz_conn_conf fizzler_item.enable_cmd = fizz_conn_conf.enable_cmd fizzler_item.disable_cmd = fizz_conn_conf.disable_cmd fizzler_item.sec_enable_cmd = fizz_conn_conf.sec_enable_cmd fizzler_item.sec_disable_cmd = fizz_conn_conf.sec_disable_cmd 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'])) fizz_norm_axis = fizzler.normal().axis() # 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'] = inst_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'] = inst_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. s, l = Vec.bbox(itertools.chain.from_iterable(fizzler.emitters)) if fizz_norm_axis == 'z': # For z-axis, just compare to the center point of the emitters. sign_dir = ((s.x + l.x) / 2, (s.y + l.y) / 2, 0) - sign_floor_loc else: # For the other two, we compare to the line, # or compare to the closest side (in line with the fizz) if fizz_norm_axis == 'x': # Extends in Y direction other_axis = 'y' side_min = s.y side_max = l.y normal = s.x else: # Extends in X direction other_axis = 'x' side_min = s.x side_max = l.x normal = s.y # Right in line with the fizzler. Point at the closest emitter. if abs(sign_floor_loc[other_axis] - normal) < 32: # Compare to the closest side. sign_dir = min( (sign_floor_loc - Vec.with_axes( fizz_norm_axis, side_min, other_axis, normal, ), sign_floor_loc - Vec.with_axes( fizz_norm_axis, side_max, other_axis, normal, )), key=Vec.mag, ) else: # Align just based on whether we're in front or behind. sign_dir = Vec.with_axes( fizz_norm_axis, normal - sign_floor_loc[fizz_norm_axis]).norm() sign_yaw = 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_yaw = (sign_yaw + 45) // 90 * 90 # Rotate to fit the instances - south is down sign_yaw = int(sign_yaw - 90) % 360 if inst_normal.z > 0: sign_angle = '0 {} 0'.format(sign_yaw) elif inst_normal.z < 0: # Flip upside-down for ceilings sign_angle = '0 {} 180'.format(sign_yaw) else: raise AssertionError('Cannot be zero here!') 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_on if blue_enabled else blue_sign_off if disable_other else None oran_sign = oran_sign_on if oran_enabled else oran_sign_off if disable_other else None if blue_sign: vmf.create_ent( classname='func_instance', file=blue_sign, targetname=inst['targetname'], angles=sign_angle, origin=blue_loc.join(' '), ) if oran_sign: vmf.create_ent( classname='func_instance', file=oran_sign, targetname=inst['targetname'], angles=sign_angle, origin=oran_loc.join(' '), ) # Now modify the fizzler... # Subtract the sign from the list of connections, but don't go below # zero fizzler.base_inst.fixup['$connectioncount'] = str( max( 0, srctools.conv_int(fizzler.base_inst.fixup['$connectioncount', '']) - 1)) # Find the direction the fizzler normal is. # Signs will associate with the given side! bbox_min, bbox_max = fizzler.emitters[0] sign_center = (bbox_min[fizz_norm_axis] + bbox_max[fizz_norm_axis]) / 2 # 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_norm_axis] < sign_center: pos_blue = blue_enabled pos_oran = oran_enabled else: neg_blue = blue_enabled neg_oran = oran_enabled # If it activates the paint gun, use different textures fizzler.tag_on_pos = pos_blue or pos_oran fizzler.tag_on_neg = neg_blue or neg_oran # Now make the trigger ents. We special-case these since they need to swap # depending on the sign config and position. 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'] = fizzler.base_inst['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = local_name(fizzler.base_inst, 'trig_pos') neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_neg') pos_trig['startdisabled'] = neg_trig['startdisabled'] = ( not fizzler.base_inst.fixup.bool('start_enabled')) pos_trig.outputs = [ Output(output, neg_trig, 'Enable'), Output(output, pos_trig, 'Disable'), ] neg_trig.outputs = [ Output(output, pos_trig, 'Enable'), Output(output, neg_trig, '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'] = local_name(fizzler.base_inst, 'trig_off') neg_trig.outputs.clear() neg_trig.add_out( Output(output, '@BlueIsEnabled', 'SetValue', param='0')) neg_trig.add_out( Output(output, '@OrangeIsEnabled', 'SetValue', param='0')) # Make the triggers. for bbox_min, bbox_max in fizzler.emitters: bbox_min = bbox_min.copy() - 64 * fizzler.up_axis bbox_max = bbox_max.copy() + 64 * fizzler.up_axis # The triggers are 8 units thick, with a 32-unit gap in the middle neg_min, neg_max = Vec(bbox_min), Vec(bbox_max) neg_min[fizz_norm_axis] -= 24 neg_max[fizz_norm_axis] -= 16 pos_min, pos_max = Vec(bbox_min), Vec(bbox_max) pos_min[fizz_norm_axis] += 16 pos_max[fizz_norm_axis] += 24 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 load_occupiedvoxel(item: Item, ent: Entity) -> None: """Parse voxel collisions contained in the VMF.""" bbox_min, bbox_max = ent.get_bbox() bbox_min = round(bbox_min, 0) bbox_max = round(bbox_max, 0) coll_type = parse_colltype(ent['coll_type']) if ent['coll_against']: coll_against = parse_colltype(ent['coll_against']) else: coll_against = None if bbox_min % 128 == (64.0, 64.0, 64.0) and bbox_max % 128 == (64.0, 64.0, 64.0): # Full voxels. for voxel in Vec.iter_grid( (bbox_min + (64, 64, 64)) / 128, (bbox_max - (64, 64, 64)) / 128, ): item.occupy_voxels.add( OccupiedVoxel( coll_type, coll_against, Coord.from_vec(voxel), )) return elif bbox_min % 32 == (0.0, 0.0, 0.0) and bbox_max % 32 == (0.0, 0.0, 0.0): # Subvoxel sections. for subvoxel in Vec.iter_grid( bbox_min / 32, (bbox_max - (32.0, 32.0, 32.0)) / 32, ): item.occupy_voxels.add( OccupiedVoxel( coll_type, coll_against, Coord.from_vec((subvoxel + (2, 2, 2)) // 4), Coord.from_vec((subvoxel - (2, 2, 2)) % 4), )) return # else, is this a surface definition? size = round(bbox_max - bbox_min, 0) for axis in ['x', 'y', 'z']: if size[axis] < 8: u, v = Vec.INV_AXIS[axis] # Figure out if we're aligned to the min or max side of the voxel. # Compute the normal, then flatten to zero thick. if bbox_min[axis] % 32 == 0: norm = +1 plane_dist = bbox_max[axis] = bbox_min[axis] elif bbox_max[axis] % 32 == 0: norm = -1 plane_dist = bbox_min[axis] = bbox_max[axis] else: # Both faces aren't aligned to the grid, skip to error. break if bbox_min[u] % 128 == bbox_min[v] % 128 == bbox_max[ v] % 128 == bbox_max[v] % 128 == 64.0: # Full voxel surface definitions. for voxel in Vec.iter_grid( Vec.with_axes(u, bbox_min[u] + 64, v, bbox_min[v] + 64, axis, plane_dist + 64 * norm) / 128, Vec.with_axes(u, bbox_max[u] - 64, v, bbox_max[v] - 64, axis, plane_dist + 64 * norm) / 128, ): item.occupy_voxels.add( OccupiedVoxel( coll_type, coll_against, Coord.from_vec(voxel), normal=Coord.from_vec(Vec.with_axes(axis, norm)), )) return elif bbox_min[u] % 32 == bbox_min[v] % 32 == bbox_max[ v] % 32 == bbox_max[v] % 32 == 0.0: # Subvoxel surface definitions. return else: # Not aligned to grid, skip to error. break LOGGER.warning( 'Unknown occupied voxel definition: ({}) - ({}), type="{}", against="{}"', bbox_min, bbox_max, ent['coll_type'], ent['coll_against'], )
def add_glass_floorbeams(vmf: VMF, temp_name: str): """Add beams to separate large glass panels. The texture is assumed to match plasticwall004a's shape. """ template = template_brush.get_template(temp_name) temp_world, temp_detail, temp_over = template.visgrouped() try: [beam_template] = temp_world + temp_detail # type: Solid except ValueError: raise ValueError('Bad Glass Floorbeam template!') # Grab the 'end' side, which we move around. for side in beam_template.sides: if side.normal() == (-1, 0, 0): beam_end_face = side break else: raise ValueError('Not aligned to world...') separation = options.get(int, 'glass_floorbeam_sep') + 1 separation *= 128 # First we want to find all the groups of contiguous glass sections. # This is a mapping from some glass piece to its group list. groups = {} for (origin, normal), barr_type in BARRIERS.items(): # Grating doesn't use it. if barr_type is not BarrierType.GLASS: continue normal = Vec(normal) if not normal.z: # Not walls. continue pos = Vec(origin) + normal * 62 groups[pos.as_tuple()] = [pos] # Loop over every pos and check in the +x/y directions for another glass # piece. If there, merge the two lists and set every pos in the group to # point to the new list. # Once done, every unique list = a group. for pos_tup in groups.keys(): pos = Vec(pos_tup) for off in ((128, 0, 0), (0, 128, 0)): neighbour = (pos + off).as_tuple() if neighbour in groups: our_group = groups[pos_tup] neigh_group = groups[neighbour] if our_group is neigh_group: continue # Now merge the two lists. We then need to update all dict # locations to point to the new list. if len(neigh_group) > len(our_group): small_group, large_group = our_group, neigh_group else: small_group, large_group = neigh_group, our_group large_group.extend(small_group) for pos in small_group: groups[pos.as_tuple()] = large_group # Remove duplicates objects by using the ID as key.. groups = list({id(group): group for group in groups.values()}.values()) # Side -> u, v or None for group in groups: bbox_min, bbox_max = Vec.bbox(group) dimensions = bbox_max - bbox_min # Our beams align to the smallest axis. if dimensions.y > dimensions.x: beam_ax = 'x' side_ax = 'y' rot = Matrix() else: beam_ax = 'y' side_ax = 'x' rot = Matrix.from_yaw(90) # Build min, max tuples for each axis in the other direction. # This tells us where the beams will be. beams: dict[float, tuple[float, float]] = {} # Add 128 so the first pos isn't a beam. offset = bbox_min[side_ax] + 128 for pos in group: side_off = pos[side_ax] beam_off = pos[beam_ax] # Skip over non-'sep' positions.. if (side_off - offset) % separation != 0: continue try: min_pos, max_pos = beams[side_off] except KeyError: beams[side_off] = beam_off, beam_off else: beams[side_off] = min(min_pos, beam_off), max(max_pos, beam_off) detail = vmf.create_ent('func_detail') for side_off, (min_off, max_off) in beams.items(): for min_pos, max_pos in beam_hole_split( beam_ax, Vec.with_axes(side_ax, side_off, beam_ax, min_off, 'z', bbox_min), Vec.with_axes(side_ax, side_off, beam_ax, max_off, 'z', bbox_min), ): if min_pos[beam_ax] >= max_pos[beam_ax]: raise ValueError(min_pos, max_pos, beam_ax) # Make the beam. # Grab the end face and snap to the length we want. beam_end_off = max_pos[beam_ax] - min_pos[beam_ax] assert beam_end_off > 0, beam_end_off for plane in beam_end_face.planes: plane.x = beam_end_off new_beam = beam_template.copy(vmf_file=vmf) new_beam.localise(min_pos, rot) detail.solids.append(new_beam)
def group_props_ent( prop_groups: Dict[Optional[tuple], List[StaticProp]], rejected: List[StaticProp], get_model: Callable[[str], Tuple[Optional[QC], Optional[Model]]], bbox_ents: List[Entity], min_cluster: int, ) -> Iterator[List[StaticProp]]: """Given the groups of props, merge props according to the provided ents.""" # Ents with group names. We have to split those by filter too. grouped_sets: Dict[Tuple[str, FrozenSet[str]], CombineVolume] = {} # Skinset filter -> volumes that match. sets_by_skin: Dict[FrozenSet[str], List[CombineVolume]] = defaultdict(list) empty_fs = frozenset('') for ent in bbox_ents: origin = Vec.from_str(ent['origin']) skinset = empty_fs mdl_name = ent['prop'] if mdl_name: qc, mdl = get_model(mdl_name) if mdl is not None: skinset = frozenset({ tex.casefold().replace('\\', '/') for tex in mdl.iter_textures([conv_int(ent['skin'])]) }) # Compute 6 planes to use for collision detection. mat = Matrix.from_angle(Angle.from_str(ent['angles'])) mins, maxes = Vec.bbox( Vec.from_str(ent['mins']), Vec.from_str(ent['maxs']), ) size = maxes - mins # Enlarge slightly to ensure it never has a zero area. # Otherwise the normal could potentially be invalid. mins -= 0.05 maxes += 0.05 # Group name group_name = ent['name'] if group_name: try: combine_set = grouped_sets[group_name, skinset] except KeyError: combine_set = grouped_sets[group_name, skinset] = CombineVolume( group_name, skinset, origin) sets_by_skin[skinset].append(combine_set) else: combine_set = CombineVolume(group_name, skinset, origin) sets_by_skin[skinset].append(combine_set) combine_set.volume += size.x * size.y * size.z # For each direction, compute a position on the plane and # the normal vector. combine_set.collision.append([( origin + Vec.with_axes(axis, offset) @ mat, Vec.with_axes(axis, norm) @ mat, ) for offset, norm in zip([mins, maxes], (-1, 1)) for axis in ('x', 'y', 'z')]) # We want to apply a ordering to groups, so smaller ones apply first, and # filtered ones override all others. for group_list in sets_by_skin.values(): group_list.sort(key=operator.attrgetter('volume')) # Groups with no filter have no skins in the group. unfiltered_group = sets_by_skin.get(frozenset(), []) # Each of these groups cannot be merged with other ones. for group_key, group in prop_groups.items(): if group_key is None: continue # No point merging single/empty groups. group_skinset = group_key[0] if len(group) < min_cluster: rejected.extend(group) group.clear() continue for combine_set in itertools.chain(sets_by_skin.get(group_skinset, ()), unfiltered_group): found = [] for prop in list(group): if combine_set.contains(prop.origin): found.append(prop) combine_set.used = True actual = set(found).intersection(group) if len(actual) >= min_cluster: yield list(actual) for prop in actual: group.remove(prop) # Finally, reject all the ones not in a bbox. for group in prop_groups.values(): rejected.extend(group) # And log unused groups for combine_set_list in sets_by_skin.values(): for combine_set in combine_set_list: if not combine_set.used: LOGGER.warning('Unused comp_propcombine_set {}', combine_set.desc)
def parse_antlines( vmf: VMF) -> Tuple[Dict[str, List[Antline]], Dict[int, List[Segment]]]: """Convert overlays in the map into Antline objects. This returns two dicts. The first maps targetnames to lists of antlines. The second maps solid IDs to segments, for assigning TileDefs to them. """ # We want to reconstruct the shape of the antline path. # To do that we find the neighbouring points for each overlay. LOGGER.info('Parsing antlines...') # segment -> found neighbours of it. overlay_joins = defaultdict(set) # type: Dict[Segment, Set[Segment]] segment_to_name = {} # type: Dict[Segment, str] # Points on antlines where two can connect. For corners that's each side, # for straight it's each end. Combine that with the targetname # so we only join related antlines. join_points = {} # type: Dict[Tuple[str, float, float, float], Segment] mat_straight = consts.Antlines.STRAIGHT mat_corner = consts.Antlines.CORNER side_to_seg = {} # type: Dict[int, List[Segment]] antlines = {} # type: Dict[str, List[Antline]] for over in vmf.by_class['info_overlay']: mat = over['material'] origin = Vec.from_str(over['basisorigin']) normal = Vec.from_str(over['basisnormal']) u, v = Vec.INV_AXIS[normal.axis()] if mat == mat_corner: seg_type = SegType.CORNER start = end = origin # One on each side - we know the size. points = [ origin + Vec.with_axes(u, +8), origin + Vec.with_axes(u, -8), origin + Vec.with_axes(v, +8), origin + Vec.with_axes(v, -8), ] elif mat == mat_straight: seg_type = SegType.STRAIGHT # We want to determine the length first. long_axis = Vec(y=1).rotate_by_str(over['angles']).axis() side_axis = Vec(x=1).rotate_by_str(over['angles']).axis() # The order of these isn't correct, but we need the neighbours to # fix that. start, end = overlay_bounds(over) # For whatever reason, Valve sometimes generates antlines which are # shortened by 1 unit. So snap those to grid. start = round(start / 16) * 16 end = round(end / 16) * 16 if end[long_axis] - start[long_axis] == 16: # Special case. # 1-wide antlines don't have the correct # rotation, pointing always in the U axis. # So we need to figure that out to get the correct links. # For now just create the segment with dummy values. start = end = origin points = [] else: offset = Vec.with_axes(side_axis, 8) start += offset end -= offset points = [start, end] else: # It's not an antline. continue seg = Segment(seg_type, normal, start, end) segment_to_name[seg] = over_name = over['targetname'] for side_id in over['sides'].split(): side_to_seg.setdefault(int(side_id), []).append(seg) for point in points: # Lookup the point to see if we've already checked it. # If not, write us into that spot. neighbour = join_points.setdefault( (over_name, point.x, point.y, point.z), seg, ) if neighbour is seg: # None found continue overlay_joins[neighbour].add(seg) overlay_joins[seg].add(neighbour) # Remove original from the map. over.remove() # Now fix the square straight segments. for seg, over_name in segment_to_name.items(): if seg.type is SegType.STRAIGHT and seg.start == seg.end: fix_single_straight(seg, over_name, join_points, overlay_joins) # Now, finally compute each continuous section. for segment, over_name in segment_to_name.items(): try: neighbours = overlay_joins[segment] except KeyError: continue # Done already. if len(neighbours) != 1: continue # Found a start point! segments = [segment] for segment in segments: neighbours = overlay_joins.pop(segment) # Except KeyError: this segment's already done?? for neighbour in neighbours: if neighbour not in segments: segments.append(neighbour) antlines.setdefault(over_name, []).append(Antline(over_name, segments)) LOGGER.info('Done! ({} antlines)'.format(sum(map(len, antlines.values())))) return antlines, side_to_seg
def make_barriers(vmf: VMF): """Make barrier entities. get_tex is vbsp.get_tex.""" glass_temp = template_brush.get_scaling_template( vbsp_options.get(str, "glass_template")) grate_temp = template_brush.get_scaling_template( vbsp_options.get(str, "grating_template")) # Avoid error without this package. if HOLES: # Grab the template solids we need. hole_temp = template_brush.get_template( vbsp_options.get(str, 'glass_hole_temp')) hole_world, hole_detail, _ = hole_temp.visgrouped({'small'}) hole_temp_small = hole_world + hole_detail hole_world, hole_detail, _ = hole_temp.visgrouped({'large'}) hole_temp_large = hole_world + hole_detail hole_world, hole_detail, _ = hole_temp.visgrouped({'large_corner'}) hole_temp_corner = hole_world + hole_detail else: hole_temp_small = hole_temp_large = hole_temp_corner = None floorbeam_temp = vbsp_options.get(str, 'glass_floorbeam_temp') if vbsp_options.get_itemconf('BEE_PELLET:PelletGrating', False): # Merge together these existing filters in global_pti_ents vmf.create_ent( origin=vbsp_options.get(Vec, 'global_pti_ents_loc'), targetname='@grating_filter', classname='filter_multi', filtertype=0, negated=0, filter01='@not_pellet', filter02='@not_paint_bomb', ) else: # Just skip paint bombs. vmf.create_ent( origin=vbsp_options.get(Vec, 'global_pti_ents_loc'), targetname='@grating_filter', classname='filter_activator_class', negated=1, filterclass='prop_paint_bomb', ) # Group the positions by planes in each orientation. # This makes them 2D grids which we can optimise. # (normal_dist, positive_axis, type) -> [(x, y)] slices = defaultdict( set ) # type: Dict[Tuple[Tuple[float, float, float], bool, BarrierType], Set[Tuple[float, float]]] # We have this on the 32-grid so we can cut squares for holes. for (origin, normal), barr_type in BARRIERS.items(): origin = Vec(origin) normal = Vec(normal) norm_axis = normal.axis() u, v = origin.other_axes(norm_axis) norm_pos = Vec.with_axes(norm_axis, origin) slice_plane = slices[ norm_pos.as_tuple(), # distance from origin to this plane. normal[norm_axis] > 0, barr_type, ] for u_off in [-48, -16, 16, 48]: for v_off in [-48, -16, 16, 48]: slice_plane.add(( (u + u_off) // 32, (v + v_off) // 32, )) # Remove pane sections where the holes are. We then generate those with # templates for slanted parts. for (origin, normal), hole_type in HOLES.items(): barr_type = BARRIERS[origin, normal] origin = Vec(origin) normal = Vec(normal) norm_axis = normal.axis() u, v = origin.other_axes(norm_axis) norm_pos = Vec.with_axes(norm_axis, origin) slice_plane = slices[norm_pos.as_tuple(), normal[norm_axis] > 0, barr_type, ] if hole_type is HoleType.LARGE: offsets = (-80, -48, -16, 16, 48, 80) hole_temp = hole_temp_large.copy() else: offsets = (-16, 16) hole_temp = hole_temp_small.copy() for u_off in offsets: for v_off in offsets: # Skip the corners on large holes. # Those aren't actually used, so skip them. That way # we can have them diagonally or without glass in the corner. if u_off in (-80, 80) and v_off in (-80, 80): continue slice_plane.discard(( (u + u_off) // 32, (v + v_off) // 32, )) # Now generate the curved brushwork. if barr_type is BarrierType.GLASS: front_temp = glass_temp elif barr_type is BarrierType.GRATING: front_temp = grate_temp else: raise NotImplementedError angles = normal.to_angle(0) # Angle corresponding to the brush, for the corners. angle_list = [angles] * len(hole_temp) # This is a tricky bit. Two large templates would collide # diagonally, # so chop off the corners, then put them back only if there's not # one diagonally. if hole_type is HoleType.LARGE: for roll in (0, 90, 180, 270): corn_angles = angles.copy() corn_angles.z = roll hole_off = origin + Vec(y=128, z=128).rotate(*corn_angles) diag_type = HOLES.get( (hole_off.as_tuple(), normal.as_tuple()), None, ) if diag_type is not HoleType.LARGE: hole_temp += hole_temp_corner angle_list += [corn_angles] * len(hole_temp_corner) def solid_pane_func(off1, off2, mat): """Given the two thicknesses, produce the curved hole from the template.""" off_min = min(off1, off2) off_max = max(off1, off2) new_brushes = [brush.copy(vmf_file=vmf) for brush in hole_temp] for brush, br_angles in zip(new_brushes, angle_list): for face in brush.sides: face.mat = mat f_norm = face.normal() if f_norm.x == 1: face.translate(Vec(x=4 - off_max)) # face.mat = 'min' elif f_norm.x == -1: face.translate(Vec(x=-4 - off_min)) # face.mat = 'max' face.localise(origin, br_angles) return new_brushes make_glass_grating( vmf, origin, normal, barr_type, front_temp, solid_pane_func, ) for (plane_pos, is_pos, barr_type), pos_slice in slices.items(): plane_pos = Vec(plane_pos) norm_axis = plane_pos.axis() normal = Vec.with_axes(norm_axis, 1 if is_pos else -1) if barr_type is BarrierType.GLASS: front_temp = glass_temp elif barr_type is BarrierType.GRATING: front_temp = grate_temp else: raise NotImplementedError u_axis, v_axis = Vec.INV_AXIS[norm_axis] for min_u, min_v, max_u, max_v in grid_optimise( dict.fromkeys(pos_slice, True)): # These are two points in the origin plane, at the borders. pos_min = Vec.with_axes( norm_axis, plane_pos, u_axis, min_u * 32, v_axis, min_v * 32, ) pos_max = Vec.with_axes( norm_axis, plane_pos, u_axis, max_u * 32 + 32, v_axis, max_v * 32 + 32, ) def solid_pane_func(pos1, pos2, mat): """Make the solid brush.""" return [ vmf.make_prism( pos_min + normal * (64.0 - pos1), pos_max + normal * (64.0 - pos2), mat=mat, ).solid ] make_glass_grating( vmf, (pos_min + pos_max) / 2, normal, barr_type, front_temp, solid_pane_func, ) if floorbeam_temp: LOGGER.info('Adding Glass floor beams...') add_glass_floorbeams(vmf, floorbeam_temp) LOGGER.info('Done!')
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 fizzler = None # Look for the fizzler instance we want to replace for targetname in inst.output_targets(): try: fizzler = FIZZLERS[targetname] except KeyError: # Not a fizzler. # It's an indicator toggle, remove it and the antline to clean up. for ent in vmf.by_target[targetname]: remove_ant_toggle(ent) inst.outputs.clear() # Remove the outputs now, they're not valid anyway. if fizzler is None: # No fizzler - remove this sign inst.remove() return if fizzler.fizz_type.id == 'TAG_FIZZ_ID': LOGGER.warning('Two tag signs attached to one fizzler...') inst.remove() return # Swap to the special Tag Fizzler type. fizzler.fizz_type = FIZZ_TYPES[TAG_FIZZ_ID] # 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 = calc_fizzler_orient(fizzler) # 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. sign_dir = min( sign_floor_loc - Vec.with_axes( axis,side_min, other_axis, normal, ), sign_floor_loc - Vec.with_axes( 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... # Subtract the sign from the list of connections, but don't go below # zero fizzler.base_inst.fixup['$connectioncount'] = str(max( 0, srctools.conv_int(fizzler.base_inst.fixup['$connectioncount', ''], 0) - 1 )) # Find the direction the fizzler normal is. # Signs will associate with the given side! bbox_min, bbox_max = fizzler.emitters[0] fizz_field_axis = (bbox_max-bbox_min).norm() fizz_norm_axis = fizzler.normal().axis() sign_center = (bbox_min[fizz_norm_axis] + bbox_max[fizz_norm_axis]) / 2 # 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_norm_axis] < sign_center: pos_blue = blue_enabled pos_oran = oran_enabled else: neg_blue = blue_enabled neg_oran = oran_enabled # If it activates the paint gun, use different textures fizzler.tag_on_pos = pos_blue or pos_oran fizzler.tag_on_neg = neg_blue or neg_oran # Now make the trigger ents. We special-case these since they need to swap # depending on the sign config and position. 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'] = fizzler.base_inst['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = local_name(fizzler.base_inst, 'trig_pos') neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_neg') pos_trig.outputs = [ Output(output, neg_trig, 'Enable'), Output(output, pos_trig, 'Disable'), ] neg_trig.outputs = [ Output(output, pos_trig, 'Enable'), Output(output, neg_trig, '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'] = local_name(fizzler.base_inst, 'trig_off') neg_trig.outputs.clear() neg_trig.add_out(Output( output, '@BlueIsEnabled', 'SetValue', param='0' )) neg_trig.add_out(Output( output, '@OrangeIsEnabled', 'SetValue', param='0' )) # Make the triggers. for bbox_min, bbox_max in fizzler.emitters: bbox_min = bbox_min.copy() - 64 * fizzler.up_axis bbox_max = bbox_max.copy() + 64 * fizzler.up_axis # The triggers are 8 units thick, with a 32-unit gap in the middle neg_min, neg_max = Vec(bbox_min), Vec(bbox_max) neg_min[fizz_norm_axis] -= 24 neg_max[fizz_norm_axis] -= 16 pos_min, pos_max = Vec(bbox_min), Vec(bbox_max) pos_min[fizz_norm_axis] += 16 pos_max[fizz_norm_axis] += 24 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 group_props_ent( prop_groups: Dict[Optional[tuple], List[StaticProp]], rejected: List[StaticProp], get_model: Callable[[str], Tuple[Optional[QC], Optional[Model]]], bbox_ents: List[Entity], min_cluster: int, ) -> Iterator[List[StaticProp]]: """Given the groups of props, merge props according to the provided ents.""" # (name, skinset) -> list of boxes, constructed as 6 (pos, norm) tuples. combine_sets = defaultdict( list ) # type: Dict[Tuple[str, FrozenSet[str]], List[List[Tuple[Vec, Vec]]]] empty_fs = frozenset('') for ent in bbox_ents: # Either provided name, or unique value. name = ent['name'] or format(int(ent['hammerid']), 'X') origin = Vec.from_str(ent['origin']) skinset = empty_fs mdl_name = ent['prop'] if mdl_name: qc, mdl = get_model(mdl_name) if mdl is not None: skinset = frozenset({ tex.casefold().replace('\\', '/') for tex in mdl.iter_textures([conv_int(ent['skin'])]) }) # Compute 6 planes to use for collision detection. mat = Matrix.from_angle(Angle.from_str(ent['angles'])) mins, maxes = Vec.bbox( Vec.from_str(ent['mins']), Vec.from_str(ent['maxs']), ) # Enlarge slightly to ensure it never has a zero area. # Otherwise the normal could potentially be invalid. mins -= 0.05 maxes += 0.05 # For each direction, compute a position on the plane and # the normal vector. combine_sets[name, skinset].append([( origin + Vec.with_axes(axis, offset) @ mat, Vec.with_axes(axis, norm) @ mat, ) for offset, norm in zip([mins, maxes], (-1, 1)) for axis in ('x', 'y', 'z')]) # Each of these groups cannot be merged with other ones. for group_key, group in prop_groups.items(): if group_key is None: continue # No point merging single/empty groups. group_skinset = group_key[0] if len(group) < min_cluster: rejected.extend(group) group.clear() continue for (name, skinset), boxes in combine_sets.items(): if skinset and skinset != group_skinset: continue # No match found = defaultdict(list) # type: Dict[int, List[StaticProp]] for prop in list(group): for box in boxes: if bsp_collision(prop.origin, box): # Group by this box object's identity. # That's a cheap way to keep each propcombine set # grouped uniquely. found[id(boxes)].append(prop) break for subgroup in found.values(): actual = set(subgroup).intersection(group) if len(actual) >= min_cluster: yield list(actual) for prop in actual: group.remove(prop) # Finally, reject all the ones not in a bbox. for group in prop_groups.values(): rejected.extend(group)
def make_frames(vmf: VMF, targ: str, conf: dict, bbox_min: Vec, bbox_max: Vec, norm: Vec): """Generate frames for a rectangular glass item.""" def make_frame(frame_type, loc, angles): """Make a frame instance.""" vmf.create_ent( classname='func_instance', targetname=targ, file=conf['frame_' + frame_type], # Position at the center of the block, instead of at the glass. origin=loc - norm * 64, angles=angles, ) if bbox_min == bbox_max: # 1x1 glass.. make_frame('single', bbox_min, norm.to_angle()) return norm_axis = norm.axis() u_axis, v_axis = Vec.INV_AXIS[norm_axis] u_norm = Vec() v_norm = Vec() u_norm[u_axis] = 1 v_norm[v_axis] = 1 single_u = bbox_min[u_axis] == bbox_max[u_axis] single_v = bbox_min[v_axis] == bbox_max[v_axis] # If single in either direction, it needs a u-bend. if single_u: ubend_axis = v_axis elif single_v: ubend_axis = u_axis else: ubend_axis = None if ubend_axis is not None: for bend_mag, bbox in [(1, bbox_min), (-1, bbox_max)]: make_frame( 'ubend', bbox, norm.to_angle_roll(Vec.with_axes(ubend_axis, bend_mag)), ) else: # Make 4 corners - one in each roll direction. for roll in range(0, 360, 90): angles = norm.to_angle(roll) # The two directions with a border in the corner instance. # We want to put it on those sides. corner_a = Vec(y=-1).rotate(*angles) corner_b = Vec(z=-1).rotate(*angles) # If the normal is positive, we want to be bbox_max in that axis, # otherwise bbox_min. pos = Vec.with_axes( norm_axis, bbox_min, corner_a.axis(), (bbox_max if corner_a >= (0, 0, 0) else bbox_min), corner_b.axis(), (bbox_max if corner_b >= (0, 0, 0) else bbox_min), ) make_frame( 'corner', pos, angles, ) # Make straight sections. straight_u_pos = norm.to_angle_roll(v_norm) straight_u_neg = norm.to_angle_roll(-v_norm) straight_v_pos = norm.to_angle_roll(u_norm) straight_v_neg = norm.to_angle_roll(-u_norm) for u_pos in range(int(bbox_min[u_axis] + 128), int(bbox_max[u_axis]), 128): make_frame( 'edge', Vec.with_axes(u_axis, u_pos, v_axis, bbox_min, norm_axis, bbox_min), straight_u_pos, ) make_frame( 'edge', Vec.with_axes(u_axis, u_pos, v_axis, bbox_max, norm_axis, bbox_min), straight_u_neg, ) for v_pos in range(int(bbox_min[v_axis] + 128), int(bbox_max[v_axis]), 128): make_frame( 'edge', Vec.with_axes(v_axis, v_pos, u_axis, bbox_min, norm_axis, bbox_min), straight_v_pos, ) make_frame( 'edge', Vec.with_axes(v_axis, v_pos, u_axis, bbox_max, norm_axis, bbox_min), straight_v_neg, )
def res_fix_rotation_axis(vmf: VMF, ent: Entity, res: Property): """Properly setup rotating brush entities to match the instance. This uses the orientation of the instance to determine the correct spawnflags to make it rotate in the correct direction. This can either modify an existing entity (which may be in an instance), or generate a new one. The generated brush will be 2x2x2 units large, and always set to be non-solid. For both modes: - `Axis`: specifies the rotation axis local to the instance. - `Reversed`: If set, flips the direction around. - `Classname`: Specifies which entity, since the spawnflags required varies. For application to an existing entity: - `ModifyTarget`: The local name of the entity to modify. For brush generation mode: - `Pos` and `name` are local to the instance, and will set the `origin` and `targetname` respectively. - `Keys` are any other keyvalues to be be set. - `Flags` sets additional spawnflags. Multiple values may be separated by `+`, and will be added together. - `Classname` specifies which entity will be created, as well as which other values will be set to specify the correct orientation. - `AddOut` is used to add outputs to the generated entity. It takes the options `Output`, `Target`, `Input`, `Inst_targ`, `Param` and `Delay`. If `Inst_targ` is defined, it will be used with the input to construct an instance proxy input. If `OnceOnly` is set, the output will be deleted when fired. Permitted entities: * [`func_door_rotating`](https://developer.valvesoftware.com/wiki/func_door_rotating) * [`func_platrot`](https://developer.valvesoftware.com/wiki/func_platrot) * [`func_rot_button`](https://developer.valvesoftware.com/wiki/func_rot_button) * [`func_rotating`](https://developer.valvesoftware.com/wiki/func_rotating) * [`momentary_rot_button`](https://developer.valvesoftware.com/wiki/momentary_rot_button) """ des_axis = res['axis', 'z'].casefold() reverse = res.bool('reversed') door_type = res['classname', 'func_door_rotating'] orient = round(Matrix.from_angle(Angle.from_str(ent['angles'])), 6) axis = Vec.with_axes(des_axis, 1) @ orient if axis.x > 0 or axis.y > 0 or axis.z > 0: # If it points forward, we need to reverse the rotating door reverse = not reverse try: flag_values = FLAG_ROTATING[door_type] except KeyError: LOGGER.warning('Unknown rotating brush type "{}"!', door_type) return name = res['ModifyTarget', ''] if name: name = conditions.local_name(ent, name) setter_loc = ent['origin'] else: # Generate a brush. name = conditions.local_name(ent, res['name', '']) pos = res.vec('Pos') @ Angle.from_str(ent['angles', '0 0 0']) pos += Vec.from_str(ent['origin', '0 0 0']) setter_loc = str(pos) door_ent = vmf.create_ent( classname=door_type, targetname=name, origin=pos.join(' '), # Extra stuff to apply to the flags (USE, toggle, etc) spawnflags=sum( map( # Add together multiple values srctools.conv_int, res['flags', '0'].split('+') # Make the door always non-solid! )) | flag_values.get('solid_flags', 0), ) conditions.set_ent_keys(door_ent, ent, res) for output in res.find_all('AddOut'): door_ent.add_out( Output( out=output['Output', 'OnUse'], inp=output['Input', 'Use'], targ=output['Target', ''], inst_in=output['Inst_targ', None], param=output['Param', ''], delay=srctools.conv_float(output['Delay', '']), times=(1 if srctools.conv_bool(output['OnceOnly', False]) else -1), )) # Generate brush door_ent.solids = [vmf.make_prism(pos - 1, pos + 1).solid] # Add or remove flags as needed by creating KV setters. for flag, value in zip( ('x', 'y', 'z', 'rev'), [axis.x != 0, axis.y != 0, axis.z != 0, reverse], ): if flag in flag_values: vmf.create_ent( 'comp_kv_setter', origin=setter_loc, target=name, mode='flags', kv_name=flag_values[flag], kv_value_local=value, ) # This ent uses a keyvalue for reversing... if door_type == 'momentary_rot_button': vmf.create_ent( 'comp_kv_setter', origin=setter_loc, target=name, mode='kv', kv_name='StartDirection', kv_value_local='1' if reverse else '-1', )
def make_barriers(vmf: VMF): """Make barrier entities. get_tex is vbsp.get_tex.""" glass_temp = template_brush.get_scaling_template( options.get(str, "glass_template")) grate_temp = template_brush.get_scaling_template( options.get(str, "grating_template")) hole_temp_small: List[Solid] hole_temp_lrg_diag: List[Solid] hole_temp_lrg_cutout: List[Solid] hole_temp_lrg_square: List[Solid] # Avoid error without this package. if HOLES: # Grab the template solids we need. hole_combined_temp = template_brush.get_template( options.get(str, 'glass_hole_temp')) hole_world, hole_detail, _ = hole_combined_temp.visgrouped({'small'}) hole_temp_small = hole_world + hole_detail hole_world, hole_detail, _ = hole_combined_temp.visgrouped( {'large_diagonal'}) hole_temp_lrg_diag = hole_world + hole_detail hole_world, hole_detail, _ = hole_combined_temp.visgrouped( {'large_cutout'}) hole_temp_lrg_cutout = hole_world + hole_detail hole_world, hole_detail, _ = hole_combined_temp.visgrouped( {'large_square'}) hole_temp_lrg_square = hole_world + hole_detail else: hole_temp_small = hole_temp_lrg_diag = hole_temp_lrg_cutout = hole_temp_lrg_square = [] floorbeam_temp = options.get(str, 'glass_floorbeam_temp') if options.get_itemconf('BEE_PELLET:PelletGrating', False): # Merge together these existing filters in global_pti_ents vmf.create_ent( origin=options.get(Vec, 'global_pti_ents_loc'), targetname='@grating_filter', classname='filter_multi', filtertype=0, negated=0, filter01='@not_pellet', filter02='@not_paint_bomb', ) else: # Just skip paint bombs. vmf.create_ent( origin=options.get(Vec, 'global_pti_ents_loc'), targetname='@grating_filter', classname='filter_activator_class', negated=1, filterclass='prop_paint_bomb', ) # Group the positions by planes in each orientation. # This makes them 2D grids which we can optimise. # (normal_dist, positive_axis, type) -> [(x, y)] slices: Dict[Tuple[Tuple[float, float, float], bool, BarrierType], Dict[Tuple[int, int], False]] = defaultdict(dict) # We have this on the 32-grid so we can cut squares for holes. for (origin_tup, normal_tup), barr_type in BARRIERS.items(): origin = Vec(origin_tup) normal = Vec(normal_tup) norm_axis = normal.axis() u, v = origin.other_axes(norm_axis) norm_pos = Vec.with_axes(norm_axis, origin) slice_plane = slices[ norm_pos.as_tuple(), # distance from origin to this plane. normal[norm_axis] > 0, barr_type, ] for u_off in [-48, -16, 16, 48]: for v_off in [-48, -16, 16, 48]: slice_plane[int((u + u_off) // 32), int((v + v_off) // 32), ] = True # Remove pane sections where the holes are. We then generate those with # templates for slanted parts. for (origin_tup, norm_tup), hole_type in HOLES.items(): barr_type = BARRIERS[origin_tup, norm_tup] origin = Vec(origin_tup) normal = Vec(norm_tup) norm_axis = normal.axis() u, v = origin.other_axes(norm_axis) norm_pos = Vec.with_axes(norm_axis, origin) slice_plane = slices[norm_pos.as_tuple(), normal[norm_axis] > 0, barr_type, ] if hole_type is HoleType.LARGE: offsets = (-80, -48, -16, 16, 48, 80) else: offsets = (-16, 16) for u_off in offsets: for v_off in offsets: # Remove these squares, but keep them in the dict # so we can check if there was glass there. uv = ( int((u + u_off) // 32), int((v + v_off) // 32), ) if uv in slice_plane: slice_plane[uv] = False # These have to be present, except for the corners # on the large hole. elif abs(u_off) != 80 or abs(v_off) != 80: u_ax, v_ax = Vec.INV_AXIS[norm_axis] LOGGER.warning( 'Hole tried to remove missing tile at ({})?', Vec.with_axes(norm_axis, norm_pos, u_ax, u + u_off, v_ax, v + v_off), ) # Now generate the curved brushwork. if barr_type is BarrierType.GLASS: front_temp = glass_temp elif barr_type is BarrierType.GRATING: front_temp = grate_temp else: raise NotImplementedError angles = normal.to_angle() hole_temp: List[Tuple[List[Solid], Matrix]] = [] # This is a tricky bit. Two large templates would collide # diagonally, and we allow the corner glass to not be present since # the hole doesn't actually use that 32x32 segment. # So we need to determine which of 3 templates to use. corn_angles = angles.copy() if hole_type is HoleType.LARGE: for corn_angles.roll in (0, 90, 180, 270): corn_mat = Matrix.from_angle(corn_angles) corn_dir = Vec(y=1, z=1) @ corn_angles hole_off = origin + 128 * corn_dir diag_type = HOLES.get( (hole_off.as_tuple(), normal.as_tuple()), None, ) corner_pos = origin + 80 * corn_dir corn_u, corn_v = corner_pos.other_axes(norm_axis) corn_u = int(corn_u // 32) corn_v = int(corn_v // 32) if diag_type is HoleType.LARGE: # There's another large template to this direction. # Just have 1 generate both combined, so the brushes can # be more optimal. To pick, arbitrarily make the upper one # be in charge. if corn_v > v // 32: hole_temp.append((hole_temp_lrg_diag, corn_mat)) continue if (corn_u, corn_v) in slice_plane: hole_temp.append((hole_temp_lrg_square, corn_mat)) else: hole_temp.append((hole_temp_lrg_cutout, corn_mat)) else: hole_temp.append((hole_temp_small, Matrix.from_angle(angles))) def solid_pane_func(off1: float, off2: float, mat: str) -> List[Solid]: """Given the two thicknesses, produce the curved hole from the template.""" off_min = 64 - max(off1, off2) off_max = 64 - min(off1, off2) new_brushes = [] for brushes, matrix in hole_temp: for orig_brush in brushes: brush = orig_brush.copy(vmf_file=vmf) new_brushes.append(brush) for face in brush.sides: face.mat = mat for point in face.planes: if point.x > 64: point.x = off_max else: point.x = off_min face.localise(origin, matrix) # Increase precision, these are small detail brushes. face.lightmap = 8 return new_brushes make_glass_grating( vmf, origin, normal, barr_type, front_temp, solid_pane_func, ) for (plane_pos, is_pos, barr_type), pos_slice in slices.items(): plane_pos = Vec(plane_pos) norm_axis = plane_pos.axis() normal = Vec.with_axes(norm_axis, 1 if is_pos else -1) if barr_type is BarrierType.GLASS: front_temp = glass_temp elif barr_type is BarrierType.GRATING: front_temp = grate_temp else: raise NotImplementedError u_axis, v_axis = Vec.INV_AXIS[norm_axis] for min_u, min_v, max_u, max_v in grid_optimise(pos_slice): # These are two points in the origin plane, at the borders. pos_min = Vec.with_axes( norm_axis, plane_pos, u_axis, min_u * 32, v_axis, min_v * 32, ) pos_max = Vec.with_axes( norm_axis, plane_pos, u_axis, max_u * 32 + 32, v_axis, max_v * 32 + 32, ) def solid_pane_func(pos1: float, pos2: float, mat: str) -> List[Solid]: """Make the solid brush.""" return [ vmf.make_prism( pos_min + normal * (64.0 - pos1), pos_max + normal * (64.0 - pos2), mat=mat, ).solid ] make_glass_grating( vmf, (pos_min + pos_max) / 2 + 63 * normal, normal, barr_type, front_temp, solid_pane_func, ) # Generate hint brushes, to ensure sorting is done correctly. [hint] = solid_pane_func(0, 4.0, consts.Tools.SKIP) for side in hint: if abs(Vec.dot(side.normal(), normal)) > 0.99: side.mat = consts.Tools.HINT vmf.add_brush(hint) if floorbeam_temp: LOGGER.info('Adding Glass floor beams...') add_glass_floorbeams(vmf, floorbeam_temp) LOGGER.info('Done!')
def export(self, vmf: VMF, wall_conf: AntType, floor_conf: AntType) -> None: """Add the antlines into the map.""" # First, do some optimisation. If corners aren't defined, try and # optimise those antlines out by merging the straight segment # before/after it into the corners. if not wall_conf.tex_corner or not floor_conf.tex_corner: collapse_line = list(self.line) # type: List[Optional[Segment]] for i, seg in enumerate(collapse_line): if seg is None or seg.type is not SegType.STRAIGHT: continue if (floor_conf if seg.on_floor else wall_conf).tex_corner: continue for corner_ind in [i - 1, i + 1]: if i == -1: continue try: corner = collapse_line[corner_ind] except IndexError: # Each end of the list. continue if (corner is not None and corner.type is SegType.CORNER and corner.normal == seg.normal): corner_pos = corner.start if (seg.start - corner_pos).mag_sq() == 8**2: # The line segment is at the border between them, # the corner is at the center. So move double the # distance towards the corner, so it reaches to the # other side of the corner and replaces it. seg.start += 2 * (corner_pos - seg.start) # Remove corner by setting to None, so we aren't # resizing the list constantly. collapse_line[corner_ind] = None # Now merge together the tiledefs. seg.tiles.update(corner.tiles) elif (seg.end - corner_pos).mag_sq() == 8**2: seg.end += 2 * (corner_pos - seg.end) collapse_line[corner_ind] = None seg.tiles.update(corner.tiles) self.line[:] = [seg for seg in collapse_line if seg is not None] LOGGER.info('Collapsed {} antline corners', collapse_line.count(None)) for seg in self.line: conf = floor_conf if seg.on_floor else wall_conf random.seed('ant {} {}'.format(seg.start, seg.end)) if seg.type is SegType.CORNER: mat: AntTex if random.randrange(100) < conf.broken_chance: mat = random.choice(conf.broken_corner or conf.broken_straight) else: mat = random.choice(conf.tex_corner or conf.tex_straight) axis_u, axis_v = Vec.INV_AXIS[seg.normal.axis()] self._make_overlay( vmf, seg, seg.start, Vec.with_axes(axis_u, 16), 16 * seg.normal.cross(Vec.with_axes(axis_u, 1)), mat, ) else: # Straight # TODO: Break up these segments. for a, b, is_broken in seg.broken_iter(conf.broken_chance): if is_broken: mat = random.choice(conf.broken_straight) else: mat = random.choice(conf.tex_straight) self._make_straight( vmf, seg, a, b, mat, )
def edit_panel(vmf: VMF, inst: Entity, props: Property, create: bool) -> None: """Implements SetPanelOptions and CreatePanel.""" orient = Matrix.from_angle(Angle.from_str(inst['angles'])) normal: Vec = round(props.vec('normal', 0, 0, 1) @ orient, 6) origin = Vec.from_str(inst['origin']) uaxis, vaxis = Vec.INV_AXIS[normal.axis()] points: Set[Tuple[float, float, float]] = set() if 'point' in props: for prop in props.find_all('point'): points.add( conditions.resolve_offset(inst, prop.value, zoff=-64).as_tuple()) elif 'pos1' in props and 'pos2' in props: pos1, pos2 = Vec.bbox( conditions.resolve_offset(inst, props['pos1', '-48 -48 0'], zoff=-64), conditions.resolve_offset(inst, props['pos2', '48 48 0'], zoff=-64), ) points.update(map(Vec.as_tuple, Vec.iter_grid(pos1, pos2, 32))) else: # Default to the full tile. points.update({(Vec(u, v, -64.0) @ orient + origin).as_tuple() for u in [-48.0, -16.0, 16.0, 48.0] for v in [-48.0, -16.0, 16.0, 48.0]}) tiles_to_uv: Dict[tiling.TileDef, Set[Tuple[int, int]]] = defaultdict(set) for pos in points: try: tile, u, v = tiling.find_tile(Vec(pos), normal, force=create) except KeyError: continue tiles_to_uv[tile].add((u, v)) if not tiles_to_uv: LOGGER.warning('"{}": No tiles found for panels!', inst['targetname']) return # If bevels is provided, parse out the overall world positions. bevel_world: Optional[Set[Tuple[int, int]]] try: bevel_prop = props.find_key('bevel') except NoKeyError: bevel_world = None else: bevel_world = set() if bevel_prop.has_children(): # Individually specifying offsets. for bevel_str in bevel_prop.as_array(): bevel_point = Vec.from_str(bevel_str) @ orient + origin bevel_world.add( (int(bevel_point[uaxis]), int(bevel_point[vaxis]))) elif srctools.conv_bool(bevel_prop.value): # Fill the bounding box. bbox_min, bbox_max = Vec.bbox(map(Vec, points)) off = Vec.with_axes(uaxis, 32, vaxis, 32) bbox_min -= off bbox_max += off for pos in Vec.iter_grid(bbox_min, bbox_max, 32): if pos.as_tuple() not in points: bevel_world.add((pos[uaxis], pos[vaxis])) # else: No bevels. panels: List[tiling.Panel] = [] for tile, uvs in tiles_to_uv.items(): if create: panel = tiling.Panel( None, inst, tiling.PanelType.NORMAL, thickness=4, bevels=(), ) panel.points = uvs tile.panels.append(panel) else: for panel in tile.panels: if panel.same_item(inst) and panel.points == uvs: break else: LOGGER.warning('No panel to modify found for "{}"!', inst['targetname']) continue panels.append(panel) pan_type = '<nothing?>' try: pan_type = conditions.resolve_value(inst, props['type']) panel.pan_type = tiling.PanelType(pan_type.lower()) except LookupError: pass except ValueError: raise ValueError('Unknown panel type "{}"!'.format(pan_type)) if 'thickness' in props: panel.thickness = srctools.conv_int( conditions.resolve_value(inst, props['thickness'])) if panel.thickness not in (2, 4, 8): raise ValueError( '"{}": Invalid panel thickess {}!\n' 'Must be 2, 4 or 8.', inst['targetname'], panel.thickness, ) if bevel_world is not None: panel.bevels.clear() for u, v in bevel_world: # Convert from world points to UV positions. u = (u - tile.pos[uaxis] + 48) / 32 v = (v - tile.pos[vaxis] + 48) / 32 # Cull outside here, we wont't use them. if -1 <= u <= 4 and -1 <= v <= 4: panel.bevels.add((u, v)) if 'offset' in props: panel.offset = conditions.resolve_offset(inst, props['offset']) panel.offset -= Vec.from_str(inst['origin']) if 'template' in props: # We only want the template inserted once. So remove it from all but one. if len(panels) == 1: panel.template = conditions.resolve_value( inst, props['template']) else: panel.template = '' if 'nodraw' in props: panel.nodraw = srctools.conv_bool( conditions.resolve_value(inst, props['nodraw'])) if 'seal' in props: panel.seal = srctools.conv_bool( conditions.resolve_value(inst, props['seal'])) if 'move_bullseye' in props: panel.steals_bullseye = srctools.conv_bool( conditions.resolve_value(inst, props['move_bullseye'])) if 'keys' in props or 'localkeys' in props: # First grab the existing ent, so we can edit it. # These should all have the same value, unless they were independently # edited with mismatching point sets. In that case destroy all those existing ones. existing_ents: Set[Optional[Entity]] = { panel.brush_ent for panel in panels } try: [brush_ent] = existing_ents except ValueError: LOGGER.warning( 'Multiple independent panels for "{}" were made, then the ' 'brush entity was edited as a group! Discarding ' 'individual ents...', inst['targetname']) for brush_ent in existing_ents: if brush_ent is not None and brush_ent in vmf.entities: brush_ent.remove() brush_ent = None if brush_ent is None: brush_ent = vmf.create_ent('') old_pos = brush_ent.keys.pop('origin', None) conditions.set_ent_keys(brush_ent, inst, props) if not brush_ent['classname']: if create: # This doesn't make sense, you could just omit the prop. LOGGER.warning( 'No classname provided for panel "{}"!', inst['targetname'], ) # Make it a world brush. brush_ent.remove() brush_ent = None else: # We want to do some post-processing. # Localise any origin value. if 'origin' in brush_ent.keys: pos = Vec.from_str(brush_ent['origin']) pos.localise( Vec.from_str(inst['origin']), Vec.from_str(inst['angles']), ) brush_ent['origin'] = pos elif old_pos is not None: brush_ent['origin'] = old_pos # If it's func_detail, clear out all the keys. # Particularly `origin`, but the others are useless too. if brush_ent['classname'] == 'func_detail': brush_ent.clear_keys() brush_ent['classname'] = 'func_detail' for panel in panels: panel.brush_ent = brush_ent
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. This must be before -250 so it runs before fizzlers and connections. """ ( sign_offset, fizz_io_type, inst_frame_double, inst_frame_single, blue_sign_on, blue_sign_off, oran_sign_on, oran_sign_off, ) = res.value # type: int, ItemType, str, str, str, str, str, str 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 fizzler = None fizzler_item = None # Look for the fizzler instance we want to replace. sign_item = ITEMS[inst['targetname']] for conn in list(sign_item.outputs): if conn.to_item.name in FIZZLERS: if fizzler is None: fizzler = FIZZLERS[conn.to_item.name] fizzler_item = conn.to_item else: raise ValueError('Multiple fizzlers attached to a sign!') conn.remove() # Regardless, remove the useless output. sign_item.delete_antlines() if fizzler is None: # No fizzler - remove this sign inst.remove() return if fizzler.fizz_type.id == TAG_FIZZ_ID: LOGGER.warning('Two tag signs attached to one fizzler...') inst.remove() return # Swap to the special Tag Fizzler type. fizzler.fizz_type = FIZZ_TYPES[TAG_FIZZ_ID] # And also swap the connection's type. fizzler_item.item_type = fizz_io_type fizzler_item.enable_cmd = fizz_io_type.enable_cmd fizzler_item.disable_cmd = fizz_io_type.disable_cmd fizzler_item.sec_enable_cmd = fizz_io_type.sec_enable_cmd fizzler_item.sec_disable_cmd = fizz_io_type.sec_disable_cmd 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'] = inst_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'] = inst_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 = calc_fizzler_orient(fizzler) # 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. sign_dir = min( sign_floor_loc - Vec.with_axes( axis,side_min, other_axis, normal, ), sign_floor_loc - Vec.with_axes( 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_on if blue_enabled else blue_sign_off if disable_other else None oran_sign = oran_sign_on if oran_enabled else oran_sign_off if disable_other else None if blue_sign: vmf.create_ent( classname='func_instance', file=blue_sign, targetname=inst['targetname'], angles=sign_angle, origin=blue_loc.join(' '), ) if oran_sign: vmf.create_ent( classname='func_instance', file=oran_sign, targetname=inst['targetname'], angles=sign_angle, origin=oran_loc.join(' '), ) # Now modify the fizzler... # Subtract the sign from the list of connections, but don't go below # zero fizzler.base_inst.fixup['$connectioncount'] = str(max( 0, srctools.conv_int(fizzler.base_inst.fixup['$connectioncount', '']) - 1 )) # Find the direction the fizzler normal is. # Signs will associate with the given side! bbox_min, bbox_max = fizzler.emitters[0] fizz_norm_axis = fizzler.normal().axis() sign_center = (bbox_min[fizz_norm_axis] + bbox_max[fizz_norm_axis]) / 2 # 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_norm_axis] < sign_center: pos_blue = blue_enabled pos_oran = oran_enabled else: neg_blue = blue_enabled neg_oran = oran_enabled # If it activates the paint gun, use different textures fizzler.tag_on_pos = pos_blue or pos_oran fizzler.tag_on_neg = neg_blue or neg_oran # Now make the trigger ents. We special-case these since they need to swap # depending on the sign config and position. 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'] = fizzler.base_inst['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = local_name(fizzler.base_inst, 'trig_pos') neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_neg') pos_trig['startdisabled'] = neg_trig['startdisabled'] = ( not fizzler.base_inst.fixup.bool('start_enabled') ) pos_trig.outputs = [ Output(output, neg_trig, 'Enable'), Output(output, pos_trig, 'Disable'), ] neg_trig.outputs = [ Output(output, pos_trig, 'Enable'), Output(output, neg_trig, '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'] = local_name(fizzler.base_inst, 'trig_off') neg_trig.outputs.clear() neg_trig.add_out(Output( output, '@BlueIsEnabled', 'SetValue', param='0' )) neg_trig.add_out(Output( output, '@OrangeIsEnabled', 'SetValue', param='0' )) # Make the triggers. for bbox_min, bbox_max in fizzler.emitters: bbox_min = bbox_min.copy() - 64 * fizzler.up_axis bbox_max = bbox_max.copy() + 64 * fizzler.up_axis # The triggers are 8 units thick, with a 32-unit gap in the middle neg_min, neg_max = Vec(bbox_min), Vec(bbox_max) neg_min[fizz_norm_axis] -= 24 neg_max[fizz_norm_axis] -= 16 pos_min, pos_max = Vec(bbox_min), Vec(bbox_max) pos_min[fizz_norm_axis] += 16 pos_max[fizz_norm_axis] += 24 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, )