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

Window showing players waiting to join a server.

PartyQueueWindow(queue_id: str, address: str, port: int)
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()
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:
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 import viewer
356
357        if account_id is None:
358            bui.getsound('error').play()
359            return
360        viewer.AccountViewerWindow(
361            account_id=account_id,
362            position=origin_widget.get_screen_space_center(),
363        )

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

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

Close the ui.

def on_update_response(self, response: dict[str, typing.Any] | None) -> None:
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                    bs.connect_to_party(
556                        address=self._address,
557                        port=self._port,
558                        print_progress=False,
559                    )
560                    self._last_connect_attempt_time = now

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

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

Boost was pressed.

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

Update!

Inherited Members
bauiv1._uitypes.Window
get_root_widget
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            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

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            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
claimed
def set_target_distance(self, dist: float) -> None:
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                )

Set distance for a dude.

def step(self, smoothing: float) -> None:
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

Step this dude.

def boost(self, amount: float, smoothing: float) -> None:
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

Boost this dude.