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

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

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

Close the ui.

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

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

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

Boost was pressed.

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