Beispiel #1
0
 def randomise(inst: Entity) -> None:
     """Apply the random number."""
     rng = rand.seed(b'rand_num', inst, seed)
     if is_float:
         inst.fixup[var] = rng.uniform(min_val, max_val)
     else:
         inst.fixup[var] = rng.randint(min_val, max_val)
Beispiel #2
0
def res_rand_vec(inst: Entity, res: Property) -> None:
    """A modification to RandomNum which generates a random vector instead.

    `decimal`, `seed` and `ResultVar` work like RandomNum. `min_x`, `max_y` etc
    are used to define the boundaries. If the min and max are equal that number
    will be always used instead.
    """
    is_float = srctools.conv_bool(res['decimal'])
    var = res['resultvar', '$random']

    rng = rand.seed(b'rand_vec', inst, res['seed', ''])

    if is_float:
        func = rng.uniform
    else:
        func = rng.randint

    value = Vec()

    for axis in 'xyz':
        max_val = srctools.conv_float(res['max_' + axis, 0.0])
        min_val = srctools.conv_float(res['min_' + axis, 0.0])
        if min_val == max_val:
            value[axis] = min_val
        else:
            value[axis] = func(min_val, max_val)

    inst.fixup[var] = value.join(' ')
Beispiel #3
0
    def broken_iter(
        self,
        chance: float,
    ) -> Iterator[tuple[Vec, Vec, bool]]:
        """Iterator to compute positions for straight segments.

        This produces point pairs which fill the space from 0-dist.
        Neighbouring sections will be merged when they have the same
        type.
        """
        rng = rand.seed(b'ant_broken', self.start, self.end, chance)
        offset = self.end - self.start
        dist = offset.mag() // 16
        norm = 16 * offset.norm()

        if dist < 3 or chance == 0:
            # Short antlines always are either on/off.
            yield self.start, self.end, (rng.randrange(100) < chance)
        else:
            run_start = self.start
            last_type = rng.randrange(100) < chance
            for i in range(1, int(dist)):
                next_type = rng.randrange(100) < chance
                if next_type != last_type:
                    yield run_start, self.start + i * norm, last_type
                    last_type = next_type
                    run_start = self.start + i * norm
            yield run_start, self.end, last_type
Beispiel #4
0
 def _get(self, loc: Vec, tex_name: str):
     if type(tex_name) != str:
         try:
             tex_name = self.enum_data[id(tex_name)]
         except KeyError:
             raise ValueError(
                 f'Unknown enum value {tex_name!r} '
                 f'for generator type {self.category}!') from None
     return rand.seed(b'tex_rand', loc).choice(self.textures[tex_name])
Beispiel #5
0
    def _get(self, loc: Vec, tex_name: str) -> str:
        clump_seed = self._find_clump(loc)

        if clump_seed is None:
            # No clump found - return the gap texture.
            # But if the texture is GOO_SIDE, do that instead.
            # If we don't have a gap texture, just use any one.
            rng = rand.seed(b'tex_clump_side', loc)
            if tex_name == TileSize.GOO_SIDE or TileSize.CLUMP_GAP not in self:
                return rng.choice(self.textures[tex_name])
            else:
                return rng.choice(self.textures[TileSize.CLUMP_GAP])

        # Mix these three values together to determine the texture.
        # The clump seed makes each clump different, and adding the texture
        # name makes sure different surface types don't copy each other's
        # indexes.
        rng = rand.seed(b'tex_clump_side', self.gen_seed, tex_name, clump_seed)
        return rng.choice(self.textures[tex_name])
Beispiel #6
0
 def shift_ent(inst: Entity) -> None:
     """Randomly shift the instance."""
     rng = rand.seed(b'rand_shift', inst, seed)
     pos = Vec(
         rng.uniform(min_x, max_x),
         rng.uniform(min_y, max_y),
         rng.uniform(min_z, max_z),
     )
     pos.localise(Vec.from_str(inst['origin']),
                  Angle.from_str(inst['angles']))
     inst['origin'] = pos
Beispiel #7
0
    def apply_switch(inst: Entity) -> None:
        """Execute a switch."""
        if method is SWITCH_TYPE.RANDOM:
            cases = conf_cases.copy()
            rand.seed(b'switch', rand_seed, inst).shuffle(cases)
        else:  # Won't change.
            cases = conf_cases

        run_default = True
        for flag, results in cases:
            # If not set, always succeed for the random situation.
            if flag.real_name and not check_flag(inst.map, flag, inst):
                continue
            for sub_res in results:
                Condition.test_result(inst, sub_res)
            run_default = False
            if method is not SWITCH_TYPE.ALL:
                # All does them all, otherwise we quit now.
                break
        if run_default:
            for sub_res in default:
                Condition.test_result(inst, sub_res)
Beispiel #8
0
    def apply_random(inst: Entity) -> None:
        """Pick a random result and run it."""
        rng = rand.seed(b'rand_res', inst, seed)
        if rng.randrange(100) > chance:
            return

        ind = rng.choice(weights_list)
        choice = results[ind]
        if choice.name == 'nop':
            pass
        elif choice.name == 'group':
            for sub_res in choice:
                if Condition.test_result(coll, inst, sub_res) is RES_EXHAUSTED:
                    sub_res.name = 'nop'
                    sub_res.value = ''
        else:
            if Condition.test_result(coll, inst, choice) is RES_EXHAUSTED:
                choice.name = 'nop'
                choice.value = ''
Beispiel #9
0
 def add_group(inst: Entity) -> None:
     """Place the group."""
     rng = rand.seed(b'shufflegroup', conf_seed, inst)
     pools = all_pools.copy()
     for (flags, value, potential_pools) in conf_selectors:
         for flag in flags:
             if not conditions.check_flag(vmf, flag, inst):
                 break
         else:  # Succeeded.
             allowed_inst = [(name, inst) for (name, inst) in pools
                             if name in potential_pools]
             name, filename = rng.choice(allowed_inst)
             pools.remove((name, filename))
             vmf.create_ent(
                 'func_instance',
                 targetname=inst['targetname'],
                 file=filename,
                 angles=inst['angles'],
                 origin=inst['origin'],
                 fixup_style='0',
             ).fixup[conf_variable] = value
Beispiel #10
0
    def insert_over(inst: Entity) -> None:
        """Apply the result."""
        temp_id = inst.fixup.substitute(orig_temp_id)

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

        face_pos = conditions.resolve_offset(inst, face_str)
        normal = orig_norm @ angles

        # Don't make offset change the face_pos value..
        origin += offset @ angles

        for axis, norm in enumerate(normal):
            # Align to the center of the block grid. The normal direction is
            # already correct.
            if norm == 0:
                face_pos[axis] = face_pos[axis] // 128 * 128 + 64

        # 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(
            vmf,
            temp_id,
            origin,
            angles,
            targetname=inst['targetname', ''],
            force_type=TEMP_TYPES.detail,
        )

        for over in temp.overlay:
            pos = Vec.from_str(over['basisorigin'])
            mat = over['material']
            try:
                replace = replace_tex[mat.casefold().replace('\\', '/')]
            except KeyError:
                pass
            else:
                mat = rand.seed(b'temp_over', temp_id, pos).choice(replace)

            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(pos, 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,
            )
Beispiel #11
0
def res_set_tile(inst: Entity, res: Property) -> None:
    """Set 4x4 parts of a tile to the given values.

    `Offset` defines the position of the upper-left tile in the grid.
    Each `Tile` section defines a row of the positions to edit like so:
        "Tile" "bbbb"
        "Tile" "b..b"
        "Tile" "b..b"
        "Tile" "bbbb"
    If `Force` is true, the specified tiles will override any existing ones
    and create the tile if necessary.
    Otherwise they will be merged in - white/black tiles will not replace
    tiles set to nodraw or void for example.
    `chance`, if specified allows producing irregular tiles by randomly not
    changing the tile.

    If you need less regular placement (other orientation, precise positions)
    use a bee2_template_tilesetter in a template.

    Allowed tile characters:
    - `W`: White tile.
    - `w`: White 4x4 only tile.
    - `B`: Black tile.
    - `b`: Black 4x4 only tile.
    - `g`: The side/bottom of goo pits.
    - `n`: Nodraw surface.
    - `i`: Invert the tile surface, if black/white.
    - `1`: Convert to a 1x1 only tile, if a black/white tile.
    - `4`: Convert to a 4x4 only tile, if a black/white tile.
    - `.`: Void (remove the tile in this position).
    - `_` or ` `: Placeholder (don't modify this space).
    - `x`: Cutout Tile (Broken)
    - `o`: Cutout Tile (Partial)
    """
    origin = Vec.from_str(inst['origin'])
    orient = Matrix.from_angle(Angle.from_str(inst['angles']))

    offset = (res.vec('offset', -48, 48) - (0, 0, 64)) @ orient + origin

    norm = round(orient.up(), 6)

    force_tile = res.bool('force')

    tiles: list[str] = [
        row.value for row in res if row.name in ('tile', 'tiles')
    ]
    if not tiles:
        raise ValueError('No "tile" parameters in SetTile!')

    chance = srctools.conv_float(res['chance', '100'].rstrip('%'), 100.0)
    if chance < 100.0:
        rng = rand.seed(b'tile', inst, res['seed', ''])
    else:
        rng = None

    for y, row in enumerate(tiles):
        for x, val in enumerate(row):
            if val in '_ ':
                continue

            if rng is not None and rng.uniform(0, 100) > chance:
                continue

            pos = Vec(32 * x, -32 * y, 0) @ orient + offset

            if val == '4':
                size = tiling.TileSize.TILE_4x4
            elif val == '1':
                size = tiling.TileSize.TILE_1x1
            elif val == 'i':
                size = None
            else:
                try:
                    new_tile = tiling.TILETYPE_FROM_CHAR[val]
                except KeyError:
                    LOGGER.warning('Unknown tiletype "{}"!', val)
                else:
                    tiling.edit_quarter_tile(pos, norm, new_tile, force_tile)
                continue

            # Edit the existing tile.
            try:
                tile, u, v = tiling.find_tile(pos, norm, force_tile)
            except KeyError:
                LOGGER.warning(
                    'Expected tile, but none found: {}, {}',
                    pos,
                    norm,
                )
                continue

            if size is None:
                # Invert the tile.
                tile[u, v] = tile[u, v].inverted
                continue

            # Unless forcing is enabled don't alter the size of GOO_SIDE.
            if tile[u, v].is_tile and tile[u,
                                           v] is not tiling.TileType.GOO_SIDE:
                tile[u, v] = tiling.TileType.with_color_and_size(
                    size, tile[u, v].color)
            elif force_tile:
                # If forcing, make it black. Otherwise no need to change.
                tile[u, v] = tiling.TileType.with_color_and_size(
                    size, tiling.Portalable.BLACK)
Beispiel #12
0
    def place_template(inst: Entity) -> None:
        """Place a template."""
        temp_id = inst.fixup.substitute(orig_temp_id)

        # Special case - if blank, just do nothing silently.
        if not temp_id:
            return

        temp_name, visgroups = template_brush.parse_temp_name(temp_id)
        try:
            template = template_brush.get_template(temp_name)
        except template_brush.InvalidTemplateName:
            # If we did lookup, display both forms.
            if temp_id != orig_temp_id:
                LOGGER.warning('{} -> "{}" is not a valid template!',
                               orig_temp_id, temp_name)
            else:
                LOGGER.warning('"{}" is not a valid template!', temp_name)
            # We don't want an error, just quit.
            return

        for vis_flag_block in visgroup_instvars:
            if all(
                    conditions.check_flag(flag, coll, inst)
                    for flag in vis_flag_block):
                visgroups.add(vis_flag_block.real_name)

        force_colour = conf_force_colour
        if color_var == '<editor>':
            # Check traits for the colour it should be.
            traits = instance_traits.get(inst)
            if 'white' in traits:
                force_colour = texturing.Portalable.white
            elif 'black' in traits:
                force_colour = texturing.Portalable.black
            else:
                LOGGER.warning(
                    '"{}": Instance "{}" '
                    "isn't one with inherent color!",
                    temp_id,
                    inst['file'],
                )
        elif color_var:
            color_val = conditions.resolve_value(inst, color_var).casefold()

            if color_val == 'white':
                force_colour = texturing.Portalable.white
            elif color_val == 'black':
                force_colour = texturing.Portalable.black
        # else: no color var

        if srctools.conv_bool(conditions.resolve_value(inst, invert_var)):
            force_colour = template_brush.TEMP_COLOUR_INVERT[conf_force_colour]
        # else: False value, no invert.

        if ang_override is not None:
            orient = ang_override
        else:
            orient = rotation @ Angle.from_str(inst['angles', '0 0 0'])
        origin = conditions.resolve_offset(inst, offset)

        # If this var is set, it forces all to be included.
        if srctools.conv_bool(
                conditions.resolve_value(inst, visgroup_force_var)):
            visgroups.update(template.visgroups)
        elif visgroup_func is not None:
            visgroups.update(
                visgroup_func(
                    rand.seed(b'temp', template.id, origin, orient),
                    list(template.visgroups),
                ))

        LOGGER.debug('Placing template "{}" at {} with visgroups {}',
                     template.id, origin, visgroups)

        temp_data = template_brush.import_template(
            vmf,
            template,
            origin,
            orient,
            targetname=inst['targetname'],
            force_type=force_type,
            add_to_map=True,
            coll=coll,
            additional_visgroups=visgroups,
            bind_tile_pos=bind_tile_pos,
            align_bind=align_bind_overlay,
        )

        if key_block is not None:
            conditions.set_ent_keys(temp_data.detail, inst, key_block)
            br_origin = Vec.from_str(key_block.find_key('keys')['origin'])
            br_origin.localise(origin, orient)
            temp_data.detail['origin'] = br_origin

            move_dir = temp_data.detail['movedir', '']
            if move_dir.startswith('<') and move_dir.endswith('>'):
                move_dir = Vec.from_str(move_dir) @ orient
                temp_data.detail['movedir'] = move_dir.to_angle()

            for out in outputs:
                out = out.copy()
                out.target = conditions.local_name(inst, out.target)
                temp_data.detail.add_out(out)

        template_brush.retexture_template(
            temp_data,
            origin,
            inst.fixup,
            replace_tex,
            force_colour,
            force_grid,
            surf_cat,
            sense_offset,
        )

        for picker_name, picker_var in picker_vars:
            picker_val = temp_data.picker_results.get(picker_name, None)
            if picker_val is not None:
                inst.fixup[picker_var] = picker_val.value
            else:
                inst.fixup[picker_var] = ''
Beispiel #13
0
def add_voice(
    voice_attrs: dict,
    style_vars: dict,
    vmf: VMF,
    use_priority=True,
) -> None:
    """Add a voice line to the map."""
    from precomp.conditions.monitor import make_voice_studio
    LOGGER.info('Adding Voice Lines!')

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

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

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

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

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

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

    mid_quotes = []

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

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

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

    if has_responses():
        LOGGER.info('Generating responses data..')
        encode_coop_responses(vmf, quote_loc, allow_dings, voice_attrs)

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

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

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

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

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

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

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

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

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

            if use_priority:
                chosen = possible_quotes[0].lines
            else:
                # Chose one of the quote blocks.
                chosen = rand.seed(
                    b'VOICE_QUOTE_BLOCK', *[
                        prop['id', 'ID'] for quoteblock in possible_quotes
                        for prop in quoteblock.lines
                    ]).choice(possible_quotes).lines

            # Use the IDs for the voice lines, so each quote block will chose different lines.
            rng = rand.seed(b'VOICE_QUOTE',
                            *[prop['id', 'ID'] for prop in chosen])

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

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

    LOGGER.info('{} Mid quotes', len(mid_quotes))
    for mid_lines in mid_quotes:
        line = rand.seed(b'mid_quote',
                         *[name for item, ding, name in mid_lines
                           ]).choice(mid_lines)
        mid_item, use_ding, mid_name = line
        add_quote(vmf, mid_item, mid_name, quote_loc, style_vars, use_ding)

    LOGGER.info('Done!')
Beispiel #14
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
Beispiel #15
0
 def rand_func(inst: Entity) -> bool:
     """Apply the random chance."""
     return rand.seed(b'rand_flag', inst, seed).randrange(100) < chance
Beispiel #16
0
    def export(self, vmf: VMF, *, wall_conf: AntType, floor_conf: AntType) -> None:
        """Add the antlines into the map."""

        # First, do some optimisation. If corners aren't defined, try and
        # optimise those antlines out by merging the straight segment
        # before/after it into the corners.

        collapse_line: list[Segment | None]
        if not wall_conf.tex_corner or not floor_conf.tex_corner:
            collapse_line = list(self.line)
            for i, seg in enumerate(collapse_line):
                if seg is None or seg.type is not SegType.STRAIGHT:
                    continue
                if (floor_conf if seg.on_floor else wall_conf).tex_corner:
                    continue
                for corner_ind in [i-1, i+1]:
                    if i == -1:
                        continue
                    try:
                        corner = collapse_line[corner_ind]
                    except IndexError:
                        # Each end of the list.
                        continue

                    if (
                        corner is not None and
                        corner.type is SegType.CORNER and
                        corner.normal == seg.normal
                    ):
                        corner_pos = corner.start
                        if (seg.start - corner_pos).mag_sq() == 8 ** 2:
                            # The line segment is at the border between them,
                            # the corner is at the center. So move double the
                            # distance towards the corner, so it reaches to the
                            # other side of the corner and replaces it.
                            seg.start += 2 * (corner_pos - seg.start)
                            # Remove corner by setting to None, so we aren't
                            # resizing the list constantly.
                            collapse_line[corner_ind] = None
                            # Now merge together the tiledefs.
                            seg.tiles.update(corner.tiles)
                        elif (seg.end - corner_pos).mag_sq() == 8 ** 2:
                            seg.end += 2 * (corner_pos - seg.end)
                            collapse_line[corner_ind] = None
                            seg.tiles.update(corner.tiles)

            self.line[:] = [seg for seg in collapse_line if seg is not None]
            LOGGER.info('Collapsed {} antline corners', collapse_line.count(None))

        for seg in self.line:
            conf = floor_conf if seg.on_floor else wall_conf
            # Check tiledefs in the voxels, and assign just in case.
            # antline corner items don't have them defined, and some embedfaces don't work
            # properly. But we keep any segments actually defined also.
            mins, maxs = Vec.bbox(seg.start, seg.end)
            norm_axis = seg.normal.axis()
            u_axis, v_axis = Vec.INV_AXIS[norm_axis]
            for pos in Vec.iter_line(mins, maxs, 128):
                pos[u_axis] = pos[u_axis] // 128 * 128 + 64
                pos[v_axis] = pos[v_axis] // 128 * 128 + 64
                pos -= 64 * seg.normal
                try:
                    tile = tiling.TILES[pos.as_tuple(), seg.normal.as_tuple()]
                except KeyError:
                    pass
                else:
                    seg.tiles.add(tile)

            rng = rand.seed(b'antline', seg.start, seg.end)
            if seg.type is SegType.CORNER:
                mat: AntTex
                if rng.randrange(100) < conf.broken_chance:
                    mat = rng.choice(conf.broken_corner or conf.broken_straight)
                else:
                    mat = rng.choice(conf.tex_corner or conf.tex_straight)

                # Because we can, apply a random rotation to mix up the texture.
                orient = Matrix.from_angle(seg.normal.to_angle(
                    rng.choice((0.0, 90.0, 180.0, 270.0))
                ))
                self._make_overlay(
                    vmf,
                    seg,
                    seg.start,
                    16.0 * orient.left(),
                    16.0 * orient.up(),
                    mat,
                )
            else:  # Straight
                # TODO: Break up these segments.
                for a, b, is_broken in seg.broken_iter(conf.broken_chance):
                    if is_broken:
                        mat = rng.choice(conf.broken_straight)
                    else:
                        mat = rng.choice(conf.tex_straight)
                    self._make_straight(
                        vmf,
                        seg,
                        a,
                        b,
                        mat,
                    )
Beispiel #17
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

        rng = rand.seed(b'pit', pos.x, pos.y)
        file = rng.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 = rng.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()
Beispiel #18
0
    def setup(self, vmf: VMF, tiles: List['TileDef']) -> None:
        """Build the list of clump locations."""
        assert self.portal is not None
        assert self.orient is not None

        # Convert the generator key to a generator-specific seed.
        # That ensures different surfaces don't end up reusing the same
        # texture indexes.
        self.gen_seed = b''.join([
            self.category.name.encode(),
            self.portal.name.encode(),
            self.orient.name.encode(),
        ])

        LOGGER.info('Generating texture clumps...')

        clump_length: int = self.options['clump_length']
        clump_width: int = self.options['clump_width']

        # The tiles currently present in the map.
        orient_z = self.orient.z
        remaining_tiles: Set[Tuple[float, float, float]] = {
            (tile.pos + 64 * tile.normal // 128 * 128).as_tuple()
            for tile in tiles if tile.normal.z == orient_z
        }

        # A global RNG for picking clump positions.
        clump_rand = rand.seed(b'clump_pos')

        pos_min = Vec()
        pos_max = Vec()

        # For debugging, generate skip brushes with the shape of the clumps.
        debug_visgroup: Optional[VisGroup]
        if self.options['clump_debug']:
            debug_visgroup = vmf.create_visgroup(
                f'{self.category.name}_{self.orient.name}_{self.portal.name}')
        else:
            debug_visgroup = None

        while remaining_tiles:
            # Pick from a random tile.
            tile_pos = next(
                itertools.islice(
                    remaining_tiles,
                    clump_rand.randrange(0, len(remaining_tiles)),
                    len(remaining_tiles),
                ))
            remaining_tiles.remove(tile_pos)

            pos = Vec(tile_pos)

            # Clumps are long strips mainly extended in one direction
            # In the other directions extend by 'width'. It can point any axis.
            direction = clump_rand.choice('xyz')
            for axis in 'xyz':
                if axis == direction:
                    dist = clump_length
                else:
                    dist = clump_width
                pos_min[axis] = pos[axis] - clump_rand.randint(0, dist) * 128
                pos_max[axis] = pos[axis] + clump_rand.randint(0, dist) * 128

            remaining_tiles.difference_update(
                map(Vec.as_tuple, Vec.iter_grid(pos_min, pos_max, 128)))

            self._clump_locs.append(
                Clump(
                    pos_min.x,
                    pos_min.y,
                    pos_min.z,
                    pos_max.x,
                    pos_max.y,
                    pos_max.z,
                    # We use this to reseed an RNG, giving us the same textures
                    # each time for the same clump.
                    clump_rand.getrandbits(64).to_bytes(8, 'little'),
                ))
            if debug_visgroup is not None:
                # noinspection PyUnboundLocalVariable
                debug_brush: Solid = vmf.make_prism(
                    pos_min - 64,
                    pos_max + 64,
                    'tools/toolsskip',
                ).solid
                debug_brush.visgroup_ids.add(debug_visgroup.id)
                debug_brush.vis_shown = False
                vmf.add_brush(debug_brush)

        LOGGER.info(
            '{}.{}.{}: {} Clumps for {} tiles',
            self.category.name,
            self.orient.name,
            self.portal.name,
            len(self._clump_locs),
            len(tiles),
        )
Beispiel #19
0
 def apply_variant(inst: Entity) -> None:
     """Apply the variant."""
     rng = rand.seed(b'variant', inst, seed)
     conditions.add_suffix(inst, f"_var{rng.choice(weighting) + 1}")