diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index 2722a296..8c54e556 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -1556,6 +1556,7 @@ spammy sparx spawner + spawners spawnpoints spawnpt spawntype diff --git a/assets/src/ba_data/python/ba/_gameactivity.py b/assets/src/ba_data/python/ba/_gameactivity.py index de6ec230..c64af244 100644 --- a/assets/src/ba_data/python/ba/_gameactivity.py +++ b/assets/src/ba_data/python/ba/_gameactivity.py @@ -34,6 +34,7 @@ if TYPE_CHECKING: from typing import (List, Optional, Dict, Type, Any, Callable, Sequence, Tuple, Union) from bastd.actor.playerspaz import PlayerSpaz + from bastd.actor.bomb import TNTSpawner import ba @@ -330,7 +331,7 @@ class GameActivity(Activity): self._map_type.preload() self._map: Optional[ba.Map] = None self._powerup_drop_timer: Optional[ba.Timer] = None - self._tnt_objs: Optional[Dict[int, Any]] = None + self._tnt_spawners: Optional[Dict[int, TNTSpawner]] = None self._tnt_drop_timer: Optional[ba.Timer] = None self.initial_player_info: Optional[List[Dict[str, Any]]] = None self._game_scoreboard_name_text: Optional[ba.Actor] = None @@ -1111,12 +1112,8 @@ class GameActivity(Activity): repeat=True) self._standard_drop_powerups() if enable_tnt: - self._tnt_objs = {} - self._tnt_drop_timer = _ba.Timer(5.5, - _general.WeakCall( - self._standard_drop_tnt), - repeat=True) - self._standard_drop_tnt() + self._tnt_spawners = {} + self._setup_standard_tnt_drops() def _standard_drop_powerup(self, index: int, expire: bool = True) -> None: # pylint: disable=cyclic-import @@ -1136,24 +1133,15 @@ class GameActivity(Activity): _ba.timer(i * 0.4, _general.WeakCall(self._standard_drop_powerup, i)) - def _standard_drop_tnt(self) -> None: + def _setup_standard_tnt_drops(self) -> None: """Standard tnt drop.""" # pylint: disable=cyclic-import - from bastd.actor import bomb + from bastd.actor.bomb import TNTSpawner - # Drop TNT on the map for any tnt location with no existing tnt box. for i, point in enumerate(self.map.tnt_points): - assert self._tnt_objs is not None - if i not in self._tnt_objs: - self._tnt_objs[i] = {'absent_ticks': 9999, 'obj': None} - tnt_obj = self._tnt_objs[i] - - # Respawn once its been dead for a while. - if not tnt_obj['obj']: - tnt_obj['absent_ticks'] += 1 - if tnt_obj['absent_ticks'] > 3: - tnt_obj['obj'] = bomb.Bomb(position=point, bomb_type='tnt') - tnt_obj['absent_ticks'] = 0 + assert self._tnt_spawners is not None + if self._tnt_spawners.get(i) is None: + self._tnt_spawners[i] = TNTSpawner(point) def setup_standard_time_limit(self, duration: float) -> None: """ diff --git a/assets/src/ba_data/python/ba/_netutils.py b/assets/src/ba_data/python/ba/_netutils.py index ace052dd..8369d335 100644 --- a/assets/src/ba_data/python/ba/_netutils.py +++ b/assets/src/ba_data/python/ba/_netutils.py @@ -87,16 +87,18 @@ class ServerCallThread(threading.Thread): activity) if activity is not None else None def _run_callback(self, arg: Union[None, Dict[str, Any]]) -> None: - # If we were created in an activity context and that activity has - # since died, do nothing (hmm should we be using a context-call - # instead of doing this manually?). - activity = None if self._activity is None else self._activity() - if activity is None or activity.is_expired(): - return + # since died, do nothing. + # FIXME: Should we just be using a ContextCall instead of doing + # this check manually? + if self._activity is not None: + activity = self._activity() + if activity is None or activity.is_expired(): + return # Technically we could do the same check for session contexts, # but not gonna worry about it for now. + assert self._context is not None assert self._callback is not None with self._context: self._callback(arg) diff --git a/assets/src/ba_data/python/bastd/activity/coopscorescreen.py b/assets/src/ba_data/python/bastd/activity/coopscorescreen.py index d97ad1e7..f96ca753 100644 --- a/assets/src/ba_data/python/bastd/activity/coopscorescreen.py +++ b/assets/src/ba_data/python/bastd/activity/coopscorescreen.py @@ -29,6 +29,8 @@ from typing import TYPE_CHECKING import _ba import ba from ba.internal import get_achievements_for_coop_level +from bastd.actor.text import Text +from bastd.actor.zoomtext import ZoomText if TYPE_CHECKING: from typing import Optional, Tuple, List, Dict, Any, Sequence @@ -50,9 +52,6 @@ class CoopScoreScreen(ba.Activity): self.inherits_music = True self.use_fixed_vr_overlay = True - self._tournament_time_remaining = None - self._tournament_time_remaining_text = None - self._do_new_rating: bool = self.session.tournament_id is not None self._score_display_sound = ba.getsound("scoreHit01") @@ -134,6 +133,8 @@ class CoopScoreScreen(ba.Activity): self._name_str: Optional[str] = None self._friends_loading_status: Optional[ba.Actor] = None self._score_loading_status: Optional[ba.Actor] = None + self._tournament_time_remaining: Optional[float] = None + self._tournament_time_remaining_text: Optional[Text] = None self._tournament_time_remaining_text_timer: Optional[ba.Timer] = None self._player_info = settings['player_info'] @@ -288,7 +289,6 @@ class CoopScoreScreen(ba.Activity): ba.open_url(self._score_link) def _ui_error(self) -> None: - from bastd.actor.text import Text with ba.Context(self): self._next_level_error = Text( ba.Lstr(resource='completeThisLevelToProceedText'), @@ -515,8 +515,6 @@ class CoopScoreScreen(ba.Activity): # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals - from bastd.actor.text import Text - from bastd.actor.zoomtext import ZoomText super().on_begin() # Calc whether the level is complete and other stuff. @@ -893,7 +891,6 @@ class CoopScoreScreen(ba.Activity): # pylint: disable=too-many-branches # pylint: disable=too-many-statements # delay a bit if results come in too fast - from bastd.actor.text import Text base_delay = max(0, 1.9 - (ba.time() - self._begin_time)) ts_height = 300 ts_h_offs = -550 @@ -1009,7 +1006,6 @@ class CoopScoreScreen(ba.Activity): # We need to manually run this in the context of our activity # and only if we aren't shutting down. # (really should make the submit_score call handle that stuff itself) - from bastd.actor.text import Text if self.is_expired(): return with ba.Context(self): @@ -1169,10 +1165,9 @@ class CoopScoreScreen(ba.Activity): # pylint: disable=too-many-branches # pylint: disable=too-many-statements from ba.internal import get_tournament_prize_strings - from bastd.actor.text import Text - from bastd.actor.zoomtext import ZoomText assert self._show_info is not None available = (self._show_info['results'] is not None) + if available: error = (self._show_info['results']['error'] if 'error' in self._show_info['results'] else None) @@ -1193,22 +1188,22 @@ class CoopScoreScreen(ba.Activity): Text(ba.Lstr(resource='coopSelectWindow.timeRemainingText'), position=(-360, -70 - 100), color=(1, 1, 1, 0.7), - h_align='center', - v_align='center', - transition='fade_in', + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + transition=Text.Transition.FADE_IN, scale=0.8, maxwidth=300, transition_delay=2.0).autoretain() - self._tournament_time_remaining_text = Text('', - position=(-360, - -110 - 100), - color=(1, 1, 1, 0.7), - h_align='center', - v_align='center', - transition='fade_in', - scale=1.6, - maxwidth=150, - transition_delay=2.0) + self._tournament_time_remaining_text = Text( + '', + position=(-360, -110 - 100), + color=(1, 1, 1, 0.7), + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + transition=Text.Transition.FADE_IN, + scale=1.6, + maxwidth=150, + transition_delay=2.0) # If we're a tournament, show prizes. try: @@ -1439,8 +1434,6 @@ class CoopScoreScreen(ba.Activity): ba.timer(0.35, ba.Call(ba.playsound, self.cymbal_sound)) def _show_fail(self) -> None: - from bastd.actor.text import Text - from bastd.actor.zoomtext import ZoomText ZoomText(ba.Lstr(resource='failText'), maxwidth=300, flash=False, @@ -1460,8 +1453,6 @@ class CoopScoreScreen(ba.Activity): ba.timer(0.35, ba.Call(ba.playsound, self._score_display_sound)) def _show_score_val(self, offs_x: float) -> None: - from bastd.actor.text import Text - from bastd.actor.zoomtext import ZoomText assert self._score_type is not None assert self._score is not None ZoomText((str(self._score) if self._score_type == 'points' else diff --git a/assets/src/ba_data/python/bastd/actor/bomb.py b/assets/src/ba_data/python/bastd/actor/bomb.py index 460d97a9..d4be4fdb 100644 --- a/assets/src/ba_data/python/bastd/actor/bomb.py +++ b/assets/src/ba_data/python/bastd/actor/bomb.py @@ -1047,23 +1047,24 @@ class TNTSpawner: category: Gameplay Classes """ - def __init__(self, position: Sequence[float], respawn_time: float = 30.0): + def __init__(self, position: Sequence[float], respawn_time: float = 20.0): """Instantiate with given position and respawn_time (in seconds).""" self._position = position self._tnt: Optional[Bomb] = None + self._respawn_time = random.uniform(0.8, 1.2) * respawn_time + self._wait_time = 0.0 self._update() - # (go with slightly more than 1 second to avoid timer stacking) + + # Go with slightly more than 1 second to avoid timer stacking. self._update_timer = ba.Timer(1.1, ba.WeakCall(self._update), repeat=True) - self._respawn_time = random.uniform(0.8, 1.2) * respawn_time - self._wait_time = 0.0 def _update(self) -> None: tnt_alive = self._tnt is not None and self._tnt.node if not tnt_alive: - # respawn if its been long enough.. otherwise just increment our - # how-long-since-we-died value + # Respawn if its been long enough.. otherwise just increment our + # how-long-since-we-died value. if self._tnt is None or self._wait_time >= self._respawn_time: self._tnt = Bomb(position=self._position, bomb_type='tnt') self._wait_time = 0.0 diff --git a/assets/src/ba_data/python/bastd/game/meteorshower.py b/assets/src/ba_data/python/bastd/game/meteorshower.py index 31fb53b6..2b1dd8c4 100644 --- a/assets/src/ba_data/python/bastd/game/meteorshower.py +++ b/assets/src/ba_data/python/bastd/game/meteorshower.py @@ -164,7 +164,7 @@ class MeteorShowerGame(ba.TeamGameActivity): # Augment standard behavior. super().handlemessage(msg) - death_time = ba.time() + death_time = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) # Record the player's moment of death. msg.spaz.player.gamedata['death_time'] = death_time @@ -240,8 +240,10 @@ class MeteorShowerGame(ba.TeamGameActivity): self._meteor_time = max(0.01, self._meteor_time * 0.9) def end_game(self) -> None: - cur_time = ba.time() + cur_time = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) assert self._timer is not None + start_time = self._timer.getstarttime( + timeformat=ba.TimeFormat.MILLISECONDS) # Mark 'death-time' as now for any still-living players # and award players points for how long they lasted. @@ -252,13 +254,14 @@ class MeteorShowerGame(ba.TeamGameActivity): # Throw an extra fudge factor in so teams that # didn't die come out ahead of teams that did. if 'death_time' not in player.gamedata: - player.gamedata['death_time'] = cur_time + 0.001 + player.gamedata['death_time'] = cur_time + 1 # Award a per-player score depending on how many seconds # they lasted (per-player scores only affect teams mode; # everywhere else just looks at the per-team score). score = int(player.gamedata['death_time'] - - self._timer.getstarttime()) + self._timer.getstarttime( + timeformat=ba.TimeFormat.MILLISECONDS)) if 'death_time' not in player.gamedata: score += 50 # a bit extra for survivors self.stats.player_scored(player, score, screenmessage=False) @@ -281,8 +284,7 @@ class MeteorShowerGame(ba.TeamGameActivity): longest_life = 0 for player in team.players: longest_life = max(longest_life, - (player.gamedata['death_time'] - - self._timer.getstarttime())) + player.gamedata['death_time'] - start_time) results.set_team_score(team, longest_life) self.end(results=results) diff --git a/assets/src/ba_data/python/bastd/ui/store/browser.py b/assets/src/ba_data/python/bastd/ui/store/browser.py index 18ede61f..1e0398fc 100644 --- a/assets/src/ba_data/python/bastd/ui/store/browser.py +++ b/assets/src/ba_data/python/bastd/ui/store/browser.py @@ -88,7 +88,7 @@ class StoreBrowserWindow(ba.Window): self._request: Any = None self._r = 'store' - self._last_buy_time = None + self._last_buy_time: Optional[float] = None super().__init__(root_widget=ba.containerwidget( size=(self._width, self._height + extra_top), @@ -179,7 +179,7 @@ class StoreBrowserWindow(ba.Window): ('minigames', ba.Lstr(resource=self._r + '.miniGamesText')), ('characters', ba.Lstr(resource=self._r + '.charactersText')), ('icons', ba.Lstr(resource=self._r + '.iconsText')), - ] # yapf : disable + ] tab_results = tabs.create_tab_buttons(self._root_widget, tabs_def, @@ -454,16 +454,19 @@ class StoreBrowserWindow(ba.Window): # purchase this. Better to fail now than after we've # paid locally. app = ba.app - serverget('bsAccountPurchaseCheck', { - 'item': item, - 'platform': app.platform, - 'subplatform': app.subplatform, - 'version': app.version, - 'buildNumber': app.build_number, - 'purchaseType': 'ticket' if is_ticket_purchase else 'real' - }, - callback=ba.WeakCall(self._purchase_check_result, item, - is_ticket_purchase)) + serverget( + 'bsAccountPurchaseCheck', + { + 'item': item, + 'platform': app.platform, + 'subplatform': app.subplatform, + 'version': app.version, + 'buildNumber': app.build_number, + 'purchaseType': 'ticket' if is_ticket_purchase else 'real' + }, + callback=ba.WeakCall(self._purchase_check_result, item, + is_ticket_purchase), + ) def buy(self, item: str) -> None: """Attempt to purchase the provided item.""" @@ -476,7 +479,8 @@ class StoreBrowserWindow(ba.Window): # Prevent pressing buy within a few seconds of the last press # (gives the buttons time to disable themselves and whatnot). curtime = ba.time(ba.TimeType.REAL) - if self._last_buy_time is None or curtime - self._last_buy_time < 2.0: + if self._last_buy_time is not None and (curtime - + self._last_buy_time) < 2.0: ba.playsound(ba.getsound('error')) else: if _ba.get_account_state() != 'signed_in': @@ -537,8 +541,7 @@ class StoreBrowserWindow(ba.Window): sales_raw = _ba.get_account_misc_read_val('sales', {}) sales = {} try: - # look at the current set of sales; filter any with - # time remaining.. + # Look at the current set of sales; filter any with time remaining. for sale_item, sale_info in list(sales_raw.items()): to_end = (datetime.datetime.utcfromtimestamp(sale_info['e']) - datetime.datetime.utcnow()).total_seconds() @@ -548,7 +551,7 @@ class StoreBrowserWindow(ba.Window): 'original_price': sale_info['op'] } except Exception: - ba.print_exception("Error parsing sales") + ba.print_exception("Error parsing sales.") assert self.button_infos is not None for b_type, b_info in self.button_infos.items(): @@ -602,7 +605,8 @@ class StoreBrowserWindow(ba.Window): price_text_right = '' else: price = _ba.get_account_misc_read_val('price.' + b_type, 0) - # color button differently if we cant afford this + + # Color the button differently if we cant afford this. if _ba.get_account_state() == 'signed_in': if _ba.get_account_ticket_count() < price: color = (0.6, 0.61, 0.6)