# Released under the MIT License. See LICENSE for details.
#
"""Wrangles the game tutorial sequence."""

# Not too concerned with keeping this old module pretty;
# don't expect to be revisiting it.
# pylint: disable=too-many-branches
# pylint: disable=too-many-lines
# pylint: disable=missing-function-docstring, missing-class-docstring
# pylint: disable=unused-argument

import math
import logging
from collections import deque
from typing import TYPE_CHECKING, override

import bascenev1 as bs
from bascenev1 import stdassets
from bascenev1 import builtinassets

from bascenev1lib.actor.spaz import Spaz

if TYPE_CHECKING:
    from typing import Any, Callable, Sequence


def _safesetattr(node: bs.Node | None, attr: str, value: Any) -> None:
    if node:
        setattr(node, attr, value)


class ButtonPress:
    def __init__(
        self,
        button: str,
        delay: int = 0,
        release: bool = True,
        release_delay: int = 0,
    ):
        self._button = button
        self._delay = delay
        self._release = release
        self._release_delay = release_delay

    def run(self, a: TutorialActivity) -> None:
        s = a.current_spaz
        assert s is not None
        img: bs.Node | None
        release_call: Callable[[], None] | None
        color: Sequence[float] | None
        if self._button == 'punch':
            call = s.on_punch_press
            release_call = s.on_punch_release
            img = a.punch_image
            color = a.punch_image_color
        elif self._button == 'jump':
            call = s.on_jump_press
            release_call = s.on_jump_release
            img = a.jump_image
            color = a.jump_image_color
        elif self._button == 'bomb':
            call = s.on_bomb_press
            release_call = s.on_bomb_release
            img = a.bomb_image
            color = a.bomb_image_color
        elif self._button == 'pickUp':
            call = s.on_pickup_press
            release_call = s.on_pickup_release
            img = a.pickup_image
            color = a.pickup_image_color
        elif self._button == 'run':
            call = bs.CallStrict(s.on_run, 1.0)
            release_call = bs.CallStrict(s.on_run, 0.0)
            img = None
            color = None
        else:
            raise RuntimeError(f'invalid button: {self._button}')

        brightness = 4.0
        if color is not None:
            c_bright = list(color)
            c_bright[0] *= brightness
            c_bright[1] *= brightness
            c_bright[2] *= brightness
        else:
            c_bright = [1.0, 1.0, 1.0]

        if self._delay == 0:
            call()
            if img is not None:
                img.color = c_bright
                img.vr_depth = -40
        else:
            bs.timer(self._delay / 1000.0, call)
            if img is not None:
                bs.timer(
                    self._delay / 1000.0,
                    bs.CallStrict(_safesetattr, img, 'color', c_bright),
                )
                bs.timer(
                    self._delay / 1000.0,
                    bs.CallStrict(_safesetattr, img, 'vr_depth', -30),
                )
        if self._release:
            if self._delay == 0 and self._release_delay == 0:
                release_call()
            else:
                bs.timer(
                    0.001 * (self._delay + self._release_delay), release_call
                )
            if img is not None:
                bs.timer(
                    (self._delay + self._release_delay + 100) / 1000.0,
                    bs.CallStrict(_safesetattr, img, 'color', color),
                )
                bs.timer(
                    (self._delay + self._release_delay + 100) / 1000.0,
                    bs.CallStrict(_safesetattr, img, 'vr_depth', -20),
                )


class ButtonRelease:
    def __init__(self, button: str, delay: int = 0):
        self._button = button
        self._delay = delay

    def run(self, a: TutorialActivity) -> None:
        s = a.current_spaz
        assert s is not None
        call: Callable[[], None] | None
        img: bs.Node | None
        color: Sequence[float] | None
        if self._button == 'punch':
            call = s.on_punch_release
            img = a.punch_image
            color = a.punch_image_color
        elif self._button == 'jump':
            call = s.on_jump_release
            img = a.jump_image
            color = a.jump_image_color
        elif self._button == 'bomb':
            call = s.on_bomb_release
            img = a.bomb_image
            color = a.bomb_image_color
        elif self._button == 'pickUp':
            call = s.on_pickup_press
            img = a.pickup_image
            color = a.pickup_image_color
        elif self._button == 'run':
            call = bs.CallStrict(s.on_run, 0.0)
            img = None
            color = None
        else:
            raise RuntimeError('invalid button: ' + self._button)
        if self._delay == 0:
            call()
        else:
            bs.timer(self._delay / 1000.0, call)
        if img is not None:
            bs.timer(
                (self._delay + 100) / 1000.0,
                bs.CallStrict(_safesetattr, img, 'color', color),
            )
            bs.timer(
                (self._delay + 100 / 1000.0),
                bs.CallStrict(_safesetattr, img, 'vr_depth', -20),
            )


class Player(bs.Player['Team']):
    """Our player type for this game."""

    def __init__(self) -> None:
        self.pressed = False


class Team(bs.Team[Player]):
    """Our team type for this game."""

    def __init__(self) -> None:
        pass


class Reset:
    def __init__(self) -> None:
        pass

    def run(self, a: TutorialActivity) -> None:
        # if we're looping, print out how long each cycle took
        # print out how long each cycle took..
        if a.last_start_time is not None:
            tval = int(bs.apptime() * 1000.0) - a.last_start_time
            assert isinstance(tval, int)
            diff = tval
            a.cycle_times.append(diff)
            bs.broadcastmessage(
                'cycle time: '
                + str(diff)
                + ' (average: '
                + str(sum(a.cycle_times) / len(a.cycle_times))
                + ')'
            )
        tval = int(bs.apptime() * 1000.0)
        assert isinstance(tval, int)
        a.last_start_time = tval

        assert a.text
        a.text.text = ''
        for spaz in list(a.spazzes.values()):
            spaz.handlemessage(bs.DieMessage(immediate=True))
        a.spazzes = {}
        a.current_spaz = None
        for n in a.control_ui_nodes:
            n.opacity = 0.0
        a.set_stick_image_position(0, 0)


# Can be used for debugging.
class SetSpeed:
    def __init__(self, speed: int):
        self._speed = speed

    def run(self, a: TutorialActivity) -> None:
        print('setting to', self._speed)
        bs.set_debug_speed_exponent(self._speed)


class RemoveGloves:
    def __init__(self) -> None:
        pass

    def run(self, a: TutorialActivity) -> None:
        # pylint: disable=protected-access
        assert a.current_spaz is not None
        a.current_spaz._gloves_wear_off()


class KillSpaz:
    def __init__(self, num: int, explode: bool = False):
        self._num = num
        self._explode = explode

    def run(self, a: TutorialActivity) -> None:
        if self._explode:
            a.spazzes[self._num].shatter()
        del a.spazzes[self._num]


class SpawnSpaz:
    def __init__(
        self,
        num: int,
        position: Sequence[float],
        *,
        color: Sequence[float] = (1.0, 1.0, 1.0),
        make_current: bool = False,
        relative_to: int | None = None,
        name: str | bs.Lstr = '',
        flash: bool = True,
        angle: float = 0.0,
    ):
        self._num = num
        self._position = position
        self._make_current = make_current
        self._color = color
        self._relative_to = relative_to
        self._name = name
        self._flash = flash
        self._angle = angle

    def run(self, a: TutorialActivity) -> None:
        # if they gave a 'relative to' spaz, position is relative
        # to them
        pos: Sequence[float]
        if self._relative_to is not None:
            snode = a.spazzes[self._relative_to].node
            assert snode
            their_pos = snode.position
            pos = (
                their_pos[0] + self._position[0],
                their_pos[1] + self._position[1],
                their_pos[2] + self._position[2],
            )
        else:
            pos = self._position

        # if there's already a spaz at this spot, insta-kill it
        if self._num in a.spazzes:
            a.spazzes[self._num].handlemessage(bs.DieMessage(immediate=True))

        s = a.spazzes[self._num] = Spaz(
            color=self._color,
            start_invincible=self._flash,
            demo_mode=True,
        )

        # FIXME: Should extend spaz to support Lstr names.
        assert s.node
        if isinstance(self._name, bs.Lstr):
            s.node.name = self._name.evaluate()
        else:
            s.node.name = self._name
        s.node.name_color = self._color
        s.handlemessage(bs.StandMessage(pos, self._angle))
        if self._make_current:
            a.current_spaz = s
        if self._flash:
            a.spawn_sound.play(position=pos)


class Powerup:
    def __init__(
        self,
        num: int,
        position: Sequence[float],
        *,
        color: Sequence[float] = (1.0, 1.0, 1.0),
        make_current: bool = False,
        relative_to: int | None = None,
    ):
        self._position = position
        self._relative_to = relative_to

    def run(self, a: TutorialActivity) -> None:
        # If they gave a 'relative to' spaz, position is relative
        # to them.
        pos: Sequence[float]
        if self._relative_to is not None:
            snode = a.spazzes[self._relative_to].node
            assert snode
            their_pos = snode.position
            pos = (
                their_pos[0] + self._position[0],
                their_pos[1] + self._position[1],
                their_pos[2] + self._position[2],
            )
        else:
            pos = self._position
        from bascenev1lib.actor import powerupbox

        powerupbox.PowerupBox(position=pos, poweruptype='punch').autoretain()


class Delay:
    def __init__(self, time: int) -> None:
        self._time = time

    def run(self, a: TutorialActivity) -> int:
        return self._time


class AnalyticsScreen:
    def __init__(self, screen: str) -> None:
        self._screen = screen

    def run(self, a: TutorialActivity) -> None:
        bs.set_analytics_screen(self._screen)


class DelayOld:
    def __init__(self, time: int) -> None:
        self._time = time

    def run(self, a: TutorialActivity) -> int:
        return int(0.9 * self._time)


class DelayOld2:
    def __init__(self, time: int) -> None:
        self._time = time

    def run(self, a: TutorialActivity) -> int:
        return int(0.8 * self._time)


class End:
    def __init__(self) -> None:
        pass

    def run(self, a: TutorialActivity) -> None:
        bs.increment_analytics_count('Tutorial finish')
        a.end()


class Move:
    def __init__(self, x: float, y: float):
        self._x = float(x)
        self._y = float(y)

    def run(self, a: TutorialActivity) -> None:
        s = a.current_spaz
        assert s
        # FIXME: Game should take floats for this.
        x_clamped = self._x
        y_clamped = self._y
        s.on_move_left_right(x_clamped)
        s.on_move_up_down(y_clamped)
        a.set_stick_image_position(self._x, self._y)


class MoveLR:
    def __init__(self, x: float):
        self._x = float(x)

    def run(self, a: TutorialActivity) -> None:
        s = a.current_spaz
        assert s
        # FIXME: Game should take floats for this.
        x_clamped = self._x
        s.on_move_left_right(x_clamped)
        a.set_stick_image_position(self._x, a.stick_image_position_y)


class MoveUD:
    def __init__(self, y: float):
        self._y = float(y)

    def run(self, a: TutorialActivity) -> None:
        s = a.current_spaz
        assert s
        # FIXME: Game should take floats for this.
        y_clamped = self._y
        s.on_move_up_down(y_clamped)
        a.set_stick_image_position(a.stick_image_position_x, self._y)


class Bomb(ButtonPress):
    def __init__(
        self,
        delay: int = 0,
        release: bool = True,
        release_delay: int = 500,
    ):
        ButtonPress.__init__(
            self,
            'bomb',
            delay=delay,
            release=release,
            release_delay=release_delay,
        )


class Jump(ButtonPress):
    def __init__(
        self,
        delay: int = 0,
        release: bool = True,
        release_delay: int = 500,
    ):
        ButtonPress.__init__(
            self,
            'jump',
            delay=delay,
            release=release,
            release_delay=release_delay,
        )


class Punch(ButtonPress):
    def __init__(
        self,
        delay: int = 0,
        release: bool = True,
        release_delay: int = 500,
    ):
        ButtonPress.__init__(
            self,
            'punch',
            delay=delay,
            release=release,
            release_delay=release_delay,
        )


class PickUp(ButtonPress):
    def __init__(
        self,
        delay: int = 0,
        release: bool = True,
        release_delay: int = 500,
    ):
        ButtonPress.__init__(
            self,
            'pickUp',
            delay=delay,
            release=release,
            release_delay=release_delay,
        )


class Run(ButtonPress):
    def __init__(
        self,
        delay: int = 0,
        release: bool = True,
        release_delay: int = 500,
    ):
        ButtonPress.__init__(
            self,
            'run',
            delay=delay,
            release=release,
            release_delay=release_delay,
        )


class BombRelease(ButtonRelease):
    def __init__(self, delay: int = 0):
        super().__init__('bomb', delay=delay)


class JumpRelease(ButtonRelease):
    def __init__(self, delay: int = 0):
        super().__init__('jump', delay=delay)


class PunchRelease(ButtonRelease):
    def __init__(self, delay: int = 0):
        super().__init__('punch', delay=delay)


class PickUpRelease(ButtonRelease):
    def __init__(self, delay: int = 0):
        super().__init__('pickUp', delay=delay)


class RunRelease(ButtonRelease):
    def __init__(self, delay: int = 0):
        super().__init__('run', delay=delay)


class ShowControls:
    def __init__(self) -> None:
        pass

    def run(self, a: TutorialActivity) -> None:
        for n in a.control_ui_nodes:
            bs.animate(n, 'opacity', {0.0: 0.0, 1.0: 1.0})


class Text:
    def __init__(self, text: str | bs.Lstr | bs.LangStr):
        self.text = text

    def run(self, a: TutorialActivity) -> None:
        assert a.text
        a.text.text = self.text


class PrintPos:
    def __init__(self, spaz_num: int | None = None):
        self._spaz_num = spaz_num

    def run(self, a: TutorialActivity) -> None:
        if self._spaz_num is None:
            s = a.current_spaz
        else:
            s = a.spazzes[self._spaz_num]
        assert s and s.node
        t = list(s.node.position)
        print('RestorePos(' + str((t[0], t[1] - 1.0, t[2])) + '),')


class RestorePos:
    def __init__(self, pos: Sequence[float]) -> None:
        self._pos = pos

    def run(self, a: TutorialActivity) -> None:
        s = a.current_spaz
        assert s
        s.handlemessage(bs.StandMessage(self._pos, 0))


class Celebrate:
    def __init__(
        self,
        celebrate_type: str = 'both',
        spaz_num: int | None = None,
        duration: int = 1000,
    ):
        self._spaz_num = spaz_num
        self._celebrate_type = celebrate_type
        self._duration = duration

    def run(self, a: TutorialActivity) -> None:
        if self._spaz_num is None:
            s = a.current_spaz
        else:
            s = a.spazzes[self._spaz_num]
        assert s and s.node
        if self._celebrate_type == 'right':
            s.node.handlemessage('celebrate_r', self._duration)
        elif self._celebrate_type == 'left':
            s.node.handlemessage('celebrate_l', self._duration)
        elif self._celebrate_type == 'both':
            s.node.handlemessage('celebrate', self._duration)
        else:
            raise RuntimeError('invalid celebrate type ' + self._celebrate_type)


class TutorialActivity(bs.Activity[Player, Team]):
    def __init__(self, settings: dict | None = None):
        from bascenev1lib.maps import Rampage

        if settings is None:
            settings = {}
        super().__init__(settings)
        self.current_spaz: Spaz | None = None
        self._benchmark_type = getattr(bs.getsession(), 'benchmark_type', None)
        self.last_start_time: int | None = None
        self.cycle_times: list[int] = []
        self.allow_pausing = True
        self.allow_kick_idle_players = False
        self._issued_warning = False
        self._map_type = Rampage
        self._map_type.preload()
        self._jump_button_tex = stdassets.textures.button_jump
        self._pick_up_button_tex = stdassets.textures.button_pick_up
        self._bomb_button_tex = stdassets.textures.button_bomb
        self._punch_button_tex = stdassets.textures.button_punch
        self._r = 'tutorial'
        self._have_skipped = False
        self.stick_image_position_x = self.stick_image_position_y = 0.0
        self.spawn_sound = stdassets.audio.spawn
        self.map: bs.Map | None = None
        self.text: bs.Node | None = None
        self._skip_text: bs.Node | None = None
        self._skip_count_text: bs.Node | None = None
        self._scale: float | None = None
        self._stick_base_position: tuple[float, float] = (0.0, 0.0)
        self._stick_nub_position: tuple[float, float] = (0.0, 0.0)
        self._stick_base_image_color: Sequence[float] = (1.0, 1.0, 1.0, 1.0)
        self._stick_nub_image_color: Sequence[float] = (1.0, 1.0, 1.0, 1.0)
        self._time: int = -1
        self.punch_image_color = (1.0, 1.0, 1.0)
        self.punch_image: bs.Node | None = None
        self.bomb_image: bs.Node | None = None
        self.jump_image: bs.Node | None = None
        self.pickup_image: bs.Node | None = None
        self._stick_base_image: bs.Node | None = None
        self._stick_nub_image: bs.Node | None = None
        self.bomb_image_color = (1.0, 1.0, 1.0)
        self.pickup_image_color = (1.0, 1.0, 1.0)
        self.control_ui_nodes: list[bs.Node] = []
        self.spazzes: dict[int, Spaz] = {}
        self.jump_image_color = (1.0, 1.0, 1.0)
        self._entries: deque[Any] = deque()
        self._read_entries_timer: bs.Timer | None = None
        self._entry_timer: bs.Timer | None = None

    @override
    def on_transition_in(self) -> None:
        super().on_transition_in()
        bs.setmusic(bs.MusicType.CHAR_SELECT, continuous=True)
        self.map = self._map_type()

    @override
    def on_begin(self) -> None:
        super().on_begin()

        bs.set_analytics_screen('Tutorial Start')
        bs.increment_analytics_count('Tutorial start')

        if bool(False):
            # Buttons on top.
            text_y = 140
            buttons_y = 250
        else:
            # Buttons on bottom.
            text_y = 260
            buttons_y = 160

        # Need different versions of this: taps/buttons/keys.
        self.text = bs.newnode(
            'text',
            attrs={
                'text': '',
                'scale': 1.9,
                'position': (0, text_y),
                'maxwidth': 500,
                'flatness': 0.0,
                'shadow': 0.5,
                'h_align': 'center',
                'v_align': 'center',
                'v_attach': 'center',
            },
        )

        # Need different versions of this: taps/buttons/keys.
        txt = (
            stdassets.strings.tutorial.cpu_benchmark
            if self._benchmark_type == 'cpu'
            else stdassets.strings.tutorial.to_skip_press_anything
        )
        t = self._skip_text = bs.newnode(
            'text',
            attrs={
                'text': txt,
                'maxwidth': 900,
                'scale': 1.1,
                'vr_depth': 100,
                'position': (0, 30),
                'h_align': 'center',
                'v_align': 'center',
                'v_attach': 'bottom',
            },
        )
        bs.animate(t, 'opacity', {1.0: 0.0, 2.0: 0.7})
        self._skip_count_text = bs.newnode(
            'text',
            attrs={
                'text': '',
                'scale': 1.4,
                'vr_depth': 90,
                'position': (0, 70),
                'h_align': 'center',
                'v_align': 'center',
                'v_attach': 'bottom',
            },
        )

        ouya = False

        self._scale = scale = 0.6
        center_offs = 130.0 * scale
        offs = 65.0 * scale
        position = (0, buttons_y)
        image_size = 90.0 * scale
        image_size_2 = 220.0 * scale
        nub_size = 110.0 * scale
        p = (position[0] + center_offs, position[1] - offs)

        def _sc(r: float, g: float, b: float) -> tuple[float, float, float]:
            return 0.6 * r, 0.6 * g, 0.6 * b

        self.jump_image_color = c = _sc(0.4, 1, 0.4)
        self.jump_image = bs.newnode(
            'image',
            attrs={
                'texture': self._jump_button_tex,
                'absolute_scale': True,
                'vr_depth': -20,
                'position': p,
                'scale': (image_size, image_size),
                'color': c,
            },
        )
        p = (position[0] + center_offs - offs, position[1])
        self.punch_image_color = c = (
            _sc(0.2, 0.6, 1) if ouya else _sc(1, 0.7, 0.3)
        )
        self.punch_image = bs.newnode(
            'image',
            attrs={
                'texture': stdassets.textures.button_punch,
                'absolute_scale': True,
                'vr_depth': -20,
                'position': p,
                'scale': (image_size, image_size),
                'color': c,
            },
        )
        p = (position[0] + center_offs + offs, position[1])
        self.bomb_image_color = c = _sc(1, 0.3, 0.3)
        self.bomb_image = bs.newnode(
            'image',
            attrs={
                'texture': stdassets.textures.button_bomb,
                'absolute_scale': True,
                'vr_depth': -20,
                'position': p,
                'scale': (image_size, image_size),
                'color': c,
            },
        )
        p = (position[0] + center_offs, position[1] + offs)
        self.pickup_image_color = c = (
            _sc(1, 0.8, 0.3) if ouya else _sc(0.5, 0.5, 1)
        )
        self.pickup_image = bs.newnode(
            'image',
            attrs={
                'texture': stdassets.textures.button_pick_up,
                'absolute_scale': True,
                'vr_depth': -20,
                'position': p,
                'scale': (image_size, image_size),
                'color': c,
            },
        )

        self._stick_base_position = p = (position[0] - center_offs, position[1])
        self._stick_base_image_color = c2 = (0.25, 0.25, 0.25, 1.0)
        self._stick_base_image = bs.newnode(
            'image',
            attrs={
                'texture': builtinassets.textures.nub,
                'absolute_scale': True,
                'vr_depth': -40,
                'position': p,
                'scale': (image_size_2, image_size_2),
                'color': c2,
            },
        )
        self._stick_nub_position = p = (position[0] - center_offs, position[1])
        self._stick_nub_image_color = c3 = (0.4, 0.4, 0.4, 1.0)
        self._stick_nub_image = bs.newnode(
            'image',
            attrs={
                'texture': builtinassets.textures.nub,
                'absolute_scale': True,
                'position': p,
                'scale': (nub_size, nub_size),
                'color': c3,
            },
        )
        self.control_ui_nodes = [
            self.jump_image,
            self.punch_image,
            self.bomb_image,
            self.pickup_image,
            self._stick_base_image,
            self._stick_nub_image,
        ]
        for n in self.control_ui_nodes:
            n.opacity = 0.0
        self._read_entries()

    def set_stick_image_position(self, x: float, y: float) -> None:
        # Clamp this to a circle.
        len_squared = x * x + y * y
        if len_squared > 1.0:
            length = math.sqrt(len_squared)
            mult = 1.0 / length
            x *= mult
            y *= mult

        self.stick_image_position_x = x
        self.stick_image_position_y = y
        offs = 50.0
        assert self._scale is not None
        p = [
            self._stick_nub_position[0] + x * offs * self._scale,
            self._stick_nub_position[1] + y * offs * self._scale,
        ]
        c = list(self._stick_nub_image_color)
        if abs(x) > 0.1 or abs(y) > 0.1:
            c[0] *= 2.0
            c[1] *= 4.0
            c[2] *= 2.0
        assert self._stick_nub_image is not None
        self._stick_nub_image.position = p
        self._stick_nub_image.color = c
        c = list(self._stick_base_image_color)
        if abs(x) > 0.1 or abs(y) > 0.1:
            c[0] *= 1.5
            c[1] *= 1.5
            c[2] *= 1.5
        assert self._stick_base_image is not None
        self._stick_base_image.color = c

    def _read_entries(self) -> None:
        try:

            self._entries = deque(
                [
                    Reset(),
                    SpawnSpaz(0, (0, 5.5, -3.0), make_current=True),
                    DelayOld(1000),
                    AnalyticsScreen('Tutorial Section 1'),
                    Text(stdassets.strings.tutorial.phrase01),  # hi there
                    Celebrate('left'),
                    DelayOld(2000),
                    Text(
                        bs.Lstr(
                            resource=f'{self._r}.phrase02Text',
                            subs=[
                                ('${APP_NAME}', bs.Lstr(resource='titleText'))
                            ],
                        )
                    ),  # welcome to <appname>
                    DelayOld(80),
                    Run(release=False),
                    Jump(release=False),
                    MoveLR(1),
                    MoveUD(0),
                    DelayOld(70),
                    RunRelease(),
                    JumpRelease(),
                    DelayOld(60),
                    MoveUD(1),
                    DelayOld(30),
                    MoveLR(0),
                    DelayOld(90),
                    MoveLR(-1),
                    DelayOld(20),
                    MoveUD(0),
                    DelayOld(70),
                    MoveUD(-1),
                    DelayOld(20),
                    MoveLR(0),
                    DelayOld(80),
                    MoveUD(0),
                    DelayOld(1500),
                    Text(
                        stdassets.strings.tutorial.phrase03
                    ),  # here's a few tips
                    DelayOld(1000),
                    ShowControls(),
                    DelayOld(1000),
                    Jump(),
                    DelayOld(1000),
                    Jump(),
                    DelayOld(1000),
                    AnalyticsScreen('Tutorial Section 2'),
                    Text(
                        bs.Lstr(
                            resource=f'{self._r}.phrase04Text',
                            subs=[
                                ('${APP_NAME}', bs.Lstr(resource='titleText'))
                            ],
                        )
                    ),  # many things are based on physics
                    DelayOld(20),
                    MoveUD(0),
                    DelayOld(60),
                    MoveLR(0),
                    DelayOld(10),
                    MoveLR(0),
                    MoveUD(0),
                    DelayOld(10),
                    MoveLR(0),
                    MoveUD(0),
                    DelayOld(20),
                    MoveUD(-0.0575579),
                    DelayOld(10),
                    MoveUD(-0.207831),
                    DelayOld(30),
                    MoveUD(-0.309793),
                    DelayOld(10),
                    MoveUD(-0.474502),
                    DelayOld(10),
                    MoveLR(0.00390637),
                    MoveUD(-0.647053),
                    DelayOld(20),
                    MoveLR(-0.0745262),
                    MoveUD(-0.819605),
                    DelayOld(10),
                    MoveLR(-0.168645),
                    MoveUD(-0.937254),
                    DelayOld(30),
                    MoveLR(-0.294137),
                    MoveUD(-1),
                    DelayOld(10),
                    MoveLR(-0.411786),
                    DelayOld(10),
                    MoveLR(-0.639241),
                    DelayOld(30),
                    MoveLR(-0.75689),
                    DelayOld(10),
                    MoveLR(-0.905911),
                    DelayOld(20),
                    MoveLR(-1),
                    DelayOld(50),
                    MoveUD(-0.960784),
                    DelayOld(20),
                    MoveUD(-0.819605),
                    MoveUD(-0.61568),
                    DelayOld(20),
                    MoveUD(-0.427442),
                    DelayOld(20),
                    MoveUD(-0.231361),
                    DelayOld(10),
                    MoveUD(-0.00390637),
                    DelayOld(30),
                    MoveUD(0.333354),
                    MoveUD(0.584338),
                    DelayOld(20),
                    MoveUD(0.764733),
                    DelayOld(30),
                    MoveLR(-0.803949),
                    MoveUD(0.913755),
                    DelayOld(10),
                    MoveLR(-0.647084),
                    MoveUD(0.992187),
                    DelayOld(20),
                    MoveLR(-0.435316),
                    MoveUD(1),
                    DelayOld(20),
                    MoveLR(-0.168645),
                    MoveUD(0.976501),
                    MoveLR(0.0744957),
                    MoveUD(0.905911),
                    DelayOld(20),
                    MoveLR(0.270577),
                    MoveUD(0.843165),
                    DelayOld(20),
                    MoveLR(0.435286),
                    MoveUD(0.780419),
                    DelayOld(10),
                    MoveLR(0.66274),
                    MoveUD(0.647084),
                    DelayOld(30),
                    MoveLR(0.803919),
                    MoveUD(0.458846),
                    MoveLR(0.929411),
                    MoveUD(0.223548),
                    DelayOld(20),
                    MoveLR(0.95294),
                    MoveUD(0.137272),
                    DelayOld(20),
                    MoveLR(1),
                    MoveUD(-0.0509659),
                    DelayOld(20),
                    MoveUD(-0.247047),
                    DelayOld(20),
                    MoveUD(-0.443129),
                    DelayOld(20),
                    MoveUD(-0.694113),
                    MoveUD(-0.921567),
                    DelayOld(30),
                    MoveLR(0.858821),
                    MoveUD(-1),
                    DelayOld(10),
                    MoveLR(0.68627),
                    DelayOld(10),
                    MoveLR(0.364696),
                    DelayOld(20),
                    MoveLR(0.0509659),
                    DelayOld(20),
                    MoveLR(-0.223548),
                    DelayOld(10),
                    MoveLR(-0.600024),
                    MoveUD(-0.913724),
                    DelayOld(30),
                    MoveLR(-0.858852),
                    MoveUD(-0.717643),
                    MoveLR(-1),
                    MoveUD(-0.474502),
                    DelayOld(20),
                    MoveUD(-0.396069),
                    DelayOld(20),
                    MoveUD(-0.286264),
                    DelayOld(20),
                    MoveUD(-0.137242),
                    DelayOld(20),
                    MoveUD(0.0353099),
                    DelayOld(10),
                    MoveUD(0.32551),
                    DelayOld(20),
                    MoveUD(0.592181),
                    DelayOld(10),
                    MoveUD(0.851009),
                    DelayOld(10),
                    MoveUD(1),
                    DelayOld(30),
                    MoveLR(-0.764733),
                    DelayOld(20),
                    MoveLR(-0.403943),
                    MoveLR(-0.145116),
                    DelayOld(30),
                    MoveLR(0.0901822),
                    MoveLR(0.32548),
                    DelayOld(30),
                    MoveLR(0.560778),
                    MoveUD(0.929441),
                    DelayOld(20),
                    MoveLR(0.709799),
                    MoveUD(0.73336),
                    MoveLR(0.803919),
                    MoveUD(0.545122),
                    DelayOld(20),
                    MoveLR(0.882351),
                    MoveUD(0.356883),
                    DelayOld(10),
                    MoveLR(0.968627),
                    MoveUD(0.113742),
                    DelayOld(20),
                    MoveLR(0.992157),
                    MoveUD(-0.0823389),
                    DelayOld(30),
                    MoveUD(-0.309793),
                    DelayOld(10),
                    MoveUD(-0.545091),
                    DelayOld(20),
                    MoveLR(0.882351),
                    MoveUD(-0.874508),
                    DelayOld(20),
                    MoveLR(0.756859),
                    MoveUD(-1),
                    DelayOld(10),
                    MoveLR(0.576464),
                    DelayOld(20),
                    MoveLR(0.254891),
                    DelayOld(10),
                    MoveLR(-0.0274667),
                    DelayOld(10),
                    MoveLR(-0.356883),
                    DelayOld(30),
                    MoveLR(-0.592181),
                    MoveLR(-0.827479),
                    MoveUD(-0.921567),
                    DelayOld(20),
                    MoveLR(-1),
                    MoveUD(-0.749016),
                    DelayOld(20),
                    MoveUD(-0.61568),
                    DelayOld(10),
                    MoveUD(-0.403912),
                    DelayOld(20),
                    MoveUD(-0.207831),
                    DelayOld(10),
                    MoveUD(0.121586),
                    DelayOld(30),
                    MoveUD(0.34904),
                    DelayOld(10),
                    MoveUD(0.560808),
                    DelayOld(10),
                    MoveUD(0.827479),
                    DelayOld(30),
                    MoveUD(1),
                    DelayOld(20),
                    MoveLR(-0.976501),
                    MoveLR(-0.670614),
                    DelayOld(20),
                    MoveLR(-0.239235),
                    DelayOld(20),
                    MoveLR(0.160772),
                    DelayOld(20),
                    MoveLR(0.443129),
                    DelayOld(10),
                    MoveLR(0.68627),
                    MoveUD(0.976501),
                    DelayOld(30),
                    MoveLR(0.929411),
                    MoveUD(0.73336),
                    MoveLR(1),
                    MoveUD(0.482376),
                    DelayOld(20),
                    MoveUD(0.34904),
                    DelayOld(10),
                    MoveUD(0.160802),
                    DelayOld(30),
                    MoveUD(-0.0744957),
                    DelayOld(10),
                    MoveUD(-0.333323),
                    DelayOld(20),
                    MoveUD(-0.647053),
                    DelayOld(20),
                    MoveUD(-0.937254),
                    DelayOld(10),
                    MoveLR(0.858821),
                    MoveUD(-1),
                    DelayOld(10),
                    MoveLR(0.576464),
                    DelayOld(30),
                    MoveLR(0.184301),
                    DelayOld(10),
                    MoveLR(-0.121586),
                    DelayOld(10),
                    MoveLR(-0.474532),
                    DelayOld(30),
                    MoveLR(-0.670614),
                    MoveLR(-0.851009),
                    DelayOld(30),
                    MoveLR(-1),
                    MoveUD(-0.968627),
                    DelayOld(20),
                    MoveUD(-0.843135),
                    DelayOld(10),
                    MoveUD(-0.631367),
                    DelayOld(20),
                    MoveUD(-0.403912),
                    MoveUD(-0.176458),
                    DelayOld(20),
                    MoveUD(0.0902127),
                    DelayOld(20),
                    MoveUD(0.380413),
                    DelayOld(10),
                    MoveUD(0.717673),
                    DelayOld(30),
                    MoveUD(1),
                    DelayOld(10),
                    MoveLR(-0.741203),
                    DelayOld(20),
                    MoveLR(-0.458846),
                    DelayOld(10),
                    MoveLR(-0.145116),
                    DelayOld(10),
                    MoveLR(0.0980255),
                    DelayOld(20),
                    MoveLR(0.294107),
                    DelayOld(30),
                    MoveLR(0.466659),
                    MoveLR(0.717643),
                    MoveUD(0.796106),
                    DelayOld(20),
                    MoveLR(0.921567),
                    MoveUD(0.443159),
                    DelayOld(20),
                    MoveLR(1),
                    MoveUD(0.145116),
                    DelayOld(10),
                    MoveUD(-0.0274361),
                    DelayOld(30),
                    MoveUD(-0.223518),
                    MoveUD(-0.427442),
                    DelayOld(20),
                    MoveUD(-0.874508),
                    DelayOld(20),
                    MoveUD(-1),
                    DelayOld(10),
                    MoveLR(0.929411),
                    DelayOld(20),
                    MoveLR(0.68627),
                    DelayOld(20),
                    MoveLR(0.364696),
                    DelayOld(20),
                    MoveLR(0.0431227),
                    DelayOld(10),
                    MoveLR(-0.333354),
                    DelayOld(20),
                    MoveLR(-0.639241),
                    DelayOld(20),
                    MoveLR(-0.968657),
                    MoveUD(-0.968627),
                    DelayOld(20),
                    MoveLR(-1),
                    MoveUD(-0.890194),
                    MoveUD(-0.866665),
                    DelayOld(20),
                    MoveUD(-0.749016),
                    DelayOld(20),
                    MoveUD(-0.529405),
                    DelayOld(20),
                    MoveUD(-0.30195),
                    DelayOld(10),
                    MoveUD(-0.00390637),
                    DelayOld(10),
                    MoveUD(0.262764),
                    DelayOld(30),
                    MoveLR(-0.600024),
                    MoveUD(0.458846),
                    DelayOld(10),
                    MoveLR(-0.294137),
                    MoveUD(0.482376),
                    DelayOld(20),
                    MoveLR(-0.200018),
                    MoveUD(0.505905),
                    DelayOld(10),
                    MoveLR(-0.145116),
                    MoveUD(0.545122),
                    DelayOld(20),
                    MoveLR(-0.0353099),
                    MoveUD(0.584338),
                    DelayOld(20),
                    MoveLR(0.137242),
                    MoveUD(0.592181),
                    DelayOld(20),
                    MoveLR(0.30195),
                    DelayOld(10),
                    MoveLR(0.490188),
                    DelayOld(10),
                    MoveLR(0.599994),
                    MoveUD(0.529435),
                    DelayOld(30),
                    MoveLR(0.66274),
                    MoveUD(0.3961),
                    DelayOld(20),
                    MoveLR(0.670583),
                    MoveUD(0.231391),
                    MoveLR(0.68627),
                    MoveUD(0.0745262),
                    Move(0, -0.01),
                    DelayOld(100),
                    Move(0, 0),
                    DelayOld(1000),
                    Text(
                        stdassets.strings.tutorial.phrase05
                    ),  # for example when you punch..
                    DelayOld(510),
                    Move(0, -0.01),
                    DelayOld(100),
                    Move(0, 0),
                    DelayOld(500),
                    SpawnSpaz(
                        0,
                        (-0.09249162673950195, 4.337906360626221, -2.3),
                        make_current=True,
                        flash=False,
                    ),
                    SpawnSpaz(
                        1,
                        (-3.1, 4.3, -2.0),
                        make_current=False,
                        color=(1, 1, 0.4),
                        name=bs.Lstr(resource=f'{self._r}.randomName1Text'),
                    ),
                    Move(-1.0, 0),
                    DelayOld(1050),
                    Move(0, -0.01),
                    DelayOld(100),
                    Move(0, 0),
                    DelayOld(1000),
                    Text(
                        stdassets.strings.tutorial.phrase06
                    ),  # your damage is based
                    DelayOld(1200),
                    Move(-0.05, 0),
                    DelayOld(200),
                    Punch(),
                    DelayOld(800),
                    Punch(),
                    DelayOld(800),
                    Punch(),
                    DelayOld(800),
                    Move(0, -0.01),
                    DelayOld(100),
                    Move(0, 0),
                    Text(
                        bs.Lstr(
                            resource=f'{self._r}.phrase07Text',
                            subs=[
                                (
                                    '${NAME}',
                                    bs.Lstr(
                                        resource=f'{self._r}.randomName1Text'
                                    ),
                                )
                            ],
                        )
                    ),  # see that didn't hurt fred
                    DelayOld(2000),
                    Celebrate('right', spaz_num=1),
                    DelayOld(1400),
                    Text(
                        stdassets.strings.tutorial.phrase08
                    ),  # lets jump and spin to get more speed
                    DelayOld(30),
                    MoveLR(0),
                    DelayOld(40),
                    MoveLR(0),
                    DelayOld(40),
                    MoveLR(0),
                    DelayOld(130),
                    MoveLR(0),
                    DelayOld(100),
                    MoveLR(0),
                    DelayOld(10),
                    MoveLR(0.0480667),
                    DelayOld(40),
                    MoveLR(0.056093),
                    MoveLR(0.0681173),
                    DelayOld(30),
                    MoveLR(0.0801416),
                    DelayOld(10),
                    MoveLR(0.184301),
                    DelayOld(10),
                    MoveLR(0.207831),
                    DelayOld(20),
                    MoveLR(0.231361),
                    DelayOld(30),
                    MoveLR(0.239204),
                    DelayOld(30),
                    MoveLR(0.254891),
                    DelayOld(40),
                    MoveLR(0.270577),
                    DelayOld(10),
                    MoveLR(0.30195),
                    DelayOld(20),
                    MoveLR(0.341166),
                    DelayOld(30),
                    MoveLR(0.388226),
                    MoveLR(0.435286),
                    DelayOld(30),
                    MoveLR(0.490188),
                    DelayOld(10),
                    MoveLR(0.560778),
                    DelayOld(20),
                    MoveLR(0.599994),
                    DelayOld(10),
                    MoveLR(0.647053),
                    DelayOld(10),
                    MoveLR(0.68627),
                    DelayOld(30),
                    MoveLR(0.733329),
                    DelayOld(20),
                    MoveLR(0.764702),
                    DelayOld(10),
                    MoveLR(0.827448),
                    DelayOld(20),
                    MoveLR(0.874508),
                    DelayOld(20),
                    MoveLR(0.929411),
                    DelayOld(10),
                    MoveLR(1),
                    DelayOld(830),
                    MoveUD(0.0274667),
                    DelayOld(10),
                    MoveLR(0.95294),
                    MoveUD(0.113742),
                    DelayOld(30),
                    MoveLR(0.780389),
                    MoveUD(0.184332),
                    DelayOld(10),
                    MoveLR(0.27842),
                    MoveUD(0.0745262),
                    DelayOld(20),
                    MoveLR(0),
                    MoveUD(0),
                    DelayOld(390),
                    MoveLR(0),
                    MoveLR(0),
                    DelayOld(20),
                    MoveLR(0),
                    DelayOld(20),
                    MoveLR(0),
                    DelayOld(10),
                    MoveLR(-0.0537431),
                    DelayOld(20),
                    MoveLR(-0.215705),
                    DelayOld(30),
                    MoveLR(-0.388256),
                    MoveLR(-0.529435),
                    DelayOld(30),
                    MoveLR(-0.694143),
                    DelayOld(20),
                    MoveLR(-0.851009),
                    MoveUD(0.0588397),
                    DelayOld(10),
                    MoveLR(-1),
                    MoveUD(0.0745262),
                    Run(release=False),
                    DelayOld(200),
                    MoveUD(0.0509964),
                    DelayOld(30),
                    MoveUD(0.0117801),
                    DelayOld(20),
                    MoveUD(-0.0901822),
                    MoveUD(-0.372539),
                    DelayOld(30),
                    MoveLR(-0.898068),
                    MoveUD(-0.890194),
                    Jump(release=False),
                    DelayOld(20),
                    MoveLR(-0.647084),
                    MoveUD(-1),
                    MoveLR(-0.427473),
                    DelayOld(20),
                    MoveLR(-0.00393689),
                    DelayOld(10),
                    MoveLR(0.537248),
                    DelayOld(30),
                    MoveLR(1),
                    DelayOld(50),
                    RunRelease(),
                    JumpRelease(),
                    DelayOld(50),
                    MoveUD(-0.921567),
                    MoveUD(-0.749016),
                    DelayOld(30),
                    MoveUD(-0.552934),
                    DelayOld(10),
                    MoveUD(-0.247047),
                    DelayOld(20),
                    MoveUD(0.200018),
                    DelayOld(20),
                    MoveUD(0.670614),
                    MoveUD(1),
                    DelayOld(70),
                    MoveLR(0.97647),
                    DelayOld(20),
                    MoveLR(0.764702),
                    DelayOld(20),
                    MoveLR(0.364696),
                    DelayOld(20),
                    MoveLR(0.00390637),
                    MoveLR(-0.309824),
                    DelayOld(20),
                    MoveLR(-0.576495),
                    DelayOld(30),
                    MoveLR(-0.898068),
                    DelayOld(10),
                    MoveLR(-1),
                    MoveUD(0.905911),
                    DelayOld(20),
                    MoveUD(0.498062),
                    DelayOld(20),
                    MoveUD(0.0274667),
                    MoveUD(-0.403912),
                    DelayOld(20),
                    MoveUD(-1),
                    Run(release=False),
                    Jump(release=False),
                    DelayOld(10),
                    Punch(release=False),
                    DelayOld(70),
                    JumpRelease(),
                    DelayOld(110),
                    MoveLR(-0.976501),
                    RunRelease(),
                    PunchRelease(),
                    DelayOld(10),
                    MoveLR(-0.952971),
                    DelayOld(20),
                    MoveLR(-0.905911),
                    MoveLR(-0.827479),
                    DelayOld(20),
                    MoveLR(-0.75689),
                    DelayOld(30),
                    MoveLR(-0.73336),
                    MoveLR(-0.694143),
                    DelayOld(20),
                    MoveLR(-0.670614),
                    DelayOld(30),
                    MoveLR(-0.66277),
                    DelayOld(10),
                    MoveUD(-0.960784),
                    DelayOld(20),
                    MoveLR(-0.623554),
                    MoveUD(-0.874508),
                    DelayOld(10),
                    MoveLR(-0.545122),
                    MoveUD(-0.694113),
                    DelayOld(20),
                    MoveLR(-0.505905),
                    MoveUD(-0.474502),
                    DelayOld(20),
                    MoveLR(-0.458846),
                    MoveUD(-0.356853),
                    MoveLR(-0.364727),
                    MoveUD(-0.27842),
                    DelayOld(20),
                    MoveLR(0.00390637),
                    Move(0, 0),
                    DelayOld(1000),
                    Text(
                        stdassets.strings.tutorial.phrase09
                    ),  # ah that's better
                    DelayOld(1900),
                    AnalyticsScreen('Tutorial Section 3'),
                    Text(
                        stdassets.strings.tutorial.phrase10
                    ),  # running also helps
                    DelayOld(100),
                    SpawnSpaz(
                        0, (-3.2, 4.3, -4.4), make_current=True, flash=False
                    ),
                    SpawnSpaz(
                        1,
                        (3.3, 4.2, -5.8),
                        make_current=False,
                        color=(0.9, 0.5, 1.0),
                        name=bs.Lstr(resource=f'{self._r}.randomName2Text'),
                    ),
                    DelayOld(1800),
                    Text(
                        stdassets.strings.tutorial.phrase11
                    ),  # hold ANY button to run
                    DelayOld(300),
                    MoveUD(0),
                    DelayOld(20),
                    MoveUD(-0.0520646),
                    DelayOld(20),
                    MoveLR(0),
                    MoveUD(-0.223518),
                    Run(release=False),
                    Jump(release=False),
                    DelayOld(10),
                    MoveLR(0.0980255),
                    MoveUD(-0.309793),
                    DelayOld(30),
                    MoveLR(0.160772),
                    MoveUD(-0.427442),
                    DelayOld(20),
                    MoveLR(0.231361),
                    MoveUD(-0.545091),
                    DelayOld(10),
                    MoveLR(0.317637),
                    MoveUD(-0.678426),
                    DelayOld(20),
                    MoveLR(0.396069),
                    MoveUD(-0.819605),
                    MoveLR(0.482345),
                    MoveUD(-0.913724),
                    DelayOld(20),
                    MoveLR(0.560778),
                    MoveUD(-1),
                    DelayOld(20),
                    MoveLR(0.607837),
                    DelayOld(10),
                    MoveLR(0.623524),
                    DelayOld(30),
                    MoveLR(0.647053),
                    DelayOld(20),
                    MoveLR(0.670583),
                    MoveLR(0.694113),
                    DelayOld(30),
                    MoveLR(0.733329),
                    DelayOld(20),
                    MoveLR(0.764702),
                    MoveLR(0.788232),
                    DelayOld(20),
                    MoveLR(0.827448),
                    DelayOld(10),
                    MoveLR(0.858821),
                    DelayOld(20),
                    MoveLR(0.921567),
                    DelayOld(30),
                    MoveLR(0.97647),
                    MoveLR(1),
                    DelayOld(130),
                    MoveUD(-0.960784),
                    DelayOld(20),
                    MoveUD(-0.921567),
                    DelayOld(30),
                    MoveUD(-0.866665),
                    MoveUD(-0.819605),
                    DelayOld(30),
                    MoveUD(-0.772546),
                    MoveUD(-0.725486),
                    DelayOld(30),
                    MoveUD(-0.631367),
                    DelayOld(10),
                    MoveUD(-0.552934),
                    DelayOld(20),
                    MoveUD(-0.474502),
                    DelayOld(10),
                    MoveUD(-0.403912),
                    DelayOld(30),
                    MoveUD(-0.356853),
                    DelayOld(30),
                    MoveUD(-0.34901),
                    DelayOld(20),
                    MoveUD(-0.333323),
                    DelayOld(20),
                    MoveUD(-0.32548),
                    DelayOld(10),
                    MoveUD(-0.30195),
                    DelayOld(20),
                    MoveUD(-0.27842),
                    DelayOld(30),
                    MoveUD(-0.254891),
                    MoveUD(-0.231361),
                    DelayOld(30),
                    MoveUD(-0.207831),
                    DelayOld(20),
                    MoveUD(-0.199988),
                    MoveUD(-0.176458),
                    DelayOld(30),
                    MoveUD(-0.137242),
                    MoveUD(-0.0823389),
                    DelayOld(20),
                    MoveUD(-0.0274361),
                    DelayOld(20),
                    MoveUD(0.00393689),
                    DelayOld(40),
                    MoveUD(0.0353099),
                    DelayOld(20),
                    MoveUD(0.113742),
                    DelayOld(10),
                    MoveUD(0.137272),
                    DelayOld(20),
                    MoveUD(0.160802),
                    MoveUD(0.184332),
                    DelayOld(20),
                    MoveUD(0.207862),
                    DelayOld(30),
                    MoveUD(0.247078),
                    MoveUD(0.262764),
                    DelayOld(20),
                    MoveUD(0.270608),
                    DelayOld(30),
                    MoveUD(0.294137),
                    MoveUD(0.32551),
                    DelayOld(30),
                    MoveUD(0.37257),
                    Celebrate('left', 1),
                    DelayOld(20),
                    MoveUD(0.498062),
                    MoveUD(0.560808),
                    DelayOld(30),
                    MoveUD(0.654927),
                    MoveUD(0.694143),
                    DelayOld(30),
                    MoveUD(0.741203),
                    DelayOld(20),
                    MoveUD(0.780419),
                    MoveUD(0.819636),
                    DelayOld(20),
                    MoveUD(0.843165),
                    DelayOld(20),
                    MoveUD(0.882382),
                    DelayOld(10),
                    MoveUD(0.913755),
                    DelayOld(30),
                    MoveUD(0.968657),
                    MoveUD(1),
                    DelayOld(560),
                    Punch(release=False),
                    DelayOld(210),
                    MoveUD(0.968657),
                    DelayOld(30),
                    MoveUD(0.75689),
                    PunchRelease(),
                    DelayOld(20),
                    MoveLR(0.95294),
                    MoveUD(0.435316),
                    RunRelease(),
                    JumpRelease(),
                    MoveLR(0.811762),
                    MoveUD(0.270608),
                    DelayOld(20),
                    MoveLR(0.670583),
                    MoveUD(0.160802),
                    DelayOld(20),
                    MoveLR(0.466659),
                    MoveUD(0.0588397),
                    DelayOld(10),
                    MoveLR(0.317637),
                    MoveUD(-0.00390637),
                    DelayOld(20),
                    MoveLR(0.0801416),
                    DelayOld(10),
                    MoveLR(0),
                    DelayOld(20),
                    MoveLR(0),
                    DelayOld(30),
                    MoveLR(0),
                    DelayOld(30),
                    MoveLR(0),
                    DelayOld(20),
                    MoveLR(0),
                    DelayOld(100),
                    MoveLR(0),
                    DelayOld(30),
                    MoveUD(0),
                    DelayOld(30),
                    MoveUD(0),
                    DelayOld(50),
                    MoveUD(0),
                    MoveUD(0),
                    DelayOld(30),
                    MoveLR(0),
                    MoveUD(-0.0520646),
                    MoveLR(0),
                    MoveUD(-0.0640889),
                    DelayOld(20),
                    MoveLR(0),
                    MoveUD(-0.0881375),
                    DelayOld(30),
                    MoveLR(-0.0498978),
                    MoveUD(-0.199988),
                    MoveLR(-0.121586),
                    MoveUD(-0.207831),
                    DelayOld(20),
                    MoveLR(-0.145116),
                    MoveUD(-0.223518),
                    DelayOld(30),
                    MoveLR(-0.152959),
                    MoveUD(-0.231361),
                    MoveLR(-0.192175),
                    MoveUD(-0.262734),
                    DelayOld(30),
                    MoveLR(-0.200018),
                    MoveUD(-0.27842),
                    DelayOld(20),
                    MoveLR(-0.239235),
                    MoveUD(-0.30195),
                    MoveUD(-0.309793),
                    DelayOld(40),
                    MoveUD(-0.333323),
                    DelayOld(10),
                    MoveUD(-0.34901),
                    DelayOld(30),
                    MoveUD(-0.372539),
                    MoveUD(-0.396069),
                    DelayOld(20),
                    MoveUD(-0.443129),
                    DelayOld(20),
                    MoveUD(-0.458815),
                    DelayOld(10),
                    MoveUD(-0.474502),
                    DelayOld(50),
                    MoveUD(-0.482345),
                    DelayOld(30),
                    MoveLR(-0.215705),
                    DelayOld(30),
                    MoveLR(-0.200018),
                    DelayOld(10),
                    MoveLR(-0.192175),
                    DelayOld(10),
                    MoveLR(-0.176489),
                    DelayOld(30),
                    MoveLR(-0.152959),
                    DelayOld(20),
                    MoveLR(-0.145116),
                    MoveLR(-0.121586),
                    MoveUD(-0.458815),
                    DelayOld(30),
                    MoveLR(-0.098056),
                    MoveUD(-0.419599),
                    DelayOld(10),
                    MoveLR(-0.0745262),
                    MoveUD(-0.333323),
                    DelayOld(10),
                    MoveLR(0.00390637),
                    MoveUD(0),
                    DelayOld(990),
                    MoveLR(0),
                    DelayOld(660),
                    MoveUD(0),
                    AnalyticsScreen('Tutorial Section 4'),
                    Text(
                        stdassets.strings.tutorial.phrase12
                    ),  # for extra-awesome punches,...
                    DelayOld(200),
                    SpawnSpaz(
                        0,
                        (
                            2.368781805038452,
                            4.337533950805664,
                            -4.360159873962402,
                        ),
                        make_current=True,
                        flash=False,
                    ),
                    SpawnSpaz(
                        1,
                        (-3.2, 4.3, -4.5),
                        make_current=False,
                        color=(1.0, 0.7, 0.3),
                        # name=R.randomName3Text),
                        name=bs.Lstr(resource=f'{self._r}.randomName3Text'),
                    ),
                    DelayOld(100),
                    Powerup(1, (2.5, 0.0, 0), relative_to=0),
                    Move(1, 0),
                    DelayOld(1700),
                    Move(0, -0.1),
                    DelayOld(100),
                    Move(0, 0),
                    DelayOld(500),
                    DelayOld(320),
                    MoveLR(0),
                    DelayOld(20),
                    MoveLR(0),
                    DelayOld(10),
                    MoveLR(0),
                    DelayOld(20),
                    MoveLR(-0.333354),
                    MoveLR(-0.592181),
                    DelayOld(20),
                    MoveLR(-0.788263),
                    DelayOld(20),
                    MoveLR(-1),
                    MoveUD(0.0353099),
                    MoveUD(0.0588397),
                    DelayOld(10),
                    Run(release=False),
                    DelayOld(780),
                    MoveUD(0.0274667),
                    MoveUD(0.00393689),
                    DelayOld(10),
                    MoveUD(-0.00390637),
                    DelayOld(440),
                    MoveUD(0.0353099),
                    DelayOld(20),
                    MoveUD(0.0588397),
                    DelayOld(10),
                    MoveUD(0.0902127),
                    DelayOld(260),
                    MoveUD(0.0353099),
                    DelayOld(30),
                    MoveUD(0.00393689),
                    DelayOld(10),
                    MoveUD(-0.00390637),
                    MoveUD(-0.0274361),
                    Celebrate('left', 1),
                    DelayOld(10),
                    MoveUD(-0.0823389),
                    DelayOld(30),
                    MoveUD(-0.176458),
                    MoveUD(-0.286264),
                    DelayOld(20),
                    MoveUD(-0.498032),
                    Jump(release=False),
                    MoveUD(-0.764702),
                    DelayOld(30),
                    MoveLR(-0.858852),
                    MoveUD(-1),
                    MoveLR(-0.780419),
                    DelayOld(20),
                    MoveLR(-0.717673),
                    DelayOld(10),
                    MoveLR(-0.552965),
                    DelayOld(10),
                    MoveLR(-0.341197),
                    DelayOld(10),
                    MoveLR(-0.0274667),
                    DelayOld(10),
                    MoveLR(0.27842),
                    DelayOld(20),
                    MoveLR(0.811762),
                    MoveLR(1),
                    RunRelease(),
                    JumpRelease(),
                    DelayOld(260),
                    MoveLR(0.95294),
                    DelayOld(30),
                    MoveLR(0.756859),
                    DelayOld(10),
                    MoveLR(0.317637),
                    MoveLR(-0.00393689),
                    DelayOld(10),
                    MoveLR(-0.341197),
                    DelayOld(10),
                    MoveLR(-0.647084),
                    MoveUD(-0.921567),
                    DelayOld(10),
                    MoveLR(-1),
                    MoveUD(-0.599994),
                    MoveUD(-0.474502),
                    DelayOld(10),
                    MoveUD(-0.309793),
                    DelayOld(10),
                    MoveUD(-0.160772),
                    MoveUD(-0.0352794),
                    Delay(10),
                    MoveUD(0.176489),
                    Delay(10),
                    MoveUD(0.607868),
                    Run(release=False),
                    Jump(release=False),
                    DelayOld(20),
                    MoveUD(1),
                    DelayOld(30),
                    MoveLR(-0.921598),
                    DelayOld(10),
                    Punch(release=False),
                    MoveLR(-0.639241),
                    DelayOld(10),
                    MoveLR(-0.223548),
                    DelayOld(10),
                    MoveLR(0.254891),
                    DelayOld(10),
                    MoveLR(0.741172),
                    MoveLR(1),
                    DelayOld(40),
                    JumpRelease(),
                    DelayOld(40),
                    MoveUD(0.976501),
                    DelayOld(10),
                    MoveUD(0.73336),
                    DelayOld(10),
                    MoveUD(0.309824),
                    DelayOld(20),
                    MoveUD(-0.184301),
                    DelayOld(20),
                    MoveUD(-0.811762),
                    MoveUD(-1),
                    KillSpaz(1, explode=True),
                    DelayOld(10),
                    RunRelease(),
                    PunchRelease(),
                    DelayOld(110),
                    MoveLR(0.97647),
                    MoveLR(0.898038),
                    DelayOld(20),
                    MoveLR(0.788232),
                    DelayOld(20),
                    MoveLR(0.670583),
                    DelayOld(10),
                    MoveLR(0.505875),
                    DelayOld(10),
                    MoveLR(0.32548),
                    DelayOld(20),
                    MoveLR(0.137242),
                    DelayOld(10),
                    MoveLR(-0.00393689),
                    DelayOld(10),
                    MoveLR(-0.215705),
                    MoveLR(-0.356883),
                    DelayOld(20),
                    MoveLR(-0.451003),
                    DelayOld(10),
                    MoveLR(-0.552965),
                    DelayOld(20),
                    MoveLR(-0.670614),
                    MoveLR(-0.780419),
                    DelayOld(10),
                    MoveLR(-0.898068),
                    DelayOld(20),
                    MoveLR(-1),
                    DelayOld(370),
                    MoveLR(-0.976501),
                    DelayOld(10),
                    MoveLR(-0.952971),
                    DelayOld(10),
                    MoveLR(-0.929441),
                    MoveLR(-0.898068),
                    DelayOld(30),
                    MoveLR(-0.874538),
                    DelayOld(10),
                    MoveLR(-0.851009),
                    DelayOld(10),
                    MoveLR(-0.835322),
                    MoveUD(-0.968627),
                    DelayOld(10),
                    MoveLR(-0.827479),
                    MoveUD(-0.960784),
                    DelayOld(20),
                    MoveUD(-0.945097),
                    DelayOld(70),
                    MoveUD(-0.937254),
                    DelayOld(20),
                    MoveUD(-0.913724),
                    DelayOld(20),
                    MoveUD(-0.890194),
                    MoveLR(-0.780419),
                    MoveUD(-0.827448),
                    DelayOld(20),
                    MoveLR(0.317637),
                    MoveUD(0.3961),
                    MoveLR(0.0195929),
                    MoveUD(0.056093),
                    DelayOld(20),
                    MoveUD(0),
                    DelayOld(750),
                    MoveLR(0),
                    Text(
                        bs.Lstr(
                            resource=f'{self._r}.phrase13Text',
                            subs=[
                                (
                                    '${NAME}',
                                    bs.Lstr(
                                        resource=f'{self._r}.randomName3Text'
                                    ),
                                )
                            ],
                        )
                    ),  # whoops sorry bill
                    RemoveGloves(),
                    DelayOld(2000),
                    AnalyticsScreen('Tutorial Section 5'),
                    Text(
                        bs.Lstr(
                            resource=f'{self._r}.phrase14Text',
                            subs=[
                                (
                                    '${NAME}',
                                    bs.Lstr(
                                        resource=f'{self._r}.randomName4Text'
                                    ),
                                )
                            ],
                        )
                    ),  # you can pick up and throw things such as chuck here
                    SpawnSpaz(
                        0,
                        (-4.0, 4.3, -2.5),
                        make_current=True,
                        flash=False,
                        angle=90,
                    ),
                    SpawnSpaz(
                        1,
                        (5, 0, -1.0),
                        relative_to=0,
                        make_current=False,
                        color=(0.4, 1.0, 0.7),
                        name=bs.Lstr(resource=f'{self._r}.randomName4Text'),
                    ),
                    DelayOld(1000),
                    Celebrate('left', 1, duration=1000),
                    Move(1, 0.2),
                    DelayOld(2000),
                    PickUp(),
                    DelayOld(200),
                    Move(0.5, 1.0),
                    DelayOld(1200),
                    PickUp(),
                    Move(0, 0),
                    DelayOld(1000),
                    Celebrate('left'),
                    DelayOld(1500),
                    Move(0, -1.0),
                    DelayOld(800),
                    Move(0, 0),
                    DelayOld(800),
                    SpawnSpaz(
                        0,
                        (1.5, 4.3, -4.0),
                        make_current=True,
                        flash=False,
                        angle=0,
                    ),
                    AnalyticsScreen('Tutorial Section 6'),
                    Text(
                        stdassets.strings.tutorial.phrase15
                    ),  # lastly there's bombs
                    DelayOld(1900),
                    Text(
                        stdassets.strings.tutorial.phrase16
                    ),  # throwing bombs takes practice
                    DelayOld(2000),
                    Bomb(),
                    Move(-0.1, -0.1),
                    DelayOld(100),
                    Move(0, 0),
                    DelayOld(500),
                    DelayOld(1000),
                    Bomb(),
                    DelayOld(2000),
                    Text(
                        stdassets.strings.tutorial.phrase17
                    ),  # not a very good throw
                    DelayOld(3000),
                    Text(
                        stdassets.strings.tutorial.phrase18
                    ),  # moving helps you get distance
                    DelayOld(1000),
                    Bomb(),
                    DelayOld(500),
                    Move(-0.3, 0),
                    DelayOld(100),
                    Move(-0.6, 0),
                    DelayOld(100),
                    Move(-1, 0),
                    DelayOld(800),
                    Bomb(),
                    DelayOld(400),
                    Move(0, -0.1),
                    DelayOld(100),
                    Move(0, 0),
                    DelayOld(2500),
                    Text(
                        stdassets.strings.tutorial.phrase19
                    ),  # jumping helps you get height
                    DelayOld(2000),
                    Bomb(),
                    DelayOld(500),
                    Move(1, 0),
                    DelayOld(300),
                    Jump(release_delay=250),
                    DelayOld(500),
                    Jump(release_delay=250),
                    DelayOld(550),
                    Jump(release_delay=250),
                    DelayOld(160),
                    Punch(),
                    DelayOld(500),
                    Move(0, -0.1),
                    DelayOld(100),
                    Move(0, 0),
                    DelayOld(2000),
                    Text(
                        stdassets.strings.tutorial.phrase20
                    ),  # whiplash your bombs
                    DelayOld(1000),
                    Bomb(release=False),
                    DelayOld2(80),
                    RunRelease(),
                    BombRelease(),
                    DelayOld2(620),
                    MoveLR(0),
                    DelayOld2(10),
                    MoveLR(0),
                    DelayOld2(40),
                    MoveLR(0),
                    DelayOld2(10),
                    MoveLR(-0.0537431),
                    MoveUD(0),
                    DelayOld2(20),
                    MoveLR(-0.262764),
                    DelayOld2(20),
                    MoveLR(-0.498062),
                    DelayOld2(10),
                    MoveLR(-0.639241),
                    DelayOld2(20),
                    MoveLR(-0.73336),
                    DelayOld2(10),
                    MoveLR(-0.843165),
                    MoveUD(-0.0352794),
                    DelayOld2(30),
                    MoveLR(-1),
                    DelayOld2(10),
                    MoveUD(-0.0588092),
                    DelayOld2(10),
                    MoveUD(-0.160772),
                    DelayOld2(20),
                    MoveUD(-0.286264),
                    DelayOld2(20),
                    MoveUD(-0.427442),
                    DelayOld2(10),
                    MoveUD(-0.623524),
                    DelayOld2(20),
                    MoveUD(-0.843135),
                    DelayOld2(10),
                    MoveUD(-1),
                    DelayOld2(40),
                    MoveLR(-0.890225),
                    DelayOld2(10),
                    MoveLR(-0.670614),
                    DelayOld2(20),
                    MoveLR(-0.435316),
                    DelayOld2(20),
                    MoveLR(-0.184332),
                    DelayOld2(10),
                    MoveLR(0.00390637),
                    DelayOld2(20),
                    MoveLR(0.223518),
                    DelayOld2(10),
                    MoveLR(0.388226),
                    DelayOld2(20),
                    MoveLR(0.560778),
                    DelayOld2(20),
                    MoveLR(0.717643),
                    DelayOld2(10),
                    MoveLR(0.890194),
                    DelayOld2(20),
                    MoveLR(1),
                    DelayOld2(30),
                    MoveUD(-0.968627),
                    DelayOld2(20),
                    MoveUD(-0.898038),
                    DelayOld2(10),
                    MoveUD(-0.741172),
                    DelayOld2(20),
                    MoveUD(-0.498032),
                    DelayOld2(20),
                    MoveUD(-0.247047),
                    DelayOld2(10),
                    MoveUD(0.00393689),
                    DelayOld2(20),
                    MoveUD(0.239235),
                    DelayOld2(20),
                    MoveUD(0.458846),
                    DelayOld2(10),
                    MoveUD(0.70983),
                    DelayOld2(30),
                    MoveUD(1),
                    DelayOld2(10),
                    MoveLR(0.827448),
                    DelayOld2(10),
                    MoveLR(0.678426),
                    DelayOld2(20),
                    MoveLR(0.396069),
                    DelayOld2(10),
                    MoveLR(0.0980255),
                    DelayOld2(20),
                    MoveLR(-0.160802),
                    DelayOld2(20),
                    MoveLR(-0.388256),
                    DelayOld2(10),
                    MoveLR(-0.545122),
                    DelayOld2(30),
                    MoveLR(-0.73336),
                    DelayOld2(10),
                    MoveLR(-0.945128),
                    DelayOld2(10),
                    MoveLR(-1),
                    DelayOld2(50),
                    MoveUD(0.960814),
                    DelayOld2(20),
                    MoveUD(0.890225),
                    DelayOld2(10),
                    MoveUD(0.749046),
                    DelayOld2(20),
                    MoveUD(0.623554),
                    DelayOld2(20),
                    MoveUD(0.498062),
                    DelayOld2(10),
                    MoveUD(0.34904),
                    DelayOld2(20),
                    MoveUD(0.239235),
                    DelayOld2(20),
                    MoveUD(0.137272),
                    DelayOld2(10),
                    MoveUD(0.0117801),
                    DelayOld2(20),
                    MoveUD(-0.0117496),
                    DelayOld2(10),
                    MoveUD(-0.0274361),
                    DelayOld2(90),
                    MoveUD(-0.0352794),
                    Run(release=False),
                    Jump(release=False),
                    Delay(80),
                    Punch(release=False),
                    DelayOld2(60),
                    MoveLR(-0.968657),
                    DelayOld2(20),
                    MoveLR(-0.835322),
                    DelayOld2(10),
                    MoveLR(-0.70983),
                    JumpRelease(),
                    DelayOld2(30),
                    MoveLR(-0.592181),
                    MoveUD(-0.0588092),
                    DelayOld2(10),
                    MoveLR(-0.490219),
                    MoveUD(-0.0744957),
                    DelayOld2(10),
                    MoveLR(-0.41963),
                    DelayOld2(20),
                    MoveLR(0),
                    MoveUD(0),
                    DelayOld2(20),
                    MoveUD(0),
                    PunchRelease(),
                    RunRelease(),
                    DelayOld(500),
                    Move(0, -0.1),
                    DelayOld(100),
                    Move(0, 0),
                    DelayOld(2000),
                    AnalyticsScreen('Tutorial Section 7'),
                    Text(
                        stdassets.strings.tutorial.phrase21
                    ),  # timing your bombs can be tricky
                    Move(-1, 0),
                    DelayOld(1000),
                    Move(0, -0.1),
                    DelayOld(100),
                    Move(0, 0),
                    SpawnSpaz(
                        0,
                        (-0.7, 4.3, -3.9),
                        make_current=True,
                        flash=False,
                        angle=-30,
                    ),
                    SpawnSpaz(
                        1,
                        (6.5, 0, -0.75),
                        relative_to=0,
                        make_current=False,
                        color=(0.3, 0.8, 1.0),
                        name=bs.Lstr(resource=f'{self._r}.randomName5Text'),
                    ),
                    DelayOld2(1000),
                    Move(-1, 0),
                    DelayOld2(1800),
                    Bomb(),
                    Move(0, 0),
                    DelayOld2(300),
                    Move(1, 0),
                    DelayOld2(600),
                    Jump(),
                    DelayOld2(150),
                    Punch(),
                    DelayOld2(800),
                    Move(-1, 0),
                    DelayOld2(1000),
                    Move(0, 0),
                    DelayOld2(1500),
                    Text(stdassets.strings.tutorial.phrase22),  # dang
                    Delay(1500),
                    Text(''),
                    Delay(200),
                    Text(
                        stdassets.strings.tutorial.phrase23
                    ),  # try cooking off
                    Delay(1500),
                    Bomb(),
                    Delay(800),
                    Move(1, 0.12),
                    Delay(1100),
                    Jump(),
                    Delay(100),
                    Punch(),
                    Delay(100),
                    Move(0, -0.1),
                    Delay(100),
                    Move(0, 0),
                    Delay(2000),
                    Text(
                        stdassets.strings.tutorial.phrase24
                    ),  # hooray nicely cooked
                    Celebrate(),
                    DelayOld(2000),
                    KillSpaz(1),
                    Text(''),
                    Move(0.5, -0.5),
                    DelayOld(1000),
                    Move(0, -0.1),
                    DelayOld(100),
                    Move(0, 0),
                    DelayOld(1000),
                    AnalyticsScreen('Tutorial Section 8'),
                    Text(
                        stdassets.strings.tutorial.phrase25
                    ),  # well that's just about it
                    DelayOld(2000),
                    Text(
                        stdassets.strings.tutorial.phrase26
                    ),  # go get em tiger
                    DelayOld(2000),
                    Text(
                        stdassets.strings.tutorial.phrase27
                    ),  # remember you training
                    DelayOld(3000),
                    Text(stdassets.strings.tutorial.phrase28),  # well maybe
                    DelayOld(1600),
                    Text(stdassets.strings.tutorial.phrase29),  # good luck
                    Celebrate('right', duration=10000),
                    DelayOld(1000),
                    AnalyticsScreen('Tutorial Complete'),
                    End(),
                ]
            )

        except Exception:
            logging.exception('Error running tutorial.')

        # If we read some, exec them.
        if self._entries:
            self._run_next_entry()
        # Otherwise try again in a few seconds.
        else:
            self._read_entries_timer = bs.Timer(
                3.0, bs.WeakCallStrict(self._read_entries)
            )

    def _run_next_entry(self) -> None:
        while self._entries:
            entry = self._entries.popleft()
            try:
                result = entry.run(self)
            except Exception:
                result = None
                logging.exception('Error in tutorial _run_next_entry.')

            # If the entry returns an int value, set a timer;
            # otherwise just keep going.
            if result is not None:
                self._entry_timer = bs.Timer(
                    result / 1000.0, bs.WeakCallStrict(self._run_next_entry)
                )
                return

        # Done with these entries.. start over soon.
        self._read_entries_timer = bs.Timer(
            1.0, bs.WeakCallStrict(self._read_entries)
        )

    def _update_skip_votes(self) -> None:
        count = sum(1 for player in self.players if player.pressed)
        assert self._skip_count_text
        self._skip_count_text.text = (
            bs.Lstr(
                resource=f'{self._r}.skipVoteCountText',
                subs=[
                    ('${COUNT}', str(count)),
                    ('${TOTAL}', str(len(self.players))),
                ],
            )
            if count > 0
            else ''
        )
        if (
            count >= len(self.players)
            and self.players
            and not self._have_skipped
        ):
            bs.increment_analytics_count('Tutorial skip')
            bs.set_analytics_screen('Tutorial Skip')
            self._have_skipped = True
            builtinassets.audio.swish.play()
            # self._skip_count_text.text = self._r.skippingText
            self._skip_count_text.text = stdassets.strings.tutorial.skipping
            assert self._skip_text
            self._skip_text.text = ''
            self.end()

    def _player_pressed_button(self, player: Player) -> None:
        # Special case: if there's only one player, we give them a
        # warning on their first press (some players were thinking the
        # on-screen guide meant they were supposed to press something).
        if len(self.players) == 1 and not self._issued_warning:
            self._issued_warning = True
            assert self._skip_text
            self._skip_text.text = stdassets.strings.tutorial.skip_confirm
            self._skip_text.color = (1, 1, 1)
            self._skip_text.scale = 1.3
            incr = 50
            t = incr
            for _i in range(6):
                bs.timer(
                    t / 1000.0,
                    bs.CallStrict(
                        setattr, self._skip_text, 'color', (1, 0.5, 0.1)
                    ),
                )
                t += incr
                bs.timer(
                    t / 1000.0,
                    bs.CallStrict(setattr, self._skip_text, 'color', (1, 1, 0)),
                )
                t += incr
            bs.timer(6.0, bs.WeakCallStrict(self._revert_confirm))
            return

        player.pressed = True

        # test...
        if not all(self.players):
            logging.error(
                'Nonexistent player in _player_pressed_button:'
                ' %s: we are %s',
                [str(p) for p in self.players],
                player,
            )

        self._update_skip_votes()

    def _revert_confirm(self) -> None:
        assert self._skip_text
        self._skip_text.text = stdassets.strings.tutorial.to_skip_press_anything
        self._skip_text.color = (1, 1, 1)
        self._issued_warning = False

    @override
    def on_player_join(self, player: Player) -> None:
        super().on_player_join(player)

        # We just wanna know if this player presses anything.
        player.assigninput(
            (
                bs.InputType.JUMP_PRESS,
                bs.InputType.PUNCH_PRESS,
                bs.InputType.BOMB_PRESS,
                bs.InputType.PICK_UP_PRESS,
            ),
            bs.CallStrict(self._player_pressed_button, player),
        )

    @override
    def on_player_leave(self, player: Player) -> None:
        if not all(self.players):
            logging.error(
                'Nonexistent player in on_player_leave: %s: we are %s',
                [str(p) for p in self.players],
                player,
            )
        super().on_player_leave(player)
        # our leaving may influence the vote total needed/etc
        self._update_skip_votes()
