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=invalid-name
 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            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        bui.app.ui_v1.have_party_queue_window = True
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
279                    if uiscale is bui.UIScale.MEDIUM
280                    else 1.0
281                ),
282            )
283        )
284
285        self._cancel_button = bui.buttonwidget(
286            parent=self._root_widget,
287            scale=1.0,
288            position=(60, self._height - 80),
289            size=(50, 50),
290            label='',
291            on_activate_call=self.close,
292            autoselect=True,
293            color=(0.45, 0.63, 0.15),
294            icon=bui.gettexture('crossOut'),
295            iconscale=1.2,
296        )
297        bui.containerwidget(
298            edit=self._root_widget, cancel_button=self._cancel_button
299        )
300
301        self._title_text = bui.textwidget(
302            parent=self._root_widget,
303            position=(self._width * 0.5, self._height * 0.55),
304            size=(0, 0),
305            color=(1.0, 3.0, 1.0),
306            scale=1.3,
307            h_align='center',
308            v_align='center',
309            text=bui.Lstr(resource='internal.connectingToPartyText'),
310            maxwidth=self._width * 0.65,
311        )
312
313        self._tickets_text = bui.textwidget(
314            parent=self._root_widget,
315            position=(self._width - 180, self._height - 20),
316            size=(0, 0),
317            color=(0.2, 1.0, 0.2),
318            scale=0.7,
319            h_align='center',
320            v_align='center',
321            text='',
322        )
323
324        # update at roughly 30fps
325        self._update_timer = bui.AppTimer(
326            0.033, bui.WeakCall(self.update), repeat=True
327        )
328        self.update()
329
330    def __del__(self) -> None:
331        try:
332            plus = bui.app.plus
333            assert plus is not None
334
335            assert bui.app.classic is not None
336            bui.app.ui_v1.have_party_queue_window = False
337            plus.add_v1_account_transaction(
338                {'type': 'PARTY_QUEUE_REMOVE', 'q': self._queue_id}
339            )
340            plus.run_v1_account_transactions()
341        except Exception:
342            logging.exception('Error removing self from party queue.')
343
344    def get_line_left(self) -> float:
345        """(internal)"""
346        return self._line_left
347
348    def get_line_width(self) -> float:
349        """(internal)"""
350        return self._line_width
351
352    def get_line_bottom(self) -> float:
353        """(internal)"""
354        return self._line_bottom
355
356    def on_account_press(
357        self, account_id: str | None, origin_widget: bui.Widget
358    ) -> None:
359        """A dude was clicked so we should show his account info."""
360        from bauiv1lib.account import viewer
361
362        if account_id is None:
363            bui.getsound('error').play()
364            return
365        viewer.AccountViewerWindow(
366            account_id=account_id,
367            position=origin_widget.get_screen_space_center(),
368        )
369
370    def close(self) -> None:
371        """Close the ui."""
372        bui.containerwidget(edit=self._root_widget, transition='out_scale')
373
374    def _update_field(self, response: dict[str, Any]) -> None:
375        plus = bui.app.plus
376        assert plus is not None
377
378        if self._angry_computer_image is None:
379            self._angry_computer_image = bui.imagewidget(
380                parent=self._root_widget,
381                position=(self._width - 180, self._height * 0.5 - 65),
382                size=(150, 150),
383                texture=self.lineup_tex,
384                mesh_transparent=self._angry_computer_transparent_mesh,
385            )
386        if self._line_image is None:
387            self._line_image = bui.imagewidget(
388                parent=self._root_widget,
389                color=(0.0, 0.0, 0.0),
390                opacity=0.2,
391                position=(self._line_left, self._line_bottom - 2.0),
392                size=(self._line_width, 4.0),
393                texture=self._white_tex,
394            )
395
396        # now go through the data they sent, creating dudes for us and our
397        # enemies as needed and updating target positions on all of them..
398
399        # mark all as unclaimed so we know which ones to kill off..
400        for dude in self._dudes:
401            dude.claimed = False
402
403        # always have a dude for ourself..
404        if -1 not in self._dudes_by_id:
405            dude = self.Dude(
406                self,
407                response['d'],
408                self._initial_offset,
409                True,
410                plus.get_v1_account_misc_read_val_2('resolvedAccountID', None),
411                plus.get_v1_account_display_string(),
412            )
413            self._dudes_by_id[-1] = dude
414            self._dudes.append(dude)
415        else:
416            self._dudes_by_id[-1].set_target_distance(response['d'])
417        self._dudes_by_id[-1].claimed = True
418
419        # now create/destroy enemies
420        for (
421            enemy_id,
422            enemy_distance,
423            enemy_account_id,
424            enemy_name,
425        ) in response['e']:
426            if enemy_id not in self._dudes_by_id:
427                dude = self.Dude(
428                    self,
429                    enemy_distance,
430                    self._initial_offset,
431                    False,
432                    enemy_account_id,
433                    enemy_name,
434                )
435                self._dudes_by_id[enemy_id] = dude
436                self._dudes.append(dude)
437            else:
438                self._dudes_by_id[enemy_id].set_target_distance(enemy_distance)
439            self._dudes_by_id[enemy_id].claimed = True
440
441        # remove unclaimed dudes from both of our lists
442        # noinspection PyUnresolvedReferences
443        self._dudes_by_id = dict(
444            [
445                item
446                for item in list(self._dudes_by_id.items())
447                if item[1].claimed
448            ]
449        )
450        self._dudes = [dude for dude in self._dudes if dude.claimed]
451
452    def _hide_field(self) -> None:
453        if self._angry_computer_image:
454            self._angry_computer_image.delete()
455        self._angry_computer_image = None
456        if self._line_image:
457            self._line_image.delete()
458        self._line_image = None
459        self._dudes = []
460        self._dudes_by_id = {}
461
462    def on_update_response(self, response: dict[str, Any] | None) -> None:
463        """We've received a response from an update to the server."""
464        # pylint: disable=too-many-branches
465        if not self._root_widget:
466            return
467
468        # Seeing this in logs; debugging...
469        if not self._title_text:
470            print('PartyQueueWindows update: Have root but no title_text.')
471            return
472
473        if response is not None:
474            should_show_field = response.get('d') is not None
475            self._smoothing = response['s']
476            self._initial_offset = response['o']
477
478            # If they gave us a position, show the field.
479            if should_show_field:
480                bui.textwidget(
481                    edit=self._title_text,
482                    text=bui.Lstr(resource='waitingInLineText'),
483                    position=(self._width * 0.5, self._height * 0.85),
484                )
485                self._update_field(response)
486                self._field_shown = True
487            if not should_show_field and self._field_shown:
488                bui.textwidget(
489                    edit=self._title_text,
490                    text=bui.Lstr(resource='internal.connectingToPartyText'),
491                    position=(self._width * 0.5, self._height * 0.55),
492                )
493                self._hide_field()
494                self._field_shown = False
495
496            # if they told us there's a boost button, update..
497            if response.get('bt') is not None:
498                self._boost_tickets = response['bt']
499                self._boost_strength = response['ba']
500                if self._boost_button is None:
501                    self._boost_button = bui.buttonwidget(
502                        parent=self._root_widget,
503                        scale=1.0,
504                        position=(self._width * 0.5 - 75, 20),
505                        size=(150, 100),
506                        button_type='square',
507                        label='',
508                        on_activate_call=self.on_boost_press,
509                        enable_sound=False,
510                        color=(0, 1, 0),
511                        autoselect=True,
512                    )
513                    self._boost_label = bui.textwidget(
514                        parent=self._root_widget,
515                        draw_controller=self._boost_button,
516                        position=(self._width * 0.5, 88),
517                        size=(0, 0),
518                        color=(0.8, 1.0, 0.8),
519                        scale=1.5,
520                        h_align='center',
521                        v_align='center',
522                        text=bui.Lstr(resource='boostText'),
523                        maxwidth=150,
524                    )
525                    self._boost_price = bui.textwidget(
526                        parent=self._root_widget,
527                        draw_controller=self._boost_button,
528                        position=(self._width * 0.5, 50),
529                        size=(0, 0),
530                        color=(0, 1, 0),
531                        scale=0.9,
532                        h_align='center',
533                        v_align='center',
534                        text=bui.charstr(bui.SpecialChar.TICKET)
535                        + str(self._boost_tickets),
536                        maxwidth=150,
537                    )
538            else:
539                if self._boost_button is not None:
540                    self._boost_button.delete()
541                    self._boost_button = None
542                if self._boost_price is not None:
543                    self._boost_price.delete()
544                    self._boost_price = None
545                if self._boost_label is not None:
546                    self._boost_label.delete()
547                    self._boost_label = None
548
549            # if they told us to go ahead and try and connect, do so..
550            # (note: servers will disconnect us if we try to connect before
551            # getting this go-ahead, so don't get any bright ideas...)
552            if response.get('c', False):
553                # enforce a delay between connection attempts
554                # (in case they're jamming on the boost button)
555                now = time.time()
556                if (
557                    self._last_connect_attempt_time is None
558                    or now - self._last_connect_attempt_time > 10.0
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        from bauiv1lib import getcurrency
571
572        plus = bui.app.plus
573        assert plus is not None
574
575        if plus.get_v1_account_state() != 'signed_in':
576            account.show_sign_in_prompt()
577            return
578
579        if plus.get_v1_account_ticket_count() < self._boost_tickets:
580            bui.getsound('error').play()
581            getcurrency.show_get_tickets_prompt()
582            return
583
584        bui.getsound('laserReverse').play()
585        plus.add_v1_account_transaction(
586            {
587                'type': 'PARTY_QUEUE_BOOST',
588                't': self._boost_tickets,
589                'q': self._queue_id,
590            },
591            callback=bui.WeakCall(self.on_update_response),
592        )
593        # lets not run these immediately (since they may be rapid-fire,
594        # just bucket them until the next tick)
595
596        # the transaction handles the local ticket change, but we apply our
597        # local boost vis manually here..
598        # (our visualization isn't really wired up to be transaction-based)
599        our_dude = self._dudes_by_id.get(-1)
600        if our_dude is not None:
601            our_dude.boost(self._boost_strength, self._smoothing)
602
603    def update(self) -> None:
604        """Update!"""
605        plus = bui.app.plus
606        assert plus is not None
607
608        if not self._root_widget:
609            return
610
611        # Update boost-price.
612        if self._boost_price is not None:
613            bui.textwidget(
614                edit=self._boost_price,
615                text=bui.charstr(bui.SpecialChar.TICKET)
616                + str(self._boost_tickets),
617            )
618
619        # Update boost button color based on if we have enough moola.
620        if self._boost_button is not None:
621            can_boost = (
622                plus.get_v1_account_state() == 'signed_in'
623                and plus.get_v1_account_ticket_count() >= self._boost_tickets
624            )
625            bui.buttonwidget(
626                edit=self._boost_button,
627                color=(0, 1, 0) if can_boost else (0.7, 0.7, 0.7),
628            )
629
630        # Update ticket-count.
631        if self._tickets_text is not None:
632            if self._boost_button is not None:
633                if plus.get_v1_account_state() == 'signed_in':
634                    val = bui.charstr(bui.SpecialChar.TICKET) + str(
635                        plus.get_v1_account_ticket_count()
636                    )
637                else:
638                    val = bui.charstr(bui.SpecialChar.TICKET) + '???'
639                bui.textwidget(edit=self._tickets_text, text=val)
640            else:
641                bui.textwidget(edit=self._tickets_text, text='')
642
643        current_time = bui.apptime()
644        if (
645            self._last_transaction_time is None
646            or current_time - self._last_transaction_time
647            > 0.001 * plus.get_v1_account_misc_read_val('pqInt', 5000)
648        ):
649            self._last_transaction_time = current_time
650            plus.add_v1_account_transaction(
651                {'type': 'PARTY_QUEUE_QUERY', 'q': self._queue_id},
652                callback=bui.WeakCall(self.on_update_response),
653            )
654            plus.run_v1_account_transactions()
655
656        # step our dudes
657        for dude in self._dudes:
658            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=invalid-name
 25    # pylint: disable=consider-using-dict-comprehension
 26    class Dude:
 27        """Represents a single dude waiting in a server line."""
 28
 29        def __init__(
 30            self,
 31            parent: PartyQueueWindow,
 32            distance: float,
 33            initial_offset: float,
 34            is_player: bool,
 35            account_id: str,
 36            name: str,
 37        ):
 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        bui.app.ui_v1.have_party_queue_window = True
229        self._address = address
230        self._port = port
231        self._queue_id = queue_id
232        self._width = 800
233        self._height = 400
234        self._last_connect_attempt_time: float | None = None
235        self._last_transaction_time: float | None = None
236        self._boost_button: bui.Widget | None = None
237        self._boost_price: bui.Widget | None = None
238        self._boost_label: bui.Widget | None = None
239        self._field_shown = False
240        self._dudes: list[PartyQueueWindow.Dude] = []
241        self._dudes_by_id: dict[int, PartyQueueWindow.Dude] = {}
242        self._line_left = 40.0
243        self._line_width = self._width - 190
244        self._line_bottom = self._height * 0.4
245        self.lineup_tex: bui.Texture = bui.gettexture('playerLineup')
246        self._smoothing = 0.0
247        self._initial_offset = 0.0
248        self._boost_tickets = 0
249        self._boost_strength = 0.0
250        self._angry_computer_transparent_mesh = bui.getmesh(
251            'angryComputerTransparent'
252        )
253        self._angry_computer_image: bui.Widget | None = None
254        self.lineup_1_transparent_mesh: bui.Mesh = bui.getmesh(
255            'playerLineup1Transparent'
256        )
257        self._lineup_2_transparent_mesh: bui.Mesh = bui.getmesh(
258            'playerLineup2Transparent'
259        )
260
261        self._lineup_3_transparent_mesh = bui.getmesh(
262            'playerLineup3Transparent'
263        )
264        self._lineup_4_transparent_mesh = bui.getmesh(
265            'playerLineup4Transparent'
266        )
267        self._line_image: bui.Widget | None = None
268        self.eyes_mesh: bui.Mesh = bui.getmesh('plasticEyesTransparent')
269        self._white_tex = bui.gettexture('white')
270        uiscale = bui.app.ui_v1.uiscale
271        super().__init__(
272            root_widget=bui.containerwidget(
273                size=(self._width, self._height),
274                color=(0.45, 0.63, 0.15),
275                transition='in_scale',
276                scale=(
277                    1.4
278                    if uiscale is bui.UIScale.SMALL
279                    else 1.2
280                    if uiscale is bui.UIScale.MEDIUM
281                    else 1.0
282                ),
283            )
284        )
285
286        self._cancel_button = bui.buttonwidget(
287            parent=self._root_widget,
288            scale=1.0,
289            position=(60, self._height - 80),
290            size=(50, 50),
291            label='',
292            on_activate_call=self.close,
293            autoselect=True,
294            color=(0.45, 0.63, 0.15),
295            icon=bui.gettexture('crossOut'),
296            iconscale=1.2,
297        )
298        bui.containerwidget(
299            edit=self._root_widget, cancel_button=self._cancel_button
300        )
301
302        self._title_text = bui.textwidget(
303            parent=self._root_widget,
304            position=(self._width * 0.5, self._height * 0.55),
305            size=(0, 0),
306            color=(1.0, 3.0, 1.0),
307            scale=1.3,
308            h_align='center',
309            v_align='center',
310            text=bui.Lstr(resource='internal.connectingToPartyText'),
311            maxwidth=self._width * 0.65,
312        )
313
314        self._tickets_text = bui.textwidget(
315            parent=self._root_widget,
316            position=(self._width - 180, self._height - 20),
317            size=(0, 0),
318            color=(0.2, 1.0, 0.2),
319            scale=0.7,
320            h_align='center',
321            v_align='center',
322            text='',
323        )
324
325        # update at roughly 30fps
326        self._update_timer = bui.AppTimer(
327            0.033, bui.WeakCall(self.update), repeat=True
328        )
329        self.update()
330
331    def __del__(self) -> None:
332        try:
333            plus = bui.app.plus
334            assert plus is not None
335
336            assert bui.app.classic is not None
337            bui.app.ui_v1.have_party_queue_window = False
338            plus.add_v1_account_transaction(
339                {'type': 'PARTY_QUEUE_REMOVE', 'q': self._queue_id}
340            )
341            plus.run_v1_account_transactions()
342        except Exception:
343            logging.exception('Error removing self from party queue.')
344
345    def get_line_left(self) -> float:
346        """(internal)"""
347        return self._line_left
348
349    def get_line_width(self) -> float:
350        """(internal)"""
351        return self._line_width
352
353    def get_line_bottom(self) -> float:
354        """(internal)"""
355        return self._line_bottom
356
357    def on_account_press(
358        self, account_id: str | None, origin_widget: bui.Widget
359    ) -> None:
360        """A dude was clicked so we should show his account info."""
361        from bauiv1lib.account import viewer
362
363        if account_id is None:
364            bui.getsound('error').play()
365            return
366        viewer.AccountViewerWindow(
367            account_id=account_id,
368            position=origin_widget.get_screen_space_center(),
369        )
370
371    def close(self) -> None:
372        """Close the ui."""
373        bui.containerwidget(edit=self._root_widget, transition='out_scale')
374
375    def _update_field(self, response: dict[str, Any]) -> None:
376        plus = bui.app.plus
377        assert plus is not None
378
379        if self._angry_computer_image is None:
380            self._angry_computer_image = bui.imagewidget(
381                parent=self._root_widget,
382                position=(self._width - 180, self._height * 0.5 - 65),
383                size=(150, 150),
384                texture=self.lineup_tex,
385                mesh_transparent=self._angry_computer_transparent_mesh,
386            )
387        if self._line_image is None:
388            self._line_image = bui.imagewidget(
389                parent=self._root_widget,
390                color=(0.0, 0.0, 0.0),
391                opacity=0.2,
392                position=(self._line_left, self._line_bottom - 2.0),
393                size=(self._line_width, 4.0),
394                texture=self._white_tex,
395            )
396
397        # now go through the data they sent, creating dudes for us and our
398        # enemies as needed and updating target positions on all of them..
399
400        # mark all as unclaimed so we know which ones to kill off..
401        for dude in self._dudes:
402            dude.claimed = False
403
404        # always have a dude for ourself..
405        if -1 not in self._dudes_by_id:
406            dude = self.Dude(
407                self,
408                response['d'],
409                self._initial_offset,
410                True,
411                plus.get_v1_account_misc_read_val_2('resolvedAccountID', None),
412                plus.get_v1_account_display_string(),
413            )
414            self._dudes_by_id[-1] = dude
415            self._dudes.append(dude)
416        else:
417            self._dudes_by_id[-1].set_target_distance(response['d'])
418        self._dudes_by_id[-1].claimed = True
419
420        # now create/destroy enemies
421        for (
422            enemy_id,
423            enemy_distance,
424            enemy_account_id,
425            enemy_name,
426        ) in response['e']:
427            if enemy_id not in self._dudes_by_id:
428                dude = self.Dude(
429                    self,
430                    enemy_distance,
431                    self._initial_offset,
432                    False,
433                    enemy_account_id,
434                    enemy_name,
435                )
436                self._dudes_by_id[enemy_id] = dude
437                self._dudes.append(dude)
438            else:
439                self._dudes_by_id[enemy_id].set_target_distance(enemy_distance)
440            self._dudes_by_id[enemy_id].claimed = True
441
442        # remove unclaimed dudes from both of our lists
443        # noinspection PyUnresolvedReferences
444        self._dudes_by_id = dict(
445            [
446                item
447                for item in list(self._dudes_by_id.items())
448                if item[1].claimed
449            ]
450        )
451        self._dudes = [dude for dude in self._dudes if dude.claimed]
452
453    def _hide_field(self) -> None:
454        if self._angry_computer_image:
455            self._angry_computer_image.delete()
456        self._angry_computer_image = None
457        if self._line_image:
458            self._line_image.delete()
459        self._line_image = None
460        self._dudes = []
461        self._dudes_by_id = {}
462
463    def on_update_response(self, response: dict[str, Any] | None) -> None:
464        """We've received a response from an update to the server."""
465        # pylint: disable=too-many-branches
466        if not self._root_widget:
467            return
468
469        # Seeing this in logs; debugging...
470        if not self._title_text:
471            print('PartyQueueWindows update: Have root but no title_text.')
472            return
473
474        if response is not None:
475            should_show_field = response.get('d') is not None
476            self._smoothing = response['s']
477            self._initial_offset = response['o']
478
479            # If they gave us a position, show the field.
480            if should_show_field:
481                bui.textwidget(
482                    edit=self._title_text,
483                    text=bui.Lstr(resource='waitingInLineText'),
484                    position=(self._width * 0.5, self._height * 0.85),
485                )
486                self._update_field(response)
487                self._field_shown = True
488            if not should_show_field and self._field_shown:
489                bui.textwidget(
490                    edit=self._title_text,
491                    text=bui.Lstr(resource='internal.connectingToPartyText'),
492                    position=(self._width * 0.5, self._height * 0.55),
493                )
494                self._hide_field()
495                self._field_shown = False
496
497            # if they told us there's a boost button, update..
498            if response.get('bt') is not None:
499                self._boost_tickets = response['bt']
500                self._boost_strength = response['ba']
501                if self._boost_button is None:
502                    self._boost_button = bui.buttonwidget(
503                        parent=self._root_widget,
504                        scale=1.0,
505                        position=(self._width * 0.5 - 75, 20),
506                        size=(150, 100),
507                        button_type='square',
508                        label='',
509                        on_activate_call=self.on_boost_press,
510                        enable_sound=False,
511                        color=(0, 1, 0),
512                        autoselect=True,
513                    )
514                    self._boost_label = bui.textwidget(
515                        parent=self._root_widget,
516                        draw_controller=self._boost_button,
517                        position=(self._width * 0.5, 88),
518                        size=(0, 0),
519                        color=(0.8, 1.0, 0.8),
520                        scale=1.5,
521                        h_align='center',
522                        v_align='center',
523                        text=bui.Lstr(resource='boostText'),
524                        maxwidth=150,
525                    )
526                    self._boost_price = bui.textwidget(
527                        parent=self._root_widget,
528                        draw_controller=self._boost_button,
529                        position=(self._width * 0.5, 50),
530                        size=(0, 0),
531                        color=(0, 1, 0),
532                        scale=0.9,
533                        h_align='center',
534                        v_align='center',
535                        text=bui.charstr(bui.SpecialChar.TICKET)
536                        + str(self._boost_tickets),
537                        maxwidth=150,
538                    )
539            else:
540                if self._boost_button is not None:
541                    self._boost_button.delete()
542                    self._boost_button = None
543                if self._boost_price is not None:
544                    self._boost_price.delete()
545                    self._boost_price = None
546                if self._boost_label is not None:
547                    self._boost_label.delete()
548                    self._boost_label = None
549
550            # if they told us to go ahead and try and connect, do so..
551            # (note: servers will disconnect us if we try to connect before
552            # getting this go-ahead, so don't get any bright ideas...)
553            if response.get('c', False):
554                # enforce a delay between connection attempts
555                # (in case they're jamming on the boost button)
556                now = time.time()
557                if (
558                    self._last_connect_attempt_time is None
559                    or now - self._last_connect_attempt_time > 10.0
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        from bauiv1lib import getcurrency
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            getcurrency.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)

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

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

def close(self) -> None:
371    def close(self) -> None:
372        """Close the ui."""
373        bui.containerwidget(edit=self._root_widget, transition='out_scale')

Close the ui.

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

Boost was pressed.

def update(self) -> None:
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)

Update!

Inherited Members
bauiv1._uitypes.Window
get_root_widget
class PartyQueueWindow.Dude:
 26    class Dude:
 27        """Represents a single dude waiting in a server line."""
 28
 29        def __init__(
 30            self,
 31            parent: PartyQueueWindow,
 32            distance: float,
 33            initial_offset: float,
 34            is_player: bool,
 35            account_id: str,
 36            name: str,
 37        ):
 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)
 29        def __init__(
 30            self,
 31            parent: PartyQueueWindow,
 32            distance: float,
 33            initial_offset: float,
 34            is_player: bool,
 35            account_id: str,
 36            name: str,
 37        ):
 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.