Пример #1
0
def res_rotate_inst(inst: Entity, res: Property) -> None:
    """Rotate the instance around an axis.

    If `axis` is specified, it should be a normal vector and the instance will
    be rotated `angle` degrees around it.
    Otherwise, `angle` is a pitch-yaw-roll angle which is applied.
    `around` can be a point (local, pre-rotation) which is used as the origin.
    """
    angles = Angle.from_str(inst['angles'])
    if 'axis' in res:
        orient = Matrix.axis_angle(
            Vec.from_str(inst.fixup.substitute(res['axis'])),
            conv_float(inst.fixup.substitute(res['angle'])),
        )
    else:
        orient = Matrix.from_angle(
            Angle.from_str(inst.fixup.substitute(res['angle'])))

    try:
        offset = Vec.from_str(inst.fixup.substitute(res['around']))
    except NoKeyError:
        pass
    else:
        origin = Vec.from_str(inst['origin'])
        inst['origin'] = origin + (-offset @ orient + offset) @ angles

    inst['angles'] = (orient @ angles).to_angle()
Пример #2
0
def res_set_marker(inst: Entity, res: Property) -> None:
    """Set a marker at a specific position.

    Parameters:
        * `global`: If true, the position is an absolute position, ignoring this instance.
        * `name`: A name to store to identify this marker/item.
        * `pos`: The position or offset to use for the marker.
    """
    origin = Vec.from_str(inst['origin'])
    orient = Matrix.from_angle(Angle.from_str(inst['angles']))

    try:
        is_global = srctools.conv_bool(
            inst.fixup.substitute(res['global'], allow_invert=True))
    except LookupError:
        is_global = False

    name = inst.fixup.substitute(res['name']).casefold()
    pos = Vec.from_str(inst.fixup.substitute(res['pos']))
    if not is_global:
        pos = pos @ orient + origin

    mark = Marker(pos, name, inst)
    MARKERS.append(mark)
    LOGGER.debug('Marker added: {}', mark)
Пример #3
0
 def __init__(self, inst: Entity, conf: Config, size: int) -> None:
     self.ent = inst
     self.conf = conf
     self.next = None
     self.no_prev = True
     self.size = size
     self.orient = Matrix.from_angle(Angle.from_str(inst['angles']))
Пример #4
0
def res_replace_instance(vmf: VMF, 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.
    """
    origin = Vec.from_str(inst['origin'])
    angles = Angle.from_str(inst['angles'])

    if not res.bool('keep_instance'):
        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'

    vmf.add_ent(new_ent)

    conditions.set_ent_keys(new_ent, inst, res)

    new_ent['origin'] = Vec.from_str(new_ent['origin']) @ angles + origin
    new_ent['angles'] = angles
    new_ent['targetname'] = inst['targetname']
Пример #5
0
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 = round(Vec(z=-1) @ Angle.from_str(inst['angles']), 6)
    origin: Vec = 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()
Пример #6
0
def res_add_placement_helper(inst: Entity, res: Property):
    """Add a placement helper to a specific tile.

    `Offset` and `normal` specify the position and direction out of the surface
    the helper should be added to. If `upDir` is specified, this is the
    direction of the top of the portal.
    """
    orient = Matrix.from_angle(Angle.from_str(inst['angles']))

    pos = conditions.resolve_offset(inst, res['offset', '0 0 0'], zoff=-64)
    normal = res.vec('normal', 0, 0, 1) @ orient

    up_dir: Optional[Vec]
    try:
        up_dir = Vec.from_str(res['upDir']) @ orient
    except LookupError:
        up_dir = None

    try:
        tile = tiling.TILES[(pos - 64 * normal).as_tuple(), normal.as_tuple()]
    except KeyError:
        LOGGER.warning('No tile at {} @ {}', pos, normal)
        return

    tile.add_portal_helper(up_dir)
Пример #7
0
class Node(Generic[ConfT]):
    """Represents a single node in the chain."""
    item: Item = attr.ib(init=True)
    conf: ConfT = attr.ib(init=True)

    # Origin and angles of the instance.
    pos = attr.ib(init=False,
                  default=attr.Factory(
                      lambda self: Vec.from_str(self.item.inst['origin']),
                      takes_self=True,
                  ))
    orient = attr.ib(init=False,
                     default=attr.Factory(
                         lambda self: Matrix.from_angle(
                             Angle.from_str(self.item.inst['angles'])),
                         takes_self=True,
                     ))

    # The links between nodes
    prev: Optional[Node[ConfT]] = attr.ib(default=None, init=False)
    next: Optional[Node[ConfT]] = attr.ib(default=None, init=False)

    @property
    def inst(self) -> Entity:
        """Return the relevant instance."""
        return self.item.inst

    @classmethod
    def from_inst(cls, inst: Entity, conf: ConfT) -> Node[ConfT]:
        """Find the item for this instance, and return the node."""
        name = inst['targetname']
        try:
            return Node(connections.ITEMS[name], conf)
        except KeyError:
            raise ValueError('No item for "{}"?'.format(name)) from None
Пример #8
0
 def add_item_coll(self, item: Item, inst: Entity) -> None:
     """Add the default collisions from an item definition for this instance."""
     origin = Vec.from_str(inst['origin'])
     orient = Matrix.from_angle(Angle.from_str(inst['angles']))
     for coll in item.collisions:
         self.add(
             (coll @ orient + origin).with_attrs(name=inst['targetname']))
Пример #9
0
    def __init__(self, ent: Entity) -> None:
        self.origin = Vec.from_str(ent['origin'])
        self.matrix = Matrix.from_angle(Angle.from_str(ent['angles']))
        self.ent = ent

        self.has_input = False  # We verify every node has an input if used.
        # DestType -> output.
        self.outputs: Dict[DestType, Optional[Node]] = dict.fromkeys(
            self.out_types, None)
        # Outputs fired when cubes reach this point.
        pass_outputs = [
            out for out in ent.outputs
            if out.output.casefold() == self.pass_out_name
        ]
        self.has_pass = bool(pass_outputs)
        if self.has_pass:
            for out in pass_outputs:
                out.output = 'On' + PASS_OUT
            if ent['classname'].startswith('comp_'):
                # Remove the extra keyvalues we use.
                ent.keys = {
                    'classname': 'info_target',
                    'targetname': ent['targetname'],
                    'origin': ent['origin'],
                    'angles': ent['angles'],
                }
            ent.make_unique('_vac_node')
        elif not self.keep_ent:
            ent.remove()
Пример #10
0
def res_monitor(inst: Entity, res: Property) -> None:
    """Result for the monitor component.

    Options:
    - bullseye_name: If possible to break this, this is the name to give the npc_bullseye.
    - bullseye_loc: This is the position to place the bullseye at.
    - bullseye_parent: This is the parent to give the bullseye.

    The fixup variable $is_breakable is set to True if lasers or turrets
    are present to indicate the func_breakable should be added.
    """
    global HAS_MONITOR
    import vbsp

    (
        bullseye_name,
        bullseye_loc,
        bullseye_parent,
    ) = res.value

    HAS_MONITOR = True

    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 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']),
            Angle.from_str(inst['angles']),
        )
        bullseye_name = conditions.local_name(inst, bullseye_name)
        inst.map.create_ent(
            classname='npc_bullseye',
            targetname=bullseye_name,
            parentname=conditions.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',
            parentname=bullseye_name,  # When killed, destroy this too.
            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)
Пример #11
0
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'), )
Пример #12
0
    def check_orient(inst: Entity) -> bool:
        """Check the orientation against the instance."""
        inst_normal = from_dir @ Angle.from_str(inst['angles'])

        if normal == 'WALL':
            # Special case - it's not on the floor or ceiling
            return abs(inst_normal.z) < 1e-6
        else:
            return inst_normal == normal or (
                allow_inverse and -inst_normal == normal
            )
Пример #13
0
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) @ Angle.from_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
Пример #14
0
 def shift_ent(inst: Entity) -> None:
     """Randomly shift the instance."""
     rng = rand.seed(b'rand_shift', inst, seed)
     pos = Vec(
         rng.uniform(min_x, max_x),
         rng.uniform(min_y, max_y),
         rng.uniform(min_z, max_z),
     )
     pos.localise(Vec.from_str(inst['origin']),
                  Angle.from_str(inst['angles']))
     inst['origin'] = pos
Пример #15
0
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']
Пример #16
0
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
Пример #17
0
def res_make_funnel_light(inst: Entity) -> None:
    """Place a light for Funnel items."""
    oran_on = inst.fixup.bool('$start_reversed')
    if inst.fixup['$conn_count_b'] != '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:
        name = ''
        if oran_on:
            need_oran = True
            need_blue = False
        else:
            need_blue = True
            need_oran = False

    loc = Vec(0, 0, -56) @ Angle.from_str(inst['angles']) + Vec.from_str(
        inst['origin'])

    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,
        )
Пример #18
0
def res_antigel(inst: Entity) -> None:
    """Implement the Antigel marker."""
    inst.remove()
    origin = Vec.from_str(inst['origin'])
    orient = Matrix.from_angle(Angle.from_str(inst['angles']))

    pos = round(origin - 128 * orient.up(), 6)
    norm = round(orient.up(), 6)
    try:
        tiling.TILES[pos.as_tuple(), norm.as_tuple()].is_antigel = True
    except KeyError:
        LOGGER.warning('No tile to set antigel at {}, {}', pos, norm)
    texturing.ANTIGEL_LOCS.add((origin // 128).as_tuple())
Пример #19
0
def parse_map(vmf: VMF, has_attr: Dict[str, bool]) -> None:
    """Find all glass/grating in the map.

    This removes the per-tile instances, and all original brushwork.
    The frames are updated with a fixup var, as appropriate.
    """
    frame_inst = resolve('[glass_frames]', silent=True)
    glass_inst = resolve_one('[glass_128]')

    pos = None
    for brush_ent in vmf.by_class['func_detail']:
        is_glass = False
        for face in brush_ent.sides():
            if face.mat == consts.Special.GLASS:
                has_attr['glass'] = True
                pos = face.get_origin()
                is_glass = True
                break
        if is_glass:
            brush_ent.remove()
            BARRIERS[get_pos_norm(pos)] = BarrierType.GLASS

    for brush_ent in vmf.by_class['func_brush']:
        is_grating = False
        for face in brush_ent.sides():
            if face.mat == consts.Special.GRATING:
                has_attr['grating'] = True
                pos = face.get_origin()
                is_grating = True
                break
        if is_grating:
            brush_ent.remove()
            BARRIERS[get_pos_norm(pos)] = BarrierType.GRATING

    for inst in vmf.by_class['func_instance']:
        filename = inst['file'].casefold()
        if filename == glass_inst:
            inst.remove()
        elif filename in frame_inst:
            # Add a fixup to allow distinguishing the type.
            pos = Vec.from_str(inst['origin']) // 128 * 128 + (64, 64, 64)
            norm = Vec(z=-1) @ Angle.from_str(inst['angles'])
            try:
                inst.fixup[consts.FixupVars.BEE_GLS_TYPE] = BARRIERS[
                    pos.as_tuple(), norm.as_tuple()].value
            except KeyError:
                LOGGER.warning('No glass/grating for frame at {}, {}?', pos,
                               norm)

    if options.get(str, 'glass_pack') and has_attr['glass']:
        packing.pack_list(vmf, options.get(str, 'glass_pack'))
Пример #20
0
def resolve_offset(inst, value: str, scale: float=1, zoff: float=0) -> Vec:
    """Retrieve an offset from an instance var. This allows several special values:

    * Any $replace variables
    * <piston_start> or <piston> to get the unpowered position of a piston plat
    * <piston_end> to get the powered position of a piston plat
    * <piston_top> to get the extended position of a piston plat
    * <piston_bottom> to get the retracted position of a piston plat

    If scale is set, read values are multiplied by this, and zoff is added to Z.
    """
    value = inst.fixup.substitute(value).casefold()
    # Offset the overlay by the given distance
    # Some special placeholder values:
    if value == '<piston_start>' or value == '<piston>':
        if inst.fixup.bool(consts.FixupVars.PIST_IS_UP):
            value = '<piston_top>'
        else:
            value = '<piston_bottom>'
    elif value == '<piston_end>':
        if inst.fixup.bool(consts.FixupVars.PIST_IS_UP):
            value = '<piston_bottom>'
        else:
            value = '<piston_top>'

    if value == '<piston_bottom>':
        offset = Vec(
            z=inst.fixup.int(consts.FixupVars.PIST_BTM) * 128,
        )
    elif value == '<piston_top>':
        offset = Vec(
            z=inst.fixup.int(consts.FixupVars.PIST_TOP) * 128,
        )
    else:
        # Regular vector
        offset = Vec.from_str(value) * scale
    offset.z += zoff

    offset.localise(
        Vec.from_str(inst['origin']),
        Angle.from_str(inst['angles']),
    )

    return offset
Пример #21
0
def res_transfer_bullseye(inst: Entity, props: Property):
    """Transfer catapult targets and placement helpers from one tile to another."""
    start_pos = conditions.resolve_offset(inst, props['start_pos', ''])
    end_pos = conditions.resolve_offset(inst, props['end_pos', ''])
    angles = Angle.from_str(inst['angles'])
    start_norm = props.vec('start_norm', 0, 0, 1) @ angles
    end_norm = props.vec('end_norm', 0, 0, 1) @ angles

    try:
        start_tile = tiling.TILES[(start_pos - 64 * start_norm).as_tuple(),
                                  start_norm.as_tuple()]
    except KeyError:
        LOGGER.warning(
            '"{}": Cannot find tile to transfer from at {}, {}!'.format(
                inst['targetname'], start_pos, start_norm))
        return

    end_tile = tiling.TileDef.ensure(
        end_pos - 64 * end_norm,
        end_norm,
    )
    # Now transfer the stuff.
    if start_tile.has_oriented_portal_helper:
        # We need to rotate this.
        orient = start_tile.portal_helper_orient.copy()
        # If it's directly opposite, just mirror - we have no clue what the
        # intent is.
        if Vec.dot(start_norm, end_norm) != -1.0:
            # Use the dict to compute the rotation to apply.
            orient @= NORM_ROTATIONS[start_norm.as_tuple(),
                                     end_norm.as_tuple()]
        end_tile.add_portal_helper(orient)
    elif start_tile.has_portal_helper:
        # Non-oriented, don't orient.
        end_tile.add_portal_helper()
    start_tile.remove_portal_helper(all=True)

    if start_tile.bullseye_count:
        end_tile.bullseye_count = start_tile.bullseye_count
        start_tile.bullseye_count = 0
        # Then transfer the targets across.
        for plate in faithplate.PLATES.values():
            if getattr(plate, 'target', None) is start_tile:
                plate.target = end_tile
Пример #22
0
def load_connectionpoint(item: Item, ent: Entity) -> None:
    """Allow more conveniently defining connectionpoints."""
    origin = Vec.from_str(ent['origin'])
    angles = Angle.from_str(ent['angles'])
    if round(angles.pitch) != 0.0 or round(angles.roll) != 0.0:
        LOGGER.warning(
            "Connection Point at {} is not flat on the floor, PeTI doesn't allow this.",
            origin,
        )
        return
    try:
        side = ConnSide.from_yaw(round(angles.yaw))
    except ValueError:
        LOGGER.warning(
            "Connection Point at {} must point in a cardinal direction, not {}!",
            origin,
            angles,
        )
        return

    orient = Matrix.from_yaw(round(angles.yaw))

    center = (origin - (-56, 56, 0)) / 16
    center.z = 0
    center.y = -center.y
    try:
        offset = SKIN_TO_CONN_OFFSETS[ent['skin']] @ orient
    except KeyError:
        LOGGER.warning('Connection Point at {} has invalid skin "{}"!', origin)
        return
    ant_pos = Coord(round(center.x + offset.x), round(center.y - offset.y), 0)
    sign_pos = Coord(round(center.x - offset.x), round(center.y + offset.y), 0)

    group_str = ent['group_id']

    item.antline_points[side].append(
        AntlinePoint(ant_pos, sign_pos, conv_int(ent['priority']),
                     int(group_str) if group_str.strip() else None))
Пример #23
0
    def insert_over(inst: Entity) -> None:
        """Apply the result."""
        temp_id = inst.fixup.substitute(orig_temp_id)

        origin = Vec.from_str(inst['origin'])
        angles = Angle.from_str(inst['angles', '0 0 0'])

        face_pos = conditions.resolve_offset(inst, face_str)
        normal = orig_norm @ angles

        # Don't make offset change the face_pos value..
        origin += offset @ angles

        for axis, norm in enumerate(normal):
            # Align to the center of the block grid. The normal direction is
            # already correct.
            if norm == 0:
                face_pos[axis] = face_pos[axis] // 128 * 128 + 64

        # Shift so that the user perceives the position as the pos of the face
        # itself.
        face_pos -= 64 * normal

        try:
            tiledef = tiling.TILES[face_pos.as_tuple(), normal.as_tuple()]
        except KeyError:
            LOGGER.warning(
                'Overlay brush position is not valid: {}',
                face_pos,
            )
            return

        temp = template_brush.import_template(
            vmf,
            temp_id,
            origin,
            angles,
            targetname=inst['targetname', ''],
            force_type=TEMP_TYPES.detail,
        )

        for over in temp.overlay:
            pos = Vec.from_str(over['basisorigin'])
            mat = over['material']
            try:
                replace = replace_tex[mat.casefold().replace('\\', '/')]
            except KeyError:
                pass
            else:
                mat = rand.seed(b'temp_over', temp_id, pos).choice(replace)

            if mat[:1] == '$':
                mat = inst.fixup[mat]
            if mat.startswith('<') or mat.endswith('>'):
                # Lookup in the texture data.
                gen, mat = texturing.parse_name(mat[1:-1])
                mat = gen.get(pos, mat)
            over['material'] = mat
            tiledef.bind_overlay(over)

        # Wipe the brushes from the map.
        if temp.detail is not None:
            temp.detail.remove()
            LOGGER.info(
                'Overlay template "{}" could set keep_brushes=0.',
                temp_id,
            )
Пример #24
0
def res_water_splash(vmf: VMF, inst: Entity, res: Property) -> None:
    """Creates splashes when something goes in and out of water.

    Arguments:

    - `parent`: The name of the parent entity.
    - `name`: The name given to the env_splash.
    - `scale`: The size of the effect (8 by default).
    - `position`: The offset position to place the entity.
    - `position2`: The offset to which the entity will move.
    - `type`: Use certain fixup values to calculate pos2 instead:
       `piston_1`/`2`/`3`/`4`: Use `$bottom_level` and `$top_level` as offsets.
       `track_platform`: Use `$travel_direction`, `$travel_distance`, etc.
    - `fast_check`: Check faster for movement. Needed for items which
      move quickly.
    """
    (
        name,
        parent,
        scale,
        pos1,
        pos2,
        calc_type,
        fast_check,
    ) = res.value  # type: str, str, float, Vec, Vec, str, str

    pos1 = pos1.copy()
    splash_pos = pos1.copy()

    if calc_type == 'track_platform':
        lin_off = srctools.conv_int(inst.fixup['$travel_distance'])
        travel_ang = Angle.from_str(inst.fixup['$travel_direction'])
        start_pos = srctools.conv_float(inst.fixup['$starting_position'])
        if start_pos:
            start_pos = round(start_pos * lin_off)
            pos1 += Vec(x=-start_pos) @ travel_ang

        pos2 = Vec(x=lin_off) @ travel_ang + pos1
    elif calc_type.startswith('piston'):
        # Use piston-platform offsetting.
        # The number is the highest offset to move to.
        max_pist = srctools.conv_int(calc_type.split('_', 2)[1], 4)
        bottom_pos = srctools.conv_int(inst.fixup['$bottom_level'])
        top_pos = min(srctools.conv_int(inst.fixup['$top_level']), max_pist)

        pos2 = pos1.copy()
        pos1 += Vec(z=128 * bottom_pos)
        pos2 += Vec(z=128 * top_pos)
        LOGGER.info('Bottom: {}, top: {}', bottom_pos, top_pos)
    else:
        # Directly from the given value.
        pos2 = Vec.from_str(conditions.resolve_value(inst, pos2))

    origin = Vec.from_str(inst['origin'])
    angles = Angle.from_str(inst['angles'])
    splash_pos.localise(origin, angles)
    pos1.localise(origin, angles)
    pos2.localise(origin, angles)

    # Since it's a straight line and you can't go through walls,
    # if pos1 and pos2 aren't in goo we aren't ever in goo.

    check_pos = [pos1, pos2]

    if pos1.z < origin.z:
        # If embedding in the floor, the positions can both be below the
        # actual surface. In that case check the origin too.
        check_pos.append(Vec(pos1.x, pos1.y, origin.z))

    if pos1.z == pos2.z:
        # Flat - this won't do anything...
        return

    for pos in check_pos:
        grid_pos = pos // 128 * 128
        grid_pos += (64, 64, 64)

        block = BLOCK_POS['world':pos]
        if block.is_goo:
            break
    else:
        return  # Not in goo at all

    water_pos = grid_pos + (0, 0, 32)

    # Check if both positions are above or below the water..
    # that means it won't ever trigger.
    if max(pos1.z, pos2.z) < water_pos.z - 8:
        return
    if min(pos1.z, pos2.z) > water_pos.z + 8:
        return

    # Pass along the water_pos encoded into the targetname.
    # Restrict the number of characters to allow direct slicing
    # in the script.
    enc_data = '_{:09.3f}{}'.format(
        water_pos.z + 12,
        'f' if fast_check else 's',
    )

    vmf.create_ent(
        classname='env_splash',
        targetname=conditions.local_name(inst, name) + enc_data,
        parentname=conditions.local_name(inst, parent),
        origin=splash_pos + (0, 0, 16),
        scale=scale,
        vscripts='BEE2/water_splash.nut',
        thinkfunction='Think',
        spawnflags='1',  # Trace downward to water surface.
    )
Пример #25
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
Пример #26
0
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 = vmf.create_ent(
        classname='func_instance',
        targetname=inst['targetname', ''],
        file=filename,
        angles=angles,
        origin=inst['origin'],
        fixup_style=res['fixup_style', '0'],
    )
    # 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
Пример #27
0
def res_signage(vmf: VMF, inst: Entity, res: Property):
    """Implement the Signage item."""
    sign: Optional[Sign]
    try:
        sign = (CONN_SIGNAGES if res.bool('connection') else
                SIGNAGES)[inst.fixup[consts.FixupVars.TIM_DELAY]]
    except KeyError:
        # Blank sign
        sign = None

    has_arrow = inst.fixup.bool(consts.FixupVars.ST_ENABLED)
    make_4x4 = res.bool('set4x4tile')

    sign_prim: Optional[Sign]
    sign_sec: Optional[Sign]

    if has_arrow:
        sign_prim = sign
        sign_sec = SIGNAGES['arrow']
    elif sign is not None:
        sign_prim = sign.primary or sign
        sign_sec = sign.secondary or None
    else:
        # Neither sign or arrow, delete this.
        inst.remove()
        return

    origin = Vec.from_str(inst['origin'])
    orient = Matrix.from_angle(Angle.from_str(inst['angles']))

    normal = -orient.up()
    forward = -orient.forward()

    prim_pos = Vec(0, -16, -64) @ orient + origin
    sec_pos = Vec(0, +16, -64) @ orient + origin

    template_id = res['template_id', '']

    if inst.fixup.bool(consts.FixupVars.ST_REVERSED):
        # Flip around.
        forward = -forward
        prim_visgroup = 'secondary'
        sec_visgroup = 'primary'
        prim_pos, sec_pos = sec_pos, prim_pos
    else:
        prim_visgroup = 'primary'
        sec_visgroup = 'secondary'

    if sign_prim and sign_sec:
        inst['file'] = res['large_clip', '']
        inst['origin'] = (prim_pos + sec_pos) / 2
    else:
        inst['file'] = res['small_clip', '']
        inst['origin'] = prim_pos if sign_prim else sec_pos

    brush_faces: List[Side] = []
    tiledef: Optional[tiling.TileDef] = None

    if template_id:
        if sign_prim and sign_sec:
            visgroup = [prim_visgroup, sec_visgroup]
        elif sign_prim:
            visgroup = [prim_visgroup]
        else:
            visgroup = [sec_visgroup]
        template = template_brush.import_template(
            vmf,
            template_id,
            origin,
            orient,
            force_type=template_brush.TEMP_TYPES.detail,
            additional_visgroups=visgroup,
        )

        for face in template.detail.sides():
            if face.normal() == normal:
                brush_faces.append(face)
    else:
        # Direct on the surface.
        # Find the grid pos first.
        grid_pos = (origin // 128) * 128 + 64
        try:
            tiledef = tiling.TILES[(grid_pos + 128 * normal).as_tuple(),
                                   (-normal).as_tuple()]
        except KeyError:
            LOGGER.warning(
                "Can't place signage at ({}) in ({}) direction!",
                origin,
                normal,
                exc_info=True,
            )
            return

    if sign_prim is not None:
        over = place_sign(
            vmf,
            brush_faces,
            sign_prim,
            prim_pos,
            normal,
            forward,
            rotate=True,
        )

        if tiledef is not None:
            tiledef.bind_overlay(over)
        if make_4x4:
            try:
                tile, u, v = tiling.find_tile(prim_pos, -normal)
            except KeyError:
                pass
            else:
                tile[u, v] = tile[u, v].as_4x4

    if sign_sec is not None:
        if has_arrow and res.bool('arrowDown'):
            # Arrow texture points down, need to flip it.
            forward = -forward

        over = place_sign(
            vmf,
            brush_faces,
            sign_sec,
            sec_pos,
            normal,
            forward,
            rotate=not has_arrow,
        )

        if tiledef is not None:
            tiledef.bind_overlay(over)
        if make_4x4:
            try:
                tile, u, v = tiling.find_tile(sec_pos, -normal)
            except KeyError:
                pass
            else:
                tile[u, v] = tile[u, v].as_4x4
Пример #28
0
def add_timer_relay(item: Item, has_sounds: bool) -> None:
    """Make a relay to play timer sounds, or fire once the outputs are done."""
    assert item.timer is not None

    rl_name = item.name + '_timer_rl'

    relay = item.inst.map.create_ent(
        'logic_relay',
        targetname=rl_name,
        startDisabled=0,
        spawnflags=0,
    )

    if item.config.timer_sound_pos:
        relay_loc = item.config.timer_sound_pos.copy()
        relay_loc.localise(
            Vec.from_str(item.inst['origin']),
            Angle.from_str(item.inst['angles']),
        )
        relay['origin'] = relay_loc
    else:
        relay['origin'] = item.inst['origin']

    for cmd in item.config.timer_done_cmd:
        if cmd:
            relay.add_out(
                Output(
                    'OnTrigger',
                    conditions.local_name(item.inst, cmd.target) or item.inst,
                    conditions.resolve_value(item.inst, cmd.input),
                    conditions.resolve_value(item.inst, cmd.params),
                    inst_in=cmd.inst_in,
                    delay=item.timer + cmd.delay,
                    times=cmd.times,
                ))

    if item.config.timer_sound_pos is not None and has_sounds:
        timer_sound = options.get(str, 'timer_sound')
        timer_cc = options.get(str, 'timer_sound_cc')

        # The default sound has 'ticking' closed captions.
        # So reuse that if the style doesn't specify a different noise.
        # If explicitly set to '', we don't use this at all!
        if timer_cc is None and timer_sound != 'Portal.room1_TickTock':
            timer_cc = 'Portal.room1_TickTock'
        if timer_cc:
            timer_cc = 'cc_emit ' + timer_cc

        # Write out the VScript code to precache the sound, and play it on
        # demand.
        relay['vscript_init_code'] = (
            'function Precache() {'
            f'self.PrecacheSoundScript(`{timer_sound}`)'
            '}')
        relay['vscript_init_code2'] = ('function snd() {'
                                       f'self.EmitSound(`{timer_sound}`)'
                                       '}')
        packing.pack_files(item.inst.map, timer_sound, file_type='sound')

        for delay in range(item.timer):
            relay.add_out(
                Output(
                    'OnTrigger',
                    '!self',
                    'CallScriptFunction',
                    'snd',
                    delay=delay,
                ))
            if timer_cc:
                relay.add_out(
                    Output(
                        'OnTrigger',
                        '@command',
                        'Command',
                        timer_cc,
                        delay=delay,
                    ))

    for outputs, cmd in [(item.timer_output_start(), 'Trigger'),
                         (item.timer_output_stop(), 'CancelPending')]:
        for output in outputs:
            item.add_io_command(output, rl_name, cmd)
Пример #29
0
    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, )
Пример #30
0
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 `angles` must be set to determine the required position, or `facing` can be set for older files.
    If `global` is set, the spawn point will be absolute instead of relative to the current instance.
    """
    if vbsp.GAME_MODE != 'COOP':
        return conditions.RES_EXHAUSTED

    is_tag = options.get(str, 'game_id') == utils.STEAM_IDS['TAG']

    origin = res.vec('origin')
    if 'angles' in res:
        angles = Angle.from_str(res['angles'])
    else:
        # Older system, specify the forward direction.
        angles = res.vec('facing', z=1).to_angle()

    # Some styles might want to ignore the instance we're running on.
    if not res.bool('global'):
        orient = Matrix.from_angle(Angle.from_str(inst['angles']))
        origin @= orient
        angles @= orient
        origin += Vec.from_str(inst['origin'])

    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',
            # Spelling mistake is correct.
            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 conditions.RES_EXHAUSTED