def res_replace_instance(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. """ import vbsp origin = Vec.from_str(inst['origin']) angles = inst['angles'] if not srctools.conv_bool(res['keep_instance', '0'], False): 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' vbsp.VMF.add_ent(new_ent) conditions.set_ent_keys(new_ent, inst, res) origin += Vec.from_str(new_ent['origin']).rotate_by_str(angles) new_ent['origin'] = origin new_ent['angles'] = angles new_ent['targetname'] = inst['targetname']
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 join_markers(inst_a, inst_b, is_start=False): """Join two marker ents together with corners. This returns a list of solids used for the vphysics_motion trigger. """ origin_a = Vec.from_str(inst_a['ent']['origin']) origin_b = Vec.from_str(inst_b['ent']['origin']) norm_a = Vec(-1, 0, 0).rotate_by_str(inst_a['ent']['angles']) norm_b = Vec(-1, 0, 0).rotate_by_str(inst_b['ent']['angles']) config = inst_a['conf'] if norm_a == norm_b: # Either straight-line, or s-bend. dist = (origin_a - origin_b).mag() if origin_a + (norm_a * dist) == origin_b: make_straight( origin_a, norm_a, dist, config, is_start, ) # else: S-bend, we don't do the geometry for this.. return if norm_a == -norm_b: # U-shape bend.. make_ubend( origin_a, origin_b, norm_a, config, max_size=inst_a['size'], ) return try: corner_ang, flat_angle = CORNER_ANG[norm_a.as_tuple(), norm_b.as_tuple()] if origin_a[flat_angle] != origin_b[flat_angle]: # It needs to be flat in this angle! raise ValueError except ValueError: # The tubes need two corners to join together - abort for that. return else: make_bend( origin_a, origin_b, norm_a, norm_b, corner_ang, config, max_size=inst_a['size'], )
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': block['startlogic', ''], 'logic_end': block['endLogic', ''], 'logic_mid': block['midLogic', ''], 'logic_start_rev': block['StartLogicRev', None], 'logic_end_rev': block['EndLogicRev', None], 'logic_mid_rev': block['EndLogicRev', None], 'inst_wall': block['wallInst', ''], 'inst_floor': block['floorInst', ''], 'inst_offset': block['offsetInst', None], # Specially rotated to face the next track! 'inst_end': block['endInst', None], } 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 res_camera_setup(res: Property): return { 'cam_off': Vec.from_str(res['CamOff', '']), 'yaw_off': Vec.from_str(res['YawOff', '']), 'pitch_off': Vec.from_str(res['PitchOff', '']), 'yaw_inst': instanceLocs.resolve_one(res['yawInst', '']), 'pitch_inst': instanceLocs.resolve_one(res['pitchInst', '']), 'yaw_range': srctools.conv_int(res['YawRange', ''], 90), 'pitch_range': srctools.conv_int(res['YawRange', ''], 90), }
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": block["startlogic", ""], "logic_end": block["endLogic", ""], "logic_mid": block["midLogic", ""], "logic_start_rev": block["StartLogicRev", None], "logic_end_rev": block["EndLogicRev", None], "logic_mid_rev": block["EndLogicRev", None], "inst_wall": block["wallInst", ""], "inst_floor": block["floorInst", ""], "inst_offset": block["offsetInst", None], # Specially rotated to face the next track! "inst_end": block["endInst", None], } 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 resolve_inst(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_goo_at_loc(inst: Entity, flag: Property): """Check to see if a given location is submerged in goo. 0 0 0 is the origin of the instance, values are in 128 increments. """ pos = Vec.from_str(flag.value).rotate_by_str(inst['angles', '0 0 0']) pos *= 128 pos += Vec.from_str(inst['origin']) # Round to 128 units, then offset to the center pos = pos // 128 * 128 + 64 # type: Vec val = pos.as_tuple() in GOO_LOCS return val
def res_make_funnel_light(inst: Entity): """Place a light for Funnel items.""" oran_on = inst.fixup.bool('$start_reversed') need_blue = need_oran = False name = '' if inst.fixup['$connectioncount_polarity'] != '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: if oran_on: need_oran = True else: need_blue = True loc = Vec(0, 0, -56) loc.localise(Vec.from_str(inst['origin']), Vec.from_str(inst['angles'])) 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_monitor(inst: Entity, res: Property) -> None: """Result for the monitor component. """ import vbsp ( break_inst, bullseye_name, bullseye_loc, bullseye_parent, ) = res.value ALL_MONITORS.append(Monitor(inst)) 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 vbsp_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']), Vec.from_str(inst['angles']), ) bullseye_name = local_name(inst, bullseye_name) inst.map.create_ent( classname='npc_bullseye', targetname=bullseye_name, parentname=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', 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 get_config( node: item_chain.Node, ) -> Tuple[str, Vec]: """Compute the config values for a node.""" orient = ( 'floor' if Vec(0, 0, 1).rotate_by_str(node.inst['angles']) == (0, 0, 1) else 'wall' ) # Find the offset used for the platform. offset = (node.conf['off_' + orient]).copy() # type: Vec if node.conf['is_piston']: # Adjust based on the piston position offset.z += 128 * srctools.conv_int( node.inst.fixup[ '$top_level' if node.inst.fixup[ '$start_up'] == '1' else '$bottom_level' ] ) offset.rotate_by_str(node.inst['angles']) offset += Vec.from_str(node.inst['origin']) return orient, offset
def find_glass_items(config, vmf: VMF) -> Iterator[Tuple[str, Vec, Vec, Vec, dict]]: """Find the bounding boxes for all the glass items matching a config. This yields (targetname, min, max, normal, config) tuples. """ # targetname -> min, max, normal, config glass_items = {} for inst in vmf.by_class['func_instance']: # type: Entity try: conf = config[inst['file'].casefold()] except KeyError: continue targ = inst['targetname'] norm = Vec(x=1).rotate_by_str(inst['angles']) origin = Vec.from_str(inst['origin']) - 64 * norm try: bbox_min, bbox_max, group_norm, group_conf = glass_items[targ] except KeyError: # First of this group.. bbox_min, bbox_max = origin.copy(), origin.copy() group_norm = norm.copy() glass_items[targ] = bbox_min, bbox_max, group_norm, conf else: bbox_min.min(origin) bbox_max.max(origin) assert group_norm == norm, '"{}" is inconsistently rotated!'.format(targ) assert group_conf is conf, '"{}" has multiple configs!'.format(targ) inst.remove() for targ, (bbox_min, bbox_max, norm, conf) in glass_items.items(): yield targ, bbox_min, bbox_max, norm, conf
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(z=-1).rotate_by_str(inst['angles']) origin = 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 track_scan( tr_set: Set[Entity], track_inst: Dict[Tuple[float, float, float], Entity], start_track: Entity, middle_file: str, x_dir: int, ): """Build a set of track instances extending from a point. :param track_inst: A dictionary mapping origins to track instances :param start_track: The instance we start on :param middle_file: The file for the center track piece :param x_dir: The direction to look (-1 or 1) """ track = start_track move_dir = Vec(x_dir*128, 0, 0).rotate_by_str(track['angles']) while track: tr_set.add(track) next_pos = Vec.from_str(track['origin']) + move_dir track = track_inst.get(next_pos.as_tuple(), None) if track is None: return if track['file'].casefold() != middle_file: # If the next piece is an end section, add it then quit tr_set.add(track) return
def mon_remove_bullseyes(inst: Entity) -> Optional[object]: """Remove bullsyes used for cameras.""" if not BULLSYE_LOCS: return RES_EXHAUSTED if inst['file'].casefold() not in instanceLocs.resolve('<ITEM_CATAPULT_TARGET>'): return origin = Vec(0, 0, -64) origin.localise(Vec.from_str(inst['origin']), Vec.from_str(inst['angles'])) origin = origin.as_tuple() LOGGER.info('Pos: {} -> ', origin, BULLSYE_LOCS[origin]) if BULLSYE_LOCS[origin]: BULLSYE_LOCS[origin] -= 1 inst.remove()
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 resolve_offset(inst, value: str, scale: float=1, zoff: float=0) -> Vec: """Retrieve an offset from an instance var. This allows several special values: * $var to read from a variable * <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 = value.casefold() # Offset the overlay by the given distance # Some special placeholder values: if value == '<piston_start>' or value == '<piston>': if inst.fixup.bool(const.FixupVars.PIST_IS_UP): value = '<piston_top>' else: value = '<piston_bottom>' elif value == '<piston_end>': if inst.fixup.bool(const.FixupVars.PIST_IS_UP): value = '<piston_bottom>' else: value = '<piston_top>' if value == '<piston_bottom>': offset = Vec( z=inst.fixup.int(const.FixupVars.PIST_BTM) * 128, ) elif value == '<piston_top>': offset = Vec( z=inst.fixup.int(const.FixupVars.PIST_TOP) * 128, ) else: # Regular vector offset = Vec.from_str(resolve_value(inst, value)) * scale offset.z += zoff offset.localise( Vec.from_str(inst['origin']), Vec.from_str(inst['angles']), ) return offset
def res_water_splash_setup(res: Property): parent = res['parent'] name = res['name'] scale = srctools.conv_float(res['scale', ''], 8.0) pos1 = Vec.from_str(res['position', '']) calc_type = res['type', ''] pos2 = res['position2', ''] fast_check = srctools.conv_bool(res['fast_check', '']) return name, parent, scale, pos1, pos2, calc_type, fast_check
def res_import_template_setup(res: Property): temp_id = res['id'].casefold() face = Vec.from_str(res['face_pos', '0 0 -64']) norm = Vec.from_str(res['normal', '0 0 1']) replace_tex = defaultdict(list) for prop in res.find_key('replace', []): replace_tex[prop.name].append(prop.value) offset = Vec.from_str(res['offset', '0 0 0']) return ( temp_id, dict(replace_tex), face, norm, offset, )
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 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 get_color(): """Parse out the color.""" color = var.get() if color.startswith('#'): try: r = int(color[1:3], base=16) g = int(color[3:5], base=16) b = int(color[5:], base=16) except ValueError: LOGGER.warning('Invalid RGB value: "{}"!', color) r = g = b = 128 else: r, g, b = map(int, Vec.from_str(color, 128, 128, 128)) return r, g, b
def res_translate_inst(inst: Entity, res: Property): """Translate the instance locally by the given amount. The special values <piston>, <piston_bottom> and <piston_top> can be used to offset it based on the starting position, bottom or top position of a piston platform. """ folded_val = res.value.casefold() if folded_val == '<piston>': folded_val = ( '<piston_top>' if srctools.conv_bool(inst.fixup['$start_up']) else '<piston_bottom>' ) if folded_val == '<piston_top>': val = Vec(z=128 * srctools.conv_int(inst.fixup['$top_level', '1'], 1)) elif folded_val == '<piston_bottom>': val = Vec(z=128 * srctools.conv_int(inst.fixup['$bottom_level', '0'], 0)) else: val = Vec.from_str(res.value) offset = val.rotate_by_str(inst['angles']) inst['origin'] = (offset + Vec.from_str(inst['origin'])).join(' ')
def get_itemconf( name: Union[str, Tuple[str, str]], default: Optional[OptionT], timer_delay: int=None, ) -> Optional[OptionT]: """Get an itemconfig value. The name should be an 'ID:Section', or a tuple of the same. The type of the default sets what value it will be converted to. None returns the string, or None if not present. If set, timer_value is the value used for the timer. """ if name == '': return default try: if isinstance(name, tuple): group_id, wid_id = name else: group_id, wid_id = name.split(':') except ValueError: LOGGER.warning('Invalid item config: {!r}!', name) return default wid_id = wid_id.casefold() if timer_delay is not None: if timer_delay < 3 or timer_delay > 30: wid_id += '_inf' else: wid_id += '_{}'.format(timer_delay) value = ITEM_CONFIG.get_val(group_id, wid_id, '') if not value: return default if isinstance(default, str) or default is None: return value elif isinstance(default, Vec): return Vec.from_str(value, default.x, default.y, default.z) elif isinstance(default, bool): return srctools.conv_bool(value, default) elif isinstance(default, float): return srctools.conv_int(value, default) elif isinstance(default, int): return srctools.conv_int(value, default) else: raise TypeError('Invalid default type "{}"!'.format(type(default).__name__))
def find_indicator_panels(inst: Entity): """We need to locate indicator panels, so they aren't overwritten. """ if inst['file'].casefold() not in resolve_inst('[indpan]'): return loc = Vec(0, 0, -64).rotate_by_str(inst['angles']) loc += Vec.from_str(inst['origin']) # Sometimes (light bridges etc) a sign will be halfway between # tiles, so in that case we need to force 2 tiles. loc_min = (loc - (15, 15, 0)) // 32 * 32 # type: Vec loc_max = (loc + (15, 15, 0)) // 32 * 32 # type: Vec loc_min += (16, 16, 0) loc_max += (16, 16, 0) FORCE_LOCATIONS.add(loc_min.as_tuple()) FORCE_LOCATIONS.add(loc_max.as_tuple())
def res_checkpoint_trigger(inst: Entity, res: Property): """Generate a trigger underneath coop checkpoint items """ if vbsp.GAME_MODE == 'SP': # We can't have a respawn dropper in singleplayer. # Not generating the trigger means it's not going to # do anything. return pos = brushLoc.POS.raycast_world( Vec.from_str(inst['origin']), direction=(0, 0, -1), ) bbox_min = pos - (192, 192, 64) bbox_max = pos + (192, 192, 64) # Find triggers already placed next to ours, and # merge with them if that's the case for offset in CHECKPOINT_NEIGHBOURS: near_pos = pos + offset try: trig = CHECKPOINT_TRIG[near_pos.as_tuple()] break except KeyError: pass else: # None found, make one. trig = inst.map.create_ent( classname='trigger_playerteam', origin=pos, ) trig.solids = [] CHECKPOINT_TRIG[pos.as_tuple()] = trig trig.solids.append(inst.map.make_prism( bbox_min, bbox_max, mat=const.Tools.TRIGGER, ).solid) for prop in res: out = Output.parse(prop) out.target = conditions.local_name(inst, out.target) trig.add_out(out)
def res_calc_opposite_wall_dist(inst: Entity, res: Property): """Calculate the distance between this item and the opposing wall. The value is stored in the `$var` specified by the property value. Alternately it is set by `ResultVar`, and `offset` adds or subtracts to the value. `GooCollide` means that it will stop when goo is found, otherwise it is ignored. `GooAdjust` means additionally if the space is goo, the distance will be modified so that it specifies the surface of the goo. """ if res.has_children(): result_var = res['ResultVar'] dist_off = res.float('offset') collide_goo = res.bool('GooCollide') adjust_goo = res.bool('GooAdjust') else: result_var = res.value dist_off = 0 collide_goo = adjust_goo = False origin = Vec.from_str(inst['origin']) normal = Vec(z=1).rotate_by_str(inst['angles']) mask = [ brushLoc.Block.SOLID, brushLoc.Block.EMBED, brushLoc.Block.PIT_BOTTOM, brushLoc.Block.PIT_SINGLE, ] # Only if actually downward. if normal == (0, 0, -1) and collide_goo: mask.append(brushLoc.Block.GOO_TOP) mask.append(brushLoc.Block.GOO_SINGLE) opposing_pos = brushLoc.POS.raycast_world( origin, normal, mask, ) if adjust_goo and brushLoc.POS['world': opposing_pos + 128*normal].is_goo: # If the top is goo, adjust so the 64 below is the top of the goo. dist_off += 32 inst.fixup[result_var] = (origin - opposing_pos).mag() + dist_off
def make_corner(origin, angle, size, config): vbsp.VMF.create_ent( classname='func_instance', origin=origin, angles=angle, file=config['corner', size], ) temp = config['corner_temp', size] if temp: temp_solids = template_brush.import_template( temp, origin=origin, angles=Vec.from_str(angle), force_type=template_brush.TEMP_TYPES.world, ).world for solid in temp_solids: vbsp.VMF.remove_brush(solid) motion_trigger(*temp_solids)
def res_hollow_brush(inst: Entity, res: Property): """Hollow out the attached brush, as if EmbeddedVoxel was set. This just removes the surface if it's already an embeddedVoxel. This allows multiple items to embed thinly in the same block without affecting each other. """ loc = Vec(0, 0, -64).rotate_by_str(inst['angles']) loc += Vec.from_str(inst['origin']) try: group = SOLIDS[loc.as_tuple()] except KeyError: LOGGER.warning('No brush for hollowing at ({})', loc) return # No brush here? conditions.hollow_block( group, remove_orig_face=srctools.conv_bool(res['RemoveFace', False]) )
def flag_angles(inst: Entity, flag: Property): """Check that a instance is pointed in a direction. The value should be either just the angle to check, or a block of options: - `Angle`: A unit vector (XYZ value) pointing in a direction, or some keywords: `+z`, `-y`, `N`/`S`/`E`/`W`, `up`/`down`, `floor`/`ceiling`, or `walls` for any wall side. - `From_dir`: The direction the unrotated instance is pointed in. This lets the flag check multiple directions - `Allow_inverse`: If true, this also returns True if the instance is pointed the opposite direction . """ angle = inst['angles', '0 0 0'] if flag.has_children(): targ_angle = flag['direction', '0 0 0'] from_dir = flag['from_dir', '0 0 1'] if from_dir.casefold() in DIRECTIONS: from_dir = Vec(DIRECTIONS[from_dir.casefold()]) else: from_dir = Vec.from_str(from_dir, 0, 0, 1) allow_inverse = srctools.conv_bool(flag['allow_inverse', '0']) else: targ_angle = flag.value from_dir = Vec(0, 0, 1) allow_inverse = False normal = DIRECTIONS.get(targ_angle.casefold(), None) if normal is None: return False # If it's not a special angle, # so it failed the exact match inst_normal = from_dir.rotate_by_str(angle) if normal == 'WALL': # Special case - it's not on the floor or ceiling return not (inst_normal == (0, 0, 1) or inst_normal == (0, 0, -1)) else: return inst_normal == normal or ( allow_inverse and -inst_normal == normal )
def make_vac_track(start, all_markers): """Create a vactube path section. """ start_normal = Vec(-1, 0, 0).rotate_by_str(start['ent']['angles']) # First create the start section.. start_logic = start['ent'].copy() vbsp.VMF.add_ent(start_logic) start_logic['file'] = start['conf']['entry', ( 'ceiling' if (start_normal.z > 0) else 'floor' if (start_normal.z < 0) else 'wall' )] end = start for inst, end in follow_vac_path(all_markers, start): join_markers(inst, end, inst is start) end_loc = Vec.from_str(end['ent']['origin']) end_norm = Vec(-1, 0, 0).rotate_by_str(end['ent']['angles']) # join_markers creates straight parts up-to the marker, but not at it's # location - create the last one. make_straight( end_loc, end_norm, 128, end['conf'], ) # If the end is placed in goo, don't add logic - it isn't visible, and # the object is on a one-way trip anyway. if end_loc.as_tuple() not in GOO_LOCS: end_logic = end['ent'].copy() vbsp.VMF.add_ent(end_logic) end_logic['file'] = end['conf']['exit']
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 improve_item(item: Property) -> None: """Improve editoritems formats in various ways. This operates inplace. """ # OccupiedVoxels does not allow specifying 'volume' regions like # EmbeddedVoxel. Implement that. # First for 32^2 cube sections. for voxel_part in item.find_all("Exporting", "OccupiedVoxels", "SurfaceVolume"): if 'subpos1' not in voxel_part or 'subpos2' not in voxel_part: LOGGER.warning( 'Item {} has invalid OccupiedVoxels part ' '(needs SubPos1 and SubPos2)!', item['type'], ) continue voxel_part.name = "Voxel" pos_1 = None voxel_subprops = list(voxel_part) voxel_part.clear() for prop in voxel_subprops: if prop.name not in ('subpos', 'subpos1', 'subpos2'): voxel_part.append(prop) continue pos_2 = Vec.from_str(prop.value) if pos_1 is None: pos_1 = pos_2 continue bbox_min, bbox_max = Vec.bbox(pos_1, pos_2) pos_1 = None for pos in Vec.iter_grid(bbox_min, bbox_max): voxel_part.append( Property("Surface", [ Property("Pos", str(pos)), ])) if pos_1 is not None: LOGGER.warning( 'Item {} has only half of SubPos bbox!', item['type'], ) # Full blocks for occu_voxels in item.find_all("Exporting", "OccupiedVoxels"): for voxel_part in list(occu_voxels.find_all("Volume")): del occu_voxels['Volume'] if 'pos1' not in voxel_part or 'pos2' not in voxel_part: LOGGER.warning( 'Item {} has invalid OccupiedVoxels part ' '(needs Pos1 and Pos2)!', item['type']) continue voxel_part.name = "Voxel" bbox_min, bbox_max = Vec.bbox( voxel_part.vec('pos1'), voxel_part.vec('pos2'), ) del voxel_part['pos1'] del voxel_part['pos2'] for pos in Vec.iter_grid(bbox_min, bbox_max): new_part = voxel_part.copy() new_part['Pos'] = str(pos) occu_voxels.append(new_part)
def res_add_brush(inst: Entity, res: Property): """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. """ import vbsp 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.rotate_by_str(inst['angles']) point2.rotate_by_str(inst['angles']) origin = Vec.from_str(inst['origin']) point1 += origin # Then offset to the location of the instance point2 += origin tex_type = res['type', None] if tex_type not in ('white', 'black'): LOGGER.warning( 'AddBrush: "{}" is not a valid brush ' 'color! (white or black)', tex_type, ) tex_type = 'black' dim = point2 - point1 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. x_maxsize = min(dim.y, dim.z) y_maxsize = min(dim.x, dim.z) if x_maxsize <= 32: x_grid = '4x4' elif x_maxsize <= 64: x_grid = '2x2' else: x_grid = 'wall' if y_maxsize <= 32: y_grid = '4x4' elif y_maxsize <= 64: y_grid = '2x2' else: y_grid = 'wall' 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 = vbsp.VMF.make_prism(point1, point2) ':type solids: VLib.PrismFace' # Ensure the faces aren't re-textured later vbsp.IGNORED_FACES.update(solids.solid.sides) solids.north.mat = vbsp.get_tex(tex_type + '.' + y_grid) solids.south.mat = vbsp.get_tex(tex_type + '.' + y_grid) solids.east.mat = vbsp.get_tex(tex_type + '.' + x_grid) solids.west.mat = vbsp.get_tex(tex_type + '.' + x_grid) solids.top.mat = vbsp.get_tex(tex_type + '.floor') solids.bottom.mat = vbsp.get_tex(tex_type + '.ceiling') if srctools.conv_bool(res['detail', False], False): # Add the brush to a func_detail entity vbsp.VMF.create_ent( classname='func_detail' ).solids = [ solids.solid ] else: # Add to the world vbsp.VMF.add_brush(solids.solid)
def mode_ang(comp_ent: Entity, ent: Entity) -> str: """Return the angle of the entity, as a Vector.""" return vs_vec(Vec.from_str(ent['angles']))
def mode_dist(comp_ent: Entity, ent: Entity) -> str: """Return the distance from the ent to the reference.""" scale = conv_float(comp_ent['const'], 1.0) offset = Vec.from_str(ent['origin']) - Vec.from_str(comp_ent['origin']) return offset.mag() * scale
def load_templates(): """Load in the template file, used for import_template().""" with open(TEMPLATE_LOCATION) as file: props = Property.parse(file, TEMPLATE_LOCATION) vmf = srctools.VMF.parse(props, preserve_ids=True) def make_subdict(): return defaultdict(list) # detail_ents[temp_id][visgroup] detail_ents = defaultdict(make_subdict) world_ents = defaultdict(make_subdict) overlay_ents = defaultdict(make_subdict) conf_ents = {} color_pickers = defaultdict(list) for ent in vmf.by_class['bee2_template_world']: world_ents[ ent['template_id'].casefold() ][ ent['visgroup'].casefold() ].extend(ent.solids) for ent in vmf.by_class['bee2_template_detail']: detail_ents[ ent['template_id'].casefold() ][ ent['visgroup'].casefold() ].extend(ent.solids) for ent in vmf.by_class['bee2_template_overlay']: overlay_ents[ ent['template_id'].casefold() ][ ent['visgroup'].casefold() ].append(ent) for ent in vmf.by_class['bee2_template_conf']: conf_ents[ent['template_id'].casefold()] = ent for ent in vmf.by_class['bee2_template_scaling']: temp = ScalingTemplate.parse(ent) TEMPLATES[temp.id.casefold()] = temp for ent in vmf.by_class['bee2_template_colorpicker']: # Parse the colorpicker data. temp_id = ent['template_id'].casefold() try: priority = Decimal(ent['priority']) except ValueError: LOGGER.warning( 'Bad priority for colorpicker in "{}" template!', temp_id.upper(), ) priority = Decimal(0) color_pickers[temp_id].append(ColorPicker( priority, offset=Vec.from_str(ent['origin']), normal=Vec(x=1).rotate_by_str(ent['angles']), sides=ent['faces'].split(' '), grid_snap=srctools.conv_bool(ent['grid_snap']), remove_brush=srctools.conv_bool(ent['remove_brush']), )) for temp_id in set(detail_ents).union(world_ents, overlay_ents): try: conf = conf_ents[temp_id] except KeyError: overlay_faces = [] skip_faces = [] vertical_faces = [] realign_faces = [] else: vertical_faces = conf['vertical_faces'].split() realign_faces = conf['realign_faces'].split() overlay_faces = conf['overlay_faces'].split() skip_faces = conf['skip_faces'].split() TEMPLATES[temp_id.casefold()] = Template( temp_id, world_ents[temp_id], detail_ents[temp_id], overlay_ents[temp_id], skip_faces, realign_faces, overlay_faces, vertical_faces, color_pickers[temp_id], )
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
def get_studio_loc() -> Vec: """Return the location of the voice studio.""" return Vec.from_str(QUOTE_DATA['quote_loc', '-10000 0 0'], x=-10000)
def read_from_map(self, vmf: VMF, has_attr: Dict[str, bool]) -> None: """Given the map file, set blocks.""" from conditions import EMBED_OFFSETS from instance_traits import get_item_id # Starting points to fill air and goo. # We want to fill goo first... air_search_locs: List[Tuple[Vec, bool]] = [] goo_search_locs: List[Tuple[Vec, bool]] = [] for ent in vmf.entities: str_pos = ent['origin', None] if str_pos is None: continue pos = world_to_grid(Vec.from_str(str_pos)) # Exclude entities outside the main area - elevators mainly. # The border should never be set to air! if (0, 0, 0) <= pos <= (25, 25, 25): air_search_locs.append((Vec(pos.x, pos.y, pos.z), False)) # We need to manually set EmbeddedVoxel locations. # These might not be detected for items where there's a block # which is entirely empty - corridors and obs rooms for example. item_id = get_item_id(ent) if item_id: try: embed_locs = EMBED_OFFSETS[item_id] except KeyError: continue angles = Vec.from_str(ent['angles']) for local_pos in embed_locs: world_pos = local_pos - (0, 0, 1) world_pos.localise(pos, angles) self[world_pos] = Block.EMBED can_have_pit = bottomlessPit.pits_allowed() for brush in vmf.brushes[:]: tex = {face.mat.casefold() for face in brush.sides} bbox_min, bbox_max = brush.get_bbox() if ( 'nature/toxicslime_a2_bridge_intro' in tex or 'nature/toxicslime_puzzlemaker_cheap' in tex ): # It's goo! x = bbox_min.x + 64 y = bbox_min.y + 64 g_x = x // 128 g_y = y // 128 is_pit = can_have_pit and bottomlessPit.is_pit(bbox_min, bbox_max) # If goo is multi-level, we want to record all pos! z_pos = range(int(bbox_min.z) + 64, int(bbox_max.z), 128) top_ind = len(z_pos) - 1 for ind, z in enumerate(z_pos): g_z = z // 128 self[g_x, g_y, g_z] = Block.from_pitgoo_attr( is_pit, is_top=(ind == top_ind), is_bottom=(ind == 0), ) # If goo has totally submerged tunnels, they are not filled. # Add each horizontal neighbour to the search list. # If not found they'll be ignored. goo_search_locs += [ (Vec(g_x - 1, g_y, g_z), True), (Vec(g_x + 1, g_y, g_z), True), (Vec(g_x, g_y + 1, g_z), True), (Vec(g_x, g_y - 1, g_z), True), ] # Remove the brush, since we're not going to use it. vmf.remove_brush(brush) # Indicate that this map contains goo/pits if is_pit: has_attr[VOICE_ATTR_PIT] = True else: has_attr[VOICE_ATTR_GOO] = True continue pos = world_to_grid(brush.get_origin(bbox_min, bbox_max)) if bbox_max - bbox_min == (128, 128, 128): # Full block.. self[pos] = Block.SOLID else: # Must be an embbedvoxel block self[pos] = Block.EMBED LOGGER.info( 'Analysed map, filling air... ({} starting positions..)', len(air_search_locs) ) self.fill_air(goo_search_locs + air_search_locs) LOGGER.info('Air filled!')
def res_cutout_tile(res: Property): """Generate random quarter tiles, like in Destroyed or Retro maps. - "MarkerItem" is the instance to look for. - "TileSize" can be "2x2" or "4x4". - rotateMax is the amount of degrees to rotate squarebeam models. Materials: - "squarebeams" is the squarebeams variant to use. - "ceilingwalls" are the sides of the ceiling section. - "floorbase" is the texture under floor sections. - "tile_glue" is used on top of a thinner tile segment. - "clip" is the player_clip texture used over floor segments. (This allows customising the surfaceprop.) - "Floor4x4Black", "Ceil2x2White" and other combinations can be used to override the textures used. """ item = instanceLocs.resolve(res['markeritem']) INST_LOCS = {} # Map targetnames -> surface loc CEIL_IO = [] # Pairs of ceil inst corners to cut out. FLOOR_IO = [] # Pairs of floor inst corners to cut out. overlay_ids = {} # When we replace brushes, we need to fix any overlays # on that surface. MATS.clear() floor_edges = [] # Values to pass to add_floor_sides() at the end sign_loc = set(FORCE_LOCATIONS) # If any signage is present in the map, we need to force tiles to # appear at that location! for over in conditions.VMF.by_class['info_overlay']: if (over['material'].casefold() in FORCE_TILE_MATS and # Only check floor/ceiling overlays over['basisnormal'] in ('0 0 1', '0 0 -1')): loc = Vec.from_str(over['origin']) # Sometimes (light bridges etc) a sign will be halfway between # tiles, so in that case we need to force 2 tiles. loc_min = (loc - (15, 15, 0)) // 32 * 32 # type: Vec loc_max = (loc + (15, 15, 0)) // 32 * 32 # type: Vec loc_min += (16, 16, 0) loc_max += (16, 16, 0) FORCE_LOCATIONS.add(loc_min.as_tuple()) FORCE_LOCATIONS.add(loc_max.as_tuple()) SETTINGS = { 'floor_chance': srctools.conv_int(res['floorChance', '100'], 100), 'ceil_chance': srctools.conv_int(res['ceilingChance', '100'], 100), 'floor_glue_chance': srctools.conv_int(res['floorGlueChance', '0']), 'ceil_glue_chance': srctools.conv_int(res['ceilingGlueChance', '0']), 'rotate_beams': int(srctools.conv_float(res['rotateMax', '0']) * BEAM_ROT_PRECISION), 'beam_skin': res['squarebeamsSkin', '0'], 'base_is_disp': srctools.conv_bool(res['dispBase', '0']), 'quad_floor': res['FloorSize', '4x4'].casefold() == '2x2', 'quad_ceil': res['CeilingSize', '4x4'].casefold() == '2x2', } random.seed(vbsp.MAP_RAND_SEED + '_CUTOUT_TILE_NOISE') noise = SimplexNoise(period=4 * 40) # 4 tiles/block, 50 blocks max # We want to know the number of neighbouring tile cutouts before # placing tiles - blocks away from the sides generate fewer tiles. floor_neighbours = defaultdict(dict) # all_floors[z][x,y] = count for mat_prop in res['Materials', []]: MATS[mat_prop.name].append(mat_prop.value) if SETTINGS['base_is_disp']: # We want the normal brushes to become nodraw. MATS['floorbase_disp'] = MATS['floorbase'] MATS['floorbase'] = ['tools/toolsnodraw'] # Since this uses random data for initialisation, the alpha and # regular will use slightly different patterns. alpha_noise = SimplexNoise(period=4 * 50) else: alpha_noise = None for key, default in TEX_DEFAULT: if key not in MATS: MATS[key] = [default] # Find our marker ents for inst in conditions.VMF.by_class['func_instance']: # type: VLib.Entity if inst['file'].casefold() not in item: continue targ = inst['targetname'] orient = Vec(0, 0, 1).rotate_by_str(inst['angles', '0 0 0']) # Check the orientation of the marker to figure out what to generate if orient == (0, 0, 1): io_list = FLOOR_IO else: io_list = CEIL_IO # Reuse orient to calculate where the solid face will be. loc = (orient * -64) + Vec.from_str(inst['origin']) INST_LOCS[targ] = loc for out in inst.output_targets(): io_list.append((targ, out)) if not inst.outputs and inst.fixup['$connectioncount'] == '0': # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 128x128 tile segment. io_list.append((targ, targ)) inst.remove() # Remove the instance itself from the map. for start_floor, end_floor in FLOOR_IO: if end_floor not in INST_LOCS: # Not a marker - remove this and the antline. for toggle in conditions.VMF.by_target[end_floor]: conditions.remove_ant_toggle(toggle) continue box_min = Vec(INST_LOCS[start_floor]) box_min.min(INST_LOCS[end_floor]) box_max = Vec(INST_LOCS[start_floor]) box_max.max(INST_LOCS[end_floor]) if box_min.z != box_max.z: continue # They're not in the same level! z = box_min.z if SETTINGS['rotate_beams']: # We have to generate 1 model per 64x64 block to do rotation... gen_rotated_squarebeams( box_min - (64, 64, 0), box_max + (64, 64, -8), skin=SETTINGS['beam_skin'], max_rot=SETTINGS['rotate_beams'], ) else: # Make the squarebeams props, using big models if possible gen_squarebeams(box_min + (-64, -64, 0), box_max + (64, 64, -8), skin=SETTINGS['beam_skin']) # Add a player_clip brush across the whole area conditions.VMF.add_brush( conditions.VMF.make_prism( p1=box_min - (64, 64, FLOOR_DEPTH), p2=box_max + (64, 64, 0), mat=MATS['clip'][0], ).solid) # Add a noportal_volume covering the surface, in case there's # room for a portal. noportal_solid = conditions.VMF.make_prism( # Don't go all the way to the sides, so it doesn't affect wall # brushes. p1=box_min - (63, 63, 9), p2=box_max + (63, 63, 0), mat='tools/toolsinvisible', ).solid noportal_ent = conditions.VMF.create_ent( classname='func_noportal_volume', origin=box_min.join(' '), ) noportal_ent.solids.append(noportal_solid) if SETTINGS['base_is_disp']: # Use displacements for the base instead. make_alpha_base( box_min + (-64, -64, 0), box_max + (64, 64, 0), noise=alpha_noise, ) for x, y in utils.iter_grid( min_x=int(box_min.x), max_x=int(box_max.x) + 1, min_y=int(box_min.y), max_y=int(box_max.y) + 1, stride=128, ): # Build the set of all positions.. floor_neighbours[z][x, y] = -1 # Mark borders we need to fill in, and the angle (for func_instance) # The wall is the face pointing inwards towards the bottom brush, # and the ceil is the ceiling of the block above the bordering grid # points. for x in range(int(box_min.x), int(box_max.x) + 1, 128): # North floor_edges.append( BorderPoints( wall=Vec(x, box_max.y + 64, z - 64), ceil=Vec_tuple(x, box_max.y + 128, z), rot=270, )) # South floor_edges.append( BorderPoints( wall=Vec(x, box_min.y - 64, z - 64), ceil=Vec_tuple(x, box_min.y - 128, z), rot=90, )) for y in range(int(box_min.y), int(box_max.y) + 1, 128): # East floor_edges.append( BorderPoints( wall=Vec(box_max.x + 64, y, z - 64), ceil=Vec_tuple(box_max.x + 128, y, z), rot=180, )) # West floor_edges.append( BorderPoints( wall=Vec(box_min.x - 64, y, z - 64), ceil=Vec_tuple(box_min.x - 128, y, z), rot=0, )) # Now count boundries near tiles, then generate them. # Do it seperately for each z-level: for z, xy_dict in floor_neighbours.items(): # type: float, dict for x, y in xy_dict: # type: float, float # We want to count where there aren't any tiles xy_dict[x, y] = (((x - 128, y - 128) not in xy_dict) + ((x - 128, y + 128) not in xy_dict) + ((x + 128, y - 128) not in xy_dict) + ((x + 128, y + 128) not in xy_dict) + ((x - 128, y) not in xy_dict) + ((x + 128, y) not in xy_dict) + ((x, y - 128) not in xy_dict) + ((x, y + 128) not in xy_dict)) max_x = max_y = 0 weights = {} # Now the counts are all correct, compute the weight to apply # for tiles. # Adding the neighbouring counts will make a 5x5 area needed to set # the center to 0. for (x, y), cur_count in xy_dict.items(): max_x = max(x, max_x) max_y = max(y, max_y) # Orthrogonal is worth 0.2, diagonal is worth 0.1. # Not-present tiles would be 8 - the maximum tile_count = (0.8 * cur_count + 0.1 * xy_dict.get( (x - 128, y - 128), 8) + 0.1 * xy_dict.get( (x - 128, y + 128), 8) + 0.1 * xy_dict.get( (x + 128, y - 128), 8) + 0.1 * xy_dict.get( (x + 128, y + 128), 8) + 0.2 * xy_dict.get( (x - 128, y), 8) + 0.2 * xy_dict.get( (x, y - 128), 8) + 0.2 * xy_dict.get( (x, y + 128), 8) + 0.2 * xy_dict.get( (x + 128, y), 8)) # The number ranges from 0 (all tiles) to 12.8 (no tiles). # All tiles should still have a small chance to generate tiles. weights[x, y] = min((tile_count + 0.5) / 8, 1) # Share the detail entity among same-height tiles.. detail_ent = conditions.VMF.create_ent(classname='func_detail', ) for x, y in xy_dict: convert_floor( Vec(x, y, z), overlay_ids, MATS, SETTINGS, sign_loc, detail_ent, noise_weight=weights[x, y], noise_func=noise, ) add_floor_sides(floor_edges) conditions.reallocate_overlays(overlay_ids) return conditions.RES_EXHAUSTED
def brush_at_loc( inst: Entity, props: Property, ) -> Tuple[tiling.TileType, bool, Set[tiling.TileType]]: """Common code for posIsSolid and ReadSurfType. This returns the average tiletype, if both colors were found, and a set of all types found. """ origin = Vec.from_str(inst['origin']) angles = Angle.from_str(inst['angles']) # Allow using pos1 instead, to match pos2. pos = props.vec('pos1' if 'pos1' in props else 'pos') pos.z -= 64 # Subtract so origin is the floor-position pos.localise(origin, angles) norm: Vec = round(props.vec('dir', 0, 0, 1) @ angles, 6) if props.bool('gridpos') and norm is not None: for axis in 'xyz': # Don't realign things in the normal's axis - # those are already fine. if norm[axis] == 0: pos[axis] = pos[axis] // 128 * 128 + 64 result_var = props['setVar', ''] # RemoveBrush is the pre-tiling name. should_remove = props.bool('RemoveTile', props.bool('RemoveBrush', False)) tile_types: Set[tiling.TileType] = set() both_colors = False if 'pos2' in props: pos2 = props.vec('pos2') pos2.z -= 64 # Subtract so origin is the floor-position pos2.localise(origin, angles) bbox_min, bbox_max = Vec.bbox(round(pos, 6), round(pos2, 6)) white_count = black_count = 0 for pos in Vec.iter_grid(bbox_min, bbox_max, 32): try: tiledef, u, v = tiling.find_tile(pos, norm) except KeyError: continue tile_type = tiledef[u, v] tile_types.add(tile_type) if should_remove: tiledef[u, v] = tiling.TileType.VOID if tile_type.is_tile: if tile_type.color is tiling.Portalable.WHITE: white_count += 1 else: black_count += 1 both_colors = white_count > 0 and black_count > 0 if white_count == black_count == 0: tile_type = tiling.TileType.VOID tile_types.add(tiling.TileType.VOID) elif white_count > black_count: tile_type = tiling.TileType.WHITE else: tile_type = tiling.TileType.BLACK else: # Single tile. try: tiledef, u, v = tiling.find_tile(pos, norm) except KeyError: tile_type = tiling.TileType.VOID else: tile_type = tiledef[u, v] if should_remove: tiledef[u, v] = tiling.TileType.VOID tile_types.add(tile_type) if result_var: if tile_type.is_tile: # Don't distinguish between 4x4, goo sides inst.fixup[result_var] = tile_type.color.value elif tile_type is tiling.TileType.VOID: inst.fixup[result_var] = 'none' else: inst.fixup[result_var] = tile_type.name.casefold() return tile_type, both_colors, tile_types
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_faith_mods(inst: Entity, res: Property): """Modify the `trigger_catapult` that is created for `ItemFaithPlate` items. Values: - `raise_trig`: Raise or lower the `trigger_catapult`s by this amount. - `angled_targ`, `angled_in`: Instance entity and input for angled plates - `straight_targ`, `straight_in`: Instance entity and input for straight plates - `instvar`: A $replace value to set to either 'angled' or ' 'straight'. - `enabledVar`: A `$replace` value which will be copied to the main trigger's Start Disabled value (and inverted). - `trig_temp`: An ID for a template brush to add. This will be offset by the trigger's position (in the case of the `helper` trigger). """ # Get data about the trigger this instance uses for flinging fixup_var = res['instvar', ''] trig_enabled = res['enabledVar', None] trig_temp = res['trig_temp', ''] offset = srctools.conv_int(res['raise_trig', '0']) if offset: offset = Vec(0, 0, offset).rotate_by_str(inst['angles', '0 0 0']) else: offset = Vec() if trig_enabled is not None: trig_enabled = srctools.conv_bool(inst.fixup[trig_enabled]) else: trig_enabled = None for trig in vbsp.VMF.by_class['trigger_catapult']: if inst['targetname'] not in trig['targetname']: continue # Edit both the normal and the helper trigger.. trig_origin = trig['origin'] = Vec.from_str(trig['origin']) + offset if offset and not trig_temp: # No template, shift the current brushes. for solid in trig.solids: solid.translate(offset) elif trig_temp: trig.solids = template_brush.import_template( temp_name=trig_temp, origin=trig_origin, angles=Vec.from_str(inst['angles']), force_type=template_brush.TEMP_TYPES.world, ).world # Remove the trigger solids from worldspawn.. for solid in trig.solids: vbsp.VMF.remove_brush(solid) if trig_enabled is not None and 'helper' not in trig['targetname']: trig['startdisabled'] = srctools.bool_as_int(not trig_enabled) # Inspect the outputs to determine the type. # We also change them if desired, since that's not possible # otherwise. for out in trig.outputs: if out.inst_in == 'animate_angled_relay': # Instead of an instance: output, use local names. # This allows us to strip the proxy, as well as use # overlay instances. out.inst_in = None out.target = conditions.local_name( inst, res['angled_targ', 'animate_angled_relay']) out.input = res['angled_in', 'Trigger'] if fixup_var: inst.fixup[fixup_var] = 'angled' break # There's only one output we want to look for... elif out.inst_in == 'animate_straightup_relay': out.inst_in = None out.target = conditions.local_name( inst, res['straight_targ', 'animate_straightup_relay'], ) out.input = res['straight_in', 'Trigger'] if fixup_var: inst.fixup[fixup_var] = 'straight' break
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_orient = Matrix.from_angle(Angle.from_str(shape_inst['angles'])) up_axis: Vec = round(res.vec('up_axis') @ shape_orient, 6) for conn in shape_item.outputs: fizz_item = conn.to_item try: fizz = fizzler.FIZZLERS[fizz_item.name] except KeyError: continue # Detach this connection and remove traces of it. conn.remove() 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 = conditions.add_inst( vmf, targetname=shape_name, origin=shape_inst['origin'], angles=shape_inst['angles'], file=resolve_one('<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'], ant_floor_style=shape_item.ant_floor_style, ant_wall_style=shape_item.ant_wall_style, ) connections.ITEMS[shape_name] = fizz_item # Transfer the input/outputs from us to the fizzler. for inp in list(shape_item.inputs): inp.to_item = fizz_item for conn in list(shape_item.outputs): conn.from_item = fizz_item # If the fizzler has no outputs, then strip out antlines. Otherwise, # they need to be transferred across, so we can't tell safely. if fizz_item.output_act() is None and fizz_item.output_deact() is None: shape_item.delete_antlines() else: shape_item.transfer_antlines(fizz_item) 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) @ shape_orient + origin, Vec.from_str(vec2) @ shape_orient + origin, ) fizz.emitters.append(seg_min_max)
def modify_platform(inst: Entity) -> None: """Modify each platform.""" 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, fname) for fname in conf_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'] = fname = inst_filenames['fullstatic_' + str(position)] conditions.ALL_INST.add(fname) 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=conditions.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', f'moveto({st_pos})'), Output('OnUser2', '!self', 'RunScriptCode', f'moveto({end_pos})'), ) origin = Vec.from_str(inst['origin']) orient = Matrix.from_angle(Angle.from_str(inst['angles'])) off = orient.up(128) move_ang = off.to_angle() # Index -> func_movelinear. pistons: dict[int, Entity] = {} static_ent = vmf.create_ent('func_brush', origin=origin) for pist_ind in [1, 2, 3, 4]: 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'] = fname = 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'] = fname = 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. # The max pos was evaluated earlier, so this must be set. 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=conditions.local_name( pist_ent, f'pist{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'] = conditions.local_name( pist_ent, f'pist{pist_ind - 1}', ) if fname: conditions.ALL_INST.add(fname.casefold()) else: # No actual instance, remove. pist_ent.remove() temp_result = template_brush.import_template( vmf, template, brush_pos, orient, 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, generator=GenCat.PANEL, ) # Associate any set panel with the same entity, if it's present. tile_pos = origin - orient.up(128) panel: Optional[Panel] = None try: tiledef = TILES[tile_pos.as_tuple(), off.norm().as_tuple()] except KeyError: pass else: for panel in tiledef.panels: if panel.same_item(inst): break else: # Checked all of them. panel = None if panel is not None: if panel.brush_ent in vmf.entities and not panel.brush_ent.solids: panel.brush_ent.remove() panel.brush_ent = pistons[max(pistons.keys())] panel.offset = st_pos * off if not static_ent.solids and (panel is None or panel.brush_ent is not static_ent): 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'] = conditions.local_name(inst, source_ent)
def parse(cls, conf: Property): """Read in a fizzler from a config.""" fizz_id = conf['id'] item_ids = [prop.value.casefold() for prop in conf.find_all('item_id')] try: model_name_type = ModelName(conf['NameType', 'same'].casefold()) except ValueError: LOGGER.warning('Bad model name type: "{}"', conf['NameType']) model_name_type = ModelName.SAME model_local_name = conf['ModelName', ''] if not model_local_name: # We can't rename without a local name. model_name_type = ModelName.SAME inst = {} for inst_type, is_static in itertools.product(FizzInst, (False, True)): inst_type_name = inst_type.value + ('_static' if is_static else '') inst[inst_type, is_static] = instances = [ file for prop in conf.find_all(inst_type_name) for file in instanceLocs.resolve(prop.value) ] # Allow specifying weights to bias model locations weights = conf[inst_type_name + '_weight', ''] if weights: # Produce the weights, then process through the original # list to build a new one with repeated elements. inst[inst_type, is_static] = instances = [ instances[i] for i in conditions.weighted_random( len(instances), weights) ] # If static versions aren't given, reuse non-static ones. # We do False, True so it's already been calculated. if not instances and is_static: inst[inst_type, True] = inst[inst_type, False] if not inst[FizzInst.BASE, False]: LOGGER.warning('No base instance set! for "{}"!', fizz_id) voice_attrs = [] for prop in conf.find_all('Has'): if prop.has_children(): for child in prop: voice_attrs.append(child.name.casefold()) else: voice_attrs.append(prop.value.casefold()) pack_lists = {prop.value for prop in conf.find_all('Pack')} pack_lists_static = { prop.value for prop in conf.find_all('PackStatic') } brushes = [FizzlerBrush.parse(prop) for prop in conf.find_all('Brush')] beams = [] # type: List[FizzBeam] for beam_prop in conf.find_all('Beam'): offsets = [ Vec.from_str(off.value) for off in beam_prop.find_all('pos') ] keys = Property('', [ beam_prop.find_key('Keys', []), beam_prop.find_key('LocalKeys', []) ]) beams.append( FizzBeam( offsets, keys, beam_prop.int('RandSpeedMin', 0), beam_prop.int('RandSpeedMax', 0), )) try: temp_conf = conf.find_key('TemplateBrush') except NoKeyError: temp_brush_keys = temp_min = temp_max = temp_single = None else: temp_brush_keys = Property('--', [ temp_conf.find_key('Keys'), temp_conf.find_key('LocalKeys', []), ]) # Find and load the templates. temp_min = temp_conf['Left', None] temp_max = temp_conf['Right', None] temp_single = temp_conf['Single', None] return FizzlerType( fizz_id, item_ids, voice_attrs, pack_lists, pack_lists_static, model_local_name, model_name_type, brushes, beams, inst, temp_brush_keys, temp_min, temp_max, temp_single, )
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 parse_map(vmf: VMF, voice_attrs: Dict[str, bool]) -> None: """Analyse fizzler instances to assign fizzler types. Instance traits are required. The model instances and brushes will be removed from the map. Needs connections to be parsed. """ # Item ID and model skin -> fizzler type fizz_types = {} # type: Dict[Tuple[str, int], FizzlerType] for fizz_type in FIZZ_TYPES.values(): for item_id in fizz_type.item_ids: if ':' in item_id: item_id, barrier_type = item_id.split(':') if barrier_type == 'laserfield': barrier_skin = 2 elif barrier_type == 'fizzler': barrier_skin = 0 else: LOGGER.error('Invalid barrier type ({}) for "{}"!', barrier_type, item_id) fizz_types[item_id, 0] = fizz_type fizz_types[item_id, 2] = fizz_type continue fizz_types[item_id, barrier_skin] = fizz_type else: fizz_types[item_id, 0] = fizz_type fizz_types[item_id, 2] = fizz_type fizz_bases = {} # type: Dict[str, Entity] fizz_models = defaultdict(list) # type: Dict[str, List[Entity]] # Position and normal -> name, for output relays. fizz_pos = { } # type: Dict[Tuple[Tuple[float, float, float], Tuple[float, float, float]], str] # First use traits to gather up all the instances. for inst in vmf.by_class['func_instance']: traits = instance_traits.get(inst) if 'fizzler' not in traits: continue name = inst['targetname'] if 'fizzler_model' in traits: name = name.rsplit('_model', 1)[0] fizz_models[name].append(inst) inst.remove() elif 'fizzler_base' in traits: fizz_bases[name] = inst else: LOGGER.warning('Fizzler "{}" has non-base, non-model instance?', name) continue origin = Vec.from_str(inst['origin']) normal = Vec(z=1).rotate_by_str(inst['angles']) fizz_pos[origin.as_tuple(), normal.as_tuple()] = name for name, base_inst in fizz_bases.items(): models = fizz_models[name] up_axis = Vec(y=1).rotate_by_str(base_inst['angles']) # If upside-down, make it face upright. if up_axis == (0, 0, -1): up_axis = Vec(z=1) base_inst.outputs.clear() # Now match the pairs of models to each other. # The length axis is the line between them. # We don't care about the instances after this, so don't keep track. length_axis = Vec(z=1).rotate_by_str(base_inst['angles']).axis() emitters = [] # type: List[Tuple[Vec, Vec]] model_pairs = {} # type: Dict[Tuple[float, float], Vec] model_skin = models[0].fixup.int('$skin') try: item_id, item_subtype = instanceLocs.ITEM_FOR_FILE[ base_inst['file'].casefold()] fizz_type = fizz_types[item_id, model_skin] except KeyError: LOGGER.warning('Fizzler types: {}', fizz_types.keys()) raise ValueError('No fizzler type for "{}"!'.format( base_inst['file'], )) from None for attr_name in fizz_type.voice_attrs: voice_attrs[attr_name] = True for model in models: pos = Vec.from_str(model['origin']) try: other_pos = model_pairs.pop(pos.other_axes(length_axis)) except KeyError: # No other position yet, we need to find that. model_pairs[pos.other_axes(length_axis)] = pos continue min_pos, max_pos = Vec.bbox(pos, other_pos) # Move positions to the wall surface. min_pos[length_axis] -= 64 max_pos[length_axis] += 64 emitters.append((min_pos, max_pos)) FIZZLERS[name] = Fizzler(fizz_type, up_axis, base_inst, emitters) # Delete all the old brushes associated with fizzlers for brush in (vmf.by_class['trigger_portal_cleanser'] | vmf.by_class['trigger_hurt'] | vmf.by_class['func_brush']): name = brush['targetname'] if not name: continue name = name.rsplit('_brush')[0] if name in FIZZLERS: brush.remove() # Check for fizzler output relays. relay_file = instanceLocs.resolve('<ITEM_BEE2_FIZZLER_OUT_RELAY>', silent=True) if not relay_file: # No relay item - deactivated most likely. return for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in relay_file: continue inst.remove() relay_item = connections.ITEMS[inst['targetname']] try: fizz_name = fizz_pos[ Vec.from_str(inst['origin']).as_tuple(), Vec(0, 0, 1).rotate_by_str(inst['angles']).as_tuple()] fizz_item = connections.ITEMS[fizz_name] except KeyError: # Not placed on a fizzler, or a fizzler with no IO # - ignore, and destroy. for out in list(relay_item.outputs): out.remove() for out in list(relay_item.inputs): out.remove() del connections.ITEMS[relay_item.name] continue # Copy over fixup values fizz_item.inst.fixup.update(inst.fixup) # Copy over the timer delay set in the relay. fizz_item.timer = relay_item.timer # Transfer over antlines. fizz_item.antlines |= relay_item.antlines fizz_item.shape_signs += relay_item.shape_signs fizz_item.ind_panels |= relay_item.ind_panels # Remove the relay item so it doesn't get added to the map. del connections.ITEMS[relay_item.name] for conn in list(relay_item.outputs): conn.from_item = fizz_item
def mode_pos(comp_ent: Entity, ent: Entity) -> str: """Return the position of the entity.""" pos = Vec.from_str(ent['origin']) scale = conv_float(comp_ent['const'], 1.0) return vs_vec(scale * pos)
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 mode_off(comp_ent: Entity, ent: Entity) -> str: """Return the offset from the ent to the reference.""" scale = conv_float(comp_ent['const'], 1.0) offset = Vec.from_str(ent['origin']) - Vec.from_str(comp_ent['origin']) return vs_vec(offset * scale)
def res_unst_scaffold(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 ) TARG_INST, LINKS = SCAFFOLD_CONFIGS[res.value] del SCAFFOLD_CONFIGS[res.value] # Don't let this run twice instances = {} # Find all the instances we're wanting to change, and map them to # targetnames for ent in vbsp.VMF.by_class['func_instance']: file = ent['file'].casefold() targ = ent['targetname'] if file not in TARG_INST: continue config = TARG_INST[file] next_inst = set( out.target for out in ent.outputs ) # Destroy these outputs, they're useless now! ent.outputs.clear() instances[targ] = { 'ent': ent, 'conf': config, 'next': next_inst, 'prev': None, } # Now link each instance to its in and outputs for targ, inst in instances.items(): scaff_targs = 0 for ent_targ in inst['next']: if ent_targ in instances: instances[ent_targ]['prev'] = targ inst['next'] = ent_targ scaff_targs += 1 else: # If it's not a scaffold, it's probably an indicator_toggle. # We want to remove any them as well as the assoicated # antlines! for toggle in vbsp.VMF.by_target[ent_targ]: conditions.remove_ant_toggle(toggle) if scaff_targs > 1: raise Exception('A scaffold item has multiple destinations!') elif scaff_targs == 0: inst['next'] = None # End instance starting_inst = [] # We need to find the start instances, so we can set everything up for inst in instances.values(): if inst['prev'] is None and inst['next'] is None: # Static item! continue elif inst['prev'] is None: starting_inst.append(inst) # We need to make the link entities unique for each scaffold set, # otherwise the AllVar property won't work. group_counter = 0 # Set all the instances and properties for start_inst in starting_inst: group_counter += 1 ent = start_inst['ent'] for vals in LINKS.values(): if vals['all'] is not None: ent.fixup[vals['all']] = SCAFF_PATTERN.format( name=vals['name'], group=group_counter, index='*', ) should_reverse = srctools.conv_bool(ent.fixup['$start_reversed']) # Now set each instance in the chain, including first and last for index, inst in enumerate(scaff_scan(instances, start_inst)): ent, conf = inst['ent'], inst['conf'] orient = ( 'floor' if Vec(0, 0, 1).rotate_by_str(ent['angles']) == (0, 0, 1) else 'wall' ) # Find the offset used for the logic ents offset = (conf['off_' + orient]).copy() if conf['is_piston']: # Adjust based on the piston position offset.z += 128 * srctools.conv_int(ent.fixup[ '$top_level' if ent.fixup['$start_up'] == '1' else '$bottom_level' ]) offset.rotate_by_str(ent['angles']) offset += Vec.from_str(ent['origin']) if inst['prev'] is None: link_type = 'start' elif inst['next'] is None: link_type = 'end' else: link_type = 'mid' if ( orient == 'floor' and link_type != 'mid' and conf['inst_end'] is not None ): # Add an extra instance pointing in the direction # of the connected track. This would be the endcap # model. other_ent = instances[inst[ 'next' if link_type == 'start' else 'prev' ]]['ent'] other_pos = Vec.from_str(other_ent['origin']) our_pos = Vec.from_str(ent['origin']) link_dir = other_pos - our_pos 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=ent['targetname'], file=conf['inst_end'], origin=offset.join(' '), angles='0 {:.0f} 0'.format(link_ang), ) # Don't place the offset instance, this replaces that! elif 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=ent['targetname'], file=conf['inst_offset'], origin=offset.join(' '), angles=ent['angles'], ) logic_inst = vbsp.VMF.create_ent( classname='func_instance', targetname=ent['targetname'], file=conf.get( 'logic_' + link_type + ( '_rev' if should_reverse else '' ), '', ), origin=offset.join(' '), angles=( '0 0 0' if conf['rotate_logic'] else ent['angles'] ), ) for key, val in ent.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 inst['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 != '': ent['file'] = new_file LOGGER.info('Finished Scaffold generation!') return RES_EXHAUSTED
def mode_func(comp_ent: Entity, ent: Entity) -> str: """Rotate the axis by the given value.""" pos = Vec.from_str(ent['origin']) scale = conv_float(comp_ent['const'], 1.0) return str(pos[axis] * scale)
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 = 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)) 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_set_texture(inst: Entity, res: Property): """Set the brush face at a location to a particular texture. pos is the position, relative to the instance (0 0 0 is the floor-surface). dir is the normal of the texture. If gridPos is true, the position will be snapped so it aligns with the 128 brushes (Useful with fizzler/light strip items). tex is the texture used. If tex begins and ends with `<>`, certain textures will be used based on style: - `<delete>` will remove the brush entirely (it should be hollow). Caution should be used to ensure no leaks occur. - `<special>` the brush will be given a special texture like angled and flip panels. - `<white>` and `<black>` will use the regular textures for the given color. - `<white-2x2>`, `<white-4x4>`, `<black-2x2>`, `<black-4x4>` will use the given wall-sizes. If on floors or ceilings these always use 4x4. - `<2x2>` or `<4x4>` will force to the given wall-size, keeping color. - `<special-white>` and `<special-black>` will use a special texture of the given color. If tex begins and ends with `[]`, it is an option in the `Textures` list. These are composed of a group and texture, separated by `.`. `white.wall` are the white wall textures; `special.goo` is the goo texture. If `template` is set, the template should be an axis aligned cube. This will be rotated by the instance angles, and then the face with the same orientation will be applied to the face (with the rotation and texture). """ import vbsp pos = Vec.from_str(res['pos', '0 0 0']) pos.z -= 64 # Subtract so origin is the floor-position pos = pos.rotate_by_str(inst['angles', '0 0 0']) # Relative to the instance origin pos += Vec.from_str(inst['origin', '0 0 0']) norm = Vec.from_str(res['dir', '0 0 -1']).rotate_by_str( inst['angles', '0 0 0'] ) if srctools.conv_bool(res['gridpos', '0']): for axis in 'xyz': # Don't realign things in the normal's axis - # those are already fine. if not norm[axis]: pos[axis] //= 128 pos[axis] *= 128 pos[axis] += 64 brush = SOLIDS.get(pos.as_tuple(), None) if not brush or brush.normal != norm: return face_to_mod = brush.face # type: Side # Don't allow this to get overwritten later. vbsp.IGNORED_FACES.add(face_to_mod) temp = res['template', None] if temp: # Grab the scaling template and apply it to the brush. template_brush.get_scaling_template(temp).rotate( Vec.from_str(inst['angles']), Vec.from_str(inst['origin']), ).apply(face_to_mod) return tex = res['tex'] if tex.startswith('[') and tex.endswith(']'): face_to_mod.mat = vbsp.get_tex(tex[1:-1]) elif tex.startswith('<') and tex.endswith('>'): # Special texture names! tex = tex[1:-1].casefold() if tex == 'delete': vbsp.VMF.remove_brush(brush) return if tex == 'white': face_to_mod.mat = 'tile/white_wall_tile003a' elif tex == 'black': face_to_mod.mat = 'metal/black_wall_metal_002c' if tex == 'black' or tex == 'white': # For these two, run the regular logic to apply textures # correctly. vbsp.alter_mat( face_to_mod, vbsp.face_seed(face_to_mod), vbsp_options.get(bool, 'tile_texture_lock'), ) if tex == 'special': vbsp.set_special_mat(face_to_mod, str(brush.color)) elif tex == 'special-white': vbsp.set_special_mat(face_to_mod, 'white') return elif tex == 'special-black': vbsp.set_special_mat(brush.face, 'black') # Do <4x4>, <white-2x4>, etc color = str(brush.color) if tex.startswith('black') or tex.endswith('white'): # Override the color used for 2x2/4x4 brushes color = tex[:5] if tex.endswith('2x2') or tex.endswith('4x4'): # 4x4 and 2x2 instructions are ignored on floors and ceilings. orient = vbsp.get_face_orient(face_to_mod) if orient == vbsp.ORIENT.wall: face_to_mod.mat = vbsp.get_tex( color + '.' + tex[-3:] ) else: face_to_mod.mat = vbsp.get_tex( color + '.' + str(orient) ) else: face_to_mod.mat = tex
def res_insert_overlay(inst: Entity, res: Property) -> None: """Use a template to insert one or more overlays on a surface. Options: - ID: The template ID. Brushes will be ignored. - Replace: old -> new material replacements. - Face_pos: The offset of the brush face. - Normal: The direction of the brush face. - Offset: An offset to move the overlays by. """ ( temp_id, replace, face, norm, offset, ) = res.value if temp_id[:1] == '$': temp_id = inst.fixup[temp_id] origin = Vec.from_str(inst['origin']) # type: Vec angles = Vec.from_str(inst['angles', '0 0 0']) face_pos = Vec(face).rotate(*angles) face_pos += origin normal = Vec(norm).rotate(*angles) # Don't make offset change the face_pos value.. origin += offset.copy().rotate_by_str(inst['angles', '0 0 0']) # 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( temp_id, origin, angles, targetname=inst['targetname', ''], force_type=TEMP_TYPES.detail, ) for over in temp.overlay: # type: Entity random.seed('TEMP_OVERLAY_' + over['basisorigin']) mat = over['material'] try: mat = random.choice(replace[over['material'].casefold().replace( '\\', '/')]) except KeyError: pass 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(Vec.from_str(over['basisorigin']), 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_import_template_setup(res: Property): temp_id = res['id'] force = res['force', ''].casefold().split() if 'white' in force: force_colour = template_brush.MAT_TYPES.white elif 'black' in force: force_colour = template_brush.MAT_TYPES.black elif 'invert' in force: force_colour = 'INVERT' else: force_colour = None if 'world' in force: force_type = template_brush.TEMP_TYPES.world elif 'detail' in force: force_type = template_brush.TEMP_TYPES.detail else: force_type = template_brush.TEMP_TYPES.default for size in ('2x2', '4x4', 'wall', 'special'): if size in force: force_grid = size break else: force_grid = None invert_var = res['invertVar', ''] color_var = res['colorVar', ''] replace_tex = defaultdict(list) for prop in res.find_key('replace', []): replace_tex[prop.name].append(prop.value) rem_replace_brush = True additional_ids = set() transfer_overlays = '1' try: replace_brush = res.find_key('replaceBrush') except NoKeyError: replace_brush_pos = None else: if replace_brush.has_children(): replace_brush_pos = replace_brush['Pos', '0 0 0'] additional_ids = set(map( srctools.conv_int, replace_brush['additionalIDs', ''].split(), )) rem_replace_brush = replace_brush.bool('removeBrush', True) transfer_overlays = replace_brush['transferOverlay', '1'] else: replace_brush_pos = replace_brush.value # type: str replace_brush_pos = Vec.from_str(replace_brush_pos) replace_brush_pos.z -= 64 # 0 0 0 defaults to the floor. key_values = res.find_key("Keys", []) if key_values: keys = Property("", [ key_values, res.find_key("LocalKeys", []), ]) # Ensure we have a 'origin' keyvalue - we automatically offset that. if 'origin' not in key_values: key_values['origin'] = '0 0 0' # Spawn everything as detail, so they get put into a brush # entity. force_type = template_brush.TEMP_TYPES.detail outputs = [ Output.parse(prop) for prop in res.find_children('Outputs') ] else: keys = None outputs = [] visgroup_mode = res['visgroup', 'none'].casefold() if visgroup_mode not in ('none', 'choose'): visgroup_mode = srctools.conv_float(visgroup_mode.rstrip('%'), 0.00) if visgroup_mode == 0: visgroup_mode = 'none' # Generate the function which picks which visgroups to add to the map. if visgroup_mode == 'none': def visgroup_func(_): """none = don't add any visgroups.""" return () elif visgroup_mode == 'choose': def visgroup_func(groups): """choose = add one random group.""" return [random.choice(groups)] else: def visgroup_func(groups): """Number = percent chance for each to be added""" for group in groups: val = random.uniform(0, 100) if val <= visgroup_mode: yield group # If true, force visgroups to all be used. visgroup_force_var = res['forceVisVar', ''] return ( temp_id, dict(replace_tex), force_colour, force_grid, force_type, replace_brush_pos, rem_replace_brush, transfer_overlays, additional_ids, invert_var, color_var, visgroup_func, visgroup_force_var, keys, outputs, )
def add_voice( has_items: dict, style_vars_: dict, vmf_file_: VMF, map_seed: str, use_priority=True, ): """Add a voice line to the map.""" global ALLOW_MID_VOICES, vmf_file, map_attr, style_vars LOGGER.info('Adding Voice Lines!') vmf_file = vmf_file_ map_attr = has_items style_vars = style_vars_ norm_config = ConfigFile('voice.cfg', root='bee2') mid_config = ConfigFile('mid_voice.cfg', root='bee2') quote_base = QUOTE_DATA['base', False] quote_loc = Vec.from_str(QUOTE_DATA['quote_loc', '-10000 0 0'], x=-10000) if quote_base: LOGGER.info('Adding Base instance!') vmf_file.create_ent( classname='func_instance', targetname='voice', file=INST_PREFIX + quote_base, angles='0 0 0', origin=quote_loc, fixup_style='0', ) # Either box in with nodraw, or place the voiceline studio. has_studio = conditions.monitor.make_voice_studio(vmf_file, quote_loc) 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(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_file.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_file.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..') with open(RESP_LOC, 'w') as f: generate_resp_script(f, allow_dings) else: LOGGER.info('No responses data..') try: os.remove(RESP_LOC) except FileNotFoundError: pass for ind, file in enumerate(QUOTE_EVENTS.values()): if not file: continue vmf_file.create_ent( classname='func_instance', targetname='voice_event_' + str(ind), file=file, angles='0 0 0', origin=quote_loc, fixup_style='0', ) # Determine the flags that enable/disable specific lines based on which # players are used. player_model = vbsp.BEE2_config.get_val( 'General', 'player_model', 'PETI', ).casefold() is_coop = (vbsp.GAME_MODE == 'COOP') is_sp = (vbsp.GAME_MODE == 'SP') player_flags = { 'sp': is_sp, 'coop': is_coop, 'atlas': is_coop or player_model == 'atlas', 'pbody': is_coop or player_model == 'pbody', 'bendy': is_sp and player_model == 'peti', 'chell': is_sp and player_model == 'sp', 'human': is_sp and player_model in ('peti', 'sp'), 'robot': is_coop or player_model in ('atlas', 'pbody'), } # All which are True. player_flag_set = {val for val, flag in player_flags.items() if flag} # For each group, locate the voice lines. for group in itertools.chain( QUOTE_DATA.find_all('group'), QUOTE_DATA.find_all('midchamber'), ): # type: Property quote_targetname = group['Choreo_Name', '@choreo'] use_dings = group.bool('use_dings', allow_dings) possible_quotes = sorted( find_group_quotes( group, mid_quotes, use_dings, 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, ) 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.. random.seed('{}-VOICE_QUOTE_{}'.format( map_seed, len(possible_quotes), )) chosen = random.choice(possible_quotes).lines # Join the IDs for # the voice lines to the map seed, # so each quote block will chose different lines. random.seed(map_seed + '-VOICE_LINE_' + '|'.join(prop['id', 'ID'] for prop in chosen)) # Add one of the associated quotes add_quote( random.choice(chosen), quote_targetname, choreo_loc, 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_file.create_ent( classname='env_microphone', targetname='player_speaker_sp', speakername='!player', maxRange='386', origin=quote_loc, ) else: vmf_file.create_ent( classname='env_microphone', targetname='player_speaker_blue', speakername='!player_blue', maxRange='386', origin=quote_loc, ) vmf_file.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 = random.choice(mid_lines) mid_item, use_ding, mid_name = line add_quote(mid_item, mid_name, quote_loc, use_ding) LOGGER.info('Done!')
def res_camera(inst: Entity, res: Property): """Result for the camera item. Options: - cam_off: The position that the camera yaws around. - yaw_off: The offset from cam_off that the camera rotates up/down. - pitch_off: The offset from yaw_off that is where the sensor is. - yaw_inst: The instance to place for the yaw rotation. - pitch_inst: The instance to place for the up/down rotation. - yaw_range: How many degrees can the camera rotate from a forward position? - pitch_range: How many degrees can the camera rotate up/down? """ conf = res.value normal = Vec(0, 0, 1).rotate_by_str(inst['angles']) if normal.z != 0: # Can't be on floor/ceiling! inst.remove() return base_yaw = math.degrees(math.atan2(normal.y, normal.x)) % 360 inst['angles'] = '0 {:g} 0'.format(base_yaw) base_loc = Vec.from_str(inst['origin']) try: plate = faithplate.PLATES.pop(inst['targetname']) except KeyError: LOGGER.warning( 'No faith plate info found for camera {}!', inst['targetname'], ) inst.remove() return # Remove the triggers. plate.trig.remove() if isinstance(plate, faithplate.StraightPlate): # Just point straight ahead. target_loc = base_loc + 512 * normal # And remove the helper. plate.helper_trig.remove() else: if isinstance(plate.target, Vec): target_loc = plate.target else: # We don't particularly care about aiming to the front of angled # panels. target_loc = plate.target.pos + 64 * plate.target.normal # Remove the helper and a bullseye. plate.target.remove_portal_helper() plate.target.bullseye_count -= 1 # Move three times to position the camera arms and lens. yaw_pos = Vec(conf['yaw_off']).rotate_by_str(inst['angles']) yaw_pos += base_loc pitch, yaw, _ = (target_loc - yaw_pos).to_angle() inst.map.create_ent( classname='func_instance', targetname=inst['targetname'], file=conf['yaw_inst'], angles='0 {:g} 0'.format(yaw), origin=yaw_pos, ) pitch_pos = Vec(conf['pitch_off']) pitch_pos.rotate(yaw=yaw) pitch_pos.rotate_by_str(inst['angles']) pitch_pos += yaw_pos inst.map.create_ent( classname='func_instance', targetname=inst['targetname'], file=conf['pitch_inst'], angles='{:g} {:g} 0'.format(pitch, yaw), origin=pitch_pos, ) cam_pos = Vec(conf['cam_off']) cam_pos.rotate(pitch=pitch, yaw=yaw) cam_pos += pitch_pos # Recompute, since this can be slightly different if the camera is large. cam_angles = (target_loc - cam_pos).to_angle() ALL_CAMERAS.append(Camera(inst, cam_pos, cam_angles))
def comp_trigger_goo(ctx: Context): """Creates triggers for Toxic Goo.""" reloader_cache = { } # type: Dict[Tuple[float, float, float, float], Entity] for trig in ctx.vmf.by_class['comp_trigger_p2_goo']: trig.remove() outputs = trig.outputs.copy() trig.outputs.clear() failsafe_delay = conv_float(trig['failsafe_delay'], 0.5) if failsafe_delay < 0.01: failsafe_delay = 0.01 hurt = trig.copy() diss = trig.copy() ctx.vmf.add_ents([hurt, diss]) spawnflags = conv_int(trig['spawnflags']) for keyvalue in [ 'dissolve_filter', 'phys_offset', 'failsafe_delay', 'fadepreset', 'fadecolor', 'fadetime', ]: del diss[keyvalue], hurt[keyvalue] diss['classname'] = 'trigger_multiple' # No clients, add physics. But otherwise leave it to the user. diss['spawnflags'] = (spawnflags & ~1) | 8 diss['wait'] = 0 # No delay. diss['filtername'] = trig['dissolve_filter'] del diss['damagetype'] diss_pos = Vec.from_str(diss['origin']) diss_pos.z -= conv_float(trig['phys_offset']) diss['origin'] = diss_pos hurt['spawnflags'] = 1 # Players. if conv_bool(trig['enablefade']): fade_time = conv_float(trig['fadetime']) fade_color = Vec.from_str(trig['fadepreset']) if fade_color == (-1, -1, -1): fade_color = Vec.from_str(trig['fadecolor']) fade_key = fade_color.x, fade_color.y, fade_color.z, fade_time try: reloader = reloader_cache[fade_key] except KeyError: reloader = reloader_cache[fade_key] = ctx.vmf.create_ent( 'player_loadsaved', origin=diss['origin'], rendercolor=str(fade_color), renderamt=255, duration=fade_time, holdtime=10, loadtime=fade_time + 0.1, ) reloader.make_unique('reloader') hurt['classname'] = 'trigger_once' del hurt['damagetype'] hurt.add_out( Output('OnStartTouch', reloader, 'Reload', only_once=True)) # Make sure the failsafe delay is longer than the total fade time. failsafe_delay = min(failsafe_delay, fade_time + 0.15) else: hurt['classname'] = 'trigger_hurt' hurt['damage'] = hurt['damagecap'] = 10000 hurt['damagemodel'] = 0 # No doubling hurt['nodmgforce'] = 1 # Don't throw players around. for out in outputs: if out.output.casefold() == 'onkillplayer': # Better than OnStartTouch, doesn't apply for god mode. out.output = 'OnHurtPlayer' hurt.add_out(out) elif out.output.casefold() == 'ondissolvephysics': out.output = 'OnStartTouch' diss.add_out(out) diss.add_out( Output('OnStartTouch', '!activator', 'SilentDissolve'), Output('OnStartTouch', '!activator', 'Kill', delay=failsafe_delay), )