def res_replace_instance(vmf: VMF, inst: Entity, res: Property): """Replace an instance with another entity. `keys` and `localkeys` defines the new keyvalues used. `targetname` and `angles` are preset, and `origin` will be used to offset the given amount from the current location. If `keep_instance` is true, the instance entity will be kept instead of removed. """ origin = Vec.from_str(inst['origin']) angles = Angle.from_str(inst['angles']) if not res.bool('keep_instance'): inst.remove() # Do this first to free the ent ID, so the new ent has # the same one. # We copy to allow us to still access the $fixups and other values. new_ent = inst.copy(des_id=inst.id) new_ent.clear_keys() # Ensure there's a classname, just in case. new_ent['classname'] = 'info_null' vmf.add_ent(new_ent) conditions.set_ent_keys(new_ent, inst, res) new_ent['origin'] = Vec.from_str(new_ent['origin']) @ angles + origin new_ent['angles'] = angles new_ent['targetname'] = inst['targetname']
def make_static_pist(vmf: srctools.VMF, ent: Entity, res: Property): """Convert a regular piston into a static version. This is done to save entities and improve lighting. If changed to static pistons, the $bottom and $top level become equal. Instances: Bottom_1/2/3: Moving piston with the given $bottom_level Logic_0/1/2/3: Additional logic instance for the given $bottom_level Static_0/1/2/3/4: A static piston at the given height. Alternatively, specify all instances via editoritems, by setting the value to the item ID optionally followed by a :prefix. """ bottom_pos = ent.fixup.int(consts.FixupVars.PIST_BTM, 0) if (ent.fixup.int(consts.FixupVars.CONN_COUNT) > 0 or ent.fixup.bool(consts.FixupVars.DIS_AUTO_DROP)): # can it move? ent.fixup[consts.FixupVars.BEE_PIST_IS_STATIC] = True # Use instances based on the height of the bottom position. val = res.value['bottom_' + str(bottom_pos)] if val: # Only if defined ent['file'] = val logic_file = res.value['logic_' + str(bottom_pos)] if logic_file: # Overlay an additional logic file on top of the original # piston. This allows easily splitting the piston logic # from the styled components logic_ent = ent.copy() logic_ent['file'] = logic_file vmf.add_ent(logic_ent) # If no connections are present, set the 'enable' value in # the logic to True so the piston can function logic_ent.fixup[consts.FixupVars.BEE_PIST_MANAGER_A] = ( ent.fixup.int(consts.FixupVars.CONN_COUNT) == 0) else: # we are static ent.fixup[consts.FixupVars.BEE_PIST_IS_STATIC] = False if ent.fixup.bool(consts.FixupVars.PIST_IS_UP): pos = bottom_pos = ent.fixup.int(consts.FixupVars.PIST_TOP, 1) else: pos = bottom_pos ent.fixup[consts.FixupVars.PIST_TOP] = ent.fixup[ consts.FixupVars.PIST_BTM] = pos val = res.value['static_' + str(pos)] if val: ent['file'] = val # Add in the grating for the bottom as an overlay. # It's low to fit the piston at minimum, or higher if needed. grate = res.value['grate_high' if bottom_pos > 0 else 'grate_low'] if grate: grate_ent = ent.copy() grate_ent['file'] = grate vmf.add_ent(grate_ent)
def make_static_pist(vmf: srctools.VMF, ent: Entity, res: Property): """Convert a regular piston into a static version. This is done to save entities and improve lighting. If changed to static pistons, the $bottom and $top level become equal. Instances: Bottom_1/2/3: Moving piston with the given $bottom_level Logic_0/1/2/3: Additional logic instance for the given $bottom_level Static_0/1/2/3/4: A static piston at the given height. Alternatively, specify all instances via editoritems, by setting the value to the item ID optionally followed by a :prefix. """ bottom_pos = ent.fixup.int('bottom_level', 0) if (ent.fixup['connectioncount', '0'] != "0" or ent.fixup['disable_autodrop', '0'] != "0"): # can it move? ent.fixup['$is_static'] = True # Use instances based on the height of the bottom position. val = res.value['bottom_' + str(bottom_pos)] if val: # Only if defined ent['file'] = val logic_file = res.value['logic_' + str(bottom_pos)] if logic_file: # Overlay an additional logic file on top of the original # piston. This allows easily splitting the piston logic # from the styled components logic_ent = ent.copy() logic_ent['file'] = logic_file vmf.add_ent(logic_ent) # If no connections are present, set the 'enable' value in # the logic to True so the piston can function logic_ent.fixup['manager_a'] = srctools.bool_as_int( ent.fixup['connectioncount', '0'] == '0') else: # we are static ent.fixup['$is_static'] = False if ent.fixup.bool('start_up'): pos = bottom_pos = ent.fixup.int('top_level', 1) else: pos = bottom_pos ent.fixup['top_level'] = ent.fixup['bottom_level'] = pos val = res.value['static_' + str(pos)] if val: ent['file'] = val # Add in the grating for the bottom as an overlay. # It's low to fit the piston at minimum, or higher if needed. grate = res.value['grate_high' if bottom_pos > 0 else 'grate_low'] if grate: grate_ent = ent.copy() grate_ent['file'] = grate vmf.add_ent(grate_ent)
def vactube_gen(vmf: VMF) -> None: """Generate the vactubes, after most conditions have run.""" if not VAC_TRACKS: return LOGGER.info('Generating vactubes...') for start, all_markers in VAC_TRACKS: start_normal = start.orient.forward() # First create the start section.. start_logic = start.ent.copy() vmf.add_ent(start_logic) if start_normal.z > 0: start_logic['file'] = fname = start.conf.inst_entry_ceil elif start_normal.z < 0: start_logic['file'] = fname = start.conf.inst_entry_floor else: start_logic['file'] = fname = start.conf.inst_entry_wall conditions.ALL_INST.add(fname.casefold()) end = start for inst, end in start.follow_path(all_markers): join_markers(vmf, inst, end, inst is start) end_loc = Vec.from_str(end.ent['origin']) end_norm = end.orient.forward() # join_markers creates straight parts up-to the marker, but not at it's # location - create the last one. make_straight( vmf, 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 not (BLOCK_POS['world':end_loc].is_goo and end_norm.z < -1e-6): end_logic = end.ent.copy() vmf.add_ent(end_logic) end_logic['file'] = end.conf.inst_exit conditions.ALL_INST.add(end.conf.inst_exit.casefold())
def ap_tag_modifications(vmf: VMF): """Perform modifications for Aperture Tag. * All fizzlers will be combined with a trigger_paint_cleanser * Paint is always present in every map! * Suppress ATLAS's Portalgun in coop * Override the transition ent instance to have the Gel Gun * Create subdirectories with the user's steam ID """ if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['APTAG']: return # Wrong game! LOGGER.info('Performing Aperture Tag modifications...') has = vbsp.settings['has_attr'] # This will enable the PaintInMap property. has['Gel'] = True # Set as if the player spawned with no pgun has['spawn_dual'] = False has['spawn_single'] = False has['spawn_nogun'] = True # Add paint fizzlers to all normal fizzlers for fizz in vmf.by_class['trigger_portal_cleanser']: p_fizz = fizz.copy() p_fizz['classname'] = 'trigger_paint_cleanser' vmf.add_ent(p_fizz) if p_fizz['targetname'].endswith('_brush'): p_fizz['targetname'] = p_fizz['targetname'][:-6] + '-br_fizz' del p_fizz['drawinfastreflection'] del p_fizz['visible'] del p_fizz['useScanline'] for side in p_fizz.sides(): side.mat = 'tools/toolstrigger' side.scale = 0.25 transition_ents = get_special_inst('transitionents') for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in transition_ents: continue inst['file'] = 'instances/bee2/transition_ents_tag.vmf' # Because of a bug in P2, these folders aren't created automatically. # We need a folder with the user's ID in portal2/maps/puzzlemaker. try: puzz_folders = os.listdir('../aperturetag/puzzles') except FileNotFoundError: LOGGER.warning("Aperturetag/puzzles/ doesn't exist??") else: for puzz_folder in puzz_folders: new_folder = os.path.abspath(os.path.join( '../portal2/maps/puzzlemaker', puzz_folder, )) LOGGER.info('Creating', new_folder) os.makedirs( new_folder, exist_ok=True, )
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) 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'] = 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, generator=GenCat.PANEL, ) # Associate any set panel with the same entity, if it's present. tile_pos = Vec(z=-128) tile_pos.localise(origin, angles) 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'] = local_name(inst, source_ent)
def res_piston_plat(vmf: VMF, inst: Entity, res: Property): """Generates piston platforms with optimized logic.""" ( template, visgroup_names, inst_filenames, automatic_var, color_var, source_ent, snd_start, snd_loop, snd_stop, ) = res.value # type: template_brush.Template, List[str], Dict[str, str], str, str, str, str, str, str min_pos = inst.fixup.int(FixupVars.PIST_BTM) max_pos = inst.fixup.int(FixupVars.PIST_TOP) start_up = inst.fixup.bool(FixupVars.PIST_IS_UP) # Allow doing variable lookups here. visgroup_names = [ conditions.resolve_value(inst, name) for name in visgroup_names ] if len(ITEMS[inst['targetname']].inputs) == 0: # No inputs. Check for the 'auto' var if applicable. if automatic_var and inst.fixup.bool(automatic_var): pass # The item is automatically moving, so we generate the dynamics. else: # It's static, we just make that and exit. position = max_pos if start_up else min_pos inst.fixup[FixupVars.PIST_BTM] = position inst.fixup[FixupVars.PIST_TOP] = position static_inst = inst.copy() vmf.add_ent(static_inst) static_inst['file'] = inst_filenames['fullstatic_' + str(position)] return init_script = 'SPAWN_UP <- {}'.format('true' if start_up else 'false') if snd_start and snd_stop: packing.pack_files(vmf, snd_start, snd_stop, file_type='sound') init_script += '; START_SND <- `{}`; STOP_SND <- `{}`'.format( snd_start, snd_stop) elif snd_start: packing.pack_files(vmf, snd_start, file_type='sound') init_script += '; START_SND <- `{}`'.format(snd_start) elif snd_stop: packing.pack_files(vmf, snd_stop, file_type='sound') init_script += '; STOP_SND <- `{}`'.format(snd_stop) script_ent = vmf.create_ent( classname='info_target', targetname=local_name(inst, 'script'), vscripts='BEE2/piston/common.nut', vscript_init_code=init_script, origin=inst['origin'], ) if start_up: st_pos, end_pos = max_pos, min_pos else: st_pos, end_pos = min_pos, max_pos script_ent.add_out( Output('OnUser1', '!self', 'RunScriptCode', 'moveto({})'.format(st_pos)), Output('OnUser2', '!self', 'RunScriptCode', 'moveto({})'.format(end_pos)), ) origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles']) off = Vec(z=128).rotate(*angles) move_ang = off.to_angle() # Index -> func_movelinear. pistons = {} # type: Dict[int, Entity] static_ent = vmf.create_ent('func_brush', origin=origin) color_var = conditions.resolve_value(inst, color_var).casefold() if color_var == 'white': top_color = template_brush.MAT_TYPES.white elif color_var == 'black': top_color = template_brush.MAT_TYPES.black else: top_color = None for pist_ind in range(1, 5): pist_ent = inst.copy() vmf.add_ent(pist_ent) if pist_ind <= min_pos: # It's below the lowest position, so it can be static. pist_ent['file'] = inst_filenames['static_' + str(pist_ind)] pist_ent['origin'] = brush_pos = origin + pist_ind * off temp_targ = static_ent else: # It's a moving component. pist_ent['file'] = inst_filenames['dynamic_' + str(pist_ind)] if pist_ind > max_pos: # It's 'after' the highest position, so it never extends. # So simplify by merging those all. # That's before this so it'll have to exist. temp_targ = pistons[max_pos] if start_up: pist_ent['origin'] = brush_pos = origin + max_pos * off else: pist_ent['origin'] = brush_pos = origin + min_pos * off pist_ent.fixup['$parent'] = 'pist' + str(max_pos) else: # It's actually a moving piston. if start_up: brush_pos = origin + pist_ind * off else: brush_pos = origin + min_pos * off pist_ent['origin'] = brush_pos pist_ent.fixup['$parent'] = 'pist' + str(pist_ind) pistons[pist_ind] = temp_targ = vmf.create_ent( 'func_movelinear', targetname=local_name(pist_ent, 'pist' + str(pist_ind)), origin=brush_pos - off, movedir=move_ang, startposition=start_up, movedistance=128, speed=150, ) if pist_ind - 1 in pistons: pistons[pist_ind]['parentname'] = local_name( pist_ent, 'pist' + str(pist_ind - 1), ) if not pist_ent['file']: # No actual instance, remove. pist_ent.remove() temp_result = template_brush.import_template( template, brush_pos, angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, additional_visgroups={visgroup_names[pist_ind - 1]}, ) temp_targ.solids.extend(temp_result.world) template_brush.retexture_template( temp_result, origin, pist_ent.fixup, force_colour=top_color, force_grid='special', no_clumping=True, ) if not static_ent.solids: static_ent.remove() if snd_loop: script_ent['classname'] = 'ambient_generic' script_ent['message'] = snd_loop script_ent['health'] = 10 # Volume script_ent['pitch'] = '100' script_ent['spawnflags'] = 16 # Start silent, looped. script_ent['radius'] = 1024 if source_ent: # Parent is irrelevant for actual entity locations, but it # survives for the script to read. script_ent['SourceEntityName'] = script_ent[ 'parentname'] = local_name(inst, source_ent)
def calc_connections( vmf: VMF, antlines: Dict[str, List[Antline]], shape_frame_tex: List[str], enable_shape_frame: bool, *, # Don't mix up antlines! antline_wall: AntType, antline_floor: AntType, ) -> None: """Compute item connections from the map file. This also fixes cases where items have incorrect checkmark/timer signs. Instance Traits must have been calculated. It also applies frames to shape signage to distinguish repeats. """ # First we want to match targetnames to item types. toggles = {} # type: Dict[str, Entity] # Accumulate all the signs into groups, so the list should be 2-long: # sign_shapes[name, material][0/1] sign_shape_overlays = defaultdict( list) # type: Dict[Tuple[str, str], List[Entity]] # Indicator panels panels = {} # type: Dict[str, Entity] # We only need to pay attention for TBeams, other items we can # just detect any output. tbeam_polarity = {OutNames.IN_SEC_ACT, OutNames.IN_SEC_DEACT} # Also applies to other items, but not needed for this analysis. tbeam_io = {OutNames.IN_ACT, OutNames.IN_DEACT} for inst in vmf.by_class['func_instance']: inst_name = inst['targetname'] # No connections, so nothing to worry about. if not inst_name: continue traits = instance_traits.get(inst) if 'indicator_toggle' in traits: toggles[inst_name] = inst # We do not use toggle instances. inst.remove() elif 'indicator_panel' in traits: panels[inst_name] = inst elif 'fizzler_model' in traits: # Ignore fizzler models - they shouldn't have the connections. # Just the base itself. pass else: # Normal item. item_id = instance_traits.get_item_id(inst) if item_id is None: LOGGER.warning('No item ID for "{}"!', inst) continue try: item_type = ITEM_TYPES[item_id.casefold()] except KeyError: LOGGER.warning('No item type for "{}"!', item_id) continue if item_type is None: # It exists, but has no I/O. continue # Pass in the defaults for antline styles. ITEMS[inst_name] = Item( inst, item_type, ant_floor_style=antline_floor, ant_wall_style=antline_wall, ) # Strip off the original connection count variables, these are # invalid. if item_type.input_type is InputType.DUAL: del inst.fixup[consts.FixupVars.CONN_COUNT] del inst.fixup[consts.FixupVars.CONN_COUNT_TBEAM] for over in vmf.by_class['info_overlay']: name = over['targetname'] mat = over['material'] if mat in SIGN_ORDER_LOOKUP: sign_shape_overlays[name, mat.casefold()].append(over) # Name -> signs pairs sign_shapes = defaultdict(list) # type: Dict[str, List[ShapeSignage]] # By material index, for group frames. sign_shape_by_index = defaultdict( list) # type: Dict[int, List[ShapeSignage]] for (name, mat), sign_pair in sign_shape_overlays.items(): # It's possible - but rare - for more than 2 to be in a pair. # We have to just treat them as all in their 'pair'. # Shouldn't be an issue, it'll be both from one item... shape = ShapeSignage(sign_pair) sign_shapes[name].append(shape) sign_shape_by_index[shape.index].append(shape) # Now build the connections and items. for item in ITEMS.values(): input_items: List[Item] = [] # Instances we trigger inputs: Dict[str, List[Output]] = defaultdict(list) if item.inst.outputs and item.config is None: raise ValueError('No connections for item "{}", ' 'but outputs in the map!'.format( instance_traits.get_item_id(item.inst))) for out in item.inst.outputs: inputs[out.target].append(out) # Remove the original outputs, we've consumed those already. item.inst.outputs.clear() # Pre-set the timer value, for items without antlines but with an output. if consts.FixupVars.TIM_DELAY in item.inst.fixup: if item.config.output_act or item.config.output_deact: item.timer = tim = item.inst.fixup.int( consts.FixupVars.TIM_DELAY) if not (1 <= tim <= 30): # These would be infinite. item.timer = None for out_name in inputs: # Fizzler base -> model/brush outputs, ignore these (discard). # fizzler.py will regenerate as needed. if out_name.rstrip('0123456789').endswith( ('_modelStart', '_modelEnd', '_brush')): continue if out_name in toggles: inst_toggle = toggles[out_name] try: item.antlines.update( antlines[inst_toggle.fixup['indicator_name']]) except KeyError: pass elif out_name in panels: pan = panels[out_name] item.ind_panels.add(pan) if pan.fixup.bool(consts.FixupVars.TIM_ENABLED): item.timer = tim = pan.fixup.int( consts.FixupVars.TIM_DELAY) if not (1 <= tim <= 30): # These would be infinite. item.timer = None else: item.timer = None else: try: inp_item = ITEMS[out_name] except KeyError: raise ValueError( '"{}" is not a known instance!'.format(out_name)) else: input_items.append(inp_item) if inp_item.config is None: raise ValueError('No connections for item "{}", ' 'but inputs in the map!'.format( instance_traits.get_item_id( inp_item.inst))) for inp_item in input_items: # Default A/B type. conn_type = ConnType.DEFAULT in_outputs = inputs[inp_item.name] if inp_item.config.id == 'ITEM_TBEAM': # It's a funnel - we need to figure out if this is polarity, # or normal on/off. for out in in_outputs: if out.input in tbeam_polarity: conn_type = ConnType.TBEAM_DIR break elif out.input in tbeam_io: conn_type = ConnType.TBEAM_IO break else: raise ValueError('Excursion Funnel "{}" has inputs, ' 'but no valid types!'.format( inp_item.name)) conn = Connection( inp_item, item, conn_type, in_outputs, ) conn.add() # Make signage frames shape_frame_tex = [mat for mat in shape_frame_tex if mat] if shape_frame_tex and enable_shape_frame: for shape_mat in sign_shape_by_index.values(): # Sort so which gets what frame is consistent. shape_mat.sort() for index, shape in enumerate(shape_mat): shape.repeat_group = index if index == 0: continue # First, no frames.. frame_mat = shape_frame_tex[(index - 1) % len(shape_frame_tex)] for overlay in shape: frame = overlay.copy() shape.overlay_frames.append(frame) vmf.add_ent(frame) frame['material'] = frame_mat frame['renderorder'] = 1 # On top
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 gen_flinch_trigs(self, vmf: VMF, name: str, start_disabled: str) -> None: """For deadly fizzlers optionally make them safer. This adds logic to force players back instead when walking into the field. Only applies to vertical triggers. """ normal = abs(self.normal()) # type: Vec # Horizontal fizzlers would just have you fall through. if normal.z: return # Disabled. if not vbsp_options.get_itemconf( ('VALVE_FIZZLER', 'FlinchBack'), False): return # Make global entities if not present. if '_fizz_flinch_hurt' not in vmf.by_target: glob_ent_loc = vbsp_options.get(Vec, 'global_ents_loc') vmf.create_ent( classname='point_hurt', targetname='_fizz_flinch_hurt', Damage=10, # Just for visuals and sounds. # BURN | ENERGYBEAM | PREVENT_PHYSICS_FORCE DamageType=8 | 1024 | 2048, DamageTarget='!activator', # Hurt the triggering player. DamageRadius=1, # Target makes this unused. origin=glob_ent_loc, ) # We need two catapults - one for each side. neg_brush = vmf.create_ent( targetname=name, classname='trigger_catapult', spawnflags=1, # Players only. origin=self.base_inst['origin'], physicsSpeed=0, playerSpeed=96, launchDirection=(-normal).to_angle(), startDisabled=start_disabled, ) neg_brush.add_out(Output('OnCatapulted', '_fizz_flinch_hurt', 'Hurt')) pos_brush = neg_brush.copy() pos_brush['launchDirection'] = normal.to_angle() vmf.add_ent(pos_brush) for seg_min, seg_max in self.emitters: neg_brush.solids.append( vmf.make_prism( p1=(seg_min - 4 * normal - 64 * self.up_axis), p2=seg_max + 64 * self.up_axis, mat=const.Tools.TRIGGER, ).solid) pos_brush.solids.append( vmf.make_prism( p1=seg_min - 64 * self.up_axis, p2=(seg_max + 4 * normal + 64 * self.up_axis), mat=const.Tools.TRIGGER, ).solid)
def calc_connections( vmf: VMF, shape_frame_tex: List[str], enable_shape_frame: bool, ): """Compute item connections from the map file. This also fixes cases where items have incorrect checkmark/timer signs. Instance Traits must have been calculated. It also applies frames to shape signage to distinguish repeats. """ # First we want to match targetnames to item types. toggles = {} # type: Dict[str, Entity] overlays = defaultdict(set) # type: Dict[str, Set[Entity]] # Accumulate all the signs into groups, so the list should be 2-long: # sign_shapes[name, material][0/1] sign_shape_overlays = defaultdict(list) # type: Dict[Tuple[str, str], List[Entity]] panels = {} # type: Dict[str, Entity] panel_timer = instanceLocs.resolve_one('[indPanTimer]', error=True) panel_check = instanceLocs.resolve_one('[indPanCheck]', error=True) tbeam_polarity = { conditions.TBEAM_CONN_ACT, conditions.TBEAM_CONN_DEACT, } tbeam_io = conditions.CONNECTIONS['item_tbeam'] tbeam_io = {tbeam_io.in_act, tbeam_io.in_deact} for inst in vmf.by_class['func_instance']: inst_name = inst['targetname'] if not inst_name: continue traits = instance_traits.get(inst) if 'indicator_toggle' in traits: toggles[inst['targetname']] = inst elif 'indicator_panel' in traits: panels[inst['targetname']] = inst else: ITEMS[inst_name] = Item(inst) for over in vmf.by_class['info_overlay']: name = over['targetname'] mat = over['material'] if mat in SIGN_ORDER_LOOKUP: sign_shape_overlays[name, mat.casefold()].append(over) else: # Antlines overlays[name].add(over) # Name -> signs pairs sign_shapes = defaultdict(list) # type: Dict[str, List[ShapeSignage]] # By material index, for group frames. sign_shape_by_index = defaultdict(list) # type: Dict[int, List[ShapeSignage]] for (name, mat), sign_pair in sign_shape_overlays.items(): # It's possible - but rare - for more than 2 to be in a pair. # We have to just treat them as all in their 'pair'. # Shouldn't be an issue, it'll be both from one item... shape = ShapeSignage(sign_pair) sign_shapes[name].append(shape) sign_shape_by_index[shape.index].append(shape) # Now build the connections and items. for item in ITEMS.values(): input_items = [] # Instances we trigger inputs = defaultdict(list) # type: Dict[str, List[Output]] for out in item.inst.outputs: inputs[out.target].append(out) # inst.outputs.clear() for out_name in inputs: # Fizzler base -> model/brush outputs, skip and readd. if out_name.endswith(('_modelStart', '_modelEnd', '_brush')): # item.inst.add_out(*inputs[out_name]) continue if out_name in toggles: inst_toggle = toggles[out_name] item.antlines |= overlays[inst_toggle.fixup['indicator_name']] elif out_name in panels: pan = panels[out_name] item.ind_panels.add(pan) if pan.fixup.bool('$is_timer'): item.timer = tim = pan.fixup.int('$timer_delay') if not (1 <= tim <= 30): # These would be infinite. item.timer = None else: item.timer = None else: try: input_items.append(ITEMS[out_name]) except KeyError: raise ValueError('"{}" is not a known instance!'.format(out_name)) desired_panel_inst = panel_check if item.timer is None else panel_timer # Check/cross instances sometimes don't match the kind of timer delay. for pan in item.ind_panels: pan['file'] = desired_panel_inst pan.fixup['$is_timer'] = int(item.timer is not None) for inp_item in input_items: # type: Item # Default A/B type. conn_type = ConnType.DEFAULT in_outputs = inputs[inp_item.name] if 'tbeam_emitter' in inp_item.traits: # It's a funnel - we need to figure out if this is polarity, # or normal on/off. for out in in_outputs: # type: Output input_tuple = (out.inst_in, out.input) if input_tuple in tbeam_polarity: conn_type = ConnType.TBEAM_DIR break elif input_tuple in tbeam_io: conn_type = ConnType.TBEAM_IO break else: raise ValueError( 'Excursion Funnel "{}" has inputs, ' 'but no valid types!'.format(inp_item.name) ) conn = Connection( inp_item, item, conn_type, in_outputs, ) conn.add() for item in ITEMS.values(): # Copying items can fail to update the connection counts. # Make sure they're correct. if '$connectioncount' in item.inst.fixup: # Don't count the polarity outputs... item.inst.fixup['$connectioncount'] = sum( 1 for conn in item.inputs if conn.type is not ConnType.TBEAM_DIR ) if '$connectioncount_polarity' in item.inst.fixup: # Only count the polarity outputs... item.inst.fixup['$connectioncount_polarity'] = sum( 1 for conn in item.inputs if conn.type is ConnType.TBEAM_DIR ) # Make signage frames shape_frame_tex = [mat for mat in shape_frame_tex if mat] if shape_frame_tex and enable_shape_frame: for shape_mat in sign_shape_by_index.values(): # Sort by name, so which gets what frame is consistent shape_mat.sort(key=lambda shape: shape.name) for index, shape in enumerate(shape_mat): shape.repeat_group = index if index == 0: continue # First, no frames.. frame_mat = shape_frame_tex[(index-1) % len(shape_frame_tex)] for overlay in shape: frame = overlay.copy() shape.overlay_frames.append(frame) vmf.add_ent(frame) frame['material'] = frame_mat frame['renderorder'] = 1 # On top
def ap_tag_modifications(vmf: VMF): """Perform modifications for Aperture Tag. * All fizzlers will be combined with a trigger_paint_cleanser * Paint is always present in every map! * Suppress ATLAS's Portalgun in coop * Override the transition ent instance to have the Gel Gun * Create subdirectories with the user's steam ID """ if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['APTAG']: return # Wrong game! LOGGER.info('Performing Aperture Tag modifications...') has = vbsp.settings['has_attr'] # This will enable the PaintInMap property. has['Gel'] = True # Set as if the player spawned with no pgun has['spawn_dual'] = False has['spawn_single'] = False has['spawn_nogun'] = True # Add paint fizzlers to all normal fizzlers for fizz in vmf.by_class['trigger_portal_cleanser']: p_fizz = fizz.copy() p_fizz['classname'] = 'trigger_paint_cleanser' vmf.add_ent(p_fizz) if p_fizz['targetname'].endswith('_brush'): p_fizz['targetname'] = p_fizz['targetname'][:-6] + '-br_fizz' del p_fizz['drawinfastreflection'] del p_fizz['visible'] del p_fizz['useScanline'] for side in p_fizz.sides(): side.mat = 'tools/toolstrigger' side.scale = 0.25 transition_ents = instanceLocs.get_special_inst('transitionents') for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in transition_ents: continue inst['file'] = 'instances/bee2/transition_ents_tag.vmf' # Because of a bug in P2, these folders aren't created automatically. # We need a folder with the user's ID in portal2/maps/puzzlemaker. try: puzz_folders = os.listdir('../aperturetag/puzzles') except FileNotFoundError: LOGGER.warning("Aperturetag/puzzles/ doesn't exist??") else: for puzz_folder in puzz_folders: new_folder = os.path.abspath( os.path.join( '../portal2/maps/puzzlemaker', puzz_folder, )) LOGGER.info('Creating', new_folder) os.makedirs( new_folder, exist_ok=True, )
def res_piston_plat(vmf: VMF, inst: Entity, res: Property): """Generates piston platforms with optimized logic.""" ( template, visgroup_names, inst_filenames, automatic_var, color_var, source_ent, snd_start, snd_loop, snd_stop, ) = res.value # type: template_brush.Template, List[str], Dict[str, str], str, str, str, str, str, str min_pos = inst.fixup.int(FixupVars.PIST_BTM) max_pos = inst.fixup.int(FixupVars.PIST_TOP) start_up = inst.fixup.bool(FixupVars.PIST_IS_UP) # Allow doing variable lookups here. visgroup_names = [ conditions.resolve_value(inst, name) for name in visgroup_names ] if len(ITEMS[inst['targetname']].inputs) == 0: # No inputs. Check for the 'auto' var if applicable. if automatic_var and inst.fixup.bool(automatic_var): pass # The item is automatically moving, so we generate the dynamics. else: # It's static, we just make that and exit. position = max_pos if start_up else min_pos inst.fixup[FixupVars.PIST_BTM] = position inst.fixup[FixupVars.PIST_TOP] = position static_inst = inst.copy() vmf.add_ent(static_inst) static_inst['file'] = inst_filenames['fullstatic_' + str(position)] return init_script = 'SPAWN_UP <- {}'.format('true' if start_up else 'false') if snd_start and snd_stop: packing.pack_files(vmf, snd_start, snd_stop, file_type='sound') init_script += '; START_SND <- `{}`; STOP_SND <- `{}`'.format(snd_start, snd_stop) elif snd_start: packing.pack_files(vmf, snd_start, file_type='sound') init_script += '; START_SND <- `{}`'.format(snd_start) elif snd_stop: packing.pack_files(vmf, snd_stop, file_type='sound') init_script += '; STOP_SND <- `{}`'.format(snd_stop) script_ent = vmf.create_ent( classname='info_target', targetname=local_name(inst, 'script'), vscripts='BEE2/piston/common.nut', vscript_init_code=init_script, origin=inst['origin'], ) if start_up: st_pos, end_pos = max_pos, min_pos else: st_pos, end_pos = min_pos, max_pos script_ent.add_out( Output('OnUser1', '!self', 'RunScriptCode', 'moveto({})'.format(st_pos)), Output('OnUser2', '!self', 'RunScriptCode', 'moveto({})'.format(end_pos)), ) origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles']) off = Vec(z=128).rotate(*angles) move_ang = off.to_angle() # Index -> func_movelinear. pistons = {} # type: Dict[int, Entity] static_ent = vmf.create_ent('func_brush', origin=origin) color_var = conditions.resolve_value(inst, color_var).casefold() if color_var == 'white': top_color = template_brush.MAT_TYPES.white elif color_var == 'black': top_color = template_brush.MAT_TYPES.black else: top_color = None for pist_ind in range(1, 5): pist_ent = inst.copy() vmf.add_ent(pist_ent) if pist_ind <= min_pos: # It's below the lowest position, so it can be static. pist_ent['file'] = inst_filenames['static_' + str(pist_ind)] pist_ent['origin'] = brush_pos = origin + pist_ind * off temp_targ = static_ent else: # It's a moving component. pist_ent['file'] = inst_filenames['dynamic_' + str(pist_ind)] if pist_ind > max_pos: # It's 'after' the highest position, so it never extends. # So simplify by merging those all. # That's before this so it'll have to exist. temp_targ = pistons[max_pos] if start_up: pist_ent['origin'] = brush_pos = origin + max_pos * off else: pist_ent['origin'] = brush_pos = origin + min_pos * off pist_ent.fixup['$parent'] = 'pist' + str(max_pos) else: # It's actually a moving piston. if start_up: brush_pos = origin + pist_ind * off else: brush_pos = origin + min_pos * off pist_ent['origin'] = brush_pos pist_ent.fixup['$parent'] = 'pist' + str(pist_ind) pistons[pist_ind] = temp_targ = vmf.create_ent( 'func_movelinear', targetname=local_name(pist_ent, 'pist' + str(pist_ind)), origin=brush_pos - off, movedir=move_ang, startposition=start_up, movedistance=128, speed=150, ) if pist_ind - 1 in pistons: pistons[pist_ind]['parentname'] = local_name( pist_ent, 'pist' + str(pist_ind - 1), ) if not pist_ent['file']: # No actual instance, remove. pist_ent.remove() temp_result = template_brush.import_template( template, brush_pos, angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, additional_visgroups={visgroup_names[pist_ind - 1]}, ) temp_targ.solids.extend(temp_result.world) template_brush.retexture_template( temp_result, origin, pist_ent.fixup, force_colour=top_color, force_grid='special', no_clumping=True, ) if not static_ent.solids: static_ent.remove() if snd_loop: script_ent['classname'] = 'ambient_generic' script_ent['message'] = snd_loop script_ent['health'] = 10 # Volume script_ent['pitch'] = '100' script_ent['spawnflags'] = 16 # Start silent, looped. script_ent['radius'] = 1024 if source_ent: # Parent is irrelevant for actual entity locations, but it # survives for the script to read. script_ent['SourceEntityName'] = script_ent['parentname'] = local_name(inst, source_ent)
def generate_fizzlers(vmf: VMF): """Generates fizzler models and the brushes according to their set types. After this is done, fizzler-related conditions will not function correctly. However the model instances are now available for modification. """ from vbsp import MAP_RAND_SEED for fizz in FIZZLERS.values(): if fizz.base_inst not in vmf.entities: continue # The fizzler was removed from the map. fizz_name = fizz.base_inst['targetname'] fizz_type = fizz.fizz_type # Static versions are only used for fizzlers which start on. # Permanently-off fizzlers are kinda useless, so we don't need # to bother optimising for it. is_static = bool( fizz.base_inst.fixup.int('$connectioncount', 0) == 0 and fizz.base_inst.fixup.bool('$start_enabled', 1) ) pack_list = ( fizz.fizz_type.pack_lists_static if is_static else fizz.fizz_type.pack_lists ) for pack in pack_list: packing.pack_list(vmf, pack) if fizz_type.inst[FizzInst.BASE, is_static]: random.seed('{}_fizz_base_{}'.format(MAP_RAND_SEED, fizz_name)) fizz.base_inst['file'] = random.choice(fizz_type.inst[FizzInst.BASE, is_static]) if not fizz.emitters: LOGGER.warning('No emitters for fizzler "{}"!', fizz_name) continue # Brush index -> entity for ones that need to merge. # template_brush is used for the templated one. single_brushes = {} # type: Dict[FizzlerBrush, Entity] if fizz_type.temp_max or fizz_type.temp_min: template_brush_ent = vmf.create_ent( classname='func_brush', origin=fizz.base_inst['origin'], ) conditions.set_ent_keys( template_brush_ent, fizz.base_inst, fizz_type.temp_brush_keys, ) else: template_brush_ent = None up_dir = fizz.up_axis forward = (fizz.emitters[0][1] - fizz.emitters[0][0]).norm() min_angles = FIZZ_ANGLES[forward.as_tuple(), up_dir.as_tuple()] max_angles = FIZZ_ANGLES[(-forward).as_tuple(), up_dir.as_tuple()] model_min = ( fizz_type.inst[FizzInst.PAIR_MIN, is_static] or fizz_type.inst[FizzInst.ALL, is_static] ) model_max = ( fizz_type.inst[FizzInst.PAIR_MAX, is_static] or fizz_type.inst[FizzInst.ALL, is_static] ) if not model_min or not model_max: raise ValueError( 'No model specified for one side of "{}"' ' fizzlers'.format(fizz_type.id), ) # Define a function to do the model names. model_index = 0 if fizz_type.model_naming is ModelName.SAME: def get_model_name(ind): """Give every emitter the base's name.""" return fizz_name elif fizz_type.model_naming is ModelName.LOCAL: def get_model_name(ind): """Give every emitter a name local to the base.""" return fizz_name + '-' + fizz_type.model_name elif fizz_type.model_naming is ModelName.PAIRED: def get_model_name(ind): """Give each pair of emitters the same unique name.""" return '{}-{}{:02}'.format( fizz_name, fizz_type.model_name, ind, ) elif fizz_type.model_naming is ModelName.UNIQUE: def get_model_name(ind): """Give every model a unique name.""" nonlocal model_index model_index += 1 return '{}-{}{:02}'.format( fizz_name, fizz_type.model_name, model_index, ) else: raise ValueError('Bad ModelName?') # Generate env_beam pairs. for beam in fizz_type.beams: beam_template = Entity(vmf) conditions.set_ent_keys(beam_template, fizz.base_inst, beam.keys) beam_template['classname'] = 'env_beam' del beam_template['LightningEnd'] # Don't allow users to set end pos. name = beam_template['targetname'] + '_' counter = 1 for seg_min, seg_max in fizz.emitters: for offset in beam.offset: # type: Vec min_off = offset.copy() max_off = offset.copy() min_off.localise(seg_min, min_angles) max_off.localise(seg_max, max_angles) beam_ent = beam_template.copy() vmf.add_ent(beam_ent) # Allow randomising speed and direction. if 0 < beam.speed_min < beam.speed_max: random.seed('{}{}{}'.format(MAP_RAND_SEED, min_off, max_off)) beam_ent['TextureScroll'] = random.randint(beam.speed_min, beam.speed_max) if random.choice((False, True)): # Flip to reverse direction. min_off, max_off = max_off, min_off beam_ent['origin'] = min_off beam_ent['LightningStart'] = beam_ent['targetname'] = ( name + str(counter) ) counter += 1 beam_ent['targetpoint'] = max_off # Prepare to copy over instance traits for the emitters. fizz_traits = instance_traits.get(fizz.base_inst).copy() # Special case, mark emitters that have a custom position for Clean # models. if fizz.has_cust_position: fizz_traits.add('cust_shape') mat_mod_tex = {} # type: Dict[FizzlerBrush, Set[str]] for brush_type in fizz_type.brushes: if brush_type.mat_mod_var is not None: mat_mod_tex[brush_type] = set() # Record the data for trigger hurts so flinch triggers can match them. trigger_hurt_name = '' trigger_hurt_start_disabled = '0' for seg_ind, (seg_min, seg_max) in enumerate(fizz.emitters, start=1): length = (seg_max - seg_min).mag() random.seed('{}_fizz_{}'.format(MAP_RAND_SEED, seg_min)) if length == 128 and fizz_type.inst[FizzInst.PAIR_SINGLE, is_static]: min_inst = vmf.create_ent( targetname=get_model_name(seg_ind), classname='func_instance', file=random.choice(fizz_type.inst[FizzInst.PAIR_SINGLE, is_static]), origin=(seg_min + seg_max)/2, angles=min_angles, ) else: # Both side models. min_inst = vmf.create_ent( targetname=get_model_name(seg_ind), classname='func_instance', file=random.choice(model_min), origin=seg_min, angles=min_angles, ) random.seed('{}_fizz_{}'.format(MAP_RAND_SEED, seg_max)) max_inst = vmf.create_ent( targetname=get_model_name(seg_ind), classname='func_instance', file=random.choice(model_max), origin=seg_max, angles=max_angles, ) max_inst.fixup.update(fizz.base_inst.fixup) instance_traits.get(max_inst).update(fizz_traits) min_inst.fixup.update(fizz.base_inst.fixup) instance_traits.get(min_inst).update(fizz_traits) if fizz_type.inst[FizzInst.GRID, is_static]: # Generate one instance for each position. # Go 64 from each side, and always have at least 1 section # A 128 gap will have length = 0 for ind, dist in enumerate(range(64, round(length) - 63, 128)): mid_pos = seg_min + forward * dist random.seed('{}_fizz_mid_{}'.format(MAP_RAND_SEED, mid_pos)) mid_inst = vmf.create_ent( classname='func_instance', targetname=fizz_name, angles=min_angles, file=random.choice(fizz_type.inst[FizzInst.GRID, is_static]), origin=mid_pos, ) mid_inst.fixup.update(fizz.base_inst.fixup) instance_traits.get(mid_inst).update(fizz_traits) if template_brush_ent is not None: if length == 128 and fizz_type.temp_single: temp = template_brush.import_template( fizz_type.temp_single, (seg_min + seg_max) / 2, min_angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) template_brush_ent.solids.extend(temp.world) else: if fizz_type.temp_min: temp = template_brush.import_template( fizz_type.temp_min, seg_min, min_angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) template_brush_ent.solids.extend(temp.world) if fizz_type.temp_max: temp = template_brush.import_template( fizz_type.temp_max, seg_max, max_angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) template_brush_ent.solids.extend(temp.world) # Generate the brushes. for brush_type in fizz_type.brushes: brush_ent = None # If singular, we reuse the same brush ent for all the segments. if brush_type.singular: brush_ent = single_brushes.get(brush_type, None) # Non-singular or not generated yet - make the entity. if brush_ent is None: brush_ent = vmf.create_ent(classname='func_brush') for key_name, key_value in brush_type.keys.items(): brush_ent[key_name] = conditions.resolve_value(fizz.base_inst, key_value) for key_name, key_value in brush_type.local_keys.items(): brush_ent[key_name] = conditions.local_name( fizz.base_inst, conditions.resolve_value( fizz.base_inst, key_value, ) ) brush_ent['targetname'] = conditions.local_name( fizz.base_inst, brush_type.name, ) # Set this to the center, to make sure it's not going to leak. brush_ent['origin'] = (seg_min + seg_max)/2 # For fizzlers flat on the floor/ceiling, scanlines look # useless. Turn them off. if 'usescanline' in brush_ent and fizz.normal().z: brush_ent['UseScanline'] = 0 if brush_ent['classname'] == 'trigger_hurt': trigger_hurt_name = brush_ent['targetname'] trigger_hurt_start_disabled = brush_ent['startdisabled'] if brush_type.set_axis_var: brush_ent['vscript_init_code'] = ( 'axis <- `{}`;'.format( fizz.normal().axis(), ) ) for out in brush_type.outputs: new_out = out.copy() new_out.target = conditions.local_name( fizz.base_inst, new_out.target, ) brush_ent.add_out(new_out) if brush_type.singular: # Record for the next iteration. single_brushes[brush_type] = brush_ent # If we have a material_modify_control to generate, # we need to parent it to ourselves to restrict it to us # only. We also need one for each material, so provide a # function to the generator which adds to a set. if brush_type.mat_mod_var is not None: used_tex_func = mat_mod_tex[brush_type].add else: def used_tex_func(val): """If not, ignore those calls.""" return None # Generate the brushes and texture them. brush_ent.solids.extend( brush_type.generate( vmf, fizz, seg_min, seg_max, used_tex_func, ) ) if trigger_hurt_name: fizz.gen_flinch_trigs( vmf, trigger_hurt_name, trigger_hurt_start_disabled, ) # If we have the config, but no templates used anywhere... if template_brush_ent is not None and not template_brush_ent.solids: template_brush_ent.remove() for brush_type, used_tex in mat_mod_tex.items(): brush_name = conditions.local_name(fizz.base_inst, brush_type.name) mat_mod_name = conditions.local_name(fizz.base_inst, brush_type.mat_mod_name) for off, tex in zip(MATMOD_OFFSETS, sorted(used_tex)): pos = off.copy().rotate(*min_angles) pos += Vec.from_str(fizz.base_inst['origin']) vmf.create_ent( classname='material_modify_control', origin=pos, targetname=mat_mod_name, materialName='materials/' + tex + '.vmt', materialVar=brush_type.mat_mod_var, parentname=brush_name, )
def gen_flinch_trigs(self, vmf: VMF, name: str, start_disabled: str) -> None: """For deadly fizzlers optionally make them safer. This adds logic to force players back instead when walking into the field. Only applies to vertical triggers. """ normal = abs(self.normal()) # type: Vec # Horizontal fizzlers would just have you fall through. if normal.z: return # Disabled. if not vbsp_options.get_itemconf(('VALVE_FIZZLER', 'FlinchBack'), False): return # Make global entities if not present. if '_fizz_flinch_hurt' not in vmf.by_target: glob_ent_loc = vbsp_options.get(Vec, 'global_ents_loc') vmf.create_ent( classname='point_hurt', targetname='_fizz_flinch_hurt', Damage=10, # Just for visuals and sounds. # BURN | ENERGYBEAM | PREVENT_PHYSICS_FORCE DamageType=8 | 1024 | 2048, DamageTarget='!activator', # Hurt the triggering player. DamageRadius=1, # Target makes this unused. origin=glob_ent_loc, ) # We need two catapults - one for each side. neg_brush = vmf.create_ent( targetname=name, classname='trigger_catapult', spawnflags=1, # Players only. origin=self.base_inst['origin'], physicsSpeed=0, playerSpeed=96, launchDirection=(-normal).to_angle(), startDisabled=start_disabled, ) neg_brush.add_out(Output('OnCatapulted', '_fizz_flinch_hurt', 'Hurt')) pos_brush = neg_brush.copy() pos_brush['launchDirection'] = normal.to_angle() vmf.add_ent(pos_brush) for seg_min, seg_max in self.emitters: neg_brush.solids.append(vmf.make_prism( p1=(seg_min - 4 * normal - 64 * self.up_axis ), p2=seg_max + 64 * self.up_axis, mat=const.Tools.TRIGGER, ).solid) pos_brush.solids.append(vmf.make_prism( p1=seg_min - 64 * self.up_axis, p2=(seg_max + 4 * normal + 64 * self.up_axis ), mat=const.Tools.TRIGGER, ).solid)