bascenev1lib.actor.controlsguide
Defines Actors related to controls guides.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Defines Actors related to controls guides.""" 4 5from __future__ import annotations 6 7from typing import TYPE_CHECKING 8 9import bascenev1 as bs 10 11if TYPE_CHECKING: 12 from typing import Any, Sequence 13 14 15class ControlsGuide(bs.Actor): 16 """A screen overlay of game controls. 17 18 category: Gameplay Classes 19 20 Shows button mappings based on what controllers are connected. 21 Handy to show at the start of a series or whenever there might 22 be newbies watching. 23 """ 24 25 def __init__( 26 self, 27 position: tuple[float, float] = (390.0, 120.0), 28 scale: float = 1.0, 29 delay: float = 0.0, 30 lifespan: float | None = None, 31 bright: bool = False, 32 ): 33 """Instantiate an overlay. 34 35 delay: is the time in seconds before the overlay fades in. 36 37 lifespan: if not None, the overlay will fade back out and die after 38 that long (in milliseconds). 39 40 bright: if True, brighter colors will be used; handy when showing 41 over gameplay but may be too bright for join-screens, etc. 42 """ 43 # pylint: disable=too-many-statements 44 # pylint: disable=too-many-locals 45 super().__init__() 46 show_title = True 47 scale *= 0.75 48 image_size = 90.0 * scale 49 offs = 74.0 * scale 50 offs5 = 43.0 * scale 51 ouya = False 52 maxw = 50 53 self._lifespan = lifespan 54 self._dead = False 55 self._bright = bright 56 self._cancel_timer: bs.Timer | None = None 57 self._fade_in_timer: bs.Timer | None = None 58 self._update_timer: bs.Timer | None = None 59 self._title_text: bs.Node | None 60 clr: Sequence[float] 61 punch_pos = (position[0] - offs * 1.1, position[1]) 62 jump_pos = (position[0], position[1] - offs) 63 bomb_pos = (position[0] + offs * 1.1, position[1]) 64 pickup_pos = (position[0], position[1] + offs) 65 self._force_hide_button_names = False 66 67 if show_title: 68 self._title_text_pos_top = ( 69 position[0], 70 position[1] + 139.0 * scale, 71 ) 72 self._title_text_pos_bottom = ( 73 position[0], 74 position[1] + 139.0 * scale, 75 ) 76 clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7) 77 tval = bs.Lstr( 78 value='${A}:', subs=[('${A}', bs.Lstr(resource='controlsText'))] 79 ) 80 self._title_text = bs.newnode( 81 'text', 82 attrs={ 83 'text': tval, 84 'host_only': True, 85 'scale': 1.1 * scale, 86 'shadow': 0.5, 87 'flatness': 1.0, 88 'maxwidth': 480, 89 'v_align': 'center', 90 'h_align': 'center', 91 'color': clr, 92 }, 93 ) 94 else: 95 self._title_text = None 96 pos = jump_pos 97 clr = (0.4, 1, 0.4) 98 self._jump_image = bs.newnode( 99 'image', 100 attrs={ 101 'texture': bs.gettexture('buttonJump'), 102 'absolute_scale': True, 103 'host_only': True, 104 'vr_depth': 10, 105 'position': pos, 106 'scale': (image_size, image_size), 107 'color': clr, 108 }, 109 ) 110 self._jump_text = bs.newnode( 111 'text', 112 attrs={ 113 'v_align': 'top', 114 'h_align': 'center', 115 'scale': 1.5 * scale, 116 'flatness': 1.0, 117 'host_only': True, 118 'shadow': 1.0, 119 'maxwidth': maxw, 120 'position': (pos[0], pos[1] - offs5), 121 'color': clr, 122 }, 123 ) 124 clr = (0.2, 0.6, 1) if ouya else (1, 0.7, 0.3) 125 pos = punch_pos 126 self._punch_image = bs.newnode( 127 'image', 128 attrs={ 129 'texture': bs.gettexture('buttonPunch'), 130 'absolute_scale': True, 131 'host_only': True, 132 'vr_depth': 10, 133 'position': pos, 134 'scale': (image_size, image_size), 135 'color': clr, 136 }, 137 ) 138 self._punch_text = bs.newnode( 139 'text', 140 attrs={ 141 'v_align': 'top', 142 'h_align': 'center', 143 'scale': 1.5 * scale, 144 'flatness': 1.0, 145 'host_only': True, 146 'shadow': 1.0, 147 'maxwidth': maxw, 148 'position': (pos[0], pos[1] - offs5), 149 'color': clr, 150 }, 151 ) 152 pos = bomb_pos 153 clr = (1, 0.3, 0.3) 154 self._bomb_image = bs.newnode( 155 'image', 156 attrs={ 157 'texture': bs.gettexture('buttonBomb'), 158 'absolute_scale': True, 159 'host_only': True, 160 'vr_depth': 10, 161 'position': pos, 162 'scale': (image_size, image_size), 163 'color': clr, 164 }, 165 ) 166 self._bomb_text = bs.newnode( 167 'text', 168 attrs={ 169 'h_align': 'center', 170 'v_align': 'top', 171 'scale': 1.5 * scale, 172 'flatness': 1.0, 173 'host_only': True, 174 'shadow': 1.0, 175 'maxwidth': maxw, 176 'position': (pos[0], pos[1] - offs5), 177 'color': clr, 178 }, 179 ) 180 pos = pickup_pos 181 clr = (1, 0.8, 0.3) if ouya else (0.8, 0.5, 1) 182 self._pickup_image = bs.newnode( 183 'image', 184 attrs={ 185 'texture': bs.gettexture('buttonPickUp'), 186 'absolute_scale': True, 187 'host_only': True, 188 'vr_depth': 10, 189 'position': pos, 190 'scale': (image_size, image_size), 191 'color': clr, 192 }, 193 ) 194 self._pick_up_text = bs.newnode( 195 'text', 196 attrs={ 197 'v_align': 'top', 198 'h_align': 'center', 199 'scale': 1.5 * scale, 200 'flatness': 1.0, 201 'host_only': True, 202 'shadow': 1.0, 203 'maxwidth': maxw, 204 'position': (pos[0], pos[1] - offs5), 205 'color': clr, 206 }, 207 ) 208 clr = (0.9, 0.9, 2.0, 1.0) if bright else (0.8, 0.8, 2.0, 1.0) 209 self._run_text_pos_top = (position[0], position[1] - 135.0 * scale) 210 self._run_text_pos_bottom = (position[0], position[1] - 172.0 * scale) 211 sval = 1.0 * scale if bs.app.env.vr else 0.8 * scale 212 self._run_text = bs.newnode( 213 'text', 214 attrs={ 215 'scale': sval, 216 'host_only': True, 217 'shadow': 1.0 if bs.app.env.vr else 0.5, 218 'flatness': 1.0, 219 'maxwidth': 380, 220 'v_align': 'top', 221 'h_align': 'center', 222 'color': clr, 223 }, 224 ) 225 clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7) 226 self._extra_text = bs.newnode( 227 'text', 228 attrs={ 229 'scale': 0.8 * scale, 230 'host_only': True, 231 'shadow': 0.5, 232 'flatness': 1.0, 233 'maxwidth': 380, 234 'v_align': 'top', 235 'h_align': 'center', 236 'color': clr, 237 }, 238 ) 239 240 self._extra_image_1 = None 241 self._extra_image_2 = None 242 243 self._nodes = [ 244 self._bomb_image, 245 self._bomb_text, 246 self._punch_image, 247 self._punch_text, 248 self._jump_image, 249 self._jump_text, 250 self._pickup_image, 251 self._pick_up_text, 252 self._run_text, 253 self._extra_text, 254 ] 255 if show_title: 256 assert self._title_text 257 self._nodes.append(self._title_text) 258 259 # Start everything invisible. 260 for node in self._nodes: 261 node.opacity = 0.0 262 263 # Don't do anything until our delay has passed. 264 bs.timer(delay, bs.WeakCall(self._start_updating)) 265 266 @staticmethod 267 def _meaningful_button_name(device: bs.InputDevice, button: int) -> str: 268 """Return a flattened string button name; empty for non-meaningful.""" 269 if not device.has_meaningful_button_names: 270 return '' 271 return device.get_button_name(button).evaluate() 272 273 def _start_updating(self) -> None: 274 # Ok, our delay has passed. Now lets periodically see if we can fade 275 # in (if a touch-screen is present we only want to show up if gamepads 276 # are connected, etc). 277 # Also set up a timer so if we haven't faded in by the end of our 278 # duration, abort. 279 if self._lifespan is not None: 280 self._cancel_timer = bs.Timer( 281 self._lifespan, 282 bs.WeakCall(self.handlemessage, bs.DieMessage(immediate=True)), 283 ) 284 self._fade_in_timer = bs.Timer( 285 1.0, bs.WeakCall(self._check_fade_in), repeat=True 286 ) 287 self._check_fade_in() # Do one check immediately. 288 289 def _check_fade_in(self) -> None: 290 assert bs.app.classic is not None 291 292 # If we have a touchscreen, we only fade in if we have a player with 293 # an input device that is *not* the touchscreen. 294 # (otherwise it is confusing to see the touchscreen buttons right 295 # next to our display buttons) 296 touchscreen: bs.InputDevice | None = bs.getinputdevice( 297 'TouchScreen', '#1', doraise=False 298 ) 299 300 if touchscreen is not None: 301 # We look at the session's players; not the activity's. 302 # We want to get ones who are still in the process of 303 # selecting a character, etc. 304 input_devices = [ 305 p.inputdevice for p in bs.getsession().sessionplayers 306 ] 307 input_devices = [ 308 i for i in input_devices if i and i is not touchscreen 309 ] 310 fade_in = False 311 if input_devices: 312 # Only count this one if it has non-empty button names 313 # (filters out wiimotes, the remote-app, etc). 314 for device in input_devices: 315 for name in ( 316 'buttonPunch', 317 'buttonJump', 318 'buttonBomb', 319 'buttonPickUp', 320 ): 321 if ( 322 self._meaningful_button_name( 323 device, 324 bs.app.classic.get_input_device_mapped_value( 325 device, name 326 ), 327 ) 328 != '' 329 ): 330 fade_in = True 331 break 332 if fade_in: 333 break # No need to keep looking. 334 else: 335 # No touch-screen; fade in immediately. 336 fade_in = True 337 if fade_in: 338 self._cancel_timer = None # Didn't need this. 339 self._fade_in_timer = None # Done with this. 340 self._fade_in() 341 342 def _fade_in(self) -> None: 343 for node in self._nodes: 344 bs.animate(node, 'opacity', {0: 0.0, 2.0: 1.0}) 345 346 # If we were given a lifespan, transition out after it. 347 if self._lifespan is not None: 348 bs.timer( 349 self._lifespan, bs.WeakCall(self.handlemessage, bs.DieMessage()) 350 ) 351 self._update() 352 self._update_timer = bs.Timer( 353 1.0, bs.WeakCall(self._update), repeat=True 354 ) 355 356 def _update(self) -> None: 357 # pylint: disable=too-many-statements 358 # pylint: disable=too-many-branches 359 # pylint: disable=too-many-locals 360 361 if self._dead: 362 return 363 364 classic = bs.app.classic 365 assert classic is not None 366 367 punch_button_names = set() 368 jump_button_names = set() 369 pickup_button_names = set() 370 bomb_button_names = set() 371 372 # We look at the session's players; not the activity's - we want to 373 # get ones who are still in the process of selecting a character, etc. 374 input_devices = [p.inputdevice for p in bs.getsession().sessionplayers] 375 input_devices = [i for i in input_devices if i] 376 377 # If there's no players with input devices yet, try to default to 378 # showing keyboard controls. 379 if not input_devices: 380 kbd = bs.getinputdevice('Keyboard', '#1', doraise=False) 381 if kbd is not None: 382 input_devices.append(kbd) 383 384 # We word things specially if we have nothing but keyboards. 385 all_keyboards = input_devices and all( 386 i.name == 'Keyboard' for i in input_devices 387 ) 388 only_remote = len(input_devices) == 1 and all( 389 i.name == 'Amazon Fire TV Remote' for i in input_devices 390 ) 391 392 right_button_names = set() 393 left_button_names = set() 394 up_button_names = set() 395 down_button_names = set() 396 397 # For each player in the game with an input device, 398 # get the name of the button for each of these 4 actions. 399 # If any of them are uniform across all devices, display the name. 400 for device in input_devices: 401 # We only care about movement buttons in the case of keyboards. 402 if all_keyboards: 403 right_button_names.add( 404 device.get_button_name( 405 classic.get_input_device_mapped_value( 406 device, 'buttonRight' 407 ) 408 ) 409 ) 410 left_button_names.add( 411 device.get_button_name( 412 classic.get_input_device_mapped_value( 413 device, 'buttonLeft' 414 ) 415 ) 416 ) 417 down_button_names.add( 418 device.get_button_name( 419 classic.get_input_device_mapped_value( 420 device, 'buttonDown' 421 ) 422 ) 423 ) 424 up_button_names.add( 425 device.get_button_name( 426 classic.get_input_device_mapped_value( 427 device, 'buttonUp' 428 ) 429 ) 430 ) 431 432 # Ignore empty values; things like the remote app or 433 # wiimotes can return these. 434 bname = self._meaningful_button_name( 435 device, 436 classic.get_input_device_mapped_value(device, 'buttonPunch'), 437 ) 438 if bname != '': 439 punch_button_names.add(bname) 440 bname = self._meaningful_button_name( 441 device, 442 classic.get_input_device_mapped_value(device, 'buttonJump'), 443 ) 444 if bname != '': 445 jump_button_names.add(bname) 446 bname = self._meaningful_button_name( 447 device, 448 classic.get_input_device_mapped_value(device, 'buttonBomb'), 449 ) 450 if bname != '': 451 bomb_button_names.add(bname) 452 bname = self._meaningful_button_name( 453 device, 454 classic.get_input_device_mapped_value(device, 'buttonPickUp'), 455 ) 456 if bname != '': 457 pickup_button_names.add(bname) 458 459 # If we have no values yet, we may want to throw out some sane 460 # defaults. 461 if all( 462 not lst 463 for lst in ( 464 punch_button_names, 465 jump_button_names, 466 bomb_button_names, 467 pickup_button_names, 468 ) 469 ): 470 # Otherwise on android show standard buttons. 471 if classic.platform == 'android': 472 punch_button_names.add('X') 473 jump_button_names.add('A') 474 bomb_button_names.add('B') 475 pickup_button_names.add('Y') 476 477 run_text = bs.Lstr( 478 value='${R}: ${B}', 479 subs=[ 480 ('${R}', bs.Lstr(resource='runText')), 481 ( 482 '${B}', 483 bs.Lstr( 484 resource='holdAnyKeyText' 485 if all_keyboards 486 else 'holdAnyButtonText' 487 ), 488 ), 489 ], 490 ) 491 492 # If we're all keyboards, lets show move keys too. 493 if ( 494 all_keyboards 495 and len(up_button_names) == 1 496 and len(down_button_names) == 1 497 and len(left_button_names) == 1 498 and len(right_button_names) == 1 499 ): 500 up_text = list(up_button_names)[0] 501 down_text = list(down_button_names)[0] 502 left_text = list(left_button_names)[0] 503 right_text = list(right_button_names)[0] 504 run_text = bs.Lstr( 505 value='${M}: ${U}, ${L}, ${D}, ${R}\n${RUN}', 506 subs=[ 507 ('${M}', bs.Lstr(resource='moveText')), 508 ('${U}', up_text), 509 ('${L}', left_text), 510 ('${D}', down_text), 511 ('${R}', right_text), 512 ('${RUN}', run_text), 513 ], 514 ) 515 516 if self._force_hide_button_names: 517 jump_button_names.clear() 518 punch_button_names.clear() 519 bomb_button_names.clear() 520 pickup_button_names.clear() 521 522 self._run_text.text = run_text 523 w_text: bs.Lstr | str 524 if only_remote and self._lifespan is None: 525 w_text = bs.Lstr( 526 resource='fireTVRemoteWarningText', 527 subs=[('${REMOTE_APP_NAME}', bs.get_remote_app_name())], 528 ) 529 else: 530 w_text = '' 531 self._extra_text.text = w_text 532 if len(punch_button_names) == 1: 533 self._punch_text.text = list(punch_button_names)[0] 534 else: 535 self._punch_text.text = '' 536 537 if len(jump_button_names) == 1: 538 tval = list(jump_button_names)[0] 539 else: 540 tval = '' 541 self._jump_text.text = tval 542 if tval == '': 543 self._run_text.position = self._run_text_pos_top 544 self._extra_text.position = ( 545 self._run_text_pos_top[0], 546 self._run_text_pos_top[1] - 50, 547 ) 548 else: 549 self._run_text.position = self._run_text_pos_bottom 550 self._extra_text.position = ( 551 self._run_text_pos_bottom[0], 552 self._run_text_pos_bottom[1] - 50, 553 ) 554 if len(bomb_button_names) == 1: 555 self._bomb_text.text = list(bomb_button_names)[0] 556 else: 557 self._bomb_text.text = '' 558 559 # Also move our title up/down depending on if this is shown. 560 if len(pickup_button_names) == 1: 561 self._pick_up_text.text = list(pickup_button_names)[0] 562 if self._title_text is not None: 563 self._title_text.position = self._title_text_pos_top 564 else: 565 self._pick_up_text.text = '' 566 if self._title_text is not None: 567 self._title_text.position = self._title_text_pos_bottom 568 569 def _die(self) -> None: 570 for node in self._nodes: 571 node.delete() 572 self._nodes = [] 573 self._update_timer = None 574 self._dead = True 575 576 def exists(self) -> bool: 577 return not self._dead 578 579 def handlemessage(self, msg: Any) -> Any: 580 assert not self.expired 581 if isinstance(msg, bs.DieMessage): 582 if msg.immediate: 583 self._die() 584 else: 585 # If they don't need immediate, 586 # fade out our nodes and die later. 587 for node in self._nodes: 588 bs.animate(node, 'opacity', {0: node.opacity, 3.0: 0.0}) 589 bs.timer(3.1, bs.WeakCall(self._die)) 590 return None 591 return super().handlemessage(msg)
16class ControlsGuide(bs.Actor): 17 """A screen overlay of game controls. 18 19 category: Gameplay Classes 20 21 Shows button mappings based on what controllers are connected. 22 Handy to show at the start of a series or whenever there might 23 be newbies watching. 24 """ 25 26 def __init__( 27 self, 28 position: tuple[float, float] = (390.0, 120.0), 29 scale: float = 1.0, 30 delay: float = 0.0, 31 lifespan: float | None = None, 32 bright: bool = False, 33 ): 34 """Instantiate an overlay. 35 36 delay: is the time in seconds before the overlay fades in. 37 38 lifespan: if not None, the overlay will fade back out and die after 39 that long (in milliseconds). 40 41 bright: if True, brighter colors will be used; handy when showing 42 over gameplay but may be too bright for join-screens, etc. 43 """ 44 # pylint: disable=too-many-statements 45 # pylint: disable=too-many-locals 46 super().__init__() 47 show_title = True 48 scale *= 0.75 49 image_size = 90.0 * scale 50 offs = 74.0 * scale 51 offs5 = 43.0 * scale 52 ouya = False 53 maxw = 50 54 self._lifespan = lifespan 55 self._dead = False 56 self._bright = bright 57 self._cancel_timer: bs.Timer | None = None 58 self._fade_in_timer: bs.Timer | None = None 59 self._update_timer: bs.Timer | None = None 60 self._title_text: bs.Node | None 61 clr: Sequence[float] 62 punch_pos = (position[0] - offs * 1.1, position[1]) 63 jump_pos = (position[0], position[1] - offs) 64 bomb_pos = (position[0] + offs * 1.1, position[1]) 65 pickup_pos = (position[0], position[1] + offs) 66 self._force_hide_button_names = False 67 68 if show_title: 69 self._title_text_pos_top = ( 70 position[0], 71 position[1] + 139.0 * scale, 72 ) 73 self._title_text_pos_bottom = ( 74 position[0], 75 position[1] + 139.0 * scale, 76 ) 77 clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7) 78 tval = bs.Lstr( 79 value='${A}:', subs=[('${A}', bs.Lstr(resource='controlsText'))] 80 ) 81 self._title_text = bs.newnode( 82 'text', 83 attrs={ 84 'text': tval, 85 'host_only': True, 86 'scale': 1.1 * scale, 87 'shadow': 0.5, 88 'flatness': 1.0, 89 'maxwidth': 480, 90 'v_align': 'center', 91 'h_align': 'center', 92 'color': clr, 93 }, 94 ) 95 else: 96 self._title_text = None 97 pos = jump_pos 98 clr = (0.4, 1, 0.4) 99 self._jump_image = bs.newnode( 100 'image', 101 attrs={ 102 'texture': bs.gettexture('buttonJump'), 103 'absolute_scale': True, 104 'host_only': True, 105 'vr_depth': 10, 106 'position': pos, 107 'scale': (image_size, image_size), 108 'color': clr, 109 }, 110 ) 111 self._jump_text = bs.newnode( 112 'text', 113 attrs={ 114 'v_align': 'top', 115 'h_align': 'center', 116 'scale': 1.5 * scale, 117 'flatness': 1.0, 118 'host_only': True, 119 'shadow': 1.0, 120 'maxwidth': maxw, 121 'position': (pos[0], pos[1] - offs5), 122 'color': clr, 123 }, 124 ) 125 clr = (0.2, 0.6, 1) if ouya else (1, 0.7, 0.3) 126 pos = punch_pos 127 self._punch_image = bs.newnode( 128 'image', 129 attrs={ 130 'texture': bs.gettexture('buttonPunch'), 131 'absolute_scale': True, 132 'host_only': True, 133 'vr_depth': 10, 134 'position': pos, 135 'scale': (image_size, image_size), 136 'color': clr, 137 }, 138 ) 139 self._punch_text = bs.newnode( 140 'text', 141 attrs={ 142 'v_align': 'top', 143 'h_align': 'center', 144 'scale': 1.5 * scale, 145 'flatness': 1.0, 146 'host_only': True, 147 'shadow': 1.0, 148 'maxwidth': maxw, 149 'position': (pos[0], pos[1] - offs5), 150 'color': clr, 151 }, 152 ) 153 pos = bomb_pos 154 clr = (1, 0.3, 0.3) 155 self._bomb_image = bs.newnode( 156 'image', 157 attrs={ 158 'texture': bs.gettexture('buttonBomb'), 159 'absolute_scale': True, 160 'host_only': True, 161 'vr_depth': 10, 162 'position': pos, 163 'scale': (image_size, image_size), 164 'color': clr, 165 }, 166 ) 167 self._bomb_text = bs.newnode( 168 'text', 169 attrs={ 170 'h_align': 'center', 171 'v_align': 'top', 172 'scale': 1.5 * scale, 173 'flatness': 1.0, 174 'host_only': True, 175 'shadow': 1.0, 176 'maxwidth': maxw, 177 'position': (pos[0], pos[1] - offs5), 178 'color': clr, 179 }, 180 ) 181 pos = pickup_pos 182 clr = (1, 0.8, 0.3) if ouya else (0.8, 0.5, 1) 183 self._pickup_image = bs.newnode( 184 'image', 185 attrs={ 186 'texture': bs.gettexture('buttonPickUp'), 187 'absolute_scale': True, 188 'host_only': True, 189 'vr_depth': 10, 190 'position': pos, 191 'scale': (image_size, image_size), 192 'color': clr, 193 }, 194 ) 195 self._pick_up_text = bs.newnode( 196 'text', 197 attrs={ 198 'v_align': 'top', 199 'h_align': 'center', 200 'scale': 1.5 * scale, 201 'flatness': 1.0, 202 'host_only': True, 203 'shadow': 1.0, 204 'maxwidth': maxw, 205 'position': (pos[0], pos[1] - offs5), 206 'color': clr, 207 }, 208 ) 209 clr = (0.9, 0.9, 2.0, 1.0) if bright else (0.8, 0.8, 2.0, 1.0) 210 self._run_text_pos_top = (position[0], position[1] - 135.0 * scale) 211 self._run_text_pos_bottom = (position[0], position[1] - 172.0 * scale) 212 sval = 1.0 * scale if bs.app.env.vr else 0.8 * scale 213 self._run_text = bs.newnode( 214 'text', 215 attrs={ 216 'scale': sval, 217 'host_only': True, 218 'shadow': 1.0 if bs.app.env.vr else 0.5, 219 'flatness': 1.0, 220 'maxwidth': 380, 221 'v_align': 'top', 222 'h_align': 'center', 223 'color': clr, 224 }, 225 ) 226 clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7) 227 self._extra_text = bs.newnode( 228 'text', 229 attrs={ 230 'scale': 0.8 * scale, 231 'host_only': True, 232 'shadow': 0.5, 233 'flatness': 1.0, 234 'maxwidth': 380, 235 'v_align': 'top', 236 'h_align': 'center', 237 'color': clr, 238 }, 239 ) 240 241 self._extra_image_1 = None 242 self._extra_image_2 = None 243 244 self._nodes = [ 245 self._bomb_image, 246 self._bomb_text, 247 self._punch_image, 248 self._punch_text, 249 self._jump_image, 250 self._jump_text, 251 self._pickup_image, 252 self._pick_up_text, 253 self._run_text, 254 self._extra_text, 255 ] 256 if show_title: 257 assert self._title_text 258 self._nodes.append(self._title_text) 259 260 # Start everything invisible. 261 for node in self._nodes: 262 node.opacity = 0.0 263 264 # Don't do anything until our delay has passed. 265 bs.timer(delay, bs.WeakCall(self._start_updating)) 266 267 @staticmethod 268 def _meaningful_button_name(device: bs.InputDevice, button: int) -> str: 269 """Return a flattened string button name; empty for non-meaningful.""" 270 if not device.has_meaningful_button_names: 271 return '' 272 return device.get_button_name(button).evaluate() 273 274 def _start_updating(self) -> None: 275 # Ok, our delay has passed. Now lets periodically see if we can fade 276 # in (if a touch-screen is present we only want to show up if gamepads 277 # are connected, etc). 278 # Also set up a timer so if we haven't faded in by the end of our 279 # duration, abort. 280 if self._lifespan is not None: 281 self._cancel_timer = bs.Timer( 282 self._lifespan, 283 bs.WeakCall(self.handlemessage, bs.DieMessage(immediate=True)), 284 ) 285 self._fade_in_timer = bs.Timer( 286 1.0, bs.WeakCall(self._check_fade_in), repeat=True 287 ) 288 self._check_fade_in() # Do one check immediately. 289 290 def _check_fade_in(self) -> None: 291 assert bs.app.classic is not None 292 293 # If we have a touchscreen, we only fade in if we have a player with 294 # an input device that is *not* the touchscreen. 295 # (otherwise it is confusing to see the touchscreen buttons right 296 # next to our display buttons) 297 touchscreen: bs.InputDevice | None = bs.getinputdevice( 298 'TouchScreen', '#1', doraise=False 299 ) 300 301 if touchscreen is not None: 302 # We look at the session's players; not the activity's. 303 # We want to get ones who are still in the process of 304 # selecting a character, etc. 305 input_devices = [ 306 p.inputdevice for p in bs.getsession().sessionplayers 307 ] 308 input_devices = [ 309 i for i in input_devices if i and i is not touchscreen 310 ] 311 fade_in = False 312 if input_devices: 313 # Only count this one if it has non-empty button names 314 # (filters out wiimotes, the remote-app, etc). 315 for device in input_devices: 316 for name in ( 317 'buttonPunch', 318 'buttonJump', 319 'buttonBomb', 320 'buttonPickUp', 321 ): 322 if ( 323 self._meaningful_button_name( 324 device, 325 bs.app.classic.get_input_device_mapped_value( 326 device, name 327 ), 328 ) 329 != '' 330 ): 331 fade_in = True 332 break 333 if fade_in: 334 break # No need to keep looking. 335 else: 336 # No touch-screen; fade in immediately. 337 fade_in = True 338 if fade_in: 339 self._cancel_timer = None # Didn't need this. 340 self._fade_in_timer = None # Done with this. 341 self._fade_in() 342 343 def _fade_in(self) -> None: 344 for node in self._nodes: 345 bs.animate(node, 'opacity', {0: 0.0, 2.0: 1.0}) 346 347 # If we were given a lifespan, transition out after it. 348 if self._lifespan is not None: 349 bs.timer( 350 self._lifespan, bs.WeakCall(self.handlemessage, bs.DieMessage()) 351 ) 352 self._update() 353 self._update_timer = bs.Timer( 354 1.0, bs.WeakCall(self._update), repeat=True 355 ) 356 357 def _update(self) -> None: 358 # pylint: disable=too-many-statements 359 # pylint: disable=too-many-branches 360 # pylint: disable=too-many-locals 361 362 if self._dead: 363 return 364 365 classic = bs.app.classic 366 assert classic is not None 367 368 punch_button_names = set() 369 jump_button_names = set() 370 pickup_button_names = set() 371 bomb_button_names = set() 372 373 # We look at the session's players; not the activity's - we want to 374 # get ones who are still in the process of selecting a character, etc. 375 input_devices = [p.inputdevice for p in bs.getsession().sessionplayers] 376 input_devices = [i for i in input_devices if i] 377 378 # If there's no players with input devices yet, try to default to 379 # showing keyboard controls. 380 if not input_devices: 381 kbd = bs.getinputdevice('Keyboard', '#1', doraise=False) 382 if kbd is not None: 383 input_devices.append(kbd) 384 385 # We word things specially if we have nothing but keyboards. 386 all_keyboards = input_devices and all( 387 i.name == 'Keyboard' for i in input_devices 388 ) 389 only_remote = len(input_devices) == 1 and all( 390 i.name == 'Amazon Fire TV Remote' for i in input_devices 391 ) 392 393 right_button_names = set() 394 left_button_names = set() 395 up_button_names = set() 396 down_button_names = set() 397 398 # For each player in the game with an input device, 399 # get the name of the button for each of these 4 actions. 400 # If any of them are uniform across all devices, display the name. 401 for device in input_devices: 402 # We only care about movement buttons in the case of keyboards. 403 if all_keyboards: 404 right_button_names.add( 405 device.get_button_name( 406 classic.get_input_device_mapped_value( 407 device, 'buttonRight' 408 ) 409 ) 410 ) 411 left_button_names.add( 412 device.get_button_name( 413 classic.get_input_device_mapped_value( 414 device, 'buttonLeft' 415 ) 416 ) 417 ) 418 down_button_names.add( 419 device.get_button_name( 420 classic.get_input_device_mapped_value( 421 device, 'buttonDown' 422 ) 423 ) 424 ) 425 up_button_names.add( 426 device.get_button_name( 427 classic.get_input_device_mapped_value( 428 device, 'buttonUp' 429 ) 430 ) 431 ) 432 433 # Ignore empty values; things like the remote app or 434 # wiimotes can return these. 435 bname = self._meaningful_button_name( 436 device, 437 classic.get_input_device_mapped_value(device, 'buttonPunch'), 438 ) 439 if bname != '': 440 punch_button_names.add(bname) 441 bname = self._meaningful_button_name( 442 device, 443 classic.get_input_device_mapped_value(device, 'buttonJump'), 444 ) 445 if bname != '': 446 jump_button_names.add(bname) 447 bname = self._meaningful_button_name( 448 device, 449 classic.get_input_device_mapped_value(device, 'buttonBomb'), 450 ) 451 if bname != '': 452 bomb_button_names.add(bname) 453 bname = self._meaningful_button_name( 454 device, 455 classic.get_input_device_mapped_value(device, 'buttonPickUp'), 456 ) 457 if bname != '': 458 pickup_button_names.add(bname) 459 460 # If we have no values yet, we may want to throw out some sane 461 # defaults. 462 if all( 463 not lst 464 for lst in ( 465 punch_button_names, 466 jump_button_names, 467 bomb_button_names, 468 pickup_button_names, 469 ) 470 ): 471 # Otherwise on android show standard buttons. 472 if classic.platform == 'android': 473 punch_button_names.add('X') 474 jump_button_names.add('A') 475 bomb_button_names.add('B') 476 pickup_button_names.add('Y') 477 478 run_text = bs.Lstr( 479 value='${R}: ${B}', 480 subs=[ 481 ('${R}', bs.Lstr(resource='runText')), 482 ( 483 '${B}', 484 bs.Lstr( 485 resource='holdAnyKeyText' 486 if all_keyboards 487 else 'holdAnyButtonText' 488 ), 489 ), 490 ], 491 ) 492 493 # If we're all keyboards, lets show move keys too. 494 if ( 495 all_keyboards 496 and len(up_button_names) == 1 497 and len(down_button_names) == 1 498 and len(left_button_names) == 1 499 and len(right_button_names) == 1 500 ): 501 up_text = list(up_button_names)[0] 502 down_text = list(down_button_names)[0] 503 left_text = list(left_button_names)[0] 504 right_text = list(right_button_names)[0] 505 run_text = bs.Lstr( 506 value='${M}: ${U}, ${L}, ${D}, ${R}\n${RUN}', 507 subs=[ 508 ('${M}', bs.Lstr(resource='moveText')), 509 ('${U}', up_text), 510 ('${L}', left_text), 511 ('${D}', down_text), 512 ('${R}', right_text), 513 ('${RUN}', run_text), 514 ], 515 ) 516 517 if self._force_hide_button_names: 518 jump_button_names.clear() 519 punch_button_names.clear() 520 bomb_button_names.clear() 521 pickup_button_names.clear() 522 523 self._run_text.text = run_text 524 w_text: bs.Lstr | str 525 if only_remote and self._lifespan is None: 526 w_text = bs.Lstr( 527 resource='fireTVRemoteWarningText', 528 subs=[('${REMOTE_APP_NAME}', bs.get_remote_app_name())], 529 ) 530 else: 531 w_text = '' 532 self._extra_text.text = w_text 533 if len(punch_button_names) == 1: 534 self._punch_text.text = list(punch_button_names)[0] 535 else: 536 self._punch_text.text = '' 537 538 if len(jump_button_names) == 1: 539 tval = list(jump_button_names)[0] 540 else: 541 tval = '' 542 self._jump_text.text = tval 543 if tval == '': 544 self._run_text.position = self._run_text_pos_top 545 self._extra_text.position = ( 546 self._run_text_pos_top[0], 547 self._run_text_pos_top[1] - 50, 548 ) 549 else: 550 self._run_text.position = self._run_text_pos_bottom 551 self._extra_text.position = ( 552 self._run_text_pos_bottom[0], 553 self._run_text_pos_bottom[1] - 50, 554 ) 555 if len(bomb_button_names) == 1: 556 self._bomb_text.text = list(bomb_button_names)[0] 557 else: 558 self._bomb_text.text = '' 559 560 # Also move our title up/down depending on if this is shown. 561 if len(pickup_button_names) == 1: 562 self._pick_up_text.text = list(pickup_button_names)[0] 563 if self._title_text is not None: 564 self._title_text.position = self._title_text_pos_top 565 else: 566 self._pick_up_text.text = '' 567 if self._title_text is not None: 568 self._title_text.position = self._title_text_pos_bottom 569 570 def _die(self) -> None: 571 for node in self._nodes: 572 node.delete() 573 self._nodes = [] 574 self._update_timer = None 575 self._dead = True 576 577 def exists(self) -> bool: 578 return not self._dead 579 580 def handlemessage(self, msg: Any) -> Any: 581 assert not self.expired 582 if isinstance(msg, bs.DieMessage): 583 if msg.immediate: 584 self._die() 585 else: 586 # If they don't need immediate, 587 # fade out our nodes and die later. 588 for node in self._nodes: 589 bs.animate(node, 'opacity', {0: node.opacity, 3.0: 0.0}) 590 bs.timer(3.1, bs.WeakCall(self._die)) 591 return None 592 return super().handlemessage(msg)
A screen overlay of game controls.
category: Gameplay Classes
Shows button mappings based on what controllers are connected. Handy to show at the start of a series or whenever there might be newbies watching.
26 def __init__( 27 self, 28 position: tuple[float, float] = (390.0, 120.0), 29 scale: float = 1.0, 30 delay: float = 0.0, 31 lifespan: float | None = None, 32 bright: bool = False, 33 ): 34 """Instantiate an overlay. 35 36 delay: is the time in seconds before the overlay fades in. 37 38 lifespan: if not None, the overlay will fade back out and die after 39 that long (in milliseconds). 40 41 bright: if True, brighter colors will be used; handy when showing 42 over gameplay but may be too bright for join-screens, etc. 43 """ 44 # pylint: disable=too-many-statements 45 # pylint: disable=too-many-locals 46 super().__init__() 47 show_title = True 48 scale *= 0.75 49 image_size = 90.0 * scale 50 offs = 74.0 * scale 51 offs5 = 43.0 * scale 52 ouya = False 53 maxw = 50 54 self._lifespan = lifespan 55 self._dead = False 56 self._bright = bright 57 self._cancel_timer: bs.Timer | None = None 58 self._fade_in_timer: bs.Timer | None = None 59 self._update_timer: bs.Timer | None = None 60 self._title_text: bs.Node | None 61 clr: Sequence[float] 62 punch_pos = (position[0] - offs * 1.1, position[1]) 63 jump_pos = (position[0], position[1] - offs) 64 bomb_pos = (position[0] + offs * 1.1, position[1]) 65 pickup_pos = (position[0], position[1] + offs) 66 self._force_hide_button_names = False 67 68 if show_title: 69 self._title_text_pos_top = ( 70 position[0], 71 position[1] + 139.0 * scale, 72 ) 73 self._title_text_pos_bottom = ( 74 position[0], 75 position[1] + 139.0 * scale, 76 ) 77 clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7) 78 tval = bs.Lstr( 79 value='${A}:', subs=[('${A}', bs.Lstr(resource='controlsText'))] 80 ) 81 self._title_text = bs.newnode( 82 'text', 83 attrs={ 84 'text': tval, 85 'host_only': True, 86 'scale': 1.1 * scale, 87 'shadow': 0.5, 88 'flatness': 1.0, 89 'maxwidth': 480, 90 'v_align': 'center', 91 'h_align': 'center', 92 'color': clr, 93 }, 94 ) 95 else: 96 self._title_text = None 97 pos = jump_pos 98 clr = (0.4, 1, 0.4) 99 self._jump_image = bs.newnode( 100 'image', 101 attrs={ 102 'texture': bs.gettexture('buttonJump'), 103 'absolute_scale': True, 104 'host_only': True, 105 'vr_depth': 10, 106 'position': pos, 107 'scale': (image_size, image_size), 108 'color': clr, 109 }, 110 ) 111 self._jump_text = bs.newnode( 112 'text', 113 attrs={ 114 'v_align': 'top', 115 'h_align': 'center', 116 'scale': 1.5 * scale, 117 'flatness': 1.0, 118 'host_only': True, 119 'shadow': 1.0, 120 'maxwidth': maxw, 121 'position': (pos[0], pos[1] - offs5), 122 'color': clr, 123 }, 124 ) 125 clr = (0.2, 0.6, 1) if ouya else (1, 0.7, 0.3) 126 pos = punch_pos 127 self._punch_image = bs.newnode( 128 'image', 129 attrs={ 130 'texture': bs.gettexture('buttonPunch'), 131 'absolute_scale': True, 132 'host_only': True, 133 'vr_depth': 10, 134 'position': pos, 135 'scale': (image_size, image_size), 136 'color': clr, 137 }, 138 ) 139 self._punch_text = bs.newnode( 140 'text', 141 attrs={ 142 'v_align': 'top', 143 'h_align': 'center', 144 'scale': 1.5 * scale, 145 'flatness': 1.0, 146 'host_only': True, 147 'shadow': 1.0, 148 'maxwidth': maxw, 149 'position': (pos[0], pos[1] - offs5), 150 'color': clr, 151 }, 152 ) 153 pos = bomb_pos 154 clr = (1, 0.3, 0.3) 155 self._bomb_image = bs.newnode( 156 'image', 157 attrs={ 158 'texture': bs.gettexture('buttonBomb'), 159 'absolute_scale': True, 160 'host_only': True, 161 'vr_depth': 10, 162 'position': pos, 163 'scale': (image_size, image_size), 164 'color': clr, 165 }, 166 ) 167 self._bomb_text = bs.newnode( 168 'text', 169 attrs={ 170 'h_align': 'center', 171 'v_align': 'top', 172 'scale': 1.5 * scale, 173 'flatness': 1.0, 174 'host_only': True, 175 'shadow': 1.0, 176 'maxwidth': maxw, 177 'position': (pos[0], pos[1] - offs5), 178 'color': clr, 179 }, 180 ) 181 pos = pickup_pos 182 clr = (1, 0.8, 0.3) if ouya else (0.8, 0.5, 1) 183 self._pickup_image = bs.newnode( 184 'image', 185 attrs={ 186 'texture': bs.gettexture('buttonPickUp'), 187 'absolute_scale': True, 188 'host_only': True, 189 'vr_depth': 10, 190 'position': pos, 191 'scale': (image_size, image_size), 192 'color': clr, 193 }, 194 ) 195 self._pick_up_text = bs.newnode( 196 'text', 197 attrs={ 198 'v_align': 'top', 199 'h_align': 'center', 200 'scale': 1.5 * scale, 201 'flatness': 1.0, 202 'host_only': True, 203 'shadow': 1.0, 204 'maxwidth': maxw, 205 'position': (pos[0], pos[1] - offs5), 206 'color': clr, 207 }, 208 ) 209 clr = (0.9, 0.9, 2.0, 1.0) if bright else (0.8, 0.8, 2.0, 1.0) 210 self._run_text_pos_top = (position[0], position[1] - 135.0 * scale) 211 self._run_text_pos_bottom = (position[0], position[1] - 172.0 * scale) 212 sval = 1.0 * scale if bs.app.env.vr else 0.8 * scale 213 self._run_text = bs.newnode( 214 'text', 215 attrs={ 216 'scale': sval, 217 'host_only': True, 218 'shadow': 1.0 if bs.app.env.vr else 0.5, 219 'flatness': 1.0, 220 'maxwidth': 380, 221 'v_align': 'top', 222 'h_align': 'center', 223 'color': clr, 224 }, 225 ) 226 clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7) 227 self._extra_text = bs.newnode( 228 'text', 229 attrs={ 230 'scale': 0.8 * scale, 231 'host_only': True, 232 'shadow': 0.5, 233 'flatness': 1.0, 234 'maxwidth': 380, 235 'v_align': 'top', 236 'h_align': 'center', 237 'color': clr, 238 }, 239 ) 240 241 self._extra_image_1 = None 242 self._extra_image_2 = None 243 244 self._nodes = [ 245 self._bomb_image, 246 self._bomb_text, 247 self._punch_image, 248 self._punch_text, 249 self._jump_image, 250 self._jump_text, 251 self._pickup_image, 252 self._pick_up_text, 253 self._run_text, 254 self._extra_text, 255 ] 256 if show_title: 257 assert self._title_text 258 self._nodes.append(self._title_text) 259 260 # Start everything invisible. 261 for node in self._nodes: 262 node.opacity = 0.0 263 264 # Don't do anything until our delay has passed. 265 bs.timer(delay, bs.WeakCall(self._start_updating))
Instantiate an overlay.
delay: is the time in seconds before the overlay fades in.
lifespan: if not None, the overlay will fade back out and die after that long (in milliseconds).
bright: if True, brighter colors will be used; handy when showing over gameplay but may be too bright for join-screens, etc.
Returns whether the Actor is still present in a meaningful way.
Note that a dying character should still return True here as long as their corpse is visible; this is about presence, not being 'alive' (see bascenev1.Actor.is_alive() for that).
If this returns False, it is assumed the Actor can be completely deleted without affecting the game; this call is often used when pruning lists of Actors, such as with bascenev1.Actor.autoretain()
The default implementation of this method always return True.
Note that the boolean operator for the Actor class calls this method, so a simple "if myactor" test will conveniently do the right thing even if myactor is set to None.
580 def handlemessage(self, msg: Any) -> Any: 581 assert not self.expired 582 if isinstance(msg, bs.DieMessage): 583 if msg.immediate: 584 self._die() 585 else: 586 # If they don't need immediate, 587 # fade out our nodes and die later. 588 for node in self._nodes: 589 bs.animate(node, 'opacity', {0: node.opacity, 3.0: 0.0}) 590 bs.timer(3.1, bs.WeakCall(self._die)) 591 return None 592 return super().handlemessage(msg)
General message handling; can be passed any message object.
Inherited Members
- bascenev1._actor.Actor
- autoretain
- on_expire
- expired
- is_alive
- activity
- getactivity