def res_cust_antline_setup(res: Property): def find(cat): """Helper to reduce code duplication.""" return [p.value for p in res.find_all(cat)] # Allow overriding these options. If unset use the style's value - the # amount of destruction will usually be the same. broken_chance = res.float( 'broken_antline_chance', vbsp_options.get(float, 'broken_antline_chance'), ) broken_dist = res.int( 'broken_antline_distance', vbsp_options.get(int, 'broken_antline_distance'), ) toggle_inst = res['instance', ''] toggle_out = list(res.find_all('addOut')) # These textures are required - the base ones. straight_tex = find('straight') corner_tex = find('corner') # Arguments to pass to setAntlineMat straight_args = [ straight_tex, find('straightFloor') or (), # Extra broken antline textures / options, if desired. broken_chance, broken_dist, find('brokenStraight') or (), find('brokenStraightFloor') or (), ] # The same but for corners. corner_args = [ corner_tex, find('cornerFloor') or (), broken_chance, broken_dist, find('brokenCorner') or (), find('brokenCornerFloor') or (), ] if not straight_tex or not corner_tex: # If we don't have two textures, something's wrong. Remove this result. LOGGER.warning('custAntline has no textures!') return None else: return straight_args, corner_args, toggle_inst, toggle_out
def res_make_tag_fizzler_setup(res: Property): """We need this to pre-parse the fizzler type.""" if 'ioconf' in res: fizz_conn = Config.parse('<TAG_FIZZER>', res.find_key('ioconf')) else: fizz_conn = None # The distance from origin the double signs are seperated by. sign_offset = res.int('signoffset', 16) return ( sign_offset, fizz_conn, res['frame_double'], res['frame_single'], res['blue_sign', ''], res['blue_off_sign', ''], res['oran_sign', ''], res['oran_off_sign', ''], )
def res_make_tag_fizzler_setup(res: Property): """We need this to pre-parse the fizzler type.""" if 'ioconf' in res: fizz_type = ItemType.parse('<TAG_FIZZER>', res.find_key('ioconf')) else: fizz_type = None # The distance from origin the double signs are seperated by. sign_offset = res.int('signoffset', 16) return ( sign_offset, fizz_type, res['frame_double'], res['frame_single'], res['blue_sign', ''], res['blue_off_sign', ''], res['oran_sign', ''], res['oran_off_sign', ''], )
def widget_minute_seconds(parent: tk.Frame, var: tk.StringVar, conf: Property) -> tk.Widget: """A widget for specifying times - minutes and seconds. The value is saved as seconds. Max specifies the largest amount. """ max_value = conf.int('max', 60) min_value = conf.int('min', 0) if min_value > max_value: raise ValueError('Bad min and max values!') values = timer_values(min_value, max_value) # Stores the 'pretty' value in the actual textbox. disp_var = tk.StringVar() existing_value = var.get() def update_disp(var_name: str, var_index: str, operation: str) -> None: """Whenever the string changes, update the displayed text.""" seconds = conv_int(var.get(), -1) if min_value <= seconds <= max_value: disp_var.set('{}:{:02}'.format(seconds // 60, seconds % 60)) else: LOGGER.warning('Bad timer value "{}" for "{}"!', var.get(), conf['id']) # Recurse, with a known safe value. var.set(values[0]) # Whenever written to, call this. var.trace_add('write', update_disp) def set_var(): """Set the variable to the current value.""" try: minutes, seconds = disp_var.get().split(':') var.set(str(int(minutes) * 60 + int(seconds))) except (ValueError, TypeError): pass def validate(reason: str, operation_type: str, cur_value: str, new_char: str, new_value: str): """Validate the values for the text. This is called when the textbox is modified, to allow cancelling bad inputs. Reason is the reason this was fired: 'key', 'focusin', 'focusout', 'forced'. operation_type is '1' for insert, '0' for delete', '-1' for programmatic changes. cur_val is the value before the change occurs. new_char is the added/removed text. new_value is the value after the change, if accepted. """ if operation_type == '0' or reason == 'forced': # Deleting or done by the program, allow that always. return True if operation_type == '1': # Inserted text. # Disallow non number and colons if new_char not in '0123456789:': return False # Only one colon. if ':' in cur_value and new_char == ':': return False # Don't allow more values if it has more than 2 numbers after # the colon - if there is one, and it's not in the last 3 characters. if ':' in new_value and ':' not in new_value[-3:]: return False if reason == 'focusout': # When leaving focus, apply range limits and set the var. try: str_min, str_sec = new_value.split(':') seconds = int(str_min) * 60 + int(str_sec) except (ValueError, TypeError): seconds = min_value else: if seconds < min_value: seconds = min_value if seconds > max_value: seconds = max_value var.set(str(seconds)) # This then re-writes the textbox. return True validate_cmd = parent.register(validate) spinbox = tk.Spinbox( parent, exportselection=False, textvariable=disp_var, command=set_var, wrap=True, values=values, width=5, validate='all', # These define which of the possible values will be passed along. # http://tcl.tk/man/tcl8.6/TkCmd/spinbox.htm#M26 validatecommand=(validate_cmd, '%V', '%d', '%s', '%S', '%P'), ) # We need to set this after, it gets reset to the first one. var.set(existing_value) return spinbox
def res_make_tag_fizzler(vmf: VMF, inst: Entity, res: Property): """Add an Aperture Tag Paint Gun activation fizzler. These fizzlers are created via signs, and work very specially. MUST be priority -100 so it runs before fizzlers! """ import vbsp if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']: # Abort - TAG fizzlers shouldn't appear in any other game! inst.remove() return fizz_base = fizz_name = None # Look for the fizzler instance we want to replace for targetname in inst.output_targets(): if targetname in tag_fizzlers: fizz_name = targetname fizz_base = tag_fizzlers[targetname] del tag_fizzlers[targetname] # Don't let other signs mod this one! continue else: # It's an indicator toggle, remove it and the antline to clean up. LOGGER.warning('Toggle: {}', targetname) for ent in vmf.by_target[targetname]: remove_ant_toggle(ent) inst.outputs.clear() # Remove the outptuts now, they're not valid anyway. if fizz_base is None: # No fizzler - remove this sign inst.remove() return # The distance from origin the double signs are seperated by. sign_offset = res.int('signoffset', 16) sign_loc = ( # The actual location of the sign - on the wall Vec.from_str(inst['origin']) + Vec(0, 0, -64).rotate_by_str(inst['angles']) ) # Now deal with the visual aspect: # Blue signs should be on top. blue_enabled = inst.fixup.bool('$start_enabled') oran_enabled = inst.fixup.bool('$start_reversed') # If True, single-color signs will also turn off the other color. # This also means we always show both signs. # If both are enabled or disabled, this has no effect. disable_other = ( not inst.fixup.bool('$disable_autorespawn', True) and blue_enabled != oran_enabled ) # Delete fixups now, they aren't useful. inst.fixup.clear() if not blue_enabled and not oran_enabled: # Hide the sign in this case! inst.remove() inst_angle = srctools.parse_vec_str(inst['angles']) inst_normal = Vec(0, 0, 1).rotate(*inst_angle) loc = Vec.from_str(inst['origin']) if disable_other or (blue_enabled and oran_enabled): inst['file'] = res['frame_double'] # On a wall, and pointing vertically if inst_normal.z == 0 and Vec(y=1).rotate(*inst_angle).z: # They're vertical, make sure blue's on top! blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset) oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset) # If orange is enabled, with two frames put that on top # instead since it's more important if disable_other and oran_enabled: blue_loc, oran_loc = oran_loc, blue_loc else: offset = Vec(0, sign_offset, 0).rotate(*inst_angle) blue_loc = loc + offset oran_loc = loc - offset else: inst['file'] = res['frame_single'] # They're always centered blue_loc = loc oran_loc = loc if inst_normal.z != 0: # If on floors/ceilings, rotate to point at the fizzler! sign_floor_loc = sign_loc.copy() sign_floor_loc.z = 0 # We don't care about z-positions. # Grab the data saved earlier in res_find_potential_tag_fizzlers() axis, side_min, side_max, normal = tag_fizzler_locs[fizz_name] # The Z-axis fizzler (horizontal) must be treated differently. if axis == 'z': # For z-axis, just compare to the center point. # The values are really x, y, z, not what they're named. sign_dir = sign_floor_loc - (side_min, side_max, normal) else: # For the other two, we compare to the line, # or compare to the closest side (in line with the fizz) other_axis = 'x' if axis == 'y' else 'y' if abs(sign_floor_loc[other_axis] - normal) < 32: # Compare to the closest side. Use ** to swap x/y arguments # appropriately. The closest side is the one with the # smallest magnitude. vmf.create_ent( classname='info_null', targetname=inst['targetname'] + '_min', origin=sign_floor_loc - Vec(**{ axis: side_min, other_axis: normal, }), ) vmf.create_ent( classname='info_null', targetname=inst['targetname'] + '_max', origin=sign_floor_loc - Vec(**{ axis: side_max, other_axis: normal, }), ) sign_dir = min( sign_floor_loc - Vec(**{ axis: side_min, other_axis: normal, }), sign_floor_loc - Vec(**{ axis: side_max, other_axis: normal, }), key=Vec.mag, ) else: # Align just based on whether we're in front or behind. sign_dir = Vec() sign_dir[other_axis] = sign_floor_loc[other_axis] - normal sign_angle = math.degrees( math.atan2(sign_dir.y, sign_dir.x) ) # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals sign_angle = (sign_angle + 45) // 90 * 90 # Rotate to fit the instances - south is down sign_angle = int(sign_angle + 90) % 360 if inst_normal.z > 0: sign_angle = '0 {} 0'.format(sign_angle) elif inst_normal.z < 0: # Flip upside-down for ceilings sign_angle = '0 {} 180'.format(sign_angle) else: # On a wall, face upright sign_angle = PETI_INST_ANGLE[inst_normal.as_tuple()] # If disable_other, we show off signs. Otherwise we don't use that sign. blue_sign = 'blue_sign' if blue_enabled else 'blue_off_sign' if disable_other else None oran_sign = 'oran_sign' if oran_enabled else 'oran_off_sign' if disable_other else None if blue_sign: vmf.create_ent( classname='func_instance', file=res[blue_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=blue_loc.join(' '), ) if oran_sign: vmf.create_ent( classname='func_instance', file=res[oran_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=oran_loc.join(' '), ) # Now modify the fizzler... fizz_brushes = list( vmf.by_class['trigger_portal_cleanser'] & vmf.by_target[fizz_name + '_brush'] ) if 'base_inst' in res: fizz_base['file'] = resolve_inst(res['base_inst'])[0] fizz_base.outputs.clear() # Remove outputs, otherwise they break # branch_toggle entities # Subtract the sign from the list of connections, but don't go below # zero fizz_base.fixup['$connectioncount'] = str(max( 0, srctools.conv_int(fizz_base.fixup['$connectioncount', ''], 0) - 1 )) if 'model_inst' in res: model_inst = resolve_inst(res['model_inst'])[0] for mdl_inst in vmf.by_class['func_instance']: if mdl_inst['targetname', ''].startswith(fizz_name + '_model'): mdl_inst['file'] = model_inst # Find the direction the fizzler front/back points - z=floor fizz # Signs will associate with the given side! bbox_min, bbox_max = fizz_brushes[0].get_bbox() for axis, val in zip('xyz', bbox_max-bbox_min): if val == 2: fizz_axis = axis sign_center = (bbox_min[axis] + bbox_max[axis]) / 2 break else: # A fizzler that's not 128*x*2? raise Exception('Invalid fizzler brush ({})!'.format(fizz_name)) # Figure out what the sides will set values to... pos_blue = False pos_oran = False neg_blue = False neg_oran = False if sign_loc[fizz_axis] < sign_center: pos_blue = blue_enabled pos_oran = oran_enabled else: neg_blue = blue_enabled neg_oran = oran_enabled fizz_off_tex = { 'left': res['off_left'], 'center': res['off_center'], 'right': res['off_right'], 'short': res['off_short'], } fizz_on_tex = { 'left': res['on_left'], 'center': res['on_center'], 'right': res['on_right'], 'short': res['on_short'], } # If it activates the paint gun, use different textures if pos_blue or pos_oran: pos_tex = fizz_on_tex else: pos_tex = fizz_off_tex if neg_blue or neg_oran: neg_tex = fizz_on_tex else: neg_tex = fizz_off_tex if vbsp.GAME_MODE == 'COOP': # We need ATLAS-specific triggers pos_trig = vmf.create_ent( classname='trigger_playerteam', ) neg_trig = vmf.create_ent( classname='trigger_playerteam', ) output = 'OnStartTouchBluePlayer' else: pos_trig = vmf.create_ent( classname='trigger_multiple', ) neg_trig = vmf.create_ent( classname='trigger_multiple', spawnflags='1', ) output = 'OnStartTouch' pos_trig['origin'] = neg_trig['origin'] = fizz_base['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = fizz_name + '-trig_pos' neg_trig['targetname'] = fizz_name + '-trig_neg' pos_trig.outputs = [ Output( output, fizz_name + '-trig_neg', 'Enable', ), Output( output, fizz_name + '-trig_pos', 'Disable', ), ] neg_trig.outputs = [ Output( output, fizz_name + '-trig_pos', 'Enable', ), Output( output, fizz_name + '-trig_neg', 'Disable', ), ] voice_attr = vbsp.settings['has_attr'] if blue_enabled or disable_other: # If this is blue/oran only, don't affect the other color neg_trig.outputs.append(Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_blue), )) pos_trig.outputs.append(Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_blue), )) if blue_enabled: # Add voice attributes - we have the gun and gel! voice_attr['bluegelgun'] = True voice_attr['bluegel'] = True voice_attr['bouncegun'] = True voice_attr['bouncegel'] = True if oran_enabled or disable_other: neg_trig.outputs.append(Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_oran), )) pos_trig.outputs.append(Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_oran), )) if oran_enabled: voice_attr['orangegelgun'] = True voice_attr['orangegel'] = True voice_attr['speedgelgun'] = True voice_attr['speedgel'] = True if not oran_enabled and not blue_enabled: # If both are disabled, we must shutdown the gun when touching # either side - use neg_trig for that purpose! # We want to get rid of pos_trig to save ents vmf.remove_ent(pos_trig) neg_trig['targetname'] = fizz_name + '-trig' neg_trig.outputs.clear() neg_trig.add_out(Output( output, '@BlueIsEnabled', 'SetValue', param='0' )) neg_trig.add_out(Output( output, '@OrangeIsEnabled', 'SetValue', param='0' )) for fizz_brush in fizz_brushes: # portal_cleanser ent, not solid! # Modify fizzler textures bbox_min, bbox_max = fizz_brush.get_bbox() for side in fizz_brush.sides(): norm = side.normal() if norm[fizz_axis] == 0: # Not the front/back: force nodraw # Otherwise the top/bottom will have the odd stripes # which won't match the sides side.mat = 'tools/toolsnodraw' continue if norm[fizz_axis] == 1: side.mat = pos_tex[ vbsp.TEX_FIZZLER[ side.mat.casefold() ] ] else: side.mat = neg_tex[ vbsp.TEX_FIZZLER[ side.mat.casefold() ] ] # The fizzler shouldn't kill cubes fizz_brush['spawnflags'] = '1' fizz_brush.outputs.append(Output( 'OnStartTouch', '@shake_global', 'StartShake', )) fizz_brush.outputs.append(Output( 'OnStartTouch', '@shake_global_sound', 'PlaySound', )) # The triggers are 8 units thick, 24 from the center # (-1 because fizzlers are 2 thick on each side). neg_min, neg_max = Vec(bbox_min), Vec(bbox_max) neg_min[fizz_axis] -= 23 neg_max[fizz_axis] -= 17 pos_min, pos_max = Vec(bbox_min), Vec(bbox_max) pos_min[fizz_axis] += 17 pos_max[fizz_axis] += 23 if blue_enabled or oran_enabled: neg_trig.solids.append( vmf.make_prism( neg_min, neg_max, mat='tools/toolstrigger', ).solid, ) pos_trig.solids.append( vmf.make_prism( pos_min, pos_max, mat='tools/toolstrigger', ).solid, ) else: # If neither enabled, use one trigger neg_trig.solids.append( vmf.make_prism( neg_min, pos_max, mat='tools/toolstrigger', ).solid, )
def widget_minute_seconds(parent: tk.Frame, var: tk.StringVar, conf: Property) -> tk.Misc: """A widget for specifying times - minutes and seconds. The value is saved as seconds. Max specifies the largest amount. """ max_value = conf.int('max', 60) min_value = conf.int('min', 0) if min_value > max_value: raise ValueError('Bad min and max values!') values = timer_values(min_value, max_value) default_value = conv_int(var.get(), -1) if min_value <= default_value <= max_value: default_text = values[default_value - min_value] else: LOGGER.warning('Bad timer value "{}" for "{}"!', var.get(), conf['id']) default_text = '0:01' var.set('1') disp_var = tk.StringVar() def set_var(): """Set the variable to the current value.""" try: minutes, seconds = disp_var.get().split(':') var.set(int(minutes) * 60 + int(seconds)) except (ValueError, TypeError): pass def validate(reason: str, operation_type: str, cur_value: str, new_char: str, new_value: str): """Validate the values for the text.""" if operation_type == '0' or reason == 'forced': # Deleting or done by the program, allow that always. return True if operation_type == '1': # Disallow non number and colons if new_char not in '0123456789:': return False # Only one colon. if ':' in cur_value and new_char == ':': return False # Don't allow more values if it has more than 2 numbers after # the colon - if there is one, and it's not in the last 3 characters. if ':' in new_value and ':' not in new_value[-3:]: return False if reason == 'focusout': # When leaving focus, apply range limits and set the var. try: minutes, seconds = new_value.split(':') seconds = int(minutes) * 60 + int(seconds) except (ValueError, TypeError): seconds = default_value if seconds < min_value: seconds = min_value if seconds > max_value: seconds = max_value disp_var.set('{}:{:02}'.format(seconds // 60, seconds % 60)) var.set(seconds) return True validate_cmd = parent.register(validate) spinbox = tk.Spinbox( parent, exportselection=False, textvariable=disp_var, command=set_var, wrap=True, values=values, validate='all', # %args substitute the values for the args to validate_cmd. validatecommand=(validate_cmd, '%V', '%d', '%s', '%S', '%P'), ) # We need to set this after, it gets reset to the first one. disp_var.set(default_text) return spinbox
def res_goo_debris(res: Property): """Add random instances to goo squares. Options: - file: The filename for the instance. The variant files should be suffixed with '_1.vmf', '_2.vmf', etc. - space: the number of border squares which must be filled with goo for a square to be eligible - defaults to 1. - weight, number: see the 'Variant' result, a set of weights for the options - chance: The percentage chance a square will have a debris item - offset: A random xy offset applied to the instances. """ import brushLoc space = res.int('spacing', 1) rand_count = res.int('number', None) if rand_count: rand_list = weighted_random( rand_count, res['weights', ''], ) else: rand_list = None chance = res.int('chance', 30) / 100 file = res['file'] offset = res.int('offset', 0) if file.endswith('.vmf'): file = file[:-4] goo_top_locs = { pos.as_tuple() for pos, block in brushLoc.POS.items() if block.is_goo and block.is_top } if space == 0: # No spacing needed, just copy possible_locs = [Vec(loc) for loc in goo_top_locs] else: possible_locs = [] for x, y, z in goo_top_locs: # Check to ensure the neighbouring blocks are also # goo brushes (depending on spacing). for x_off, y_off in utils.iter_grid( min_x=-space, max_x=space + 1, min_y=-space, max_y=space + 1, stride=1, ): if x_off == y_off == 0: continue # We already know this is a goo location if (x + x_off, y + y_off, z) not in goo_top_locs: break # This doesn't qualify else: possible_locs.append(brushLoc.grid_to_world(Vec(x, y, z))) LOGGER.info( 'GooDebris: {}/{} locations', len(possible_locs), len(goo_top_locs), ) suff = '' for loc in possible_locs: random.seed('goo_debris_{}_{}_{}'.format(loc.x, loc.y, loc.z)) if random.random() > chance: continue if rand_list is not None: suff = '_' + str(random.choice(rand_list) + 1) if offset > 0: loc.x += random.randint(-offset, offset) loc.y += random.randint(-offset, offset) loc.z -= 32 # Position the instances in the center of the 128 grid. VMF.create_ent(classname='func_instance', file=file + suff + '.vmf', origin=loc.join(' '), angles='0 {} 0'.format(random.randrange(0, 3600) / 10)) return RES_EXHAUSTED
def res_make_tag_fizzler(vmf: VMF, res: Property): """Add an Aperture Tag Paint Gun activation fizzler. These fizzlers are created via signs, and work very specially. This must be before -250 so it runs before fizzlers and connections. """ if 'ioconf' in res: fizz_conn_conf = Config.parse('<TAG_FIZZER>', res.find_key('ioconf')) else: fizz_conn_conf = None # The distance from origin the double signs are seperated by. sign_offset = res.int('signoffset', 16) inst_frame_double = res['frame_double'] inst_frame_single = res['frame_single'] blue_sign_on = res['blue_sign', ''] blue_sign_off = res['blue_off_sign', ''] oran_sign_on = res['oran_sign', ''] oran_sign_off = res['oran_off_sign', ''] import vbsp if options.get(str, 'game_id') != utils.STEAM_IDS['TAG']: # Abort - TAG fizzlers shouldn't appear in any other game! # So simply remove the fizzler. return Entity.remove def make_tag_fizz(inst: Entity) -> None: """Create the Tag fizzler.""" fizzler: Optional[Fizzler] = None fizzler_item: Optional[Item] = None # Look for the fizzler instance we want to replace. sign_item = connections.ITEMS[inst['targetname']] for conn in list(sign_item.outputs): if conn.to_item.name in FIZZLERS: if fizzler is None: fizzler = FIZZLERS[conn.to_item.name] fizzler_item = conn.to_item else: raise ValueError('Multiple fizzlers attached to a sign!') conn.remove() # Regardless, remove the useless output. sign_item.delete_antlines() if fizzler is None: # No fizzler - remove this sign inst.remove() return if fizzler.fizz_type.id == TAG_FIZZ_ID: LOGGER.warning('Two tag signs attached to one fizzler...') inst.remove() return # Swap to the special Tag Fizzler type. fizzler.fizz_type = FIZZ_TYPES[TAG_FIZZ_ID] # And also swap the connection's type. if fizz_conn_conf is not None: fizzler_item.config = fizz_conn_conf fizzler_item.enable_cmd = fizz_conn_conf.enable_cmd fizzler_item.disable_cmd = fizz_conn_conf.disable_cmd fizzler_item.sec_enable_cmd = fizz_conn_conf.sec_enable_cmd fizzler_item.sec_disable_cmd = fizz_conn_conf.sec_disable_cmd inst_orient = Matrix.from_angle(Angle.from_str(inst['angles'])) # The actual location of the sign - on the wall sign_loc = Vec.from_str(inst['origin']) + Vec(0, 0, -64) @ inst_orient fizz_norm_axis = round(fizzler.normal(), 3).axis() # Now deal with the visual aspect: # Blue signs should be on top. blue_enabled = inst.fixup.bool('$start_enabled') oran_enabled = inst.fixup.bool('$start_reversed') # If True, single-color signs will also turn off the other color. # This also means we always show both signs. # If both are enabled or disabled, this has no effect. disable_other = (not inst.fixup.bool('$disable_autorespawn', True) and blue_enabled != oran_enabled) # Delete fixups now, they aren't useful. inst.fixup.clear() if not blue_enabled and not oran_enabled: # Hide the sign in this case! inst.remove() inst_normal = inst_orient.up() loc = Vec.from_str(inst['origin']) if disable_other or (blue_enabled and oran_enabled): inst['file'] = inst_frame_double conditions.ALL_INST.add(inst_frame_double.casefold()) # On a wall, and pointing vertically if abs(inst_normal.z) < 0.01 and abs(inst_orient.left().z) > 0.01: # They're vertical, make sure blue's on top! blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset) oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset) # If orange is enabled, with two frames put that on top # instead since it's more important if disable_other and oran_enabled: blue_loc, oran_loc = oran_loc, blue_loc else: offset = Vec(0, sign_offset, 0) @ inst_orient blue_loc = loc + offset oran_loc = loc - offset else: inst['file'] = inst_frame_single conditions.ALL_INST.add(inst_frame_single.casefold()) # They're always centered blue_loc = loc oran_loc = loc if inst_normal.z != 0: # If on floors/ceilings, rotate to point at the fizzler! sign_floor_loc = sign_loc.copy() sign_floor_loc.z = 0 # We don't care about z-positions. s, l = Vec.bbox(itertools.chain.from_iterable(fizzler.emitters)) if fizz_norm_axis == 'z': # For z-axis, just compare to the center point of the emitters. sign_dir = ((s.x + l.x) / 2, (s.y + l.y) / 2, 0) - sign_floor_loc else: # For the other two, we compare to the line, # or compare to the closest side (in line with the fizz) if fizz_norm_axis == 'x': # Extends in Y direction other_axis = 'y' side_min = s.y side_max = l.y normal = s.x else: # Extends in X direction other_axis = 'x' side_min = s.x side_max = l.x normal = s.y # Right in line with the fizzler. Point at the closest emitter. if abs(sign_floor_loc[other_axis] - normal) < 32: # Compare to the closest side. sign_dir = min([ sign_floor_loc - Vec.with_axes( fizz_norm_axis, side_min, other_axis, normal, ), sign_floor_loc - Vec.with_axes( fizz_norm_axis, side_max, other_axis, normal, ) ], key=Vec.mag) else: # Align just based on whether we're in front or behind. sign_dir = Vec.with_axes( fizz_norm_axis, normal - sign_floor_loc[fizz_norm_axis]).norm() sign_yaw = math.degrees(math.atan2(sign_dir.y, sign_dir.x)) # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals sign_yaw = (sign_yaw + 45) // 90 * 90 # Rotate to fit the instances - south is down sign_yaw = int(sign_yaw - 90) % 360 if inst_normal.z > 0: sign_angle = '0 {} 0'.format(sign_yaw) elif inst_normal.z < 0: # Flip upside-down for ceilings sign_angle = '0 {} 180'.format(sign_yaw) else: raise AssertionError('Cannot be zero here!') else: # On a wall, face upright sign_angle = conditions.PETI_INST_ANGLE[inst_normal.as_tuple()] # If disable_other, we show off signs. Otherwise we don't use that sign. blue_sign = blue_sign_on if blue_enabled else blue_sign_off if disable_other else None oran_sign = oran_sign_on if oran_enabled else oran_sign_off if disable_other else None if blue_sign: conditions.add_inst( vmf, file=blue_sign, targetname=inst['targetname'], angles=sign_angle, origin=blue_loc, ) if oran_sign: conditions.add_inst( vmf, file=oran_sign, targetname=inst['targetname'], angles=sign_angle, origin=oran_loc, ) # Now modify the fizzler... # Subtract the sign from the list of connections, but don't go below # zero fizzler.base_inst.fixup['$connectioncount'] = max( 0, fizzler.base_inst.fixup.int('$connectioncount') - 1) # Find the direction the fizzler normal is. # Signs will associate with the given side! bbox_min, bbox_max = fizzler.emitters[0] sign_center = (bbox_min[fizz_norm_axis] + bbox_max[fizz_norm_axis]) / 2 # Figure out what the sides will set values to... pos_blue = pos_oran = False neg_blue = neg_oran = False if sign_loc[fizz_norm_axis] < sign_center: pos_blue = blue_enabled pos_oran = oran_enabled else: neg_blue = blue_enabled neg_oran = oran_enabled # If it activates the paint gun, use different textures fizzler.tag_on_pos = pos_blue or pos_oran fizzler.tag_on_neg = neg_blue or neg_oran # Now make the trigger ents. We special-case these since they need to # swap # depending on the sign config and position. if vbsp.GAME_MODE == 'COOP': # We need ATLAS-specific triggers. pos_trig = vmf.create_ent(classname='trigger_playerteam') neg_trig = vmf.create_ent(classname='trigger_playerteam') output = 'OnStartTouchBluePlayer' else: pos_trig = vmf.create_ent(classname='trigger_multiple') neg_trig = vmf.create_ent(classname='trigger_multiple') output = 'OnStartTouch' pos_trig['origin'] = neg_trig['origin'] = fizzler.base_inst['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = conditions.local_name(fizzler.base_inst, 'trig_pos') neg_trig['targetname'] = conditions.local_name(fizzler.base_inst, 'trig_neg') pos_trig['startdisabled'] = neg_trig['startdisabled'] = ( not fizzler.base_inst.fixup.bool('start_enabled')) pos_trig.outputs = [ Output(output, neg_trig, 'Enable'), Output(output, pos_trig, 'Disable'), ] neg_trig.outputs = [ Output(output, pos_trig, 'Enable'), Output(output, neg_trig, 'Disable'), ] voice_attr = vbsp.settings['has_attr'] if blue_enabled or disable_other: # If this is blue/oran only, don't affect the other color neg_trig.outputs.append( Output( output, '@BlueIsEnabled', 'SetValue', neg_blue, )) pos_trig.outputs.append( Output( output, '@BlueIsEnabled', 'SetValue', pos_blue, )) if blue_enabled: # Add voice attributes - we have the gun and gel! voice_attr['bluegelgun'] = True voice_attr['bluegel'] = True voice_attr['bouncegun'] = True voice_attr['bouncegel'] = True if oran_enabled or disable_other: neg_trig.outputs.append( Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_oran), )) pos_trig.outputs.append( Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_oran), )) if oran_enabled: voice_attr['orangegelgun'] = True voice_attr['orangegel'] = True voice_attr['speedgelgun'] = True voice_attr['speedgel'] = True if not oran_enabled and not blue_enabled: # If both are disabled, we must shutdown the gun when touching # either side - use neg_trig for that purpose! # We want to get rid of pos_trig to save ents vmf.remove_ent(pos_trig) neg_trig['targetname'] = conditions.local_name( fizzler.base_inst, 'trig_off') neg_trig.outputs.clear() neg_trig.add_out( Output(output, '@BlueIsEnabled', 'SetValue', param='0')) neg_trig.add_out( Output(output, '@OrangeIsEnabled', 'SetValue', param='0')) # Make the triggers. for bbox_min, bbox_max in fizzler.emitters: bbox_min = bbox_min.copy() - 64 * fizzler.up_axis bbox_max = bbox_max.copy() + 64 * fizzler.up_axis # The triggers are 8 units thick, with a 32-unit gap in the middle neg_min, neg_max = Vec(bbox_min), Vec(bbox_max) neg_min[fizz_norm_axis] -= 24 neg_max[fizz_norm_axis] -= 16 pos_min, pos_max = Vec(bbox_min), Vec(bbox_max) pos_min[fizz_norm_axis] += 16 pos_max[fizz_norm_axis] += 24 if blue_enabled or oran_enabled: neg_trig.solids.append( vmf.make_prism( neg_min, neg_max, mat='tools/toolstrigger', ).solid, ) pos_trig.solids.append( vmf.make_prism( pos_min, pos_max, mat='tools/toolstrigger', ).solid, ) else: # If neither enabled, use one trigger neg_trig.solids.append( vmf.make_prism( neg_min, pos_max, mat='tools/toolstrigger', ).solid, ) return make_tag_fizz
def res_add_overlay_inst(vmf: VMF, inst: Entity, res: Property) -> Optional[Entity]: """Add another instance on top of this one. If a single value, this sets only the filename. Values: - `file`: The filename. - `fixup_style`: The Fixup style for the instance. '0' (default) is Prefix, '1' is Suffix, and '2' is None. - `copy_fixup`: If true, all the `$replace` values from the original instance will be copied over. - `move_outputs`: If true, outputs will be moved to this instance. - `offset`: The offset (relative to the base) that the instance will be placed. Can be set to `<piston_top>` and `<piston_bottom>` to offset based on the configuration. `<piston_start>` will set it to the starting position, and `<piston_end>` will set it to the ending position of the Piston Platform's handles. - `rotation`: Rotate the instance by this amount. - `angles`: If set, overrides `rotation` and the instance angles entirely. - `fixup`/`localfixup`: Keyvalues in this block will be copied to the overlay entity. - If the value starts with `$`, the variable will be copied over. - If this is present, `copy_fixup` will be disabled. """ if not res.has_children(): # Use all the defaults. res = Property('AddOverlay', [ Property('File', res.value) ]) if 'angles' in res: angles = Angle.from_str(res['angles']) if 'rotation' in res: LOGGER.warning('"angles" option overrides "rotation"!') else: angles = Angle.from_str(res['rotation', '0 0 0']) angles @= Angle.from_str(inst['angles', '0 0 0']) orig_name = conditions.resolve_value(inst, res['file', '']) filename = instanceLocs.resolve_one(orig_name) if not filename: if not res.bool('silentLookup'): LOGGER.warning('Bad filename for "{}" when adding overlay!', orig_name) # Don't bother making a overlay which will be deleted. return None overlay_inst = conditions.add_inst( vmf, targetname=inst['targetname', ''], file=filename, angles=angles, origin=inst['origin'], fixup_style=res.int('fixup_style'), ) # Don't run if the fixup block exists.. if srctools.conv_bool(res['copy_fixup', '1']): if 'fixup' not in res and 'localfixup' not in res: # Copy the fixup values across from the original instance for fixup, value in inst.fixup.items(): overlay_inst.fixup[fixup] = value conditions.set_ent_keys(overlay_inst.fixup, inst, res, 'fixup') if res.bool('move_outputs', False): overlay_inst.outputs = inst.outputs inst.outputs = [] if 'offset' in res: overlay_inst['origin'] = conditions.resolve_offset(inst, res['offset']) return overlay_inst
def res_make_tag_fizzler(vmf: VMF, inst: Entity, res: Property): """Add an Aperture Tag Paint Gun activation fizzler. These fizzlers are created via signs, and work very specially. MUST be priority -100 so it runs before fizzlers! """ import vbsp if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']: # Abort - TAG fizzlers shouldn't appear in any other game! inst.remove() return fizzler = None # Look for the fizzler instance we want to replace for targetname in inst.output_targets(): try: fizzler = FIZZLERS[targetname] except KeyError: # Not a fizzler. # It's an indicator toggle, remove it and the antline to clean up. for ent in vmf.by_target[targetname]: remove_ant_toggle(ent) inst.outputs.clear() # Remove the outputs now, they're not valid anyway. if fizzler is None: # No fizzler - remove this sign inst.remove() return if fizzler.fizz_type.id == 'TAG_FIZZ_ID': LOGGER.warning('Two tag signs attached to one fizzler...') inst.remove() return # Swap to the special Tag Fizzler type. fizzler.fizz_type = FIZZ_TYPES[TAG_FIZZ_ID] # The distance from origin the double signs are seperated by. sign_offset = res.int('signoffset', 16) sign_loc = ( # The actual location of the sign - on the wall Vec.from_str(inst['origin']) + Vec(0, 0, -64).rotate_by_str(inst['angles']) ) # Now deal with the visual aspect: # Blue signs should be on top. blue_enabled = inst.fixup.bool('$start_enabled') oran_enabled = inst.fixup.bool('$start_reversed') # If True, single-color signs will also turn off the other color. # This also means we always show both signs. # If both are enabled or disabled, this has no effect. disable_other = ( not inst.fixup.bool('$disable_autorespawn', True) and blue_enabled != oran_enabled ) # Delete fixups now, they aren't useful. inst.fixup.clear() if not blue_enabled and not oran_enabled: # Hide the sign in this case! inst.remove() inst_angle = srctools.parse_vec_str(inst['angles']) inst_normal = Vec(0, 0, 1).rotate(*inst_angle) loc = Vec.from_str(inst['origin']) if disable_other or (blue_enabled and oran_enabled): inst['file'] = res['frame_double'] # On a wall, and pointing vertically if inst_normal.z == 0 and Vec(y=1).rotate(*inst_angle).z: # They're vertical, make sure blue's on top! blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset) oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset) # If orange is enabled, with two frames put that on top # instead since it's more important if disable_other and oran_enabled: blue_loc, oran_loc = oran_loc, blue_loc else: offset = Vec(0, sign_offset, 0).rotate(*inst_angle) blue_loc = loc + offset oran_loc = loc - offset else: inst['file'] = res['frame_single'] # They're always centered blue_loc = loc oran_loc = loc if inst_normal.z != 0: # If on floors/ceilings, rotate to point at the fizzler! sign_floor_loc = sign_loc.copy() sign_floor_loc.z = 0 # We don't care about z-positions. # Grab the data saved earlier in res_find_potential_tag_fizzlers() axis, side_min, side_max, normal = calc_fizzler_orient(fizzler) # The Z-axis fizzler (horizontal) must be treated differently. if axis == 'z': # For z-axis, just compare to the center point. # The values are really x, y, z, not what they're named. sign_dir = sign_floor_loc - (side_min, side_max, normal) else: # For the other two, we compare to the line, # or compare to the closest side (in line with the fizz) other_axis = 'x' if axis == 'y' else 'y' if abs(sign_floor_loc[other_axis] - normal) < 32: # Compare to the closest side. Use ** to swap x/y arguments # appropriately. The closest side is the one with the # smallest magnitude. sign_dir = min( sign_floor_loc - Vec.with_axes( axis,side_min, other_axis, normal, ), sign_floor_loc - Vec.with_axes( axis, side_max, other_axis, normal, ), key=Vec.mag, ) else: # Align just based on whether we're in front or behind. sign_dir = Vec() sign_dir[other_axis] = sign_floor_loc[other_axis] - normal sign_angle = math.degrees( math.atan2(sign_dir.y, sign_dir.x) ) # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals sign_angle = (sign_angle + 45) // 90 * 90 # Rotate to fit the instances - south is down sign_angle = int(sign_angle + 90) % 360 if inst_normal.z > 0: sign_angle = '0 {} 0'.format(sign_angle) elif inst_normal.z < 0: # Flip upside-down for ceilings sign_angle = '0 {} 180'.format(sign_angle) else: # On a wall, face upright sign_angle = PETI_INST_ANGLE[inst_normal.as_tuple()] # If disable_other, we show off signs. Otherwise we don't use that sign. blue_sign = 'blue_sign' if blue_enabled else 'blue_off_sign' if disable_other else None oran_sign = 'oran_sign' if oran_enabled else 'oran_off_sign' if disable_other else None if blue_sign: vmf.create_ent( classname='func_instance', file=res[blue_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=blue_loc.join(' '), ) if oran_sign: vmf.create_ent( classname='func_instance', file=res[oran_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=oran_loc.join(' '), ) # Now modify the fizzler... # Subtract the sign from the list of connections, but don't go below # zero fizzler.base_inst.fixup['$connectioncount'] = str(max( 0, srctools.conv_int(fizzler.base_inst.fixup['$connectioncount', ''], 0) - 1 )) # Find the direction the fizzler normal is. # Signs will associate with the given side! bbox_min, bbox_max = fizzler.emitters[0] fizz_field_axis = (bbox_max-bbox_min).norm() fizz_norm_axis = fizzler.normal().axis() sign_center = (bbox_min[fizz_norm_axis] + bbox_max[fizz_norm_axis]) / 2 # Figure out what the sides will set values to... pos_blue = False pos_oran = False neg_blue = False neg_oran = False if sign_loc[fizz_norm_axis] < sign_center: pos_blue = blue_enabled pos_oran = oran_enabled else: neg_blue = blue_enabled neg_oran = oran_enabled # If it activates the paint gun, use different textures fizzler.tag_on_pos = pos_blue or pos_oran fizzler.tag_on_neg = neg_blue or neg_oran # Now make the trigger ents. We special-case these since they need to swap # depending on the sign config and position. if vbsp.GAME_MODE == 'COOP': # We need ATLAS-specific triggers pos_trig = vmf.create_ent( classname='trigger_playerteam', ) neg_trig = vmf.create_ent( classname='trigger_playerteam', ) output = 'OnStartTouchBluePlayer' else: pos_trig = vmf.create_ent( classname='trigger_multiple', ) neg_trig = vmf.create_ent( classname='trigger_multiple', spawnflags='1', ) output = 'OnStartTouch' pos_trig['origin'] = neg_trig['origin'] = fizzler.base_inst['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = local_name(fizzler.base_inst, 'trig_pos') neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_neg') pos_trig.outputs = [ Output(output, neg_trig, 'Enable'), Output(output, pos_trig, 'Disable'), ] neg_trig.outputs = [ Output(output, pos_trig, 'Enable'), Output(output, neg_trig, 'Disable'), ] voice_attr = vbsp.settings['has_attr'] if blue_enabled or disable_other: # If this is blue/oran only, don't affect the other color neg_trig.outputs.append(Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_blue), )) pos_trig.outputs.append(Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_blue), )) if blue_enabled: # Add voice attributes - we have the gun and gel! voice_attr['bluegelgun'] = True voice_attr['bluegel'] = True voice_attr['bouncegun'] = True voice_attr['bouncegel'] = True if oran_enabled or disable_other: neg_trig.outputs.append(Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_oran), )) pos_trig.outputs.append(Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_oran), )) if oran_enabled: voice_attr['orangegelgun'] = True voice_attr['orangegel'] = True voice_attr['speedgelgun'] = True voice_attr['speedgel'] = True if not oran_enabled and not blue_enabled: # If both are disabled, we must shutdown the gun when touching # either side - use neg_trig for that purpose! # We want to get rid of pos_trig to save ents vmf.remove_ent(pos_trig) neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_off') neg_trig.outputs.clear() neg_trig.add_out(Output( output, '@BlueIsEnabled', 'SetValue', param='0' )) neg_trig.add_out(Output( output, '@OrangeIsEnabled', 'SetValue', param='0' )) # Make the triggers. for bbox_min, bbox_max in fizzler.emitters: bbox_min = bbox_min.copy() - 64 * fizzler.up_axis bbox_max = bbox_max.copy() + 64 * fizzler.up_axis # The triggers are 8 units thick, with a 32-unit gap in the middle neg_min, neg_max = Vec(bbox_min), Vec(bbox_max) neg_min[fizz_norm_axis] -= 24 neg_max[fizz_norm_axis] -= 16 pos_min, pos_max = Vec(bbox_min), Vec(bbox_max) pos_min[fizz_norm_axis] += 16 pos_max[fizz_norm_axis] += 24 if blue_enabled or oran_enabled: neg_trig.solids.append( vmf.make_prism( neg_min, neg_max, mat='tools/toolstrigger', ).solid, ) pos_trig.solids.append( vmf.make_prism( pos_min, pos_max, mat='tools/toolstrigger', ).solid, ) else: # If neither enabled, use one trigger neg_trig.solids.append( vmf.make_prism( neg_min, pos_max, mat='tools/toolstrigger', ).solid, )
def res_make_tag_fizzler(vmf: VMF, inst: Entity, res: Property): """Add an Aperture Tag Paint Gun activation fizzler. These fizzlers are created via signs, and work very specially. MUST be priority -100 so it runs before fizzlers! """ import vbsp if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']: # Abort - TAG fizzlers shouldn't appear in any other game! inst.remove() return fizz_base = fizz_name = None # Look for the fizzler instance we want to replace for targetname in inst.output_targets(): if targetname in tag_fizzlers: fizz_name = targetname fizz_base = tag_fizzlers[targetname] del tag_fizzlers[targetname] # Don't let other signs mod this one! continue else: # It's an indicator toggle, remove it and the antline to clean up. LOGGER.warning('Toggle: {}', targetname) for ent in vmf.by_target[targetname]: remove_ant_toggle(ent) inst.outputs.clear() # Remove the outptuts now, they're not valid anyway. if fizz_base is None: # No fizzler - remove this sign inst.remove() return # The distance from origin the double signs are seperated by. sign_offset = res.int('signoffset', 16) sign_loc = ( # The actual location of the sign - on the wall Vec.from_str(inst['origin']) + Vec(0, 0, -64).rotate_by_str(inst['angles'])) # Now deal with the visual aspect: # Blue signs should be on top. blue_enabled = inst.fixup.bool('$start_enabled') oran_enabled = inst.fixup.bool('$start_reversed') # If True, single-color signs will also turn off the other color. # This also means we always show both signs. # If both are enabled or disabled, this has no effect. disable_other = (not inst.fixup.bool('$disable_autorespawn', True) and blue_enabled != oran_enabled) # Delete fixups now, they aren't useful. inst.fixup.clear() if not blue_enabled and not oran_enabled: # Hide the sign in this case! inst.remove() inst_angle = srctools.parse_vec_str(inst['angles']) inst_normal = Vec(0, 0, 1).rotate(*inst_angle) loc = Vec.from_str(inst['origin']) if disable_other or (blue_enabled and oran_enabled): inst['file'] = res['frame_double'] # On a wall, and pointing vertically if inst_normal.z == 0 and Vec(y=1).rotate(*inst_angle).z: # They're vertical, make sure blue's on top! blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset) oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset) # If orange is enabled, with two frames put that on top # instead since it's more important if disable_other and oran_enabled: blue_loc, oran_loc = oran_loc, blue_loc else: offset = Vec(0, sign_offset, 0).rotate(*inst_angle) blue_loc = loc + offset oran_loc = loc - offset else: inst['file'] = res['frame_single'] # They're always centered blue_loc = loc oran_loc = loc if inst_normal.z != 0: # If on floors/ceilings, rotate to point at the fizzler! sign_floor_loc = sign_loc.copy() sign_floor_loc.z = 0 # We don't care about z-positions. # Grab the data saved earlier in res_find_potential_tag_fizzlers() axis, side_min, side_max, normal = tag_fizzler_locs[fizz_name] # The Z-axis fizzler (horizontal) must be treated differently. if axis == 'z': # For z-axis, just compare to the center point. # The values are really x, y, z, not what they're named. sign_dir = sign_floor_loc - (side_min, side_max, normal) else: # For the other two, we compare to the line, # or compare to the closest side (in line with the fizz) other_axis = 'x' if axis == 'y' else 'y' if abs(sign_floor_loc[other_axis] - normal) < 32: # Compare to the closest side. Use ** to swap x/y arguments # appropriately. The closest side is the one with the # smallest magnitude. vmf.create_ent( classname='info_null', targetname=inst['targetname'] + '_min', origin=sign_floor_loc - Vec(**{ axis: side_min, other_axis: normal, }), ) vmf.create_ent( classname='info_null', targetname=inst['targetname'] + '_max', origin=sign_floor_loc - Vec(**{ axis: side_max, other_axis: normal, }), ) sign_dir = min( sign_floor_loc - Vec(**{ axis: side_min, other_axis: normal, }), sign_floor_loc - Vec(**{ axis: side_max, other_axis: normal, }), key=Vec.mag, ) else: # Align just based on whether we're in front or behind. sign_dir = Vec() sign_dir[other_axis] = sign_floor_loc[other_axis] - normal sign_angle = math.degrees(math.atan2(sign_dir.y, sign_dir.x)) # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals sign_angle = (sign_angle + 45) // 90 * 90 # Rotate to fit the instances - south is down sign_angle = int(sign_angle + 90) % 360 if inst_normal.z > 0: sign_angle = '0 {} 0'.format(sign_angle) elif inst_normal.z < 0: # Flip upside-down for ceilings sign_angle = '0 {} 180'.format(sign_angle) else: # On a wall, face upright sign_angle = PETI_INST_ANGLE[inst_normal.as_tuple()] # If disable_other, we show off signs. Otherwise we don't use that sign. blue_sign = 'blue_sign' if blue_enabled else 'blue_off_sign' if disable_other else None oran_sign = 'oran_sign' if oran_enabled else 'oran_off_sign' if disable_other else None if blue_sign: vmf.create_ent( classname='func_instance', file=res[blue_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=blue_loc.join(' '), ) if oran_sign: vmf.create_ent( classname='func_instance', file=res[oran_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=oran_loc.join(' '), ) # Now modify the fizzler... fizz_brushes = list(vmf.by_class['trigger_portal_cleanser'] & vmf.by_target[fizz_name + '_brush']) if 'base_inst' in res: fizz_base['file'] = instanceLocs.resolve_one(res['base_inst'], error=True) fizz_base.outputs.clear() # Remove outputs, otherwise they break # branch_toggle entities # Subtract the sign from the list of connections, but don't go below # zero fizz_base.fixup['$connectioncount'] = str( max(0, srctools.conv_int(fizz_base.fixup['$connectioncount', ''], 0) - 1)) if 'model_inst' in res: model_inst = instanceLocs.resolve_one(res['model_inst'], error=True) for mdl_inst in vmf.by_class['func_instance']: if mdl_inst['targetname', ''].startswith(fizz_name + '_model'): mdl_inst['file'] = model_inst # Find the direction the fizzler front/back points - z=floor fizz # Signs will associate with the given side! bbox_min, bbox_max = fizz_brushes[0].get_bbox() for axis, val in zip('xyz', bbox_max - bbox_min): if val == 2: fizz_axis = axis sign_center = (bbox_min[axis] + bbox_max[axis]) / 2 break else: # A fizzler that's not 128*x*2? raise Exception('Invalid fizzler brush ({})!'.format(fizz_name)) # Figure out what the sides will set values to... pos_blue = False pos_oran = False neg_blue = False neg_oran = False if sign_loc[fizz_axis] < sign_center: pos_blue = blue_enabled pos_oran = oran_enabled else: neg_blue = blue_enabled neg_oran = oran_enabled fizz_off_tex = { 'left': res['off_left'], 'center': res['off_center'], 'right': res['off_right'], 'short': res['off_short'], } fizz_on_tex = { 'left': res['on_left'], 'center': res['on_center'], 'right': res['on_right'], 'short': res['on_short'], } # If it activates the paint gun, use different textures if pos_blue or pos_oran: pos_tex = fizz_on_tex else: pos_tex = fizz_off_tex if neg_blue or neg_oran: neg_tex = fizz_on_tex else: neg_tex = fizz_off_tex if vbsp.GAME_MODE == 'COOP': # We need ATLAS-specific triggers pos_trig = vmf.create_ent(classname='trigger_playerteam', ) neg_trig = vmf.create_ent(classname='trigger_playerteam', ) output = 'OnStartTouchBluePlayer' else: pos_trig = vmf.create_ent(classname='trigger_multiple', ) neg_trig = vmf.create_ent( classname='trigger_multiple', spawnflags='1', ) output = 'OnStartTouch' pos_trig['origin'] = neg_trig['origin'] = fizz_base['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = fizz_name + '-trig_pos' neg_trig['targetname'] = fizz_name + '-trig_neg' pos_trig.outputs = [ Output( output, fizz_name + '-trig_neg', 'Enable', ), Output( output, fizz_name + '-trig_pos', 'Disable', ), ] neg_trig.outputs = [ Output( output, fizz_name + '-trig_pos', 'Enable', ), Output( output, fizz_name + '-trig_neg', 'Disable', ), ] voice_attr = vbsp.settings['has_attr'] if blue_enabled or disable_other: # If this is blue/oran only, don't affect the other color neg_trig.outputs.append( Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_blue), )) pos_trig.outputs.append( Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_blue), )) if blue_enabled: # Add voice attributes - we have the gun and gel! voice_attr['bluegelgun'] = True voice_attr['bluegel'] = True voice_attr['bouncegun'] = True voice_attr['bouncegel'] = True if oran_enabled or disable_other: neg_trig.outputs.append( Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_oran), )) pos_trig.outputs.append( Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_oran), )) if oran_enabled: voice_attr['orangegelgun'] = True voice_attr['orangegel'] = True voice_attr['speedgelgun'] = True voice_attr['speedgel'] = True if not oran_enabled and not blue_enabled: # If both are disabled, we must shutdown the gun when touching # either side - use neg_trig for that purpose! # We want to get rid of pos_trig to save ents vmf.remove_ent(pos_trig) neg_trig['targetname'] = fizz_name + '-trig_off' neg_trig.outputs.clear() neg_trig.add_out( Output(output, '@BlueIsEnabled', 'SetValue', param='0')) neg_trig.add_out( Output(output, '@OrangeIsEnabled', 'SetValue', param='0')) for fizz_brush in fizz_brushes: # portal_cleanser ent, not solid! # Modify fizzler textures bbox_min, bbox_max = fizz_brush.get_bbox() for side in fizz_brush.sides(): norm = side.normal() if norm[fizz_axis] == 0: # Not the front/back: force nodraw # Otherwise the top/bottom will have the odd stripes # which won't match the sides side.mat = 'tools/toolsnodraw' continue if norm[fizz_axis] == 1: side.mat = pos_tex[vbsp.TEX_FIZZLER[side.mat.casefold()]] else: side.mat = neg_tex[vbsp.TEX_FIZZLER[side.mat.casefold()]] # The fizzler shouldn't kill cubes fizz_brush['spawnflags'] = '1' fizz_brush.outputs.append( Output( 'OnStartTouch', '@shake_global', 'StartShake', )) fizz_brush.outputs.append( Output( 'OnStartTouch', '@shake_global_sound', 'PlaySound', )) # The triggers are 8 units thick, 24 from the center # (-1 because fizzlers are 2 thick on each side). neg_min, neg_max = Vec(bbox_min), Vec(bbox_max) neg_min[fizz_axis] -= 23 neg_max[fizz_axis] -= 17 pos_min, pos_max = Vec(bbox_min), Vec(bbox_max) pos_min[fizz_axis] += 17 pos_max[fizz_axis] += 23 if blue_enabled or oran_enabled: neg_trig.solids.append( vmf.make_prism( neg_min, neg_max, mat='tools/toolstrigger', ).solid, ) pos_trig.solids.append( vmf.make_prism( pos_min, pos_max, mat='tools/toolstrigger', ).solid, ) else: # If neither enabled, use one trigger neg_trig.solids.append( vmf.make_prism( neg_min, pos_max, mat='tools/toolstrigger', ).solid, )
def res_goo_debris(res: Property): """Add random instances to goo squares. Options: - file: The filename for the instance. The variant files should be suffixed with `_1.vmf`, `_2.vmf`, etc. - space: the number of border squares which must be filled with goo for a square to be eligible - defaults to 1. - weight, number: see the `Variant` result, a set of weights for the options - chance: The percentage chance a square will have a debris item - offset: A random xy offset applied to the instances. """ import brushLoc space = res.int('spacing', 1) rand_count = res.int('number', None) if rand_count: rand_list = weighted_random( rand_count, res['weights', ''], ) else: rand_list = None # type: Optional[List[int]] chance = res.int('chance', 30) / 100 file = res['file'] offset = res.int('offset', 0) if file.endswith('.vmf'): file = file[:-4] goo_top_locs = { pos.as_tuple() for pos, block in brushLoc.POS.items() if block.is_goo and block.is_top } if space == 0: # No spacing needed, just copy possible_locs = [Vec(loc) for loc in goo_top_locs] else: possible_locs = [] for x, y, z in goo_top_locs: # Check to ensure the neighbouring blocks are also # goo brushes (depending on spacing). for x_off, y_off in utils.iter_grid( min_x=-space, max_x=space + 1, min_y=-space, max_y=space + 1, stride=1, ): if x_off == y_off == 0: continue # We already know this is a goo location if (x + x_off, y + y_off, z) not in goo_top_locs: break # This doesn't qualify else: possible_locs.append(brushLoc.grid_to_world(Vec(x,y,z))) LOGGER.info( 'GooDebris: {}/{} locations', len(possible_locs), len(goo_top_locs), ) suff = '' for loc in possible_locs: random.seed('goo_debris_{}_{}_{}'.format(loc.x, loc.y, loc.z)) if random.random() > chance: continue if rand_list is not None: suff = '_' + str(random.choice(rand_list) + 1) if offset > 0: loc.x += random.randint(-offset, offset) loc.y += random.randint(-offset, offset) loc.z -= 32 # Position the instances in the center of the 128 grid. VMF.create_ent( classname='func_instance', file=file + suff + '.vmf', origin=loc.join(' '), angles='0 {} 0'.format(random.randrange(0, 3600)/10) ) return RES_EXHAUSTED