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)