def make_corner( vmf: VMF, origin: Vec, start_dir: Vec, end_dir: Vec, size: int, config: Config, ) -> None: angles = Matrix.from_basis(z=start_dir, x=end_dir).to_angle() vmf.create_ent( classname='func_instance', origin=origin, angles=angles, file=config.inst_corner[size], ) temp = config.temp_corner[size] if temp: temp_solids = template_brush.import_template( vmf, temp, origin=origin, angles=angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ).world motion_trigger(vmf, *temp_solids)
def make_corner( vmf: VMF, origin: Vec, start_dir: Vec, end_dir: Vec, size: int, config: Config, ) -> None: """Place a corner.""" angles = Matrix.from_basis(z=start_dir, x=end_dir) conditions.add_inst( vmf, origin=origin, angles=angles, file=config.inst_corner[int(size)], ) temp, visgroups = config.temp_corner[int(size)] if temp is not None: temp_solids = template_brush.import_template( vmf, temp, additional_visgroups=visgroups, origin=origin, angles=angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ).world motion_trigger(vmf, *temp_solids)
def compute_orients(nodes: Iterable[Node]) -> None: """Compute the appropriate orientation for each node.""" # This is based on the info at: # https://janakiev.com/blog/framing-parametric-curves/ tangents: Dict[Node, Vec] = {} all_nodes: Set[Node] = set() for node in nodes: if node.prev is node.next is None: continue node_prev = node.prev if node.prev is not None else node node_next = node.next if node.next is not None else node tangents[node] = (node_next.pos - node_prev.pos).norm() all_nodes.add(node) while all_nodes: node1 = all_nodes.pop() node1 = node1.find_start() tanj1 = tangents[node1] # Start with an arbitrary roll for the first orientation. node1.orient = Matrix.from_angle(tanj1.to_angle()) while node1.next is not None: node2 = node1.next all_nodes.discard(node2) tanj1 = tangents[node1] tanj2 = tangents[node2] b = Vec.cross(tanj1, tanj2) if b.mag_sq() < 0.001: node2.orient = node1.orient.copy() else: b = b.norm() phi = math.acos(Vec.dot(tanj1, tanj2)) up = node1.orient.up() @ Matrix.axis_angle( b, math.degrees(phi)) node2.orient = Matrix.from_basis(x=tanj2, z=up) node1 = node2
def test_gen_check(py_c_vec) -> None: """Do an exhaustive check on all rotation math using data from the engine.""" Vec, Angle, Matrix, parse_vec_str = py_c_vec X = Vec(x=1) Y = Vec(y=1) Z = Vec(z=1) with open('rotation.txt') as f: for line_num, line in enumerate(f, start=1): if not line.startswith('|'): # Skip other junk in the log. continue (pit, yaw, roll, for_x, for_y, for_z, left_x, left_y, left_z, up_x, up_y, up_z) = map(float, line[1:].split()) mat = Matrix.from_angle(Angle(pit, yaw, roll)) # Then check rotating vectors works correctly. # The engine actually gave us a right vector, so we need to flip that. assert_vec(X @ mat, for_x, for_y, for_z) assert_vec(Y @ mat, -left_x, -left_y, -left_z) assert_vec(Z @ mat, up_x, up_y, up_z) assert math.isclose(for_x, mat[0, 0], abs_tol=EPSILON) assert math.isclose(for_y, mat[0, 1], abs_tol=EPSILON) assert math.isclose(for_z, mat[0, 2], abs_tol=EPSILON) assert math.isclose(-left_x, mat[1, 0], abs_tol=EPSILON) assert math.isclose(-left_y, mat[1, 1], abs_tol=EPSILON) assert math.isclose(-left_z, mat[1, 2], abs_tol=EPSILON) assert math.isclose(up_x, mat[2, 0], abs_tol=EPSILON) assert math.isclose(up_y, mat[2, 1], abs_tol=EPSILON) assert math.isclose(up_z, mat[2, 2], abs_tol=EPSILON) # Also test Matrix.from_basis(). x = Vec(for_x, for_y, for_z) y = -Vec(left_x, left_y, left_z) z = Vec(up_x, up_y, up_z) assert_rot(Matrix.from_basis(x=x, y=y, z=z), mat) assert_rot(Matrix.from_basis(x=x, y=y), mat) assert_rot(Matrix.from_basis(y=y, z=z), mat) assert_rot(Matrix.from_basis(x=x, z=z), mat)
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 res_conveyor_belt(vmf: VMF, inst: Entity, res: Property) -> None: """Create a conveyor belt. * Options: * `SegmentInst`: Generated at each square. (`track` is the name of the path to attach to.) * `TrackTeleport`: Set the track points so they teleport trains to the start. * `Speed`: The fixup or number for the train speed. * `MotionTrig`: If set, a trigger_multiple will be spawned that `EnableMotion`s weighted cubes. The value is the name of the relevant filter. * `EndOutput`: Adds an output to the last track. The value is the same as outputs in VMFs. `RotateSegments`: If true (default), force segments to face in the direction of movement. * `BeamKeys`: If set, a list of keyvalues to use to generate an env_beam travelling from start to end. The origin is treated specially - X is the distance from walls, y is the distance to the side, and z is the height. `RailTemplate`: A template for the track sections. This is made into a non-solid func_brush, combining all sections. * `NoPortalFloor`: If set, add a `func_noportal_volume` on the floor under the track. * `PaintFizzler`: If set, add a paint fizzler underneath the belt. """ move_dist = inst.fixup.int('$travel_distance') if move_dist <= 2: # There isn't room for a conveyor, so don't bother. inst.remove() return orig_orient = Matrix.from_angle(Angle.from_str(inst['angles'])) move_dir = Vec(1, 0, 0) @ Angle.from_str(inst.fixup['$travel_direction']) move_dir = move_dir @ orig_orient start_offset = inst.fixup.float('$starting_position') teleport_to_start = res.bool('TrackTeleport', True) segment_inst_file = instanceLocs.resolve_one(res['SegmentInst', '']) rail_template = res['RailTemplate', None] track_speed = res['speed', None] start_pos = Vec.from_str(inst['origin']) end_pos = start_pos + move_dist * move_dir if start_offset > 0: # If an oscillating platform, move to the closest side.. offset = start_offset * move_dir # The instance is placed this far along, so move back to the end. start_pos -= offset end_pos -= offset if start_offset > 0.5: # Swap the direction of movement.. start_pos, end_pos = end_pos, start_pos inst['origin'] = start_pos norm = orig_orient.up() if res.bool('rotateSegments', True): orient = Matrix.from_basis(x=move_dir, z=norm) inst['angles'] = orient.to_angle() else: orient = orig_orient # Add the EnableMotion trigger_multiple seen in platform items. # This wakes up cubes when it starts moving. motion_filter = res['motionTrig', None] # Disable on walls, or if the conveyor can't be turned on. if norm != (0, 0, 1) or inst.fixup['$connectioncount'] == '0': motion_filter = None track_name = conditions.local_name(inst, 'segment_{}') rail_temp_solids = [] last_track = None # Place tracks at the top, so they don't appear inside wall sections. track_start: Vec = start_pos + 48 * norm track_end: Vec = end_pos + 48 * norm for index, pos in enumerate(track_start.iter_line(track_end, stride=128), start=1): track = vmf.create_ent( classname='path_track', targetname=track_name.format(index) + '-track', origin=pos, spawnflags=0, orientationtype=0, # Don't rotate ) if track_speed is not None: track['speed'] = track_speed if last_track: last_track['target'] = track['targetname'] if index == 1 and teleport_to_start: track['spawnflags'] = 16 # Teleport here.. last_track = track # Don't place at the last point - it doesn't teleport correctly, # and would be one too many. if segment_inst_file and pos != track_end: seg_inst = conditions.add_inst( vmf, targetname=track_name.format(index), file=segment_inst_file, origin=pos, angles=orient, ) seg_inst.fixup.update(inst.fixup) if rail_template: temp = template_brush.import_template( vmf, rail_template, pos, orient, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) rail_temp_solids.extend(temp.world) if rail_temp_solids: vmf.create_ent( classname='func_brush', origin=track_start, spawnflags=1, # Ignore +USE solidity=1, # Not solid vrad_brush_cast_shadows=1, drawinfastreflection=1, ).solids = rail_temp_solids if teleport_to_start: # Link back to the first track.. last_track['target'] = track_name.format(1) + '-track' # Generate an env_beam pointing from the start to the end of the track. try: beam_keys = res.find_key('BeamKeys') except LookupError: pass else: beam = vmf.create_ent(classname='env_beam') beam_off = beam_keys.vec('origin', 0, 63, 56) for prop in beam_keys: beam[prop.real_name] = prop.value # Localise the targetname so it can be triggered.. beam['LightningStart'] = beam['targetname'] = conditions.local_name( inst, beam['targetname', 'beam']) del beam['LightningEnd'] beam['origin'] = start_pos + Vec( -beam_off.x, beam_off.y, beam_off.z, ) @ orient beam['TargetPoint'] = end_pos + Vec( +beam_off.x, beam_off.y, beam_off.z, ) @ orient # Allow adding outputs to the last path_track. for prop in res.find_all('EndOutput'): output = Output.parse(prop) output.output = 'OnPass' output.inst_out = None output.comma_sep = False output.target = conditions.local_name(inst, output.target) last_track.add_out(output) if motion_filter is not None: motion_trig = vmf.create_ent( classname='trigger_multiple', targetname=conditions.local_name(inst, 'enable_motion_trig'), origin=start_pos, filtername=motion_filter, startDisabled=1, wait=0.1, ) motion_trig.add_out( Output('OnStartTouch', '!activator', 'ExitDisabledState')) # Match the size of the original... motion_trig.solids.append( vmf.make_prism( start_pos + Vec(72, -56, 58) @ orient, end_pos + Vec(-72, 56, 144) @ orient, mat=consts.Tools.TRIGGER, ).solid) if res.bool('NoPortalFloor'): # Block portals on the floor.. floor_noportal = vmf.create_ent( classname='func_noportal_volume', origin=track_start, ) floor_noportal.solids.append( vmf.make_prism( start_pos + Vec(-60, -60, -66) @ orient, end_pos + Vec(60, 60, -60) @ orient, mat=consts.Tools.INVISIBLE, ).solid) # A brush covering under the platform. base_trig = vmf.make_prism( start_pos + Vec(-64, -64, 48) @ orient, end_pos + Vec(64, 64, 56) @ orient, mat=consts.Tools.INVISIBLE, ).solid vmf.add_brush(base_trig) # Make a paint_cleanser under the belt.. if res.bool('PaintFizzler'): pfizz = vmf.create_ent( classname='trigger_paint_cleanser', origin=start_pos, ) pfizz.solids.append(base_trig.copy()) for face in pfizz.sides(): face.mat = consts.Tools.TRIGGER
def make_straight( vmf: VMF, origin: Vec, normal: Vec, dist: int, config: Config, is_start=False, ) -> None: """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 = vmf.make_prism( # Expand to 64x64 in the other two directions p1 - 32, p2 + 32, mat='tools/toolstrigger', ).solid motion_trigger(vmf, solid.copy()) push_trigger(vmf, origin, normal, [solid]) angles = normal.to_angle() orient = Matrix.from_angle(angles) for off in range(0, int(dist), 128): position = origin + off * normal vmf.create_ent( classname='func_instance', origin=position, angles=orient.to_angle(), file=config.inst_straight, ) for supp_dir in [ orient.up(), orient.left(), -orient.left(), -orient.up() ]: try: tile = tiling.TILES[(position - 128 * supp_dir).as_tuple(), supp_dir.norm().as_tuple()] except KeyError: continue # Check all 4 center tiles are present. if all(tile[u, v].is_tile for u in (1, 2) for v in (1, 2)): vmf.create_ent( classname='func_instance', origin=position, angles=Matrix.from_basis(x=normal, z=supp_dir).to_angle(), file=config.inst_support, )
def make_straight( vmf: VMF, origin: Vec, normal: Vec, dist: int, config: Config, is_start=False, ) -> None: """Make a straight line of instances from one point to another.""" angles = round(normal, 6).to_angle() orient = Matrix.from_angle(angles) # The starting brush needs to stick out a bit further, to cover the # point_push entity. start_off = -96 if is_start else -64 p1, p2 = Vec.bbox( origin + Vec(start_off, -config.trig_radius, -config.trig_radius) @ orient, origin + Vec(dist - 64, config.trig_radius, config.trig_radius) @ orient, ) solid = vmf.make_prism(p1, p2, mat='tools/toolstrigger').solid motion_trigger(vmf, solid.copy()) push_trigger(vmf, origin, normal, [solid]) off = 0 for seg_dist in utils.fit(dist, config.inst_straight_sizes): vmf.create_ent( classname='func_instance', origin=origin + off * orient.forward(), angles=angles, file=config.inst_straight[seg_dist], ) off += seg_dist # Supports. if config.inst_support: for off in range(0, int(dist), 128): position = origin + off * normal placed_support = False for supp_dir in [ orient.up(), orient.left(), -orient.left(), -orient.up() ]: try: tile = tiling.TILES[ (position - 128 * supp_dir).as_tuple(), supp_dir.norm().as_tuple() ] except KeyError: continue # Check all 4 center tiles are present. if all(tile[u, v].is_tile for u in (1, 2) for v in (1, 2)): vmf.create_ent( classname='func_instance', origin=position, angles=Matrix.from_basis(x=normal, z=supp_dir).to_angle(), file=config.inst_support, ) placed_support = True if placed_support and config.inst_support_ring: vmf.create_ent( classname='func_instance', origin=position, angles=angles, file=config.inst_support_ring, )