Esempio n. 1
0
def parse_map(vmf: VMF, has_attr: Dict[str, bool]) -> None:
    """Find all glass/grating in the map.

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

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

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

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

    if options.get(str, 'glass_pack') and has_attr['glass']:
        packing.pack_list(vmf, options.get(str, 'glass_pack'))
Esempio n. 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 options.get(str, 'voice_id') == '<NONE>'
    for char in options.get(str, 'voice_char').split(','):
        if targ_char in char.casefold():
            return True
    raise Unsatisfiable
Esempio n. 3
0
def global_input(
    vmf: VMF,
    pos: Union[Vec, str],
    output: Output,
    relay_name: str = None,
) -> None:
    """Create a global input, either from a relay or logic_auto.

    If the name is empty, a logic_auto is created.
    The position is used to place the entity if this is the first time.
    """
    try:
        glob_ent = GLOBAL_INPUT_ENTS[relay_name]
    except KeyError:
        if not relay_name:
            glob_ent = GLOBAL_INPUT_ENTS[''] = vmf.create_ent(
                classname='logic_auto',
                spawnflags='1',  # Remove on fire
                origin=options.get(Vec, 'global_ents_loc'),
            )
        else:
            glob_ent = GLOBAL_INPUT_ENTS[relay_name] = vmf.create_ent(
                classname='logic_relay',
                targetname=relay_name,
                origin=pos,
            )
    if not relay_name:
        output.output = 'OnMapSpawn'
        output.only_once = True
    output.comma_sep = False
    glob_ent.add_out(output)
Esempio n. 4
0
def res_global_input(vmf: VMF, inst: Entity, res: Property) -> None:
    """Trigger an input either on map spawn, or when a relay is triggered.

    Arguments:

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

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

    output: Output = out.copy()

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

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

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

    if on_load:
        try:
            ent = GLOBAL_INPUT_ENTS[ON_LOAD]
        except KeyError:
            ent = GLOBAL_INPUT_ENTS[ON_LOAD] = vmf.create_ent(
                'logic_auto',
                origin=options.get(Vec, 'global_ents_loc'),
                spawnflags='0',  # Don't remove on fire.
            )
        load_out = output.copy()
        output.output = 'OnMapSpawn'
        load_out.output = 'OnLoadGame'
        output.only_once = True
        ent.add_out(output, load_out)
    else:
        global_input(vmf, inst['origin'], output, relay_name)
Esempio n. 5
0
def precache_model(vmf: VMF, mdl_name: str,
                   skinset: Collection[int] = ()) -> None:
    """Precache the given model for switching.

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

    try:
        skins, ent = CACHED_MODELS[mdl_name]
    except KeyError:
        ent = vmf.create_ent(
            classname='comp_precache_model',
            origin=options.get(Vec, 'global_ents_loc'),
            model=mdl_name,
        )
        skins = set(skinset)
        CACHED_MODELS[mdl_name] = skins, ent
    else:
        if skins:  # If empty, it's wildcard so ignore specifics.
            if len(skinset) == 0:
                skins.clear()
            else:
                skins.update(skinset)
    if skins:
        ent['skinset'] = ' '.join(map(str, sorted(skinset)))
    else:
        ent['skinset'] = ''
Esempio n. 6
0
def make_voice_studio(vmf: VMF) -> bool:
    """Create the voice-line studio.

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

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

    if HAS_MONITOR and studio_file:
        conditions.add_inst(
            vmf,
            file=studio_file,
            origin=loc,
        )
        return True
    else:
        # If there aren't monitors, the studio instance isn't used.
        # We need to seal anyway.
        vmf.add_brushes(vmf.make_hollow(
            loc - 256,
            loc + 256,
            thick=32,
        ))
        return False
Esempio n. 7
0
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property):
    """Create the spawn point for ATLAS in the entry corridor.

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

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

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

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

    angles = normal.to_angle()

    if is_tag:
        vmf.create_ent(
            classname='func_instance',
            targetname='paint_gun',
            origin=origin - (0, 0, 16),
            angles=angles,
            # Generated by the BEE2 app.
            file='instances/bee2/tag_coop_gun.vmf',
        )
        # Blocks ATLAS from having a gun
        vmf.create_ent(
            classname='info_target',
            targetname='supress_blue_portalgun_spawn',
            origin=origin,
            angles='0 0 0',
        )
        # Allows info_target to work
        vmf.create_ent(
            classname='env_global',
            targetname='no_spawns',
            globalstate='portalgun_nospawn',
            initialstate=1,
            spawnflags=1,  # Use initial state
            origin=origin,
        )
    vmf.create_ent(
        classname='info_coop_spawn',
        targetname='@coop_spawn_blue',
        ForceGunOnSpawn=int(not is_tag),
        origin=origin,
        angles=angles,
        enabled=1,
        StartingTeam=3,  # ATLAS
    )
    return RES_EXHAUSTED
Esempio n. 8
0
def res_monitor(inst: Entity, res: Property) -> None:
    """Result for the monitor component.

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

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

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

    HAS_MONITOR = True

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

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

    # We need to generate an ai_relationship, which makes turrets hate
    # a bullseye.
    if needs_turret:
        loc = Vec(bullseye_loc)
        loc.localise(
            Vec.from_str(inst['origin']),
            Angle.from_str(inst['angles']),
        )
        bullseye_name = conditions.local_name(inst, bullseye_name)
        inst.map.create_ent(
            classname='npc_bullseye',
            targetname=bullseye_name,
            parentname=conditions.local_name(inst, bullseye_parent),
            spawnflags=221186,  # Non-solid, invisible, etc..
            origin=loc,
        )
        relation = inst.map.create_ent(
            classname='ai_relationship',
            targetname='@monitor_turr_hate',
            parentname=bullseye_name,  # When killed, destroy this too.
            spawnflags=2,  # Notify turrets about monitor locations
            disposition=1,  # Hate
            origin=loc,
            subject='npc_portal_turret_floor',
            target=bullseye_name,
        )
        MONITOR_RELATIONSHIP_ENTS.append(relation)
Esempio n. 9
0
def res_cave_portrait(vmf: VMF, inst: Entity, res: Property) -> None:
    """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. Config values match that of addOverlay.
    """
    skin = options.get(int, 'cave_port_skin')
    if skin is not None:
        new_inst = res_add_overlay_inst(vmf, inst, res)
        if new_inst is not None:
            new_inst.fixup['$skin'] = skin
Esempio n. 10
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 = options.get(float, 'glass_hole_size_large') / 2
        hole_size_small = 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)
        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)
Esempio n. 11
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 options.get(str, 'glass_pack') and has_attr['glass']:
        packing.pack_list(vmf, options.get(str, 'glass_pack'))
Esempio n. 12
0
def do_item_optimisation(vmf: VMF) -> None:
    """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.config is None or not item.config.input_type.is_logic:
            continue

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

        sec_inverted = conv_bool(conditions.resolve_value(
            item.inst,
            item.config.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 ant in item.antlines:
                ant.name = '_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=options.get(Vec, 'global_ents_loc'),
            targetname='_static_ind_tog',
            target='_static_ind',
        )
Esempio n. 13
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 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,
            )
Esempio n. 14
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=options.get(Vec, 'global_ents_loc'),
    )

    for i, file in enumerate(packlist, start=1):
        ent[file_type + str(i)] = file
Esempio n. 15
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 options.get(str, 'game_id') == utils.STEAM_IDS.get(
        flag.value.upper(),
        flag.value,
    )
Esempio n. 16
0
def res_add_global_inst(vmf: VMF, 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)])
    file = instanceLocs.resolve_one(res['file'], error=True)

    if res.bool('allow_multiple') or file.casefold() not in conditions.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 = vmf.create_ent(
            classname="func_instance",
            targetname=res['name', ''],
            file=file,
            angles=res['angles', '0 0 0'],
            fixup_style=res['fixup_style', '0'],
        )
        try:
            new_inst['origin'] = res['position']
        except IndexError:
            new_inst['origin'] = options.get(Vec, 'global_ents_loc')
        conditions.GLOBAL_INSTANCES.add(file.casefold())
        conditions.ALL_INST.add(file.casefold())
        if new_inst['targetname'] == '':
            new_inst['targetname'] = "inst_"
            new_inst.make_unique()
    return conditions.RES_EXHAUSTED
Esempio n. 17
0
def add(
    vmf: VMF,
    loc: Vec,
    conf: Property,
    voice_attr: Dict[str, str],
    is_sp: bool,
) -> None:
    """Add music to the map."""
    LOGGER.info("Adding Music...")
    # These values are exported by the BEE2 app, indicating the
    # options on the music item.
    inst = options.get(str, 'music_instance')
    snd_length = options.get(int, 'music_looplen')

    # Don't add our logic if an instance was provided.
    # If this settings is set, we have a music config.
    if conf and not inst:
        music = vmf.create_ent(
            classname='ambient_generic',
            spawnflags='17',  # Looping, Infinite Range, Starts Silent
            targetname='@music',
            origin=loc,
            message='music.BEE2',
            health='10',  # Volume
        )

        music_start = vmf.create_ent(
            classname='logic_relay',
            spawnflags='0',
            targetname='@music_start',
            origin=loc + (-16, 0, -16),
        )
        music_stop = vmf.create_ent(
            classname='logic_relay',
            spawnflags='0',
            targetname='@music_stop',
            origin=loc + (16, 0, -16),
        )
        music_stop.add_out(
            Output('OnTrigger', music, 'StopSound'),
            Output('OnTrigger', music, 'Volume', '0'),
        )

        # In SinglePlayer, music gets killed during reload,
        # so we need to restart it.

        # If snd_length is set, we have a non-loopable MP3
        # and want to re-trigger it after the time elapses, to simulate
        # looping.

        # In either case, we need @music_restart to do that safely.
        if is_sp or snd_length > 0:
            music_restart = vmf.create_ent(
                classname='logic_relay',
                spawnflags='2',  # Allow fast retrigger.
                targetname='@music_restart',
                StartDisabled='1',
                origin=loc + (0, 0, -16),
            )

            music_start.add_out(
                Output('OnTrigger', music_restart, 'Enable'),
                Output('OnTrigger', music_restart, 'Trigger', delay=0.01),
            )

            music_stop.add_out(
                Output('OnTrigger', music_restart, 'Disable'),
                Output('OnTrigger', music_restart, 'CancelPending'),
            )

            music_restart.add_out(
                Output('OnTrigger', music, 'StopSound'),
                Output('OnTrigger', music, 'Volume', '0'),
                Output('OnTrigger', music, 'Volume', '10', delay=0.1),
                Output('OnTrigger', music, 'PlaySound', delay=0.1),
            )

            if is_sp == 'SP':
                # Trigger on level loads.
                vmf.create_ent(
                    classname='logic_auto',
                    origin=loc + (0, 0, 16),
                    spawnflags='0',  # Don't remove after fire
                    globalstate='',
                ).add_out(
                    Output('OnLoadGame', music_restart, 'CancelPending'),
                    Output('OnLoadGame', music_restart, 'Trigger', delay=0.01),
                )

            if snd_length > 0:
                # Re-trigger after the music duration.
                music_restart.add_out(
                    Output('OnTrigger', '!self', 'Trigger', delay=snd_length)
                )
                # Set to non-looping, so re-playing will restart it correctly.
                music['spawnflags'] = '49'
        else:
            # The music track never needs to have repeating managed,
            # just directly trigger.
            music_start.add_out(
                Output('OnTrigger', music, 'PlaySound'),
                Output('OnTrigger', music, 'Volume', '10'),
            )

        # Add the ents for the config itself.
        # If the items aren't in the map, we can skip adding them.
        # Speed-gel sounds also play when flinging, so keep it always.
        funnel = conf.find_key('tbeam', or_blank=True)
        bounce = conf.find_key('bouncegel', or_blank=True)

        make_channel_conf(
            vmf, loc,
            Channel.BASE,
            conf.find_key('base', or_blank=True).as_array(),
        )
        make_channel_conf(
            vmf, loc,
            Channel.SPEED,
            conf.find_key('speedgel', or_blank=True).as_array(),
        )
        if 'funnel' in voice_attr or 'excursionfunnel' in voice_attr:
            make_channel_conf(
                vmf, loc,
                Channel.TBEAM,
                funnel.as_array(),
                conf.bool('sync_funnel'),
            )

        if 'bouncegel' in voice_attr or 'bluegel' in voice_attr:
            make_channel_conf(
                vmf, loc,
                Channel.BOUNCE,
                bounce.as_array(),
            )

        packfiles = conf.find_key('pack', or_blank=True).as_array()
        if packfiles:
            packer = vmf.create_ent('comp_pack', origin=loc)
            for i, fname in enumerate(packfiles, 1):
                packer[f'generic{i:02}'] = fname

    if inst:
        # We assume the instance is setup correct.
        vmf.create_ent(
            classname='func_instance',
            targetname='music',
            angles='0 0 0',
            origin=loc,
            file=inst,
            fixup_style='0',
        )
Esempio n. 18
0
def add_timer_relay(item: Item, has_sounds: bool) -> None:
    """Make a relay to play timer sounds, or fire once the outputs are done."""
    assert item.timer is not None

    rl_name = item.name + '_timer_rl'

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

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

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

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

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

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

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

    for outputs, cmd in [(item.timer_output_start(), 'Trigger'),
                         (item.timer_output_stop(), 'CancelPending')]:
        for output in outputs:
            item.add_io_command(output, rl_name, cmd)
Esempio n. 19
0
def gen_item_outputs(vmf: VMF) -> None:
    """Create outputs for all items with connections.

    This performs an optimization pass over items with outputs to remove
    redundancy, then applies all the outputs to the instances. Before this,
    connection count and inversion values are not valid. After this point,
    items may not have connections altered.
    """
    LOGGER.info('Generating item IO...')

    pan_switching_check = options.get(PanelSwitchingStyle,
                                      'ind_pan_check_switching')
    pan_switching_timer = options.get(PanelSwitchingStyle,
                                      'ind_pan_timer_switching')

    pan_check_type = ITEM_TYPES['item_indicator_panel']
    pan_timer_type = ITEM_TYPES['item_indicator_panel_timer']

    # For logic items without inputs, collect the instances to fix up later.
    dummy_logic_ents: list[Entity] = []

    # Apply input A/B types to connections.
    # After here, all connections are primary or secondary only.
    for item in ITEMS.values():
        for conn in item.outputs:
            # If not a dual item, it's primary.
            if conn.to_item.config.input_type is not InputType.DUAL:
                conn.type = ConnType.PRIMARY
                continue
            # If already set, that is the priority.
            if conn.type is not ConnType.DEFAULT:
                continue
            # Our item set the type of outputs.
            if item.config.output_type is not ConnType.DEFAULT:
                conn.type = item.config.output_type
            else:
                # Use the affinity of the target.
                conn.type = conn.to_item.config.default_dual

    do_item_optimisation(vmf)

    # We go 'backwards', creating all the inputs for each item.
    # That way we can change behaviour based on item counts.
    for item in ITEMS.values():
        if item.config is None:
            continue

        # Try to add the locking IO.
        add_locking(item)

        # Check we actually have timers, and that we want the relay.
        if item.timer is not None and (item.config.timer_sound_pos is not None
                                       or item.config.timer_done_cmd):
            has_sound = item.config.force_timer_sound or len(
                item.ind_panels) > 0
            add_timer_relay(item, has_sound)

        # Add outputs for antlines.
        if item.antlines or item.ind_panels:
            if item.timer is None:
                add_item_indicators(item, pan_switching_check, pan_check_type)
            else:
                add_item_indicators(item, pan_switching_timer, pan_timer_type)

        if item.config.input_type is InputType.DUAL:
            prim_inputs = [
                conn for conn in item.inputs
                if conn.type is ConnType.PRIMARY or conn.type is ConnType.BOTH
            ]
            sec_inputs = [
                conn for conn in item.inputs if conn.type is ConnType.SECONDARY
                or conn.type is ConnType.BOTH
            ]
            add_item_inputs(
                dummy_logic_ents,
                item,
                InputType.AND,
                prim_inputs,
                consts.FixupVars.BEE_CONN_COUNT_A,
                item.enable_cmd,
                item.disable_cmd,
                item.config.invert_var,
                item.config.spawn_fire,
                '_prim_inv_rl',
            )
            add_item_inputs(
                dummy_logic_ents,
                item,
                InputType.AND,
                sec_inputs,
                consts.FixupVars.BEE_CONN_COUNT_B,
                item.sec_enable_cmd,
                item.sec_disable_cmd,
                item.config.sec_invert_var,
                item.config.sec_spawn_fire,
                '_sec_inv_rl',
            )
        else:
            add_item_inputs(
                dummy_logic_ents,
                item,
                item.config.input_type,
                list(item.inputs),
                consts.FixupVars.CONN_COUNT,
                item.enable_cmd,
                item.disable_cmd,
                item.config.invert_var,
                item.config.spawn_fire,
                '_inv_rl',
            )

    # Check/cross instances sometimes don't match the kind of timer delay.
    # We also might want to swap them out.

    panel_timer = instanceLocs.resolve_one('[indPanTimer]', error=True)
    panel_check = instanceLocs.resolve_one('[indPanCheck]', error=True)

    for item in ITEMS.values():
        desired_panel_inst = panel_check if item.timer is None else panel_timer

        for pan in item.ind_panels:
            pan['file'] = desired_panel_inst
            pan.fixup[consts.FixupVars.TIM_ENABLED] = item.timer is not None

    logic_auto = vmf.create_ent('logic_auto',
                                origin=options.get(Vec, 'global_ents_loc'))

    for ent in dummy_logic_ents:
        # Condense all these together now.
        # User2 is the one that enables the target.
        ent.remove()
        for out in ent.outputs:
            if out.output == 'OnUser2':
                out.output = 'OnMapSpawn'
                logic_auto.add_out(out)
                out.only_once = True

    LOGGER.info('Item IO generated.')
Esempio n. 20
0
def res_resizeable_trigger(vmf: VMF, res: Property):
    """Replace two markers with a trigger brush.

    This is run once to affect all of an item.
    Options:

    * `markerInst`: <ITEM_ID:1,2> value referencing the marker instances, or a filename.
    * `markerItem`: The item's ID
    * `previewConf`: A item config which enables/disables the preview overlay.
    * `previewInst`: An instance to place at the marker location in preview mode.
        This should contain checkmarks to display the value when testing.
    * `previewMat`: If set, the material to use for an overlay func_brush.
        The brush will be parented to the trigger, so it vanishes once killed.
        It is also non-solid.
    * `previewScale`: The scale for the func_brush materials.
    * `previewActivate`, `previewDeactivate`: The VMF output to turn the
        previewInst on and off.
    * `triggerActivate, triggerDeactivate`: The `instance:name;Output`
        outputs used when the trigger turns on or off.
    * `coopVar`: The instance variable which enables detecting both Coop players.
        The trigger will be a trigger_playerteam.
    * `coopActivate, coopDeactivate`: The `instance:name;Output` outputs used
        when coopVar is enabled. These should be suitable for a logic_coop_manager.
    * `coopOnce`: If true, kill the manager after it first activates.
    * `keys`: A block of keyvalues for the trigger brush. Origin and targetname
        will be set automatically.
    * `localkeys`: The same as above, except values will be changed to use
        instance-local names.
    """
    marker = instanceLocs.resolve(res['markerInst'])

    marker_names = set()

    inst = None
    for inst in vmf.by_class['func_instance']:
        if inst['file'].casefold() in marker:
            marker_names.add(inst['targetname'])
            # Unconditionally delete from the map, so it doesn't
            # appear even if placed wrongly.
            inst.remove()
    del inst  # Make sure we don't use this later.

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

    item_id = res['markerItem']

    # Synthesise the connection config used for the final trigger.
    conn_conf_sp = connections.Config(
        id=item_id + ':TRIGGER',
        output_act=Output.parse_name(res['triggerActivate',
                                         'OnStartTouchAll']),
        output_deact=Output.parse_name(res['triggerDeactivate',
                                           'OnEndTouchAll']),
    )

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

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

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

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

        inst1 = mark1.inst
        inst2 = mark2.inst

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

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

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

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

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

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

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

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

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

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

        if preview_mat:
            preview_brush = vmf.create_ent(
                classname='func_brush',
                parentname=targ,
                origin=origin,
                Solidity='1',  # Not solid
                drawinfastreflection='1',  # Draw in goo..

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

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

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

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

    return RES_EXHAUSTED
Esempio n. 21
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 = 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
                # locations 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

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

        # Build min, max tuples for each axis in the other direction.
        # This tells us where the beams will be.
        beams: dict[float, tuple[float, float]] = {}

        # 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)
Esempio n. 22
0
def make_barriers(vmf: VMF):
    """Make barrier entities. get_tex is vbsp.get_tex."""
    glass_temp = template_brush.get_scaling_template(
        options.get(str, "glass_template"))
    grate_temp = template_brush.get_scaling_template(
        options.get(str, "grating_template"))
    hole_temp_small: List[Solid]
    hole_temp_lrg_diag: List[Solid]
    hole_temp_lrg_cutout: List[Solid]
    hole_temp_lrg_square: List[Solid]

    # Avoid error without this package.
    if HOLES:
        # Grab the template solids we need.
        hole_combined_temp = template_brush.get_template(
            options.get(str, 'glass_hole_temp'))
        hole_world, hole_detail, _ = hole_combined_temp.visgrouped({'small'})
        hole_temp_small = hole_world + hole_detail
        hole_world, hole_detail, _ = hole_combined_temp.visgrouped(
            {'large_diagonal'})
        hole_temp_lrg_diag = hole_world + hole_detail
        hole_world, hole_detail, _ = hole_combined_temp.visgrouped(
            {'large_cutout'})
        hole_temp_lrg_cutout = hole_world + hole_detail
        hole_world, hole_detail, _ = hole_combined_temp.visgrouped(
            {'large_square'})
        hole_temp_lrg_square = hole_world + hole_detail
    else:
        hole_temp_small = hole_temp_lrg_diag = hole_temp_lrg_cutout = hole_temp_lrg_square = []

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

    if options.get_itemconf('BEE_PELLET:PelletGrating', False):
        # Merge together these existing filters in global_pti_ents
        vmf.create_ent(
            origin=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=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: Dict[Tuple[Tuple[float, float, float], bool, BarrierType],
                 Dict[Tuple[int, int], False]] = defaultdict(dict)
    # We have this on the 32-grid so we can cut squares for holes.

    for (origin_tup, normal_tup), barr_type in BARRIERS.items():
        origin = Vec(origin_tup)
        normal = Vec(normal_tup)
        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[int((u + u_off) // 32),
                            int((v + v_off) // 32), ] = True

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

        origin = Vec(origin_tup)
        normal = Vec(norm_tup)
        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)
        else:
            offsets = (-16, 16)
        for u_off in offsets:
            for v_off in offsets:
                # Remove these squares, but keep them in the dict
                # so we can check if there was glass there.
                uv = (
                    int((u + u_off) // 32),
                    int((v + v_off) // 32),
                )
                if uv in slice_plane:
                    slice_plane[uv] = False
                # These have to be present, except for the corners
                # on the large hole.
                elif abs(u_off) != 80 or abs(v_off) != 80:
                    u_ax, v_ax = Vec.INV_AXIS[norm_axis]
                    LOGGER.warning(
                        'Hole tried to remove missing tile at ({})?',
                        Vec.with_axes(norm_axis, norm_pos, u_ax, u + u_off,
                                      v_ax, v + v_off),
                    )

        # 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()
        hole_temp: List[Tuple[List[Solid], Matrix]] = []

        # This is a tricky bit. Two large templates would collide
        # diagonally, and we allow the corner glass to not be present since
        # the hole doesn't actually use that 32x32 segment.
        # So we need to determine which of 3 templates to use.
        corn_angles = angles.copy()
        if hole_type is HoleType.LARGE:
            for corn_angles.roll in (0, 90, 180, 270):
                corn_mat = Matrix.from_angle(corn_angles)

                corn_dir = Vec(y=1, z=1) @ corn_angles
                hole_off = origin + 128 * corn_dir
                diag_type = HOLES.get(
                    (hole_off.as_tuple(), normal.as_tuple()),
                    None,
                )
                corner_pos = origin + 80 * corn_dir
                corn_u, corn_v = corner_pos.other_axes(norm_axis)
                corn_u = int(corn_u // 32)
                corn_v = int(corn_v // 32)

                if diag_type is HoleType.LARGE:
                    # There's another large template to this direction.
                    # Just have 1 generate both combined, so the brushes can
                    # be more optimal. To pick, arbitrarily make the upper one
                    # be in charge.
                    if corn_v > v // 32:
                        hole_temp.append((hole_temp_lrg_diag, corn_mat))
                    continue
                if (corn_u, corn_v) in slice_plane:
                    hole_temp.append((hole_temp_lrg_square, corn_mat))
                else:
                    hole_temp.append((hole_temp_lrg_cutout, corn_mat))

        else:
            hole_temp.append((hole_temp_small, Matrix.from_angle(angles)))

        def solid_pane_func(off1: float, off2: float, mat: str) -> List[Solid]:
            """Given the two thicknesses, produce the curved hole from the template."""
            off_min = 64 - max(off1, off2)
            off_max = 64 - min(off1, off2)
            new_brushes = []
            for brushes, matrix in hole_temp:
                for orig_brush in brushes:
                    brush = orig_brush.copy(vmf_file=vmf)
                    new_brushes.append(brush)
                    for face in brush.sides:
                        face.mat = mat
                        for point in face.planes:
                            if point.x > 64:
                                point.x = off_max
                            else:
                                point.x = off_min
                        face.localise(origin, matrix)
                        # Increase precision, these are small detail brushes.
                        face.lightmap = 8
            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(pos_slice):
            # 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: float, pos2: float,
                                mat: str) -> List[Solid]:
                """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 + 63 * normal,
                normal,
                barr_type,
                front_temp,
                solid_pane_func,
            )
            # Generate hint brushes, to ensure sorting is done correctly.
            [hint] = solid_pane_func(0, 4.0, consts.Tools.SKIP)
            for side in hint:
                if abs(Vec.dot(side.normal(), normal)) > 0.99:
                    side.mat = consts.Tools.HINT
            vmf.add_brush(hint)

    if floorbeam_temp:
        LOGGER.info('Adding Glass floor beams...')
        add_glass_floorbeams(vmf, floorbeam_temp)
        LOGGER.info('Done!')
Esempio n. 23
0
def get_studio_pose() -> Vec:
    """Return the position of the studio camera."""
    return voice_line.get_studio_loc() + options.get(Vec,
                                                     'voice_studio_cam_loc')
Esempio n. 24
0
def make_bottomless_pit(vmf: VMF, max_height):
    """Generate bottomless pits."""
    import vbsp

    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 = 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 hurt_trig is not None:
        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()
Esempio n. 25
0
def res_cave_portrait() -> bool:
    """Checks to see if the Cave Portrait option is set for the given voice pack.
    """
    return global_bool(options.get(int, 'cave_port_skin') is not None)
Esempio n. 26
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_conn_conf,
        inst_frame_double,
        inst_frame_single,
        blue_sign_on,
        blue_sign_off,
        oran_sign_on,
        oran_sign_off,
    ) = res.value  # type: int, Optional[connections.Config], str, str, str, str, str, str
    import vbsp
    if 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 = connections.ITEMS[inst['targetname']]
    for conn in list(sign_item.outputs):
        if conn.to_item.name in FIZZLERS:
            if fizzler is None:
                fizzler = FIZZLERS[conn.to_item.name]
                fizzler_item = conn.to_item
            else:
                raise ValueError('Multiple fizzlers attached to a sign!')

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

    sign_item.delete_antlines()

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

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

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

    # And also swap the connection's type.
    if fizz_conn_conf is not None:
        fizzler_item.config = fizz_conn_conf
        fizzler_item.enable_cmd = fizz_conn_conf.enable_cmd
        fizzler_item.disable_cmd = fizz_conn_conf.disable_cmd
        fizzler_item.sec_enable_cmd = fizz_conn_conf.sec_enable_cmd
        fizzler_item.sec_disable_cmd = fizz_conn_conf.sec_disable_cmd

    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']))

    fizz_norm_axis = fizzler.normal().axis()

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

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

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

    inst_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.

        s, l = Vec.bbox(itertools.chain.from_iterable(fizzler.emitters))

        if fizz_norm_axis == 'z':
            # For z-axis, just compare to the center point of the emitters.
            sign_dir = ((s.x + l.x) / 2, (s.y + l.y) / 2, 0) - sign_floor_loc
        else:
            # For the other two, we compare to the line,
            # or compare to the closest side (in line with the fizz)

            if fizz_norm_axis == 'x':  #  Extends in Y direction
                other_axis = 'y'
                side_min = s.y
                side_max = l.y
                normal = s.x
            else:  # Extends in X direction
                other_axis = 'x'
                side_min = s.x
                side_max = l.x
                normal = s.y

            # Right in line with the fizzler. Point at the closest emitter.
            if abs(sign_floor_loc[other_axis] - normal) < 32:
                # Compare to the closest side.
                sign_dir = min(
                    (sign_floor_loc - Vec.with_axes(
                        fizz_norm_axis,
                        side_min,
                        other_axis,
                        normal,
                    ), sign_floor_loc - Vec.with_axes(
                        fizz_norm_axis,
                        side_max,
                        other_axis,
                        normal,
                    )),
                    key=Vec.mag,
                )
            else:
                # Align just based on whether we're in front or behind.
                sign_dir = Vec.with_axes(
                    fizz_norm_axis,
                    normal - sign_floor_loc[fizz_norm_axis]).norm()

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

        # Rotate to fit the instances - south is down
        sign_yaw = int(sign_yaw - 90) % 360

        if inst_normal.z > 0:
            sign_angle = '0 {} 0'.format(sign_yaw)
        elif inst_normal.z < 0:
            # Flip upside-down for ceilings
            sign_angle = '0 {} 180'.format(sign_yaw)
        else:
            raise AssertionError('Cannot be zero here!')
    else:
        # On a wall, face upright
        sign_angle = 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]

    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, )
Esempio n. 27
0
def add_voice(
    voice_attrs: dict,
    style_vars: dict,
    vmf: VMF,
    map_seed: str,
    use_priority=True,
) -> None:
    """Add a voice line to the map."""
    from precomp.conditions.monitor import make_voice_studio
    LOGGER.info('Adding Voice Lines!')

    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.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 = make_voice_studio(vmf)

    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(vmf, 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.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.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..')
        encode_coop_responses(vmf, quote_loc, allow_dings, voice_attrs)

    for ind, file in enumerate(QUOTE_EVENTS.values()):
        if not file:
            continue
        vmf.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(
                vmf,
                group,
                mid_quotes,
                use_dings=use_dings,
                allow_mid_voices=allow_mid_voices,
                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,
        )

        LOGGER.debug('Possible {}quotes:',
                     'mid ' if group.name == 'midchamber' else '')
        for quot in possible_quotes:
            LOGGER.debug('- {}', quot)

        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(
                vmf,
                random.choice(chosen),
                quote_targetname,
                choreo_loc,
                style_vars,
                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.create_ent(
                classname='env_microphone',
                targetname='player_speaker_sp',
                speakername='!player',
                maxRange='386',
                origin=quote_loc,
            )
        else:
            vmf.create_ent(
                classname='env_microphone',
                targetname='player_speaker_blue',
                speakername='!player_blue',
                maxRange='386',
                origin=quote_loc,
            )
            vmf.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(vmf, mid_item, mid_name, quote_loc, style_vars, use_ding)

    LOGGER.info('Done!')
Esempio n. 28
0
def mon_camera_link(vmf: VMF) -> None:
    """Link cameras to monitors."""
    import vbsp

    if not HAS_MONITOR:
        return

    ALL_CAMERAS.sort(key=lambda cam: cam.cam_pos)

    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:
        # No cameras start active, we need to be positioned elsewhere.
        if options.get(str, 'voice_studio_inst'):
            # Start at the studio, if it exists.
            start_pos = get_studio_pose()
            start_angles = '{:g} {:g} 0'.format(
                options.get(float, 'voice_studio_cam_pitch'),
                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 = Vec(-2500, -2500, 0)
            start_angles = '0 90 0'

    cam_ent = 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'],
    )

    if not ALL_CAMERAS:
        return
        # We only need the script if we're moving at all.
    cam_ent['vscripts'] = 'BEE2/mon_camera.nut'
    cam_ent['thinkfunction'] = 'Think'

    # Now start adding all the variables the script needs.
    # Tell it the number of cameras, and how many start active.
    # That lets it trivially determine when they're all off.
    # We keep the list of active counts to reuse after.
    active_counts = [
        srctools.conv_int(cam.inst.fixup['$start_enabled', '0'])
        for cam in ALL_CAMERAS
    ]
    scriptvar_set(cam_ent, start_pos - (0, 0, 16), 'CAM_NUM', len(ALL_CAMERAS))
    scriptvar_set(cam_ent, start_pos - (0, 0, 16), 'CAM_ACTIVE_NUM',
                  sum(active_counts))
    # Then add the values for each camera. We can use the setter's modes
    # to include the position as the actual loc.
    for i, (cam, active) in enumerate(zip(ALL_CAMERAS, active_counts)):
        scriptvar_set(
            cam_ent,
            cam.cam_pos,
            'CAM_LOC',
            index=i,
            angles=cam.cam_angles,
            mode='pos',
        )
        scriptvar_set(
            cam_ent,
            cam.cam_pos,
            'CAM_ANGLES',
            index=i,
            angles=cam.cam_angles,
            mode='ang',
        )
        scriptvar_set(
            cam_ent,
            cam.cam_pos + (0, 0, 8),
            'CAM_ACTIVE',
            index=i,
            value=active,
        )

    if options.get(str, 'voice_studio_inst'):
        # We have a voice studio, send values to the script.
        scriptvar_set(cam_ent, get_studio_pose(), 'CAM_STUDIO_LOC', mode='pos')
        scriptvar_set(
            cam_ent,
            get_studio_pose(),
            'CAM_STUDIO_ANG',
            mode='ang',
            angles='{:g} {:g} 0'.format(
                options.get(float, 'voice_studio_cam_pitch'),
                options.get(float, 'voice_studio_cam_yaw'),
            ),
        )
        use_turret = '1' if MONITOR_RELATIONSHIP_ENTS else '0'
        swap_chance = options.get(float, 'voice_studio_inter_chance')
    else:
        use_turret = '0'
        swap_chance = -1

    scriptvar_set(cam_ent, start_pos + (0, 0, 16), 'CAM_STUDIO_TURRET',
                  use_turret)
    scriptvar_set(cam_ent, start_pos + (0, 0, 16), 'CAM_STUDIO_CHANCE',
                  swap_chance)
Esempio n. 29
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=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=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
    ]