From a44821e03ef3642a95b0c821632d300bed1c52e5 Mon Sep 17 00:00:00 2001
From: Eric Froemling
Date: Thu, 28 May 2020 23:19:12 -0700
Subject: [PATCH] More type safety cleanup
---
.idea/dictionaries/ericf.xml | 5 +
assets/.asset_manifest_public.json | 4 +
assets/Makefile | 14 +
assets/src/ba_data/python/ba/_activity.py | 3 +
assets/src/ba_data/python/ba/_actor.py | 3 +-
assets/src/ba_data/python/ba/_analytics.py | 91 +++++++
assets/src/ba_data/python/ba/_gameactivity.py | 251 ++++++------------
assets/src/ba_data/python/ba/_gameutils.py | 11 +
assets/src/ba_data/python/ba/_meta.py | 4 +-
assets/src/ba_data/python/ba/_teamgame.py | 9 +-
assets/src/ba_data/python/bastd/actor/flag.py | 71 +++--
.../src/ba_data/python/bastd/game/assault.py | 2 +-
.../python/bastd/game/capturetheflag.py | 2 +-
.../ba_data/python/bastd/game/chosenone.py | 2 +-
.../src/ba_data/python/bastd/game/conquest.py | 3 +-
.../python/bastd/game/easteregghunt.py | 9 +-
.../src/ba_data/python/bastd/game/football.py | 17 +-
.../src/ba_data/python/bastd/game/keepaway.py | 2 +-
.../python/bastd/game/kingofthehill.py | 2 +-
.../ba_data/python/bastd/game/runaround.py | 7 +-
.../ba_data/python/bastd/game/thelaststand.py | 58 ++--
assets/src/ba_data/python/bastd/gameutils.py | 28 ++
docs/ba_module.md | 35 +--
23 files changed, 355 insertions(+), 278 deletions(-)
create mode 100644 assets/src/ba_data/python/ba/_analytics.py
create mode 100644 assets/src/ba_data/python/bastd/gameutils.py
diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml
index 1586fccd..4859d5d1 100644
--- a/.idea/dictionaries/ericf.xml
+++ b/.idea/dictionaries/ericf.xml
@@ -438,6 +438,7 @@
diemessages
difflib
dilateerode
+ dincrease
dingsound
dingsoundhigh
dirmanifest
@@ -1539,6 +1540,8 @@
resample
resourcetypeinfo
respawn
+ respawnable
+ respawned
respawnicon
responsetype
returncode
@@ -1689,8 +1692,10 @@
sparx
spawner
spawners
+ spawninfo
spawnpoints
spawnpt
+ spawnrate
spawntype
spaz
spazappearance
diff --git a/assets/.asset_manifest_public.json b/assets/.asset_manifest_public.json
index fea31d89..24552382 100644
--- a/assets/.asset_manifest_public.json
+++ b/assets/.asset_manifest_public.json
@@ -6,6 +6,7 @@
"ba_data/python/ba/__pycache__/_activity.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_activitytypes.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_actor.cpython-37.opt-1.pyc",
+ "ba_data/python/ba/__pycache__/_analytics.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_app.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_appconfig.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_appdelegate.cpython-37.opt-1.pyc",
@@ -61,6 +62,7 @@
"ba_data/python/ba/_activity.py",
"ba_data/python/ba/_activitytypes.py",
"ba_data/python/ba/_actor.py",
+ "ba_data/python/ba/_analytics.py",
"ba_data/python/ba/_app.py",
"ba_data/python/ba/_appconfig.py",
"ba_data/python/ba/_appdelegate.py",
@@ -124,6 +126,7 @@
"ba_data/python/bastd/__init__.py",
"ba_data/python/bastd/__pycache__/__init__.cpython-37.opt-1.pyc",
"ba_data/python/bastd/__pycache__/appdelegate.cpython-37.opt-1.pyc",
+ "ba_data/python/bastd/__pycache__/gameutils.cpython-37.opt-1.pyc",
"ba_data/python/bastd/__pycache__/mainmenu.cpython-37.opt-1.pyc",
"ba_data/python/bastd/__pycache__/maps.cpython-37.opt-1.pyc",
"ba_data/python/bastd/__pycache__/stdmap.cpython-37.opt-1.pyc",
@@ -227,6 +230,7 @@
"ba_data/python/bastd/game/runaround.py",
"ba_data/python/bastd/game/targetpractice.py",
"ba_data/python/bastd/game/thelaststand.py",
+ "ba_data/python/bastd/gameutils.py",
"ba_data/python/bastd/mainmenu.py",
"ba_data/python/bastd/mapdata/__init__.py",
"ba_data/python/bastd/mapdata/__pycache__/__init__.cpython-37.opt-1.pyc",
diff --git a/assets/Makefile b/assets/Makefile
index c77bdbb8..4af402bb 100644
--- a/assets/Makefile
+++ b/assets/Makefile
@@ -179,6 +179,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \
build/ba_data/python/ba/_benchmark.py \
build/ba_data/python/ba/_tournament.py \
build/ba_data/python/ba/_messages.py \
+ build/ba_data/python/ba/_analytics.py \
build/ba_data/python/ba/_freeforallsession.py \
build/ba_data/python/ba/_score.py \
build/ba_data/python/ba/_playlist.py \
@@ -200,6 +201,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \
build/ba_data/python/ba/_nodeactor.py \
build/ba_data/python/ba/_teamgame.py \
build/ba_data/python/ba/ui/__init__.py \
+ build/ba_data/python/bastd/gameutils.py \
build/ba_data/python/bastd/mainmenu.py \
build/ba_data/python/bastd/maps.py \
build/ba_data/python/bastd/appdelegate.py \
@@ -407,6 +409,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
build/ba_data/python/ba/__pycache__/_benchmark.cpython-37.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_tournament.cpython-37.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_messages.cpython-37.opt-1.pyc \
+ build/ba_data/python/ba/__pycache__/_analytics.cpython-37.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_freeforallsession.cpython-37.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_score.cpython-37.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_playlist.cpython-37.opt-1.pyc \
@@ -428,6 +431,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
build/ba_data/python/ba/__pycache__/_nodeactor.cpython-37.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_teamgame.cpython-37.opt-1.pyc \
build/ba_data/python/ba/ui/__pycache__/__init__.cpython-37.opt-1.pyc \
+ build/ba_data/python/bastd/__pycache__/gameutils.cpython-37.opt-1.pyc \
build/ba_data/python/bastd/__pycache__/mainmenu.cpython-37.opt-1.pyc \
build/ba_data/python/bastd/__pycache__/maps.cpython-37.opt-1.pyc \
build/ba_data/python/bastd/__pycache__/appdelegate.cpython-37.opt-1.pyc \
@@ -795,6 +799,11 @@ build/ba_data/python/ba/__pycache__/_messages.cpython-37.opt-1.pyc: \
@echo Compiling script: $^
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
+build/ba_data/python/ba/__pycache__/_analytics.cpython-37.opt-1.pyc: \
+ build/ba_data/python/ba/_analytics.py
+ @echo Compiling script: $^
+ @rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
+
build/ba_data/python/ba/__pycache__/_freeforallsession.cpython-37.opt-1.pyc: \
build/ba_data/python/ba/_freeforallsession.py
@echo Compiling script: $^
@@ -900,6 +909,11 @@ build/ba_data/python/ba/ui/__pycache__/__init__.cpython-37.opt-1.pyc: \
@echo Compiling script: $^
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
+build/ba_data/python/bastd/__pycache__/gameutils.cpython-37.opt-1.pyc: \
+ build/ba_data/python/bastd/gameutils.py
+ @echo Compiling script: $^
+ @rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
+
build/ba_data/python/bastd/__pycache__/mainmenu.cpython-37.opt-1.pyc: \
build/ba_data/python/bastd/mainmenu.py
@echo Compiling script: $^
diff --git a/assets/src/ba_data/python/ba/_activity.py b/assets/src/ba_data/python/ba/_activity.py
index fe501643..6088ed89 100644
--- a/assets/src/ba_data/python/ba/_activity.py
+++ b/assets/src/ba_data/python/ba/_activity.py
@@ -29,6 +29,7 @@ from ba._player import Player
from ba._error import print_exception, print_error, SessionTeamNotFoundError
from ba._dependency import DependencyComponent
from ba._general import Call, verify_object_death
+from ba._messages import UNHANDLED
import _ba
if TYPE_CHECKING:
@@ -414,6 +415,8 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
def handlemessage(self, msg: Any) -> Any:
"""General message handling; can be passed any message object."""
+ del msg # Unused arg.
+ return UNHANDLED
def end(self,
results: Any = None,
diff --git a/assets/src/ba_data/python/ba/_actor.py b/assets/src/ba_data/python/ba/_actor.py
index c3e2a8cd..c86a7be1 100644
--- a/assets/src/ba_data/python/ba/_actor.py
+++ b/assets/src/ba_data/python/ba/_actor.py
@@ -46,7 +46,8 @@ class Actor:
Actors act as controllers, combining some number of ba.Nodes,
ba.Textures, ba.Sounds, etc. into a high-level cohesive unit.
- Some example actors include Bomb, Flag, and Spaz classes in bastd.
+ Some example actors include the Bomb, Flag, and Spaz classes that
+ live in the bastd.actor.* modules.
One key feature of Actors is that they generally 'die'
(killing off or transitioning out their nodes) when the last Python
diff --git a/assets/src/ba_data/python/ba/_analytics.py b/assets/src/ba_data/python/ba/_analytics.py
new file mode 100644
index 00000000..948629e0
--- /dev/null
+++ b/assets/src/ba_data/python/ba/_analytics.py
@@ -0,0 +1,91 @@
+# Copyright (c) 2011-2020 Eric Froemling
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+# -----------------------------------------------------------------------------
+"""Functionality related to analytics."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import _ba
+
+if TYPE_CHECKING:
+ pass
+
+
+def game_begin_analytics() -> None:
+ """Update analytics events for the start of a game."""
+ # pylint: disable=too-many-branches
+ # pylint: disable=cyclic-import
+ from ba._dualteamsession import DualTeamSession
+ from ba._freeforallsession import FreeForAllSession
+ from ba._coopsession import CoopSession
+ from ba._gameactivity import GameActivity
+ activity = _ba.getactivity(False)
+ session = _ba.getsession(False)
+
+ # Fail gracefully if we didn't cleanly get a session and game activity.
+ if not activity or not session or not isinstance(activity, GameActivity):
+ return
+
+ if isinstance(session, CoopSession):
+ campaign = session.campaign
+ assert campaign is not None
+ _ba.set_analytics_screen(
+ 'Coop Game: ' + campaign.name + ' ' +
+ campaign.get_level(_ba.app.coop_session_args['level']).name)
+ _ba.increment_analytics_count('Co-op round start')
+ if len(activity.players) == 1:
+ _ba.increment_analytics_count('Co-op round start 1 human player')
+ elif len(activity.players) == 2:
+ _ba.increment_analytics_count('Co-op round start 2 human players')
+ elif len(activity.players) == 3:
+ _ba.increment_analytics_count('Co-op round start 3 human players')
+ elif len(activity.players) >= 4:
+ _ba.increment_analytics_count('Co-op round start 4+ human players')
+
+ elif isinstance(session, DualTeamSession):
+ _ba.set_analytics_screen('Teams Game: ' + activity.get_name())
+ _ba.increment_analytics_count('Teams round start')
+ if len(activity.players) == 1:
+ _ba.increment_analytics_count('Teams round start 1 human player')
+ elif 1 < len(activity.players) < 8:
+ _ba.increment_analytics_count('Teams round start ' +
+ str(len(activity.players)) +
+ ' human players')
+ elif len(activity.players) >= 8:
+ _ba.increment_analytics_count('Teams round start 8+ human players')
+
+ elif isinstance(session, FreeForAllSession):
+ _ba.set_analytics_screen('FreeForAll Game: ' + activity.get_name())
+ _ba.increment_analytics_count('Free-for-all round start')
+ if len(activity.players) == 1:
+ _ba.increment_analytics_count(
+ 'Free-for-all round start 1 human player')
+ elif 1 < len(activity.players) < 8:
+ _ba.increment_analytics_count('Free-for-all round start ' +
+ str(len(activity.players)) +
+ ' human players')
+ elif len(activity.players) >= 8:
+ _ba.increment_analytics_count(
+ 'Free-for-all round start 8+ human players')
+
+ # For some analytics tracking on the c layer.
+ _ba.reset_game_activity_tracking()
diff --git a/assets/src/ba_data/python/ba/_gameactivity.py b/assets/src/ba_data/python/ba/_gameactivity.py
index dad009cd..b2bc28fa 100644
--- a/assets/src/ba_data/python/ba/_gameactivity.py
+++ b/assets/src/ba_data/python/ba/_gameactivity.py
@@ -29,8 +29,11 @@ from typing import TYPE_CHECKING, TypeVar
from ba._activity import Activity
from ba._score import ScoreInfo
from ba._lang import Lstr
-from ba._messages import PlayerDiedMessage
+from ba._messages import PlayerDiedMessage, StandMessage, DieMessage, DeathType
from ba._error import NotFoundError, print_error, print_exception
+from ba._general import Call, WeakCall
+from ba._player import PlayerInfo
+from ba import _map
import _ba
if TYPE_CHECKING:
@@ -65,6 +68,17 @@ class GameActivity(Activity[PlayerType, TeamType]):
# Default get_score_info() will return this if not None.
score_info: Optional[ba.ScoreInfo] = None
+ # Override some defaults.
+ allow_pausing = True
+ allow_kick_idle_players = True
+
+ # Whether to show points for kills.
+ show_kill_points = True
+
+ # If not None, the music type that should play in on_transition_in()
+ # (unless overridden by the map).
+ default_music: Optional[ba.MusicType] = None
+
@classmethod
def create_settings_ui(
cls,
@@ -139,7 +153,7 @@ class GameActivity(Activity[PlayerType, TeamType]):
Classes which want to change their description depending on the session
can override this method.
"""
- del sessiontype # unused arg
+ del sessiontype # Unused arg.
return cls.description if cls.description is not None else ''
@classmethod
@@ -223,8 +237,7 @@ class GameActivity(Activity[PlayerType, TeamType]):
implementation; should return a list of map names valid
for this game-type for the given ba.Session type.
"""
- from ba import _map
- del sessiontype # unused arg
+ del sessiontype # Unused arg.
return _map.getmaps('melee')
@classmethod
@@ -234,7 +247,6 @@ class GameActivity(Activity[PlayerType, TeamType]):
This is used when viewing game-lists or showing what game
is up next in a series.
"""
- from ba import _map
name = cls.get_display_string(config['settings'])
# In newer configs, map is in settings; it used to be in the
@@ -268,42 +280,16 @@ class GameActivity(Activity[PlayerType, TeamType]):
def __init__(self, settings: Dict[str, Any]):
"""Instantiate the Activity."""
- from ba import _map
super().__init__(settings)
- # Set some defaults.
- self.allow_pausing = True
- self.allow_kick_idle_players = True
-
- # Whether to show points for kills.
- self.show_kill_points = True
-
- # If not None, the music type that should play in on_transition_in()
- # (unless overridden by the map).
- self.default_music: Optional[ba.MusicType] = None
-
# Holds some flattened info about the player set at the point
# when on_begin() is called.
self.initial_player_info: Optional[List[ba.PlayerInfo]] = None
# Go ahead and get our map loading.
- map_name: str
- if 'map' in settings:
- map_name = settings['map']
- else:
- # If settings doesn't specify a map, pick a random one from the
- # list of supported ones.
- unowned_maps = _map.get_unowned_maps()
- valid_maps: List[str] = [
- m for m in self.get_supported_maps(type(self.session))
- if m not in unowned_maps
- ]
- if not valid_maps:
- _ba.screenmessage(Lstr(resource='noValidMapsErrorText'))
- raise Exception('No valid maps')
- map_name = valid_maps[random.randrange(len(valid_maps))]
+ self._map_type = _map.get_map_class(self._calc_map_name(settings))
+
self._spawn_sound = _ba.getsound('spawn')
- self._map_type = _map.get_map_class(map_name)
self._map_type.preload()
self._map: Optional[ba.Map] = None
self._powerup_drop_timer: Optional[ba.Timer] = None
@@ -422,8 +408,8 @@ class GameActivity(Activity[PlayerType, TeamType]):
# Make our map.
self._map = self._map_type()
- # Give our map a chance to override the music
- # (for happy-thoughts and other such themed maps).
+ # Give our map a chance to override the music.
+ # (for happy-thoughts and other such themed maps)
map_music = self._map_type.get_music_type()
music = map_music if map_music is not None else self.default_music
@@ -470,13 +456,11 @@ class GameActivity(Activity[PlayerType, TeamType]):
# pylint: disable=cyclic-import
from bastd.ui.continues import ContinuesWindow
from ba._gameutils import sharedobj
- from ba._general import WeakCall
from ba._coopsession import CoopSession
from ba._enums import TimeType
try:
if _ba.get_account_misc_read_val('enableContinues', False):
-
session = self.session
# We only support continuing in non-tournament games.
@@ -510,82 +494,21 @@ class GameActivity(Activity[PlayerType, TeamType]):
return
except Exception:
- print_exception('error continuing game')
+ print_exception('Error handling continues.')
self.end_game()
- # FIXME: this logic should live in the session classes.
- def _game_begin_analytics(self) -> None:
- """Update analytics events for the start of the game."""
- # pylint: disable=too-many-branches
- from ba._dualteamsession import DualTeamSession
- from ba._freeforallsession import FreeForAllSession
- from ba._coopsession import CoopSession
- session = self.session
- if isinstance(session, CoopSession):
- campaign = session.campaign
- assert campaign is not None
- _ba.set_analytics_screen(
- 'Coop Game: ' + campaign.name + ' ' +
- campaign.get_level(_ba.app.coop_session_args['level']).name)
- _ba.increment_analytics_count('Co-op round start')
- if len(self.players) == 1:
- _ba.increment_analytics_count(
- 'Co-op round start 1 human player')
- elif len(self.players) == 2:
- _ba.increment_analytics_count(
- 'Co-op round start 2 human players')
- elif len(self.players) == 3:
- _ba.increment_analytics_count(
- 'Co-op round start 3 human players')
- elif len(self.players) >= 4:
- _ba.increment_analytics_count(
- 'Co-op round start 4+ human players')
- elif isinstance(session, DualTeamSession):
- _ba.set_analytics_screen('Teams Game: ' + self.get_name())
- _ba.increment_analytics_count('Teams round start')
- if len(self.players) == 1:
- _ba.increment_analytics_count(
- 'Teams round start 1 human player')
- elif 1 < len(self.players) < 8:
- _ba.increment_analytics_count('Teams round start ' +
- str(len(self.players)) +
- ' human players')
- elif len(self.players) >= 8:
- _ba.increment_analytics_count(
- 'Teams round start 8+ human players')
- elif isinstance(session, FreeForAllSession):
- _ba.set_analytics_screen('FreeForAll Game: ' + self.get_name())
- _ba.increment_analytics_count('Free-for-all round start')
- if len(self.players) == 1:
- _ba.increment_analytics_count(
- 'Free-for-all round start 1 human player')
- elif 1 < len(self.players) < 8:
- _ba.increment_analytics_count('Free-for-all round start ' +
- str(len(self.players)) +
- ' human players')
- elif len(self.players) >= 8:
- _ba.increment_analytics_count(
- 'Free-for-all round start 8+ human players')
-
- # For some analytics tracking on the c layer.
- _ba.reset_game_activity_tracking()
-
def on_begin(self) -> None:
- from ba._general import WeakCall
- from ba._player import PlayerInfo
+ from ba._analytics import game_begin_analytics
super().on_begin()
- try:
- self._game_begin_analytics()
- except Exception:
- print_exception('error in game-begin-analytics')
+ game_begin_analytics()
# We don't do this in on_transition_in because it may depend on
# players/teams which aren't available until now.
- _ba.timer(0.001, WeakCall(self.show_scoreboard_info))
- _ba.timer(1.0, WeakCall(self.show_info))
- _ba.timer(2.5, WeakCall(self._show_tip))
+ _ba.timer(0.001, self._show_scoreboard_info)
+ _ba.timer(1.0, self._show_info)
+ _ba.timer(2.5, self._show_tip)
# Store some basic info about players present at start time.
self.initial_player_info = [
@@ -600,7 +523,6 @@ class GameActivity(Activity[PlayerType, TeamType]):
# If this is a tournament, query info about it such as how much
# time is left.
tournament_id = self.session.tournament_id
-
if tournament_id is not None:
_ba.tournament_query(
args={
@@ -628,9 +550,6 @@ class GameActivity(Activity[PlayerType, TeamType]):
self.spawn_player(player)
def on_player_leave(self, player: PlayerType) -> None:
- from ba._general import Call
- from ba._messages import DieMessage, DeathType
-
super().on_player_leave(player)
# If the player has an actor, send it a deferred die message.
@@ -669,8 +588,10 @@ class GameActivity(Activity[PlayerType, TeamType]):
victim_player=player,
importance=importance,
showpoints=self.show_kill_points)
+ return None
+ return super().handlemessage(msg)
- def show_scoreboard_info(self) -> None:
+ def _show_scoreboard_info(self) -> None:
"""Create the game info display.
This is the thing in the top left corner showing the name
@@ -682,8 +603,8 @@ class GameActivity(Activity[PlayerType, TeamType]):
from ba._nodeactor import NodeActor
sb_name = self.get_instance_scoreboard_display_string()
- # the description can be either a string or a sequence with args
- # to swap in post-translation
+ # The description can be either a string or a sequence with args
+ # to swap in post-translation.
sb_desc_in = self.get_instance_description_short()
sb_desc_l: Sequence
if isinstance(sb_desc_in, str):
@@ -754,10 +675,9 @@ class GameActivity(Activity[PlayerType, TeamType]):
1.0: 1.0
})
- def show_info(self) -> None:
+ def _show_info(self) -> None:
"""Show the game description."""
from ba._gameutils import animate
- from ba._general import Call
from bastd.actor.zoomtext import ZoomText
name = self.get_instance_display_string()
ZoomText(name,
@@ -786,7 +706,7 @@ class GameActivity(Activity[PlayerType, TeamType]):
translation = Lstr(translate=('gameDescriptions', desc_l[0]),
subs=subs)
- # do some standard filters (epic mode, etc)
+ # Do some standard filters (epic mode, etc).
if self.settings_raw.get('Epic Mode', False):
translation = Lstr(resource='epicDescriptionFilterText',
subs=[('${DESCRIPTION}', translation)])
@@ -822,7 +742,8 @@ class GameActivity(Activity[PlayerType, TeamType]):
# pylint: disable=too-many-locals
from ba._gameutils import animate
from ba._enums import SpecialChar
- # if there's any tips left on the list, display one..
+
+ # If there's any tips left on the list, display one.
if self.tips:
tip = self.tips.pop(random.randrange(len(self.tips)))
tip_title = Lstr(value='${A}:',
@@ -837,7 +758,7 @@ class GameActivity(Activity[PlayerType, TeamType]):
tip = tip['tip']
assert isinstance(tip, str)
- # a few subs..
+ # A few substitutions...
tip_lstr = Lstr(translate=('tips', tip),
subs=[('${PICKUP}',
_ba.charstr(SpecialChar.TOP_BUTTON))])
@@ -947,26 +868,6 @@ class GameActivity(Activity[PlayerType, TeamType]):
print('WARNING: default end_game() implementation called;'
' your game should override this.')
- def spawn_player_if_exists(self, player: PlayerType) -> None:
- """
- A utility method which calls self.spawn_player() *only* if the
- ba.Player provided still exists; handy for use in timers and whatnot.
-
- There is no need to override this; just override spawn_player().
- """
- if player:
- self.spawn_player(player)
-
- def spawn_player(self, player: PlayerType) -> ba.Actor:
- """Spawn *something* for the provided ba.Player.
-
- The default implementation simply calls spawn_player_spaz().
- """
- if not player:
- raise TypeError('spawn_player() called for nonexistent player')
-
- return self.spawn_player_spaz(player)
-
def respawn_player(self,
player: PlayerType,
respawn_time: Optional[float] = None) -> None:
@@ -992,21 +893,39 @@ class GameActivity(Activity[PlayerType, TeamType]):
else:
respawn_time = 7.0
- # If this standard setting is present, factor it in
+ # If this standard setting is present, factor it in.
if 'Respawn Times' in self.settings_raw:
respawn_time *= self.settings_raw['Respawn Times']
- # we want whole seconds
+ # We want whole seconds.
assert respawn_time is not None
respawn_time = round(max(1.0, respawn_time), 0)
if player.actor and not self.has_ended():
- from ba._general import WeakCall
from bastd.actor.respawnicon import RespawnIcon
player.gamedata['respawn_timer'] = _ba.Timer(
respawn_time, WeakCall(self.spawn_player_if_exists, player))
player.gamedata['respawn_icon'] = RespawnIcon(player, respawn_time)
+ def spawn_player_if_exists(self, player: PlayerType) -> None:
+ """
+ A utility method which calls self.spawn_player() *only* if the
+ ba.Player provided still exists; handy for use in timers and whatnot.
+
+ There is no need to override this; just override spawn_player().
+ """
+ if player:
+ self.spawn_player(player)
+
+ def spawn_player(self, player: PlayerType) -> ba.Actor:
+ """Spawn *something* for the provided ba.Player.
+
+ The default implementation simply calls spawn_player_spaz().
+ """
+ assert player # Dead references should never be passed as args.
+
+ return self.spawn_player_spaz(player)
+
def spawn_player_spaz(self,
player: PlayerType,
position: Sequence[float] = (0, 0, 0),
@@ -1015,7 +934,6 @@ class GameActivity(Activity[PlayerType, TeamType]):
# pylint: disable=too-many-locals
# pylint: disable=cyclic-import
from ba import _math
- from ba import _messages
from ba._gameutils import animate
from ba._coopsession import CoopSession
from bastd.actor.playerspaz import PlayerSpaz
@@ -1051,7 +969,7 @@ class GameActivity(Activity[PlayerType, TeamType]):
# Move to the stand position and add a flash of light.
spaz.handlemessage(
- _messages.StandMessage(
+ StandMessage(
position,
angle if angle is not None else random.uniform(0, 360)))
_ba.playsound(self._spawn_sound, 1, position=spaz.node.position)
@@ -1061,30 +979,14 @@ class GameActivity(Activity[PlayerType, TeamType]):
_ba.timer(0.5, light.delete)
return spaz
- def project_flag_stand(self, pos: Sequence[float]) -> None:
- """Project a flag-stand onto the ground at the given position.
-
- Useful for games such as capture-the-flag to show where a
- movable flag originated from.
- """
- from ba._general import WeakCall
-
- # Need to do this in a timer for it to work.. need to look into that.
- # (might not still be the case?...)
- _ba.pushcall(WeakCall(self._project_flag_stand, pos[:3]))
-
- def _project_flag_stand(self, pos: Sequence[float]) -> None:
- _ba.emitfx(position=pos, emit_type='flag_stand')
-
def setup_standard_powerup_drops(self, enable_tnt: bool = True) -> None:
"""Create standard powerup drops for the current map."""
# pylint: disable=cyclic-import
- from bastd.actor import powerupbox
- from ba import _general
- self._powerup_drop_timer = _ba.Timer(
- powerupbox.DEFAULT_POWERUP_INTERVAL,
- _general.WeakCall(self._standard_drop_powerups),
- repeat=True)
+ from bastd.actor.powerupbox import DEFAULT_POWERUP_INTERVAL
+ self._powerup_drop_timer = _ba.Timer(DEFAULT_POWERUP_INTERVAL,
+ WeakCall(
+ self._standard_drop_powerups),
+ repeat=True)
self._standard_drop_powerups()
if enable_tnt:
self._tnt_spawners = {}
@@ -1100,19 +1002,16 @@ class GameActivity(Activity[PlayerType, TeamType]):
def _standard_drop_powerups(self) -> None:
"""Standard powerup drop."""
- from ba import _general
# Drop one powerup per point.
points = self.map.powerup_spawn_points
for i in range(len(points)):
- _ba.timer(i * 0.4, _general.WeakCall(self._standard_drop_powerup,
- i))
+ _ba.timer(i * 0.4, WeakCall(self._standard_drop_powerup, i))
def _setup_standard_tnt_drops(self) -> None:
"""Standard tnt drop."""
# pylint: disable=cyclic-import
from bastd.actor.bomb import TNTSpawner
-
for i, point in enumerate(self.map.tnt_points):
assert self._tnt_spawners is not None
if self._tnt_spawners.get(i) is None:
@@ -1126,7 +1025,6 @@ class GameActivity(Activity[PlayerType, TeamType]):
If the time-limit expires, end_game() will be called.
"""
from ba._gameutils import sharedobj
- from ba._general import WeakCall
from ba._nodeactor import NodeActor
if duration <= 0.0:
return
@@ -1200,7 +1098,6 @@ class GameActivity(Activity[PlayerType, TeamType]):
This will be displayed at the top of the screen.
If the time-limit expires, end_game() will be called.
"""
- from ba._general import WeakCall
from ba._nodeactor import NodeActor
from ba._enums import TimeType
if duration <= 0.0:
@@ -1337,3 +1234,21 @@ class GameActivity(Activity[PlayerType, TeamType]):
maxwidth=800,
trail=trail,
color=color).autoretain()
+
+ def _calc_map_name(self, settings: Dict[str, Any]) -> str:
+ map_name: str
+ if 'map' in settings:
+ map_name = settings['map']
+ else:
+ # If settings doesn't specify a map, pick a random one from the
+ # list of supported ones.
+ unowned_maps = _map.get_unowned_maps()
+ valid_maps: List[str] = [
+ m for m in self.get_supported_maps(type(self.session))
+ if m not in unowned_maps
+ ]
+ if not valid_maps:
+ _ba.screenmessage(Lstr(resource='noValidMapsErrorText'))
+ raise Exception('No valid maps')
+ map_name = valid_maps[random.randrange(len(valid_maps))]
+ return map_name
diff --git a/assets/src/ba_data/python/ba/_gameutils.py b/assets/src/ba_data/python/ba/_gameutils.py
index 491f4140..7391925d 100644
--- a/assets/src/ba_data/python/ba/_gameutils.py
+++ b/assets/src/ba_data/python/ba/_gameutils.py
@@ -19,9 +19,11 @@
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Utility functionality pertaining to gameplay."""
+
from __future__ import annotations
from typing import TYPE_CHECKING
+# from typing_extensions import Protocol
import _ba
from ba._enums import TimeType, TimeFormat, SpecialChar
@@ -39,6 +41,15 @@ TROPHY_CHARS = {
'4': SpecialChar.TROPHY4
}
+# class Respawnable(Protocol):
+# """A Protocol for objects able to be respawned.
+
+# Category: Protocols
+# """
+
+# respawn_timer: Optional[ba.Timer]
+# respawn_icon: Optional[RespawnIcon]
+
def get_trophy_string(trophy_id: str) -> str:
"""Given a trophy id, returns a string to visualize it."""
diff --git a/assets/src/ba_data/python/ba/_meta.py b/assets/src/ba_data/python/ba/_meta.py
index e25de5bb..1de050db 100644
--- a/assets/src/ba_data/python/ba/_meta.py
+++ b/assets/src/ba_data/python/ba/_meta.py
@@ -271,7 +271,7 @@ class DirectoryScan:
cbits = lbits[1].split('(')
if len(cbits) > 1 and cbits[0].isidentifier():
classname = cbits[0]
- break # success!
+ break # Success!
if classname is None:
self.results.warnings += (
'Warning: ' + str(subpath) + ': class definition not found'
@@ -291,7 +291,7 @@ class DirectoryScan:
and l[1] == 'require' and l[2] == 'api' and l[3].isdigit()
]
- # we're successful if we find exactly one properly formatted line
+ # We're successful if we find exactly one properly formatted line.
if len(lines) == 1:
return int(lines[0][3])
diff --git a/assets/src/ba_data/python/ba/_teamgame.py b/assets/src/ba_data/python/ba/_teamgame.py
index 128b959b..47c86520 100644
--- a/assets/src/ba_data/python/ba/_teamgame.py
+++ b/assets/src/ba_data/python/ba/_teamgame.py
@@ -59,14 +59,13 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]):
or issubclass(sessiontype, FreeForAllSession))
def __init__(self, settings: Dict[str, Any]):
-
super().__init__(settings)
- # By default we don't show kill-points in free-for-all.
+ # By default we don't show kill-points in free-for-all sessions.
# (there's usually some activity-specific score and we don't
# wanna confuse things)
- if isinstance(_ba.getsession(), FreeForAllSession):
- self._show_kill_points = False
+ if isinstance(self.session, FreeForAllSession):
+ self.show_kill_points = False
def on_transition_in(self) -> None:
# pylint: disable=cyclic-import
@@ -78,7 +77,6 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]):
# (unless we're being run in co-op mode, in which case we leave
# it up to them)
if not isinstance(self.session, CoopSession):
- # FIXME: Need an elegant way to store on session.
if not self.session.have_shown_controls_help_overlay:
delay = 4.0
lifespan = 10.0
@@ -151,6 +149,7 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]):
if not isinstance(session, CoopSession):
do_announce = not self.has_ended()
super().end(results, delay=2.0 + announce_delay, force=force)
+
# Need to do this *after* end end call so that results is valid.
assert isinstance(results, TeamGameResults)
if do_announce and isinstance(session, MultiTeamSession):
diff --git a/assets/src/ba_data/python/bastd/actor/flag.py b/assets/src/ba_data/python/bastd/actor/flag.py
index f7df8bd3..59515af0 100644
--- a/assets/src/ba_data/python/bastd/actor/flag.py
+++ b/assets/src/ba_data/python/bastd/actor/flag.py
@@ -67,41 +67,58 @@ class FlagFactory:
self.flagmaterial = ba.Material()
self.flagmaterial.add_actions(
- conditions=(('we_are_younger_than', 100),
- 'and', ('they_have_material',
- ba.sharedobj('object_material'))),
- actions=('modify_node_collision', 'collide', False))
+ conditions=(
+ ('we_are_younger_than', 100),
+ 'and',
+ ('they_have_material', ba.sharedobj('object_material')),
+ ),
+ actions=('modify_node_collision', 'collide', False),
+ )
self.flagmaterial.add_actions(
- conditions=('they_have_material',
- ba.sharedobj('footing_material')),
- actions=(('message', 'our_node', 'at_connect', 'footing', 1),
- ('message', 'our_node', 'at_disconnect', 'footing', -1)))
+ conditions=(
+ 'they_have_material',
+ ba.sharedobj('footing_material'),
+ ),
+ actions=(
+ ('message', 'our_node', 'at_connect', 'footing', 1),
+ ('message', 'our_node', 'at_disconnect', 'footing', -1),
+ ),
+ )
self.impact_sound = ba.getsound('metalHit')
self.skid_sound = ba.getsound('metalSkid')
self.flagmaterial.add_actions(
- conditions=('they_have_material',
- ba.sharedobj('footing_material')),
- actions=(('impact_sound', self.impact_sound, 2, 5),
- ('skid_sound', self.skid_sound, 2, 5)))
+ conditions=(
+ 'they_have_material',
+ ba.sharedobj('footing_material'),
+ ),
+ actions=(
+ ('impact_sound', self.impact_sound, 2, 5),
+ ('skid_sound', self.skid_sound, 2, 5),
+ ),
+ )
self.no_hit_material = ba.Material()
self.no_hit_material.add_actions(
- conditions=(('they_have_material',
- ba.sharedobj('pickup_material')),
- 'or', ('they_have_material',
- ba.sharedobj('attack_material'))),
- actions=('modify_part_collision', 'collide', False))
+ conditions=(
+ ('they_have_material', ba.sharedobj('pickup_material')),
+ 'or',
+ ('they_have_material', ba.sharedobj('attack_material')),
+ ),
+ actions=('modify_part_collision', 'collide', False),
+ )
# We also don't want anything moving it.
self.no_hit_material.add_actions(
- conditions=(('they_have_material',
- ba.sharedobj('object_material')), 'or',
- ('they_dont_have_material',
- ba.sharedobj('footing_material'))),
+ conditions=(
+ ('they_have_material', ba.sharedobj('object_material')),
+ 'or',
+ ('they_dont_have_material', ba.sharedobj('footing_material')),
+ ),
actions=(('modify_part_collision', 'collide', False),
- ('modify_part_collision', 'physical', False)))
+ ('modify_part_collision', 'physical', False)),
+ )
self.flag_texture = ba.gettexture('flagColor')
@@ -353,3 +370,13 @@ class Flag(ba.Actor):
self.activity.handlemessage(FlagDroppedMessage(self, msg.node))
else:
super().handlemessage(msg)
+
+ @staticmethod
+ def project_stand(pos: Sequence[float]) -> None:
+ """Project a flag-stand onto the ground at the given position.
+
+ Useful for games such as capture-the-flag to show where a
+ movable flag originated from.
+ """
+ assert len(pos) == 3
+ ba.emitfx(position=pos, emit_type='flag_stand')
diff --git a/assets/src/ba_data/python/bastd/game/assault.py b/assets/src/ba_data/python/bastd/game/assault.py
index 0020bb0c..062a030e 100644
--- a/assets/src/ba_data/python/bastd/game/assault.py
+++ b/assets/src/ba_data/python/bastd/game/assault.py
@@ -121,7 +121,7 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]):
'radius': 0.1,
'color': sessionteam.color
})
- self.project_flag_stand(base_pos)
+ Flag.project_stand(base_pos)
flag = Flag(touchable=False,
position=base_pos,
color=sessionteam.color)
diff --git a/assets/src/ba_data/python/bastd/game/capturetheflag.py b/assets/src/ba_data/python/bastd/game/capturetheflag.py
index 43ff82e4..711672d9 100644
--- a/assets/src/ba_data/python/bastd/game/capturetheflag.py
+++ b/assets/src/ba_data/python/bastd/game/capturetheflag.py
@@ -190,7 +190,7 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
# Create our team instance and its initial values.
base_pos = self.map.get_flag_position(sessionteam.id)
- self.project_flag_stand(base_pos)
+ Flag.project_stand(base_pos)
ba.newnode('light',
attrs={
diff --git a/assets/src/ba_data/python/bastd/game/chosenone.py b/assets/src/ba_data/python/bastd/game/chosenone.py
index 256ea606..f65d12c8 100644
--- a/assets/src/ba_data/python/bastd/game/chosenone.py
+++ b/assets/src/ba_data/python/bastd/game/chosenone.py
@@ -145,7 +145,7 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]):
self.setup_standard_time_limit(self._time_limit)
self.setup_standard_powerup_drops()
self._flag_spawn_pos = self.map.get_flag_position(None)
- self.project_flag_stand(self._flag_spawn_pos)
+ Flag.project_stand(self._flag_spawn_pos)
self._set_chosen_one_player(None)
pos = self._flag_spawn_pos
diff --git a/assets/src/ba_data/python/bastd/game/conquest.py b/assets/src/ba_data/python/bastd/game/conquest.py
index 0f1e51e9..c6ba9c4d 100644
--- a/assets/src/ba_data/python/bastd/game/conquest.py
+++ b/assets/src/ba_data/python/bastd/game/conquest.py
@@ -167,8 +167,7 @@ class ConquestGame(ba.TeamGameActivity[Player, Team]):
touchable=False,
materials=[self._extraflagmat])
self._flags.append(flag)
- # FIXME: Move next few lines to the flag class.
- self.project_flag_stand(point)
+ Flag.project_stand(point)
flag.light = ba.newnode('light',
owner=flag.node,
attrs={
diff --git a/assets/src/ba_data/python/bastd/game/easteregghunt.py b/assets/src/ba_data/python/bastd/game/easteregghunt.py
index 503d4196..244e79d4 100644
--- a/assets/src/ba_data/python/bastd/game/easteregghunt.py
+++ b/assets/src/ba_data/python/bastd/game/easteregghunt.py
@@ -198,15 +198,11 @@ class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]):
# Respawn dead players.
if isinstance(msg, ba.PlayerDiedMessage):
-
# Augment standard behavior.
super().handlemessage(msg)
- player = msg.getplayer(Player)
- if not player:
- return
- self.stats.player_was_killed(player)
# Respawn them shortly.
+ player = msg.getplayer(Player)
assert self.initial_player_info is not None
respawn_time = 2.0 + len(self.initial_player_info) * 1.0
player.respawn_timer = ba.Timer(
@@ -226,7 +222,8 @@ class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]):
pos[2] + random.uniform(-spread, spread))))
else:
# Default handler.
- super().handlemessage(msg)
+ return super().handlemessage(msg)
+ return None
def _update_scoreboard(self) -> None:
for team in self.teams:
diff --git a/assets/src/ba_data/python/bastd/game/football.py b/assets/src/ba_data/python/bastd/game/football.py
index 5ded7ace..e0f5d7b0 100644
--- a/assets/src/ba_data/python/bastd/game/football.py
+++ b/assets/src/ba_data/python/bastd/game/football.py
@@ -798,21 +798,17 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]):
def handlemessage(self, msg: Any) -> Any:
""" handle high-level game messages """
if isinstance(msg, ba.PlayerDiedMessage):
-
- # Respawn dead players.
- player = msg.getplayer(Player)
- self.stats.player_was_killed(player)
- assert self.initial_player_info is not None
- respawn_time = 2.0 + len(self.initial_player_info) * 1.0
+ # Augment standard behavior.
+ super().handlemessage(msg)
# Respawn them shortly.
+ player = msg.getplayer(Player)
+ assert self.initial_player_info is not None
+ respawn_time = 2.0 + len(self.initial_player_info) * 1.0
player.respawn_timer = ba.Timer(
respawn_time, ba.Call(self.spawn_player_if_exists, player))
player.respawn_icon = RespawnIcon(player, respawn_time)
- # Augment standard behavior.
- super().handlemessage(msg)
-
elif isinstance(msg, SpazBotDiedMessage):
# Every time a bad guy dies, spawn a new one.
@@ -848,7 +844,8 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]):
loop=True)
ba.timer(3.0, self._flag_respawn_light.node.delete)
else:
- super().handlemessage(msg)
+ return super().handlemessage(msg)
+ return None
def _handle_player_dropped_bomb(self, player: Spaz,
bomb: ba.Actor) -> None:
diff --git a/assets/src/ba_data/python/bastd/game/keepaway.py b/assets/src/ba_data/python/bastd/game/keepaway.py
index e0d065dc..fae6bada 100644
--- a/assets/src/ba_data/python/bastd/game/keepaway.py
+++ b/assets/src/ba_data/python/bastd/game/keepaway.py
@@ -138,7 +138,7 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]):
self._spawn_flag()
self._update_timer = ba.Timer(1.0, call=self._tick, repeat=True)
self._update_flag_state()
- self.project_flag_stand(self._flag_spawn_pos)
+ Flag.project_stand(self._flag_spawn_pos)
def _tick(self) -> None:
self._update_flag_state()
diff --git a/assets/src/ba_data/python/bastd/game/kingofthehill.py b/assets/src/ba_data/python/bastd/game/kingofthehill.py
index 4b11464c..b0dfe47c 100644
--- a/assets/src/ba_data/python/bastd/game/kingofthehill.py
+++ b/assets/src/ba_data/python/bastd/game/kingofthehill.py
@@ -150,7 +150,7 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]):
self._flag_pos = self.map.get_flag_position(None)
ba.timer(1.0, self._tick, repeat=True)
self._flag_state = FlagState.NEW
- self.project_flag_stand(self._flag_pos)
+ Flag.project_stand(self._flag_pos)
self._flag = Flag(position=self._flag_pos,
touchable=False,
diff --git a/assets/src/ba_data/python/bastd/game/runaround.py b/assets/src/ba_data/python/bastd/game/runaround.py
index f134816a..7f6ca1d7 100644
--- a/assets/src/ba_data/python/bastd/game/runaround.py
+++ b/assets/src/ba_data/python/bastd/game/runaround.py
@@ -1108,13 +1108,14 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]):
self._score += msg.score
self._update_scores()
- # Respawn dead players.
elif isinstance(msg, ba.PlayerDiedMessage):
+ # Augment standard behavior.
+ super().handlemessage(msg)
+
self._a_player_has_been_killed = True
- player = msg.getplayer(Player)
- self.stats.player_was_killed(player)
# Respawn them shortly.
+ player = msg.getplayer(Player)
assert self.initial_player_info is not None
respawn_time = 2.0 + len(self.initial_player_info) * 1.0
player.respawn_timer = ba.Timer(
diff --git a/assets/src/ba_data/python/bastd/game/thelaststand.py b/assets/src/ba_data/python/bastd/game/thelaststand.py
index ae9a1a9a..20c4b626 100644
--- a/assets/src/ba_data/python/bastd/game/thelaststand.py
+++ b/assets/src/ba_data/python/bastd/game/thelaststand.py
@@ -23,6 +23,7 @@
from __future__ import annotations
import random
+from dataclasses import dataclass
from typing import TYPE_CHECKING
import ba
@@ -42,6 +43,14 @@ if TYPE_CHECKING:
from bastd.actor.spazbot import SpazBot
+@dataclass
+class SpawnInfo:
+ """Spawning info for a particular bot type."""
+ spawnrate: float
+ increase: float
+ dincrease: float
+
+
class Player(ba.Player['Team']):
"""Our player type for this game."""
@@ -78,7 +87,7 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]):
self._tntspawnpos = (0, 5.5, -6)
self._powerup_center = (0, 7, -4.14)
self._powerup_spread = (7, 2)
- self._preset = self.settings_raw.get('preset', 'default')
+ self._preset = str(settings.get('preset', 'default'))
self._excludepowerups: List[str] = []
self._scoreboard: Optional[Scoreboard] = None
self._score = 0
@@ -90,21 +99,22 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]):
self._bot_update_timer: Optional[ba.Timer] = None
self._powerup_drop_timer = None
- # For each bot type: [spawn-rate, increase, d_increase]
+ # For each bot type: [spawnrate, increase, d_increase]
self._bot_spawn_types = {
- BomberBot: [1.00, 0.00, 0.000],
- BomberBotPro: [0.00, 0.05, 0.001],
- BomberBotProShielded: [0.00, 0.02, 0.002],
- BrawlerBot: [1.00, 0.00, 0.000],
- BrawlerBotPro: [0.00, 0.05, 0.001],
- BrawlerBotProShielded: [0.00, 0.02, 0.002],
- TriggerBot: [0.30, 0.00, 0.000],
- TriggerBotPro: [0.00, 0.05, 0.001],
- TriggerBotProShielded: [0.00, 0.02, 0.002],
- ChargerBot: [0.30, 0.05, 0.000],
- StickyBot: [0.10, 0.03, 0.001],
- ExplodeyBot: [0.05, 0.02, 0.002]
- } # yapf: disable
+ BomberBot: SpawnInfo(1.00, 0.00, 0.000),
+ BomberBotPro: SpawnInfo(0.00, 0.05, 0.001),
+ BomberBotProShielded: SpawnInfo(0.00, 0.02, 0.002),
+ BrawlerBot: SpawnInfo(1.00, 0.00, 0.000),
+ BrawlerBotPro: SpawnInfo(0.00, 0.05, 0.001),
+ BrawlerBotProShielded: SpawnInfo(0.00, 0.02, 0.002),
+ TriggerBot: SpawnInfo(0.30, 0.00, 0.000),
+ TriggerBotPro: SpawnInfo(0.00, 0.05, 0.001),
+ TriggerBotProShielded: SpawnInfo(0.00, 0.02, 0.002),
+ ChargerBot: SpawnInfo(0.30, 0.05, 0.000),
+ StickyBot: SpawnInfo(0.10, 0.03, 0.001),
+ ExplodeyBot: SpawnInfo(0.05, 0.02, 0.002)
+ } # yapf: disable
+
def on_transition_in(self) -> None:
super().on_transition_in()
@@ -121,8 +131,6 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]):
ba.timer(0.001, ba.WeakCall(self._start_bot_updates))
self.setup_low_life_warning_sound()
self._update_scores()
-
- # Our TNT spawner (if applicable).
self._tntspawner = TNTSpawner(position=self._tntspawnpos,
respawn_time=10.0)
@@ -225,17 +233,17 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]):
# Normalize our bot type total and find a random number within that.
total = 0.0
- for spawntype in self._bot_spawn_types.items():
- total += spawntype[1][0]
+ for spawninfo in self._bot_spawn_types.values():
+ total += spawninfo.spawnrate
randval = random.random() * total
# Now go back through and see where this value falls.
total = 0
bottype: Optional[Type[SpazBot]] = None
- for spawntype in self._bot_spawn_types.items():
- total += spawntype[1][0]
+ for spawntype, spawninfo in self._bot_spawn_types.items():
+ total += spawninfo.spawnrate
if randval <= total:
- bottype = spawntype[0]
+ bottype = spawntype
break
spawn_time = 1.0
assert bottype is not None
@@ -243,9 +251,9 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]):
# After every spawn we adjust our ratios slightly to get more
# difficult.
- for spawntype in self._bot_spawn_types.items():
- spawntype[1][0] += spawntype[1][1] # incr spawn rate
- spawntype[1][1] += spawntype[1][2] # incr spawn rate incr rate
+ for spawninfo in self._bot_spawn_types.values():
+ spawninfo.spawnrate += spawninfo.increase
+ spawninfo.increase += spawninfo.dincrease
def _update_scores(self) -> None:
score = self._score
diff --git a/assets/src/ba_data/python/bastd/gameutils.py b/assets/src/ba_data/python/bastd/gameutils.py
new file mode 100644
index 00000000..17cf39a8
--- /dev/null
+++ b/assets/src/ba_data/python/bastd/gameutils.py
@@ -0,0 +1,28 @@
+# Copyright (c) 2011-2020 Eric Froemling
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+# -----------------------------------------------------------------------------
+"""Various utilities useful for gameplay."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing import Sequence
diff --git a/docs/ba_module.md b/docs/ba_module.md
index 844b2a5d..dbdf5458 100644
--- a/docs/ba_module.md
+++ b/docs/ba_module.md
@@ -1,5 +1,5 @@
-last updated on 2020-05-28 for Ballistica version 1.5.0 build 20032
+last updated on 2020-05-28 for Ballistica version 1.5.0 build 20033
This page documents the Python classes and functions in the 'ba' module,
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please let me know. Happy modding!
@@ -604,7 +604,8 @@ is a convenient way to access this same functionality.
Actors act as controllers, combining some number of ba.Nodes,
ba.Textures, ba.Sounds, etc. into a high-level cohesive unit.
- Some example actors include Bomb, Flag, and Spaz classes in bastd.
+ Some example actors include the Bomb, Flag, and Spaz classes that
+ live in the bastd.actor.* modules.
One key feature of Actors is that they generally 'die'
(killing off or transitioning out their nodes) when the last Python
@@ -1584,7 +1585,7 @@ start_long_action(callback_when_done=ba.ContextC
Methods Inherited:
-
+
Methods Defined or Overridden:
@@ -2143,7 +2144,7 @@ its time with lingering corpses, sound effects, etc.
Methods Inherited:
Methods Defined or Overridden:
-
+
-
ba.GameActivity(settings: Dict[str, Any])
@@ -2455,15 +2456,6 @@ start playing music, etc. It does not yet have access to players
or teams, however. They remain owned by the previous Activity
up until ba.Activity.on_begin() is called.
-
--
-
project_flag_stand(self, pos: Sequence[float]) -> None
-
-Project a flag-stand onto the ground at the given position.
-
-Useful for games such as capture-the-flag to show where a
-movable flag originated from.
-
-
respawn_player(self, player: PlayerType, respawn_time: Optional[float] = None) -> None
@@ -2490,21 +2482,6 @@ duration in seconds.
This will be displayed at the top of the screen.
If the time-limit expires, end_game() will be called.
-
--
-
show_info(self) -> None
-
-Show the game description.
-
-
--
-
show_scoreboard_info(self) -> None
-
-Create the game info display.
-
-This is the thing in the top left corner showing the name
-and short description of the game.
-
-
show_zoom_message(self, message: ba.Lstr, color: Sequence[float] = (0.9, 0.4, 0.0), scale: float = 0.8, duration: float = 2.0, trail: bool = False) -> None
@@ -5028,7 +5005,7 @@ of the session.
Methods Inherited:
-
+
Methods Defined or Overridden: