Пример #1
0
def res_replace_instance(inst: Entity, res: Property):
    """Replace an instance with another entity.

    `keys` and `localkeys` defines the new keyvalues used.
    `targetname` and `angles` are preset, and `origin` will be used to offset
    the given amount from the current location.
    If `keep_instance` is true, the instance entity will be kept instead of
    removed.
    """
    import vbsp

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

    if not srctools.conv_bool(res['keep_instance', '0'], False):
        inst.remove()  # Do this first to free the ent ID, so the new ent has
        # the same one.

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

    vbsp.VMF.add_ent(new_ent)

    conditions.set_ent_keys(new_ent, inst, res)

    origin += Vec.from_str(new_ent['origin']).rotate_by_str(angles)
    new_ent['origin'] = origin
    new_ent['angles'] = angles
    new_ent['targetname'] = inst['targetname']
Пример #2
0
def res_sendificator(vmf: VMF, inst: Entity):
    """Implement Sendificators."""
    # For our version, we know which sendtor connects to what laser,
    # so we can couple the logic together (avoiding @sendtor_mutex).

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

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

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

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

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

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

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

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

        relay['StartDisabled'] = not is_on
        las_item.enable_cmd += (Output('', relay_name, 'Enable'),)
        las_item.disable_cmd += (Output('', relay_name, 'Disable'),)
        LOGGER.info('Relay: {}', relay)
Пример #3
0
def join_markers(inst_a, inst_b, is_start=False):
    """Join two marker ents together with corners.

    This returns a list of solids used for the vphysics_motion trigger.
    """
    origin_a = Vec.from_str(inst_a['ent']['origin'])
    origin_b = Vec.from_str(inst_b['ent']['origin'])

    norm_a = Vec(-1, 0, 0).rotate_by_str(inst_a['ent']['angles'])
    norm_b = Vec(-1, 0, 0).rotate_by_str(inst_b['ent']['angles'])

    config = inst_a['conf']

    if norm_a == norm_b:
        # Either straight-line, or s-bend.
        dist = (origin_a - origin_b).mag()

        if origin_a + (norm_a * dist) == origin_b:
            make_straight(
                origin_a,
                norm_a,
                dist,
                config,
                is_start,
            )
        # else: S-bend, we don't do the geometry for this..
        return

    if norm_a == -norm_b:
        # U-shape bend..
        make_ubend(
            origin_a,
            origin_b,
            norm_a,
            config,
            max_size=inst_a['size'],
        )
        return

    try:
        corner_ang, flat_angle = CORNER_ANG[norm_a.as_tuple(), norm_b.as_tuple()]

        if origin_a[flat_angle] != origin_b[flat_angle]:
            # It needs to be flat in this angle!
            raise ValueError
    except ValueError:
        # The tubes need two corners to join together - abort for that.
        return
    else:
        make_bend(
            origin_a,
            origin_b,
            norm_a,
            norm_b,
            corner_ang,
            config,
            max_size=inst_a['size'],
        )
Пример #4
0
def res_unst_scaffold_setup(res: Property):
    group = res['group', 'DEFAULT_GROUP']

    if group not in SCAFFOLD_CONFIGS:
        # Store our values in the CONFIGS dictionary
        targ_inst, links = SCAFFOLD_CONFIGS[group] = {}, {}
    else:
        # Grab the already-filled values, and add to them
        targ_inst, links = SCAFFOLD_CONFIGS[group]

    for block in res.find_all("Instance"):
        conf = {
            # If set, adjusts the offset appropriately
            'is_piston': srctools.conv_bool(block['isPiston', '0']),
            'rotate_logic': srctools.conv_bool(block['AlterAng', '1'], True),
            'off_floor': Vec.from_str(block['FloorOff', '0 0 0']),
            'off_wall': Vec.from_str(block['WallOff', '0 0 0']),

            'logic_start': block['startlogic', ''],
            'logic_end': block['endLogic', ''],
            'logic_mid': block['midLogic', ''],

            'logic_start_rev': block['StartLogicRev', None],
            'logic_end_rev': block['EndLogicRev', None],
            'logic_mid_rev': block['EndLogicRev', None],

            'inst_wall': block['wallInst', ''],
            'inst_floor': block['floorInst', ''],
            'inst_offset': block['offsetInst', None],
            # Specially rotated to face the next track!
            'inst_end': block['endInst', None],
        }
        for logic_type in ('logic_start', 'logic_mid', 'logic_end'):
            if conf[logic_type + '_rev'] is None:
                conf[logic_type + '_rev'] = conf[logic_type]

        for inst in instanceLocs.resolve(block['file']):
            targ_inst[inst] = conf

    # We need to provide vars to link the tracks and beams.
    for block in res.find_all('LinkEnt'):
        # The name for this set of entities.
        # It must be a '@' name, or the name will be fixed-up incorrectly!
        loc_name = block['name']
        if not loc_name.startswith('@'):
            loc_name = '@' + loc_name
        links[block['nameVar']] = {
            'name': loc_name,
            # The next entity (not set in end logic)
            'next': block['nextVar'],
            # A '*' name to reference all the ents (set on the start logic)
            'all': block['allVar', None],
        }

    return group  # We look up the group name to find the values.
Пример #5
0
def res_camera_setup(res: Property):
    return {
        'cam_off': Vec.from_str(res['CamOff', '']),
        'yaw_off': Vec.from_str(res['YawOff', '']),
        'pitch_off': Vec.from_str(res['PitchOff', '']),

        'yaw_inst': instanceLocs.resolve_one(res['yawInst', '']),
        'pitch_inst': instanceLocs.resolve_one(res['pitchInst', '']),

        'yaw_range': srctools.conv_int(res['YawRange', ''], 90),
        'pitch_range': srctools.conv_int(res['YawRange', ''], 90),
    }
Пример #6
0
def res_unst_scaffold_setup(res: Property):
    group = res["group", "DEFAULT_GROUP"]

    if group not in SCAFFOLD_CONFIGS:
        # Store our values in the CONFIGS dictionary
        targ_inst, links = SCAFFOLD_CONFIGS[group] = {}, {}
    else:
        # Grab the already-filled values, and add to them
        targ_inst, links = SCAFFOLD_CONFIGS[group]

    for block in res.find_all("Instance"):
        conf = {
            # If set, adjusts the offset appropriately
            "is_piston": srctools.conv_bool(block["isPiston", "0"]),
            "rotate_logic": srctools.conv_bool(block["AlterAng", "1"], True),
            "off_floor": Vec.from_str(block["FloorOff", "0 0 0"]),
            "off_wall": Vec.from_str(block["WallOff", "0 0 0"]),
            "logic_start": block["startlogic", ""],
            "logic_end": block["endLogic", ""],
            "logic_mid": block["midLogic", ""],
            "logic_start_rev": block["StartLogicRev", None],
            "logic_end_rev": block["EndLogicRev", None],
            "logic_mid_rev": block["EndLogicRev", None],
            "inst_wall": block["wallInst", ""],
            "inst_floor": block["floorInst", ""],
            "inst_offset": block["offsetInst", None],
            # Specially rotated to face the next track!
            "inst_end": block["endInst", None],
        }
        for logic_type in ("logic_start", "logic_mid", "logic_end"):
            if conf[logic_type + "_rev"] is None:
                conf[logic_type + "_rev"] = conf[logic_type]

        for inst in resolve_inst(block["file"]):
            targ_inst[inst] = conf

    # We need to provide vars to link the tracks and beams.
    for block in res.find_all("LinkEnt"):
        # The name for this set of entities.
        # It must be a '@' name, or the name will be fixed-up incorrectly!
        loc_name = block["name"]
        if not loc_name.startswith("@"):
            loc_name = "@" + loc_name
        links[block["nameVar"]] = {
            "name": loc_name,
            # The next entity (not set in end logic)
            "next": block["nextVar"],
            # A '*' name to reference all the ents (set on the start logic)
            "all": block["allVar", None],
        }

    return group  # We look up the group name to find the values.
Пример #7
0
def flag_goo_at_loc(inst: Entity, flag: Property):
    """Check to see if a given location is submerged in goo.

    0 0 0 is the origin of the instance, values are in 128 increments.
    """
    pos = Vec.from_str(flag.value).rotate_by_str(inst['angles', '0 0 0'])
    pos *= 128
    pos += Vec.from_str(inst['origin'])

    # Round to 128 units, then offset to the center
    pos = pos // 128 * 128 + 64  # type: Vec
    val = pos.as_tuple() in GOO_LOCS
    return val
Пример #8
0
def res_make_funnel_light(inst: Entity):
    """Place a light for Funnel items."""
    oran_on = inst.fixup.bool('$start_reversed')
    need_blue = need_oran = False
    name = ''
    if inst.fixup['$connectioncount_polarity'] != '0':
        import vbsp
        if not vbsp.settings['style_vars']['funnelallowswitchedlights']:
            # Allow disabling adding switchable lights.
            return
        name = conditions.local_name(inst, 'light')
        need_blue = need_oran = True
    else:
        if oran_on:
            need_oran = True
        else:
            need_blue = True

    loc = Vec(0, 0, -56)
    loc.localise(Vec.from_str(inst['origin']), Vec.from_str(inst['angles']))

    if need_blue:
        inst.map.create_ent(
            classname='light',
            targetname=name + '_b' if name else '',
            spawnflags=int(oran_on),  # 1 = Initially Dark
            origin=loc,
            _light='50 120 250 50',
            _lightHDR='-1 -1 -1 1',
            _lightscaleHDR=2,
            _fifty_percent_distance=48,
            _zero_percent_distance=96,
            _hardfalloff=1,
            _distance=0,
            style=0,
        )
    if need_oran:
        inst.map.create_ent(
            classname='light',
            targetname=name + '_o' if name else '',
            spawnflags=int(not oran_on),
            origin=loc,
            _light='250 120 50 50',
            _lightHDR='-1 -1 -1 1',
            _lightscaleHDR=2,
            _fifty_percent_distance=48,
            _zero_percent_distance=96,
            _hardfalloff=1,
            _distance=0,
            style=0,
        )
Пример #9
0
def res_monitor(inst: Entity, res: Property) -> None:
    """Result for the monitor component.

    """
    import vbsp

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

    ALL_MONITORS.append(Monitor(inst))

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

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

    # We need to generate an ai_relationship, which makes turrets hate
    # a bullseye.
    if needs_turret:
        loc = Vec(bullseye_loc)
        loc.localise(
            Vec.from_str(inst['origin']),
            Vec.from_str(inst['angles']),
        )
        bullseye_name = local_name(inst, bullseye_name)
        inst.map.create_ent(
            classname='npc_bullseye',
            targetname=bullseye_name,
            parentname=local_name(inst, bullseye_parent),
            spawnflags=221186,  # Non-solid, invisible, etc..
            origin=loc,
        )
        relation = inst.map.create_ent(
            classname='ai_relationship',
            targetname='@monitor_turr_hate',
            spawnflags=2,  # Notify turrets about monitor locations
            disposition=1,  # Hate
            origin=loc,
            subject='npc_portal_turret_floor',
            target=bullseye_name,
        )
        MONITOR_RELATIONSHIP_ENTS.append(relation)
Пример #10
0
def get_config(
    node: item_chain.Node,
) -> Tuple[str, Vec]:
    """Compute the config values for a node."""

    orient = (
        'floor' if
        Vec(0, 0, 1).rotate_by_str(node.inst['angles']) == (0, 0, 1)
        else 'wall'
    )
    # Find the offset used for the platform.
    offset = (node.conf['off_' + orient]).copy()  # type: Vec
    if node.conf['is_piston']:
        # Adjust based on the piston position
        offset.z += 128 * srctools.conv_int(
            node.inst.fixup[
                '$top_level' if
                node.inst.fixup[
                    '$start_up'] == '1'
                else '$bottom_level'
            ]
        )
    offset.rotate_by_str(node.inst['angles'])
    offset += Vec.from_str(node.inst['origin'])
    return orient, offset
Пример #11
0
def find_glass_items(config, vmf: VMF) -> Iterator[Tuple[str, Vec, Vec, Vec, dict]]:
    """Find the bounding boxes for all the glass items matching a config.

    This yields (targetname, min, max, normal, config) tuples.
    """
    # targetname -> min, max, normal, config
    glass_items = {}
    for inst in vmf.by_class['func_instance']:  # type: Entity
        try:
            conf = config[inst['file'].casefold()]
        except KeyError:
            continue
        targ = inst['targetname']
        norm = Vec(x=1).rotate_by_str(inst['angles'])
        origin = Vec.from_str(inst['origin']) - 64 * norm
        try:
            bbox_min, bbox_max, group_norm, group_conf = glass_items[targ]
        except KeyError:
            # First of this group..
            bbox_min, bbox_max = origin.copy(), origin.copy()
            group_norm = norm.copy()
            glass_items[targ] = bbox_min, bbox_max, group_norm, conf
        else:
            bbox_min.min(origin)
            bbox_max.max(origin)
            assert group_norm == norm, '"{}" is inconsistently rotated!'.format(targ)
            assert group_conf is conf, '"{}" has multiple configs!'.format(targ)
        inst.remove()

    for targ, (bbox_min, bbox_max, norm, conf) in glass_items.items():
        yield targ, bbox_min, bbox_max, norm, conf
Пример #12
0
def res_glass_hole(inst: Entity, res: Property):
    """Add Glass/grating holes. The value should be 'large' or 'small'."""
    hole_type = HoleType(res.value)

    normal = Vec(z=-1).rotate_by_str(inst['angles'])
    origin = Vec.from_str(inst['origin']) // 128 * 128 + 64

    if test_hole_spot(origin, normal, hole_type):
        HOLES[origin.as_tuple(), normal.as_tuple()] = hole_type
        inst['origin'] = origin
        inst['angles'] = normal.to_angle()
        return

    # Test the opposite side of the glass too.

    inv_origin = origin + 128 * normal
    inv_normal = -normal

    if test_hole_spot(inv_origin, inv_normal, hole_type):
        HOLES[inv_origin.as_tuple(), inv_normal.as_tuple()] = hole_type
        inst['origin'] = inv_origin
        inst['angles'] = inv_normal.to_angle()
    else:
        # Remove the instance, so this does nothing.
        inst.remove()
Пример #13
0
def track_scan(
    tr_set: Set[Entity],
    track_inst: Dict[Tuple[float, float, float], Entity],
    start_track: Entity,
    middle_file: str,
    x_dir: int,
):
    """Build a set of track instances extending from a point.
    :param track_inst: A dictionary mapping origins to track instances
    :param start_track: The instance we start on
    :param middle_file: The file for the center track piece
    :param x_dir: The direction to look (-1 or 1)
    """
    track = start_track
    move_dir = Vec(x_dir*128, 0, 0).rotate_by_str(track['angles'])
    while track:
        tr_set.add(track)

        next_pos = Vec.from_str(track['origin']) + move_dir
        track = track_inst.get(next_pos.as_tuple(), None)
        if track is None:
            return
        if track['file'].casefold() != middle_file:
            # If the next piece is an end section, add it then quit
            tr_set.add(track)
            return
Пример #14
0
def mon_remove_bullseyes(inst: Entity) -> Optional[object]:
    """Remove bullsyes used for cameras."""
    if not BULLSYE_LOCS:
        return RES_EXHAUSTED

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

    origin = Vec(0, 0, -64)
    origin.localise(Vec.from_str(inst['origin']), Vec.from_str(inst['angles']))
    origin = origin.as_tuple()

    LOGGER.info('Pos: {} -> ', origin, BULLSYE_LOCS[origin])

    if BULLSYE_LOCS[origin]:
        BULLSYE_LOCS[origin] -= 1
        inst.remove()
Пример #15
0
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property):
    """Create the spawn point for ATLAS in the entry corridor.

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

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

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

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

    angles = normal.to_angle()

    if is_tag:
        vmf.create_ent(
            classname='func_instance',
            targetname='paint_gun',
            origin=origin - (0, 0, 16),
            angles=angles,
            # Generated by the BEE2 app.
            file='instances/bee2/tag_coop_gun.vmf',
        )
        # Blocks ATLAS from having a gun
        vmf.create_ent(
            classname='info_target',
            targetname='supress_blue_portalgun_spawn',
            origin=origin,
            angles='0 0 0',
        )
        # Allows info_target to work
        vmf.create_ent(
            classname='env_global',
            targetname='no_spawns',
            globalstate='portalgun_nospawn',
            initialstate=1,
            spawnflags=1,  # Use initial state
            origin=origin,
        )
    vmf.create_ent(
        classname='info_coop_spawn',
        targetname='@coop_spawn_blue',
        ForceGunOnSpawn=int(not is_tag),
        origin=origin,
        angles=angles,
        enabled=1,
        StartingTeam=3,  # ATLAS
    )
    return RES_EXHAUSTED
Пример #16
0
def resolve_offset(inst, value: str, scale: float=1, zoff: float=0) -> Vec:
    """Retrieve an offset from an instance var. This allows several special values:

    * $var to read from a variable
    * <piston_start> or <piston> to get the unpowered position of a piston plat
    * <piston_end> to get the powered position of a piston plat
    * <piston_top> to get the extended position of a piston plat
    * <piston_bottom> to get the retracted position of a piston plat

    If scale is set, read values are multiplied by this, and zoff is added to Z.
    """
    value = value.casefold()
    # Offset the overlay by the given distance
    # Some special placeholder values:
    if value == '<piston_start>' or value == '<piston>':
        if inst.fixup.bool(const.FixupVars.PIST_IS_UP):
            value = '<piston_top>'
        else:
            value = '<piston_bottom>'
    elif value == '<piston_end>':
        if inst.fixup.bool(const.FixupVars.PIST_IS_UP):
            value = '<piston_bottom>'
        else:
            value = '<piston_top>'

    if value == '<piston_bottom>':
        offset = Vec(
            z=inst.fixup.int(const.FixupVars.PIST_BTM) * 128,
        )
    elif value == '<piston_top>':
        offset = Vec(
            z=inst.fixup.int(const.FixupVars.PIST_TOP) * 128,
        )
    else:
        # Regular vector
        offset = Vec.from_str(resolve_value(inst, value)) * scale
    offset.z += zoff

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

    return offset
Пример #17
0
def res_water_splash_setup(res: Property):
    parent = res['parent']
    name = res['name']
    scale = srctools.conv_float(res['scale', ''], 8.0)
    pos1 = Vec.from_str(res['position', ''])
    calc_type = res['type', '']
    pos2 = res['position2', '']
    fast_check = srctools.conv_bool(res['fast_check', ''])

    return name, parent, scale, pos1, pos2, calc_type, fast_check
Пример #18
0
def res_import_template_setup(res: Property):
    temp_id = res['id'].casefold()

    face = Vec.from_str(res['face_pos', '0 0 -64'])
    norm = Vec.from_str(res['normal', '0 0 1'])

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

    offset = Vec.from_str(res['offset', '0 0 0'])

    return (
        temp_id,
        dict(replace_tex),
        face,
        norm,
        offset,
    )
Пример #19
0
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property):
    """Create the spawn point for ATLAS, in Aperture Tag.

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

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

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

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

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

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

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

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

    conditions.set_ent_keys(new_ent, inst, res)

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

    new_ent['origin'] = origin
    new_ent['angles'] = inst['angles']
Пример #21
0
 def get_color():
     """Parse out the color."""
     color = var.get()
     if color.startswith('#'):
         try:
             r = int(color[1:3], base=16)
             g = int(color[3:5], base=16)
             b = int(color[5:], base=16)
         except ValueError:
             LOGGER.warning('Invalid RGB value: "{}"!', color)
             r = g = b = 128
     else:
         r, g, b = map(int, Vec.from_str(color, 128, 128, 128))
     return r, g, b
Пример #22
0
def res_translate_inst(inst: Entity, res: Property):
    """Translate the instance locally by the given amount.

    The special values <piston>, <piston_bottom> and <piston_top> can be
    used to offset it based on the starting position, bottom or top position
    of a piston platform.
    """
    folded_val = res.value.casefold()
    if folded_val == '<piston>':
        folded_val = (
            '<piston_top>' if
            srctools.conv_bool(inst.fixup['$start_up'])
            else '<piston_bottom>'
        )

    if folded_val == '<piston_top>':
        val = Vec(z=128 * srctools.conv_int(inst.fixup['$top_level', '1'], 1))
    elif folded_val == '<piston_bottom>':
        val = Vec(z=128 * srctools.conv_int(inst.fixup['$bottom_level', '0'], 0))
    else:
        val = Vec.from_str(res.value)

    offset = val.rotate_by_str(inst['angles'])
    inst['origin'] = (offset + Vec.from_str(inst['origin'])).join(' ')
Пример #23
0
def get_itemconf(
    name: Union[str, Tuple[str, str]],
    default: Optional[OptionT],
    timer_delay: int=None,
) -> Optional[OptionT]:
    """Get an itemconfig value.

    The name should be an 'ID:Section', or a tuple of the same.
    The type of the default sets what value it will be converted to.
    None returns the string, or None if not present.
    If set, timer_value is the value used for the timer.
    """
    if name == '':
        return default

    try:
        if isinstance(name, tuple):
            group_id, wid_id = name
        else:
            group_id, wid_id = name.split(':')
    except ValueError:
        LOGGER.warning('Invalid item config: {!r}!', name)
        return default

    wid_id = wid_id.casefold()

    if timer_delay is not None:
        if timer_delay < 3 or timer_delay > 30:
            wid_id += '_inf'
        else:
            wid_id += '_{}'.format(timer_delay)

    value = ITEM_CONFIG.get_val(group_id, wid_id, '')
    if not value:
        return default

    if isinstance(default, str) or default is None:
        return value
    elif isinstance(default, Vec):
        return Vec.from_str(value, default.x, default.y, default.z)
    elif isinstance(default, bool):
        return srctools.conv_bool(value, default)
    elif isinstance(default, float):
        return srctools.conv_int(value, default)
    elif isinstance(default, int):
        return srctools.conv_int(value, default)
    else:
        raise TypeError('Invalid default type "{}"!'.format(type(default).__name__))
Пример #24
0
def find_indicator_panels(inst: Entity):
    """We need to locate indicator panels, so they aren't overwritten.
    """
    if inst['file'].casefold() not in resolve_inst('[indpan]'):
        return
    loc = Vec(0, 0, -64).rotate_by_str(inst['angles'])
    loc += Vec.from_str(inst['origin'])

    # Sometimes (light bridges etc) a sign will be halfway between
    # tiles, so in that case we need to force 2 tiles.
    loc_min = (loc - (15, 15, 0)) // 32 * 32  # type: Vec
    loc_max = (loc + (15, 15, 0)) // 32 * 32  # type: Vec
    loc_min += (16, 16, 0)
    loc_max += (16, 16, 0)
    FORCE_LOCATIONS.add(loc_min.as_tuple())
    FORCE_LOCATIONS.add(loc_max.as_tuple())
Пример #25
0
def res_checkpoint_trigger(inst: Entity, res: Property):
    """Generate a trigger underneath coop checkpoint items

    """

    if vbsp.GAME_MODE == 'SP':
        # We can't have a respawn dropper in singleplayer.
        # Not generating the trigger means it's not going to
        # do anything.
        return

    pos = brushLoc.POS.raycast_world(
        Vec.from_str(inst['origin']),
        direction=(0, 0, -1),
    )
    bbox_min = pos - (192, 192, 64)
    bbox_max = pos + (192, 192, 64)

    # Find triggers already placed next to ours, and
    # merge with them if that's the case
    for offset in CHECKPOINT_NEIGHBOURS:
        near_pos = pos + offset
        try:
            trig = CHECKPOINT_TRIG[near_pos.as_tuple()]
            break
        except KeyError:
            pass
    else:
        # None found, make one.
        trig = inst.map.create_ent(
            classname='trigger_playerteam',
            origin=pos,
        )
        trig.solids = []
        CHECKPOINT_TRIG[pos.as_tuple()] = trig

    trig.solids.append(inst.map.make_prism(
        bbox_min,
        bbox_max,
        mat=const.Tools.TRIGGER,
    ).solid)

    for prop in res:
        out = Output.parse(prop)
        out.target = conditions.local_name(inst, out.target)
        trig.add_out(out)
Пример #26
0
def res_calc_opposite_wall_dist(inst: Entity, res: Property):
    """Calculate the distance between this item and the opposing wall.

    The value is stored in the `$var` specified by the property value.
    Alternately it is set by `ResultVar`, and `offset` adds or subtracts to the value.
    `GooCollide` means that it will stop when goo is found, otherwise it is
    ignored.
    `GooAdjust` means additionally if the space is goo, the distance will
    be modified so that it specifies the surface of the goo.
    """
    if res.has_children():
        result_var = res['ResultVar']
        dist_off = res.float('offset')
        collide_goo = res.bool('GooCollide')
        adjust_goo = res.bool('GooAdjust')
    else:
        result_var = res.value
        dist_off = 0
        collide_goo = adjust_goo = False

    origin = Vec.from_str(inst['origin'])
    normal = Vec(z=1).rotate_by_str(inst['angles'])

    mask = [
        brushLoc.Block.SOLID,
        brushLoc.Block.EMBED,
        brushLoc.Block.PIT_BOTTOM,
        brushLoc.Block.PIT_SINGLE,
    ]

    # Only if actually downward.
    if normal == (0, 0, -1) and collide_goo:
        mask.append(brushLoc.Block.GOO_TOP)
        mask.append(brushLoc.Block.GOO_SINGLE)

    opposing_pos = brushLoc.POS.raycast_world(
        origin,
        normal,
        mask,
    )

    if adjust_goo and brushLoc.POS['world': opposing_pos + 128*normal].is_goo:
        # If the top is goo, adjust so the 64 below is the top of the goo.
        dist_off += 32

    inst.fixup[result_var] = (origin - opposing_pos).mag() + dist_off
Пример #27
0
def make_corner(origin, angle, size, config):
    vbsp.VMF.create_ent(
        classname='func_instance',
        origin=origin,
        angles=angle,
        file=config['corner', size],
    )

    temp = config['corner_temp', size]
    if temp:
        temp_solids = template_brush.import_template(
            temp,
            origin=origin,
            angles=Vec.from_str(angle),
            force_type=template_brush.TEMP_TYPES.world,
        ).world
        for solid in temp_solids:
            vbsp.VMF.remove_brush(solid)
        motion_trigger(*temp_solids)
Пример #28
0
def res_hollow_brush(inst: Entity, res: Property):
    """Hollow out the attached brush, as if EmbeddedVoxel was set.

    This just removes the surface if it's already an embeddedVoxel. This allows
    multiple items to embed thinly in the same block without affecting each
    other.
    """
    loc = Vec(0, 0, -64).rotate_by_str(inst['angles'])
    loc += Vec.from_str(inst['origin'])

    try:
        group = SOLIDS[loc.as_tuple()]
    except KeyError:
        LOGGER.warning('No brush for hollowing at ({})', loc)
        return  # No brush here?

    conditions.hollow_block(
        group,
        remove_orig_face=srctools.conv_bool(res['RemoveFace', False])
    )
Пример #29
0
def flag_angles(inst: Entity, flag: Property):
    """Check that a instance is pointed in a direction.

    The value should be either just the angle to check, or a block of
    options:
    - `Angle`: A unit vector (XYZ value) pointing in a direction, or some
        keywords: `+z`, `-y`, `N`/`S`/`E`/`W`, `up`/`down`, `floor`/`ceiling`, or `walls` for any wall side.
    - `From_dir`: The direction the unrotated instance is pointed in.
        This lets the flag check multiple directions
    - `Allow_inverse`: If true, this also returns True if the instance is
        pointed the opposite direction .
    """
    angle = inst['angles', '0 0 0']

    if flag.has_children():
        targ_angle = flag['direction', '0 0 0']
        from_dir = flag['from_dir', '0 0 1']
        if from_dir.casefold() in DIRECTIONS:
            from_dir = Vec(DIRECTIONS[from_dir.casefold()])
        else:
            from_dir = Vec.from_str(from_dir, 0, 0, 1)
        allow_inverse = srctools.conv_bool(flag['allow_inverse', '0'])
    else:
        targ_angle = flag.value
        from_dir = Vec(0, 0, 1)
        allow_inverse = False

    normal = DIRECTIONS.get(targ_angle.casefold(), None)
    if normal is None:
        return False  # If it's not a special angle,
        # so it failed the exact match

    inst_normal = from_dir.rotate_by_str(angle)

    if normal == 'WALL':
        # Special case - it's not on the floor or ceiling
        return not (inst_normal == (0, 0, 1) or inst_normal == (0, 0, -1))
    else:
        return inst_normal == normal or (
            allow_inverse and -inst_normal == normal
        )
Пример #30
0
def make_vac_track(start, all_markers):
    """Create a vactube path section.

    """

    start_normal = Vec(-1, 0, 0).rotate_by_str(start['ent']['angles'])

    # First create the start section..
    start_logic = start['ent'].copy()
    vbsp.VMF.add_ent(start_logic)

    start_logic['file'] = start['conf']['entry', (
        'ceiling' if (start_normal.z > 0) else
        'floor' if (start_normal.z < 0) else
        'wall'
    )]

    end = start

    for inst, end in follow_vac_path(all_markers, start):
        join_markers(inst, end, inst is start)

    end_loc = Vec.from_str(end['ent']['origin'])
    end_norm = Vec(-1, 0, 0).rotate_by_str(end['ent']['angles'])

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

    # If the end is placed in goo, don't add logic - it isn't visible, and
    # the object is on a one-way trip anyway.
    if end_loc.as_tuple() not in GOO_LOCS:
        end_logic = end['ent'].copy()
        vbsp.VMF.add_ent(end_logic)
        end_logic['file'] = end['conf']['exit']
Пример #31
0
def res_reshape_fizzler(vmf: VMF, shape_inst: Entity, res: Property):
    """Convert a fizzler connected via the output to a new shape.

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

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

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

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

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

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

    for seg_prop in res.find_all('Segment'):
        vec1, vec2 = seg_prop.value.split(';')
        seg_min_max = Vec.bbox(
            Vec.from_str(vec1).rotate(*shape_angles) + origin,
            Vec.from_str(vec2).rotate(*shape_angles) + origin,
        )
        fizz.emitters.append(seg_min_max)
Пример #32
0
def improve_item(item: Property) -> None:
    """Improve editoritems formats in various ways.

    This operates inplace.
    """
    # OccupiedVoxels does not allow specifying 'volume' regions like
    # EmbeddedVoxel. Implement that.

    # First for 32^2 cube sections.
    for voxel_part in item.find_all("Exporting", "OccupiedVoxels",
                                    "SurfaceVolume"):
        if 'subpos1' not in voxel_part or 'subpos2' not in voxel_part:
            LOGGER.warning(
                'Item {} has invalid OccupiedVoxels part '
                '(needs SubPos1 and SubPos2)!',
                item['type'],
            )
            continue
        voxel_part.name = "Voxel"
        pos_1 = None
        voxel_subprops = list(voxel_part)
        voxel_part.clear()
        for prop in voxel_subprops:
            if prop.name not in ('subpos', 'subpos1', 'subpos2'):
                voxel_part.append(prop)
                continue
            pos_2 = Vec.from_str(prop.value)
            if pos_1 is None:
                pos_1 = pos_2
                continue

            bbox_min, bbox_max = Vec.bbox(pos_1, pos_2)
            pos_1 = None
            for pos in Vec.iter_grid(bbox_min, bbox_max):
                voxel_part.append(
                    Property("Surface", [
                        Property("Pos", str(pos)),
                    ]))
        if pos_1 is not None:
            LOGGER.warning(
                'Item {} has only half of SubPos bbox!',
                item['type'],
            )

    # Full blocks
    for occu_voxels in item.find_all("Exporting", "OccupiedVoxels"):
        for voxel_part in list(occu_voxels.find_all("Volume")):
            del occu_voxels['Volume']

            if 'pos1' not in voxel_part or 'pos2' not in voxel_part:
                LOGGER.warning(
                    'Item {} has invalid OccupiedVoxels part '
                    '(needs Pos1 and Pos2)!', item['type'])
                continue
            voxel_part.name = "Voxel"
            bbox_min, bbox_max = Vec.bbox(
                voxel_part.vec('pos1'),
                voxel_part.vec('pos2'),
            )
            del voxel_part['pos1']
            del voxel_part['pos2']
            for pos in Vec.iter_grid(bbox_min, bbox_max):
                new_part = voxel_part.copy()
                new_part['Pos'] = str(pos)
                occu_voxels.append(new_part)
Пример #33
0
def res_add_brush(inst: Entity, res: Property):
    """Spawn in a brush at the indicated points.

    - `point1` and `point2` are locations local to the instance, with `0 0 0`
      as the floor-position.
    - `type` is either `black` or `white`.
    - detail should be set to `1/0`. If true the brush will be a
      func_detail instead of a world brush.

    The sides will be textured with 1x1, 2x2 or 4x4 wall, ceiling and floor
    textures as needed.
    """
    import vbsp

    point1 = Vec.from_str(res['point1'])
    point2 = Vec.from_str(res['point2'])

    point1.z -= 64  # Offset to the location of the floor
    point2.z -= 64

    # Rotate to match the instance
    point1.rotate_by_str(inst['angles'])
    point2.rotate_by_str(inst['angles'])

    origin = Vec.from_str(inst['origin'])
    point1 += origin  # Then offset to the location of the instance
    point2 += origin

    tex_type = res['type', None]
    if tex_type not in ('white', 'black'):
        LOGGER.warning(
            'AddBrush: "{}" is not a valid brush '
            'color! (white or black)',
            tex_type,
        )
        tex_type = 'black'

    dim = point2 - point1
    dim.max(-dim)

    # Figure out what grid size and scale is needed
    # Check the dimensions in two axes to figure out the largest
    # tile size that can fit in it.
    x_maxsize = min(dim.y, dim.z)
    y_maxsize = min(dim.x, dim.z)
    if x_maxsize <= 32:
        x_grid = '4x4'
    elif x_maxsize <= 64:
        x_grid = '2x2'
    else:
        x_grid = 'wall'

    if y_maxsize <= 32:
        y_grid = '4x4'
    elif y_maxsize <= 64:
        y_grid = '2x2'
    else:
        y_grid = 'wall'

    grid_offset = origin // 128  # type: Vec

    # All brushes in each grid have the same textures for each side.
    random.seed(grid_offset.join(' ') + '-partial_block')

    solids = vbsp.VMF.make_prism(point1, point2)
    ':type solids: VLib.PrismFace'

    # Ensure the faces aren't re-textured later
    vbsp.IGNORED_FACES.update(solids.solid.sides)

    solids.north.mat = vbsp.get_tex(tex_type + '.' + y_grid)
    solids.south.mat = vbsp.get_tex(tex_type + '.' + y_grid)
    solids.east.mat = vbsp.get_tex(tex_type + '.' + x_grid)
    solids.west.mat = vbsp.get_tex(tex_type + '.' + x_grid)
    solids.top.mat = vbsp.get_tex(tex_type + '.floor')
    solids.bottom.mat = vbsp.get_tex(tex_type + '.ceiling')

    if srctools.conv_bool(res['detail', False], False):
        # Add the brush to a func_detail entity
        vbsp.VMF.create_ent(
            classname='func_detail'
        ).solids = [
            solids.solid
        ]
    else:
        # Add to the world
        vbsp.VMF.add_brush(solids.solid)
Пример #34
0
def mode_ang(comp_ent: Entity, ent: Entity) -> str:
    """Return the angle of the entity, as a Vector."""
    return vs_vec(Vec.from_str(ent['angles']))
Пример #35
0
def mode_dist(comp_ent: Entity, ent: Entity) -> str:
    """Return the distance from the ent to the reference."""
    scale = conv_float(comp_ent['const'], 1.0)
    offset = Vec.from_str(ent['origin']) - Vec.from_str(comp_ent['origin'])
    return offset.mag() * scale
Пример #36
0
def load_templates():
    """Load in the template file, used for import_template()."""
    with open(TEMPLATE_LOCATION) as file:
        props = Property.parse(file, TEMPLATE_LOCATION)
    vmf = srctools.VMF.parse(props, preserve_ids=True)

    def make_subdict():
        return defaultdict(list)
    # detail_ents[temp_id][visgroup]
    detail_ents = defaultdict(make_subdict)
    world_ents = defaultdict(make_subdict)
    overlay_ents = defaultdict(make_subdict)
    conf_ents = {}

    color_pickers = defaultdict(list)

    for ent in vmf.by_class['bee2_template_world']:
        world_ents[
            ent['template_id'].casefold()
        ][
            ent['visgroup'].casefold()
        ].extend(ent.solids)

    for ent in vmf.by_class['bee2_template_detail']:
        detail_ents[
            ent['template_id'].casefold()
        ][
            ent['visgroup'].casefold()
        ].extend(ent.solids)

    for ent in vmf.by_class['bee2_template_overlay']:
        overlay_ents[
            ent['template_id'].casefold()
        ][
            ent['visgroup'].casefold()
        ].append(ent)

    for ent in vmf.by_class['bee2_template_conf']:
        conf_ents[ent['template_id'].casefold()] = ent

    for ent in vmf.by_class['bee2_template_scaling']:
        temp = ScalingTemplate.parse(ent)
        TEMPLATES[temp.id.casefold()] = temp

    for ent in vmf.by_class['bee2_template_colorpicker']:
        # Parse the colorpicker data.
        temp_id = ent['template_id'].casefold()
        try:
            priority = Decimal(ent['priority'])
        except ValueError:
            LOGGER.warning(
                'Bad priority for colorpicker in "{}" template!',
                temp_id.upper(),
            )
            priority = Decimal(0)
        color_pickers[temp_id].append(ColorPicker(
            priority,
            offset=Vec.from_str(ent['origin']),
            normal=Vec(x=1).rotate_by_str(ent['angles']),
            sides=ent['faces'].split(' '),
            grid_snap=srctools.conv_bool(ent['grid_snap']),
            remove_brush=srctools.conv_bool(ent['remove_brush']),
        ))

    for temp_id in set(detail_ents).union(world_ents, overlay_ents):
        try:
            conf = conf_ents[temp_id]
        except KeyError:
            overlay_faces = []
            skip_faces = []
            vertical_faces = []
            realign_faces = []
        else:
            vertical_faces = conf['vertical_faces'].split()
            realign_faces = conf['realign_faces'].split()
            overlay_faces = conf['overlay_faces'].split()
            skip_faces = conf['skip_faces'].split()

        TEMPLATES[temp_id.casefold()] = Template(
            temp_id,
            world_ents[temp_id],
            detail_ents[temp_id],
            overlay_ents[temp_id],
            skip_faces,
            realign_faces,
            overlay_faces,
            vertical_faces,
            color_pickers[temp_id],
        )
Пример #37
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 `angles` must be set to determine the required position, or `facing` can be set for older files.
    If `global` is set, the spawn point will be absolute instead of relative to the current instance.
    """
    if vbsp.GAME_MODE != 'COOP':
        return conditions.RES_EXHAUSTED

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

    origin = res.vec('origin')
    if 'angles' in res:
        angles = Angle.from_str(res['angles'])
    else:
        # Older system, specify the forward direction.
        angles = res.vec('facing', z=1).to_angle()

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

    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',
            # Spelling mistake is correct.
            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 conditions.RES_EXHAUSTED
Пример #38
0
def get_studio_loc() -> Vec:
    """Return the location of the voice studio."""
    return Vec.from_str(QUOTE_DATA['quote_loc', '-10000 0 0'], x=-10000)
Пример #39
0
    def read_from_map(self, vmf: VMF, has_attr: Dict[str, bool]) -> None:
        """Given the map file, set blocks."""
        from conditions import EMBED_OFFSETS
        from instance_traits import get_item_id

        # Starting points to fill air and goo.
        # We want to fill goo first...
        air_search_locs: List[Tuple[Vec, bool]] = []
        goo_search_locs: List[Tuple[Vec, bool]] = []

        for ent in vmf.entities:
            str_pos = ent['origin', None]
            if str_pos is None:
                continue

            pos = world_to_grid(Vec.from_str(str_pos))

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

            # We need to manually set EmbeddedVoxel locations.
            # These might not be detected for items where there's a block
            # which is entirely empty - corridors and obs rooms for example.
            item_id = get_item_id(ent)
            if item_id:
                try:
                    embed_locs = EMBED_OFFSETS[item_id]
                except KeyError:
                    continue
                angles = Vec.from_str(ent['angles'])
                for local_pos in embed_locs:
                    world_pos = local_pos - (0, 0, 1)
                    world_pos.localise(pos, angles)
                    self[world_pos] = Block.EMBED

        can_have_pit = bottomlessPit.pits_allowed()

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

            bbox_min, bbox_max = brush.get_bbox()

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

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

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

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

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

                # Remove the brush, since we're not going to use it.
                vmf.remove_brush(brush)

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

                continue

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

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

        LOGGER.info(
            'Analysed map, filling air... ({} starting positions..)',
            len(air_search_locs)
        )
        self.fill_air(goo_search_locs + air_search_locs)
        LOGGER.info('Air filled!')
Пример #40
0
def res_cutout_tile(res: Property):
    """Generate random quarter tiles, like in Destroyed or Retro maps.

    - "MarkerItem" is the instance to look for.
    - "TileSize" can be "2x2" or "4x4".
    - rotateMax is the amount of degrees to rotate squarebeam models.

    Materials:
    - "squarebeams" is the squarebeams variant to use.
    - "ceilingwalls" are the sides of the ceiling section.
    - "floorbase" is the texture under floor sections.
    - "tile_glue" is used on top of a thinner tile segment.
    - "clip" is the player_clip texture used over floor segments.
        (This allows customising the surfaceprop.)
    - "Floor4x4Black", "Ceil2x2White" and other combinations can be used to
       override the textures used.
    """
    item = instanceLocs.resolve(res['markeritem'])

    INST_LOCS = {}  # Map targetnames -> surface loc
    CEIL_IO = []  # Pairs of ceil inst corners to cut out.
    FLOOR_IO = []  # Pairs of floor inst corners to cut out.

    overlay_ids = {}  # When we replace brushes, we need to fix any overlays
    # on that surface.

    MATS.clear()
    floor_edges = []  # Values to pass to add_floor_sides() at the end

    sign_loc = set(FORCE_LOCATIONS)
    # If any signage is present in the map, we need to force tiles to
    # appear at that location!
    for over in conditions.VMF.by_class['info_overlay']:
        if (over['material'].casefold() in FORCE_TILE_MATS and
                # Only check floor/ceiling overlays
                over['basisnormal'] in ('0 0 1', '0 0 -1')):
            loc = Vec.from_str(over['origin'])
            # Sometimes (light bridges etc) a sign will be halfway between
            # tiles, so in that case we need to force 2 tiles.
            loc_min = (loc - (15, 15, 0)) // 32 * 32  # type: Vec
            loc_max = (loc + (15, 15, 0)) // 32 * 32  # type: Vec
            loc_min += (16, 16, 0)
            loc_max += (16, 16, 0)
            FORCE_LOCATIONS.add(loc_min.as_tuple())
            FORCE_LOCATIONS.add(loc_max.as_tuple())

    SETTINGS = {
        'floor_chance':
        srctools.conv_int(res['floorChance', '100'], 100),
        'ceil_chance':
        srctools.conv_int(res['ceilingChance', '100'], 100),
        'floor_glue_chance':
        srctools.conv_int(res['floorGlueChance', '0']),
        'ceil_glue_chance':
        srctools.conv_int(res['ceilingGlueChance', '0']),
        'rotate_beams':
        int(srctools.conv_float(res['rotateMax', '0']) * BEAM_ROT_PRECISION),
        'beam_skin':
        res['squarebeamsSkin', '0'],
        'base_is_disp':
        srctools.conv_bool(res['dispBase', '0']),
        'quad_floor':
        res['FloorSize', '4x4'].casefold() == '2x2',
        'quad_ceil':
        res['CeilingSize', '4x4'].casefold() == '2x2',
    }

    random.seed(vbsp.MAP_RAND_SEED + '_CUTOUT_TILE_NOISE')
    noise = SimplexNoise(period=4 * 40)  # 4 tiles/block, 50 blocks max

    # We want to know the number of neighbouring tile cutouts before
    # placing tiles - blocks away from the sides generate fewer tiles.
    floor_neighbours = defaultdict(dict)  # all_floors[z][x,y] = count

    for mat_prop in res['Materials', []]:
        MATS[mat_prop.name].append(mat_prop.value)

    if SETTINGS['base_is_disp']:
        # We want the normal brushes to become nodraw.
        MATS['floorbase_disp'] = MATS['floorbase']
        MATS['floorbase'] = ['tools/toolsnodraw']

        # Since this uses random data for initialisation, the alpha and
        # regular will use slightly different patterns.
        alpha_noise = SimplexNoise(period=4 * 50)
    else:
        alpha_noise = None

    for key, default in TEX_DEFAULT:
        if key not in MATS:
            MATS[key] = [default]

    # Find our marker ents
    for inst in conditions.VMF.by_class['func_instance']:  # type: VLib.Entity
        if inst['file'].casefold() not in item:
            continue
        targ = inst['targetname']
        orient = Vec(0, 0, 1).rotate_by_str(inst['angles', '0 0 0'])
        # Check the orientation of the marker to figure out what to generate
        if orient == (0, 0, 1):
            io_list = FLOOR_IO
        else:
            io_list = CEIL_IO

        # Reuse orient to calculate where the solid face will be.
        loc = (orient * -64) + Vec.from_str(inst['origin'])
        INST_LOCS[targ] = loc

        for out in inst.output_targets():
            io_list.append((targ, out))

        if not inst.outputs and inst.fixup['$connectioncount'] == '0':
            # If the item doesn't have any connections, 'connect'
            # it to itself so we'll generate a 128x128 tile segment.
            io_list.append((targ, targ))
        inst.remove()  # Remove the instance itself from the map.

    for start_floor, end_floor in FLOOR_IO:
        if end_floor not in INST_LOCS:
            # Not a marker - remove this and the antline.
            for toggle in conditions.VMF.by_target[end_floor]:
                conditions.remove_ant_toggle(toggle)
            continue

        box_min = Vec(INST_LOCS[start_floor])
        box_min.min(INST_LOCS[end_floor])

        box_max = Vec(INST_LOCS[start_floor])
        box_max.max(INST_LOCS[end_floor])

        if box_min.z != box_max.z:
            continue  # They're not in the same level!
        z = box_min.z

        if SETTINGS['rotate_beams']:
            # We have to generate 1 model per 64x64 block to do rotation...
            gen_rotated_squarebeams(
                box_min - (64, 64, 0),
                box_max + (64, 64, -8),
                skin=SETTINGS['beam_skin'],
                max_rot=SETTINGS['rotate_beams'],
            )
        else:
            # Make the squarebeams props, using big models if possible
            gen_squarebeams(box_min + (-64, -64, 0),
                            box_max + (64, 64, -8),
                            skin=SETTINGS['beam_skin'])

        # Add a player_clip brush across the whole area
        conditions.VMF.add_brush(
            conditions.VMF.make_prism(
                p1=box_min - (64, 64, FLOOR_DEPTH),
                p2=box_max + (64, 64, 0),
                mat=MATS['clip'][0],
            ).solid)

        # Add a noportal_volume covering the surface, in case there's
        # room for a portal.
        noportal_solid = conditions.VMF.make_prism(
            # Don't go all the way to the sides, so it doesn't affect wall
            # brushes.
            p1=box_min - (63, 63, 9),
            p2=box_max + (63, 63, 0),
            mat='tools/toolsinvisible',
        ).solid
        noportal_ent = conditions.VMF.create_ent(
            classname='func_noportal_volume',
            origin=box_min.join(' '),
        )
        noportal_ent.solids.append(noportal_solid)

        if SETTINGS['base_is_disp']:
            # Use displacements for the base instead.
            make_alpha_base(
                box_min + (-64, -64, 0),
                box_max + (64, 64, 0),
                noise=alpha_noise,
            )

        for x, y in utils.iter_grid(
                min_x=int(box_min.x),
                max_x=int(box_max.x) + 1,
                min_y=int(box_min.y),
                max_y=int(box_max.y) + 1,
                stride=128,
        ):
            # Build the set of all positions..
            floor_neighbours[z][x, y] = -1

        # Mark borders we need to fill in, and the angle (for func_instance)
        # The wall is the face pointing inwards towards the bottom brush,
        # and the ceil is the ceiling of the block above the bordering grid
        # points.
        for x in range(int(box_min.x), int(box_max.x) + 1, 128):
            # North
            floor_edges.append(
                BorderPoints(
                    wall=Vec(x, box_max.y + 64, z - 64),
                    ceil=Vec_tuple(x, box_max.y + 128, z),
                    rot=270,
                ))
            # South
            floor_edges.append(
                BorderPoints(
                    wall=Vec(x, box_min.y - 64, z - 64),
                    ceil=Vec_tuple(x, box_min.y - 128, z),
                    rot=90,
                ))

        for y in range(int(box_min.y), int(box_max.y) + 1, 128):
            # East
            floor_edges.append(
                BorderPoints(
                    wall=Vec(box_max.x + 64, y, z - 64),
                    ceil=Vec_tuple(box_max.x + 128, y, z),
                    rot=180,
                ))

            # West
            floor_edges.append(
                BorderPoints(
                    wall=Vec(box_min.x - 64, y, z - 64),
                    ceil=Vec_tuple(box_min.x - 128, y, z),
                    rot=0,
                ))

    # Now count boundries near tiles, then generate them.

    # Do it seperately for each z-level:
    for z, xy_dict in floor_neighbours.items():  # type: float, dict
        for x, y in xy_dict:  # type: float, float
            # We want to count where there aren't any tiles
            xy_dict[x, y] = (((x - 128, y - 128) not in xy_dict) +
                             ((x - 128, y + 128) not in xy_dict) +
                             ((x + 128, y - 128) not in xy_dict) +
                             ((x + 128, y + 128) not in xy_dict) +
                             ((x - 128, y) not in xy_dict) +
                             ((x + 128, y) not in xy_dict) +
                             ((x, y - 128) not in xy_dict) +
                             ((x, y + 128) not in xy_dict))

        max_x = max_y = 0

        weights = {}
        # Now the counts are all correct, compute the weight to apply
        # for tiles.
        # Adding the neighbouring counts will make a 5x5 area needed to set
        # the center to 0.

        for (x, y), cur_count in xy_dict.items():
            max_x = max(x, max_x)
            max_y = max(y, max_y)

            # Orthrogonal is worth 0.2, diagonal is worth 0.1.
            # Not-present tiles would be 8 - the maximum
            tile_count = (0.8 * cur_count + 0.1 * xy_dict.get(
                (x - 128, y - 128), 8) + 0.1 * xy_dict.get(
                    (x - 128, y + 128), 8) + 0.1 * xy_dict.get(
                        (x + 128, y - 128), 8) + 0.1 * xy_dict.get(
                            (x + 128, y + 128), 8) + 0.2 * xy_dict.get(
                                (x - 128, y), 8) + 0.2 * xy_dict.get(
                                    (x, y - 128), 8) + 0.2 * xy_dict.get(
                                        (x, y + 128), 8) + 0.2 * xy_dict.get(
                                            (x + 128, y), 8))
            # The number ranges from 0 (all tiles) to 12.8 (no tiles).
            # All tiles should still have a small chance to generate tiles.
            weights[x, y] = min((tile_count + 0.5) / 8, 1)

        # Share the detail entity among same-height tiles..
        detail_ent = conditions.VMF.create_ent(classname='func_detail', )

        for x, y in xy_dict:
            convert_floor(
                Vec(x, y, z),
                overlay_ids,
                MATS,
                SETTINGS,
                sign_loc,
                detail_ent,
                noise_weight=weights[x, y],
                noise_func=noise,
            )

    add_floor_sides(floor_edges)

    conditions.reallocate_overlays(overlay_ids)

    return conditions.RES_EXHAUSTED
Пример #41
0
def brush_at_loc(
    inst: Entity,
    props: Property,
) -> Tuple[tiling.TileType, bool, Set[tiling.TileType]]:
    """Common code for posIsSolid and ReadSurfType.

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

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

    pos.localise(origin, angles)

    norm: Vec = round(props.vec('dir', 0, 0, 1) @ angles, 6)

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

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

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

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

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

        white_count = black_count = 0

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

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

        both_colors = white_count > 0 and black_count > 0

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

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

    return tile_type, both_colors, tile_types
Пример #42
0
def res_piston_plat(vmf: VMF, inst: Entity, res: Property) -> None:
    """Generates piston platforms with optimized logic."""
    template: template_brush.Template
    visgroup_names: List[str]
    inst_filenames: Dict[str, str]
    has_dn_fizz: bool
    automatic_var: str
    color_var: str
    source_ent: str
    snd_start: str
    snd_loop: str
    snd_stop: str
    (
        template,
        visgroup_names,
        inst_filenames,
        has_dn_fizz,
        automatic_var,
        color_var,
        source_ent,
        snd_start,
        snd_loop,
        snd_stop,
    ) = res.value

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    if not static_ent.solids:
        static_ent.remove()

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

        if source_ent:
            # Parent is irrelevant for actual entity locations, but it
            # survives for the script to read.
            script_ent['SourceEntityName'] = script_ent['parentname'] = local_name(inst, source_ent)
Пример #43
0
def res_faith_mods(inst: Entity, res: Property):
    """Modify the `trigger_catapult` that is created for `ItemFaithPlate` items.

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

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

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

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

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

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

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

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

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

                if fixup_var:
                    inst.fixup[fixup_var] = 'straight'
                break
Пример #44
0
def res_reshape_fizzler(vmf: VMF, shape_inst: Entity, res: Property):
    """Convert a fizzler connected via the output to a new shape.

    This allows for different placing of fizzler items.

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

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

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

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

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

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

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

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

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

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

    for seg_prop in res.find_all('Segment'):
        vec1, vec2 = seg_prop.value.split(';')
        seg_min_max = Vec.bbox(
            Vec.from_str(vec1) @ shape_orient + origin,
            Vec.from_str(vec2) @ shape_orient + origin,
        )
        fizz.emitters.append(seg_min_max)
Пример #45
0
    def modify_platform(inst: Entity) -> None:
        """Modify each platform."""
        min_pos = inst.fixup.int(FixupVars.PIST_BTM)
        max_pos = inst.fixup.int(FixupVars.PIST_TOP)
        start_up = inst.fixup.bool(FixupVars.PIST_IS_UP)

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

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

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

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

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

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

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

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

        origin = Vec.from_str(inst['origin'])
        orient = Matrix.from_angle(Angle.from_str(inst['angles']))
        off = orient.up(128)
        move_ang = off.to_angle()

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

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

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

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

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

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

            if fname:
                conditions.ALL_INST.add(fname.casefold())
            else:
                # No actual instance, remove.
                pist_ent.remove()

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

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

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

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

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

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

            if source_ent:
                # Parent is irrelevant for actual entity locations, but it
                # survives for the script to read.
                script_ent['SourceEntityName'] = script_ent[
                    'parentname'] = conditions.local_name(inst, source_ent)
Пример #46
0
    def parse(cls, conf: Property):
        """Read in a fizzler from a config."""
        fizz_id = conf['id']
        item_ids = [prop.value.casefold() for prop in conf.find_all('item_id')]

        try:
            model_name_type = ModelName(conf['NameType', 'same'].casefold())
        except ValueError:
            LOGGER.warning('Bad model name type: "{}"', conf['NameType'])
            model_name_type = ModelName.SAME

        model_local_name = conf['ModelName', '']
        if not model_local_name:
            # We can't rename without a local name.
            model_name_type = ModelName.SAME

        inst = {}
        for inst_type, is_static in itertools.product(FizzInst, (False, True)):
            inst_type_name = inst_type.value + ('_static' if is_static else '')
            inst[inst_type, is_static] = instances = [
                file for prop in conf.find_all(inst_type_name)
                for file in instanceLocs.resolve(prop.value)
            ]
            # Allow specifying weights to bias model locations
            weights = conf[inst_type_name + '_weight', '']
            if weights:
                # Produce the weights, then process through the original
                # list to build a new one with repeated elements.
                inst[inst_type, is_static] = instances = [
                    instances[i] for i in conditions.weighted_random(
                        len(instances), weights)
                ]
            # If static versions aren't given, reuse non-static ones.
            # We do False, True so it's already been calculated.
            if not instances and is_static:
                inst[inst_type, True] = inst[inst_type, False]

        if not inst[FizzInst.BASE, False]:
            LOGGER.warning('No base instance set! for "{}"!', fizz_id)

        voice_attrs = []
        for prop in conf.find_all('Has'):
            if prop.has_children():
                for child in prop:
                    voice_attrs.append(child.name.casefold())
            else:
                voice_attrs.append(prop.value.casefold())

        pack_lists = {prop.value for prop in conf.find_all('Pack')}
        pack_lists_static = {
            prop.value
            for prop in conf.find_all('PackStatic')
        }

        brushes = [FizzlerBrush.parse(prop) for prop in conf.find_all('Brush')]

        beams = []  # type: List[FizzBeam]
        for beam_prop in conf.find_all('Beam'):
            offsets = [
                Vec.from_str(off.value) for off in beam_prop.find_all('pos')
            ]
            keys = Property('', [
                beam_prop.find_key('Keys', []),
                beam_prop.find_key('LocalKeys', [])
            ])
            beams.append(
                FizzBeam(
                    offsets,
                    keys,
                    beam_prop.int('RandSpeedMin', 0),
                    beam_prop.int('RandSpeedMax', 0),
                ))

        try:
            temp_conf = conf.find_key('TemplateBrush')
        except NoKeyError:
            temp_brush_keys = temp_min = temp_max = temp_single = None
        else:
            temp_brush_keys = Property('--', [
                temp_conf.find_key('Keys'),
                temp_conf.find_key('LocalKeys', []),
            ])

            # Find and load the templates.
            temp_min = temp_conf['Left', None]
            temp_max = temp_conf['Right', None]
            temp_single = temp_conf['Single', None]

        return FizzlerType(
            fizz_id,
            item_ids,
            voice_attrs,
            pack_lists,
            pack_lists_static,
            model_local_name,
            model_name_type,
            brushes,
            beams,
            inst,
            temp_brush_keys,
            temp_min,
            temp_max,
            temp_single,
        )
Пример #47
0
    def make_tag_fizz(inst: Entity) -> None:
        """Create the Tag fizzler."""
        fizzler: Optional[Fizzler] = None
        fizzler_item: Optional[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

        inst_orient = Matrix.from_angle(Angle.from_str(inst['angles']))

        # The actual location of the sign - on the wall
        sign_loc = Vec.from_str(inst['origin']) + Vec(0, 0, -64) @ inst_orient
        fizz_norm_axis = round(fizzler.normal(), 3).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_normal = inst_orient.up()
        loc = Vec.from_str(inst['origin'])

        if disable_other or (blue_enabled and oran_enabled):
            inst['file'] = inst_frame_double
            conditions.ALL_INST.add(inst_frame_double.casefold())
            # On a wall, and pointing vertically
            if abs(inst_normal.z) < 0.01 and abs(inst_orient.left().z) > 0.01:
                # 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) @ inst_orient
                blue_loc = loc + offset
                oran_loc = loc - offset
        else:
            inst['file'] = inst_frame_single
            conditions.ALL_INST.add(inst_frame_single.casefold())
            # 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 = conditions.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:
            conditions.add_inst(
                vmf,
                file=blue_sign,
                targetname=inst['targetname'],
                angles=sign_angle,
                origin=blue_loc,
            )

        if oran_sign:
            conditions.add_inst(
                vmf,
                file=oran_sign,
                targetname=inst['targetname'],
                angles=sign_angle,
                origin=oran_loc,
            )

        # Now modify the fizzler...

        # Subtract the sign from the list of connections, but don't go below
        # zero
        fizzler.base_inst.fixup['$connectioncount'] = max(
            0,
            fizzler.base_inst.fixup.int('$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 = pos_oran = False
        neg_blue = 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')
            output = 'OnStartTouch'

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

        pos_trig['targetname'] = conditions.local_name(fizzler.base_inst,
                                                       'trig_pos')
        neg_trig['targetname'] = conditions.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',
                    neg_blue,
                ))
            pos_trig.outputs.append(
                Output(
                    output,
                    '@BlueIsEnabled',
                    'SetValue',
                    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'] = conditions.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, )
Пример #48
0
def parse_map(vmf: VMF, voice_attrs: Dict[str, bool]) -> None:
    """Analyse fizzler instances to assign fizzler types.

    Instance traits are required.
    The model instances and brushes will be removed from the map.
    Needs connections to be parsed.
    """

    # Item ID and model skin -> fizzler type
    fizz_types = {}  # type: Dict[Tuple[str, int], FizzlerType]

    for fizz_type in FIZZ_TYPES.values():
        for item_id in fizz_type.item_ids:
            if ':' in item_id:
                item_id, barrier_type = item_id.split(':')
                if barrier_type == 'laserfield':
                    barrier_skin = 2
                elif barrier_type == 'fizzler':
                    barrier_skin = 0
                else:
                    LOGGER.error('Invalid barrier type ({}) for "{}"!',
                                 barrier_type, item_id)
                    fizz_types[item_id, 0] = fizz_type
                    fizz_types[item_id, 2] = fizz_type
                    continue
                fizz_types[item_id, barrier_skin] = fizz_type
            else:
                fizz_types[item_id, 0] = fizz_type
                fizz_types[item_id, 2] = fizz_type

    fizz_bases = {}  # type: Dict[str, Entity]
    fizz_models = defaultdict(list)  # type: Dict[str, List[Entity]]

    # Position and normal -> name, for output relays.
    fizz_pos = {
    }  # type: Dict[Tuple[Tuple[float, float, float], Tuple[float, float, float]], str]

    # First use traits to gather up all the instances.
    for inst in vmf.by_class['func_instance']:
        traits = instance_traits.get(inst)
        if 'fizzler' not in traits:
            continue

        name = inst['targetname']

        if 'fizzler_model' in traits:
            name = name.rsplit('_model', 1)[0]
            fizz_models[name].append(inst)
            inst.remove()
        elif 'fizzler_base' in traits:
            fizz_bases[name] = inst
        else:
            LOGGER.warning('Fizzler "{}" has non-base, non-model instance?',
                           name)
            continue

        origin = Vec.from_str(inst['origin'])
        normal = Vec(z=1).rotate_by_str(inst['angles'])
        fizz_pos[origin.as_tuple(), normal.as_tuple()] = name

    for name, base_inst in fizz_bases.items():
        models = fizz_models[name]
        up_axis = Vec(y=1).rotate_by_str(base_inst['angles'])

        # If upside-down, make it face upright.
        if up_axis == (0, 0, -1):
            up_axis = Vec(z=1)

        base_inst.outputs.clear()

        # Now match the pairs of models to each other.
        # The length axis is the line between them.
        # We don't care about the instances after this, so don't keep track.
        length_axis = Vec(z=1).rotate_by_str(base_inst['angles']).axis()

        emitters = []  # type: List[Tuple[Vec, Vec]]

        model_pairs = {}  # type: Dict[Tuple[float, float], Vec]

        model_skin = models[0].fixup.int('$skin')

        try:
            item_id, item_subtype = instanceLocs.ITEM_FOR_FILE[
                base_inst['file'].casefold()]
            fizz_type = fizz_types[item_id, model_skin]
        except KeyError:
            LOGGER.warning('Fizzler types: {}', fizz_types.keys())
            raise ValueError('No fizzler type for "{}"!'.format(
                base_inst['file'], )) from None

        for attr_name in fizz_type.voice_attrs:
            voice_attrs[attr_name] = True

        for model in models:
            pos = Vec.from_str(model['origin'])
            try:
                other_pos = model_pairs.pop(pos.other_axes(length_axis))
            except KeyError:
                # No other position yet, we need to find that.
                model_pairs[pos.other_axes(length_axis)] = pos
                continue

            min_pos, max_pos = Vec.bbox(pos, other_pos)

            # Move positions to the wall surface.
            min_pos[length_axis] -= 64
            max_pos[length_axis] += 64
            emitters.append((min_pos, max_pos))

        FIZZLERS[name] = Fizzler(fizz_type, up_axis, base_inst, emitters)

    # Delete all the old brushes associated with fizzlers
    for brush in (vmf.by_class['trigger_portal_cleanser']
                  | vmf.by_class['trigger_hurt'] | vmf.by_class['func_brush']):
        name = brush['targetname']
        if not name:
            continue
        name = name.rsplit('_brush')[0]
        if name in FIZZLERS:
            brush.remove()

    # Check for fizzler output relays.
    relay_file = instanceLocs.resolve('<ITEM_BEE2_FIZZLER_OUT_RELAY>',
                                      silent=True)
    if not relay_file:
        # No relay item - deactivated most likely.
        return

    for inst in vmf.by_class['func_instance']:
        if inst['file'].casefold() not in relay_file:
            continue

        inst.remove()

        relay_item = connections.ITEMS[inst['targetname']]

        try:
            fizz_name = fizz_pos[
                Vec.from_str(inst['origin']).as_tuple(),
                Vec(0, 0, 1).rotate_by_str(inst['angles']).as_tuple()]
            fizz_item = connections.ITEMS[fizz_name]
        except KeyError:
            # Not placed on a fizzler, or a fizzler with no IO
            # - ignore, and destroy.
            for out in list(relay_item.outputs):
                out.remove()
            for out in list(relay_item.inputs):
                out.remove()
            del connections.ITEMS[relay_item.name]
            continue

        # Copy over fixup values
        fizz_item.inst.fixup.update(inst.fixup)

        # Copy over the timer delay set in the relay.
        fizz_item.timer = relay_item.timer
        # Transfer over antlines.
        fizz_item.antlines |= relay_item.antlines
        fizz_item.shape_signs += relay_item.shape_signs
        fizz_item.ind_panels |= relay_item.ind_panels

        # Remove the relay item so it doesn't get added to the map.
        del connections.ITEMS[relay_item.name]

        for conn in list(relay_item.outputs):
            conn.from_item = fizz_item
Пример #49
0
def mode_pos(comp_ent: Entity, ent: Entity) -> str:
    """Return the position of the entity."""
    pos = Vec.from_str(ent['origin'])
    scale = conv_float(comp_ent['const'], 1.0)
    return vs_vec(scale * pos)
Пример #50
0
def generate_fizzlers(vmf: VMF):
    """Generates fizzler models and the brushes according to their set types.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        for brush_type, used_tex in mat_mod_tex.items():
            brush_name = conditions.local_name(fizz.base_inst, brush_type.name)
            mat_mod_name = conditions.local_name(fizz.base_inst,
                                                 brush_type.mat_mod_name)
            for off, tex in zip(MATMOD_OFFSETS, sorted(used_tex)):
                pos = off.copy().rotate(*min_angles)
                pos += Vec.from_str(fizz.base_inst['origin'])
                vmf.create_ent(
                    classname='material_modify_control',
                    origin=pos,
                    targetname=mat_mod_name,
                    materialName='materials/' + tex + '.vmt',
                    materialVar=brush_type.mat_mod_var,
                    parentname=brush_name,
                )
Пример #51
0
def mode_off(comp_ent: Entity, ent: Entity) -> str:
    """Return the offset from the ent to the reference."""
    scale = conv_float(comp_ent['const'], 1.0)
    offset = Vec.from_str(ent['origin']) - Vec.from_str(comp_ent['origin'])
    return vs_vec(offset * scale)
Пример #52
0
def res_unst_scaffold(res: Property):
    """The condition to generate Unstationary Scaffolds.

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

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

    instances = {}
    # Find all the instances we're wanting to change, and map them to
    # targetnames
    for ent in vbsp.VMF.by_class['func_instance']:
        file = ent['file'].casefold()
        targ = ent['targetname']
        if file not in TARG_INST:
            continue
        config = TARG_INST[file]
        next_inst = set(
            out.target
            for out in
            ent.outputs
        )
        # Destroy these outputs, they're useless now!
        ent.outputs.clear()
        instances[targ] = {
            'ent': ent,
            'conf': config,
            'next': next_inst,
            'prev': None,
        }

    # Now link each instance to its in and outputs
    for targ, inst in instances.items():
        scaff_targs = 0
        for ent_targ in inst['next']:
            if ent_targ in instances:
                instances[ent_targ]['prev'] = targ
                inst['next'] = ent_targ
                scaff_targs += 1
            else:
                # If it's not a scaffold, it's probably an indicator_toggle.
                # We want to remove any them as well as the assoicated
                # antlines!
                for toggle in vbsp.VMF.by_target[ent_targ]:
                    conditions.remove_ant_toggle(toggle)
        if scaff_targs > 1:
            raise Exception('A scaffold item has multiple destinations!')
        elif scaff_targs == 0:
            inst['next'] = None  # End instance

    starting_inst = []
    # We need to find the start instances, so we can set everything up
    for inst in instances.values():
        if inst['prev'] is None and inst['next'] is None:
            # Static item!
            continue
        elif inst['prev'] is None:
            starting_inst.append(inst)

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

    # Set all the instances and properties
    for start_inst in starting_inst:
        group_counter += 1
        ent = start_inst['ent']
        for vals in LINKS.values():
            if vals['all'] is not None:
                ent.fixup[vals['all']] = SCAFF_PATTERN.format(
                    name=vals['name'],
                    group=group_counter,
                    index='*',
                )

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

        # Now set each instance in the chain, including first and last
        for index, inst in enumerate(scaff_scan(instances, start_inst)):
            ent, conf = inst['ent'], inst['conf']
            orient = (
                'floor' if
                Vec(0, 0, 1).rotate_by_str(ent['angles']) == (0, 0, 1)
                else 'wall'
            )

            # Find the offset used for the logic ents
            offset = (conf['off_' + orient]).copy()
            if conf['is_piston']:
                # Adjust based on the piston position
                offset.z += 128 * srctools.conv_int(ent.fixup[
                    '$top_level' if
                    ent.fixup['$start_up'] == '1'
                    else '$bottom_level'
                ])
            offset.rotate_by_str(ent['angles'])
            offset += Vec.from_str(ent['origin'])

            if inst['prev'] is None:
                link_type = 'start'
            elif inst['next'] is None:
                link_type = 'end'
            else:
                link_type = 'mid'

            if (
                    orient == 'floor' and
                    link_type != 'mid' and
                    conf['inst_end'] is not None
                    ):
                # Add an extra instance pointing in the direction
                # of the connected track. This would be the endcap
                # model.
                other_ent = instances[inst[
                    'next' if link_type == 'start' else 'prev'
                ]]['ent']

                other_pos = Vec.from_str(other_ent['origin'])
                our_pos = Vec.from_str(ent['origin'])
                link_dir = other_pos - our_pos
                link_ang = math.degrees(
                    math.atan2(link_dir.y, link_dir.x)
                )
                # Round to nearest 90 degrees
                # Add 45 so the switchover point is at the diagonals
                link_ang = (link_ang + 45) // 90 * 90
                vbsp.VMF.create_ent(
                    classname='func_instance',
                    targetname=ent['targetname'],
                    file=conf['inst_end'],
                    origin=offset.join(' '),
                    angles='0 {:.0f} 0'.format(link_ang),
                )
                # Don't place the offset instance, this replaces that!
            elif conf['inst_offset'] is not None:
                # Add an additional rotated entity at the offset.
                # This is useful for the piston item.
                vbsp.VMF.create_ent(
                    classname='func_instance',
                    targetname=ent['targetname'],
                    file=conf['inst_offset'],
                    origin=offset.join(' '),
                    angles=ent['angles'],
                )

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

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

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

    LOGGER.info('Finished Scaffold generation!')
    return RES_EXHAUSTED
Пример #53
0
 def mode_func(comp_ent: Entity, ent: Entity) -> str:
     """Rotate the axis by the given value."""
     pos = Vec.from_str(ent['origin'])
     scale = conv_float(comp_ent['const'], 1.0)
     return str(pos[axis] * scale)
Пример #54
0
def res_water_splash(vmf: VMF, inst: Entity, res: Property) -> None:
    """Creates splashes when something goes in and out of water.

    Arguments:

    - `parent`: The name of the parent entity.
    - `name`: The name given to the env_splash.
    - `scale`: The size of the effect (8 by default).
    - `position`: The offset position to place the entity.
    - `position2`: The offset to which the entity will move.
    - `type`: Use certain fixup values to calculate pos2 instead:
       `piston_1`/`2`/`3`/`4`: Use `$bottom_level` and `$top_level` as offsets.
       `track_platform`: Use `$travel_direction`, `$travel_distance`, etc.
    - `fast_check`: Check faster for movement. Needed for items which
      move quickly.
    """
    (
        name,
        parent,
        scale,
        pos1,
        pos2,
        calc_type,
        fast_check,
    ) = res.value  # type: str, str, float, Vec, Vec, str, str

    pos1 = pos1.copy()
    splash_pos = pos1.copy()

    if calc_type == 'track_platform':
        lin_off = srctools.conv_int(inst.fixup['$travel_distance'])
        travel_ang = inst.fixup['$travel_direction']
        start_pos = srctools.conv_float(inst.fixup['$starting_position'])
        if start_pos:
            start_pos = round(start_pos * lin_off)
            pos1 += Vec(x=-start_pos).rotate_by_str(travel_ang)

        pos2 = Vec(x=lin_off).rotate_by_str(travel_ang)
        pos2 += pos1
    elif calc_type.startswith('piston'):
        # Use piston-platform offsetting.
        # The number is the highest offset to move to.
        max_pist = srctools.conv_int(calc_type.split('_', 2)[1], 4)
        bottom_pos = srctools.conv_int(inst.fixup['$bottom_level'])
        top_pos = min(srctools.conv_int(inst.fixup['$top_level']), max_pist)

        pos2 = pos1.copy()
        pos1 += Vec(z=128 * bottom_pos)
        pos2 += Vec(z=128 * top_pos)
        LOGGER.info('Bottom: {}, top: {}', bottom_pos, top_pos)
    else:
        # Directly from the given value.
        pos2 = Vec.from_str(conditions.resolve_value(inst, pos2))

    origin = Vec.from_str(inst['origin'])
    angles = Vec.from_str(inst['angles'])
    splash_pos.localise(origin, angles)
    pos1.localise(origin, angles)
    pos2.localise(origin, angles)

    # Since it's a straight line and you can't go through walls,
    # if pos1 and pos2 aren't in goo we aren't ever in goo.

    check_pos = [pos1, pos2]

    if pos1.z < origin.z:
        # If embedding in the floor, the positions can both be below the
        # actual surface. In that case check the origin too.
        check_pos.append(Vec(pos1.x, pos1.y, origin.z))

    if pos1.z == pos2.z:
        # Flat - this won't do anything...
        return

    for pos in check_pos:
        grid_pos = pos // 128 * 128
        grid_pos += (64, 64, 64)

        block = BLOCK_POS['world':pos]
        if block.is_goo:
            break
    else:
        return  # Not in goo at all

    water_pos = grid_pos + (0, 0, 32)

    # Check if both positions are above or below the water..
    # that means it won't ever trigger.
    if max(pos1.z, pos2.z) < water_pos.z - 8:
        return
    if min(pos1.z, pos2.z) > water_pos.z + 8:
        return

    # Pass along the water_pos encoded into the targetname.
    # Restrict the number of characters to allow direct slicing
    # in the script.
    enc_data = '_{:09.3f}{}'.format(
        water_pos.z + 12,
        'f' if fast_check else 's',
    )

    vmf.create_ent(
        classname='env_splash',
        targetname=conditions.local_name(inst, name) + enc_data,
        parentname=conditions.local_name(inst, parent),
        origin=splash_pos + (0, 0, 16),
        scale=scale,
        vscripts='BEE2/water_splash.nut',
        thinkfunction='Think',
        spawnflags='1',  # Trace downward to water surface.
    )
Пример #55
0
def res_set_texture(inst: Entity, res: Property):
    """Set the brush face at a location to a particular texture.

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

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

    If `template` is set, the template should be an axis aligned cube. This
    will be rotated by the instance angles, and then the face with the same
    orientation will be applied to the face (with the rotation and texture).
    """
    import vbsp
    pos = Vec.from_str(res['pos', '0 0 0'])
    pos.z -= 64  # Subtract so origin is the floor-position
    pos = pos.rotate_by_str(inst['angles', '0 0 0'])

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

    norm = Vec.from_str(res['dir', '0 0 -1']).rotate_by_str(
        inst['angles', '0 0 0']
    )

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

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

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

    face_to_mod = brush.face  # type: Side

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

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

    tex = res['tex']

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

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

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

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

        # Do <4x4>, <white-2x4>, etc
        color = str(brush.color)
        if tex.startswith('black') or tex.endswith('white'):
            # Override the color used for 2x2/4x4 brushes
            color = tex[:5]
        if tex.endswith('2x2') or tex.endswith('4x4'):
            # 4x4 and 2x2 instructions are ignored on floors and ceilings.
            orient = vbsp.get_face_orient(face_to_mod)
            if orient == vbsp.ORIENT.wall:
                face_to_mod.mat = vbsp.get_tex(
                    color + '.' + tex[-3:]
                )
            else:
                face_to_mod.mat = vbsp.get_tex(
                    color + '.' + str(orient)
                )
    else:
        face_to_mod.mat = tex
Пример #56
0
def res_insert_overlay(inst: Entity, res: Property) -> None:
    """Use a template to insert one or more overlays on a surface.

    Options:

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

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

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

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

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

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

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

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

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

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

    # Wipe the brushes from the map.
    if temp.detail is not None:
        temp.detail.remove()
        LOGGER.info(
            'Overlay template "{}" could set keep_brushes=0.',
            temp_id,
        )
Пример #57
0
def res_import_template_setup(res: Property):
    temp_id = res['id']

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

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

    for size in ('2x2', '4x4', 'wall', 'special'):
        if size in force:
            force_grid = size
            break
    else:
        force_grid = None

    invert_var = res['invertVar', '']
    color_var = res['colorVar', '']

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

    rem_replace_brush = True
    additional_ids = set()
    transfer_overlays = '1'
    try:
        replace_brush = res.find_key('replaceBrush')
    except NoKeyError:
        replace_brush_pos = None
    else:
        if replace_brush.has_children():
            replace_brush_pos = replace_brush['Pos', '0 0 0']
            additional_ids = set(map(
                srctools.conv_int,
                replace_brush['additionalIDs', ''].split(),
            ))
            rem_replace_brush = replace_brush.bool('removeBrush', True)
            transfer_overlays = replace_brush['transferOverlay', '1']
        else:
            replace_brush_pos = replace_brush.value  # type: str

        replace_brush_pos = Vec.from_str(replace_brush_pos)
        replace_brush_pos.z -= 64  # 0 0 0 defaults to the floor.

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

        # Spawn everything as detail, so they get put into a brush
        # entity.
        force_type = template_brush.TEMP_TYPES.detail
        outputs = [
            Output.parse(prop)
            for prop in
            res.find_children('Outputs')
        ]
    else:
        keys = None
        outputs = []
    visgroup_mode = res['visgroup', 'none'].casefold()
    if visgroup_mode not in ('none', 'choose'):
        visgroup_mode = srctools.conv_float(visgroup_mode.rstrip('%'), 0.00)
        if visgroup_mode == 0:
            visgroup_mode = 'none'

    # Generate the function which picks which visgroups to add to the map.
    if visgroup_mode == 'none':
        def visgroup_func(_):
            """none = don't add any visgroups."""
            return ()
    elif visgroup_mode == 'choose':
        def visgroup_func(groups):
            """choose = add one random group."""
            return [random.choice(groups)]
    else:
        def visgroup_func(groups):
            """Number = percent chance for each to be added"""
            for group in groups:
                val = random.uniform(0, 100)
                if val <= visgroup_mode:
                    yield group

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

    return (
        temp_id,
        dict(replace_tex),
        force_colour,
        force_grid,
        force_type,
        replace_brush_pos,
        rem_replace_brush,
        transfer_overlays,
        additional_ids,
        invert_var,
        color_var,
        visgroup_func,
        visgroup_force_var,
        keys,
        outputs,
    )
Пример #58
0
def add_voice(
    has_items: dict,
    style_vars_: dict,
    vmf_file_: VMF,
    map_seed: str,
    use_priority=True,
):
    """Add a voice line to the map."""
    global ALLOW_MID_VOICES, vmf_file, map_attr, style_vars
    LOGGER.info('Adding Voice Lines!')

    vmf_file = vmf_file_
    map_attr = has_items
    style_vars = style_vars_

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

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

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

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

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

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

    mid_quotes = []

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    LOGGER.info('Done!')
Пример #59
0
def res_camera(inst: Entity, res: Property):
    """Result for the camera item.

    Options:
    - cam_off: The position that the camera yaws around.
    - yaw_off: The offset from cam_off that the camera rotates up/down.
    - pitch_off: The offset from yaw_off that is where the sensor is.

    - yaw_inst: The instance to place for the yaw rotation.
    - pitch_inst: The instance to place for the up/down rotation.

    - yaw_range: How many degrees can the camera rotate from a forward position?
    - pitch_range: How many degrees can the camera rotate up/down?
    """
    conf = res.value
    normal = Vec(0, 0, 1).rotate_by_str(inst['angles'])
    if normal.z != 0:
        # Can't be on floor/ceiling!
        inst.remove()
        return
    base_yaw = math.degrees(math.atan2(normal.y, normal.x)) % 360
    inst['angles'] = '0 {:g} 0'.format(base_yaw)

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

    try:
        plate = faithplate.PLATES.pop(inst['targetname'])
    except KeyError:
        LOGGER.warning(
            'No faith plate info found for camera {}!',
            inst['targetname'],
        )
        inst.remove()
        return

    # Remove the triggers.
    plate.trig.remove()

    if isinstance(plate, faithplate.StraightPlate):
        # Just point straight ahead.
        target_loc = base_loc + 512 * normal
        # And remove the helper.
        plate.helper_trig.remove()
    else:
        if isinstance(plate.target, Vec):
            target_loc = plate.target
        else:
            # We don't particularly care about aiming to the front of angled
            # panels.
            target_loc = plate.target.pos + 64 * plate.target.normal
            # Remove the helper and a bullseye.
            plate.target.remove_portal_helper()
            plate.target.bullseye_count -= 1

    # Move three times to position the camera arms and lens.
    yaw_pos = Vec(conf['yaw_off']).rotate_by_str(inst['angles'])
    yaw_pos += base_loc

    pitch, yaw, _ = (target_loc - yaw_pos).to_angle()

    inst.map.create_ent(
        classname='func_instance',
        targetname=inst['targetname'],
        file=conf['yaw_inst'],
        angles='0 {:g} 0'.format(yaw),
        origin=yaw_pos,
    )

    pitch_pos = Vec(conf['pitch_off'])
    pitch_pos.rotate(yaw=yaw)
    pitch_pos.rotate_by_str(inst['angles'])
    pitch_pos += yaw_pos

    inst.map.create_ent(
        classname='func_instance',
        targetname=inst['targetname'],
        file=conf['pitch_inst'],
        angles='{:g} {:g} 0'.format(pitch, yaw),
        origin=pitch_pos,
    )

    cam_pos = Vec(conf['cam_off'])
    cam_pos.rotate(pitch=pitch, yaw=yaw)
    cam_pos += pitch_pos

    # Recompute, since this can be slightly different if the camera is large.
    cam_angles = (target_loc - cam_pos).to_angle()

    ALL_CAMERAS.append(Camera(inst, cam_pos, cam_angles))
Пример #60
0
def comp_trigger_goo(ctx: Context):
    """Creates triggers for Toxic Goo."""
    reloader_cache = {
    }  # type: Dict[Tuple[float, float, float, float], Entity]

    for trig in ctx.vmf.by_class['comp_trigger_p2_goo']:
        trig.remove()
        outputs = trig.outputs.copy()
        trig.outputs.clear()

        failsafe_delay = conv_float(trig['failsafe_delay'], 0.5)
        if failsafe_delay < 0.01:
            failsafe_delay = 0.01

        hurt = trig.copy()
        diss = trig.copy()
        ctx.vmf.add_ents([hurt, diss])
        spawnflags = conv_int(trig['spawnflags'])

        for keyvalue in [
                'dissolve_filter',
                'phys_offset',
                'failsafe_delay',
                'fadepreset',
                'fadecolor',
                'fadetime',
        ]:
            del diss[keyvalue], hurt[keyvalue]

        diss['classname'] = 'trigger_multiple'
        # No clients, add physics. But otherwise leave it to the user.
        diss['spawnflags'] = (spawnflags & ~1) | 8
        diss['wait'] = 0  # No delay.
        diss['filtername'] = trig['dissolve_filter']
        del diss['damagetype']

        diss_pos = Vec.from_str(diss['origin'])
        diss_pos.z -= conv_float(trig['phys_offset'])
        diss['origin'] = diss_pos

        hurt['spawnflags'] = 1  # Players.

        if conv_bool(trig['enablefade']):
            fade_time = conv_float(trig['fadetime'])
            fade_color = Vec.from_str(trig['fadepreset'])
            if fade_color == (-1, -1, -1):
                fade_color = Vec.from_str(trig['fadecolor'])
            fade_key = fade_color.x, fade_color.y, fade_color.z, fade_time
            try:
                reloader = reloader_cache[fade_key]
            except KeyError:
                reloader = reloader_cache[fade_key] = ctx.vmf.create_ent(
                    'player_loadsaved',
                    origin=diss['origin'],
                    rendercolor=str(fade_color),
                    renderamt=255,
                    duration=fade_time,
                    holdtime=10,
                    loadtime=fade_time + 0.1,
                )
                reloader.make_unique('reloader')
            hurt['classname'] = 'trigger_once'
            del hurt['damagetype']
            hurt.add_out(
                Output('OnStartTouch', reloader, 'Reload', only_once=True))

            # Make sure the failsafe delay is longer than the total fade time.
            failsafe_delay = min(failsafe_delay, fade_time + 0.15)
        else:
            hurt['classname'] = 'trigger_hurt'
            hurt['damage'] = hurt['damagecap'] = 10000
            hurt['damagemodel'] = 0  # No doubling
            hurt['nodmgforce'] = 1  # Don't throw players around.

        for out in outputs:
            if out.output.casefold() == 'onkillplayer':
                # Better than OnStartTouch, doesn't apply for god mode.
                out.output = 'OnHurtPlayer'
                hurt.add_out(out)
            elif out.output.casefold() == 'ondissolvephysics':
                out.output = 'OnStartTouch'
                diss.add_out(out)

        diss.add_out(
            Output('OnStartTouch', '!activator', 'SilentDissolve'),
            Output('OnStartTouch', '!activator', 'Kill', delay=failsafe_delay),
        )