예제 #1
0
def res_temp_reset_gridded(inst: Entity):
    """Temporary result - reset gridded state on a surface.

    Used for antline routers to undo ItemLightStrip's 4x4 texturing.
    This should be removed after geometry is done.
    """
    pos = Vec(0, 0, -64)
    pos.localise(
        Vec.from_str(inst['origin']),
        Vec.from_str(inst['angles'])
    )
    norm = Vec(z=-1).rotate_by_str(inst['angles'])
    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 brush is None:
        return

    if brush.color is template_brush.MAT_TYPES.white:
        brush.face.mat = const.WhitePan.WHITE_1x1
    else:
        brush.face.mat = const.BlackPan.BLACK_1
예제 #2
0
파일: monitor.py 프로젝트: Thedoczek/BEE2.4
def res_monitor(inst: Entity, res: Property) -> None:
    """Result for the monitor component.

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

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

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

    HAS_MONITOR = True

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

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

    # We need to generate an ai_relationship, which makes turrets hate
    # a bullseye.
    if needs_turret:
        loc = Vec(bullseye_loc)
        loc.localise(
            Vec.from_str(inst['origin']),
            Angle.from_str(inst['angles']),
        )
        bullseye_name = conditions.local_name(inst, bullseye_name)
        inst.map.create_ent(
            classname='npc_bullseye',
            targetname=bullseye_name,
            parentname=conditions.local_name(inst, bullseye_parent),
            spawnflags=221186,  # Non-solid, invisible, etc..
            origin=loc,
        )
        relation = inst.map.create_ent(
            classname='ai_relationship',
            targetname='@monitor_turr_hate',
            parentname=bullseye_name,  # When killed, destroy this too.
            spawnflags=2,  # Notify turrets about monitor locations
            disposition=1,  # Hate
            origin=loc,
            subject='npc_portal_turret_floor',
            target=bullseye_name,
        )
        MONITOR_RELATIONSHIP_ENTS.append(relation)
예제 #3
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
예제 #4
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,
        )
예제 #5
0
def res_make_funnel_light(inst: Entity) -> None:
    """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']), Angle.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,
        )
def res_monitor(inst: Entity, res: Property):
    """Result for the monitor component.

    """
    global NEEDS_TURRET
    import vbsp

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

    ALL_MONITORS.append(Monitor(inst))

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

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

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

        NEEDS_TURRET = True
예제 #7
0
파일: monitor.py 프로젝트: BenVlodgi/BEE2.4
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)
예제 #8
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()
예제 #9
0
def flag_blockpos_type(inst: Entity, flag: Property):
    """Determine the type of a grid position.

    If the value is single value, that should be the type.
    Otherwise, the value should be a block with 'offset' and 'type' values.
    The offset is in block increments, with 0 0 0 equal to the mounting surface.

    The type should be a space-seperated list of locations:
    * VOID (Outside the map)
    * SOLID (Full wall cube)
    * EMBED (Hollow wall cube)
    * AIR (Inside the map, may be occupied by items)
    * OCCUPIED (Known to be occupied by items)
    * PIT (Bottomless pits, any)
      * PIT_SINGLE (one-high)
      * PIT_TOP
      * PIT_MID
      * PIT_BOTTOM
    * GOO
      * GOO_SINGLE (one-deep goo)
      * GOO_TOP (goo surface)
      * GOO_MID
      * GOO_BOTTOM (floor)
    """
    if flag.has_children():
        pos = flag.vec('offset') * 128
        types = flag['type'].split()
    else:
        types = flag.value.split()
        pos = Vec()
    pos.z -= 128
    pos.localise(
        Vec.from_str(inst['origin']),
        Vec.from_str(inst['angles']),
    )
    block = brushLoc.POS['world':pos]
    for block_type in types:
        try:
            allowed = brushLoc.BLOCK_LOOKUP[block_type.casefold()]
        except KeyError:
            raise ValueError(
                '"{}" is not a valid block type!'.format(block_type))
        if block in allowed:
            return True
    return False
예제 #10
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
예제 #11
0
파일: __init__.py 프로젝트: BEEmod/BEE2.4
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:

    * Any $replace variables
    * <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 = inst.fixup.substitute(value).casefold()
    # Offset the overlay by the given distance
    # Some special placeholder values:
    if value == '<piston_start>' or value == '<piston>':
        if inst.fixup.bool(consts.FixupVars.PIST_IS_UP):
            value = '<piston_top>'
        else:
            value = '<piston_bottom>'
    elif value == '<piston_end>':
        if inst.fixup.bool(consts.FixupVars.PIST_IS_UP):
            value = '<piston_bottom>'
        else:
            value = '<piston_top>'

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

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

    return offset
예제 #12
0
def res_signage(vmf: VMF, inst: Entity, res: Property):
    """Implement the Signage item."""
    sign: Optional[Sign]
    try:
        sign = (CONN_SIGNAGES if res.bool('connection') else
                SIGNAGES)[inst.fixup[const.FixupVars.TIM_DELAY]]
    except KeyError:
        # Blank sign
        sign = None

    has_arrow = inst.fixup.bool(const.FixupVars.ST_ENABLED)

    sign_prim: Optional[Sign]
    sign_sec: Optional[Sign]

    if has_arrow:
        sign_prim = sign
        sign_sec = SIGNAGES['arrow']
    elif sign is not None:
        sign_prim = sign.primary or sign
        sign_sec = sign.secondary or None
    else:
        # Neither sign or arrow, delete this.
        inst.remove()
        return

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

    normal = Vec(z=-1).rotate(*angles)
    forward = Vec(x=-1).rotate(*angles)

    prim_pos = Vec(0, -16, -64)
    sec_pos = Vec(0, 16, -64)

    prim_pos.localise(origin, angles)
    sec_pos.localise(origin, angles)

    template_id = res['template_id', '']

    face: Side

    if inst.fixup.bool(const.FixupVars.ST_REVERSED):
        # Flip around.
        forward = -forward
        prim_visgroup = 'secondary'
        sec_visgroup = 'primary'
        prim_pos, sec_pos = sec_pos, prim_pos
    else:
        prim_visgroup = 'primary'
        sec_visgroup = 'secondary'

    if template_id:
        brush_faces: List[Side] = []
        if sign_prim and sign_sec:
            visgroup = [prim_visgroup, sec_visgroup]
        elif sign_prim:
            visgroup = [prim_visgroup]
        else:
            visgroup = [sec_visgroup]
        template = template_brush.import_template(
            template_id,
            origin,
            angles,
            force_type=template_brush.TEMP_TYPES.detail,
            additional_visgroups=visgroup,
        )

        for face in template.detail.sides():
            if face.normal() == normal:
                brush_faces.append(face)
    else:
        # Direct on the surface.
        block_center = origin // 128 * 128 + (64, 64, 64)
        try:
            face = conditions.SOLIDS[(block_center +
                                      64 * normal).as_tuple()].face
        except KeyError:
            LOGGER.warning(
                "Can't place signage at ({}) in ({}) direction!",
                block_center,
                normal,
            )
            return
        brush_faces = [face]

    if sign_prim is not None:
        place_sign(
            vmf,
            brush_faces,
            sign_prim,
            prim_pos,
            normal,
            forward,
            rotate=True,
        )

    if sign_sec is not None:
        if has_arrow and res.bool('arrowDown'):
            # Arrow texture points down, need to flip it.
            forward = -forward
        place_sign(
            vmf,
            brush_faces,
            sign_sec,
            sec_pos,
            normal,
            forward,
            rotate=not has_arrow,
        )
예제 #13
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 = Angle.from_str(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) @ travel_ang

        pos2 = Vec(x=lin_off) @ travel_ang + 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 = Angle.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.
    )
예제 #14
0
def res_cutout_tile(vmf: srctools.VMF, res: Property):
    """Generate random quarter tiles, like in Destroyed or Retro maps.

    - `MarkerItem` is the instance file to look for (`<ITEM_BEE2_CUTOUT_TILE>`)
    - `floor_chance`: The percentage change for a segment in the middle of the floor to be a normal tile.
    - `floor_glue_chance`: The chance for any tile to be glue - this should be higher than the regular chance, as that overrides this.
    - `rotateMax` is the maximum angle to rotate squarebeam models.
    - `squarebeamsSkin` sets the skin to use for the squarebeams floor frame.
    - `dispBase`, if true makes the floor a displacement with random alpha.
    - `Materials` blocks specify the possible materials to use:
          - `squarebeams` is the squarebeams variant to use.
          - `ceilingwalls` are the sides of the ceiling section.
          - `floorbase` is the texture under floor sections.
            If `dispBase` is True this is a displacement material.
          - `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.)

    """
    marker_filenames = instanceLocs.resolve(res['markeritem'])

    # TODO: Reimplement cutout tiles.
    for inst in vmf.by_class['func_instance']:
        if inst['file'].casefold() in marker_filenames:
            inst.remove()
    return

    x: float
    y: float
    max_x: float
    max_y: float

    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_locs = set()
    # If any signage is present in the map, we need to force tiles to
    # appear at that location!
    for over in 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')):
            add_signage_loc(sign_locs, Vec.from_str(over['origin']))

    for item in connections.ITEMS.values():
        for ind_pan in item.ind_panels:
            loc = Vec(0, 0, -64)
            loc.localise(
                Vec.from_str(ind_pan['origin']),
                Vec.from_str(ind_pan['angles']),
            )
            add_signage_loc(sign_locs, loc)

    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.
    # all_floors[z][x,y] = count
    floor_neighbours = defaultdict(
        dict)  # type: Dict[float, Dict[Tuple[float, float], int]]

    for mat_prop in res.find_key('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 vmf.by_class['func_instance']:
        if inst['file'].casefold() not in marker_filenames:
            continue
        targ = inst['targetname']
        normal = 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 normal == (0, 0, 1):
            io_list = FLOOR_IO
        else:
            io_list = CEIL_IO

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

        item = connections.ITEMS[targ]
        item.delete_antlines()

        if item.outputs:
            for conn in list(item.outputs):
                if conn.to_item.inst['file'].casefold() in marker_filenames:
                    io_list.append((targ, conn.to_item.name))
                else:
                    LOGGER.warning('Cutout tile connected to non-cutout!')
                conn.remove()  # Delete the connection.
        else:
            # 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))

        # Remove all traces of this item (other than in connections lists).
        inst.remove()
        del connections.ITEMS[targ]

    for start_floor, end_floor in FLOOR_IO:
        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(
                vmf,
                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(vmf,
                            box_min + (-64, -64, 0),
                            box_max + (64, 64, -8),
                            skin=SETTINGS['beam_skin'])

        # Add a player_clip brush across the whole area
        vmf.add_brush(
            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 = 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 = 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(
                vmf,
                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 boundaries near tiles, then generate them.

    # Do it separately for each z-level:
    for z, xy_dict in floor_neighbours.items():
        for x, y in xy_dict:
            # 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 = vmf.create_ent(classname='func_detail', )

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

    add_floor_sides(vmf, floor_edges)

    return conditions.RES_EXHAUSTED
예제 #15
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)

    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'] = 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,
            generator=GenCat.PANEL,
        )

    # Associate any set panel with the same entity, if it's present.
    tile_pos = Vec(z=-128)
    tile_pos.localise(origin, angles)
    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'] = local_name(inst, source_ent)
예제 #16
0
def res_signage(vmf: VMF, inst: Entity, res: Property):
    """Implement the Signage item."""
    sign: Optional[Sign]
    try:
        sign = (
            CONN_SIGNAGES if
            res.bool('connection')
            else SIGNAGES
        )[inst.fixup[consts.FixupVars.TIM_DELAY]]
    except KeyError:
        # Blank sign
        sign = None

    has_arrow = inst.fixup.bool(consts.FixupVars.ST_ENABLED)

    sign_prim: Optional[Sign]
    sign_sec: Optional[Sign]

    if has_arrow:
        sign_prim = sign
        sign_sec = SIGNAGES['arrow']
    elif sign is not None:
        sign_prim = sign.primary or sign
        sign_sec = sign.secondary or None
    else:
        # Neither sign or arrow, delete this.
        inst.remove()
        return

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

    normal = Vec(z=-1).rotate(*angles)
    forward = Vec(x=-1).rotate(*angles)

    prim_pos = Vec(0, -16, -64)
    sec_pos = Vec(0, 16, -64)

    prim_pos.localise(origin, angles)
    sec_pos.localise(origin, angles)

    template_id = res['template_id', '']

    if inst.fixup.bool(consts.FixupVars.ST_REVERSED):
        # Flip around.
        forward = -forward
        prim_visgroup = 'secondary'
        sec_visgroup = 'primary'
        prim_pos, sec_pos = sec_pos, prim_pos
    else:
        prim_visgroup = 'primary'
        sec_visgroup = 'secondary'

    if sign_prim and sign_sec:
        inst['file'] = res['large_clip', '']
        inst['origin'] = (prim_pos + sec_pos) / 2
    else:
        inst['file'] = res['small_clip', '']
        inst['origin'] = prim_pos if sign_prim else sec_pos

    brush_faces: List[Side] = []
    tiledef: Optional[tiling.TileDef] = None

    if template_id:
        if sign_prim and sign_sec:
            visgroup = [prim_visgroup, sec_visgroup]
        elif sign_prim:
            visgroup = [prim_visgroup]
        else:
            visgroup = [sec_visgroup]
        template = template_brush.import_template(
            vmf,
            template_id,
            origin,
            angles,
            force_type=template_brush.TEMP_TYPES.detail,
            additional_visgroups=visgroup,
        )

        for face in template.detail.sides():
            if face.normal() == normal:
                brush_faces.append(face)
    else:
        # Direct on the surface.
        # Find the grid pos first.
        grid_pos = (origin // 128) * 128 + 64
        try:
            tiledef = tiling.TILES[(grid_pos + 128 * normal).as_tuple(), (-normal).as_tuple()]
        except KeyError:
            LOGGER.warning(
                "Can't place signage at ({}) in ({}) direction!",
                origin,
                normal,
                exc_info=True,
            )
            return

    if sign_prim is not None:
        over = place_sign(
            vmf,
            brush_faces,
            sign_prim,
            prim_pos,
            normal,
            forward,
            rotate=True,
        )

        if tiledef is not None:
            tiledef.bind_overlay(over)

    if sign_sec is not None:
        if has_arrow and res.bool('arrowDown'):
            # Arrow texture points down, need to flip it.
            forward = -forward

        over = place_sign(
            vmf,
            brush_faces,
            sign_sec,
            sec_pos,
            normal,
            forward,
            rotate=not has_arrow,
        )

        if tiledef is not None:
            tiledef.bind_overlay(over)
예제 #17
0
def res_water_splash(vmf: VMF, inst: Entity, res: Property):
    """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, str, str

    pos1 = pos1.copy()  # type: Vec
    splash_pos = pos1.copy()  # type: Vec

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

    for pos in check_pos:
        grid_pos = pos // 128 * 128  # type: Vec
        grid_pos += (64, 64, 64)
        try:
            surf = conditions.GOO_LOCS[grid_pos.as_tuple()]
        except KeyError:
            continue
        break
    else:
        return  # Not in goo at all

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

    water_pos = surf.get_origin()

    # Check if both positions are above or below the water..
    # that means it won't ever trigger.
    LOGGER.info('pos1: {}, pos2: {}, water_pos: {}', pos1.z, pos2.z, water_pos.z)
    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.
    )
예제 #18
0
    def read_from_map(self, vmf: VMF, has_attr: Dict[str, bool], items: Dict[str, editoritems.Item]) -> None:
        """Given the map file, set blocks."""
        from precomp.instance_traits import get_item_id
        from precomp import bottomlessPit

        # 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:
                    item = items[item_id]
                except KeyError:
                    continue
                angles = Vec.from_str(ent['angles'])
                for local_pos in item.embed_voxels:
                    world_pos = Vec(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!')
예제 #19
0
def res_cutout_tile(vmf: srctools.VMF, 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.
    """
    marker_filenames = 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_locs = set()
    # If any signage is present in the map, we need to force tiles to
    # appear at that location!
    for over in 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')
                ):
            add_signage_loc(sign_locs, Vec.from_str(over['origin']))

    for item in connections.ITEMS.values():
        for ind_pan in item.ind_panels:
            loc = Vec(0, 0, -64)
            loc.localise(
                Vec.from_str(ind_pan['origin']),
                Vec.from_str(ind_pan['angles']),
            )
            add_signage_loc(sign_locs, loc)

    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.find_key('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 vmf.by_class['func_instance']:
        if inst['file'].casefold() not in marker_filenames:
            continue
        targ = inst['targetname']
        normal = 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 normal == (0, 0, 1):
            io_list = FLOOR_IO
        else:
            io_list = CEIL_IO

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

        item = connections.ITEMS[targ]
        item.delete_antlines()

        if item.outputs:
            for conn in list(item.outputs):
                if conn.to_item.inst['file'].casefold() in marker_filenames:
                    io_list.append((targ, conn.to_item.name))
                else:
                    LOGGER.warning('Cutout tile connected to non-cutout!')
                conn.remove()  # Delete the connection.
        else:
            # 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))

        # Remove all traces of this item (other than in connections lists).
        inst.remove()
        del connections.ITEMS[targ]

    for start_floor, end_floor in FLOOR_IO:
        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(
                vmf,
                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(
                vmf,
                box_min + (-64, -64, 0),
                box_max + (64, 64, -8),
                skin=SETTINGS['beam_skin']
            )

        # Add a player_clip brush across the whole area
        vmf.add_brush(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 = 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 = 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(
                vmf,
                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 boundaries near tiles, then generate them.

    # Do it separately 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 = vmf.create_ent(
            classname='func_detail',
        )

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

    add_floor_sides(vmf, floor_edges)

    conditions.reallocate_overlays(overlay_ids)

    return conditions.RES_EXHAUSTED
def res_water_splash(inst: Entity, res: Property):
    """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, str, str

    pos1 = pos1.copy()  # type: Vec
    splash_pos = pos1.copy()  # type: Vec

    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)

    conditions.VMF.create_ent(
        classname='env_beam',
        targetname=conditions.local_name(inst, name + '_pos'),
        origin=str(pos1),
        targetpoint=str(pos2),
    )

    # 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))

    for pos in check_pos:
        grid_pos = pos // 128 * 128  # type: Vec
        grid_pos += (64, 64, 64)
        try:
            surf = conditions.GOO_LOCS[grid_pos.as_tuple()]
        except KeyError:
            continue
        break
    else:
        return  # Not in goo at all

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

    water_pos = surf.get_origin()

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

    import vbsp

    # 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',
    )

    conditions.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.
    )

    vbsp.PACK_FILES.add('scripts/vscripts/BEE2/water_splash.nut')