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