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

Window showing players waiting to join a server.

PartyQueueWindow(queue_id: str, address: str, port: int)
226    def __init__(self, queue_id: str, address: str, port: int):
227        ba.app.ui.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: ba.Widget | None = None
236        self._boost_price: ba.Widget | None = None
237        self._boost_label: ba.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 = ba.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_model = ba.getmodel(
250            'angryComputerTransparent'
251        )
252        self._angry_computer_image: ba.Widget | None = None
253        self.lineup_1_transparent_model = ba.getmodel(
254            'playerLineup1Transparent'
255        )
256        self._lineup_2_transparent_model = ba.getmodel(
257            'playerLineup2Transparent'
258        )
259        self._lineup_3_transparent_model = ba.getmodel(
260            'playerLineup3Transparent'
261        )
262        self._lineup_4_transparent_model = ba.getmodel(
263            'playerLineup4Transparent'
264        )
265        self._line_image: ba.Widget | None = None
266        self.eyes_model = ba.getmodel('plasticEyesTransparent')
267        self._white_tex = ba.gettexture('white')
268        uiscale = ba.app.ui.uiscale
269        super().__init__(
270            root_widget=ba.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 ba.UIScale.SMALL
277                    else 1.2
278                    if uiscale is ba.UIScale.MEDIUM
279                    else 1.0
280                ),
281            )
282        )
283
284        self._cancel_button = ba.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=ba.gettexture('crossOut'),
294            iconscale=1.2,
295        )
296        ba.containerwidget(
297            edit=self._root_widget, cancel_button=self._cancel_button
298        )
299
300        self._title_text = ba.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=ba.Lstr(resource='internal.connectingToPartyText'),
309            maxwidth=self._width * 0.65,
310        )
311
312        self._tickets_text = ba.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 = ba.Timer(
325            0.033,
326            ba.WeakCall(self.update),
327            repeat=True,
328            timetype=ba.TimeType.REAL,
329        )
330        self.update()
def on_account_press(self, account_id: str | None, origin_widget: _ba.Widget) -> None:
354    def on_account_press(
355        self, account_id: str | None, origin_widget: ba.Widget
356    ) -> None:
357        """A dude was clicked so we should show his account info."""
358        from bastd.ui.account import viewer
359
360        if account_id is None:
361            ba.playsound(ba.getsound('error'))
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        ba.containerwidget(edit=self._root_widget, transition='out_scale')

Close the ui.

def on_update_response(self, response: dict[str, typing.Any] | None) -> None:
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                ba.textwidget(
478                    edit=self._title_text,
479                    text=ba.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                ba.textwidget(
486                    edit=self._title_text,
487                    text=ba.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 = ba.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 = ba.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=ba.Lstr(resource='boostText'),
520                        maxwidth=150,
521                    )
522                    self._boost_price = ba.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=ba.charstr(ba.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                    ba.internal.connect_to_party(
558                        address=self._address,
559                        port=self._port,
560                        print_progress=False,
561                    )
562                    self._last_connect_attempt_time = now

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

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

Boost was pressed.

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

Update!

Inherited Members
ba.ui.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 = ba.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                model_transparent=parent.lineup_1_transparent_model,
 70            )
 71            ba.buttonwidget(
 72                edit=self._body_image,
 73                on_activate_call=ba.WeakCall(
 74                    parent.on_account_press, account_id, self._body_image
 75                ),
 76            )
 77            ba.widget(edit=self._body_image, autoselect=True)
 78            self._eyes_image = ba.imagewidget(
 79                parent=parent.get_root_widget(),
 80                size=(sc * 36, sc * 18),
 81                texture=parent.lineup_tex,
 82                color=self._eye_color,
 83                model_transparent=parent.eyes_model,
 84            )
 85            self._name_text = ba.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: ba.Widget | None
101            self._eyes_image_target: ba.Widget | None
102            if self._debug:
103                self._body_image_target = ba.imagewidget(
104                    parent=parent.get_root_widget(),
105                    size=(sc * 60, sc * 80),
106                    color=self._color,
107                    texture=parent.lineup_tex,
108                    model_transparent=parent.lineup_1_transparent_model,
109                )
110                self._eyes_image_target = ba.imagewidget(
111                    parent=parent.get_root_widget(),
112                    size=(sc * 36, sc * 18),
113                    texture=parent.lineup_tex,
114                    color=self._eye_color,
115                    model_transparent=parent.eyes_model,
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
124            # ew.  our destructor here may get called as part of an internal
125            # widget tear-down.
126            # running further widget calls here can quietly break stuff, so we
127            # need to push a deferred call to kill these as necessary instead.
128            # (should bulletproof internal widget code to give a clean error
129            # in this case)
130            def kill_widgets(widgets: Sequence[ba.Widget | None]) -> None:
131                for widget in widgets:
132                    if widget:
133                        widget.delete()
134
135            ba.pushcall(
136                ba.Call(
137                    kill_widgets,
138                    [
139                        self._body_image,
140                        self._eyes_image,
141                        self._body_image_target,
142                        self._eyes_image_target,
143                        self._name_text,
144                    ],
145                )
146            )
147
148        def set_target_distance(self, dist: float) -> None:
149            """Set distance for a dude."""
150            self._target_distance = dist
151            if self._debug:
152                sc = self._sc
153                position = (
154                    self._line_left
155                    + self._line_width * (1.0 - self._target_distance),
156                    self._line_bottom - 30,
157                )
158                ba.imagewidget(
159                    edit=self._body_image_target,
160                    position=(
161                        position[0] - sc * 30,
162                        position[1] - sc * 25 - 70,
163                    ),
164                )
165                ba.imagewidget(
166                    edit=self._eyes_image_target,
167                    position=(
168                        position[0] - sc * 18,
169                        position[1] + sc * 31 - 70,
170                    ),
171                )
172
173        def step(self, smoothing: float) -> None:
174            """Step this dude."""
175            self._distance = (
176                smoothing * self._distance
177                + (1.0 - smoothing) * self._target_distance
178            )
179            self._update_image()
180            self._boost_brightness *= 0.9
181
182        def _update_image(self) -> None:
183            sc = self._sc
184            position = (
185                self._line_left + self._line_width * (1.0 - self._distance),
186                self._line_bottom + 40,
187            )
188            brightness = 1.0 + self._boost_brightness
189            ba.buttonwidget(
190                edit=self._body_image,
191                position=(
192                    position[0] - sc * 30,
193                    position[1] - sc * 25 + self._y_offs,
194                ),
195                color=(
196                    self._color[0] * brightness,
197                    self._color[1] * brightness,
198                    self._color[2] * brightness,
199                ),
200            )
201            ba.imagewidget(
202                edit=self._eyes_image,
203                position=(
204                    position[0] - sc * 18,
205                    position[1] + sc * 31 + self._y_offs,
206                ),
207                color=(
208                    self._eye_color[0] * brightness,
209                    self._eye_color[1] * brightness,
210                    self._eye_color[2] * brightness,
211                ),
212            )
213            ba.textwidget(
214                edit=self._name_text,
215                position=(position[0] - sc * 0, position[1] + sc * 40.0),
216            )
217
218        def boost(self, amount: float, smoothing: float) -> None:
219            """Boost this dude."""
220            del smoothing  # unused arg
221            self._distance = max(0.0, self._distance - amount)
222            self._update_image()
223            self._last_boost_time = time.time()
224            self._boost_brightness += 0.6

Represents a single dude waiting in a server line.

PartyQueueWindow.Dude( parent: bastd.ui.partyqueue.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 = ba.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                model_transparent=parent.lineup_1_transparent_model,
 70            )
 71            ba.buttonwidget(
 72                edit=self._body_image,
 73                on_activate_call=ba.WeakCall(
 74                    parent.on_account_press, account_id, self._body_image
 75                ),
 76            )
 77            ba.widget(edit=self._body_image, autoselect=True)
 78            self._eyes_image = ba.imagewidget(
 79                parent=parent.get_root_widget(),
 80                size=(sc * 36, sc * 18),
 81                texture=parent.lineup_tex,
 82                color=self._eye_color,
 83                model_transparent=parent.eyes_model,
 84            )
 85            self._name_text = ba.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: ba.Widget | None
101            self._eyes_image_target: ba.Widget | None
102            if self._debug:
103                self._body_image_target = ba.imagewidget(
104                    parent=parent.get_root_widget(),
105                    size=(sc * 60, sc * 80),
106                    color=self._color,
107                    texture=parent.lineup_tex,
108                    model_transparent=parent.lineup_1_transparent_model,
109                )
110                self._eyes_image_target = ba.imagewidget(
111                    parent=parent.get_root_widget(),
112                    size=(sc * 36, sc * 18),
113                    texture=parent.lineup_tex,
114                    color=self._eye_color,
115                    model_transparent=parent.eyes_model,
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
def set_target_distance(self, dist: float) -> None:
148        def set_target_distance(self, dist: float) -> None:
149            """Set distance for a dude."""
150            self._target_distance = dist
151            if self._debug:
152                sc = self._sc
153                position = (
154                    self._line_left
155                    + self._line_width * (1.0 - self._target_distance),
156                    self._line_bottom - 30,
157                )
158                ba.imagewidget(
159                    edit=self._body_image_target,
160                    position=(
161                        position[0] - sc * 30,
162                        position[1] - sc * 25 - 70,
163                    ),
164                )
165                ba.imagewidget(
166                    edit=self._eyes_image_target,
167                    position=(
168                        position[0] - sc * 18,
169                        position[1] + sc * 31 - 70,
170                    ),
171                )

Set distance for a dude.

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

Step this dude.

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

Boost this dude.