def make_bottomless_pit(vmf: VMF, max_height): """Generate bottomless pits.""" import vbsp tele_ref = SETTINGS['tele_ref'] tele_dest = SETTINGS['tele_dest'] use_skybox = bool(SETTINGS['skybox']) if use_skybox: tele_off = Vec( x=SETTINGS['off_x'], y=SETTINGS['off_y'], ) else: tele_off = Vec(0, 0, 0) # Controlled by the style, not skybox! blend_light = options.get(str, 'pit_blend_light') if use_skybox: # Add in the actual skybox edges and triggers. vmf.create_ent( classname='func_instance', file=SETTINGS['skybox'], targetname='skybox', angles='0 0 0', origin=tele_off, ) fog_opt = vbsp.settings['fog'] # Now generate the sky_camera, with appropriate values. sky_camera = vmf.create_ent( classname='sky_camera', scale='1.0', origin=tele_off, angles=fog_opt['direction'], fogdir=fog_opt['direction'], fogcolor=fog_opt['primary'], fogstart=fog_opt['start'], fogend=fog_opt['end'], fogenable='1', heightFogStart=fog_opt['height_start'], heightFogDensity=fog_opt['height_density'], heightFogMaxDensity=fog_opt['height_max_density'], ) if fog_opt['secondary']: # Only enable fog blending if a secondary color is enabled sky_camera['fogblend'] = '1' sky_camera['fogcolor2'] = fog_opt['secondary'] sky_camera['use_angles'] = '1' else: sky_camera['fogblend'] = '0' sky_camera['use_angles'] = '0' if SETTINGS['skybox_ceil'] != '': # We dynamically add the ceiling so it resizes to match the map, # and lighting won't be too far away. vmf.create_ent( classname='func_instance', file=SETTINGS['skybox_ceil'], targetname='skybox', angles='0 0 0', origin=tele_off + (0, 0, max_height), ) if SETTINGS['targ'] != '': # Add in the teleport reference target. vmf.create_ent( classname='func_instance', file=SETTINGS['targ'], targetname='skybox', angles='0 0 0', origin='0 0 0', ) # First, remove all of Valve's triggers inside pits. for trig in vmf.by_class['trigger_multiple'] | vmf.by_class['trigger_hurt']: if brushLoc.POS['world':Vec.from_str(trig['origin'])].is_pit: trig.remove() # Potential locations of bordering brushes.. wall_pos = set() side_dirs = [ (0, -128, 0), # N (0, +128, 0), # S (-128, 0, 0), # E (+128, 0, 0) # W ] # Only use 1 entity for the teleport triggers. If multiple are used, # cubes can contact two at once and get teleported odd places. tele_trig = None hurt_trig = None for grid_pos, block_type in brushLoc.POS.items( ): # type: Vec, brushLoc.Block pos = brushLoc.grid_to_world(grid_pos) if not block_type.is_pit: continue # Physics objects teleport when they hit the bottom of a pit. if block_type.is_bottom and use_skybox: if tele_trig is None: tele_trig = vmf.create_ent( classname='trigger_teleport', spawnflags='4106', # Physics and npcs landmark=tele_ref, target=tele_dest, origin=pos, ) tele_trig.solids.append( vmf.make_prism( pos + (-64, -64, -64), pos + (64, 64, -8), mat='tools/toolstrigger', ).solid, ) # Players, however get hurt as soon as they enter - that way it's # harder to see that they don't teleport. if block_type.is_top: if hurt_trig is None: hurt_trig = vmf.create_ent( classname='trigger_hurt', damagetype=32, # FALL spawnflags=1, # CLients damage=100000, nodmgforce=1, # No physics force when hurt.. damagemodel=0, # Always apply full damage. origin=pos, # We know this is not in the void.. ) hurt_trig.solids.append( vmf.make_prism( Vec(pos.x - 64, pos.y - 64, -128), pos + (64, 64, 48 if use_skybox else 16), mat='tools/toolstrigger', ).solid, ) if not block_type.is_bottom: continue # Everything else is only added to the bottom-most position. if use_skybox and blend_light: # Generate dim lights at the skybox location, # to blend the lighting together. light_pos = pos + (0, 0, -60) vmf.create_ent( classname='light', origin=light_pos, _light=blend_light, _fifty_percent_distance='256', _zero_percent_distance='512', ) vmf.create_ent( classname='light', origin=light_pos + tele_off, _light=blend_light, _fifty_percent_distance='256', _zero_percent_distance='512', ) wall_pos.update([(pos + off).as_tuple() for off in side_dirs]) if hurt_trig is not None: hurt_trig.outputs.append(Output( 'OnHurtPlayer', '@goo_fade', 'Fade', ), ) if not use_skybox: make_pit_shell(vmf) return # Now determine the position of side instances. # We use the utils.CONN_TYPES dict to determine instance positions # based on where nearby walls are. side_types = { utils.CONN_TYPES.side: PIT_INST['side'], # o| utils.CONN_TYPES.corner: PIT_INST['corner'], # _| utils.CONN_TYPES.straight: PIT_INST['side'], # Add this twice for |o| utils.CONN_TYPES.triple: PIT_INST['triple'], # U-shape utils.CONN_TYPES.all: PIT_INST['pillar'], # [o] } LOGGER.info('Pit instances: {}', side_types) for pos in wall_pos: pos = Vec(pos) if not brushLoc.POS['world':pos].is_solid: # Not actually a wall here! continue # CONN_TYPES has n,s,e,w as keys - whether there's something in that direction. nsew = tuple(brushLoc.POS['world':pos + off].is_pit for off in side_dirs) LOGGER.info('Pos: {}, NSEW: {}, lookup: {}', pos, nsew, utils.CONN_LOOKUP[nsew]) inst_type, angle = utils.CONN_LOOKUP[nsew] if inst_type is utils.CONN_TYPES.none: # Middle of the pit... continue random.seed('pit_' + str(pos.x) + str(pos.y) + 'sides') file = random.choice(side_types[inst_type]) if file != '': vmf.create_ent( classname='func_instance', file=file, targetname='goo_side', origin=tele_off + pos, angles=angle, ).make_unique() # Straight uses two side-instances in parallel - "|o|" if inst_type is utils.CONN_TYPES.straight: file = random.choice(side_types[inst_type]) if file != '': vmf.create_ent( classname='func_instance', file=file, targetname='goo_side', origin=tele_off + pos, # Reverse direction angles=Vec.from_str(angle) + (0, 180, 0), ).make_unique()
def res_goo_debris(vmf: VMF, res: Property) -> object: """Add random instances to goo squares. Options: - file: The filename for the instance. The variant files should be suffixed with `_1.vmf`, `_2.vmf`, etc. - space: the number of border squares which must be filled with goo for a square to be eligible - defaults to 1. - weight, number: see the `Variant` result, a set of weights for the options - chance: The percentage chance a square will have a debris item - offset: A random xy offset applied to the instances. """ from precomp import brushLoc space = res.int('spacing', 1) rand_count = res.int('number', None) rand_list: list[int] | None if rand_count: rand_list = rand.parse_weights( rand_count, res['weights', ''], ) else: rand_list = None chance = res.int('chance', 30) / 100 file = res['file'] offset = res.int('offset', 0) if file.endswith('.vmf'): file = file[:-4] goo_top_locs = { pos.as_tuple() for pos, block in brushLoc.POS.items() if block.is_goo and block.is_top } if space == 0: # No spacing needed, just copy possible_locs = [Vec(loc) for loc in goo_top_locs] else: possible_locs = [] for x, y, z in goo_top_locs: # Check to ensure the neighbouring blocks are also # goo brushes (depending on spacing). for x_off, y_off in utils.iter_grid( min_x=-space, max_x=space + 1, min_y=-space, max_y=space + 1, stride=1, ): if x_off == y_off == 0: continue # We already know this is a goo location if (x + x_off, y + y_off, z) not in goo_top_locs: break # This doesn't qualify else: possible_locs.append(brushLoc.grid_to_world(Vec(x, y, z))) LOGGER.info( 'GooDebris: {}/{} locations', len(possible_locs), len(goo_top_locs), ) for loc in possible_locs: rng = rand.seed(b'goo_debris', loc) if rng.random() > chance: continue if rand_list is not None: rand_fname = f'{file}_{rng.choice(rand_list) + 1}.vmf' else: rand_fname = file + '.vmf' if offset > 0: loc.x += rng.randint(-offset, offset) loc.y += rng.randint(-offset, offset) loc.z -= 32 # Position the instances in the center of the 128 grid. vmf.create_ent( classname='func_instance', file=rand_fname, origin=loc.join(' '), angles=f'0 {rng.randrange(0, 3600) / 10} 0' ) return RES_EXHAUSTED
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=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=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 ]