Beispiel #1
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 `facing` must be set to determine the required position.
    If `global` is set, the spawn point will be absolute instead of relative to the current instance.
    """
    if vbsp.GAME_MODE != 'COOP':
        return RES_EXHAUSTED

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

    origin = res.vec('origin')
    normal = res.vec('facing', z=1)

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

    angles = normal.to_angle()

    if is_tag:
        vmf.create_ent(
            classname='func_instance',
            targetname='paint_gun',
            origin=origin - (0, 0, 16),
            angles=angles,
            # Generated by the BEE2 app.
            file='instances/bee2/tag_coop_gun.vmf',
        )
        # Blocks ATLAS from having a gun
        vmf.create_ent(
            classname='info_target',
            targetname='supress_blue_portalgun_spawn',
            origin=origin,
            angles='0 0 0',
        )
        # Allows info_target to work
        vmf.create_ent(
            classname='env_global',
            targetname='no_spawns',
            globalstate='portalgun_nospawn',
            initialstate=1,
            spawnflags=1,  # Use initial state
            origin=origin,
        )
    vmf.create_ent(
        classname='info_coop_spawn',
        targetname='@coop_spawn_blue',
        ForceGunOnSpawn=int(not is_tag),
        origin=origin,
        angles=angles,
        enabled=1,
        StartingTeam=3,  # ATLAS
    )
    return RES_EXHAUSTED
Beispiel #2
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 `facing` must be set to determine the required position.
    If `global` is set, the spawn point will be absolute instead of relative to the current instance.
    """
    if vbsp.GAME_MODE != 'COOP':
        return RES_EXHAUSTED

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

    origin = res.vec('origin')
    normal = res.vec('facing', z=1)

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

    angles = normal.to_angle()

    if is_tag:
        vmf.create_ent(
            classname='func_instance',
            targetname='paint_gun',
            origin=origin - (0, 0, 16),
            angles=angles,
            # Generated by the BEE2 app.
            file='instances/bee2/tag_coop_gun.vmf',
        )
        # Blocks ATLAS from having a gun
        vmf.create_ent(
            classname='info_target',
            targetname='supress_blue_portalgun_spawn',
            origin=origin,
            angles='0 0 0',
        )
        # Allows info_target to work
        vmf.create_ent(
            classname='env_global',
            targetname='no_spawns',
            globalstate='portalgun_nospawn',
            initialstate=1,
            spawnflags=1,  # Use initial state
            origin=origin,
        )
    vmf.create_ent(
        classname='info_coop_spawn',
        targetname='@coop_spawn_blue',
        ForceGunOnSpawn=int(not is_tag),
        origin=origin,
        angles=angles,
        enabled=1,
        StartingTeam=3,  # ATLAS
    )
    return RES_EXHAUSTED
Beispiel #3
0
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property):
    """Create the spawn point for ATLAS, in Aperture Tag.

    This creates an instance with the desired orientation.
    The two parameters 'origin' and 'angles' must be set.
    """
    if vbsp.GAME_MODE != 'COOP':
        return RES_EXHAUSTED

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

    origin = res.vec('origin')
    normal = res.vec('facing', z=1)

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

    angles = normal.to_angle()

    if is_tag:
        vmf.create_ent(
            classname='func_instance',
            targetname='paint_gun',
            origin=origin - (0, 0, 16),
            angles=angles,
            # Generated by the BEE2 app.
            file='instances/bee2/tag_coop_gun.vmf',
        )
        # Blocks ATLAS from having a gun
        vmf.create_ent(
            classname='info_target',
            targetname='supress_blue_portalgun_spawn',
            origin=origin,
            angles='0 0 0',
        )
        # Allows info_target to work
        vmf.create_ent(
            classname='env_global',
            targetname='no_spawns',
            globalstate='portalgun_nospawn',
            initialstate=1,
            spawnflags=1,  # Use initial state
            origin=origin,
        )
    vmf.create_ent(
        classname='info_coop_spawn',
        targetname='@coop_spawn_blue',
        ForceGunOnSpawn=int(not is_tag),
        origin=origin,
        angles=angles,
        enabled=1,
        StartingTeam=3,  # ATLAS
    )
    return RES_EXHAUSTED
Beispiel #4
0
def res_sendificator_laser(res: Property):
    """Record the position of the target for Sendificator Lasers."""
    target = res.vec('offset'), res.vec('direction', 0, 0, 1)

    def set_laser(inst: Entity) -> None:
        """Store off the target position."""
        SENDTOR_TARGETS[inst['targetname']] = target

    return set_laser
Beispiel #5
0
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property):
    """Create the spawn point for ATLAS, in Aperture Tag.

    This creates an instance with the desired orientation.
    The two parameters 'origin' and 'angles' must be set.
    """
    if vbsp.GAME_MODE != 'COOP':
        return RES_EXHAUSTED

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

    offset = res.vec('origin').rotate_by_str(inst['angles'])
    normal = res.vec('facing', z=1).rotate_by_str(
        inst['angles'],
    )

    origin = Vec.from_str(inst['origin'])
    origin += offset
    angles = normal.to_angle()

    if is_tag:
        vmf.create_ent(
            classname='func_instance',
            targetname='paint_gun',
            origin=origin - (0, 0, 16),
            angles=angles,
            # Generated by the BEE2 app.
            file='instances/bee2/tag_coop_gun.vmf',
        )
        # Blocks ATLAS from having a gun
        vmf.create_ent(
            classname='info_target',
            targetname='supress_blue_portalgun_spawn',
            origin=origin,
            angles='0 0 0',
        )
        # Allows info_target to work
        vmf.create_ent(
            classname='env_global',
            targetname='no_spawns',
            globalstate='portalgun_nospawn',
            initialstate=1,
            spawnflags=1,  # Use initial state
            origin=origin,
        )
    vmf.create_ent(
        classname='info_coop_spawn',
        targetname='@coop_spawn_blue',
        ForceGunOnSpawn=int(not is_tag),
        origin=origin,
        angles=angles,
        enabled=1,
        StartingTeam=3,  # ATLAS
    )
    return RES_EXHAUSTED
Beispiel #6
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', ''])
    start_norm = props.vec('start_norm', 0, 0, 1).rotate_by_str(inst['angles'])
    end_norm = props.vec('end_norm', 0, 0, 1).rotate_by_str(inst['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.rotate(*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
Beispiel #7
0
def res_monitor_setup(res: Property):
    return (
        res['breakInst', None],
        res['bullseye_name', ''],
        res.vec('bullseye_loc'),
        res['bullseye_parent', ''],
    )
Beispiel #8
0
def res_monitor_setup(res: Property):
    return (
        res['breakInst', None],
        res['bullseye_name', ''],
        res.vec('bullseye_loc'),
        res['bullseye_parent', ''],
    )
Beispiel #9
0
def res_monitor_setup(res: Property):
    """Pre-parse options for monitors."""
    return (
        res['bullseye_name', ''],
        res.vec('bullseye_loc'),
        res['bullseye_parent', ''],
    )
Beispiel #10
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: Vec | None
    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)
Beispiel #11
0
def res_set_faith_setup(res: Property) -> tuple:
    temp_name = res['template', '']

    if temp_name:
        template = template_brush.get_template(temp_name)
    else:
        template = None

    return (
        template,
        res.vec('offset'),
    )
Beispiel #12
0
    def parse(cls, conf: Property) -> 'FizzlerBrush':
        """Parse from a config file."""
        if 'side_color' in conf:
            side_color = conf.vec('side_color')
        else:
            side_color = None

        outputs = [
            Output.parse(prop)
            for prop in
            conf.find_children('Outputs')
        ]

        textures = {}
        for group in TexGroup:
            textures[group] = conf['tex_' + group.value, None]

        keys = {
            prop.name: prop.value
            for prop in
            conf.find_children('keys')
        }

        local_keys = {
            prop.name: prop.value
            for prop in
            conf.find_children('localkeys')
        }

        if 'classname' not in keys:
            raise ValueError(
                'Fizzler Brush "{}" does not have a classname!'.format(
                conf['name'],
                )
            )

        return FizzlerBrush(
            name=conf['name'],
            textures=textures,
            keys=keys,
            local_keys=local_keys,
            outputs=outputs,
            thickness=conf.float('thickness', 2.0),
            stretch_center=conf.bool('stretch_center', True),
            side_color=side_color,
            singular=conf.bool('singular'),
            mat_mod_name=conf['mat_mod_name', None],
            mat_mod_var=conf['mat_mod_var', None],
            set_axis_var=conf.bool('set_axis_var'),
        )
Beispiel #13
0
    def parse(cls, conf: Property) -> 'FizzlerBrush':
        """Parse from a config file."""
        if 'side_color' in conf:
            side_color = conf.vec('side_color')
        else:
            side_color = None

        outputs = [
            Output.parse(prop)
            for prop in
            conf.find_children('Outputs')
        ]

        textures = {}
        for group in TexGroup:
            textures[group] = conf['tex_' + group.value, None]

        keys = {
            prop.name: prop.value
            for prop in
            conf.find_children('keys')
        }

        local_keys = {
            prop.name: prop.value
            for prop in
            conf.find_children('localkeys')
        }

        if 'classname' not in keys:
            raise ValueError(
                'Fizzler Brush "{}" does not have a classname!'.format(
                conf['name'],
                )
            )

        return FizzlerBrush(
            name=conf['name'],
            textures=textures,
            keys=keys,
            local_keys=local_keys,
            outputs=outputs,
            thickness=conf.float('thickness', 2.0),
            stretch_center=conf.bool('stretch_center', True),
            side_color=side_color,
            singular=conf.bool('singular'),
            mat_mod_name=conf['mat_mod_name', None],
            mat_mod_var=conf['mat_mod_var', None],
            set_axis_var=conf.bool('set_axis_var'),
        )
Beispiel #14
0
    def __init__(
        self,
        sky_id,
        selitem_data: SelitemData,
        config: lazy_conf.LazyConf,
        fog_opts: Property,
        mat,
    ) -> None:
        self.id = sky_id
        self.selitem_data = selitem_data
        self.material = mat
        self.config = config
        self.fog_opts = fog_opts

        # Extract this for selector windows to easily display
        self.fog_color = fog_opts.vec('primarycolor', 255, 255, 255)
Beispiel #15
0
    def __init__(
        self,
        sky_id,
        selitem_data: SelitemData,
        config: Property,
        fog_opts: Property,
        mat,
    ) -> None:
        self.id = sky_id
        self.selitem_data = selitem_data
        self.material = mat
        self.config = config
        set_cond_source(config, 'Skybox <{}>'.format(sky_id))
        self.fog_opts = fog_opts

        # Extract this for selector windows to easily display
        self.fog_color = fog_opts.vec('primarycolor', 255, 255, 255)
Beispiel #16
0
def flag_blockpos_type(inst: Entity, flag: Property):
    """Determine the type of a grid position.

    If the value is single value, that should be the type.
    Otherwise, the value should be a block with 'offset' and 'type' values.
    The offset is in block increments, with 0 0 0 equal to the mounting surface.

    The type should be a space-seperated list of locations:
    * VOID (Outside the map)
    * SOLID (Full wall cube)
    * EMBED (Hollow wall cube)
    * AIR (Inside the map, may be occupied by items)
    * OCCUPIED (Known to be occupied by items)
    * PIT (Bottomless pits, any)
      * PIT_SINGLE (one-high)
      * PIT_TOP
      * PIT_MID
      * PIT_BOTTOM
    * GOO
      * GOO_SINGLE (one-deep goo)
      * GOO_TOP (goo surface)
      * GOO_MID
      * GOO_BOTTOM (floor)
    """
    if flag.has_children():
        pos = flag.vec('offset') * 128
        types = flag['type'].split()
    else:
        types = flag.value.split()
        pos = Vec()
    pos.z -= 128
    pos.localise(
        Vec.from_str(inst['origin']),
        Vec.from_str(inst['angles']),
    )
    block = brushLoc.POS['world':pos]
    for block_type in types:
        try:
            allowed = brushLoc.BLOCK_LOOKUP[block_type.casefold()]
        except KeyError:
            raise ValueError(
                '"{}" is not a valid block type!'.format(block_type))
        if block in allowed:
            return True
    return False
Beispiel #17
0
def res_set_block(inst: Entity, res: Property):
    """Set a block to the given value.

    This should be used only if you know what is in the position.
    The offset is in block increments, with 0 0 0 equal to the mounting surface.
    """
    pos = res.vec('offset') * 128
    try:
        new_vals = brushLoc.BLOCK_LOOKUP[res['type'].casefold()]
    except KeyError:
        raise ValueError('"{}" is not a valid block type!'.format(res['type']))

    try:
        [new_val] = new_vals
    except ValueError:
        raise ValueError("Can't use compound block types ({})!".format(
            res['type']))

    pos.z -= 128
    pos.localise(
        Vec.from_str(inst['origin']),
        Vec.from_str(inst['angles']),
    )
    brushLoc.POS['world':pos] = new_val
Beispiel #18
0
def res_fix_rotation_axis(vmf: VMF, ent: Entity, res: Property):
    """Properly setup rotating brush entities to match the instance.

    This uses the orientation of the instance to determine the correct
    spawnflags to make it rotate in the correct direction.

    This can either modify an existing entity (which may be in an instance),
    or generate a new one. The generated brush will be 2x2x2 units large,
    and always set to be non-solid.

    For both modes:
    - `Axis`: specifies the rotation axis local to the instance.
    - `Reversed`: If set, flips the direction around.
    - `Classname`: Specifies which entity, since the spawnflags required varies.

    For application to an existing entity:
    - `ModifyTarget`: The local name of the entity to modify.

    For brush generation mode:

    - `Pos` and `name` are local to the
      instance, and will set the `origin` and `targetname` respectively.
    - `Keys` are any other keyvalues to be be set.
    - `Flags` sets additional spawnflags. Multiple values may be
       separated by `+`, and will be added together.
    - `Classname` specifies which entity will be created, as well as
       which other values will be set to specify the correct orientation.
    - `AddOut` is used to add outputs to the generated entity. It takes
       the options `Output`, `Target`, `Input`, `Inst_targ`, `Param` and `Delay`. If
       `Inst_targ` is defined, it will be used with the input to construct
       an instance proxy input. If `OnceOnly` is set, the output will be
       deleted when fired.

    Permitted entities:

       * [`func_door_rotating`](https://developer.valvesoftware.com/wiki/func_door_rotating)
       * [`func_platrot`](https://developer.valvesoftware.com/wiki/func_platrot)
       * [`func_rot_button`](https://developer.valvesoftware.com/wiki/func_rot_button)
       * [`func_rotating`](https://developer.valvesoftware.com/wiki/func_rotating)
       * [`momentary_rot_button`](https://developer.valvesoftware.com/wiki/momentary_rot_button)
    """
    des_axis = res['axis', 'z'].casefold()
    reverse = res.bool('reversed')
    door_type = res['classname', 'func_door_rotating']
    orient = Matrix.from_angle(Angle.from_str(ent['angles']))

    axis = round(Vec.with_axes(des_axis, 1) @ orient, 6)

    if axis.x > 0 or axis.y > 0 or axis.z > 0:
        # If it points forward, we need to reverse the rotating door
        reverse = not reverse
    axis = abs(axis)

    try:
        flag_values = FLAG_ROTATING[door_type]
    except KeyError:
        LOGGER.warning('Unknown rotating brush type "{}"!', door_type)
        return

    name = res['ModifyTarget', '']
    door_ent: Entity | None
    if name:
        name = conditions.local_name(ent, name)
        setter_loc = ent['origin']
        door_ent = None
        spawnflags = 0
    else:
        # Generate a brush.
        name = conditions.local_name(ent, res['name', ''])

        pos = res.vec('Pos') @ Angle.from_str(ent['angles', '0 0 0'])
        pos += Vec.from_str(ent['origin', '0 0 0'])
        setter_loc = str(pos)

        door_ent = vmf.create_ent(
            classname=door_type,
            targetname=name,
            origin=pos.join(' '),
        )
        # Extra stuff to apply to the flags (USE, toggle, etc)
        spawnflags = sum(
            map(
                # Add together multiple values
                srctools.conv_int,
                res['flags', '0'].split('+')
                # Make the door always non-solid!
            )) | flag_values.get('solid_flags', 0)

        conditions.set_ent_keys(door_ent, ent, res)

        for output in res.find_all('AddOut'):
            door_ent.add_out(
                Output(
                    out=output['Output', 'OnUse'],
                    inp=output['Input', 'Use'],
                    targ=output['Target', ''],
                    inst_in=output['Inst_targ', None],
                    param=output['Param', ''],
                    delay=srctools.conv_float(output['Delay', '']),
                    times=(1 if srctools.conv_bool(output['OnceOnly',
                                                          False]) else -1),
                ))

        # Generate brush
        door_ent.solids = [vmf.make_prism(pos - 1, pos + 1).solid]

    # Add or remove flags as needed
    for flag, value in zip(
        ('x', 'y', 'z', 'rev'),
        [axis.x > 1e-6, axis.y > 1e-6, axis.z > 1e-6, reverse],
    ):
        if flag not in flag_values:
            continue
        if door_ent is not None:
            if value:
                spawnflags |= flag_values[flag]
            else:
                spawnflags &= ~flag_values[flag]
        else:  # Place a KV setter to set this.
            vmf.create_ent(
                'comp_kv_setter',
                origin=setter_loc,
                target=name,
                mode='flags',
                kv_name=flag_values[flag],
                kv_value_global=value,
            )
    if door_ent is not None:
        door_ent['spawnflags'] = spawnflags

    # This ent uses a keyvalue for reversing...
    if door_type == 'momentary_rot_button':
        vmf.create_ent(
            'comp_kv_setter',
            origin=setter_loc,
            target=name,
            mode='kv',
            kv_name='StartDirection',
            kv_value_global='1' if reverse else '-1',
        )
Beispiel #19
0
def res_import_template(vmf: VMF, coll: Collisions, res: Property):
    """Import a template VMF file, retexturing it to match orientation.

    It will be placed overlapping the given instance. If no block is used, only
    ID can be specified.
    Options:

    - `ID`: The ID of the template to be inserted. Add visgroups to additionally
            add after a colon, comma-seperated (`temp_id:vis1,vis2`).
            Either section, or the whole value can be a `$fixup`.
    - `angles`: Override the instance rotation, so it is always rotated this much.
    - `rotation`: Apply the specified rotation before the instance's rotation.
    - `offset`: Offset the template from the instance's position.
    - `force`: a space-seperated list of overrides. If 'white' or 'black' is
             present, the colour of tiles will be overridden. If `invert` is
            added, white/black tiles will be swapped. If a tile size
            (`2x2`, `4x4`, `wall`, `special`) is included, all tiles will
            be switched to that size (if not a floor/ceiling). If 'world' or
            'detail' is present, the brush will be forced to that type.
    - `replace`: A block of template material -> replacement textures.
            This is case insensitive - any texture here will not be altered
            otherwise. If the material starts with a `#`, it is instead a
            list of face IDs separated by spaces. If the result evaluates
            to "", no change occurs. Both can be $fixups (parsed first).
    - `bindOverlay`: Bind overlays in this template to the given surface, and
            bind overlays on a surface to surfaces in this template.
            The value specifies the offset to the surface, where 0 0 0 is the
            floor position. It can also be a block of multiple positions.
    - `alignBindOverlay`: If set, align the bindOverlay offsets to the grid.
    - `keys`/`localkeys`: If set, a brush entity will instead be generated with
            these values. This overrides force world/detail.
            Specially-handled keys:
            - `"origin"`, offset automatically.
            - `"movedir"` on func_movelinear - set a normal surrounded by `<>`,
              this gets replaced with angles.
    - `colorVar`: If this fixup var is set
            to `white` or `black`, that colour will be forced.
            If the value is `<editor>`, the colour will be chosen based on
            the color of the surface for ItemButtonFloor, funnels or
            entry/exit frames.
    - `invertVar`: If this fixup value is true, tile colour will be
            swapped to the opposite of the current force option. This applies
            after colorVar.
    - `visgroup`: Sets how visgrouped parts are handled. Several values are possible:
            - A property block: Each name should match a visgroup, and the
              value should be a block of flags that if true enables that group.
            - 'none' (default): All extra groups are ignored.
            - 'choose': One group is chosen randomly.
            - a number: The percentage chance for each visgroup to be added.
    - `visgroup_force_var`: If set and True, visgroup is ignored and all groups
            are added.
    - `pickerVars`:
            If this is set, the results of colorpickers can be read
            out of the template. The key is the name of the picker, the value
            is the fixup name to write to. The output is either 'white',
            'black' or ''.
    - `outputs`: Add outputs to the brush ent. Syntax is like VMFs, and all names
            are local to the instance.
    - `senseOffset`: If set, colorpickers and tilesetters will be treated
            as being offset by this amount.
    """
    if res.has_children():
        orig_temp_id = res['id']
    else:
        orig_temp_id = res.value
        res = Property('TemplateBrush', [])

    force = res['force', ''].casefold().split()
    if 'white' in force:
        conf_force_colour = texturing.Portalable.white
    elif 'black' in force:
        conf_force_colour = texturing.Portalable.black
    elif 'invert' in force:
        conf_force_colour = 'INVERT'
    else:
        conf_force_colour = None

    if 'world' in force:
        force_type = template_brush.TEMP_TYPES.world
    elif 'detail' in force:
        force_type = template_brush.TEMP_TYPES.detail
    else:
        force_type = template_brush.TEMP_TYPES.default

    force_grid: texturing.TileSize | None
    size: texturing.TileSize
    for size in texturing.TileSize:
        if size in force:
            force_grid = size
            break
    else:
        force_grid = None

    if 'bullseye' in force:
        surf_cat = texturing.GenCat.BULLSEYE
    elif 'special' in force or 'panel' in force:
        surf_cat = texturing.GenCat.PANEL
    else:
        surf_cat = texturing.GenCat.NORMAL

    replace_tex: dict[str, list[str]] = {}
    for prop in res.find_block('replace', or_blank=True):
        replace_tex.setdefault(prop.name, []).append(prop.value)

    if 'replaceBrush' in res:
        LOGGER.warning(
            'replaceBrush command used for template "{}", which is no '
            'longer used.',
            orig_temp_id,
        )
    bind_tile_pos = [
        # So it's the floor block location.
        Vec.from_str(value) - (0, 0, 128)
        for value in res.find_key('BindOverlay', or_blank=True).as_array()
    ]
    align_bind_overlay = res.bool('alignBindOverlay')

    key_values = res.find_block("Keys", or_blank=True)
    if key_values:
        key_block = Property("", [
            key_values,
            res.find_block("LocalKeys", or_blank=True),
        ])
        # Ensure we have a 'origin' keyvalue - we automatically offset that.
        if 'origin' not in key_values:
            key_values['origin'] = '0 0 0'

        # Spawn everything as detail, so they get put into a brush
        # entity.
        force_type = template_brush.TEMP_TYPES.detail
        outputs = [Output.parse(prop) for prop in res.find_children('Outputs')]
    else:
        key_block = None
        outputs = []

    # None = don't add any more.
    visgroup_func: Callable[[Random, list[str]], Iterable[str]] | None = None

    try:  # allow both spellings.
        visgroup_prop = res.find_key('visgroups')
    except NoKeyError:
        visgroup_prop = res.find_key('visgroup', 'none')
    if visgroup_prop.has_children():
        visgroup_instvars = list(visgroup_prop)
    else:
        visgroup_instvars = []
        visgroup_mode = res['visgroup', 'none'].casefold()
        # Generate the function which picks which visgroups to add to the map.
        if visgroup_mode == 'none':
            pass
        elif visgroup_mode == 'choose':

            def visgroup_func(rng: Random, groups: list[str]) -> Iterable[str]:
                """choose = add one random group."""
                return [rng.choice(groups)]
        else:
            percent = srctools.conv_float(visgroup_mode.rstrip('%'), 0.00)
            if percent > 0.0:

                def visgroup_func(rng: Random,
                                  groups: list[str]) -> Iterable[str]:
                    """Number = percent chance for each to be added"""
                    for group in sorted(groups):
                        if rng.uniform(0, 100) <= percent:
                            yield group

    picker_vars = [(prop.real_name, prop.value)
                   for prop in res.find_children('pickerVars')]
    try:
        ang_override = to_matrix(Angle.from_str(res['angles']))
    except LookupError:
        ang_override = None
    try:
        rotation = to_matrix(Angle.from_str(res['rotation']))
    except LookupError:
        rotation = Matrix()

    offset = res['offset', '0 0 0']
    invert_var = res['invertVar', '']
    color_var = res['colorVar', '']
    if color_var.casefold() == '<editor>':
        color_var = '<editor>'

    # If true, force visgroups to all be used.
    visgroup_force_var = res['forceVisVar', '']

    sense_offset = res.vec('senseOffset')

    def place_template(inst: Entity) -> None:
        """Place a template."""
        temp_id = inst.fixup.substitute(orig_temp_id)

        # Special case - if blank, just do nothing silently.
        if not temp_id:
            return

        temp_name, visgroups = template_brush.parse_temp_name(temp_id)
        try:
            template = template_brush.get_template(temp_name)
        except template_brush.InvalidTemplateName:
            # If we did lookup, display both forms.
            if temp_id != orig_temp_id:
                LOGGER.warning('{} -> "{}" is not a valid template!',
                               orig_temp_id, temp_name)
            else:
                LOGGER.warning('"{}" is not a valid template!', temp_name)
            # We don't want an error, just quit.
            return

        for vis_flag_block in visgroup_instvars:
            if all(
                    conditions.check_flag(flag, coll, inst)
                    for flag in vis_flag_block):
                visgroups.add(vis_flag_block.real_name)

        force_colour = conf_force_colour
        if color_var == '<editor>':
            # Check traits for the colour it should be.
            traits = instance_traits.get(inst)
            if 'white' in traits:
                force_colour = texturing.Portalable.white
            elif 'black' in traits:
                force_colour = texturing.Portalable.black
            else:
                LOGGER.warning(
                    '"{}": Instance "{}" '
                    "isn't one with inherent color!",
                    temp_id,
                    inst['file'],
                )
        elif color_var:
            color_val = conditions.resolve_value(inst, color_var).casefold()

            if color_val == 'white':
                force_colour = texturing.Portalable.white
            elif color_val == 'black':
                force_colour = texturing.Portalable.black
        # else: no color var

        if srctools.conv_bool(conditions.resolve_value(inst, invert_var)):
            force_colour = template_brush.TEMP_COLOUR_INVERT[conf_force_colour]
        # else: False value, no invert.

        if ang_override is not None:
            orient = ang_override
        else:
            orient = rotation @ Angle.from_str(inst['angles', '0 0 0'])
        origin = conditions.resolve_offset(inst, offset)

        # If this var is set, it forces all to be included.
        if srctools.conv_bool(
                conditions.resolve_value(inst, visgroup_force_var)):
            visgroups.update(template.visgroups)
        elif visgroup_func is not None:
            visgroups.update(
                visgroup_func(
                    rand.seed(b'temp', template.id, origin, orient),
                    list(template.visgroups),
                ))

        LOGGER.debug('Placing template "{}" at {} with visgroups {}',
                     template.id, origin, visgroups)

        temp_data = template_brush.import_template(
            vmf,
            template,
            origin,
            orient,
            targetname=inst['targetname'],
            force_type=force_type,
            add_to_map=True,
            coll=coll,
            additional_visgroups=visgroups,
            bind_tile_pos=bind_tile_pos,
            align_bind=align_bind_overlay,
        )

        if key_block is not None:
            conditions.set_ent_keys(temp_data.detail, inst, key_block)
            br_origin = Vec.from_str(key_block.find_key('keys')['origin'])
            br_origin.localise(origin, orient)
            temp_data.detail['origin'] = br_origin

            move_dir = temp_data.detail['movedir', '']
            if move_dir.startswith('<') and move_dir.endswith('>'):
                move_dir = Vec.from_str(move_dir) @ orient
                temp_data.detail['movedir'] = move_dir.to_angle()

            for out in outputs:
                out = out.copy()
                out.target = conditions.local_name(inst, out.target)
                temp_data.detail.add_out(out)

        template_brush.retexture_template(
            temp_data,
            origin,
            inst.fixup,
            replace_tex,
            force_colour,
            force_grid,
            surf_cat,
            sense_offset,
        )

        for picker_name, picker_var in picker_vars:
            picker_val = temp_data.picker_results.get(picker_name, None)
            if picker_val is not None:
                inst.fixup[picker_var] = picker_val.value
            else:
                inst.fixup[picker_var] = ''

    return place_template
Beispiel #20
0
def edit_panel(vmf: VMF, inst: Entity, props: Property, create: bool) -> None:
    """Implements SetPanelOptions and CreatePanel."""
    orient = Matrix.from_angle(Angle.from_str(inst['angles']))
    normal: Vec = round(props.vec('normal', 0, 0, 1) @ orient, 6)
    origin = Vec.from_str(inst['origin'])
    uaxis, vaxis = Vec.INV_AXIS[normal.axis()]

    points: set[tuple[float, float, float]] = set()

    if 'point' in props:
        for prop in props.find_all('point'):
            points.add(
                conditions.resolve_offset(inst, prop.value,
                                          zoff=-64).as_tuple())
    elif 'pos1' in props and 'pos2' in props:
        pos1, pos2 = Vec.bbox(
            conditions.resolve_offset(inst,
                                      props['pos1', '-48 -48 0'],
                                      zoff=-64),
            conditions.resolve_offset(inst, props['pos2', '48 48 0'],
                                      zoff=-64),
        )
        points.update(map(Vec.as_tuple, Vec.iter_grid(pos1, pos2, 32)))
    else:
        # Default to the full tile.
        points.update({(Vec(u, v, -64.0) @ orient + origin).as_tuple()
                       for u in [-48.0, -16.0, 16.0, 48.0]
                       for v in [-48.0, -16.0, 16.0, 48.0]})

    tiles_to_uv: dict[tiling.TileDef, set[tuple[int, int]]] = defaultdict(set)
    for pos in points:
        try:
            tile, u, v = tiling.find_tile(Vec(pos), normal, force=create)
        except KeyError:
            continue
        tiles_to_uv[tile].add((u, v))

    if not tiles_to_uv:
        LOGGER.warning('"{}": No tiles found for panels!', inst['targetname'])
        return

    # If bevels is provided, parse out the overall world positions.
    bevel_world: set[tuple[int, int]] | None
    try:
        bevel_prop = props.find_key('bevel')
    except NoKeyError:
        bevel_world = None
    else:
        bevel_world = set()
        if bevel_prop.has_children():
            # Individually specifying offsets.
            for bevel_str in bevel_prop.as_array():
                bevel_point = Vec.from_str(bevel_str) @ orient + origin
                bevel_world.add(
                    (int(bevel_point[uaxis]), int(bevel_point[vaxis])))
        elif srctools.conv_bool(bevel_prop.value):
            # Fill the bounding box.
            bbox_min, bbox_max = Vec.bbox(map(Vec, points))
            off = Vec.with_axes(uaxis, 32, vaxis, 32)
            bbox_min -= off
            bbox_max += off
            for pos in Vec.iter_grid(bbox_min, bbox_max, 32):
                if pos.as_tuple() not in points:
                    bevel_world.add((pos[uaxis], pos[vaxis]))
        # else: No bevels.
    panels: list[tiling.Panel] = []

    for tile, uvs in tiles_to_uv.items():
        if create:
            panel = tiling.Panel(
                None,
                inst,
                tiling.PanelType.NORMAL,
                thickness=4,
                bevels=(),
            )
            panel.points = uvs
            tile.panels.append(panel)
        else:
            for panel in tile.panels:
                if panel.same_item(inst) and panel.points == uvs:
                    break
            else:
                LOGGER.warning('No panel to modify found for "{}"!',
                               inst['targetname'])
                continue
        panels.append(panel)

        pan_type = '<nothing?>'
        try:
            pan_type = conditions.resolve_value(inst, props['type'])
            panel.pan_type = tiling.PanelType(pan_type.lower())
        except LookupError:
            pass
        except ValueError:
            raise ValueError('Unknown panel type "{}"!'.format(pan_type))

        if 'thickness' in props:
            panel.thickness = srctools.conv_int(
                conditions.resolve_value(inst, props['thickness']))
            if panel.thickness not in (2, 4, 8):
                raise ValueError(
                    '"{}": Invalid panel thickess {}!\n'
                    'Must be 2, 4 or 8.',
                    inst['targetname'],
                    panel.thickness,
                )

        if bevel_world is not None:
            panel.bevels.clear()
            for u, v in bevel_world:
                # Convert from world points to UV positions.
                u = (u - tile.pos[uaxis] + 48) // 32
                v = (v - tile.pos[vaxis] + 48) // 32
                # Cull outside here, we wont't use them.
                if -1 <= u <= 4 and -1 <= v <= 4:
                    panel.bevels.add((u, v))

        if 'offset' in props:
            panel.offset = conditions.resolve_offset(inst, props['offset'])
            panel.offset -= Vec.from_str(inst['origin'])
        if 'template' in props:
            # We only want the template inserted once. So remove it from all but one.
            if len(panels) == 1:
                panel.template = inst.fixup.substitute(props['template'])
            else:
                panel.template = ''
        if 'nodraw' in props:
            panel.nodraw = srctools.conv_bool(
                inst.fixup.substitute(props['nodraw'], allow_invert=True))
        if 'seal' in props:
            panel.seal = srctools.conv_bool(
                inst.fixup.substitute(props['seal'], allow_invert=True))
        if 'move_bullseye' in props:
            panel.steals_bullseye = srctools.conv_bool(
                inst.fixup.substitute(props['move_bullseye'],
                                      allow_invert=True))
    if 'keys' in props or 'localkeys' in props:
        # First grab the existing ent, so we can edit it.
        # These should all have the same value, unless they were independently
        # edited with mismatching point sets. In that case destroy all those existing ones.
        existing_ents: set[Entity
                           | None] = {panel.brush_ent
                                      for panel in panels}
        try:
            [brush_ent] = existing_ents
        except ValueError:
            LOGGER.warning(
                'Multiple independent panels for "{}" were made, then the '
                'brush entity was edited as a group! Discarding '
                'individual ents...', inst['targetname'])
            for brush_ent in existing_ents:
                if brush_ent is not None and brush_ent in vmf.entities:
                    brush_ent.remove()
            brush_ent = None

        if brush_ent is None:
            brush_ent = vmf.create_ent('')

        old_pos = brush_ent.keys.pop('origin', None)

        conditions.set_ent_keys(brush_ent, inst, props)
        if not brush_ent['classname']:
            if create:  # This doesn't make sense, you could just omit the prop.
                LOGGER.warning(
                    'No classname provided for panel "{}"!',
                    inst['targetname'],
                )
            # Make it a world brush.
            brush_ent.remove()
            brush_ent = None
        else:
            # We want to do some post-processing.
            # Localise any origin value.
            if 'origin' in brush_ent.keys:
                pos = Vec.from_str(brush_ent['origin'])
                pos.localise(
                    Vec.from_str(inst['origin']),
                    Angle.from_str(inst['angles']),
                )
                brush_ent['origin'] = pos
            elif old_pos is not None:
                brush_ent['origin'] = old_pos

            # If it's func_detail, clear out all the keys.
            # Particularly `origin`, but the others are useless too.
            if brush_ent['classname'] == 'func_detail':
                brush_ent.clear_keys()
                brush_ent['classname'] = 'func_detail'
        for panel in panels:
            panel.brush_ent = brush_ent
Beispiel #21
0
def res_sendificator_laser_setup(res: Property):
    return (
        res.vec('offset'),
        res.vec('direction', 0, 0, 1)
    )
Beispiel #22
0
def res_reshape_fizzler(vmf: VMF, shape_inst: Entity, res: Property):
    """Convert a fizzler connected via the output to a new shape.

    This allows for different placing of fizzler items.
    Each `segment` parameter should be a `x y z;x y z` pair of positions
    that represent the ends of the fizzler.
    `up_axis` should be set to a normal vector pointing in the new 'upward'
    direction.
    `default` is the ID of a fizzler type which should be used if no outputs
    are fired.
    """
    shape_name = shape_inst['targetname']
    shape_item = connections.ITEMS[shape_name]

    for conn in shape_item.outputs:
        fizz_name = conn.inp.name
        try:
            fizz = fizzler.FIZZLERS[fizz_name]
        except KeyError:
            LOGGER.warning(
                'Reshaping fizzler with non-fizzler output! Ignoring!')
            continue
        break
    else:
        # No fizzler - create one.
        conn = None
        fizz_type = fizzler.FIZZ_TYPES[res['default']]
        base_inst = vmf.create_ent(
            targetname=shape_name,
            classname='func_instance',
            origin=shape_inst['origin'],
            file=fizz_type.inst[fizzler.FizzInst.BASE][0],
        )
        base_inst.fixup.update(shape_inst.fixup)
        fizz = fizzler.FIZZLERS[shape_name] = fizzler.Fizzler(
            fizz_type,
            Vec(),
            base_inst,
            [],
        )

    # Detach this connection and remove traces of it.
    if conn:
        conn.remove()
        if shape_item.ind_toggle:
            remove_ant_toggle(shape_item.ind_toggle)

    fizz_base = fizz.base_inst
    fizz_base['origin'] = shape_inst['origin']
    origin = Vec.from_str(shape_inst['origin'])

    shape_angles = Vec.from_str(shape_inst['angles'])

    fizz.up_axis = res.vec('up_axis').rotate(*shape_angles)
    fizz.emitters.clear()

    for seg_prop in res.find_all('Segment'):
        vec1, vec2 = seg_prop.value.split(';')
        seg_min_max = Vec.bbox(
            Vec.from_str(vec1).rotate(*shape_angles) + origin,
            Vec.from_str(vec2).rotate(*shape_angles) + origin,
        )
        fizz.emitters.append(seg_min_max)
Beispiel #23
0
    def parse(item_id: str, conf: Property):
        """Read the item type info from the given config."""

        def get_outputs(prop_name):
            """Parse all the outputs with this name."""
            return [
                Output.parse(prop)
                for prop in
                conf.find_all(prop_name)
                # Allow blank to indicate no output.
                if prop.value != ''
            ]

        enable_cmd = get_outputs('enable_cmd')
        disable_cmd = get_outputs('disable_cmd')
        lock_cmd = get_outputs('lock_cmd')
        unlock_cmd = get_outputs('unlock_cmd')

        inf_lock_only = conf.bool('inf_lock_only')

        timer_done_cmd = get_outputs('timer_done_cmd')
        if 'timer_sound_pos' in conf:
            timer_sound_pos = conf.vec('timer_sound_pos')
            force_timer_sound = conf.bool('force_timer_sound')
        else:
            timer_sound_pos = None
            force_timer_sound = False

        try:
            input_type = InputType(
                conf['Type', 'default'].casefold()
            )
        except ValueError:
            raise ValueError('Invalid input type "{}": {}'.format(
                item_id, conf['type'],
            )) from None

        invert_var = conf['invertVar', '0']

        try:
            spawn_fire = FeatureMode(conf['spawnfire', 'never'].casefold())
        except ValueError:
            # Older config option - it was a bool for always/never.
            spawn_fire_bool = conf.bool('spawnfire', None)
            if spawn_fire_bool is None:
                raise  # Nope, not a bool.

            spawn_fire = FeatureMode.ALWAYS if spawn_fire_bool else FeatureMode.NEVER

        try:
            sec_spawn_fire = FeatureMode(conf['sec_spawnfire', 'never'].casefold())
        except ValueError:  # Default to primary value.
            sec_spawn_fire = FeatureMode.NEVER

        if input_type is InputType.DUAL:
            sec_enable_cmd = get_outputs('sec_enable_cmd')
            sec_disable_cmd = get_outputs('sec_disable_cmd')

            try:
                default_dual = CONN_TYPE_NAMES[
                    conf['Default_Dual', 'primary'].casefold()
                ]
            except KeyError:
                raise ValueError('Invalid default type for "{}": {}'.format(
                    item_id, conf['Default_Dual'],
                )) from None

            # We need an affinity to use when nothing else specifies it.
            if default_dual is ConnType.DEFAULT:
                raise ValueError('Must specify a default type for "{}"!'.format(
                    item_id,
                )) from None

            sec_invert_var = conf['sec_invertVar', '0']
        else:
            # No dual type, set to dummy values.
            sec_enable_cmd = []
            sec_disable_cmd = []
            default_dual = ConnType.DEFAULT
            sec_invert_var = ''

        try:
            output_type = CONN_TYPE_NAMES[
                conf['DualType', 'default'].casefold()
            ]
        except KeyError:
            raise ValueError('Invalid output affinity for "{}": {}'.format(
                item_id, conf['DualType'],
            )) from None

        def get_input(prop_name: str):
            """Parse an input command."""
            try:
                return Output.parse_name(conf[prop_name])
            except IndexError:
                return None

        out_act = get_input('out_activate')
        out_deact = get_input('out_deactivate')
        out_lock = get_input('out_lock')
        out_unlock = get_input('out_unlock')

        timer_start = timer_stop = None
        if 'out_timer_start' in conf:
            timer_start = [
                Output.parse_name(prop.value)
                for prop in conf.find_all('out_timer_start')
                if prop.value
            ]
        if 'out_timer_stop' in conf:
            timer_stop = [
                Output.parse_name(prop.value)
                for prop in conf.find_all('out_timer_stop')
                if prop.value
            ]

        return Config(
            item_id, default_dual, input_type,
            spawn_fire, invert_var, enable_cmd, disable_cmd,
            sec_spawn_fire, sec_invert_var, sec_enable_cmd, sec_disable_cmd,
            output_type, out_act, out_deact,
            lock_cmd, unlock_cmd, out_lock, out_unlock, inf_lock_only,
            timer_sound_pos, timer_done_cmd, force_timer_sound,
            timer_start, timer_stop,
        )
Beispiel #24
0
def res_sendificator_laser_setup(res: Property):
    return (res.vec('offset'), res.vec('direction', 0, 0, 1))
Beispiel #25
0
def res_antlaser(vmf: VMF, res: Property):
    """The condition to generate AntLasers.

    This is executed once to modify all instances.
    """
    conf_inst = instanceLocs.resolve(res['instance'])
    conf_glow_height = Vec(z=res.float('GlowHeight', 48) - 64)
    conf_las_start = Vec(z=res.float('LasStart') - 64)
    conf_rope_off = res.vec('RopePos')
    conf_toggle_targ = res['toggleTarg', '']

    beam_conf = res.find_key('BeamKeys', [])
    glow_conf = res.find_key('GlowKeys', [])
    cable_conf = res.find_key('CableKeys', [])

    if beam_conf:
        # Grab a copy of the beam spawnflags so we can set our own options.
        conf_beam_flags = beam_conf.int('spawnflags')
        # Mask out certain flags.
        conf_beam_flags &= (
            0
            | 1  # Start On
            | 2  # Toggle
            | 4  # Random Strike
            | 8  # Ring
            | 16  # StartSparks
            | 32  # EndSparks
            | 64  # Decal End
            #| 128  # Shade Start
            #| 256  # Shade End
            #| 512  # Taper Out
        )
    else:
        conf_beam_flags = 0

    conf_outputs = [
        Output.parse(prop)
        for prop in res
        if prop.name in ('onenabled', 'ondisabled')
    ]

    # Find all the markers.
    nodes = {}  # type: Dict[str, Item]

    for inst in vmf.by_class['func_instance']:
        if inst['file'].casefold() not in conf_inst:
            continue
        name = inst['targetname']
        try:
            # Remove the item - it's no longer going to exist after
            # we're done.
            nodes[name] = connections.ITEMS.pop(name)
        except KeyError:
            raise ValueError('No item for "{}"?'.format(name)) from None

    if not nodes:
        # None at all.
        return conditions.RES_EXHAUSTED

    # Now find every connected group, recording inputs, outputs and links.
    todo = set(nodes.values())

    groups = []  # type: List[Group]

    # Node -> is grouped already.
    node_pairing = dict.fromkeys(nodes.values(), False)

    while todo:
        start = todo.pop()
        # Synthesise the Item used for logic.
        # We use a random info_target to manage the IO data.
        group = Group(start)
        groups.append(group)
        for node in group.nodes:
            # If this node has no non-node outputs, destroy the antlines.
            has_output = False
            node_pairing[node] = True

            for conn in list(node.outputs):
                neighbour = conn.to_item
                todo.discard(neighbour)
                pair_state = node_pairing.get(neighbour, None)
                if pair_state is None:
                    # Not a node, a target of our logic.
                    conn.from_item = group.item
                    has_output = True
                    continue
                elif pair_state is False:
                    # Another node.
                    group.nodes.append(neighbour)
                # else: True, node already added.

                # For nodes, connect link.
                conn.remove()
                group.links.add(frozenset({node, neighbour}))

            # If we have a real output, we need to transfer it.
            # Otherwise we can just destroy it.
            if has_output:
                node.transfer_antlines(group.item)
            else:
                node.delete_antlines()

            # Do the same for inputs, so we can catch that.
            for conn in list(node.inputs):
                neighbour = conn.from_item
                todo.discard(neighbour)
                pair_state = node_pairing.get(neighbour, None)
                if pair_state is None:
                    # Not a node, an input to the group.
                    conn.to_item = group.item
                    continue
                elif pair_state is False:
                    # Another node.
                    group.nodes.append(neighbour)
                # else: True, node already added.

                # For nodes, connect link.
                conn.remove()
                group.links.add(frozenset({neighbour, node}))

    # Now every node is in a group. Generate the actual entities.
    for group in groups:
        # We generate two ent types. For each marker, we add a sprite
        # and a beam pointing at it. Then for each connection
        # another beam.

        # Choose a random antlaser name to use for our group.
        base_name = group.nodes[0].name

        out_enable = [Output('', '', 'FireUser2')]
        out_disable = [Output('', '', 'FireUser1')]
        for output in conf_outputs:
            if output.output.casefold() == 'onenabled':
                out_enable.append(output.copy())
            else:
                out_disable.append(output.copy())

        if conf_toggle_targ:
            # Make the group info_target into a texturetoggle.
            toggle = group.item.inst
            toggle['classname'] = 'env_texturetoggle'
            toggle['target'] = conditions.local_name(group.nodes[0].inst, conf_toggle_targ)

        group.item.enable_cmd = tuple(out_enable)
        group.item.disable_cmd = tuple(out_disable)

        # Node -> index for targetnames.
        indexes = {}  # type: Dict[Item, int]

        # For cables, it's a bit trickier than the beams.
        # The cable ent itself is the one which decides what it links to,
        # so we need to potentially make endpoint cables at locations with
        # only "incoming" lines.
        # So this dict is either a targetname to indicate cables with an
        # outgoing connection, or the entity for endpoints without an outgoing
        # connection.
        cable_points = {}  # type: Dict[Item, Union[Entity, str]]

        for i, node in enumerate(group.nodes, start=1):
            indexes[node] = i
            node.name = base_name

            sprite_pos = conf_glow_height.copy()
            sprite_pos.localise(
                Vec.from_str(node.inst['origin']),
                Vec.from_str(node.inst['angles']),
            )

            if glow_conf:
                # First add the sprite at the right height.
                sprite = vmf.create_ent('env_sprite')
                for prop in glow_conf:
                    sprite[prop.name] = conditions.resolve_value(node.inst, prop.value)

                sprite['origin'] = sprite_pos
                sprite['targetname'] = NAME_SPR(base_name, i)
            elif beam_conf:
                # If beams but not sprites, we need a target.
                vmf.create_ent(
                    'info_target',
                    origin=sprite_pos,
                    targetname=NAME_SPR(base_name, i),
                )

            if beam_conf:
                # Now the beam going from below up to the sprite.
                beam_pos = conf_las_start.copy()
                beam_pos.localise(
                    Vec.from_str(node.inst['origin']),
                    Vec.from_str(node.inst['angles']),
                )
                beam = vmf.create_ent('env_beam')
                for prop in beam_conf:
                    beam[prop.name] = conditions.resolve_value(node.inst, prop.value)

                beam['origin'] = beam['targetpoint'] = beam_pos
                beam['targetname'] = NAME_BEAM_LOW(base_name, i)
                beam['LightningStart'] = beam['targetname']
                beam['LightningEnd'] = NAME_SPR(base_name, i)
                beam['spawnflags'] = conf_beam_flags | 128  # Shade Start

        if beam_conf:
            for i, (node_a, node_b) in enumerate(group.links):
                beam = vmf.create_ent('env_beam')
                conditions.set_ent_keys(beam, node_a.inst, res, 'BeamKeys')
                beam['origin'] = beam['targetpoint'] = node_a.inst['origin']
                beam['targetname'] = NAME_BEAM_CONN(base_name, i)
                beam['LightningStart'] = NAME_SPR(base_name, indexes[node_a])
                beam['LightningEnd'] = NAME_SPR(base_name, indexes[node_b])
                beam['spawnflags'] = conf_beam_flags

        # We have a couple different situations to deal with here.
        # Either end could Not exist, be Unlinked, or be Linked = 8 combos.
        # Always flip so we do A to B.
        # AB |
        # NN | Make 2 new ones, one is an endpoint.
        # NU | Flip, do UN.
        # NL | Make A, link A to B. Both are linked.
        # UN | Make B, link A to B. B is unlinked.
        # UU | Link A to B, A is now linked, B is unlinked.
        # UL | Link A to B. Both are linked.
        # LN | Flip, do NL.
        # LU | Flip, do UL
        # LL | Make A, link A to B. Both are linked.
        if cable_conf:
            rope_ind = 0  # Uniqueness value.
            for node_a, node_b in group.links:
                state_a, ent_a = RopeState.from_node(cable_points, node_a)
                state_b, ent_b = RopeState.from_node(cable_points, node_b)

                if (state_a is RopeState.LINKED
                   or (state_a is RopeState.NONE and
                       state_b is RopeState.UNLINKED)
                ):
                    # Flip these, handle the opposite order.
                    state_a, state_b = state_b, state_a
                    ent_a, ent_b = ent_b, ent_a
                    node_a, node_b = node_b, node_a

                pos_a = conf_rope_off.copy()
                pos_a.localise(
                    Vec.from_str(node_a.inst['origin']),
                    Vec.from_str(node_a.inst['angles']),
                )

                pos_b = conf_rope_off.copy()
                pos_b.localise(
                    Vec.from_str(node_b.inst['origin']),
                    Vec.from_str(node_b.inst['angles']),
                )

                # Need to make the A rope if we don't have one that's unlinked.
                if state_a is not RopeState.UNLINKED:
                    rope_a = vmf.create_ent('move_rope')
                    for prop in beam_conf:
                        rope_a[prop.name] = conditions.resolve_value(node_a.inst, prop.value)
                    rope_a['origin'] = pos_a
                    rope_ind += 1
                    rope_a['targetname'] = NAME_CABLE(base_name, rope_ind)
                else:
                    # It is unlinked, so it's the rope to use.
                    rope_a = ent_a

                # Only need to make the B rope if it doesn't have one.
                if state_b is RopeState.NONE:
                    rope_b = vmf.create_ent('move_rope')
                    for prop in beam_conf:
                        rope_b[prop.name] = conditions.resolve_value(node_b.inst, prop.value)
                    rope_b['origin'] = pos_b
                    rope_ind += 1
                    name_b = rope_b['targetname'] = NAME_CABLE(base_name, rope_ind)

                    cable_points[node_b] = rope_b  # Someone can use this.
                elif state_b is RopeState.UNLINKED:
                    # Both must be unlinked, we aren't using this link though.
                    name_b = ent_b['targetname']
                else:  # Linked, we just have the name.
                    name_b = ent_b

                # By here, rope_a should be an unlinked rope,
                # and name_b should be a name to link to.
                rope_a['nextkey'] = name_b

                # Figure out how much slack to give.
                # If on floor, we need to be taut to have clearance.

                if on_floor(node_a) or on_floor(node_b):
                    rope_a['slack'] = 60
                else:
                    rope_a['slack'] = 125

                # We're always linking A to B, so A is always linked!
                if state_a is not RopeState.LINKED:
                    cable_points[node_a] = rope_a['targetname']

    return conditions.RES_EXHAUSTED
Beispiel #26
0
def res_antlaser(vmf: VMF, res: Property) -> object:
    """The condition to generate AntLasers and Antline Corners.

    This is executed once to modify all instances.
    """
    conf_inst_corner = instanceLocs.resolve('<item_bee2_antline_corner>',
                                            silent=True)
    conf_inst_laser = instanceLocs.resolve(res['instance'])
    conf_glow_height = Vec(z=res.float('GlowHeight', 48) - 64)
    conf_las_start = Vec(z=res.float('LasStart') - 64)
    conf_rope_off = res.vec('RopePos')
    conf_toggle_targ = res['toggleTarg', '']

    beam_conf = res.find_key('BeamKeys', or_blank=True)
    glow_conf = res.find_key('GlowKeys', or_blank=True)
    cable_conf = res.find_key('CableKeys', or_blank=True)

    if beam_conf:
        # Grab a copy of the beam spawnflags so we can set our own options.
        conf_beam_flags = beam_conf.int('spawnflags')
        # Mask out certain flags.
        conf_beam_flags &= (
            0
            | 1  # Start On
            | 2  # Toggle
            | 4  # Random Strike
            | 8  # Ring
            | 16  # StartSparks
            | 32  # EndSparks
            | 64  # Decal End
            #| 128  # Shade Start
            #| 256  # Shade End
            #| 512  # Taper Out
        )
    else:
        conf_beam_flags = 0

    conf_outputs = [
        Output.parse(prop) for prop in res
        if prop.name in ('onenabled', 'ondisabled')
    ]

    # Find all the markers.
    nodes: dict[str, Node] = {}

    for inst in vmf.by_class['func_instance']:
        filename = inst['file'].casefold()
        name = inst['targetname']
        if filename in conf_inst_laser:
            node_type = NodeType.LASER
        elif filename in conf_inst_corner:
            node_type = NodeType.CORNER
        else:
            continue

        try:
            # Remove the item - it's no longer going to exist after
            # we're done.
            item = connections.ITEMS.pop(name)
        except KeyError:
            raise ValueError('No item for "{}"?'.format(name)) from None
        pos = Vec.from_str(inst['origin'])
        orient = Matrix.from_angle(Angle.from_str(inst['angles']))
        if node_type is NodeType.CORNER:
            timer_delay = item.inst.fixup.int('$timer_delay')
            # We treat inf, 1, 2 and 3 as the same, to get around the 1 and 2 not
            # being selectable issue.
            pos = CORNER_POS[max(0, timer_delay - 3) % 8] @ orient + pos
        nodes[name] = Node(node_type, inst, item, pos, orient)

    if not nodes:
        # None at all.
        return conditions.RES_EXHAUSTED

    # Now find every connected group, recording inputs, outputs and links.
    todo = set(nodes.values())

    groups: list[Group] = []

    while todo:
        start = todo.pop()
        # Synthesise the Item used for logic.
        # We use a random info_target to manage the IO data.
        group = Group(start, start.type)
        groups.append(group)
        for node in group.nodes:
            # If this node has no non-node outputs, destroy the antlines.
            has_output = False
            node.is_grouped = True

            for conn in list(node.item.outputs):
                neighbour = conn.to_item
                neigh_node = nodes.get(neighbour.name, None)
                todo.discard(neigh_node)
                if neigh_node is None or neigh_node.type is not node.type:
                    # Not a node or different item type, it must therefore
                    # be a target of our logic.
                    conn.from_item = group.item
                    has_output = True
                    continue
                elif not neigh_node.is_grouped:
                    # Another node.
                    group.nodes.append(neigh_node)
                # else: True, node already added.

                # For nodes, connect link.
                conn.remove()
                group.links.add(frozenset({node, neigh_node}))

            # If we have a real output, we need to transfer it.
            # Otherwise we can just destroy it.
            if has_output:
                node.item.transfer_antlines(group.item)
            else:
                node.item.delete_antlines()

            # Do the same for inputs, so we can catch that.
            for conn in list(node.item.inputs):
                neighbour = conn.from_item
                neigh_node = nodes.get(neighbour.name, None)
                todo.discard(neigh_node)
                if neigh_node is None or neigh_node.type is not node.type:
                    # Not a node or different item type, it must therefore
                    # be a target of our logic.
                    conn.to_item = group.item
                    node.had_input = True
                    continue
                elif not neigh_node.is_grouped:
                    # Another node.
                    group.nodes.append(neigh_node)
                # else: True, node already added.

                # For nodes, connect link.
                conn.remove()
                group.links.add(frozenset({neigh_node, node}))

    # Now every node is in a group. Generate the actual entities.
    for group in groups:
        # We generate two ent types. For each marker, we add a sprite
        # and a beam pointing at it. Then for each connection
        # another beam.

        # Choose a random item name to use for our group.
        base_name = group.nodes[0].item.name

        out_enable = [Output('', '', 'FireUser2')]
        out_disable = [Output('', '', 'FireUser1')]
        if group.type is NodeType.LASER:
            for output in conf_outputs:
                if output.output.casefold() == 'onenabled':
                    out_enable.append(output.copy())
                else:
                    out_disable.append(output.copy())

        group.item.enable_cmd = tuple(out_enable)
        group.item.disable_cmd = tuple(out_disable)

        if group.type is NodeType.LASER and conf_toggle_targ:
            # Make the group info_target into a texturetoggle.
            toggle = group.item.inst
            toggle['classname'] = 'env_texturetoggle'
            toggle['target'] = conditions.local_name(group.nodes[0].inst,
                                                     conf_toggle_targ)

        # Node -> index for targetnames.
        indexes: dict[Node, int] = {}

        # For antline corners, the antline segments.
        segments: list[antlines.Segment] = []

        # frozenset[Node] unpacking isn't clear.
        node_a: Node
        node_b: Node

        if group.type is NodeType.CORNER:
            for node_a, node_b in group.links:
                # Place a straight antline between each connected node.
                # If on the same plane, we only need one. If not, we need to
                # do one for each plane it's in.
                offset = node_b.pos - node_a.pos
                up_a = node_a.orient.up()
                up_b = node_b.orient.up()
                plane_a = Vec.dot(node_a.pos, up_a)
                plane_b = Vec.dot(node_b.pos, up_b)
                if Vec.dot(up_a, up_b) > 0.9:
                    if abs(plane_a - plane_b) > 1e-6:
                        LOGGER.warning(
                            'Antline corners "{}" - "{}" '
                            'are on different planes',
                            node_a.item.name,
                            node_b.item.name,
                        )
                        continue
                    u = node_a.orient.left()
                    v = node_a.orient.forward()
                    # Which are we aligned to?
                    if abs(Vec.dot(offset, u)) < 1e-6 or abs(Vec.dot(
                            offset, v)) < 1e-6:
                        forward = offset.norm()
                        group.add_ant_straight(
                            up_a,
                            node_a.pos + 8.0 * forward,
                            node_b.pos - 8.0 * forward,
                        )
                    else:
                        LOGGER.warning(
                            'Antline corners "{}" - "{}" '
                            'are not directly aligned',
                            node_a.item.name,
                            node_b.item.name,
                        )
                else:
                    # We expect them be aligned to each other.
                    side = Vec.cross(up_a, up_b)
                    if abs(Vec.dot(side, offset)) < 1e-6:
                        mid1 = node_a.pos + Vec.dot(offset, up_b) * up_b
                        mid2 = node_b.pos - Vec.dot(offset, up_a) * up_a
                        if mid1 != mid2:
                            LOGGER.warning(
                                'Midpoint mismatch: {} != {} for "{}" - "{}"',
                                mid1,
                                mid2,
                                node_a.item.name,
                                node_b.item.name,
                            )
                        group.add_ant_straight(
                            up_a,
                            node_a.pos + 8.0 * (mid1 - node_a.pos).norm(),
                            mid1,
                        )
                        group.add_ant_straight(
                            up_b,
                            node_b.pos + 8.0 * (mid2 - node_b.pos).norm(),
                            mid2,
                        )

        # For cables, it's a bit trickier than the beams.
        # The cable ent itself is the one which decides what it links to,
        # so we need to potentially make endpoint cables at locations with
        # only "incoming" lines.
        # So this dict is either a targetname to indicate cables with an
        # outgoing connection, or the entity for endpoints without an outgoing
        # connection.
        cable_points: dict[Node, Union[Entity, str]] = {}

        for i, node in enumerate(group.nodes, start=1):
            indexes[node] = i
            node.item.name = base_name

            if group.type is NodeType.CORNER:
                node.inst.remove()
                # Figure out whether we want a corner at this point, or
                # just a regular dot. If a non-node input was provided it's
                # always a corner. Otherwise it's one if there's an L, T or X
                # junction.
                use_corner = True
                norm = node.orient.up().as_tuple()
                if not node.had_input:
                    neighbors = [
                        mag * direction for direction in [
                            node.orient.forward(),
                            node.orient.left(),
                        ] for mag in [-8.0, 8.0]
                        if ((node.pos + mag * direction).as_tuple(),
                            norm) in group.ant_seg
                    ]
                    if len(neighbors) == 2:
                        [off1, off2] = neighbors
                        if Vec.dot(off1, off2) < -0.99:
                            # ---o---, merge together. The endpoints we want
                            # are the other ends of the two segments.
                            group.add_ant_straight(
                                node.orient.up(),
                                group.rem_ant_straight(norm, node.pos + off1),
                                group.rem_ant_straight(norm, node.pos + off2),
                            )
                            use_corner = False
                    elif len(neighbors) == 1:
                        # o-----, merge.
                        [offset] = neighbors
                        group.add_ant_straight(
                            node.orient.up(),
                            group.rem_ant_straight(norm, node.pos + offset),
                            node.pos - offset,
                        )
                        use_corner = False
                if use_corner:
                    segments.append(
                        antlines.Segment(
                            antlines.SegType.CORNER,
                            round(node.orient.up(), 3),
                            Vec(node.pos),
                            Vec(node.pos),
                        ))
            elif group.type is NodeType.LASER:
                sprite_pos = node.pos + conf_glow_height @ node.orient

                if glow_conf:
                    # First add the sprite at the right height.
                    sprite = vmf.create_ent('env_sprite')
                    for prop in glow_conf:
                        sprite[prop.name] = conditions.resolve_value(
                            node.inst, prop.value)

                    sprite['origin'] = sprite_pos
                    sprite['targetname'] = NAME_SPR(base_name, i)
                elif beam_conf:
                    # If beams but not sprites, we need a target.
                    vmf.create_ent(
                        'info_target',
                        origin=sprite_pos,
                        targetname=NAME_SPR(base_name, i),
                    )

                if beam_conf:
                    # Now the beam going from below up to the sprite.
                    beam_pos = node.pos + conf_las_start @ node.orient
                    beam = vmf.create_ent('env_beam')
                    for prop in beam_conf:
                        beam[prop.name] = conditions.resolve_value(
                            node.inst, prop.value)

                    beam['origin'] = beam['targetpoint'] = beam_pos
                    beam['targetname'] = NAME_BEAM_LOW(base_name, i)
                    beam['LightningStart'] = beam['targetname']
                    beam['LightningEnd'] = NAME_SPR(base_name, i)
                    beam['spawnflags'] = conf_beam_flags | 128  # Shade Start

        segments += set(group.ant_seg.values())
        if group.type is NodeType.CORNER and segments:
            group.item.antlines.add(
                antlines.Antline(group.item.name + '_antline', segments))

        if group.type is NodeType.LASER and beam_conf:
            for i, (node_a, node_b) in enumerate(group.links):
                beam = vmf.create_ent('env_beam')
                conditions.set_ent_keys(beam, node_a.inst, res, 'BeamKeys')
                beam['origin'] = beam['targetpoint'] = node_a.pos
                beam['targetname'] = NAME_BEAM_CONN(base_name, i)
                beam['LightningStart'] = NAME_SPR(base_name, indexes[node_a])
                beam['LightningEnd'] = NAME_SPR(base_name, indexes[node_b])
                beam['spawnflags'] = conf_beam_flags

        if group.type is NodeType.LASER and cable_conf:
            build_cables(
                vmf,
                group,
                cable_points,
                base_name,
                beam_conf,
                conf_rope_off,
            )

    return conditions.RES_EXHAUSTED
Beispiel #27
0
def res_set_tile(inst: Entity, res: Property) -> None:
    """Set 4x4 parts of a tile to the given values.

    `Offset` defines the position of the upper-left tile in the grid.
    Each `Tile` section defines a row of the positions to edit like so:
        "Tile" "bbbb"
        "Tile" "b..b"
        "Tile" "b..b"
        "Tile" "bbbb"
    If `Force` is true, the specified tiles will override any existing ones
    and create the tile if necessary.
    Otherwise they will be merged in - white/black tiles will not replace
    tiles set to nodraw or void for example.
    `chance`, if specified allows producing irregular tiles by randomly not
    changing the tile.

    If you need less regular placement (other orientation, precise positions)
    use a bee2_template_tilesetter in a template.

    Allowed tile characters:
    - `W`: White tile.
    - `w`: White 4x4 only tile.
    - `B`: Black tile.
    - `b`: Black 4x4 only tile.
    - `g`: The side/bottom of goo pits.
    - `n`: Nodraw surface.
    - `i`: Invert the tile surface, if black/white.
    - `1`: Convert to a 1x1 only tile, if a black/white tile.
    - `4`: Convert to a 4x4 only tile, if a black/white tile.
    - `.`: Void (remove the tile in this position).
    - `_` or ` `: Placeholder (don't modify this space).
    - `x`: Cutout Tile (Broken)
    - `o`: Cutout Tile (Partial)
    """
    origin = Vec.from_str(inst['origin'])
    orient = Matrix.from_angle(Angle.from_str(inst['angles']))

    offset = (res.vec('offset', -48, 48) - (0, 0, 64)) @ orient + origin

    norm = round(orient.up(), 6)

    force_tile = res.bool('force')

    tiles: list[str] = [
        row.value for row in res if row.name in ('tile', 'tiles')
    ]
    if not tiles:
        raise ValueError('No "tile" parameters in SetTile!')

    chance = srctools.conv_float(res['chance', '100'].rstrip('%'), 100.0)
    if chance < 100.0:
        rng = rand.seed(b'tile', inst, res['seed', ''])
    else:
        rng = None

    for y, row in enumerate(tiles):
        for x, val in enumerate(row):
            if val in '_ ':
                continue

            if rng is not None and rng.uniform(0, 100) > chance:
                continue

            pos = Vec(32 * x, -32 * y, 0) @ orient + offset

            if val == '4':
                size = tiling.TileSize.TILE_4x4
            elif val == '1':
                size = tiling.TileSize.TILE_1x1
            elif val == 'i':
                size = None
            else:
                try:
                    new_tile = tiling.TILETYPE_FROM_CHAR[val]
                except KeyError:
                    LOGGER.warning('Unknown tiletype "{}"!', val)
                else:
                    tiling.edit_quarter_tile(pos, norm, new_tile, force_tile)
                continue

            # Edit the existing tile.
            try:
                tile, u, v = tiling.find_tile(pos, norm, force_tile)
            except KeyError:
                LOGGER.warning(
                    'Expected tile, but none found: {}, {}',
                    pos,
                    norm,
                )
                continue

            if size is None:
                # Invert the tile.
                tile[u, v] = tile[u, v].inverted
                continue

            # Unless forcing is enabled don't alter the size of GOO_SIDE.
            if tile[u, v].is_tile and tile[u,
                                           v] is not tiling.TileType.GOO_SIDE:
                tile[u, v] = tiling.TileType.with_color_and_size(
                    size, tile[u, v].color)
            elif force_tile:
                # If forcing, make it black. Otherwise no need to change.
                tile[u, v] = tiling.TileType.with_color_and_size(
                    size, tiling.Portalable.BLACK)
Beispiel #28
0
def res_antlaser(vmf: VMF, res: Property):
    """The condition to generate AntLasers.

    This is executed once to modify all instances.
    """
    conf_inst = instanceLocs.resolve(res['instance'])
    conf_glow_height = Vec(z=res.float('GlowHeight', 48) - 64)
    conf_las_start = Vec(z=res.float('LasStart') - 64)
    conf_rope_off = res.vec('RopePos')
    conf_toggle_targ = res['toggleTarg', '']

    beam_conf = res.find_key('BeamKeys', [])
    glow_conf = res.find_key('GlowKeys', [])
    cable_conf = res.find_key('CableKeys', [])

    if beam_conf:
        # Grab a copy of the beam spawnflags so we can set our own options.
        conf_beam_flags = beam_conf.int('spawnflags')
        # Mask out certain flags.
        conf_beam_flags &= (
            0
            | 1  # Start On
            | 2  # Toggle
            | 4  # Random Strike
            | 8  # Ring
            | 16  # StartSparks
            | 32  # EndSparks
            | 64  # Decal End
            #| 128  # Shade Start
            #| 256  # Shade End
            #| 512  # Taper Out
        )
    else:
        conf_beam_flags = 0

    conf_outputs = [
        Output.parse(prop) for prop in res
        if prop.name in ('onenabled', 'ondisabled')
    ]

    # Find all the markers.
    nodes: Dict[str, connections.Item] = {}

    for inst in vmf.by_class['func_instance']:
        if inst['file'].casefold() not in conf_inst:
            continue
        name = inst['targetname']
        try:
            # Remove the item - it's no longer going to exist after
            # we're done.
            nodes[name] = connections.ITEMS.pop(name)
        except KeyError:
            raise ValueError('No item for "{}"?'.format(name)) from None

    if not nodes:
        # None at all.
        return conditions.RES_EXHAUSTED

    # Now find every connected group, recording inputs, outputs and links.
    todo = set(nodes.values())

    groups = []  # type: List[Group]

    # Node -> is grouped already.
    node_pairing = dict.fromkeys(nodes.values(), False)

    while todo:
        start = todo.pop()
        # Synthesise the Item used for logic.
        # We use a random info_target to manage the IO data.
        group = Group(start)
        groups.append(group)
        for node in group.nodes:
            # If this node has no non-node outputs, destroy the antlines.
            has_output = False
            node_pairing[node] = True

            for conn in list(node.outputs):
                neighbour = conn.to_item
                todo.discard(neighbour)
                pair_state = node_pairing.get(neighbour, None)
                if pair_state is None:
                    # Not a node, a target of our logic.
                    conn.from_item = group.item
                    has_output = True
                    continue
                elif pair_state is False:
                    # Another node.
                    group.nodes.append(neighbour)
                # else: True, node already added.

                # For nodes, connect link.
                conn.remove()
                group.links.add(frozenset({node, neighbour}))

            # If we have a real output, we need to transfer it.
            # Otherwise we can just destroy it.
            if has_output:
                node.transfer_antlines(group.item)
            else:
                node.delete_antlines()

            # Do the same for inputs, so we can catch that.
            for conn in list(node.inputs):
                neighbour = conn.from_item
                todo.discard(neighbour)
                pair_state = node_pairing.get(neighbour, None)
                if pair_state is None:
                    # Not a node, an input to the group.
                    conn.to_item = group.item
                    continue
                elif pair_state is False:
                    # Another node.
                    group.nodes.append(neighbour)
                # else: True, node already added.

                # For nodes, connect link.
                conn.remove()
                group.links.add(frozenset({neighbour, node}))

    # Now every node is in a group. Generate the actual entities.
    for group in groups:
        # We generate two ent types. For each marker, we add a sprite
        # and a beam pointing at it. Then for each connection
        # another beam.

        # Choose a random antlaser name to use for our group.
        base_name = group.nodes[0].name

        out_enable = [Output('', '', 'FireUser2')]
        out_disable = [Output('', '', 'FireUser1')]
        for output in conf_outputs:
            if output.output.casefold() == 'onenabled':
                out_enable.append(output.copy())
            else:
                out_disable.append(output.copy())

        if conf_toggle_targ:
            # Make the group info_target into a texturetoggle.
            toggle = group.item.inst
            toggle['classname'] = 'env_texturetoggle'
            toggle['target'] = conditions.local_name(group.nodes[0].inst,
                                                     conf_toggle_targ)

        group.item.enable_cmd = tuple(out_enable)
        group.item.disable_cmd = tuple(out_disable)

        # Node -> index for targetnames.
        indexes: Dict[connections.Item, int] = {}

        # For cables, it's a bit trickier than the beams.
        # The cable ent itself is the one which decides what it links to,
        # so we need to potentially make endpoint cables at locations with
        # only "incoming" lines.
        # So this dict is either a targetname to indicate cables with an
        # outgoing connection, or the entity for endpoints without an outgoing
        # connection.
        cable_points: Dict[connections.Item, Union[Entity, str]] = {}

        for i, node in enumerate(group.nodes, start=1):
            indexes[node] = i
            node.name = base_name

            sprite_pos = conf_glow_height.copy()
            sprite_pos.localise(
                Vec.from_str(node.inst['origin']),
                Vec.from_str(node.inst['angles']),
            )

            if glow_conf:
                # First add the sprite at the right height.
                sprite = vmf.create_ent('env_sprite')
                for prop in glow_conf:
                    sprite[prop.name] = conditions.resolve_value(
                        node.inst, prop.value)

                sprite['origin'] = sprite_pos
                sprite['targetname'] = NAME_SPR(base_name, i)
            elif beam_conf:
                # If beams but not sprites, we need a target.
                vmf.create_ent(
                    'info_target',
                    origin=sprite_pos,
                    targetname=NAME_SPR(base_name, i),
                )

            if beam_conf:
                # Now the beam going from below up to the sprite.
                beam_pos = conf_las_start.copy()
                beam_pos.localise(
                    Vec.from_str(node.inst['origin']),
                    Vec.from_str(node.inst['angles']),
                )
                beam = vmf.create_ent('env_beam')
                for prop in beam_conf:
                    beam[prop.name] = conditions.resolve_value(
                        node.inst, prop.value)

                beam['origin'] = beam['targetpoint'] = beam_pos
                beam['targetname'] = NAME_BEAM_LOW(base_name, i)
                beam['LightningStart'] = beam['targetname']
                beam['LightningEnd'] = NAME_SPR(base_name, i)
                beam['spawnflags'] = conf_beam_flags | 128  # Shade Start

        if beam_conf:
            for i, (node_a, node_b) in enumerate(group.links):
                beam = vmf.create_ent('env_beam')
                conditions.set_ent_keys(beam, node_a.inst, res, 'BeamKeys')
                beam['origin'] = beam['targetpoint'] = node_a.inst['origin']
                beam['targetname'] = NAME_BEAM_CONN(base_name, i)
                beam['LightningStart'] = NAME_SPR(base_name, indexes[node_a])
                beam['LightningEnd'] = NAME_SPR(base_name, indexes[node_b])
                beam['spawnflags'] = conf_beam_flags

        # We have a couple different situations to deal with here.
        # Either end could Not exist, be Unlinked, or be Linked = 8 combos.
        # Always flip so we do A to B.
        # AB |
        # NN | Make 2 new ones, one is an endpoint.
        # NU | Flip, do UN.
        # NL | Make A, link A to B. Both are linked.
        # UN | Make B, link A to B. B is unlinked.
        # UU | Link A to B, A is now linked, B is unlinked.
        # UL | Link A to B. Both are linked.
        # LN | Flip, do NL.
        # LU | Flip, do UL
        # LL | Make A, link A to B. Both are linked.
        if cable_conf:
            rope_ind = 0  # Uniqueness value.
            for node_a, node_b in group.links:
                state_a, ent_a = RopeState.from_node(cable_points, node_a)
                state_b, ent_b = RopeState.from_node(cable_points, node_b)

                if (state_a is RopeState.LINKED
                        or (state_a is RopeState.NONE
                            and state_b is RopeState.UNLINKED)):
                    # Flip these, handle the opposite order.
                    state_a, state_b = state_b, state_a
                    ent_a, ent_b = ent_b, ent_a
                    node_a, node_b = node_b, node_a

                pos_a = conf_rope_off.copy()
                pos_a.localise(
                    Vec.from_str(node_a.inst['origin']),
                    Vec.from_str(node_a.inst['angles']),
                )

                pos_b = conf_rope_off.copy()
                pos_b.localise(
                    Vec.from_str(node_b.inst['origin']),
                    Vec.from_str(node_b.inst['angles']),
                )

                # Need to make the A rope if we don't have one that's unlinked.
                if state_a is not RopeState.UNLINKED:
                    rope_a = vmf.create_ent('move_rope')
                    for prop in beam_conf:
                        rope_a[prop.name] = conditions.resolve_value(
                            node_a.inst, prop.value)
                    rope_a['origin'] = pos_a
                    rope_ind += 1
                    rope_a['targetname'] = NAME_CABLE(base_name, rope_ind)
                else:
                    # It is unlinked, so it's the rope to use.
                    rope_a = ent_a

                # Only need to make the B rope if it doesn't have one.
                if state_b is RopeState.NONE:
                    rope_b = vmf.create_ent('move_rope')
                    for prop in beam_conf:
                        rope_b[prop.name] = conditions.resolve_value(
                            node_b.inst, prop.value)
                    rope_b['origin'] = pos_b
                    rope_ind += 1
                    name_b = rope_b['targetname'] = NAME_CABLE(
                        base_name, rope_ind)

                    cable_points[node_b] = rope_b  # Someone can use this.
                elif state_b is RopeState.UNLINKED:
                    # Both must be unlinked, we aren't using this link though.
                    name_b = ent_b['targetname']
                else:  # Linked, we just have the name.
                    name_b = ent_b

                # By here, rope_a should be an unlinked rope,
                # and name_b should be a name to link to.
                rope_a['nextkey'] = name_b

                # Figure out how much slack to give.
                # If on floor, we need to be taut to have clearance.

                if on_floor(node_a) or on_floor(node_b):
                    rope_a['slack'] = 60
                else:
                    rope_a['slack'] = 125

                # We're always linking A to B, so A is always linked!
                if state_a is not RopeState.LINKED:
                    cable_points[node_a] = rope_a['targetname']

    return conditions.RES_EXHAUSTED
Beispiel #29
0
def res_reshape_fizzler(vmf: VMF, shape_inst: Entity, res: Property):
    """Convert a fizzler connected via the output to a new shape.

    This allows for different placing of fizzler items.
    Each `segment` parameter should be a `x y z;x y z` pair of positions
    that represent the ends of the fizzler.
    `up_axis` should be set to a normal vector pointing in the new 'upward'
    direction.
    `default` is the ID of a fizzler type which should be used if no outputs
    are fired.
    """
    shape_name = shape_inst['targetname']
    shape_item = connections.ITEMS.pop(shape_name)

    shape_angles = Vec.from_str(shape_inst['angles'])
    up_axis = res.vec('up_axis').rotate(*shape_angles)

    for conn in shape_item.outputs:
        fizz_item = conn.to_item
        try:
            fizz = fizzler.FIZZLERS[fizz_item.name]
        except KeyError:
            LOGGER.warning(
                'Reshaping fizzler with non-fizzler output ({})! Ignoring!',
                fizz_item.name)
            continue
        fizz.emitters.clear()  # Remove old positions.
        fizz.up_axis = up_axis
        break
    else:
        # No fizzler, so generate a default.
        # We create the fizzler instance, Fizzler object, and Item object
        # matching it.
        # This is hardcoded to use regular Emancipation Fields.
        base_inst = vmf.create_ent(
            targetname=shape_name,
            classname='func_instance',
            origin=shape_inst['origin'],
            file=resolve_inst('<ITEM_BARRIER_HAZARD:fizz_base>'),
        )
        base_inst.fixup.update(shape_inst.fixup)
        fizz = fizzler.FIZZLERS[shape_name] = fizzler.Fizzler(
            fizzler.FIZZ_TYPES['VALVE_MATERIAL_EMANCIPATION_GRID'],
            up_axis,
            base_inst,
            [],
        )
        fizz_item = connections.Item(
            base_inst,
            connections.ITEM_TYPES['item_barrier_hazard'],
            shape_item.ant_floor_style,
            shape_item.ant_wall_style,
        )
        connections.ITEMS[shape_name] = fizz_item

    # Detach this connection and remove traces of it.
    for conn in list(shape_item.outputs):
        conn.remove()
    for coll in [
            shape_item.antlines, shape_item.ind_panels, shape_item.shape_signs
    ]:
        for ent in coll:
            ent.remove()
        coll.clear()

    for inp in list(shape_item.inputs):
        inp.to_item = fizz_item

    fizz_base = fizz.base_inst
    fizz_base['origin'] = shape_inst['origin']
    origin = Vec.from_str(shape_inst['origin'])

    for seg_prop in res.find_all('Segment'):
        vec1, vec2 = seg_prop.value.split(';')
        seg_min_max = Vec.bbox(
            Vec.from_str(vec1).rotate(*shape_angles) + origin,
            Vec.from_str(vec2).rotate(*shape_angles) + origin,
        )
        fizz.emitters.append(seg_min_max)
Beispiel #30
0
def res_import_template_setup(res: Property):
    if res.has_children():
        temp_id = res['id']
    else:
        temp_id = res.value
        res = Property('TemplateBrush', [])

    force = res['force', ''].casefold().split()
    if 'white' in force:
        force_colour = texturing.Portalable.white
    elif 'black' in force:
        force_colour = texturing.Portalable.black
    elif 'invert' in force:
        force_colour = 'INVERT'
    else:
        force_colour = None

    if 'world' in force:
        force_type = template_brush.TEMP_TYPES.world
    elif 'detail' in force:
        force_type = template_brush.TEMP_TYPES.detail
    else:
        force_type = template_brush.TEMP_TYPES.default

    force_grid: Optional[texturing.TileSize]
    size: texturing.TileSize
    for size in texturing.TileSize:
        if size in force:
            force_grid = size
            break
    else:
        force_grid = None

    if 'bullseye' in force:
        surf_cat = texturing.GenCat.BULLSEYE
    elif 'special' in force or 'panel' in force:
        surf_cat = texturing.GenCat.PANEL
    else:
        surf_cat = texturing.GenCat.NORMAL

    replace_tex = defaultdict(list)
    for prop in res.find_key('replace', []):
        replace_tex[prop.name].append(prop.value)

    if 'replaceBrush' in res:
        LOGGER.warning(
            'replaceBrush command used for template "{}", which is no '
            'longer used.',
            temp_id,
        )
    bind_tile_pos = [
        # So it's the floor block location.
        Vec.from_str(value) - (0, 0, 128)
        for value in res.find_key('BindOverlay', []).as_array()
    ]

    key_values = res.find_key("Keys", [])
    if key_values:
        keys = Property("", [
            key_values,
            res.find_key("LocalKeys", []),
        ])
        # Ensure we have a 'origin' keyvalue - we automatically offset that.
        if 'origin' not in key_values:
            key_values['origin'] = '0 0 0'

        # Spawn everything as detail, so they get put into a brush
        # entity.
        force_type = template_brush.TEMP_TYPES.detail
        outputs = [Output.parse(prop) for prop in res.find_children('Outputs')]
    else:
        keys = None
        outputs = []

    visgroup_func: Callable[[Set[str]], Iterable[str]]

    def visgroup_func(groups):
        """none = don't add any visgroups."""
        return ()

    visgroup_prop = res.find_key('visgroup', 'none')
    if visgroup_prop.has_children():
        visgroup_vars = list(visgroup_prop)
    else:
        visgroup_vars = []
        visgroup_mode = res['visgroup', 'none'].casefold()
        # Generate the function which picks which visgroups to add to the map.
        if visgroup_mode == 'none':
            pass
        elif visgroup_mode == 'choose':

            def visgroup_func(groups):
                """choose = add one random group."""
                return [random.choice(groups)]
        else:
            percent = srctools.conv_float(visgroup_mode.rstrip('%'), 0.00)
            if percent > 0.0:

                def visgroup_func(groups):
                    """Number = percent chance for each to be added"""
                    for group in groups:
                        val = random.uniform(0, 100)
                        if val <= percent:
                            yield group

    picker_vars = [(prop.real_name, prop.value)
                   for prop in res.find_children('pickerVars')]

    return (
        temp_id,
        dict(replace_tex),
        force_colour,
        force_grid,
        force_type,
        surf_cat,
        bind_tile_pos,
        res['invertVar', ''],
        res['colorVar', ''],
        visgroup_func,
        # If true, force visgroups to all be used.
        res['forceVisVar', ''],
        visgroup_vars,
        keys,
        picker_vars,
        outputs,
        res.vec('senseOffset'),
    )
Beispiel #31
0
def brush_at_loc(
    inst: Entity,
    props: Property,
) -> Tuple[tiling.TileType, bool, Set[tiling.TileType]]:
    """Common code for posIsSolid and ReadSurfType.

    This returns the average tiletype, if both colors were found,
    and a set of all types found.
    """
    origin = Vec.from_str(inst['origin'])
    angles = Vec.from_str(inst['angles'])

    # Allow using pos1 instead, to match pos2.
    pos = props.vec('pos1' if 'pos1' in props else 'pos')
    pos.z -= 64  # Subtract so origin is the floor-position

    pos.localise(origin, angles)

    norm = props.vec('dir', 0, 0, 1).rotate(*angles)

    if props.bool('gridpos') and norm is not None:
        for axis in 'xyz':
            # Don't realign things in the normal's axis -
            # those are already fine.
            if norm[axis] == 0:
                pos[axis] = pos[axis] // 128 * 128 + 64

    result_var = props['setVar', '']
    # RemoveBrush is the pre-tiling name.
    should_remove = props.bool('RemoveTile', props.bool('RemoveBrush', False))

    tile_types: Set[tiling.TileType] = set()
    both_colors = False

    if 'pos2' in props:
        pos2 = props.vec('pos2')
        pos2.z -= 64  # Subtract so origin is the floor-position
        pos2.localise(origin, angles)

        bbox_min, bbox_max = Vec.bbox(pos, pos2)

        white_count = black_count = 0

        for pos in Vec.iter_grid(bbox_min, bbox_max, 32):
            try:
                tiledef, u, v = tiling.find_tile(pos, norm)
            except KeyError:
                continue

            tile_type = tiledef[u, v]
            tile_types.add(tile_type)
            if should_remove:
                tiledef[u, v] = tiling.TileType.VOID
            if tile_type.is_tile:
                if tile_type.color is tiling.Portalable.WHITE:
                    white_count += 1
                else:
                    black_count += 1

        both_colors = white_count > 0 and black_count > 0

        if white_count == black_count == 0:
            tile_type = tiling.TileType.VOID
            tile_types.add(tiling.TileType.VOID)
        elif white_count > black_count:
            tile_type = tiling.TileType.WHITE
        else:
            tile_type = tiling.TileType.BLACK
    else:
        # Single tile.
        try:
            tiledef, u, v = tiling.find_tile(pos, norm)
        except KeyError:
            tile_type = tiling.TileType.VOID
        else:
            tile_type = tiledef[u, v]
            if should_remove:
                tiledef[u, v] = tiling.TileType.VOID
        tile_types.add(tile_type)

    if result_var:
        if tile_type.is_tile:
            # Don't distinguish between 4x4, goo sides
            inst.fixup[result_var] = tile_type.color.value
        elif tile_type is tiling.TileType.VOID:
            inst.fixup[result_var] = 'none'
        else:
            inst.fixup[result_var] = tile_type.name.casefold()

    return tile_type, both_colors, tile_types
Beispiel #32
0
def res_reshape_fizzler(vmf: VMF, shape_inst: Entity, res: Property):
    """Convert a fizzler connected via the output to a new shape.

    This allows for different placing of fizzler items.

    * Each `segment` parameter should be a `x y z;x y z` pair of positions
    that represent the ends of the fizzler.
    * `up_axis` should be set to a normal vector pointing in the new 'upward'
    direction.
    * If none are connected, a regular fizzler will be synthesized.

    The following fixup vars will be set to allow the shape to match the fizzler:

    * `$uses_nodraw` will be 1 if the fizzler nodraws surfaces behind it.
    """
    shape_name = shape_inst['targetname']
    shape_item = connections.ITEMS.pop(shape_name)

    shape_orient = Matrix.from_angle(Angle.from_str(shape_inst['angles']))
    up_axis: Vec = round(res.vec('up_axis') @ shape_orient, 6)

    for conn in shape_item.outputs:
        fizz_item = conn.to_item
        try:
            fizz = fizzler.FIZZLERS[fizz_item.name]
        except KeyError:
            continue
        # Detach this connection and remove traces of it.
        conn.remove()

        fizz.emitters.clear()  # Remove old positions.
        fizz.up_axis = up_axis
        fizz.base_inst['origin'] = shape_inst['origin']
        fizz.base_inst['angles'] = shape_inst['angles']
        break
    else:
        # No fizzler, so generate a default.
        # We create the fizzler instance, Fizzler object, and Item object
        # matching it.
        # This is hardcoded to use regular Emancipation Fields.
        base_inst = conditions.add_inst(
            vmf,
            targetname=shape_name,
            origin=shape_inst['origin'],
            angles=shape_inst['angles'],
            file=resolve_one('<ITEM_BARRIER_HAZARD:fizz_base>'),
        )
        base_inst.fixup.update(shape_inst.fixup)
        fizz = fizzler.FIZZLERS[shape_name] = fizzler.Fizzler(
            fizzler.FIZZ_TYPES['VALVE_MATERIAL_EMANCIPATION_GRID'],
            up_axis,
            base_inst,
            [],
        )
        fizz_item = connections.Item(
            base_inst,
            connections.ITEM_TYPES['item_barrier_hazard'],
            ant_floor_style=shape_item.ant_floor_style,
            ant_wall_style=shape_item.ant_wall_style,
        )
        connections.ITEMS[shape_name] = fizz_item

    # Transfer the input/outputs from us to the fizzler.
    for inp in list(shape_item.inputs):
        inp.to_item = fizz_item
    for conn in list(shape_item.outputs):
        conn.from_item = fizz_item

    # If the fizzler has no outputs, then strip out antlines. Otherwise,
    # they need to be transferred across, so we can't tell safely.
    if fizz_item.output_act() is None and fizz_item.output_deact() is None:
        shape_item.delete_antlines()
    else:
        shape_item.transfer_antlines(fizz_item)

    fizz_base = fizz.base_inst
    fizz_base['origin'] = shape_inst['origin']
    origin = Vec.from_str(shape_inst['origin'])

    fizz.has_cust_position = True
    # Since the fizzler is moved elsewhere, it's the responsibility of
    # the new item to have holes.
    fizz.embedded = False
    # So tell it whether or not it needs to do so.
    shape_inst.fixup['$uses_nodraw'] = fizz.fizz_type.nodraw_behind

    for seg_prop in res.find_all('Segment'):
        vec1, vec2 = seg_prop.value.split(';')
        seg_min_max = Vec.bbox(
            Vec.from_str(vec1) @ shape_orient + origin,
            Vec.from_str(vec2) @ shape_orient + origin,
        )
        fizz.emitters.append(seg_min_max)