Ejemplo n.º 1
0
    def dump_to_map(self, vmf: VMF):
        """Debug purposes: Dump the info as entities in the map.

        This makes the map effectively uncompilable...
        """
        # Icons which somewhat match the block type...
        block_icons = {
            Block.VOID: 'env_global',
            Block.SOLID: 'env_cubemap',
            Block.EMBED: 'func_instance_parms',
            Block.OCCUPIED: 'info_target',
            Block.AIR: 'info_null',

            Block.GOO_SINGLE: 'water_lod_control',
            Block.GOO_TOP: 'water_lod_control',
            Block.GOO_MID: 'water_lod_control',
            Block.GOO_BOTTOM: 'water_lod_control',

            Block.PIT_SINGLE: 'logic_autosave',
            Block.PIT_TOP: 'logic_autosave',
            Block.PIT_MID: 'logic_autosave',
            Block.PIT_BOTTOM: 'logic_autosave',
        }
        for pos, block in self.items():  # type: Vec, Block
            vmf.create_ent(
                targetname=block.name.title(),
                classname=block_icons[block],
                origin=grid_to_world(pos),
                pos=str(pos),
            )
Ejemplo n.º 2
0
def test_bbox_parse_block() -> None:
    """Test parsing of a block-shaped bbox from a VMF."""
    vmf = VMF()
    ent = vmf.create_ent(
        'bee2_collision_bbox',
        coll_deco=1,
        coll_physics=1,
        coll_grating=0,
        tags='standard excellent',
    )
    ent.solids.append(vmf.make_prism(Vec(80, 10, 40), Vec(150, 220, 70)).solid)
    ent.solids.append(vmf.make_prism(Vec(-30, 45, 80), Vec(-20, 60, 120)).solid)
    bb2, bb1 =  BBox.from_ent(ent)
    # Allow it to produce in either order.
    if bb1.min_x == -30:
        bb1, bb2 = bb2, bb1
    assert_bbox(
        bb1,
        (80, 10, 40), (150, 220, 70),
        CollideType.DECORATION | CollideType.PHYSICS,
        {'standard', 'excellent'},
    )
    assert_bbox(
        bb2,
        (-30, 45, 80), (-20, 60, 120),
        CollideType.DECORATION | CollideType.PHYSICS,
        {'standard', 'excellent'},
    )
Ejemplo n.º 3
0
def precache_model(vmf: VMF, mdl_name: str):
    """Precache the given model for switching.

    This places it as a `prop_dynamic_override`.
    """

    if not mdl_name.startswith('models/'):
        mdl_name = 'models/' + mdl_name
    if not mdl_name.endswith('.mdl'):
        mdl_name += '.mdl'

    if mdl_name in CACHED_MODELS:
        return

    CACHED_MODELS.add(mdl_name)
    vmf.create_ent(
        classname='prop_dynamic_override',
        targetname='@precache',
        origin=vbsp_options.get(Vec, 'global_ents_loc'),
        model=mdl_name,

        # Disable shadows and similar things, it shouldn't ever be in
        # PVS but we might as well.
        rendermode=10,
        disableshadowdepth=1,
        disableshadows=1,
        solid=0,
        shadowdepthnocache=2,
        spawnflags=256,  # Start with collision off.
    )
Ejemplo n.º 4
0
def make_alpha_base(vmf: VMF, bbox_min: Vec, bbox_max: Vec,
                    noise: SimplexNoise):
    """Add the base to a CutoutTile, using displacements."""
    # We want to limit the size of brushes to 512, so the vertexes don't
    # get too far apart.
    # This now contains each point from beginning to end inclusive.
    x, y, z = bbox_min

    dim_x = bbox_max.x - bbox_min.x
    dim_y = bbox_max.y - bbox_min.y

    widths = [x for x in range(0, int(dim_x), 512)] + [dim_x]
    heights = [y for y in range(0, int(dim_y), 512)] + [dim_y]

    # Loop over two offset copies, so we get a min and max each time.
    for x1, x2 in zip(widths, widths[1:]):
        for y1, y2 in zip(heights, heights[1:]):
            # We place our displacement 1 unit above the surface, then offset
            # the verts down.
            brush = vmf.make_prism(
                Vec(x + x1, y + y1, z - FLOOR_DEPTH),
                Vec(x + x2, y + y2, z - FLOOR_DEPTH - 1),
            )
            brush.top.mat = random.choice(MATS['floorbase_disp'])
            make_displacement(
                brush.top,
                offset=-1,
                noise=noise,
            )
            vmf.add_brush(brush.solid)
Ejemplo n.º 5
0
def fix_base_brush(vmf: VMF, solid: Solid, face: Side):
    """Retexture the brush forming the bottom of a pit."""
    if SETTINGS['skybox'] != '':
        face.mat = 'tools/toolsskybox'
    else:
        # We have a pit shell, we don't want a bottom.
        vmf.remove_brush(solid)
Ejemplo n.º 6
0
def make_voice_studio(vmf: VMF, loc: Vec):
    """Create the voice-line studio.

    This is either an instance (if monitors are present), or a nodraw room.
    """
    global VOICELINE_LOC

    studio_file = vbsp_options.get(str, 'voice_studio_inst')

    if ALL_MONITORS and studio_file:
        vmf.create_ent(
            classname='func_instance',
            file=studio_file,
            origin=loc,
            angles='0 0 0',
        )
        VOICELINE_LOC = loc + vbsp_options.get(Vec, 'voice_studio_cam_loc')
        return True
    else:
        # If there aren't monitors, the studio instance isn't used.
        # We need to seal anyway.
        vmf.add_brushes(vmf.make_hollow(
            loc - 256,
            loc + 256,
            thick=32,
        ))
        return False
Ejemplo n.º 7
0
    def as_ent(self, vmf: VMF) -> Entity:
        """Convert back into an entity."""
        # If a plane, then we have to produce a valid brush - subtract in the negative dir, put skip
        # on all other sides.
        norm = self.plane_normal
        if norm is not None:
            prism = vmf.make_prism(self.mins - norm, self.maxes, consts.Tools.SKIP)
            if norm == (1, 0, 0):
                prism.east.mat = consts.Tools.CLIP
            elif norm == (0, 1, 0):
                prism.north.mat = consts.Tools.CLIP
            elif norm == (0, 0, 1):
                prism.top.mat = consts.Tools.CLIP
            else:
                raise AssertionError(norm)
        else:
            prism = vmf.make_prism(self.mins, self.maxes, consts.Tools.CLIP)

        ent = vmf.create_ent(
            'bee2_collision_bbox',
            tags=' '.join(sorted(self.tags)),
            item_name=self.name,
        )
        for coll in CollideType:
            if coll is not CollideType.EVERYTHING:
                ent[f'coll_{coll.name.lower()}'] = (coll & self.contents) is not CollideType.NOTHING
        ent.solids.append(prism.solid)
        return ent
Ejemplo n.º 8
0
def res_replace_instance(vmf: VMF, inst: Entity, res: Property):
    """Replace an instance with another entity.

    `keys` and `localkeys` defines the new keyvalues used.
    `targetname` and `angles` are preset, and `origin` will be used to offset
    the given amount from the current location.
    If `keep_instance` is true, the instance entity will be kept instead of
    removed.
    """
    origin = Vec.from_str(inst['origin'])
    angles = Angle.from_str(inst['angles'])

    if not res.bool('keep_instance'):
        inst.remove()  # Do this first to free the ent ID, so the new ent has
        # the same one.

    # We copy to allow us to still access the $fixups and other values.
    new_ent = inst.copy(des_id=inst.id)
    new_ent.clear_keys()
    # Ensure there's a classname, just in case.
    new_ent['classname'] = 'info_null'

    vmf.add_ent(new_ent)

    conditions.set_ent_keys(new_ent, inst, res)

    new_ent['origin'] = Vec.from_str(new_ent['origin']) @ angles + origin
    new_ent['angles'] = angles
    new_ent['targetname'] = inst['targetname']
Ejemplo n.º 9
0
def global_input(
    vmf: VMF,
    pos: Union[Vec, str],
    output: Output,
    relay_name: str=None,
):
    """Create a global input, either from a relay or logic_auto.

    The position is used to place the relay if this is the first time.
    """
    try:
        glob_ent = GLOBAL_INPUT_ENTS[relay_name]
    except KeyError:
        if relay_name == '':
            glob_ent = GLOBAL_INPUT_ENTS[''] = vmf.create_ent(
                classname='logic_auto',
                spawnflags='1',  # Remove on fire
                origin=pos,
            )
        else:
            glob_ent = GLOBAL_INPUT_ENTS[relay_name] = vmf.create_ent(
                classname='logic_relay',
                targetname=relay_name,
                origin=pos,
            )
    if not relay_name:
        output.output = 'OnMapSpawn'
        output.only_once = True
    output.comma_sep = False
    glob_ent.add_out(output)
Ejemplo n.º 10
0
def global_input(
    vmf: VMF,
    pos: Union[Vec, str],
    output: Output,
    relay_name: str = None,
) -> None:
    """Create a global input, either from a relay or logic_auto.

    If the name is empty, a logic_auto is created.
    The position is used to place the entity if this is the first time.
    """
    try:
        glob_ent = GLOBAL_INPUT_ENTS[relay_name]
    except KeyError:
        if not relay_name:
            glob_ent = GLOBAL_INPUT_ENTS[''] = vmf.create_ent(
                classname='logic_auto',
                spawnflags='1',  # Remove on fire
                origin=options.get(Vec, 'global_ents_loc'),
            )
        else:
            glob_ent = GLOBAL_INPUT_ENTS[relay_name] = vmf.create_ent(
                classname='logic_relay',
                targetname=relay_name,
                origin=pos,
            )
    if not relay_name:
        output.output = 'OnMapSpawn'
        output.only_once = True
    output.comma_sep = False
    glob_ent.add_out(output)
Ejemplo n.º 11
0
def make_corner(
    vmf: VMF,
    origin: Vec,
    start_dir: Vec,
    end_dir: Vec,
    size: int,
    config: Config,
) -> None:
    angles = Matrix.from_basis(z=start_dir, x=end_dir).to_angle()
    vmf.create_ent(
        classname='func_instance',
        origin=origin,
        angles=angles,
        file=config.inst_corner[size],
    )

    temp = config.temp_corner[size]
    if temp:
        temp_solids = template_brush.import_template(
            vmf,
            temp,
            origin=origin,
            angles=angles,
            force_type=template_brush.TEMP_TYPES.world,
            add_to_map=False,
        ).world
        motion_trigger(vmf, *temp_solids)
Ejemplo n.º 12
0
def make_voice_studio(vmf: VMF) -> bool:
    """Create the voice-line studio.

    This is either an instance (if monitors are present), or a nodraw room.
    """

    studio_file = options.get(str, 'voice_studio_inst')
    loc = voice_line.get_studio_loc()

    if HAS_MONITOR and studio_file:
        conditions.add_inst(
            vmf,
            file=studio_file,
            origin=loc,
        )
        return True
    else:
        # If there aren't monitors, the studio instance isn't used.
        # We need to seal anyway.
        vmf.add_brushes(vmf.make_hollow(
            loc - 256,
            loc + 256,
            thick=32,
        ))
        return False
Ejemplo n.º 13
0
def make_voice_studio(vmf: VMF) -> bool:
    """Create the voice-line studio.

    This is either an instance (if monitors are present), or a nodraw room.
    """

    studio_file = vbsp_options.get(str, 'voice_studio_inst')
    loc = voiceLine.get_studio_loc()

    if ALL_MONITORS and studio_file:
        vmf.create_ent(
            classname='func_instance',
            file=studio_file,
            origin=loc,
            angles='0 0 0',
        )
        return True
    else:
        # If there aren't monitors, the studio instance isn't used.
        # We need to seal anyway.
        vmf.add_brushes(vmf.make_hollow(
            loc - 256,
            loc + 256,
            thick=32,
        ))
        return False
Ejemplo n.º 14
0
def fix_base_brush(vmf: VMF, solid: Solid, face: Side):
    """Retexture the brush forming the bottom of a pit."""
    if SETTINGS['skybox'] != '':
        face.mat = 'tools/toolsskybox'
        vbsp.IGNORED_FACES.add(face)
    else:
        # We have a pit shell, we don't want a bottom.
        vmf.remove_brush(solid)
Ejemplo n.º 15
0
def flag_brush_at_loc(vmf: VMF, inst: Entity, flag: Property):
    """Checks to see if a wall is present at the given location.

    - `Pos` is the position of the brush, where `0 0 0` is the floor-position
       of the brush.
    - `Dir` is the normal the face is pointing. `(0 0 -1)` is up.
    - `Type` defines the type the brush must be:
      - `Any` requires either a black or white brush.
      - `None` means that no brush must be present.
      - `White` requires a portalable surface.
      - `Black` requires a non-portalable surface.
    - `SetVar` defines an instvar which will be given a value of `black`,
      `white` or `none` to allow the result to be reused.
    - If `gridPos` is true, the position will be snapped so it aligns with
      the 128 grid (Useful with fizzler/light strip items).
    - `RemoveBrush`: If set to `1`, the brush will be removed if found.
      Only do this to `EmbedFace` brushes, since it will remove the other
      sides as well.
    """
    pos = Vec.from_str(flag['pos', '0 0 0'])
    pos.z -= 64  # Subtract so origin is the floor-position
    pos = pos.rotate_by_str(inst['angles', '0 0 0'])

    # Relative to the instance origin
    pos += Vec.from_str(inst['origin', '0 0 0'])

    norm = flag['dir', None]
    if norm is not None:
        norm = Vec.from_str(norm).rotate_by_str(inst['angles', '0 0 0'], )

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

    result_var = flag['setVar', '']
    should_remove = srctools.conv_bool(flag['RemoveBrush', False], False)
    des_type = flag['type', 'any'].casefold()

    brush = SOLIDS.get(pos.as_tuple(), None)

    if brush is None or (norm is not None and abs(brush.normal) != abs(norm)):
        br_type = 'none'
    else:
        br_type = str(brush.color)
        if should_remove:
            vmf.remove_brush(brush.solid, )

    if result_var:
        inst.fixup[result_var] = br_type

    if des_type == 'any' and br_type != 'none':
        return True

    return des_type == br_type
Ejemplo n.º 16
0
def make_static_pist(vmf: srctools.VMF, ent: Entity, res: Property):
    """Convert a regular piston into a static version.

    This is done to save entities and improve lighting.
    If changed to static pistons, the $bottom and $top level become equal.
    Instances:
        Bottom_1/2/3: Moving piston with the given $bottom_level
        Logic_0/1/2/3: Additional logic instance for the given $bottom_level
        Static_0/1/2/3/4: A static piston at the given height.
    Alternatively, specify all instances via editoritems, by setting the value
    to the item ID optionally followed by a :prefix.
    """

    bottom_pos = ent.fixup.int(consts.FixupVars.PIST_BTM, 0)

    if (ent.fixup.int(consts.FixupVars.CONN_COUNT) > 0
            or ent.fixup.bool(consts.FixupVars.DIS_AUTO_DROP)):  # can it move?
        ent.fixup[consts.FixupVars.BEE_PIST_IS_STATIC] = True

        # Use instances based on the height of the bottom position.
        val = res.value['bottom_' + str(bottom_pos)]
        if val:  # Only if defined
            ent['file'] = val

        logic_file = res.value['logic_' + str(bottom_pos)]
        if logic_file:
            # Overlay an additional logic file on top of the original
            # piston. This allows easily splitting the piston logic
            # from the styled components
            logic_ent = ent.copy()
            logic_ent['file'] = logic_file
            vmf.add_ent(logic_ent)
            # If no connections are present, set the 'enable' value in
            # the logic to True so the piston can function
            logic_ent.fixup[consts.FixupVars.BEE_PIST_MANAGER_A] = (
                ent.fixup.int(consts.FixupVars.CONN_COUNT) == 0)
    else:  # we are static
        ent.fixup[consts.FixupVars.BEE_PIST_IS_STATIC] = False
        if ent.fixup.bool(consts.FixupVars.PIST_IS_UP):
            pos = bottom_pos = ent.fixup.int(consts.FixupVars.PIST_TOP, 1)
        else:
            pos = bottom_pos
        ent.fixup[consts.FixupVars.PIST_TOP] = ent.fixup[
            consts.FixupVars.PIST_BTM] = pos

        val = res.value['static_' + str(pos)]
        if val:
            ent['file'] = val

    # Add in the grating for the bottom as an overlay.
    # It's low to fit the piston at minimum, or higher if needed.
    grate = res.value['grate_high' if bottom_pos > 0 else 'grate_low']
    if grate:
        grate_ent = ent.copy()
        grate_ent['file'] = grate
        vmf.add_ent(grate_ent)
Ejemplo n.º 17
0
def make_static_pist(vmf: srctools.VMF, ent: Entity, res: Property):
    """Convert a regular piston into a static version.

    This is done to save entities and improve lighting.
    If changed to static pistons, the $bottom and $top level become equal.
    Instances:
        Bottom_1/2/3: Moving piston with the given $bottom_level
        Logic_0/1/2/3: Additional logic instance for the given $bottom_level
        Static_0/1/2/3/4: A static piston at the given height.
    Alternatively, specify all instances via editoritems, by setting the value
    to the item ID optionally followed by a :prefix.
    """

    bottom_pos = ent.fixup.int('bottom_level', 0)

    if (ent.fixup['connectioncount', '0'] != "0"
            or ent.fixup['disable_autodrop', '0'] != "0"):  # can it move?
        ent.fixup['$is_static'] = True

        # Use instances based on the height of the bottom position.
        val = res.value['bottom_' + str(bottom_pos)]
        if val:  # Only if defined
            ent['file'] = val

        logic_file = res.value['logic_' + str(bottom_pos)]
        if logic_file:
            # Overlay an additional logic file on top of the original
            # piston. This allows easily splitting the piston logic
            # from the styled components
            logic_ent = ent.copy()
            logic_ent['file'] = logic_file
            vmf.add_ent(logic_ent)
            # If no connections are present, set the 'enable' value in
            # the logic to True so the piston can function
            logic_ent.fixup['manager_a'] = srctools.bool_as_int(
                ent.fixup['connectioncount', '0'] == '0')
    else:  # we are static
        ent.fixup['$is_static'] = False
        if ent.fixup.bool('start_up'):
            pos = bottom_pos = ent.fixup.int('top_level', 1)
        else:
            pos = bottom_pos
        ent.fixup['top_level'] = ent.fixup['bottom_level'] = pos

        val = res.value['static_' + str(pos)]
        if val:
            ent['file'] = val

    # Add in the grating for the bottom as an overlay.
    # It's low to fit the piston at minimum, or higher if needed.
    grate = res.value['grate_high' if bottom_pos > 0 else 'grate_low']
    if grate:
        grate_ent = ent.copy()
        grate_ent['file'] = grate
        vmf.add_ent(grate_ent)
Ejemplo n.º 18
0
def test_bbox_parse_plane(axis: str, mins: tuple3, maxes: tuple3) -> None:
    """Test parsing planar bboxes from a VMF.

    With 5 skip sides, the brush is flattened into the remaining plane.
    """
    vmf = VMF()
    ent = vmf.create_ent('bee2_collision_bbox', coll_solid=1)
    prism = vmf.make_prism(Vec(80, 10, 40), Vec(150, 220, 70), mat='tools/toolsskip')
    getattr(prism, axis).mat = 'tools/toolsclip'
    ent.solids.append(prism.solid)
    [bbox] = BBox.from_ent(ent)
    assert_bbox(bbox, mins, maxes, CollideType.SOLID, set())
Ejemplo n.º 19
0
def do_item_optimisation(vmf: VMF):
    """Optimise redundant logic items."""
    needs_global_toggle = False

    for item in list(ITEMS.values()):
        # We can't remove items that have functionality, or don't have IO.
        if item.item_type is None or not item.item_type.input_type.is_logic:
            continue

        prim_inverted = conv_bool(
            conditions.resolve_value(
                item.inst,
                item.item_type.invert_var,
            ))

        sec_inverted = conv_bool(
            conditions.resolve_value(
                item.inst,
                item.item_type.sec_invert_var,
            ))

        # Don't optimise if inverted.
        if prim_inverted or sec_inverted:
            continue
        inp_count = len(item.inputs)
        if inp_count == 0:
            # Totally useless, remove.
            # We just leave the panel entities, and tie all the antlines
            # to the same toggle.
            needs_global_toggle = True
            for ent in item.antlines:
                ent['targetname'] = '_static_ind'

            del ITEMS[item.name]
            item.inst.remove()
        elif inp_count == 1:
            # Only one input, so AND or OR are useless.
            # Transfer input item to point to the output(s).
            collapse_item(item)

    # The antlines need a toggle entity, otherwise they'll copy random other
    # overlays.
    if needs_global_toggle:
        vmf.create_ent(
            classname='env_texturetoggle',
            origin=vbsp_options.get(Vec, 'global_ents_loc'),
            targetname='_static_ind_tog',
            target='_static_ind',
        )
Ejemplo n.º 20
0
def add_bullseye(vmf: VMF, quote_loc: Vec, name: str) -> None:
    """Add a bullseye to the map."""
    # Cave's voice lines require a special named bullseye to
    # work correctly.
    # Don't add the same one more than once.
    if name not in ADDED_BULLSEYES:
        vmf.create_ent(
            classname='npc_bullseye',
            # Not solid, Take No Damage, Think outside PVS
            spawnflags='222224',
            targetname=name,
            origin=quote_loc - (0, 0, 16),
            angles='0 0 0',
        )
        ADDED_BULLSEYES.add(name)
Ejemplo n.º 21
0
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property):
    """Create the spawn point for ATLAS in the entry corridor.

    It produces either an instance or the normal spawn entity. This is required since ATLAS may need to have the paint gun logic.
    The two parameters `origin` and `facing` must be set to determine the required position.
    If `global` is set, the spawn point will be absolute instead of relative to the current instance.
    """
    if vbsp.GAME_MODE != 'COOP':
        return RES_EXHAUSTED

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

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

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

    angles = normal.to_angle()

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

    It produces either an instance or the normal spawn entity. This is required since ATLAS may need to have the paint gun logic.
    The two parameters `origin` and `facing` must be set to determine the required position.
    If `global` is set, the spawn point will be absolute instead of relative to the current instance.
    """
    if vbsp.GAME_MODE != 'COOP':
        return RES_EXHAUSTED

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

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

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

    angles = normal.to_angle()

    if is_tag:
        vmf.create_ent(
            classname='func_instance',
            targetname='paint_gun',
            origin=origin - (0, 0, 16),
            angles=angles,
            # Generated by the BEE2 app.
            file='instances/bee2/tag_coop_gun.vmf',
        )
        # Blocks ATLAS from having a gun
        vmf.create_ent(
            classname='info_target',
            targetname='supress_blue_portalgun_spawn',
            origin=origin,
            angles='0 0 0',
        )
        # Allows info_target to work
        vmf.create_ent(
            classname='env_global',
            targetname='no_spawns',
            globalstate='portalgun_nospawn',
            initialstate=1,
            spawnflags=1,  # Use initial state
            origin=origin,
        )
    vmf.create_ent(
        classname='info_coop_spawn',
        targetname='@coop_spawn_blue',
        ForceGunOnSpawn=int(not is_tag),
        origin=origin,
        angles=angles,
        enabled=1,
        StartingTeam=3,  # ATLAS
    )
    return RES_EXHAUSTED
Ejemplo n.º 23
0
def precache_model(vmf: VMF, mdl_name: str,
                   skinset: Collection[int] = ()) -> None:
    """Precache the given model for switching.

    This places it as a `comp_precache_model`.
    """
    mdl_name = mdl_name.casefold().replace('\\', '/')
    if not mdl_name.startswith('models/'):
        mdl_name = 'models/' + mdl_name
    if not mdl_name.endswith('.mdl'):
        mdl_name += '.mdl'
    if mdl_name in CACHED_MODELS:
        return

    try:
        skins, ent = CACHED_MODELS[mdl_name]
    except KeyError:
        ent = vmf.create_ent(
            classname='comp_precache_model',
            origin=options.get(Vec, 'global_ents_loc'),
            model=mdl_name,
        )
        skins = set(skinset)
        CACHED_MODELS[mdl_name] = skins, ent
    else:
        if skins:  # If empty, it's wildcard so ignore specifics.
            if len(skinset) == 0:
                skins.clear()
            else:
                skins.update(skinset)
    if skins:
        ent['skinset'] = ' '.join(map(str, sorted(skinset)))
    else:
        ent['skinset'] = ''
Ejemplo n.º 24
0
def get_intersect_testcases() -> list:
    """Use a VMF to make it easier to generate the bounding boxes."""
    with Path(__file__, '../bbox_samples.vmf').open() as f:
        vmf = VMF.parse(Property.parse(f))

    def process(brush: Solid | None) -> tuple[tuple[int, ...], tuple[int, ...]] | None:
        """Extract the bounding box from the brush."""
        if brush is None:
            return None
        bb_min, bb_max = brush.get_bbox()
        for vec in [bb_min, bb_max]:
            for ax in 'xyz':
                # If one thick, make zero thick so we can test planes.
                if abs(vec[ax]) == 63:
                    vec[ax] = math.copysign(64, vec[ax])
        return (tuple(map(int, bb_min)), tuple(map(int, bb_max)))

    for ent in vmf.entities:
        test = expected = None
        for solid in ent.solids:
            if solid.sides[0].mat.casefold() == 'tools/toolsskip':
                expected = solid
            if solid.sides[0].mat.casefold() == 'tools/toolstrigger':
                test = solid
        if test is None:
            raise ValueError(ent.id)
        yield (*process(test), process(expected))
Ejemplo n.º 25
0
def motion_trigger(vmf: VMF, *solids: Solid) -> None:
    """Create the anti-gravity trigger, and force crouching."""
    motion_trig = vmf.create_ent(
        classname='trigger_vphysics_motion',
        SetGravityScale='0.0',
        origin=solids[0].get_origin(),
        spawnflags='1103',  # Clients, Physics, Everything
    )
    duck_trig = vmf.create_ent(
        classname='trigger_playermovement',
        origin=motion_trig['origin'],
        spawnflags=1 + 2048,  # Clients, Auto-duck while in trigger.
    )
    for solid in solids:
        motion_trig.solids.append(solid.copy())
        duck_trig.solids.append(solid.copy())
Ejemplo n.º 26
0
def res_global_input(vmf: VMF, inst: Entity, res: Property) -> None:
    """Trigger an input either on map spawn, or when a relay is triggered.

    Arguments:

    - `Input`: the input to use, either a name or an `instance:` command.
    - `Target`: If set, a local name to send commands to. Otherwise, the instance itself.
    - `Delay`: Number of seconds to delay the input.
    - `Name`: If set the name of the `logic_relay` which must be triggered.
        If not set the output will fire `OnMapSpawn`.
    - `Output`: The name of the output, defaulting to `OnTrigger`. Ignored
        if Name is not set.
    - `Param`: The parameter for the output.
    - `AlsoOnLoad`: If output is firing on map spawn, also fire it on save load too.

    Alternatively pass a string VMF-style output, which only provides
    OnMapSpawn functionality.
    """
    relay_name, out = res.value

    output: Output = out.copy()

    if output.target:
        output.target = conditions.local_name(
            inst,
            inst.fixup.substitute(output.target),
        )
    else:
        output.target = inst['targetname']

    if relay_name is ON_LOAD:
        relay_name = ''
        on_load = True
    else:
        relay_name = inst.fixup.substitute(relay_name)
        on_load = False

    output.output = inst.fixup.substitute(output.output)
    output.params = inst.fixup.substitute(output.params)
    if output.inst_in is not None:
        output.inst_in = inst.fixup.substitute(output.inst_in)
    if output.inst_out is not None:
        output.input = inst.fixup.substitute(output.input)

    if on_load:
        try:
            ent = GLOBAL_INPUT_ENTS[ON_LOAD]
        except KeyError:
            ent = GLOBAL_INPUT_ENTS[ON_LOAD] = vmf.create_ent(
                'logic_auto',
                origin=options.get(Vec, 'global_ents_loc'),
                spawnflags='0',  # Don't remove on fire.
            )
        load_out = output.copy()
        output.output = 'OnMapSpawn'
        load_out.output = 'OnLoadGame'
        output.only_once = True
        ent.add_out(output, load_out)
    else:
        global_input(vmf, inst['origin'], output, relay_name)
Ejemplo n.º 27
0
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property):
    """Create the spawn point for ATLAS, in Aperture Tag.

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

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

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

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

    angles = normal.to_angle()

    if is_tag:
        vmf.create_ent(
            classname='func_instance',
            targetname='paint_gun',
            origin=origin - (0, 0, 16),
            angles=angles,
            # Generated by the BEE2 app.
            file='instances/bee2/tag_coop_gun.vmf',
        )
        # Blocks ATLAS from having a gun
        vmf.create_ent(
            classname='info_target',
            targetname='supress_blue_portalgun_spawn',
            origin=origin,
            angles='0 0 0',
        )
        # Allows info_target to work
        vmf.create_ent(
            classname='env_global',
            targetname='no_spawns',
            globalstate='portalgun_nospawn',
            initialstate=1,
            spawnflags=1,  # Use initial state
            origin=origin,
        )
    vmf.create_ent(
        classname='info_coop_spawn',
        targetname='@coop_spawn_blue',
        ForceGunOnSpawn=int(not is_tag),
        origin=origin,
        angles=angles,
        enabled=1,
        StartingTeam=3,  # ATLAS
    )
    return RES_EXHAUSTED
Ejemplo n.º 28
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.º 29
0
def res_sendificator(vmf: VMF, inst: Entity):
    """Implement Sendificators."""
    # For our version, we know which sendtor connects to what laser,
    # so we can couple the logic together (avoiding @sendtor_mutex).

    sendtor_name = inst['targetname']
    sendtor = connections.ITEMS[sendtor_name]

    sendtor.enable_cmd += (Output(
        '',
        '@{}_las_relay_*'.format(sendtor_name),
        'Trigger',
        delay=0.01,
    ), )

    for ind, conn in enumerate(list(sendtor.outputs), start=1):
        las_item = conn.to_item
        conn.remove()
        try:
            targ_offset, targ_normal = SENDTOR_TARGETS[las_item.name]
        except KeyError:
            LOGGER.warning('"{}" is not a Sendificator target!', las_item.name)
            continue

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

        targ_offset = targ_offset.copy()
        targ_normal = targ_normal.copy().rotate(*angles)

        targ_offset.localise(
            Vec.from_str(las_item.inst['origin']),
            angles,
        )

        relay_name = '@{}_las_relay_{}'.format(sendtor_name, ind)

        relay = vmf.create_ent(
            'logic_relay',
            targetname=relay_name,
            origin=targ_offset,
            angles=targ_normal.to_angle(),
        )
        relay.add_out(
            Output('OnTrigger', '!self', 'RunScriptCode',
                   '::sendtor_source <- self;'),
            Output('OnTrigger', '@sendtor_fire', 'Trigger'),
        )
        if not las_item.inputs:
            # No other inputs, make it on always. PeTI automatically turns
            # it off when inputs are connected, which is annoying.
            las_item.inst.fixup['$start_enabled'] = '1'
            is_on = True
        else:
            is_on = las_item.inst.fixup.bool('$start_enabled')

        relay['StartDisabled'] = not is_on
        las_item.enable_cmd += (Output('', relay_name, 'Enable'), )
        las_item.disable_cmd += (Output('', relay_name, 'Disable'), )
        LOGGER.info('Relay: {}', relay)
Ejemplo n.º 30
0
def make_tag_coop_inst(tag_loc: str):
    """Make the coop version of the tag instances.

    This needs to be shrunk, so all the logic entities are not spread
    out so much (coop tubes are small).

    This way we avoid distributing the logic.
    """
    global TAG_COOP_INST_VMF
    TAG_COOP_INST_VMF = vmf = VMF.parse(
        os.path.join(tag_loc, TAG_GUN_COOP_INST)
    )

    def logic_pos():
        """Put the entities in a nice circle..."""
        while True:
            for ang in range(0, 44):
                ang *= 360/44
                yield Vec(16*math.sin(ang), 16*math.cos(ang), 32)
    pos = logic_pos()
    # Move all entities that don't care about position to the base of the player
    for ent in TAG_COOP_INST_VMF.iter_ents():
        if ent['classname'] == 'info_coop_spawn':
            # Remove the original spawn point from the instance.
            # That way it can overlay over other dropper instances.
            ent.remove()
        elif ent['classname'] in ('info_target', 'info_paint_sprayer'):
            pass
        else:
            ent['origin'] = next(pos)

            # These originally use the coop spawn point, but this doesn't
            # always work. Switch to the name of the player, which is much
            # more reliable.
            if ent['classname'] == 'logic_measure_movement':
                ent['measuretarget'] = '!player_blue'

    # Add in a trigger to start the gel gun, and reset the activated
    # gel whenever the player spawns.
    trig_brush = vmf.make_prism(
        Vec(-32, -32, 0),
        Vec(32, 32, 16),
        mat='tools/toolstrigger',
    ).solid
    start_trig = vmf.create_ent(
        classname='trigger_playerteam',
        target_team=3,  # ATLAS
        spawnflags=1,  # Clients only
        origin='0 0 8',
    )
    start_trig.solids = [trig_brush]
    start_trig.add_out(
        # This uses the !activator as the target player so it must be via trigger.
        Output('OnStartTouchBluePlayer', '@gel_ui', 'Activate', delay=0, only_once=True),
        # Reset the gun to fire nothing.
        Output('OnStartTouchBluePlayer', '@blueisenabled', 'SetValue', 0, delay=0.1),
        Output('OnStartTouchBluePlayer', '@orangeisenabled', 'SetValue', 0, delay=0.1),
    )
Ejemplo n.º 31
0
def res_sendificator(vmf: VMF, inst: Entity):
    """Implement Sendificators."""
    # For our version, we know which sendtor connects to what laser,
    # so we can couple the logic together (avoiding @sendtor_mutex).

    sendtor_name = inst['targetname']
    sendtor = connections.ITEMS[sendtor_name]

    sendtor.enable_cmd += (Output(
        '',
        '@{}_las_relay_*'.format(sendtor_name),
        'Trigger',
        delay=0.01,
    ), )

    for ind, conn in enumerate(list(sendtor.outputs), start=1):
        las_item = conn.to_item
        conn.remove()
        try:
            targ_offset, targ_normal = SENDTOR_TARGETS[las_item.name]
        except KeyError:
            LOGGER.warning('"{}" is not a Sendificator target!', las_item.name)
            continue

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

        targ_offset = targ_offset.copy()
        targ_normal = targ_normal.copy().rotate(*angles)

        targ_offset.localise(
            Vec.from_str(las_item.inst['origin']),
            angles,
        )

        relay_name = '@{}_las_relay_{}'.format(sendtor_name, ind)

        relay = vmf.create_ent(
            'logic_relay',
            targetname=relay_name,
            origin=targ_offset,
            angles=targ_normal.to_angle(),
        )
        relay.add_out(
            Output('OnTrigger', '!self', 'RunScriptCode', '::sendtor_source <- self;'),
            Output('OnTrigger', '@sendtor_fire', 'Trigger'),
        )
        if not las_item.inputs:
            # No other inputs, make it on always. PeTI automatically turns
            # it off when inputs are connected, which is annoying.
            las_item.inst.fixup['$start_enabled'] = '1'
            is_on = True
        else:
            is_on = las_item.inst.fixup.bool('$start_enabled')

        relay['StartDisabled'] = not is_on
        las_item.enable_cmd += (Output('', relay_name, 'Enable'),)
        las_item.disable_cmd += (Output('', relay_name, 'Disable'),)
        LOGGER.info('Relay: {}', relay)
Ejemplo n.º 32
0
def clean_vmf(vmf_path):
    """Optimise the VMFs, removing unneeded entities or objects."""
    inst = VMF.parse(vmf_path)

    for ent in itertools.chain([inst.spawn], inst.entities[:]):  # type: Entity
        # Remove comments
        ent.comments = ''

        # Remove entities that have their visgroups hidden.
        if ent.hidden or not ent.vis_shown:
            print('Removing hidden ent')
            inst.remove_ent(ent)
            continue

        # Remove hammer_notes entities
        if ent['classname'] == 'hammer_notes':
            print('Removing hammer_notes...')
            inst.remove_ent(ent)
            continue
            
        # All instances must be in bee2/, so any reference outside there is a map error!
        # It's ok if it's in p2editor and not in a subfolder though.
        # There's also an exception needed for the Tag gun instance.
        if ent['classname'] == 'func_instance':
            inst_loc = ent['file'].casefold().replace('\\','/')
            if not inst_loc.startswith('instances/bee2/') and not (inst_loc.startswith('instances/p2editor/') and inst_loc.count('/') == 2) and 'alatag' not in inst_loc:
                input('Invalid instance path "{}" in\n"{}"! Press Enter to continue..'.format(ent['file'], vmf_path))
                yield from clean_vmf(vmf_path) # Re-run so we check again..

        for solid in ent.solids[:]:
            if all(face.mat.casefold() == 'tools/toolsskip' for face in solid):
                print('Removing SKIP brush')
                ent.solids.remove(solid)
                continue

            if solid.hidden or not solid.vis_shown:
                print('Removing hidden brush')
                ent.solids.remove(solid)
                continue

    for detail in inst.by_class['func_detail']:
        # Remove several unused default options from func_detail.
        # We're not on xbox!
        del detail['disableX360']
        # These aren't used in any instances, and it doesn't seem as if
        # VBSP preserves these values anyway.
        del detail['maxcpulevel'], detail['mincpulevel']
        del detail['maxgpulevel'], detail['mingpulevel']

    # Since all VMFs are instances or similar (not complete maps), we'll never
    # use worldspawn's settings. Keep mapversion though.
    del inst.spawn['maxblobcount'], inst.spawn['maxpropscreenwidth']
    del inst.spawn['maxblobcount'],
    del inst.spawn['detailvbsp'], inst.spawn['detailmaterial']

    lines = inst.export(inc_version=False, minimal=True).splitlines()
    for line in lines:
        yield line.lstrip()
Ejemplo n.º 33
0
def clean_vmf(vmf_path):
    """Optimise the VMFs, removing unneeded entities or objects."""
    inst = VMF.parse(vmf_path)

    for ent in itertools.chain([inst.spawn], inst.entities[:]):  # type: Entity
        # Remove comments
        ent.comments = ''

        # Remove entities that have their visgroups hidden.
        if ent.hidden or not ent.vis_shown:
            print('Removing hidden ent')
            inst.remove_ent(ent)
            continue

        # Remove info_null entities
        if ent['classname'] == 'info_null':
            print('Removing info_null...')
            inst.remove_ent(ent)
            continue
            
        # All instances must be in bee2/, so any reference outside there is a map error!
        # It's ok if it's in p2editor and not in a subfolder though.
        # There's also an exception needed for the Tag gun instance.
        if ent['classname'] == 'func_instance':
            inst_loc = ent['file'].casefold().replace('\\','/')
            if not inst_loc.startswith('instances/bee2/') and not (inst_loc.startswith('instances/p2editor/') and inst_loc.count('/') == 2) and 'alatag' not in inst_loc:
                input('Invalid instance path "{}" in\n"{}"! Press Enter to continue..'.format(ent['file'], vmf_path))
                yield from clean_vmf(vmf_path) # Re-run so we check again..

        for solid in ent.solids[:]:
            if all(face.mat.casefold() == 'tools/toolsskip' for face in solid):
                print('Removing SKIP brush')
                ent.solids.remove(solid)
                continue

            if solid.hidden or not solid.vis_shown:
                print('Removing hidden brush')
                ent.solids.remove(solid)
                continue

    for detail in inst.by_class['func_detail']:
        # Remove several unused default options from func_detail.
        # We're not on xbox!
        del detail['disableX360']
        # These aren't used in any instances, and it doesn't seem as if
        # VBSP preserves these values anyway.
        del detail['maxcpulevel'], detail['mincpulevel']
        del detail['maxgpulevel'], detail['mingpulevel']

    # Since all VMFs are instances or similar (not complete maps), we'll never
    # use worldspawn's settings. Keep mapversion though.
    del inst.spawn['maxblobcount'], inst.spawn['maxpropscreenwidth']
    del inst.spawn['maxblobcount'],
    del inst.spawn['detailvbsp'], inst.spawn['detailmaterial']

    lines = inst.export(inc_version=False, minimal=True).splitlines()
    for line in lines:
        yield line.lstrip()
Ejemplo n.º 34
0
def vactube_gen(vmf: VMF) -> None:
    """Generate the vactubes, after most conditions have run."""
    if not VAC_TRACKS:
        return
    LOGGER.info('Generating vactubes...')
    for start, all_markers in VAC_TRACKS:
        start_normal = start.orient.forward()

        # First create the start section..
        start_logic = start.ent.copy()
        vmf.add_ent(start_logic)

        if start_normal.z > 0:
            start_logic['file'] = fname = start.conf.inst_entry_ceil
        elif start_normal.z < 0:
            start_logic['file'] = fname = start.conf.inst_entry_floor
        else:
            start_logic['file'] = fname = start.conf.inst_entry_wall
        conditions.ALL_INST.add(fname.casefold())

        end = start

        for inst, end in start.follow_path(all_markers):
            join_markers(vmf, inst, end, inst is start)

        end_loc = Vec.from_str(end.ent['origin'])
        end_norm = end.orient.forward()

        # join_markers creates straight parts up-to the marker, but not at it's
        # location - create the last one.
        make_straight(
            vmf,
            end_loc,
            end_norm,
            128,
            end.conf,
        )

        # If the end is placed in goo, don't add logic - it isn't visible, and
        # the object is on a one-way trip anyway.
        if not (BLOCK_POS['world':end_loc].is_goo and end_norm.z < -1e-6):
            end_logic = end.ent.copy()
            vmf.add_ent(end_logic)
            end_logic['file'] = end.conf.inst_exit
            conditions.ALL_INST.add(end.conf.inst_exit.casefold())
Ejemplo n.º 35
0
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property):
    """Create the spawn point for ATLAS, in Aperture Tag.

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

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

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

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

    if is_tag:
        vmf.create_ent(
            classname='func_instance',
            targetname='paint_gun',
            origin=origin - (0, 0, 16),
            angles=angles,
            # Generated by the BEE2 app.
            file='instances/bee2/tag_coop_gun.vmf',
        )
        # Blocks ATLAS from having a gun
        vmf.create_ent(
            classname='info_target',
            targetname='supress_blue_portalgun_spawn',
            origin=origin,
            angles='0 0 0',
        )
        # Allows info_target to work
        vmf.create_ent(
            classname='env_global',
            targetname='no_spawns',
            globalstate='portalgun_nospawn',
            initialstate=1,
            spawnflags=1,  # Use initial state
            origin=origin,
        )
    vmf.create_ent(
        classname='info_coop_spawn',
        targetname='@coop_spawn_blue',
        ForceGunOnSpawn=int(not is_tag),
        origin=origin,
        angles=angles,
        enabled=1,
        StartingTeam=3,  # ATLAS
    )
    return RES_EXHAUSTED
Ejemplo n.º 36
0
def _make_squarebeam(vmf: VMF, origin: Vec, skin='0', size=''):
    """Make a squarebeam prop at the given location."""
    return vmf.create_ent(
        classname='prop_static',
        angles='0 0 0',
        origin=origin,
        model='models/anim_wp/framework/squarebeam_off' + size + '.mdl',
        skin=skin,
        disableshadows='1',
    )
Ejemplo n.º 37
0
def _make_squarebeam(vmf: VMF, origin: Vec, skin='0', size=''):
    """Make a squarebeam prop at the given location."""
    return vmf.create_ent(
        classname='prop_static',
        angles='0 0 0',
        origin=origin,
        model='models/anim_wp/framework/squarebeam_off' + size + '.mdl',
        skin=skin,
        disableshadows='1',
    )
Ejemplo n.º 38
0
def precache_model(vmf: VMF, mdl_name: str):
    """Precache the given model for switching.

    This places it as a `prop_dynamic_override`.
    """

    if not mdl_name.startswith('models/'):
        mdl_name = 'models/' + mdl_name
    if not mdl_name.endswith('.mdl'):
        mdl_name += '.mdl'

    if mdl_name in CACHED_MODELS:
        return

    CACHED_MODELS.add(mdl_name)
    vmf.create_ent(
        classname='comp_precache_model',
        origin=vbsp_options.get(Vec, 'global_ents_loc'),
        model=mdl_name,
    )
Ejemplo n.º 39
0
def precache_model(vmf: VMF, mdl_name: str):
    """Precache the given model for switching.

    This places it as a `prop_dynamic_override`.
    """

    if not mdl_name.startswith('models/'):
        mdl_name = 'models/' + mdl_name
    if not mdl_name.endswith('.mdl'):
        mdl_name += '.mdl'

    if mdl_name in CACHED_MODELS:
        return

    CACHED_MODELS.add(mdl_name)
    vmf.create_ent(
        classname='comp_precache_model',
        origin=vbsp_options.get(Vec, 'global_ents_loc'),
        model=mdl_name,
    )
Ejemplo n.º 40
0
 def dump(self, vmf: VMF, vis_name: str = 'Collisions') -> None:
     """Dump all the bounding boxes as a set of brushes."""
     visgroup = vmf.create_visgroup(vis_name)
     for name, bb_list in self._by_name.items():
         group = EntityGroup(vmf, shown=False)
         for bbox in bb_list:
             ent = bbox.as_ent(vmf)
             ent['item_id'] = name
             ent.visgroup_ids.add(visgroup.id)
             ent.groups.add(group.id)
             ent.vis_shown = False
             ent.hidden = True
Ejemplo n.º 41
0
def make_tag_coop_inst(tag_loc: str):
    """Make the coop version of the tag instances.

    This needs to be shrunk, so all the logic entities are not spread
    out so much (coop tubes are small).

    This way we avoid distributing the logic.
    """
    global TAG_COOP_INST_VMF
    TAG_COOP_INST_VMF = vmf = VMF.parse(os.path.join(tag_loc, TAG_GUN_COOP_INST))

    def logic_pos():
        """Put the entities in a nice circle..."""
        while True:
            for ang in range(0, 44):
                ang *= 360 / 44
                yield Vec(16 * math.sin(ang), 16 * math.cos(ang), 32)

    pos = logic_pos()
    # Move all entities that don't care about position to the base of the player
    for ent in TAG_COOP_INST_VMF.iter_ents():
        if ent["classname"] == "info_coop_spawn":
            # Remove the original spawn point from the instance.
            # That way it can overlay over other dropper instances.
            ent.remove()
        elif ent["classname"] in ("info_target", "info_paint_sprayer"):
            pass
        else:
            ent["origin"] = next(pos)

            # These originally use the coop spawn point, but this doesn't
            # always work. Switch to the name of the player, which is much
            # more reliable.
            if ent["classname"] == "logic_measure_movement":
                ent["measuretarget"] = "!player_blue"

    # Add in a trigger to start the gel gun, and reset the activated
    # gel whenever the player spawns.
    trig_brush = vmf.make_prism(Vec(-32, -32, 0), Vec(32, 32, 16), mat="tools/toolstrigger").solid
    start_trig = vmf.create_ent(
        classname="trigger_playerteam", target_team=3, spawnflags=1, origin="0 0 8"  # ATLAS  # Clients only
    )
    start_trig.solids = [trig_brush]
    start_trig.add_out(
        # This uses the !activator as the target player so it must be via trigger.
        Output("OnStartTouchBluePlayer", "@gel_ui", "Activate", delay=0, only_once=True),
        # Reset the gun to fire nothing.
        Output("OnStartTouchBluePlayer", "@blueisenabled", "SetValue", 0, delay=0.1),
        Output("OnStartTouchBluePlayer", "@orangeisenabled", "SetValue", 0, delay=0.1),
    )
Ejemplo n.º 42
0
def make_alpha_base(vmf: VMF, bbox_min: Vec, bbox_max: Vec, noise: SimplexNoise):
    """Add the base to a CutoutTile, using displacements."""
    # We want to limit the size of brushes to 512, so the vertexes don't
    # get too far apart.
    # This now contains each point from beginning to end inclusive.
    x, y, z = bbox_min

    dim_x = bbox_max.x - bbox_min.x
    dim_y = bbox_max.y - bbox_min.y

    widths = [
        x
        for x in
        range(0, int(dim_x), 512)
    ] + [dim_x]
    heights = [
        y
        for y in
        range(0, int(dim_y), 512)
    ] + [dim_y]

    # Loop over two offset copies, so we get a min and max each time.
    for x1, x2 in zip(widths, widths[1:]):
        for y1, y2 in zip(heights, heights[1:]):
            # We place our displacement 1 unit above the surface, then offset
            # the verts down.
            brush = vmf.make_prism(
                Vec(x + x1, y + y1, z - FLOOR_DEPTH),
                Vec(x + x2, y + y2, z - FLOOR_DEPTH - 1),
            )
            brush.top.mat = random.choice(MATS['floorbase_disp'])
            make_displacement(
                brush.top,
                offset=-1,
                noise=noise,
            )
            vmf.add_brush(brush.solid)
Ejemplo n.º 43
0
def make_tile(vmf: VMF, p1: Vec, p2: Vec, top_mat, bottom_mat, beam_mat):
    """Generate a 2 or 1 unit thick squarebeams tile.

    """
    prism = vmf.make_prism(p1, p2)
    brush, t, b, n, s, e, w = prism
    t.mat = top_mat
    b.mat = bottom_mat

    n.mat = beam_mat
    s.mat = beam_mat
    e.mat = beam_mat
    w.mat = beam_mat

    thickness = abs(p1.z - p2.z)

    if thickness == 2:
        # The z-axis texture offset needed
        # The texture is 512 high, so wrap around
        # 56 is the offset for the thin-line part of squarebeams
        # Textures are at 0.25 size, so 4 per hammer unit
        z_off = ((max(p1.z, p2.z) * 4) + 56) % 512
    elif thickness == 1:
        # Slightly different offset, so the line is still centered
        z_off = ((max(p1.z, p2.z) * 4) + 54) % 512
    else:
        raise ValueError(
            'Tile has incorrect thickness '
            '(expected 1 or 2, got {})'.format(thickness)
        )

    n.uaxis = UVAxis(
        0, 0, 1, offset=z_off)
    n.vaxis = UVAxis(
        1, 0, 0, offset=0)
    s.uaxis = n.uaxis.copy()
    s.vaxis = n.vaxis.copy()

    e.uaxis = UVAxis(
        0, 0, 1, offset=z_off)
    e.vaxis = UVAxis(
        0, 1, 0, offset=0)
    w.uaxis = e.uaxis.copy()
    w.vaxis = e.vaxis.copy()

    # Ensure the squarebeams textures aren't replaced, as well as floor tex
    vbsp.IGNORED_FACES.update(brush.sides)

    return prism
Ejemplo n.º 44
0
def pack_files(
    vmf: VMF,
    *files: str,
    file_type: str='generic',
) -> None:
    """Add the given files to the packing list."""

    packlist = set(files) - _PACKED_FILES

    if not packlist:
        return

    ent = vmf.create_ent(
        classname='comp_pack',
        origin=vbsp_options.get(Vec, 'global_ents_loc'),
    )

    for i, file in enumerate(packlist, start=1):
        ent[file_type + str(i)] = file
Ejemplo n.º 45
0
def res_create_entity(vmf: VMF, inst: Entity, res: Property):
    """Create an entity.

    'keys' and 'localkeys' defines the new keyvalues used.
    'Origin' will be used to offset the given amount from the current location.
    """

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

    new_ent = vmf.create_ent(
        # Ensure there's a classname, just in case.
        classname='info_null'
    )

    conditions.set_ent_keys(new_ent, inst, res)

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

    new_ent['origin'] = origin
    new_ent['angles'] = inst['angles']
Ejemplo n.º 46
0
def make_glass_grating(
    vmf: VMF,
    ent_pos: Vec,
    normal: Vec,
    barr_type: BarrierType,
    front_temp: template_brush.ScalingTemplate,
    front_mat: str,
    solid_func: Callable[[float, float, str], List[Solid]],
):
    """Make all the brushes needed for glass/grating.

    solid_func() is called with two offsets from the voxel edge, and returns a
    matching list of solids. This allows doing holes and normal panes with the
    same function.
    """

    if barr_type is BarrierType.GLASS:
        main_ent = vmf.create_ent('func_detail')
        player_clip_mat = consts.Tools.PLAYER_CLIP_GLASS
    else:
        player_clip_mat = consts.Tools.PLAYER_CLIP_GRATE
        main_ent = vmf.create_ent(
            'func_brush',
            renderfx=14,  # Constant Glow
            solidity=1,  # Never solid
        )
    # The actual glass/grating brush - 0.5-1 units back from the surface.
    main_ent.solids = solid_func(0.5, 1, consts.Tools.NODRAW)

    abs_norm = abs(normal)
    for face in main_ent.sides():
        f_normal = face.normal()
        if abs(f_normal) == abs_norm:
            face.mat = front_mat
            front_temp.apply(face, change_mat=False)

    if normal.z == 0:
        # If vertical, we don't care about footsteps.
        # So just use 'normal' clips.
        player_clip = vmf.create_ent('func_detail')
        player_clip_mat = consts.Tools.PLAYER_CLIP
    else:
        # This needs to be a func_brush, otherwise the clip texture data
        # will be merged with other clips.
        player_clip = vmf.create_ent(
            'func_brush',
            solidbsp=1,
            origin=ent_pos,
        )
        # We also need a func_detail clip, which functions on portals.
        # Make it thinner, so it doesn't impact footsteps.
        player_thin_clip = vmf.create_ent('func_detail')
        player_thin_clip.solids = solid_func(0.5, 3.5, consts.Tools.PLAYER_CLIP)

    player_clip.solids = solid_func(0, 4, player_clip_mat)

    if barr_type is BarrierType.GRATING:
        # Add the VPhysics clip.
        phys_clip = vmf.create_ent(
            'func_clip_vphysics',
            filtername='@grating_filter',
            origin=ent_pos,
            StartDisabled=0,
        )
        phys_clip.solids = solid_func(0, 2, consts.Tools.TRIGGER)
Ejemplo n.º 47
0
def make_barriers(vmf: VMF, get_tex: Callable[[str], str]):
    """Make barrier entities. get_tex is vbsp.get_tex."""
    glass_temp = template_brush.get_scaling_template(
        vbsp_options.get(str, "glass_template")
    )
    grate_temp = template_brush.get_scaling_template(
        vbsp_options.get(str, "grating_template")
    )
    # Avoid error without this package.
    if HOLES:
        # Grab the template solids we need.
        hole_temp = template_brush.get_template(
            vbsp_options.get(str, 'glass_hole_temp')
        )
        hole_world, hole_detail, _ = hole_temp.visgrouped({'small'})
        hole_temp_small = hole_world + hole_detail
        hole_world, hole_detail, _ = hole_temp.visgrouped({'large'})
        hole_temp_large = hole_world + hole_detail
        hole_world, hole_detail, _ = hole_temp.visgrouped({'large_corner'})
        hole_temp_corner = hole_world + hole_detail
    else:
        hole_temp_small = hole_temp_large = hole_temp_corner = None

    floorbeam_temp = vbsp_options.get(str, 'glass_floorbeam_temp')

    if vbsp_options.get_itemconf('BEE_PELLET:PelletGrating', False):
        # Merge together these existing filters in global_pti_ents
        vmf.create_ent(
            origin=vbsp_options.get(Vec, 'global_pti_ents_loc'),
            targetname='@grating_filter',
            classname='filter_multi',
            filtertype=0,
            negated=0,
            filter01='@not_pellet',
            filter02='@not_paint_bomb',
        )
    else:
        # Just skip paint bombs.
        vmf.create_ent(
            origin=vbsp_options.get(Vec, 'global_pti_ents_loc'),
            targetname='@grating_filter',
            classname='filter_activator_class',
            negated=1,
            filterclass='prop_paint_bomb',
        )

    # Group the positions by planes in each orientation.
    # This makes them 2D grids which we can optimise.
    # (normal_dist, positive_axis, type) -> [(x, y)]
    slices = defaultdict(set)  # type: Dict[Tuple[Tuple[float, float, float], bool, BarrierType], Set[Tuple[float, float]]]
    # We have this on the 32-grid so we can cut squares for holes.

    for (origin, normal), barr_type in BARRIERS.items():
        origin = Vec(origin)
        normal = Vec(normal)
        norm_axis = normal.axis()
        u, v = origin.other_axes(norm_axis)
        norm_pos = Vec.with_axes(norm_axis, origin)
        slice_plane = slices[
            norm_pos.as_tuple(),  # distance from origin to this plane.
            normal[norm_axis] > 0,
            barr_type,
        ]
        for u_off in [-48, -16, 16, 48]:
            for v_off in [-48, -16, 16, 48]:
                slice_plane.add((
                    (u + u_off) // 32,
                    (v + v_off) // 32,
                ))

    # Remove pane sections where the holes are. We then generate those with
    # templates for slanted parts.
    for (origin, normal), hole_type in HOLES.items():
        barr_type = BARRIERS[origin, normal]

        origin = Vec(origin)
        normal = Vec(normal)
        norm_axis = normal.axis()
        u, v = origin.other_axes(norm_axis)
        norm_pos = Vec.with_axes(norm_axis, origin)
        slice_plane = slices[
            norm_pos.as_tuple(),
            normal[norm_axis] > 0,
            barr_type,
        ]
        if hole_type is HoleType.LARGE:
            offsets = (-80, -48, -16, 16, 48, 80)
            hole_temp = hole_temp_large.copy()
        else:
            offsets = (-16, 16)
            hole_temp = hole_temp_small.copy()
        for u_off in offsets:
            for v_off in offsets:
                # Skip the corners on large holes.
                # Those aren't actually used, so skip them. That way
                # we can have them diagonally or  without glass in the corner.
                if u_off in (-80, 80) and v_off in (-80, 80):
                    continue

                slice_plane.discard((
                    (u + u_off) // 32,
                    (v + v_off) // 32,
                ))

        # Now generate the curved brushwork.

        if barr_type is BarrierType.GLASS:
            front_temp = glass_temp
            front_mat = get_tex('special.glass')
        elif barr_type is BarrierType.GRATING:
            front_temp = grate_temp
            front_mat = get_tex('special.grating')
        else:
            raise NotImplementedError

        angles = normal.to_angle(0)
        # Angle corresponding to the brush, for the corners.
        angle_list = [angles] * len(hole_temp)

        # This is a tricky bit. Two large templates would collide
        # diagonally,
        # so chop off the corners, then put them back only if there's not
        # one diagonally.
        if hole_type is HoleType.LARGE:
            for roll in (0, 90, 180, 270):
                corn_angles = angles.copy()
                corn_angles.z = roll
                hole_off = origin + Vec(y=128, z=128).rotate(*corn_angles)
                diag_type = HOLES.get(
                    (hole_off.as_tuple(), normal.as_tuple()),
                    None,
                )
                if diag_type is not HoleType.LARGE:
                    hole_temp += hole_temp_corner
                    angle_list += [corn_angles] * len(hole_temp_corner)

        def solid_pane_func(off1, off2, mat):
            """Given the two thicknesses, produce the curved hole from the template."""
            off_min = min(off1, off2)
            off_max = max(off1, off2)
            new_brushes = [
                brush.copy(vmf_file=vmf)
                for brush in hole_temp
            ]

            for brush, br_angles in zip(new_brushes, angle_list):
                for face in brush.sides:
                    face.mat = mat
                    f_norm = face.normal()
                    if f_norm.x == 1:
                        face.translate(Vec(x=4 - off_max))
                        # face.mat = 'min'
                    elif f_norm.x == -1:
                        face.translate(Vec(x=-4 - off_min))
                        # face.mat = 'max'
                    face.localise(origin, br_angles)
            return new_brushes

        make_glass_grating(
            vmf,
            origin,
            normal,
            barr_type,
            front_temp,
            front_mat,
            solid_pane_func,
        )

    for (plane_pos, is_pos, barr_type), pos_slice in slices.items():
        plane_pos = Vec(plane_pos)
        norm_axis = plane_pos.axis()
        normal = Vec.with_axes(norm_axis, 1 if is_pos else -1)

        if barr_type is BarrierType.GLASS:
            front_temp = glass_temp
            front_mat = get_tex('special.glass')
        elif barr_type is BarrierType.GRATING:
            front_temp = grate_temp
            front_mat = get_tex('special.grating')
        else:
            raise NotImplementedError

        u_axis, v_axis = Vec.INV_AXIS[norm_axis]

        for min_u, min_v, max_u, max_v in grid_optimise(dict.fromkeys(pos_slice, True)):
            # These are two points in the origin plane, at the borders.
            pos_min = Vec.with_axes(
                norm_axis, plane_pos,
                u_axis, min_u * 32,
                v_axis, min_v * 32,
            )
            pos_max = Vec.with_axes(
                norm_axis, plane_pos,
                u_axis, max_u * 32 + 32,
                v_axis, max_v * 32 + 32,
            )

            def solid_pane_func(pos1, pos2, mat):
                """Make the solid brush."""
                return [vmf.make_prism(
                    pos_min + normal * (64.0 - pos1),
                    pos_max + normal * (64.0 - pos2),
                    mat=mat,
                ).solid]

            make_glass_grating(
                vmf,
                (pos_min + pos_max)/2,
                normal,
                barr_type,
                front_temp,
                front_mat,
                solid_pane_func,
            )

    if floorbeam_temp:
        LOGGER.info('Adding Glass floor beams...')
        add_glass_floorbeams(vmf, floorbeam_temp)
        LOGGER.info('Done!')
Ejemplo n.º 48
0
def gen_squarebeams(vmf: VMF, p1: Vec, p2: Vec, skin, gen_collision=True):
    """Generate squarebeams props to fill the space given.

    The space should be in multiples of 64. The squarebeams brush will
    be aligned to the lowest point in the space.
    """
    z = min(p1.z, p2.z) + 8
    min_x = min(p1.x, p2.x)
    min_y = min(p1.y, p2.y)
    max_x = max(p1.x, p2.x)
    max_y = max(p1.y, p2.y)

    dist_x = max_x - min_x
    dist_y = max_y - min_y

    # After this x or y dist, move to the next grid size.
    cutoff_512_x = dist_x // 512 * 512
    cutoff_256_x = dist_x // 256 * 256
    cutoff_128_x = dist_x // 128 * 128

    cutoff_512_y = dist_y // 512 * 512
    cutoff_256_y = dist_y // 256 * 256
    cutoff_128_y = dist_y // 128 * 128

    for x, y in utils.iter_grid(
            max_x=int(dist_x),
            max_y=int(dist_y),
            stride=64,
            ):
        if x < cutoff_512_x and y < cutoff_512_y:
            # Make 1 prop every 512 units, at the center
            if x % 512 == 0 and y % 512 == 0:
                _make_squarebeam(
                    vmf,
                    Vec(min_x + x + 256, min_y +  y + 256, z),
                    skin, '_8x8',
                )
        elif x < cutoff_256_x and y < cutoff_256_y:
            if x % 256 == 0 and y % 256 == 0:
                _make_squarebeam(
                    vmf,
                    Vec(min_x + x + 128, min_y + y + 128, z),
                    skin, '_4x4',
                )
        elif x < cutoff_128_x and y < cutoff_128_y:
            if x % 128 == 0 and y % 128 == 0:
                _make_squarebeam(
                    vmf,
                    Vec(min_x + x + 64, min_y + y + 64, z),
                    skin, '_2x2',
                )
        else:
            # Make squarebeams for every point!
            _make_squarebeam(
                vmf,
                Vec(min_x + x + 32, min_y + y + 32, z),
                skin,
            )

    if gen_collision:
        collision = vmf.create_ent(
            classname='func_brush',
            disableshadows='1',
            disableflashlight='1',
            disablereceiveshadows='1',
            shadowdepthnocache='1',
            solidity='2',  # Always Solid
            solidbsp='1',
        )
        for x in range(int(min_x)+64, int(max_x), 64):
            collision.solids.append(
                vmf.make_prism(
                    p1=Vec(x-2, min_y+2, z-2),
                    p2=Vec(x+2, max_y-2, z-8),
                    mat='tools/toolsnodraw',
                ).solid
            )
        for y in range(int(min_y)+64, int(max_y), 64):
            collision.solids.append(
                vmf.make_prism(
                    p1=Vec(min_x+2, y-2, z-2),
                    p2=Vec(max_x-2, y+2, z-8),
                    mat='tools/toolsnodraw',
                ).solid
            )
        for x1, y1, x2, y2 in [
                (min_x, min_y, max_x, min_y+2),
                (min_x, max_y, max_x, max_y-2),
                (min_x, min_y, min_x+2, max_y),
                (max_x, min_y, max_x-2, max_y),
                ]:
            collision.solids.append(
                vmf.make_prism(
                    p1=Vec(x1, y1, z-2),
                    p2=Vec(x2, y2, z-8),
                    mat='tools/toolsnodraw',
                ).solid
            )
Ejemplo n.º 49
0
def flag_brush_at_loc(vmf: VMF, inst: Entity, flag: Property):
    """Checks to see if a wall is present at the given location.

    - `Pos` is the position of the brush, where `0 0 0` is the floor-position
       of the brush.
    - `Dir` is the normal the face is pointing. `(0 0 -1)` is up.
    - `Type` defines the type the brush must be:
      - `Any` requires either a black or white brush.
      - `None` means that no brush must be present.
      - `White` requires a portalable surface.
      - `Black` requires a non-portalable surface.
    - `SetVar` defines an instvar which will be given a value of `black`,
      `white` or `none` to allow the result to be reused.
    - If `gridPos` is true, the position will be snapped so it aligns with
      the 128 grid (Useful with fizzler/light strip items).
    - `RemoveBrush`: If set to `1`, the brush will be removed if found.
      Only do this to `EmbedFace` brushes, since it will remove the other
      sides as well.
    """
    pos = Vec.from_str(flag['pos', '0 0 0'])
    pos.z -= 64  # Subtract so origin is the floor-position
    pos = pos.rotate_by_str(inst['angles', '0 0 0'])

    # Relative to the instance origin
    pos += Vec.from_str(inst['origin', '0 0 0'])

    norm = flag['dir', None]
    if norm is not None:
        norm = Vec.from_str(norm).rotate_by_str(
            inst['angles', '0 0 0'],
        )

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

    result_var = flag['setVar', '']
    should_remove = srctools.conv_bool(flag['RemoveBrush', False], False)
    des_type = flag['type', 'any'].casefold()

    brush = SOLIDS.get(pos.as_tuple(), None)

    if brush is None or (norm is not None and abs(brush.normal) != abs(norm)):
        br_type = 'none'
    else:
        br_type = str(brush.color)
        if should_remove:
            vmf.remove_brush(
                brush.solid,
            )

    if result_var:
        inst.fixup[result_var] = br_type

    if des_type == 'any' and br_type != 'none':
        return True

    return des_type == br_type
Ejemplo n.º 50
0
def ap_tag_modifications(vmf: VMF):
    """Perform modifications for Aperture Tag.

    * All fizzlers will be combined with a trigger_paint_cleanser
    * Paint is always present in every map!
    * Suppress ATLAS's Portalgun in coop
    * Override the transition ent instance to have the Gel Gun
    * Create subdirectories with the user's steam ID
    """
    if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['APTAG']:
        return  # Wrong game!

    LOGGER.info('Performing Aperture Tag modifications...')

    has = vbsp.settings['has_attr']
    # This will enable the PaintInMap property.
    has['Gel'] = True

    # Set as if the player spawned with no pgun
    has['spawn_dual'] = False
    has['spawn_single'] = False
    has['spawn_nogun'] = True

    # Add paint fizzlers to all normal fizzlers
    for fizz in vmf.by_class['trigger_portal_cleanser']:
        p_fizz = fizz.copy()
        p_fizz['classname'] = 'trigger_paint_cleanser'
        vmf.add_ent(p_fizz)

        if p_fizz['targetname'].endswith('_brush'):
            p_fizz['targetname'] = p_fizz['targetname'][:-6] + '-br_fizz'

        del p_fizz['drawinfastreflection']
        del p_fizz['visible']
        del p_fizz['useScanline']

        for side in p_fizz.sides():
            side.mat = 'tools/toolstrigger'
            side.scale = 0.25

    transition_ents = get_special_inst('transitionents')
    for inst in vmf.by_class['func_instance']:
        if inst['file'].casefold() not in transition_ents:
            continue
        inst['file'] = 'instances/bee2/transition_ents_tag.vmf'

    # Because of a bug in P2, these folders aren't created automatically.
    # We need a folder with the user's ID in portal2/maps/puzzlemaker.
    try:
        puzz_folders = os.listdir('../aperturetag/puzzles')
    except FileNotFoundError:
        LOGGER.warning("Aperturetag/puzzles/ doesn't exist??")
    else:
        for puzz_folder in puzz_folders:
            new_folder = os.path.abspath(os.path.join(
                '../portal2/maps/puzzlemaker',
                puzz_folder,
            ))
            LOGGER.info('Creating', new_folder)
            os.makedirs(
                new_folder,
                exist_ok=True,
            )
Ejemplo n.º 51
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.º 52
0
def res_make_tag_fizzler(vmf: VMF, inst: Entity, res: Property):
    """Add an Aperture Tag Paint Gun activation fizzler.

    These fizzlers are created via signs, and work very specially.
    This must be before -250 so it runs before fizzlers and connections.
    """
    (
        sign_offset,
        fizz_io_type,
        inst_frame_double,
        inst_frame_single,
        blue_sign_on,
        blue_sign_off,
        oran_sign_on,
        oran_sign_off,
    ) = res.value  # type: int, ItemType, str, str, str, str, str, str
    import vbsp
    if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']:
        # Abort - TAG fizzlers shouldn't appear in any other game!
        inst.remove()
        return

    fizzler = None
    fizzler_item = None

    # Look for the fizzler instance we want to replace.
    sign_item = ITEMS[inst['targetname']]
    for conn in list(sign_item.outputs):
        if conn.to_item.name in FIZZLERS:
            if fizzler is None:
                fizzler = FIZZLERS[conn.to_item.name]
                fizzler_item = conn.to_item
            else:
                raise ValueError('Multiple fizzlers attached to a sign!')

        conn.remove()  # Regardless, remove the useless output.

    sign_item.delete_antlines()

    if fizzler is None:
        # No fizzler - remove this sign
        inst.remove()
        return

    if fizzler.fizz_type.id == TAG_FIZZ_ID:
        LOGGER.warning('Two tag signs attached to one fizzler...')
        inst.remove()
        return

    # Swap to the special Tag Fizzler type.
    fizzler.fizz_type = FIZZ_TYPES[TAG_FIZZ_ID]

    # And also swap the connection's type.
    fizzler_item.item_type = fizz_io_type
    fizzler_item.enable_cmd = fizz_io_type.enable_cmd
    fizzler_item.disable_cmd = fizz_io_type.disable_cmd
    fizzler_item.sec_enable_cmd = fizz_io_type.sec_enable_cmd
    fizzler_item.sec_disable_cmd = fizz_io_type.sec_disable_cmd

    sign_loc = (
        # The actual location of the sign - on the wall
        Vec.from_str(inst['origin']) +
        Vec(0, 0, -64).rotate_by_str(inst['angles'])
    )

    # Now deal with the visual aspect:
    # Blue signs should be on top.

    blue_enabled = inst.fixup.bool('$start_enabled')
    oran_enabled = inst.fixup.bool('$start_reversed')
    # If True, single-color signs will also turn off the other color.
    # This also means we always show both signs.
    # If both are enabled or disabled, this has no effect.
    disable_other = (
        not inst.fixup.bool('$disable_autorespawn', True) and
        blue_enabled != oran_enabled
    )
    # Delete fixups now, they aren't useful.
    inst.fixup.clear()

    if not blue_enabled and not oran_enabled:
        # Hide the sign in this case!
        inst.remove()

    inst_angle = srctools.parse_vec_str(inst['angles'])

    inst_normal = Vec(0, 0, 1).rotate(*inst_angle)
    loc = Vec.from_str(inst['origin'])

    if disable_other or (blue_enabled and oran_enabled):
        inst['file'] = inst_frame_double
        # On a wall, and pointing vertically
        if inst_normal.z == 0 and Vec(y=1).rotate(*inst_angle).z:
            # They're vertical, make sure blue's on top!
            blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset)
            oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset)
            # If orange is enabled, with two frames put that on top
            # instead since it's more important
            if disable_other and oran_enabled:
                blue_loc, oran_loc = oran_loc, blue_loc

        else:
            offset = Vec(0, sign_offset, 0).rotate(*inst_angle)
            blue_loc = loc + offset
            oran_loc = loc - offset
    else:
        inst['file'] = inst_frame_single
        # They're always centered
        blue_loc = loc
        oran_loc = loc

    if inst_normal.z != 0:
        # If on floors/ceilings, rotate to point at the fizzler!
        sign_floor_loc = sign_loc.copy()
        sign_floor_loc.z = 0  # We don't care about z-positions.

        # Grab the data saved earlier in res_find_potential_tag_fizzlers()
        axis, side_min, side_max, normal = calc_fizzler_orient(fizzler)

        # The Z-axis fizzler (horizontal) must be treated differently.
        if axis == 'z':
            # For z-axis, just compare to the center point.
            # The values are really x, y, z, not what they're named.
            sign_dir = sign_floor_loc - (side_min, side_max, normal)
        else:
            # For the other two, we compare to the line,
            # or compare to the closest side (in line with the fizz)
            other_axis = 'x' if axis == 'y' else 'y'
            if abs(sign_floor_loc[other_axis] - normal) < 32:
                # Compare to the closest side. Use ** to swap x/y arguments
                # appropriately. The closest side is the one with the
                # smallest magnitude.
                sign_dir = min(
                    sign_floor_loc - Vec.with_axes(
                        axis,side_min,
                        other_axis, normal,
                    ),
                    sign_floor_loc - Vec.with_axes(
                        axis, side_max,
                        other_axis, normal,
                    ),
                    key=Vec.mag,
                )
            else:
                # Align just based on whether we're in front or behind.
                sign_dir = Vec()
                sign_dir[other_axis] = sign_floor_loc[other_axis] - normal

        sign_angle = math.degrees(
            math.atan2(sign_dir.y, sign_dir.x)
        )
        # Round to nearest 90 degrees
        # Add 45 so the switchover point is at the diagonals
        sign_angle = (sign_angle + 45) // 90 * 90

        # Rotate to fit the instances - south is down
        sign_angle = int(sign_angle + 90) % 360
        if inst_normal.z > 0:
            sign_angle = '0 {} 0'.format(sign_angle)
        elif inst_normal.z < 0:
            # Flip upside-down for ceilings
            sign_angle = '0 {} 180'.format(sign_angle)
    else:
        # On a wall, face upright
        sign_angle = PETI_INST_ANGLE[inst_normal.as_tuple()]

    # If disable_other, we show off signs. Otherwise we don't use that sign.
    blue_sign = blue_sign_on if blue_enabled else blue_sign_off if disable_other else None
    oran_sign = oran_sign_on if oran_enabled else oran_sign_off if disable_other else None

    if blue_sign:
        vmf.create_ent(
            classname='func_instance',
            file=blue_sign,
            targetname=inst['targetname'],
            angles=sign_angle,
            origin=blue_loc.join(' '),
        )

    if oran_sign:
        vmf.create_ent(
            classname='func_instance',
            file=oran_sign,
            targetname=inst['targetname'],
            angles=sign_angle,
            origin=oran_loc.join(' '),
        )

    # Now modify the fizzler...

    # Subtract the sign from the list of connections, but don't go below
    # zero
    fizzler.base_inst.fixup['$connectioncount'] = str(max(
        0,
        srctools.conv_int(fizzler.base_inst.fixup['$connectioncount', '']) - 1
    ))

    # Find the direction the fizzler normal is.
    # Signs will associate with the given side!

    bbox_min, bbox_max = fizzler.emitters[0]
    fizz_norm_axis = fizzler.normal().axis()

    sign_center = (bbox_min[fizz_norm_axis] + bbox_max[fizz_norm_axis]) / 2

    # Figure out what the sides will set values to...
    pos_blue = False
    pos_oran = False
    neg_blue = False
    neg_oran = False

    if sign_loc[fizz_norm_axis] < sign_center:
        pos_blue = blue_enabled
        pos_oran = oran_enabled
    else:
        neg_blue = blue_enabled
        neg_oran = oran_enabled

    # If it activates the paint gun, use different textures
    fizzler.tag_on_pos = pos_blue or pos_oran
    fizzler.tag_on_neg = neg_blue or neg_oran

    # Now make the trigger ents. We special-case these since they need to swap
    # depending on the sign config and position.

    if vbsp.GAME_MODE == 'COOP':
        # We need ATLAS-specific triggers
        pos_trig = vmf.create_ent(
            classname='trigger_playerteam',
        )
        neg_trig = vmf.create_ent(
            classname='trigger_playerteam',
        )
        output = 'OnStartTouchBluePlayer'
    else:
        pos_trig = vmf.create_ent(
            classname='trigger_multiple',
        )
        neg_trig = vmf.create_ent(
            classname='trigger_multiple',
            spawnflags='1',
        )
        output = 'OnStartTouch'

    pos_trig['origin'] = neg_trig['origin'] = fizzler.base_inst['origin']
    pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1'  # Clients Only

    pos_trig['targetname'] = local_name(fizzler.base_inst, 'trig_pos')
    neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_neg')

    pos_trig['startdisabled'] = neg_trig['startdisabled'] = (
        not fizzler.base_inst.fixup.bool('start_enabled')
    )

    pos_trig.outputs = [
        Output(output, neg_trig, 'Enable'),
        Output(output, pos_trig, 'Disable'),
    ]

    neg_trig.outputs = [
        Output(output, pos_trig, 'Enable'),
        Output(output, neg_trig, 'Disable'),
    ]

    voice_attr = vbsp.settings['has_attr']

    if blue_enabled or disable_other:
        # If this is blue/oran only, don't affect the other color
        neg_trig.outputs.append(Output(
            output,
            '@BlueIsEnabled',
            'SetValue',
            param=srctools.bool_as_int(neg_blue),
        ))
        pos_trig.outputs.append(Output(
            output,
            '@BlueIsEnabled',
            'SetValue',
            param=srctools.bool_as_int(pos_blue),
        ))
        if blue_enabled:
            # Add voice attributes - we have the gun and gel!
            voice_attr['bluegelgun'] = True
            voice_attr['bluegel'] = True
            voice_attr['bouncegun'] = True
            voice_attr['bouncegel'] = True

    if oran_enabled or disable_other:
        neg_trig.outputs.append(Output(
            output,
            '@OrangeIsEnabled',
            'SetValue',
            param=srctools.bool_as_int(neg_oran),
        ))
        pos_trig.outputs.append(Output(
            output,
            '@OrangeIsEnabled',
            'SetValue',
            param=srctools.bool_as_int(pos_oran),
        ))
        if oran_enabled:
            voice_attr['orangegelgun'] = True
            voice_attr['orangegel'] = True
            voice_attr['speedgelgun'] = True
            voice_attr['speedgel'] = True

    if not oran_enabled and not blue_enabled:
        # If both are disabled, we must shutdown the gun when touching
        # either side - use neg_trig for that purpose!
        # We want to get rid of pos_trig to save ents
        vmf.remove_ent(pos_trig)
        neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_off')
        neg_trig.outputs.clear()
        neg_trig.add_out(Output(
            output,
            '@BlueIsEnabled',
            'SetValue',
            param='0'
        ))
        neg_trig.add_out(Output(
            output,
            '@OrangeIsEnabled',
            'SetValue',
            param='0'
        ))

    # Make the triggers.
    for bbox_min, bbox_max in fizzler.emitters:
        bbox_min = bbox_min.copy() - 64 * fizzler.up_axis
        bbox_max = bbox_max.copy() + 64 * fizzler.up_axis

        # The triggers are 8 units thick, with a 32-unit gap in the middle
        neg_min, neg_max = Vec(bbox_min), Vec(bbox_max)
        neg_min[fizz_norm_axis] -= 24
        neg_max[fizz_norm_axis] -= 16

        pos_min, pos_max = Vec(bbox_min), Vec(bbox_max)
        pos_min[fizz_norm_axis] += 16
        pos_max[fizz_norm_axis] += 24

        if blue_enabled or oran_enabled:
            neg_trig.solids.append(
                vmf.make_prism(
                    neg_min,
                    neg_max,
                    mat='tools/toolstrigger',
                ).solid,
            )
            pos_trig.solids.append(
                vmf.make_prism(
                    pos_min,
                    pos_max,
                    mat='tools/toolstrigger',
                ).solid,
            )
        else:
            # If neither enabled, use one trigger
            neg_trig.solids.append(
                vmf.make_prism(
                    neg_min,
                    pos_max,
                    mat='tools/toolstrigger',
                ).solid,
            )
Ejemplo n.º 53
0
def res_resizeable_trigger(vmf: VMF, res: Property):
    """Replace two markers with a trigger brush.  

    This is run once to affect all of an item.  
    Options:
    * `markerInst`: <ITEM_ID:1,2> value referencing the marker instances, or a filename.
    * `markerItem`: The item's ID
    * `previewConf`: A item config which enables/disables the preview overlay.
    * `previewInst`: An instance to place at the marker location in preview mode.
        This should contain checkmarks to display the value when testing.
    * `previewMat`: If set, the material to use for an overlay func_brush.
        The brush will be parented to the trigger, so it vanishes once killed.
        It is also non-solid.
    * `previewScale`: The scale for the func_brush materials.
    * `previewActivate`, `previewDeactivate`: The VMF output to turn the
        previewInst on and off.

    * `triggerActivate, triggerDeactivate`: The `instance:name;Output`
        outputs used when the trigger turns on or off.

    * `coopVar`: The instance variable which enables detecting both Coop players.
        The trigger will be a trigger_playerteam.

    * `coopActivate, coopDeactivate`: The `instance:name;Output` outputs used
        when coopVar is enabled. These should be suitable for a logic_coop_manager.
    * `coopOnce`: If true, kill the manager after it first activates.

    * `keys`: A block of keyvalues for the trigger brush. Origin and targetname
        will be set automatically.
    * `localkeys`: The same as above, except values will be changed to use
        instance-local names.
    """
    marker = instanceLocs.resolve(res['markerInst'])

    marker_names = set()

    for inst in vmf.by_class['func_instance']:
        if inst['file'].casefold() in marker:
            marker_names.add(inst['targetname'])
            # Unconditionally delete from the map, so it doesn't
            # appear even if placed wrongly.
            inst.remove()

    if not marker_names:  # No markers in the map - abort
        return RES_EXHAUSTED

    item_id = res['markerItem']

    # Synthesise the item type used for the final trigger.
    item_type_sp = connections.ItemType(
        id=item_id + ':TRIGGER',
        output_act=Output.parse_name(res['triggerActivate', 'OnStartTouchAll']),
        output_deact=Output.parse_name(res['triggerDeactivate', 'OnEndTouchAll']),
    )

    # For Coop, we add a logic_coop_manager in the mix so both players can
    # be handled.
    try:
        coop_var = res['coopVar']
    except LookupError:
        coop_var = item_type_coop = None
        coop_only_once = False
    else:
        coop_only_once = res.bool('coopOnce')
        item_type_coop = connections.ItemType(
            id=item_id + ':TRIGGER_COOP',
            output_act=Output.parse_name(
                res['coopActivate', 'OnChangeToAllTrue']
            ),
            output_deact=Output.parse_name(
                res['coopDeactivate', 'OnChangeToAnyFalse']
            ),
        )

    # Display preview overlays if it's preview mode, and the config is true
    pre_act = pre_deact = None
    if vbsp.IS_PREVIEW and vbsp_options.get_itemconf(res['previewConf', ''], False):
        preview_mat = res['previewMat', '']
        preview_inst_file = res['previewInst', '']
        preview_scale = res.float('previewScale', 0.25)
        # None if not found.
        with suppress(LookupError):
            pre_act = Output.parse(res.find_key('previewActivate'))
        with suppress(LookupError):
            pre_deact = Output.parse(res.find_key('previewDeactivate'))
    else:
        # Deactivate the preview_ options when publishing.
        preview_mat = preview_inst_file = ''
        preview_scale = 0.25

    # Now go through each brush.
    # We do while + pop to allow removing both names each loop through.
    todo_names = set(marker_names)
    while todo_names:
        targ = todo_names.pop()

        mark1 = connections.ITEMS.pop(targ)
        for conn in mark1.outputs:
            if conn.to_item.name in marker_names:
                mark2 = conn.to_item
                conn.remove()  # Delete this connection.
                todo_names.discard(mark2.name)
                del connections.ITEMS[mark2.name]
                break
        else:
            if not mark1.inputs:
                # If the item doesn't have any connections, 'connect'
                # it to itself so we'll generate a 1-block trigger.
                mark2 = mark1
            else:
                # It's a marker with an input, the other in the pair
                # will handle everything.
                # But reinstate it in ITEMS.
                connections.ITEMS[targ] = mark1
                continue

        inst1 = mark1.inst
        inst2 = mark2.inst

        is_coop = coop_var is not None and vbsp.GAME_MODE == 'COOP' and (
            inst1.fixup.bool(coop_var) or
            inst2.fixup.bool(coop_var)
        )

        bbox_min, bbox_max = Vec.bbox(
            Vec.from_str(inst1['origin']),
            Vec.from_str(inst2['origin'])
        )
        origin = (bbox_max + bbox_min) / 2

        # Extend to the edge of the blocks.
        bbox_min -= 64
        bbox_max += 64

        out_ent = trig_ent = vmf.create_ent(
            classname='trigger_multiple',  # Default
            targetname=targ,
            origin=origin,
            angles='0 0 0',
        )
        trig_ent.solids = [
            vmf.make_prism(
                bbox_min,
                bbox_max,
                mat=const.Tools.TRIGGER,
            ).solid,
        ]

        # Use 'keys' and 'localkeys' blocks to set all the other keyvalues.
        conditions.set_ent_keys(trig_ent, inst, res)

        if is_coop:
            trig_ent['spawnflags'] = '1'  # Clients
            trig_ent['classname'] = 'trigger_playerteam'

            out_ent = manager = vmf.create_ent(
                classname='logic_coop_manager',
                targetname=conditions.local_name(inst, 'man'),
                origin=origin,
            )

            item = connections.Item(
                out_ent,
                item_type_coop,
                mark1.ant_floor_style,
                mark1.ant_wall_style,
            )

            if coop_only_once:
                # Kill all the ents when both players are present.
                manager.add_out(
                    Output('OnChangeToAllTrue', manager, 'Kill'),
                    Output('OnChangeToAllTrue', targ, 'Kill'),
                )
            trig_ent.add_out(
                Output('OnStartTouchBluePlayer', manager, 'SetStateATrue'),
                Output('OnStartTouchOrangePlayer', manager, 'SetStateBTrue'),
                Output('OnEndTouchBluePlayer', manager, 'SetStateAFalse'),
                Output('OnEndTouchOrangePlayer', manager, 'SetStateBFalse'),
            )
        else:
            item = connections.Item(
                trig_ent,
                item_type_sp,
                mark1.ant_floor_style,
                mark1.ant_wall_style,
            )

        # Register, and copy over all the antlines.
        connections.ITEMS[item.name] = item
        item.ind_panels = mark1.ind_panels | mark2.ind_panels
        item.antlines = mark1.antlines | mark2.antlines
        item.shape_signs = mark1.shape_signs + mark2.shape_signs

        if preview_mat:
            preview_brush = vmf.create_ent(
                classname='func_brush',
                parentname=targ,
                origin=origin,

                Solidity='1',  # Not solid
                drawinfastreflection='1',  # Draw in goo..

                # Disable shadows and lighting..
                disableflashlight='1',
                disablereceiveshadows='1',
                disableshadowdepth='1',
                disableshadows='1',
            )
            preview_brush.solids = [
                # Make it slightly smaller, so it doesn't z-fight with surfaces.
                vmf.make_prism(
                    bbox_min + 0.5,
                    bbox_max - 0.5,
                    mat=preview_mat,
                ).solid,
            ]
            for face in preview_brush.sides():
                face.scale = preview_scale

        if preview_inst_file:
            pre_inst = vmf.create_ent(
                classname='func_instance',
                targetname=targ + '_preview',
                file=preview_inst_file,
                # Put it at the second marker, since that's usually
                # closest to antlines if present.
                origin=inst2['origin'],
            )

            if pre_act is not None:
                out = pre_act.copy()
                out.inst_out, out.output = item.output_act()
                out.target = conditions.local_name(pre_inst, out.target)
                out_ent.add_out(out)
            if pre_deact is not None:
                out = pre_deact.copy()
                out.inst_out, out.output = item.output_deact()
                out.target = conditions.local_name(pre_inst, out.target)
                out_ent.add_out(out)

        for conn in mark1.outputs | mark2.outputs:
            conn.from_item = item

    return RES_EXHAUSTED
Ejemplo n.º 54
0
def place_catwalk_connections(vmf: VMF, instances, point_a: Vec, point_b: Vec):
    """Place catwalk sections to connect two straight points."""
    diff = point_b - point_a

    # The horizontal unit vector in the direction we are placing catwalks
    direction = diff.copy()
    direction.z = 0
    distance = direction.len() - 128
    direction = direction.norm()

    if diff.z > 0:
        angle = INST_ANGLE[direction.as_tuple()]
        # We need to add stairs
        for stair_pos in range(0, int(diff.z), 128):
            # Move twice the vertical horizontally
            # plus 128 so we don't start in point A
            loc = point_a + (2 * stair_pos + 128) * direction
            # Do the vertical offset
            loc.z += stair_pos
            vmf.create_ent(
                classname='func_instance',
                origin=loc.join(' '),
                angles=angle,
                file=instances['stair'],
            )
        # This is the location we start flat sections at
        point_a = loc + 128 * direction
        point_a.z += 128
    elif diff.z < 0:
        # We need to add downward stairs
        # They point opposite to normal ones
        LOGGER.debug('down from {}', point_a)
        angle = INST_ANGLE[(-direction).as_tuple()]
        for stair_pos in range(0, -int(diff.z), 128):
            LOGGER.debug(stair_pos)
            # Move twice the vertical horizontally
            loc = point_a + (2 * stair_pos + 256) * direction  # type: Vec
            # Do the vertical offset plus additional 128 units
            # to account for the moved instance
            loc.z -= (stair_pos + 128)
            vmf.create_ent(
                classname='func_instance',
                origin=loc.join(' '),
                angles=angle,
                file=instances['stair'],
            )
        # Adjust point A to be at the end of the catwalks
        point_a = loc
    # Remove the space the stairs take up from the horiz distance
    distance -= abs(diff.z) * 2

    # Now do straight sections
    LOGGER.debug('Stretching {} {}', distance, direction)
    angle = INST_ANGLE[direction.as_tuple()]
    loc = point_a + (direction * 128)

    # Figure out the most efficent number of sections
    for segment_len in utils.fit(
            distance,
            [512, 256, 128]
            ):
        vmf.create_ent(
            classname='func_instance',
            origin=loc.join(' '),
            angles=angle,
            file=instances['straight_' + str(segment_len)],
        )
        loc += (segment_len * direction)
Ejemplo n.º 55
0
    def read_from_map(self, vmf: VMF, has_attr: Dict[str, bool]) -> None:
        """Given the map file, set blocks."""
        search_locs = []

        for ent in vmf.entities:
            pos = ent['origin', None]
            if pos is None:
                continue
            pos = world_to_grid(Vec.from_str(pos))

            # Exclude entities outside the main area - elevators mainly.
            # The border should never be set to air!
            if (0, 0, 0) <= pos <= (25, 25, 25):
                search_locs.append(pos)

        can_have_pit = bottomlessPit.pits_allowed()

        for brush in vmf.brushes[:]:
            tex = {face.mat.casefold() for face in brush.sides}

            bbox_min, bbox_max = brush.get_bbox()

            if ('nature/toxicslime_a2_bridge_intro' in tex or
                'nature/toxicslime_puzzlemaker_cheap' in tex):
                # It's goo!

                x = bbox_min.x + 64
                y = bbox_min.y + 64

                g_x = x // 128
                g_y = y // 128

                is_pit = can_have_pit and bottomlessPit.is_pit(bbox_min, bbox_max)

                # If goo is multi-level, we want to record all pos!
                z_pos = range(int(bbox_min.z) + 64, int(bbox_max.z), 128)
                top_ind = len(z_pos) - 1
                for ind, z in enumerate(z_pos):
                    g_z = z // 128
                    self[g_x, g_y, g_z] = Block.from_pitgoo_attr(
                        is_pit,
                        is_top=(ind == top_ind),
                        is_bottom=(ind == 0),
                    )
                    # If goo has totally submerged tunnels, they are not filled.
                    # Add each horizontal neighbour to the search list.
                    # If not found they'll be ignored.
                    if ind != top_ind: # Don't bother on the top level..
                        search_locs.extend([
                            (g_x - 1, g_y, g_z),
                            (g_x + 1, g_y, g_z),
                            (g_x, g_y + 1, g_z),
                            (g_x, g_y - 1, g_z),
                        ])

                # Bottomless pits don't use goo, so remove the water..
                if is_pit:
                    vmf.remove_brush(brush)

                # Indicate that this map contains goo/pits
                if is_pit:
                    has_attr[VOICE_ATTR_PIT] = True
                else:
                    has_attr[VOICE_ATTR_GOO] = True

                continue

            pos = world_to_grid(brush.get_origin(bbox_min, bbox_max))

            if bbox_max - bbox_min == (128, 128, 128):
                # Full block..
                self[pos] = Block.SOLID
            else:
                # Must be an embbedvoxel block
                self[pos] = Block.EMBED

        LOGGER.info(
            'Analysed map, filling air... ({} starting positions..)',
            len(search_locs)
        )
        self.fill_air(search_locs)
        LOGGER.info('Air filled!')
Ejemplo n.º 56
0
def res_make_catwalk(vmf: VMF, res: Property):
    """Speciallised result to generate catwalks from markers.

    Only runs once, and then quits the condition list.
    
    * Instances:
        * `markerInst: The instance set in editoritems.
        * `straight_128`/`256`/`512`: Straight sections. Extends East.
        * `corner: An L-corner piece. Connects on North and West sides.
        * `TJunction`: A T-piece. Connects on all but the East side.
        * `crossJunction`: A X-piece. Connects on all sides.
        * `end`: An end piece. Connects on the East side.
        * `stair`: A stair. Starts East and goes Up and West.
        * `end_wall`: Connects a West wall to a East catwalk.
        * `support_wall`: A support extending from the East wall.
        * `support_ceil`: A support extending from the ceiling.
        * `support_floor`: A support extending from the floor.
        * `support_goo`: A floor support, designed for goo pits.
        * `single_wall`: A section connecting to an East wall.
    """
    LOGGER.info("Starting catwalk generator...")
    marker = instanceLocs.resolve(res['markerInst'])

    instances = {
        name: instanceLocs.resolve_one(res[name, ''], error=True)
        for name in
        (
            'straight_128', 'straight_256', 'straight_512',
            'corner', 'tjunction', 'crossjunction', 'end', 'stair', 'end_wall',
            'support_wall', 'support_ceil', 'support_floor', 'support_goo',
            'single_wall',
            'markerInst',
        )
    }
    # If there are no attachments remove a catwalk piece
    instances['NONE'] = ''
    if instances['end_wall'] == '':
        instances['end_wall'] = instances['end']

    # The directions this instance is connected by (NSEW)
    links = {}  # type: Dict[Entity, Link]
    markers = {}

    # Find all our markers, so we can look them up by targetname.
    for inst in vmf.by_class['func_instance']:
        if inst['file'].casefold() not in marker:
            continue
        links[inst] = Link()
        markers[inst['targetname']] = inst

        # Snap the markers to the grid. If on glass it can become offset...
        origin = Vec.from_str(inst['origin'])
        origin = origin // 128 * 128
        origin += 64

        while brushLoc.POS['world': origin].is_goo:
            # The instance is in goo! Switch to floor orientation, and move
            # up until it's in air.
            inst['angles'] = '0 0 0'
            origin.z += 128

        inst['origin'] = str(origin)

    if not markers:
        return RES_EXHAUSTED

    LOGGER.info('Connections: {}', links)
    LOGGER.info('Markers: {}', markers)

    # First loop through all the markers, adding connecting sections
    for marker_name, inst in markers.items():
        mark_item = ITEMS[marker_name]
        mark_item.delete_antlines()
        for conn in list(mark_item.outputs):
            try:
                inst2 = markers[conn.to_item.name]
            except KeyError:
                LOGGER.warning('Catwalk connected to non-catwalk!')

            conn.remove()

            origin1 = Vec.from_str(inst['origin'])
            origin2 = Vec.from_str(inst2['origin'])
            if origin1.x != origin2.x and origin1.y != origin2.y:
                LOGGER.warning('Instances not aligned!')
                continue

            y_dir = origin1.x == origin2.x  # Which way the connection is
            if y_dir:
                dist = abs(origin1.y - origin2.y)
            else:
                dist = abs(origin1.x - origin2.x)
            vert_dist = origin1.z - origin2.z

            if (dist - 128) // 2 < abs(vert_dist):
                # The stairs are 2 long, 1 high. Check there's enough room
                # Subtract the last block though, since that's a corner.
                LOGGER.warning('Not enough room for stairs!')
                continue

            if dist > 128:
                # add straight sections in between
                place_catwalk_connections(vmf, instances, origin1, origin2)

            # Update the lists based on the directions that were set
            conn_lst1 = links[inst]
            conn_lst2 = links[inst2]
            if origin1.x < origin2.x:
                conn_lst1.E = conn_lst2.W = True
            elif origin2.x < origin1.x:
                conn_lst1.W = conn_lst2.E = True

            if origin1.y < origin2.y:
                conn_lst1.N = conn_lst2.S = True
            elif origin2.y < origin1.y:
                conn_lst1.S = conn_lst2.N = True

    for inst, dir_mask in links.items():
        # Set the marker instances based on the attached walkways.
        normal = Vec(0, 0, 1).rotate_by_str(inst['angles'])

        new_type, inst['angles'] = utils.CONN_LOOKUP[dir_mask.as_tuple()]
        inst['file'] = instances[CATWALK_TYPES[new_type]]

        if new_type is utils.CONN_TYPES.side:
            # If the end piece is pointing at a wall, switch the instance.
            if normal.z == 0:
                if normal == dir_mask.conn_dir():
                    inst['file'] = instances['end_wall']
            continue  # We never have normal supports on end pieces
        elif new_type is utils.CONN_TYPES.none:
            # Unconnected catwalks on the wall switch to a special instance.
            # This lets players stand next to a portal surface on the wall.
            if normal.z == 0:
                inst['file'] = instances['single_wall']
                inst['angles'] = INST_ANGLE[normal.as_tuple()]
            else:
                inst.remove()
            continue  # These don't get supports otherwise

        # Add regular supports
        supp = None
        if normal == (0, 0, 1):
            # If in goo, use different supports!
            origin = Vec.from_str(inst['origin'])
            origin.z -= 128
            if brushLoc.POS['world': origin].is_goo:
                supp = instances['support_goo']
            else:
                supp = instances['support_floor']
        elif normal == (0, 0, -1):
            supp = instances['support_ceil']
        else:
            supp = instances['support_wall']

        if supp:
            vmf.create_ent(
                classname='func_instance',
                origin=inst['origin'],
                angles=INST_ANGLE[normal.as_tuple()],
                file=supp,
            )

    LOGGER.info('Finished catwalk generation!')
    return RES_EXHAUSTED
Ejemplo n.º 57
0
def add_glass_floorbeams(vmf: VMF, temp_name: str):
    """Add beams to separate large glass panels.

    The texture is assumed to match plasticwall004a's shape.
    """
    template = template_brush.get_template(temp_name)
    temp_world, temp_detail, temp_over = template.visgrouped()
    try:
        [beam_template] = temp_world + temp_detail  # type: Solid
    except ValueError:
        raise ValueError('Bad Glass Floorbeam template!')

    # Grab the 'end' side, which we move around.
    for side in beam_template.sides:
        if side.normal() == (-1, 0, 0):
            beam_end_face = side
            break
    else:
        raise ValueError('Not aligned to world...')

    separation = vbsp_options.get(int, 'glass_floorbeam_sep') + 1
    separation *= 128

    # First we want to find all the groups of contiguous glass sections.
    # This is a mapping from some glass piece to its group list.
    groups = {}

    for (origin, normal), barr_type in BARRIERS.items():
        # Grating doesn't use it.
        if barr_type is not BarrierType.GLASS:
            continue

        normal = Vec(normal)

        if not normal.z:
            # Not walls.
            continue

        pos = Vec(origin) + normal * 62

        groups[pos.as_tuple()] = [pos]

    # Loop over every pos and check in the +x/y directions for another glass
    # piece. If there, merge the two lists and set every pos in the group to
    # point to the new list.
    # Once done, every unique list = a group.

    for pos_tup in groups.keys():
        pos = Vec(pos_tup)
        for off in ((128, 0, 0), (0, 128, 0)):
            neighbour = (pos + off).as_tuple()
            if neighbour in groups:
                our_group = groups[pos_tup]
                neigh_group = groups[neighbour]
                if our_group is neigh_group:
                    continue

                # Now merge the two lists. We then need to update all dict locs
                # to point to the new list.

                if len(neigh_group) > len(our_group):
                    small_group, large_group = our_group, neigh_group
                else:
                    small_group, large_group = neigh_group, our_group

                large_group.extend(small_group)
                for pos in small_group:
                    groups[pos.as_tuple()] = large_group

    # Remove duplicates objects by using the ID as key..
    groups = list({
        id(group): group
        for group in groups.values()
    }.values())

    # Side -> u, v or None

    for group in groups:

        bbox_min, bbox_max = Vec.bbox(group)
        dimensions = bbox_max - bbox_min
        LOGGER.info('Size = {}', dimensions)

        # Our beams align to the smallest axis.
        if dimensions.y > dimensions.x:
            beam_ax = 'x'
            side_ax = 'y'
            rot = Vec(0, 0, 0)
        else:
            beam_ax = 'y'
            side_ax = 'x'
            rot = Vec(0, 90, 0)

        # Build min, max tuples for each axis in the other direction.
        # This tells us where the beams will be.
        beams = {}  # type: Dict[int, Tuple[int, int]]

        # Add 128 so the first pos isn't a beam.
        offset = bbox_min[side_ax] + 128

        for pos in group:
            side_off = pos[side_ax]
            beam_off = pos[beam_ax]
            # Skip over non-'sep' positions..
            if (side_off - offset) % separation != 0:
                continue

            try:
                min_pos, max_pos = beams[side_off]
            except KeyError:
                beams[side_off] = beam_off, beam_off
            else:
                beams[side_off] = min(min_pos, beam_off), max(max_pos, beam_off)

        detail = vmf.create_ent('func_detail')

        for side_off, (min_off, max_off) in beams.items():
            for min_pos, max_pos in beam_hole_split(
                beam_ax,
                Vec.with_axes(side_ax, side_off, beam_ax, min_off, 'z', bbox_min),
                Vec.with_axes(side_ax, side_off, beam_ax, max_off, 'z', bbox_min),
            ):

                if min_pos[beam_ax] >= max_pos[beam_ax]:
                    raise ValueError(min_pos, max_pos, beam_ax)

                # Make the beam.
                # Grab the end face and snap to the length we want.
                beam_end_off = max_pos[beam_ax] - min_pos[beam_ax]
                assert beam_end_off > 0, beam_end_off
                for plane in beam_end_face.planes:
                    plane.x = beam_end_off

                new_beam = beam_template.copy(vmf_file=vmf)
                new_beam.localise(min_pos, rot)
                detail.solids.append(new_beam)
Ejemplo n.º 58
0
def res_make_tag_fizzler(vmf: VMF, inst: Entity, res: Property):
    """Add an Aperture Tag Paint Gun activation fizzler.

    These fizzlers are created via signs, and work very specially.
    MUST be priority -100 so it runs before fizzlers!
    """
    import vbsp
    if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']:
        # Abort - TAG fizzlers shouldn't appear in any other game!
        inst.remove()
        return

    fizz_base = fizz_name = None

    # Look for the fizzler instance we want to replace
    for targetname in inst.output_targets():
        if targetname in tag_fizzlers:
            fizz_name = targetname
            fizz_base = tag_fizzlers[targetname]
            del tag_fizzlers[targetname]  # Don't let other signs mod this one!
            continue
        else:
            # It's an indicator toggle, remove it and the antline to clean up.
            LOGGER.warning('Toggle: {}', targetname)
            for ent in vmf.by_target[targetname]:
                remove_ant_toggle(ent)
    inst.outputs.clear()  # Remove the outptuts now, they're not valid anyway.

    if fizz_base is None:
        # No fizzler - remove this sign
        inst.remove()
        return

    # The distance from origin the double signs are seperated by.
    sign_offset = res.int('signoffset', 16)

    sign_loc = (
        # The actual location of the sign - on the wall
        Vec.from_str(inst['origin']) +
        Vec(0, 0, -64).rotate_by_str(inst['angles'])
    )

    # Now deal with the visual aspect:
    # Blue signs should be on top.

    blue_enabled = inst.fixup.bool('$start_enabled')
    oran_enabled = inst.fixup.bool('$start_reversed')
    # If True, single-color signs will also turn off the other color.
    # This also means we always show both signs.
    # If both are enabled or disabled, this has no effect.
    disable_other = (
        not inst.fixup.bool('$disable_autorespawn', True) and
        blue_enabled != oran_enabled
    )
    # Delete fixups now, they aren't useful.
    inst.fixup.clear()

    if not blue_enabled and not oran_enabled:
        # Hide the sign in this case!
        inst.remove()

    inst_angle = srctools.parse_vec_str(inst['angles'])

    inst_normal = Vec(0, 0, 1).rotate(*inst_angle)
    loc = Vec.from_str(inst['origin'])

    if disable_other or (blue_enabled and oran_enabled):
        inst['file'] = res['frame_double']
        # On a wall, and pointing vertically
        if inst_normal.z == 0 and Vec(y=1).rotate(*inst_angle).z:
            # They're vertical, make sure blue's on top!
            blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset)
            oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset)
            # If orange is enabled, with two frames put that on top
            # instead since it's more important
            if disable_other and oran_enabled:
                blue_loc, oran_loc = oran_loc, blue_loc

        else:
            offset = Vec(0, sign_offset, 0).rotate(*inst_angle)
            blue_loc = loc + offset
            oran_loc = loc - offset
    else:
        inst['file'] = res['frame_single']
        # They're always centered
        blue_loc = loc
        oran_loc = loc

    if inst_normal.z != 0:
        # If on floors/ceilings, rotate to point at the fizzler!
        sign_floor_loc = sign_loc.copy()
        sign_floor_loc.z = 0  # We don't care about z-positions.

        # Grab the data saved earlier in res_find_potential_tag_fizzlers()
        axis, side_min, side_max, normal = tag_fizzler_locs[fizz_name]

        # The Z-axis fizzler (horizontal) must be treated differently.
        if axis == 'z':
            # For z-axis, just compare to the center point.
            # The values are really x, y, z, not what they're named.
            sign_dir = sign_floor_loc - (side_min, side_max, normal)
        else:
            # For the other two, we compare to the line,
            # or compare to the closest side (in line with the fizz)
            other_axis = 'x' if axis == 'y' else 'y'
            if abs(sign_floor_loc[other_axis] - normal) < 32:
                # Compare to the closest side. Use ** to swap x/y arguments
                # appropriately. The closest side is the one with the
                # smallest magnitude.
                vmf.create_ent(
                    classname='info_null',
                    targetname=inst['targetname'] + '_min',
                    origin=sign_floor_loc - Vec(**{
                        axis: side_min,
                        other_axis: normal,
                    }),
                )
                vmf.create_ent(
                    classname='info_null',
                    targetname=inst['targetname'] + '_max',
                    origin=sign_floor_loc - Vec(**{
                        axis: side_max,
                        other_axis: normal,
                    }),
                )
                sign_dir = min(
                    sign_floor_loc - Vec(**{
                        axis: side_min,
                        other_axis: normal,
                    }),
                    sign_floor_loc - Vec(**{
                        axis: side_max,
                        other_axis: normal,
                    }),
                    key=Vec.mag,
                )
            else:
                # Align just based on whether we're in front or behind.
                sign_dir = Vec()
                sign_dir[other_axis] = sign_floor_loc[other_axis] - normal

        sign_angle = math.degrees(
            math.atan2(sign_dir.y, sign_dir.x)
        )
        # Round to nearest 90 degrees
        # Add 45 so the switchover point is at the diagonals
        sign_angle = (sign_angle + 45) // 90 * 90

        # Rotate to fit the instances - south is down
        sign_angle = int(sign_angle + 90) % 360
        if inst_normal.z > 0:
            sign_angle = '0 {} 0'.format(sign_angle)
        elif inst_normal.z < 0:
            # Flip upside-down for ceilings
            sign_angle = '0 {} 180'.format(sign_angle)
    else:
        # On a wall, face upright
        sign_angle = PETI_INST_ANGLE[inst_normal.as_tuple()]

    # If disable_other, we show off signs. Otherwise we don't use that sign.
    blue_sign = 'blue_sign' if blue_enabled else 'blue_off_sign' if disable_other else None
    oran_sign = 'oran_sign' if oran_enabled else 'oran_off_sign' if disable_other else None

    if blue_sign:
        vmf.create_ent(
            classname='func_instance',
            file=res[blue_sign, ''],
            targetname=inst['targetname'],
            angles=sign_angle,
            origin=blue_loc.join(' '),
        )

    if oran_sign:
        vmf.create_ent(
            classname='func_instance',
            file=res[oran_sign, ''],
            targetname=inst['targetname'],
            angles=sign_angle,
            origin=oran_loc.join(' '),
        )

    # Now modify the fizzler...

    fizz_brushes = list(
        vmf.by_class['trigger_portal_cleanser'] &
        vmf.by_target[fizz_name + '_brush']
    )

    if 'base_inst' in res:
        fizz_base['file'] = resolve_inst(res['base_inst'])[0]
    fizz_base.outputs.clear()  # Remove outputs, otherwise they break
    # branch_toggle entities

    # Subtract the sign from the list of connections, but don't go below
    # zero
    fizz_base.fixup['$connectioncount'] = str(max(
        0,
        srctools.conv_int(fizz_base.fixup['$connectioncount', ''], 0) - 1
    ))

    if 'model_inst' in res:
        model_inst = resolve_inst(res['model_inst'])[0]
        for mdl_inst in vmf.by_class['func_instance']:
            if mdl_inst['targetname', ''].startswith(fizz_name + '_model'):
                mdl_inst['file'] = model_inst

    # Find the direction the fizzler front/back points - z=floor fizz
    # Signs will associate with the given side!
    bbox_min, bbox_max = fizz_brushes[0].get_bbox()
    for axis, val in zip('xyz', bbox_max-bbox_min):
        if val == 2:
            fizz_axis = axis
            sign_center = (bbox_min[axis] + bbox_max[axis]) / 2
            break
    else:
        # A fizzler that's not 128*x*2?
        raise Exception('Invalid fizzler brush ({})!'.format(fizz_name))

    # Figure out what the sides will set values to...
    pos_blue = False
    pos_oran = False
    neg_blue = False
    neg_oran = False
    if sign_loc[fizz_axis] < sign_center:
        pos_blue = blue_enabled
        pos_oran = oran_enabled
    else:
        neg_blue = blue_enabled
        neg_oran = oran_enabled

    fizz_off_tex = {
        'left': res['off_left'],
        'center': res['off_center'],
        'right': res['off_right'],
        'short': res['off_short'],
    }
    fizz_on_tex = {
        'left': res['on_left'],
        'center': res['on_center'],
        'right': res['on_right'],
        'short': res['on_short'],
    }

    # If it activates the paint gun, use different textures
    if pos_blue or pos_oran:
        pos_tex = fizz_on_tex
    else:
        pos_tex = fizz_off_tex

    if neg_blue or neg_oran:
        neg_tex = fizz_on_tex
    else:
        neg_tex = fizz_off_tex

    if vbsp.GAME_MODE == 'COOP':
        # We need ATLAS-specific triggers
        pos_trig = vmf.create_ent(
            classname='trigger_playerteam',
        )
        neg_trig = vmf.create_ent(
            classname='trigger_playerteam',
        )
        output = 'OnStartTouchBluePlayer'
    else:
        pos_trig = vmf.create_ent(
            classname='trigger_multiple',
        )
        neg_trig = vmf.create_ent(
            classname='trigger_multiple',
            spawnflags='1',
        )
        output = 'OnStartTouch'

    pos_trig['origin'] = neg_trig['origin'] = fizz_base['origin']
    pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1'  # Clients Only

    pos_trig['targetname'] = fizz_name + '-trig_pos'
    neg_trig['targetname'] = fizz_name + '-trig_neg'

    pos_trig.outputs = [
        Output(
            output,
            fizz_name + '-trig_neg',
            'Enable',
        ),
        Output(
            output,
            fizz_name + '-trig_pos',
            'Disable',
        ),
    ]

    neg_trig.outputs = [
        Output(
            output,
            fizz_name + '-trig_pos',
            'Enable',
        ),
        Output(
            output,
            fizz_name + '-trig_neg',
            'Disable',
        ),
    ]

    voice_attr = vbsp.settings['has_attr']

    if blue_enabled or disable_other:
        # If this is blue/oran only, don't affect the other color
        neg_trig.outputs.append(Output(
            output,
            '@BlueIsEnabled',
            'SetValue',
            param=srctools.bool_as_int(neg_blue),
        ))
        pos_trig.outputs.append(Output(
            output,
            '@BlueIsEnabled',
            'SetValue',
            param=srctools.bool_as_int(pos_blue),
        ))
        if blue_enabled:
            # Add voice attributes - we have the gun and gel!
            voice_attr['bluegelgun'] = True
            voice_attr['bluegel'] = True
            voice_attr['bouncegun'] = True
            voice_attr['bouncegel'] = True

    if oran_enabled or disable_other:
        neg_trig.outputs.append(Output(
            output,
            '@OrangeIsEnabled',
            'SetValue',
            param=srctools.bool_as_int(neg_oran),
        ))
        pos_trig.outputs.append(Output(
            output,
            '@OrangeIsEnabled',
            'SetValue',
            param=srctools.bool_as_int(pos_oran),
        ))
        if oran_enabled:
            voice_attr['orangegelgun'] = True
            voice_attr['orangegel'] = True
            voice_attr['speedgelgun'] = True
            voice_attr['speedgel'] = True

    if not oran_enabled and not blue_enabled:
        # If both are disabled, we must shutdown the gun when touching
        # either side - use neg_trig for that purpose!
        # We want to get rid of pos_trig to save ents
        vmf.remove_ent(pos_trig)
        neg_trig['targetname'] = fizz_name + '-trig'
        neg_trig.outputs.clear()
        neg_trig.add_out(Output(
            output,
            '@BlueIsEnabled',
            'SetValue',
            param='0'
        ))
        neg_trig.add_out(Output(
            output,
            '@OrangeIsEnabled',
            'SetValue',
            param='0'
        ))

    for fizz_brush in fizz_brushes:  # portal_cleanser ent, not solid!
        # Modify fizzler textures
        bbox_min, bbox_max = fizz_brush.get_bbox()
        for side in fizz_brush.sides():
            norm = side.normal()
            if norm[fizz_axis] == 0:
                # Not the front/back: force nodraw
                # Otherwise the top/bottom will have the odd stripes
                # which won't match the sides
                side.mat = 'tools/toolsnodraw'
                continue
            if norm[fizz_axis] == 1:
                side.mat = pos_tex[
                    vbsp.TEX_FIZZLER[
                        side.mat.casefold()
                    ]
                ]
            else:
                side.mat = neg_tex[
                    vbsp.TEX_FIZZLER[
                        side.mat.casefold()
                    ]
                ]
        # The fizzler shouldn't kill cubes
        fizz_brush['spawnflags'] = '1'

        fizz_brush.outputs.append(Output(
            'OnStartTouch',
            '@shake_global',
            'StartShake',
        ))

        fizz_brush.outputs.append(Output(
            'OnStartTouch',
            '@shake_global_sound',
            'PlaySound',
        ))

        # The triggers are 8 units thick, 24 from the center
        # (-1 because fizzlers are 2 thick on each side).
        neg_min, neg_max = Vec(bbox_min), Vec(bbox_max)
        neg_min[fizz_axis] -= 23
        neg_max[fizz_axis] -= 17

        pos_min, pos_max = Vec(bbox_min), Vec(bbox_max)
        pos_min[fizz_axis] += 17
        pos_max[fizz_axis] += 23

        if blue_enabled or oran_enabled:
            neg_trig.solids.append(
                vmf.make_prism(
                    neg_min,
                    neg_max,
                    mat='tools/toolstrigger',
                ).solid,
            )
            pos_trig.solids.append(
                vmf.make_prism(
                    pos_min,
                    pos_max,
                    mat='tools/toolstrigger',
                ).solid,
            )
        else:
            # If neither enabled, use one trigger
            neg_trig.solids.append(
                vmf.make_prism(
                    neg_min,
                    pos_max,
                    mat='tools/toolstrigger',
                ).solid,
            )
Ejemplo n.º 59
0
def res_unst_scaffold(vmf: VMF, res: Property):
    """The condition to generate Unstationary Scaffolds.

    This is executed once to modify all instances.
    """
    # The instance types we're modifying
    if res.value not in SCAFFOLD_CONFIGS:
        # We've already executed this config group
        return RES_EXHAUSTED

    LOGGER.info(
        'Running Scaffold Generator ({})...',
        res.value
    )
    inst_to_config, LINKS = SCAFFOLD_CONFIGS[res.value]
    del SCAFFOLD_CONFIGS[res.value]  # Don't let this run twice

    chains = item_chain.chain(vmf, inst_to_config.keys(), allow_loop=False)

    # We need to make the link entities unique for each scaffold set,
    # otherwise the AllVar property won't work.

    for group_counter, node_list in enumerate(chains):
        # Set all the instances and properties
        start_inst = node_list[0].item.inst
        for vals in LINKS.values():
            if vals['all'] is not None:
                start_inst.fixup[vals['all']] = SCAFF_PATTERN.format(
                    name=vals['name'],
                    group=group_counter,
                    index='*',
                )

        should_reverse = srctools.conv_bool(start_inst.fixup['$start_reversed'])

        # Stash this off to start, so we can find this after items are processed
        # and the instance names change.
        for node in node_list:
            node.conf = inst_to_config[node.inst['file'].casefold()]

        # Now set each instance in the chain, including first and last
        for index, node in enumerate(node_list):
            conf = node.conf
            orient, offset = get_config(node)

            if node.prev is None:
                link_type = LinkType.START
            elif node.next is None:
                link_type = LinkType.END
            else:
                link_type = LinkType.MID

            # Special case - add an extra instance for the ends, pointing
            # in the direction
            # of the connected track. This would be the endcap
            # model.
            placed_endcap = False
            if (
                    orient == 'floor' and
                    link_type is not LinkType.MID and
                    conf['inst_end'] is not None
                    ):
                if link_type is LinkType.START:
                    other_node = node.next
                else:
                    other_node = node.prev

                other_offset = get_config(other_node)[1]
                link_dir = other_offset - offset

                # Compute the horizontal gradient (z / xy dist).
                # Don't use endcap if rising more than ~45 degrees, or lowering
                # more than ~12 degrees.
                # If
                horiz_dist = math.sqrt(link_dir.x ** 2 + link_dir.y ** 2)
                if horiz_dist != 0 and -0.15 <= (link_dir.z / horiz_dist) <= 1:
                    link_ang = math.degrees(
                        math.atan2(link_dir.y, link_dir.x)
                    )
                    # Round to nearest 90 degrees
                    # Add 45 so the switchover point is at the diagonals
                    link_ang = (link_ang + 45) // 90 * 90
                    vbsp.VMF.create_ent(
                        classname='func_instance',
                        targetname=node.inst['targetname'],
                        file=conf['inst_end'],
                        origin=offset,
                        angles='0 {:.0f} 0'.format(link_ang),
                    )
                    # Don't place the offset instance, this replaces that!
                    placed_endcap = True

            if not placed_endcap and conf['inst_offset'] is not None:
                # Add an additional rotated entity at the offset.
                # This is useful for the piston item.
                vbsp.VMF.create_ent(
                    classname='func_instance',
                    targetname=node.inst['targetname'],
                    file=conf['inst_offset'],
                    origin=offset,
                    angles=node.inst['angles'],
                )

            logic_inst = vmf.create_ent(
                classname='func_instance',
                targetname=node.inst['targetname'],
                file=conf.get(
                    'logic_' + link_type.value + (
                        '_rev' if
                        should_reverse
                        else ''
                        ),
                    '',
                ),
                origin=offset,
                angles=(
                    '0 0 0' if
                    conf['rotate_logic']
                    else node.inst['angles']
                ),
            )
            for key, val in node.inst.fixup.items():
                # Copy over fixup values
                logic_inst.fixup[key] = val

            # Add the link-values
            for linkVar, link in LINKS.items():
                logic_inst.fixup[linkVar] = SCAFF_PATTERN.format(
                    name=link['name'],
                    group=group_counter,
                    index=index,
                )
                if node.next is not None:
                    logic_inst.fixup[link['next']] = SCAFF_PATTERN.format(
                        name=link['name'],
                        group=group_counter,
                        index=index + 1,
                    )

            new_file = conf.get('inst_' + orient, '')
            if new_file != '':
                node.inst['file'] = new_file

    LOGGER.info('Finished Scaffold generation!')
    return RES_EXHAUSTED