public static void HidePlayerList(NarrativeUi *self)
    {
        var playerAvatars     = self->playerAvatars;
        var playerBubbles     = self->playerBubbles;
        var playerBubbleTexts = self->playerBubbleTexts;

        float Scr2World = Scr.Scr2World;

        var job = EsWorker.PrepBatch(esWorker, Es.CubicOut, PlayerListTransitionDuration);

        for (int i = 0, count = self->playerCount; i < count; i += 1)
        {
            var playerAvatar     = playerAvatarArr[i];
            var playerBubble     = playerBubbleArr[i];
            var playerBubbleText = playerBubbleTextArr[i];

            EsBatchJob.ShiftPos(job, playerAvatar, 0, Scr2World * -PlayerListShiftOffset);
            EsBatchJob.SetOpacity(job, playerAvatar, 0, EsWorker.SetVisibleAtEnd);

            if (playerBubble->isVisible)
            {
                EsBatchJob.ShiftPos(job, playerBubble, 0, Scr2World * -PlayerListShiftOffset);
                EsBatchJob.ShiftPos(job, playerBubbleText, 0, Scr2World * -PlayerListShiftOffset);
                EsBatchJob.SetOpacity(job, playerBubble, 0, EsWorker.SetVisibleAtEnd);
                EsBatchJob.SetOpacity(job, playerBubbleText, 0, EsWorker.SetVisibleAtEnd);
            }
        }
        EsWorker.ExecBatch(esWorker, job);
    }
    public static void ShowPlayerList(NarrativeUi *self)
    {
        var playerAvatars     = self->playerAvatars;
        var playerBubbles     = self->playerBubbles;
        var playerBubbleTexts = self->playerBubbleTexts;

        float Scr2World = Scr.Scr2World;

        var job = EsWorker.PrepBatch(esWorker, Es.CubicOut, PlayerListTransitionDuration);

        for (int i = 0, count = self->playerCount; i < count; i += 1)
        {
            var playerAvatar = playerAvatarArr[i];

            Node.SetPos(playerAvatar, Rel.TopLeft,
                        Scr2World * PlayerAvatarMarginLeft,
                        Scr2World * (-PlayerAvatarMarginTop - i * PlayerAvatarSpacingY));
            Node.SetOpacity(playerAvatar, 0);
            Node.SetVisible(playerAvatar, true);

            EsBatchJob.RestorePos(job, playerAvatar, 0, Scr2World * PlayerListShiftOffset);
            EsBatchJob.SetOpacity(job, playerAvatar, 1);
        }
        EsWorker.ExecBatch(esWorker, job);
    }
    public static void SetPlayerAvatars(NarrativeUi *self, int[] playerAvatarImgIds)
    {
        var playerAvatars     = self->playerAvatars;
        var playerBubbles     = self->playerBubbles;
        var playerBubbleTexts = self->playerBubbleTexts;

        int oldPlayerCount = playerCount;

        playerCount = playerAvatarImgIds.Length;

        float Scr2World = Scr.Scr2World;

        var job = EsWorker.PrepBatch(esWorker, Es.CubicOut, PlayerListTransitionDuration);

        for (int i = 0; i < oldPlayerCount; i += 1)
        {
            var playerAvatar     = playerAvatars + i;
            var playerBubble     = playerBubbles + i;
            var playerBubbleText = playerBubbleTexts + i;

            if (i < playerCount)                // already showing, switch
            {
                TpSprite.SetMeta(playerAvatar, Res.GetTpSpriteMeta(playerAvatarImgIds[i]));
                if (playerAvatar->meta->name != playerAvatarImgIds[i])                    // different image
                {
                    EsBatchJob.RestoreScaleAlt(playerAvatar, Rel.Center, 0, 0, Es.BackOut);
                }
            }
            else                  // not showing any more, hide
            {
                EsBatchJob.SetScale(playerAvatar, Rel.Center, 0, 0, EsWorker.SetVisibleAtEnd);
                EsBatchJob.ShiftPos(playerAvatar, 0, Scr2World * PlayerListShiftOffset);
            }

            if (playerBubble->isVisible)                // hide all bubbles
            {
                EsBatchJob.ShiftPos(job, playerBubble, 0, Scr2World * -PlayerListShiftOffset);
                EsBatchJob.ShiftPos(job, playerBubbleText, 0, Scr2World * -PlayerListShiftOffset);
                EsBatchJob.SetOpacity(job, playerBubble, 0, EsWorker.SetVisibleAtEnd);
                EsBatchJob.SetOpacity(job, playerBubbleText, 0, EsWorker.SetVisibleAtEnd);
            }
        }

        for (int i = oldPlayerCount; i < playerCount; i += 1)            // not showing, show
        {
            var playerAvatar = playerAvatarArr[i];

            Node.SetMeta(playerAvatar, Res.GetTpSpriteMeta(playerAvatarImgIds[i]));
            Node.SetOpacity(playerAvatar, 0);
            Node.SetVisible(playerAvatar, true);
            EsBatchJob.RestoreScaleAlt(playerAvatar, Rel.Center, 0, 0, Es.BackOut);
            EsBatchJob.SetOpacity(playerAvatar, 1);
        }
        EsWorker.ExecBatch(esWorker, job);
    }
    public static void HidePlayerBubble(NarrativeUi *self, int playerIdx)
    {
        var playerAvatars     = self->playerAvatars;
        var playerBubbles     = self->playerBubbles;
        var playerBubbleTexts = self->playerBubbleTexts;

        float Scr2World = Scr.Scr2World;

        var job = EsWorker.PrepBatch(esWorker, Es.CubicOut, PlayerListTransitionDuration);

        EsBatchJob.ShiftPos(job, playerBubble, Scr2World * -PlayerListShiftOffset, 0);
        EsBatchJob.ShiftPos(job, playerBubbleText, Scr2World * -PlayerListShiftOffset, 0);
        EsBatchJob.SetOpacity(job, playerBubble, 0, EsWorker.SetVisibleAtEnd);
        EsBatchJob.SetOpacity(job, playerBubbleText, 0, EsWorker.SetVisibleAtEnd);
        EsWorker.ExecBatch(esWorker, job);
    }
    public static void SetPlayerBubbleText(NarrativeUi *self, int playerIdx, string message)
    {
        var playerAvatar     = self->playerAvatars[playerIdx];
        var playerBubble     = self->playerBubbles[playerIdx];
        var playerBubbleText = self->playerBubbleTexts[playerIdx];

        float Scr2World = Scr.Scr2World;

        Node.SetText(playerBubbleText, message);
        float height = Node.GetHeight(playerBubbleText);

        Node.SetScale(playerBubbleText, 1, .5f);
        Node.SetOpacity(playerBubbleText, 0);
        Node.SetVisible(playerBubbleText, true);

        height += Scr2World * (PlayerBubbleTextMarginTop + PlayerBubbleTextMarginTop);

        var job = EsWorker.PrepBatch(esWorker, Es.CubicOut, PlayerListTransitionDuration);

        if (!playerBubble->isVisible)            // not visible -> fade in from top
        {
            Node.SetHeight(playerBubble, heigh);
            Node.SetOpacity(playerBubble, 0);
            Node.SetVisible(playerBubble, true);

            EsBatchJob.RestorePos(playerBubble, 0, Scr2World * PlayerListShiftOffset);
            EsBatchJob.RestorePos(playerBubbleText, 0, Scr2World * PlayerListShiftOffset);
            EsBatchJob.SetOpacity(playerBubble, 1);
            EsBatchJob.SetOpacity(playerBubbleText, 1);
            EsBatchJob.SetScaleAlt(playerBubbleText, 1, 1, Es.BackOut);
        }
        else              // already visible -> ease height
        {
            EsBatchJob.SetHeight(playerBubble, heigh);
            EsBatchJob.SetOpacity(playerBubbleText, 1);
            EsBatchJob.SetScaleAlt(playerBubbleText, 1, 1, Es.BackOut);
        }
        EsWorker.ExecBatch(esWorker, job);
    }