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

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
279                    if uiscale is bui.UIScale.MEDIUM
280                    else 1.0
281                ),
282            )
283        )
284
285        self._cancel_button = bui.buttonwidget(
286            parent=self._root_widget,
287            scale=1.0,
288            position=(60, self._height - 80),
289            size=(50, 50),
290            label='',
291            on_activate_call=self.close,
292            autoselect=True,
293            color=(0.45, 0.63, 0.15),
294            icon=bui.gettexture('crossOut'),
295            iconscale=1.2,
296        )
297        bui.containerwidget(
298            edit=self._root_widget, cancel_button=self._cancel_button
299        )
300
301        self._title_text = bui.textwidget(
302            parent=self._root_widget,
303            position=(self._width * 0.5, self._height * 0.55),
304            size=(0, 0),
305            color=(1.0, 3.0, 1.0),
306            scale=1.3,
307            h_align='center',
308            v_align='center',
309            text=bui.Lstr(resource='internal.connectingToPartyText'),
310            maxwidth=self._width * 0.65,
311        )
312
313        self._tickets_text = bui.textwidget(
314            parent=self._root_widget,
315            position=(self._width - 180, self._height - 20),
316            size=(0, 0),
317            color=(0.2, 1.0, 0.2),
318            scale=0.7,
319            h_align='center',
320            v_align='center',
321            text='',
322        )
323
324        # update at roughly 30fps
325        self._update_timer = bui.AppTimer(
326            0.033, bui.WeakCall(self.update), repeat=True
327        )
328        self.update()
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:
356    def on_account_press(
357        self, account_id: str | None, origin_widget: bui.Widget
358    ) -> None:
359        """A dude was clicked so we should show his account info."""
360        from bauiv1lib.account import viewer
361
362        if account_id is None:
363            bui.getsound('error').play()
364            return
365        viewer.AccountViewerWindow(
366            account_id=account_id,
367            position=origin_widget.get_screen_space_center(),
368        )

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

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

Close the ui.

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

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

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

Boost was pressed.

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

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.