bascenev1lib.activity.multiteamvictory
Functionality related to the final screen in multi-teams sessions.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Functionality related to the final screen in multi-teams sessions.""" 4 5from __future__ import annotations 6 7from typing import override, TYPE_CHECKING 8 9import bascenev1 as bs 10 11from bascenev1lib.activity.multiteamscore import MultiTeamScoreScreenActivity 12 13if TYPE_CHECKING: 14 from typing import Any 15 16 17class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): 18 """Final score screen for a team series.""" 19 20 # Dont' play music by default; (we do manually after a delay). 21 default_music = None 22 23 def __init__(self, settings: dict): 24 super().__init__(settings=settings) 25 self._min_view_time = 15.0 26 self._is_ffa = isinstance(self.session, bs.FreeForAllSession) 27 self._allow_server_transition = True 28 self._tips_text = None 29 self._default_show_tips = False 30 self._ffa_top_player_info: list[Any] | None = None 31 self._ffa_top_player_rec: bs.PlayerRecord | None = None 32 33 @override 34 def on_begin(self) -> None: 35 # pylint: disable=too-many-branches 36 # pylint: disable=too-many-locals 37 # pylint: disable=too-many-statements 38 from bascenev1lib.actor.text import Text 39 from bascenev1lib.actor.image import Image 40 41 bs.set_analytics_screen( 42 'FreeForAll Series Victory Screen' 43 if self._is_ffa 44 else 'Teams Series Victory Screen' 45 ) 46 assert bs.app.classic is not None 47 if bs.app.ui_v1.uiscale is bs.UIScale.LARGE: 48 sval = bs.Lstr(resource='pressAnyKeyButtonPlayAgainText') 49 else: 50 sval = bs.Lstr(resource='pressAnyButtonPlayAgainText') 51 self._show_up_next = False 52 self._custom_continue_message = sval 53 super().on_begin() 54 winning_sessionteam = self.settings_raw['winner'] 55 56 # Pause a moment before playing victory music. 57 bs.timer(0.6, bs.WeakCall(self._play_victory_music)) 58 bs.timer( 59 4.4, bs.WeakCall(self._show_winner, self.settings_raw['winner']) 60 ) 61 bs.timer(4.6, self._score_display_sound.play) 62 63 # Score / Name / Player-record. 64 player_entries: list[tuple[int, str, bs.PlayerRecord]] = [] 65 66 # Note: for ffa, exclude players who haven't entered the game yet. 67 if self._is_ffa: 68 for _pkey, prec in self.stats.get_records().items(): 69 if prec.player.in_game: 70 player_entries.append( 71 ( 72 prec.player.sessionteam.customdata['score'], 73 prec.getname(full=True), 74 prec, 75 ) 76 ) 77 player_entries.sort(reverse=True, key=lambda x: x[0]) 78 if len(player_entries) > 0: 79 # Store some info for the top ffa player so we can 80 # show winner info even if they leave. 81 self._ffa_top_player_info = list(player_entries[0]) 82 self._ffa_top_player_info[1] = self._ffa_top_player_info[ 83 2 84 ].getname() 85 self._ffa_top_player_info[2] = self._ffa_top_player_info[ 86 2 87 ].get_icon() 88 else: 89 for _pkey, prec in self.stats.get_records().items(): 90 player_entries.append((prec.score, prec.name_full, prec)) 91 player_entries.sort(reverse=True, key=lambda x: x[0]) 92 93 ts_height = 300.0 94 ts_h_offs = -390.0 95 tval = 6.4 96 t_incr = 0.12 97 98 always_use_first_to = bs.app.lang.get_resource( 99 'bestOfUseFirstToInstead' 100 ) 101 102 session = self.session 103 if self._is_ffa: 104 assert isinstance(session, bs.FreeForAllSession) 105 txt = bs.Lstr( 106 value='${A}:', 107 subs=[ 108 ( 109 '${A}', 110 bs.Lstr( 111 resource='firstToFinalText', 112 subs=[ 113 ( 114 '${COUNT}', 115 str(session.get_ffa_series_length()), 116 ) 117 ], 118 ), 119 ) 120 ], 121 ) 122 else: 123 assert isinstance(session, bs.MultiTeamSession) 124 125 # Some languages may prefer to always show 'first to X' instead of 126 # 'best of X'. 127 # FIXME: This will affect all clients connected to us even if 128 # they're not using this language. Should try to come up 129 # with a wording that works everywhere. 130 if always_use_first_to: 131 txt = bs.Lstr( 132 value='${A}:', 133 subs=[ 134 ( 135 '${A}', 136 bs.Lstr( 137 resource='firstToFinalText', 138 subs=[ 139 ( 140 '${COUNT}', 141 str( 142 session.get_series_length() / 2 + 1 143 ), 144 ) 145 ], 146 ), 147 ) 148 ], 149 ) 150 else: 151 txt = bs.Lstr( 152 value='${A}:', 153 subs=[ 154 ( 155 '${A}', 156 bs.Lstr( 157 resource='bestOfFinalText', 158 subs=[ 159 ( 160 '${COUNT}', 161 str(session.get_series_length()), 162 ) 163 ], 164 ), 165 ) 166 ], 167 ) 168 169 Text( 170 txt, 171 v_align=Text.VAlign.CENTER, 172 maxwidth=300, 173 color=(0.5, 0.5, 0.5, 1.0), 174 position=(0, 220), 175 scale=1.2, 176 transition=Text.Transition.IN_TOP_SLOW, 177 h_align=Text.HAlign.CENTER, 178 transition_delay=t_incr * 4, 179 ).autoretain() 180 181 win_score = (session.get_series_length() - 1) // 2 + 1 182 lose_score = 0 183 for team in self.teams: 184 if team.sessionteam.customdata['score'] != win_score: 185 lose_score = team.sessionteam.customdata['score'] 186 187 if not self._is_ffa: 188 Text( 189 bs.Lstr( 190 resource='gamesToText', 191 subs=[ 192 ('${WINCOUNT}', str(win_score)), 193 ('${LOSECOUNT}', str(lose_score)), 194 ], 195 ), 196 color=(0.5, 0.5, 0.5, 1.0), 197 maxwidth=160, 198 v_align=Text.VAlign.CENTER, 199 position=(0, -215), 200 scale=1.8, 201 transition=Text.Transition.IN_LEFT, 202 h_align=Text.HAlign.CENTER, 203 transition_delay=4.8 + t_incr * 4, 204 ).autoretain() 205 206 if self._is_ffa: 207 v_extra = 120 208 else: 209 v_extra = 0 210 211 mvp: bs.PlayerRecord | None = None 212 mvp_name: str | None = None 213 214 # Show game MVP. 215 if not self._is_ffa: 216 mvp, mvp_name = None, None 217 for entry in player_entries: 218 if entry[2].team == winning_sessionteam: 219 mvp = entry[2] 220 mvp_name = entry[1] 221 break 222 if mvp is not None: 223 Text( 224 bs.Lstr(resource='mostValuablePlayerText'), 225 color=(0.5, 0.5, 0.5, 1.0), 226 v_align=Text.VAlign.CENTER, 227 maxwidth=300, 228 position=(180, ts_height / 2 + 15), 229 transition=Text.Transition.IN_LEFT, 230 h_align=Text.HAlign.LEFT, 231 transition_delay=tval, 232 ).autoretain() 233 tval += 4 * t_incr 234 235 Image( 236 mvp.get_icon(), 237 position=(230, ts_height / 2 - 55 + 14 - 5), 238 scale=(70, 70), 239 transition=Image.Transition.IN_LEFT, 240 transition_delay=tval, 241 ).autoretain() 242 assert mvp_name is not None 243 Text( 244 bs.Lstr(value=mvp_name), 245 position=(280, ts_height / 2 - 55 + 15 - 5), 246 h_align=Text.HAlign.LEFT, 247 v_align=Text.VAlign.CENTER, 248 maxwidth=170, 249 scale=1.3, 250 color=bs.safecolor(mvp.team.color + (1,)), 251 transition=Text.Transition.IN_LEFT, 252 transition_delay=tval, 253 ).autoretain() 254 tval += 4 * t_incr 255 256 # Most violent. 257 most_kills = 0 258 for entry in player_entries: 259 if entry[2].kill_count >= most_kills: 260 mvp = entry[2] 261 mvp_name = entry[1] 262 most_kills = entry[2].kill_count 263 if mvp is not None: 264 Text( 265 bs.Lstr(resource='mostViolentPlayerText'), 266 color=(0.5, 0.5, 0.5, 1.0), 267 v_align=Text.VAlign.CENTER, 268 maxwidth=300, 269 position=(180, ts_height / 2 - 150 + v_extra + 15), 270 transition=Text.Transition.IN_LEFT, 271 h_align=Text.HAlign.LEFT, 272 transition_delay=tval, 273 ).autoretain() 274 Text( 275 bs.Lstr( 276 value='(${A})', 277 subs=[ 278 ( 279 '${A}', 280 bs.Lstr( 281 resource='killsTallyText', 282 subs=[('${COUNT}', str(most_kills))], 283 ), 284 ) 285 ], 286 ), 287 position=(260, ts_height / 2 - 150 - 15 + v_extra), 288 color=(0.3, 0.3, 0.3, 1.0), 289 scale=0.6, 290 h_align=Text.HAlign.LEFT, 291 transition=Text.Transition.IN_LEFT, 292 transition_delay=tval, 293 ).autoretain() 294 tval += 4 * t_incr 295 296 Image( 297 mvp.get_icon(), 298 position=(233, ts_height / 2 - 150 - 30 - 46 + 25 + v_extra), 299 scale=(50, 50), 300 transition=Image.Transition.IN_LEFT, 301 transition_delay=tval, 302 ).autoretain() 303 assert mvp_name is not None 304 Text( 305 bs.Lstr(value=mvp_name), 306 position=(270, ts_height / 2 - 150 - 30 - 36 + v_extra + 15), 307 h_align=Text.HAlign.LEFT, 308 v_align=Text.VAlign.CENTER, 309 maxwidth=180, 310 color=bs.safecolor(mvp.team.color + (1,)), 311 transition=Text.Transition.IN_LEFT, 312 transition_delay=tval, 313 ).autoretain() 314 tval += 4 * t_incr 315 316 # Most killed. 317 most_killed = 0 318 mkp, mkp_name = None, None 319 for entry in player_entries: 320 if entry[2].killed_count >= most_killed: 321 mkp = entry[2] 322 mkp_name = entry[1] 323 most_killed = entry[2].killed_count 324 if mkp is not None: 325 Text( 326 bs.Lstr(resource='mostViolatedPlayerText'), 327 color=(0.5, 0.5, 0.5, 1.0), 328 v_align=Text.VAlign.CENTER, 329 maxwidth=300, 330 position=(180, ts_height / 2 - 300 + v_extra + 15), 331 transition=Text.Transition.IN_LEFT, 332 h_align=Text.HAlign.LEFT, 333 transition_delay=tval, 334 ).autoretain() 335 Text( 336 bs.Lstr( 337 value='(${A})', 338 subs=[ 339 ( 340 '${A}', 341 bs.Lstr( 342 resource='deathsTallyText', 343 subs=[('${COUNT}', str(most_killed))], 344 ), 345 ) 346 ], 347 ), 348 position=(260, ts_height / 2 - 300 - 15 + v_extra), 349 h_align=Text.HAlign.LEFT, 350 scale=0.6, 351 color=(0.3, 0.3, 0.3, 1.0), 352 transition=Text.Transition.IN_LEFT, 353 transition_delay=tval, 354 ).autoretain() 355 tval += 4 * t_incr 356 Image( 357 mkp.get_icon(), 358 position=(233, ts_height / 2 - 300 - 30 - 46 + 25 + v_extra), 359 scale=(50, 50), 360 transition=Image.Transition.IN_LEFT, 361 transition_delay=tval, 362 ).autoretain() 363 assert mkp_name is not None 364 Text( 365 bs.Lstr(value=mkp_name), 366 position=(270, ts_height / 2 - 300 - 30 - 36 + v_extra + 15), 367 h_align=Text.HAlign.LEFT, 368 v_align=Text.VAlign.CENTER, 369 color=bs.safecolor(mkp.team.color + (1,)), 370 maxwidth=180, 371 transition=Text.Transition.IN_LEFT, 372 transition_delay=tval, 373 ).autoretain() 374 tval += 4 * t_incr 375 376 # Now show individual scores. 377 tdelay = tval 378 Text( 379 bs.Lstr(resource='finalScoresText'), 380 color=(0.5, 0.5, 0.5, 1.0), 381 position=(ts_h_offs, ts_height / 2), 382 transition=Text.Transition.IN_RIGHT, 383 transition_delay=tdelay, 384 ).autoretain() 385 tdelay += 4 * t_incr 386 387 v_offs = 0.0 388 tdelay += len(player_entries) * 8 * t_incr 389 for _score, name, prec in player_entries: 390 if not prec.player.in_game: 391 continue 392 tdelay -= 4 * t_incr 393 v_offs -= 40 394 Text( 395 ( 396 str(prec.team.customdata['score']) 397 if self._is_ffa 398 else str(prec.score) 399 ), 400 color=(0.5, 0.5, 0.5, 1.0), 401 position=(ts_h_offs + 230, ts_height / 2 + v_offs), 402 h_align=Text.HAlign.RIGHT, 403 transition=Text.Transition.IN_RIGHT, 404 transition_delay=tdelay, 405 ).autoretain() 406 tdelay -= 4 * t_incr 407 408 Image( 409 prec.get_icon(), 410 position=(ts_h_offs - 72, ts_height / 2 + v_offs + 15), 411 scale=(30, 30), 412 transition=Image.Transition.IN_LEFT, 413 transition_delay=tdelay, 414 ).autoretain() 415 Text( 416 bs.Lstr(value=name), 417 position=(ts_h_offs - 50, ts_height / 2 + v_offs + 15), 418 h_align=Text.HAlign.LEFT, 419 v_align=Text.VAlign.CENTER, 420 maxwidth=180, 421 color=bs.safecolor(prec.team.color + (1,)), 422 transition=Text.Transition.IN_RIGHT, 423 transition_delay=tdelay, 424 ).autoretain() 425 426 bs.timer(15.0, bs.WeakCall(self._show_tips)) 427 428 def _show_tips(self) -> None: 429 from bascenev1lib.actor.tipstext import TipsText 430 431 self._tips_text = TipsText(offs_y=70) 432 433 def _play_victory_music(self) -> None: 434 # Make sure we don't stomp on the next activity's music choice. 435 if not self.is_transitioning_out(): 436 bs.setmusic(bs.MusicType.VICTORY) 437 438 def _show_winner(self, team: bs.SessionTeam) -> None: 439 from bascenev1lib.actor.image import Image 440 from bascenev1lib.actor.zoomtext import ZoomText 441 442 if not self._is_ffa: 443 offs_v = 0.0 444 ZoomText( 445 team.name, 446 position=(0, 97), 447 color=team.color, 448 scale=1.15, 449 jitter=1.0, 450 maxwidth=250, 451 ).autoretain() 452 else: 453 offs_v = -80 454 assert isinstance(self.session, bs.MultiTeamSession) 455 series_length = self.session.get_ffa_series_length() 456 icon: dict | None 457 # Pull live player info if they're still around. 458 if len(team.players) == 1: 459 icon = team.players[0].get_icon() 460 player_name = team.players[0].getname(full=True, icon=False) 461 # Otherwise use the special info we stored when we came in. 462 elif ( 463 self._ffa_top_player_info is not None 464 and self._ffa_top_player_info[0] >= series_length 465 ): 466 icon = self._ffa_top_player_info[2] 467 player_name = self._ffa_top_player_info[1] 468 else: 469 icon = None 470 player_name = 'Player Not Found' 471 472 if icon is not None: 473 i = Image( 474 icon, 475 position=(0, 143), 476 scale=(100, 100), 477 ).autoretain() 478 assert i.node 479 bs.animate(i.node, 'opacity', {0.0: 0.0, 0.25: 1.0}) 480 481 ZoomText( 482 bs.Lstr(value=player_name), 483 position=(0, 97 + offs_v + (0 if icon is not None else 60)), 484 color=team.color, 485 scale=1.15, 486 jitter=1.0, 487 maxwidth=250, 488 ).autoretain() 489 490 s_extra = 1.0 if self._is_ffa else 1.0 491 492 # Some languages say "FOO WINS" differently for teams vs players. 493 if isinstance(self.session, bs.FreeForAllSession): 494 wins_resource = 'seriesWinLine1PlayerText' 495 else: 496 wins_resource = 'seriesWinLine1TeamText' 497 wins_text = bs.Lstr(resource=wins_resource) 498 499 # Temp - if these come up as the english default, fall-back to the 500 # unified old form which is more likely to be translated. 501 ZoomText( 502 wins_text, 503 position=(0, -10 + offs_v), 504 color=team.color, 505 scale=0.65 * s_extra, 506 jitter=1.0, 507 maxwidth=250, 508 ).autoretain() 509 ZoomText( 510 bs.Lstr(resource='seriesWinLine2Text'), 511 position=(0, -110 + offs_v), 512 scale=1.0 * s_extra, 513 color=team.color, 514 jitter=1.0, 515 maxwidth=250, 516 ).autoretain()
class
TeamSeriesVictoryScoreScreenActivity(bascenev1._activity.Activity[bascenev1._player.EmptyPlayer, bascenev1._team.EmptyTeam]):
18class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): 19 """Final score screen for a team series.""" 20 21 # Dont' play music by default; (we do manually after a delay). 22 default_music = None 23 24 def __init__(self, settings: dict): 25 super().__init__(settings=settings) 26 self._min_view_time = 15.0 27 self._is_ffa = isinstance(self.session, bs.FreeForAllSession) 28 self._allow_server_transition = True 29 self._tips_text = None 30 self._default_show_tips = False 31 self._ffa_top_player_info: list[Any] | None = None 32 self._ffa_top_player_rec: bs.PlayerRecord | None = None 33 34 @override 35 def on_begin(self) -> None: 36 # pylint: disable=too-many-branches 37 # pylint: disable=too-many-locals 38 # pylint: disable=too-many-statements 39 from bascenev1lib.actor.text import Text 40 from bascenev1lib.actor.image import Image 41 42 bs.set_analytics_screen( 43 'FreeForAll Series Victory Screen' 44 if self._is_ffa 45 else 'Teams Series Victory Screen' 46 ) 47 assert bs.app.classic is not None 48 if bs.app.ui_v1.uiscale is bs.UIScale.LARGE: 49 sval = bs.Lstr(resource='pressAnyKeyButtonPlayAgainText') 50 else: 51 sval = bs.Lstr(resource='pressAnyButtonPlayAgainText') 52 self._show_up_next = False 53 self._custom_continue_message = sval 54 super().on_begin() 55 winning_sessionteam = self.settings_raw['winner'] 56 57 # Pause a moment before playing victory music. 58 bs.timer(0.6, bs.WeakCall(self._play_victory_music)) 59 bs.timer( 60 4.4, bs.WeakCall(self._show_winner, self.settings_raw['winner']) 61 ) 62 bs.timer(4.6, self._score_display_sound.play) 63 64 # Score / Name / Player-record. 65 player_entries: list[tuple[int, str, bs.PlayerRecord]] = [] 66 67 # Note: for ffa, exclude players who haven't entered the game yet. 68 if self._is_ffa: 69 for _pkey, prec in self.stats.get_records().items(): 70 if prec.player.in_game: 71 player_entries.append( 72 ( 73 prec.player.sessionteam.customdata['score'], 74 prec.getname(full=True), 75 prec, 76 ) 77 ) 78 player_entries.sort(reverse=True, key=lambda x: x[0]) 79 if len(player_entries) > 0: 80 # Store some info for the top ffa player so we can 81 # show winner info even if they leave. 82 self._ffa_top_player_info = list(player_entries[0]) 83 self._ffa_top_player_info[1] = self._ffa_top_player_info[ 84 2 85 ].getname() 86 self._ffa_top_player_info[2] = self._ffa_top_player_info[ 87 2 88 ].get_icon() 89 else: 90 for _pkey, prec in self.stats.get_records().items(): 91 player_entries.append((prec.score, prec.name_full, prec)) 92 player_entries.sort(reverse=True, key=lambda x: x[0]) 93 94 ts_height = 300.0 95 ts_h_offs = -390.0 96 tval = 6.4 97 t_incr = 0.12 98 99 always_use_first_to = bs.app.lang.get_resource( 100 'bestOfUseFirstToInstead' 101 ) 102 103 session = self.session 104 if self._is_ffa: 105 assert isinstance(session, bs.FreeForAllSession) 106 txt = bs.Lstr( 107 value='${A}:', 108 subs=[ 109 ( 110 '${A}', 111 bs.Lstr( 112 resource='firstToFinalText', 113 subs=[ 114 ( 115 '${COUNT}', 116 str(session.get_ffa_series_length()), 117 ) 118 ], 119 ), 120 ) 121 ], 122 ) 123 else: 124 assert isinstance(session, bs.MultiTeamSession) 125 126 # Some languages may prefer to always show 'first to X' instead of 127 # 'best of X'. 128 # FIXME: This will affect all clients connected to us even if 129 # they're not using this language. Should try to come up 130 # with a wording that works everywhere. 131 if always_use_first_to: 132 txt = bs.Lstr( 133 value='${A}:', 134 subs=[ 135 ( 136 '${A}', 137 bs.Lstr( 138 resource='firstToFinalText', 139 subs=[ 140 ( 141 '${COUNT}', 142 str( 143 session.get_series_length() / 2 + 1 144 ), 145 ) 146 ], 147 ), 148 ) 149 ], 150 ) 151 else: 152 txt = bs.Lstr( 153 value='${A}:', 154 subs=[ 155 ( 156 '${A}', 157 bs.Lstr( 158 resource='bestOfFinalText', 159 subs=[ 160 ( 161 '${COUNT}', 162 str(session.get_series_length()), 163 ) 164 ], 165 ), 166 ) 167 ], 168 ) 169 170 Text( 171 txt, 172 v_align=Text.VAlign.CENTER, 173 maxwidth=300, 174 color=(0.5, 0.5, 0.5, 1.0), 175 position=(0, 220), 176 scale=1.2, 177 transition=Text.Transition.IN_TOP_SLOW, 178 h_align=Text.HAlign.CENTER, 179 transition_delay=t_incr * 4, 180 ).autoretain() 181 182 win_score = (session.get_series_length() - 1) // 2 + 1 183 lose_score = 0 184 for team in self.teams: 185 if team.sessionteam.customdata['score'] != win_score: 186 lose_score = team.sessionteam.customdata['score'] 187 188 if not self._is_ffa: 189 Text( 190 bs.Lstr( 191 resource='gamesToText', 192 subs=[ 193 ('${WINCOUNT}', str(win_score)), 194 ('${LOSECOUNT}', str(lose_score)), 195 ], 196 ), 197 color=(0.5, 0.5, 0.5, 1.0), 198 maxwidth=160, 199 v_align=Text.VAlign.CENTER, 200 position=(0, -215), 201 scale=1.8, 202 transition=Text.Transition.IN_LEFT, 203 h_align=Text.HAlign.CENTER, 204 transition_delay=4.8 + t_incr * 4, 205 ).autoretain() 206 207 if self._is_ffa: 208 v_extra = 120 209 else: 210 v_extra = 0 211 212 mvp: bs.PlayerRecord | None = None 213 mvp_name: str | None = None 214 215 # Show game MVP. 216 if not self._is_ffa: 217 mvp, mvp_name = None, None 218 for entry in player_entries: 219 if entry[2].team == winning_sessionteam: 220 mvp = entry[2] 221 mvp_name = entry[1] 222 break 223 if mvp is not None: 224 Text( 225 bs.Lstr(resource='mostValuablePlayerText'), 226 color=(0.5, 0.5, 0.5, 1.0), 227 v_align=Text.VAlign.CENTER, 228 maxwidth=300, 229 position=(180, ts_height / 2 + 15), 230 transition=Text.Transition.IN_LEFT, 231 h_align=Text.HAlign.LEFT, 232 transition_delay=tval, 233 ).autoretain() 234 tval += 4 * t_incr 235 236 Image( 237 mvp.get_icon(), 238 position=(230, ts_height / 2 - 55 + 14 - 5), 239 scale=(70, 70), 240 transition=Image.Transition.IN_LEFT, 241 transition_delay=tval, 242 ).autoretain() 243 assert mvp_name is not None 244 Text( 245 bs.Lstr(value=mvp_name), 246 position=(280, ts_height / 2 - 55 + 15 - 5), 247 h_align=Text.HAlign.LEFT, 248 v_align=Text.VAlign.CENTER, 249 maxwidth=170, 250 scale=1.3, 251 color=bs.safecolor(mvp.team.color + (1,)), 252 transition=Text.Transition.IN_LEFT, 253 transition_delay=tval, 254 ).autoretain() 255 tval += 4 * t_incr 256 257 # Most violent. 258 most_kills = 0 259 for entry in player_entries: 260 if entry[2].kill_count >= most_kills: 261 mvp = entry[2] 262 mvp_name = entry[1] 263 most_kills = entry[2].kill_count 264 if mvp is not None: 265 Text( 266 bs.Lstr(resource='mostViolentPlayerText'), 267 color=(0.5, 0.5, 0.5, 1.0), 268 v_align=Text.VAlign.CENTER, 269 maxwidth=300, 270 position=(180, ts_height / 2 - 150 + v_extra + 15), 271 transition=Text.Transition.IN_LEFT, 272 h_align=Text.HAlign.LEFT, 273 transition_delay=tval, 274 ).autoretain() 275 Text( 276 bs.Lstr( 277 value='(${A})', 278 subs=[ 279 ( 280 '${A}', 281 bs.Lstr( 282 resource='killsTallyText', 283 subs=[('${COUNT}', str(most_kills))], 284 ), 285 ) 286 ], 287 ), 288 position=(260, ts_height / 2 - 150 - 15 + v_extra), 289 color=(0.3, 0.3, 0.3, 1.0), 290 scale=0.6, 291 h_align=Text.HAlign.LEFT, 292 transition=Text.Transition.IN_LEFT, 293 transition_delay=tval, 294 ).autoretain() 295 tval += 4 * t_incr 296 297 Image( 298 mvp.get_icon(), 299 position=(233, ts_height / 2 - 150 - 30 - 46 + 25 + v_extra), 300 scale=(50, 50), 301 transition=Image.Transition.IN_LEFT, 302 transition_delay=tval, 303 ).autoretain() 304 assert mvp_name is not None 305 Text( 306 bs.Lstr(value=mvp_name), 307 position=(270, ts_height / 2 - 150 - 30 - 36 + v_extra + 15), 308 h_align=Text.HAlign.LEFT, 309 v_align=Text.VAlign.CENTER, 310 maxwidth=180, 311 color=bs.safecolor(mvp.team.color + (1,)), 312 transition=Text.Transition.IN_LEFT, 313 transition_delay=tval, 314 ).autoretain() 315 tval += 4 * t_incr 316 317 # Most killed. 318 most_killed = 0 319 mkp, mkp_name = None, None 320 for entry in player_entries: 321 if entry[2].killed_count >= most_killed: 322 mkp = entry[2] 323 mkp_name = entry[1] 324 most_killed = entry[2].killed_count 325 if mkp is not None: 326 Text( 327 bs.Lstr(resource='mostViolatedPlayerText'), 328 color=(0.5, 0.5, 0.5, 1.0), 329 v_align=Text.VAlign.CENTER, 330 maxwidth=300, 331 position=(180, ts_height / 2 - 300 + v_extra + 15), 332 transition=Text.Transition.IN_LEFT, 333 h_align=Text.HAlign.LEFT, 334 transition_delay=tval, 335 ).autoretain() 336 Text( 337 bs.Lstr( 338 value='(${A})', 339 subs=[ 340 ( 341 '${A}', 342 bs.Lstr( 343 resource='deathsTallyText', 344 subs=[('${COUNT}', str(most_killed))], 345 ), 346 ) 347 ], 348 ), 349 position=(260, ts_height / 2 - 300 - 15 + v_extra), 350 h_align=Text.HAlign.LEFT, 351 scale=0.6, 352 color=(0.3, 0.3, 0.3, 1.0), 353 transition=Text.Transition.IN_LEFT, 354 transition_delay=tval, 355 ).autoretain() 356 tval += 4 * t_incr 357 Image( 358 mkp.get_icon(), 359 position=(233, ts_height / 2 - 300 - 30 - 46 + 25 + v_extra), 360 scale=(50, 50), 361 transition=Image.Transition.IN_LEFT, 362 transition_delay=tval, 363 ).autoretain() 364 assert mkp_name is not None 365 Text( 366 bs.Lstr(value=mkp_name), 367 position=(270, ts_height / 2 - 300 - 30 - 36 + v_extra + 15), 368 h_align=Text.HAlign.LEFT, 369 v_align=Text.VAlign.CENTER, 370 color=bs.safecolor(mkp.team.color + (1,)), 371 maxwidth=180, 372 transition=Text.Transition.IN_LEFT, 373 transition_delay=tval, 374 ).autoretain() 375 tval += 4 * t_incr 376 377 # Now show individual scores. 378 tdelay = tval 379 Text( 380 bs.Lstr(resource='finalScoresText'), 381 color=(0.5, 0.5, 0.5, 1.0), 382 position=(ts_h_offs, ts_height / 2), 383 transition=Text.Transition.IN_RIGHT, 384 transition_delay=tdelay, 385 ).autoretain() 386 tdelay += 4 * t_incr 387 388 v_offs = 0.0 389 tdelay += len(player_entries) * 8 * t_incr 390 for _score, name, prec in player_entries: 391 if not prec.player.in_game: 392 continue 393 tdelay -= 4 * t_incr 394 v_offs -= 40 395 Text( 396 ( 397 str(prec.team.customdata['score']) 398 if self._is_ffa 399 else str(prec.score) 400 ), 401 color=(0.5, 0.5, 0.5, 1.0), 402 position=(ts_h_offs + 230, ts_height / 2 + v_offs), 403 h_align=Text.HAlign.RIGHT, 404 transition=Text.Transition.IN_RIGHT, 405 transition_delay=tdelay, 406 ).autoretain() 407 tdelay -= 4 * t_incr 408 409 Image( 410 prec.get_icon(), 411 position=(ts_h_offs - 72, ts_height / 2 + v_offs + 15), 412 scale=(30, 30), 413 transition=Image.Transition.IN_LEFT, 414 transition_delay=tdelay, 415 ).autoretain() 416 Text( 417 bs.Lstr(value=name), 418 position=(ts_h_offs - 50, ts_height / 2 + v_offs + 15), 419 h_align=Text.HAlign.LEFT, 420 v_align=Text.VAlign.CENTER, 421 maxwidth=180, 422 color=bs.safecolor(prec.team.color + (1,)), 423 transition=Text.Transition.IN_RIGHT, 424 transition_delay=tdelay, 425 ).autoretain() 426 427 bs.timer(15.0, bs.WeakCall(self._show_tips)) 428 429 def _show_tips(self) -> None: 430 from bascenev1lib.actor.tipstext import TipsText 431 432 self._tips_text = TipsText(offs_y=70) 433 434 def _play_victory_music(self) -> None: 435 # Make sure we don't stomp on the next activity's music choice. 436 if not self.is_transitioning_out(): 437 bs.setmusic(bs.MusicType.VICTORY) 438 439 def _show_winner(self, team: bs.SessionTeam) -> None: 440 from bascenev1lib.actor.image import Image 441 from bascenev1lib.actor.zoomtext import ZoomText 442 443 if not self._is_ffa: 444 offs_v = 0.0 445 ZoomText( 446 team.name, 447 position=(0, 97), 448 color=team.color, 449 scale=1.15, 450 jitter=1.0, 451 maxwidth=250, 452 ).autoretain() 453 else: 454 offs_v = -80 455 assert isinstance(self.session, bs.MultiTeamSession) 456 series_length = self.session.get_ffa_series_length() 457 icon: dict | None 458 # Pull live player info if they're still around. 459 if len(team.players) == 1: 460 icon = team.players[0].get_icon() 461 player_name = team.players[0].getname(full=True, icon=False) 462 # Otherwise use the special info we stored when we came in. 463 elif ( 464 self._ffa_top_player_info is not None 465 and self._ffa_top_player_info[0] >= series_length 466 ): 467 icon = self._ffa_top_player_info[2] 468 player_name = self._ffa_top_player_info[1] 469 else: 470 icon = None 471 player_name = 'Player Not Found' 472 473 if icon is not None: 474 i = Image( 475 icon, 476 position=(0, 143), 477 scale=(100, 100), 478 ).autoretain() 479 assert i.node 480 bs.animate(i.node, 'opacity', {0.0: 0.0, 0.25: 1.0}) 481 482 ZoomText( 483 bs.Lstr(value=player_name), 484 position=(0, 97 + offs_v + (0 if icon is not None else 60)), 485 color=team.color, 486 scale=1.15, 487 jitter=1.0, 488 maxwidth=250, 489 ).autoretain() 490 491 s_extra = 1.0 if self._is_ffa else 1.0 492 493 # Some languages say "FOO WINS" differently for teams vs players. 494 if isinstance(self.session, bs.FreeForAllSession): 495 wins_resource = 'seriesWinLine1PlayerText' 496 else: 497 wins_resource = 'seriesWinLine1TeamText' 498 wins_text = bs.Lstr(resource=wins_resource) 499 500 # Temp - if these come up as the english default, fall-back to the 501 # unified old form which is more likely to be translated. 502 ZoomText( 503 wins_text, 504 position=(0, -10 + offs_v), 505 color=team.color, 506 scale=0.65 * s_extra, 507 jitter=1.0, 508 maxwidth=250, 509 ).autoretain() 510 ZoomText( 511 bs.Lstr(resource='seriesWinLine2Text'), 512 position=(0, -110 + offs_v), 513 scale=1.0 * s_extra, 514 color=team.color, 515 jitter=1.0, 516 maxwidth=250, 517 ).autoretain()
Final score screen for a team series.
TeamSeriesVictoryScoreScreenActivity(settings: dict)
24 def __init__(self, settings: dict): 25 super().__init__(settings=settings) 26 self._min_view_time = 15.0 27 self._is_ffa = isinstance(self.session, bs.FreeForAllSession) 28 self._allow_server_transition = True 29 self._tips_text = None 30 self._default_show_tips = False 31 self._ffa_top_player_info: list[Any] | None = None 32 self._ffa_top_player_rec: bs.PlayerRecord | None = None
Creates an Activity in the current bascenev1.Session.
The activity will not be actually run until bascenev1.Session.setactivity is called. 'settings' should be a dict of key/value pairs specific to the activity.
Activities should preload as much of their media/etc as possible in their constructor, but none of it should actually be used until they are transitioned in.
@override
def
on_begin(self) -> None:
34 @override 35 def on_begin(self) -> None: 36 # pylint: disable=too-many-branches 37 # pylint: disable=too-many-locals 38 # pylint: disable=too-many-statements 39 from bascenev1lib.actor.text import Text 40 from bascenev1lib.actor.image import Image 41 42 bs.set_analytics_screen( 43 'FreeForAll Series Victory Screen' 44 if self._is_ffa 45 else 'Teams Series Victory Screen' 46 ) 47 assert bs.app.classic is not None 48 if bs.app.ui_v1.uiscale is bs.UIScale.LARGE: 49 sval = bs.Lstr(resource='pressAnyKeyButtonPlayAgainText') 50 else: 51 sval = bs.Lstr(resource='pressAnyButtonPlayAgainText') 52 self._show_up_next = False 53 self._custom_continue_message = sval 54 super().on_begin() 55 winning_sessionteam = self.settings_raw['winner'] 56 57 # Pause a moment before playing victory music. 58 bs.timer(0.6, bs.WeakCall(self._play_victory_music)) 59 bs.timer( 60 4.4, bs.WeakCall(self._show_winner, self.settings_raw['winner']) 61 ) 62 bs.timer(4.6, self._score_display_sound.play) 63 64 # Score / Name / Player-record. 65 player_entries: list[tuple[int, str, bs.PlayerRecord]] = [] 66 67 # Note: for ffa, exclude players who haven't entered the game yet. 68 if self._is_ffa: 69 for _pkey, prec in self.stats.get_records().items(): 70 if prec.player.in_game: 71 player_entries.append( 72 ( 73 prec.player.sessionteam.customdata['score'], 74 prec.getname(full=True), 75 prec, 76 ) 77 ) 78 player_entries.sort(reverse=True, key=lambda x: x[0]) 79 if len(player_entries) > 0: 80 # Store some info for the top ffa player so we can 81 # show winner info even if they leave. 82 self._ffa_top_player_info = list(player_entries[0]) 83 self._ffa_top_player_info[1] = self._ffa_top_player_info[ 84 2 85 ].getname() 86 self._ffa_top_player_info[2] = self._ffa_top_player_info[ 87 2 88 ].get_icon() 89 else: 90 for _pkey, prec in self.stats.get_records().items(): 91 player_entries.append((prec.score, prec.name_full, prec)) 92 player_entries.sort(reverse=True, key=lambda x: x[0]) 93 94 ts_height = 300.0 95 ts_h_offs = -390.0 96 tval = 6.4 97 t_incr = 0.12 98 99 always_use_first_to = bs.app.lang.get_resource( 100 'bestOfUseFirstToInstead' 101 ) 102 103 session = self.session 104 if self._is_ffa: 105 assert isinstance(session, bs.FreeForAllSession) 106 txt = bs.Lstr( 107 value='${A}:', 108 subs=[ 109 ( 110 '${A}', 111 bs.Lstr( 112 resource='firstToFinalText', 113 subs=[ 114 ( 115 '${COUNT}', 116 str(session.get_ffa_series_length()), 117 ) 118 ], 119 ), 120 ) 121 ], 122 ) 123 else: 124 assert isinstance(session, bs.MultiTeamSession) 125 126 # Some languages may prefer to always show 'first to X' instead of 127 # 'best of X'. 128 # FIXME: This will affect all clients connected to us even if 129 # they're not using this language. Should try to come up 130 # with a wording that works everywhere. 131 if always_use_first_to: 132 txt = bs.Lstr( 133 value='${A}:', 134 subs=[ 135 ( 136 '${A}', 137 bs.Lstr( 138 resource='firstToFinalText', 139 subs=[ 140 ( 141 '${COUNT}', 142 str( 143 session.get_series_length() / 2 + 1 144 ), 145 ) 146 ], 147 ), 148 ) 149 ], 150 ) 151 else: 152 txt = bs.Lstr( 153 value='${A}:', 154 subs=[ 155 ( 156 '${A}', 157 bs.Lstr( 158 resource='bestOfFinalText', 159 subs=[ 160 ( 161 '${COUNT}', 162 str(session.get_series_length()), 163 ) 164 ], 165 ), 166 ) 167 ], 168 ) 169 170 Text( 171 txt, 172 v_align=Text.VAlign.CENTER, 173 maxwidth=300, 174 color=(0.5, 0.5, 0.5, 1.0), 175 position=(0, 220), 176 scale=1.2, 177 transition=Text.Transition.IN_TOP_SLOW, 178 h_align=Text.HAlign.CENTER, 179 transition_delay=t_incr * 4, 180 ).autoretain() 181 182 win_score = (session.get_series_length() - 1) // 2 + 1 183 lose_score = 0 184 for team in self.teams: 185 if team.sessionteam.customdata['score'] != win_score: 186 lose_score = team.sessionteam.customdata['score'] 187 188 if not self._is_ffa: 189 Text( 190 bs.Lstr( 191 resource='gamesToText', 192 subs=[ 193 ('${WINCOUNT}', str(win_score)), 194 ('${LOSECOUNT}', str(lose_score)), 195 ], 196 ), 197 color=(0.5, 0.5, 0.5, 1.0), 198 maxwidth=160, 199 v_align=Text.VAlign.CENTER, 200 position=(0, -215), 201 scale=1.8, 202 transition=Text.Transition.IN_LEFT, 203 h_align=Text.HAlign.CENTER, 204 transition_delay=4.8 + t_incr * 4, 205 ).autoretain() 206 207 if self._is_ffa: 208 v_extra = 120 209 else: 210 v_extra = 0 211 212 mvp: bs.PlayerRecord | None = None 213 mvp_name: str | None = None 214 215 # Show game MVP. 216 if not self._is_ffa: 217 mvp, mvp_name = None, None 218 for entry in player_entries: 219 if entry[2].team == winning_sessionteam: 220 mvp = entry[2] 221 mvp_name = entry[1] 222 break 223 if mvp is not None: 224 Text( 225 bs.Lstr(resource='mostValuablePlayerText'), 226 color=(0.5, 0.5, 0.5, 1.0), 227 v_align=Text.VAlign.CENTER, 228 maxwidth=300, 229 position=(180, ts_height / 2 + 15), 230 transition=Text.Transition.IN_LEFT, 231 h_align=Text.HAlign.LEFT, 232 transition_delay=tval, 233 ).autoretain() 234 tval += 4 * t_incr 235 236 Image( 237 mvp.get_icon(), 238 position=(230, ts_height / 2 - 55 + 14 - 5), 239 scale=(70, 70), 240 transition=Image.Transition.IN_LEFT, 241 transition_delay=tval, 242 ).autoretain() 243 assert mvp_name is not None 244 Text( 245 bs.Lstr(value=mvp_name), 246 position=(280, ts_height / 2 - 55 + 15 - 5), 247 h_align=Text.HAlign.LEFT, 248 v_align=Text.VAlign.CENTER, 249 maxwidth=170, 250 scale=1.3, 251 color=bs.safecolor(mvp.team.color + (1,)), 252 transition=Text.Transition.IN_LEFT, 253 transition_delay=tval, 254 ).autoretain() 255 tval += 4 * t_incr 256 257 # Most violent. 258 most_kills = 0 259 for entry in player_entries: 260 if entry[2].kill_count >= most_kills: 261 mvp = entry[2] 262 mvp_name = entry[1] 263 most_kills = entry[2].kill_count 264 if mvp is not None: 265 Text( 266 bs.Lstr(resource='mostViolentPlayerText'), 267 color=(0.5, 0.5, 0.5, 1.0), 268 v_align=Text.VAlign.CENTER, 269 maxwidth=300, 270 position=(180, ts_height / 2 - 150 + v_extra + 15), 271 transition=Text.Transition.IN_LEFT, 272 h_align=Text.HAlign.LEFT, 273 transition_delay=tval, 274 ).autoretain() 275 Text( 276 bs.Lstr( 277 value='(${A})', 278 subs=[ 279 ( 280 '${A}', 281 bs.Lstr( 282 resource='killsTallyText', 283 subs=[('${COUNT}', str(most_kills))], 284 ), 285 ) 286 ], 287 ), 288 position=(260, ts_height / 2 - 150 - 15 + v_extra), 289 color=(0.3, 0.3, 0.3, 1.0), 290 scale=0.6, 291 h_align=Text.HAlign.LEFT, 292 transition=Text.Transition.IN_LEFT, 293 transition_delay=tval, 294 ).autoretain() 295 tval += 4 * t_incr 296 297 Image( 298 mvp.get_icon(), 299 position=(233, ts_height / 2 - 150 - 30 - 46 + 25 + v_extra), 300 scale=(50, 50), 301 transition=Image.Transition.IN_LEFT, 302 transition_delay=tval, 303 ).autoretain() 304 assert mvp_name is not None 305 Text( 306 bs.Lstr(value=mvp_name), 307 position=(270, ts_height / 2 - 150 - 30 - 36 + v_extra + 15), 308 h_align=Text.HAlign.LEFT, 309 v_align=Text.VAlign.CENTER, 310 maxwidth=180, 311 color=bs.safecolor(mvp.team.color + (1,)), 312 transition=Text.Transition.IN_LEFT, 313 transition_delay=tval, 314 ).autoretain() 315 tval += 4 * t_incr 316 317 # Most killed. 318 most_killed = 0 319 mkp, mkp_name = None, None 320 for entry in player_entries: 321 if entry[2].killed_count >= most_killed: 322 mkp = entry[2] 323 mkp_name = entry[1] 324 most_killed = entry[2].killed_count 325 if mkp is not None: 326 Text( 327 bs.Lstr(resource='mostViolatedPlayerText'), 328 color=(0.5, 0.5, 0.5, 1.0), 329 v_align=Text.VAlign.CENTER, 330 maxwidth=300, 331 position=(180, ts_height / 2 - 300 + v_extra + 15), 332 transition=Text.Transition.IN_LEFT, 333 h_align=Text.HAlign.LEFT, 334 transition_delay=tval, 335 ).autoretain() 336 Text( 337 bs.Lstr( 338 value='(${A})', 339 subs=[ 340 ( 341 '${A}', 342 bs.Lstr( 343 resource='deathsTallyText', 344 subs=[('${COUNT}', str(most_killed))], 345 ), 346 ) 347 ], 348 ), 349 position=(260, ts_height / 2 - 300 - 15 + v_extra), 350 h_align=Text.HAlign.LEFT, 351 scale=0.6, 352 color=(0.3, 0.3, 0.3, 1.0), 353 transition=Text.Transition.IN_LEFT, 354 transition_delay=tval, 355 ).autoretain() 356 tval += 4 * t_incr 357 Image( 358 mkp.get_icon(), 359 position=(233, ts_height / 2 - 300 - 30 - 46 + 25 + v_extra), 360 scale=(50, 50), 361 transition=Image.Transition.IN_LEFT, 362 transition_delay=tval, 363 ).autoretain() 364 assert mkp_name is not None 365 Text( 366 bs.Lstr(value=mkp_name), 367 position=(270, ts_height / 2 - 300 - 30 - 36 + v_extra + 15), 368 h_align=Text.HAlign.LEFT, 369 v_align=Text.VAlign.CENTER, 370 color=bs.safecolor(mkp.team.color + (1,)), 371 maxwidth=180, 372 transition=Text.Transition.IN_LEFT, 373 transition_delay=tval, 374 ).autoretain() 375 tval += 4 * t_incr 376 377 # Now show individual scores. 378 tdelay = tval 379 Text( 380 bs.Lstr(resource='finalScoresText'), 381 color=(0.5, 0.5, 0.5, 1.0), 382 position=(ts_h_offs, ts_height / 2), 383 transition=Text.Transition.IN_RIGHT, 384 transition_delay=tdelay, 385 ).autoretain() 386 tdelay += 4 * t_incr 387 388 v_offs = 0.0 389 tdelay += len(player_entries) * 8 * t_incr 390 for _score, name, prec in player_entries: 391 if not prec.player.in_game: 392 continue 393 tdelay -= 4 * t_incr 394 v_offs -= 40 395 Text( 396 ( 397 str(prec.team.customdata['score']) 398 if self._is_ffa 399 else str(prec.score) 400 ), 401 color=(0.5, 0.5, 0.5, 1.0), 402 position=(ts_h_offs + 230, ts_height / 2 + v_offs), 403 h_align=Text.HAlign.RIGHT, 404 transition=Text.Transition.IN_RIGHT, 405 transition_delay=tdelay, 406 ).autoretain() 407 tdelay -= 4 * t_incr 408 409 Image( 410 prec.get_icon(), 411 position=(ts_h_offs - 72, ts_height / 2 + v_offs + 15), 412 scale=(30, 30), 413 transition=Image.Transition.IN_LEFT, 414 transition_delay=tdelay, 415 ).autoretain() 416 Text( 417 bs.Lstr(value=name), 418 position=(ts_h_offs - 50, ts_height / 2 + v_offs + 15), 419 h_align=Text.HAlign.LEFT, 420 v_align=Text.VAlign.CENTER, 421 maxwidth=180, 422 color=bs.safecolor(prec.team.color + (1,)), 423 transition=Text.Transition.IN_RIGHT, 424 transition_delay=tdelay, 425 ).autoretain() 426 427 bs.timer(15.0, bs.WeakCall(self._show_tips))
Called once the previous Activity has finished transitioning out.
At this point the activity's initial players and teams are filled in and it should begin its actual game logic.
Inherited Members
- bascenev1._activitytypes.ScoreScreenActivity
- transition_time
- inherits_tint
- inherits_vr_camera_offset
- use_fixed_vr_overlay
- on_player_join
- on_transition_in
- bascenev1._activity.Activity
- settings_raw
- teams
- players
- announce_player_deaths
- is_joining_activity
- allow_pausing
- allow_kick_idle_players
- slow_motion
- inherits_slow_motion
- inherits_music
- inherits_vr_overlay_center
- allow_mid_activity_joins
- can_show_ad_on_death
- paused_text
- preloads
- lobby
- context
- globalsnode
- stats
- on_expire
- customdata
- expired
- playertype
- teamtype
- retain_actor
- add_actor_weak_ref
- session
- on_player_leave
- on_team_join
- on_team_leave
- on_transition_out
- handlemessage
- has_transitioned_in
- has_begun
- has_ended
- is_transitioning_out
- transition_out
- end
- create_player
- create_team
- bascenev1._dependency.DependencyComponent
- dep_is_present
- get_dynamic_deps