def res_rand_vec(inst: Entity, res: Property) -> None: """A modification to RandomNum which generates a random vector instead. `decimal`, `seed` and `ResultVar` work like RandomNum. `min_x`, `max_y` etc are used to define the boundaries. If the min and max are equal that number will be always used instead. """ is_float = srctools.conv_bool(res['decimal']) var = res['resultvar', '$random'] set_random_seed(inst, 'e' + res['seed', 'random']) if is_float: func = random.uniform else: func = random.randint value = Vec() for axis in 'xyz': max_val = srctools.conv_float(res['max_' + axis, 0.0]) min_val = srctools.conv_float(res['min_' + axis, 0.0]) if min_val == max_val: value[axis] = min_val else: value[axis] = func(min_val, max_val) inst.fixup[var] = value.join(' ')
def res_rand_vec(inst: Entity, res: Property) -> None: """A modification to RandomNum which generates a random vector instead. 'decimal', 'seed' and 'ResultVar' work like RandomNum. min/max x/y/z are for each section. If the min and max are equal that number will be used instead. """ is_float = srctools.conv_bool(res['decimal']) var = res['resultvar', '$random'] set_random_seed(inst, 'e' + res['seed', 'random']) if is_float: func = random.uniform else: func = random.randint value = Vec() for axis in 'xyz': max_val = srctools.conv_float(res['max_' + axis, 0.0]) min_val = srctools.conv_float(res['min_' + axis, 0.0]) if min_val == max_val: value[axis] = min_val else: value[axis] = func(min_val, max_val) inst.fixup[var] = value.join(' ')
def split_float( val: str, enum: Callable[[str], EnumType], default, ) -> Tuple[Union[float, EnumType], Union[float, EnumType]]: """Handle values which can be either single or a low, high pair of numbers. If single, low and high are the same. enum is a Enum with values to match text constants, or a converter function returning enums or raising ValueError, KeyError or IndexError. """ if ',' in val: s_low, s_high = val.split(',') try: low = enum(s_low.upper()) except (LookupError, ValueError): low = conv_float(s_low, default) try: high = enum(s_high.upper()) except (LookupError, ValueError): high = conv_float(s_high, default) return low, high else: try: out = enum(val.upper()) except (LookupError, ValueError): out = conv_float(val, default) return out, out
def res_rand_vec(inst: Entity, res: Property): """A modification to RandomNum which generates a random vector instead. 'decimal', 'seed' and 'ResultVar' work like RandomNum. min/max x/y/z are for each section. If the min and max are equal that number will be used instead. """ is_float = srctools.conv_bool(res['decimal']) var = res['resultvar', '$random'] seed = res['seed', 'random'] random.seed(inst['origin'] + inst['angles'] + 'random_' + seed) if is_float: func = random.uniform else: func = random.randint value = Vec() for axis in 'xyz': max_val = srctools.conv_float(res['max_' + axis, 0.0]) min_val = srctools.conv_float(res['min_' + axis, 0.0]) if min_val == max_val: value[axis] = min_val else: value[axis] = func(min_val, max_val) inst.fixup[var] = value.join(' ')
def test_conv_float(): # Float should convert integers too for string, result in ints: assert srctools.conv_float(string) == float(result) assert srctools.conv_float(string) == result for string in non_floats: # Default default value assert srctools.conv_float(string) == 0.0 for default in def_vals: # Check all default values pass through unchanged assert srctools.conv_float(string, default) is default
def parse(prop: Property): """Parse from property values. The value can be in four forms: "prop" "material" "prop" "<scale>|material" "prop" "<scale>|material|static" "prop" { "tex" "<mat>" "scale" "<scale>" "static" "<is_static>" } """ if prop.has_children(): tex = prop['tex'] scale = prop.float('scale', 0.25) static = prop.bool('static') else: vals = prop.value.split('|') opts = () scale_str = '0.25' if len(vals) == 2: scale_str, tex = vals elif len(vals) > 2: scale_str, tex, *opts = vals else: # Unpack to ensure it only has 1 section [tex] = vals scale = conv_float(scale_str, 0.25) static = 'static' in opts return AntTex(tex, scale, static)
def sceneset(ctx: Context): """Chains a set of choreographed scenes together.""" for ent in ctx.vmf.by_class['comp_choreo_sceneset']: scenes = [ ent['scene{:02}'.format(i)] for i in range(1, 21) if ent['scene{:02}'.format(i)] ] if not scenes: LOGGER.warning( '"{}" at ({}) has no scenes!', ent['targetname'], ent['origin'], ) continue if conv_bool(ent['play_dings']): scenes.insert(0, 'scenes/npc/glados_manual/ding_on.vcd') scenes.append('scenes/npc/glados_manual/ding_off.vcd') delay = conv_float(ent['delay'], 0.1) only_once = conv_bool(ent['only_once']) ent.remove() start_ent = None name = ent['targetname'] or '_choreo_{}'.format(ent.id) for i, scene in enumerate(scenes): part = ctx.vmf.create_ent( classname='logic_choreographed_scene', targetname=('{}_{}'.format(name, i) if i > 0 else name), origin=ent['origin'], scenefile=scene, ) if i + 1 < len(scenes): part.add_out( Output( 'OnCompletion', '{}_{}'.format(name, i + 1), 'Start', delay=delay, )) if only_once: # When started blank the name so it can't be triggered, # then clean up after finished part.add_out( Output('OnStart', '!self', 'AddOutput', 'targetname '), Output('OnCompletion', '!self', 'Kill'), ) if start_ent is None: start_ent = part assert start_ent is not None, "Has scenes but none made?" for out in ent.outputs: if out.output.casefold() == 'onstart': start_ent.add_out(out) elif out.output.casefold() == 'onfinish': # Part is the last in the loop. out.output = 'OnCompletion' part.add_out(out)
def res_timed_relay(inst: Entity, res: Property): """Generate a logic_relay with outputs delayed by a certain amount. This allows triggering outputs based $timer_delay values. """ var, name, disabled, flags, final_outs, rep_outs = res.value relay = VMF.create_ent( classname='logic_relay', spawnflags=flags, origin=inst['origin'], targetname=local_name(inst, name), ) relay['StartDisabled'] = (inst.fixup[disabled] if disabled.startswith('$') else disabled) delay = srctools.conv_float(inst.fixup[ var, '0'] if var.startswith('$') else var) for off in range(int(math.ceil(delay))): for out in rep_outs: new_out = out.copy() # type: Output new_out.target = local_name(inst, new_out.target) new_out.delay += off new_out.comma_sep = False relay.add_out(new_out) for out in final_outs: new_out = out.copy() # type: Output new_out.target = local_name(inst, new_out.target) new_out.delay += delay new_out.comma_sep = False relay.add_out(new_out)
def res_rotate_inst(inst: Entity, res: Property) -> None: """Rotate the instance around an axis. If `axis` is specified, it should be a normal vector and the instance will be rotated `angle` degrees around it. Otherwise, `angle` is a pitch-yaw-roll angle which is applied. `around` can be a point (local, pre-rotation) which is used as the origin. """ angles = Angle.from_str(inst['angles']) if 'axis' in res: orient = Matrix.axis_angle( Vec.from_str(inst.fixup.substitute(res['axis'])), conv_float(inst.fixup.substitute(res['angle'])), ) else: orient = Matrix.from_angle( Angle.from_str(inst.fixup.substitute(res['angle']))) try: offset = Vec.from_str(inst.fixup.substitute(res['around'])) except NoKeyError: pass else: origin = Vec.from_str(inst['origin']) inst['origin'] = origin + (-offset @ orient + offset) @ angles inst['angles'] = (orient @ angles).to_angle()
def res_global_input_setup(res: Property): target = res['target', ''] or None name = res['name', ''] or None output = res['output', 'OnTrigger'] param = res['param', ''] delay = srctools.conv_float(res['delay', '']) inp_name, inp_command = Output.parse_name(res['input']) return name, inp_name, inp_command, output, delay, param, target
def res_water_splash_setup(res: Property): parent = res['parent'] name = res['name'] scale = srctools.conv_float(res['scale', ''], 8.0) pos1 = Vec.from_str(res['position', '']) calc_type = res['type', ''] pos2 = res['position2', ''] fast_check = srctools.conv_bool(res['fast_check', '']) return name, parent, scale, pos1, pos2, calc_type, fast_check
def res_add_output(inst: Entity, res: Property): """Add an output from an instance to a global or local name. Values: - output: The output name.Can be <ITEM_ID:activate> or <ITEM_ID:deactivate> to lookup that item type. - target: The name of the target entity - input: The input to give - parm: Parameters for the input - delay: Delay for the output - only_once: True to make the input last only once (overrides times) - times: The number of times to trigger the input """ ( out_type, out_id, targ, input_name, parm, delay, times, inst_in, inst_out, ) = res.value LOGGER.info('Conn: {}', res.value) if out_type in ('activate', 'deactivate'): try: connection = CONNECTIONS[out_id] except KeyError: LOGGER.warning('"{}" has no connections!', out_id) return if out_type[0] == 'a': inst_out, output = connection.out_act else: inst_out, output = connection.out_deact else: output = resolve_value(inst, out_id) inst_out = resolve_value(inst, inst_out) inst.add_out( Output( resolve_value(inst, output), local_name(inst, resolve_value(inst, targ)), resolve_value(inst, input_name), resolve_value(inst, parm), srctools.conv_float(resolve_value(inst, delay)), times=times, inst_out=resolve_value(inst, inst_out) or None, inst_in=resolve_value(inst, inst_in) or None, ))
def res_rand_num(inst: Entity, res: Property): """Generate a random number and save in a fixup value. If 'decimal' is true, the value will contain decimals. 'max' and 'min' are inclusive. 'ResultVar' is the variable the result will be saved in. If 'seed' is set, it will be used to keep the value constant across map recompiles. This should be unique. """ is_float = srctools.conv_bool(res['decimal']) max_val = srctools.conv_float(res['max', 1.0]) min_val = srctools.conv_float(res['min', 0.0]) var = res['resultvar', '$random'] seed = res['seed', 'random'] random.seed(inst['origin'] + inst['angles'] + 'random_' + seed) if is_float: func = random.uniform else: func = random.randint inst.fixup[var] = str(func(min_val, max_val))
def res_rand_num(inst: Entity, res: Property) -> None: """Generate a random number and save in a fixup value. If 'decimal' is true, the value will contain decimals. 'max' and 'min' are inclusive. 'ResultVar' is the variable the result will be saved in. If 'seed' is set, it will be used to keep the value constant across map recompiles. This should be unique. """ is_float = srctools.conv_bool(res['decimal']) max_val = srctools.conv_float(res['max', 1.0]) min_val = srctools.conv_float(res['min', 0.0]) var = res['resultvar', '$random'] seed = 'd' + res['seed', 'random'] set_random_seed(inst, seed) if is_float: func = random.uniform else: func = random.randint inst.fixup[var] = str(func(min_val, max_val))
def res_add_output(inst: Entity, res: Property): """Add an output from an instance to a global or local name. Values: - output: The output name.Can be <ITEM_ID:activate> or <ITEM_ID:deactivate> to lookup that item type. - target: The name of the target entity - input: The input to give - parm: Parameters for the input - delay: Delay for the output - only_once: True to make the input last only once (overrides times) - times: The number of times to trigger the input """ ( out_type, out_id, targ, input_name, parm, delay, times, inst_in, inst_out, ) = res.value LOGGER.info('Conn: {}', res.value) if out_type in ('activate', 'deactivate'): try: connection = CONNECTIONS[out_id] except KeyError: LOGGER.warning('"{}" has no connections!', out_id) return if out_type[0] == 'a': inst_out, output = connection.out_act else: inst_out, output = connection.out_deact else: output = resolve_value(inst, out_id) inst_out = resolve_value(inst, inst_out) inst.add_out(Output( resolve_value(inst, output), local_name(inst, resolve_value(inst, targ)), resolve_value(inst, input_name), resolve_value(inst, parm), srctools.conv_float(resolve_value(inst, delay)), times=times, inst_out=resolve_value(inst, inst_out) or None, inst_in=resolve_value(inst, inst_in) or None, ))
def res_rand_num(res: Property) -> Callable[[Entity], None]: """Generate a random number and save in a fixup value. If 'decimal' is true, the value will contain decimals. 'max' and 'min' are inclusive. 'ResultVar' is the variable the result will be saved in. If 'seed' is set, it will be used to keep the value constant across map recompiles. This should be unique. """ is_float = srctools.conv_bool(res['decimal']) max_val = srctools.conv_float(res['max', 1.0]) min_val = srctools.conv_float(res['min', 0.0]) var = res['resultvar', '$random'] seed = res['seed', ''] def randomise(inst: Entity) -> None: """Apply the random number.""" rng = rand.seed(b'rand_num', inst, seed) if is_float: inst.fixup[var] = rng.uniform(min_val, max_val) else: inst.fixup[var] = rng.randint(min_val, max_val) return randomise
def res_change_inputs_setup(res: Property): vals = {} for prop in res: out_key = Output.parse_name(prop.real_name) if prop.has_children(): vals[out_key] = ( prop['inst_in', None], prop['input'], prop['params', ''], srctools.conv_float(prop['delay', 0.0]), 1 if srctools.conv_bool(prop['only_once', '0']) else -1, ) else: vals[out_key] = None return vals
def comp_entity_mover(ctx: Context): """Move an entity to another location.""" for mover in ctx.vmf.by_class['comp_entity_mover']: mover.remove() ref_name = mover['reference'] offset = Vec() if ref_name: for ent in ctx.vmf.search(ref_name): offset = Vec.from_str(mover['origin']) - Vec.from_str(ent['origin']) offset *= conv_float(mover['distance']) break else: LOGGER.warning( 'Can\'t find ref entity named "{}" ' 'for comp_ent_mover at <{}>!', ref_name, mover['origin'], ) else: # Use angles + movement. offset = Vec(x=conv_float(mover['distance'])) offset.rotate_by_str(mover['direction']) found_ent = None for found_ent in ctx.vmf.search(mover['target']): origin = Vec.from_str(found_ent['origin']) origin += offset found_ent['origin'] = str(origin) if found_ent is None: LOGGER.warning( 'No entities found named "{}" for comp_ent_mover at ({})!', mover['target'], mover['origin'], )
def res_global_input_setup(res: Property): if res.has_children(): name = res['name', ''] inp_name, inp_command = Output.parse_name(res['input']) return name, Output( out=res['output', 'OnTrigger'], targ=res['target', ''], inp=inp_command, inst_in=inp_name, delay=srctools.conv_float(res['delay', '']), param=res['param', ''], ) else: out = Output.parse(res) out.output = '' # Don't need to store GlobalInput... return '', out
def comp_trigger_goo(ctx: Context): """Creates triggers for Toxic Goo.""" for trig in ctx.vmf.by_class['comp_trigger_p2_goo']: trig.remove() outputs = trig.outputs.copy() trig.outputs.clear() failsafe_delay = conv_float(trig['failsafe_delay'], 0.5) del trig['failsafe_delay'] if failsafe_delay < 0.01: failsafe_delay = 0.01 hurt = trig.copy() diss = trig.copy() ctx.vmf.add_ents([hurt, diss]) hurt['classname'] = 'trigger_hurt' hurt['damagetype'] = 262144 # Radiation hurt['damage'] = hurt['damagecap'] = 10000 hurt['damagemodel'] = 0 # No doubling hurt['nodmgforce'] = 1 # Don't throw players around. hurt['spawnflags'] = 1 # Players. del hurt['filtername'] diss['classname'] = 'trigger_multiple' diss['spawnflags'] = 1096 # Physics, physics debris, everything diss['wait'] = 0 # No delay. diss['filtername'] = trig['dissolve_filter'] diss.add_out( Output('OnStartTouch', '!activator', 'SilentDissolve'), Output('OnStartTouch', '!activator', 'Kill', delay=failsafe_delay), ) for out in outputs: if out.output.casefold() == 'onkillplayer': out.output = 'OnStartTouch' hurt.add_out(out) elif out.output.casefold() == 'ondissolvephysics': out.output = 'OnStartTouch' diss.add_out(out)
def parse(cls, ent: Entity): """Parse a template from a config entity. This should be a 'bee2_template_scaling' entity. """ axes = {} for norm, name in ( ((0, 0, 1), 'up'), ((0, 0, -1), 'dn'), ((0, 1, 0), 'n'), ((0, -1, 0), 's'), ((1, 0, 0), 'e'), ((-1, 0, 0), 'w'), ): axes[norm] = ( ent[name + '_tex'], UVAxis.parse(ent[name + '_uaxis']), UVAxis.parse(ent[name + '_vaxis']), srctools.conv_float(ent[name + '_rotation']), ) return cls(ent['template_id'], axes)
def res_fix_rotation_axis(ent: Entity, res: Property): """Generate a `func_rotating`, `func_door_rotating` or any similar entity. This uses the orientation of the instance to detemine the correct spawnflags to make it rotate in the correct direction. The brush will be 2x2x2 units large, and always set to be non-solid. - `Pos` and `name` are local to the instance, and will set the `origin` and `targetname` respectively. - `Keys` are any other keyvalues to be be set. - `Flags` sets additional spawnflags. Multiple values may be separated by `+`, and will be added together. - `Classname` specifies which entity will be created, as well as which other values will be set to specify the correct orientation. - `AddOut` is used to add outputs to the generated entity. It takes the options `Output`, `Target`, `Input`, `Param` and `Delay`. If `Inst_targ` is defined, it will be used with the input to construct an instance proxy input. If `OnceOnly` is set, the output will be deleted when fired. Permitted entities: * `func_rotating` * `func_door_rotating` * `func_rot_button` * `func_platrot` """ des_axis = res['axis', 'z'].casefold() reverse = srctools.conv_bool(res['reversed', '0']) door_type = res['classname', 'func_door_rotating'] # Extra stuff to apply to the flags (USE, toggle, etc) flags = sum( map( # Add together multiple values srctools.conv_int, res['flags', '0'].split('+'))) name = conditions.local_name(ent, res['name', '']) axis = Vec(**{des_axis: 1}).rotate_by_str(ent['angles', '0 0 0']) pos = Vec.from_str(res['Pos', '0 0 0']).rotate_by_str(ent['angles', '0 0 0']) pos += Vec.from_str(ent['origin', '0 0 0']) door_ent = vbsp.VMF.create_ent( classname=door_type, targetname=name, origin=pos.join(' '), ) conditions.set_ent_keys(door_ent, ent, res) for output in res.find_all('AddOut'): door_ent.add_out( Output( out=output['Output', 'OnUse'], inp=output['Input', 'Use'], targ=output['Target', ''], inst_in=output['Inst_targ', None], param=output['Param', ''], delay=srctools.conv_float(output['Delay', '']), times=(1 if srctools.conv_bool(output['OnceOnly', False]) else -1), )) # Generate brush door_ent.solids = [vbsp.VMF.make_prism(pos - 1, pos + 1).solid] if axis.x > 0 or axis.y > 0 or axis.z > 0: # If it points forward, we need to reverse the rotating door reverse = not reverse flag_values = FLAG_ROTATING[door_type] # Make the door always non-solid! flags |= flag_values.get('solid_flags', 0) # Add or remove flags as needed. # flags |= bit sets it to 1. # flags |= ~bit sets it to 0. if axis.x != 0: flags |= flag_values.get('x', 0) else: flags &= ~flag_values.get('x', 0) if axis.y != 0: flags |= flag_values.get('y', 0) else: flags &= ~flag_values.get('y', 0) if axis.z != 0: flags |= flag_values.get('z', 0) else: flags &= ~flag_values.get('z', 0) if door_type == 'momentary_rot_button': door_ent['startdirection'] = '1' if reverse else '-1' else: if reverse: flags |= flag_values.get('rev', 0) else: flags &= ~flag_values.get('rev', 0) door_ent['spawnflags'] = str(flags)
def res_import_template_setup(res: Property): temp_id = res['id'] force = res['force', ''].casefold().split() if 'white' in force: force_colour = template_brush.MAT_TYPES.white elif 'black' in force: force_colour = template_brush.MAT_TYPES.black elif 'invert' in force: force_colour = 'INVERT' else: force_colour = None if 'world' in force: force_type = template_brush.TEMP_TYPES.world elif 'detail' in force: force_type = template_brush.TEMP_TYPES.detail else: force_type = template_brush.TEMP_TYPES.default for size in ('2x2', '4x4', 'wall', 'special'): if size in force: force_grid = size break else: force_grid = None invert_var = res['invertVar', ''] color_var = res['colorVar', ''] replace_tex = defaultdict(list) for prop in res.find_key('replace', []): replace_tex[prop.name].append(prop.value) rem_replace_brush = True additional_ids = set() transfer_overlays = '1' try: replace_brush = res.find_key('replaceBrush') except NoKeyError: replace_brush_pos = None else: if replace_brush.has_children(): replace_brush_pos = replace_brush['Pos', '0 0 0'] additional_ids = set( map( srctools.conv_int, replace_brush['additionalIDs', ''].split(), )) rem_replace_brush = replace_brush.bool('removeBrush', True) transfer_overlays = replace_brush['transferOverlay', '1'] else: replace_brush_pos = replace_brush.value # type: str replace_brush_pos = Vec.from_str(replace_brush_pos) replace_brush_pos.z -= 64 # 0 0 0 defaults to the floor. key_values = res.find_key("Keys", []) if key_values: keys = Property("", [ key_values, res.find_key("LocalKeys", []), ]) # Ensure we have a 'origin' keyvalue - we automatically offset that. if 'origin' not in key_values: key_values['origin'] = '0 0 0' # Spawn everything as detail, so they get put into a brush # entity. force_type = template_brush.TEMP_TYPES.detail else: keys = None visgroup_mode = res['visgroup', 'none'].casefold() if visgroup_mode not in ('none', 'choose'): visgroup_mode = srctools.conv_float(visgroup_mode.rstrip('%'), 0.00) if visgroup_mode == 0: visgroup_mode = 'none' # Generate the function which picks which visgroups to add to the map. if visgroup_mode == 'none': def visgroup_func(_): """none = don't add any visgroups.""" return () elif visgroup_mode == 'choose': def visgroup_func(groups): """choose = add one random group.""" return [random.choice(groups)] else: def visgroup_func(groups): """Number = percent chance for each to be added""" for group in groups: val = random.uniform(0, 100) if val <= visgroup_mode: yield group # If true, force visgroups to all be used. visgroup_force_var = res['forceVisVar', ''] return ( temp_id, dict(replace_tex), force_colour, force_grid, force_type, replace_brush_pos, rem_replace_brush, transfer_overlays, additional_ids, invert_var, color_var, visgroup_func, visgroup_force_var, keys, )
def sceneset(ctx: Context): """Chains a set of choreographed scenes together.""" for ent in ctx.vmf.by_class['comp_choreo_sceneset']: scenes = [ ent['scene{:02}'.format(i)] for i in range(1, 21) if ent['scene{:02}'.format(i)] ] if not scenes: LOGGER.warning( '"{}" at ({}) has no scenes!', ent['targetname'], ent['origin'], ) continue if conv_bool(ent['play_dings']): scenes.insert(0, 'scenes/npc/glados_manual/ding_on.vcd') scenes.append('scenes/npc/glados_manual/ding_off.vcd') delay = conv_float(ent['delay'], 0.1) only_once = conv_bool(ent['only_once']) ent.remove() start_ent = None name = ent['targetname'] or '_choreo_{}'.format(ent.id) for i, scene in enumerate(scenes): part = ctx.vmf.create_ent( classname='logic_choreographed_scene', targetname=( '{}_{}'.format(name, i) if i > 0 else name ), origin=ent['origin'], scenefile=scene, ) if i + 1 < len(scenes): part.add_out(Output( 'OnCompletion', '{}_{}'.format(name, i+1), 'Start', delay=delay, )) if only_once: # When started blank the name so it can't be triggered, # then clean up after finished part.add_out( Output('OnStart', '!self', 'AddOutput', 'targetname '), Output('OnCompletion', '!self', 'Kill'), ) if start_ent is None: start_ent = part assert start_ent is not None, "Has scenes but none made?" for out in ent.outputs: if out.output.casefold() == 'onstart': start_ent.add_out(out) elif out.output.casefold() == 'onfinish': # Part is the last in the loop. out.output = 'OnCompletion' part.add_out(out)
def res_resizeable_trigger(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 'previewVar': A stylevar 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 'instance:name;Input' value to turn the previewInst on and off. 'triggerActivate, triggerDeactivate': The 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 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 = resolve_inst(res['markerInst']) markers = {} for inst in vbsp.VMF.by_class['func_instance']: if inst['file'].casefold() in marker: markers[inst['targetname']] = inst if not markers: # No markers in the map - abort return RES_EXHAUSTED trig_act = res['triggerActivate', 'OnStartTouchAll'] trig_deact = res['triggerDeactivate','OnEndTouchAll'] coop_var = res['coopVar', None] coop_act = res['coopActivate', 'OnChangeToAllTrue'] coop_deact = res['coopDeactivate', 'OnChangeToAnyFalse'] coop_only_once = res.bool('coopOnce') marker_connection = conditions.CONNECTIONS[res['markerItem'].casefold()] mark_act_name, mark_act_out = marker_connection.out_act mark_deact_name, mark_deact_out = marker_connection.out_deact del marker_connection preview_var = res['previewVar', ''].casefold() # Display preview overlays if it's preview mode, and the style var is true # or does not exist if vbsp.IS_PREVIEW and (not preview_var or vbsp.settings['style_vars'][preview_var]): preview_mat = res['previewMat', ''] preview_inst_file = res['previewInst', ''] pre_act_name, pre_act_inp = Output.parse_name( res['previewActivate', '']) pre_deact_name, pre_deact_inp = Output.parse_name( res['previewDeactivate', '']) preview_scale = srctools.conv_float(res['previewScale', '0.25'], 0.25) else: # Deactivate the preview_ options when publishing. preview_mat = preview_inst_file = '' pre_act_name = pre_deact_name = None pre_act_inp = pre_deact_inp = '' preview_scale = 0.25 # Now convert each brush # Use list() to freeze it, allowing us to delete from the dict for targ, inst in list(markers.items()): # type: str, VLib.Entity for out in inst.output_targets(): if out in markers: other = markers[out] # type: Entity del markers[out] # Don't let it get repeated break else: if inst.fixup['$connectioncount'] == '0': # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 1-block trigger. other = inst else: continue # It's a marker with an input, the other in the pair # will handle everything. for ent in {inst, other}: # Only do once if inst == other ent.remove() is_coop = vbsp.GAME_MODE == 'COOP' and ( inst.fixup.bool(coop_var) or other.fixup.bool(coop_var) ) bbox_min, bbox_max = Vec.bbox( Vec.from_str(inst['origin']), Vec.from_str(other['origin']) ) # Extend to the edge of the blocks. bbox_min -= 64 bbox_max += 64 out_ent = trig_ent = vbsp.VMF.create_ent( classname='trigger_multiple', # Default # Use the 1st instance's name - that way other inputs control the # trigger itself. targetname=targ, origin=inst['origin'], angles='0 0 0', ) trig_ent.solids = [ vbsp.VMF.make_prism( bbox_min, bbox_max, mat=const.Tools.TRIGGER, ).solid, ] # Use 'keys' and 'localkeys' blocks to set all the other keyvalues. conditions.set_ent_keys(trig_ent, inst, res) if is_coop: trig_ent['spawnflags'] = '1' # Clients trig_ent['classname'] = 'trigger_playerteam' out_ent_name = conditions.local_name(inst, 'man') out_ent = vbsp.VMF.create_ent( classname='logic_coop_manager', targetname=out_ent_name, origin=inst['origin'] ) if coop_only_once: # Kill all the ents when both players are present. out_ent.add_out( Output('OnChangeToAllTrue', out_ent_name, 'Kill'), Output('OnChangeToAllTrue', targ, 'Kill'), ) trig_ent.add_out( Output('OnStartTouchBluePlayer', out_ent_name, 'SetStateATrue'), Output('OnStartTouchOrangePlayer', out_ent_name, 'SetStateBTrue'), Output('OnEndTouchBluePlayer', out_ent_name, 'SetStateAFalse'), Output('OnEndTouchOrangePlayer', out_ent_name, 'SetStateBFalse'), ) act_out = coop_act deact_out = coop_deact else: act_out = trig_act deact_out = trig_deact if preview_mat: preview_brush = vbsp.VMF.create_ent( classname='func_brush', parentname=targ, origin=inst['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. vbsp.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: vbsp.VMF.create_ent( classname='func_instance', targetname=targ + '_preview', file=preview_inst_file, # Put it at the second marker, since that's usually # closest to antlines if present. origin=other['origin'], ) if pre_act_name and trig_act: out_ent.add_out(Output( trig_act, targ + '_preview', inst_in=pre_act_name, inp=pre_act_inp, )) if pre_deact_name and trig_deact: out_ent.add_out(Output( trig_deact, targ + '_preview', inst_in=pre_deact_name, inp=pre_deact_inp, )) # Now copy over the outputs from the markers, making it work. for out in inst.outputs + other.outputs: # Skip the output joining the two markers together. if out.target == other['targetname']: continue if out.inst_out == mark_act_name and out.output == mark_act_out: ent_out = act_out elif out.inst_out == mark_deact_name and out.output == mark_deact_out: ent_out = deact_out else: continue # Skip this output - it's somehow invalid for this item. if not ent_out: continue # Allow setting the output to "" to skip out_ent.add_out(Output( ent_out, out.target, inst_in=out.inst_in, inp=out.input, param=out.params, delay=out.delay, times=out.times, )) return RES_EXHAUSTED
def res_conveyor_belt(inst: Entity, res: Property): """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. `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 = srctools.conv_int(inst.fixup['$travel_distance']) if move_dist <= 2: # There isn't room for a catwalk, so don't bother. inst.remove() return move_dir = Vec(1, 0, 0).rotate_by_str(inst.fixup['$travel_direction']) move_dir.rotate_by_str(inst['angles']) start_offset = srctools.conv_float(inst.fixup['$starting_position'], 0) teleport_to_start = res.bool('TrackTeleport', True) segment_inst_file = instanceLocs.resolve_one(res['SegmentInst', '']) rail_template = res['RailTemplate', None] vmf = inst.map 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 # Find the angle which generates an instance pointing in the direction # of movement, with the same normal. norm = Vec(z=1).rotate_by_str(inst['angles']) for roll in range(0, 360, 90): angles = move_dir.to_angle(roll) if Vec(z=1).rotate(*angles) == norm: break else: raise ValueError("Can't find angles to give a" ' z={} and x={}!'.format(norm, move_dir)) if res.bool('rotateSegments', True): inst['angles'] = angles else: angles = Vec.from_str(inst['angles']) # 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 beams at the top, so they don't appear inside wall sections. beam_start = start_pos + 48 * norm # type: Vec beam_end = end_pos + 48 * norm # type: Vec for index, pos in enumerate(beam_start.iter_line(beam_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 != end_pos: seg_inst = vmf.create_ent( classname='func_instance', targetname=track_name.format(index), file=segment_inst_file, origin=pos, angles=angles, ) seg_inst.fixup.update(inst.fixup) if rail_template: temp = template_brush.import_template( rail_template, pos, angles, 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=beam_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. beam_keys = res.find_key('BeamKeys', []) if beam_keys.value: beam = vmf.create_ent(classname='env_beam') # 3 offsets - x = distance from walls, y = side, z = height 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, ).rotate(*angles) beam['TargetPoint'] = end_pos + Vec( +beam_off.x, beam_off.y, beam_off.z, ).rotate(*angles) # 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).rotate(*angles), end_pos + Vec(-72, 56, 144).rotate(*angles), mat='tools/toolstrigger', ).solid) if res.bool('NoPortalFloor'): # Block portals on the floor.. floor_noportal = vmf.create_ent( classname='func_noportal_volume', origin=beam_start, ) floor_noportal.solids.append( vmf.make_prism( start_pos + Vec(-60, -60, -66).rotate(*angles), end_pos + Vec(60, 60, -60).rotate(*angles), mat='tools/toolsinvisible', ).solid) # A brush covering under the platform. base_trig = vmf.make_prism( start_pos + Vec(-64, -64, 48).rotate(*angles), end_pos + Vec(64, 64, 56).rotate(*angles), mat='tools/toolsinvisible', ).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 = 'tools/toolstrigger'
def res_conveyor_belt(inst: Entity, res: Property): """Create a conveyor belt. Options: SegmentInst: Generated at each square. ('track' is the name of the path.) 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 EnableMotions 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 RailTemplate: A template for the railings. This is made into a non-solid func_brush, combining all sections. """ move_dist = srctools.conv_int(inst.fixup['$travel_distance']) if move_dist <= 2: # There isn't room for a catwalk, so don't bother. inst.remove() return move_dir = Vec(1, 0, 0).rotate_by_str(inst.fixup['$travel_direction']) move_dir.rotate_by_str(inst['angles']) start_offset = srctools.conv_float(inst.fixup['$starting_position'], 0) teleport_to_start = res.bool('TrackTeleport', True) segment_inst_file = res['SegmentInst', ''] rail_template = res['RailTemplate', None] vmf = inst.map if segment_inst_file: segment_inst_file = conditions.resolve_inst(segment_inst_file)[0] 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 # Find the angle which generates an instance pointing in the direction # of movement, with the same normal. norm = Vec(z=1).rotate_by_str(inst['angles']) for roll in range(0, 360, 90): angles = move_dir.to_angle(roll) if Vec(z=1).rotate(*angles) == norm: break else: raise ValueError( "Can't find angles to give a" ' z={} and x={}!'.format(norm, move_dir) ) if res.bool('rotateSegments', True): inst['angles'] = angles else: angles = Vec.from_str(inst['angles']) # 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 beams at the top, so they don't appear inside wall sections. beam_start = start_pos + 48 * norm # type: Vec beam_end = end_pos + 48 * norm # type: Vec for index, pos in enumerate(beam_start.iter_line(beam_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 != end_pos: seg_inst = vmf.create_ent( classname='func_instance', targetname=track_name.format(index), file=segment_inst_file, origin=pos, angles=angles, ) seg_inst.fixup.update(inst.fixup) if rail_template: temp = conditions.import_template( rail_template, pos, angles, force_type=conditions.TEMP_TYPES.world, add_to_map=False, ) rail_temp_solids.extend(temp.world) if rail_temp_solids: vmf.create_ent( classname='func_brush', origin=beam_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. beam_keys = res.find_key('BeamKeys', []) if beam_keys.value: beam = vmf.create_ent(classname='env_beam') # 3 offsets - x = distance from walls, y = side, z = height 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, ).rotate(*angles) beam['TargetPoint'] = end_pos + Vec( +beam_off.x, beam_off.y, beam_off.z, ).rotate(*angles) # 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).rotate(*angles), end_pos + Vec(-72, 56, 144).rotate(*angles), mat='tools/toolstrigger', ).solid) if res.bool('NoPortalFloor'): # Block portals on the floor.. floor_noportal = vmf.create_ent( classname='func_noportal_volume', origin=beam_start, ) floor_noportal.solids.append(vmf.make_prism( start_pos + Vec(-60, -60, -66).rotate(*angles), end_pos + Vec(60, 60, -60).rotate(*angles), mat='tools/toolsinvisible', ).solid) # A brush covering under the platform. base_trig = vmf.make_prism( start_pos + Vec(-64, -64, 48).rotate(*angles), end_pos + Vec(64, 64, 56).rotate(*angles), mat='tools/toolsinvisible', ).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 = 'tools/toolstrigger'
def res_cutout_tile(res: Property): """Generate random quarter tiles, like in Destroyed or Retro maps. - "MarkerItem" is the instance to look for. - "TileSize" can be "2x2" or "4x4". - rotateMax is the amount of degrees to rotate squarebeam models. Materials: - "squarebeams" is the squarebeams variant to use. - "ceilingwalls" are the sides of the ceiling section. - "floorbase" is the texture under floor sections. - "tile_glue" is used on top of a thinner tile segment. - "clip" is the player_clip texture used over floor segments. (This allows customising the surfaceprop.) - "Floor4x4Black", "Ceil2x2White" and other combinations can be used to override the textures used. """ item = instanceLocs.resolve(res['markeritem']) INST_LOCS = {} # Map targetnames -> surface loc CEIL_IO = [] # Pairs of ceil inst corners to cut out. FLOOR_IO = [] # Pairs of floor inst corners to cut out. overlay_ids = {} # When we replace brushes, we need to fix any overlays # on that surface. MATS.clear() floor_edges = [] # Values to pass to add_floor_sides() at the end sign_loc = set(FORCE_LOCATIONS) # If any signage is present in the map, we need to force tiles to # appear at that location! for over in conditions.VMF.by_class['info_overlay']: if (over['material'].casefold() in FORCE_TILE_MATS and # Only check floor/ceiling overlays over['basisnormal'] in ('0 0 1', '0 0 -1')): loc = Vec.from_str(over['origin']) # Sometimes (light bridges etc) a sign will be halfway between # tiles, so in that case we need to force 2 tiles. loc_min = (loc - (15, 15, 0)) // 32 * 32 # type: Vec loc_max = (loc + (15, 15, 0)) // 32 * 32 # type: Vec loc_min += (16, 16, 0) loc_max += (16, 16, 0) FORCE_LOCATIONS.add(loc_min.as_tuple()) FORCE_LOCATIONS.add(loc_max.as_tuple()) SETTINGS = { 'floor_chance': srctools.conv_int(res['floorChance', '100'], 100), 'ceil_chance': srctools.conv_int(res['ceilingChance', '100'], 100), 'floor_glue_chance': srctools.conv_int(res['floorGlueChance', '0']), 'ceil_glue_chance': srctools.conv_int(res['ceilingGlueChance', '0']), 'rotate_beams': int(srctools.conv_float(res['rotateMax', '0']) * BEAM_ROT_PRECISION), 'beam_skin': res['squarebeamsSkin', '0'], 'base_is_disp': srctools.conv_bool(res['dispBase', '0']), 'quad_floor': res['FloorSize', '4x4'].casefold() == '2x2', 'quad_ceil': res['CeilingSize', '4x4'].casefold() == '2x2', } random.seed(vbsp.MAP_RAND_SEED + '_CUTOUT_TILE_NOISE') noise = SimplexNoise(period=4 * 40) # 4 tiles/block, 50 blocks max # We want to know the number of neighbouring tile cutouts before # placing tiles - blocks away from the sides generate fewer tiles. floor_neighbours = defaultdict(dict) # all_floors[z][x,y] = count for mat_prop in res['Materials', []]: MATS[mat_prop.name].append(mat_prop.value) if SETTINGS['base_is_disp']: # We want the normal brushes to become nodraw. MATS['floorbase_disp'] = MATS['floorbase'] MATS['floorbase'] = ['tools/toolsnodraw'] # Since this uses random data for initialisation, the alpha and # regular will use slightly different patterns. alpha_noise = SimplexNoise(period=4 * 50) else: alpha_noise = None for key, default in TEX_DEFAULT: if key not in MATS: MATS[key] = [default] # Find our marker ents for inst in conditions.VMF.by_class['func_instance']: # type: VLib.Entity if inst['file'].casefold() not in item: continue targ = inst['targetname'] orient = Vec(0, 0, 1).rotate_by_str(inst['angles', '0 0 0']) # Check the orientation of the marker to figure out what to generate if orient == (0, 0, 1): io_list = FLOOR_IO else: io_list = CEIL_IO # Reuse orient to calculate where the solid face will be. loc = (orient * -64) + Vec.from_str(inst['origin']) INST_LOCS[targ] = loc for out in inst.output_targets(): io_list.append((targ, out)) if not inst.outputs and inst.fixup['$connectioncount'] == '0': # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 128x128 tile segment. io_list.append((targ, targ)) inst.remove() # Remove the instance itself from the map. for start_floor, end_floor in FLOOR_IO: if end_floor not in INST_LOCS: # Not a marker - remove this and the antline. for toggle in conditions.VMF.by_target[end_floor]: conditions.remove_ant_toggle(toggle) continue box_min = Vec(INST_LOCS[start_floor]) box_min.min(INST_LOCS[end_floor]) box_max = Vec(INST_LOCS[start_floor]) box_max.max(INST_LOCS[end_floor]) if box_min.z != box_max.z: continue # They're not in the same level! z = box_min.z if SETTINGS['rotate_beams']: # We have to generate 1 model per 64x64 block to do rotation... gen_rotated_squarebeams( box_min - (64, 64, 0), box_max + (64, 64, -8), skin=SETTINGS['beam_skin'], max_rot=SETTINGS['rotate_beams'], ) else: # Make the squarebeams props, using big models if possible gen_squarebeams(box_min + (-64, -64, 0), box_max + (64, 64, -8), skin=SETTINGS['beam_skin']) # Add a player_clip brush across the whole area conditions.VMF.add_brush( conditions.VMF.make_prism( p1=box_min - (64, 64, FLOOR_DEPTH), p2=box_max + (64, 64, 0), mat=MATS['clip'][0], ).solid) # Add a noportal_volume covering the surface, in case there's # room for a portal. noportal_solid = conditions.VMF.make_prism( # Don't go all the way to the sides, so it doesn't affect wall # brushes. p1=box_min - (63, 63, 9), p2=box_max + (63, 63, 0), mat='tools/toolsinvisible', ).solid noportal_ent = conditions.VMF.create_ent( classname='func_noportal_volume', origin=box_min.join(' '), ) noportal_ent.solids.append(noportal_solid) if SETTINGS['base_is_disp']: # Use displacements for the base instead. make_alpha_base( box_min + (-64, -64, 0), box_max + (64, 64, 0), noise=alpha_noise, ) for x, y in utils.iter_grid( min_x=int(box_min.x), max_x=int(box_max.x) + 1, min_y=int(box_min.y), max_y=int(box_max.y) + 1, stride=128, ): # Build the set of all positions.. floor_neighbours[z][x, y] = -1 # Mark borders we need to fill in, and the angle (for func_instance) # The wall is the face pointing inwards towards the bottom brush, # and the ceil is the ceiling of the block above the bordering grid # points. for x in range(int(box_min.x), int(box_max.x) + 1, 128): # North floor_edges.append( BorderPoints( wall=Vec(x, box_max.y + 64, z - 64), ceil=Vec_tuple(x, box_max.y + 128, z), rot=270, )) # South floor_edges.append( BorderPoints( wall=Vec(x, box_min.y - 64, z - 64), ceil=Vec_tuple(x, box_min.y - 128, z), rot=90, )) for y in range(int(box_min.y), int(box_max.y) + 1, 128): # East floor_edges.append( BorderPoints( wall=Vec(box_max.x + 64, y, z - 64), ceil=Vec_tuple(box_max.x + 128, y, z), rot=180, )) # West floor_edges.append( BorderPoints( wall=Vec(box_min.x - 64, y, z - 64), ceil=Vec_tuple(box_min.x - 128, y, z), rot=0, )) # Now count boundries near tiles, then generate them. # Do it seperately for each z-level: for z, xy_dict in floor_neighbours.items(): # type: float, dict for x, y in xy_dict: # type: float, float # We want to count where there aren't any tiles xy_dict[x, y] = (((x - 128, y - 128) not in xy_dict) + ((x - 128, y + 128) not in xy_dict) + ((x + 128, y - 128) not in xy_dict) + ((x + 128, y + 128) not in xy_dict) + ((x - 128, y) not in xy_dict) + ((x + 128, y) not in xy_dict) + ((x, y - 128) not in xy_dict) + ((x, y + 128) not in xy_dict)) max_x = max_y = 0 weights = {} # Now the counts are all correct, compute the weight to apply # for tiles. # Adding the neighbouring counts will make a 5x5 area needed to set # the center to 0. for (x, y), cur_count in xy_dict.items(): max_x = max(x, max_x) max_y = max(y, max_y) # Orthrogonal is worth 0.2, diagonal is worth 0.1. # Not-present tiles would be 8 - the maximum tile_count = (0.8 * cur_count + 0.1 * xy_dict.get( (x - 128, y - 128), 8) + 0.1 * xy_dict.get( (x - 128, y + 128), 8) + 0.1 * xy_dict.get( (x + 128, y - 128), 8) + 0.1 * xy_dict.get( (x + 128, y + 128), 8) + 0.2 * xy_dict.get( (x - 128, y), 8) + 0.2 * xy_dict.get( (x, y - 128), 8) + 0.2 * xy_dict.get( (x, y + 128), 8) + 0.2 * xy_dict.get( (x + 128, y), 8)) # The number ranges from 0 (all tiles) to 12.8 (no tiles). # All tiles should still have a small chance to generate tiles. weights[x, y] = min((tile_count + 0.5) / 8, 1) # Share the detail entity among same-height tiles.. detail_ent = conditions.VMF.create_ent(classname='func_detail', ) for x, y in xy_dict: convert_floor( Vec(x, y, z), overlay_ids, MATS, SETTINGS, sign_loc, detail_ent, noise_weight=weights[x, y], noise_func=noise, ) add_floor_sides(floor_edges) conditions.reallocate_overlays(overlay_ids) return conditions.RES_EXHAUSTED
def mode_func(comp_ent: Entity, ent: Entity) -> str: """Rotate the axis by the given value.""" pos = Vec.from_str(ent['origin']) scale = conv_float(comp_ent['const'], 1.0) return str(pos[axis] * scale)
def mode_func(comp_ent: Entity, ent: Entity) -> str: """Rotate the axis by the given value.""" out = norm.copy().rotate_by_str(ent['angles', '0 0 0']) scale = conv_float(comp_ent['const'], 1.0) return vs_vec(scale * out)
def mode_dist(comp_ent: Entity, ent: Entity) -> str: """Return the distance from the ent to the reference.""" scale = conv_float(comp_ent['const'], 1.0) offset = Vec.from_str(ent['origin']) - Vec.from_str(comp_ent['origin']) return offset.mag() * scale
def mode_off(comp_ent: Entity, ent: Entity) -> str: """Return the offset from the ent to the reference.""" scale = conv_float(comp_ent['const'], 1.0) offset = Vec.from_str(ent['origin']) - Vec.from_str(comp_ent['origin']) return vs_vec(offset * scale)
def mode_pos(comp_ent: Entity, ent: Entity) -> str: """Return the position of the entity.""" pos = Vec.from_str(ent['origin']) scale = conv_float(comp_ent['const'], 1.0) return vs_vec(scale * pos)
def res_cutout_tile(res: Property): """Generate random quarter tiles, like in Destroyed or Retro maps. - "MarkerItem" is the instance to look for. - "TileSize" can be "2x2" or "4x4". - rotateMax is the amount of degrees to rotate squarebeam models. Materials: - "squarebeams" is the squarebeams variant to use. - "ceilingwalls" are the sides of the ceiling section. - "floorbase" is the texture under floor sections. - "tile_glue" is used on top of a thinner tile segment. - "clip" is the player_clip texture used over floor segments. (This allows customising the surfaceprop.) - "Floor4x4Black", "Ceil2x2White" and other combinations can be used to override the textures used. """ item = resolve_inst(res['markeritem']) INST_LOCS = {} # Map targetnames -> surface loc CEIL_IO = [] # Pairs of ceil inst corners to cut out. FLOOR_IO = [] # Pairs of floor inst corners to cut out. overlay_ids = {} # When we replace brushes, we need to fix any overlays # on that surface. MATS.clear() floor_edges = [] # Values to pass to add_floor_sides() at the end sign_loc = set(FORCE_LOCATIONS) # If any signage is present in the map, we need to force tiles to # appear at that location! for over in conditions.VMF.by_class['info_overlay']: if ( over['material'].casefold() in FORCE_TILE_MATS and # Only check floor/ceiling overlays over['basisnormal'] in ('0 0 1', '0 0 -1') ): loc = Vec.from_str(over['origin']) # Sometimes (light bridges etc) a sign will be halfway between # tiles, so in that case we need to force 2 tiles. loc_min = (loc - (15, 15, 0)) // 32 * 32 # type: Vec loc_max = (loc + (15, 15, 0)) // 32 * 32 # type: Vec loc_min += (16, 16, 0) loc_max += (16, 16, 0) FORCE_LOCATIONS.add(loc_min.as_tuple()) FORCE_LOCATIONS.add(loc_max.as_tuple()) SETTINGS = { 'floor_chance': srctools.conv_int( res['floorChance', '100'], 100), 'ceil_chance': srctools.conv_int( res['ceilingChance', '100'], 100), 'floor_glue_chance': srctools.conv_int( res['floorGlueChance', '0']), 'ceil_glue_chance': srctools.conv_int( res['ceilingGlueChance', '0']), 'rotate_beams': int(srctools.conv_float( res['rotateMax', '0']) * BEAM_ROT_PRECISION), 'beam_skin': res['squarebeamsSkin', '0'], 'base_is_disp': srctools.conv_bool(res['dispBase', '0']), 'quad_floor': res['FloorSize', '4x4'].casefold() == '2x2', 'quad_ceil': res['CeilingSize', '4x4'].casefold() == '2x2', } random.seed(vbsp.MAP_RAND_SEED + '_CUTOUT_TILE_NOISE') noise = SimplexNoise(period=4 * 40) # 4 tiles/block, 50 blocks max # We want to know the number of neighbouring tile cutouts before # placing tiles - blocks away from the sides generate fewer tiles. floor_neighbours = defaultdict(dict) # all_floors[z][x,y] = count for mat_prop in res['Materials', []]: MATS[mat_prop.name].append(mat_prop.value) if SETTINGS['base_is_disp']: # We want the normal brushes to become nodraw. MATS['floorbase_disp'] = MATS['floorbase'] MATS['floorbase'] = ['tools/toolsnodraw'] # Since this uses random data for initialisation, the alpha and # regular will use slightly different patterns. alpha_noise = SimplexNoise(period=4 * 50) else: alpha_noise = None for key, default in TEX_DEFAULT: if key not in MATS: MATS[key] = [default] # Find our marker ents for inst in conditions.VMF.by_class['func_instance']: # type: VLib.Entity if inst['file'].casefold() not in item: continue targ = inst['targetname'] orient = Vec(0, 0, 1).rotate_by_str(inst['angles', '0 0 0']) # Check the orientation of the marker to figure out what to generate if orient == (0, 0, 1): io_list = FLOOR_IO else: io_list = CEIL_IO # Reuse orient to calculate where the solid face will be. loc = (orient * -64) + Vec.from_str(inst['origin']) INST_LOCS[targ] = loc for out in inst.output_targets(): io_list.append((targ, out)) if not inst.outputs and inst.fixup['$connectioncount'] == '0': # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 128x128 tile segment. io_list.append((targ, targ)) inst.remove() # Remove the instance itself from the map. for start_floor, end_floor in FLOOR_IO: if end_floor not in INST_LOCS: # Not a marker - remove this and the antline. for toggle in conditions.VMF.by_target[end_floor]: conditions.remove_ant_toggle(toggle) continue box_min = Vec(INST_LOCS[start_floor]) box_min.min(INST_LOCS[end_floor]) box_max = Vec(INST_LOCS[start_floor]) box_max.max(INST_LOCS[end_floor]) if box_min.z != box_max.z: continue # They're not in the same level! z = box_min.z if SETTINGS['rotate_beams']: # We have to generate 1 model per 64x64 block to do rotation... gen_rotated_squarebeams( box_min - (64, 64, 0), box_max + (64, 64, -8), skin=SETTINGS['beam_skin'], max_rot=SETTINGS['rotate_beams'], ) else: # Make the squarebeams props, using big models if possible gen_squarebeams( box_min + (-64, -64, 0), box_max + (64, 64, -8), skin=SETTINGS['beam_skin'] ) # Add a player_clip brush across the whole area conditions.VMF.add_brush(conditions.VMF.make_prism( p1=box_min - (64, 64, FLOOR_DEPTH), p2=box_max + (64, 64, 0), mat=MATS['clip'][0], ).solid) # Add a noportal_volume covering the surface, in case there's # room for a portal. noportal_solid = conditions.VMF.make_prism( # Don't go all the way to the sides, so it doesn't affect wall # brushes. p1=box_min - (63, 63, 9), p2=box_max + (63, 63, 0), mat='tools/toolsinvisible', ).solid noportal_ent = conditions.VMF.create_ent( classname='func_noportal_volume', origin=box_min.join(' '), ) noportal_ent.solids.append(noportal_solid) if SETTINGS['base_is_disp']: # Use displacements for the base instead. make_alpha_base( box_min + (-64, -64, 0), box_max + (64, 64, 0), noise=alpha_noise, ) for x, y in utils.iter_grid( min_x=int(box_min.x), max_x=int(box_max.x) + 1, min_y=int(box_min.y), max_y=int(box_max.y) + 1, stride=128, ): # Build the set of all positions.. floor_neighbours[z][x, y] = -1 # Mark borders we need to fill in, and the angle (for func_instance) # The wall is the face pointing inwards towards the bottom brush, # and the ceil is the ceiling of the block above the bordering grid # points. for x in range(int(box_min.x), int(box_max.x) + 1, 128): # North floor_edges.append(BorderPoints( wall=Vec(x, box_max.y + 64, z - 64), ceil=Vec_tuple(x, box_max.y + 128, z), rot=270, )) # South floor_edges.append(BorderPoints( wall=Vec(x, box_min.y - 64, z - 64), ceil=Vec_tuple(x, box_min.y - 128, z), rot=90, )) for y in range(int(box_min.y), int(box_max.y) + 1, 128): # East floor_edges.append(BorderPoints( wall=Vec(box_max.x + 64, y, z - 64), ceil=Vec_tuple(box_max.x + 128, y, z), rot=180, )) # West floor_edges.append(BorderPoints( wall=Vec(box_min.x - 64, y, z - 64), ceil=Vec_tuple(box_min.x - 128, y, z), rot=0, )) # Now count boundries near tiles, then generate them. # Do it seperately for each z-level: for z, xy_dict in floor_neighbours.items(): # type: float, dict for x, y in xy_dict: # type: float, float # We want to count where there aren't any tiles xy_dict[x, y] = ( ((x - 128, y - 128) not in xy_dict) + ((x - 128, y + 128) not in xy_dict) + ((x + 128, y - 128) not in xy_dict) + ((x + 128, y + 128) not in xy_dict) + ((x - 128, y) not in xy_dict) + ((x + 128, y) not in xy_dict) + ((x, y - 128) not in xy_dict) + ((x, y + 128) not in xy_dict) ) max_x = max_y = 0 weights = {} # Now the counts are all correct, compute the weight to apply # for tiles. # Adding the neighbouring counts will make a 5x5 area needed to set # the center to 0. for (x, y), cur_count in xy_dict.items(): max_x = max(x, max_x) max_y = max(y, max_y) # Orthrogonal is worth 0.2, diagonal is worth 0.1. # Not-present tiles would be 8 - the maximum tile_count = ( 0.8 * cur_count + 0.1 * xy_dict.get((x - 128, y - 128), 8) + 0.1 * xy_dict.get((x - 128, y + 128), 8) + 0.1 * xy_dict.get((x + 128, y - 128), 8) + 0.1 * xy_dict.get((x + 128, y + 128), 8) + 0.2 * xy_dict.get((x - 128, y), 8) + 0.2 * xy_dict.get((x, y - 128), 8) + 0.2 * xy_dict.get((x, y + 128), 8) + 0.2 * xy_dict.get((x + 128, y), 8) ) # The number ranges from 0 (all tiles) to 12.8 (no tiles). # All tiles should still have a small chance to generate tiles. weights[x, y] = min((tile_count + 0.5) / 8, 1) # Share the detail entity among same-height tiles.. detail_ent = conditions.VMF.create_ent( classname='func_detail', ) for x, y in xy_dict: convert_floor( Vec(x, y, z), overlay_ids, MATS, SETTINGS, sign_loc, detail_ent, noise_weight=weights[x, y], noise_func=noise, ) add_floor_sides(floor_edges) conditions.reallocate_overlays(overlay_ids) return conditions.RES_EXHAUSTED
def comp_trigger_goo(ctx: Context): """Creates triggers for Toxic Goo.""" reloader_cache = { } # type: Dict[Tuple[float, float, float, float], Entity] for trig in ctx.vmf.by_class['comp_trigger_p2_goo']: trig.remove() outputs = trig.outputs.copy() trig.outputs.clear() failsafe_delay = conv_float(trig['failsafe_delay'], 0.5) if failsafe_delay < 0.01: failsafe_delay = 0.01 hurt = trig.copy() diss = trig.copy() ctx.vmf.add_ents([hurt, diss]) spawnflags = conv_int(trig['spawnflags']) for keyvalue in [ 'dissolve_filter', 'phys_offset', 'failsafe_delay', 'fadepreset', 'fadecolor', 'fadetime', ]: del diss[keyvalue], hurt[keyvalue] diss['classname'] = 'trigger_multiple' # No clients, add physics. But otherwise leave it to the user. diss['spawnflags'] = (spawnflags & ~1) | 8 diss['wait'] = 0 # No delay. diss['filtername'] = trig['dissolve_filter'] del diss['damagetype'] diss_pos = Vec.from_str(diss['origin']) diss_pos.z -= conv_float(trig['phys_offset']) diss['origin'] = diss_pos hurt['spawnflags'] = 1 # Players. if conv_bool(trig['enablefade']): fade_time = conv_float(trig['fadetime']) fade_color = Vec.from_str(trig['fadepreset']) if fade_color == (-1, -1, -1): fade_color = Vec.from_str(trig['fadecolor']) fade_key = fade_color.x, fade_color.y, fade_color.z, fade_time try: reloader = reloader_cache[fade_key] except KeyError: reloader = reloader_cache[fade_key] = ctx.vmf.create_ent( 'player_loadsaved', origin=diss['origin'], rendercolor=str(fade_color), renderamt=255, duration=fade_time, holdtime=10, loadtime=fade_time + 0.1, ) reloader.make_unique('reloader') hurt['classname'] = 'trigger_once' del hurt['damagetype'] hurt.add_out( Output('OnStartTouch', reloader, 'Reload', only_once=True)) # Make sure the failsafe delay is longer than the total fade time. failsafe_delay = min(failsafe_delay, fade_time + 0.15) else: hurt['classname'] = 'trigger_hurt' hurt['damage'] = hurt['damagecap'] = 10000 hurt['damagemodel'] = 0 # No doubling hurt['nodmgforce'] = 1 # Don't throw players around. for out in outputs: if out.output.casefold() == 'onkillplayer': # Better than OnStartTouch, doesn't apply for god mode. out.output = 'OnHurtPlayer' hurt.add_out(out) elif out.output.casefold() == 'ondissolvephysics': out.output = 'OnStartTouch' diss.add_out(out) diss.add_out( Output('OnStartTouch', '!activator', 'SilentDissolve'), Output('OnStartTouch', '!activator', 'Kill', delay=failsafe_delay), )
def _parse_value(value: str) -> float: return conv_float(value)