Ejemplo n.º 1
0
def make_bottomless_pit(vmf: VMF, max_height):
    """Generate bottomless pits."""
    import vbsp

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

    use_skybox = bool(SETTINGS['skybox'])

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

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

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

        fog_opt = vbsp.settings['fog']

        # Now generate the sky_camera, with appropriate values.
        sky_camera = vmf.create_ent(
            classname='sky_camera',
            scale='1.0',
            origin=tele_off,
            angles=fog_opt['direction'],
            fogdir=fog_opt['direction'],
            fogcolor=fog_opt['primary'],
            fogstart=fog_opt['start'],
            fogend=fog_opt['end'],
            fogenable='1',
            heightFogStart=fog_opt['height_start'],
            heightFogDensity=fog_opt['height_density'],
            heightFogMaxDensity=fog_opt['height_max_density'],
        )

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    if not use_skybox:
        make_pit_shell(vmf)
        return

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

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

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

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

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

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

        file = random.choice(side_types[inst_type])

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

        # Straight uses two side-instances in parallel - "|o|"
        if inst_type is utils.CONN_TYPES.straight:
            file = random.choice(side_types[inst_type])
            if file != '':
                vmf.create_ent(
                    classname='func_instance',
                    file=file,
                    targetname='goo_side',
                    origin=tele_off + pos,
                    # Reverse direction
                    angles=Vec.from_str(angle) + (0, 180, 0),
                ).make_unique()
Ejemplo n.º 2
0
def res_goo_debris(vmf: VMF, res: Property) -> object:
    """Add random instances to goo squares.

    Options:
        - file: The filename for the instance. The variant files should be
            suffixed with `_1.vmf`, `_2.vmf`, etc.
        - space: the number of border squares which must be filled with goo
                 for a square to be eligible - defaults to 1.
        - weight, number: see the `Variant` result, a set of weights for the
                options
        - chance: The percentage chance a square will have a debris item
        - offset: A random xy offset applied to the instances.
    """
    from precomp import brushLoc

    space = res.int('spacing', 1)
    rand_count = res.int('number', None)
    rand_list: list[int] | None
    if rand_count:
        rand_list = rand.parse_weights(
            rand_count,
            res['weights', ''],
        )
    else:
        rand_list = None
    chance = res.int('chance', 30) / 100
    file = res['file']
    offset = res.int('offset', 0)

    if file.endswith('.vmf'):
        file = file[:-4]

    goo_top_locs = {
        pos.as_tuple()
        for pos, block in
        brushLoc.POS.items()
        if block.is_goo and block.is_top
    }

    if space == 0:
        # No spacing needed, just copy
        possible_locs = [Vec(loc) for loc in goo_top_locs]
    else:
        possible_locs = []
        for x, y, z in goo_top_locs:
            # Check to ensure the neighbouring blocks are also
            # goo brushes (depending on spacing).
            for x_off, y_off in utils.iter_grid(
                min_x=-space,
                max_x=space + 1,
                min_y=-space,
                max_y=space + 1,
                stride=1,
            ):
                if x_off == y_off == 0:
                    continue  # We already know this is a goo location
                if (x + x_off, y + y_off, z) not in goo_top_locs:
                    break  # This doesn't qualify
            else:
                possible_locs.append(brushLoc.grid_to_world(Vec(x, y, z)))

    LOGGER.info(
        'GooDebris: {}/{} locations',
        len(possible_locs),
        len(goo_top_locs),
    )

    for loc in possible_locs:
        rng = rand.seed(b'goo_debris', loc)
        if rng.random() > chance:
            continue

        if rand_list is not None:
            rand_fname = f'{file}_{rng.choice(rand_list) + 1}.vmf'
        else:
            rand_fname = file + '.vmf'

        if offset > 0:
            loc.x += rng.randint(-offset, offset)
            loc.y += rng.randint(-offset, offset)
        loc.z -= 32  # Position the instances in the center of the 128 grid.
        vmf.create_ent(
            classname='func_instance',
            file=rand_fname,
            origin=loc.join(' '),
            angles=f'0 {rng.randrange(0, 3600) / 10} 0'
        )

    return RES_EXHAUSTED
Ejemplo n.º 3
0
def make_pit_shell(vmf: VMF):
    """If the pit is surrounded on all sides, we can just extend walls down.

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

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

                vmf.add_brush(prism.solid)
                continue

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

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

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

    # Since we can chuck gel down the pit, cover it in a noportal_volume
    # to stop players from portalling past the hurt trigger.
    diss_trig = vmf.create_ent(
        classname='func_noportal_volume',
        origin=options.get(Vec, 'global_pti_ents_loc'),
    )
    diss_trig.solids = [
        vmf.make_prism(
            Vec(-8 * 128, -8 * 128, -64),
            Vec(20 * 128, 20 * 128, -4864),
            mat='tools/toolstrigger',
        ).solid
    ]