Exemple #1
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
Exemple #2
0
def flag_voice_char(flag: Property) -> bool:
    """Checks to see if the given charcter is present in the voice pack.

    `<NONE>` means no voice pack is chosen.
    This is case-insensitive, and allows partial matches - 'Cave' matches
    a voice pack with 'Cave Johnson'.
    """
    targ_char = flag.value.casefold()
    if targ_char == '<none>':
        return vbsp_options.get(str, 'voice_id') == '<NONE>'
    for char in vbsp_options.get(str, 'voice_char').split(','):
        if targ_char in char.casefold():
            return True
    return False
Exemple #3
0
def flag_voice_char(flag: Property):
    """Checks to see if the given charcter is present in the voice pack.

    "<NONE>" means no voice pack is chosen.
    This is case-insensitive, and allows partial matches - 'Cave' matches
    a voice pack with 'Cave Johnson'.
    """
    targ_char = flag.value.casefold()
    if targ_char == '<none>':
        return vbsp_options.get(str, 'voice_id') == '<NONE>'
    for char in vbsp_options.get(str, 'voice_char').split(','):
        if targ_char in char.casefold():
            return True
    return False
Exemple #4
0
def res_cust_antline_setup(res: Property):
    def find(cat):
        """Helper to reduce code duplication."""
        return [p.value for p in res.find_all(cat)]

    # Allow overriding these options. If unset use the style's value - the
    # amount of destruction will usually be the same.
    broken_chance = res.float(
        'broken_antline_chance',
        vbsp_options.get(float, 'broken_antline_chance'),
    )
    broken_dist = res.int(
        'broken_antline_distance',
        vbsp_options.get(int, 'broken_antline_distance'),
    )

    toggle_inst = res['instance', '']
    toggle_out = list(res.find_all('addOut'))

    # These textures are required - the base ones.
    straight_tex = find('straight')
    corner_tex = find('corner')

    # Arguments to pass to setAntlineMat
    straight_args = [
        straight_tex,
        find('straightFloor') or (),
        # Extra broken antline textures / options, if desired.
        broken_chance,
        broken_dist,
        find('brokenStraight') or (),
        find('brokenStraightFloor') or (),
    ]

    # The same but for corners.
    corner_args = [
        corner_tex,
        find('cornerFloor') or (),
        broken_chance,
        broken_dist,
        find('brokenCorner') or (),
        find('brokenCornerFloor') or (),
    ]

    if not straight_tex or not corner_tex:
        # If we don't have two textures, something's wrong. Remove this result.
        LOGGER.warning('custAntline has no textures!')
        return None
    else:
        return straight_args, corner_args, toggle_inst, toggle_out
Exemple #5
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
Exemple #6
0
def res_pre_cache_model(res: Property):
    """Precache the given model for switching.

    This places it as a prop_dynamic_override.
    """
    mdl_name = res.value.casefold()
    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)
    vbsp.VMF.create_ent(
        classname='prop_dynamic_override',
        targetname='@precache',
        origin=vbsp_options.get(Vec, 'global_pti_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.
    )
Exemple #7
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 HAS_MONITOR 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
Exemple #8
0
def mon_camera_script() -> None:
    """Write out a script containing the arguments to the camera."""
    active_counts = [
        srctools.conv_int(cam.inst.fixup['$start_enabled', '0'])
        for cam in
        ALL_CAMERAS
    ]

    with open(MON_ARGS_SCRIPT, 'w') as scr:
        scr.write('CAM_NUM <- {};\n'.format(len(ALL_CAMERAS)))
        scr.write('CAM_ACTIVE_NUM <- {};\n'.format(sum(active_counts)))
        scr.write('CAM_ACTIVE <- {!r};\n'.format(active_counts))
        scr.write('CAM_LOC <- [\n')
        scr.write(',\n'.join([
            ' Vector({0.x:.3f}, {0.y:.3f}, {0.z:.3f})'.format(cam.cam_pos)
            for cam in ALL_CAMERAS
        ]))
        scr.write('\n];\n')
        scr.write('CAM_ANGLES <- [\n')
        scr.write(',\n'.join([
            ' Vector({0.x:.3f}, {0.y:.3f})'.format(cam.cam_angles)
            for cam in ALL_CAMERAS
        ]))
        scr.write('\n];\n')

        if vbsp_options.get(str, 'voice_studio_inst'):
            # We have a voice studio, send values to the script.
            scr.write(
                'CAM_STUDIO_LOC <- Vector({0.x:.3f}, '
                '{0.y:.3f}, {0.z:.3f});\n'.format(get_studio_pose()),
            )
            scr.write(
                'CAM_STUDIO_CHANCE <- {chance};\n'
                'CAM_STUDIO_PITCH <- {pitch};\n'
                'CAM_STUDIO_YAW <- {yaw};\n'
                'CAM_STUDIO_TURRET <- {turret!r};\n'.format(
                    chance=vbsp_options.get(float, 'voice_studio_inter_chance'),
                    pitch=vbsp_options.get(float, 'voice_studio_cam_pitch'),
                    yaw=vbsp_options.get(float, 'voice_studio_cam_yaw'),
                    turret='1' if MONITOR_RELATIONSHIP_ENTS else '0',
                )
            )
        else:
            scr.write(
                'CAM_STUDIO_CHANCE <- -1;\n'
                'CAM_STUDIO_TURRET <- 0;\n'
            )
Exemple #9
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
Exemple #10
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
Exemple #11
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
Exemple #12
0
def res_find_potential_tag_fizzlers(vmf: VMF, inst: Entity):
    """We need to know which items are 'real' fizzlers.

    This is used for Aperture Tag paint fizzlers.
    """
    if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']:
        return RES_EXHAUSTED

    if inst['file'].casefold() not in instanceLocs.resolve(
            '<ITEM_BARRIER_HAZARD:0>'):
        return

    # The key list in the dict will be a set of all fizzler items!
    tag_fizzlers[inst['targetname']] = inst

    if tag_fizzler_locs:  # Only loop through fizzlers once.
        return

    # Determine the origins by first finding the bounding box of the brushes,
    # then averaging.
    for fizz in vmf.by_class['trigger_portal_cleanser']:
        name = fizz['targetname'][:-6]  # Strip off '_brush'
        bbox_min, bbox_max = fizz.get_bbox()
        if name in tag_fizzler_locs:
            orig_min, orig_max = tag_fizzler_locs[name]
            orig_min.min(bbox_min)
            orig_max.max(bbox_max)
        else:
            tag_fizzler_locs[name] = bbox_min, bbox_max

    for name, (s, l) in tag_fizzler_locs.items():
        # Figure out how to compare for this brush.
        # If it's horizontal, signs should point to the center:
        if abs(s.z - l.z) == 2:
            tag_fizzler_locs[name] = (
                'z',
                s.x + l.x / 2,
                s.y + l.y / 2,
                s.z + 1,
            )
            continue
        # For the vertical directions, we want to compare based on the line segment.
        if abs(s.x - l.x) == 2:  # Y direction
            tag_fizzler_locs[name] = (
                'y',
                s.y,
                l.y,
                s.x + 1,
            )
        else:  # Extends in X direction
            tag_fizzler_locs[name] = (
                'x',
                s.x,
                l.x,
                s.y + 1,
            )
Exemple #13
0
def res_monitor(inst: Entity, res: Property) -> None:
    """Result for the monitor component.

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

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

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

    HAS_MONITOR = True

    has_laser = vbsp.settings['has_attr']['laser']
    # Allow turrets if the monitor is setup to allow it, and the actor should
    # be shot.
    needs_turret = bullseye_name and vbsp_options.get(bool, 'voice_studio_should_shoot')

    inst.fixup['$is_breakable'] = has_laser or needs_turret

    # We need to generate an ai_relationship, which makes turrets hate
    # a bullseye.
    if needs_turret:
        loc = Vec(bullseye_loc)
        loc.localise(
            Vec.from_str(inst['origin']),
            Vec.from_str(inst['angles']),
        )
        bullseye_name = local_name(inst, bullseye_name)
        inst.map.create_ent(
            classname='npc_bullseye',
            targetname=bullseye_name,
            parentname=local_name(inst, bullseye_parent),
            spawnflags=221186,  # Non-solid, invisible, etc..
            origin=loc,
        )
        relation = inst.map.create_ent(
            classname='ai_relationship',
            targetname='@monitor_turr_hate',
            parentname=bullseye_name,  # When killed, destroy this too.
            spawnflags=2,  # Notify turrets about monitor locations
            disposition=1,  # Hate
            origin=loc,
            subject='npc_portal_turret_floor',
            target=bullseye_name,
        )
        MONITOR_RELATIONSHIP_ENTS.append(relation)
Exemple #14
0
def res_find_potential_tag_fizzlers(vmf: VMF, inst: Entity):
    """We need to know which items are 'real' fizzlers.

    This is used for Aperture Tag paint fizzlers.
    """
    if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']:
        return RES_EXHAUSTED

    if inst['file'].casefold() not in resolve_inst('<ITEM_BARRIER_HAZARD:0>'):
        return

    # The key list in the dict will be a set of all fizzler items!
    tag_fizzlers[inst['targetname']] = inst

    if tag_fizzler_locs:  # Only loop through fizzlers once.
        return

    # Determine the origins by first finding the bounding box of the brushes,
    # then averaging.
    for fizz in vmf.by_class['trigger_portal_cleanser']:
        name = fizz['targetname'][:-6]  # Strip off '_brush'
        bbox_min, bbox_max = fizz.get_bbox()
        if name in tag_fizzler_locs:
            orig_min, orig_max = tag_fizzler_locs[name]
            orig_min.min(bbox_min)
            orig_max.max(bbox_max)
        else:
            tag_fizzler_locs[name] = bbox_min, bbox_max

    for name, (s, l) in tag_fizzler_locs.items():
        # Figure out how to compare for this brush.
        # If it's horizontal, signs should point to the center:
        if abs(s.z - l.z) == 2:
            tag_fizzler_locs[name] =(
                'z',
                s.x + l.x / 2,
                s.y + l.y / 2,
                s.z + 1,
            )
            continue
        # For the vertical directions, we want to compare based on the line segment.
        if abs(s.x - l.x) == 2:  # Y direction
            tag_fizzler_locs[name] = (
                'y',
                s.y,
                l.y,
                s.x + 1,
            )
        else:  # Extends in X direction
            tag_fizzler_locs[name] = (
                'x',
                s.x,
                l.x,
                s.y + 1,
            )
Exemple #15
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
Exemple #16
0
def res_cave_portrait(inst: Entity, res: Property):
    """A variant of AddOverlay for adding Cave Portraits.

    If the set quote pack is not Cave Johnson, this does nothing.
    Otherwise, this overlays an instance, setting the $skin variable
    appropriately.
    """
    skin = vbsp_options.get(int, 'cave_port_skin')
    if skin is not None:
        new_inst = res_add_overlay_inst(inst, res)
        new_inst.fixup['$skin'] = skin
Exemple #17
0
def res_cave_portrait(inst: Entity, res: Property):
    """A variant of AddOverlay for adding Cave Portraits.

    If the set quote pack is not Cave Johnson, this does nothing.
    Otherwise, this overlays an instance, setting the $skin variable
    appropriately.
    """
    skin = vbsp_options.get(int, "cave_port_skin")
    if skin is not None:
        new_inst = res_add_overlay_inst(inst, res)
        new_inst.fixup["$skin"] = skin
def res_monitor(inst: Entity, res: Property):
    """Result for the monitor component.

    """
    global NEEDS_TURRET
    import vbsp

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

    ALL_MONITORS.append(Monitor(inst))

    has_laser = vbsp.settings['style_vars'].get('haslaser', False)
    # Allow turrets if the monitor is setup to allow it, and the actor should
    # be shot.
    needs_turret = bullseye_name and vbsp_options.get(
        bool, 'voice_studio_should_shoot')

    inst.fixup['$is_breakable'] = has_laser or needs_turret

    # We need to generate an ai_relationship, which makes turrets hate
    # a bullseye.
    if needs_turret:
        loc = Vec(bullseye_loc)
        loc.localise(
            Vec.from_str(inst['origin']),
            Vec.from_str(inst['angles']),
        )
        bullseye_name = local_name(inst, bullseye_name)
        inst.map.create_ent(
            classname='npc_bullseye',
            targetname=bullseye_name,
            parentname=local_name(inst, bullseye_parent),
            spawnflags=221186,  # Non-solid, invisible, etc..
            origin=loc,
        )
        inst.map.create_ent(
            classname='ai_relationship',
            targetname='@monitor_turr_hate',
            spawnflags=2,  # Notify turrets about monitor locations
            disposition=1,  # Hate
            origin=loc,
            subject='npc_portal_turret_floor',
            target=bullseye_name,
        )

        NEEDS_TURRET = True
Exemple #19
0
def beam_hole_split(axis: str, min_pos: Vec, max_pos: Vec):
    """Break up floor beams to fit around holes."""

    # Go along the shape. For each point, check if a hole is present,
    # and split at that.
    # Our positions are centered, but we return ones at the ends.

    # Inset in 4 units from each end to not overlap with the frames.
    start_pos = min_pos - Vec.with_axes(axis, 60)
    if HOLES:
        hole_size_large = vbsp_options.get(float, 'glass_hole_size_large') / 2
        hole_size_small = vbsp_options.get(float, 'glass_hole_size_small') / 2

        # Extract normal from the z-axis.
        grid_height = min_pos.z // 128 * 128 + 64
        if grid_height < min_pos.z:
            normal = (0, 0, 1)
        else:
            normal = (0, 0, -1)
        import vbsp
        for pos in min_pos.iter_line(max_pos, 128):
            try:
                hole_type = HOLES[(pos.x, pos.y, grid_height), normal]
            except KeyError:
                continue
            else:
                if hole_type is HoleType.SMALL:
                    size = hole_size_small
                elif hole_type is HoleType.LARGE:
                    size = hole_size_large
                else:
                    raise AssertionError(hole_type)

                yield start_pos, pos - Vec.with_axes(axis, size)
                start_pos = pos + Vec.with_axes(axis, size)

    # Last segment, or all if no holes.
    yield start_pos, max_pos + Vec.with_axes(axis, 60)
Exemple #20
0
def beam_hole_split(axis: str, min_pos: Vec, max_pos: Vec):
    """Break up floor beams to fit around holes."""

    # Go along the shape. For each point, check if a hole is present,
    # and split at that.
    # Our positions are centered, but we return ones at the ends.

    # Inset in 4 units from each end to not overlap with the frames.
    start_pos = min_pos - Vec.with_axes(axis, 60)
    if HOLES:
        hole_size_large = vbsp_options.get(float, 'glass_hole_size_large') / 2
        hole_size_small = vbsp_options.get(float, 'glass_hole_size_small') / 2

        # Extract normal from the z-axis.
        grid_height = min_pos.z // 128 * 128 + 64
        if grid_height < min_pos.z:
            normal = (0, 0, 1)
        else:
            normal = (0, 0, -1)
        import vbsp
        for pos in min_pos.iter_line(max_pos, 128):
            try:
                hole_type = HOLES[(pos.x, pos.y, grid_height), normal]
            except KeyError:
                continue
            else:
                if hole_type is HoleType.SMALL:
                    size = hole_size_small
                elif hole_type is HoleType.LARGE:
                    size = hole_size_large
                else:
                    raise AssertionError(hole_type)

                yield start_pos, pos - Vec.with_axes(axis, size)
                start_pos = pos + Vec.with_axes(axis, size)

    # Last segment, or all if no holes.
    yield start_pos, max_pos + Vec.with_axes(axis, 60)
Exemple #21
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',
        )
Exemple #22
0
def read_configs(conf: Property) -> None:
    """Read in the fizzler data."""
    for fizz_conf in conf.find_all('Fizzlers', 'Fizzler'):
        fizz = FizzlerType.parse(fizz_conf)

        if fizz.id in FIZZ_TYPES:
            raise ValueError('Duplicate fizzler ID "{}"'.format(fizz.id))

        FIZZ_TYPES[fizz.id] = fizz

    LOGGER.info('Loaded {} fizzlers.', len(FIZZ_TYPES))

    if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['APTAG']:
        return
    # In Aperture Tag, we don't have portals. For fizzler types which block
    # portals (trigger_portal_cleanser), additionally fizzle paint.
    for fizz in FIZZ_TYPES.values():
        for brush in fizz.brushes:
            if brush.keys['classname'].casefold() == 'trigger_portal_cleanser':
                brush_name = brush.name
                # Retrieve what key is used for start-disabled.
                brush_start_disabled = None
                for key_map in [brush.keys, brush.local_keys]:
                    if brush_start_disabled is None:
                        for key, value in key_map.items():
                            if key.casefold() == 'startdisabled':
                                brush_start_disabled = value
                                break
                break  # Jump past else.
        else:
            # No fizzlers in this item.
            continue

        # Add a paint fizzler brush to these fizzlers.
        fizz.brushes.append(
            FizzlerBrush(
                brush_name,
                textures={
                    TexGroup.TRIGGER: const.Tools.TRIGGER,
                },
                keys={
                    'classname': 'trigger_paint_cleanser',
                    'startdisabled': brush_start_disabled or '0',
                    'spawnflags': '9',
                },
                local_keys={},
                outputs=[],
                singular=True,
            ))
Exemple #23
0
def parse_map(vmf: VMF, has_attr: Dict[str, bool]) -> None:
    """Remove instances from the map, and store off the positions."""
    glass_inst = resolve_one('[glass_128]')

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

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

    for inst in vmf.by_class['func_instance']:
        filename = inst['file'].casefold()
        if filename == glass_inst:
            inst.remove()

    if vbsp_options.get(str, 'glass_pack') and has_attr['glass']:
        packing.pack_list(vmf, vbsp_options.get(str, 'glass_pack'))
Exemple #24
0
def parse_map(vmf: VMF, has_attr: Dict[str, bool]) -> None:
    """Remove instances from the map, and store off the positions."""
    glass_inst = resolve_one('[glass_128]')

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

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

    for inst in vmf.by_class['func_instance']:
        filename = inst['file'].casefold()
        if filename == glass_inst:
            inst.remove()

    if vbsp_options.get(str, 'glass_pack') and has_attr['glass']:
        packing.pack_list(vmf, vbsp_options.get(str, 'glass_pack'))
Exemple #25
0
def res_monitor(inst: Entity, res: Property) -> None:
    """Result for the monitor component.

    """
    import vbsp

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

    ALL_MONITORS.append(Monitor(inst))

    has_laser = vbsp.settings['has_attr']['laser']
    # Allow turrets if the monitor is setup to allow it, and the actor should
    # be shot.
    needs_turret = bullseye_name and vbsp_options.get(bool, 'voice_studio_should_shoot')

    inst.fixup['$is_breakable'] = has_laser or needs_turret

    # We need to generate an ai_relationship, which makes turrets hate
    # a bullseye.
    if needs_turret:
        loc = Vec(bullseye_loc)
        loc.localise(
            Vec.from_str(inst['origin']),
            Vec.from_str(inst['angles']),
        )
        bullseye_name = local_name(inst, bullseye_name)
        inst.map.create_ent(
            classname='npc_bullseye',
            targetname=bullseye_name,
            parentname=local_name(inst, bullseye_parent),
            spawnflags=221186,  # Non-solid, invisible, etc..
            origin=loc,
        )
        relation = inst.map.create_ent(
            classname='ai_relationship',
            targetname='@monitor_turr_hate',
            spawnflags=2,  # Notify turrets about monitor locations
            disposition=1,  # Hate
            origin=loc,
            subject='npc_portal_turret_floor',
            target=bullseye_name,
        )
        MONITOR_RELATIONSHIP_ENTS.append(relation)
Exemple #26
0
def read_configs(conf: Property) -> None:
    """Read in the fizzler data."""
    for fizz_conf in conf.find_all('Fizzlers', 'Fizzler'):
        fizz = FizzlerType.parse(fizz_conf)

        if fizz.id in FIZZ_TYPES:
            raise ValueError('Duplicate fizzler ID "{}"'.format(fizz.id))

        FIZZ_TYPES[fizz.id] = fizz

    LOGGER.info('Loaded {} fizzlers.', len(FIZZ_TYPES))

    if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['APTAG']:
        return
    # In Aperture Tag, we don't have portals. For fizzler types which block
    # portals (trigger_portal_cleanser), additionally fizzle paint.
    for fizz in FIZZ_TYPES.values():
        for brush in fizz.brushes:
            if brush.keys['classname'].casefold() == 'trigger_portal_cleanser':
                brush_name = brush.name
                # Retrieve what key is used for start-disabled.
                brush_start_disabled = None
                for key_map in [brush.keys, brush.local_keys]:
                    if brush_start_disabled is None:
                        for key, value in key_map.items():
                            if key.casefold() == 'startdisabled':
                                brush_start_disabled = value
                                break
                break  # Jump past else.
        else:
            # No fizzlers in this item.
            continue

        # Add a paint fizzler brush to these fizzlers.
        fizz.brushes.append(FizzlerBrush(
            brush_name,
            textures={
                TexGroup.TRIGGER: const.Tools.TRIGGER,
            },
            keys={
                'classname': 'trigger_paint_cleanser',
                'startdisabled': brush_start_disabled or '0',
                'spawnflags': '9',
            },
            local_keys={},
            outputs=[],
            singular=True,
        ))
Exemple #27
0
def ap_tag_modifications(vmf: VMF):
    """Perform modifications for Aperture Tag.
    
    * Paint is always present in every map!
    * Suppress ATLAS's Portalgun in coop.
    * In singleplayer, override the transition ent instance to have the Gel Gun.
    * Create subdirectories with the user's steam ID to fix a workshop compile bug.
    """
    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

    transition_ents = instanceLocs.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 puzzle folder "{}"', new_folder)
            os.makedirs(
                new_folder,
                exist_ok=True,
            )
Exemple #28
0
def ap_tag_modifications(vmf: VMF):
    """Perform modifications for Aperture Tag.
    
    * Paint is always present in every map!
    * Suppress ATLAS's Portalgun in coop.
    * In singleplayer, override the transition ent instance to have the Gel Gun.
    * Create subdirectories with the user's steam ID to fix a workshop compile bug.
    """
    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

    transition_ents = instanceLocs.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 puzzle folder "{}"', new_folder)
            os.makedirs(
                new_folder,
                exist_ok=True,
            )
Exemple #29
0
def flag_game(flag: Property) -> bool:
    """Checks which game is being modded.

    Accepts the following aliases instead of a Steam ID:
    - PORTAL2
    - APTAG
    - ALATAG
    - TAG
    - Aperture Tag
    - TWTM,
    - Thinking With Time Machine
    - DEST_AP
    - Destroyed Aperture
    """
    return vbsp_options.get(str, 'game_id') == utils.STEAM_IDS.get(
        flag.value.upper(),
        flag.value,
    )
Exemple #30
0
def flag_game(flag: Property):
    """Checks which game is being modded.

    Accepts the following aliases instead of a Steam ID:
     - PORTAL2
     - APTAG
     - ALATAG
     - TAG
     - Aperture Tag
     - TWTM,
     - Thinking With Time Machine
     - DEST_AP
     - Destroyed Aperture
    """
    return vbsp_options.get(str, 'game_id') == utils.STEAM_IDS.get(
        flag.value.upper(),
        flag.value,
    )
Exemple #31
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
Exemple #32
0
def flag_game(flag: Property) -> bool:
    """Checks which game is being modded.

    Accepts the following aliases instead of a Steam ID:

    - `PORTAL2`
    - `APTAG`
    - `ALATAG`
    - `TAG`
    - `Aperture Tag`
    - `TWTM`
    - `Thinking With Time Machine`
    - `DEST_AP`
    - `Destroyed Aperture`
    """
    return vbsp_options.get(str, 'game_id') == utils.STEAM_IDS.get(
        flag.value.upper(),
        flag.value,
    )
Exemple #33
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
Exemple #34
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,
    )
Exemple #35
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,
    )
def res_add_global_inst(res: Property):
    """Add one instance in a specific location.

    Options:

    - `allow_multiple`: Allow multiple copies of this instance. If 0, the
        instance will not be added if it was already added.
    - `name`: The targetname of the instance. If blank, the instance will
          be given a name of the form `inst_1234`.
    - `file`: The filename for the instance.
    - `angles`: The orientation of the instance (defaults to `0 0 0`).
    - `fixup_style`: The Fixup style for the instance. `0` (default) is
        Prefix, `1` is Suffix, and `2` is None.
    - `position`: The location of the instance. If not set, it will be placed
        in a 128x128 nodraw room somewhere in the map. Objects which can
        interact with nearby object should not be placed there.
    """
    if not res.has_children():
        res = Property('AddGlobal', [Property('File', res.value)])

    if res.bool('allow_multiple') or res['file'] not in GLOBAL_INSTANCES:
        # By default we will skip adding the instance
        # if was already added - this is helpful for
        # items that add to original items, or to avoid
        # bugs.
        new_inst = vbsp.VMF.create_ent(
            classname="func_instance",
            targetname=res['name', ''],
            file=instanceLocs.resolve_one(res['file'], error=True),
            angles=res['angles', '0 0 0'],
            fixup_style=res['fixup_style', '0'],
        )
        try:
            new_inst['origin'] = res['position']
        except IndexError:
            new_inst['origin'] = vbsp_options.get(Vec, 'global_ents_loc')
        GLOBAL_INSTANCES.add(res['file'])
        if new_inst['targetname'] == '':
            new_inst['targetname'] = "inst_"
            new_inst.make_unique()
    return RES_EXHAUSTED
Exemple #37
0
def res_add_global_inst(res: Property):
    """Add one instance in a specific location.

    Options:
        `allow_multiple`: Allow multiple copies of this instance. If 0, the
            instance will not be added if it was already added.
        `name`: The targetname of the instance. If blank, the instance will
              be given a name of the form `inst_1234`.
        `file`: The filename for the instance.
        `angles`: The orientation of the instance (defaults to `0 0 0`).
        `fixup_style`: The Fixup style for the instance. `0` (default) is
            Prefix, `1` is Suffix, and `2` is None.
        `position`: The location of the instance. If not set, it will be placed
            in a 128x128 nodraw room somewhere in the map. Objects which can
            interact with nearby object should not be placed there.
    """
    if not res.has_children():
        res = Property('AddGlobal', [Property('File', res.value)])

    if res.bool('allow_multiple') or res['file'] not in GLOBAL_INSTANCES:
        # By default we will skip adding the instance
        # if was already added - this is helpful for
        # items that add to original items, or to avoid
        # bugs.
        new_inst = vbsp.VMF.create_ent(
            classname="func_instance",
            targetname=res['name', ''],
            file=instanceLocs.resolve_one(res['file'], error=True),
            angles=res['angles', '0 0 0'],
            fixup_style=res['fixup_style', '0'],
        )
        try:
            new_inst['origin'] = res['position']
        except IndexError:
            new_inst['origin'] = vbsp_options.get(Vec, 'global_ents_loc')
        GLOBAL_INSTANCES.add(res['file'])
        if new_inst['targetname'] == '':
            new_inst['targetname'] = "inst_"
            new_inst.make_unique()
    return RES_EXHAUSTED
Exemple #38
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)
Exemple #39
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!')
Exemple #40
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,
            )
Exemple #41
0
def get_studio_pose() -> Vec:
    """Return the position of the studio camera."""
    return voiceLine.get_studio_loc() + vbsp_options.get(Vec, 'voice_studio_cam_loc')
Exemple #42
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,
            )
Exemple #43
0
def add_voice(
        has_items: dict,
        style_vars_: dict,
        vmf_file_: VMF,
        map_seed: str,
        use_priority=True,
):
    """Add a voice line to the map."""
    global ALLOW_MID_VOICES, vmf_file, map_attr, style_vars
    LOGGER.info('Adding Voice Lines!')

    vmf_file = vmf_file_
    map_attr = has_items
    style_vars = style_vars_

    norm_config = ConfigFile('bee2/voice.cfg', in_conf_folder=False)
    mid_config = ConfigFile('bee2/mid_voice.cfg', in_conf_folder=False)

    quote_base = QUOTE_DATA['base', False]
    quote_loc = get_studio_loc()
    if quote_base:
        LOGGER.info('Adding Base instance!')
        vmf_file.create_ent(
            classname='func_instance',
            targetname='voice',
            file=INST_PREFIX + quote_base,
            angles='0 0 0',
            origin=quote_loc,
            fixup_style='0',
        )

    # Either box in with nodraw, or place the voiceline studio.
    has_studio = conditions.monitor.make_voice_studio(vmf_file)

    bullsye_actor = vbsp_options.get(str, 'voice_studio_actor')
    if bullsye_actor and has_studio:
        ADDED_BULLSEYES.add(bullsye_actor)

    global_bullseye = QUOTE_DATA['bullseye', '']
    if global_bullseye:
        add_bullseye(quote_loc, global_bullseye)

    ALLOW_MID_VOICES = not style_vars.get('nomidvoices', False)

    mid_quotes = []

    # Enable using the beep before and after choreo lines.
    allow_dings = srctools.conv_bool(QUOTE_DATA['use_dings', '0'])
    if allow_dings:
        vmf_file.create_ent(
            classname='logic_choreographed_scene',
            targetname='@ding_on',
            origin=quote_loc + (-8, -16, 0),
            scenefile='scenes/npc/glados_manual/ding_on.vcd',
            busyactor="1",  # Wait for actor to stop talking
            onplayerdeath='0',
        )
        vmf_file.create_ent(
            classname='logic_choreographed_scene',
            targetname='@ding_off',
            origin=quote_loc + (8, -16, 0),
            scenefile='scenes/npc/glados_manual/ding_off.vcd',
            busyactor="1",  # Wait for actor to stop talking
            onplayerdeath='0',
        )

    # QuoteEvents allows specifying an instance for particular items,
    # so a voice line can be played at a certain time. It's only active
    # in certain styles, but uses the default if not set.
    for event in QUOTE_DATA.find_all('QuoteEvents', 'Event'):
        event_id = event['id', ''].casefold()
        # We ignore the config if no result was executed.
        if event_id and event_id in QUOTE_EVENTS:
            # Instances from the voiceline config are in this subfolder,
            # but not the default item - that's set from the conditions
            QUOTE_EVENTS[event_id] = INST_PREFIX + event['file']

    LOGGER.info('Quote events: {}', list(QUOTE_EVENTS.keys()))

    if has_responses():
        LOGGER.info('Generating responses data..')
        with open(RESP_LOC, 'w') as f:
            generate_resp_script(f, allow_dings)
    else:
        LOGGER.info('No responses data..')
        try:
            os.remove(RESP_LOC)
        except FileNotFoundError:
            pass

    for ind, file in enumerate(QUOTE_EVENTS.values()):
        if not file:
            continue
        vmf_file.create_ent(
            classname='func_instance',
            targetname='voice_event_' + str(ind),
            file=file,
            angles='0 0 0',
            origin=quote_loc,
            fixup_style='0',
        )

    # Determine the flags that enable/disable specific lines based on which
    # players are used.
    player_model = vbsp.BEE2_config.get_val(
        'General', 'player_model', 'PETI',
    ).casefold()

    is_coop = (vbsp.GAME_MODE == 'COOP')
    is_sp = (vbsp.GAME_MODE == 'SP')

    player_flags = {
        'sp': is_sp,
        'coop': is_coop,
        'atlas': is_coop or player_model == 'atlas',
        'pbody': is_coop or player_model == 'pbody',
        'bendy': is_sp and player_model == 'peti',
        'chell': is_sp and player_model == 'sp',
        'human': is_sp and player_model in ('peti', 'sp'),
        'robot': is_coop or player_model in ('atlas', 'pbody'),
    }
    # All which are True.
    player_flag_set = {val for val, flag in player_flags.items() if flag}

    # For each group, locate the voice lines.
    for group in itertools.chain(
        QUOTE_DATA.find_all('group'),
        QUOTE_DATA.find_all('midchamber'),
    ):  # type: Property

        quote_targetname = group['Choreo_Name', '@choreo']
        use_dings = group.bool('use_dings', allow_dings)

        possible_quotes = sorted(
            find_group_quotes(
                group,
                mid_quotes,
                use_dings,
                conf=mid_config if group.name == 'midchamber' else norm_config,
                mid_name=quote_targetname,
                player_flag_set=player_flag_set,
            ),
            key=sort_func,
            reverse=True,
        )

        if possible_quotes:
            choreo_loc = group.vec('choreo_loc', *quote_loc)

            if use_priority:
                chosen = possible_quotes[0].lines
            else:
                # Chose one of the quote blocks..
                random.seed('{}-VOICE_QUOTE_{}'.format(
                    map_seed,
                    len(possible_quotes),
                ))
                chosen = random.choice(possible_quotes).lines

            # Join the IDs for
            # the voice lines to the map seed,
            # so each quote block will chose different lines.
            random.seed(map_seed + '-VOICE_LINE_' + '|'.join(
                prop['id', 'ID']
                for prop in
                chosen
            ))

            # Add one of the associated quotes
            add_quote(
                random.choice(chosen),
                quote_targetname,
                choreo_loc,
                use_dings,
            )

    if ADDED_BULLSEYES or QUOTE_DATA.bool('UseMicrophones'):
        # Add microphones that broadcast audio directly at players.
        # This ensures it is heard regardless of location.
        # This is used for Cave and core Wheatley.
        LOGGER.info('Using microphones...')
        if vbsp.GAME_MODE == 'SP':
            vmf_file.create_ent(
                classname='env_microphone',
                targetname='player_speaker_sp',
                speakername='!player',
                maxRange='386',
                origin=quote_loc,
            )
        else:
            vmf_file.create_ent(
                classname='env_microphone',
                targetname='player_speaker_blue',
                speakername='!player_blue',
                maxRange='386',
                origin=quote_loc,
            )
            vmf_file.create_ent(
                classname='env_microphone',
                targetname='player_speaker_orange',
                speakername='!player_orange',
                maxRange='386',
                origin=quote_loc,
            )

    LOGGER.info('{} Mid quotes', len(mid_quotes))
    for mid_lines in mid_quotes:
        line = random.choice(mid_lines)
        mid_item, use_ding, mid_name = line
        add_quote(mid_item, mid_name, quote_loc, use_ding)

    LOGGER.info('Done!')
Exemple #44
0
def retexture_template(
    template_data: ExportedTemplate,
    origin: Vec,
    fixup: srctools.vmf.EntityFixup=None,
    replace_tex: dict= srctools.EmptyMapping,
    force_colour: MAT_TYPES=None,
    force_grid: str=None,
    use_bullseye=False,
    no_clumping=False,
):
    """Retexture a template at the given location.

    - Only textures in the TEMPLATE_RETEXTURE dict will be replaced.
    - Others will be ignored (nodraw, plasticwall, etc)
    - Wall textures pointing up and down will switch to floor/ceiling textures.
    - Textures of the same type, normal and inst origin will randomise to the
      same type.
    - replace_tex is a replacement table. This overrides everything else.
      The values should either be a list (random), or a single value.
    - If force_colour is set, all tile textures will be switched accordingly.
      If set to 'INVERT', white and black textures will be swapped.
    - If force_grid is set, all tile textures will be that size:
      ('wall', '2x2', '4x4', 'special')
    - If use_bullseye is true, the bullseye textures will be used for all panel
      sides instead of the normal textures. (This overrides force_grid.)
    - Fixup is the inst.fixup value, used to allow $replace in replace_tex.
    - Set no_clump if the brush is used on a special entity, and therefore
      won't get retextured by the main code. That means we need to directly
      retexture here.
    """
    import vbsp

    template = template_data.template  # type: Template

    rev_id_mapping = {
        new_id: str(old_id)
        for old_id, new_id in
        template_data.orig_ids.items()
    }

    all_brushes = list(template_data.world)  # type: List[Solid]
    if template_data.detail is not None:
        all_brushes.extend(template_data.detail.solids)

    # Template faces are randomised per block and side. This means
    # multiple templates in the same block get the same texture, so they
    # can clip into each other without looking bad.
    rand_prefix = 'TEMPLATE_{}_{}_{}:'.format(*(origin // 128))

    # Even if not axis-aligned, make mostly-flat surfaces
    # floor/ceiling (+-40 degrees)
    # sin(40) = ~0.707
    floor_tolerance = 0.8

    can_clump = vbsp.can_clump()

    # Ensure all values are lists.
    replace_tex = {
        key.casefold(): ([value] if isinstance(value, str) else value)
        for key, value in
        replace_tex.items()
    }

    # For each face, if it needs to be forced to a colour, or None if not.
    force_colour_face = defaultdict(lambda: None)

    # Already sorted by priority.
    for color_picker in template.color_pickers:
        picker_pos = color_picker.offset.copy().rotate(*template_data.angles)
        picker_pos += template_data.origin
        picker_norm = color_picker.normal.copy().rotate(*template_data.angles)

        if color_picker.grid_snap:
            for axis in 'xyz':
                # Don't realign things in the normal's axis -
                # those are already fine.
                if not picker_norm[axis]:
                    picker_pos[axis] = picker_pos[axis] // 128 * 128 + 64

        brush = conditions.SOLIDS.get(picker_pos.as_tuple(), None)

        if brush is None or abs(brush.normal) != abs(picker_norm):
            # Doesn't exist.
            continue

        if color_picker.remove_brush and brush.solid in vbsp.VMF.brushes:
            brush.solid.remove()

        for side in color_picker.sides:
            # Only do the highest priority successful one.
            if force_colour_face[side] is None:
                force_colour_face[side] = brush.color

    for brush in all_brushes:
        for face in brush:
            orig_id = rev_id_mapping[face.id]

            if orig_id in template.skip_faces:
                continue

            folded_mat = face.mat.casefold()

            norm = face.normal()
            random.seed(rand_prefix + norm.join('_'))

            if orig_id in template.realign_faces:
                try:
                    uaxis, vaxis = REALIGN_UVS[norm.as_tuple()]
                except KeyError:
                    LOGGER.warning(
                        'Realign face in template "{}" ({} in final) is '
                        'not on grid!',
                        template.id,
                        face.id,
                    )
                else:
                    face.uaxis = uaxis.copy()
                    face.vaxis = vaxis.copy()

            try:
                override_mat = replace_tex['#' + orig_id]
            except KeyError:
                try:
                    override_mat = replace_tex[folded_mat]
                except KeyError:
                    override_mat = None

            if override_mat is not None:
                # Replace_tex overrides everything.
                mat = random.choice(override_mat)
                if mat[:1] == '$' and fixup is not None:
                    mat = fixup[mat]
                if mat.startswith('<') or mat.endswith('>'):
                    # Lookup in the style data.
                    mat = vbsp.get_tex(mat[1:-1])
                face.mat = mat
                continue

            try:
                tex_type = TEMPLATE_RETEXTURE[folded_mat]
            except KeyError:
                continue  # It's nodraw, or something we shouldn't change

            if isinstance(tex_type, str):
                # It's something like squarebeams or backpanels, just look
                # it up
                face.mat = vbsp.get_tex(tex_type)

                if tex_type == 'special.goo_cheap':
                    if norm != (0, 0, 1):
                        # Goo must be facing upright!
                        # Retexture to nodraw, so a template can be made with
                        # all faces goo to work in multiple orientations.
                        face.mat = 'tools/toolsnodraw'
                    else:
                        # Goo always has the same orientation!
                        face.uaxis = UVAxis(
                            1, 0, 0,
                            offset=0,
                            scale=vbsp_options.get(float, 'goo_scale'),
                        )
                        face.vaxis = UVAxis(
                            0, -1, 0,
                            offset=0,
                            scale=vbsp_options.get(float, 'goo_scale'),
                        )
                continue
            # It's a regular wall type!
            tex_colour, grid_size = tex_type

            if force_colour_face[orig_id] is not None:
                tex_colour = force_colour_face[orig_id]
            elif force_colour == 'INVERT':
                # Invert the texture
                tex_colour = (
                    MAT_TYPES.white
                    if tex_colour is MAT_TYPES.black else
                    MAT_TYPES.black
                )
            elif force_colour is not None:
                tex_colour = force_colour

            if force_grid is not None:
                grid_size = force_grid

            if 1 in norm or -1 in norm:  # Facing NSEW or up/down
                # If axis-aligned, make the orientation aligned to world
                # That way multiple items merge well, and walls are upright.
                # We allow offsets < 1 grid tile, so items can be offset.
                face.uaxis.offset %= TEMP_TILE_PIX_SIZE[grid_size]
                face.vaxis.offset %= TEMP_TILE_PIX_SIZE[grid_size]

            if use_bullseye:
                # We want to use the bullseye textures, instead of normal
                # ones
                if norm.z < -floor_tolerance:
                    face.mat = vbsp.get_tex(
                        'special.bullseye_{}_floor'.format(tex_colour)
                    )
                elif norm.z > floor_tolerance:
                    face.mat = vbsp.get_tex(
                        'special.bullseye_{}_ceiling'.format(tex_colour)
                    )
                else:
                    face.mat = ''  # Ensure next if statement triggers

                # If those aren't defined, try the wall texture..
                if face.mat == '':
                    face.mat = vbsp.get_tex(
                        'special.bullseye_{}_wall'.format(tex_colour)
                    )
                if face.mat != '':
                    continue  # Set to a bullseye texture,
                    # don't use the wall one

            if grid_size == 'special':
                # Don't use wall on faces similar to floor/ceiling:
                if -floor_tolerance < norm.z < floor_tolerance:
                    face.mat = vbsp.get_tex(
                        'special.{!s}_wall'.format(tex_colour)
                    )
                else:
                    face.mat = ''  # Ensure next if statement triggers

                # Various fallbacks if not defined
                if face.mat == '':
                    face.mat = vbsp.get_tex(
                        'special.{!s}'.format(tex_colour)
                    )
                if face.mat == '':
                    # No special texture - use a wall one.
                    grid_size = 'wall'
                else:
                    # Set to a special texture,
                    continue # don't use the wall one

            if norm.z > floor_tolerance:
                grid_size = 'ceiling'
            if norm.z < -floor_tolerance:
                grid_size = 'floor'

            if can_clump and not no_clumping:
                # For the clumping algorithm, set to Valve PeTI and let
                # clumping handle retexturing.
                vbsp.IGNORED_FACES.remove(face)
                if tex_colour is MAT_TYPES.white:
                    if grid_size == '4x4':
                        face.mat = 'tile/white_wall_tile003f'
                    elif grid_size == '2x2':
                        face.mat = 'tile/white_wall_tile003c'
                    else:
                        face.mat = 'tile/white_wall_tile003h'
                elif tex_colour is MAT_TYPES.black:
                    if grid_size == '4x4':
                        face.mat = 'metal/black_wall_metal_002b'
                    elif grid_size == '2x2':
                        face.mat = 'metal/black_wall_metal_002a'
                    else:
                        face.mat = 'metal/black_wall_metal_002e'
            else:
                face.mat = vbsp.get_tex(
                    '{!s}.{!s}'.format(tex_colour, grid_size)
                )

    for over in template_data.overlay[:]:
        random.seed('TEMP_OVERLAY_' + over['basisorigin'])
        mat = over['material'].casefold()
        if mat in replace_tex:
            mat = random.choice(replace_tex[mat])
            if mat[:1] == '$':
                mat = fixup[mat]
            if mat.startswith('<') or mat.endswith('>'):
                # Lookup in the style data.
                mat = vbsp.get_tex(mat[1:-1])
        elif mat in vbsp.TEX_VALVE:
            mat = vbsp.get_tex(vbsp.TEX_VALVE[mat])
        else:
            continue
        if mat == '':
            # If blank, remove the overlay from the map and the list.
            # (Since it's inplace, this can affect the tuple.)
            template_data.overlay.remove(over)
            over.remove()
        else:
            over['material'] = mat
Exemple #45
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,
            )
Exemple #46
0
def res_cave_portrait() -> bool:
    """Checks to see if the Cave Portrait option is set for the given

    voice pack.
    """
    return vbsp_options.get(int, 'cave_port_skin') is not None
Exemple #47
0
def flag_music(flag: Property):
    """Checks the selected music ID.

    Use "<NONE>" for no music.
    """
    return vbsp_options.get(str, 'music_id') == flag.value
Exemple #48
0
def res_set_texture(inst: Entity, res: Property):
    """Set the brush face at a location to a particular texture.

    pos is the position, relative to the instance
      (0 0 0 is the floor-surface).
    dir is the normal of the texture.
    If gridPos is true, the position will be snapped so it aligns with
     the 128 brushes (Useful with fizzler/light strip items).

    tex is the texture used.
    If tex begins and ends with `<>`, certain
    textures will be used based on style:
    - `<delete>` will remove the brush entirely (it should be hollow).
      Caution should be used to ensure no leaks occur.
    - `<special>` the brush will be given a special texture
      like angled and flip panels.
    - `<white>` and `<black>` will use the regular textures for the
      given color.
    - `<white-2x2>`, `<white-4x4>`, `<black-2x2>`, `<black-4x4>` will use
      the given wall-sizes. If on floors or ceilings these always use 4x4.
    - `<2x2>` or `<4x4>` will force to the given wall-size, keeping color.
    - `<special-white>` and `<special-black>` will use a special texture
       of the given color.
    If tex begins and ends with `[]`, it is an option in the `Textures` list.
    These are composed of a group and texture, separated by `.`. `white.wall`
    are the white wall textures; `special.goo` is the goo texture.

    If `template` is set, the template should be an axis aligned cube. This
    will be rotated by the instance angles, and then the face with the same
    orientation will be applied to the face (with the rotation and texture).
    """
    import vbsp
    pos = Vec.from_str(res['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 = Vec.from_str(res['dir', '0 0 -1']).rotate_by_str(
        inst['angles', '0 0 0']
    )

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

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

    if not brush or brush.normal != norm:
        return

    face_to_mod = brush.face  # type: Side

    # Don't allow this to get overwritten later.
    vbsp.IGNORED_FACES.add(face_to_mod)

    temp = res['template', None]
    if temp:
        # Grab the scaling template and apply it to the brush.
        template_brush.get_scaling_template(temp).rotate(
            Vec.from_str(inst['angles']),
            Vec.from_str(inst['origin']),
        ).apply(face_to_mod)
        return

    tex = res['tex']

    if tex.startswith('[') and tex.endswith(']'):
        face_to_mod.mat = vbsp.get_tex(tex[1:-1])
    elif tex.startswith('<') and tex.endswith('>'):
        # Special texture names!
        tex = tex[1:-1].casefold()
        if tex == 'delete':
            vbsp.VMF.remove_brush(brush)
            return

        if tex == 'white':
            face_to_mod.mat = 'tile/white_wall_tile003a'
        elif tex == 'black':
            face_to_mod.mat = 'metal/black_wall_metal_002c'

        if tex == 'black' or tex == 'white':
            # For these two, run the regular logic to apply textures
            # correctly.
            vbsp.alter_mat(
                face_to_mod,
                vbsp.face_seed(face_to_mod),
                vbsp_options.get(bool, 'tile_texture_lock'),
            )

        if tex == 'special':
            vbsp.set_special_mat(face_to_mod, str(brush.color))
        elif tex == 'special-white':
            vbsp.set_special_mat(face_to_mod, 'white')
            return
        elif tex == 'special-black':
            vbsp.set_special_mat(brush.face, 'black')

        # Do <4x4>, <white-2x4>, etc
        color = str(brush.color)
        if tex.startswith('black') or tex.endswith('white'):
            # Override the color used for 2x2/4x4 brushes
            color = tex[:5]
        if tex.endswith('2x2') or tex.endswith('4x4'):
            # 4x4 and 2x2 instructions are ignored on floors and ceilings.
            orient = vbsp.get_face_orient(face_to_mod)
            if orient == vbsp.ORIENT.wall:
                face_to_mod.mat = vbsp.get_tex(
                    color + '.' + tex[-3:]
                )
            else:
                face_to_mod.mat = vbsp.get_tex(
                    color + '.' + str(orient)
                )
    else:
        face_to_mod.mat = tex
Exemple #49
0
def add_voice(
    has_items: dict,
    style_vars_: dict,
    vmf_file_: VMF,
    map_seed: str,
    use_priority=True,
):
    """Add a voice line to the map."""
    global ALLOW_MID_VOICES, vmf_file, map_attr, style_vars
    LOGGER.info('Adding Voice Lines!')

    vmf_file = vmf_file_
    map_attr = has_items
    style_vars = style_vars_

    norm_config = ConfigFile('voice.cfg', root='bee2')
    mid_config = ConfigFile('mid_voice.cfg', root='bee2')

    quote_base = QUOTE_DATA['base', False]
    quote_loc = Vec.from_str(QUOTE_DATA['quote_loc', '-10000 0 0'], x=-10000)
    if quote_base:
        LOGGER.info('Adding Base instance!')
        vmf_file.create_ent(
            classname='func_instance',
            targetname='voice',
            file=INST_PREFIX + quote_base,
            angles='0 0 0',
            origin=quote_loc,
            fixup_style='0',
        )

    # Either box in with nodraw, or place the voiceline studio.
    has_studio = conditions.monitor.make_voice_studio(vmf_file, quote_loc)

    bullsye_actor = vbsp_options.get(str, 'voice_studio_actor')
    if bullsye_actor and has_studio:
        ADDED_BULLSEYES.add(bullsye_actor)

    global_bullseye = QUOTE_DATA['bullseye', '']
    if global_bullseye:
        add_bullseye(quote_loc, global_bullseye)

    ALLOW_MID_VOICES = not style_vars.get('nomidvoices', False)

    mid_quotes = []

    # Enable using the beep before and after choreo lines.
    allow_dings = srctools.conv_bool(QUOTE_DATA['use_dings', '0'])
    if allow_dings:
        vmf_file.create_ent(
            classname='logic_choreographed_scene',
            targetname='@ding_on',
            origin=quote_loc + (-8, -16, 0),
            scenefile='scenes/npc/glados_manual/ding_on.vcd',
            busyactor="1",  # Wait for actor to stop talking
            onplayerdeath='0',
        )
        vmf_file.create_ent(
            classname='logic_choreographed_scene',
            targetname='@ding_off',
            origin=quote_loc + (8, -16, 0),
            scenefile='scenes/npc/glados_manual/ding_off.vcd',
            busyactor="1",  # Wait for actor to stop talking
            onplayerdeath='0',
        )

    # QuoteEvents allows specifying an instance for particular items,
    # so a voice line can be played at a certain time. It's only active
    # in certain styles, but uses the default if not set.
    for event in QUOTE_DATA.find_all('QuoteEvents', 'Event'):
        event_id = event['id', ''].casefold()
        # We ignore the config if no result was executed.
        if event_id and event_id in QUOTE_EVENTS:
            # Instances from the voiceline config are in this subfolder,
            # but not the default item - that's set from the conditions
            QUOTE_EVENTS[event_id] = INST_PREFIX + event['file']

    LOGGER.info('Quote events: {}', list(QUOTE_EVENTS.keys()))

    if has_responses():
        LOGGER.info('Generating responses data..')
        with open(RESP_LOC, 'w') as f:
            generate_resp_script(f, allow_dings)
    else:
        LOGGER.info('No responses data..')
        try:
            os.remove(RESP_LOC)
        except FileNotFoundError:
            pass

    for ind, file in enumerate(QUOTE_EVENTS.values()):
        if not file:
            continue
        vmf_file.create_ent(
            classname='func_instance',
            targetname='voice_event_' + str(ind),
            file=file,
            angles='0 0 0',
            origin=quote_loc,
            fixup_style='0',
        )

    # Determine the flags that enable/disable specific lines based on which
    # players are used.
    player_model = vbsp.BEE2_config.get_val(
        'General',
        'player_model',
        'PETI',
    ).casefold()

    is_coop = (vbsp.GAME_MODE == 'COOP')
    is_sp = (vbsp.GAME_MODE == 'SP')

    player_flags = {
        'sp': is_sp,
        'coop': is_coop,
        'atlas': is_coop or player_model == 'atlas',
        'pbody': is_coop or player_model == 'pbody',
        'bendy': is_sp and player_model == 'peti',
        'chell': is_sp and player_model == 'sp',
        'human': is_sp and player_model in ('peti', 'sp'),
        'robot': is_coop or player_model in ('atlas', 'pbody'),
    }
    # All which are True.
    player_flag_set = {val for val, flag in player_flags.items() if flag}

    # For each group, locate the voice lines.
    for group in itertools.chain(
            QUOTE_DATA.find_all('group'),
            QUOTE_DATA.find_all('midchamber'),
    ):  # type: Property

        quote_targetname = group['Choreo_Name', '@choreo']
        use_dings = group.bool('use_dings', allow_dings)

        possible_quotes = sorted(
            find_group_quotes(
                group,
                mid_quotes,
                use_dings,
                conf=mid_config if group.name == 'midchamber' else norm_config,
                mid_name=quote_targetname,
                player_flag_set=player_flag_set,
            ),
            key=sort_func,
            reverse=True,
        )

        if possible_quotes:
            choreo_loc = group.vec('choreo_loc', *quote_loc)

            if use_priority:
                chosen = possible_quotes[0].lines
            else:
                # Chose one of the quote blocks..
                random.seed('{}-VOICE_QUOTE_{}'.format(
                    map_seed,
                    len(possible_quotes),
                ))
                chosen = random.choice(possible_quotes).lines

            # Join the IDs for
            # the voice lines to the map seed,
            # so each quote block will chose different lines.
            random.seed(map_seed + '-VOICE_LINE_' +
                        '|'.join(prop['id', 'ID'] for prop in chosen))

            # Add one of the associated quotes
            add_quote(
                random.choice(chosen),
                quote_targetname,
                choreo_loc,
                use_dings,
            )

    if ADDED_BULLSEYES or QUOTE_DATA.bool('UseMicrophones'):
        # Add microphones that broadcast audio directly at players.
        # This ensures it is heard regardless of location.
        # This is used for Cave and core Wheatley.
        LOGGER.info('Using microphones...')
        if vbsp.GAME_MODE == 'SP':
            vmf_file.create_ent(
                classname='env_microphone',
                targetname='player_speaker_sp',
                speakername='!player',
                maxRange='386',
                origin=quote_loc,
            )
        else:
            vmf_file.create_ent(
                classname='env_microphone',
                targetname='player_speaker_blue',
                speakername='!player_blue',
                maxRange='386',
                origin=quote_loc,
            )
            vmf_file.create_ent(
                classname='env_microphone',
                targetname='player_speaker_orange',
                speakername='!player_orange',
                maxRange='386',
                origin=quote_loc,
            )

    LOGGER.info('{} Mid quotes', len(mid_quotes))
    for mid_lines in mid_quotes:
        line = random.choice(mid_lines)
        mid_item, use_ding, mid_name = line
        add_quote(mid_item, mid_name, quote_loc, use_ding)

    LOGGER.info('Done!')
Exemple #50
0
    def gen_flinch_trigs(self, vmf: VMF, name: str,
                         start_disabled: str) -> None:
        """For deadly fizzlers optionally make them safer.

        This adds logic to force players
        back instead when walking into the field.
        Only applies to vertical triggers.
        """
        normal = abs(self.normal())  # type: Vec

        # Horizontal fizzlers would just have you fall through.
        if normal.z:
            return

        # Disabled.
        if not vbsp_options.get_itemconf(
            ('VALVE_FIZZLER', 'FlinchBack'), False):
            return

        # Make global entities if not present.
        if '_fizz_flinch_hurt' not in vmf.by_target:
            glob_ent_loc = vbsp_options.get(Vec, 'global_ents_loc')
            vmf.create_ent(
                classname='point_hurt',
                targetname='_fizz_flinch_hurt',
                Damage=10,  # Just for visuals and sounds.
                # BURN | ENERGYBEAM | PREVENT_PHYSICS_FORCE
                DamageType=8 | 1024 | 2048,
                DamageTarget='!activator',  # Hurt the triggering player.
                DamageRadius=1,  # Target makes this unused.
                origin=glob_ent_loc,
            )

        # We need two catapults - one for each side.
        neg_brush = vmf.create_ent(
            targetname=name,
            classname='trigger_catapult',
            spawnflags=1,  # Players only.
            origin=self.base_inst['origin'],
            physicsSpeed=0,
            playerSpeed=96,
            launchDirection=(-normal).to_angle(),
            startDisabled=start_disabled,
        )
        neg_brush.add_out(Output('OnCatapulted', '_fizz_flinch_hurt', 'Hurt'))

        pos_brush = neg_brush.copy()
        pos_brush['launchDirection'] = normal.to_angle()
        vmf.add_ent(pos_brush)

        for seg_min, seg_max in self.emitters:
            neg_brush.solids.append(
                vmf.make_prism(
                    p1=(seg_min - 4 * normal - 64 * self.up_axis),
                    p2=seg_max + 64 * self.up_axis,
                    mat=const.Tools.TRIGGER,
                ).solid)
            pos_brush.solids.append(
                vmf.make_prism(
                    p1=seg_min - 64 * self.up_axis,
                    p2=(seg_max + 4 * normal + 64 * self.up_axis),
                    mat=const.Tools.TRIGGER,
                ).solid)
Exemple #51
0
def make_barriers(vmf: VMF):
    """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
        elif barr_type is BarrierType.GRATING:
            front_temp = grate_temp
        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,
            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
        elif barr_type is BarrierType.GRATING:
            front_temp = grate_temp
        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,
                solid_pane_func,
            )

    if floorbeam_temp:
        LOGGER.info('Adding Glass floor beams...')
        add_glass_floorbeams(vmf, floorbeam_temp)
        LOGGER.info('Done!')
Exemple #52
0
def make_pit_shell(vmf: VMF):
    """If the pit is surrounded on all sides, we can just extend walls down.

    That avoids needing to use skybox workarounds."""
    LOGGER.info('Making pit shell...')
    for x in range(-8, 20):
        for y in range(-8, 20):
            block_types = [
                brushLoc.POS[x, y, z]
                for z in
                range(-15, 1)
            ]
            lowest = max((
                z for z in
                range(-15, 1)
                if block_types[z] is not brushLoc.Block.VOID
            ), default=None)

            if lowest is None:
                continue
                # TODO: For opened areas (wheatley), generate a floor...
                real_pos = brushLoc.grid_to_world(Vec(x, y, 0))
                prism = vmf.make_prism(
                    real_pos + (64, 64, BOTTOMLESS_PIT_MIN + 8),
                    real_pos + (-64, -64, BOTTOMLESS_PIT_MIN),
                    mat='tools/toolsnodraw',
                )
                prism.bottom.mat = consts.Special.BACKPANELS_CHEAP

                vmf.add_brush(prism.solid)
                continue

            if block_types[lowest].is_solid:
                real_pos = brushLoc.grid_to_world(Vec(x, y, lowest))
                for z in range(0, 10):
                    br_pos = real_pos - (0, 0, 512 * z)
                    vmf.add_brush(
                        vmf.make_prism(br_pos + 64, br_pos - (64, 64, 512-64), vbsp.BLACK_PAN[1]).solid
                    )

    prism = vmf.make_prism(
        Vec(-8 * 128, -8 * 128, -4864),
        Vec(20 * 128, 20 * 128, -4896),
    )
    prism.top.mat = 'tools/toolsblack'
    vmf.add_brush(prism.solid)

    diss_trig = vmf.create_ent(
        classname='trigger_multiple',
        spawnflags=4104,
        wait=0.1,
        origin=vbsp_options.get(Vec, 'global_pti_ents_loc'),
    )
    diss_trig.solids = [vmf.make_prism(
        Vec(-8 * 128, -8 * 128, -4182),
        Vec(20 * 128, 20 * 128, -4864),
        mat='tools/toolstrigger',
    ).solid]
    diss_trig.add_out(
        Output('OnStartTouch', '!activator', 'SilentDissolve'),
        Output('OnStartTouch', '!activator', 'Break', delay=0.1),
        Output('OnStartTouch', '!activator', 'Kill', delay=0.5),
    )

    # Since we can chuck gel down the pit, cover it in a noportal_volume
    # to stop players from portalling past the hurt trigger.
    diss_trig = vmf.create_ent(
        classname='func_noportal_volume',
        origin=vbsp_options.get(Vec, 'global_pti_ents_loc'),
    )
    diss_trig.solids = [vmf.make_prism(
        Vec(-8 * 128, -8 * 128, -64),
        Vec(20 * 128, 20 * 128, -4864),
        mat='tools/toolstrigger',
    ).solid]
Exemple #53
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)
Exemple #54
0
def make_bottomless_pit(vmf: VMF, max_height):
    """Generate bottomless pits."""

    tele_ref = SETTINGS['tele_ref']
    tele_dest = SETTINGS['tele_dest']

    use_skybox = bool(SETTINGS['skybox'])

    if use_skybox:
        tele_off = Vec(
            x=SETTINGS['off_x'],
            y=SETTINGS['off_y'],
        )
    else:
        tele_off = Vec(0, 0, 0)

    # Controlled by the style, not skybox!
    blend_light = vbsp_options.get(str, 'pit_blend_light')

    if use_skybox:
        # Add in the actual skybox edges and triggers.
        vmf.create_ent(
            classname='func_instance',
            file=SETTINGS['skybox'],
            targetname='skybox',
            angles='0 0 0',
            origin=tele_off,
        )

        fog_opt = vbsp.settings['fog']

        # Now generate the sky_camera, with appropriate values.
        sky_camera = vmf.create_ent(
            classname='sky_camera',
            scale='1.0',

            origin=tele_off,

            angles=fog_opt['direction'],
            fogdir=fog_opt['direction'],
            fogcolor=fog_opt['primary'],
            fogstart=fog_opt['start'],
            fogend=fog_opt['end'],

            fogenable='1',

            heightFogStart=fog_opt['height_start'],
            heightFogDensity=fog_opt['height_density'],
            heightFogMaxDensity=fog_opt['height_max_density'],
        )

        if fog_opt['secondary']:
            # Only enable fog blending if a secondary color is enabled
            sky_camera['fogblend'] = '1'
            sky_camera['fogcolor2'] = fog_opt['secondary']
            sky_camera['use_angles'] = '1'
        else:
            sky_camera['fogblend'] = '0'
            sky_camera['use_angles'] = '0'

        if SETTINGS['skybox_ceil'] != '':
            # We dynamically add the ceiling so it resizes to match the map,
            # and lighting won't be too far away.
            vmf.create_ent(
                classname='func_instance',
                file=SETTINGS['skybox_ceil'],
                targetname='skybox',
                angles='0 0 0',
                origin=tele_off + (0, 0, max_height),
            )

        if SETTINGS['targ'] != '':
            # Add in the teleport reference target.
            vmf.create_ent(
                classname='func_instance',
                file=SETTINGS['targ'],
                targetname='skybox',
                angles='0 0 0',
                origin='0 0 0',
            )

    # First, remove all of Valve's triggers inside pits.
    for trig in vmf.by_class['trigger_multiple'] | vmf.by_class['trigger_hurt']:
        if brushLoc.POS['world': Vec.from_str(trig['origin'])].is_pit:
            trig.remove()

    # Potential locations of bordering brushes..
    wall_pos = set()

    side_dirs = [
        (0, -128, 0),   # N
        (0, +128, 0),  # S
        (-128, 0, 0),   # E
        (+128, 0, 0)   # W
    ]

    # Only use 1 entity for the teleport triggers. If multiple are used,
    # cubes can contact two at once and get teleported odd places.
    tele_trig = None
    hurt_trig = None

    for grid_pos, block_type in brushLoc.POS.items():  # type: Vec, brushLoc.Block
        pos = brushLoc.grid_to_world(grid_pos)
        if not block_type.is_pit:
            continue

        # Physics objects teleport when they hit the bottom of a pit.
        if block_type.is_bottom and use_skybox:
            if tele_trig is None:
                tele_trig = vmf.create_ent(
                    classname='trigger_teleport',
                    spawnflags='4106',  # Physics and npcs
                    landmark=tele_ref,
                    target=tele_dest,
                    origin=pos,
                )
            tele_trig.solids.append(
                vmf.make_prism(
                    pos + (-64, -64, -64),
                    pos + (64, 64, -8),
                    mat='tools/toolstrigger',
                ).solid,
            )

        # Players, however get hurt as soon as they enter - that way it's
        # harder to see that they don't teleport.
        if block_type.is_top:
            if hurt_trig is None:
                hurt_trig = vmf.create_ent(
                    classname='trigger_hurt',
                    damagetype=32,  # FALL
                    spawnflags=1,  # CLients
                    damage=100000,
                    nodmgforce=1,  # No physics force when hurt..
                    damagemodel=0,  # Always apply full damage.
                    origin=pos,  # We know this is not in the void..
                )
            hurt_trig.solids.append(
                vmf.make_prism(
                    Vec(pos.x - 64, pos.y - 64, -128),
                    pos + (64, 64, 48 if use_skybox else 16),
                    mat='tools/toolstrigger',
                ).solid,
            )

        if not block_type.is_bottom:
            continue
        # Everything else is only added to the bottom-most position.

        if use_skybox and blend_light:
            # Generate dim lights at the skybox location,
            # to blend the lighting together.
            light_pos = pos + (0, 0, -60)
            vmf.create_ent(
                classname='light',
                origin=light_pos,
                _light=blend_light,
                _fifty_percent_distance='256',
                _zero_percent_distance='512',
            )
            vmf.create_ent(
                classname='light',
                origin=light_pos + tele_off,
                _light=blend_light,
                _fifty_percent_distance='256',
                _zero_percent_distance='512',
            )

        wall_pos.update([
            (pos + off).as_tuple()
            for off in
            side_dirs
        ])

    if tele_trig is not None:
        vbsp.IGNORED_BRUSH_ENTS.add(tele_trig)
    if hurt_trig is not None:
        vbsp.IGNORED_BRUSH_ENTS.add(hurt_trig)
        hurt_trig.outputs.append(
            Output(
                'OnHurtPlayer',
                '@goo_fade',
                'Fade',
            ),
        )

    if not use_skybox:
        make_pit_shell(vmf)
        return

    # Now determine the position of side instances.
    # We use the utils.CONN_TYPES dict to determine instance positions
    # based on where nearby walls are.
    side_types = {
        utils.CONN_TYPES.side: PIT_INST['side'],  # o|
        utils.CONN_TYPES.corner: PIT_INST['corner'],  # _|
        utils.CONN_TYPES.straight: PIT_INST['side'],  # Add this twice for |o|
        utils.CONN_TYPES.triple: PIT_INST['triple'],  # U-shape
        utils.CONN_TYPES.all: PIT_INST['pillar'],  # [o]
    }

    LOGGER.info('Pit instances: {}', side_types)

    for pos in wall_pos:
        pos = Vec(pos)
        if not brushLoc.POS['world': pos].is_solid:
            # Not actually a wall here!
            continue

        # CONN_TYPES has n,s,e,w as keys - whether there's something in that direction.
        nsew = tuple(
            brushLoc.POS['world': pos + off].is_pit
            for off in
            side_dirs
        )
        LOGGER.info('Pos: {}, NSEW: {}, lookup: {}', pos, nsew, utils.CONN_LOOKUP[nsew])
        inst_type, angle = utils.CONN_LOOKUP[nsew]

        if inst_type is utils.CONN_TYPES.none:
            # Middle of the pit...
            continue

        random.seed('pit_' + str(pos.x) + str(pos.y) + 'sides')

        file = random.choice(side_types[inst_type])

        if file != '':
            vmf.create_ent(
                classname='func_instance',
                file=file,
                targetname='goo_side',
                origin=tele_off + pos,
                angles=angle,
            ).make_unique()

        # Straight uses two side-instances in parallel - "|o|"
        if inst_type is utils.CONN_TYPES.straight:
            file = random.choice(side_types[inst_type])
            if file != '':
                vmf.create_ent(
                    classname='func_instance',
                    file=file,
                    targetname='goo_side',
                    origin=tele_off + pos,
                    # Reverse direction
                    angles=Vec.from_str(angle) + (0, 180, 0),
                ).make_unique()
Exemple #55
0
def res_cave_portrait():
    """Checks to see if the Cave Portrait option is set for the given

    skin pack.
    """
    return vbsp_options.get(int, 'cave_port_skin') is not None
Exemple #56
0
def mon_camera_link() -> None:
    """Link cameras to monitors."""
    import vbsp
    LOGGER.info('Bullseye {}', BULLSYE_LOCS)

    if not ALL_MONITORS:
        return

    ALL_CAMERAS.sort(key=Camera.cam_pos.fget)

    fog_opt = vbsp.settings['fog']

    active_counts = [
        srctools.conv_int(cam.inst.fixup['$start_enabled', '0'])
        for cam in
        ALL_CAMERAS
    ]

    for index, cam in enumerate(ALL_CAMERAS):  # type: int, Camera
        if srctools.conv_int(cam.inst.fixup['$connectioncount']) == 0:
            continue

        conn_item = connections.ITEMS[cam.inst['targetname']]
        # Generate an input to the VScript which turns on/off this camera.
        # Everything's by index.
        conn_item.enable_cmd = (Output(
            '',
            '@camera',
            'RunScriptCode',
            'CamEnable({})'.format(index),
        ), )
        conn_item.disable_cmd = (Output(
            '',
            '@camera',
            'RunScriptCode',
            'CamDisable({})'.format(index),
        ), )

    for is_act, cam in zip(active_counts, ALL_CAMERAS):
        if is_act:
            start_pos = cam.cam_pos
            start_angles = cam.cam_angles
            break
    else:
        if vbsp_options.get(str, 'voice_studio_inst'):
            # Start at the studio, if it exists.
            start_pos = get_studio_pose()
            start_angles = '{:g} {:g} 0'.format(
                vbsp_options.get(float, 'voice_studio_cam_pitch'),
                vbsp_options.get(float, 'voice_studio_cam_yaw'),
            )
            # If we start at the studio, make the ai_relationships
            # for turret fire start active.
            for relation in MONITOR_RELATIONSHIP_ENTS:
                relation['StartActive'] = '1'
        else:
            # Start in arrival_departure_transition_ents...
            start_pos = '-2500 -2500 0'
            start_angles = '0 90 0'

    cam = vbsp.VMF.create_ent(
        classname='point_camera',
        targetname='@camera',
        spawnflags='0',  # Start on
        origin=start_pos,
        angles=start_angles,
        fov='60',

        # Copy fog settings from the skybox.
        fogEnable='1',
        fogMaxDensity='1',
        fogColor=fog_opt['primary'],
        fogStart=fog_opt['start'],
        fogEnd=fog_opt['end'],
    )  # type: Entity

    if not ALL_CAMERAS:
        # No cameras in the map - we don't move at all.
        # So we don't need the script.
        return

    # Set the vscripts
    cam['vscripts'] = 'BEE2/mon_camera_args.nut BEE2/mon_camera.nut'
    cam['thinkfunction'] = 'Think'
Exemple #57
0
def res_set_texture(inst: Entity, res: Property):
    """Set the brush face at a location to a particular texture.

    pos is the position, relative to the instance
      (0 0 0 is the floor-surface).
    dir is the normal of the texture.
    If gridPos is true, the position will be snapped so it aligns with
     the 128 brushes (Useful with fizzler/light strip items).

    tex is the texture used.
    If tex begins and ends with '<>', certain
    textures will be used based on style:
    - '<delete>' will remove the brush entirely (it should be hollow).
      Caution should be used to ensure no leaks occur.
    - '<special>' the brush will be given a special texture
      like angled and flip panels.
    - '<white>' and '<black>' will use the regular textures for the
      given color.
    - '<white-2x2>', '<white-4x4>', '<black-2x2>', '<black-4x4'> will use
      the given wall-sizes. If on floors or ceilings these always use 4x4.
    - '<2x2>' or '<4x4>' will force to the given wall-size, keeping color.
    - '<special-white>' and '<special-black>' will use a special texture
       of the given color.
    If tex begins and ends with '[]', it is an option in the 'Textures' list.
    These are composed of a group and texture, separated by '.'. 'white.wall'
    are the white wall textures; 'special.goo' is the goo texture.

    If 'template' is set, the template should be an axis aligned cube. This
    will be rotated by the instance angles, and then the face with the same
    orientation will be applied to the face (with the rotation and texture).
    """
    import vbsp
    pos = Vec.from_str(res['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 = Vec.from_str(res['dir', '0 0 -1']).rotate_by_str(inst['angles',
                                                                 '0 0 0'])

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

    brush = SOLIDS.get(pos.as_tuple(), None)
    ':type brush: solidGroup'

    if not brush or brush.normal != norm:
        return

    face_to_mod = brush.face  # type: Side

    # Don't allow this to get overwritten later.
    vbsp.IGNORED_FACES.add(face_to_mod)

    temp = res['template', None]
    if temp:
        # Grab the scaling template and apply it to the brush.
        template_brush.get_scaling_template(temp).rotate(
            Vec.from_str(inst['angles']),
            Vec.from_str(inst['origin']),
        ).apply(face_to_mod)
        return

    tex = res['tex']

    if tex.startswith('[') and tex.endswith(']'):
        face_to_mod.mat = vbsp.get_tex(tex[1:-1])
    elif tex.startswith('<') and tex.endswith('>'):
        # Special texture names!
        tex = tex[1:-1].casefold()
        if tex == 'delete':
            vbsp.VMF.remove_brush(brush)
            return

        if tex == 'white':
            face_to_mod.mat = 'tile/white_wall_tile003a'
        elif tex == 'black':
            face_to_mod.mat = 'metal/black_wall_metal_002c'

        if tex == 'black' or tex == 'white':
            # For these two, run the regular logic to apply textures
            # correctly.
            vbsp.alter_mat(
                face_to_mod,
                vbsp.face_seed(face_to_mod),
                vbsp_options.get(bool, 'tile_texture_lock'),
            )

        if tex == 'special':
            vbsp.set_special_mat(face_to_mod, str(brush.color))
        elif tex == 'special-white':
            vbsp.set_special_mat(face_to_mod, 'white')
            return
        elif tex == 'special-black':
            vbsp.set_special_mat(brush.face, 'black')

        # Do <4x4>, <white-2x4>, etc
        color = str(brush.color)
        if tex.startswith('black') or tex.endswith('white'):
            # Override the color used for 2x2/4x4 brushes
            color = tex[:5]
        if tex.endswith('2x2') or tex.endswith('4x4'):
            # 4x4 and 2x2 instructions are ignored on floors and ceilings.
            orient = vbsp.get_face_orient(face_to_mod)
            if orient == vbsp.ORIENT.wall:
                face_to_mod.mat = vbsp.get_tex(color + '.' + tex[-3:])
            else:
                face_to_mod.mat = vbsp.get_tex(color + '.' + str(orient))
    else:
        face_to_mod.mat = tex
Exemple #58
0
    def gen_flinch_trigs(self, vmf: VMF, name: str, start_disabled: str) -> None:
        """For deadly fizzlers optionally make them safer.

        This adds logic to force players
        back instead when walking into the field.
        Only applies to vertical triggers.
        """
        normal = abs(self.normal())  # type: Vec

        # Horizontal fizzlers would just have you fall through.
        if normal.z:
            return

        # Disabled.
        if not vbsp_options.get_itemconf(('VALVE_FIZZLER', 'FlinchBack'), False):
            return

        # Make global entities if not present.
        if '_fizz_flinch_hurt' not in vmf.by_target:
            glob_ent_loc = vbsp_options.get(Vec, 'global_ents_loc')
            vmf.create_ent(
                classname='point_hurt',
                targetname='_fizz_flinch_hurt',
                Damage=10,  # Just for visuals and sounds.
                # BURN | ENERGYBEAM | PREVENT_PHYSICS_FORCE
                DamageType=8 | 1024 | 2048,
                DamageTarget='!activator',  # Hurt the triggering player.
                DamageRadius=1,  # Target makes this unused.
                origin=glob_ent_loc,
            )

        # We need two catapults - one for each side.
        neg_brush = vmf.create_ent(
            targetname=name,
            classname='trigger_catapult',
            spawnflags=1,  # Players only.
            origin=self.base_inst['origin'],
            physicsSpeed=0,
            playerSpeed=96,
            launchDirection=(-normal).to_angle(),
            startDisabled=start_disabled,
        )
        neg_brush.add_out(Output('OnCatapulted', '_fizz_flinch_hurt', 'Hurt'))

        pos_brush = neg_brush.copy()
        pos_brush['launchDirection'] = normal.to_angle()
        vmf.add_ent(pos_brush)

        for seg_min, seg_max in self.emitters:
            neg_brush.solids.append(vmf.make_prism(
                p1=(seg_min
                    - 4 * normal
                    - 64 * self.up_axis
                    ),
                p2=seg_max + 64 * self.up_axis,
                mat=const.Tools.TRIGGER,
            ).solid)
            pos_brush.solids.append(vmf.make_prism(
                p1=seg_min - 64 * self.up_axis,
                p2=(seg_max
                    + 4 * normal
                    + 64 * self.up_axis
                    ),
                mat=const.Tools.TRIGGER,
            ).solid)