def res_rotate_inst(inst: Entity, res: Property) -> None: """Rotate the instance around an axis. If `axis` is specified, it should be a normal vector and the instance will be rotated `angle` degrees around it. Otherwise, `angle` is a pitch-yaw-roll angle which is applied. `around` can be a point (local, pre-rotation) which is used as the origin. """ angles = Angle.from_str(inst['angles']) if 'axis' in res: orient = Matrix.axis_angle( Vec.from_str(inst.fixup.substitute(res['axis'])), conv_float(inst.fixup.substitute(res['angle'])), ) else: orient = Matrix.from_angle( Angle.from_str(inst.fixup.substitute(res['angle']))) try: offset = Vec.from_str(inst.fixup.substitute(res['around'])) except NoKeyError: pass else: origin = Vec.from_str(inst['origin']) inst['origin'] = origin + (-offset @ orient + offset) @ angles inst['angles'] = (orient @ angles).to_angle()
def res_set_marker(inst: Entity, res: Property) -> None: """Set a marker at a specific position. Parameters: * `global`: If true, the position is an absolute position, ignoring this instance. * `name`: A name to store to identify this marker/item. * `pos`: The position or offset to use for the marker. """ origin = Vec.from_str(inst['origin']) orient = Matrix.from_angle(Angle.from_str(inst['angles'])) try: is_global = srctools.conv_bool( inst.fixup.substitute(res['global'], allow_invert=True)) except LookupError: is_global = False name = inst.fixup.substitute(res['name']).casefold() pos = Vec.from_str(inst.fixup.substitute(res['pos'])) if not is_global: pos = pos @ orient + origin mark = Marker(pos, name, inst) MARKERS.append(mark) LOGGER.debug('Marker added: {}', mark)
def __init__(self, inst: Entity, conf: Config, size: int) -> None: self.ent = inst self.conf = conf self.next = None self.no_prev = True self.size = size self.orient = Matrix.from_angle(Angle.from_str(inst['angles']))
def res_replace_instance(vmf: VMF, inst: Entity, res: Property): """Replace an instance with another entity. `keys` and `localkeys` defines the new keyvalues used. `targetname` and `angles` are preset, and `origin` will be used to offset the given amount from the current location. If `keep_instance` is true, the instance entity will be kept instead of removed. """ origin = Vec.from_str(inst['origin']) angles = Angle.from_str(inst['angles']) if not res.bool('keep_instance'): inst.remove() # Do this first to free the ent ID, so the new ent has # the same one. # We copy to allow us to still access the $fixups and other values. new_ent = inst.copy(des_id=inst.id) new_ent.clear_keys() # Ensure there's a classname, just in case. new_ent['classname'] = 'info_null' vmf.add_ent(new_ent) conditions.set_ent_keys(new_ent, inst, res) new_ent['origin'] = Vec.from_str(new_ent['origin']) @ angles + origin new_ent['angles'] = angles new_ent['targetname'] = inst['targetname']
def res_glass_hole(inst: Entity, res: Property): """Add Glass/grating holes. The value should be 'large' or 'small'.""" hole_type = HoleType(res.value) normal: Vec = round(Vec(z=-1) @ Angle.from_str(inst['angles']), 6) origin: Vec = Vec.from_str(inst['origin']) // 128 * 128 + 64 if test_hole_spot(origin, normal, hole_type): HOLES[origin.as_tuple(), normal.as_tuple()] = hole_type inst['origin'] = origin inst['angles'] = normal.to_angle() return # Test the opposite side of the glass too. inv_origin = origin + 128 * normal inv_normal = -normal if test_hole_spot(inv_origin, inv_normal, hole_type): HOLES[inv_origin.as_tuple(), inv_normal.as_tuple()] = hole_type inst['origin'] = inv_origin inst['angles'] = inv_normal.to_angle() else: # Remove the instance, so this does nothing. inst.remove()
def res_add_placement_helper(inst: Entity, res: Property): """Add a placement helper to a specific tile. `Offset` and `normal` specify the position and direction out of the surface the helper should be added to. If `upDir` is specified, this is the direction of the top of the portal. """ orient = Matrix.from_angle(Angle.from_str(inst['angles'])) pos = conditions.resolve_offset(inst, res['offset', '0 0 0'], zoff=-64) normal = res.vec('normal', 0, 0, 1) @ orient up_dir: Optional[Vec] try: up_dir = Vec.from_str(res['upDir']) @ orient except LookupError: up_dir = None try: tile = tiling.TILES[(pos - 64 * normal).as_tuple(), normal.as_tuple()] except KeyError: LOGGER.warning('No tile at {} @ {}', pos, normal) return tile.add_portal_helper(up_dir)
class Node(Generic[ConfT]): """Represents a single node in the chain.""" item: Item = attr.ib(init=True) conf: ConfT = attr.ib(init=True) # Origin and angles of the instance. pos = attr.ib(init=False, default=attr.Factory( lambda self: Vec.from_str(self.item.inst['origin']), takes_self=True, )) orient = attr.ib(init=False, default=attr.Factory( lambda self: Matrix.from_angle( Angle.from_str(self.item.inst['angles'])), takes_self=True, )) # The links between nodes prev: Optional[Node[ConfT]] = attr.ib(default=None, init=False) next: Optional[Node[ConfT]] = attr.ib(default=None, init=False) @property def inst(self) -> Entity: """Return the relevant instance.""" return self.item.inst @classmethod def from_inst(cls, inst: Entity, conf: ConfT) -> Node[ConfT]: """Find the item for this instance, and return the node.""" name = inst['targetname'] try: return Node(connections.ITEMS[name], conf) except KeyError: raise ValueError('No item for "{}"?'.format(name)) from None
def add_item_coll(self, item: Item, inst: Entity) -> None: """Add the default collisions from an item definition for this instance.""" origin = Vec.from_str(inst['origin']) orient = Matrix.from_angle(Angle.from_str(inst['angles'])) for coll in item.collisions: self.add( (coll @ orient + origin).with_attrs(name=inst['targetname']))
def __init__(self, ent: Entity) -> None: self.origin = Vec.from_str(ent['origin']) self.matrix = Matrix.from_angle(Angle.from_str(ent['angles'])) self.ent = ent self.has_input = False # We verify every node has an input if used. # DestType -> output. self.outputs: Dict[DestType, Optional[Node]] = dict.fromkeys( self.out_types, None) # Outputs fired when cubes reach this point. pass_outputs = [ out for out in ent.outputs if out.output.casefold() == self.pass_out_name ] self.has_pass = bool(pass_outputs) if self.has_pass: for out in pass_outputs: out.output = 'On' + PASS_OUT if ent['classname'].startswith('comp_'): # Remove the extra keyvalues we use. ent.keys = { 'classname': 'info_target', 'targetname': ent['targetname'], 'origin': ent['origin'], 'angles': ent['angles'], } ent.make_unique('_vac_node') elif not self.keep_ent: ent.remove()
def res_monitor(inst: Entity, res: Property) -> None: """Result for the monitor component. Options: - bullseye_name: If possible to break this, this is the name to give the npc_bullseye. - bullseye_loc: This is the position to place the bullseye at. - bullseye_parent: This is the parent to give the bullseye. The fixup variable $is_breakable is set to True if lasers or turrets are present to indicate the func_breakable should be added. """ global HAS_MONITOR import vbsp ( bullseye_name, bullseye_loc, bullseye_parent, ) = res.value HAS_MONITOR = True has_laser = vbsp.settings['has_attr']['laser'] # Allow turrets if the monitor is setup to allow it, and the actor should # be shot. needs_turret = bullseye_name and options.get(bool, 'voice_studio_should_shoot') inst.fixup['$is_breakable'] = has_laser or needs_turret # We need to generate an ai_relationship, which makes turrets hate # a bullseye. if needs_turret: loc = Vec(bullseye_loc) loc.localise( Vec.from_str(inst['origin']), Angle.from_str(inst['angles']), ) bullseye_name = conditions.local_name(inst, bullseye_name) inst.map.create_ent( classname='npc_bullseye', targetname=bullseye_name, parentname=conditions.local_name(inst, bullseye_parent), spawnflags=221186, # Non-solid, invisible, etc.. origin=loc, ) relation = inst.map.create_ent( classname='ai_relationship', targetname='@monitor_turr_hate', parentname=bullseye_name, # When killed, destroy this too. spawnflags=2, # Notify turrets about monitor locations disposition=1, # Hate origin=loc, subject='npc_portal_turret_floor', target=bullseye_name, ) MONITOR_RELATIONSHIP_ENTS.append(relation)
def 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( '', f'@{sendtor_name}_las_relay_*', '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 orient = Matrix.from_angle(Angle.from_str(las_item.inst['angles'])) targ_offset = Vec.from_str( las_item.inst['origin']) + targ_offset @ orient targ_normal = targ_normal @ orient relay_name = f'@{sendtor_name}_las_relay_{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'), )
def check_orient(inst: Entity) -> bool: """Check the orientation against the instance.""" inst_normal = from_dir @ Angle.from_str(inst['angles']) if normal == 'WALL': # Special case - it's not on the floor or ceiling return abs(inst_normal.z) < 1e-6 else: return inst_normal == normal or ( allow_inverse and -inst_normal == normal )
def res_force_upright(inst: Entity): """Position an instance to orient upwards while keeping the normal. The result angle will have pitch and roll set to 0. Vertical instances are unaffected. """ normal = Vec(0, 0, 1) @ Angle.from_str(inst['angles']) if normal.z != 0: return ang = math.degrees(math.atan2(normal.y, normal.x)) inst['angles'] = '0 {:g} 0'.format(ang % 360) # Don't use negatives
def shift_ent(inst: Entity) -> None: """Randomly shift the instance.""" rng = rand.seed(b'rand_shift', inst, seed) pos = Vec( rng.uniform(min_x, max_x), rng.uniform(min_y, max_y), rng.uniform(min_z, max_z), ) pos.localise(Vec.from_str(inst['origin']), Angle.from_str(inst['angles'])) inst['origin'] = pos
def gen_faithplates(vmf: VMF) -> None: """Place the targets and catapults into the map.""" # Target positions -> list of triggers wanting to aim there. pos_to_trigs: Dict[Union[Tuple[float, float, float], tiling.TileDef], List[Entity]] = collections.defaultdict(list) for plate in PLATES.values(): if isinstance(plate, (AngledPlate, PaintDropper)): targ_pos: Union[Tuple[float, float, float], tiling.TileDef] if isinstance(plate.target, tiling.TileDef): targ_pos = plate.target # Use the ID directly. else: targ_pos = plate.target.as_tuple() pos_to_trigs[targ_pos].append(plate.trig) if isinstance(plate, StraightPlate): trigs = [plate.trig, plate.helper_trig] else: trigs = [plate.trig] for trig in trigs: trig_origin = trig.get_origin() if plate.template is not None: trig.solids = template_brush.import_template( vmf, plate.template, trig_origin + plate.trig_offset, Angle.from_str(plate.inst['angles']), force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ).world elif plate.trig_offset: for solid in trig.solids: solid.translate(plate.trig_offset) # Now, generate each target needed. for pos_or_tile, trigs in pos_to_trigs.items(): target = vmf.create_ent( 'info_target', angles='0 0 0', spawnflags='3', # Transmit to PVS and always transmit. ) if isinstance(pos_or_tile, tiling.TileDef): pos_or_tile.position_bullseye(target) else: # Static target. target['origin'] = Vec(pos_or_tile) target.make_unique('faith_target') for trig in trigs: trig['launchTarget'] = target['targetname']
def res_create_entity(vmf: VMF, inst: Entity, res: Property): """Create an entity. * `keys` and `localkeys` defines the new keyvalues used. * `origin` and `angles` are local to the instance. """ origin = Vec.from_str(inst['origin']) orient = Angle.from_str(inst['angles']) new_ent = vmf.create_ent( # Ensure there's these critical values. classname='info_null', origin='0 0 0', angles='0 0 0', ) conditions.set_ent_keys(new_ent, inst, res) new_ent['origin'] = Vec.from_str(new_ent['origin']) @ orient + origin new_ent['angles'] = Angle.from_str(new_ent['angles']) @ orient
def res_make_funnel_light(inst: Entity) -> None: """Place a light for Funnel items.""" oran_on = inst.fixup.bool('$start_reversed') if inst.fixup['$conn_count_b'] != '0': import vbsp if not vbsp.settings['style_vars']['funnelallowswitchedlights']: # Allow disabling adding switchable lights. return name = conditions.local_name(inst, 'light') need_blue = need_oran = True else: name = '' if oran_on: need_oran = True need_blue = False else: need_blue = True need_oran = False loc = Vec(0, 0, -56) @ Angle.from_str(inst['angles']) + Vec.from_str( inst['origin']) if need_blue: inst.map.create_ent( classname='light', targetname=name + '_b' if name else '', spawnflags=int(oran_on), # 1 = Initially Dark origin=loc, _light='50 120 250 50', _lightHDR='-1 -1 -1 1', _lightscaleHDR=2, _fifty_percent_distance=48, _zero_percent_distance=96, _hardfalloff=1, _distance=0, style=0, ) if need_oran: inst.map.create_ent( classname='light', targetname=name + '_o' if name else '', spawnflags=int(not oran_on), origin=loc, _light='250 120 50 50', _lightHDR='-1 -1 -1 1', _lightscaleHDR=2, _fifty_percent_distance=48, _zero_percent_distance=96, _hardfalloff=1, _distance=0, style=0, )
def res_antigel(inst: Entity) -> None: """Implement the Antigel marker.""" inst.remove() origin = Vec.from_str(inst['origin']) orient = Matrix.from_angle(Angle.from_str(inst['angles'])) pos = round(origin - 128 * orient.up(), 6) norm = round(orient.up(), 6) try: tiling.TILES[pos.as_tuple(), norm.as_tuple()].is_antigel = True except KeyError: LOGGER.warning('No tile to set antigel at {}, {}', pos, norm) texturing.ANTIGEL_LOCS.add((origin // 128).as_tuple())
def parse_map(vmf: VMF, has_attr: Dict[str, bool]) -> None: """Find all glass/grating in the map. This removes the per-tile instances, and all original brushwork. The frames are updated with a fixup var, as appropriate. """ frame_inst = resolve('[glass_frames]', silent=True) glass_inst = resolve_one('[glass_128]') pos = None for brush_ent in vmf.by_class['func_detail']: is_glass = False for face in brush_ent.sides(): if face.mat == consts.Special.GLASS: has_attr['glass'] = True pos = face.get_origin() is_glass = True break if is_glass: brush_ent.remove() BARRIERS[get_pos_norm(pos)] = BarrierType.GLASS for brush_ent in vmf.by_class['func_brush']: is_grating = False for face in brush_ent.sides(): if face.mat == consts.Special.GRATING: has_attr['grating'] = True pos = face.get_origin() is_grating = True break if is_grating: brush_ent.remove() BARRIERS[get_pos_norm(pos)] = BarrierType.GRATING for inst in vmf.by_class['func_instance']: filename = inst['file'].casefold() if filename == glass_inst: inst.remove() elif filename in frame_inst: # Add a fixup to allow distinguishing the type. pos = Vec.from_str(inst['origin']) // 128 * 128 + (64, 64, 64) norm = Vec(z=-1) @ Angle.from_str(inst['angles']) try: inst.fixup[consts.FixupVars.BEE_GLS_TYPE] = BARRIERS[ pos.as_tuple(), norm.as_tuple()].value except KeyError: LOGGER.warning('No glass/grating for frame at {}, {}?', pos, norm) if options.get(str, 'glass_pack') and has_attr['glass']: packing.pack_list(vmf, options.get(str, 'glass_pack'))
def resolve_offset(inst, value: str, scale: float=1, zoff: float=0) -> Vec: """Retrieve an offset from an instance var. This allows several special values: * Any $replace variables * <piston_start> or <piston> to get the unpowered position of a piston plat * <piston_end> to get the powered position of a piston plat * <piston_top> to get the extended position of a piston plat * <piston_bottom> to get the retracted position of a piston plat If scale is set, read values are multiplied by this, and zoff is added to Z. """ value = inst.fixup.substitute(value).casefold() # Offset the overlay by the given distance # Some special placeholder values: if value == '<piston_start>' or value == '<piston>': if inst.fixup.bool(consts.FixupVars.PIST_IS_UP): value = '<piston_top>' else: value = '<piston_bottom>' elif value == '<piston_end>': if inst.fixup.bool(consts.FixupVars.PIST_IS_UP): value = '<piston_bottom>' else: value = '<piston_top>' if value == '<piston_bottom>': offset = Vec( z=inst.fixup.int(consts.FixupVars.PIST_BTM) * 128, ) elif value == '<piston_top>': offset = Vec( z=inst.fixup.int(consts.FixupVars.PIST_TOP) * 128, ) else: # Regular vector offset = Vec.from_str(value) * scale offset.z += zoff offset.localise( Vec.from_str(inst['origin']), Angle.from_str(inst['angles']), ) return offset
def res_transfer_bullseye(inst: Entity, props: Property): """Transfer catapult targets and placement helpers from one tile to another.""" start_pos = conditions.resolve_offset(inst, props['start_pos', '']) end_pos = conditions.resolve_offset(inst, props['end_pos', '']) angles = Angle.from_str(inst['angles']) start_norm = props.vec('start_norm', 0, 0, 1) @ angles end_norm = props.vec('end_norm', 0, 0, 1) @ angles try: start_tile = tiling.TILES[(start_pos - 64 * start_norm).as_tuple(), start_norm.as_tuple()] except KeyError: LOGGER.warning( '"{}": Cannot find tile to transfer from at {}, {}!'.format( inst['targetname'], start_pos, start_norm)) return end_tile = tiling.TileDef.ensure( end_pos - 64 * end_norm, end_norm, ) # Now transfer the stuff. if start_tile.has_oriented_portal_helper: # We need to rotate this. orient = start_tile.portal_helper_orient.copy() # If it's directly opposite, just mirror - we have no clue what the # intent is. if Vec.dot(start_norm, end_norm) != -1.0: # Use the dict to compute the rotation to apply. orient @= NORM_ROTATIONS[start_norm.as_tuple(), end_norm.as_tuple()] end_tile.add_portal_helper(orient) elif start_tile.has_portal_helper: # Non-oriented, don't orient. end_tile.add_portal_helper() start_tile.remove_portal_helper(all=True) if start_tile.bullseye_count: end_tile.bullseye_count = start_tile.bullseye_count start_tile.bullseye_count = 0 # Then transfer the targets across. for plate in faithplate.PLATES.values(): if getattr(plate, 'target', None) is start_tile: plate.target = end_tile
def load_connectionpoint(item: Item, ent: Entity) -> None: """Allow more conveniently defining connectionpoints.""" origin = Vec.from_str(ent['origin']) angles = Angle.from_str(ent['angles']) if round(angles.pitch) != 0.0 or round(angles.roll) != 0.0: LOGGER.warning( "Connection Point at {} is not flat on the floor, PeTI doesn't allow this.", origin, ) return try: side = ConnSide.from_yaw(round(angles.yaw)) except ValueError: LOGGER.warning( "Connection Point at {} must point in a cardinal direction, not {}!", origin, angles, ) return orient = Matrix.from_yaw(round(angles.yaw)) center = (origin - (-56, 56, 0)) / 16 center.z = 0 center.y = -center.y try: offset = SKIN_TO_CONN_OFFSETS[ent['skin']] @ orient except KeyError: LOGGER.warning('Connection Point at {} has invalid skin "{}"!', origin) return ant_pos = Coord(round(center.x + offset.x), round(center.y - offset.y), 0) sign_pos = Coord(round(center.x - offset.x), round(center.y + offset.y), 0) group_str = ent['group_id'] item.antline_points[side].append( AntlinePoint(ant_pos, sign_pos, conv_int(ent['priority']), int(group_str) if group_str.strip() else None))
def insert_over(inst: Entity) -> None: """Apply the result.""" temp_id = inst.fixup.substitute(orig_temp_id) origin = Vec.from_str(inst['origin']) angles = Angle.from_str(inst['angles', '0 0 0']) face_pos = conditions.resolve_offset(inst, face_str) normal = orig_norm @ angles # Don't make offset change the face_pos value.. origin += offset @ angles for axis, norm in enumerate(normal): # Align to the center of the block grid. The normal direction is # already correct. if norm == 0: face_pos[axis] = face_pos[axis] // 128 * 128 + 64 # Shift so that the user perceives the position as the pos of the face # itself. face_pos -= 64 * normal try: tiledef = tiling.TILES[face_pos.as_tuple(), normal.as_tuple()] except KeyError: LOGGER.warning( 'Overlay brush position is not valid: {}', face_pos, ) return temp = template_brush.import_template( vmf, temp_id, origin, angles, targetname=inst['targetname', ''], force_type=TEMP_TYPES.detail, ) for over in temp.overlay: pos = Vec.from_str(over['basisorigin']) mat = over['material'] try: replace = replace_tex[mat.casefold().replace('\\', '/')] except KeyError: pass else: mat = rand.seed(b'temp_over', temp_id, pos).choice(replace) if mat[:1] == '$': mat = inst.fixup[mat] if mat.startswith('<') or mat.endswith('>'): # Lookup in the texture data. gen, mat = texturing.parse_name(mat[1:-1]) mat = gen.get(pos, mat) over['material'] = mat tiledef.bind_overlay(over) # Wipe the brushes from the map. if temp.detail is not None: temp.detail.remove() LOGGER.info( 'Overlay template "{}" could set keep_brushes=0.', temp_id, )
def res_water_splash(vmf: VMF, inst: Entity, res: Property) -> None: """Creates splashes when something goes in and out of water. Arguments: - `parent`: The name of the parent entity. - `name`: The name given to the env_splash. - `scale`: The size of the effect (8 by default). - `position`: The offset position to place the entity. - `position2`: The offset to which the entity will move. - `type`: Use certain fixup values to calculate pos2 instead: `piston_1`/`2`/`3`/`4`: Use `$bottom_level` and `$top_level` as offsets. `track_platform`: Use `$travel_direction`, `$travel_distance`, etc. - `fast_check`: Check faster for movement. Needed for items which move quickly. """ ( name, parent, scale, pos1, pos2, calc_type, fast_check, ) = res.value # type: str, str, float, Vec, Vec, str, str pos1 = pos1.copy() splash_pos = pos1.copy() if calc_type == 'track_platform': lin_off = srctools.conv_int(inst.fixup['$travel_distance']) travel_ang = Angle.from_str(inst.fixup['$travel_direction']) start_pos = srctools.conv_float(inst.fixup['$starting_position']) if start_pos: start_pos = round(start_pos * lin_off) pos1 += Vec(x=-start_pos) @ travel_ang pos2 = Vec(x=lin_off) @ travel_ang + pos1 elif calc_type.startswith('piston'): # Use piston-platform offsetting. # The number is the highest offset to move to. max_pist = srctools.conv_int(calc_type.split('_', 2)[1], 4) bottom_pos = srctools.conv_int(inst.fixup['$bottom_level']) top_pos = min(srctools.conv_int(inst.fixup['$top_level']), max_pist) pos2 = pos1.copy() pos1 += Vec(z=128 * bottom_pos) pos2 += Vec(z=128 * top_pos) LOGGER.info('Bottom: {}, top: {}', bottom_pos, top_pos) else: # Directly from the given value. pos2 = Vec.from_str(conditions.resolve_value(inst, pos2)) origin = Vec.from_str(inst['origin']) angles = Angle.from_str(inst['angles']) splash_pos.localise(origin, angles) pos1.localise(origin, angles) pos2.localise(origin, angles) # Since it's a straight line and you can't go through walls, # if pos1 and pos2 aren't in goo we aren't ever in goo. check_pos = [pos1, pos2] if pos1.z < origin.z: # If embedding in the floor, the positions can both be below the # actual surface. In that case check the origin too. check_pos.append(Vec(pos1.x, pos1.y, origin.z)) if pos1.z == pos2.z: # Flat - this won't do anything... return for pos in check_pos: grid_pos = pos // 128 * 128 grid_pos += (64, 64, 64) block = BLOCK_POS['world':pos] if block.is_goo: break else: return # Not in goo at all water_pos = grid_pos + (0, 0, 32) # Check if both positions are above or below the water.. # that means it won't ever trigger. if max(pos1.z, pos2.z) < water_pos.z - 8: return if min(pos1.z, pos2.z) > water_pos.z + 8: return # Pass along the water_pos encoded into the targetname. # Restrict the number of characters to allow direct slicing # in the script. enc_data = '_{:09.3f}{}'.format( water_pos.z + 12, 'f' if fast_check else 's', ) vmf.create_ent( classname='env_splash', targetname=conditions.local_name(inst, name) + enc_data, parentname=conditions.local_name(inst, parent), origin=splash_pos + (0, 0, 16), scale=scale, vscripts='BEE2/water_splash.nut', thinkfunction='Think', spawnflags='1', # Trace downward to water surface. )
def res_conveyor_belt(vmf: VMF, inst: Entity, res: Property) -> None: """Create a conveyor belt. * Options: * `SegmentInst`: Generated at each square. (`track` is the name of the path to attach to.) * `TrackTeleport`: Set the track points so they teleport trains to the start. * `Speed`: The fixup or number for the train speed. * `MotionTrig`: If set, a trigger_multiple will be spawned that `EnableMotion`s weighted cubes. The value is the name of the relevant filter. * `EndOutput`: Adds an output to the last track. The value is the same as outputs in VMFs. `RotateSegments`: If true (default), force segments to face in the direction of movement. * `BeamKeys`: If set, a list of keyvalues to use to generate an env_beam travelling from start to end. The origin is treated specially - X is the distance from walls, y is the distance to the side, and z is the height. `RailTemplate`: A template for the track sections. This is made into a non-solid func_brush, combining all sections. * `NoPortalFloor`: If set, add a `func_noportal_volume` on the floor under the track. * `PaintFizzler`: If set, add a paint fizzler underneath the belt. """ move_dist = inst.fixup.int('$travel_distance') if move_dist <= 2: # There isn't room for a conveyor, so don't bother. inst.remove() return orig_orient = Matrix.from_angle(Angle.from_str(inst['angles'])) move_dir = Vec(1, 0, 0) @ Angle.from_str(inst.fixup['$travel_direction']) move_dir = move_dir @ orig_orient start_offset = inst.fixup.float('$starting_position') teleport_to_start = res.bool('TrackTeleport', True) segment_inst_file = instanceLocs.resolve_one(res['SegmentInst', '']) rail_template = res['RailTemplate', None] track_speed = res['speed', None] start_pos = Vec.from_str(inst['origin']) end_pos = start_pos + move_dist * move_dir if start_offset > 0: # If an oscillating platform, move to the closest side.. offset = start_offset * move_dir # The instance is placed this far along, so move back to the end. start_pos -= offset end_pos -= offset if start_offset > 0.5: # Swap the direction of movement.. start_pos, end_pos = end_pos, start_pos inst['origin'] = start_pos norm = orig_orient.up() if res.bool('rotateSegments', True): orient = Matrix.from_basis(x=move_dir, z=norm) inst['angles'] = orient.to_angle() else: orient = orig_orient # Add the EnableMotion trigger_multiple seen in platform items. # This wakes up cubes when it starts moving. motion_filter = res['motionTrig', None] # Disable on walls, or if the conveyor can't be turned on. if norm != (0, 0, 1) or inst.fixup['$connectioncount'] == '0': motion_filter = None track_name = conditions.local_name(inst, 'segment_{}') rail_temp_solids = [] last_track = None # Place tracks at the top, so they don't appear inside wall sections. track_start: Vec = start_pos + 48 * norm track_end: Vec = end_pos + 48 * norm for index, pos in enumerate(track_start.iter_line(track_end, stride=128), start=1): track = vmf.create_ent( classname='path_track', targetname=track_name.format(index) + '-track', origin=pos, spawnflags=0, orientationtype=0, # Don't rotate ) if track_speed is not None: track['speed'] = track_speed if last_track: last_track['target'] = track['targetname'] if index == 1 and teleport_to_start: track['spawnflags'] = 16 # Teleport here.. last_track = track # Don't place at the last point - it doesn't teleport correctly, # and would be one too many. if segment_inst_file and pos != track_end: seg_inst = conditions.add_inst( vmf, targetname=track_name.format(index), file=segment_inst_file, origin=pos, angles=orient, ) seg_inst.fixup.update(inst.fixup) if rail_template: temp = template_brush.import_template( vmf, rail_template, pos, orient, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) rail_temp_solids.extend(temp.world) if rail_temp_solids: vmf.create_ent( classname='func_brush', origin=track_start, spawnflags=1, # Ignore +USE solidity=1, # Not solid vrad_brush_cast_shadows=1, drawinfastreflection=1, ).solids = rail_temp_solids if teleport_to_start: # Link back to the first track.. last_track['target'] = track_name.format(1) + '-track' # Generate an env_beam pointing from the start to the end of the track. try: beam_keys = res.find_key('BeamKeys') except LookupError: pass else: beam = vmf.create_ent(classname='env_beam') beam_off = beam_keys.vec('origin', 0, 63, 56) for prop in beam_keys: beam[prop.real_name] = prop.value # Localise the targetname so it can be triggered.. beam['LightningStart'] = beam['targetname'] = conditions.local_name( inst, beam['targetname', 'beam']) del beam['LightningEnd'] beam['origin'] = start_pos + Vec( -beam_off.x, beam_off.y, beam_off.z, ) @ orient beam['TargetPoint'] = end_pos + Vec( +beam_off.x, beam_off.y, beam_off.z, ) @ orient # Allow adding outputs to the last path_track. for prop in res.find_all('EndOutput'): output = Output.parse(prop) output.output = 'OnPass' output.inst_out = None output.comma_sep = False output.target = conditions.local_name(inst, output.target) last_track.add_out(output) if motion_filter is not None: motion_trig = vmf.create_ent( classname='trigger_multiple', targetname=conditions.local_name(inst, 'enable_motion_trig'), origin=start_pos, filtername=motion_filter, startDisabled=1, wait=0.1, ) motion_trig.add_out( Output('OnStartTouch', '!activator', 'ExitDisabledState')) # Match the size of the original... motion_trig.solids.append( vmf.make_prism( start_pos + Vec(72, -56, 58) @ orient, end_pos + Vec(-72, 56, 144) @ orient, mat=consts.Tools.TRIGGER, ).solid) if res.bool('NoPortalFloor'): # Block portals on the floor.. floor_noportal = vmf.create_ent( classname='func_noportal_volume', origin=track_start, ) floor_noportal.solids.append( vmf.make_prism( start_pos + Vec(-60, -60, -66) @ orient, end_pos + Vec(60, 60, -60) @ orient, mat=consts.Tools.INVISIBLE, ).solid) # A brush covering under the platform. base_trig = vmf.make_prism( start_pos + Vec(-64, -64, 48) @ orient, end_pos + Vec(64, 64, 56) @ orient, mat=consts.Tools.INVISIBLE, ).solid vmf.add_brush(base_trig) # Make a paint_cleanser under the belt.. if res.bool('PaintFizzler'): pfizz = vmf.create_ent( classname='trigger_paint_cleanser', origin=start_pos, ) pfizz.solids.append(base_trig.copy()) for face in pfizz.sides(): face.mat = consts.Tools.TRIGGER
def res_add_overlay_inst(vmf: VMF, inst: Entity, res: Property) -> Optional[Entity]: """Add another instance on top of this one. If a single value, this sets only the filename. Values: - `file`: The filename. - `fixup_style`: The Fixup style for the instance. '0' (default) is Prefix, '1' is Suffix, and '2' is None. - `copy_fixup`: If true, all the `$replace` values from the original instance will be copied over. - `move_outputs`: If true, outputs will be moved to this instance. - `offset`: The offset (relative to the base) that the instance will be placed. Can be set to `<piston_top>` and `<piston_bottom>` to offset based on the configuration. `<piston_start>` will set it to the starting position, and `<piston_end>` will set it to the ending position of the Piston Platform's handles. - `rotation`: Rotate the instance by this amount. - `angles`: If set, overrides `rotation` and the instance angles entirely. - `fixup`/`localfixup`: Keyvalues in this block will be copied to the overlay entity. - If the value starts with `$`, the variable will be copied over. - If this is present, `copy_fixup` will be disabled. """ if not res.has_children(): # Use all the defaults. res = Property('AddOverlay', [Property('File', res.value)]) if 'angles' in res: angles = Angle.from_str(res['angles']) if 'rotation' in res: LOGGER.warning('"angles" option overrides "rotation"!') else: angles = Angle.from_str(res['rotation', '0 0 0']) angles @= Angle.from_str(inst['angles', '0 0 0']) orig_name = conditions.resolve_value(inst, res['file', '']) filename = instanceLocs.resolve_one(orig_name) if not filename: if not res.bool('silentLookup'): LOGGER.warning('Bad filename for "{}" when adding overlay!', orig_name) # Don't bother making a overlay which will be deleted. return None overlay_inst = vmf.create_ent( classname='func_instance', targetname=inst['targetname', ''], file=filename, angles=angles, origin=inst['origin'], fixup_style=res['fixup_style', '0'], ) # Don't run if the fixup block exists.. if srctools.conv_bool(res['copy_fixup', '1']): if 'fixup' not in res and 'localfixup' not in res: # Copy the fixup values across from the original instance for fixup, value in inst.fixup.items(): overlay_inst.fixup[fixup] = value conditions.set_ent_keys(overlay_inst.fixup, inst, res, 'fixup') if res.bool('move_outputs', False): overlay_inst.outputs = inst.outputs inst.outputs = [] if 'offset' in res: overlay_inst['origin'] = conditions.resolve_offset(inst, res['offset']) return overlay_inst
def res_signage(vmf: VMF, inst: Entity, res: Property): """Implement the Signage item.""" sign: Optional[Sign] try: sign = (CONN_SIGNAGES if res.bool('connection') else SIGNAGES)[inst.fixup[consts.FixupVars.TIM_DELAY]] except KeyError: # Blank sign sign = None has_arrow = inst.fixup.bool(consts.FixupVars.ST_ENABLED) make_4x4 = res.bool('set4x4tile') sign_prim: Optional[Sign] sign_sec: Optional[Sign] if has_arrow: sign_prim = sign sign_sec = SIGNAGES['arrow'] elif sign is not None: sign_prim = sign.primary or sign sign_sec = sign.secondary or None else: # Neither sign or arrow, delete this. inst.remove() return origin = Vec.from_str(inst['origin']) orient = Matrix.from_angle(Angle.from_str(inst['angles'])) normal = -orient.up() forward = -orient.forward() prim_pos = Vec(0, -16, -64) @ orient + origin sec_pos = Vec(0, +16, -64) @ orient + origin template_id = res['template_id', ''] if inst.fixup.bool(consts.FixupVars.ST_REVERSED): # Flip around. forward = -forward prim_visgroup = 'secondary' sec_visgroup = 'primary' prim_pos, sec_pos = sec_pos, prim_pos else: prim_visgroup = 'primary' sec_visgroup = 'secondary' if sign_prim and sign_sec: inst['file'] = res['large_clip', ''] inst['origin'] = (prim_pos + sec_pos) / 2 else: inst['file'] = res['small_clip', ''] inst['origin'] = prim_pos if sign_prim else sec_pos brush_faces: List[Side] = [] tiledef: Optional[tiling.TileDef] = None if template_id: if sign_prim and sign_sec: visgroup = [prim_visgroup, sec_visgroup] elif sign_prim: visgroup = [prim_visgroup] else: visgroup = [sec_visgroup] template = template_brush.import_template( vmf, template_id, origin, orient, force_type=template_brush.TEMP_TYPES.detail, additional_visgroups=visgroup, ) for face in template.detail.sides(): if face.normal() == normal: brush_faces.append(face) else: # Direct on the surface. # Find the grid pos first. grid_pos = (origin // 128) * 128 + 64 try: tiledef = tiling.TILES[(grid_pos + 128 * normal).as_tuple(), (-normal).as_tuple()] except KeyError: LOGGER.warning( "Can't place signage at ({}) in ({}) direction!", origin, normal, exc_info=True, ) return if sign_prim is not None: over = place_sign( vmf, brush_faces, sign_prim, prim_pos, normal, forward, rotate=True, ) if tiledef is not None: tiledef.bind_overlay(over) if make_4x4: try: tile, u, v = tiling.find_tile(prim_pos, -normal) except KeyError: pass else: tile[u, v] = tile[u, v].as_4x4 if sign_sec is not None: if has_arrow and res.bool('arrowDown'): # Arrow texture points down, need to flip it. forward = -forward over = place_sign( vmf, brush_faces, sign_sec, sec_pos, normal, forward, rotate=not has_arrow, ) if tiledef is not None: tiledef.bind_overlay(over) if make_4x4: try: tile, u, v = tiling.find_tile(sec_pos, -normal) except KeyError: pass else: tile[u, v] = tile[u, v].as_4x4
def add_timer_relay(item: Item, has_sounds: bool) -> None: """Make a relay to play timer sounds, or fire once the outputs are done.""" assert item.timer is not None rl_name = item.name + '_timer_rl' relay = item.inst.map.create_ent( 'logic_relay', targetname=rl_name, startDisabled=0, spawnflags=0, ) if item.config.timer_sound_pos: relay_loc = item.config.timer_sound_pos.copy() relay_loc.localise( Vec.from_str(item.inst['origin']), Angle.from_str(item.inst['angles']), ) relay['origin'] = relay_loc else: relay['origin'] = item.inst['origin'] for cmd in item.config.timer_done_cmd: if cmd: relay.add_out( Output( 'OnTrigger', conditions.local_name(item.inst, cmd.target) or item.inst, conditions.resolve_value(item.inst, cmd.input), conditions.resolve_value(item.inst, cmd.params), inst_in=cmd.inst_in, delay=item.timer + cmd.delay, times=cmd.times, )) if item.config.timer_sound_pos is not None and has_sounds: timer_sound = options.get(str, 'timer_sound') timer_cc = options.get(str, 'timer_sound_cc') # The default sound has 'ticking' closed captions. # So reuse that if the style doesn't specify a different noise. # If explicitly set to '', we don't use this at all! if timer_cc is None and timer_sound != 'Portal.room1_TickTock': timer_cc = 'Portal.room1_TickTock' if timer_cc: timer_cc = 'cc_emit ' + timer_cc # Write out the VScript code to precache the sound, and play it on # demand. relay['vscript_init_code'] = ( 'function Precache() {' f'self.PrecacheSoundScript(`{timer_sound}`)' '}') relay['vscript_init_code2'] = ('function snd() {' f'self.EmitSound(`{timer_sound}`)' '}') packing.pack_files(item.inst.map, timer_sound, file_type='sound') for delay in range(item.timer): relay.add_out( Output( 'OnTrigger', '!self', 'CallScriptFunction', 'snd', delay=delay, )) if timer_cc: relay.add_out( Output( 'OnTrigger', '@command', 'Command', timer_cc, delay=delay, )) for outputs, cmd in [(item.timer_output_start(), 'Trigger'), (item.timer_output_stop(), 'CancelPending')]: for output in outputs: item.add_io_command(output, rl_name, cmd)
def make_tag_fizz(inst: Entity) -> None: """Create the Tag fizzler.""" fizzler: Optional[Fizzler] = None fizzler_item: Optional[Item] = None # Look for the fizzler instance we want to replace. sign_item = connections.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. if fizz_conn_conf is not None: fizzler_item.config = fizz_conn_conf fizzler_item.enable_cmd = fizz_conn_conf.enable_cmd fizzler_item.disable_cmd = fizz_conn_conf.disable_cmd fizzler_item.sec_enable_cmd = fizz_conn_conf.sec_enable_cmd fizzler_item.sec_disable_cmd = fizz_conn_conf.sec_disable_cmd inst_orient = Matrix.from_angle(Angle.from_str(inst['angles'])) # The actual location of the sign - on the wall sign_loc = Vec.from_str(inst['origin']) + Vec(0, 0, -64) @ inst_orient fizz_norm_axis = round(fizzler.normal(), 3).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_normal = inst_orient.up() loc = Vec.from_str(inst['origin']) if disable_other or (blue_enabled and oran_enabled): inst['file'] = inst_frame_double conditions.ALL_INST.add(inst_frame_double.casefold()) # On a wall, and pointing vertically if abs(inst_normal.z) < 0.01 and abs(inst_orient.left().z) > 0.01: # 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) @ inst_orient blue_loc = loc + offset oran_loc = loc - offset else: inst['file'] = inst_frame_single conditions.ALL_INST.add(inst_frame_single.casefold()) # 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 = conditions.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: conditions.add_inst( vmf, file=blue_sign, targetname=inst['targetname'], angles=sign_angle, origin=blue_loc, ) if oran_sign: conditions.add_inst( vmf, file=oran_sign, targetname=inst['targetname'], angles=sign_angle, origin=oran_loc, ) # Now modify the fizzler... # Subtract the sign from the list of connections, but don't go below # zero fizzler.base_inst.fixup['$connectioncount'] = max( 0, fizzler.base_inst.fixup.int('$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 = pos_oran = False neg_blue = 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') output = 'OnStartTouch' pos_trig['origin'] = neg_trig['origin'] = fizzler.base_inst['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = conditions.local_name(fizzler.base_inst, 'trig_pos') neg_trig['targetname'] = conditions.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', neg_blue, )) pos_trig.outputs.append( Output( output, '@BlueIsEnabled', 'SetValue', 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'] = conditions.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_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 `angles` must be set to determine the required position, or `facing` can be set for older files. If `global` is set, the spawn point will be absolute instead of relative to the current instance. """ if vbsp.GAME_MODE != 'COOP': return conditions.RES_EXHAUSTED is_tag = options.get(str, 'game_id') == utils.STEAM_IDS['TAG'] origin = res.vec('origin') if 'angles' in res: angles = Angle.from_str(res['angles']) else: # Older system, specify the forward direction. angles = res.vec('facing', z=1).to_angle() # Some styles might want to ignore the instance we're running on. if not res.bool('global'): orient = Matrix.from_angle(Angle.from_str(inst['angles'])) origin @= orient angles @= orient origin += Vec.from_str(inst['origin']) 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', # Spelling mistake is correct. 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 conditions.RES_EXHAUSTED