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 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 add_quote(quote: Property, targetname, quote_loc: Vec, use_dings=False): """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': vmf_file.create_ent( classname='func_instance', targetname='', file=INST_PREFIX + prop.value, origin=quote_loc, fixup_style='2', # No fixup ) 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( 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( 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_file.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(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_file, prop.value) elif name == 'pack': if prop.has_children(): packing.pack_files(vmf_file, *[subprop.value for subprop in prop]) else: packing.pack_files(vmf_file, 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 add_quote(quote: Property, targetname, quote_loc: Vec, use_dings=False): """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': vmf_file.create_ent( classname='func_instance', targetname='', file=INST_PREFIX + prop.value, origin=quote_loc, fixup_style='2', # No fixup ) 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( 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( 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_file.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(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_file, prop.value) elif name == 'pack': if prop.has_children(): packing.pack_files(vmf_file, *[ subprop.value for subprop in prop ]) else: packing.pack_files(vmf_file, 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, ))