def pose(f, rot): global FRAME # Calculate the piston's rotation. # First find the real position of the piston hinge. hinge_pos = Vec(-43, 0, 10.5) hinge_pos.x -= 64 hinge_pos.rotate(float(rot), 0, 0) hinge_pos.x += 64 # Where we want the end of the piston to be. anchor_point = Vec(z=-96, x=rot*1.5 + 96) piston_off = hinge_pos - anchor_point print(piston_off) piston_rot = math.degrees(math.atan2(piston_off.z, -piston_off.x)) f.write(frame_temp.format( time=FRAME, rot=-round(math.radians(rot), 6), # Cancel the effect of rot on pist_rot pist_rot=round(math.radians((piston_rot + rot) % 360), 6), len=-piston_off.mag(), marker=Vec(z=anchor_point.z, y=-anchor_point.x), )) FRAME += 1
def _texture_fit( self, side: Side, tex_size: float, field_length: float, fizz: Fizzler, neg: Vec, pos: Vec, is_laserfield=False, ) -> None: """Calculate the texture offsets required for fitting a texture.""" if side.vaxis.vec() != -fizz.up_axis: # Rotate it rot_angle = side.normal().rotation_around() for _ in range(4): side.uaxis = side.uaxis.rotate(rot_angle) side.vaxis = side.vaxis.rotate(rot_angle) if side.vaxis.vec() == -fizz.up_axis: break else: LOGGER.warning("Can't fix rotation for {} -> {}", side.vaxis, fizz.up_axis) side.uaxis.offset = -(tex_size / field_length) * neg.dot(side.uaxis.vec()) side.vaxis.offset = -(tex_size / 128) * neg.dot(side.vaxis.vec()) # The above fits it correctly, except it's vertically half-offset. # For laserfields that's what we want, for fizzlers we want it normal. if not is_laserfield: side.vaxis.offset += tex_size / 2 side.uaxis.scale = field_length / tex_size side.vaxis.scale = 128 / tex_size side.uaxis.offset %= tex_size side.vaxis.offset %= tex_size
def find_glass_items(config, vmf: VMF) -> Iterator[Tuple[str, Vec, Vec, Vec, dict]]: """Find the bounding boxes for all the glass items matching a config. This yields (targetname, min, max, normal, config) tuples. """ # targetname -> min, max, normal, config glass_items = {} for inst in vmf.by_class['func_instance']: # type: Entity try: conf = config[inst['file'].casefold()] except KeyError: continue targ = inst['targetname'] norm = Vec(x=1).rotate_by_str(inst['angles']) origin = Vec.from_str(inst['origin']) - 64 * norm try: bbox_min, bbox_max, group_norm, group_conf = glass_items[targ] except KeyError: # First of this group.. bbox_min, bbox_max = origin.copy(), origin.copy() group_norm = norm.copy() glass_items[targ] = bbox_min, bbox_max, group_norm, conf else: bbox_min.min(origin) bbox_max.max(origin) assert group_norm == norm, '"{}" is inconsistently rotated!'.format(targ) assert group_conf is conf, '"{}" has multiple configs!'.format(targ) inst.remove() for targ, (bbox_min, bbox_max, norm, conf) in glass_items.items(): yield targ, bbox_min, bbox_max, norm, conf
def res_replace_instance(inst: Entity, res: Property): """Replace an instance with another entity. `keys` and `localkeys` defines the new keyvalues used. `targetname` and `angles` are preset, and `origin` will be used to offset the given amount from the current location. If `keep_instance` is true, the instance entity will be kept instead of removed. """ import vbsp origin = Vec.from_str(inst['origin']) angles = inst['angles'] if not srctools.conv_bool(res['keep_instance', '0'], False): inst.remove() # Do this first to free the ent ID, so the new ent has # the same one. # We copy to allow us to still access the $fixups and other values. new_ent = inst.copy(des_id=inst.id) new_ent.clear_keys() # Ensure there's a classname, just in case. new_ent['classname'] = 'info_null' vbsp.VMF.add_ent(new_ent) conditions.set_ent_keys(new_ent, inst, res) origin += Vec.from_str(new_ent['origin']).rotate_by_str(angles) new_ent['origin'] = origin new_ent['angles'] = angles new_ent['targetname'] = inst['targetname']
def res_glass_hole(inst: Entity, res: Property): """Add Glass/grating holes. The value should be 'large' or 'small'.""" hole_type = HoleType(res.value) normal = Vec(z=-1).rotate_by_str(inst['angles']) origin = Vec.from_str(inst['origin']) // 128 * 128 + 64 if test_hole_spot(origin, normal, hole_type): HOLES[origin.as_tuple(), normal.as_tuple()] = hole_type inst['origin'] = origin inst['angles'] = normal.to_angle() return # Test the opposite side of the glass too. inv_origin = origin + 128 * normal inv_normal = -normal if test_hole_spot(inv_origin, inv_normal, hole_type): HOLES[inv_origin.as_tuple(), inv_normal.as_tuple()] = hole_type inst['origin'] = inv_origin inst['angles'] = inv_normal.to_angle() else: # Remove the instance, so this does nothing. inst.remove()
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 res_sendificator(vmf: VMF, inst: Entity): """Implement Sendificators.""" # For our version, we know which sendtor connects to what laser, # so we can couple the logic together (avoiding @sendtor_mutex). sendtor_name = inst['targetname'] sendtor = connections.ITEMS[sendtor_name] sendtor.enable_cmd += (Output( '', '@{}_las_relay_*'.format(sendtor_name), 'Trigger', delay=0.01, ), ) for ind, conn in enumerate(list(sendtor.outputs), start=1): las_item = conn.to_item conn.remove() try: targ_offset, targ_normal = SENDTOR_TARGETS[las_item.name] except KeyError: LOGGER.warning('"{}" is not a Sendificator target!', las_item.name) continue angles = Vec.from_str(las_item.inst['angles']) targ_offset = targ_offset.copy() targ_normal = targ_normal.copy().rotate(*angles) targ_offset.localise( Vec.from_str(las_item.inst['origin']), angles, ) relay_name = '@{}_las_relay_{}'.format(sendtor_name, ind) relay = vmf.create_ent( 'logic_relay', targetname=relay_name, origin=targ_offset, angles=targ_normal.to_angle(), ) relay.add_out( Output('OnTrigger', '!self', 'RunScriptCode', '::sendtor_source <- self;'), Output('OnTrigger', '@sendtor_fire', 'Trigger'), ) if not las_item.inputs: # No other inputs, make it on always. PeTI automatically turns # it off when inputs are connected, which is annoying. las_item.inst.fixup['$start_enabled'] = '1' is_on = True else: is_on = las_item.inst.fixup.bool('$start_enabled') relay['StartDisabled'] = not is_on las_item.enable_cmd += (Output('', relay_name, 'Enable'),) las_item.disable_cmd += (Output('', relay_name, 'Disable'),) LOGGER.info('Relay: {}', relay)
def make_straight( origin: Vec, normal: Vec, dist: int, config: dict, is_start=False, ): """Make a straight line of instances from one point to another.""" # 32 added to the other directions, plus extended dist in the direction # of the normal - 1 p1 = origin + (normal * ((dist // 128 * 128) - 96)) # The starting brush needs to # stick out a bit further, to cover the # point_push entity. p2 = origin - (normal * (96 if is_start else 32)) # bbox before +- 32 to ensure the above doesn't wipe it out p1, p2 = Vec.bbox(p1, p2) solid = vbsp.VMF.make_prism( # Expand to 64x64 in the other two directions p1 - 32, p2 + 32, mat='tools/toolstrigger', ).solid motion_trigger(solid.copy()) push_trigger(origin, normal, [solid]) angles = normal.to_angle() support_file = config['support'] straight_file = config['straight'] support_positions = ( SUPPORT_POS[normal.as_tuple()] if support_file else [] ) for off in range(0, int(dist), 128): position = origin + off * normal vbsp.VMF.create_ent( classname='func_instance', origin=position, angles=angles, file=straight_file, ) for supp_ang, supp_off in support_positions: if (position + supp_off).as_tuple() in SOLIDS: vbsp.VMF.create_ent( classname='func_instance', origin=position, angles=supp_ang, file=support_file, )
def res_unst_scaffold_setup(res: Property): group = res['group', 'DEFAULT_GROUP'] if group not in SCAFFOLD_CONFIGS: # Store our values in the CONFIGS dictionary targ_inst, links = SCAFFOLD_CONFIGS[group] = {}, {} else: # Grab the already-filled values, and add to them targ_inst, links = SCAFFOLD_CONFIGS[group] for block in res.find_all("Instance"): conf = { # If set, adjusts the offset appropriately 'is_piston': srctools.conv_bool(block['isPiston', '0']), 'rotate_logic': srctools.conv_bool(block['AlterAng', '1'], True), 'off_floor': Vec.from_str(block['FloorOff', '0 0 0']), 'off_wall': Vec.from_str(block['WallOff', '0 0 0']), 'logic_start': block['startlogic', ''], 'logic_end': block['endLogic', ''], 'logic_mid': block['midLogic', ''], 'logic_start_rev': block['StartLogicRev', None], 'logic_end_rev': block['EndLogicRev', None], 'logic_mid_rev': block['EndLogicRev', None], 'inst_wall': block['wallInst', ''], 'inst_floor': block['floorInst', ''], 'inst_offset': block['offsetInst', None], # Specially rotated to face the next track! 'inst_end': block['endInst', None], } for logic_type in ('logic_start', 'logic_mid', 'logic_end'): if conf[logic_type + '_rev'] is None: conf[logic_type + '_rev'] = conf[logic_type] for inst in instanceLocs.resolve(block['file']): targ_inst[inst] = conf # We need to provide vars to link the tracks and beams. for block in res.find_all('LinkEnt'): # The name for this set of entities. # It must be a '@' name, or the name will be fixed-up incorrectly! loc_name = block['name'] if not loc_name.startswith('@'): loc_name = '@' + loc_name links[block['nameVar']] = { 'name': loc_name, # The next entity (not set in end logic) 'next': block['nextVar'], # A '*' name to reference all the ents (set on the start logic) 'all': block['allVar', None], } return group # We look up the group name to find the values.
def flag_blockpos_type(inst: Entity, flag: Property): """Determine the type of a grid position. If the value is single value, that should be the type. Otherwise, the value should be a block with 'offset' and 'type' values. The offset is in block increments, with 0 0 0 equal to the mounting surface. If 'offset2' is also provided, all positions in the bounding box will be checked. The type should be a space-seperated list of locations: * `VOID` (Outside the map) * `SOLID` (Full wall cube) * `EMBED` (Hollow wall cube) * `AIR` (Inside the map, may be occupied by items) * `OCCUPIED` (Known to be occupied by items) * `PIT` (Bottomless pits, any) * `PIT_SINGLE` (one-high) * `PIT_TOP` * `PIT_MID` * `PIT_BOTTOM` * `GOO` * `GOO_SINGLE` (one-deep goo) * `GOO_TOP` (goo surface) * `GOO_MID` * `GOO_BOTTOM` (floor) """ pos2 = None if flag.has_children(): pos1 = resolve_offset(inst, flag['offset', '0 0 0'], scale=128, zoff=-128) types = flag['type'].split() if 'offset2' in flag: pos2 = resolve_offset(inst, flag.value, scale=128, zoff=-128) else: types = flag.value.split() pos1 = Vec() if pos2 is not None: bbox = Vec.iter_grid(*Vec.bbox(pos1, pos2), stride=128) else: bbox = [pos1] for pos in bbox: block = brushLoc.POS['world': pos] for block_type in types: try: allowed = brushLoc.BLOCK_LOOKUP[block_type.casefold()] except KeyError: raise ValueError('"{}" is not a valid block type!'.format(block_type)) if block in allowed: break # To next position else: return False # Didn't match any in this list. return True # Matched all positions.
def res_camera_setup(res: Property): return { 'cam_off': Vec.from_str(res['CamOff', '']), 'yaw_off': Vec.from_str(res['YawOff', '']), 'pitch_off': Vec.from_str(res['PitchOff', '']), 'yaw_inst': instanceLocs.resolve_one(res['yawInst', '']), 'pitch_inst': instanceLocs.resolve_one(res['pitchInst', '']), 'yaw_range': srctools.conv_int(res['YawRange', ''], 90), 'pitch_range': srctools.conv_int(res['YawRange', ''], 90), }
def res_unst_scaffold_setup(res: Property): group = res["group", "DEFAULT_GROUP"] if group not in SCAFFOLD_CONFIGS: # Store our values in the CONFIGS dictionary targ_inst, links = SCAFFOLD_CONFIGS[group] = {}, {} else: # Grab the already-filled values, and add to them targ_inst, links = SCAFFOLD_CONFIGS[group] for block in res.find_all("Instance"): conf = { # If set, adjusts the offset appropriately "is_piston": srctools.conv_bool(block["isPiston", "0"]), "rotate_logic": srctools.conv_bool(block["AlterAng", "1"], True), "off_floor": Vec.from_str(block["FloorOff", "0 0 0"]), "off_wall": Vec.from_str(block["WallOff", "0 0 0"]), "logic_start": block["startlogic", ""], "logic_end": block["endLogic", ""], "logic_mid": block["midLogic", ""], "logic_start_rev": block["StartLogicRev", None], "logic_end_rev": block["EndLogicRev", None], "logic_mid_rev": block["EndLogicRev", None], "inst_wall": block["wallInst", ""], "inst_floor": block["floorInst", ""], "inst_offset": block["offsetInst", None], # Specially rotated to face the next track! "inst_end": block["endInst", None], } for logic_type in ("logic_start", "logic_mid", "logic_end"): if conf[logic_type + "_rev"] is None: conf[logic_type + "_rev"] = conf[logic_type] for inst in resolve_inst(block["file"]): targ_inst[inst] = conf # We need to provide vars to link the tracks and beams. for block in res.find_all("LinkEnt"): # The name for this set of entities. # It must be a '@' name, or the name will be fixed-up incorrectly! loc_name = block["name"] if not loc_name.startswith("@"): loc_name = "@" + loc_name links[block["nameVar"]] = { "name": loc_name, # The next entity (not set in end logic) "next": block["nextVar"], # A '*' name to reference all the ents (set on the start logic) "all": block["allVar", None], } return group # We look up the group name to find the values.
def flag_goo_at_loc(inst: Entity, flag: Property): """Check to see if a given location is submerged in goo. 0 0 0 is the origin of the instance, values are in 128 increments. """ pos = Vec.from_str(flag.value).rotate_by_str(inst['angles', '0 0 0']) pos *= 128 pos += Vec.from_str(inst['origin']) # Round to 128 units, then offset to the center pos = pos // 128 * 128 + 64 # type: Vec val = pos.as_tuple() in GOO_LOCS return val
def res_make_funnel_light(inst: Entity): """Place a light for Funnel items.""" oran_on = inst.fixup.bool('$start_reversed') need_blue = need_oran = False name = '' if inst.fixup['$connectioncount_polarity'] != '0': import vbsp if not vbsp.settings['style_vars']['funnelallowswitchedlights']: # Allow disabling adding switchable lights. return name = conditions.local_name(inst, 'light') need_blue = need_oran = True else: if oran_on: need_oran = True else: need_blue = True loc = Vec(0, 0, -56) loc.localise(Vec.from_str(inst['origin']), Vec.from_str(inst['angles'])) if need_blue: inst.map.create_ent( classname='light', targetname=name + '_b' if name else '', spawnflags=int(oran_on), # 1 = Initially Dark origin=loc, _light='50 120 250 50', _lightHDR='-1 -1 -1 1', _lightscaleHDR=2, _fifty_percent_distance=48, _zero_percent_distance=96, _hardfalloff=1, _distance=0, style=0, ) if need_oran: inst.map.create_ent( classname='light', targetname=name + '_o' if name else '', spawnflags=int(not oran_on), origin=loc, _light='250 120 50 50', _lightHDR='-1 -1 -1 1', _lightscaleHDR=2, _fifty_percent_distance=48, _zero_percent_distance=96, _hardfalloff=1, _distance=0, style=0, )
def _calc_fizz_angles() -> None: """Generate FIZZ_ANGLES.""" it = itertools.product('xyz', (-1, 1), 'xyz', (-1, 1)) for norm_axis, norm_mag, roll_axis, roll_mag in it: if norm_axis == roll_axis: # They can't both be the same... continue norm = Vec.with_axes(norm_axis, norm_mag) roll = Vec.with_axes(roll_axis, roll_mag) # Norm is Z, roll is X, we want y. angle = roll.to_angle_roll(norm) up_dir = norm.cross(roll) FIZZ_ANGLES[norm.as_tuple(), up_dir.as_tuple()] = angle
def res_monitor(inst: Entity, res: Property) -> None: """Result for the monitor component. """ import vbsp ( break_inst, bullseye_name, bullseye_loc, bullseye_parent, ) = res.value ALL_MONITORS.append(Monitor(inst)) has_laser = vbsp.settings['has_attr']['laser'] # Allow turrets if the monitor is setup to allow it, and the actor should # be shot. needs_turret = bullseye_name and vbsp_options.get(bool, 'voice_studio_should_shoot') inst.fixup['$is_breakable'] = has_laser or needs_turret # We need to generate an ai_relationship, which makes turrets hate # a bullseye. if needs_turret: loc = Vec(bullseye_loc) loc.localise( Vec.from_str(inst['origin']), Vec.from_str(inst['angles']), ) bullseye_name = local_name(inst, bullseye_name) inst.map.create_ent( classname='npc_bullseye', targetname=bullseye_name, parentname=local_name(inst, bullseye_parent), spawnflags=221186, # Non-solid, invisible, etc.. origin=loc, ) relation = inst.map.create_ent( classname='ai_relationship', targetname='@monitor_turr_hate', spawnflags=2, # Notify turrets about monitor locations disposition=1, # Hate origin=loc, subject='npc_portal_turret_floor', target=bullseye_name, ) MONITOR_RELATIONSHIP_ENTS.append(relation)
def get_config( node: item_chain.Node, ) -> Tuple[str, Vec]: """Compute the config values for a node.""" orient = ( 'floor' if Vec(0, 0, 1).rotate_by_str(node.inst['angles']) == (0, 0, 1) else 'wall' ) # Find the offset used for the platform. offset = (node.conf['off_' + orient]).copy() # type: Vec if node.conf['is_piston']: # Adjust based on the piston position offset.z += 128 * srctools.conv_int( node.inst.fixup[ '$top_level' if node.inst.fixup[ '$start_up'] == '1' else '$bottom_level' ] ) offset.rotate_by_str(node.inst['angles']) offset += Vec.from_str(node.inst['origin']) return orient, offset
def track_scan( tr_set: Set[Entity], track_inst: Dict[Tuple[float, float, float], Entity], start_track: Entity, middle_file: str, x_dir: int, ): """Build a set of track instances extending from a point. :param track_inst: A dictionary mapping origins to track instances :param start_track: The instance we start on :param middle_file: The file for the center track piece :param x_dir: The direction to look (-1 or 1) """ track = start_track move_dir = Vec(x_dir*128, 0, 0).rotate_by_str(track['angles']) while track: tr_set.add(track) next_pos = Vec.from_str(track['origin']) + move_dir track = track_inst.get(next_pos.as_tuple(), None) if track is None: return if track['file'].casefold() != middle_file: # If the next piece is an end section, add it then quit tr_set.add(track) return
def calc_fizzler_orient(fizzler: Fizzler): # Figure out how to compare for this fizzler. s, l = Vec.bbox(itertools.chain.from_iterable(fizzler.emitters)) # If it's horizontal, signs should point to the center: if abs(s.z - l.z) == 2: return ( 'z', s.x + l.x / 2, s.y + l.y / 2, s.z + 1, ) # For the vertical directions, we want to compare based on the line segment. if abs(s.x - l.x) == 2: # Y direction return ( 'y', s.y, l.y, s.x + 1, ) else: # Extends in X direction return ( 'x', s.x, l.x, s.y + 1, )
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property): """Create the spawn point for ATLAS in the entry corridor. It produces either an instance or the normal spawn entity. This is required since ATLAS may need to have the paint gun logic. The two parameters `origin` and `facing` must be set to determine the required position. If `global` is set, the spawn point will be absolute instead of relative to the current instance. """ if vbsp.GAME_MODE != 'COOP': return RES_EXHAUSTED is_tag = vbsp_options.get(str, 'game_id') == utils.STEAM_IDS['TAG'] origin = res.vec('origin') normal = res.vec('facing', z=1) # Some styles might want to ignore the instance we're running on. if not res.bool('global'): origin = origin.rotate_by_str(inst['angles']) normal = normal.rotate_by_str(inst['angles']) origin += Vec.from_str(inst['origin']) angles = normal.to_angle() if is_tag: vmf.create_ent( classname='func_instance', targetname='paint_gun', origin=origin - (0, 0, 16), angles=angles, # Generated by the BEE2 app. file='instances/bee2/tag_coop_gun.vmf', ) # Blocks ATLAS from having a gun vmf.create_ent( classname='info_target', targetname='supress_blue_portalgun_spawn', origin=origin, angles='0 0 0', ) # Allows info_target to work vmf.create_ent( classname='env_global', targetname='no_spawns', globalstate='portalgun_nospawn', initialstate=1, spawnflags=1, # Use initial state origin=origin, ) vmf.create_ent( classname='info_coop_spawn', targetname='@coop_spawn_blue', ForceGunOnSpawn=int(not is_tag), origin=origin, angles=angles, enabled=1, StartingTeam=3, # ATLAS ) return RES_EXHAUSTED
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 gen_rotated_squarebeams(p1: Vec, p2: Vec, skin, max_rot: int): """Generate broken/rotated squarebeams in a region. They will be rotated around their centers, not the model origin. """ z = min(p1.z, p2.z) + 3 # The center of the beams for x, y in utils.iter_grid(min_x=int(p1.x), min_y=int(p1.y), max_x=int(p2.x), max_y=int(p2.y), stride=64): rand_x = random.randint(-max_rot, max_rot) / BEAM_ROT_PRECISION rand_z = random.randint(-max_rot, max_rot) / BEAM_ROT_PRECISION # Don't rotate around yaw - the vertical axis. # Squarebeams are offset 5 units from their real center offset = Vec(0, 0, 5).rotate(rand_x, 0, rand_z) prop = _make_squarebeam(Vec(x + 32, y + 32, z) + offset, skin=skin) prop['angles'] = '{} 0 {}'.format(rand_x, rand_z)
def res_create_entity(vmf: VMF, inst: Entity, res: Property): """Create an entity. * `keys` and `localkeys` defines the new keyvalues used. * `Origin` will be used to offset the given amount from the current location. """ origin = Vec.from_str(inst['origin']) new_ent = vmf.create_ent( # Ensure there's a classname, just in case. classname='info_null') conditions.set_ent_keys(new_ent, inst, res) origin += Vec.from_str(new_ent['origin']).rotate_by_str(inst['angles']) new_ent['origin'] = origin new_ent['angles'] = inst['angles']
def res_cube_coloriser(inst: Entity): """Allows recoloring cubes placed at a position.""" origin = Vec.from_str(inst['origin']) timer_delay = inst.fixup.int('$timer_delay') if 3 <= timer_delay <= 30: COLOR_POS[origin.as_tuple()] = COLORS[timer_delay - 3] else: LOGGER.warning('Unknown timer value "{}"!', timer_delay) inst.remove() # If pointing up, copy the value to the ceiling, so droppers # can find a coloriser placed on the illusory cube item under them. if Vec(z=1).rotate_by_str(inst['angles']) == (0, 0, 1): pos = brushLoc.POS.raycast_world( origin, direction=(0, 0, 1), ) COLOR_SEC_POS[pos.as_tuple()] = COLORS[timer_delay - 3]
def res_import_template_setup(res: Property): temp_id = res['id'].casefold() face = Vec.from_str(res['face_pos', '0 0 -64']) norm = Vec.from_str(res['normal', '0 0 1']) replace_tex = defaultdict(list) for prop in res.find_key('replace', []): replace_tex[prop.name].append(prop.value) offset = Vec.from_str(res['offset', '0 0 0']) return ( temp_id, dict(replace_tex), face, norm, offset, )
def mode_handle(comp_ent: Entity, ent: Entity) -> str: """Compute and return a handle to tis entity.""" if ent['targetname']: return 'Entities.FindByName(null, "{}")'.format(ent['targetname']) else: # No name, use classname and position. return 'Entities.FindByClassnameWithin(null, "{}", {}, 1)'.format( ent['classname'], vs_vec(Vec.from_str(ent['origin'])) )
def _fill_norm_rotations() -> Dict[ Tuple[Tuple[float, float, float], Tuple[float, float, float]], Tuple[float, float, float] ]: """Given a norm->norm rotation, return the angles producing that.""" rotations = {} for norm_ax in 'xyz': for norm_mag in [-1, +1]: norm = Vec.with_axes(norm_ax, norm_mag) for angle_ax in 'xyz': for angle_mag in (-90, 90): angle = Vec.with_axes(angle_ax, angle_mag) new_norm = norm.copy().rotate(*angle) if new_norm != norm: rotations[tuple(norm), tuple(new_norm)] = angle.as_tuple() # Assign a null rotation as well. rotations[tuple(norm), tuple(norm)] = (0.0, 0.0, 0.0) rotations[tuple(norm), tuple(-norm)] = (0.0, 0.0, 0.0) return rotations
def res_hollow_brush(inst: Entity, res: Property): """Hollow out the attached brush, as if EmbeddedVoxel was set. This just removes the surface if it's already an embeddedVoxel. This allows multiple items to embed thinly in the same block without affecting each other. """ loc = Vec(0, 0, -64).rotate_by_str(inst['angles']) loc += Vec.from_str(inst['origin']) try: group = SOLIDS[loc.as_tuple()] except KeyError: LOGGER.warning('No brush for hollowing at ({})', loc) return # No brush here? conditions.hollow_block(group, remove_orig_face=srctools.conv_bool( res['RemoveFace', False]))
def load(opt_blocks: Iterator[Property]) -> None: """Read settings from the given property block.""" SETTINGS.clear() set_vals = {} for opt_block in opt_blocks: for prop in opt_block: set_vals[prop.name] = prop.value options = {opt.id: opt for opt in DEFAULTS} if len(options) != len(DEFAULTS): from collections import Counter # Find ids used more than once.. raise Exception('Duplicate option(s)! ({})'.format(', '.join( k for k, v in Counter(opt.id for opt in DEFAULTS).items() if v > 1 ))) fallback_opts = [] for opt in DEFAULTS: try: val = set_vals.pop(opt.id) except KeyError: if opt.fallback is not None: fallback_opts.append(opt) assert opt.fallback in options, 'Invalid fallback in ' + opt.id else: SETTINGS[opt.id] = opt.default continue if opt.type is TYPE.VEC: # Pass nones so we can check if it failed.. parsed_vals = parse_vec_str(val, x=None) if parsed_vals[0] is None: SETTINGS[opt.id] = opt.default else: SETTINGS[opt.id] = Vec(*parsed_vals) elif opt.type is TYPE.BOOL: SETTINGS[opt.id] = srctools.conv_bool(val, opt.default) else: # int, float, str - no special handling... try: SETTINGS[opt.id] = opt.type.convert(val) except (ValueError, TypeError): SETTINGS[opt.id] = opt.default for opt in fallback_opts: try: SETTINGS[opt.id] = SETTINGS[opt.fallback] except KeyError: raise Exception('Bad fallback for "{}"!'.format(opt.id)) # Check they have the same type. assert opt.type is options[opt.fallback].type if set_vals: LOGGER.warning('Extra config options: {}', set_vals)
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property): """Create the spawn point for ATLAS, in Aperture Tag. This creates an instance with the desired orientation. The two parameters 'origin' and 'angles' must be set. """ if vbsp.GAME_MODE != 'COOP': return RES_EXHAUSTED is_tag = vbsp_options.get(str, 'game_id') == utils.STEAM_IDS['TAG'] offset = res.vec('origin').rotate_by_str(inst['angles']) normal = res.vec('facing', z=1).rotate_by_str( inst['angles'], ) origin = Vec.from_str(inst['origin']) origin += offset angles = normal.to_angle() if is_tag: vmf.create_ent( classname='func_instance', targetname='paint_gun', origin=origin - (0, 0, 16), angles=angles, # Generated by the BEE2 app. file='instances/bee2/tag_coop_gun.vmf', ) # Blocks ATLAS from having a gun vmf.create_ent( classname='info_target', targetname='supress_blue_portalgun_spawn', origin=origin, angles='0 0 0', ) # Allows info_target to work vmf.create_ent( classname='env_global', targetname='no_spawns', globalstate='portalgun_nospawn', initialstate=1, spawnflags=1, # Use initial state origin=origin, ) vmf.create_ent( classname='info_coop_spawn', targetname='@coop_spawn_blue', ForceGunOnSpawn=int(not is_tag), origin=origin, angles=angles, enabled=1, StartingTeam=3, # ATLAS ) return RES_EXHAUSTED
def res_set_block(inst: Entity, res: Property) -> None: """Set a block to the given value, overwriting the existing value. - `type` is the type of block to set: * `VOID` (Outside the map) * `SOLID` (Full wall cube) * `EMBED` (Hollow wall cube) * `AIR` (Inside the map, may be occupied by items) * `OCCUPIED` (Known to be occupied by items) * `PIT_SINGLE` (one-high) * `PIT_TOP` * `PIT_MID` * `PIT_BOTTOM` * `GOO_SINGLE` (one-deep goo) * `GOO_TOP` (goo surface) * `GOO_MID` * `GOO_BOTTOM` (floor) - `offset` is in block increments, with `0 0 0` equal to the mounting surface. - If 'offset2' is also provided, all positions in the bounding box will be set. """ try: new_vals = brushLoc.BLOCK_LOOKUP[res['type'].casefold()] except KeyError: raise ValueError('"{}" is not a valid block type!'.format(res['type'])) try: [new_val] = new_vals except ValueError: # TODO: This could spread top/mid/bottom through the bbox... raise ValueError( f'Can\'t use compound block type "{res["type"]}", specify ' "_SINGLE/TOP/MID/BOTTOM" ) pos1 = resolve_offset(inst, res['offset', '0 0 0'], scale=128, zoff=-128) if 'offset2' in res: pos2 = resolve_offset(inst, res['offset2', '0 0 0'], scale=128, zoff=-128) for pos in Vec.iter_grid(*Vec.bbox(pos1, pos2), stride=128): brushLoc.POS['world': pos] = new_val else: brushLoc.POS['world': pos1] = new_val
def test_ang_matrix_roundtrip(py_c_vec): """Check converting to and from a Matrix does not change values.""" Vec, Angle, Matrix, parse_vec_str = py_c_vec for p, y, r in iter_vec(range(0, 360, 90)): vert = Vec(x=1).rotate(p, y, r).z if vert < 0.99 or vert > 0.99: # If nearly vertical, gimbal lock prevents roundtrips. continue mat = Matrix.from_angle(Angle(p, y, r)) assert_ang(mat.to_angle(), p, y, r)
def res_create_entity(vmf: VMF, inst: Entity, res: Property): """Create an entity. 'keys' and 'localkeys' defines the new keyvalues used. 'Origin' will be used to offset the given amount from the current location. """ origin = Vec.from_str(inst['origin']) new_ent = vmf.create_ent( # Ensure there's a classname, just in case. classname='info_null' ) conditions.set_ent_keys(new_ent, inst, res) origin += Vec.from_str(new_ent['origin']).rotate_by_str(inst['angles']) new_ent['origin'] = origin new_ent['angles'] = inst['angles']
def res_import_template_setup( res: Property, ) -> Tuple[str, Dict[str, List[str]], Vec, Vec, Vec]: temp_id = res['id'].casefold() face = Vec.from_str(res['face_pos', '0 0 -64']) norm = Vec.from_str(res['normal', '0 0 1']) replace_tex = defaultdict(list) # type: Dict[str, List[str]] for prop in res.find_key('replace', []): replace_tex[prop.name.replace('\\', '/')].append(prop.value) offset = Vec.from_str(res['offset', '0 0 0']) return ( temp_id, dict(replace_tex), face, norm, offset, )
def push_trigger(vmf: VMF, loc: Vec, normal: Vec, solids: list[Solid]) -> None: """Generate the push trigger for these solids.""" # We only need one trigger per direction, for now. try: ent = PUSH_TRIGS[normal.as_tuple()] except KeyError: ent = PUSH_TRIGS[normal.as_tuple()] = vmf.create_ent( classname='trigger_push', origin=loc, # The z-direction is reversed.. pushdir=normal.to_angle(), speed=( UP_PUSH_SPEED if normal.z > 1e-6 else DN_PUSH_SPEED if normal.z < -1e-6 else PUSH_SPEED ), spawnflags='1103', # Clients, Physics, Everything ) ent.solids.extend(solids)
def __init__(self, ent: Entity): """Convert the entity to have the right logic.""" self.scanner = None self.persist_tv = conv_bool(ent.keys.pop('persist_tv', False)) pos = Vec.from_str(ent['origin']) for prop in ent.map.by_class['prop_dynamic']: if (Vec.from_str(prop['origin']) - pos).mag_sq() > 64**2: continue model = prop['model'].casefold().replace('\\', '/') # Allow spelling this correctly, if you're not Valve. if 'vacum_scanner_tv' in model or 'vacuum_scanner_tv' in model: self.scanner = prop prop.make_unique('_vac_scanner') elif 'vacum_scanner_motion' in model or 'vacuum_scanner_motion' in model: prop.make_unique('_vac_scanner') ent.add_out(Output(self.pass_out_name, prop, "SetAnimation", "scan01")) super(Straight, self).__init__(ent)
def res_hollow_brush(inst: Entity, res: Property): """Hollow out the attached brush, as if EmbeddedVoxel was set. This just removes the surface if it's already an embeddedVoxel. This allows multiple items to embed thinly in the same block without affecting each other. """ loc = Vec(0, 0, -64).rotate_by_str(inst['angles']) loc += Vec.from_str(inst['origin']) try: group = SOLIDS[loc.as_tuple()] except KeyError: LOGGER.warning('No brush for hollowing at ({})', loc) return # No brush here? conditions.hollow_block( group, remove_orig_face=srctools.conv_bool(res['RemoveFace', False]) )
def res_force_upright(inst: Entity): """Position an instance to orient upwards while keeping the normal. The result angle will have pitch and roll set to 0. Vertical instances are unaffected. """ normal = Vec(0, 0, 1).rotate_by_str(inst['angles']) if normal.z != 0: return ang = math.degrees(math.atan2(normal.y, normal.x)) inst['angles'] = '0 {:g} 0'.format(ang % 360) # Don't use negatives
def _conv_key(pos: _grid_keys) -> Vec_tuple: """Convert the key given in [] to a grid-position, as a x,y,z tuple.""" if isinstance(pos, slice): system, pos = pos.start, pos.stop pos = Grid._conv_key(pos) if system == 'world': return tuple(world_to_grid(Vec(pos))) else: return pos x, y, z = pos return x, y, z
def res_sendificator(vmf: VMF, inst: Entity): """Implement Sendificators.""" # For our version, we know which sendtor connects to what laser, # so we can couple the logic together (avoiding @sendtor_mutex). sendtor_name = inst['targetname'] sendtor = connections.ITEMS[sendtor_name] sendtor.enable_cmd += (Output( '', f'@{sendtor_name}_las_relay_*', 'Trigger', delay=0.01, ), ) for ind, conn in enumerate(list(sendtor.outputs), start=1): las_item = conn.to_item conn.remove() try: targ_offset, targ_normal = SENDTOR_TARGETS[las_item.name] except KeyError: LOGGER.warning('"{}" is not a Sendificator target!', las_item.name) continue orient = Matrix.from_angle(Angle.from_str(las_item.inst['angles'])) targ_offset = Vec.from_str( las_item.inst['origin']) + targ_offset @ orient targ_normal = targ_normal @ orient relay_name = f'@{sendtor_name}_las_relay_{ind}' relay = vmf.create_ent( 'logic_relay', targetname=relay_name, origin=targ_offset, angles=targ_normal.to_angle(), ) relay.add_out( Output('OnTrigger', '!self', 'RunScriptCode', '::sendtor_source <- self;'), Output('OnTrigger', '@sendtor_fire', 'Trigger'), ) if not las_item.inputs: # No other inputs, make it on always. PeTI automatically turns # it off when inputs are connected, which is annoying. las_item.inst.fixup['$start_enabled'] = '1' is_on = True else: is_on = las_item.inst.fixup.bool('$start_enabled') relay['StartDisabled'] = not is_on las_item.enable_cmd += (Output('', relay_name, 'Enable'), ) las_item.disable_cmd += (Output('', relay_name, 'Disable'), )
def raycast( self, pos: _grid_keys, direction: Vec, collide: Iterable[Block]=frozenset({ Block.SOLID, Block.EMBED, Block.PIT_BOTTOM, Block.PIT_SINGLE, }), ) -> Vec: """Move in a direction until hitting a block of a certain type. This returns the position just before hitting a block (which might be the start position.) The direction vector should be integer numbers (1/0 usually). collide is the set of position types to stop at. The default is all "solid" walls. ValueError is raised if VOID is encountered, or this moves outside the map. """ start_pos = pos = Vec(*_conv_key(pos)) direction = Vec(direction) collide_set = frozenset(collide) # 50x50x50 diagonal = 86, so that's the largest distance # you could possibly move. for i in range(90): next_pos = pos + direction block = super().get(next_pos.as_tuple(), Block.VOID) if block is Block.VOID: raise ValueError( 'Reached VOID at ({}) when ' 'raycasting from {} with direction {}!'.format( next_pos, start_pos, direction ) ) if block in collide_set: return pos pos = next_pos else: raise ValueError('Moved too far! (> 90)')
def make_color_swatch(parent: tk.Frame, var: tk.StringVar, size=16) -> ttk.Label: """Make a single swatch.""" # Note: tkinter requires RGB as ints, not float! color = var.get() if color.startswith('#'): try: r, g, b = int(var[0:2], base=16), int(var[2:4], base=16), int(var[4:], base=16) except ValueError: LOGGER.warning('Invalid RGB value: "{}"!', color) r = g = b = 128 else: r, g, b = map(int, Vec.from_str(color, 128, 128, 128)) def open_win(e): """Display the color selection window.""" nonlocal r, g, b widget_sfx() new_color, tk_color = askcolor( color=(r, g, b), parent=parent.winfo_toplevel(), title=_('Choose a Color'), ) if new_color is not None: r, g, b = map(int, new_color) # Returned as floats, which is wrong. var.set('{} {} {}'.format(int(r), int(g), int(b))) swatch['image'] = img.color_square(round(Vec(r, g, b)), size) swatch = ttk.Label( parent, relief='raised', image=img.color_square(Vec(r, g, b), size), ) utils.bind_leftclick(swatch, open_win) return swatch
def save_occupiedvoxel(item: Item, vmf: VMF) -> None: """Save occupied voxel volumes.""" for voxel in item.occupy_voxels: pos = Vec(voxel.pos) * 128 if voxel.subpos is not None: pos += Vec(voxel.subpos) * 32 - (48, 48, 48) p1 = pos - (16.0, 16.0, 16.0) p2 = pos + (16.0, 16.0, 16.0) norm_dist = 32.0 - 4.0 else: p1 = pos - (64.0, 64.0, 64.0) p2 = pos + (64.0, 64.0, 64.0) norm_dist = 128.0 - 4.0 if voxel.normal is not None: for axis in ['x', 'y', 'z']: val = getattr(voxel.normal, axis) if val == +1: p2[axis] -= norm_dist elif val == -1: p1[axis] += norm_dist if voxel.against is not None: against = str(voxel.against).replace('COLLIDE_', '') else: against = '' vmf.create_ent( 'bee2_editor_occupiedvoxel', coll_type=str(voxel.type).replace('COLLIDE_', ''), coll_against=against, ).solids.append( vmf.make_prism( p1, p2, # Use clip for voxels, invisible for normals. # Entirely ignored, but makes it easier to use. 'tools/toolsclip' if voxel.normal is None else 'tools/toolsinvisible', ).solid)
def flag_angles(inst: Entity, flag: Property): """Check that a instance is pointed in a direction. The value should be either just the angle to check, or a block of options: - `Angle`: A unit vector (XYZ value) pointing in a direction, or some keywords: `+z`, `-y`, `N`/`S`/`E`/`W`, `up`/`down`, `floor`/`ceiling`, or `walls` for any wall side. - `From_dir`: The direction the unrotated instance is pointed in. This lets the flag check multiple directions - `Allow_inverse`: If true, this also returns True if the instance is pointed the opposite direction . """ angle = inst['angles', '0 0 0'] if flag.has_children(): targ_angle = flag['direction', '0 0 0'] from_dir = flag['from_dir', '0 0 1'] if from_dir.casefold() in DIRECTIONS: from_dir = Vec(DIRECTIONS[from_dir.casefold()]) else: from_dir = Vec.from_str(from_dir, 0, 0, 1) allow_inverse = srctools.conv_bool(flag['allow_inverse', '0']) else: targ_angle = flag.value from_dir = Vec(0, 0, 1) allow_inverse = False normal = DIRECTIONS.get(targ_angle.casefold(), None) if normal is None: return False # If it's not a special angle, # so it failed the exact match inst_normal = from_dir.rotate_by_str(angle) if normal == 'WALL': # Special case - it's not on the floor or ceiling return not (inst_normal == (0, 0, 1) or inst_normal == (0, 0, -1)) else: return inst_normal == normal or ( allow_inverse and -inst_normal == normal )
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property): """Create the spawn point for ATLAS, in Aperture Tag. This creates an instance with the desired orientation. The two parameters 'origin' and 'angles' must be set. """ if vbsp.GAME_MODE != 'COOP': return RES_EXHAUSTED is_tag = vbsp_options.get(str, 'game_id') == utils.STEAM_IDS['TAG'] offset = res.vec('origin').rotate_by_str(inst['angles']) normal = res.vec('facing', z=1).rotate_by_str(inst['angles'], ) origin = Vec.from_str(inst['origin']) origin += offset angles = normal.to_angle() if is_tag: vmf.create_ent( classname='func_instance', targetname='paint_gun', origin=origin - (0, 0, 16), angles=angles, # Generated by the BEE2 app. file='instances/bee2/tag_coop_gun.vmf', ) # Blocks ATLAS from having a gun vmf.create_ent( classname='info_target', targetname='supress_blue_portalgun_spawn', origin=origin, angles='0 0 0', ) # Allows info_target to work vmf.create_ent( classname='env_global', targetname='no_spawns', globalstate='portalgun_nospawn', initialstate=1, spawnflags=1, # Use initial state origin=origin, ) vmf.create_ent( classname='info_coop_spawn', targetname='@coop_spawn_blue', ForceGunOnSpawn=int(not is_tag), origin=origin, angles=angles, enabled=1, StartingTeam=3, # ATLAS ) return RES_EXHAUSTED
def _conv_key(pos: _grid_keys) -> Tuple[float, float, float]: """Convert the key given in [] to a grid-position, as a x,y,z tuple.""" # TODO: Slices are assumed to be int by typeshed. # type: ignore if isinstance(pos, slice): system, slice_pos = pos.start, pos.stop if system == 'world': return tuple(world_to_grid(Vec(slice_pos))) else: return tuple(slice_pos) x, y, z = pos return x, y, z
def gen_faithplates(vmf: VMF) -> None: """Place the targets and catapults into the map.""" # Target positions -> list of triggers wanting to aim there. pos_to_trigs: Dict[Union[Tuple[float, float, float], tiling.TileDef], List[Entity]] = collections.defaultdict(list) for plate in PLATES.values(): if isinstance(plate, (AngledPlate, PaintDropper)): targ_pos: Union[Tuple[float, float, float], tiling.TileDef] if isinstance(plate.target, tiling.TileDef): targ_pos = plate.target # Use the ID directly. else: targ_pos = plate.target.as_tuple() pos_to_trigs[targ_pos].append(plate.trig) if isinstance(plate, StraightPlate): trigs = [plate.trig, plate.helper_trig] else: trigs = [plate.trig] for trig in trigs: trig_origin = trig.get_origin() if plate.template is not None: trig.solids = template_brush.import_template( vmf, plate.template, trig_origin + plate.trig_offset, Angle.from_str(plate.inst['angles']), force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ).world elif plate.trig_offset: for solid in trig.solids: solid.translate(plate.trig_offset) # Now, generate each target needed. for pos_or_tile, trigs in pos_to_trigs.items(): target = vmf.create_ent( 'info_target', angles='0 0 0', spawnflags='3', # Transmit to PVS and always transmit. ) if isinstance(pos_or_tile, tiling.TileDef): pos_or_tile.position_bullseye(target) else: # Static target. target['origin'] = Vec(pos_or_tile) target.make_unique('faith_target') for trig in trigs: trig['launchTarget'] = target['targetname']
def test_bbox_rotation( pitch: float, yaw: float, roll: float, ) -> None: """Test the rotation logic against the slow direct approach.""" ang = Angle(pitch, yaw, roll) bb_start = BBox(100, 200, 300, 300, 450, 600, contents=CollideType.ANTLINES, tags='blah') # Directly compute, by rotating all the angles, points = [ Vec(x, y, z) for x in [100, 300] for y in [200, 450] for z in [300, 600] ] result_ang = bb_start @ ang result_mat = bb_start @ Matrix.from_angle(ang) assert result_ang == result_mat bb_min, bb_max = Vec.bbox( point @ ang for point in points ) assert_bbox(result_mat, round(bb_min, 0), round(bb_max, 0), CollideType.ANTLINES, {'blah'})
def contains(self, point: Vec) -> bool: """Check if the given position is inside the volume.""" for convex in self.collision: for pos, norm in convex: off = pos - point # This is the actual distance, so we'll use a rather large # "epsilon" to catch objects close to the edges. if Vec.dot(off, norm) < -0.1: break # Outside a plane, it doesn't match this convex. else: # Inside all these planes, it's inside. return True return False # All failed, not present.
def res_create_entity(vmf: VMF, inst: Entity, res: Property): """Create an entity. * `keys` and `localkeys` defines the new keyvalues used. * `origin` and `angles` are local to the instance. """ origin = Vec.from_str(inst['origin']) orient = Angle.from_str(inst['angles']) new_ent = vmf.create_ent( # Ensure there's these critical values. classname='info_null', origin='0 0 0', angles='0 0 0', ) conditions.set_ent_keys(new_ent, inst, res) new_ent['origin'] = Vec.from_str(new_ent['origin']) @ orient + origin new_ent['angles'] = Angle.from_str(new_ent['angles']) @ orient
def test_minmax(): """Test Vec.min() and Vec.max().""" vec_a = Vec() vec_b = Vec() for a, b in MINMAX_VALUES: max_val = max(a, b) min_val = min(a, b) for axis in 'xyz': vec_a.x = vec_a.y = vec_a.z = 0 vec_b.x = vec_b.y = vec_b.z = 0 vec_a[axis] = a vec_b[axis] = b assert vec_a.min(vec_b) is None, (a, b, axis, min_val) assert vec_a[axis] == min_val, (a, b, axis, min_val) vec_a[axis] = a vec_b[axis] = b assert vec_a.max(vec_b) is None, (a, b, axis, max_val) assert vec_a[axis] == max_val, (a, b, axis, max_val)
def test_bad_from_basis(py_c_vec) -> None: """Test invalid arguments to Matrix.from_basis()""" Vec, Angle, Matrix, parse_vec_str = py_c_vec v = Vec(0, 1, 0) with pytest.raises(TypeError): Matrix.from_basis() with pytest.raises(TypeError): Matrix.from_basis(x=v) with pytest.raises(TypeError): Matrix.from_basis(y=v) with pytest.raises(TypeError): Matrix.from_basis(z=v)
def vactube_gen(vmf: VMF) -> None: """Generate the vactubes, after most conditions have run.""" if not VAC_TRACKS: return LOGGER.info('Generating vactubes...') for start, all_markers in VAC_TRACKS: start_normal = Vec(-1, 0, 0).rotate_by_str(start.ent['angles']) # First create the start section.. start_logic = start.ent.copy() vbsp.VMF.add_ent(start_logic) start_logic['file'] = start.conf['entry', ('ceiling' if (start_normal.z > 0) else 'floor' if (start_normal.z < 0) else 'wall')] end = start for inst, end in start.follow_path(all_markers): join_markers(inst, end, inst is start) end_loc = Vec.from_str(end.ent['origin']) end_norm = Vec(-1, 0, 0).rotate_by_str(end.ent['angles']) # join_markers creates straight parts up-to the marker, but not at it's # location - create the last one. make_straight( end_loc, end_norm, 128, end.conf, ) # If the end is placed in goo, don't add logic - it isn't visible, and # the object is on a one-way trip anyway. if BLOCK_POS['world':end_loc].is_goo and end_norm == (0, 0, -1): end_logic = end.ent.copy() vbsp.VMF.add_ent(end_logic) end_logic['file'] = end.conf['exit']
def test_old_rotation(py_c_vec) -> None: """Verify that the code matches the results from the earlier Vec.rotate code.""" Vec, Angle, Matrix, parse_vec_str = py_c_vec for pitch in range(0, 360, 15): for yaw in range(0, 360, 15): for roll in range(0, 360, 15): ang = Angle(pitch, yaw, roll) mat = Matrix.from_angle(ang) # Construct a matrix directly from 3 vector rotations. old_mat = Matrix() old_mat[0, 0], old_mat[0, 1], old_mat[0, 2] = old_rotate( Vec(x=1), pitch, yaw, roll) old_mat[1, 0], old_mat[1, 1], old_mat[1, 2] = old_rotate( Vec(y=1), pitch, yaw, roll) old_mat[2, 0], old_mat[2, 1], old_mat[2, 2] = old_rotate( Vec(z=1), pitch, yaw, roll) assert_rot(mat, old_mat, ang) old = old_rotate(Vec(128, 0, 0), pitch, yaw, roll) by_ang = Vec(128, 0, 0) @ ang by_mat = Vec(128, 0, 0) @ mat assert_vec(by_ang, old.x, old.y, old.z, ang, tol=1e-1) assert_vec(by_mat, old.x, old.y, old.z, ang, tol=1e-1)
def beam_hole_split(axis: str, min_pos: Vec, max_pos: Vec): """Break up floor beams to fit around holes.""" # Go along the shape. For each point, check if a hole is present, # and split at that. # Our positions are centered, but we return ones at the ends. # Inset in 4 units from each end to not overlap with the frames. start_pos = min_pos - Vec.with_axes(axis, 60) if HOLES: hole_size_large = vbsp_options.get(float, 'glass_hole_size_large') / 2 hole_size_small = vbsp_options.get(float, 'glass_hole_size_small') / 2 # Extract normal from the z-axis. grid_height = min_pos.z // 128 * 128 + 64 if grid_height < min_pos.z: normal = (0, 0, 1) else: normal = (0, 0, -1) import vbsp for pos in min_pos.iter_line(max_pos, 128): try: hole_type = HOLES[(pos.x, pos.y, grid_height), normal] except KeyError: continue else: if hole_type is HoleType.SMALL: size = hole_size_small elif hole_type is HoleType.LARGE: size = hole_size_large else: raise AssertionError(hole_type) yield start_pos, pos - Vec.with_axes(axis, size) start_pos = pos + Vec.with_axes(axis, size) # Last segment, or all if no holes. yield start_pos, max_pos + Vec.with_axes(axis, 60)
def join_markers(inst_a, inst_b, is_start=False): """Join two marker ents together with corners. This returns a list of solids used for the vphysics_motion trigger. """ origin_a = Vec.from_str(inst_a['ent']['origin']) origin_b = Vec.from_str(inst_b['ent']['origin']) norm_a = Vec(-1, 0, 0).rotate_by_str(inst_a['ent']['angles']) norm_b = Vec(-1, 0, 0).rotate_by_str(inst_b['ent']['angles']) config = inst_a['conf'] if norm_a == norm_b: # Either straight-line, or s-bend. dist = (origin_a - origin_b).mag() if origin_a + (norm_a * dist) == origin_b: make_straight( origin_a, norm_a, dist, config, is_start, ) # else: S-bend, we don't do the geometry for this.. return if norm_a == -norm_b: # U-shape bend.. make_ubend( origin_a, origin_b, norm_a, config, max_size=inst_a['size'], ) return try: corner_ang, flat_angle = CORNER_ANG[norm_a.as_tuple(), norm_b.as_tuple()] if origin_a[flat_angle] != origin_b[flat_angle]: # It needs to be flat in this angle! raise ValueError except ValueError: # The tubes need two corners to join together - abort for that. return else: make_bend( origin_a, origin_b, norm_a, norm_b, corner_ang, config, max_size=inst_a['size'], )
def get_color(): """Parse out the color.""" color = var.get() if color.startswith('#'): try: r = int(color[1:3], base=16) g = int(color[3:5], base=16) b = int(color[5:], base=16) except ValueError: LOGGER.warning('Invalid RGB value: "{}"!', color) r = g = b = 128 else: r, g, b = map(int, Vec.from_str(color, 128, 128, 128)) return r, g, b
def test_hole_spot(origin: Vec, normal: Vec, hole_type: HoleType): """Check if the given position is valid for holes. We need to check that it's actually placed on glass/grating, and that all the parts are the same. Otherwise it'd collide with the borders. """ try: center_type = BARRIERS[origin.as_tuple(), normal.as_tuple()] except KeyError: LOGGER.warning('No center barrier at {}, {}', origin, normal) return False if hole_type is HoleType.SMALL: return True u, v = Vec.INV_AXIS[normal.axis()] # The corners don't matter, but all 4 neighbours must be there. for u_off, v_off in [ (-128, 0), (0, -128), (128, 0), (0, 128), ]: pos = origin + Vec.with_axes(u, u_off, v, v_off) try: off_type = BARRIERS[pos.as_tuple(), normal.as_tuple()] except KeyError: # No side LOGGER.warning('No offset barrier at {}, {}', pos, normal) return False if off_type is not center_type: # Different type. LOGGER.warning('Wrong barrier type at {}, {}', pos, normal) return False return True