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/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/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/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/ninjafightplug.py": "https://files.ballistica.net/cache/ba1/f1/8d/934610811db8bdd58ea84a8f4408",
"assets/build/workspace/onslaughtplug.py": "https://files.ballistica.net/cache/ba1/c6/4c/47fe21bbcd938711d1ec2a19589d", "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/b1/38/beac9de90bee75363d1de76706b4", "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/__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", "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", "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/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/64/91/fd33398c70b862e46adb6c39e9e1", "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/e9/15/70169fee1aac6a96861e4862b889", "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/57/68/9d3fdec5450f357b6c57d35ab72c", "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/9f/1f/141dd5847b0efca0817dcbba7500", "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/49/64/d20ca535494f0af62272b0851268", "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/8f/df/c356b826190808fbc45625cf8ae1", "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/6f/33/424b11de9d4825beb58aa8394906", "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/05/c4/bfd09f076c82e5a3c7c4414f4d70", "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/51/92/6082c1fa0758c80cef9ebb984a83", "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/51/33/3f6541f11a29786e91234e0288b8", "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/6f/49/3565fc9e6e1d69f06e486f963fd0", "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/78/eb/543d44646dcfbd8476a1516cd77a", "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/2b/1e/b7ce51aa9f579e4a5a00ce4dd8bc", "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/04/d5/f27969fe1b0d587197c8fdc58c2e", "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/26/6c/da8ae887cb6ac020fad3c93e7cc9", "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/e6/64/460d773403bea1b2a1fb6a846b42", "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/84/51/54f2af640302aa0c116a0722f8f4", "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/45/06/55dd8fffa7c2ea80256e339d4c36", "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/25/78/a7abd69f73bc65602fa2e8662c88", "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/9d/a6/c963859c531bb19f507f405cf589", "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/df/dc/79349a169d3b00964d9f35853f84", "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/a7/63/d057e19cb7806302b9570b91c573", "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/17/c0/3da5a81581aa9275b6c32bb03fc8", "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/3f/84/d66202e8a15d5518f876dccdd6d6", "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/5a/ba/3a6f95b9e4a9c310ac63d0bc0a8c", "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/2f/58/9462e34601e2f442eb429185ee35", "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/50/09/6e8feb718cb60ea80ecaab4ba9a0", "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/7d/a3/f8fd8ad1037e5f5b47b72a6d1edf", "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/9d/c6/3902a717b71f9f8781d724c8ca23", "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/e4/5a/3b25cbbca51ef2a6036d8d1805aa", "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/fd/11/f0fb88f01753350f88f068b6c6a0", "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/e4/22/d3700b99ec02b9bd99b8801ad69b", "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/8b/d8/4b2e840ace5be8dd8fc9d6841cdd", "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/46/07/545eec0e6bde25bba8b3857d7e9b", "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/b4/ae/d9a2b38dc9824ac6acc79d520404", "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/b1/0f/10eeef2cec516e68c5821f55fe1c", "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/46/19/b3826f960327eb7585f546c3c878", "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/50/d8/1ca969ac3469fa1d1f75ea9f62b9", "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/51/37/888771c771d08b32be09d9203c19", "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/2f/04/0ac355606ea7f1609adf1bba693b", "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/5e/87/4774a28073c2cc9e1d918977252c", "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/9b/fe/f8a6cbf3ec67157fd09b66b344e7", "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/54/73/160cb6f003d538aca54a4e98c531", "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/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.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" "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>addr</w>
<w>addrstr</w> <w>addrstr</w>
<w>adisp</w> <w>adisp</w>
<w>advb</w>
<w>advertizing</w> <w>advertizing</w>
<w>aidl</w> <w>aidl</w>
<w>aint</w> <w>aint</w>
@ -1935,6 +1936,7 @@
<w>ppre</w> <w>ppre</w>
<w>pproxy</w> <w>pproxy</w>
<w>pptabcom</w> <w>pptabcom</w>
<w>prab</w>
<w>pragmas</w> <w>pragmas</w>
<w>prch</w> <w>prch</w>
<w>prec</w> <w>prec</w>
@ -1994,6 +1996,7 @@
<w>projs</w> <w>projs</w>
<w>promocode</w> <w>promocode</w>
<w>proxykey</w> <w>proxykey</w>
<w>prtb</w>
<w>prunedir</w> <w>prunedir</w>
<w>prval</w> <w>prval</w>
<w>pstats</w> <w>pstats</w>
@ -2041,6 +2044,7 @@
<w>pvrtcbest</w> <w>pvrtcbest</w>
<w>pvrtcfast</w> <w>pvrtcfast</w>
<w>pvval</w> <w>pvval</w>
<w>pwin</w>
<w>pybee</w> <w>pybee</w>
<w>pybuild</w> <w>pybuild</w>
<w>pybuildapple</w> <w>pybuildapple</w>
@ -2932,6 +2936,7 @@
<w>yscl</w> <w>yscl</w>
<w>ytweak</w> <w>ytweak</w>
<w>yval</w> <w>yval</w>
<w>zabcdefghijklmnopqrstuvwxyz</w>
<w>zaggy</w> <w>zaggy</w>
<w>zimbot</w> <w>zimbot</w>
<w>zipapp</w> <w>zipapp</w>

View File

@ -1,4 +1,5 @@
### 1.7.11 (build 20897, api 7, 2022-10-09) ### 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) ### 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. - 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: if TYPE_CHECKING:
from typing import Any, Callable from typing import Any, Callable
_T = TypeVar('_T') _T = TypeVar('_T')
@ -44,13 +45,16 @@ def _uninferrable() -> Any:
return _not_a_real_variable # type: ignore return _not_a_real_variable # type: ignore
def add_transaction(transaction: dict, def add_transaction(
callback: Callable | None = None) -> None: transaction: dict, callback: Callable | None = None
) -> None:
"""(internal)""" """(internal)"""
return None return None
def game_service_has_leaderboard(game: str, config: str) -> bool: def game_service_has_leaderboard(game: str, config: str) -> bool:
"""(internal) """(internal)
Given a game and config string, returns whether there is a leaderboard 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: def get_master_server_address(source: int = -1, version: int = 1) -> str:
"""(internal) """(internal)
Return the address of the master server. 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: def get_news_show() -> str:
"""(internal)""" """(internal)"""
return str() return str()
def get_price(item: str) -> str | None: def get_price(item: str) -> str | None:
"""(internal)""" """(internal)"""
return '' return ''
def get_public_login_id() -> str | None: def get_public_login_id() -> str | None:
"""(internal)""" """(internal)"""
return '' return ''
def get_purchased(item: str) -> bool: def get_purchased(item: str) -> bool:
"""(internal)""" """(internal)"""
return bool() return bool()
def get_purchases_state() -> int: def get_purchases_state() -> int:
"""(internal)""" """(internal)"""
return int() return int()
def get_v1_account_display_string(full: bool = True) -> str: def get_v1_account_display_string(full: bool = True) -> str:
"""(internal)""" """(internal)"""
return str() return str()
def get_v1_account_misc_read_val(name: str, default_value: Any) -> Any: def get_v1_account_misc_read_val(name: str, default_value: Any) -> Any:
"""(internal)""" """(internal)"""
return _uninferrable() return _uninferrable()
def get_v1_account_misc_read_val_2(name: str, default_value: Any) -> Any: def get_v1_account_misc_read_val_2(name: str, default_value: Any) -> Any:
"""(internal)""" """(internal)"""
return _uninferrable() return _uninferrable()
def get_v1_account_misc_val(name: str, default_value: Any) -> Any: def get_v1_account_misc_val(name: str, default_value: Any) -> Any:
"""(internal)""" """(internal)"""
return _uninferrable() return _uninferrable()
def get_v1_account_name() -> str: def get_v1_account_name() -> str:
"""(internal)""" """(internal)"""
return str() return str()
def get_v1_account_state() -> str: def get_v1_account_state() -> str:
"""(internal)""" """(internal)"""
return str() return str()
def get_v1_account_state_num() -> int: def get_v1_account_state_num() -> int:
"""(internal)""" """(internal)"""
return int() return int()
def get_v1_account_ticket_count() -> int: def get_v1_account_ticket_count() -> int:
"""(internal) """(internal)
Returns the number of tickets for the current account. 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: def get_v1_account_type() -> str:
"""(internal)""" """(internal)"""
return str() return str()
def get_v2_fleet() -> str: def get_v2_fleet() -> str:
"""(internal)""" """(internal)"""
return str() return str()
def have_outstanding_transactions() -> bool: def have_outstanding_transactions() -> bool:
"""(internal)""" """(internal)"""
return bool() return bool()
def in_game_purchase(item: str, price: int) -> None: def in_game_purchase(item: str, price: int) -> None:
"""(internal)""" """(internal)"""
return None return None
def is_blessed() -> bool: def is_blessed() -> bool:
"""(internal)""" """(internal)"""
return bool() return bool()
def mark_config_dirty() -> None: def mark_config_dirty() -> None:
"""(internal) """(internal)
Category: General Utility Functions Category: General Utility Functions
@ -169,36 +193,43 @@ def mark_config_dirty() -> None:
def power_ranking_query(callback: Callable, season: Any = None) -> None: def power_ranking_query(callback: Callable, season: Any = None) -> None:
"""(internal)""" """(internal)"""
return None return None
def purchase(item: str) -> None: def purchase(item: str) -> None:
"""(internal)""" """(internal)"""
return None return None
def report_achievement(achievement: str, pass_to_account: bool = True) -> None: def report_achievement(achievement: str, pass_to_account: bool = True) -> None:
"""(internal)""" """(internal)"""
return None return None
def reset_achievements() -> None: def reset_achievements() -> None:
"""(internal)""" """(internal)"""
return None return None
def restore_purchases() -> None: def restore_purchases() -> None:
"""(internal)""" """(internal)"""
return None return None
def run_transactions() -> None: def run_transactions() -> None:
"""(internal)""" """(internal)"""
return None return None
def sign_in_v1(account_type: str) -> None: def sign_in_v1(account_type: str) -> None:
"""(internal) """(internal)
Category: General Utility Functions 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: def sign_out_v1(v2_embedded: bool = False) -> None:
"""(internal) """(internal)
Category: General Utility Functions Category: General Utility Functions
@ -214,17 +246,20 @@ def sign_out_v1(v2_embedded: bool = False) -> None:
return None return None
def submit_score(game: str, def submit_score(
config: str, game: str,
name: Any, config: str,
score: int | None, name: Any,
callback: Callable, score: int | None,
friend_callback: Callable | None, callback: Callable,
order: str = 'increasing', friend_callback: Callable | None,
tournament_id: str | None = None, order: str = 'increasing',
score_type: str = 'points', tournament_id: str | None = None,
campaign: str | None = None, score_type: str = 'points',
level: str | None = None) -> None: campaign: str | None = None,
level: str | None = None,
) -> None:
"""(internal) """(internal)
Submit a score to the server; callback will be called with the results. Submit a score to the server; callback will be called with the results.
@ -235,7 +270,9 @@ def submit_score(game: str,
return None return None
def tournament_query(callback: Callable[[dict | None], None], def tournament_query(
args: dict) -> None: callback: Callable[[dict | None], None], args: dict
) -> None:
"""(internal)""" """(internal)"""
return None 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 # pylint: disable=redefined-builtin
from _ba import ( from _ba import (
CollideModel, Context, ContextCall, Data, InputDevice, Material, Model, CollideModel,
Node, SessionPlayer, Sound, Texture, Timer, Vec3, Widget, buttonwidget, Context,
camerashake, checkboxwidget, columnwidget, containerwidget, do_once, ContextCall,
emitfx, getactivity, getcollidemodel, getmodel, getnodes, getsession, Data,
getsound, gettexture, hscrollwidget, imagewidget, newactivity, newnode, InputDevice,
playsound, printnodes, printobjects, pushcall, quit, rowwidget, safecolor, Material,
screenmessage, scrollwidget, set_analytics_screen, charstr, textwidget, Model,
time, timer, open_url, widget, clipboard_is_supported, clipboard_has_text, Node,
clipboard_get_text, clipboard_set_text, getdata, in_logic_thread) 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._activity import Activity
from ba._plugin import PotentialPlugin, Plugin, PluginSubsystem from ba._plugin import PotentialPlugin, Plugin, PluginSubsystem
from ba._actor import Actor from ba._actor import Actor
@ -27,21 +73,50 @@ from ba._app import App
from ba._cloud import CloudSubsystem from ba._cloud import CloudSubsystem
from ba._coopgame import CoopGameActivity from ba._coopgame import CoopGameActivity
from ba._coopsession import CoopSession from ba._coopsession import CoopSession
from ba._dependency import (Dependency, DependencyComponent, DependencySet, from ba._dependency import (
AssetPackage) Dependency,
from ba._generated.enums import (TimeType, Permission, TimeFormat, SpecialChar, DependencyComponent,
InputType, UIScale) DependencySet,
AssetPackage,
)
from ba._generated.enums import (
TimeType,
Permission,
TimeFormat,
SpecialChar,
InputType,
UIScale,
)
from ba._error import ( from ba._error import (
print_exception, print_error, ContextError, NotFoundError, print_exception,
PlayerNotFoundError, SessionPlayerNotFoundError, NodeNotFoundError, print_error,
ActorNotFoundError, InputDeviceNotFoundError, WidgetNotFoundError, ContextError,
ActivityNotFoundError, TeamNotFoundError, SessionTeamNotFoundError, NotFoundError,
SessionNotFoundError, DelegateNotFoundError, DependencyError) PlayerNotFoundError,
SessionPlayerNotFoundError,
NodeNotFoundError,
ActorNotFoundError,
InputDeviceNotFoundError,
WidgetNotFoundError,
ActivityNotFoundError,
TeamNotFoundError,
SessionTeamNotFoundError,
SessionNotFoundError,
DelegateNotFoundError,
DependencyError,
)
from ba._freeforallsession import FreeForAllSession from ba._freeforallsession import FreeForAllSession
from ba._gameactivity import GameActivity from ba._gameactivity import GameActivity
from ba._gameresults import GameResults from ba._gameresults import GameResults
from ba._settings import (Setting, IntSetting, FloatSetting, ChoiceSetting, from ba._settings import (
BoolSetting, IntChoiceSetting, FloatChoiceSetting) Setting,
IntSetting,
FloatSetting,
ChoiceSetting,
BoolSetting,
IntChoiceSetting,
FloatChoiceSetting,
)
from ba._language import Lstr, LanguageSubsystem from ba._language import Lstr, LanguageSubsystem
from ba._map import Map, getmaps from ba._map import Map, getmaps
from ba._session import Session from ba._session import Session
@ -57,23 +132,53 @@ from ba._appconfig import AppConfig
from ba._appdelegate import AppDelegate from ba._appdelegate import AppDelegate
from ba._apputils import is_browser_likely_available, garbage_collect from ba._apputils import is_browser_likely_available, garbage_collect
from ba._campaign import Campaign from ba._campaign import Campaign
from ba._gameutils import (GameTip, animate, animate_array, show_damage_count, from ba._gameutils import (
timestring, cameraflash) GameTip,
from ba._general import (WeakCall, Call, existing, Existable, animate,
verify_object_death, storagename, getclass) 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._keyboard import Keyboard
from ba._level import Level from ba._level import Level
from ba._lobby import Lobby, Chooser from ba._lobby import Lobby, Chooser
from ba._math import normalized_color, is_point_in_box, vec3validate from ba._math import normalized_color, is_point_in_box, vec3validate
from ba._meta import MetadataSubsystem from ba._meta import MetadataSubsystem
from ba._messages import (UNHANDLED, OutOfBoundsMessage, DeathType, DieMessage, from ba._messages import (
PlayerDiedMessage, StandMessage, PickUpMessage, UNHANDLED,
DropMessage, PickedUpMessage, DroppedMessage, OutOfBoundsMessage,
ShouldShatterMessage, ImpactDamageMessage, DeathType,
FreezeMessage, ThawMessage, HitMessage, DieMessage,
CelebrateMessage) PlayerDiedMessage,
from ba._music import (setmusic, MusicPlayer, MusicType, MusicPlayMode, StandMessage,
MusicSubsystem) 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._powerup import PowerupMessage, PowerupAcceptMessage
from ba._multiteamsession import MultiTeamSession from ba._multiteamsession import MultiTeamSession
from ba.ui import Window, UIController, uicleanupcheck from ba.ui import Window, UIController, uicleanupcheck
@ -82,47 +187,185 @@ from ba._collision import Collision, getcollision
app: App app: App
__all__ = [ __all__ = [
'Achievement', 'AchievementSubsystem', 'Activity', 'ActivityNotFoundError', 'Achievement',
'Actor', 'ActorNotFoundError', 'animate', 'animate_array', 'app', 'App', 'AchievementSubsystem',
'AppConfig', 'AppDelegate', 'AssetPackage', 'BoolSetting', 'buttonwidget', 'Activity',
'Call', 'cameraflash', 'camerashake', 'Campaign', 'CelebrateMessage', 'ActivityNotFoundError',
'charstr', 'checkboxwidget', 'ChoiceSetting', 'Chooser', 'Actor',
'clipboard_get_text', 'clipboard_has_text', 'clipboard_is_supported', 'ActorNotFoundError',
'clipboard_set_text', 'CollideModel', 'Collision', 'columnwidget', 'animate',
'containerwidget', 'Context', 'ContextCall', 'ContextError', 'animate_array',
'CloudSubsystem', 'CoopGameActivity', 'CoopSession', 'Data', 'DeathType', 'app',
'DelegateNotFoundError', 'Dependency', 'DependencyComponent', 'App',
'DependencyError', 'DependencySet', 'DieMessage', 'do_once', 'DropMessage', 'AppConfig',
'DroppedMessage', 'DualTeamSession', 'emitfx', 'EmptyPlayer', 'EmptyTeam', 'AppDelegate',
'Existable', 'existing', 'FloatChoiceSetting', 'FloatSetting', 'AssetPackage',
'FreeForAllSession', 'FreezeMessage', 'GameActivity', 'GameResults', 'BoolSetting',
'GameTip', 'garbage_collect', 'getactivity', 'getclass', 'getcollidemodel', 'buttonwidget',
'getcollision', 'getdata', 'getmaps', 'getmodel', 'getnodes', 'getsession', 'Call',
'getsound', 'gettexture', 'HitMessage', 'hscrollwidget', 'imagewidget', 'cameraflash',
'ImpactDamageMessage', 'in_logic_thread', 'InputDevice', 'camerashake',
'InputDeviceNotFoundError', 'InputType', 'IntChoiceSetting', 'IntSetting', 'Campaign',
'is_browser_likely_available', 'is_point_in_box', 'Keyboard', 'CelebrateMessage',
'LanguageSubsystem', 'Level', 'Lobby', 'Lstr', 'Map', 'Material', 'charstr',
'MetadataSubsystem', 'Model', 'MultiTeamSession', 'MusicPlayer', 'checkboxwidget',
'MusicPlayMode', 'MusicSubsystem', 'MusicType', 'newactivity', 'newnode', 'ChoiceSetting',
'Node', 'NodeActor', 'NodeNotFoundError', 'normalized_color', 'Chooser',
'NotFoundError', 'open_url', 'OutOfBoundsMessage', 'Permission', 'clipboard_get_text',
'PickedUpMessage', 'PickUpMessage', 'Player', 'PlayerDiedMessage', 'clipboard_has_text',
'PlayerInfo', 'PlayerNotFoundError', 'PlayerRecord', 'PlayerScoredMessage', 'clipboard_is_supported',
'playsound', 'Plugin', 'PluginSubsystem', 'PotentialPlugin', 'clipboard_set_text',
'PowerupAcceptMessage', 'PowerupMessage', 'print_error', 'print_exception', 'CollideModel',
'printnodes', 'printobjects', 'pushcall', 'quit', 'rowwidget', 'safecolor', 'Collision',
'ScoreConfig', 'ScoreType', 'screenmessage', 'scrollwidget', 'columnwidget',
'ServerController', 'Session', 'SessionNotFoundError', 'SessionPlayer', 'containerwidget',
'SessionPlayerNotFoundError', 'SessionTeam', 'SessionTeamNotFoundError', 'Context',
'set_analytics_screen', 'setmusic', 'Setting', 'ShouldShatterMessage', 'ContextCall',
'show_damage_count', 'Sound', 'SpecialChar', 'StandLocation', 'ContextError',
'StandMessage', 'Stats', 'storagename', 'Team', 'TeamGameActivity', 'CloudSubsystem',
'TeamNotFoundError', 'Texture', 'textwidget', 'ThawMessage', 'time', 'CoopGameActivity',
'TimeFormat', 'Timer', 'timer', 'timestring', 'TimeType', 'uicleanupcheck', 'CoopSession',
'UIController', 'UIScale', 'UISubsystem', 'UNHANDLED', 'Vec3', 'Data',
'vec3validate', 'verify_object_death', 'WeakCall', 'Widget', 'widget', 'DeathType',
'WidgetNotFoundError', 'Window' '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. # so let's make an exception for it.
if os.environ.get('BA_DOCS_GENERATION', '0') != '1': if os.environ.get('BA_DOCS_GENERATION', '0') != '1':
from efro.util import set_canonical_module from efro.util import set_canonical_module
globs = globals() globs = globals()
set_canonical_module( set_canonical_module(
module_globals=globs, 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() _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. # Auto-sign-in to a local account in a moment if we're set to.
def do_auto_sign_in() -> None: def do_auto_sign_in() -> None:
if _ba.app.headless_mode or _ba.app.config.get( if (
'Auto Account State') == 'Local': _ba.app.headless_mode
or _ba.app.config.get('Auto Account State') == 'Local'
):
_internal.sign_in_v1('Local') _internal.sign_in_v1('Local')
_ba.pushcall(do_auto_sign_in) _ba.pushcall(do_auto_sign_in)
@ -60,9 +62,14 @@ class AccountV1Subsystem:
(internal) (internal)
""" """
from ba._language import Lstr from ba._language import Lstr
_ba.screenmessage(Lstr(resource='getTicketsWindow.receivedTicketsText',
subs=[('${COUNT}', str(count))]), _ba.screenmessage(
color=(0, 1, 0)) Lstr(
resource='getTicketsWindow.receivedTicketsText',
subs=[('${COUNT}', str(count))],
),
color=(0, 1, 0),
)
_ba.playsound(_ba.getsound('cashRegister')) _ba.playsound(_ba.getsound('cashRegister'))
def cache_league_rank_data(self, data: Any) -> None: def cache_league_rank_data(self, data: Any) -> None:
@ -73,9 +80,9 @@ class AccountV1Subsystem:
"""(internal)""" """(internal)"""
return self.league_rank_cache.get('info', None) return self.league_rank_cache.get('info', None)
def get_league_rank_points(self, def get_league_rank_points(
data: dict[str, Any] | None, self, data: dict[str, Any] | None, subset: str | None = None
subset: str | None = None) -> int: ) -> int:
"""(internal)""" """(internal)"""
if data is None: if data is None:
return 0 return 0
@ -90,15 +97,23 @@ class AccountV1Subsystem:
if ach.complete: if ach.complete:
total_ach_value += ach.power_ranking_value total_ach_value += ach.power_ranking_value
trophies_total: int = (data['t0a'] * data['t0am'] + trophies_total: int = (
data['t0b'] * data['t0bm'] + data['t0a'] * data['t0am']
data['t1'] * data['t1m'] + + data['t0b'] * data['t0bm']
data['t2'] * data['t2m'] + + data['t1'] * data['t1m']
data['t3'] * data['t3m'] + + data['t2'] * data['t2m']
data['t4'] * data['t4m']) + data['t3'] * data['t3m']
+ data['t4'] * data['t4m']
)
if subset == 'trophyCount': if subset == 'trophyCount':
val: int = (data['t0a'] + data['t0b'] + data['t1'] + data['t2'] + val: int = (
data['t3'] + data['t4']) data['t0a']
+ data['t0b']
+ data['t1']
+ data['t2']
+ data['t3']
+ data['t4']
)
assert isinstance(val, int) assert isinstance(val, int)
return val return val
if subset == 'trophies': if subset == 'trophies':
@ -108,41 +123,54 @@ class AccountV1Subsystem:
raise ValueError('invalid subset value: ' + str(subset)) raise ValueError('invalid subset value: ' + str(subset))
if data['p']: if data['p']:
pro_mult = 1.0 + float( pro_mult = (
_internal.get_v1_account_misc_read_val('proPowerRankingBoost', 1.0
0.0)) * 0.01 + float(
_internal.get_v1_account_misc_read_val(
'proPowerRankingBoost', 0.0
)
)
* 0.01
)
else: else:
pro_mult = 1.0 pro_mult = 1.0
# For final value, apply our pro mult and activeness-mult. # For final value, apply our pro mult and activeness-mult.
return int( return int(
(total_ach_value + trophies_total) * (total_ach_value + trophies_total)
(data['act'] if data['act'] is not None else 1.0) * pro_mult) * (data['act'] if data['act'] is not None else 1.0)
* pro_mult
)
def cache_tournament_info(self, info: Any) -> None: def cache_tournament_info(self, info: Any) -> None:
"""(internal)""" """(internal)"""
from ba._generated.enums import TimeType, TimeFormat from ba._generated.enums import TimeType, TimeFormat
for entry in info: for entry in info:
cache_entry = self.tournament_info[entry['tournamentID']] = ( cache_entry = self.tournament_info[
copy.deepcopy(entry)) entry['tournamentID']
] = copy.deepcopy(entry)
# Also store the time we received this, so we can adjust # Also store the time we received this, so we can adjust
# time-remaining values/etc. # time-remaining values/etc.
cache_entry['timeReceived'] = _ba.time(TimeType.REAL, cache_entry['timeReceived'] = _ba.time(
TimeFormat.MILLISECONDS) TimeType.REAL, TimeFormat.MILLISECONDS
)
cache_entry['valid'] = True cache_entry['valid'] = True
def get_purchased_icons(self) -> list[str]: def get_purchased_icons(self) -> list[str]:
"""(internal)""" """(internal)"""
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from ba import _store from ba import _store
if _internal.get_v1_account_state() != 'signed_in': if _internal.get_v1_account_state() != 'signed_in':
return [] return []
icons = [] icons = []
store_items = _store.get_store_items() store_items = _store.get_store_items()
for item_name, item in list(store_items.items()): for item_name, item in list(store_items.items()):
if item_name.startswith('icons.') and _internal.get_purchased( if item_name.startswith('icons.') and _internal.get_purchased(
item_name): item_name
):
icons.append(item['icon']) icons.append(item['icon'])
return icons return icons
@ -160,23 +188,28 @@ class AccountV1Subsystem:
# If the short version of our account name currently cant be # If the short version of our account name currently cant be
# displayed by the game, cancel. # displayed by the game, cancel.
if not _ba.have_chars( if not _ba.have_chars(
_internal.get_v1_account_display_string(full=False)): _internal.get_v1_account_display_string(full=False)
):
return return
config = _ba.app.config config = _ba.app.config
if ('Player Profiles' not in config if (
or '__account__' not in config['Player Profiles']): 'Player Profiles' not in config
or '__account__' not in config['Player Profiles']
):
# Create a spaz with a nice default purply color. # Create a spaz with a nice default purply color.
_internal.add_transaction({ _internal.add_transaction(
'type': 'ADD_PLAYER_PROFILE', {
'name': '__account__', 'type': 'ADD_PLAYER_PROFILE',
'profile': { 'name': '__account__',
'character': 'Spaz', 'profile': {
'color': [0.5, 0.25, 1.0], 'character': 'Spaz',
'highlight': [0.5, 0.25, 1.0] 'color': [0.5, 0.25, 1.0],
'highlight': [0.5, 0.25, 1.0],
},
} }
}) )
_internal.run_transactions() _internal.run_transactions()
def have_pro(self) -> bool: def have_pro(self) -> bool:
@ -188,7 +221,8 @@ class AccountV1Subsystem:
_internal.get_purchased('upgrades.pro') _internal.get_purchased('upgrades.pro')
or _internal.get_purchased('static.pro') or _internal.get_purchased('static.pro')
or _internal.get_purchased('static.pro_sale') or _internal.get_purchased('static.pro_sale')
or 'ballistica' + 'core' == _ba.appname()) or 'ballistica' + 'core' == _ba.appname()
)
def have_pro_options(self) -> bool: def have_pro_options(self) -> bool:
"""Return whether pro-options are present. """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 # or also if we've been grandfathered in or are using ballistica-core
# builds. # builds.
return self.have_pro() or bool( return self.have_pro() or bool(
_internal.get_v1_account_misc_read_val_2('proOptionsUnlocked', _internal.get_v1_account_misc_read_val_2(
False) 'proOptionsUnlocked', False
or _ba.app.config.get('lc14292', 0) > 1) )
or _ba.app.config.get('lc14292', 0) > 1
)
def show_post_purchase_message(self) -> None: def show_post_purchase_message(self) -> None:
"""(internal)""" """(internal)"""
from ba._language import Lstr from ba._language import Lstr
from ba._generated.enums import TimeType from ba._generated.enums import TimeType
cur_time = _ba.time(TimeType.REAL) cur_time = _ba.time(TimeType.REAL)
if (self.last_post_purchase_message_time is None if (
or cur_time - self.last_post_purchase_message_time > 3.0): 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 self.last_post_purchase_message_time = cur_time
with _ba.Context('ui'): with _ba.Context('ui'):
_ba.screenmessage(Lstr(resource='updatingAccountText', _ba.screenmessage(
fallback_resource='purchasingText'), Lstr(
color=(0, 1, 0)) resource='updatingAccountText',
fallback_resource='purchasingText',
),
color=(0, 1, 0),
)
_ba.playsound(_ba.getsound('click01')) _ba.playsound(_ba.getsound('click01'))
def on_account_state_changed(self) -> None: def on_account_state_changed(self) -> None:
@ -225,16 +268,21 @@ class AccountV1Subsystem:
from ba._language import Lstr from ba._language import Lstr
# Run any pending promo codes we had queued up while not signed in. # Run any pending promo codes we had queued up while not signed in.
if _internal.get_v1_account_state( if (
) == 'signed_in' and self.pending_promo_codes: _internal.get_v1_account_state() == 'signed_in'
and self.pending_promo_codes
):
for code in self.pending_promo_codes: for code in self.pending_promo_codes:
_ba.screenmessage(Lstr(resource='submittingPromoCodeText'), _ba.screenmessage(
color=(0, 1, 0)) Lstr(resource='submittingPromoCodeText'), color=(0, 1, 0)
_internal.add_transaction({ )
'type': 'PROMO_CODE', _internal.add_transaction(
'expire_time': time.time() + 5, {
'code': code 'type': 'PROMO_CODE',
}) 'expire_time': time.time() + 5,
'code': code,
}
)
_internal.run_transactions() _internal.run_transactions()
self.pending_promo_codes = [] self.pending_promo_codes = []
@ -254,18 +302,18 @@ class AccountV1Subsystem:
# If we're still not signed in and have pending codes, # If we're still not signed in and have pending codes,
# inform the user that they need to sign in to use them. # inform the user that they need to sign in to use them.
if self.pending_promo_codes: if self.pending_promo_codes:
_ba.screenmessage(Lstr(resource='signInForPromoCodeText'), _ba.screenmessage(
color=(1, 0, 0)) Lstr(resource='signInForPromoCodeText'), color=(1, 0, 0)
)
_ba.playsound(_ba.getsound('error')) _ba.playsound(_ba.getsound('error'))
self.pending_promo_codes.append(code) self.pending_promo_codes.append(code)
_ba.timer(6.0, check_pending_codes, timetype=TimeType.REAL) _ba.timer(6.0, check_pending_codes, timetype=TimeType.REAL)
return return
_ba.screenmessage(Lstr(resource='submittingPromoCodeText'), _ba.screenmessage(
color=(0, 1, 0)) Lstr(resource='submittingPromoCodeText'), color=(0, 1, 0)
_internal.add_transaction({ )
'type': 'PROMO_CODE', _internal.add_transaction(
'expire_time': time.time() + 5, {'type': 'PROMO_CODE', 'expire_time': time.time() + 5, 'code': code}
'code': code )
})
_internal.run_transactions() _internal.run_transactions()

View File

@ -55,8 +55,9 @@ class AccountV2Subsystem:
"""Internal - should be overridden by subclass.""" """Internal - should be overridden by subclass."""
return None return None
def on_primary_account_changed(self, def on_primary_account_changed(
account: AccountV2Handle | None) -> None: self, account: AccountV2Handle | None
) -> None:
"""Callback run after the primary account changes. """Callback run after the primary account changes.
Will be called with None on log-outs or when new credentials Will be called with None on log-outs or when new credentials
@ -70,13 +71,16 @@ class AccountV2Subsystem:
# informed when that process completes. # informed when that process completes.
if account.workspaceid is not None: if account.workspaceid is not None:
assert account.workspacename is not None assert account.workspacename is not None
if (not self._initial_login_completed if (
and not self._kicked_off_workspace_load): not self._initial_login_completed
and not self._kicked_off_workspace_load
):
self._kicked_off_workspace_load = True self._kicked_off_workspace_load = True
_ba.app.workspaces.set_active_workspace( _ba.app.workspaces.set_active_workspace(
workspaceid=account.workspaceid, workspaceid=account.workspaceid,
workspacename=account.workspacename, workspacename=account.workspacename,
on_completed=self._on_set_active_workspace_completed) on_completed=self._on_set_active_workspace_completed,
)
else: else:
# Don't activate workspaces if we've already told the game # Don't activate workspaces if we've already told the game
# that initial-log-in is done or if we've already kicked # that initial-log-in is done or if we've already kicked
@ -84,7 +88,8 @@ class AccountV2Subsystem:
_ba.screenmessage( _ba.screenmessage(
f'\'{account.workspacename}\'' f'\'{account.workspacename}\''
f' will be activated at next app launch.', f' will be activated at next app launch.',
color=(1, 1, 0)) color=(1, 1, 0),
)
_ba.playsound(_ba.getsound('error')) _ba.playsound(_ba.getsound('error'))
return return
@ -124,8 +129,7 @@ class AccountV2Handle:
self.workspaceid: str | None = None self.workspaceid: str | None = None
def __enter__(self) -> 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: def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any:
"""Support for "with" statement.""" """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 import _ba
from ba._team import Team from ba._team import Team
from ba._player import Player from ba._player import Player
from ba._error import (print_exception, SessionTeamNotFoundError, from ba._error import (
SessionPlayerNotFoundError, NodeNotFoundError) print_exception,
SessionTeamNotFoundError,
SessionPlayerNotFoundError,
NodeNotFoundError,
)
from ba._dependency import DependencyComponent from ba._dependency import DependencyComponent
from ba._general import Call, verify_object_death from ba._general import Call, verify_object_death
from ba._messages import UNHANDLED from ba._messages import UNHANDLED
@ -199,8 +203,11 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
session = self._session() session = self._session()
if session is not None: if session is not None:
_ba.pushcall( _ba.pushcall(
Call(session.transitioning_out_activity_was_freed, Call(
self.can_show_ad_on_death)) session.transitioning_out_activity_was_freed,
self.can_show_ad_on_death,
)
)
@property @property
def globalsnode(self) -> ba.Node: def globalsnode(self) -> ba.Node:
@ -220,6 +227,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
""" """
if self._stats is None: if self._stats is None:
from ba._error import NotFoundError from ba._error import NotFoundError
raise NotFoundError() raise NotFoundError()
return self._stats return self._stats
@ -285,7 +293,8 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
5.0, 5.0,
Call(self._check_activity_death, ref, [0]), Call(self._check_activity_death, ref, [0]),
repeat=True, repeat=True,
timetype=TimeType.REAL) timetype=TimeType.REAL,
)
# Run _expire in an empty context; nothing should be happening in # Run _expire in an empty context; nothing should be happening in
# there except deleting things which requires no context. # there except deleting things which requires no context.
@ -296,8 +305,9 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
with _ba.Context('empty'): with _ba.Context('empty'):
self._expire() self._expire()
else: else:
raise RuntimeError(f'destroy() called when' raise RuntimeError(
f' already expired for {self}') f'destroy() called when' f' already expired for {self}'
)
def retain_actor(self, actor: ba.Actor) -> None: def retain_actor(self, actor: ba.Actor) -> None:
"""Add a strong-reference to a ba.Actor to this Activity. """Add a strong-reference to a ba.Actor to this Activity.
@ -308,6 +318,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
""" """
if __debug__: if __debug__:
from ba._actor import Actor from ba._actor import Actor
assert isinstance(actor, Actor) assert isinstance(actor, Actor)
self._actor_refs.append(actor) self._actor_refs.append(actor)
@ -318,6 +329,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
""" """
if __debug__: if __debug__:
from ba._actor import Actor from ba._actor import Actor
assert isinstance(actor, Actor) assert isinstance(actor, Actor)
self._actor_weak_refs.append(weakref.ref(actor)) self._actor_weak_refs.append(weakref.ref(actor))
@ -330,6 +342,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
session = self._session() session = self._session()
if session is None: if session is None:
from ba._error import SessionNotFoundError from ba._error import SessionNotFoundError
raise SessionNotFoundError() raise SessionNotFoundError()
return session return session
@ -381,7 +394,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
def has_transitioned_in(self) -> bool: def has_transitioned_in(self) -> bool:
"""Return whether ba.Activity.on_transition_in() """Return whether ba.Activity.on_transition_in()
has been called.""" has been called."""
return self._has_transitioned_in return self._has_transitioned_in
def has_begun(self) -> bool: 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: 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 = prev_globals.vr_overlay_center
glb.vr_overlay_center_enabled = ( 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 they want to inherit tint from the previous self.
if self.inherits_tint and prev_globals is not None: 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. # Start pruning our various things periodically.
self._prune_dead_actors() self._prune_dead_actors()
self._prune_dead_actors_timer = _ba.Timer(5.17, self._prune_dead_actors_timer = _ba.Timer(
self._prune_dead_actors, 5.17, self._prune_dead_actors, repeat=True
repeat=True) )
_ba.timer(13.3, self._prune_delay_deletes, 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. # activity launch; just wanna be sure that is intentional.
self.on_begin() self.on_begin()
def end(self, def end(
results: Any = None, self, results: Any = None, delay: float = 0.0, force: bool = False
delay: float = 0.0, ) -> None:
force: bool = False) -> None:
"""Commences Activity shutdown and delivers results to the ba.Session. """Commences Activity shutdown and delivers results to the ba.Session.
'delay' is the time delay before the Activity actually ends 'delay' is the time delay before the Activity actually ends
@ -543,7 +556,8 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
sessionplayer.setactivity(self) sessionplayer.setactivity(self)
with _ba.Context(self): with _ba.Context(self):
sessionplayer.activityplayer = player = self.create_player( sessionplayer.activityplayer = player = self.create_player(
sessionplayer) sessionplayer
)
player.postinit(sessionplayer) player.postinit(sessionplayer)
assert player not in team.players assert player not in team.players
@ -654,7 +668,8 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
self._teams_that_left.append(weakref.ref(team)) self._teams_that_left.append(weakref.ref(team))
def _reset_session_player_for_no_activity( 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 # Let's be extra-defensive here: killing a node/input-call/etc
# could trigger user-code resulting in errors, but we would still # could trigger user-code resulting in errors, but we would still
@ -664,13 +679,15 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
except Exception: except Exception:
print_exception( print_exception(
f'Error resetting SessionPlayer node on {sessionplayer}' f'Error resetting SessionPlayer node on {sessionplayer}'
f' for {self}.') f' for {self}.'
)
try: try:
sessionplayer.resetinput() sessionplayer.resetinput()
except Exception: except Exception:
print_exception( print_exception(
f'Error resetting SessionPlayer input on {sessionplayer}' f'Error resetting SessionPlayer input on {sessionplayer}'
f' for {self}.') f' for {self}.'
)
# These should never fail I think... # These should never fail I think...
sessionplayer.setactivity(None) sessionplayer.setactivity(None)
@ -691,21 +708,26 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
self._playertype = type(self).__orig_bases__[-1].__args__[0] self._playertype = type(self).__orig_bases__[-1].__args__[0]
if not isinstance(self._playertype, type): if not isinstance(self._playertype, type):
self._playertype = Player self._playertype = Player
print(f'ERROR: {type(self)} was not passed a Player' print(
f' type argument; please explicitly pass ba.Player' f'ERROR: {type(self)} was not passed a Player'
f' if you do not want to override it.') 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] self._teamtype = type(self).__orig_bases__[-1].__args__[1]
if not isinstance(self._teamtype, type): if not isinstance(self._teamtype, type):
self._teamtype = Team self._teamtype = Team
print(f'ERROR: {type(self)} was not passed a Team' print(
f' type argument; please explicitly pass ba.Team' f'ERROR: {type(self)} was not passed a Team'
f' if you do not want to override it.') 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._playertype, Player)
assert issubclass(self._teamtype, Team) assert issubclass(self._teamtype, Team)
@classmethod @classmethod
def _check_activity_death(cls, activity_ref: weakref.ref[Activity], def _check_activity_death(
counter: list[int]) -> None: cls, activity_ref: weakref.ref[Activity], counter: list[int]
) -> None:
"""Sanity check to make sure an Activity was destroyed properly. """Sanity check to make sure an Activity was destroyed properly.
Receives a weakref to a ba.Activity which should have torn itself Receives a weakref to a ba.Activity which should have torn itself
@ -715,9 +737,13 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
try: try:
import gc import gc
import types import types
activity = activity_ref() activity = activity_ref()
print('ERROR: Activity is not dying when expected:', activity, print(
'(warning ' + str(counter[0] + 1) + ')') 'ERROR: Activity is not dying when expected:',
activity,
'(warning ' + str(counter[0] + 1) + ')',
)
print('This means something is still strong-referencing it.') print('This means something is still strong-referencing it.')
counter[0] += 1 counter[0] += 1
@ -784,8 +810,9 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
try: try:
actor.on_expire() actor.on_expire()
except Exception: except Exception:
print_exception(f'Error in Actor.on_expire()' print_exception(
f' for {actor_ref()}.') f'Error in Actor.on_expire()' f' for {actor_ref()}.'
)
def _expire_players(self) -> None: def _expire_players(self) -> None:

View File

@ -9,6 +9,7 @@ import _ba
from ba._activity import Activity from ba._activity import Activity
from ba._music import setmusic, MusicType from ba._music import setmusic, MusicType
from ba._generated.enums import InputType, UIScale from ba._generated.enums import InputType, UIScale
# False-positive from pylint due to our class-generics-filter. # False-positive from pylint due to our class-generics-filter.
from ba._player import EmptyPlayer # pylint: disable=W0611 from ba._player import EmptyPlayer # pylint: disable=W0611
from ba._team import EmptyTeam # pylint: disable=W0611 from ba._team import EmptyTeam # pylint: disable=W0611
@ -40,6 +41,7 @@ class EndSessionActivity(Activity[EmptyPlayer, EmptyTeam]):
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from bastd.mainmenu import MainMenuSession from bastd.mainmenu import MainMenuSession
from ba._general import Call from ba._general import Call
super().on_begin() super().on_begin()
_ba.unlock_all_input() _ba.unlock_all_input()
_ba.app.ads.call_after_ad(Call(_ba.new_host_session, MainMenuSession)) _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 # pylint: disable=cyclic-import
from bastd.actor.tipstext import TipsText from bastd.actor.tipstext import TipsText
from bastd.actor.background import Background from bastd.actor.background import Background
super().on_transition_in() super().on_transition_in()
self._background = Background(fade_time=0.5, self._background = Background(
start_faded=True, fade_time=0.5, start_faded=True, show_logo=True
show_logo=True) )
self._tips_text = TipsText() self._tips_text = TipsText()
setmusic(MusicType.CHAR_SELECT) setmusic(MusicType.CHAR_SELECT)
self._join_info = self.session.lobby.create_join_info() self._join_info = self.session.lobby.create_join_info()
@ -103,10 +106,11 @@ class TransitionActivity(Activity[EmptyPlayer, EmptyTeam]):
def on_transition_in(self) -> None: def on_transition_in(self) -> None:
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from bastd.actor import background # FIXME: Don't use bastd from ba. from bastd.actor import background # FIXME: Don't use bastd from ba.
super().on_transition_in() super().on_transition_in()
self._background = background.Background(fade_time=0.5, self._background = background.Background(
start_faded=False, fade_time=0.5, start_faded=False, show_logo=False
show_logo=False) )
def on_begin(self) -> None: def on_begin(self) -> None:
super().on_begin() super().on_begin()
@ -143,9 +147,11 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
def on_player_join(self, player: EmptyPlayer) -> None: def on_player_join(self, player: EmptyPlayer) -> None:
from ba._general import WeakCall from ba._general import WeakCall
super().on_player_join(player) super().on_player_join(player)
time_till_assign = max( 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 # If we're still kicking at the end of our assign-delay, assign this
# guy's input to trigger us. # guy's input to trigger us.
@ -154,10 +160,11 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
def on_transition_in(self) -> None: def on_transition_in(self) -> None:
from bastd.actor.tipstext import TipsText from bastd.actor.tipstext import TipsText
from bastd.actor.background import Background from bastd.actor.background import Background
super().on_transition_in() super().on_transition_in()
self._background = Background(fade_time=0.5, self._background = Background(
start_faded=False, fade_time=0.5, start_faded=False, show_logo=True
show_logo=True) )
if self._default_show_tips: if self._default_show_tips:
self._tips_text = TipsText() self._tips_text = TipsText()
setmusic(self.default_music) setmusic(self.default_music)
@ -166,6 +173,7 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from bastd.actor.text import Text from bastd.actor.text import Text
from ba import _language from ba import _language
super().on_begin() super().on_begin()
# Pop up a 'press any button to continue' statement after our # Pop up a 'press any button to continue' statement after our
@ -178,24 +186,30 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
else: else:
sval = _language.Lstr(resource='pressAnyButtonText') sval = _language.Lstr(resource='pressAnyButtonText')
Text(self._custom_continue_message Text(
if self._custom_continue_message is not None else sval, self._custom_continue_message
v_attach=Text.VAttach.BOTTOM, if self._custom_continue_message is not None
h_align=Text.HAlign.CENTER, else sval,
flash=True, v_attach=Text.VAttach.BOTTOM,
vr_depth=50, h_align=Text.HAlign.CENTER,
position=(0, 10), flash=True,
scale=0.8, vr_depth=50,
color=(0.5, 0.7, 0.5, 0.5), position=(0, 10),
transition=Text.Transition.IN_BOTTOM_SLOW, scale=0.8,
transition_delay=self._min_view_time).autoretain() 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: def _player_press(self) -> None:
# If this activity is a good 'end point', ask server-mode just once if # 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. # 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 if (
and self._server_transitioning is None): 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() self._server_transitioning = _ba.app.server.handle_transition()
assert isinstance(self._server_transitioning, bool) 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. # Just to be extra careful, don't assign if we're transitioning out.
# (though theoretically that should be ok). # (though theoretically that should be ok).
if not self.is_transitioning_out() and player: if not self.is_transitioning_out() and player:
player.assigninput((InputType.JUMP_PRESS, InputType.PUNCH_PRESS, player.assigninput(
InputType.BOMB_PRESS, InputType.PICK_UP_PRESS), (
self._player_press) 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. # Print this message once every 10 minutes at most.
tval = _ba.time(TimeType.REAL) tval = _ba.time(TimeType.REAL)
if (self.last_in_game_ad_remove_message_show_time is None or 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)): tval - self.last_in_game_ad_remove_message_show_time > 60 * 10
):
self.last_in_game_ad_remove_message_show_time = tval self.last_in_game_ad_remove_message_show_time = tval
with _ba.Context('ui'): with _ba.Context('ui'):
_ba.timer( _ba.timer(
1.0, 1.0,
lambda: _ba.screenmessage(Lstr( lambda: _ba.screenmessage(
resource='removeInGameAdsText', Lstr(
subs=[('${PRO}', resource='removeInGameAdsText',
Lstr(resource='store.bombSquadProNameText')), subs=[
('${APP_NAME}', Lstr(resource='titleText'))]), (
color=(1, 1, 0)), '${PRO}',
timetype=TimeType.REAL) Lstr(resource='store.bombSquadProNameText'),
),
('${APP_NAME}', Lstr(resource='titleText')),
],
),
color=(1, 1, 0),
),
timetype=TimeType.REAL,
)
def show_ad(self, def show_ad(
purpose: str, self, purpose: str, on_completion_call: Callable[[], Any] | None = None
on_completion_call: Callable[[], Any] | None = None) -> None: ) -> None:
"""(internal)""" """(internal)"""
self.last_ad_purpose = purpose self.last_ad_purpose = purpose
_ba.show_ad(purpose, on_completion_call) _ba.show_ad(purpose, on_completion_call)
def show_ad_2( def show_ad_2(
self, self,
purpose: str, purpose: str,
on_completion_call: Callable[[bool], Any] | None = None) -> None: on_completion_call: Callable[[bool], Any] | None = None,
) -> None:
"""(internal)""" """(internal)"""
self.last_ad_purpose = purpose self.last_ad_purpose = purpose
_ba.show_ad_2(purpose, on_completion_call) _ba.show_ad_2(purpose, on_completion_call)
@ -73,6 +83,7 @@ class AdsSubsystem:
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
from ba._generated.enums import TimeType from ba._generated.enums import TimeType
app = _ba.app app = _ba.app
show = True show = True
@ -95,16 +106,22 @@ class AdsSubsystem:
launch_count = app.config.get('launchCount', 0) launch_count = app.config.get('launchCount', 0)
# If we're seeing short ads we may want to space them differently. # If we're seeing short ads we may want to space them differently.
interval_mult = (_internal.get_v1_account_misc_read_val( interval_mult = (
'ads.shortIntervalMult', 1.0) _internal.get_v1_account_misc_read_val(
if self.last_ad_was_short else 1.0) 'ads.shortIntervalMult', 1.0
)
if self.last_ad_was_short
else 1.0
)
if self.ad_amt is None: if self.ad_amt is None:
if launch_count <= 1: if launch_count <= 1:
self.ad_amt = _internal.get_v1_account_misc_read_val( self.ad_amt = _internal.get_v1_account_misc_read_val(
'ads.startVal1', 0.99) 'ads.startVal1', 0.99
)
else: else:
self.ad_amt = _internal.get_v1_account_misc_read_val( self.ad_amt = _internal.get_v1_account_misc_read_val(
'ads.startVal2', 1.0) 'ads.startVal2', 1.0
)
interval = None interval = None
else: else:
# So far we're cleared to show; now calc our # So far we're cleared to show; now calc our
@ -113,27 +130,33 @@ class AdsSubsystem:
# playing). # playing).
base = 'ads' if _ba.has_video_ads() else 'ads2' base = 'ads' if _ba.has_video_ads() else 'ads2'
min_lc = _internal.get_v1_account_misc_read_val( 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( max_lc = _internal.get_v1_account_misc_read_val(
base + '.maxLC', 5.0) base + '.maxLC', 5.0
min_lc_scale = (_internal.get_v1_account_misc_read_val( )
base + '.minLCScale', 0.25)) min_lc_scale = _internal.get_v1_account_misc_read_val(
max_lc_scale = (_internal.get_v1_account_misc_read_val( base + '.minLCScale', 0.25
base + '.maxLCScale', 0.34)) )
min_lc_interval = (_internal.get_v1_account_misc_read_val( max_lc_scale = _internal.get_v1_account_misc_read_val(
base + '.minLCInterval', 360)) base + '.maxLCScale', 0.34
max_lc_interval = (_internal.get_v1_account_misc_read_val( )
base + '.maxLCInterval', 300)) 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: if launch_count < min_lc:
lc_amt = 0.0 lc_amt = 0.0
elif launch_count > max_lc: elif launch_count > max_lc:
lc_amt = 1.0 lc_amt = 1.0
else: else:
lc_amt = ((float(launch_count) - min_lc) / lc_amt = (float(launch_count) - min_lc) / (max_lc - min_lc)
(max_lc - min_lc))
incr = (1.0 - lc_amt) * min_lc_scale + lc_amt * max_lc_scale incr = (1.0 - lc_amt) * min_lc_scale + lc_amt * max_lc_scale
interval = ((1.0 - lc_amt) * min_lc_interval + interval = (
lc_amt * max_lc_interval) 1.0 - lc_amt
) * min_lc_interval + lc_amt * max_lc_interval
self.ad_amt += incr self.ad_amt += incr
assert self.ad_amt is not None assert self.ad_amt is not None
if self.ad_amt >= 1.0: if self.ad_amt >= 1.0:
@ -143,12 +166,14 @@ class AdsSubsystem:
# After we've reached the traditional show-threshold once, # After we've reached the traditional show-threshold once,
# try again whenever its been INTERVAL since our last successful # try again whenever its been INTERVAL since our last successful
# show. # show.
elif ( elif self.attempted_first_ad and (
self.attempted_first_ad and self.last_ad_completion_time is None
(self.last_ad_completion_time is None or or (
(interval is not None interval is not None
and _ba.time(TimeType.REAL) - self.last_ad_completion_time > and _ba.time(TimeType.REAL) - self.last_ad_completion_time
(interval * interval_mult)))): > (interval * interval_mult)
)
):
# Reset our other counter too in this case. # Reset our other counter too in this case.
self.ad_amt = 0.0 self.ad_amt = 0.0
else: else:
@ -161,7 +186,6 @@ class AdsSubsystem:
# (in case some random ad network doesn't properly deliver its # (in case some random ad network doesn't properly deliver its
# completion callback). # completion callback).
class _Payload: class _Payload:
def __init__(self, pcall: Callable[[], Any]): def __init__(self, pcall: Callable[[], Any]):
self._call = pcall self._call = pcall
self._ran = False self._ran = False
@ -171,20 +195,31 @@ class AdsSubsystem:
if not self._ran: if not self._ran:
if fallback: if fallback:
print( print(
('ERROR: relying on fallback ad-callback! ' (
'last network: ' + app.ads.last_ad_network + 'ERROR: relying on fallback ad-callback! '
' (set ' + str( 'last network: '
int(time.time() - + app.ads.last_ad_network
app.ads.last_ad_network_set_time)) + + ' (set '
's ago); purpose=' + app.ads.last_ad_purpose)) + str(
int(
time.time()
- app.ads.last_ad_network_set_time
)
)
+ 's ago); purpose='
+ app.ads.last_ad_purpose
)
)
_ba.pushcall(self._call) _ba.pushcall(self._call)
self._ran = True self._ran = True
payload = _Payload(call) payload = _Payload(call)
with _ba.Context('ui'): with _ba.Context('ui'):
_ba.timer(5.0, _ba.timer(
lambda: payload.run(fallback=True), 5.0,
timetype=TimeType.REAL) lambda: payload.run(fallback=True),
timetype=TimeType.REAL,
)
self.show_ad('between_game', on_completion_call=payload.run) self.show_ad('between_game', on_completion_call=payload.run)
else: else:
_ba.pushcall(call) # Just run the callback without the ad. _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._freeforallsession import FreeForAllSession
from ba._coopsession import CoopSession from ba._coopsession import CoopSession
from ba._gameactivity import GameActivity from ba._gameactivity import GameActivity
activity = _ba.getactivity(False) activity = _ba.getactivity(False)
session = _ba.getsession(False) session = _ba.getsession(False)
@ -31,8 +32,11 @@ def game_begin_analytics() -> None:
campaign = session.campaign campaign = session.campaign
assert campaign is not None assert campaign is not None
_ba.set_analytics_screen( _ba.set_analytics_screen(
'Coop Game: ' + campaign.name + ' ' + 'Coop Game: '
campaign.getlevel(_ba.app.coop_session_args['level']).name) + campaign.name
+ ' '
+ campaign.getlevel(_ba.app.coop_session_args['level']).name
)
_ba.increment_analytics_count('Co-op round start') _ba.increment_analytics_count('Co-op round start')
if len(activity.players) == 1: if len(activity.players) == 1:
_ba.increment_analytics_count('Co-op round start 1 human player') _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: if len(activity.players) == 1:
_ba.increment_analytics_count('Teams round start 1 human player') _ba.increment_analytics_count('Teams round start 1 human player')
elif 1 < len(activity.players) < 8: elif 1 < len(activity.players) < 8:
_ba.increment_analytics_count('Teams round start ' + _ba.increment_analytics_count(
str(len(activity.players)) + 'Teams round start '
' human players') + str(len(activity.players))
+ ' human players'
)
elif len(activity.players) >= 8: elif len(activity.players) >= 8:
_ba.increment_analytics_count('Teams round start 8+ human players') _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') _ba.increment_analytics_count('Free-for-all round start')
if len(activity.players) == 1: if len(activity.players) == 1:
_ba.increment_analytics_count( _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: elif 1 < len(activity.players) < 8:
_ba.increment_analytics_count('Free-for-all round start ' + _ba.increment_analytics_count(
str(len(activity.players)) + 'Free-for-all round start '
' human players') + str(len(activity.players))
+ ' human players'
)
elif len(activity.players) >= 8: elif len(activity.players) >= 8:
_ba.increment_analytics_count( _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. # For some analytics tracking on the c layer.
_ba.reset_game_activity_tracking() _ba.reset_game_activity_tracking()

View File

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

View File

@ -17,9 +17,12 @@ class AppDelegate:
""" """
def create_default_game_settings_ui( def create_default_game_settings_ui(
self, gameclass: type[ba.GameActivity], self,
sessiontype: type[ba.Session], settings: dict | None, gameclass: type[ba.GameActivity],
completion_call: Callable[[dict | None], None]) -> None: sessiontype: type[ba.Session],
settings: dict | None,
completion_call: Callable[[dict | None], None],
) -> None:
"""Launch a UI to configure the given game config. """Launch a UI to configure the given game config.
It should manipulate the contents of config and call completion_call It should manipulate the contents of config and call completion_call
@ -27,5 +30,7 @@ class AppDelegate:
""" """
del gameclass, sessiontype, settings, completion_call # Unused. del gameclass, sessiontype, settings, completion_call # Unused.
from ba import _error from ba import _error
_error.print_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: def get_remote_app_name() -> ba.Lstr:
"""(internal)""" """(internal)"""
from ba import _language from ba import _language
return _language.Lstr(resource='remote_app.app_name') 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._net import master_server_post
from ba._generated.enums import TimeType from ba._generated.enums import TimeType
from ba._internal import get_news_show from ba._internal import get_news_show
app = _ba.app app = _ba.app
app.log_have_new = True app.log_have_new = True
if not app.log_upload_timer_started: if not app.log_upload_timer_started:
@ -112,10 +114,12 @@ def handle_v1_cloud_log() -> None:
if not _ba.is_log_full(): if not _ba.is_log_full():
with _ba.Context('ui'): with _ba.Context('ui'):
_ba.timer(600.0, _ba.timer(
_reset, 600.0,
timetype=TimeType.REAL, _reset,
suppress_format_warning=True) timetype=TimeType.REAL,
suppress_format_warning=True,
)
def handle_leftover_v1_cloud_log_file() -> None: 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 from ba._net import master_server_post
if os.path.exists(_ba.get_v1_cloud_log_file_path()): if os.path.exists(_ba.get_v1_cloud_log_file_path()):
with open(_ba.get_v1_cloud_log_file_path(), with open(
encoding='utf-8') as infile: _ba.get_v1_cloud_log_file_path(), encoding='utf-8'
) as infile:
info = json.loads(infile.read()) info = json.loads(infile.read())
infile.close() infile.close()
do_send = should_submit_debug_info() 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()) os.remove(_ba.get_v1_cloud_log_file_path())
except Exception: except Exception:
from ba import _error from ba import _error
_error.print_exception('Error handling leftover log file.') _error.print_exception('Error handling leftover log file.')
@ -180,9 +186,10 @@ def garbage_collect() -> None:
def print_live_object_warnings( def print_live_object_warnings(
when: Any, when: Any,
ignore_session: ba.Session | None = None, ignore_session: ba.Session | None = None,
ignore_activity: ba.Activity | None = None) -> None: ignore_activity: ba.Activity | None = None,
) -> None:
"""Print warnings for remaining objects in the current context.""" """Print warnings for remaining objects in the current context."""
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from ba._session import Session from ba._session import Session
@ -229,13 +236,17 @@ def print_corrupt_file_error() -> None:
"""Print an error if a corrupt file is found.""" """Print an error if a corrupt file is found."""
from ba._general import Call from ba._general import Call
from ba._generated.enums import TimeType from ba._generated.enums import TimeType
_ba.timer(2.0,
lambda: _ba.screenmessage( _ba.timer(
_ba.app.lang.get_resource('internal.corruptFileText'). 2.0,
replace('${EMAIL}', 'support@froemling.net'), lambda: _ba.screenmessage(
color=(1, 0, 0), _ba.app.lang.get_resource('internal.corruptFileText').replace(
), '${EMAIL}', 'support@froemling.net'
timetype=TimeType.REAL) ),
_ba.timer(2.0, color=(1, 0, 0),
Call(_ba.playsound, _ba.getsound('error')), ),
timetype=TimeType.REAL) 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 os
import sys import sys
from efro.dataclassio import (ioprepped, IOAttrs, dataclass_from_json, from efro.dataclassio import (
dataclass_to_json) ioprepped,
IOAttrs,
dataclass_from_json,
dataclass_to_json,
)
import _ba import _ba
if TYPE_CHECKING: if TYPE_CHECKING:
@ -34,8 +38,9 @@ class FileValue:
class State: class State:
"""Holds all persistent state for the asset-manager.""" """Holds all persistent state for the asset-manager."""
files: Annotated[dict[str, FileValue], files: Annotated[dict[str, FileValue], IOAttrs('files')] = field(
IOAttrs('files')] = field(default_factory=dict) default_factory=dict
)
class AssetManager: class AssetManager:
@ -66,8 +71,14 @@ class AssetManager:
account_token: str, account_token: str,
) -> AssetGather: ) -> AssetGather:
"""Spawn an asset-gather operation from this manager.""" """Spawn an asset-gather operation from this manager."""
print('would gather', packages, 'and flavor', flavor, 'with token', print(
account_token) 'would gather',
packages,
'and flavor',
flavor,
'with token',
account_token,
)
return AssetGather(self) return AssetGather(self)
def update(self) -> None: def update(self) -> None:
@ -162,9 +173,7 @@ class AssetGather:
def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None: 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 # pylint: disable=consider-using-with
import socket 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 # Pass a very short timeout to urllib so we have opportunities
# to cancel even with network blockage. # to cancel even with network blockage.
req = urllib.request.urlopen(url, req = urllib.request.urlopen(url, context=_ba.app.net.sslcontext, timeout=1)
context=_ba.app.net.sslcontext,
timeout=1)
file_size = int(req.headers['Content-Length']) file_size = int(req.headers['Content-Length'])
print(f'\nDownloading: {filename} Bytes: {file_size:,}') 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) data = req.read(block_sz)
except ValueError: except ValueError:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
print('VALUEERROR', flush=True) print('VALUEERROR', flush=True)
break break
@ -210,8 +218,10 @@ def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None:
print('\n\n\nsorry -- try back later') print('\n\n\nsorry -- try back later')
os.unlink(filename) os.unlink(filename)
raise raise
print('\nHmmm... little issue... ' print(
'I\'ll wait a couple of seconds') '\nHmmm... little issue... '
'I\'ll wait a couple of seconds'
)
time.sleep(3) time.sleep(3)
time_outs += 1 time_outs += 1
continue continue

View File

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

View File

@ -50,31 +50,42 @@ def run_cpu_benchmark() -> None:
_ba.new_host_session(BenchmarkSession, benchmark_type='cpu') _ba.new_host_session(BenchmarkSession, benchmark_type='cpu')
def run_stress_test(playlist_type: str = 'Random', def run_stress_test(
playlist_name: str = '__default__', playlist_type: str = 'Random',
player_count: int = 8, playlist_name: str = '__default__',
round_duration: int = 30) -> None: player_count: int = 8,
round_duration: int = 30,
) -> None:
"""Run a stress test.""" """Run a stress test."""
from ba import modutils from ba import modutils
from ba._general import Call from ba._general import Call
from ba._generated.enums import TimeType from ba._generated.enums import TimeType
_ba.screenmessage( _ba.screenmessage(
'Beginning stress test.. use ' "Beginning stress test.. use 'End Test' to stop testing.",
"'End Test' to stop testing.", color=(1, 1, 0),
color=(1, 1, 0)) )
with _ba.Context('ui'): with _ba.Context('ui'):
start_stress_test({ start_stress_test(
'playlist_type': playlist_type, {
'playlist_name': playlist_name, 'playlist_type': playlist_type,
'player_count': player_count, 'playlist_name': playlist_name,
'round_duration': round_duration 'player_count': player_count,
}) 'round_duration': round_duration,
_ba.timer(7.0, }
Call(_ba.screenmessage, )
('stats will be written to ' + _ba.timer(
modutils.get_human_readable_user_scripts_path() + 7.0,
'/stress_test_stats.csv')), Call(
timetype=TimeType.REAL) _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: 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._dualteamsession import DualTeamSession
from ba._freeforallsession import FreeForAllSession from ba._freeforallsession import FreeForAllSession
from ba._generated.enums import TimeType, TimeFormat from ba._generated.enums import TimeType, TimeFormat
appconfig = _ba.app.config appconfig = _ba.app.config
playlist_type = args['playlist_type'] playlist_type = args['playlist_type']
if playlist_type == 'Random': if playlist_type == 'Random':
@ -101,33 +113,42 @@ def start_stress_test(args: dict[str, Any]) -> None:
playlist_type = 'Teams' playlist_type = 'Teams'
else: else:
playlist_type = 'Free-For-All' playlist_type = 'Free-For-All'
_ba.screenmessage('Running Stress Test (listType="' + playlist_type + _ba.screenmessage(
'", listName="' + args['playlist_name'] + '")...') 'Running Stress Test (listType="'
+ playlist_type
+ '", listName="'
+ args['playlist_name']
+ '")...'
)
if playlist_type == 'Teams': if playlist_type == 'Teams':
appconfig['Team Tournament Playlist Selection'] = args['playlist_name'] appconfig['Team Tournament Playlist Selection'] = args['playlist_name']
appconfig['Team Tournament Playlist Randomize'] = 1 appconfig['Team Tournament Playlist Randomize'] = 1
_ba.timer(1.0, _ba.timer(
Call(_ba.pushcall, Call(_ba.new_host_session, 1.0,
DualTeamSession)), Call(_ba.pushcall, Call(_ba.new_host_session, DualTeamSession)),
timetype=TimeType.REAL) timetype=TimeType.REAL,
)
else: else:
appconfig['Free-for-All Playlist Selection'] = args['playlist_name'] appconfig['Free-for-All Playlist Selection'] = args['playlist_name']
appconfig['Free-for-All Playlist Randomize'] = 1 appconfig['Free-for-All Playlist Randomize'] = 1
_ba.timer(1.0, _ba.timer(
Call(_ba.pushcall, 1.0,
Call(_ba.new_host_session, FreeForAllSession)), Call(_ba.pushcall, Call(_ba.new_host_session, FreeForAllSession)),
timetype=TimeType.REAL) timetype=TimeType.REAL,
)
_ba.set_stress_testing(True, args['player_count']) _ba.set_stress_testing(True, args['player_count'])
_ba.app.stress_test_reset_timer = _ba.Timer( _ba.app.stress_test_reset_timer = _ba.Timer(
args['round_duration'] * 1000, args['round_duration'] * 1000,
Call(_reset_stress_test, args), Call(_reset_stress_test, args),
timetype=TimeType.REAL, timetype=TimeType.REAL,
timeformat=TimeFormat.MILLISECONDS) timeformat=TimeFormat.MILLISECONDS,
)
def _reset_stress_test(args: dict[str, Any]) -> None: def _reset_stress_test(args: dict[str, Any]) -> None:
from ba._general import Call from ba._general import Call
from ba._generated.enums import TimeType from ba._generated.enums import TimeType
_ba.set_stress_testing(False, args['player_count']) _ba.set_stress_testing(False, args['player_count'])
_ba.screenmessage('Resetting stress test...') _ba.screenmessage('Resetting stress test...')
session = _ba.get_foreground_host_session() 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.""" """Kick off a benchmark to test media reloading speeds."""
from ba._general import Call from ba._general import Call
from ba._generated.enums import TimeType from ba._generated.enums import TimeType
_ba.reload_media() _ba.reload_media()
_ba.show_progress_bar() _ba.show_progress_bar()
def delay_add(start_time: float) -> None: def delay_add(start_time: float) -> None:
def doit(start_time_2: float) -> None: def doit(start_time_2: float) -> None:
_ba.screenmessage( _ba.screenmessage(
_ba.app.lang.get_resource( _ba.app.lang.get_resource(
'debugWindow.totalReloadTimeText').replace( 'debugWindow.totalReloadTimeText'
'${TIME}', ).replace(
str(_ba.time(TimeType.REAL) - start_time_2))) '${TIME}', str(_ba.time(TimeType.REAL) - start_time_2)
)
)
_ba.print_load_info() _ba.print_load_info()
if _ba.app.config.resolve('Texture Quality') != 'High': if _ba.app.config.resolve('Texture Quality') != 'High':
_ba.screenmessage(_ba.app.lang.get_resource( _ba.screenmessage(
'debugWindow.reloadBenchmarkBestResultsText'), _ba.app.lang.get_resource(
color=(1, 1, 0)) 'debugWindow.reloadBenchmarkBestResultsText'
),
color=(1, 1, 0),
)
_ba.add_clean_frame_callback(Call(doit, start_time)) _ba.add_clean_frame_callback(Call(doit, start_time))
# The reload starts (should add a completion callback to the # The reload starts (should add a completion callback to the
# reload func to fix this). # reload func to fix this).
_ba.timer(0.05, _ba.timer(
Call(delay_add, _ba.time(TimeType.REAL)), 0.05, Call(delay_add, _ba.time(TimeType.REAL)), timetype=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 # Python's stdout/stderr into it. Then we can at least debug problems
# on systems where native stdout/stderr is not easily accessible # on systems where native stdout/stderr is not easily accessible
# such as Android. # such as Android.
log_handler = setup_logging(log_path=None, log_handler = setup_logging(
level=LogLevel.DEBUG, log_path=None,
suppress_non_root_debug=True, level=LogLevel.DEBUG,
log_stdout_stderr=True, suppress_non_root_debug=True,
cache_size_limit=1024 * 1024) log_stdout_stderr=True,
cache_size_limit=1024 * 1024,
)
log_handler.add_callback(_on_log) log_handler.add_callback(_on_log)
@ -53,7 +55,8 @@ def bootstrap() -> None:
f' Ballistica build {expected_build}.\n' f' Ballistica build {expected_build}.\n'
f' You are running build {running_build}.' f' You are running build {running_build}.'
f' This might cause the app to error or misbehave.', 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 # 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 # (because that must be done in the main thread). Now we finish the
@ -69,7 +72,8 @@ def bootstrap() -> None:
print( print(
'ERROR: Python\'s UTF-8 mode is not set.' 'ERROR: Python\'s UTF-8 mode is not set.'
' This will likely result in errors.', ' This will likely result in errors.',
file=sys.stderr) file=sys.stderr,
)
debug_build = env['debug_build'] debug_build = env['debug_build']
@ -78,20 +82,24 @@ def bootstrap() -> None:
print( print(
f'WARNING: Mismatch in debug_build {debug_build}' f'WARNING: Mismatch in debug_build {debug_build}'
f' and sys.flags.dev_mode {sys.flags.dev_mode}', 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 # 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 # 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 # this in particular embedded cases if we can verify that system certs
# are working. # are working.
# (We also allow forcing this via an env var if the user desires) # (We also allow forcing this via an env var if the user desires)
if (_ba.contains_python_dist() if (
or os.environ.get('BA_USE_BUNDLED_ROOT_CERTS') == '1'): _ba.contains_python_dist()
or os.environ.get('BA_USE_BUNDLED_ROOT_CERTS') == '1'
):
import certifi import certifi
# Let both OpenSSL and requests (if present) know to use this. # Let both OpenSSL and requests (if present) know to use this.
os.environ['SSL_CERT_FILE'] = os.environ['REQUESTS_CA_BUNDLE'] = ( os.environ['SSL_CERT_FILE'] = os.environ[
certifi.where()) 'REQUESTS_CA_BUNDLE'
] = certifi.where()
# On Windows I'm seeing the following error creating asyncio loops in # On Windows I'm seeing the following error creating asyncio loops in
# background threads with the default proactor setup: # background threads with the default proactor setup:
@ -104,6 +112,7 @@ def bootstrap() -> None:
# to default to selector in that case?.. # to default to selector in that case?..
if sys.platform == 'win32': if sys.platform == 'win32':
import asyncio import asyncio
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
# pylint: disable=c-extension-no-member # 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. # Now spin up our App instance and store it on both _ba and ba.
from ba._app import App from ba._app import App
import ba import ba
_ba.app = ba.app = App() _ba.app = ba.app = App()
_ba.app.log_handler = log_handler _ba.app.log_handler = log_handler
@ -138,6 +148,7 @@ class _CustomHelper:
# (but then things mostly work). Let's get the ugly error out # (but then things mostly work). Let's get the ugly error out
# of the way explicitly. # of the way explicitly.
import sysconfig import sysconfig
try: try:
# This errors once but seems to run cleanly after, so let's # This errors once but seems to run cleanly after, so let's
# get the error out of the way. # get the error out of the way.
@ -146,13 +157,16 @@ class _CustomHelper:
pass pass
import pydoc import pydoc
# Disable pager and interactive help since neither works well # Disable pager and interactive help since neither works well
# with our funky multi-threaded setup or in-game/cloud consoles. # with our funky multi-threaded setup or in-game/cloud consoles.
# Let's just do simple text dumps. # Let's just do simple text dumps.
pydoc.pager = pydoc.plainpager pydoc.pager = pydoc.plainpager
if not args and not kwds: if not args and not kwds:
print('Interactive help is not available in this environment.\n' print(
'Type help(object) for help about object.') 'Interactive help is not available in this environment.\n'
'Type help(object) for help about object.'
)
return None return None
return pydoc.help(*args, **kwds) 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. # 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 # Let's go with anything warning or higher as well as the stdout/stderr
# log messages that ba.app.log_handler creates for us. # log messages that ba.app.log_handler creates for us.
if entry.level.value >= LogLevel.WARNING.value or entry.name in ('stdout', if entry.level.value >= LogLevel.WARNING.value or entry.name in (
'stderr'): 'stdout',
'stderr',
):
_ba.v1_cloud_log(entry.message) _ba.v1_cloud_log(entry.message)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -21,7 +21,7 @@ TROPHY_CHARS = {
'3': SpecialChar.TROPHY3, '3': SpecialChar.TROPHY3,
'0a': SpecialChar.TROPHY0A, '0a': SpecialChar.TROPHY0A,
'0b': SpecialChar.TROPHY0B, '0b': SpecialChar.TROPHY0B,
'4': SpecialChar.TROPHY4 '4': SpecialChar.TROPHY4,
} }
@ -31,6 +31,7 @@ class GameTip:
Category: **Gameplay Classes** Category: **Gameplay Classes**
""" """
text: str text: str
icon: ba.Texture | None = None icon: ba.Texture | None = None
sound: ba.Sound | None = None sound: ba.Sound | None = None
@ -43,14 +44,16 @@ def get_trophy_string(trophy_id: str) -> str:
return '?' return '?'
def animate(node: ba.Node, def animate(
attr: str, node: ba.Node,
keys: dict[float, float], attr: str,
loop: bool = False, keys: dict[float, float],
offset: float = 0, loop: bool = False,
timetype: ba.TimeType = TimeType.SIM, offset: float = 0,
timeformat: ba.TimeFormat = TimeFormat.SECONDS, timetype: ba.TimeType = TimeType.SIM,
suppress_format_warning: bool = False) -> ba.Node: timeformat: ba.TimeFormat = TimeFormat.SECONDS,
suppress_format_warning: bool = False,
) -> ba.Node:
"""Animate values on a target ba.Node. """Animate values on a target ba.Node.
Category: **Gameplay Functions** Category: **Gameplay Functions**
@ -76,9 +79,11 @@ def animate(node: ba.Node,
for item in items: for item in items:
_ba.time_format_check(timeformat, item[0]) _ba.time_format_check(timeformat, item[0])
curve = _ba.newnode('animcurve', curve = _ba.newnode(
owner=node, 'animcurve',
name='Driving ' + str(node) + ' \'' + attr + '\'') owner=node,
name='Driving ' + str(node) + ' \'' + attr + '\'',
)
if timeformat is TimeFormat.SECONDS: if timeformat is TimeFormat.SECONDS:
mult = 1000 mult = 1000
@ -89,7 +94,8 @@ def animate(node: ba.Node,
curve.times = [int(mult * time) for time, val in items] curve.times = [int(mult * time) for time, val in items]
curve.offset = _ba.time(timeformat=TimeFormat.MILLISECONDS) + int( curve.offset = _ba.time(timeformat=TimeFormat.MILLISECONDS) + int(
mult * offset) mult * offset
)
curve.values = [val for time, val in items] curve.values = [val for time, val in items]
curve.loop = loop curve.loop = loop
@ -99,9 +105,11 @@ def animate(node: ba.Node,
# get disconnected. # get disconnected.
if not loop: if not loop:
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
_ba.timer(int(mult * items[-1][0]) + 1000, _ba.timer(
curve.delete, int(mult * items[-1][0]) + 1000,
timeformat=TimeFormat.MILLISECONDS) curve.delete,
timeformat=TimeFormat.MILLISECONDS,
)
# Do the connects last so all our attrs are in place when we push initial # Do the connects last so all our attrs are in place when we push initial
# values through. # values through.
@ -117,15 +125,17 @@ def animate(node: ba.Node,
return curve return curve
def animate_array(node: ba.Node, def animate_array(
attr: str, node: ba.Node,
size: int, attr: str,
keys: dict[float, Sequence[float]], size: int,
loop: bool = False, keys: dict[float, Sequence[float]],
offset: float = 0, loop: bool = False,
timetype: ba.TimeType = TimeType.SIM, offset: float = 0,
timeformat: ba.TimeFormat = TimeFormat.SECONDS, timetype: ba.TimeType = TimeType.SIM,
suppress_format_warning: bool = False) -> None: timeformat: ba.TimeFormat = TimeFormat.SECONDS,
suppress_format_warning: bool = False,
) -> None:
"""Animate an array of values on a target ba.Node. """Animate an array of values on a target ba.Node.
Category: **Gameplay Functions** Category: **Gameplay Functions**
@ -163,15 +173,19 @@ def animate_array(node: ba.Node,
globalsnode = _ba.getsession().sessionglobalsnode globalsnode = _ba.getsession().sessionglobalsnode
for i in range(size): for i in range(size):
curve = _ba.newnode('animcurve', curve = _ba.newnode(
owner=node, 'animcurve',
name=('Driving ' + str(node) + ' \'' + attr + owner=node,
'\' member ' + str(i))) name=(
'Driving ' + str(node) + ' \'' + attr + '\' member ' + str(i)
),
)
globalsnode.connectattr(driver, curve, 'in') globalsnode.connectattr(driver, curve, 'in')
curve.times = [int(mult * time) for time, val in items] curve.times = [int(mult * time) for time, val in items]
curve.values = [val[i] for time, val in items] curve.values = [val[i] for time, val in items]
curve.offset = _ba.time(timeformat=TimeFormat.MILLISECONDS) + int( curve.offset = _ba.time(timeformat=TimeFormat.MILLISECONDS) + int(
mult * offset) mult * offset
)
curve.loop = loop curve.loop = loop
curve.connectattr('out', combine, 'input' + str(i)) curve.connectattr('out', combine, 'input' + str(i))
@ -180,9 +194,11 @@ def animate_array(node: ba.Node,
if not loop: if not loop:
# (PyCharm seems to think item is a float, not a tuple) # (PyCharm seems to think item is a float, not a tuple)
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
_ba.timer(int(mult * items[-1][0]) + 1000, _ba.timer(
curve.delete, int(mult * items[-1][0]) + 1000,
timeformat=TimeFormat.MILLISECONDS) curve.delete,
timeformat=TimeFormat.MILLISECONDS,
)
combine.connectattr('output', node, attr) combine.connectattr('output', node, attr)
# If we're not looping, set a timer to kill the combine once # 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: if not loop:
# (PyCharm seems to think item is a float, not a tuple) # (PyCharm seems to think item is a float, not a tuple)
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
_ba.timer(int(mult * items[-1][0]) + 1000, _ba.timer(
combine.delete, int(mult * items[-1][0]) + 1000,
timeformat=TimeFormat.MILLISECONDS) combine.delete,
timeformat=TimeFormat.MILLISECONDS,
)
def show_damage_count(damage: str, position: Sequence[float], def show_damage_count(
direction: Sequence[float]) -> None: damage: str, position: Sequence[float], direction: Sequence[float]
) -> None:
"""Pop up a damage count at a position in space. """Pop up a damage count at a position in space.
Category: **Gameplay Functions** 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 # (connected clients may have differing configs so they won't
# get the intended results). # get the intended results).
do_big = app.ui.uiscale is UIScale.SMALL or app.vr_mode do_big = app.ui.uiscale is UIScale.SMALL or app.vr_mode
txtnode = _ba.newnode('text', txtnode = _ba.newnode(
attrs={ 'text',
'text': damage, attrs={
'in_world': True, 'text': damage,
'h_align': 'center', 'in_world': True,
'flatness': 1.0, 'h_align': 'center',
'shadow': 1.0 if do_big else 0.7, 'flatness': 1.0,
'color': (1, 0.25, 0.25, 1), 'shadow': 1.0 if do_big else 0.7,
'scale': 0.015 if do_big else 0.01 'color': (1, 0.25, 0.25, 1),
}) 'scale': 0.015 if do_big else 0.01,
},
)
# Translate upward. # Translate upward.
tcombine = _ba.newnode('combine', owner=txtnode, attrs={'size': 3}) tcombine = _ba.newnode('combine', owner=txtnode, attrs={'size': 3})
tcombine.connectattr('output', txtnode, 'position') tcombine.connectattr('output', txtnode, 'position')
@ -233,27 +254,35 @@ def show_damage_count(damage: str, position: Sequence[float],
vval *= 0.5 vval *= 0.5
p_start = position[0] p_start = position[0]
p_dir = direction[0] p_dir = direction[0]
animate(tcombine, 'input0', animate(
{i[0] * lifespan: p_start + p_dir * i[1] tcombine,
for i in v_vals}) 'input0',
{i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals},
)
p_start = position[1] p_start = position[1]
p_dir = direction[1] p_dir = direction[1]
animate(tcombine, 'input1', animate(
{i[0] * lifespan: p_start + p_dir * i[1] tcombine,
for i in v_vals}) 'input1',
{i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals},
)
p_start = position[2] p_start = position[2]
p_dir = direction[2] p_dir = direction[2]
animate(tcombine, 'input2', animate(
{i[0] * lifespan: p_start + p_dir * i[1] tcombine,
for i in v_vals}) '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}) animate(txtnode, 'opacity', {0.7 * lifespan: 1.0, lifespan: 0.0})
_ba.timer(lifespan, txtnode.delete) _ba.timer(lifespan, txtnode.delete)
def timestring(timeval: float, def timestring(
centi: bool = True, timeval: float,
timeformat: ba.TimeFormat = TimeFormat.SECONDS, centi: bool = True,
suppress_format_warning: bool = False) -> ba.Lstr: timeformat: ba.TimeFormat = TimeFormat.SECONDS,
suppress_format_warning: bool = False,
) -> ba.Lstr:
"""Generate a ba.Lstr for displaying a time value. """Generate a ba.Lstr for displaying a time value.
Category: **General Utility Functions** Category: **General Utility Functions**
@ -292,32 +321,56 @@ def timestring(timeval: float,
hval = (timeval // 1000) // (60 * 60) hval = (timeval // 1000) // (60 * 60)
if hval != 0: if hval != 0:
bits.append('${H}') bits.append('${H}')
subs.append(('${H}', subs.append(
Lstr(resource='timeSuffixHoursText', (
subs=[('${COUNT}', str(hval))]))) '${H}',
Lstr(
resource='timeSuffixHoursText',
subs=[('${COUNT}', str(hval))],
),
)
)
mval = ((timeval // 1000) // 60) % 60 mval = ((timeval // 1000) // 60) % 60
if mval != 0: if mval != 0:
bits.append('${M}') bits.append('${M}')
subs.append(('${M}', subs.append(
Lstr(resource='timeSuffixMinutesText', (
subs=[('${COUNT}', str(mval))]))) '${M}',
Lstr(
resource='timeSuffixMinutesText',
subs=[('${COUNT}', str(mval))],
),
)
)
# We add seconds if its non-zero *or* we haven't added anything else. # We add seconds if its non-zero *or* we haven't added anything else.
if centi: if centi:
# pylint: disable=consider-using-f-string # 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: if sval >= 0.005 or not bits:
bits.append('${S}') bits.append('${S}')
subs.append(('${S}', subs.append(
Lstr(resource='timeSuffixSecondsText', (
subs=[('${COUNT}', ('%.2f' % sval))]))) '${S}',
Lstr(
resource='timeSuffixSecondsText',
subs=[('${COUNT}', ('%.2f' % sval))],
),
)
)
else: else:
sval = (timeval // 1000 % 60) sval = timeval // 1000 % 60
if sval != 0 or not bits: if sval != 0 or not bits:
bits.append('${S}') bits.append('${S}')
subs.append(('${S}', subs.append(
Lstr(resource='timeSuffixSecondsText', (
subs=[('${COUNT}', str(sval))]))) '${S}',
Lstr(
resource='timeSuffixSecondsText',
subs=[('${COUNT}', str(sval))],
),
)
)
return Lstr(value=' '.join(bits), subs=subs) return Lstr(value=' '.join(bits), subs=subs)
@ -332,11 +385,17 @@ def cameraflash(duration: float = 999.0) -> None:
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
import random import random
from ba._nodeactor import NodeActor from ba._nodeactor import NodeActor
x_spread = 10 x_spread = 10
y_spread = 5 y_spread = 5
positions = [[-x_spread, -y_spread], [0, -y_spread], [0, y_spread], positions = [
[x_spread, -y_spread], [x_spread, y_spread], [-x_spread, -y_spread],
[-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] times = [0, 2700, 1000, 1800, 500, 1400]
# Store this on the current activity so we only have one at a time. # 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 activity.camera_flash_data = [] # type: ignore
for i in range(6): for i in range(6):
light = NodeActor( light = NodeActor(
_ba.newnode('light', _ba.newnode(
attrs={ 'light',
'position': (positions[i][0], 0, positions[i][1]), attrs={
'radius': 1.0, 'position': (positions[i][0], 0, positions[i][1]),
'lights_volumes': False, 'radius': 1.0,
'height_attenuated': False, 'lights_volumes': False,
'color': (0.2, 0.2, 0.8) 'height_attenuated': False,
})) 'color': (0.2, 0.2, 0.8),
},
)
)
sval = 1.87 sval = 1.87
iscale = 1.3 iscale = 1.3
tcombine = _ba.newnode('combine', tcombine = _ba.newnode(
owner=light.node, 'combine',
attrs={ owner=light.node,
'size': 3, attrs={
'input0': positions[i][0], 'size': 3,
'input1': 0, 'input0': positions[i][0],
'input2': positions[i][1] 'input1': 0,
}) 'input2': positions[i][1],
},
)
assert light.node assert light.node
tcombine.connectattr('output', light.node, 'position') tcombine.connectattr('output', light.node, 'position')
xval = positions[i][0] xval = positions[i][0]
yval = positions[i][1] yval = positions[i][1]
spd = 0.5 + random.random() spd = 0.5 + random.random()
spd2 = 0.5 + random.random() spd2 = 0.5 + random.random()
animate(tcombine, animate(
'input0', { tcombine,
0.0: xval + 0, 'input0',
0.069 * spd: xval + 10.0, {
0.143 * spd: xval - 10.0, 0.0: xval + 0,
0.201 * spd: xval + 0 0.069 * spd: xval + 10.0,
}, 0.143 * spd: xval - 10.0,
loop=True) 0.201 * spd: xval + 0,
animate(tcombine, },
'input2', { loop=True,
0.0: yval + 0, )
0.15 * spd2: yval + 10.0, animate(
0.287 * spd2: yval - 10.0, tcombine,
0.398 * spd2: yval + 0 'input2',
}, {
loop=True) 0.0: yval + 0,
animate(light.node, 0.15 * spd2: yval + 10.0,
'intensity', { 0.287 * spd2: yval - 10.0,
0.0: 0, 0.398 * spd2: yval + 0,
0.02 * sval: 0, },
0.05 * sval: 0.8 * iscale, loop=True,
0.08 * sval: 0, )
0.1 * sval: 0 animate(
}, light.node,
loop=True, 'intensity',
offset=times[i]) {
_ba.timer((times[i] + random.randint(1, int(duration)) * 40 * sval), 0.0: 0,
light.node.delete, 0.02 * sval: 0,
timeformat=TimeFormat.MILLISECONDS) 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 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. 'subclassof' class, and a TypeError will be raised if not.
""" """
import importlib import importlib
splits = name.split('.') splits = name.split('.')
modulename = '.'.join(splits[:-1]) modulename = '.'.join(splits[:-1])
classname = splits[-1] classname = splits[-1]
@ -84,8 +85,10 @@ def json_prep(data: Any) -> Any:
""" """
if isinstance(data, dict): if isinstance(data, dict):
return dict((json_prep(key), json_prep(value)) return dict(
for key, value in list(data.items())) (json_prep(key), json_prep(value))
for key, value in list(data.items())
)
if isinstance(data, list): if isinstance(data, list):
return [json_prep(element) for element in data] return [json_prep(element) for element in data]
if isinstance(data, tuple): if isinstance(data, tuple):
@ -96,19 +99,23 @@ def json_prep(data: Any) -> Any:
return data.decode(errors='ignore') return data.decode(errors='ignore')
except Exception: except Exception:
from ba import _error from ba import _error
print_error('json_prep encountered utf-8 decode error', once=True) print_error('json_prep encountered utf-8 decode error', once=True)
return data.decode(errors='ignore') return data.decode(errors='ignore')
if not isinstance(data, (str, float, bool, type(None), int)): if not isinstance(data, (str, float, bool, type(None), int)):
print_error('got unsupported type in json_prep:' + str(type(data)), print_error(
once=True) 'got unsupported type in json_prep:' + str(type(data)), once=True
)
return data return data
def utf8_all(data: Any) -> Any: def utf8_all(data: Any) -> Any:
"""Convert any unicode data in provided sequence(s) to utf8 bytes.""" """Convert any unicode data in provided sequence(s) to utf8 bytes."""
if isinstance(data, dict): if isinstance(data, dict):
return dict((utf8_all(key), utf8_all(value)) return dict(
for key, value in list(data.items())) (utf8_all(key), utf8_all(value))
for key, value in list(data.items())
)
if isinstance(data, list): if isinstance(data, list):
return [utf8_all(element) for element in data] return [utf8_all(element) for element in data]
if isinstance(data, tuple): if isinstance(data, tuple):
@ -190,11 +197,17 @@ class _WeakCall:
else: else:
app = _ba.app app = _ba.app
if not app.did_weak_call_warning: if not app.did_weak_call_warning:
print(('Warning: callable passed to ba.WeakCall() is not' print(
' weak-referencable (' + str(args[0]) + (
'); use ba.Call() instead to avoid this ' 'Warning: callable passed to ba.WeakCall() is not'
'warning. Stack-trace:')) ' weak-referencable ('
+ str(args[0])
+ '); use ba.Call() instead to avoid this '
'warning. Stack-trace:'
)
)
import traceback import traceback
traceback.print_stack() traceback.print_stack()
app.did_weak_call_warning = True app.did_weak_call_warning = True
self._call = args[0] self._call = args[0]
@ -205,8 +218,15 @@ class _WeakCall:
return self._call(*self._args + args_extra, **self._keywds) return self._call(*self._args + args_extra, **self._keywds)
def __str__(self) -> str: def __str__(self) -> str:
return ('<ba.WeakCall object; _call=' + str(self._call) + ' _args=' + return (
str(self._args) + ' _keywds=' + str(self._keywds) + '>') '<ba.WeakCall object; _call='
+ str(self._call)
+ ' _args='
+ str(self._args)
+ ' _keywds='
+ str(self._keywds)
+ '>'
)
class _Call: class _Call:
@ -244,8 +264,15 @@ class _Call:
return self._call(*self._args + args_extra, **self._keywds) return self._call(*self._args + args_extra, **self._keywds)
def __str__(self) -> str: def __str__(self) -> str:
return ('<ba.Call object; _call=' + str(self._call) + ' _args=' + return (
str(self._args) + ' _keywds=' + str(self._keywds) + '>') '<ba.Call object; _call='
+ str(self._call)
+ ' _args='
+ str(self._args)
+ ' _keywds='
+ str(self._keywds)
+ '>'
)
if TYPE_CHECKING: if TYPE_CHECKING:
@ -278,7 +305,7 @@ class WeakMethod:
obj = self._obj() obj = self._obj()
if obj is None: if obj is None:
return None return None
return self._func(*((obj, ) + args), **keywds) return self._func(*((obj,) + args), **keywds)
def __str__(self) -> str: def __str__(self) -> str:
return '<ba.WeakMethod object; call=' + str(self._func) + '>' 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. # if we queue a lot of them.
delay = random.uniform(2.0, 5.5) delay = random.uniform(2.0, 5.5)
with _ba.Context('ui'): with _ba.Context('ui'):
_ba.timer(delay, _ba.timer(
lambda: _verify_object_death(ref), delay, lambda: _verify_object_death(ref), timetype=TimeType.REAL
timetype=TimeType.REAL) )
def print_active_refs(obj: Any) -> None: def print_active_refs(obj: Any) -> None:
@ -314,6 +341,7 @@ def print_active_refs(obj: Any) -> None:
""" """
# pylint: disable=too-many-nested-blocks # pylint: disable=too-many-nested-blocks
from types import FrameType, TracebackType from types import FrameType, TracebackType
refs = list(gc.get_referrers(obj)) refs = list(gc.get_referrers(obj))
print(f'{Clr.YLW}Active referrers to {obj}:{Clr.RST}') print(f'{Clr.YLW}Active referrers to {obj}:{Clr.RST}')
for i, ref in enumerate(refs): 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... # Can go further down the rabbit-hole if needed...
if bool(False): if bool(False):
if isinstance(ref2, TracebackType): if isinstance(ref2, TracebackType):
print(f'{Clr.YLW} ' print(
f'Active referrers to #a{j+1}:{Clr.RST}') f'{Clr.YLW} '
f'Active referrers to #a{j+1}:{Clr.RST}'
)
refs3 = list(gc.get_referrers(ref2)) refs3 = list(gc.get_referrers(ref2))
for k, ref3 in enumerate(refs3): for k, ref3 in enumerate(refs3):
print(f'{Clr.YLW} ' print(
f'#b{k+1}:{Clr.BLU} {ref3}{Clr.RST}') f'{Clr.YLW} '
f'#b{k+1}:{Clr.BLU} {ref3}{Clr.RST}'
)
if isinstance(ref3, BaseException): if isinstance(ref3, BaseException):
print(f'{Clr.YLW} Active referrers to' print(
f' #b{k+1}:{Clr.RST}') f'{Clr.YLW} Active referrers to'
f' #b{k+1}:{Clr.RST}'
)
refs4 = list(gc.get_referrers(ref3)) refs4 = list(gc.get_referrers(ref3))
for x, ref4 in enumerate(refs4): for x, ref4 in enumerate(refs4):
print(f'{Clr.YLW} #c{x+1}:{Clr.BLU}' print(
f' {ref4}{Clr.RST}') f'{Clr.YLW} #c{x+1}:{Clr.BLU}'
f' {ref4}{Clr.RST}'
)
def _verify_object_death(wref: weakref.ref) -> None: 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}') print(f'Note: unable to get type name for {obj}')
name = 'object' name = 'object'
print(f'{Clr.RED}Error: {name} not dying when expected to:' print(
f' {Clr.BLD}{obj}{Clr.RST}') f'{Clr.RED}Error: {name} not dying when expected to:'
f' {Clr.BLD}{obj}{Clr.RST}'
)
print_active_refs(obj) print_active_refs(obj)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Implements lobby system for gathering before games, char select, etc.""" """Implements lobby system for gathering before games, char select, etc."""
# pylint: disable=too-many-lines
from __future__ import annotations from __future__ import annotations
@ -31,15 +32,20 @@ class JoinInfo:
def __init__(self, lobby: ba.Lobby): def __init__(self, lobby: ba.Lobby):
from ba._nodeactor import NodeActor from ba._nodeactor import NodeActor
from ba._general import WeakCall from ba._general import WeakCall
self._state = 0 self._state = 0
self._press_to_punch: str | ba.Lstr = ('C' if _ba.app.iircade_mode else self._press_to_punch: str | ba.Lstr = (
_ba.charstr( 'C'
SpecialChar.LEFT_BUTTON)) if _ba.app.iircade_mode
self._press_to_bomb: str | ba.Lstr = ('B' if _ba.app.iircade_mode else else _ba.charstr(SpecialChar.LEFT_BUTTON)
_ba.charstr( )
SpecialChar.RIGHT_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') 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. # If we have a keyboard, grab keys for punch and pickup.
# FIXME: This of course is only correct on the local device; # 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 flatness = 1.0 if _ba.app.vr_mode else 0.0
self._text = NodeActor( self._text = NodeActor(
_ba.newnode('text', _ba.newnode(
attrs={ 'text',
'position': (0, -40), attrs={
'h_attach': 'center', 'position': (0, -40),
'v_attach': 'top', 'h_attach': 'center',
'h_align': 'center', 'v_attach': 'top',
'color': (0.7, 0.7, 0.95, 1.0), 'h_align': 'center',
'flatness': flatness, 'color': (0.7, 0.7, 0.95, 1.0),
'text': self._joinmsg 'flatness': flatness,
})) 'text': self._joinmsg,
},
)
)
if _ba.app.demo_mode or _ba.app.arcade_mode: if _ba.app.demo_mode or _ba.app.arcade_mode:
self._messages = [self._joinmsg] self._messages = [self._joinmsg]
else: else:
msg1 = Lstr(resource='pressToSelectProfileText', msg1 = Lstr(
subs=[ resource='pressToSelectProfileText',
('${BUTTONS}', _ba.charstr(SpecialChar.UP_ARROW) + subs=[
' ' + _ba.charstr(SpecialChar.DOWN_ARROW)) (
]) '${BUTTONS}',
msg2 = Lstr(resource='pressToOverrideCharacterText', _ba.charstr(SpecialChar.UP_ARROW)
subs=[('${BUTTONS}', Lstr(resource='bombBoldText'))]) + ' '
msg3 = Lstr(value='${A} < ${B} >', + _ba.charstr(SpecialChar.DOWN_ARROW),
subs=[('${A}', msg2), ('${B}', self._press_to_bomb)]) )
self._messages = (([ ],
Lstr( )
resource='pressToSelectTeamText', msg2 = Lstr(
subs=[('${BUTTONS}', _ba.charstr(SpecialChar.LEFT_ARROW) + resource='pressToOverrideCharacterText',
' ' + _ba.charstr(SpecialChar.RIGHT_ARROW))], 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) self._timer = _ba.Timer(4.0, WeakCall(self._update), repeat=True)
def _update_for_keyboard(self, keyboard: ba.InputDevice) -> None: def _update_for_keyboard(self, keyboard: ba.InputDevice) -> None:
from ba import _input from ba import _input
punch_key = keyboard.get_button_name( punch_key = keyboard.get_button_name(
_input.get_device_value(keyboard, 'buttonPunch')) _input.get_device_value(keyboard, 'buttonPunch')
self._press_to_punch = Lstr(resource='orText', )
subs=[('${A}', self._press_to_punch = Lstr(
Lstr(value='\'${K}\'', resource='orText',
subs=[('${K}', punch_key)])), subs=[
('${B}', self._press_to_punch)]) ('${A}', Lstr(value='\'${K}\'', subs=[('${K}', punch_key)])),
('${B}', self._press_to_punch),
],
)
bomb_key = keyboard.get_button_name( bomb_key = keyboard.get_button_name(
_input.get_device_value(keyboard, 'buttonBomb')) _input.get_device_value(keyboard, 'buttonBomb')
self._press_to_bomb = Lstr(resource='orText', )
subs=[('${A}', self._press_to_bomb = Lstr(
Lstr(value='\'${K}\'', resource='orText',
subs=[('${K}', bomb_key)])), subs=[
('${B}', self._press_to_bomb)]) ('${A}', Lstr(value='\'${K}\'', subs=[('${K}', bomb_key)])),
self._joinmsg = Lstr(value='${A} < ${B} >', ('${B}', self._press_to_bomb),
subs=[('${A}', ],
Lstr(resource='pressPunchToJoinText')), )
('${B}', self._press_to_punch)]) self._joinmsg = Lstr(
value='${A} < ${B} >',
subs=[
('${A}', Lstr(resource='pressPunchToJoinText')),
('${B}', self._press_to_punch),
],
)
def _update(self) -> None: def _update(self) -> None:
assert self._text.node assert self._text.node
@ -113,12 +157,14 @@ class JoinInfo:
@dataclass @dataclass
class PlayerReadyMessage: class PlayerReadyMessage:
"""Tells an object a player has been selected from the given chooser.""" """Tells an object a player has been selected from the given chooser."""
chooser: ba.Chooser chooser: ba.Chooser
@dataclass @dataclass
class ChangeMessage: class ChangeMessage:
"""Tells an object that a selection is being changed.""" """Tells an object that a selection is being changed."""
what: str what: str
value: int value: int
@ -135,8 +181,9 @@ class Chooser:
if self._text_node: if self._text_node:
self._text_node.delete() self._text_node.delete()
def __init__(self, vpos: float, sessionplayer: _ba.SessionPlayer, def __init__(
lobby: 'Lobby') -> None: self, vpos: float, sessionplayer: _ba.SessionPlayer, lobby: 'Lobby'
) -> None:
self._deek_sound = _ba.getsound('deek') self._deek_sound = _ba.getsound('deek')
self._click_sound = _ba.getsound('click01') self._click_sound = _ba.getsound('click01')
self._punchsound = _ba.getsound('punch01') self._punchsound = _ba.getsound('punch01')
@ -170,48 +217,54 @@ class Chooser:
# for the '_random' profile. Let's use their input_device id to seed # for the '_random' profile. Let's use their input_device id to seed
# it. This will give a persistent character for them between games # it. This will give a persistent character for them between games
# and will distribute characters nicely if everyone is random. # and will distribute characters nicely if everyone is random.
self._random_color, self._random_highlight = ( self._random_color, self._random_highlight = get_player_profile_colors(
get_player_profile_colors(None)) None
)
# To calc our random character we pick a random one out of our # 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 # unlocked list and then locate that character's index in the full
# list. # list.
char_index_offset = app.lobby_random_char_index_offset char_index_offset = app.lobby_random_char_index_offset
self._random_character_index = ( self._random_character_index = (
(sessionplayer.inputdevice.id + char_index_offset) % sessionplayer.inputdevice.id + char_index_offset
len(self._character_names)) ) % len(self._character_names)
# Attempt to set an initial profile based on what was used previously # Attempt to set an initial profile based on what was used previously
# for this input-device, etc. # for this input-device, etc.
self._profileindex = self._select_initial_profile() self._profileindex = self._select_initial_profile()
self._profilename = self._profilenames[self._profileindex] self._profilename = self._profilenames[self._profileindex]
self._text_node = _ba.newnode('text', self._text_node = _ba.newnode(
delegate=self, 'text',
attrs={ delegate=self,
'position': (-100, self._vpos), attrs={
'maxwidth': 160, 'position': (-100, self._vpos),
'shadow': 0.5, 'maxwidth': 160,
'vr_depth': -20, 'shadow': 0.5,
'h_align': 'left', 'vr_depth': -20,
'v_align': 'center', 'h_align': 'left',
'v_attach': 'top' 'v_align': 'center',
}) 'v_attach': 'top',
},
)
animate(self._text_node, 'scale', {0: 0, 0.1: 1.0}) animate(self._text_node, 'scale', {0: 0, 0.1: 1.0})
self.icon = _ba.newnode('image', self.icon = _ba.newnode(
owner=self._text_node, 'image',
attrs={ owner=self._text_node,
'position': (-130, self._vpos + 20), attrs={
'mask_texture': self._mask_texture, 'position': (-130, self._vpos + 20),
'vr_depth': -10, 'mask_texture': self._mask_texture,
'attach': 'topCenter' 'vr_depth': -10,
}) 'attach': 'topCenter',
},
)
animate_array(self.icon, 'scale', 2, {0: (0, 0), 0.1: (45, 45)}) animate_array(self.icon, 'scale', 2, {0: (0, 0), 0.1: (45, 45)})
# Set our initial name to '<choosing player>' in case anyone asks. # Set our initial name to '<choosing player>' in case anyone asks.
self._sessionplayer.setname( 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 # Init these to our rando but they should get switched to the
# selected profile (if any) right after. # 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 # If we've got a set profile name for this device, work backwards
# from that to get our index. # from that to get our index.
dprofilename = (app.config.get('Default Player Profiles', dprofilename = app.config.get('Default Player Profiles', {}).get(
{}).get(inputdevice.name + ' ' + inputdevice.name + ' ' + inputdevice.unique_identifier
inputdevice.unique_identifier)) )
if dprofilename is not None and dprofilename in profilenames: if dprofilename is not None and dprofilename in profilenames:
# If we got '__account__' and its local and we haven't marked # If we got '__account__' and its local and we haven't marked
# anyone as the 'account profile' device yet, mark this guy as # anyone as the 'account profile' device yet, mark this guy as
# it. (prevents the next joiner from getting the account # it. (prevents the next joiner from getting the account
# profile too). # profile too).
if (dprofilename == '__account__' if (
and not inputdevice.is_remote_client dprofilename == '__account__'
and app.lobby_account_profile_device_id is None): and not inputdevice.is_remote_client
and app.lobby_account_profile_device_id is None
):
app.lobby_account_profile_device_id = inputdevice.id app.lobby_account_profile_device_id = inputdevice.id
return profilenames.index(dprofilename) return profilenames.index(dprofilename)
# We want to mark the first local input-device in the game # We want to mark the first local input-device in the game
# as the 'account profile' device. # as the 'account profile' device.
if (not inputdevice.is_remote_client if (
and not inputdevice.is_controller_app): not inputdevice.is_remote_client
if (app.lobby_account_profile_device_id is None and not inputdevice.is_controller_app
and '__account__' in profilenames): ):
if (
app.lobby_account_profile_device_id is None
and '__account__' in profilenames
):
app.lobby_account_profile_device_id = inputdevice.id app.lobby_account_profile_device_id = inputdevice.id
# If this is the designated account-profile-device, try to default # If this is the designated account-profile-device, try to default
# to the account profile. # to the account profile.
if (inputdevice.id == app.lobby_account_profile_device_id if (
and '__account__' in profilenames): inputdevice.id == app.lobby_account_profile_device_id
and '__account__' in profilenames
):
return profilenames.index('__account__') return profilenames.index('__account__')
# If this is the controller app, it defaults to using a random # 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 # Cycle through our non-random profiles once; after
# that, everyone gets random. # that, everyone gets random.
while (app.lobby_random_profile_index < len(profilenames) while app.lobby_random_profile_index < len(
and profilenames[app.lobby_random_profile_index] profilenames
in ('_random', '__account__', '_edit')): ) and profilenames[app.lobby_random_profile_index] in (
'_random',
'__account__',
'_edit',
):
app.lobby_random_profile_index += 1 app.lobby_random_profile_index += 1
if app.lobby_random_profile_index < len(profilenames): if app.lobby_random_profile_index < len(profilenames):
profileindex = app.lobby_random_profile_index profileindex = app.lobby_random_profile_index
@ -340,18 +405,22 @@ class Chooser:
# character to others they own, but profile characters # character to others they own, but profile characters
# should work (and we validate profiles on the master server # should work (and we validate profiles on the master server
# so no exploit opportunities) # so no exploit opportunities)
if (character not in self._character_names if (
and character in _ba.app.spaz_appearances): character not in self._character_names
and character in _ba.app.spaz_appearances
):
self._character_names.append(character) self._character_names.append(character)
self._character_index = self._character_names.index(character) self._character_index = self._character_names.index(character)
self._color, self._highlight = (get_player_profile_colors( self._color, self._highlight = get_player_profile_colors(
self._profilename, profiles=self._profiles)) self._profilename, profiles=self._profiles
)
self._update_icon() self._update_icon()
self._update_text() self._update_text()
def reload_profiles(self) -> None: def reload_profiles(self) -> None:
"""Reload all player profiles.""" """Reload all player profiles."""
from ba._general import json_prep from ba._general import json_prep
app = _ba.app app = _ba.app
# Re-construct our profile index and other stuff since the profile # 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 # For local devices, add it an 'edit' option which will pop up
# the profile window. # the profile window.
if not is_remote and not is_test_input and not (app.demo_mode if (
or app.arcade_mode): not is_remote
and not is_test_input
and not (app.demo_mode or app.arcade_mode)
):
self._profiles['_edit'] = {} self._profiles['_edit'] = {}
# Build a sorted name list we can iterate through. # Build a sorted name list we can iterate through.
@ -417,18 +489,25 @@ class Chooser:
assert self._text_node assert self._text_node
spacing = 350 spacing = 350
sessionteams = self.lobby.sessionteams sessionteams = self.lobby.sessionteams
offs = (spacing * -0.5 * len(sessionteams) + offs = (
spacing * self._selected_team_index + 250) spacing * -0.5 * len(sessionteams)
+ spacing * self._selected_team_index
+ 250
)
if len(sessionteams) > 1: if len(sessionteams) > 1:
offs -= 35 offs -= 35
animate_array(self._text_node, 'position', 2, { animate_array(
0: self._text_node.position, self._text_node,
0.1: (-100 + offs, self._vpos + 23) 'position',
}) 2,
animate_array(self.icon, 'position', 2, { {0: self._text_node.position, 0.1: (-100 + offs, self._vpos + 23)},
0: self.icon.position, )
0.1: (-130 + offs, self._vpos + 22) animate_array(
}) self.icon,
'position',
2,
{0: self.icon.position, 0.1: (-130 + offs, self._vpos + 22)},
)
def get_character_name(self) -> str: def get_character_name(self) -> str:
"""Return the selected character name.""" """Return the selected character name."""
@ -442,16 +521,14 @@ class Chooser:
clamp = False clamp = False
if name == '_random': if name == '_random':
try: try:
name = ( name = self._sessionplayer.inputdevice.get_default_player_name()
self._sessionplayer.inputdevice.get_default_player_name())
except Exception: except Exception:
print_exception('Error getting _random chooser name.') print_exception('Error getting _random chooser name.')
name = 'Invalid' name = 'Invalid'
clamp = not full clamp = not full
elif name == '__account__': elif name == '__account__':
try: try:
name = self._sessionplayer.inputdevice.get_v1_account_name( name = self._sessionplayer.inputdevice.get_v1_account_name(full)
full)
except Exception: except Exception:
print_exception('Error getting account name for chooser.') print_exception('Error getting account name for chooser.')
name = 'Invalid' name = 'Invalid'
@ -459,18 +536,21 @@ class Chooser:
elif name == '_edit': elif name == '_edit':
# Explicitly flattening this to a str; it's only relevant on # Explicitly flattening this to a str; it's only relevant on
# the host so that's ok. # the host so that's ok.
name = (Lstr( name = Lstr(
resource='createEditPlayerText', resource='createEditPlayerText',
fallback_resource='editProfileWindow.titleNewText').evaluate()) fallback_resource='editProfileWindow.titleNewText',
).evaluate()
else: else:
# If we have a regular profile marked as global with an icon, # If we have a regular profile marked as global with an icon,
# use it (for full only). # use it (for full only).
if full: if full:
try: try:
if self._profiles[name_raw].get('global', False): if self._profiles[name_raw].get('global', False):
icon = (self._profiles[name_raw]['icon'] icon = (
if 'icon' in self._profiles[name_raw] else self._profiles[name_raw]['icon']
_ba.charstr(SpecialChar.LOGO)) if 'icon' in self._profiles[name_raw]
else _ba.charstr(SpecialChar.LOGO)
)
name = icon + name name = icon + name
except Exception: except Exception:
print_exception('Error applying global icon.') print_exception('Error applying global icon.')
@ -488,6 +568,7 @@ class Chooser:
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from bastd.ui.profile import browser as pbrowser from bastd.ui.profile import browser as pbrowser
from ba._general import Call from ba._general import Call
profilename = self._profilenames[self._profileindex] profilename = self._profilenames[self._profileindex]
# Handle '_edit' as a special case. # Handle '_edit' as a special case.
@ -503,50 +584,71 @@ class Chooser:
if not ready: if not ready:
self._sessionplayer.assigninput( self._sessionplayer.assigninput(
InputType.LEFT_PRESS, InputType.LEFT_PRESS,
Call(self.handlemessage, ChangeMessage('team', -1))) Call(self.handlemessage, ChangeMessage('team', -1)),
)
self._sessionplayer.assigninput( self._sessionplayer.assigninput(
InputType.RIGHT_PRESS, InputType.RIGHT_PRESS,
Call(self.handlemessage, ChangeMessage('team', 1))) Call(self.handlemessage, ChangeMessage('team', 1)),
)
self._sessionplayer.assigninput( self._sessionplayer.assigninput(
InputType.BOMB_PRESS, InputType.BOMB_PRESS,
Call(self.handlemessage, ChangeMessage('character', 1))) Call(self.handlemessage, ChangeMessage('character', 1)),
)
self._sessionplayer.assigninput( self._sessionplayer.assigninput(
InputType.UP_PRESS, InputType.UP_PRESS,
Call(self.handlemessage, ChangeMessage('profileindex', -1))) Call(self.handlemessage, ChangeMessage('profileindex', -1)),
)
self._sessionplayer.assigninput( self._sessionplayer.assigninput(
InputType.DOWN_PRESS, InputType.DOWN_PRESS,
Call(self.handlemessage, ChangeMessage('profileindex', 1))) Call(self.handlemessage, ChangeMessage('profileindex', 1)),
)
self._sessionplayer.assigninput( self._sessionplayer.assigninput(
(InputType.JUMP_PRESS, InputType.PICK_UP_PRESS, (
InputType.PUNCH_PRESS), InputType.JUMP_PRESS,
Call(self.handlemessage, ChangeMessage('ready', 1))) InputType.PICK_UP_PRESS,
InputType.PUNCH_PRESS,
),
Call(self.handlemessage, ChangeMessage('ready', 1)),
)
self._ready = False self._ready = False
self._update_text() self._update_text()
self._sessionplayer.setname('untitled', real=False) self._sessionplayer.setname('untitled', real=False)
else: else:
self._sessionplayer.assigninput( self._sessionplayer.assigninput(
(InputType.LEFT_PRESS, InputType.RIGHT_PRESS, (
InputType.UP_PRESS, InputType.DOWN_PRESS, InputType.LEFT_PRESS,
InputType.JUMP_PRESS, InputType.BOMB_PRESS, InputType.RIGHT_PRESS,
InputType.PICK_UP_PRESS), self._do_nothing) InputType.UP_PRESS,
InputType.DOWN_PRESS,
InputType.JUMP_PRESS,
InputType.BOMB_PRESS,
InputType.PICK_UP_PRESS,
),
self._do_nothing,
)
self._sessionplayer.assigninput( self._sessionplayer.assigninput(
(InputType.JUMP_PRESS, InputType.BOMB_PRESS, (
InputType.PICK_UP_PRESS, InputType.PUNCH_PRESS), InputType.JUMP_PRESS,
Call(self.handlemessage, ChangeMessage('ready', 0))) 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. # Store the last profile picked by this input for reuse.
input_device = self._sessionplayer.inputdevice input_device = self._sessionplayer.inputdevice
name = input_device.name name = input_device.name
unique_id = input_device.unique_identifier unique_id = input_device.unique_identifier
device_profiles = _ba.app.config.setdefault( device_profiles = _ba.app.config.setdefault(
'Default Player Profiles', {}) 'Default Player Profiles', {}
)
# Make an exception if we have no custom profiles and are set # 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 # to random; in that case we'll want to start picking up custom
# profiles if/when one is made so keep our setting cleared. # profiles if/when one is made so keep our setting cleared.
special = ('_random', '_edit', '__account__') special = ('_random', '_edit', '__account__')
have_custom_profiles = any(p not in special have_custom_profiles = any(p not in special for p in self._profiles)
for p in self._profiles)
profilekey = name + ' ' + unique_id profilekey = name + ' ' + unique_id
if profilename == '_random' and not have_custom_profiles: if profilename == '_random' and not have_custom_profiles:
@ -557,9 +659,9 @@ class Chooser:
_ba.app.config.commit() _ba.app.config.commit()
# Set this player's short and full name. # Set this player's short and full name.
self._sessionplayer.setname(self._getname(), self._sessionplayer.setname(
self._getname(full=True), self._getname(), self._getname(full=True), real=True
real=True) )
self._ready = True self._ready = True
self._update_text() self._update_text()
@ -583,18 +685,21 @@ class Chooser:
team_player_counts = {} team_player_counts = {}
for sessionteam in sessionteams: for sessionteam in sessionteams:
team_player_counts[sessionteam.id] = len( team_player_counts[sessionteam.id] = len(
sessionteam.players) sessionteam.players
)
for chooser in lobby.choosers: for chooser in lobby.choosers:
if chooser.ready: if chooser.ready:
team_player_counts[chooser.sessionteam.id] += 1 team_player_counts[chooser.sessionteam.id] += 1
largest_team_size = max(team_player_counts.values()) 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 # Force switch if we're on the biggest sessionteam
# and there's a smaller one available. # and there's a smaller one available.
if (largest_team_size != smallest_team_size if (
and team_player_counts[self.sessionteam.id] >= largest_team_size != smallest_team_size
largest_team_size): and team_player_counts[self.sessionteam.id]
>= largest_team_size
):
force_team_switch = True force_team_switch = True
# Either force switch teams, or actually for realsies do the set-ready. # 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: if now - self._last_change[0] < QUICK_CHANGE_INTERVAL:
count += 1 count += 1
if count > MAX_QUICK_CHANGE_COUNT: if count > MAX_QUICK_CHANGE_COUNT:
_ba.disconnect_client( _ba.disconnect_client(self._sessionplayer.inputdevice.client_id)
self._sessionplayer.inputdevice.client_id)
elif now - self._last_change[0] > QUICK_CHANGE_RESET_INTERVAL: elif now - self._last_change[0] > QUICK_CHANGE_RESET_INTERVAL:
count = 0 count = 0
self._last_change = (now, count) self._last_change = (now, count)
@ -638,8 +742,8 @@ class Chooser:
if len(sessionteams) > 1: if len(sessionteams) > 1:
_ba.playsound(self._swish_sound) _ba.playsound(self._swish_sound)
self._selected_team_index = ( self._selected_team_index = (
(self._selected_team_index + msg.value) % self._selected_team_index + msg.value
len(sessionteams)) ) % len(sessionteams)
self._update_text() self._update_text()
self.update_position() self.update_position()
self._update_icon() self._update_icon()
@ -655,15 +759,17 @@ class Chooser:
# Pick the next player profile and assign our name # Pick the next player profile and assign our name
# and character based on that. # and character based on that.
_ba.playsound(self._deek_sound) _ba.playsound(self._deek_sound)
self._profileindex = ((self._profileindex + msg.value) % self._profileindex = (self._profileindex + msg.value) % len(
len(self._profilenames)) self._profilenames
)
self.update_from_profile() self.update_from_profile()
elif msg.what == 'character': elif msg.what == 'character':
_ba.playsound(self._click_sound) _ba.playsound(self._click_sound)
# update our index in our local list of characters # update our index in our local list of characters
self._character_index = ((self._character_index + msg.value) % self._character_index = (
len(self._character_names)) self._character_index + msg.value
) % len(self._character_names)
self._update_text() self._update_text()
self._update_icon() self._update_icon()
@ -677,30 +783,34 @@ class Chooser:
# Once we're ready, we've saved the name, so lets ask the system # Once we're ready, we've saved the name, so lets ask the system
# for it so we get appended numbers and stuff. # for it so we get appended numbers and stuff.
text = Lstr(value=self._sessionplayer.getname(full=True)) text = Lstr(value=self._sessionplayer.getname(full=True))
text = Lstr(value='${A} (${B})', text = Lstr(
subs=[('${A}', text), value='${A} (${B})',
('${B}', Lstr(resource='readyText'))]) subs=[('${A}', text), ('${B}', Lstr(resource='readyText'))],
)
else: else:
text = Lstr(value=self._getname(full=True)) text = Lstr(value=self._getname(full=True))
can_switch_teams = len(self.lobby.sessionteams) > 1 can_switch_teams = len(self.lobby.sessionteams) > 1
# Flash as we're coming in. # 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: if not self._inited:
animate_array(self._text_node, 'color', 4, { animate_array(
0.15: fin_color, self._text_node,
0.25: (2, 2, 2, 1), 'color',
0.35: fin_color 4,
}) {0.15: fin_color, 0.25: (2, 2, 2, 1), 0.35: fin_color},
)
else: else:
# Blend if we're in teams mode; switch instantly otherwise. # Blend if we're in teams mode; switch instantly otherwise.
if can_switch_teams: if can_switch_teams:
animate_array(self._text_node, 'color', 4, { animate_array(
0: self._text_node.color, self._text_node,
0.1: fin_color 'color',
}) 4,
{0: self._text_node.color, 0.1: fin_color},
)
else: else:
self._text_node.color = fin_color self._text_node.color = fin_color
@ -740,9 +850,11 @@ class Chooser:
max_val = sessionteam.color[j] max_val = sessionteam.color[j]
max_index = j max_index = j
that_color_for_us = highlight[max_index] that_color_for_us = highlight[max_index]
our_second_biggest = max(highlight[(max_index + 1) % 3], our_second_biggest = max(
highlight[(max_index + 2) % 3]) highlight[(max_index + 1) % 3],
diff = (that_color_for_us - our_second_biggest) highlight[(max_index + 2) % 3],
)
diff = that_color_for_us - our_second_biggest
if diff > 0: if diff > 0:
highlight[max_index] -= diff * 0.6 highlight[max_index] -= diff * 0.6
highlight[(max_index + 1) % 3] += diff * 0.3 highlight[(max_index + 1) % 3] += diff * 0.3
@ -764,10 +876,12 @@ class Chooser:
return return
try: try:
tex_name = (_ba.app.spaz_appearances[self._character_names[ tex_name = _ba.app.spaz_appearances[
self._character_index]].icon_texture) self._character_names[self._character_index]
tint_tex_name = (_ba.app.spaz_appearances[self._character_names[ ].icon_texture
self._character_index]].icon_mask_texture) tint_tex_name = _ba.app.spaz_appearances[
self._character_names[self._character_index]
].icon_mask_texture
except Exception: except Exception:
print_exception('Error updating char icon list') print_exception('Error updating char icon list')
tex_name = 'neoSpazIcon' tex_name = 'neoSpazIcon'
@ -786,18 +900,18 @@ class Chooser:
# If we're initing, flash. # If we're initing, flash.
if not self._inited: if not self._inited:
animate_array(self.icon, 'color', 3, { animate_array(
0.15: (1, 1, 1), self.icon,
0.25: (2, 2, 2), 'color',
0.35: (1, 1, 1) 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. # Blend in teams mode; switch instantly in ffa-mode.
if can_switch_teams: if can_switch_teams:
animate_array(self.icon, 'tint_color', 3, { animate_array(
0: self.icon.tint_color, self.icon, 'tint_color', 3, {0: self.icon.tint_color, 0.1: clr}
0.1: clr )
})
else: else:
self.icon.tint_color = clr self.icon.tint_color = clr
self.icon.tint2_color = clr2 self.icon.tint2_color = clr2
@ -825,6 +939,7 @@ class Lobby:
def __init__(self) -> None: def __init__(self) -> None:
from ba._team import SessionTeam from ba._team import SessionTeam
from ba._coopsession import CoopSession from ba._coopsession import CoopSession
session = _ba.getsession() session = _ba.getsession()
self._use_team_colors = session.use_team_colors self._use_team_colors = session.use_team_colors
if session.use_teams: if session.use_teams:
@ -834,7 +949,7 @@ class Lobby:
else: else:
self._dummy_teams = SessionTeam() self._dummy_teams = SessionTeam()
self._sessionteams = [weakref.ref(self._dummy_teams)] 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.choosers: list[Chooser] = []
self.base_v_offset = v_offset self.base_v_offset = v_offset
self.update_positions() self.update_positions()
@ -916,9 +1031,11 @@ class Lobby:
def add_chooser(self, sessionplayer: ba.SessionPlayer) -> None: def add_chooser(self, sessionplayer: ba.SessionPlayer) -> None:
"""Add a chooser to the lobby for the provided player.""" """Add a chooser to the lobby for the provided player."""
self.choosers.append( 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._next_add_team = (self._next_add_team + 1) % len(
self._sessionteams) self._sessionteams
)
self._vpos -= 48 self._vpos -= 48
def remove_chooser(self, player: ba.SessionPlayer) -> None: 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** Category: **Asset Functions**
""" """
from ba import _language from ba import _language
return _language.Lstr(translate=('mapsNames', name)) 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. For racing games where players much touch each region in order.
Has two or more 'race_point' locations. Has two or more 'race_point' locations.
""" """
return sorted(key for key, val in _ba.app.maps.items() return sorted(
if playtype in val.get_play_types()) 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]: 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] return _ba.app.maps[name]
except KeyError: except KeyError:
from ba import _error from ba import _error
raise _error.NotFoundError(f"Map not found: '{name}'") from None 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 Consists of a collection of terrain nodes, metadata, and other
functionality comprising a game map. functionality comprising a game map.
""" """
defs: Any = None defs: Any = None
name = 'Map' name = 'Map'
_playtypes: list[str] = [] _playtypes: list[str] = []
@ -170,8 +176,9 @@ class Map(Actor):
""" """
return None return None
def __init__(self, def __init__(
vr_overlay_offset: Sequence[float] | None = None) -> None: self, vr_overlay_offset: Sequence[float] | None = None
) -> None:
"""Instantiate a map.""" """Instantiate a map."""
super().__init__() super().__init__()
@ -185,10 +192,13 @@ class Map(Actor):
self.preloaddata = _ba.getactivity().preloads[type(self)] self.preloaddata = _ba.getactivity().preloads[type(self)]
except Exception as exc: except Exception as exc:
from ba import _error from ba import _error
raise _error.NotFoundError( raise _error.NotFoundError(
'Preload data not found for ' + str(type(self)) + 'Preload data not found for '
'; make sure to call the type\'s preload()' + str(type(self))
' staticmethod in the activity constructor') from exc + '; make sure to call the type\'s preload()'
' staticmethod in the activity constructor'
) from exc
# Set various globals. # Set various globals.
gnode = _ba.getactivity().globalsnode gnode = _ba.getactivity().globalsnode
@ -210,9 +220,12 @@ class Map(Actor):
# Set shadow ranges. # Set shadow ranges.
try: try:
gnode.shadow_range = [ gnode.shadow_range = [
self.defs.points[v][1] for v in [ self.defs.points[v][1]
'shadow_lower_bottom', 'shadow_lower_top', for v in [
'shadow_upper_bottom', 'shadow_upper_top' 'shadow_lower_bottom',
'shadow_lower_top',
'shadow_upper_bottom',
'shadow_upper_top',
] ]
] ]
except Exception: except Exception:
@ -220,36 +233,42 @@ class Map(Actor):
# In vr, set a fixed point in space for the overlay to show up at. # 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. # By default we use the bounds center but allow the map to override it.
center = ((aoi_bounds[0] + aoi_bounds[3]) * 0.5, center = (
(aoi_bounds[1] + aoi_bounds[4]) * 0.5, (aoi_bounds[0] + aoi_bounds[3]) * 0.5,
(aoi_bounds[2] + aoi_bounds[5]) * 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: if vr_overlay_offset is not None:
center = (center[0] + vr_overlay_offset[0], center = (
center[1] + vr_overlay_offset[1], center[0] + vr_overlay_offset[0],
center[2] + vr_overlay_offset[2]) center[1] + vr_overlay_offset[1],
center[2] + vr_overlay_offset[2],
)
gnode.vr_overlay_center = center gnode.vr_overlay_center = center
gnode.vr_overlay_center_enabled = True gnode.vr_overlay_center_enabled = True
self.spawn_points = (self.get_def_points('spawn') self.spawn_points = self.get_def_points('spawn') or [(0, 0, 0, 0, 0, 0)]
or [(0, 0, 0, 0, 0, 0)]) self.ffa_spawn_points = self.get_def_points('ffa_spawn') or [
self.ffa_spawn_points = (self.get_def_points('ffa_spawn') (0, 0, 0, 0, 0, 0)
or [(0, 0, 0, 0, 0, 0)]) ]
self.spawn_by_flag_points = (self.get_def_points('spawn_by_flag') self.spawn_by_flag_points = self.get_def_points('spawn_by_flag') or [
or [(0, 0, 0, 0, 0, 0)]) (0, 0, 0, 0, 0, 0)
]
self.flag_points = self.get_def_points('flag') or [(0, 0, 0)] self.flag_points = self.get_def_points('flag') or [(0, 0, 0)]
# We just want points. # We just want points.
self.flag_points = [p[:3] for p in self.flag_points] self.flag_points = [p[:3] for p in self.flag_points]
self.flag_points_default = (self.get_def_point('flag_default') self.flag_points_default = self.get_def_point('flag_default') or (
or (0, 1, 0)) 0,
1,
0,
)
self.powerup_spawn_points = self.get_def_points('powerup_spawn') or [ self.powerup_spawn_points = self.get_def_points('powerup_spawn') or [
(0, 0, 0) (0, 0, 0)
] ]
# We just want points. # We just want points.
self.powerup_spawn_points = ([ self.powerup_spawn_points = [p[:3] for p in self.powerup_spawn_points]
p[:3] for p in self.powerup_spawn_points
])
self.tnt_points = self.get_def_points('tnt') or [] self.tnt_points = self.get_def_points('tnt') or []
# We just want points. # We just want points.
@ -262,11 +281,10 @@ class Map(Actor):
# Let's select random index for first spawn point, # Let's select random index for first spawn point,
# so that no one is offended by the constant spawn on the edge. # so that no one is offended by the constant spawn on the edge.
self._next_ffa_start_index = random.randrange( self._next_ffa_start_index = random.randrange(
len(self.ffa_spawn_points)) len(self.ffa_spawn_points)
)
def is_point_near_edge(self, def is_point_near_edge(self, point: ba.Vec3, running: bool = False) -> bool:
point: ba.Vec3,
running: bool = False) -> bool:
"""Return whether the provided point is near an edge of the map. """Return whether the provided point is near an edge of the map.
Simple bot logic uses this call to determine if they Simple bot logic uses this call to determine if they
@ -278,22 +296,32 @@ class Map(Actor):
return False return False
def get_def_bound_box( def get_def_bound_box(
self, name: str self, name: str
) -> tuple[float, float, float, float, float, float] | None: ) -> tuple[float, float, float, float, float, float] | None:
"""Return a 6 member bounds tuple or None if it is not defined.""" """Return a 6 member bounds tuple or None if it is not defined."""
try: try:
box = self.defs.boxes[name] box = self.defs.boxes[name]
return (box[0] - box[6] / 2.0, box[1] - box[7] / 2.0, return (
box[2] - box[8] / 2.0, box[0] + box[6] / 2.0, box[0] - box[6] / 2.0,
box[1] + box[7] / 2.0, box[2] + box[8] / 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: except Exception:
return None return None
def get_def_point(self, name: str) -> Sequence[float] | None: def get_def_point(self, name: str) -> Sequence[float] | None:
"""Return a single defined point or a default value in its absence.""" """Return a single defined point or a default value in its absence."""
val = self.defs.points.get(name) val = self.defs.points.get(name)
return (None if val is None else return (
_math.vec3validate(val) if __debug__ else val) None
if val is None
else _math.vec3validate(val)
if __debug__
else val
)
def get_def_points(self, name: str) -> list[Sequence[float]]: def get_def_points(self, name: str) -> list[Sequence[float]]:
"""Return a list of named points. """Return a list of named points.
@ -320,12 +348,16 @@ class Map(Actor):
pnt = self.spawn_points[team_index % len(self.spawn_points)] 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]) 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]) 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 = (
pnt[2] + random.uniform(*z_range)) pnt[0] + random.uniform(*x_range),
pnt[1],
pnt[2] + random.uniform(*z_range),
)
return pnt return pnt
def get_ffa_start_position( 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. """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 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]: def _getpt() -> Sequence[float]:
point = self.ffa_spawn_points[self._next_ffa_start_index] point = self.ffa_spawn_points[self._next_ffa_start_index]
self._next_ffa_start_index = ((self._next_ffa_start_index + 1) % self._next_ffa_start_index = (self._next_ffa_start_index + 1) % len(
len(self.ffa_spawn_points)) self.ffa_spawn_points
)
x_range = (-0.5, 0.5) if point[3] == 0.0 else (-point[3], point[3]) 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]) 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 = (
point[2] + random.uniform(*z_range)) point[0] + random.uniform(*x_range),
point[1],
point[2] + random.uniform(*z_range),
)
return point return point
if not player_pts: if not player_pts:
@ -368,8 +404,9 @@ class Map(Actor):
assert farthestpt is not None assert farthestpt is not None
return tuple(farthestpt) return tuple(farthestpt)
def get_flag_position(self, def get_flag_position(
team_index: int | None = None) -> Sequence[float]: self, team_index: int | None = None
) -> Sequence[float]:
"""Return a flag position on the map for the given team index. """Return a flag position on the map for the given team index.
Pass None to get the default flag point. Pass None to get the default flag point.
@ -384,6 +421,7 @@ class Map(Actor):
def handlemessage(self, msg: Any) -> Any: def handlemessage(self, msg: Any) -> Any:
from ba import _messages from ba import _messages
if isinstance(msg, _messages.DieMessage): if isinstance(msg, _messages.DieMessage):
if self.node: if self.node:
self.node.delete() self.node.delete()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,7 @@ import _ba
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Callable from typing import Any, Callable
import socket import socket
MasterServerCallback = Callable[[None | dict[str, Any]], None] MasterServerCallback = Callable[[None | dict[str, Any]], None]
# Timeout for standard functions talking to the master-server/etc. # 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: def get_ip_address_type(addr: str) -> socket.AddressFamily:
"""Return socket.AF_INET6 or socket.AF_INET4 for the provided address.""" """Return socket.AF_INET6 or socket.AF_INET4 for the provided address."""
import socket import socket
socket_type = None socket_type = None
# First try it as an ipv4 address. # First try it as an ipv4 address.
@ -84,16 +86,21 @@ def get_ip_address_type(addr: str) -> socket.AddressFamily:
class MasterServerResponseType(Enum): class MasterServerResponseType(Enum):
"""How to interpret responses from the master-server.""" """How to interpret responses from the master-server."""
JSON = 0 JSON = 0
class MasterServerCallThread(threading.Thread): class MasterServerCallThread(threading.Thread):
"""Thread to communicate with the master-server.""" """Thread to communicate with the master-server."""
def __init__(self, request: str, request_type: str, def __init__(
data: dict[str, Any] | None, self,
callback: MasterServerCallback | None, request: str,
response_type: MasterServerResponseType): request_type: str,
data: dict[str, Any] | None,
callback: MasterServerCallback | None,
response_type: MasterServerResponseType,
):
super().__init__() super().__init__()
self._request = request self._request = request
self._request_type = request_type self._request_type = request_type
@ -106,8 +113,7 @@ class MasterServerCallThread(threading.Thread):
# Save and restore the context we were created from. # Save and restore the context we were created from.
activity = _ba.getactivity(doraise=False) activity = _ba.getactivity(doraise=False)
self._activity = weakref.ref( self._activity = weakref.ref(activity) if activity is not None else None
activity) if activity is not None else None
def _run_callback(self, arg: None | dict[str, Any]) -> None: def _run_callback(self, arg: None | dict[str, Any]) -> None:
# If we were created in an activity context and that activity has # 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) self._data = utf8_all(self._data)
_ba.set_thread_name('BA_ServerCallThread') _ba.set_thread_name('BA_ServerCallThread')
if self._request_type == 'get': if self._request_type == 'get':
url = (get_master_server_address() + '/' + self._request + url = (
'?' + urllib.parse.urlencode(self._data)) get_master_server_address()
+ '/'
+ self._request
+ '?'
+ urllib.parse.urlencode(self._data)
)
response = urllib.request.urlopen( response = urllib.request.urlopen(
urllib.request.Request( 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, context=_ba.app.net.sslcontext,
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS) timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS,
)
elif self._request_type == 'post': elif self._request_type == 'post':
url = get_master_server_address() + '/' + self._request url = get_master_server_address() + '/' + self._request
response = urllib.request.urlopen( response = urllib.request.urlopen(
urllib.request.Request( urllib.request.Request(
url, url,
urllib.parse.urlencode(self._data).encode(), 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, context=_ba.app.net.sslcontext,
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS) timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS,
)
else: else:
raise TypeError('Invalid request_type: ' + self._request_type) raise TypeError('Invalid request_type: ' + self._request_type)
@ -180,37 +195,43 @@ class MasterServerCallThread(threading.Thread):
# Ignore common network errors; note unexpected ones. # Ignore common network errors; note unexpected ones.
if not is_urllib_communication_error(exc, url=url): if not is_urllib_communication_error(exc, url=url):
print(f'Error in MasterServerCallThread' print(
f' (url={url},' f'Error in MasterServerCallThread'
f' response-type={self._response_type},' f' (url={url},'
f' response-data={response_data}):') f' response-type={self._response_type},'
f' response-data={response_data}):'
)
import traceback import traceback
traceback.print_exc() traceback.print_exc()
response_data = None response_data = None
if self._callback is not None: if self._callback is not None:
_ba.pushcall(Call(self._run_callback, response_data), _ba.pushcall(
from_other_thread=True) Call(self._run_callback, response_data), from_other_thread=True
)
def master_server_get( def master_server_get(
request: str, request: str,
data: dict[str, Any], data: dict[str, Any],
callback: MasterServerCallback | None = None, callback: MasterServerCallback | None = None,
response_type: MasterServerResponseType = MasterServerResponseType.JSON response_type: MasterServerResponseType = MasterServerResponseType.JSON,
) -> None: ) -> None:
"""Make a call to the master server via a http GET.""" """Make a call to the master server via a http GET."""
MasterServerCallThread(request, 'get', data, callback, MasterServerCallThread(
response_type).start() request, 'get', data, callback, response_type
).start()
def master_server_post( def master_server_post(
request: str, request: str,
data: dict[str, Any], data: dict[str, Any],
callback: MasterServerCallback | None = None, callback: MasterServerCallback | None = None,
response_type: MasterServerResponseType = MasterServerResponseType.JSON response_type: MasterServerResponseType = MasterServerResponseType.JSON,
) -> None: ) -> None:
"""Make a call to the master server via a http POST.""" """Make a call to the master server via a http POST."""
MasterServerCallThread(request, 'post', data, callback, MasterServerCallThread(
response_type).start() 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 from typing import TYPE_CHECKING, TypeVar, Generic, cast
import _ba import _ba
from ba._error import (SessionPlayerNotFoundError, print_exception, from ba._error import (
ActorNotFoundError) SessionPlayerNotFoundError,
print_exception,
ActorNotFoundError,
)
from ba._messages import DeathType, DieMessage from ba._messages import DeathType, DieMessage
if TYPE_CHECKING: if TYPE_CHECKING:
@ -28,6 +31,7 @@ class PlayerInfo:
Category: Gameplay Classes Category: Gameplay Classes
""" """
name: str name: str
character: str character: str
@ -38,6 +42,7 @@ class StandLocation:
Category: Gameplay Classes Category: Gameplay Classes
""" """
position: ba.Vec3 position: ba.Vec3
angle: float | None = None angle: float | None = None
@ -90,7 +95,8 @@ class Player(Generic[TeamType]):
f' operator (__eq__) which will break internal' f' operator (__eq__) which will break internal'
f' logic. Please remove it.\n' f' logic. Please remove it.\n'
f'For dataclasses you can do "dataclass(eq=False)"' f'For dataclasses you can do "dataclass(eq=False)"'
f' in the class decorator.') f' in the class decorator.'
)
self.actor = None self.actor = None
self.character = '' self.character = ''
@ -249,8 +255,9 @@ class Player(Generic[TeamType]):
assert not self._expired assert not self._expired
return self._sessionplayer.get_icon() return self._sessionplayer.get_icon()
def assigninput(self, inputtype: ba.InputType | tuple[ba.InputType, ...], def assigninput(
call: Callable) -> None: self, inputtype: ba.InputType | tuple[ba.InputType, ...], call: Callable
) -> None:
""" """
Set the python callable to be run for one or more types of input. 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 # NOTE: ideally we should have a single playercast() call and use overloads
# for the optional variety, but that currently seems to not be working. # for the optional variety, but that currently seems to not be working.
# See: https://github.com/python/mypy/issues/8800 # See: https://github.com/python/mypy/issues/8800
def playercast_o(totype: type[PlayerType], def playercast_o(
player: ba.Player | None) -> PlayerType | None: totype: type[PlayerType], player: ba.Player | None
) -> PlayerType | None:
"""A variant of ba.playercast() for use with optional ba.Player values. """A variant of ba.playercast() for use with optional ba.Player values.
Category: Gameplay Functions Category: Gameplay Functions

View File

@ -15,12 +15,14 @@ if TYPE_CHECKING:
PlaylistType = list[dict[str, Any]] PlaylistType = list[dict[str, Any]]
def filter_playlist(playlist: PlaylistType, def filter_playlist(
sessiontype: type[_session.Session], playlist: PlaylistType,
add_resolved_type: bool = False, sessiontype: type[_session.Session],
remove_unowned: bool = True, add_resolved_type: bool = False,
mark_unowned: bool = False, remove_unowned: bool = True,
name: str = '?') -> PlaylistType: mark_unowned: bool = False,
name: str = '?',
) -> PlaylistType:
"""Return a filtered version of a playlist. """Return a filtered version of a playlist.
Strips out or replaces invalid or unowned game types, makes sure all 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._store import get_unowned_maps, get_unowned_game_types
from ba._general import getclass from ba._general import getclass
from ba._gameactivity import GameActivity from ba._gameactivity import GameActivity
goodlist: list[dict] = [] goodlist: list[dict] = []
unowned_maps: Sequence[str] unowned_maps: Sequence[str]
if remove_unowned or mark_unowned: if remove_unowned or mark_unowned:
@ -56,7 +59,8 @@ def filter_playlist(playlist: PlaylistType,
# Update old map names to new ones. # Update old map names to new ones.
entry['settings']['map'] = get_filtered_map_name( entry['settings']['map'] = get_filtered_map_name(
entry['settings']['map']) entry['settings']['map']
)
if remove_unowned and entry['settings']['map'] in unowned_maps: if remove_unowned and entry['settings']['map'] in unowned_maps:
continue continue
@ -67,60 +71,89 @@ def filter_playlist(playlist: PlaylistType,
raise TypeError('invalid entry format') raise TypeError('invalid entry format')
try: try:
# Do some type filters for backwards compat. # Do some type filters for backwards compat.
if entry['type'] in ('Assault.AssaultGame', if entry['type'] in (
'Happy_Thoughts.HappyThoughtsGame', 'Assault.AssaultGame',
'bsAssault.AssaultGame', 'Happy_Thoughts.HappyThoughtsGame',
'bs_assault.AssaultGame'): 'bsAssault.AssaultGame',
'bs_assault.AssaultGame',
):
entry['type'] = 'bastd.game.assault.AssaultGame' entry['type'] = 'bastd.game.assault.AssaultGame'
if entry['type'] in ('King_of_the_Hill.KingOfTheHillGame', if entry['type'] in (
'bsKingOfTheHill.KingOfTheHillGame', 'King_of_the_Hill.KingOfTheHillGame',
'bs_king_of_the_hill.KingOfTheHillGame'): 'bsKingOfTheHill.KingOfTheHillGame',
'bs_king_of_the_hill.KingOfTheHillGame',
):
entry['type'] = 'bastd.game.kingofthehill.KingOfTheHillGame' entry['type'] = 'bastd.game.kingofthehill.KingOfTheHillGame'
if entry['type'] in ('Capture_the_Flag.CTFGame', if entry['type'] in (
'bsCaptureTheFlag.CTFGame', 'Capture_the_Flag.CTFGame',
'bs_capture_the_flag.CTFGame'): 'bsCaptureTheFlag.CTFGame',
entry['type'] = ( 'bs_capture_the_flag.CTFGame',
'bastd.game.capturetheflag.CaptureTheFlagGame') ):
if entry['type'] in ('Death_Match.DeathMatchGame', entry['type'] = 'bastd.game.capturetheflag.CaptureTheFlagGame'
'bsDeathMatch.DeathMatchGame', if entry['type'] in (
'bs_death_match.DeathMatchGame'): 'Death_Match.DeathMatchGame',
'bsDeathMatch.DeathMatchGame',
'bs_death_match.DeathMatchGame',
):
entry['type'] = 'bastd.game.deathmatch.DeathMatchGame' entry['type'] = 'bastd.game.deathmatch.DeathMatchGame'
if entry['type'] in ('ChosenOne.ChosenOneGame', if entry['type'] in (
'bsChosenOne.ChosenOneGame', 'ChosenOne.ChosenOneGame',
'bs_chosen_one.ChosenOneGame'): 'bsChosenOne.ChosenOneGame',
'bs_chosen_one.ChosenOneGame',
):
entry['type'] = 'bastd.game.chosenone.ChosenOneGame' entry['type'] = 'bastd.game.chosenone.ChosenOneGame'
if entry['type'] in ('Conquest.Conquest', 'Conquest.ConquestGame', if entry['type'] in (
'bsConquest.ConquestGame', 'Conquest.Conquest',
'bs_conquest.ConquestGame'): 'Conquest.ConquestGame',
'bsConquest.ConquestGame',
'bs_conquest.ConquestGame',
):
entry['type'] = 'bastd.game.conquest.ConquestGame' entry['type'] = 'bastd.game.conquest.ConquestGame'
if entry['type'] in ('Elimination.EliminationGame', if entry['type'] in (
'bsElimination.EliminationGame', 'Elimination.EliminationGame',
'bs_elimination.EliminationGame'): 'bsElimination.EliminationGame',
'bs_elimination.EliminationGame',
):
entry['type'] = 'bastd.game.elimination.EliminationGame' entry['type'] = 'bastd.game.elimination.EliminationGame'
if entry['type'] in ('Football.FootballGame', if entry['type'] in (
'bsFootball.FootballTeamGame', 'Football.FootballGame',
'bs_football.FootballTeamGame'): 'bsFootball.FootballTeamGame',
'bs_football.FootballTeamGame',
):
entry['type'] = 'bastd.game.football.FootballTeamGame' entry['type'] = 'bastd.game.football.FootballTeamGame'
if entry['type'] in ('Hockey.HockeyGame', 'bsHockey.HockeyGame', if entry['type'] in (
'bs_hockey.HockeyGame'): 'Hockey.HockeyGame',
'bsHockey.HockeyGame',
'bs_hockey.HockeyGame',
):
entry['type'] = 'bastd.game.hockey.HockeyGame' entry['type'] = 'bastd.game.hockey.HockeyGame'
if entry['type'] in ('Keep_Away.KeepAwayGame', if entry['type'] in (
'bsKeepAway.KeepAwayGame', 'Keep_Away.KeepAwayGame',
'bs_keep_away.KeepAwayGame'): 'bsKeepAway.KeepAwayGame',
'bs_keep_away.KeepAwayGame',
):
entry['type'] = 'bastd.game.keepaway.KeepAwayGame' entry['type'] = 'bastd.game.keepaway.KeepAwayGame'
if entry['type'] in ('Race.RaceGame', 'bsRace.RaceGame', if entry['type'] in (
'bs_race.RaceGame'): 'Race.RaceGame',
'bsRace.RaceGame',
'bs_race.RaceGame',
):
entry['type'] = 'bastd.game.race.RaceGame' entry['type'] = 'bastd.game.race.RaceGame'
if entry['type'] in ('bsEasterEggHunt.EasterEggHuntGame', if entry['type'] in (
'bs_easter_egg_hunt.EasterEggHuntGame'): 'bsEasterEggHunt.EasterEggHuntGame',
'bs_easter_egg_hunt.EasterEggHuntGame',
):
entry['type'] = 'bastd.game.easteregghunt.EasterEggHuntGame' entry['type'] = 'bastd.game.easteregghunt.EasterEggHuntGame'
if entry['type'] in ('bsMeteorShower.MeteorShowerGame', if entry['type'] in (
'bs_meteor_shower.MeteorShowerGame'): 'bsMeteorShower.MeteorShowerGame',
'bs_meteor_shower.MeteorShowerGame',
):
entry['type'] = 'bastd.game.meteorshower.MeteorShowerGame' entry['type'] = 'bastd.game.meteorshower.MeteorShowerGame'
if entry['type'] in ('bsTargetPractice.TargetPracticeGame', if entry['type'] in (
'bs_target_practice.TargetPracticeGame'): 'bsTargetPractice.TargetPracticeGame',
entry['type'] = ( 'bs_target_practice.TargetPracticeGame',
'bastd.game.targetpractice.TargetPracticeGame') ):
entry['type'] = 'bastd.game.targetpractice.TargetPracticeGame'
gameclass = getclass(entry['type'], GameActivity) gameclass = getclass(entry['type'], GameActivity)
@ -140,10 +173,12 @@ def filter_playlist(playlist: PlaylistType,
entry['settings'][setting.name] = setting.default entry['settings'][setting.name] = setting.default
goodlist.append(entry) goodlist.append(entry)
except ImportError as exc: except ImportError as exc:
logging.warning('Import failed while scanning playlist \'%s\': %s', logging.warning(
name, exc) 'Import failed while scanning playlist \'%s\': %s', name, exc
)
except Exception: except Exception:
from ba import _error from ba import _error
_error.print_exception() _error.print_exception()
return goodlist return goodlist
@ -155,123 +190,134 @@ def get_default_free_for_all_playlist() -> PlaylistType:
# but filtering translates them properly to the new ones. # but filtering translates them properly to the new ones.
# (is kinda a handy way to ensure filtering is working). # (is kinda a handy way to ensure filtering is working).
# Eventually should update these though. # Eventually should update these though.
return [{ return [
'settings': { {
'Epic Mode': False, 'settings': {
'Kills to Win Per Player': 10, 'Epic Mode': False,
'Respawn Times': 1.0, 'Kills to Win Per Player': 10,
'Time Limit': 300, 'Respawn Times': 1.0,
'map': 'Doom Shroom' 'Time Limit': 300,
'map': 'Doom Shroom',
},
'type': 'bs_death_match.DeathMatchGame',
}, },
'type': 'bs_death_match.DeathMatchGame' {
}, { 'settings': {
'settings': { 'Chosen One Gets Gloves': True,
'Chosen One Gets Gloves': True, 'Chosen One Gets Shield': False,
'Chosen One Gets Shield': False, 'Chosen One Time': 30,
'Chosen One Time': 30, 'Epic Mode': 0,
'Epic Mode': 0, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Time Limit': 300,
'Time Limit': 300, 'map': 'Monkey Face',
'map': 'Monkey Face' },
'type': 'bs_chosen_one.ChosenOneGame',
}, },
'type': 'bs_chosen_one.ChosenOneGame' {
}, { 'settings': {
'settings': { 'Hold Time': 30,
'Hold Time': 30, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Time Limit': 300,
'Time Limit': 300, 'map': 'Zigzag',
'map': 'Zigzag' },
'type': 'bs_king_of_the_hill.KingOfTheHillGame',
}, },
'type': 'bs_king_of_the_hill.KingOfTheHillGame' {
}, { 'settings': {'Epic Mode': False, 'map': 'Rampage'},
'settings': { 'type': 'bs_meteor_shower.MeteorShowerGame',
'Epic Mode': False,
'map': 'Rampage'
}, },
'type': 'bs_meteor_shower.MeteorShowerGame' {
}, { 'settings': {
'settings': { 'Epic Mode': 1,
'Epic Mode': 1, 'Lives Per Player': 1,
'Lives Per Player': 1, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Time Limit': 120,
'Time Limit': 120, 'map': 'Tip Top',
'map': 'Tip Top' },
'type': 'bs_elimination.EliminationGame',
}, },
'type': 'bs_elimination.EliminationGame' {
}, { 'settings': {
'settings': { 'Hold Time': 30,
'Hold Time': 30, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Time Limit': 300,
'Time Limit': 300, 'map': 'The Pad',
'map': 'The Pad' },
'type': 'bs_keep_away.KeepAwayGame',
}, },
'type': 'bs_keep_away.KeepAwayGame' {
}, { 'settings': {
'settings': { 'Epic Mode': True,
'Epic Mode': True, 'Kills to Win Per Player': 10,
'Kills to Win Per Player': 10, 'Respawn Times': 0.25,
'Respawn Times': 0.25, 'Time Limit': 120,
'Time Limit': 120, 'map': 'Rampage',
'map': 'Rampage' },
'type': 'bs_death_match.DeathMatchGame',
}, },
'type': 'bs_death_match.DeathMatchGame' {
}, { 'settings': {
'settings': { 'Bomb Spawning': 1000,
'Bomb Spawning': 1000, 'Epic Mode': False,
'Epic Mode': False, 'Laps': 3,
'Laps': 3, 'Mine Spawn Interval': 4000,
'Mine Spawn Interval': 4000, 'Mine Spawning': 4000,
'Mine Spawning': 4000, 'Time Limit': 300,
'Time Limit': 300, 'map': 'Big G',
'map': 'Big G' },
'type': 'bs_race.RaceGame',
}, },
'type': 'bs_race.RaceGame' {
}, { 'settings': {
'settings': { 'Hold Time': 30,
'Hold Time': 30, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Time Limit': 300,
'Time Limit': 300, 'map': 'Happy Thoughts',
'map': 'Happy Thoughts' },
'type': 'bs_king_of_the_hill.KingOfTheHillGame',
}, },
'type': 'bs_king_of_the_hill.KingOfTheHillGame' {
}, { 'settings': {
'settings': { 'Enable Impact Bombs': 1,
'Enable Impact Bombs': 1, 'Enable Triple Bombs': False,
'Enable Triple Bombs': False, 'Target Count': 2,
'Target Count': 2, 'map': 'Doom Shroom',
'map': 'Doom Shroom' },
'type': 'bs_target_practice.TargetPracticeGame',
}, },
'type': 'bs_target_practice.TargetPracticeGame' {
}, { 'settings': {
'settings': { 'Epic Mode': False,
'Epic Mode': False, 'Lives Per Player': 5,
'Lives Per Player': 5, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Time Limit': 300,
'Time Limit': 300, 'map': 'Step Right Up',
'map': 'Step Right Up' },
'type': 'bs_elimination.EliminationGame',
}, },
'type': 'bs_elimination.EliminationGame' {
}, { 'settings': {
'settings': { 'Epic Mode': False,
'Epic Mode': False, 'Kills to Win Per Player': 10,
'Kills to Win Per Player': 10, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Time Limit': 300,
'Time Limit': 300, 'map': 'Crag Castle',
'map': 'Crag Castle' },
'type': 'bs_death_match.DeathMatchGame',
}, },
'type': 'bs_death_match.DeathMatchGame' {
}, { 'map': 'Lake Frigid',
'map': 'Lake Frigid', 'settings': {
'settings': { 'Bomb Spawning': 0,
'Bomb Spawning': 0, 'Epic Mode': False,
'Epic Mode': False, 'Laps': 6,
'Laps': 6, 'Mine Spawning': 2000,
'Mine Spawning': 2000, 'Time Limit': 300,
'Time Limit': 300, 'map': 'Lake Frigid',
'map': 'Lake Frigid' },
'type': 'bs_race.RaceGame',
}, },
'type': 'bs_race.RaceGame' ]
}]
def get_default_teams_playlist() -> PlaylistType: 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. # but filtering translates them properly to the new ones.
# (is kinda a handy way to ensure filtering is working). # (is kinda a handy way to ensure filtering is working).
# Eventually should update these though. # Eventually should update these though.
return [{ return [
'settings': { {
'Epic Mode': False, 'settings': {
'Flag Idle Return Time': 30, 'Epic Mode': False,
'Flag Touch Return Time': 0, 'Flag Idle Return Time': 30,
'Respawn Times': 1.0, 'Flag Touch Return Time': 0,
'Score to Win': 3, 'Respawn Times': 1.0,
'Time Limit': 600, 'Score to Win': 3,
'map': 'Bridgit' 'Time Limit': 600,
'map': 'Bridgit',
},
'type': 'bs_capture_the_flag.CTFGame',
}, },
'type': 'bs_capture_the_flag.CTFGame' {
}, { 'settings': {
'settings': { 'Epic Mode': False,
'Epic Mode': False, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Score to Win': 3,
'Score to Win': 3, 'Time Limit': 600,
'Time Limit': 600, 'map': 'Step Right Up',
'map': 'Step Right Up' },
'type': 'bs_assault.AssaultGame',
}, },
'type': 'bs_assault.AssaultGame' {
}, { 'settings': {
'settings': { 'Balance Total Lives': False,
'Balance Total Lives': False, 'Epic Mode': False,
'Epic Mode': False, 'Lives Per Player': 3,
'Lives Per Player': 3, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Solo Mode': True,
'Solo Mode': True, 'Time Limit': 600,
'Time Limit': 600, 'map': 'Rampage',
'map': 'Rampage' },
'type': 'bs_elimination.EliminationGame',
}, },
'type': 'bs_elimination.EliminationGame' {
}, { 'settings': {
'settings': { 'Epic Mode': False,
'Epic Mode': False, 'Kills to Win Per Player': 5,
'Kills to Win Per Player': 5, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Time Limit': 300,
'Time Limit': 300, 'map': 'Roundabout',
'map': 'Roundabout' },
'type': 'bs_death_match.DeathMatchGame',
}, },
'type': 'bs_death_match.DeathMatchGame' {
}, { 'settings': {
'settings': { 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Score to Win': 1,
'Score to Win': 1, 'Time Limit': 600,
'Time Limit': 600, 'map': 'Hockey Stadium',
'map': 'Hockey Stadium' },
'type': 'bs_hockey.HockeyGame',
}, },
'type': 'bs_hockey.HockeyGame' {
}, { 'settings': {
'settings': { 'Hold Time': 30,
'Hold Time': 30, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Time Limit': 300,
'Time Limit': 300, 'map': 'Monkey Face',
'map': 'Monkey Face' },
'type': 'bs_keep_away.KeepAwayGame',
}, },
'type': 'bs_keep_away.KeepAwayGame' {
}, { 'settings': {
'settings': { 'Balance Total Lives': False,
'Balance Total Lives': False, 'Epic Mode': True,
'Epic Mode': True, 'Lives Per Player': 1,
'Lives Per Player': 1, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Solo Mode': False,
'Solo Mode': False, 'Time Limit': 120,
'Time Limit': 120, 'map': 'Tip Top',
'map': 'Tip Top' },
'type': 'bs_elimination.EliminationGame',
}, },
'type': 'bs_elimination.EliminationGame' {
}, { 'settings': {
'settings': { 'Epic Mode': False,
'Epic Mode': False, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Score to Win': 3,
'Score to Win': 3, 'Time Limit': 300,
'Time Limit': 300, 'map': 'Crag Castle',
'map': 'Crag Castle' },
'type': 'bs_assault.AssaultGame',
}, },
'type': 'bs_assault.AssaultGame' {
}, { 'settings': {
'settings': { 'Epic Mode': False,
'Epic Mode': False, 'Kills to Win Per Player': 5,
'Kills to Win Per Player': 5, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Time Limit': 300,
'Time Limit': 300, 'map': 'Doom Shroom',
'map': 'Doom Shroom' },
'type': 'bs_death_match.DeathMatchGame',
}, },
'type': 'bs_death_match.DeathMatchGame' {
}, { 'settings': {'Epic Mode': False, 'map': 'Rampage'},
'settings': { 'type': 'bs_meteor_shower.MeteorShowerGame',
'Epic Mode': False,
'map': 'Rampage'
}, },
'type': 'bs_meteor_shower.MeteorShowerGame' {
}, { 'settings': {
'settings': { 'Epic Mode': False,
'Epic Mode': False, 'Flag Idle Return Time': 30,
'Flag Idle Return Time': 30, 'Flag Touch Return Time': 0,
'Flag Touch Return Time': 0, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Score to Win': 2,
'Score to Win': 2, 'Time Limit': 600,
'Time Limit': 600, 'map': 'Roundabout',
'map': 'Roundabout' },
'type': 'bs_capture_the_flag.CTFGame',
}, },
'type': 'bs_capture_the_flag.CTFGame' {
}, { 'settings': {
'settings': { 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Score to Win': 21,
'Score to Win': 21, 'Time Limit': 600,
'Time Limit': 600, 'map': 'Football Stadium',
'map': 'Football Stadium' },
'type': 'bs_football.FootballTeamGame',
}, },
'type': 'bs_football.FootballTeamGame' {
}, { 'settings': {
'settings': { 'Epic Mode': True,
'Epic Mode': True, 'Respawn Times': 0.25,
'Respawn Times': 0.25, 'Score to Win': 3,
'Score to Win': 3, 'Time Limit': 120,
'Time Limit': 120, 'map': 'Bridgit',
'map': 'Bridgit' },
'type': 'bs_assault.AssaultGame',
}, },
'type': 'bs_assault.AssaultGame' {
}, { 'map': 'Doom Shroom',
'map': 'Doom Shroom', 'settings': {
'settings': { 'Enable Impact Bombs': 1,
'Enable Impact Bombs': 1, 'Enable Triple Bombs': False,
'Enable Triple Bombs': False, 'Target Count': 2,
'Target Count': 2, 'map': 'Doom Shroom',
'map': 'Doom Shroom' },
'type': 'bs_target_practice.TargetPracticeGame',
}, },
'type': 'bs_target_practice.TargetPracticeGame' {
}, { 'settings': {
'settings': { 'Hold Time': 30,
'Hold Time': 30, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Time Limit': 300,
'Time Limit': 300, 'map': 'Tip Top',
'map': 'Tip Top' },
'type': 'bs_king_of_the_hill.KingOfTheHillGame',
}, },
'type': 'bs_king_of_the_hill.KingOfTheHillGame' {
}, { 'settings': {
'settings': { 'Epic Mode': False,
'Epic Mode': False, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Score to Win': 2,
'Score to Win': 2, 'Time Limit': 300,
'Time Limit': 300, 'map': 'Zigzag',
'map': 'Zigzag' },
'type': 'bs_assault.AssaultGame',
}, },
'type': 'bs_assault.AssaultGame' {
}, { 'settings': {
'settings': { 'Epic Mode': False,
'Epic Mode': False, 'Flag Idle Return Time': 30,
'Flag Idle Return Time': 30, 'Flag Touch Return Time': 0,
'Flag Touch Return Time': 0, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Score to Win': 3,
'Score to Win': 3, 'Time Limit': 300,
'Time Limit': 300, 'map': 'Happy Thoughts',
'map': 'Happy Thoughts' },
'type': 'bs_capture_the_flag.CTFGame',
}, },
'type': 'bs_capture_the_flag.CTFGame' {
}, { 'settings': {
'settings': { 'Bomb Spawning': 1000,
'Bomb Spawning': 1000, 'Epic Mode': True,
'Epic Mode': True, 'Laps': 1,
'Laps': 1, 'Mine Spawning': 2000,
'Mine Spawning': 2000, 'Time Limit': 300,
'Time Limit': 300, 'map': 'Big G',
'map': 'Big G' },
'type': 'bs_race.RaceGame',
}, },
'type': 'bs_race.RaceGame' {
}, { 'settings': {
'settings': { 'Epic Mode': False,
'Epic Mode': False, 'Kills to Win Per Player': 5,
'Kills to Win Per Player': 5, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Time Limit': 300,
'Time Limit': 300, 'map': 'Monkey Face',
'map': 'Monkey Face' },
'type': 'bs_death_match.DeathMatchGame',
}, },
'type': 'bs_death_match.DeathMatchGame' {
}, { 'settings': {
'settings': { 'Hold Time': 30,
'Hold Time': 30, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Time Limit': 300,
'Time Limit': 300, 'map': 'Lake Frigid',
'map': 'Lake Frigid' },
'type': 'bs_keep_away.KeepAwayGame',
}, },
'type': 'bs_keep_away.KeepAwayGame' {
}, { 'settings': {
'settings': { 'Epic Mode': False,
'Epic Mode': False, 'Flag Idle Return Time': 30,
'Flag Idle Return Time': 30, 'Flag Touch Return Time': 3,
'Flag Touch Return Time': 3, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Score to Win': 2,
'Score to Win': 2, 'Time Limit': 300,
'Time Limit': 300, 'map': 'Tip Top',
'map': 'Tip Top' },
'type': 'bs_capture_the_flag.CTFGame',
}, },
'type': 'bs_capture_the_flag.CTFGame' {
}, { 'settings': {
'settings': { 'Balance Total Lives': False,
'Balance Total Lives': False, 'Epic Mode': False,
'Epic Mode': False, 'Lives Per Player': 3,
'Lives Per Player': 3, 'Respawn Times': 1.0,
'Respawn Times': 1.0, 'Solo Mode': False,
'Solo Mode': False, 'Time Limit': 300,
'Time Limit': 300, 'map': 'Crag Castle',
'map': 'Crag Castle' },
'type': 'bs_elimination.EliminationGame',
}, },
'type': 'bs_elimination.EliminationGame' {
}, { 'settings': {
'settings': { 'Epic Mode': True,
'Epic Mode': True, 'Respawn Times': 0.25,
'Respawn Times': 0.25, 'Time Limit': 120,
'Time Limit': 120, 'map': 'Zigzag',
'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. # Create a potential-plugin for each class we found in the scan.
for class_path in results.exports_of_class(Plugin): for class_path in results.exports_of_class(Plugin):
plugs.potential_plugins.append( plugs.potential_plugins.append(
PotentialPlugin(display_name=Lstr(value=class_path), PotentialPlugin(
class_path=class_path, display_name=Lstr(value=class_path),
available=True)) class_path=class_path,
available=True,
)
)
if class_path not in plugstates: if class_path not in plugstates:
# Go ahead and enable new plugins by default, but we'll # Go ahead and enable new plugins by default, but we'll
# inform the user that they need to restart to pick them up. # 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' # plugins, so we don't need the message about 'restart to activate'
# anymore. # anymore.
if found_new and bool(False): if found_new and bool(False):
_ba.screenmessage(Lstr(resource='pluginsDetectedText'), _ba.screenmessage(
color=(0, 1, 0)) Lstr(resource='pluginsDetectedText'), color=(0, 1, 0)
)
_ba.playsound(_ba.getsound('ding')) _ba.playsound(_ba.getsound('ding'))
if config_changed: if config_changed:
@ -75,6 +79,7 @@ class PluginSubsystem:
plugin.on_app_running() plugin.on_app_running()
except Exception: except Exception:
from ba import _error from ba import _error
_error.print_exception('Error in plugin on_app_running()') _error.print_exception('Error in plugin on_app_running()')
def on_app_pause(self) -> None: def on_app_pause(self) -> None:
@ -84,6 +89,7 @@ class PluginSubsystem:
plugin.on_app_pause() plugin.on_app_pause()
except Exception: except Exception:
from ba import _error from ba import _error
_error.print_exception('Error in plugin on_app_pause()') _error.print_exception('Error in plugin on_app_pause()')
def on_app_resume(self) -> None: def on_app_resume(self) -> None:
@ -93,6 +99,7 @@ class PluginSubsystem:
plugin.on_app_resume() plugin.on_app_resume()
except Exception: except Exception:
from ba import _error from ba import _error
_error.print_exception('Error in plugin on_app_resume()') _error.print_exception('Error in plugin on_app_resume()')
def on_app_shutdown(self) -> None: def on_app_shutdown(self) -> None:
@ -102,6 +109,7 @@ class PluginSubsystem:
plugin.on_app_shutdown() plugin.on_app_shutdown()
except Exception: except Exception:
from ba import _error from ba import _error
_error.print_exception('Error in plugin on_app_shutdown()') _error.print_exception('Error in plugin on_app_shutdown()')
def load_plugins(self) -> None: 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. # in the app config. Its not our job to look at meta stuff here.
plugstates: dict[str, dict] = _ba.app.config.get('Plugins', {}) plugstates: dict[str, dict] = _ba.app.config.get('Plugins', {})
assert isinstance(plugstates, dict) assert isinstance(plugstates, dict)
plugkeys: list[str] = sorted(key for key, val in plugstates.items() plugkeys: list[str] = sorted(
if val.get('enabled', False)) key for key, val in plugstates.items() if val.get('enabled', False)
)
disappeared_plugs: set[str] = set() disappeared_plugs: set[str] = set()
for plugkey in plugkeys: for plugkey in plugkeys:
try: try:
@ -124,10 +133,13 @@ class PluginSubsystem:
continue continue
except Exception as exc: except Exception as exc:
_ba.playsound(_ba.getsound('error')) _ba.playsound(_ba.getsound('error'))
_ba.screenmessage(Lstr(resource='pluginClassLoadErrorText', _ba.screenmessage(
subs=[('${PLUGIN}', plugkey), Lstr(
('${ERROR}', str(exc))]), resource='pluginClassLoadErrorText',
color=(1, 0, 0)) subs=[('${PLUGIN}', plugkey), ('${ERROR}', str(exc))],
),
color=(1, 0, 0),
)
logging.exception("Error loading plugin class '%s'", plugkey) logging.exception("Error loading plugin class '%s'", plugkey)
continue continue
try: try:
@ -136,11 +148,15 @@ class PluginSubsystem:
self.active_plugins[plugkey] = plugin self.active_plugins[plugkey] = plugin
except Exception as exc: except Exception as exc:
from ba import _error from ba import _error
_ba.playsound(_ba.getsound('error')) _ba.playsound(_ba.getsound('error'))
_ba.screenmessage(Lstr(resource='pluginInitErrorText', _ba.screenmessage(
subs=[('${PLUGIN}', plugkey), Lstr(
('${ERROR}', str(exc))]), resource='pluginInitErrorText',
color=(1, 0, 0)) subs=[('${PLUGIN}', plugkey), ('${ERROR}', str(exc))],
),
color=(1, 0, 0),
)
_error.print_exception(f"Error initing plugin: '{plugkey}'.") _error.print_exception(f"Error initing plugin: '{plugkey}'.")
# If plugins disappeared, let the user know gently and remove them # If plugins disappeared, let the user know gently and remove them
@ -150,13 +166,18 @@ class PluginSubsystem:
if disappeared_plugs: if disappeared_plugs:
_ba.playsound(_ba.getsound('shieldDown')) _ba.playsound(_ba.getsound('shieldDown'))
_ba.screenmessage( _ba.screenmessage(
Lstr(resource='pluginsRemovedText', Lstr(
subs=[('${NUM}', str(len(disappeared_plugs)))]), resource='pluginsRemovedText',
subs=[('${NUM}', str(len(disappeared_plugs)))],
),
color=(1, 1, 0), color=(1, 1, 0),
) )
plugnames = ', '.join(disappeared_plugs) plugnames = ', '.join(disappeared_plugs)
logging.warning('%d plugin(s) no longer found: %s.', logging.warning(
len(disappeared_plugs), plugnames) '%d plugin(s) no longer found: %s.',
len(disappeared_plugs),
plugnames,
)
for goneplug in disappeared_plugs: for goneplug in disappeared_plugs:
del _ba.app.config['Plugins'][goneplug] del _ba.app.config['Plugins'][goneplug]
_ba.app.config.commit() _ba.app.config.commit()
@ -173,6 +194,7 @@ class PotentialPlugin:
were previously set to be loaded but which were unable to be were previously set to be loaded but which were unable to be
for some reason. In that case, 'available' will be set to False. for some reason. In that case, 'available' will be set to False.
""" """
display_name: ba.Lstr display_name: ba.Lstr
class_path: str class_path: str
available: bool available: bool

View File

@ -45,6 +45,14 @@ class PowerupAcceptMessage:
def get_default_powerup_distribution() -> Sequence[tuple[str, int]]: def get_default_powerup_distribution() -> Sequence[tuple[str, int]]:
"""Standard set of powerups.""" """Standard set of powerups."""
return (('triple_bombs', 3), ('ice_bombs', 3), ('punch', 3), return (
('impact_bombs', 3), ('land_mines', 2), ('sticky_bombs', 3), ('triple_bombs', 3),
('shield', 2), ('health', 1), ('curse', 1)) ('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 # NOTE: player color options are enforced server-side for non-pro accounts
# so don't change these or they won't stick... # 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), PLAYER_COLORS = [
(0.5, 0.25, 1.0), (1, 1, 0), (1, 0.5, 0), (1, 0.3, 0.5), (1, 0.15, 0.15),
(0.1, 0.1, 0.5), (0.4, 0.2, 0.1), (0.1, 0.35, 0.1), (0.2, 1, 0.2),
(1, 0.8, 0.5), (0.4, 0.05, 0.05), (0.13, 0.13, 0.13), (0.1, 0.1, 1),
(0.5, 0.5, 0.5), (1, 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]]: 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( def get_player_profile_colors(
profilename: str | None, profilename: str | None, profiles: dict[str, dict[str, Any]] | None = None
profiles: dict[str, dict[str, Any]] | None = None
) -> tuple[tuple[float, float, float], tuple[float, float, float]]: ) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
"""Given a profile, return colors for them.""" """Given a profile, return colors for them."""
appconfig = _ba.app.config appconfig = _ba.app.config
@ -83,11 +95,13 @@ def get_player_profile_colors(
if profilename is None: if profilename is None:
# Last 2 are grey and white; ignore those or we # Last 2 are grey and white; ignore those or we
# get lots of old-looking players. # get lots of old-looking players.
highlight = PLAYER_COLORS[random.randrange( highlight = PLAYER_COLORS[
len(PLAYER_COLORS) - 2)] random.randrange(len(PLAYER_COLORS) - 2)
]
else: else:
highlight = PLAYER_COLORS[sum(ord(c) + 1 highlight = PLAYER_COLORS[
for c in profilename) % sum(ord(c) + 1 for c in profilename)
(len(PLAYER_COLORS) - 2)] % (len(PLAYER_COLORS) - 2)
]
return color, highlight return color, highlight

View File

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

View File

@ -9,13 +9,18 @@ import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from efro.terminal import Clr from efro.terminal import Clr
from bacommon.servermanager import (ServerCommand, StartServerModeCommand, from bacommon.servermanager import (
ShutdownCommand, ShutdownReason, ServerCommand,
ChatMessageCommand, ScreenMessageCommand, StartServerModeCommand,
ClientListCommand, KickCommand) ShutdownCommand,
ShutdownReason,
ChatMessageCommand,
ScreenMessageCommand,
ClientListCommand,
KickCommand,
)
import _ba import _ba
from ba._internal import (add_transaction, run_transactions, from ba._internal import add_transaction, run_transactions, get_v1_account_state
get_v1_account_state)
from ba._generated.enums import TimeType from ba._generated.enums import TimeType
from ba._freeforallsession import FreeForAllSession from ba._freeforallsession import FreeForAllSession
from ba._dualteamsession import DualTeamSession from ba._dualteamsession import DualTeamSession
@ -31,6 +36,7 @@ if TYPE_CHECKING:
def _cmd(command_data: bytes) -> None: def _cmd(command_data: bytes) -> None:
"""Handle commands coming in from our server manager parent process.""" """Handle commands coming in from our server manager parent process."""
import pickle import pickle
command = pickle.loads(command_data) command = pickle.loads(command_data)
assert isinstance(command, ServerCommand) assert isinstance(command, ServerCommand)
@ -41,8 +47,9 @@ def _cmd(command_data: bytes) -> None:
if isinstance(command, ShutdownCommand): if isinstance(command, ShutdownCommand):
assert _ba.app.server is not None assert _ba.app.server is not None
_ba.app.server.shutdown(reason=command.reason, _ba.app.server.shutdown(
immediate=command.immediate) reason=command.reason, immediate=command.immediate
)
return return
if isinstance(command, ChatMessageCommand): if isinstance(command, ChatMessageCommand):
@ -56,10 +63,12 @@ def _cmd(command_data: bytes) -> None:
# Note: we have to do transient messages if # Note: we have to do transient messages if
# clients is specified, so they won't show up # clients is specified, so they won't show up
# in replays. # in replays.
_ba.screenmessage(command.message, _ba.screenmessage(
color=command.color, command.message,
clients=command.clients, color=command.color,
transient=command.clients is not None) clients=command.clients,
transient=command.clients is not None,
)
return return
if isinstance(command, ClientListCommand): if isinstance(command, ClientListCommand):
@ -69,12 +78,15 @@ def _cmd(command_data: bytes) -> None:
if isinstance(command, KickCommand): if isinstance(command, KickCommand):
assert _ba.app.server is not None assert _ba.app.server is not None
_ba.app.server.kick(client_id=command.client_id, _ba.app.server.kick(
ban_time=command.ban_time) client_id=command.client_id, ban_time=command.ban_time
)
return return
print(f'{Clr.SRED}ERROR: server process' print(
f' got unknown command: {type(command)}{Clr.RST}') f'{Clr.SRED}ERROR: server process'
f' got unknown command: {type(command)}{Clr.RST}'
)
class ServerController: class ServerController:
@ -105,23 +117,28 @@ class ServerController:
# account sign-in or fetching playlists; this will kick off the # account sign-in or fetching playlists; this will kick off the
# session once done. # session once done.
with _ba.Context('ui'): with _ba.Context('ui'):
self._prep_timer = _ba.Timer(0.25, self._prep_timer = _ba.Timer(
self._prepare_to_serve, 0.25,
timetype=TimeType.REAL, self._prepare_to_serve,
repeat=True) timetype=TimeType.REAL,
repeat=True,
)
def print_client_list(self) -> None: def print_client_list(self) -> None:
"""Print info about all connected clients.""" """Print info about all connected clients."""
import json import json
roster = _ba.get_game_roster() roster = _ba.get_game_roster()
title1 = 'Client ID' title1 = 'Client ID'
title2 = 'Account Name' title2 = 'Account Name'
title3 = 'Players' title3 = 'Players'
col1 = 10 col1 = 10
col2 = 16 col2 = 16
out = (f'{Clr.BLD}' out = (
f'{title1:<{col1}} {title2:<{col2}} {title3}' f'{Clr.BLD}'
f'{Clr.RST}') f'{title1:<{col1}} {title2:<{col2}} {title3}'
f'{Clr.RST}'
)
for client in roster: for client in roster:
if client['client_id'] == -1: if client['client_id'] == -1:
continue continue
@ -153,9 +170,11 @@ class ServerController:
print(f'{Clr.SBLU}Immediate shutdown initiated.{Clr.RST}') print(f'{Clr.SBLU}Immediate shutdown initiated.{Clr.RST}')
self._execute_shutdown() self._execute_shutdown()
else: else:
print(f'{Clr.SBLU}Shutdown initiated;' print(
f' server process will exit at the next clean opportunity.' f'{Clr.SBLU}Shutdown initiated;'
f'{Clr.RST}') f' server process will exit at the next clean opportunity.'
f'{Clr.RST}'
)
def handle_transition(self) -> bool: def handle_transition(self) -> bool:
"""Handle transitioning to a new ba.Session or quitting the app. """Handle transitioning to a new ba.Session or quitting the app.
@ -172,37 +191,45 @@ class ServerController:
def _execute_shutdown(self) -> None: def _execute_shutdown(self) -> None:
from ba._language import Lstr from ba._language import Lstr
if self._executing_shutdown: if self._executing_shutdown:
return return
self._executing_shutdown = True self._executing_shutdown = True
timestrval = time.strftime('%c') timestrval = time.strftime('%c')
if self._shutdown_reason is ShutdownReason.RESTARTING: if self._shutdown_reason is ShutdownReason.RESTARTING:
_ba.screenmessage(Lstr(resource='internal.serverRestartingText'), _ba.screenmessage(
color=(1, 0.5, 0.0)) Lstr(resource='internal.serverRestartingText'),
print(f'{Clr.SBLU}Exiting for server-restart' color=(1, 0.5, 0.0),
f' at {timestrval}.{Clr.RST}') )
print(
f'{Clr.SBLU}Exiting for server-restart'
f' at {timestrval}.{Clr.RST}'
)
else: else:
_ba.screenmessage(Lstr(resource='internal.serverShuttingDownText'), _ba.screenmessage(
color=(1, 0.5, 0.0)) Lstr(resource='internal.serverShuttingDownText'),
print(f'{Clr.SBLU}Exiting for server-shutdown' color=(1, 0.5, 0.0),
f' at {timestrval}.{Clr.RST}') )
print(
f'{Clr.SBLU}Exiting for server-shutdown'
f' at {timestrval}.{Clr.RST}'
)
with _ba.Context('ui'): with _ba.Context('ui'):
_ba.timer(2.0, _ba.quit, timetype=TimeType.REAL) _ba.timer(2.0, _ba.quit, timetype=TimeType.REAL)
def _run_access_check(self) -> None: def _run_access_check(self) -> None:
"""Check with the master server to see if we're likely joinable.""" """Check with the master server to see if we're likely joinable."""
from ba._net import master_server_get from ba._net import master_server_get
master_server_get( master_server_get(
'bsAccessCheck', '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, callback=self._access_check_response,
) )
def _access_check_response(self, data: dict[str, Any] | None) -> None: def _access_check_response(self, data: dict[str, Any] | None) -> None:
import os import os
if data is None: if data is None:
print('error on UDP port access check (internet down?)') print('error on UDP port access check (internet down?)')
else: else:
@ -216,17 +243,22 @@ class ServerController:
addrstr = '' addrstr = ''
poststr = ( poststr = (
'\nSet environment variable BA_ACCESS_CHECK_VERBOSE=1' '\nSet environment variable BA_ACCESS_CHECK_VERBOSE=1'
' for more info.') ' for more info.'
)
if data['accessible']: if data['accessible']:
print(f'{Clr.SBLU}Master server access check of{addrstr}' print(
f' udp port {port} succeeded.\n' f'{Clr.SBLU}Master server access check of{addrstr}'
f'Your server appears to be' f' udp port {port} succeeded.\n'
f' joinable from the internet.{poststr}{Clr.RST}') f'Your server appears to be'
f' joinable from the internet.{poststr}{Clr.RST}'
)
else: else:
print(f'{Clr.SRED}Master server access check of{addrstr}' print(
f' udp port {port} failed.\n' f'{Clr.SRED}Master server access check of{addrstr}'
f'Your server does not appear to be' f' udp port {port} failed.\n'
f' joinable from the internet.{poststr}{Clr.RST}') f'Your server does not appear to be'
f' joinable from the internet.{poststr}{Clr.RST}'
)
def _prepare_to_serve(self) -> None: def _prepare_to_serve(self) -> None:
"""Run in a timer to do prep before beginning to serve.""" """Run in a timer to do prep before beginning to serve."""
@ -248,15 +280,18 @@ class ServerController:
can_launch = True can_launch = True
else: else:
if not self._playlist_fetch_sent_request: if not self._playlist_fetch_sent_request:
print(f'{Clr.SBLU}Requesting shared-playlist' print(
f' {self._config.playlist_code}...{Clr.RST}') f'{Clr.SBLU}Requesting shared-playlist'
f' {self._config.playlist_code}...{Clr.RST}'
)
add_transaction( add_transaction(
{ {
'type': 'IMPORT_PLAYLIST', 'type': 'IMPORT_PLAYLIST',
'code': str(self._config.playlist_code), 'code': str(self._config.playlist_code),
'overwrite': True 'overwrite': True,
}, },
callback=self._on_playlist_fetch_response) callback=self._on_playlist_fetch_response,
)
run_transactions() run_transactions()
self._playlist_fetch_sent_request = True self._playlist_fetch_sent_request = True
@ -278,13 +313,17 @@ class ServerController:
# Once we get here, simply modify our config to use this playlist. # Once we get here, simply modify our config to use this playlist.
typename = ( typename = (
'teams' if result['playlistType'] == 'Team Tournament' else 'teams'
'ffa' if result['playlistType'] == 'Free-for-All' else '??') if result['playlistType'] == 'Team Tournament'
else 'ffa'
if result['playlistType'] == 'Free-for-All'
else '??'
)
plistname = result['playlistName'] plistname = result['playlistName']
print(f'{Clr.SBLU}Got playlist: "{plistname}" ({typename}).{Clr.RST}') print(f'{Clr.SBLU}Got playlist: "{plistname}" ({typename}).{Clr.RST}')
self._playlist_fetch_got_response = True self._playlist_fetch_got_response = True
self._config.session_type = typename self._config.session_type = typename
self._playlist_name = (result['playlistName']) self._playlist_name = result['playlistName']
def _get_session_type(self) -> type[ba.Session]: def _get_session_type(self) -> type[ba.Session]:
# Convert string session type to the class. # Convert string session type to the class.
@ -296,7 +335,8 @@ class ServerController:
if self._config.session_type == 'coop': if self._config.session_type == 'coop':
return CoopSession return CoopSession
raise RuntimeError( raise RuntimeError(
f'Invalid session_type: "{self._config.session_type}"') f'Invalid session_type: "{self._config.session_type}"'
)
def _launch_server_session(self) -> None: def _launch_server_session(self) -> None:
"""Kick off a host-session based on the current server config.""" """Kick off a host-session based on the current server config."""
@ -306,13 +346,17 @@ class ServerController:
sessiontype = self._get_session_type() sessiontype = self._get_session_type()
if get_v1_account_state() != 'signed_in': if get_v1_account_state() != 'signed_in':
print('WARNING: launch_server_session() expects to run ' print(
'with a signed in server account') '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 # 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. # server-config, pull it in to the game config and use it.
if (self._config.playlist_code is None if (
and self._config.playlist_inline is not None): self._config.playlist_code is None
and self._config.playlist_inline is not None
):
self._playlist_name = 'ServerModePlaylist' self._playlist_name = 'ServerModePlaylist'
if sessiontype is FreeForAllSession: if sessiontype is FreeForAllSession:
ptypename = 'Free-for-All' ptypename = 'Free-for-All'
@ -325,12 +369,14 @@ class ServerController:
# Need to add this in a transaction instead of just setting # Need to add this in a transaction instead of just setting
# it directly or it will get overwritten by the master-server. # it directly or it will get overwritten by the master-server.
add_transaction({ add_transaction(
'type': 'ADD_PLAYLIST', {
'playlistType': ptypename, 'type': 'ADD_PLAYLIST',
'playlistName': self._playlist_name, 'playlistType': ptypename,
'playlist': self._config.playlist_inline 'playlistName': self._playlist_name,
}) 'playlist': self._config.playlist_inline,
}
)
run_transactions() run_transactions()
if self._first_run: if self._first_run:
@ -338,17 +384,20 @@ class ServerController:
startupmsg = ( startupmsg = (
f'{Clr.BLD}{Clr.BLU}{_ba.appnameupper()} {app.version}' f'{Clr.BLD}{Clr.BLU}{_ba.appnameupper()} {app.version}'
f' ({app.build_number})' f' ({app.build_number})'
f' entering server-mode {curtimestr}{Clr.RST}') f' entering server-mode {curtimestr}{Clr.RST}'
)
logging.info(startupmsg) logging.info(startupmsg)
if sessiontype is FreeForAllSession: if sessiontype is FreeForAllSession:
appcfg['Free-for-All Playlist Selection'] = self._playlist_name appcfg['Free-for-All Playlist Selection'] = self._playlist_name
appcfg['Free-for-All Playlist Randomize'] = ( appcfg[
self._config.playlist_shuffle) 'Free-for-All Playlist Randomize'
] = self._config.playlist_shuffle
elif sessiontype is DualTeamSession: elif sessiontype is DualTeamSession:
appcfg['Team Tournament Playlist Selection'] = self._playlist_name appcfg['Team Tournament Playlist Selection'] = self._playlist_name
appcfg['Team Tournament Playlist Randomize'] = ( appcfg[
self._config.playlist_shuffle) 'Team Tournament Playlist Randomize'
] = self._config.playlist_shuffle
elif sessiontype is CoopSession: elif sessiontype is CoopSession:
app.coop_session_args = { app.coop_session_args = {
'campaign': self._config.coop_campaign, 'campaign': self._config.coop_campaign,
@ -363,7 +412,8 @@ class ServerController:
_ba.set_authenticate_clients(self._config.authenticate_clients) _ba.set_authenticate_clients(self._config.authenticate_clients)
_ba.set_enable_default_kick_voting( _ba.set_enable_default_kick_voting(
self._config.enable_default_kick_voting) self._config.enable_default_kick_voting
)
_ba.set_admins(self._config.admins) _ba.set_admins(self._config.admins)
# Call set-enabled last (will push state to the cloud). # 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: if self._config.stress_test_players is not None:
# Special case: run a stress test. # Special case: run a stress test.
from ba.internal import run_stress_test from ba.internal import run_stress_test
run_stress_test(playlist_type='Random',
playlist_name='__default__', run_stress_test(
player_count=self._config.stress_test_players, playlist_type='Random',
round_duration=30) playlist_name='__default__',
player_count=self._config.stress_test_players,
round_duration=30,
)
else: else:
_ba.new_host_session(sessiontype) _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 """All the ba.SessionTeams in the Session. Most things should use the
list of ba.Team-s in ba.Activity; not this.""" list of ba.Team-s in ba.Activity; not this."""
def __init__(self, def __init__(
depsets: Sequence[ba.DependencySet], self,
team_names: Sequence[str] | None = None, depsets: Sequence[ba.DependencySet],
team_colors: Sequence[Sequence[float]] | None = None, team_names: Sequence[str] | None = None,
min_players: int = 1, team_colors: Sequence[Sequence[float]] | None = None,
max_players: int = 8): min_players: int = 1,
max_players: int = 8,
):
"""Instantiate a session. """Instantiate a session.
depsets should be a sequence of successfully resolved ba.DependencySet 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. # Throw a combined exception if we found anything missing.
if missing_asset_packages: if missing_asset_packages:
raise DependencyError([ raise DependencyError(
Dependency(AssetPackage, set_id) [
for set_id in missing_asset_packages Dependency(AssetPackage, set_id)
]) for set_id in missing_asset_packages
]
)
# Ok; looks like our dependencies check out. # Ok; looks like our dependencies check out.
# Now give the engine a list of asset-set-ids to pass along to clients. # 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._wants_to_end = False
self._ending = False self._ending = False
self._activity_should_end_immediately = False self._activity_should_end_immediately = False
self._activity_should_end_immediately_results: (ba.GameResults self._activity_should_end_immediately_results: (
| None) = None ba.GameResults | None
) = None
self._activity_should_end_immediately_delay = 0.0 self._activity_should_end_immediately_delay = 0.0
# Create static teams if we're using them. # Create static teams if we're using them.
if self.use_teams: if self.use_teams:
if team_names is None: if team_names is None:
raise RuntimeError( 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: if team_colors is None:
raise RuntimeError( 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): if len(team_colors) != len(team_names):
raise RuntimeError(f'Got {len(team_names)} team_names' raise RuntimeError(
f' and {len(team_colors)} team_colors;' f'Got {len(team_names)} team_names'
f' these numbers must match.') f' and {len(team_colors)} team_colors;'
f' these numbers must match.'
)
for i, color in enumerate(team_colors): for i, color in enumerate(team_colors):
team = SessionTeam(team_id=self._next_team_id, team = SessionTeam(
name=GameActivity.get_team_display_string( team_id=self._next_team_id,
team_names[i]), name=GameActivity.get_team_display_string(team_names[i]),
color=color) color=color,
)
self.sessionteams.append(team) self.sessionteams.append(team)
self._next_team_id += 1 self._next_team_id += 1
try: try:
@ -218,12 +228,15 @@ class Session:
# Print a rejection message *only* to the client trying to # Print a rejection message *only* to the client trying to
# join (prevents spamming everyone else in the game). # join (prevents spamming everyone else in the game).
_ba.playsound(_ba.getsound('error')) _ba.playsound(_ba.getsound('error'))
_ba.screenmessage(Lstr(resource='playerLimitReachedText', _ba.screenmessage(
subs=[('${COUNT}', Lstr(
str(self.max_players))]), resource='playerLimitReachedText',
color=(0.8, 0.0, 0.0), subs=[('${COUNT}', str(self.max_players))],
clients=[player.inputdevice.client_id], ),
transient=True) color=(0.8, 0.0, 0.0),
clients=[player.inputdevice.client_id],
transient=True,
)
return False return False
_ba.playsound(_ba.getsound('dripity')) _ba.playsound(_ba.getsound('dripity'))
@ -233,8 +246,10 @@ class Session:
"""Called when a previously-accepted ba.SessionPlayer leaves.""" """Called when a previously-accepted ba.SessionPlayer leaves."""
if sessionplayer not in self.sessionplayers: if sessionplayer not in self.sessionplayers:
print('ERROR: Session.on_player_leave called' print(
' for player not in our list.') 'ERROR: Session.on_player_leave called'
' for player not in our list.'
)
return return
_ba.playsound(_ba.getsound('playerLeft')) _ba.playsound(_ba.getsound('playerLeft'))
@ -256,15 +271,20 @@ class Session:
assert sessionteam is not None assert sessionteam is not None
_ba.screenmessage( _ba.screenmessage(
Lstr(resource='playerLeftText', Lstr(
subs=[('${PLAYER}', sessionplayer.getname(full=True))])) resource='playerLeftText',
subs=[('${PLAYER}', sessionplayer.getname(full=True))],
)
)
# Remove them from their SessionTeam. # Remove them from their SessionTeam.
if sessionplayer in sessionteam.players: if sessionplayer in sessionteam.players:
sessionteam.players.remove(sessionplayer) sessionteam.players.remove(sessionplayer)
else: else:
print('SessionPlayer not found in SessionTeam' print(
' in on_player_leave.') 'SessionPlayer not found in SessionTeam'
' in on_player_leave.'
)
# Grab their activity-specific player instance. # Grab their activity-specific player instance.
player = sessionplayer.activityplayer player = sessionplayer.activityplayer
@ -284,8 +304,9 @@ class Session:
# Now remove them from the session list. # Now remove them from the session list.
self.sessionplayers.remove(sessionplayer) self.sessionplayers.remove(sessionplayer)
def _remove_player_team(self, sessionteam: ba.SessionTeam, def _remove_player_team(
activity: ba.Activity | None) -> None: self, sessionteam: ba.SessionTeam, activity: ba.Activity | None
) -> None:
"""Remove the player-specific team in non-teams mode.""" """Remove the player-specific team in non-teams mode."""
# They should have been the only one on their team. # They should have been the only one on their team.
@ -306,14 +327,17 @@ class Session:
self.on_team_leave(sessionteam) self.on_team_leave(sessionteam)
except Exception: except Exception:
print_exception( print_exception(
f'Error in on_team_leave for Session {self}.') f'Error in on_team_leave for Session {self}.'
)
else: else:
print('Team no in Session teams in on_player_leave.') print('Team no in Session teams in on_player_leave.')
try: try:
sessionteam.leave() sessionteam.leave()
except Exception: except Exception:
print_exception(f'Error clearing sessiondata' print_exception(
f' for team {sessionteam} in session {self}.') f'Error clearing sessiondata'
f' for team {sessionteam} in session {self}.'
)
def end(self) -> None: def end(self) -> None:
"""Initiates an end to the session and a return to the main menu. """Initiates an end to the session and a return to the main menu.
@ -329,17 +353,20 @@ class Session:
"""(internal)""" """(internal)"""
from ba._activitytypes import EndSessionActivity from ba._activitytypes import EndSessionActivity
from ba._generated.enums import TimeType from ba._generated.enums import TimeType
with _ba.Context(self): with _ba.Context(self):
curtime = _ba.time(TimeType.REAL) curtime = _ba.time(TimeType.REAL)
if self._ending: if self._ending:
# Ignore repeats unless its been a while. # Ignore repeats unless its been a while.
assert self._launch_end_session_activity_time is not None 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: if since_last < 30.0:
return return
print_error( print_error(
'_launch_end_session_activity called twice (since_last=' + '_launch_end_session_activity called twice (since_last='
str(since_last) + ')') + str(since_last)
+ ')'
)
self._launch_end_session_activity_time = curtime self._launch_end_session_activity_time = curtime
self.setactivity(_ba.newactivity(EndSessionActivity)) self.setactivity(_ba.newactivity(EndSessionActivity))
self._wants_to_end = False self._wants_to_end = False
@ -351,8 +378,9 @@ class Session:
def on_team_leave(self, team: ba.SessionTeam) -> None: def on_team_leave(self, team: ba.SessionTeam) -> None:
"""Called when a ba.Team is leaving the session.""" """Called when a ba.Team is leaving the session."""
def end_activity(self, activity: ba.Activity, results: Any, delay: float, def end_activity(
force: bool) -> None: self, activity: ba.Activity, results: Any, delay: float, force: bool
) -> None:
"""Commence shutdown of a ba.Activity (if not already occurring). """Commence shutdown of a ba.Activity (if not already occurring).
'delay' is the time delay before the Activity actually ends 'delay' is the time delay before the Activity actually ends
@ -385,7 +413,8 @@ class Session:
self._activity_end_timer = _ba.Timer( self._activity_end_timer = _ba.Timer(
delay, delay,
Call(self._complete_end_activity, activity, results), Call(self._complete_end_activity, activity, results),
timetype=TimeType.BASE) timetype=TimeType.BASE,
)
def handlemessage(self, msg: Any) -> Any: def handlemessage(self, msg: Any) -> Any:
"""General message handling; can be passed any message object.""" """General message handling; can be passed any message object."""
@ -407,7 +436,6 @@ class Session:
return None return None
class _SetActivityScopedLock: class _SetActivityScopedLock:
def __init__(self, session: ba.Session) -> None: def __init__(self, session: ba.Session) -> None:
self._session = session self._session = session
if session._in_set_activity: if session._in_set_activity:
@ -442,12 +470,16 @@ class Session:
return return
if self._next_activity is not None: if self._next_activity is not None:
raise RuntimeError('Activity switch already in progress (to ' + raise RuntimeError(
str(self._next_activity) + ')') 'Activity switch already in progress (to '
+ str(self._next_activity)
+ ')'
)
prev_activity = self._activity_retained prev_activity = self._activity_retained
prev_globals = (prev_activity.globalsnode prev_globals = (
if prev_activity is not None else None) prev_activity.globalsnode if prev_activity is not None else None
)
# Let the activity do its thing. # Let the activity do its thing.
activity.transition_in(prev_globals) activity.transition_in(prev_globals)
@ -476,9 +508,11 @@ class Session:
# will trigger the next activity to run). # will trigger the next activity to run).
if prev_activity is not None: if prev_activity is not None:
with _ba.Context('ui'): with _ba.Context('ui'):
_ba.timer(max(0.0, activity.transition_time), _ba.timer(
prev_activity.expire, max(0.0, activity.transition_time),
timetype=TimeType.REAL) prev_activity.expire,
timetype=TimeType.REAL,
)
self._in_set_activity = False self._in_set_activity = False
def getactivity(self) -> ba.Activity | None: def getactivity(self) -> ba.Activity | None:
@ -495,15 +529,18 @@ class Session:
""" """
return [] return []
def _complete_end_activity(self, activity: ba.Activity, def _complete_end_activity(
results: Any) -> None: self, activity: ba.Activity, results: Any
) -> None:
# Run the subclass callback in the session context. # Run the subclass callback in the session context.
try: try:
with _ba.Context(self): with _ba.Context(self):
self.on_activity_end(activity, results) self.on_activity_end(activity, results)
except Exception: except Exception:
print_exception(f'Error in on_activity_end() for session {self}' print_exception(
f' activity {activity} with results {results}') f'Error in on_activity_end() for session {self}'
f' activity {activity} with results {results}'
)
def _request_player(self, sessionplayer: ba.SessionPlayer) -> bool: def _request_player(self, sessionplayer: ba.SessionPlayer) -> bool:
"""Called by the native layer when a player wants to join.""" """Called by the native layer when a player wants to join."""
@ -571,7 +608,8 @@ class Session:
if self._activity_should_end_immediately: if self._activity_should_end_immediately:
self._activity_retained.end( self._activity_retained.end(
self._activity_should_end_immediately_results, 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: def _on_player_ready(self, chooser: ba.Chooser) -> None:
"""Called when a ba.Player has checked themself ready.""" """Called when a ba.Player has checked themself ready."""
@ -601,8 +639,10 @@ class Session:
self._complete_end_activity(activity, {}) self._complete_end_activity(activity, {})
else: else:
_ba.screenmessage( _ba.screenmessage(
Lstr(resource='notEnoughPlayersText', Lstr(
subs=[('${COUNT}', str(min_players))]), resource='notEnoughPlayersText',
subs=[('${COUNT}', str(min_players))],
),
color=(1, 1, 0), color=(1, 1, 0),
) )
_ba.playsound(_ba.getsound('error')) _ba.playsound(_ba.getsound('error'))
@ -613,7 +653,8 @@ class Session:
lobby.remove_chooser(chooser.getplayer()) lobby.remove_chooser(chooser.getplayer())
def transitioning_out_activity_was_freed( def transitioning_out_activity_was_freed(
self, can_show_ad_on_death: bool) -> None: self, can_show_ad_on_death: bool
) -> None:
"""(internal)""" """(internal)"""
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from ba._apputils import garbage_collect from ba._apputils import garbage_collect
@ -632,10 +673,12 @@ class Session:
def _add_chosen_player(self, chooser: ba.Chooser) -> ba.SessionPlayer: def _add_chosen_player(self, chooser: ba.Chooser) -> ba.SessionPlayer:
from ba._team import SessionTeam from ba._team import SessionTeam
sessionplayer = chooser.getplayer() sessionplayer = chooser.getplayer()
assert sessionplayer in self.sessionplayers, ( assert sessionplayer in self.sessionplayers, (
'SessionPlayer not found in session ' 'SessionPlayer not found in session '
'player-list after chooser selection.') 'player-list after chooser selection.'
)
activity = self._activity_weak() activity = self._activity_weak()
assert activity is not None assert activity is not None
@ -646,20 +689,26 @@ class Session:
# We can pass it to the current activity if it has already begun # We can pass it to the current activity if it has already begun
# (otherwise it'll get passed once begin is called). # (otherwise it'll get passed once begin is called).
pass_to_activity = (activity.has_begun() pass_to_activity = (
and not activity.is_joining_activity) activity.has_begun() and not activity.is_joining_activity
)
# However, if we're not allowing mid-game joins, don't actually pass; # However, if we're not allowing mid-game joins, don't actually pass;
# just announce the arrival and say they'll partake next round. # just announce the arrival and say they'll partake next round.
if pass_to_activity: if pass_to_activity:
if not (activity.allow_mid_activity_joins if not (
and self.should_allow_mid_activity_joins(activity)): activity.allow_mid_activity_joins
and self.should_allow_mid_activity_joins(activity)
):
pass_to_activity = False pass_to_activity = False
with _ba.Context(self): with _ba.Context(self):
_ba.screenmessage( _ba.screenmessage(
Lstr(resource='playerDelayedJoinText', Lstr(
subs=[('${PLAYER}', resource='playerDelayedJoinText',
sessionplayer.getname(full=True))]), subs=[
('${PLAYER}', sessionplayer.getname(full=True))
],
),
color=(0, 1, 0), color=(0, 1, 0),
) )
@ -691,10 +740,12 @@ class Session:
assert sessionplayer not in sessionteam.players assert sessionplayer not in sessionteam.players
sessionteam.players.append(sessionplayer) sessionteam.players.append(sessionplayer)
sessionplayer.setdata(team=sessionteam, sessionplayer.setdata(
character=chooser.get_character_name(), team=sessionteam,
color=chooser.get_color(), character=chooser.get_character_name(),
highlight=chooser.get_highlight()) color=chooser.get_color(),
highlight=chooser.get_highlight(),
)
self.stats.register_sessionplayer(sessionplayer) self.stats.register_sessionplayer(sessionplayer)
if pass_to_activity: if pass_to_activity:

View File

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

View File

@ -9,8 +9,13 @@ from typing import TYPE_CHECKING
from dataclasses import dataclass from dataclasses import dataclass
import _ba import _ba
from ba._error import (print_exception, print_error, SessionTeamNotFoundError, from ba._error import (
SessionPlayerNotFoundError, NotFoundError) print_exception,
print_error,
SessionTeamNotFoundError,
SessionPlayerNotFoundError,
NotFoundError,
)
if TYPE_CHECKING: if TYPE_CHECKING:
import ba import ba
@ -37,10 +42,16 @@ class PlayerRecord:
still present (stats may be retained for players that leave still present (stats may be retained for players that leave
mid-game) mid-game)
""" """
character: str character: str
def __init__(self, name: str, name_full: str, def __init__(
sessionplayer: ba.SessionPlayer, stats: ba.Stats): self,
name: str,
name_full: str,
sessionplayer: ba.SessionPlayer,
stats: ba.Stats,
):
self.name = name self.name = name
self.name_full = name_full self.name_full = name_full
self.score = 0 self.score = 0
@ -105,8 +116,9 @@ class PlayerRecord:
return stats.getactivity() return stats.getactivity()
return None return None
def associate_with_sessionplayer(self, def associate_with_sessionplayer(
sessionplayer: ba.SessionPlayer) -> None: self, sessionplayer: ba.SessionPlayer
) -> None:
"""Associate this entry with a ba.SessionPlayer.""" """Associate this entry with a ba.SessionPlayer."""
self._sessionteam = weakref.ref(sessionplayer.sessionteam) self._sessionteam = weakref.ref(sessionplayer.sessionteam)
self.character = sessionplayer.character self.character = sessionplayer.character
@ -129,6 +141,7 @@ class PlayerRecord:
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
from ba._language import Lstr from ba._language import Lstr
from ba._general import Call from ba._general import Call
self._multi_kill_count += 1 self._multi_kill_count += 1
stats = self._stats() stats = self._stats()
assert stats assert stats
@ -169,16 +182,23 @@ class PlayerRecord:
sound = stats.orchestrahitsound4 sound = stats.orchestrahitsound4
else: else:
score = 100 score = 100
name = Lstr(resource='multiKillText', name = Lstr(
subs=[('${COUNT}', str(self._multi_kill_count))]) resource='multiKillText',
subs=[('${COUNT}', str(self._multi_kill_count))],
)
color = (1.0, 0.5, 0.0, 1) color = (1.0, 0.5, 0.0, 1)
scale = 1.3 scale = 1.3
delay = 1.0 delay = 1.0
sound = stats.orchestrahitsound4 sound = stats.orchestrahitsound4
def _apply(name2: Lstr, score2: int, showpoints2: bool, def _apply(
color2: tuple[float, float, float, float], scale2: float, name2: Lstr,
sound2: ba.Sound | None) -> None: score2: int,
showpoints2: bool,
color2: tuple[float, float, float, float],
scale2: float,
sound2: ba.Sound | None,
) -> None:
from bastd.actor.popuptext import PopupText from bastd.actor.popuptext import PopupText
# Only award this if they're still alive and we can get # Only award this if they're still alive and we can get
@ -194,18 +214,23 @@ class PlayerRecord:
return return
# Jitter position a bit since these often come in clusters. # 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 = _ba.Vec3(
our_pos[1] + (random.random() - 0.5) * 2.0, our_pos[0] + (random.random() - 0.5) * 2.0,
our_pos[2] + (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() activity = self.getactivity()
if activity is not None: if activity is not None:
PopupText(Lstr( PopupText(
value=(('+' + str(score2) + ' ') if showpoints2 else '') + Lstr(
'${N}', value=(('+' + str(score2) + ' ') if showpoints2 else '')
subs=[('${N}', name2)]), + '${N}',
color=color2, subs=[('${N}', name2)],
scale=scale2, ),
position=our_pos).autoretain() color=color2,
scale=scale2,
position=our_pos,
).autoretain()
if sound2: if sound2:
_ba.playsound(sound2) _ba.playsound(sound2)
@ -219,7 +244,8 @@ class PlayerRecord:
if name is not None: if name is not None:
_ba.timer( _ba.timer(
0.3 + delay, 0.3 + delay,
Call(_apply, name, score, showpoints, color, scale, sound)) Call(_apply, name, score, showpoints, color, scale, sound),
)
# Keep the tally rollin'... # Keep the tally rollin'...
# set a timer for a bit in the future. # set a timer for a bit in the future.
@ -296,8 +322,9 @@ class Stats:
self._player_records[name].associate_with_sessionplayer(player) self._player_records[name].associate_with_sessionplayer(player)
else: else:
name_full = player.getname(full=True) name_full = player.getname(full=True)
self._player_records[name] = PlayerRecord(name, name_full, player, self._player_records[name] = PlayerRecord(
self) name, name_full, player, self
)
def get_records(self) -> dict[str, ba.PlayerRecord]: def get_records(self) -> dict[str, ba.PlayerRecord]:
"""Get PlayerRecord corresponding to still-existing players.""" """Get PlayerRecord corresponding to still-existing players."""
@ -311,20 +338,22 @@ class Stats:
records[record_id] = record records[record_id] = record
return records return records
def player_scored(self, def player_scored(
player: ba.Player, self,
base_points: int = 1, player: ba.Player,
target: Sequence[float] | None = None, base_points: int = 1,
kill: bool = False, target: Sequence[float] | None = None,
victim_player: ba.Player | None = None, kill: bool = False,
scale: float = 1.0, victim_player: ba.Player | None = None,
color: Sequence[float] | None = None, scale: float = 1.0,
title: str | ba.Lstr | None = None, color: Sequence[float] | None = None,
screenmessage: bool = True, title: str | ba.Lstr | None = None,
display: bool = True, screenmessage: bool = True,
importance: int = 1, display: bool = True,
showpoints: bool = True, importance: int = 1,
big_message: bool = False) -> int: showpoints: bool = True,
big_message: bool = False,
) -> int:
"""Register a score for the player. """Register a score for the player.
Return value is actual score with multipliers and such factored in. Return value is actual score with multipliers and such factored in.
@ -338,6 +367,7 @@ class Stats:
from ba import _math from ba import _math
from ba._gameactivity import GameActivity from ba._gameactivity import GameActivity
from ba._language import Lstr from ba._language import Lstr
del victim_player # Currently unused. del victim_player # Currently unused.
name = player.getname() name = player.getname()
s_player = self._player_records[name] s_player = self._player_records[name]
@ -361,9 +391,12 @@ class Stats:
if isinstance(activity, GameActivity): if isinstance(activity, GameActivity):
name_full = player.getname(full=True, icon=False) name_full = player.getname(full=True, icon=False)
activity.show_zoom_message( activity.show_zoom_message(
Lstr(resource='nameScoresText', Lstr(
subs=[('${NAME}', name_full)]), resource='nameScoresText',
color=_math.normalized_color(player.team.color)) subs=[('${NAME}', name_full)],
),
color=_math.normalized_color(player.team.color),
)
except Exception: except Exception:
print_exception('error showing big_message') print_exception('error showing big_message')
@ -376,21 +409,26 @@ class Stats:
# If display-pos is *way* lower than us, raise it up # If display-pos is *way* lower than us, raise it up
# (so we can still see scores from dudes that fell off cliffs). # (so we can still see scores from dudes that fell off cliffs).
display_pos = (target[0], max(target[1], our_pos[1] - 2.0), display_pos = (
min(target[2], our_pos[2] + 2.0)) target[0],
max(target[1], our_pos[1] - 2.0),
min(target[2], our_pos[2] + 2.0),
)
activity = self.getactivity() activity = self.getactivity()
if activity is not None: if activity is not None:
if title is not None: if title is not None:
sval = Lstr(value='+${A} ${B}', sval = Lstr(
subs=[('${A}', str(points)), value='+${A} ${B}',
('${B}', title)]) subs=[('${A}', str(points)), ('${B}', title)],
)
else: else:
sval = Lstr(value='+${A}', sval = Lstr(value='+${A}', subs=[('${A}', str(points))])
subs=[('${A}', str(points))]) PopupText(
PopupText(sval, sval,
color=display_color, color=display_color,
scale=1.2 * scale, scale=1.2 * scale,
position=display_pos).autoretain() position=display_pos,
).autoretain()
# Tally kills. # Tally kills.
if kill: if kill:
@ -400,11 +438,12 @@ class Stats:
# Report non-kill scorings. # Report non-kill scorings.
try: try:
if screenmessage and not kill: if screenmessage and not kill:
_ba.screenmessage(Lstr(resource='nameScoresText', _ba.screenmessage(
subs=[('${NAME}', name)]), Lstr(resource='nameScoresText', subs=[('${NAME}', name)]),
top=True, top=True,
color=player.color, color=player.color,
image=player.get_icon()) image=player.get_icon(),
)
except Exception: except Exception:
print_exception('error announcing score') print_exception('error announcing score')
@ -419,12 +458,15 @@ class Stats:
return points return points
def player_was_killed(self, def player_was_killed(
player: ba.Player, self,
killed: bool = False, player: ba.Player,
killer: ba.Player | None = None) -> None: killed: bool = False,
killer: ba.Player | None = None,
) -> None:
"""Should be called when a player is killed.""" """Should be called when a player is killed."""
from ba._language import Lstr from ba._language import Lstr
name = player.getname() name = player.getname()
prec = self._player_records[name] prec = self._player_records[name]
prec.streak = 0 prec.streak = 0
@ -434,33 +476,47 @@ class Stats:
try: try:
if killed and _ba.getactivity().announce_player_deaths: if killed and _ba.getactivity().announce_player_deaths:
if killer is player: if killer is player:
_ba.screenmessage(Lstr(resource='nameSuicideText', _ba.screenmessage(
subs=[('${NAME}', name)]), Lstr(
top=True, resource='nameSuicideText', subs=[('${NAME}', name)]
color=player.color, ),
image=player.get_icon()) top=True,
color=player.color,
image=player.get_icon(),
)
elif killer is not None: elif killer is not None:
if killer.team is player.team: if killer.team is player.team:
_ba.screenmessage(Lstr(resource='nameBetrayedText', _ba.screenmessage(
subs=[('${NAME}', Lstr(
killer.getname()), resource='nameBetrayedText',
('${VICTIM}', name)]), subs=[
top=True, ('${NAME}', killer.getname()),
color=killer.color, ('${VICTIM}', name),
image=killer.get_icon()) ],
),
top=True,
color=killer.color,
image=killer.get_icon(),
)
else: else:
_ba.screenmessage(Lstr(resource='nameKilledText', _ba.screenmessage(
subs=[('${NAME}', Lstr(
killer.getname()), resource='nameKilledText',
('${VICTIM}', name)]), subs=[
top=True, ('${NAME}', killer.getname()),
color=killer.color, ('${VICTIM}', name),
image=killer.get_icon()) ],
),
top=True,
color=killer.color,
image=killer.get_icon(),
)
else: else:
_ba.screenmessage(Lstr(resource='nameDiedText', _ba.screenmessage(
subs=[('${NAME}', name)]), Lstr(resource='nameDiedText', subs=[('${NAME}', name)]),
top=True, top=True,
color=player.color, color=player.color,
image=player.get_icon()) image=player.get_icon(),
)
except Exception: except Exception:
print_exception('error announcing kill') 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 # pylint: disable=cyclic-import
from ba import _language from ba import _language
from ba import _map from ba import _map
item_info = get_store_item(item_name) item_info = get_store_item(item_name)
if item_name.startswith('characters.'): if item_name.startswith('characters.'):
return _language.Lstr(translate=('characterNames', return _language.Lstr(
item_info['character'])) translate=('characterNames', item_info['character'])
)
if item_name in ['upgrades.pro', 'pro']: if item_name in ['upgrades.pro', 'pro']:
return _language.Lstr(resource='store.bombSquadProNameText', return _language.Lstr(
subs=[('${APP_NAME}', resource='store.bombSquadProNameText',
_language.Lstr(resource='titleText'))]) subs=[('${APP_NAME}', _language.Lstr(resource='titleText'))],
)
if item_name.startswith('maps.'): if item_name.startswith('maps.'):
map_type: type[ba.Map] = item_info['map_type'] map_type: type[ba.Map] = item_info['map_type']
return _map.get_map_display_string(map_type.name) return _map.get_map_display_string(map_type.name)
@ -64,6 +67,7 @@ def get_store_items() -> dict[str, dict]:
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from ba._generated.enums import SpecialChar from ba._generated.enums import SpecialChar
from bastd import maps from bastd import maps
if _ba.app.store_items is None: if _ba.app.store_items is None:
from bastd.game import ninjafight from bastd.game import ninjafight
from bastd.game import meteorshower 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. # IMPORTANT - need to keep this synced with the master server.
# (doing so manually for now) # (doing so manually for now)
_ba.app.store_items = { _ba.app.store_items = {
'characters.kronk': { 'characters.kronk': {'character': 'Kronk'},
'character': 'Kronk' 'characters.zoe': {'character': 'Zoe'},
}, 'characters.jackmorgan': {'character': 'Jack Morgan'},
'characters.zoe': { 'characters.mel': {'character': 'Mel'},
'character': 'Zoe' 'characters.snakeshadow': {'character': 'Snake Shadow'},
}, 'characters.bones': {'character': 'Bones'},
'characters.jackmorgan': {
'character': 'Jack Morgan'
},
'characters.mel': {
'character': 'Mel'
},
'characters.snakeshadow': {
'character': 'Snake Shadow'
},
'characters.bones': {
'character': 'Bones'
},
'characters.bernard': { 'characters.bernard': {
'character': 'Bernard', 'character': 'Bernard',
'highlight': (0.6, 0.5, 0.8) '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'
}, },
'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': {}, 'pro': {},
'maps.lake_frigid': { 'maps.lake_frigid': {'map_type': maps.LakeFrigid},
'map_type': maps.LakeFrigid
},
'games.ninja_fight': { 'games.ninja_fight': {
'gametype': ninjafight.NinjaFightGame, 'gametype': ninjafight.NinjaFightGame,
'previewTex': 'courtyardPreview' 'previewTex': 'courtyardPreview',
}, },
'games.meteor_shower': { 'games.meteor_shower': {
'gametype': meteorshower.MeteorShowerGame, 'gametype': meteorshower.MeteorShowerGame,
'previewTex': 'rampagePreview' 'previewTex': 'rampagePreview',
}, },
'games.target_practice': { 'games.target_practice': {
'gametype': targetpractice.TargetPracticeGame, 'gametype': targetpractice.TargetPracticeGame,
'previewTex': 'doomShroomPreview' 'previewTex': 'doomShroomPreview',
}, },
'games.easter_egg_hunt': { 'games.easter_egg_hunt': {
'gametype': easteregghunt.EasterEggHuntGame, 'gametype': easteregghunt.EasterEggHuntGame,
'previewTex': 'towerDPreview' 'previewTex': 'towerDPreview',
}, },
'icons.flag_us': { 'icons.flag_us': {
'icon': _ba.charstr(SpecialChar.FLAG_UNITED_STATES) 'icon': _ba.charstr(SpecialChar.FLAG_UNITED_STATES)
}, },
'icons.flag_mexico': { 'icons.flag_mexico': {'icon': _ba.charstr(SpecialChar.FLAG_MEXICO)},
'icon': _ba.charstr(SpecialChar.FLAG_MEXICO)
},
'icons.flag_germany': { 'icons.flag_germany': {
'icon': _ba.charstr(SpecialChar.FLAG_GERMANY) 'icon': _ba.charstr(SpecialChar.FLAG_GERMANY)
}, },
'icons.flag_brazil': { 'icons.flag_brazil': {'icon': _ba.charstr(SpecialChar.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_russia': {
'icon': _ba.charstr(SpecialChar.FLAG_RUSSIA)
},
'icons.flag_china': {
'icon': _ba.charstr(SpecialChar.FLAG_CHINA)
},
'icons.flag_uk': { 'icons.flag_uk': {
'icon': _ba.charstr(SpecialChar.FLAG_UNITED_KINGDOM) 'icon': _ba.charstr(SpecialChar.FLAG_UNITED_KINGDOM)
}, },
'icons.flag_canada': { 'icons.flag_canada': {'icon': _ba.charstr(SpecialChar.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_india': { 'icons.flag_france': {'icon': _ba.charstr(SpecialChar.FLAG_FRANCE)},
'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': { 'icons.flag_indonesia': {
'icon': _ba.charstr(SpecialChar.FLAG_INDONESIA) 'icon': _ba.charstr(SpecialChar.FLAG_INDONESIA)
}, },
'icons.flag_italy': { 'icons.flag_italy': {'icon': _ba.charstr(SpecialChar.FLAG_ITALY)},
'icon': _ba.charstr(SpecialChar.FLAG_ITALY)
},
'icons.flag_south_korea': { 'icons.flag_south_korea': {
'icon': _ba.charstr(SpecialChar.FLAG_SOUTH_KOREA) 'icon': _ba.charstr(SpecialChar.FLAG_SOUTH_KOREA)
}, },
@ -190,15 +144,9 @@ def get_store_items() -> dict[str, dict]:
'icons.flag_uae': { 'icons.flag_uae': {
'icon': _ba.charstr(SpecialChar.FLAG_UNITED_ARAB_EMIRATES) 'icon': _ba.charstr(SpecialChar.FLAG_UNITED_ARAB_EMIRATES)
}, },
'icons.flag_qatar': { 'icons.flag_qatar': {'icon': _ba.charstr(SpecialChar.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_egypt': {
'icon': _ba.charstr(SpecialChar.FLAG_EGYPT)
},
'icons.flag_kuwait': {
'icon': _ba.charstr(SpecialChar.FLAG_KUWAIT)
},
'icons.flag_algeria': { 'icons.flag_algeria': {
'icon': _ba.charstr(SpecialChar.FLAG_ALGERIA) 'icon': _ba.charstr(SpecialChar.FLAG_ALGERIA)
}, },
@ -217,69 +165,33 @@ def get_store_items() -> dict[str, dict]:
'icons.flag_singapore': { 'icons.flag_singapore': {
'icon': _ba.charstr(SpecialChar.FLAG_SINGAPORE) 'icon': _ba.charstr(SpecialChar.FLAG_SINGAPORE)
}, },
'icons.flag_iran': { 'icons.flag_iran': {'icon': _ba.charstr(SpecialChar.FLAG_IRAN)},
'icon': _ba.charstr(SpecialChar.FLAG_IRAN) 'icons.flag_poland': {'icon': _ba.charstr(SpecialChar.FLAG_POLAND)},
},
'icons.flag_poland': {
'icon': _ba.charstr(SpecialChar.FLAG_POLAND)
},
'icons.flag_argentina': { 'icons.flag_argentina': {
'icon': _ba.charstr(SpecialChar.FLAG_ARGENTINA) 'icon': _ba.charstr(SpecialChar.FLAG_ARGENTINA)
}, },
'icons.flag_philippines': { 'icons.flag_philippines': {
'icon': _ba.charstr(SpecialChar.FLAG_PHILIPPINES) 'icon': _ba.charstr(SpecialChar.FLAG_PHILIPPINES)
}, },
'icons.flag_chile': { 'icons.flag_chile': {'icon': _ba.charstr(SpecialChar.FLAG_CHILE)},
'icon': _ba.charstr(SpecialChar.FLAG_CHILE) 'icons.fedora': {'icon': _ba.charstr(SpecialChar.FEDORA)},
}, 'icons.hal': {'icon': _ba.charstr(SpecialChar.HAL)},
'icons.fedora': { 'icons.crown': {'icon': _ba.charstr(SpecialChar.CROWN)},
'icon': _ba.charstr(SpecialChar.FEDORA) 'icons.yinyang': {'icon': _ba.charstr(SpecialChar.YIN_YANG)},
}, 'icons.eyeball': {'icon': _ba.charstr(SpecialChar.EYE_BALL)},
'icons.hal': { 'icons.skull': {'icon': _ba.charstr(SpecialChar.SKULL)},
'icon': _ba.charstr(SpecialChar.HAL) 'icons.heart': {'icon': _ba.charstr(SpecialChar.HEART)},
}, 'icons.dragon': {'icon': _ba.charstr(SpecialChar.DRAGON)},
'icons.crown': { 'icons.helmet': {'icon': _ba.charstr(SpecialChar.HELMET)},
'icon': _ba.charstr(SpecialChar.CROWN) 'icons.mushroom': {'icon': _ba.charstr(SpecialChar.MUSHROOM)},
}, 'icons.ninja_star': {'icon': _ba.charstr(SpecialChar.NINJA_STAR)},
'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': { 'icons.viking_helmet': {
'icon': _ba.charstr(SpecialChar.VIKING_HELMET) 'icon': _ba.charstr(SpecialChar.VIKING_HELMET)
}, },
'icons.moon': { 'icons.moon': {'icon': _ba.charstr(SpecialChar.MOON)},
'icon': _ba.charstr(SpecialChar.MOON) 'icons.spider': {'icon': _ba.charstr(SpecialChar.SPIDER)},
}, 'icons.fireball': {'icon': _ba.charstr(SpecialChar.FIREBALL)},
'icons.spider': { 'icons.mikirog': {'icon': _ba.charstr(SpecialChar.MIKIROG)},
'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 store_items = _ba.app.store_items
assert store_items is not None 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]]]: def get_store_layout() -> dict[str, list[dict[str, Any]]]:
"""Return what's available in the store at a given time. """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: if _ba.app.store_layout is None:
_ba.app.store_layout = { _ba.app.store_layout = {
'characters': [{ 'characters': [{'items': []}],
'items': [] 'extras': [{'items': ['pro']}],
}], 'maps': [{'items': ['maps.lake_frigid']}],
'extras': [{
'items': ['pro']
}],
'maps': [{
'items': ['maps.lake_frigid']
}],
'minigames': [], 'minigames': [],
'icons': [{ 'icons': [
'items': [ {
'icons.mushroom', 'items': [
'icons.heart', 'icons.mushroom',
'icons.eyeball', 'icons.heart',
'icons.yinyang', 'icons.eyeball',
'icons.hal', 'icons.yinyang',
'icons.flag_us', 'icons.hal',
'icons.flag_mexico', 'icons.flag_us',
'icons.flag_germany', 'icons.flag_mexico',
'icons.flag_brazil', 'icons.flag_germany',
'icons.flag_russia', 'icons.flag_brazil',
'icons.flag_china', 'icons.flag_russia',
'icons.flag_uk', 'icons.flag_china',
'icons.flag_canada', 'icons.flag_uk',
'icons.flag_india', 'icons.flag_canada',
'icons.flag_japan', 'icons.flag_india',
'icons.flag_france', 'icons.flag_japan',
'icons.flag_indonesia', 'icons.flag_france',
'icons.flag_italy', 'icons.flag_indonesia',
'icons.flag_south_korea', 'icons.flag_italy',
'icons.flag_netherlands', 'icons.flag_south_korea',
'icons.flag_uae', 'icons.flag_netherlands',
'icons.flag_qatar', 'icons.flag_uae',
'icons.flag_egypt', 'icons.flag_qatar',
'icons.flag_kuwait', 'icons.flag_egypt',
'icons.flag_algeria', 'icons.flag_kuwait',
'icons.flag_saudi_arabia', 'icons.flag_algeria',
'icons.flag_malaysia', 'icons.flag_saudi_arabia',
'icons.flag_czech_republic', 'icons.flag_malaysia',
'icons.flag_australia', 'icons.flag_czech_republic',
'icons.flag_singapore', 'icons.flag_australia',
'icons.flag_iran', 'icons.flag_singapore',
'icons.flag_poland', 'icons.flag_iran',
'icons.flag_argentina', 'icons.flag_poland',
'icons.flag_philippines', 'icons.flag_argentina',
'icons.flag_chile', 'icons.flag_philippines',
'icons.moon', 'icons.flag_chile',
'icons.fedora', 'icons.moon',
'icons.spider', 'icons.fedora',
'icons.ninja_star', 'icons.spider',
'icons.skull', 'icons.ninja_star',
'icons.dragon', 'icons.skull',
'icons.viking_helmet', 'icons.dragon',
'icons.fireball', 'icons.viking_helmet',
'icons.helmet', 'icons.fireball',
'icons.crown', 'icons.helmet',
] 'icons.crown',
}] ]
}
],
} }
store_layout = _ba.app.store_layout store_layout = _ba.app.store_layout
assert store_layout is not None assert store_layout is not None
store_layout['characters'] = [{ store_layout['characters'] = [
'items': [ {
'characters.kronk', 'characters.zoe', 'characters.jackmorgan', 'items': [
'characters.mel', 'characters.snakeshadow', 'characters.bones', 'characters.kronk',
'characters.bernard', 'characters.agent', 'characters.frosty', 'characters.zoe',
'characters.pascal', 'characters.pixie' 'characters.jackmorgan',
] 'characters.mel',
}] 'characters.snakeshadow',
store_layout['minigames'] = [{ 'characters.bones',
'items': [ 'characters.bernard',
'games.ninja_fight', 'games.meteor_shower', 'games.target_practice' '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): 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.santa')
store_layout['characters'][0]['items'].append('characters.wizard') store_layout['characters'][0]['items'].append('characters.wizard')
store_layout['characters'][0]['items'].append('characters.cyborg') store_layout['characters'][0]['items'].append('characters.cyborg')
if _internal.get_v1_account_misc_read_val('easter', False): if _internal.get_v1_account_misc_read_val('easter', False):
store_layout['characters'].append({ store_layout['characters'].append(
'title': 'store.holidaySpecialText', {'title': 'store.holidaySpecialText', 'items': ['characters.bunny']}
'items': ['characters.bunny'] )
}) store_layout['minigames'].append(
store_layout['minigames'].append({ {
'title': 'store.holidaySpecialText', 'title': 'store.holidaySpecialText',
'items': ['games.easter_egg_hunt'] 'items': ['games.easter_egg_hunt'],
}) }
)
return store_layout return store_layout
@ -394,7 +316,7 @@ def get_clean_price(price_string: str) -> str:
'$4.99': '$5.00', '$4.99': '$5.00',
'$9.99': '$10.00', '$9.99': '$10.00',
'$19.99': '$20.00', '$19.99': '$20.00',
'$49.99': '$50.00' '$49.99': '$50.00',
} }
return psubs.get(price_string, price_string) return psubs.get(price_string, price_string)
@ -418,19 +340,23 @@ def get_available_purchase_count(tab: str | None = None) -> int:
return count return count
except Exception: except Exception:
from ba import _error from ba import _error
_error.print_exception('error calcing available purchases') _error.print_exception('error calcing available purchases')
return 0 return 0
def _calc_count_for_tab(tabval: list[dict[str, Any]], our_tickets: int, def _calc_count_for_tab(
count: int) -> int: tabval: list[dict[str, Any]], our_tickets: int, count: int
) -> int:
for section in tabval: for section in tabval:
for item in section['items']: for item in section['items']:
ticket_cost = _internal.get_v1_account_misc_read_val( ticket_cost = _internal.get_v1_account_misc_read_val(
'price.' + item, None) 'price.' + item, None
)
if ticket_cost is not None: if ticket_cost is not None:
if (our_tickets >= ticket_cost if our_tickets >= ticket_cost and not _internal.get_purchased(
and not _internal.get_purchased(item)): item
):
count += 1 count += 1
return count return count
@ -443,6 +369,7 @@ def get_available_sale_time(tab: str) -> int | None:
try: try:
import datetime import datetime
from ba._generated.enums import TimeType, TimeFormat from ba._generated.enums import TimeType, TimeFormat
app = _ba.app app = _ba.app
sale_times: list[int | None] = [] 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 we've got a time-remaining in our config, start there.
if 'PSTR' in config: if 'PSTR' in config:
app.pro_sale_start_time = int( 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'] app.pro_sale_start_val = config['PSTR']
else: else:
# We start the timer once we get the duration from # We start the timer once we get the duration from
# the server. # the server.
start_duration = _internal.get_v1_account_misc_read_val( start_duration = _internal.get_v1_account_misc_read_val(
'proSaleDurationMinutes', None) 'proSaleDurationMinutes', None
)
if start_duration is not None: if start_duration is not None:
app.pro_sale_start_time = int( app.pro_sale_start_time = int(
_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS)) _ba.time(TimeType.REAL, TimeFormat.MILLISECONDS)
app.pro_sale_start_val = (60000 * start_duration) )
app.pro_sale_start_val = 60000 * start_duration
# If we haven't heard from the server yet, no sale.. # If we haven't heard from the server yet, no sale..
else: else:
@ -477,9 +407,13 @@ def get_available_sale_time(tab: str) -> int | None:
assert app.pro_sale_start_val is not None assert app.pro_sale_start_val is not None
val: int | None = max( val: int | None = max(
0, app.pro_sale_start_val - 0,
(_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS) - app.pro_sale_start_val
app.pro_sale_start_time)) - (
_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS)
- app.pro_sale_start_time
),
)
# Keep the value in the config up to date. I suppose we should # Keep the value in the config up to date. I suppose we should
# write the config occasionally but it should happen often enough # 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']: for item in section['items']:
if item in sales_raw: if item in sales_raw:
if not _internal.get_purchased(item): if not _internal.get_purchased(item):
to_end = ((datetime.datetime.utcfromtimestamp( to_end = (
sales_raw[item]['e']) - datetime.datetime.utcfromtimestamp(
datetime.datetime.utcnow()).total_seconds()) sales_raw[item]['e']
)
- datetime.datetime.utcnow()
).total_seconds()
if to_end > 0: if to_end > 0:
sale_times.append(int(to_end * 1000)) sale_times.append(int(to_end * 1000))
@ -508,6 +445,7 @@ def get_available_sale_time(tab: str) -> int | None:
except Exception: except Exception:
from ba import _error from ba import _error
_error.print_exception('error calcing sale time') _error.print_exception('error calcing sale time')
return None return None
@ -540,5 +478,6 @@ def get_unowned_game_types() -> set[type[ba.GameActivity]]:
return unowned_games return unowned_games
except Exception: except Exception:
from ba import _error from ba import _error
_error.print_exception('error calcing un-owned games') _error.print_exception('error calcing un-owned games')
return set() return set()

View File

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

View File

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

View File

@ -27,53 +27,94 @@ def get_next_tip() -> str:
def get_all_tips() -> list[str]: def get_all_tips() -> list[str]:
"""Return the complete list of tips.""" """Return the complete list of tips."""
tips = [ tips = [
('If you are short on controllers, install the \'${REMOTE_APP_NAME}\' ' (
'app\non your mobile devices to use them as controllers.'), 'If you are short on controllers,'
('Create player profiles for yourself and your friends with\nyour ' ' install the \'${REMOTE_APP_NAME}\' app\n'
'preferred names and appearances instead of using random ones.'), 'on your mobile devices to use them as controllers.'
('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 ' 'Create player profiles for yourself and your friends with\nyour '
'health powerup in the next few seconds.'), 'preferred names and appearances instead of using random ones.'
('A perfectly timed running-jumping-spin-punch can kill in a single ' ),
'hit\nand earn you lifelong respect from your friends.'), (
'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.', 'Always remember to floss.',
'Don\'t run all the time. Really. You will fall off cliffs.', '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 ' 'In Capture-the-Flag, your own flag must be at your base to score, '
'a good way to stop them.'), 'If the other\nteam is about to score, stealing their flag can be '
('If you get a sticky-bomb stuck to you, jump around and spin in ' 'a good way to stop them.'
'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 ' 'If you get a sticky-bomb stuck to you, jump around and spin in '
'whack your head on things.'), '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.', '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.', '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.'), 'Throw strength is based on the direction you are holding.\n'
('If someone picks you up, punch them and they\'ll let go.\nThis ' 'To toss something gently in front of you, don\'t'
'works in real life too.'), ' hold any direction.'
('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. ' 'If someone picks you up, punch them and they\'ll let go.\nThis '
'Tossing\nyour enemies off cliffs can be an effective and ' 'works in real life too.'
'emotionally fulfilling strategy.'), ),
('Ice bombs are not very powerful, but they freeze\nwhoever they ' (
'hit, leaving them vulnerable to shattering.'), '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.', '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.'), 'Run back and forth before throwing a bomb\nto \'whiplash\' it '
('Punches do more damage the faster your fists are moving,\nso ' 'and throw it farther.'
'try running, jumping, and spinning like crazy.'), ),
(
'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.', '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.'), 'The head is the most vulnerable area, so a sticky-bomb\nto the '
('Hold down any button to run. You\'ll get places faster\nbut ' 'noggin usually means game-over.'
'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.'), '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 app = _ba.app
if not app.iircade_mode: 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 ' 'If your framerate is choppy, try turning down resolution\nor '
'visuals in the game\'s graphics settings.' 'visuals in the game\'s graphics settings.'
] ]
if (app.platform in ('android', 'ios') and not app.on_tv if (
and not app.iircade_mode): app.platform in ('android', 'ios')
and not app.on_tv
and not app.iircade_mode
):
tips += [ tips += [
('If your device gets too warm or you\'d like to conserve ' (
'battery power,\nturn down "Visuals" or "Resolution" ' 'If your device gets too warm or you\'d like to conserve '
'in Settings->Graphics'), 'battery power,\nturn down "Visuals" or "Resolution" '
'in Settings->Graphics'
),
] ]
if app.platform in ['mac', 'android'] and not app.iircade_mode: if app.platform in ['mac', 'android'] and not app.iircade_mode:
tips += [ tips += [

View File

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

View File

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

View File

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

View File

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

View File

@ -24,12 +24,18 @@ class MacMusicAppMusicPlayer(MusicPlayer):
self._thread = _MacMusicAppThread() self._thread = _MacMusicAppThread()
self._thread.start() self._thread.start()
def on_select_entry(self, callback: Callable[[Any], None], def on_select_entry(
current_entry: Any, selection_target_name: str) -> Any: self,
callback: Callable[[Any], None],
current_entry: Any,
selection_target_name: str,
) -> Any:
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from bastd.ui.soundtrack import entrytypeselect as etsel 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: def on_set_volume(self, volume: float) -> None:
self._thread.set_volume(volume) self._thread.set_volume(volume)
@ -44,8 +50,10 @@ class MacMusicAppMusicPlayer(MusicPlayer):
if entry_type == 'iTunesPlaylist': if entry_type == 'iTunesPlaylist':
self._thread.play_playlist(music.get_soundtrack_entry_name(entry)) self._thread.play_playlist(music.get_soundtrack_entry_name(entry))
else: else:
print('MacMusicAppMusicPlayer passed unrecognized entry type:', print(
entry_type) 'MacMusicAppMusicPlayer passed unrecognized entry type:',
entry_type,
)
def on_stop(self) -> None: def on_stop(self) -> None:
self._thread.play_playlist(None) self._thread.play_playlist(None)
@ -70,6 +78,7 @@ class _MacMusicAppThread(threading.Thread):
from ba._general import Call from ba._general import Call
from ba._language import Lstr from ba._language import Lstr
from ba._generated.enums import TimeType from ba._generated.enums import TimeType
_ba.set_thread_name('BA_MacMusicAppThread') _ba.set_thread_name('BA_MacMusicAppThread')
_ba.mac_music_app_init() _ba.mac_music_app_init()
@ -77,10 +86,15 @@ class _MacMusicAppThread(threading.Thread):
# it causes any funny business (this used to background the app # it causes any funny business (this used to background the app
# sometimes, though I think that is fixed now) # sometimes, though I think that is fixed now)
def do_print() -> None: def do_print() -> None:
_ba.timer(1.0, _ba.timer(
Call(_ba.screenmessage, Lstr(resource='usingItunesText'), 1.0,
(0, 1, 0)), Call(
timetype=TimeType.REAL) _ba.screenmessage,
Lstr(resource='usingItunesText'),
(0, 1, 0),
),
timetype=TimeType.REAL,
)
_ba.pushcall(do_print, from_other_thread=True) _ba.pushcall(do_print, from_other_thread=True)
@ -153,15 +167,29 @@ class _MacMusicAppThread(threading.Thread):
self._commands_available.set() self._commands_available.set()
def _handle_get_playlists_command( def _handle_get_playlists_command(
self, target: Callable[[list[str]], None]) -> None: self, target: Callable[[list[str]], None]
) -> None:
from ba._general import Call from ba._general import Call
try: try:
playlists = _ba.mac_music_app_get_playlists() playlists = _ba.mac_music_app_get_playlists()
playlists = [ playlists = [
p for p in playlists if p not in [ p
'Music', 'Movies', 'TV Shows', 'Podcasts', 'iTunes\xa0U', for p in playlists
'Books', 'Genius', 'iTunes DJ', 'Music Videos', if p
'Home Videos', 'Voice Memos', 'Audiobooks' 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()) 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. # Set our playlist and play it if our volume is up.
self._current_playlist = target self._current_playlist = target
if self._volume > 0: 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._update_mac_music_app_volume()
self._play_current_playlist() self._play_current_playlist()
@ -213,20 +241,30 @@ class _MacMusicAppThread(threading.Thread):
def _play_current_playlist(self) -> None: def _play_current_playlist(self) -> None:
try: try:
from ba._general import Call from ba._general import Call
assert self._current_playlist is not None assert self._current_playlist is not None
if _ba.mac_music_app_play_playlist(self._current_playlist): if _ba.mac_music_app_play_playlist(self._current_playlist):
pass pass
else: else:
_ba.pushcall(Call( _ba.pushcall(
_ba.screenmessage, Call(
_ba.app.lang.get_resource('playlistNotFoundText') + _ba.screenmessage,
': \'' + self._current_playlist + '\'', (1, 0, 0)), _ba.app.lang.get_resource('playlistNotFoundText')
from_other_thread=True) + ': \''
+ self._current_playlist
+ '\'',
(1, 0, 0),
),
from_other_thread=True,
)
except Exception: except Exception:
from ba import _error from ba import _error
_error.print_exception( _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: def _update_mac_music_app_volume(self) -> None:
_ba.mac_music_app_set_volume( _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).""" """If needed, requests storage permission from the user (& return true)."""
from ba._language import Lstr from ba._language import Lstr
from ba._generated.enums import Permission from ba._generated.enums import Permission
if not _ba.have_permission(Permission.STORAGE): if not _ba.have_permission(Permission.STORAGE):
_ba.playsound(_ba.getsound('error')) _ba.playsound(_ba.getsound('error'))
_ba.screenmessage(Lstr(resource='storagePermissionAccessText'), _ba.screenmessage(
color=(1, 0, 0)) Lstr(resource='storagePermissionAccessText'), color=(1, 0, 0)
)
_ba.timer(1.0, lambda: _ba.request_permission(Permission.STORAGE)) _ba.timer(1.0, lambda: _ba.request_permission(Permission.STORAGE))
return True return True
return False return False
@ -80,12 +82,15 @@ def show_user_scripts() -> None:
if usd is not None and os.path.isdir(usd): if usd is not None and os.path.isdir(usd):
file_name = usd + '/about_this_folder.txt' file_name = usd + '/about_this_folder.txt'
with open(file_name, 'w', encoding='utf-8') as outfile: with open(file_name, 'w', encoding='utf-8') as outfile:
outfile.write('You can drop files in here to mod the game.' outfile.write(
' See settings/advanced' 'You can drop files in here to mod the game.'
' in the game for more info.') ' See settings/advanced'
' in the game for more info.'
)
_ba.android_media_scan_file(file_name) _ba.android_media_scan_file(file_name)
except Exception: except Exception:
from ba import _error from ba import _error
_error.print_exception('error writing about_this_folder stuff') _error.print_exception('error writing about_this_folder stuff')
# On a few platforms we try to open the dir in the UI. # 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) (for editing and experiment with)
""" """
import shutil import shutil
app = _ba.app app = _ba.app
# First off, if we need permission for this, ask for it. # First off, if we need permission for this, ask for it.
if _request_storage_permission(): if _request_storage_permission():
return return
path = (app.python_directory_user + '/sys/' + app.version) path = app.python_directory_user + '/sys/' + app.version
pathtmp = path + '_tmp' pathtmp = path + '_tmp'
if os.path.exists(path): if os.path.exists(path):
shutil.rmtree(path) shutil.rmtree(path)
@ -123,31 +129,38 @@ def create_user_system_scripts() -> None:
# to blow them away anyway to make changes; # to blow them away anyway to make changes;
# See https://github.com/efroemling/ballistica/wiki # See https://github.com/efroemling/ballistica/wiki
# /Knowledge-Nuggets#python-cache-files-gotcha # /Knowledge-Nuggets#python-cache-files-gotcha
return ('__pycache__', ) return ('__pycache__',)
print(f'COPYING "{app.python_directory_app}" -> "{pathtmp}".') print(f'COPYING "{app.python_directory_app}" -> "{pathtmp}".')
shutil.copytree(app.python_directory_app, pathtmp, ignore=_ignore_filter) shutil.copytree(app.python_directory_app, pathtmp, ignore=_ignore_filter)
print(f'MOVING "{pathtmp}" -> "{path}".') print(f'MOVING "{pathtmp}" -> "{path}".')
shutil.move(pathtmp, path) shutil.move(pathtmp, path)
print(f"Created system scripts at :'{path}" print(
f"'\nRestart {_ba.appname()} to use them." f"Created system scripts at :'{path}"
f' (use ba.quit() to exit the game)') f"'\nRestart {_ba.appname()} to use them."
f' (use ba.quit() to exit the game)'
)
if app.platform == 'android': if app.platform == 'android':
print('Note: the new files may not be visible via ' print(
'android-file-transfer until you restart your device.') 'Note: the new files may not be visible via '
'android-file-transfer until you restart your device.'
)
def delete_user_system_scripts() -> None: def delete_user_system_scripts() -> None:
"""Clean out the scripts created by create_user_system_scripts().""" """Clean out the scripts created by create_user_system_scripts()."""
import shutil import shutil
app = _ba.app app = _ba.app
path = (app.python_directory_user + '/sys/' + app.version) path = app.python_directory_user + '/sys/' + app.version
if os.path.exists(path): if os.path.exists(path):
shutil.rmtree(path) shutil.rmtree(path)
print(f'User system scripts deleted.\n' print(
f'Restart {_ba.appname()} to use internal' f'User system scripts deleted.\n'
f' scripts. (use ba.quit() to exit the game)') f'Restart {_ba.appname()} to use internal'
f' scripts. (use ba.quit() to exit the game)'
)
else: else:
print('User system scripts not found.') 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. # FIXME: should ask the C++ layer for these; just hard-coding for now.
return ['mp3', 'ogg', 'm4a', 'wav', 'flac', 'mid'] return ['mp3', 'ogg', 'm4a', 'wav', 'flac', 'mid']
def on_select_entry(self, callback: Callable[[Any], None], def on_select_entry(
current_entry: Any, selection_target_name: str) -> Any: self,
callback: Callable[[Any], None],
current_entry: Any,
selection_target_name: str,
) -> Any:
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from bastd.ui.soundtrack.entrytypeselect import ( from bastd.ui.soundtrack.entrytypeselect import (
SoundtrackEntryTypeSelectWindow) SoundtrackEntryTypeSelectWindow,
return SoundtrackEntryTypeSelectWindow(callback, current_entry, )
selection_target_name)
return SoundtrackEntryTypeSelectWindow(
callback, current_entry, selection_target_name
)
def on_set_volume(self, volume: float) -> None: def on_set_volume(self, volume: float) -> None:
_ba.music_player_set_volume(volume) _ba.music_player_set_volume(volume)
@ -56,22 +63,31 @@ class OSMusicPlayer(MusicPlayer):
# valid file within it. # valid file within it.
self._want_to_play = True self._want_to_play = True
self._actually_playing = False self._actually_playing = False
_PickFolderSongThread(name, self.get_valid_music_file_extensions(), _PickFolderSongThread(
self._on_play_folder_cb).start() name,
self.get_valid_music_file_extensions(),
self._on_play_folder_cb,
).start()
def _on_play_folder_cb(self, def _on_play_folder_cb(
result: str | list[str], self, result: str | list[str], error: str | None = None
error: str | None = None) -> None: ) -> None:
from ba import _language from ba import _language
if error is not None: if error is not None:
rstr = (_language.Lstr( rstr = _language.Lstr(
resource='internal.errorPlayingMusicText').evaluate()) resource='internal.errorPlayingMusicText'
).evaluate()
if isinstance(result, str): if isinstance(result, str):
err_str = (rstr.replace('${MUSIC}', os.path.basename(result)) + err_str = (
'; ' + str(error)) rstr.replace('${MUSIC}', os.path.basename(result))
+ '; '
+ str(error)
)
else: else:
err_str = (rstr.replace('${MUSIC}', '<multiple>') + '; ' + err_str = (
str(error)) rstr.replace('${MUSIC}', '<multiple>') + '; ' + str(error)
)
_ba.screenmessage(err_str, color=(1, 0, 0)) _ba.screenmessage(err_str, color=(1, 0, 0))
return return
@ -93,9 +109,12 @@ class OSMusicPlayer(MusicPlayer):
class _PickFolderSongThread(threading.Thread): class _PickFolderSongThread(threading.Thread):
def __init__(
def __init__(self, path: str, valid_extensions: list[str], self,
callback: Callable[[str | list[str], str | None], None]): path: str,
valid_extensions: list[str],
callback: Callable[[str | list[str], str | None], None],
):
super().__init__() super().__init__()
self._valid_extensions = valid_extensions self._valid_extensions = valid_extensions
self._callback = callback self._callback = callback
@ -104,6 +123,7 @@ class _PickFolderSongThread(threading.Thread):
def run(self) -> None: def run(self) -> None:
from ba import _language from ba import _language
from ba._general import Call from ba._general import Call
do_print_error = True do_print_error = True
try: try:
_ba.set_thread_name('BA_PickFolderSongThread') _ba.set_thread_name('BA_PickFolderSongThread')
@ -111,24 +131,33 @@ class _PickFolderSongThread(threading.Thread):
valid_extensions = ['.' + x for x in self._valid_extensions] valid_extensions = ['.' + x for x in self._valid_extensions]
for root, _subdirs, filenames in os.walk(self._path): for root, _subdirs, filenames in os.walk(self._path):
for fname in filenames: for fname in filenames:
if any(fname.lower().endswith(ext) if any(
for ext in valid_extensions): fname.lower().endswith(ext) for ext in valid_extensions
all_files.insert(random.randrange(len(all_files) + 1), ):
root + '/' + fname) all_files.insert(
random.randrange(len(all_files) + 1),
root + '/' + fname,
)
if not all_files: if not all_files:
do_print_error = False do_print_error = False
raise RuntimeError( raise RuntimeError(
_language.Lstr(resource='internal.noMusicFilesInFolderText' _language.Lstr(
).evaluate()) resource='internal.noMusicFilesInFolderText'
_ba.pushcall(Call(self._callback, all_files, None), ).evaluate()
from_other_thread=True) )
_ba.pushcall(
Call(self._callback, all_files, None), from_other_thread=True
)
except Exception as exc: except Exception as exc:
from ba import _error from ba import _error
if do_print_error: if do_print_error:
_error.print_exception() _error.print_exception()
try: try:
err_str = str(exc) err_str = str(exc)
except Exception: except Exception:
err_str = '<ENCERR4523>' err_str = '<ENCERR4523>'
_ba.pushcall(Call(self._callback, self._path, err_str), _ba.pushcall(
from_other_thread=True) Call(self._callback, self._path, err_str),
from_other_thread=True,
)

View File

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

View File

@ -27,19 +27,23 @@ class CoopJoinActivity(JoinActivity):
def on_transition_in(self) -> None: def on_transition_in(self) -> None:
from bastd.actor.controlsguide import ControlsGuide from bastd.actor.controlsguide import ControlsGuide
from bastd.actor.text import Text from bastd.actor.text import Text
super().on_transition_in() super().on_transition_in()
assert isinstance(self.session, ba.CoopSession) assert isinstance(self.session, ba.CoopSession)
assert self.session.campaign assert self.session.campaign
Text(self.session.campaign.getlevel( Text(
self.session.campaign_level_name).displayname, self.session.campaign.getlevel(
scale=1.3, self.session.campaign_level_name
h_attach=Text.HAttach.CENTER, ).displayname,
h_align=Text.HAlign.CENTER, scale=1.3,
v_attach=Text.VAttach.TOP, h_attach=Text.HAttach.CENTER,
transition=Text.Transition.FADE_IN, h_align=Text.HAlign.CENTER,
transition_delay=4.0, v_attach=Text.VAttach.TOP,
color=(1, 1, 1, 0.6), transition=Text.Transition.FADE_IN,
position=(0, -95)).autoretain() transition_delay=4.0,
color=(1, 1, 1, 0.6),
position=(0, -95),
).autoretain()
ControlsGuide(delay=1.0).autoretain() ControlsGuide(delay=1.0).autoretain()
ba.pushcall(self._show_remaining_achievements) ba.pushcall(self._show_remaining_achievements)
@ -60,30 +64,34 @@ class CoopJoinActivity(JoinActivity):
# Now list our remaining achievements for this level. # Now list our remaining achievements for this level.
assert self.session.campaign is not None assert self.session.campaign is not None
assert isinstance(self.session, ba.CoopSession) assert isinstance(self.session, ba.CoopSession)
levelname = (self.session.campaign.name + ':' + levelname = (
self.session.campaign_level_name) self.session.campaign.name + ':' + self.session.campaign_level_name
)
ts_h_offs = 60 ts_h_offs = 60
if not (ba.app.demo_mode or ba.app.arcade_mode): if not (ba.app.demo_mode or ba.app.arcade_mode):
achievements = [ 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 if not a.complete
] ]
have_achievements = bool(achievements) have_achievements = bool(achievements)
achievements = [a for a in achievements if not a.complete] achievements = [a for a in achievements if not a.complete]
vrmode = ba.app.vr_mode vrmode = ba.app.vr_mode
if have_achievements: if have_achievements:
Text(ba.Lstr(resource='achievementsRemainingText'), Text(
host_only=True, ba.Lstr(resource='achievementsRemainingText'),
position=(ts_h_offs - 10, vpos), host_only=True,
transition=Text.Transition.FADE_IN, position=(ts_h_offs - 10, vpos),
scale=1.1 * 0.76, transition=Text.Transition.FADE_IN,
h_attach=Text.HAttach.LEFT, scale=1.1 * 0.76,
v_attach=Text.VAttach.TOP, h_attach=Text.HAttach.LEFT,
color=(1, 1, 1.2, 1) if vrmode else (0.8, 0.8, 1, 1), v_attach=Text.VAttach.TOP,
shadow=1.0, color=(1, 1, 1.2, 1) if vrmode else (0.8, 0.8, 1, 1),
flatness=1.0 if vrmode else 0.6, shadow=1.0,
transition_delay=delay).autoretain() flatness=1.0 if vrmode else 0.6,
transition_delay=delay,
).autoretain()
hval = ts_h_offs + 50 hval = ts_h_offs + 50
vpos -= 35 vpos -= 35
for ach in achievements: for ach in achievements:
@ -91,12 +99,14 @@ class CoopJoinActivity(JoinActivity):
ach.create_display(hval, vpos, delay, style='in_game') ach.create_display(hval, vpos, delay, style='in_game')
vpos -= 55 vpos -= 55
if not achievements: if not achievements:
Text(ba.Lstr(resource='noAchievementsRemainingText'), Text(
host_only=True, ba.Lstr(resource='noAchievementsRemainingText'),
position=(ts_h_offs + 15, vpos + 10), host_only=True,
transition=Text.Transition.FADE_IN, position=(ts_h_offs + 15, vpos + 10),
scale=0.7, transition=Text.Transition.FADE_IN,
h_attach=Text.HAttach.LEFT, scale=0.7,
v_attach=Text.VAttach.TOP, h_attach=Text.HAttach.LEFT,
color=(1, 1, 1, 0.5), v_attach=Text.VAttach.TOP,
transition_delay=delay + 0.5).autoretain() 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: def on_begin(self) -> None:
ba.set_analytics_screen('Draw Score Screen') ba.set_analytics_screen('Draw Score Screen')
super().on_begin() super().on_begin()
ZoomText(ba.Lstr(resource='drawText'), ZoomText(
position=(0, 0), ba.Lstr(resource='drawText'),
maxwidth=400, position=(0, 0),
shiftposition=(-220, 0), maxwidth=400,
shiftdelay=2.0, shiftposition=(-220, 0),
flash=False, shiftdelay=2.0,
trail=False, flash=False,
jitter=1.0).autoretain() trail=False,
jitter=1.0,
).autoretain()
ba.timer(0.35, ba.Call(ba.playsound, self._score_display_sound)) ba.timer(0.35, ba.Call(ba.playsound, self._score_display_sound))
self.show_player_scores(results=self.settings_raw.get('results', None)) self.show_player_scores(results=self.settings_raw.get('results', None))

View File

@ -37,91 +37,132 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
session = self.session session = self.session
assert isinstance(session, ba.MultiTeamSession) assert isinstance(session, ba.MultiTeamSession)
if ba.app.lang.get_resource('bestOfUseFirstToInstead'): if ba.app.lang.get_resource('bestOfUseFirstToInstead'):
best_txt = ba.Lstr(resource='firstToSeriesText', best_txt = ba.Lstr(
subs=[('${COUNT}', resource='firstToSeriesText',
str(session.get_series_length() / 2 + 1)) subs=[('${COUNT}', str(session.get_series_length() / 2 + 1))],
]) )
else: else:
best_txt = ba.Lstr(resource='bestOfSeriesText', best_txt = ba.Lstr(
subs=[('${COUNT}', resource='bestOfSeriesText',
str(session.get_series_length()))]) subs=[('${COUNT}', str(session.get_series_length()))],
)
ZoomText(best_txt, ZoomText(
position=(0, 175), best_txt,
shiftposition=(-250, 175), position=(0, 175),
shiftdelay=2.5, shiftposition=(-250, 175),
flash=False, shiftdelay=2.5,
trail=False, flash=False,
h_align='center', trail=False,
scale=0.25, h_align='center',
color=(0.5, 0.5, 0.5, 1.0), scale=0.25,
jitter=3.0).autoretain() color=(0.5, 0.5, 0.5, 1.0),
jitter=3.0,
).autoretain()
for team in self.session.sessionteams: for team in self.session.sessionteams:
ba.timer( ba.timer(
i * 0.15 + 0.15, i * 0.15 + 0.15,
ba.WeakCall(self._show_team_name, vval - i * height, team, ba.WeakCall(
i * 0.2, shift_time - (i * 0.150 + 0.150))) self._show_team_name,
ba.timer(i * 0.150 + 0.5, vval - i * height,
ba.Call(ba.playsound, self._score_display_sound_small)) team,
scored = (team is self._winner) 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 delay = 0.2
if scored: if scored:
delay = 1.2 delay = 1.2
ba.timer( ba.timer(
i * 0.150 + 0.2, i * 0.150 + 0.2,
ba.WeakCall(self._show_team_old_score, vval - i * height, ba.WeakCall(
team, shift_time - (i * 0.15 + 0.2))) self._show_team_old_score,
ba.timer(i * 0.15 + 1.5, vval - i * height,
ba.Call(ba.playsound, self._score_display_sound)) 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( ba.timer(
i * 0.150 + delay, i * 0.150 + delay,
ba.WeakCall(self._show_team_score, vval - i * height, team, ba.WeakCall(
scored, i * 0.2 + 0.1, self._show_team_score,
shift_time - (i * 0.15 + delay))) vval - i * height,
team,
scored,
i * 0.2 + 0.1,
shift_time - (i * 0.15 + delay),
),
)
i += 1 i += 1
self.show_player_scores() self.show_player_scores()
def _show_team_name(self, pos_v: float, team: ba.SessionTeam, def _show_team_name(
kill_delay: float, shiftdelay: float) -> None: self,
pos_v: float,
team: ba.SessionTeam,
kill_delay: float,
shiftdelay: float,
) -> None:
del kill_delay # Unused arg. del kill_delay # Unused arg.
ZoomText(ba.Lstr(value='${A}:', subs=[('${A}', team.name)]), ZoomText(
position=(100, pos_v), ba.Lstr(value='${A}:', subs=[('${A}', team.name)]),
shiftposition=(-150, pos_v), position=(100, pos_v),
shiftdelay=shiftdelay, shiftposition=(-150, pos_v),
flash=False, shiftdelay=shiftdelay,
trail=False, flash=False,
h_align='right', trail=False,
maxwidth=300, h_align='right',
color=team.color, maxwidth=300,
jitter=1.0).autoretain() color=team.color,
jitter=1.0,
).autoretain()
def _show_team_old_score(self, pos_v: float, sessionteam: ba.SessionTeam, def _show_team_old_score(
shiftdelay: float) -> None: self, pos_v: float, sessionteam: ba.SessionTeam, shiftdelay: float
ZoomText(str(sessionteam.customdata['score'] - 1), ) -> None:
position=(150, pos_v), ZoomText(
maxwidth=100, str(sessionteam.customdata['score'] - 1),
color=(0.6, 0.6, 0.7), position=(150, pos_v),
shiftposition=(-100, pos_v), maxwidth=100,
shiftdelay=shiftdelay, color=(0.6, 0.6, 0.7),
flash=False, shiftposition=(-100, pos_v),
trail=False, shiftdelay=shiftdelay,
lifespan=1.0, flash=False,
h_align='left', trail=False,
jitter=1.0).autoretain() lifespan=1.0,
h_align='left',
jitter=1.0,
).autoretain()
def _show_team_score(self, pos_v: float, sessionteam: ba.SessionTeam, def _show_team_score(
scored: bool, kill_delay: float, self,
shiftdelay: float) -> None: pos_v: float,
sessionteam: ba.SessionTeam,
scored: bool,
kill_delay: float,
shiftdelay: float,
) -> None:
del kill_delay # Unused arg. del kill_delay # Unused arg.
ZoomText(str(sessionteam.customdata['score']), ZoomText(
position=(150, pos_v), str(sessionteam.customdata['score']),
maxwidth=100, position=(150, pos_v),
color=(1.0, 0.9, 0.5) if scored else (0.6, 0.6, 0.7), maxwidth=100,
shiftposition=(-100, pos_v), color=(1.0, 0.9, 0.5) if scored else (0.6, 0.6, 0.7),
shiftdelay=shiftdelay, shiftposition=(-100, pos_v),
flash=scored, shiftdelay=shiftdelay,
trail=scored, flash=scored,
h_align='left', trail=scored,
jitter=1.0, h_align='left',
trailcolor=(1, 0.8, 0.0, 0)).autoretain() 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 # pylint: disable=too-many-statements
from bastd.actor.text import Text from bastd.actor.text import Text
from bastd.actor.image import Image from bastd.actor.image import Image
ba.set_analytics_screen('FreeForAll Score Screen') ba.set_analytics_screen('FreeForAll Score Screen')
super().on_begin() super().on_begin()
@ -45,14 +46,17 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
key=lambda p: ( key=lambda p: (
p.team.sessionteam.customdata['previous_score'], p.team.sessionteam.customdata['previous_score'],
p.getname(full=True), p.getname(full=True),
)) ),
)
player_order = list(self.players) player_order = list(self.players)
player_order.sort(reverse=True, player_order.sort(
key=lambda p: ( reverse=True,
p.team.sessionteam.customdata['score'], key=lambda p: (
p.team.sessionteam.customdata['score'], p.team.sessionteam.customdata['score'],
p.getname(full=True), p.team.sessionteam.customdata['score'],
)) p.getname(full=True),
),
)
v_offs = -74.0 + spacing * len(player_order_prev) * 0.5 v_offs = -74.0 + spacing * len(player_order_prev) * 0.5
delay1 = 1.3 + 0.1 delay1 = 1.3 + 0.1
@ -66,30 +70,36 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
ba.timer(0.3, ba.Call(ba.playsound, self._score_display_sound)) ba.timer(0.3, ba.Call(ba.playsound, self._score_display_sound))
results = self.settings_raw['results'] results = self.settings_raw['results']
assert isinstance(results, ba.GameResults) assert isinstance(results, ba.GameResults)
self.show_player_scores(delay=0.001, self.show_player_scores(
results=results, delay=0.001, results=results, scale=1.2, x_offset=-110.0
scale=1.2, )
x_offset=-110.0)
sound_times: set[float] = set() sound_times: set[float] = set()
def _scoretxt(text: str, def _scoretxt(
x_offs: float, text: str,
y_offs: float, x_offs: float,
highlight: bool, y_offs: float,
delay: float, highlight: bool,
extrascale: float, delay: float,
flash: bool = False) -> Text: extrascale: float,
return Text(text, flash: bool = False,
position=(ts_h_offs + x_offs * scale, ) -> Text:
y_base + (y_offs + v_offs + 2.0) * scale), return Text(
scale=scale * extrascale, text,
color=((1.0, 0.7, 0.3, 1.0) if highlight else position=(
(0.7, 0.7, 0.7, 0.7)), ts_h_offs + x_offs * scale,
h_align=Text.HAlign.RIGHT, y_base + (y_offs + v_offs + 2.0) * scale,
transition=Text.Transition.IN_LEFT, ),
transition_delay=tdelay + delay, scale=scale * extrascale,
flash=flash).autoretain() 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 v_offs -= spacing
slide_amt = 0.0 slide_amt = 0.0
@ -98,16 +108,21 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
session = self.session session = self.session
assert isinstance(session, ba.FreeForAllSession) assert isinstance(session, ba.FreeForAllSession)
title = Text(ba.Lstr(resource='firstToSeriesText', title = Text(
subs=[('${COUNT}', ba.Lstr(
str(session.get_ffa_series_length()))]), resource='firstToSeriesText',
scale=1.05 * scale, subs=[('${COUNT}', str(session.get_ffa_series_length()))],
position=(ts_h_offs - 0.0 * scale, ),
y_base + (v_offs + 50.0) * scale), scale=1.05 * scale,
h_align=Text.HAlign.CENTER, position=(
color=(0.5, 0.5, 0.5, 0.5), ts_h_offs - 0.0 * scale,
transition=Text.Transition.IN_LEFT, y_base + (v_offs + 50.0) * scale,
transition_delay=tdelay).autoretain() ),
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 -= 25
v_offs_start = v_offs v_offs_start = v_offs
@ -115,152 +130,239 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
ba.timer( ba.timer(
tdelay + delay3, tdelay + delay3,
ba.WeakCall( ba.WeakCall(
self._safe_animate, title.position_combine, 'input0', { self._safe_animate,
title.position_combine,
'input0',
{
0.0: ts_h_offs - 0.0 * scale, 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): for i, player in enumerate(player_order_prev):
v_offs_2 = v_offs_start - spacing * (player_order.index(player)) v_offs_2 = v_offs_start - spacing * (player_order.index(player))
ba.timer(tdelay + 0.3, ba.timer(
ba.Call(ba.playsound, self._score_display_sound_small)) tdelay + 0.3,
ba.Call(ba.playsound, self._score_display_sound_small),
)
if order_change: if order_change:
ba.timer(tdelay + delay2 + 0.1, ba.timer(
ba.Call(ba.playsound, self._cymbal_sound)) tdelay + delay2 + 0.1,
img = Image(player.get_icon(), ba.Call(ba.playsound, self._cymbal_sound),
position=(ts_h_offs - 72.0 * scale, )
y_base + (v_offs + 15.0) * scale), img = Image(
scale=(30.0 * scale, 30.0 * scale), player.get_icon(),
transition=Image.Transition.IN_LEFT, position=(
transition_delay=tdelay).autoretain() 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( ba.timer(
tdelay + delay2, tdelay + delay2,
ba.WeakCall( ba.WeakCall(
self._safe_animate, img.position_combine, 'input1', { self._safe_animate,
img.position_combine,
'input1',
{
0: y_base + (v_offs + 15.0) * scale, 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( ba.timer(
tdelay + delay3, tdelay + delay3,
ba.WeakCall( ba.WeakCall(
self._safe_animate, img.position_combine, 'input0', { self._safe_animate,
img.position_combine,
'input0',
{
0: ts_h_offs - 72.0 * scale, 0: ts_h_offs - 72.0 * scale,
transtime2: ts_h_offs - (72.0 + slide_amt) * 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, txt = Text(
position=(ts_h_offs - 50.0 * scale, ba.Lstr(value=player.getname(full=True)),
y_base + (v_offs + 15.0) * scale), maxwidth=130.0,
h_align=Text.HAlign.LEFT, scale=0.75 * scale,
v_align=Text.VAlign.CENTER, position=(
color=ba.safecolor(player.team.color + (1, )), ts_h_offs - 50.0 * scale,
transition=Text.Transition.IN_LEFT, y_base + (v_offs + 15.0) * scale,
transition_delay=tdelay).autoretain() ),
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( ba.timer(
tdelay + delay2, tdelay + delay2,
ba.WeakCall( ba.WeakCall(
self._safe_animate, txt.position_combine, 'input1', { self._safe_animate,
txt.position_combine,
'input1',
{
0: y_base + (v_offs + 15.0) * scale, 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( ba.timer(
tdelay + delay3, tdelay + delay3,
ba.WeakCall( ba.WeakCall(
self._safe_animate, txt.position_combine, 'input0', { self._safe_animate,
txt.position_combine,
'input0',
{
0: ts_h_offs - 50.0 * scale, 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), txt_num = Text(
scale=0.55 * scale, '#' + str(i + 1),
position=(ts_h_offs - 95.0 * scale, scale=0.55 * scale,
y_base + (v_offs + 8.0) * scale), position=(
h_align=Text.HAlign.RIGHT, ts_h_offs - 95.0 * scale,
color=(0.6, 0.6, 0.6, 0.6), y_base + (v_offs + 8.0) * scale,
transition=Text.Transition.IN_LEFT, ),
transition_delay=tdelay).autoretain() 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( ba.timer(
tdelay + delay3, tdelay + delay3,
ba.WeakCall( 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, 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( s_txt = _scoretxt(
str(player.team.sessionteam.customdata['previous_score']), 80, str(player.team.sessionteam.customdata['previous_score']),
0, False, 0, 1.0) 80,
0,
False,
0,
1.0,
)
ba.timer( ba.timer(
tdelay + delay2, tdelay + delay2,
ba.WeakCall( 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, 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( ba.timer(
tdelay + delay3, tdelay + delay3,
ba.WeakCall( 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, 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 = ( score_change = (
player.team.sessionteam.customdata['score'] - player.team.sessionteam.customdata['score']
player.team.sessionteam.customdata['previous_score']) - player.team.sessionteam.customdata['previous_score']
)
if score_change > 0: if score_change > 0:
xval = 113 xval = 113
yval = 3.0 yval = 3.0
s_txt_2 = _scoretxt('+' + str(score_change), s_txt_2 = _scoretxt(
xval, '+' + str(score_change),
yval, xval,
True, yval,
0, True,
0.7, 0,
flash=True) 0.7,
flash=True,
)
ba.timer( ba.timer(
tdelay + delay2, tdelay + delay2,
ba.WeakCall( 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, 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( ba.timer(
tdelay + delay3, tdelay + delay3,
ba.WeakCall( 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, 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, def _safesetattr(
value: Any) -> None: node: ba.Node | None, attr: str, value: Any
) -> None:
if node: if node:
setattr(node, attr, value) setattr(node, attr, value)
ba.timer( ba.timer(
tdelay + delay1, 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): for j in range(score_change):
ba.timer((tdelay + delay1 + 0.15 * j), ba.timer(
ba.Call( (tdelay + delay1 + 0.15 * j),
_safesetattr, s_txt.node, 'text', ba.Call(
str(player.team.sessionteam. _safesetattr,
customdata['previous_score'] + j + 1))) s_txt.node,
'text',
str(
player.team.sessionteam.customdata[
'previous_score'
]
+ j
+ 1
),
),
)
tfin = tdelay + delay1 + 0.15 * j tfin = tdelay + delay1 + 0.15 * j
if tfin not in sound_times: if tfin not in sound_times:
sound_times.add(tfin) sound_times.add(tfin)
ba.timer( ba.timer(
tfin, tfin,
ba.Call(ba.playsound, ba.Call(
self._score_display_sound_small)) ba.playsound, self._score_display_sound_small
),
)
v_offs -= spacing v_offs -= spacing
def _safe_animate(self, node: ba.Node | None, attr: str, def _safe_animate(
keys: dict[float, float]) -> None: self, node: ba.Node | None, attr: str, keys: dict[float, float]
) -> None:
"""Run an animation on a node if the node still exists.""" """Run an animation on a node if the node still exists."""
if node: if node:
ba.animate(node, attr, keys) ba.animate(node, attr, keys)

View File

@ -24,6 +24,7 @@ class MultiTeamJoinActivity(JoinActivity):
def on_transition_in(self) -> None: def on_transition_in(self) -> None:
from bastd.actor.controlsguide import ControlsGuide from bastd.actor.controlsguide import ControlsGuide
from ba import DualTeamSession from ba import DualTeamSession
super().on_transition_in() super().on_transition_in()
ControlsGuide(delay=1.0).autoretain() ControlsGuide(delay=1.0).autoretain()
@ -31,50 +32,62 @@ class MultiTeamJoinActivity(JoinActivity):
assert isinstance(session, ba.MultiTeamSession) assert isinstance(session, ba.MultiTeamSession)
# Show info about the next up game. # Show info about the next up game.
self._next_up_text = Text(ba.Lstr( self._next_up_text = Text(
value='${1} ${2}', ba.Lstr(
subs=[('${1}', ba.Lstr(resource='upFirstText')), value='${1} ${2}',
('${2}', session.get_next_game_description())]), subs=[
h_attach=Text.HAttach.CENTER, ('${1}', ba.Lstr(resource='upFirstText')),
scale=0.7, ('${2}', session.get_next_game_description()),
v_attach=Text.VAttach.TOP, ],
h_align=Text.HAlign.CENTER, ),
position=(0, -70), h_attach=Text.HAttach.CENTER,
flash=False, scale=0.7,
color=(0.5, 0.5, 0.5, 1.0), v_attach=Text.VAttach.TOP,
transition=Text.Transition.FADE_IN, h_align=Text.HAlign.CENTER,
transition_delay=5.0) 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. # In teams mode, show our two team names.
# FIXME: Lobby should handle this. # FIXME: Lobby should handle this.
if isinstance(ba.getsession(), DualTeamSession): if isinstance(ba.getsession(), DualTeamSession):
team_names = [team.name for team in ba.getsession().sessionteams] team_names = [team.name for team in ba.getsession().sessionteams]
team_colors = [ team_colors = [
tuple(team.color) + (0.5, ) tuple(team.color) + (0.5,)
for team in ba.getsession().sessionteams for team in ba.getsession().sessionteams
] ]
if len(team_names) == 2: if len(team_names) == 2:
for i in range(2): for i in range(2):
Text(team_names[i], Text(
scale=0.7, team_names[i],
h_attach=Text.HAttach.CENTER, scale=0.7,
v_attach=Text.VAttach.TOP, h_attach=Text.HAttach.CENTER,
h_align=Text.HAlign.CENTER, v_attach=Text.VAttach.TOP,
position=(-200 + 350 * i, -100), h_align=Text.HAlign.CENTER,
color=team_colors[i], position=(-200 + 350 * i, -100),
transition=Text.Transition.FADE_IN).autoretain() color=team_colors[i],
transition=Text.Transition.FADE_IN,
).autoretain()
Text(ba.Lstr(resource='mustInviteFriendsText', Text(
subs=[('${GATHER}', ba.Lstr(
ba.Lstr(resource='gatherWindow.titleText'))]), resource='mustInviteFriendsText',
h_attach=Text.HAttach.CENTER, subs=[
scale=0.8, ('${GATHER}', ba.Lstr(resource='gatherWindow.titleText'))
host_only=True, ],
v_attach=Text.VAttach.CENTER, ),
h_align=Text.HAlign.CENTER, h_attach=Text.HAttach.CENTER,
position=(0, 0), scale=0.8,
flash=False, host_only=True,
color=(0, 1, 0, 1.0), v_attach=Text.VAttach.CENTER,
transition=Text.Transition.FADE_IN, h_align=Text.HAlign.CENTER,
transition_delay=2.0, position=(0, 0),
transition_out_delay=7.0).autoretain() 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() super().on_begin()
session = self.session session = self.session
if self._show_up_next and isinstance(session, ba.MultiTeamSession): if self._show_up_next and isinstance(session, ba.MultiTeamSession):
txt = ba.Lstr(value='${A} ${B}', txt = ba.Lstr(
subs=[ value='${A} ${B}',
('${A}', subs=[
ba.Lstr(resource='upNextText', (
subs=[ '${A}',
('${COUNT}', ba.Lstr(
str(session.get_game_number() + 1)) resource='upNextText',
])), subs=[
('${B}', session.get_next_game_description()) ('${COUNT}', str(session.get_game_number() + 1))
]) ],
Text(txt, ),
maxwidth=900, ),
h_attach=Text.HAttach.CENTER, ('${B}', session.get_next_game_description()),
v_attach=Text.VAttach.BOTTOM, ],
h_align=Text.HAlign.CENTER, )
v_align=Text.VAlign.CENTER, Text(
position=(0, 53), txt,
flash=False, maxwidth=900,
color=(0.3, 0.3, 0.35, 1.0), h_attach=Text.HAttach.CENTER,
transition=Text.Transition.FADE_IN, v_attach=Text.VAttach.BOTTOM,
transition_delay=2.0).autoretain() 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, def show_player_scores(
delay: float = 2.5, self,
results: ba.GameResults | None = None, delay: float = 2.5,
scale: float = 1.0, results: ba.GameResults | None = None,
x_offset: float = 0.0, scale: float = 1.0,
y_offset: float = 0.0) -> None: x_offset: float = 0.0,
y_offset: float = 0.0,
) -> None:
"""Show scores for individual players.""" """Show scores for individual players."""
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
@ -96,7 +105,8 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
def _get_player_score_set_entry( def _get_player_score_set_entry(
player: ba.SessionPlayer) -> ba.PlayerRecord | None: player: ba.SessionPlayer,
) -> ba.PlayerRecord | None:
for p_rec in valid_players: for p_rec in valid_players:
if p_rec[1].player is player: if p_rec[1].player is player:
return p_rec[1] return p_rec[1]
@ -108,7 +118,8 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
for team in winnergroup.teams: for team in winnergroup.teams:
if len(team.players) == 1: if len(team.players) == 1:
player_entry = _get_player_score_set_entry( player_entry = _get_player_score_set_entry(
team.players[0]) team.players[0]
)
if player_entry is not None: if player_entry is not None:
player_records.append(player_entry) player_records.append(player_entry)
else: else:
@ -124,33 +135,43 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
voffs = -140.0 + spacing * len(player_records) * 0.5 voffs = -140.0 + spacing * len(player_records) * 0.5
def _txt(xoffs: float, def _txt(
yoffs: float, xoffs: float,
text: ba.Lstr, yoffs: float,
h_align: Text.HAlign = Text.HAlign.RIGHT, text: ba.Lstr,
extrascale: float = 1.0, h_align: Text.HAlign = Text.HAlign.RIGHT,
maxwidth: float | None = 120.0) -> None: extrascale: float = 1.0,
Text(text, maxwidth: float | None = 120.0,
color=(0.5, 0.5, 0.6, 0.5), ) -> None:
position=(ts_h_offs + xoffs * scale, Text(
ts_v_offset + (voffs + yoffs + 4.0) * scale), text,
h_align=h_align, color=(0.5, 0.5, 0.6, 0.5),
v_align=Text.VAlign.CENTER, position=(
scale=0.8 * scale * extrascale, ts_h_offs + xoffs * scale,
maxwidth=maxwidth, ts_v_offset + (voffs + yoffs + 4.0) * scale,
transition=Text.Transition.IN_LEFT, ),
transition_delay=tdelay).autoretain() 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 session = self.session
assert isinstance(session, ba.MultiTeamSession) assert isinstance(session, ba.MultiTeamSession)
tval = ba.Lstr(resource='gameLeadersText', tval = ba.Lstr(
subs=[('${COUNT}', str(session.get_game_number()))]) resource='gameLeadersText',
_txt(180, subs=[('${COUNT}', str(session.get_game_number()))],
43, )
tval, _txt(
h_align=Text.HAlign.CENTER, 180,
extrascale=1.4, 43,
maxwidth=None) tval,
h_align=Text.HAlign.CENTER,
extrascale=1.4,
maxwidth=None,
)
_txt(-15, 4, ba.Lstr(resource='playerText'), h_align=Text.HAlign.LEFT) _txt(-15, 4, ba.Lstr(resource='playerText'), h_align=Text.HAlign.LEFT)
_txt(180, 4, ba.Lstr(resource='killsText')) _txt(180, 4, ba.Lstr(resource='killsText'))
_txt(280, 4, ba.Lstr(resource='deathsText'), maxwidth=100) _txt(280, 4, ba.Lstr(resource='deathsText'), maxwidth=100)
@ -162,52 +183,80 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
topkillcount = 0 topkillcount = 0
topkilledcount = 99999 topkilledcount = 99999
top_score = 0 if not player_records else _get_prec_score( top_score = (
player_records[0]) 0 if not player_records else _get_prec_score(player_records[0])
)
for prec in player_records: for prec in player_records:
topkillcount = max(topkillcount, prec.accum_kill_count) topkillcount = max(topkillcount, prec.accum_kill_count)
topkilledcount = min(topkilledcount, prec.accum_killed_count) topkilledcount = min(topkilledcount, prec.accum_killed_count)
def _scoretxt(text: str | ba.Lstr, def _scoretxt(
x_offs: float, text: str | ba.Lstr,
highlight: bool, x_offs: float,
delay2: float, highlight: bool,
maxwidth: float = 70.0) -> None: delay2: float,
Text(text, maxwidth: float = 70.0,
position=(ts_h_offs + x_offs * scale, ) -> None:
ts_v_offset + (voffs + 15) * scale), Text(
scale=scale, text,
color=(1.0, 0.9, 0.5, 1.0) if highlight else position=(
(0.5, 0.5, 0.6, 0.5), ts_h_offs + x_offs * scale,
h_align=Text.HAlign.RIGHT, ts_v_offset + (voffs + 15) * scale,
v_align=Text.VAlign.CENTER, ),
maxwidth=maxwidth, scale=scale,
transition=Text.Transition.IN_LEFT, color=(1.0, 0.9, 0.5, 1.0)
transition_delay=tdelay + delay2).autoretain() 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: for playerrec in player_records:
tdelay += 0.05 tdelay += 0.05
voffs -= spacing voffs -= spacing
Image(playerrec.get_icon(), Image(
position=(ts_h_offs - 12 * scale, playerrec.get_icon(),
ts_v_offset + (voffs + 15.0) * scale), position=(
scale=(30.0 * scale, 30.0 * scale), ts_h_offs - 12 * scale,
transition=Image.Transition.IN_LEFT, ts_v_offset + (voffs + 15.0) * scale,
transition_delay=tdelay).autoretain() ),
Text(ba.Lstr(value=playerrec.getname(full=True)), scale=(30.0 * scale, 30.0 * scale),
maxwidth=160, transition=Image.Transition.IN_LEFT,
scale=0.75 * scale, transition_delay=tdelay,
position=(ts_h_offs + 10.0 * scale, ).autoretain()
ts_v_offset + (voffs + 15) * scale), Text(
h_align=Text.HAlign.LEFT, ba.Lstr(value=playerrec.getname(full=True)),
v_align=Text.VAlign.CENTER, maxwidth=160,
color=ba.safecolor(playerrec.team.color + (1, )), scale=0.75 * scale,
transition=Text.Transition.IN_LEFT, position=(
transition_delay=tdelay).autoretain() ts_h_offs + 10.0 * scale,
_scoretxt(str(playerrec.accum_kill_count), 180, ts_v_offset + (voffs + 15) * scale,
playerrec.accum_kill_count == topkillcount, 0.1) ),
_scoretxt(str(playerrec.accum_killed_count), 280, h_align=Text.HAlign.LEFT,
playerrec.accum_killed_count == topkilledcount, 0.1) v_align=Text.VAlign.CENTER,
_scoretxt(_get_prec_score_str(playerrec), 390, color=ba.safecolor(playerrec.team.color + (1,)),
_get_prec_score(playerrec) == top_score, 0.2) 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 # pylint: disable=too-many-statements
from bastd.actor.text import Text from bastd.actor.text import Text
from bastd.actor.image import Image 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: if ba.app.ui.uiscale is ba.UIScale.LARGE:
sval = ba.Lstr(resource='pressAnyKeyButtonPlayAgainText') sval = ba.Lstr(resource='pressAnyKeyButtonPlayAgainText')
else: else:
@ -46,8 +50,9 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
# Pause a moment before playing victory music. # Pause a moment before playing victory music.
ba.timer(0.6, ba.WeakCall(self._play_victory_music)) ba.timer(0.6, ba.WeakCall(self._play_victory_music))
ba.timer(4.4, ba.timer(
ba.WeakCall(self._show_winner, self.settings_raw['winner'])) 4.4, ba.WeakCall(self._show_winner, self.settings_raw['winner'])
)
ba.timer(4.6, ba.Call(ba.playsound, self._score_display_sound)) ba.timer(4.6, ba.Call(ba.playsound, self._score_display_sound))
# Score / Name / Player-record. # Score / Name / Player-record.
@ -58,8 +63,12 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
for _pkey, prec in self.stats.get_records().items(): for _pkey, prec in self.stats.get_records().items():
if prec.player.in_game: if prec.player.in_game:
player_entries.append( 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]) player_entries.sort(reverse=True, key=lambda x: x[0])
else: else:
for _pkey, prec in self.stats.get_records().items(): for _pkey, prec in self.stats.get_records().items():
@ -72,18 +81,29 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
t_incr = 0.12 t_incr = 0.12
always_use_first_to = ba.app.lang.get_resource( always_use_first_to = ba.app.lang.get_resource(
'bestOfUseFirstToInstead') 'bestOfUseFirstToInstead'
)
session = self.session session = self.session
if self._is_ffa: if self._is_ffa:
assert isinstance(session, ba.FreeForAllSession) assert isinstance(session, ba.FreeForAllSession)
txt = ba.Lstr( txt = ba.Lstr(
value='${A}:', value='${A}:',
subs=[('${A}', subs=[
ba.Lstr(resource='firstToFinalText', (
subs=[('${COUNT}', '${A}',
str(session.get_ffa_series_length()))])) ba.Lstr(
]) resource='firstToFinalText',
subs=[
(
'${COUNT}',
str(session.get_ffa_series_length()),
)
],
),
)
],
)
else: else:
assert isinstance(session, ba.MultiTeamSession) assert isinstance(session, ba.MultiTeamSession)
@ -96,31 +116,52 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
txt = ba.Lstr( txt = ba.Lstr(
value='${A}:', value='${A}:',
subs=[ subs=[
('${A}', (
ba.Lstr(resource='firstToFinalText', '${A}',
subs=[ ba.Lstr(
('${COUNT}', resource='firstToFinalText',
str(session.get_series_length() / 2 + 1)) subs=[
])) (
]) '${COUNT}',
str(
session.get_series_length() / 2 + 1
),
)
],
),
)
],
)
else: else:
txt = ba.Lstr( txt = ba.Lstr(
value='${A}:', value='${A}:',
subs=[('${A}', subs=[
ba.Lstr(resource='bestOfFinalText', (
subs=[('${COUNT}', '${A}',
str(session.get_series_length()))])) ba.Lstr(
]) resource='bestOfFinalText',
subs=[
(
'${COUNT}',
str(session.get_series_length()),
)
],
),
)
],
)
Text(txt, Text(
v_align=Text.VAlign.CENTER, txt,
maxwidth=300, v_align=Text.VAlign.CENTER,
color=(0.5, 0.5, 0.5, 1.0), maxwidth=300,
position=(0, 220), color=(0.5, 0.5, 0.5, 1.0),
scale=1.2, position=(0, 220),
transition=Text.Transition.IN_TOP_SLOW, scale=1.2,
h_align=Text.HAlign.CENTER, transition=Text.Transition.IN_TOP_SLOW,
transition_delay=t_incr * 4).autoretain() h_align=Text.HAlign.CENTER,
transition_delay=t_incr * 4,
).autoretain()
win_score = (session.get_series_length() - 1) // 2 + 1 win_score = (session.get_series_length() - 1) // 2 + 1
lose_score = 0 lose_score = 0
@ -129,17 +170,23 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
lose_score = team.sessionteam.customdata['score'] lose_score = team.sessionteam.customdata['score']
if not self._is_ffa: if not self._is_ffa:
Text(ba.Lstr(resource='gamesToText', Text(
subs=[('${WINCOUNT}', str(win_score)), ba.Lstr(
('${LOSECOUNT}', str(lose_score))]), resource='gamesToText',
color=(0.5, 0.5, 0.5, 1.0), subs=[
maxwidth=160, ('${WINCOUNT}', str(win_score)),
v_align=Text.VAlign.CENTER, ('${LOSECOUNT}', str(lose_score)),
position=(0, -215), ],
scale=1.8, ),
transition=Text.Transition.IN_LEFT, color=(0.5, 0.5, 0.5, 1.0),
h_align=Text.HAlign.CENTER, maxwidth=160,
transition_delay=4.8 + t_incr * 4).autoretain() 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: if self._is_ffa:
v_extra = 120 v_extra = 120
@ -158,31 +205,37 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
mvp_name = entry[1] mvp_name = entry[1]
break break
if mvp is not None: if mvp is not None:
Text(ba.Lstr(resource='mostValuablePlayerText'), Text(
color=(0.5, 0.5, 0.5, 1.0), ba.Lstr(resource='mostValuablePlayerText'),
v_align=Text.VAlign.CENTER, color=(0.5, 0.5, 0.5, 1.0),
maxwidth=300, v_align=Text.VAlign.CENTER,
position=(180, ts_height / 2 + 15), maxwidth=300,
transition=Text.Transition.IN_LEFT, position=(180, ts_height / 2 + 15),
h_align=Text.HAlign.LEFT, transition=Text.Transition.IN_LEFT,
transition_delay=tval).autoretain() h_align=Text.HAlign.LEFT,
transition_delay=tval,
).autoretain()
tval += 4 * t_incr tval += 4 * t_incr
Image(mvp.get_icon(), Image(
position=(230, ts_height / 2 - 55 + 14 - 5), mvp.get_icon(),
scale=(70, 70), position=(230, ts_height / 2 - 55 + 14 - 5),
transition=Image.Transition.IN_LEFT, scale=(70, 70),
transition_delay=tval).autoretain() transition=Image.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
assert mvp_name is not None assert mvp_name is not None
Text(ba.Lstr(value=mvp_name), Text(
position=(280, ts_height / 2 - 55 + 15 - 5), ba.Lstr(value=mvp_name),
h_align=Text.HAlign.LEFT, position=(280, ts_height / 2 - 55 + 15 - 5),
v_align=Text.VAlign.CENTER, h_align=Text.HAlign.LEFT,
maxwidth=170, v_align=Text.VAlign.CENTER,
scale=1.3, maxwidth=170,
color=ba.safecolor(mvp.team.color + (1, )), scale=1.3,
transition=Text.Transition.IN_LEFT, color=ba.safecolor(mvp.team.color + (1,)),
transition_delay=tval).autoretain() transition=Text.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
tval += 4 * t_incr tval += 4 * t_incr
# Most violent. # Most violent.
@ -193,41 +246,56 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
mvp_name = entry[1] mvp_name = entry[1]
most_kills = entry[2].kill_count most_kills = entry[2].kill_count
if mvp is not None: if mvp is not None:
Text(ba.Lstr(resource='mostViolentPlayerText'), Text(
color=(0.5, 0.5, 0.5, 1.0), ba.Lstr(resource='mostViolentPlayerText'),
v_align=Text.VAlign.CENTER, color=(0.5, 0.5, 0.5, 1.0),
maxwidth=300, v_align=Text.VAlign.CENTER,
position=(180, ts_height / 2 - 150 + v_extra + 15), maxwidth=300,
transition=Text.Transition.IN_LEFT, position=(180, ts_height / 2 - 150 + v_extra + 15),
h_align=Text.HAlign.LEFT, transition=Text.Transition.IN_LEFT,
transition_delay=tval).autoretain() h_align=Text.HAlign.LEFT,
Text(ba.Lstr(value='(${A})', transition_delay=tval,
subs=[('${A}', ).autoretain()
ba.Lstr(resource='killsTallyText', Text(
subs=[('${COUNT}', str(most_kills))])) ba.Lstr(
]), value='(${A})',
position=(260, ts_height / 2 - 150 - 15 + v_extra), subs=[
color=(0.3, 0.3, 0.3, 1.0), (
scale=0.6, '${A}',
h_align=Text.HAlign.LEFT, ba.Lstr(
transition=Text.Transition.IN_LEFT, resource='killsTallyText',
transition_delay=tval).autoretain() 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 tval += 4 * t_incr
Image(mvp.get_icon(), Image(
position=(233, ts_height / 2 - 150 - 30 - 46 + 25 + v_extra), mvp.get_icon(),
scale=(50, 50), position=(233, ts_height / 2 - 150 - 30 - 46 + 25 + v_extra),
transition=Image.Transition.IN_LEFT, scale=(50, 50),
transition_delay=tval).autoretain() transition=Image.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
assert mvp_name is not None assert mvp_name is not None
Text(ba.Lstr(value=mvp_name), Text(
position=(270, ts_height / 2 - 150 - 30 - 36 + v_extra + 15), ba.Lstr(value=mvp_name),
h_align=Text.HAlign.LEFT, position=(270, ts_height / 2 - 150 - 30 - 36 + v_extra + 15),
v_align=Text.VAlign.CENTER, h_align=Text.HAlign.LEFT,
maxwidth=180, v_align=Text.VAlign.CENTER,
color=ba.safecolor(mvp.team.color + (1, )), maxwidth=180,
transition=Text.Transition.IN_LEFT, color=ba.safecolor(mvp.team.color + (1,)),
transition_delay=tval).autoretain() transition=Text.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
tval += 4 * t_incr tval += 4 * t_incr
# Most killed. # Most killed.
@ -239,49 +307,66 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
mkp_name = entry[1] mkp_name = entry[1]
most_killed = entry[2].killed_count most_killed = entry[2].killed_count
if mkp is not None: if mkp is not None:
Text(ba.Lstr(resource='mostViolatedPlayerText'), Text(
color=(0.5, 0.5, 0.5, 1.0), ba.Lstr(resource='mostViolatedPlayerText'),
v_align=Text.VAlign.CENTER, color=(0.5, 0.5, 0.5, 1.0),
maxwidth=300, v_align=Text.VAlign.CENTER,
position=(180, ts_height / 2 - 300 + v_extra + 15), maxwidth=300,
transition=Text.Transition.IN_LEFT, position=(180, ts_height / 2 - 300 + v_extra + 15),
h_align=Text.HAlign.LEFT, transition=Text.Transition.IN_LEFT,
transition_delay=tval).autoretain() h_align=Text.HAlign.LEFT,
Text(ba.Lstr(value='(${A})', transition_delay=tval,
subs=[('${A}', ).autoretain()
ba.Lstr(resource='deathsTallyText', Text(
subs=[('${COUNT}', str(most_killed))])) ba.Lstr(
]), value='(${A})',
position=(260, ts_height / 2 - 300 - 15 + v_extra), subs=[
h_align=Text.HAlign.LEFT, (
scale=0.6, '${A}',
color=(0.3, 0.3, 0.3, 1.0), ba.Lstr(
transition=Text.Transition.IN_LEFT, resource='deathsTallyText',
transition_delay=tval).autoretain() 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 tval += 4 * t_incr
Image(mkp.get_icon(), Image(
position=(233, ts_height / 2 - 300 - 30 - 46 + 25 + v_extra), mkp.get_icon(),
scale=(50, 50), position=(233, ts_height / 2 - 300 - 30 - 46 + 25 + v_extra),
transition=Image.Transition.IN_LEFT, scale=(50, 50),
transition_delay=tval).autoretain() transition=Image.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
assert mkp_name is not None assert mkp_name is not None
Text(ba.Lstr(value=mkp_name), Text(
position=(270, ts_height / 2 - 300 - 30 - 36 + v_extra + 15), ba.Lstr(value=mkp_name),
h_align=Text.HAlign.LEFT, position=(270, ts_height / 2 - 300 - 30 - 36 + v_extra + 15),
v_align=Text.VAlign.CENTER, h_align=Text.HAlign.LEFT,
color=ba.safecolor(mkp.team.color + (1, )), v_align=Text.VAlign.CENTER,
maxwidth=180, color=ba.safecolor(mkp.team.color + (1,)),
transition=Text.Transition.IN_LEFT, maxwidth=180,
transition_delay=tval).autoretain() transition=Text.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
tval += 4 * t_incr tval += 4 * t_incr
# Now show individual scores. # Now show individual scores.
tdelay = tval tdelay = tval
Text(ba.Lstr(resource='finalScoresText'), Text(
color=(0.5, 0.5, 0.5, 1.0), ba.Lstr(resource='finalScoresText'),
position=(ts_h_offs, ts_height / 2), color=(0.5, 0.5, 0.5, 1.0),
transition=Text.Transition.IN_RIGHT, position=(ts_h_offs, ts_height / 2),
transition_delay=tdelay).autoretain() transition=Text.Transition.IN_RIGHT,
transition_delay=tdelay,
).autoretain()
tdelay += 4 * t_incr tdelay += 4 * t_incr
v_offs = 0.0 v_offs = 0.0
@ -289,33 +374,41 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
for _score, name, prec in player_entries: for _score, name, prec in player_entries:
tdelay -= 4 * t_incr tdelay -= 4 * t_incr
v_offs -= 40 v_offs -= 40
Text(str(prec.team.customdata['score']) Text(
if self._is_ffa else str(prec.score), str(prec.team.customdata['score'])
color=(0.5, 0.5, 0.5, 1.0), if self._is_ffa
position=(ts_h_offs + 230, ts_height / 2 + v_offs), else str(prec.score),
h_align=Text.HAlign.RIGHT, color=(0.5, 0.5, 0.5, 1.0),
transition=Text.Transition.IN_RIGHT, position=(ts_h_offs + 230, ts_height / 2 + v_offs),
transition_delay=tdelay).autoretain() h_align=Text.HAlign.RIGHT,
transition=Text.Transition.IN_RIGHT,
transition_delay=tdelay,
).autoretain()
tdelay -= 4 * t_incr tdelay -= 4 * t_incr
Image(prec.get_icon(), Image(
position=(ts_h_offs - 72, ts_height / 2 + v_offs + 15), prec.get_icon(),
scale=(30, 30), position=(ts_h_offs - 72, ts_height / 2 + v_offs + 15),
transition=Image.Transition.IN_LEFT, scale=(30, 30),
transition_delay=tdelay).autoretain() transition=Image.Transition.IN_LEFT,
Text(ba.Lstr(value=name), transition_delay=tdelay,
position=(ts_h_offs - 50, ts_height / 2 + v_offs + 15), ).autoretain()
h_align=Text.HAlign.LEFT, Text(
v_align=Text.VAlign.CENTER, ba.Lstr(value=name),
maxwidth=180, position=(ts_h_offs - 50, ts_height / 2 + v_offs + 15),
color=ba.safecolor(prec.team.color + (1, )), h_align=Text.HAlign.LEFT,
transition=Text.Transition.IN_RIGHT, v_align=Text.VAlign.CENTER,
transition_delay=tdelay).autoretain() 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)) ba.timer(15.0, ba.WeakCall(self._show_tips))
def _show_tips(self) -> None: def _show_tips(self) -> None:
from bastd.actor.tipstext import TipsText from bastd.actor.tipstext import TipsText
self._tips_text = TipsText(offs_y=70) self._tips_text = TipsText(offs_y=70)
def _play_victory_music(self) -> None: def _play_victory_music(self) -> None:
@ -327,29 +420,37 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
def _show_winner(self, team: ba.SessionTeam) -> None: def _show_winner(self, team: ba.SessionTeam) -> None:
from bastd.actor.image import Image from bastd.actor.image import Image
from bastd.actor.zoomtext import ZoomText from bastd.actor.zoomtext import ZoomText
if not self._is_ffa: if not self._is_ffa:
offs_v = 0.0 offs_v = 0.0
ZoomText(team.name, ZoomText(
position=(0, 97), team.name,
color=team.color, position=(0, 97),
scale=1.15, color=team.color,
jitter=1.0, scale=1.15,
maxwidth=250).autoretain() jitter=1.0,
maxwidth=250,
).autoretain()
else: else:
offs_v = -80.0 offs_v = -80.0
if len(team.players) == 1: if len(team.players) == 1:
i = Image(team.players[0].get_icon(), i = Image(
position=(0, 143), team.players[0].get_icon(),
scale=(100, 100)).autoretain() position=(0, 143),
scale=(100, 100),
).autoretain()
assert i.node assert i.node
ba.animate(i.node, 'opacity', {0.0: 0.0, 0.25: 1.0}) ba.animate(i.node, 'opacity', {0.0: 0.0, 0.25: 1.0})
ZoomText(ba.Lstr( ZoomText(
value=team.players[0].getname(full=True, icon=False)), ba.Lstr(
position=(0, 97 + offs_v), value=team.players[0].getname(full=True, icon=False)
color=team.color, ),
scale=1.15, position=(0, 97 + offs_v),
jitter=1.0, color=team.color,
maxwidth=250).autoretain() scale=1.15,
jitter=1.0,
maxwidth=250,
).autoretain()
s_extra = 1.0 if self._is_ffa else 1.0 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 # Temp - if these come up as the english default, fall-back to the
# unified old form which is more likely to be translated. # unified old form which is more likely to be translated.
ZoomText(wins_text, ZoomText(
position=(0, -10 + offs_v), wins_text,
color=team.color, position=(0, -10 + offs_v),
scale=0.65 * s_extra, color=team.color,
jitter=1.0, scale=0.65 * s_extra,
maxwidth=250).autoretain() jitter=1.0,
ZoomText(ba.Lstr(resource='seriesWinLine2Text'), maxwidth=250,
position=(0, -110 + offs_v), ).autoretain()
scale=1.0 * s_extra, ZoomText(
color=team.color, ba.Lstr(resource='seriesWinLine2Text'),
jitter=1.0, position=(0, -110 + offs_v),
maxwidth=250).autoretain() 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): class Background(ba.Actor):
"""Simple Fading Background Actor.""" """Simple Fading Background Actor."""
def __init__(self, def __init__(
fade_time: float = 0.5, self,
start_faded: bool = False, fade_time: float = 0.5,
show_logo: bool = False): start_faded: bool = False,
show_logo: bool = False,
):
super().__init__() super().__init__()
self._dying = False self._dying = False
self.fade_time = fade_time self.fade_time = fade_time
@ -31,22 +33,24 @@ class Background(ba.Actor):
session = ba.getsession() session = ba.getsession()
self._session = weakref.ref(session) self._session = weakref.ref(session)
with ba.Context(session): with ba.Context(session):
self.node = ba.newnode('image', self.node = ba.newnode(
delegate=self, 'image',
attrs={ delegate=self,
'fill_screen': True, attrs={
'texture': ba.gettexture('bg'), 'fill_screen': True,
'tilt_translate': -0.3, 'texture': ba.gettexture('bg'),
'has_alpha_channel': False, 'tilt_translate': -0.3,
'color': (1, 1, 1) 'has_alpha_channel': False,
}) 'color': (1, 1, 1),
},
)
if not start_faded: if not start_faded:
ba.animate(self.node, ba.animate(
'opacity', { self.node,
0.0: 0.0, 'opacity',
self.fade_time: 1.0 {0.0: 0.0, self.fade_time: 1.0},
}, loop=False,
loop=False) )
if show_logo: if show_logo:
logo_texture = ba.gettexture('logo') logo_texture = ba.gettexture('logo')
logo_model = ba.getmodel('logo') logo_model = ba.getmodel('logo')
@ -63,27 +67,27 @@ class Background(ba.Actor):
'color': (0.15, 0.15, 0.15), 'color': (0.15, 0.15, 0.15),
'position': (0, 0), 'position': (0, 0),
'tilt_translate': -0.05, 'tilt_translate': -0.05,
'absolute_scale': False 'absolute_scale': False,
}) },
)
self.node.connectattr('opacity', self.logo, 'opacity') self.node.connectattr('opacity', self.logo, 'opacity')
# add jitter/pulse for a stop-motion-y look unless we're in VR # add jitter/pulse for a stop-motion-y look unless we're in VR
# in which case stillness is better # in which case stillness is better
if not ba.app.vr_mode: if not ba.app.vr_mode:
self.cmb = ba.newnode('combine', self.cmb = ba.newnode(
owner=self.node, 'combine', owner=self.node, attrs={'size': 2}
attrs={'size': 2}) )
for attr in ['input0', 'input1']: for attr in ['input0', 'input1']:
ba.animate(self.cmb, ba.animate(
attr, { self.cmb,
0.0: 0.693, attr,
0.05: 0.7, {0.0: 0.693, 0.05: 0.7, 0.5: 0.693},
0.5: 0.693 loop=True,
}, )
loop=True)
self.cmb.connectattr('output', self.logo, 'scale') self.cmb.connectattr('output', self.logo, 'scale')
cmb = ba.newnode('combine', cmb = ba.newnode(
owner=self.node, 'combine', owner=self.node, attrs={'size': 2}
attrs={'size': 2}) )
cmb.connectattr('output', self.logo, 'position') cmb.connectattr('output', self.logo, 'position')
# Gen some random keys for that stop-motion-y look. # Gen some random keys for that stop-motion-y look.
keys = {} keys = {}
@ -114,8 +118,10 @@ class Background(ba.Actor):
# since it was part of the session's scene. # since it was part of the session's scene.
# Let's make sure that's the case. # Let's make sure that's the case.
# (since otherwise we have no way to kill it) # (since otherwise we have no way to kill it)
ba.print_error('got None session on Background _die' ba.print_error(
' (and node still exists!)') 'got None session on Background _die'
' (and node still exists!)'
)
elif session is not None: elif session is not None:
with ba.Context(session): with ba.Context(session):
if not self._dying and self.node: if not self._dying and self.node:
@ -123,12 +129,12 @@ class Background(ba.Actor):
if immediate: if immediate:
self.node.delete() self.node.delete()
else: else:
ba.animate(self.node, ba.animate(
'opacity', { self.node,
0.0: 1.0, 'opacity',
self.fade_time: 0.0 {0.0: 1.0, self.fade_time: 0.0},
}, loop=False,
loop=False) )
ba.timer(self.fade_time + 0.1, self.node.delete) ba.timer(self.fade_time + 0.1, self.node.delete)
def handlemessage(self, msg: Any) -> Any: 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. be newbies watching.
""" """
def __init__(self, def __init__(
position: tuple[float, float] = (390.0, 120.0), self,
scale: float = 1.0, position: tuple[float, float] = (390.0, 120.0),
delay: float = 0.0, scale: float = 1.0,
lifespan: float | None = None, delay: float = 0.0,
bright: bool = False): lifespan: float | None = None,
bright: bool = False,
):
"""Instantiate an overlay. """Instantiate an overlay.
delay: is the time in seconds before the overlay fades in. delay: is the time in seconds before the overlay fades in.
@ -62,19 +64,31 @@ class ControlsGuide(ba.Actor):
if ba.app.iircade_mode: if ba.app.iircade_mode:
xtweak = 0.2 xtweak = 0.2
ytweak = 0.2 ytweak = 0.2
jump_pos = (position[0] + offs * (-1.2 + xtweak), jump_pos = (
position[1] + offs * (0.1 + ytweak)) position[0] + offs * (-1.2 + xtweak),
bomb_pos = (position[0] + offs * (0.0 + xtweak), position[1] + offs * (0.1 + ytweak),
position[1] + offs * (0.5 + ytweak)) )
punch_pos = (position[0] + offs * (1.2 + xtweak), bomb_pos = (
position[1] + offs * (0.5 + ytweak)) 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), pickup_pos = (
position[1] + offs * (-1.2 + ytweak)) position[0] + offs * (-1.4 + xtweak),
extra_pos_1 = (position[0] + offs * (-0.2 + xtweak), position[1] + offs * (-1.2 + ytweak),
position[1] + offs * (-0.8 + ytweak)) )
extra_pos_2 = (position[0] + offs * (1.0 + xtweak), extra_pos_1 = (
position[1] + offs * (-0.8 + ytweak)) 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 self._force_hide_button_names = True
else: else:
punch_pos = (position[0] - offs * 1.1, position[1]) punch_pos = (position[0] - offs * 1.1, position[1])
@ -86,25 +100,32 @@ class ControlsGuide(ba.Actor):
self._force_hide_button_names = False self._force_hide_button_names = False
if show_title: if show_title:
self._title_text_pos_top = (position[0], self._title_text_pos_top = (
position[1] + 139.0 * scale) position[0],
self._title_text_pos_bottom = (position[0], position[1] + 139.0 * scale,
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) clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7)
tval = ba.Lstr(value='${A}:', tval = ba.Lstr(
subs=[('${A}', ba.Lstr(resource='controlsText'))]) value='${A}:', subs=[('${A}', ba.Lstr(resource='controlsText'))]
self._title_text = ba.newnode('text', )
attrs={ self._title_text = ba.newnode(
'text': tval, 'text',
'host_only': True, attrs={
'scale': 1.1 * scale, 'text': tval,
'shadow': 0.5, 'host_only': True,
'flatness': 1.0, 'scale': 1.1 * scale,
'maxwidth': 480, 'shadow': 0.5,
'v_align': 'center', 'flatness': 1.0,
'h_align': 'center', 'maxwidth': 480,
'color': clr 'v_align': 'center',
}) 'h_align': 'center',
'color': clr,
},
)
else: else:
self._title_text = None self._title_text = None
pos = jump_pos pos = jump_pos
@ -118,20 +139,23 @@ class ControlsGuide(ba.Actor):
'vr_depth': 10, 'vr_depth': 10,
'position': pos, 'position': pos,
'scale': (image_size, image_size), 'scale': (image_size, image_size),
'color': clr 'color': clr,
}) },
self._jump_text = ba.newnode('text', )
attrs={ self._jump_text = ba.newnode(
'v_align': 'top', 'text',
'h_align': 'center', attrs={
'scale': 1.5 * scale, 'v_align': 'top',
'flatness': 1.0, 'h_align': 'center',
'host_only': True, 'scale': 1.5 * scale,
'shadow': 1.0, 'flatness': 1.0,
'maxwidth': maxw, 'host_only': True,
'position': (pos[0], pos[1] - offs5), 'shadow': 1.0,
'color': clr 'maxwidth': maxw,
}) 'position': (pos[0], pos[1] - offs5),
'color': clr,
},
)
clr = (0.2, 0.6, 1) if ouya else (1, 0.7, 0.3) clr = (0.2, 0.6, 1) if ouya else (1, 0.7, 0.3)
pos = punch_pos pos = punch_pos
self._punch_image = ba.newnode( self._punch_image = ba.newnode(
@ -143,20 +167,23 @@ class ControlsGuide(ba.Actor):
'vr_depth': 10, 'vr_depth': 10,
'position': pos, 'position': pos,
'scale': (image_size, image_size), 'scale': (image_size, image_size),
'color': clr 'color': clr,
}) },
self._punch_text = ba.newnode('text', )
attrs={ self._punch_text = ba.newnode(
'v_align': 'top', 'text',
'h_align': 'center', attrs={
'scale': 1.5 * scale, 'v_align': 'top',
'flatness': 1.0, 'h_align': 'center',
'host_only': True, 'scale': 1.5 * scale,
'shadow': 1.0, 'flatness': 1.0,
'maxwidth': maxw, 'host_only': True,
'position': (pos[0], pos[1] - offs5), 'shadow': 1.0,
'color': clr 'maxwidth': maxw,
}) 'position': (pos[0], pos[1] - offs5),
'color': clr,
},
)
pos = bomb_pos pos = bomb_pos
clr = (1, 0.3, 0.3) clr = (1, 0.3, 0.3)
self._bomb_image = ba.newnode( self._bomb_image = ba.newnode(
@ -168,20 +195,23 @@ class ControlsGuide(ba.Actor):
'vr_depth': 10, 'vr_depth': 10,
'position': pos, 'position': pos,
'scale': (image_size, image_size), 'scale': (image_size, image_size),
'color': clr 'color': clr,
}) },
self._bomb_text = ba.newnode('text', )
attrs={ self._bomb_text = ba.newnode(
'h_align': 'center', 'text',
'v_align': 'top', attrs={
'scale': 1.5 * scale, 'h_align': 'center',
'flatness': 1.0, 'v_align': 'top',
'host_only': True, 'scale': 1.5 * scale,
'shadow': 1.0, 'flatness': 1.0,
'maxwidth': maxw, 'host_only': True,
'position': (pos[0], pos[1] - offs5), 'shadow': 1.0,
'color': clr 'maxwidth': maxw,
}) 'position': (pos[0], pos[1] - offs5),
'color': clr,
},
)
pos = pickup_pos pos = pickup_pos
clr = (1, 0.8, 0.3) if ouya else (0.8, 0.5, 1) clr = (1, 0.8, 0.3) if ouya else (0.8, 0.5, 1)
self._pickup_image = ba.newnode( self._pickup_image = ba.newnode(
@ -193,25 +223,27 @@ class ControlsGuide(ba.Actor):
'vr_depth': 10, 'vr_depth': 10,
'position': pos, 'position': pos,
'scale': (image_size, image_size), 'scale': (image_size, image_size),
'color': clr 'color': clr,
}) },
self._pick_up_text = ba.newnode('text', )
attrs={ self._pick_up_text = ba.newnode(
'v_align': 'top', 'text',
'h_align': 'center', attrs={
'scale': 1.5 * scale, 'v_align': 'top',
'flatness': 1.0, 'h_align': 'center',
'host_only': True, 'scale': 1.5 * scale,
'shadow': 1.0, 'flatness': 1.0,
'maxwidth': maxw, 'host_only': True,
'position': 'shadow': 1.0,
(pos[0], pos[1] - offs5), 'maxwidth': maxw,
'color': clr '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) 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_top = (position[0], position[1] - 135.0 * scale)
self._run_text_pos_bottom = (position[0], position[1] - 172.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( self._run_text = ba.newnode(
'text', 'text',
attrs={ attrs={
@ -222,20 +254,23 @@ class ControlsGuide(ba.Actor):
'maxwidth': 380, 'maxwidth': 380,
'v_align': 'top', 'v_align': 'top',
'h_align': 'center', 'h_align': 'center',
'color': clr 'color': clr,
}) },
)
clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7) clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7)
self._extra_text = ba.newnode('text', self._extra_text = ba.newnode(
attrs={ 'text',
'scale': 0.8 * scale, attrs={
'host_only': True, 'scale': 0.8 * scale,
'shadow': 0.5, 'host_only': True,
'flatness': 1.0, 'shadow': 0.5,
'maxwidth': 380, 'flatness': 1.0,
'v_align': 'top', 'maxwidth': 380,
'h_align': 'center', 'v_align': 'top',
'color': clr 'h_align': 'center',
}) 'color': clr,
},
)
if extra_pos_1 is not None: if extra_pos_1 is not None:
self._extra_image_1: ba.Node | None = ba.newnode( self._extra_image_1: ba.Node | None = ba.newnode(
@ -247,8 +282,9 @@ class ControlsGuide(ba.Actor):
'vr_depth': 10, 'vr_depth': 10,
'position': extra_pos_1, 'position': extra_pos_1,
'scale': (image_size, image_size), 'scale': (image_size, image_size),
'color': (0.5, 0.5, 0.5) 'color': (0.5, 0.5, 0.5),
}) },
)
else: else:
self._extra_image_1 = None self._extra_image_1 = None
if extra_pos_2 is not None: if extra_pos_2 is not None:
@ -261,16 +297,23 @@ class ControlsGuide(ba.Actor):
'vr_depth': 10, 'vr_depth': 10,
'position': extra_pos_2, 'position': extra_pos_2,
'scale': (image_size, image_size), 'scale': (image_size, image_size),
'color': (0.5, 0.5, 0.5) 'color': (0.5, 0.5, 0.5),
}) },
)
else: else:
self._extra_image_2 = None self._extra_image_2 = None
self._nodes = [ self._nodes = [
self._bomb_image, self._bomb_text, self._punch_image, self._bomb_image,
self._punch_text, self._jump_image, self._jump_text, self._bomb_text,
self._pickup_image, self._pick_up_text, self._run_text, self._punch_image,
self._extra_text 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: if show_title:
assert self._title_text assert self._title_text
@ -304,10 +347,11 @@ class ControlsGuide(ba.Actor):
if self._lifespan is not None: if self._lifespan is not None:
self._cancel_timer = ba.Timer( self._cancel_timer = ba.Timer(
self._lifespan, self._lifespan,
ba.WeakCall(self.handlemessage, ba.DieMessage(immediate=True))) ba.WeakCall(self.handlemessage, ba.DieMessage(immediate=True)),
self._fade_in_timer = ba.Timer(1.0, )
ba.WeakCall(self._check_fade_in), self._fade_in_timer = ba.Timer(
repeat=True) 1.0, ba.WeakCall(self._check_fade_in), repeat=True
)
self._check_fade_in() # Do one check immediately. self._check_fade_in() # Do one check immediately.
def _check_fade_in(self) -> None: def _check_fade_in(self) -> None:
@ -318,7 +362,8 @@ class ControlsGuide(ba.Actor):
# (otherwise it is confusing to see the touchscreen buttons right # (otherwise it is confusing to see the touchscreen buttons right
# next to our display buttons) # next to our display buttons)
touchscreen: ba.InputDevice | None = ba.internal.getinputdevice( touchscreen: ba.InputDevice | None = ba.internal.getinputdevice(
'TouchScreen', '#1', doraise=False) 'TouchScreen', '#1', doraise=False
)
if touchscreen is not None: if touchscreen is not None:
# We look at the session's players; not the activity's. # 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 # Only count this one if it has non-empty button names
# (filters out wiimotes, the remote-app, etc). # (filters out wiimotes, the remote-app, etc).
for device in input_devices: for device in input_devices:
for name in ('buttonPunch', 'buttonJump', 'buttonBomb', for name in (
'buttonPickUp'): 'buttonPunch',
if self._meaningful_button_name( 'buttonJump',
device, get_device_value(device, name)) != '': 'buttonBomb',
'buttonPickUp',
):
if (
self._meaningful_button_name(
device, get_device_value(device, name)
)
!= ''
):
fade_in = True fade_in = True
break break
if fade_in: if fade_in:
@ -357,18 +410,20 @@ class ControlsGuide(ba.Actor):
# If we were given a lifespan, transition out after it. # If we were given a lifespan, transition out after it.
if self._lifespan is not None: if self._lifespan is not None:
ba.timer(self._lifespan, ba.timer(
ba.WeakCall(self.handlemessage, ba.DieMessage())) self._lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage())
)
self._update() self._update()
self._update_timer = ba.Timer(1.0, self._update_timer = ba.Timer(
ba.WeakCall(self._update), 1.0, ba.WeakCall(self._update), repeat=True
repeat=True) )
def _update(self) -> None: def _update(self) -> None:
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
from ba.internal import get_device_value, get_remote_app_name from ba.internal import get_device_value, get_remote_app_name
if self._dead: if self._dead:
return return
punch_button_names = set() punch_button_names = set()
@ -389,11 +444,12 @@ class ControlsGuide(ba.Actor):
input_devices.append(kbd) input_devices.append(kbd)
# We word things specially if we have nothing but keyboards. # We word things specially if we have nothing but keyboards.
all_keyboards = (input_devices all_keyboards = input_devices and all(
and all(i.name == 'Keyboard' for i in input_devices)) i.name == 'Keyboard' for i in input_devices
only_remote = (len(input_devices) == 1 )
and all(i.name == 'Amazon Fire TV Remote' only_remote = len(input_devices) == 1 and all(
for i in input_devices)) i.name == 'Amazon Fire TV Remote' for i in input_devices
)
right_button_names = set() right_button_names = set()
left_button_names = set() left_button_names = set()
@ -408,40 +464,57 @@ class ControlsGuide(ba.Actor):
if all_keyboards: if all_keyboards:
right_button_names.add( right_button_names.add(
device.get_button_name( device.get_button_name(
get_device_value(device, 'buttonRight'))) get_device_value(device, 'buttonRight')
)
)
left_button_names.add( left_button_names.add(
device.get_button_name( device.get_button_name(
get_device_value(device, 'buttonLeft'))) get_device_value(device, 'buttonLeft')
)
)
down_button_names.add( down_button_names.add(
device.get_button_name( device.get_button_name(
get_device_value(device, 'buttonDown'))) get_device_value(device, 'buttonDown')
)
)
up_button_names.add( up_button_names.add(
device.get_button_name(get_device_value( device.get_button_name(get_device_value(device, 'buttonUp'))
device, 'buttonUp'))) )
# Ignore empty values; things like the remote app or # Ignore empty values; things like the remote app or
# wiimotes can return these. # wiimotes can return these.
bname = self._meaningful_button_name( bname = self._meaningful_button_name(
device, get_device_value(device, 'buttonPunch')) device, get_device_value(device, 'buttonPunch')
)
if bname != '': if bname != '':
punch_button_names.add(bname) punch_button_names.add(bname)
bname = self._meaningful_button_name( bname = self._meaningful_button_name(
device, get_device_value(device, 'buttonJump')) device, get_device_value(device, 'buttonJump')
)
if bname != '': if bname != '':
jump_button_names.add(bname) jump_button_names.add(bname)
bname = self._meaningful_button_name( bname = self._meaningful_button_name(
device, get_device_value(device, 'buttonBomb')) device, get_device_value(device, 'buttonBomb')
)
if bname != '': if bname != '':
bomb_button_names.add(bname) bomb_button_names.add(bname)
bname = self._meaningful_button_name( bname = self._meaningful_button_name(
device, get_device_value(device, 'buttonPickUp')) device, get_device_value(device, 'buttonPickUp')
)
if bname != '': if bname != '':
pickup_button_names.add(bname) pickup_button_names.add(bname)
# If we have no values yet, we may want to throw out some sane # If we have no values yet, we may want to throw out some sane
# defaults. # defaults.
if all(not lst for lst in (punch_button_names, jump_button_names, if all(
bomb_button_names, pickup_button_names)): not lst
for lst in (
punch_button_names,
jump_button_names,
bomb_button_names,
pickup_button_names,
)
):
# Otherwise on android show standard buttons. # Otherwise on android show standard buttons.
if ba.app.platform == 'android': if ba.app.platform == 'android':
punch_button_names.add('X') punch_button_names.add('X')
@ -451,24 +524,42 @@ class ControlsGuide(ba.Actor):
run_text = ba.Lstr( run_text = ba.Lstr(
value='${R}: ${B}', value='${R}: ${B}',
subs=[('${R}', ba.Lstr(resource='runText')), subs=[
('${B}', ('${R}', ba.Lstr(resource='runText')),
ba.Lstr(resource='holdAnyKeyText' (
if all_keyboards else 'holdAnyButtonText'))]) '${B}',
ba.Lstr(
resource='holdAnyKeyText'
if all_keyboards
else 'holdAnyButtonText'
),
),
],
)
# If we're all keyboards, lets show move keys too. # If we're all keyboards, lets show move keys too.
if (all_keyboards and len(up_button_names) == 1 if (
and len(down_button_names) == 1 and len(left_button_names) == 1 all_keyboards
and len(right_button_names) == 1): 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] up_text = list(up_button_names)[0]
down_text = list(down_button_names)[0] down_text = list(down_button_names)[0]
left_text = list(left_button_names)[0] left_text = list(left_button_names)[0]
right_text = list(right_button_names)[0] right_text = list(right_button_names)[0]
run_text = ba.Lstr(value='${M}: ${U}, ${L}, ${D}, ${R}\n${RUN}', run_text = ba.Lstr(
subs=[('${M}', ba.Lstr(resource='moveText')), value='${M}: ${U}, ${L}, ${D}, ${R}\n${RUN}',
('${U}', up_text), ('${L}', left_text), subs=[
('${D}', down_text), ('${R}', right_text), ('${M}', ba.Lstr(resource='moveText')),
('${RUN}', run_text)]) ('${U}', up_text),
('${L}', left_text),
('${D}', down_text),
('${R}', right_text),
('${RUN}', run_text),
],
)
if self._force_hide_button_names: if self._force_hide_button_names:
jump_button_names.clear() jump_button_names.clear()
@ -479,9 +570,10 @@ class ControlsGuide(ba.Actor):
self._run_text.text = run_text self._run_text.text = run_text
w_text: ba.Lstr | str w_text: ba.Lstr | str
if only_remote and self._lifespan is None: if only_remote and self._lifespan is None:
w_text = ba.Lstr(resource='fireTVRemoteWarningText', w_text = ba.Lstr(
subs=[('${REMOTE_APP_NAME}', resource='fireTVRemoteWarningText',
get_remote_app_name())]) subs=[('${REMOTE_APP_NAME}', get_remote_app_name())],
)
else: else:
w_text = '' w_text = ''
self._extra_text.text = w_text self._extra_text.text = w_text
@ -497,12 +589,16 @@ class ControlsGuide(ba.Actor):
self._jump_text.text = tval self._jump_text.text = tval
if tval == '': if tval == '':
self._run_text.position = self._run_text_pos_top self._run_text.position = self._run_text_pos_top
self._extra_text.position = (self._run_text_pos_top[0], self._extra_text.position = (
self._run_text_pos_top[1] - 50) self._run_text_pos_top[0],
self._run_text_pos_top[1] - 50,
)
else: else:
self._run_text.position = self._run_text_pos_bottom self._run_text.position = self._run_text_pos_bottom
self._extra_text.position = (self._run_text_pos_bottom[0], self._extra_text.position = (
self._run_text_pos_bottom[1] - 50) self._run_text_pos_bottom[0],
self._run_text_pos_bottom[1] - 50,
)
if len(bomb_button_names) == 1: if len(bomb_button_names) == 1:
self._bomb_text.text = list(bomb_button_names)[0] self._bomb_text.text = list(bomb_button_names)[0]
else: else:

View File

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

View File

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

View File

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

View File

@ -22,23 +22,23 @@ class OnScreenTimer(ba.Actor):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self._starttime_ms: int | None = None self._starttime_ms: int | None = None
self.node = ba.newnode('text', self.node = ba.newnode(
attrs={ 'text',
'v_attach': 'top', attrs={
'h_attach': 'center', 'v_attach': 'top',
'h_align': 'center', 'h_attach': 'center',
'color': (1, 1, 0.5, 1), 'h_align': 'center',
'flatness': 0.5, 'color': (1, 1, 0.5, 1),
'shadow': 0.5, 'flatness': 0.5,
'position': (0, -70), 'shadow': 0.5,
'scale': 1.4, 'position': (0, -70),
'text': '' 'scale': 1.4,
}) 'text': '',
self.inputnode = ba.newnode('timedisplay', },
attrs={ )
'timemin': 0, self.inputnode = ba.newnode(
'showsubseconds': True 'timedisplay', attrs={'timemin': 0, 'showsubseconds': True}
}) )
self.inputnode.connectattr('output', self.node, 'text') self.inputnode.connectattr('output', self.node, 'text')
def start(self) -> None: def start(self) -> None:
@ -47,16 +47,19 @@ class OnScreenTimer(ba.Actor):
assert isinstance(tval, int) assert isinstance(tval, int)
self._starttime_ms = tval self._starttime_ms = tval
self.inputnode.time1 = self._starttime_ms self.inputnode.time1 = self._starttime_ms
ba.getactivity().globalsnode.connectattr('time', self.inputnode, ba.getactivity().globalsnode.connectattr(
'time2') 'time', self.inputnode, 'time2'
)
def has_started(self) -> bool: def has_started(self) -> bool:
"""Return whether this timer has started yet.""" """Return whether this timer has started yet."""
return self._starttime_ms is not None return self._starttime_ms is not None
def stop(self, def stop(
endtime: int | float | None = None, self,
timeformat: ba.TimeFormat = ba.TimeFormat.SECONDS) -> None: endtime: int | float | None = None,
timeformat: ba.TimeFormat = ba.TimeFormat.SECONDS,
) -> None:
"""End the timer. """End the timer.
If 'endtime' is not None, it is used when calculating 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. # Overloads so type checker knows our exact return type based in args.
@overload @overload
def getstarttime( def getstarttime(
self, self, timeformat: Literal[ba.TimeFormat.SECONDS] = ba.TimeFormat.SECONDS
timeformat: Literal[ba.TimeFormat.SECONDS] = ba.TimeFormat.SECONDS
) -> float: ) -> float:
... ...
@overload @overload
def getstarttime(self, def getstarttime(
timeformat: Literal[ba.TimeFormat.MILLISECONDS]) -> int: self, timeformat: Literal[ba.TimeFormat.MILLISECONDS]
) -> int:
... ...
def getstarttime( def getstarttime(
self, self, timeformat: ba.TimeFormat = ba.TimeFormat.SECONDS
timeformat: ba.TimeFormat = ba.TimeFormat.SECONDS) -> int | float: ) -> int | float:
"""Return the sim-time when start() was called. """Return the sim-time when start() was called.
Time will be returned in seconds if timeformat is SECONDS or 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. to the current ba.Activity.
""" """
def __init__(self, def __init__(
player: ba.Player, self,
color: Sequence[float] = (1.0, 1.0, 1.0), player: ba.Player,
highlight: Sequence[float] = (0.5, 0.5, 0.5), color: Sequence[float] = (1.0, 1.0, 1.0),
character: str = 'Spaz', highlight: Sequence[float] = (0.5, 0.5, 0.5),
powerups_expire: bool = True): character: str = 'Spaz',
powerups_expire: bool = True,
):
"""Create a spaz for the provided ba.Player. """Create a spaz for the provided ba.Player.
Note: this does not wire up any controls; Note: this does not wire up any controls;
you must call connect_controls_to_player() to do so. you must call connect_controls_to_player() to do so.
""" """
super().__init__(color=color, super().__init__(
highlight=highlight, color=color,
character=character, highlight=highlight,
source_player=player, character=character,
start_invincible=True, source_player=player,
powerups_expire=powerups_expire) start_invincible=True,
powerups_expire=powerups_expire,
)
self.last_player_attacked_by: ba.Player | None = None self.last_player_attacked_by: ba.Player | None = None
self.last_attacked_time = 0.0 self.last_attacked_time = 0.0
self.last_attacked_type: tuple[str, str] | None = None 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. # Overloads to tell the type system our return type based on doraise val.
@overload @overload
def getplayer(self, def getplayer(
playertype: type[PlayerType], self, playertype: type[PlayerType], doraise: Literal[False] = False
doraise: Literal[False] = False) -> PlayerType | None: ) -> PlayerType | None:
... ...
@overload @overload
def getplayer(self, playertype: type[PlayerType], def getplayer(
doraise: Literal[True]) -> PlayerType: self, playertype: type[PlayerType], doraise: Literal[True]
) -> PlayerType:
... ...
def getplayer(self, def getplayer(
playertype: type[PlayerType], self, playertype: type[PlayerType], doraise: bool = False
doraise: bool = False) -> PlayerType | None: ) -> PlayerType | None:
"""Get the ba.Player associated with this Spaz. """Get the ba.Player associated with this Spaz.
By default this will return None if the Player no longer exists. By default this will return None if the Player no longer exists.
@ -99,13 +104,15 @@ class PlayerSpaz(Spaz):
raise ba.PlayerNotFoundError() raise ba.PlayerNotFoundError()
return player if player.exists() else None return player if player.exists() else None
def connect_controls_to_player(self, def connect_controls_to_player(
enable_jump: bool = True, self,
enable_punch: bool = True, enable_jump: bool = True,
enable_pickup: bool = True, enable_punch: bool = True,
enable_bomb: bool = True, enable_pickup: bool = True,
enable_run: bool = True, enable_bomb: bool = True,
enable_fly: bool = True) -> None: enable_run: bool = True,
enable_fly: bool = True,
) -> None:
"""Wire this spaz up to the provided ba.Player. """Wire this spaz up to the provided ba.Player.
Full control of the character is given by default 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.UP_DOWN, self.on_move_up_down)
player.assigninput(ba.InputType.LEFT_RIGHT, self.on_move_left_right) player.assigninput(ba.InputType.LEFT_RIGHT, self.on_move_left_right)
player.assigninput(ba.InputType.HOLD_POSITION_PRESS, player.assigninput(
self.on_hold_position_press) 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_RELEASE, self.on_hold_position_release
)
intp = ba.InputType intp = ba.InputType
if enable_jump: if enable_jump:
player.assigninput(intp.JUMP_PRESS, self.on_jump_press) player.assigninput(intp.JUMP_PRESS, self.on_jump_press)
@ -171,8 +180,10 @@ class PlayerSpaz(Spaz):
self.on_run(0.0) self.on_run(0.0)
self.on_fly_release() self.on_fly_release()
else: else:
print('WARNING: disconnect_controls_from_player() called for' print(
' non-connected player') 'WARNING: disconnect_controls_from_player() called for'
' non-connected player'
)
def handlemessage(self, msg: Any) -> Any: def handlemessage(self, msg: Any) -> Any:
# FIXME: Tidy this up. # FIXME: Tidy this up.
@ -216,8 +227,9 @@ class PlayerSpaz(Spaz):
if not self._dead: if not self._dead:
# Immediate-mode or left-game deaths don't count as 'kills'. # Immediate-mode or left-game deaths don't count as 'kills'.
killed = (not msg.immediate killed = (
and msg.how is not ba.DeathType.LEFT_GAME) not msg.immediate and msg.how is not ba.DeathType.LEFT_GAME
)
activity = self._activity() activity = self._activity()
@ -237,13 +249,16 @@ class PlayerSpaz(Spaz):
# all bot kills would register as suicides; need to # all bot kills would register as suicides; need to
# change this from last_player_attacked_by to # change this from last_player_attacked_by to
# something like last_actor_attacked_by to fix that. # something like last_actor_attacked_by to fix that.
if (self.last_player_attacked_by if (
and ba.time() - self.last_attacked_time < 4.0): self.last_player_attacked_by
and ba.time() - self.last_attacked_time < 4.0
):
killerplayer = self.last_player_attacked_by killerplayer = self.last_player_attacked_by
else: else:
# ok, call it a suicide unless we're in co-op # ok, call it a suicide unless we're in co-op
if (activity is not None and not isinstance( if activity is not None and not isinstance(
activity.session, ba.CoopSession)): activity.session, ba.CoopSession
):
killerplayer = player killerplayer = player
else: else:
killerplayer = None killerplayer = None
@ -255,8 +270,10 @@ class PlayerSpaz(Spaz):
# Only report if both the player and the activity still exist. # Only report if both the player and the activity still exist.
if killed and activity is not None and player: if killed and activity is not None and player:
activity.handlemessage( activity.handlemessage(
ba.PlayerDiedMessage(player, killed, killerplayer, ba.PlayerDiedMessage(
msg.how)) player, killed, killerplayer, msg.how
)
)
super().handlemessage(msg) # Augment standard behavior. super().handlemessage(msg) # Augment standard behavior.

View File

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

View File

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

View File

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

View File

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

View File

@ -38,10 +38,10 @@ class Spawner:
"""The spawn position.""" """The spawn position."""
def __init__( def __init__(
self, self,
spawner: Spawner, spawner: Spawner,
data: Any, data: Any,
pt: Sequence[float], # pylint: disable=invalid-name pt: Sequence[float], # pylint: disable=invalid-name
): ):
"""Instantiate with the given values.""" """Instantiate with the given values."""
self.spawner = spawner self.spawner = spawner
@ -49,12 +49,13 @@ class Spawner:
self.pt = pt # pylint: disable=invalid-name self.pt = pt # pylint: disable=invalid-name
def __init__( def __init__(
self, self,
data: Any = None, data: Any = None,
pt: Sequence[float] = (0, 0, 0), # pylint: disable=invalid-name pt: Sequence[float] = (0, 0, 0), # pylint: disable=invalid-name
spawn_time: float = 1.0, spawn_time: float = 1.0,
send_spawn_message: bool = True, send_spawn_message: bool = True,
spawn_callback: Callable[[], Any] | None = None): spawn_callback: Callable[[], Any] | None = None,
):
"""Instantiate a Spawner. """Instantiate a Spawner.
Requires some custom data, a position, Requires some custom data, a position,
@ -66,19 +67,23 @@ class Spawner:
self._data = data self._data = data
self._pt = pt self._pt = pt
# create a light where the spawn will happen # create a light where the spawn will happen
self._light = ba.newnode('light', self._light = ba.newnode(
attrs={ 'light',
'position': tuple(pt), attrs={
'radius': 0.1, 'position': tuple(pt),
'color': (1.0, 0.1, 0.1), 'radius': 0.1,
'lights_volumes': False 'color': (1.0, 0.1, 0.1),
}) 'lights_volumes': False,
},
)
scl = float(spawn_time) / 3.75 scl = float(spawn_time) / 3.75
min_val = 0.4 min_val = 0.4
max_val = 0.7 max_val = 0.7
ba.playsound(self._spawner_sound, position=self._light.position) ba.playsound(self._spawner_sound, position=self._light.position)
ba.animate( ba.animate(
self._light, 'intensity', { self._light,
'intensity',
{
0.0: 0.0, 0.0: 0.0,
0.25 * scl: max_val, 0.25 * scl: max_val,
0.500 * scl: min_val, 0.500 * scl: min_val,
@ -95,8 +100,9 @@ class Spawner:
3.250 * scl: 1.5 * max_val, 3.250 * scl: 1.5 * max_val,
3.500 * scl: min_val, 3.500 * scl: min_val,
3.750 * scl: 2.0, 3.750 * scl: 2.0,
4.000 * scl: 0.0 4.000 * scl: 0.0,
}) },
)
ba.timer(spawn_time, self._spawn) ba.timer(spawn_time, self._spawn)
def _spawn(self) -> None: def _spawn(self) -> None:
@ -108,4 +114,5 @@ class Spawner:
activity = ba.getactivity() activity = ba.getactivity()
if activity is not None: if activity is not None:
activity.handlemessage( 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): def __init__(self, name: str):
self.name = name self.name = name
if self.name in ba.app.spaz_appearances: if self.name in ba.app.spaz_appearances:
raise Exception('spaz appearance name "' + self.name + raise Exception(
'" already exists.') 'spaz appearance name "' + self.name + '" already exists.'
)
ba.app.spaz_appearances[self.name] = self ba.app.spaz_appearances[self.name] = self
self.color_texture = '' self.color_texture = ''
self.color_mask_texture = '' self.color_mask_texture = ''
@ -141,10 +142,16 @@ def register_appearances() -> None:
t.toes_model = 'neoSpazToes' t.toes_model = 'neoSpazToes'
t.jump_sounds = ['spazJump01', 'spazJump02', 'spazJump03', 'spazJump04'] t.jump_sounds = ['spazJump01', 'spazJump02', 'spazJump03', 'spazJump04']
t.attack_sounds = [ t.attack_sounds = [
'spazAttack01', 'spazAttack02', 'spazAttack03', 'spazAttack04' 'spazAttack01',
'spazAttack02',
'spazAttack03',
'spazAttack04',
] ]
t.impact_sounds = [ t.impact_sounds = [
'spazImpact01', 'spazImpact02', 'spazImpact03', 'spazImpact04' 'spazImpact01',
'spazImpact02',
'spazImpact03',
'spazImpact04',
] ]
t.death_sounds = ['spazDeath01'] t.death_sounds = ['spazDeath01']
t.pickup_sounds = ['spazPickup01'] t.pickup_sounds = ['spazPickup01']
@ -170,10 +177,16 @@ def register_appearances() -> None:
t.toes_model = 'zoeToes' t.toes_model = 'zoeToes'
t.jump_sounds = ['zoeJump01', 'zoeJump02', 'zoeJump03'] t.jump_sounds = ['zoeJump01', 'zoeJump02', 'zoeJump03']
t.attack_sounds = [ t.attack_sounds = [
'zoeAttack01', 'zoeAttack02', 'zoeAttack03', 'zoeAttack04' 'zoeAttack01',
'zoeAttack02',
'zoeAttack03',
'zoeAttack04',
] ]
t.impact_sounds = [ t.impact_sounds = [
'zoeImpact01', 'zoeImpact02', 'zoeImpact03', 'zoeImpact04' 'zoeImpact01',
'zoeImpact02',
'zoeImpact03',
'zoeImpact04',
] ]
t.death_sounds = ['zoeDeath01'] t.death_sounds = ['zoeDeath01']
t.pickup_sounds = ['zoePickup01'] t.pickup_sounds = ['zoePickup01']
@ -226,8 +239,16 @@ def register_appearances() -> None:
t.lower_leg_model = 'kronkLowerLeg' t.lower_leg_model = 'kronkLowerLeg'
t.toes_model = 'kronkToes' t.toes_model = 'kronkToes'
kronk_sounds = [ kronk_sounds = [
'kronk1', 'kronk2', 'kronk3', 'kronk4', 'kronk5', 'kronk6', 'kronk7', 'kronk1',
'kronk8', 'kronk9', 'kronk10' 'kronk2',
'kronk3',
'kronk4',
'kronk5',
'kronk6',
'kronk7',
'kronk8',
'kronk9',
'kronk10',
] ]
t.jump_sounds = kronk_sounds t.jump_sounds = kronk_sounds
t.attack_sounds = kronk_sounds t.attack_sounds = kronk_sounds
@ -255,8 +276,16 @@ def register_appearances() -> None:
t.lower_leg_model = 'melLowerLeg' t.lower_leg_model = 'melLowerLeg'
t.toes_model = 'melToes' t.toes_model = 'melToes'
mel_sounds = [ mel_sounds = [
'mel01', 'mel02', 'mel03', 'mel04', 'mel05', 'mel06', 'mel07', 'mel08', 'mel01',
'mel09', 'mel10' 'mel02',
'mel03',
'mel04',
'mel05',
'mel06',
'mel07',
'mel08',
'mel09',
'mel10',
] ]
t.attack_sounds = mel_sounds t.attack_sounds = mel_sounds
t.jump_sounds = mel_sounds t.jump_sounds = mel_sounds
@ -284,8 +313,13 @@ def register_appearances() -> None:
t.lower_leg_model = 'jackLowerLeg' t.lower_leg_model = 'jackLowerLeg'
t.toes_model = 'jackToes' t.toes_model = 'jackToes'
hit_sounds = [ hit_sounds = [
'jackHit01', 'jackHit02', 'jackHit03', 'jackHit04', 'jackHit05', 'jackHit01',
'jackHit06', 'jackHit07' 'jackHit02',
'jackHit03',
'jackHit04',
'jackHit05',
'jackHit06',
'jackHit07',
] ]
sounds = ['jack01', 'jack02', 'jack03', 'jack04', 'jack05', 'jack06'] sounds = ['jack01', 'jack02', 'jack03', 'jack04', 'jack05', 'jack06']
t.attack_sounds = sounds t.attack_sounds = sounds
@ -340,9 +374,7 @@ def register_appearances() -> None:
t.upper_leg_model = 'frostyUpperLeg' t.upper_leg_model = 'frostyUpperLeg'
t.lower_leg_model = 'frostyLowerLeg' t.lower_leg_model = 'frostyLowerLeg'
t.toes_model = 'frostyToes' t.toes_model = 'frostyToes'
frosty_sounds = [ frosty_sounds = ['frosty01', 'frosty02', 'frosty03', 'frosty04', 'frosty05']
'frosty01', 'frosty02', 'frosty03', 'frosty04', 'frosty05'
]
frosty_hit_sounds = ['frostyHit01', 'frostyHit02', 'frostyHit03'] frosty_hit_sounds = ['frostyHit01', 'frostyHit02', 'frostyHit03']
t.attack_sounds = frosty_sounds t.attack_sounds = frosty_sounds
t.jump_sounds = frosty_sounds t.jump_sounds = frosty_sounds
@ -558,7 +590,10 @@ def register_appearances() -> None:
t.lower_leg_model = 'actionHeroLowerLeg' t.lower_leg_model = 'actionHeroLowerLeg'
t.toes_model = 'actionHeroToes' t.toes_model = 'actionHeroToes'
action_hero_sounds = [ action_hero_sounds = [
'actionHero1', 'actionHero2', 'actionHero3', 'actionHero4' 'actionHero1',
'actionHero2',
'actionHero3',
'actionHero4',
] ]
action_hero_hit_sounds = ['actionHeroHit1', 'actionHeroHit2'] action_hero_hit_sounds = ['actionHeroHit1', 'actionHeroHit2']
t.attack_sounds = action_hero_sounds t.attack_sounds = action_hero_sounds
@ -857,7 +892,10 @@ def register_appearances() -> None:
t.lower_leg_model = 'operaSingerLowerLeg' t.lower_leg_model = 'operaSingerLowerLeg'
t.toes_model = 'operaSingerToes' t.toes_model = 'operaSingerToes'
opera_singer_sounds = [ opera_singer_sounds = [
'operaSinger1', 'operaSinger2', 'operaSinger3', 'operaSinger4' 'operaSinger1',
'operaSinger2',
'operaSinger3',
'operaSinger4',
] ]
opera_singer_hit_sounds = ['operaSingerHit1', 'operaSingerHit2'] opera_singer_hit_sounds = ['operaSingerHit1', 'operaSingerHit2']
t.attack_sounds = opera_singer_sounds t.attack_sounds = opera_singer_sounds

View File

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

View File

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

View File

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

View File

@ -20,44 +20,47 @@ class TipsText(ba.Actor):
self._tip_scale = 0.8 self._tip_scale = 0.8
self._tip_title_scale = 1.1 self._tip_title_scale = 1.1
self._offs_y = offs_y self._offs_y = offs_y
self.node = ba.newnode('text', self.node = ba.newnode(
delegate=self, 'text',
attrs={ delegate=self,
'text': '', attrs={
'scale': self._tip_scale, 'text': '',
'h_align': 'left', 'scale': self._tip_scale,
'maxwidth': 800, 'h_align': 'left',
'vr_depth': -20, 'maxwidth': 800,
'v_align': 'center', 'vr_depth': -20,
'v_attach': 'bottom' 'v_align': 'center',
}) 'v_attach': 'bottom',
tval = ba.Lstr(value='${A}:', },
subs=[('${A}', ba.Lstr(resource='tipText'))]) )
self.title_node = ba.newnode('text', tval = ba.Lstr(
delegate=self, value='${A}:', subs=[('${A}', ba.Lstr(resource='tipText'))]
attrs={ )
'text': tval, self.title_node = ba.newnode(
'scale': self._tip_title_scale, 'text',
'maxwidth': 122, delegate=self,
'h_align': 'right', attrs={
'vr_depth': -20, 'text': tval,
'v_align': 'center', 'scale': self._tip_title_scale,
'v_attach': 'bottom' 'maxwidth': 122,
}) 'h_align': 'right',
'vr_depth': -20,
'v_align': 'center',
'v_attach': 'bottom',
},
)
self._message_duration = 10000 self._message_duration = 10000
self._message_spacing = 3000 self._message_spacing = 3000
self._change_timer = ba.Timer( self._change_timer = ba.Timer(
0.001 * (self._message_duration + self._message_spacing), 0.001 * (self._message_duration + self._message_spacing),
ba.WeakCall(self.change_phrase), ba.WeakCall(self.change_phrase),
repeat=True) repeat=True,
self._combine = ba.newnode('combine', )
owner=self.node, self._combine = ba.newnode(
attrs={ 'combine',
'input0': 1.0, owner=self.node,
'input1': 0.8, attrs={'input0': 1.0, 'input1': 0.8, 'input2': 1.0, 'size': 4},
'input2': 1.0, )
'size': 4
})
self._combine.connectattr('output', self.node, 'color') self._combine.connectattr('output', self.node, 'color')
self._combine.connectattr('output', self.title_node, 'color') self._combine.connectattr('output', self.title_node, 'color')
self.change_phrase() self.change_phrase()
@ -65,9 +68,11 @@ class TipsText(ba.Actor):
def change_phrase(self) -> None: def change_phrase(self) -> None:
"""Switch the visible tip phrase.""" """Switch the visible tip phrase."""
from ba.internal import get_remote_app_name, get_next_tip 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 spc = self._message_spacing
assert self.node assert self.node
self.node.position = (-200, self._offs_y) self.node.position = (-200, self._offs_y)
@ -76,12 +81,14 @@ class TipsText(ba.Actor):
spc: 0, spc: 0,
spc + 1000: 1.0, spc + 1000: 1.0,
spc + self._message_duration - 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, ba.animate(
'input3', {k: v * 0.5 self._combine,
for k, v in list(keys.items())}, 'input3',
timeformat=ba.TimeFormat.MILLISECONDS) {k: v * 0.5 for k, v in list(keys.items())},
timeformat=ba.TimeFormat.MILLISECONDS,
)
self.node.text = next_tip self.node.text = next_tip
def handlemessage(self, msg: Any) -> Any: 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. Used for things such as the 'BOB WINS' victory messages.
""" """
def __init__(self, def __init__(
text: str | ba.Lstr, self,
position: tuple[float, float] = (0.0, 0.0), text: str | ba.Lstr,
shiftposition: tuple[float, float] | None = None, position: tuple[float, float] = (0.0, 0.0),
shiftdelay: float | None = None, shiftposition: tuple[float, float] | None = None,
lifespan: float | None = None, shiftdelay: float | None = None,
flash: bool = True, lifespan: float | None = None,
trail: bool = True, flash: bool = True,
h_align: str = 'center', trail: bool = True,
color: Sequence[float] = (0.9, 0.4, 0.0), h_align: str = 'center',
jitter: float = 0.0, color: Sequence[float] = (0.9, 0.4, 0.0),
trailcolor: Sequence[float] = (1.0, 0.35, 0.1, 0.0), jitter: float = 0.0,
scale: float = 1.0, trailcolor: Sequence[float] = (1.0, 0.35, 0.1, 0.0),
project_scale: float = 1.0, scale: float = 1.0,
tilt_translate: float = 0.0, project_scale: float = 1.0,
maxwidth: float | None = None): tilt_translate: float = 0.0,
maxwidth: float | None = None,
):
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
super().__init__() super().__init__()
self._dying = False self._dying = False
@ -61,8 +63,9 @@ class ZoomText(ba.Actor):
'maxwidth': maxwidth if maxwidth is not None else 0.0, 'maxwidth': maxwidth if maxwidth is not None else 0.0,
'tilt_translate': tilt_translate, 'tilt_translate': tilt_translate,
'h_align': h_align, 'h_align': h_align,
'v_align': 'center' 'v_align': 'center',
}) },
)
# we never jitter in vr mode.. # we never jitter in vr mode..
if ba.app.vr_mode: if ba.app.vr_mode:
@ -78,79 +81,81 @@ class ZoomText(ba.Actor):
positionadjusted2 = (shiftposition[0], shiftposition[1] - 100) positionadjusted2 = (shiftposition[0], shiftposition[1] - 100)
ba.timer( ba.timer(
shiftdelay, shiftdelay,
ba.WeakCall(self._shift, positionadjusted, positionadjusted2)) ba.WeakCall(self._shift, positionadjusted, positionadjusted2),
)
if jitter > 0.0: if jitter > 0.0:
ba.timer( ba.timer(
shiftdelay + 0.25, shiftdelay + 0.25,
ba.WeakCall(self._jitter, positionadjusted2, ba.WeakCall(
jitter * scale)) self._jitter, positionadjusted2, jitter * scale
color_combine = ba.newnode('combine', ),
owner=self.node, )
attrs={ color_combine = ba.newnode(
'input2': color[2], 'combine',
'input3': 1.0, owner=self.node,
'size': 4 attrs={'input2': color[2], 'input3': 1.0, 'size': 4},
}) )
if trail: if trail:
trailcolor_n = ba.newnode('combine', trailcolor_n = ba.newnode(
owner=self.node, 'combine',
attrs={ owner=self.node,
'size': 3, attrs={
'input0': trailcolor[0], 'size': 3,
'input1': trailcolor[1], 'input0': trailcolor[0],
'input2': trailcolor[2] 'input1': trailcolor[1],
}) 'input2': trailcolor[2],
},
)
trailcolor_n.connectattr('output', self.node, 'trailcolor') trailcolor_n.connectattr('output', self.node, 'trailcolor')
basemult = 0.85 basemult = 0.85
ba.animate( ba.animate(
self.node, 'trail_project_scale', { self.node,
'trail_project_scale',
{
0: 0 * project_scale, 0: 0 * project_scale,
basemult * 0.201: 0.6 * project_scale, basemult * 0.201: 0.6 * project_scale,
basemult * 0.347: 0.8 * project_scale, basemult * 0.347: 0.8 * project_scale,
basemult * 0.478: 0.9 * project_scale, basemult * 0.478: 0.9 * project_scale,
basemult * 0.595: 0.93 * project_scale, basemult * 0.595: 0.93 * project_scale,
basemult * 0.748: 0.95 * 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: if flash:
mult = 2.0 mult = 2.0
tm1 = 0.15 tm1 = 0.15
tm2 = 0.3 tm2 = 0.3
ba.animate(color_combine, ba.animate(
'input0', { color_combine,
0: color[0] * mult, 'input0',
tm1: color[0], {0: color[0] * mult, tm1: color[0], tm2: color[0] * mult},
tm2: color[0] * mult loop=True,
}, )
loop=True) ba.animate(
ba.animate(color_combine, color_combine,
'input1', { 'input1',
0: color[1] * mult, {0: color[1] * mult, tm1: color[1], tm2: color[1] * mult},
tm1: color[1], loop=True,
tm2: color[1] * mult )
}, ba.animate(
loop=True) color_combine,
ba.animate(color_combine, 'input2',
'input2', { {0: color[2] * mult, tm1: color[2], tm2: color[2] * mult},
0: color[2] * mult, loop=True,
tm1: color[2], )
tm2: color[2] * mult
},
loop=True)
else: else:
color_combine.input0 = color[0] color_combine.input0 = color[0]
color_combine.input1 = color[1] color_combine.input1 = color[1]
color_combine.connectattr('output', self.node, 'color') color_combine.connectattr('output', self.node, 'color')
ba.animate(self.node, 'project_scale', { ba.animate(
0: 0, self.node,
0.27: 1.05 * project_scale, 'project_scale',
0.3: 1 * 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 they give us a lifespan, kill ourself down the line
if lifespan is not None: if lifespan is not None:
ba.timer(lifespan, ba.WeakCall(self.handlemessage, ba.timer(lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage()))
ba.DieMessage()))
def handlemessage(self, msg: Any) -> Any: def handlemessage(self, msg: Any) -> Any:
assert not self.expired assert not self.expired
@ -161,18 +166,22 @@ class ZoomText(ba.Actor):
self.node.delete() self.node.delete()
else: else:
ba.animate( ba.animate(
self.node, 'project_scale', { self.node,
'project_scale',
{
0.0: 1 * self._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, 'opacity', {0.0: 1, 0.3: 0})
ba.animate(self.node, 'trail_opacity', {0.0: 1, 0.6: 0}) ba.animate(self.node, 'trail_opacity', {0.0: 1, 0.6: 0})
ba.timer(0.7, self.node.delete) ba.timer(0.7, self.node.delete)
return None return None
return super().handlemessage(msg) return super().handlemessage(msg)
def _jitter(self, position: tuple[float, float], def _jitter(
jitter_amount: float) -> None: self, position: tuple[float, float], jitter_amount: float
) -> None:
if not self.node: if not self.node:
return return
cmb = ba.newnode('combine', owner=self.node, attrs={'size': 2}) cmb = ba.newnode('combine', owner=self.node, attrs={'size': 2})
@ -181,14 +190,17 @@ class ZoomText(ba.Actor):
timeval = 0.0 timeval = 0.0
# gen some random keys for that stop-motion-y look # gen some random keys for that stop-motion-y look
for _i in range(10): for _i in range(10):
keys[timeval] = (position[index] + keys[timeval] = (
(random.random() - 0.5) * jitter_amount * 1.6) position[index]
+ (random.random() - 0.5) * jitter_amount * 1.6
)
timeval += random.random() * 0.1 timeval += random.random() * 0.1
ba.animate(cmb, attr, keys, loop=True) ba.animate(cmb, attr, keys, loop=True)
cmb.connectattr('output', self.node, 'position') cmb.connectattr('output', self.node, 'position')
def _shift(self, position1: tuple[float, float], def _shift(
position2: tuple[float, float]) -> None: self, position1: tuple[float, float], position2: tuple[float, float]
) -> None:
if not self.node: if not self.node:
return return
cmb = ba.newnode('combine', owner=self.node, attrs={'size': 2}) 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.""" """Defines handlers for high level app functionality."""
def create_default_game_settings_ui( def create_default_game_settings_ui(
self, gameclass: type[ba.GameActivity], self,
sessiontype: type[ba.Session], settings: dict | None, gameclass: type[ba.GameActivity],
completion_call: Callable[[dict | None], Any]) -> None: sessiontype: type[ba.Session],
settings: dict | None,
completion_call: Callable[[dict | None], Any],
) -> None:
"""(internal)""" """(internal)"""
# Replace the main window once we come up successfully. # Replace the main window once we come up successfully.
from bastd.ui.playlist.editgame import PlaylistEditGameWindow from bastd.ui.playlist.editgame import PlaylistEditGameWindow
ba.app.ui.clear_main_menu_window(transition='out_left') ba.app.ui.clear_main_menu_window(transition='out_left')
ba.app.ui.set_main_menu_window( ba.app.ui.set_main_menu_window(
PlaylistEditGameWindow( PlaylistEditGameWindow(
gameclass, gameclass,
sessiontype, sessiontype,
settings, 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 # Base class overrides
self.slow_motion = self._epic_mode self.slow_motion = self._epic_mode
self.default_music = (ba.MusicType.EPIC if self._epic_mode else self.default_music = (
ba.MusicType.FORWARD_MARCH) ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FORWARD_MARCH
)
def get_instance_description(self) -> str | Sequence: def get_instance_description(self) -> str | Sequence:
if self._score_to_win == 1: if self._score_to_win == 1:
@ -107,19 +108,19 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]):
def create_team(self, sessionteam: ba.SessionTeam) -> Team: def create_team(self, sessionteam: ba.SessionTeam) -> Team:
shared = SharedObjects.get() shared = SharedObjects.get()
base_pos = self.map.get_flag_position(sessionteam.id) base_pos = self.map.get_flag_position(sessionteam.id)
ba.newnode('light', ba.newnode(
attrs={ 'light',
'position': base_pos, attrs={
'intensity': 0.6, 'position': base_pos,
'height_attenuated': False, 'intensity': 0.6,
'volume_intensity_scale': 0.1, 'height_attenuated': False,
'radius': 0.1, 'volume_intensity_scale': 0.1,
'color': sessionteam.color 'radius': 0.1,
}) 'color': sessionteam.color,
},
)
Flag.project_stand(base_pos) Flag.project_stand(base_pos)
flag = Flag(touchable=False, flag = Flag(touchable=False, position=base_pos, color=sessionteam.color)
position=base_pos,
color=sessionteam.color)
team = Team(base_pos=base_pos, flag=flag) team = Team(base_pos=base_pos, flag=flag)
mat = self._base_region_materials[sessionteam.id] = ba.Material() mat = self._base_region_materials[sessionteam.id] = ba.Material()
@ -128,8 +129,11 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]):
actions=( actions=(
('modify_part_collision', 'collide', True), ('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False), ('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]), 'position': (base_pos[0], base_pos[1] + 0.75, base_pos[2]),
'scale': (0.5, 0.5, 0.5), 'scale': (0.5, 0.5, 0.5),
'type': 'sphere', 'type': 'sphere',
'materials': [self._base_region_materials[sessionteam.id]] 'materials': [self._base_region_materials[sessionteam.id]],
}) },
)
return team return team
@ -163,13 +168,15 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]):
super().handlemessage(msg) super().handlemessage(msg)
def _flash_base(self, team: Team, length: float = 2.0) -> None: def _flash_base(self, team: Team, length: float = 2.0) -> None:
light = ba.newnode('light', light = ba.newnode(
attrs={ 'light',
'position': team.base_pos, attrs={
'height_attenuated': False, 'position': team.base_pos,
'radius': 0.3, 'height_attenuated': False,
'color': team.color 'radius': 0.3,
}) 'color': team.color,
},
)
ba.animate(light, 'intensity', {0: 0, 0.25: 2.0, 0.5: 0}, loop=True) ba.animate(light, 'intensity', {0: 0, 0.25: 2.0, 0.5: 0}, loop=True)
ba.timer(length, light.delete) ba.timer(length, light.delete)
@ -203,38 +210,34 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]):
for player in player_team.players: for player in player_team.players:
if player.is_alive(): if player.is_alive():
pos = player.node.position pos = player.node.position
light = ba.newnode('light', light = ba.newnode(
attrs={ 'light',
'position': pos, attrs={
'color': player_team.color, 'position': pos,
'height_attenuated': False, 'color': player_team.color,
'radius': 0.4 'height_attenuated': False,
}) 'radius': 0.4,
},
)
ba.timer(0.5, light.delete) ba.timer(0.5, light.delete)
ba.animate(light, 'intensity', { ba.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0})
0: 0,
0.1: 1.0,
0.5: 0
})
new_pos = (self.map.get_start_position(player_team.id)) new_pos = self.map.get_start_position(player_team.id)
light = ba.newnode('light', light = ba.newnode(
attrs={ 'light',
'position': new_pos, attrs={
'color': player_team.color, 'position': new_pos,
'radius': 0.4, 'color': player_team.color,
'height_attenuated': False 'radius': 0.4,
}) 'height_attenuated': False,
},
)
ba.timer(0.5, light.delete) ba.timer(0.5, light.delete)
ba.animate(light, 'intensity', { ba.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0})
0: 0,
0.1: 1.0,
0.5: 0
})
if player.actor: if player.actor:
player.actor.handlemessage( player.actor.handlemessage(
ba.StandMessage(new_pos, ba.StandMessage(new_pos, random.uniform(0, 360))
random.uniform(0, 360))) )
# Have teammates celebrate. # Have teammates celebrate.
for player in player_team.players: for player in player_team.players:
@ -254,5 +257,6 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]):
def _update_scoreboard(self) -> None: def _update_scoreboard(self) -> None:
for team in self.teams: for team in self.teams:
self._scoreboard.set_team_value(team, team.score, self._scoreboard.set_team_value(
self._score_to_win) team, team.score, self._score_to_win
)

View File

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

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