def res_temp_reset_gridded(inst: Entity): """Temporary result - reset gridded state on a surface. Used for antline routers to undo ItemLightStrip's 4x4 texturing. This should be removed after geometry is done. """ pos = Vec(0, 0, -64) pos.localise( Vec.from_str(inst['origin']), Vec.from_str(inst['angles']) ) norm = Vec(z=-1).rotate_by_str(inst['angles']) for axis in 'xyz': # Don't realign things in the normal's axis - # those are already fine. if not norm[axis]: pos[axis] //= 128 pos[axis] *= 128 pos[axis] += 64 brush = SOLIDS.get(pos.as_tuple(), None) if brush is None: return if brush.color is template_brush.MAT_TYPES.white: brush.face.mat = const.WhitePan.WHITE_1x1 else: brush.face.mat = const.BlackPan.BLACK_1
def res_monitor(inst: Entity, res: Property) -> None: """Result for the monitor component. Options: - bullseye_name: If possible to break this, this is the name to give the npc_bullseye. - bullseye_loc: This is the position to place the bullseye at. - bullseye_parent: This is the parent to give the bullseye. The fixup variable $is_breakable is set to True if lasers or turrets are present to indicate the func_breakable should be added. """ global HAS_MONITOR import vbsp ( bullseye_name, bullseye_loc, bullseye_parent, ) = res.value HAS_MONITOR = True has_laser = vbsp.settings['has_attr']['laser'] # Allow turrets if the monitor is setup to allow it, and the actor should # be shot. needs_turret = bullseye_name and options.get(bool, 'voice_studio_should_shoot') inst.fixup['$is_breakable'] = has_laser or needs_turret # We need to generate an ai_relationship, which makes turrets hate # a bullseye. if needs_turret: loc = Vec(bullseye_loc) loc.localise( Vec.from_str(inst['origin']), Angle.from_str(inst['angles']), ) bullseye_name = conditions.local_name(inst, bullseye_name) inst.map.create_ent( classname='npc_bullseye', targetname=bullseye_name, parentname=conditions.local_name(inst, bullseye_parent), spawnflags=221186, # Non-solid, invisible, etc.. origin=loc, ) relation = inst.map.create_ent( classname='ai_relationship', targetname='@monitor_turr_hate', parentname=bullseye_name, # When killed, destroy this too. spawnflags=2, # Notify turrets about monitor locations disposition=1, # Hate origin=loc, subject='npc_portal_turret_floor', target=bullseye_name, ) MONITOR_RELATIONSHIP_ENTS.append(relation)
def shift_ent(inst: Entity) -> None: """Randomly shift the instance.""" rng = rand.seed(b'rand_shift', inst, seed) pos = Vec( rng.uniform(min_x, max_x), rng.uniform(min_y, max_y), rng.uniform(min_z, max_z), ) pos.localise(Vec.from_str(inst['origin']), Angle.from_str(inst['angles'])) inst['origin'] = pos
def res_make_funnel_light(inst: Entity): """Place a light for Funnel items.""" oran_on = inst.fixup.bool('$start_reversed') need_blue = need_oran = False name = '' if inst.fixup['$connectioncount_polarity'] != '0': import vbsp if not vbsp.settings['style_vars']['funnelallowswitchedlights']: # Allow disabling adding switchable lights. return name = conditions.local_name(inst, 'light') need_blue = need_oran = True else: if oran_on: need_oran = True else: need_blue = True loc = Vec(0, 0, -56) loc.localise(Vec.from_str(inst['origin']), Vec.from_str(inst['angles'])) if need_blue: inst.map.create_ent( classname='light', targetname=name + '_b' if name else '', spawnflags=int(oran_on), # 1 = Initially Dark origin=loc, _light='50 120 250 50', _lightHDR='-1 -1 -1 1', _lightscaleHDR=2, _fifty_percent_distance=48, _zero_percent_distance=96, _hardfalloff=1, _distance=0, style=0, ) if need_oran: inst.map.create_ent( classname='light', targetname=name + '_o' if name else '', spawnflags=int(not oran_on), origin=loc, _light='250 120 50 50', _lightHDR='-1 -1 -1 1', _lightscaleHDR=2, _fifty_percent_distance=48, _zero_percent_distance=96, _hardfalloff=1, _distance=0, style=0, )
def res_make_funnel_light(inst: Entity) -> None: """Place a light for Funnel items.""" oran_on = inst.fixup.bool('$start_reversed') need_blue = need_oran = False name = '' if inst.fixup['$connectioncount_polarity'] != '0': import vbsp if not vbsp.settings['style_vars']['funnelallowswitchedlights']: # Allow disabling adding switchable lights. return name = conditions.local_name(inst, 'light') need_blue = need_oran = True else: if oran_on: need_oran = True else: need_blue = True loc = Vec(0, 0, -56) loc.localise(Vec.from_str(inst['origin']), Angle.from_str(inst['angles'])) if need_blue: inst.map.create_ent( classname='light', targetname=name + '_b' if name else '', spawnflags=int(oran_on), # 1 = Initially Dark origin=loc, _light='50 120 250 50', _lightHDR='-1 -1 -1 1', _lightscaleHDR=2, _fifty_percent_distance=48, _zero_percent_distance=96, _hardfalloff=1, _distance=0, style=0, ) if need_oran: inst.map.create_ent( classname='light', targetname=name + '_o' if name else '', spawnflags=int(not oran_on), origin=loc, _light='250 120 50 50', _lightHDR='-1 -1 -1 1', _lightscaleHDR=2, _fifty_percent_distance=48, _zero_percent_distance=96, _hardfalloff=1, _distance=0, style=0, )
def res_monitor(inst: Entity, res: Property): """Result for the monitor component. """ global NEEDS_TURRET import vbsp ( break_inst, bullseye_name, bullseye_loc, bullseye_parent, ) = res.value ALL_MONITORS.append(Monitor(inst)) has_laser = vbsp.settings['style_vars'].get('haslaser', False) # Allow turrets if the monitor is setup to allow it, and the actor should # be shot. needs_turret = bullseye_name and vbsp_options.get( bool, 'voice_studio_should_shoot') inst.fixup['$is_breakable'] = has_laser or needs_turret # We need to generate an ai_relationship, which makes turrets hate # a bullseye. if needs_turret: loc = Vec(bullseye_loc) loc.localise( Vec.from_str(inst['origin']), Vec.from_str(inst['angles']), ) bullseye_name = local_name(inst, bullseye_name) inst.map.create_ent( classname='npc_bullseye', targetname=bullseye_name, parentname=local_name(inst, bullseye_parent), spawnflags=221186, # Non-solid, invisible, etc.. origin=loc, ) inst.map.create_ent( classname='ai_relationship', targetname='@monitor_turr_hate', spawnflags=2, # Notify turrets about monitor locations disposition=1, # Hate origin=loc, subject='npc_portal_turret_floor', target=bullseye_name, ) NEEDS_TURRET = True
def res_monitor(inst: Entity, res: Property) -> None: """Result for the monitor component. """ import vbsp ( break_inst, bullseye_name, bullseye_loc, bullseye_parent, ) = res.value ALL_MONITORS.append(Monitor(inst)) has_laser = vbsp.settings['has_attr']['laser'] # Allow turrets if the monitor is setup to allow it, and the actor should # be shot. needs_turret = bullseye_name and vbsp_options.get(bool, 'voice_studio_should_shoot') inst.fixup['$is_breakable'] = has_laser or needs_turret # We need to generate an ai_relationship, which makes turrets hate # a bullseye. if needs_turret: loc = Vec(bullseye_loc) loc.localise( Vec.from_str(inst['origin']), Vec.from_str(inst['angles']), ) bullseye_name = local_name(inst, bullseye_name) inst.map.create_ent( classname='npc_bullseye', targetname=bullseye_name, parentname=local_name(inst, bullseye_parent), spawnflags=221186, # Non-solid, invisible, etc.. origin=loc, ) relation = inst.map.create_ent( classname='ai_relationship', targetname='@monitor_turr_hate', spawnflags=2, # Notify turrets about monitor locations disposition=1, # Hate origin=loc, subject='npc_portal_turret_floor', target=bullseye_name, ) MONITOR_RELATIONSHIP_ENTS.append(relation)
def mon_remove_bullseyes(inst: Entity) -> Optional[object]: """Remove bullsyes used for cameras.""" if not BULLSYE_LOCS: return RES_EXHAUSTED if inst['file'].casefold() not in instanceLocs.resolve('<ITEM_CATAPULT_TARGET>'): return origin = Vec(0, 0, -64) origin.localise(Vec.from_str(inst['origin']), Vec.from_str(inst['angles'])) origin = origin.as_tuple() LOGGER.info('Pos: {} -> ', origin, BULLSYE_LOCS[origin]) if BULLSYE_LOCS[origin]: BULLSYE_LOCS[origin] -= 1 inst.remove()
def flag_blockpos_type(inst: Entity, flag: Property): """Determine the type of a grid position. If the value is single value, that should be the type. Otherwise, the value should be a block with 'offset' and 'type' values. The offset is in block increments, with 0 0 0 equal to the mounting surface. The type should be a space-seperated list of locations: * VOID (Outside the map) * SOLID (Full wall cube) * EMBED (Hollow wall cube) * AIR (Inside the map, may be occupied by items) * OCCUPIED (Known to be occupied by items) * PIT (Bottomless pits, any) * PIT_SINGLE (one-high) * PIT_TOP * PIT_MID * PIT_BOTTOM * GOO * GOO_SINGLE (one-deep goo) * GOO_TOP (goo surface) * GOO_MID * GOO_BOTTOM (floor) """ if flag.has_children(): pos = flag.vec('offset') * 128 types = flag['type'].split() else: types = flag.value.split() pos = Vec() pos.z -= 128 pos.localise( Vec.from_str(inst['origin']), Vec.from_str(inst['angles']), ) block = brushLoc.POS['world':pos] for block_type in types: try: allowed = brushLoc.BLOCK_LOOKUP[block_type.casefold()] except KeyError: raise ValueError( '"{}" is not a valid block type!'.format(block_type)) if block in allowed: return True return False
def resolve_offset(inst, value: str, scale: float=1, zoff: float=0) -> Vec: """Retrieve an offset from an instance var. This allows several special values: * $var to read from a variable * <piston_start> or <piston> to get the unpowered position of a piston plat * <piston_end> to get the powered position of a piston plat * <piston_top> to get the extended position of a piston plat * <piston_bottom> to get the retracted position of a piston plat If scale is set, read values are multiplied by this, and zoff is added to Z. """ value = value.casefold() # Offset the overlay by the given distance # Some special placeholder values: if value == '<piston_start>' or value == '<piston>': if inst.fixup.bool(const.FixupVars.PIST_IS_UP): value = '<piston_top>' else: value = '<piston_bottom>' elif value == '<piston_end>': if inst.fixup.bool(const.FixupVars.PIST_IS_UP): value = '<piston_bottom>' else: value = '<piston_top>' if value == '<piston_bottom>': offset = Vec( z=inst.fixup.int(const.FixupVars.PIST_BTM) * 128, ) elif value == '<piston_top>': offset = Vec( z=inst.fixup.int(const.FixupVars.PIST_TOP) * 128, ) else: # Regular vector offset = Vec.from_str(resolve_value(inst, value)) * scale offset.z += zoff offset.localise( Vec.from_str(inst['origin']), Vec.from_str(inst['angles']), ) return offset
def resolve_offset(inst, value: str, scale: float=1, zoff: float=0) -> Vec: """Retrieve an offset from an instance var. This allows several special values: * Any $replace variables * <piston_start> or <piston> to get the unpowered position of a piston plat * <piston_end> to get the powered position of a piston plat * <piston_top> to get the extended position of a piston plat * <piston_bottom> to get the retracted position of a piston plat If scale is set, read values are multiplied by this, and zoff is added to Z. """ value = inst.fixup.substitute(value).casefold() # Offset the overlay by the given distance # Some special placeholder values: if value == '<piston_start>' or value == '<piston>': if inst.fixup.bool(consts.FixupVars.PIST_IS_UP): value = '<piston_top>' else: value = '<piston_bottom>' elif value == '<piston_end>': if inst.fixup.bool(consts.FixupVars.PIST_IS_UP): value = '<piston_bottom>' else: value = '<piston_top>' if value == '<piston_bottom>': offset = Vec( z=inst.fixup.int(consts.FixupVars.PIST_BTM) * 128, ) elif value == '<piston_top>': offset = Vec( z=inst.fixup.int(consts.FixupVars.PIST_TOP) * 128, ) else: # Regular vector offset = Vec.from_str(value) * scale offset.z += zoff offset.localise( Vec.from_str(inst['origin']), Angle.from_str(inst['angles']), ) return offset
def res_signage(vmf: VMF, inst: Entity, res: Property): """Implement the Signage item.""" sign: Optional[Sign] try: sign = (CONN_SIGNAGES if res.bool('connection') else SIGNAGES)[inst.fixup[const.FixupVars.TIM_DELAY]] except KeyError: # Blank sign sign = None has_arrow = inst.fixup.bool(const.FixupVars.ST_ENABLED) sign_prim: Optional[Sign] sign_sec: Optional[Sign] if has_arrow: sign_prim = sign sign_sec = SIGNAGES['arrow'] elif sign is not None: sign_prim = sign.primary or sign sign_sec = sign.secondary or None else: # Neither sign or arrow, delete this. inst.remove() return origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles']) normal = Vec(z=-1).rotate(*angles) forward = Vec(x=-1).rotate(*angles) prim_pos = Vec(0, -16, -64) sec_pos = Vec(0, 16, -64) prim_pos.localise(origin, angles) sec_pos.localise(origin, angles) template_id = res['template_id', ''] face: Side if inst.fixup.bool(const.FixupVars.ST_REVERSED): # Flip around. forward = -forward prim_visgroup = 'secondary' sec_visgroup = 'primary' prim_pos, sec_pos = sec_pos, prim_pos else: prim_visgroup = 'primary' sec_visgroup = 'secondary' if template_id: brush_faces: List[Side] = [] if sign_prim and sign_sec: visgroup = [prim_visgroup, sec_visgroup] elif sign_prim: visgroup = [prim_visgroup] else: visgroup = [sec_visgroup] template = template_brush.import_template( template_id, origin, angles, force_type=template_brush.TEMP_TYPES.detail, additional_visgroups=visgroup, ) for face in template.detail.sides(): if face.normal() == normal: brush_faces.append(face) else: # Direct on the surface. block_center = origin // 128 * 128 + (64, 64, 64) try: face = conditions.SOLIDS[(block_center + 64 * normal).as_tuple()].face except KeyError: LOGGER.warning( "Can't place signage at ({}) in ({}) direction!", block_center, normal, ) return brush_faces = [face] if sign_prim is not None: place_sign( vmf, brush_faces, sign_prim, prim_pos, normal, forward, rotate=True, ) if sign_sec is not None: if has_arrow and res.bool('arrowDown'): # Arrow texture points down, need to flip it. forward = -forward place_sign( vmf, brush_faces, sign_sec, sec_pos, normal, forward, rotate=not has_arrow, )
def res_water_splash(vmf: VMF, inst: Entity, res: Property) -> None: """Creates splashes when something goes in and out of water. Arguments: - `parent`: The name of the parent entity. - `name`: The name given to the env_splash. - `scale`: The size of the effect (8 by default). - `position`: The offset position to place the entity. - `position2`: The offset to which the entity will move. - `type`: Use certain fixup values to calculate pos2 instead: `piston_1`/`2`/`3`/`4`: Use `$bottom_level` and `$top_level` as offsets. `track_platform`: Use `$travel_direction`, `$travel_distance`, etc. - `fast_check`: Check faster for movement. Needed for items which move quickly. """ ( name, parent, scale, pos1, pos2, calc_type, fast_check, ) = res.value # type: str, str, float, Vec, Vec, str, str pos1 = pos1.copy() splash_pos = pos1.copy() if calc_type == 'track_platform': lin_off = srctools.conv_int(inst.fixup['$travel_distance']) travel_ang = Angle.from_str(inst.fixup['$travel_direction']) start_pos = srctools.conv_float(inst.fixup['$starting_position']) if start_pos: start_pos = round(start_pos * lin_off) pos1 += Vec(x=-start_pos) @ travel_ang pos2 = Vec(x=lin_off) @ travel_ang + pos1 elif calc_type.startswith('piston'): # Use piston-platform offsetting. # The number is the highest offset to move to. max_pist = srctools.conv_int(calc_type.split('_', 2)[1], 4) bottom_pos = srctools.conv_int(inst.fixup['$bottom_level']) top_pos = min(srctools.conv_int(inst.fixup['$top_level']), max_pist) pos2 = pos1.copy() pos1 += Vec(z=128 * bottom_pos) pos2 += Vec(z=128 * top_pos) LOGGER.info('Bottom: {}, top: {}', bottom_pos, top_pos) else: # Directly from the given value. pos2 = Vec.from_str(conditions.resolve_value(inst, pos2)) origin = Vec.from_str(inst['origin']) angles = Angle.from_str(inst['angles']) splash_pos.localise(origin, angles) pos1.localise(origin, angles) pos2.localise(origin, angles) # Since it's a straight line and you can't go through walls, # if pos1 and pos2 aren't in goo we aren't ever in goo. check_pos = [pos1, pos2] if pos1.z < origin.z: # If embedding in the floor, the positions can both be below the # actual surface. In that case check the origin too. check_pos.append(Vec(pos1.x, pos1.y, origin.z)) if pos1.z == pos2.z: # Flat - this won't do anything... return for pos in check_pos: grid_pos = pos // 128 * 128 grid_pos += (64, 64, 64) block = BLOCK_POS['world':pos] if block.is_goo: break else: return # Not in goo at all water_pos = grid_pos + (0, 0, 32) # Check if both positions are above or below the water.. # that means it won't ever trigger. if max(pos1.z, pos2.z) < water_pos.z - 8: return if min(pos1.z, pos2.z) > water_pos.z + 8: return # Pass along the water_pos encoded into the targetname. # Restrict the number of characters to allow direct slicing # in the script. enc_data = '_{:09.3f}{}'.format( water_pos.z + 12, 'f' if fast_check else 's', ) vmf.create_ent( classname='env_splash', targetname=conditions.local_name(inst, name) + enc_data, parentname=conditions.local_name(inst, parent), origin=splash_pos + (0, 0, 16), scale=scale, vscripts='BEE2/water_splash.nut', thinkfunction='Think', spawnflags='1', # Trace downward to water surface. )
def res_cutout_tile(vmf: srctools.VMF, res: Property): """Generate random quarter tiles, like in Destroyed or Retro maps. - `MarkerItem` is the instance file to look for (`<ITEM_BEE2_CUTOUT_TILE>`) - `floor_chance`: The percentage change for a segment in the middle of the floor to be a normal tile. - `floor_glue_chance`: The chance for any tile to be glue - this should be higher than the regular chance, as that overrides this. - `rotateMax` is the maximum angle to rotate squarebeam models. - `squarebeamsSkin` sets the skin to use for the squarebeams floor frame. - `dispBase`, if true makes the floor a displacement with random alpha. - `Materials` blocks specify the possible materials to use: - `squarebeams` is the squarebeams variant to use. - `ceilingwalls` are the sides of the ceiling section. - `floorbase` is the texture under floor sections. If `dispBase` is True this is a displacement material. - `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.) """ marker_filenames = instanceLocs.resolve(res['markeritem']) # TODO: Reimplement cutout tiles. for inst in vmf.by_class['func_instance']: if inst['file'].casefold() in marker_filenames: inst.remove() return x: float y: float max_x: float max_y: float 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_locs = set() # 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')): add_signage_loc(sign_locs, Vec.from_str(over['origin'])) for item in connections.ITEMS.values(): for ind_pan in item.ind_panels: loc = Vec(0, 0, -64) loc.localise( Vec.from_str(ind_pan['origin']), Vec.from_str(ind_pan['angles']), ) add_signage_loc(sign_locs, loc) 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. # all_floors[z][x,y] = count floor_neighbours = defaultdict( dict) # type: Dict[float, Dict[Tuple[float, float], int]] 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 marker_filenames: continue targ = inst['targetname'] normal = 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 normal == (0, 0, 1): io_list = FLOOR_IO else: io_list = CEIL_IO # Reuse orient to calculate where the solid face will be. loc = Vec.from_str(inst['origin']) - 64 * normal INST_LOCS[targ] = loc item = connections.ITEMS[targ] item.delete_antlines() if item.outputs: for conn in list(item.outputs): if conn.to_item.inst['file'].casefold() in marker_filenames: io_list.append((targ, conn.to_item.name)) else: LOGGER.warning('Cutout tile connected to non-cutout!') conn.remove() # Delete the connection. else: # 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)) # Remove all traces of this item (other than in connections lists). inst.remove() del connections.ITEMS[targ] for start_floor, end_floor in FLOOR_IO: 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 boundaries near tiles, then generate them. # Do it separately for each z-level: for z, xy_dict in floor_neighbours.items(): for x, y in xy_dict: # 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_locs, detail_ent, noise_weight=weights[x, y], noise_func=noise, ) add_floor_sides(vmf, floor_edges) return conditions.RES_EXHAUSTED
def res_piston_plat(vmf: VMF, inst: Entity, res: Property) -> None: """Generates piston platforms with optimized logic.""" template: template_brush.Template visgroup_names: List[str] inst_filenames: Dict[str, str] has_dn_fizz: bool automatic_var: str color_var: str source_ent: str snd_start: str snd_loop: str snd_stop: str ( template, visgroup_names, inst_filenames, has_dn_fizz, automatic_var, color_var, source_ent, snd_start, snd_loop, snd_stop, ) = res.value min_pos = inst.fixup.int(FixupVars.PIST_BTM) max_pos = inst.fixup.int(FixupVars.PIST_TOP) start_up = inst.fixup.bool(FixupVars.PIST_IS_UP) # Allow doing variable lookups here. visgroup_names = [ conditions.resolve_value(inst, name) for name in visgroup_names ] if len(ITEMS[inst['targetname']].inputs) == 0: # No inputs. Check for the 'auto' var if applicable. if automatic_var and inst.fixup.bool(automatic_var): pass # The item is automatically moving, so we generate the dynamics. else: # It's static, we just make that and exit. position = max_pos if start_up else min_pos inst.fixup[FixupVars.PIST_BTM] = position inst.fixup[FixupVars.PIST_TOP] = position static_inst = inst.copy() vmf.add_ent(static_inst) static_inst['file'] = inst_filenames['fullstatic_' + str(position)] return init_script = 'SPAWN_UP <- {}'.format('true' if start_up else 'false') if snd_start and snd_stop: packing.pack_files(vmf, snd_start, snd_stop, file_type='sound') init_script += '; START_SND <- `{}`; STOP_SND <- `{}`'.format( snd_start, snd_stop) elif snd_start: packing.pack_files(vmf, snd_start, file_type='sound') init_script += '; START_SND <- `{}`'.format(snd_start) elif snd_stop: packing.pack_files(vmf, snd_stop, file_type='sound') init_script += '; STOP_SND <- `{}`'.format(snd_stop) script_ent = vmf.create_ent( classname='info_target', targetname=local_name(inst, 'script'), vscripts='BEE2/piston/common.nut', vscript_init_code=init_script, origin=inst['origin'], ) if has_dn_fizz: script_ent['thinkfunction'] = 'FizzThink' if start_up: st_pos, end_pos = max_pos, min_pos else: st_pos, end_pos = min_pos, max_pos script_ent.add_out( Output('OnUser1', '!self', 'RunScriptCode', 'moveto({})'.format(st_pos)), Output('OnUser2', '!self', 'RunScriptCode', 'moveto({})'.format(end_pos)), ) origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles']) off = Vec(z=128).rotate(*angles) move_ang = off.to_angle() # Index -> func_movelinear. pistons = {} # type: Dict[int, Entity] static_ent = vmf.create_ent('func_brush', origin=origin) for pist_ind in [1, 2, 3, 4]: pist_ent = inst.copy() vmf.add_ent(pist_ent) if pist_ind <= min_pos: # It's below the lowest position, so it can be static. pist_ent['file'] = inst_filenames['static_' + str(pist_ind)] pist_ent['origin'] = brush_pos = origin + pist_ind * off temp_targ = static_ent else: # It's a moving component. pist_ent['file'] = inst_filenames['dynamic_' + str(pist_ind)] if pist_ind > max_pos: # It's 'after' the highest position, so it never extends. # So simplify by merging those all. # That's before this so it'll have to exist. temp_targ = pistons[max_pos] if start_up: pist_ent['origin'] = brush_pos = origin + max_pos * off else: pist_ent['origin'] = brush_pos = origin + min_pos * off pist_ent.fixup['$parent'] = 'pist' + str(max_pos) else: # It's actually a moving piston. if start_up: brush_pos = origin + pist_ind * off else: brush_pos = origin + min_pos * off pist_ent['origin'] = brush_pos pist_ent.fixup['$parent'] = 'pist' + str(pist_ind) pistons[pist_ind] = temp_targ = vmf.create_ent( 'func_movelinear', targetname=local_name(pist_ent, 'pist' + str(pist_ind)), origin=brush_pos - off, movedir=move_ang, startposition=start_up, movedistance=128, speed=150, ) if pist_ind - 1 in pistons: pistons[pist_ind]['parentname'] = local_name( pist_ent, 'pist' + str(pist_ind - 1), ) if not pist_ent['file']: # No actual instance, remove. pist_ent.remove() temp_result = template_brush.import_template( template, brush_pos, angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, additional_visgroups={visgroup_names[pist_ind - 1]}, ) temp_targ.solids.extend(temp_result.world) template_brush.retexture_template( temp_result, origin, pist_ent.fixup, generator=GenCat.PANEL, ) # Associate any set panel with the same entity, if it's present. tile_pos = Vec(z=-128) tile_pos.localise(origin, angles) panel: Optional[Panel] = None try: tiledef = TILES[tile_pos.as_tuple(), off.norm().as_tuple()] except KeyError: pass else: for panel in tiledef.panels: if panel.same_item(inst): break else: # Checked all of them. panel = None if panel is not None: if panel.brush_ent in vmf.entities and not panel.brush_ent.solids: panel.brush_ent.remove() panel.brush_ent = pistons[max(pistons.keys())] panel.offset = st_pos * off if not static_ent.solids and (panel is None or panel.brush_ent is not static_ent): static_ent.remove() if snd_loop: script_ent['classname'] = 'ambient_generic' script_ent['message'] = snd_loop script_ent['health'] = 10 # Volume script_ent['pitch'] = '100' script_ent['spawnflags'] = 16 # Start silent, looped. script_ent['radius'] = 1024 if source_ent: # Parent is irrelevant for actual entity locations, but it # survives for the script to read. script_ent['SourceEntityName'] = script_ent[ 'parentname'] = local_name(inst, source_ent)
def res_signage(vmf: VMF, inst: Entity, res: Property): """Implement the Signage item.""" sign: Optional[Sign] try: sign = ( CONN_SIGNAGES if res.bool('connection') else SIGNAGES )[inst.fixup[consts.FixupVars.TIM_DELAY]] except KeyError: # Blank sign sign = None has_arrow = inst.fixup.bool(consts.FixupVars.ST_ENABLED) sign_prim: Optional[Sign] sign_sec: Optional[Sign] if has_arrow: sign_prim = sign sign_sec = SIGNAGES['arrow'] elif sign is not None: sign_prim = sign.primary or sign sign_sec = sign.secondary or None else: # Neither sign or arrow, delete this. inst.remove() return origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles']) normal = Vec(z=-1).rotate(*angles) forward = Vec(x=-1).rotate(*angles) prim_pos = Vec(0, -16, -64) sec_pos = Vec(0, 16, -64) prim_pos.localise(origin, angles) sec_pos.localise(origin, angles) template_id = res['template_id', ''] if inst.fixup.bool(consts.FixupVars.ST_REVERSED): # Flip around. forward = -forward prim_visgroup = 'secondary' sec_visgroup = 'primary' prim_pos, sec_pos = sec_pos, prim_pos else: prim_visgroup = 'primary' sec_visgroup = 'secondary' if sign_prim and sign_sec: inst['file'] = res['large_clip', ''] inst['origin'] = (prim_pos + sec_pos) / 2 else: inst['file'] = res['small_clip', ''] inst['origin'] = prim_pos if sign_prim else sec_pos brush_faces: List[Side] = [] tiledef: Optional[tiling.TileDef] = None if template_id: if sign_prim and sign_sec: visgroup = [prim_visgroup, sec_visgroup] elif sign_prim: visgroup = [prim_visgroup] else: visgroup = [sec_visgroup] template = template_brush.import_template( vmf, template_id, origin, angles, force_type=template_brush.TEMP_TYPES.detail, additional_visgroups=visgroup, ) for face in template.detail.sides(): if face.normal() == normal: brush_faces.append(face) else: # Direct on the surface. # Find the grid pos first. grid_pos = (origin // 128) * 128 + 64 try: tiledef = tiling.TILES[(grid_pos + 128 * normal).as_tuple(), (-normal).as_tuple()] except KeyError: LOGGER.warning( "Can't place signage at ({}) in ({}) direction!", origin, normal, exc_info=True, ) return if sign_prim is not None: over = place_sign( vmf, brush_faces, sign_prim, prim_pos, normal, forward, rotate=True, ) if tiledef is not None: tiledef.bind_overlay(over) if sign_sec is not None: if has_arrow and res.bool('arrowDown'): # Arrow texture points down, need to flip it. forward = -forward over = place_sign( vmf, brush_faces, sign_sec, sec_pos, normal, forward, rotate=not has_arrow, ) if tiledef is not None: tiledef.bind_overlay(over)
def res_water_splash(vmf: VMF, inst: Entity, res: Property): """Creates splashes when something goes in and out of water. Arguments: - parent: The name of the parent entity. - name: The name given to the env_splash. - scale: The size of the effect (8 by default). - position: The offset position to place the entity. - position2: The offset to which the entity will move. - type: Use certain fixup values to calculate pos2 instead: 'piston_1/2/3/4': Use $bottom_level and $top_level as offsets. 'track_platform': Use $travel_direction, $travel_distance, etc. - fast_check: Check faster for movement. Needed for items which move quickly. """ ( name, parent, scale, pos1, pos2, calc_type, fast_check, ) = res.value # type: str, str, float, Vec, str, str pos1 = pos1.copy() # type: Vec splash_pos = pos1.copy() # type: Vec if calc_type == 'track_platform': lin_off = srctools.conv_int(inst.fixup['$travel_distance']) travel_ang = inst.fixup['$travel_direction'] start_pos = srctools.conv_float(inst.fixup['$starting_position']) if start_pos: start_pos = round(start_pos * lin_off) pos1 += Vec(x=-start_pos).rotate_by_str(travel_ang) pos2 = Vec(x=lin_off).rotate_by_str(travel_ang) pos2 += pos1 elif calc_type.startswith('piston'): # Use piston-platform offsetting. # The number is the highest offset to move to. max_pist = srctools.conv_int(calc_type.split('_', 2)[1], 4) bottom_pos = srctools.conv_int(inst.fixup['$bottom_level']) top_pos = min(srctools.conv_int(inst.fixup['$top_level']), max_pist) pos2 = pos1.copy() pos1 += Vec(z=128 * bottom_pos) pos2 += Vec(z=128 * top_pos) LOGGER.info('Bottom: {}, top: {}', bottom_pos, top_pos) else: # Directly from the given value. pos2 = Vec.from_str(conditions.resolve_value(inst, pos2)) origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles']) splash_pos.localise(origin, angles) pos1.localise(origin, angles) pos2.localise(origin, angles) # Since it's a straight line and you can't go through walls, # if pos1 and pos2 aren't in goo we aren't ever in goo. check_pos = [pos1, pos2] if pos1.z < origin.z: # If embedding in the floor, the positions can both be below the # actual surface. In that case check the origin too. check_pos.append(Vec(pos1.x, pos1.y, origin.z)) for pos in check_pos: grid_pos = pos // 128 * 128 # type: Vec grid_pos += (64, 64, 64) try: surf = conditions.GOO_LOCS[grid_pos.as_tuple()] except KeyError: continue break else: return # Not in goo at all if pos1.z == pos2.z: # Flat - this won't do anything... return water_pos = surf.get_origin() # Check if both positions are above or below the water.. # that means it won't ever trigger. LOGGER.info('pos1: {}, pos2: {}, water_pos: {}', pos1.z, pos2.z, water_pos.z) if max(pos1.z, pos2.z) < water_pos.z - 8: return if min(pos1.z, pos2.z) > water_pos.z + 8: return # Pass along the water_pos encoded into the targetname. # Restrict the number of characters to allow direct slicing # in the script. enc_data = '_{:09.3f}{}'.format( water_pos.z + 12, 'f' if fast_check else 's', ) vmf.create_ent( classname='env_splash', targetname=conditions.local_name(inst, name) + enc_data, parentname=conditions.local_name(inst, parent), origin=splash_pos + (0, 0, 16), scale=scale, vscripts='BEE2/water_splash.nut', thinkfunction='Think', spawnflags='1', # Trace downward to water surface. )
def read_from_map(self, vmf: VMF, has_attr: Dict[str, bool], items: Dict[str, editoritems.Item]) -> None: """Given the map file, set blocks.""" from precomp.instance_traits import get_item_id from precomp import bottomlessPit # Starting points to fill air and goo. # We want to fill goo first... air_search_locs: List[Tuple[Vec, bool]] = [] goo_search_locs: List[Tuple[Vec, bool]] = [] for ent in vmf.entities: str_pos = ent['origin', None] if str_pos is None: continue pos = world_to_grid(Vec.from_str(str_pos)) # Exclude entities outside the main area - elevators mainly. # The border should never be set to air! if (0, 0, 0) <= pos <= (25, 25, 25): air_search_locs.append((Vec(pos.x, pos.y, pos.z), False)) # We need to manually set EmbeddedVoxel locations. # These might not be detected for items where there's a block # which is entirely empty - corridors and obs rooms for example. item_id = get_item_id(ent) if item_id: try: item = items[item_id] except KeyError: continue angles = Vec.from_str(ent['angles']) for local_pos in item.embed_voxels: world_pos = Vec(local_pos) - (0, 0, 1) world_pos.localise(pos, angles) self[world_pos] = Block.EMBED can_have_pit = bottomlessPit.pits_allowed() for brush in vmf.brushes[:]: tex = {face.mat.casefold() for face in brush.sides} bbox_min, bbox_max = brush.get_bbox() if ( 'nature/toxicslime_a2_bridge_intro' in tex or 'nature/toxicslime_puzzlemaker_cheap' in tex ): # It's goo! x = bbox_min.x + 64 y = bbox_min.y + 64 g_x = x // 128 g_y = y // 128 is_pit = can_have_pit and bottomlessPit.is_pit(bbox_min, bbox_max) # If goo is multi-level, we want to record all pos! z_pos = range(int(bbox_min.z) + 64, int(bbox_max.z), 128) top_ind = len(z_pos) - 1 for ind, z in enumerate(z_pos): g_z = z // 128 self[g_x, g_y, g_z] = Block.from_pitgoo_attr( is_pit, is_top=(ind == top_ind), is_bottom=(ind == 0), ) # If goo has totally submerged tunnels, they are not filled. # Add each horizontal neighbour to the search list. # If not found they'll be ignored. goo_search_locs += [ (Vec(g_x - 1, g_y, g_z), True), (Vec(g_x + 1, g_y, g_z), True), (Vec(g_x, g_y + 1, g_z), True), (Vec(g_x, g_y - 1, g_z), True), ] # Remove the brush, since we're not going to use it. vmf.remove_brush(brush) # Indicate that this map contains goo/pits if is_pit: has_attr[VOICE_ATTR_PIT] = True else: has_attr[VOICE_ATTR_GOO] = True continue pos = world_to_grid(brush.get_origin(bbox_min, bbox_max)) if bbox_max - bbox_min == (128, 128, 128): # Full block.. self[pos] = Block.SOLID else: # Must be an embbedvoxel block self[pos] = Block.EMBED LOGGER.info( 'Analysed map, filling air... ({} starting positions..)', len(air_search_locs) ) self.fill_air(goo_search_locs + air_search_locs) LOGGER.info('Air filled!')
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. """ marker_filenames = 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_locs = set() # 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') ): add_signage_loc(sign_locs, Vec.from_str(over['origin'])) for item in connections.ITEMS.values(): for ind_pan in item.ind_panels: loc = Vec(0, 0, -64) loc.localise( Vec.from_str(ind_pan['origin']), Vec.from_str(ind_pan['angles']), ) add_signage_loc(sign_locs, loc) 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 marker_filenames: continue targ = inst['targetname'] normal = 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 normal == (0, 0, 1): io_list = FLOOR_IO else: io_list = CEIL_IO # Reuse orient to calculate where the solid face will be. loc = Vec.from_str(inst['origin']) - 64 * normal INST_LOCS[targ] = loc item = connections.ITEMS[targ] item.delete_antlines() if item.outputs: for conn in list(item.outputs): if conn.to_item.inst['file'].casefold() in marker_filenames: io_list.append((targ, conn.to_item.name)) else: LOGGER.warning('Cutout tile connected to non-cutout!') conn.remove() # Delete the connection. else: # 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)) # Remove all traces of this item (other than in connections lists). inst.remove() del connections.ITEMS[targ] for start_floor, end_floor in FLOOR_IO: 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 boundaries near tiles, then generate them. # Do it separately 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_locs, 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
def res_water_splash(inst: Entity, res: Property): """Creates splashes when something goes in and out of water. Arguments: - parent: The name of the parent entity. - name: The name given to the env_splash. - scale: The size of the effect (8 by default). - position: The offset position to place the entity. - position2: The offset to which the entity will move. - type: Use certain fixup values to calculate pos2 instead: 'piston_1/2/3/4': Use $bottom_level and $top_level as offsets. 'track_platform': Use $travel_direction, $travel_distance, etc. - fast_check: Check faster for movement. Needed for items which move quickly. """ ( name, parent, scale, pos1, pos2, calc_type, fast_check, ) = res.value # type: str, str, float, Vec, str, str pos1 = pos1.copy() # type: Vec splash_pos = pos1.copy() # type: Vec if calc_type == 'track_platform': lin_off = srctools.conv_int(inst.fixup['$travel_distance']) travel_ang = inst.fixup['$travel_direction'] start_pos = srctools.conv_float(inst.fixup['$starting_position']) if start_pos: start_pos = round(start_pos * lin_off) pos1 += Vec(x=-start_pos).rotate_by_str(travel_ang) pos2 = Vec(x=lin_off).rotate_by_str(travel_ang) pos2 += pos1 elif calc_type.startswith('piston'): # Use piston-platform offsetting. # The number is the highest offset to move to. max_pist = srctools.conv_int(calc_type.split('_', 2)[1], 4) bottom_pos = srctools.conv_int(inst.fixup['$bottom_level']) top_pos = min(srctools.conv_int(inst.fixup['$top_level']), max_pist) pos2 = pos1.copy() pos1 += Vec(z=128 * bottom_pos) pos2 += Vec(z=128 * top_pos) LOGGER.info('Bottom: {}, top: {}', bottom_pos, top_pos) else: # Directly from the given value. pos2 = Vec.from_str(conditions.resolve_value(inst, pos2)) origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles']) splash_pos.localise(origin, angles) pos1.localise(origin, angles) pos2.localise(origin, angles) conditions.VMF.create_ent( classname='env_beam', targetname=conditions.local_name(inst, name + '_pos'), origin=str(pos1), targetpoint=str(pos2), ) # Since it's a straight line and you can't go through walls, # if pos1 and pos2 aren't in goo we aren't ever in goo. check_pos = [pos1, pos2] if pos1.z < origin.z: # If embedding in the floor, the positions can both be below the # actual surface. In that case check the origin too. check_pos.append(Vec(pos1.x, pos1.y, origin.z)) for pos in check_pos: grid_pos = pos // 128 * 128 # type: Vec grid_pos += (64, 64, 64) try: surf = conditions.GOO_LOCS[grid_pos.as_tuple()] except KeyError: continue break else: return # Not in goo at all if pos1.z == pos2.z: # Flat - this won't do anything... return water_pos = surf.get_origin() # Check if both positions are above or below the water.. # that means it won't ever trigger. LOGGER.info('pos1: {}, pos2: {}, water_pos: {}', pos1.z, pos2.z, water_pos.z) if max(pos1.z, pos2.z) < water_pos.z - 8: return if min(pos1.z, pos2.z) > water_pos.z + 8: return import vbsp # Pass along the water_pos encoded into the targetname. # Restrict the number of characters to allow direct slicing # in the script. enc_data = '_{:09.3f}{}'.format( water_pos.z + 12, 'f' if fast_check else 's', ) conditions.VMF.create_ent( classname='env_splash', targetname=conditions.local_name(inst, name) + enc_data, parentname=conditions.local_name(inst, parent), origin=splash_pos + (0, 0, 16), scale=scale, vscripts='BEE2/water_splash.nut', thinkfunction='Think', spawnflags='1', # Trace downward to water surface. ) vbsp.PACK_FILES.add('scripts/vscripts/BEE2/water_splash.nut')