def dump_to_map(self, vmf: VMF): """Debug purposes: Dump the info as entities in the map. This makes the map effectively uncompilable... """ # Icons which somewhat match the block type... block_icons = { Block.VOID: 'env_global', Block.SOLID: 'env_cubemap', Block.EMBED: 'func_instance_parms', Block.OCCUPIED: 'info_target', Block.AIR: 'info_null', Block.GOO_SINGLE: 'water_lod_control', Block.GOO_TOP: 'water_lod_control', Block.GOO_MID: 'water_lod_control', Block.GOO_BOTTOM: 'water_lod_control', Block.PIT_SINGLE: 'logic_autosave', Block.PIT_TOP: 'logic_autosave', Block.PIT_MID: 'logic_autosave', Block.PIT_BOTTOM: 'logic_autosave', } for pos, block in self.items(): # type: Vec, Block vmf.create_ent( targetname=block.name.title(), classname=block_icons[block], origin=grid_to_world(pos), pos=str(pos), )
def make_voice_studio(vmf: VMF) -> bool: """Create the voice-line studio. This is either an instance (if monitors are present), or a nodraw room. """ studio_file = vbsp_options.get(str, 'voice_studio_inst') loc = voiceLine.get_studio_loc() if ALL_MONITORS and studio_file: vmf.create_ent( classname='func_instance', file=studio_file, origin=loc, angles='0 0 0', ) return True else: # If there aren't monitors, the studio instance isn't used. # We need to seal anyway. vmf.add_brushes(vmf.make_hollow( loc - 256, loc + 256, thick=32, )) return False
def global_input( vmf: VMF, pos: Union[Vec, str], output: Output, relay_name: str=None, ): """Create a global input, either from a relay or logic_auto. The position is used to place the relay if this is the first time. """ try: glob_ent = GLOBAL_INPUT_ENTS[relay_name] except KeyError: if relay_name == '': glob_ent = GLOBAL_INPUT_ENTS[''] = vmf.create_ent( classname='logic_auto', spawnflags='1', # Remove on fire origin=pos, ) else: glob_ent = GLOBAL_INPUT_ENTS[relay_name] = vmf.create_ent( classname='logic_relay', targetname=relay_name, origin=pos, ) if not relay_name: output.output = 'OnMapSpawn' output.only_once = True output.comma_sep = False glob_ent.add_out(output)
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property): """Create the spawn point for ATLAS in the entry corridor. It produces either an instance or the normal spawn entity. This is required since ATLAS may need to have the paint gun logic. The two parameters `origin` and `facing` must be set to determine the required position. If `global` is set, the spawn point will be absolute instead of relative to the current instance. """ if vbsp.GAME_MODE != 'COOP': return RES_EXHAUSTED is_tag = vbsp_options.get(str, 'game_id') == utils.STEAM_IDS['TAG'] origin = res.vec('origin') normal = res.vec('facing', z=1) # Some styles might want to ignore the instance we're running on. if not res.bool('global'): origin = origin.rotate_by_str(inst['angles']) normal = normal.rotate_by_str(inst['angles']) origin += Vec.from_str(inst['origin']) angles = normal.to_angle() if is_tag: vmf.create_ent( classname='func_instance', targetname='paint_gun', origin=origin - (0, 0, 16), angles=angles, # Generated by the BEE2 app. file='instances/bee2/tag_coop_gun.vmf', ) # Blocks ATLAS from having a gun vmf.create_ent( classname='info_target', targetname='supress_blue_portalgun_spawn', origin=origin, angles='0 0 0', ) # Allows info_target to work vmf.create_ent( classname='env_global', targetname='no_spawns', globalstate='portalgun_nospawn', initialstate=1, spawnflags=1, # Use initial state origin=origin, ) vmf.create_ent( classname='info_coop_spawn', targetname='@coop_spawn_blue', ForceGunOnSpawn=int(not is_tag), origin=origin, angles=angles, enabled=1, StartingTeam=3, # ATLAS ) return RES_EXHAUSTED
def res_sendificator(vmf: VMF, inst: Entity): """Implement Sendificators.""" # For our version, we know which sendtor connects to what laser, # so we can couple the logic together (avoiding @sendtor_mutex). sendtor_name = inst['targetname'] sendtor = connections.ITEMS[sendtor_name] sendtor.enable_cmd += (Output( '', '@{}_las_relay_*'.format(sendtor_name), 'Trigger', delay=0.01, ), ) for ind, conn in enumerate(list(sendtor.outputs), start=1): las_item = conn.to_item conn.remove() try: targ_offset, targ_normal = SENDTOR_TARGETS[las_item.name] except KeyError: LOGGER.warning('"{}" is not a Sendificator target!', las_item.name) continue angles = Vec.from_str(las_item.inst['angles']) targ_offset = targ_offset.copy() targ_normal = targ_normal.copy().rotate(*angles) targ_offset.localise( Vec.from_str(las_item.inst['origin']), angles, ) relay_name = '@{}_las_relay_{}'.format(sendtor_name, ind) relay = vmf.create_ent( 'logic_relay', targetname=relay_name, origin=targ_offset, angles=targ_normal.to_angle(), ) relay.add_out( Output('OnTrigger', '!self', 'RunScriptCode', '::sendtor_source <- self;'), Output('OnTrigger', '@sendtor_fire', 'Trigger'), ) if not las_item.inputs: # No other inputs, make it on always. PeTI automatically turns # it off when inputs are connected, which is annoying. las_item.inst.fixup['$start_enabled'] = '1' is_on = True else: is_on = las_item.inst.fixup.bool('$start_enabled') relay['StartDisabled'] = not is_on las_item.enable_cmd += (Output('', relay_name, 'Enable'),) las_item.disable_cmd += (Output('', relay_name, 'Disable'),) LOGGER.info('Relay: {}', relay)
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property): """Create the spawn point for ATLAS, in Aperture Tag. This creates an instance with the desired orientation. The two parameters 'origin' and 'angles' must be set. """ if vbsp.GAME_MODE != 'COOP': return RES_EXHAUSTED is_tag = vbsp_options.get(str, 'game_id') == utils.STEAM_IDS['TAG'] offset = res.vec('origin').rotate_by_str(inst['angles']) normal = res.vec('facing', z=1).rotate_by_str( inst['angles'], ) origin = Vec.from_str(inst['origin']) origin += offset angles = normal.to_angle() if is_tag: vmf.create_ent( classname='func_instance', targetname='paint_gun', origin=origin - (0, 0, 16), angles=angles, # Generated by the BEE2 app. file='instances/bee2/tag_coop_gun.vmf', ) # Blocks ATLAS from having a gun vmf.create_ent( classname='info_target', targetname='supress_blue_portalgun_spawn', origin=origin, angles='0 0 0', ) # Allows info_target to work vmf.create_ent( classname='env_global', targetname='no_spawns', globalstate='portalgun_nospawn', initialstate=1, spawnflags=1, # Use initial state origin=origin, ) vmf.create_ent( classname='info_coop_spawn', targetname='@coop_spawn_blue', ForceGunOnSpawn=int(not is_tag), origin=origin, angles=angles, enabled=1, StartingTeam=3, # ATLAS ) return RES_EXHAUSTED
def _make_squarebeam(vmf: VMF, origin: Vec, skin='0', size=''): """Make a squarebeam prop at the given location.""" return vmf.create_ent( classname='prop_static', angles='0 0 0', origin=origin, model='models/anim_wp/framework/squarebeam_off' + size + '.mdl', skin=skin, disableshadows='1', )
def precache_model(vmf: VMF, mdl_name: str): """Precache the given model for switching. This places it as a `prop_dynamic_override`. """ if not mdl_name.startswith('models/'): mdl_name = 'models/' + mdl_name if not mdl_name.endswith('.mdl'): mdl_name += '.mdl' if mdl_name in CACHED_MODELS: return CACHED_MODELS.add(mdl_name) vmf.create_ent( classname='comp_precache_model', origin=vbsp_options.get(Vec, 'global_ents_loc'), model=mdl_name, )
def pack_files( vmf: VMF, *files: str, file_type: str='generic', ) -> None: """Add the given files to the packing list.""" packlist = set(files) - _PACKED_FILES if not packlist: return ent = vmf.create_ent( classname='comp_pack', origin=vbsp_options.get(Vec, 'global_ents_loc'), ) for i, file in enumerate(packlist, start=1): ent[file_type + str(i)] = file
def res_create_entity(vmf: VMF, inst: Entity, res: Property): """Create an entity. 'keys' and 'localkeys' defines the new keyvalues used. 'Origin' will be used to offset the given amount from the current location. """ origin = Vec.from_str(inst['origin']) new_ent = vmf.create_ent( # Ensure there's a classname, just in case. classname='info_null' ) conditions.set_ent_keys(new_ent, inst, res) origin += Vec.from_str(new_ent['origin']).rotate_by_str(inst['angles']) new_ent['origin'] = origin new_ent['angles'] = inst['angles']
def gen_squarebeams(vmf: VMF, p1: Vec, p2: Vec, skin, gen_collision=True): """Generate squarebeams props to fill the space given. The space should be in multiples of 64. The squarebeams brush will be aligned to the lowest point in the space. """ z = min(p1.z, p2.z) + 8 min_x = min(p1.x, p2.x) min_y = min(p1.y, p2.y) max_x = max(p1.x, p2.x) max_y = max(p1.y, p2.y) dist_x = max_x - min_x dist_y = max_y - min_y # After this x or y dist, move to the next grid size. cutoff_512_x = dist_x // 512 * 512 cutoff_256_x = dist_x // 256 * 256 cutoff_128_x = dist_x // 128 * 128 cutoff_512_y = dist_y // 512 * 512 cutoff_256_y = dist_y // 256 * 256 cutoff_128_y = dist_y // 128 * 128 for x, y in utils.iter_grid( max_x=int(dist_x), max_y=int(dist_y), stride=64, ): if x < cutoff_512_x and y < cutoff_512_y: # Make 1 prop every 512 units, at the center if x % 512 == 0 and y % 512 == 0: _make_squarebeam( vmf, Vec(min_x + x + 256, min_y + y + 256, z), skin, '_8x8', ) elif x < cutoff_256_x and y < cutoff_256_y: if x % 256 == 0 and y % 256 == 0: _make_squarebeam( vmf, Vec(min_x + x + 128, min_y + y + 128, z), skin, '_4x4', ) elif x < cutoff_128_x and y < cutoff_128_y: if x % 128 == 0 and y % 128 == 0: _make_squarebeam( vmf, Vec(min_x + x + 64, min_y + y + 64, z), skin, '_2x2', ) else: # Make squarebeams for every point! _make_squarebeam( vmf, Vec(min_x + x + 32, min_y + y + 32, z), skin, ) if gen_collision: collision = vmf.create_ent( classname='func_brush', disableshadows='1', disableflashlight='1', disablereceiveshadows='1', shadowdepthnocache='1', solidity='2', # Always Solid solidbsp='1', ) for x in range(int(min_x)+64, int(max_x), 64): collision.solids.append( vmf.make_prism( p1=Vec(x-2, min_y+2, z-2), p2=Vec(x+2, max_y-2, z-8), mat='tools/toolsnodraw', ).solid ) for y in range(int(min_y)+64, int(max_y), 64): collision.solids.append( vmf.make_prism( p1=Vec(min_x+2, y-2, z-2), p2=Vec(max_x-2, y+2, z-8), mat='tools/toolsnodraw', ).solid ) for x1, y1, x2, y2 in [ (min_x, min_y, max_x, min_y+2), (min_x, max_y, max_x, max_y-2), (min_x, min_y, min_x+2, max_y), (max_x, min_y, max_x-2, max_y), ]: collision.solids.append( vmf.make_prism( p1=Vec(x1, y1, z-2), p2=Vec(x2, y2, z-8), mat='tools/toolsnodraw', ).solid )
def res_fix_rotation_axis(vmf: VMF, ent: Entity, res: Property): """Properly setup rotating brush entities to match the instance. This uses the orientation of the instance to determine the correct spawnflags to make it rotate in the correct direction. This can either modify an existing entity (which may be in an instance), or generate a new one. The generated brush will be 2x2x2 units large, and always set to be non-solid. For both modes: - `Axis`: specifies the rotation axis local to the instance. - `Reversed`: If set, flips the direction around. - `Classname`: Specifies which entity, since the spawnflags required varies. For application to an existing entity: - `ModifyTarget`: The local name of the entity to modify. For brush generation mode: - `Pos` and `name` are local to the instance, and will set the `origin` and `targetname` respectively. - `Keys` are any other keyvalues to be be set. - `Flags` sets additional spawnflags. Multiple values may be separated by `+`, and will be added together. - `Classname` specifies which entity will be created, as well as which other values will be set to specify the correct orientation. - `AddOut` is used to add outputs to the generated entity. It takes the options `Output`, `Target`, `Input`, `Inst_targ`, `Param` and `Delay`. If `Inst_targ` is defined, it will be used with the input to construct an instance proxy input. If `OnceOnly` is set, the output will be deleted when fired. Permitted entities: * [`func_door_rotating`](https://developer.valvesoftware.com/wiki/func_door_rotating) * [`func_platrot`](https://developer.valvesoftware.com/wiki/func_platrot) * [`func_rot_button`](https://developer.valvesoftware.com/wiki/func_rot_button) * [`func_rotating`](https://developer.valvesoftware.com/wiki/func_rotating) * [`momentary_rot_button`](https://developer.valvesoftware.com/wiki/momentary_rot_button) """ des_axis = res['axis', 'z'].casefold() reverse = res.bool('reversed') door_type = res['classname', 'func_door_rotating'] orient = round(Matrix.from_angle(Angle.from_str(ent['angles'])), 6) axis = Vec.with_axes(des_axis, 1) @ orient if axis.x > 0 or axis.y > 0 or axis.z > 0: # If it points forward, we need to reverse the rotating door reverse = not reverse try: flag_values = FLAG_ROTATING[door_type] except KeyError: LOGGER.warning('Unknown rotating brush type "{}"!', door_type) return name = res['ModifyTarget', ''] if name: name = conditions.local_name(ent, name) setter_loc = ent['origin'] else: # Generate a brush. name = conditions.local_name(ent, res['name', '']) pos = res.vec('Pos') @ Angle.from_str(ent['angles', '0 0 0']) pos += Vec.from_str(ent['origin', '0 0 0']) setter_loc = str(pos) door_ent = vmf.create_ent( classname=door_type, targetname=name, origin=pos.join(' '), # Extra stuff to apply to the flags (USE, toggle, etc) spawnflags=sum( map( # Add together multiple values srctools.conv_int, res['flags', '0'].split('+') # Make the door always non-solid! )) | flag_values.get('solid_flags', 0), ) conditions.set_ent_keys(door_ent, ent, res) for output in res.find_all('AddOut'): door_ent.add_out( Output( out=output['Output', 'OnUse'], inp=output['Input', 'Use'], targ=output['Target', ''], inst_in=output['Inst_targ', None], param=output['Param', ''], delay=srctools.conv_float(output['Delay', '']), times=(1 if srctools.conv_bool(output['OnceOnly', False]) else -1), )) # Generate brush door_ent.solids = [vmf.make_prism(pos - 1, pos + 1).solid] # Add or remove flags as needed by creating KV setters. for flag, value in zip( ('x', 'y', 'z', 'rev'), [axis.x != 0, axis.y != 0, axis.z != 0, reverse], ): if flag in flag_values: vmf.create_ent( 'comp_kv_setter', origin=setter_loc, target=name, mode='flags', kv_name=flag_values[flag], kv_value_local=value, ) # This ent uses a keyvalue for reversing... if door_type == 'momentary_rot_button': vmf.create_ent( 'comp_kv_setter', origin=setter_loc, target=name, mode='kv', kv_name='StartDirection', kv_value_local='1' if reverse else '-1', )
def edit_panel(vmf: VMF, inst: Entity, props: Property, create: bool) -> None: """Implements SetPanelOptions and CreatePanel.""" orient = Matrix.from_angle(Angle.from_str(inst['angles'])) normal: Vec = round(props.vec('normal', 0, 0, 1) @ orient, 6) origin = Vec.from_str(inst['origin']) uaxis, vaxis = Vec.INV_AXIS[normal.axis()] points: Set[Tuple[float, float, float]] = set() if 'point' in props: for prop in props.find_all('point'): points.add( conditions.resolve_offset(inst, prop.value, zoff=-64).as_tuple()) elif 'pos1' in props and 'pos2' in props: pos1, pos2 = Vec.bbox( conditions.resolve_offset(inst, props['pos1', '-48 -48 0'], zoff=-64), conditions.resolve_offset(inst, props['pos2', '48 48 0'], zoff=-64), ) points.update(map(Vec.as_tuple, Vec.iter_grid(pos1, pos2, 32))) else: # Default to the full tile. points.update({(Vec(u, v, -64.0) @ orient + origin).as_tuple() for u in [-48.0, -16.0, 16.0, 48.0] for v in [-48.0, -16.0, 16.0, 48.0]}) tiles_to_uv: Dict[tiling.TileDef, Set[Tuple[int, int]]] = defaultdict(set) for pos in points: try: tile, u, v = tiling.find_tile(Vec(pos), normal, force=create) except KeyError: continue tiles_to_uv[tile].add((u, v)) if not tiles_to_uv: LOGGER.warning('"{}": No tiles found for panels!', inst['targetname']) return # If bevels is provided, parse out the overall world positions. bevel_world: Optional[Set[Tuple[int, int]]] try: bevel_prop = props.find_key('bevel') except NoKeyError: bevel_world = None else: bevel_world = set() if bevel_prop.has_children(): # Individually specifying offsets. for bevel_str in bevel_prop.as_array(): bevel_point = Vec.from_str(bevel_str) @ orient + origin bevel_world.add( (int(bevel_point[uaxis]), int(bevel_point[vaxis]))) elif srctools.conv_bool(bevel_prop.value): # Fill the bounding box. bbox_min, bbox_max = Vec.bbox(map(Vec, points)) off = Vec.with_axes(uaxis, 32, vaxis, 32) bbox_min -= off bbox_max += off for pos in Vec.iter_grid(bbox_min, bbox_max, 32): if pos.as_tuple() not in points: bevel_world.add((pos[uaxis], pos[vaxis])) # else: No bevels. panels: List[tiling.Panel] = [] for tile, uvs in tiles_to_uv.items(): if create: panel = tiling.Panel( None, inst, tiling.PanelType.NORMAL, thickness=4, bevels=(), ) panel.points = uvs tile.panels.append(panel) else: for panel in tile.panels: if panel.same_item(inst) and panel.points == uvs: break else: LOGGER.warning('No panel to modify found for "{}"!', inst['targetname']) continue panels.append(panel) pan_type = '<nothing?>' try: pan_type = conditions.resolve_value(inst, props['type']) panel.pan_type = tiling.PanelType(pan_type.lower()) except LookupError: pass except ValueError: raise ValueError('Unknown panel type "{}"!'.format(pan_type)) if 'thickness' in props: panel.thickness = srctools.conv_int( conditions.resolve_value(inst, props['thickness'])) if panel.thickness not in (2, 4, 8): raise ValueError( '"{}": Invalid panel thickess {}!\n' 'Must be 2, 4 or 8.', inst['targetname'], panel.thickness, ) if bevel_world is not None: panel.bevels.clear() for u, v in bevel_world: # Convert from world points to UV positions. u = (u - tile.pos[uaxis] + 48) / 32 v = (v - tile.pos[vaxis] + 48) / 32 # Cull outside here, we wont't use them. if -1 <= u <= 4 and -1 <= v <= 4: panel.bevels.add((u, v)) if 'offset' in props: panel.offset = conditions.resolve_offset(inst, props['offset']) panel.offset -= Vec.from_str(inst['origin']) if 'template' in props: # We only want the template inserted once. So remove it from all but one. if len(panels) == 1: panel.template = conditions.resolve_value( inst, props['template']) else: panel.template = '' if 'nodraw' in props: panel.nodraw = srctools.conv_bool( conditions.resolve_value(inst, props['nodraw'])) if 'seal' in props: panel.seal = srctools.conv_bool( conditions.resolve_value(inst, props['seal'])) if 'move_bullseye' in props: panel.steals_bullseye = srctools.conv_bool( conditions.resolve_value(inst, props['move_bullseye'])) if 'keys' in props or 'localkeys' in props: # First grab the existing ent, so we can edit it. # These should all have the same value, unless they were independently # edited with mismatching point sets. In that case destroy all those existing ones. existing_ents: Set[Optional[Entity]] = { panel.brush_ent for panel in panels } try: [brush_ent] = existing_ents except ValueError: LOGGER.warning( 'Multiple independent panels for "{}" were made, then the ' 'brush entity was edited as a group! Discarding ' 'individual ents...', inst['targetname']) for brush_ent in existing_ents: if brush_ent is not None and brush_ent in vmf.entities: brush_ent.remove() brush_ent = None if brush_ent is None: brush_ent = vmf.create_ent('') old_pos = brush_ent.keys.pop('origin', None) conditions.set_ent_keys(brush_ent, inst, props) if not brush_ent['classname']: if create: # This doesn't make sense, you could just omit the prop. LOGGER.warning( 'No classname provided for panel "{}"!', inst['targetname'], ) # Make it a world brush. brush_ent.remove() brush_ent = None else: # We want to do some post-processing. # Localise any origin value. if 'origin' in brush_ent.keys: pos = Vec.from_str(brush_ent['origin']) pos.localise( Vec.from_str(inst['origin']), Vec.from_str(inst['angles']), ) brush_ent['origin'] = pos elif old_pos is not None: brush_ent['origin'] = old_pos # If it's func_detail, clear out all the keys. # Particularly `origin`, but the others are useless too. if brush_ent['classname'] == 'func_detail': brush_ent.clear_keys() brush_ent['classname'] = 'func_detail' for panel in panels: panel.brush_ent = brush_ent
def place_catwalk_connections(vmf: VMF, instances, point_a: Vec, point_b: Vec): """Place catwalk sections to connect two straight points.""" diff = point_b - point_a # The horizontal unit vector in the direction we are placing catwalks direction = diff.copy() direction.z = 0 distance = direction.len() - 128 direction = direction.norm() if diff.z > 0: angle = INST_ANGLE[direction.as_tuple()] # We need to add stairs for stair_pos in range(0, int(diff.z), 128): # Move twice the vertical horizontally # plus 128 so we don't start in point A loc = point_a + (2 * stair_pos + 128) * direction # Do the vertical offset loc.z += stair_pos vmf.create_ent( classname='func_instance', origin=loc.join(' '), angles=angle, file=instances['stair'], ) # This is the location we start flat sections at point_a = loc + 128 * direction point_a.z += 128 elif diff.z < 0: # We need to add downward stairs # They point opposite to normal ones LOGGER.debug('down from {}', point_a) angle = INST_ANGLE[(-direction).as_tuple()] for stair_pos in range(0, -int(diff.z), 128): LOGGER.debug(stair_pos) # Move twice the vertical horizontally loc = point_a + (2 * stair_pos + 256) * direction # type: Vec # Do the vertical offset plus additional 128 units # to account for the moved instance loc.z -= (stair_pos + 128) vmf.create_ent( classname='func_instance', origin=loc.join(' '), angles=angle, file=instances['stair'], ) # Adjust point A to be at the end of the catwalks point_a = loc # Remove the space the stairs take up from the horiz distance distance -= abs(diff.z) * 2 # Now do straight sections LOGGER.debug('Stretching {} {}', distance, direction) angle = INST_ANGLE[direction.as_tuple()] loc = point_a + (direction * 128) # Figure out the most efficent number of sections for segment_len in utils.fit(distance, [512, 256, 128]): vmf.create_ent( classname='func_instance', origin=loc.join(' '), angles=angle, file=instances['straight_' + str(segment_len)], ) loc += (segment_len * direction)
def res_reshape_fizzler(vmf: VMF, shape_inst: Entity, res: Property): """Convert a fizzler connected via the output to a new shape. This allows for different placing of fizzler items. * Each `segment` parameter should be a `x y z;x y z` pair of positions that represent the ends of the fizzler. * `up_axis` should be set to a normal vector pointing in the new 'upward' direction. * If none are connected, a regular fizzler will be synthesized. The following fixup vars will be set to allow the shape to match the fizzler: * `$uses_nodraw` will be 1 if the fizzler nodraws surfaces behind it. """ shape_name = shape_inst['targetname'] shape_item = connections.ITEMS.pop(shape_name) shape_angles = Vec.from_str(shape_inst['angles']) up_axis = res.vec('up_axis').rotate(*shape_angles) for conn in shape_item.outputs: fizz_item = conn.to_item try: fizz = fizzler.FIZZLERS[fizz_item.name] except KeyError: LOGGER.warning( 'Reshaping fizzler with non-fizzler output ({})! Ignoring!', fizz_item.name) continue fizz.emitters.clear() # Remove old positions. fizz.up_axis = up_axis fizz.base_inst['origin'] = shape_inst['origin'] fizz.base_inst['angles'] = shape_inst['angles'] break else: # No fizzler, so generate a default. # We create the fizzler instance, Fizzler object, and Item object # matching it. # This is hardcoded to use regular Emancipation Fields. base_inst = vmf.create_ent( targetname=shape_name, classname='func_instance', origin=shape_inst['origin'], angles=shape_inst['angles'], file=resolve_inst('<ITEM_BARRIER_HAZARD:fizz_base>'), ) base_inst.fixup.update(shape_inst.fixup) fizz = fizzler.FIZZLERS[shape_name] = fizzler.Fizzler( fizzler.FIZZ_TYPES['VALVE_MATERIAL_EMANCIPATION_GRID'], up_axis, base_inst, [], ) fizz_item = connections.Item( base_inst, connections.ITEM_TYPES['item_barrier_hazard'], shape_item.ant_floor_style, shape_item.ant_wall_style, ) connections.ITEMS[shape_name] = fizz_item # Detach this connection and remove traces of it. for conn in list(shape_item.outputs): conn.remove() # Transfer the inputs from us to the fizzler. for inp in list(shape_item.inputs): inp.to_item = fizz_item shape_item.delete_antlines() fizz_base = fizz.base_inst fizz_base['origin'] = shape_inst['origin'] origin = Vec.from_str(shape_inst['origin']) fizz.has_cust_position = True # Since the fizzler is moved elsewhere, it's the responsibility of # the new item to have holes. fizz.embedded = False # So tell it whether or not it needs to do so. shape_inst.fixup['$uses_nodraw'] = fizz.fizz_type.nodraw_behind for seg_prop in res.find_all('Segment'): vec1, vec2 = seg_prop.value.split(';') seg_min_max = Vec.bbox( Vec.from_str(vec1).rotate(*shape_angles) + origin, Vec.from_str(vec2).rotate(*shape_angles) + origin, ) fizz.emitters.append(seg_min_max)
def make_barriers(vmf: VMF, get_tex: Callable[[str], str]): """Make barrier entities. get_tex is vbsp.get_tex.""" glass_temp = template_brush.get_scaling_template( vbsp_options.get(str, "glass_template")) grate_temp = template_brush.get_scaling_template( vbsp_options.get(str, "grating_template")) # Avoid error without this package. if HOLES: # Grab the template solids we need. hole_temp = template_brush.get_template( vbsp_options.get(str, 'glass_hole_temp')) hole_world, hole_detail, _ = hole_temp.visgrouped({'small'}) hole_temp_small = hole_world + hole_detail hole_world, hole_detail, _ = hole_temp.visgrouped({'large'}) hole_temp_large = hole_world + hole_detail hole_world, hole_detail, _ = hole_temp.visgrouped({'large_corner'}) hole_temp_corner = hole_world + hole_detail else: hole_temp_small = hole_temp_large = hole_temp_corner = None floorbeam_temp = vbsp_options.get(str, 'glass_floorbeam_temp') if vbsp_options.get_itemconf('BEE_PELLET:PelletGrating', False): # Merge together these existing filters in global_pti_ents vmf.create_ent( origin=vbsp_options.get(Vec, 'global_pti_ents_loc'), targetname='@grating_filter', classname='filter_multi', filtertype=0, negated=0, filter01='@not_pellet', filter02='@not_paint_bomb', ) else: # Just skip paint bombs. vmf.create_ent( origin=vbsp_options.get(Vec, 'global_pti_ents_loc'), targetname='@grating_filter', classname='filter_activator_class', negated=1, filterclass='prop_paint_bomb', ) # Group the positions by planes in each orientation. # This makes them 2D grids which we can optimise. # (normal_dist, positive_axis, type) -> [(x, y)] slices = defaultdict( set ) # type: Dict[Tuple[Tuple[float, float, float], bool, BarrierType], Set[Tuple[float, float]]] # We have this on the 32-grid so we can cut squares for holes. for (origin, normal), barr_type in BARRIERS.items(): origin = Vec(origin) normal = Vec(normal) norm_axis = normal.axis() u, v = origin.other_axes(norm_axis) norm_pos = Vec.with_axes(norm_axis, origin) slice_plane = slices[ norm_pos.as_tuple(), # distance from origin to this plane. normal[norm_axis] > 0, barr_type, ] for u_off in [-48, -16, 16, 48]: for v_off in [-48, -16, 16, 48]: slice_plane.add(( (u + u_off) // 32, (v + v_off) // 32, )) # Remove pane sections where the holes are. We then generate those with # templates for slanted parts. for (origin, normal), hole_type in HOLES.items(): barr_type = BARRIERS[origin, normal] origin = Vec(origin) normal = Vec(normal) norm_axis = normal.axis() u, v = origin.other_axes(norm_axis) norm_pos = Vec.with_axes(norm_axis, origin) slice_plane = slices[norm_pos.as_tuple(), normal[norm_axis] > 0, barr_type, ] if hole_type is HoleType.LARGE: offsets = (-80, -48, -16, 16, 48, 80) hole_temp = hole_temp_large.copy() else: offsets = (-16, 16) hole_temp = hole_temp_small.copy() for u_off in offsets: for v_off in offsets: # Skip the corners on large holes. # Those aren't actually used, so skip them. That way # we can have them diagonally or without glass in the corner. if u_off in (-80, 80) and v_off in (-80, 80): continue slice_plane.discard(( (u + u_off) // 32, (v + v_off) // 32, )) # Now generate the curved brushwork. if barr_type is BarrierType.GLASS: front_temp = glass_temp front_mat = get_tex('special.glass') elif barr_type is BarrierType.GRATING: front_temp = grate_temp front_mat = get_tex('special.grating') else: raise NotImplementedError angles = normal.to_angle(0) # Angle corresponding to the brush, for the corners. angle_list = [angles] * len(hole_temp) # This is a tricky bit. Two large templates would collide # diagonally, # so chop off the corners, then put them back only if there's not # one diagonally. if hole_type is HoleType.LARGE: for roll in (0, 90, 180, 270): corn_angles = angles.copy() corn_angles.z = roll hole_off = origin + Vec(y=128, z=128).rotate(*corn_angles) diag_type = HOLES.get( (hole_off.as_tuple(), normal.as_tuple()), None, ) if diag_type is not HoleType.LARGE: hole_temp += hole_temp_corner angle_list += [corn_angles] * len(hole_temp_corner) def solid_pane_func(off1, off2, mat): """Given the two thicknesses, produce the curved hole from the template.""" off_min = min(off1, off2) off_max = max(off1, off2) new_brushes = [brush.copy(vmf_file=vmf) for brush in hole_temp] for brush, br_angles in zip(new_brushes, angle_list): for face in brush.sides: face.mat = mat f_norm = face.normal() if f_norm.x == 1: face.translate(Vec(x=4 - off_max)) # face.mat = 'min' elif f_norm.x == -1: face.translate(Vec(x=-4 - off_min)) # face.mat = 'max' face.localise(origin, br_angles) return new_brushes make_glass_grating( vmf, origin, normal, barr_type, front_temp, front_mat, solid_pane_func, ) for (plane_pos, is_pos, barr_type), pos_slice in slices.items(): plane_pos = Vec(plane_pos) norm_axis = plane_pos.axis() normal = Vec.with_axes(norm_axis, 1 if is_pos else -1) if barr_type is BarrierType.GLASS: front_temp = glass_temp front_mat = get_tex('special.glass') elif barr_type is BarrierType.GRATING: front_temp = grate_temp front_mat = get_tex('special.grating') else: raise NotImplementedError u_axis, v_axis = Vec.INV_AXIS[norm_axis] for min_u, min_v, max_u, max_v in grid_optimise( dict.fromkeys(pos_slice, True)): # These are two points in the origin plane, at the borders. pos_min = Vec.with_axes( norm_axis, plane_pos, u_axis, min_u * 32, v_axis, min_v * 32, ) pos_max = Vec.with_axes( norm_axis, plane_pos, u_axis, max_u * 32 + 32, v_axis, max_v * 32 + 32, ) def solid_pane_func(pos1, pos2, mat): """Make the solid brush.""" return [ vmf.make_prism( pos_min + normal * (64.0 - pos1), pos_max + normal * (64.0 - pos2), mat=mat, ).solid ] make_glass_grating( vmf, (pos_min + pos_max) / 2, normal, barr_type, front_temp, front_mat, solid_pane_func, ) if floorbeam_temp: LOGGER.info('Adding Glass floor beams...') add_glass_floorbeams(vmf, floorbeam_temp) LOGGER.info('Done!')
def make_glass_grating( vmf: VMF, ent_pos: Vec, normal: Vec, barr_type: BarrierType, front_temp: template_brush.ScalingTemplate, solid_func: Callable[[float, float, str], list[Solid]], ): """Make all the brushes needed for glass/grating. solid_func() is called with two offsets from the voxel edge, and returns a matching list of solids. This allows doing holes and normal panes with the same function. barrier_type is either 'glass' or 'grating'. """ if barr_type is BarrierType.GLASS: main_ent = vmf.create_ent('func_detail') player_clip_mat = consts.Tools.PLAYER_CLIP_GLASS tex_cat = 'glass' else: player_clip_mat = consts.Tools.PLAYER_CLIP_GRATE main_ent = vmf.create_ent( 'func_brush', renderfx=14, # Constant Glow solidity=1, # Never solid origin=ent_pos, ) tex_cat = 'grating' # The actual glass/grating brush - 0.5-1.5 units back from the surface. main_ent.solids = solid_func(0.5, 1.5, consts.Tools.NODRAW) for face in main_ent.sides(): if abs(Vec.dot(normal, face.normal())) > 0.99: texturing.apply(texturing.GenCat.SPECIAL, face, tex_cat) front_temp.apply(face, change_mat=False) if normal.z == 0: # If vertical, we don't care about footsteps. # So just use 'normal' clips. player_clip = vmf.create_ent('func_detail') player_clip_mat = consts.Tools.PLAYER_CLIP else: # This needs to be a func_brush, otherwise the clip texture data # will be merged with other clips. player_clip = vmf.create_ent( 'func_brush', solidbsp=1, origin=ent_pos, ) # We also need a func_detail clip, which functions on portals. # Make it thinner, so it doesn't impact footsteps. player_thin_clip = vmf.create_ent('func_detail') player_thin_clip.solids = solid_func(0.5, 3.5, consts.Tools.PLAYER_CLIP) player_clip.solids = solid_func(0, 4, player_clip_mat) if barr_type is BarrierType.GRATING: # Add the VPhysics clip. phys_clip = vmf.create_ent( 'func_clip_vphysics', filtername='@grating_filter', origin=ent_pos, StartDisabled=0, ) phys_clip.solids = solid_func(0, 2, consts.Tools.TRIGGER)
def add_glass_floorbeams(vmf: VMF, temp_name: str): """Add beams to separate large glass panels. The texture is assumed to match plasticwall004a's shape. """ template = template_brush.get_template(temp_name) beam_template: Solid try: [beam_template] = template.visgrouped_solids() except ValueError: raise ValueError( 'Bad Glass Floorbeam template! Must have exactly one brush.') # Grab the 'end' side, which we move around. for side in beam_template.sides: if side.normal() == (-1, 0, 0): beam_end_face = side break else: raise ValueError('Not aligned to world...') separation = options.get(int, 'glass_floorbeam_sep') + 1 separation *= 128 # First we want to find all the groups of contiguous glass sections. # This is a mapping from some glass piece to its group list. groups = {} for (origin, normal_tup), barr_type in BARRIERS.items(): # Grating doesn't use it. if barr_type is not BarrierType.GLASS: continue normal = Vec(normal_tup) if not normal.z: # Not walls. continue pos = Vec(origin) + normal * 62 groups[pos.as_tuple()] = [pos] # Loop over every pos and check in the +x/y directions for another glass # piece. If there, merge the two lists and set every pos in the group to # point to the new list. # Once done, every unique list = a group. for pos_tup in groups.keys(): pos = Vec(pos_tup) for off in ((128, 0, 0), (0, 128, 0)): neighbour = (pos + off).as_tuple() if neighbour in groups: our_group = groups[pos_tup] neigh_group = groups[neighbour] if our_group is neigh_group: continue # Now merge the two lists. We then need to update all dict # locations to point to the new list. if len(neigh_group) > len(our_group): small_group, large_group = our_group, neigh_group else: small_group, large_group = neigh_group, our_group large_group.extend(small_group) for pos in small_group: groups[pos.as_tuple()] = large_group # Remove duplicates objects by using the ID as key.. groups = list({id(group): group for group in groups.values()}.values()) # Side -> u, v or None for group in groups: bbox_min, bbox_max = Vec.bbox(group) dimensions = bbox_max - bbox_min # Our beams align to the smallest axis. if dimensions.y > dimensions.x: beam_ax = 'x' side_ax = 'y' rot = Matrix() else: beam_ax = 'y' side_ax = 'x' rot = Matrix.from_yaw(90) # Build min, max tuples for each axis in the other direction. # This tells us where the beams will be. beams: dict[float, tuple[float, float]] = {} # Add 128 so the first pos isn't a beam. offset = bbox_min[side_ax] + 128 for pos in group: side_off = pos[side_ax] beam_off = pos[beam_ax] # Skip over non-'sep' positions.. if (side_off - offset) % separation != 0: continue try: min_off, max_off = beams[side_off] except KeyError: beams[side_off] = beam_off, beam_off else: beams[side_off] = min(min_off, beam_off), max(max_off, beam_off) detail = vmf.create_ent('func_detail') for side_off, (min_off, max_off) in beams.items(): for min_pos, max_pos in beam_hole_split( beam_ax, Vec.with_axes(side_ax, side_off, beam_ax, min_off, 'z', bbox_min), Vec.with_axes(side_ax, side_off, beam_ax, max_off, 'z', bbox_min), ): if min_pos[beam_ax] >= max_pos[beam_ax]: raise ValueError(min_pos, max_pos, beam_ax) # Make the beam. # Grab the end face and snap to the length we want. beam_end_off = max_pos[beam_ax] - min_pos[beam_ax] assert beam_end_off > 0, beam_end_off for plane in beam_end_face.planes: plane.x = beam_end_off new_beam = beam_template.copy(vmf_file=vmf) new_beam.localise(min_pos, rot) detail.solids.append(new_beam)
def make_barriers(vmf: VMF, coll: collisions.Collisions) -> None: """Make barrier entities. get_tex is vbsp.get_tex.""" glass_temp = template_brush.get_scaling_template( options.get(str, "glass_template")) grate_temp = template_brush.get_scaling_template( options.get(str, "grating_template")) barr_type: BarrierType | None # Avoid error without this package. if HOLES: # Grab the template solids we need. hole_combined_temp = template_brush.get_template( options.get(str, 'glass_hole_temp')) else: hole_combined_temp = None hole_temp_small = template_solids_and_coll(hole_combined_temp, 'small') hole_temp_lrg_diag = template_solids_and_coll(hole_combined_temp, 'large_diagonal') hole_temp_lrg_cutout = template_solids_and_coll(hole_combined_temp, 'large_cutout') hole_temp_lrg_square = template_solids_and_coll(hole_combined_temp, 'large_square') floorbeam_temp = options.get(str, 'glass_floorbeam_temp') if options.get_itemconf('BEE_PELLET:PelletGrating', False): # Merge together these existing filters in global_pti_ents vmf.create_ent( origin=options.get(Vec, 'global_pti_ents_loc'), targetname='@grating_filter', classname='filter_multi', filtertype=0, negated=0, filter01='@not_pellet', filter02='@not_paint_bomb', ) else: # Just skip paint bombs. vmf.create_ent( origin=options.get(Vec, 'global_pti_ents_loc'), targetname='@grating_filter', classname='filter_activator_class', negated=1, filterclass='prop_paint_bomb', ) # Group the positions by planes in each orientation. # This makes them 2D grids which we can optimise. # (normal_dist, positive_axis, type) -> Plane(type) slices: dict[tuple[tuple[float, float, float], bool], Plane[BarrierType | None]] = defaultdict(Plane) # We have this on the 32-grid to allow us to cut squares for holes. for (origin_tup, normal_tup), barr_type in BARRIERS.items(): origin = Vec(origin_tup) normal = Vec(normal_tup) norm_axis = normal.axis() u, v = origin.other_axes(norm_axis) norm_pos = Vec.with_axes(norm_axis, origin) slice_plane = slices[ norm_pos.as_tuple(), # distance from origin to this plane. normal[norm_axis] > 0, ] for u_off in [-48, -16, 16, 48]: for v_off in [-48, -16, 16, 48]: slice_plane[int((u + u_off) // 32), int((v + v_off) // 32), ] = barr_type # Compute contiguous sections of any barrier type, then place hint brushes to ensure sorting # is done correctly. for (plane_pos_tup, is_pos), pos_slice in slices.items(): plane_pos = Vec(plane_pos_tup) norm_axis = plane_pos.axis() normal = Vec.with_axes(norm_axis, 1 if is_pos else -1) u_axis, v_axis = Vec.INV_AXIS[norm_axis] is_present: Plane[object] = pos_slice.copy() for pos in is_present: is_present[pos] = True for min_u, min_v, max_u, max_v, _ in grid_optimise(is_present): # These are two points in the origin plane, at the borders. pos_min = Vec.with_axes( norm_axis, plane_pos, u_axis, min_u * 32, v_axis, min_v * 32, ) pos_max = Vec.with_axes( norm_axis, plane_pos, u_axis, max_u * 32 + 32, v_axis, max_v * 32 + 32, ) hint = vmf.make_prism( pos_min + normal * 64, pos_max + normal * 60, mat=consts.Tools.SKIP, ).solid for side in hint: if abs(Vec.dot(side.normal(), normal)) > 0.99: side.mat = consts.Tools.HINT vmf.add_brush(hint) # Remove pane sections where the holes are. We then generate those with # templates for slanted parts. for (origin_tup, norm_tup), hole_type in HOLES.items(): barr_type = BARRIERS[origin_tup, norm_tup] origin = Vec(origin_tup) normal = Vec(norm_tup) norm_axis = normal.axis() u, v = origin.other_axes(norm_axis) norm_pos = Vec.with_axes(norm_axis, origin) slice_plane = slices[norm_pos.as_tuple(), normal[norm_axis] > 0, ] offsets: tuple[int, ...] if hole_type is HoleType.LARGE: offsets = (-80, -48, -16, 16, 48, 80) else: offsets = (-16, 16) for u_off in offsets: for v_off in offsets: # Remove these squares, but keep them in the Plane, # so we can check if there was glass there. uv = ( int((u + u_off) // 32), int((v + v_off) // 32), ) if uv in slice_plane: slice_plane[uv] = None # These have to be present, except for the corners # on the large hole. elif abs(u_off) != 80 or abs(v_off) != 80: u_ax, v_ax = Vec.INV_AXIS[norm_axis] LOGGER.warning( 'Hole tried to remove missing tile at ({})?', Vec.with_axes(norm_axis, norm_pos, u_ax, u + u_off, v_ax, v + v_off), ) # Now generate the curved brushwork. if barr_type is BarrierType.GLASS: contents = collisions.CollideType.GLASS front_temp = glass_temp elif barr_type is BarrierType.GRATING: contents = collisions.CollideType.GRATING front_temp = grate_temp else: raise NotImplementedError angles = normal.to_angle() hole_temp: list[tuple[list[Solid], list[collisions.BBox], Matrix]] = [] # This is a tricky bit. Two large templates would collide # diagonally, and we allow the corner glass to not be present since # the hole doesn't actually use that 32x32 segment. # So we need to determine which of 3 templates to use. corn_angles = angles.copy() if hole_type is HoleType.LARGE: for corn_angles.roll in (0, 90, 180, 270): corn_mat = Matrix.from_angle(corn_angles) corn_dir = Vec(y=1, z=1) @ corn_angles hole_off = origin + 128 * corn_dir diag_type = HOLES.get( (hole_off.as_tuple(), normal.as_tuple()), None, ) corner_pos = origin + 80 * corn_dir corn_u, corn_v = corner_pos.other_axes(norm_axis) corn_u = int(corn_u // 32) corn_v = int(corn_v // 32) if diag_type is HoleType.LARGE: # There's another large template to this direction. # Just have 1 generate both combined, so the brushes can # be more optimal. To pick, arbitrarily make the upper one # be in charge. if corn_v > v // 32: hole_temp.append(hole_temp_lrg_diag + (corn_mat, )) continue # This bit of the glass is present, so include it in our brush, then clear. if (corn_u, corn_v) in slice_plane: hole_temp.append(hole_temp_lrg_square + (corn_mat, )) else: hole_temp.append(hole_temp_lrg_cutout + (corn_mat, )) else: hole_temp.append(hole_temp_small + (Matrix.from_angle(angles), )) for _, bbox_list, matrix in hole_temp: # Place the collisions. for bbox in bbox_list: bbox = bbox @ matrix + origin coll.add( bbox.with_attrs(name=barr_type.name, contents=contents)) def solid_pane_func(off1: float, off2: float, mat: str) -> list[Solid]: """Given the two thicknesses, produce the curved hole from the template.""" off_min = 64 - max(off1, off2) off_max = 64 - min(off1, off2) new_brushes = [] for brushes, _, matrix in hole_temp: for orig_brush in brushes: brush = orig_brush.copy(vmf_file=vmf) new_brushes.append(brush) for face in brush.sides: face.mat = mat for point in face.planes: if point.x > 64: point.x = off_max else: point.x = off_min face.localise(origin, matrix) # Increase precision, these are small detail brushes. face.lightmap = 8 return new_brushes make_glass_grating( vmf, origin, normal, barr_type, front_temp, solid_pane_func, ) for (plane_pos_tup, is_pos), pos_slice in slices.items(): plane_pos = Vec(plane_pos_tup) norm_axis = plane_pos.axis() normal = Vec.with_axes(norm_axis, 1 if is_pos else -1) u_axis, v_axis = Vec.INV_AXIS[norm_axis] for min_u, min_v, max_u, max_v, barr_type in grid_optimise(pos_slice): if barr_type is None: # Hole placed here and overwrote the glass/grating. continue elif barr_type is BarrierType.GLASS: contents = collisions.CollideType.GLASS front_temp = glass_temp elif barr_type is BarrierType.GRATING: contents = collisions.CollideType.GRATING front_temp = grate_temp else: raise NotImplementedError(barr_type) # These are two points in the origin plane, at the borders. pos_min = Vec.with_axes( norm_axis, plane_pos, u_axis, min_u * 32, v_axis, min_v * 32, ) pos_max = Vec.with_axes( norm_axis, plane_pos, u_axis, max_u * 32 + 32, v_axis, max_v * 32 + 32, ) coll.add( collisions.BBox( pos_min + normal * 64.0, pos_max + normal * 60.0, name=barr_type.name, contents=contents, )) def solid_pane_func(off1: float, off2: float, mat: str) -> list[Solid]: """Make the solid brush.""" return [ vmf.make_prism( pos_min + normal * (64.0 - off1), pos_max + normal * (64.0 - off2), mat=mat, ).solid ] make_glass_grating( vmf, (pos_min + pos_max) / 2 + 63 * normal, normal, barr_type, front_temp, solid_pane_func, ) if floorbeam_temp: LOGGER.info('Adding Glass floor beams...') add_glass_floorbeams(vmf, floorbeam_temp) LOGGER.info('Done!')
def res_piston_plat(vmf: VMF, inst: Entity, res: Property): """Generates piston platforms with optimized logic.""" ( template, visgroup_names, inst_filenames, automatic_var, color_var, source_ent, snd_start, snd_loop, snd_stop, ) = res.value # type: template_brush.Template, List[str], Dict[str, str], str, str, str, str, str, str 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 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) color_var = conditions.resolve_value(inst, color_var).casefold() if color_var == 'white': top_color = template_brush.MAT_TYPES.white elif color_var == 'black': top_color = template_brush.MAT_TYPES.black else: top_color = None for pist_ind in range(1, 5): 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, force_colour=top_color, force_grid='special', no_clumping=True, ) if not static_ent.solids: 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_unst_scaffold(vmf: VMF, res: Property): """The condition to generate Unstationary Scaffolds. This is executed once to modify all instances. """ # The instance types we're modifying if res.value not in SCAFFOLD_CONFIGS: # We've already executed this config group return RES_EXHAUSTED LOGGER.info( 'Running Scaffold Generator ({})...', res.value ) inst_to_config, LINKS = SCAFFOLD_CONFIGS[res.value] del SCAFFOLD_CONFIGS[res.value] # Don't let this run twice chains = item_chain.chain(vmf, inst_to_config.keys(), allow_loop=False) # We need to make the link entities unique for each scaffold set, # otherwise the AllVar property won't work. for group_counter, node_list in enumerate(chains): # Set all the instances and properties start_inst = node_list[0].item.inst for vals in LINKS.values(): if vals['all'] is not None: start_inst.fixup[vals['all']] = SCAFF_PATTERN.format( name=vals['name'], group=group_counter, index='*', ) should_reverse = srctools.conv_bool(start_inst.fixup['$start_reversed']) # Stash this off to start, so we can find this after items are processed # and the instance names change. for node in node_list: node.conf = inst_to_config[node.inst['file'].casefold()] # Now set each instance in the chain, including first and last for index, node in enumerate(node_list): conf = node.conf orient, offset = get_config(node) if node.prev is None: link_type = LinkType.START elif node.next is None: link_type = LinkType.END else: link_type = LinkType.MID # Special case - add an extra instance for the ends, pointing # in the direction # of the connected track. This would be the endcap # model. placed_endcap = False if ( orient == 'floor' and link_type is not LinkType.MID and conf['inst_end'] is not None ): if link_type is LinkType.START: other_node = node.next else: other_node = node.prev other_offset = get_config(other_node)[1] link_dir = other_offset - offset # Compute the horizontal gradient (z / xy dist). # Don't use endcap if rising more than ~45 degrees, or lowering # more than ~12 degrees. # If horiz_dist = math.sqrt(link_dir.x ** 2 + link_dir.y ** 2) if horiz_dist != 0 and -0.15 <= (link_dir.z / horiz_dist) <= 1: link_ang = math.degrees( math.atan2(link_dir.y, link_dir.x) ) # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals link_ang = (link_ang + 45) // 90 * 90 vbsp.VMF.create_ent( classname='func_instance', targetname=node.inst['targetname'], file=conf['inst_end'], origin=offset, angles='0 {:.0f} 0'.format(link_ang), ) # Don't place the offset instance, this replaces that! placed_endcap = True if not placed_endcap and conf['inst_offset'] is not None: # Add an additional rotated entity at the offset. # This is useful for the piston item. vbsp.VMF.create_ent( classname='func_instance', targetname=node.inst['targetname'], file=conf['inst_offset'], origin=offset, angles=node.inst['angles'], ) logic_inst = vmf.create_ent( classname='func_instance', targetname=node.inst['targetname'], file=conf.get( 'logic_' + link_type.value + ( '_rev' if should_reverse else '' ), '', ), origin=offset, angles=( '0 0 0' if conf['rotate_logic'] else node.inst['angles'] ), ) for key, val in node.inst.fixup.items(): # Copy over fixup values logic_inst.fixup[key] = val # Add the link-values for linkVar, link in LINKS.items(): logic_inst.fixup[linkVar] = SCAFF_PATTERN.format( name=link['name'], group=group_counter, index=index, ) if node.next is not None: logic_inst.fixup[link['next']] = SCAFF_PATTERN.format( name=link['name'], group=group_counter, index=index + 1, ) new_file = conf.get('inst_' + orient, '') if new_file != '': node.inst['file'] = new_file LOGGER.info('Finished Scaffold generation!') 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=vbsp_options.get(Vec, 'global_pti_ents_loc'), ) diss_trig.solids = [vmf.make_prism( Vec(-8 * 128, -8 * 128, -4182), Vec(20 * 128, 20 * 128, -4864), mat='tools/toolstrigger', ).solid] diss_trig.add_out( Output('OnStartTouch', '!activator', 'SilentDissolve'), Output('OnStartTouch', '!activator', 'Break', delay=0.1), Output('OnStartTouch', '!activator', 'Kill', delay=0.5), ) # Since we can chuck gel down the pit, cover it in a noportal_volume # to stop players from portalling past the hurt trigger. diss_trig = vmf.create_ent( classname='func_noportal_volume', origin=vbsp_options.get(Vec, 'global_pti_ents_loc'), ) diss_trig.solids = [vmf.make_prism( Vec(-8 * 128, -8 * 128, -64), Vec(20 * 128, 20 * 128, -4864), mat='tools/toolstrigger', ).solid]
def make_glass_grating( vmf: VMF, ent_pos: Vec, normal: Vec, barr_type: BarrierType, front_temp: template_brush.ScalingTemplate, front_mat: str, solid_func: Callable[[float, float, str], List[Solid]], ): """Make all the brushes needed for glass/grating. solid_func() is called with two offsets from the voxel edge, and returns a matching list of solids. This allows doing holes and normal panes with the same function. """ if barr_type is BarrierType.GLASS: main_ent = vmf.create_ent('func_detail') player_clip_mat = consts.Tools.PLAYER_CLIP_GLASS else: player_clip_mat = consts.Tools.PLAYER_CLIP_GRATE main_ent = vmf.create_ent( 'func_brush', renderfx=14, # Constant Glow solidity=1, # Never solid ) # The actual glass/grating brush - 0.5-1 units back from the surface. main_ent.solids = solid_func(0.5, 1, consts.Tools.NODRAW) abs_norm = abs(normal) for face in main_ent.sides(): f_normal = face.normal() if abs(f_normal) == abs_norm: face.mat = front_mat front_temp.apply(face, change_mat=False) if normal.z == 0: # If vertical, we don't care about footsteps. # So just use 'normal' clips. player_clip = vmf.create_ent('func_detail') player_clip_mat = consts.Tools.PLAYER_CLIP else: # This needs to be a func_brush, otherwise the clip texture data # will be merged with other clips. player_clip = vmf.create_ent( 'func_brush', solidbsp=1, origin=ent_pos, ) # We also need a func_detail clip, which functions on portals. # Make it thinner, so it doesn't impact footsteps. player_thin_clip = vmf.create_ent('func_detail') player_thin_clip.solids = solid_func(0.5, 3.5, consts.Tools.PLAYER_CLIP) player_clip.solids = solid_func(0, 4, player_clip_mat) if barr_type is BarrierType.GRATING: # Add the VPhysics clip. phys_clip = vmf.create_ent( 'func_clip_vphysics', filtername='@grating_filter', origin=ent_pos, StartDisabled=0, ) phys_clip.solids = solid_func(0, 2, consts.Tools.TRIGGER)
def gen_flinch_trigs(self, vmf: VMF, name: str, start_disabled: str) -> None: """For deadly fizzlers optionally make them safer. This adds logic to force players back instead when walking into the field. Only applies to vertical triggers. """ normal = abs(self.normal()) # type: Vec # Horizontal fizzlers would just have you fall through. if normal.z: return # Disabled. if not vbsp_options.get_itemconf( ('VALVE_FIZZLER', 'FlinchBack'), False): return # Make global entities if not present. if '_fizz_flinch_hurt' not in vmf.by_target: glob_ent_loc = vbsp_options.get(Vec, 'global_ents_loc') vmf.create_ent( classname='point_hurt', targetname='_fizz_flinch_hurt', Damage=10, # Just for visuals and sounds. # BURN | ENERGYBEAM | PREVENT_PHYSICS_FORCE DamageType=8 | 1024 | 2048, DamageTarget='!activator', # Hurt the triggering player. DamageRadius=1, # Target makes this unused. origin=glob_ent_loc, ) # We need two catapults - one for each side. neg_brush = vmf.create_ent( targetname=name, classname='trigger_catapult', spawnflags=1, # Players only. origin=self.base_inst['origin'], physicsSpeed=0, playerSpeed=96, launchDirection=(-normal).to_angle(), startDisabled=start_disabled, ) neg_brush.add_out(Output('OnCatapulted', '_fizz_flinch_hurt', 'Hurt')) pos_brush = neg_brush.copy() pos_brush['launchDirection'] = normal.to_angle() vmf.add_ent(pos_brush) for seg_min, seg_max in self.emitters: neg_brush.solids.append( vmf.make_prism( p1=(seg_min - 4 * normal - 64 * self.up_axis), p2=seg_max + 64 * self.up_axis, mat=const.Tools.TRIGGER, ).solid) pos_brush.solids.append( vmf.make_prism( p1=seg_min - 64 * self.up_axis, p2=(seg_max + 4 * normal + 64 * self.up_axis), mat=const.Tools.TRIGGER, ).solid)
def res_make_tag_fizzler(vmf: VMF, inst: Entity, res: Property): """Add an Aperture Tag Paint Gun activation fizzler. These fizzlers are created via signs, and work very specially. MUST be priority -100 so it runs before fizzlers! """ import vbsp if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']: # Abort - TAG fizzlers shouldn't appear in any other game! inst.remove() return fizzler = None # Look for the fizzler instance we want to replace for targetname in inst.output_targets(): try: fizzler = FIZZLERS[targetname] except KeyError: # Not a fizzler. # It's an indicator toggle, remove it and the antline to clean up. for ent in vmf.by_target[targetname]: remove_ant_toggle(ent) inst.outputs.clear() # Remove the outputs now, they're not valid anyway. if fizzler is None: # No fizzler - remove this sign inst.remove() return if fizzler.fizz_type.id == 'TAG_FIZZ_ID': LOGGER.warning('Two tag signs attached to one fizzler...') inst.remove() return # Swap to the special Tag Fizzler type. fizzler.fizz_type = FIZZ_TYPES[TAG_FIZZ_ID] # The distance from origin the double signs are seperated by. sign_offset = res.int('signoffset', 16) sign_loc = ( # The actual location of the sign - on the wall Vec.from_str(inst['origin']) + Vec(0, 0, -64).rotate_by_str(inst['angles']) ) # Now deal with the visual aspect: # Blue signs should be on top. blue_enabled = inst.fixup.bool('$start_enabled') oran_enabled = inst.fixup.bool('$start_reversed') # If True, single-color signs will also turn off the other color. # This also means we always show both signs. # If both are enabled or disabled, this has no effect. disable_other = ( not inst.fixup.bool('$disable_autorespawn', True) and blue_enabled != oran_enabled ) # Delete fixups now, they aren't useful. inst.fixup.clear() if not blue_enabled and not oran_enabled: # Hide the sign in this case! inst.remove() inst_angle = srctools.parse_vec_str(inst['angles']) inst_normal = Vec(0, 0, 1).rotate(*inst_angle) loc = Vec.from_str(inst['origin']) if disable_other or (blue_enabled and oran_enabled): inst['file'] = res['frame_double'] # On a wall, and pointing vertically if inst_normal.z == 0 and Vec(y=1).rotate(*inst_angle).z: # They're vertical, make sure blue's on top! blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset) oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset) # If orange is enabled, with two frames put that on top # instead since it's more important if disable_other and oran_enabled: blue_loc, oran_loc = oran_loc, blue_loc else: offset = Vec(0, sign_offset, 0).rotate(*inst_angle) blue_loc = loc + offset oran_loc = loc - offset else: inst['file'] = res['frame_single'] # They're always centered blue_loc = loc oran_loc = loc if inst_normal.z != 0: # If on floors/ceilings, rotate to point at the fizzler! sign_floor_loc = sign_loc.copy() sign_floor_loc.z = 0 # We don't care about z-positions. # Grab the data saved earlier in res_find_potential_tag_fizzlers() axis, side_min, side_max, normal = calc_fizzler_orient(fizzler) # The Z-axis fizzler (horizontal) must be treated differently. if axis == 'z': # For z-axis, just compare to the center point. # The values are really x, y, z, not what they're named. sign_dir = sign_floor_loc - (side_min, side_max, normal) else: # For the other two, we compare to the line, # or compare to the closest side (in line with the fizz) other_axis = 'x' if axis == 'y' else 'y' if abs(sign_floor_loc[other_axis] - normal) < 32: # Compare to the closest side. Use ** to swap x/y arguments # appropriately. The closest side is the one with the # smallest magnitude. sign_dir = min( sign_floor_loc - Vec.with_axes( axis,side_min, other_axis, normal, ), sign_floor_loc - Vec.with_axes( axis, side_max, other_axis, normal, ), key=Vec.mag, ) else: # Align just based on whether we're in front or behind. sign_dir = Vec() sign_dir[other_axis] = sign_floor_loc[other_axis] - normal sign_angle = math.degrees( math.atan2(sign_dir.y, sign_dir.x) ) # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals sign_angle = (sign_angle + 45) // 90 * 90 # Rotate to fit the instances - south is down sign_angle = int(sign_angle + 90) % 360 if inst_normal.z > 0: sign_angle = '0 {} 0'.format(sign_angle) elif inst_normal.z < 0: # Flip upside-down for ceilings sign_angle = '0 {} 180'.format(sign_angle) else: # On a wall, face upright sign_angle = PETI_INST_ANGLE[inst_normal.as_tuple()] # If disable_other, we show off signs. Otherwise we don't use that sign. blue_sign = 'blue_sign' if blue_enabled else 'blue_off_sign' if disable_other else None oran_sign = 'oran_sign' if oran_enabled else 'oran_off_sign' if disable_other else None if blue_sign: vmf.create_ent( classname='func_instance', file=res[blue_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=blue_loc.join(' '), ) if oran_sign: vmf.create_ent( classname='func_instance', file=res[oran_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=oran_loc.join(' '), ) # Now modify the fizzler... # Subtract the sign from the list of connections, but don't go below # zero fizzler.base_inst.fixup['$connectioncount'] = str(max( 0, srctools.conv_int(fizzler.base_inst.fixup['$connectioncount', ''], 0) - 1 )) # Find the direction the fizzler normal is. # Signs will associate with the given side! bbox_min, bbox_max = fizzler.emitters[0] fizz_field_axis = (bbox_max-bbox_min).norm() fizz_norm_axis = fizzler.normal().axis() sign_center = (bbox_min[fizz_norm_axis] + bbox_max[fizz_norm_axis]) / 2 # Figure out what the sides will set values to... pos_blue = False pos_oran = False neg_blue = False neg_oran = False if sign_loc[fizz_norm_axis] < sign_center: pos_blue = blue_enabled pos_oran = oran_enabled else: neg_blue = blue_enabled neg_oran = oran_enabled # If it activates the paint gun, use different textures fizzler.tag_on_pos = pos_blue or pos_oran fizzler.tag_on_neg = neg_blue or neg_oran # Now make the trigger ents. We special-case these since they need to swap # depending on the sign config and position. if vbsp.GAME_MODE == 'COOP': # We need ATLAS-specific triggers pos_trig = vmf.create_ent( classname='trigger_playerteam', ) neg_trig = vmf.create_ent( classname='trigger_playerteam', ) output = 'OnStartTouchBluePlayer' else: pos_trig = vmf.create_ent( classname='trigger_multiple', ) neg_trig = vmf.create_ent( classname='trigger_multiple', spawnflags='1', ) output = 'OnStartTouch' pos_trig['origin'] = neg_trig['origin'] = fizzler.base_inst['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = local_name(fizzler.base_inst, 'trig_pos') neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_neg') pos_trig.outputs = [ Output(output, neg_trig, 'Enable'), Output(output, pos_trig, 'Disable'), ] neg_trig.outputs = [ Output(output, pos_trig, 'Enable'), Output(output, neg_trig, 'Disable'), ] voice_attr = vbsp.settings['has_attr'] if blue_enabled or disable_other: # If this is blue/oran only, don't affect the other color neg_trig.outputs.append(Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_blue), )) pos_trig.outputs.append(Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_blue), )) if blue_enabled: # Add voice attributes - we have the gun and gel! voice_attr['bluegelgun'] = True voice_attr['bluegel'] = True voice_attr['bouncegun'] = True voice_attr['bouncegel'] = True if oran_enabled or disable_other: neg_trig.outputs.append(Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_oran), )) pos_trig.outputs.append(Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_oran), )) if oran_enabled: voice_attr['orangegelgun'] = True voice_attr['orangegel'] = True voice_attr['speedgelgun'] = True voice_attr['speedgel'] = True if not oran_enabled and not blue_enabled: # If both are disabled, we must shutdown the gun when touching # either side - use neg_trig for that purpose! # We want to get rid of pos_trig to save ents vmf.remove_ent(pos_trig) neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_off') neg_trig.outputs.clear() neg_trig.add_out(Output( output, '@BlueIsEnabled', 'SetValue', param='0' )) neg_trig.add_out(Output( output, '@OrangeIsEnabled', 'SetValue', param='0' )) # Make the triggers. for bbox_min, bbox_max in fizzler.emitters: bbox_min = bbox_min.copy() - 64 * fizzler.up_axis bbox_max = bbox_max.copy() + 64 * fizzler.up_axis # The triggers are 8 units thick, with a 32-unit gap in the middle neg_min, neg_max = Vec(bbox_min), Vec(bbox_max) neg_min[fizz_norm_axis] -= 24 neg_max[fizz_norm_axis] -= 16 pos_min, pos_max = Vec(bbox_min), Vec(bbox_max) pos_min[fizz_norm_axis] += 16 pos_max[fizz_norm_axis] += 24 if blue_enabled or oran_enabled: neg_trig.solids.append( vmf.make_prism( neg_min, neg_max, mat='tools/toolstrigger', ).solid, ) pos_trig.solids.append( vmf.make_prism( pos_min, pos_max, mat='tools/toolstrigger', ).solid, ) else: # If neither enabled, use one trigger neg_trig.solids.append( vmf.make_prism( neg_min, pos_max, mat='tools/toolstrigger', ).solid, )
def res_make_catwalk(vmf: VMF, res: Property): """Speciallised result to generate catwalks from markers. Only runs once, and then quits the condition list. * Instances: * `markerInst: The instance set in editoritems. * `straight_128`/`256`/`512`: Straight sections. Extends East. * `corner: An L-corner piece. Connects on North and West sides. * `TJunction`: A T-piece. Connects on all but the East side. * `crossJunction`: A X-piece. Connects on all sides. * `end`: An end piece. Connects on the East side. * `stair`: A stair. Starts East and goes Up and West. * `end_wall`: Connects a West wall to a East catwalk. * `support_wall`: A support extending from the East wall. * `support_ceil`: A support extending from the ceiling. * `support_floor`: A support extending from the floor. * `support_goo`: A floor support, designed for goo pits. * `single_wall`: A section connecting to an East wall. """ LOGGER.info("Starting catwalk generator...") marker = instanceLocs.resolve(res['markerInst']) instances = { name: instanceLocs.resolve_one(res[name, ''], error=True) for name in ( 'straight_128', 'straight_256', 'straight_512', 'corner', 'tjunction', 'crossjunction', 'end', 'stair', 'end_wall', 'support_wall', 'support_ceil', 'support_floor', 'support_goo', 'single_wall', 'markerInst', ) } # If there are no attachments remove a catwalk piece instances['NONE'] = '' if instances['end_wall'] == '': instances['end_wall'] = instances['end'] # The directions this instance is connected by (NSEW) links = {} # type: Dict[Entity, Link] markers = {} # Find all our markers, so we can look them up by targetname. for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in marker: continue links[inst] = Link() markers[inst['targetname']] = inst # Snap the markers to the grid. If on glass it can become offset... origin = Vec.from_str(inst['origin']) origin = origin // 128 * 128 origin += 64 while brushLoc.POS['world': origin].is_goo: # The instance is in goo! Switch to floor orientation, and move # up until it's in air. inst['angles'] = '0 0 0' origin.z += 128 inst['origin'] = str(origin) if not markers: return RES_EXHAUSTED LOGGER.info('Connections: {}', links) LOGGER.info('Markers: {}', markers) # First loop through all the markers, adding connecting sections for marker_name, inst in markers.items(): mark_item = ITEMS[marker_name] mark_item.delete_antlines() for conn in list(mark_item.outputs): try: inst2 = markers[conn.to_item.name] except KeyError: LOGGER.warning('Catwalk connected to non-catwalk!') conn.remove() origin1 = Vec.from_str(inst['origin']) origin2 = Vec.from_str(inst2['origin']) if origin1.x != origin2.x and origin1.y != origin2.y: LOGGER.warning('Instances not aligned!') continue y_dir = origin1.x == origin2.x # Which way the connection is if y_dir: dist = abs(origin1.y - origin2.y) else: dist = abs(origin1.x - origin2.x) vert_dist = origin1.z - origin2.z if (dist - 128) // 2 < abs(vert_dist): # The stairs are 2 long, 1 high. Check there's enough room # Subtract the last block though, since that's a corner. LOGGER.warning('Not enough room for stairs!') continue if dist > 128: # add straight sections in between place_catwalk_connections(vmf, instances, origin1, origin2) # Update the lists based on the directions that were set conn_lst1 = links[inst] conn_lst2 = links[inst2] if origin1.x < origin2.x: conn_lst1.E = conn_lst2.W = True elif origin2.x < origin1.x: conn_lst1.W = conn_lst2.E = True if origin1.y < origin2.y: conn_lst1.N = conn_lst2.S = True elif origin2.y < origin1.y: conn_lst1.S = conn_lst2.N = True for inst, dir_mask in links.items(): # Set the marker instances based on the attached walkways. normal = Vec(0, 0, 1).rotate_by_str(inst['angles']) new_type, inst['angles'] = utils.CONN_LOOKUP[dir_mask.as_tuple()] inst['file'] = instances[CATWALK_TYPES[new_type]] if new_type is utils.CONN_TYPES.side: # If the end piece is pointing at a wall, switch the instance. if normal.z == 0: if normal == dir_mask.conn_dir(): inst['file'] = instances['end_wall'] continue # We never have normal supports on end pieces elif new_type is utils.CONN_TYPES.none: # Unconnected catwalks on the wall switch to a special instance. # This lets players stand next to a portal surface on the wall. if normal.z == 0: inst['file'] = instances['single_wall'] inst['angles'] = INST_ANGLE[normal.as_tuple()] else: inst.remove() continue # These don't get supports otherwise # Add regular supports supp = None if normal == (0, 0, 1): # If in goo, use different supports! origin = Vec.from_str(inst['origin']) origin.z -= 128 if brushLoc.POS['world': origin].is_goo: supp = instances['support_goo'] else: supp = instances['support_floor'] elif normal == (0, 0, -1): supp = instances['support_ceil'] else: supp = instances['support_wall'] if supp: vmf.create_ent( classname='func_instance', origin=inst['origin'], angles=INST_ANGLE[normal.as_tuple()], file=supp, ) LOGGER.info('Finished catwalk generation!') return RES_EXHAUSTED
def make_bottomless_pit(vmf: VMF, max_height): """Generate bottomless pits.""" 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 = vbsp_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 tele_trig is not None: vbsp.IGNORED_BRUSH_ENTS.add(tele_trig) if hurt_trig is not None: vbsp.IGNORED_BRUSH_ENTS.add(hurt_trig) 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 generate_fizzlers(vmf: VMF): """Generates fizzler models and the brushes according to their set types. After this is done, fizzler-related conditions will not function correctly. However the model instances are now available for modification. """ from vbsp import MAP_RAND_SEED for fizz in FIZZLERS.values(): if fizz.base_inst not in vmf.entities: continue # The fizzler was removed from the map. fizz_name = fizz.base_inst['targetname'] fizz_type = fizz.fizz_type # Static versions are only used for fizzlers which start on. # Permanently-off fizzlers are kinda useless, so we don't need # to bother optimising for it. is_static = bool( fizz.base_inst.fixup.int('$connectioncount', 0) == 0 and fizz.base_inst.fixup.bool('$start_enabled', 1) ) pack_list = ( fizz.fizz_type.pack_lists_static if is_static else fizz.fizz_type.pack_lists ) for pack in pack_list: packing.pack_list(vmf, pack) if fizz_type.inst[FizzInst.BASE, is_static]: random.seed('{}_fizz_base_{}'.format(MAP_RAND_SEED, fizz_name)) fizz.base_inst['file'] = random.choice(fizz_type.inst[FizzInst.BASE, is_static]) if not fizz.emitters: LOGGER.warning('No emitters for fizzler "{}"!', fizz_name) continue # Brush index -> entity for ones that need to merge. # template_brush is used for the templated one. single_brushes = {} # type: Dict[FizzlerBrush, Entity] if fizz_type.temp_max or fizz_type.temp_min: template_brush_ent = vmf.create_ent( classname='func_brush', origin=fizz.base_inst['origin'], ) conditions.set_ent_keys( template_brush_ent, fizz.base_inst, fizz_type.temp_brush_keys, ) else: template_brush_ent = None up_dir = fizz.up_axis forward = (fizz.emitters[0][1] - fizz.emitters[0][0]).norm() min_angles = FIZZ_ANGLES[forward.as_tuple(), up_dir.as_tuple()] max_angles = FIZZ_ANGLES[(-forward).as_tuple(), up_dir.as_tuple()] model_min = ( fizz_type.inst[FizzInst.PAIR_MIN, is_static] or fizz_type.inst[FizzInst.ALL, is_static] ) model_max = ( fizz_type.inst[FizzInst.PAIR_MAX, is_static] or fizz_type.inst[FizzInst.ALL, is_static] ) if not model_min or not model_max: raise ValueError( 'No model specified for one side of "{}"' ' fizzlers'.format(fizz_type.id), ) # Define a function to do the model names. model_index = 0 if fizz_type.model_naming is ModelName.SAME: def get_model_name(ind): """Give every emitter the base's name.""" return fizz_name elif fizz_type.model_naming is ModelName.LOCAL: def get_model_name(ind): """Give every emitter a name local to the base.""" return fizz_name + '-' + fizz_type.model_name elif fizz_type.model_naming is ModelName.PAIRED: def get_model_name(ind): """Give each pair of emitters the same unique name.""" return '{}-{}{:02}'.format( fizz_name, fizz_type.model_name, ind, ) elif fizz_type.model_naming is ModelName.UNIQUE: def get_model_name(ind): """Give every model a unique name.""" nonlocal model_index model_index += 1 return '{}-{}{:02}'.format( fizz_name, fizz_type.model_name, model_index, ) else: raise ValueError('Bad ModelName?') # Generate env_beam pairs. for beam in fizz_type.beams: beam_template = Entity(vmf) conditions.set_ent_keys(beam_template, fizz.base_inst, beam.keys) beam_template['classname'] = 'env_beam' del beam_template['LightningEnd'] # Don't allow users to set end pos. name = beam_template['targetname'] + '_' counter = 1 for seg_min, seg_max in fizz.emitters: for offset in beam.offset: # type: Vec min_off = offset.copy() max_off = offset.copy() min_off.localise(seg_min, min_angles) max_off.localise(seg_max, max_angles) beam_ent = beam_template.copy() vmf.add_ent(beam_ent) # Allow randomising speed and direction. if 0 < beam.speed_min < beam.speed_max: random.seed('{}{}{}'.format(MAP_RAND_SEED, min_off, max_off)) beam_ent['TextureScroll'] = random.randint(beam.speed_min, beam.speed_max) if random.choice((False, True)): # Flip to reverse direction. min_off, max_off = max_off, min_off beam_ent['origin'] = min_off beam_ent['LightningStart'] = beam_ent['targetname'] = ( name + str(counter) ) counter += 1 beam_ent['targetpoint'] = max_off # Prepare to copy over instance traits for the emitters. fizz_traits = instance_traits.get(fizz.base_inst).copy() # Special case, mark emitters that have a custom position for Clean # models. if fizz.has_cust_position: fizz_traits.add('cust_shape') mat_mod_tex = {} # type: Dict[FizzlerBrush, Set[str]] for brush_type in fizz_type.brushes: if brush_type.mat_mod_var is not None: mat_mod_tex[brush_type] = set() # Record the data for trigger hurts so flinch triggers can match them. trigger_hurt_name = '' trigger_hurt_start_disabled = '0' for seg_ind, (seg_min, seg_max) in enumerate(fizz.emitters, start=1): length = (seg_max - seg_min).mag() random.seed('{}_fizz_{}'.format(MAP_RAND_SEED, seg_min)) if length == 128 and fizz_type.inst[FizzInst.PAIR_SINGLE, is_static]: min_inst = vmf.create_ent( targetname=get_model_name(seg_ind), classname='func_instance', file=random.choice(fizz_type.inst[FizzInst.PAIR_SINGLE, is_static]), origin=(seg_min + seg_max)/2, angles=min_angles, ) else: # Both side models. min_inst = vmf.create_ent( targetname=get_model_name(seg_ind), classname='func_instance', file=random.choice(model_min), origin=seg_min, angles=min_angles, ) random.seed('{}_fizz_{}'.format(MAP_RAND_SEED, seg_max)) max_inst = vmf.create_ent( targetname=get_model_name(seg_ind), classname='func_instance', file=random.choice(model_max), origin=seg_max, angles=max_angles, ) max_inst.fixup.update(fizz.base_inst.fixup) instance_traits.get(max_inst).update(fizz_traits) min_inst.fixup.update(fizz.base_inst.fixup) instance_traits.get(min_inst).update(fizz_traits) if fizz_type.inst[FizzInst.GRID, is_static]: # Generate one instance for each position. # Go 64 from each side, and always have at least 1 section # A 128 gap will have length = 0 for ind, dist in enumerate(range(64, round(length) - 63, 128)): mid_pos = seg_min + forward * dist random.seed('{}_fizz_mid_{}'.format(MAP_RAND_SEED, mid_pos)) mid_inst = vmf.create_ent( classname='func_instance', targetname=fizz_name, angles=min_angles, file=random.choice(fizz_type.inst[FizzInst.GRID, is_static]), origin=mid_pos, ) mid_inst.fixup.update(fizz.base_inst.fixup) instance_traits.get(mid_inst).update(fizz_traits) if template_brush_ent is not None: if length == 128 and fizz_type.temp_single: temp = template_brush.import_template( fizz_type.temp_single, (seg_min + seg_max) / 2, min_angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) template_brush_ent.solids.extend(temp.world) else: if fizz_type.temp_min: temp = template_brush.import_template( fizz_type.temp_min, seg_min, min_angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) template_brush_ent.solids.extend(temp.world) if fizz_type.temp_max: temp = template_brush.import_template( fizz_type.temp_max, seg_max, max_angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) template_brush_ent.solids.extend(temp.world) # Generate the brushes. for brush_type in fizz_type.brushes: brush_ent = None # If singular, we reuse the same brush ent for all the segments. if brush_type.singular: brush_ent = single_brushes.get(brush_type, None) # Non-singular or not generated yet - make the entity. if brush_ent is None: brush_ent = vmf.create_ent(classname='func_brush') for key_name, key_value in brush_type.keys.items(): brush_ent[key_name] = conditions.resolve_value(fizz.base_inst, key_value) for key_name, key_value in brush_type.local_keys.items(): brush_ent[key_name] = conditions.local_name( fizz.base_inst, conditions.resolve_value( fizz.base_inst, key_value, ) ) brush_ent['targetname'] = conditions.local_name( fizz.base_inst, brush_type.name, ) # Set this to the center, to make sure it's not going to leak. brush_ent['origin'] = (seg_min + seg_max)/2 # For fizzlers flat on the floor/ceiling, scanlines look # useless. Turn them off. if 'usescanline' in brush_ent and fizz.normal().z: brush_ent['UseScanline'] = 0 if brush_ent['classname'] == 'trigger_hurt': trigger_hurt_name = brush_ent['targetname'] trigger_hurt_start_disabled = brush_ent['startdisabled'] if brush_type.set_axis_var: brush_ent['vscript_init_code'] = ( 'axis <- `{}`;'.format( fizz.normal().axis(), ) ) for out in brush_type.outputs: new_out = out.copy() new_out.target = conditions.local_name( fizz.base_inst, new_out.target, ) brush_ent.add_out(new_out) if brush_type.singular: # Record for the next iteration. single_brushes[brush_type] = brush_ent # If we have a material_modify_control to generate, # we need to parent it to ourselves to restrict it to us # only. We also need one for each material, so provide a # function to the generator which adds to a set. if brush_type.mat_mod_var is not None: used_tex_func = mat_mod_tex[brush_type].add else: def used_tex_func(val): """If not, ignore those calls.""" return None # Generate the brushes and texture them. brush_ent.solids.extend( brush_type.generate( vmf, fizz, seg_min, seg_max, used_tex_func, ) ) if trigger_hurt_name: fizz.gen_flinch_trigs( vmf, trigger_hurt_name, trigger_hurt_start_disabled, ) # If we have the config, but no templates used anywhere... if template_brush_ent is not None and not template_brush_ent.solids: template_brush_ent.remove() for brush_type, used_tex in mat_mod_tex.items(): brush_name = conditions.local_name(fizz.base_inst, brush_type.name) mat_mod_name = conditions.local_name(fizz.base_inst, brush_type.mat_mod_name) for off, tex in zip(MATMOD_OFFSETS, sorted(used_tex)): pos = off.copy().rotate(*min_angles) pos += Vec.from_str(fizz.base_inst['origin']) vmf.create_ent( classname='material_modify_control', origin=pos, targetname=mat_mod_name, materialName='materials/' + tex + '.vmt', materialVar=brush_type.mat_mod_var, parentname=brush_name, )
def place_catwalk_connections(vmf: VMF, instances, point_a: Vec, point_b: Vec): """Place catwalk sections to connect two straight points.""" diff = point_b - point_a # The horizontal unit vector in the direction we are placing catwalks direction = diff.copy() direction.z = 0 distance = direction.len() - 128 direction = direction.norm() if diff.z > 0: angle = INST_ANGLE[direction.as_tuple()] # We need to add stairs for stair_pos in range(0, int(diff.z), 128): # Move twice the vertical horizontally # plus 128 so we don't start in point A loc = point_a + (2 * stair_pos + 128) * direction # Do the vertical offset loc.z += stair_pos vmf.create_ent( classname='func_instance', origin=loc.join(' '), angles=angle, file=instances['stair'], ) # This is the location we start flat sections at point_a = loc + 128 * direction point_a.z += 128 elif diff.z < 0: # We need to add downward stairs # They point opposite to normal ones LOGGER.debug('down from {}', point_a) angle = INST_ANGLE[(-direction).as_tuple()] for stair_pos in range(0, -int(diff.z), 128): LOGGER.debug(stair_pos) # Move twice the vertical horizontally loc = point_a + (2 * stair_pos + 256) * direction # type: Vec # Do the vertical offset plus additional 128 units # to account for the moved instance loc.z -= (stair_pos + 128) vmf.create_ent( classname='func_instance', origin=loc.join(' '), angles=angle, file=instances['stair'], ) # Adjust point A to be at the end of the catwalks point_a = loc # Remove the space the stairs take up from the horiz distance distance -= abs(diff.z) * 2 # Now do straight sections LOGGER.debug('Stretching {} {}', distance, direction) angle = INST_ANGLE[direction.as_tuple()] loc = point_a + (direction * 128) # Figure out the most efficent number of sections for segment_len in utils.fit( distance, [512, 256, 128] ): vmf.create_ent( classname='func_instance', origin=loc.join(' '), angles=angle, file=instances['straight_' + str(segment_len)], ) loc += (segment_len * direction)
def res_make_catwalk(vmf: VMF, res: Property): """Speciallised result to generate catwalks from markers. Only runs once, and then quits the condition list. * Instances: * `markerInst: The instance set in editoritems. * `straight_128`/`256`/`512`: Straight sections. Extends East. * `corner: An L-corner piece. Connects on North and West sides. * `TJunction`: A T-piece. Connects on all but the East side. * `crossJunction`: A X-piece. Connects on all sides. * `end`: An end piece. Connects on the East side. * `stair`: A stair. Starts East and goes Up and West. * `end_wall`: Connects a West wall to a East catwalk. * `support_wall`: A support extending from the East wall. * `support_ceil`: A support extending from the ceiling. * `support_floor`: A support extending from the floor. * `support_goo`: A floor support, designed for goo pits. * `single_wall`: A section connecting to an East wall. """ LOGGER.info("Starting catwalk generator...") marker = instanceLocs.resolve(res['markerInst']) instances = { name: instanceLocs.resolve_one(res[name, ''], error=True) for name in ( 'straight_128', 'straight_256', 'straight_512', 'corner', 'tjunction', 'crossjunction', 'end', 'stair', 'end_wall', 'support_wall', 'support_ceil', 'support_floor', 'support_goo', 'single_wall', 'markerInst', ) } # If there are no attachments remove a catwalk piece instances['NONE'] = '' if instances['end_wall'] == '': instances['end_wall'] = instances['end'] # The directions this instance is connected by (NSEW) links = {} # type: Dict[Entity, Link] markers = {} # Find all our markers, so we can look them up by targetname. for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in marker: continue links[inst] = Link() markers[inst['targetname']] = inst # Snap the markers to the grid. If on glass it can become offset... origin = Vec.from_str(inst['origin']) origin = origin // 128 * 128 origin += 64 while brushLoc.POS['world':origin].is_goo: # The instance is in goo! Switch to floor orientation, and move # up until it's in air. inst['angles'] = '0 0 0' origin.z += 128 inst['origin'] = str(origin) if not markers: return RES_EXHAUSTED LOGGER.info('Connections: {}', links) LOGGER.info('Markers: {}', markers) # First loop through all the markers, adding connecting sections for marker_name, inst in markers.items(): mark_item = ITEMS[marker_name] mark_item.delete_antlines() for conn in list(mark_item.outputs): try: inst2 = markers[conn.to_item.name] except KeyError: LOGGER.warning('Catwalk connected to non-catwalk!') conn.remove() origin1 = Vec.from_str(inst['origin']) origin2 = Vec.from_str(inst2['origin']) if origin1.x != origin2.x and origin1.y != origin2.y: LOGGER.warning('Instances not aligned!') continue y_dir = origin1.x == origin2.x # Which way the connection is if y_dir: dist = abs(origin1.y - origin2.y) else: dist = abs(origin1.x - origin2.x) vert_dist = origin1.z - origin2.z if (dist - 128) // 2 < abs(vert_dist): # The stairs are 2 long, 1 high. Check there's enough room # Subtract the last block though, since that's a corner. LOGGER.warning('Not enough room for stairs!') continue if dist > 128: # add straight sections in between place_catwalk_connections(vmf, instances, origin1, origin2) # Update the lists based on the directions that were set conn_lst1 = links[inst] conn_lst2 = links[inst2] if origin1.x < origin2.x: conn_lst1.E = conn_lst2.W = True elif origin2.x < origin1.x: conn_lst1.W = conn_lst2.E = True if origin1.y < origin2.y: conn_lst1.N = conn_lst2.S = True elif origin2.y < origin1.y: conn_lst1.S = conn_lst2.N = True for inst, dir_mask in links.items(): # Set the marker instances based on the attached walkways. normal = Vec(0, 0, 1).rotate_by_str(inst['angles']) new_type, inst['angles'] = utils.CONN_LOOKUP[dir_mask.as_tuple()] inst['file'] = instances[CATWALK_TYPES[new_type]] if new_type is utils.CONN_TYPES.side: # If the end piece is pointing at a wall, switch the instance. if normal.z == 0: if normal == dir_mask.conn_dir(): inst['file'] = instances['end_wall'] continue # We never have normal supports on end pieces elif new_type is utils.CONN_TYPES.none: # Unconnected catwalks on the wall switch to a special instance. # This lets players stand next to a portal surface on the wall. if normal.z == 0: inst['file'] = instances['single_wall'] inst['angles'] = INST_ANGLE[normal.as_tuple()] else: inst.remove() continue # These don't get supports otherwise # Add regular supports supp = None if normal == (0, 0, 1): # If in goo, use different supports! origin = Vec.from_str(inst['origin']) origin.z -= 128 if brushLoc.POS['world':origin].is_goo: supp = instances['support_goo'] else: supp = instances['support_floor'] elif normal == (0, 0, -1): supp = instances['support_ceil'] else: supp = instances['support_wall'] if supp: vmf.create_ent( classname='func_instance', origin=inst['origin'], angles=INST_ANGLE[normal.as_tuple()], file=supp, ) LOGGER.info('Finished catwalk generation!') return RES_EXHAUSTED
def res_antlaser(vmf: VMF, res: Property) -> object: """The condition to generate AntLasers and Antline Corners. This is executed once to modify all instances. """ conf_inst_corner = instanceLocs.resolve('<item_bee2_antline_corner>', silent=True) conf_inst_laser = instanceLocs.resolve(res['instance']) conf_glow_height = Vec(z=res.float('GlowHeight', 48) - 64) conf_las_start = Vec(z=res.float('LasStart') - 64) conf_rope_off = res.vec('RopePos') conf_toggle_targ = res['toggleTarg', ''] beam_conf = res.find_key('BeamKeys', or_blank=True) glow_conf = res.find_key('GlowKeys', or_blank=True) cable_conf = res.find_key('CableKeys', or_blank=True) if beam_conf: # Grab a copy of the beam spawnflags so we can set our own options. conf_beam_flags = beam_conf.int('spawnflags') # Mask out certain flags. conf_beam_flags &= ( 0 | 1 # Start On | 2 # Toggle | 4 # Random Strike | 8 # Ring | 16 # StartSparks | 32 # EndSparks | 64 # Decal End #| 128 # Shade Start #| 256 # Shade End #| 512 # Taper Out ) else: conf_beam_flags = 0 conf_outputs = [ Output.parse(prop) for prop in res if prop.name in ('onenabled', 'ondisabled') ] # Find all the markers. nodes: dict[str, Node] = {} for inst in vmf.by_class['func_instance']: filename = inst['file'].casefold() name = inst['targetname'] if filename in conf_inst_laser: node_type = NodeType.LASER elif filename in conf_inst_corner: node_type = NodeType.CORNER else: continue try: # Remove the item - it's no longer going to exist after # we're done. item = connections.ITEMS.pop(name) except KeyError: raise ValueError('No item for "{}"?'.format(name)) from None pos = Vec.from_str(inst['origin']) orient = Matrix.from_angle(Angle.from_str(inst['angles'])) if node_type is NodeType.CORNER: timer_delay = item.inst.fixup.int('$timer_delay') # We treat inf, 1, 2 and 3 as the same, to get around the 1 and 2 not # being selectable issue. pos = CORNER_POS[max(0, timer_delay - 3) % 8] @ orient + pos nodes[name] = Node(node_type, inst, item, pos, orient) if not nodes: # None at all. return conditions.RES_EXHAUSTED # Now find every connected group, recording inputs, outputs and links. todo = set(nodes.values()) groups: list[Group] = [] while todo: start = todo.pop() # Synthesise the Item used for logic. # We use a random info_target to manage the IO data. group = Group(start, start.type) groups.append(group) for node in group.nodes: # If this node has no non-node outputs, destroy the antlines. has_output = False node.is_grouped = True for conn in list(node.item.outputs): neighbour = conn.to_item neigh_node = nodes.get(neighbour.name, None) todo.discard(neigh_node) if neigh_node is None or neigh_node.type is not node.type: # Not a node or different item type, it must therefore # be a target of our logic. conn.from_item = group.item has_output = True continue elif not neigh_node.is_grouped: # Another node. group.nodes.append(neigh_node) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add(frozenset({node, neigh_node})) # If we have a real output, we need to transfer it. # Otherwise we can just destroy it. if has_output: node.item.transfer_antlines(group.item) else: node.item.delete_antlines() # Do the same for inputs, so we can catch that. for conn in list(node.item.inputs): neighbour = conn.from_item neigh_node = nodes.get(neighbour.name, None) todo.discard(neigh_node) if neigh_node is None or neigh_node.type is not node.type: # Not a node or different item type, it must therefore # be a target of our logic. conn.to_item = group.item node.had_input = True continue elif not neigh_node.is_grouped: # Another node. group.nodes.append(neigh_node) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add(frozenset({neigh_node, node})) # Now every node is in a group. Generate the actual entities. for group in groups: # We generate two ent types. For each marker, we add a sprite # and a beam pointing at it. Then for each connection # another beam. # Choose a random item name to use for our group. base_name = group.nodes[0].item.name out_enable = [Output('', '', 'FireUser2')] out_disable = [Output('', '', 'FireUser1')] if group.type is NodeType.LASER: for output in conf_outputs: if output.output.casefold() == 'onenabled': out_enable.append(output.copy()) else: out_disable.append(output.copy()) group.item.enable_cmd = tuple(out_enable) group.item.disable_cmd = tuple(out_disable) if group.type is NodeType.LASER and conf_toggle_targ: # Make the group info_target into a texturetoggle. toggle = group.item.inst toggle['classname'] = 'env_texturetoggle' toggle['target'] = conditions.local_name(group.nodes[0].inst, conf_toggle_targ) # Node -> index for targetnames. indexes: dict[Node, int] = {} # For antline corners, the antline segments. segments: list[antlines.Segment] = [] # frozenset[Node] unpacking isn't clear. node_a: Node node_b: Node if group.type is NodeType.CORNER: for node_a, node_b in group.links: # Place a straight antline between each connected node. # If on the same plane, we only need one. If not, we need to # do one for each plane it's in. offset = node_b.pos - node_a.pos up_a = node_a.orient.up() up_b = node_b.orient.up() plane_a = Vec.dot(node_a.pos, up_a) plane_b = Vec.dot(node_b.pos, up_b) if Vec.dot(up_a, up_b) > 0.9: if abs(plane_a - plane_b) > 1e-6: LOGGER.warning( 'Antline corners "{}" - "{}" ' 'are on different planes', node_a.item.name, node_b.item.name, ) continue u = node_a.orient.left() v = node_a.orient.forward() # Which are we aligned to? if abs(Vec.dot(offset, u)) < 1e-6 or abs(Vec.dot( offset, v)) < 1e-6: forward = offset.norm() group.add_ant_straight( up_a, node_a.pos + 8.0 * forward, node_b.pos - 8.0 * forward, ) else: LOGGER.warning( 'Antline corners "{}" - "{}" ' 'are not directly aligned', node_a.item.name, node_b.item.name, ) else: # We expect them be aligned to each other. side = Vec.cross(up_a, up_b) if abs(Vec.dot(side, offset)) < 1e-6: mid1 = node_a.pos + Vec.dot(offset, up_b) * up_b mid2 = node_b.pos - Vec.dot(offset, up_a) * up_a if mid1 != mid2: LOGGER.warning( 'Midpoint mismatch: {} != {} for "{}" - "{}"', mid1, mid2, node_a.item.name, node_b.item.name, ) group.add_ant_straight( up_a, node_a.pos + 8.0 * (mid1 - node_a.pos).norm(), mid1, ) group.add_ant_straight( up_b, node_b.pos + 8.0 * (mid2 - node_b.pos).norm(), mid2, ) # For cables, it's a bit trickier than the beams. # The cable ent itself is the one which decides what it links to, # so we need to potentially make endpoint cables at locations with # only "incoming" lines. # So this dict is either a targetname to indicate cables with an # outgoing connection, or the entity for endpoints without an outgoing # connection. cable_points: dict[Node, Union[Entity, str]] = {} for i, node in enumerate(group.nodes, start=1): indexes[node] = i node.item.name = base_name if group.type is NodeType.CORNER: node.inst.remove() # Figure out whether we want a corner at this point, or # just a regular dot. If a non-node input was provided it's # always a corner. Otherwise it's one if there's an L, T or X # junction. use_corner = True norm = node.orient.up().as_tuple() if not node.had_input: neighbors = [ mag * direction for direction in [ node.orient.forward(), node.orient.left(), ] for mag in [-8.0, 8.0] if ((node.pos + mag * direction).as_tuple(), norm) in group.ant_seg ] if len(neighbors) == 2: [off1, off2] = neighbors if Vec.dot(off1, off2) < -0.99: # ---o---, merge together. The endpoints we want # are the other ends of the two segments. group.add_ant_straight( node.orient.up(), group.rem_ant_straight(norm, node.pos + off1), group.rem_ant_straight(norm, node.pos + off2), ) use_corner = False elif len(neighbors) == 1: # o-----, merge. [offset] = neighbors group.add_ant_straight( node.orient.up(), group.rem_ant_straight(norm, node.pos + offset), node.pos - offset, ) use_corner = False if use_corner: segments.append( antlines.Segment( antlines.SegType.CORNER, round(node.orient.up(), 3), Vec(node.pos), Vec(node.pos), )) elif group.type is NodeType.LASER: sprite_pos = node.pos + conf_glow_height @ node.orient if glow_conf: # First add the sprite at the right height. sprite = vmf.create_ent('env_sprite') for prop in glow_conf: sprite[prop.name] = conditions.resolve_value( node.inst, prop.value) sprite['origin'] = sprite_pos sprite['targetname'] = NAME_SPR(base_name, i) elif beam_conf: # If beams but not sprites, we need a target. vmf.create_ent( 'info_target', origin=sprite_pos, targetname=NAME_SPR(base_name, i), ) if beam_conf: # Now the beam going from below up to the sprite. beam_pos = node.pos + conf_las_start @ node.orient beam = vmf.create_ent('env_beam') for prop in beam_conf: beam[prop.name] = conditions.resolve_value( node.inst, prop.value) beam['origin'] = beam['targetpoint'] = beam_pos beam['targetname'] = NAME_BEAM_LOW(base_name, i) beam['LightningStart'] = beam['targetname'] beam['LightningEnd'] = NAME_SPR(base_name, i) beam['spawnflags'] = conf_beam_flags | 128 # Shade Start segments += set(group.ant_seg.values()) if group.type is NodeType.CORNER and segments: group.item.antlines.add( antlines.Antline(group.item.name + '_antline', segments)) if group.type is NodeType.LASER and beam_conf: for i, (node_a, node_b) in enumerate(group.links): beam = vmf.create_ent('env_beam') conditions.set_ent_keys(beam, node_a.inst, res, 'BeamKeys') beam['origin'] = beam['targetpoint'] = node_a.pos beam['targetname'] = NAME_BEAM_CONN(base_name, i) beam['LightningStart'] = NAME_SPR(base_name, indexes[node_a]) beam['LightningEnd'] = NAME_SPR(base_name, indexes[node_b]) beam['spawnflags'] = conf_beam_flags if group.type is NodeType.LASER and cable_conf: build_cables( vmf, group, cable_points, base_name, beam_conf, conf_rope_off, ) return conditions.RES_EXHAUSTED
def gen_squarebeams(vmf: VMF, p1: Vec, p2: Vec, skin, gen_collision=True): """Generate squarebeams props to fill the space given. The space should be in multiples of 64. The squarebeams brush will be aligned to the lowest point in the space. """ z = min(p1.z, p2.z) + 8 min_x = min(p1.x, p2.x) min_y = min(p1.y, p2.y) max_x = max(p1.x, p2.x) max_y = max(p1.y, p2.y) dist_x = max_x - min_x dist_y = max_y - min_y # After this x or y dist, move to the next grid size. cutoff_512_x = dist_x // 512 * 512 cutoff_256_x = dist_x // 256 * 256 cutoff_128_x = dist_x // 128 * 128 cutoff_512_y = dist_y // 512 * 512 cutoff_256_y = dist_y // 256 * 256 cutoff_128_y = dist_y // 128 * 128 for x, y in utils.iter_grid( max_x=int(dist_x), max_y=int(dist_y), stride=64, ): if x < cutoff_512_x and y < cutoff_512_y: # Make 1 prop every 512 units, at the center if x % 512 == 0 and y % 512 == 0: _make_squarebeam( vmf, Vec(min_x + x + 256, min_y + y + 256, z), skin, '_8x8', ) elif x < cutoff_256_x and y < cutoff_256_y: if x % 256 == 0 and y % 256 == 0: _make_squarebeam( vmf, Vec(min_x + x + 128, min_y + y + 128, z), skin, '_4x4', ) elif x < cutoff_128_x and y < cutoff_128_y: if x % 128 == 0 and y % 128 == 0: _make_squarebeam( vmf, Vec(min_x + x + 64, min_y + y + 64, z), skin, '_2x2', ) else: # Make squarebeams for every point! _make_squarebeam( vmf, Vec(min_x + x + 32, min_y + y + 32, z), skin, ) if gen_collision: collision = vmf.create_ent( classname='func_brush', disableshadows='1', disableflashlight='1', disablereceiveshadows='1', shadowdepthnocache='1', solidity='2', # Always Solid solidbsp='1', ) for x in range(int(min_x) + 64, int(max_x), 64): collision.solids.append( vmf.make_prism( p1=Vec(x - 2, min_y + 2, z - 2), p2=Vec(x + 2, max_y - 2, z - 8), mat='tools/toolsnodraw', ).solid) for y in range(int(min_y) + 64, int(max_y), 64): collision.solids.append( vmf.make_prism( p1=Vec(min_x + 2, y - 2, z - 2), p2=Vec(max_x - 2, y + 2, z - 8), mat='tools/toolsnodraw', ).solid) for x1, y1, x2, y2 in [ (min_x, min_y, max_x, min_y + 2), (min_x, max_y, max_x, max_y - 2), (min_x, min_y, min_x + 2, max_y), (max_x, min_y, max_x - 2, max_y), ]: collision.solids.append( vmf.make_prism( p1=Vec(x1, y1, z - 2), p2=Vec(x2, y2, z - 8), mat='tools/toolsnodraw', ).solid)
def build_cables( vmf: VMF, group: Group, cable_points: dict[Node, Union[Entity, str]], base_name: str, beam_conf: Property, conf_rope_off: Vec, ) -> None: """Place Old-Aperture style cabling.""" # We have a couple different situations to deal with here. # Either end could Not exist, be Unlinked, or be Linked = 8 combos. # Always flip so we do A to B. # AB | # NN | Make 2 new ones, one is an endpoint. # NU | Flip, do UN. # NL | Make A, link A to B. Both are linked. # UN | Make B, link A to B. B is unlinked. # UU | Link A to B, A is now linked, B is unlinked. # UL | Link A to B. Both are linked. # LN | Flip, do NL. # LU | Flip, do UL # LL | Make A, link A to B. Both are linked. rope_ind = 0 # Uniqueness value. node_a: Node node_b: Node rope_a: Entity rope_b: Entity for node_a, node_b in group.links: state_a, ent_a = RopeState.from_node(cable_points, node_a) state_b, ent_b = RopeState.from_node(cable_points, node_b) if (state_a is RopeState.LINKED or (state_a is RopeState.NONE and state_b is RopeState.UNLINKED)): # Flip these, handle the opposite order. state_a, state_b = state_b, state_a ent_a, ent_b = ent_b, ent_a node_a, node_b = node_b, node_a pos_a = node_a.pos + conf_rope_off @ node_a.orient pos_b = node_b.pos + conf_rope_off @ node_b.orient # Need to make the A rope if we don't have one that's unlinked. if state_a is not RopeState.UNLINKED: rope_a = vmf.create_ent('move_rope') for prop in beam_conf: rope_a[prop.name] = node_a.inst.fixup.substitute( node_a.inst, prop.value) rope_a['origin'] = pos_a rope_ind += 1 rope_a['targetname'] = NAME_CABLE(base_name, rope_ind) else: # It is unlinked, so it's the rope to use. assert isinstance(ent_a, Entity) rope_a = ent_a # Only need to make the B rope if it doesn't have one. if state_b is RopeState.NONE: rope_b = vmf.create_ent('move_rope') for prop in beam_conf: rope_b[prop.name] = node_b.inst.fixup.substitute(prop.value) rope_b['origin'] = pos_b rope_ind += 1 name_b = rope_b['targetname'] = NAME_CABLE(base_name, rope_ind) cable_points[node_b] = rope_b # Someone can use this. elif state_b is RopeState.UNLINKED: # Both must be unlinked, we aren't using this link though. assert isinstance(ent_b, Entity) name_b = ent_b['targetname'] else: # Linked, we just have the name. name_b = ent_b # By here, rope_a should be an unlinked rope, # and name_b should be a name to link to. rope_a['nextkey'] = name_b # Figure out how much slack to give. # If on floor, we need to be taut to have clearance. if node_a.on_floor or node_b.on_floor: rope_a['slack'] = 60 else: rope_a['slack'] = 125 # We're always linking A to B, so A is always linked! if state_a is not RopeState.LINKED: cable_points[node_a] = rope_a['targetname']
def res_add_brush(vmf: VMF, inst: Entity, res: Property) -> None: """Spawn in a brush at the indicated points. - `point1` and `point2` are locations local to the instance, with `0 0 0` as the floor-position. - `type` is either `black` or `white`. - detail should be set to `1/0`. If true the brush will be a func_detail instead of a world brush. The sides will be textured with 1x1, 2x2 or 4x4 wall, ceiling and floor textures as needed. """ origin = Vec.from_str(inst['origin']) angles = Angle.from_str(inst['angles']) point1 = Vec.from_str(res['point1']) point2 = Vec.from_str(res['point2']) point1.z -= 64 # Offset to the location of the floor point2.z -= 64 # Rotate to match the instance point1 = point1 @ angles + origin point2 = point2 @ angles + origin try: tex_type = texturing.Portalable(res['type', 'black']) except ValueError: LOGGER.warning( 'AddBrush: "{}" is not a valid brush ' 'color! (white or black)', res['type'], ) tex_type = texturing.Portalable.BLACK dim = round(point2 - point1, 6) dim.max(-dim) # Figure out what grid size and scale is needed # Check the dimensions in two axes to figure out the largest # tile size that can fit in it. tile_grids = { 'x': tiling.TileSize.TILE_4x4, 'y': tiling.TileSize.TILE_4x4, 'z': tiling.TileSize.TILE_4x4, } for axis in 'xyz': u, v = Vec.INV_AXIS[axis] max_size = min(dim[u], dim[v]) if max_size % 128 == 0: tile_grids[axis] = tiling.TileSize.TILE_1x1 elif dim[u] % 64 == 0 and dim[v] % 128 == 0: tile_grids[axis] = tiling.TileSize.TILE_2x1 elif max_size % 64 == 0: tile_grids[axis] = tiling.TileSize.TILE_2x2 else: tile_grids[axis] = tiling.TileSize.TILE_4x4 grid_offset = origin // 128 # type: Vec # All brushes in each grid have the same textures for each side. random.seed(grid_offset.join(' ') + '-partial_block') solids = vmf.make_prism(point1, point2) solids.north.mat = texturing.gen( texturing.GenCat.NORMAL, Vec(Vec.N), tex_type, ).get(solids.north.get_origin(), tile_grids['y']) solids.south.mat = texturing.gen( texturing.GenCat.NORMAL, Vec(Vec.S), tex_type, ).get(solids.north.get_origin(), tile_grids['y']) solids.east.mat = texturing.gen( texturing.GenCat.NORMAL, Vec(Vec.E), tex_type, ).get(solids.north.get_origin(), tile_grids['x']) solids.west.mat = texturing.gen( texturing.GenCat.NORMAL, Vec(Vec.W), tex_type, ).get(solids.north.get_origin(), tile_grids['x']) solids.top.mat = texturing.gen( texturing.GenCat.NORMAL, Vec(Vec.T), tex_type, ).get(solids.north.get_origin(), tile_grids['z']) solids.bottom.mat = texturing.gen( texturing.GenCat.NORMAL, Vec(Vec.B), tex_type, ).get(solids.north.get_origin(), tile_grids['z']) if res.bool('detail'): # Add the brush to a func_detail entity vmf.create_ent(classname='func_detail').solids = [solids.solid] else: # Add to the world vmf.add_brush(solids.solid)
def res_make_tag_fizzler(vmf: VMF, inst: Entity, res: Property): """Add an Aperture Tag Paint Gun activation fizzler. These fizzlers are created via signs, and work very specially. This must be before -250 so it runs before fizzlers and connections. """ ( sign_offset, fizz_io_type, inst_frame_double, inst_frame_single, blue_sign_on, blue_sign_off, oran_sign_on, oran_sign_off, ) = res.value # type: int, ItemType, str, str, str, str, str, str import vbsp if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']: # Abort - TAG fizzlers shouldn't appear in any other game! inst.remove() return fizzler = None fizzler_item = None # Look for the fizzler instance we want to replace. sign_item = ITEMS[inst['targetname']] for conn in list(sign_item.outputs): if conn.to_item.name in FIZZLERS: if fizzler is None: fizzler = FIZZLERS[conn.to_item.name] fizzler_item = conn.to_item else: raise ValueError('Multiple fizzlers attached to a sign!') conn.remove() # Regardless, remove the useless output. sign_item.delete_antlines() if fizzler is None: # No fizzler - remove this sign inst.remove() return if fizzler.fizz_type.id == TAG_FIZZ_ID: LOGGER.warning('Two tag signs attached to one fizzler...') inst.remove() return # Swap to the special Tag Fizzler type. fizzler.fizz_type = FIZZ_TYPES[TAG_FIZZ_ID] # And also swap the connection's type. fizzler_item.item_type = fizz_io_type fizzler_item.enable_cmd = fizz_io_type.enable_cmd fizzler_item.disable_cmd = fizz_io_type.disable_cmd fizzler_item.sec_enable_cmd = fizz_io_type.sec_enable_cmd fizzler_item.sec_disable_cmd = fizz_io_type.sec_disable_cmd sign_loc = ( # The actual location of the sign - on the wall Vec.from_str(inst['origin']) + Vec(0, 0, -64).rotate_by_str(inst['angles']) ) # Now deal with the visual aspect: # Blue signs should be on top. blue_enabled = inst.fixup.bool('$start_enabled') oran_enabled = inst.fixup.bool('$start_reversed') # If True, single-color signs will also turn off the other color. # This also means we always show both signs. # If both are enabled or disabled, this has no effect. disable_other = ( not inst.fixup.bool('$disable_autorespawn', True) and blue_enabled != oran_enabled ) # Delete fixups now, they aren't useful. inst.fixup.clear() if not blue_enabled and not oran_enabled: # Hide the sign in this case! inst.remove() inst_angle = srctools.parse_vec_str(inst['angles']) inst_normal = Vec(0, 0, 1).rotate(*inst_angle) loc = Vec.from_str(inst['origin']) if disable_other or (blue_enabled and oran_enabled): inst['file'] = inst_frame_double # On a wall, and pointing vertically if inst_normal.z == 0 and Vec(y=1).rotate(*inst_angle).z: # They're vertical, make sure blue's on top! blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset) oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset) # If orange is enabled, with two frames put that on top # instead since it's more important if disable_other and oran_enabled: blue_loc, oran_loc = oran_loc, blue_loc else: offset = Vec(0, sign_offset, 0).rotate(*inst_angle) blue_loc = loc + offset oran_loc = loc - offset else: inst['file'] = inst_frame_single # They're always centered blue_loc = loc oran_loc = loc if inst_normal.z != 0: # If on floors/ceilings, rotate to point at the fizzler! sign_floor_loc = sign_loc.copy() sign_floor_loc.z = 0 # We don't care about z-positions. # Grab the data saved earlier in res_find_potential_tag_fizzlers() axis, side_min, side_max, normal = calc_fizzler_orient(fizzler) # The Z-axis fizzler (horizontal) must be treated differently. if axis == 'z': # For z-axis, just compare to the center point. # The values are really x, y, z, not what they're named. sign_dir = sign_floor_loc - (side_min, side_max, normal) else: # For the other two, we compare to the line, # or compare to the closest side (in line with the fizz) other_axis = 'x' if axis == 'y' else 'y' if abs(sign_floor_loc[other_axis] - normal) < 32: # Compare to the closest side. Use ** to swap x/y arguments # appropriately. The closest side is the one with the # smallest magnitude. sign_dir = min( sign_floor_loc - Vec.with_axes( axis,side_min, other_axis, normal, ), sign_floor_loc - Vec.with_axes( axis, side_max, other_axis, normal, ), key=Vec.mag, ) else: # Align just based on whether we're in front or behind. sign_dir = Vec() sign_dir[other_axis] = sign_floor_loc[other_axis] - normal sign_angle = math.degrees( math.atan2(sign_dir.y, sign_dir.x) ) # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals sign_angle = (sign_angle + 45) // 90 * 90 # Rotate to fit the instances - south is down sign_angle = int(sign_angle + 90) % 360 if inst_normal.z > 0: sign_angle = '0 {} 0'.format(sign_angle) elif inst_normal.z < 0: # Flip upside-down for ceilings sign_angle = '0 {} 180'.format(sign_angle) else: # On a wall, face upright sign_angle = PETI_INST_ANGLE[inst_normal.as_tuple()] # If disable_other, we show off signs. Otherwise we don't use that sign. blue_sign = blue_sign_on if blue_enabled else blue_sign_off if disable_other else None oran_sign = oran_sign_on if oran_enabled else oran_sign_off if disable_other else None if blue_sign: vmf.create_ent( classname='func_instance', file=blue_sign, targetname=inst['targetname'], angles=sign_angle, origin=blue_loc.join(' '), ) if oran_sign: vmf.create_ent( classname='func_instance', file=oran_sign, targetname=inst['targetname'], angles=sign_angle, origin=oran_loc.join(' '), ) # Now modify the fizzler... # Subtract the sign from the list of connections, but don't go below # zero fizzler.base_inst.fixup['$connectioncount'] = str(max( 0, srctools.conv_int(fizzler.base_inst.fixup['$connectioncount', '']) - 1 )) # Find the direction the fizzler normal is. # Signs will associate with the given side! bbox_min, bbox_max = fizzler.emitters[0] fizz_norm_axis = fizzler.normal().axis() sign_center = (bbox_min[fizz_norm_axis] + bbox_max[fizz_norm_axis]) / 2 # Figure out what the sides will set values to... pos_blue = False pos_oran = False neg_blue = False neg_oran = False if sign_loc[fizz_norm_axis] < sign_center: pos_blue = blue_enabled pos_oran = oran_enabled else: neg_blue = blue_enabled neg_oran = oran_enabled # If it activates the paint gun, use different textures fizzler.tag_on_pos = pos_blue or pos_oran fizzler.tag_on_neg = neg_blue or neg_oran # Now make the trigger ents. We special-case these since they need to swap # depending on the sign config and position. if vbsp.GAME_MODE == 'COOP': # We need ATLAS-specific triggers pos_trig = vmf.create_ent( classname='trigger_playerteam', ) neg_trig = vmf.create_ent( classname='trigger_playerteam', ) output = 'OnStartTouchBluePlayer' else: pos_trig = vmf.create_ent( classname='trigger_multiple', ) neg_trig = vmf.create_ent( classname='trigger_multiple', spawnflags='1', ) output = 'OnStartTouch' pos_trig['origin'] = neg_trig['origin'] = fizzler.base_inst['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = local_name(fizzler.base_inst, 'trig_pos') neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_neg') pos_trig['startdisabled'] = neg_trig['startdisabled'] = ( not fizzler.base_inst.fixup.bool('start_enabled') ) pos_trig.outputs = [ Output(output, neg_trig, 'Enable'), Output(output, pos_trig, 'Disable'), ] neg_trig.outputs = [ Output(output, pos_trig, 'Enable'), Output(output, neg_trig, 'Disable'), ] voice_attr = vbsp.settings['has_attr'] if blue_enabled or disable_other: # If this is blue/oran only, don't affect the other color neg_trig.outputs.append(Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_blue), )) pos_trig.outputs.append(Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_blue), )) if blue_enabled: # Add voice attributes - we have the gun and gel! voice_attr['bluegelgun'] = True voice_attr['bluegel'] = True voice_attr['bouncegun'] = True voice_attr['bouncegel'] = True if oran_enabled or disable_other: neg_trig.outputs.append(Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_oran), )) pos_trig.outputs.append(Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_oran), )) if oran_enabled: voice_attr['orangegelgun'] = True voice_attr['orangegel'] = True voice_attr['speedgelgun'] = True voice_attr['speedgel'] = True if not oran_enabled and not blue_enabled: # If both are disabled, we must shutdown the gun when touching # either side - use neg_trig for that purpose! # We want to get rid of pos_trig to save ents vmf.remove_ent(pos_trig) neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_off') neg_trig.outputs.clear() neg_trig.add_out(Output( output, '@BlueIsEnabled', 'SetValue', param='0' )) neg_trig.add_out(Output( output, '@OrangeIsEnabled', 'SetValue', param='0' )) # Make the triggers. for bbox_min, bbox_max in fizzler.emitters: bbox_min = bbox_min.copy() - 64 * fizzler.up_axis bbox_max = bbox_max.copy() + 64 * fizzler.up_axis # The triggers are 8 units thick, with a 32-unit gap in the middle neg_min, neg_max = Vec(bbox_min), Vec(bbox_max) neg_min[fizz_norm_axis] -= 24 neg_max[fizz_norm_axis] -= 16 pos_min, pos_max = Vec(bbox_min), Vec(bbox_max) pos_min[fizz_norm_axis] += 16 pos_max[fizz_norm_axis] += 24 if blue_enabled or oran_enabled: neg_trig.solids.append( vmf.make_prism( neg_min, neg_max, mat='tools/toolstrigger', ).solid, ) pos_trig.solids.append( vmf.make_prism( pos_min, pos_max, mat='tools/toolstrigger', ).solid, ) else: # If neither enabled, use one trigger neg_trig.solids.append( vmf.make_prism( neg_min, pos_max, mat='tools/toolstrigger', ).solid, )
def make_straight( vmf: VMF, origin: Vec, normal: Vec, dist: int, config: Config, is_start=False, ) -> None: """Make a straight line of instances from one point to another.""" angles = round(normal, 6).to_angle() orient = Matrix.from_angle(angles) # The starting brush needs to stick out a bit further, to cover the # point_push entity. start_off = -96 if is_start else -64 p1, p2 = Vec.bbox( origin + Vec(start_off, -config.trig_radius, -config.trig_radius) @ orient, origin + Vec(dist - 64, config.trig_radius, config.trig_radius) @ orient, ) solid = vmf.make_prism(p1, p2, mat='tools/toolstrigger').solid motion_trigger(vmf, solid.copy()) push_trigger(vmf, origin, normal, [solid]) off = 0 for seg_dist in utils.fit(dist, config.inst_straight_sizes): vmf.create_ent( classname='func_instance', origin=origin + off * orient.forward(), angles=angles, file=config.inst_straight[seg_dist], ) off += seg_dist # Supports. if config.inst_support: for off in range(0, int(dist), 128): position = origin + off * normal placed_support = False for supp_dir in [ orient.up(), orient.left(), -orient.left(), -orient.up() ]: try: tile = tiling.TILES[ (position - 128 * supp_dir).as_tuple(), supp_dir.norm().as_tuple() ] except KeyError: continue # Check all 4 center tiles are present. if all(tile[u, v].is_tile for u in (1, 2) for v in (1, 2)): vmf.create_ent( classname='func_instance', origin=position, angles=Matrix.from_basis(x=normal, z=supp_dir).to_angle(), file=config.inst_support, ) placed_support = True if placed_support and config.inst_support_ring: vmf.create_ent( classname='func_instance', origin=position, angles=angles, file=config.inst_support_ring, )
def add_quote( vmf: VMF, quote: Property, targetname: str, quote_loc: Vec, style_vars: dict, use_dings: bool, ) -> None: """Add a quote to the map.""" LOGGER.info('Adding quote: {}', quote) only_once = atomic = False cc_emit_name = None start_ents = [] # type: List[Entity] end_commands = [] start_names = [] # The OnUser1 outputs always play the quote (PlaySound/Start), so you can # mix ent types in the same pack. for prop in quote: name = prop.name.casefold() if name == 'file': conditions.add_inst( vmf, file=INST_PREFIX + prop.value, origin=quote_loc, no_fixup=True, ) elif name == 'choreo': # If the property has children, the children are a set of sequential # voice lines. # If the name is set to '@glados_line', the ents will be named # ('@glados_line', 'glados_line_2', 'glados_line_3', ...) start_names.append(targetname) if prop.has_children(): secondary_name = targetname.lstrip('@') + '_' # Evenly distribute the choreo ents across the width of the # voice-line room. off = Vec(y=120 / (len(prop) + 1)) start = quote_loc - (0, 60, 0) + off for ind, choreo_line in enumerate( prop, start=1): # type: int, Property is_first = (ind == 1) is_last = (ind == len(prop)) name = (targetname if is_first else secondary_name + str(ind)) choreo = add_choreo( vmf, choreo_line.value, targetname=name, loc=start + off * (ind - 1), use_dings=use_dings, is_first=is_first, is_last=is_last, only_once=only_once, ) # Add a IO command to start the next one. if not is_last: choreo.add_out( Output( 'OnCompletion', secondary_name + str(ind + 1), 'Start', delay=0.1, )) if is_first: # Ensure this works with cc_emit start_ents.append(choreo) if is_last: for out in end_commands: choreo.add_out(out.copy()) end_commands.clear() else: # Add a single choreo command. choreo = add_choreo( vmf, prop.value, targetname, quote_loc, use_dings=use_dings, only_once=only_once, ) start_ents.append(choreo) for out in end_commands: choreo.add_out(out.copy()) end_commands.clear() elif name == 'snd': start_names.append(targetname) snd = vmf.create_ent( classname='ambient_generic', spawnflags='49', # Infinite Range, Starts Silent targetname=targetname, origin=quote_loc, message=prop.value, health='10', # Volume ) snd.add_out( Output( 'OnUser1', targetname, 'PlaySound', only_once=only_once, )) start_ents.append(snd) elif name == 'bullseye': add_bullseye(vmf, quote_loc, prop.value) elif name == 'cc_emit': # In Aperture Tag, this additional console command is used # to add the closed captions. # Store in a variable, so we can be sure to add the output # regardless of the property order. cc_emit_name = prop.value elif name == 'setstylevar': # Set this stylevar to True # This is useful so some styles can react to which line was # chosen. style_vars[prop.value.casefold()] = True elif name == 'packlist': packing.pack_list(vmf, prop.value) elif name == 'pack': if prop.has_children(): packing.pack_files(vmf, *[subprop.value for subprop in prop]) else: packing.pack_files(vmf, prop.value) elif name == 'choreo_name': # Change the targetname used for subsequent entities targetname = prop.value elif name == 'onlyonce': only_once = srctools.conv_bool(prop.value) elif name == 'atomic': atomic = srctools.conv_bool(prop.value) elif name == 'endcommand': end_commands.append( Output( 'OnCompletion', prop['target'], prop['input'], prop['parm', ''], prop.float('delay'), only_once=prop.bool('only_once'), times=prop.int('times', -1), )) if cc_emit_name: for ent in start_ents: ent.add_out( Output( 'OnUser1', '@command', 'Command', param='cc_emit ' + cc_emit_name, )) # If Atomic is true, after a line is started all variants # are blocked from playing. if atomic: for ent in start_ents: for name in start_names: if ent['targetname'] == name: # Don't block yourself. continue ent.add_out(Output( 'OnUser1', name, 'Kill', only_once=True, ))
def res_make_tag_fizzler(vmf: VMF, inst: Entity, res: Property): """Add an Aperture Tag Paint Gun activation fizzler. These fizzlers are created via signs, and work very specially. This must be before -250 so it runs before fizzlers and connections. """ ( sign_offset, fizz_io_type, inst_frame_double, inst_frame_single, blue_sign_on, blue_sign_off, oran_sign_on, oran_sign_off, ) = res.value # type: int, ItemType, str, str, str, str, str, str import vbsp if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']: # Abort - TAG fizzlers shouldn't appear in any other game! inst.remove() return fizzler = None fizzler_item = None # Look for the fizzler instance we want to replace. sign_item = ITEMS[inst['targetname']] for conn in list(sign_item.outputs): if conn.to_item.name in FIZZLERS: if fizzler is None: fizzler = FIZZLERS[conn.to_item.name] fizzler_item = conn.to_item else: raise ValueError('Multiple fizzlers attached to a sign!') conn.remove() # Regardless, remove the useless output. sign_item.delete_antlines() if fizzler is None: # No fizzler - remove this sign inst.remove() return if fizzler.fizz_type.id == TAG_FIZZ_ID: LOGGER.warning('Two tag signs attached to one fizzler...') inst.remove() return # Swap to the special Tag Fizzler type. fizzler.fizz_type = FIZZ_TYPES[TAG_FIZZ_ID] # And also swap the connection's type. fizzler_item.item_type = fizz_io_type fizzler_item.enable_cmd = fizz_io_type.enable_cmd fizzler_item.disable_cmd = fizz_io_type.disable_cmd fizzler_item.sec_enable_cmd = fizz_io_type.sec_enable_cmd fizzler_item.sec_disable_cmd = fizz_io_type.sec_disable_cmd sign_loc = ( # The actual location of the sign - on the wall Vec.from_str(inst['origin']) + Vec(0, 0, -64).rotate_by_str(inst['angles'])) fizz_norm_axis = fizzler.normal().axis() # Now deal with the visual aspect: # Blue signs should be on top. blue_enabled = inst.fixup.bool('$start_enabled') oran_enabled = inst.fixup.bool('$start_reversed') # If True, single-color signs will also turn off the other color. # This also means we always show both signs. # If both are enabled or disabled, this has no effect. disable_other = (not inst.fixup.bool('$disable_autorespawn', True) and blue_enabled != oran_enabled) # Delete fixups now, they aren't useful. inst.fixup.clear() if not blue_enabled and not oran_enabled: # Hide the sign in this case! inst.remove() inst_angle = srctools.parse_vec_str(inst['angles']) inst_normal = Vec(0, 0, 1).rotate(*inst_angle) loc = Vec.from_str(inst['origin']) if disable_other or (blue_enabled and oran_enabled): inst['file'] = inst_frame_double # On a wall, and pointing vertically if inst_normal.z == 0 and Vec(y=1).rotate(*inst_angle).z: # They're vertical, make sure blue's on top! blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset) oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset) # If orange is enabled, with two frames put that on top # instead since it's more important if disable_other and oran_enabled: blue_loc, oran_loc = oran_loc, blue_loc else: offset = Vec(0, sign_offset, 0).rotate(*inst_angle) blue_loc = loc + offset oran_loc = loc - offset else: inst['file'] = inst_frame_single # They're always centered blue_loc = loc oran_loc = loc if inst_normal.z != 0: # If on floors/ceilings, rotate to point at the fizzler! sign_floor_loc = sign_loc.copy() sign_floor_loc.z = 0 # We don't care about z-positions. s, l = Vec.bbox(itertools.chain.from_iterable(fizzler.emitters)) if fizz_norm_axis == 'z': # For z-axis, just compare to the center point of the emitters. sign_dir = ((s.x + l.x) / 2, (s.y + l.y) / 2, 0) - sign_floor_loc else: # For the other two, we compare to the line, # or compare to the closest side (in line with the fizz) if fizz_norm_axis == 'x': # Extends in Y direction other_axis = 'y' side_min = s.y side_max = l.y normal = s.x else: # Extends in X direction other_axis = 'x' side_min = s.x side_max = l.x normal = s.y # Right in line with the fizzler. Point at the closest emitter. if abs(sign_floor_loc[other_axis] - normal) < 32: # Compare to the closest side. sign_dir = min( (sign_floor_loc - Vec.with_axes( fizz_norm_axis, side_min, other_axis, normal, ), sign_floor_loc - Vec.with_axes( fizz_norm_axis, side_max, other_axis, normal, )), key=Vec.mag, ) else: # Align just based on whether we're in front or behind. sign_dir = Vec.with_axes( fizz_norm_axis, normal - sign_floor_loc[fizz_norm_axis]).norm() sign_yaw = math.degrees(math.atan2(sign_dir.y, sign_dir.x)) # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals sign_yaw = (sign_yaw + 45) // 90 * 90 # Rotate to fit the instances - south is down sign_yaw = int(sign_yaw - 90) % 360 if inst_normal.z > 0: sign_angle = '0 {} 0'.format(sign_yaw) elif inst_normal.z < 0: # Flip upside-down for ceilings sign_angle = '0 {} 180'.format(sign_yaw) else: raise AssertionError('Cannot be zero here!') else: # On a wall, face upright sign_angle = PETI_INST_ANGLE[inst_normal.as_tuple()] # If disable_other, we show off signs. Otherwise we don't use that sign. blue_sign = blue_sign_on if blue_enabled else blue_sign_off if disable_other else None oran_sign = oran_sign_on if oran_enabled else oran_sign_off if disable_other else None if blue_sign: vmf.create_ent( classname='func_instance', file=blue_sign, targetname=inst['targetname'], angles=sign_angle, origin=blue_loc.join(' '), ) if oran_sign: vmf.create_ent( classname='func_instance', file=oran_sign, targetname=inst['targetname'], angles=sign_angle, origin=oran_loc.join(' '), ) # Now modify the fizzler... # Subtract the sign from the list of connections, but don't go below # zero fizzler.base_inst.fixup['$connectioncount'] = str( max( 0, srctools.conv_int(fizzler.base_inst.fixup['$connectioncount', '']) - 1)) # Find the direction the fizzler normal is. # Signs will associate with the given side! bbox_min, bbox_max = fizzler.emitters[0] sign_center = (bbox_min[fizz_norm_axis] + bbox_max[fizz_norm_axis]) / 2 # Figure out what the sides will set values to... pos_blue = False pos_oran = False neg_blue = False neg_oran = False if sign_loc[fizz_norm_axis] < sign_center: pos_blue = blue_enabled pos_oran = oran_enabled else: neg_blue = blue_enabled neg_oran = oran_enabled # If it activates the paint gun, use different textures fizzler.tag_on_pos = pos_blue or pos_oran fizzler.tag_on_neg = neg_blue or neg_oran # Now make the trigger ents. We special-case these since they need to swap # depending on the sign config and position. if vbsp.GAME_MODE == 'COOP': # We need ATLAS-specific triggers pos_trig = vmf.create_ent(classname='trigger_playerteam', ) neg_trig = vmf.create_ent(classname='trigger_playerteam', ) output = 'OnStartTouchBluePlayer' else: pos_trig = vmf.create_ent(classname='trigger_multiple', ) neg_trig = vmf.create_ent( classname='trigger_multiple', spawnflags='1', ) output = 'OnStartTouch' pos_trig['origin'] = neg_trig['origin'] = fizzler.base_inst['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = local_name(fizzler.base_inst, 'trig_pos') neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_neg') pos_trig['startdisabled'] = neg_trig['startdisabled'] = ( not fizzler.base_inst.fixup.bool('start_enabled')) pos_trig.outputs = [ Output(output, neg_trig, 'Enable'), Output(output, pos_trig, 'Disable'), ] neg_trig.outputs = [ Output(output, pos_trig, 'Enable'), Output(output, neg_trig, 'Disable'), ] voice_attr = vbsp.settings['has_attr'] if blue_enabled or disable_other: # If this is blue/oran only, don't affect the other color neg_trig.outputs.append( Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_blue), )) pos_trig.outputs.append( Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_blue), )) if blue_enabled: # Add voice attributes - we have the gun and gel! voice_attr['bluegelgun'] = True voice_attr['bluegel'] = True voice_attr['bouncegun'] = True voice_attr['bouncegel'] = True if oran_enabled or disable_other: neg_trig.outputs.append( Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_oran), )) pos_trig.outputs.append( Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_oran), )) if oran_enabled: voice_attr['orangegelgun'] = True voice_attr['orangegel'] = True voice_attr['speedgelgun'] = True voice_attr['speedgel'] = True if not oran_enabled and not blue_enabled: # If both are disabled, we must shutdown the gun when touching # either side - use neg_trig for that purpose! # We want to get rid of pos_trig to save ents vmf.remove_ent(pos_trig) neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_off') neg_trig.outputs.clear() neg_trig.add_out( Output(output, '@BlueIsEnabled', 'SetValue', param='0')) neg_trig.add_out( Output(output, '@OrangeIsEnabled', 'SetValue', param='0')) # Make the triggers. for bbox_min, bbox_max in fizzler.emitters: bbox_min = bbox_min.copy() - 64 * fizzler.up_axis bbox_max = bbox_max.copy() + 64 * fizzler.up_axis # The triggers are 8 units thick, with a 32-unit gap in the middle neg_min, neg_max = Vec(bbox_min), Vec(bbox_max) neg_min[fizz_norm_axis] -= 24 neg_max[fizz_norm_axis] -= 16 pos_min, pos_max = Vec(bbox_min), Vec(bbox_max) pos_min[fizz_norm_axis] += 16 pos_max[fizz_norm_axis] += 24 if blue_enabled or oran_enabled: neg_trig.solids.append( vmf.make_prism( neg_min, neg_max, mat='tools/toolstrigger', ).solid, ) pos_trig.solids.append( vmf.make_prism( pos_min, pos_max, mat='tools/toolstrigger', ).solid, ) else: # If neither enabled, use one trigger neg_trig.solids.append( vmf.make_prism( neg_min, pos_max, mat='tools/toolstrigger', ).solid, )
def mon_camera_link(vmf: VMF) -> None: """Link cameras to monitors.""" import vbsp if not HAS_MONITOR: return ALL_CAMERAS.sort(key=lambda cam: cam.cam_pos) fog_opt = vbsp.settings['fog'] active_counts = [ srctools.conv_int(cam.inst.fixup['$start_enabled', '0']) for cam in ALL_CAMERAS ] for index, cam in enumerate(ALL_CAMERAS): # type: int, Camera if srctools.conv_int(cam.inst.fixup['$connectioncount']) == 0: continue conn_item = connections.ITEMS[cam.inst['targetname']] # Generate an input to the VScript which turns on/off this camera. # Everything's by index. conn_item.enable_cmd = (Output( '', '@camera', 'RunScriptCode', 'CamEnable({})'.format(index), ), ) conn_item.disable_cmd = (Output( '', '@camera', 'RunScriptCode', 'CamDisable({})'.format(index), ), ) for is_act, cam in zip(active_counts, ALL_CAMERAS): if is_act: start_pos = cam.cam_pos start_angles = cam.cam_angles break else: # No cameras start active, we need to be positioned elsewhere. if options.get(str, 'voice_studio_inst'): # Start at the studio, if it exists. start_pos = get_studio_pose() start_angles = '{:g} {:g} 0'.format( options.get(float, 'voice_studio_cam_pitch'), options.get(float, 'voice_studio_cam_yaw'), ) # If we start at the studio, make the ai_relationships # for turret fire start active. for relation in MONITOR_RELATIONSHIP_ENTS: relation['StartActive'] = '1' else: # Start in arrival_departure_transition_ents... start_pos = Vec(-2500, -2500, 0) start_angles = '0 90 0' cam_ent = vmf.create_ent( classname='point_camera', targetname='@camera', spawnflags='0', # Start on origin=start_pos, angles=start_angles, fov='60', # Copy fog settings from the skybox. fogEnable='1', fogMaxDensity='1', fogColor=fog_opt['primary'], fogStart=fog_opt['start'], fogEnd=fog_opt['end'], ) if not ALL_CAMERAS: return # We only need the script if we're moving at all. cam_ent['vscripts'] = 'BEE2/mon_camera.nut' cam_ent['thinkfunction'] = 'Think' # Now start adding all the variables the script needs. # Tell it the number of cameras, and how many start active. # That lets it trivially determine when they're all off. # We keep the list of active counts to reuse after. active_counts = [ srctools.conv_int(cam.inst.fixup['$start_enabled', '0']) for cam in ALL_CAMERAS ] scriptvar_set(cam_ent, start_pos - (0, 0, 16), 'CAM_NUM', len(ALL_CAMERAS)) scriptvar_set(cam_ent, start_pos - (0, 0, 16), 'CAM_ACTIVE_NUM', sum(active_counts)) # Then add the values for each camera. We can use the setter's modes # to include the position as the actual loc. for i, (cam, active) in enumerate(zip(ALL_CAMERAS, active_counts)): scriptvar_set( cam_ent, cam.cam_pos, 'CAM_LOC', index=i, angles=cam.cam_angles, mode='pos', ) scriptvar_set( cam_ent, cam.cam_pos, 'CAM_ANGLES', index=i, angles=cam.cam_angles, mode='ang', ) scriptvar_set( cam_ent, cam.cam_pos + (0, 0, 8), 'CAM_ACTIVE', index=i, value=active, ) if options.get(str, 'voice_studio_inst'): # We have a voice studio, send values to the script. scriptvar_set(cam_ent, get_studio_pose(), 'CAM_STUDIO_LOC', mode='pos') scriptvar_set( cam_ent, get_studio_pose(), 'CAM_STUDIO_ANG', mode='ang', angles='{:g} {:g} 0'.format( options.get(float, 'voice_studio_cam_pitch'), options.get(float, 'voice_studio_cam_yaw'), ), ) use_turret = '1' if MONITOR_RELATIONSHIP_ENTS else '0' swap_chance = options.get(float, 'voice_studio_inter_chance') else: use_turret = '0' swap_chance = -1 scriptvar_set(cam_ent, start_pos + (0, 0, 16), 'CAM_STUDIO_TURRET', use_turret) scriptvar_set(cam_ent, start_pos + (0, 0, 16), 'CAM_STUDIO_CHANCE', swap_chance)
def res_conveyor_belt(vmf: VMF, inst: Entity, res: Property) -> None: """Create a conveyor belt. * Options: * `SegmentInst`: Generated at each square. (`track` is the name of the path to attach to.) * `TrackTeleport`: Set the track points so they teleport trains to the start. * `Speed`: The fixup or number for the train speed. * `MotionTrig`: If set, a trigger_multiple will be spawned that `EnableMotion`s weighted cubes. The value is the name of the relevant filter. * `EndOutput`: Adds an output to the last track. The value is the same as outputs in VMFs. `RotateSegments`: If true (default), force segments to face in the direction of movement. * `BeamKeys`: If set, a list of keyvalues to use to generate an env_beam travelling from start to end. The origin is treated specially - X is the distance from walls, y is the distance to the side, and z is the height. `RailTemplate`: A template for the track sections. This is made into a non-solid func_brush, combining all sections. * `NoPortalFloor`: If set, add a `func_noportal_volume` on the floor under the track. * `PaintFizzler`: If set, add a paint fizzler underneath the belt. """ move_dist = inst.fixup.int('$travel_distance') if move_dist <= 2: # There isn't room for a conveyor, so don't bother. inst.remove() return move_dir = Vec(1, 0, 0).rotate_by_str(inst.fixup['$travel_direction']) move_dir = move_dir.rotate_by_str(inst['angles']) start_offset = inst.fixup.float('$starting_position') teleport_to_start = res.bool('TrackTeleport', True) segment_inst_file = instanceLocs.resolve_one(res['SegmentInst', '']) rail_template = res['RailTemplate', None] track_speed = res['speed', None] start_pos = Vec.from_str(inst['origin']) end_pos = start_pos + move_dist * move_dir if start_offset > 0: # If an oscillating platform, move to the closest side.. offset = start_offset * move_dir # The instance is placed this far along, so move back to the end. start_pos -= offset end_pos -= offset if start_offset > 0.5: # Swap the direction of movement.. start_pos, end_pos = end_pos, start_pos inst['origin'] = start_pos norm = Vec(z=1).rotate_by_str(inst['angles']) if res.bool('rotateSegments', True): inst['angles'] = angles = move_dir.to_angle_roll(norm) else: angles = Vec.from_str(inst['angles']) # Add the EnableMotion trigger_multiple seen in platform items. # This wakes up cubes when it starts moving. motion_filter = res['motionTrig', None] # Disable on walls, or if the conveyor can't be turned on. if norm != (0, 0, 1) or inst.fixup['$connectioncount'] == '0': motion_filter = None track_name = conditions.local_name(inst, 'segment_{}') rail_temp_solids = [] last_track = None # Place tracks at the top, so they don't appear inside wall sections. track_start: Vec = start_pos + 48 * norm track_end: Vec = end_pos + 48 * norm for index, pos in enumerate(track_start.iter_line(track_end, stride=128), start=1): track = vmf.create_ent( classname='path_track', targetname=track_name.format(index) + '-track', origin=pos, spawnflags=0, orientationtype=0, # Don't rotate ) if track_speed is not None: track['speed'] = track_speed if last_track: last_track['target'] = track['targetname'] if index == 1 and teleport_to_start: track['spawnflags'] = 16 # Teleport here.. last_track = track # Don't place at the last point - it doesn't teleport correctly, # and would be one too many. if segment_inst_file and pos != track_end: seg_inst = vmf.create_ent( classname='func_instance', targetname=track_name.format(index), file=segment_inst_file, origin=pos, angles=angles, ) seg_inst.fixup.update(inst.fixup) if rail_template: temp = template_brush.import_template( vmf, rail_template, pos, angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) rail_temp_solids.extend(temp.world) if rail_temp_solids: vmf.create_ent( classname='func_brush', origin=track_start, spawnflags=1, # Ignore +USE solidity=1, # Not solid vrad_brush_cast_shadows=1, drawinfastreflection=1, ).solids = rail_temp_solids if teleport_to_start: # Link back to the first track.. last_track['target'] = track_name.format(1) + '-track' # Generate an env_beam pointing from the start to the end of the track. try: beam_keys = res.find_key('BeamKeys') except LookupError: pass else: beam = vmf.create_ent(classname='env_beam') beam_off = beam_keys.vec('origin', 0, 63, 56) for prop in beam_keys: beam[prop.real_name] = prop.value # Localise the targetname so it can be triggered.. beam['LightningStart'] = beam['targetname'] = conditions.local_name( inst, beam['targetname', 'beam']) del beam['LightningEnd'] beam['origin'] = start_pos + Vec( -beam_off.x, beam_off.y, beam_off.z, ).rotate(*angles) beam['TargetPoint'] = end_pos + Vec( +beam_off.x, beam_off.y, beam_off.z, ).rotate(*angles) # Allow adding outputs to the last path_track. for prop in res.find_all('EndOutput'): output = Output.parse(prop) output.output = 'OnPass' output.inst_out = None output.comma_sep = False output.target = conditions.local_name(inst, output.target) last_track.add_out(output) if motion_filter is not None: motion_trig = vmf.create_ent( classname='trigger_multiple', targetname=conditions.local_name(inst, 'enable_motion_trig'), origin=start_pos, filtername=motion_filter, startDisabled=1, wait=0.1, ) motion_trig.add_out( Output('OnStartTouch', '!activator', 'ExitDisabledState')) # Match the size of the original... motion_trig.solids.append( vmf.make_prism( start_pos + Vec(72, -56, 58).rotate(*angles), end_pos + Vec(-72, 56, 144).rotate(*angles), mat=consts.Tools.TRIGGER, ).solid) if res.bool('NoPortalFloor'): # Block portals on the floor.. floor_noportal = vmf.create_ent( classname='func_noportal_volume', origin=track_start, ) floor_noportal.solids.append( vmf.make_prism( start_pos + Vec(-60, -60, -66).rotate(*angles), end_pos + Vec(60, 60, -60).rotate(*angles), mat=consts.Tools.INVISIBLE, ).solid) # A brush covering under the platform. base_trig = vmf.make_prism( start_pos + Vec(-64, -64, 48).rotate(*angles), end_pos + Vec(64, 64, 56).rotate(*angles), mat=consts.Tools.INVISIBLE, ).solid vmf.add_brush(base_trig) # Make a paint_cleanser under the belt.. if res.bool('PaintFizzler'): pfizz = vmf.create_ent( classname='trigger_paint_cleanser', origin=start_pos, ) pfizz.solids.append(base_trig.copy()) for face in pfizz.sides(): face.mat = consts.Tools.TRIGGER
def res_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 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) color_var = conditions.resolve_value(inst, color_var).casefold() if color_var == 'white': top_color = template_brush.MAT_TYPES.white elif color_var == 'black': top_color = template_brush.MAT_TYPES.black else: top_color = None for pist_ind in range(1, 5): 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, force_colour=top_color, force_grid='special', no_clumping=True, ) if not static_ent.solids: 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_fizzler_pair(vmf: VMF, begin_inst: Entity, res: Property): """Modify the instance of a fizzler to link with its pair. Each pair will be given a name along the lines of "fizz_name-model1334". Values: - StartInst, EndInst: The instances used for each end - MidInst: An instance placed every 128 units between emitters. - SingleInst: If the models are 1 block apart, replace both with this instance. - BrushKeys, LocalBrushKeys: If specified, a brush entity will be generated from some templates at the position of the models. - StartTemp, EndTemp, SingleTemp: Templates for the above. - SingleBrush: If true, the brush will be shared among the entirety of this fizzler. - uniqueName: If true, all pairs get a unique name for themselves. if False, all instances use the base instance name. """ orig_target = begin_inst['targetname'] if 'modelEnd' in orig_target: return # We only execute starting from the start side. orig_target = orig_target[:-11] # remove "_modelStart" end_name = orig_target + '_modelEnd' # What we search for # The name all these instances get if srctools.conv_bool(res['uniqueName', '1'], True): pair_name = orig_target + '-model' + str(begin_inst.id) else: pair_name = orig_target orig_file = begin_inst['file'] begin_inst['file'] = instanceLocs.resolve_one(res['StartInst'], error=True) end_file = instanceLocs.resolve_one(res['EndInst'], error=True) mid_file = instanceLocs.resolve_one(res['MidInst', '']) single_file = instanceLocs.resolve_one(res['SingleInst', '']) begin_inst['targetname'] = pair_name brush = None if 'brushkeys' in res: begin_temp = res['StartTemp', ''] end_temp = res['EndTemp', ''] single_temp = res['SingleTemp'] if res.bool('SingleBrush'): try: brush = PAIR_FIZZ_BRUSHES[orig_target] except KeyError: pass if not brush: brush = vmf.create_ent( classname='func_brush', # default origin=begin_inst['origin'], ) conditions.set_ent_keys( brush, begin_inst, res, 'BrushKeys', ) if res.bool('SingleBrush'): PAIR_FIZZ_BRUSHES[orig_target] = brush else: begin_temp = end_temp = single_temp = None direction = Vec(0, 0, 1).rotate_by_str(begin_inst['angles']) begin_pos = Vec.from_str(begin_inst['origin']) axis_1, axis_2, main_axis = PAIR_AXES[direction.as_tuple()] for end_inst in vbsp.VMF.by_class['func_instance']: if end_inst['targetname', ''] != end_name: # Only examine this barrier hazard's instances! continue if end_inst['file'] != orig_file: # Allow adding overlays or other instances at the ends. continue end_pos = Vec.from_str(end_inst['origin']) if (begin_pos[axis_1] == end_pos[axis_1] and begin_pos[axis_2] == end_pos[axis_2]): length = int(end_pos[main_axis] - begin_pos[main_axis]) break else: LOGGER.warning('No matching pair for {}!!', orig_target) return if length == 0: if single_temp: temp_brushes = template_brush.import_template( single_temp, Vec.from_str(begin_inst['origin']), Vec.from_str(begin_inst['angles']), force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) brush.solids.extend(temp_brushes.world) if single_file: end_inst.remove() begin_inst['file'] = single_file # Don't do anything else with end instances. return else: if begin_temp: temp_brushes = template_brush.import_template( begin_temp, Vec.from_str(begin_inst['origin']), Vec.from_str(begin_inst['angles']), force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) brush.solids.extend(temp_brushes.world) if end_temp: temp_brushes = template_brush.import_template( end_temp, Vec.from_str(end_inst['origin']), Vec.from_str(end_inst['angles']), force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) brush.solids.extend(temp_brushes.world) end_inst['targetname'] = pair_name end_inst['file'] = end_file if mid_file != '' and length: # Go 64 from each side, and always have at least 1 section # A 128 gap will have length = 0 for dis in range(0, abs(length) + 1, 128): new_pos = begin_pos + direction * dis vbsp.VMF.create_ent( classname='func_instance', targetname=pair_name, angles=begin_inst['angles'], file=mid_file, origin=new_pos, )
def add_voice( voice_attrs: dict, style_vars: dict, vmf: VMF, coll: Collisions, 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!') conditions.add_inst( vmf, targetname='voice', file=INST_PREFIX + quote_base, origin=quote_loc, ) # 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 conditions.add_inst( vmf, targetname='voice_event_' + str(ind), file=file, origin=quote_loc, ) # 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, coll, 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 combine( bsp: BSP, bsp_ents: VMF, pack: PackList, game: Game, *, studiomdl_loc: Path = None, qc_folders: List[Path] = None, crowbar_loc: Optional[Path] = None, decomp_cache_loc: Path = None, auto_range: float = 0, min_cluster: int = 2, debug_tint: bool = False, debug_dump: bool = False, ) -> None: """Combine props in this map.""" # First parse out the bbox ents, so they are always removed. bbox_ents = list(bsp_ents.by_class['comp_propcombine_set']) for ent in bbox_ents: ent.remove() if not studiomdl_loc.exists(): LOGGER.warning('No studioMDL! Cannot propcombine!') return if not qc_folders and decomp_cache_loc is None: # If gameinfo is blah/game/hl2/gameinfo.txt, # QCs should be in blah/content/ according to Valve's scheme. # But allow users to override this. # If Crowbar's path is provided, that means they may want to just supply nothing. qc_folders = [game.path.parent.parent / 'content'] # Parse through all the QC files. LOGGER.info('Parsing QC files. Paths: \n{}', '\n'.join(map(str, qc_folders))) qc_map: Dict[str, Optional[QC]] = {} for qc_folder in qc_folders: load_qcs(qc_map, qc_folder) LOGGER.info('Done! {} props.', len(qc_map)) map_name = Path(bsp.filename).stem # Don't re-parse models continually. mdl_map: Dict[str, Optional[Model]] = {} # Wipe these, if they're being used again. _mesh_cache.clear() _coll_cache.clear() missing_qcs: Set[str] = set() def get_model(filename: str) -> Union[Tuple[QC, Model], Tuple[None, None]]: """Given a filename, load/parse the QC and MDL data. Either both are returned, or neither are. """ key = unify_mdl(filename) try: model = mdl_map[key] except KeyError: try: mdl_file = pack.fsys[filename] except FileNotFoundError: # We don't have this model, we can't combine... return None, None model = mdl_map[key] = Model(pack.fsys, mdl_file) if 'no_propcombine' in model.keyvalues.casefold(): mdl_map[key] = qc_map[key] = None return None, None if model is None or key in missing_qcs: return None, None try: qc = qc_map[key] except KeyError: if crowbar_loc is None: missing_qcs.add(key) return None, None qc = decompile_model(pack.fsys, decomp_cache_loc, crowbar_loc, filename, model.checksum) qc_map[key] = qc if qc is None: return None, None else: return qc, model # Ignore these two, they don't affect our new prop. relevant_flags = ~(StaticPropFlags.HAS_LIGHTING_ORIGIN | StaticPropFlags.DOES_FADE) def get_grouping_key(prop: StaticProp) -> Optional[tuple]: """Compute a grouping key for this prop. Only props with matching key can be possibly combined. If None it cannot be combined. """ qc, model = get_model(prop.model) if model is None or qc is None: return None return ( # Must be first, we pull this out later. frozenset({ tex.casefold().replace('\\', '/') for tex in model.iter_textures([prop.skin]) }), model.flags.value, (prop.flags & relevant_flags).value, model.contents, model.surfaceprop, prop.renderfx, *prop.tint, ) prop_count = 0 # First, construct groups of props that can possibly be combined. prop_groups = defaultdict( list) # type: Dict[Optional[tuple], List[StaticProp]] # This holds the list of all props we want in the map - # combined ones, and any we reject for whatever reason. final_props: List[StaticProp] = [] rejected: List[StaticProp] = [] if bbox_ents: LOGGER.info('Propcombine sets present ({}), combining...', len(bbox_ents)) grouper = group_props_ent( prop_groups, rejected, get_model, bbox_ents, min_cluster, ) elif auto_range > 0: LOGGER.info('Automatically finding propcombine sets...') grouper = group_props_auto( prop_groups, rejected, auto_range, min_cluster, ) else: # No way provided to choose props. LOGGER.info('No propcombine groups provided.') return for prop in bsp.static_props(): prop_groups[get_grouping_key(prop)].append(prop) prop_count += 1 # These are models we cannot merge no matter what - # no source files etc. cannot_merge = prop_groups.pop(None, []) final_props.extend(cannot_merge) LOGGER.debug( 'Prop groups: \n{}', '\n'.join([ f'{group}: {len(props)}' for group, props in sorted(prop_groups.items(), key=operator.itemgetter(0)) ])) group_count = 0 with ModelCompiler( game, studiomdl_loc, pack, map_name, 'propcombine', ) as compiler: for group in grouper: grouped_prop = combine_group(compiler, group, get_model) if debug_tint: # Compute a random hue, and convert back to RGB 0-255. r, g, b = colorsys.hsv_to_rgb(random.random(), 1, 1) grouped_prop.tint = Vec(round(r * 255), round(g * 255), round(b * 255)) final_props.append(grouped_prop) group_count += 1 final_props.extend(rejected) if debug_dump: dump_vmf = VMF() for prop in rejected: dump_vmf.create_ent( 'prop_static', model=prop.model, origin=prop.origin, angles=prop.angles, solid=prop.solidity, rendercolor=prop.tint, ) dump_fname = Path(bsp.filename).with_name(map_name + '_propcombine_reject.vmf') LOGGER.info('Dumping uncombined props to {}...', dump_fname) with dump_fname.open('w') as f: dump_vmf.export(f) LOGGER.info( 'Combined {} props into {}:\n - {} grouped models\n - {} ineligable\n - {} had no group', prop_count, len(final_props), group_count, len(cannot_merge), len(rejected), ) LOGGER.debug('Models with unknown QCs: \n{}', '\n'.join(sorted(missing_qcs))) # If present, delete old cache file. We'll have cleaned up the models. try: os.remove(compiler.model_folder_abs / 'cache.vdf') except FileNotFoundError: pass bsp.write_static_props(final_props)
def res_reshape_fizzler(vmf: VMF, shape_inst: Entity, res: Property): """Convert a fizzler connected via the output to a new shape. This allows for different placing of fizzler items. Each `segment` parameter should be a `x y z;x y z` pair of positions that represent the ends of the fizzler. `up_axis` should be set to a normal vector pointing in the new 'upward' direction. `default` is the ID of a fizzler type which should be used if no outputs are fired. """ shape_name = shape_inst['targetname'] shape_item = connections.ITEMS.pop(shape_name) shape_angles = Vec.from_str(shape_inst['angles']) up_axis = res.vec('up_axis').rotate(*shape_angles) for conn in shape_item.outputs: fizz_item = conn.to_item try: fizz = fizzler.FIZZLERS[fizz_item.name] except KeyError: LOGGER.warning( 'Reshaping fizzler with non-fizzler output ({})! Ignoring!', fizz_item.name) continue fizz.emitters.clear() # Remove old positions. fizz.up_axis = up_axis break else: # No fizzler, so generate a default. # We create the fizzler instance, Fizzler object, and Item object # matching it. # This is hardcoded to use regular Emancipation Fields. base_inst = vmf.create_ent( targetname=shape_name, classname='func_instance', origin=shape_inst['origin'], file=resolve_inst('<ITEM_BARRIER_HAZARD:fizz_base>'), ) base_inst.fixup.update(shape_inst.fixup) fizz = fizzler.FIZZLERS[shape_name] = fizzler.Fizzler( fizzler.FIZZ_TYPES['VALVE_MATERIAL_EMANCIPATION_GRID'], up_axis, base_inst, [], ) fizz_item = connections.Item( base_inst, connections.ITEM_TYPES['item_barrier_hazard'], shape_item.ant_floor_style, shape_item.ant_wall_style, ) connections.ITEMS[shape_name] = fizz_item # Detach this connection and remove traces of it. for conn in list(shape_item.outputs): conn.remove() for coll in [ shape_item.antlines, shape_item.ind_panels, shape_item.shape_signs ]: for ent in coll: ent.remove() coll.clear() for inp in list(shape_item.inputs): inp.to_item = fizz_item fizz_base = fizz.base_inst fizz_base['origin'] = shape_inst['origin'] origin = Vec.from_str(shape_inst['origin']) for seg_prop in res.find_all('Segment'): vec1, vec2 = seg_prop.value.split(';') seg_min_max = Vec.bbox( Vec.from_str(vec1).rotate(*shape_angles) + origin, Vec.from_str(vec2).rotate(*shape_angles) + origin, ) fizz.emitters.append(seg_min_max)
def res_antlaser(vmf: VMF, res: Property): """The condition to generate AntLasers. This is executed once to modify all instances. """ conf_inst = instanceLocs.resolve(res['instance']) conf_glow_height = Vec(z=res.float('GlowHeight', 48) - 64) conf_las_start = Vec(z=res.float('LasStart') - 64) # Grab a copy of the beam spawnflags so we can set our own options. conf_beam_flags = res.find_key('BeamKeys', []).int('spawnflags') # Mask out certain flags. conf_beam_flags &= ( 0 | 1 # Start On | 2 # Toggle | 4 # Random Strike | 8 # Ring | 16 # StartSparks | 32 # EndSparks | 64 # Decal End #| 128 # Shade Start #| 256 # Shade End #| 512 # Taper Out ) conf_outputs = [ Output.parse(prop) for prop in res if prop.name in ('onenabled', 'ondisabled') ] # Find all the markers. nodes = {} # type: Dict[str, Item] for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in conf_inst: continue name = inst['targetname'] try: # Remove the item - it's no longer going to exist after # we're done. nodes[name] = connections.ITEMS.pop(name) except KeyError: raise ValueError('No item for "{}"?'.format(name)) from None # Now find every connected group, recording inputs, outputs and links. todo = set(nodes.values()) groups = [] # type: List[Group] # Node -> is grouped already. node_pairing = dict.fromkeys(nodes.values(), False) while todo: start = todo.pop() # Synthesise the Item used for logic. # We use a random info_target to manage the IO data. group = Group(start) groups.append(group) for node in group.nodes: # If this node has no non-node outputs, destroy the antlines. has_output = False node_pairing[node] = True for conn in list(node.outputs): neighbour = conn.to_item todo.discard(neighbour) pair_state = node_pairing.get(neighbour, None) if pair_state is None: # Not a node, a target of our logic. conn.from_item = group.item has_output = True continue elif pair_state is False: # Another node. group.nodes.append(neighbour) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add((node, neighbour)) # If we have a real output, we need to transfer it. # Otherwise we can just destroy it. if has_output: group.item.antlines.update(node.antlines) group.item.ind_panels.update(node.ind_panels) group.item.shape_signs.extend(node.shape_signs) else: node.delete_antlines() # Do the same for inputs, so we can catch that. for conn in list(node.inputs): neighbour = conn.from_item todo.discard(neighbour) pair_state = node_pairing.get(neighbour, None) if pair_state is None: # Not a node, an input to the group. conn.to_item = group.item continue elif pair_state is False: # Another node. group.nodes.append(neighbour) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add((neighbour, node)) # Now every node is in a group. Generate the actual entities. for group in groups: # We generate two ent types. For each marker, we add a sprite # and a beam pointing at it. Then for each connection # another beam. # Choose a random antlaser name to use for our group. base_name = group.nodes[0].name out_enable = [Output('', '', 'FireUser2')] out_disable = [Output('', '', 'FireUser1')] for output in conf_outputs: if output.output.casefold() == 'onenabled': out_enable.append(output.copy()) else: out_disable.append(output.copy()) group.item.enable_cmd = tuple(out_enable) group.item.disable_cmd = tuple(out_disable) # Node -> index for targetnames. indexes = {} # type: Dict[Item, int] for i, node in enumerate(group.nodes, start=1): indexes[node] = i node.name = base_name # First add the sprite at the right height. sprite_pos = conf_glow_height.copy() sprite_pos.localise( Vec.from_str(node.inst['origin']), Vec.from_str(node.inst['angles']), ) sprite = vmf.create_ent('env_sprite') conditions.set_ent_keys(sprite, node.inst, res, 'GlowKeys') sprite['origin'] = sprite_pos sprite['targetname'] = NAME_SPR(base_name, i) # Now the beam going from below up to the sprite. beam_pos = conf_las_start.copy() beam_pos.localise( Vec.from_str(node.inst['origin']), Vec.from_str(node.inst['angles']), ) beam = vmf.create_ent('env_beam') conditions.set_ent_keys(beam, node.inst, res, 'BeamKeys') beam['origin'] = beam_pos beam['targetname'] = NAME_BEAM_LOW(base_name, i) beam['LightningStart'] = beam['targetname'] beam['LightningEnd'] = NAME_SPR(base_name, i) beam['spawnflags'] = conf_beam_flags | 128 # Shade Start for i, (node1, node2) in enumerate(group.links): beam = vmf.create_ent('env_beam') conditions.set_ent_keys(beam, node1.inst, res, 'BeamKeys') beam['origin'] = node1.inst['origin'] beam['targetname'] = NAME_BEAM_CONN(base_name, i) beam['LightningStart'] = NAME_SPR(base_name, indexes[node1]) beam['LightningEnd'] = NAME_SPR(base_name, indexes[node2]) beam['spawnflags'] = conf_beam_flags return conditions.RES_EXHAUSTED
def generate_fizzlers(vmf: VMF): """Generates fizzler models and the brushes according to their set types. After this is done, fizzler-related conditions will not function correctly. However the model instances are now available for modification. """ from vbsp import MAP_RAND_SEED for fizz in FIZZLERS.values(): if fizz.base_inst not in vmf.entities: continue # The fizzler was removed from the map. fizz_name = fizz.base_inst['targetname'] fizz_type = fizz.fizz_type # Static versions are only used for fizzlers which start on. # Permanently-off fizzlers are kinda useless, so we don't need # to bother optimising for it. is_static = bool( fizz.base_inst.fixup.int('$connectioncount', 0) == 0 and fizz.base_inst.fixup.bool('$start_enabled', 1)) pack_list = (fizz.fizz_type.pack_lists_static if is_static else fizz.fizz_type.pack_lists) for pack in pack_list: packing.pack_list(vmf, pack) if fizz_type.inst[FizzInst.BASE, is_static]: random.seed('{}_fizz_base_{}'.format(MAP_RAND_SEED, fizz_name)) fizz.base_inst['file'] = random.choice( fizz_type.inst[FizzInst.BASE, is_static]) if not fizz.emitters: LOGGER.warning('No emitters for fizzler "{}"!', fizz_name) continue # Brush index -> entity for ones that need to merge. # template_brush is used for the templated one. single_brushes = {} # type: Dict[FizzlerBrush, Entity] if fizz_type.temp_max or fizz_type.temp_min: template_brush_ent = vmf.create_ent( classname='func_brush', origin=fizz.base_inst['origin'], ) conditions.set_ent_keys( template_brush_ent, fizz.base_inst, fizz_type.temp_brush_keys, ) else: template_brush_ent = None up_dir = fizz.up_axis forward = (fizz.emitters[0][1] - fizz.emitters[0][0]).norm() min_angles = FIZZ_ANGLES[forward.as_tuple(), up_dir.as_tuple()] max_angles = FIZZ_ANGLES[(-forward).as_tuple(), up_dir.as_tuple()] model_min = (fizz_type.inst[FizzInst.PAIR_MIN, is_static] or fizz_type.inst[FizzInst.ALL, is_static]) model_max = (fizz_type.inst[FizzInst.PAIR_MAX, is_static] or fizz_type.inst[FizzInst.ALL, is_static]) if not model_min or not model_max: raise ValueError( 'No model specified for one side of "{}"' ' fizzlers'.format(fizz_type.id), ) # Define a function to do the model names. model_index = 0 if fizz_type.model_naming is ModelName.SAME: def get_model_name(ind): """Give every emitter the base's name.""" return fizz_name elif fizz_type.model_naming is ModelName.LOCAL: def get_model_name(ind): """Give every emitter a name local to the base.""" return fizz_name + '-' + fizz_type.model_name elif fizz_type.model_naming is ModelName.PAIRED: def get_model_name(ind): """Give each pair of emitters the same unique name.""" return '{}-{}{:02}'.format( fizz_name, fizz_type.model_name, ind, ) elif fizz_type.model_naming is ModelName.UNIQUE: def get_model_name(ind): """Give every model a unique name.""" nonlocal model_index model_index += 1 return '{}-{}{:02}'.format( fizz_name, fizz_type.model_name, model_index, ) else: raise ValueError('Bad ModelName?') # Generate env_beam pairs. for beam in fizz_type.beams: beam_template = Entity(vmf) conditions.set_ent_keys(beam_template, fizz.base_inst, beam.keys) beam_template['classname'] = 'env_beam' del beam_template[ 'LightningEnd'] # Don't allow users to set end pos. name = beam_template['targetname'] + '_' counter = 1 for seg_min, seg_max in fizz.emitters: for offset in beam.offset: # type: Vec min_off = offset.copy() max_off = offset.copy() min_off.localise(seg_min, min_angles) max_off.localise(seg_max, max_angles) beam_ent = beam_template.copy() vmf.add_ent(beam_ent) # Allow randomising speed and direction. if 0 < beam.speed_min < beam.speed_max: random.seed('{}{}{}'.format(MAP_RAND_SEED, min_off, max_off)) beam_ent['TextureScroll'] = random.randint( beam.speed_min, beam.speed_max) if random.choice((False, True)): # Flip to reverse direction. min_off, max_off = max_off, min_off beam_ent['origin'] = min_off beam_ent['LightningStart'] = beam_ent['targetname'] = ( name + str(counter)) counter += 1 beam_ent['targetpoint'] = max_off mat_mod_tex = {} # type: Dict[FizzlerBrush, Set[str]] for brush_type in fizz_type.brushes: if brush_type.mat_mod_var is not None: mat_mod_tex[brush_type] = set() # Record the data for trigger hurts so flinch triggers can match them. trigger_hurt_name = '' trigger_hurt_start_disabled = '0' for seg_ind, (seg_min, seg_max) in enumerate(fizz.emitters, start=1): length = (seg_max - seg_min).mag() random.seed('{}_fizz_{}'.format(MAP_RAND_SEED, seg_min)) if length == 128 and fizz_type.inst[FizzInst.PAIR_SINGLE, is_static]: min_inst = vmf.create_ent( targetname=get_model_name(seg_ind), classname='func_instance', file=random.choice(fizz_type.inst[FizzInst.PAIR_SINGLE, is_static]), origin=(seg_min + seg_max) / 2, angles=min_angles, ) else: # Both side models. min_inst = vmf.create_ent( targetname=get_model_name(seg_ind), classname='func_instance', file=random.choice(model_min), origin=seg_min, angles=min_angles, ) random.seed('{}_fizz_{}'.format(MAP_RAND_SEED, seg_max)) max_inst = vmf.create_ent( targetname=get_model_name(seg_ind), classname='func_instance', file=random.choice(model_max), origin=seg_max, angles=max_angles, ) max_inst.fixup.update(fizz.base_inst.fixup) min_inst.fixup.update(fizz.base_inst.fixup) if fizz_type.inst[FizzInst.GRID, is_static]: # Generate one instance for each position. # Go 64 from each side, and always have at least 1 section # A 128 gap will have length = 0 for ind, dist in enumerate(range(64, round(length) - 63, 128)): mid_pos = seg_min + forward * dist random.seed('{}_fizz_mid_{}'.format( MAP_RAND_SEED, mid_pos)) mid_inst = vmf.create_ent( classname='func_instance', targetname=fizz_name, angles=min_angles, file=random.choice(fizz_type.inst[FizzInst.GRID, is_static]), origin=mid_pos, ) mid_inst.fixup.update(fizz.base_inst.fixup) if template_brush_ent is not None: if length == 128 and fizz_type.temp_single: temp = template_brush.import_template( fizz_type.temp_single, (seg_min + seg_max) / 2, min_angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) template_brush_ent.solids.extend(temp.world) else: if fizz_type.temp_min: temp = template_brush.import_template( fizz_type.temp_min, seg_min, min_angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) template_brush_ent.solids.extend(temp.world) if fizz_type.temp_max: temp = template_brush.import_template( fizz_type.temp_max, seg_max, max_angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) template_brush_ent.solids.extend(temp.world) # Generate the brushes. for brush_type in fizz_type.brushes: brush_ent = None # If singular, we reuse the same brush ent for all the segments. if brush_type.singular: brush_ent = single_brushes.get(brush_type, None) # Non-singular or not generated yet - make the entity. if brush_ent is None: brush_ent = vmf.create_ent(classname='func_brush') for key_name, key_value in brush_type.keys.items(): brush_ent[key_name] = conditions.resolve_value( fizz.base_inst, key_value) for key_name, key_value in brush_type.local_keys.items(): brush_ent[key_name] = conditions.local_name( fizz.base_inst, conditions.resolve_value( fizz.base_inst, key_value, )) brush_ent['targetname'] = conditions.local_name( fizz.base_inst, brush_type.name, ) # Set this to the center, to make sure it's not going to leak. brush_ent['origin'] = (seg_min + seg_max) / 2 # For fizzlers flat on the floor/ceiling, scanlines look # useless. Turn them off. if 'usescanline' in brush_ent and fizz.normal().z: brush_ent['UseScanline'] = 0 if brush_ent['classname'] == 'trigger_hurt': trigger_hurt_name = brush_ent['targetname'] trigger_hurt_start_disabled = brush_ent[ 'startdisabled'] if brush_type.set_axis_var: brush_ent['vscript_init_code'] = ( 'axis <- `{}`;'.format(fizz.normal().axis(), )) for out in brush_type.outputs: new_out = out.copy() new_out.target = conditions.local_name( fizz.base_inst, new_out.target, ) brush_ent.add_out(new_out) if brush_type.singular: # Record for the next iteration. single_brushes[brush_type] = brush_ent # If we have a material_modify_control to generate, # we need to parent it to ourselves to restrict it to us # only. We also need one for each material, so provide a # function to the generator which adds to a set. if brush_type.mat_mod_var is not None: used_tex_func = mat_mod_tex[brush_type].add else: def used_tex_func(val): """If not, ignore those calls.""" return None # Generate the brushes and texture them. brush_ent.solids.extend( brush_type.generate( vmf, fizz, seg_min, seg_max, used_tex_func, )) if trigger_hurt_name: fizz.gen_flinch_trigs( vmf, trigger_hurt_name, trigger_hurt_start_disabled, ) # If we have the config, but no templates used anywhere... if template_brush_ent is not None and not template_brush_ent.solids: template_brush_ent.remove() for brush_type, used_tex in mat_mod_tex.items(): brush_name = conditions.local_name(fizz.base_inst, brush_type.name) mat_mod_name = conditions.local_name(fizz.base_inst, brush_type.mat_mod_name) for off, tex in zip(MATMOD_OFFSETS, sorted(used_tex)): pos = off.copy().rotate(*min_angles) pos += Vec.from_str(fizz.base_inst['origin']) vmf.create_ent( classname='material_modify_control', origin=pos, targetname=mat_mod_name, materialName='materials/' + tex + '.vmt', materialVar=brush_type.mat_mod_var, parentname=brush_name, )
def res_resizeable_trigger(vmf: VMF, res: Property): """Replace two markers with a trigger brush. This is run once to affect all of an item. Options: * `markerInst`: <ITEM_ID:1,2> value referencing the marker instances, or a filename. * `markerItem`: The item's ID * `previewConf`: A item config which enables/disables the preview overlay. * `previewInst`: An instance to place at the marker location in preview mode. This should contain checkmarks to display the value when testing. * `previewMat`: If set, the material to use for an overlay func_brush. The brush will be parented to the trigger, so it vanishes once killed. It is also non-solid. * `previewScale`: The scale for the func_brush materials. * `previewActivate`, `previewDeactivate`: The VMF output to turn the previewInst on and off. * `triggerActivate, triggerDeactivate`: The `instance:name;Output` outputs used when the trigger turns on or off. * `coopVar`: The instance variable which enables detecting both Coop players. The trigger will be a trigger_playerteam. * `coopActivate, coopDeactivate`: The `instance:name;Output` outputs used when coopVar is enabled. These should be suitable for a logic_coop_manager. * `coopOnce`: If true, kill the manager after it first activates. * `keys`: A block of keyvalues for the trigger brush. Origin and targetname will be set automatically. * `localkeys`: The same as above, except values will be changed to use instance-local names. """ marker = instanceLocs.resolve(res['markerInst']) marker_names = set() for inst in vmf.by_class['func_instance']: if inst['file'].casefold() in marker: marker_names.add(inst['targetname']) # Unconditionally delete from the map, so it doesn't # appear even if placed wrongly. inst.remove() if not marker_names: # No markers in the map - abort return RES_EXHAUSTED item_id = res['markerItem'] # Synthesise the item type used for the final trigger. item_type_sp = connections.ItemType( id=item_id + ':TRIGGER', output_act=Output.parse_name(res['triggerActivate', 'OnStartTouchAll']), output_deact=Output.parse_name(res['triggerDeactivate', 'OnEndTouchAll']), ) # For Coop, we add a logic_coop_manager in the mix so both players can # be handled. try: coop_var = res['coopVar'] except LookupError: coop_var = item_type_coop = None coop_only_once = False else: coop_only_once = res.bool('coopOnce') item_type_coop = connections.ItemType( id=item_id + ':TRIGGER_COOP', output_act=Output.parse_name( res['coopActivate', 'OnChangeToAllTrue'] ), output_deact=Output.parse_name( res['coopDeactivate', 'OnChangeToAnyFalse'] ), ) # Display preview overlays if it's preview mode, and the config is true pre_act = pre_deact = None if vbsp.IS_PREVIEW and vbsp_options.get_itemconf(res['previewConf', ''], False): preview_mat = res['previewMat', ''] preview_inst_file = res['previewInst', ''] preview_scale = res.float('previewScale', 0.25) # None if not found. with suppress(LookupError): pre_act = Output.parse(res.find_key('previewActivate')) with suppress(LookupError): pre_deact = Output.parse(res.find_key('previewDeactivate')) else: # Deactivate the preview_ options when publishing. preview_mat = preview_inst_file = '' preview_scale = 0.25 # Now go through each brush. # We do while + pop to allow removing both names each loop through. todo_names = set(marker_names) while todo_names: targ = todo_names.pop() mark1 = connections.ITEMS.pop(targ) for conn in mark1.outputs: if conn.to_item.name in marker_names: mark2 = conn.to_item conn.remove() # Delete this connection. todo_names.discard(mark2.name) del connections.ITEMS[mark2.name] break else: if not mark1.inputs: # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 1-block trigger. mark2 = mark1 else: # It's a marker with an input, the other in the pair # will handle everything. # But reinstate it in ITEMS. connections.ITEMS[targ] = mark1 continue inst1 = mark1.inst inst2 = mark2.inst is_coop = coop_var is not None and vbsp.GAME_MODE == 'COOP' and ( inst1.fixup.bool(coop_var) or inst2.fixup.bool(coop_var) ) bbox_min, bbox_max = Vec.bbox( Vec.from_str(inst1['origin']), Vec.from_str(inst2['origin']) ) origin = (bbox_max + bbox_min) / 2 # Extend to the edge of the blocks. bbox_min -= 64 bbox_max += 64 out_ent = trig_ent = vmf.create_ent( classname='trigger_multiple', # Default targetname=targ, origin=origin, angles='0 0 0', ) trig_ent.solids = [ vmf.make_prism( bbox_min, bbox_max, mat=const.Tools.TRIGGER, ).solid, ] # Use 'keys' and 'localkeys' blocks to set all the other keyvalues. conditions.set_ent_keys(trig_ent, inst, res) if is_coop: trig_ent['spawnflags'] = '1' # Clients trig_ent['classname'] = 'trigger_playerteam' out_ent = manager = vmf.create_ent( classname='logic_coop_manager', targetname=conditions.local_name(inst, 'man'), origin=origin, ) item = connections.Item( out_ent, item_type_coop, mark1.ant_floor_style, mark1.ant_wall_style, ) if coop_only_once: # Kill all the ents when both players are present. manager.add_out( Output('OnChangeToAllTrue', manager, 'Kill'), Output('OnChangeToAllTrue', targ, 'Kill'), ) trig_ent.add_out( Output('OnStartTouchBluePlayer', manager, 'SetStateATrue'), Output('OnStartTouchOrangePlayer', manager, 'SetStateBTrue'), Output('OnEndTouchBluePlayer', manager, 'SetStateAFalse'), Output('OnEndTouchOrangePlayer', manager, 'SetStateBFalse'), ) else: item = connections.Item( trig_ent, item_type_sp, mark1.ant_floor_style, mark1.ant_wall_style, ) # Register, and copy over all the antlines. connections.ITEMS[item.name] = item item.ind_panels = mark1.ind_panels | mark2.ind_panels item.antlines = mark1.antlines | mark2.antlines item.shape_signs = mark1.shape_signs + mark2.shape_signs if preview_mat: preview_brush = vmf.create_ent( classname='func_brush', parentname=targ, origin=origin, Solidity='1', # Not solid drawinfastreflection='1', # Draw in goo.. # Disable shadows and lighting.. disableflashlight='1', disablereceiveshadows='1', disableshadowdepth='1', disableshadows='1', ) preview_brush.solids = [ # Make it slightly smaller, so it doesn't z-fight with surfaces. vmf.make_prism( bbox_min + 0.5, bbox_max - 0.5, mat=preview_mat, ).solid, ] for face in preview_brush.sides(): face.scale = preview_scale if preview_inst_file: pre_inst = vmf.create_ent( classname='func_instance', targetname=targ + '_preview', file=preview_inst_file, # Put it at the second marker, since that's usually # closest to antlines if present. origin=inst2['origin'], ) if pre_act is not None: out = pre_act.copy() out.inst_out, out.output = item.output_act() out.target = conditions.local_name(pre_inst, out.target) out_ent.add_out(out) if pre_deact is not None: out = pre_deact.copy() out.inst_out, out.output = item.output_deact() out.target = conditions.local_name(pre_inst, out.target) out_ent.add_out(out) for conn in mark1.outputs | mark2.outputs: conn.from_item = item return RES_EXHAUSTED
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 res_antlaser(vmf: VMF, res: Property): """The condition to generate AntLasers. This is executed once to modify all instances. """ conf_inst = instanceLocs.resolve(res['instance']) conf_glow_height = Vec(z=res.float('GlowHeight', 48) - 64) conf_las_start = Vec(z=res.float('LasStart') - 64) conf_rope_off = res.vec('RopePos') conf_toggle_targ = res['toggleTarg', ''] beam_conf = res.find_key('BeamKeys', []) glow_conf = res.find_key('GlowKeys', []) cable_conf = res.find_key('CableKeys', []) if beam_conf: # Grab a copy of the beam spawnflags so we can set our own options. conf_beam_flags = beam_conf.int('spawnflags') # Mask out certain flags. conf_beam_flags &= ( 0 | 1 # Start On | 2 # Toggle | 4 # Random Strike | 8 # Ring | 16 # StartSparks | 32 # EndSparks | 64 # Decal End #| 128 # Shade Start #| 256 # Shade End #| 512 # Taper Out ) else: conf_beam_flags = 0 conf_outputs = [ Output.parse(prop) for prop in res if prop.name in ('onenabled', 'ondisabled') ] # Find all the markers. nodes = {} # type: Dict[str, Item] for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in conf_inst: continue name = inst['targetname'] try: # Remove the item - it's no longer going to exist after # we're done. nodes[name] = connections.ITEMS.pop(name) except KeyError: raise ValueError('No item for "{}"?'.format(name)) from None if not nodes: # None at all. return conditions.RES_EXHAUSTED # Now find every connected group, recording inputs, outputs and links. todo = set(nodes.values()) groups = [] # type: List[Group] # Node -> is grouped already. node_pairing = dict.fromkeys(nodes.values(), False) while todo: start = todo.pop() # Synthesise the Item used for logic. # We use a random info_target to manage the IO data. group = Group(start) groups.append(group) for node in group.nodes: # If this node has no non-node outputs, destroy the antlines. has_output = False node_pairing[node] = True for conn in list(node.outputs): neighbour = conn.to_item todo.discard(neighbour) pair_state = node_pairing.get(neighbour, None) if pair_state is None: # Not a node, a target of our logic. conn.from_item = group.item has_output = True continue elif pair_state is False: # Another node. group.nodes.append(neighbour) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add(frozenset({node, neighbour})) # If we have a real output, we need to transfer it. # Otherwise we can just destroy it. if has_output: node.transfer_antlines(group.item) else: node.delete_antlines() # Do the same for inputs, so we can catch that. for conn in list(node.inputs): neighbour = conn.from_item todo.discard(neighbour) pair_state = node_pairing.get(neighbour, None) if pair_state is None: # Not a node, an input to the group. conn.to_item = group.item continue elif pair_state is False: # Another node. group.nodes.append(neighbour) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add(frozenset({neighbour, node})) # Now every node is in a group. Generate the actual entities. for group in groups: # We generate two ent types. For each marker, we add a sprite # and a beam pointing at it. Then for each connection # another beam. # Choose a random antlaser name to use for our group. base_name = group.nodes[0].name out_enable = [Output('', '', 'FireUser2')] out_disable = [Output('', '', 'FireUser1')] for output in conf_outputs: if output.output.casefold() == 'onenabled': out_enable.append(output.copy()) else: out_disable.append(output.copy()) if conf_toggle_targ: # Make the group info_target into a texturetoggle. toggle = group.item.inst toggle['classname'] = 'env_texturetoggle' toggle['target'] = conditions.local_name(group.nodes[0].inst, conf_toggle_targ) group.item.enable_cmd = tuple(out_enable) group.item.disable_cmd = tuple(out_disable) # Node -> index for targetnames. indexes = {} # type: Dict[Item, int] # For cables, it's a bit trickier than the beams. # The cable ent itself is the one which decides what it links to, # so we need to potentially make endpoint cables at locations with # only "incoming" lines. # So this dict is either a targetname to indicate cables with an # outgoing connection, or the entity for endpoints without an outgoing # connection. cable_points = {} # type: Dict[Item, Union[Entity, str]] for i, node in enumerate(group.nodes, start=1): indexes[node] = i node.name = base_name sprite_pos = conf_glow_height.copy() sprite_pos.localise( Vec.from_str(node.inst['origin']), Vec.from_str(node.inst['angles']), ) if glow_conf: # First add the sprite at the right height. sprite = vmf.create_ent('env_sprite') for prop in glow_conf: sprite[prop.name] = conditions.resolve_value(node.inst, prop.value) sprite['origin'] = sprite_pos sprite['targetname'] = NAME_SPR(base_name, i) elif beam_conf: # If beams but not sprites, we need a target. vmf.create_ent( 'info_target', origin=sprite_pos, targetname=NAME_SPR(base_name, i), ) if beam_conf: # Now the beam going from below up to the sprite. beam_pos = conf_las_start.copy() beam_pos.localise( Vec.from_str(node.inst['origin']), Vec.from_str(node.inst['angles']), ) beam = vmf.create_ent('env_beam') for prop in beam_conf: beam[prop.name] = conditions.resolve_value(node.inst, prop.value) beam['origin'] = beam['targetpoint'] = beam_pos beam['targetname'] = NAME_BEAM_LOW(base_name, i) beam['LightningStart'] = beam['targetname'] beam['LightningEnd'] = NAME_SPR(base_name, i) beam['spawnflags'] = conf_beam_flags | 128 # Shade Start if beam_conf: for i, (node_a, node_b) in enumerate(group.links): beam = vmf.create_ent('env_beam') conditions.set_ent_keys(beam, node_a.inst, res, 'BeamKeys') beam['origin'] = beam['targetpoint'] = node_a.inst['origin'] beam['targetname'] = NAME_BEAM_CONN(base_name, i) beam['LightningStart'] = NAME_SPR(base_name, indexes[node_a]) beam['LightningEnd'] = NAME_SPR(base_name, indexes[node_b]) beam['spawnflags'] = conf_beam_flags # We have a couple different situations to deal with here. # Either end could Not exist, be Unlinked, or be Linked = 8 combos. # Always flip so we do A to B. # AB | # NN | Make 2 new ones, one is an endpoint. # NU | Flip, do UN. # NL | Make A, link A to B. Both are linked. # UN | Make B, link A to B. B is unlinked. # UU | Link A to B, A is now linked, B is unlinked. # UL | Link A to B. Both are linked. # LN | Flip, do NL. # LU | Flip, do UL # LL | Make A, link A to B. Both are linked. if cable_conf: rope_ind = 0 # Uniqueness value. for node_a, node_b in group.links: state_a, ent_a = RopeState.from_node(cable_points, node_a) state_b, ent_b = RopeState.from_node(cable_points, node_b) if (state_a is RopeState.LINKED or (state_a is RopeState.NONE and state_b is RopeState.UNLINKED) ): # Flip these, handle the opposite order. state_a, state_b = state_b, state_a ent_a, ent_b = ent_b, ent_a node_a, node_b = node_b, node_a pos_a = conf_rope_off.copy() pos_a.localise( Vec.from_str(node_a.inst['origin']), Vec.from_str(node_a.inst['angles']), ) pos_b = conf_rope_off.copy() pos_b.localise( Vec.from_str(node_b.inst['origin']), Vec.from_str(node_b.inst['angles']), ) # Need to make the A rope if we don't have one that's unlinked. if state_a is not RopeState.UNLINKED: rope_a = vmf.create_ent('move_rope') for prop in beam_conf: rope_a[prop.name] = conditions.resolve_value(node_a.inst, prop.value) rope_a['origin'] = pos_a rope_ind += 1 rope_a['targetname'] = NAME_CABLE(base_name, rope_ind) else: # It is unlinked, so it's the rope to use. rope_a = ent_a # Only need to make the B rope if it doesn't have one. if state_b is RopeState.NONE: rope_b = vmf.create_ent('move_rope') for prop in beam_conf: rope_b[prop.name] = conditions.resolve_value(node_b.inst, prop.value) rope_b['origin'] = pos_b rope_ind += 1 name_b = rope_b['targetname'] = NAME_CABLE(base_name, rope_ind) cable_points[node_b] = rope_b # Someone can use this. elif state_b is RopeState.UNLINKED: # Both must be unlinked, we aren't using this link though. name_b = ent_b['targetname'] else: # Linked, we just have the name. name_b = ent_b # By here, rope_a should be an unlinked rope, # and name_b should be a name to link to. rope_a['nextkey'] = name_b # Figure out how much slack to give. # If on floor, we need to be taut to have clearance. if on_floor(node_a) or on_floor(node_b): rope_a['slack'] = 60 else: rope_a['slack'] = 125 # We're always linking A to B, so A is always linked! if state_a is not RopeState.LINKED: cable_points[node_a] = rope_a['targetname'] return conditions.RES_EXHAUSTED
def res_make_tag_fizzler(vmf: VMF, inst: Entity, res: Property): """Add an Aperture Tag Paint Gun activation fizzler. These fizzlers are created via signs, and work very specially. MUST be priority -100 so it runs before fizzlers! """ import vbsp if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']: # Abort - TAG fizzlers shouldn't appear in any other game! inst.remove() return fizz_base = fizz_name = None # Look for the fizzler instance we want to replace for targetname in inst.output_targets(): if targetname in tag_fizzlers: fizz_name = targetname fizz_base = tag_fizzlers[targetname] del tag_fizzlers[targetname] # Don't let other signs mod this one! continue else: # It's an indicator toggle, remove it and the antline to clean up. LOGGER.warning('Toggle: {}', targetname) for ent in vmf.by_target[targetname]: remove_ant_toggle(ent) inst.outputs.clear() # Remove the outptuts now, they're not valid anyway. if fizz_base is None: # No fizzler - remove this sign inst.remove() return # The distance from origin the double signs are seperated by. sign_offset = res.int('signoffset', 16) sign_loc = ( # The actual location of the sign - on the wall Vec.from_str(inst['origin']) + Vec(0, 0, -64).rotate_by_str(inst['angles']) ) # Now deal with the visual aspect: # Blue signs should be on top. blue_enabled = inst.fixup.bool('$start_enabled') oran_enabled = inst.fixup.bool('$start_reversed') # If True, single-color signs will also turn off the other color. # This also means we always show both signs. # If both are enabled or disabled, this has no effect. disable_other = ( not inst.fixup.bool('$disable_autorespawn', True) and blue_enabled != oran_enabled ) # Delete fixups now, they aren't useful. inst.fixup.clear() if not blue_enabled and not oran_enabled: # Hide the sign in this case! inst.remove() inst_angle = srctools.parse_vec_str(inst['angles']) inst_normal = Vec(0, 0, 1).rotate(*inst_angle) loc = Vec.from_str(inst['origin']) if disable_other or (blue_enabled and oran_enabled): inst['file'] = res['frame_double'] # On a wall, and pointing vertically if inst_normal.z == 0 and Vec(y=1).rotate(*inst_angle).z: # They're vertical, make sure blue's on top! blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset) oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset) # If orange is enabled, with two frames put that on top # instead since it's more important if disable_other and oran_enabled: blue_loc, oran_loc = oran_loc, blue_loc else: offset = Vec(0, sign_offset, 0).rotate(*inst_angle) blue_loc = loc + offset oran_loc = loc - offset else: inst['file'] = res['frame_single'] # They're always centered blue_loc = loc oran_loc = loc if inst_normal.z != 0: # If on floors/ceilings, rotate to point at the fizzler! sign_floor_loc = sign_loc.copy() sign_floor_loc.z = 0 # We don't care about z-positions. # Grab the data saved earlier in res_find_potential_tag_fizzlers() axis, side_min, side_max, normal = tag_fizzler_locs[fizz_name] # The Z-axis fizzler (horizontal) must be treated differently. if axis == 'z': # For z-axis, just compare to the center point. # The values are really x, y, z, not what they're named. sign_dir = sign_floor_loc - (side_min, side_max, normal) else: # For the other two, we compare to the line, # or compare to the closest side (in line with the fizz) other_axis = 'x' if axis == 'y' else 'y' if abs(sign_floor_loc[other_axis] - normal) < 32: # Compare to the closest side. Use ** to swap x/y arguments # appropriately. The closest side is the one with the # smallest magnitude. vmf.create_ent( classname='info_null', targetname=inst['targetname'] + '_min', origin=sign_floor_loc - Vec(**{ axis: side_min, other_axis: normal, }), ) vmf.create_ent( classname='info_null', targetname=inst['targetname'] + '_max', origin=sign_floor_loc - Vec(**{ axis: side_max, other_axis: normal, }), ) sign_dir = min( sign_floor_loc - Vec(**{ axis: side_min, other_axis: normal, }), sign_floor_loc - Vec(**{ axis: side_max, other_axis: normal, }), key=Vec.mag, ) else: # Align just based on whether we're in front or behind. sign_dir = Vec() sign_dir[other_axis] = sign_floor_loc[other_axis] - normal sign_angle = math.degrees( math.atan2(sign_dir.y, sign_dir.x) ) # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals sign_angle = (sign_angle + 45) // 90 * 90 # Rotate to fit the instances - south is down sign_angle = int(sign_angle + 90) % 360 if inst_normal.z > 0: sign_angle = '0 {} 0'.format(sign_angle) elif inst_normal.z < 0: # Flip upside-down for ceilings sign_angle = '0 {} 180'.format(sign_angle) else: # On a wall, face upright sign_angle = PETI_INST_ANGLE[inst_normal.as_tuple()] # If disable_other, we show off signs. Otherwise we don't use that sign. blue_sign = 'blue_sign' if blue_enabled else 'blue_off_sign' if disable_other else None oran_sign = 'oran_sign' if oran_enabled else 'oran_off_sign' if disable_other else None if blue_sign: vmf.create_ent( classname='func_instance', file=res[blue_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=blue_loc.join(' '), ) if oran_sign: vmf.create_ent( classname='func_instance', file=res[oran_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=oran_loc.join(' '), ) # Now modify the fizzler... fizz_brushes = list( vmf.by_class['trigger_portal_cleanser'] & vmf.by_target[fizz_name + '_brush'] ) if 'base_inst' in res: fizz_base['file'] = resolve_inst(res['base_inst'])[0] fizz_base.outputs.clear() # Remove outputs, otherwise they break # branch_toggle entities # Subtract the sign from the list of connections, but don't go below # zero fizz_base.fixup['$connectioncount'] = str(max( 0, srctools.conv_int(fizz_base.fixup['$connectioncount', ''], 0) - 1 )) if 'model_inst' in res: model_inst = resolve_inst(res['model_inst'])[0] for mdl_inst in vmf.by_class['func_instance']: if mdl_inst['targetname', ''].startswith(fizz_name + '_model'): mdl_inst['file'] = model_inst # Find the direction the fizzler front/back points - z=floor fizz # Signs will associate with the given side! bbox_min, bbox_max = fizz_brushes[0].get_bbox() for axis, val in zip('xyz', bbox_max-bbox_min): if val == 2: fizz_axis = axis sign_center = (bbox_min[axis] + bbox_max[axis]) / 2 break else: # A fizzler that's not 128*x*2? raise Exception('Invalid fizzler brush ({})!'.format(fizz_name)) # Figure out what the sides will set values to... pos_blue = False pos_oran = False neg_blue = False neg_oran = False if sign_loc[fizz_axis] < sign_center: pos_blue = blue_enabled pos_oran = oran_enabled else: neg_blue = blue_enabled neg_oran = oran_enabled fizz_off_tex = { 'left': res['off_left'], 'center': res['off_center'], 'right': res['off_right'], 'short': res['off_short'], } fizz_on_tex = { 'left': res['on_left'], 'center': res['on_center'], 'right': res['on_right'], 'short': res['on_short'], } # If it activates the paint gun, use different textures if pos_blue or pos_oran: pos_tex = fizz_on_tex else: pos_tex = fizz_off_tex if neg_blue or neg_oran: neg_tex = fizz_on_tex else: neg_tex = fizz_off_tex if vbsp.GAME_MODE == 'COOP': # We need ATLAS-specific triggers pos_trig = vmf.create_ent( classname='trigger_playerteam', ) neg_trig = vmf.create_ent( classname='trigger_playerteam', ) output = 'OnStartTouchBluePlayer' else: pos_trig = vmf.create_ent( classname='trigger_multiple', ) neg_trig = vmf.create_ent( classname='trigger_multiple', spawnflags='1', ) output = 'OnStartTouch' pos_trig['origin'] = neg_trig['origin'] = fizz_base['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = fizz_name + '-trig_pos' neg_trig['targetname'] = fizz_name + '-trig_neg' pos_trig.outputs = [ Output( output, fizz_name + '-trig_neg', 'Enable', ), Output( output, fizz_name + '-trig_pos', 'Disable', ), ] neg_trig.outputs = [ Output( output, fizz_name + '-trig_pos', 'Enable', ), Output( output, fizz_name + '-trig_neg', 'Disable', ), ] voice_attr = vbsp.settings['has_attr'] if blue_enabled or disable_other: # If this is blue/oran only, don't affect the other color neg_trig.outputs.append(Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_blue), )) pos_trig.outputs.append(Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_blue), )) if blue_enabled: # Add voice attributes - we have the gun and gel! voice_attr['bluegelgun'] = True voice_attr['bluegel'] = True voice_attr['bouncegun'] = True voice_attr['bouncegel'] = True if oran_enabled or disable_other: neg_trig.outputs.append(Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_oran), )) pos_trig.outputs.append(Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_oran), )) if oran_enabled: voice_attr['orangegelgun'] = True voice_attr['orangegel'] = True voice_attr['speedgelgun'] = True voice_attr['speedgel'] = True if not oran_enabled and not blue_enabled: # If both are disabled, we must shutdown the gun when touching # either side - use neg_trig for that purpose! # We want to get rid of pos_trig to save ents vmf.remove_ent(pos_trig) neg_trig['targetname'] = fizz_name + '-trig' neg_trig.outputs.clear() neg_trig.add_out(Output( output, '@BlueIsEnabled', 'SetValue', param='0' )) neg_trig.add_out(Output( output, '@OrangeIsEnabled', 'SetValue', param='0' )) for fizz_brush in fizz_brushes: # portal_cleanser ent, not solid! # Modify fizzler textures bbox_min, bbox_max = fizz_brush.get_bbox() for side in fizz_brush.sides(): norm = side.normal() if norm[fizz_axis] == 0: # Not the front/back: force nodraw # Otherwise the top/bottom will have the odd stripes # which won't match the sides side.mat = 'tools/toolsnodraw' continue if norm[fizz_axis] == 1: side.mat = pos_tex[ vbsp.TEX_FIZZLER[ side.mat.casefold() ] ] else: side.mat = neg_tex[ vbsp.TEX_FIZZLER[ side.mat.casefold() ] ] # The fizzler shouldn't kill cubes fizz_brush['spawnflags'] = '1' fizz_brush.outputs.append(Output( 'OnStartTouch', '@shake_global', 'StartShake', )) fizz_brush.outputs.append(Output( 'OnStartTouch', '@shake_global_sound', 'PlaySound', )) # The triggers are 8 units thick, 24 from the center # (-1 because fizzlers are 2 thick on each side). neg_min, neg_max = Vec(bbox_min), Vec(bbox_max) neg_min[fizz_axis] -= 23 neg_max[fizz_axis] -= 17 pos_min, pos_max = Vec(bbox_min), Vec(bbox_max) pos_min[fizz_axis] += 17 pos_max[fizz_axis] += 23 if blue_enabled or oran_enabled: neg_trig.solids.append( vmf.make_prism( neg_min, neg_max, mat='tools/toolstrigger', ).solid, ) pos_trig.solids.append( vmf.make_prism( pos_min, pos_max, mat='tools/toolstrigger', ).solid, ) else: # If neither enabled, use one trigger neg_trig.solids.append( vmf.make_prism( neg_min, pos_max, mat='tools/toolstrigger', ).solid, )
def gen_item_outputs(vmf: VMF) -> None: """Create outputs for all items with connections. This performs an optimization pass over items with outputs to remove redundancy, then applies all the outputs to the instances. Before this, connection count and inversion values are not valid. After this point, items may not have connections altered. """ LOGGER.info('Generating item IO...') pan_switching_check = options.get(PanelSwitchingStyle, 'ind_pan_check_switching') pan_switching_timer = options.get(PanelSwitchingStyle, 'ind_pan_timer_switching') pan_check_type = ITEM_TYPES['item_indicator_panel'] pan_timer_type = ITEM_TYPES['item_indicator_panel_timer'] auto_logic = [] # Apply input A/B types to connections. # After here, all connections are primary or secondary only. for item in ITEMS.values(): for conn in item.outputs: # If not a dual item, it's primary. if conn.to_item.config.input_type is not InputType.DUAL: conn.type = ConnType.PRIMARY continue # If already set, that is the priority. if conn.type is not ConnType.DEFAULT: continue # Our item set the type of outputs. if item.config.output_type is not ConnType.DEFAULT: conn.type = item.config.output_type else: # Use the affinity of the target. conn.type = conn.to_item.config.default_dual do_item_optimisation(vmf) # We go 'backwards', creating all the inputs for each item. # That way we can change behaviour based on item counts. for item in ITEMS.values(): if item.config is None: continue # Try to add the locking IO. add_locking(item) # Check we actually have timers, and that we want the relay. if item.timer is not None and ( item.config.timer_sound_pos is not None or item.config.timer_done_cmd ): has_sound = item.config.force_timer_sound or len(item.ind_panels) > 0 add_timer_relay(item, has_sound) # Add outputs for antlines. if item.antlines or item.ind_panels: if item.timer is None: add_item_indicators(item, pan_switching_check, pan_check_type) else: add_item_indicators(item, pan_switching_timer, pan_timer_type) # Special case - spawnfire items with no inputs need to fire # off the outputs. There's no way to control those, so we can just # fire it off. if not item.inputs and item.config.spawn_fire is FeatureMode.ALWAYS: if item.is_logic: # Logic gates need to trigger their outputs. # Make a logic_auto temporarily for this to collect the # outputs we need. item.inst.clear_keys() item.inst['classname'] = 'logic_auto' auto_logic.append(item.inst) else: is_inverted = conv_bool(conditions.resolve_value( item.inst, item.config.invert_var, )) logic_auto = vmf.create_ent( 'logic_auto', origin=item.inst['origin'], spawnflags=1, ) for cmd in (item.enable_cmd if is_inverted else item.disable_cmd): logic_auto.add_out( Output( 'OnMapSpawn', conditions.local_name( item.inst, conditions.resolve_value(item.inst, cmd.target), ) or item.inst, conditions.resolve_value(item.inst, cmd.input), conditions.resolve_value(item.inst, cmd.params), delay=cmd.delay, only_once=True, ) ) if item.config.input_type is InputType.DUAL: prim_inputs = [ conn for conn in item.inputs if conn.type is ConnType.PRIMARY or conn.type is ConnType.BOTH ] sec_inputs = [ conn for conn in item.inputs if conn.type is ConnType.SECONDARY or conn.type is ConnType.BOTH ] add_item_inputs( item, InputType.AND, prim_inputs, consts.FixupVars.BEE_CONN_COUNT_A, item.enable_cmd, item.disable_cmd, item.config.invert_var, ) add_item_inputs( item, InputType.AND, sec_inputs, consts.FixupVars.BEE_CONN_COUNT_B, item.sec_enable_cmd, item.sec_disable_cmd, item.config.sec_invert_var, ) else: add_item_inputs( item, item.config.input_type, list(item.inputs), consts.FixupVars.CONN_COUNT, item.enable_cmd, item.disable_cmd, item.config.invert_var, ) # Check/cross instances sometimes don't match the kind of timer delay. # We also might want to swap them out. panel_timer = instanceLocs.resolve_one('[indPanTimer]', error=True) panel_check = instanceLocs.resolve_one('[indPanCheck]', error=True) for item in ITEMS.values(): desired_panel_inst = panel_check if item.timer is None else panel_timer for pan in item.ind_panels: pan['file'] = desired_panel_inst pan.fixup[consts.FixupVars.TIM_ENABLED] = item.timer is not None logic_auto = vmf.create_ent( 'logic_auto', origin=options.get(Vec, 'global_ents_loc') ) for ent in auto_logic: # Condense all these together now. # User2 is the one that enables the target. ent.remove() for out in ent.outputs: if out.output == 'OnUser2': out.output = 'OnMapSpawn' logic_auto.add_out(out) out.only_once = True LOGGER.info('Item IO generated.')
def make_barriers(vmf: VMF, get_tex: Callable[[str], str]): """Make barrier entities. get_tex is vbsp.get_tex.""" glass_temp = template_brush.get_scaling_template( vbsp_options.get(str, "glass_template") ) grate_temp = template_brush.get_scaling_template( vbsp_options.get(str, "grating_template") ) # Avoid error without this package. if HOLES: # Grab the template solids we need. hole_temp = template_brush.get_template( vbsp_options.get(str, 'glass_hole_temp') ) hole_world, hole_detail, _ = hole_temp.visgrouped({'small'}) hole_temp_small = hole_world + hole_detail hole_world, hole_detail, _ = hole_temp.visgrouped({'large'}) hole_temp_large = hole_world + hole_detail hole_world, hole_detail, _ = hole_temp.visgrouped({'large_corner'}) hole_temp_corner = hole_world + hole_detail else: hole_temp_small = hole_temp_large = hole_temp_corner = None floorbeam_temp = vbsp_options.get(str, 'glass_floorbeam_temp') if vbsp_options.get_itemconf('BEE_PELLET:PelletGrating', False): # Merge together these existing filters in global_pti_ents vmf.create_ent( origin=vbsp_options.get(Vec, 'global_pti_ents_loc'), targetname='@grating_filter', classname='filter_multi', filtertype=0, negated=0, filter01='@not_pellet', filter02='@not_paint_bomb', ) else: # Just skip paint bombs. vmf.create_ent( origin=vbsp_options.get(Vec, 'global_pti_ents_loc'), targetname='@grating_filter', classname='filter_activator_class', negated=1, filterclass='prop_paint_bomb', ) # Group the positions by planes in each orientation. # This makes them 2D grids which we can optimise. # (normal_dist, positive_axis, type) -> [(x, y)] slices = defaultdict(set) # type: Dict[Tuple[Tuple[float, float, float], bool, BarrierType], Set[Tuple[float, float]]] # We have this on the 32-grid so we can cut squares for holes. for (origin, normal), barr_type in BARRIERS.items(): origin = Vec(origin) normal = Vec(normal) norm_axis = normal.axis() u, v = origin.other_axes(norm_axis) norm_pos = Vec.with_axes(norm_axis, origin) slice_plane = slices[ norm_pos.as_tuple(), # distance from origin to this plane. normal[norm_axis] > 0, barr_type, ] for u_off in [-48, -16, 16, 48]: for v_off in [-48, -16, 16, 48]: slice_plane.add(( (u + u_off) // 32, (v + v_off) // 32, )) # Remove pane sections where the holes are. We then generate those with # templates for slanted parts. for (origin, normal), hole_type in HOLES.items(): barr_type = BARRIERS[origin, normal] origin = Vec(origin) normal = Vec(normal) norm_axis = normal.axis() u, v = origin.other_axes(norm_axis) norm_pos = Vec.with_axes(norm_axis, origin) slice_plane = slices[ norm_pos.as_tuple(), normal[norm_axis] > 0, barr_type, ] if hole_type is HoleType.LARGE: offsets = (-80, -48, -16, 16, 48, 80) hole_temp = hole_temp_large.copy() else: offsets = (-16, 16) hole_temp = hole_temp_small.copy() for u_off in offsets: for v_off in offsets: # Skip the corners on large holes. # Those aren't actually used, so skip them. That way # we can have them diagonally or without glass in the corner. if u_off in (-80, 80) and v_off in (-80, 80): continue slice_plane.discard(( (u + u_off) // 32, (v + v_off) // 32, )) # Now generate the curved brushwork. if barr_type is BarrierType.GLASS: front_temp = glass_temp front_mat = get_tex('special.glass') elif barr_type is BarrierType.GRATING: front_temp = grate_temp front_mat = get_tex('special.grating') else: raise NotImplementedError angles = normal.to_angle(0) # Angle corresponding to the brush, for the corners. angle_list = [angles] * len(hole_temp) # This is a tricky bit. Two large templates would collide # diagonally, # so chop off the corners, then put them back only if there's not # one diagonally. if hole_type is HoleType.LARGE: for roll in (0, 90, 180, 270): corn_angles = angles.copy() corn_angles.z = roll hole_off = origin + Vec(y=128, z=128).rotate(*corn_angles) diag_type = HOLES.get( (hole_off.as_tuple(), normal.as_tuple()), None, ) if diag_type is not HoleType.LARGE: hole_temp += hole_temp_corner angle_list += [corn_angles] * len(hole_temp_corner) def solid_pane_func(off1, off2, mat): """Given the two thicknesses, produce the curved hole from the template.""" off_min = min(off1, off2) off_max = max(off1, off2) new_brushes = [ brush.copy(vmf_file=vmf) for brush in hole_temp ] for brush, br_angles in zip(new_brushes, angle_list): for face in brush.sides: face.mat = mat f_norm = face.normal() if f_norm.x == 1: face.translate(Vec(x=4 - off_max)) # face.mat = 'min' elif f_norm.x == -1: face.translate(Vec(x=-4 - off_min)) # face.mat = 'max' face.localise(origin, br_angles) return new_brushes make_glass_grating( vmf, origin, normal, barr_type, front_temp, front_mat, solid_pane_func, ) for (plane_pos, is_pos, barr_type), pos_slice in slices.items(): plane_pos = Vec(plane_pos) norm_axis = plane_pos.axis() normal = Vec.with_axes(norm_axis, 1 if is_pos else -1) if barr_type is BarrierType.GLASS: front_temp = glass_temp front_mat = get_tex('special.glass') elif barr_type is BarrierType.GRATING: front_temp = grate_temp front_mat = get_tex('special.grating') else: raise NotImplementedError u_axis, v_axis = Vec.INV_AXIS[norm_axis] for min_u, min_v, max_u, max_v in grid_optimise(dict.fromkeys(pos_slice, True)): # These are two points in the origin plane, at the borders. pos_min = Vec.with_axes( norm_axis, plane_pos, u_axis, min_u * 32, v_axis, min_v * 32, ) pos_max = Vec.with_axes( norm_axis, plane_pos, u_axis, max_u * 32 + 32, v_axis, max_v * 32 + 32, ) def solid_pane_func(pos1, pos2, mat): """Make the solid brush.""" return [vmf.make_prism( pos_min + normal * (64.0 - pos1), pos_max + normal * (64.0 - pos2), mat=mat, ).solid] make_glass_grating( vmf, (pos_min + pos_max)/2, normal, barr_type, front_temp, front_mat, solid_pane_func, ) if floorbeam_temp: LOGGER.info('Adding Glass floor beams...') add_glass_floorbeams(vmf, floorbeam_temp) LOGGER.info('Done!')
def res_unst_scaffold(vmf: VMF, res: Property): """The condition to generate Unstationary Scaffolds. This is executed once to modify all instances. """ # The instance types we're modifying if res.value not in SCAFFOLD_CONFIGS: # We've already executed this config group return RES_EXHAUSTED LOGGER.info('Running Scaffold Generator ({})...', res.value) inst_to_config, LINKS = SCAFFOLD_CONFIGS[res.value] del SCAFFOLD_CONFIGS[res.value] # Don't let this run twice chains = item_chain.chain(vmf, inst_to_config.keys(), allow_loop=False) # We need to make the link entities unique for each scaffold set, # otherwise the AllVar property won't work. for group_counter, node_list in enumerate(chains): # Set all the instances and properties start_inst = node_list[0].item.inst for vals in LINKS.values(): if vals['all'] is not None: start_inst.fixup[vals['all']] = SCAFF_PATTERN.format( name=vals['name'], group=group_counter, index='*', ) should_reverse = srctools.conv_bool( start_inst.fixup['$start_reversed']) # Stash this off to start, so we can find this after items are processed # and the instance names change. for node in node_list: node.conf = inst_to_config[node.inst['file'].casefold()] # Now set each instance in the chain, including first and last for index, node in enumerate(node_list): conf = node.conf orient, offset = get_config(node) new_file = conf.get('inst_' + orient, '') if new_file: node.inst['file'] = new_file if node.prev is None: link_type = LinkType.START if node.next is None: # No connections in either direction, just skip. # Generate the piston tip if we would have. if conf['inst_offset'] is not None: vmf.create_ent( classname='func_instance', targetname=node.inst['targetname'], file=conf['inst_offset'], origin=offset, angles=node.inst['angles'], ) continue elif node.next is None: link_type = LinkType.END else: link_type = LinkType.MID # Special case - add an extra instance for the ends, pointing # in the direction # of the connected track. This would be the endcap # model. placed_endcap = False if (orient == 'floor' and link_type is not LinkType.MID and conf['inst_end'] is not None): if link_type is LinkType.START: other_node = node.next else: other_node = node.prev other_offset = get_config(other_node)[1] link_dir = other_offset - offset # Compute the horizontal gradient (z / xy dist). # Don't use endcap if rising more than ~45 degrees, or lowering # more than ~12 degrees. horiz_dist = math.sqrt(link_dir.x**2 + link_dir.y**2) if horiz_dist != 0 and -0.15 <= (link_dir.z / horiz_dist) <= 1: link_ang = math.degrees(math.atan2(link_dir.y, link_dir.x)) if not conf['free_rotation']: # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals link_ang = (link_ang + 45) // 90 * 90 vmf.create_ent( classname='func_instance', targetname=node.inst['targetname'], file=conf['inst_end'], origin=offset, angles='0 {:.0f} 0'.format(link_ang), ) # Don't place the offset instance, this replaces that! placed_endcap = True if not placed_endcap and conf['inst_offset'] is not None: # Add an additional rotated entity at the offset. # This is useful for the piston item. vmf.create_ent( classname='func_instance', targetname=node.inst['targetname'], file=conf['inst_offset'], origin=offset, angles=node.inst['angles'], ) logic_inst = vmf.create_ent( classname='func_instance', targetname=node.inst['targetname'], file=conf.get( 'logic_' + link_type.value + ('_rev' if should_reverse else ''), '', ), origin=offset, angles=('0 0 0' if conf['rotate_logic'] else node.inst['angles']), ) # Add the link-values for linkVar, link in LINKS.items(): node.inst.fixup[linkVar] = SCAFF_PATTERN.format( name=link['name'], group=group_counter, index=index, ) if node.next is not None: node.inst.fixup[link['next']] = SCAFF_PATTERN.format( name=link['name'], group=group_counter, index=index + 1, ) for key, val in node.inst.fixup.items(): # Copy over fixup values logic_inst.fixup[key] = val LOGGER.info('Finished Scaffold generation!') return RES_EXHAUSTED
def add_glass_floorbeams(vmf: VMF, temp_name: str): """Add beams to separate large glass panels. The texture is assumed to match plasticwall004a's shape. """ template = template_brush.get_template(temp_name) temp_world, temp_detail, temp_over = template.visgrouped() try: [beam_template] = temp_world + temp_detail # type: Solid except ValueError: raise ValueError('Bad Glass Floorbeam template!') # Grab the 'end' side, which we move around. for side in beam_template.sides: if side.normal() == (-1, 0, 0): beam_end_face = side break else: raise ValueError('Not aligned to world...') separation = vbsp_options.get(int, 'glass_floorbeam_sep') + 1 separation *= 128 # First we want to find all the groups of contiguous glass sections. # This is a mapping from some glass piece to its group list. groups = {} for (origin, normal), barr_type in BARRIERS.items(): # Grating doesn't use it. if barr_type is not BarrierType.GLASS: continue normal = Vec(normal) if not normal.z: # Not walls. continue pos = Vec(origin) + normal * 62 groups[pos.as_tuple()] = [pos] # Loop over every pos and check in the +x/y directions for another glass # piece. If there, merge the two lists and set every pos in the group to # point to the new list. # Once done, every unique list = a group. for pos_tup in groups.keys(): pos = Vec(pos_tup) for off in ((128, 0, 0), (0, 128, 0)): neighbour = (pos + off).as_tuple() if neighbour in groups: our_group = groups[pos_tup] neigh_group = groups[neighbour] if our_group is neigh_group: continue # Now merge the two lists. We then need to update all dict locs # to point to the new list. if len(neigh_group) > len(our_group): small_group, large_group = our_group, neigh_group else: small_group, large_group = neigh_group, our_group large_group.extend(small_group) for pos in small_group: groups[pos.as_tuple()] = large_group # Remove duplicates objects by using the ID as key.. groups = list({ id(group): group for group in groups.values() }.values()) # Side -> u, v or None for group in groups: bbox_min, bbox_max = Vec.bbox(group) dimensions = bbox_max - bbox_min LOGGER.info('Size = {}', dimensions) # Our beams align to the smallest axis. if dimensions.y > dimensions.x: beam_ax = 'x' side_ax = 'y' rot = Vec(0, 0, 0) else: beam_ax = 'y' side_ax = 'x' rot = Vec(0, 90, 0) # Build min, max tuples for each axis in the other direction. # This tells us where the beams will be. beams = {} # type: Dict[int, Tuple[int, int]] # Add 128 so the first pos isn't a beam. offset = bbox_min[side_ax] + 128 for pos in group: side_off = pos[side_ax] beam_off = pos[beam_ax] # Skip over non-'sep' positions.. if (side_off - offset) % separation != 0: continue try: min_pos, max_pos = beams[side_off] except KeyError: beams[side_off] = beam_off, beam_off else: beams[side_off] = min(min_pos, beam_off), max(max_pos, beam_off) detail = vmf.create_ent('func_detail') for side_off, (min_off, max_off) in beams.items(): for min_pos, max_pos in beam_hole_split( beam_ax, Vec.with_axes(side_ax, side_off, beam_ax, min_off, 'z', bbox_min), Vec.with_axes(side_ax, side_off, beam_ax, max_off, 'z', bbox_min), ): if min_pos[beam_ax] >= max_pos[beam_ax]: raise ValueError(min_pos, max_pos, beam_ax) # Make the beam. # Grab the end face and snap to the length we want. beam_end_off = max_pos[beam_ax] - min_pos[beam_ax] assert beam_end_off > 0, beam_end_off for plane in beam_end_face.planes: plane.x = beam_end_off new_beam = beam_template.copy(vmf_file=vmf) new_beam.localise(min_pos, rot) detail.solids.append(new_beam)
def gen_flinch_trigs(self, vmf: VMF, name: str, start_disabled: str) -> None: """For deadly fizzlers optionally make them safer. This adds logic to force players back instead when walking into the field. Only applies to vertical triggers. """ normal = abs(self.normal()) # type: Vec # Horizontal fizzlers would just have you fall through. if normal.z: return # Disabled. if not vbsp_options.get_itemconf(('VALVE_FIZZLER', 'FlinchBack'), False): return # Make global entities if not present. if '_fizz_flinch_hurt' not in vmf.by_target: glob_ent_loc = vbsp_options.get(Vec, 'global_ents_loc') vmf.create_ent( classname='point_hurt', targetname='_fizz_flinch_hurt', Damage=10, # Just for visuals and sounds. # BURN | ENERGYBEAM | PREVENT_PHYSICS_FORCE DamageType=8 | 1024 | 2048, DamageTarget='!activator', # Hurt the triggering player. DamageRadius=1, # Target makes this unused. origin=glob_ent_loc, ) # We need two catapults - one for each side. neg_brush = vmf.create_ent( targetname=name, classname='trigger_catapult', spawnflags=1, # Players only. origin=self.base_inst['origin'], physicsSpeed=0, playerSpeed=96, launchDirection=(-normal).to_angle(), startDisabled=start_disabled, ) neg_brush.add_out(Output('OnCatapulted', '_fizz_flinch_hurt', 'Hurt')) pos_brush = neg_brush.copy() pos_brush['launchDirection'] = normal.to_angle() vmf.add_ent(pos_brush) for seg_min, seg_max in self.emitters: neg_brush.solids.append(vmf.make_prism( p1=(seg_min - 4 * normal - 64 * self.up_axis ), p2=seg_max + 64 * self.up_axis, mat=const.Tools.TRIGGER, ).solid) pos_brush.solids.append(vmf.make_prism( p1=seg_min - 64 * self.up_axis, p2=(seg_max + 4 * normal + 64 * self.up_axis ), mat=const.Tools.TRIGGER, ).solid)