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 tdelay -= 4 * t_incr 391 v_offs -= 40 392 Text( 393 ( 394 str(prec.team.customdata['score']) 395 if self._is_ffa 396 else str(prec.score) 397 ), 398 color=(0.5, 0.5, 0.5, 1.0), 399 position=(ts_h_offs + 230, ts_height / 2 + v_offs), 400 h_align=Text.HAlign.RIGHT, 401 transition=Text.Transition.IN_RIGHT, 402 transition_delay=tdelay, 403 ).autoretain() 404 tdelay -= 4 * t_incr 405 406 Image( 407 prec.get_icon(), 408 position=(ts_h_offs - 72, ts_height / 2 + v_offs + 15), 409 scale=(30, 30), 410 transition=Image.Transition.IN_LEFT, 411 transition_delay=tdelay, 412 ).autoretain() 413 Text( 414 bs.Lstr(value=name), 415 position=(ts_h_offs - 50, ts_height / 2 + v_offs + 15), 416 h_align=Text.HAlign.LEFT, 417 v_align=Text.VAlign.CENTER, 418 maxwidth=180, 419 color=bs.safecolor(prec.team.color + (1,)), 420 transition=Text.Transition.IN_RIGHT, 421 transition_delay=tdelay, 422 ).autoretain() 423 424 bs.timer(15.0, bs.WeakCall(self._show_tips)) 425 426 def _show_tips(self) -> None: 427 from bascenev1lib.actor.tipstext import TipsText 428 429 self._tips_text = TipsText(offs_y=70) 430 431 def _play_victory_music(self) -> None: 432 # Make sure we don't stomp on the next activity's music choice. 433 if not self.is_transitioning_out(): 434 bs.setmusic(bs.MusicType.VICTORY) 435 436 def _show_winner(self, team: bs.SessionTeam) -> None: 437 from bascenev1lib.actor.image import Image 438 from bascenev1lib.actor.zoomtext import ZoomText 439 440 if not self._is_ffa: 441 offs_v = 0.0 442 ZoomText( 443 team.name, 444 position=(0, 97), 445 color=team.color, 446 scale=1.15, 447 jitter=1.0, 448 maxwidth=250, 449 ).autoretain() 450 else: 451 offs_v = -80 452 assert isinstance(self.session, bs.MultiTeamSession) 453 series_length = self.session.get_ffa_series_length() 454 icon: dict | None 455 # Pull live player info if they're still around. 456 if len(team.players) == 1: 457 icon = team.players[0].get_icon() 458 player_name = team.players[0].getname(full=True, icon=False) 459 # Otherwise use the special info we stored when we came in. 460 elif ( 461 self._ffa_top_player_info is not None 462 and self._ffa_top_player_info[0] >= series_length 463 ): 464 icon = self._ffa_top_player_info[2] 465 player_name = self._ffa_top_player_info[1] 466 else: 467 icon = None 468 player_name = 'Player Not Found' 469 470 if icon is not None: 471 i = Image( 472 icon, 473 position=(0, 143), 474 scale=(100, 100), 475 ).autoretain() 476 assert i.node 477 bs.animate(i.node, 'opacity', {0.0: 0.0, 0.25: 1.0}) 478 479 ZoomText( 480 bs.Lstr(value=player_name), 481 position=(0, 97 + offs_v + (0 if icon is not None else 60)), 482 color=team.color, 483 scale=1.15, 484 jitter=1.0, 485 maxwidth=250, 486 ).autoretain() 487 488 s_extra = 1.0 if self._is_ffa else 1.0 489 490 # Some languages say "FOO WINS" differently for teams vs players. 491 if isinstance(self.session, bs.FreeForAllSession): 492 wins_resource = 'seriesWinLine1PlayerText' 493 else: 494 wins_resource = 'seriesWinLine1TeamText' 495 wins_text = bs.Lstr(resource=wins_resource) 496 497 # Temp - if these come up as the english default, fall-back to the 498 # unified old form which is more likely to be translated. 499 ZoomText( 500 wins_text, 501 position=(0, -10 + offs_v), 502 color=team.color, 503 scale=0.65 * s_extra, 504 jitter=1.0, 505 maxwidth=250, 506 ).autoretain() 507 ZoomText( 508 bs.Lstr(resource='seriesWinLine2Text'), 509 position=(0, -110 + offs_v), 510 scale=1.0 * s_extra, 511 color=team.color, 512 jitter=1.0, 513 maxwidth=250, 514 ).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 tdelay -= 4 * t_incr 392 v_offs -= 40 393 Text( 394 ( 395 str(prec.team.customdata['score']) 396 if self._is_ffa 397 else str(prec.score) 398 ), 399 color=(0.5, 0.5, 0.5, 1.0), 400 position=(ts_h_offs + 230, ts_height / 2 + v_offs), 401 h_align=Text.HAlign.RIGHT, 402 transition=Text.Transition.IN_RIGHT, 403 transition_delay=tdelay, 404 ).autoretain() 405 tdelay -= 4 * t_incr 406 407 Image( 408 prec.get_icon(), 409 position=(ts_h_offs - 72, ts_height / 2 + v_offs + 15), 410 scale=(30, 30), 411 transition=Image.Transition.IN_LEFT, 412 transition_delay=tdelay, 413 ).autoretain() 414 Text( 415 bs.Lstr(value=name), 416 position=(ts_h_offs - 50, ts_height / 2 + v_offs + 15), 417 h_align=Text.HAlign.LEFT, 418 v_align=Text.VAlign.CENTER, 419 maxwidth=180, 420 color=bs.safecolor(prec.team.color + (1,)), 421 transition=Text.Transition.IN_RIGHT, 422 transition_delay=tdelay, 423 ).autoretain() 424 425 bs.timer(15.0, bs.WeakCall(self._show_tips)) 426 427 def _show_tips(self) -> None: 428 from bascenev1lib.actor.tipstext import TipsText 429 430 self._tips_text = TipsText(offs_y=70) 431 432 def _play_victory_music(self) -> None: 433 # Make sure we don't stomp on the next activity's music choice. 434 if not self.is_transitioning_out(): 435 bs.setmusic(bs.MusicType.VICTORY) 436 437 def _show_winner(self, team: bs.SessionTeam) -> None: 438 from bascenev1lib.actor.image import Image 439 from bascenev1lib.actor.zoomtext import ZoomText 440 441 if not self._is_ffa: 442 offs_v = 0.0 443 ZoomText( 444 team.name, 445 position=(0, 97), 446 color=team.color, 447 scale=1.15, 448 jitter=1.0, 449 maxwidth=250, 450 ).autoretain() 451 else: 452 offs_v = -80 453 assert isinstance(self.session, bs.MultiTeamSession) 454 series_length = self.session.get_ffa_series_length() 455 icon: dict | None 456 # Pull live player info if they're still around. 457 if len(team.players) == 1: 458 icon = team.players[0].get_icon() 459 player_name = team.players[0].getname(full=True, icon=False) 460 # Otherwise use the special info we stored when we came in. 461 elif ( 462 self._ffa_top_player_info is not None 463 and self._ffa_top_player_info[0] >= series_length 464 ): 465 icon = self._ffa_top_player_info[2] 466 player_name = self._ffa_top_player_info[1] 467 else: 468 icon = None 469 player_name = 'Player Not Found' 470 471 if icon is not None: 472 i = Image( 473 icon, 474 position=(0, 143), 475 scale=(100, 100), 476 ).autoretain() 477 assert i.node 478 bs.animate(i.node, 'opacity', {0.0: 0.0, 0.25: 1.0}) 479 480 ZoomText( 481 bs.Lstr(value=player_name), 482 position=(0, 97 + offs_v + (0 if icon is not None else 60)), 483 color=team.color, 484 scale=1.15, 485 jitter=1.0, 486 maxwidth=250, 487 ).autoretain() 488 489 s_extra = 1.0 if self._is_ffa else 1.0 490 491 # Some languages say "FOO WINS" differently for teams vs players. 492 if isinstance(self.session, bs.FreeForAllSession): 493 wins_resource = 'seriesWinLine1PlayerText' 494 else: 495 wins_resource = 'seriesWinLine1TeamText' 496 wins_text = bs.Lstr(resource=wins_resource) 497 498 # Temp - if these come up as the english default, fall-back to the 499 # unified old form which is more likely to be translated. 500 ZoomText( 501 wins_text, 502 position=(0, -10 + offs_v), 503 color=team.color, 504 scale=0.65 * s_extra, 505 jitter=1.0, 506 maxwidth=250, 507 ).autoretain() 508 ZoomText( 509 bs.Lstr(resource='seriesWinLine2Text'), 510 position=(0, -110 + offs_v), 511 scale=1.0 * s_extra, 512 color=team.color, 513 jitter=1.0, 514 maxwidth=250, 515 ).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 tdelay -= 4 * t_incr 392 v_offs -= 40 393 Text( 394 ( 395 str(prec.team.customdata['score']) 396 if self._is_ffa 397 else str(prec.score) 398 ), 399 color=(0.5, 0.5, 0.5, 1.0), 400 position=(ts_h_offs + 230, ts_height / 2 + v_offs), 401 h_align=Text.HAlign.RIGHT, 402 transition=Text.Transition.IN_RIGHT, 403 transition_delay=tdelay, 404 ).autoretain() 405 tdelay -= 4 * t_incr 406 407 Image( 408 prec.get_icon(), 409 position=(ts_h_offs - 72, ts_height / 2 + v_offs + 15), 410 scale=(30, 30), 411 transition=Image.Transition.IN_LEFT, 412 transition_delay=tdelay, 413 ).autoretain() 414 Text( 415 bs.Lstr(value=name), 416 position=(ts_h_offs - 50, ts_height / 2 + v_offs + 15), 417 h_align=Text.HAlign.LEFT, 418 v_align=Text.VAlign.CENTER, 419 maxwidth=180, 420 color=bs.safecolor(prec.team.color + (1,)), 421 transition=Text.Transition.IN_RIGHT, 422 transition_delay=tdelay, 423 ).autoretain() 424 425 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.