def make_corner( vmf: VMF, origin: Vec, start_dir: Vec, end_dir: Vec, size: int, config: Config, ) -> None: """Place a corner.""" angles = Matrix.from_basis(z=start_dir, x=end_dir) conditions.add_inst( vmf, origin=origin, angles=angles, file=config.inst_corner[int(size)], ) temp, visgroups = config.temp_corner[int(size)] if temp is not None: temp_solids = template_brush.import_template( vmf, temp, additional_visgroups=visgroups, origin=origin, angles=angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ).world motion_trigger(vmf, *temp_solids)
def make_voice_studio(vmf: VMF) -> bool: """Create the voice-line studio. This is either an instance (if monitors are present), or a nodraw room. """ studio_file = options.get(str, 'voice_studio_inst') loc = voice_line.get_studio_loc() if HAS_MONITOR and studio_file: conditions.add_inst( vmf, file=studio_file, origin=loc, ) return True else: # If there aren't monitors, the studio instance isn't used. # We need to seal anyway. vmf.add_brushes(vmf.make_hollow( loc - 256, loc + 256, thick=32, )) return False
def make_frame(frame_type: str, loc: Vec, angles: Angle) -> None: """Make a frame instance.""" conditions.add_inst( vmf, targetname=targ, file=conf['frame_' + frame_type], # Position at the center of the block, instead of at the glass. origin=loc - norm * 64, angles=angles, )
def add_group(inst: Entity) -> None: """Place the group.""" rng = rand.seed(b'shufflegroup', conf_seed, inst) pools = all_pools.copy() for (flags, value, potential_pools) in conf_selectors: for flag in flags: if not conditions.check_flag(flag, coll, inst): break else: # Succeeded. allowed_inst = [ (name, inst) for (name, inst) in pools if name in potential_pools ] name, filename = rng.choice(allowed_inst) pools.remove((name, filename)) conditions.add_inst( vmf, targetname=inst['targetname'], file=filename, angles=inst['angles'], origin=inst['origin'], ).fixup[conf_variable] = value
def res_camera(vmf: VMF, 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() conditions.add_inst( vmf, targetname=inst['targetname'], file=conf['yaw_inst'], angles=Angle(yaw=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 conditions.add_inst( vmf, targetname=inst['targetname'], file=conf['pitch_inst'], angles=Angle(pitch, yaw, 0.0), 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 make_tag_fizz(inst: Entity) -> None: """Create the Tag fizzler.""" fizzler: Optional[Fizzler] = None fizzler_item: Optional[Item] = None # Look for the fizzler instance we want to replace. sign_item = connections.ITEMS[inst['targetname']] for conn in list(sign_item.outputs): if conn.to_item.name in FIZZLERS: if fizzler is None: fizzler = FIZZLERS[conn.to_item.name] fizzler_item = conn.to_item else: raise ValueError('Multiple fizzlers attached to a sign!') conn.remove() # Regardless, remove the useless output. sign_item.delete_antlines() if fizzler is None: # No fizzler - remove this sign inst.remove() return if fizzler.fizz_type.id == TAG_FIZZ_ID: LOGGER.warning('Two tag signs attached to one fizzler...') inst.remove() return # Swap to the special Tag Fizzler type. fizzler.fizz_type = FIZZ_TYPES[TAG_FIZZ_ID] # And also swap the connection's type. if fizz_conn_conf is not None: fizzler_item.config = fizz_conn_conf fizzler_item.enable_cmd = fizz_conn_conf.enable_cmd fizzler_item.disable_cmd = fizz_conn_conf.disable_cmd fizzler_item.sec_enable_cmd = fizz_conn_conf.sec_enable_cmd fizzler_item.sec_disable_cmd = fizz_conn_conf.sec_disable_cmd inst_orient = Matrix.from_angle(Angle.from_str(inst['angles'])) # The actual location of the sign - on the wall sign_loc = Vec.from_str(inst['origin']) + Vec(0, 0, -64) @ inst_orient fizz_norm_axis = round(fizzler.normal(), 3).axis() # Now deal with the visual aspect: # Blue signs should be on top. blue_enabled = inst.fixup.bool('$start_enabled') oran_enabled = inst.fixup.bool('$start_reversed') # If True, single-color signs will also turn off the other color. # This also means we always show both signs. # If both are enabled or disabled, this has no effect. disable_other = (not inst.fixup.bool('$disable_autorespawn', True) and blue_enabled != oran_enabled) # Delete fixups now, they aren't useful. inst.fixup.clear() if not blue_enabled and not oran_enabled: # Hide the sign in this case! inst.remove() inst_normal = inst_orient.up() loc = Vec.from_str(inst['origin']) if disable_other or (blue_enabled and oran_enabled): inst['file'] = inst_frame_double conditions.ALL_INST.add(inst_frame_double.casefold()) # On a wall, and pointing vertically if abs(inst_normal.z) < 0.01 and abs(inst_orient.left().z) > 0.01: # They're vertical, make sure blue's on top! blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset) oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset) # If orange is enabled, with two frames put that on top # instead since it's more important if disable_other and oran_enabled: blue_loc, oran_loc = oran_loc, blue_loc else: offset = Vec(0, sign_offset, 0) @ inst_orient blue_loc = loc + offset oran_loc = loc - offset else: inst['file'] = inst_frame_single conditions.ALL_INST.add(inst_frame_single.casefold()) # They're always centered blue_loc = loc oran_loc = loc if inst_normal.z != 0: # If on floors/ceilings, rotate to point at the fizzler! sign_floor_loc = sign_loc.copy() sign_floor_loc.z = 0 # We don't care about z-positions. s, l = Vec.bbox(itertools.chain.from_iterable(fizzler.emitters)) if fizz_norm_axis == 'z': # For z-axis, just compare to the center point of the emitters. sign_dir = ((s.x + l.x) / 2, (s.y + l.y) / 2, 0) - sign_floor_loc else: # For the other two, we compare to the line, # or compare to the closest side (in line with the fizz) if fizz_norm_axis == 'x': # Extends in Y direction other_axis = 'y' side_min = s.y side_max = l.y normal = s.x else: # Extends in X direction other_axis = 'x' side_min = s.x side_max = l.x normal = s.y # Right in line with the fizzler. Point at the closest emitter. if abs(sign_floor_loc[other_axis] - normal) < 32: # Compare to the closest side. sign_dir = min([ sign_floor_loc - Vec.with_axes( fizz_norm_axis, side_min, other_axis, normal, ), sign_floor_loc - Vec.with_axes( fizz_norm_axis, side_max, other_axis, normal, ) ], key=Vec.mag) else: # Align just based on whether we're in front or behind. sign_dir = Vec.with_axes( fizz_norm_axis, normal - sign_floor_loc[fizz_norm_axis]).norm() sign_yaw = math.degrees(math.atan2(sign_dir.y, sign_dir.x)) # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals sign_yaw = (sign_yaw + 45) // 90 * 90 # Rotate to fit the instances - south is down sign_yaw = int(sign_yaw - 90) % 360 if inst_normal.z > 0: sign_angle = '0 {} 0'.format(sign_yaw) elif inst_normal.z < 0: # Flip upside-down for ceilings sign_angle = '0 {} 180'.format(sign_yaw) else: raise AssertionError('Cannot be zero here!') else: # On a wall, face upright sign_angle = conditions.PETI_INST_ANGLE[inst_normal.as_tuple()] # If disable_other, we show off signs. Otherwise we don't use that sign. blue_sign = blue_sign_on if blue_enabled else blue_sign_off if disable_other else None oran_sign = oran_sign_on if oran_enabled else oran_sign_off if disable_other else None if blue_sign: conditions.add_inst( vmf, file=blue_sign, targetname=inst['targetname'], angles=sign_angle, origin=blue_loc, ) if oran_sign: conditions.add_inst( vmf, file=oran_sign, targetname=inst['targetname'], angles=sign_angle, origin=oran_loc, ) # Now modify the fizzler... # Subtract the sign from the list of connections, but don't go below # zero fizzler.base_inst.fixup['$connectioncount'] = max( 0, fizzler.base_inst.fixup.int('$connectioncount') - 1) # Find the direction the fizzler normal is. # Signs will associate with the given side! bbox_min, bbox_max = fizzler.emitters[0] sign_center = (bbox_min[fizz_norm_axis] + bbox_max[fizz_norm_axis]) / 2 # Figure out what the sides will set values to... pos_blue = pos_oran = False neg_blue = neg_oran = False if sign_loc[fizz_norm_axis] < sign_center: pos_blue = blue_enabled pos_oran = oran_enabled else: neg_blue = blue_enabled neg_oran = oran_enabled # If it activates the paint gun, use different textures fizzler.tag_on_pos = pos_blue or pos_oran fizzler.tag_on_neg = neg_blue or neg_oran # Now make the trigger ents. We special-case these since they need to # swap # depending on the sign config and position. if vbsp.GAME_MODE == 'COOP': # We need ATLAS-specific triggers. pos_trig = vmf.create_ent(classname='trigger_playerteam') neg_trig = vmf.create_ent(classname='trigger_playerteam') output = 'OnStartTouchBluePlayer' else: pos_trig = vmf.create_ent(classname='trigger_multiple') neg_trig = vmf.create_ent(classname='trigger_multiple') output = 'OnStartTouch' pos_trig['origin'] = neg_trig['origin'] = fizzler.base_inst['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = conditions.local_name(fizzler.base_inst, 'trig_pos') neg_trig['targetname'] = conditions.local_name(fizzler.base_inst, 'trig_neg') pos_trig['startdisabled'] = neg_trig['startdisabled'] = ( not fizzler.base_inst.fixup.bool('start_enabled')) pos_trig.outputs = [ Output(output, neg_trig, 'Enable'), Output(output, pos_trig, 'Disable'), ] neg_trig.outputs = [ Output(output, pos_trig, 'Enable'), Output(output, neg_trig, 'Disable'), ] voice_attr = vbsp.settings['has_attr'] if blue_enabled or disable_other: # If this is blue/oran only, don't affect the other color neg_trig.outputs.append( Output( output, '@BlueIsEnabled', 'SetValue', neg_blue, )) pos_trig.outputs.append( Output( output, '@BlueIsEnabled', 'SetValue', pos_blue, )) if blue_enabled: # Add voice attributes - we have the gun and gel! voice_attr['bluegelgun'] = True voice_attr['bluegel'] = True voice_attr['bouncegun'] = True voice_attr['bouncegel'] = True if oran_enabled or disable_other: neg_trig.outputs.append( Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_oran), )) pos_trig.outputs.append( Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_oran), )) if oran_enabled: voice_attr['orangegelgun'] = True voice_attr['orangegel'] = True voice_attr['speedgelgun'] = True voice_attr['speedgel'] = True if not oran_enabled and not blue_enabled: # If both are disabled, we must shutdown the gun when touching # either side - use neg_trig for that purpose! # We want to get rid of pos_trig to save ents vmf.remove_ent(pos_trig) neg_trig['targetname'] = conditions.local_name( fizzler.base_inst, 'trig_off') neg_trig.outputs.clear() neg_trig.add_out( Output(output, '@BlueIsEnabled', 'SetValue', param='0')) neg_trig.add_out( Output(output, '@OrangeIsEnabled', 'SetValue', param='0')) # Make the triggers. for bbox_min, bbox_max in fizzler.emitters: bbox_min = bbox_min.copy() - 64 * fizzler.up_axis bbox_max = bbox_max.copy() + 64 * fizzler.up_axis # The triggers are 8 units thick, with a 32-unit gap in the middle neg_min, neg_max = Vec(bbox_min), Vec(bbox_max) neg_min[fizz_norm_axis] -= 24 neg_max[fizz_norm_axis] -= 16 pos_min, pos_max = Vec(bbox_min), Vec(bbox_max) pos_min[fizz_norm_axis] += 16 pos_max[fizz_norm_axis] += 24 if blue_enabled or oran_enabled: neg_trig.solids.append( vmf.make_prism( neg_min, neg_max, mat='tools/toolstrigger', ).solid, ) pos_trig.solids.append( vmf.make_prism( pos_min, pos_max, mat='tools/toolstrigger', ).solid, ) else: # If neither enabled, use one trigger neg_trig.solids.append( vmf.make_prism( neg_min, pos_max, mat='tools/toolstrigger', ).solid, )
def res_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 add_voice( voice_attrs: dict, style_vars: dict, vmf: VMF, coll: Collisions, use_priority=True, ) -> None: """Add a voice line to the map.""" from precomp.conditions.monitor import make_voice_studio LOGGER.info('Adding Voice Lines!') norm_config = ConfigFile('bee2/voice.cfg', in_conf_folder=False) mid_config = ConfigFile('bee2/mid_voice.cfg', in_conf_folder=False) quote_base = QUOTE_DATA['base', False] quote_loc = get_studio_loc() if quote_base: LOGGER.info('Adding Base instance!') conditions.add_inst( vmf, targetname='voice', file=INST_PREFIX + quote_base, origin=quote_loc, ) # Either box in with nodraw, or place the voiceline studio. has_studio = make_voice_studio(vmf) bullsye_actor = vbsp_options.get(str, 'voice_studio_actor') if bullsye_actor and has_studio: ADDED_BULLSEYES.add(bullsye_actor) global_bullseye = QUOTE_DATA['bullseye', ''] if global_bullseye: add_bullseye(vmf, quote_loc, global_bullseye) allow_mid_voices = not style_vars.get('nomidvoices', False) mid_quotes = [] # Enable using the beep before and after choreo lines. allow_dings = srctools.conv_bool(QUOTE_DATA['use_dings', '0']) if allow_dings: vmf.create_ent( classname='logic_choreographed_scene', targetname='@ding_on', origin=quote_loc + (-8, -16, 0), scenefile='scenes/npc/glados_manual/ding_on.vcd', busyactor="1", # Wait for actor to stop talking onplayerdeath='0', ) vmf.create_ent( classname='logic_choreographed_scene', targetname='@ding_off', origin=quote_loc + (8, -16, 0), scenefile='scenes/npc/glados_manual/ding_off.vcd', busyactor="1", # Wait for actor to stop talking onplayerdeath='0', ) # QuoteEvents allows specifying an instance for particular items, # so a voice line can be played at a certain time. It's only active # in certain styles, but uses the default if not set. for event in QUOTE_DATA.find_all('QuoteEvents', 'Event'): event_id = event['id', ''].casefold() # We ignore the config if no result was executed. if event_id and event_id in QUOTE_EVENTS: # Instances from the voiceline config are in this subfolder, # but not the default item - that's set from the conditions QUOTE_EVENTS[event_id] = INST_PREFIX + event['file'] LOGGER.info('Quote events: {}', list(QUOTE_EVENTS.keys())) if has_responses(): LOGGER.info('Generating responses data..') encode_coop_responses(vmf, quote_loc, allow_dings, voice_attrs) for ind, file in enumerate(QUOTE_EVENTS.values()): if not file: continue conditions.add_inst( vmf, targetname='voice_event_' + str(ind), file=file, origin=quote_loc, ) # Determine the flags that enable/disable specific lines based on which # players are used. player_model = vbsp.BEE2_config.get_val( 'General', 'player_model', 'PETI', ).casefold() is_coop = (vbsp.GAME_MODE == 'COOP') is_sp = (vbsp.GAME_MODE == 'SP') player_flags = { 'sp': is_sp, 'coop': is_coop, 'atlas': is_coop or player_model == 'atlas', 'pbody': is_coop or player_model == 'pbody', 'bendy': is_sp and player_model == 'peti', 'chell': is_sp and player_model == 'sp', 'human': is_sp and player_model in ('peti', 'sp'), 'robot': is_coop or player_model in ('atlas', 'pbody'), } # All which are True. player_flag_set = {val for val, flag in player_flags.items() if flag} # For each group, locate the voice lines. for group in itertools.chain( QUOTE_DATA.find_all('group'), QUOTE_DATA.find_all('midchamber'), ): # type: Property quote_targetname = group['Choreo_Name', '@choreo'] use_dings = group.bool('use_dings', allow_dings) possible_quotes = sorted( find_group_quotes( vmf, coll, group, mid_quotes, use_dings=use_dings, allow_mid_voices=allow_mid_voices, conf=mid_config if group.name == 'midchamber' else norm_config, mid_name=quote_targetname, player_flag_set=player_flag_set, ), key=sort_func, reverse=True, ) LOGGER.debug('Possible {}quotes:', 'mid ' if group.name == 'midchamber' else '') for quot in possible_quotes: LOGGER.debug('- {}', quot) if possible_quotes: choreo_loc = group.vec('choreo_loc', *quote_loc) if use_priority: chosen = possible_quotes[0].lines else: # Chose one of the quote blocks. chosen = rand.seed( b'VOICE_QUOTE_BLOCK', *[ prop['id', 'ID'] for quoteblock in possible_quotes for prop in quoteblock.lines ]).choice(possible_quotes).lines # Use the IDs for the voice lines, so each quote block will chose different lines. rng = rand.seed(b'VOICE_QUOTE', *[prop['id', 'ID'] for prop in chosen]) # Add one of the associated quotes add_quote( vmf, rng.choice(chosen), quote_targetname, choreo_loc, style_vars, use_dings, ) if ADDED_BULLSEYES or QUOTE_DATA.bool('UseMicrophones'): # Add microphones that broadcast audio directly at players. # This ensures it is heard regardless of location. # This is used for Cave and core Wheatley. LOGGER.info('Using microphones...') if vbsp.GAME_MODE == 'SP': vmf.create_ent( classname='env_microphone', targetname='player_speaker_sp', speakername='!player', maxRange='386', origin=quote_loc, ) else: vmf.create_ent( classname='env_microphone', targetname='player_speaker_blue', speakername='!player_blue', maxRange='386', origin=quote_loc, ) vmf.create_ent( classname='env_microphone', targetname='player_speaker_orange', speakername='!player_orange', maxRange='386', origin=quote_loc, ) LOGGER.info('{} Mid quotes', len(mid_quotes)) for mid_lines in mid_quotes: line = rand.seed(b'mid_quote', *[name for item, ding, name in mid_lines ]).choice(mid_lines) mid_item, use_ding, mid_name = line add_quote(vmf, mid_item, mid_name, quote_loc, style_vars, use_ding) LOGGER.info('Done!')
def add_quote( vmf: VMF, quote: Property, targetname: str, quote_loc: Vec, style_vars: dict, use_dings: bool, ) -> None: """Add a quote to the map.""" LOGGER.info('Adding quote: {}', quote) only_once = atomic = False cc_emit_name = None start_ents = [] # type: List[Entity] end_commands = [] start_names = [] # The OnUser1 outputs always play the quote (PlaySound/Start), so you can # mix ent types in the same pack. for prop in quote: name = prop.name.casefold() if name == 'file': conditions.add_inst( vmf, file=INST_PREFIX + prop.value, origin=quote_loc, no_fixup=True, ) elif name == 'choreo': # If the property has children, the children are a set of sequential # voice lines. # If the name is set to '@glados_line', the ents will be named # ('@glados_line', 'glados_line_2', 'glados_line_3', ...) start_names.append(targetname) if prop.has_children(): secondary_name = targetname.lstrip('@') + '_' # Evenly distribute the choreo ents across the width of the # voice-line room. off = Vec(y=120 / (len(prop) + 1)) start = quote_loc - (0, 60, 0) + off for ind, choreo_line in enumerate( prop, start=1): # type: int, Property is_first = (ind == 1) is_last = (ind == len(prop)) name = (targetname if is_first else secondary_name + str(ind)) choreo = add_choreo( vmf, choreo_line.value, targetname=name, loc=start + off * (ind - 1), use_dings=use_dings, is_first=is_first, is_last=is_last, only_once=only_once, ) # Add a IO command to start the next one. if not is_last: choreo.add_out( Output( 'OnCompletion', secondary_name + str(ind + 1), 'Start', delay=0.1, )) if is_first: # Ensure this works with cc_emit start_ents.append(choreo) if is_last: for out in end_commands: choreo.add_out(out.copy()) end_commands.clear() else: # Add a single choreo command. choreo = add_choreo( vmf, prop.value, targetname, quote_loc, use_dings=use_dings, only_once=only_once, ) start_ents.append(choreo) for out in end_commands: choreo.add_out(out.copy()) end_commands.clear() elif name == 'snd': start_names.append(targetname) snd = vmf.create_ent( classname='ambient_generic', spawnflags='49', # Infinite Range, Starts Silent targetname=targetname, origin=quote_loc, message=prop.value, health='10', # Volume ) snd.add_out( Output( 'OnUser1', targetname, 'PlaySound', only_once=only_once, )) start_ents.append(snd) elif name == 'bullseye': add_bullseye(vmf, quote_loc, prop.value) elif name == 'cc_emit': # In Aperture Tag, this additional console command is used # to add the closed captions. # Store in a variable, so we can be sure to add the output # regardless of the property order. cc_emit_name = prop.value elif name == 'setstylevar': # Set this stylevar to True # This is useful so some styles can react to which line was # chosen. style_vars[prop.value.casefold()] = True elif name == 'packlist': packing.pack_list(vmf, prop.value) elif name == 'pack': if prop.has_children(): packing.pack_files(vmf, *[subprop.value for subprop in prop]) else: packing.pack_files(vmf, prop.value) elif name == 'choreo_name': # Change the targetname used for subsequent entities targetname = prop.value elif name == 'onlyonce': only_once = srctools.conv_bool(prop.value) elif name == 'atomic': atomic = srctools.conv_bool(prop.value) elif name == 'endcommand': end_commands.append( Output( 'OnCompletion', prop['target'], prop['input'], prop['parm', ''], prop.float('delay'), only_once=prop.bool('only_once'), times=prop.int('times', -1), )) if cc_emit_name: for ent in start_ents: ent.add_out( Output( 'OnUser1', '@command', 'Command', param='cc_emit ' + cc_emit_name, )) # If Atomic is true, after a line is started all variants # are blocked from playing. if atomic: for ent in start_ents: for name in start_names: if ent['targetname'] == name: # Don't block yourself. continue ent.add_out(Output( 'OnUser1', name, 'Kill', only_once=True, ))
def link_item(vmf: VMF, group: list[item_chain.Node[Config]]) -> None: """Link together a single group of items.""" chains = item_chain.chain(group, allow_loop=True) for group_counter, node_list in enumerate(chains): is_looped = False if node_list[ 0].prev is not None: # It's looped, check if it's allowed. if not all(node.conf.allow_loop for node in node_list): LOGGER.warning( '- Group is looped, but this is not allowed! Arbitarily breaking.' ) node_list[0].prev = node_list[-1].next = None else: is_looped = True for index, node in enumerate(node_list): conf = node.conf is_floor = node.orient.up().z > 0.99 if node.next is None and node.prev is None: # No connections in either direction, just skip. continue # We can't touch antlines if the item has regular outputs. if not node.item.outputs: if conf.antline is AntlineHandling.REMOVE: node.item.delete_antlines() elif conf.antline is AntlineHandling.MOVE: if index != 0: node.item.transfer_antlines(node_list[0].item) elif conf.antline is AntlineHandling.KEEP: pass else: raise AssertionError(conf.antline) # Transfer inputs and outputs to the first. if index != 0 and conf.transfer_io: for conn in list(node.item.outputs): conn.from_item = node_list[0].item for conn in list(node.item.inputs): conn.to_item = node_list[0].item # If start/end, the other node. other_node: Optional[item_chain.Node[Config]] = None if is_looped: node.inst.fixup['$type'] = 'loop' logic_fname = conf.logic_loop elif node.prev is None: node.inst.fixup['$type'] = 'start' logic_fname = conf.logic_start other_node = node.next elif node.next is None: node.inst.fixup['$type'] = 'end' logic_fname = conf.logic_end other_node = node.prev else: node.inst.fixup['$type'] = 'mid' logic_fname = conf.logic_mid # Add values indicating the group, position, and next item. node.inst.fixup['$group'] = group_counter node.inst.fixup['$ind'] = index if node.next is not None: # If looped, it might have to wrap around. if node.next is node_list[0]: node.inst.fixup['$next'] = '0' else: node.inst.fixup['$next'] = index + 1 if logic_fname: inst_logic = conditions.add_inst( vmf, targetname=node.inst['targetname'], file=logic_fname, origin=node.pos, angles=node.inst['angles'], ) inst_logic.fixup.update(node.inst.fixup) # Special case for Unstationary Scaffolds - change to an instance # for the ends, pointing in the direction of the connected track. if other_node is not None and is_floor and conf.scaff_endcap: link_dir = other_node.pos - node.pos # Compute the horizontal gradient (z / xy dist). # Don't use endcap if rising more than ~45 degrees, or lowering # more than ~12 degrees. horiz_dist = math.sqrt(link_dir.x**2 + link_dir.y**2) if horiz_dist != 0 and -0.15 <= (link_dir.z / horiz_dist) <= 1: link_ang = math.degrees(math.atan2(link_dir.y, link_dir.x)) if not conf.scaff_endcap_free_rot: # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals link_ang = (link_ang + 45) // 90 * 90 node.inst['file'] = conf.scaff_endcap conditions.ALL_INST.add(conf.scaff_endcap.casefold()) node.inst['angles'] = '0 {:.0f} 0'.format(link_ang)
def make_bottomless_pit(vmf: VMF, max_height): """Generate bottomless pits.""" import vbsp tele_ref = SETTINGS['tele_ref'] tele_dest = SETTINGS['tele_dest'] use_skybox = bool(SETTINGS['skybox']) if use_skybox: tele_off = Vec( x=SETTINGS['off_x'], y=SETTINGS['off_y'], ) else: tele_off = Vec(0, 0, 0) # Controlled by the style, not skybox! blend_light = options.get(str, 'pit_blend_light') if use_skybox: # Add in the actual skybox edges and triggers. conditions.add_inst( vmf, file=SETTINGS['skybox'], targetname='skybox', origin=tele_off, ) fog_opt = vbsp.settings['fog'] # Now generate the sky_camera, with appropriate values. sky_camera = vmf.create_ent( classname='sky_camera', scale='1.0', origin=tele_off, angles=fog_opt['direction'], fogdir=fog_opt['direction'], fogcolor=fog_opt['primary'], fogstart=fog_opt['start'], fogend=fog_opt['end'], fogenable='1', heightFogStart=fog_opt['height_start'], heightFogDensity=fog_opt['height_density'], heightFogMaxDensity=fog_opt['height_max_density'], ) if fog_opt['secondary']: # Only enable fog blending if a secondary color is enabled sky_camera['fogblend'] = '1' sky_camera['fogcolor2'] = fog_opt['secondary'] sky_camera['use_angles'] = '1' else: sky_camera['fogblend'] = '0' sky_camera['use_angles'] = '0' if SETTINGS['skybox_ceil'] != '': # We dynamically add the ceiling so it resizes to match the map, # and lighting won't be too far away. conditions.add_inst( vmf, file=SETTINGS['skybox_ceil'], targetname='skybox', origin=tele_off + (0, 0, max_height), ) if SETTINGS['targ'] != '': # Add in the teleport reference target. conditions.add_inst( vmf, file=SETTINGS['targ'], targetname='skybox', origin='0 0 0', ) # First, remove all of Valve's triggers inside pits. for trig in vmf.by_class['trigger_multiple'] | vmf.by_class['trigger_hurt']: if brushLoc.POS['world':Vec.from_str(trig['origin'])].is_pit: trig.remove() # Potential locations of bordering brushes.. wall_pos = set() side_dirs = [ (0, -128, 0), # N (0, +128, 0), # S (-128, 0, 0), # E (+128, 0, 0) # W ] # Only use 1 entity for the teleport triggers. If multiple are used, # cubes can contact two at once and get teleported odd places. tele_trig = None hurt_trig = None for grid_pos, block_type in brushLoc.POS.items( ): # type: Vec, brushLoc.Block pos = brushLoc.grid_to_world(grid_pos) if not block_type.is_pit: continue # Physics objects teleport when they hit the bottom of a pit. if block_type.is_bottom and use_skybox: if tele_trig is None: tele_trig = vmf.create_ent( classname='trigger_teleport', spawnflags='4106', # Physics and npcs landmark=tele_ref, target=tele_dest, origin=pos, ) tele_trig.solids.append( vmf.make_prism( pos + (-64, -64, -64), pos + (64, 64, -8), mat='tools/toolstrigger', ).solid, ) # Players, however get hurt as soon as they enter - that way it's # harder to see that they don't teleport. if block_type.is_top: if hurt_trig is None: hurt_trig = vmf.create_ent( classname='trigger_hurt', damagetype=32, # FALL spawnflags=1, # CLients damage=100000, nodmgforce=1, # No physics force when hurt.. damagemodel=0, # Always apply full damage. origin=pos, # We know this is not in the void.. ) hurt_trig.solids.append( vmf.make_prism( Vec(pos.x - 64, pos.y - 64, -128), pos + (64, 64, 48 if use_skybox else 16), mat='tools/toolstrigger', ).solid, ) if not block_type.is_bottom: continue # Everything else is only added to the bottom-most position. if use_skybox and blend_light: # Generate dim lights at the skybox location, # to blend the lighting together. light_pos = pos + (0, 0, -60) vmf.create_ent( classname='light', origin=light_pos, _light=blend_light, _fifty_percent_distance='256', _zero_percent_distance='512', ) vmf.create_ent( classname='light', origin=light_pos + tele_off, _light=blend_light, _fifty_percent_distance='256', _zero_percent_distance='512', ) wall_pos.update([(pos + off).as_tuple() for off in side_dirs]) if hurt_trig is not None: hurt_trig.outputs.append(Output( 'OnHurtPlayer', '@goo_fade', 'Fade', ), ) if not use_skybox: make_pit_shell(vmf) return # Now determine the position of side instances. # We use the utils.CONN_TYPES dict to determine instance positions # based on where nearby walls are. side_types = { utils.CONN_TYPES.side: PIT_INST['side'], # o| utils.CONN_TYPES.corner: PIT_INST['corner'], # _| utils.CONN_TYPES.straight: PIT_INST['side'], # Add this twice for |o| utils.CONN_TYPES.triple: PIT_INST['triple'], # U-shape utils.CONN_TYPES.all: PIT_INST['pillar'], # [o] } LOGGER.info('Pit instances: {}', side_types) for pos in wall_pos: pos = Vec(pos) if not brushLoc.POS['world':pos].is_solid: # Not actually a wall here! continue # CONN_TYPES has n,s,e,w as keys - whether there's something in that direction. nsew = tuple(brushLoc.POS['world':pos + off].is_pit for off in side_dirs) LOGGER.info('Pos: {}, NSEW: {}, lookup: {}', pos, nsew, utils.CONN_LOOKUP[nsew]) inst_type, angle = utils.CONN_LOOKUP[nsew] if inst_type is utils.CONN_TYPES.none: # Middle of the pit... continue rng = rand.seed(b'pit', pos.x, pos.y) file = rng.choice(side_types[inst_type]) if file != '': conditions.add_inst( vmf, file=file, targetname='goo_side', origin=tele_off + pos, angles=angle, ).make_unique() # Straight uses two side-instances in parallel - "|o|" if inst_type is utils.CONN_TYPES.straight: file = rng.choice(side_types[inst_type]) if file != '': conditions.add_inst( vmf, file=file, targetname='goo_side', origin=tele_off + pos, # Reverse direction angles=Angle.from_str(angle) + (0, 180, 0), ).make_unique()
def res_make_catwalk(vmf: VMF, res: Property): """Speciallised result to generate catwalks from markers. Only runs once, and then quits the condition list. * Instances: * `markerInst: The instance set in editoritems. * `straight_128`/`256`/`512`: Straight sections. Extends East. * `corner: An L-corner piece. Connects on North and West sides. * `TJunction`: A T-piece. Connects on all but the East side. * `crossJunction`: A X-piece. Connects on all sides. * `end`: An end piece. Connects on the East side. * `stair`: A stair. Starts East and goes Up and West. * `end_wall`: Connects a West wall to a East catwalk. * `support_wall`: A support extending from the East wall. * `support_ceil`: A support extending from the ceiling. * `support_floor`: A support extending from the floor. * `support_goo`: A floor support, designed for goo pits. * `single_wall`: A section connecting to an East wall. """ LOGGER.info("Starting catwalk generator...") marker = instanceLocs.resolve(res['markerInst']) instances = { name: instanceLocs.resolve_one(res[name, ''], error=True) for name in ( 'straight_128', 'straight_256', 'straight_512', 'corner', 'tjunction', 'crossjunction', 'end', 'stair', 'end_wall', 'support_wall', 'support_ceil', 'support_floor', 'support_goo', 'single_wall', 'markerInst', ) } # If there are no attachments remove a catwalk piece instances['NONE'] = '' if instances['end_wall'] == '': instances['end_wall'] = instances['end'] # The directions this instance is connected by (NSEW) links = {} # type: Dict[Entity, Link] markers = {} # Find all our markers, so we can look them up by targetname. for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in marker: continue links[inst] = Link() markers[inst['targetname']] = inst # Snap the markers to the grid. If on glass it can become offset... origin = Vec.from_str(inst['origin']) origin = origin // 128 * 128 origin += 64 while brushLoc.POS['world':origin].is_goo: # The instance is in goo! Switch to floor orientation, and move # up until it's in air. inst['angles'] = '0 0 0' origin.z += 128 inst['origin'] = str(origin) if not markers: return conditions.RES_EXHAUSTED LOGGER.info('Connections: {}', links) LOGGER.info('Markers: {}', markers) # First loop through all the markers, adding connecting sections for marker_name, inst in markers.items(): mark_item = ITEMS[marker_name] mark_item.delete_antlines() for conn in list(mark_item.outputs): try: inst2 = markers[conn.to_item.name] except KeyError: LOGGER.warning('Catwalk connected to non-catwalk!') conn.remove() origin1 = Vec.from_str(inst['origin']) origin2 = Vec.from_str(inst2['origin']) if origin1.x != origin2.x and origin1.y != origin2.y: LOGGER.warning('Instances not aligned!') continue y_dir = origin1.x == origin2.x # Which way the connection is if y_dir: dist = abs(origin1.y - origin2.y) else: dist = abs(origin1.x - origin2.x) vert_dist = origin1.z - origin2.z if (dist - 128) // 2 < abs(vert_dist): # The stairs are 2 long, 1 high. Check there's enough room # Subtract the last block though, since that's a corner. LOGGER.warning('Not enough room for stairs!') continue if dist > 128: # add straight sections in between place_catwalk_connections(vmf, instances, origin1, origin2) # Update the lists based on the directions that were set conn_lst1 = links[inst] conn_lst2 = links[inst2] if origin1.x < origin2.x: conn_lst1.E = conn_lst2.W = True elif origin2.x < origin1.x: conn_lst1.W = conn_lst2.E = True if origin1.y < origin2.y: conn_lst1.N = conn_lst2.S = True elif origin2.y < origin1.y: conn_lst1.S = conn_lst2.N = True for inst, dir_mask in links.items(): # Set the marker instances based on the attached walkways. normal = Vec(0, 0, 1).rotate_by_str(inst['angles']) new_type, inst['angles'] = utils.CONN_LOOKUP[dir_mask.as_tuple()] inst['file'] = filename = instances[CATWALK_TYPES[new_type]] conditions.ALL_INST.add(filename.casefold()) if new_type is utils.CONN_TYPES.side: # If the end piece is pointing at a wall, switch the instance. if normal.z == 0: if normal == dir_mask.conn_dir(): inst['file'] = instances['end_wall'] conditions.ALL_INST.add(instances['end_wall'].casefold()) continue # We never have normal supports on end pieces elif new_type is utils.CONN_TYPES.none: # Unconnected catwalks on the wall switch to a special instance. # This lets players stand next to a portal surface on the wall. if normal.z == 0: inst['file'] = instances['single_wall'] conditions.ALL_INST.add(instances['single_wall'].casefold()) inst['angles'] = conditions.INST_ANGLE[normal.as_tuple()] else: inst.remove() continue # These don't get supports otherwise # Add regular supports supp = None if normal == (0, 0, 1): # If in goo, use different supports! origin = Vec.from_str(inst['origin']) origin.z -= 128 if brushLoc.POS['world':origin].is_goo: supp = instances['support_goo'] else: supp = instances['support_floor'] elif normal == (0, 0, -1): supp = instances['support_ceil'] else: supp = instances['support_wall'] if supp: conditions.add_inst( vmf, origin=inst['origin'], angles=conditions.INST_ANGLE[normal.as_tuple()], file=supp, ) LOGGER.info('Finished catwalk generation!') return conditions.RES_EXHAUSTED
def res_add_overlay_inst(vmf: VMF, inst: Entity, res: Property) -> Optional[Entity]: """Add another instance on top of this one. If a single value, this sets only the filename. Values: - `file`: The filename. - `fixup_style`: The Fixup style for the instance. '0' (default) is Prefix, '1' is Suffix, and '2' is None. - `copy_fixup`: If true, all the `$replace` values from the original instance will be copied over. - `move_outputs`: If true, outputs will be moved to this instance. - `offset`: The offset (relative to the base) that the instance will be placed. Can be set to `<piston_top>` and `<piston_bottom>` to offset based on the configuration. `<piston_start>` will set it to the starting position, and `<piston_end>` will set it to the ending position of the Piston Platform's handles. - `rotation`: Rotate the instance by this amount. - `angles`: If set, overrides `rotation` and the instance angles entirely. - `fixup`/`localfixup`: Keyvalues in this block will be copied to the overlay entity. - If the value starts with `$`, the variable will be copied over. - If this is present, `copy_fixup` will be disabled. """ if not res.has_children(): # Use all the defaults. res = Property('AddOverlay', [ Property('File', res.value) ]) if 'angles' in res: angles = Angle.from_str(res['angles']) if 'rotation' in res: LOGGER.warning('"angles" option overrides "rotation"!') else: angles = Angle.from_str(res['rotation', '0 0 0']) angles @= Angle.from_str(inst['angles', '0 0 0']) orig_name = conditions.resolve_value(inst, res['file', '']) filename = instanceLocs.resolve_one(orig_name) if not filename: if not res.bool('silentLookup'): LOGGER.warning('Bad filename for "{}" when adding overlay!', orig_name) # Don't bother making a overlay which will be deleted. return None overlay_inst = conditions.add_inst( vmf, targetname=inst['targetname', ''], file=filename, angles=angles, origin=inst['origin'], fixup_style=res.int('fixup_style'), ) # Don't run if the fixup block exists.. if srctools.conv_bool(res['copy_fixup', '1']): if 'fixup' not in res and 'localfixup' not in res: # Copy the fixup values across from the original instance for fixup, value in inst.fixup.items(): overlay_inst.fixup[fixup] = value conditions.set_ent_keys(overlay_inst.fixup, inst, res, 'fixup') if res.bool('move_outputs', False): overlay_inst.outputs = inst.outputs inst.outputs = [] if 'offset' in res: overlay_inst['origin'] = conditions.resolve_offset(inst, res['offset']) return overlay_inst
def make_straight( vmf: VMF, origin: Vec, normal: Vec, dist: int, config: Config, is_start=False, ) -> None: """Make a straight line of instances from one point to another.""" angles = round(normal, 6).to_angle() orient = Matrix.from_angle(angles) # The starting brush needs to stick out a bit further, to cover the # point_push entity. start_off = -96 if is_start else -64 p1, p2 = Vec.bbox( origin + Vec(start_off, -config.trig_radius, -config.trig_radius) @ orient, origin + Vec(dist - 64, config.trig_radius, config.trig_radius) @ orient, ) solid = vmf.make_prism(p1, p2, mat='tools/toolstrigger').solid motion_trigger(vmf, solid.copy()) push_trigger(vmf, origin, normal, [solid]) off = 0 for seg_dist in utils.fit(dist, config.inst_straight_sizes): conditions.add_inst( vmf, origin=origin + off * orient.forward(), angles=angles, file=config.inst_straight[seg_dist], ) off += seg_dist # Supports. if config.inst_support: for off in range(0, int(dist), 128): position = origin + off * normal placed_support = False for supp_dir in [ orient.up(), orient.left(), -orient.left(), -orient.up() ]: try: tile = tiling.TILES[(position - 128 * supp_dir).as_tuple(), supp_dir.norm().as_tuple()] except KeyError: continue # Check all 4 center tiles are present. if all(tile[u, v].is_tile for u in (1, 2) for v in (1, 2)): conditions.add_inst( vmf, origin=position, angles=Matrix.from_basis(x=normal, z=supp_dir).to_angle(), file=config.inst_support, ) placed_support = True if placed_support and config.inst_support_ring: conditions.add_inst( vmf, origin=position, angles=angles, file=config.inst_support_ring, )
def add( vmf: VMF, loc: Vec, conf: Property, voice_attr: Dict[str, str], is_sp: bool, ) -> None: """Add music to the map.""" LOGGER.info("Adding Music...") # These values are exported by the BEE2 app, indicating the # options on the music item. inst = options.get(str, 'music_instance') snd_length = options.get(int, 'music_looplen') # Don't add our logic if an instance was provided. # If this settings is set, we have a music config. if conf and not inst: music = vmf.create_ent( classname='ambient_generic', spawnflags='17', # Looping, Infinite Range, Starts Silent targetname='@music', origin=loc, message='music.BEE2', health='10', # Volume ) music_start = vmf.create_ent( classname='logic_relay', spawnflags='0', targetname='@music_start', origin=loc + (-16, 0, -16), ) music_stop = vmf.create_ent( classname='logic_relay', spawnflags='0', targetname='@music_stop', origin=loc + (16, 0, -16), ) music_stop.add_out( Output('OnTrigger', music, 'StopSound'), Output('OnTrigger', music, 'Volume', '0'), ) # In SinglePlayer, music gets killed during reload, # so we need to restart it. # If snd_length is set, we have a non-loopable MP3 # and want to re-trigger it after the time elapses, to simulate # looping. # In either case, we need @music_restart to do that safely. if is_sp or snd_length > 0: music_restart = vmf.create_ent( classname='logic_relay', spawnflags='2', # Allow fast retrigger. targetname='@music_restart', StartDisabled='1', origin=loc + (0, 0, -16), ) music_start.add_out( Output('OnTrigger', music_restart, 'Enable'), Output('OnTrigger', music_restart, 'Trigger', delay=0.01), ) music_stop.add_out( Output('OnTrigger', music_restart, 'Disable'), Output('OnTrigger', music_restart, 'CancelPending'), ) music_restart.add_out( Output('OnTrigger', music, 'StopSound'), Output('OnTrigger', music, 'Volume', '0'), Output('OnTrigger', music, 'Volume', '10', delay=0.1), Output('OnTrigger', music, 'PlaySound', delay=0.1), ) if is_sp == 'SP': # Trigger on level loads. vmf.create_ent( classname='logic_auto', origin=loc + (0, 0, 16), spawnflags='0', # Don't remove after fire globalstate='', ).add_out( Output('OnLoadGame', music_restart, 'CancelPending'), Output('OnLoadGame', music_restart, 'Trigger', delay=0.01), ) if snd_length > 0: # Re-trigger after the music duration. music_restart.add_out( Output('OnTrigger', '!self', 'Trigger', delay=snd_length)) # Set to non-looping, so re-playing will restart it correctly. music['spawnflags'] = '49' else: # The music track never needs to have repeating managed, # just directly trigger. music_start.add_out( Output('OnTrigger', music, 'PlaySound'), Output('OnTrigger', music, 'Volume', '10'), ) # Add the ents for the config itself. # If the items aren't in the map, we can skip adding them. # Speed-gel sounds also play when flinging, so keep it always. funnel = conf.find_key('tbeam', or_blank=True) bounce = conf.find_key('bouncegel', or_blank=True) make_channel_conf( vmf, loc, Channel.BASE, conf.find_key('base', or_blank=True).as_array(), ) make_channel_conf( vmf, loc, Channel.SPEED, conf.find_key('speedgel', or_blank=True).as_array(), ) if 'funnel' in voice_attr or 'excursionfunnel' in voice_attr: make_channel_conf( vmf, loc, Channel.TBEAM, funnel.as_array(), conf.bool('sync_funnel'), ) if 'bouncegel' in voice_attr or 'bluegel' in voice_attr: make_channel_conf( vmf, loc, Channel.BOUNCE, bounce.as_array(), ) packfiles = conf.find_key('pack', or_blank=True).as_array() if packfiles: packer = vmf.create_ent('comp_pack', origin=loc) for i, fname in enumerate(packfiles, 1): packer[f'generic{i:02}'] = fname if inst: # We assume the instance is setup correct. conditions.add_inst( vmf, targetname='music', origin=loc, file=inst, )
def place_catwalk_connections(vmf: VMF, instances, point_a: Vec, point_b: Vec): """Place catwalk sections to connect two straight points.""" diff = point_b - point_a # The horizontal unit vector in the direction we are placing catwalks direction = diff.copy() direction.z = 0 distance = direction.len() - 128 direction = direction.norm() if diff.z > 0: angle = conditions.INST_ANGLE[direction.as_tuple()] # We need to add stairs for stair_pos in range(0, int(diff.z), 128): # Move twice the vertical horizontally # plus 128 so we don't start in point A loc = point_a + (2 * stair_pos + 128) * direction # Do the vertical offset loc.z += stair_pos conditions.add_inst( vmf, origin=loc, angles=angle, file=instances['stair'], ) # This is the location we start flat sections at point_a = loc + 128 * direction point_a.z += 128 elif diff.z < 0: # We need to add downward stairs # They point opposite to normal ones LOGGER.debug('down from {}', point_a) angle = conditions.INST_ANGLE[(-direction).as_tuple()] for stair_pos in range(0, -int(diff.z), 128): LOGGER.debug(stair_pos) # Move twice the vertical horizontally loc = point_a + (2 * stair_pos + 256) * direction # Do the vertical offset plus additional 128 units # to account for the moved instance loc.z -= (stair_pos + 128) conditions.add_inst( vmf, origin=loc, angles=angle, file=instances['stair'], ) # Adjust point A to be at the end of the catwalks point_a = loc # Remove the space the stairs take up from the horiz distance distance -= abs(diff.z) * 2 # Now do straight sections LOGGER.debug('Stretching {} {}', distance, direction) angle = conditions.INST_ANGLE[direction.as_tuple()] loc = point_a + (direction * 128) # Figure out the most efficient number of sections for segment_len in utils.fit(distance, [512, 256, 128]): conditions.add_inst( vmf, origin=loc, angles=angle, file=instances['straight_' + str(segment_len)], ) loc += (segment_len * direction)
def res_conveyor_belt(vmf: VMF, inst: Entity, res: Property) -> None: """Create a conveyor belt. * Options: * `SegmentInst`: Generated at each square. (`track` is the name of the path to attach to.) * `TrackTeleport`: Set the track points so they teleport trains to the start. * `Speed`: The fixup or number for the train speed. * `MotionTrig`: If set, a trigger_multiple will be spawned that `EnableMotion`s weighted cubes. The value is the name of the relevant filter. * `EndOutput`: Adds an output to the last track. The value is the same as outputs in VMFs. `RotateSegments`: If true (default), force segments to face in the direction of movement. * `BeamKeys`: If set, a list of keyvalues to use to generate an env_beam travelling from start to end. The origin is treated specially - X is the distance from walls, y is the distance to the side, and z is the height. `RailTemplate`: A template for the track sections. This is made into a non-solid func_brush, combining all sections. * `NoPortalFloor`: If set, add a `func_noportal_volume` on the floor under the track. * `PaintFizzler`: If set, add a paint fizzler underneath the belt. """ move_dist = inst.fixup.int('$travel_distance') if move_dist <= 2: # There isn't room for a conveyor, so don't bother. inst.remove() return orig_orient = Matrix.from_angle(Angle.from_str(inst['angles'])) move_dir = Vec(1, 0, 0) @ Angle.from_str(inst.fixup['$travel_direction']) move_dir = move_dir @ orig_orient start_offset = inst.fixup.float('$starting_position') teleport_to_start = res.bool('TrackTeleport', True) segment_inst_file = instanceLocs.resolve_one(res['SegmentInst', '']) rail_template = res['RailTemplate', None] track_speed = res['speed', None] start_pos = Vec.from_str(inst['origin']) end_pos = start_pos + move_dist * move_dir if start_offset > 0: # If an oscillating platform, move to the closest side.. offset = start_offset * move_dir # The instance is placed this far along, so move back to the end. start_pos -= offset end_pos -= offset if start_offset > 0.5: # Swap the direction of movement.. start_pos, end_pos = end_pos, start_pos inst['origin'] = start_pos norm = orig_orient.up() if res.bool('rotateSegments', True): orient = Matrix.from_basis(x=move_dir, z=norm) inst['angles'] = orient.to_angle() else: orient = orig_orient # Add the EnableMotion trigger_multiple seen in platform items. # This wakes up cubes when it starts moving. motion_filter = res['motionTrig', None] # Disable on walls, or if the conveyor can't be turned on. if norm != (0, 0, 1) or inst.fixup['$connectioncount'] == '0': motion_filter = None track_name = conditions.local_name(inst, 'segment_{}') rail_temp_solids = [] last_track = None # Place tracks at the top, so they don't appear inside wall sections. track_start: Vec = start_pos + 48 * norm track_end: Vec = end_pos + 48 * norm for index, pos in enumerate(track_start.iter_line(track_end, stride=128), start=1): track = vmf.create_ent( classname='path_track', targetname=track_name.format(index) + '-track', origin=pos, spawnflags=0, orientationtype=0, # Don't rotate ) if track_speed is not None: track['speed'] = track_speed if last_track: last_track['target'] = track['targetname'] if index == 1 and teleport_to_start: track['spawnflags'] = 16 # Teleport here.. last_track = track # Don't place at the last point - it doesn't teleport correctly, # and would be one too many. if segment_inst_file and pos != track_end: seg_inst = conditions.add_inst( vmf, targetname=track_name.format(index), file=segment_inst_file, origin=pos, angles=orient, ) seg_inst.fixup.update(inst.fixup) if rail_template: temp = template_brush.import_template( vmf, rail_template, pos, orient, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) rail_temp_solids.extend(temp.world) if rail_temp_solids: vmf.create_ent( classname='func_brush', origin=track_start, spawnflags=1, # Ignore +USE solidity=1, # Not solid vrad_brush_cast_shadows=1, drawinfastreflection=1, ).solids = rail_temp_solids if teleport_to_start: # Link back to the first track.. last_track['target'] = track_name.format(1) + '-track' # Generate an env_beam pointing from the start to the end of the track. try: beam_keys = res.find_key('BeamKeys') except LookupError: pass else: beam = vmf.create_ent(classname='env_beam') beam_off = beam_keys.vec('origin', 0, 63, 56) for prop in beam_keys: beam[prop.real_name] = prop.value # Localise the targetname so it can be triggered.. beam['LightningStart'] = beam['targetname'] = conditions.local_name( inst, beam['targetname', 'beam']) del beam['LightningEnd'] beam['origin'] = start_pos + Vec( -beam_off.x, beam_off.y, beam_off.z, ) @ orient beam['TargetPoint'] = end_pos + Vec( +beam_off.x, beam_off.y, beam_off.z, ) @ orient # Allow adding outputs to the last path_track. for prop in res.find_all('EndOutput'): output = Output.parse(prop) output.output = 'OnPass' output.inst_out = None output.comma_sep = False output.target = conditions.local_name(inst, output.target) last_track.add_out(output) if motion_filter is not None: motion_trig = vmf.create_ent( classname='trigger_multiple', targetname=conditions.local_name(inst, 'enable_motion_trig'), origin=start_pos, filtername=motion_filter, startDisabled=1, wait=0.1, ) motion_trig.add_out( Output('OnStartTouch', '!activator', 'ExitDisabledState')) # Match the size of the original... motion_trig.solids.append( vmf.make_prism( start_pos + Vec(72, -56, 58) @ orient, end_pos + Vec(-72, 56, 144) @ orient, mat=consts.Tools.TRIGGER, ).solid) if res.bool('NoPortalFloor'): # Block portals on the floor.. floor_noportal = vmf.create_ent( classname='func_noportal_volume', origin=track_start, ) floor_noportal.solids.append( vmf.make_prism( start_pos + Vec(-60, -60, -66) @ orient, end_pos + Vec(60, 60, -60) @ orient, mat=consts.Tools.INVISIBLE, ).solid) # A brush covering under the platform. base_trig = vmf.make_prism( start_pos + Vec(-64, -64, 48) @ orient, end_pos + Vec(64, 64, 56) @ orient, mat=consts.Tools.INVISIBLE, ).solid vmf.add_brush(base_trig) # Make a paint_cleanser under the belt.. if res.bool('PaintFizzler'): pfizz = vmf.create_ent( classname='trigger_paint_cleanser', origin=start_pos, ) pfizz.solids.append(base_trig.copy()) for face in pfizz.sides(): face.mat = consts.Tools.TRIGGER
def res_resizeable_trigger(vmf: VMF, res: Property): """Replace two markers with a trigger brush. This is run once to affect all of an item. Options: * `markerInst`: <ITEM_ID:1,2> value referencing the marker instances, or a filename. * `markerItem`: The item's ID * `previewConf`: A item config which enables/disables the preview overlay. * `previewInst`: An instance to place at the marker location in preview mode. This should contain checkmarks to display the value when testing. * `previewMat`: If set, the material to use for an overlay func_brush. The brush will be parented to the trigger, so it vanishes once killed. It is also non-solid. * `previewScale`: The scale for the func_brush materials. * `previewActivate`, `previewDeactivate`: The VMF output to turn the previewInst on and off. * `triggerActivate, triggerDeactivate`: The `instance:name;Output` outputs used when the trigger turns on or off. * `coopVar`: The instance variable which enables detecting both Coop players. The trigger will be a trigger_playerteam. * `coopActivate, coopDeactivate`: The `instance:name;Output` outputs used when coopVar is enabled. These should be suitable for a logic_coop_manager. * `coopOnce`: If true, kill the manager after it first activates. * `keys`: A block of keyvalues for the trigger brush. Origin and targetname will be set automatically. * `localkeys`: The same as above, except values will be changed to use instance-local names. """ marker = instanceLocs.resolve(res['markerInst']) marker_names = set() inst = None for inst in vmf.by_class['func_instance']: if inst['file'].casefold() in marker: marker_names.add(inst['targetname']) # Unconditionally delete from the map, so it doesn't # appear even if placed wrongly. inst.remove() del inst # Make sure we don't use this later. if not marker_names: # No markers in the map - abort return RES_EXHAUSTED item_id = res['markerItem'] # Synthesise the connection config used for the final trigger. conn_conf_sp = connections.Config( id=item_id + ':TRIGGER', output_act=Output.parse_name(res['triggerActivate', 'OnStartTouchAll']), output_deact=Output.parse_name(res['triggerDeactivate', 'OnEndTouchAll']), ) # For Coop, we add a logic_coop_manager in the mix so both players can # be handled. try: coop_var = res['coopVar'] except LookupError: coop_var = conn_conf_coop = None coop_only_once = False else: coop_only_once = res.bool('coopOnce') conn_conf_coop = connections.Config( id=item_id + ':TRIGGER', output_act=Output.parse_name(res['coopActivate', 'OnChangeToAllTrue']), output_deact=Output.parse_name(res['coopDeactivate', 'OnChangeToAnyFalse']), ) # Display preview overlays if it's preview mode, and the config is true pre_act = pre_deact = None if vbsp.IS_PREVIEW and options.get_itemconf(res['previewConf', ''], False): preview_mat = res['previewMat', ''] preview_inst_file = res['previewInst', ''] preview_scale = res.float('previewScale', 0.25) # None if not found. with suppress(LookupError): pre_act = Output.parse(res.find_key('previewActivate')) with suppress(LookupError): pre_deact = Output.parse(res.find_key('previewDeactivate')) else: # Deactivate the preview_ options when publishing. preview_mat = preview_inst_file = '' preview_scale = 0.25 # Now go through each brush. # We do while + pop to allow removing both names each loop through. todo_names = set(marker_names) while todo_names: targ = todo_names.pop() mark1 = connections.ITEMS.pop(targ) for conn in mark1.outputs: if conn.to_item.name in marker_names: mark2 = conn.to_item conn.remove() # Delete this connection. todo_names.discard(mark2.name) del connections.ITEMS[mark2.name] break else: if not mark1.inputs: # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 1-block trigger. mark2 = mark1 else: # It's a marker with an input, the other in the pair # will handle everything. # But reinstate it in ITEMS. connections.ITEMS[targ] = mark1 continue inst1 = mark1.inst inst2 = mark2.inst is_coop = coop_var is not None and vbsp.GAME_MODE == 'COOP' and ( inst1.fixup.bool(coop_var) or inst2.fixup.bool(coop_var)) bbox_min, bbox_max = Vec.bbox(Vec.from_str(inst1['origin']), Vec.from_str(inst2['origin'])) origin = (bbox_max + bbox_min) / 2 # Extend to the edge of the blocks. bbox_min -= 64 bbox_max += 64 out_ent = trig_ent = vmf.create_ent( classname='trigger_multiple', # Default targetname=targ, origin=options.get(Vec, "global_ents_loc"), angles='0 0 0', ) trig_ent.solids = [ vmf.make_prism( bbox_min, bbox_max, mat=consts.Tools.TRIGGER, ).solid, ] # Use 'keys' and 'localkeys' blocks to set all the other keyvalues. conditions.set_ent_keys(trig_ent, inst1, res) if is_coop: trig_ent['spawnflags'] = '1' # Clients trig_ent['classname'] = 'trigger_playerteam' out_ent = manager = vmf.create_ent( classname='logic_coop_manager', targetname=conditions.local_name(inst1, 'man'), origin=origin, ) item = connections.Item( out_ent, conn_conf_coop, ant_floor_style=mark1.ant_floor_style, ant_wall_style=mark1.ant_wall_style, ) if coop_only_once: # Kill all the ents when both players are present. manager.add_out( Output('OnChangeToAllTrue', manager, 'Kill'), Output('OnChangeToAllTrue', targ, 'Kill'), ) trig_ent.add_out( Output('OnStartTouchBluePlayer', manager, 'SetStateATrue'), Output('OnStartTouchOrangePlayer', manager, 'SetStateBTrue'), Output('OnEndTouchBluePlayer', manager, 'SetStateAFalse'), Output('OnEndTouchOrangePlayer', manager, 'SetStateBFalse'), ) else: item = connections.Item( trig_ent, conn_conf_sp, ant_floor_style=mark1.ant_floor_style, ant_wall_style=mark1.ant_wall_style, ) # Register, and copy over all the antlines. connections.ITEMS[item.name] = item item.ind_panels = mark1.ind_panels | mark2.ind_panels item.antlines = mark1.antlines | mark2.antlines item.shape_signs = mark1.shape_signs + mark2.shape_signs if preview_mat: preview_brush = vmf.create_ent( classname='func_brush', parentname=targ, origin=origin, Solidity='1', # Not solid drawinfastreflection='1', # Draw in goo.. # Disable shadows and lighting.. disableflashlight='1', disablereceiveshadows='1', disableshadowdepth='1', disableshadows='1', ) preview_brush.solids = [ # Make it slightly smaller, so it doesn't z-fight with surfaces. vmf.make_prism( bbox_min + 0.5, bbox_max - 0.5, mat=preview_mat, ).solid, ] for face in preview_brush.sides(): face.scale = preview_scale if preview_inst_file: pre_inst = conditions.add_inst( vmf, targetname=targ + '_preview', file=preview_inst_file, # Put it at the second marker, since that's usually # closest to antlines if present. origin=inst2['origin'], ) if pre_act is not None: out = pre_act.copy() out.inst_out, out.output = item.output_act() out.target = conditions.local_name(pre_inst, out.target) out_ent.add_out(out) if pre_deact is not None: out = pre_deact.copy() out.inst_out, out.output = item.output_deact() out.target = conditions.local_name(pre_inst, out.target) out_ent.add_out(out) for conn in mark1.outputs | mark2.outputs: conn.from_item = item return RES_EXHAUSTED