def res_unst_scaffold_setup(res: Property): group = res['group', 'DEFAULT_GROUP'] if group not in SCAFFOLD_CONFIGS: # Store our values in the CONFIGS dictionary targ_inst, links = SCAFFOLD_CONFIGS[group] = {}, {} else: # Grab the already-filled values, and add to them targ_inst, links = SCAFFOLD_CONFIGS[group] for block in res.find_all("Instance"): conf = { # If set, adjusts the offset appropriately 'is_piston': srctools.conv_bool(block['isPiston', '0']), 'rotate_logic': srctools.conv_bool(block['AlterAng', '1'], True), 'off_floor': Vec.from_str(block['FloorOff', '0 0 0']), 'off_wall': Vec.from_str(block['WallOff', '0 0 0']), 'logic_start': resolve_optional(block, 'startlogic'), 'logic_end': resolve_optional(block, 'endLogic'), 'logic_mid': resolve_optional(block, 'midLogic'), 'logic_start_rev': resolve_optional(block, 'StartLogicRev'), 'logic_end_rev': resolve_optional(block, 'EndLogicRev'), 'logic_mid_rev': resolve_optional(block, 'EndLogicRev'), 'inst_wall': resolve_optional(block, 'wallInst'), 'inst_floor': resolve_optional(block, 'floorInst'), 'inst_offset': resolve_optional(block, 'offsetInst'), # Specially rotated to face the next track! 'inst_end': resolve_optional(block, 'endInst'), # If it's allowed to point any direction, not just 90 degrees. 'free_rotation': block.bool('free_rotate_end'), } for logic_type in ('logic_start', 'logic_mid', 'logic_end'): if conf[logic_type + '_rev'] is None: conf[logic_type + '_rev'] = conf[logic_type] for inst in instanceLocs.resolve(block['file']): targ_inst[inst] = conf # We need to provide vars to link the tracks and beams. for block in res.find_all('LinkEnt'): # The name for this set of entities. # It must be a '@' name, or the name will be fixed-up incorrectly! loc_name = block['name'] if not loc_name.startswith('@'): loc_name = '@' + loc_name links[block['nameVar']] = { 'name': loc_name, # The next entity (not set in end logic) 'next': block['nextVar'], # A '*' name to reference all the ents (set on the start logic) 'all': block['allVar', None], } return group # We look up the group name to find the values.
def flag_file_equal(flag: Property) -> Callable[[Entity], bool]: """Evaluates True if the instance matches the given file.""" inst_list = set(instanceLocs.resolve(flag.value)) def check_inst(inst: Entity) -> bool: """Each time, check if no matching instances exist, so we can skip conditions.""" if conditions.ALL_INST.isdisjoint(inst_list): raise conditions.Unsatisfiable return inst['file'].casefold() in inst_list return check_inst
def res_track_plat(vmf: VMF, res: Property): """Logic specific to Track Platforms. This allows switching the instances used depending on if the track is horizontal or vertical and sets the track targetnames to a useful value. This should be run unconditionally, not once per item. Values: * `orig_item`: The "<ITEM_ID>" for the track platform, with angle brackets. This is used to determine all the instance filenames. * `single_plat`: An instance used for the entire platform, if it's one rail long (and therefore can't move). * `track_name`: If set, rename track instances following the pattern `plat_name-track_nameXX`. Otherwise all tracks will receive the name of the platform. * `plat_suffix`: If set, add a `_vert` or `_horiz` suffix to the platform. * `plat_var`: If set, save the orientation (`vert`/`horiz`) to the provided $fixup variable. * `track_var`: If set, save `N`, `S`, `E`, or `W` to the provided $fixup variable to indicate the relative direction the top faces. """ # Get the instances from editoritems ( inst_bot_grate, inst_bottom, inst_middle, inst_top, inst_plat, inst_plat_oscil, inst_single ) = instanceLocs.resolve(res['orig_item']) single_plat_inst = instanceLocs.resolve_one(res['single_plat', '']) track_targets = res['track_name', ''] track_files = [inst_bottom, inst_middle, inst_top, inst_single] platforms = [inst_plat, inst_plat_oscil] # All the track_set in the map, indexed by origin track_instances = { Vec.from_str(inst['origin']).as_tuple(): inst for inst in vmf.by_class['func_instance'] if inst['file'].casefold() in track_files } LOGGER.debug('Track instances:') LOGGER.debug('\n'.join( '{!s}: {}'.format(k, v['file']) for k, v in track_instances.items() )) if not track_instances: return RES_EXHAUSTED # Now we loop through all platforms in the map, and then locate their # track_set for plat_inst in vmf.by_class['func_instance']: if plat_inst['file'].casefold() not in platforms: continue # Not a platform! LOGGER.debug('Modifying "' + plat_inst['targetname'] + '"!') plat_loc = Vec.from_str(plat_inst['origin']) # The direction away from the wall/floor/ceil normal = Vec(0, 0, 1).rotate_by_str( plat_inst['angles'] ) for tr_origin, first_track in track_instances.items(): if plat_loc == tr_origin: # Check direction if normal == Vec(0, 0, 1).rotate( *Vec.from_str(first_track['angles']) ): break else: raise Exception('Platform "{}" has no track!'.format( plat_inst['targetname'] )) track_type = first_track['file'].casefold() if track_type == inst_single: # Track is one block long, use a single-only instance and # remove track! plat_inst['file'] = single_plat_inst conditions.ALL_INST.add(single_plat_inst.casefold()) first_track.remove() continue # Next platform track_set: set[Entity] = set() if track_type == inst_top or track_type == inst_middle: # search left track_scan( track_set, track_instances, first_track, middle_file=inst_middle, x_dir=-1, ) if track_type == inst_bottom or track_type == inst_middle: # search right track_scan( track_set, track_instances, first_track, middle_file=inst_middle, x_dir=+1, ) # Give every track a targetname matching the platform for ind, track in enumerate(track_set, start=1): if track_targets == '': track['targetname'] = plat_inst['targetname'] else: track['targetname'] = ( plat_inst['targetname'] + '-' + track_targets + str(ind) ) # Now figure out which way the track faces: # The direction of the platform surface facing = Vec(-1, 0, 0).rotate_by_str(plat_inst['angles']) # The direction horizontal track is offset uaxis = Vec(x=1).rotate_by_str(first_track['angles']) vaxis = Vec(y=1).rotate_by_str(first_track['angles']) if uaxis == facing: plat_facing = 'vert' track_facing = 'E' elif uaxis == -facing: plat_facing = 'vert' track_facing = 'W' elif vaxis == facing: plat_facing = 'horiz' track_facing = 'N' elif vaxis == -facing: plat_facing = 'horiz' track_facing = 'S' else: raise ValueError('Facing {} is not U({}) or V({})!'.format( facing, uaxis, vaxis, )) if res.bool('plat_suffix'): conditions.add_suffix(plat_inst, '_' + plat_facing) plat_var = res['plat_var', ''] if plat_var: plat_inst.fixup[plat_var] = plat_facing track_var = res['track_var', ''] if track_var: plat_inst.fixup[track_var] = track_facing for track in track_set: track.fixup.update(plat_inst.fixup) return RES_EXHAUSTED # Don't re-run
def flag_file_equal(inst: Entity, flag: Property): """Evaluates True if the instance matches the given file.""" return inst['file'].casefold() in instanceLocs.resolve(flag.value)
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 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() inst = None 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() del inst # Make sure we don't use this later. if not marker_names: # No markers in the map - abort return RES_EXHAUSTED item_id = res['markerItem'] # Synthesise the connection config used for the final trigger. conn_conf_sp = connections.Config( 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 = conn_conf_coop = None coop_only_once = False else: coop_only_once = res.bool('coopOnce') conn_conf_coop = connections.Config( id=item_id + ':TRIGGER', 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 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=options.get(Vec, "global_ents_loc"), angles='0 0 0', ) trig_ent.solids = [ vmf.make_prism( bbox_min, bbox_max, mat=consts.Tools.TRIGGER, ).solid, ] # Use 'keys' and 'localkeys' blocks to set all the other keyvalues. conditions.set_ent_keys(trig_ent, inst1, 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(inst1, 'man'), origin=origin, ) item = connections.Item( out_ent, conn_conf_coop, ant_floor_style=mark1.ant_floor_style, ant_wall_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, conn_conf_sp, ant_floor_style=mark1.ant_floor_style, ant_wall_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 associate_faith_plates(vmf: VMF) -> None: """Parse through the map, collecting all faithplate segments. Tiling, instancelocs and connections must have been parsed first. Once complete all targets have been removed. This is done as a meta-condition to allow placing tiles we will attach to. """ # Find all the triggers and targets first. triggers: Dict[str, Entity] = {} helper_trigs: Dict[str, Entity] = {} paint_trigs: Dict[str, Entity] = {} for trig in vmf.by_class['trigger_catapult']: name = trig['targetname'] # Conveniently, we can determine what sort of catapult was made by # examining the local name used. if name.endswith('-helperTrigger'): helper_trigs[name[:-14]] = trig # Also store None in the main trigger if no key is there, # so we can detect missing main triggers... triggers.setdefault(name[:-14], None) elif name.endswith('-trigger'): triggers[name[:-8]] = trig # Remove the original relay inputs. We need to keep the output # to the helper if necessary. trig.outputs[:] = [out for out in trig.outputs if not out.inst_in] elif name.endswith('-catapult'): # Paint droppers. paint_trigs[name[:-9]] = trig else: LOGGER.warning('Unknown trigger "{}"?', name) target_to_pos: Dict[str, Union[Vec, tiling.TileDef]] = {} for targ in vmf.by_class['info_target']: name = targ['targetname'] # All should be faith targets, with this name. if not name.endswith('-target'): LOGGER.warning('Unknown info_target "{}" @ {}?', name, targ['origin']) continue name = name[:-7] # Find the tile we're attached to. Unfortunately no angles, so we # have to try both directions. origin = Vec.from_str(targ['origin']) # If the plate isn't on a tile (placed on goo for example), # use the direct position. tile = Vec.from_str(targ['origin']) grid_pos: Vec = origin // 128 * 128 + 64 norm = (origin - grid_pos).norm() # If we're on the floor above the top of goo, move down to the surface. block_type = brushLoc.POS['world':tile - (0, 0, 64)] if block_type.is_goo and block_type.is_top: tile.z -= 32 for norm in [norm, -norm]: # Try both directions. try: tile = tiling.TILES[(origin - 64 * norm).as_tuple(), norm.as_tuple(), ] break except KeyError: pass # We don't need the entity anymore, we'll regenerate them later. targ.remove() target_to_pos[name] = tile # Loop over instances, recording plates and moving targets into the tiledefs. instances: Dict[str, Entity] = {} faith_targ_file = instanceLocs.resolve('<ITEM_CATAPULT_TARGET>') for inst in vmf.by_class['func_instance']: if inst['file'].casefold() in faith_targ_file: inst.remove() # Don't keep the targets. origin = Vec.from_str(inst['origin']) norm = Vec(z=1).rotate_by_str(inst['angles']) try: tile = tiling.TILES[(origin - 128 * norm).as_tuple(), norm.as_tuple()] except KeyError: LOGGER.warning('No tile for bullseye at {}!', origin - 64 * norm) continue tile.bullseye_count += 1 tile.add_portal_helper() else: instances[inst['targetname']] = inst # Now, combine into plate objects for each. for name, trig in triggers.items(): if trig is None: raise ValueError(f'Faith plate {name} has a helper ' 'trigger but no main trigger!') try: pos = target_to_pos[name] except KeyError: # No position, it's a straight plate. PLATES[name] = StraightPlate(instances[name], trig, helper_trigs[name]) else: # Target position, angled plate. PLATES[name] = AngledPlate(instances[name], trig, pos) # And paint droppers for name, trig in paint_trigs.items(): try: pos = target_to_pos[name] except KeyError: LOGGER.warning('No target for paint dropper {}!', name) continue # Target position, angled plate. PLATES[name] = PaintDropper(instances[name], trig, pos)
def flag_has_inst(flag: Property) -> Callable[[Entity], bool]: """Checks if the given instance is present anywhere in the map.""" flags = set(instanceLocs.resolve(flag.value)) return lambda inst: flags.isdisjoint(conditions.ALL_INST)