def make_alpha_base(vmf: VMF, bbox_min: Vec, bbox_max: Vec, noise: SimplexNoise): """Add the base to a CutoutTile, using displacements.""" # We want to limit the size of brushes to 512, so the vertexes don't # get too far apart. # This now contains each point from beginning to end inclusive. x, y, z = bbox_min dim_x = bbox_max.x - bbox_min.x dim_y = bbox_max.y - bbox_min.y widths = [x for x in range(0, int(dim_x), 512)] + [dim_x] heights = [y for y in range(0, int(dim_y), 512)] + [dim_y] # Loop over two offset copies, so we get a min and max each time. for x1, x2 in zip(widths, widths[1:]): for y1, y2 in zip(heights, heights[1:]): # We place our displacement 1 unit above the surface, then offset # the verts down. brush = vmf.make_prism( Vec(x + x1, y + y1, z - FLOOR_DEPTH), Vec(x + x2, y + y2, z - FLOOR_DEPTH - 1), ) brush.top.mat = random.choice(MATS['floorbase_disp']) make_displacement( brush.top, offset=-1, noise=noise, ) vmf.add_brush(brush.solid)
def make_alpha_base(vmf: VMF, bbox_min: Vec, bbox_max: Vec, noise: SimplexNoise): """Add the base to a CutoutTile, using displacements.""" # We want to limit the size of brushes to 512, so the vertexes don't # get too far apart. # This now contains each point from beginning to end inclusive. x, y, z = bbox_min dim_x = bbox_max.x - bbox_min.x dim_y = bbox_max.y - bbox_min.y widths = [ x for x in range(0, int(dim_x), 512) ] + [dim_x] heights = [ y for y in range(0, int(dim_y), 512) ] + [dim_y] # Loop over two offset copies, so we get a min and max each time. for x1, x2 in zip(widths, widths[1:]): for y1, y2 in zip(heights, heights[1:]): # We place our displacement 1 unit above the surface, then offset # the verts down. brush = vmf.make_prism( Vec(x + x1, y + y1, z - FLOOR_DEPTH), Vec(x + x2, y + y2, z - FLOOR_DEPTH - 1), ) brush.top.mat = random.choice(MATS['floorbase_disp']) make_displacement( brush.top, offset=-1, noise=noise, ) vmf.add_brush(brush.solid)
def res_conveyor_belt(vmf: VMF, inst: Entity, res: Property) -> None: """Create a conveyor belt. * Options: * `SegmentInst`: Generated at each square. (`track` is the name of the path to attach to.) * `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 `EnableMotion`s 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. * `BeamKeys`: If set, a list of keyvalues to use to generate an env_beam travelling from start to end. The origin is treated specially - X is the distance from walls, y is the distance to the side, and z is the height. `RailTemplate`: A template for the track sections. This is made into a non-solid func_brush, combining all sections. * `NoPortalFloor`: If set, add a `func_noportal_volume` on the floor under the track. * `PaintFizzler`: If set, add a paint fizzler underneath the belt. """ move_dist = inst.fixup.int('$travel_distance') if move_dist <= 2: # There isn't room for a conveyor, so don't bother. inst.remove() return orig_orient = Matrix.from_angle(Angle.from_str(inst['angles'])) move_dir = Vec(1, 0, 0) @ Angle.from_str(inst.fixup['$travel_direction']) move_dir = move_dir @ orig_orient start_offset = inst.fixup.float('$starting_position') teleport_to_start = res.bool('TrackTeleport', True) segment_inst_file = instanceLocs.resolve_one(res['SegmentInst', '']) rail_template = res['RailTemplate', None] 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 norm = orig_orient.up() if res.bool('rotateSegments', True): orient = Matrix.from_basis(x=move_dir, z=norm) inst['angles'] = orient.to_angle() else: orient = orig_orient # 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 tracks at the top, so they don't appear inside wall sections. track_start: Vec = start_pos + 48 * norm track_end: Vec = end_pos + 48 * norm for index, pos in enumerate(track_start.iter_line(track_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 != track_end: seg_inst = conditions.add_inst( vmf, targetname=track_name.format(index), file=segment_inst_file, origin=pos, angles=orient, ) seg_inst.fixup.update(inst.fixup) if rail_template: temp = template_brush.import_template( vmf, rail_template, pos, orient, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) rail_temp_solids.extend(temp.world) if rail_temp_solids: vmf.create_ent( classname='func_brush', origin=track_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. try: beam_keys = res.find_key('BeamKeys') except LookupError: pass else: beam = vmf.create_ent(classname='env_beam') 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, ) @ orient beam['TargetPoint'] = end_pos + Vec( +beam_off.x, beam_off.y, beam_off.z, ) @ orient # 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) @ orient, end_pos + Vec(-72, 56, 144) @ orient, mat=consts.Tools.TRIGGER, ).solid) if res.bool('NoPortalFloor'): # Block portals on the floor.. floor_noportal = vmf.create_ent( classname='func_noportal_volume', origin=track_start, ) floor_noportal.solids.append( vmf.make_prism( start_pos + Vec(-60, -60, -66) @ orient, end_pos + Vec(60, 60, -60) @ orient, mat=consts.Tools.INVISIBLE, ).solid) # A brush covering under the platform. base_trig = vmf.make_prism( start_pos + Vec(-64, -64, 48) @ orient, end_pos + Vec(64, 64, 56) @ orient, mat=consts.Tools.INVISIBLE, ).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 = consts.Tools.TRIGGER
def res_add_brush(vmf: VMF, inst: Entity, res: Property) -> None: """Spawn in a brush at the indicated points. - `point1` and `point2` are locations local to the instance, with `0 0 0` as the floor-position. - `type` is either `black` or `white`. - detail should be set to `1/0`. If true the brush will be a func_detail instead of a world brush. The sides will be textured with 1x1, 2x2 or 4x4 wall, ceiling and floor textures as needed. """ origin = Vec.from_str(inst['origin']) angles = Angle.from_str(inst['angles']) point1 = Vec.from_str(res['point1']) point2 = Vec.from_str(res['point2']) point1.z -= 64 # Offset to the location of the floor point2.z -= 64 # Rotate to match the instance point1 = point1 @ angles + origin point2 = point2 @ angles + origin try: tex_type = texturing.Portalable(res['type', 'black']) except ValueError: LOGGER.warning( 'AddBrush: "{}" is not a valid brush ' 'color! (white or black)', res['type'], ) tex_type = texturing.Portalable.BLACK dim = round(point2 - point1, 6) dim.max(-dim) # Figure out what grid size and scale is needed # Check the dimensions in two axes to figure out the largest # tile size that can fit in it. tile_grids = { 'x': tiling.TileSize.TILE_4x4, 'y': tiling.TileSize.TILE_4x4, 'z': tiling.TileSize.TILE_4x4, } for axis in 'xyz': u, v = Vec.INV_AXIS[axis] max_size = min(dim[u], dim[v]) if max_size % 128 == 0: tile_grids[axis] = tiling.TileSize.TILE_1x1 elif dim[u] % 64 == 0 and dim[v] % 128 == 0: tile_grids[axis] = tiling.TileSize.TILE_2x1 elif max_size % 64 == 0: tile_grids[axis] = tiling.TileSize.TILE_2x2 else: tile_grids[axis] = tiling.TileSize.TILE_4x4 solids = vmf.make_prism(point1, point2) solids.north.mat = texturing.gen( texturing.GenCat.NORMAL, Vec(Vec.N), tex_type, ).get(solids.north.get_origin(), tile_grids['y']) solids.south.mat = texturing.gen( texturing.GenCat.NORMAL, Vec(Vec.S), tex_type, ).get(solids.north.get_origin(), tile_grids['y']) solids.east.mat = texturing.gen( texturing.GenCat.NORMAL, Vec(Vec.E), tex_type, ).get(solids.north.get_origin(), tile_grids['x']) solids.west.mat = texturing.gen( texturing.GenCat.NORMAL, Vec(Vec.W), tex_type, ).get(solids.north.get_origin(), tile_grids['x']) solids.top.mat = texturing.gen( texturing.GenCat.NORMAL, Vec(Vec.T), tex_type, ).get(solids.north.get_origin(), tile_grids['z']) solids.bottom.mat = texturing.gen( texturing.GenCat.NORMAL, Vec(Vec.B), tex_type, ).get(solids.north.get_origin(), tile_grids['z']) if res.bool('detail'): # Add the brush to a func_detail entity vmf.create_ent(classname='func_detail').solids = [solids.solid] else: # Add to the world vmf.add_brush(solids.solid)
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 make_pit_shell(vmf: VMF): """If the pit is surrounded on all sides, we can just extend walls down. That avoids needing to use skybox workarounds.""" LOGGER.info('Making pit shell...') for x in range(-8, 20): for y in range(-8, 20): block_types = [ brushLoc.POS[x, y, z] for z in range(-15, 1) ] lowest = max(( z for z in range(-15, 1) if block_types[z] is not brushLoc.Block.VOID ), default=None) if lowest is None: continue # TODO: For opened areas (wheatley), generate a floor... real_pos = brushLoc.grid_to_world(Vec(x, y, 0)) prism = vmf.make_prism( real_pos + (64, 64, BOTTOMLESS_PIT_MIN + 8), real_pos + (-64, -64, BOTTOMLESS_PIT_MIN), mat='tools/toolsnodraw', ) prism.bottom.mat = consts.Special.BACKPANELS_CHEAP vmf.add_brush(prism.solid) continue if block_types[lowest].is_solid: real_pos = brushLoc.grid_to_world(Vec(x, y, lowest)) for z in range(0, 10): br_pos = real_pos - (0, 0, 512 * z) vmf.add_brush( vmf.make_prism(br_pos + 64, br_pos - (64, 64, 512-64), vbsp.BLACK_PAN[1]).solid ) prism = vmf.make_prism( Vec(-8 * 128, -8 * 128, -4864), Vec(20 * 128, 20 * 128, -4896), ) prism.top.mat = 'tools/toolsblack' vmf.add_brush(prism.solid) diss_trig = vmf.create_ent( classname='trigger_multiple', spawnflags=4104, wait=0.1, origin=vbsp_options.get(Vec, 'global_pti_ents_loc'), ) diss_trig.solids = [vmf.make_prism( Vec(-8 * 128, -8 * 128, -4182), Vec(20 * 128, 20 * 128, -4864), mat='tools/toolstrigger', ).solid] diss_trig.add_out( Output('OnStartTouch', '!activator', 'SilentDissolve'), Output('OnStartTouch', '!activator', 'Break', delay=0.1), Output('OnStartTouch', '!activator', 'Kill', delay=0.5), ) # Since we can chuck gel down the pit, cover it in a noportal_volume # to stop players from portalling past the hurt trigger. diss_trig = vmf.create_ent( classname='func_noportal_volume', origin=vbsp_options.get(Vec, 'global_pti_ents_loc'), ) diss_trig.solids = [vmf.make_prism( Vec(-8 * 128, -8 * 128, -64), Vec(20 * 128, 20 * 128, -4864), mat='tools/toolstrigger', ).solid]
def res_cutout_tile(vmf: srctools.VMF, res: Property): """Generate random quarter tiles, like in Destroyed or Retro maps. - "MarkerItem" is the instance to look for. - "TileSize" can be "2x2" or "4x4". - rotateMax is the amount of degrees to rotate squarebeam models. Materials: - "squarebeams" is the squarebeams variant to use. - "ceilingwalls" are the sides of the ceiling section. - "floorbase" is the texture under floor sections. - "tile_glue" is used on top of a thinner tile segment. - "clip" is the player_clip texture used over floor segments. (This allows customising the surfaceprop.) - "Floor4x4Black", "Ceil2x2White" and other combinations can be used to override the textures used. """ item = instanceLocs.resolve(res['markeritem']) INST_LOCS = {} # Map targetnames -> surface loc CEIL_IO = [] # Pairs of ceil inst corners to cut out. FLOOR_IO = [] # Pairs of floor inst corners to cut out. overlay_ids = {} # When we replace brushes, we need to fix any overlays # on that surface. MATS.clear() floor_edges = [] # Values to pass to add_floor_sides() at the end sign_loc = set(FORCE_LOCATIONS) # If any signage is present in the map, we need to force tiles to # appear at that location! for over in vmf.by_class['info_overlay']: if (over['material'].casefold() in FORCE_TILE_MATS and # Only check floor/ceiling overlays over['basisnormal'] in ('0 0 1', '0 0 -1')): loc = Vec.from_str(over['origin']) # Sometimes (light bridges etc) a sign will be halfway between # tiles, so in that case we need to force 2 tiles. loc_min = (loc - (15, 15, 0)) // 32 * 32 # type: Vec loc_max = (loc + (15, 15, 0)) // 32 * 32 # type: Vec loc_min += (16, 16, 0) loc_max += (16, 16, 0) FORCE_LOCATIONS.add(loc_min.as_tuple()) FORCE_LOCATIONS.add(loc_max.as_tuple()) SETTINGS = { 'floor_chance': srctools.conv_int(res['floorChance', '100'], 100), 'ceil_chance': srctools.conv_int(res['ceilingChance', '100'], 100), 'floor_glue_chance': srctools.conv_int(res['floorGlueChance', '0']), 'ceil_glue_chance': srctools.conv_int(res['ceilingGlueChance', '0']), 'rotate_beams': int(srctools.conv_float(res['rotateMax', '0']) * BEAM_ROT_PRECISION), 'beam_skin': res['squarebeamsSkin', '0'], 'base_is_disp': srctools.conv_bool(res['dispBase', '0']), 'quad_floor': res['FloorSize', '4x4'].casefold() == '2x2', 'quad_ceil': res['CeilingSize', '4x4'].casefold() == '2x2', } random.seed(vbsp.MAP_RAND_SEED + '_CUTOUT_TILE_NOISE') noise = SimplexNoise(period=4 * 40) # 4 tiles/block, 50 blocks max # We want to know the number of neighbouring tile cutouts before # placing tiles - blocks away from the sides generate fewer tiles. floor_neighbours = defaultdict(dict) # all_floors[z][x,y] = count for mat_prop in res.find_key('Materials', []): MATS[mat_prop.name].append(mat_prop.value) if SETTINGS['base_is_disp']: # We want the normal brushes to become nodraw. MATS['floorbase_disp'] = MATS['floorbase'] MATS['floorbase'] = ['tools/toolsnodraw'] # Since this uses random data for initialisation, the alpha and # regular will use slightly different patterns. alpha_noise = SimplexNoise(period=4 * 50) else: alpha_noise = None for key, default in TEX_DEFAULT: if key not in MATS: MATS[key] = [default] # Find our marker ents for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in item: continue targ = inst['targetname'] orient = Vec(0, 0, 1).rotate_by_str(inst['angles', '0 0 0']) # Check the orientation of the marker to figure out what to generate if orient == (0, 0, 1): io_list = FLOOR_IO else: io_list = CEIL_IO # Reuse orient to calculate where the solid face will be. loc = (orient * -64) + Vec.from_str(inst['origin']) INST_LOCS[targ] = loc for out in inst.output_targets(): io_list.append((targ, out)) if not inst.outputs and inst.fixup['$connectioncount'] == '0': # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 128x128 tile segment. io_list.append((targ, targ)) inst.remove() # Remove the instance itself from the map. for start_floor, end_floor in FLOOR_IO: if end_floor not in INST_LOCS: # Not a marker - remove this and the antline. for toggle in vmf.by_target[end_floor]: conditions.remove_ant_toggle(toggle) continue box_min = Vec(INST_LOCS[start_floor]) box_min.min(INST_LOCS[end_floor]) box_max = Vec(INST_LOCS[start_floor]) box_max.max(INST_LOCS[end_floor]) if box_min.z != box_max.z: continue # They're not in the same level! z = box_min.z if SETTINGS['rotate_beams']: # We have to generate 1 model per 64x64 block to do rotation... gen_rotated_squarebeams( vmf, box_min - (64, 64, 0), box_max + (64, 64, -8), skin=SETTINGS['beam_skin'], max_rot=SETTINGS['rotate_beams'], ) else: # Make the squarebeams props, using big models if possible gen_squarebeams(vmf, box_min + (-64, -64, 0), box_max + (64, 64, -8), skin=SETTINGS['beam_skin']) # Add a player_clip brush across the whole area vmf.add_brush( vmf.make_prism( p1=box_min - (64, 64, FLOOR_DEPTH), p2=box_max + (64, 64, 0), mat=MATS['clip'][0], ).solid) # Add a noportal_volume covering the surface, in case there's # room for a portal. noportal_solid = vmf.make_prism( # Don't go all the way to the sides, so it doesn't affect wall # brushes. p1=box_min - (63, 63, 9), p2=box_max + (63, 63, 0), mat='tools/toolsinvisible', ).solid noportal_ent = vmf.create_ent( classname='func_noportal_volume', origin=box_min.join(' '), ) noportal_ent.solids.append(noportal_solid) if SETTINGS['base_is_disp']: # Use displacements for the base instead. make_alpha_base( vmf, box_min + (-64, -64, 0), box_max + (64, 64, 0), noise=alpha_noise, ) for x, y in utils.iter_grid( min_x=int(box_min.x), max_x=int(box_max.x) + 1, min_y=int(box_min.y), max_y=int(box_max.y) + 1, stride=128, ): # Build the set of all positions.. floor_neighbours[z][x, y] = -1 # Mark borders we need to fill in, and the angle (for func_instance) # The wall is the face pointing inwards towards the bottom brush, # and the ceil is the ceiling of the block above the bordering grid # points. for x in range(int(box_min.x), int(box_max.x) + 1, 128): # North floor_edges.append( BorderPoints( wall=Vec(x, box_max.y + 64, z - 64), ceil=Vec_tuple(x, box_max.y + 128, z), rot=270, )) # South floor_edges.append( BorderPoints( wall=Vec(x, box_min.y - 64, z - 64), ceil=Vec_tuple(x, box_min.y - 128, z), rot=90, )) for y in range(int(box_min.y), int(box_max.y) + 1, 128): # East floor_edges.append( BorderPoints( wall=Vec(box_max.x + 64, y, z - 64), ceil=Vec_tuple(box_max.x + 128, y, z), rot=180, )) # West floor_edges.append( BorderPoints( wall=Vec(box_min.x - 64, y, z - 64), ceil=Vec_tuple(box_min.x - 128, y, z), rot=0, )) # Now count boundries near tiles, then generate them. # Do it seperately for each z-level: for z, xy_dict in floor_neighbours.items(): # type: float, dict for x, y in xy_dict: # type: float, float # We want to count where there aren't any tiles xy_dict[x, y] = (((x - 128, y - 128) not in xy_dict) + ((x - 128, y + 128) not in xy_dict) + ((x + 128, y - 128) not in xy_dict) + ((x + 128, y + 128) not in xy_dict) + ((x - 128, y) not in xy_dict) + ((x + 128, y) not in xy_dict) + ((x, y - 128) not in xy_dict) + ((x, y + 128) not in xy_dict)) max_x = max_y = 0 weights = {} # Now the counts are all correct, compute the weight to apply # for tiles. # Adding the neighbouring counts will make a 5x5 area needed to set # the center to 0. for (x, y), cur_count in xy_dict.items(): max_x = max(x, max_x) max_y = max(y, max_y) # Orthrogonal is worth 0.2, diagonal is worth 0.1. # Not-present tiles would be 8 - the maximum tile_count = (0.8 * cur_count + 0.1 * xy_dict.get( (x - 128, y - 128), 8) + 0.1 * xy_dict.get( (x - 128, y + 128), 8) + 0.1 * xy_dict.get( (x + 128, y - 128), 8) + 0.1 * xy_dict.get( (x + 128, y + 128), 8) + 0.2 * xy_dict.get( (x - 128, y), 8) + 0.2 * xy_dict.get( (x, y - 128), 8) + 0.2 * xy_dict.get( (x, y + 128), 8) + 0.2 * xy_dict.get( (x + 128, y), 8)) # The number ranges from 0 (all tiles) to 12.8 (no tiles). # All tiles should still have a small chance to generate tiles. weights[x, y] = min((tile_count + 0.5) / 8, 1) # Share the detail entity among same-height tiles.. detail_ent = vmf.create_ent(classname='func_detail', ) for x, y in xy_dict: convert_floor( vmf, Vec(x, y, z), overlay_ids, MATS, SETTINGS, sign_loc, detail_ent, noise_weight=weights[x, y], noise_func=noise, ) add_floor_sides(vmf, floor_edges) conditions.reallocate_overlays(overlay_ids) return conditions.RES_EXHAUSTED