Exemple #1
0
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)
Exemple #2
0
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)
Exemple #3
0
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
Exemple #4
0
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)
Exemple #5
0
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)
Exemple #6
0
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
Exemple #7
0
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,
                )
Exemple #8
0
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,
                )