bastd.actor.scoreboard
Defines ScoreBoard Actor and related functionality.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Defines ScoreBoard Actor and related functionality.""" 4 5from __future__ import annotations 6 7import weakref 8from typing import TYPE_CHECKING 9 10import ba 11 12if TYPE_CHECKING: 13 from typing import Any, Sequence 14 15 16class _Entry: 17 18 def __init__(self, scoreboard: Scoreboard, team: ba.Team, do_cover: bool, 19 scale: float, label: ba.Lstr | None, flash_length: float): 20 # pylint: disable=too-many-statements 21 self._scoreboard = weakref.ref(scoreboard) 22 self._do_cover = do_cover 23 self._scale = scale 24 self._flash_length = flash_length 25 self._width = 140.0 * self._scale 26 self._height = 32.0 * self._scale 27 self._bar_width = 2.0 * self._scale 28 self._bar_height = 32.0 * self._scale 29 self._bar_tex = self._backing_tex = ba.gettexture('bar') 30 self._cover_tex = ba.gettexture('uiAtlas') 31 self._model = ba.getmodel('meterTransparent') 32 self._pos: Sequence[float] | None = None 33 self._flash_timer: ba.Timer | None = None 34 self._flash_counter: int | None = None 35 self._flash_colors: bool | None = None 36 self._score: float | None = None 37 38 safe_team_color = ba.safecolor(team.color, target_intensity=1.0) 39 40 # FIXME: Should not do things conditionally for vr-mode, as there may 41 # be non-vr clients connected which will also get these value. 42 vrmode = ba.app.vr_mode 43 44 if self._do_cover: 45 if vrmode: 46 self._backing_color = [0.1 + c * 0.1 for c in safe_team_color] 47 else: 48 self._backing_color = [ 49 0.05 + c * 0.17 for c in safe_team_color 50 ] 51 else: 52 self._backing_color = [0.05 + c * 0.1 for c in safe_team_color] 53 54 opacity = (0.8 if vrmode else 0.8) if self._do_cover else 0.5 55 self._backing = ba.NodeActor( 56 ba.newnode('image', 57 attrs={ 58 'scale': (self._width, self._height), 59 'opacity': opacity, 60 'color': self._backing_color, 61 'vr_depth': -3, 62 'attach': 'topLeft', 63 'texture': self._backing_tex 64 })) 65 66 self._barcolor = safe_team_color 67 self._bar = ba.NodeActor( 68 ba.newnode('image', 69 attrs={ 70 'opacity': 0.7, 71 'color': self._barcolor, 72 'attach': 'topLeft', 73 'texture': self._bar_tex 74 })) 75 76 self._bar_scale = ba.newnode('combine', 77 owner=self._bar.node, 78 attrs={ 79 'size': 2, 80 'input0': self._bar_width, 81 'input1': self._bar_height 82 }) 83 assert self._bar.node 84 self._bar_scale.connectattr('output', self._bar.node, 'scale') 85 self._bar_position = ba.newnode('combine', 86 owner=self._bar.node, 87 attrs={ 88 'size': 2, 89 'input0': 0, 90 'input1': 0 91 }) 92 self._bar_position.connectattr('output', self._bar.node, 'position') 93 self._cover_color = safe_team_color 94 if self._do_cover: 95 self._cover = ba.NodeActor( 96 ba.newnode('image', 97 attrs={ 98 'scale': 99 (self._width * 1.15, self._height * 1.6), 100 'opacity': 1.0, 101 'color': self._cover_color, 102 'vr_depth': 2, 103 'attach': 'topLeft', 104 'texture': self._cover_tex, 105 'model_transparent': self._model 106 })) 107 108 clr = safe_team_color 109 maxwidth = 130.0 * (1.0 - scoreboard.score_split) 110 flatness = ((1.0 if vrmode else 0.5) if self._do_cover else 1.0) 111 self._score_text = ba.NodeActor( 112 ba.newnode('text', 113 attrs={ 114 'h_attach': 'left', 115 'v_attach': 'top', 116 'h_align': 'right', 117 'v_align': 'center', 118 'maxwidth': maxwidth, 119 'vr_depth': 2, 120 'scale': self._scale * 0.9, 121 'text': '', 122 'shadow': 1.0 if vrmode else 0.5, 123 'flatness': flatness, 124 'color': clr 125 })) 126 127 clr = safe_team_color 128 129 team_name_label: str | ba.Lstr 130 if label is not None: 131 team_name_label = label 132 else: 133 team_name_label = team.name 134 135 # We do our own clipping here; should probably try to tap into some 136 # existing functionality. 137 if isinstance(team_name_label, ba.Lstr): 138 139 # Hmmm; if the team-name is a non-translatable value lets go 140 # ahead and clip it otherwise we leave it as-is so 141 # translation can occur.. 142 if team_name_label.is_flat_value(): 143 val = team_name_label.evaluate() 144 if len(val) > 10: 145 team_name_label = ba.Lstr(value=val[:10] + '...') 146 else: 147 if len(team_name_label) > 10: 148 team_name_label = team_name_label[:10] + '...' 149 team_name_label = ba.Lstr(value=team_name_label) 150 151 flatness = ((1.0 if vrmode else 0.5) if self._do_cover else 1.0) 152 self._name_text = ba.NodeActor( 153 ba.newnode('text', 154 attrs={ 155 'h_attach': 'left', 156 'v_attach': 'top', 157 'h_align': 'left', 158 'v_align': 'center', 159 'vr_depth': 2, 160 'scale': self._scale * 0.9, 161 'shadow': 1.0 if vrmode else 0.5, 162 'flatness': flatness, 163 'maxwidth': 130 * scoreboard.score_split, 164 'text': team_name_label, 165 'color': clr + (1.0, ) 166 })) 167 168 def flash(self, countdown: bool, extra_flash: bool) -> None: 169 """Flash momentarily.""" 170 self._flash_timer = ba.Timer(0.1, 171 ba.WeakCall(self._do_flash), 172 repeat=True) 173 if countdown: 174 self._flash_counter = 10 175 else: 176 self._flash_counter = int(20.0 * self._flash_length) 177 if extra_flash: 178 self._flash_counter *= 4 179 self._set_flash_colors(True) 180 181 def set_position(self, position: Sequence[float]) -> None: 182 """Set the entry's position.""" 183 184 # Abort if we've been killed 185 if not self._backing.node: 186 return 187 188 self._pos = tuple(position) 189 self._backing.node.position = (position[0] + self._width / 2, 190 position[1] - self._height / 2) 191 if self._do_cover: 192 assert self._cover.node 193 self._cover.node.position = (position[0] + self._width / 2, 194 position[1] - self._height / 2) 195 self._bar_position.input0 = self._pos[0] + self._bar_width / 2 196 self._bar_position.input1 = self._pos[1] - self._bar_height / 2 197 assert self._score_text.node 198 self._score_text.node.position = (self._pos[0] + self._width - 199 7.0 * self._scale, 200 self._pos[1] - self._bar_height + 201 16.0 * self._scale) 202 assert self._name_text.node 203 self._name_text.node.position = (self._pos[0] + 7.0 * self._scale, 204 self._pos[1] - self._bar_height + 205 16.0 * self._scale) 206 207 def _set_flash_colors(self, flash: bool) -> None: 208 self._flash_colors = flash 209 210 def _safesetcolor(node: ba.Node | None, val: Any) -> None: 211 if node: 212 node.color = val 213 214 if flash: 215 scale = 2.0 216 _safesetcolor( 217 self._backing.node, 218 (self._backing_color[0] * scale, self._backing_color[1] * 219 scale, self._backing_color[2] * scale)) 220 _safesetcolor(self._bar.node, 221 (self._barcolor[0] * scale, self._barcolor[1] * 222 scale, self._barcolor[2] * scale)) 223 if self._do_cover: 224 _safesetcolor( 225 self._cover.node, 226 (self._cover_color[0] * scale, self._cover_color[1] * 227 scale, self._cover_color[2] * scale)) 228 else: 229 _safesetcolor(self._backing.node, self._backing_color) 230 _safesetcolor(self._bar.node, self._barcolor) 231 if self._do_cover: 232 _safesetcolor(self._cover.node, self._cover_color) 233 234 def _do_flash(self) -> None: 235 assert self._flash_counter is not None 236 if self._flash_counter <= 0: 237 self._set_flash_colors(False) 238 else: 239 self._flash_counter -= 1 240 self._set_flash_colors(not self._flash_colors) 241 242 def set_value(self, 243 score: float, 244 max_score: float | None = None, 245 countdown: bool = False, 246 flash: bool = True, 247 show_value: bool = True) -> None: 248 """Set the value for the scoreboard entry.""" 249 250 # If we have no score yet, just set it.. otherwise compare 251 # and see if we should flash. 252 if self._score is None: 253 self._score = score 254 else: 255 if score > self._score or (countdown and score < self._score): 256 extra_flash = (max_score is not None and score >= max_score 257 and not countdown) or (countdown and score == 0) 258 if flash: 259 self.flash(countdown, extra_flash) 260 self._score = score 261 262 if max_score is None: 263 self._bar_width = 0.0 264 else: 265 if countdown: 266 self._bar_width = max( 267 2.0 * self._scale, 268 self._width * (1.0 - (float(score) / max_score))) 269 else: 270 self._bar_width = max( 271 2.0 * self._scale, 272 self._width * (min(1.0, 273 float(score) / max_score))) 274 275 cur_width = self._bar_scale.input0 276 ba.animate(self._bar_scale, 'input0', { 277 0.0: cur_width, 278 0.25: self._bar_width 279 }) 280 self._bar_scale.input1 = self._bar_height 281 cur_x = self._bar_position.input0 282 assert self._pos is not None 283 ba.animate(self._bar_position, 'input0', { 284 0.0: cur_x, 285 0.25: self._pos[0] + self._bar_width / 2 286 }) 287 self._bar_position.input1 = self._pos[1] - self._bar_height / 2 288 assert self._score_text.node 289 if show_value: 290 self._score_text.node.text = str(score) 291 else: 292 self._score_text.node.text = '' 293 294 295class _EntryProxy: 296 """Encapsulates adding/removing of a scoreboard Entry.""" 297 298 def __init__(self, scoreboard: Scoreboard, team: ba.Team): 299 self._scoreboard = weakref.ref(scoreboard) 300 301 # Have to store ID here instead of a weak-ref since the team will be 302 # dead when we die and need to remove it. 303 self._team_id = team.id 304 305 def __del__(self) -> None: 306 scoreboard = self._scoreboard() 307 308 # Remove our team from the scoreboard if its still around. 309 # (but deferred, in case we die in a sim step or something where 310 # its illegal to modify nodes) 311 if scoreboard is None: 312 return 313 314 try: 315 ba.pushcall(ba.Call(scoreboard.remove_team, self._team_id)) 316 except ba.ContextError: 317 # This happens if we fire after the activity expires. 318 # In that case we don't need to do anything. 319 pass 320 321 322class Scoreboard: 323 """A display for player or team scores during a game. 324 325 category: Gameplay Classes 326 """ 327 328 _ENTRYSTORENAME = ba.storagename('entry') 329 330 def __init__(self, label: ba.Lstr | None = None, score_split: float = 0.7): 331 """Instantiate a scoreboard. 332 333 Label can be something like 'points' and will 334 show up on boards if provided. 335 """ 336 self._flat_tex = ba.gettexture('null') 337 self._entries: dict[int, _Entry] = {} 338 self._label = label 339 self.score_split = score_split 340 341 # For free-for-all we go simpler since we have one per player. 342 self._pos: Sequence[float] 343 if isinstance(ba.getsession(), ba.FreeForAllSession): 344 self._do_cover = False 345 self._spacing = 35.0 346 self._pos = (17.0, -65.0) 347 self._scale = 0.8 348 self._flash_length = 0.5 349 else: 350 self._do_cover = True 351 self._spacing = 50.0 352 self._pos = (20.0, -70.0) 353 self._scale = 1.0 354 self._flash_length = 1.0 355 356 def set_team_value(self, 357 team: ba.Team, 358 score: float, 359 max_score: float | None = None, 360 countdown: bool = False, 361 flash: bool = True, 362 show_value: bool = True) -> None: 363 """Update the score-board display for the given ba.Team.""" 364 if team.id not in self._entries: 365 self._add_team(team) 366 367 # Create a proxy in the team which will kill 368 # our entry when it dies (for convenience) 369 assert self._ENTRYSTORENAME not in team.customdata 370 team.customdata[self._ENTRYSTORENAME] = _EntryProxy(self, team) 371 372 # Now set the entry. 373 self._entries[team.id].set_value(score=score, 374 max_score=max_score, 375 countdown=countdown, 376 flash=flash, 377 show_value=show_value) 378 379 def _add_team(self, team: ba.Team) -> None: 380 if team.id in self._entries: 381 raise RuntimeError('Duplicate team add') 382 self._entries[team.id] = _Entry(self, 383 team, 384 do_cover=self._do_cover, 385 scale=self._scale, 386 label=self._label, 387 flash_length=self._flash_length) 388 self._update_teams() 389 390 def remove_team(self, team_id: int) -> None: 391 """Remove the team with the given id from the scoreboard.""" 392 del self._entries[team_id] 393 self._update_teams() 394 395 def _update_teams(self) -> None: 396 pos = list(self._pos) 397 for entry in list(self._entries.values()): 398 entry.set_position(pos) 399 pos[1] -= self._spacing * self._scale
class
Scoreboard:
323class Scoreboard: 324 """A display for player or team scores during a game. 325 326 category: Gameplay Classes 327 """ 328 329 _ENTRYSTORENAME = ba.storagename('entry') 330 331 def __init__(self, label: ba.Lstr | None = None, score_split: float = 0.7): 332 """Instantiate a scoreboard. 333 334 Label can be something like 'points' and will 335 show up on boards if provided. 336 """ 337 self._flat_tex = ba.gettexture('null') 338 self._entries: dict[int, _Entry] = {} 339 self._label = label 340 self.score_split = score_split 341 342 # For free-for-all we go simpler since we have one per player. 343 self._pos: Sequence[float] 344 if isinstance(ba.getsession(), ba.FreeForAllSession): 345 self._do_cover = False 346 self._spacing = 35.0 347 self._pos = (17.0, -65.0) 348 self._scale = 0.8 349 self._flash_length = 0.5 350 else: 351 self._do_cover = True 352 self._spacing = 50.0 353 self._pos = (20.0, -70.0) 354 self._scale = 1.0 355 self._flash_length = 1.0 356 357 def set_team_value(self, 358 team: ba.Team, 359 score: float, 360 max_score: float | None = None, 361 countdown: bool = False, 362 flash: bool = True, 363 show_value: bool = True) -> None: 364 """Update the score-board display for the given ba.Team.""" 365 if team.id not in self._entries: 366 self._add_team(team) 367 368 # Create a proxy in the team which will kill 369 # our entry when it dies (for convenience) 370 assert self._ENTRYSTORENAME not in team.customdata 371 team.customdata[self._ENTRYSTORENAME] = _EntryProxy(self, team) 372 373 # Now set the entry. 374 self._entries[team.id].set_value(score=score, 375 max_score=max_score, 376 countdown=countdown, 377 flash=flash, 378 show_value=show_value) 379 380 def _add_team(self, team: ba.Team) -> None: 381 if team.id in self._entries: 382 raise RuntimeError('Duplicate team add') 383 self._entries[team.id] = _Entry(self, 384 team, 385 do_cover=self._do_cover, 386 scale=self._scale, 387 label=self._label, 388 flash_length=self._flash_length) 389 self._update_teams() 390 391 def remove_team(self, team_id: int) -> None: 392 """Remove the team with the given id from the scoreboard.""" 393 del self._entries[team_id] 394 self._update_teams() 395 396 def _update_teams(self) -> None: 397 pos = list(self._pos) 398 for entry in list(self._entries.values()): 399 entry.set_position(pos) 400 pos[1] -= self._spacing * self._scale
A display for player or team scores during a game.
category: Gameplay Classes
Scoreboard(label: ba._language.Lstr | None = None, score_split: float = 0.7)
331 def __init__(self, label: ba.Lstr | None = None, score_split: float = 0.7): 332 """Instantiate a scoreboard. 333 334 Label can be something like 'points' and will 335 show up on boards if provided. 336 """ 337 self._flat_tex = ba.gettexture('null') 338 self._entries: dict[int, _Entry] = {} 339 self._label = label 340 self.score_split = score_split 341 342 # For free-for-all we go simpler since we have one per player. 343 self._pos: Sequence[float] 344 if isinstance(ba.getsession(), ba.FreeForAllSession): 345 self._do_cover = False 346 self._spacing = 35.0 347 self._pos = (17.0, -65.0) 348 self._scale = 0.8 349 self._flash_length = 0.5 350 else: 351 self._do_cover = True 352 self._spacing = 50.0 353 self._pos = (20.0, -70.0) 354 self._scale = 1.0 355 self._flash_length = 1.0
Instantiate a scoreboard.
Label can be something like 'points' and will show up on boards if provided.
def
set_team_value( self, team: ba._team.Team, score: float, max_score: float | None = None, countdown: bool = False, flash: bool = True, show_value: bool = True) -> None:
357 def set_team_value(self, 358 team: ba.Team, 359 score: float, 360 max_score: float | None = None, 361 countdown: bool = False, 362 flash: bool = True, 363 show_value: bool = True) -> None: 364 """Update the score-board display for the given ba.Team.""" 365 if team.id not in self._entries: 366 self._add_team(team) 367 368 # Create a proxy in the team which will kill 369 # our entry when it dies (for convenience) 370 assert self._ENTRYSTORENAME not in team.customdata 371 team.customdata[self._ENTRYSTORENAME] = _EntryProxy(self, team) 372 373 # Now set the entry. 374 self._entries[team.id].set_value(score=score, 375 max_score=max_score, 376 countdown=countdown, 377 flash=flash, 378 show_value=show_value)
Update the score-board display for the given ba.Team.