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

Window showing players waiting to join a server.

PartyQueueWindow(queue_id: str, address: str, port: int)
172    def __init__(self, queue_id: str, address: str, port: int):
173        ba.app.ui.have_party_queue_window = True
174        self._address = address
175        self._port = port
176        self._queue_id = queue_id
177        self._width = 800
178        self._height = 400
179        self._last_connect_attempt_time: float | None = None
180        self._last_transaction_time: float | None = None
181        self._boost_button: ba.Widget | None = None
182        self._boost_price: ba.Widget | None = None
183        self._boost_label: ba.Widget | None = None
184        self._field_shown = False
185        self._dudes: list[PartyQueueWindow.Dude] = []
186        self._dudes_by_id: dict[int, PartyQueueWindow.Dude] = {}
187        self._line_left = 40.0
188        self._line_width = self._width - 190
189        self._line_bottom = self._height * 0.4
190        self.lineup_tex = ba.gettexture('playerLineup')
191        self._smoothing = 0.0
192        self._initial_offset = 0.0
193        self._boost_tickets = 0
194        self._boost_strength = 0.0
195        self._angry_computer_transparent_model = ba.getmodel(
196            'angryComputerTransparent')
197        self._angry_computer_image: ba.Widget | None = None
198        self.lineup_1_transparent_model = ba.getmodel(
199            'playerLineup1Transparent')
200        self._lineup_2_transparent_model = ba.getmodel(
201            'playerLineup2Transparent')
202        self._lineup_3_transparent_model = ba.getmodel(
203            'playerLineup3Transparent')
204        self._lineup_4_transparent_model = ba.getmodel(
205            'playerLineup4Transparent')
206        self._line_image: ba.Widget | None = None
207        self.eyes_model = ba.getmodel('plasticEyesTransparent')
208        self._white_tex = ba.gettexture('white')
209        uiscale = ba.app.ui.uiscale
210        super().__init__(root_widget=ba.containerwidget(
211            size=(self._width, self._height),
212            color=(0.45, 0.63, 0.15),
213            transition='in_scale',
214            scale=(1.4 if uiscale is ba.UIScale.SMALL else
215                   1.2 if uiscale is ba.UIScale.MEDIUM else 1.0)))
216
217        self._cancel_button = ba.buttonwidget(parent=self._root_widget,
218                                              scale=1.0,
219                                              position=(60, self._height - 80),
220                                              size=(50, 50),
221                                              label='',
222                                              on_activate_call=self.close,
223                                              autoselect=True,
224                                              color=(0.45, 0.63, 0.15),
225                                              icon=ba.gettexture('crossOut'),
226                                              iconscale=1.2)
227        ba.containerwidget(edit=self._root_widget,
228                           cancel_button=self._cancel_button)
229
230        self._title_text = ba.textwidget(
231            parent=self._root_widget,
232            position=(self._width * 0.5, self._height * 0.55),
233            size=(0, 0),
234            color=(1.0, 3.0, 1.0),
235            scale=1.3,
236            h_align='center',
237            v_align='center',
238            text=ba.Lstr(resource='internal.connectingToPartyText'),
239            maxwidth=self._width * 0.65)
240
241        self._tickets_text = ba.textwidget(parent=self._root_widget,
242                                           position=(self._width - 180,
243                                                     self._height - 20),
244                                           size=(0, 0),
245                                           color=(0.2, 1.0, 0.2),
246                                           scale=0.7,
247                                           h_align='center',
248                                           v_align='center',
249                                           text='')
250
251        # update at roughly 30fps
252        self._update_timer = ba.Timer(0.033,
253                                      ba.WeakCall(self.update),
254                                      repeat=True,
255                                      timetype=ba.TimeType.REAL)
256        self.update()
def on_account_press(self, account_id: str | None, origin_widget: _ba.Widget) -> None:
281    def on_account_press(self, account_id: str | None,
282                         origin_widget: ba.Widget) -> None:
283        """A dude was clicked so we should show his account info."""
284        from bastd.ui.account import viewer
285        if account_id is None:
286            ba.playsound(ba.getsound('error'))
287            return
288        viewer.AccountViewerWindow(
289            account_id=account_id,
290            position=origin_widget.get_screen_space_center())

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

def close(self) -> None:
292    def close(self) -> None:
293        """Close the ui."""
294        ba.containerwidget(edit=self._root_widget, transition='out_scale')

Close the ui.

def on_update_response(self, response: dict[str, typing.Any] | None) -> None:
361    def on_update_response(self, response: dict[str, Any] | None) -> None:
362        """We've received a response from an update to the server."""
363        # pylint: disable=too-many-branches
364        if not self._root_widget:
365            return
366
367        # Seeing this in logs; debugging...
368        if not self._title_text:
369            print('PartyQueueWindows update: Have root but no title_text.')
370            return
371
372        if response is not None:
373            should_show_field = (response.get('d') is not None)
374            self._smoothing = response['s']
375            self._initial_offset = response['o']
376
377            # If they gave us a position, show the field.
378            if should_show_field:
379                ba.textwidget(edit=self._title_text,
380                              text=ba.Lstr(resource='waitingInLineText'),
381                              position=(self._width * 0.5,
382                                        self._height * 0.85))
383                self._update_field(response)
384                self._field_shown = True
385            if not should_show_field and self._field_shown:
386                ba.textwidget(
387                    edit=self._title_text,
388                    text=ba.Lstr(resource='internal.connectingToPartyText'),
389                    position=(self._width * 0.5, self._height * 0.55))
390                self._hide_field()
391                self._field_shown = False
392
393            # if they told us there's a boost button, update..
394            if response.get('bt') is not None:
395                self._boost_tickets = response['bt']
396                self._boost_strength = response['ba']
397                if self._boost_button is None:
398                    self._boost_button = ba.buttonwidget(
399                        parent=self._root_widget,
400                        scale=1.0,
401                        position=(self._width * 0.5 - 75, 20),
402                        size=(150, 100),
403                        button_type='square',
404                        label='',
405                        on_activate_call=self.on_boost_press,
406                        enable_sound=False,
407                        color=(0, 1, 0),
408                        autoselect=True)
409                    self._boost_label = ba.textwidget(
410                        parent=self._root_widget,
411                        draw_controller=self._boost_button,
412                        position=(self._width * 0.5, 88),
413                        size=(0, 0),
414                        color=(0.8, 1.0, 0.8),
415                        scale=1.5,
416                        h_align='center',
417                        v_align='center',
418                        text=ba.Lstr(resource='boostText'),
419                        maxwidth=150)
420                    self._boost_price = ba.textwidget(
421                        parent=self._root_widget,
422                        draw_controller=self._boost_button,
423                        position=(self._width * 0.5, 50),
424                        size=(0, 0),
425                        color=(0, 1, 0),
426                        scale=0.9,
427                        h_align='center',
428                        v_align='center',
429                        text=ba.charstr(ba.SpecialChar.TICKET) +
430                        str(self._boost_tickets),
431                        maxwidth=150)
432            else:
433                if self._boost_button is not None:
434                    self._boost_button.delete()
435                    self._boost_button = None
436                if self._boost_price is not None:
437                    self._boost_price.delete()
438                    self._boost_price = None
439                if self._boost_label is not None:
440                    self._boost_label.delete()
441                    self._boost_label = None
442
443            # if they told us to go ahead and try and connect, do so..
444            # (note: servers will disconnect us if we try to connect before
445            # getting this go-ahead, so don't get any bright ideas...)
446            if response.get('c', False):
447                # enforce a delay between connection attempts
448                # (in case they're jamming on the boost button)
449                now = time.time()
450                if (self._last_connect_attempt_time is None
451                        or now - self._last_connect_attempt_time > 10.0):
452                    _ba.connect_to_party(address=self._address,
453                                         port=self._port,
454                                         print_progress=False)
455                    self._last_connect_attempt_time = now

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

def on_boost_press(self) -> None:
457    def on_boost_press(self) -> None:
458        """Boost was pressed."""
459        from bastd.ui import account
460        from bastd.ui import getcurrency
461        if _ba.get_v1_account_state() != 'signed_in':
462            account.show_sign_in_prompt()
463            return
464
465        if _ba.get_v1_account_ticket_count() < self._boost_tickets:
466            ba.playsound(ba.getsound('error'))
467            getcurrency.show_get_tickets_prompt()
468            return
469
470        ba.playsound(ba.getsound('laserReverse'))
471        _ba.add_transaction(
472            {
473                'type': 'PARTY_QUEUE_BOOST',
474                't': self._boost_tickets,
475                'q': self._queue_id
476            },
477            callback=ba.WeakCall(self.on_update_response))
478        # lets not run these immediately (since they may be rapid-fire,
479        # just bucket them until the next tick)
480
481        # the transaction handles the local ticket change, but we apply our
482        # local boost vis manually here..
483        # (our visualization isn't really wired up to be transaction-based)
484        our_dude = self._dudes_by_id.get(-1)
485        if our_dude is not None:
486            our_dude.boost(self._boost_strength, self._smoothing)

Boost was pressed.

def update(self) -> None:
488    def update(self) -> None:
489        """Update!"""
490        if not self._root_widget:
491            return
492
493        # Update boost-price.
494        if self._boost_price is not None:
495            ba.textwidget(edit=self._boost_price,
496                          text=ba.charstr(ba.SpecialChar.TICKET) +
497                          str(self._boost_tickets))
498
499        # Update boost button color based on if we have enough moola.
500        if self._boost_button is not None:
501            can_boost = (
502                (_ba.get_v1_account_state() == 'signed_in'
503                 and _ba.get_v1_account_ticket_count() >= self._boost_tickets))
504            ba.buttonwidget(edit=self._boost_button,
505                            color=(0, 1, 0) if can_boost else (0.7, 0.7, 0.7))
506
507        # Update ticket-count.
508        if self._tickets_text is not None:
509            if self._boost_button is not None:
510                if _ba.get_v1_account_state() == 'signed_in':
511                    val = ba.charstr(ba.SpecialChar.TICKET) + str(
512                        _ba.get_v1_account_ticket_count())
513                else:
514                    val = ba.charstr(ba.SpecialChar.TICKET) + '???'
515                ba.textwidget(edit=self._tickets_text, text=val)
516            else:
517                ba.textwidget(edit=self._tickets_text, text='')
518
519        current_time = ba.time(ba.TimeType.REAL)
520        if (self._last_transaction_time is None
521                or current_time - self._last_transaction_time >
522                0.001 * _ba.get_v1_account_misc_read_val('pqInt', 5000)):
523            self._last_transaction_time = current_time
524            _ba.add_transaction(
525                {
526                    'type': 'PARTY_QUEUE_QUERY',
527                    'q': self._queue_id
528                },
529                callback=ba.WeakCall(self.on_update_response))
530            _ba.run_transactions()
531
532        # step our dudes
533        for dude in self._dudes:
534            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__(self, parent: PartyQueueWindow, distance: float,
 29                     initial_offset: float, is_player: bool, account_id: str,
 30                     name: str):
 31            self.claimed = False
 32            self._line_left = parent.get_line_left()
 33            self._line_width = parent.get_line_width()
 34            self._line_bottom = parent.get_line_bottom()
 35            self._target_distance = distance
 36            self._distance = distance + initial_offset
 37            self._boost_brightness = 0.0
 38            self._debug = False
 39            self._sc = sc = 1.1 if is_player else 0.6 + random.random() * 0.2
 40            self._y_offs = -30.0 if is_player else -47.0 * sc
 41            self._last_boost_time = 0.0
 42            self._color = (0.2, 1.0,
 43                           0.2) if is_player else (0.5 + 0.3 * random.random(),
 44                                                   0.4 + 0.2 * random.random(),
 45                                                   0.5 + 0.3 * random.random())
 46            self._eye_color = (0.7 * 1.0 + 0.3 * self._color[0],
 47                               0.7 * 1.0 + 0.3 * self._color[1],
 48                               0.7 * 1.0 + 0.3 * self._color[2])
 49            self._body_image = ba.buttonwidget(
 50                parent=parent.get_root_widget(),
 51                selectable=True,
 52                label='',
 53                size=(sc * 60, sc * 80),
 54                color=self._color,
 55                texture=parent.lineup_tex,
 56                model_transparent=parent.lineup_1_transparent_model)
 57            ba.buttonwidget(edit=self._body_image,
 58                            on_activate_call=ba.WeakCall(
 59                                parent.on_account_press, account_id,
 60                                self._body_image))
 61            ba.widget(edit=self._body_image, autoselect=True)
 62            self._eyes_image = ba.imagewidget(
 63                parent=parent.get_root_widget(),
 64                size=(sc * 36, sc * 18),
 65                texture=parent.lineup_tex,
 66                color=self._eye_color,
 67                model_transparent=parent.eyes_model)
 68            self._name_text = ba.textwidget(parent=parent.get_root_widget(),
 69                                            size=(0, 0),
 70                                            shadow=0,
 71                                            flatness=1.0,
 72                                            text=name,
 73                                            maxwidth=100,
 74                                            h_align='center',
 75                                            v_align='center',
 76                                            scale=0.75,
 77                                            color=(1, 1, 1, 0.6))
 78            self._update_image()
 79
 80            # DEBUG: vis target pos..
 81            self._body_image_target: ba.Widget | None
 82            self._eyes_image_target: ba.Widget | None
 83            if self._debug:
 84                self._body_image_target = ba.imagewidget(
 85                    parent=parent.get_root_widget(),
 86                    size=(sc * 60, sc * 80),
 87                    color=self._color,
 88                    texture=parent.lineup_tex,
 89                    model_transparent=parent.lineup_1_transparent_model)
 90                self._eyes_image_target = ba.imagewidget(
 91                    parent=parent.get_root_widget(),
 92                    size=(sc * 36, sc * 18),
 93                    texture=parent.lineup_tex,
 94                    color=self._eye_color,
 95                    model_transparent=parent.eyes_model)
 96                # (updates our image positions)
 97                self.set_target_distance(self._target_distance)
 98            else:
 99                self._body_image_target = self._eyes_image_target = None
100
101        def __del__(self) -> None:
102
103            # ew.  our destructor here may get called as part of an internal
104            # widget tear-down.
105            # running further widget calls here can quietly break stuff, so we
106            # need to push a deferred call to kill these as necessary instead.
107            # (should bulletproof internal widget code to give a clean error
108            # in this case)
109            def kill_widgets(widgets: Sequence[ba.Widget | None]) -> None:
110                for widget in widgets:
111                    if widget:
112                        widget.delete()
113
114            ba.pushcall(
115                ba.Call(kill_widgets, [
116                    self._body_image, self._eyes_image,
117                    self._body_image_target, self._eyes_image_target,
118                    self._name_text
119                ]))
120
121        def set_target_distance(self, dist: float) -> None:
122            """Set distance for a dude."""
123            self._target_distance = dist
124            if self._debug:
125                sc = self._sc
126                position = (self._line_left + self._line_width *
127                            (1.0 - self._target_distance),
128                            self._line_bottom - 30)
129                ba.imagewidget(edit=self._body_image_target,
130                               position=(position[0] - sc * 30,
131                                         position[1] - sc * 25 - 70))
132                ba.imagewidget(edit=self._eyes_image_target,
133                               position=(position[0] - sc * 18,
134                                         position[1] + sc * 31 - 70))
135
136        def step(self, smoothing: float) -> None:
137            """Step this dude."""
138            self._distance = (smoothing * self._distance +
139                              (1.0 - smoothing) * self._target_distance)
140            self._update_image()
141            self._boost_brightness *= 0.9
142
143        def _update_image(self) -> None:
144            sc = self._sc
145            position = (self._line_left + self._line_width *
146                        (1.0 - self._distance), self._line_bottom + 40)
147            brightness = 1.0 + self._boost_brightness
148            ba.buttonwidget(edit=self._body_image,
149                            position=(position[0] - sc * 30,
150                                      position[1] - sc * 25 + self._y_offs),
151                            color=(self._color[0] * brightness,
152                                   self._color[1] * brightness,
153                                   self._color[2] * brightness))
154            ba.imagewidget(edit=self._eyes_image,
155                           position=(position[0] - sc * 18,
156                                     position[1] + sc * 31 + self._y_offs),
157                           color=(self._eye_color[0] * brightness,
158                                  self._eye_color[1] * brightness,
159                                  self._eye_color[2] * brightness))
160            ba.textwidget(edit=self._name_text,
161                          position=(position[0] - sc * 0,
162                                    position[1] + sc * 40.0))
163
164        def boost(self, amount: float, smoothing: float) -> None:
165            """Boost this dude."""
166            del smoothing  # unused arg
167            self._distance = max(0.0, self._distance - amount)
168            self._update_image()
169            self._last_boost_time = time.time()
170            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__(self, parent: PartyQueueWindow, distance: float,
29                     initial_offset: float, is_player: bool, account_id: str,
30                     name: str):
31            self.claimed = False
32            self._line_left = parent.get_line_left()
33            self._line_width = parent.get_line_width()
34            self._line_bottom = parent.get_line_bottom()
35            self._target_distance = distance
36            self._distance = distance + initial_offset
37            self._boost_brightness = 0.0
38            self._debug = False
39            self._sc = sc = 1.1 if is_player else 0.6 + random.random() * 0.2
40            self._y_offs = -30.0 if is_player else -47.0 * sc
41            self._last_boost_time = 0.0
42            self._color = (0.2, 1.0,
43                           0.2) if is_player else (0.5 + 0.3 * random.random(),
44                                                   0.4 + 0.2 * random.random(),
45                                                   0.5 + 0.3 * random.random())
46            self._eye_color = (0.7 * 1.0 + 0.3 * self._color[0],
47                               0.7 * 1.0 + 0.3 * self._color[1],
48                               0.7 * 1.0 + 0.3 * self._color[2])
49            self._body_image = ba.buttonwidget(
50                parent=parent.get_root_widget(),
51                selectable=True,
52                label='',
53                size=(sc * 60, sc * 80),
54                color=self._color,
55                texture=parent.lineup_tex,
56                model_transparent=parent.lineup_1_transparent_model)
57            ba.buttonwidget(edit=self._body_image,
58                            on_activate_call=ba.WeakCall(
59                                parent.on_account_press, account_id,
60                                self._body_image))
61            ba.widget(edit=self._body_image, autoselect=True)
62            self._eyes_image = ba.imagewidget(
63                parent=parent.get_root_widget(),
64                size=(sc * 36, sc * 18),
65                texture=parent.lineup_tex,
66                color=self._eye_color,
67                model_transparent=parent.eyes_model)
68            self._name_text = ba.textwidget(parent=parent.get_root_widget(),
69                                            size=(0, 0),
70                                            shadow=0,
71                                            flatness=1.0,
72                                            text=name,
73                                            maxwidth=100,
74                                            h_align='center',
75                                            v_align='center',
76                                            scale=0.75,
77                                            color=(1, 1, 1, 0.6))
78            self._update_image()
79
80            # DEBUG: vis target pos..
81            self._body_image_target: ba.Widget | None
82            self._eyes_image_target: ba.Widget | None
83            if self._debug:
84                self._body_image_target = ba.imagewidget(
85                    parent=parent.get_root_widget(),
86                    size=(sc * 60, sc * 80),
87                    color=self._color,
88                    texture=parent.lineup_tex,
89                    model_transparent=parent.lineup_1_transparent_model)
90                self._eyes_image_target = ba.imagewidget(
91                    parent=parent.get_root_widget(),
92                    size=(sc * 36, sc * 18),
93                    texture=parent.lineup_tex,
94                    color=self._eye_color,
95                    model_transparent=parent.eyes_model)
96                # (updates our image positions)
97                self.set_target_distance(self._target_distance)
98            else:
99                self._body_image_target = self._eyes_image_target = None
def set_target_distance(self, dist: float) -> None:
121        def set_target_distance(self, dist: float) -> None:
122            """Set distance for a dude."""
123            self._target_distance = dist
124            if self._debug:
125                sc = self._sc
126                position = (self._line_left + self._line_width *
127                            (1.0 - self._target_distance),
128                            self._line_bottom - 30)
129                ba.imagewidget(edit=self._body_image_target,
130                               position=(position[0] - sc * 30,
131                                         position[1] - sc * 25 - 70))
132                ba.imagewidget(edit=self._eyes_image_target,
133                               position=(position[0] - sc * 18,
134                                         position[1] + sc * 31 - 70))

Set distance for a dude.

def step(self, smoothing: float) -> None:
136        def step(self, smoothing: float) -> None:
137            """Step this dude."""
138            self._distance = (smoothing * self._distance +
139                              (1.0 - smoothing) * self._target_distance)
140            self._update_image()
141            self._boost_brightness *= 0.9

Step this dude.

def boost(self, amount: float, smoothing: float) -> None:
164        def boost(self, amount: float, smoothing: float) -> None:
165            """Boost this dude."""
166            del smoothing  # unused arg
167            self._distance = max(0.0, self._distance - amount)
168            self._update_image()
169            self._last_boost_time = time.time()
170            self._boost_brightness += 0.6

Boost this dude.