Restored NodeActor, tidying, minor gameplay fixes.

This commit is contained in:
Eric Froemling 2020-04-05 22:01:04 -07:00
parent 3e7ed8c0e3
commit bc600f602e
31 changed files with 379 additions and 274 deletions

View File

@ -479,6 +479,7 @@
<w>dxml</w>
<w>dynload</w>
<w>eachother</w>
<w>eaddrnotavail</w>
<w>easteregghunt</w>
<w>edcc</w>
<w>editcontroller</w>
@ -1132,6 +1133,7 @@
<w>nline</w>
<w>nlines</w>
<w>nntplib</w>
<w>nodeactor</w>
<w>nodepos</w>
<w>nodpi</w>
<w>nofiles</w>

View File

@ -35,6 +35,7 @@
"ba_data/python/ba/__pycache__/_modutils.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_music.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_netutils.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_nodeactor.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_playlist.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_powerup.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_profile.cpython-37.opt-1.pyc",
@ -84,6 +85,7 @@
"ba_data/python/ba/_modutils.py",
"ba_data/python/ba/_music.py",
"ba_data/python/ba/_netutils.py",
"ba_data/python/ba/_nodeactor.py",
"ba_data/python/ba/_playlist.py",
"ba_data/python/ba/_powerup.py",
"ba_data/python/ba/_profile.py",

View File

@ -207,6 +207,7 @@ SCRIPT_TARGETS_PY_1 = \
build/ba_data/python/ba/_account.py \
build/ba_data/python/ba/_music.py \
build/ba_data/python/ba/_lang.py \
build/ba_data/python/ba/_nodeactor.py \
build/ba_data/python/ba/_teamgame.py \
build/ba_data/python/ba/ui/__init__.py \
build/ba_data/python/bastd/mainmenu.py \
@ -444,6 +445,7 @@ SCRIPT_TARGETS_PYC_1 = \
build/ba_data/python/ba/__pycache__/_account.cpython-37.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_music.cpython-37.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_lang.cpython-37.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_nodeactor.cpython-37.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_teamgame.cpython-37.opt-1.pyc \
build/ba_data/python/ba/ui/__pycache__/__init__.cpython-37.opt-1.pyc \
build/ba_data/python/bastd/__pycache__/mainmenu.cpython-37.opt-1.pyc \
@ -953,6 +955,11 @@ build/ba_data/python/ba/__pycache__/_lang.cpython-37.opt-1.pyc: \
@echo Compiling script: $^
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
build/ba_data/python/ba/__pycache__/_nodeactor.cpython-37.opt-1.pyc: \
build/ba_data/python/ba/_nodeactor.py
@echo Compiling script: $^
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
build/ba_data/python/ba/__pycache__/_teamgame.cpython-37.opt-1.pyc: \
build/ba_data/python/ba/_teamgame.py
@echo Compiling script: $^

View File

@ -34,7 +34,7 @@ NOTE: This file was autogenerated by gendummymodule; do not edit by hand.
"""
# (hash we can use to see if this file is out of date)
# SOURCES_HASH=279775284016994471710131432123768789736
# SOURCES_HASH=92329394206923431348342003830010439152
# I'm sorry Pylint. I know this file saddens you. Be strong.
# pylint: disable=useless-suppression
@ -161,8 +161,8 @@ class Context:
sets the context as current on entry and resets it to the previous
value on exit.
# example: load a few textures into the UI context
# (for use in widgets, etc)
# Example: load a few textures into the UI context
# (for use in widgets, etc):
with ba.Context('ui'):
tex1 = ba.gettexture('foo_tex_1')
tex2 = ba.gettexture('foo_tex_2')
@ -667,7 +667,7 @@ class Node:
target attribute to any value or connecting another node attribute
to it.
# example: create a locator and attach a light to it
# Example: create a locator and attach a light to it:
light = ba.newnode('light')
loc = ba.newnode('locator', attrs={'position': (0,10,0)})
loc.connectattr('position', light, 'position')
@ -1044,7 +1044,7 @@ class Timer:
the 'timeformat' arg defaults to SECONDS but can also be MILLISECONDS
if you want to pass time as milliseconds.
# example: use a Timer object to print repeatedly for a few seconds:
# Example: use a Timer object to print repeatedly for a few seconds:
def say_it():
ba.screenmessage('BADGER!')
def stop_saying_it():
@ -1745,6 +1745,11 @@ def do_once() -> bool:
logs. The call functions by registering the filename and line where
The call is made from. Returns True if this location has not been
registered already, and False if it has.
# Example: this print will only fire for the first loop iteration:
for i in range(10):
if ba.do_once():
print('Hello once from loop!')
"""
return bool()

View File

@ -40,6 +40,7 @@ from _ba import (CollideModel, Context, ContextCall, Data, InputDevice,
charstr, textwidget, time, timer, open_url, widget)
from ba._activity import Activity
from ba._actor import Actor
from ba._nodeactor import NodeActor
from ba._app import App
from ba._coopgame import CoopGameActivity
from ba._coopsession import CoopSession

View File

@ -558,12 +558,12 @@ class Activity(DependencyComponent):
def create_player_node(self, player: ba.Player) -> ba.Node:
"""Create the 'player' node associated with the provided ba.Player."""
from ba import _actor
from ba._nodeactor import NodeActor
with _ba.Context(self):
node = _ba.newnode('player', attrs={'playerID': player.get_id()})
# FIXME: Should add a dedicated slot for this on ba.Player
# instead of cluttering up their gamedata dict.
player.gamedata['_playernode'] = _actor.Actor(node)
player.gamedata['_playernode'] = NodeActor(node)
return node
def begin(self, session: ba.Session) -> None:

View File

@ -37,24 +37,25 @@ T = TypeVar('T', bound='Actor')
class Actor:
"""High level logical entities in a game/activity.
category: Gameplay Classes
Category: Gameplay Classes
Actors act as controllers, combining some number of ba.Nodes,
ba.Textures, ba.Sounds, etc. into one cohesive unit.
ba.Textures, ba.Sounds, etc. into a high-level cohesive unit.
Some example actors include ba.Bomb, ba.Flag, and ba.Spaz.
Some example actors include Bomb, Flag, and Spaz classes in bastd.
One key feature of Actors is that they generally 'die'
(killing off or transitioning out their nodes) when the last Python
reference to them disappears, so you can use logic such as:
# create a flag Actor in our game activity
self.flag = ba.Flag(position=(0, 10, 0))
# Create a flag Actor in our game activity:
from bastd.actor.flag import Flag
self.flag = Flag(position=(0, 10, 0))
# later, destroy the flag..
# Later, destroy the flag.
# (provided nothing else is holding a reference to it)
# we could also just assign a new flag to this value.
# either way, the old flag disappears.
# We could also just assign a new flag to this value.
# Either way, the old flag disappears.
self.flag = None
This is in contrast to the behavior of the more low level ba.Nodes,
@ -69,30 +70,25 @@ class Actor:
takes a single arbitrary object as an argument. This provides a safe way
to communicate between ba.Actor, ba.Activity, ba.Session, and any other
class providing a handlemessage() method. The most universally handled
message type for actors is the ba.DieMessage.
message type for Actors is the ba.DieMessage.
# another way to kill the flag from the example above:
# we can safely call this on any type with a 'handlemessage' method
# (though its not guaranteed to always have a meaningful effect)
# in this case the Actor instance will still be around, but its exists()
# and is_alive() methods will both return False
# Another way to kill the flag from the example above:
# We can safely call this on any type with a 'handlemessage' method
# (though its not guaranteed to always have a meaningful effect).
# In this case the Actor instance will still be around, but its exists()
# and is_alive() methods will both return False.
self.flag.handlemessage(ba.DieMessage())
"""
def __init__(self, node: ba.Node = None):
"""Instantiates an Actor in the current ba.Activity.
def __init__(self) -> None:
"""Instantiates an Actor in the current ba.Activity."""
If 'node' is provided, it is stored as the 'node' attribute
and the default ba.Actor.handlemessage() and ba.Actor.exists()
implementations will apply to it. This allows the creation of
simple node-wrapping Actors without having to create a new subclass.
"""
# FIXME: Actor should not be require to have a 'node' attr.
self.node: Optional[ba.Node] = None
activity = _ba.getactivity()
self._activity = weakref.ref(activity)
activity.add_actor_weak_ref(self)
if node is not None:
self.node = node
def __del__(self) -> None:
try:
@ -112,14 +108,9 @@ class Actor:
The default implementation will handle ba.DieMessages by
calling self.node.delete() if self contains a 'node' attribute.
"""
from ba import _messages
from ba import _error
if isinstance(msg, _messages.DieMessage):
node = getattr(self, 'node', None)
if node is not None:
node.delete()
return None
return _error.UNHANDLED
from ba._error import UNHANDLED
del msg # Unused.
return UNHANDLED
def _handlemessage_sanity_check(self) -> None:
if self.is_expired():
@ -178,18 +169,13 @@ class Actor:
deleted without affecting the game; this call is often used
when pruning lists of Actors, such as with ba.Actor.autoretain()
The default implementation of this method returns 'node.exists()'
if the Actor has a 'node' attr; otherwise True.
The default implementation of this method always return True.
Note that the boolean operator for the Actor class calls this method,
so a simple "if myactor" test will conveniently do the right thing
even if myactor is set to None.
"""
# As a default, if we have a 'node' attr, return whether it exists.
node: ba.Node = getattr(self, 'node', None)
if node is not None:
return node.exists()
return True
def __bool__(self) -> bool:

View File

@ -638,7 +638,8 @@ class App:
activity = _ba.get_foreground_host_activity()
if (activity is not None and activity.allow_pausing
and not _ba.have_connected_clients()):
from ba import _gameutils, _actor, _lang
from ba import _gameutils, _lang
from ba._nodeactor import NodeActor
# FIXME: Shouldn't be touching scene stuff here;
# should just pass the request on to the host-session.
with _ba.Context(activity):
@ -648,7 +649,7 @@ class App:
globs.paused = True
# FIXME: This should not be an attr on Actor.
activity.paused_text = _actor.Actor(
activity.paused_text = NodeActor(
_ba.newnode(
'text',
attrs={

View File

@ -80,8 +80,8 @@ class CoopGameActivity(GameActivity):
def _show_standard_scores_to_beat_ui(self,
scores: List[Dict[str, Any]]) -> None:
from ba import _gameutils
from ba import _actor
from ba._gameutils import timestring, animate
from ba._nodeactor import NodeActor
from ba._enums import TimeFormat
display_type = self.get_score_type()
if scores is not None:
@ -92,16 +92,14 @@ class CoopGameActivity(GameActivity):
# Now make a display for the most recent challenge.
for score in scores:
if score['type'] == 'score_challenge':
tval = (
score['player'] + ': ' +
(_gameutils.timestring(
int(score['value']) * 10,
timeformat=TimeFormat.MILLISECONDS).evaluate()
if display_type == 'time' else str(score['value'])))
tval = (score['player'] + ': ' + timestring(
int(score['value']) * 10,
timeformat=TimeFormat.MILLISECONDS).evaluate()
if display_type == 'time' else str(score['value']))
hattach = 'center' if display_type == 'time' else 'left'
halign = 'center' if display_type == 'time' else 'left'
pos = (20, -70) if display_type == 'time' else (20, -130)
txt = _actor.Actor(
txt = NodeActor(
_ba.newnode('text',
attrs={
'v_attach': 'top',
@ -115,11 +113,7 @@ class CoopGameActivity(GameActivity):
'text': tval
})).autoretain()
assert txt.node is not None
_gameutils.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
# FIXME: this is now redundant with activityutils.get_score_info();
@ -279,8 +273,8 @@ class CoopGameActivity(GameActivity):
should_beep = True
break
if should_beep and self._life_warning_beep is None:
from ba import _actor
self._life_warning_beep = _actor.Actor(
from ba._nodeactor import NodeActor
self._life_warning_beep = NodeActor(
_ba.newnode('sound',
attrs={
'sound': self._warn_beeps_sound,

View File

@ -703,7 +703,7 @@ class GameActivity(Activity):
# pylint: disable=too-many-locals
from ba._freeforallsession import FreeForAllSession
from ba._gameutils import animate
from ba._actor import Actor
from ba._nodeactor import NodeActor
sb_name = self.get_instance_scoreboard_display_string()
# the description can be either a string or a sequence with args
@ -731,7 +731,7 @@ class GameActivity(Activity):
yval -= 16
sbpos = ((15, yval) if isinstance(self.session, FreeForAllSession) else
(15, yval))
self._game_scoreboard_name_text = Actor(
self._game_scoreboard_name_text = NodeActor(
_ba.newnode("text",
attrs={
'text': sb_name,
@ -756,7 +756,7 @@ class GameActivity(Activity):
descpos = (((17, -44 +
10) if isinstance(self.session, FreeForAllSession) else
(17, -44 + 10)))
self._game_scoreboard_description_text = Actor(
self._game_scoreboard_description_text = NodeActor(
_ba.newnode(
"text",
attrs={
@ -1163,13 +1163,13 @@ class GameActivity(Activity):
"""
from ba._gameutils import sharedobj
from ba._general import WeakCall
from ba._actor import Actor
from ba._nodeactor import NodeActor
if duration <= 0.0:
return
self._standard_time_limit_time = int(duration)
self._standard_time_limit_timer = _ba.Timer(
1.0, WeakCall(self._standard_time_limit_tick), repeat=True)
self._standard_time_limit_text = Actor(
self._standard_time_limit_text = NodeActor(
_ba.newnode('text',
attrs={
'v_attach': 'top',
@ -1180,7 +1180,7 @@ class GameActivity(Activity):
'flatness': 1.0,
'scale': 0.9
}))
self._standard_time_limit_text_input = Actor(
self._standard_time_limit_text_input = NodeActor(
_ba.newnode('timedisplay',
attrs={
'time2': duration * 1000,
@ -1237,7 +1237,7 @@ class GameActivity(Activity):
If the time-limit expires, end_game() will be called.
"""
from ba._general import WeakCall
from ba._actor import Actor
from ba._nodeactor import NodeActor
from ba._enums import TimeType
if duration <= 0.0:
return
@ -1251,7 +1251,7 @@ class GameActivity(Activity):
WeakCall(self._tournament_time_limit_tick),
repeat=True,
timetype=TimeType.BASE)
self._tournament_time_limit_title_text = Actor(
self._tournament_time_limit_title_text = NodeActor(
_ba.newnode('text',
attrs={
'v_attach': 'bottom',
@ -1266,7 +1266,7 @@ class GameActivity(Activity):
'scale': 0.5,
'text': Lstr(resource='tournamentText')
}))
self._tournament_time_limit_text = Actor(
self._tournament_time_limit_text = NodeActor(
_ba.newnode('text',
attrs={
'v_attach': 'bottom',
@ -1280,7 +1280,7 @@ class GameActivity(Activity):
'flatness': 1.0,
'scale': 0.9
}))
self._tournament_time_limit_text_input = Actor(
self._tournament_time_limit_text_input = NodeActor(
_ba.newnode('timedisplay',
attrs={
'timemin': 0,

View File

@ -441,7 +441,7 @@ def cameraflash(duration: float = 999.0) -> None:
"""
# pylint: disable=too-many-locals
import random
from ba._actor import Actor
from ba._nodeactor import NodeActor
x_spread = 10
y_spread = 5
positions = [[-x_spread, -y_spread], [0, -y_spread], [0, y_spread],
@ -455,7 +455,7 @@ def cameraflash(duration: float = 999.0) -> None:
# noinspection PyTypeHints
activity.camera_flash_data = [] # type: ignore
for i in range(6):
light = Actor(
light = NodeActor(
_ba.newnode("light",
attrs={
'position': (positions[i][0], 0, positions[i][1]),

View File

@ -159,7 +159,7 @@ class _WeakCall:
Instantiate a WeakCall; pass a callable as the first
arg, followed by any number of arguments or keywords.
# example: wrap a method call with some positional and
# Example: wrap a method call with some positional and
# keyword args:
myweakcall = ba.WeakCall(myobj.dostuff, argval1, namedarg=argval2)
@ -211,7 +211,7 @@ class _Call:
Instantiate a Call; pass a callable as the first
arg, followed by any number of arguments or keywords.
# Example: wrap a method call with 1 positional and 1 keyword arg.
# Example: wrap a method call with 1 positional and 1 keyword arg:
mycall = ba.Call(myobj.dostuff, argval1, namedarg=argval2)
# Now we have a single callable to run that whole mess.

View File

@ -42,8 +42,8 @@ class JoinInfo:
# pylint: disable=too-many-locals
from ba import _input
from ba._lang import Lstr
from ba import _actor
from ba import _general
from ba._nodeactor import NodeActor
from ba._general import WeakCall
from ba._enums import SpecialChar
can_switch_teams = (len(lobby.teams) > 1)
self._state = 0
@ -79,7 +79,7 @@ class JoinInfo:
join_str = Lstr(resource='pressAnyButtonToJoinText')
flatness = 1.0 if _ba.app.vr_mode else 0.0
self._text = _actor.Actor(
self._text = NodeActor(
_ba.newnode('text',
attrs={
'position': (0, -40),
@ -109,9 +109,7 @@ class JoinInfo:
' ' + _ba.charstr(SpecialChar.RIGHT_ARROW))])
] if can_switch_teams else []) + [msg1] + [msg3] + [join_str])
self._timer = _ba.Timer(4.0,
_general.WeakCall(self._update),
repeat=True)
self._timer = _ba.Timer(4.0, WeakCall(self._update), repeat=True)
def _update(self) -> None:
assert self._text.node
@ -393,7 +391,7 @@ class Chooser:
def reload_profiles(self) -> None:
"""Reload all player profiles."""
from ba import _general
from ba._general import json_prep
app = _ba.app
# Re-construct our profile index and other stuff since the profile
@ -422,7 +420,7 @@ class Chooser:
# (non-unicode/non-json) version.
# Make sure they conform to our standards
# (unicode strings, no tuples, etc)
self.profiles = _general.json_prep(self.profiles)
self.profiles = json_prep(self.profiles)
# Filter out any characters we're unaware of.
for profile in list(self.profiles.items()):
@ -551,7 +549,7 @@ class Chooser:
def _set_ready(self, ready: bool) -> None:
# pylint: disable=cyclic-import
from bastd.ui.profile import browser as pbrowser
from ba import _general
from ba._general import Call
profilename = self._profilenames[self._profileindex]
# Handle '_edit' as a special case.
@ -566,26 +564,23 @@ class Chooser:
if not ready:
self._player.assign_input_call(
'leftPress',
_general.Call(self.handlemessage, ChangeMessage('team', -1)))
'leftPress', Call(self.handlemessage,
ChangeMessage('team', -1)))
self._player.assign_input_call(
'rightPress',
_general.Call(self.handlemessage, ChangeMessage('team', 1)))
'rightPress', Call(self.handlemessage,
ChangeMessage('team', 1)))
self._player.assign_input_call(
'bombPress',
_general.Call(self.handlemessage,
ChangeMessage('character', 1)))
Call(self.handlemessage, ChangeMessage('character', 1)))
self._player.assign_input_call(
'upPress',
_general.Call(self.handlemessage,
ChangeMessage('profileindex', -1)))
Call(self.handlemessage, ChangeMessage('profileindex', -1)))
self._player.assign_input_call(
'downPress',
_general.Call(self.handlemessage,
ChangeMessage('profileindex', 1)))
Call(self.handlemessage, ChangeMessage('profileindex', 1)))
self._player.assign_input_call(
('jumpPress', 'pickUpPress', 'punchPress'),
_general.Call(self.handlemessage, ChangeMessage('ready', 1)))
Call(self.handlemessage, ChangeMessage('ready', 1)))
self._ready = False
self._update_text()
self._player.set_name('untitled', real=False)
@ -595,7 +590,7 @@ class Chooser:
'jumpPress', 'bombPress', 'pickUpPress'), self._do_nothing)
self._player.assign_input_call(
('jumpPress', 'bombPress', 'pickUpPress', 'punchPress'),
_general.Call(self.handlemessage, ChangeMessage('ready', 0)))
Call(self.handlemessage, ChangeMessage('ready', 0)))
# Store the last profile picked by this input for reuse.
input_device = self._player.get_input_device()

View File

@ -0,0 +1,53 @@
# Copyright (c) 2011-2020 Eric Froemling
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Defines NodeActor class."""
from __future__ import annotations
from typing import TYPE_CHECKING
from ba._messages import DieMessage
from ba._actor import Actor
if TYPE_CHECKING:
import ba
from typing import Any
class NodeActor(Actor):
"""A simple ba.Actor type that wraps a single ba.Node.
Category: Gameplay Classes
This Actor will delete its Node when told to die, and it's
exists() call will return whether the Node still exists or not.
"""
def __init__(self, node: ba.Node):
super().__init__()
self.node = node
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, DieMessage):
if self.node:
self.node.delete()
return None
return super().handlemessage(msg)

View File

@ -65,7 +65,7 @@ class TeamBaseSession(Session):
team_names = None
team_colors = None
print('FIXME: TEAM BASE SESSION WOULD CALC DEPS.')
# print('FIXME: TEAM BASE SESSION WOULD CALC DEPS.')
depsets: Sequence[ba.DependencySet] = []
super().__init__(depsets,
team_names=team_names,

View File

@ -867,7 +867,7 @@ class CoopScoreScreen(ba.Activity):
ba.timer(5.0, ba.WeakCall(self._show_tips))
def _play_drumroll(self) -> None:
ba.Actor(
ba.NodeActor(
ba.newnode('sound',
attrs={
'sound': self.drum_roll_sound,
@ -1314,7 +1314,7 @@ class CoopScoreScreen(ba.Activity):
star_tex = ba.gettexture('star')
star_x = 135 + offs_x
for _i in range(stars):
img = ba.Actor(
img = ba.NodeActor(
ba.newnode('image',
attrs={
'texture': star_tex,
@ -1329,7 +1329,7 @@ class CoopScoreScreen(ba.Activity):
ba.animate(img.node, 'opacity', {0.15: 0, 0.4: 1})
star_x += 60
for _i in range(3 - stars):
img = ba.Actor(
img = ba.NodeActor(
ba.newnode('image',
attrs={
'texture': star_tex,
@ -1355,7 +1355,7 @@ class CoopScoreScreen(ba.Activity):
transition_delay=1.0).autoretain()
stx = xval + 20
for _i2 in range(count):
img2 = ba.Actor(
img2 = ba.NodeActor(
ba.newnode('image',
attrs={
'texture': star_tex,

View File

@ -89,7 +89,7 @@ class RespawnIcon:
texture = icon['texture']
h_offs = -10
ipos = (-40 - h_offs if on_right else 40 + h_offs, -180 + offs)
self._image: Optional[ba.Actor] = ba.Actor(
self._image: Optional[ba.NodeActor] = ba.NodeActor(
ba.newnode('image',
attrs={
'texture': texture,
@ -108,7 +108,7 @@ class RespawnIcon:
ba.animate(self._image.node, 'opacity', {0.0: 0, 0.2: 0.7})
npos = (-40 - h_offs if on_right else 40 + h_offs, -205 + 49 + offs)
self._name: Optional[ba.Actor] = ba.Actor(
self._name: Optional[ba.NodeActor] = ba.NodeActor(
ba.newnode('text',
attrs={
'v_attach': 'top',
@ -128,7 +128,7 @@ class RespawnIcon:
ba.animate(self._name.node, 'scale', {0: 0, 0.1: 0.5})
tpos = (-60 - h_offs if on_right else 60 + h_offs, -192 + offs)
self._text: Optional[ba.Actor] = ba.Actor(
self._text: Optional[ba.NodeActor] = ba.NodeActor(
ba.newnode('text',
attrs={
'position': tpos,

View File

@ -72,7 +72,7 @@ class _Entry:
self._backing_color = [0.05 + c * 0.1 for c in safe_team_color]
opacity = (0.8 if vrmode else 0.8) if self._do_cover else 0.5
self._backing = ba.Actor(
self._backing = ba.NodeActor(
ba.newnode('image',
attrs={
'scale': (self._width, self._height),
@ -84,7 +84,7 @@ class _Entry:
}))
self._barcolor = safe_team_color
self._bar = ba.Actor(
self._bar = ba.NodeActor(
ba.newnode('image',
attrs={
'opacity': 0.7,
@ -112,7 +112,7 @@ class _Entry:
self._bar_position.connectattr('output', self._bar.node, 'position')
self._cover_color = safe_team_color
if self._do_cover:
self._cover = ba.Actor(
self._cover = ba.NodeActor(
ba.newnode('image',
attrs={
'scale':
@ -128,7 +128,7 @@ class _Entry:
clr = safe_team_color
maxwidth = 130.0 * (1.0 - scoreboard.score_split)
flatness = ((1.0 if vrmode else 0.5) if self._do_cover else 1.0)
self._score_text = ba.Actor(
self._score_text = ba.NodeActor(
ba.newnode('text',
attrs={
'h_attach': 'left',
@ -169,7 +169,7 @@ class _Entry:
team_name_label = ba.Lstr(value=team_name_label)
flatness = ((1.0 if vrmode else 0.5) if self._do_cover else 1.0)
self._name_text = ba.Actor(
self._name_text = ba.NodeActor(
ba.newnode('text',
attrs={
'h_attach': 'left',

View File

@ -58,10 +58,10 @@ class BombDiedMessage:
def get_factory() -> SpazFactory:
"""Return the shared ba.SpazFactory object, creating it if necessary."""
# pylint: disable=cyclic-import
from bastd.actor.spazfactory import SpazFactory
activity = ba.getactivity()
factory = getattr(activity, 'shared_spaz_factory', None)
if factory is None:
from bastd.actor.spazfactory import SpazFactory
# noinspection PyTypeHints
factory = activity.shared_spaz_factory = SpazFactory() # type: ignore
assert isinstance(factory, SpazFactory)
@ -693,11 +693,11 @@ class Spaz(ba.Actor):
# pylint: disable=too-many-branches
if __debug__ is True:
self._handlemessage_sanity_check()
assert self.node
if isinstance(msg, ba.PickedUpMessage):
self.node.handlemessage("hurt_sound")
self.node.handlemessage("picked_up")
if self.node:
self.node.handlemessage("hurt_sound")
self.node.handlemessage("picked_up")
# this counts as a hit
self._num_times_hit += 1
@ -713,7 +713,7 @@ class Spaz(ba.Actor):
ba.timer(0.001, ba.WeakCall(self._hit_self, msg.intensity))
elif isinstance(msg, ba.PowerupMessage):
if self._dead:
if self._dead or not self.node:
return True
if self.pick_up_powerup_callback is not None:
self.pick_up_powerup_callback(self)
@ -1069,14 +1069,15 @@ class Spaz(ba.Actor):
newdamage = max(damage - 200, self.hitpoints - 10)
damage = newdamage
self.node.handlemessage("flash")
# if we're holding something, drop it
# If we're holding something, drop it.
if damage > 0.0 and self.node.hold_node:
# self.node.hold_node = ba.Node(None)
self.node.hold_node = None
self.hitpoints -= damage
self.node.hurt = 1.0 - float(
self.hitpoints) / self.hitpoints_max
# if we're cursed, *any* damage blows us up
# If we're cursed, *any* damage blows us up.
if self._cursed and damage > 0:
ba.timer(
0.05, ba.WeakCall(self.curse_explode,
@ -1103,7 +1104,8 @@ class Spaz(ba.Actor):
self._dead = True
self.hitpoints = 0
if msg.immediate:
self.node.delete()
if self.node:
self.node.delete()
elif self.node:
self.node.hurt = 1.0
if self.play_big_death_sound and not wasdead:
@ -1117,11 +1119,15 @@ class Spaz(ba.Actor):
elif isinstance(msg, ba.StandMessage):
self._last_stand_pos = (msg.position[0], msg.position[1],
msg.position[2])
self.node.handlemessage("stand", msg.position[0], msg.position[1],
msg.position[2], msg.angle)
if self.node:
self.node.handlemessage("stand", msg.position[0],
msg.position[1], msg.position[2],
msg.angle)
elif isinstance(msg, CurseExplodeMessage):
self.curse_explode()
elif isinstance(msg, PunchHitMessage):
if not self.node:
return None
node = ba.get_collision_info("opposing_node")
# only allow one hit per node per punch
@ -1178,6 +1184,9 @@ class Spaz(ba.Actor):
ppos[2], punchdir[0], punchdir[1],
punchdir[2], mag)
elif isinstance(msg, PickupMessage):
if not self.node:
return None
opposing_node, opposing_body = ba.get_collision_info(
'opposing_node', 'opposing_body')

View File

@ -354,7 +354,7 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
team.gamedata['touch_return_timer_ticking'] = None
return # No need to return when its at home.
if team.gamedata['touch_return_timer_ticking'] is None:
team.gamedata['touch_return_timer_ticking'] = ba.Actor(
team.gamedata['touch_return_timer_ticking'] = ba.NodeActor(
ba.newnode('sound',
attrs={
'sound': self._ticking_sound,

View File

@ -293,7 +293,7 @@ class ChosenOneGame(ba.TeamGameActivity):
0.3 + c * 0.7
for c in ba.normalized_color(player.team.color)
]
light = player.gamedata['chosen_light'] = ba.Actor(
light = player.gamedata['chosen_light'] = ba.NodeActor(
ba.newnode('light',
attrs={
"intensity": 0.6,

View File

@ -449,7 +449,7 @@ class EliminationGame(ba.TeamGameActivity):
self.setup_standard_time_limit(self.settings['Time Limit'])
self.setup_standard_powerup_drops()
if self._solo_mode:
self._vs_text = ba.Actor(
self._vs_text = ba.NodeActor(
ba.newnode("text",
attrs={
'position': (0, 105),

View File

@ -157,7 +157,7 @@ class FootballTeamGame(ba.TeamGameActivity):
self._spawn_flag()
defs = self.map.defs
self._score_regions.append(
ba.Actor(
ba.NodeActor(
ba.newnode('region',
attrs={
'position': defs.boxes['goal1'][0:3],
@ -166,7 +166,7 @@ class FootballTeamGame(ba.TeamGameActivity):
'materials': (self.score_region_material, )
})))
self._score_regions.append(
ba.Actor(
ba.NodeActor(
ba.newnode('region',
attrs={
'position': defs.boxes['goal2'][0:3],
@ -279,7 +279,7 @@ class FootballTeamGame(ba.TeamGameActivity):
elif isinstance(msg, stdflag.FlagDeathMessage):
if not self.has_ended():
self._flag_respawn_timer = ba.Timer(3.0, self._spawn_flag)
self._flag_respawn_light = ba.Actor(
self._flag_respawn_light = ba.NodeActor(
ba.newnode('light',
attrs={
'position': self._flag_spawn_pos,
@ -406,7 +406,7 @@ class FootballCoopGame(ba.CoopGameActivity):
# Set up the two score regions.
defs = self.map.defs
self.score_regions.append(
ba.Actor(
ba.NodeActor(
ba.newnode('region',
attrs={
'position': defs.boxes['goal1'][0:3],
@ -415,7 +415,7 @@ class FootballCoopGame(ba.CoopGameActivity):
'materials': [self._score_region_material]
})))
self.score_regions.append(
ba.Actor(
ba.NodeActor(
ba.newnode('region',
attrs={
'position': defs.boxes['goal2'][0:3],
@ -516,7 +516,7 @@ class FootballCoopGame(ba.CoopGameActivity):
starttime_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS)
assert isinstance(starttime_ms, int)
self._starttime_ms = starttime_ms
self._time_text = ba.Actor(
self._time_text = ba.NodeActor(
ba.newnode('text',
attrs={
'v_attach': 'top',
@ -529,7 +529,7 @@ class FootballCoopGame(ba.CoopGameActivity):
'scale': 1.3,
'text': ''
}))
self._time_text_input = ba.Actor(
self._time_text_input = ba.NodeActor(
ba.newnode('timedisplay', attrs={'showsubseconds': True}))
ba.sharedobj('globals').connectattr('time', self._time_text_input.node,
'time2')
@ -844,7 +844,7 @@ class FootballCoopGame(ba.CoopGameActivity):
elif isinstance(msg, stdflag.FlagDeathMessage):
assert isinstance(msg.flag, FootballFlag)
msg.flag.respawn_timer = ba.Timer(3.0, self._spawn_flag)
self._flag_respawn_light = ba.Actor(
self._flag_respawn_light = ba.NodeActor(
ba.newnode('light',
attrs={
'position': self._flag_spawn_pos,

View File

@ -222,7 +222,7 @@ class HockeyGame(ba.TeamGameActivity):
defs = self.map.defs
self._score_regions = []
self._score_regions.append(
ba.Actor(
ba.NodeActor(
ba.newnode("region",
attrs={
'position': defs.boxes["goal1"][0:3],
@ -231,7 +231,7 @@ class HockeyGame(ba.TeamGameActivity):
'materials': [self._score_region_material]
})))
self._score_regions.append(
ba.Actor(
ba.NodeActor(
ba.newnode("region",
attrs={
'position': defs.boxes["goal2"][0:3],

View File

@ -170,7 +170,7 @@ class OnslaughtGame(ba.CoopGameActivity):
'sound': ba.getsound('ding')
}]
self._spawn_info_text = ba.Actor(
self._spawn_info_text = ba.NodeActor(
ba.newnode("text",
attrs={
'position': (15, -130),
@ -1049,7 +1049,7 @@ class OnslaughtGame(ba.CoopGameActivity):
('${A}', ba.Lstr(resource='timeBonusText')),
('${B}', str(self._time_bonus)),
])
self._time_bonus_text = ba.Actor(
self._time_bonus_text = ba.NodeActor(
ba.newnode('text',
attrs={
'v_attach': 'top',
@ -1075,7 +1075,7 @@ class OnslaughtGame(ba.CoopGameActivity):
('' if self._preset in ['endless', 'endless_tournament'] else
('/' + str(len(self._waves)))))
])
self._wave_text = ba.Actor(
self._wave_text = ba.NodeActor(
ba.newnode('text',
attrs={
'v_attach': 'top',

View File

@ -406,7 +406,7 @@ class RaceGame(ba.TeamGameActivity):
self._team_finish_pts = 100
# Throw a timer up on-screen.
self._time_text = ba.Actor(
self._time_text = ba.NodeActor(
ba.newnode('text',
attrs={
'v_attach': 'top',

View File

@ -134,7 +134,7 @@ class RunaroundGame(ba.CoopGameActivity):
super().on_transition_in()
self._scoreboard = Scoreboard(label=ba.Lstr(resource='scoreText'),
score_split=0.5)
self._score_region = ba.Actor(
self._score_region = ba.NodeActor(
ba.newnode(
'region',
attrs={
@ -365,7 +365,7 @@ class RunaroundGame(ba.CoopGameActivity):
l_offs = (-80 if interface_type == 'small' else
-40 if interface_type == 'medium' else 0)
self._lives_bg = ba.Actor(
self._lives_bg = ba.NodeActor(
ba.newnode('image',
attrs={
'texture': self._heart_tex,
@ -379,7 +379,7 @@ class RunaroundGame(ba.CoopGameActivity):
# FIXME; should not set things based on vr mode.
# (won't look right to non-vr connected clients, etc)
vrmode = ba.app.vr_mode
self._lives_text = ba.Actor(
self._lives_text = ba.NodeActor(
ba.newnode(
'text',
attrs={
@ -943,7 +943,7 @@ class RunaroundGame(ba.CoopGameActivity):
subs=[('${A}', ba.Lstr(resource='timeBonusText')),
('${B}', str(int(self._time_bonus * self._time_bonus_mult)))
])
self._time_bonus_text = ba.Actor(
self._time_bonus_text = ba.NodeActor(
ba.newnode('text',
attrs={
'v_attach': 'top',
@ -973,7 +973,7 @@ class RunaroundGame(ba.CoopGameActivity):
('' if self._preset in ['endless', 'endless_tournament'] else
('/' + str(len(self._waves)))))
])
self._wave_text = ba.Actor(
self._wave_text = ba.NodeActor(
ba.newnode('text',
attrs={
'v_attach': 'top',

View File

@ -67,7 +67,7 @@ class MainMenuActivity(ba.Activity):
# FIXME: Need a node attr for vr-specific-scale.
scale = (0.9 if
(app.interface_type == 'small' or vr_mode) else 0.7)
self.my_name = ba.Actor(
self.my_name = ba.NodeActor(
ba.newnode('text',
attrs={
'v_attach': 'bottom',
@ -86,7 +86,7 @@ class MainMenuActivity(ba.Activity):
# empty-ish screen.
tval = ba.Lstr(resource='hostIsNavigatingMenusText',
subs=[('${HOST}', _ba.get_account_display_string())])
self._host_is_navigating_text = ba.Actor(
self._host_is_navigating_text = ba.NodeActor(
ba.newnode('text',
attrs={
'text': tval,
@ -130,7 +130,7 @@ class MainMenuActivity(ba.Activity):
text = ba.Lstr(value='${V}', subs=[('${V}', app.version)])
scale = 0.9 if (interface_type == 'small' or vr_mode) else 0.7
color = (1, 1, 1, 1) if vr_mode else (0.5, 0.6, 0.5, 0.7)
self.version = ba.Actor(
self.version = ba.NodeActor(
ba.newnode(
'text',
attrs={
@ -153,7 +153,7 @@ class MainMenuActivity(ba.Activity):
self.beta_info = self.beta_info_2 = None
if app.test_build and not app.kiosk_mode:
pos = (230, 125) if app.kiosk_mode else (230, 35)
self.beta_info = ba.Actor(
self.beta_info = ba.NodeActor(
ba.newnode('text',
attrs={
'v_attach': 'center',
@ -191,7 +191,7 @@ class MainMenuActivity(ba.Activity):
gnode.vignette_outer = (0.45, 0.55, 0.54)
gnode.vignette_inner = (0.99, 0.98, 0.98)
self.bottom = ba.Actor(
self.bottom = ba.NodeActor(
ba.newnode('terrain',
attrs={
'model': bottom_model,
@ -200,7 +200,7 @@ class MainMenuActivity(ba.Activity):
'reflection_scale': [0.45],
'color_texture': color_texture
}))
self.vr_bottom_fill = ba.Actor(
self.vr_bottom_fill = ba.NodeActor(
ba.newnode('terrain',
attrs={
'model': vr_bottom_fill_model,
@ -208,7 +208,7 @@ class MainMenuActivity(ba.Activity):
'vr_only': True,
'color_texture': color_texture
}))
self.vr_top_fill = ba.Actor(
self.vr_top_fill = ba.NodeActor(
ba.newnode('terrain',
attrs={
'model': vr_top_fill_model,
@ -216,7 +216,7 @@ class MainMenuActivity(ba.Activity):
'lighting': False,
'color_texture': bgtex
}))
self.terrain = ba.Actor(
self.terrain = ba.NodeActor(
ba.newnode('terrain',
attrs={
'model': model,
@ -224,7 +224,7 @@ class MainMenuActivity(ba.Activity):
'reflection': 'soft',
'reflection_scale': [0.3]
}))
self.trees = ba.Actor(
self.trees = ba.NodeActor(
ba.newnode('terrain',
attrs={
'model': trees_model,
@ -233,7 +233,7 @@ class MainMenuActivity(ba.Activity):
'reflection_scale': [0.1],
'color_texture': trees_texture
}))
self.bgterrain = ba.Actor(
self.bgterrain = ba.NodeActor(
ba.newnode('terrain',
attrs={
'model': bgmodel,
@ -380,7 +380,7 @@ class MainMenuActivity(ba.Activity):
color2 = ((1, 1, 1, 1) if ba.app.vr_mode else
(0.7, 0.65, 0.75, 1.0))
shadow = (1.0 if ba.app.vr_mode else 0.4)
self._text = ba.Actor(
self._text = ba.NodeActor(
ba.newnode('text',
attrs={
'v_attach': 'top',
@ -652,7 +652,7 @@ class MainMenuActivity(ba.Activity):
vr_depth_offset: float = 0.0,
shadow: bool = False) -> None:
if shadow:
word_obj = ba.Actor(
word_obj = ba.NodeActor(
ba.newnode('text',
attrs={
'position': (x, y),
@ -669,7 +669,7 @@ class MainMenuActivity(ba.Activity):
}))
self._word_actors.append(word_obj)
else:
word_obj = ba.Actor(
word_obj = ba.NodeActor(
ba.newnode('text',
attrs={
'position': (x, y),
@ -776,7 +776,7 @@ class MainMenuActivity(ba.Activity):
mopaque = (None if custom_texture is not None else ba.getmodel('logo'))
mtrans = (None if custom_texture is not None else
ba.getmodel('logoTransparent'))
logo = ba.Actor(
logo = ba.NodeActor(
ba.newnode('image',
attrs={
'texture': ltex,

View File

@ -180,7 +180,7 @@ class GatherWindow(ba.Window):
self._scroll_width = self._width - scroll_buffer_h
self._scroll_height = self._height - 180.0
# not actually using a scroll widget anymore; just an image
# Not actually using a scroll widget anymore; just an image.
scroll_left = (self._width - self._scroll_width) * 0.5
scroll_bottom = self._height - self._scroll_height - 79 - 48
buffer_h = 10
@ -214,8 +214,8 @@ class GatherWindow(ba.Window):
or _ba.get_account_type() != 'Google Play'):
account.show_sign_in_prompt('Google Play')
else:
# if there's google play people connected to us, inform the user
# that they will get disconnected.. otherwise just go ahead..
# If there's google play people connected to us, inform the user
# that they will get disconnected. Otherwise just go ahead.
google_player_count = (_ba.get_google_play_party_client_count())
if google_player_count > 0:
confirm.ConfirmWindow(
@ -246,25 +246,25 @@ class GatherWindow(ba.Window):
return
self._current_tab = tab
# we wanna preserve our current tab between runs
# We wanna preserve our current tab between runs.
cfg = ba.app.config
cfg['Gather Tab'] = tab
cfg.commit()
# update tab colors based on which is selected
# Update tab colors based on which is selected.
tabs.update_tab_button_colors(self._tab_buttons, tab)
# (re)create scroll widget
# (Re)create scroll widget.
if self._tab_container:
self._tab_container.delete()
scroll_left = (self._width - self._scroll_width) * 0.5
scroll_bottom = self._height - self._scroll_height - 79 - 48
# a place where tabs can store data to get cleared when switching to
# a different tab
# A place where tabs can store data to get cleared when switching to
# a different tab.
self._tab_data = {}
# so we can still select root level widgets with direction buttons
# So we can still select root level widgets with direction buttons.
def _simple_message(tab2: str,
message: ba.Lstr,
string_height: float,
@ -331,7 +331,7 @@ class GatherWindow(ba.Window):
('${BUTTON}',
ba.charstr(ba.SpecialChar.TOP_BUTTON))])
# let's not talk about sharing in vr-mode; its tricky to fit more
# Let's not talk about sharing in vr-mode; its tricky to fit more
# than one head in a VR-headset ;-)
if not ba.app.vr_mode:
msg = ba.Lstr(
@ -472,8 +472,8 @@ class GatherWindow(ba.Window):
up_widget=self._tab_buttons[tab])
ba.widget(edit=self._internet_join_text, right_widget=txt)
# attempt to fetch our local address so we have it for
# error messages
# Attempt to fetch our local address so we have it for
# error messages.
self._internet_local_address = None
class AddrFetchThread(threading.Thread):
@ -501,9 +501,9 @@ class GatherWindow(ba.Window):
ba.pushcall(ba.Call(self._call, val),
from_other_thread=True)
except Exception:
# FIXME: Should filter out expected errors and
# report others here.
ba.print_exception()
# FIXME: Should screen out expected errors and
# report others here.
AddrFetchThread(ba.WeakCall(
self._internet_fetch_local_addr_cb)).start()
@ -518,8 +518,8 @@ class GatherWindow(ba.Window):
timetype=ba.TimeType.REAL)
}
# also update it immediately so we don't have to wait for the
# initial query..
# Also update it immediately so we don't have to wait for the
# initial query.
self._update_internet_tab()
elif tab == 'local_network':
@ -545,8 +545,8 @@ class GatherWindow(ba.Window):
ba.WeakCall(self.update),
timetype=ba.TimeType.REAL,
repeat=True)
# go ahead and run a few *almost* immediately so we don't
# have to wait a second
# Go ahead and run a few *almost* immediately so we don't
# have to wait a second.
self.update()
ba.timer(0.25,
ba.WeakCall(self.update),
@ -566,7 +566,8 @@ class GatherWindow(ba.Window):
t_scale = 1.6
for child in self._columnwidget.get_children():
child.delete()
# grab this now this since adding widgets will change it
# Grab this now this since adding widgets will change it.
last_selected_host = self._last_selected_host
hosts = _ba.host_scan_cycle()
for i, host in enumerate(hosts):
@ -843,7 +844,7 @@ class GatherWindow(ba.Window):
color=(1, 0, 0))
ba.playsound(ba.getsound('error'))
else:
# store for later
# Store for later.
cfg2 = ba.app.config
cfg2['Last Manual Party Connect Address'] = addr2
cfg2.commit()
@ -866,7 +867,6 @@ class GatherWindow(ba.Window):
tscl = 0.85
tspc = 25
# v -= 35
def _safe_set_text(txt3: ba.Widget,
val: Union[str, ba.Lstr],
success: bool = True) -> None:
@ -875,11 +875,10 @@ class GatherWindow(ba.Window):
text=val,
color=(0, 1, 0) if success else (1, 1, 0))
# this currently doesn't work from china since we go through a
# reverse proxy there
# EDIT - it should work now; our proxy server forwards along
# original IPs
# app = ba.app
# This currently doesn't work from china since we go through a
# reverse proxy there.
# UPDATE: it should work now; our proxy server forwards along
# original IPs.
do_internet_check = True
def do_it(v2: float, cnt2: Optional[ba.Widget]) -> None:
@ -940,6 +939,7 @@ class GatherWindow(ba.Window):
from_other_thread=True)
except Exception as exc:
err_str = str(exc)
# FIXME: Should look at exception types here,
# not strings.
if 'Network is unreachable' in err_str:
@ -1024,14 +1024,15 @@ class GatherWindow(ba.Window):
text='')
self._doing_access_check = False
self._access_check_count = 0 # cap our refreshes eventually..
self._access_check_count = 0 # Cap our refreshes eventually.
self._tab_data['access_check_timer'] = ba.Timer(
10.0,
ba.WeakCall(self._access_check_update, t_addr,
t_accessible, t_accessible_extra),
repeat=True,
timetype=ba.TimeType.REAL)
# kick initial off
# Kick initial off.
self._access_check_update(t_addr, t_accessible,
t_accessible_extra)
if check_button:
@ -1061,7 +1062,7 @@ class GatherWindow(ba.Window):
if playsound:
ba.playsound(ba.getsound('click01'))
# if we're switching in from elsewhere, reset our selection
# If we're switching in from elsewhere, reset our selection.
# (prevents selecting something way down the list if we switched away
# and came back)
if self._internet_tab != value:
@ -1077,7 +1078,7 @@ class GatherWindow(ba.Window):
edit=self._internet_host_text,
color=active_color if value == 'host' else inactive_color)
# clear anything in existence..
# Clear anything in existence.
for widget in [
self._internet_host_scrollwidget,
self._internet_host_name_text,
@ -1095,7 +1096,6 @@ class GatherWindow(ba.Window):
self._internet_join_status_text,
self._internet_host_dedicated_server_info_text
]:
# widget = getattr(self, attr, None)
if widget is not None:
widget.delete()
@ -1107,9 +1107,10 @@ class GatherWindow(ba.Window):
v -= 25
is_public_enabled = _ba.get_public_party_enabled()
if value == 'join':
# reset this so we do an immediate refresh query
# Reset this so we do an immediate refresh query.
self._internet_join_last_refresh_time = -99999.0
# reset our list of public parties
# Reset our list of public parties.
self._public_parties = {}
self._last_public_party_list_rebuild_time = 0
self._first_public_party_list_rebuild_time = None
@ -1125,8 +1126,7 @@ class GatherWindow(ba.Window):
shadow=0.0,
h_align='center',
v_align='center')
# noinspection PyUnreachableCode
if False: # pylint: disable=using-constant-test
if bool(False):
self._internet_join_party_language_label = ba.textwidget(
text=ba.Lstr(
resource='settingsWindowAdvanced.languageText'),
@ -1192,8 +1192,6 @@ class GatherWindow(ba.Window):
color=(0.6, 0.6, 0.6),
position=(c_width * 0.5, c_height * 0.5))
# t_scale = 1.6
if value == 'host':
v -= 30
party_name_text = ba.Lstr(
@ -1313,15 +1311,15 @@ class GatherWindow(ba.Window):
color=ba.app.infotextcolor,
position=(c_width * 0.5, v))
# if public sharing is already on,
# launch a status-check immediately
# If public sharing is already on,
# launch a status-check immediately.
if _ba.get_public_party_enabled():
self._do_internet_status_check()
# now add a lock icon overlay for if we don't have pro
# Now add a lock icon overlay for if we don't have pro.
icon = self._internet_lock_icon
if icon and self._internet_lock_icon:
self._internet_lock_icon.delete() # kill any existing
self._internet_lock_icon.delete() # Kill any existing.
self._internet_lock_icon = ba.imagewidget(
parent=self._tab_container,
position=(c_width * 0.5 - 60, c_height * 0.5 - 50),
@ -1354,11 +1352,11 @@ class GatherWindow(ba.Window):
def _on_public_party_query_result(
self, result: Optional[Dict[str, Any]]) -> None:
with ba.Context('ui'):
# any time we get any result at all, kill our loading status
# Any time we get any result at all, kill our loading status.
status_text = self._internet_join_status_text
if status_text:
# don't show results if not signed in (probably didn't get any
# anyway)
# Don't show results if not signed in
# (probably didn't get any anyway).
if _ba.get_account_state() != 'signed_in':
ba.textwidget(edit=status_text,
text=ba.Lstr(resource='notSignedInText'))
@ -1378,11 +1376,11 @@ class GatherWindow(ba.Window):
partyval['claimed'] = False
for party_in in parties_in:
# party is indexed by (ADDR)_(PORT)
# Party is indexed by (ADDR)_(PORT)
party_key = party_in['a'] + '_' + str(party_in['p'])
party = self._public_parties.get(party_key)
if party is None:
# if this party is new to us, init it..
# If this party is new to us, init it.
index = self._next_public_party_entry_index
self._next_public_party_entry_index = index + 1
party = self._public_parties[party_key] = {
@ -1395,7 +1393,8 @@ class GatherWindow(ba.Window):
'index':
index,
}
# now, new or not, update its values
# Now, new or not, update its values.
party['queue'] = party_in.get('q')
party['port'] = party_in.get('p')
party['name'] = party_in['n']
@ -1407,7 +1406,7 @@ class GatherWindow(ba.Window):
party['ping_interval'] = 0.001 * party_in['pi']
party['stats_addr'] = party_in['sa']
# prune unclaimed party entries
# Prune unclaimed party entries.
self._public_parties = {
key: val
for key, val in list(self._public_parties.items())
@ -1423,8 +1422,9 @@ class GatherWindow(ba.Window):
cur_time = ba.time(ba.TimeType.REAL)
if self._first_public_party_list_rebuild_time is None:
self._first_public_party_list_rebuild_time = cur_time
# update faster for the first few seconds;
# then ease off to keep the list from jumping around
# Update faster for the first few seconds;
# then ease off to keep the list from jumping around.
since_first = cur_time - self._first_public_party_list_rebuild_time
wait_time = (1.0 if since_first < 2.0 else
2.5 if since_first < 10.0 else 5.0)
@ -1433,23 +1433,23 @@ class GatherWindow(ba.Window):
return
self._last_public_party_list_rebuild_time = cur_time
# first off, check for the existence of our column widget;
# if we don't have this, we're done
# First off, check for the existence of our column widget;
# if we don't have this, we're done.
columnwidget = self._internet_host_columnwidget
if not columnwidget:
return
with ba.Context('ui'):
# now kill and recreate all widgets
# Now kill and recreate all widgets.
for widget in columnwidget.get_children():
widget.delete()
# sort - show queue-enabled ones first and sort by lowest ping
# Sort - show queue-enabled ones first and sort by lowest ping.
ordered_parties = sorted(
list(self._public_parties.values()),
key=lambda p: (
p['queue'] is None, # show non-queued last
p['queue'] is None, # Show non-queued last.
p['ping'] if p['ping'] is not None else 999999,
p['index'],
p))
@ -1457,22 +1457,19 @@ class GatherWindow(ba.Window):
first = True
sub_scroll_width = 830
# rval = random.randrange(4, 10)
# print 'doing', rval
# ordered_parties = ordered_parties[:rval]
lineheight = 42
sub_scroll_height = lineheight * len(ordered_parties) + 50
ba.containerwidget(edit=columnwidget,
size=(sub_scroll_width, sub_scroll_height))
# ew; this rebuilding generates deferred selection callbacks
# so we need to generated deferred ignore notices for ourself
# Ew; this rebuilding generates deferred selection callbacks
# so we need to generated deferred ignore notices for ourself.
def refresh_on() -> None:
self._refreshing_public_party_list = True
ba.pushcall(refresh_on)
# janky - allow escaping if there's nothing in us
# Janky - allow escaping if there's nothing in us.
ba.containerwidget(edit=self._internet_host_scrollwidget,
claims_up_down=(len(ordered_parties) > 0))
@ -1504,8 +1501,7 @@ class GatherWindow(ba.Window):
if existing_selection == (party['address'], 'name'):
ba.containerwidget(edit=columnwidget,
selected_child=party['name_widget'])
# noinspection PyUnreachableCode
if False: # pylint: disable=using-constant-test
if bool(False):
party['language_widget'] = ba.textwidget(
text=ba.Lstr(translate=('languages',
party['language'])),
@ -1621,20 +1617,20 @@ class GatherWindow(ba.Window):
def _update_internet_tab(self) -> None:
# pylint: disable=too-many-statements
# special case - if a party-queue window is up, don't do any of this
# (keeps things smoother)
# Special case: if a party-queue window is up, don't do any of this
# (keeps things smoother).
if ba.app.have_party_queue_window:
return
# if we've got a party-name text widget, keep its value plugged
# into our public host name...
# If we've got a party-name text widget, keep its value plugged
# into our public host name.
text = self._internet_host_name_text
if text:
name = cast(str,
ba.textwidget(query=self._internet_host_name_text))
_ba.set_public_party_name(name)
# show/hide the lock icon depending on if we've got pro
# Show/hide the lock icon depending on if we've got pro.
icon = self._internet_lock_icon
if icon:
if self._is_internet_locked():
@ -1657,17 +1653,17 @@ class GatherWindow(ba.Window):
callback=ba.WeakCall(self._on_public_party_query_result))
_ba.run_transactions()
# go through our existing public party entries firing off pings
# for any that have timed out
# Go through our existing public party entries firing off pings
# for any that have timed out.
for party in list(self._public_parties.values()):
if (party['next_ping_time'] <= now
and ba.app.ping_thread_count < 15):
# make sure to fully catch up and not to multi-ping if
# we're way behind somehow..
# Make sure to fully catch up and not to multi-ping if
# we're way behind somehow.
while party['next_ping_time'] <= now:
# crank the interval up for high-latency parties to
# save us some work
# Crank the interval up for high-latency parties to
# save us some work.
mult = 1
if party['ping'] is not None:
mult = (10 if party['ping'] > 300 else
@ -1682,8 +1678,6 @@ class GatherWindow(ba.Window):
call: Callable[[str, int, Optional[int]],
Optional[int]]):
super().__init__()
# need utf8 here to avoid an error on our minimum
# bundled python
self._address = address
self._port = port
self._call = call
@ -1701,11 +1695,9 @@ class GatherWindow(ba.Window):
accessible = False
starttime = time.time()
# send a simple ping and wait for a response;
# if we get it, they're accessible...
# send a few pings and wait a second for
# a response
# Send a few pings and wait a second for
# a response.
sock.settimeout(1)
for _i in range(3):
sock.send(b'\x0b')
@ -1728,12 +1720,25 @@ class GatherWindow(ba.Window):
from_other_thread=True)
except OSError as exc:
import errno
# ignore harmless errors
if exc.errno != errno.EHOSTUNREACH:
ba.print_exception('error on ping',
# Ignore harmless errors.
if exc.errno == errno.EHOSTUNREACH:
pass
elif exc.errno == errno.EADDRNOTAVAIL:
if self._port == 0:
# This has happened. Ignore.
pass
elif ba.do_once():
print(
f'Got EADDRNOTAVAIL on gather ping'
f' for addr {self._address}'
f' port {self._port}.')
else:
ba.print_exception('Error on gather ping.',
once=True)
except Exception:
ba.print_exception('error on ping', once=True)
ba.print_exception('Error on gather ping',
once=True)
ba.app.ping_thread_count -= 1
PingThread(party['address'], party['port'],
@ -1741,8 +1746,8 @@ class GatherWindow(ba.Window):
def _ping_callback(self, address: str, port: Optional[int],
result: Optional[int]) -> None:
# Look for a widget corresponding to this target; if we find one,
# update our list.
# Look for a widget corresponding to this target.
# If we find one, update our list.
party = self._public_parties.get(address + '_' + str(port))
if party is not None:
# We now smooth ping a bit to reduce jumping around in the list
@ -1755,9 +1760,11 @@ class GatherWindow(ba.Window):
(1.0 - smoothing) * result)
else:
party['ping'] = result
if 'ping_widget' not in party:
pass # This can happen if we switch away and then back to the
# This can happen if we switch away and then back to the
# client tab while pings are in flight.
if 'ping_widget' not in party:
pass
elif party['ping_widget']:
self._rebuild_public_party_list()
@ -1946,7 +1953,7 @@ class GatherWindow(ba.Window):
ba.app.window_states[self.__class__.__name__] = {
'sel_name': sel_name,
'tab': self._current_tab,
'internetTab': self._internet_tab
'internet_tab': self._internet_tab
}
except Exception:
ba.print_exception('error saving state for', self.__class__)
@ -1955,7 +1962,7 @@ class GatherWindow(ba.Window):
try:
winstate = ba.app.window_states.get(self.__class__.__name__, {})
sel_name = winstate.get('sel_name', None)
self._internet_tab = winstate.get('internetTab', 'join')
self._internet_tab = winstate.get('internet_tab', 'join')
current_tab = ba.app.config.get('Gather Tab', None)
if current_tab is None or current_tab not in self._tab_buttons:
current_tab = 'about'

View File

@ -75,7 +75,7 @@ class CallbackSet(Generic[CT]):
# passed.
# To use this, simply assign your call type to this Call for type checking:
# example:
# Example:
# class _MyCallWrapper:
# <runtime class defined here>
# if TYPE_CHECKING:

View File

@ -17,6 +17,7 @@
<li><a href="#class_ba_Actor">ba.Actor</a></li>
<ul>
<li><a href="#class_ba_Map">ba.Map</a></li>
<li><a href="#class_ba_NodeActor">ba.NodeActor</a></li>
</ul>
<li><a href="#class_ba_Chooser">ba.Chooser</a></li>
<li><a href="#class_ba_InputDevice">ba.InputDevice</a></li>
@ -537,21 +538,22 @@ is a convenient way to access this same functionality.</p>
<p>Category: <a href="#class_category_Gameplay_Classes">Gameplay Classes</a></p>
<p> Actors act as controllers, combining some number of <a href="#class_ba_Node">ba.Nodes</a>,
<a href="#class_ba_Texture">ba.Textures</a>, <a href="#class_ba_Sound">ba.Sounds</a>, etc. into one cohesive unit.</p>
<a href="#class_ba_Texture">ba.Textures</a>, <a href="#class_ba_Sound">ba.Sounds</a>, etc. into a high-level cohesive unit.</p>
<p> Some example actors include ba.Bomb, ba.Flag, and ba.Spaz.</p>
<p> Some example actors include Bomb, Flag, and Spaz classes in bastd.</p>
<p> One key feature of Actors is that they generally 'die'
(killing off or transitioning out their nodes) when the last Python
reference to them disappears, so you can use logic such as:</p>
<pre><span><em><small> # create a flag Actor in our game activity</small></em></span>
self.flag = ba.Flag(position=(0, 10, 0))</pre>
<pre><span><em><small> # Create a flag Actor in our game activity:</small></em></span>
from bastd.actor.flag import Flag
self.flag = Flag(position=(0, 10, 0))</pre>
<pre><span><em><small> # later, destroy the flag..</small></em></span>
<pre><span><em><small> # Later, destroy the flag.</small></em></span>
<span><em><small> # (provided nothing else is holding a reference to it)</small></em></span>
<span><em><small> # we could also just assign a new flag to this value.</small></em></span>
<span><em><small> # either way, the old flag disappears.</small></em></span>
<span><em><small> # We could also just assign a new flag to this value.</small></em></span>
<span><em><small> # Either way, the old flag disappears.</small></em></span>
self.flag = None</pre>
<p> This is in contrast to the behavior of the more low level <a href="#class_ba_Node">ba.Nodes</a>,
@ -566,13 +568,13 @@ is a convenient way to access this same functionality.</p>
takes a single arbitrary object as an argument. This provides a safe way
to communicate between <a href="#class_ba_Actor">ba.Actor</a>, <a href="#class_ba_Activity">ba.Activity</a>, <a href="#class_ba_Session">ba.Session</a>, and any other
class providing a handlemessage() method. The most universally handled
message type for actors is the <a href="#class_ba_DieMessage">ba.DieMessage</a>.</p>
message type for Actors is the <a href="#class_ba_DieMessage">ba.DieMessage</a>.</p>
<pre><span><em><small> # another way to kill the flag from the example above:</small></em></span>
<span><em><small> # we can safely call this on any type with a 'handlemessage' method</small></em></span>
<span><em><small> # (though its not guaranteed to always have a meaningful effect)</small></em></span>
<span><em><small> # in this case the Actor instance will still be around, but its exists()</small></em></span>
<span><em><small> # and is_alive() methods will both return False</small></em></span>
<pre><span><em><small> # Another way to kill the flag from the example above:</small></em></span>
<span><em><small> # We can safely call this on any type with a 'handlemessage' method</small></em></span>
<span><em><small> # (though its not guaranteed to always have a meaningful effect).</small></em></span>
<span><em><small> # In this case the Actor instance will still be around, but its exists()</small></em></span>
<span><em><small> # and is_alive() methods will both return False.</small></em></span>
self.flag.handlemessage(<a href="#class_ba_DieMessage">ba.DieMessage</a>())
</pre>
@ -590,15 +592,10 @@ is a convenient way to access this same functionality.</p>
<h5><a href="#method_ba_Actor____init__">&lt;constructor&gt;</a>, <a href="#method_ba_Actor__autoretain">autoretain()</a>, <a href="#method_ba_Actor__exists">exists()</a>, <a href="#method_ba_Actor__getactivity">getactivity()</a>, <a href="#method_ba_Actor__handlemessage">handlemessage()</a>, <a href="#method_ba_Actor__is_alive">is_alive()</a>, <a href="#method_ba_Actor__is_expired">is_expired()</a>, <a href="#method_ba_Actor__on_expire">on_expire()</a></h5>
<dl>
<dt><h4><a name="method_ba_Actor____init__">&lt;constructor&gt;</a></dt></h4><dd>
<p><span>ba.Actor(node: <a href="#class_ba_Node">ba.Node</a> = None)</span></p>
<p><span>ba.Actor()</span></p>
<p>Instantiates an Actor in the current <a href="#class_ba_Activity">ba.Activity</a>.</p>
<p>If 'node' is provided, it is stored as the 'node' attribute
and the default <a href="#method_ba_Actor__handlemessage">ba.Actor.handlemessage</a>() and <a href="#method_ba_Actor__exists">ba.Actor.exists</a>()
implementations will apply to it. This allows the creation of
simple node-wrapping Actors without having to create a new subclass.</p>
</dd>
<dt><h4><a name="method_ba_Actor__autoretain">autoretain()</a></dt></h4><dd>
<p><span>autoretain(self: T) -&gt; T</span></p>
@ -627,8 +624,7 @@ their corpse is visible; this is about presence, not being 'alive'
deleted without affecting the game; this call is often used
when pruning lists of Actors, such as with <a href="#method_ba_Actor__autoretain">ba.Actor.autoretain</a>()</p>
<p>The default implementation of this method returns 'node.exists()'
if the Actor has a 'node' attr; otherwise True.</p>
<p>The default implementation of this method always return True.</p>
<p>Note that the boolean operator for the Actor class calls this method,
so a simple "if myactor" test will conveniently do the right thing
@ -1106,7 +1102,7 @@ when done.</p>
<p>Instantiate a Call; pass a callable as the first
arg, followed by any number of arguments or keywords.</p>
<pre><span><em><small># Example: wrap a method call with 1 positional and 1 keyword arg.</small></em></span>
<pre><span><em><small># Example: wrap a method call with 1 positional and 1 keyword arg:</small></em></span>
mycall = ba.Call(myobj.dostuff, argval1, namedarg=argval2)</pre>
<pre><span><em><small># Now we have a single callable to run that whole mess.</small></em></span>
@ -1345,8 +1341,8 @@ Usage:</strong></p>
sets the context as current on entry and resets it to the previous
value on exit.</p>
<pre><span><em><small># example: load a few textures into the UI context</small></em></span>
<span><em><small># (for use in widgets, etc)</small></em></span>
<pre><span><em><small># Example: load a few textures into the UI context</small></em></span>
<span><em><small># (for use in widgets, etc):</small></em></span>
with <a href="#class_ba_Context">ba.Context</a>('ui'):
tex1 = <a href="#function_ba_gettexture">ba.gettexture</a>('foo_tex_1')
tex2 = <a href="#function_ba_gettexture">ba.gettexture</a>('foo_tex_2')</pre>
@ -3250,7 +3246,7 @@ the two nodes exist. The connection can be severed by setting the
target attribute to any value or connecting another node attribute
to it.</p>
<pre><span><em><small># example: create a locator and attach a light to it</small></em></span>
<pre><span><em><small># Example: create a locator and attach a light to it:</small></em></span>
light = <a href="#function_ba_newnode">ba.newnode</a>('light')
loc = <a href="#function_ba_newnode">ba.newnode</a>('locator', attrs={'position': (0,10,0)})
loc.connectattr('position', light, 'position')</pre>
@ -3311,6 +3307,48 @@ Node-messages communicate directly with the low-level node layer
and are delivered simultaneously on all game clients,
acting as an alternative to setting node attributes.</p>
</dd>
</dl>
<hr>
<h2><strong><a name="class_ba_NodeActor">ba.NodeActor</a></strong></h3>
<p>inherits from: <a href="#class_ba_Actor">ba.Actor</a></p>
<p>A simple <a href="#class_ba_Actor">ba.Actor</a> type that wraps a single <a href="#class_ba_Node">ba.Node</a>.</p>
<p>Category: <a href="#class_category_Gameplay_Classes">Gameplay Classes</a></p>
<p> This Actor will delete its Node when told to die, and it's
exists() call will return whether the Node still exists or not.
</p>
<h3>Attributes:</h3>
<dl>
<dt><h4><a name="attr_ba_NodeActor__activity">activity</a></h4></dt><dd>
<p><span><a href="#class_ba_Activity">ba.Activity</a></span></p>
<p>The Activity this Actor was created in.</p>
<p> Raises a <a href="#class_ba_ActivityNotFoundError">ba.ActivityNotFoundError</a> if the Activity no longer exists.</p>
</dd>
</dl>
<h3>Methods Inherited:</h3>
<h5><a href="#method_ba_Actor__autoretain">autoretain()</a>, <a href="#method_ba_Actor__exists">exists()</a>, <a href="#method_ba_Actor__getactivity">getactivity()</a>, <a href="#method_ba_Actor__is_alive">is_alive()</a>, <a href="#method_ba_Actor__is_expired">is_expired()</a>, <a href="#method_ba_Actor__on_expire">on_expire()</a></h5>
<h3>Methods Defined or Overridden:</h3>
<h5><a href="#method_ba_NodeActor____init__">&lt;constructor&gt;</a>, <a href="#method_ba_NodeActor__handlemessage">handlemessage()</a></h5>
<dl>
<dt><h4><a name="method_ba_NodeActor____init__">&lt;constructor&gt;</a></dt></h4><dd>
<p><span>ba.NodeActor(node: <a href="#class_ba_Node">ba.Node</a>)</span></p>
<p>Instantiates an Actor in the current <a href="#class_ba_Activity">ba.Activity</a>.</p>
</dd>
<dt><h4><a name="method_ba_NodeActor__handlemessage">handlemessage()</a></dt></h4><dd>
<p><span>handlemessage(self, msg: Any) -&gt; Any</span></p>
<p>General message handling; can be passed any <a href="#class_category_Message_Classes">message object</a>.</p>
<p>The default implementation will handle <a href="#class_ba_DieMessage">ba.DieMessages</a> by
calling self.node.delete() if self contains a 'node' attribute.</p>
</dd>
</dl>
<hr>
@ -4652,7 +4690,7 @@ Real time timers are currently only available in the UI context.</p>
<p>the 'timeformat' arg defaults to SECONDS but can also be MILLISECONDS
if you want to pass time as milliseconds.</p>
<pre><span><em><small># example: use a Timer object to print repeatedly for a few seconds:</small></em></span>
<pre><span><em><small># Example: use a Timer object to print repeatedly for a few seconds:</small></em></span>
def say_it():
<a href="#function_ba_screenmessage">ba.screenmessage</a>('BADGER!')
def stop_saying_it():
@ -4817,7 +4855,7 @@ self.t = <a href="#class_ba_Timer">ba.Timer</a>(0.3, say_it, repeat=True)
<p>Instantiate a WeakCall; pass a callable as the first
arg, followed by any number of arguments or keywords.</p>
<pre><span><em><small># example: wrap a method call with some positional and</small></em></span>
<pre><span><em><small># Example: wrap a method call with some positional and</small></em></span>
<span><em><small># keyword args:</small></em></span>
myweakcall = ba.WeakCall(myobj.dostuff, argval1, namedarg=argval2)</pre>
@ -5149,6 +5187,11 @@ logs. The call functions by registering the filename and line where
The call is made from. Returns True if this location has not been
registered already, and False if it has.</p>
<pre><span><em><small># Example: this print will only fire for the first loop iteration:</small></em></span>
for i in range(10):
if <a href="#function_ba_do_once">ba.do_once</a>():
print('Hello once from loop!')</pre>
<hr>
<h2><strong><a name="function_ba_emitfx">ba.emitfx()</a></strong></h3>
<p><span>emitfx(position: Sequence[float],