switching from yapf to black for python formatting

This commit is contained in:
Eric 2022-10-10 11:55:40 -07:00
parent eea5c2130e
commit 86f6933e54
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
301 changed files with 39425 additions and 24840 deletions

View File

@ -3997,56 +3997,56 @@
"assets/build/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/f5/8b/14895df9caf46f326a3c939b34a4",
"assets/build/windows/Win32/vc_redist.x86.exe": "https://files.ballistica.net/cache/ba1/1c/e1/4a1a2eddda2f4aebd5f8b64ab08e",
"assets/build/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/50/8d/bc2600ac9491f1b14d659709451f",
"assets/build/workspace/ninjafightplug.py": "https://files.ballistica.net/cache/ba1/27/16/71d2713a32c66caf37806f645a71",
"assets/build/workspace/onslaughtplug.py": "https://files.ballistica.net/cache/ba1/c6/4c/47fe21bbcd938711d1ec2a19589d",
"assets/build/workspace/runaroundplug.py": "https://files.ballistica.net/cache/ba1/b1/38/beac9de90bee75363d1de76706b4",
"assets/build/workspace/ninjafightplug.py": "https://files.ballistica.net/cache/ba1/f1/8d/934610811db8bdd58ea84a8f4408",
"assets/build/workspace/onslaughtplug.py": "https://files.ballistica.net/cache/ba1/08/ed/d671c39a3ece6362a6d985112c8e",
"assets/build/workspace/runaroundplug.py": "https://files.ballistica.net/cache/ba1/4d/71/1292911f5369bdb83ef6a34921c0",
"assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e",
"assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/b2/e5/0ee0561e16257a32830645239f34",
"ballisticacore-windows/Generic/BallisticaCore.ico": "https://files.ballistica.net/cache/ba1/89/c0/e32c7d2a35dc9aef57cc73b0911a",
"build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/fe/97/44d57cdd1e67e3fd8f55c0c31cad",
"build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/64/91/fd33398c70b862e46adb6c39e9e1",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e9/15/70169fee1aac6a96861e4862b889",
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/57/68/9d3fdec5450f357b6c57d35ab72c",
"build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/9f/1f/141dd5847b0efca0817dcbba7500",
"build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/49/64/d20ca535494f0af62272b0851268",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/8f/df/c356b826190808fbc45625cf8ae1",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/6f/33/424b11de9d4825beb58aa8394906",
"build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/05/c4/bfd09f076c82e5a3c7c4414f4d70",
"build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/51/92/6082c1fa0758c80cef9ebb984a83",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/51/33/3f6541f11a29786e91234e0288b8",
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/6f/49/3565fc9e6e1d69f06e486f963fd0",
"build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/78/eb/543d44646dcfbd8476a1516cd77a",
"build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/2b/1e/b7ce51aa9f579e4a5a00ce4dd8bc",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/04/d5/f27969fe1b0d587197c8fdc58c2e",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/26/6c/da8ae887cb6ac020fad3c93e7cc9",
"build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/e6/64/460d773403bea1b2a1fb6a846b42",
"build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/84/51/54f2af640302aa0c116a0722f8f4",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/45/06/55dd8fffa7c2ea80256e339d4c36",
"build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/25/78/a7abd69f73bc65602fa2e8662c88",
"build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9d/a6/c963859c531bb19f507f405cf589",
"build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/df/dc/79349a169d3b00964d9f35853f84",
"build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a7/63/d057e19cb7806302b9570b91c573",
"build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/17/c0/3da5a81581aa9275b6c32bb03fc8",
"build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3f/84/d66202e8a15d5518f876dccdd6d6",
"build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/5a/ba/3a6f95b9e4a9c310ac63d0bc0a8c",
"build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2f/58/9462e34601e2f442eb429185ee35",
"build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/50/09/6e8feb718cb60ea80ecaab4ba9a0",
"build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7d/a3/f8fd8ad1037e5f5b47b72a6d1edf",
"build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9d/c6/3902a717b71f9f8781d724c8ca23",
"build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e4/5a/3b25cbbca51ef2a6036d8d1805aa",
"build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fd/11/f0fb88f01753350f88f068b6c6a0",
"build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e4/22/d3700b99ec02b9bd99b8801ad69b",
"build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8b/d8/4b2e840ace5be8dd8fc9d6841cdd",
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/46/07/545eec0e6bde25bba8b3857d7e9b",
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b4/ae/d9a2b38dc9824ac6acc79d520404",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/b1/0f/10eeef2cec516e68c5821f55fe1c",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/46/19/b3826f960327eb7585f546c3c878",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/50/d8/1ca969ac3469fa1d1f75ea9f62b9",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/51/37/888771c771d08b32be09d9203c19",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/2f/04/0ac355606ea7f1609adf1bba693b",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/5e/87/4774a28073c2cc9e1d918977252c",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/9b/fe/f8a6cbf3ec67157fd09b66b344e7",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/54/73/160cb6f003d538aca54a4e98c531",
"build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/bf/79/93a020d1ffda39d0d01ea8e0479a",
"build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/7a/15/d3208b2f2a3eef6fd99e1f8ff33a",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/68/c7/d03c7b9e2b27080abf8dab81c353",
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/85/e8/5ac6499c33d01935df05b28d6e35",
"build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/eb/26/456b39bca29b1f9409467be6cbc9",
"build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/a5/bd/e6e7e45ccb7a4d4569e1a574ccb5",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f7/56/994e3e2dd054c838db5fa60089b1",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/62/ca/fb78c9774ea0d670854fd7c3b68f",
"build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/79/b9/0886caeb0bcceaeefde3009b3260",
"build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/04/60/a49352dea8100f7f979c42428566",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/49/b3/ed8416d34b63ad225d0199c70c40",
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f9/69/1f530c56a627c8d36a1c14bfc023",
"build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/ee/a0/8f3610554e441a9f55e9a0b5a20a",
"build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/7e/5d/28fc24d104d3f0a8f98e6f830062",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/fd/4b/348eebb51a0e01a740fe1cac1b0c",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/44/03/fc0726f576a224b811eb4767c08d",
"build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/0b/b3/57b75dfabbb22802aaca7eec164e",
"build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/1f/63/a05a78b98665377e24e9f8db8bf1",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/b2/55/ba110ac1a33d84a1458315d4b4fe",
"build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/c1/32/730d6c42e157f214083d5dcdbea2",
"build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a0/2a/df32b0c34525af7de212e9a2cf30",
"build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8f/28/f103463730f785ba4fe6a91f8943",
"build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/29/4a/0df587009671edf52d6939c8f966",
"build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c8/df/12ea6b0a703568e52a84001ca2db",
"build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fb/29/5a74e41f2aa46aaa4ebb62b2612f",
"build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d0/89/1995ef2194458c78c546698de761",
"build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7c/7e/01b2fbad142edaac0c0af7853874",
"build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9c/2f/3d0f4b2439b7da9def68597c14c8",
"build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/26/d8/2a352f23af0375e97752da915d38",
"build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a6/0f/72032ea268956736a6eabce738c3",
"build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/92/5d/a8e2b0137a4e67821fc0c233704f",
"build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/01/7c/c21ceaa90f0a0794979056c0a5dc",
"build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/60/4a/238ad7b1d8b5cac2376e58cdd6de",
"build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/27/b6/676cee2d1996ab592e1006ba4721",
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b1/fe/bf79be22fc78040b28b60aa2a8e3",
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/37/3e/689be0ffce83ac46775c5dec6446",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/76/0a/1fbf3abebda99e8bddc30c60a776",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/cb/ce/496131ca3fec3ebd3cf7e8356465",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/2f/02/99c9b9887c50a45007a3825c923a",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/11/10/4e32246cf49a018b34012d167a31",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/02/62/edbc7740c386330cd72be2f7b50a",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/1e/d8/bb3d3b9c170b1696709ba53ef356",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/61/12/5ff38e6e4b2d3dcc1db55db9cf06",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/eb/92/0c028cdd75d65b2f8c37f6fbc1da",
"src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/c0/32/b7907e3859a5c5013a3d97b6b523",
"src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/2d/4f/f4fe67827f36cd59cd5193333a02",
"src/ballistica/generated/python_embedded/bootstrap_monolithic.inc": "https://files.ballistica.net/cache/ba1/ef/c1/aa5f1aa10af89f5c0b1e616355fd"

View File

@ -57,6 +57,7 @@
<w>addr</w>
<w>addrstr</w>
<w>adisp</w>
<w>advb</w>
<w>advertizing</w>
<w>aidl</w>
<w>aint</w>
@ -1935,6 +1936,7 @@
<w>ppre</w>
<w>pproxy</w>
<w>pptabcom</w>
<w>prab</w>
<w>pragmas</w>
<w>prch</w>
<w>prec</w>
@ -1994,6 +1996,7 @@
<w>projs</w>
<w>promocode</w>
<w>proxykey</w>
<w>prtb</w>
<w>prunedir</w>
<w>prval</w>
<w>pstats</w>
@ -2041,6 +2044,7 @@
<w>pvrtcbest</w>
<w>pvrtcfast</w>
<w>pvval</w>
<w>pwin</w>
<w>pybee</w>
<w>pybuild</w>
<w>pybuildapple</w>
@ -2932,6 +2936,7 @@
<w>yscl</w>
<w>ytweak</w>
<w>yval</w>
<w>zabcdefghijklmnopqrstuvwxyz</w>
<w>zaggy</w>
<w>zimbot</w>
<w>zipapp</w>

View File

@ -1,4 +1,5 @@
### 1.7.11 (build 20897, api 7, 2022-10-09)
- Switched our Python autoformatting from yapf to black. The yapf project seems to be mostly dead whereas black seems to be thriving. The final straw was yapf not supporting the `match` statement in Python 3.10.
### 1.7.10 (build 20895, api 7, 2022-10-09)
- Added eval support for cloud-console. This means you can type something like '1+1' in the console and see '2' printed. This is how Python behaves in the stdin console or in-game console or the standard Python interpreter.

View File

@ -1 +1 @@
194057364831757023796080999188881665880
180157672676216986210895241045962795292

File diff suppressed because it is too large Load Diff

View File

@ -35,6 +35,7 @@ from typing import TYPE_CHECKING, TypeVar
if TYPE_CHECKING:
from typing import Any, Callable
_T = TypeVar('_T')
@ -44,13 +45,16 @@ def _uninferrable() -> Any:
return _not_a_real_variable # type: ignore
def add_transaction(transaction: dict,
callback: Callable | None = None) -> None:
def add_transaction(
transaction: dict, callback: Callable | None = None
) -> None:
"""(internal)"""
return None
def game_service_has_leaderboard(game: str, config: str) -> bool:
"""(internal)
Given a game and config string, returns whether there is a leaderboard
@ -60,6 +64,7 @@ def game_service_has_leaderboard(game: str, config: str) -> bool:
def get_master_server_address(source: int = -1, version: int = 1) -> str:
"""(internal)
Return the address of the master server.
@ -68,66 +73,79 @@ def get_master_server_address(source: int = -1, version: int = 1) -> str:
def get_news_show() -> str:
"""(internal)"""
return str()
def get_price(item: str) -> str | None:
"""(internal)"""
return ''
def get_public_login_id() -> str | None:
"""(internal)"""
return ''
def get_purchased(item: str) -> bool:
"""(internal)"""
return bool()
def get_purchases_state() -> int:
"""(internal)"""
return int()
def get_v1_account_display_string(full: bool = True) -> str:
"""(internal)"""
return str()
def get_v1_account_misc_read_val(name: str, default_value: Any) -> Any:
"""(internal)"""
return _uninferrable()
def get_v1_account_misc_read_val_2(name: str, default_value: Any) -> Any:
"""(internal)"""
return _uninferrable()
def get_v1_account_misc_val(name: str, default_value: Any) -> Any:
"""(internal)"""
return _uninferrable()
def get_v1_account_name() -> str:
"""(internal)"""
return str()
def get_v1_account_state() -> str:
"""(internal)"""
return str()
def get_v1_account_state_num() -> int:
"""(internal)"""
return int()
def get_v1_account_ticket_count() -> int:
"""(internal)
Returns the number of tickets for the current account.
@ -136,31 +154,37 @@ def get_v1_account_ticket_count() -> int:
def get_v1_account_type() -> str:
"""(internal)"""
return str()
def get_v2_fleet() -> str:
"""(internal)"""
return str()
def have_outstanding_transactions() -> bool:
"""(internal)"""
return bool()
def in_game_purchase(item: str, price: int) -> None:
"""(internal)"""
return None
def is_blessed() -> bool:
"""(internal)"""
return bool()
def mark_config_dirty() -> None:
"""(internal)
Category: General Utility Functions
@ -169,36 +193,43 @@ def mark_config_dirty() -> None:
def power_ranking_query(callback: Callable, season: Any = None) -> None:
"""(internal)"""
return None
def purchase(item: str) -> None:
"""(internal)"""
return None
def report_achievement(achievement: str, pass_to_account: bool = True) -> None:
"""(internal)"""
return None
def reset_achievements() -> None:
"""(internal)"""
return None
def restore_purchases() -> None:
"""(internal)"""
return None
def run_transactions() -> None:
"""(internal)"""
return None
def sign_in_v1(account_type: str) -> None:
"""(internal)
Category: General Utility Functions
@ -207,6 +238,7 @@ def sign_in_v1(account_type: str) -> None:
def sign_out_v1(v2_embedded: bool = False) -> None:
"""(internal)
Category: General Utility Functions
@ -214,17 +246,20 @@ def sign_out_v1(v2_embedded: bool = False) -> None:
return None
def submit_score(game: str,
config: str,
name: Any,
score: int | None,
callback: Callable,
friend_callback: Callable | None,
order: str = 'increasing',
tournament_id: str | None = None,
score_type: str = 'points',
campaign: str | None = None,
level: str | None = None) -> None:
def submit_score(
game: str,
config: str,
name: Any,
score: int | None,
callback: Callable,
friend_callback: Callable | None,
order: str = 'increasing',
tournament_id: str | None = None,
score_type: str = 'points',
campaign: str | None = None,
level: str | None = None,
) -> None:
"""(internal)
Submit a score to the server; callback will be called with the results.
@ -235,7 +270,9 @@ def submit_score(game: str,
return None
def tournament_query(callback: Callable[[dict | None], None],
args: dict) -> None:
def tournament_query(
callback: Callable[[dict | None], None], args: dict
) -> None:
"""(internal)"""
return None

View File

@ -9,15 +9,61 @@ In some specific cases you may need to pull in individual submodules instead.
# pylint: disable=redefined-builtin
from _ba import (
CollideModel, Context, ContextCall, Data, InputDevice, Material, Model,
Node, SessionPlayer, Sound, Texture, Timer, Vec3, Widget, buttonwidget,
camerashake, checkboxwidget, columnwidget, containerwidget, do_once,
emitfx, getactivity, getcollidemodel, getmodel, getnodes, getsession,
getsound, gettexture, hscrollwidget, imagewidget, newactivity, newnode,
playsound, printnodes, printobjects, pushcall, quit, rowwidget, safecolor,
screenmessage, scrollwidget, set_analytics_screen, charstr, textwidget,
time, timer, open_url, widget, clipboard_is_supported, clipboard_has_text,
clipboard_get_text, clipboard_set_text, getdata, in_logic_thread)
CollideModel,
Context,
ContextCall,
Data,
InputDevice,
Material,
Model,
Node,
SessionPlayer,
Sound,
Texture,
Timer,
Vec3,
Widget,
buttonwidget,
camerashake,
checkboxwidget,
columnwidget,
containerwidget,
do_once,
emitfx,
getactivity,
getcollidemodel,
getmodel,
getnodes,
getsession,
getsound,
gettexture,
hscrollwidget,
imagewidget,
newactivity,
newnode,
playsound,
printnodes,
printobjects,
pushcall,
quit,
rowwidget,
safecolor,
screenmessage,
scrollwidget,
set_analytics_screen,
charstr,
textwidget,
time,
timer,
open_url,
widget,
clipboard_is_supported,
clipboard_has_text,
clipboard_get_text,
clipboard_set_text,
getdata,
in_logic_thread,
)
from ba._activity import Activity
from ba._plugin import PotentialPlugin, Plugin, PluginSubsystem
from ba._actor import Actor
@ -27,21 +73,50 @@ from ba._app import App
from ba._cloud import CloudSubsystem
from ba._coopgame import CoopGameActivity
from ba._coopsession import CoopSession
from ba._dependency import (Dependency, DependencyComponent, DependencySet,
AssetPackage)
from ba._generated.enums import (TimeType, Permission, TimeFormat, SpecialChar,
InputType, UIScale)
from ba._dependency import (
Dependency,
DependencyComponent,
DependencySet,
AssetPackage,
)
from ba._generated.enums import (
TimeType,
Permission,
TimeFormat,
SpecialChar,
InputType,
UIScale,
)
from ba._error import (
print_exception, print_error, ContextError, NotFoundError,
PlayerNotFoundError, SessionPlayerNotFoundError, NodeNotFoundError,
ActorNotFoundError, InputDeviceNotFoundError, WidgetNotFoundError,
ActivityNotFoundError, TeamNotFoundError, SessionTeamNotFoundError,
SessionNotFoundError, DelegateNotFoundError, DependencyError)
print_exception,
print_error,
ContextError,
NotFoundError,
PlayerNotFoundError,
SessionPlayerNotFoundError,
NodeNotFoundError,
ActorNotFoundError,
InputDeviceNotFoundError,
WidgetNotFoundError,
ActivityNotFoundError,
TeamNotFoundError,
SessionTeamNotFoundError,
SessionNotFoundError,
DelegateNotFoundError,
DependencyError,
)
from ba._freeforallsession import FreeForAllSession
from ba._gameactivity import GameActivity
from ba._gameresults import GameResults
from ba._settings import (Setting, IntSetting, FloatSetting, ChoiceSetting,
BoolSetting, IntChoiceSetting, FloatChoiceSetting)
from ba._settings import (
Setting,
IntSetting,
FloatSetting,
ChoiceSetting,
BoolSetting,
IntChoiceSetting,
FloatChoiceSetting,
)
from ba._language import Lstr, LanguageSubsystem
from ba._map import Map, getmaps
from ba._session import Session
@ -57,23 +132,53 @@ from ba._appconfig import AppConfig
from ba._appdelegate import AppDelegate
from ba._apputils import is_browser_likely_available, garbage_collect
from ba._campaign import Campaign
from ba._gameutils import (GameTip, animate, animate_array, show_damage_count,
timestring, cameraflash)
from ba._general import (WeakCall, Call, existing, Existable,
verify_object_death, storagename, getclass)
from ba._gameutils import (
GameTip,
animate,
animate_array,
show_damage_count,
timestring,
cameraflash,
)
from ba._general import (
WeakCall,
Call,
existing,
Existable,
verify_object_death,
storagename,
getclass,
)
from ba._keyboard import Keyboard
from ba._level import Level
from ba._lobby import Lobby, Chooser
from ba._math import normalized_color, is_point_in_box, vec3validate
from ba._meta import MetadataSubsystem
from ba._messages import (UNHANDLED, OutOfBoundsMessage, DeathType, DieMessage,
PlayerDiedMessage, StandMessage, PickUpMessage,
DropMessage, PickedUpMessage, DroppedMessage,
ShouldShatterMessage, ImpactDamageMessage,
FreezeMessage, ThawMessage, HitMessage,
CelebrateMessage)
from ba._music import (setmusic, MusicPlayer, MusicType, MusicPlayMode,
MusicSubsystem)
from ba._messages import (
UNHANDLED,
OutOfBoundsMessage,
DeathType,
DieMessage,
PlayerDiedMessage,
StandMessage,
PickUpMessage,
DropMessage,
PickedUpMessage,
DroppedMessage,
ShouldShatterMessage,
ImpactDamageMessage,
FreezeMessage,
ThawMessage,
HitMessage,
CelebrateMessage,
)
from ba._music import (
setmusic,
MusicPlayer,
MusicType,
MusicPlayMode,
MusicSubsystem,
)
from ba._powerup import PowerupMessage, PowerupAcceptMessage
from ba._multiteamsession import MultiTeamSession
from ba.ui import Window, UIController, uicleanupcheck
@ -82,47 +187,185 @@ from ba._collision import Collision, getcollision
app: App
__all__ = [
'Achievement', 'AchievementSubsystem', 'Activity', 'ActivityNotFoundError',
'Actor', 'ActorNotFoundError', 'animate', 'animate_array', 'app', 'App',
'AppConfig', 'AppDelegate', 'AssetPackage', 'BoolSetting', 'buttonwidget',
'Call', 'cameraflash', 'camerashake', 'Campaign', 'CelebrateMessage',
'charstr', 'checkboxwidget', 'ChoiceSetting', 'Chooser',
'clipboard_get_text', 'clipboard_has_text', 'clipboard_is_supported',
'clipboard_set_text', 'CollideModel', 'Collision', 'columnwidget',
'containerwidget', 'Context', 'ContextCall', 'ContextError',
'CloudSubsystem', 'CoopGameActivity', 'CoopSession', 'Data', 'DeathType',
'DelegateNotFoundError', 'Dependency', 'DependencyComponent',
'DependencyError', 'DependencySet', 'DieMessage', 'do_once', 'DropMessage',
'DroppedMessage', 'DualTeamSession', 'emitfx', 'EmptyPlayer', 'EmptyTeam',
'Existable', 'existing', 'FloatChoiceSetting', 'FloatSetting',
'FreeForAllSession', 'FreezeMessage', 'GameActivity', 'GameResults',
'GameTip', 'garbage_collect', 'getactivity', 'getclass', 'getcollidemodel',
'getcollision', 'getdata', 'getmaps', 'getmodel', 'getnodes', 'getsession',
'getsound', 'gettexture', 'HitMessage', 'hscrollwidget', 'imagewidget',
'ImpactDamageMessage', 'in_logic_thread', 'InputDevice',
'InputDeviceNotFoundError', 'InputType', 'IntChoiceSetting', 'IntSetting',
'is_browser_likely_available', 'is_point_in_box', 'Keyboard',
'LanguageSubsystem', 'Level', 'Lobby', 'Lstr', 'Map', 'Material',
'MetadataSubsystem', 'Model', 'MultiTeamSession', 'MusicPlayer',
'MusicPlayMode', 'MusicSubsystem', 'MusicType', 'newactivity', 'newnode',
'Node', 'NodeActor', 'NodeNotFoundError', 'normalized_color',
'NotFoundError', 'open_url', 'OutOfBoundsMessage', 'Permission',
'PickedUpMessage', 'PickUpMessage', 'Player', 'PlayerDiedMessage',
'PlayerInfo', 'PlayerNotFoundError', 'PlayerRecord', 'PlayerScoredMessage',
'playsound', 'Plugin', 'PluginSubsystem', 'PotentialPlugin',
'PowerupAcceptMessage', 'PowerupMessage', 'print_error', 'print_exception',
'printnodes', 'printobjects', 'pushcall', 'quit', 'rowwidget', 'safecolor',
'ScoreConfig', 'ScoreType', 'screenmessage', 'scrollwidget',
'ServerController', 'Session', 'SessionNotFoundError', 'SessionPlayer',
'SessionPlayerNotFoundError', 'SessionTeam', 'SessionTeamNotFoundError',
'set_analytics_screen', 'setmusic', 'Setting', 'ShouldShatterMessage',
'show_damage_count', 'Sound', 'SpecialChar', 'StandLocation',
'StandMessage', 'Stats', 'storagename', 'Team', 'TeamGameActivity',
'TeamNotFoundError', 'Texture', 'textwidget', 'ThawMessage', 'time',
'TimeFormat', 'Timer', 'timer', 'timestring', 'TimeType', 'uicleanupcheck',
'UIController', 'UIScale', 'UISubsystem', 'UNHANDLED', 'Vec3',
'vec3validate', 'verify_object_death', 'WeakCall', 'Widget', 'widget',
'WidgetNotFoundError', 'Window'
'Achievement',
'AchievementSubsystem',
'Activity',
'ActivityNotFoundError',
'Actor',
'ActorNotFoundError',
'animate',
'animate_array',
'app',
'App',
'AppConfig',
'AppDelegate',
'AssetPackage',
'BoolSetting',
'buttonwidget',
'Call',
'cameraflash',
'camerashake',
'Campaign',
'CelebrateMessage',
'charstr',
'checkboxwidget',
'ChoiceSetting',
'Chooser',
'clipboard_get_text',
'clipboard_has_text',
'clipboard_is_supported',
'clipboard_set_text',
'CollideModel',
'Collision',
'columnwidget',
'containerwidget',
'Context',
'ContextCall',
'ContextError',
'CloudSubsystem',
'CoopGameActivity',
'CoopSession',
'Data',
'DeathType',
'DelegateNotFoundError',
'Dependency',
'DependencyComponent',
'DependencyError',
'DependencySet',
'DieMessage',
'do_once',
'DropMessage',
'DroppedMessage',
'DualTeamSession',
'emitfx',
'EmptyPlayer',
'EmptyTeam',
'Existable',
'existing',
'FloatChoiceSetting',
'FloatSetting',
'FreeForAllSession',
'FreezeMessage',
'GameActivity',
'GameResults',
'GameTip',
'garbage_collect',
'getactivity',
'getclass',
'getcollidemodel',
'getcollision',
'getdata',
'getmaps',
'getmodel',
'getnodes',
'getsession',
'getsound',
'gettexture',
'HitMessage',
'hscrollwidget',
'imagewidget',
'ImpactDamageMessage',
'in_logic_thread',
'InputDevice',
'InputDeviceNotFoundError',
'InputType',
'IntChoiceSetting',
'IntSetting',
'is_browser_likely_available',
'is_point_in_box',
'Keyboard',
'LanguageSubsystem',
'Level',
'Lobby',
'Lstr',
'Map',
'Material',
'MetadataSubsystem',
'Model',
'MultiTeamSession',
'MusicPlayer',
'MusicPlayMode',
'MusicSubsystem',
'MusicType',
'newactivity',
'newnode',
'Node',
'NodeActor',
'NodeNotFoundError',
'normalized_color',
'NotFoundError',
'open_url',
'OutOfBoundsMessage',
'Permission',
'PickedUpMessage',
'PickUpMessage',
'Player',
'PlayerDiedMessage',
'PlayerInfo',
'PlayerNotFoundError',
'PlayerRecord',
'PlayerScoredMessage',
'playsound',
'Plugin',
'PluginSubsystem',
'PotentialPlugin',
'PowerupAcceptMessage',
'PowerupMessage',
'print_error',
'print_exception',
'printnodes',
'printobjects',
'pushcall',
'quit',
'rowwidget',
'safecolor',
'ScoreConfig',
'ScoreType',
'screenmessage',
'scrollwidget',
'ServerController',
'Session',
'SessionNotFoundError',
'SessionPlayer',
'SessionPlayerNotFoundError',
'SessionTeam',
'SessionTeamNotFoundError',
'set_analytics_screen',
'setmusic',
'Setting',
'ShouldShatterMessage',
'show_damage_count',
'Sound',
'SpecialChar',
'StandLocation',
'StandMessage',
'Stats',
'storagename',
'Team',
'TeamGameActivity',
'TeamNotFoundError',
'Texture',
'textwidget',
'ThawMessage',
'time',
'TimeFormat',
'Timer',
'timer',
'timestring',
'TimeType',
'uicleanupcheck',
'UIController',
'UIScale',
'UISubsystem',
'UNHANDLED',
'Vec3',
'vec3validate',
'verify_object_death',
'WeakCall',
'Widget',
'widget',
'WidgetNotFoundError',
'Window',
]
@ -135,10 +378,12 @@ def _simplify_module_names() -> None:
# so let's make an exception for it.
if os.environ.get('BA_DOCS_GENERATION', '0') != '1':
from efro.util import set_canonical_module
globs = globals()
set_canonical_module(
module_globals=globs,
names=[n for n in globs.keys() if not n.startswith('_')])
names=[n for n in globs.keys() if not n.startswith('_')],
)
_simplify_module_names()

View File

@ -40,8 +40,10 @@ class AccountV1Subsystem:
# Auto-sign-in to a local account in a moment if we're set to.
def do_auto_sign_in() -> None:
if _ba.app.headless_mode or _ba.app.config.get(
'Auto Account State') == 'Local':
if (
_ba.app.headless_mode
or _ba.app.config.get('Auto Account State') == 'Local'
):
_internal.sign_in_v1('Local')
_ba.pushcall(do_auto_sign_in)
@ -60,9 +62,14 @@ class AccountV1Subsystem:
(internal)
"""
from ba._language import Lstr
_ba.screenmessage(Lstr(resource='getTicketsWindow.receivedTicketsText',
subs=[('${COUNT}', str(count))]),
color=(0, 1, 0))
_ba.screenmessage(
Lstr(
resource='getTicketsWindow.receivedTicketsText',
subs=[('${COUNT}', str(count))],
),
color=(0, 1, 0),
)
_ba.playsound(_ba.getsound('cashRegister'))
def cache_league_rank_data(self, data: Any) -> None:
@ -73,9 +80,9 @@ class AccountV1Subsystem:
"""(internal)"""
return self.league_rank_cache.get('info', None)
def get_league_rank_points(self,
data: dict[str, Any] | None,
subset: str | None = None) -> int:
def get_league_rank_points(
self, data: dict[str, Any] | None, subset: str | None = None
) -> int:
"""(internal)"""
if data is None:
return 0
@ -90,15 +97,23 @@ class AccountV1Subsystem:
if ach.complete:
total_ach_value += ach.power_ranking_value
trophies_total: int = (data['t0a'] * data['t0am'] +
data['t0b'] * data['t0bm'] +
data['t1'] * data['t1m'] +
data['t2'] * data['t2m'] +
data['t3'] * data['t3m'] +
data['t4'] * data['t4m'])
trophies_total: int = (
data['t0a'] * data['t0am']
+ data['t0b'] * data['t0bm']
+ data['t1'] * data['t1m']
+ data['t2'] * data['t2m']
+ data['t3'] * data['t3m']
+ data['t4'] * data['t4m']
)
if subset == 'trophyCount':
val: int = (data['t0a'] + data['t0b'] + data['t1'] + data['t2'] +
data['t3'] + data['t4'])
val: int = (
data['t0a']
+ data['t0b']
+ data['t1']
+ data['t2']
+ data['t3']
+ data['t4']
)
assert isinstance(val, int)
return val
if subset == 'trophies':
@ -108,41 +123,54 @@ class AccountV1Subsystem:
raise ValueError('invalid subset value: ' + str(subset))
if data['p']:
pro_mult = 1.0 + float(
_internal.get_v1_account_misc_read_val('proPowerRankingBoost',
0.0)) * 0.01
pro_mult = (
1.0
+ float(
_internal.get_v1_account_misc_read_val(
'proPowerRankingBoost', 0.0
)
)
* 0.01
)
else:
pro_mult = 1.0
# For final value, apply our pro mult and activeness-mult.
return int(
(total_ach_value + trophies_total) *
(data['act'] if data['act'] is not None else 1.0) * pro_mult)
(total_ach_value + trophies_total)
* (data['act'] if data['act'] is not None else 1.0)
* pro_mult
)
def cache_tournament_info(self, info: Any) -> None:
"""(internal)"""
from ba._generated.enums import TimeType, TimeFormat
for entry in info:
cache_entry = self.tournament_info[entry['tournamentID']] = (
copy.deepcopy(entry))
cache_entry = self.tournament_info[
entry['tournamentID']
] = copy.deepcopy(entry)
# Also store the time we received this, so we can adjust
# time-remaining values/etc.
cache_entry['timeReceived'] = _ba.time(TimeType.REAL,
TimeFormat.MILLISECONDS)
cache_entry['timeReceived'] = _ba.time(
TimeType.REAL, TimeFormat.MILLISECONDS
)
cache_entry['valid'] = True
def get_purchased_icons(self) -> list[str]:
"""(internal)"""
# pylint: disable=cyclic-import
from ba import _store
if _internal.get_v1_account_state() != 'signed_in':
return []
icons = []
store_items = _store.get_store_items()
for item_name, item in list(store_items.items()):
if item_name.startswith('icons.') and _internal.get_purchased(
item_name):
item_name
):
icons.append(item['icon'])
return icons
@ -160,23 +188,28 @@ class AccountV1Subsystem:
# If the short version of our account name currently cant be
# displayed by the game, cancel.
if not _ba.have_chars(
_internal.get_v1_account_display_string(full=False)):
_internal.get_v1_account_display_string(full=False)
):
return
config = _ba.app.config
if ('Player Profiles' not in config
or '__account__' not in config['Player Profiles']):
if (
'Player Profiles' not in config
or '__account__' not in config['Player Profiles']
):
# Create a spaz with a nice default purply color.
_internal.add_transaction({
'type': 'ADD_PLAYER_PROFILE',
'name': '__account__',
'profile': {
'character': 'Spaz',
'color': [0.5, 0.25, 1.0],
'highlight': [0.5, 0.25, 1.0]
_internal.add_transaction(
{
'type': 'ADD_PLAYER_PROFILE',
'name': '__account__',
'profile': {
'character': 'Spaz',
'color': [0.5, 0.25, 1.0],
'highlight': [0.5, 0.25, 1.0],
},
}
})
)
_internal.run_transactions()
def have_pro(self) -> bool:
@ -188,7 +221,8 @@ class AccountV1Subsystem:
_internal.get_purchased('upgrades.pro')
or _internal.get_purchased('static.pro')
or _internal.get_purchased('static.pro_sale')
or 'ballistica' + 'core' == _ba.appname())
or 'ballistica' + 'core' == _ba.appname()
)
def have_pro_options(self) -> bool:
"""Return whether pro-options are present.
@ -202,22 +236,31 @@ class AccountV1Subsystem:
# or also if we've been grandfathered in or are using ballistica-core
# builds.
return self.have_pro() or bool(
_internal.get_v1_account_misc_read_val_2('proOptionsUnlocked',
False)
or _ba.app.config.get('lc14292', 0) > 1)
_internal.get_v1_account_misc_read_val_2(
'proOptionsUnlocked', False
)
or _ba.app.config.get('lc14292', 0) > 1
)
def show_post_purchase_message(self) -> None:
"""(internal)"""
from ba._language import Lstr
from ba._generated.enums import TimeType
cur_time = _ba.time(TimeType.REAL)
if (self.last_post_purchase_message_time is None
or cur_time - self.last_post_purchase_message_time > 3.0):
if (
self.last_post_purchase_message_time is None
or cur_time - self.last_post_purchase_message_time > 3.0
):
self.last_post_purchase_message_time = cur_time
with _ba.Context('ui'):
_ba.screenmessage(Lstr(resource='updatingAccountText',
fallback_resource='purchasingText'),
color=(0, 1, 0))
_ba.screenmessage(
Lstr(
resource='updatingAccountText',
fallback_resource='purchasingText',
),
color=(0, 1, 0),
)
_ba.playsound(_ba.getsound('click01'))
def on_account_state_changed(self) -> None:
@ -225,16 +268,21 @@ class AccountV1Subsystem:
from ba._language import Lstr
# Run any pending promo codes we had queued up while not signed in.
if _internal.get_v1_account_state(
) == 'signed_in' and self.pending_promo_codes:
if (
_internal.get_v1_account_state() == 'signed_in'
and self.pending_promo_codes
):
for code in self.pending_promo_codes:
_ba.screenmessage(Lstr(resource='submittingPromoCodeText'),
color=(0, 1, 0))
_internal.add_transaction({
'type': 'PROMO_CODE',
'expire_time': time.time() + 5,
'code': code
})
_ba.screenmessage(
Lstr(resource='submittingPromoCodeText'), color=(0, 1, 0)
)
_internal.add_transaction(
{
'type': 'PROMO_CODE',
'expire_time': time.time() + 5,
'code': code,
}
)
_internal.run_transactions()
self.pending_promo_codes = []
@ -254,18 +302,18 @@ class AccountV1Subsystem:
# If we're still not signed in and have pending codes,
# inform the user that they need to sign in to use them.
if self.pending_promo_codes:
_ba.screenmessage(Lstr(resource='signInForPromoCodeText'),
color=(1, 0, 0))
_ba.screenmessage(
Lstr(resource='signInForPromoCodeText'), color=(1, 0, 0)
)
_ba.playsound(_ba.getsound('error'))
self.pending_promo_codes.append(code)
_ba.timer(6.0, check_pending_codes, timetype=TimeType.REAL)
return
_ba.screenmessage(Lstr(resource='submittingPromoCodeText'),
color=(0, 1, 0))
_internal.add_transaction({
'type': 'PROMO_CODE',
'expire_time': time.time() + 5,
'code': code
})
_ba.screenmessage(
Lstr(resource='submittingPromoCodeText'), color=(0, 1, 0)
)
_internal.add_transaction(
{'type': 'PROMO_CODE', 'expire_time': time.time() + 5, 'code': code}
)
_internal.run_transactions()

View File

@ -55,8 +55,9 @@ class AccountV2Subsystem:
"""Internal - should be overridden by subclass."""
return None
def on_primary_account_changed(self,
account: AccountV2Handle | None) -> None:
def on_primary_account_changed(
self, account: AccountV2Handle | None
) -> None:
"""Callback run after the primary account changes.
Will be called with None on log-outs or when new credentials
@ -70,13 +71,16 @@ class AccountV2Subsystem:
# informed when that process completes.
if account.workspaceid is not None:
assert account.workspacename is not None
if (not self._initial_login_completed
and not self._kicked_off_workspace_load):
if (
not self._initial_login_completed
and not self._kicked_off_workspace_load
):
self._kicked_off_workspace_load = True
_ba.app.workspaces.set_active_workspace(
workspaceid=account.workspaceid,
workspacename=account.workspacename,
on_completed=self._on_set_active_workspace_completed)
on_completed=self._on_set_active_workspace_completed,
)
else:
# Don't activate workspaces if we've already told the game
# that initial-log-in is done or if we've already kicked
@ -84,7 +88,8 @@ class AccountV2Subsystem:
_ba.screenmessage(
f'\'{account.workspacename}\''
f' will be activated at next app launch.',
color=(1, 1, 0))
color=(1, 1, 0),
)
_ba.playsound(_ba.getsound('error'))
return
@ -124,8 +129,7 @@ class AccountV2Handle:
self.workspaceid: str | None = None
def __enter__(self) -> None:
"""Support for "with" statement.
"""
"""Support for "with" statement."""
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any:
"""Support for "with" statement."""

File diff suppressed because it is too large Load Diff

View File

@ -9,8 +9,12 @@ from typing import TYPE_CHECKING, Generic, TypeVar
import _ba
from ba._team import Team
from ba._player import Player
from ba._error import (print_exception, SessionTeamNotFoundError,
SessionPlayerNotFoundError, NodeNotFoundError)
from ba._error import (
print_exception,
SessionTeamNotFoundError,
SessionPlayerNotFoundError,
NodeNotFoundError,
)
from ba._dependency import DependencyComponent
from ba._general import Call, verify_object_death
from ba._messages import UNHANDLED
@ -199,8 +203,11 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
session = self._session()
if session is not None:
_ba.pushcall(
Call(session.transitioning_out_activity_was_freed,
self.can_show_ad_on_death))
Call(
session.transitioning_out_activity_was_freed,
self.can_show_ad_on_death,
)
)
@property
def globalsnode(self) -> ba.Node:
@ -220,6 +227,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
"""
if self._stats is None:
from ba._error import NotFoundError
raise NotFoundError()
return self._stats
@ -285,7 +293,8 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
5.0,
Call(self._check_activity_death, ref, [0]),
repeat=True,
timetype=TimeType.REAL)
timetype=TimeType.REAL,
)
# Run _expire in an empty context; nothing should be happening in
# there except deleting things which requires no context.
@ -296,8 +305,9 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
with _ba.Context('empty'):
self._expire()
else:
raise RuntimeError(f'destroy() called when'
f' already expired for {self}')
raise RuntimeError(
f'destroy() called when' f' already expired for {self}'
)
def retain_actor(self, actor: ba.Actor) -> None:
"""Add a strong-reference to a ba.Actor to this Activity.
@ -308,6 +318,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
"""
if __debug__:
from ba._actor import Actor
assert isinstance(actor, Actor)
self._actor_refs.append(actor)
@ -318,6 +329,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
"""
if __debug__:
from ba._actor import Actor
assert isinstance(actor, Actor)
self._actor_weak_refs.append(weakref.ref(actor))
@ -330,6 +342,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
session = self._session()
if session is None:
from ba._error import SessionNotFoundError
raise SessionNotFoundError()
return session
@ -381,7 +394,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
def has_transitioned_in(self) -> bool:
"""Return whether ba.Activity.on_transition_in()
has been called."""
has been called."""
return self._has_transitioned_in
def has_begun(self) -> bool:
@ -425,7 +438,8 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
if self.inherits_vr_overlay_center and prev_globals is not None:
glb.vr_overlay_center = prev_globals.vr_overlay_center
glb.vr_overlay_center_enabled = (
prev_globals.vr_overlay_center_enabled)
prev_globals.vr_overlay_center_enabled
)
# If they want to inherit tint from the previous self.
if self.inherits_tint and prev_globals is not None:
@ -435,9 +449,9 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
# Start pruning our various things periodically.
self._prune_dead_actors()
self._prune_dead_actors_timer = _ba.Timer(5.17,
self._prune_dead_actors,
repeat=True)
self._prune_dead_actors_timer = _ba.Timer(
5.17, self._prune_dead_actors, repeat=True
)
_ba.timer(13.3, self._prune_delay_deletes, repeat=True)
@ -491,10 +505,9 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
# activity launch; just wanna be sure that is intentional.
self.on_begin()
def end(self,
results: Any = None,
delay: float = 0.0,
force: bool = False) -> None:
def end(
self, results: Any = None, delay: float = 0.0, force: bool = False
) -> None:
"""Commences Activity shutdown and delivers results to the ba.Session.
'delay' is the time delay before the Activity actually ends
@ -543,7 +556,8 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
sessionplayer.setactivity(self)
with _ba.Context(self):
sessionplayer.activityplayer = player = self.create_player(
sessionplayer)
sessionplayer
)
player.postinit(sessionplayer)
assert player not in team.players
@ -654,7 +668,8 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
self._teams_that_left.append(weakref.ref(team))
def _reset_session_player_for_no_activity(
self, sessionplayer: ba.SessionPlayer) -> None:
self, sessionplayer: ba.SessionPlayer
) -> None:
# Let's be extra-defensive here: killing a node/input-call/etc
# could trigger user-code resulting in errors, but we would still
@ -664,13 +679,15 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
except Exception:
print_exception(
f'Error resetting SessionPlayer node on {sessionplayer}'
f' for {self}.')
f' for {self}.'
)
try:
sessionplayer.resetinput()
except Exception:
print_exception(
f'Error resetting SessionPlayer input on {sessionplayer}'
f' for {self}.')
f' for {self}.'
)
# These should never fail I think...
sessionplayer.setactivity(None)
@ -691,21 +708,26 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
self._playertype = type(self).__orig_bases__[-1].__args__[0]
if not isinstance(self._playertype, type):
self._playertype = Player
print(f'ERROR: {type(self)} was not passed a Player'
f' type argument; please explicitly pass ba.Player'
f' if you do not want to override it.')
print(
f'ERROR: {type(self)} was not passed a Player'
f' type argument; please explicitly pass ba.Player'
f' if you do not want to override it.'
)
self._teamtype = type(self).__orig_bases__[-1].__args__[1]
if not isinstance(self._teamtype, type):
self._teamtype = Team
print(f'ERROR: {type(self)} was not passed a Team'
f' type argument; please explicitly pass ba.Team'
f' if you do not want to override it.')
print(
f'ERROR: {type(self)} was not passed a Team'
f' type argument; please explicitly pass ba.Team'
f' if you do not want to override it.'
)
assert issubclass(self._playertype, Player)
assert issubclass(self._teamtype, Team)
@classmethod
def _check_activity_death(cls, activity_ref: weakref.ref[Activity],
counter: list[int]) -> None:
def _check_activity_death(
cls, activity_ref: weakref.ref[Activity], counter: list[int]
) -> None:
"""Sanity check to make sure an Activity was destroyed properly.
Receives a weakref to a ba.Activity which should have torn itself
@ -715,9 +737,13 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
try:
import gc
import types
activity = activity_ref()
print('ERROR: Activity is not dying when expected:', activity,
'(warning ' + str(counter[0] + 1) + ')')
print(
'ERROR: Activity is not dying when expected:',
activity,
'(warning ' + str(counter[0] + 1) + ')',
)
print('This means something is still strong-referencing it.')
counter[0] += 1
@ -784,8 +810,9 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
try:
actor.on_expire()
except Exception:
print_exception(f'Error in Actor.on_expire()'
f' for {actor_ref()}.')
print_exception(
f'Error in Actor.on_expire()' f' for {actor_ref()}.'
)
def _expire_players(self) -> None:

View File

@ -9,6 +9,7 @@ import _ba
from ba._activity import Activity
from ba._music import setmusic, MusicType
from ba._generated.enums import InputType, UIScale
# False-positive from pylint due to our class-generics-filter.
from ba._player import EmptyPlayer # pylint: disable=W0611
from ba._team import EmptyTeam # pylint: disable=W0611
@ -40,6 +41,7 @@ class EndSessionActivity(Activity[EmptyPlayer, EmptyTeam]):
# pylint: disable=cyclic-import
from bastd.mainmenu import MainMenuSession
from ba._general import Call
super().on_begin()
_ba.unlock_all_input()
_ba.app.ads.call_after_ad(Call(_ba.new_host_session, MainMenuSession))
@ -72,10 +74,11 @@ class JoinActivity(Activity[EmptyPlayer, EmptyTeam]):
# pylint: disable=cyclic-import
from bastd.actor.tipstext import TipsText
from bastd.actor.background import Background
super().on_transition_in()
self._background = Background(fade_time=0.5,
start_faded=True,
show_logo=True)
self._background = Background(
fade_time=0.5, start_faded=True, show_logo=True
)
self._tips_text = TipsText()
setmusic(MusicType.CHAR_SELECT)
self._join_info = self.session.lobby.create_join_info()
@ -103,10 +106,11 @@ class TransitionActivity(Activity[EmptyPlayer, EmptyTeam]):
def on_transition_in(self) -> None:
# pylint: disable=cyclic-import
from bastd.actor import background # FIXME: Don't use bastd from ba.
super().on_transition_in()
self._background = background.Background(fade_time=0.5,
start_faded=False,
show_logo=False)
self._background = background.Background(
fade_time=0.5, start_faded=False, show_logo=False
)
def on_begin(self) -> None:
super().on_begin()
@ -143,9 +147,11 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
def on_player_join(self, player: EmptyPlayer) -> None:
from ba._general import WeakCall
super().on_player_join(player)
time_till_assign = max(
0, self._birth_time + self._min_view_time - _ba.time())
0, self._birth_time + self._min_view_time - _ba.time()
)
# If we're still kicking at the end of our assign-delay, assign this
# guy's input to trigger us.
@ -154,10 +160,11 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
def on_transition_in(self) -> None:
from bastd.actor.tipstext import TipsText
from bastd.actor.background import Background
super().on_transition_in()
self._background = Background(fade_time=0.5,
start_faded=False,
show_logo=True)
self._background = Background(
fade_time=0.5, start_faded=False, show_logo=True
)
if self._default_show_tips:
self._tips_text = TipsText()
setmusic(self.default_music)
@ -166,6 +173,7 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
# pylint: disable=cyclic-import
from bastd.actor.text import Text
from ba import _language
super().on_begin()
# Pop up a 'press any button to continue' statement after our
@ -178,24 +186,30 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
else:
sval = _language.Lstr(resource='pressAnyButtonText')
Text(self._custom_continue_message
if self._custom_continue_message is not None else sval,
v_attach=Text.VAttach.BOTTOM,
h_align=Text.HAlign.CENTER,
flash=True,
vr_depth=50,
position=(0, 10),
scale=0.8,
color=(0.5, 0.7, 0.5, 0.5),
transition=Text.Transition.IN_BOTTOM_SLOW,
transition_delay=self._min_view_time).autoretain()
Text(
self._custom_continue_message
if self._custom_continue_message is not None
else sval,
v_attach=Text.VAttach.BOTTOM,
h_align=Text.HAlign.CENTER,
flash=True,
vr_depth=50,
position=(0, 10),
scale=0.8,
color=(0.5, 0.7, 0.5, 0.5),
transition=Text.Transition.IN_BOTTOM_SLOW,
transition_delay=self._min_view_time,
).autoretain()
def _player_press(self) -> None:
# If this activity is a good 'end point', ask server-mode just once if
# it wants to do anything special like switch sessions or kill the app.
if (self._allow_server_transition and _ba.app.server is not None
and self._server_transitioning is None):
if (
self._allow_server_transition
and _ba.app.server is not None
and self._server_transitioning is None
):
self._server_transitioning = _ba.app.server.handle_transition()
assert isinstance(self._server_transitioning, bool)
@ -211,6 +225,12 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
# Just to be extra careful, don't assign if we're transitioning out.
# (though theoretically that should be ok).
if not self.is_transitioning_out() and player:
player.assigninput((InputType.JUMP_PRESS, InputType.PUNCH_PRESS,
InputType.BOMB_PRESS, InputType.PICK_UP_PRESS),
self._player_press)
player.assigninput(
(
InputType.JUMP_PRESS,
InputType.PUNCH_PRESS,
InputType.BOMB_PRESS,
InputType.PICK_UP_PRESS,
),
self._player_press,
)

View File

@ -38,31 +38,41 @@ class AdsSubsystem:
# Print this message once every 10 minutes at most.
tval = _ba.time(TimeType.REAL)
if (self.last_in_game_ad_remove_message_show_time is None or
(tval - self.last_in_game_ad_remove_message_show_time > 60 * 10)):
if self.last_in_game_ad_remove_message_show_time is None or (
tval - self.last_in_game_ad_remove_message_show_time > 60 * 10
):
self.last_in_game_ad_remove_message_show_time = tval
with _ba.Context('ui'):
_ba.timer(
1.0,
lambda: _ba.screenmessage(Lstr(
resource='removeInGameAdsText',
subs=[('${PRO}',
Lstr(resource='store.bombSquadProNameText')),
('${APP_NAME}', Lstr(resource='titleText'))]),
color=(1, 1, 0)),
timetype=TimeType.REAL)
lambda: _ba.screenmessage(
Lstr(
resource='removeInGameAdsText',
subs=[
(
'${PRO}',
Lstr(resource='store.bombSquadProNameText'),
),
('${APP_NAME}', Lstr(resource='titleText')),
],
),
color=(1, 1, 0),
),
timetype=TimeType.REAL,
)
def show_ad(self,
purpose: str,
on_completion_call: Callable[[], Any] | None = None) -> None:
def show_ad(
self, purpose: str, on_completion_call: Callable[[], Any] | None = None
) -> None:
"""(internal)"""
self.last_ad_purpose = purpose
_ba.show_ad(purpose, on_completion_call)
def show_ad_2(
self,
purpose: str,
on_completion_call: Callable[[bool], Any] | None = None) -> None:
self,
purpose: str,
on_completion_call: Callable[[bool], Any] | None = None,
) -> None:
"""(internal)"""
self.last_ad_purpose = purpose
_ba.show_ad_2(purpose, on_completion_call)
@ -73,6 +83,7 @@ class AdsSubsystem:
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
from ba._generated.enums import TimeType
app = _ba.app
show = True
@ -95,16 +106,22 @@ class AdsSubsystem:
launch_count = app.config.get('launchCount', 0)
# If we're seeing short ads we may want to space them differently.
interval_mult = (_internal.get_v1_account_misc_read_val(
'ads.shortIntervalMult', 1.0)
if self.last_ad_was_short else 1.0)
interval_mult = (
_internal.get_v1_account_misc_read_val(
'ads.shortIntervalMult', 1.0
)
if self.last_ad_was_short
else 1.0
)
if self.ad_amt is None:
if launch_count <= 1:
self.ad_amt = _internal.get_v1_account_misc_read_val(
'ads.startVal1', 0.99)
'ads.startVal1', 0.99
)
else:
self.ad_amt = _internal.get_v1_account_misc_read_val(
'ads.startVal2', 1.0)
'ads.startVal2', 1.0
)
interval = None
else:
# So far we're cleared to show; now calc our
@ -113,27 +130,33 @@ class AdsSubsystem:
# playing).
base = 'ads' if _ba.has_video_ads() else 'ads2'
min_lc = _internal.get_v1_account_misc_read_val(
base + '.minLC', 0.0)
base + '.minLC', 0.0
)
max_lc = _internal.get_v1_account_misc_read_val(
base + '.maxLC', 5.0)
min_lc_scale = (_internal.get_v1_account_misc_read_val(
base + '.minLCScale', 0.25))
max_lc_scale = (_internal.get_v1_account_misc_read_val(
base + '.maxLCScale', 0.34))
min_lc_interval = (_internal.get_v1_account_misc_read_val(
base + '.minLCInterval', 360))
max_lc_interval = (_internal.get_v1_account_misc_read_val(
base + '.maxLCInterval', 300))
base + '.maxLC', 5.0
)
min_lc_scale = _internal.get_v1_account_misc_read_val(
base + '.minLCScale', 0.25
)
max_lc_scale = _internal.get_v1_account_misc_read_val(
base + '.maxLCScale', 0.34
)
min_lc_interval = _internal.get_v1_account_misc_read_val(
base + '.minLCInterval', 360
)
max_lc_interval = _internal.get_v1_account_misc_read_val(
base + '.maxLCInterval', 300
)
if launch_count < min_lc:
lc_amt = 0.0
elif launch_count > max_lc:
lc_amt = 1.0
else:
lc_amt = ((float(launch_count) - min_lc) /
(max_lc - min_lc))
lc_amt = (float(launch_count) - min_lc) / (max_lc - min_lc)
incr = (1.0 - lc_amt) * min_lc_scale + lc_amt * max_lc_scale
interval = ((1.0 - lc_amt) * min_lc_interval +
lc_amt * max_lc_interval)
interval = (
1.0 - lc_amt
) * min_lc_interval + lc_amt * max_lc_interval
self.ad_amt += incr
assert self.ad_amt is not None
if self.ad_amt >= 1.0:
@ -143,12 +166,14 @@ class AdsSubsystem:
# After we've reached the traditional show-threshold once,
# try again whenever its been INTERVAL since our last successful
# show.
elif (
self.attempted_first_ad and
(self.last_ad_completion_time is None or
(interval is not None
and _ba.time(TimeType.REAL) - self.last_ad_completion_time >
(interval * interval_mult)))):
elif self.attempted_first_ad and (
self.last_ad_completion_time is None
or (
interval is not None
and _ba.time(TimeType.REAL) - self.last_ad_completion_time
> (interval * interval_mult)
)
):
# Reset our other counter too in this case.
self.ad_amt = 0.0
else:
@ -161,7 +186,6 @@ class AdsSubsystem:
# (in case some random ad network doesn't properly deliver its
# completion callback).
class _Payload:
def __init__(self, pcall: Callable[[], Any]):
self._call = pcall
self._ran = False
@ -171,20 +195,31 @@ class AdsSubsystem:
if not self._ran:
if fallback:
print(
('ERROR: relying on fallback ad-callback! '
'last network: ' + app.ads.last_ad_network +
' (set ' + str(
int(time.time() -
app.ads.last_ad_network_set_time)) +
's ago); purpose=' + app.ads.last_ad_purpose))
(
'ERROR: relying on fallback ad-callback! '
'last network: '
+ app.ads.last_ad_network
+ ' (set '
+ str(
int(
time.time()
- app.ads.last_ad_network_set_time
)
)
+ 's ago); purpose='
+ app.ads.last_ad_purpose
)
)
_ba.pushcall(self._call)
self._ran = True
payload = _Payload(call)
with _ba.Context('ui'):
_ba.timer(5.0,
lambda: payload.run(fallback=True),
timetype=TimeType.REAL)
_ba.timer(
5.0,
lambda: payload.run(fallback=True),
timetype=TimeType.REAL,
)
self.show_ad('between_game', on_completion_call=payload.run)
else:
_ba.pushcall(call) # Just run the callback without the ad.

View File

@ -20,6 +20,7 @@ def game_begin_analytics() -> None:
from ba._freeforallsession import FreeForAllSession
from ba._coopsession import CoopSession
from ba._gameactivity import GameActivity
activity = _ba.getactivity(False)
session = _ba.getsession(False)
@ -31,8 +32,11 @@ def game_begin_analytics() -> None:
campaign = session.campaign
assert campaign is not None
_ba.set_analytics_screen(
'Coop Game: ' + campaign.name + ' ' +
campaign.getlevel(_ba.app.coop_session_args['level']).name)
'Coop Game: '
+ campaign.name
+ ' '
+ campaign.getlevel(_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')
@ -49,9 +53,11 @@ def game_begin_analytics() -> None:
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')
_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')
@ -60,14 +66,18 @@ def game_begin_analytics() -> None:
_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')
'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')
_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')
'Free-for-all round start 8+ human players'
)
# For some analytics tracking on the c layer.
_ba.reset_game_activity_tracking()

View File

@ -198,6 +198,7 @@ class App:
accordingly and set to target the new API version number.
"""
from ba._meta import CURRENT_API_VERSION
return CURRENT_API_VERSION
@property
@ -369,19 +370,33 @@ class App:
# FIXME: This should not be hard-coded.
for maptype in [
stdmaps.HockeyStadium, stdmaps.FootballStadium,
stdmaps.Bridgit, stdmaps.BigG, stdmaps.Roundabout,
stdmaps.MonkeyFace, stdmaps.ZigZag, stdmaps.ThePad,
stdmaps.DoomShroom, stdmaps.LakeFrigid, stdmaps.TipTop,
stdmaps.CragCastle, stdmaps.TowerD, stdmaps.HappyThoughts,
stdmaps.StepRightUp, stdmaps.Courtyard, stdmaps.Rampage
stdmaps.HockeyStadium,
stdmaps.FootballStadium,
stdmaps.Bridgit,
stdmaps.BigG,
stdmaps.Roundabout,
stdmaps.MonkeyFace,
stdmaps.ZigZag,
stdmaps.ThePad,
stdmaps.DoomShroom,
stdmaps.LakeFrigid,
stdmaps.TipTop,
stdmaps.CragCastle,
stdmaps.TowerD,
stdmaps.HappyThoughts,
stdmaps.StepRightUp,
stdmaps.Courtyard,
stdmaps.Rampage,
]:
_map.register_map(maptype)
# Non-test, non-debug builds should generally be blessed; warn if not.
# (so I don't accidentally release a build that can't play tourneys)
if (not self.debug_build and not self.test_build
and not _internal.is_blessed()):
if (
not self.debug_build
and not self.test_build
and not _internal.is_blessed()
):
_ba.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0))
# If there's a leftover log file, attempt to upload it to the
@ -393,6 +408,7 @@ class App:
if not self.config_file_healthy:
if self.platform in ('mac', 'linux', 'windows'):
from bastd.ui import configerror
configerror.ConfigErrorWindow()
return
@ -418,10 +434,13 @@ class App:
# pending special offer.
def check_special_offer() -> None:
from bastd.ui.specialoffer import show_offer
config = self.config
if ('pendingSpecialOffer' in config
and _internal.get_public_login_id()
== config['pendingSpecialOffer']['a']):
if (
'pendingSpecialOffer' in config
and _internal.get_public_login_id()
== config['pendingSpecialOffer']['a']
):
self.special_offer = config['pendingSpecialOffer']['o']
show_offer()
@ -436,8 +455,9 @@ class App:
# See note below in on_app_pause.
if self.state != self.State.LAUNCHING:
logging.error('on_app_launch found state %s; expected LAUNCHING.',
self.state)
logging.error(
'on_app_launch found state %s; expected LAUNCHING.', self.state
)
self._launch_completed = True
self._update_state()
@ -501,6 +521,7 @@ class App:
def read_config(self) -> None:
"""(internal)"""
from ba._appconfig import read_config
self._config, self.config_file_healthy = read_config()
def pause(self) -> None:
@ -510,8 +531,11 @@ class App:
to pause ..we now no longer pause if there are connected clients.
"""
activity: ba.Activity | None = _ba.get_foreground_host_activity()
if (activity is not None and activity.allow_pausing
and not _ba.have_connected_clients()):
if (
activity is not None
and activity.allow_pausing
and not _ba.have_connected_clients()
):
from ba._language import Lstr
from ba._nodeactor import NodeActor
@ -525,13 +549,16 @@ class App:
# FIXME: This should not be an attr on Actor.
activity.paused_text = NodeActor(
_ba.newnode('text',
attrs={
'text': Lstr(resource='pausedByHostText'),
'client_only': True,
'flatness': 1.0,
'h_align': 'center'
}))
_ba.newnode(
'text',
attrs={
'text': Lstr(resource='pausedByHostText'),
'client_only': True,
'flatness': 1.0,
'h_align': 'center',
},
)
)
def resume(self) -> None:
"""Resume the game due to a user request or menu closing.
@ -562,13 +589,15 @@ class App:
# Make note to add it to our challenges UI.
self.custom_coop_practice_games.append(f'Challenges:{level.name}')
def return_to_main_menu_session_gracefully(self,
reset_ui: bool = True) -> None:
def return_to_main_menu_session_gracefully(
self, reset_ui: bool = True
) -> None:
"""Attempt to cleanly get back to the main menu."""
# pylint: disable=cyclic-import
from ba import _benchmark
from ba._general import Call
from bastd.mainmenu import MainMenuSession
if reset_ui:
_ba.app.ui.clear_main_menu_window()
@ -587,10 +616,9 @@ class App:
# Kick off a little transaction so we'll hopefully have all the
# latest account state when we get back to the menu.
_internal.add_transaction({
'type': 'END_SESSION',
'sType': str(type(host_session))
})
_internal.add_transaction(
{'type': 'END_SESSION', 'sType': str(type(host_session))}
)
_internal.run_transactions()
host_session.end()
@ -609,14 +637,14 @@ class App:
else:
self.main_menu_resume_callbacks.append(call)
def launch_coop_game(self,
game: str,
force: bool = False,
args: dict | None = None) -> bool:
def launch_coop_game(
self, game: str, force: bool = False, args: dict | None = None
) -> bool:
"""High level way to launch a local co-op session."""
# pylint: disable=cyclic-import
from ba._campaign import getcampaign
from bastd.ui.coop.level import CoopLevelLockedWindow
if args is None:
args = {}
if game == '':
@ -633,7 +661,8 @@ class App:
if not level.complete:
CoopLevelLockedWindow(
campaign.getlevel(levelname).displayname,
campaign.getlevel(level.name).displayname)
campaign.getlevel(level.name).displayname,
)
return False
# Ok, we're good to go.
@ -646,12 +675,15 @@ class App:
def _fade_end() -> None:
from ba import _coopsession
try:
_ba.new_host_session(_coopsession.CoopSession)
except Exception:
from ba import _error
_error.print_exception()
from bastd.mainmenu import MainMenuSession
_ba.new_host_session(MainMenuSession)
_ba.fade_screen(False, endcall=_fade_end)
@ -660,6 +692,7 @@ class App:
def handle_deep_link(self, url: str) -> None:
"""Handle a deep link URL."""
from ba._language import Lstr
appname = _ba.appname()
if url.startswith(f'{appname}://code/'):
code = url.replace(f'{appname}://code/', '')

View File

@ -115,16 +115,29 @@ def read_config() -> tuple[AppConfig, bool]:
config_file_healthy = True
except Exception as exc:
print(('error reading config file at time ' +
str(_ba.time(TimeType.REAL)) + ': \'' + config_file_path +
'\':\n'), exc)
print(
(
'error reading config file at time '
+ str(_ba.time(TimeType.REAL))
+ ': \''
+ config_file_path
+ '\':\n'
),
exc,
)
# Whenever this happens lets back up the broken one just in case it
# gets overwritten accidentally.
print(('backing up current config file to \'' + config_file_path +
".broken\'"))
print(
(
'backing up current config file to \''
+ config_file_path
+ ".broken\'"
)
)
try:
import shutil
shutil.copyfile(config_file_path, config_file_path + '.broken')
except Exception as exc2:
print('EXC copying broken config:', exc2)
@ -154,8 +167,11 @@ def commit_app_config(force: bool = False) -> None:
(internal)
"""
from ba._internal import mark_config_dirty
if not _ba.app.config_file_healthy and not force:
print('Current config file is broken; '
'skipping write to avoid losing settings.')
print(
'Current config file is broken; '
'skipping write to avoid losing settings.'
)
return
mark_config_dirty()

View File

@ -17,9 +17,12 @@ class AppDelegate:
"""
def create_default_game_settings_ui(
self, gameclass: type[ba.GameActivity],
sessiontype: type[ba.Session], settings: dict | None,
completion_call: Callable[[dict | None], None]) -> None:
self,
gameclass: type[ba.GameActivity],
sessiontype: type[ba.Session],
settings: dict | None,
completion_call: Callable[[dict | None], None],
) -> None:
"""Launch a UI to configure the given game config.
It should manipulate the contents of config and call completion_call
@ -27,5 +30,7 @@ class AppDelegate:
"""
del gameclass, sessiontype, settings, completion_call # Unused.
from ba import _error
_error.print_error(
"create_default_game_settings_ui needs to be overridden")
"create_default_game_settings_ui needs to be overridden"
)

View File

@ -42,6 +42,7 @@ def is_browser_likely_available() -> bool:
def get_remote_app_name() -> ba.Lstr:
"""(internal)"""
from ba import _language
return _language.Lstr(resource='remote_app.app_name')
@ -59,6 +60,7 @@ def handle_v1_cloud_log() -> None:
from ba._net import master_server_post
from ba._generated.enums import TimeType
from ba._internal import get_news_show
app = _ba.app
app.log_have_new = True
if not app.log_upload_timer_started:
@ -112,10 +114,12 @@ def handle_v1_cloud_log() -> None:
if not _ba.is_log_full():
with _ba.Context('ui'):
_ba.timer(600.0,
_reset,
timetype=TimeType.REAL,
suppress_format_warning=True)
_ba.timer(
600.0,
_reset,
timetype=TimeType.REAL,
suppress_format_warning=True,
)
def handle_leftover_v1_cloud_log_file() -> None:
@ -125,8 +129,9 @@ def handle_leftover_v1_cloud_log_file() -> None:
from ba._net import master_server_post
if os.path.exists(_ba.get_v1_cloud_log_file_path()):
with open(_ba.get_v1_cloud_log_file_path(),
encoding='utf-8') as infile:
with open(
_ba.get_v1_cloud_log_file_path(), encoding='utf-8'
) as infile:
info = json.loads(infile.read())
infile.close()
do_send = should_submit_debug_info()
@ -150,6 +155,7 @@ def handle_leftover_v1_cloud_log_file() -> None:
os.remove(_ba.get_v1_cloud_log_file_path())
except Exception:
from ba import _error
_error.print_exception('Error handling leftover log file.')
@ -180,9 +186,10 @@ def garbage_collect() -> None:
def print_live_object_warnings(
when: Any,
ignore_session: ba.Session | None = None,
ignore_activity: ba.Activity | None = None) -> None:
when: Any,
ignore_session: ba.Session | None = None,
ignore_activity: ba.Activity | None = None,
) -> None:
"""Print warnings for remaining objects in the current context."""
# pylint: disable=cyclic-import
from ba._session import Session
@ -229,13 +236,17 @@ def print_corrupt_file_error() -> None:
"""Print an error if a corrupt file is found."""
from ba._general import Call
from ba._generated.enums import TimeType
_ba.timer(2.0,
lambda: _ba.screenmessage(
_ba.app.lang.get_resource('internal.corruptFileText').
replace('${EMAIL}', 'support@froemling.net'),
color=(1, 0, 0),
),
timetype=TimeType.REAL)
_ba.timer(2.0,
Call(_ba.playsound, _ba.getsound('error')),
timetype=TimeType.REAL)
_ba.timer(
2.0,
lambda: _ba.screenmessage(
_ba.app.lang.get_resource('internal.corruptFileText').replace(
'${EMAIL}', 'support@froemling.net'
),
color=(1, 0, 0),
),
timetype=TimeType.REAL,
)
_ba.timer(
2.0, Call(_ba.playsound, _ba.getsound('error')), timetype=TimeType.REAL
)

View File

@ -15,8 +15,12 @@ import time
import os
import sys
from efro.dataclassio import (ioprepped, IOAttrs, dataclass_from_json,
dataclass_to_json)
from efro.dataclassio import (
ioprepped,
IOAttrs,
dataclass_from_json,
dataclass_to_json,
)
import _ba
if TYPE_CHECKING:
@ -34,8 +38,9 @@ class FileValue:
class State:
"""Holds all persistent state for the asset-manager."""
files: Annotated[dict[str, FileValue],
IOAttrs('files')] = field(default_factory=dict)
files: Annotated[dict[str, FileValue], IOAttrs('files')] = field(
default_factory=dict
)
class AssetManager:
@ -66,8 +71,14 @@ class AssetManager:
account_token: str,
) -> AssetGather:
"""Spawn an asset-gather operation from this manager."""
print('would gather', packages, 'and flavor', flavor, 'with token',
account_token)
print(
'would gather',
packages,
'and flavor',
flavor,
'with token',
account_token,
)
return AssetGather(self)
def update(self) -> None:
@ -162,9 +173,7 @@ class AssetGather:
def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None:
"""Fetch a given url to a given filename for a given AssetGather.
"""
"""Fetch a given url to a given filename for a given AssetGather."""
# pylint: disable=consider-using-with
import socket
@ -175,9 +184,7 @@ def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None:
# Pass a very short timeout to urllib so we have opportunities
# to cancel even with network blockage.
req = urllib.request.urlopen(url,
context=_ba.app.net.sslcontext,
timeout=1)
req = urllib.request.urlopen(url, context=_ba.app.net.sslcontext, timeout=1)
file_size = int(req.headers['Content-Length'])
print(f'\nDownloading: {filename} Bytes: {file_size:,}')
@ -200,6 +207,7 @@ def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None:
data = req.read(block_sz)
except ValueError:
import traceback
traceback.print_exc()
print('VALUEERROR', flush=True)
break
@ -210,8 +218,10 @@ def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None:
print('\n\n\nsorry -- try back later')
os.unlink(filename)
raise
print('\nHmmm... little issue... '
'I\'ll wait a couple of seconds')
print(
'\nHmmm... little issue... '
'I\'ll wait a couple of seconds'
)
time.sleep(3)
time_outs += 1
continue

View File

@ -69,13 +69,14 @@ def setup_asyncio() -> asyncio.AbstractEventLoop:
if duration > warn_time:
logging.warning(
'Asyncio loop step took %.4fs; ideal max is %.4f',
duration, warn_time)
duration,
warn_time,
)
global _asyncio_timer # pylint: disable=invalid-name
_asyncio_timer = _ba.Timer(1.0 / 30.0,
run_cycle,
timetype=TimeType.REAL,
repeat=True)
_asyncio_timer = _ba.Timer(
1.0 / 30.0, run_cycle, timetype=TimeType.REAL, repeat=True
)
if bool(False):

View File

@ -50,31 +50,42 @@ def run_cpu_benchmark() -> None:
_ba.new_host_session(BenchmarkSession, benchmark_type='cpu')
def run_stress_test(playlist_type: str = 'Random',
playlist_name: str = '__default__',
player_count: int = 8,
round_duration: int = 30) -> None:
def run_stress_test(
playlist_type: str = 'Random',
playlist_name: str = '__default__',
player_count: int = 8,
round_duration: int = 30,
) -> None:
"""Run a stress test."""
from ba import modutils
from ba._general import Call
from ba._generated.enums import TimeType
_ba.screenmessage(
'Beginning stress test.. use '
"'End Test' to stop testing.",
color=(1, 1, 0))
"Beginning stress test.. use 'End Test' to stop testing.",
color=(1, 1, 0),
)
with _ba.Context('ui'):
start_stress_test({
'playlist_type': playlist_type,
'playlist_name': playlist_name,
'player_count': player_count,
'round_duration': round_duration
})
_ba.timer(7.0,
Call(_ba.screenmessage,
('stats will be written to ' +
modutils.get_human_readable_user_scripts_path() +
'/stress_test_stats.csv')),
timetype=TimeType.REAL)
start_stress_test(
{
'playlist_type': playlist_type,
'playlist_name': playlist_name,
'player_count': player_count,
'round_duration': round_duration,
}
)
_ba.timer(
7.0,
Call(
_ba.screenmessage,
(
'stats will be written to '
+ modutils.get_human_readable_user_scripts_path()
+ '/stress_test_stats.csv'
),
),
timetype=TimeType.REAL,
)
def stop_stress_test() -> None:
@ -94,6 +105,7 @@ def start_stress_test(args: dict[str, Any]) -> None:
from ba._dualteamsession import DualTeamSession
from ba._freeforallsession import FreeForAllSession
from ba._generated.enums import TimeType, TimeFormat
appconfig = _ba.app.config
playlist_type = args['playlist_type']
if playlist_type == 'Random':
@ -101,33 +113,42 @@ def start_stress_test(args: dict[str, Any]) -> None:
playlist_type = 'Teams'
else:
playlist_type = 'Free-For-All'
_ba.screenmessage('Running Stress Test (listType="' + playlist_type +
'", listName="' + args['playlist_name'] + '")...')
_ba.screenmessage(
'Running Stress Test (listType="'
+ playlist_type
+ '", listName="'
+ args['playlist_name']
+ '")...'
)
if playlist_type == 'Teams':
appconfig['Team Tournament Playlist Selection'] = args['playlist_name']
appconfig['Team Tournament Playlist Randomize'] = 1
_ba.timer(1.0,
Call(_ba.pushcall, Call(_ba.new_host_session,
DualTeamSession)),
timetype=TimeType.REAL)
_ba.timer(
1.0,
Call(_ba.pushcall, Call(_ba.new_host_session, DualTeamSession)),
timetype=TimeType.REAL,
)
else:
appconfig['Free-for-All Playlist Selection'] = args['playlist_name']
appconfig['Free-for-All Playlist Randomize'] = 1
_ba.timer(1.0,
Call(_ba.pushcall,
Call(_ba.new_host_session, FreeForAllSession)),
timetype=TimeType.REAL)
_ba.timer(
1.0,
Call(_ba.pushcall, Call(_ba.new_host_session, FreeForAllSession)),
timetype=TimeType.REAL,
)
_ba.set_stress_testing(True, args['player_count'])
_ba.app.stress_test_reset_timer = _ba.Timer(
args['round_duration'] * 1000,
Call(_reset_stress_test, args),
timetype=TimeType.REAL,
timeformat=TimeFormat.MILLISECONDS)
timeformat=TimeFormat.MILLISECONDS,
)
def _reset_stress_test(args: dict[str, Any]) -> None:
from ba._general import Call
from ba._generated.enums import TimeType
_ba.set_stress_testing(False, args['player_count'])
_ba.screenmessage('Resetting stress test...')
session = _ba.get_foreground_host_session()
@ -146,27 +167,32 @@ def run_media_reload_benchmark() -> None:
"""Kick off a benchmark to test media reloading speeds."""
from ba._general import Call
from ba._generated.enums import TimeType
_ba.reload_media()
_ba.show_progress_bar()
def delay_add(start_time: float) -> None:
def doit(start_time_2: float) -> None:
_ba.screenmessage(
_ba.app.lang.get_resource(
'debugWindow.totalReloadTimeText').replace(
'${TIME}',
str(_ba.time(TimeType.REAL) - start_time_2)))
'debugWindow.totalReloadTimeText'
).replace(
'${TIME}', str(_ba.time(TimeType.REAL) - start_time_2)
)
)
_ba.print_load_info()
if _ba.app.config.resolve('Texture Quality') != 'High':
_ba.screenmessage(_ba.app.lang.get_resource(
'debugWindow.reloadBenchmarkBestResultsText'),
color=(1, 1, 0))
_ba.screenmessage(
_ba.app.lang.get_resource(
'debugWindow.reloadBenchmarkBestResultsText'
),
color=(1, 1, 0),
)
_ba.add_clean_frame_callback(Call(doit, start_time))
# The reload starts (should add a completion callback to the
# reload func to fix this).
_ba.timer(0.05,
Call(delay_add, _ba.time(TimeType.REAL)),
timetype=TimeType.REAL)
_ba.timer(
0.05, Call(delay_add, _ba.time(TimeType.REAL)), timetype=TimeType.REAL
)

View File

@ -33,11 +33,13 @@ def bootstrap() -> None:
# Python's stdout/stderr into it. Then we can at least debug problems
# on systems where native stdout/stderr is not easily accessible
# such as Android.
log_handler = setup_logging(log_path=None,
level=LogLevel.DEBUG,
suppress_non_root_debug=True,
log_stdout_stderr=True,
cache_size_limit=1024 * 1024)
log_handler = setup_logging(
log_path=None,
level=LogLevel.DEBUG,
suppress_non_root_debug=True,
log_stdout_stderr=True,
cache_size_limit=1024 * 1024,
)
log_handler.add_callback(_on_log)
@ -53,7 +55,8 @@ def bootstrap() -> None:
f' Ballistica build {expected_build}.\n'
f' You are running build {running_build}.'
f' This might cause the app to error or misbehave.',
file=sys.stderr)
file=sys.stderr,
)
# In bootstrap_monolithic.py we told Python not to handle SIGINT itself
# (because that must be done in the main thread). Now we finish the
@ -69,7 +72,8 @@ def bootstrap() -> None:
print(
'ERROR: Python\'s UTF-8 mode is not set.'
' This will likely result in errors.',
file=sys.stderr)
file=sys.stderr,
)
debug_build = env['debug_build']
@ -78,20 +82,24 @@ def bootstrap() -> None:
print(
f'WARNING: Mismatch in debug_build {debug_build}'
f' and sys.flags.dev_mode {sys.flags.dev_mode}',
file=sys.stderr)
file=sys.stderr,
)
# In embedded situations (when we're providing our own Python) let's
# also provide our own root certs so ssl works. We can consider overriding
# this in particular embedded cases if we can verify that system certs
# are working.
# (We also allow forcing this via an env var if the user desires)
if (_ba.contains_python_dist()
or os.environ.get('BA_USE_BUNDLED_ROOT_CERTS') == '1'):
if (
_ba.contains_python_dist()
or os.environ.get('BA_USE_BUNDLED_ROOT_CERTS') == '1'
):
import certifi
# Let both OpenSSL and requests (if present) know to use this.
os.environ['SSL_CERT_FILE'] = os.environ['REQUESTS_CA_BUNDLE'] = (
certifi.where())
os.environ['SSL_CERT_FILE'] = os.environ[
'REQUESTS_CA_BUNDLE'
] = certifi.where()
# On Windows I'm seeing the following error creating asyncio loops in
# background threads with the default proactor setup:
@ -104,6 +112,7 @@ def bootstrap() -> None:
# to default to selector in that case?..
if sys.platform == 'win32':
import asyncio
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
# pylint: disable=c-extension-no-member
@ -122,6 +131,7 @@ def bootstrap() -> None:
# Now spin up our App instance and store it on both _ba and ba.
from ba._app import App
import ba
_ba.app = ba.app = App()
_ba.app.log_handler = log_handler
@ -138,6 +148,7 @@ class _CustomHelper:
# (but then things mostly work). Let's get the ugly error out
# of the way explicitly.
import sysconfig
try:
# This errors once but seems to run cleanly after, so let's
# get the error out of the way.
@ -146,13 +157,16 @@ class _CustomHelper:
pass
import pydoc
# Disable pager and interactive help since neither works well
# with our funky multi-threaded setup or in-game/cloud consoles.
# Let's just do simple text dumps.
pydoc.pager = pydoc.plainpager
if not args and not kwds:
print('Interactive help is not available in this environment.\n'
'Type help(object) for help about object.')
print(
'Interactive help is not available in this environment.\n'
'Type help(object) for help about object.'
)
return None
return pydoc.help(*args, **kwds)
@ -170,6 +184,8 @@ def _on_log(entry: LogEntry) -> None:
# We also want to feed some logs to the old V1-cloud-log system.
# Let's go with anything warning or higher as well as the stdout/stderr
# log messages that ba.app.log_handler creates for us.
if entry.level.value >= LogLevel.WARNING.value or entry.name in ('stdout',
'stderr'):
if entry.level.value >= LogLevel.WARNING.value or entry.name in (
'stdout',
'stderr',
):
_ba.v1_cloud_log(entry.message)

View File

@ -28,10 +28,12 @@ class Campaign:
Category: **App Classes**
"""
def __init__(self,
name: str,
sequential: bool = True,
levels: list[ba.Level] | None = None):
def __init__(
self,
name: str,
sequential: bool = True,
levels: list[ba.Level] | None = None,
):
self._name = name
self._sequential = sequential
self._levels: list[ba.Level] = []
@ -67,12 +69,13 @@ class Campaign:
def getlevel(self, name: str) -> ba.Level:
"""Return a contained ba.Level by name."""
from ba import _error
for level in self._levels:
if level.name == name:
return level
raise _error.NotFoundError("Level '" + name +
"' not found in campaign '" + self.name +
"'")
raise _error.NotFoundError(
"Level '" + name + "' not found in campaign '" + self.name + "'"
)
def reset(self) -> None:
"""Reset state for the Campaign."""
@ -91,9 +94,9 @@ class Campaign:
@property
def configdict(self) -> dict[str, Any]:
"""Return the live config dict for this campaign."""
val: dict[str, Any] = (_ba.app.config.setdefault('Campaigns',
{}).setdefault(
self._name, {}))
val: dict[str, Any] = _ba.app.config.setdefault(
'Campaigns', {}
).setdefault(self._name, {})
assert isinstance(val, dict)
return val
@ -121,92 +124,132 @@ def init_campaigns() -> None:
Campaign(
'Easy',
levels=[
Level('Onslaught Training',
gametype=OnslaughtGame,
settings={'preset': 'training_easy'},
preview_texture_name='doomShroomPreview'),
Level('Rookie Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'rookie_easy'},
preview_texture_name='courtyardPreview'),
Level('Rookie Football',
gametype=FootballCoopGame,
settings={'preset': 'rookie_easy'},
preview_texture_name='footballStadiumPreview'),
Level('Pro Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'pro_easy'},
preview_texture_name='doomShroomPreview'),
Level('Pro Football',
gametype=FootballCoopGame,
settings={'preset': 'pro_easy'},
preview_texture_name='footballStadiumPreview'),
Level('Pro Runaround',
gametype=RunaroundGame,
settings={'preset': 'pro_easy'},
preview_texture_name='towerDPreview'),
Level('Uber Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'uber_easy'},
preview_texture_name='courtyardPreview'),
Level('Uber Football',
gametype=FootballCoopGame,
settings={'preset': 'uber_easy'},
preview_texture_name='footballStadiumPreview'),
Level('Uber Runaround',
gametype=RunaroundGame,
settings={'preset': 'uber_easy'},
preview_texture_name='towerDPreview')
Level(
'Onslaught Training',
gametype=OnslaughtGame,
settings={'preset': 'training_easy'},
preview_texture_name='doomShroomPreview',
),
Level(
'Rookie Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'rookie_easy'},
preview_texture_name='courtyardPreview',
),
Level(
'Rookie Football',
gametype=FootballCoopGame,
settings={'preset': 'rookie_easy'},
preview_texture_name='footballStadiumPreview',
),
Level(
'Pro Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'pro_easy'},
preview_texture_name='doomShroomPreview',
),
Level(
'Pro Football',
gametype=FootballCoopGame,
settings={'preset': 'pro_easy'},
preview_texture_name='footballStadiumPreview',
),
Level(
'Pro Runaround',
gametype=RunaroundGame,
settings={'preset': 'pro_easy'},
preview_texture_name='towerDPreview',
),
Level(
'Uber Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'uber_easy'},
preview_texture_name='courtyardPreview',
),
Level(
'Uber Football',
gametype=FootballCoopGame,
settings={'preset': 'uber_easy'},
preview_texture_name='footballStadiumPreview',
),
Level(
'Uber Runaround',
gametype=RunaroundGame,
settings={'preset': 'uber_easy'},
preview_texture_name='towerDPreview',
),
],
))
)
)
# "hard" mode
register_campaign(
Campaign(
'Default',
levels=[
Level('Onslaught Training',
gametype=OnslaughtGame,
settings={'preset': 'training'},
preview_texture_name='doomShroomPreview'),
Level('Rookie Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'rookie'},
preview_texture_name='courtyardPreview'),
Level('Rookie Football',
gametype=FootballCoopGame,
settings={'preset': 'rookie'},
preview_texture_name='footballStadiumPreview'),
Level('Pro Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'pro'},
preview_texture_name='doomShroomPreview'),
Level('Pro Football',
gametype=FootballCoopGame,
settings={'preset': 'pro'},
preview_texture_name='footballStadiumPreview'),
Level('Pro Runaround',
gametype=RunaroundGame,
settings={'preset': 'pro'},
preview_texture_name='towerDPreview'),
Level('Uber Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'uber'},
preview_texture_name='courtyardPreview'),
Level('Uber Football',
gametype=FootballCoopGame,
settings={'preset': 'uber'},
preview_texture_name='footballStadiumPreview'),
Level('Uber Runaround',
gametype=RunaroundGame,
settings={'preset': 'uber'},
preview_texture_name='towerDPreview'),
Level('The Last Stand',
gametype=TheLastStandGame,
settings={},
preview_texture_name='rampagePreview')
Level(
'Onslaught Training',
gametype=OnslaughtGame,
settings={'preset': 'training'},
preview_texture_name='doomShroomPreview',
),
Level(
'Rookie Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'rookie'},
preview_texture_name='courtyardPreview',
),
Level(
'Rookie Football',
gametype=FootballCoopGame,
settings={'preset': 'rookie'},
preview_texture_name='footballStadiumPreview',
),
Level(
'Pro Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'pro'},
preview_texture_name='doomShroomPreview',
),
Level(
'Pro Football',
gametype=FootballCoopGame,
settings={'preset': 'pro'},
preview_texture_name='footballStadiumPreview',
),
Level(
'Pro Runaround',
gametype=RunaroundGame,
settings={'preset': 'pro'},
preview_texture_name='towerDPreview',
),
Level(
'Uber Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'uber'},
preview_texture_name='courtyardPreview',
),
Level(
'Uber Football',
gametype=FootballCoopGame,
settings={'preset': 'uber'},
preview_texture_name='footballStadiumPreview',
),
Level(
'Uber Runaround',
gametype=RunaroundGame,
settings={'preset': 'uber'},
preview_texture_name='towerDPreview',
),
Level(
'The Last Stand',
gametype=TheLastStandGame,
settings={},
preview_texture_name='rampagePreview',
),
],
))
)
)
# challenges: our 'official' random extra co-op levels
register_campaign(
@ -214,121 +257,153 @@ def init_campaigns() -> None:
'Challenges',
sequential=False,
levels=[
Level('Infinite Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'endless'},
preview_texture_name='doomShroomPreview'),
Level('Infinite Runaround',
gametype=RunaroundGame,
settings={'preset': 'endless'},
preview_texture_name='towerDPreview'),
Level('Race',
displayname='${GAME}',
gametype=RaceGame,
settings={
'map': 'Big G',
'Laps': 3,
'Bomb Spawning': 0
},
preview_texture_name='bigGPreview'),
Level('Pro Race',
displayname='Pro ${GAME}',
gametype=RaceGame,
settings={
'map': 'Big G',
'Laps': 3,
'Bomb Spawning': 1000
},
preview_texture_name='bigGPreview'),
Level('Lake Frigid Race',
displayname='${GAME}',
gametype=RaceGame,
settings={
'map': 'Lake Frigid',
'Laps': 6,
'Mine Spawning': 2000,
'Bomb Spawning': 0
},
preview_texture_name='lakeFrigidPreview'),
Level('Football',
displayname='${GAME}',
gametype=FootballCoopGame,
settings={'preset': 'tournament'},
preview_texture_name='footballStadiumPreview'),
Level('Pro Football',
displayname='Pro ${GAME}',
gametype=FootballCoopGame,
settings={'preset': 'tournament_pro'},
preview_texture_name='footballStadiumPreview'),
Level('Runaround',
displayname='${GAME}',
gametype=RunaroundGame,
settings={'preset': 'tournament'},
preview_texture_name='towerDPreview'),
Level('Uber Runaround',
displayname='Uber ${GAME}',
gametype=RunaroundGame,
settings={'preset': 'tournament_uber'},
preview_texture_name='towerDPreview'),
Level('The Last Stand',
displayname='${GAME}',
gametype=TheLastStandGame,
settings={'preset': 'tournament'},
preview_texture_name='rampagePreview'),
Level('Tournament Infinite Onslaught',
displayname='Infinite Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'endless_tournament'},
preview_texture_name='doomShroomPreview'),
Level('Tournament Infinite Runaround',
displayname='Infinite Runaround',
gametype=RunaroundGame,
settings={'preset': 'endless_tournament'},
preview_texture_name='towerDPreview'),
Level('Target Practice',
displayname='Pro ${GAME}',
gametype=TargetPracticeGame,
settings={},
preview_texture_name='doomShroomPreview'),
Level('Target Practice B',
displayname='${GAME}',
gametype=TargetPracticeGame,
settings={
'Target Count': 2,
'Enable Impact Bombs': False,
'Enable Triple Bombs': False
},
preview_texture_name='doomShroomPreview'),
Level('Meteor Shower',
displayname='${GAME}',
gametype=MeteorShowerGame,
settings={},
preview_texture_name='rampagePreview'),
Level('Epic Meteor Shower',
displayname='${GAME}',
gametype=MeteorShowerGame,
settings={'Epic Mode': True},
preview_texture_name='rampagePreview'),
Level('Easter Egg Hunt',
displayname='${GAME}',
gametype=EasterEggHuntGame,
settings={},
preview_texture_name='towerDPreview'),
Level('Pro Easter Egg Hunt',
displayname='Pro ${GAME}',
gametype=EasterEggHuntGame,
settings={'Pro Mode': True},
preview_texture_name='towerDPreview'),
Level(
'Infinite Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'endless'},
preview_texture_name='doomShroomPreview',
),
Level(
'Infinite Runaround',
gametype=RunaroundGame,
settings={'preset': 'endless'},
preview_texture_name='towerDPreview',
),
Level(
'Race',
displayname='${GAME}',
gametype=RaceGame,
settings={'map': 'Big G', 'Laps': 3, 'Bomb Spawning': 0},
preview_texture_name='bigGPreview',
),
Level(
'Pro Race',
displayname='Pro ${GAME}',
gametype=RaceGame,
settings={'map': 'Big G', 'Laps': 3, 'Bomb Spawning': 1000},
preview_texture_name='bigGPreview',
),
Level(
'Lake Frigid Race',
displayname='${GAME}',
gametype=RaceGame,
settings={
'map': 'Lake Frigid',
'Laps': 6,
'Mine Spawning': 2000,
'Bomb Spawning': 0,
},
preview_texture_name='lakeFrigidPreview',
),
Level(
'Football',
displayname='${GAME}',
gametype=FootballCoopGame,
settings={'preset': 'tournament'},
preview_texture_name='footballStadiumPreview',
),
Level(
'Pro Football',
displayname='Pro ${GAME}',
gametype=FootballCoopGame,
settings={'preset': 'tournament_pro'},
preview_texture_name='footballStadiumPreview',
),
Level(
'Runaround',
displayname='${GAME}',
gametype=RunaroundGame,
settings={'preset': 'tournament'},
preview_texture_name='towerDPreview',
),
Level(
'Uber Runaround',
displayname='Uber ${GAME}',
gametype=RunaroundGame,
settings={'preset': 'tournament_uber'},
preview_texture_name='towerDPreview',
),
Level(
'The Last Stand',
displayname='${GAME}',
gametype=TheLastStandGame,
settings={'preset': 'tournament'},
preview_texture_name='rampagePreview',
),
Level(
'Tournament Infinite Onslaught',
displayname='Infinite Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'endless_tournament'},
preview_texture_name='doomShroomPreview',
),
Level(
'Tournament Infinite Runaround',
displayname='Infinite Runaround',
gametype=RunaroundGame,
settings={'preset': 'endless_tournament'},
preview_texture_name='towerDPreview',
),
Level(
'Target Practice',
displayname='Pro ${GAME}',
gametype=TargetPracticeGame,
settings={},
preview_texture_name='doomShroomPreview',
),
Level(
'Target Practice B',
displayname='${GAME}',
gametype=TargetPracticeGame,
settings={
'Target Count': 2,
'Enable Impact Bombs': False,
'Enable Triple Bombs': False,
},
preview_texture_name='doomShroomPreview',
),
Level(
'Meteor Shower',
displayname='${GAME}',
gametype=MeteorShowerGame,
settings={},
preview_texture_name='rampagePreview',
),
Level(
'Epic Meteor Shower',
displayname='${GAME}',
gametype=MeteorShowerGame,
settings={'Epic Mode': True},
preview_texture_name='rampagePreview',
),
Level(
'Easter Egg Hunt',
displayname='${GAME}',
gametype=EasterEggHuntGame,
settings={},
preview_texture_name='towerDPreview',
),
Level(
'Pro Easter Egg Hunt',
displayname='Pro ${GAME}',
gametype=EasterEggHuntGame,
settings={'Pro Mode': True},
preview_texture_name='towerDPreview',
),
Level(
name='Ninja Fight', # (unique id not seen by player)
displayname='${GAME}', # (readable name seen by player)
gametype=NinjaFightGame,
settings={'preset': 'regular'},
preview_texture_name='courtyardPreview'),
Level(name='Pro Ninja Fight',
displayname='Pro ${GAME}',
gametype=NinjaFightGame,
settings={'preset': 'pro'},
preview_texture_name='courtyardPreview')
preview_texture_name='courtyardPreview',
),
Level(
name='Pro Ninja Fight',
displayname='Pro ${GAME}',
gametype=NinjaFightGame,
settings={'preset': 'pro'},
preview_texture_name='courtyardPreview',
),
],
))
)
)

View File

@ -35,7 +35,8 @@ class CloudSubsystem:
self,
msg: bacommon.cloud.LoginProxyRequestMessage,
on_response: Callable[
[bacommon.cloud.LoginProxyRequestResponse | Exception], None],
[bacommon.cloud.LoginProxyRequestResponse | Exception], None
],
) -> None:
...
@ -44,7 +45,8 @@ class CloudSubsystem:
self,
msg: bacommon.cloud.LoginProxyStateQueryMessage,
on_response: Callable[
[bacommon.cloud.LoginProxyStateQueryResponse | Exception], None],
[bacommon.cloud.LoginProxyStateQueryResponse | Exception], None
],
) -> None:
...
@ -75,11 +77,15 @@ class CloudSubsystem:
and passed either the response or the error that occurred.
"""
from ba._general import Call
del msg # Unused.
_ba.pushcall(
Call(on_response,
RuntimeError('Cloud functionality is not available.')))
Call(
on_response,
RuntimeError('Cloud functionality is not available.'),
)
)
@overload
def send_message(
@ -89,8 +95,8 @@ class CloudSubsystem:
@overload
def send_message(
self,
msg: bacommon.cloud.TestMessage) -> bacommon.cloud.TestResponse:
self, msg: bacommon.cloud.TestMessage
) -> bacommon.cloud.TestResponse:
...
def send_message(self, msg: Message) -> Response | None:
@ -107,6 +113,7 @@ def cloud_console_exec(code: str) -> None:
import logging
import __main__
from ba._generated.enums import TimeType
try:
# First try it as eval.
@ -118,7 +125,8 @@ def cloud_console_exec(code: str) -> None:
# hmm; when we can't compile it as eval will we always get
# syntax error?
logging.exception(
'unexpected error compiling code for cloud-console eval.')
'unexpected error compiling code for cloud-console eval.'
)
evalcode = None
if evalcode is not None:
# pylint: disable=eval-used
@ -135,6 +143,7 @@ def cloud_console_exec(code: str) -> None:
exec(execcode, vars(__main__), vars(__main__))
except Exception:
import traceback
apptime = _ba.time(TimeType.REAL)
print(f'Exec error at time {apptime:.2f}.', file=sys.stderr)
traceback.print_exc()

View File

@ -33,6 +33,7 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
from ba._coopsession import CoopSession
return issubclass(sessiontype, CoopSession)
def __init__(self, settings: dict):
@ -55,12 +56,14 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
# Preload achievement images in case we get some.
_ba.timer(2.0, WeakCall(self._preload_achievements))
def _show_standard_scores_to_beat_ui(self,
scores: list[dict[str, Any]]) -> None:
def _show_standard_scores_to_beat_ui(
self, scores: list[dict[str, Any]]
) -> None:
from efro.util import asserttype
from ba._gameutils import timestring, animate
from ba._nodeactor import NodeActor
from ba._generated.enums import TimeFormat
display_type = self.get_score_type()
if scores is not None:
@ -70,26 +73,35 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
# Now make a display for the most recent challenge.
for score in scores:
if score['type'] == 'score_challenge':
tval = (score['player'] + ': ' + timestring(
int(score['value']) * 10,
timeformat=TimeFormat.MILLISECONDS).evaluate()
if display_type == 'time' else str(score['value']))
tval = (
score['player']
+ ': '
+ timestring(
int(score['value']) * 10,
timeformat=TimeFormat.MILLISECONDS,
).evaluate()
if display_type == 'time'
else str(score['value'])
)
hattach = 'center' if display_type == 'time' else 'left'
halign = 'center' if display_type == 'time' else 'left'
pos = (20, -70) if display_type == 'time' else (20, -130)
txt = NodeActor(
_ba.newnode('text',
attrs={
'v_attach': 'top',
'h_attach': hattach,
'h_align': halign,
'color': (0.7, 0.4, 1, 1),
'shadow': 0.5,
'flatness': 1.0,
'position': pos,
'scale': 0.6,
'text': tval
})).autoretain()
_ba.newnode(
'text',
attrs={
'v_attach': 'top',
'h_attach': hattach,
'h_align': halign,
'color': (0.7, 0.4, 1, 1),
'shadow': 0.5,
'flatness': 1.0,
'position': pos,
'scale': 0.6,
'text': tval,
},
)
).autoretain()
assert txt.node is not None
animate(txt.node, 'scale', {1.0: 0.0, 1.1: 0.7, 1.2: 0.6})
break
@ -104,8 +116,7 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
def _get_coop_level_name(self) -> str:
assert self.session.campaign is not None
return self.session.campaign.name + ':' + str(
self.settings_raw['name'])
return self.session.campaign.name + ':' + str(self.settings_raw['name'])
def celebrate(self, duration: float) -> None:
"""Tells all existing player-controlled characters to celebrate.
@ -115,13 +126,15 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
duration is given in seconds.
"""
from ba._messages import CelebrateMessage
for player in self.players:
if player.actor:
player.actor.handlemessage(CelebrateMessage(duration))
def _preload_achievements(self) -> None:
achievements = _ba.app.ach.achievements_for_coop_level(
self._get_coop_level_name())
self._get_coop_level_name()
)
for ach in achievements:
ach.get_icon_texture(True)
@ -129,43 +142,52 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
# pylint: disable=cyclic-import
from ba._language import Lstr
from bastd.actor.text import Text
ts_h_offs = 30
v_offs = -200
achievements = [
a for a in _ba.app.ach.achievements_for_coop_level(
self._get_coop_level_name()) if not a.complete
a
for a in _ba.app.ach.achievements_for_coop_level(
self._get_coop_level_name()
)
if not a.complete
]
vrmode = _ba.app.vr_mode
if achievements:
Text(Lstr(resource='achievementsRemainingText'),
host_only=True,
position=(ts_h_offs - 10 + 40, v_offs - 10),
transition=Text.Transition.FADE_IN,
scale=1.1,
h_attach=Text.HAttach.LEFT,
v_attach=Text.VAttach.TOP,
color=(1, 1, 1.2, 1) if vrmode else (0.8, 0.8, 1.0, 1.0),
flatness=1.0 if vrmode else 0.6,
shadow=1.0 if vrmode else 0.5,
transition_delay=0.0,
transition_out_delay=1.3
if self.slow_motion else 4.0).autoretain()
Text(
Lstr(resource='achievementsRemainingText'),
host_only=True,
position=(ts_h_offs - 10 + 40, v_offs - 10),
transition=Text.Transition.FADE_IN,
scale=1.1,
h_attach=Text.HAttach.LEFT,
v_attach=Text.VAttach.TOP,
color=(1, 1, 1.2, 1) if vrmode else (0.8, 0.8, 1.0, 1.0),
flatness=1.0 if vrmode else 0.6,
shadow=1.0 if vrmode else 0.5,
transition_delay=0.0,
transition_out_delay=1.3 if self.slow_motion else 4.0,
).autoretain()
hval = 70
vval = -50
tdelay = 0.0
for ach in achievements:
tdelay += 0.05
ach.create_display(hval + 40,
vval + v_offs,
0 + tdelay,
outdelay=1.3 if self.slow_motion else 4.0,
style='in_game')
ach.create_display(
hval + 40,
vval + v_offs,
0 + tdelay,
outdelay=1.3 if self.slow_motion else 4.0,
style='in_game',
)
vval -= 55
def spawn_player_spaz(self,
player: PlayerType,
position: Sequence[float] = (0.0, 0.0, 0.0),
angle: float | None = None) -> PlayerSpaz:
def spawn_player_spaz(
self,
player: PlayerType,
position: Sequence[float] = (0.0, 0.0, 0.0),
angle: float | None = None,
) -> PlayerSpaz:
"""Spawn and wire up a standard player spaz."""
spaz = super().spawn_player_spaz(player, position, angle)
@ -173,9 +195,9 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
spaz.play_big_death_sound = True
return spaz
def _award_achievement(self,
achievement_name: str,
sound: bool = True) -> None:
def _award_achievement(
self, achievement_name: str, sound: bool = True
) -> None:
"""Award an achievement.
Returns True if a banner will be shown;
@ -196,6 +218,7 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
return
except Exception:
from ba._error import print_exception
print_exception()
# If we haven't awarded this one, check to see if we've got it.
@ -208,10 +231,9 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
_internal.report_achievement(achievement_name)
# ...and to our account.
_internal.add_transaction({
'type': 'ACHIEVEMENT',
'name': achievement_name
})
_internal.add_transaction(
{'type': 'ACHIEVEMENT', 'name': achievement_name}
)
# Now bring up a celebration banner.
ach.announce_completion(sound=sound)
@ -219,14 +241,17 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
def fade_to_red(self) -> None:
"""Fade the screen to red; (such as when the good guys have lost)."""
from ba import _gameutils
c_existing = self.globalsnode.tint
cnode = _ba.newnode('combine',
attrs={
'input0': c_existing[0],
'input1': c_existing[1],
'input2': c_existing[2],
'size': 3
})
cnode = _ba.newnode(
'combine',
attrs={
'input0': c_existing[0],
'input1': c_existing[1],
'input2': c_existing[2],
'size': 3,
},
)
_gameutils.animate(cnode, 'input1', {0: c_existing[1], 2.0: 0})
_gameutils.animate(cnode, 'input2', {0: c_existing[2], 2.0: 0})
cnode.connectattr('output', self.globalsnode, 'tint')
@ -235,7 +260,8 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
"""Set up a beeping noise to play when any players are near death."""
self._life_warning_beep = None
self._life_warning_beep_timer = _ba.Timer(
1.0, WeakCall(self._update_life_warning), repeat=True)
1.0, WeakCall(self._update_life_warning), repeat=True
)
def _update_life_warning(self) -> None:
# Beep continuously if anyone is close to death.
@ -249,12 +275,16 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
break
if should_beep and self._life_warning_beep is None:
from ba._nodeactor import NodeActor
self._life_warning_beep = NodeActor(
_ba.newnode('sound',
attrs={
'sound': self._warn_beeps_sound,
'positional': False,
'loop': True
}))
_ba.newnode(
'sound',
attrs={
'sound': self._warn_beeps_sound,
'positional': False,
'loop': True,
},
)
)
if self._life_warning_beep is not None and not should_beep:
self._life_warning_beep = None

View File

@ -60,15 +60,18 @@ class CoopSession(Session):
# print('FIXME: COOP SESSION WOULD CALC DEPS.')
depsets: Sequence[ba.DependencySet] = []
super().__init__(depsets,
team_names=TEAM_NAMES,
team_colors=TEAM_COLORS,
min_players=min_players,
max_players=max_players)
super().__init__(
depsets,
team_names=TEAM_NAMES,
team_colors=TEAM_COLORS,
min_players=min_players,
max_players=max_players,
)
# Tournament-ID if we correspond to a co-op tournament (otherwise None)
self.tournament_id: str | None = (
app.coop_session_args.get('tournament_id'))
self.tournament_id: str | None = app.coop_session_args.get(
'tournament_id'
)
self.campaign = getcampaign(app.coop_session_args['campaign'])
self.campaign_level_name: str = app.coop_session_args['level']
@ -151,10 +154,13 @@ class CoopSession(Session):
# Special case:
# If our current level is 'onslaught training', instantiate
# our tutorial so its ready to go. (if we haven't run it yet).
if (self.campaign_level_name == 'Onslaught Training'
and self._tutorial_activity is None
and not self._ran_tutorial_activity):
if (
self.campaign_level_name == 'Onslaught Training'
and self._tutorial_activity is None
and not self._ran_tutorial_activity
):
from bastd.tutorial import TutorialActivity
self._tutorial_activity = _ba.newactivity(TutorialActivity)
def get_custom_menu_entries(self) -> list[dict[str, Any]]:
@ -162,6 +168,7 @@ class CoopSession(Session):
def on_player_leave(self, sessionplayer: ba.SessionPlayer) -> None:
from ba._general import WeakCall
super().on_player_leave(sessionplayer)
_ba.timer(2.0, WeakCall(self._handle_empty_activity))
@ -170,6 +177,7 @@ class CoopSession(Session):
"""Handle cases where all players have left the current activity."""
from ba._gameactivity import GameActivity
activity = self.getactivity()
if activity is None:
return # Hmm what should we do in this case?
@ -204,17 +212,21 @@ class CoopSession(Session):
activity.end_game()
def _on_tournament_restart_menu_press(
self, resume_callback: Callable[[], Any]) -> None:
self, resume_callback: Callable[[], Any]
) -> None:
# pylint: disable=cyclic-import
from bastd.ui.tournamententry import TournamentEntryWindow
from ba._gameactivity import GameActivity
activity = self.getactivity()
if activity is not None and not activity.expired:
assert self.tournament_id is not None
assert isinstance(activity, GameActivity)
TournamentEntryWindow(tournament_id=self.tournament_id,
tournament_activity=activity,
on_close_call=resume_callback)
TournamentEntryWindow(
tournament_id=self.tournament_id,
tournament_activity=activity,
on_close_call=resume_callback,
)
def restart(self) -> None:
"""Restart the current game activity."""
@ -277,8 +289,9 @@ class CoopSession(Session):
# If we're in a between-round activity or a restart-activity,
# hop into a round.
if (isinstance(activity,
(JoinActivity, CoopScoreScreen, TransitionActivity))):
if isinstance(
activity, (JoinActivity, CoopScoreScreen, TransitionActivity)
):
if outcome == 'next_level':
if self._next_game_instance is None:
@ -292,9 +305,11 @@ class CoopSession(Session):
# Special case: if we're coming from a joining-activity
# and will be going into onslaught-training, show the
# tutorial first.
if (isinstance(activity, JoinActivity)
and self.campaign_level_name == 'Onslaught Training'
and not (app.demo_mode or app.arcade_mode)):
if (
isinstance(activity, JoinActivity)
and self.campaign_level_name == 'Onslaught Training'
and not (app.demo_mode or app.arcade_mode)
):
if self._tutorial_activity is None:
raise RuntimeError('Tutorial not preloaded properly.')
self.setactivity(self._tutorial_activity)
@ -319,20 +334,22 @@ class CoopSession(Session):
if not (app.demo_mode or app.arcade_mode):
if self.tournament_id is not None:
self._custom_menu_ui = [{
'label':
Lstr(resource='restartText'),
'resume_on_call':
False,
'call':
WeakCall(self._on_tournament_restart_menu_press
)
}]
self._custom_menu_ui = [
{
'label': Lstr(resource='restartText'),
'resume_on_call': False,
'call': WeakCall(
self._on_tournament_restart_menu_press
),
}
]
else:
self._custom_menu_ui = [{
'label': Lstr(resource='restartText'),
'call': WeakCall(self.restart)
}]
self._custom_menu_ui = [
{
'label': Lstr(resource='restartText'),
'call': WeakCall(self.restart),
}
]
# If we were in a tutorial, just pop a transition to get to the
# actual round.
@ -347,10 +364,13 @@ class CoopSession(Session):
playerinfos = results.playerinfos
score = results.get_sessionteam_score(results.sessionteams[0])
fail_message = None
score_order = ('decreasing'
if results.lower_is_better else 'increasing')
if results.scoretype in (ScoreType.SECONDS,
ScoreType.MILLISECONDS):
score_order = (
'decreasing' if results.lower_is_better else 'increasing'
)
if results.scoretype in (
ScoreType.SECONDS,
ScoreType.MILLISECONDS,
):
scoretype = 'time'
# ScoreScreen wants hundredths of a second.
@ -363,20 +383,28 @@ class CoopSession(Session):
raise RuntimeError('FIXME')
else:
if results.scoretype is not ScoreType.POINTS:
print(f'Unknown ScoreType:'
f' "{results.scoretype}"')
print(f'Unknown ScoreType:' f' "{results.scoretype}"')
scoretype = 'points'
# Old coop-game-specific results; should migrate away from these.
else:
playerinfos = results.get('playerinfos')
score = results['score'] if 'score' in results else None
fail_message = (results['fail_message']
if 'fail_message' in results else None)
score_order = (results['score_order']
if 'score_order' in results else 'increasing')
activity_score_type = (activity.get_score_type() if isinstance(
activity, CoopGameActivity) else None)
fail_message = (
results['fail_message']
if 'fail_message' in results
else None
)
score_order = (
results['score_order']
if 'score_order' in results
else 'increasing'
)
activity_score_type = (
activity.get_score_type()
if isinstance(activity, CoopGameActivity)
else None
)
assert activity_score_type is not None
scoretype = activity_score_type
@ -394,7 +422,8 @@ class CoopSession(Session):
else:
self.setactivity(
_ba.newactivity(
CoopScoreScreen, {
CoopScoreScreen,
{
'playerinfos': playerinfos,
'score': score,
'fail_message': fail_message,
@ -402,8 +431,10 @@ class CoopSession(Session):
'score_type': scoretype,
'outcome': outcome,
'campaign': self.campaign,
'level': self.campaign_level_name
}))
'level': self.campaign_level_name,
},
)
)
# No matter what, get the next 2 levels ready to go.
self._update_on_deck_game_instances()

View File

@ -5,7 +5,7 @@
from __future__ import annotations
import weakref
from typing import (Generic, TypeVar, TYPE_CHECKING)
from typing import Generic, TypeVar, TYPE_CHECKING
import _ba
@ -44,6 +44,7 @@ class Dependency(Generic[T]):
def get_hash(self) -> int:
"""Return the dependency's hash, calculating it if necessary."""
from efro.util import make_hash
if self._hash is None:
self._hash = make_hash((self.cls, self.config))
return self._hash
@ -52,10 +53,12 @@ class Dependency(Generic[T]):
if not isinstance(obj, DependencyComponent):
if obj is None:
raise TypeError(
'Dependency must be accessed through an instance.')
'Dependency must be accessed through an instance.'
)
raise TypeError(
f'Dependency cannot be added to class of type {type(obj)}'
' (class must inherit from ba.DependencyComponent).')
' (class must inherit from ba.DependencyComponent).'
)
# We expect to be instantiated from an already living
# DependencyComponent with valid dep-data in place..
@ -73,7 +76,8 @@ class Dependency(Generic[T]):
if not depset.resolved:
raise RuntimeError(
"Can't access data on an unresolved DependencySet.")
"Can't access data on an unresolved DependencySet."
)
# Look up the data in the set based on the hash for this Dependency.
assert self._hash in depset.entries
@ -157,8 +161,10 @@ class DependencyEntry:
component = self.component
assert isinstance(component, self.cls)
if component is None:
raise RuntimeError(f'Accessing DependencyComponent {self.cls} '
'in an invalid state.')
raise RuntimeError(
f'Accessing DependencyComponent {self.cls} '
'in an invalid state.'
)
return component
@ -209,6 +215,7 @@ class DependencySet(Generic[T]):
]
if missing:
from ba._error import DependencyError
raise DependencyError(missing)
self._resolved = True
@ -278,7 +285,8 @@ class DependencySet(Generic[T]):
# Grab all Dependency instances we find in the class.
subdeps = [
cls for cls in dep.cls.__dict__.values()
cls
for cls in dep.cls.__dict__.values()
if isinstance(cls, Dependency)
]
@ -395,6 +403,7 @@ def test_depset() -> None:
def doit() -> None:
from ba._error import DependencyError
depset = DependencySet(Dependency(TestClass))
try:
depset.resolve()
@ -404,7 +413,8 @@ def test_depset() -> None:
print('MISSING ASSET PACKAGE', dep.config)
else:
raise RuntimeError(
f'Unknown dependency error for {dep.cls}') from exc
f'Unknown dependency error for {dep.cls}'
) from exc
except Exception as exc:
print('DependencySet resolve failed with exc type:', type(exc))
if depset.resolved:

View File

@ -33,10 +33,11 @@ class DualTeamSession(MultiTeamSession):
def _switch_to_score_screen(self, results: ba.GameResults) -> None:
# pylint: disable=cyclic-import
from bastd.activity.drawscore import DrawScoreScreenActivity
from bastd.activity.dualteamscore import (
TeamVictoryScoreScreenActivity)
from bastd.activity.dualteamscore import TeamVictoryScoreScreenActivity
from bastd.activity.multiteamvictory import (
TeamSeriesVictoryScoreScreenActivity)
TeamSeriesVictoryScoreScreenActivity,
)
winnergroups = results.winnergroups
# If everyone has the same score, call it a draw.
@ -49,9 +50,13 @@ class DualTeamSession(MultiTeamSession):
# If a team has won, show final victory screen.
if winner.customdata['score'] >= (self._series_length - 1) / 2 + 1:
self.setactivity(
_ba.newactivity(TeamSeriesVictoryScoreScreenActivity,
{'winner': winner}))
_ba.newactivity(
TeamSeriesVictoryScoreScreenActivity, {'winner': winner}
)
)
else:
self.setactivity(
_ba.newactivity(TeamVictoryScoreScreenActivity,
{'winner': winner}))
_ba.newactivity(
TeamVictoryScoreScreenActivity, {'winner': winner}
)
)

View File

@ -141,6 +141,7 @@ def print_exception(*args: Any, **keywds: Any) -> None:
one time from an exact calling location.
"""
import traceback
if keywds:
allowed_keywds = ['once']
if any(keywd not in allowed_keywds for keywd in keywds):
@ -181,6 +182,7 @@ def print_error(err_str: str, once: bool = False) -> None:
one time from an exact calling location.
"""
import traceback
try:
# If we're only printing once and already have, bail.
if once:

View File

@ -18,6 +18,7 @@ class FreeForAllSession(MultiTeamSession):
Category: **Gameplay Classes**
"""
use_teams = False
use_team_colors = False
_playlist_selection_var = 'Free-for-All Playlist Selection'
@ -55,42 +56,54 @@ class FreeForAllSession(MultiTeamSession):
from efro.util import asserttype
from bastd.activity.drawscore import DrawScoreScreenActivity
from bastd.activity.multiteamvictory import (
TeamSeriesVictoryScoreScreenActivity)
TeamSeriesVictoryScoreScreenActivity,
)
from bastd.activity.freeforallvictory import (
FreeForAllVictoryScoreScreenActivity)
FreeForAllVictoryScoreScreenActivity,
)
winners = results.winnergroups
# If there's multiple players and everyone has the same score,
# call it a draw.
if len(self.sessionplayers) > 1 and len(winners) < 2:
self.setactivity(
_ba.newactivity(DrawScoreScreenActivity, {'results': results}))
_ba.newactivity(DrawScoreScreenActivity, {'results': results})
)
else:
# Award different point amounts based on number of players.
point_awards = self.get_ffa_point_awards()
for i, winner in enumerate(winners):
for team in winner.teams:
points = (point_awards[i] if i in point_awards else 0)
team.customdata['previous_score'] = (
team.customdata['score'])
points = point_awards[i] if i in point_awards else 0
team.customdata['previous_score'] = team.customdata['score']
team.customdata['score'] += points
series_winners = [
team for team in self.sessionteams
team
for team in self.sessionteams
if team.customdata['score'] >= self._ffa_series_length
]
series_winners.sort(
reverse=True,
key=lambda t: asserttype(t.customdata['score'], int))
if (len(series_winners) == 1
or (len(series_winners) > 1
and series_winners[0].customdata['score'] !=
series_winners[1].customdata['score'])):
key=lambda t: asserttype(t.customdata['score'], int),
)
if len(series_winners) == 1 or (
len(series_winners) > 1
and series_winners[0].customdata['score']
!= series_winners[1].customdata['score']
):
self.setactivity(
_ba.newactivity(TeamSeriesVictoryScoreScreenActivity,
{'winner': series_winners[0]}))
_ba.newactivity(
TeamSeriesVictoryScoreScreenActivity,
{'winner': series_winners[0]},
)
)
else:
self.setactivity(
_ba.newactivity(FreeForAllVictoryScoreScreenActivity,
{'results': results}))
_ba.newactivity(
FreeForAllVictoryScoreScreenActivity,
{'results': results},
)
)

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,7 @@ if TYPE_CHECKING:
@dataclass
class WinnerGroup:
"""Entry for a winning team or teams calculated by game-results."""
score: int | None
teams: Sequence[ba.SessionTeam]
@ -35,8 +36,9 @@ class GameResults:
def __init__(self) -> None:
self._game_set = False
self._scores: dict[int, tuple[weakref.ref[ba.SessionTeam],
int | None]] = {}
self._scores: dict[
int, tuple[weakref.ref[ba.SessionTeam], int | None]
] = {}
self._sessionteams: list[weakref.ref[ba.SessionTeam]] | None = None
self._playerinfos: list[ba.PlayerInfo] | None = None
self._lower_is_better: bool | None = None
@ -96,8 +98,7 @@ class GameResults:
"""Return whether there is a score for a given session-team."""
return any(s[0]() is sessionteam for s in self._scores.values())
def get_sessionteam_score_str(self,
sessionteam: ba.SessionTeam) -> ba.Lstr:
def get_sessionteam_score_str(self, sessionteam: ba.SessionTeam) -> ba.Lstr:
"""Return the score for the given session-team as an Lstr.
(properly formatted for the score type.)
@ -106,6 +107,7 @@ class GameResults:
from ba._language import Lstr
from ba._generated.enums import TimeFormat
from ba._score import ScoreType
if not self._game_set:
raise RuntimeError("Can't get team-score-str until game is set.")
for score in list(self._scores.values()):
@ -113,13 +115,15 @@ class GameResults:
if score[1] is None:
return Lstr(value='-')
if self._scoretype is ScoreType.SECONDS:
return timestring(score[1] * 1000,
centi=False,
timeformat=TimeFormat.MILLISECONDS)
return timestring(
score[1] * 1000,
centi=False,
timeformat=TimeFormat.MILLISECONDS,
)
if self._scoretype is ScoreType.MILLISECONDS:
return timestring(score[1],
centi=True,
timeformat=TimeFormat.MILLISECONDS)
return timestring(
score[1], centi=True, timeformat=TimeFormat.MILLISECONDS
)
return Lstr(value=str(score[1]))
return Lstr(value='-')
@ -174,7 +178,8 @@ class GameResults:
# Group by best scoring teams.
winners: dict[int, list[ba.SessionTeam]] = {}
scores = [
score for score in self._scores.values()
score
for score in self._scores.values()
if score[0]() is not None and score[1] is not None
]
for score in scores:
@ -183,10 +188,13 @@ class GameResults:
team = score[0]()
assert team is not None
sval.append(team)
results: list[tuple[int | None,
list[ba.SessionTeam]]] = list(winners.items())
results.sort(reverse=not self._lower_is_better,
key=lambda x: asserttype(x[0], int))
results: list[tuple[int | None, list[ba.SessionTeam]]] = list(
winners.items()
)
results.sort(
reverse=not self._lower_is_better,
key=lambda x: asserttype(x[0], int),
)
# Also group the 'None' scores.
none_sessionteams: list[ba.SessionTeam] = []

View File

@ -21,7 +21,7 @@ TROPHY_CHARS = {
'3': SpecialChar.TROPHY3,
'0a': SpecialChar.TROPHY0A,
'0b': SpecialChar.TROPHY0B,
'4': SpecialChar.TROPHY4
'4': SpecialChar.TROPHY4,
}
@ -31,6 +31,7 @@ class GameTip:
Category: **Gameplay Classes**
"""
text: str
icon: ba.Texture | None = None
sound: ba.Sound | None = None
@ -43,14 +44,16 @@ def get_trophy_string(trophy_id: str) -> str:
return '?'
def animate(node: ba.Node,
attr: str,
keys: dict[float, float],
loop: bool = False,
offset: float = 0,
timetype: ba.TimeType = TimeType.SIM,
timeformat: ba.TimeFormat = TimeFormat.SECONDS,
suppress_format_warning: bool = False) -> ba.Node:
def animate(
node: ba.Node,
attr: str,
keys: dict[float, float],
loop: bool = False,
offset: float = 0,
timetype: ba.TimeType = TimeType.SIM,
timeformat: ba.TimeFormat = TimeFormat.SECONDS,
suppress_format_warning: bool = False,
) -> ba.Node:
"""Animate values on a target ba.Node.
Category: **Gameplay Functions**
@ -76,9 +79,11 @@ def animate(node: ba.Node,
for item in items:
_ba.time_format_check(timeformat, item[0])
curve = _ba.newnode('animcurve',
owner=node,
name='Driving ' + str(node) + ' \'' + attr + '\'')
curve = _ba.newnode(
'animcurve',
owner=node,
name='Driving ' + str(node) + ' \'' + attr + '\'',
)
if timeformat is TimeFormat.SECONDS:
mult = 1000
@ -89,7 +94,8 @@ def animate(node: ba.Node,
curve.times = [int(mult * time) for time, val in items]
curve.offset = _ba.time(timeformat=TimeFormat.MILLISECONDS) + int(
mult * offset)
mult * offset
)
curve.values = [val for time, val in items]
curve.loop = loop
@ -99,9 +105,11 @@ def animate(node: ba.Node,
# get disconnected.
if not loop:
# noinspection PyUnresolvedReferences
_ba.timer(int(mult * items[-1][0]) + 1000,
curve.delete,
timeformat=TimeFormat.MILLISECONDS)
_ba.timer(
int(mult * items[-1][0]) + 1000,
curve.delete,
timeformat=TimeFormat.MILLISECONDS,
)
# Do the connects last so all our attrs are in place when we push initial
# values through.
@ -117,15 +125,17 @@ def animate(node: ba.Node,
return curve
def animate_array(node: ba.Node,
attr: str,
size: int,
keys: dict[float, Sequence[float]],
loop: bool = False,
offset: float = 0,
timetype: ba.TimeType = TimeType.SIM,
timeformat: ba.TimeFormat = TimeFormat.SECONDS,
suppress_format_warning: bool = False) -> None:
def animate_array(
node: ba.Node,
attr: str,
size: int,
keys: dict[float, Sequence[float]],
loop: bool = False,
offset: float = 0,
timetype: ba.TimeType = TimeType.SIM,
timeformat: ba.TimeFormat = TimeFormat.SECONDS,
suppress_format_warning: bool = False,
) -> None:
"""Animate an array of values on a target ba.Node.
Category: **Gameplay Functions**
@ -163,15 +173,19 @@ def animate_array(node: ba.Node,
globalsnode = _ba.getsession().sessionglobalsnode
for i in range(size):
curve = _ba.newnode('animcurve',
owner=node,
name=('Driving ' + str(node) + ' \'' + attr +
'\' member ' + str(i)))
curve = _ba.newnode(
'animcurve',
owner=node,
name=(
'Driving ' + str(node) + ' \'' + attr + '\' member ' + str(i)
),
)
globalsnode.connectattr(driver, curve, 'in')
curve.times = [int(mult * time) for time, val in items]
curve.values = [val[i] for time, val in items]
curve.offset = _ba.time(timeformat=TimeFormat.MILLISECONDS) + int(
mult * offset)
mult * offset
)
curve.loop = loop
curve.connectattr('out', combine, 'input' + str(i))
@ -180,9 +194,11 @@ def animate_array(node: ba.Node,
if not loop:
# (PyCharm seems to think item is a float, not a tuple)
# noinspection PyUnresolvedReferences
_ba.timer(int(mult * items[-1][0]) + 1000,
curve.delete,
timeformat=TimeFormat.MILLISECONDS)
_ba.timer(
int(mult * items[-1][0]) + 1000,
curve.delete,
timeformat=TimeFormat.MILLISECONDS,
)
combine.connectattr('output', node, attr)
# If we're not looping, set a timer to kill the combine once
@ -192,13 +208,16 @@ def animate_array(node: ba.Node,
if not loop:
# (PyCharm seems to think item is a float, not a tuple)
# noinspection PyUnresolvedReferences
_ba.timer(int(mult * items[-1][0]) + 1000,
combine.delete,
timeformat=TimeFormat.MILLISECONDS)
_ba.timer(
int(mult * items[-1][0]) + 1000,
combine.delete,
timeformat=TimeFormat.MILLISECONDS,
)
def show_damage_count(damage: str, position: Sequence[float],
direction: Sequence[float]) -> None:
def show_damage_count(
damage: str, position: Sequence[float], direction: Sequence[float]
) -> None:
"""Pop up a damage count at a position in space.
Category: **Gameplay Functions**
@ -210,16 +229,18 @@ def show_damage_count(damage: str, position: Sequence[float],
# (connected clients may have differing configs so they won't
# get the intended results).
do_big = app.ui.uiscale is UIScale.SMALL or app.vr_mode
txtnode = _ba.newnode('text',
attrs={
'text': damage,
'in_world': True,
'h_align': 'center',
'flatness': 1.0,
'shadow': 1.0 if do_big else 0.7,
'color': (1, 0.25, 0.25, 1),
'scale': 0.015 if do_big else 0.01
})
txtnode = _ba.newnode(
'text',
attrs={
'text': damage,
'in_world': True,
'h_align': 'center',
'flatness': 1.0,
'shadow': 1.0 if do_big else 0.7,
'color': (1, 0.25, 0.25, 1),
'scale': 0.015 if do_big else 0.01,
},
)
# Translate upward.
tcombine = _ba.newnode('combine', owner=txtnode, attrs={'size': 3})
tcombine.connectattr('output', txtnode, 'position')
@ -233,27 +254,35 @@ def show_damage_count(damage: str, position: Sequence[float],
vval *= 0.5
p_start = position[0]
p_dir = direction[0]
animate(tcombine, 'input0',
{i[0] * lifespan: p_start + p_dir * i[1]
for i in v_vals})
animate(
tcombine,
'input0',
{i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals},
)
p_start = position[1]
p_dir = direction[1]
animate(tcombine, 'input1',
{i[0] * lifespan: p_start + p_dir * i[1]
for i in v_vals})
animate(
tcombine,
'input1',
{i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals},
)
p_start = position[2]
p_dir = direction[2]
animate(tcombine, 'input2',
{i[0] * lifespan: p_start + p_dir * i[1]
for i in v_vals})
animate(
tcombine,
'input2',
{i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals},
)
animate(txtnode, 'opacity', {0.7 * lifespan: 1.0, lifespan: 0.0})
_ba.timer(lifespan, txtnode.delete)
def timestring(timeval: float,
centi: bool = True,
timeformat: ba.TimeFormat = TimeFormat.SECONDS,
suppress_format_warning: bool = False) -> ba.Lstr:
def timestring(
timeval: float,
centi: bool = True,
timeformat: ba.TimeFormat = TimeFormat.SECONDS,
suppress_format_warning: bool = False,
) -> ba.Lstr:
"""Generate a ba.Lstr for displaying a time value.
Category: **General Utility Functions**
@ -292,32 +321,56 @@ def timestring(timeval: float,
hval = (timeval // 1000) // (60 * 60)
if hval != 0:
bits.append('${H}')
subs.append(('${H}',
Lstr(resource='timeSuffixHoursText',
subs=[('${COUNT}', str(hval))])))
subs.append(
(
'${H}',
Lstr(
resource='timeSuffixHoursText',
subs=[('${COUNT}', str(hval))],
),
)
)
mval = ((timeval // 1000) // 60) % 60
if mval != 0:
bits.append('${M}')
subs.append(('${M}',
Lstr(resource='timeSuffixMinutesText',
subs=[('${COUNT}', str(mval))])))
subs.append(
(
'${M}',
Lstr(
resource='timeSuffixMinutesText',
subs=[('${COUNT}', str(mval))],
),
)
)
# We add seconds if its non-zero *or* we haven't added anything else.
if centi:
# pylint: disable=consider-using-f-string
sval = (timeval / 1000.0 % 60.0)
sval = timeval / 1000.0 % 60.0
if sval >= 0.005 or not bits:
bits.append('${S}')
subs.append(('${S}',
Lstr(resource='timeSuffixSecondsText',
subs=[('${COUNT}', ('%.2f' % sval))])))
subs.append(
(
'${S}',
Lstr(
resource='timeSuffixSecondsText',
subs=[('${COUNT}', ('%.2f' % sval))],
),
)
)
else:
sval = (timeval // 1000 % 60)
sval = timeval // 1000 % 60
if sval != 0 or not bits:
bits.append('${S}')
subs.append(('${S}',
Lstr(resource='timeSuffixSecondsText',
subs=[('${COUNT}', str(sval))])))
subs.append(
(
'${S}',
Lstr(
resource='timeSuffixSecondsText',
subs=[('${COUNT}', str(sval))],
),
)
)
return Lstr(value=' '.join(bits), subs=subs)
@ -332,11 +385,17 @@ def cameraflash(duration: float = 999.0) -> None:
# pylint: disable=too-many-locals
import random
from ba._nodeactor import NodeActor
x_spread = 10
y_spread = 5
positions = [[-x_spread, -y_spread], [0, -y_spread], [0, y_spread],
[x_spread, -y_spread], [x_spread, y_spread],
[-x_spread, y_spread]]
positions = [
[-x_spread, -y_spread],
[0, -y_spread],
[0, y_spread],
[x_spread, -y_spread],
[x_spread, y_spread],
[-x_spread, y_spread],
]
times = [0, 2700, 1000, 1800, 500, 1400]
# Store this on the current activity so we only have one at a time.
@ -345,57 +404,73 @@ def cameraflash(duration: float = 999.0) -> None:
activity.camera_flash_data = [] # type: ignore
for i in range(6):
light = NodeActor(
_ba.newnode('light',
attrs={
'position': (positions[i][0], 0, positions[i][1]),
'radius': 1.0,
'lights_volumes': False,
'height_attenuated': False,
'color': (0.2, 0.2, 0.8)
}))
_ba.newnode(
'light',
attrs={
'position': (positions[i][0], 0, positions[i][1]),
'radius': 1.0,
'lights_volumes': False,
'height_attenuated': False,
'color': (0.2, 0.2, 0.8),
},
)
)
sval = 1.87
iscale = 1.3
tcombine = _ba.newnode('combine',
owner=light.node,
attrs={
'size': 3,
'input0': positions[i][0],
'input1': 0,
'input2': positions[i][1]
})
tcombine = _ba.newnode(
'combine',
owner=light.node,
attrs={
'size': 3,
'input0': positions[i][0],
'input1': 0,
'input2': positions[i][1],
},
)
assert light.node
tcombine.connectattr('output', light.node, 'position')
xval = positions[i][0]
yval = positions[i][1]
spd = 0.5 + random.random()
spd2 = 0.5 + random.random()
animate(tcombine,
'input0', {
0.0: xval + 0,
0.069 * spd: xval + 10.0,
0.143 * spd: xval - 10.0,
0.201 * spd: xval + 0
},
loop=True)
animate(tcombine,
'input2', {
0.0: yval + 0,
0.15 * spd2: yval + 10.0,
0.287 * spd2: yval - 10.0,
0.398 * spd2: yval + 0
},
loop=True)
animate(light.node,
'intensity', {
0.0: 0,
0.02 * sval: 0,
0.05 * sval: 0.8 * iscale,
0.08 * sval: 0,
0.1 * sval: 0
},
loop=True,
offset=times[i])
_ba.timer((times[i] + random.randint(1, int(duration)) * 40 * sval),
light.node.delete,
timeformat=TimeFormat.MILLISECONDS)
animate(
tcombine,
'input0',
{
0.0: xval + 0,
0.069 * spd: xval + 10.0,
0.143 * spd: xval - 10.0,
0.201 * spd: xval + 0,
},
loop=True,
)
animate(
tcombine,
'input2',
{
0.0: yval + 0,
0.15 * spd2: yval + 10.0,
0.287 * spd2: yval - 10.0,
0.398 * spd2: yval + 0,
},
loop=True,
)
animate(
light.node,
'intensity',
{
0.0: 0,
0.02 * sval: 0,
0.05 * sval: 0.8 * iscale,
0.08 * sval: 0,
0.1 * sval: 0,
},
loop=True,
offset=times[i],
)
_ba.timer(
(times[i] + random.randint(1, int(duration)) * 40 * sval),
light.node.delete,
timeformat=TimeFormat.MILLISECONDS,
)
activity.camera_flash_data.append(light) # type: ignore

View File

@ -64,6 +64,7 @@ def getclass(name: str, subclassof: type[T]) -> type[T]:
'subclassof' class, and a TypeError will be raised if not.
"""
import importlib
splits = name.split('.')
modulename = '.'.join(splits[:-1])
classname = splits[-1]
@ -84,8 +85,10 @@ def json_prep(data: Any) -> Any:
"""
if isinstance(data, dict):
return dict((json_prep(key), json_prep(value))
for key, value in list(data.items()))
return dict(
(json_prep(key), json_prep(value))
for key, value in list(data.items())
)
if isinstance(data, list):
return [json_prep(element) for element in data]
if isinstance(data, tuple):
@ -96,19 +99,23 @@ def json_prep(data: Any) -> Any:
return data.decode(errors='ignore')
except Exception:
from ba import _error
print_error('json_prep encountered utf-8 decode error', once=True)
return data.decode(errors='ignore')
if not isinstance(data, (str, float, bool, type(None), int)):
print_error('got unsupported type in json_prep:' + str(type(data)),
once=True)
print_error(
'got unsupported type in json_prep:' + str(type(data)), once=True
)
return data
def utf8_all(data: Any) -> Any:
"""Convert any unicode data in provided sequence(s) to utf8 bytes."""
if isinstance(data, dict):
return dict((utf8_all(key), utf8_all(value))
for key, value in list(data.items()))
return dict(
(utf8_all(key), utf8_all(value))
for key, value in list(data.items())
)
if isinstance(data, list):
return [utf8_all(element) for element in data]
if isinstance(data, tuple):
@ -190,11 +197,17 @@ class _WeakCall:
else:
app = _ba.app
if not app.did_weak_call_warning:
print(('Warning: callable passed to ba.WeakCall() is not'
' weak-referencable (' + str(args[0]) +
'); use ba.Call() instead to avoid this '
'warning. Stack-trace:'))
print(
(
'Warning: callable passed to ba.WeakCall() is not'
' weak-referencable ('
+ str(args[0])
+ '); use ba.Call() instead to avoid this '
'warning. Stack-trace:'
)
)
import traceback
traceback.print_stack()
app.did_weak_call_warning = True
self._call = args[0]
@ -205,8 +218,15 @@ class _WeakCall:
return self._call(*self._args + args_extra, **self._keywds)
def __str__(self) -> str:
return ('<ba.WeakCall object; _call=' + str(self._call) + ' _args=' +
str(self._args) + ' _keywds=' + str(self._keywds) + '>')
return (
'<ba.WeakCall object; _call='
+ str(self._call)
+ ' _args='
+ str(self._args)
+ ' _keywds='
+ str(self._keywds)
+ '>'
)
class _Call:
@ -244,8 +264,15 @@ class _Call:
return self._call(*self._args + args_extra, **self._keywds)
def __str__(self) -> str:
return ('<ba.Call object; _call=' + str(self._call) + ' _args=' +
str(self._args) + ' _keywds=' + str(self._keywds) + '>')
return (
'<ba.Call object; _call='
+ str(self._call)
+ ' _args='
+ str(self._args)
+ ' _keywds='
+ str(self._keywds)
+ '>'
)
if TYPE_CHECKING:
@ -278,7 +305,7 @@ class WeakMethod:
obj = self._obj()
if obj is None:
return None
return self._func(*((obj, ) + args), **keywds)
return self._func(*((obj,) + args), **keywds)
def __str__(self) -> str:
return '<ba.WeakMethod object; call=' + str(self._func) + '>'
@ -300,9 +327,9 @@ def verify_object_death(obj: object) -> None:
# if we queue a lot of them.
delay = random.uniform(2.0, 5.5)
with _ba.Context('ui'):
_ba.timer(delay,
lambda: _verify_object_death(ref),
timetype=TimeType.REAL)
_ba.timer(
delay, lambda: _verify_object_death(ref), timetype=TimeType.REAL
)
def print_active_refs(obj: Any) -> None:
@ -314,6 +341,7 @@ def print_active_refs(obj: Any) -> None:
"""
# pylint: disable=too-many-nested-blocks
from types import FrameType, TracebackType
refs = list(gc.get_referrers(obj))
print(f'{Clr.YLW}Active referrers to {obj}:{Clr.RST}')
for i, ref in enumerate(refs):
@ -330,20 +358,28 @@ def print_active_refs(obj: Any) -> None:
# Can go further down the rabbit-hole if needed...
if bool(False):
if isinstance(ref2, TracebackType):
print(f'{Clr.YLW} '
f'Active referrers to #a{j+1}:{Clr.RST}')
print(
f'{Clr.YLW} '
f'Active referrers to #a{j+1}:{Clr.RST}'
)
refs3 = list(gc.get_referrers(ref2))
for k, ref3 in enumerate(refs3):
print(f'{Clr.YLW} '
f'#b{k+1}:{Clr.BLU} {ref3}{Clr.RST}')
print(
f'{Clr.YLW} '
f'#b{k+1}:{Clr.BLU} {ref3}{Clr.RST}'
)
if isinstance(ref3, BaseException):
print(f'{Clr.YLW} Active referrers to'
f' #b{k+1}:{Clr.RST}')
print(
f'{Clr.YLW} Active referrers to'
f' #b{k+1}:{Clr.RST}'
)
refs4 = list(gc.get_referrers(ref3))
for x, ref4 in enumerate(refs4):
print(f'{Clr.YLW} #c{x+1}:{Clr.BLU}'
f' {ref4}{Clr.RST}')
print(
f'{Clr.YLW} #c{x+1}:{Clr.BLU}'
f' {ref4}{Clr.RST}'
)
def _verify_object_death(wref: weakref.ref) -> None:
@ -357,8 +393,10 @@ def _verify_object_death(wref: weakref.ref) -> None:
print(f'Note: unable to get type name for {obj}')
name = 'object'
print(f'{Clr.RED}Error: {name} not dying when expected to:'
f' {Clr.BLD}{obj}{Clr.RST}')
print(
f'{Clr.RED}Error: {name} not dying when expected to:'
f' {Clr.BLD}{obj}{Clr.RST}'
)
print_active_refs(obj)

View File

@ -54,87 +54,113 @@ def set_config_fullscreen_off() -> None:
def not_signed_in_screen_message() -> None:
from ba._language import Lstr
_ba.screenmessage(Lstr(resource='notSignedInErrorText'))
def connecting_to_party_message() -> None:
from ba._language import Lstr
_ba.screenmessage(Lstr(resource='internal.connectingToPartyText'),
color=(1, 1, 1))
_ba.screenmessage(
Lstr(resource='internal.connectingToPartyText'), color=(1, 1, 1)
)
def rejecting_invite_already_in_party_message() -> None:
from ba._language import Lstr
_ba.screenmessage(
Lstr(resource='internal.rejectingInviteAlreadyInPartyText'),
color=(1, 0.5, 0))
color=(1, 0.5, 0),
)
def connection_failed_message() -> None:
from ba._language import Lstr
_ba.screenmessage(Lstr(resource='internal.connectionFailedText'),
color=(1, 0.5, 0))
_ba.screenmessage(
Lstr(resource='internal.connectionFailedText'), color=(1, 0.5, 0)
)
def temporarily_unavailable_message() -> None:
from ba._language import Lstr
_ba.playsound(_ba.getsound('error'))
_ba.screenmessage(
Lstr(resource='getTicketsWindow.unavailableTemporarilyText'),
color=(1, 0, 0))
color=(1, 0, 0),
)
def in_progress_message() -> None:
from ba._language import Lstr
_ba.playsound(_ba.getsound('error'))
_ba.screenmessage(Lstr(resource='getTicketsWindow.inProgressText'),
color=(1, 0, 0))
_ba.screenmessage(
Lstr(resource='getTicketsWindow.inProgressText'), color=(1, 0, 0)
)
def error_message() -> None:
from ba._language import Lstr
_ba.playsound(_ba.getsound('error'))
_ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
def purchase_not_valid_error() -> None:
from ba._language import Lstr
_ba.playsound(_ba.getsound('error'))
_ba.screenmessage(Lstr(resource='store.purchaseNotValidError',
subs=[('${EMAIL}', 'support@froemling.net')]),
color=(1, 0, 0))
_ba.screenmessage(
Lstr(
resource='store.purchaseNotValidError',
subs=[('${EMAIL}', 'support@froemling.net')],
),
color=(1, 0, 0),
)
def purchase_already_in_progress_error() -> None:
from ba._language import Lstr
_ba.playsound(_ba.getsound('error'))
_ba.screenmessage(Lstr(resource='store.purchaseAlreadyInProgressText'),
color=(1, 0, 0))
_ba.screenmessage(
Lstr(resource='store.purchaseAlreadyInProgressText'), color=(1, 0, 0)
)
def gear_vr_controller_warning() -> None:
from ba._language import Lstr
_ba.playsound(_ba.getsound('error'))
_ba.screenmessage(Lstr(resource='usesExternalControllerText'),
color=(1, 0, 0))
_ba.screenmessage(
Lstr(resource='usesExternalControllerText'), color=(1, 0, 0)
)
def uuid_str() -> str:
import uuid
return str(uuid.uuid4())
def orientation_reset_cb_message() -> None:
from ba._language import Lstr
_ba.screenmessage(
Lstr(resource='internal.vrOrientationResetCardboardText'),
color=(0, 1, 0))
color=(0, 1, 0),
)
def orientation_reset_message() -> None:
from ba._language import Lstr
_ba.screenmessage(Lstr(resource='internal.vrOrientationResetText'),
color=(0, 1, 0))
_ba.screenmessage(
Lstr(resource='internal.vrOrientationResetText'), color=(0, 1, 0)
)
def on_app_pause() -> None:
@ -147,12 +173,14 @@ def on_app_resume() -> None:
def launch_main_menu_session() -> None:
from bastd.mainmenu import MainMenuSession
_ba.new_host_session(MainMenuSession)
def language_test_toggle() -> None:
_ba.app.lang.setlanguage('Gibberish' if _ba.app.lang.language ==
'English' else 'English')
_ba.app.lang.setlanguage(
'Gibberish' if _ba.app.lang.language == 'English' else 'English'
)
def award_in_control_achievement() -> None:
@ -173,8 +201,10 @@ def launch_coop_game(name: str) -> None:
def purchases_restored_message() -> None:
from ba._language import Lstr
_ba.screenmessage(Lstr(resource='getTicketsWindow.purchasesRestoredText'),
color=(0, 1, 0))
_ba.screenmessage(
Lstr(resource='getTicketsWindow.purchasesRestoredText'), color=(0, 1, 0)
)
def dismiss_wii_remotes_window() -> None:
@ -185,8 +215,10 @@ def dismiss_wii_remotes_window() -> None:
def unavailable_message() -> None:
from ba._language import Lstr
_ba.screenmessage(Lstr(resource='getTicketsWindow.unavailableText'),
color=(1, 0, 0))
_ba.screenmessage(
Lstr(resource='getTicketsWindow.unavailableText'), color=(1, 0, 0)
)
def submit_analytics_counts(sval: str) -> None:
@ -196,19 +228,23 @@ def submit_analytics_counts(sval: str) -> None:
def set_last_ad_network(sval: str) -> None:
import time
_ba.app.ads.last_ad_network = sval
_ba.app.ads.last_ad_network_set_time = time.time()
def no_game_circle_message() -> None:
from ba._language import Lstr
_ba.screenmessage(Lstr(resource='noGameCircleText'), color=(1, 0, 0))
def google_play_purchases_not_available_message() -> None:
from ba._language import Lstr
_ba.screenmessage(Lstr(resource='googlePlayPurchasesNotAvailableText'),
color=(1, 0, 0))
_ba.screenmessage(
Lstr(resource='googlePlayPurchasesNotAvailableText'), color=(1, 0, 0)
)
def empty_call() -> None:
@ -229,8 +265,10 @@ def coin_icon_press() -> None:
def ticket_icon_press() -> None:
from bastd.ui.resourcetypeinfo import ResourceTypeInfoWindow
ResourceTypeInfoWindow(
origin_widget=_ba.get_special_widget('tickets_info_button'))
origin_widget=_ba.get_special_widget('tickets_info_button')
)
def back_button_press() -> None:
@ -243,6 +281,7 @@ def friends_button_press() -> None:
def print_trace() -> None:
import traceback
print('Python Traceback (most recent call last):')
traceback.print_stack()
@ -256,6 +295,7 @@ def toggle_fullscreen() -> None:
def party_icon_activate(origin: Sequence[float]) -> None:
import weakref
from bastd.ui.party import PartyWindow
app = _ba.app
_ba.playsound(_ba.getsound('swish'))
@ -276,13 +316,16 @@ def ui_remote_press() -> None:
# Can be called without a context; need a context for getsound.
with _ba.Context('ui'):
_ba.screenmessage(Lstr(resource='internal.controllerForMenusOnlyText'),
color=(1, 0, 0))
_ba.screenmessage(
Lstr(resource='internal.controllerForMenusOnlyText'),
color=(1, 0, 0),
)
_ba.playsound(_ba.getsound('error'))
def quit_window() -> None:
from bastd.ui.confirm import QuitWindow
QuitWindow()
@ -292,6 +335,7 @@ def remove_in_game_ads_message() -> None:
def telnet_access_request() -> None:
from bastd.ui.telnet import TelnetAccessRequestWindow
TelnetAccessRequestWindow()
@ -305,11 +349,13 @@ def shutdown() -> None:
def gc_disable() -> None:
import gc
gc.disable()
def device_menu_press(device: ba.InputDevice) -> None:
from bastd.ui.mainmenu import MainMenuWindow
in_main_menu = _ba.app.ui.has_main_menu_window()
if not in_main_menu:
_ba.set_ui_input_device(device)
@ -319,6 +365,7 @@ def device_menu_press(device: ba.InputDevice) -> None:
def show_url_window(address: str) -> None:
from bastd.ui.url import ShowURLWindow
ShowURLWindow(address)
@ -328,8 +375,9 @@ def party_invite_revoke(invite_id: str) -> None:
for winref in _ba.app.invite_confirm_windows:
win = winref()
if win is not None and win.ew_party_invite_id == invite_id:
_ba.containerwidget(edit=win.get_root_widget(),
transition='out_right')
_ba.containerwidget(
edit=win.get_root_widget(), transition='out_right'
)
def filter_chat_message(msg: str, client_id: int) -> str | None:
@ -345,8 +393,10 @@ def filter_chat_message(msg: str, client_id: int) -> str | None:
def local_chat_message(msg: str) -> None:
if (_ba.app.ui.party_window is not None
and _ba.app.ui.party_window() is not None):
if (
_ba.app.ui.party_window is not None
and _ba.app.ui.party_window() is not None
):
_ba.app.ui.party_window().on_chat_message(msg)
@ -356,13 +406,14 @@ def get_player_icon(sessionplayer: ba.SessionPlayer) -> dict[str, Any]:
'texture': _ba.gettexture(info['texture']),
'tint_texture': _ba.gettexture(info['tint_texture']),
'tint_color': info['tint_color'],
'tint2_color': info['tint2_color']
'tint2_color': info['tint2_color'],
}
def hash_strings(inputs: list[str]) -> str:
"""Hash provided strings into a short output string."""
import hashlib
sha = hashlib.sha1()
for inp in inputs:
sha.update(inp.encode())

View File

@ -48,7 +48,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonRight': 23,
'buttonUp': 20,
'buttonDown': 21,
'buttonVRReorient': 110
'buttonVRReorient': 110,
}.get(name, -1)
# If there's an entry in our config for this controller, use it.
@ -79,7 +79,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonRun2': 5,
'buttonRun1': 6,
'buttonJump': 1,
'buttonIgnored': 11
'buttonIgnored': 11,
}.get(name, -1)
# Ps4 controller.
@ -94,7 +94,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonPunch': 1,
'buttonRun2': 5,
'buttonRun1': 6,
'triggerRun1': 5
'triggerRun1': 5,
}.get(name, -1)
# Look for some exact types.
@ -112,7 +112,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonPunch': 100,
'buttonRun2': 103,
'buttonRun1': 104,
'triggerRun1': 24
'triggerRun1': 24,
}.get(name, -1)
if devicename == 'NYKO PLAYPAD PRO':
return {
@ -126,7 +126,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonRight': 23,
'buttonStart': 83,
'buttonPunch': 100,
'buttonDown': 21
'buttonDown': 21,
}.get(name, -1)
if devicename == 'Logitech Dual Action':
return {
@ -136,7 +136,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonBomb': 101,
'buttonJump': 100,
'buttonStart': 109,
'buttonPunch': 97
'buttonPunch': 97,
}.get(name, -1)
if devicename == 'Xbox 360 Wireless Receiver':
return {
@ -150,7 +150,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonRight': 23,
'buttonStart': 83,
'buttonPunch': 100,
'buttonDown': 21
'buttonDown': 21,
}.get(name, -1)
if devicename == 'Microsoft X-Box 360 pad':
return {
@ -160,11 +160,12 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonBomb': 98,
'buttonJump': 97,
'buttonStart': 83,
'buttonPunch': 100
'buttonPunch': 100,
}.get(name, -1)
if devicename in [
'Amazon Remote', 'Amazon Bluetooth Dev',
'Amazon Fire TV Remote'
'Amazon Remote',
'Amazon Bluetooth Dev',
'Amazon Fire TV Remote',
]:
return {
'triggerRun2': 23,
@ -178,7 +179,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonRight': 23,
'buttonStart': 83,
'buttonPunch': 90,
'buttonDown': 21
'buttonDown': 21,
}.get(name, -1)
elif 'NVIDIA SHIELD;' in useragentstring:
@ -193,7 +194,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonStart': 109,
'buttonPunch': 100,
'buttonIgnored': 184,
'buttonIgnored2': 86
'buttonIgnored2': 86,
}.get(name, -1)
elif platform == 'mac':
if devicename == 'PLAYSTATION(R)3 Controller':
@ -207,7 +208,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonBomb': 14,
'buttonPickUp': 13,
'buttonStart': 4,
'buttonIgnored': 17
'buttonIgnored': 17,
}.get(name, -1)
if devicename in ['Wireless 360 Controller', 'Controller']:
@ -225,16 +226,18 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonUp': 1,
'triggerRun1': 5,
'triggerRun2': 6,
'buttonIgnored': 11
'buttonIgnored': 11,
}.get(name, -1)
if (devicename
in ['Logitech Dual Action', 'Logitech Cordless RumblePad 2']):
if devicename in [
'Logitech Dual Action',
'Logitech Cordless RumblePad 2',
]:
return {
'buttonJump': 2,
'buttonPunch': 1,
'buttonBomb': 3,
'buttonPickUp': 4,
'buttonStart': 10
'buttonStart': 10,
}.get(name, -1)
# Old gravis gamepad.
@ -244,7 +247,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonPunch': 1,
'buttonBomb': 3,
'buttonPickUp': 4,
'buttonStart': 10
'buttonStart': 10,
}.get(name, -1)
if devicename == 'Microsoft SideWinder Plug & Play Game Pad':
@ -253,7 +256,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonPunch': 3,
'buttonBomb': 2,
'buttonPickUp': 4,
'buttonStart': 6
'buttonStart': 6,
}.get(name, -1)
# Saitek P2500 Rumble Force Pad.. (hopefully works for others too?..)
@ -263,7 +266,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonPunch': 1,
'buttonBomb': 4,
'buttonPickUp': 2,
'buttonStart': 11
'buttonStart': 11,
}.get(name, -1)
# Some crazy 'Senze' dual gamepad.
@ -288,7 +291,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonStart': 10,
'buttonStart_B': 22,
'enableSecondary': 1,
'unassignedButtonsRun': False
'unassignedButtonsRun': False,
}.get(name, -1)
if devicename == 'USB Gamepad ': # some weird 'JITE' gamepad
return {
@ -298,7 +301,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonPunch': 4,
'buttonBomb': 2,
'buttonPickUp': 1,
'buttonStart': 10
'buttonStart': 10,
}.get(name, -1)
default_android_mapping = {
@ -317,7 +320,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonRight': 23,
'buttonUp': 20,
'buttonDown': 21,
'buttonVRReorient': 110
'buttonVRReorient': 110,
}
# Generic android...
@ -341,7 +344,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonRight': 23,
'buttonUp': 20,
'buttonDown': 21,
'buttonVRReorient': 108
'buttonVRReorient': 108,
}.get(name, -1)
# Adt-1 gamepad (use funky 'mode' button for start).
@ -357,7 +360,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'startButtonActivatesDefaultWidget': False,
'buttonRun2': 104,
'buttonRun1': 103,
'triggerRun1': 18
'triggerRun1': 18,
}.get(name, -1)
# Nexus player remote.
if devicename == 'Nexus Remote':
@ -376,7 +379,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonPunch': 24,
'buttonRun2': 104,
'buttonRun1': 103,
'triggerRun1': 18
'triggerRun1': 18,
}.get(name, -1)
if devicename == 'virtual-remote':
@ -397,13 +400,15 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonRun1': 103,
'buttonDown': 21,
'startButtonActivatesDefaultWidget': False,
'uiOnly': True
'uiOnly': True,
}.get(name, -1)
# flag particular gamepads to use exact android defaults..
# (so they don't even ask to configure themselves)
if devicename in ['Samsung Game Pad EI-GP20', 'ASUS Gamepad'
] or devicename.startswith('Freefly VR Glide'):
if devicename in [
'Samsung Game Pad EI-GP20',
'ASUS Gamepad',
] or devicename.startswith('Freefly VR Glide'):
return default_android_mapping.get(name, -1)
# Nvidia controller is default, but gets some strange
@ -423,7 +428,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonPunch': 100,
'buttonRun2': 104,
'buttonRun1': 103,
'triggerRun1': 18
'triggerRun1': 18,
}.get(name, -1)
# Default keyboard vals across platforms..
@ -438,7 +443,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonDown': 274,
'buttonLeft': 276,
'buttonRight': 275,
'buttonStart': 263
'buttonStart': 263,
}.get(name, -1)
return {
'buttonPickUp': 1073741917,
@ -449,7 +454,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonRight': 1073741903,
'buttonStart': 1073741919,
'buttonPunch': 1073741913,
'buttonDown': 1073741905
'buttonDown': 1073741905,
}.get(name, -1)
if devicename == 'Keyboard' and unique_id == '#1':
return {
@ -460,7 +465,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonUp': 119,
'buttonDown': 115,
'buttonLeft': 97,
'buttonRight': 100
'buttonRight': 100,
}.get(name, -1)
# Ok, this gamepad's not in our specific preset list;
@ -479,7 +484,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonPunch': 16,
'buttonBomb': 14,
'buttonPickUp': 13,
'buttonStart': 4
'buttonStart': 4,
}.get(name, -1)
# Dual Action Config - hopefully applies to more...
@ -489,7 +494,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonPunch': 1,
'buttonBomb': 3,
'buttonPickUp': 4,
'buttonStart': 10
'buttonStart': 10,
}.get(name, -1)
# Saitek P2500 Rumble Force Pad.. (hopefully works for others too?..)
@ -499,7 +504,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonPunch': 1,
'buttonBomb': 4,
'buttonPickUp': 2,
'buttonStart': 11
'buttonStart': 11,
}.get(name, -1)
# Gravis stuff?...
@ -509,7 +514,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonPunch': 1,
'buttonBomb': 3,
'buttonPickUp': 4,
'buttonStart': 10
'buttonStart': 10,
}.get(name, -1)
# Reasonable defaults.
@ -542,21 +547,23 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
'buttonPunch': 2,
'buttonBomb': 3,
'buttonPickUp': 4,
'buttonStart': 5
'buttonStart': 5,
}.get(name, -1)
def _gen_android_input_hash() -> str:
import os
import hashlib
md5 = hashlib.md5()
# Currently we just do a single hash of *all* inputs on android
# and that's it.. good enough.
# (grabbing mappings for a specific device looks to be non-trivial)
for dirname in [
'/system/usr/keylayout', '/data/usr/keylayout',
'/data/system/devices/keylayout'
'/system/usr/keylayout',
'/data/usr/keylayout',
'/data/system/devices/keylayout',
]:
try:
if os.path.isdir(dirname):
@ -573,8 +580,10 @@ def _gen_android_input_hash() -> str:
pass
except Exception:
from ba import _error
_error.print_exception(
'error in _gen_android_input_hash inner loop')
'error in _gen_android_input_hash inner loop'
)
return md5.hexdigest()
@ -597,12 +606,14 @@ def get_input_map_hash(inputdevice: ba.InputDevice) -> str:
return app.input_map_hash
except Exception:
from ba import _error
_error.print_exception('Exception in get_input_map_hash')
return ''
def get_input_device_config(device: ba.InputDevice,
default: bool) -> tuple[dict, str]:
def get_input_device_config(
device: ba.InputDevice, default: bool
) -> tuple[dict, str]:
"""Given an input device, return its config dict in the app config.
The dict will be created if it does not exist.
@ -634,8 +645,10 @@ def get_last_player_name_from_input_device(device: ba.InputDevice) -> str:
# otherwise default to their current random name.
profilename = '_random'
key_name = device.name + ' ' + device.unique_identifier
if ('Default Player Profiles' in appconfig
and key_name in appconfig['Default Player Profiles']):
if (
'Default Player Profiles' in appconfig
and key_name in appconfig['Default Player Profiles']
):
profilename = appconfig['Default Player Profiles'][key_name]
if profilename == '_random':
profilename = device.get_default_player_name()

View File

@ -14,6 +14,7 @@ from typing import TYPE_CHECKING
try:
# noinspection PyUnresolvedReferences
import _bainternal
HAVE_INTERNAL = True
except ImportError:
HAVE_INTERNAL = False
@ -26,6 +27,7 @@ if TYPE_CHECKING:
# to account for its absence should call this to draw attention to itself.
def _no_bainternal_warning() -> None:
import logging
logging.warning('INTERNAL CALL RUN WITHOUT INTERNAL PRESENT.')
@ -47,8 +49,9 @@ def get_master_server_address(source: int = -1, version: int = 1) -> str:
Return the address of the master server.
"""
if HAVE_INTERNAL:
return _bainternal.get_master_server_address(source=source,
version=version)
return _bainternal.get_master_server_address(
source=source, version=version
)
raise _no_bainternal_error()
@ -75,8 +78,9 @@ def game_service_has_leaderboard(game: str, config: str) -> bool:
for it on the game service.
"""
if HAVE_INTERNAL:
return _bainternal.game_service_has_leaderboard(game=game,
config=config)
return _bainternal.game_service_has_leaderboard(
game=game, config=config
)
# Harmless to always just say no here.
return False
@ -84,8 +88,9 @@ def game_service_has_leaderboard(game: str, config: str) -> bool:
def report_achievement(achievement: str, pass_to_account: bool = True) -> None:
"""(internal)"""
if HAVE_INTERNAL:
_bainternal.report_achievement(achievement=achievement,
pass_to_account=pass_to_account)
_bainternal.report_achievement(
achievement=achievement, pass_to_account=pass_to_account
)
return
# Need to see if this actually still works as expected.. warning for now.
@ -93,17 +98,19 @@ def report_achievement(achievement: str, pass_to_account: bool = True) -> None:
# noinspection PyUnresolvedReferences
def submit_score(game: str,
config: str,
name: Any,
score: int | None,
callback: Callable,
friend_callback: Callable | None,
order: str = 'increasing',
tournament_id: str | None = None,
score_type: str = 'points',
campaign: str | None = None,
level: str | None = None) -> None:
def submit_score(
game: str,
config: str,
name: Any,
score: int | None,
callback: Callable,
friend_callback: Callable | None,
order: str = 'increasing',
tournament_id: str | None = None,
score_type: str = 'points',
campaign: str | None = None,
level: str | None = None,
) -> None:
"""(internal)
Submit a score to the server; callback will be called with the results.
@ -112,24 +119,27 @@ def submit_score(game: str,
score server more mischief-proof.
"""
if HAVE_INTERNAL:
_bainternal.submit_score(game=game,
config=config,
name=name,
score=score,
callback=callback,
friend_callback=friend_callback,
order=order,
tournament_id=tournament_id,
score_type=score_type,
campaign=campaign,
level=level)
_bainternal.submit_score(
game=game,
config=config,
name=name,
score=score,
callback=callback,
friend_callback=friend_callback,
order=order,
tournament_id=tournament_id,
score_type=score_type,
campaign=campaign,
level=level,
)
return
# This technically breaks since callback will never be called/etc.
raise _no_bainternal_error()
def tournament_query(callback: Callable[[dict | None], None],
args: dict) -> None:
def tournament_query(
callback: Callable[[dict | None], None], args: dict
) -> None:
"""(internal)"""
if HAVE_INTERNAL:
_bainternal.tournament_query(callback=callback, args=args)
@ -212,8 +222,9 @@ def in_game_purchase(item: str, price: int) -> None:
# noinspection PyUnresolvedReferences
def add_transaction(transaction: dict,
callback: Callable | None = None) -> None:
def add_transaction(
transaction: dict, callback: Callable | None = None
) -> None:
"""(internal)"""
if HAVE_INTERNAL:
_bainternal.add_transaction(transaction=transaction, callback=callback)
@ -265,7 +276,8 @@ def get_v1_account_misc_read_val(name: str, default_value: Any) -> Any:
"""(internal)"""
if HAVE_INTERNAL:
return _bainternal.get_v1_account_misc_read_val(
name=name, default_value=default_value)
name=name, default_value=default_value
)
raise _no_bainternal_error()
@ -273,15 +285,17 @@ def get_v1_account_misc_read_val_2(name: str, default_value: Any) -> Any:
"""(internal)"""
if HAVE_INTERNAL:
return _bainternal.get_v1_account_misc_read_val_2(
name=name, default_value=default_value)
name=name, default_value=default_value
)
raise _no_bainternal_error()
def get_v1_account_misc_val(name: str, default_value: Any) -> Any:
"""(internal)"""
if HAVE_INTERNAL:
return _bainternal.get_v1_account_misc_val(name=name,
default_value=default_value)
return _bainternal.get_v1_account_misc_val(
name=name, default_value=default_value
)
raise _no_bainternal_error()

View File

@ -35,10 +35,21 @@ class LanguageSubsystem:
"""
# We don't yet support full unicode display on windows or linux :-(.
if (language in {
'Chinese', 'ChineseTraditional', 'Persian', 'Korean', 'Arabic',
'Hindi', 'Vietnamese', 'Thai', 'Tamil'
} and not _ba.can_display_full_unicode()):
if (
language
in {
'Chinese',
'ChineseTraditional',
'Persian',
'Korean',
'Arabic',
'Hindi',
'Vietnamese',
'Thai',
'Tamil',
}
and not _ba.can_display_full_unicode()
):
return False
return True
@ -130,18 +141,22 @@ class LanguageSubsystem:
names[i] = 'ChineseTraditional'
except Exception:
from ba import _error
_error.print_exception()
names = []
for name in names:
if self._can_display_language(name):
langs.add(name)
return sorted(name for name in names
if self._can_display_language(name))
return sorted(
name for name in names if self._can_display_language(name)
)
def setlanguage(self,
language: str | None,
print_change: bool = True,
store_to_config: bool = True) -> None:
def setlanguage(
self,
language: str | None,
print_change: bool = True,
store_to_config: bool = True,
) -> None:
"""Set the active language used for the game.
Pass None to use OS default language.
@ -164,8 +179,9 @@ class LanguageSubsystem:
else:
switched = False
with open('ba_data/data/languages/english.json',
encoding='utf-8') as infile:
with open(
'ba_data/data/languages/english.json', encoding='utf-8'
) as infile:
lenglishvalues = json.loads(infile.read())
# None implies default.
@ -175,16 +191,21 @@ class LanguageSubsystem:
if language == 'English':
lmodvalues = None
else:
lmodfile = 'ba_data/data/languages/' + language.lower(
) + '.json'
lmodfile = (
'ba_data/data/languages/' + language.lower() + '.json'
)
with open(lmodfile, encoding='utf-8') as infile:
lmodvalues = json.loads(infile.read())
except Exception:
from ba import _error
_error.print_exception('Exception importing language:', language)
_ba.screenmessage("Error setting language to '" + language +
"'; see log for details",
color=(1, 0, 0))
_ba.screenmessage(
"Error setting language to '"
+ language
+ "'; see log for details",
color=(1, 0, 0),
)
switched = False
lmodvalues = None
@ -193,8 +214,8 @@ class LanguageSubsystem:
langtarget = self.language_target
assert langtarget is not None
_add_to_attr_dict(
langtarget,
lmodvalues if lmodvalues is not None else lenglishvalues)
langtarget, lmodvalues if lmodvalues is not None else lenglishvalues
)
# Create an attrdict of our target language overlaid
# on our base (english).
@ -209,20 +230,22 @@ class LanguageSubsystem:
# Pass some keys/values in for low level code to use;
# start with everything in their 'internal' section.
internal_vals = [
v for v in list(lfull['internal'].items())
if isinstance(v[1], str)
v for v in list(lfull['internal'].items()) if isinstance(v[1], str)
]
# Cherry-pick various other values to include.
# (should probably get rid of the 'internal' section
# and do everything this way)
for value in [
'replayNameDefaultText', 'replayWriteErrorText',
'replayVersionErrorText', 'replayReadErrorText'
'replayNameDefaultText',
'replayWriteErrorText',
'replayVersionErrorText',
'replayReadErrorText',
]:
internal_vals.append((value, lfull[value]))
internal_vals.append(
('axisText', lfull['configGamepadWindow']['axisText']))
('axisText', lfull['configGamepadWindow']['axisText'])
)
internal_vals.append(('buttonText', lfull['buttonText']))
lmerged = self.language_merged
assert lmerged is not None
@ -232,16 +255,22 @@ class LanguageSubsystem:
random_names = [n for n in random_names if n != '']
_ba.set_internal_language_keys(internal_vals, random_names)
if switched and print_change:
_ba.screenmessage(Lstr(resource='languageSetText',
subs=[('${LANGUAGE}',
Lstr(translate=('languages',
language)))]),
color=(0, 1, 0))
_ba.screenmessage(
Lstr(
resource='languageSetText',
subs=[
('${LANGUAGE}', Lstr(translate=('languages', language)))
],
),
color=(0, 1, 0),
)
def get_resource(self,
resource: str,
fallback_resource: str | None = None,
fallback_value: Any = None) -> Any:
def get_resource(
self,
resource: str,
fallback_resource: str | None = None,
fallback_value: Any = None,
) -> Any:
"""Return a translation resource by name.
DEPRECATED; use ba.Lstr functionality for these purposes.
@ -251,24 +280,29 @@ class LanguageSubsystem:
if self.language_merged is None:
language = self.language
try:
self.setlanguage(language,
print_change=False,
store_to_config=False)
self.setlanguage(
language, print_change=False, store_to_config=False
)
except Exception:
from ba import _error
_error.print_exception('exception setting language to',
language)
_error.print_exception(
'exception setting language to', language
)
# Try english as a fallback.
if language != 'English':
print('Resorting to fallback language (English)')
try:
self.setlanguage('English',
print_change=False,
store_to_config=False)
self.setlanguage(
'English',
print_change=False,
store_to_config=False,
)
except Exception:
_error.print_exception(
'error setting language to english fallback')
'error setting language to english fallback'
)
# If they provided a fallback_resource value, try the
# target-language-only dict first and then fall back to trying the
@ -325,16 +359,20 @@ class LanguageSubsystem:
# anywhere. Now if we've been given a fallback value, return it;
# otherwise fail.
from ba import _error
if fallback_value is not None:
return fallback_value
raise _error.NotFoundError(
f"Resource not found: '{resource}'") from None
f"Resource not found: '{resource}'"
) from None
def translate(self,
category: str,
strval: str,
raise_exceptions: bool = False,
print_errors: bool = False) -> str:
def translate(
self,
category: str,
strval: str,
raise_exceptions: bool = False,
print_errors: bool = False,
) -> str:
"""Translate a value (or return the value if no translation available)
DEPRECATED; use ba.Lstr functionality for these purposes.
@ -345,8 +383,17 @@ class LanguageSubsystem:
if raise_exceptions:
raise
if print_errors:
print(('Translate error: category=\'' + category +
'\' name=\'' + strval + '\' exc=' + str(exc) + ''))
print(
(
'Translate error: category=\''
+ category
+ '\' name=\''
+ strval
+ '\' exc='
+ str(exc)
+ ''
)
)
translated = None
translated_out: str
if translated is None:
@ -403,28 +450,31 @@ class Lstr:
# pylint: disable=dangerous-default-value
# noinspection PyDefaultArgument
@overload
def __init__(self,
*,
resource: str,
fallback_resource: str = '',
fallback_value: str = '',
subs: Sequence[tuple[str, str | Lstr]] = []) -> None:
def __init__(
self,
*,
resource: str,
fallback_resource: str = '',
fallback_value: str = '',
subs: Sequence[tuple[str, str | Lstr]] = [],
) -> None:
"""Create an Lstr from a string resource."""
# noinspection PyShadowingNames,PyDefaultArgument
@overload
def __init__(self,
*,
translate: tuple[str, str],
subs: Sequence[tuple[str, str | Lstr]] = []) -> None:
def __init__(
self,
*,
translate: tuple[str, str],
subs: Sequence[tuple[str, str | Lstr]] = [],
) -> None:
"""Create an Lstr by translating a string in a category."""
# noinspection PyDefaultArgument
@overload
def __init__(self,
*,
value: str,
subs: Sequence[tuple[str, str | Lstr]] = []) -> None:
def __init__(
self, *, value: str, subs: Sequence[tuple[str, str | Lstr]] = []
) -> None:
"""Create an Lstr from a raw string value."""
# pylint: enable=redefined-outer-name, dangerous-default-value
@ -478,10 +528,12 @@ class Lstr:
del keywds['value']
if 'fallback' in keywds:
from ba import _error
_error.print_error(
'deprecated "fallback" arg passed to Lstr(); use '
'either "fallback_resource" or "fallback_value"',
once=True)
once=True,
)
keywds['f'] = keywds['fallback']
del keywds['fallback']
if 'fallback_resource' in keywds:
@ -517,6 +569,7 @@ class Lstr:
return json.dumps(self.args, separators=(',', ':'))
except Exception:
from ba import _error
_error.print_exception('_get_json failed for', self.args)
return 'JSON_ERR'
@ -542,13 +595,20 @@ def _add_to_attr_dict(dst: AttrDict, src: dict) -> None:
except Exception:
dst_dict = dst[key] = AttrDict()
if not isinstance(dst_dict, AttrDict):
raise RuntimeError("language key '" + key +
"' is defined both as a dict and value")
raise RuntimeError(
"language key '"
+ key
+ "' is defined both as a dict and value"
)
_add_to_attr_dict(dst_dict, value)
else:
if not isinstance(value, (float, int, bool, str, str, type(None))):
raise TypeError("invalid value type for res '" + key + "': " +
str(type(value)))
raise TypeError(
"invalid value type for res '"
+ key
+ "': "
+ str(type(value))
)
dst[key] = value

View File

@ -20,12 +20,14 @@ class Level:
Category: **Gameplay Classes**
"""
def __init__(self,
name: str,
gametype: type[ba.GameActivity],
settings: dict,
preview_texture_name: str,
displayname: str | None = None):
def __init__(
self,
name: str,
gametype: type[ba.GameActivity],
settings: dict,
preview_texture_name: str,
displayname: str | None = None,
):
self._name = name
self._gametype = gametype
self._settings = settings
@ -66,11 +68,18 @@ class Level:
def displayname(self) -> ba.Lstr:
"""The localized name for this Level."""
from ba import _language
return _language.Lstr(
translate=('coopLevelNames', self._displayname
if self._displayname is not None else self._name),
subs=[('${GAME}',
self._gametype.get_display_string(self._settings))])
translate=(
'coopLevelNames',
self._displayname
if self._displayname is not None
else self._name,
),
subs=[
('${GAME}', self._gametype.get_display_string(self._settings))
],
)
@property
def gametype(self) -> type[ba.GameActivity]:
@ -156,10 +165,9 @@ class Level:
if campaign is None:
raise RuntimeError('Level is not in a campaign.')
configdict = campaign.configdict
val: dict[str, Any] = configdict.setdefault(self._name, {
'Rating': 0.0,
'Complete': False
})
val: dict[str, Any] = configdict.setdefault(
self._name, {'Rating': 0.0, 'Complete': False}
)
assert isinstance(val, dict)
return val

View File

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details.
#
"""Implements lobby system for gathering before games, char select, etc."""
# pylint: disable=too-many-lines
from __future__ import annotations
@ -31,15 +32,20 @@ class JoinInfo:
def __init__(self, lobby: ba.Lobby):
from ba._nodeactor import NodeActor
from ba._general import WeakCall
self._state = 0
self._press_to_punch: str | ba.Lstr = ('C' if _ba.app.iircade_mode else
_ba.charstr(
SpecialChar.LEFT_BUTTON))
self._press_to_bomb: str | ba.Lstr = ('B' if _ba.app.iircade_mode else
_ba.charstr(
SpecialChar.RIGHT_BUTTON))
self._press_to_punch: str | ba.Lstr = (
'C'
if _ba.app.iircade_mode
else _ba.charstr(SpecialChar.LEFT_BUTTON)
)
self._press_to_bomb: str | ba.Lstr = (
'B'
if _ba.app.iircade_mode
else _ba.charstr(SpecialChar.RIGHT_BUTTON)
)
self._joinmsg = Lstr(resource='pressAnyButtonToJoinText')
can_switch_teams = (len(lobby.sessionteams) > 1)
can_switch_teams = len(lobby.sessionteams) > 1
# If we have a keyboard, grab keys for punch and pickup.
# FIXME: This of course is only correct on the local device;
@ -50,59 +56,97 @@ class JoinInfo:
flatness = 1.0 if _ba.app.vr_mode else 0.0
self._text = NodeActor(
_ba.newnode('text',
attrs={
'position': (0, -40),
'h_attach': 'center',
'v_attach': 'top',
'h_align': 'center',
'color': (0.7, 0.7, 0.95, 1.0),
'flatness': flatness,
'text': self._joinmsg
}))
_ba.newnode(
'text',
attrs={
'position': (0, -40),
'h_attach': 'center',
'v_attach': 'top',
'h_align': 'center',
'color': (0.7, 0.7, 0.95, 1.0),
'flatness': flatness,
'text': self._joinmsg,
},
)
)
if _ba.app.demo_mode or _ba.app.arcade_mode:
self._messages = [self._joinmsg]
else:
msg1 = Lstr(resource='pressToSelectProfileText',
subs=[
('${BUTTONS}', _ba.charstr(SpecialChar.UP_ARROW) +
' ' + _ba.charstr(SpecialChar.DOWN_ARROW))
])
msg2 = Lstr(resource='pressToOverrideCharacterText',
subs=[('${BUTTONS}', Lstr(resource='bombBoldText'))])
msg3 = Lstr(value='${A} < ${B} >',
subs=[('${A}', msg2), ('${B}', self._press_to_bomb)])
self._messages = (([
Lstr(
resource='pressToSelectTeamText',
subs=[('${BUTTONS}', _ba.charstr(SpecialChar.LEFT_ARROW) +
' ' + _ba.charstr(SpecialChar.RIGHT_ARROW))],
msg1 = Lstr(
resource='pressToSelectProfileText',
subs=[
(
'${BUTTONS}',
_ba.charstr(SpecialChar.UP_ARROW)
+ ' '
+ _ba.charstr(SpecialChar.DOWN_ARROW),
)
],
)
msg2 = Lstr(
resource='pressToOverrideCharacterText',
subs=[('${BUTTONS}', Lstr(resource='bombBoldText'))],
)
msg3 = Lstr(
value='${A} < ${B} >',
subs=[('${A}', msg2), ('${B}', self._press_to_bomb)],
)
self._messages = (
(
[
Lstr(
resource='pressToSelectTeamText',
subs=[
(
'${BUTTONS}',
_ba.charstr(SpecialChar.LEFT_ARROW)
+ ' '
+ _ba.charstr(SpecialChar.RIGHT_ARROW),
)
],
)
]
if can_switch_teams
else []
)
] if can_switch_teams else []) + [msg1] + [msg3] + [self._joinmsg])
+ [msg1]
+ [msg3]
+ [self._joinmsg]
)
self._timer = _ba.Timer(4.0, WeakCall(self._update), repeat=True)
def _update_for_keyboard(self, keyboard: ba.InputDevice) -> None:
from ba import _input
punch_key = keyboard.get_button_name(
_input.get_device_value(keyboard, 'buttonPunch'))
self._press_to_punch = Lstr(resource='orText',
subs=[('${A}',
Lstr(value='\'${K}\'',
subs=[('${K}', punch_key)])),
('${B}', self._press_to_punch)])
_input.get_device_value(keyboard, 'buttonPunch')
)
self._press_to_punch = Lstr(
resource='orText',
subs=[
('${A}', Lstr(value='\'${K}\'', subs=[('${K}', punch_key)])),
('${B}', self._press_to_punch),
],
)
bomb_key = keyboard.get_button_name(
_input.get_device_value(keyboard, 'buttonBomb'))
self._press_to_bomb = Lstr(resource='orText',
subs=[('${A}',
Lstr(value='\'${K}\'',
subs=[('${K}', bomb_key)])),
('${B}', self._press_to_bomb)])
self._joinmsg = Lstr(value='${A} < ${B} >',
subs=[('${A}',
Lstr(resource='pressPunchToJoinText')),
('${B}', self._press_to_punch)])
_input.get_device_value(keyboard, 'buttonBomb')
)
self._press_to_bomb = Lstr(
resource='orText',
subs=[
('${A}', Lstr(value='\'${K}\'', subs=[('${K}', bomb_key)])),
('${B}', self._press_to_bomb),
],
)
self._joinmsg = Lstr(
value='${A} < ${B} >',
subs=[
('${A}', Lstr(resource='pressPunchToJoinText')),
('${B}', self._press_to_punch),
],
)
def _update(self) -> None:
assert self._text.node
@ -113,12 +157,14 @@ class JoinInfo:
@dataclass
class PlayerReadyMessage:
"""Tells an object a player has been selected from the given chooser."""
chooser: ba.Chooser
@dataclass
class ChangeMessage:
"""Tells an object that a selection is being changed."""
what: str
value: int
@ -135,8 +181,9 @@ class Chooser:
if self._text_node:
self._text_node.delete()
def __init__(self, vpos: float, sessionplayer: _ba.SessionPlayer,
lobby: 'Lobby') -> None:
def __init__(
self, vpos: float, sessionplayer: _ba.SessionPlayer, lobby: 'Lobby'
) -> None:
self._deek_sound = _ba.getsound('deek')
self._click_sound = _ba.getsound('click01')
self._punchsound = _ba.getsound('punch01')
@ -170,48 +217,54 @@ class Chooser:
# for the '_random' profile. Let's use their input_device id to seed
# it. This will give a persistent character for them between games
# and will distribute characters nicely if everyone is random.
self._random_color, self._random_highlight = (
get_player_profile_colors(None))
self._random_color, self._random_highlight = get_player_profile_colors(
None
)
# To calc our random character we pick a random one out of our
# unlocked list and then locate that character's index in the full
# list.
char_index_offset = app.lobby_random_char_index_offset
self._random_character_index = (
(sessionplayer.inputdevice.id + char_index_offset) %
len(self._character_names))
sessionplayer.inputdevice.id + char_index_offset
) % len(self._character_names)
# Attempt to set an initial profile based on what was used previously
# for this input-device, etc.
self._profileindex = self._select_initial_profile()
self._profilename = self._profilenames[self._profileindex]
self._text_node = _ba.newnode('text',
delegate=self,
attrs={
'position': (-100, self._vpos),
'maxwidth': 160,
'shadow': 0.5,
'vr_depth': -20,
'h_align': 'left',
'v_align': 'center',
'v_attach': 'top'
})
self._text_node = _ba.newnode(
'text',
delegate=self,
attrs={
'position': (-100, self._vpos),
'maxwidth': 160,
'shadow': 0.5,
'vr_depth': -20,
'h_align': 'left',
'v_align': 'center',
'v_attach': 'top',
},
)
animate(self._text_node, 'scale', {0: 0, 0.1: 1.0})
self.icon = _ba.newnode('image',
owner=self._text_node,
attrs={
'position': (-130, self._vpos + 20),
'mask_texture': self._mask_texture,
'vr_depth': -10,
'attach': 'topCenter'
})
self.icon = _ba.newnode(
'image',
owner=self._text_node,
attrs={
'position': (-130, self._vpos + 20),
'mask_texture': self._mask_texture,
'vr_depth': -10,
'attach': 'topCenter',
},
)
animate_array(self.icon, 'scale', 2, {0: (0, 0), 0.1: (45, 45)})
# Set our initial name to '<choosing player>' in case anyone asks.
self._sessionplayer.setname(
Lstr(resource='choosingPlayerText').evaluate(), real=False)
Lstr(resource='choosingPlayerText').evaluate(), real=False
)
# Init these to our rando but they should get switched to the
# selected profile (if any) right after.
@ -232,32 +285,40 @@ class Chooser:
# If we've got a set profile name for this device, work backwards
# from that to get our index.
dprofilename = (app.config.get('Default Player Profiles',
{}).get(inputdevice.name + ' ' +
inputdevice.unique_identifier))
dprofilename = app.config.get('Default Player Profiles', {}).get(
inputdevice.name + ' ' + inputdevice.unique_identifier
)
if dprofilename is not None and dprofilename in profilenames:
# If we got '__account__' and its local and we haven't marked
# anyone as the 'account profile' device yet, mark this guy as
# it. (prevents the next joiner from getting the account
# profile too).
if (dprofilename == '__account__'
and not inputdevice.is_remote_client
and app.lobby_account_profile_device_id is None):
if (
dprofilename == '__account__'
and not inputdevice.is_remote_client
and app.lobby_account_profile_device_id is None
):
app.lobby_account_profile_device_id = inputdevice.id
return profilenames.index(dprofilename)
# We want to mark the first local input-device in the game
# as the 'account profile' device.
if (not inputdevice.is_remote_client
and not inputdevice.is_controller_app):
if (app.lobby_account_profile_device_id is None
and '__account__' in profilenames):
if (
not inputdevice.is_remote_client
and not inputdevice.is_controller_app
):
if (
app.lobby_account_profile_device_id is None
and '__account__' in profilenames
):
app.lobby_account_profile_device_id = inputdevice.id
# If this is the designated account-profile-device, try to default
# to the account profile.
if (inputdevice.id == app.lobby_account_profile_device_id
and '__account__' in profilenames):
if (
inputdevice.id == app.lobby_account_profile_device_id
and '__account__' in profilenames
):
return profilenames.index('__account__')
# If this is the controller app, it defaults to using a random
@ -274,9 +335,13 @@ class Chooser:
# Cycle through our non-random profiles once; after
# that, everyone gets random.
while (app.lobby_random_profile_index < len(profilenames)
and profilenames[app.lobby_random_profile_index]
in ('_random', '__account__', '_edit')):
while app.lobby_random_profile_index < len(
profilenames
) and profilenames[app.lobby_random_profile_index] in (
'_random',
'__account__',
'_edit',
):
app.lobby_random_profile_index += 1
if app.lobby_random_profile_index < len(profilenames):
profileindex = app.lobby_random_profile_index
@ -340,18 +405,22 @@ class Chooser:
# character to others they own, but profile characters
# should work (and we validate profiles on the master server
# so no exploit opportunities)
if (character not in self._character_names
and character in _ba.app.spaz_appearances):
if (
character not in self._character_names
and character in _ba.app.spaz_appearances
):
self._character_names.append(character)
self._character_index = self._character_names.index(character)
self._color, self._highlight = (get_player_profile_colors(
self._profilename, profiles=self._profiles))
self._color, self._highlight = get_player_profile_colors(
self._profilename, profiles=self._profiles
)
self._update_icon()
self._update_text()
def reload_profiles(self) -> None:
"""Reload all player profiles."""
from ba._general import json_prep
app = _ba.app
# Re-construct our profile index and other stuff since the profile
@ -396,8 +465,11 @@ class Chooser:
# For local devices, add it an 'edit' option which will pop up
# the profile window.
if not is_remote and not is_test_input and not (app.demo_mode
or app.arcade_mode):
if (
not is_remote
and not is_test_input
and not (app.demo_mode or app.arcade_mode)
):
self._profiles['_edit'] = {}
# Build a sorted name list we can iterate through.
@ -417,18 +489,25 @@ class Chooser:
assert self._text_node
spacing = 350
sessionteams = self.lobby.sessionteams
offs = (spacing * -0.5 * len(sessionteams) +
spacing * self._selected_team_index + 250)
offs = (
spacing * -0.5 * len(sessionteams)
+ spacing * self._selected_team_index
+ 250
)
if len(sessionteams) > 1:
offs -= 35
animate_array(self._text_node, 'position', 2, {
0: self._text_node.position,
0.1: (-100 + offs, self._vpos + 23)
})
animate_array(self.icon, 'position', 2, {
0: self.icon.position,
0.1: (-130 + offs, self._vpos + 22)
})
animate_array(
self._text_node,
'position',
2,
{0: self._text_node.position, 0.1: (-100 + offs, self._vpos + 23)},
)
animate_array(
self.icon,
'position',
2,
{0: self.icon.position, 0.1: (-130 + offs, self._vpos + 22)},
)
def get_character_name(self) -> str:
"""Return the selected character name."""
@ -442,16 +521,14 @@ class Chooser:
clamp = False
if name == '_random':
try:
name = (
self._sessionplayer.inputdevice.get_default_player_name())
name = self._sessionplayer.inputdevice.get_default_player_name()
except Exception:
print_exception('Error getting _random chooser name.')
name = 'Invalid'
clamp = not full
elif name == '__account__':
try:
name = self._sessionplayer.inputdevice.get_v1_account_name(
full)
name = self._sessionplayer.inputdevice.get_v1_account_name(full)
except Exception:
print_exception('Error getting account name for chooser.')
name = 'Invalid'
@ -459,18 +536,21 @@ class Chooser:
elif name == '_edit':
# Explicitly flattening this to a str; it's only relevant on
# the host so that's ok.
name = (Lstr(
name = Lstr(
resource='createEditPlayerText',
fallback_resource='editProfileWindow.titleNewText').evaluate())
fallback_resource='editProfileWindow.titleNewText',
).evaluate()
else:
# If we have a regular profile marked as global with an icon,
# use it (for full only).
if full:
try:
if self._profiles[name_raw].get('global', False):
icon = (self._profiles[name_raw]['icon']
if 'icon' in self._profiles[name_raw] else
_ba.charstr(SpecialChar.LOGO))
icon = (
self._profiles[name_raw]['icon']
if 'icon' in self._profiles[name_raw]
else _ba.charstr(SpecialChar.LOGO)
)
name = icon + name
except Exception:
print_exception('Error applying global icon.')
@ -488,6 +568,7 @@ class Chooser:
# pylint: disable=cyclic-import
from bastd.ui.profile import browser as pbrowser
from ba._general import Call
profilename = self._profilenames[self._profileindex]
# Handle '_edit' as a special case.
@ -503,50 +584,71 @@ class Chooser:
if not ready:
self._sessionplayer.assigninput(
InputType.LEFT_PRESS,
Call(self.handlemessage, ChangeMessage('team', -1)))
Call(self.handlemessage, ChangeMessage('team', -1)),
)
self._sessionplayer.assigninput(
InputType.RIGHT_PRESS,
Call(self.handlemessage, ChangeMessage('team', 1)))
Call(self.handlemessage, ChangeMessage('team', 1)),
)
self._sessionplayer.assigninput(
InputType.BOMB_PRESS,
Call(self.handlemessage, ChangeMessage('character', 1)))
Call(self.handlemessage, ChangeMessage('character', 1)),
)
self._sessionplayer.assigninput(
InputType.UP_PRESS,
Call(self.handlemessage, ChangeMessage('profileindex', -1)))
Call(self.handlemessage, ChangeMessage('profileindex', -1)),
)
self._sessionplayer.assigninput(
InputType.DOWN_PRESS,
Call(self.handlemessage, ChangeMessage('profileindex', 1)))
Call(self.handlemessage, ChangeMessage('profileindex', 1)),
)
self._sessionplayer.assigninput(
(InputType.JUMP_PRESS, InputType.PICK_UP_PRESS,
InputType.PUNCH_PRESS),
Call(self.handlemessage, ChangeMessage('ready', 1)))
(
InputType.JUMP_PRESS,
InputType.PICK_UP_PRESS,
InputType.PUNCH_PRESS,
),
Call(self.handlemessage, ChangeMessage('ready', 1)),
)
self._ready = False
self._update_text()
self._sessionplayer.setname('untitled', real=False)
else:
self._sessionplayer.assigninput(
(InputType.LEFT_PRESS, InputType.RIGHT_PRESS,
InputType.UP_PRESS, InputType.DOWN_PRESS,
InputType.JUMP_PRESS, InputType.BOMB_PRESS,
InputType.PICK_UP_PRESS), self._do_nothing)
(
InputType.LEFT_PRESS,
InputType.RIGHT_PRESS,
InputType.UP_PRESS,
InputType.DOWN_PRESS,
InputType.JUMP_PRESS,
InputType.BOMB_PRESS,
InputType.PICK_UP_PRESS,
),
self._do_nothing,
)
self._sessionplayer.assigninput(
(InputType.JUMP_PRESS, InputType.BOMB_PRESS,
InputType.PICK_UP_PRESS, InputType.PUNCH_PRESS),
Call(self.handlemessage, ChangeMessage('ready', 0)))
(
InputType.JUMP_PRESS,
InputType.BOMB_PRESS,
InputType.PICK_UP_PRESS,
InputType.PUNCH_PRESS,
),
Call(self.handlemessage, ChangeMessage('ready', 0)),
)
# Store the last profile picked by this input for reuse.
input_device = self._sessionplayer.inputdevice
name = input_device.name
unique_id = input_device.unique_identifier
device_profiles = _ba.app.config.setdefault(
'Default Player Profiles', {})
'Default Player Profiles', {}
)
# Make an exception if we have no custom profiles and are set
# to random; in that case we'll want to start picking up custom
# profiles if/when one is made so keep our setting cleared.
special = ('_random', '_edit', '__account__')
have_custom_profiles = any(p not in special
for p in self._profiles)
have_custom_profiles = any(p not in special for p in self._profiles)
profilekey = name + ' ' + unique_id
if profilename == '_random' and not have_custom_profiles:
@ -557,9 +659,9 @@ class Chooser:
_ba.app.config.commit()
# Set this player's short and full name.
self._sessionplayer.setname(self._getname(),
self._getname(full=True),
real=True)
self._sessionplayer.setname(
self._getname(), self._getname(full=True), real=True
)
self._ready = True
self._update_text()
@ -583,18 +685,21 @@ class Chooser:
team_player_counts = {}
for sessionteam in sessionteams:
team_player_counts[sessionteam.id] = len(
sessionteam.players)
sessionteam.players
)
for chooser in lobby.choosers:
if chooser.ready:
team_player_counts[chooser.sessionteam.id] += 1
largest_team_size = max(team_player_counts.values())
smallest_team_size = (min(team_player_counts.values()))
smallest_team_size = min(team_player_counts.values())
# Force switch if we're on the biggest sessionteam
# and there's a smaller one available.
if (largest_team_size != smallest_team_size
and team_player_counts[self.sessionteam.id] >=
largest_team_size):
if (
largest_team_size != smallest_team_size
and team_player_counts[self.sessionteam.id]
>= largest_team_size
):
force_team_switch = True
# Either force switch teams, or actually for realsies do the set-ready.
@ -612,8 +717,7 @@ class Chooser:
if now - self._last_change[0] < QUICK_CHANGE_INTERVAL:
count += 1
if count > MAX_QUICK_CHANGE_COUNT:
_ba.disconnect_client(
self._sessionplayer.inputdevice.client_id)
_ba.disconnect_client(self._sessionplayer.inputdevice.client_id)
elif now - self._last_change[0] > QUICK_CHANGE_RESET_INTERVAL:
count = 0
self._last_change = (now, count)
@ -638,8 +742,8 @@ class Chooser:
if len(sessionteams) > 1:
_ba.playsound(self._swish_sound)
self._selected_team_index = (
(self._selected_team_index + msg.value) %
len(sessionteams))
self._selected_team_index + msg.value
) % len(sessionteams)
self._update_text()
self.update_position()
self._update_icon()
@ -655,15 +759,17 @@ class Chooser:
# Pick the next player profile and assign our name
# and character based on that.
_ba.playsound(self._deek_sound)
self._profileindex = ((self._profileindex + msg.value) %
len(self._profilenames))
self._profileindex = (self._profileindex + msg.value) % len(
self._profilenames
)
self.update_from_profile()
elif msg.what == 'character':
_ba.playsound(self._click_sound)
# update our index in our local list of characters
self._character_index = ((self._character_index + msg.value) %
len(self._character_names))
self._character_index = (
self._character_index + msg.value
) % len(self._character_names)
self._update_text()
self._update_icon()
@ -677,30 +783,34 @@ class Chooser:
# Once we're ready, we've saved the name, so lets ask the system
# for it so we get appended numbers and stuff.
text = Lstr(value=self._sessionplayer.getname(full=True))
text = Lstr(value='${A} (${B})',
subs=[('${A}', text),
('${B}', Lstr(resource='readyText'))])
text = Lstr(
value='${A} (${B})',
subs=[('${A}', text), ('${B}', Lstr(resource='readyText'))],
)
else:
text = Lstr(value=self._getname(full=True))
can_switch_teams = len(self.lobby.sessionteams) > 1
# Flash as we're coming in.
fin_color = _ba.safecolor(self.get_color()) + (1, )
fin_color = _ba.safecolor(self.get_color()) + (1,)
if not self._inited:
animate_array(self._text_node, 'color', 4, {
0.15: fin_color,
0.25: (2, 2, 2, 1),
0.35: fin_color
})
animate_array(
self._text_node,
'color',
4,
{0.15: fin_color, 0.25: (2, 2, 2, 1), 0.35: fin_color},
)
else:
# Blend if we're in teams mode; switch instantly otherwise.
if can_switch_teams:
animate_array(self._text_node, 'color', 4, {
0: self._text_node.color,
0.1: fin_color
})
animate_array(
self._text_node,
'color',
4,
{0: self._text_node.color, 0.1: fin_color},
)
else:
self._text_node.color = fin_color
@ -740,9 +850,11 @@ class Chooser:
max_val = sessionteam.color[j]
max_index = j
that_color_for_us = highlight[max_index]
our_second_biggest = max(highlight[(max_index + 1) % 3],
highlight[(max_index + 2) % 3])
diff = (that_color_for_us - our_second_biggest)
our_second_biggest = max(
highlight[(max_index + 1) % 3],
highlight[(max_index + 2) % 3],
)
diff = that_color_for_us - our_second_biggest
if diff > 0:
highlight[max_index] -= diff * 0.6
highlight[(max_index + 1) % 3] += diff * 0.3
@ -764,10 +876,12 @@ class Chooser:
return
try:
tex_name = (_ba.app.spaz_appearances[self._character_names[
self._character_index]].icon_texture)
tint_tex_name = (_ba.app.spaz_appearances[self._character_names[
self._character_index]].icon_mask_texture)
tex_name = _ba.app.spaz_appearances[
self._character_names[self._character_index]
].icon_texture
tint_tex_name = _ba.app.spaz_appearances[
self._character_names[self._character_index]
].icon_mask_texture
except Exception:
print_exception('Error updating char icon list')
tex_name = 'neoSpazIcon'
@ -786,18 +900,18 @@ class Chooser:
# If we're initing, flash.
if not self._inited:
animate_array(self.icon, 'color', 3, {
0.15: (1, 1, 1),
0.25: (2, 2, 2),
0.35: (1, 1, 1)
})
animate_array(
self.icon,
'color',
3,
{0.15: (1, 1, 1), 0.25: (2, 2, 2), 0.35: (1, 1, 1)},
)
# Blend in teams mode; switch instantly in ffa-mode.
if can_switch_teams:
animate_array(self.icon, 'tint_color', 3, {
0: self.icon.tint_color,
0.1: clr
})
animate_array(
self.icon, 'tint_color', 3, {0: self.icon.tint_color, 0.1: clr}
)
else:
self.icon.tint_color = clr
self.icon.tint2_color = clr2
@ -825,6 +939,7 @@ class Lobby:
def __init__(self) -> None:
from ba._team import SessionTeam
from ba._coopsession import CoopSession
session = _ba.getsession()
self._use_team_colors = session.use_team_colors
if session.use_teams:
@ -834,7 +949,7 @@ class Lobby:
else:
self._dummy_teams = SessionTeam()
self._sessionteams = [weakref.ref(self._dummy_teams)]
v_offset = (-150 if isinstance(session, CoopSession) else -50)
v_offset = -150 if isinstance(session, CoopSession) else -50
self.choosers: list[Chooser] = []
self.base_v_offset = v_offset
self.update_positions()
@ -916,9 +1031,11 @@ class Lobby:
def add_chooser(self, sessionplayer: ba.SessionPlayer) -> None:
"""Add a chooser to the lobby for the provided player."""
self.choosers.append(
Chooser(vpos=self._vpos, sessionplayer=sessionplayer, lobby=self))
Chooser(vpos=self._vpos, sessionplayer=sessionplayer, lobby=self)
)
self._next_add_team = (self._next_add_team + 1) % len(
self._sessionteams)
self._sessionteams
)
self._vpos -= 48
def remove_chooser(self, player: ba.SessionPlayer) -> None:

View File

@ -49,6 +49,7 @@ def get_map_display_string(name: str) -> ba.Lstr:
Category: **Asset Functions**
"""
from ba import _language
return _language.Lstr(translate=('mapsNames', name))
@ -97,8 +98,11 @@ def getmaps(playtype: str) -> list[str]:
For racing games where players much touch each region in order.
Has two or more 'race_point' locations.
"""
return sorted(key for key, val in _ba.app.maps.items()
if playtype in val.get_play_types())
return sorted(
key
for key, val in _ba.app.maps.items()
if playtype in val.get_play_types()
)
def get_map_class(name: str) -> type[ba.Map]:
@ -111,6 +115,7 @@ def get_map_class(name: str) -> type[ba.Map]:
return _ba.app.maps[name]
except KeyError:
from ba import _error
raise _error.NotFoundError(f"Map not found: '{name}'") from None
@ -122,6 +127,7 @@ class Map(Actor):
Consists of a collection of terrain nodes, metadata, and other
functionality comprising a game map.
"""
defs: Any = None
name = 'Map'
_playtypes: list[str] = []
@ -170,8 +176,9 @@ class Map(Actor):
"""
return None
def __init__(self,
vr_overlay_offset: Sequence[float] | None = None) -> None:
def __init__(
self, vr_overlay_offset: Sequence[float] | None = None
) -> None:
"""Instantiate a map."""
super().__init__()
@ -185,10 +192,13 @@ class Map(Actor):
self.preloaddata = _ba.getactivity().preloads[type(self)]
except Exception as exc:
from ba import _error
raise _error.NotFoundError(
'Preload data not found for ' + str(type(self)) +
'; make sure to call the type\'s preload()'
' staticmethod in the activity constructor') from exc
'Preload data not found for '
+ str(type(self))
+ '; make sure to call the type\'s preload()'
' staticmethod in the activity constructor'
) from exc
# Set various globals.
gnode = _ba.getactivity().globalsnode
@ -210,9 +220,12 @@ class Map(Actor):
# Set shadow ranges.
try:
gnode.shadow_range = [
self.defs.points[v][1] for v in [
'shadow_lower_bottom', 'shadow_lower_top',
'shadow_upper_bottom', 'shadow_upper_top'
self.defs.points[v][1]
for v in [
'shadow_lower_bottom',
'shadow_lower_top',
'shadow_upper_bottom',
'shadow_upper_top',
]
]
except Exception:
@ -220,36 +233,42 @@ class Map(Actor):
# In vr, set a fixed point in space for the overlay to show up at.
# By default we use the bounds center but allow the map to override it.
center = ((aoi_bounds[0] + aoi_bounds[3]) * 0.5,
(aoi_bounds[1] + aoi_bounds[4]) * 0.5,
(aoi_bounds[2] + aoi_bounds[5]) * 0.5)
center = (
(aoi_bounds[0] + aoi_bounds[3]) * 0.5,
(aoi_bounds[1] + aoi_bounds[4]) * 0.5,
(aoi_bounds[2] + aoi_bounds[5]) * 0.5,
)
if vr_overlay_offset is not None:
center = (center[0] + vr_overlay_offset[0],
center[1] + vr_overlay_offset[1],
center[2] + vr_overlay_offset[2])
center = (
center[0] + vr_overlay_offset[0],
center[1] + vr_overlay_offset[1],
center[2] + vr_overlay_offset[2],
)
gnode.vr_overlay_center = center
gnode.vr_overlay_center_enabled = True
self.spawn_points = (self.get_def_points('spawn')
or [(0, 0, 0, 0, 0, 0)])
self.ffa_spawn_points = (self.get_def_points('ffa_spawn')
or [(0, 0, 0, 0, 0, 0)])
self.spawn_by_flag_points = (self.get_def_points('spawn_by_flag')
or [(0, 0, 0, 0, 0, 0)])
self.spawn_points = self.get_def_points('spawn') or [(0, 0, 0, 0, 0, 0)]
self.ffa_spawn_points = self.get_def_points('ffa_spawn') or [
(0, 0, 0, 0, 0, 0)
]
self.spawn_by_flag_points = self.get_def_points('spawn_by_flag') or [
(0, 0, 0, 0, 0, 0)
]
self.flag_points = self.get_def_points('flag') or [(0, 0, 0)]
# We just want points.
self.flag_points = [p[:3] for p in self.flag_points]
self.flag_points_default = (self.get_def_point('flag_default')
or (0, 1, 0))
self.flag_points_default = self.get_def_point('flag_default') or (
0,
1,
0,
)
self.powerup_spawn_points = self.get_def_points('powerup_spawn') or [
(0, 0, 0)
]
# We just want points.
self.powerup_spawn_points = ([
p[:3] for p in self.powerup_spawn_points
])
self.powerup_spawn_points = [p[:3] for p in self.powerup_spawn_points]
self.tnt_points = self.get_def_points('tnt') or []
# We just want points.
@ -262,11 +281,10 @@ class Map(Actor):
# Let's select random index for first spawn point,
# so that no one is offended by the constant spawn on the edge.
self._next_ffa_start_index = random.randrange(
len(self.ffa_spawn_points))
len(self.ffa_spawn_points)
)
def is_point_near_edge(self,
point: ba.Vec3,
running: bool = False) -> bool:
def is_point_near_edge(self, point: ba.Vec3, running: bool = False) -> bool:
"""Return whether the provided point is near an edge of the map.
Simple bot logic uses this call to determine if they
@ -278,22 +296,32 @@ class Map(Actor):
return False
def get_def_bound_box(
self, name: str
self, name: str
) -> tuple[float, float, float, float, float, float] | None:
"""Return a 6 member bounds tuple or None if it is not defined."""
try:
box = self.defs.boxes[name]
return (box[0] - box[6] / 2.0, box[1] - box[7] / 2.0,
box[2] - box[8] / 2.0, box[0] + box[6] / 2.0,
box[1] + box[7] / 2.0, box[2] + box[8] / 2.0)
return (
box[0] - box[6] / 2.0,
box[1] - box[7] / 2.0,
box[2] - box[8] / 2.0,
box[0] + box[6] / 2.0,
box[1] + box[7] / 2.0,
box[2] + box[8] / 2.0,
)
except Exception:
return None
def get_def_point(self, name: str) -> Sequence[float] | None:
"""Return a single defined point or a default value in its absence."""
val = self.defs.points.get(name)
return (None if val is None else
_math.vec3validate(val) if __debug__ else val)
return (
None
if val is None
else _math.vec3validate(val)
if __debug__
else val
)
def get_def_points(self, name: str) -> list[Sequence[float]]:
"""Return a list of named points.
@ -320,12 +348,16 @@ class Map(Actor):
pnt = self.spawn_points[team_index % len(self.spawn_points)]
x_range = (-0.5, 0.5) if pnt[3] == 0.0 else (-pnt[3], pnt[3])
z_range = (-0.5, 0.5) if pnt[5] == 0.0 else (-pnt[5], pnt[5])
pnt = (pnt[0] + random.uniform(*x_range), pnt[1],
pnt[2] + random.uniform(*z_range))
pnt = (
pnt[0] + random.uniform(*x_range),
pnt[1],
pnt[2] + random.uniform(*z_range),
)
return pnt
def get_ffa_start_position(
self, players: Sequence[ba.Player]) -> Sequence[float]:
self, players: Sequence[ba.Player]
) -> Sequence[float]:
"""Return a random starting position in one of the FFA spawn areas.
If a list of ba.Player-s is provided; the returned points will be
@ -340,12 +372,16 @@ class Map(Actor):
def _getpt() -> Sequence[float]:
point = self.ffa_spawn_points[self._next_ffa_start_index]
self._next_ffa_start_index = ((self._next_ffa_start_index + 1) %
len(self.ffa_spawn_points))
self._next_ffa_start_index = (self._next_ffa_start_index + 1) % len(
self.ffa_spawn_points
)
x_range = (-0.5, 0.5) if point[3] == 0.0 else (-point[3], point[3])
z_range = (-0.5, 0.5) if point[5] == 0.0 else (-point[5], point[5])
point = (point[0] + random.uniform(*x_range), point[1],
point[2] + random.uniform(*z_range))
point = (
point[0] + random.uniform(*x_range),
point[1],
point[2] + random.uniform(*z_range),
)
return point
if not player_pts:
@ -368,8 +404,9 @@ class Map(Actor):
assert farthestpt is not None
return tuple(farthestpt)
def get_flag_position(self,
team_index: int | None = None) -> Sequence[float]:
def get_flag_position(
self, team_index: int | None = None
) -> Sequence[float]:
"""Return a flag position on the map for the given team index.
Pass None to get the default flag point.
@ -384,6 +421,7 @@ class Map(Actor):
def handlemessage(self, msg: Any) -> Any:
from ba import _messages
if isinstance(msg, _messages.DieMessage):
if self.node:
self.node.delete()

View File

@ -24,6 +24,7 @@ def vec3validate(value: Sequence[float]) -> Sequence[float]:
to keep runtime overhead minimal.
"""
from numbers import Number
if not isinstance(value, abc.Sequence):
raise TypeError(f"Expected a sequence; got {type(value)}")
if len(value) != 3:
@ -40,9 +41,11 @@ def is_point_in_box(pnt: Sequence[float], box: Sequence[float]) -> bool:
For use with standard def boxes (position|rotate|scale).
"""
return ((abs(pnt[0] - box[0]) <= box[6] * 0.5)
and (abs(pnt[1] - box[1]) <= box[7] * 0.5)
and (abs(pnt[2] - box[2]) <= box[8] * 0.5))
return (
(abs(pnt[0] - box[0]) <= box[6] * 0.5)
and (abs(pnt[1] - box[1]) <= box[7] * 0.5)
and (abs(pnt[2] - box[2]) <= box[8] * 0.5)
)
def normalized_color(color: Sequence[float]) -> tuple[float, ...]:

View File

@ -38,6 +38,7 @@ class DeathType(Enum):
Category: Enums
"""
GENERIC = 'generic'
OUT_OF_BOUNDS = 'out_of_bounds'
IMPACT = 'impact'
@ -83,8 +84,13 @@ class PlayerDiedMessage:
how: ba.DeathType
"""The particular type of death."""
def __init__(self, player: ba.Player, was_killed: bool,
killerplayer: ba.Player | None, how: ba.DeathType):
def __init__(
self,
player: ba.Player,
was_killed: bool,
killerplayer: ba.Player | None,
how: ba.DeathType,
):
"""Instantiate a message with the given values."""
# Invalid refs should never be passed as args.
@ -97,8 +103,9 @@ class PlayerDiedMessage:
self.killed = was_killed
self.how = how
def getkillerplayer(self,
playertype: type[PlayerType]) -> PlayerType | None:
def getkillerplayer(
self, playertype: type[PlayerType]
) -> PlayerType | None:
"""Return the ba.Player responsible for the killing, if any.
Pass the Player type being used by the current game.
@ -235,19 +242,21 @@ class HitMessage:
their effect to a target.
"""
def __init__(self,
srcnode: ba.Node | None = None,
pos: Sequence[float] | None = None,
velocity: Sequence[float] | None = None,
magnitude: float = 1.0,
velocity_magnitude: float = 0.0,
radius: float = 1.0,
source_player: ba.Player | None = None,
kick_back: float = 1.0,
flat_damage: float | None = None,
hit_type: str = 'generic',
force_direction: Sequence[float] | None = None,
hit_subtype: str = 'default'):
def __init__(
self,
srcnode: ba.Node | None = None,
pos: Sequence[float] | None = None,
velocity: Sequence[float] | None = None,
magnitude: float = 1.0,
velocity_magnitude: float = 0.0,
radius: float = 1.0,
source_player: ba.Player | None = None,
kick_back: float = 1.0,
flat_damage: float | None = None,
hit_type: str = 'generic',
force_direction: Sequence[float] | None = None,
hit_subtype: str = 'default',
):
"""Instantiate a message with given values."""
self.srcnode = srcnode
@ -264,11 +273,13 @@ class HitMessage:
self.flat_damage = flat_damage
self.hit_type = hit_type
self.hit_subtype = hit_subtype
self.force_direction = (force_direction
if force_direction is not None else velocity)
self.force_direction = (
force_direction if force_direction is not None else velocity
)
def get_source_player(self,
playertype: type[PlayerType]) -> PlayerType | None:
def get_source_player(
self, playertype: type[PlayerType]
) -> PlayerType | None:
"""Return the source-player if one exists and is the provided type."""
player: Any = self._source_player

View File

@ -39,6 +39,7 @@ T = TypeVar('T')
@dataclass
class ScanResults:
"""Final results from a meta-scan."""
exports: dict[str, list[str]] = field(default_factory=dict)
errors: list[str] = field(default_factory=list)
warnings: list[str] = field(default_factory=list)
@ -80,7 +81,8 @@ class MetadataSubsystem:
self._scan_complete_cb = scan_complete_cb
self._scan = DirectoryScan(
[_ba.app.python_directory_app, _ba.app.python_directory_user])
[_ba.app.python_directory_app, _ba.app.python_directory_user]
)
Thread(target=self._run_scan_in_bg, daemon=True).start()
@ -111,8 +113,12 @@ class MetadataSubsystem:
loading work happens, pass completion_cb_in_bg_thread=True.
"""
Thread(
target=tpartial(self._load_exported_classes, cls, completion_cb,
completion_cb_in_bg_thread),
target=tpartial(
self._load_exported_classes,
cls,
completion_cb,
completion_cb_in_bg_thread,
),
daemon=True,
).start()
@ -123,6 +129,7 @@ class MetadataSubsystem:
completion_cb_in_bg_thread: bool,
) -> None:
from ba._general import getclass
classes: list[type[T]] = []
try:
classnames = self._wait_for_scan_results().exports_of_class(cls)
@ -148,7 +155,8 @@ class MetadataSubsystem:
logging.warning(
'ba.meta._wait_for_scan_results()'
' called in logic thread before scan completed;'
' this can cause hitches.')
' this can cause hitches.'
)
# Now wait a bit for the scan to complete.
# Eventually error though if it doesn't.
@ -157,7 +165,8 @@ class MetadataSubsystem:
time.sleep(0.05)
if time.time() - starttime > 10.0:
raise TimeoutError(
'timeout waiting for meta scan to complete.')
'timeout waiting for meta scan to complete.'
)
return self.scanresults
def _run_scan_in_bg(self) -> None:
@ -177,6 +186,7 @@ class MetadataSubsystem:
def _handle_scan_results(self) -> None:
"""Called in the logic thread with results of a completed scan."""
from ba._language import Lstr
assert _ba.in_logic_thread()
results = self.scanresults
@ -188,16 +198,20 @@ class MetadataSubsystem:
# Errors are more serious and will get included in the regular log.
if results.warnings or results.errors:
import textwrap
_ba.screenmessage(Lstr(resource='scanScriptsErrorText'),
color=(1, 0, 0))
_ba.screenmessage(
Lstr(resource='scanScriptsErrorText'), color=(1, 0, 0)
)
_ba.playsound(_ba.getsound('error'))
if results.warnings:
allwarnings = textwrap.indent('\n'.join(results.warnings),
'Warning (meta-scan): ')
allwarnings = textwrap.indent(
'\n'.join(results.warnings), 'Warning (meta-scan): '
)
logging.warning(allwarnings)
if results.errors:
allerrors = textwrap.indent('\n'.join(results.errors),
'Error (meta-scan): ')
allerrors = textwrap.indent(
'\n'.join(results.errors), 'Error (meta-scan): '
)
logging.error(allerrors)
# Let the game know we're done.
@ -244,23 +258,28 @@ class DirectoryScan:
self._scan_module(moduledir, subpath)
except Exception:
import traceback
self.results.warnings.append(
f"Error scanning '{subpath}': " +
traceback.format_exc())
f"Error scanning '{subpath}': " + traceback.format_exc()
)
# Sort our results
for exportlist in self.results.exports.values():
exportlist.sort()
def _get_path_module_entries(self, path: Path, subpath: str | Path,
modules: list[tuple[Path, Path]]) -> None:
def _get_path_module_entries(
self, path: Path, subpath: str | Path, modules: list[tuple[Path, Path]]
) -> None:
"""Scan provided path and add module entries to provided list."""
try:
# Special case: let's save some time and skip the whole 'ba'
# package since we know it doesn't contain any meta tags.
fullpath = Path(path, subpath)
entries = [(path, Path(subpath, name))
for name in os.listdir(fullpath) if name != 'ba']
entries = [
(path, Path(subpath, name))
for name in os.listdir(fullpath)
if name != 'ba'
]
except PermissionError:
# Expected sometimes.
entries = []
@ -273,8 +292,10 @@ class DirectoryScan:
for entry in entries:
if entry[1].name.endswith('.py'):
modules.append(entry)
elif (Path(entry[0], entry[1]).is_dir()
and Path(entry[0], entry[1], '__init__.py').is_file()):
elif (
Path(entry[0], entry[1]).is_dir()
and Path(entry[0], entry[1], '__init__.py').is_file()
):
modules.append(entry)
def _scan_module(self, moduledir: Path, subpath: Path) -> None:
@ -289,11 +310,13 @@ class DirectoryScan:
flines = infile.readlines()
meta_lines = {
lnum: l[1:].split()
for lnum, l in enumerate(flines) if '# ba_meta ' in l
for lnum, l in enumerate(flines)
if '# ba_meta ' in l
}
is_top_level = len(subpath.parts) <= 1
required_api = self._get_api_requirement(subpath, meta_lines,
is_top_level)
required_api = self._get_api_requirement(
subpath, meta_lines, is_top_level
)
# Top level modules with no discernible api version get ignored.
if is_top_level and required_api is None:
@ -304,7 +327,8 @@ class DirectoryScan:
if required_api is not None and required_api != CURRENT_API_VERSION:
self.results.warnings.append(
f'Warning: {subpath} requires api {required_api} but'
f' we are running {CURRENT_API_VERSION}; ignoring module.')
f' we are running {CURRENT_API_VERSION}; ignoring module.'
)
return
# Ok; can proceed with a full scan of this module.
@ -320,11 +344,14 @@ class DirectoryScan:
self._scan_module(submodule[0], submodule[1])
except Exception:
import traceback
self.results.warnings.append(
f"Error scanning '{subpath}': {traceback.format_exc()}")
def _process_module_meta_tags(self, subpath: Path, flines: list[str],
meta_lines: dict[int, list[str]]) -> None:
self.results.warnings.append(
f"Error scanning '{subpath}': {traceback.format_exc()}"
)
def _process_module_meta_tags(
self, subpath: Path, flines: list[str], meta_lines: dict[int, list[str]]
) -> None:
"""Pull data from a module based on its ba_meta tags."""
for lindex, mline in meta_lines.items():
# meta_lines is just anything containing '# ba_meta '; make sure
@ -332,9 +359,11 @@ class DirectoryScan:
if mline[0] != 'ba_meta':
self.results.warnings.append(
f'Warning: {subpath}:'
f' malformed ba_meta statement on line {lindex + 1}.')
elif (len(mline) == 4 and mline[1] == 'require'
and mline[2] == 'api'):
f' malformed ba_meta statement on line {lindex + 1}.'
)
elif (
len(mline) == 4 and mline[1] == 'require' and mline[2] == 'api'
):
# Ignore 'require api X' lines in this pass.
pass
elif len(mline) != 3 or mline[1] != 'export':
@ -342,7 +371,8 @@ class DirectoryScan:
# complain for anything else we see.
self.results.warnings.append(
f'Warning: {subpath}'
f': unrecognized ba_meta statement on line {lindex + 1}.')
f': unrecognized ba_meta statement on line {lindex + 1}.'
)
else:
# Looks like we've got a valid export line!
modulename = '.'.join(subpath.parts)
@ -350,7 +380,8 @@ class DirectoryScan:
modulename = modulename[:-3]
exporttypestr = mline[2]
export_class_name = self._get_export_class_name(
subpath, flines, lindex)
subpath, flines, lindex
)
if export_class_name is not None:
classname = modulename + '.' + export_class_name
@ -360,11 +391,13 @@ class DirectoryScan:
exporttype = EXPORT_CLASS_NAME_SHORTCUTS.get(exporttypestr)
if exporttype is None:
exporttype = exporttypestr
self.results.exports.setdefault(exporttype,
[]).append(classname)
self.results.exports.setdefault(exporttype, []).append(
classname
)
def _get_export_class_name(self, subpath: Path, lines: list[str],
lindex: int) -> str | None:
def _get_export_class_name(
self, subpath: Path, lines: list[str], lindex: int
) -> str | None:
"""Given line num of an export tag, returns its operand class name."""
lindexorig = lindex
classname = None
@ -385,7 +418,8 @@ class DirectoryScan:
if classname is None:
self.results.warnings.append(
f'Warning: {subpath}: class definition not found below'
f' "ba_meta export" statement on line {lindexorig + 1}.')
f' "ba_meta export" statement on line {lindexorig + 1}.'
)
return classname
def _get_api_requirement(
@ -399,8 +433,13 @@ class DirectoryScan:
Malformed api requirement strings will be logged as warnings.
"""
lines = [
l for l in meta_lines.values() if len(l) == 4 and l[0] == 'ba_meta'
and l[1] == 'require' and l[2] == 'api' and l[3].isdigit()
l
for l in meta_lines.values()
if len(l) == 4
and l[0] == 'ba_meta'
and l[1] == 'require'
and l[2] == 'api'
and l[3].isdigit()
]
# We're successful if we find exactly one properly formatted line.
@ -412,12 +451,14 @@ class DirectoryScan:
self.results.warnings.append(
f'Warning: {subpath}: multiple'
' "# ba_meta require api <NUM>" lines found;'
' ignoring module.')
' ignoring module.'
)
elif not lines and toplevel and meta_lines:
# If we're a top-level module containing meta lines but
# no valid "require api" line found, complain.
self.results.warnings.append(
f'Warning: {subpath}:'
' no valid "# ba_meta require api <NUM>" line found;'
' ignoring module.')
' ignoring module.'
)
return None

View File

@ -38,6 +38,7 @@ class MultiTeamSession(Session):
# pylint: disable=cyclic-import
from ba import _playlist
from bastd.activity.multiteamjoin import MultiTeamJoinActivity
app = _ba.app
cfg = app.config
@ -51,11 +52,13 @@ class MultiTeamSession(Session):
# print('FIXME: TEAM BASE SESSION WOULD CALC DEPS.')
depsets: Sequence[ba.DependencySet] = []
super().__init__(depsets,
team_names=team_names,
team_colors=team_colors,
min_players=1,
max_players=self.get_max_players())
super().__init__(
depsets,
team_names=team_names,
team_colors=team_colors,
min_players=1,
max_players=self.get_max_players(),
)
self._series_length = app.teams_series_length
self._ffa_series_length = app.ffa_series_length
@ -67,13 +70,13 @@ class MultiTeamSession(Session):
from bastd.tutorial import TutorialActivity
# Get this loading.
self._tutorial_activity_instance = _ba.newactivity(
TutorialActivity)
self._tutorial_activity_instance = _ba.newactivity(TutorialActivity)
else:
self._tutorial_activity_instance = None
self._playlist_name = cfg.get(self._playlist_selection_var,
'__default__')
self._playlist_name = cfg.get(
self._playlist_selection_var, '__default__'
)
self._playlist_randomize = cfg.get(self._playlist_randomize_var, False)
# Which game activity we're on.
@ -81,8 +84,10 @@ class MultiTeamSession(Session):
playlists = cfg.get(self._playlists_var, {})
if (self._playlist_name != '__default__'
and self._playlist_name in playlists):
if (
self._playlist_name != '__default__'
and self._playlist_name in playlists
):
# Make sure to copy this, as we muck with it in place once we've
# got it and we don't want that to affect our config.
@ -98,19 +103,22 @@ class MultiTeamSession(Session):
playlist,
sessiontype=type(self),
add_resolved_type=True,
name='default teams' if self.use_teams else 'default ffa')
name='default teams' if self.use_teams else 'default ffa',
)
if not playlist_resolved:
raise RuntimeError('Playlist contains no valid games.')
self._playlist = ShuffleList(playlist_resolved,
shuffle=self._playlist_randomize)
self._playlist = ShuffleList(
playlist_resolved, shuffle=self._playlist_randomize
)
# Get a game on deck ready to go.
self._current_game_spec: dict[str, Any] | None = None
self._next_game_spec: dict[str, Any] = self._playlist.pull_next()
self._next_game: type[ba.GameActivity] = (
self._next_game_spec['resolved_type'])
self._next_game: type[ba.GameActivity] = self._next_game_spec[
'resolved_type'
]
# Go ahead and instantiate the next game we'll
# use so it has lots of time to load.
@ -131,6 +139,7 @@ class MultiTeamSession(Session):
"""Returns a description of the next game on deck."""
# pylint: disable=cyclic-import
from ba._gameactivity import GameActivity
gametype: type[GameActivity] = self._next_game_spec['resolved_type']
assert issubclass(gametype, GameActivity)
return gametype.get_settings_display_string(self._next_game_spec)
@ -151,15 +160,20 @@ class MultiTeamSession(Session):
def _instantiate_next_game(self) -> None:
self._next_game_instance = _ba.newactivity(
self._next_game_spec['resolved_type'],
self._next_game_spec['settings'])
self._next_game_spec['settings'],
)
def on_activity_end(self, activity: ba.Activity, results: Any) -> None:
# pylint: disable=cyclic-import
from bastd.tutorial import TutorialActivity
from bastd.activity.multiteamvictory import (
TeamSeriesVictoryScoreScreenActivity)
from ba._activitytypes import (TransitionActivity, JoinActivity,
ScoreScreenActivity)
TeamSeriesVictoryScoreScreenActivity,
)
from ba._activitytypes import (
TransitionActivity,
JoinActivity,
ScoreScreenActivity,
)
# If we have a tutorial to show, that's the first thing we do no
# matter what.
@ -176,8 +190,8 @@ class MultiTeamSession(Session):
# If we're in a between-round activity or a restart-activity, hop
# into a round.
elif isinstance(
activity,
(JoinActivity, TransitionActivity, ScoreScreenActivity)):
activity, (JoinActivity, TransitionActivity, ScoreScreenActivity)
):
# If we're coming from a series-end activity, reset scores.
if isinstance(activity, TeamSeriesVictoryScoreScreenActivity):
@ -204,7 +218,7 @@ class MultiTeamSession(Session):
# ..but only ones who have been placed on a team
# (ie: no longer sitting in the lobby).
try:
has_team = (player.sessionteam is not None)
has_team = player.sessionteam is not None
except NotFoundError:
has_team = False
if has_team:
@ -223,11 +237,13 @@ class MultiTeamSession(Session):
del results # Unused arg.
print_error('this should be overridden')
def announce_game_results(self,
activity: ba.GameActivity,
results: ba.GameResults,
delay: float,
announce_winning_team: bool = True) -> None:
def announce_game_results(
self,
activity: ba.GameActivity,
results: ba.GameResults,
delay: float,
announce_winning_team: bool = True,
) -> None:
"""Show basic game result at the end of a game.
(before transitioning to a score screen).
@ -243,6 +259,7 @@ class MultiTeamSession(Session):
from ba._language import Lstr
from ba._freeforallsession import FreeForAllSession
from ba._messages import CelebrateMessage
_ba.timer(delay, Call(_ba.playsound, _ba.getsound('boxingBell')))
if announce_winning_team:
@ -261,8 +278,10 @@ class MultiTeamSession(Session):
wins_resource = 'winsPlayerText'
else:
wins_resource = 'winsTeamText'
wins_text = Lstr(resource=wins_resource,
subs=[('${NAME}', winning_sessionteam.name)])
wins_text = Lstr(
resource=wins_resource,
subs=[('${NAME}', winning_sessionteam.name)],
)
activity.show_zoom_message(
wins_text,
scale=0.85,
@ -300,8 +319,10 @@ class ShuffleList:
# If the new one is the same map or game-type as the previous,
# lets try to keep looking.
if len(self.shuffle_list) > 1 and self.last_gotten is not None:
if (test_obj['settings']['map'] ==
self.last_gotten['settings']['map']):
if (
test_obj['settings']['map']
== self.last_gotten['settings']['map']
):
continue
if test_obj['type'] == self.last_gotten['type']:
continue

View File

@ -24,6 +24,7 @@ class MusicType(Enum):
'situations'. The actual music played for each type can be overridden
by the game or by the user.
"""
MENU = 'Menu'
VICTORY = 'Victory'
CHAR_SELECT = 'CharSelect'
@ -53,6 +54,7 @@ class MusicPlayMode(Enum):
Category: **Enums**
"""
REGULAR = 'regular'
TEST = 'test'
@ -63,6 +65,7 @@ class AssetSoundtrackEntry:
Category: **App Classes**
"""
assetname: str
volume: float = 1.0
loop: bool = True
@ -70,50 +73,38 @@ class AssetSoundtrackEntry:
# What gets played by default for our different music types:
ASSET_SOUNDTRACK_ENTRIES: dict[MusicType, AssetSoundtrackEntry] = {
MusicType.MENU:
AssetSoundtrackEntry('menuMusic'),
MusicType.VICTORY:
AssetSoundtrackEntry('victoryMusic', volume=1.2, loop=False),
MusicType.CHAR_SELECT:
AssetSoundtrackEntry('charSelectMusic', volume=0.4),
MusicType.RUN_AWAY:
AssetSoundtrackEntry('runAwayMusic', volume=1.2),
MusicType.ONSLAUGHT:
AssetSoundtrackEntry('runAwayMusic', volume=1.2),
MusicType.KEEP_AWAY:
AssetSoundtrackEntry('runAwayMusic', volume=1.2),
MusicType.RACE:
AssetSoundtrackEntry('runAwayMusic', volume=1.2),
MusicType.EPIC_RACE:
AssetSoundtrackEntry('slowEpicMusic', volume=1.2),
MusicType.SCORES:
AssetSoundtrackEntry('scoresEpicMusic', volume=0.6, loop=False),
MusicType.GRAND_ROMP:
AssetSoundtrackEntry('grandRompMusic', volume=1.2),
MusicType.TO_THE_DEATH:
AssetSoundtrackEntry('toTheDeathMusic', volume=1.2),
MusicType.CHOSEN_ONE:
AssetSoundtrackEntry('survivalMusic', volume=0.8),
MusicType.FORWARD_MARCH:
AssetSoundtrackEntry('forwardMarchMusic', volume=0.8),
MusicType.FLAG_CATCHER:
AssetSoundtrackEntry('flagCatcherMusic', volume=1.2),
MusicType.SURVIVAL:
AssetSoundtrackEntry('survivalMusic', volume=0.8),
MusicType.EPIC:
AssetSoundtrackEntry('slowEpicMusic', volume=1.2),
MusicType.SPORTS:
AssetSoundtrackEntry('sportsMusic', volume=0.8),
MusicType.HOCKEY:
AssetSoundtrackEntry('sportsMusic', volume=0.8),
MusicType.FOOTBALL:
AssetSoundtrackEntry('sportsMusic', volume=0.8),
MusicType.FLYING:
AssetSoundtrackEntry('flyingMusic', volume=0.8),
MusicType.SCARY:
AssetSoundtrackEntry('scaryMusic', volume=0.8),
MusicType.MARCHING:
AssetSoundtrackEntry('whenJohnnyComesMarchingHomeMusic', volume=0.8),
MusicType.MENU: AssetSoundtrackEntry('menuMusic'),
MusicType.VICTORY: AssetSoundtrackEntry(
'victoryMusic', volume=1.2, loop=False
),
MusicType.CHAR_SELECT: AssetSoundtrackEntry('charSelectMusic', volume=0.4),
MusicType.RUN_AWAY: AssetSoundtrackEntry('runAwayMusic', volume=1.2),
MusicType.ONSLAUGHT: AssetSoundtrackEntry('runAwayMusic', volume=1.2),
MusicType.KEEP_AWAY: AssetSoundtrackEntry('runAwayMusic', volume=1.2),
MusicType.RACE: AssetSoundtrackEntry('runAwayMusic', volume=1.2),
MusicType.EPIC_RACE: AssetSoundtrackEntry('slowEpicMusic', volume=1.2),
MusicType.SCORES: AssetSoundtrackEntry(
'scoresEpicMusic', volume=0.6, loop=False
),
MusicType.GRAND_ROMP: AssetSoundtrackEntry('grandRompMusic', volume=1.2),
MusicType.TO_THE_DEATH: AssetSoundtrackEntry('toTheDeathMusic', volume=1.2),
MusicType.CHOSEN_ONE: AssetSoundtrackEntry('survivalMusic', volume=0.8),
MusicType.FORWARD_MARCH: AssetSoundtrackEntry(
'forwardMarchMusic', volume=0.8
),
MusicType.FLAG_CATCHER: AssetSoundtrackEntry(
'flagCatcherMusic', volume=1.2
),
MusicType.SURVIVAL: AssetSoundtrackEntry('survivalMusic', volume=0.8),
MusicType.EPIC: AssetSoundtrackEntry('slowEpicMusic', volume=1.2),
MusicType.SPORTS: AssetSoundtrackEntry('sportsMusic', volume=0.8),
MusicType.HOCKEY: AssetSoundtrackEntry('sportsMusic', volume=0.8),
MusicType.FOOTBALL: AssetSoundtrackEntry('sportsMusic', volume=0.8),
MusicType.FLYING: AssetSoundtrackEntry('flyingMusic', volume=0.8),
MusicType.SCARY: AssetSoundtrackEntry('scaryMusic', volume=0.8),
MusicType.MARCHING: AssetSoundtrackEntry(
'whenJohnnyComesMarchingHomeMusic', volume=0.8
),
}
@ -133,7 +124,7 @@ class MusicSubsystem:
self._music_player_type: type[MusicPlayer] | None = None
self.music_types: dict[MusicPlayMode, MusicType | None] = {
MusicPlayMode.REGULAR: None,
MusicPlayMode.TEST: None
MusicPlayMode.TEST: None,
}
# Set up custom music players for platforms that support them.
@ -143,9 +134,11 @@ class MusicSubsystem:
# instead of a special case.
if self.supports_soundtrack_entry_type('musicFile'):
from ba.osmusic import OSMusicPlayer
self._music_player_type = OSMusicPlayer
elif self.supports_soundtrack_entry_type('iTunesPlaylist'):
from ba.macmusicapp import MacMusicAppMusicPlayer
self._music_player_type = MacMusicAppMusicPlayer
def on_app_launch(self) -> None:
@ -156,11 +149,14 @@ class MusicSubsystem:
# out than later).
try:
cfg = _ba.app.config
if ('Soundtrack' in cfg and cfg['Soundtrack']
not in ['__default__', 'Default Soundtrack']):
if 'Soundtrack' in cfg and cfg['Soundtrack'] not in [
'__default__',
'Default Soundtrack',
]:
self.get_music_player()
except Exception:
from ba import _error
_error.print_exception('error prepping music-player')
def on_app_shutdown(self) -> None:
@ -185,9 +181,9 @@ class MusicSubsystem:
if self._music_player is not None:
self._music_player.set_volume(val)
def set_music_play_mode(self,
mode: MusicPlayMode,
force_restart: bool = False) -> None:
def set_music_play_mode(
self, mode: MusicPlayMode, force_restart: bool = False
) -> None:
"""Sets music play mode; used for soundtrack testing/etc."""
old_mode = self._music_mode
self._music_mode = mode
@ -210,8 +206,10 @@ class MusicSubsystem:
if entry_type == 'iTunesPlaylist':
return 'Mac' in uas
if entry_type in ('musicFile', 'musicFolder'):
return ('android' in uas
and _ba.android_get_external_files_dir() is not None)
return (
'android' in uas
and _ba.android_get_external_files_dir() is not None
)
if entry_type == 'default':
return True
return False
@ -228,18 +226,28 @@ class MusicSubsystem:
entry_type = 'iTunesPlaylist'
# For other entries we expect type and name strings in a dict.
elif (isinstance(entry, dict) and 'type' in entry
and isinstance(entry['type'], str) and 'name' in entry
and isinstance(entry['name'], str)):
elif (
isinstance(entry, dict)
and 'type' in entry
and isinstance(entry['type'], str)
and 'name' in entry
and isinstance(entry['name'], str)
):
entry_type = entry['type']
else:
raise TypeError('invalid soundtrack entry: ' + str(entry) +
' (type ' + str(type(entry)) + ')')
raise TypeError(
'invalid soundtrack entry: '
+ str(entry)
+ ' (type '
+ str(type(entry))
+ ')'
)
if self.supports_soundtrack_entry_type(entry_type):
return entry_type
raise ValueError('invalid soundtrack entry:' + str(entry))
except Exception:
from ba import _error
_error.print_exception()
return 'default'
@ -254,13 +262,18 @@ class MusicSubsystem:
return entry
# For other entries we expect type and name strings in a dict.
if (isinstance(entry, dict) and 'type' in entry
and isinstance(entry['type'], str) and 'name' in entry
and isinstance(entry['name'], str)):
if (
isinstance(entry, dict)
and 'type' in entry
and isinstance(entry['type'], str)
and 'name' in entry
and isinstance(entry['name'], str)
):
return entry['name']
raise ValueError('invalid soundtrack entry:' + str(entry))
except Exception:
from ba import _error
_error.print_exception()
return 'default'
@ -269,11 +282,13 @@ class MusicSubsystem:
if _ba.is_os_playing_music():
self.do_play_music(None)
def do_play_music(self,
musictype: MusicType | str | None,
continuous: bool = False,
mode: MusicPlayMode = MusicPlayMode.REGULAR,
testsoundtrack: dict[str, Any] | None = None) -> None:
def do_play_music(
self,
musictype: MusicType | str | None,
continuous: bool = False,
mode: MusicPlayMode = MusicPlayMode.REGULAR,
testsoundtrack: dict[str, Any] | None = None,
) -> None:
"""Plays the requested music type/mode.
For most cases, setmusic() is the proper call to use, which itself
@ -378,8 +393,9 @@ class MusicSubsystem:
'positional': False,
'music': True,
'volume': entry.volume * 5.0,
'loop': entry.loop
})
'loop': entry.loop,
},
)
class MusicPlayer:
@ -397,11 +413,16 @@ class MusicPlayer:
self._volume = 1.0
self._actually_playing = False
def select_entry(self, callback: Callable[[Any], None], current_entry: Any,
selection_target_name: str) -> Any:
def select_entry(
self,
callback: Callable[[Any], None],
current_entry: Any,
selection_target_name: str,
) -> Any:
"""Summons a UI to select a new soundtrack entry."""
return self.on_select_entry(callback, current_entry,
selection_target_name)
return self.on_select_entry(
callback, current_entry, selection_target_name
)
def set_volume(self, volume: float) -> None:
"""Set player volume (value should be between 0 and 1)."""
@ -435,8 +456,12 @@ class MusicPlayer:
"""Shutdown music playback completely."""
self.on_app_shutdown()
def on_select_entry(self, callback: Callable[[Any], None],
current_entry: Any, selection_target_name: str) -> Any:
def on_select_entry(
self,
callback: Callable[[Any], None],
current_entry: Any,
selection_target_name: str,
) -> Any:
"""Present a GUI to select an entry.
The callback should be called with a valid entry or None to
@ -464,8 +489,9 @@ class MusicPlayer:
self.on_play(self._entry_to_play)
self._actually_playing = True
else:
if self._actually_playing and (self._entry_to_play is None
or self._volume <= 0.0):
if self._actually_playing and (
self._entry_to_play is None or self._volume <= 0.0
):
self.on_stop()
self._actually_playing = False

View File

@ -15,6 +15,7 @@ import _ba
if TYPE_CHECKING:
from typing import Any, Callable
import socket
MasterServerCallback = Callable[[None | dict[str, Any]], None]
# Timeout for standard functions talking to the master-server/etc.
@ -61,6 +62,7 @@ class NetworkSubsystem:
def get_ip_address_type(addr: str) -> socket.AddressFamily:
"""Return socket.AF_INET6 or socket.AF_INET4 for the provided address."""
import socket
socket_type = None
# First try it as an ipv4 address.
@ -84,16 +86,21 @@ def get_ip_address_type(addr: str) -> socket.AddressFamily:
class MasterServerResponseType(Enum):
"""How to interpret responses from the master-server."""
JSON = 0
class MasterServerCallThread(threading.Thread):
"""Thread to communicate with the master-server."""
def __init__(self, request: str, request_type: str,
data: dict[str, Any] | None,
callback: MasterServerCallback | None,
response_type: MasterServerResponseType):
def __init__(
self,
request: str,
request_type: str,
data: dict[str, Any] | None,
callback: MasterServerCallback | None,
response_type: MasterServerResponseType,
):
super().__init__()
self._request = request
self._request_type = request_type
@ -106,8 +113,7 @@ class MasterServerCallThread(threading.Thread):
# Save and restore the context we were created from.
activity = _ba.getactivity(doraise=False)
self._activity = weakref.ref(
activity) if activity is not None else None
self._activity = weakref.ref(activity) if activity is not None else None
def _run_callback(self, arg: None | dict[str, Any]) -> None:
# If we were created in an activity context and that activity has
@ -143,22 +149,31 @@ class MasterServerCallThread(threading.Thread):
self._data = utf8_all(self._data)
_ba.set_thread_name('BA_ServerCallThread')
if self._request_type == 'get':
url = (get_master_server_address() + '/' + self._request +
'?' + urllib.parse.urlencode(self._data))
url = (
get_master_server_address()
+ '/'
+ self._request
+ '?'
+ urllib.parse.urlencode(self._data)
)
response = urllib.request.urlopen(
urllib.request.Request(
url, None, {'User-Agent': _ba.app.user_agent_string}),
url, None, {'User-Agent': _ba.app.user_agent_string}
),
context=_ba.app.net.sslcontext,
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS)
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS,
)
elif self._request_type == 'post':
url = get_master_server_address() + '/' + self._request
response = urllib.request.urlopen(
urllib.request.Request(
url,
urllib.parse.urlencode(self._data).encode(),
{'User-Agent': _ba.app.user_agent_string}),
{'User-Agent': _ba.app.user_agent_string},
),
context=_ba.app.net.sslcontext,
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS)
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS,
)
else:
raise TypeError('Invalid request_type: ' + self._request_type)
@ -180,37 +195,43 @@ class MasterServerCallThread(threading.Thread):
# Ignore common network errors; note unexpected ones.
if not is_urllib_communication_error(exc, url=url):
print(f'Error in MasterServerCallThread'
f' (url={url},'
f' response-type={self._response_type},'
f' response-data={response_data}):')
print(
f'Error in MasterServerCallThread'
f' (url={url},'
f' response-type={self._response_type},'
f' response-data={response_data}):'
)
import traceback
traceback.print_exc()
response_data = None
if self._callback is not None:
_ba.pushcall(Call(self._run_callback, response_data),
from_other_thread=True)
_ba.pushcall(
Call(self._run_callback, response_data), from_other_thread=True
)
def master_server_get(
request: str,
data: dict[str, Any],
callback: MasterServerCallback | None = None,
response_type: MasterServerResponseType = MasterServerResponseType.JSON
response_type: MasterServerResponseType = MasterServerResponseType.JSON,
) -> None:
"""Make a call to the master server via a http GET."""
MasterServerCallThread(request, 'get', data, callback,
response_type).start()
MasterServerCallThread(
request, 'get', data, callback, response_type
).start()
def master_server_post(
request: str,
data: dict[str, Any],
callback: MasterServerCallback | None = None,
response_type: MasterServerResponseType = MasterServerResponseType.JSON
response_type: MasterServerResponseType = MasterServerResponseType.JSON,
) -> None:
"""Make a call to the master server via a http POST."""
MasterServerCallThread(request, 'post', data, callback,
response_type).start()
MasterServerCallThread(
request, 'post', data, callback, response_type
).start()

View File

@ -8,8 +8,11 @@ from dataclasses import dataclass
from typing import TYPE_CHECKING, TypeVar, Generic, cast
import _ba
from ba._error import (SessionPlayerNotFoundError, print_exception,
ActorNotFoundError)
from ba._error import (
SessionPlayerNotFoundError,
print_exception,
ActorNotFoundError,
)
from ba._messages import DeathType, DieMessage
if TYPE_CHECKING:
@ -28,6 +31,7 @@ class PlayerInfo:
Category: Gameplay Classes
"""
name: str
character: str
@ -38,6 +42,7 @@ class StandLocation:
Category: Gameplay Classes
"""
position: ba.Vec3
angle: float | None = None
@ -90,7 +95,8 @@ class Player(Generic[TeamType]):
f' operator (__eq__) which will break internal'
f' logic. Please remove it.\n'
f'For dataclasses you can do "dataclass(eq=False)"'
f' in the class decorator.')
f' in the class decorator.'
)
self.actor = None
self.character = ''
@ -249,8 +255,9 @@ class Player(Generic[TeamType]):
assert not self._expired
return self._sessionplayer.get_icon()
def assigninput(self, inputtype: ba.InputType | tuple[ba.InputType, ...],
call: Callable) -> None:
def assigninput(
self, inputtype: ba.InputType | tuple[ba.InputType, ...], call: Callable
) -> None:
"""
Set the python callable to be run for one or more types of input.
"""
@ -311,8 +318,9 @@ def playercast(totype: type[PlayerType], player: ba.Player) -> PlayerType:
# NOTE: ideally we should have a single playercast() call and use overloads
# for the optional variety, but that currently seems to not be working.
# See: https://github.com/python/mypy/issues/8800
def playercast_o(totype: type[PlayerType],
player: ba.Player | None) -> PlayerType | None:
def playercast_o(
totype: type[PlayerType], player: ba.Player | None
) -> PlayerType | None:
"""A variant of ba.playercast() for use with optional ba.Player values.
Category: Gameplay Functions

View File

@ -15,12 +15,14 @@ if TYPE_CHECKING:
PlaylistType = list[dict[str, Any]]
def filter_playlist(playlist: PlaylistType,
sessiontype: type[_session.Session],
add_resolved_type: bool = False,
remove_unowned: bool = True,
mark_unowned: bool = False,
name: str = '?') -> PlaylistType:
def filter_playlist(
playlist: PlaylistType,
sessiontype: type[_session.Session],
add_resolved_type: bool = False,
remove_unowned: bool = True,
mark_unowned: bool = False,
name: str = '?',
) -> PlaylistType:
"""Return a filtered version of a playlist.
Strips out or replaces invalid or unowned game types, makes sure all
@ -34,6 +36,7 @@ def filter_playlist(playlist: PlaylistType,
from ba._store import get_unowned_maps, get_unowned_game_types
from ba._general import getclass
from ba._gameactivity import GameActivity
goodlist: list[dict] = []
unowned_maps: Sequence[str]
if remove_unowned or mark_unowned:
@ -56,7 +59,8 @@ def filter_playlist(playlist: PlaylistType,
# Update old map names to new ones.
entry['settings']['map'] = get_filtered_map_name(
entry['settings']['map'])
entry['settings']['map']
)
if remove_unowned and entry['settings']['map'] in unowned_maps:
continue
@ -67,60 +71,89 @@ def filter_playlist(playlist: PlaylistType,
raise TypeError('invalid entry format')
try:
# Do some type filters for backwards compat.
if entry['type'] in ('Assault.AssaultGame',
'Happy_Thoughts.HappyThoughtsGame',
'bsAssault.AssaultGame',
'bs_assault.AssaultGame'):
if entry['type'] in (
'Assault.AssaultGame',
'Happy_Thoughts.HappyThoughtsGame',
'bsAssault.AssaultGame',
'bs_assault.AssaultGame',
):
entry['type'] = 'bastd.game.assault.AssaultGame'
if entry['type'] in ('King_of_the_Hill.KingOfTheHillGame',
'bsKingOfTheHill.KingOfTheHillGame',
'bs_king_of_the_hill.KingOfTheHillGame'):
if entry['type'] in (
'King_of_the_Hill.KingOfTheHillGame',
'bsKingOfTheHill.KingOfTheHillGame',
'bs_king_of_the_hill.KingOfTheHillGame',
):
entry['type'] = 'bastd.game.kingofthehill.KingOfTheHillGame'
if entry['type'] in ('Capture_the_Flag.CTFGame',
'bsCaptureTheFlag.CTFGame',
'bs_capture_the_flag.CTFGame'):
entry['type'] = (
'bastd.game.capturetheflag.CaptureTheFlagGame')
if entry['type'] in ('Death_Match.DeathMatchGame',
'bsDeathMatch.DeathMatchGame',
'bs_death_match.DeathMatchGame'):
if entry['type'] in (
'Capture_the_Flag.CTFGame',
'bsCaptureTheFlag.CTFGame',
'bs_capture_the_flag.CTFGame',
):
entry['type'] = 'bastd.game.capturetheflag.CaptureTheFlagGame'
if entry['type'] in (
'Death_Match.DeathMatchGame',
'bsDeathMatch.DeathMatchGame',
'bs_death_match.DeathMatchGame',
):
entry['type'] = 'bastd.game.deathmatch.DeathMatchGame'
if entry['type'] in ('ChosenOne.ChosenOneGame',
'bsChosenOne.ChosenOneGame',
'bs_chosen_one.ChosenOneGame'):
if entry['type'] in (
'ChosenOne.ChosenOneGame',
'bsChosenOne.ChosenOneGame',
'bs_chosen_one.ChosenOneGame',
):
entry['type'] = 'bastd.game.chosenone.ChosenOneGame'
if entry['type'] in ('Conquest.Conquest', 'Conquest.ConquestGame',
'bsConquest.ConquestGame',
'bs_conquest.ConquestGame'):
if entry['type'] in (
'Conquest.Conquest',
'Conquest.ConquestGame',
'bsConquest.ConquestGame',
'bs_conquest.ConquestGame',
):
entry['type'] = 'bastd.game.conquest.ConquestGame'
if entry['type'] in ('Elimination.EliminationGame',
'bsElimination.EliminationGame',
'bs_elimination.EliminationGame'):
if entry['type'] in (
'Elimination.EliminationGame',
'bsElimination.EliminationGame',
'bs_elimination.EliminationGame',
):
entry['type'] = 'bastd.game.elimination.EliminationGame'
if entry['type'] in ('Football.FootballGame',
'bsFootball.FootballTeamGame',
'bs_football.FootballTeamGame'):
if entry['type'] in (
'Football.FootballGame',
'bsFootball.FootballTeamGame',
'bs_football.FootballTeamGame',
):
entry['type'] = 'bastd.game.football.FootballTeamGame'
if entry['type'] in ('Hockey.HockeyGame', 'bsHockey.HockeyGame',
'bs_hockey.HockeyGame'):
if entry['type'] in (
'Hockey.HockeyGame',
'bsHockey.HockeyGame',
'bs_hockey.HockeyGame',
):
entry['type'] = 'bastd.game.hockey.HockeyGame'
if entry['type'] in ('Keep_Away.KeepAwayGame',
'bsKeepAway.KeepAwayGame',
'bs_keep_away.KeepAwayGame'):
if entry['type'] in (
'Keep_Away.KeepAwayGame',
'bsKeepAway.KeepAwayGame',
'bs_keep_away.KeepAwayGame',
):
entry['type'] = 'bastd.game.keepaway.KeepAwayGame'
if entry['type'] in ('Race.RaceGame', 'bsRace.RaceGame',
'bs_race.RaceGame'):
if entry['type'] in (
'Race.RaceGame',
'bsRace.RaceGame',
'bs_race.RaceGame',
):
entry['type'] = 'bastd.game.race.RaceGame'
if entry['type'] in ('bsEasterEggHunt.EasterEggHuntGame',
'bs_easter_egg_hunt.EasterEggHuntGame'):
if entry['type'] in (
'bsEasterEggHunt.EasterEggHuntGame',
'bs_easter_egg_hunt.EasterEggHuntGame',
):
entry['type'] = 'bastd.game.easteregghunt.EasterEggHuntGame'
if entry['type'] in ('bsMeteorShower.MeteorShowerGame',
'bs_meteor_shower.MeteorShowerGame'):
if entry['type'] in (
'bsMeteorShower.MeteorShowerGame',
'bs_meteor_shower.MeteorShowerGame',
):
entry['type'] = 'bastd.game.meteorshower.MeteorShowerGame'
if entry['type'] in ('bsTargetPractice.TargetPracticeGame',
'bs_target_practice.TargetPracticeGame'):
entry['type'] = (
'bastd.game.targetpractice.TargetPracticeGame')
if entry['type'] in (
'bsTargetPractice.TargetPracticeGame',
'bs_target_practice.TargetPracticeGame',
):
entry['type'] = 'bastd.game.targetpractice.TargetPracticeGame'
gameclass = getclass(entry['type'], GameActivity)
@ -140,10 +173,12 @@ def filter_playlist(playlist: PlaylistType,
entry['settings'][setting.name] = setting.default
goodlist.append(entry)
except ImportError as exc:
logging.warning('Import failed while scanning playlist \'%s\': %s',
name, exc)
logging.warning(
'Import failed while scanning playlist \'%s\': %s', name, exc
)
except Exception:
from ba import _error
_error.print_exception()
return goodlist
@ -155,123 +190,134 @@ def get_default_free_for_all_playlist() -> PlaylistType:
# but filtering translates them properly to the new ones.
# (is kinda a handy way to ensure filtering is working).
# Eventually should update these though.
return [{
'settings': {
'Epic Mode': False,
'Kills to Win Per Player': 10,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Doom Shroom'
return [
{
'settings': {
'Epic Mode': False,
'Kills to Win Per Player': 10,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Doom Shroom',
},
'type': 'bs_death_match.DeathMatchGame',
},
'type': 'bs_death_match.DeathMatchGame'
}, {
'settings': {
'Chosen One Gets Gloves': True,
'Chosen One Gets Shield': False,
'Chosen One Time': 30,
'Epic Mode': 0,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Monkey Face'
{
'settings': {
'Chosen One Gets Gloves': True,
'Chosen One Gets Shield': False,
'Chosen One Time': 30,
'Epic Mode': 0,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Monkey Face',
},
'type': 'bs_chosen_one.ChosenOneGame',
},
'type': 'bs_chosen_one.ChosenOneGame'
}, {
'settings': {
'Hold Time': 30,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Zigzag'
{
'settings': {
'Hold Time': 30,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Zigzag',
},
'type': 'bs_king_of_the_hill.KingOfTheHillGame',
},
'type': 'bs_king_of_the_hill.KingOfTheHillGame'
}, {
'settings': {
'Epic Mode': False,
'map': 'Rampage'
{
'settings': {'Epic Mode': False, 'map': 'Rampage'},
'type': 'bs_meteor_shower.MeteorShowerGame',
},
'type': 'bs_meteor_shower.MeteorShowerGame'
}, {
'settings': {
'Epic Mode': 1,
'Lives Per Player': 1,
'Respawn Times': 1.0,
'Time Limit': 120,
'map': 'Tip Top'
{
'settings': {
'Epic Mode': 1,
'Lives Per Player': 1,
'Respawn Times': 1.0,
'Time Limit': 120,
'map': 'Tip Top',
},
'type': 'bs_elimination.EliminationGame',
},
'type': 'bs_elimination.EliminationGame'
}, {
'settings': {
'Hold Time': 30,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'The Pad'
{
'settings': {
'Hold Time': 30,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'The Pad',
},
'type': 'bs_keep_away.KeepAwayGame',
},
'type': 'bs_keep_away.KeepAwayGame'
}, {
'settings': {
'Epic Mode': True,
'Kills to Win Per Player': 10,
'Respawn Times': 0.25,
'Time Limit': 120,
'map': 'Rampage'
{
'settings': {
'Epic Mode': True,
'Kills to Win Per Player': 10,
'Respawn Times': 0.25,
'Time Limit': 120,
'map': 'Rampage',
},
'type': 'bs_death_match.DeathMatchGame',
},
'type': 'bs_death_match.DeathMatchGame'
}, {
'settings': {
'Bomb Spawning': 1000,
'Epic Mode': False,
'Laps': 3,
'Mine Spawn Interval': 4000,
'Mine Spawning': 4000,
'Time Limit': 300,
'map': 'Big G'
{
'settings': {
'Bomb Spawning': 1000,
'Epic Mode': False,
'Laps': 3,
'Mine Spawn Interval': 4000,
'Mine Spawning': 4000,
'Time Limit': 300,
'map': 'Big G',
},
'type': 'bs_race.RaceGame',
},
'type': 'bs_race.RaceGame'
}, {
'settings': {
'Hold Time': 30,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Happy Thoughts'
{
'settings': {
'Hold Time': 30,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Happy Thoughts',
},
'type': 'bs_king_of_the_hill.KingOfTheHillGame',
},
'type': 'bs_king_of_the_hill.KingOfTheHillGame'
}, {
'settings': {
'Enable Impact Bombs': 1,
'Enable Triple Bombs': False,
'Target Count': 2,
'map': 'Doom Shroom'
{
'settings': {
'Enable Impact Bombs': 1,
'Enable Triple Bombs': False,
'Target Count': 2,
'map': 'Doom Shroom',
},
'type': 'bs_target_practice.TargetPracticeGame',
},
'type': 'bs_target_practice.TargetPracticeGame'
}, {
'settings': {
'Epic Mode': False,
'Lives Per Player': 5,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Step Right Up'
{
'settings': {
'Epic Mode': False,
'Lives Per Player': 5,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Step Right Up',
},
'type': 'bs_elimination.EliminationGame',
},
'type': 'bs_elimination.EliminationGame'
}, {
'settings': {
'Epic Mode': False,
'Kills to Win Per Player': 10,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Crag Castle'
{
'settings': {
'Epic Mode': False,
'Kills to Win Per Player': 10,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Crag Castle',
},
'type': 'bs_death_match.DeathMatchGame',
},
'type': 'bs_death_match.DeathMatchGame'
}, {
'map': 'Lake Frigid',
'settings': {
'Bomb Spawning': 0,
'Epic Mode': False,
'Laps': 6,
'Mine Spawning': 2000,
'Time Limit': 300,
'map': 'Lake Frigid'
{
'map': 'Lake Frigid',
'settings': {
'Bomb Spawning': 0,
'Epic Mode': False,
'Laps': 6,
'Mine Spawning': 2000,
'Time Limit': 300,
'map': 'Lake Frigid',
},
'type': 'bs_race.RaceGame',
},
'type': 'bs_race.RaceGame'
}]
]
def get_default_teams_playlist() -> PlaylistType:
@ -281,217 +327,238 @@ def get_default_teams_playlist() -> PlaylistType:
# but filtering translates them properly to the new ones.
# (is kinda a handy way to ensure filtering is working).
# Eventually should update these though.
return [{
'settings': {
'Epic Mode': False,
'Flag Idle Return Time': 30,
'Flag Touch Return Time': 0,
'Respawn Times': 1.0,
'Score to Win': 3,
'Time Limit': 600,
'map': 'Bridgit'
return [
{
'settings': {
'Epic Mode': False,
'Flag Idle Return Time': 30,
'Flag Touch Return Time': 0,
'Respawn Times': 1.0,
'Score to Win': 3,
'Time Limit': 600,
'map': 'Bridgit',
},
'type': 'bs_capture_the_flag.CTFGame',
},
'type': 'bs_capture_the_flag.CTFGame'
}, {
'settings': {
'Epic Mode': False,
'Respawn Times': 1.0,
'Score to Win': 3,
'Time Limit': 600,
'map': 'Step Right Up'
{
'settings': {
'Epic Mode': False,
'Respawn Times': 1.0,
'Score to Win': 3,
'Time Limit': 600,
'map': 'Step Right Up',
},
'type': 'bs_assault.AssaultGame',
},
'type': 'bs_assault.AssaultGame'
}, {
'settings': {
'Balance Total Lives': False,
'Epic Mode': False,
'Lives Per Player': 3,
'Respawn Times': 1.0,
'Solo Mode': True,
'Time Limit': 600,
'map': 'Rampage'
{
'settings': {
'Balance Total Lives': False,
'Epic Mode': False,
'Lives Per Player': 3,
'Respawn Times': 1.0,
'Solo Mode': True,
'Time Limit': 600,
'map': 'Rampage',
},
'type': 'bs_elimination.EliminationGame',
},
'type': 'bs_elimination.EliminationGame'
}, {
'settings': {
'Epic Mode': False,
'Kills to Win Per Player': 5,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Roundabout'
{
'settings': {
'Epic Mode': False,
'Kills to Win Per Player': 5,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Roundabout',
},
'type': 'bs_death_match.DeathMatchGame',
},
'type': 'bs_death_match.DeathMatchGame'
}, {
'settings': {
'Respawn Times': 1.0,
'Score to Win': 1,
'Time Limit': 600,
'map': 'Hockey Stadium'
{
'settings': {
'Respawn Times': 1.0,
'Score to Win': 1,
'Time Limit': 600,
'map': 'Hockey Stadium',
},
'type': 'bs_hockey.HockeyGame',
},
'type': 'bs_hockey.HockeyGame'
}, {
'settings': {
'Hold Time': 30,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Monkey Face'
{
'settings': {
'Hold Time': 30,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Monkey Face',
},
'type': 'bs_keep_away.KeepAwayGame',
},
'type': 'bs_keep_away.KeepAwayGame'
}, {
'settings': {
'Balance Total Lives': False,
'Epic Mode': True,
'Lives Per Player': 1,
'Respawn Times': 1.0,
'Solo Mode': False,
'Time Limit': 120,
'map': 'Tip Top'
{
'settings': {
'Balance Total Lives': False,
'Epic Mode': True,
'Lives Per Player': 1,
'Respawn Times': 1.0,
'Solo Mode': False,
'Time Limit': 120,
'map': 'Tip Top',
},
'type': 'bs_elimination.EliminationGame',
},
'type': 'bs_elimination.EliminationGame'
}, {
'settings': {
'Epic Mode': False,
'Respawn Times': 1.0,
'Score to Win': 3,
'Time Limit': 300,
'map': 'Crag Castle'
{
'settings': {
'Epic Mode': False,
'Respawn Times': 1.0,
'Score to Win': 3,
'Time Limit': 300,
'map': 'Crag Castle',
},
'type': 'bs_assault.AssaultGame',
},
'type': 'bs_assault.AssaultGame'
}, {
'settings': {
'Epic Mode': False,
'Kills to Win Per Player': 5,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Doom Shroom'
{
'settings': {
'Epic Mode': False,
'Kills to Win Per Player': 5,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Doom Shroom',
},
'type': 'bs_death_match.DeathMatchGame',
},
'type': 'bs_death_match.DeathMatchGame'
}, {
'settings': {
'Epic Mode': False,
'map': 'Rampage'
{
'settings': {'Epic Mode': False, 'map': 'Rampage'},
'type': 'bs_meteor_shower.MeteorShowerGame',
},
'type': 'bs_meteor_shower.MeteorShowerGame'
}, {
'settings': {
'Epic Mode': False,
'Flag Idle Return Time': 30,
'Flag Touch Return Time': 0,
'Respawn Times': 1.0,
'Score to Win': 2,
'Time Limit': 600,
'map': 'Roundabout'
{
'settings': {
'Epic Mode': False,
'Flag Idle Return Time': 30,
'Flag Touch Return Time': 0,
'Respawn Times': 1.0,
'Score to Win': 2,
'Time Limit': 600,
'map': 'Roundabout',
},
'type': 'bs_capture_the_flag.CTFGame',
},
'type': 'bs_capture_the_flag.CTFGame'
}, {
'settings': {
'Respawn Times': 1.0,
'Score to Win': 21,
'Time Limit': 600,
'map': 'Football Stadium'
{
'settings': {
'Respawn Times': 1.0,
'Score to Win': 21,
'Time Limit': 600,
'map': 'Football Stadium',
},
'type': 'bs_football.FootballTeamGame',
},
'type': 'bs_football.FootballTeamGame'
}, {
'settings': {
'Epic Mode': True,
'Respawn Times': 0.25,
'Score to Win': 3,
'Time Limit': 120,
'map': 'Bridgit'
{
'settings': {
'Epic Mode': True,
'Respawn Times': 0.25,
'Score to Win': 3,
'Time Limit': 120,
'map': 'Bridgit',
},
'type': 'bs_assault.AssaultGame',
},
'type': 'bs_assault.AssaultGame'
}, {
'map': 'Doom Shroom',
'settings': {
'Enable Impact Bombs': 1,
'Enable Triple Bombs': False,
'Target Count': 2,
'map': 'Doom Shroom'
{
'map': 'Doom Shroom',
'settings': {
'Enable Impact Bombs': 1,
'Enable Triple Bombs': False,
'Target Count': 2,
'map': 'Doom Shroom',
},
'type': 'bs_target_practice.TargetPracticeGame',
},
'type': 'bs_target_practice.TargetPracticeGame'
}, {
'settings': {
'Hold Time': 30,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Tip Top'
{
'settings': {
'Hold Time': 30,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Tip Top',
},
'type': 'bs_king_of_the_hill.KingOfTheHillGame',
},
'type': 'bs_king_of_the_hill.KingOfTheHillGame'
}, {
'settings': {
'Epic Mode': False,
'Respawn Times': 1.0,
'Score to Win': 2,
'Time Limit': 300,
'map': 'Zigzag'
{
'settings': {
'Epic Mode': False,
'Respawn Times': 1.0,
'Score to Win': 2,
'Time Limit': 300,
'map': 'Zigzag',
},
'type': 'bs_assault.AssaultGame',
},
'type': 'bs_assault.AssaultGame'
}, {
'settings': {
'Epic Mode': False,
'Flag Idle Return Time': 30,
'Flag Touch Return Time': 0,
'Respawn Times': 1.0,
'Score to Win': 3,
'Time Limit': 300,
'map': 'Happy Thoughts'
{
'settings': {
'Epic Mode': False,
'Flag Idle Return Time': 30,
'Flag Touch Return Time': 0,
'Respawn Times': 1.0,
'Score to Win': 3,
'Time Limit': 300,
'map': 'Happy Thoughts',
},
'type': 'bs_capture_the_flag.CTFGame',
},
'type': 'bs_capture_the_flag.CTFGame'
}, {
'settings': {
'Bomb Spawning': 1000,
'Epic Mode': True,
'Laps': 1,
'Mine Spawning': 2000,
'Time Limit': 300,
'map': 'Big G'
{
'settings': {
'Bomb Spawning': 1000,
'Epic Mode': True,
'Laps': 1,
'Mine Spawning': 2000,
'Time Limit': 300,
'map': 'Big G',
},
'type': 'bs_race.RaceGame',
},
'type': 'bs_race.RaceGame'
}, {
'settings': {
'Epic Mode': False,
'Kills to Win Per Player': 5,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Monkey Face'
{
'settings': {
'Epic Mode': False,
'Kills to Win Per Player': 5,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Monkey Face',
},
'type': 'bs_death_match.DeathMatchGame',
},
'type': 'bs_death_match.DeathMatchGame'
}, {
'settings': {
'Hold Time': 30,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Lake Frigid'
{
'settings': {
'Hold Time': 30,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Lake Frigid',
},
'type': 'bs_keep_away.KeepAwayGame',
},
'type': 'bs_keep_away.KeepAwayGame'
}, {
'settings': {
'Epic Mode': False,
'Flag Idle Return Time': 30,
'Flag Touch Return Time': 3,
'Respawn Times': 1.0,
'Score to Win': 2,
'Time Limit': 300,
'map': 'Tip Top'
{
'settings': {
'Epic Mode': False,
'Flag Idle Return Time': 30,
'Flag Touch Return Time': 3,
'Respawn Times': 1.0,
'Score to Win': 2,
'Time Limit': 300,
'map': 'Tip Top',
},
'type': 'bs_capture_the_flag.CTFGame',
},
'type': 'bs_capture_the_flag.CTFGame'
}, {
'settings': {
'Balance Total Lives': False,
'Epic Mode': False,
'Lives Per Player': 3,
'Respawn Times': 1.0,
'Solo Mode': False,
'Time Limit': 300,
'map': 'Crag Castle'
{
'settings': {
'Balance Total Lives': False,
'Epic Mode': False,
'Lives Per Player': 3,
'Respawn Times': 1.0,
'Solo Mode': False,
'Time Limit': 300,
'map': 'Crag Castle',
},
'type': 'bs_elimination.EliminationGame',
},
'type': 'bs_elimination.EliminationGame'
}, {
'settings': {
'Epic Mode': True,
'Respawn Times': 0.25,
'Time Limit': 120,
'map': 'Zigzag'
{
'settings': {
'Epic Mode': True,
'Respawn Times': 0.25,
'Time Limit': 120,
'map': 'Zigzag',
},
'type': 'bs_conquest.ConquestGame',
},
'type': 'bs_conquest.ConquestGame'
}]
]

View File

@ -42,9 +42,12 @@ class PluginSubsystem:
# Create a potential-plugin for each class we found in the scan.
for class_path in results.exports_of_class(Plugin):
plugs.potential_plugins.append(
PotentialPlugin(display_name=Lstr(value=class_path),
class_path=class_path,
available=True))
PotentialPlugin(
display_name=Lstr(value=class_path),
class_path=class_path,
available=True,
)
)
if class_path not in plugstates:
# Go ahead and enable new plugins by default, but we'll
# inform the user that they need to restart to pick them up.
@ -59,8 +62,9 @@ class PluginSubsystem:
# plugins, so we don't need the message about 'restart to activate'
# anymore.
if found_new and bool(False):
_ba.screenmessage(Lstr(resource='pluginsDetectedText'),
color=(0, 1, 0))
_ba.screenmessage(
Lstr(resource='pluginsDetectedText'), color=(0, 1, 0)
)
_ba.playsound(_ba.getsound('ding'))
if config_changed:
@ -75,6 +79,7 @@ class PluginSubsystem:
plugin.on_app_running()
except Exception:
from ba import _error
_error.print_exception('Error in plugin on_app_running()')
def on_app_pause(self) -> None:
@ -84,6 +89,7 @@ class PluginSubsystem:
plugin.on_app_pause()
except Exception:
from ba import _error
_error.print_exception('Error in plugin on_app_pause()')
def on_app_resume(self) -> None:
@ -93,6 +99,7 @@ class PluginSubsystem:
plugin.on_app_resume()
except Exception:
from ba import _error
_error.print_exception('Error in plugin on_app_resume()')
def on_app_shutdown(self) -> None:
@ -102,6 +109,7 @@ class PluginSubsystem:
plugin.on_app_shutdown()
except Exception:
from ba import _error
_error.print_exception('Error in plugin on_app_shutdown()')
def load_plugins(self) -> None:
@ -113,8 +121,9 @@ class PluginSubsystem:
# in the app config. Its not our job to look at meta stuff here.
plugstates: dict[str, dict] = _ba.app.config.get('Plugins', {})
assert isinstance(plugstates, dict)
plugkeys: list[str] = sorted(key for key, val in plugstates.items()
if val.get('enabled', False))
plugkeys: list[str] = sorted(
key for key, val in plugstates.items() if val.get('enabled', False)
)
disappeared_plugs: set[str] = set()
for plugkey in plugkeys:
try:
@ -124,10 +133,13 @@ class PluginSubsystem:
continue
except Exception as exc:
_ba.playsound(_ba.getsound('error'))
_ba.screenmessage(Lstr(resource='pluginClassLoadErrorText',
subs=[('${PLUGIN}', plugkey),
('${ERROR}', str(exc))]),
color=(1, 0, 0))
_ba.screenmessage(
Lstr(
resource='pluginClassLoadErrorText',
subs=[('${PLUGIN}', plugkey), ('${ERROR}', str(exc))],
),
color=(1, 0, 0),
)
logging.exception("Error loading plugin class '%s'", plugkey)
continue
try:
@ -136,11 +148,15 @@ class PluginSubsystem:
self.active_plugins[plugkey] = plugin
except Exception as exc:
from ba import _error
_ba.playsound(_ba.getsound('error'))
_ba.screenmessage(Lstr(resource='pluginInitErrorText',
subs=[('${PLUGIN}', plugkey),
('${ERROR}', str(exc))]),
color=(1, 0, 0))
_ba.screenmessage(
Lstr(
resource='pluginInitErrorText',
subs=[('${PLUGIN}', plugkey), ('${ERROR}', str(exc))],
),
color=(1, 0, 0),
)
_error.print_exception(f"Error initing plugin: '{plugkey}'.")
# If plugins disappeared, let the user know gently and remove them
@ -150,13 +166,18 @@ class PluginSubsystem:
if disappeared_plugs:
_ba.playsound(_ba.getsound('shieldDown'))
_ba.screenmessage(
Lstr(resource='pluginsRemovedText',
subs=[('${NUM}', str(len(disappeared_plugs)))]),
Lstr(
resource='pluginsRemovedText',
subs=[('${NUM}', str(len(disappeared_plugs)))],
),
color=(1, 1, 0),
)
plugnames = ', '.join(disappeared_plugs)
logging.warning('%d plugin(s) no longer found: %s.',
len(disappeared_plugs), plugnames)
logging.warning(
'%d plugin(s) no longer found: %s.',
len(disappeared_plugs),
plugnames,
)
for goneplug in disappeared_plugs:
del _ba.app.config['Plugins'][goneplug]
_ba.app.config.commit()
@ -173,6 +194,7 @@ class PotentialPlugin:
were previously set to be loaded but which were unable to be
for some reason. In that case, 'available' will be set to False.
"""
display_name: ba.Lstr
class_path: str
available: bool

View File

@ -45,6 +45,14 @@ class PowerupAcceptMessage:
def get_default_powerup_distribution() -> Sequence[tuple[str, int]]:
"""Standard set of powerups."""
return (('triple_bombs', 3), ('ice_bombs', 3), ('punch', 3),
('impact_bombs', 3), ('land_mines', 2), ('sticky_bombs', 3),
('shield', 2), ('health', 1), ('curse', 1))
return (
('triple_bombs', 3),
('ice_bombs', 3),
('punch', 3),
('impact_bombs', 3),
('land_mines', 2),
('sticky_bombs', 3),
('shield', 2),
('health', 1),
('curse', 1),
)

View File

@ -13,11 +13,24 @@ if TYPE_CHECKING:
# NOTE: player color options are enforced server-side for non-pro accounts
# so don't change these or they won't stick...
PLAYER_COLORS = [(1, 0.15, 0.15), (0.2, 1, 0.2), (0.1, 0.1, 1), (0.2, 1, 1),
(0.5, 0.25, 1.0), (1, 1, 0), (1, 0.5, 0), (1, 0.3, 0.5),
(0.1, 0.1, 0.5), (0.4, 0.2, 0.1), (0.1, 0.35, 0.1),
(1, 0.8, 0.5), (0.4, 0.05, 0.05), (0.13, 0.13, 0.13),
(0.5, 0.5, 0.5), (1, 1, 1)]
PLAYER_COLORS = [
(1, 0.15, 0.15),
(0.2, 1, 0.2),
(0.1, 0.1, 1),
(0.2, 1, 1),
(0.5, 0.25, 1.0),
(1, 1, 0),
(1, 0.5, 0),
(1, 0.3, 0.5),
(0.1, 0.1, 0.5),
(0.4, 0.2, 0.1),
(0.1, 0.35, 0.1),
(1, 0.8, 0.5),
(0.4, 0.05, 0.05),
(0.13, 0.13, 0.13),
(0.5, 0.5, 0.5),
(1, 1, 1),
]
def get_player_colors() -> list[tuple[float, float, float]]:
@ -49,8 +62,7 @@ def get_player_profile_icon(profilename: str) -> str:
def get_player_profile_colors(
profilename: str | None,
profiles: dict[str, dict[str, Any]] | None = None
profilename: str | None, profiles: dict[str, dict[str, Any]] | None = None
) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
"""Given a profile, return colors for them."""
appconfig = _ba.app.config
@ -83,11 +95,13 @@ def get_player_profile_colors(
if profilename is None:
# Last 2 are grey and white; ignore those or we
# get lots of old-looking players.
highlight = PLAYER_COLORS[random.randrange(
len(PLAYER_COLORS) - 2)]
highlight = PLAYER_COLORS[
random.randrange(len(PLAYER_COLORS) - 2)
]
else:
highlight = PLAYER_COLORS[sum(ord(c) + 1
for c in profilename) %
(len(PLAYER_COLORS) - 2)]
highlight = PLAYER_COLORS[
sum(ord(c) + 1 for c in profilename)
% (len(PLAYER_COLORS) - 2)
]
return color, highlight

View File

@ -18,6 +18,7 @@ class ScoreType(Enum):
Category: **Enums**
"""
SECONDS = 's'
MILLISECONDS = 'ms'
POINTS = 'p'

View File

@ -9,13 +9,18 @@ import logging
from typing import TYPE_CHECKING
from efro.terminal import Clr
from bacommon.servermanager import (ServerCommand, StartServerModeCommand,
ShutdownCommand, ShutdownReason,
ChatMessageCommand, ScreenMessageCommand,
ClientListCommand, KickCommand)
from bacommon.servermanager import (
ServerCommand,
StartServerModeCommand,
ShutdownCommand,
ShutdownReason,
ChatMessageCommand,
ScreenMessageCommand,
ClientListCommand,
KickCommand,
)
import _ba
from ba._internal import (add_transaction, run_transactions,
get_v1_account_state)
from ba._internal import add_transaction, run_transactions, get_v1_account_state
from ba._generated.enums import TimeType
from ba._freeforallsession import FreeForAllSession
from ba._dualteamsession import DualTeamSession
@ -31,6 +36,7 @@ if TYPE_CHECKING:
def _cmd(command_data: bytes) -> None:
"""Handle commands coming in from our server manager parent process."""
import pickle
command = pickle.loads(command_data)
assert isinstance(command, ServerCommand)
@ -41,8 +47,9 @@ def _cmd(command_data: bytes) -> None:
if isinstance(command, ShutdownCommand):
assert _ba.app.server is not None
_ba.app.server.shutdown(reason=command.reason,
immediate=command.immediate)
_ba.app.server.shutdown(
reason=command.reason, immediate=command.immediate
)
return
if isinstance(command, ChatMessageCommand):
@ -56,10 +63,12 @@ def _cmd(command_data: bytes) -> None:
# Note: we have to do transient messages if
# clients is specified, so they won't show up
# in replays.
_ba.screenmessage(command.message,
color=command.color,
clients=command.clients,
transient=command.clients is not None)
_ba.screenmessage(
command.message,
color=command.color,
clients=command.clients,
transient=command.clients is not None,
)
return
if isinstance(command, ClientListCommand):
@ -69,12 +78,15 @@ def _cmd(command_data: bytes) -> None:
if isinstance(command, KickCommand):
assert _ba.app.server is not None
_ba.app.server.kick(client_id=command.client_id,
ban_time=command.ban_time)
_ba.app.server.kick(
client_id=command.client_id, ban_time=command.ban_time
)
return
print(f'{Clr.SRED}ERROR: server process'
f' got unknown command: {type(command)}{Clr.RST}')
print(
f'{Clr.SRED}ERROR: server process'
f' got unknown command: {type(command)}{Clr.RST}'
)
class ServerController:
@ -105,23 +117,28 @@ class ServerController:
# account sign-in or fetching playlists; this will kick off the
# session once done.
with _ba.Context('ui'):
self._prep_timer = _ba.Timer(0.25,
self._prepare_to_serve,
timetype=TimeType.REAL,
repeat=True)
self._prep_timer = _ba.Timer(
0.25,
self._prepare_to_serve,
timetype=TimeType.REAL,
repeat=True,
)
def print_client_list(self) -> None:
"""Print info about all connected clients."""
import json
roster = _ba.get_game_roster()
title1 = 'Client ID'
title2 = 'Account Name'
title3 = 'Players'
col1 = 10
col2 = 16
out = (f'{Clr.BLD}'
f'{title1:<{col1}} {title2:<{col2}} {title3}'
f'{Clr.RST}')
out = (
f'{Clr.BLD}'
f'{title1:<{col1}} {title2:<{col2}} {title3}'
f'{Clr.RST}'
)
for client in roster:
if client['client_id'] == -1:
continue
@ -153,9 +170,11 @@ class ServerController:
print(f'{Clr.SBLU}Immediate shutdown initiated.{Clr.RST}')
self._execute_shutdown()
else:
print(f'{Clr.SBLU}Shutdown initiated;'
f' server process will exit at the next clean opportunity.'
f'{Clr.RST}')
print(
f'{Clr.SBLU}Shutdown initiated;'
f' server process will exit at the next clean opportunity.'
f'{Clr.RST}'
)
def handle_transition(self) -> bool:
"""Handle transitioning to a new ba.Session or quitting the app.
@ -172,37 +191,45 @@ class ServerController:
def _execute_shutdown(self) -> None:
from ba._language import Lstr
if self._executing_shutdown:
return
self._executing_shutdown = True
timestrval = time.strftime('%c')
if self._shutdown_reason is ShutdownReason.RESTARTING:
_ba.screenmessage(Lstr(resource='internal.serverRestartingText'),
color=(1, 0.5, 0.0))
print(f'{Clr.SBLU}Exiting for server-restart'
f' at {timestrval}.{Clr.RST}')
_ba.screenmessage(
Lstr(resource='internal.serverRestartingText'),
color=(1, 0.5, 0.0),
)
print(
f'{Clr.SBLU}Exiting for server-restart'
f' at {timestrval}.{Clr.RST}'
)
else:
_ba.screenmessage(Lstr(resource='internal.serverShuttingDownText'),
color=(1, 0.5, 0.0))
print(f'{Clr.SBLU}Exiting for server-shutdown'
f' at {timestrval}.{Clr.RST}')
_ba.screenmessage(
Lstr(resource='internal.serverShuttingDownText'),
color=(1, 0.5, 0.0),
)
print(
f'{Clr.SBLU}Exiting for server-shutdown'
f' at {timestrval}.{Clr.RST}'
)
with _ba.Context('ui'):
_ba.timer(2.0, _ba.quit, timetype=TimeType.REAL)
def _run_access_check(self) -> None:
"""Check with the master server to see if we're likely joinable."""
from ba._net import master_server_get
master_server_get(
'bsAccessCheck',
{
'port': _ba.get_game_port(),
'b': _ba.app.build_number
},
{'port': _ba.get_game_port(), 'b': _ba.app.build_number},
callback=self._access_check_response,
)
def _access_check_response(self, data: dict[str, Any] | None) -> None:
import os
if data is None:
print('error on UDP port access check (internet down?)')
else:
@ -216,17 +243,22 @@ class ServerController:
addrstr = ''
poststr = (
'\nSet environment variable BA_ACCESS_CHECK_VERBOSE=1'
' for more info.')
' for more info.'
)
if data['accessible']:
print(f'{Clr.SBLU}Master server access check of{addrstr}'
f' udp port {port} succeeded.\n'
f'Your server appears to be'
f' joinable from the internet.{poststr}{Clr.RST}')
print(
f'{Clr.SBLU}Master server access check of{addrstr}'
f' udp port {port} succeeded.\n'
f'Your server appears to be'
f' joinable from the internet.{poststr}{Clr.RST}'
)
else:
print(f'{Clr.SRED}Master server access check of{addrstr}'
f' udp port {port} failed.\n'
f'Your server does not appear to be'
f' joinable from the internet.{poststr}{Clr.RST}')
print(
f'{Clr.SRED}Master server access check of{addrstr}'
f' udp port {port} failed.\n'
f'Your server does not appear to be'
f' joinable from the internet.{poststr}{Clr.RST}'
)
def _prepare_to_serve(self) -> None:
"""Run in a timer to do prep before beginning to serve."""
@ -248,15 +280,18 @@ class ServerController:
can_launch = True
else:
if not self._playlist_fetch_sent_request:
print(f'{Clr.SBLU}Requesting shared-playlist'
f' {self._config.playlist_code}...{Clr.RST}')
print(
f'{Clr.SBLU}Requesting shared-playlist'
f' {self._config.playlist_code}...{Clr.RST}'
)
add_transaction(
{
'type': 'IMPORT_PLAYLIST',
'code': str(self._config.playlist_code),
'overwrite': True
'overwrite': True,
},
callback=self._on_playlist_fetch_response)
callback=self._on_playlist_fetch_response,
)
run_transactions()
self._playlist_fetch_sent_request = True
@ -278,13 +313,17 @@ class ServerController:
# Once we get here, simply modify our config to use this playlist.
typename = (
'teams' if result['playlistType'] == 'Team Tournament' else
'ffa' if result['playlistType'] == 'Free-for-All' else '??')
'teams'
if result['playlistType'] == 'Team Tournament'
else 'ffa'
if result['playlistType'] == 'Free-for-All'
else '??'
)
plistname = result['playlistName']
print(f'{Clr.SBLU}Got playlist: "{plistname}" ({typename}).{Clr.RST}')
self._playlist_fetch_got_response = True
self._config.session_type = typename
self._playlist_name = (result['playlistName'])
self._playlist_name = result['playlistName']
def _get_session_type(self) -> type[ba.Session]:
# Convert string session type to the class.
@ -296,7 +335,8 @@ class ServerController:
if self._config.session_type == 'coop':
return CoopSession
raise RuntimeError(
f'Invalid session_type: "{self._config.session_type}"')
f'Invalid session_type: "{self._config.session_type}"'
)
def _launch_server_session(self) -> None:
"""Kick off a host-session based on the current server config."""
@ -306,13 +346,17 @@ class ServerController:
sessiontype = self._get_session_type()
if get_v1_account_state() != 'signed_in':
print('WARNING: launch_server_session() expects to run '
'with a signed in server account')
print(
'WARNING: launch_server_session() expects to run '
'with a signed in server account'
)
# If we didn't fetch a playlist but there's an inline one in the
# server-config, pull it in to the game config and use it.
if (self._config.playlist_code is None
and self._config.playlist_inline is not None):
if (
self._config.playlist_code is None
and self._config.playlist_inline is not None
):
self._playlist_name = 'ServerModePlaylist'
if sessiontype is FreeForAllSession:
ptypename = 'Free-for-All'
@ -325,12 +369,14 @@ class ServerController:
# Need to add this in a transaction instead of just setting
# it directly or it will get overwritten by the master-server.
add_transaction({
'type': 'ADD_PLAYLIST',
'playlistType': ptypename,
'playlistName': self._playlist_name,
'playlist': self._config.playlist_inline
})
add_transaction(
{
'type': 'ADD_PLAYLIST',
'playlistType': ptypename,
'playlistName': self._playlist_name,
'playlist': self._config.playlist_inline,
}
)
run_transactions()
if self._first_run:
@ -338,17 +384,20 @@ class ServerController:
startupmsg = (
f'{Clr.BLD}{Clr.BLU}{_ba.appnameupper()} {app.version}'
f' ({app.build_number})'
f' entering server-mode {curtimestr}{Clr.RST}')
f' entering server-mode {curtimestr}{Clr.RST}'
)
logging.info(startupmsg)
if sessiontype is FreeForAllSession:
appcfg['Free-for-All Playlist Selection'] = self._playlist_name
appcfg['Free-for-All Playlist Randomize'] = (
self._config.playlist_shuffle)
appcfg[
'Free-for-All Playlist Randomize'
] = self._config.playlist_shuffle
elif sessiontype is DualTeamSession:
appcfg['Team Tournament Playlist Selection'] = self._playlist_name
appcfg['Team Tournament Playlist Randomize'] = (
self._config.playlist_shuffle)
appcfg[
'Team Tournament Playlist Randomize'
] = self._config.playlist_shuffle
elif sessiontype is CoopSession:
app.coop_session_args = {
'campaign': self._config.coop_campaign,
@ -363,7 +412,8 @@ class ServerController:
_ba.set_authenticate_clients(self._config.authenticate_clients)
_ba.set_enable_default_kick_voting(
self._config.enable_default_kick_voting)
self._config.enable_default_kick_voting
)
_ba.set_admins(self._config.admins)
# Call set-enabled last (will push state to the cloud).
@ -376,10 +426,13 @@ class ServerController:
if self._config.stress_test_players is not None:
# Special case: run a stress test.
from ba.internal import run_stress_test
run_stress_test(playlist_type='Random',
playlist_name='__default__',
player_count=self._config.stress_test_players,
round_duration=30)
run_stress_test(
playlist_type='Random',
playlist_name='__default__',
player_count=self._config.stress_test_players,
round_duration=30,
)
else:
_ba.new_host_session(sessiontype)

View File

@ -70,12 +70,14 @@ class Session:
"""All the ba.SessionTeams in the Session. Most things should use the
list of ba.Team-s in ba.Activity; not this."""
def __init__(self,
depsets: Sequence[ba.DependencySet],
team_names: Sequence[str] | None = None,
team_colors: Sequence[Sequence[float]] | None = None,
min_players: int = 1,
max_players: int = 8):
def __init__(
self,
depsets: Sequence[ba.DependencySet],
team_names: Sequence[str] | None = None,
team_colors: Sequence[Sequence[float]] | None = None,
min_players: int = 1,
max_players: int = 8,
):
"""Instantiate a session.
depsets should be a sequence of successfully resolved ba.DependencySet
@ -116,10 +118,12 @@ class Session:
# Throw a combined exception if we found anything missing.
if missing_asset_packages:
raise DependencyError([
Dependency(AssetPackage, set_id)
for set_id in missing_asset_packages
])
raise DependencyError(
[
Dependency(AssetPackage, set_id)
for set_id in missing_asset_packages
]
)
# Ok; looks like our dependencies check out.
# Now give the engine a list of asset-set-ids to pass along to clients.
@ -152,27 +156,33 @@ class Session:
self._wants_to_end = False
self._ending = False
self._activity_should_end_immediately = False
self._activity_should_end_immediately_results: (ba.GameResults
| None) = None
self._activity_should_end_immediately_results: (
ba.GameResults | None
) = None
self._activity_should_end_immediately_delay = 0.0
# Create static teams if we're using them.
if self.use_teams:
if team_names is None:
raise RuntimeError(
'use_teams is True but team_names not provided.')
'use_teams is True but team_names not provided.'
)
if team_colors is None:
raise RuntimeError(
'use_teams is True but team_colors not provided.')
'use_teams is True but team_colors not provided.'
)
if len(team_colors) != len(team_names):
raise RuntimeError(f'Got {len(team_names)} team_names'
f' and {len(team_colors)} team_colors;'
f' these numbers must match.')
raise RuntimeError(
f'Got {len(team_names)} team_names'
f' and {len(team_colors)} team_colors;'
f' these numbers must match.'
)
for i, color in enumerate(team_colors):
team = SessionTeam(team_id=self._next_team_id,
name=GameActivity.get_team_display_string(
team_names[i]),
color=color)
team = SessionTeam(
team_id=self._next_team_id,
name=GameActivity.get_team_display_string(team_names[i]),
color=color,
)
self.sessionteams.append(team)
self._next_team_id += 1
try:
@ -218,12 +228,15 @@ class Session:
# Print a rejection message *only* to the client trying to
# join (prevents spamming everyone else in the game).
_ba.playsound(_ba.getsound('error'))
_ba.screenmessage(Lstr(resource='playerLimitReachedText',
subs=[('${COUNT}',
str(self.max_players))]),
color=(0.8, 0.0, 0.0),
clients=[player.inputdevice.client_id],
transient=True)
_ba.screenmessage(
Lstr(
resource='playerLimitReachedText',
subs=[('${COUNT}', str(self.max_players))],
),
color=(0.8, 0.0, 0.0),
clients=[player.inputdevice.client_id],
transient=True,
)
return False
_ba.playsound(_ba.getsound('dripity'))
@ -233,8 +246,10 @@ class Session:
"""Called when a previously-accepted ba.SessionPlayer leaves."""
if sessionplayer not in self.sessionplayers:
print('ERROR: Session.on_player_leave called'
' for player not in our list.')
print(
'ERROR: Session.on_player_leave called'
' for player not in our list.'
)
return
_ba.playsound(_ba.getsound('playerLeft'))
@ -256,15 +271,20 @@ class Session:
assert sessionteam is not None
_ba.screenmessage(
Lstr(resource='playerLeftText',
subs=[('${PLAYER}', sessionplayer.getname(full=True))]))
Lstr(
resource='playerLeftText',
subs=[('${PLAYER}', sessionplayer.getname(full=True))],
)
)
# Remove them from their SessionTeam.
if sessionplayer in sessionteam.players:
sessionteam.players.remove(sessionplayer)
else:
print('SessionPlayer not found in SessionTeam'
' in on_player_leave.')
print(
'SessionPlayer not found in SessionTeam'
' in on_player_leave.'
)
# Grab their activity-specific player instance.
player = sessionplayer.activityplayer
@ -284,8 +304,9 @@ class Session:
# Now remove them from the session list.
self.sessionplayers.remove(sessionplayer)
def _remove_player_team(self, sessionteam: ba.SessionTeam,
activity: ba.Activity | None) -> None:
def _remove_player_team(
self, sessionteam: ba.SessionTeam, activity: ba.Activity | None
) -> None:
"""Remove the player-specific team in non-teams mode."""
# They should have been the only one on their team.
@ -306,14 +327,17 @@ class Session:
self.on_team_leave(sessionteam)
except Exception:
print_exception(
f'Error in on_team_leave for Session {self}.')
f'Error in on_team_leave for Session {self}.'
)
else:
print('Team no in Session teams in on_player_leave.')
try:
sessionteam.leave()
except Exception:
print_exception(f'Error clearing sessiondata'
f' for team {sessionteam} in session {self}.')
print_exception(
f'Error clearing sessiondata'
f' for team {sessionteam} in session {self}.'
)
def end(self) -> None:
"""Initiates an end to the session and a return to the main menu.
@ -329,17 +353,20 @@ class Session:
"""(internal)"""
from ba._activitytypes import EndSessionActivity
from ba._generated.enums import TimeType
with _ba.Context(self):
curtime = _ba.time(TimeType.REAL)
if self._ending:
# Ignore repeats unless its been a while.
assert self._launch_end_session_activity_time is not None
since_last = (curtime - self._launch_end_session_activity_time)
since_last = curtime - self._launch_end_session_activity_time
if since_last < 30.0:
return
print_error(
'_launch_end_session_activity called twice (since_last=' +
str(since_last) + ')')
'_launch_end_session_activity called twice (since_last='
+ str(since_last)
+ ')'
)
self._launch_end_session_activity_time = curtime
self.setactivity(_ba.newactivity(EndSessionActivity))
self._wants_to_end = False
@ -351,8 +378,9 @@ class Session:
def on_team_leave(self, team: ba.SessionTeam) -> None:
"""Called when a ba.Team is leaving the session."""
def end_activity(self, activity: ba.Activity, results: Any, delay: float,
force: bool) -> None:
def end_activity(
self, activity: ba.Activity, results: Any, delay: float, force: bool
) -> None:
"""Commence shutdown of a ba.Activity (if not already occurring).
'delay' is the time delay before the Activity actually ends
@ -385,7 +413,8 @@ class Session:
self._activity_end_timer = _ba.Timer(
delay,
Call(self._complete_end_activity, activity, results),
timetype=TimeType.BASE)
timetype=TimeType.BASE,
)
def handlemessage(self, msg: Any) -> Any:
"""General message handling; can be passed any message object."""
@ -407,7 +436,6 @@ class Session:
return None
class _SetActivityScopedLock:
def __init__(self, session: ba.Session) -> None:
self._session = session
if session._in_set_activity:
@ -442,12 +470,16 @@ class Session:
return
if self._next_activity is not None:
raise RuntimeError('Activity switch already in progress (to ' +
str(self._next_activity) + ')')
raise RuntimeError(
'Activity switch already in progress (to '
+ str(self._next_activity)
+ ')'
)
prev_activity = self._activity_retained
prev_globals = (prev_activity.globalsnode
if prev_activity is not None else None)
prev_globals = (
prev_activity.globalsnode if prev_activity is not None else None
)
# Let the activity do its thing.
activity.transition_in(prev_globals)
@ -476,9 +508,11 @@ class Session:
# will trigger the next activity to run).
if prev_activity is not None:
with _ba.Context('ui'):
_ba.timer(max(0.0, activity.transition_time),
prev_activity.expire,
timetype=TimeType.REAL)
_ba.timer(
max(0.0, activity.transition_time),
prev_activity.expire,
timetype=TimeType.REAL,
)
self._in_set_activity = False
def getactivity(self) -> ba.Activity | None:
@ -495,15 +529,18 @@ class Session:
"""
return []
def _complete_end_activity(self, activity: ba.Activity,
results: Any) -> None:
def _complete_end_activity(
self, activity: ba.Activity, results: Any
) -> None:
# Run the subclass callback in the session context.
try:
with _ba.Context(self):
self.on_activity_end(activity, results)
except Exception:
print_exception(f'Error in on_activity_end() for session {self}'
f' activity {activity} with results {results}')
print_exception(
f'Error in on_activity_end() for session {self}'
f' activity {activity} with results {results}'
)
def _request_player(self, sessionplayer: ba.SessionPlayer) -> bool:
"""Called by the native layer when a player wants to join."""
@ -571,7 +608,8 @@ class Session:
if self._activity_should_end_immediately:
self._activity_retained.end(
self._activity_should_end_immediately_results,
self._activity_should_end_immediately_delay)
self._activity_should_end_immediately_delay,
)
def _on_player_ready(self, chooser: ba.Chooser) -> None:
"""Called when a ba.Player has checked themself ready."""
@ -601,8 +639,10 @@ class Session:
self._complete_end_activity(activity, {})
else:
_ba.screenmessage(
Lstr(resource='notEnoughPlayersText',
subs=[('${COUNT}', str(min_players))]),
Lstr(
resource='notEnoughPlayersText',
subs=[('${COUNT}', str(min_players))],
),
color=(1, 1, 0),
)
_ba.playsound(_ba.getsound('error'))
@ -613,7 +653,8 @@ class Session:
lobby.remove_chooser(chooser.getplayer())
def transitioning_out_activity_was_freed(
self, can_show_ad_on_death: bool) -> None:
self, can_show_ad_on_death: bool
) -> None:
"""(internal)"""
# pylint: disable=cyclic-import
from ba._apputils import garbage_collect
@ -632,10 +673,12 @@ class Session:
def _add_chosen_player(self, chooser: ba.Chooser) -> ba.SessionPlayer:
from ba._team import SessionTeam
sessionplayer = chooser.getplayer()
assert sessionplayer in self.sessionplayers, (
'SessionPlayer not found in session '
'player-list after chooser selection.')
'player-list after chooser selection.'
)
activity = self._activity_weak()
assert activity is not None
@ -646,20 +689,26 @@ class Session:
# We can pass it to the current activity if it has already begun
# (otherwise it'll get passed once begin is called).
pass_to_activity = (activity.has_begun()
and not activity.is_joining_activity)
pass_to_activity = (
activity.has_begun() and not activity.is_joining_activity
)
# However, if we're not allowing mid-game joins, don't actually pass;
# just announce the arrival and say they'll partake next round.
if pass_to_activity:
if not (activity.allow_mid_activity_joins
and self.should_allow_mid_activity_joins(activity)):
if not (
activity.allow_mid_activity_joins
and self.should_allow_mid_activity_joins(activity)
):
pass_to_activity = False
with _ba.Context(self):
_ba.screenmessage(
Lstr(resource='playerDelayedJoinText',
subs=[('${PLAYER}',
sessionplayer.getname(full=True))]),
Lstr(
resource='playerDelayedJoinText',
subs=[
('${PLAYER}', sessionplayer.getname(full=True))
],
),
color=(0, 1, 0),
)
@ -691,10 +740,12 @@ class Session:
assert sessionplayer not in sessionteam.players
sessionteam.players.append(sessionplayer)
sessionplayer.setdata(team=sessionteam,
character=chooser.get_character_name(),
color=chooser.get_color(),
highlight=chooser.get_highlight())
sessionplayer.setdata(
team=sessionteam,
character=chooser.get_character_name(),
color=chooser.get_color(),
highlight=chooser.get_highlight(),
)
self.stats.register_sessionplayer(sessionplayer)
if pass_to_activity:

View File

@ -28,6 +28,7 @@ class BoolSetting(Setting):
Category: Settings Classes
"""
default: bool
@ -37,6 +38,7 @@ class IntSetting(Setting):
Category: Settings Classes
"""
default: int
min_value: int = 0
max_value: int = 9999
@ -49,6 +51,7 @@ class FloatSetting(Setting):
Category: Settings Classes
"""
default: float
min_value: float = 0.0
max_value: float = 9999.0
@ -61,6 +64,7 @@ class ChoiceSetting(Setting):
Category: Settings Classes
"""
choices: list[tuple[str, Any]]
@ -70,6 +74,7 @@ class IntChoiceSetting(ChoiceSetting):
Category: Settings Classes
"""
default: int
choices: list[tuple[str, int]]
@ -80,5 +85,6 @@ class FloatChoiceSetting(ChoiceSetting):
Category: Settings Classes
"""
default: float
choices: list[tuple[str, float]]

View File

@ -9,8 +9,13 @@ from typing import TYPE_CHECKING
from dataclasses import dataclass
import _ba
from ba._error import (print_exception, print_error, SessionTeamNotFoundError,
SessionPlayerNotFoundError, NotFoundError)
from ba._error import (
print_exception,
print_error,
SessionTeamNotFoundError,
SessionPlayerNotFoundError,
NotFoundError,
)
if TYPE_CHECKING:
import ba
@ -37,10 +42,16 @@ class PlayerRecord:
still present (stats may be retained for players that leave
mid-game)
"""
character: str
def __init__(self, name: str, name_full: str,
sessionplayer: ba.SessionPlayer, stats: ba.Stats):
def __init__(
self,
name: str,
name_full: str,
sessionplayer: ba.SessionPlayer,
stats: ba.Stats,
):
self.name = name
self.name_full = name_full
self.score = 0
@ -105,8 +116,9 @@ class PlayerRecord:
return stats.getactivity()
return None
def associate_with_sessionplayer(self,
sessionplayer: ba.SessionPlayer) -> None:
def associate_with_sessionplayer(
self, sessionplayer: ba.SessionPlayer
) -> None:
"""Associate this entry with a ba.SessionPlayer."""
self._sessionteam = weakref.ref(sessionplayer.sessionteam)
self.character = sessionplayer.character
@ -129,6 +141,7 @@ class PlayerRecord:
# pylint: disable=too-many-statements
from ba._language import Lstr
from ba._general import Call
self._multi_kill_count += 1
stats = self._stats()
assert stats
@ -169,16 +182,23 @@ class PlayerRecord:
sound = stats.orchestrahitsound4
else:
score = 100
name = Lstr(resource='multiKillText',
subs=[('${COUNT}', str(self._multi_kill_count))])
name = Lstr(
resource='multiKillText',
subs=[('${COUNT}', str(self._multi_kill_count))],
)
color = (1.0, 0.5, 0.0, 1)
scale = 1.3
delay = 1.0
sound = stats.orchestrahitsound4
def _apply(name2: Lstr, score2: int, showpoints2: bool,
color2: tuple[float, float, float, float], scale2: float,
sound2: ba.Sound | None) -> None:
def _apply(
name2: Lstr,
score2: int,
showpoints2: bool,
color2: tuple[float, float, float, float],
scale2: float,
sound2: ba.Sound | None,
) -> None:
from bastd.actor.popuptext import PopupText
# Only award this if they're still alive and we can get
@ -194,18 +214,23 @@ class PlayerRecord:
return
# Jitter position a bit since these often come in clusters.
our_pos = _ba.Vec3(our_pos[0] + (random.random() - 0.5) * 2.0,
our_pos[1] + (random.random() - 0.5) * 2.0,
our_pos[2] + (random.random() - 0.5) * 2.0)
our_pos = _ba.Vec3(
our_pos[0] + (random.random() - 0.5) * 2.0,
our_pos[1] + (random.random() - 0.5) * 2.0,
our_pos[2] + (random.random() - 0.5) * 2.0,
)
activity = self.getactivity()
if activity is not None:
PopupText(Lstr(
value=(('+' + str(score2) + ' ') if showpoints2 else '') +
'${N}',
subs=[('${N}', name2)]),
color=color2,
scale=scale2,
position=our_pos).autoretain()
PopupText(
Lstr(
value=(('+' + str(score2) + ' ') if showpoints2 else '')
+ '${N}',
subs=[('${N}', name2)],
),
color=color2,
scale=scale2,
position=our_pos,
).autoretain()
if sound2:
_ba.playsound(sound2)
@ -219,7 +244,8 @@ class PlayerRecord:
if name is not None:
_ba.timer(
0.3 + delay,
Call(_apply, name, score, showpoints, color, scale, sound))
Call(_apply, name, score, showpoints, color, scale, sound),
)
# Keep the tally rollin'...
# set a timer for a bit in the future.
@ -296,8 +322,9 @@ class Stats:
self._player_records[name].associate_with_sessionplayer(player)
else:
name_full = player.getname(full=True)
self._player_records[name] = PlayerRecord(name, name_full, player,
self)
self._player_records[name] = PlayerRecord(
name, name_full, player, self
)
def get_records(self) -> dict[str, ba.PlayerRecord]:
"""Get PlayerRecord corresponding to still-existing players."""
@ -311,20 +338,22 @@ class Stats:
records[record_id] = record
return records
def player_scored(self,
player: ba.Player,
base_points: int = 1,
target: Sequence[float] | None = None,
kill: bool = False,
victim_player: ba.Player | None = None,
scale: float = 1.0,
color: Sequence[float] | None = None,
title: str | ba.Lstr | None = None,
screenmessage: bool = True,
display: bool = True,
importance: int = 1,
showpoints: bool = True,
big_message: bool = False) -> int:
def player_scored(
self,
player: ba.Player,
base_points: int = 1,
target: Sequence[float] | None = None,
kill: bool = False,
victim_player: ba.Player | None = None,
scale: float = 1.0,
color: Sequence[float] | None = None,
title: str | ba.Lstr | None = None,
screenmessage: bool = True,
display: bool = True,
importance: int = 1,
showpoints: bool = True,
big_message: bool = False,
) -> int:
"""Register a score for the player.
Return value is actual score with multipliers and such factored in.
@ -338,6 +367,7 @@ class Stats:
from ba import _math
from ba._gameactivity import GameActivity
from ba._language import Lstr
del victim_player # Currently unused.
name = player.getname()
s_player = self._player_records[name]
@ -361,9 +391,12 @@ class Stats:
if isinstance(activity, GameActivity):
name_full = player.getname(full=True, icon=False)
activity.show_zoom_message(
Lstr(resource='nameScoresText',
subs=[('${NAME}', name_full)]),
color=_math.normalized_color(player.team.color))
Lstr(
resource='nameScoresText',
subs=[('${NAME}', name_full)],
),
color=_math.normalized_color(player.team.color),
)
except Exception:
print_exception('error showing big_message')
@ -376,21 +409,26 @@ class Stats:
# If display-pos is *way* lower than us, raise it up
# (so we can still see scores from dudes that fell off cliffs).
display_pos = (target[0], max(target[1], our_pos[1] - 2.0),
min(target[2], our_pos[2] + 2.0))
display_pos = (
target[0],
max(target[1], our_pos[1] - 2.0),
min(target[2], our_pos[2] + 2.0),
)
activity = self.getactivity()
if activity is not None:
if title is not None:
sval = Lstr(value='+${A} ${B}',
subs=[('${A}', str(points)),
('${B}', title)])
sval = Lstr(
value='+${A} ${B}',
subs=[('${A}', str(points)), ('${B}', title)],
)
else:
sval = Lstr(value='+${A}',
subs=[('${A}', str(points))])
PopupText(sval,
color=display_color,
scale=1.2 * scale,
position=display_pos).autoretain()
sval = Lstr(value='+${A}', subs=[('${A}', str(points))])
PopupText(
sval,
color=display_color,
scale=1.2 * scale,
position=display_pos,
).autoretain()
# Tally kills.
if kill:
@ -400,11 +438,12 @@ class Stats:
# Report non-kill scorings.
try:
if screenmessage and not kill:
_ba.screenmessage(Lstr(resource='nameScoresText',
subs=[('${NAME}', name)]),
top=True,
color=player.color,
image=player.get_icon())
_ba.screenmessage(
Lstr(resource='nameScoresText', subs=[('${NAME}', name)]),
top=True,
color=player.color,
image=player.get_icon(),
)
except Exception:
print_exception('error announcing score')
@ -419,12 +458,15 @@ class Stats:
return points
def player_was_killed(self,
player: ba.Player,
killed: bool = False,
killer: ba.Player | None = None) -> None:
def player_was_killed(
self,
player: ba.Player,
killed: bool = False,
killer: ba.Player | None = None,
) -> None:
"""Should be called when a player is killed."""
from ba._language import Lstr
name = player.getname()
prec = self._player_records[name]
prec.streak = 0
@ -434,33 +476,47 @@ class Stats:
try:
if killed and _ba.getactivity().announce_player_deaths:
if killer is player:
_ba.screenmessage(Lstr(resource='nameSuicideText',
subs=[('${NAME}', name)]),
top=True,
color=player.color,
image=player.get_icon())
_ba.screenmessage(
Lstr(
resource='nameSuicideText', subs=[('${NAME}', name)]
),
top=True,
color=player.color,
image=player.get_icon(),
)
elif killer is not None:
if killer.team is player.team:
_ba.screenmessage(Lstr(resource='nameBetrayedText',
subs=[('${NAME}',
killer.getname()),
('${VICTIM}', name)]),
top=True,
color=killer.color,
image=killer.get_icon())
_ba.screenmessage(
Lstr(
resource='nameBetrayedText',
subs=[
('${NAME}', killer.getname()),
('${VICTIM}', name),
],
),
top=True,
color=killer.color,
image=killer.get_icon(),
)
else:
_ba.screenmessage(Lstr(resource='nameKilledText',
subs=[('${NAME}',
killer.getname()),
('${VICTIM}', name)]),
top=True,
color=killer.color,
image=killer.get_icon())
_ba.screenmessage(
Lstr(
resource='nameKilledText',
subs=[
('${NAME}', killer.getname()),
('${VICTIM}', name),
],
),
top=True,
color=killer.color,
image=killer.get_icon(),
)
else:
_ba.screenmessage(Lstr(resource='nameDiedText',
subs=[('${NAME}', name)]),
top=True,
color=player.color,
image=player.get_icon())
_ba.screenmessage(
Lstr(resource='nameDiedText', subs=[('${NAME}', name)]),
top=True,
color=player.color,
image=player.get_icon(),
)
except Exception:
print_exception('error announcing kill')

View File

@ -24,14 +24,17 @@ def get_store_item_name_translated(item_name: str) -> ba.Lstr:
# pylint: disable=cyclic-import
from ba import _language
from ba import _map
item_info = get_store_item(item_name)
if item_name.startswith('characters.'):
return _language.Lstr(translate=('characterNames',
item_info['character']))
return _language.Lstr(
translate=('characterNames', item_info['character'])
)
if item_name in ['upgrades.pro', 'pro']:
return _language.Lstr(resource='store.bombSquadProNameText',
subs=[('${APP_NAME}',
_language.Lstr(resource='titleText'))])
return _language.Lstr(
resource='store.bombSquadProNameText',
subs=[('${APP_NAME}', _language.Lstr(resource='titleText'))],
)
if item_name.startswith('maps.'):
map_type: type[ba.Map] = item_info['map_type']
return _map.get_map_display_string(map_type.name)
@ -64,6 +67,7 @@ def get_store_items() -> dict[str, dict]:
# pylint: disable=cyclic-import
from ba._generated.enums import SpecialChar
from bastd import maps
if _ba.app.store_items is None:
from bastd.game import ninjafight
from bastd.game import meteorshower
@ -73,114 +77,64 @@ def get_store_items() -> dict[str, dict]:
# IMPORTANT - need to keep this synced with the master server.
# (doing so manually for now)
_ba.app.store_items = {
'characters.kronk': {
'character': 'Kronk'
},
'characters.zoe': {
'character': 'Zoe'
},
'characters.jackmorgan': {
'character': 'Jack Morgan'
},
'characters.mel': {
'character': 'Mel'
},
'characters.snakeshadow': {
'character': 'Snake Shadow'
},
'characters.bones': {
'character': 'Bones'
},
'characters.kronk': {'character': 'Kronk'},
'characters.zoe': {'character': 'Zoe'},
'characters.jackmorgan': {'character': 'Jack Morgan'},
'characters.mel': {'character': 'Mel'},
'characters.snakeshadow': {'character': 'Snake Shadow'},
'characters.bones': {'character': 'Bones'},
'characters.bernard': {
'character': 'Bernard',
'highlight': (0.6, 0.5, 0.8)
},
'characters.pixie': {
'character': 'Pixel'
},
'characters.wizard': {
'character': 'Grumbledorf'
},
'characters.frosty': {
'character': 'Frosty'
},
'characters.pascal': {
'character': 'Pascal'
},
'characters.cyborg': {
'character': 'B-9000'
},
'characters.agent': {
'character': 'Agent Johnson'
},
'characters.taobaomascot': {
'character': 'Taobao Mascot'
},
'characters.santa': {
'character': 'Santa Claus'
},
'characters.bunny': {
'character': 'Easter Bunny'
'highlight': (0.6, 0.5, 0.8),
},
'characters.pixie': {'character': 'Pixel'},
'characters.wizard': {'character': 'Grumbledorf'},
'characters.frosty': {'character': 'Frosty'},
'characters.pascal': {'character': 'Pascal'},
'characters.cyborg': {'character': 'B-9000'},
'characters.agent': {'character': 'Agent Johnson'},
'characters.taobaomascot': {'character': 'Taobao Mascot'},
'characters.santa': {'character': 'Santa Claus'},
'characters.bunny': {'character': 'Easter Bunny'},
'pro': {},
'maps.lake_frigid': {
'map_type': maps.LakeFrigid
},
'maps.lake_frigid': {'map_type': maps.LakeFrigid},
'games.ninja_fight': {
'gametype': ninjafight.NinjaFightGame,
'previewTex': 'courtyardPreview'
'previewTex': 'courtyardPreview',
},
'games.meteor_shower': {
'gametype': meteorshower.MeteorShowerGame,
'previewTex': 'rampagePreview'
'previewTex': 'rampagePreview',
},
'games.target_practice': {
'gametype': targetpractice.TargetPracticeGame,
'previewTex': 'doomShroomPreview'
'previewTex': 'doomShroomPreview',
},
'games.easter_egg_hunt': {
'gametype': easteregghunt.EasterEggHuntGame,
'previewTex': 'towerDPreview'
'previewTex': 'towerDPreview',
},
'icons.flag_us': {
'icon': _ba.charstr(SpecialChar.FLAG_UNITED_STATES)
},
'icons.flag_mexico': {
'icon': _ba.charstr(SpecialChar.FLAG_MEXICO)
},
'icons.flag_mexico': {'icon': _ba.charstr(SpecialChar.FLAG_MEXICO)},
'icons.flag_germany': {
'icon': _ba.charstr(SpecialChar.FLAG_GERMANY)
},
'icons.flag_brazil': {
'icon': _ba.charstr(SpecialChar.FLAG_BRAZIL)
},
'icons.flag_russia': {
'icon': _ba.charstr(SpecialChar.FLAG_RUSSIA)
},
'icons.flag_china': {
'icon': _ba.charstr(SpecialChar.FLAG_CHINA)
},
'icons.flag_brazil': {'icon': _ba.charstr(SpecialChar.FLAG_BRAZIL)},
'icons.flag_russia': {'icon': _ba.charstr(SpecialChar.FLAG_RUSSIA)},
'icons.flag_china': {'icon': _ba.charstr(SpecialChar.FLAG_CHINA)},
'icons.flag_uk': {
'icon': _ba.charstr(SpecialChar.FLAG_UNITED_KINGDOM)
},
'icons.flag_canada': {
'icon': _ba.charstr(SpecialChar.FLAG_CANADA)
},
'icons.flag_india': {
'icon': _ba.charstr(SpecialChar.FLAG_INDIA)
},
'icons.flag_japan': {
'icon': _ba.charstr(SpecialChar.FLAG_JAPAN)
},
'icons.flag_france': {
'icon': _ba.charstr(SpecialChar.FLAG_FRANCE)
},
'icons.flag_canada': {'icon': _ba.charstr(SpecialChar.FLAG_CANADA)},
'icons.flag_india': {'icon': _ba.charstr(SpecialChar.FLAG_INDIA)},
'icons.flag_japan': {'icon': _ba.charstr(SpecialChar.FLAG_JAPAN)},
'icons.flag_france': {'icon': _ba.charstr(SpecialChar.FLAG_FRANCE)},
'icons.flag_indonesia': {
'icon': _ba.charstr(SpecialChar.FLAG_INDONESIA)
},
'icons.flag_italy': {
'icon': _ba.charstr(SpecialChar.FLAG_ITALY)
},
'icons.flag_italy': {'icon': _ba.charstr(SpecialChar.FLAG_ITALY)},
'icons.flag_south_korea': {
'icon': _ba.charstr(SpecialChar.FLAG_SOUTH_KOREA)
},
@ -190,15 +144,9 @@ def get_store_items() -> dict[str, dict]:
'icons.flag_uae': {
'icon': _ba.charstr(SpecialChar.FLAG_UNITED_ARAB_EMIRATES)
},
'icons.flag_qatar': {
'icon': _ba.charstr(SpecialChar.FLAG_QATAR)
},
'icons.flag_egypt': {
'icon': _ba.charstr(SpecialChar.FLAG_EGYPT)
},
'icons.flag_kuwait': {
'icon': _ba.charstr(SpecialChar.FLAG_KUWAIT)
},
'icons.flag_qatar': {'icon': _ba.charstr(SpecialChar.FLAG_QATAR)},
'icons.flag_egypt': {'icon': _ba.charstr(SpecialChar.FLAG_EGYPT)},
'icons.flag_kuwait': {'icon': _ba.charstr(SpecialChar.FLAG_KUWAIT)},
'icons.flag_algeria': {
'icon': _ba.charstr(SpecialChar.FLAG_ALGERIA)
},
@ -217,69 +165,33 @@ def get_store_items() -> dict[str, dict]:
'icons.flag_singapore': {
'icon': _ba.charstr(SpecialChar.FLAG_SINGAPORE)
},
'icons.flag_iran': {
'icon': _ba.charstr(SpecialChar.FLAG_IRAN)
},
'icons.flag_poland': {
'icon': _ba.charstr(SpecialChar.FLAG_POLAND)
},
'icons.flag_iran': {'icon': _ba.charstr(SpecialChar.FLAG_IRAN)},
'icons.flag_poland': {'icon': _ba.charstr(SpecialChar.FLAG_POLAND)},
'icons.flag_argentina': {
'icon': _ba.charstr(SpecialChar.FLAG_ARGENTINA)
},
'icons.flag_philippines': {
'icon': _ba.charstr(SpecialChar.FLAG_PHILIPPINES)
},
'icons.flag_chile': {
'icon': _ba.charstr(SpecialChar.FLAG_CHILE)
},
'icons.fedora': {
'icon': _ba.charstr(SpecialChar.FEDORA)
},
'icons.hal': {
'icon': _ba.charstr(SpecialChar.HAL)
},
'icons.crown': {
'icon': _ba.charstr(SpecialChar.CROWN)
},
'icons.yinyang': {
'icon': _ba.charstr(SpecialChar.YIN_YANG)
},
'icons.eyeball': {
'icon': _ba.charstr(SpecialChar.EYE_BALL)
},
'icons.skull': {
'icon': _ba.charstr(SpecialChar.SKULL)
},
'icons.heart': {
'icon': _ba.charstr(SpecialChar.HEART)
},
'icons.dragon': {
'icon': _ba.charstr(SpecialChar.DRAGON)
},
'icons.helmet': {
'icon': _ba.charstr(SpecialChar.HELMET)
},
'icons.mushroom': {
'icon': _ba.charstr(SpecialChar.MUSHROOM)
},
'icons.ninja_star': {
'icon': _ba.charstr(SpecialChar.NINJA_STAR)
},
'icons.flag_chile': {'icon': _ba.charstr(SpecialChar.FLAG_CHILE)},
'icons.fedora': {'icon': _ba.charstr(SpecialChar.FEDORA)},
'icons.hal': {'icon': _ba.charstr(SpecialChar.HAL)},
'icons.crown': {'icon': _ba.charstr(SpecialChar.CROWN)},
'icons.yinyang': {'icon': _ba.charstr(SpecialChar.YIN_YANG)},
'icons.eyeball': {'icon': _ba.charstr(SpecialChar.EYE_BALL)},
'icons.skull': {'icon': _ba.charstr(SpecialChar.SKULL)},
'icons.heart': {'icon': _ba.charstr(SpecialChar.HEART)},
'icons.dragon': {'icon': _ba.charstr(SpecialChar.DRAGON)},
'icons.helmet': {'icon': _ba.charstr(SpecialChar.HELMET)},
'icons.mushroom': {'icon': _ba.charstr(SpecialChar.MUSHROOM)},
'icons.ninja_star': {'icon': _ba.charstr(SpecialChar.NINJA_STAR)},
'icons.viking_helmet': {
'icon': _ba.charstr(SpecialChar.VIKING_HELMET)
},
'icons.moon': {
'icon': _ba.charstr(SpecialChar.MOON)
},
'icons.spider': {
'icon': _ba.charstr(SpecialChar.SPIDER)
},
'icons.fireball': {
'icon': _ba.charstr(SpecialChar.FIREBALL)
},
'icons.mikirog': {
'icon': _ba.charstr(SpecialChar.MIKIROG)
},
'icons.moon': {'icon': _ba.charstr(SpecialChar.MOON)},
'icons.spider': {'icon': _ba.charstr(SpecialChar.SPIDER)},
'icons.fireball': {'icon': _ba.charstr(SpecialChar.FIREBALL)},
'icons.mikirog': {'icon': _ba.charstr(SpecialChar.MIKIROG)},
}
store_items = _ba.app.store_items
assert store_items is not None
@ -289,97 +201,107 @@ def get_store_items() -> dict[str, dict]:
def get_store_layout() -> dict[str, list[dict[str, Any]]]:
"""Return what's available in the store at a given time.
Categorized by tab and by section."""
Categorized by tab and by section."""
if _ba.app.store_layout is None:
_ba.app.store_layout = {
'characters': [{
'items': []
}],
'extras': [{
'items': ['pro']
}],
'maps': [{
'items': ['maps.lake_frigid']
}],
'characters': [{'items': []}],
'extras': [{'items': ['pro']}],
'maps': [{'items': ['maps.lake_frigid']}],
'minigames': [],
'icons': [{
'items': [
'icons.mushroom',
'icons.heart',
'icons.eyeball',
'icons.yinyang',
'icons.hal',
'icons.flag_us',
'icons.flag_mexico',
'icons.flag_germany',
'icons.flag_brazil',
'icons.flag_russia',
'icons.flag_china',
'icons.flag_uk',
'icons.flag_canada',
'icons.flag_india',
'icons.flag_japan',
'icons.flag_france',
'icons.flag_indonesia',
'icons.flag_italy',
'icons.flag_south_korea',
'icons.flag_netherlands',
'icons.flag_uae',
'icons.flag_qatar',
'icons.flag_egypt',
'icons.flag_kuwait',
'icons.flag_algeria',
'icons.flag_saudi_arabia',
'icons.flag_malaysia',
'icons.flag_czech_republic',
'icons.flag_australia',
'icons.flag_singapore',
'icons.flag_iran',
'icons.flag_poland',
'icons.flag_argentina',
'icons.flag_philippines',
'icons.flag_chile',
'icons.moon',
'icons.fedora',
'icons.spider',
'icons.ninja_star',
'icons.skull',
'icons.dragon',
'icons.viking_helmet',
'icons.fireball',
'icons.helmet',
'icons.crown',
]
}]
'icons': [
{
'items': [
'icons.mushroom',
'icons.heart',
'icons.eyeball',
'icons.yinyang',
'icons.hal',
'icons.flag_us',
'icons.flag_mexico',
'icons.flag_germany',
'icons.flag_brazil',
'icons.flag_russia',
'icons.flag_china',
'icons.flag_uk',
'icons.flag_canada',
'icons.flag_india',
'icons.flag_japan',
'icons.flag_france',
'icons.flag_indonesia',
'icons.flag_italy',
'icons.flag_south_korea',
'icons.flag_netherlands',
'icons.flag_uae',
'icons.flag_qatar',
'icons.flag_egypt',
'icons.flag_kuwait',
'icons.flag_algeria',
'icons.flag_saudi_arabia',
'icons.flag_malaysia',
'icons.flag_czech_republic',
'icons.flag_australia',
'icons.flag_singapore',
'icons.flag_iran',
'icons.flag_poland',
'icons.flag_argentina',
'icons.flag_philippines',
'icons.flag_chile',
'icons.moon',
'icons.fedora',
'icons.spider',
'icons.ninja_star',
'icons.skull',
'icons.dragon',
'icons.viking_helmet',
'icons.fireball',
'icons.helmet',
'icons.crown',
]
}
],
}
store_layout = _ba.app.store_layout
assert store_layout is not None
store_layout['characters'] = [{
'items': [
'characters.kronk', 'characters.zoe', 'characters.jackmorgan',
'characters.mel', 'characters.snakeshadow', 'characters.bones',
'characters.bernard', 'characters.agent', 'characters.frosty',
'characters.pascal', 'characters.pixie'
]
}]
store_layout['minigames'] = [{
'items': [
'games.ninja_fight', 'games.meteor_shower', 'games.target_practice'
]
}]
store_layout['characters'] = [
{
'items': [
'characters.kronk',
'characters.zoe',
'characters.jackmorgan',
'characters.mel',
'characters.snakeshadow',
'characters.bones',
'characters.bernard',
'characters.agent',
'characters.frosty',
'characters.pascal',
'characters.pixie',
]
}
]
store_layout['minigames'] = [
{
'items': [
'games.ninja_fight',
'games.meteor_shower',
'games.target_practice',
]
}
]
if _internal.get_v1_account_misc_read_val('xmas', False):
store_layout['characters'][0]['items'].append('characters.santa')
store_layout['characters'][0]['items'].append('characters.wizard')
store_layout['characters'][0]['items'].append('characters.cyborg')
if _internal.get_v1_account_misc_read_val('easter', False):
store_layout['characters'].append({
'title': 'store.holidaySpecialText',
'items': ['characters.bunny']
})
store_layout['minigames'].append({
'title': 'store.holidaySpecialText',
'items': ['games.easter_egg_hunt']
})
store_layout['characters'].append(
{'title': 'store.holidaySpecialText', 'items': ['characters.bunny']}
)
store_layout['minigames'].append(
{
'title': 'store.holidaySpecialText',
'items': ['games.easter_egg_hunt'],
}
)
return store_layout
@ -394,7 +316,7 @@ def get_clean_price(price_string: str) -> str:
'$4.99': '$5.00',
'$9.99': '$10.00',
'$19.99': '$20.00',
'$49.99': '$50.00'
'$49.99': '$50.00',
}
return psubs.get(price_string, price_string)
@ -418,19 +340,23 @@ def get_available_purchase_count(tab: str | None = None) -> int:
return count
except Exception:
from ba import _error
_error.print_exception('error calcing available purchases')
return 0
def _calc_count_for_tab(tabval: list[dict[str, Any]], our_tickets: int,
count: int) -> int:
def _calc_count_for_tab(
tabval: list[dict[str, Any]], our_tickets: int, count: int
) -> int:
for section in tabval:
for item in section['items']:
ticket_cost = _internal.get_v1_account_misc_read_val(
'price.' + item, None)
'price.' + item, None
)
if ticket_cost is not None:
if (our_tickets >= ticket_cost
and not _internal.get_purchased(item)):
if our_tickets >= ticket_cost and not _internal.get_purchased(
item
):
count += 1
return count
@ -443,6 +369,7 @@ def get_available_sale_time(tab: str) -> int | None:
try:
import datetime
from ba._generated.enums import TimeType, TimeFormat
app = _ba.app
sale_times: list[int | None] = []
@ -458,18 +385,21 @@ def get_available_sale_time(tab: str) -> int | None:
# If we've got a time-remaining in our config, start there.
if 'PSTR' in config:
app.pro_sale_start_time = int(
_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS))
_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS)
)
app.pro_sale_start_val = config['PSTR']
else:
# We start the timer once we get the duration from
# the server.
start_duration = _internal.get_v1_account_misc_read_val(
'proSaleDurationMinutes', None)
'proSaleDurationMinutes', None
)
if start_duration is not None:
app.pro_sale_start_time = int(
_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS))
app.pro_sale_start_val = (60000 * start_duration)
_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS)
)
app.pro_sale_start_val = 60000 * start_duration
# If we haven't heard from the server yet, no sale..
else:
@ -477,9 +407,13 @@ def get_available_sale_time(tab: str) -> int | None:
assert app.pro_sale_start_val is not None
val: int | None = max(
0, app.pro_sale_start_val -
(_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS) -
app.pro_sale_start_time))
0,
app.pro_sale_start_val
- (
_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS)
- app.pro_sale_start_time
),
)
# Keep the value in the config up to date. I suppose we should
# write the config occasionally but it should happen often enough
@ -496,9 +430,12 @@ def get_available_sale_time(tab: str) -> int | None:
for item in section['items']:
if item in sales_raw:
if not _internal.get_purchased(item):
to_end = ((datetime.datetime.utcfromtimestamp(
sales_raw[item]['e']) -
datetime.datetime.utcnow()).total_seconds())
to_end = (
datetime.datetime.utcfromtimestamp(
sales_raw[item]['e']
)
- datetime.datetime.utcnow()
).total_seconds()
if to_end > 0:
sale_times.append(int(to_end * 1000))
@ -508,6 +445,7 @@ def get_available_sale_time(tab: str) -> int | None:
except Exception:
from ba import _error
_error.print_exception('error calcing sale time')
return None
@ -540,5 +478,6 @@ def get_unowned_game_types() -> set[type[ba.GameActivity]]:
return unowned_games
except Exception:
from ba import _error
_error.print_exception('error calcing un-owned games')
return set()

View File

@ -44,10 +44,12 @@ class SessionTeam:
id: int
"""The unique numeric id of the team."""
def __init__(self,
team_id: int = 0,
name: ba.Lstr | str = '',
color: Sequence[float] = (1.0, 1.0, 1.0)):
def __init__(
self,
team_id: int = 0,
name: ba.Lstr | str = '',
color: Sequence[float] = (1.0, 1.0, 1.0),
):
"""Instantiate a ba.SessionTeam.
In most cases, all teams are provided to you by the ba.Session,
@ -109,7 +111,8 @@ class Team(Generic[PlayerType]):
f' operator (__eq__) which will break internal'
f' logic. Please remove it.\n'
f'For dataclasses you can do "dataclass(eq=False)"'
f' in the class decorator.')
f' in the class decorator.'
)
self.players = []
self._sessionteam = weakref.ref(sessionteam)
@ -120,8 +123,9 @@ class Team(Generic[PlayerType]):
self._expired = False
self._postinited = True
def manual_init(self, team_id: int, name: ba.Lstr | str,
color: tuple[float, ...]) -> None:
def manual_init(
self, team_id: int, name: ba.Lstr | str, color: tuple[float, ...]
) -> None:
"""Manually init a team for uses such as bots."""
self.id = team_id
self.name = name
@ -186,6 +190,7 @@ class Team(Generic[PlayerType]):
if sessionteam is not None:
return sessionteam
from ba import _error
raise _error.SessionTeamNotFoundError()

View File

@ -39,8 +39,9 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]):
returns True for ba.DualTeamSessions and ba.FreeForAllSessions;
False otherwise.
"""
return (issubclass(sessiontype, DualTeamSession)
or issubclass(sessiontype, FreeForAllSession))
return issubclass(sessiontype, DualTeamSession) or issubclass(
sessiontype, FreeForAllSession
)
def __init__(self, settings: dict):
super().__init__(settings)
@ -55,6 +56,7 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]):
# pylint: disable=cyclic-import
from ba._coopsession import CoopSession
from bastd.actor.controlsguide import ControlsGuide
super().on_transition_in()
# On the first game, show the controls UI momentarily.
@ -67,11 +69,13 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]):
lifespan = 10.0
if self.slow_motion:
lifespan *= 0.3
ControlsGuide(delay=delay,
lifespan=lifespan,
scale=0.8,
position=(380, 200),
bright=True).autoretain()
ControlsGuide(
delay=delay,
lifespan=lifespan,
scale=0.8,
position=(380, 200),
bright=True,
).autoretain()
setattr(self.session, attrname, True)
def on_begin(self) -> None:
@ -84,15 +88,19 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]):
elif isinstance(self.session, DualTeamSession):
if len(self.players) >= 4:
from ba import _achievement
_ba.app.ach.award_local_achievement('Team Player')
except Exception:
from ba import _error
_error.print_exception()
def spawn_player_spaz(self,
player: PlayerType,
position: Sequence[float] | None = None,
angle: float | None = None) -> PlayerSpaz:
def spawn_player_spaz(
self,
player: PlayerType,
position: Sequence[float] | None = None,
angle: float | None = None,
) -> PlayerSpaz:
"""
Method override; spawns and wires up a standard ba.PlayerSpaz for
a ba.Player.
@ -103,7 +111,7 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]):
if position is None:
# In teams-mode get our team-start-location.
if isinstance(self.session, DualTeamSession):
position = (self.map.get_start_position(player.team.id))
position = self.map.get_start_position(player.team.id)
else:
# Otherwise do free-for-all spawn locations.
position = self.map.get_ffa_start_position(self.players)
@ -112,11 +120,12 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]):
# FIXME: need to unify these arguments with GameActivity.end()
def end( # type: ignore
self,
results: Any = None,
announce_winning_team: bool = True,
announce_delay: float = 0.1,
force: bool = False) -> None:
self,
results: Any = None,
announce_winning_team: bool = True,
announce_delay: float = 0.1,
force: bool = False,
) -> None:
"""
End the game and announce the single winning team
unless 'announce_winning_team' is False.
@ -141,15 +150,19 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]):
self,
results,
delay=announce_delay,
announce_winning_team=announce_winning_team)
announce_winning_team=announce_winning_team,
)
# For co-op we just pass this up the chain with a delay added
# (in most cases). Team games expect a delay for the announce
# portion in teams/ffa mode so this keeps it consistent.
else:
# don't want delay on restarts..
if (isinstance(results, dict) and 'outcome' in results
and results['outcome'] == 'restart'):
if (
isinstance(results, dict)
and 'outcome' in results
and results['outcome'] == 'restart'
):
delay = 0.0
else:
delay = 2.0

View File

@ -27,53 +27,94 @@ def get_next_tip() -> str:
def get_all_tips() -> list[str]:
"""Return the complete list of tips."""
tips = [
('If you are short on controllers, install the \'${REMOTE_APP_NAME}\' '
'app\non your mobile devices to use them as controllers.'),
('Create player profiles for yourself and your friends with\nyour '
'preferred names and appearances instead of using random ones.'),
('You can \'aim\' your punches by spinning left or right.\nThis is '
'useful for knocking bad guys off edges or scoring in hockey.'),
('If you pick up a curse, your only hope for survival is to\nfind a '
'health powerup in the next few seconds.'),
('A perfectly timed running-jumping-spin-punch can kill in a single '
'hit\nand earn you lifelong respect from your friends.'),
(
'If you are short on controllers,'
' install the \'${REMOTE_APP_NAME}\' app\n'
'on your mobile devices to use them as controllers.'
),
(
'Create player profiles for yourself and your friends with\nyour '
'preferred names and appearances instead of using random ones.'
),
(
'You can \'aim\' your punches by spinning left or right.\nThis is '
'useful for knocking bad guys off edges or scoring in hockey.'
),
(
'If you pick up a curse, your only hope for survival is to\nfind a '
'health powerup in the next few seconds.'
),
(
'A perfectly timed running-jumping-spin-punch can kill in a single '
'hit\nand earn you lifelong respect from your friends.'
),
'Always remember to floss.',
'Don\'t run all the time. Really. You will fall off cliffs.',
('In Capture-the-Flag, your own flag must be at your base to score, '
'If the other\nteam is about to score, stealing their flag can be '
'a good way to stop them.'),
('If you get a sticky-bomb stuck to you, jump around and spin in '
'circles. You might\nshake the bomb off, or if nothing else your '
'last moments will be entertaining.'),
('You take damage when you whack your head on things,\nso try to not '
'whack your head on things.'),
(
'In Capture-the-Flag, your own flag must be at your base to score, '
'If the other\nteam is about to score, stealing their flag can be '
'a good way to stop them.'
),
(
'If you get a sticky-bomb stuck to you, jump around and spin in '
'circles. You might\nshake the bomb off, or if nothing else your '
'last moments will be entertaining.'
),
(
'You take damage when you whack your head on things,\n'
'so try to not whack your head on things.'
),
'If you kill an enemy in one hit you get double points for it.',
('Despite their looks, all characters\' abilities are identical,\nso '
'just pick whichever one you most closely resemble.'),
(
'Despite their looks, all characters\' abilities are identical,\n'
'so just pick whichever one you most closely resemble.'
),
'You can throw bombs higher if you jump just before throwing.',
('Throw strength is based on the direction you are holding.\nTo toss '
'something gently in front of you, don\'t hold any direction.'),
('If someone picks you up, punch them and they\'ll let go.\nThis '
'works in real life too.'),
('Don\'t get too cocky with that energy shield; you can still get '
'yourself thrown off a cliff.'),
('Many things can be picked up and thrown, including other players. '
'Tossing\nyour enemies off cliffs can be an effective and '
'emotionally fulfilling strategy.'),
('Ice bombs are not very powerful, but they freeze\nwhoever they '
'hit, leaving them vulnerable to shattering.'),
(
'Throw strength is based on the direction you are holding.\n'
'To toss something gently in front of you, don\'t'
' hold any direction.'
),
(
'If someone picks you up, punch them and they\'ll let go.\nThis '
'works in real life too.'
),
(
'Don\'t get too cocky with that energy shield; you can still get '
'yourself thrown off a cliff.'
),
(
'Many things can be picked up and thrown,'
' including other players. '
'Tossing\nyour enemies off cliffs can be an effective and '
'emotionally fulfilling strategy.'
),
(
'Ice bombs are not very powerful, but they freeze\nwhoever they '
'hit, leaving them vulnerable to shattering.'
),
'Don\'t spin for too long; you\'ll become dizzy and fall.',
('Run back and forth before throwing a bomb\nto \'whiplash\' it '
'and throw it farther.'),
('Punches do more damage the faster your fists are moving,\nso '
'try running, jumping, and spinning like crazy.'),
(
'Run back and forth before throwing a bomb\nto \'whiplash\' it '
'and throw it farther.'
),
(
'Punches do more damage the faster your fists are moving,\nso '
'try running, jumping, and spinning like crazy.'
),
'In hockey, you\'ll maintain more speed if you turn gradually.',
('The head is the most vulnerable area, so a sticky-bomb\nto the '
'noggin usually means game-over.'),
('Hold down any button to run. You\'ll get places faster\nbut '
'won\'t turn very well, so watch out for cliffs.'),
('You can judge when a bomb is going to explode based on the\n'
'color of sparks from its fuse: yellow..orange..red..BOOM.'),
(
'The head is the most vulnerable area, so a sticky-bomb\nto the '
'noggin usually means game-over.'
),
(
'Hold down any button to run. You\'ll get places faster\nbut '
'won\'t turn very well, so watch out for cliffs.'
),
(
'You can judge when a bomb is going to explode based on the\n'
'color of sparks from its fuse: yellow..orange..red..BOOM.'
),
]
app = _ba.app
if not app.iircade_mode:
@ -81,12 +122,17 @@ def get_all_tips() -> list[str]:
'If your framerate is choppy, try turning down resolution\nor '
'visuals in the game\'s graphics settings.'
]
if (app.platform in ('android', 'ios') and not app.on_tv
and not app.iircade_mode):
if (
app.platform in ('android', 'ios')
and not app.on_tv
and not app.iircade_mode
):
tips += [
('If your device gets too warm or you\'d like to conserve '
'battery power,\nturn down "Visuals" or "Resolution" '
'in Settings->Graphics'),
(
'If your device gets too warm or you\'d like to conserve '
'battery power,\nturn down "Visuals" or "Resolution" '
'in Settings->Graphics'
),
]
if app.platform in ['mac', 'android'] and not app.iircade_mode:
tips += [

View File

@ -17,6 +17,7 @@ def get_tournament_prize_strings(entry: dict[str, Any]) -> list[str]:
# pylint: disable=too-many-locals
from ba._generated.enums import SpecialChar
from ba._gameutils import get_trophy_string
range1 = entry.get('prizeRange1')
range2 = entry.get('prizeRange2')
range3 = entry.get('prizeRange3')
@ -27,12 +28,18 @@ def get_tournament_prize_strings(entry: dict[str, Any]) -> list[str]:
trophy_type_2 = entry.get('prizeTrophy2')
trophy_type_3 = entry.get('prizeTrophy3')
out_vals = []
for rng, prize, trophy_type in ((range1, prize1, trophy_type_1),
(range2, prize2, trophy_type_2),
(range3, prize3, trophy_type_3)):
prval = ('' if rng is None else ('#' + str(rng[0])) if
(rng[0] == rng[1]) else
('#' + str(rng[0]) + '-' + str(rng[1])))
for rng, prize, trophy_type in (
(range1, prize1, trophy_type_1),
(range2, prize2, trophy_type_2),
(range3, prize3, trophy_type_3),
):
prval = (
''
if rng is None
else ('#' + str(rng[0]))
if (rng[0] == rng[1])
else ('#' + str(rng[0]) + '-' + str(rng[1]))
)
pvval = ''
if trophy_type is not None:
pvval += get_trophy_string(trophy_type)
@ -40,8 +47,7 @@ def get_tournament_prize_strings(entry: dict[str, Any]) -> list[str]:
# If we've got trophies but not for this entry, throw some space
# in to compensate so the ticket counts line up.
if prize is not None:
pvval = _ba.charstr(
SpecialChar.TICKET_BACKING) + str(prize) + pvval
pvval = _ba.charstr(SpecialChar.TICKET_BACKING) + str(prize) + pvval
out_vals.append(prval)
out_vals.append(pvval)
return out_vals

View File

@ -89,19 +89,21 @@ class UISubsystem:
if bool(False): # force-test ui scale
self._uiscale = UIScale.SMALL
with _ba.Context('ui'):
_ba.pushcall(lambda: _ba.screenmessage(
f'FORCING UISCALE {self._uiscale.name} FOR TESTING',
color=(1, 0, 1),
log=True))
_ba.pushcall(
lambda: _ba.screenmessage(
f'FORCING UISCALE {self._uiscale.name} FOR TESTING',
color=(1, 0, 1),
log=True,
)
)
self.controller = UIController()
# Kick off our periodic UI upkeep.
# FIXME: Can probably kill this if we do immediate UI death checks.
self.upkeeptimer = _ba.Timer(2.6543,
ui_upkeep,
timetype=TimeType.REAL,
repeat=True)
self.upkeeptimer = _ba.Timer(
2.6543, ui_upkeep, timetype=TimeType.REAL, repeat=True
)
def set_main_menu_window(self, window: ba.Widget) -> None:
"""Set the current 'main' window, replacing any existing."""
@ -122,6 +124,7 @@ class UISubsystem:
frameline = f'{frameinfo.filename} {frameinfo.lineno}'
except Exception:
from ba._error import print_exception
print_exception('Error calcing line for set_main_menu_window')
# With our legacy main-menu system, the caller is responsible for
@ -136,9 +139,12 @@ class UISubsystem:
# things.
def _delay_kill() -> None:
import time
if existing:
print(f'Killing old main_menu_window'
f' when called at: {frameline} t={time.time():.3f}')
print(
f'Killing old main_menu_window'
f' when called at: {frameline} t={time.time():.3f}'
)
existing.delete()
_ba.timer(1.0, _delay_kill, timetype=TimeType.REAL)
@ -148,8 +154,9 @@ class UISubsystem:
"""Clear any existing 'main' window with the provided transition."""
if self._main_menu_window:
if transition is not None:
_ba.containerwidget(edit=self._main_menu_window,
transition=transition)
_ba.containerwidget(
edit=self._main_menu_window, transition=transition
)
else:
self._main_menu_window.delete()

View File

@ -48,7 +48,8 @@ class WorkspaceSubsystem:
target=lambda: self._set_active_workspace_bg(
workspaceid=workspaceid,
workspacename=workspacename,
on_completed=on_completed),
on_completed=on_completed,
),
daemon=True,
).start()
@ -60,16 +61,21 @@ class WorkspaceSubsystem:
_ba.screenmessage(msg, color=(0, 1, 0))
_ba.playsound(_ba.getsound('gunCocking'))
def _set_active_workspace_bg(self, workspaceid: str, workspacename: str,
on_completed: Callable[[], None]) -> None:
def _set_active_workspace_bg(
self,
workspaceid: str,
workspacename: str,
on_completed: Callable[[], None],
) -> None:
from ba._language import Lstr
class _SkipSyncError(RuntimeError):
pass
set_path = True
wspath = Path(_ba.get_volatile_data_directory(), 'workspaces',
workspaceid)
wspath = Path(
_ba.get_volatile_data_directory(), 'workspaces', workspaceid
)
try:
# If it seems we're offline, don't even attempt a sync,
@ -87,13 +93,17 @@ class WorkspaceSubsystem:
while True:
response = _ba.app.cloud.send_message(
bacommon.cloud.WorkspaceFetchMessage(
workspaceid=workspaceid, state=state))
workspaceid=workspaceid, state=state
)
)
state = response.state
self._handle_deletes(workspace_dir=wspath,
deletes=response.deletes)
self._handle_deletes(
workspace_dir=wspath, deletes=response.deletes
)
self._handle_downloads_inline(
workspace_dir=wspath,
downloads_inline=response.downloads_inline)
downloads_inline=response.downloads_inline,
)
if response.done:
# Server only deals in files; let's clean up any
# leftover empty dirs after the dust has cleared.
@ -104,8 +114,10 @@ class WorkspaceSubsystem:
_ba.pushcall(
tpartial(
self._successmsg,
Lstr(resource='activatedText',
subs=[('${THING}', workspacename)]),
Lstr(
resource='activatedText',
subs=[('${THING}', workspacename)],
),
),
from_other_thread=True,
)
@ -114,8 +126,11 @@ class WorkspaceSubsystem:
_ba.pushcall(
tpartial(
self._errmsg,
Lstr(resource='workspaceSyncReuseText',
subs=[('${WORKSPACE}', workspacename)])),
Lstr(
resource='workspaceSyncReuseText',
subs=[('${WORKSPACE}', workspacename)],
),
),
from_other_thread=True,
)
@ -123,8 +138,10 @@ class WorkspaceSubsystem:
# Avoid reusing existing if we fail in the middle; could
# be in wonky state.
set_path = False
_ba.pushcall(tpartial(self._errmsg, Lstr(value=str(exc))),
from_other_thread=True)
_ba.pushcall(
tpartial(self._errmsg, Lstr(value=str(exc))),
from_other_thread=True,
)
except Exception:
# Ditto.
set_path = False
@ -132,8 +149,10 @@ class WorkspaceSubsystem:
_ba.pushcall(
tpartial(
self._errmsg,
Lstr(resource='workspaceSyncErrorText',
subs=[('${WORKSPACE}', workspacename)]),
Lstr(
resource='workspaceSyncErrorText',
subs=[('${WORKSPACE}', workspacename)],
),
),
from_other_thread=True,
)
@ -186,8 +205,7 @@ class WorkspaceSubsystem:
# listed when the parent dir is visited, so lets make sure
# to only acknowledge still-existing ones.
dirnames = [
d for d in dirnames
if os.path.exists(os.path.join(basename, d))
d for d in dirnames if os.path.exists(os.path.join(basename, d))
]
if not dirnames and not filenames and basename != prunedir:
os.rmdir(basename)

View File

@ -9,73 +9,173 @@ defensively) in mods.
from __future__ import annotations
from _ba import (
show_online_score_ui, set_ui_input_device, is_party_icon_visible,
getinputdevice, add_clean_frame_callback, unlock_all_input,
increment_analytics_count, set_debug_speed_exponent, get_special_widget,
get_qrcode_texture, get_string_height, get_string_width, show_app_invite,
appnameupper, lock_all_input, open_file_externally, fade_screen, appname,
have_incentivized_ad, has_video_ads, workspaces_in_use,
set_party_icon_always_visible, connect_to_party, get_game_port,
end_host_scanning, host_scan_cycle, charstr, get_public_party_enabled,
get_public_party_max_size, set_public_party_name,
set_public_party_max_size, set_authenticate_clients,
set_public_party_enabled, reset_random_player_names, new_host_session,
get_foreground_host_session, get_local_active_input_devices_count,
get_ui_input_device, is_in_replay, set_replay_speed_exponent,
get_replay_speed_exponent, disconnect_from_host, set_party_window_open,
get_connection_to_host_info, get_chat_messages, get_game_roster,
disconnect_client, chatmessage, get_random_names, have_permission,
request_permission, have_touchscreen_input, is_xcode_build,
set_low_level_config_value, get_low_level_config_value,
capture_gamepad_input, release_gamepad_input, has_gamma_control,
get_max_graphics_quality, get_display_resolution, capture_keyboard_input,
release_keyboard_input, value_test, set_touchscreen_editing,
is_running_on_fire_tv, android_get_external_files_dir,
set_telnet_access_enabled, new_replay_session, get_replays_dir)
show_online_score_ui,
set_ui_input_device,
is_party_icon_visible,
getinputdevice,
add_clean_frame_callback,
unlock_all_input,
increment_analytics_count,
set_debug_speed_exponent,
get_special_widget,
get_qrcode_texture,
get_string_height,
get_string_width,
show_app_invite,
appnameupper,
lock_all_input,
open_file_externally,
fade_screen,
appname,
have_incentivized_ad,
has_video_ads,
workspaces_in_use,
set_party_icon_always_visible,
connect_to_party,
get_game_port,
end_host_scanning,
host_scan_cycle,
charstr,
get_public_party_enabled,
get_public_party_max_size,
set_public_party_name,
set_public_party_max_size,
set_authenticate_clients,
set_public_party_enabled,
reset_random_player_names,
new_host_session,
get_foreground_host_session,
get_local_active_input_devices_count,
get_ui_input_device,
is_in_replay,
set_replay_speed_exponent,
get_replay_speed_exponent,
disconnect_from_host,
set_party_window_open,
get_connection_to_host_info,
get_chat_messages,
get_game_roster,
disconnect_client,
chatmessage,
get_random_names,
have_permission,
request_permission,
have_touchscreen_input,
is_xcode_build,
set_low_level_config_value,
get_low_level_config_value,
capture_gamepad_input,
release_gamepad_input,
has_gamma_control,
get_max_graphics_quality,
get_display_resolution,
capture_keyboard_input,
release_keyboard_input,
value_test,
set_touchscreen_editing,
is_running_on_fire_tv,
android_get_external_files_dir,
set_telnet_access_enabled,
new_replay_session,
get_replays_dir,
)
from ba._map import (get_map_class, register_map, preload_map_preview_media,
get_map_display_string, get_filtered_map_name)
from ba._map import (
get_map_class,
register_map,
preload_map_preview_media,
get_map_display_string,
get_filtered_map_name,
)
from ba._appconfig import commit_app_config
from ba._input import (get_device_value, get_input_map_hash,
get_input_device_config)
from ba._input import (
get_device_value,
get_input_map_hash,
get_input_device_config,
)
from ba._general import getclass, json_prep, get_type_name
from ba._activitytypes import JoinActivity, ScoreScreenActivity
from ba._apputils import (is_browser_likely_available, get_remote_app_name,
should_submit_debug_info)
from ba._benchmark import (run_gpu_benchmark, run_cpu_benchmark,
run_media_reload_benchmark, run_stress_test)
from ba._apputils import (
is_browser_likely_available,
get_remote_app_name,
should_submit_debug_info,
)
from ba._benchmark import (
run_gpu_benchmark,
run_cpu_benchmark,
run_media_reload_benchmark,
run_stress_test,
)
from ba._campaign import getcampaign
from ba._messages import PlayerProfilesChangedMessage
from ba._multiteamsession import DEFAULT_TEAM_COLORS, DEFAULT_TEAM_NAMES
from ba._music import do_play_music
from ba._net import (master_server_get, master_server_post,
get_ip_address_type, DEFAULT_REQUEST_TIMEOUT_SECONDS)
from ba._net import (
master_server_get,
master_server_post,
get_ip_address_type,
DEFAULT_REQUEST_TIMEOUT_SECONDS,
)
from ba._powerup import get_default_powerup_distribution
from ba._profile import (get_player_profile_colors, get_player_profile_icon,
get_player_colors)
from ba._profile import (
get_player_profile_colors,
get_player_profile_icon,
get_player_colors,
)
from ba._tips import get_next_tip
from ba._playlist import (get_default_free_for_all_playlist,
get_default_teams_playlist, filter_playlist)
from ba._store import (get_available_sale_time, get_available_purchase_count,
get_store_item_name_translated,
get_store_item_display_size, get_store_layout,
get_store_item, get_clean_price, get_unowned_maps,
get_unowned_game_types)
from ba._playlist import (
get_default_free_for_all_playlist,
get_default_teams_playlist,
filter_playlist,
)
from ba._store import (
get_available_sale_time,
get_available_purchase_count,
get_store_item_name_translated,
get_store_item_display_size,
get_store_layout,
get_store_item,
get_clean_price,
get_unowned_maps,
get_unowned_game_types,
)
from ba._tournament import get_tournament_prize_strings
from ba._gameutils import get_trophy_string
from ba._internal import (
get_v2_fleet, get_master_server_address, is_blessed, get_news_show,
game_service_has_leaderboard, report_achievement, submit_score,
tournament_query, power_ranking_query, restore_purchases, purchase,
get_purchases_state, get_purchased, get_price, in_game_purchase,
add_transaction, reset_achievements, get_public_login_id,
have_outstanding_transactions, run_transactions,
get_v1_account_misc_read_val, get_v1_account_misc_read_val_2,
get_v1_account_misc_val, get_v1_account_ticket_count,
get_v1_account_state_num, get_v1_account_state,
get_v1_account_display_string, get_v1_account_type, get_v1_account_name,
sign_out_v1, sign_in_v1, mark_config_dirty)
get_v2_fleet,
get_master_server_address,
is_blessed,
get_news_show,
game_service_has_leaderboard,
report_achievement,
submit_score,
tournament_query,
power_ranking_query,
restore_purchases,
purchase,
get_purchases_state,
get_purchased,
get_price,
in_game_purchase,
add_transaction,
reset_achievements,
get_public_login_id,
have_outstanding_transactions,
run_transactions,
get_v1_account_misc_read_val,
get_v1_account_misc_read_val_2,
get_v1_account_misc_val,
get_v1_account_ticket_count,
get_v1_account_state_num,
get_v1_account_state,
get_v1_account_display_string,
get_v1_account_type,
get_v1_account_name,
sign_out_v1,
sign_in_v1,
mark_config_dirty,
)
__all__ = [
'show_online_score_ui',

View File

@ -24,12 +24,18 @@ class MacMusicAppMusicPlayer(MusicPlayer):
self._thread = _MacMusicAppThread()
self._thread.start()
def on_select_entry(self, callback: Callable[[Any], None],
current_entry: Any, selection_target_name: str) -> Any:
def on_select_entry(
self,
callback: Callable[[Any], None],
current_entry: Any,
selection_target_name: str,
) -> Any:
# pylint: disable=cyclic-import
from bastd.ui.soundtrack import entrytypeselect as etsel
return etsel.SoundtrackEntryTypeSelectWindow(callback, current_entry,
selection_target_name)
return etsel.SoundtrackEntryTypeSelectWindow(
callback, current_entry, selection_target_name
)
def on_set_volume(self, volume: float) -> None:
self._thread.set_volume(volume)
@ -44,8 +50,10 @@ class MacMusicAppMusicPlayer(MusicPlayer):
if entry_type == 'iTunesPlaylist':
self._thread.play_playlist(music.get_soundtrack_entry_name(entry))
else:
print('MacMusicAppMusicPlayer passed unrecognized entry type:',
entry_type)
print(
'MacMusicAppMusicPlayer passed unrecognized entry type:',
entry_type,
)
def on_stop(self) -> None:
self._thread.play_playlist(None)
@ -70,6 +78,7 @@ class _MacMusicAppThread(threading.Thread):
from ba._general import Call
from ba._language import Lstr
from ba._generated.enums import TimeType
_ba.set_thread_name('BA_MacMusicAppThread')
_ba.mac_music_app_init()
@ -77,10 +86,15 @@ class _MacMusicAppThread(threading.Thread):
# it causes any funny business (this used to background the app
# sometimes, though I think that is fixed now)
def do_print() -> None:
_ba.timer(1.0,
Call(_ba.screenmessage, Lstr(resource='usingItunesText'),
(0, 1, 0)),
timetype=TimeType.REAL)
_ba.timer(
1.0,
Call(
_ba.screenmessage,
Lstr(resource='usingItunesText'),
(0, 1, 0),
),
timetype=TimeType.REAL,
)
_ba.pushcall(do_print, from_other_thread=True)
@ -153,15 +167,29 @@ class _MacMusicAppThread(threading.Thread):
self._commands_available.set()
def _handle_get_playlists_command(
self, target: Callable[[list[str]], None]) -> None:
self, target: Callable[[list[str]], None]
) -> None:
from ba._general import Call
try:
playlists = _ba.mac_music_app_get_playlists()
playlists = [
p for p in playlists if p not in [
'Music', 'Movies', 'TV Shows', 'Podcasts', 'iTunes\xa0U',
'Books', 'Genius', 'iTunes DJ', 'Music Videos',
'Home Videos', 'Voice Memos', 'Audiobooks'
p
for p in playlists
if p
not in [
'Music',
'Movies',
'TV Shows',
'Podcasts',
'iTunes\xa0U',
'Books',
'Genius',
'iTunes DJ',
'Music Videos',
'Home Videos',
'Voice Memos',
'Audiobooks',
]
]
playlists.sort(key=lambda x: x.lower())
@ -194,7 +222,7 @@ class _MacMusicAppThread(threading.Thread):
# Set our playlist and play it if our volume is up.
self._current_playlist = target
if self._volume > 0:
self._orig_volume = (_ba.mac_music_app_get_volume())
self._orig_volume = _ba.mac_music_app_get_volume()
self._update_mac_music_app_volume()
self._play_current_playlist()
@ -213,20 +241,30 @@ class _MacMusicAppThread(threading.Thread):
def _play_current_playlist(self) -> None:
try:
from ba._general import Call
assert self._current_playlist is not None
if _ba.mac_music_app_play_playlist(self._current_playlist):
pass
else:
_ba.pushcall(Call(
_ba.screenmessage,
_ba.app.lang.get_resource('playlistNotFoundText') +
': \'' + self._current_playlist + '\'', (1, 0, 0)),
from_other_thread=True)
_ba.pushcall(
Call(
_ba.screenmessage,
_ba.app.lang.get_resource('playlistNotFoundText')
+ ': \''
+ self._current_playlist
+ '\'',
(1, 0, 0),
),
from_other_thread=True,
)
except Exception:
from ba import _error
_error.print_exception(
f'error playing playlist {self._current_playlist}')
f'error playing playlist {self._current_playlist}'
)
def _update_mac_music_app_volume(self) -> None:
_ba.mac_music_app_set_volume(
max(0, min(100, int(100.0 * self._volume))))
max(0, min(100, int(100.0 * self._volume)))
)

View File

@ -47,10 +47,12 @@ def _request_storage_permission() -> bool:
"""If needed, requests storage permission from the user (& return true)."""
from ba._language import Lstr
from ba._generated.enums import Permission
if not _ba.have_permission(Permission.STORAGE):
_ba.playsound(_ba.getsound('error'))
_ba.screenmessage(Lstr(resource='storagePermissionAccessText'),
color=(1, 0, 0))
_ba.screenmessage(
Lstr(resource='storagePermissionAccessText'), color=(1, 0, 0)
)
_ba.timer(1.0, lambda: _ba.request_permission(Permission.STORAGE))
return True
return False
@ -80,12 +82,15 @@ def show_user_scripts() -> None:
if usd is not None and os.path.isdir(usd):
file_name = usd + '/about_this_folder.txt'
with open(file_name, 'w', encoding='utf-8') as outfile:
outfile.write('You can drop files in here to mod the game.'
' See settings/advanced'
' in the game for more info.')
outfile.write(
'You can drop files in here to mod the game.'
' See settings/advanced'
' in the game for more info.'
)
_ba.android_media_scan_file(file_name)
except Exception:
from ba import _error
_error.print_exception('error writing about_this_folder stuff')
# On a few platforms we try to open the dir in the UI.
@ -103,13 +108,14 @@ def create_user_system_scripts() -> None:
(for editing and experiment with)
"""
import shutil
app = _ba.app
# First off, if we need permission for this, ask for it.
if _request_storage_permission():
return
path = (app.python_directory_user + '/sys/' + app.version)
path = app.python_directory_user + '/sys/' + app.version
pathtmp = path + '_tmp'
if os.path.exists(path):
shutil.rmtree(path)
@ -123,31 +129,38 @@ def create_user_system_scripts() -> None:
# to blow them away anyway to make changes;
# See https://github.com/efroemling/ballistica/wiki
# /Knowledge-Nuggets#python-cache-files-gotcha
return ('__pycache__', )
return ('__pycache__',)
print(f'COPYING "{app.python_directory_app}" -> "{pathtmp}".')
shutil.copytree(app.python_directory_app, pathtmp, ignore=_ignore_filter)
print(f'MOVING "{pathtmp}" -> "{path}".')
shutil.move(pathtmp, path)
print(f"Created system scripts at :'{path}"
f"'\nRestart {_ba.appname()} to use them."
f' (use ba.quit() to exit the game)')
print(
f"Created system scripts at :'{path}"
f"'\nRestart {_ba.appname()} to use them."
f' (use ba.quit() to exit the game)'
)
if app.platform == 'android':
print('Note: the new files may not be visible via '
'android-file-transfer until you restart your device.')
print(
'Note: the new files may not be visible via '
'android-file-transfer until you restart your device.'
)
def delete_user_system_scripts() -> None:
"""Clean out the scripts created by create_user_system_scripts()."""
import shutil
app = _ba.app
path = (app.python_directory_user + '/sys/' + app.version)
path = app.python_directory_user + '/sys/' + app.version
if os.path.exists(path):
shutil.rmtree(path)
print(f'User system scripts deleted.\n'
f'Restart {_ba.appname()} to use internal'
f' scripts. (use ba.quit() to exit the game)')
print(
f'User system scripts deleted.\n'
f'Restart {_ba.appname()} to use internal'
f' scripts. (use ba.quit() to exit the game)'
)
else:
print('User system scripts not found.')

View File

@ -31,13 +31,20 @@ class OSMusicPlayer(MusicPlayer):
# FIXME: should ask the C++ layer for these; just hard-coding for now.
return ['mp3', 'ogg', 'm4a', 'wav', 'flac', 'mid']
def on_select_entry(self, callback: Callable[[Any], None],
current_entry: Any, selection_target_name: str) -> Any:
def on_select_entry(
self,
callback: Callable[[Any], None],
current_entry: Any,
selection_target_name: str,
) -> Any:
# pylint: disable=cyclic-import
from bastd.ui.soundtrack.entrytypeselect import (
SoundtrackEntryTypeSelectWindow)
return SoundtrackEntryTypeSelectWindow(callback, current_entry,
selection_target_name)
SoundtrackEntryTypeSelectWindow,
)
return SoundtrackEntryTypeSelectWindow(
callback, current_entry, selection_target_name
)
def on_set_volume(self, volume: float) -> None:
_ba.music_player_set_volume(volume)
@ -56,22 +63,31 @@ class OSMusicPlayer(MusicPlayer):
# valid file within it.
self._want_to_play = True
self._actually_playing = False
_PickFolderSongThread(name, self.get_valid_music_file_extensions(),
self._on_play_folder_cb).start()
_PickFolderSongThread(
name,
self.get_valid_music_file_extensions(),
self._on_play_folder_cb,
).start()
def _on_play_folder_cb(self,
result: str | list[str],
error: str | None = None) -> None:
def _on_play_folder_cb(
self, result: str | list[str], error: str | None = None
) -> None:
from ba import _language
if error is not None:
rstr = (_language.Lstr(
resource='internal.errorPlayingMusicText').evaluate())
rstr = _language.Lstr(
resource='internal.errorPlayingMusicText'
).evaluate()
if isinstance(result, str):
err_str = (rstr.replace('${MUSIC}', os.path.basename(result)) +
'; ' + str(error))
err_str = (
rstr.replace('${MUSIC}', os.path.basename(result))
+ '; '
+ str(error)
)
else:
err_str = (rstr.replace('${MUSIC}', '<multiple>') + '; ' +
str(error))
err_str = (
rstr.replace('${MUSIC}', '<multiple>') + '; ' + str(error)
)
_ba.screenmessage(err_str, color=(1, 0, 0))
return
@ -93,9 +109,12 @@ class OSMusicPlayer(MusicPlayer):
class _PickFolderSongThread(threading.Thread):
def __init__(self, path: str, valid_extensions: list[str],
callback: Callable[[str | list[str], str | None], None]):
def __init__(
self,
path: str,
valid_extensions: list[str],
callback: Callable[[str | list[str], str | None], None],
):
super().__init__()
self._valid_extensions = valid_extensions
self._callback = callback
@ -104,6 +123,7 @@ class _PickFolderSongThread(threading.Thread):
def run(self) -> None:
from ba import _language
from ba._general import Call
do_print_error = True
try:
_ba.set_thread_name('BA_PickFolderSongThread')
@ -111,24 +131,33 @@ class _PickFolderSongThread(threading.Thread):
valid_extensions = ['.' + x for x in self._valid_extensions]
for root, _subdirs, filenames in os.walk(self._path):
for fname in filenames:
if any(fname.lower().endswith(ext)
for ext in valid_extensions):
all_files.insert(random.randrange(len(all_files) + 1),
root + '/' + fname)
if any(
fname.lower().endswith(ext) for ext in valid_extensions
):
all_files.insert(
random.randrange(len(all_files) + 1),
root + '/' + fname,
)
if not all_files:
do_print_error = False
raise RuntimeError(
_language.Lstr(resource='internal.noMusicFilesInFolderText'
).evaluate())
_ba.pushcall(Call(self._callback, all_files, None),
from_other_thread=True)
_language.Lstr(
resource='internal.noMusicFilesInFolderText'
).evaluate()
)
_ba.pushcall(
Call(self._callback, all_files, None), from_other_thread=True
)
except Exception as exc:
from ba import _error
if do_print_error:
_error.print_exception()
try:
err_str = str(exc)
except Exception:
err_str = '<ENCERR4523>'
_ba.pushcall(Call(self._callback, self._path, err_str),
from_other_thread=True)
_ba.pushcall(
Call(self._callback, self._path, err_str),
from_other_thread=True,
)

View File

@ -44,6 +44,7 @@ class Window:
@dataclass
class UICleanupCheck:
"""Holds info about a uicleanupcheck target."""
obj: weakref.ref
widget: ba.Widget
widget_death_time: float | None
@ -112,6 +113,7 @@ class UIEntry:
# TEMP HARD CODED - WILL REPLACE THIS WITH BA_META LOOKUPS.
if self._name == 'mainmenu':
from bastd.ui import mainmenu
return cast(Type[UILocation], mainmenu.MainMenuWindow)
raise ValueError('unknown ui class ' + str(self._name))
@ -139,8 +141,9 @@ class UIController:
"""Show the main menu, clearing other UIs from location stacks."""
self._main_stack = []
self._dialog_stack = []
self._main_stack = (self._main_stack_game
if in_game else self._main_stack_menu)
self._main_stack = (
self._main_stack_game if in_game else self._main_stack_menu
)
self._main_stack.append(UIEntry('mainmenu', self))
self._update_ui()
@ -154,8 +157,13 @@ class UIController:
entry.destroy()
# Now create the topmost one if there is one.
entrynew = (self._dialog_stack[-1] if self._dialog_stack else
self._main_stack[-1] if self._main_stack else None)
entrynew = (
self._dialog_stack[-1]
if self._dialog_stack
else self._main_stack[-1]
if self._main_stack
else None
)
if entrynew is not None:
entrynew.create()
@ -190,9 +198,10 @@ def uicleanupcheck(obj: Any, widget: ba.Widget) -> None:
widget.add_delete_callback(foobar)
_ba.app.ui.cleanupchecks.append(
UICleanupCheck(obj=weakref.ref(obj),
widget=widget,
widget_death_time=None))
UICleanupCheck(
obj=weakref.ref(obj), widget=widget, widget_death_time=None
)
)
def ui_upkeep() -> None:
@ -218,9 +227,11 @@ def ui_upkeep() -> None:
# Widget was already dead; complain if its been too long.
if now - check.widget_death_time > 5.0:
print(
'WARNING:', obj,
'WARNING:',
obj,
'is still alive 5 second after its widget died;'
' you might have a memory leak.')
' you might have a memory leak.',
)
print_active_refs(obj)
else:

View File

@ -27,19 +27,23 @@ class CoopJoinActivity(JoinActivity):
def on_transition_in(self) -> None:
from bastd.actor.controlsguide import ControlsGuide
from bastd.actor.text import Text
super().on_transition_in()
assert isinstance(self.session, ba.CoopSession)
assert self.session.campaign
Text(self.session.campaign.getlevel(
self.session.campaign_level_name).displayname,
scale=1.3,
h_attach=Text.HAttach.CENTER,
h_align=Text.HAlign.CENTER,
v_attach=Text.VAttach.TOP,
transition=Text.Transition.FADE_IN,
transition_delay=4.0,
color=(1, 1, 1, 0.6),
position=(0, -95)).autoretain()
Text(
self.session.campaign.getlevel(
self.session.campaign_level_name
).displayname,
scale=1.3,
h_attach=Text.HAttach.CENTER,
h_align=Text.HAlign.CENTER,
v_attach=Text.VAttach.TOP,
transition=Text.Transition.FADE_IN,
transition_delay=4.0,
color=(1, 1, 1, 0.6),
position=(0, -95),
).autoretain()
ControlsGuide(delay=1.0).autoretain()
ba.pushcall(self._show_remaining_achievements)
@ -60,30 +64,34 @@ class CoopJoinActivity(JoinActivity):
# Now list our remaining achievements for this level.
assert self.session.campaign is not None
assert isinstance(self.session, ba.CoopSession)
levelname = (self.session.campaign.name + ':' +
self.session.campaign_level_name)
levelname = (
self.session.campaign.name + ':' + self.session.campaign_level_name
)
ts_h_offs = 60
if not (ba.app.demo_mode or ba.app.arcade_mode):
achievements = [
a for a in ba.app.ach.achievements_for_coop_level(levelname)
a
for a in ba.app.ach.achievements_for_coop_level(levelname)
if not a.complete
]
have_achievements = bool(achievements)
achievements = [a for a in achievements if not a.complete]
vrmode = ba.app.vr_mode
if have_achievements:
Text(ba.Lstr(resource='achievementsRemainingText'),
host_only=True,
position=(ts_h_offs - 10, vpos),
transition=Text.Transition.FADE_IN,
scale=1.1 * 0.76,
h_attach=Text.HAttach.LEFT,
v_attach=Text.VAttach.TOP,
color=(1, 1, 1.2, 1) if vrmode else (0.8, 0.8, 1, 1),
shadow=1.0,
flatness=1.0 if vrmode else 0.6,
transition_delay=delay).autoretain()
Text(
ba.Lstr(resource='achievementsRemainingText'),
host_only=True,
position=(ts_h_offs - 10, vpos),
transition=Text.Transition.FADE_IN,
scale=1.1 * 0.76,
h_attach=Text.HAttach.LEFT,
v_attach=Text.VAttach.TOP,
color=(1, 1, 1.2, 1) if vrmode else (0.8, 0.8, 1, 1),
shadow=1.0,
flatness=1.0 if vrmode else 0.6,
transition_delay=delay,
).autoretain()
hval = ts_h_offs + 50
vpos -= 35
for ach in achievements:
@ -91,12 +99,14 @@ class CoopJoinActivity(JoinActivity):
ach.create_display(hval, vpos, delay, style='in_game')
vpos -= 55
if not achievements:
Text(ba.Lstr(resource='noAchievementsRemainingText'),
host_only=True,
position=(ts_h_offs + 15, vpos + 10),
transition=Text.Transition.FADE_IN,
scale=0.7,
h_attach=Text.HAttach.LEFT,
v_attach=Text.VAttach.TOP,
color=(1, 1, 1, 0.5),
transition_delay=delay + 0.5).autoretain()
Text(
ba.Lstr(resource='noAchievementsRemainingText'),
host_only=True,
position=(ts_h_offs + 15, vpos + 10),
transition=Text.Transition.FADE_IN,
scale=0.7,
h_attach=Text.HAttach.LEFT,
v_attach=Text.VAttach.TOP,
color=(1, 1, 1, 0.5),
transition_delay=delay + 0.5,
).autoretain()

File diff suppressed because it is too large Load Diff

View File

@ -22,13 +22,15 @@ class DrawScoreScreenActivity(MultiTeamScoreScreenActivity):
def on_begin(self) -> None:
ba.set_analytics_screen('Draw Score Screen')
super().on_begin()
ZoomText(ba.Lstr(resource='drawText'),
position=(0, 0),
maxwidth=400,
shiftposition=(-220, 0),
shiftdelay=2.0,
flash=False,
trail=False,
jitter=1.0).autoretain()
ZoomText(
ba.Lstr(resource='drawText'),
position=(0, 0),
maxwidth=400,
shiftposition=(-220, 0),
shiftdelay=2.0,
flash=False,
trail=False,
jitter=1.0,
).autoretain()
ba.timer(0.35, ba.Call(ba.playsound, self._score_display_sound))
self.show_player_scores(results=self.settings_raw.get('results', None))

View File

@ -37,91 +37,132 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
session = self.session
assert isinstance(session, ba.MultiTeamSession)
if ba.app.lang.get_resource('bestOfUseFirstToInstead'):
best_txt = ba.Lstr(resource='firstToSeriesText',
subs=[('${COUNT}',
str(session.get_series_length() / 2 + 1))
])
best_txt = ba.Lstr(
resource='firstToSeriesText',
subs=[('${COUNT}', str(session.get_series_length() / 2 + 1))],
)
else:
best_txt = ba.Lstr(resource='bestOfSeriesText',
subs=[('${COUNT}',
str(session.get_series_length()))])
best_txt = ba.Lstr(
resource='bestOfSeriesText',
subs=[('${COUNT}', str(session.get_series_length()))],
)
ZoomText(best_txt,
position=(0, 175),
shiftposition=(-250, 175),
shiftdelay=2.5,
flash=False,
trail=False,
h_align='center',
scale=0.25,
color=(0.5, 0.5, 0.5, 1.0),
jitter=3.0).autoretain()
ZoomText(
best_txt,
position=(0, 175),
shiftposition=(-250, 175),
shiftdelay=2.5,
flash=False,
trail=False,
h_align='center',
scale=0.25,
color=(0.5, 0.5, 0.5, 1.0),
jitter=3.0,
).autoretain()
for team in self.session.sessionteams:
ba.timer(
i * 0.15 + 0.15,
ba.WeakCall(self._show_team_name, vval - i * height, team,
i * 0.2, shift_time - (i * 0.150 + 0.150)))
ba.timer(i * 0.150 + 0.5,
ba.Call(ba.playsound, self._score_display_sound_small))
scored = (team is self._winner)
ba.WeakCall(
self._show_team_name,
vval - i * height,
team,
i * 0.2,
shift_time - (i * 0.150 + 0.150),
),
)
ba.timer(
i * 0.150 + 0.5,
ba.Call(ba.playsound, self._score_display_sound_small),
)
scored = team is self._winner
delay = 0.2
if scored:
delay = 1.2
ba.timer(
i * 0.150 + 0.2,
ba.WeakCall(self._show_team_old_score, vval - i * height,
team, shift_time - (i * 0.15 + 0.2)))
ba.timer(i * 0.15 + 1.5,
ba.Call(ba.playsound, self._score_display_sound))
ba.WeakCall(
self._show_team_old_score,
vval - i * height,
team,
shift_time - (i * 0.15 + 0.2),
),
)
ba.timer(
i * 0.15 + 1.5,
ba.Call(ba.playsound, self._score_display_sound),
)
ba.timer(
i * 0.150 + delay,
ba.WeakCall(self._show_team_score, vval - i * height, team,
scored, i * 0.2 + 0.1,
shift_time - (i * 0.15 + delay)))
ba.WeakCall(
self._show_team_score,
vval - i * height,
team,
scored,
i * 0.2 + 0.1,
shift_time - (i * 0.15 + delay),
),
)
i += 1
self.show_player_scores()
def _show_team_name(self, pos_v: float, team: ba.SessionTeam,
kill_delay: float, shiftdelay: float) -> None:
def _show_team_name(
self,
pos_v: float,
team: ba.SessionTeam,
kill_delay: float,
shiftdelay: float,
) -> None:
del kill_delay # Unused arg.
ZoomText(ba.Lstr(value='${A}:', subs=[('${A}', team.name)]),
position=(100, pos_v),
shiftposition=(-150, pos_v),
shiftdelay=shiftdelay,
flash=False,
trail=False,
h_align='right',
maxwidth=300,
color=team.color,
jitter=1.0).autoretain()
ZoomText(
ba.Lstr(value='${A}:', subs=[('${A}', team.name)]),
position=(100, pos_v),
shiftposition=(-150, pos_v),
shiftdelay=shiftdelay,
flash=False,
trail=False,
h_align='right',
maxwidth=300,
color=team.color,
jitter=1.0,
).autoretain()
def _show_team_old_score(self, pos_v: float, sessionteam: ba.SessionTeam,
shiftdelay: float) -> None:
ZoomText(str(sessionteam.customdata['score'] - 1),
position=(150, pos_v),
maxwidth=100,
color=(0.6, 0.6, 0.7),
shiftposition=(-100, pos_v),
shiftdelay=shiftdelay,
flash=False,
trail=False,
lifespan=1.0,
h_align='left',
jitter=1.0).autoretain()
def _show_team_old_score(
self, pos_v: float, sessionteam: ba.SessionTeam, shiftdelay: float
) -> None:
ZoomText(
str(sessionteam.customdata['score'] - 1),
position=(150, pos_v),
maxwidth=100,
color=(0.6, 0.6, 0.7),
shiftposition=(-100, pos_v),
shiftdelay=shiftdelay,
flash=False,
trail=False,
lifespan=1.0,
h_align='left',
jitter=1.0,
).autoretain()
def _show_team_score(self, pos_v: float, sessionteam: ba.SessionTeam,
scored: bool, kill_delay: float,
shiftdelay: float) -> None:
def _show_team_score(
self,
pos_v: float,
sessionteam: ba.SessionTeam,
scored: bool,
kill_delay: float,
shiftdelay: float,
) -> None:
del kill_delay # Unused arg.
ZoomText(str(sessionteam.customdata['score']),
position=(150, pos_v),
maxwidth=100,
color=(1.0, 0.9, 0.5) if scored else (0.6, 0.6, 0.7),
shiftposition=(-100, pos_v),
shiftdelay=shiftdelay,
flash=scored,
trail=scored,
h_align='left',
jitter=1.0,
trailcolor=(1, 0.8, 0.0, 0)).autoretain()
ZoomText(
str(sessionteam.customdata['score']),
position=(150, pos_v),
maxwidth=100,
color=(1.0, 0.9, 0.5) if scored else (0.6, 0.6, 0.7),
shiftposition=(-100, pos_v),
shiftdelay=shiftdelay,
flash=scored,
trail=scored,
h_align='left',
jitter=1.0,
trailcolor=(1, 0.8, 0.0, 0),
).autoretain()

View File

@ -28,6 +28,7 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
# pylint: disable=too-many-statements
from bastd.actor.text import Text
from bastd.actor.image import Image
ba.set_analytics_screen('FreeForAll Score Screen')
super().on_begin()
@ -45,14 +46,17 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
key=lambda p: (
p.team.sessionteam.customdata['previous_score'],
p.getname(full=True),
))
),
)
player_order = list(self.players)
player_order.sort(reverse=True,
key=lambda p: (
p.team.sessionteam.customdata['score'],
p.team.sessionteam.customdata['score'],
p.getname(full=True),
))
player_order.sort(
reverse=True,
key=lambda p: (
p.team.sessionteam.customdata['score'],
p.team.sessionteam.customdata['score'],
p.getname(full=True),
),
)
v_offs = -74.0 + spacing * len(player_order_prev) * 0.5
delay1 = 1.3 + 0.1
@ -66,30 +70,36 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
ba.timer(0.3, ba.Call(ba.playsound, self._score_display_sound))
results = self.settings_raw['results']
assert isinstance(results, ba.GameResults)
self.show_player_scores(delay=0.001,
results=results,
scale=1.2,
x_offset=-110.0)
self.show_player_scores(
delay=0.001, results=results, scale=1.2, x_offset=-110.0
)
sound_times: set[float] = set()
def _scoretxt(text: str,
x_offs: float,
y_offs: float,
highlight: bool,
delay: float,
extrascale: float,
flash: bool = False) -> Text:
return Text(text,
position=(ts_h_offs + x_offs * scale,
y_base + (y_offs + v_offs + 2.0) * scale),
scale=scale * extrascale,
color=((1.0, 0.7, 0.3, 1.0) if highlight else
(0.7, 0.7, 0.7, 0.7)),
h_align=Text.HAlign.RIGHT,
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay + delay,
flash=flash).autoretain()
def _scoretxt(
text: str,
x_offs: float,
y_offs: float,
highlight: bool,
delay: float,
extrascale: float,
flash: bool = False,
) -> Text:
return Text(
text,
position=(
ts_h_offs + x_offs * scale,
y_base + (y_offs + v_offs + 2.0) * scale,
),
scale=scale * extrascale,
color=(
(1.0, 0.7, 0.3, 1.0) if highlight else (0.7, 0.7, 0.7, 0.7)
),
h_align=Text.HAlign.RIGHT,
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay + delay,
flash=flash,
).autoretain()
v_offs -= spacing
slide_amt = 0.0
@ -98,16 +108,21 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
session = self.session
assert isinstance(session, ba.FreeForAllSession)
title = Text(ba.Lstr(resource='firstToSeriesText',
subs=[('${COUNT}',
str(session.get_ffa_series_length()))]),
scale=1.05 * scale,
position=(ts_h_offs - 0.0 * scale,
y_base + (v_offs + 50.0) * scale),
h_align=Text.HAlign.CENTER,
color=(0.5, 0.5, 0.5, 0.5),
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay).autoretain()
title = Text(
ba.Lstr(
resource='firstToSeriesText',
subs=[('${COUNT}', str(session.get_ffa_series_length()))],
),
scale=1.05 * scale,
position=(
ts_h_offs - 0.0 * scale,
y_base + (v_offs + 50.0) * scale,
),
h_align=Text.HAlign.CENTER,
color=(0.5, 0.5, 0.5, 0.5),
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay,
).autoretain()
v_offs -= 25
v_offs_start = v_offs
@ -115,152 +130,239 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
ba.timer(
tdelay + delay3,
ba.WeakCall(
self._safe_animate, title.position_combine, 'input0', {
self._safe_animate,
title.position_combine,
'input0',
{
0.0: ts_h_offs - 0.0 * scale,
transtime2: ts_h_offs - (0.0 + slide_amt) * scale
}))
transtime2: ts_h_offs - (0.0 + slide_amt) * scale,
},
),
)
for i, player in enumerate(player_order_prev):
v_offs_2 = v_offs_start - spacing * (player_order.index(player))
ba.timer(tdelay + 0.3,
ba.Call(ba.playsound, self._score_display_sound_small))
ba.timer(
tdelay + 0.3,
ba.Call(ba.playsound, self._score_display_sound_small),
)
if order_change:
ba.timer(tdelay + delay2 + 0.1,
ba.Call(ba.playsound, self._cymbal_sound))
img = Image(player.get_icon(),
position=(ts_h_offs - 72.0 * scale,
y_base + (v_offs + 15.0) * scale),
scale=(30.0 * scale, 30.0 * scale),
transition=Image.Transition.IN_LEFT,
transition_delay=tdelay).autoretain()
ba.timer(
tdelay + delay2 + 0.1,
ba.Call(ba.playsound, self._cymbal_sound),
)
img = Image(
player.get_icon(),
position=(
ts_h_offs - 72.0 * scale,
y_base + (v_offs + 15.0) * scale,
),
scale=(30.0 * scale, 30.0 * scale),
transition=Image.Transition.IN_LEFT,
transition_delay=tdelay,
).autoretain()
ba.timer(
tdelay + delay2,
ba.WeakCall(
self._safe_animate, img.position_combine, 'input1', {
self._safe_animate,
img.position_combine,
'input1',
{
0: y_base + (v_offs + 15.0) * scale,
transtime: y_base + (v_offs_2 + 15.0) * scale
}))
transtime: y_base + (v_offs_2 + 15.0) * scale,
},
),
)
ba.timer(
tdelay + delay3,
ba.WeakCall(
self._safe_animate, img.position_combine, 'input0', {
self._safe_animate,
img.position_combine,
'input0',
{
0: ts_h_offs - 72.0 * scale,
transtime2: ts_h_offs - (72.0 + slide_amt) * scale
}))
txt = Text(ba.Lstr(value=player.getname(full=True)),
maxwidth=130.0,
scale=0.75 * scale,
position=(ts_h_offs - 50.0 * scale,
y_base + (v_offs + 15.0) * scale),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
color=ba.safecolor(player.team.color + (1, )),
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay).autoretain()
transtime2: ts_h_offs - (72.0 + slide_amt) * scale,
},
),
)
txt = Text(
ba.Lstr(value=player.getname(full=True)),
maxwidth=130.0,
scale=0.75 * scale,
position=(
ts_h_offs - 50.0 * scale,
y_base + (v_offs + 15.0) * scale,
),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
color=ba.safecolor(player.team.color + (1,)),
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay,
).autoretain()
ba.timer(
tdelay + delay2,
ba.WeakCall(
self._safe_animate, txt.position_combine, 'input1', {
self._safe_animate,
txt.position_combine,
'input1',
{
0: y_base + (v_offs + 15.0) * scale,
transtime: y_base + (v_offs_2 + 15.0) * scale
}))
transtime: y_base + (v_offs_2 + 15.0) * scale,
},
),
)
ba.timer(
tdelay + delay3,
ba.WeakCall(
self._safe_animate, txt.position_combine, 'input0', {
self._safe_animate,
txt.position_combine,
'input0',
{
0: ts_h_offs - 50.0 * scale,
transtime2: ts_h_offs - (50.0 + slide_amt) * scale
}))
transtime2: ts_h_offs - (50.0 + slide_amt) * scale,
},
),
)
txt_num = Text('#' + str(i + 1),
scale=0.55 * scale,
position=(ts_h_offs - 95.0 * scale,
y_base + (v_offs + 8.0) * scale),
h_align=Text.HAlign.RIGHT,
color=(0.6, 0.6, 0.6, 0.6),
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay).autoretain()
txt_num = Text(
'#' + str(i + 1),
scale=0.55 * scale,
position=(
ts_h_offs - 95.0 * scale,
y_base + (v_offs + 8.0) * scale,
),
h_align=Text.HAlign.RIGHT,
color=(0.6, 0.6, 0.6, 0.6),
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay,
).autoretain()
ba.timer(
tdelay + delay3,
ba.WeakCall(
self._safe_animate, txt_num.position_combine, 'input0', {
self._safe_animate,
txt_num.position_combine,
'input0',
{
0: ts_h_offs - 95.0 * scale,
transtime2: ts_h_offs - (95.0 + slide_amt) * scale
}))
transtime2: ts_h_offs - (95.0 + slide_amt) * scale,
},
),
)
s_txt = _scoretxt(
str(player.team.sessionteam.customdata['previous_score']), 80,
0, False, 0, 1.0)
str(player.team.sessionteam.customdata['previous_score']),
80,
0,
False,
0,
1.0,
)
ba.timer(
tdelay + delay2,
ba.WeakCall(
self._safe_animate, s_txt.position_combine, 'input1', {
self._safe_animate,
s_txt.position_combine,
'input1',
{
0: y_base + (v_offs + 2.0) * scale,
transtime: y_base + (v_offs_2 + 2.0) * scale
}))
transtime: y_base + (v_offs_2 + 2.0) * scale,
},
),
)
ba.timer(
tdelay + delay3,
ba.WeakCall(
self._safe_animate, s_txt.position_combine, 'input0', {
self._safe_animate,
s_txt.position_combine,
'input0',
{
0: ts_h_offs + 80.0 * scale,
transtime2: ts_h_offs + (80.0 - slide_amt) * scale
}))
transtime2: ts_h_offs + (80.0 - slide_amt) * scale,
},
),
)
score_change = (
player.team.sessionteam.customdata['score'] -
player.team.sessionteam.customdata['previous_score'])
player.team.sessionteam.customdata['score']
- player.team.sessionteam.customdata['previous_score']
)
if score_change > 0:
xval = 113
yval = 3.0
s_txt_2 = _scoretxt('+' + str(score_change),
xval,
yval,
True,
0,
0.7,
flash=True)
s_txt_2 = _scoretxt(
'+' + str(score_change),
xval,
yval,
True,
0,
0.7,
flash=True,
)
ba.timer(
tdelay + delay2,
ba.WeakCall(
self._safe_animate, s_txt_2.position_combine, 'input1',
self._safe_animate,
s_txt_2.position_combine,
'input1',
{
0: y_base + (v_offs + yval + 2.0) * scale,
transtime: y_base + (v_offs_2 + yval + 2.0) * scale
}))
transtime: y_base + (v_offs_2 + yval + 2.0) * scale,
},
),
)
ba.timer(
tdelay + delay3,
ba.WeakCall(
self._safe_animate, s_txt_2.position_combine, 'input0',
self._safe_animate,
s_txt_2.position_combine,
'input0',
{
0: ts_h_offs + xval * scale,
transtime2: ts_h_offs + (xval - slide_amt) * scale
}))
transtime2: ts_h_offs + (xval - slide_amt) * scale,
},
),
)
def _safesetattr(node: ba.Node | None, attr: str,
value: Any) -> None:
def _safesetattr(
node: ba.Node | None, attr: str, value: Any
) -> None:
if node:
setattr(node, attr, value)
ba.timer(
tdelay + delay1,
ba.Call(_safesetattr, s_txt.node, 'color', (1, 1, 1, 1)))
ba.Call(_safesetattr, s_txt.node, 'color', (1, 1, 1, 1)),
)
for j in range(score_change):
ba.timer((tdelay + delay1 + 0.15 * j),
ba.Call(
_safesetattr, s_txt.node, 'text',
str(player.team.sessionteam.
customdata['previous_score'] + j + 1)))
ba.timer(
(tdelay + delay1 + 0.15 * j),
ba.Call(
_safesetattr,
s_txt.node,
'text',
str(
player.team.sessionteam.customdata[
'previous_score'
]
+ j
+ 1
),
),
)
tfin = tdelay + delay1 + 0.15 * j
if tfin not in sound_times:
sound_times.add(tfin)
ba.timer(
tfin,
ba.Call(ba.playsound,
self._score_display_sound_small))
ba.Call(
ba.playsound, self._score_display_sound_small
),
)
v_offs -= spacing
def _safe_animate(self, node: ba.Node | None, attr: str,
keys: dict[float, float]) -> None:
def _safe_animate(
self, node: ba.Node | None, attr: str, keys: dict[float, float]
) -> None:
"""Run an animation on a node if the node still exists."""
if node:
ba.animate(node, attr, keys)

View File

@ -24,6 +24,7 @@ class MultiTeamJoinActivity(JoinActivity):
def on_transition_in(self) -> None:
from bastd.actor.controlsguide import ControlsGuide
from ba import DualTeamSession
super().on_transition_in()
ControlsGuide(delay=1.0).autoretain()
@ -31,50 +32,62 @@ class MultiTeamJoinActivity(JoinActivity):
assert isinstance(session, ba.MultiTeamSession)
# Show info about the next up game.
self._next_up_text = Text(ba.Lstr(
value='${1} ${2}',
subs=[('${1}', ba.Lstr(resource='upFirstText')),
('${2}', session.get_next_game_description())]),
h_attach=Text.HAttach.CENTER,
scale=0.7,
v_attach=Text.VAttach.TOP,
h_align=Text.HAlign.CENTER,
position=(0, -70),
flash=False,
color=(0.5, 0.5, 0.5, 1.0),
transition=Text.Transition.FADE_IN,
transition_delay=5.0)
self._next_up_text = Text(
ba.Lstr(
value='${1} ${2}',
subs=[
('${1}', ba.Lstr(resource='upFirstText')),
('${2}', session.get_next_game_description()),
],
),
h_attach=Text.HAttach.CENTER,
scale=0.7,
v_attach=Text.VAttach.TOP,
h_align=Text.HAlign.CENTER,
position=(0, -70),
flash=False,
color=(0.5, 0.5, 0.5, 1.0),
transition=Text.Transition.FADE_IN,
transition_delay=5.0,
)
# In teams mode, show our two team names.
# FIXME: Lobby should handle this.
if isinstance(ba.getsession(), DualTeamSession):
team_names = [team.name for team in ba.getsession().sessionteams]
team_colors = [
tuple(team.color) + (0.5, )
tuple(team.color) + (0.5,)
for team in ba.getsession().sessionteams
]
if len(team_names) == 2:
for i in range(2):
Text(team_names[i],
scale=0.7,
h_attach=Text.HAttach.CENTER,
v_attach=Text.VAttach.TOP,
h_align=Text.HAlign.CENTER,
position=(-200 + 350 * i, -100),
color=team_colors[i],
transition=Text.Transition.FADE_IN).autoretain()
Text(
team_names[i],
scale=0.7,
h_attach=Text.HAttach.CENTER,
v_attach=Text.VAttach.TOP,
h_align=Text.HAlign.CENTER,
position=(-200 + 350 * i, -100),
color=team_colors[i],
transition=Text.Transition.FADE_IN,
).autoretain()
Text(ba.Lstr(resource='mustInviteFriendsText',
subs=[('${GATHER}',
ba.Lstr(resource='gatherWindow.titleText'))]),
h_attach=Text.HAttach.CENTER,
scale=0.8,
host_only=True,
v_attach=Text.VAttach.CENTER,
h_align=Text.HAlign.CENTER,
position=(0, 0),
flash=False,
color=(0, 1, 0, 1.0),
transition=Text.Transition.FADE_IN,
transition_delay=2.0,
transition_out_delay=7.0).autoretain()
Text(
ba.Lstr(
resource='mustInviteFriendsText',
subs=[
('${GATHER}', ba.Lstr(resource='gatherWindow.titleText'))
],
),
h_attach=Text.HAttach.CENTER,
scale=0.8,
host_only=True,
v_attach=Text.VAttach.CENTER,
h_align=Text.HAlign.CENTER,
position=(0, 0),
flash=False,
color=(0, 1, 0, 1.0),
transition=Text.Transition.FADE_IN,
transition_delay=2.0,
transition_out_delay=7.0,
).autoretain()

View File

@ -28,34 +28,43 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
super().on_begin()
session = self.session
if self._show_up_next and isinstance(session, ba.MultiTeamSession):
txt = ba.Lstr(value='${A} ${B}',
subs=[
('${A}',
ba.Lstr(resource='upNextText',
subs=[
('${COUNT}',
str(session.get_game_number() + 1))
])),
('${B}', session.get_next_game_description())
])
Text(txt,
maxwidth=900,
h_attach=Text.HAttach.CENTER,
v_attach=Text.VAttach.BOTTOM,
h_align=Text.HAlign.CENTER,
v_align=Text.VAlign.CENTER,
position=(0, 53),
flash=False,
color=(0.3, 0.3, 0.35, 1.0),
transition=Text.Transition.FADE_IN,
transition_delay=2.0).autoretain()
txt = ba.Lstr(
value='${A} ${B}',
subs=[
(
'${A}',
ba.Lstr(
resource='upNextText',
subs=[
('${COUNT}', str(session.get_game_number() + 1))
],
),
),
('${B}', session.get_next_game_description()),
],
)
Text(
txt,
maxwidth=900,
h_attach=Text.HAttach.CENTER,
v_attach=Text.VAttach.BOTTOM,
h_align=Text.HAlign.CENTER,
v_align=Text.VAlign.CENTER,
position=(0, 53),
flash=False,
color=(0.3, 0.3, 0.35, 1.0),
transition=Text.Transition.FADE_IN,
transition_delay=2.0,
).autoretain()
def show_player_scores(self,
delay: float = 2.5,
results: ba.GameResults | None = None,
scale: float = 1.0,
x_offset: float = 0.0,
y_offset: float = 0.0) -> None:
def show_player_scores(
self,
delay: float = 2.5,
results: ba.GameResults | None = None,
scale: float = 1.0,
x_offset: float = 0.0,
y_offset: float = 0.0,
) -> None:
"""Show scores for individual players."""
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
@ -96,7 +105,8 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
# noinspection PyUnresolvedReferences
def _get_player_score_set_entry(
player: ba.SessionPlayer) -> ba.PlayerRecord | None:
player: ba.SessionPlayer,
) -> ba.PlayerRecord | None:
for p_rec in valid_players:
if p_rec[1].player is player:
return p_rec[1]
@ -108,7 +118,8 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
for team in winnergroup.teams:
if len(team.players) == 1:
player_entry = _get_player_score_set_entry(
team.players[0])
team.players[0]
)
if player_entry is not None:
player_records.append(player_entry)
else:
@ -124,33 +135,43 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
voffs = -140.0 + spacing * len(player_records) * 0.5
def _txt(xoffs: float,
yoffs: float,
text: ba.Lstr,
h_align: Text.HAlign = Text.HAlign.RIGHT,
extrascale: float = 1.0,
maxwidth: float | None = 120.0) -> None:
Text(text,
color=(0.5, 0.5, 0.6, 0.5),
position=(ts_h_offs + xoffs * scale,
ts_v_offset + (voffs + yoffs + 4.0) * scale),
h_align=h_align,
v_align=Text.VAlign.CENTER,
scale=0.8 * scale * extrascale,
maxwidth=maxwidth,
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay).autoretain()
def _txt(
xoffs: float,
yoffs: float,
text: ba.Lstr,
h_align: Text.HAlign = Text.HAlign.RIGHT,
extrascale: float = 1.0,
maxwidth: float | None = 120.0,
) -> None:
Text(
text,
color=(0.5, 0.5, 0.6, 0.5),
position=(
ts_h_offs + xoffs * scale,
ts_v_offset + (voffs + yoffs + 4.0) * scale,
),
h_align=h_align,
v_align=Text.VAlign.CENTER,
scale=0.8 * scale * extrascale,
maxwidth=maxwidth,
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay,
).autoretain()
session = self.session
assert isinstance(session, ba.MultiTeamSession)
tval = ba.Lstr(resource='gameLeadersText',
subs=[('${COUNT}', str(session.get_game_number()))])
_txt(180,
43,
tval,
h_align=Text.HAlign.CENTER,
extrascale=1.4,
maxwidth=None)
tval = ba.Lstr(
resource='gameLeadersText',
subs=[('${COUNT}', str(session.get_game_number()))],
)
_txt(
180,
43,
tval,
h_align=Text.HAlign.CENTER,
extrascale=1.4,
maxwidth=None,
)
_txt(-15, 4, ba.Lstr(resource='playerText'), h_align=Text.HAlign.LEFT)
_txt(180, 4, ba.Lstr(resource='killsText'))
_txt(280, 4, ba.Lstr(resource='deathsText'), maxwidth=100)
@ -162,52 +183,80 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
topkillcount = 0
topkilledcount = 99999
top_score = 0 if not player_records else _get_prec_score(
player_records[0])
top_score = (
0 if not player_records else _get_prec_score(player_records[0])
)
for prec in player_records:
topkillcount = max(topkillcount, prec.accum_kill_count)
topkilledcount = min(topkilledcount, prec.accum_killed_count)
def _scoretxt(text: str | ba.Lstr,
x_offs: float,
highlight: bool,
delay2: float,
maxwidth: float = 70.0) -> None:
Text(text,
position=(ts_h_offs + x_offs * scale,
ts_v_offset + (voffs + 15) * scale),
scale=scale,
color=(1.0, 0.9, 0.5, 1.0) if highlight else
(0.5, 0.5, 0.6, 0.5),
h_align=Text.HAlign.RIGHT,
v_align=Text.VAlign.CENTER,
maxwidth=maxwidth,
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay + delay2).autoretain()
def _scoretxt(
text: str | ba.Lstr,
x_offs: float,
highlight: bool,
delay2: float,
maxwidth: float = 70.0,
) -> None:
Text(
text,
position=(
ts_h_offs + x_offs * scale,
ts_v_offset + (voffs + 15) * scale,
),
scale=scale,
color=(1.0, 0.9, 0.5, 1.0)
if highlight
else (0.5, 0.5, 0.6, 0.5),
h_align=Text.HAlign.RIGHT,
v_align=Text.VAlign.CENTER,
maxwidth=maxwidth,
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay + delay2,
).autoretain()
for playerrec in player_records:
tdelay += 0.05
voffs -= spacing
Image(playerrec.get_icon(),
position=(ts_h_offs - 12 * scale,
ts_v_offset + (voffs + 15.0) * scale),
scale=(30.0 * scale, 30.0 * scale),
transition=Image.Transition.IN_LEFT,
transition_delay=tdelay).autoretain()
Text(ba.Lstr(value=playerrec.getname(full=True)),
maxwidth=160,
scale=0.75 * scale,
position=(ts_h_offs + 10.0 * scale,
ts_v_offset + (voffs + 15) * scale),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
color=ba.safecolor(playerrec.team.color + (1, )),
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay).autoretain()
_scoretxt(str(playerrec.accum_kill_count), 180,
playerrec.accum_kill_count == topkillcount, 0.1)
_scoretxt(str(playerrec.accum_killed_count), 280,
playerrec.accum_killed_count == topkilledcount, 0.1)
_scoretxt(_get_prec_score_str(playerrec), 390,
_get_prec_score(playerrec) == top_score, 0.2)
Image(
playerrec.get_icon(),
position=(
ts_h_offs - 12 * scale,
ts_v_offset + (voffs + 15.0) * scale,
),
scale=(30.0 * scale, 30.0 * scale),
transition=Image.Transition.IN_LEFT,
transition_delay=tdelay,
).autoretain()
Text(
ba.Lstr(value=playerrec.getname(full=True)),
maxwidth=160,
scale=0.75 * scale,
position=(
ts_h_offs + 10.0 * scale,
ts_v_offset + (voffs + 15) * scale,
),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
color=ba.safecolor(playerrec.team.color + (1,)),
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay,
).autoretain()
_scoretxt(
str(playerrec.accum_kill_count),
180,
playerrec.accum_kill_count == topkillcount,
0.1,
)
_scoretxt(
str(playerrec.accum_killed_count),
280,
playerrec.accum_killed_count == topkilledcount,
0.1,
)
_scoretxt(
_get_prec_score_str(playerrec),
390,
_get_prec_score(playerrec) == top_score,
0.2,
)

View File

@ -33,8 +33,12 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
# pylint: disable=too-many-statements
from bastd.actor.text import Text
from bastd.actor.image import Image
ba.set_analytics_screen('FreeForAll Series Victory Screen' if self.
_is_ffa else 'Teams Series Victory Screen')
ba.set_analytics_screen(
'FreeForAll Series Victory Screen'
if self._is_ffa
else 'Teams Series Victory Screen'
)
if ba.app.ui.uiscale is ba.UIScale.LARGE:
sval = ba.Lstr(resource='pressAnyKeyButtonPlayAgainText')
else:
@ -46,8 +50,9 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
# Pause a moment before playing victory music.
ba.timer(0.6, ba.WeakCall(self._play_victory_music))
ba.timer(4.4,
ba.WeakCall(self._show_winner, self.settings_raw['winner']))
ba.timer(
4.4, ba.WeakCall(self._show_winner, self.settings_raw['winner'])
)
ba.timer(4.6, ba.Call(ba.playsound, self._score_display_sound))
# Score / Name / Player-record.
@ -58,8 +63,12 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
for _pkey, prec in self.stats.get_records().items():
if prec.player.in_game:
player_entries.append(
(prec.player.sessionteam.customdata['score'],
prec.getname(full=True), prec))
(
prec.player.sessionteam.customdata['score'],
prec.getname(full=True),
prec,
)
)
player_entries.sort(reverse=True, key=lambda x: x[0])
else:
for _pkey, prec in self.stats.get_records().items():
@ -72,18 +81,29 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
t_incr = 0.12
always_use_first_to = ba.app.lang.get_resource(
'bestOfUseFirstToInstead')
'bestOfUseFirstToInstead'
)
session = self.session
if self._is_ffa:
assert isinstance(session, ba.FreeForAllSession)
txt = ba.Lstr(
value='${A}:',
subs=[('${A}',
ba.Lstr(resource='firstToFinalText',
subs=[('${COUNT}',
str(session.get_ffa_series_length()))]))
])
subs=[
(
'${A}',
ba.Lstr(
resource='firstToFinalText',
subs=[
(
'${COUNT}',
str(session.get_ffa_series_length()),
)
],
),
)
],
)
else:
assert isinstance(session, ba.MultiTeamSession)
@ -96,31 +116,52 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
txt = ba.Lstr(
value='${A}:',
subs=[
('${A}',
ba.Lstr(resource='firstToFinalText',
subs=[
('${COUNT}',
str(session.get_series_length() / 2 + 1))
]))
])
(
'${A}',
ba.Lstr(
resource='firstToFinalText',
subs=[
(
'${COUNT}',
str(
session.get_series_length() / 2 + 1
),
)
],
),
)
],
)
else:
txt = ba.Lstr(
value='${A}:',
subs=[('${A}',
ba.Lstr(resource='bestOfFinalText',
subs=[('${COUNT}',
str(session.get_series_length()))]))
])
subs=[
(
'${A}',
ba.Lstr(
resource='bestOfFinalText',
subs=[
(
'${COUNT}',
str(session.get_series_length()),
)
],
),
)
],
)
Text(txt,
v_align=Text.VAlign.CENTER,
maxwidth=300,
color=(0.5, 0.5, 0.5, 1.0),
position=(0, 220),
scale=1.2,
transition=Text.Transition.IN_TOP_SLOW,
h_align=Text.HAlign.CENTER,
transition_delay=t_incr * 4).autoretain()
Text(
txt,
v_align=Text.VAlign.CENTER,
maxwidth=300,
color=(0.5, 0.5, 0.5, 1.0),
position=(0, 220),
scale=1.2,
transition=Text.Transition.IN_TOP_SLOW,
h_align=Text.HAlign.CENTER,
transition_delay=t_incr * 4,
).autoretain()
win_score = (session.get_series_length() - 1) // 2 + 1
lose_score = 0
@ -129,17 +170,23 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
lose_score = team.sessionteam.customdata['score']
if not self._is_ffa:
Text(ba.Lstr(resource='gamesToText',
subs=[('${WINCOUNT}', str(win_score)),
('${LOSECOUNT}', str(lose_score))]),
color=(0.5, 0.5, 0.5, 1.0),
maxwidth=160,
v_align=Text.VAlign.CENTER,
position=(0, -215),
scale=1.8,
transition=Text.Transition.IN_LEFT,
h_align=Text.HAlign.CENTER,
transition_delay=4.8 + t_incr * 4).autoretain()
Text(
ba.Lstr(
resource='gamesToText',
subs=[
('${WINCOUNT}', str(win_score)),
('${LOSECOUNT}', str(lose_score)),
],
),
color=(0.5, 0.5, 0.5, 1.0),
maxwidth=160,
v_align=Text.VAlign.CENTER,
position=(0, -215),
scale=1.8,
transition=Text.Transition.IN_LEFT,
h_align=Text.HAlign.CENTER,
transition_delay=4.8 + t_incr * 4,
).autoretain()
if self._is_ffa:
v_extra = 120
@ -158,31 +205,37 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
mvp_name = entry[1]
break
if mvp is not None:
Text(ba.Lstr(resource='mostValuablePlayerText'),
color=(0.5, 0.5, 0.5, 1.0),
v_align=Text.VAlign.CENTER,
maxwidth=300,
position=(180, ts_height / 2 + 15),
transition=Text.Transition.IN_LEFT,
h_align=Text.HAlign.LEFT,
transition_delay=tval).autoretain()
Text(
ba.Lstr(resource='mostValuablePlayerText'),
color=(0.5, 0.5, 0.5, 1.0),
v_align=Text.VAlign.CENTER,
maxwidth=300,
position=(180, ts_height / 2 + 15),
transition=Text.Transition.IN_LEFT,
h_align=Text.HAlign.LEFT,
transition_delay=tval,
).autoretain()
tval += 4 * t_incr
Image(mvp.get_icon(),
position=(230, ts_height / 2 - 55 + 14 - 5),
scale=(70, 70),
transition=Image.Transition.IN_LEFT,
transition_delay=tval).autoretain()
Image(
mvp.get_icon(),
position=(230, ts_height / 2 - 55 + 14 - 5),
scale=(70, 70),
transition=Image.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
assert mvp_name is not None
Text(ba.Lstr(value=mvp_name),
position=(280, ts_height / 2 - 55 + 15 - 5),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
maxwidth=170,
scale=1.3,
color=ba.safecolor(mvp.team.color + (1, )),
transition=Text.Transition.IN_LEFT,
transition_delay=tval).autoretain()
Text(
ba.Lstr(value=mvp_name),
position=(280, ts_height / 2 - 55 + 15 - 5),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
maxwidth=170,
scale=1.3,
color=ba.safecolor(mvp.team.color + (1,)),
transition=Text.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
tval += 4 * t_incr
# Most violent.
@ -193,41 +246,56 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
mvp_name = entry[1]
most_kills = entry[2].kill_count
if mvp is not None:
Text(ba.Lstr(resource='mostViolentPlayerText'),
color=(0.5, 0.5, 0.5, 1.0),
v_align=Text.VAlign.CENTER,
maxwidth=300,
position=(180, ts_height / 2 - 150 + v_extra + 15),
transition=Text.Transition.IN_LEFT,
h_align=Text.HAlign.LEFT,
transition_delay=tval).autoretain()
Text(ba.Lstr(value='(${A})',
subs=[('${A}',
ba.Lstr(resource='killsTallyText',
subs=[('${COUNT}', str(most_kills))]))
]),
position=(260, ts_height / 2 - 150 - 15 + v_extra),
color=(0.3, 0.3, 0.3, 1.0),
scale=0.6,
h_align=Text.HAlign.LEFT,
transition=Text.Transition.IN_LEFT,
transition_delay=tval).autoretain()
Text(
ba.Lstr(resource='mostViolentPlayerText'),
color=(0.5, 0.5, 0.5, 1.0),
v_align=Text.VAlign.CENTER,
maxwidth=300,
position=(180, ts_height / 2 - 150 + v_extra + 15),
transition=Text.Transition.IN_LEFT,
h_align=Text.HAlign.LEFT,
transition_delay=tval,
).autoretain()
Text(
ba.Lstr(
value='(${A})',
subs=[
(
'${A}',
ba.Lstr(
resource='killsTallyText',
subs=[('${COUNT}', str(most_kills))],
),
)
],
),
position=(260, ts_height / 2 - 150 - 15 + v_extra),
color=(0.3, 0.3, 0.3, 1.0),
scale=0.6,
h_align=Text.HAlign.LEFT,
transition=Text.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
tval += 4 * t_incr
Image(mvp.get_icon(),
position=(233, ts_height / 2 - 150 - 30 - 46 + 25 + v_extra),
scale=(50, 50),
transition=Image.Transition.IN_LEFT,
transition_delay=tval).autoretain()
Image(
mvp.get_icon(),
position=(233, ts_height / 2 - 150 - 30 - 46 + 25 + v_extra),
scale=(50, 50),
transition=Image.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
assert mvp_name is not None
Text(ba.Lstr(value=mvp_name),
position=(270, ts_height / 2 - 150 - 30 - 36 + v_extra + 15),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
maxwidth=180,
color=ba.safecolor(mvp.team.color + (1, )),
transition=Text.Transition.IN_LEFT,
transition_delay=tval).autoretain()
Text(
ba.Lstr(value=mvp_name),
position=(270, ts_height / 2 - 150 - 30 - 36 + v_extra + 15),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
maxwidth=180,
color=ba.safecolor(mvp.team.color + (1,)),
transition=Text.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
tval += 4 * t_incr
# Most killed.
@ -239,49 +307,66 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
mkp_name = entry[1]
most_killed = entry[2].killed_count
if mkp is not None:
Text(ba.Lstr(resource='mostViolatedPlayerText'),
color=(0.5, 0.5, 0.5, 1.0),
v_align=Text.VAlign.CENTER,
maxwidth=300,
position=(180, ts_height / 2 - 300 + v_extra + 15),
transition=Text.Transition.IN_LEFT,
h_align=Text.HAlign.LEFT,
transition_delay=tval).autoretain()
Text(ba.Lstr(value='(${A})',
subs=[('${A}',
ba.Lstr(resource='deathsTallyText',
subs=[('${COUNT}', str(most_killed))]))
]),
position=(260, ts_height / 2 - 300 - 15 + v_extra),
h_align=Text.HAlign.LEFT,
scale=0.6,
color=(0.3, 0.3, 0.3, 1.0),
transition=Text.Transition.IN_LEFT,
transition_delay=tval).autoretain()
Text(
ba.Lstr(resource='mostViolatedPlayerText'),
color=(0.5, 0.5, 0.5, 1.0),
v_align=Text.VAlign.CENTER,
maxwidth=300,
position=(180, ts_height / 2 - 300 + v_extra + 15),
transition=Text.Transition.IN_LEFT,
h_align=Text.HAlign.LEFT,
transition_delay=tval,
).autoretain()
Text(
ba.Lstr(
value='(${A})',
subs=[
(
'${A}',
ba.Lstr(
resource='deathsTallyText',
subs=[('${COUNT}', str(most_killed))],
),
)
],
),
position=(260, ts_height / 2 - 300 - 15 + v_extra),
h_align=Text.HAlign.LEFT,
scale=0.6,
color=(0.3, 0.3, 0.3, 1.0),
transition=Text.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
tval += 4 * t_incr
Image(mkp.get_icon(),
position=(233, ts_height / 2 - 300 - 30 - 46 + 25 + v_extra),
scale=(50, 50),
transition=Image.Transition.IN_LEFT,
transition_delay=tval).autoretain()
Image(
mkp.get_icon(),
position=(233, ts_height / 2 - 300 - 30 - 46 + 25 + v_extra),
scale=(50, 50),
transition=Image.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
assert mkp_name is not None
Text(ba.Lstr(value=mkp_name),
position=(270, ts_height / 2 - 300 - 30 - 36 + v_extra + 15),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
color=ba.safecolor(mkp.team.color + (1, )),
maxwidth=180,
transition=Text.Transition.IN_LEFT,
transition_delay=tval).autoretain()
Text(
ba.Lstr(value=mkp_name),
position=(270, ts_height / 2 - 300 - 30 - 36 + v_extra + 15),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
color=ba.safecolor(mkp.team.color + (1,)),
maxwidth=180,
transition=Text.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
tval += 4 * t_incr
# Now show individual scores.
tdelay = tval
Text(ba.Lstr(resource='finalScoresText'),
color=(0.5, 0.5, 0.5, 1.0),
position=(ts_h_offs, ts_height / 2),
transition=Text.Transition.IN_RIGHT,
transition_delay=tdelay).autoretain()
Text(
ba.Lstr(resource='finalScoresText'),
color=(0.5, 0.5, 0.5, 1.0),
position=(ts_h_offs, ts_height / 2),
transition=Text.Transition.IN_RIGHT,
transition_delay=tdelay,
).autoretain()
tdelay += 4 * t_incr
v_offs = 0.0
@ -289,33 +374,41 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
for _score, name, prec in player_entries:
tdelay -= 4 * t_incr
v_offs -= 40
Text(str(prec.team.customdata['score'])
if self._is_ffa else str(prec.score),
color=(0.5, 0.5, 0.5, 1.0),
position=(ts_h_offs + 230, ts_height / 2 + v_offs),
h_align=Text.HAlign.RIGHT,
transition=Text.Transition.IN_RIGHT,
transition_delay=tdelay).autoretain()
Text(
str(prec.team.customdata['score'])
if self._is_ffa
else str(prec.score),
color=(0.5, 0.5, 0.5, 1.0),
position=(ts_h_offs + 230, ts_height / 2 + v_offs),
h_align=Text.HAlign.RIGHT,
transition=Text.Transition.IN_RIGHT,
transition_delay=tdelay,
).autoretain()
tdelay -= 4 * t_incr
Image(prec.get_icon(),
position=(ts_h_offs - 72, ts_height / 2 + v_offs + 15),
scale=(30, 30),
transition=Image.Transition.IN_LEFT,
transition_delay=tdelay).autoretain()
Text(ba.Lstr(value=name),
position=(ts_h_offs - 50, ts_height / 2 + v_offs + 15),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
maxwidth=180,
color=ba.safecolor(prec.team.color + (1, )),
transition=Text.Transition.IN_RIGHT,
transition_delay=tdelay).autoretain()
Image(
prec.get_icon(),
position=(ts_h_offs - 72, ts_height / 2 + v_offs + 15),
scale=(30, 30),
transition=Image.Transition.IN_LEFT,
transition_delay=tdelay,
).autoretain()
Text(
ba.Lstr(value=name),
position=(ts_h_offs - 50, ts_height / 2 + v_offs + 15),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
maxwidth=180,
color=ba.safecolor(prec.team.color + (1,)),
transition=Text.Transition.IN_RIGHT,
transition_delay=tdelay,
).autoretain()
ba.timer(15.0, ba.WeakCall(self._show_tips))
def _show_tips(self) -> None:
from bastd.actor.tipstext import TipsText
self._tips_text = TipsText(offs_y=70)
def _play_victory_music(self) -> None:
@ -327,29 +420,37 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
def _show_winner(self, team: ba.SessionTeam) -> None:
from bastd.actor.image import Image
from bastd.actor.zoomtext import ZoomText
if not self._is_ffa:
offs_v = 0.0
ZoomText(team.name,
position=(0, 97),
color=team.color,
scale=1.15,
jitter=1.0,
maxwidth=250).autoretain()
ZoomText(
team.name,
position=(0, 97),
color=team.color,
scale=1.15,
jitter=1.0,
maxwidth=250,
).autoretain()
else:
offs_v = -80.0
if len(team.players) == 1:
i = Image(team.players[0].get_icon(),
position=(0, 143),
scale=(100, 100)).autoretain()
i = Image(
team.players[0].get_icon(),
position=(0, 143),
scale=(100, 100),
).autoretain()
assert i.node
ba.animate(i.node, 'opacity', {0.0: 0.0, 0.25: 1.0})
ZoomText(ba.Lstr(
value=team.players[0].getname(full=True, icon=False)),
position=(0, 97 + offs_v),
color=team.color,
scale=1.15,
jitter=1.0,
maxwidth=250).autoretain()
ZoomText(
ba.Lstr(
value=team.players[0].getname(full=True, icon=False)
),
position=(0, 97 + offs_v),
color=team.color,
scale=1.15,
jitter=1.0,
maxwidth=250,
).autoretain()
s_extra = 1.0 if self._is_ffa else 1.0
@ -362,15 +463,19 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
# Temp - if these come up as the english default, fall-back to the
# unified old form which is more likely to be translated.
ZoomText(wins_text,
position=(0, -10 + offs_v),
color=team.color,
scale=0.65 * s_extra,
jitter=1.0,
maxwidth=250).autoretain()
ZoomText(ba.Lstr(resource='seriesWinLine2Text'),
position=(0, -110 + offs_v),
scale=1.0 * s_extra,
color=team.color,
jitter=1.0,
maxwidth=250).autoretain()
ZoomText(
wins_text,
position=(0, -10 + offs_v),
color=team.color,
scale=0.65 * s_extra,
jitter=1.0,
maxwidth=250,
).autoretain()
ZoomText(
ba.Lstr(resource='seriesWinLine2Text'),
position=(0, -110 + offs_v),
scale=1.0 * s_extra,
color=team.color,
jitter=1.0,
maxwidth=250,
).autoretain()

View File

@ -17,10 +17,12 @@ if TYPE_CHECKING:
class Background(ba.Actor):
"""Simple Fading Background Actor."""
def __init__(self,
fade_time: float = 0.5,
start_faded: bool = False,
show_logo: bool = False):
def __init__(
self,
fade_time: float = 0.5,
start_faded: bool = False,
show_logo: bool = False,
):
super().__init__()
self._dying = False
self.fade_time = fade_time
@ -31,22 +33,24 @@ class Background(ba.Actor):
session = ba.getsession()
self._session = weakref.ref(session)
with ba.Context(session):
self.node = ba.newnode('image',
delegate=self,
attrs={
'fill_screen': True,
'texture': ba.gettexture('bg'),
'tilt_translate': -0.3,
'has_alpha_channel': False,
'color': (1, 1, 1)
})
self.node = ba.newnode(
'image',
delegate=self,
attrs={
'fill_screen': True,
'texture': ba.gettexture('bg'),
'tilt_translate': -0.3,
'has_alpha_channel': False,
'color': (1, 1, 1),
},
)
if not start_faded:
ba.animate(self.node,
'opacity', {
0.0: 0.0,
self.fade_time: 1.0
},
loop=False)
ba.animate(
self.node,
'opacity',
{0.0: 0.0, self.fade_time: 1.0},
loop=False,
)
if show_logo:
logo_texture = ba.gettexture('logo')
logo_model = ba.getmodel('logo')
@ -63,27 +67,27 @@ class Background(ba.Actor):
'color': (0.15, 0.15, 0.15),
'position': (0, 0),
'tilt_translate': -0.05,
'absolute_scale': False
})
'absolute_scale': False,
},
)
self.node.connectattr('opacity', self.logo, 'opacity')
# add jitter/pulse for a stop-motion-y look unless we're in VR
# in which case stillness is better
if not ba.app.vr_mode:
self.cmb = ba.newnode('combine',
owner=self.node,
attrs={'size': 2})
self.cmb = ba.newnode(
'combine', owner=self.node, attrs={'size': 2}
)
for attr in ['input0', 'input1']:
ba.animate(self.cmb,
attr, {
0.0: 0.693,
0.05: 0.7,
0.5: 0.693
},
loop=True)
ba.animate(
self.cmb,
attr,
{0.0: 0.693, 0.05: 0.7, 0.5: 0.693},
loop=True,
)
self.cmb.connectattr('output', self.logo, 'scale')
cmb = ba.newnode('combine',
owner=self.node,
attrs={'size': 2})
cmb = ba.newnode(
'combine', owner=self.node, attrs={'size': 2}
)
cmb.connectattr('output', self.logo, 'position')
# Gen some random keys for that stop-motion-y look.
keys = {}
@ -114,8 +118,10 @@ class Background(ba.Actor):
# since it was part of the session's scene.
# Let's make sure that's the case.
# (since otherwise we have no way to kill it)
ba.print_error('got None session on Background _die'
' (and node still exists!)')
ba.print_error(
'got None session on Background _die'
' (and node still exists!)'
)
elif session is not None:
with ba.Context(session):
if not self._dying and self.node:
@ -123,12 +129,12 @@ class Background(ba.Actor):
if immediate:
self.node.delete()
else:
ba.animate(self.node,
'opacity', {
0.0: 1.0,
self.fade_time: 0.0
},
loop=False)
ba.animate(
self.node,
'opacity',
{0.0: 1.0, self.fade_time: 0.0},
loop=False,
)
ba.timer(self.fade_time + 0.1, self.node.delete)
def handlemessage(self, msg: Any) -> Any:

File diff suppressed because it is too large Load Diff

View File

@ -23,12 +23,14 @@ class ControlsGuide(ba.Actor):
be newbies watching.
"""
def __init__(self,
position: tuple[float, float] = (390.0, 120.0),
scale: float = 1.0,
delay: float = 0.0,
lifespan: float | None = None,
bright: bool = False):
def __init__(
self,
position: tuple[float, float] = (390.0, 120.0),
scale: float = 1.0,
delay: float = 0.0,
lifespan: float | None = None,
bright: bool = False,
):
"""Instantiate an overlay.
delay: is the time in seconds before the overlay fades in.
@ -62,19 +64,31 @@ class ControlsGuide(ba.Actor):
if ba.app.iircade_mode:
xtweak = 0.2
ytweak = 0.2
jump_pos = (position[0] + offs * (-1.2 + xtweak),
position[1] + offs * (0.1 + ytweak))
bomb_pos = (position[0] + offs * (0.0 + xtweak),
position[1] + offs * (0.5 + ytweak))
punch_pos = (position[0] + offs * (1.2 + xtweak),
position[1] + offs * (0.5 + ytweak))
jump_pos = (
position[0] + offs * (-1.2 + xtweak),
position[1] + offs * (0.1 + ytweak),
)
bomb_pos = (
position[0] + offs * (0.0 + xtweak),
position[1] + offs * (0.5 + ytweak),
)
punch_pos = (
position[0] + offs * (1.2 + xtweak),
position[1] + offs * (0.5 + ytweak),
)
pickup_pos = (position[0] + offs * (-1.4 + xtweak),
position[1] + offs * (-1.2 + ytweak))
extra_pos_1 = (position[0] + offs * (-0.2 + xtweak),
position[1] + offs * (-0.8 + ytweak))
extra_pos_2 = (position[0] + offs * (1.0 + xtweak),
position[1] + offs * (-0.8 + ytweak))
pickup_pos = (
position[0] + offs * (-1.4 + xtweak),
position[1] + offs * (-1.2 + ytweak),
)
extra_pos_1 = (
position[0] + offs * (-0.2 + xtweak),
position[1] + offs * (-0.8 + ytweak),
)
extra_pos_2 = (
position[0] + offs * (1.0 + xtweak),
position[1] + offs * (-0.8 + ytweak),
)
self._force_hide_button_names = True
else:
punch_pos = (position[0] - offs * 1.1, position[1])
@ -86,25 +100,32 @@ class ControlsGuide(ba.Actor):
self._force_hide_button_names = False
if show_title:
self._title_text_pos_top = (position[0],
position[1] + 139.0 * scale)
self._title_text_pos_bottom = (position[0],
position[1] + 139.0 * scale)
self._title_text_pos_top = (
position[0],
position[1] + 139.0 * scale,
)
self._title_text_pos_bottom = (
position[0],
position[1] + 139.0 * scale,
)
clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7)
tval = ba.Lstr(value='${A}:',
subs=[('${A}', ba.Lstr(resource='controlsText'))])
self._title_text = ba.newnode('text',
attrs={
'text': tval,
'host_only': True,
'scale': 1.1 * scale,
'shadow': 0.5,
'flatness': 1.0,
'maxwidth': 480,
'v_align': 'center',
'h_align': 'center',
'color': clr
})
tval = ba.Lstr(
value='${A}:', subs=[('${A}', ba.Lstr(resource='controlsText'))]
)
self._title_text = ba.newnode(
'text',
attrs={
'text': tval,
'host_only': True,
'scale': 1.1 * scale,
'shadow': 0.5,
'flatness': 1.0,
'maxwidth': 480,
'v_align': 'center',
'h_align': 'center',
'color': clr,
},
)
else:
self._title_text = None
pos = jump_pos
@ -118,20 +139,23 @@ class ControlsGuide(ba.Actor):
'vr_depth': 10,
'position': pos,
'scale': (image_size, image_size),
'color': clr
})
self._jump_text = ba.newnode('text',
attrs={
'v_align': 'top',
'h_align': 'center',
'scale': 1.5 * scale,
'flatness': 1.0,
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
'position': (pos[0], pos[1] - offs5),
'color': clr
})
'color': clr,
},
)
self._jump_text = ba.newnode(
'text',
attrs={
'v_align': 'top',
'h_align': 'center',
'scale': 1.5 * scale,
'flatness': 1.0,
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
'position': (pos[0], pos[1] - offs5),
'color': clr,
},
)
clr = (0.2, 0.6, 1) if ouya else (1, 0.7, 0.3)
pos = punch_pos
self._punch_image = ba.newnode(
@ -143,20 +167,23 @@ class ControlsGuide(ba.Actor):
'vr_depth': 10,
'position': pos,
'scale': (image_size, image_size),
'color': clr
})
self._punch_text = ba.newnode('text',
attrs={
'v_align': 'top',
'h_align': 'center',
'scale': 1.5 * scale,
'flatness': 1.0,
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
'position': (pos[0], pos[1] - offs5),
'color': clr
})
'color': clr,
},
)
self._punch_text = ba.newnode(
'text',
attrs={
'v_align': 'top',
'h_align': 'center',
'scale': 1.5 * scale,
'flatness': 1.0,
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
'position': (pos[0], pos[1] - offs5),
'color': clr,
},
)
pos = bomb_pos
clr = (1, 0.3, 0.3)
self._bomb_image = ba.newnode(
@ -168,20 +195,23 @@ class ControlsGuide(ba.Actor):
'vr_depth': 10,
'position': pos,
'scale': (image_size, image_size),
'color': clr
})
self._bomb_text = ba.newnode('text',
attrs={
'h_align': 'center',
'v_align': 'top',
'scale': 1.5 * scale,
'flatness': 1.0,
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
'position': (pos[0], pos[1] - offs5),
'color': clr
})
'color': clr,
},
)
self._bomb_text = ba.newnode(
'text',
attrs={
'h_align': 'center',
'v_align': 'top',
'scale': 1.5 * scale,
'flatness': 1.0,
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
'position': (pos[0], pos[1] - offs5),
'color': clr,
},
)
pos = pickup_pos
clr = (1, 0.8, 0.3) if ouya else (0.8, 0.5, 1)
self._pickup_image = ba.newnode(
@ -193,25 +223,27 @@ class ControlsGuide(ba.Actor):
'vr_depth': 10,
'position': pos,
'scale': (image_size, image_size),
'color': clr
})
self._pick_up_text = ba.newnode('text',
attrs={
'v_align': 'top',
'h_align': 'center',
'scale': 1.5 * scale,
'flatness': 1.0,
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
'position':
(pos[0], pos[1] - offs5),
'color': clr
})
'color': clr,
},
)
self._pick_up_text = ba.newnode(
'text',
attrs={
'v_align': 'top',
'h_align': 'center',
'scale': 1.5 * scale,
'flatness': 1.0,
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
'position': (pos[0], pos[1] - offs5),
'color': clr,
},
)
clr = (0.9, 0.9, 2.0, 1.0) if bright else (0.8, 0.8, 2.0, 1.0)
self._run_text_pos_top = (position[0], position[1] - 135.0 * scale)
self._run_text_pos_bottom = (position[0], position[1] - 172.0 * scale)
sval = (1.0 * scale if ba.app.vr_mode else 0.8 * scale)
sval = 1.0 * scale if ba.app.vr_mode else 0.8 * scale
self._run_text = ba.newnode(
'text',
attrs={
@ -222,20 +254,23 @@ class ControlsGuide(ba.Actor):
'maxwidth': 380,
'v_align': 'top',
'h_align': 'center',
'color': clr
})
'color': clr,
},
)
clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7)
self._extra_text = ba.newnode('text',
attrs={
'scale': 0.8 * scale,
'host_only': True,
'shadow': 0.5,
'flatness': 1.0,
'maxwidth': 380,
'v_align': 'top',
'h_align': 'center',
'color': clr
})
self._extra_text = ba.newnode(
'text',
attrs={
'scale': 0.8 * scale,
'host_only': True,
'shadow': 0.5,
'flatness': 1.0,
'maxwidth': 380,
'v_align': 'top',
'h_align': 'center',
'color': clr,
},
)
if extra_pos_1 is not None:
self._extra_image_1: ba.Node | None = ba.newnode(
@ -247,8 +282,9 @@ class ControlsGuide(ba.Actor):
'vr_depth': 10,
'position': extra_pos_1,
'scale': (image_size, image_size),
'color': (0.5, 0.5, 0.5)
})
'color': (0.5, 0.5, 0.5),
},
)
else:
self._extra_image_1 = None
if extra_pos_2 is not None:
@ -261,16 +297,23 @@ class ControlsGuide(ba.Actor):
'vr_depth': 10,
'position': extra_pos_2,
'scale': (image_size, image_size),
'color': (0.5, 0.5, 0.5)
})
'color': (0.5, 0.5, 0.5),
},
)
else:
self._extra_image_2 = None
self._nodes = [
self._bomb_image, self._bomb_text, self._punch_image,
self._punch_text, self._jump_image, self._jump_text,
self._pickup_image, self._pick_up_text, self._run_text,
self._extra_text
self._bomb_image,
self._bomb_text,
self._punch_image,
self._punch_text,
self._jump_image,
self._jump_text,
self._pickup_image,
self._pick_up_text,
self._run_text,
self._extra_text,
]
if show_title:
assert self._title_text
@ -304,10 +347,11 @@ class ControlsGuide(ba.Actor):
if self._lifespan is not None:
self._cancel_timer = ba.Timer(
self._lifespan,
ba.WeakCall(self.handlemessage, ba.DieMessage(immediate=True)))
self._fade_in_timer = ba.Timer(1.0,
ba.WeakCall(self._check_fade_in),
repeat=True)
ba.WeakCall(self.handlemessage, ba.DieMessage(immediate=True)),
)
self._fade_in_timer = ba.Timer(
1.0, ba.WeakCall(self._check_fade_in), repeat=True
)
self._check_fade_in() # Do one check immediately.
def _check_fade_in(self) -> None:
@ -318,7 +362,8 @@ class ControlsGuide(ba.Actor):
# (otherwise it is confusing to see the touchscreen buttons right
# next to our display buttons)
touchscreen: ba.InputDevice | None = ba.internal.getinputdevice(
'TouchScreen', '#1', doraise=False)
'TouchScreen', '#1', doraise=False
)
if touchscreen is not None:
# We look at the session's players; not the activity's.
@ -335,10 +380,18 @@ class ControlsGuide(ba.Actor):
# Only count this one if it has non-empty button names
# (filters out wiimotes, the remote-app, etc).
for device in input_devices:
for name in ('buttonPunch', 'buttonJump', 'buttonBomb',
'buttonPickUp'):
if self._meaningful_button_name(
device, get_device_value(device, name)) != '':
for name in (
'buttonPunch',
'buttonJump',
'buttonBomb',
'buttonPickUp',
):
if (
self._meaningful_button_name(
device, get_device_value(device, name)
)
!= ''
):
fade_in = True
break
if fade_in:
@ -357,18 +410,20 @@ class ControlsGuide(ba.Actor):
# If we were given a lifespan, transition out after it.
if self._lifespan is not None:
ba.timer(self._lifespan,
ba.WeakCall(self.handlemessage, ba.DieMessage()))
ba.timer(
self._lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage())
)
self._update()
self._update_timer = ba.Timer(1.0,
ba.WeakCall(self._update),
repeat=True)
self._update_timer = ba.Timer(
1.0, ba.WeakCall(self._update), repeat=True
)
def _update(self) -> None:
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
from ba.internal import get_device_value, get_remote_app_name
if self._dead:
return
punch_button_names = set()
@ -389,11 +444,12 @@ class ControlsGuide(ba.Actor):
input_devices.append(kbd)
# We word things specially if we have nothing but keyboards.
all_keyboards = (input_devices
and all(i.name == 'Keyboard' for i in input_devices))
only_remote = (len(input_devices) == 1
and all(i.name == 'Amazon Fire TV Remote'
for i in input_devices))
all_keyboards = input_devices and all(
i.name == 'Keyboard' for i in input_devices
)
only_remote = len(input_devices) == 1 and all(
i.name == 'Amazon Fire TV Remote' for i in input_devices
)
right_button_names = set()
left_button_names = set()
@ -408,40 +464,57 @@ class ControlsGuide(ba.Actor):
if all_keyboards:
right_button_names.add(
device.get_button_name(
get_device_value(device, 'buttonRight')))
get_device_value(device, 'buttonRight')
)
)
left_button_names.add(
device.get_button_name(
get_device_value(device, 'buttonLeft')))
get_device_value(device, 'buttonLeft')
)
)
down_button_names.add(
device.get_button_name(
get_device_value(device, 'buttonDown')))
get_device_value(device, 'buttonDown')
)
)
up_button_names.add(
device.get_button_name(get_device_value(
device, 'buttonUp')))
device.get_button_name(get_device_value(device, 'buttonUp'))
)
# Ignore empty values; things like the remote app or
# wiimotes can return these.
bname = self._meaningful_button_name(
device, get_device_value(device, 'buttonPunch'))
device, get_device_value(device, 'buttonPunch')
)
if bname != '':
punch_button_names.add(bname)
bname = self._meaningful_button_name(
device, get_device_value(device, 'buttonJump'))
device, get_device_value(device, 'buttonJump')
)
if bname != '':
jump_button_names.add(bname)
bname = self._meaningful_button_name(
device, get_device_value(device, 'buttonBomb'))
device, get_device_value(device, 'buttonBomb')
)
if bname != '':
bomb_button_names.add(bname)
bname = self._meaningful_button_name(
device, get_device_value(device, 'buttonPickUp'))
device, get_device_value(device, 'buttonPickUp')
)
if bname != '':
pickup_button_names.add(bname)
# If we have no values yet, we may want to throw out some sane
# defaults.
if all(not lst for lst in (punch_button_names, jump_button_names,
bomb_button_names, pickup_button_names)):
if all(
not lst
for lst in (
punch_button_names,
jump_button_names,
bomb_button_names,
pickup_button_names,
)
):
# Otherwise on android show standard buttons.
if ba.app.platform == 'android':
punch_button_names.add('X')
@ -451,24 +524,42 @@ class ControlsGuide(ba.Actor):
run_text = ba.Lstr(
value='${R}: ${B}',
subs=[('${R}', ba.Lstr(resource='runText')),
('${B}',
ba.Lstr(resource='holdAnyKeyText'
if all_keyboards else 'holdAnyButtonText'))])
subs=[
('${R}', ba.Lstr(resource='runText')),
(
'${B}',
ba.Lstr(
resource='holdAnyKeyText'
if all_keyboards
else 'holdAnyButtonText'
),
),
],
)
# If we're all keyboards, lets show move keys too.
if (all_keyboards and len(up_button_names) == 1
and len(down_button_names) == 1 and len(left_button_names) == 1
and len(right_button_names) == 1):
if (
all_keyboards
and len(up_button_names) == 1
and len(down_button_names) == 1
and len(left_button_names) == 1
and len(right_button_names) == 1
):
up_text = list(up_button_names)[0]
down_text = list(down_button_names)[0]
left_text = list(left_button_names)[0]
right_text = list(right_button_names)[0]
run_text = ba.Lstr(value='${M}: ${U}, ${L}, ${D}, ${R}\n${RUN}',
subs=[('${M}', ba.Lstr(resource='moveText')),
('${U}', up_text), ('${L}', left_text),
('${D}', down_text), ('${R}', right_text),
('${RUN}', run_text)])
run_text = ba.Lstr(
value='${M}: ${U}, ${L}, ${D}, ${R}\n${RUN}',
subs=[
('${M}', ba.Lstr(resource='moveText')),
('${U}', up_text),
('${L}', left_text),
('${D}', down_text),
('${R}', right_text),
('${RUN}', run_text),
],
)
if self._force_hide_button_names:
jump_button_names.clear()
@ -479,9 +570,10 @@ class ControlsGuide(ba.Actor):
self._run_text.text = run_text
w_text: ba.Lstr | str
if only_remote and self._lifespan is None:
w_text = ba.Lstr(resource='fireTVRemoteWarningText',
subs=[('${REMOTE_APP_NAME}',
get_remote_app_name())])
w_text = ba.Lstr(
resource='fireTVRemoteWarningText',
subs=[('${REMOTE_APP_NAME}', get_remote_app_name())],
)
else:
w_text = ''
self._extra_text.text = w_text
@ -497,12 +589,16 @@ class ControlsGuide(ba.Actor):
self._jump_text.text = tval
if tval == '':
self._run_text.position = self._run_text_pos_top
self._extra_text.position = (self._run_text_pos_top[0],
self._run_text_pos_top[1] - 50)
self._extra_text.position = (
self._run_text_pos_top[0],
self._run_text_pos_top[1] - 50,
)
else:
self._run_text.position = self._run_text_pos_bottom
self._extra_text.position = (self._run_text_pos_bottom[0],
self._run_text_pos_bottom[1] - 50)
self._extra_text.position = (
self._run_text_pos_bottom[0],
self._run_text_pos_bottom[1] - 50,
)
if len(bomb_button_names) == 1:
self._bomb_text.text = list(bomb_button_names)[0]
else:

View File

@ -99,8 +99,10 @@ class FlagFactory:
'or',
('they_dont_have_material', shared.footing_material),
),
actions=(('modify_part_collision', 'collide', False),
('modify_part_collision', 'physical', False)),
actions=(
('modify_part_collision', 'collide', False),
('modify_part_collision', 'physical', False),
),
)
self.flag_texture = ba.gettexture('flagColor')
@ -164,12 +166,14 @@ class Flag(ba.Actor):
Can be stationary or carry-able by players.
"""
def __init__(self,
position: Sequence[float] = (0.0, 1.0, 0.0),
color: Sequence[float] = (1.0, 1.0, 1.0),
materials: Sequence[ba.Material] | None = None,
touchable: bool = True,
dropped_timeout: int | None = None):
def __init__(
self,
position: Sequence[float] = (0.0, 1.0, 0.0),
color: Sequence[float] = (1.0, 1.0, 1.0),
materials: Sequence[ba.Material] | None = None,
touchable: bool = True,
dropped_timeout: int | None = None,
):
"""Instantiate a flag.
If 'touchable' is False, the flag will only touch terrain;
@ -198,18 +202,20 @@ class Flag(ba.Actor):
if not touchable:
materials = [factory.no_hit_material] + materials
finalmaterials = ([shared.object_material, factory.flagmaterial] +
materials)
self.node = ba.newnode('flag',
attrs={
'position':
(position[0], position[1] + 0.75,
position[2]),
'color_texture': factory.flag_texture,
'color': color,
'materials': finalmaterials
},
delegate=self)
finalmaterials = [
shared.object_material,
factory.flagmaterial,
] + materials
self.node = ba.newnode(
'flag',
attrs={
'position': (position[0], position[1] + 0.75, position[2]),
'color_texture': factory.flag_texture,
'color': color,
'materials': finalmaterials,
},
delegate=self,
)
if dropped_timeout is not None:
dropped_timeout = int(dropped_timeout)
@ -217,19 +223,21 @@ class Flag(ba.Actor):
self._counter: ba.Node | None
if self._dropped_timeout is not None:
self._count = self._dropped_timeout
self._tick_timer = ba.Timer(1.0,
call=ba.WeakCall(self._tick),
repeat=True)
self._counter = ba.newnode('text',
owner=self.node,
attrs={
'in_world': True,
'color': (1, 1, 1, 0.7),
'scale': 0.015,
'shadow': 0.5,
'flatness': 1.0,
'h_align': 'center'
})
self._tick_timer = ba.Timer(
1.0, call=ba.WeakCall(self._tick), repeat=True
)
self._counter = ba.newnode(
'text',
owner=self.node,
attrs={
'in_world': True,
'color': (1, 1, 1, 0.7),
'scale': 0.015,
'shadow': 0.5,
'flatness': 1.0,
'h_align': 'center',
},
)
else:
self._counter = None
@ -248,9 +256,13 @@ class Flag(ba.Actor):
# until then.
if not self._has_moved:
nodepos = self.node.position
if (max(
if (
max(
abs(nodepos[i] - self._initial_position[i])
for i in list(range(3))) > 1.0):
for i in list(range(3))
)
> 1.0
):
self._has_moved = True
if self._held_count > 0 or not self._has_moved:
@ -263,8 +275,11 @@ class Flag(ba.Actor):
if self._count <= 10:
nodepos = self.node.position
assert self._counter
self._counter.position = (nodepos[0], nodepos[1] + 1.3,
nodepos[2])
self._counter.position = (
nodepos[0],
nodepos[1] + 1.3,
nodepos[2],
)
self._counter.text = str(self._count)
if self._count < 1:
self.handlemessage(ba.DieMessage())
@ -275,10 +290,9 @@ class Flag(ba.Actor):
def _hide_score_text(self) -> None:
assert self._score_text is not None
assert isinstance(self._score_text.scale, float)
ba.animate(self._score_text, 'scale', {
0: self._score_text.scale,
0.2: 0
})
ba.animate(
self._score_text, 'scale', {0: self._score_text.scale, 0.2: 0}
)
def set_score_text(self, text: str) -> None:
"""Show a message over the flag; handy for scores."""
@ -286,23 +300,24 @@ class Flag(ba.Actor):
return
if not self._score_text:
start_scale = 0.0
math = ba.newnode('math',
owner=self.node,
attrs={
'input1': (0, 1.4, 0),
'operation': 'add'
})
math = ba.newnode(
'math',
owner=self.node,
attrs={'input1': (0, 1.4, 0), 'operation': 'add'},
)
self.node.connectattr('position', math, 'input2')
self._score_text = ba.newnode('text',
owner=self.node,
attrs={
'text': text,
'in_world': True,
'scale': 0.02,
'shadow': 0.5,
'flatness': 1.0,
'h_align': 'center'
})
self._score_text = ba.newnode(
'text',
owner=self.node,
attrs={
'text': text,
'in_world': True,
'scale': 0.02,
'shadow': 0.5,
'flatness': 1.0,
'h_align': 'center',
},
)
math.connectattr('output', self._score_text, 'position')
else:
assert isinstance(self._score_text.scale, float)
@ -311,7 +326,8 @@ class Flag(ba.Actor):
self._score_text.color = ba.safecolor(self.node.color)
ba.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02})
self._score_text_hide_timer = ba.Timer(
1.0, ba.WeakCall(self._hide_score_text))
1.0, ba.WeakCall(self._hide_score_text)
)
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
@ -324,10 +340,21 @@ class Flag(ba.Actor):
assert self.node
assert msg.force_direction is not None
self.node.handlemessage(
'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0],
msg.velocity[1], msg.velocity[2], msg.magnitude,
msg.velocity_magnitude, msg.radius, 0, msg.force_direction[0],
msg.force_direction[1], msg.force_direction[2])
'impulse',
msg.pos[0],
msg.pos[1],
msg.pos[2],
msg.velocity[0],
msg.velocity[1],
msg.velocity[2],
msg.magnitude,
msg.velocity_magnitude,
msg.radius,
0,
msg.force_direction[0],
msg.force_direction[1],
msg.force_direction[2],
)
elif isinstance(msg, ba.PickedUpMessage):
self._held_count += 1
if self._held_count == 1 and self._counter is not None:

View File

@ -18,6 +18,7 @@ class Image(ba.Actor):
class Transition(Enum):
"""Transition types we support."""
FADE_IN = 'fade_in'
IN_RIGHT = 'in_right'
IN_LEFT = 'in_left'
@ -27,25 +28,28 @@ class Image(ba.Actor):
class Attach(Enum):
"""Attach types we support."""
CENTER = 'center'
TOP_CENTER = 'topCenter'
TOP_LEFT = 'topLeft'
BOTTOM_CENTER = 'bottomCenter'
def __init__(self,
texture: ba.Texture | dict[str, Any],
position: tuple[float, float] = (0, 0),
transition: Transition | None = None,
transition_delay: float = 0.0,
attach: Attach = Attach.CENTER,
color: Sequence[float] = (1.0, 1.0, 1.0, 1.0),
scale: tuple[float, float] = (100.0, 100.0),
transition_out_delay: float | None = None,
model_opaque: ba.Model | None = None,
model_transparent: ba.Model | None = None,
vr_depth: float = 0.0,
host_only: bool = False,
front: bool = False):
def __init__(
self,
texture: ba.Texture | dict[str, Any],
position: tuple[float, float] = (0, 0),
transition: Transition | None = None,
transition_delay: float = 0.0,
attach: Attach = Attach.CENTER,
color: Sequence[float] = (1.0, 1.0, 1.0, 1.0),
scale: tuple[float, float] = (100.0, 100.0),
transition_out_delay: float | None = None,
model_opaque: ba.Model | None = None,
model_transparent: ba.Model | None = None,
vr_depth: float = 0.0,
host_only: bool = False,
front: bool = False,
):
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
@ -66,22 +70,24 @@ class Image(ba.Actor):
tint_texture = None
mask_texture = None
self.node = ba.newnode('image',
attrs={
'texture': texture,
'tint_color': tint_color,
'tint_texture': tint_texture,
'position': position,
'vr_depth': vr_depth,
'scale': scale,
'mask_texture': mask_texture,
'color': color,
'absolute_scale': True,
'host_only': host_only,
'front': front,
'attach': attach.value
},
delegate=self)
self.node = ba.newnode(
'image',
attrs={
'texture': texture,
'tint_color': tint_color,
'tint_texture': tint_texture,
'position': position,
'vr_depth': vr_depth,
'scale': scale,
'mask_texture': mask_texture,
'color': color,
'absolute_scale': True,
'host_only': host_only,
'front': front,
'attach': attach.value,
},
delegate=self,
)
if model_opaque is not None:
self.node.model_opaque = model_opaque
@ -95,13 +101,13 @@ class Image(ba.Actor):
keys[transition_delay + transition_out_delay] = color[3]
keys[transition_delay + transition_out_delay + 0.5] = 0
ba.animate(self.node, 'opacity', keys)
cmb = self.position_combine = ba.newnode('combine',
owner=self.node,
attrs={'size': 2})
cmb = self.position_combine = ba.newnode(
'combine', owner=self.node, attrs={'size': 2}
)
if transition is self.Transition.IN_RIGHT:
keys = {
transition_delay: position[0] + 1200,
transition_delay + 0.2: position[0]
transition_delay + 0.2: position[0],
}
o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0}
ba.animate(cmb, 'input0', keys)
@ -110,32 +116,27 @@ class Image(ba.Actor):
elif transition is self.Transition.IN_LEFT:
keys = {
transition_delay: position[0] - 1200,
transition_delay + 0.2: position[0]
transition_delay + 0.2: position[0],
}
o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0}
if transition_out_delay is not None:
keys[transition_delay + transition_out_delay] = position[0]
keys[transition_delay + transition_out_delay +
200] = -position[0] - 1200
keys[transition_delay + transition_out_delay + 200] = (
-position[0] - 1200
)
o_keys[transition_delay + transition_out_delay + 0.15] = 1.0
o_keys[transition_delay + transition_out_delay + 0.2] = 0.0
ba.animate(cmb, 'input0', keys)
cmb.input1 = position[1]
ba.animate(self.node, 'opacity', o_keys)
elif transition is self.Transition.IN_BOTTOM_SLOW:
keys = {
transition_delay: -400,
transition_delay + 3.5: position[1]
}
keys = {transition_delay: -400, transition_delay + 3.5: position[1]}
o_keys = {transition_delay: 0.0, transition_delay + 2.0: 1.0}
cmb.input0 = position[0]
ba.animate(cmb, 'input1', keys)
ba.animate(self.node, 'opacity', o_keys)
elif transition is self.Transition.IN_BOTTOM:
keys = {
transition_delay: -400,
transition_delay + 0.2: position[1]
}
keys = {transition_delay: -400, transition_delay + 0.2: position[1]}
o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0}
if transition_out_delay is not None:
keys[transition_delay + transition_out_delay] = position[1]
@ -159,8 +160,10 @@ class Image(ba.Actor):
# If we're transitioning out, die at the end of it.
if transition_out_delay is not None:
ba.timer(transition_delay + transition_out_delay + 1.0,
ba.WeakCall(self.handlemessage, ba.DieMessage()))
ba.timer(
transition_delay + transition_out_delay + 1.0,
ba.WeakCall(self.handlemessage, ba.DieMessage()),
)
def handlemessage(self, msg: Any) -> Any:
assert not self.expired

View File

@ -20,32 +20,34 @@ class OnScreenCountdown(ba.Actor):
Useful for time-based games that count down to zero.
"""
def __init__(self,
duration: int,
endcall: Callable[[], Any] | None = None):
def __init__(self, duration: int, endcall: Callable[[], Any] | None = None):
"""Duration is provided in seconds."""
super().__init__()
self._timeremaining = duration
self._ended = False
self._endcall = endcall
self.node = ba.newnode('text',
attrs={
'v_attach': 'top',
'h_attach': 'center',
'h_align': 'center',
'color': (1, 1, 0.5, 1),
'flatness': 0.5,
'shadow': 0.5,
'position': (0, -70),
'scale': 1.4,
'text': ''
})
self.inputnode = ba.newnode('timedisplay',
attrs={
'time2': duration * 1000,
'timemax': duration * 1000,
'timemin': 0
})
self.node = ba.newnode(
'text',
attrs={
'v_attach': 'top',
'h_attach': 'center',
'h_align': 'center',
'color': (1, 1, 0.5, 1),
'flatness': 0.5,
'shadow': 0.5,
'position': (0, -70),
'scale': 1.4,
'text': '',
},
)
self.inputnode = ba.newnode(
'timedisplay',
attrs={
'time2': duration * 1000,
'timemax': duration * 1000,
'timemin': 0,
},
)
self.inputnode.connectattr('output', self.node, 'text')
self._countdownsounds = {
10: ba.getsound('announceTen'),
@ -57,7 +59,7 @@ class OnScreenCountdown(ba.Actor):
4: ba.getsound('announceFour'),
3: ba.getsound('announceThree'),
2: ba.getsound('announceTwo'),
1: ba.getsound('announceOne')
1: ba.getsound('announceOne'),
}
self._timer: ba.Timer | None = None
@ -65,8 +67,9 @@ class OnScreenCountdown(ba.Actor):
"""Start the timer."""
globalsnode = ba.getactivity().globalsnode
globalsnode.connectattr('time', self.inputnode, 'time1')
self.inputnode.time2 = (globalsnode.time +
(self._timeremaining + 1) * 1000)
self.inputnode.time2 = (
globalsnode.time + (self._timeremaining + 1) * 1000
)
self._timer = ba.Timer(1.0, self._update, repeat=True)
def on_expire(self) -> None:

View File

@ -22,23 +22,23 @@ class OnScreenTimer(ba.Actor):
def __init__(self) -> None:
super().__init__()
self._starttime_ms: int | None = None
self.node = ba.newnode('text',
attrs={
'v_attach': 'top',
'h_attach': 'center',
'h_align': 'center',
'color': (1, 1, 0.5, 1),
'flatness': 0.5,
'shadow': 0.5,
'position': (0, -70),
'scale': 1.4,
'text': ''
})
self.inputnode = ba.newnode('timedisplay',
attrs={
'timemin': 0,
'showsubseconds': True
})
self.node = ba.newnode(
'text',
attrs={
'v_attach': 'top',
'h_attach': 'center',
'h_align': 'center',
'color': (1, 1, 0.5, 1),
'flatness': 0.5,
'shadow': 0.5,
'position': (0, -70),
'scale': 1.4,
'text': '',
},
)
self.inputnode = ba.newnode(
'timedisplay', attrs={'timemin': 0, 'showsubseconds': True}
)
self.inputnode.connectattr('output', self.node, 'text')
def start(self) -> None:
@ -47,16 +47,19 @@ class OnScreenTimer(ba.Actor):
assert isinstance(tval, int)
self._starttime_ms = tval
self.inputnode.time1 = self._starttime_ms
ba.getactivity().globalsnode.connectattr('time', self.inputnode,
'time2')
ba.getactivity().globalsnode.connectattr(
'time', self.inputnode, 'time2'
)
def has_started(self) -> bool:
"""Return whether this timer has started yet."""
return self._starttime_ms is not None
def stop(self,
endtime: int | float | None = None,
timeformat: ba.TimeFormat = ba.TimeFormat.SECONDS) -> None:
def stop(
self,
endtime: int | float | None = None,
timeformat: ba.TimeFormat = ba.TimeFormat.SECONDS,
) -> None:
"""End the timer.
If 'endtime' is not None, it is used when calculating
@ -85,19 +88,19 @@ class OnScreenTimer(ba.Actor):
# Overloads so type checker knows our exact return type based in args.
@overload
def getstarttime(
self,
timeformat: Literal[ba.TimeFormat.SECONDS] = ba.TimeFormat.SECONDS
self, timeformat: Literal[ba.TimeFormat.SECONDS] = ba.TimeFormat.SECONDS
) -> float:
...
@overload
def getstarttime(self,
timeformat: Literal[ba.TimeFormat.MILLISECONDS]) -> int:
def getstarttime(
self, timeformat: Literal[ba.TimeFormat.MILLISECONDS]
) -> int:
...
def getstarttime(
self,
timeformat: ba.TimeFormat = ba.TimeFormat.SECONDS) -> int | float:
self, timeformat: ba.TimeFormat = ba.TimeFormat.SECONDS
) -> int | float:
"""Return the sim-time when start() was called.
Time will be returned in seconds if timeformat is SECONDS or

View File

@ -45,24 +45,28 @@ class PlayerSpaz(Spaz):
to the current ba.Activity.
"""
def __init__(self,
player: ba.Player,
color: Sequence[float] = (1.0, 1.0, 1.0),
highlight: Sequence[float] = (0.5, 0.5, 0.5),
character: str = 'Spaz',
powerups_expire: bool = True):
def __init__(
self,
player: ba.Player,
color: Sequence[float] = (1.0, 1.0, 1.0),
highlight: Sequence[float] = (0.5, 0.5, 0.5),
character: str = 'Spaz',
powerups_expire: bool = True,
):
"""Create a spaz for the provided ba.Player.
Note: this does not wire up any controls;
you must call connect_controls_to_player() to do so.
"""
super().__init__(color=color,
highlight=highlight,
character=character,
source_player=player,
start_invincible=True,
powerups_expire=powerups_expire)
super().__init__(
color=color,
highlight=highlight,
character=character,
source_player=player,
start_invincible=True,
powerups_expire=powerups_expire,
)
self.last_player_attacked_by: ba.Player | None = None
self.last_attacked_time = 0.0
self.last_attacked_type: tuple[str, str] | None = None
@ -74,19 +78,20 @@ class PlayerSpaz(Spaz):
# Overloads to tell the type system our return type based on doraise val.
@overload
def getplayer(self,
playertype: type[PlayerType],
doraise: Literal[False] = False) -> PlayerType | None:
def getplayer(
self, playertype: type[PlayerType], doraise: Literal[False] = False
) -> PlayerType | None:
...
@overload
def getplayer(self, playertype: type[PlayerType],
doraise: Literal[True]) -> PlayerType:
def getplayer(
self, playertype: type[PlayerType], doraise: Literal[True]
) -> PlayerType:
...
def getplayer(self,
playertype: type[PlayerType],
doraise: bool = False) -> PlayerType | None:
def getplayer(
self, playertype: type[PlayerType], doraise: bool = False
) -> PlayerType | None:
"""Get the ba.Player associated with this Spaz.
By default this will return None if the Player no longer exists.
@ -99,13 +104,15 @@ class PlayerSpaz(Spaz):
raise ba.PlayerNotFoundError()
return player if player.exists() else None
def connect_controls_to_player(self,
enable_jump: bool = True,
enable_punch: bool = True,
enable_pickup: bool = True,
enable_bomb: bool = True,
enable_run: bool = True,
enable_fly: bool = True) -> None:
def connect_controls_to_player(
self,
enable_jump: bool = True,
enable_punch: bool = True,
enable_pickup: bool = True,
enable_bomb: bool = True,
enable_run: bool = True,
enable_fly: bool = True,
) -> None:
"""Wire this spaz up to the provided ba.Player.
Full control of the character is given by default
@ -126,10 +133,12 @@ class PlayerSpaz(Spaz):
player.assigninput(ba.InputType.UP_DOWN, self.on_move_up_down)
player.assigninput(ba.InputType.LEFT_RIGHT, self.on_move_left_right)
player.assigninput(ba.InputType.HOLD_POSITION_PRESS,
self.on_hold_position_press)
player.assigninput(ba.InputType.HOLD_POSITION_RELEASE,
self.on_hold_position_release)
player.assigninput(
ba.InputType.HOLD_POSITION_PRESS, self.on_hold_position_press
)
player.assigninput(
ba.InputType.HOLD_POSITION_RELEASE, self.on_hold_position_release
)
intp = ba.InputType
if enable_jump:
player.assigninput(intp.JUMP_PRESS, self.on_jump_press)
@ -171,8 +180,10 @@ class PlayerSpaz(Spaz):
self.on_run(0.0)
self.on_fly_release()
else:
print('WARNING: disconnect_controls_from_player() called for'
' non-connected player')
print(
'WARNING: disconnect_controls_from_player() called for'
' non-connected player'
)
def handlemessage(self, msg: Any) -> Any:
# FIXME: Tidy this up.
@ -216,8 +227,9 @@ class PlayerSpaz(Spaz):
if not self._dead:
# Immediate-mode or left-game deaths don't count as 'kills'.
killed = (not msg.immediate
and msg.how is not ba.DeathType.LEFT_GAME)
killed = (
not msg.immediate and msg.how is not ba.DeathType.LEFT_GAME
)
activity = self._activity()
@ -237,13 +249,16 @@ class PlayerSpaz(Spaz):
# all bot kills would register as suicides; need to
# change this from last_player_attacked_by to
# something like last_actor_attacked_by to fix that.
if (self.last_player_attacked_by
and ba.time() - self.last_attacked_time < 4.0):
if (
self.last_player_attacked_by
and ba.time() - self.last_attacked_time < 4.0
):
killerplayer = self.last_player_attacked_by
else:
# ok, call it a suicide unless we're in co-op
if (activity is not None and not isinstance(
activity.session, ba.CoopSession)):
if activity is not None and not isinstance(
activity.session, ba.CoopSession
):
killerplayer = player
else:
killerplayer = None
@ -255,8 +270,10 @@ class PlayerSpaz(Spaz):
# Only report if both the player and the activity still exist.
if killed and activity is not None and player:
activity.handlemessage(
ba.PlayerDiedMessage(player, killed, killerplayer,
msg.how))
ba.PlayerDiedMessage(
player, killed, killerplayer, msg.how
)
)
super().handlemessage(msg) # Augment standard behavior.

View File

@ -19,13 +19,15 @@ class PopupText(ba.Actor):
category: Gameplay Classes
"""
def __init__(self,
text: str | ba.Lstr,
position: Sequence[float] = (0.0, 0.0, 0.0),
color: Sequence[float] = (1.0, 1.0, 1.0, 1.0),
random_offset: float = 0.5,
offset: Sequence[float] = (0.0, 0.0, 0.0),
scale: float = 1.0):
def __init__(
self,
text: str | ba.Lstr,
position: Sequence[float] = (0.0, 0.0, 0.0),
color: Sequence[float] = (1.0, 1.0, 1.0, 1.0),
random_offset: float = 0.5,
offset: Sequence[float] = (0.0, 0.0, 0.0),
scale: float = 1.0,
):
"""Instantiate with given values.
random_offset is the amount of random offset from the provided position
@ -35,73 +37,86 @@ class PopupText(ba.Actor):
super().__init__()
if len(color) == 3:
color = (color[0], color[1], color[2], 1.0)
pos = (position[0] + offset[0] + random_offset *
(0.5 - random.random()), position[1] + offset[1] +
random_offset * (0.5 - random.random()), position[2] +
offset[2] + random_offset * (0.5 - random.random()))
pos = (
position[0] + offset[0] + random_offset * (0.5 - random.random()),
position[1] + offset[1] + random_offset * (0.5 - random.random()),
position[2] + offset[2] + random_offset * (0.5 - random.random()),
)
self.node = ba.newnode('text',
attrs={
'text': text,
'in_world': True,
'shadow': 1.0,
'flatness': 1.0,
'h_align': 'center'
},
delegate=self)
self.node = ba.newnode(
'text',
attrs={
'text': text,
'in_world': True,
'shadow': 1.0,
'flatness': 1.0,
'h_align': 'center',
},
delegate=self,
)
lifespan = 1.5
# scale up
ba.animate(
self.node, 'scale', {
self.node,
'scale',
{
0: 0.0,
lifespan * 0.11: 0.020 * 0.7 * scale,
lifespan * 0.16: 0.013 * 0.7 * scale,
lifespan * 0.25: 0.014 * 0.7 * scale
})
lifespan * 0.25: 0.014 * 0.7 * scale,
},
)
# translate upward
self._tcombine = ba.newnode('combine',
owner=self.node,
attrs={
'input0': pos[0],
'input2': pos[2],
'size': 3
})
ba.animate(self._tcombine, 'input1', {
0: pos[1] + 1.5,
lifespan: pos[1] + 2.0
})
self._tcombine = ba.newnode(
'combine',
owner=self.node,
attrs={'input0': pos[0], 'input2': pos[2], 'size': 3},
)
ba.animate(
self._tcombine, 'input1', {0: pos[1] + 1.5, lifespan: pos[1] + 2.0}
)
self._tcombine.connectattr('output', self.node, 'position')
# fade our opacity in/out
self._combine = ba.newnode('combine',
owner=self.node,
attrs={
'input0': color[0],
'input1': color[1],
'input2': color[2],
'size': 4
})
self._combine = ba.newnode(
'combine',
owner=self.node,
attrs={
'input0': color[0],
'input1': color[1],
'input2': color[2],
'size': 4,
},
)
for i in range(4):
ba.animate(
self._combine, 'input' + str(i), {
self._combine,
'input' + str(i),
{
0.13 * lifespan: color[i],
0.18 * lifespan: 4.0 * color[i],
0.22 * lifespan: color[i]
})
ba.animate(self._combine, 'input3', {
0: 0,
0.1 * lifespan: color[3],
0.7 * lifespan: color[3],
lifespan: 0
})
0.22 * lifespan: color[i],
},
)
ba.animate(
self._combine,
'input3',
{
0: 0,
0.1 * lifespan: color[3],
0.7 * lifespan: color[3],
lifespan: 0,
},
)
self._combine.connectattr('output', self.node, 'color')
# kill ourself
self._die_timer = ba.Timer(
lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage()))
lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage())
)
def handlemessage(self, msg: Any) -> Any:
assert not self.expired

View File

@ -87,6 +87,7 @@ class PowerupBoxFactory:
to get a shared instance.
"""
from ba.internal import get_default_powerup_distribution
shared = SharedObjects.get()
self._lastpoweruptype: str | None = None
self.model = ba.getmodel('powerup')
@ -118,7 +119,8 @@ class PowerupBoxFactory:
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('message', 'our_node', 'at_connect', _TouchedMessage()),
))
),
)
# We don't wanna be picked up.
self.powerup_material.add_actions(
@ -136,9 +138,11 @@ class PowerupBoxFactory:
for _i in range(int(freq)):
self._powerupdist.append(powerup)
def get_random_powerup_type(self,
forcetype: str | None = None,
excludetypes: list[str] | None = None) -> str:
def get_random_powerup_type(
self,
forcetype: str | None = None,
excludetypes: list[str] | None = None,
) -> str:
"""Returns a random powerup type (string).
See ba.Powerup.poweruptype for available type values.
@ -161,9 +165,9 @@ class PowerupBoxFactory:
ptype = 'health'
else:
while True:
ptype = self._powerupdist[random.randint(
0,
len(self._powerupdist) - 1)]
ptype = self._powerupdist[
random.randint(0, len(self._powerupdist) - 1)
]
if ptype not in excludetypes:
break
self._lastpoweruptype = ptype
@ -199,10 +203,12 @@ class PowerupBox(ba.Actor):
node: ba.Node
"""The 'prop' ba.Node representing this box."""
def __init__(self,
position: Sequence[float] = (0.0, 1.0, 0.0),
poweruptype: str = 'triple_bombs',
expire: bool = True):
def __init__(
self,
position: Sequence[float] = (0.0, 1.0, 0.0),
poweruptype: str = 'triple_bombs',
expire: bool = True,
):
"""Create a powerup-box of the requested type at the given position.
see ba.Powerup.poweruptype for valid type strings.
@ -250,19 +256,23 @@ class PowerupBox(ba.Actor):
'color_texture': tex,
'reflection': 'powerup',
'reflection_scale': [1.0],
'materials': (factory.powerup_material,
shared.object_material)
}) # yapf: disable
'materials': (factory.powerup_material, shared.object_material),
},
) # yapf: disable
# Animate in.
curve = ba.animate(self.node, 'model_scale', {0: 0, 0.14: 1.6, 0.2: 1})
ba.timer(0.2, curve.delete)
if expire:
ba.timer(DEFAULT_POWERUP_INTERVAL - 2.5,
ba.WeakCall(self._start_flashing))
ba.timer(DEFAULT_POWERUP_INTERVAL - 1.0,
ba.WeakCall(self.handlemessage, ba.DieMessage()))
ba.timer(
DEFAULT_POWERUP_INTERVAL - 2.5,
ba.WeakCall(self._start_flashing),
)
ba.timer(
DEFAULT_POWERUP_INTERVAL - 1.0,
ba.WeakCall(self.handlemessage, ba.DieMessage()),
)
def _start_flashing(self) -> None:
if self.node:
@ -275,9 +285,9 @@ class PowerupBox(ba.Actor):
factory = PowerupBoxFactory.get()
assert self.node
if self.poweruptype == 'health':
ba.playsound(factory.health_powerup_sound,
3,
position=self.node.position)
ba.playsound(
factory.health_powerup_sound, 3, position=self.node.position
)
ba.playsound(factory.powerup_sound, 3, position=self.node.position)
self._powersgiven = True
self.handlemessage(ba.DieMessage())
@ -286,7 +296,8 @@ class PowerupBox(ba.Actor):
if not self._powersgiven:
node = ba.getcollision().opposingnode
node.handlemessage(
ba.PowerupMessage(self.poweruptype, sourcenode=self.node))
ba.PowerupMessage(self.poweruptype, sourcenode=self.node)
)
elif isinstance(msg, ba.DieMessage):
if self.node:

View File

@ -39,8 +39,11 @@ class RespawnIcon:
# Now find the first unused slot and use that.
index = 0
while (index in respawn_icons and respawn_icons[index]() is not None
and respawn_icons[index]().visible):
while (
index in respawn_icons
and respawn_icons[index]() is not None
and respawn_icons[index]().visible
):
index += 1
respawn_icons[index] = weakref.ref(self)
@ -50,66 +53,75 @@ class RespawnIcon:
h_offs = -10
ipos = (-40 - h_offs if on_right else 40 + h_offs, -180 + offs)
self._image: ba.NodeActor | None = ba.NodeActor(
ba.newnode('image',
attrs={
'texture': texture,
'tint_texture': icon['tint_texture'],
'tint_color': icon['tint_color'],
'tint2_color': icon['tint2_color'],
'mask_texture': mask_tex,
'position': ipos,
'scale': (32, 32),
'opacity': 1.0,
'absolute_scale': True,
'attach': 'topRight' if on_right else 'topLeft'
}))
ba.newnode(
'image',
attrs={
'texture': texture,
'tint_texture': icon['tint_texture'],
'tint_color': icon['tint_color'],
'tint2_color': icon['tint2_color'],
'mask_texture': mask_tex,
'position': ipos,
'scale': (32, 32),
'opacity': 1.0,
'absolute_scale': True,
'attach': 'topRight' if on_right else 'topLeft',
},
)
)
assert self._image.node
ba.animate(self._image.node, 'opacity', {0.0: 0, 0.2: 0.7})
npos = (-40 - h_offs if on_right else 40 + h_offs, -205 + 49 + offs)
self._name: ba.NodeActor | None = ba.NodeActor(
ba.newnode('text',
attrs={
'v_attach': 'top',
'h_attach': 'right' if on_right else 'left',
'text': ba.Lstr(value=player.getname()),
'maxwidth': 100,
'h_align': 'center',
'v_align': 'center',
'shadow': 1.0,
'flatness': 1.0,
'color': ba.safecolor(icon['tint_color']),
'scale': 0.5,
'position': npos
}))
ba.newnode(
'text',
attrs={
'v_attach': 'top',
'h_attach': 'right' if on_right else 'left',
'text': ba.Lstr(value=player.getname()),
'maxwidth': 100,
'h_align': 'center',
'v_align': 'center',
'shadow': 1.0,
'flatness': 1.0,
'color': ba.safecolor(icon['tint_color']),
'scale': 0.5,
'position': npos,
},
)
)
assert self._name.node
ba.animate(self._name.node, 'scale', {0: 0, 0.1: 0.5})
tpos = (-60 - h_offs if on_right else 60 + h_offs, -192 + offs)
self._text: ba.NodeActor | None = ba.NodeActor(
ba.newnode('text',
attrs={
'position': tpos,
'h_attach': 'right' if on_right else 'left',
'h_align': 'right' if on_right else 'left',
'scale': 0.9,
'shadow': 0.5,
'flatness': 0.5,
'v_attach': 'top',
'color': ba.safecolor(icon['tint_color']),
'text': ''
}))
ba.newnode(
'text',
attrs={
'position': tpos,
'h_attach': 'right' if on_right else 'left',
'h_align': 'right' if on_right else 'left',
'scale': 0.9,
'shadow': 0.5,
'flatness': 0.5,
'v_attach': 'top',
'color': ba.safecolor(icon['tint_color']),
'text': '',
},
)
)
assert self._text.node
ba.animate(self._text.node, 'scale', {0: 0, 0.1: 0.9})
self._respawn_time = ba.time() + respawn_time
self._update()
self._timer: ba.Timer | None = ba.Timer(1.0,
ba.WeakCall(self._update),
repeat=True)
self._timer: ba.Timer | None = ba.Timer(
1.0, ba.WeakCall(self._update), repeat=True
)
@property
def visible(self) -> bool:

View File

@ -14,9 +14,15 @@ if TYPE_CHECKING:
class _Entry:
def __init__(self, scoreboard: Scoreboard, team: ba.Team, do_cover: bool,
scale: float, label: ba.Lstr | None, flash_length: float):
def __init__(
self,
scoreboard: Scoreboard,
team: ba.Team,
do_cover: bool,
scale: float,
label: ba.Lstr | None,
flash_length: float,
):
# pylint: disable=too-many-statements
self._scoreboard = weakref.ref(scoreboard)
self._do_cover = do_cover
@ -45,84 +51,93 @@ class _Entry:
if vrmode:
self._backing_color = [0.1 + c * 0.1 for c in safe_team_color]
else:
self._backing_color = [
0.05 + c * 0.17 for c in safe_team_color
]
self._backing_color = [0.05 + c * 0.17 for c in safe_team_color]
else:
self._backing_color = [0.05 + c * 0.1 for c in safe_team_color]
opacity = (0.8 if vrmode else 0.8) if self._do_cover else 0.5
self._backing = ba.NodeActor(
ba.newnode('image',
attrs={
'scale': (self._width, self._height),
'opacity': opacity,
'color': self._backing_color,
'vr_depth': -3,
'attach': 'topLeft',
'texture': self._backing_tex
}))
ba.newnode(
'image',
attrs={
'scale': (self._width, self._height),
'opacity': opacity,
'color': self._backing_color,
'vr_depth': -3,
'attach': 'topLeft',
'texture': self._backing_tex,
},
)
)
self._barcolor = safe_team_color
self._bar = ba.NodeActor(
ba.newnode('image',
attrs={
'opacity': 0.7,
'color': self._barcolor,
'attach': 'topLeft',
'texture': self._bar_tex
}))
ba.newnode(
'image',
attrs={
'opacity': 0.7,
'color': self._barcolor,
'attach': 'topLeft',
'texture': self._bar_tex,
},
)
)
self._bar_scale = ba.newnode('combine',
owner=self._bar.node,
attrs={
'size': 2,
'input0': self._bar_width,
'input1': self._bar_height
})
self._bar_scale = ba.newnode(
'combine',
owner=self._bar.node,
attrs={
'size': 2,
'input0': self._bar_width,
'input1': self._bar_height,
},
)
assert self._bar.node
self._bar_scale.connectattr('output', self._bar.node, 'scale')
self._bar_position = ba.newnode('combine',
owner=self._bar.node,
attrs={
'size': 2,
'input0': 0,
'input1': 0
})
self._bar_position = ba.newnode(
'combine',
owner=self._bar.node,
attrs={'size': 2, 'input0': 0, 'input1': 0},
)
self._bar_position.connectattr('output', self._bar.node, 'position')
self._cover_color = safe_team_color
if self._do_cover:
self._cover = ba.NodeActor(
ba.newnode('image',
attrs={
'scale':
(self._width * 1.15, self._height * 1.6),
'opacity': 1.0,
'color': self._cover_color,
'vr_depth': 2,
'attach': 'topLeft',
'texture': self._cover_tex,
'model_transparent': self._model
}))
ba.newnode(
'image',
attrs={
'scale': (self._width * 1.15, self._height * 1.6),
'opacity': 1.0,
'color': self._cover_color,
'vr_depth': 2,
'attach': 'topLeft',
'texture': self._cover_tex,
'model_transparent': self._model,
},
)
)
clr = safe_team_color
maxwidth = 130.0 * (1.0 - scoreboard.score_split)
flatness = ((1.0 if vrmode else 0.5) if self._do_cover else 1.0)
flatness = (1.0 if vrmode else 0.5) if self._do_cover else 1.0
self._score_text = ba.NodeActor(
ba.newnode('text',
attrs={
'h_attach': 'left',
'v_attach': 'top',
'h_align': 'right',
'v_align': 'center',
'maxwidth': maxwidth,
'vr_depth': 2,
'scale': self._scale * 0.9,
'text': '',
'shadow': 1.0 if vrmode else 0.5,
'flatness': flatness,
'color': clr
}))
ba.newnode(
'text',
attrs={
'h_attach': 'left',
'v_attach': 'top',
'h_align': 'right',
'v_align': 'center',
'maxwidth': maxwidth,
'vr_depth': 2,
'scale': self._scale * 0.9,
'text': '',
'shadow': 1.0 if vrmode else 0.5,
'flatness': flatness,
'color': clr,
},
)
)
clr = safe_team_color
@ -148,28 +163,31 @@ class _Entry:
team_name_label = team_name_label[:10] + '...'
team_name_label = ba.Lstr(value=team_name_label)
flatness = ((1.0 if vrmode else 0.5) if self._do_cover else 1.0)
flatness = (1.0 if vrmode else 0.5) if self._do_cover else 1.0
self._name_text = ba.NodeActor(
ba.newnode('text',
attrs={
'h_attach': 'left',
'v_attach': 'top',
'h_align': 'left',
'v_align': 'center',
'vr_depth': 2,
'scale': self._scale * 0.9,
'shadow': 1.0 if vrmode else 0.5,
'flatness': flatness,
'maxwidth': 130 * scoreboard.score_split,
'text': team_name_label,
'color': clr + (1.0, )
}))
ba.newnode(
'text',
attrs={
'h_attach': 'left',
'v_attach': 'top',
'h_align': 'left',
'v_align': 'center',
'vr_depth': 2,
'scale': self._scale * 0.9,
'shadow': 1.0 if vrmode else 0.5,
'flatness': flatness,
'maxwidth': 130 * scoreboard.score_split,
'text': team_name_label,
'color': clr + (1.0,),
},
)
)
def flash(self, countdown: bool, extra_flash: bool) -> None:
"""Flash momentarily."""
self._flash_timer = ba.Timer(0.1,
ba.WeakCall(self._do_flash),
repeat=True)
self._flash_timer = ba.Timer(
0.1, ba.WeakCall(self._do_flash), repeat=True
)
if countdown:
self._flash_counter = 10
else:
@ -186,23 +204,28 @@ class _Entry:
return
self._pos = tuple(position)
self._backing.node.position = (position[0] + self._width / 2,
position[1] - self._height / 2)
self._backing.node.position = (
position[0] + self._width / 2,
position[1] - self._height / 2,
)
if self._do_cover:
assert self._cover.node
self._cover.node.position = (position[0] + self._width / 2,
position[1] - self._height / 2)
self._cover.node.position = (
position[0] + self._width / 2,
position[1] - self._height / 2,
)
self._bar_position.input0 = self._pos[0] + self._bar_width / 2
self._bar_position.input1 = self._pos[1] - self._bar_height / 2
assert self._score_text.node
self._score_text.node.position = (self._pos[0] + self._width -
7.0 * self._scale,
self._pos[1] - self._bar_height +
16.0 * self._scale)
self._score_text.node.position = (
self._pos[0] + self._width - 7.0 * self._scale,
self._pos[1] - self._bar_height + 16.0 * self._scale,
)
assert self._name_text.node
self._name_text.node.position = (self._pos[0] + 7.0 * self._scale,
self._pos[1] - self._bar_height +
16.0 * self._scale)
self._name_text.node.position = (
self._pos[0] + 7.0 * self._scale,
self._pos[1] - self._bar_height + 16.0 * self._scale,
)
def _set_flash_colors(self, flash: bool) -> None:
self._flash_colors = flash
@ -215,16 +238,29 @@ class _Entry:
scale = 2.0
_safesetcolor(
self._backing.node,
(self._backing_color[0] * scale, self._backing_color[1] *
scale, self._backing_color[2] * scale))
_safesetcolor(self._bar.node,
(self._barcolor[0] * scale, self._barcolor[1] *
scale, self._barcolor[2] * scale))
(
self._backing_color[0] * scale,
self._backing_color[1] * scale,
self._backing_color[2] * scale,
),
)
_safesetcolor(
self._bar.node,
(
self._barcolor[0] * scale,
self._barcolor[1] * scale,
self._barcolor[2] * scale,
),
)
if self._do_cover:
_safesetcolor(
self._cover.node,
(self._cover_color[0] * scale, self._cover_color[1] *
scale, self._cover_color[2] * scale))
(
self._cover_color[0] * scale,
self._cover_color[1] * scale,
self._cover_color[2] * scale,
),
)
else:
_safesetcolor(self._backing.node, self._backing_color)
_safesetcolor(self._bar.node, self._barcolor)
@ -239,12 +275,14 @@ class _Entry:
self._flash_counter -= 1
self._set_flash_colors(not self._flash_colors)
def set_value(self,
score: float,
max_score: float | None = None,
countdown: bool = False,
flash: bool = True,
show_value: bool = True) -> None:
def set_value(
self,
score: float,
max_score: float | None = None,
countdown: bool = False,
flash: bool = True,
show_value: bool = True,
) -> None:
"""Set the value for the scoreboard entry."""
# If we have no score yet, just set it.. otherwise compare
@ -253,8 +291,11 @@ class _Entry:
self._score = score
else:
if score > self._score or (countdown and score < self._score):
extra_flash = (max_score is not None and score >= max_score
and not countdown) or (countdown and score == 0)
extra_flash = (
max_score is not None
and score >= max_score
and not countdown
) or (countdown and score == 0)
if flash:
self.flash(countdown, extra_flash)
self._score = score
@ -265,25 +306,26 @@ class _Entry:
if countdown:
self._bar_width = max(
2.0 * self._scale,
self._width * (1.0 - (float(score) / max_score)))
self._width * (1.0 - (float(score) / max_score)),
)
else:
self._bar_width = max(
2.0 * self._scale,
self._width * (min(1.0,
float(score) / max_score)))
self._width * (min(1.0, float(score) / max_score)),
)
cur_width = self._bar_scale.input0
ba.animate(self._bar_scale, 'input0', {
0.0: cur_width,
0.25: self._bar_width
})
ba.animate(
self._bar_scale, 'input0', {0.0: cur_width, 0.25: self._bar_width}
)
self._bar_scale.input1 = self._bar_height
cur_x = self._bar_position.input0
assert self._pos is not None
ba.animate(self._bar_position, 'input0', {
0.0: cur_x,
0.25: self._pos[0] + self._bar_width / 2
})
ba.animate(
self._bar_position,
'input0',
{0.0: cur_x, 0.25: self._pos[0] + self._bar_width / 2},
)
self._bar_position.input1 = self._pos[1] - self._bar_height / 2
assert self._score_text.node
if show_value:
@ -353,13 +395,15 @@ class Scoreboard:
self._scale = 1.0
self._flash_length = 1.0
def set_team_value(self,
team: ba.Team,
score: float,
max_score: float | None = None,
countdown: bool = False,
flash: bool = True,
show_value: bool = True) -> None:
def set_team_value(
self,
team: ba.Team,
score: float,
max_score: float | None = None,
countdown: bool = False,
flash: bool = True,
show_value: bool = True,
) -> None:
"""Update the score-board display for the given ba.Team."""
if team.id not in self._entries:
self._add_team(team)
@ -370,21 +414,25 @@ class Scoreboard:
team.customdata[self._ENTRYSTORENAME] = _EntryProxy(self, team)
# Now set the entry.
self._entries[team.id].set_value(score=score,
max_score=max_score,
countdown=countdown,
flash=flash,
show_value=show_value)
self._entries[team.id].set_value(
score=score,
max_score=max_score,
countdown=countdown,
flash=flash,
show_value=show_value,
)
def _add_team(self, team: ba.Team) -> None:
if team.id in self._entries:
raise RuntimeError('Duplicate team add')
self._entries[team.id] = _Entry(self,
team,
do_cover=self._do_cover,
scale=self._scale,
label=self._label,
flash_length=self._flash_length)
self._entries[team.id] = _Entry(
self,
team,
do_cover=self._do_cover,
scale=self._scale,
label=self._label,
flash_length=self._flash_length,
)
self._update_teams()
def remove_team(self, team_id: int) -> None:

View File

@ -38,10 +38,10 @@ class Spawner:
"""The spawn position."""
def __init__(
self,
spawner: Spawner,
data: Any,
pt: Sequence[float], # pylint: disable=invalid-name
self,
spawner: Spawner,
data: Any,
pt: Sequence[float], # pylint: disable=invalid-name
):
"""Instantiate with the given values."""
self.spawner = spawner
@ -49,12 +49,13 @@ class Spawner:
self.pt = pt # pylint: disable=invalid-name
def __init__(
self,
data: Any = None,
pt: Sequence[float] = (0, 0, 0), # pylint: disable=invalid-name
spawn_time: float = 1.0,
send_spawn_message: bool = True,
spawn_callback: Callable[[], Any] | None = None):
self,
data: Any = None,
pt: Sequence[float] = (0, 0, 0), # pylint: disable=invalid-name
spawn_time: float = 1.0,
send_spawn_message: bool = True,
spawn_callback: Callable[[], Any] | None = None,
):
"""Instantiate a Spawner.
Requires some custom data, a position,
@ -66,19 +67,23 @@ class Spawner:
self._data = data
self._pt = pt
# create a light where the spawn will happen
self._light = ba.newnode('light',
attrs={
'position': tuple(pt),
'radius': 0.1,
'color': (1.0, 0.1, 0.1),
'lights_volumes': False
})
self._light = ba.newnode(
'light',
attrs={
'position': tuple(pt),
'radius': 0.1,
'color': (1.0, 0.1, 0.1),
'lights_volumes': False,
},
)
scl = float(spawn_time) / 3.75
min_val = 0.4
max_val = 0.7
ba.playsound(self._spawner_sound, position=self._light.position)
ba.animate(
self._light, 'intensity', {
self._light,
'intensity',
{
0.0: 0.0,
0.25 * scl: max_val,
0.500 * scl: min_val,
@ -95,8 +100,9 @@ class Spawner:
3.250 * scl: 1.5 * max_val,
3.500 * scl: min_val,
3.750 * scl: 2.0,
4.000 * scl: 0.0
})
4.000 * scl: 0.0,
},
)
ba.timer(spawn_time, self._spawn)
def _spawn(self) -> None:
@ -108,4 +114,5 @@ class Spawner:
activity = ba.getactivity()
if activity is not None:
activity.handlemessage(
self.SpawnMessage(self, self._data, self._pt))
self.SpawnMessage(self, self._data, self._pt)
)

File diff suppressed because it is too large Load Diff

View File

@ -89,8 +89,9 @@ class Appearance:
def __init__(self, name: str):
self.name = name
if self.name in ba.app.spaz_appearances:
raise Exception('spaz appearance name "' + self.name +
'" already exists.')
raise Exception(
'spaz appearance name "' + self.name + '" already exists.'
)
ba.app.spaz_appearances[self.name] = self
self.color_texture = ''
self.color_mask_texture = ''
@ -141,10 +142,16 @@ def register_appearances() -> None:
t.toes_model = 'neoSpazToes'
t.jump_sounds = ['spazJump01', 'spazJump02', 'spazJump03', 'spazJump04']
t.attack_sounds = [
'spazAttack01', 'spazAttack02', 'spazAttack03', 'spazAttack04'
'spazAttack01',
'spazAttack02',
'spazAttack03',
'spazAttack04',
]
t.impact_sounds = [
'spazImpact01', 'spazImpact02', 'spazImpact03', 'spazImpact04'
'spazImpact01',
'spazImpact02',
'spazImpact03',
'spazImpact04',
]
t.death_sounds = ['spazDeath01']
t.pickup_sounds = ['spazPickup01']
@ -170,10 +177,16 @@ def register_appearances() -> None:
t.toes_model = 'zoeToes'
t.jump_sounds = ['zoeJump01', 'zoeJump02', 'zoeJump03']
t.attack_sounds = [
'zoeAttack01', 'zoeAttack02', 'zoeAttack03', 'zoeAttack04'
'zoeAttack01',
'zoeAttack02',
'zoeAttack03',
'zoeAttack04',
]
t.impact_sounds = [
'zoeImpact01', 'zoeImpact02', 'zoeImpact03', 'zoeImpact04'
'zoeImpact01',
'zoeImpact02',
'zoeImpact03',
'zoeImpact04',
]
t.death_sounds = ['zoeDeath01']
t.pickup_sounds = ['zoePickup01']
@ -226,8 +239,16 @@ def register_appearances() -> None:
t.lower_leg_model = 'kronkLowerLeg'
t.toes_model = 'kronkToes'
kronk_sounds = [
'kronk1', 'kronk2', 'kronk3', 'kronk4', 'kronk5', 'kronk6', 'kronk7',
'kronk8', 'kronk9', 'kronk10'
'kronk1',
'kronk2',
'kronk3',
'kronk4',
'kronk5',
'kronk6',
'kronk7',
'kronk8',
'kronk9',
'kronk10',
]
t.jump_sounds = kronk_sounds
t.attack_sounds = kronk_sounds
@ -255,8 +276,16 @@ def register_appearances() -> None:
t.lower_leg_model = 'melLowerLeg'
t.toes_model = 'melToes'
mel_sounds = [
'mel01', 'mel02', 'mel03', 'mel04', 'mel05', 'mel06', 'mel07', 'mel08',
'mel09', 'mel10'
'mel01',
'mel02',
'mel03',
'mel04',
'mel05',
'mel06',
'mel07',
'mel08',
'mel09',
'mel10',
]
t.attack_sounds = mel_sounds
t.jump_sounds = mel_sounds
@ -284,8 +313,13 @@ def register_appearances() -> None:
t.lower_leg_model = 'jackLowerLeg'
t.toes_model = 'jackToes'
hit_sounds = [
'jackHit01', 'jackHit02', 'jackHit03', 'jackHit04', 'jackHit05',
'jackHit06', 'jackHit07'
'jackHit01',
'jackHit02',
'jackHit03',
'jackHit04',
'jackHit05',
'jackHit06',
'jackHit07',
]
sounds = ['jack01', 'jack02', 'jack03', 'jack04', 'jack05', 'jack06']
t.attack_sounds = sounds
@ -340,9 +374,7 @@ def register_appearances() -> None:
t.upper_leg_model = 'frostyUpperLeg'
t.lower_leg_model = 'frostyLowerLeg'
t.toes_model = 'frostyToes'
frosty_sounds = [
'frosty01', 'frosty02', 'frosty03', 'frosty04', 'frosty05'
]
frosty_sounds = ['frosty01', 'frosty02', 'frosty03', 'frosty04', 'frosty05']
frosty_hit_sounds = ['frostyHit01', 'frostyHit02', 'frostyHit03']
t.attack_sounds = frosty_sounds
t.jump_sounds = frosty_sounds
@ -558,7 +590,10 @@ def register_appearances() -> None:
t.lower_leg_model = 'actionHeroLowerLeg'
t.toes_model = 'actionHeroToes'
action_hero_sounds = [
'actionHero1', 'actionHero2', 'actionHero3', 'actionHero4'
'actionHero1',
'actionHero2',
'actionHero3',
'actionHero4',
]
action_hero_hit_sounds = ['actionHeroHit1', 'actionHeroHit2']
t.attack_sounds = action_hero_sounds
@ -857,7 +892,10 @@ def register_appearances() -> None:
t.lower_leg_model = 'operaSingerLowerLeg'
t.toes_model = 'operaSingerToes'
opera_singer_sounds = [
'operaSinger1', 'operaSinger2', 'operaSinger3', 'operaSinger4'
'operaSinger1',
'operaSinger2',
'operaSinger3',
'operaSinger4',
]
opera_singer_hit_sounds = ['operaSingerHit1', 'operaSingerHit2']
t.attack_sounds = opera_singer_sounds

View File

@ -57,8 +57,12 @@ class SpazBotDiedMessage:
how: ba.DeathType
"""The particular type of death."""
def __init__(self, spazbot: SpazBot, killerplayer: ba.Player | None,
how: ba.DeathType):
def __init__(
self,
spazbot: SpazBot,
killerplayer: ba.Player | None,
how: ba.DeathType,
):
"""Instantiate with given values."""
self.spazbot = spazbot
self.killerplayer = killerplayer
@ -105,12 +109,14 @@ class SpazBot(Spaz):
def __init__(self) -> None:
"""Instantiate a spaz-bot."""
super().__init__(color=self.color,
highlight=self.highlight,
character=self.character,
source_player=None,
start_invincible=False,
can_accept_powerups=False)
super().__init__(
color=self.color,
highlight=self.highlight,
character=self.character,
source_player=None,
start_invincible=False,
can_accept_powerups=False,
)
# If you need to add custom behavior to a bot, set this to a callable
# which takes one arg (the bot) and returns False if the bot's normal
@ -126,8 +132,9 @@ class SpazBot(Spaz):
self.held_count = 0
self.last_player_held_by: ba.Player | None = None
self.target_flag: Flag | None = None
self._charge_speed = 0.5 * (self.charge_speed_min +
self.charge_speed_max)
self._charge_speed = 0.5 * (
self.charge_speed_min + self.charge_speed_max
)
self._lead_amount = 0.5
self._mode = 'wait'
self._charge_closing_in = False
@ -172,16 +179,19 @@ class SpazBot(Spaz):
# Ignore player-points that are significantly below the bot
# (keeps bots from following players off cliffs).
if (closest_dist is None
or dist < closest_dist) and (plpt[1] > botpt[1] - 5.0):
if (closest_dist is None or dist < closest_dist) and (
plpt[1] > botpt[1] - 5.0
):
closest_dist = dist
closest_vel = plvel
closest = plpt
if closest_dist is not None:
assert closest_vel is not None
assert closest is not None
return (ba.Vec3(closest[0], closest[1], closest[2]),
ba.Vec3(closest_vel[0], closest_vel[1], closest_vel[2]))
return (
ba.Vec3(closest[0], closest[1], closest[2]),
ba.Vec3(closest_vel[0], closest_vel[1], closest_vel[2]),
)
return None, None
def set_player_points(self, pts: list[tuple[ba.Vec3, ba.Vec3]]) -> None:
@ -212,7 +222,7 @@ class SpazBot(Spaz):
# towards the flag and try to pick it up.
if self.target_flag:
if self.node.hold_node:
holding_flag = (self.node.hold_node.getnodetype() == 'flag')
holding_flag = self.node.hold_node.getnodetype() == 'flag'
else:
holding_flag = False
@ -225,7 +235,7 @@ class SpazBot(Spaz):
# Otherwise try to go pick it up.
elif self.target_flag.node:
target_pt_raw = ba.Vec3(*self.target_flag.node.position)
diff = (target_pt_raw - our_pos)
diff = target_pt_raw - our_pos
diff = ba.Vec3(diff[0], 0, diff[2]) # Don't care about y.
dist = diff.length()
to_target = diff.normalized()
@ -253,8 +263,7 @@ class SpazBot(Spaz):
# Not a flag-bearer. If we're holding anything but a bomb, drop it.
if self.node.hold_node:
holding_bomb = (self.node.hold_node.getnodetype()
in ['bomb', 'prop'])
holding_bomb = self.node.hold_node.getnodetype() in ['bomb', 'prop']
if not holding_bomb:
self.node.pickup_pressed = True
self.node.pickup_pressed = False
@ -287,10 +296,11 @@ class SpazBot(Spaz):
# Use a point out in front of them as real target.
# (more out in front the farther from us they are)
target_pt = (target_pt_raw +
target_vel * dist_raw * 0.3 * self._lead_amount)
target_pt = (
target_pt_raw + target_vel * dist_raw * 0.3 * self._lead_amount
)
diff = (target_pt - our_pos)
diff = target_pt - our_pos
dist = diff.length()
to_target = diff.normalized()
@ -349,8 +359,9 @@ class SpazBot(Spaz):
elif self._mode == 'charge':
if random.random() < 0.3:
self._charge_speed = random.uniform(self.charge_speed_min,
self.charge_speed_max)
self._charge_speed = random.uniform(
self.charge_speed_min, self.charge_speed_max
)
# If we're a runner we run during charges *except when near
# an edge (otherwise we tend to fly off easily).
@ -396,22 +407,29 @@ class SpazBot(Spaz):
# from our target. When this value increases it means our charge
# is over (ran by them or something).
if self._mode == 'charge':
if (self._charge_closing_in
and self._last_charge_dist < dist < 3.0):
if (
self._charge_closing_in
and self._last_charge_dist < dist < 3.0
):
self._charge_closing_in = False
self._last_charge_dist = dist
# If we have a clean shot, throw!
if (self.throw_dist_min <= dist < self.throw_dist_max
and random.random() < self.throwiness and can_attack):
if (
self.throw_dist_min <= dist < self.throw_dist_max
and random.random() < self.throwiness
and can_attack
):
self._mode = 'throw'
self._lead_amount = ((0.4 + random.random() * 0.6)
if dist_raw > 4.0 else
(0.1 + random.random() * 0.4))
self._lead_amount = (
(0.4 + random.random() * 0.6)
if dist_raw > 4.0
else (0.1 + random.random() * 0.4)
)
self._have_dropped_throw_bomb = False
self._throw_release_time = (ba.time() +
(1.0 / self.throw_rate) *
(0.8 + 1.3 * random.random()))
self._throw_release_time = ba.time() + (
1.0 / self.throw_rate
) * (0.8 + 1.3 * random.random())
# If we're static, always charge (which for us means barely move).
elif self.static:
@ -433,8 +451,11 @@ class SpazBot(Spaz):
# We're within charging distance, backed against an edge,
# or farther than our max throw distance.. chaaarge!
elif (dist < self.charge_dist_max or dist > self.throw_dist_max
or self.map.is_point_near_edge(our_pos, self._running)):
elif (
dist < self.charge_dist_max
or dist > self.throw_dist_max
or self.map.is_point_near_edge(our_pos, self._running)
):
if self._mode != 'charge':
self._mode = 'charge'
self._lead_amount = 0.01
@ -450,10 +471,15 @@ class SpazBot(Spaz):
# Do some awesome jumps if we're running.
# FIXME: pylint: disable=too-many-boolean-expressions
if ((self._running and 1.2 < dist < 2.2
and ba.time() - self._last_jump_time > 1.0)
or (self.bouncy and ba.time() - self._last_jump_time > 0.4
and random.random() < 0.5)):
if (
self._running
and 1.2 < dist < 2.2
and ba.time() - self._last_jump_time > 1.0
) or (
self.bouncy
and ba.time() - self._last_jump_time > 0.4
and random.random() < 0.5
):
self._last_jump_time = ba.time()
self.node.jump_pressed = True
self.node.jump_pressed = False
@ -526,8 +552,10 @@ class SpazBot(Spaz):
# If they were attacked by someone in the last few
# seconds that person's the killer.
# Otherwise it was a suicide.
if (self.last_player_attacked_by
and ba.time() - self.last_attacked_time < 4.0):
if (
self.last_player_attacked_by
and ba.time() - self.last_attacked_time < 4.0
):
killerplayer = self.last_player_attacked_by
else:
killerplayer = None
@ -538,7 +566,8 @@ class SpazBot(Spaz):
killerplayer = None
if activity is not None:
activity.handlemessage(
SpazBotDiedMessage(self, killerplayer, msg.how))
SpazBotDiedMessage(self, killerplayer, msg.how)
)
super().handlemessage(msg) # Augment standard behavior.
# Keep track of the player who last hit us for point rewarding.
@ -558,6 +587,7 @@ class BomberBot(SpazBot):
category: Bot Classes
"""
character = 'Spaz'
punchiness = 0.3
@ -567,6 +597,7 @@ class BomberBotLite(BomberBot):
category: Bot Classes
"""
color = LITE_BOT_COLOR
highlight = LITE_BOT_HIGHLIGHT
punchiness = 0.2
@ -581,6 +612,7 @@ class BomberBotStaticLite(BomberBotLite):
category: Bot Classes
"""
static = True
throw_dist_min = 0.0
@ -590,6 +622,7 @@ class BomberBotStatic(BomberBot):
category: Bot Classes
"""
static = True
throw_dist_min = 0.0
@ -599,6 +632,7 @@ class BomberBotPro(BomberBot):
category: Bot Classes
"""
points_mult = 2
color = PRO_BOT_COLOR
highlight = PRO_BOT_HIGHLIGHT
@ -615,6 +649,7 @@ class BomberBotProShielded(BomberBotPro):
category: Bot Classes
"""
points_mult = 3
default_shields = True
@ -624,6 +659,7 @@ class BomberBotProStatic(BomberBotPro):
category: Bot Classes
"""
static = True
throw_dist_min = 0.0
@ -633,6 +669,7 @@ class BomberBotProStaticShielded(BomberBotProShielded):
category: Bot Classes
"""
static = True
throw_dist_min = 0.0
@ -642,6 +679,7 @@ class BrawlerBot(SpazBot):
category: Bot Classes
"""
character = 'Kronk'
punchiness = 0.9
charge_dist_max = 9999.0
@ -656,6 +694,7 @@ class BrawlerBotLite(BrawlerBot):
category: Bot Classes
"""
color = LITE_BOT_COLOR
highlight = LITE_BOT_HIGHLIGHT
punchiness = 0.3
@ -668,6 +707,7 @@ class BrawlerBotPro(BrawlerBot):
category: Bot Classes
"""
color = PRO_BOT_COLOR
highlight = PRO_BOT_HIGHLIGHT
run = True
@ -682,6 +722,7 @@ class BrawlerBotProShielded(BrawlerBotPro):
category: Bot Classes
"""
default_shields = True
points_mult = 3
@ -731,6 +772,7 @@ class ChargerBotPro(ChargerBot):
category: Bot Classes
"""
color = PRO_BOT_COLOR
highlight = PRO_BOT_HIGHLIGHT
default_shields = True
@ -743,6 +785,7 @@ class ChargerBotProShielded(ChargerBotPro):
category: Bot Classes
"""
default_shields = True
points_mult = 4
@ -752,6 +795,7 @@ class TriggerBot(SpazBot):
category: Bot Classes
"""
character = 'Zoe'
punchiness = 0.75
throwiness = 0.7
@ -769,6 +813,7 @@ class TriggerBotStatic(TriggerBot):
category: Bot Classes
"""
static = True
throw_dist_min = 0.0
@ -778,6 +823,7 @@ class TriggerBotPro(TriggerBot):
category: Bot Classes
"""
color = PRO_BOT_COLOR
highlight = PRO_BOT_HIGHLIGHT
default_bomb_count = 3
@ -796,6 +842,7 @@ class TriggerBotProShielded(TriggerBotPro):
category: Bot Classes
"""
default_shields = True
points_mult = 4
@ -805,6 +852,7 @@ class StickyBot(SpazBot):
category: Bot Classes
"""
character = 'Mel'
punchiness = 0.9
throwiness = 1.0
@ -826,6 +874,7 @@ class StickyBotStatic(StickyBot):
category: Bot Classes
"""
static = True
@ -834,6 +883,7 @@ class ExplodeyBot(SpazBot):
category: Bot Classes
"""
character = 'Jack Morgan'
run = True
charge_dist_min = 0.0
@ -851,6 +901,7 @@ class ExplodeyBotNoTimeLimit(ExplodeyBot):
category: Bot Classes
"""
curse_time = None
@ -859,6 +910,7 @@ class ExplodeyBotShielded(ExplodeyBot):
category: Bot Classes
"""
default_shields = True
points_mult = 5
@ -889,22 +941,31 @@ class SpazBotSet:
self.clear()
def spawn_bot(
self,
bot_type: type[SpazBot],
pos: Sequence[float],
spawn_time: float = 3.0,
on_spawn_call: Callable[[SpazBot], Any] | None = None) -> None:
self,
bot_type: type[SpazBot],
pos: Sequence[float],
spawn_time: float = 3.0,
on_spawn_call: Callable[[SpazBot], Any] | None = None,
) -> None:
"""Spawn a bot from this set."""
from bastd.actor import spawner
spawner.Spawner(pt=pos,
spawn_time=spawn_time,
send_spawn_message=False,
spawn_callback=ba.Call(self._spawn_bot, bot_type, pos,
on_spawn_call))
spawner.Spawner(
pt=pos,
spawn_time=spawn_time,
send_spawn_message=False,
spawn_callback=ba.Call(
self._spawn_bot, bot_type, pos, on_spawn_call
),
)
self._spawning_count += 1
def _spawn_bot(self, bot_type: type[SpazBot], pos: Sequence[float],
on_spawn_call: Callable[[SpazBot], Any] | None) -> None:
def _spawn_bot(
self,
bot_type: type[SpazBot],
pos: Sequence[float],
on_spawn_call: Callable[[SpazBot], Any] | None,
) -> None:
spaz = bot_type()
ba.playsound(self._spawn_sound, position=pos)
assert spaz.node
@ -918,8 +979,9 @@ class SpazBotSet:
def have_living_bots(self) -> bool:
"""Return whether any bots in the set are alive or spawning."""
return (self._spawning_count > 0
or any(any(b.is_alive() for b in l) for l in self._bot_lists))
return self._spawning_count > 0 or any(
any(b.is_alive() for b in l) for l in self._bot_lists
)
def get_living_bots(self) -> list[SpazBot]:
"""Get the living bots in the set."""
@ -935,15 +997,18 @@ class SpazBotSet:
# Update one of our bot lists each time through.
# First off, remove no-longer-existing bots from the list.
try:
bot_list = self._bot_lists[self._bot_update_list] = ([
bot_list = self._bot_lists[self._bot_update_list] = [
b for b in self._bot_lists[self._bot_update_list] if b
])
]
except Exception:
bot_list = []
ba.print_exception('Error updating bot list: ' +
str(self._bot_lists[self._bot_update_list]))
self._bot_update_list = (self._bot_update_list +
1) % self._bot_list_count
ba.print_exception(
'Error updating bot list: '
+ str(self._bot_lists[self._bot_update_list])
)
self._bot_update_list = (
self._bot_update_list + 1
) % self._bot_list_count
# Update our list of player points for the bots to use.
player_pts = []
@ -956,8 +1021,12 @@ class SpazBotSet:
if player.is_alive():
assert isinstance(player.actor, Spaz)
assert player.actor.node
player_pts.append((ba.Vec3(player.actor.node.position),
ba.Vec3(player.actor.node.velocity)))
player_pts.append(
(
ba.Vec3(player.actor.node.position),
ba.Vec3(player.actor.node.velocity),
)
)
except Exception:
ba.print_exception('Error on bot-set _update.')
@ -980,9 +1049,9 @@ class SpazBotSet:
def start_moving(self) -> None:
"""Start processing bot AI updates so they start doing their thing."""
self._bot_update_timer = ba.Timer(0.05,
ba.WeakCall(self._update),
repeat=True)
self._bot_update_timer = ba.Timer(
0.05, ba.WeakCall(self._update), repeat=True
)
def stop_moving(self) -> None:
"""Tell all bots to stop moving and stops updating their AI.
@ -1022,20 +1091,28 @@ class SpazBotSet:
assert bot.node # (should exist if 'if bot' was True)
bot.node.move_left_right = 0
bot.node.move_up_down = 0
ba.timer(0.5 * random.random(),
ba.Call(bot.handlemessage, ba.CelebrateMessage()))
ba.timer(
0.5 * random.random(),
ba.Call(bot.handlemessage, ba.CelebrateMessage()),
)
jump_duration = random.randrange(400, 500)
j = random.randrange(0, 200)
for _i in range(10):
bot.node.jump_pressed = True
bot.node.jump_pressed = False
j += jump_duration
ba.timer(random.uniform(0.0, 1.0),
ba.Call(bot.node.handlemessage, 'attack_sound'))
ba.timer(random.uniform(1.0, 2.0),
ba.Call(bot.node.handlemessage, 'attack_sound'))
ba.timer(random.uniform(2.0, 3.0),
ba.Call(bot.node.handlemessage, 'attack_sound'))
ba.timer(
random.uniform(0.0, 1.0),
ba.Call(bot.node.handlemessage, 'attack_sound'),
)
ba.timer(
random.uniform(1.0, 2.0),
ba.Call(bot.node.handlemessage, 'attack_sound'),
)
ba.timer(
random.uniform(2.0, 3.0),
ba.Call(bot.node.handlemessage, 'attack_sound'),
)
def add_bot(self, bot: SpazBot) -> None:
"""Add a ba.SpazBot instance to the set."""

View File

@ -89,22 +89,33 @@ class SpazFactory:
# pylint: disable=cyclic-import
# FIXME: should probably put these somewhere common so we don't
# have to import them from a module that imports us.
from bastd.actor.spaz import (PickupMessage, PunchHitMessage,
CurseExplodeMessage)
from bastd.actor.spaz import (
PickupMessage,
PunchHitMessage,
CurseExplodeMessage,
)
shared = SharedObjects.get()
self.impact_sounds_medium = (ba.getsound('impactMedium'),
ba.getsound('impactMedium2'))
self.impact_sounds_hard = (ba.getsound('impactHard'),
ba.getsound('impactHard2'),
ba.getsound('impactHard3'))
self.impact_sounds_harder = (ba.getsound('bigImpact'),
ba.getsound('bigImpact2'))
self.impact_sounds_medium = (
ba.getsound('impactMedium'),
ba.getsound('impactMedium2'),
)
self.impact_sounds_hard = (
ba.getsound('impactHard'),
ba.getsound('impactHard2'),
ba.getsound('impactHard3'),
)
self.impact_sounds_harder = (
ba.getsound('bigImpact'),
ba.getsound('bigImpact2'),
)
self.single_player_death_sound = ba.getsound('playerDeath')
self.punch_sound_weak = ba.getsound('punchWeak01')
self.punch_sound = ba.getsound('punch01')
self.punch_sound_strong = (ba.getsound('punchStrong01'),
ba.getsound('punchStrong02'))
self.punch_sound_strong = (
ba.getsound('punchStrong01'),
ba.getsound('punchStrong02'),
)
self.punch_sound_stronger = ba.getsound('superPunch')
self.swish_sound = ba.getsound('punchSwish')
self.block_sound = ba.getsound('block')
@ -126,47 +137,64 @@ class SpazFactory:
# Eww; this probably should just be built into the spaz node.
self.roller_material.add_actions(
conditions=('they_have_material', footing_material),
actions=(('message', 'our_node', 'at_connect', 'footing', 1),
('message', 'our_node', 'at_disconnect', 'footing', -1)))
actions=(
('message', 'our_node', 'at_connect', 'footing', 1),
('message', 'our_node', 'at_disconnect', 'footing', -1),
),
)
self.spaz_material.add_actions(
conditions=('they_have_material', footing_material),
actions=(('message', 'our_node', 'at_connect', 'footing', 1),
('message', 'our_node', 'at_disconnect', 'footing', -1)))
actions=(
('message', 'our_node', 'at_connect', 'footing', 1),
('message', 'our_node', 'at_disconnect', 'footing', -1),
),
)
# Punches.
self.punch_material.add_actions(
conditions=('they_are_different_node_than_us', ),
conditions=('they_are_different_node_than_us',),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('message', 'our_node', 'at_connect', PunchHitMessage()),
))
),
)
# Pickups.
self.pickup_material.add_actions(
conditions=(('they_are_different_node_than_us', ), 'and',
('they_have_material', object_material)),
conditions=(
('they_are_different_node_than_us',),
'and',
('they_have_material', object_material),
),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('message', 'our_node', 'at_connect', PickupMessage()),
))
),
)
# Curse.
self.curse_material.add_actions(
conditions=(
('they_are_different_node_than_us', ),
('they_are_different_node_than_us',),
'and',
('they_have_material', player_material),
),
actions=('message', 'our_node', 'at_connect',
CurseExplodeMessage()),
actions=(
'message',
'our_node',
'at_connect',
CurseExplodeMessage(),
),
)
self.foot_impact_sounds = (ba.getsound('footImpact01'),
ba.getsound('footImpact02'),
ba.getsound('footImpact03'))
self.foot_impact_sounds = (
ba.getsound('footImpact01'),
ba.getsound('footImpact02'),
ba.getsound('footImpact03'),
)
self.foot_skid_sound = ba.getsound('skid01')
self.foot_roll_sound = ba.getsound('scamper01')
@ -177,7 +205,8 @@ class SpazFactory:
('impact_sound', self.foot_impact_sounds, 1, 0.2),
('skid_sound', self.foot_skid_sound, 20, 0.3),
('roll_sound', self.foot_roll_sound, 20, 3.0),
))
),
)
self.skid_sound = ba.getsound('gravelSkid')
@ -187,7 +216,8 @@ class SpazFactory:
('impact_sound', self.foot_impact_sounds, 20, 6),
('skid_sound', self.skid_sound, 2.0, 1),
('roll_sound', self.skid_sound, 2.0, 1),
))
),
)
self.shield_up_sound = ba.getsound('shieldUp')
self.shield_down_sound = ba.getsound('shieldDown')
@ -200,7 +230,7 @@ class SpazFactory:
(
('we_are_younger_than', 51),
'and',
('they_are_different_node_than_us', ),
('they_are_different_node_than_us',),
),
'and',
('they_dont_have_material', region_material),
@ -213,17 +243,23 @@ class SpazFactory:
# Lets load some basic rules.
# (allows them to be tweaked from the master server)
self.shield_decay_rate = ba.internal.get_v1_account_misc_read_val(
'rsdr', 10.0)
'rsdr', 10.0
)
self.punch_cooldown = ba.internal.get_v1_account_misc_read_val(
'rpc', 400)
self.punch_cooldown_gloves = (ba.internal.get_v1_account_misc_read_val(
'rpcg', 300))
'rpc', 400
)
self.punch_cooldown_gloves = ba.internal.get_v1_account_misc_read_val(
'rpcg', 300
)
self.punch_power_scale = ba.internal.get_v1_account_misc_read_val(
'rpp', 1.2)
'rpp', 1.2
)
self.punch_power_scale_gloves = (
ba.internal.get_v1_account_misc_read_val('rppg', 1.4))
ba.internal.get_v1_account_misc_read_val('rppg', 1.4)
)
self.max_shield_spillover_damage = (
ba.internal.get_v1_account_misc_read_val('rsms', 500))
ba.internal.get_v1_account_misc_read_val('rsms', 500)
)
def get_style(self, character: str) -> str:
"""Return the named style for this character.
@ -253,7 +289,7 @@ class SpazFactory:
'hand_model': ba.getmodel(char.hand_model),
'upper_leg_model': ba.getmodel(char.upper_leg_model),
'lower_leg_model': ba.getmodel(char.lower_leg_model),
'toes_model': ba.getmodel(char.toes_model)
'toes_model': ba.getmodel(char.toes_model),
}
else:
media = self.spaz_media[character]

View File

@ -18,6 +18,7 @@ class Text(ba.Actor):
class Transition(Enum):
"""Transition types for text."""
FADE_IN = 'fade_in'
IN_RIGHT = 'in_right'
IN_LEFT = 'in_left'
@ -27,46 +28,52 @@ class Text(ba.Actor):
class HAlign(Enum):
"""Horizontal alignment type."""
LEFT = 'left'
CENTER = 'center'
RIGHT = 'right'
class VAlign(Enum):
"""Vertical alignment type."""
NONE = 'none'
CENTER = 'center'
class HAttach(Enum):
"""Horizontal attach type."""
LEFT = 'left'
CENTER = 'center'
RIGHT = 'right'
class VAttach(Enum):
"""Vertical attach type."""
BOTTOM = 'bottom'
CENTER = 'center'
TOP = 'top'
def __init__(self,
text: str | ba.Lstr,
position: tuple[float, float] = (0.0, 0.0),
h_align: HAlign = HAlign.LEFT,
v_align: VAlign = VAlign.NONE,
color: Sequence[float] = (1.0, 1.0, 1.0, 1.0),
transition: Transition | None = None,
transition_delay: float = 0.0,
flash: bool = False,
v_attach: VAttach = VAttach.CENTER,
h_attach: HAttach = HAttach.CENTER,
scale: float = 1.0,
transition_out_delay: float | None = None,
maxwidth: float | None = None,
shadow: float = 0.5,
flatness: float = 0.0,
vr_depth: float = 0.0,
host_only: bool = False,
front: bool = False):
def __init__(
self,
text: str | ba.Lstr,
position: tuple[float, float] = (0.0, 0.0),
h_align: HAlign = HAlign.LEFT,
v_align: VAlign = VAlign.NONE,
color: Sequence[float] = (1.0, 1.0, 1.0, 1.0),
transition: Transition | None = None,
transition_delay: float = 0.0,
flash: bool = False,
v_attach: VAttach = VAttach.CENTER,
h_attach: HAttach = HAttach.CENTER,
scale: float = 1.0,
transition_out_delay: float | None = None,
maxwidth: float | None = None,
shadow: float = 0.5,
flatness: float = 0.0,
vr_depth: float = 0.0,
host_only: bool = False,
front: bool = False,
):
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
@ -88,21 +95,25 @@ class Text(ba.Actor):
'maxwidth': 0.0 if maxwidth is None else maxwidth,
'host_only': host_only,
'front': front,
'scale': scale
})
'scale': scale,
},
)
if transition is self.Transition.FADE_IN:
if flash:
raise Exception('fixme: flash and fade-in'
' currently cant both be on')
cmb = ba.newnode('combine',
owner=self.node,
attrs={
'input0': color[0],
'input1': color[1],
'input2': color[2],
'size': 4
})
raise RuntimeError(
'fixme: flash and fade-in currently cant both be on'
)
cmb = ba.newnode(
'combine',
owner=self.node,
attrs={
'input0': color[0],
'input1': color[1],
'input2': color[2],
'size': 4,
},
)
keys = {transition_delay: 0.0, transition_delay + 0.5: color[3]}
if transition_out_delay is not None:
keys[transition_delay + transition_out_delay] = color[3]
@ -115,38 +126,35 @@ class Text(ba.Actor):
tm1 = 0.15
tm2 = 0.3
cmb = ba.newnode('combine', owner=self.node, attrs={'size': 4})
ba.animate(cmb,
'input0', {
0.0: color[0] * mult,
tm1: color[0],
tm2: color[0] * mult
},
loop=True)
ba.animate(cmb,
'input1', {
0.0: color[1] * mult,
tm1: color[1],
tm2: color[1] * mult
},
loop=True)
ba.animate(cmb,
'input2', {
0.0: color[2] * mult,
tm1: color[2],
tm2: color[2] * mult
},
loop=True)
ba.animate(
cmb,
'input0',
{0.0: color[0] * mult, tm1: color[0], tm2: color[0] * mult},
loop=True,
)
ba.animate(
cmb,
'input1',
{0.0: color[1] * mult, tm1: color[1], tm2: color[1] * mult},
loop=True,
)
ba.animate(
cmb,
'input2',
{0.0: color[2] * mult, tm1: color[2], tm2: color[2] * mult},
loop=True,
)
cmb.input3 = color[3]
cmb.connectattr('output', self.node, 'color')
cmb = self.position_combine = ba.newnode('combine',
owner=self.node,
attrs={'size': 2})
cmb = self.position_combine = ba.newnode(
'combine', owner=self.node, attrs={'size': 2}
)
if transition is self.Transition.IN_RIGHT:
keys = {
transition_delay: position[0] + 1300,
transition_delay + 0.2: position[0]
transition_delay + 0.2: position[0],
}
o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0}
ba.animate(cmb, 'input0', keys)
@ -155,13 +163,14 @@ class Text(ba.Actor):
elif transition is self.Transition.IN_LEFT:
keys = {
transition_delay: position[0] - 1300,
transition_delay + 0.2: position[0]
transition_delay + 0.2: position[0],
}
o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0}
if transition_out_delay is not None:
keys[transition_delay + transition_out_delay] = position[0]
keys[transition_delay + transition_out_delay +
0.2] = position[0] - 1300.0
keys[transition_delay + transition_out_delay + 0.2] = (
position[0] - 1300.0
)
o_keys[transition_delay + transition_out_delay + 0.15] = 1.0
o_keys[transition_delay + transition_out_delay + 0.2] = 0.0
ba.animate(cmb, 'input0', keys)
@ -170,7 +179,7 @@ class Text(ba.Actor):
elif transition is self.Transition.IN_BOTTOM_SLOW:
keys = {
transition_delay: -100.0,
transition_delay + 1.0: position[1]
transition_delay + 1.0: position[1],
}
o_keys = {transition_delay: 0.0, transition_delay + 0.2: 1.0}
cmb.input0 = position[0]
@ -179,7 +188,7 @@ class Text(ba.Actor):
elif transition is self.Transition.IN_BOTTOM:
keys = {
transition_delay: -100.0,
transition_delay + 0.2: position[1]
transition_delay + 0.2: position[1],
}
o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0}
if transition_out_delay is not None:
@ -193,7 +202,7 @@ class Text(ba.Actor):
elif transition is self.Transition.IN_TOP_SLOW:
keys = {
transition_delay: 400.0,
transition_delay + 3.5: position[1]
transition_delay + 3.5: position[1],
}
o_keys = {transition_delay: 0, transition_delay + 1.0: 1.0}
cmb.input0 = position[0]
@ -207,8 +216,10 @@ class Text(ba.Actor):
# If we're transitioning out, die at the end of it.
if transition_out_delay is not None:
ba.timer(transition_delay + transition_out_delay + 1.0,
ba.WeakCall(self.handlemessage, ba.DieMessage()))
ba.timer(
transition_delay + transition_out_delay + 1.0,
ba.WeakCall(self.handlemessage, ba.DieMessage()),
)
def handlemessage(self, msg: Any) -> Any:
assert not self.expired

View File

@ -20,44 +20,47 @@ class TipsText(ba.Actor):
self._tip_scale = 0.8
self._tip_title_scale = 1.1
self._offs_y = offs_y
self.node = ba.newnode('text',
delegate=self,
attrs={
'text': '',
'scale': self._tip_scale,
'h_align': 'left',
'maxwidth': 800,
'vr_depth': -20,
'v_align': 'center',
'v_attach': 'bottom'
})
tval = ba.Lstr(value='${A}:',
subs=[('${A}', ba.Lstr(resource='tipText'))])
self.title_node = ba.newnode('text',
delegate=self,
attrs={
'text': tval,
'scale': self._tip_title_scale,
'maxwidth': 122,
'h_align': 'right',
'vr_depth': -20,
'v_align': 'center',
'v_attach': 'bottom'
})
self.node = ba.newnode(
'text',
delegate=self,
attrs={
'text': '',
'scale': self._tip_scale,
'h_align': 'left',
'maxwidth': 800,
'vr_depth': -20,
'v_align': 'center',
'v_attach': 'bottom',
},
)
tval = ba.Lstr(
value='${A}:', subs=[('${A}', ba.Lstr(resource='tipText'))]
)
self.title_node = ba.newnode(
'text',
delegate=self,
attrs={
'text': tval,
'scale': self._tip_title_scale,
'maxwidth': 122,
'h_align': 'right',
'vr_depth': -20,
'v_align': 'center',
'v_attach': 'bottom',
},
)
self._message_duration = 10000
self._message_spacing = 3000
self._change_timer = ba.Timer(
0.001 * (self._message_duration + self._message_spacing),
ba.WeakCall(self.change_phrase),
repeat=True)
self._combine = ba.newnode('combine',
owner=self.node,
attrs={
'input0': 1.0,
'input1': 0.8,
'input2': 1.0,
'size': 4
})
repeat=True,
)
self._combine = ba.newnode(
'combine',
owner=self.node,
attrs={'input0': 1.0, 'input1': 0.8, 'input2': 1.0, 'size': 4},
)
self._combine.connectattr('output', self.node, 'color')
self._combine.connectattr('output', self.title_node, 'color')
self.change_phrase()
@ -65,9 +68,11 @@ class TipsText(ba.Actor):
def change_phrase(self) -> None:
"""Switch the visible tip phrase."""
from ba.internal import get_remote_app_name, get_next_tip
next_tip = ba.Lstr(translate=('tips', get_next_tip()),
subs=[('${REMOTE_APP_NAME}', get_remote_app_name())
])
next_tip = ba.Lstr(
translate=('tips', get_next_tip()),
subs=[('${REMOTE_APP_NAME}', get_remote_app_name())],
)
spc = self._message_spacing
assert self.node
self.node.position = (-200, self._offs_y)
@ -76,12 +81,14 @@ class TipsText(ba.Actor):
spc: 0,
spc + 1000: 1.0,
spc + self._message_duration - 1000: 1.0,
spc + self._message_duration: 0.0
spc + self._message_duration: 0.0,
}
ba.animate(self._combine,
'input3', {k: v * 0.5
for k, v in list(keys.items())},
timeformat=ba.TimeFormat.MILLISECONDS)
ba.animate(
self._combine,
'input3',
{k: v * 0.5 for k, v in list(keys.items())},
timeformat=ba.TimeFormat.MILLISECONDS,
)
self.node.text = next_tip
def handlemessage(self, msg: Any) -> Any:

View File

@ -21,22 +21,24 @@ class ZoomText(ba.Actor):
Used for things such as the 'BOB WINS' victory messages.
"""
def __init__(self,
text: str | ba.Lstr,
position: tuple[float, float] = (0.0, 0.0),
shiftposition: tuple[float, float] | None = None,
shiftdelay: float | None = None,
lifespan: float | None = None,
flash: bool = True,
trail: bool = True,
h_align: str = 'center',
color: Sequence[float] = (0.9, 0.4, 0.0),
jitter: float = 0.0,
trailcolor: Sequence[float] = (1.0, 0.35, 0.1, 0.0),
scale: float = 1.0,
project_scale: float = 1.0,
tilt_translate: float = 0.0,
maxwidth: float | None = None):
def __init__(
self,
text: str | ba.Lstr,
position: tuple[float, float] = (0.0, 0.0),
shiftposition: tuple[float, float] | None = None,
shiftdelay: float | None = None,
lifespan: float | None = None,
flash: bool = True,
trail: bool = True,
h_align: str = 'center',
color: Sequence[float] = (0.9, 0.4, 0.0),
jitter: float = 0.0,
trailcolor: Sequence[float] = (1.0, 0.35, 0.1, 0.0),
scale: float = 1.0,
project_scale: float = 1.0,
tilt_translate: float = 0.0,
maxwidth: float | None = None,
):
# pylint: disable=too-many-locals
super().__init__()
self._dying = False
@ -61,8 +63,9 @@ class ZoomText(ba.Actor):
'maxwidth': maxwidth if maxwidth is not None else 0.0,
'tilt_translate': tilt_translate,
'h_align': h_align,
'v_align': 'center'
})
'v_align': 'center',
},
)
# we never jitter in vr mode..
if ba.app.vr_mode:
@ -78,79 +81,81 @@ class ZoomText(ba.Actor):
positionadjusted2 = (shiftposition[0], shiftposition[1] - 100)
ba.timer(
shiftdelay,
ba.WeakCall(self._shift, positionadjusted, positionadjusted2))
ba.WeakCall(self._shift, positionadjusted, positionadjusted2),
)
if jitter > 0.0:
ba.timer(
shiftdelay + 0.25,
ba.WeakCall(self._jitter, positionadjusted2,
jitter * scale))
color_combine = ba.newnode('combine',
owner=self.node,
attrs={
'input2': color[2],
'input3': 1.0,
'size': 4
})
ba.WeakCall(
self._jitter, positionadjusted2, jitter * scale
),
)
color_combine = ba.newnode(
'combine',
owner=self.node,
attrs={'input2': color[2], 'input3': 1.0, 'size': 4},
)
if trail:
trailcolor_n = ba.newnode('combine',
owner=self.node,
attrs={
'size': 3,
'input0': trailcolor[0],
'input1': trailcolor[1],
'input2': trailcolor[2]
})
trailcolor_n = ba.newnode(
'combine',
owner=self.node,
attrs={
'size': 3,
'input0': trailcolor[0],
'input1': trailcolor[1],
'input2': trailcolor[2],
},
)
trailcolor_n.connectattr('output', self.node, 'trailcolor')
basemult = 0.85
ba.animate(
self.node, 'trail_project_scale', {
self.node,
'trail_project_scale',
{
0: 0 * project_scale,
basemult * 0.201: 0.6 * project_scale,
basemult * 0.347: 0.8 * project_scale,
basemult * 0.478: 0.9 * project_scale,
basemult * 0.595: 0.93 * project_scale,
basemult * 0.748: 0.95 * project_scale,
basemult * 0.941: 0.95 * project_scale
})
basemult * 0.941: 0.95 * project_scale,
},
)
if flash:
mult = 2.0
tm1 = 0.15
tm2 = 0.3
ba.animate(color_combine,
'input0', {
0: color[0] * mult,
tm1: color[0],
tm2: color[0] * mult
},
loop=True)
ba.animate(color_combine,
'input1', {
0: color[1] * mult,
tm1: color[1],
tm2: color[1] * mult
},
loop=True)
ba.animate(color_combine,
'input2', {
0: color[2] * mult,
tm1: color[2],
tm2: color[2] * mult
},
loop=True)
ba.animate(
color_combine,
'input0',
{0: color[0] * mult, tm1: color[0], tm2: color[0] * mult},
loop=True,
)
ba.animate(
color_combine,
'input1',
{0: color[1] * mult, tm1: color[1], tm2: color[1] * mult},
loop=True,
)
ba.animate(
color_combine,
'input2',
{0: color[2] * mult, tm1: color[2], tm2: color[2] * mult},
loop=True,
)
else:
color_combine.input0 = color[0]
color_combine.input1 = color[1]
color_combine.connectattr('output', self.node, 'color')
ba.animate(self.node, 'project_scale', {
0: 0,
0.27: 1.05 * project_scale,
0.3: 1 * project_scale
})
ba.animate(
self.node,
'project_scale',
{0: 0, 0.27: 1.05 * project_scale, 0.3: 1 * project_scale},
)
# if they give us a lifespan, kill ourself down the line
if lifespan is not None:
ba.timer(lifespan, ba.WeakCall(self.handlemessage,
ba.DieMessage()))
ba.timer(lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage()))
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
@ -161,18 +166,22 @@ class ZoomText(ba.Actor):
self.node.delete()
else:
ba.animate(
self.node, 'project_scale', {
self.node,
'project_scale',
{
0.0: 1 * self._project_scale,
0.6: 1.2 * self._project_scale
})
0.6: 1.2 * self._project_scale,
},
)
ba.animate(self.node, 'opacity', {0.0: 1, 0.3: 0})
ba.animate(self.node, 'trail_opacity', {0.0: 1, 0.6: 0})
ba.timer(0.7, self.node.delete)
return None
return super().handlemessage(msg)
def _jitter(self, position: tuple[float, float],
jitter_amount: float) -> None:
def _jitter(
self, position: tuple[float, float], jitter_amount: float
) -> None:
if not self.node:
return
cmb = ba.newnode('combine', owner=self.node, attrs={'size': 2})
@ -181,14 +190,17 @@ class ZoomText(ba.Actor):
timeval = 0.0
# gen some random keys for that stop-motion-y look
for _i in range(10):
keys[timeval] = (position[index] +
(random.random() - 0.5) * jitter_amount * 1.6)
keys[timeval] = (
position[index]
+ (random.random() - 0.5) * jitter_amount * 1.6
)
timeval += random.random() * 0.1
ba.animate(cmb, attr, keys, loop=True)
cmb.connectattr('output', self.node, 'position')
def _shift(self, position1: tuple[float, float],
position2: tuple[float, float]) -> None:
def _shift(
self, position1: tuple[float, float], position2: tuple[float, float]
) -> None:
if not self.node:
return
cmb = ba.newnode('combine', owner=self.node, attrs={'size': 2})

View File

@ -15,17 +15,23 @@ class AppDelegate(ba.AppDelegate):
"""Defines handlers for high level app functionality."""
def create_default_game_settings_ui(
self, gameclass: type[ba.GameActivity],
sessiontype: type[ba.Session], settings: dict | None,
completion_call: Callable[[dict | None], Any]) -> None:
self,
gameclass: type[ba.GameActivity],
sessiontype: type[ba.Session],
settings: dict | None,
completion_call: Callable[[dict | None], Any],
) -> None:
"""(internal)"""
# Replace the main window once we come up successfully.
from bastd.ui.playlist.editgame import PlaylistEditGameWindow
ba.app.ui.clear_main_menu_window(transition='out_left')
ba.app.ui.set_main_menu_window(
PlaylistEditGameWindow(
gameclass,
sessiontype,
settings,
completion_call=completion_call).get_root_widget())
completion_call=completion_call,
).get_root_widget()
)

View File

@ -91,8 +91,9 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]):
# Base class overrides
self.slow_motion = self._epic_mode
self.default_music = (ba.MusicType.EPIC if self._epic_mode else
ba.MusicType.FORWARD_MARCH)
self.default_music = (
ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FORWARD_MARCH
)
def get_instance_description(self) -> str | Sequence:
if self._score_to_win == 1:
@ -107,19 +108,19 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]):
def create_team(self, sessionteam: ba.SessionTeam) -> Team:
shared = SharedObjects.get()
base_pos = self.map.get_flag_position(sessionteam.id)
ba.newnode('light',
attrs={
'position': base_pos,
'intensity': 0.6,
'height_attenuated': False,
'volume_intensity_scale': 0.1,
'radius': 0.1,
'color': sessionteam.color
})
ba.newnode(
'light',
attrs={
'position': base_pos,
'intensity': 0.6,
'height_attenuated': False,
'volume_intensity_scale': 0.1,
'radius': 0.1,
'color': sessionteam.color,
},
)
Flag.project_stand(base_pos)
flag = Flag(touchable=False,
position=base_pos,
color=sessionteam.color)
flag = Flag(touchable=False, position=base_pos, color=sessionteam.color)
team = Team(base_pos=base_pos, flag=flag)
mat = self._base_region_materials[sessionteam.id] = ba.Material()
@ -128,8 +129,11 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]):
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('call', 'at_connect', ba.Call(self._handle_base_collide,
team)),
(
'call',
'at_connect',
ba.Call(self._handle_base_collide, team),
),
),
)
@ -140,8 +144,9 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]):
'position': (base_pos[0], base_pos[1] + 0.75, base_pos[2]),
'scale': (0.5, 0.5, 0.5),
'type': 'sphere',
'materials': [self._base_region_materials[sessionteam.id]]
})
'materials': [self._base_region_materials[sessionteam.id]],
},
)
return team
@ -163,13 +168,15 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]):
super().handlemessage(msg)
def _flash_base(self, team: Team, length: float = 2.0) -> None:
light = ba.newnode('light',
attrs={
'position': team.base_pos,
'height_attenuated': False,
'radius': 0.3,
'color': team.color
})
light = ba.newnode(
'light',
attrs={
'position': team.base_pos,
'height_attenuated': False,
'radius': 0.3,
'color': team.color,
},
)
ba.animate(light, 'intensity', {0: 0, 0.25: 2.0, 0.5: 0}, loop=True)
ba.timer(length, light.delete)
@ -203,38 +210,34 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]):
for player in player_team.players:
if player.is_alive():
pos = player.node.position
light = ba.newnode('light',
attrs={
'position': pos,
'color': player_team.color,
'height_attenuated': False,
'radius': 0.4
})
light = ba.newnode(
'light',
attrs={
'position': pos,
'color': player_team.color,
'height_attenuated': False,
'radius': 0.4,
},
)
ba.timer(0.5, light.delete)
ba.animate(light, 'intensity', {
0: 0,
0.1: 1.0,
0.5: 0
})
ba.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0})
new_pos = (self.map.get_start_position(player_team.id))
light = ba.newnode('light',
attrs={
'position': new_pos,
'color': player_team.color,
'radius': 0.4,
'height_attenuated': False
})
new_pos = self.map.get_start_position(player_team.id)
light = ba.newnode(
'light',
attrs={
'position': new_pos,
'color': player_team.color,
'radius': 0.4,
'height_attenuated': False,
},
)
ba.timer(0.5, light.delete)
ba.animate(light, 'intensity', {
0: 0,
0.1: 1.0,
0.5: 0
})
ba.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0})
if player.actor:
player.actor.handlemessage(
ba.StandMessage(new_pos,
random.uniform(0, 360)))
ba.StandMessage(new_pos, random.uniform(0, 360))
)
# Have teammates celebrate.
for player in player_team.players:
@ -254,5 +257,6 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]):
def _update_scoreboard(self) -> None:
for team in self.teams:
self._scoreboard.set_team_value(team, team.score,
self._score_to_win)
self._scoreboard.set_team_value(
team, team.score, self._score_to_win
)

View File

@ -12,8 +12,13 @@ from typing import TYPE_CHECKING
import ba
from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.scoreboard import Scoreboard
from bastd.actor.flag import (FlagFactory, Flag, FlagPickedUpMessage,
FlagDroppedMessage, FlagDiedMessage)
from bastd.actor.flag import (
FlagFactory,
Flag,
FlagPickedUpMessage,
FlagDroppedMessage,
FlagDiedMessage,
)
if TYPE_CHECKING:
from typing import Any, Sequence
@ -26,18 +31,18 @@ class CTFFlag(Flag):
def __init__(self, team: Team):
assert team.flagmaterial is not None
super().__init__(materials=[team.flagmaterial],
position=team.base_pos,
color=team.color)
super().__init__(
materials=[team.flagmaterial],
position=team.base_pos,
color=team.color,
)
self._team = team
self.held_count = 0
self.counter = ba.newnode('text',
owner=self.node,
attrs={
'in_world': True,
'scale': 0.02,
'h_align': 'center'
})
self.counter = ba.newnode(
'text',
owner=self.node,
attrs={'in_world': True, 'scale': 0.02, 'h_align': 'center'},
)
self.reset_return_times()
self.last_player_to_hold: Player | None = None
self.time_out_respawn_time: int | None = None
@ -64,11 +69,15 @@ class Player(ba.Player['Team']):
class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(self, base_pos: Sequence[float],
base_region_material: ba.Material, base_region: ba.Node,
spaz_material_no_flag_physical: ba.Material,
spaz_material_no_flag_collide: ba.Material,
flagmaterial: ba.Material):
def __init__(
self,
base_pos: Sequence[float],
base_region_material: ba.Material,
base_region: ba.Node,
spaz_material_no_flag_physical: ba.Material,
spaz_material_no_flag_collide: ba.Material,
flagmaterial: ba.Material,
):
self.base_pos = base_pos
self.base_region_material = base_region_material
self.base_region = base_region
@ -158,8 +167,9 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
# Base class overrides.
self.slow_motion = self._epic_mode
self.default_music = (ba.MusicType.EPIC if self._epic_mode else
ba.MusicType.FLAG_CATCHER)
self.default_music = (
ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FLAG_CATCHER
)
def get_instance_description(self) -> str | Sequence:
if self._score_to_win == 1:
@ -178,15 +188,17 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
base_pos = self.map.get_flag_position(sessionteam.id)
Flag.project_stand(base_pos)
ba.newnode('light',
attrs={
'position': base_pos,
'intensity': 0.6,
'height_attenuated': False,
'volume_intensity_scale': 0.1,
'radius': 0.1,
'color': sessionteam.color
})
ba.newnode(
'light',
attrs={
'position': base_pos,
'intensity': 0.6,
'height_attenuated': False,
'volume_intensity_scale': 0.1,
'radius': 0.1,
'color': sessionteam.color,
},
)
base_region_mat = ba.Material()
pos = base_pos
@ -196,19 +208,22 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
'position': (pos[0], pos[1] + 0.75, pos[2]),
'scale': (0.5, 0.5, 0.5),
'type': 'sphere',
'materials': [base_region_mat, self._all_bases_material]
})
'materials': [base_region_mat, self._all_bases_material],
},
)
spaz_mat_no_flag_physical = ba.Material()
spaz_mat_no_flag_collide = ba.Material()
flagmat = ba.Material()
team = Team(base_pos=base_pos,
base_region_material=base_region_mat,
base_region=base_region,
spaz_material_no_flag_physical=spaz_mat_no_flag_physical,
spaz_material_no_flag_collide=spaz_mat_no_flag_collide,
flagmaterial=flagmat)
team = Team(
base_pos=base_pos,
base_region_material=base_region_mat,
base_region=base_region,
spaz_material_no_flag_physical=spaz_mat_no_flag_physical,
spaz_material_no_flag_collide=spaz_mat_no_flag_collide,
flagmaterial=flagmat,
)
# Some parts of our spazzes don't collide physically with our
# flags but generate callbacks.
@ -216,11 +231,18 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
conditions=('they_have_material', flagmat),
actions=(
('modify_part_collision', 'physical', False),
('call', 'at_connect',
lambda: self._handle_touching_own_flag(team, True)),
('call', 'at_disconnect',
lambda: self._handle_touching_own_flag(team, False)),
))
(
'call',
'at_connect',
lambda: self._handle_touching_own_flag(team, True),
),
(
'call',
'at_disconnect',
lambda: self._handle_touching_own_flag(team, False),
),
),
)
# Other parts of our spazzes don't collide with our flags at all.
spaz_mat_no_flag_collide.add_actions(
@ -234,11 +256,18 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('call', 'at_connect',
lambda: self._handle_flag_entered_base(team)),
('call', 'at_disconnect',
lambda: self._handle_flag_left_base(team)),
))
(
'call',
'at_connect',
lambda: self._handle_flag_entered_base(team),
),
(
'call',
'at_disconnect',
lambda: self._handle_flag_left_base(team),
),
),
)
return team
@ -276,9 +305,12 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
if team.enemy_flag_at_base:
# And show team name which scored (but actually we could
# show here player who returned enemy flag).
self.show_zoom_message(ba.Lstr(resource='nameScoresText',
subs=[('${NAME}', team.name)]),
color=team.color)
self.show_zoom_message(
ba.Lstr(
resource='nameScoresText', subs=[('${NAME}', team.name)]
),
color=team.color,
)
self._score(team)
else:
team.enemy_flag_at_base = True
@ -308,15 +340,13 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
'scale': 0.013,
'color': (1, 1, 0, 1),
'h_align': 'center',
'position': (bpos[0], bpos[1] + 3.2, bpos[2])
})
'position': (bpos[0], bpos[1] + 3.2, bpos[2]),
},
)
ba.timer(5.1, tnode.delete)
ba.animate(tnode, 'scale', {
0.0: 0,
0.2: 0.013,
4.8: 0.013,
5.0: 0
})
ba.animate(
tnode, 'scale', {0.0: 0, 0.2: 0.013, 4.8: 0.013, 5.0: 0}
)
def _tick(self) -> None:
# If either flag is away from base and not being held, tick down its
@ -344,10 +374,15 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
# to show its auto-return counter. (if there's self-touches
# its showing that time).
if team.flag_return_touches == 0:
flag.counter.text = (str(flag.time_out_respawn_time) if (
time_out_counting_down
and flag.time_out_respawn_time is not None
and flag.time_out_respawn_time <= 10) else '')
flag.counter.text = (
str(flag.time_out_respawn_time)
if (
time_out_counting_down
and flag.time_out_respawn_time is not None
and flag.time_out_respawn_time <= 10
)
else ''
)
flag.counter.color = (1, 1, 1, 0.5)
flag.counter.scale = 0.014
@ -389,8 +424,10 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
if flag.team is team:
# Check times here to prevent too much flashing.
if (team.last_flag_leave_time is None
or cur_time - team.last_flag_leave_time > 3.0):
if (
team.last_flag_leave_time is None
or cur_time - team.last_flag_leave_time > 3.0
):
ba.playsound(self._alarmsound, position=team.base_pos)
self._flash_base(team)
team.last_flag_leave_time = cur_time
@ -406,12 +443,15 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
return # No need to return when its at home.
if team.touch_return_timer_ticking is None:
team.touch_return_timer_ticking = ba.NodeActor(
ba.newnode('sound',
attrs={
'sound': self._ticking_sound,
'positional': False,
'loop': True
}))
ba.newnode(
'sound',
attrs={
'sound': self._ticking_sound,
'positional': False,
'loop': True,
},
)
)
flag = team.flag
if flag.touch_return_time is not None:
flag.touch_return_time -= 0.1
@ -428,9 +468,9 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
for player in team.players:
if player.touching_own_flag > 0:
return_score = 10 + 5 * int(self.flag_touch_return_time)
self.stats.player_scored(player,
return_score,
screenmessage=False)
self.stats.player_scored(
player, return_score, screenmessage=False
)
def _handle_touching_own_flag(self, team: Team, connecting: bool) -> None:
"""Called when a player touches or stops touching their own team flag.
@ -450,14 +490,17 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
player = spaz.getplayer(Player, True)
if player:
player.touching_own_flag += (1 if connecting else -1)
player.touching_own_flag += 1 if connecting else -1
# If return-time is zero, just kill it immediately.. otherwise keep
# track of touches and count down.
if float(self.flag_touch_return_time) <= 0.0:
assert team.flag is not None
if (connecting and not team.home_flag_at_base
and team.flag.held_count == 0):
if (
connecting
and not team.home_flag_at_base
and team.flag.held_count == 0
):
self._award_players_touching_own_flag(team)
ba.getcollision().opposingnode.handlemessage(ba.DieMessage())
@ -469,7 +512,8 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
team.touch_return_timer = ba.Timer(
0.1,
call=ba.Call(self._touch_return_update, team),
repeat=True)
repeat=True,
)
team.touch_return_timer_ticking = None
else:
team.flag_return_touches -= 1
@ -480,20 +524,24 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
ba.print_error('CTF flag_return_touches < 0')
def _flash_base(self, team: Team, length: float = 2.0) -> None:
light = ba.newnode('light',
attrs={
'position': team.base_pos,
'height_attenuated': False,
'radius': 0.3,
'color': team.color
})
light = ba.newnode(
'light',
attrs={
'position': team.base_pos,
'height_attenuated': False,
'radius': 0.3,
'color': team.color,
},
)
ba.animate(light, 'intensity', {0.0: 0, 0.25: 2.0, 0.5: 0}, loop=True)
ba.timer(length, light.delete)
def spawn_player_spaz(self,
player: Player,
position: Sequence[float] | None = None,
angle: float | None = None) -> PlayerSpaz:
def spawn_player_spaz(
self,
player: Player,
position: Sequence[float] | None = None,
angle: float | None = None,
) -> PlayerSpaz:
"""Intercept new spazzes and add our team material for them."""
spaz = super().spawn_player_spaz(player, position, angle)
player = spaz.getplayer(Player, True)
@ -510,22 +558,27 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
# (so we can calc restores).
assert spaz.node
spaz.node.materials = list(spaz.node.materials) + no_physical_mats
spaz.node.roller_materials = list(
spaz.node.roller_materials) + no_physical_mats
spaz.node.roller_materials = (
list(spaz.node.roller_materials) + no_physical_mats
)
# Pickups and punches shouldn't hit at all though.
spaz.node.punch_materials = list(
spaz.node.punch_materials) + no_collide_mats
spaz.node.pickup_materials = list(
spaz.node.pickup_materials) + no_collide_mats
spaz.node.extras_material = list(
spaz.node.extras_material) + no_collide_mats
spaz.node.punch_materials = (
list(spaz.node.punch_materials) + no_collide_mats
)
spaz.node.pickup_materials = (
list(spaz.node.pickup_materials) + no_collide_mats
)
spaz.node.extras_material = (
list(spaz.node.extras_material) + no_collide_mats
)
return spaz
def _update_scoreboard(self) -> None:
for team in self.teams:
self._scoreboard.set_team_value(team, team.score,
self._score_to_win)
self._scoreboard.set_team_value(
team, team.score, self._score_to_win
)
def handlemessage(self, msg: Any) -> Any:
@ -543,7 +596,8 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
assert isinstance(msg.flag, CTFFlag)
try:
msg.flag.last_player_to_hold = msg.node.getdelegate(
PlayerSpaz, True).getplayer(Player, True)
PlayerSpaz, True
).getplayer(Player, True)
except ba.NotFoundError:
pass

Some files were not shown because too many files have changed in this diff Show More