Example #1
0
def res_cust_antline_setup(res: Property):
    def find(cat):
        """Helper to reduce code duplication."""
        return [p.value for p in res.find_all(cat)]

    # Allow overriding these options. If unset use the style's value - the
    # amount of destruction will usually be the same.
    broken_chance = res.float(
        'broken_antline_chance',
        vbsp_options.get(float, 'broken_antline_chance'),
    )
    broken_dist = res.int(
        'broken_antline_distance',
        vbsp_options.get(int, 'broken_antline_distance'),
    )

    toggle_inst = res['instance', '']
    toggle_out = list(res.find_all('addOut'))

    # These textures are required - the base ones.
    straight_tex = find('straight')
    corner_tex = find('corner')

    # Arguments to pass to setAntlineMat
    straight_args = [
        straight_tex,
        find('straightFloor') or (),
        # Extra broken antline textures / options, if desired.
        broken_chance,
        broken_dist,
        find('brokenStraight') or (),
        find('brokenStraightFloor') or (),
    ]

    # The same but for corners.
    corner_args = [
        corner_tex,
        find('cornerFloor') or (),
        broken_chance,
        broken_dist,
        find('brokenCorner') or (),
        find('brokenCornerFloor') or (),
    ]

    if not straight_tex or not corner_tex:
        # If we don't have two textures, something's wrong. Remove this result.
        LOGGER.warning('custAntline has no textures!')
        return None
    else:
        return straight_args, corner_args, toggle_inst, toggle_out
Example #2
0
def res_make_tag_fizzler_setup(res: Property):
    """We need this to pre-parse the fizzler type."""
    if 'ioconf' in res:
        fizz_conn = Config.parse('<TAG_FIZZER>', res.find_key('ioconf'))
    else:
        fizz_conn = None

    # The distance from origin the double signs are seperated by.
    sign_offset = res.int('signoffset', 16)

    return (
        sign_offset,
        fizz_conn,
        res['frame_double'],
        res['frame_single'],
        res['blue_sign', ''],
        res['blue_off_sign', ''],
        res['oran_sign', ''],
        res['oran_off_sign', ''],
    )
Example #3
0
def res_make_tag_fizzler_setup(res: Property):
    """We need this to pre-parse the fizzler type."""
    if 'ioconf' in res:
        fizz_type = ItemType.parse('<TAG_FIZZER>', res.find_key('ioconf'))
    else:
        fizz_type = None

    # The distance from origin the double signs are seperated by.
    sign_offset = res.int('signoffset', 16)

    return (
        sign_offset,
        fizz_type,
        res['frame_double'],
        res['frame_single'],
        res['blue_sign', ''],
        res['blue_off_sign', ''],
        res['oran_sign', ''],
        res['oran_off_sign', ''],
    )
Example #4
0
def widget_minute_seconds(parent: tk.Frame, var: tk.StringVar,
                          conf: Property) -> tk.Widget:
    """A widget for specifying times - minutes and seconds.

    The value is saved as seconds.
    Max specifies the largest amount.
    """
    max_value = conf.int('max', 60)
    min_value = conf.int('min', 0)
    if min_value > max_value:
        raise ValueError('Bad min and max values!')

    values = timer_values(min_value, max_value)

    # Stores the 'pretty' value in the actual textbox.
    disp_var = tk.StringVar()

    existing_value = var.get()

    def update_disp(var_name: str, var_index: str, operation: str) -> None:
        """Whenever the string changes, update the displayed text."""
        seconds = conv_int(var.get(), -1)
        if min_value <= seconds <= max_value:
            disp_var.set('{}:{:02}'.format(seconds // 60, seconds % 60))
        else:
            LOGGER.warning('Bad timer value "{}" for "{}"!', var.get(),
                           conf['id'])
            # Recurse, with a known safe value.
            var.set(values[0])

    # Whenever written to, call this.
    var.trace_add('write', update_disp)

    def set_var():
        """Set the variable to the current value."""
        try:
            minutes, seconds = disp_var.get().split(':')
            var.set(str(int(minutes) * 60 + int(seconds)))
        except (ValueError, TypeError):
            pass

    def validate(reason: str, operation_type: str, cur_value: str,
                 new_char: str, new_value: str):
        """Validate the values for the text.

        This is called when the textbox is modified, to allow cancelling bad
        inputs.

        Reason is the reason this was fired: 'key', 'focusin', 'focusout', 'forced'.
        operation_type is '1' for insert, '0' for delete', '-1' for programmatic changes.
        cur_val is the value before the change occurs.
        new_char is the added/removed text.
        new_value is the value after the change, if accepted.
        """
        if operation_type == '0' or reason == 'forced':
            # Deleting or done by the program, allow that always.
            return True

        if operation_type == '1':  # Inserted text.
            # Disallow non number and colons
            if new_char not in '0123456789:':
                return False
            # Only one colon.
            if ':' in cur_value and new_char == ':':
                return False

            # Don't allow more values if it has more than 2 numbers after
            # the colon - if there is one, and it's not in the last 3 characters.
            if ':' in new_value and ':' not in new_value[-3:]:
                return False

        if reason == 'focusout':
            # When leaving focus, apply range limits and set the var.
            try:
                str_min, str_sec = new_value.split(':')
                seconds = int(str_min) * 60 + int(str_sec)
            except (ValueError, TypeError):
                seconds = min_value
            else:
                if seconds < min_value:
                    seconds = min_value
                if seconds > max_value:
                    seconds = max_value
            var.set(str(seconds))  # This then re-writes the textbox.
        return True

    validate_cmd = parent.register(validate)

    spinbox = tk.Spinbox(
        parent,
        exportselection=False,
        textvariable=disp_var,
        command=set_var,
        wrap=True,
        values=values,
        width=5,
        validate='all',
        # These define which of the possible values will be passed along.
        # http://tcl.tk/man/tcl8.6/TkCmd/spinbox.htm#M26
        validatecommand=(validate_cmd, '%V', '%d', '%s', '%S', '%P'),
    )
    # We need to set this after, it gets reset to the first one.
    var.set(existing_value)
    return spinbox
Example #5
0
def res_make_tag_fizzler(vmf: VMF, inst: Entity, res: Property):
    """Add an Aperture Tag Paint Gun activation fizzler.

    These fizzlers are created via signs, and work very specially.
    MUST be priority -100 so it runs before fizzlers!
    """
    import vbsp
    if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']:
        # Abort - TAG fizzlers shouldn't appear in any other game!
        inst.remove()
        return

    fizz_base = fizz_name = None

    # Look for the fizzler instance we want to replace
    for targetname in inst.output_targets():
        if targetname in tag_fizzlers:
            fizz_name = targetname
            fizz_base = tag_fizzlers[targetname]
            del tag_fizzlers[targetname]  # Don't let other signs mod this one!
            continue
        else:
            # It's an indicator toggle, remove it and the antline to clean up.
            LOGGER.warning('Toggle: {}', targetname)
            for ent in vmf.by_target[targetname]:
                remove_ant_toggle(ent)
    inst.outputs.clear()  # Remove the outptuts now, they're not valid anyway.

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

    # The distance from origin the double signs are seperated by.
    sign_offset = res.int('signoffset', 16)

    sign_loc = (
        # The actual location of the sign - on the wall
        Vec.from_str(inst['origin']) +
        Vec(0, 0, -64).rotate_by_str(inst['angles'])
    )

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

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

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

    inst_angle = srctools.parse_vec_str(inst['angles'])

    inst_normal = Vec(0, 0, 1).rotate(*inst_angle)
    loc = Vec.from_str(inst['origin'])

    if disable_other or (blue_enabled and oran_enabled):
        inst['file'] = res['frame_double']
        # On a wall, and pointing vertically
        if inst_normal.z == 0 and Vec(y=1).rotate(*inst_angle).z:
            # They're vertical, make sure blue's on top!
            blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset)
            oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset)
            # If orange is enabled, with two frames put that on top
            # instead since it's more important
            if disable_other and oran_enabled:
                blue_loc, oran_loc = oran_loc, blue_loc

        else:
            offset = Vec(0, sign_offset, 0).rotate(*inst_angle)
            blue_loc = loc + offset
            oran_loc = loc - offset
    else:
        inst['file'] = res['frame_single']
        # They're always centered
        blue_loc = loc
        oran_loc = loc

    if inst_normal.z != 0:
        # If on floors/ceilings, rotate to point at the fizzler!
        sign_floor_loc = sign_loc.copy()
        sign_floor_loc.z = 0  # We don't care about z-positions.

        # Grab the data saved earlier in res_find_potential_tag_fizzlers()
        axis, side_min, side_max, normal = tag_fizzler_locs[fizz_name]

        # The Z-axis fizzler (horizontal) must be treated differently.
        if axis == 'z':
            # For z-axis, just compare to the center point.
            # The values are really x, y, z, not what they're named.
            sign_dir = sign_floor_loc - (side_min, side_max, normal)
        else:
            # For the other two, we compare to the line,
            # or compare to the closest side (in line with the fizz)
            other_axis = 'x' if axis == 'y' else 'y'
            if abs(sign_floor_loc[other_axis] - normal) < 32:
                # Compare to the closest side. Use ** to swap x/y arguments
                # appropriately. The closest side is the one with the
                # smallest magnitude.
                vmf.create_ent(
                    classname='info_null',
                    targetname=inst['targetname'] + '_min',
                    origin=sign_floor_loc - Vec(**{
                        axis: side_min,
                        other_axis: normal,
                    }),
                )
                vmf.create_ent(
                    classname='info_null',
                    targetname=inst['targetname'] + '_max',
                    origin=sign_floor_loc - Vec(**{
                        axis: side_max,
                        other_axis: normal,
                    }),
                )
                sign_dir = min(
                    sign_floor_loc - Vec(**{
                        axis: side_min,
                        other_axis: normal,
                    }),
                    sign_floor_loc - Vec(**{
                        axis: side_max,
                        other_axis: normal,
                    }),
                    key=Vec.mag,
                )
            else:
                # Align just based on whether we're in front or behind.
                sign_dir = Vec()
                sign_dir[other_axis] = sign_floor_loc[other_axis] - normal

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

        # Rotate to fit the instances - south is down
        sign_angle = int(sign_angle + 90) % 360
        if inst_normal.z > 0:
            sign_angle = '0 {} 0'.format(sign_angle)
        elif inst_normal.z < 0:
            # Flip upside-down for ceilings
            sign_angle = '0 {} 180'.format(sign_angle)
    else:
        # On a wall, face upright
        sign_angle = PETI_INST_ANGLE[inst_normal.as_tuple()]

    # If disable_other, we show off signs. Otherwise we don't use that sign.
    blue_sign = 'blue_sign' if blue_enabled else 'blue_off_sign' if disable_other else None
    oran_sign = 'oran_sign' if oran_enabled else 'oran_off_sign' if disable_other else None

    if blue_sign:
        vmf.create_ent(
            classname='func_instance',
            file=res[blue_sign, ''],
            targetname=inst['targetname'],
            angles=sign_angle,
            origin=blue_loc.join(' '),
        )

    if oran_sign:
        vmf.create_ent(
            classname='func_instance',
            file=res[oran_sign, ''],
            targetname=inst['targetname'],
            angles=sign_angle,
            origin=oran_loc.join(' '),
        )

    # Now modify the fizzler...

    fizz_brushes = list(
        vmf.by_class['trigger_portal_cleanser'] &
        vmf.by_target[fizz_name + '_brush']
    )

    if 'base_inst' in res:
        fizz_base['file'] = resolve_inst(res['base_inst'])[0]
    fizz_base.outputs.clear()  # Remove outputs, otherwise they break
    # branch_toggle entities

    # Subtract the sign from the list of connections, but don't go below
    # zero
    fizz_base.fixup['$connectioncount'] = str(max(
        0,
        srctools.conv_int(fizz_base.fixup['$connectioncount', ''], 0) - 1
    ))

    if 'model_inst' in res:
        model_inst = resolve_inst(res['model_inst'])[0]
        for mdl_inst in vmf.by_class['func_instance']:
            if mdl_inst['targetname', ''].startswith(fizz_name + '_model'):
                mdl_inst['file'] = model_inst

    # Find the direction the fizzler front/back points - z=floor fizz
    # Signs will associate with the given side!
    bbox_min, bbox_max = fizz_brushes[0].get_bbox()
    for axis, val in zip('xyz', bbox_max-bbox_min):
        if val == 2:
            fizz_axis = axis
            sign_center = (bbox_min[axis] + bbox_max[axis]) / 2
            break
    else:
        # A fizzler that's not 128*x*2?
        raise Exception('Invalid fizzler brush ({})!'.format(fizz_name))

    # Figure out what the sides will set values to...
    pos_blue = False
    pos_oran = False
    neg_blue = False
    neg_oran = False
    if sign_loc[fizz_axis] < sign_center:
        pos_blue = blue_enabled
        pos_oran = oran_enabled
    else:
        neg_blue = blue_enabled
        neg_oran = oran_enabled

    fizz_off_tex = {
        'left': res['off_left'],
        'center': res['off_center'],
        'right': res['off_right'],
        'short': res['off_short'],
    }
    fizz_on_tex = {
        'left': res['on_left'],
        'center': res['on_center'],
        'right': res['on_right'],
        'short': res['on_short'],
    }

    # If it activates the paint gun, use different textures
    if pos_blue or pos_oran:
        pos_tex = fizz_on_tex
    else:
        pos_tex = fizz_off_tex

    if neg_blue or neg_oran:
        neg_tex = fizz_on_tex
    else:
        neg_tex = fizz_off_tex

    if vbsp.GAME_MODE == 'COOP':
        # We need ATLAS-specific triggers
        pos_trig = vmf.create_ent(
            classname='trigger_playerteam',
        )
        neg_trig = vmf.create_ent(
            classname='trigger_playerteam',
        )
        output = 'OnStartTouchBluePlayer'
    else:
        pos_trig = vmf.create_ent(
            classname='trigger_multiple',
        )
        neg_trig = vmf.create_ent(
            classname='trigger_multiple',
            spawnflags='1',
        )
        output = 'OnStartTouch'

    pos_trig['origin'] = neg_trig['origin'] = fizz_base['origin']
    pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1'  # Clients Only

    pos_trig['targetname'] = fizz_name + '-trig_pos'
    neg_trig['targetname'] = fizz_name + '-trig_neg'

    pos_trig.outputs = [
        Output(
            output,
            fizz_name + '-trig_neg',
            'Enable',
        ),
        Output(
            output,
            fizz_name + '-trig_pos',
            'Disable',
        ),
    ]

    neg_trig.outputs = [
        Output(
            output,
            fizz_name + '-trig_pos',
            'Enable',
        ),
        Output(
            output,
            fizz_name + '-trig_neg',
            'Disable',
        ),
    ]

    voice_attr = vbsp.settings['has_attr']

    if blue_enabled or disable_other:
        # If this is blue/oran only, don't affect the other color
        neg_trig.outputs.append(Output(
            output,
            '@BlueIsEnabled',
            'SetValue',
            param=srctools.bool_as_int(neg_blue),
        ))
        pos_trig.outputs.append(Output(
            output,
            '@BlueIsEnabled',
            'SetValue',
            param=srctools.bool_as_int(pos_blue),
        ))
        if blue_enabled:
            # Add voice attributes - we have the gun and gel!
            voice_attr['bluegelgun'] = True
            voice_attr['bluegel'] = True
            voice_attr['bouncegun'] = True
            voice_attr['bouncegel'] = True

    if oran_enabled or disable_other:
        neg_trig.outputs.append(Output(
            output,
            '@OrangeIsEnabled',
            'SetValue',
            param=srctools.bool_as_int(neg_oran),
        ))
        pos_trig.outputs.append(Output(
            output,
            '@OrangeIsEnabled',
            'SetValue',
            param=srctools.bool_as_int(pos_oran),
        ))
        if oran_enabled:
            voice_attr['orangegelgun'] = True
            voice_attr['orangegel'] = True
            voice_attr['speedgelgun'] = True
            voice_attr['speedgel'] = True

    if not oran_enabled and not blue_enabled:
        # If both are disabled, we must shutdown the gun when touching
        # either side - use neg_trig for that purpose!
        # We want to get rid of pos_trig to save ents
        vmf.remove_ent(pos_trig)
        neg_trig['targetname'] = fizz_name + '-trig'
        neg_trig.outputs.clear()
        neg_trig.add_out(Output(
            output,
            '@BlueIsEnabled',
            'SetValue',
            param='0'
        ))
        neg_trig.add_out(Output(
            output,
            '@OrangeIsEnabled',
            'SetValue',
            param='0'
        ))

    for fizz_brush in fizz_brushes:  # portal_cleanser ent, not solid!
        # Modify fizzler textures
        bbox_min, bbox_max = fizz_brush.get_bbox()
        for side in fizz_brush.sides():
            norm = side.normal()
            if norm[fizz_axis] == 0:
                # Not the front/back: force nodraw
                # Otherwise the top/bottom will have the odd stripes
                # which won't match the sides
                side.mat = 'tools/toolsnodraw'
                continue
            if norm[fizz_axis] == 1:
                side.mat = pos_tex[
                    vbsp.TEX_FIZZLER[
                        side.mat.casefold()
                    ]
                ]
            else:
                side.mat = neg_tex[
                    vbsp.TEX_FIZZLER[
                        side.mat.casefold()
                    ]
                ]
        # The fizzler shouldn't kill cubes
        fizz_brush['spawnflags'] = '1'

        fizz_brush.outputs.append(Output(
            'OnStartTouch',
            '@shake_global',
            'StartShake',
        ))

        fizz_brush.outputs.append(Output(
            'OnStartTouch',
            '@shake_global_sound',
            'PlaySound',
        ))

        # The triggers are 8 units thick, 24 from the center
        # (-1 because fizzlers are 2 thick on each side).
        neg_min, neg_max = Vec(bbox_min), Vec(bbox_max)
        neg_min[fizz_axis] -= 23
        neg_max[fizz_axis] -= 17

        pos_min, pos_max = Vec(bbox_min), Vec(bbox_max)
        pos_min[fizz_axis] += 17
        pos_max[fizz_axis] += 23

        if blue_enabled or oran_enabled:
            neg_trig.solids.append(
                vmf.make_prism(
                    neg_min,
                    neg_max,
                    mat='tools/toolstrigger',
                ).solid,
            )
            pos_trig.solids.append(
                vmf.make_prism(
                    pos_min,
                    pos_max,
                    mat='tools/toolstrigger',
                ).solid,
            )
        else:
            # If neither enabled, use one trigger
            neg_trig.solids.append(
                vmf.make_prism(
                    neg_min,
                    pos_max,
                    mat='tools/toolstrigger',
                ).solid,
            )
Example #6
0
def widget_minute_seconds(parent: tk.Frame, var: tk.StringVar,
                          conf: Property) -> tk.Misc:
    """A widget for specifying times - minutes and seconds.

    The value is saved as seconds.
    Max specifies the largest amount.
    """
    max_value = conf.int('max', 60)
    min_value = conf.int('min', 0)
    if min_value > max_value:
        raise ValueError('Bad min and max values!')

    values = timer_values(min_value, max_value)

    default_value = conv_int(var.get(), -1)
    if min_value <= default_value <= max_value:
        default_text = values[default_value - min_value]
    else:
        LOGGER.warning('Bad timer value "{}" for "{}"!', var.get(), conf['id'])
        default_text = '0:01'
        var.set('1')

    disp_var = tk.StringVar()

    def set_var():
        """Set the variable to the current value."""
        try:
            minutes, seconds = disp_var.get().split(':')
            var.set(int(minutes) * 60 + int(seconds))
        except (ValueError, TypeError):
            pass

    def validate(reason: str, operation_type: str, cur_value: str,
                 new_char: str, new_value: str):
        """Validate the values for the text."""
        if operation_type == '0' or reason == 'forced':
            # Deleting or done by the program, allow that always.
            return True

        if operation_type == '1':
            # Disallow non number and colons
            if new_char not in '0123456789:':
                return False
            # Only one colon.
            if ':' in cur_value and new_char == ':':
                return False

            # Don't allow more values if it has more than 2 numbers after
            # the colon - if there is one, and it's not in the last 3 characters.
            if ':' in new_value and ':' not in new_value[-3:]:
                return False

        if reason == 'focusout':
            # When leaving focus, apply range limits and set the var.
            try:
                minutes, seconds = new_value.split(':')
                seconds = int(minutes) * 60 + int(seconds)
            except (ValueError, TypeError):
                seconds = default_value
            if seconds < min_value:
                seconds = min_value
            if seconds > max_value:
                seconds = max_value
            disp_var.set('{}:{:02}'.format(seconds // 60, seconds % 60))
            var.set(seconds)
        return True

    validate_cmd = parent.register(validate)

    spinbox = tk.Spinbox(
        parent,
        exportselection=False,
        textvariable=disp_var,
        command=set_var,
        wrap=True,
        values=values,
        validate='all',
        # %args substitute the values for the args to validate_cmd.
        validatecommand=(validate_cmd, '%V', '%d', '%s', '%S', '%P'),
    )
    # We need to set this after, it gets reset to the first one.
    disp_var.set(default_text)
    return spinbox
Example #7
0
def res_goo_debris(res: Property):
    """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.
    """
    import brushLoc

    space = res.int('spacing', 1)
    rand_count = res.int('number', None)
    if rand_count:
        rand_list = weighted_random(
            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),
    )

    suff = ''
    for loc in possible_locs:
        random.seed('goo_debris_{}_{}_{}'.format(loc.x, loc.y, loc.z))
        if random.random() > chance:
            continue

        if rand_list is not None:
            suff = '_' + str(random.choice(rand_list) + 1)

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

    return RES_EXHAUSTED
Example #8
0
def res_make_tag_fizzler(vmf: VMF, res: Property):
    """Add an Aperture Tag Paint Gun activation fizzler.

    These fizzlers are created via signs, and work very specially.
    This must be before -250 so it runs before fizzlers and connections.
    """
    if 'ioconf' in res:
        fizz_conn_conf = Config.parse('<TAG_FIZZER>', res.find_key('ioconf'))
    else:
        fizz_conn_conf = None

    # The distance from origin the double signs are seperated by.
    sign_offset = res.int('signoffset', 16)

    inst_frame_double = res['frame_double']
    inst_frame_single = res['frame_single']
    blue_sign_on = res['blue_sign', '']
    blue_sign_off = res['blue_off_sign', '']
    oran_sign_on = res['oran_sign', '']
    oran_sign_off = res['oran_off_sign', '']

    import vbsp
    if options.get(str, 'game_id') != utils.STEAM_IDS['TAG']:
        # Abort - TAG fizzlers shouldn't appear in any other game!
        # So simply remove the fizzler.
        return Entity.remove

    def make_tag_fizz(inst: Entity) -> None:
        """Create the Tag fizzler."""
        fizzler: Optional[Fizzler] = None
        fizzler_item: Optional[Item] = None

        # Look for the fizzler instance we want to replace.
        sign_item = connections.ITEMS[inst['targetname']]
        for conn in list(sign_item.outputs):
            if conn.to_item.name in FIZZLERS:
                if fizzler is None:
                    fizzler = FIZZLERS[conn.to_item.name]
                    fizzler_item = conn.to_item
                else:
                    raise ValueError('Multiple fizzlers attached to a sign!')

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

        sign_item.delete_antlines()

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

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

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

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

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

        # The actual location of the sign - on the wall
        sign_loc = Vec.from_str(inst['origin']) + Vec(0, 0, -64) @ inst_orient
        fizz_norm_axis = round(fizzler.normal(), 3).axis()

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

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

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

        inst_normal = inst_orient.up()
        loc = Vec.from_str(inst['origin'])

        if disable_other or (blue_enabled and oran_enabled):
            inst['file'] = inst_frame_double
            conditions.ALL_INST.add(inst_frame_double.casefold())
            # On a wall, and pointing vertically
            if abs(inst_normal.z) < 0.01 and abs(inst_orient.left().z) > 0.01:
                # They're vertical, make sure blue's on top!
                blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset)
                oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset)
                # If orange is enabled, with two frames put that on top
                # instead since it's more important
                if disable_other and oran_enabled:
                    blue_loc, oran_loc = oran_loc, blue_loc
            else:
                offset = Vec(0, sign_offset, 0) @ inst_orient
                blue_loc = loc + offset
                oran_loc = loc - offset
        else:
            inst['file'] = inst_frame_single
            conditions.ALL_INST.add(inst_frame_single.casefold())
            # They're always centered
            blue_loc = loc
            oran_loc = loc

        if inst_normal.z != 0:
            # If on floors/ceilings, rotate to point at the fizzler!
            sign_floor_loc = sign_loc.copy()
            sign_floor_loc.z = 0  # We don't care about z-positions.

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

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

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

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

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

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

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

        # If disable_other, we show off signs. Otherwise we don't use that sign.
        blue_sign = blue_sign_on if blue_enabled else blue_sign_off if disable_other else None
        oran_sign = oran_sign_on if oran_enabled else oran_sign_off if disable_other else None

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

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

        # Now modify the fizzler...

        # Subtract the sign from the list of connections, but don't go below
        # zero
        fizzler.base_inst.fixup['$connectioncount'] = max(
            0,
            fizzler.base_inst.fixup.int('$connectioncount') - 1)

        # Find the direction the fizzler normal is.
        # Signs will associate with the given side!

        bbox_min, bbox_max = fizzler.emitters[0]

        sign_center = (bbox_min[fizz_norm_axis] + bbox_max[fizz_norm_axis]) / 2

        # Figure out what the sides will set values to...
        pos_blue = pos_oran = False
        neg_blue = neg_oran = False

        if sign_loc[fizz_norm_axis] < sign_center:
            pos_blue = blue_enabled
            pos_oran = oran_enabled
        else:
            neg_blue = blue_enabled
            neg_oran = oran_enabled

        # If it activates the paint gun, use different textures
        fizzler.tag_on_pos = pos_blue or pos_oran
        fizzler.tag_on_neg = neg_blue or neg_oran

        # Now make the trigger ents. We special-case these since they need to
        # swap
        # depending on the sign config and position.

        if vbsp.GAME_MODE == 'COOP':
            # We need ATLAS-specific triggers.
            pos_trig = vmf.create_ent(classname='trigger_playerteam')
            neg_trig = vmf.create_ent(classname='trigger_playerteam')
            output = 'OnStartTouchBluePlayer'
        else:
            pos_trig = vmf.create_ent(classname='trigger_multiple')
            neg_trig = vmf.create_ent(classname='trigger_multiple')
            output = 'OnStartTouch'

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

        pos_trig['targetname'] = conditions.local_name(fizzler.base_inst,
                                                       'trig_pos')
        neg_trig['targetname'] = conditions.local_name(fizzler.base_inst,
                                                       'trig_neg')

        pos_trig['startdisabled'] = neg_trig['startdisabled'] = (
            not fizzler.base_inst.fixup.bool('start_enabled'))

        pos_trig.outputs = [
            Output(output, neg_trig, 'Enable'),
            Output(output, pos_trig, 'Disable'),
        ]

        neg_trig.outputs = [
            Output(output, pos_trig, 'Enable'),
            Output(output, neg_trig, 'Disable'),
        ]

        voice_attr = vbsp.settings['has_attr']

        if blue_enabled or disable_other:
            # If this is blue/oran only, don't affect the other color
            neg_trig.outputs.append(
                Output(
                    output,
                    '@BlueIsEnabled',
                    'SetValue',
                    neg_blue,
                ))
            pos_trig.outputs.append(
                Output(
                    output,
                    '@BlueIsEnabled',
                    'SetValue',
                    pos_blue,
                ))
            if blue_enabled:
                # Add voice attributes - we have the gun and gel!
                voice_attr['bluegelgun'] = True
                voice_attr['bluegel'] = True
                voice_attr['bouncegun'] = True
                voice_attr['bouncegel'] = True

        if oran_enabled or disable_other:
            neg_trig.outputs.append(
                Output(
                    output,
                    '@OrangeIsEnabled',
                    'SetValue',
                    param=srctools.bool_as_int(neg_oran),
                ))
            pos_trig.outputs.append(
                Output(
                    output,
                    '@OrangeIsEnabled',
                    'SetValue',
                    param=srctools.bool_as_int(pos_oran),
                ))
            if oran_enabled:
                voice_attr['orangegelgun'] = True
                voice_attr['orangegel'] = True
                voice_attr['speedgelgun'] = True
                voice_attr['speedgel'] = True

        if not oran_enabled and not blue_enabled:
            # If both are disabled, we must shutdown the gun when touching
            # either side - use neg_trig for that purpose!
            # We want to get rid of pos_trig to save ents
            vmf.remove_ent(pos_trig)
            neg_trig['targetname'] = conditions.local_name(
                fizzler.base_inst, 'trig_off')
            neg_trig.outputs.clear()
            neg_trig.add_out(
                Output(output, '@BlueIsEnabled', 'SetValue', param='0'))
            neg_trig.add_out(
                Output(output, '@OrangeIsEnabled', 'SetValue', param='0'))

        # Make the triggers.
        for bbox_min, bbox_max in fizzler.emitters:
            bbox_min = bbox_min.copy() - 64 * fizzler.up_axis
            bbox_max = bbox_max.copy() + 64 * fizzler.up_axis

            # The triggers are 8 units thick, with a 32-unit gap in the middle
            neg_min, neg_max = Vec(bbox_min), Vec(bbox_max)
            neg_min[fizz_norm_axis] -= 24
            neg_max[fizz_norm_axis] -= 16

            pos_min, pos_max = Vec(bbox_min), Vec(bbox_max)
            pos_min[fizz_norm_axis] += 16
            pos_max[fizz_norm_axis] += 24

            if blue_enabled or oran_enabled:
                neg_trig.solids.append(
                    vmf.make_prism(
                        neg_min,
                        neg_max,
                        mat='tools/toolstrigger',
                    ).solid, )
                pos_trig.solids.append(
                    vmf.make_prism(
                        pos_min,
                        pos_max,
                        mat='tools/toolstrigger',
                    ).solid, )
            else:
                # If neither enabled, use one trigger
                neg_trig.solids.append(
                    vmf.make_prism(
                        neg_min,
                        pos_max,
                        mat='tools/toolstrigger',
                    ).solid, )

    return make_tag_fizz
Example #9
0
def res_add_overlay_inst(vmf: VMF, inst: Entity, res: Property) -> Optional[Entity]:
    """Add another instance on top of this one.

    If a single value, this sets only the filename.
    Values:

    - `file`: The filename.
    - `fixup_style`: The Fixup style for the instance. '0' (default) is
            Prefix, '1' is Suffix, and '2' is None.
    - `copy_fixup`: If true, all the `$replace` values from the original
            instance will be copied over.
    - `move_outputs`: If true, outputs will be moved to this instance.
    - `offset`: The offset (relative to the base) that the instance
        will be placed. Can be set to `<piston_top>` and
        `<piston_bottom>` to offset based on the configuration.
        `<piston_start>` will set it to the starting position, and
        `<piston_end>` will set it to the ending position of the Piston
        Platform's handles.
    - `rotation`: Rotate the instance by this amount.
    - `angles`: If set, overrides `rotation` and the instance angles entirely.
    - `fixup`/`localfixup`: Keyvalues in this block will be copied to the
            overlay entity.
        - If the value starts with `$`, the variable will be copied over.
        - If this is present, `copy_fixup` will be disabled.
    """

    if not res.has_children():
        # Use all the defaults.
        res = Property('AddOverlay', [
            Property('File', res.value)
        ])

    if 'angles' in res:
        angles = Angle.from_str(res['angles'])
        if 'rotation' in res:
            LOGGER.warning('"angles" option overrides "rotation"!')
    else:
        angles = Angle.from_str(res['rotation', '0 0 0'])
        angles @= Angle.from_str(inst['angles', '0 0 0'])

    orig_name = conditions.resolve_value(inst, res['file', ''])
    filename = instanceLocs.resolve_one(orig_name)

    if not filename:
        if not res.bool('silentLookup'):
            LOGGER.warning('Bad filename for "{}" when adding overlay!', orig_name)
        # Don't bother making a overlay which will be deleted.
        return None

    overlay_inst = conditions.add_inst(
        vmf,
        targetname=inst['targetname', ''],
        file=filename,
        angles=angles,
        origin=inst['origin'],
        fixup_style=res.int('fixup_style'),
    )
    # Don't run if the fixup block exists..
    if srctools.conv_bool(res['copy_fixup', '1']):
        if 'fixup' not in res and 'localfixup' not in res:
            # Copy the fixup values across from the original instance
            for fixup, value in inst.fixup.items():
                overlay_inst.fixup[fixup] = value

    conditions.set_ent_keys(overlay_inst.fixup, inst, res, 'fixup')

    if res.bool('move_outputs', False):
        overlay_inst.outputs = inst.outputs
        inst.outputs = []

    if 'offset' in res:
        overlay_inst['origin'] = conditions.resolve_offset(inst, res['offset'])

    return overlay_inst
Example #10
0
def res_make_tag_fizzler(vmf: VMF, inst: Entity, res: Property):
    """Add an Aperture Tag Paint Gun activation fizzler.

    These fizzlers are created via signs, and work very specially.
    MUST be priority -100 so it runs before fizzlers!
    """
    import vbsp
    if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']:
        # Abort - TAG fizzlers shouldn't appear in any other game!
        inst.remove()
        return

    fizzler = None

    # Look for the fizzler instance we want to replace
    for targetname in inst.output_targets():
        try:
            fizzler = FIZZLERS[targetname]
        except KeyError:
            # Not a fizzler.

            # It's an indicator toggle, remove it and the antline to clean up.
            for ent in vmf.by_target[targetname]:
                remove_ant_toggle(ent)

    inst.outputs.clear()  # Remove the outputs now, they're not valid anyway.

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

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

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

    # The distance from origin the double signs are seperated by.
    sign_offset = res.int('signoffset', 16)

    sign_loc = (
        # The actual location of the sign - on the wall
        Vec.from_str(inst['origin']) +
        Vec(0, 0, -64).rotate_by_str(inst['angles'])
    )

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

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

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

    inst_angle = srctools.parse_vec_str(inst['angles'])

    inst_normal = Vec(0, 0, 1).rotate(*inst_angle)
    loc = Vec.from_str(inst['origin'])

    if disable_other or (blue_enabled and oran_enabled):
        inst['file'] = res['frame_double']
        # On a wall, and pointing vertically
        if inst_normal.z == 0 and Vec(y=1).rotate(*inst_angle).z:
            # They're vertical, make sure blue's on top!
            blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset)
            oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset)
            # If orange is enabled, with two frames put that on top
            # instead since it's more important
            if disable_other and oran_enabled:
                blue_loc, oran_loc = oran_loc, blue_loc

        else:
            offset = Vec(0, sign_offset, 0).rotate(*inst_angle)
            blue_loc = loc + offset
            oran_loc = loc - offset
    else:
        inst['file'] = res['frame_single']
        # They're always centered
        blue_loc = loc
        oran_loc = loc

    if inst_normal.z != 0:
        # If on floors/ceilings, rotate to point at the fizzler!
        sign_floor_loc = sign_loc.copy()
        sign_floor_loc.z = 0  # We don't care about z-positions.

        # Grab the data saved earlier in res_find_potential_tag_fizzlers()
        axis, side_min, side_max, normal = calc_fizzler_orient(fizzler)

        # The Z-axis fizzler (horizontal) must be treated differently.
        if axis == 'z':
            # For z-axis, just compare to the center point.
            # The values are really x, y, z, not what they're named.
            sign_dir = sign_floor_loc - (side_min, side_max, normal)
        else:
            # For the other two, we compare to the line,
            # or compare to the closest side (in line with the fizz)
            other_axis = 'x' if axis == 'y' else 'y'
            if abs(sign_floor_loc[other_axis] - normal) < 32:
                # Compare to the closest side. Use ** to swap x/y arguments
                # appropriately. The closest side is the one with the
                # smallest magnitude.
                sign_dir = min(
                    sign_floor_loc - Vec.with_axes(
                        axis,side_min,
                        other_axis, normal,
                    ),
                    sign_floor_loc - Vec.with_axes(
                        axis, side_max,
                        other_axis, normal,
                    ),
                    key=Vec.mag,
                )
            else:
                # Align just based on whether we're in front or behind.
                sign_dir = Vec()
                sign_dir[other_axis] = sign_floor_loc[other_axis] - normal

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

        # Rotate to fit the instances - south is down
        sign_angle = int(sign_angle + 90) % 360
        if inst_normal.z > 0:
            sign_angle = '0 {} 0'.format(sign_angle)
        elif inst_normal.z < 0:
            # Flip upside-down for ceilings
            sign_angle = '0 {} 180'.format(sign_angle)
    else:
        # On a wall, face upright
        sign_angle = PETI_INST_ANGLE[inst_normal.as_tuple()]

    # If disable_other, we show off signs. Otherwise we don't use that sign.
    blue_sign = 'blue_sign' if blue_enabled else 'blue_off_sign' if disable_other else None
    oran_sign = 'oran_sign' if oran_enabled else 'oran_off_sign' if disable_other else None

    if blue_sign:
        vmf.create_ent(
            classname='func_instance',
            file=res[blue_sign, ''],
            targetname=inst['targetname'],
            angles=sign_angle,
            origin=blue_loc.join(' '),
        )

    if oran_sign:
        vmf.create_ent(
            classname='func_instance',
            file=res[oran_sign, ''],
            targetname=inst['targetname'],
            angles=sign_angle,
            origin=oran_loc.join(' '),
        )

    # Now modify the fizzler...

    # Subtract the sign from the list of connections, but don't go below
    # zero
    fizzler.base_inst.fixup['$connectioncount'] = str(max(
        0,
        srctools.conv_int(fizzler.base_inst.fixup['$connectioncount', ''], 0) - 1
    ))

    # Find the direction the fizzler normal is.
    # Signs will associate with the given side!

    bbox_min, bbox_max = fizzler.emitters[0]
    fizz_field_axis = (bbox_max-bbox_min).norm()
    fizz_norm_axis = fizzler.normal().axis()

    sign_center = (bbox_min[fizz_norm_axis] + bbox_max[fizz_norm_axis]) / 2

    # Figure out what the sides will set values to...
    pos_blue = False
    pos_oran = False
    neg_blue = False
    neg_oran = False

    if sign_loc[fizz_norm_axis] < sign_center:
        pos_blue = blue_enabled
        pos_oran = oran_enabled
    else:
        neg_blue = blue_enabled
        neg_oran = oran_enabled

    # If it activates the paint gun, use different textures
    fizzler.tag_on_pos = pos_blue or pos_oran
    fizzler.tag_on_neg = neg_blue or neg_oran

    # Now make the trigger ents. We special-case these since they need to swap
    # depending on the sign config and position.

    if vbsp.GAME_MODE == 'COOP':
        # We need ATLAS-specific triggers
        pos_trig = vmf.create_ent(
            classname='trigger_playerteam',
        )
        neg_trig = vmf.create_ent(
            classname='trigger_playerteam',
        )
        output = 'OnStartTouchBluePlayer'
    else:
        pos_trig = vmf.create_ent(
            classname='trigger_multiple',
        )
        neg_trig = vmf.create_ent(
            classname='trigger_multiple',
            spawnflags='1',
        )
        output = 'OnStartTouch'

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

    pos_trig['targetname'] = local_name(fizzler.base_inst, 'trig_pos')
    neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_neg')

    pos_trig.outputs = [
        Output(output, neg_trig, 'Enable'),
        Output(output, pos_trig, 'Disable'),
    ]

    neg_trig.outputs = [
        Output(output, pos_trig, 'Enable'),
        Output(output, neg_trig, 'Disable'),
    ]

    voice_attr = vbsp.settings['has_attr']

    if blue_enabled or disable_other:
        # If this is blue/oran only, don't affect the other color
        neg_trig.outputs.append(Output(
            output,
            '@BlueIsEnabled',
            'SetValue',
            param=srctools.bool_as_int(neg_blue),
        ))
        pos_trig.outputs.append(Output(
            output,
            '@BlueIsEnabled',
            'SetValue',
            param=srctools.bool_as_int(pos_blue),
        ))
        if blue_enabled:
            # Add voice attributes - we have the gun and gel!
            voice_attr['bluegelgun'] = True
            voice_attr['bluegel'] = True
            voice_attr['bouncegun'] = True
            voice_attr['bouncegel'] = True

    if oran_enabled or disable_other:
        neg_trig.outputs.append(Output(
            output,
            '@OrangeIsEnabled',
            'SetValue',
            param=srctools.bool_as_int(neg_oran),
        ))
        pos_trig.outputs.append(Output(
            output,
            '@OrangeIsEnabled',
            'SetValue',
            param=srctools.bool_as_int(pos_oran),
        ))
        if oran_enabled:
            voice_attr['orangegelgun'] = True
            voice_attr['orangegel'] = True
            voice_attr['speedgelgun'] = True
            voice_attr['speedgel'] = True

    if not oran_enabled and not blue_enabled:
        # If both are disabled, we must shutdown the gun when touching
        # either side - use neg_trig for that purpose!
        # We want to get rid of pos_trig to save ents
        vmf.remove_ent(pos_trig)
        neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_off')
        neg_trig.outputs.clear()
        neg_trig.add_out(Output(
            output,
            '@BlueIsEnabled',
            'SetValue',
            param='0'
        ))
        neg_trig.add_out(Output(
            output,
            '@OrangeIsEnabled',
            'SetValue',
            param='0'
        ))

    # Make the triggers.
    for bbox_min, bbox_max in fizzler.emitters:
        bbox_min = bbox_min.copy() - 64 * fizzler.up_axis
        bbox_max = bbox_max.copy() + 64 * fizzler.up_axis

        # The triggers are 8 units thick, with a 32-unit gap in the middle
        neg_min, neg_max = Vec(bbox_min), Vec(bbox_max)
        neg_min[fizz_norm_axis] -= 24
        neg_max[fizz_norm_axis] -= 16

        pos_min, pos_max = Vec(bbox_min), Vec(bbox_max)
        pos_min[fizz_norm_axis] += 16
        pos_max[fizz_norm_axis] += 24

        if blue_enabled or oran_enabled:
            neg_trig.solids.append(
                vmf.make_prism(
                    neg_min,
                    neg_max,
                    mat='tools/toolstrigger',
                ).solid,
            )
            pos_trig.solids.append(
                vmf.make_prism(
                    pos_min,
                    pos_max,
                    mat='tools/toolstrigger',
                ).solid,
            )
        else:
            # If neither enabled, use one trigger
            neg_trig.solids.append(
                vmf.make_prism(
                    neg_min,
                    pos_max,
                    mat='tools/toolstrigger',
                ).solid,
            )
Example #11
0
def widget_minute_seconds(parent: tk.Frame, var: tk.StringVar, conf: Property) -> tk.Widget:
    """A widget for specifying times - minutes and seconds.

    The value is saved as seconds.
    Max specifies the largest amount.
    """
    max_value = conf.int('max', 60)
    min_value = conf.int('min', 0)
    if min_value > max_value:
        raise ValueError('Bad min and max values!')

    values = timer_values(min_value, max_value)

    # Stores the 'pretty' value in the actual textbox.
    disp_var = tk.StringVar()

    existing_value = var.get()

    def update_disp(var_name: str, var_index: str, operation: str) -> None:
        """Whenever the string changes, update the displayed text."""
        seconds = conv_int(var.get(), -1)
        if min_value <= seconds <= max_value:
            disp_var.set('{}:{:02}'.format(seconds // 60, seconds % 60))
        else:
            LOGGER.warning('Bad timer value "{}" for "{}"!', var.get(), conf['id'])
            # Recurse, with a known safe value.
            var.set(values[0])

    # Whenever written to, call this.
    var.trace_add('write', update_disp)

    def set_var():
        """Set the variable to the current value."""
        try:
            minutes, seconds = disp_var.get().split(':')
            var.set(str(int(minutes) * 60 + int(seconds)))
        except (ValueError, TypeError):
            pass

    def validate(reason: str, operation_type: str, cur_value: str, new_char: str, new_value: str):
        """Validate the values for the text.

        This is called when the textbox is modified, to allow cancelling bad
        inputs.

        Reason is the reason this was fired: 'key', 'focusin', 'focusout', 'forced'.
        operation_type is '1' for insert, '0' for delete', '-1' for programmatic changes.
        cur_val is the value before the change occurs.
        new_char is the added/removed text.
        new_value is the value after the change, if accepted.
        """
        if operation_type == '0' or reason == 'forced':
            # Deleting or done by the program, allow that always.
            return True

        if operation_type == '1':  # Inserted text.
            # Disallow non number and colons
            if new_char not in '0123456789:':
                return False
            # Only one colon.
            if ':' in cur_value and new_char == ':':
                return False

            # Don't allow more values if it has more than 2 numbers after
            # the colon - if there is one, and it's not in the last 3 characters.
            if ':' in new_value and ':' not in new_value[-3:]:
                return False

        if reason == 'focusout':
            # When leaving focus, apply range limits and set the var.
            try:
                str_min, str_sec = new_value.split(':')
                seconds = int(str_min) * 60 + int(str_sec)
            except (ValueError, TypeError):
                seconds = min_value
            else:
                if seconds < min_value:
                    seconds = min_value
                if seconds > max_value:
                    seconds = max_value
            var.set(str(seconds))  # This then re-writes the textbox.
        return True

    validate_cmd = parent.register(validate)

    spinbox = tk.Spinbox(
        parent,
        exportselection=False,
        textvariable=disp_var,
        command=set_var,
        wrap=True,
        values=values,
        width=5,

        validate='all',
        # These define which of the possible values will be passed along.
        # http://tcl.tk/man/tcl8.6/TkCmd/spinbox.htm#M26
        validatecommand=(validate_cmd, '%V', '%d', '%s', '%S', '%P'),
    )
    # We need to set this after, it gets reset to the first one.
    var.set(existing_value)
    return spinbox
Example #12
0
def res_make_tag_fizzler(vmf: VMF, inst: Entity, res: Property):
    """Add an Aperture Tag Paint Gun activation fizzler.

    These fizzlers are created via signs, and work very specially.
    MUST be priority -100 so it runs before fizzlers!
    """
    import vbsp
    if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']:
        # Abort - TAG fizzlers shouldn't appear in any other game!
        inst.remove()
        return

    fizz_base = fizz_name = None

    # Look for the fizzler instance we want to replace
    for targetname in inst.output_targets():
        if targetname in tag_fizzlers:
            fizz_name = targetname
            fizz_base = tag_fizzlers[targetname]
            del tag_fizzlers[targetname]  # Don't let other signs mod this one!
            continue
        else:
            # It's an indicator toggle, remove it and the antline to clean up.
            LOGGER.warning('Toggle: {}', targetname)
            for ent in vmf.by_target[targetname]:
                remove_ant_toggle(ent)
    inst.outputs.clear()  # Remove the outptuts now, they're not valid anyway.

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

    # The distance from origin the double signs are seperated by.
    sign_offset = res.int('signoffset', 16)

    sign_loc = (
        # The actual location of the sign - on the wall
        Vec.from_str(inst['origin']) +
        Vec(0, 0, -64).rotate_by_str(inst['angles']))

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

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

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

    inst_angle = srctools.parse_vec_str(inst['angles'])

    inst_normal = Vec(0, 0, 1).rotate(*inst_angle)
    loc = Vec.from_str(inst['origin'])

    if disable_other or (blue_enabled and oran_enabled):
        inst['file'] = res['frame_double']
        # On a wall, and pointing vertically
        if inst_normal.z == 0 and Vec(y=1).rotate(*inst_angle).z:
            # They're vertical, make sure blue's on top!
            blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset)
            oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset)
            # If orange is enabled, with two frames put that on top
            # instead since it's more important
            if disable_other and oran_enabled:
                blue_loc, oran_loc = oran_loc, blue_loc

        else:
            offset = Vec(0, sign_offset, 0).rotate(*inst_angle)
            blue_loc = loc + offset
            oran_loc = loc - offset
    else:
        inst['file'] = res['frame_single']
        # They're always centered
        blue_loc = loc
        oran_loc = loc

    if inst_normal.z != 0:
        # If on floors/ceilings, rotate to point at the fizzler!
        sign_floor_loc = sign_loc.copy()
        sign_floor_loc.z = 0  # We don't care about z-positions.

        # Grab the data saved earlier in res_find_potential_tag_fizzlers()
        axis, side_min, side_max, normal = tag_fizzler_locs[fizz_name]

        # The Z-axis fizzler (horizontal) must be treated differently.
        if axis == 'z':
            # For z-axis, just compare to the center point.
            # The values are really x, y, z, not what they're named.
            sign_dir = sign_floor_loc - (side_min, side_max, normal)
        else:
            # For the other two, we compare to the line,
            # or compare to the closest side (in line with the fizz)
            other_axis = 'x' if axis == 'y' else 'y'
            if abs(sign_floor_loc[other_axis] - normal) < 32:
                # Compare to the closest side. Use ** to swap x/y arguments
                # appropriately. The closest side is the one with the
                # smallest magnitude.
                vmf.create_ent(
                    classname='info_null',
                    targetname=inst['targetname'] + '_min',
                    origin=sign_floor_loc - Vec(**{
                        axis: side_min,
                        other_axis: normal,
                    }),
                )
                vmf.create_ent(
                    classname='info_null',
                    targetname=inst['targetname'] + '_max',
                    origin=sign_floor_loc - Vec(**{
                        axis: side_max,
                        other_axis: normal,
                    }),
                )
                sign_dir = min(
                    sign_floor_loc - Vec(**{
                        axis: side_min,
                        other_axis: normal,
                    }),
                    sign_floor_loc - Vec(**{
                        axis: side_max,
                        other_axis: normal,
                    }),
                    key=Vec.mag,
                )
            else:
                # Align just based on whether we're in front or behind.
                sign_dir = Vec()
                sign_dir[other_axis] = sign_floor_loc[other_axis] - normal

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

        # Rotate to fit the instances - south is down
        sign_angle = int(sign_angle + 90) % 360
        if inst_normal.z > 0:
            sign_angle = '0 {} 0'.format(sign_angle)
        elif inst_normal.z < 0:
            # Flip upside-down for ceilings
            sign_angle = '0 {} 180'.format(sign_angle)
    else:
        # On a wall, face upright
        sign_angle = PETI_INST_ANGLE[inst_normal.as_tuple()]

    # If disable_other, we show off signs. Otherwise we don't use that sign.
    blue_sign = 'blue_sign' if blue_enabled else 'blue_off_sign' if disable_other else None
    oran_sign = 'oran_sign' if oran_enabled else 'oran_off_sign' if disable_other else None

    if blue_sign:
        vmf.create_ent(
            classname='func_instance',
            file=res[blue_sign, ''],
            targetname=inst['targetname'],
            angles=sign_angle,
            origin=blue_loc.join(' '),
        )

    if oran_sign:
        vmf.create_ent(
            classname='func_instance',
            file=res[oran_sign, ''],
            targetname=inst['targetname'],
            angles=sign_angle,
            origin=oran_loc.join(' '),
        )

    # Now modify the fizzler...

    fizz_brushes = list(vmf.by_class['trigger_portal_cleanser']
                        & vmf.by_target[fizz_name + '_brush'])

    if 'base_inst' in res:
        fizz_base['file'] = instanceLocs.resolve_one(res['base_inst'],
                                                     error=True)
    fizz_base.outputs.clear()  # Remove outputs, otherwise they break
    # branch_toggle entities

    # Subtract the sign from the list of connections, but don't go below
    # zero
    fizz_base.fixup['$connectioncount'] = str(
        max(0,
            srctools.conv_int(fizz_base.fixup['$connectioncount', ''], 0) - 1))

    if 'model_inst' in res:
        model_inst = instanceLocs.resolve_one(res['model_inst'], error=True)
        for mdl_inst in vmf.by_class['func_instance']:
            if mdl_inst['targetname', ''].startswith(fizz_name + '_model'):
                mdl_inst['file'] = model_inst

    # Find the direction the fizzler front/back points - z=floor fizz
    # Signs will associate with the given side!
    bbox_min, bbox_max = fizz_brushes[0].get_bbox()
    for axis, val in zip('xyz', bbox_max - bbox_min):
        if val == 2:
            fizz_axis = axis
            sign_center = (bbox_min[axis] + bbox_max[axis]) / 2
            break
    else:
        # A fizzler that's not 128*x*2?
        raise Exception('Invalid fizzler brush ({})!'.format(fizz_name))

    # Figure out what the sides will set values to...
    pos_blue = False
    pos_oran = False
    neg_blue = False
    neg_oran = False
    if sign_loc[fizz_axis] < sign_center:
        pos_blue = blue_enabled
        pos_oran = oran_enabled
    else:
        neg_blue = blue_enabled
        neg_oran = oran_enabled

    fizz_off_tex = {
        'left': res['off_left'],
        'center': res['off_center'],
        'right': res['off_right'],
        'short': res['off_short'],
    }
    fizz_on_tex = {
        'left': res['on_left'],
        'center': res['on_center'],
        'right': res['on_right'],
        'short': res['on_short'],
    }

    # If it activates the paint gun, use different textures
    if pos_blue or pos_oran:
        pos_tex = fizz_on_tex
    else:
        pos_tex = fizz_off_tex

    if neg_blue or neg_oran:
        neg_tex = fizz_on_tex
    else:
        neg_tex = fizz_off_tex

    if vbsp.GAME_MODE == 'COOP':
        # We need ATLAS-specific triggers
        pos_trig = vmf.create_ent(classname='trigger_playerteam', )
        neg_trig = vmf.create_ent(classname='trigger_playerteam', )
        output = 'OnStartTouchBluePlayer'
    else:
        pos_trig = vmf.create_ent(classname='trigger_multiple', )
        neg_trig = vmf.create_ent(
            classname='trigger_multiple',
            spawnflags='1',
        )
        output = 'OnStartTouch'

    pos_trig['origin'] = neg_trig['origin'] = fizz_base['origin']
    pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1'  # Clients Only

    pos_trig['targetname'] = fizz_name + '-trig_pos'
    neg_trig['targetname'] = fizz_name + '-trig_neg'

    pos_trig.outputs = [
        Output(
            output,
            fizz_name + '-trig_neg',
            'Enable',
        ),
        Output(
            output,
            fizz_name + '-trig_pos',
            'Disable',
        ),
    ]

    neg_trig.outputs = [
        Output(
            output,
            fizz_name + '-trig_pos',
            'Enable',
        ),
        Output(
            output,
            fizz_name + '-trig_neg',
            'Disable',
        ),
    ]

    voice_attr = vbsp.settings['has_attr']

    if blue_enabled or disable_other:
        # If this is blue/oran only, don't affect the other color
        neg_trig.outputs.append(
            Output(
                output,
                '@BlueIsEnabled',
                'SetValue',
                param=srctools.bool_as_int(neg_blue),
            ))
        pos_trig.outputs.append(
            Output(
                output,
                '@BlueIsEnabled',
                'SetValue',
                param=srctools.bool_as_int(pos_blue),
            ))
        if blue_enabled:
            # Add voice attributes - we have the gun and gel!
            voice_attr['bluegelgun'] = True
            voice_attr['bluegel'] = True
            voice_attr['bouncegun'] = True
            voice_attr['bouncegel'] = True

    if oran_enabled or disable_other:
        neg_trig.outputs.append(
            Output(
                output,
                '@OrangeIsEnabled',
                'SetValue',
                param=srctools.bool_as_int(neg_oran),
            ))
        pos_trig.outputs.append(
            Output(
                output,
                '@OrangeIsEnabled',
                'SetValue',
                param=srctools.bool_as_int(pos_oran),
            ))
        if oran_enabled:
            voice_attr['orangegelgun'] = True
            voice_attr['orangegel'] = True
            voice_attr['speedgelgun'] = True
            voice_attr['speedgel'] = True

    if not oran_enabled and not blue_enabled:
        # If both are disabled, we must shutdown the gun when touching
        # either side - use neg_trig for that purpose!
        # We want to get rid of pos_trig to save ents
        vmf.remove_ent(pos_trig)
        neg_trig['targetname'] = fizz_name + '-trig_off'
        neg_trig.outputs.clear()
        neg_trig.add_out(
            Output(output, '@BlueIsEnabled', 'SetValue', param='0'))
        neg_trig.add_out(
            Output(output, '@OrangeIsEnabled', 'SetValue', param='0'))

    for fizz_brush in fizz_brushes:  # portal_cleanser ent, not solid!
        # Modify fizzler textures
        bbox_min, bbox_max = fizz_brush.get_bbox()
        for side in fizz_brush.sides():
            norm = side.normal()
            if norm[fizz_axis] == 0:
                # Not the front/back: force nodraw
                # Otherwise the top/bottom will have the odd stripes
                # which won't match the sides
                side.mat = 'tools/toolsnodraw'
                continue
            if norm[fizz_axis] == 1:
                side.mat = pos_tex[vbsp.TEX_FIZZLER[side.mat.casefold()]]
            else:
                side.mat = neg_tex[vbsp.TEX_FIZZLER[side.mat.casefold()]]
        # The fizzler shouldn't kill cubes
        fizz_brush['spawnflags'] = '1'

        fizz_brush.outputs.append(
            Output(
                'OnStartTouch',
                '@shake_global',
                'StartShake',
            ))

        fizz_brush.outputs.append(
            Output(
                'OnStartTouch',
                '@shake_global_sound',
                'PlaySound',
            ))

        # The triggers are 8 units thick, 24 from the center
        # (-1 because fizzlers are 2 thick on each side).
        neg_min, neg_max = Vec(bbox_min), Vec(bbox_max)
        neg_min[fizz_axis] -= 23
        neg_max[fizz_axis] -= 17

        pos_min, pos_max = Vec(bbox_min), Vec(bbox_max)
        pos_min[fizz_axis] += 17
        pos_max[fizz_axis] += 23

        if blue_enabled or oran_enabled:
            neg_trig.solids.append(
                vmf.make_prism(
                    neg_min,
                    neg_max,
                    mat='tools/toolstrigger',
                ).solid, )
            pos_trig.solids.append(
                vmf.make_prism(
                    pos_min,
                    pos_max,
                    mat='tools/toolstrigger',
                ).solid, )
        else:
            # If neither enabled, use one trigger
            neg_trig.solids.append(
                vmf.make_prism(
                    neg_min,
                    pos_max,
                    mat='tools/toolstrigger',
                ).solid, )
Example #13
0
def res_goo_debris(res: Property):
    """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.
    """
    import brushLoc

    space = res.int('spacing', 1)
    rand_count = res.int('number', None)
    if rand_count:
        rand_list = weighted_random(
            rand_count,
            res['weights', ''],
        )
    else:
        rand_list = None  # type: Optional[List[int]]
    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),
    )

    suff = ''
    for loc in possible_locs:
        random.seed('goo_debris_{}_{}_{}'.format(loc.x, loc.y, loc.z))
        if random.random() > chance:
            continue

        if rand_list is not None:
            suff = '_' + str(random.choice(rand_list) + 1)

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

    return RES_EXHAUSTED