def randomise(inst: Entity) -> None: """Apply the random number.""" rng = rand.seed(b'rand_num', inst, seed) if is_float: inst.fixup[var] = rng.uniform(min_val, max_val) else: inst.fixup[var] = rng.randint(min_val, max_val)
def res_rand_vec(inst: Entity, res: Property) -> None: """A modification to RandomNum which generates a random vector instead. `decimal`, `seed` and `ResultVar` work like RandomNum. `min_x`, `max_y` etc are used to define the boundaries. If the min and max are equal that number will be always used instead. """ is_float = srctools.conv_bool(res['decimal']) var = res['resultvar', '$random'] rng = rand.seed(b'rand_vec', inst, res['seed', '']) if is_float: func = rng.uniform else: func = rng.randint value = Vec() for axis in 'xyz': max_val = srctools.conv_float(res['max_' + axis, 0.0]) min_val = srctools.conv_float(res['min_' + axis, 0.0]) if min_val == max_val: value[axis] = min_val else: value[axis] = func(min_val, max_val) inst.fixup[var] = value.join(' ')
def broken_iter( self, chance: float, ) -> Iterator[tuple[Vec, Vec, bool]]: """Iterator to compute positions for straight segments. This produces point pairs which fill the space from 0-dist. Neighbouring sections will be merged when they have the same type. """ rng = rand.seed(b'ant_broken', self.start, self.end, chance) offset = self.end - self.start dist = offset.mag() // 16 norm = 16 * offset.norm() if dist < 3 or chance == 0: # Short antlines always are either on/off. yield self.start, self.end, (rng.randrange(100) < chance) else: run_start = self.start last_type = rng.randrange(100) < chance for i in range(1, int(dist)): next_type = rng.randrange(100) < chance if next_type != last_type: yield run_start, self.start + i * norm, last_type last_type = next_type run_start = self.start + i * norm yield run_start, self.end, last_type
def _get(self, loc: Vec, tex_name: str): if type(tex_name) != str: try: tex_name = self.enum_data[id(tex_name)] except KeyError: raise ValueError( f'Unknown enum value {tex_name!r} ' f'for generator type {self.category}!') from None return rand.seed(b'tex_rand', loc).choice(self.textures[tex_name])
def _get(self, loc: Vec, tex_name: str) -> str: clump_seed = self._find_clump(loc) if clump_seed is None: # No clump found - return the gap texture. # But if the texture is GOO_SIDE, do that instead. # If we don't have a gap texture, just use any one. rng = rand.seed(b'tex_clump_side', loc) if tex_name == TileSize.GOO_SIDE or TileSize.CLUMP_GAP not in self: return rng.choice(self.textures[tex_name]) else: return rng.choice(self.textures[TileSize.CLUMP_GAP]) # Mix these three values together to determine the texture. # The clump seed makes each clump different, and adding the texture # name makes sure different surface types don't copy each other's # indexes. rng = rand.seed(b'tex_clump_side', self.gen_seed, tex_name, clump_seed) return rng.choice(self.textures[tex_name])
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 apply_switch(inst: Entity) -> None: """Execute a switch.""" if method is SWITCH_TYPE.RANDOM: cases = conf_cases.copy() rand.seed(b'switch', rand_seed, inst).shuffle(cases) else: # Won't change. cases = conf_cases run_default = True for flag, results in cases: # If not set, always succeed for the random situation. if flag.real_name and not check_flag(inst.map, flag, inst): continue for sub_res in results: Condition.test_result(inst, sub_res) run_default = False if method is not SWITCH_TYPE.ALL: # All does them all, otherwise we quit now. break if run_default: for sub_res in default: Condition.test_result(inst, sub_res)
def apply_random(inst: Entity) -> None: """Pick a random result and run it.""" rng = rand.seed(b'rand_res', inst, seed) if rng.randrange(100) > chance: return ind = rng.choice(weights_list) choice = results[ind] if choice.name == 'nop': pass elif choice.name == 'group': for sub_res in choice: if Condition.test_result(coll, inst, sub_res) is RES_EXHAUSTED: sub_res.name = 'nop' sub_res.value = '' else: if Condition.test_result(coll, inst, choice) is RES_EXHAUSTED: choice.name = 'nop' choice.value = ''
def add_group(inst: Entity) -> None: """Place the group.""" rng = rand.seed(b'shufflegroup', conf_seed, inst) pools = all_pools.copy() for (flags, value, potential_pools) in conf_selectors: for flag in flags: if not conditions.check_flag(vmf, flag, inst): break else: # Succeeded. allowed_inst = [(name, inst) for (name, inst) in pools if name in potential_pools] name, filename = rng.choice(allowed_inst) pools.remove((name, filename)) vmf.create_ent( 'func_instance', targetname=inst['targetname'], file=filename, angles=inst['angles'], origin=inst['origin'], fixup_style='0', ).fixup[conf_variable] = value
def insert_over(inst: Entity) -> None: """Apply the result.""" temp_id = inst.fixup.substitute(orig_temp_id) origin = Vec.from_str(inst['origin']) angles = Angle.from_str(inst['angles', '0 0 0']) face_pos = conditions.resolve_offset(inst, face_str) normal = orig_norm @ angles # Don't make offset change the face_pos value.. origin += offset @ angles for axis, norm in enumerate(normal): # Align to the center of the block grid. The normal direction is # already correct. if norm == 0: face_pos[axis] = face_pos[axis] // 128 * 128 + 64 # Shift so that the user perceives the position as the pos of the face # itself. face_pos -= 64 * normal try: tiledef = tiling.TILES[face_pos.as_tuple(), normal.as_tuple()] except KeyError: LOGGER.warning( 'Overlay brush position is not valid: {}', face_pos, ) return temp = template_brush.import_template( vmf, temp_id, origin, angles, targetname=inst['targetname', ''], force_type=TEMP_TYPES.detail, ) for over in temp.overlay: pos = Vec.from_str(over['basisorigin']) mat = over['material'] try: replace = replace_tex[mat.casefold().replace('\\', '/')] except KeyError: pass else: mat = rand.seed(b'temp_over', temp_id, pos).choice(replace) if mat[:1] == '$': mat = inst.fixup[mat] if mat.startswith('<') or mat.endswith('>'): # Lookup in the texture data. gen, mat = texturing.parse_name(mat[1:-1]) mat = gen.get(pos, mat) over['material'] = mat tiledef.bind_overlay(over) # Wipe the brushes from the map. if temp.detail is not None: temp.detail.remove() LOGGER.info( 'Overlay template "{}" could set keep_brushes=0.', temp_id, )
def res_set_tile(inst: Entity, res: Property) -> None: """Set 4x4 parts of a tile to the given values. `Offset` defines the position of the upper-left tile in the grid. Each `Tile` section defines a row of the positions to edit like so: "Tile" "bbbb" "Tile" "b..b" "Tile" "b..b" "Tile" "bbbb" If `Force` is true, the specified tiles will override any existing ones and create the tile if necessary. Otherwise they will be merged in - white/black tiles will not replace tiles set to nodraw or void for example. `chance`, if specified allows producing irregular tiles by randomly not changing the tile. If you need less regular placement (other orientation, precise positions) use a bee2_template_tilesetter in a template. Allowed tile characters: - `W`: White tile. - `w`: White 4x4 only tile. - `B`: Black tile. - `b`: Black 4x4 only tile. - `g`: The side/bottom of goo pits. - `n`: Nodraw surface. - `i`: Invert the tile surface, if black/white. - `1`: Convert to a 1x1 only tile, if a black/white tile. - `4`: Convert to a 4x4 only tile, if a black/white tile. - `.`: Void (remove the tile in this position). - `_` or ` `: Placeholder (don't modify this space). - `x`: Cutout Tile (Broken) - `o`: Cutout Tile (Partial) """ origin = Vec.from_str(inst['origin']) orient = Matrix.from_angle(Angle.from_str(inst['angles'])) offset = (res.vec('offset', -48, 48) - (0, 0, 64)) @ orient + origin norm = round(orient.up(), 6) force_tile = res.bool('force') tiles: list[str] = [ row.value for row in res if row.name in ('tile', 'tiles') ] if not tiles: raise ValueError('No "tile" parameters in SetTile!') chance = srctools.conv_float(res['chance', '100'].rstrip('%'), 100.0) if chance < 100.0: rng = rand.seed(b'tile', inst, res['seed', '']) else: rng = None for y, row in enumerate(tiles): for x, val in enumerate(row): if val in '_ ': continue if rng is not None and rng.uniform(0, 100) > chance: continue pos = Vec(32 * x, -32 * y, 0) @ orient + offset if val == '4': size = tiling.TileSize.TILE_4x4 elif val == '1': size = tiling.TileSize.TILE_1x1 elif val == 'i': size = None else: try: new_tile = tiling.TILETYPE_FROM_CHAR[val] except KeyError: LOGGER.warning('Unknown tiletype "{}"!', val) else: tiling.edit_quarter_tile(pos, norm, new_tile, force_tile) continue # Edit the existing tile. try: tile, u, v = tiling.find_tile(pos, norm, force_tile) except KeyError: LOGGER.warning( 'Expected tile, but none found: {}, {}', pos, norm, ) continue if size is None: # Invert the tile. tile[u, v] = tile[u, v].inverted continue # Unless forcing is enabled don't alter the size of GOO_SIDE. if tile[u, v].is_tile and tile[u, v] is not tiling.TileType.GOO_SIDE: tile[u, v] = tiling.TileType.with_color_and_size( size, tile[u, v].color) elif force_tile: # If forcing, make it black. Otherwise no need to change. tile[u, v] = tiling.TileType.with_color_and_size( size, tiling.Portalable.BLACK)
def place_template(inst: Entity) -> None: """Place a template.""" temp_id = inst.fixup.substitute(orig_temp_id) # Special case - if blank, just do nothing silently. if not temp_id: return temp_name, visgroups = template_brush.parse_temp_name(temp_id) try: template = template_brush.get_template(temp_name) except template_brush.InvalidTemplateName: # If we did lookup, display both forms. if temp_id != orig_temp_id: LOGGER.warning('{} -> "{}" is not a valid template!', orig_temp_id, temp_name) else: LOGGER.warning('"{}" is not a valid template!', temp_name) # We don't want an error, just quit. return for vis_flag_block in visgroup_instvars: if all( conditions.check_flag(flag, coll, inst) for flag in vis_flag_block): visgroups.add(vis_flag_block.real_name) force_colour = conf_force_colour if color_var == '<editor>': # Check traits for the colour it should be. traits = instance_traits.get(inst) if 'white' in traits: force_colour = texturing.Portalable.white elif 'black' in traits: force_colour = texturing.Portalable.black else: LOGGER.warning( '"{}": Instance "{}" ' "isn't one with inherent color!", temp_id, inst['file'], ) elif color_var: color_val = conditions.resolve_value(inst, color_var).casefold() if color_val == 'white': force_colour = texturing.Portalable.white elif color_val == 'black': force_colour = texturing.Portalable.black # else: no color var if srctools.conv_bool(conditions.resolve_value(inst, invert_var)): force_colour = template_brush.TEMP_COLOUR_INVERT[conf_force_colour] # else: False value, no invert. if ang_override is not None: orient = ang_override else: orient = rotation @ Angle.from_str(inst['angles', '0 0 0']) origin = conditions.resolve_offset(inst, offset) # If this var is set, it forces all to be included. if srctools.conv_bool( conditions.resolve_value(inst, visgroup_force_var)): visgroups.update(template.visgroups) elif visgroup_func is not None: visgroups.update( visgroup_func( rand.seed(b'temp', template.id, origin, orient), list(template.visgroups), )) LOGGER.debug('Placing template "{}" at {} with visgroups {}', template.id, origin, visgroups) temp_data = template_brush.import_template( vmf, template, origin, orient, targetname=inst['targetname'], force_type=force_type, add_to_map=True, coll=coll, additional_visgroups=visgroups, bind_tile_pos=bind_tile_pos, align_bind=align_bind_overlay, ) if key_block is not None: conditions.set_ent_keys(temp_data.detail, inst, key_block) br_origin = Vec.from_str(key_block.find_key('keys')['origin']) br_origin.localise(origin, orient) temp_data.detail['origin'] = br_origin move_dir = temp_data.detail['movedir', ''] if move_dir.startswith('<') and move_dir.endswith('>'): move_dir = Vec.from_str(move_dir) @ orient temp_data.detail['movedir'] = move_dir.to_angle() for out in outputs: out = out.copy() out.target = conditions.local_name(inst, out.target) temp_data.detail.add_out(out) template_brush.retexture_template( temp_data, origin, inst.fixup, replace_tex, force_colour, force_grid, surf_cat, sense_offset, ) for picker_name, picker_var in picker_vars: picker_val = temp_data.picker_results.get(picker_name, None) if picker_val is not None: inst.fixup[picker_var] = picker_val.value else: inst.fixup[picker_var] = ''
def add_voice( voice_attrs: dict, style_vars: dict, vmf: VMF, use_priority=True, ) -> None: """Add a voice line to the map.""" from precomp.conditions.monitor import make_voice_studio LOGGER.info('Adding Voice Lines!') norm_config = ConfigFile('bee2/voice.cfg', in_conf_folder=False) mid_config = ConfigFile('bee2/mid_voice.cfg', in_conf_folder=False) quote_base = QUOTE_DATA['base', False] quote_loc = get_studio_loc() if quote_base: LOGGER.info('Adding Base instance!') vmf.create_ent( classname='func_instance', targetname='voice', file=INST_PREFIX + quote_base, angles='0 0 0', origin=quote_loc, fixup_style='0', ) # Either box in with nodraw, or place the voiceline studio. has_studio = make_voice_studio(vmf) bullsye_actor = vbsp_options.get(str, 'voice_studio_actor') if bullsye_actor and has_studio: ADDED_BULLSEYES.add(bullsye_actor) global_bullseye = QUOTE_DATA['bullseye', ''] if global_bullseye: add_bullseye(vmf, quote_loc, global_bullseye) allow_mid_voices = not style_vars.get('nomidvoices', False) mid_quotes = [] # Enable using the beep before and after choreo lines. allow_dings = srctools.conv_bool(QUOTE_DATA['use_dings', '0']) if allow_dings: vmf.create_ent( classname='logic_choreographed_scene', targetname='@ding_on', origin=quote_loc + (-8, -16, 0), scenefile='scenes/npc/glados_manual/ding_on.vcd', busyactor="1", # Wait for actor to stop talking onplayerdeath='0', ) vmf.create_ent( classname='logic_choreographed_scene', targetname='@ding_off', origin=quote_loc + (8, -16, 0), scenefile='scenes/npc/glados_manual/ding_off.vcd', busyactor="1", # Wait for actor to stop talking onplayerdeath='0', ) # QuoteEvents allows specifying an instance for particular items, # so a voice line can be played at a certain time. It's only active # in certain styles, but uses the default if not set. for event in QUOTE_DATA.find_all('QuoteEvents', 'Event'): event_id = event['id', ''].casefold() # We ignore the config if no result was executed. if event_id and event_id in QUOTE_EVENTS: # Instances from the voiceline config are in this subfolder, # but not the default item - that's set from the conditions QUOTE_EVENTS[event_id] = INST_PREFIX + event['file'] LOGGER.info('Quote events: {}', list(QUOTE_EVENTS.keys())) if has_responses(): LOGGER.info('Generating responses data..') encode_coop_responses(vmf, quote_loc, allow_dings, voice_attrs) for ind, file in enumerate(QUOTE_EVENTS.values()): if not file: continue vmf.create_ent( classname='func_instance', targetname='voice_event_' + str(ind), file=file, angles='0 0 0', origin=quote_loc, fixup_style='0', ) # Determine the flags that enable/disable specific lines based on which # players are used. player_model = vbsp.BEE2_config.get_val( 'General', 'player_model', 'PETI', ).casefold() is_coop = (vbsp.GAME_MODE == 'COOP') is_sp = (vbsp.GAME_MODE == 'SP') player_flags = { 'sp': is_sp, 'coop': is_coop, 'atlas': is_coop or player_model == 'atlas', 'pbody': is_coop or player_model == 'pbody', 'bendy': is_sp and player_model == 'peti', 'chell': is_sp and player_model == 'sp', 'human': is_sp and player_model in ('peti', 'sp'), 'robot': is_coop or player_model in ('atlas', 'pbody'), } # All which are True. player_flag_set = {val for val, flag in player_flags.items() if flag} # For each group, locate the voice lines. for group in itertools.chain( QUOTE_DATA.find_all('group'), QUOTE_DATA.find_all('midchamber'), ): # type: Property quote_targetname = group['Choreo_Name', '@choreo'] use_dings = group.bool('use_dings', allow_dings) possible_quotes = sorted( find_group_quotes( vmf, group, mid_quotes, use_dings=use_dings, allow_mid_voices=allow_mid_voices, conf=mid_config if group.name == 'midchamber' else norm_config, mid_name=quote_targetname, player_flag_set=player_flag_set, ), key=sort_func, reverse=True, ) LOGGER.debug('Possible {}quotes:', 'mid ' if group.name == 'midchamber' else '') for quot in possible_quotes: LOGGER.debug('- {}', quot) if possible_quotes: choreo_loc = group.vec('choreo_loc', *quote_loc) if use_priority: chosen = possible_quotes[0].lines else: # Chose one of the quote blocks. chosen = rand.seed( b'VOICE_QUOTE_BLOCK', *[ prop['id', 'ID'] for quoteblock in possible_quotes for prop in quoteblock.lines ]).choice(possible_quotes).lines # Use the IDs for the voice lines, so each quote block will chose different lines. rng = rand.seed(b'VOICE_QUOTE', *[prop['id', 'ID'] for prop in chosen]) # Add one of the associated quotes add_quote( vmf, rng.choice(chosen), quote_targetname, choreo_loc, style_vars, use_dings, ) if ADDED_BULLSEYES or QUOTE_DATA.bool('UseMicrophones'): # Add microphones that broadcast audio directly at players. # This ensures it is heard regardless of location. # This is used for Cave and core Wheatley. LOGGER.info('Using microphones...') if vbsp.GAME_MODE == 'SP': vmf.create_ent( classname='env_microphone', targetname='player_speaker_sp', speakername='!player', maxRange='386', origin=quote_loc, ) else: vmf.create_ent( classname='env_microphone', targetname='player_speaker_blue', speakername='!player_blue', maxRange='386', origin=quote_loc, ) vmf.create_ent( classname='env_microphone', targetname='player_speaker_orange', speakername='!player_orange', maxRange='386', origin=quote_loc, ) LOGGER.info('{} Mid quotes', len(mid_quotes)) for mid_lines in mid_quotes: line = rand.seed(b'mid_quote', *[name for item, ding, name in mid_lines ]).choice(mid_lines) mid_item, use_ding, mid_name = line add_quote(vmf, mid_item, mid_name, quote_loc, style_vars, use_ding) LOGGER.info('Done!')
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 rand_func(inst: Entity) -> bool: """Apply the random chance.""" return rand.seed(b'rand_flag', inst, seed).randrange(100) < chance
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. collapse_line: list[Segment | None] if not wall_conf.tex_corner or not floor_conf.tex_corner: collapse_line = list(self.line) 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 # Check tiledefs in the voxels, and assign just in case. # antline corner items don't have them defined, and some embedfaces don't work # properly. But we keep any segments actually defined also. mins, maxs = Vec.bbox(seg.start, seg.end) norm_axis = seg.normal.axis() u_axis, v_axis = Vec.INV_AXIS[norm_axis] for pos in Vec.iter_line(mins, maxs, 128): pos[u_axis] = pos[u_axis] // 128 * 128 + 64 pos[v_axis] = pos[v_axis] // 128 * 128 + 64 pos -= 64 * seg.normal try: tile = tiling.TILES[pos.as_tuple(), seg.normal.as_tuple()] except KeyError: pass else: seg.tiles.add(tile) rng = rand.seed(b'antline', seg.start, seg.end) if seg.type is SegType.CORNER: mat: AntTex if rng.randrange(100) < conf.broken_chance: mat = rng.choice(conf.broken_corner or conf.broken_straight) else: mat = rng.choice(conf.tex_corner or conf.tex_straight) # Because we can, apply a random rotation to mix up the texture. orient = Matrix.from_angle(seg.normal.to_angle( rng.choice((0.0, 90.0, 180.0, 270.0)) )) self._make_overlay( vmf, seg, seg.start, 16.0 * orient.left(), 16.0 * orient.up(), mat, ) else: # Straight # TODO: Break up these segments. for a, b, is_broken in seg.broken_iter(conf.broken_chance): if is_broken: mat = rng.choice(conf.broken_straight) else: mat = rng.choice(conf.tex_straight) self._make_straight( vmf, seg, a, b, mat, )
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 rng = rand.seed(b'pit', pos.x, pos.y) file = rng.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 = rng.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 setup(self, vmf: VMF, tiles: List['TileDef']) -> None: """Build the list of clump locations.""" assert self.portal is not None assert self.orient is not None # Convert the generator key to a generator-specific seed. # That ensures different surfaces don't end up reusing the same # texture indexes. self.gen_seed = b''.join([ self.category.name.encode(), self.portal.name.encode(), self.orient.name.encode(), ]) LOGGER.info('Generating texture clumps...') clump_length: int = self.options['clump_length'] clump_width: int = self.options['clump_width'] # The tiles currently present in the map. orient_z = self.orient.z remaining_tiles: Set[Tuple[float, float, float]] = { (tile.pos + 64 * tile.normal // 128 * 128).as_tuple() for tile in tiles if tile.normal.z == orient_z } # A global RNG for picking clump positions. clump_rand = rand.seed(b'clump_pos') pos_min = Vec() pos_max = Vec() # For debugging, generate skip brushes with the shape of the clumps. debug_visgroup: Optional[VisGroup] if self.options['clump_debug']: debug_visgroup = vmf.create_visgroup( f'{self.category.name}_{self.orient.name}_{self.portal.name}') else: debug_visgroup = None while remaining_tiles: # Pick from a random tile. tile_pos = next( itertools.islice( remaining_tiles, clump_rand.randrange(0, len(remaining_tiles)), len(remaining_tiles), )) remaining_tiles.remove(tile_pos) pos = Vec(tile_pos) # Clumps are long strips mainly extended in one direction # In the other directions extend by 'width'. It can point any axis. direction = clump_rand.choice('xyz') for axis in 'xyz': if axis == direction: dist = clump_length else: dist = clump_width pos_min[axis] = pos[axis] - clump_rand.randint(0, dist) * 128 pos_max[axis] = pos[axis] + clump_rand.randint(0, dist) * 128 remaining_tiles.difference_update( map(Vec.as_tuple, Vec.iter_grid(pos_min, pos_max, 128))) self._clump_locs.append( Clump( pos_min.x, pos_min.y, pos_min.z, pos_max.x, pos_max.y, pos_max.z, # We use this to reseed an RNG, giving us the same textures # each time for the same clump. clump_rand.getrandbits(64).to_bytes(8, 'little'), )) if debug_visgroup is not None: # noinspection PyUnboundLocalVariable debug_brush: Solid = vmf.make_prism( pos_min - 64, pos_max + 64, 'tools/toolsskip', ).solid debug_brush.visgroup_ids.add(debug_visgroup.id) debug_brush.vis_shown = False vmf.add_brush(debug_brush) LOGGER.info( '{}.{}.{}: {} Clumps for {} tiles', self.category.name, self.orient.name, self.portal.name, len(self._clump_locs), len(tiles), )
def apply_variant(inst: Entity) -> None: """Apply the variant.""" rng = rand.seed(b'variant', inst, seed) conditions.add_suffix(inst, f"_var{rng.choice(weighting) + 1}")