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