Ejemplo n.º 1
0
def add_floor_sides(vmf: VMF, locs):
    """We need to replace nodraw textures around the outside of the holes.

    This requires looping through all faces, since these will have been
    nodrawed.
    """
    added_locations = {
        barrier.wall.as_tuple(): False
        for barrier in
        locs
    }

    for face in vmf.iter_wfaces(world=True, detail=False):
        if face.mat != 'tools/toolsnodraw':
            continue
        loc = face.get_origin().as_tuple()
        if loc in added_locations:
            random.seed('floor_side_{}_{}_{}'.format(*loc))
            face.mat = random.choice(MATS['squarebeams'])
            added_locations[loc] = True
            # Swap these to flip the texture diagonally, so the beam is at top
            face.uaxis, face.vaxis = face.vaxis, face.uaxis
            face.uaxis.offset = 48

            vbsp.IGNORED_FACES.add(face)

    # Look for the ones without a texture - these are open to the void and
    # need to be sealed. The template chamfers the edges
    # to prevent showing void at outside corners.
    for wall_loc, ceil_loc, rot in locs:
        if added_locations[wall_loc.as_tuple()]:
            continue

        diag_loc = (wall_loc.x, wall_loc.y, wall_loc.z + 128)

        temp_data = template_brush.import_template(
            # If there's a wall surface directly above this point
            # or a ceiling brush in the next block over
            # we want to use a world brush to seal the leak.
            # Otherwise we use the detail version for inside the map.
            temp_name=(
                FLOOR_TEMP_SIDE_DETAIL if
                ceil_loc not in conditions.SOLIDS and
                diag_loc not in conditions.SOLIDS
                else FLOOR_TEMP_SIDE_WORLD
            ),
            origin=wall_loc,
            angles=Vec(0, rot, 0),
        )
        template_brush.retexture_template(
            temp_data,
            wall_loc,
            # Switch to use the configured squarebeams texture
            replace_tex={
                consts.Special.SQUAREBEAMS: random.choice(
                    MATS['squarebeams']
                ),
            }
        )
Ejemplo n.º 2
0
def gen_faithplates(vmf: VMF) -> None:
    """Place the targets and catapults into the map."""
    # Target positions -> list of triggers wanting to aim there.
    pos_to_trigs: Dict[Union[Tuple[float, float, float], tiling.TileDef],
                       List[Entity]] = collections.defaultdict(list)

    for plate in PLATES.values():
        if isinstance(plate, (AngledPlate, PaintDropper)):
            if isinstance(plate.target, tiling.TileDef):
                targ_pos = plate.target  # Use the ID directly.
            else:
                targ_pos = plate.target.as_tuple()
            pos_to_trigs[targ_pos].append(plate.trig)

        if isinstance(plate, StraightPlate):
            trigs = [plate.trig, plate.helper_trig]
        else:
            trigs = [plate.trig]

        for trig in trigs:
            trig_origin = trig.get_origin()
            if plate.template is not None:
                trig.solids = template_brush.import_template(
                    temp_name=plate.template,
                    origin=trig_origin + plate.trig_offset,
                    angles=Vec.from_str(plate.inst['angles']),
                    force_type=template_brush.TEMP_TYPES.world,
                    add_to_map=False,
                ).world
            elif plate.trig_offset:
                for solid in trig.solids:
                    solid.translate(plate.trig_offset)

    # Now, generate each target needed.
    for pos_or_tile, trigs in pos_to_trigs.items():
        target = vmf.create_ent(
            'info_target',
            angles='0 0 0',
            spawnflags='3',  # Transmit to PVS and always transmit.
        )

        if isinstance(pos_or_tile, tiling.TileDef):
            pos_or_tile.position_bullseye(target)
        else:
            # Static target.
            target['origin'] = Vec(pos_or_tile)

        target.make_unique('faith_target')

        for trig in trigs:
            trig['launchTarget'] = target['targetname']
Ejemplo n.º 3
0
def add_floor_sides(vmf: VMF, locs):
    """We need to replace nodraw textures around the outside of the holes.

    This requires looping through all faces, since these will have been
    nodrawed.
    """
    added_locations = {barrier.wall.as_tuple(): False for barrier in locs}

    for face in vmf.iter_wfaces(world=True, detail=False):
        if face.mat != 'tools/toolsnodraw':
            continue
        loc = face.get_origin().as_tuple()
        if loc in added_locations:
            random.seed('floor_side_{}_{}_{}'.format(*loc))
            face.mat = random.choice(MATS['squarebeams'])
            added_locations[loc] = True
            # Swap these to flip the texture diagonally, so the beam is at top
            face.uaxis, face.vaxis = face.vaxis, face.uaxis
            face.uaxis.offset = 48

            vbsp.IGNORED_FACES.add(face)

    # Look for the ones without a texture - these are open to the void and
    # need to be sealed. The template chamfers the edges
    # to prevent showing void at outside corners.
    for wall_loc, ceil_loc, rot in locs:
        if added_locations[wall_loc.as_tuple()]:
            continue

        diag_loc = (wall_loc.x, wall_loc.y, wall_loc.z + 128)

        temp_data = template_brush.import_template(
            # If there's a wall surface directly above this point
            # or a ceiling brush in the next block over
            # we want to use a world brush to seal the leak.
            # Otherwise we use the detail version for inside the map.
            temp_name=(FLOOR_TEMP_SIDE_DETAIL
                       if ceil_loc not in conditions.SOLIDS
                       and diag_loc not in conditions.SOLIDS else
                       FLOOR_TEMP_SIDE_WORLD),
            origin=wall_loc,
            angles=Vec(0, rot, 0),
        )
        template_brush.retexture_template(
            temp_data,
            wall_loc,
            # Switch to use the configured squarebeams texture
            replace_tex={
                consts.Special.SQUAREBEAMS: random.choice(MATS['squarebeams']),
            })
Ejemplo n.º 4
0
def make_corner(origin, angle, size, config):
    vbsp.VMF.create_ent(
        classname='func_instance',
        origin=origin,
        angles=angle,
        file=config['corner', size],
    )

    temp = config['corner_temp', size]
    if temp:
        temp_solids = template_brush.import_template(
            temp,
            origin=origin,
            angles=Vec.from_str(angle),
            force_type=template_brush.TEMP_TYPES.world,
        ).world
        for solid in temp_solids:
            vbsp.VMF.remove_brush(solid)
        motion_trigger(*temp_solids)
Ejemplo n.º 5
0
def make_corner(origin, angle, size, config):
    vbsp.VMF.create_ent(
        classname='func_instance',
        origin=origin,
        angles=angle,
        file=config['corner', size],
    )

    temp = config['corner_temp', size]
    if temp:
        temp_solids = template_brush.import_template(
            temp,
            origin=origin,
            angles=Vec.from_str(angle),
            force_type=template_brush.TEMP_TYPES.world,
        ).world
        for solid in temp_solids:
            vbsp.VMF.remove_brush(solid)
        motion_trigger(*temp_solids)
Ejemplo n.º 6
0
def hollow_block(solid_group: solidGroup, remove_orig_face=False):
    """Convert a solid into a embeddedVoxel-style block.

    The original brushes must be in the SOLIDS dict. They will be replaced.
    This returns a dict mapping normals to the new solidGroups.
    If remove_orig_face is true, the starting face will not be kept.
    """
    import vbsp
    orig_solid = solid_group.solid  # type: Solid

    bbox_min, bbox_max = orig_solid.get_bbox()
    if 4 in (bbox_max - bbox_min):
        # If it's 4 units thick, skip hollowing - PeTI did it already.
        if remove_orig_face:
            VMF.remove_brush(orig_solid)
            del SOLIDS[solid_group.face.get_origin().as_tuple()]
        return

    VMF.remove_brush(orig_solid)

    for face in orig_solid.sides:
        if remove_orig_face and face is solid_group.face:
            # Skip readding the original face, which removes it.
            continue

        solid_key = face.get_origin().as_tuple()

        if face.mat.casefold(
        ) == 'tools/toolsnodraw' and face not in vbsp.IGNORED_FACES:
            # If it's nodraw, we can skip it. If it's also in IGNORED_FACES
            # though a condition has set it, so recreate it (it might be sealing
            # the void behind a func_detail or model).
            continue

        # Remove this face from the solids list, and get the group.
        face_group = SOLIDS.pop(solid_key, None)

        normal = face.normal()

        # Generate our new brush.
        new_brushes = template_brush.import_template(
            TEMP_EMBEDDED_VOXEL,
            face.get_origin(),
            # The normal Z is swapped...
            normal.to_angle(),
            force_type=template_brush.TEMP_TYPES.world,
        ).world

        # Texture the new brush..
        for brush in new_brushes:  # type: Solid
            for new_face in brush.sides:
                # The SKIP brush is the surface, all the others are nodraw.
                if new_face.mat.casefold() != 'tools/toolsskip':
                    continue

                # Overwrite all the properties, to make the new brush
                # the same as the original.
                new_face.mat = face.mat
                new_face.uaxis = face.uaxis
                new_face.vaxis = face.vaxis
                new_face.planes = face.planes
                new_face.ham_rot = 0

                # Swap the two IDs - that way when the original face gets
                # deleted the auto-set ID will vanish, leaving the original
                # ID.
                new_face.id, face.id = face.id, new_face.id

                # Remove the new face, if the original wasn't in IGNORED_FACES.
                if face not in vbsp.IGNORED_FACES:
                    vbsp.IGNORED_FACES.remove(new_face)

                # Make a new SolidGroup to match the face.
                if face_group is not None:
                    SOLIDS[solid_key] = solidGroup(
                        new_face,
                        brush,
                        face_group.normal,
                        face_group.color,
                    )
Ejemplo n.º 7
0
def res_insert_overlay(inst: Entity, res: Property):
    """Use a template to insert one or more overlays on a surface.

    Options:

    - ID: The template ID. Brushes will be ignored.
    - Replace: old -> new material replacements.
    - Face_pos: The offset of the brush face.
    - Normal: The direction of the brush face.
    - Offset: An offset to move the overlays by.
    """
    (
        temp_id,
        replace,
        face,
        norm,
        offset,
    ) = res.value

    if temp_id[:1] == '$':
        temp_id = inst.fixup[temp_id]

    origin = Vec.from_str(inst['origin'])  # type: Vec
    angles = Vec.from_str(inst['angles', '0 0 0'])

    face_pos = Vec(face).rotate(*angles)
    face_pos += origin
    normal = Vec(norm).rotate(*angles)

    # Don't make offset change the face_pos value..
    origin += offset.copy().rotate_by_str(inst['angles', '0 0 0'])

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

    try:
        face_id = SOLIDS[face_pos.as_tuple()].face.id
    except KeyError:
        LOGGER.warning(
            'Overlay brush position is not valid: {}',
            face_pos,
        )
        return

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

    for over in temp.overlay:  # type: Entity
        random.seed('TEMP_OVERLAY_' + over['basisorigin'])
        mat = random.choice(
            replace.get(
                over['material'],
                (over['material'], ),
            ))
        if mat[:1] == '$':
            mat = inst.fixup[mat]
        if mat.startswith('<') or mat.endswith('>'):
            # Lookup in the style data.
            import vbsp
            LOGGER.info('Tex: {}', vbsp.settings['textures'].keys())
            mat = vbsp.get_tex(mat[1:-1])
        over['material'] = mat
        over['sides'] = str(face_id)

    # Wipe the brushes from the map.
    if temp.detail is not None:
        temp.detail.remove()
        LOGGER.info(
            'Overlay template "{}" could set keep_brushes=0.',
            temp_id,
        )
Ejemplo n.º 8
0
def res_import_template(inst: Entity, res: Property):
    """Import a template VMF file, retexturing it to match orientation.

    It will be placed overlapping the given instance.  
    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`.
    - `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
            face ID.
    - `replaceBrush`: The position of a brush to replace (`0 0 0`=the surface).
            This brush will be removed, and overlays will be fixed to use
            all faces with the same normal. Can alternately be a block:

            - `Pos`: The position to replace.
            - `additionalIDs`: Space-separated list of face IDs in the template
              to also fix for overlays. The surface should have close to a
              vertical normal, to prevent rescaling the overlay.
            - `removeBrush`: If true, the original brush will not be removed.
            - `transferOverlay`: Allow disabling transferring overlays to this
              template. The IDs will be removed instead. (This can be a `$fixup`).
    - `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. If `none` (default),
            they are ignored. If `choose`, one is chosen. If a number, that
            is the percentage chance for each visgroup to be added.
    - `visgroup_force_var`: If set and True, visgroup is ignored and all groups
            are added.
    - `outputs`: Add outputs to the brush ent. Syntax is like VMFs, and all names
            are local to the instance.
    """
    (
        orig_temp_id,
        replace_tex,
        force_colour,
        force_grid,
        force_type,
        replace_brush_pos,
        rem_replace_brush,
        transfer_overlays,
        additional_replace_ids,
        invert_var,
        color_var,
        visgroup_func,
        visgroup_force_var,
        key_block,
        outputs,
    ) = res.value

    if ':' in orig_temp_id:
        # Split, resolve each part, then recombine.
        temp_id, visgroup = orig_temp_id.split(':', 1)
        temp_id = (
            conditions.resolve_value(inst, temp_id) + ':' +
            conditions.resolve_value(inst, visgroup)
        )
    else:
        temp_id = conditions.resolve_value(inst, orig_temp_id)

    if srctools.conv_bool(conditions.resolve_value(inst, visgroup_force_var)):
        def visgroup_func(group):
            """Use all the groups."""
            yield from group

    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

    if color_var.casefold() == '<editor>':
        # Check traits for the colour it should be.
        traits = instance_traits.get(inst)
        if 'white' in traits:
            force_colour = template_brush.MAT_TYPES.white
        elif 'black' in traits:
            force_colour = template_brush.MAT_TYPES.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 = template_brush.MAT_TYPES.white
        elif color_val == 'black':
            force_colour = template_brush.MAT_TYPES.black
    # else: no color var

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

    origin = Vec.from_str(inst['origin'])
    angles = Vec.from_str(inst['angles', '0 0 0'])
    temp_data = template_brush.import_template(
        template,
        origin,
        angles,
        targetname=inst['targetname', ''],
        force_type=force_type,
        visgroup_choose=visgroup_func,
        add_to_map=True,
        additional_visgroups=visgroups,
    )

    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, angles)
        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).rotate(*angles)
            temp_data.detail['movedir'] = move_dir.to_angle()

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

        # Add it to the list of ignored brushes, so vbsp.change_brush() doesn't
        # modify it.
        vbsp.IGNORED_BRUSH_ENTS.add(temp_data.detail)

    try:
        # This is the original brush the template is replacing. We fix overlay
        # face IDs, so this brush is replaced by the faces in the template
        # pointing
        # the same way.
        if replace_brush_pos is None:
            raise KeyError  # Not set, raise to jump out of the try block

        pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z)
        pos += origin
        brush_group = SOLIDS[pos.as_tuple()]
    except KeyError:
        # Not set or solid group doesn't exist, skip..
        pass
    else:
        LOGGER.info('IDS: {}', additional_replace_ids | template.overlay_faces)
        conditions.steal_from_brush(
            temp_data,
            brush_group,
            rem_replace_brush,
            map(int, additional_replace_ids | template.overlay_faces),
            conv_bool(conditions.resolve_value(inst, transfer_overlays), True),
        )

    template_brush.retexture_template(
        temp_data,
        origin,
        inst.fixup,
        replace_tex,
        force_colour,
        force_grid,
        # Don't allow clumping if using custom keyvalues - then it won't be edited.
        no_clumping=key_block is not None,
    )
Ejemplo n.º 9
0
def res_faith_mods(inst: Entity, res: Property):
    """Modify the `trigger_catapult` that is created for `ItemFaithPlate` items.

    Values:
        - `raise_trig`: Raise or lower the `trigger_catapult`s by this amount.
        - `angled_targ`, `angled_in`: Instance entity and input for angled plates
        - `straight_targ`, `straight_in`: Instance entity and input for
            straight plates
        - `instvar`: A $replace value to set to either 'angled' or '
            'straight'.
        - `enabledVar`: A `$replace` value which will be copied to the main
            trigger's Start Disabled value (and inverted).
        - `trig_temp`: An ID for a template brush to add. This will be offset by
            the trigger's position (in the case of the `helper` trigger).
    """
    # Get data about the trigger this instance uses for flinging
    fixup_var = res['instvar', '']
    trig_enabled = res['enabledVar', None]
    trig_temp = res['trig_temp', '']
    offset = srctools.conv_int(res['raise_trig', '0'])
    if offset:
        offset = Vec(0, 0, offset).rotate_by_str(inst['angles', '0 0 0'])
    else:
        offset = Vec()

    if trig_enabled is not None:
        trig_enabled = srctools.conv_bool(inst.fixup[trig_enabled])
    else:
        trig_enabled = None

    for trig in vbsp.VMF.by_class['trigger_catapult']:
        if inst['targetname'] not in trig['targetname']:
            continue

        # Edit both the normal and the helper trigger..
        trig_origin = trig['origin'] = Vec.from_str(trig['origin']) + offset

        if offset and not trig_temp:
            # No template, shift the current brushes.
            for solid in trig.solids:
                solid.translate(offset)
        elif trig_temp:
            trig.solids = template_brush.import_template(
                temp_name=trig_temp,
                origin=trig_origin,
                angles=Vec.from_str(inst['angles']),
                force_type=template_brush.TEMP_TYPES.world,
            ).world
            # Remove the trigger solids from worldspawn..
            for solid in trig.solids:
                vbsp.VMF.remove_brush(solid)

        if trig_enabled is not None and 'helper' not in trig['targetname']:
            trig['startdisabled'] = srctools.bool_as_int(not trig_enabled)

        # Inspect the outputs to determine the type.
        # We also change them if desired, since that's not possible
        # otherwise.

        for out in trig.outputs:
            if out.inst_in == 'animate_angled_relay':
                # Instead of an instance: output, use local names.
                # This allows us to strip the proxy, as well as use
                # overlay instances.
                out.inst_in = None
                out.target = conditions.local_name(
                    inst, res['angled_targ', 'animate_angled_relay'])
                out.input = res['angled_in', 'Trigger']
                if fixup_var:
                    inst.fixup[fixup_var] = 'angled'
                break  # There's only one output we want to look for...

            elif out.inst_in == 'animate_straightup_relay':
                out.inst_in = None
                out.target = conditions.local_name(
                    inst,
                    res['straight_targ', 'animate_straightup_relay'],
                )
                out.input = res['straight_in', 'Trigger']

                if fixup_var:
                    inst.fixup[fixup_var] = 'straight'
                break
Ejemplo n.º 10
0
def res_import_template(inst: Entity, res: Property):
    """Import a template VMF file, retexturing it to match orientation.

    It will be placed overlapping the given instance.  
    Options:  
    - ID: The ID of the template to be inserted. Add visgroups to additionally
            add after a colon, comma-seperated (temp_id:vis1,vis2)
    - 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
            face ID.
    - replaceBrush: The position of a brush to replace (0 0 0=the surface).
            This brush will be removed, and overlays will be fixed to use
            all faces with the same normal. Can alternately be a block:
            - Pos: The position to replace.
            - additionalIDs: Space-separated list of face IDs in the template
              to also fix for overlays. The surface should have close to a
              vertical normal, to prevent rescaling the overlay.
            - removeBrush: If true, the original brush will not be removed.
            - transferOverlay: Allow disabling transferring overlays to this
              template. The IDs will be removed instead. (This can be an instvar).
    - 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. If 'none' (default),
            they are ignored. If 'choose', one is chosen. If a number, that
            is the percentage chance for each visgroup to be added.
    - visgroup_force_var: If set and True, visgroup is ignored and all groups
            are added.
    - outputs: Add outputs to the brush ent. Syntax is like VMFs, and all names
            are local to the instance.
    """
    (
        orig_temp_id,
        replace_tex,
        force_colour,
        force_grid,
        force_type,
        replace_brush_pos,
        rem_replace_brush,
        transfer_overlays,
        additional_replace_ids,
        invert_var,
        color_var,
        visgroup_func,
        visgroup_force_var,
        key_block,
        outputs,
    ) = res.value

    if ':' in orig_temp_id:
        # Split, resolve each part, then recombine.
        temp_id, visgroup = orig_temp_id.split(':', 1)
        temp_id = (
            conditions.resolve_value(inst, temp_id) + ':' +
            conditions.resolve_value(inst, visgroup)
        )
    else:
        temp_id = conditions.resolve_value(inst, orig_temp_id)

    if srctools.conv_bool(conditions.resolve_value(inst, visgroup_force_var)):
        def visgroup_func(group):
            """Use all the groups."""
            yield from group

    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

    if color_var.casefold() == '<editor>':
        # Check traits for the colour it should be.
        traits = instance_traits.get(inst)
        if 'white' in traits:
            force_colour = template_brush.MAT_TYPES.white
        elif 'black' in traits:
            force_colour = template_brush.MAT_TYPES.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 = template_brush.MAT_TYPES.white
        elif color_val == 'black':
            force_colour = template_brush.MAT_TYPES.black
    # else: no color var

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

    origin = Vec.from_str(inst['origin'])
    angles = Vec.from_str(inst['angles', '0 0 0'])
    temp_data = template_brush.import_template(
        template,
        origin,
        angles,
        targetname=inst['targetname', ''],
        force_type=force_type,
        visgroup_choose=visgroup_func,
        add_to_map=True,
        additional_visgroups=visgroups,
    )

    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, angles)
        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).rotate(*angles)
            temp_data.detail['movedir'] = move_dir.to_angle()

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

        # Add it to the list of ignored brushes, so vbsp.change_brush() doesn't
        # modify it.
        vbsp.IGNORED_BRUSH_ENTS.add(temp_data.detail)

    try:
        # This is the original brush the template is replacing. We fix overlay
        # face IDs, so this brush is replaced by the faces in the template
        # pointing
        # the same way.
        if replace_brush_pos is None:
            raise KeyError  # Not set, raise to jump out of the try block

        pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z)
        pos += origin
        brush_group = SOLIDS[pos.as_tuple()]
    except KeyError:
        # Not set or solid group doesn't exist, skip..
        pass
    else:
        LOGGER.info('IDS: {}', additional_replace_ids | template.overlay_faces)
        conditions.steal_from_brush(
            temp_data,
            brush_group,
            rem_replace_brush,
            map(int, additional_replace_ids | template.overlay_faces),
            conv_bool(conditions.resolve_value(inst, transfer_overlays), True),
        )

    template_brush.retexture_template(
        temp_data,
        origin,
        inst.fixup,
        replace_tex,
        force_colour,
        force_grid,
        # Don't allow clumping if using custom keyvalues - then it won't be edited.
        no_clumping=key_block is not None,
    )
Ejemplo n.º 11
0
def convert_floor(
    vmf: VMF,
    loc: Vec,
    overlay_ids,
    mats,
    settings,
    signage_loc,
    detail,
    noise_weight,
    noise_func: SimplexNoise,
):
    """Cut out tiles at the specified location."""
    # We pop it, so the face isn't detected by other logic - otherwise it'll
    # be retextured and whatnot, which we don't want.
    try:
        brush = conditions.SOLIDS.pop(loc.as_tuple())
    except KeyError:
        return False  # No tile here!

    if brush.normal == (0, 0, 1):
        # This is a pillar block - there isn't actually tiles here!
        # We need to generate a squarebeams brush to fill this gap.

        brush.face.mat = 'tools/toolsnodraw'  # It won't be visible
        temp_data = template_brush.import_template(
            temp_name=FLOOR_TEMP_PILLAR,
            origin=loc,
        )
        template_brush.retexture_template(
            temp_data,
            loc,
            # Switch to use the configured squarebeams texture
            replace_tex={
                consts.Special.SQUAREBEAMS: random.choice(MATS['squarebeams']),
            })
        return False

    # The new brush IDs overlays need to use
    # NOTE: strings, not ints!
    ant_locs = overlay_ids[str(brush.face.id)] = []

    # Move the floor brush down and switch to the floorbase texture.
    for plane in brush.face.planes:
        plane.z -= FLOOR_DEPTH
    brush.face.mat = random.choice(mats['floorbase'])

    loc.x -= 64
    loc.y -= 64

    for x, y in utils.iter_grid(max_x=4, max_y=4):
        tile_loc = loc + (x * 32 + 16, y * 32 + 16, 0)
        if tile_loc.as_tuple() in signage_loc:
            # Force the tile to be present under signage..
            should_make_tile = True
            rand = 100
            # We don't need to check this again in future!
            signage_loc.remove(tile_loc.as_tuple())
        else:
            # Create a number between 0-100
            rand = 100 * get_noise(tile_loc // 32, noise_func) + 10

            # Adjust based on the noise_weight value, so boundries have more tiles
            rand *= 0.1 + 0.9 * (1 - noise_weight)

            should_make_tile = rand < settings['floor_chance']
            if random.randint(0, 7) == 0:
                # Sometimes there'll be random holes/extra tiles
                should_make_tile = not should_make_tile

        if should_make_tile:
            # Full tile
            tile = make_tile(
                vmf,
                p1=tile_loc - (16, 16, 0),
                p2=tile_loc + (16, 16, -2),
                top_mat=vbsp.get_tex(str(brush.color) + '.floor'),
                bottom_mat='tools/toolsnodraw',
                beam_mat=random.choice(mats['squarebeams']),
            )
            detail.solids.append(tile.solid)
            ant_locs.append(str(tile.top.id))
        elif rand < settings['floor_glue_chance']:
            # 'Glue' tile - this chance should be higher, making these appear
            # bordering the full tiles.
            tile = make_tile(
                vmf,
                p1=tile_loc - (16, 16, 1),
                p2=tile_loc + (16, 16, -2),
                top_mat=random.choice(mats['tile_glue']),
                bottom_mat='tools/toolsnodraw',
                beam_mat=random.choice(mats['squarebeams']),
            )
            detail.solids.append(tile.solid)
        else:
            # No tile at this loc!
            pass

    return True
Ejemplo n.º 12
0
def res_piston_plat(vmf: VMF, inst: Entity, res: Property):
    """Generates piston platforms with optimized logic."""
    (
        template,
        visgroup_names,
        inst_filenames,
        automatic_var,
        color_var,
        source_ent,
        snd_start,
        snd_loop,
        snd_stop,
    ) = res.value  # type: template_brush.Template, List[str], Dict[str, str], str, str, str, str, str, str

    min_pos = inst.fixup.int(FixupVars.PIST_BTM)
    max_pos = inst.fixup.int(FixupVars.PIST_TOP)
    start_up = inst.fixup.bool(FixupVars.PIST_IS_UP)

    # Allow doing variable lookups here.
    visgroup_names = [
        conditions.resolve_value(inst, name) for name in visgroup_names
    ]

    if len(ITEMS[inst['targetname']].inputs) == 0:
        # No inputs. Check for the 'auto' var if applicable.
        if automatic_var and inst.fixup.bool(automatic_var):
            pass
            # The item is automatically moving, so we generate the dynamics.
        else:
            # It's static, we just make that and exit.
            position = max_pos if start_up else min_pos
            inst.fixup[FixupVars.PIST_BTM] = position
            inst.fixup[FixupVars.PIST_TOP] = position
            static_inst = inst.copy()
            vmf.add_ent(static_inst)
            static_inst['file'] = inst_filenames['fullstatic_' + str(position)]
            return

    init_script = 'SPAWN_UP <- {}'.format('true' if start_up else 'false')

    if snd_start and snd_stop:
        packing.pack_files(vmf, snd_start, snd_stop, file_type='sound')
        init_script += '; START_SND <- `{}`; STOP_SND <- `{}`'.format(
            snd_start, snd_stop)
    elif snd_start:
        packing.pack_files(vmf, snd_start, file_type='sound')
        init_script += '; START_SND <- `{}`'.format(snd_start)
    elif snd_stop:
        packing.pack_files(vmf, snd_stop, file_type='sound')
        init_script += '; STOP_SND <- `{}`'.format(snd_stop)

    script_ent = vmf.create_ent(
        classname='info_target',
        targetname=local_name(inst, 'script'),
        vscripts='BEE2/piston/common.nut',
        vscript_init_code=init_script,
        origin=inst['origin'],
    )

    if start_up:
        st_pos, end_pos = max_pos, min_pos
    else:
        st_pos, end_pos = min_pos, max_pos

    script_ent.add_out(
        Output('OnUser1', '!self', 'RunScriptCode',
               'moveto({})'.format(st_pos)),
        Output('OnUser2', '!self', 'RunScriptCode',
               'moveto({})'.format(end_pos)),
    )

    origin = Vec.from_str(inst['origin'])
    angles = Vec.from_str(inst['angles'])
    off = Vec(z=128).rotate(*angles)
    move_ang = off.to_angle()

    # Index -> func_movelinear.
    pistons = {}  # type: Dict[int, Entity]

    static_ent = vmf.create_ent('func_brush', origin=origin)

    color_var = conditions.resolve_value(inst, color_var).casefold()

    if color_var == 'white':
        top_color = template_brush.MAT_TYPES.white
    elif color_var == 'black':
        top_color = template_brush.MAT_TYPES.black
    else:
        top_color = None

    for pist_ind in range(1, 5):
        pist_ent = inst.copy()
        vmf.add_ent(pist_ent)

        if pist_ind <= min_pos:
            # It's below the lowest position, so it can be static.
            pist_ent['file'] = inst_filenames['static_' + str(pist_ind)]
            pist_ent['origin'] = brush_pos = origin + pist_ind * off
            temp_targ = static_ent
        else:
            # It's a moving component.
            pist_ent['file'] = inst_filenames['dynamic_' + str(pist_ind)]
            if pist_ind > max_pos:
                # It's 'after' the highest position, so it never extends.
                # So simplify by merging those all.
                # That's before this so it'll have to exist.
                temp_targ = pistons[max_pos]
                if start_up:
                    pist_ent['origin'] = brush_pos = origin + max_pos * off
                else:
                    pist_ent['origin'] = brush_pos = origin + min_pos * off
                pist_ent.fixup['$parent'] = 'pist' + str(max_pos)
            else:
                # It's actually a moving piston.
                if start_up:
                    brush_pos = origin + pist_ind * off
                else:
                    brush_pos = origin + min_pos * off

                pist_ent['origin'] = brush_pos
                pist_ent.fixup['$parent'] = 'pist' + str(pist_ind)

                pistons[pist_ind] = temp_targ = vmf.create_ent(
                    'func_movelinear',
                    targetname=local_name(pist_ent, 'pist' + str(pist_ind)),
                    origin=brush_pos - off,
                    movedir=move_ang,
                    startposition=start_up,
                    movedistance=128,
                    speed=150,
                )
                if pist_ind - 1 in pistons:
                    pistons[pist_ind]['parentname'] = local_name(
                        pist_ent,
                        'pist' + str(pist_ind - 1),
                    )

        if not pist_ent['file']:
            # No actual instance, remove.
            pist_ent.remove()

        temp_result = template_brush.import_template(
            template,
            brush_pos,
            angles,
            force_type=template_brush.TEMP_TYPES.world,
            add_to_map=False,
            additional_visgroups={visgroup_names[pist_ind - 1]},
        )
        temp_targ.solids.extend(temp_result.world)

        template_brush.retexture_template(
            temp_result,
            origin,
            pist_ent.fixup,
            force_colour=top_color,
            force_grid='special',
            no_clumping=True,
        )

    if not static_ent.solids:
        static_ent.remove()

    if snd_loop:
        script_ent['classname'] = 'ambient_generic'
        script_ent['message'] = snd_loop
        script_ent['health'] = 10  # Volume
        script_ent['pitch'] = '100'
        script_ent['spawnflags'] = 16  # Start silent, looped.
        script_ent['radius'] = 1024

        if source_ent:
            # Parent is irrelevant for actual entity locations, but it
            # survives for the script to read.
            script_ent['SourceEntityName'] = script_ent[
                'parentname'] = local_name(inst, source_ent)
Ejemplo n.º 13
0
def res_signage(vmf: VMF, inst: Entity, res: Property):
    """Implement the Signage item."""
    sign: Optional[Sign]
    try:
        sign = (CONN_SIGNAGES if res.bool('connection') else
                SIGNAGES)[inst.fixup[const.FixupVars.TIM_DELAY]]
    except KeyError:
        # Blank sign
        sign = None

    has_arrow = inst.fixup.bool(const.FixupVars.ST_ENABLED)

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

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

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

    normal = Vec(z=-1).rotate(*angles)
    forward = Vec(x=-1).rotate(*angles)

    prim_pos = Vec(0, -16, -64)
    sec_pos = Vec(0, 16, -64)

    prim_pos.localise(origin, angles)
    sec_pos.localise(origin, angles)

    template_id = res['template_id', '']

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

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

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

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

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

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

        if tiledef is not None:
            tiledef.bind_overlay(over)

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

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

        if tiledef is not None:
            tiledef.bind_overlay(over)
Ejemplo n.º 14
0
def convert_floor(
    vmf: VMF,
    loc: Vec,
    overlay_ids,
    mats,
    settings,
    signage_loc,
    detail,
    noise_weight,
    noise_func: SimplexNoise,
):
    """Cut out tiles at the specified location."""
    # We pop it, so the face isn't detected by other logic - otherwise it'll
    # be retextured and whatnot, which we don't want.
    try:
        brush = conditions.SOLIDS.pop(loc.as_tuple())
    except KeyError:
        return False  # No tile here!

    if brush.normal == (0, 0, 1):
        # This is a pillar block - there isn't actually tiles here!
        # We need to generate a squarebeams brush to fill this gap.

        brush.face.mat = 'tools/toolsnodraw'  # It won't be visible
        temp_data = template_brush.import_template(
            temp_name=FLOOR_TEMP_PILLAR,
            origin=loc,
        )
        template_brush.retexture_template(
            temp_data,
            loc,
            # Switch to use the configured squarebeams texture
            replace_tex={
                consts.Special.SQUAREBEAMS: random.choice(
                    MATS['squarebeams']
                ),
            }
        )
        return False

    # The new brush IDs overlays need to use
    # NOTE: strings, not ints!
    ant_locs = overlay_ids[str(brush.face.id)] = []

    # Move the floor brush down and switch to the floorbase texture.
    for plane in brush.face.planes:
        plane.z -= FLOOR_DEPTH
    brush.face.mat = random.choice(mats['floorbase'])

    loc.x -= 64
    loc.y -= 64

    for x, y in utils.iter_grid(max_x=4, max_y=4):
        tile_loc = loc + (x * 32 + 16, y * 32 + 16, 0)
        if tile_loc.as_tuple() in signage_loc:
            # Force the tile to be present under signage..
            should_make_tile = True
            rand = 100
            # We don't need to check this again in future!
            signage_loc.remove(tile_loc.as_tuple())
        else:
            # Create a number between 0-100
            rand = 100 * get_noise(tile_loc // 32, noise_func) + 10

            # Adjust based on the noise_weight value, so boundries have more tiles
            rand *= 0.1 + 0.9 * (1 - noise_weight)

            should_make_tile = rand < settings['floor_chance']
            if random.randint(0, 7) == 0:
                # Sometimes there'll be random holes/extra tiles
                should_make_tile = not should_make_tile

        if should_make_tile:
            # Full tile
            tile = make_tile(
                vmf,
                p1=tile_loc - (16, 16, 0),
                p2=tile_loc + (16, 16, -2),
                top_mat=vbsp.get_tex(str(brush.color) + '.floor'),
                bottom_mat='tools/toolsnodraw',
                beam_mat=random.choice(mats['squarebeams']),
            )
            detail.solids.append(tile.solid)
            ant_locs.append(str(tile.top.id))
        elif rand < settings['floor_glue_chance']:
            # 'Glue' tile - this chance should be higher, making these appear
            # bordering the full tiles.
            tile = make_tile(
                vmf,
                p1=tile_loc - (16, 16, 1),
                p2=tile_loc + (16, 16, -2),
                top_mat=random.choice(mats['tile_glue']),
                bottom_mat='tools/toolsnodraw',
                beam_mat=random.choice(mats['squarebeams']),
            )
            detail.solids.append(tile.solid)
        else:
            # No tile at this loc!
            pass

    return True
Ejemplo n.º 15
0
def res_signage(vmf: VMF, inst: Entity, res: Property):
    """Implement the Signage item."""
    sign: Optional[Sign]
    try:
        sign = (CONN_SIGNAGES if res.bool('connection') else
                SIGNAGES)[inst.fixup[const.FixupVars.TIM_DELAY]]
    except KeyError:
        # Blank sign
        sign = None

    has_arrow = inst.fixup.bool(const.FixupVars.ST_ENABLED)

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

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

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

    normal = Vec(z=-1).rotate(*angles)
    forward = Vec(x=-1).rotate(*angles)

    prim_pos = Vec(0, -16, -64)
    sec_pos = Vec(0, 16, -64)

    prim_pos.localise(origin, angles)
    sec_pos.localise(origin, angles)

    template_id = res['template_id', '']

    face: Side

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

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

        for face in template.detail.sides():
            if face.normal() == normal:
                brush_faces.append(face)
    else:
        # Direct on the surface.
        block_center = origin // 128 * 128 + (64, 64, 64)
        try:
            face = conditions.SOLIDS[(block_center +
                                      64 * normal).as_tuple()].face
        except KeyError:
            LOGGER.warning(
                "Can't place signage at ({}) in ({}) direction!",
                block_center,
                normal,
            )
            return
        brush_faces = [face]

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

    if sign_sec is not None:
        if has_arrow and res.bool('arrowDown'):
            # Arrow texture points down, need to flip it.
            forward = -forward
        place_sign(
            vmf,
            brush_faces,
            sign_sec,
            sec_pos,
            normal,
            forward,
            rotate=not has_arrow,
        )
Ejemplo n.º 16
0
def res_import_template(inst: Entity, 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`.
    - `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.
    - `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.
    """
    (
        orig_temp_id,
        replace_tex,
        force_colour,
        force_grid,
        force_type,
        surf_cat,
        bind_tile_pos,
        invert_var,
        color_var,
        visgroup_func,
        visgroup_force_var,
        visgroup_instvars,
        key_block,
        picker_vars,
        outputs,
        sense_offset,
    ) = res.value

    if ':' in orig_temp_id:
        # Split, resolve each part, then recombine.
        temp_id, visgroup = orig_temp_id.split(':', 1)
        temp_id = (
            conditions.resolve_value(inst, temp_id) + ':' +
            conditions.resolve_value(inst, visgroup)
        )
    else:
        temp_id = conditions.resolve_value(inst, orig_temp_id)

    if srctools.conv_bool(conditions.resolve_value(inst, visgroup_force_var)):
        def visgroup_func(group):
            """Use all the groups."""
            yield from group

    # 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, inst) for flag in vis_flag_block):
            visgroups.add(vis_flag_block.real_name)

    if color_var.casefold() == '<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[force_colour]
    # else: False value, no invert.

    origin = Vec.from_str(inst['origin'])
    angles = Vec.from_str(inst['angles', '0 0 0'])
    temp_data = template_brush.import_template(
        template,
        origin,
        angles,
        targetname=inst['targetname', ''],
        force_type=force_type,
        visgroup_choose=visgroup_func,
        add_to_map=True,
        additional_visgroups=visgroups,
        bind_tile_pos=bind_tile_pos,
    )

    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, angles)
        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).rotate(*angles)
            temp_data.detail['movedir'] = move_dir.to_angle()

        for out in outputs:  # type: Output
            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,
        )  # type: Optional[texturing.Portalable]
        if picker_val is not None:
            inst.fixup[picker_var] = picker_val.value
        else:
            inst.fixup[picker_var] = ''
Ejemplo n.º 17
0
def res_insert_overlay(inst: Entity, res: Property):
    """Use a template to insert one or more overlays on a surface.

    Options:

    - ID: The template ID. Brushes will be ignored.
    - Replace: old -> new material replacements.
    - Face_pos: The offset of the brush face.
    - Normal: The direction of the brush face.
    - Offset: An offset to move the overlays by.
    """
    (
        temp_id,
        replace,
        face,
        norm,
        offset,
    ) = res.value

    if temp_id[:1] == '$':
        temp_id = inst.fixup[temp_id]

    origin = Vec.from_str(inst['origin'])  # type: Vec
    angles = Vec.from_str(inst['angles', '0 0 0'])

    face_pos = Vec(face).rotate(*angles)
    face_pos += origin
    normal = Vec(norm).rotate(*angles)

    # Don't make offset change the face_pos value..
    origin += offset.copy().rotate_by_str(
        inst['angles', '0 0 0']
    )

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

    try:
        face_id = SOLIDS[face_pos.as_tuple()].face.id
    except KeyError:
        LOGGER.warning(
            'Overlay brush position is not valid: {}',
            face_pos,
        )
        return

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

    for over in temp.overlay:  # type: Entity
        random.seed('TEMP_OVERLAY_' + over['basisorigin'])
        mat = random.choice(replace.get(
            over['material'],
            (over['material'], ),
        ))
        if mat[:1] == '$':
            mat = inst.fixup[mat]
        if mat.startswith('<') or mat.endswith('>'):
            # Lookup in the style data.
            import vbsp
            LOGGER.info('Tex: {}', vbsp.settings['textures'].keys())
            mat = vbsp.get_tex(mat[1:-1])
        over['material'] = mat
        over['sides'] = str(face_id)

    # Wipe the brushes from the map.
    if temp.detail is not None:
        temp.detail.remove()
        LOGGER.info(
            'Overlay template "{}" could set keep_brushes=0.',
            temp_id,
        )
Ejemplo n.º 18
0
def res_fizzler_pair(vmf: VMF, begin_inst: Entity, res: Property):
    """Modify the instance of a fizzler to link with its pair.

    Each pair will be given a name along the lines of "fizz_name-model1334".
    Values:
        - StartInst, EndInst: The instances used for each end
        - MidInst: An instance placed every 128 units between emitters.
        - SingleInst: If the models are 1 block apart, replace both with this
            instance.
        - BrushKeys, LocalBrushKeys: If specified, a brush entity will be
           generated from some templates at the position of the models.
        - StartTemp, EndTemp, SingleTemp: Templates for the above.
        - SingleBrush: If true, the brush will be shared among the entirety
           of this fizzler.
        - uniqueName: If true, all pairs get a unique name for themselves.
          if False, all instances use the base instance name.
    """
    orig_target = begin_inst['targetname']

    if 'modelEnd' in orig_target:
        return  # We only execute starting from the start side.

    orig_target = orig_target[:-11]  # remove "_modelStart"
    end_name = orig_target + '_modelEnd'  # What we search for

    # The name all these instances get
    if srctools.conv_bool(res['uniqueName', '1'], True):
        pair_name = orig_target + '-model' + str(begin_inst.id)
    else:
        pair_name = orig_target

    orig_file = begin_inst['file']

    begin_inst['file'] = instanceLocs.resolve_one(res['StartInst'], error=True)
    end_file = instanceLocs.resolve_one(res['EndInst'], error=True)
    mid_file = instanceLocs.resolve_one(res['MidInst', ''])
    single_file = instanceLocs.resolve_one(res['SingleInst', ''])

    begin_inst['targetname'] = pair_name

    brush = None
    if 'brushkeys' in res:
        begin_temp = res['StartTemp', '']
        end_temp = res['EndTemp', '']
        single_temp = res['SingleTemp']

        if res.bool('SingleBrush'):
            try:
                brush = PAIR_FIZZ_BRUSHES[orig_target]
            except KeyError:
                pass
        if not brush:
            brush = vmf.create_ent(
                classname='func_brush',  # default
                origin=begin_inst['origin'],
            )
            conditions.set_ent_keys(
                brush,
                begin_inst,
                res,
                'BrushKeys',
            )
            if res.bool('SingleBrush'):
                PAIR_FIZZ_BRUSHES[orig_target] = brush
    else:
        begin_temp = end_temp = single_temp = None

    direction = Vec(0, 0, 1).rotate_by_str(begin_inst['angles'])

    begin_pos = Vec.from_str(begin_inst['origin'])
    axis_1, axis_2, main_axis = PAIR_AXES[direction.as_tuple()]
    for end_inst in vbsp.VMF.by_class['func_instance']:
        if end_inst['targetname', ''] != end_name:
            # Only examine this barrier hazard's instances!
            continue
        if end_inst['file'] != orig_file:
            # Allow adding overlays or other instances at the ends.
            continue
        end_pos = Vec.from_str(end_inst['origin'])
        if (begin_pos[axis_1] == end_pos[axis_1]
                and begin_pos[axis_2] == end_pos[axis_2]):
            length = int(end_pos[main_axis] - begin_pos[main_axis])
            break
    else:
        LOGGER.warning('No matching pair for {}!!', orig_target)
        return

    if length == 0:
        if single_temp:
            temp_brushes = template_brush.import_template(
                single_temp,
                Vec.from_str(begin_inst['origin']),
                Vec.from_str(begin_inst['angles']),
                force_type=template_brush.TEMP_TYPES.world,
                add_to_map=False,
            )
            brush.solids.extend(temp_brushes.world)

        if single_file:
            end_inst.remove()
            begin_inst['file'] = single_file
            # Don't do anything else with end instances.
            return
    else:
        if begin_temp:
            temp_brushes = template_brush.import_template(
                begin_temp,
                Vec.from_str(begin_inst['origin']),
                Vec.from_str(begin_inst['angles']),
                force_type=template_brush.TEMP_TYPES.world,
                add_to_map=False,
            )
            brush.solids.extend(temp_brushes.world)

        if end_temp:
            temp_brushes = template_brush.import_template(
                end_temp,
                Vec.from_str(end_inst['origin']),
                Vec.from_str(end_inst['angles']),
                force_type=template_brush.TEMP_TYPES.world,
                add_to_map=False,
            )
            brush.solids.extend(temp_brushes.world)

    end_inst['targetname'] = pair_name
    end_inst['file'] = end_file

    if mid_file != '' and length:
        # Go 64 from each side, and always have at least 1 section
        # A 128 gap will have length = 0
        for dis in range(0, abs(length) + 1, 128):
            new_pos = begin_pos + direction * dis
            vbsp.VMF.create_ent(
                classname='func_instance',
                targetname=pair_name,
                angles=begin_inst['angles'],
                file=mid_file,
                origin=new_pos,
            )
Ejemplo n.º 19
0
def res_piston_plat(vmf: VMF, inst: Entity, res: Property) -> None:
    """Generates piston platforms with optimized logic."""
    template: template_brush.Template
    visgroup_names: List[str]
    inst_filenames: Dict[str, str]
    has_dn_fizz: bool
    automatic_var: str
    color_var: str
    source_ent: str
    snd_start: str
    snd_loop: str
    snd_stop: str
    (
        template,
        visgroup_names,
        inst_filenames,
        has_dn_fizz,
        automatic_var,
        color_var,
        source_ent,
        snd_start,
        snd_loop,
        snd_stop,
    ) = res.value

    min_pos = inst.fixup.int(FixupVars.PIST_BTM)
    max_pos = inst.fixup.int(FixupVars.PIST_TOP)
    start_up = inst.fixup.bool(FixupVars.PIST_IS_UP)

    # Allow doing variable lookups here.
    visgroup_names = [
        conditions.resolve_value(inst, name) for name in visgroup_names
    ]

    if len(ITEMS[inst['targetname']].inputs) == 0:
        # No inputs. Check for the 'auto' var if applicable.
        if automatic_var and inst.fixup.bool(automatic_var):
            pass
            # The item is automatically moving, so we generate the dynamics.
        else:
            # It's static, we just make that and exit.
            position = max_pos if start_up else min_pos
            inst.fixup[FixupVars.PIST_BTM] = position
            inst.fixup[FixupVars.PIST_TOP] = position
            static_inst = inst.copy()
            vmf.add_ent(static_inst)
            static_inst['file'] = inst_filenames['fullstatic_' + str(position)]
            return

    init_script = 'SPAWN_UP <- {}'.format('true' if start_up else 'false')

    if snd_start and snd_stop:
        packing.pack_files(vmf, snd_start, snd_stop, file_type='sound')
        init_script += '; START_SND <- `{}`; STOP_SND <- `{}`'.format(
            snd_start, snd_stop)
    elif snd_start:
        packing.pack_files(vmf, snd_start, file_type='sound')
        init_script += '; START_SND <- `{}`'.format(snd_start)
    elif snd_stop:
        packing.pack_files(vmf, snd_stop, file_type='sound')
        init_script += '; STOP_SND <- `{}`'.format(snd_stop)

    script_ent = vmf.create_ent(
        classname='info_target',
        targetname=local_name(inst, 'script'),
        vscripts='BEE2/piston/common.nut',
        vscript_init_code=init_script,
        origin=inst['origin'],
    )

    if has_dn_fizz:
        script_ent['thinkfunction'] = 'FizzThink'

    if start_up:
        st_pos, end_pos = max_pos, min_pos
    else:
        st_pos, end_pos = min_pos, max_pos

    script_ent.add_out(
        Output('OnUser1', '!self', 'RunScriptCode',
               'moveto({})'.format(st_pos)),
        Output('OnUser2', '!self', 'RunScriptCode',
               'moveto({})'.format(end_pos)),
    )

    origin = Vec.from_str(inst['origin'])
    angles = Vec.from_str(inst['angles'])
    off = Vec(z=128).rotate(*angles)
    move_ang = off.to_angle()

    # Index -> func_movelinear.
    pistons = {}  # type: Dict[int, Entity]

    static_ent = vmf.create_ent('func_brush', origin=origin)

    for pist_ind in [1, 2, 3, 4]:
        pist_ent = inst.copy()
        vmf.add_ent(pist_ent)

        if pist_ind <= min_pos:
            # It's below the lowest position, so it can be static.
            pist_ent['file'] = inst_filenames['static_' + str(pist_ind)]
            pist_ent['origin'] = brush_pos = origin + pist_ind * off
            temp_targ = static_ent
        else:
            # It's a moving component.
            pist_ent['file'] = inst_filenames['dynamic_' + str(pist_ind)]
            if pist_ind > max_pos:
                # It's 'after' the highest position, so it never extends.
                # So simplify by merging those all.
                # That's before this so it'll have to exist.
                temp_targ = pistons[max_pos]
                if start_up:
                    pist_ent['origin'] = brush_pos = origin + max_pos * off
                else:
                    pist_ent['origin'] = brush_pos = origin + min_pos * off
                pist_ent.fixup['$parent'] = 'pist' + str(max_pos)
            else:
                # It's actually a moving piston.
                if start_up:
                    brush_pos = origin + pist_ind * off
                else:
                    brush_pos = origin + min_pos * off

                pist_ent['origin'] = brush_pos
                pist_ent.fixup['$parent'] = 'pist' + str(pist_ind)

                pistons[pist_ind] = temp_targ = vmf.create_ent(
                    'func_movelinear',
                    targetname=local_name(pist_ent, 'pist' + str(pist_ind)),
                    origin=brush_pos - off,
                    movedir=move_ang,
                    startposition=start_up,
                    movedistance=128,
                    speed=150,
                )
                if pist_ind - 1 in pistons:
                    pistons[pist_ind]['parentname'] = local_name(
                        pist_ent,
                        'pist' + str(pist_ind - 1),
                    )

        if not pist_ent['file']:
            # No actual instance, remove.
            pist_ent.remove()

        temp_result = template_brush.import_template(
            template,
            brush_pos,
            angles,
            force_type=template_brush.TEMP_TYPES.world,
            add_to_map=False,
            additional_visgroups={visgroup_names[pist_ind - 1]},
        )
        temp_targ.solids.extend(temp_result.world)

        template_brush.retexture_template(
            temp_result,
            origin,
            pist_ent.fixup,
            generator=GenCat.PANEL,
        )

    # Associate any set panel with the same entity, if it's present.
    tile_pos = Vec(z=-128)
    tile_pos.localise(origin, angles)
    panel: Optional[Panel] = None
    try:
        tiledef = TILES[tile_pos.as_tuple(), off.norm().as_tuple()]
    except KeyError:
        pass
    else:
        for panel in tiledef.panels:
            if panel.same_item(inst):
                break
        else:  # Checked all of them.
            panel = None

    if panel is not None:
        if panel.brush_ent in vmf.entities and not panel.brush_ent.solids:
            panel.brush_ent.remove()
        panel.brush_ent = pistons[max(pistons.keys())]
        panel.offset = st_pos * off

    if not static_ent.solids and (panel is None
                                  or panel.brush_ent is not static_ent):
        static_ent.remove()

    if snd_loop:
        script_ent['classname'] = 'ambient_generic'
        script_ent['message'] = snd_loop
        script_ent['health'] = 10  # Volume
        script_ent['pitch'] = '100'
        script_ent['spawnflags'] = 16  # Start silent, looped.
        script_ent['radius'] = 1024

        if source_ent:
            # Parent is irrelevant for actual entity locations, but it
            # survives for the script to read.
            script_ent['SourceEntityName'] = script_ent[
                'parentname'] = local_name(inst, source_ent)
Ejemplo n.º 20
0
def res_conveyor_belt(inst: Entity, res: Property):
    """Create a conveyor belt.

    * Options:
        * `SegmentInst`: Generated at each square. (`track` is the name of the path to attach to.)
        * `TrackTeleport`: Set the track points so they teleport trains to the start.
        * `Speed`: The fixup or number for the train speed.
        * `MotionTrig`: If set, a trigger_multiple will be spawned that `EnableMotion`s
          weighted cubes. The value is the name of the relevant filter.
        * `EndOutput`: Adds an output to the last track. The value is the same as
          outputs in VMFs.
        `RotateSegments`: If true (default), force segments to face in the
          direction of movement.
        * `BeamKeys`: If set, a list of keyvalues to use to generate an env_beam 
          travelling from start to end.
        `RailTemplate`: A template for the track sections. This is made into a non-solid
          func_brush, combining all sections.
        * `NoPortalFloor`: If set, add a `func_noportal_volume` on the floor under the track.
        * `PaintFizzler`: If set, add a paint fizzler underneath the belt.
    """
    move_dist = srctools.conv_int(inst.fixup['$travel_distance'])

    if move_dist <= 2:
        # There isn't room for a catwalk, so don't bother.
        inst.remove()
        return

    move_dir = Vec(1, 0, 0).rotate_by_str(inst.fixup['$travel_direction'])
    move_dir.rotate_by_str(inst['angles'])
    start_offset = srctools.conv_float(inst.fixup['$starting_position'], 0)
    teleport_to_start = res.bool('TrackTeleport', True)
    segment_inst_file = instanceLocs.resolve_one(res['SegmentInst', ''])
    rail_template = res['RailTemplate', None]

    vmf = inst.map

    track_speed = res['speed', None]

    start_pos = Vec.from_str(inst['origin'])
    end_pos = start_pos + move_dist * move_dir

    if start_offset > 0:
        # If an oscillating platform, move to the closest side..
        offset = start_offset * move_dir
        # The instance is placed this far along, so move back to the end.
        start_pos -= offset
        end_pos -= offset
        if start_offset > 0.5:
            # Swap the direction of movement..
            start_pos, end_pos = end_pos, start_pos
        inst['origin'] = start_pos

    # Find the angle which generates an instance pointing in the direction
    # of movement, with the same normal.
    norm = Vec(z=1).rotate_by_str(inst['angles'])
    for roll in range(0, 360, 90):
        angles = move_dir.to_angle(roll)
        if Vec(z=1).rotate(*angles) == norm:
            break
    else:
        raise ValueError("Can't find angles to give a"
                         ' z={} and x={}!'.format(norm, move_dir))

    if res.bool('rotateSegments', True):
        inst['angles'] = angles
    else:
        angles = Vec.from_str(inst['angles'])

    # Add the EnableMotion trigger_multiple seen in platform items.
    # This wakes up cubes when it starts moving.
    motion_filter = res['motionTrig', None]

    # Disable on walls, or if the conveyor can't be turned on.
    if norm != (0, 0, 1) or inst.fixup['$connectioncount'] == '0':
        motion_filter = None

    track_name = conditions.local_name(inst, 'segment_{}')
    rail_temp_solids = []
    last_track = None
    # Place beams at the top, so they don't appear inside wall sections.
    beam_start = start_pos + 48 * norm  # type: Vec
    beam_end = end_pos + 48 * norm  # type: Vec
    for index, pos in enumerate(beam_start.iter_line(beam_end, stride=128),
                                start=1):
        track = vmf.create_ent(
            classname='path_track',
            targetname=track_name.format(index) + '-track',
            origin=pos,
            spawnflags=0,
            orientationtype=0,  # Don't rotate
        )
        if track_speed is not None:
            track['speed'] = track_speed
        if last_track:
            last_track['target'] = track['targetname']

        if index == 1 and teleport_to_start:
            track['spawnflags'] = 16  # Teleport here..

        last_track = track

        # Don't place at the last point - it doesn't teleport correctly,
        # and would be one too many.
        if segment_inst_file and pos != end_pos:
            seg_inst = vmf.create_ent(
                classname='func_instance',
                targetname=track_name.format(index),
                file=segment_inst_file,
                origin=pos,
                angles=angles,
            )
            seg_inst.fixup.update(inst.fixup)

        if rail_template:
            temp = template_brush.import_template(
                rail_template,
                pos,
                angles,
                force_type=template_brush.TEMP_TYPES.world,
                add_to_map=False,
            )
            rail_temp_solids.extend(temp.world)

    if rail_temp_solids:
        vmf.create_ent(
            classname='func_brush',
            origin=beam_start,
            spawnflags=1,  # Ignore +USE
            solidity=1,  # Not solid
            vrad_brush_cast_shadows=1,
            drawinfastreflection=1,
        ).solids = rail_temp_solids

    if teleport_to_start:
        # Link back to the first track..
        last_track['target'] = track_name.format(1) + '-track'

    # Generate an env_beam pointing from the start to the end of the track.
    beam_keys = res.find_key('BeamKeys', [])
    if beam_keys.value:
        beam = vmf.create_ent(classname='env_beam')

        # 3 offsets - x = distance from walls, y = side, z = height
        beam_off = beam_keys.vec('origin', 0, 63, 56)

        for prop in beam_keys:
            beam[prop.real_name] = prop.value

        # Localise the targetname so it can be triggered..
        beam['LightningStart'] = beam['targetname'] = conditions.local_name(
            inst, beam['targetname', 'beam'])
        del beam['LightningEnd']
        beam['origin'] = start_pos + Vec(
            -beam_off.x,
            beam_off.y,
            beam_off.z,
        ).rotate(*angles)
        beam['TargetPoint'] = end_pos + Vec(
            +beam_off.x,
            beam_off.y,
            beam_off.z,
        ).rotate(*angles)

    # Allow adding outputs to the last path_track.
    for prop in res.find_all('EndOutput'):
        output = Output.parse(prop)
        output.output = 'OnPass'
        output.inst_out = None
        output.comma_sep = False
        output.target = conditions.local_name(inst, output.target)
        last_track.add_out(output)

    if motion_filter is not None:
        motion_trig = vmf.create_ent(
            classname='trigger_multiple',
            targetname=conditions.local_name(inst, 'enable_motion_trig'),
            origin=start_pos,
            filtername=motion_filter,
            startDisabled=1,
            wait=0.1,
        )
        motion_trig.add_out(
            Output('OnStartTouch', '!activator', 'ExitDisabledState'))
        # Match the size of the original...
        motion_trig.solids.append(
            vmf.make_prism(
                start_pos + Vec(72, -56, 58).rotate(*angles),
                end_pos + Vec(-72, 56, 144).rotate(*angles),
                mat='tools/toolstrigger',
            ).solid)

    if res.bool('NoPortalFloor'):
        # Block portals on the floor..
        floor_noportal = vmf.create_ent(
            classname='func_noportal_volume',
            origin=beam_start,
        )
        floor_noportal.solids.append(
            vmf.make_prism(
                start_pos + Vec(-60, -60, -66).rotate(*angles),
                end_pos + Vec(60, 60, -60).rotate(*angles),
                mat='tools/toolsinvisible',
            ).solid)

    # A brush covering under the platform.
    base_trig = vmf.make_prism(
        start_pos + Vec(-64, -64, 48).rotate(*angles),
        end_pos + Vec(64, 64, 56).rotate(*angles),
        mat='tools/toolsinvisible',
    ).solid

    vmf.add_brush(base_trig)

    # Make a paint_cleanser under the belt..
    if res.bool('PaintFizzler'):
        pfizz = vmf.create_ent(
            classname='trigger_paint_cleanser',
            origin=start_pos,
        )
        pfizz.solids.append(base_trig.copy())
        for face in pfizz.sides():
            face.mat = 'tools/toolstrigger'
Ejemplo n.º 21
0
def hollow_block(solid_group: solidGroup, remove_orig_face=False):
    """Convert a solid into a embeddedVoxel-style block.

    The original brushes must be in the SOLIDS dict. They will be replaced.
    This returns a dict mapping normals to the new solidGroups.
    If remove_orig_face is true, the starting face will not be kept.
    """
    import vbsp
    orig_solid = solid_group.solid  # type: Solid

    bbox_min, bbox_max = orig_solid.get_bbox()
    if 4 in (bbox_max - bbox_min):
        # If it's 4 units thick, skip hollowing - PeTI did it already.
        if remove_orig_face:
            VMF.remove_brush(orig_solid)
            del SOLIDS[solid_group.face.get_origin().as_tuple()]
        return

    VMF.remove_brush(orig_solid)

    for face in orig_solid.sides:
        if remove_orig_face and face is solid_group.face:
            # Skip readding the original face, which removes it.
            continue

        solid_key = face.get_origin().as_tuple()

        if face.mat.casefold() == 'tools/toolsnodraw' and face not in vbsp.IGNORED_FACES:
            # If it's nodraw, we can skip it. If it's also in IGNORED_FACES
            # though a condition has set it, so recreate it (it might be sealing
            # the void behind a func_detail or model).
            continue

        # Remove this face from the solids list, and get the group.
        face_group = SOLIDS.pop(solid_key, None)

        normal = face.normal()

        # Generate our new brush.
        new_brushes = template_brush.import_template(
            TEMP_EMBEDDED_VOXEL,
            face.get_origin(),
            # The normal Z is swapped...
            normal.to_angle(),
            force_type=template_brush.TEMP_TYPES.world,
        ).world

        # Texture the new brush..
        for brush in new_brushes:  # type: Solid
            for new_face in brush.sides:
                # The SKIP brush is the surface, all the others are nodraw.
                if new_face.mat.casefold() != 'tools/toolsskip':
                     continue

                # Overwrite all the properties, to make the new brush
                # the same as the original.
                new_face.mat = face.mat
                new_face.uaxis = face.uaxis
                new_face.vaxis = face.vaxis
                new_face.planes = face.planes
                new_face.ham_rot = 0

                # Swap the two IDs - that way when the original face gets
                # deleted the auto-set ID will vanish, leaving the original
                # ID.
                new_face.id, face.id = face.id, new_face.id

                # Remove the new face, if the original wasn't in IGNORED_FACES.
                if face not in vbsp.IGNORED_FACES:
                    vbsp.IGNORED_FACES.remove(new_face)

                # Make a new SolidGroup to match the face.
                if face_group is not None:
                    SOLIDS[solid_key] = solidGroup(
                        new_face,
                        brush,
                        face_group.normal,
                        face_group.color,
                    )
Ejemplo n.º 22
0
def res_faith_mods(inst: Entity, res: Property):
    """Modify the `trigger_catapult` that is created for `ItemFaithPlate` items.

    Values:
      
    - `raise_trig`: Raise or lower the `trigger_catapult`s by this amount.
    - `angled_targ`, `angled_in`: Instance entity and input for angled plates
    - `straight_targ`, `straight_in`: Instance entity and input for
      straight plates
    - `instvar`: A $replace value to set to either 'angled' or '
      'straight'.
    - `enabledVar`: A `$replace` value which will be copied to the main
      trigger's Start Disabled value (and inverted).
    - `trig_temp`: An ID for a template brush to add. This will be offset by
      the trigger's position (in the case of the `helper` trigger).
    """
    # Get data about the trigger this instance uses for flinging
    fixup_var = res['instvar', '']
    trig_enabled = res['enabledVar', None]
    trig_temp = res['trig_temp', '']
    offset = srctools.conv_int(res['raise_trig', '0'])
    if offset:
        offset = Vec(0, 0, offset).rotate_by_str(inst['angles', '0 0 0'])
    else:
        offset = Vec()

    if trig_enabled is not None:
        trig_enabled = srctools.conv_bool(inst.fixup[trig_enabled])
    else:
        trig_enabled = None

    for trig in vbsp.VMF.by_class['trigger_catapult']:
        if inst['targetname'] not in trig['targetname']:
            continue

        # Edit both the normal and the helper trigger..
        trig_origin = trig['origin'] = Vec.from_str(trig['origin']) + offset

        if offset and not trig_temp:
            # No template, shift the current brushes.
            for solid in trig.solids:
                solid.translate(offset)
        elif trig_temp:
            trig.solids = template_brush.import_template(
                temp_name=trig_temp,
                origin=trig_origin,
                angles=Vec.from_str(inst['angles']),
                force_type=template_brush.TEMP_TYPES.world,
            ).world
            # Remove the trigger solids from worldspawn..
            for solid in trig.solids:
                vbsp.VMF.remove_brush(solid)

        if trig_enabled is not None and 'helper' not in trig['targetname']:
            trig['startdisabled'] = srctools.bool_as_int(not trig_enabled)

        # Inspect the outputs to determine the type.
        # We also change them if desired, since that's not possible
        # otherwise.

        for out in trig.outputs:
            if out.inst_in == 'animate_angled_relay':
                # Instead of an instance: output, use local names.
                # This allows us to strip the proxy, as well as use
                # overlay instances.
                out.inst_in = None
                out.target = conditions.local_name(
                    inst,
                    res['angled_targ', 'animate_angled_relay']
                )
                out.input = res['angled_in', 'Trigger']
                if fixup_var:
                    inst.fixup[fixup_var] = 'angled'
                break  # There's only one output we want to look for...

            elif out.inst_in == 'animate_straightup_relay':
                out.inst_in = None
                out.target = conditions.local_name(
                    inst,
                    res[
                        'straight_targ',
                        'animate_straightup_relay'
                    ],
                )
                out.input = res['straight_in', 'Trigger']

                if fixup_var:
                    inst.fixup[fixup_var] = 'straight'
                break
Ejemplo n.º 23
0
def res_insert_overlay(inst: Entity, res: Property) -> None:
    """Use a template to insert one or more overlays on a surface.

    Options:

    - ID: The template ID. Brushes will be ignored.
    - Replace: old -> new material replacements.
    - Face_pos: The offset of the brush face.
    - Normal: The direction of the brush face.
    - Offset: An offset to move the overlays by.
    """
    (
        temp_id,
        replace,
        face,
        norm,
        offset,
    ) = res.value

    if temp_id[:1] == '$':
        temp_id = inst.fixup[temp_id]

    origin = Vec.from_str(inst['origin'])  # type: Vec
    angles = Vec.from_str(inst['angles', '0 0 0'])

    face_pos = Vec(face).rotate(*angles)
    face_pos += origin
    normal = Vec(norm).rotate(*angles)

    # Don't make offset change the face_pos value..
    origin += offset.copy().rotate_by_str(inst['angles', '0 0 0'])

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

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

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

    for over in temp.overlay:  # type: Entity
        random.seed('TEMP_OVERLAY_' + over['basisorigin'])
        mat = over['material']
        try:
            mat = random.choice(replace[over['material'].casefold().replace(
                '\\', '/')])
        except KeyError:
            pass

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

    # Wipe the brushes from the map.
    if temp.detail is not None:
        temp.detail.remove()
        LOGGER.info(
            'Overlay template "{}" could set keep_brushes=0.',
            temp_id,
        )
Ejemplo n.º 24
0
def generate_fizzlers(vmf: VMF):
    """Generates fizzler models and the brushes according to their set types.

    After this is done, fizzler-related conditions will not function correctly.
    However the model instances are now available for modification.
    """
    from vbsp import MAP_RAND_SEED

    for fizz in FIZZLERS.values():
        if fizz.base_inst not in vmf.entities:
            continue  # The fizzler was removed from the map.

        fizz_name = fizz.base_inst['targetname']
        fizz_type = fizz.fizz_type

        # Static versions are only used for fizzlers which start on.
        # Permanently-off fizzlers are kinda useless, so we don't need
        # to bother optimising for it.
        is_static = bool(
            fizz.base_inst.fixup.int('$connectioncount', 0) == 0
            and fizz.base_inst.fixup.bool('$start_enabled', 1))

        pack_list = (fizz.fizz_type.pack_lists_static
                     if is_static else fizz.fizz_type.pack_lists)
        for pack in pack_list:
            packing.pack_list(vmf, pack)

        if fizz_type.inst[FizzInst.BASE, is_static]:
            random.seed('{}_fizz_base_{}'.format(MAP_RAND_SEED, fizz_name))
            fizz.base_inst['file'] = random.choice(
                fizz_type.inst[FizzInst.BASE, is_static])

        if not fizz.emitters:
            LOGGER.warning('No emitters for fizzler "{}"!', fizz_name)
            continue

        # Brush index -> entity for ones that need to merge.
        # template_brush is used for the templated one.
        single_brushes = {}  # type: Dict[FizzlerBrush, Entity]

        if fizz_type.temp_max or fizz_type.temp_min:
            template_brush_ent = vmf.create_ent(
                classname='func_brush',
                origin=fizz.base_inst['origin'],
            )
            conditions.set_ent_keys(
                template_brush_ent,
                fizz.base_inst,
                fizz_type.temp_brush_keys,
            )
        else:
            template_brush_ent = None

        up_dir = fizz.up_axis
        forward = (fizz.emitters[0][1] - fizz.emitters[0][0]).norm()

        min_angles = FIZZ_ANGLES[forward.as_tuple(), up_dir.as_tuple()]
        max_angles = FIZZ_ANGLES[(-forward).as_tuple(), up_dir.as_tuple()]

        model_min = (fizz_type.inst[FizzInst.PAIR_MIN, is_static]
                     or fizz_type.inst[FizzInst.ALL, is_static])
        model_max = (fizz_type.inst[FizzInst.PAIR_MAX, is_static]
                     or fizz_type.inst[FizzInst.ALL, is_static])

        if not model_min or not model_max:
            raise ValueError(
                'No model specified for one side of "{}"'
                ' fizzlers'.format(fizz_type.id), )

        # Define a function to do the model names.
        model_index = 0
        if fizz_type.model_naming is ModelName.SAME:

            def get_model_name(ind):
                """Give every emitter the base's name."""
                return fizz_name
        elif fizz_type.model_naming is ModelName.LOCAL:

            def get_model_name(ind):
                """Give every emitter a name local to the base."""
                return fizz_name + '-' + fizz_type.model_name
        elif fizz_type.model_naming is ModelName.PAIRED:

            def get_model_name(ind):
                """Give each pair of emitters the same unique name."""
                return '{}-{}{:02}'.format(
                    fizz_name,
                    fizz_type.model_name,
                    ind,
                )
        elif fizz_type.model_naming is ModelName.UNIQUE:

            def get_model_name(ind):
                """Give every model a unique name."""
                nonlocal model_index
                model_index += 1
                return '{}-{}{:02}'.format(
                    fizz_name,
                    fizz_type.model_name,
                    model_index,
                )
        else:
            raise ValueError('Bad ModelName?')

        # Generate env_beam pairs.
        for beam in fizz_type.beams:
            beam_template = Entity(vmf)
            conditions.set_ent_keys(beam_template, fizz.base_inst, beam.keys)
            beam_template['classname'] = 'env_beam'
            del beam_template[
                'LightningEnd']  # Don't allow users to set end pos.
            name = beam_template['targetname'] + '_'

            counter = 1
            for seg_min, seg_max in fizz.emitters:
                for offset in beam.offset:  # type: Vec
                    min_off = offset.copy()
                    max_off = offset.copy()
                    min_off.localise(seg_min, min_angles)
                    max_off.localise(seg_max, max_angles)
                    beam_ent = beam_template.copy()
                    vmf.add_ent(beam_ent)

                    # Allow randomising speed and direction.
                    if 0 < beam.speed_min < beam.speed_max:
                        random.seed('{}{}{}'.format(MAP_RAND_SEED, min_off,
                                                    max_off))
                        beam_ent['TextureScroll'] = random.randint(
                            beam.speed_min, beam.speed_max)
                        if random.choice((False, True)):
                            # Flip to reverse direction.
                            min_off, max_off = max_off, min_off

                    beam_ent['origin'] = min_off
                    beam_ent['LightningStart'] = beam_ent['targetname'] = (
                        name + str(counter))
                    counter += 1
                    beam_ent['targetpoint'] = max_off

        mat_mod_tex = {}  # type: Dict[FizzlerBrush, Set[str]]
        for brush_type in fizz_type.brushes:
            if brush_type.mat_mod_var is not None:
                mat_mod_tex[brush_type] = set()

        # Record the data for trigger hurts so flinch triggers can match them.
        trigger_hurt_name = ''
        trigger_hurt_start_disabled = '0'

        for seg_ind, (seg_min, seg_max) in enumerate(fizz.emitters, start=1):
            length = (seg_max - seg_min).mag()
            random.seed('{}_fizz_{}'.format(MAP_RAND_SEED, seg_min))
            if length == 128 and fizz_type.inst[FizzInst.PAIR_SINGLE,
                                                is_static]:
                min_inst = vmf.create_ent(
                    targetname=get_model_name(seg_ind),
                    classname='func_instance',
                    file=random.choice(fizz_type.inst[FizzInst.PAIR_SINGLE,
                                                      is_static]),
                    origin=(seg_min + seg_max) / 2,
                    angles=min_angles,
                )
            else:
                # Both side models.
                min_inst = vmf.create_ent(
                    targetname=get_model_name(seg_ind),
                    classname='func_instance',
                    file=random.choice(model_min),
                    origin=seg_min,
                    angles=min_angles,
                )
                random.seed('{}_fizz_{}'.format(MAP_RAND_SEED, seg_max))
                max_inst = vmf.create_ent(
                    targetname=get_model_name(seg_ind),
                    classname='func_instance',
                    file=random.choice(model_max),
                    origin=seg_max,
                    angles=max_angles,
                )
                max_inst.fixup.update(fizz.base_inst.fixup)
            min_inst.fixup.update(fizz.base_inst.fixup)

            if fizz_type.inst[FizzInst.GRID, is_static]:
                # Generate one instance for each position.

                # Go 64 from each side, and always have at least 1 section
                # A 128 gap will have length = 0
                for ind, dist in enumerate(range(64, round(length) - 63, 128)):
                    mid_pos = seg_min + forward * dist
                    random.seed('{}_fizz_mid_{}'.format(
                        MAP_RAND_SEED, mid_pos))
                    mid_inst = vmf.create_ent(
                        classname='func_instance',
                        targetname=fizz_name,
                        angles=min_angles,
                        file=random.choice(fizz_type.inst[FizzInst.GRID,
                                                          is_static]),
                        origin=mid_pos,
                    )
                    mid_inst.fixup.update(fizz.base_inst.fixup)

            if template_brush_ent is not None:
                if length == 128 and fizz_type.temp_single:
                    temp = template_brush.import_template(
                        fizz_type.temp_single,
                        (seg_min + seg_max) / 2,
                        min_angles,
                        force_type=template_brush.TEMP_TYPES.world,
                        add_to_map=False,
                    )
                    template_brush_ent.solids.extend(temp.world)
                else:
                    if fizz_type.temp_min:
                        temp = template_brush.import_template(
                            fizz_type.temp_min,
                            seg_min,
                            min_angles,
                            force_type=template_brush.TEMP_TYPES.world,
                            add_to_map=False,
                        )
                        template_brush_ent.solids.extend(temp.world)
                    if fizz_type.temp_max:
                        temp = template_brush.import_template(
                            fizz_type.temp_max,
                            seg_max,
                            max_angles,
                            force_type=template_brush.TEMP_TYPES.world,
                            add_to_map=False,
                        )
                        template_brush_ent.solids.extend(temp.world)

            # Generate the brushes.
            for brush_type in fizz_type.brushes:
                brush_ent = None
                # If singular, we reuse the same brush ent for all the segments.
                if brush_type.singular:
                    brush_ent = single_brushes.get(brush_type, None)

                # Non-singular or not generated yet - make the entity.
                if brush_ent is None:
                    brush_ent = vmf.create_ent(classname='func_brush')

                    for key_name, key_value in brush_type.keys.items():
                        brush_ent[key_name] = conditions.resolve_value(
                            fizz.base_inst, key_value)

                    for key_name, key_value in brush_type.local_keys.items():
                        brush_ent[key_name] = conditions.local_name(
                            fizz.base_inst,
                            conditions.resolve_value(
                                fizz.base_inst,
                                key_value,
                            ))

                    brush_ent['targetname'] = conditions.local_name(
                        fizz.base_inst,
                        brush_type.name,
                    )
                    # Set this to the center, to make sure it's not going to leak.
                    brush_ent['origin'] = (seg_min + seg_max) / 2

                    # For fizzlers flat on the floor/ceiling, scanlines look
                    # useless. Turn them off.
                    if 'usescanline' in brush_ent and fizz.normal().z:
                        brush_ent['UseScanline'] = 0

                    if brush_ent['classname'] == 'trigger_hurt':
                        trigger_hurt_name = brush_ent['targetname']
                        trigger_hurt_start_disabled = brush_ent[
                            'startdisabled']

                    if brush_type.set_axis_var:
                        brush_ent['vscript_init_code'] = (
                            'axis <- `{}`;'.format(fizz.normal().axis(), ))

                    for out in brush_type.outputs:
                        new_out = out.copy()
                        new_out.target = conditions.local_name(
                            fizz.base_inst,
                            new_out.target,
                        )
                        brush_ent.add_out(new_out)

                    if brush_type.singular:
                        # Record for the next iteration.
                        single_brushes[brush_type] = brush_ent

                # If we have a material_modify_control to generate,
                # we need to parent it to ourselves to restrict it to us
                # only. We also need one for each material, so provide a
                # function to the generator which adds to a set.
                if brush_type.mat_mod_var is not None:
                    used_tex_func = mat_mod_tex[brush_type].add
                else:

                    def used_tex_func(val):
                        """If not, ignore those calls."""
                        return None

                # Generate the brushes and texture them.
                brush_ent.solids.extend(
                    brush_type.generate(
                        vmf,
                        fizz,
                        seg_min,
                        seg_max,
                        used_tex_func,
                    ))

        if trigger_hurt_name:
            fizz.gen_flinch_trigs(
                vmf,
                trigger_hurt_name,
                trigger_hurt_start_disabled,
            )

        # If we have the config, but no templates used anywhere...
        if template_brush_ent is not None and not template_brush_ent.solids:
            template_brush_ent.remove()

        for brush_type, used_tex in mat_mod_tex.items():
            brush_name = conditions.local_name(fizz.base_inst, brush_type.name)
            mat_mod_name = conditions.local_name(fizz.base_inst,
                                                 brush_type.mat_mod_name)
            for off, tex in zip(MATMOD_OFFSETS, sorted(used_tex)):
                pos = off.copy().rotate(*min_angles)
                pos += Vec.from_str(fizz.base_inst['origin'])
                vmf.create_ent(
                    classname='material_modify_control',
                    origin=pos,
                    targetname=mat_mod_name,
                    materialName='materials/' + tex + '.vmt',
                    materialVar=brush_type.mat_mod_var,
                    parentname=brush_name,
                )
Ejemplo n.º 25
0
def res_piston_plat(vmf: VMF, inst: Entity, res: Property):
    """Generates piston platforms with optimized logic."""
    (
        template,
        visgroup_names,
        inst_filenames,
        automatic_var,
        color_var,
        source_ent,
        snd_start,
        snd_loop,
        snd_stop,
    ) = res.value  # type: template_brush.Template, List[str], Dict[str, str], str, str, str, str, str, str

    min_pos = inst.fixup.int(FixupVars.PIST_BTM)
    max_pos = inst.fixup.int(FixupVars.PIST_TOP)
    start_up = inst.fixup.bool(FixupVars.PIST_IS_UP)

    # Allow doing variable lookups here.
    visgroup_names = [
        conditions.resolve_value(inst, name)
        for name in visgroup_names
    ]

    if len(ITEMS[inst['targetname']].inputs) == 0:
        # No inputs. Check for the 'auto' var if applicable.
        if automatic_var and inst.fixup.bool(automatic_var):
            pass
            # The item is automatically moving, so we generate the dynamics.
        else:
            # It's static, we just make that and exit.
            position = max_pos if start_up else min_pos
            inst.fixup[FixupVars.PIST_BTM] = position
            inst.fixup[FixupVars.PIST_TOP] = position
            static_inst = inst.copy()
            vmf.add_ent(static_inst)
            static_inst['file'] = inst_filenames['fullstatic_' + str(position)]
            return

    init_script = 'SPAWN_UP <- {}'.format('true' if start_up else 'false')

    if snd_start and snd_stop:
        packing.pack_files(vmf, snd_start, snd_stop, file_type='sound')
        init_script += '; START_SND <- `{}`; STOP_SND <- `{}`'.format(snd_start, snd_stop)
    elif snd_start:
        packing.pack_files(vmf, snd_start, file_type='sound')
        init_script += '; START_SND <- `{}`'.format(snd_start)
    elif snd_stop:
        packing.pack_files(vmf, snd_stop, file_type='sound')
        init_script += '; STOP_SND <- `{}`'.format(snd_stop)

    script_ent = vmf.create_ent(
        classname='info_target',
        targetname=local_name(inst, 'script'),
        vscripts='BEE2/piston/common.nut',
        vscript_init_code=init_script,
        origin=inst['origin'],
    )

    if start_up:
        st_pos, end_pos = max_pos, min_pos
    else:
        st_pos, end_pos = min_pos, max_pos

    script_ent.add_out(
        Output('OnUser1', '!self', 'RunScriptCode', 'moveto({})'.format(st_pos)),
        Output('OnUser2', '!self', 'RunScriptCode', 'moveto({})'.format(end_pos)),
    )

    origin = Vec.from_str(inst['origin'])
    angles = Vec.from_str(inst['angles'])
    off = Vec(z=128).rotate(*angles)
    move_ang = off.to_angle()

    # Index -> func_movelinear.
    pistons = {}  # type: Dict[int, Entity]

    static_ent = vmf.create_ent('func_brush', origin=origin)

    color_var = conditions.resolve_value(inst, color_var).casefold()

    if color_var == 'white':
        top_color = template_brush.MAT_TYPES.white
    elif color_var == 'black':
        top_color = template_brush.MAT_TYPES.black
    else:
        top_color = None

    for pist_ind in range(1, 5):
        pist_ent = inst.copy()
        vmf.add_ent(pist_ent)

        if pist_ind <= min_pos:
            # It's below the lowest position, so it can be static.
            pist_ent['file'] = inst_filenames['static_' + str(pist_ind)]
            pist_ent['origin'] = brush_pos = origin + pist_ind * off
            temp_targ = static_ent
        else:
            # It's a moving component.
            pist_ent['file'] = inst_filenames['dynamic_' + str(pist_ind)]
            if pist_ind > max_pos:
                # It's 'after' the highest position, so it never extends.
                # So simplify by merging those all.
                # That's before this so it'll have to exist.
                temp_targ = pistons[max_pos]
                if start_up:
                    pist_ent['origin'] = brush_pos = origin + max_pos * off
                else:
                    pist_ent['origin'] = brush_pos = origin + min_pos * off
                pist_ent.fixup['$parent'] = 'pist' + str(max_pos)
            else:
                # It's actually a moving piston.
                if start_up:
                    brush_pos = origin + pist_ind * off
                else:
                    brush_pos = origin + min_pos * off

                pist_ent['origin'] = brush_pos
                pist_ent.fixup['$parent'] = 'pist' + str(pist_ind)

                pistons[pist_ind] = temp_targ = vmf.create_ent(
                    'func_movelinear',
                    targetname=local_name(pist_ent, 'pist' + str(pist_ind)),
                    origin=brush_pos - off,
                    movedir=move_ang,
                    startposition=start_up,
                    movedistance=128,
                    speed=150,
                )
                if pist_ind - 1 in pistons:
                    pistons[pist_ind]['parentname'] = local_name(
                        pist_ent, 'pist' + str(pist_ind - 1),
                    )

        if not pist_ent['file']:
            # No actual instance, remove.
            pist_ent.remove()

        temp_result = template_brush.import_template(
            template,
            brush_pos,
            angles,
            force_type=template_brush.TEMP_TYPES.world,
            add_to_map=False,
            additional_visgroups={visgroup_names[pist_ind - 1]},
        )
        temp_targ.solids.extend(temp_result.world)

        template_brush.retexture_template(
            temp_result,
            origin,
            pist_ent.fixup,
            force_colour=top_color,
            force_grid='special',
            no_clumping=True,
        )

    if not static_ent.solids:
        static_ent.remove()

    if snd_loop:
        script_ent['classname'] = 'ambient_generic'
        script_ent['message'] = snd_loop
        script_ent['health'] = 10  # Volume
        script_ent['pitch'] = '100'
        script_ent['spawnflags'] = 16  # Start silent, looped.
        script_ent['radius'] = 1024

        if source_ent:
            # Parent is irrelevant for actual entity locations, but it
            # survives for the script to read.
            script_ent['SourceEntityName'] = script_ent['parentname'] = local_name(inst, source_ent)
Ejemplo n.º 26
0
def res_import_template(inst: Entity, res: Property):
    """Import a template VMF file, retexturing it to match orientation.

    It will be placed overlapping the given instance.  
    Options:  
    - ID: The ID of the template to be inserted. Add visgroups to additionally
            add after a colon, comma-seperated (temp_id:vis1,vis2)
    - 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.
    - replaceBrush: The position of a brush to replace (0 0 0=the surface).
            This brush will be removed, and overlays will be fixed to use
            all faces with the same normal. Can alternately be a block:
            - Pos: The position to replace.
            - additionalIDs: Space-separated list of face IDs in the template
              to also fix for overlays. The surface should have close to a
              vertical normal, to prevent rescaling the overlay.
            - removeBrush: If true, the original brush will not be removed.
            - transferOverlay: Allow disabling transferring overlays to this
              template. The IDs will be removed instead. (This can be an instvar).
    - 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. If 'none' (default),
            they are ignored. If 'choose', one is chosen. If a number, that
            is the percentage chance for each visgroup to be added.
    - visgroup_force_var: If set and True, visgroup is ignored and all groups
            are added.
    """
    (
        orig_temp_id,
        replace_tex,
        force_colour,
        force_grid,
        force_type,
        replace_brush_pos,
        rem_replace_brush,
        transfer_overlays,
        additional_replace_ids,
        invert_var,
        color_var,
        visgroup_func,
        visgroup_force_var,
        key_block,
    ) = res.value
    temp_id = conditions.resolve_value(inst, orig_temp_id)

    if srctools.conv_bool(conditions.resolve_value(inst, visgroup_force_var)):

        def visgroup_func(group):
            """Use all the groups."""
            yield from group

    temp_name, visgroups = template_brush.parse_temp_name(temp_id)
    try:
        template = template_brush.get_template(temp_name)
    except template_brush.InvalidTemplateName:
        # The template map is read in after setup is performed, so
        # it must be checked here!
        # We don't want an error, just quit
        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)
        return

    if color_var.casefold() == '<editor>':
        # Check traits for the colour it should be.
        traits = instance_traits.get(inst)
        if 'white' in traits:
            force_colour = template_brush.MAT_TYPES.white
        elif 'black' in traits:
            force_colour = template_brush.MAT_TYPES.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 = template_brush.MAT_TYPES.white
        elif color_val == 'black':
            force_colour = template_brush.MAT_TYPES.black
    # else: no color var

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

    origin = Vec.from_str(inst['origin'])
    angles = Vec.from_str(inst['angles', '0 0 0'])
    temp_data = template_brush.import_template(
        template,
        origin,
        angles,
        targetname=inst['targetname', ''],
        force_type=force_type,
        visgroup_choose=visgroup_func,
        add_to_map=True,
        additional_visgroups=visgroups,
    )

    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, angles)
        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).rotate(*angles)
            temp_data.detail['movedir'] = move_dir.to_angle()

        # Add it to the list of ignored brushes, so vbsp.change_brush() doesn't
        # modify it.
        vbsp.IGNORED_BRUSH_ENTS.add(temp_data.detail)

    try:
        # This is the original brush the template is replacing. We fix overlay
        # face IDs, so this brush is replaced by the faces in the template
        # pointing
        # the same way.
        if replace_brush_pos is None:
            raise KeyError  # Not set, raise to jump out of the try block

        pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z)
        pos += origin
        brush_group = SOLIDS[pos.as_tuple()]
    except KeyError:
        # Not set or solid group doesn't exist, skip..
        pass
    else:
        LOGGER.info('IDS: {}', additional_replace_ids | template.overlay_faces)
        conditions.steal_from_brush(
            temp_data,
            brush_group,
            rem_replace_brush,
            map(int, additional_replace_ids | template.overlay_faces),
            conv_bool(conditions.resolve_value(inst, transfer_overlays), True),
        )

    template_brush.retexture_template(
        temp_data,
        origin,
        inst.fixup,
        replace_tex,
        force_colour,
        force_grid,
    )
Ejemplo n.º 27
0
def generate_fizzlers(vmf: VMF):
    """Generates fizzler models and the brushes according to their set types.

    After this is done, fizzler-related conditions will not function correctly.
    However the model instances are now available for modification.
    """
    from vbsp import MAP_RAND_SEED

    for fizz in FIZZLERS.values():
        if fizz.base_inst not in vmf.entities:
            continue   # The fizzler was removed from the map.

        fizz_name = fizz.base_inst['targetname']
        fizz_type = fizz.fizz_type

        # Static versions are only used for fizzlers which start on.
        # Permanently-off fizzlers are kinda useless, so we don't need
        # to bother optimising for it.
        is_static = bool(
            fizz.base_inst.fixup.int('$connectioncount', 0) == 0
            and fizz.base_inst.fixup.bool('$start_enabled', 1)
        )

        pack_list = (
            fizz.fizz_type.pack_lists_static
            if is_static else
            fizz.fizz_type.pack_lists
        )
        for pack in pack_list:
            packing.pack_list(vmf, pack)

        if fizz_type.inst[FizzInst.BASE, is_static]:
            random.seed('{}_fizz_base_{}'.format(MAP_RAND_SEED, fizz_name))
            fizz.base_inst['file'] = random.choice(fizz_type.inst[FizzInst.BASE, is_static])

        if not fizz.emitters:
            LOGGER.warning('No emitters for fizzler "{}"!', fizz_name)
            continue

        # Brush index -> entity for ones that need to merge.
        # template_brush is used for the templated one.
        single_brushes = {}  # type: Dict[FizzlerBrush, Entity]

        if fizz_type.temp_max or fizz_type.temp_min:
            template_brush_ent = vmf.create_ent(
                classname='func_brush',
                origin=fizz.base_inst['origin'],
            )
            conditions.set_ent_keys(
                template_brush_ent,
                fizz.base_inst,
                fizz_type.temp_brush_keys,
            )
        else:
            template_brush_ent = None

        up_dir = fizz.up_axis
        forward = (fizz.emitters[0][1] - fizz.emitters[0][0]).norm()

        min_angles = FIZZ_ANGLES[forward.as_tuple(), up_dir.as_tuple()]
        max_angles = FIZZ_ANGLES[(-forward).as_tuple(), up_dir.as_tuple()]

        model_min = (
            fizz_type.inst[FizzInst.PAIR_MIN, is_static]
            or fizz_type.inst[FizzInst.ALL, is_static]
        )
        model_max = (
            fizz_type.inst[FizzInst.PAIR_MAX, is_static]
            or fizz_type.inst[FizzInst.ALL, is_static]
        )

        if not model_min or not model_max:
            raise ValueError(
                'No model specified for one side of "{}"'
                ' fizzlers'.format(fizz_type.id),
            )

        # Define a function to do the model names.
        model_index = 0
        if fizz_type.model_naming is ModelName.SAME:
            def get_model_name(ind):
                """Give every emitter the base's name."""
                return fizz_name
        elif fizz_type.model_naming is ModelName.LOCAL:
            def get_model_name(ind):
                """Give every emitter a name local to the base."""
                return fizz_name + '-' + fizz_type.model_name
        elif fizz_type.model_naming is ModelName.PAIRED:
            def get_model_name(ind):
                """Give each pair of emitters the same unique name."""
                return '{}-{}{:02}'.format(
                    fizz_name,
                    fizz_type.model_name,
                    ind,
                )
        elif fizz_type.model_naming is ModelName.UNIQUE:
            def get_model_name(ind):
                """Give every model a unique name."""
                nonlocal model_index
                model_index += 1
                return '{}-{}{:02}'.format(
                    fizz_name,
                    fizz_type.model_name,
                    model_index,
                )
        else:
            raise ValueError('Bad ModelName?')

        # Generate env_beam pairs.
        for beam in fizz_type.beams:
            beam_template = Entity(vmf)
            conditions.set_ent_keys(beam_template, fizz.base_inst, beam.keys)
            beam_template['classname'] = 'env_beam'
            del beam_template['LightningEnd']  # Don't allow users to set end pos.
            name = beam_template['targetname'] + '_'

            counter = 1
            for seg_min, seg_max in fizz.emitters:
                for offset in beam.offset:  # type: Vec
                    min_off = offset.copy()
                    max_off = offset.copy()
                    min_off.localise(seg_min, min_angles)
                    max_off.localise(seg_max, max_angles)
                    beam_ent = beam_template.copy()
                    vmf.add_ent(beam_ent)

                    # Allow randomising speed and direction.
                    if 0 < beam.speed_min  < beam.speed_max:
                        random.seed('{}{}{}'.format(MAP_RAND_SEED, min_off, max_off))
                        beam_ent['TextureScroll'] = random.randint(beam.speed_min, beam.speed_max)
                        if random.choice((False, True)):
                            # Flip to reverse direction.
                            min_off, max_off = max_off, min_off

                    beam_ent['origin'] = min_off
                    beam_ent['LightningStart'] = beam_ent['targetname'] = (
                        name + str(counter)
                    )
                    counter += 1
                    beam_ent['targetpoint'] = max_off

        # Prepare to copy over instance traits for the emitters.
        fizz_traits = instance_traits.get(fizz.base_inst).copy()
        # Special case, mark emitters that have a custom position for Clean
        # models.
        if fizz.has_cust_position:
            fizz_traits.add('cust_shape')

        mat_mod_tex = {}  # type: Dict[FizzlerBrush, Set[str]]
        for brush_type in fizz_type.brushes:
            if brush_type.mat_mod_var is not None:
                mat_mod_tex[brush_type] = set()

        # Record the data for trigger hurts so flinch triggers can match them.
        trigger_hurt_name = ''
        trigger_hurt_start_disabled = '0'

        for seg_ind, (seg_min, seg_max) in enumerate(fizz.emitters, start=1):
            length = (seg_max - seg_min).mag()
            random.seed('{}_fizz_{}'.format(MAP_RAND_SEED, seg_min))
            if length == 128 and fizz_type.inst[FizzInst.PAIR_SINGLE, is_static]:
                min_inst = vmf.create_ent(
                    targetname=get_model_name(seg_ind),
                    classname='func_instance',
                    file=random.choice(fizz_type.inst[FizzInst.PAIR_SINGLE, is_static]),
                    origin=(seg_min + seg_max)/2,
                    angles=min_angles,
                )
            else:
                # Both side models.
                min_inst = vmf.create_ent(
                    targetname=get_model_name(seg_ind),
                    classname='func_instance',
                    file=random.choice(model_min),
                    origin=seg_min,
                    angles=min_angles,
                )
                random.seed('{}_fizz_{}'.format(MAP_RAND_SEED, seg_max))
                max_inst = vmf.create_ent(
                    targetname=get_model_name(seg_ind),
                    classname='func_instance',
                    file=random.choice(model_max),
                    origin=seg_max,
                    angles=max_angles,
                )
                max_inst.fixup.update(fizz.base_inst.fixup)
                instance_traits.get(max_inst).update(fizz_traits)
            min_inst.fixup.update(fizz.base_inst.fixup)
            instance_traits.get(min_inst).update(fizz_traits)

            if fizz_type.inst[FizzInst.GRID, is_static]:
                # Generate one instance for each position.

                # Go 64 from each side, and always have at least 1 section
                # A 128 gap will have length = 0
                for ind, dist in enumerate(range(64, round(length) - 63, 128)):
                    mid_pos = seg_min + forward * dist
                    random.seed('{}_fizz_mid_{}'.format(MAP_RAND_SEED, mid_pos))
                    mid_inst = vmf.create_ent(
                        classname='func_instance',
                        targetname=fizz_name,
                        angles=min_angles,
                        file=random.choice(fizz_type.inst[FizzInst.GRID, is_static]),
                        origin=mid_pos,
                    )
                    mid_inst.fixup.update(fizz.base_inst.fixup)
                    instance_traits.get(mid_inst).update(fizz_traits)

            if template_brush_ent is not None:
                if length == 128 and fizz_type.temp_single:
                    temp = template_brush.import_template(
                        fizz_type.temp_single,
                        (seg_min + seg_max) / 2,
                        min_angles,
                        force_type=template_brush.TEMP_TYPES.world,
                        add_to_map=False,
                    )
                    template_brush_ent.solids.extend(temp.world)
                else:
                    if fizz_type.temp_min:
                        temp = template_brush.import_template(
                            fizz_type.temp_min,
                            seg_min,
                            min_angles,
                            force_type=template_brush.TEMP_TYPES.world,
                            add_to_map=False,
                        )
                        template_brush_ent.solids.extend(temp.world)
                    if fizz_type.temp_max:
                        temp = template_brush.import_template(
                            fizz_type.temp_max,
                            seg_max,
                            max_angles,
                            force_type=template_brush.TEMP_TYPES.world,
                            add_to_map=False,
                        )
                        template_brush_ent.solids.extend(temp.world)

            # Generate the brushes.
            for brush_type in fizz_type.brushes:
                brush_ent = None
                # If singular, we reuse the same brush ent for all the segments.
                if brush_type.singular:
                    brush_ent = single_brushes.get(brush_type, None)

                # Non-singular or not generated yet - make the entity.
                if brush_ent is None:
                    brush_ent = vmf.create_ent(classname='func_brush')

                    for key_name, key_value in brush_type.keys.items():
                        brush_ent[key_name] = conditions.resolve_value(fizz.base_inst, key_value)

                    for key_name, key_value in brush_type.local_keys.items():
                        brush_ent[key_name] = conditions.local_name(
                            fizz.base_inst, conditions.resolve_value(
                                fizz.base_inst, key_value,
                            )
                        )

                    brush_ent['targetname'] = conditions.local_name(
                        fizz.base_inst, brush_type.name,
                    )
                    # Set this to the center, to make sure it's not going to leak.
                    brush_ent['origin'] = (seg_min + seg_max)/2

                    # For fizzlers flat on the floor/ceiling, scanlines look
                    # useless. Turn them off.
                    if 'usescanline' in brush_ent and fizz.normal().z:
                        brush_ent['UseScanline'] = 0

                    if brush_ent['classname'] == 'trigger_hurt':
                        trigger_hurt_name = brush_ent['targetname']
                        trigger_hurt_start_disabled = brush_ent['startdisabled']

                    if brush_type.set_axis_var:
                        brush_ent['vscript_init_code'] = (
                            'axis <- `{}`;'.format(
                                fizz.normal().axis(),
                            )
                        )

                    for out in brush_type.outputs:
                        new_out = out.copy()
                        new_out.target = conditions.local_name(
                            fizz.base_inst,
                            new_out.target,
                        )
                        brush_ent.add_out(new_out)

                    if brush_type.singular:
                        # Record for the next iteration.
                        single_brushes[brush_type] = brush_ent

                # If we have a material_modify_control to generate,
                # we need to parent it to ourselves to restrict it to us
                # only. We also need one for each material, so provide a
                # function to the generator which adds to a set.
                if brush_type.mat_mod_var is not None:
                    used_tex_func = mat_mod_tex[brush_type].add
                else:
                    def used_tex_func(val):
                        """If not, ignore those calls."""
                        return None

                # Generate the brushes and texture them.
                brush_ent.solids.extend(
                    brush_type.generate(
                        vmf,
                        fizz,
                        seg_min,
                        seg_max,
                        used_tex_func,
                    )
                )

        if trigger_hurt_name:
            fizz.gen_flinch_trigs(
                vmf,
                trigger_hurt_name,
                trigger_hurt_start_disabled,
            )

        # If we have the config, but no templates used anywhere...
        if template_brush_ent is not None and not template_brush_ent.solids:
            template_brush_ent.remove()

        for brush_type, used_tex in mat_mod_tex.items():
            brush_name = conditions.local_name(fizz.base_inst, brush_type.name)
            mat_mod_name = conditions.local_name(fizz.base_inst, brush_type.mat_mod_name)
            for off, tex in zip(MATMOD_OFFSETS, sorted(used_tex)):
                pos = off.copy().rotate(*min_angles)
                pos += Vec.from_str(fizz.base_inst['origin'])
                vmf.create_ent(
                    classname='material_modify_control',
                    origin=pos,
                    targetname=mat_mod_name,
                    materialName='materials/' + tex + '.vmt',
                    materialVar=brush_type.mat_mod_var,
                    parentname=brush_name,
                )