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