bauiv1lib.partyqueue

UI related to waiting in line for a party.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""UI related to waiting in line for a party."""
  4
  5from __future__ import annotations
  6
  7import time
  8import random
  9import logging
 10from typing import TYPE_CHECKING
 11
 12import bauiv1 as bui
 13import bascenev1 as bs
 14
 15if TYPE_CHECKING:
 16    from typing import Any, Sequence
 17
 18
 19class PartyQueueWindow(bui.Window):
 20    """Window showing players waiting to join a server."""
 21
 22    # Ewww this needs quite a bit of de-linting if/when i revisit it..
 23    # pylint: disable=consider-using-dict-comprehension
 24    class Dude:
 25        """Represents a single dude waiting in a server line."""
 26
 27        def __init__(
 28            self,
 29            parent: PartyQueueWindow,
 30            distance: float,
 31            initial_offset: float,
 32            is_player: bool,
 33            account_id: str,
 34            name: str,
 35        ):
 36            # pylint: disable=too-many-positional-arguments
 37            self.claimed = False
 38            self._line_left = parent.get_line_left()
 39            self._line_width = parent.get_line_width()
 40            self._line_bottom = parent.get_line_bottom()
 41            self._target_distance = distance
 42            self._distance = distance + initial_offset
 43            self._boost_brightness = 0.0
 44            self._debug = False
 45            self._sc = sc = 1.1 if is_player else 0.6 + random.random() * 0.2
 46            self._y_offs = -30.0 if is_player else -47.0 * sc
 47            self._last_boost_time = 0.0
 48            self._color = (
 49                (0.2, 1.0, 0.2)
 50                if is_player
 51                else (
 52                    0.5 + 0.3 * random.random(),
 53                    0.4 + 0.2 * random.random(),
 54                    0.5 + 0.3 * random.random(),
 55                )
 56            )
 57            self._eye_color = (
 58                0.7 * 1.0 + 0.3 * self._color[0],
 59                0.7 * 1.0 + 0.3 * self._color[1],
 60                0.7 * 1.0 + 0.3 * self._color[2],
 61            )
 62            self._body_image = bui.buttonwidget(
 63                parent=parent.get_root_widget(),
 64                selectable=True,
 65                label='',
 66                size=(sc * 60, sc * 80),
 67                color=self._color,
 68                texture=parent.lineup_tex,
 69                mesh_transparent=parent.lineup_1_transparent_mesh,
 70            )
 71            bui.buttonwidget(
 72                edit=self._body_image,
 73                on_activate_call=bui.WeakCall(
 74                    parent.on_account_press, account_id, self._body_image
 75                ),
 76            )
 77            bui.widget(edit=self._body_image, autoselect=True)
 78            self._eyes_image = bui.imagewidget(
 79                parent=parent.get_root_widget(),
 80                size=(sc * 36, sc * 18),
 81                texture=parent.lineup_tex,
 82                color=self._eye_color,
 83                mesh_transparent=parent.eyes_mesh,
 84            )
 85            self._name_text = bui.textwidget(
 86                parent=parent.get_root_widget(),
 87                size=(0, 0),
 88                shadow=0,
 89                flatness=1.0,
 90                text=name,
 91                maxwidth=100,
 92                h_align='center',
 93                v_align='center',
 94                scale=0.75,
 95                color=(1, 1, 1, 0.6),
 96            )
 97            self._update_image()
 98
 99            # DEBUG: vis target pos..
100            self._body_image_target: bui.Widget | None
101            self._eyes_image_target: bui.Widget | None
102            if self._debug:
103                self._body_image_target = bui.imagewidget(
104                    parent=parent.get_root_widget(),
105                    size=(sc * 60, sc * 80),
106                    color=self._color,
107                    texture=parent.lineup_tex,
108                    mesh_transparent=parent.lineup_1_transparent_mesh,
109                )
110                self._eyes_image_target = bui.imagewidget(
111                    parent=parent.get_root_widget(),
112                    size=(sc * 36, sc * 18),
113                    texture=parent.lineup_tex,
114                    color=self._eye_color,
115                    mesh_transparent=parent.eyes_mesh,
116                )
117                # (updates our image positions)
118                self.set_target_distance(self._target_distance)
119            else:
120                self._body_image_target = self._eyes_image_target = None
121
122        def __del__(self) -> None:
123            # ew.  our destructor here may get called as part of an internal
124            # widget tear-down.
125            # running further widget calls here can quietly break stuff, so we
126            # need to push a deferred call to kill these as necessary instead.
127            # (should bulletproof internal widget code to give a clean error
128            # in this case)
129            def kill_widgets(widgets: Sequence[bui.Widget | None]) -> None:
130                for widget in widgets:
131                    if widget:
132                        widget.delete()
133
134            bui.pushcall(
135                bui.Call(
136                    kill_widgets,
137                    [
138                        self._body_image,
139                        self._eyes_image,
140                        self._body_image_target,
141                        self._eyes_image_target,
142                        self._name_text,
143                    ],
144                )
145            )
146
147        def set_target_distance(self, dist: float) -> None:
148            """Set distance for a dude."""
149            self._target_distance = dist
150            if self._debug:
151                sc = self._sc
152                position = (
153                    self._line_left
154                    + self._line_width * (1.0 - self._target_distance),
155                    self._line_bottom - 30,
156                )
157                bui.imagewidget(
158                    edit=self._body_image_target,
159                    position=(
160                        position[0] - sc * 30,
161                        position[1] - sc * 25 - 70,
162                    ),
163                )
164                bui.imagewidget(
165                    edit=self._eyes_image_target,
166                    position=(
167                        position[0] - sc * 18,
168                        position[1] + sc * 31 - 70,
169                    ),
170                )
171
172        def step(self, smoothing: float) -> None:
173            """Step this dude."""
174            self._distance = (
175                smoothing * self._distance
176                + (1.0 - smoothing) * self._target_distance
177            )
178            self._update_image()
179            self._boost_brightness *= 0.9
180
181        def _update_image(self) -> None:
182            sc = self._sc
183            position = (
184                self._line_left + self._line_width * (1.0 - self._distance),
185                self._line_bottom + 40,
186            )
187            brightness = 1.0 + self._boost_brightness
188            bui.buttonwidget(
189                edit=self._body_image,
190                position=(
191                    position[0] - sc * 30,
192                    position[1] - sc * 25 + self._y_offs,
193                ),
194                color=(
195                    self._color[0] * brightness,
196                    self._color[1] * brightness,
197                    self._color[2] * brightness,
198                ),
199            )
200            bui.imagewidget(
201                edit=self._eyes_image,
202                position=(
203                    position[0] - sc * 18,
204                    position[1] + sc * 31 + self._y_offs,
205                ),
206                color=(
207                    self._eye_color[0] * brightness,
208                    self._eye_color[1] * brightness,
209                    self._eye_color[2] * brightness,
210                ),
211            )
212            bui.textwidget(
213                edit=self._name_text,
214                position=(position[0] - sc * 0, position[1] + sc * 40.0),
215            )
216
217        def boost(self, amount: float, smoothing: float) -> None:
218            """Boost this dude."""
219            del smoothing  # unused arg
220            self._distance = max(0.0, self._distance - amount)
221            self._update_image()
222            self._last_boost_time = time.time()
223            self._boost_brightness += 0.6
224
225    def __init__(self, queue_id: str, address: str, port: int):
226        assert bui.app.classic is not None
227        self._address = address
228        self._port = port
229        self._queue_id = queue_id
230        self._width = 800
231        self._height = 400
232        self._last_connect_attempt_time: float | None = None
233        self._last_transaction_time: float | None = None
234        self._boost_button: bui.Widget | None = None
235        self._boost_price: bui.Widget | None = None
236        self._boost_label: bui.Widget | None = None
237        self._field_shown = False
238        self._dudes: list[PartyQueueWindow.Dude] = []
239        self._dudes_by_id: dict[int, PartyQueueWindow.Dude] = {}
240        self._line_left = 40.0
241        self._line_width = self._width - 190
242        self._line_bottom = self._height * 0.4
243        self.lineup_tex: bui.Texture = bui.gettexture('playerLineup')
244        self._smoothing = 0.0
245        self._initial_offset = 0.0
246        self._boost_tickets = 0
247        self._boost_strength = 0.0
248        self._angry_computer_transparent_mesh = bui.getmesh(
249            'angryComputerTransparent'
250        )
251        self._angry_computer_image: bui.Widget | None = None
252        self.lineup_1_transparent_mesh: bui.Mesh = bui.getmesh(
253            'playerLineup1Transparent'
254        )
255        self._lineup_2_transparent_mesh: bui.Mesh = bui.getmesh(
256            'playerLineup2Transparent'
257        )
258
259        self._lineup_3_transparent_mesh = bui.getmesh(
260            'playerLineup3Transparent'
261        )
262        self._lineup_4_transparent_mesh = bui.getmesh(
263            'playerLineup4Transparent'
264        )
265        self._line_image: bui.Widget | None = None
266        self.eyes_mesh: bui.Mesh = bui.getmesh('plasticEyesTransparent')
267        self._white_tex = bui.gettexture('white')
268        uiscale = bui.app.ui_v1.uiscale
269        super().__init__(
270            root_widget=bui.containerwidget(
271                size=(self._width, self._height),
272                color=(0.45, 0.63, 0.15),
273                transition='in_scale',
274                scale=(
275                    1.4
276                    if uiscale is bui.UIScale.SMALL
277                    else 1.2 if uiscale is bui.UIScale.MEDIUM else 1.0
278                ),
279            )
280        )
281
282        self._cancel_button = bui.buttonwidget(
283            parent=self._root_widget,
284            scale=1.0,
285            position=(60, self._height - 80),
286            size=(50, 50),
287            label='',
288            on_activate_call=self.close,
289            autoselect=True,
290            color=(0.45, 0.63, 0.15),
291            icon=bui.gettexture('crossOut'),
292            iconscale=1.2,
293        )
294        bui.containerwidget(
295            edit=self._root_widget, cancel_button=self._cancel_button
296        )
297
298        self._title_text = bui.textwidget(
299            parent=self._root_widget,
300            position=(self._width * 0.5, self._height * 0.55),
301            size=(0, 0),
302            color=(1.0, 3.0, 1.0),
303            scale=1.3,
304            h_align='center',
305            v_align='center',
306            text=bui.Lstr(resource='internal.connectingToPartyText'),
307            maxwidth=self._width * 0.65,
308        )
309
310        self._tickets_text = bui.textwidget(
311            parent=self._root_widget,
312            position=(self._width - 180, self._height - 20),
313            size=(0, 0),
314            color=(0.2, 1.0, 0.2),
315            scale=0.7,
316            h_align='center',
317            v_align='center',
318            text='',
319        )
320
321        # update at roughly 30fps
322        self._update_timer = bui.AppTimer(
323            0.033, bui.WeakCall(self.update), repeat=True
324        )
325        self.update()
326
327    def __del__(self) -> None:
328        try:
329            plus = bui.app.plus
330            assert plus is not None
331
332            plus.add_v1_account_transaction(
333                {'type': 'PARTY_QUEUE_REMOVE', 'q': self._queue_id}
334            )
335            plus.run_v1_account_transactions()
336        except Exception:
337            logging.exception('Error removing self from party queue.')
338
339    def get_line_left(self) -> float:
340        """(internal)"""
341        return self._line_left
342
343    def get_line_width(self) -> float:
344        """(internal)"""
345        return self._line_width
346
347    def get_line_bottom(self) -> float:
348        """(internal)"""
349        return self._line_bottom
350
351    def on_account_press(
352        self, account_id: str | None, origin_widget: bui.Widget
353    ) -> None:
354        """A dude was clicked so we should show his account info."""
355        from bauiv1lib.account.viewer import AccountViewerWindow
356
357        if account_id is None:
358            bui.getsound('error').play()
359            return
360        AccountViewerWindow(
361            account_id=account_id,
362            position=origin_widget.get_screen_space_center(),
363        )
364
365    def close(self) -> None:
366        """Close the ui."""
367        bui.containerwidget(edit=self._root_widget, transition='out_scale')
368
369    def _update_field(self, response: dict[str, Any]) -> None:
370        plus = bui.app.plus
371        assert plus is not None
372
373        if self._angry_computer_image is None:
374            self._angry_computer_image = bui.imagewidget(
375                parent=self._root_widget,
376                position=(self._width - 180, self._height * 0.5 - 65),
377                size=(150, 150),
378                texture=self.lineup_tex,
379                mesh_transparent=self._angry_computer_transparent_mesh,
380            )
381        if self._line_image is None:
382            self._line_image = bui.imagewidget(
383                parent=self._root_widget,
384                color=(0.0, 0.0, 0.0),
385                opacity=0.2,
386                position=(self._line_left, self._line_bottom - 2.0),
387                size=(self._line_width, 4.0),
388                texture=self._white_tex,
389            )
390
391        # now go through the data they sent, creating dudes for us and our
392        # enemies as needed and updating target positions on all of them..
393
394        # mark all as unclaimed so we know which ones to kill off..
395        for dude in self._dudes:
396            dude.claimed = False
397
398        # always have a dude for ourself..
399        if -1 not in self._dudes_by_id:
400            dude = self.Dude(
401                self,
402                response['d'],
403                self._initial_offset,
404                True,
405                plus.get_v1_account_misc_read_val_2('resolvedAccountID', None),
406                plus.get_v1_account_display_string(),
407            )
408            self._dudes_by_id[-1] = dude
409            self._dudes.append(dude)
410        else:
411            self._dudes_by_id[-1].set_target_distance(response['d'])
412        self._dudes_by_id[-1].claimed = True
413
414        # now create/destroy enemies
415        for (
416            enemy_id,
417            enemy_distance,
418            enemy_account_id,
419            enemy_name,
420        ) in response['e']:
421            if enemy_id not in self._dudes_by_id:
422                dude = self.Dude(
423                    self,
424                    enemy_distance,
425                    self._initial_offset,
426                    False,
427                    enemy_account_id,
428                    enemy_name,
429                )
430                self._dudes_by_id[enemy_id] = dude
431                self._dudes.append(dude)
432            else:
433                self._dudes_by_id[enemy_id].set_target_distance(enemy_distance)
434            self._dudes_by_id[enemy_id].claimed = True
435
436        # remove unclaimed dudes from both of our lists
437        # noinspection PyUnresolvedReferences
438        self._dudes_by_id = dict(
439            [
440                item
441                for item in list(self._dudes_by_id.items())
442                if item[1].claimed
443            ]
444        )
445        self._dudes = [dude for dude in self._dudes if dude.claimed]
446
447    def _hide_field(self) -> None:
448        if self._angry_computer_image:
449            self._angry_computer_image.delete()
450        self._angry_computer_image = None
451        if self._line_image:
452            self._line_image.delete()
453        self._line_image = None
454        self._dudes = []
455        self._dudes_by_id = {}
456
457    def on_update_response(self, response: dict[str, Any] | None) -> None:
458        """We've received a response from an update to the server."""
459        # pylint: disable=too-many-branches
460        if not self._root_widget:
461            return
462
463        # Seeing this in logs; debugging...
464        if not self._title_text:
465            print('PartyQueueWindows update: Have root but no title_text.')
466            return
467
468        if response is not None:
469            should_show_field = response.get('d') is not None
470            self._smoothing = response['s']
471            self._initial_offset = response['o']
472
473            # If they gave us a position, show the field.
474            if should_show_field:
475                bui.textwidget(
476                    edit=self._title_text,
477                    text=bui.Lstr(resource='waitingInLineText'),
478                    position=(self._width * 0.5, self._height * 0.85),
479                )
480                self._update_field(response)
481                self._field_shown = True
482            if not should_show_field and self._field_shown:
483                bui.textwidget(
484                    edit=self._title_text,
485                    text=bui.Lstr(resource='internal.connectingToPartyText'),
486                    position=(self._width * 0.5, self._height * 0.55),
487                )
488                self._hide_field()
489                self._field_shown = False
490
491            # if they told us there's a boost button, update..
492            if response.get('bt') is not None:
493                self._boost_tickets = response['bt']
494                self._boost_strength = response['ba']
495                if self._boost_button is None:
496                    self._boost_button = bui.buttonwidget(
497                        parent=self._root_widget,
498                        scale=1.0,
499                        position=(self._width * 0.5 - 75, 20),
500                        size=(150, 100),
501                        button_type='square',
502                        label='',
503                        on_activate_call=self.on_boost_press,
504                        enable_sound=False,
505                        color=(0, 1, 0),
506                        autoselect=True,
507                    )
508                    self._boost_label = bui.textwidget(
509                        parent=self._root_widget,
510                        draw_controller=self._boost_button,
511                        position=(self._width * 0.5, 88),
512                        size=(0, 0),
513                        color=(0.8, 1.0, 0.8),
514                        scale=1.5,
515                        h_align='center',
516                        v_align='center',
517                        text=bui.Lstr(resource='boostText'),
518                        maxwidth=150,
519                    )
520                    self._boost_price = bui.textwidget(
521                        parent=self._root_widget,
522                        draw_controller=self._boost_button,
523                        position=(self._width * 0.5, 50),
524                        size=(0, 0),
525                        color=(0, 1, 0),
526                        scale=0.9,
527                        h_align='center',
528                        v_align='center',
529                        text=bui.charstr(bui.SpecialChar.TICKET)
530                        + str(self._boost_tickets),
531                        maxwidth=150,
532                    )
533            else:
534                if self._boost_button is not None:
535                    self._boost_button.delete()
536                    self._boost_button = None
537                if self._boost_price is not None:
538                    self._boost_price.delete()
539                    self._boost_price = None
540                if self._boost_label is not None:
541                    self._boost_label.delete()
542                    self._boost_label = None
543
544            # if they told us to go ahead and try and connect, do so..
545            # (note: servers will disconnect us if we try to connect before
546            # getting this go-ahead, so don't get any bright ideas...)
547            if response.get('c', False):
548                # enforce a delay between connection attempts
549                # (in case they're jamming on the boost button)
550                now = time.time()
551                if (
552                    self._last_connect_attempt_time is None
553                    or now - self._last_connect_attempt_time > 10.0
554                ):
555
556                    # Store UI location to return to when done.
557                    if bs.app.classic is not None:
558                        bs.app.classic.save_ui_state()
559
560                    bs.connect_to_party(
561                        address=self._address,
562                        port=self._port,
563                        print_progress=False,
564                    )
565                    self._last_connect_attempt_time = now
566
567    def on_boost_press(self) -> None:
568        """Boost was pressed."""
569        from bauiv1lib import account
570
571        # from bauiv1lib import gettickets
572
573        plus = bui.app.plus
574        assert plus is not None
575
576        if plus.get_v1_account_state() != 'signed_in':
577            account.show_sign_in_prompt()
578            return
579
580        if plus.get_v1_account_ticket_count() < self._boost_tickets:
581            bui.getsound('error').play()
582            # gettickets.show_get_tickets_prompt()
583            return
584
585        bui.getsound('laserReverse').play()
586        plus.add_v1_account_transaction(
587            {
588                'type': 'PARTY_QUEUE_BOOST',
589                't': self._boost_tickets,
590                'q': self._queue_id,
591            },
592            callback=bui.WeakCall(self.on_update_response),
593        )
594        # lets not run these immediately (since they may be rapid-fire,
595        # just bucket them until the next tick)
596
597        # the transaction handles the local ticket change, but we apply our
598        # local boost vis manually here..
599        # (our visualization isn't really wired up to be transaction-based)
600        our_dude = self._dudes_by_id.get(-1)
601        if our_dude is not None:
602            our_dude.boost(self._boost_strength, self._smoothing)
603
604    def update(self) -> None:
605        """Update!"""
606        plus = bui.app.plus
607        assert plus is not None
608
609        if not self._root_widget:
610            return
611
612        # Update boost-price.
613        if self._boost_price is not None:
614            bui.textwidget(
615                edit=self._boost_price,
616                text=bui.charstr(bui.SpecialChar.TICKET)
617                + str(self._boost_tickets),
618            )
619
620        # Update boost button color based on if we have enough moola.
621        if self._boost_button is not None:
622            can_boost = (
623                plus.get_v1_account_state() == 'signed_in'
624                and plus.get_v1_account_ticket_count() >= self._boost_tickets
625            )
626            bui.buttonwidget(
627                edit=self._boost_button,
628                color=(0, 1, 0) if can_boost else (0.7, 0.7, 0.7),
629            )
630
631        # Update ticket-count.
632        if self._tickets_text is not None:
633            if self._boost_button is not None:
634                if plus.get_v1_account_state() == 'signed_in':
635                    val = bui.charstr(bui.SpecialChar.TICKET) + str(
636                        plus.get_v1_account_ticket_count()
637                    )
638                else:
639                    val = bui.charstr(bui.SpecialChar.TICKET) + '???'
640                bui.textwidget(edit=self._tickets_text, text=val)
641            else:
642                bui.textwidget(edit=self._tickets_text, text='')
643
644        current_time = bui.apptime()
645        if (
646            self._last_transaction_time is None
647            or current_time - self._last_transaction_time
648            > 0.001 * plus.get_v1_account_misc_read_val('pqInt', 5000)
649        ):
650            self._last_transaction_time = current_time
651            plus.add_v1_account_transaction(
652                {'type': 'PARTY_QUEUE_QUERY', 'q': self._queue_id},
653                callback=bui.WeakCall(self.on_update_response),
654            )
655            plus.run_v1_account_transactions()
656
657        # step our dudes
658        for dude in self._dudes:
659            dude.step(self._smoothing)
class PartyQueueWindow(bauiv1._uitypes.Window):
 20class PartyQueueWindow(bui.Window):
 21    """Window showing players waiting to join a server."""
 22
 23    # Ewww this needs quite a bit of de-linting if/when i revisit it..
 24    # pylint: disable=consider-using-dict-comprehension
 25    class Dude:
 26        """Represents a single dude waiting in a server line."""
 27
 28        def __init__(
 29            self,
 30            parent: PartyQueueWindow,
 31            distance: float,
 32            initial_offset: float,
 33            is_player: bool,
 34            account_id: str,
 35            name: str,
 36        ):
 37            # pylint: disable=too-many-positional-arguments
 38            self.claimed = False
 39            self._line_left = parent.get_line_left()
 40            self._line_width = parent.get_line_width()
 41            self._line_bottom = parent.get_line_bottom()
 42            self._target_distance = distance
 43            self._distance = distance + initial_offset
 44            self._boost_brightness = 0.0
 45            self._debug = False
 46            self._sc = sc = 1.1 if is_player else 0.6 + random.random() * 0.2
 47            self._y_offs = -30.0 if is_player else -47.0 * sc
 48            self._last_boost_time = 0.0
 49            self._color = (
 50                (0.2, 1.0, 0.2)
 51                if is_player
 52                else (
 53                    0.5 + 0.3 * random.random(),
 54                    0.4 + 0.2 * random.random(),
 55                    0.5 + 0.3 * random.random(),
 56                )
 57            )
 58            self._eye_color = (
 59                0.7 * 1.0 + 0.3 * self._color[0],
 60                0.7 * 1.0 + 0.3 * self._color[1],
 61                0.7 * 1.0 + 0.3 * self._color[2],
 62            )
 63            self._body_image = bui.buttonwidget(
 64                parent=parent.get_root_widget(),
 65                selectable=True,
 66                label='',
 67                size=(sc * 60, sc * 80),
 68                color=self._color,
 69                texture=parent.lineup_tex,
 70                mesh_transparent=parent.lineup_1_transparent_mesh,
 71            )
 72            bui.buttonwidget(
 73                edit=self._body_image,
 74                on_activate_call=bui.WeakCall(
 75                    parent.on_account_press, account_id, self._body_image
 76                ),
 77            )
 78            bui.widget(edit=self._body_image, autoselect=True)
 79            self._eyes_image = bui.imagewidget(
 80                parent=parent.get_root_widget(),
 81                size=(sc * 36, sc * 18),
 82                texture=parent.lineup_tex,
 83                color=self._eye_color,
 84                mesh_transparent=parent.eyes_mesh,
 85            )
 86            self._name_text = bui.textwidget(
 87                parent=parent.get_root_widget(),
 88                size=(0, 0),
 89                shadow=0,
 90                flatness=1.0,
 91                text=name,
 92                maxwidth=100,
 93                h_align='center',
 94                v_align='center',
 95                scale=0.75,
 96                color=(1, 1, 1, 0.6),
 97            )
 98            self._update_image()
 99
100            # DEBUG: vis target pos..
101            self._body_image_target: bui.Widget | None
102            self._eyes_image_target: bui.Widget | None
103            if self._debug:
104                self._body_image_target = bui.imagewidget(
105                    parent=parent.get_root_widget(),
106                    size=(sc * 60, sc * 80),
107                    color=self._color,
108                    texture=parent.lineup_tex,
109                    mesh_transparent=parent.lineup_1_transparent_mesh,
110                )
111                self._eyes_image_target = bui.imagewidget(
112                    parent=parent.get_root_widget(),
113                    size=(sc * 36, sc * 18),
114                    texture=parent.lineup_tex,
115                    color=self._eye_color,
116                    mesh_transparent=parent.eyes_mesh,
117                )
118                # (updates our image positions)
119                self.set_target_distance(self._target_distance)
120            else:
121                self._body_image_target = self._eyes_image_target = None
122
123        def __del__(self) -> None:
124            # ew.  our destructor here may get called as part of an internal
125            # widget tear-down.
126            # running further widget calls here can quietly break stuff, so we
127            # need to push a deferred call to kill these as necessary instead.
128            # (should bulletproof internal widget code to give a clean error
129            # in this case)
130            def kill_widgets(widgets: Sequence[bui.Widget | None]) -> None:
131                for widget in widgets:
132                    if widget:
133                        widget.delete()
134
135            bui.pushcall(
136                bui.Call(
137                    kill_widgets,
138                    [
139                        self._body_image,
140                        self._eyes_image,
141                        self._body_image_target,
142                        self._eyes_image_target,
143                        self._name_text,
144                    ],
145                )
146            )
147
148        def set_target_distance(self, dist: float) -> None:
149            """Set distance for a dude."""
150            self._target_distance = dist
151            if self._debug:
152                sc = self._sc
153                position = (
154                    self._line_left
155                    + self._line_width * (1.0 - self._target_distance),
156                    self._line_bottom - 30,
157                )
158                bui.imagewidget(
159                    edit=self._body_image_target,
160                    position=(
161                        position[0] - sc * 30,
162                        position[1] - sc * 25 - 70,
163                    ),
164                )
165                bui.imagewidget(
166                    edit=self._eyes_image_target,
167                    position=(
168                        position[0] - sc * 18,
169                        position[1] + sc * 31 - 70,
170                    ),
171                )
172
173        def step(self, smoothing: float) -> None:
174            """Step this dude."""
175            self._distance = (
176                smoothing * self._distance
177                + (1.0 - smoothing) * self._target_distance
178            )
179            self._update_image()
180            self._boost_brightness *= 0.9
181
182        def _update_image(self) -> None:
183            sc = self._sc
184            position = (
185                self._line_left + self._line_width * (1.0 - self._distance),
186                self._line_bottom + 40,
187            )
188            brightness = 1.0 + self._boost_brightness
189            bui.buttonwidget(
190                edit=self._body_image,
191                position=(
192                    position[0] - sc * 30,
193                    position[1] - sc * 25 + self._y_offs,
194                ),
195                color=(
196                    self._color[0] * brightness,
197                    self._color[1] * brightness,
198                    self._color[2] * brightness,
199                ),
200            )
201            bui.imagewidget(
202                edit=self._eyes_image,
203                position=(
204                    position[0] - sc * 18,
205                    position[1] + sc * 31 + self._y_offs,
206                ),
207                color=(
208                    self._eye_color[0] * brightness,
209                    self._eye_color[1] * brightness,
210                    self._eye_color[2] * brightness,
211                ),
212            )
213            bui.textwidget(
214                edit=self._name_text,
215                position=(position[0] - sc * 0, position[1] + sc * 40.0),
216            )
217
218        def boost(self, amount: float, smoothing: float) -> None:
219            """Boost this dude."""
220            del smoothing  # unused arg
221            self._distance = max(0.0, self._distance - amount)
222            self._update_image()
223            self._last_boost_time = time.time()
224            self._boost_brightness += 0.6
225
226    def __init__(self, queue_id: str, address: str, port: int):
227        assert bui.app.classic is not None
228        self._address = address
229        self._port = port
230        self._queue_id = queue_id
231        self._width = 800
232        self._height = 400
233        self._last_connect_attempt_time: float | None = None
234        self._last_transaction_time: float | None = None
235        self._boost_button: bui.Widget | None = None
236        self._boost_price: bui.Widget | None = None
237        self._boost_label: bui.Widget | None = None
238        self._field_shown = False
239        self._dudes: list[PartyQueueWindow.Dude] = []
240        self._dudes_by_id: dict[int, PartyQueueWindow.Dude] = {}
241        self._line_left = 40.0
242        self._line_width = self._width - 190
243        self._line_bottom = self._height * 0.4
244        self.lineup_tex: bui.Texture = bui.gettexture('playerLineup')
245        self._smoothing = 0.0
246        self._initial_offset = 0.0
247        self._boost_tickets = 0
248        self._boost_strength = 0.0
249        self._angry_computer_transparent_mesh = bui.getmesh(
250            'angryComputerTransparent'
251        )
252        self._angry_computer_image: bui.Widget | None = None
253        self.lineup_1_transparent_mesh: bui.Mesh = bui.getmesh(
254            'playerLineup1Transparent'
255        )
256        self._lineup_2_transparent_mesh: bui.Mesh = bui.getmesh(
257            'playerLineup2Transparent'
258        )
259
260        self._lineup_3_transparent_mesh = bui.getmesh(
261            'playerLineup3Transparent'
262        )
263        self._lineup_4_transparent_mesh = bui.getmesh(
264            'playerLineup4Transparent'
265        )
266        self._line_image: bui.Widget | None = None
267        self.eyes_mesh: bui.Mesh = bui.getmesh('plasticEyesTransparent')
268        self._white_tex = bui.gettexture('white')
269        uiscale = bui.app.ui_v1.uiscale
270        super().__init__(
271            root_widget=bui.containerwidget(
272                size=(self._width, self._height),
273                color=(0.45, 0.63, 0.15),
274                transition='in_scale',
275                scale=(
276                    1.4
277                    if uiscale is bui.UIScale.SMALL
278                    else 1.2 if uiscale is bui.UIScale.MEDIUM else 1.0
279                ),
280            )
281        )
282
283        self._cancel_button = bui.buttonwidget(
284            parent=self._root_widget,
285            scale=1.0,
286            position=(60, self._height - 80),
287            size=(50, 50),
288            label='',
289            on_activate_call=self.close,
290            autoselect=True,
291            color=(0.45, 0.63, 0.15),
292            icon=bui.gettexture('crossOut'),
293            iconscale=1.2,
294        )
295        bui.containerwidget(
296            edit=self._root_widget, cancel_button=self._cancel_button
297        )
298
299        self._title_text = bui.textwidget(
300            parent=self._root_widget,
301            position=(self._width * 0.5, self._height * 0.55),
302            size=(0, 0),
303            color=(1.0, 3.0, 1.0),
304            scale=1.3,
305            h_align='center',
306            v_align='center',
307            text=bui.Lstr(resource='internal.connectingToPartyText'),
308            maxwidth=self._width * 0.65,
309        )
310
311        self._tickets_text = bui.textwidget(
312            parent=self._root_widget,
313            position=(self._width - 180, self._height - 20),
314            size=(0, 0),
315            color=(0.2, 1.0, 0.2),
316            scale=0.7,
317            h_align='center',
318            v_align='center',
319            text='',
320        )
321
322        # update at roughly 30fps
323        self._update_timer = bui.AppTimer(
324            0.033, bui.WeakCall(self.update), repeat=True
325        )
326        self.update()
327
328    def __del__(self) -> None:
329        try:
330            plus = bui.app.plus
331            assert plus is not None
332
333            plus.add_v1_account_transaction(
334                {'type': 'PARTY_QUEUE_REMOVE', 'q': self._queue_id}
335            )
336            plus.run_v1_account_transactions()
337        except Exception:
338            logging.exception('Error removing self from party queue.')
339
340    def get_line_left(self) -> float:
341        """(internal)"""
342        return self._line_left
343
344    def get_line_width(self) -> float:
345        """(internal)"""
346        return self._line_width
347
348    def get_line_bottom(self) -> float:
349        """(internal)"""
350        return self._line_bottom
351
352    def on_account_press(
353        self, account_id: str | None, origin_widget: bui.Widget
354    ) -> None:
355        """A dude was clicked so we should show his account info."""
356        from bauiv1lib.account.viewer import AccountViewerWindow
357
358        if account_id is None:
359            bui.getsound('error').play()
360            return
361        AccountViewerWindow(
362            account_id=account_id,
363            position=origin_widget.get_screen_space_center(),
364        )
365
366    def close(self) -> None:
367        """Close the ui."""
368        bui.containerwidget(edit=self._root_widget, transition='out_scale')
369
370    def _update_field(self, response: dict[str, Any]) -> None:
371        plus = bui.app.plus
372        assert plus is not None
373
374        if self._angry_computer_image is None:
375            self._angry_computer_image = bui.imagewidget(
376                parent=self._root_widget,
377                position=(self._width - 180, self._height * 0.5 - 65),
378                size=(150, 150),
379                texture=self.lineup_tex,
380                mesh_transparent=self._angry_computer_transparent_mesh,
381            )
382        if self._line_image is None:
383            self._line_image = bui.imagewidget(
384                parent=self._root_widget,
385                color=(0.0, 0.0, 0.0),
386                opacity=0.2,
387                position=(self._line_left, self._line_bottom - 2.0),
388                size=(self._line_width, 4.0),
389                texture=self._white_tex,
390            )
391
392        # now go through the data they sent, creating dudes for us and our
393        # enemies as needed and updating target positions on all of them..
394
395        # mark all as unclaimed so we know which ones to kill off..
396        for dude in self._dudes:
397            dude.claimed = False
398
399        # always have a dude for ourself..
400        if -1 not in self._dudes_by_id:
401            dude = self.Dude(
402                self,
403                response['d'],
404                self._initial_offset,
405                True,
406                plus.get_v1_account_misc_read_val_2('resolvedAccountID', None),
407                plus.get_v1_account_display_string(),
408            )
409            self._dudes_by_id[-1] = dude
410            self._dudes.append(dude)
411        else:
412            self._dudes_by_id[-1].set_target_distance(response['d'])
413        self._dudes_by_id[-1].claimed = True
414
415        # now create/destroy enemies
416        for (
417            enemy_id,
418            enemy_distance,
419            enemy_account_id,
420            enemy_name,
421        ) in response['e']:
422            if enemy_id not in self._dudes_by_id:
423                dude = self.Dude(
424                    self,
425                    enemy_distance,
426                    self._initial_offset,
427                    False,
428                    enemy_account_id,
429                    enemy_name,
430                )
431                self._dudes_by_id[enemy_id] = dude
432                self._dudes.append(dude)
433            else:
434                self._dudes_by_id[enemy_id].set_target_distance(enemy_distance)
435            self._dudes_by_id[enemy_id].claimed = True
436
437        # remove unclaimed dudes from both of our lists
438        # noinspection PyUnresolvedReferences
439        self._dudes_by_id = dict(
440            [
441                item
442                for item in list(self._dudes_by_id.items())
443                if item[1].claimed
444            ]
445        )
446        self._dudes = [dude for dude in self._dudes if dude.claimed]
447
448    def _hide_field(self) -> None:
449        if self._angry_computer_image:
450            self._angry_computer_image.delete()
451        self._angry_computer_image = None
452        if self._line_image:
453            self._line_image.delete()
454        self._line_image = None
455        self._dudes = []
456        self._dudes_by_id = {}
457
458    def on_update_response(self, response: dict[str, Any] | None) -> None:
459        """We've received a response from an update to the server."""
460        # pylint: disable=too-many-branches
461        if not self._root_widget:
462            return
463
464        # Seeing this in logs; debugging...
465        if not self._title_text:
466            print('PartyQueueWindows update: Have root but no title_text.')
467            return
468
469        if response is not None:
470            should_show_field = response.get('d') is not None
471            self._smoothing = response['s']
472            self._initial_offset = response['o']
473
474            # If they gave us a position, show the field.
475            if should_show_field:
476                bui.textwidget(
477                    edit=self._title_text,
478                    text=bui.Lstr(resource='waitingInLineText'),
479                    position=(self._width * 0.5, self._height * 0.85),
480                )
481                self._update_field(response)
482                self._field_shown = True
483            if not should_show_field and self._field_shown:
484                bui.textwidget(
485                    edit=self._title_text,
486                    text=bui.Lstr(resource='internal.connectingToPartyText'),
487                    position=(self._width * 0.5, self._height * 0.55),
488                )
489                self._hide_field()
490                self._field_shown = False
491
492            # if they told us there's a boost button, update..
493            if response.get('bt') is not None:
494                self._boost_tickets = response['bt']
495                self._boost_strength = response['ba']
496                if self._boost_button is None:
497                    self._boost_button = bui.buttonwidget(
498                        parent=self._root_widget,
499                        scale=1.0,
500                        position=(self._width * 0.5 - 75, 20),
501                        size=(150, 100),
502                        button_type='square',
503                        label='',
504                        on_activate_call=self.on_boost_press,
505                        enable_sound=False,
506                        color=(0, 1, 0),
507                        autoselect=True,
508                    )
509                    self._boost_label = bui.textwidget(
510                        parent=self._root_widget,
511                        draw_controller=self._boost_button,
512                        position=(self._width * 0.5, 88),
513                        size=(0, 0),
514                        color=(0.8, 1.0, 0.8),
515                        scale=1.5,
516                        h_align='center',
517                        v_align='center',
518                        text=bui.Lstr(resource='boostText'),
519                        maxwidth=150,
520                    )
521                    self._boost_price = bui.textwidget(
522                        parent=self._root_widget,
523                        draw_controller=self._boost_button,
524                        position=(self._width * 0.5, 50),
525                        size=(0, 0),
526                        color=(0, 1, 0),
527                        scale=0.9,
528                        h_align='center',
529                        v_align='center',
530                        text=bui.charstr(bui.SpecialChar.TICKET)
531                        + str(self._boost_tickets),
532                        maxwidth=150,
533                    )
534            else:
535                if self._boost_button is not None:
536                    self._boost_button.delete()
537                    self._boost_button = None
538                if self._boost_price is not None:
539                    self._boost_price.delete()
540                    self._boost_price = None
541                if self._boost_label is not None:
542                    self._boost_label.delete()
543                    self._boost_label = None
544
545            # if they told us to go ahead and try and connect, do so..
546            # (note: servers will disconnect us if we try to connect before
547            # getting this go-ahead, so don't get any bright ideas...)
548            if response.get('c', False):
549                # enforce a delay between connection attempts
550                # (in case they're jamming on the boost button)
551                now = time.time()
552                if (
553                    self._last_connect_attempt_time is None
554                    or now - self._last_connect_attempt_time > 10.0
555                ):
556
557                    # Store UI location to return to when done.
558                    if bs.app.classic is not None:
559                        bs.app.classic.save_ui_state()
560
561                    bs.connect_to_party(
562                        address=self._address,
563                        port=self._port,
564                        print_progress=False,
565                    )
566                    self._last_connect_attempt_time = now
567
568    def on_boost_press(self) -> None:
569        """Boost was pressed."""
570        from bauiv1lib import account
571
572        # from bauiv1lib import gettickets
573
574        plus = bui.app.plus
575        assert plus is not None
576
577        if plus.get_v1_account_state() != 'signed_in':
578            account.show_sign_in_prompt()
579            return
580
581        if plus.get_v1_account_ticket_count() < self._boost_tickets:
582            bui.getsound('error').play()
583            # gettickets.show_get_tickets_prompt()
584            return
585
586        bui.getsound('laserReverse').play()
587        plus.add_v1_account_transaction(
588            {
589                'type': 'PARTY_QUEUE_BOOST',
590                't': self._boost_tickets,
591                'q': self._queue_id,
592            },
593            callback=bui.WeakCall(self.on_update_response),
594        )
595        # lets not run these immediately (since they may be rapid-fire,
596        # just bucket them until the next tick)
597
598        # the transaction handles the local ticket change, but we apply our
599        # local boost vis manually here..
600        # (our visualization isn't really wired up to be transaction-based)
601        our_dude = self._dudes_by_id.get(-1)
602        if our_dude is not None:
603            our_dude.boost(self._boost_strength, self._smoothing)
604
605    def update(self) -> None:
606        """Update!"""
607        plus = bui.app.plus
608        assert plus is not None
609
610        if not self._root_widget:
611            return
612
613        # Update boost-price.
614        if self._boost_price is not None:
615            bui.textwidget(
616                edit=self._boost_price,
617                text=bui.charstr(bui.SpecialChar.TICKET)
618                + str(self._boost_tickets),
619            )
620
621        # Update boost button color based on if we have enough moola.
622        if self._boost_button is not None:
623            can_boost = (
624                plus.get_v1_account_state() == 'signed_in'
625                and plus.get_v1_account_ticket_count() >= self._boost_tickets
626            )
627            bui.buttonwidget(
628                edit=self._boost_button,
629                color=(0, 1, 0) if can_boost else (0.7, 0.7, 0.7),
630            )
631
632        # Update ticket-count.
633        if self._tickets_text is not None:
634            if self._boost_button is not None:
635                if plus.get_v1_account_state() == 'signed_in':
636                    val = bui.charstr(bui.SpecialChar.TICKET) + str(
637                        plus.get_v1_account_ticket_count()
638                    )
639                else:
640                    val = bui.charstr(bui.SpecialChar.TICKET) + '???'
641                bui.textwidget(edit=self._tickets_text, text=val)
642            else:
643                bui.textwidget(edit=self._tickets_text, text='')
644
645        current_time = bui.apptime()
646        if (
647            self._last_transaction_time is None
648            or current_time - self._last_transaction_time
649            > 0.001 * plus.get_v1_account_misc_read_val('pqInt', 5000)
650        ):
651            self._last_transaction_time = current_time
652            plus.add_v1_account_transaction(
653                {'type': 'PARTY_QUEUE_QUERY', 'q': self._queue_id},
654                callback=bui.WeakCall(self.on_update_response),
655            )
656            plus.run_v1_account_transactions()
657
658        # step our dudes
659        for dude in self._dudes:
660            dude.step(self._smoothing)

Window showing players waiting to join a server.

PartyQueueWindow(queue_id: str, address: str, port: int)
226    def __init__(self, queue_id: str, address: str, port: int):
227        assert bui.app.classic is not None
228        self._address = address
229        self._port = port
230        self._queue_id = queue_id
231        self._width = 800
232        self._height = 400
233        self._last_connect_attempt_time: float | None = None
234        self._last_transaction_time: float | None = None
235        self._boost_button: bui.Widget | None = None
236        self._boost_price: bui.Widget | None = None
237        self._boost_label: bui.Widget | None = None
238        self._field_shown = False
239        self._dudes: list[PartyQueueWindow.Dude] = []
240        self._dudes_by_id: dict[int, PartyQueueWindow.Dude] = {}
241        self._line_left = 40.0
242        self._line_width = self._width - 190
243        self._line_bottom = self._height * 0.4
244        self.lineup_tex: bui.Texture = bui.gettexture('playerLineup')
245        self._smoothing = 0.0
246        self._initial_offset = 0.0
247        self._boost_tickets = 0
248        self._boost_strength = 0.0
249        self._angry_computer_transparent_mesh = bui.getmesh(
250            'angryComputerTransparent'
251        )
252        self._angry_computer_image: bui.Widget | None = None
253        self.lineup_1_transparent_mesh: bui.Mesh = bui.getmesh(
254            'playerLineup1Transparent'
255        )
256        self._lineup_2_transparent_mesh: bui.Mesh = bui.getmesh(
257            'playerLineup2Transparent'
258        )
259
260        self._lineup_3_transparent_mesh = bui.getmesh(
261            'playerLineup3Transparent'
262        )
263        self._lineup_4_transparent_mesh = bui.getmesh(
264            'playerLineup4Transparent'
265        )
266        self._line_image: bui.Widget | None = None
267        self.eyes_mesh: bui.Mesh = bui.getmesh('plasticEyesTransparent')
268        self._white_tex = bui.gettexture('white')
269        uiscale = bui.app.ui_v1.uiscale
270        super().__init__(
271            root_widget=bui.containerwidget(
272                size=(self._width, self._height),
273                color=(0.45, 0.63, 0.15),
274                transition='in_scale',
275                scale=(
276                    1.4
277                    if uiscale is bui.UIScale.SMALL
278                    else 1.2 if uiscale is bui.UIScale.MEDIUM else 1.0
279                ),
280            )
281        )
282
283        self._cancel_button = bui.buttonwidget(
284            parent=self._root_widget,
285            scale=1.0,
286            position=(60, self._height - 80),
287            size=(50, 50),
288            label='',
289            on_activate_call=self.close,
290            autoselect=True,
291            color=(0.45, 0.63, 0.15),
292            icon=bui.gettexture('crossOut'),
293            iconscale=1.2,
294        )
295        bui.containerwidget(
296            edit=self._root_widget, cancel_button=self._cancel_button
297        )
298
299        self._title_text = bui.textwidget(
300            parent=self._root_widget,
301            position=(self._width * 0.5, self._height * 0.55),
302            size=(0, 0),
303            color=(1.0, 3.0, 1.0),
304            scale=1.3,
305            h_align='center',
306            v_align='center',
307            text=bui.Lstr(resource='internal.connectingToPartyText'),
308            maxwidth=self._width * 0.65,
309        )
310
311        self._tickets_text = bui.textwidget(
312            parent=self._root_widget,
313            position=(self._width - 180, self._height - 20),
314            size=(0, 0),
315            color=(0.2, 1.0, 0.2),
316            scale=0.7,
317            h_align='center',
318            v_align='center',
319            text='',
320        )
321
322        # update at roughly 30fps
323        self._update_timer = bui.AppTimer(
324            0.033, bui.WeakCall(self.update), repeat=True
325        )
326        self.update()
lineup_tex: _bauiv1.Texture
lineup_1_transparent_mesh: _bauiv1.Mesh
eyes_mesh: _bauiv1.Mesh
def on_account_press(self, account_id: str | None, origin_widget: _bauiv1.Widget) -> None:
352    def on_account_press(
353        self, account_id: str | None, origin_widget: bui.Widget
354    ) -> None:
355        """A dude was clicked so we should show his account info."""
356        from bauiv1lib.account.viewer import AccountViewerWindow
357
358        if account_id is None:
359            bui.getsound('error').play()
360            return
361        AccountViewerWindow(
362            account_id=account_id,
363            position=origin_widget.get_screen_space_center(),
364        )

A dude was clicked so we should show his account info.

def close(self) -> None:
366    def close(self) -> None:
367        """Close the ui."""
368        bui.containerwidget(edit=self._root_widget, transition='out_scale')

Close the ui.

def on_update_response(self, response: dict[str, typing.Any] | None) -> None:
458    def on_update_response(self, response: dict[str, Any] | None) -> None:
459        """We've received a response from an update to the server."""
460        # pylint: disable=too-many-branches
461        if not self._root_widget:
462            return
463
464        # Seeing this in logs; debugging...
465        if not self._title_text:
466            print('PartyQueueWindows update: Have root but no title_text.')
467            return
468
469        if response is not None:
470            should_show_field = response.get('d') is not None
471            self._smoothing = response['s']
472            self._initial_offset = response['o']
473
474            # If they gave us a position, show the field.
475            if should_show_field:
476                bui.textwidget(
477                    edit=self._title_text,
478                    text=bui.Lstr(resource='waitingInLineText'),
479                    position=(self._width * 0.5, self._height * 0.85),
480                )
481                self._update_field(response)
482                self._field_shown = True
483            if not should_show_field and self._field_shown:
484                bui.textwidget(
485                    edit=self._title_text,
486                    text=bui.Lstr(resource='internal.connectingToPartyText'),
487                    position=(self._width * 0.5, self._height * 0.55),
488                )
489                self._hide_field()
490                self._field_shown = False
491
492            # if they told us there's a boost button, update..
493            if response.get('bt') is not None:
494                self._boost_tickets = response['bt']
495                self._boost_strength = response['ba']
496                if self._boost_button is None:
497                    self._boost_button = bui.buttonwidget(
498                        parent=self._root_widget,
499                        scale=1.0,
500                        position=(self._width * 0.5 - 75, 20),
501                        size=(150, 100),
502                        button_type='square',
503                        label='',
504                        on_activate_call=self.on_boost_press,
505                        enable_sound=False,
506                        color=(0, 1, 0),
507                        autoselect=True,
508                    )
509                    self._boost_label = bui.textwidget(
510                        parent=self._root_widget,
511                        draw_controller=self._boost_button,
512                        position=(self._width * 0.5, 88),
513                        size=(0, 0),
514                        color=(0.8, 1.0, 0.8),
515                        scale=1.5,
516                        h_align='center',
517                        v_align='center',
518                        text=bui.Lstr(resource='boostText'),
519                        maxwidth=150,
520                    )
521                    self._boost_price = bui.textwidget(
522                        parent=self._root_widget,
523                        draw_controller=self._boost_button,
524                        position=(self._width * 0.5, 50),
525                        size=(0, 0),
526                        color=(0, 1, 0),
527                        scale=0.9,
528                        h_align='center',
529                        v_align='center',
530                        text=bui.charstr(bui.SpecialChar.TICKET)
531                        + str(self._boost_tickets),
532                        maxwidth=150,
533                    )
534            else:
535                if self._boost_button is not None:
536                    self._boost_button.delete()
537                    self._boost_button = None
538                if self._boost_price is not None:
539                    self._boost_price.delete()
540                    self._boost_price = None
541                if self._boost_label is not None:
542                    self._boost_label.delete()
543                    self._boost_label = None
544
545            # if they told us to go ahead and try and connect, do so..
546            # (note: servers will disconnect us if we try to connect before
547            # getting this go-ahead, so don't get any bright ideas...)
548            if response.get('c', False):
549                # enforce a delay between connection attempts
550                # (in case they're jamming on the boost button)
551                now = time.time()
552                if (
553                    self._last_connect_attempt_time is None
554                    or now - self._last_connect_attempt_time > 10.0
555                ):
556
557                    # Store UI location to return to when done.
558                    if bs.app.classic is not None:
559                        bs.app.classic.save_ui_state()
560
561                    bs.connect_to_party(
562                        address=self._address,
563                        port=self._port,
564                        print_progress=False,
565                    )
566                    self._last_connect_attempt_time = now

We've received a response from an update to the server.

def on_boost_press(self) -> None:
568    def on_boost_press(self) -> None:
569        """Boost was pressed."""
570        from bauiv1lib import account
571
572        # from bauiv1lib import gettickets
573
574        plus = bui.app.plus
575        assert plus is not None
576
577        if plus.get_v1_account_state() != 'signed_in':
578            account.show_sign_in_prompt()
579            return
580
581        if plus.get_v1_account_ticket_count() < self._boost_tickets:
582            bui.getsound('error').play()
583            # gettickets.show_get_tickets_prompt()
584            return
585
586        bui.getsound('laserReverse').play()
587        plus.add_v1_account_transaction(
588            {
589                'type': 'PARTY_QUEUE_BOOST',
590                't': self._boost_tickets,
591                'q': self._queue_id,
592            },
593            callback=bui.WeakCall(self.on_update_response),
594        )
595        # lets not run these immediately (since they may be rapid-fire,
596        # just bucket them until the next tick)
597
598        # the transaction handles the local ticket change, but we apply our
599        # local boost vis manually here..
600        # (our visualization isn't really wired up to be transaction-based)
601        our_dude = self._dudes_by_id.get(-1)
602        if our_dude is not None:
603            our_dude.boost(self._boost_strength, self._smoothing)

Boost was pressed.

def update(self) -> None:
605    def update(self) -> None:
606        """Update!"""
607        plus = bui.app.plus
608        assert plus is not None
609
610        if not self._root_widget:
611            return
612
613        # Update boost-price.
614        if self._boost_price is not None:
615            bui.textwidget(
616                edit=self._boost_price,
617                text=bui.charstr(bui.SpecialChar.TICKET)
618                + str(self._boost_tickets),
619            )
620
621        # Update boost button color based on if we have enough moola.
622        if self._boost_button is not None:
623            can_boost = (
624                plus.get_v1_account_state() == 'signed_in'
625                and plus.get_v1_account_ticket_count() >= self._boost_tickets
626            )
627            bui.buttonwidget(
628                edit=self._boost_button,
629                color=(0, 1, 0) if can_boost else (0.7, 0.7, 0.7),
630            )
631
632        # Update ticket-count.
633        if self._tickets_text is not None:
634            if self._boost_button is not None:
635                if plus.get_v1_account_state() == 'signed_in':
636                    val = bui.charstr(bui.SpecialChar.TICKET) + str(
637                        plus.get_v1_account_ticket_count()
638                    )
639                else:
640                    val = bui.charstr(bui.SpecialChar.TICKET) + '???'
641                bui.textwidget(edit=self._tickets_text, text=val)
642            else:
643                bui.textwidget(edit=self._tickets_text, text='')
644
645        current_time = bui.apptime()
646        if (
647            self._last_transaction_time is None
648            or current_time - self._last_transaction_time
649            > 0.001 * plus.get_v1_account_misc_read_val('pqInt', 5000)
650        ):
651            self._last_transaction_time = current_time
652            plus.add_v1_account_transaction(
653                {'type': 'PARTY_QUEUE_QUERY', 'q': self._queue_id},
654                callback=bui.WeakCall(self.on_update_response),
655            )
656            plus.run_v1_account_transactions()
657
658        # step our dudes
659        for dude in self._dudes:
660            dude.step(self._smoothing)

Update!

class PartyQueueWindow.Dude:
 25    class Dude:
 26        """Represents a single dude waiting in a server line."""
 27
 28        def __init__(
 29            self,
 30            parent: PartyQueueWindow,
 31            distance: float,
 32            initial_offset: float,
 33            is_player: bool,
 34            account_id: str,
 35            name: str,
 36        ):
 37            # pylint: disable=too-many-positional-arguments
 38            self.claimed = False
 39            self._line_left = parent.get_line_left()
 40            self._line_width = parent.get_line_width()
 41            self._line_bottom = parent.get_line_bottom()
 42            self._target_distance = distance
 43            self._distance = distance + initial_offset
 44            self._boost_brightness = 0.0
 45            self._debug = False
 46            self._sc = sc = 1.1 if is_player else 0.6 + random.random() * 0.2
 47            self._y_offs = -30.0 if is_player else -47.0 * sc
 48            self._last_boost_time = 0.0
 49            self._color = (
 50                (0.2, 1.0, 0.2)
 51                if is_player
 52                else (
 53                    0.5 + 0.3 * random.random(),
 54                    0.4 + 0.2 * random.random(),
 55                    0.5 + 0.3 * random.random(),
 56                )
 57            )
 58            self._eye_color = (
 59                0.7 * 1.0 + 0.3 * self._color[0],
 60                0.7 * 1.0 + 0.3 * self._color[1],
 61                0.7 * 1.0 + 0.3 * self._color[2],
 62            )
 63            self._body_image = bui.buttonwidget(
 64                parent=parent.get_root_widget(),
 65                selectable=True,
 66                label='',
 67                size=(sc * 60, sc * 80),
 68                color=self._color,
 69                texture=parent.lineup_tex,
 70                mesh_transparent=parent.lineup_1_transparent_mesh,
 71            )
 72            bui.buttonwidget(
 73                edit=self._body_image,
 74                on_activate_call=bui.WeakCall(
 75                    parent.on_account_press, account_id, self._body_image
 76                ),
 77            )
 78            bui.widget(edit=self._body_image, autoselect=True)
 79            self._eyes_image = bui.imagewidget(
 80                parent=parent.get_root_widget(),
 81                size=(sc * 36, sc * 18),
 82                texture=parent.lineup_tex,
 83                color=self._eye_color,
 84                mesh_transparent=parent.eyes_mesh,
 85            )
 86            self._name_text = bui.textwidget(
 87                parent=parent.get_root_widget(),
 88                size=(0, 0),
 89                shadow=0,
 90                flatness=1.0,
 91                text=name,
 92                maxwidth=100,
 93                h_align='center',
 94                v_align='center',
 95                scale=0.75,
 96                color=(1, 1, 1, 0.6),
 97            )
 98            self._update_image()
 99
100            # DEBUG: vis target pos..
101            self._body_image_target: bui.Widget | None
102            self._eyes_image_target: bui.Widget | None
103            if self._debug:
104                self._body_image_target = bui.imagewidget(
105                    parent=parent.get_root_widget(),
106                    size=(sc * 60, sc * 80),
107                    color=self._color,
108                    texture=parent.lineup_tex,
109                    mesh_transparent=parent.lineup_1_transparent_mesh,
110                )
111                self._eyes_image_target = bui.imagewidget(
112                    parent=parent.get_root_widget(),
113                    size=(sc * 36, sc * 18),
114                    texture=parent.lineup_tex,
115                    color=self._eye_color,
116                    mesh_transparent=parent.eyes_mesh,
117                )
118                # (updates our image positions)
119                self.set_target_distance(self._target_distance)
120            else:
121                self._body_image_target = self._eyes_image_target = None
122
123        def __del__(self) -> None:
124            # ew.  our destructor here may get called as part of an internal
125            # widget tear-down.
126            # running further widget calls here can quietly break stuff, so we
127            # need to push a deferred call to kill these as necessary instead.
128            # (should bulletproof internal widget code to give a clean error
129            # in this case)
130            def kill_widgets(widgets: Sequence[bui.Widget | None]) -> None:
131                for widget in widgets:
132                    if widget:
133                        widget.delete()
134
135            bui.pushcall(
136                bui.Call(
137                    kill_widgets,
138                    [
139                        self._body_image,
140                        self._eyes_image,
141                        self._body_image_target,
142                        self._eyes_image_target,
143                        self._name_text,
144                    ],
145                )
146            )
147
148        def set_target_distance(self, dist: float) -> None:
149            """Set distance for a dude."""
150            self._target_distance = dist
151            if self._debug:
152                sc = self._sc
153                position = (
154                    self._line_left
155                    + self._line_width * (1.0 - self._target_distance),
156                    self._line_bottom - 30,
157                )
158                bui.imagewidget(
159                    edit=self._body_image_target,
160                    position=(
161                        position[0] - sc * 30,
162                        position[1] - sc * 25 - 70,
163                    ),
164                )
165                bui.imagewidget(
166                    edit=self._eyes_image_target,
167                    position=(
168                        position[0] - sc * 18,
169                        position[1] + sc * 31 - 70,
170                    ),
171                )
172
173        def step(self, smoothing: float) -> None:
174            """Step this dude."""
175            self._distance = (
176                smoothing * self._distance
177                + (1.0 - smoothing) * self._target_distance
178            )
179            self._update_image()
180            self._boost_brightness *= 0.9
181
182        def _update_image(self) -> None:
183            sc = self._sc
184            position = (
185                self._line_left + self._line_width * (1.0 - self._distance),
186                self._line_bottom + 40,
187            )
188            brightness = 1.0 + self._boost_brightness
189            bui.buttonwidget(
190                edit=self._body_image,
191                position=(
192                    position[0] - sc * 30,
193                    position[1] - sc * 25 + self._y_offs,
194                ),
195                color=(
196                    self._color[0] * brightness,
197                    self._color[1] * brightness,
198                    self._color[2] * brightness,
199                ),
200            )
201            bui.imagewidget(
202                edit=self._eyes_image,
203                position=(
204                    position[0] - sc * 18,
205                    position[1] + sc * 31 + self._y_offs,
206                ),
207                color=(
208                    self._eye_color[0] * brightness,
209                    self._eye_color[1] * brightness,
210                    self._eye_color[2] * brightness,
211                ),
212            )
213            bui.textwidget(
214                edit=self._name_text,
215                position=(position[0] - sc * 0, position[1] + sc * 40.0),
216            )
217
218        def boost(self, amount: float, smoothing: float) -> None:
219            """Boost this dude."""
220            del smoothing  # unused arg
221            self._distance = max(0.0, self._distance - amount)
222            self._update_image()
223            self._last_boost_time = time.time()
224            self._boost_brightness += 0.6

Represents a single dude waiting in a server line.

PartyQueueWindow.Dude( parent: PartyQueueWindow, distance: float, initial_offset: float, is_player: bool, account_id: str, name: str)
 28        def __init__(
 29            self,
 30            parent: PartyQueueWindow,
 31            distance: float,
 32            initial_offset: float,
 33            is_player: bool,
 34            account_id: str,
 35            name: str,
 36        ):
 37            # pylint: disable=too-many-positional-arguments
 38            self.claimed = False
 39            self._line_left = parent.get_line_left()
 40            self._line_width = parent.get_line_width()
 41            self._line_bottom = parent.get_line_bottom()
 42            self._target_distance = distance
 43            self._distance = distance + initial_offset
 44            self._boost_brightness = 0.0
 45            self._debug = False
 46            self._sc = sc = 1.1 if is_player else 0.6 + random.random() * 0.2
 47            self._y_offs = -30.0 if is_player else -47.0 * sc
 48            self._last_boost_time = 0.0
 49            self._color = (
 50                (0.2, 1.0, 0.2)
 51                if is_player
 52                else (
 53                    0.5 + 0.3 * random.random(),
 54                    0.4 + 0.2 * random.random(),
 55                    0.5 + 0.3 * random.random(),
 56                )
 57            )
 58            self._eye_color = (
 59                0.7 * 1.0 + 0.3 * self._color[0],
 60                0.7 * 1.0 + 0.3 * self._color[1],
 61                0.7 * 1.0 + 0.3 * self._color[2],
 62            )
 63            self._body_image = bui.buttonwidget(
 64                parent=parent.get_root_widget(),
 65                selectable=True,
 66                label='',
 67                size=(sc * 60, sc * 80),
 68                color=self._color,
 69                texture=parent.lineup_tex,
 70                mesh_transparent=parent.lineup_1_transparent_mesh,
 71            )
 72            bui.buttonwidget(
 73                edit=self._body_image,
 74                on_activate_call=bui.WeakCall(
 75                    parent.on_account_press, account_id, self._body_image
 76                ),
 77            )
 78            bui.widget(edit=self._body_image, autoselect=True)
 79            self._eyes_image = bui.imagewidget(
 80                parent=parent.get_root_widget(),
 81                size=(sc * 36, sc * 18),
 82                texture=parent.lineup_tex,
 83                color=self._eye_color,
 84                mesh_transparent=parent.eyes_mesh,
 85            )
 86            self._name_text = bui.textwidget(
 87                parent=parent.get_root_widget(),
 88                size=(0, 0),
 89                shadow=0,
 90                flatness=1.0,
 91                text=name,
 92                maxwidth=100,
 93                h_align='center',
 94                v_align='center',
 95                scale=0.75,
 96                color=(1, 1, 1, 0.6),
 97            )
 98            self._update_image()
 99
100            # DEBUG: vis target pos..
101            self._body_image_target: bui.Widget | None
102            self._eyes_image_target: bui.Widget | None
103            if self._debug:
104                self._body_image_target = bui.imagewidget(
105                    parent=parent.get_root_widget(),
106                    size=(sc * 60, sc * 80),
107                    color=self._color,
108                    texture=parent.lineup_tex,
109                    mesh_transparent=parent.lineup_1_transparent_mesh,
110                )
111                self._eyes_image_target = bui.imagewidget(
112                    parent=parent.get_root_widget(),
113                    size=(sc * 36, sc * 18),
114                    texture=parent.lineup_tex,
115                    color=self._eye_color,
116                    mesh_transparent=parent.eyes_mesh,
117                )
118                # (updates our image positions)
119                self.set_target_distance(self._target_distance)
120            else:
121                self._body_image_target = self._eyes_image_target = None
claimed
def set_target_distance(self, dist: float) -> None:
148        def set_target_distance(self, dist: float) -> None:
149            """Set distance for a dude."""
150            self._target_distance = dist
151            if self._debug:
152                sc = self._sc
153                position = (
154                    self._line_left
155                    + self._line_width * (1.0 - self._target_distance),
156                    self._line_bottom - 30,
157                )
158                bui.imagewidget(
159                    edit=self._body_image_target,
160                    position=(
161                        position[0] - sc * 30,
162                        position[1] - sc * 25 - 70,
163                    ),
164                )
165                bui.imagewidget(
166                    edit=self._eyes_image_target,
167                    position=(
168                        position[0] - sc * 18,
169                        position[1] + sc * 31 - 70,
170                    ),
171                )

Set distance for a dude.

def step(self, smoothing: float) -> None:
173        def step(self, smoothing: float) -> None:
174            """Step this dude."""
175            self._distance = (
176                smoothing * self._distance
177                + (1.0 - smoothing) * self._target_distance
178            )
179            self._update_image()
180            self._boost_brightness *= 0.9

Step this dude.

def boost(self, amount: float, smoothing: float) -> None:
218        def boost(self, amount: float, smoothing: float) -> None:
219            """Boost this dude."""
220            del smoothing  # unused arg
221            self._distance = max(0.0, self._distance - amount)
222            self._update_image()
223            self._last_boost_time = time.time()
224            self._boost_brightness += 0.6

Boost this dude.