def show_post_purchase_message(self) -> None: """(internal)""" from ba._language import Lstr from ba._generated.enums import TimeType cur_time = _ba.time(TimeType.REAL) if (self.last_post_purchase_message_time is None or cur_time - self.last_post_purchase_message_time > 3.0): self.last_post_purchase_message_time = cur_time with _ba.Context('ui'): _ba.screenmessage(Lstr(resource='updatingAccountText', fallback_resource='purchasingText'), color=(0, 1, 0)) _ba.playsound(_ba.getsound('click01'))
def __init__(self, lobby: ba.Lobby): from ba._nodeactor import NodeActor from ba._general import WeakCall self._state = 0 self._press_to_punch: Union[str, ba.Lstr] = ('C' if _ba.app.iircade_mode else _ba.charstr( SpecialChar.LEFT_BUTTON)) self._press_to_bomb: Union[str, ba.Lstr] = ('B' if _ba.app.iircade_mode else _ba.charstr( SpecialChar.RIGHT_BUTTON)) self._joinmsg = Lstr(resource='pressAnyButtonToJoinText') can_switch_teams = (len(lobby.sessionteams) > 1) # If we have a keyboard, grab keys for punch and pickup. # FIXME: This of course is only correct on the local device; # Should change this for net games. keyboard = _ba.getinputdevice('Keyboard', '#1', doraise=False) if keyboard is not None: self._update_for_keyboard(keyboard) flatness = 1.0 if _ba.app.vr_mode else 0.0 self._text = NodeActor( _ba.newnode('text', attrs={ 'position': (0, -40), 'h_attach': 'center', 'v_attach': 'top', 'h_align': 'center', 'color': (0.7, 0.7, 0.95, 1.0), 'flatness': flatness, 'text': self._joinmsg })) if _ba.app.demo_mode or _ba.app.arcade_mode: self._messages = [self._joinmsg] else: msg1 = Lstr(resource='pressToSelectProfileText', subs=[ ('${BUTTONS}', _ba.charstr(SpecialChar.UP_ARROW) + ' ' + _ba.charstr(SpecialChar.DOWN_ARROW)) ]) msg2 = Lstr(resource='pressToOverrideCharacterText', subs=[('${BUTTONS}', Lstr(resource='bombBoldText'))]) msg3 = Lstr(value='${A} < ${B} >', subs=[('${A}', msg2), ('${B}', self._press_to_bomb)]) self._messages = (([ Lstr( resource='pressToSelectTeamText', subs=[('${BUTTONS}', _ba.charstr(SpecialChar.LEFT_ARROW) + ' ' + _ba.charstr(SpecialChar.RIGHT_ARROW))], ) ] if can_switch_teams else []) + [msg1] + [msg3] + [self._joinmsg]) self._timer = _ba.Timer(4.0, WeakCall(self._update), repeat=True)
def on_account_state_changed(self) -> None: """(internal)""" from ba._language import Lstr # Run any pending promo codes we had queued up while not signed in. if _ba.get_account_state() == 'signed_in' and self.pending_promo_codes: for code in self.pending_promo_codes: _ba.screenmessage(Lstr(resource='submittingPromoCodeText'), color=(0, 1, 0)) _ba.add_transaction({ 'type': 'PROMO_CODE', 'expire_time': time.time() + 5, 'code': code }) _ba.run_transactions() self.pending_promo_codes = []
def display_name(self) -> ba.Lstr: """Return a ba.Lstr for this Achievement's name.""" from ba._language import Lstr name: Union[ba.Lstr, str] try: if self._level_name != '': from ba._campaign import getcampaign campaignname, campaign_level = self._level_name.split(':') name = getcampaign(campaignname).getlevel( campaign_level).displayname else: name = '' except Exception: name = '' print_exception() return Lstr(resource='achievements.' + self._name + '.name', subs=[('${LEVEL}', name)])
def _getname(self, full: bool = False) -> str: name_raw = name = self._profilenames[self._profileindex] clamp = False if name == '_random': try: name = ( self._sessionplayer.inputdevice.get_default_player_name()) except Exception: print_exception('Error getting _random chooser name.') name = 'Invalid' clamp = not full elif name == '__account__': try: name = self._sessionplayer.inputdevice.get_v1_account_name( full) except Exception: print_exception('Error getting account name for chooser.') name = 'Invalid' clamp = not full elif name == '_edit': # Explicitly flattening this to a str; it's only relevant on # the host so that's ok. name = (Lstr( resource='createEditPlayerText', fallback_resource='editProfileWindow.titleNewText').evaluate()) else: # If we have a regular profile marked as global with an icon, # use it (for full only). if full: try: if self._profiles[name_raw].get('global', False): icon = (self._profiles[name_raw]['icon'] if 'icon' in self._profiles[name_raw] else _ba.charstr(SpecialChar.LOGO)) name = icon + name except Exception: print_exception('Error applying global icon.') else: # We now clamp non-full versions of names so there's at # least some hope of reading them in-game. clamp = True if clamp: if len(name) > 10: name = name[:10] + '...' return name
def _calc_map_name(self, settings: dict) -> str: map_name: str if 'map' in settings: map_name = settings['map'] else: # If settings doesn't specify a map, pick a random one from the # list of supported ones. unowned_maps = _map.get_unowned_maps() valid_maps: List[str] = [ m for m in self.get_supported_maps(type(self.session)) if m not in unowned_maps ] if not valid_maps: _ba.screenmessage(Lstr(resource='noValidMapsErrorText')) raise Exception('No valid maps') map_name = valid_maps[random.randrange(len(valid_maps))] return map_name
def announce_game_results(self, activity: ba.GameActivity, results: ba.GameResults, delay: float, announce_winning_team: bool = True) -> None: """Show basic game result at the end of a game. (before transitioning to a score screen). This will include a zoom-text of 'BLUE WINS' or whatnot, along with a possible audio announcement of the same. """ # pylint: disable=cyclic-import # pylint: disable=too-many-locals from ba._math import normalized_color from ba._general import Call from ba._gameutils import cameraflash from ba._language import Lstr from ba._freeforallsession import FreeForAllSession from ba._messages import CelebrateMessage _ba.timer(delay, Call(_ba.playsound, _ba.getsound('boxingBell'))) if announce_winning_team: winning_sessionteam = results.winning_sessionteam if winning_sessionteam is not None: # Have all players celebrate. celebrate_msg = CelebrateMessage(duration=10.0) assert winning_sessionteam.activityteam is not None for player in winning_sessionteam.activityteam.players: if player.actor: player.actor.handlemessage(celebrate_msg) cameraflash() # Some languages say "FOO WINS" different for teams vs players. if isinstance(self, FreeForAllSession): wins_resource = 'winsPlayerText' else: wins_resource = 'winsTeamText' wins_text = Lstr(resource=wins_resource, subs=[('${NAME}', winning_sessionteam.name)]) activity.show_zoom_message( wins_text, scale=0.85, color=normalized_color(winning_sessionteam.color), )
def _apply(name2: Lstr, score2: int, showpoints2: bool, color2: tuple[float, float, float, float], scale2: float, sound2: Optional[ba.Sound]) -> None: from bastd.actor.popuptext import PopupText # Only award this if they're still alive and we can get # a current position for them. our_pos: Optional[ba.Vec3] = None if self._sessionplayer: if self._sessionplayer.activityplayer is not None: try: our_pos = self._sessionplayer.activityplayer.position except NotFoundError: pass if our_pos is None: return # Jitter position a bit since these often come in clusters. our_pos = _ba.Vec3(our_pos[0] + (random.random() - 0.5) * 2.0, our_pos[1] + (random.random() - 0.5) * 2.0, our_pos[2] + (random.random() - 0.5) * 2.0) activity = self.getactivity() if activity is not None: PopupText(Lstr( value=(('+' + str(score2) + ' ') if showpoints2 else '') + '${N}', subs=[('${N}', name2)]), color=color2, scale=scale2, position=our_pos).autoretain() if sound2: _ba.playsound(sound2) self.score += score2 self.accumscore += score2 # Inform a running game of the score. if score2 != 0 and activity is not None: activity.handlemessage(PlayerScoredMessage(score=score2))
def _on_player_ready(self, chooser: ba.Chooser) -> None: """Called when a ba.Player has checked themself ready.""" lobby = chooser.lobby activity = self._activity_weak() # This happens sometimes. That seems like it shouldn't be happening; # when would we have a session and a chooser with players but no # active activity? if activity is None: print('_on_player_ready called with no activity.') return # In joining-activities, we wait till all choosers are ready # and then create all players at once. if activity.is_joining_activity: if not lobby.check_all_ready(): return choosers = lobby.get_choosers() min_players = self.min_players if len(choosers) >= min_players: for lch in lobby.get_choosers(): self._add_chosen_player(lch) lobby.remove_all_choosers() # Get our next activity going. self._complete_end_activity(activity, {}) else: _ba.screenmessage( Lstr(resource='notEnoughPlayersText', subs=[('${COUNT}', str(min_players))]), color=(1, 1, 0), ) _ba.playsound(_ba.getsound('error')) # Otherwise just add players on the fly. else: self._add_chosen_player(chooser) lobby.remove_chooser(chooser.getplayer())
def on_player_request(self, player: ba.SessionPlayer) -> bool: """Called when a new ba.Player wants to join the Session. This should return True or False to accept/reject. """ # Limit player counts *unless* we're in a stress test. if _ba.app.stress_test_reset_timer is None: if len(self.sessionplayers) >= self.max_players: # Print a rejection message *only* to the client trying to # join (prevents spamming everyone else in the game). _ba.playsound(_ba.getsound('error')) _ba.screenmessage(Lstr(resource='playerLimitReachedText', subs=[('${COUNT}', str(self.max_players))]), color=(0.8, 0.0, 0.0), clients=[player.inputdevice.client_id], transient=True) return False _ba.playsound(_ba.getsound('dripity')) return True
def _update_text(self) -> None: assert self._text_node is not None if self._ready: # Once we're ready, we've saved the name, so lets ask the system # for it so we get appended numbers and stuff. text = Lstr(value=self._sessionplayer.getname(full=True)) if self.characterchooser: text = Lstr( value='${A}\n${B}', subs=[('${A}', text), ('${B}', Lstr(value="" + self._character_names[self._character_index]))]) self._text_node.scale = 0.8 else: text = Lstr(value='${A} (${B})', subs=[('${A}', text), ('${B}', Lstr(resource='readyText'))]) else: text = Lstr(value=self._getname(full=True)) self._text_node.scale = 1.0 can_switch_teams = len(self.lobby.sessionteams) > 1 # Flash as we're coming in. fin_color = _ba.safecolor(self.get_color()) + (1, ) if not self._inited: animate_array(self._text_node, 'color', 4, { 0.15: fin_color, 0.25: (2, 2, 2, 1), 0.35: fin_color }) else: # Blend if we're in teams mode; switch instantly otherwise. if can_switch_teams: animate_array(self._text_node, 'color', 4, { 0: self._text_node.color, 0.1: fin_color }) else: self._text_node.color = fin_color self._text_node.text = text
def _show_remaining_achievements(self) -> None: # pylint: disable=cyclic-import from ba._language import Lstr from bastd.actor.text import Text ts_h_offs = 30 v_offs = -200 achievements = [ a for a in _ba.app.ach.achievements_for_coop_level( self._get_coop_level_name()) if not a.complete ] vrmode = _ba.app.vr_mode if achievements: Text(Lstr(resource='achievementsRemainingText'), host_only=True, position=(ts_h_offs - 10 + 40, v_offs - 10), transition=Text.Transition.FADE_IN, scale=1.1, h_attach=Text.HAttach.LEFT, v_attach=Text.VAttach.TOP, color=(1, 1, 1.2, 1) if vrmode else (0.8, 0.8, 1.0, 1.0), flatness=1.0 if vrmode else 0.6, shadow=1.0 if vrmode else 0.5, transition_delay=0.0, transition_out_delay=1.3 if self.slow_motion else 4.0).autoretain() hval = 70 vval = -50 tdelay = 0.0 for ach in achievements: tdelay += 0.05 ach.create_display(hval + 40, vval + v_offs, 0 + tdelay, outdelay=1.3 if self.slow_motion else 4.0, style='in_game') vval -= 55
def _update_for_keyboard(self, keyboard: ba.InputDevice) -> None: from ba import _input punch_key = keyboard.get_button_name( _input.get_device_value(keyboard, 'buttonPunch')) self._press_to_punch = Lstr(resource='orText', subs=[('${A}', Lstr(value='\'${K}\'', subs=[('${K}', punch_key)])), ('${B}', self._press_to_punch)]) bomb_key = keyboard.get_button_name( _input.get_device_value(keyboard, 'buttonBomb')) self._press_to_bomb = Lstr(resource='orText', subs=[('${A}', Lstr(value='\'${K}\'', subs=[('${K}', bomb_key)])), ('${B}', self._press_to_bomb)]) self._joinmsg = Lstr(value='${A} < ${B} >', subs=[('${A}', Lstr(resource='pressPunchToJoinText')), ('${B}', self._press_to_punch)])
def show_completion_banner(self, sound: bool = True) -> None: """Create the banner/sound for an acquired achievement announcement.""" from ba import _gameutils from bastd.actor.text import Text from bastd.actor.image import Image from ba._general import WeakCall from ba._language import Lstr from ba._messages import DieMessage from ba._generated.enums import TimeType, SpecialChar app = _ba.app app.ach.last_achievement_display_time = _ba.time(TimeType.REAL) # Just piggy-back onto any current activity # (should we use the session instead?..) activity = _ba.getactivity(doraise=False) # If this gets called while this achievement is occupying a slot # already, ignore it. (probably should never happen in real # life but whatevs). if self._completion_banner_slot is not None: return if activity is None: print('show_completion_banner() called with no current activity!') return if sound: _ba.playsound(_ba.getsound('achievement'), host_only=True) else: _ba.timer( 0.5, lambda: _ba.playsound(_ba.getsound('ding'), host_only=True)) in_time = 0.300 out_time = 3.5 base_vr_depth = 200 # Find the first free slot. i = 0 while True: if i not in app.ach.achievement_completion_banner_slots: app.ach.achievement_completion_banner_slots.add(i) self._completion_banner_slot = i # Remove us from that slot when we close. # Use a real-timer in the UI context so the removal runs even # if our activity/session dies. with _ba.Context('ui'): _ba.timer(in_time + out_time, self._remove_banner_slot, timetype=TimeType.REAL) break i += 1 assert self._completion_banner_slot is not None y_offs = 110 * self._completion_banner_slot objs: list[ba.Actor] = [] obj = Image(_ba.gettexture('shadow'), position=(-30, 30 + y_offs), front=True, attach=Image.Attach.BOTTOM_CENTER, transition=Image.Transition.IN_BOTTOM, vr_depth=base_vr_depth - 100, transition_delay=in_time, transition_out_delay=out_time, color=(0.0, 0.1, 0, 1), scale=(1000, 300)).autoretain() objs.append(obj) assert obj.node obj.node.host_only = True obj = Image(_ba.gettexture('light'), position=(-180, 60 + y_offs), front=True, attach=Image.Attach.BOTTOM_CENTER, vr_depth=base_vr_depth, transition=Image.Transition.IN_BOTTOM, transition_delay=in_time, transition_out_delay=out_time, color=(1.8, 1.8, 1.0, 0.0), scale=(40, 300)).autoretain() objs.append(obj) assert obj.node obj.node.host_only = True obj.node.premultiplied = True combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 2}) _gameutils.animate( combine, 'input0', { in_time: 0, in_time + 0.4: 30, in_time + 0.5: 40, in_time + 0.6: 30, in_time + 2.0: 0 }) _gameutils.animate( combine, 'input1', { in_time: 0, in_time + 0.4: 200, in_time + 0.5: 500, in_time + 0.6: 200, in_time + 2.0: 0 }) combine.connectattr('output', obj.node, 'scale') _gameutils.animate(obj.node, 'rotate', { 0: 0.0, 0.35: 360.0 }, loop=True) obj = Image(self.get_icon_texture(True), position=(-180, 60 + y_offs), attach=Image.Attach.BOTTOM_CENTER, front=True, vr_depth=base_vr_depth - 10, transition=Image.Transition.IN_BOTTOM, transition_delay=in_time, transition_out_delay=out_time, scale=(100, 100)).autoretain() objs.append(obj) assert obj.node obj.node.host_only = True # Flash. color = self.get_icon_color(True) combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 3}) keys = { in_time: 1.0 * color[0], in_time + 0.4: 1.5 * color[0], in_time + 0.5: 6.0 * color[0], in_time + 0.6: 1.5 * color[0], in_time + 2.0: 1.0 * color[0] } _gameutils.animate(combine, 'input0', keys) keys = { in_time: 1.0 * color[1], in_time + 0.4: 1.5 * color[1], in_time + 0.5: 6.0 * color[1], in_time + 0.6: 1.5 * color[1], in_time + 2.0: 1.0 * color[1] } _gameutils.animate(combine, 'input1', keys) keys = { in_time: 1.0 * color[2], in_time + 0.4: 1.5 * color[2], in_time + 0.5: 6.0 * color[2], in_time + 0.6: 1.5 * color[2], in_time + 2.0: 1.0 * color[2] } _gameutils.animate(combine, 'input2', keys) combine.connectattr('output', obj.node, 'color') obj = Image(_ba.gettexture('achievementOutline'), model_transparent=_ba.getmodel('achievementOutline'), position=(-180, 60 + y_offs), front=True, attach=Image.Attach.BOTTOM_CENTER, vr_depth=base_vr_depth, transition=Image.Transition.IN_BOTTOM, transition_delay=in_time, transition_out_delay=out_time, scale=(100, 100)).autoretain() assert obj.node obj.node.host_only = True # Flash. color = (2, 1.4, 0.4, 1) combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 3}) keys = { in_time: 1.0 * color[0], in_time + 0.4: 1.5 * color[0], in_time + 0.5: 6.0 * color[0], in_time + 0.6: 1.5 * color[0], in_time + 2.0: 1.0 * color[0] } _gameutils.animate(combine, 'input0', keys) keys = { in_time: 1.0 * color[1], in_time + 0.4: 1.5 * color[1], in_time + 0.5: 6.0 * color[1], in_time + 0.6: 1.5 * color[1], in_time + 2.0: 1.0 * color[1] } _gameutils.animate(combine, 'input1', keys) keys = { in_time: 1.0 * color[2], in_time + 0.4: 1.5 * color[2], in_time + 0.5: 6.0 * color[2], in_time + 0.6: 1.5 * color[2], in_time + 2.0: 1.0 * color[2] } _gameutils.animate(combine, 'input2', keys) combine.connectattr('output', obj.node, 'color') objs.append(obj) objt = Text(Lstr(value='${A}:', subs=[('${A}', Lstr(resource='achievementText'))]), position=(-120, 91 + y_offs), front=True, v_attach=Text.VAttach.BOTTOM, vr_depth=base_vr_depth - 10, transition=Text.Transition.IN_BOTTOM, flatness=0.5, transition_delay=in_time, transition_out_delay=out_time, color=(1, 1, 1, 0.8), scale=0.65).autoretain() objs.append(objt) assert objt.node objt.node.host_only = True objt = Text(self.display_name, position=(-120, 50 + y_offs), front=True, v_attach=Text.VAttach.BOTTOM, transition=Text.Transition.IN_BOTTOM, vr_depth=base_vr_depth, flatness=0.5, transition_delay=in_time, transition_out_delay=out_time, flash=True, color=(1, 0.8, 0, 1.0), scale=1.5).autoretain() objs.append(objt) assert objt.node objt.node.host_only = True objt = Text(_ba.charstr(SpecialChar.TICKET), position=(-120 - 170 + 5, 75 + y_offs - 20), front=True, v_attach=Text.VAttach.BOTTOM, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, transition=Text.Transition.IN_BOTTOM, vr_depth=base_vr_depth, transition_delay=in_time, transition_out_delay=out_time, flash=True, color=(0.5, 0.5, 0.5, 1), scale=3.0).autoretain() objs.append(objt) assert objt.node objt.node.host_only = True objt = Text('+' + str(self.get_award_ticket_value()), position=(-120 - 180 + 5, 80 + y_offs - 20), v_attach=Text.VAttach.BOTTOM, front=True, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, transition=Text.Transition.IN_BOTTOM, vr_depth=base_vr_depth, flatness=0.5, shadow=1.0, transition_delay=in_time, transition_out_delay=out_time, flash=True, color=(0, 1, 0, 1), scale=1.5).autoretain() objs.append(objt) assert objt.node objt.node.host_only = True # Add the 'x 2' if we've got pro. if app.accounts.have_pro(): objt = Text('x 2', position=(-120 - 180 + 45, 80 + y_offs - 50), v_attach=Text.VAttach.BOTTOM, front=True, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, transition=Text.Transition.IN_BOTTOM, vr_depth=base_vr_depth, flatness=0.5, shadow=1.0, transition_delay=in_time, transition_out_delay=out_time, flash=True, color=(0.4, 0, 1, 1), scale=0.9).autoretain() objs.append(objt) assert objt.node objt.node.host_only = True objt = Text(self.description_complete, position=(-120, 30 + y_offs), front=True, v_attach=Text.VAttach.BOTTOM, transition=Text.Transition.IN_BOTTOM, vr_depth=base_vr_depth - 10, flatness=0.5, transition_delay=in_time, transition_out_delay=out_time, color=(1.0, 0.7, 0.5, 1.0), scale=0.8).autoretain() objs.append(objt) assert objt.node objt.node.host_only = True for actor in objs: _ba.timer(out_time + 1.000, WeakCall(actor.handlemessage, DieMessage()))
def create_display(self, x: float, y: float, delay: float, outdelay: float = None, color: Sequence[float] = None, style: str = 'post_game') -> list[ba.Actor]: """Create a display for the Achievement. Shows the Achievement icon, name, and description. """ # pylint: disable=cyclic-import from ba._language import Lstr from ba._generated.enums import SpecialChar from ba._coopsession import CoopSession from bastd.actor.image import Image from bastd.actor.text import Text # Yeah this needs cleaning up. if style == 'post_game': in_game_colors = False in_main_menu = False h_attach = Text.HAttach.CENTER v_attach = Text.VAttach.CENTER attach = Image.Attach.CENTER elif style == 'in_game': in_game_colors = True in_main_menu = False h_attach = Text.HAttach.LEFT v_attach = Text.VAttach.TOP attach = Image.Attach.TOP_LEFT elif style == 'news': in_game_colors = True in_main_menu = True h_attach = Text.HAttach.CENTER v_attach = Text.VAttach.TOP attach = Image.Attach.TOP_CENTER else: raise ValueError('invalid style "' + style + '"') # Attempt to determine what campaign we're in # (so we know whether to show "hard mode only"). if in_main_menu: hmo = False else: try: session = _ba.getsession() if isinstance(session, CoopSession): campaign = session.campaign assert campaign is not None hmo = (self._hard_mode_only and campaign.name == 'Easy') else: hmo = False except Exception: print_exception('Error determining campaign.') hmo = False objs: list[ba.Actor] if in_game_colors: objs = [] out_delay_fin = (delay + outdelay) if outdelay is not None else None if color is not None: cl1 = (2.0 * color[0], 2.0 * color[1], 2.0 * color[2], color[3]) cl2 = color else: cl1 = (1.5, 1.5, 2, 1.0) cl2 = (0.8, 0.8, 1.0, 1.0) if hmo: cl1 = (cl1[0], cl1[1], cl1[2], cl1[3] * 0.6) cl2 = (cl2[0], cl2[1], cl2[2], cl2[3] * 0.2) objs.append( Image(self.get_icon_texture(False), host_only=True, color=cl1, position=(x - 25, y + 5), attach=attach, transition=Image.Transition.FADE_IN, transition_delay=delay, vr_depth=4, transition_out_delay=out_delay_fin, scale=(40, 40)).autoretain()) txt = self.display_name txt_s = 0.85 txt_max_w = 300 objs.append( Text(txt, host_only=True, maxwidth=txt_max_w, position=(x, y + 2), transition=Text.Transition.FADE_IN, scale=txt_s, flatness=0.6, shadow=0.5, h_attach=h_attach, v_attach=v_attach, color=cl2, transition_delay=delay + 0.05, transition_out_delay=out_delay_fin).autoretain()) txt2_s = 0.62 txt2_max_w = 400 objs.append( Text(self.description_full if in_main_menu else self.description, host_only=True, maxwidth=txt2_max_w, position=(x, y - 14), transition=Text.Transition.FADE_IN, vr_depth=-5, h_attach=h_attach, v_attach=v_attach, scale=txt2_s, flatness=1.0, shadow=0.5, color=cl2, transition_delay=delay + 0.1, transition_out_delay=out_delay_fin).autoretain()) if hmo: txtactor = Text( Lstr(resource='difficultyHardOnlyText'), host_only=True, maxwidth=txt2_max_w * 0.7, position=(x + 60, y + 5), transition=Text.Transition.FADE_IN, vr_depth=-5, h_attach=h_attach, v_attach=v_attach, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, scale=txt_s * 0.8, flatness=1.0, shadow=0.5, color=(1, 1, 0.6, 1), transition_delay=delay + 0.1, transition_out_delay=out_delay_fin).autoretain() txtactor.node.rotate = 10 objs.append(txtactor) # Ticket-award. award_x = -100 objs.append( Text(_ba.charstr(SpecialChar.TICKET), host_only=True, position=(x + award_x + 33, y + 7), transition=Text.Transition.FADE_IN, scale=1.5, h_attach=h_attach, v_attach=v_attach, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, color=(1, 1, 1, 0.2 if hmo else 0.4), transition_delay=delay + 0.05, transition_out_delay=out_delay_fin).autoretain()) objs.append( Text('+' + str(self.get_award_ticket_value()), host_only=True, position=(x + award_x + 28, y + 16), transition=Text.Transition.FADE_IN, scale=0.7, flatness=1, h_attach=h_attach, v_attach=v_attach, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, color=cl2, transition_delay=delay + 0.05, transition_out_delay=out_delay_fin).autoretain()) else: complete = self.complete objs = [] c_icon = self.get_icon_color(complete) if hmo and not complete: c_icon = (c_icon[0], c_icon[1], c_icon[2], c_icon[3] * 0.3) objs.append( Image(self.get_icon_texture(complete), host_only=True, color=c_icon, position=(x - 25, y + 5), attach=attach, vr_depth=4, transition=Image.Transition.IN_RIGHT, transition_delay=delay, transition_out_delay=None, scale=(40, 40)).autoretain()) if complete: objs.append( Image(_ba.gettexture('achievementOutline'), host_only=True, model_transparent=_ba.getmodel('achievementOutline'), color=(2, 1.4, 0.4, 1), vr_depth=8, position=(x - 25, y + 5), attach=attach, transition=Image.Transition.IN_RIGHT, transition_delay=delay, transition_out_delay=None, scale=(40, 40)).autoretain()) else: if not complete: award_x = -100 objs.append( Text(_ba.charstr(SpecialChar.TICKET), host_only=True, position=(x + award_x + 33, y + 7), transition=Text.Transition.IN_RIGHT, scale=1.5, h_attach=h_attach, v_attach=v_attach, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, color=(1, 1, 1, 0.4) if complete else (1, 1, 1, (0.1 if hmo else 0.2)), transition_delay=delay + 0.05, transition_out_delay=None).autoretain()) objs.append( Text('+' + str(self.get_award_ticket_value()), host_only=True, position=(x + award_x + 28, y + 16), transition=Text.Transition.IN_RIGHT, scale=0.7, flatness=1, h_attach=h_attach, v_attach=v_attach, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, color=((0.8, 0.93, 0.8, 1.0) if complete else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4))), transition_delay=delay + 0.05, transition_out_delay=None).autoretain()) # Show 'hard-mode-only' only over incomplete achievements # when that's the case. if hmo: txtactor = Text( Lstr(resource='difficultyHardOnlyText'), host_only=True, maxwidth=300 * 0.7, position=(x + 60, y + 5), transition=Text.Transition.FADE_IN, vr_depth=-5, h_attach=h_attach, v_attach=v_attach, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, scale=0.85 * 0.8, flatness=1.0, shadow=0.5, color=(1, 1, 0.6, 1), transition_delay=delay + 0.05, transition_out_delay=None).autoretain() assert txtactor.node txtactor.node.rotate = 10 objs.append(txtactor) objs.append( Text(self.display_name, host_only=True, maxwidth=300, position=(x, y + 2), transition=Text.Transition.IN_RIGHT, scale=0.85, flatness=0.6, h_attach=h_attach, v_attach=v_attach, color=((0.8, 0.93, 0.8, 1.0) if complete else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4))), transition_delay=delay + 0.05, transition_out_delay=None).autoretain()) objs.append( Text(self.description_complete if complete else self.description, host_only=True, maxwidth=400, position=(x, y - 14), transition=Text.Transition.IN_RIGHT, vr_depth=-5, h_attach=h_attach, v_attach=v_attach, scale=0.62, flatness=1.0, color=((0.6, 0.6, 0.6, 1.0) if complete else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4))), transition_delay=delay + 0.1, transition_out_delay=None).autoretain()) return objs
def connection_failed_message() -> None: from ba._language import Lstr _ba.screenmessage(Lstr(resource='internal.connectionFailedText'), color=(1, 0.5, 0))
def player_scored(self, player: ba.Player, base_points: int = 1, target: Sequence[float] = None, kill: bool = False, victim_player: ba.Player = None, scale: float = 1.0, color: Sequence[float] = None, title: Union[str, ba.Lstr] = None, screenmessage: bool = True, display: bool = True, importance: int = 1, showpoints: bool = True, big_message: bool = False) -> int: """Register a score for the player. Return value is actual score with multipliers and such factored in. """ # FIXME: Tidy this up. # pylint: disable=cyclic-import # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements from bastd.actor.popuptext import PopupText from ba import _math from ba._gameactivity import GameActivity from ba._language import Lstr del victim_player # Currently unused. name = player.getname() s_player = self._player_records[name] if kill: s_player.submit_kill(showpoints=showpoints) display_color: Sequence[float] = (1.0, 1.0, 1.0, 1.0) if color is not None: display_color = color elif importance != 1: display_color = (1.0, 1.0, 0.4, 1.0) points = base_points # If they want a big announcement, throw a zoom-text up there. if display and big_message: try: assert self._activity is not None activity = self._activity() if isinstance(activity, GameActivity): name_full = player.getname(full=True, icon=False) activity.show_zoom_message( Lstr(resource='nameScoresText', subs=[('${NAME}', name_full)]), color=_math.normalized_color(player.team.color)) except Exception: print_exception('error showing big_message') # If we currently have a actor, pop up a score over it. if display and showpoints: our_pos = player.node.position if player.node else None if our_pos is not None: if target is None: target = our_pos # If display-pos is *way* lower than us, raise it up # (so we can still see scores from dudes that fell off cliffs). display_pos = (target[0], max(target[1], our_pos[1] - 2.0), min(target[2], our_pos[2] + 2.0)) activity = self.getactivity() if activity is not None: if title is not None: sval = Lstr(value='+${A} ${B}', subs=[('${A}', str(points)), ('${B}', title)]) else: sval = Lstr(value='+${A}', subs=[('${A}', str(points))]) PopupText(sval, color=display_color, scale=1.2 * scale, position=display_pos).autoretain() # Tally kills. if kill: s_player.accum_kill_count += 1 s_player.kill_count += 1 # Report non-kill scorings. try: if screenmessage and not kill: _ba.screenmessage(Lstr(resource='nameScoresText', subs=[('${NAME}', name)]), top=True, color=player.color, image=player.get_icon()) except Exception: print_exception('error announcing score') s_player.score += points s_player.accumscore += points # Inform a running game of the score. if points != 0: activity = self._activity() if self._activity is not None else None if activity is not None: activity.handlemessage(PlayerScoredMessage(score=points)) return points
def temporarily_unavailable_message() -> None: from ba._language import Lstr _ba.playsound(_ba.getsound('error')) _ba.screenmessage( Lstr(resource='getTicketsWindow.unavailableTemporarilyText'), color=(1, 0, 0))
def purchase_not_valid_error() -> None: from ba._language import Lstr _ba.playsound(_ba.getsound('error')) _ba.screenmessage(Lstr(resource='store.purchaseNotValidError', subs=[('${EMAIL}', '*****@*****.**')]), color=(1, 0, 0))
def __init__(self, vpos: float, sessionplayer: _ba.SessionPlayer, lobby: 'Lobby') -> None: self._deek_sound = _ba.getsound('deek') self._click_sound = _ba.getsound('click01') self._punchsound = _ba.getsound('punch01') self._swish_sound = _ba.getsound('punchSwish') self._errorsound = _ba.getsound('error') self._mask_texture = _ba.gettexture('characterIconMask') self._vpos = vpos self._lobby = weakref.ref(lobby) self._sessionplayer = sessionplayer self._inited = False self._dead = False self._text_node: Optional[ba.Node] = None self._profilename = '' self._profilenames: List[str] = [] self._ready: bool = False self._character_names: List[str] = [] self._last_change: Sequence[Union[float, int]] = (0, 0) self._profiles: Dict[str, Dict[str, Any]] = {} app = _ba.app # Load available player profiles either from the local config or # from the remote device. self.reload_profiles() # Note: this is just our local index out of available teams; *not* # the team-id! self._selected_team_index: int = self.lobby.next_add_team # Store a persistent random character index and colors; we'll use this # for the '_random' profile. Let's use their input_device id to seed # it. This will give a persistent character for them between games # and will distribute characters nicely if everyone is random. self._random_color, self._random_highlight = ( get_player_profile_colors(None)) # To calc our random character we pick a random one out of our # unlocked list and then locate that character's index in the full # list. char_index_offset = app.lobby_random_char_index_offset self._random_character_index = ( (sessionplayer.inputdevice.id + char_index_offset) % len(self._character_names)) # Attempt to set an initial profile based on what was used previously # for this input-device, etc. self._profileindex = self._select_initial_profile() self._profilename = self._profilenames[self._profileindex] self._text_node = _ba.newnode('text', delegate=self, attrs={ 'position': (-100, self._vpos), 'maxwidth': 160, 'shadow': 0.5, 'vr_depth': -20, 'h_align': 'left', 'v_align': 'center', 'v_attach': 'top' }) animate(self._text_node, 'scale', {0: 0, 0.1: 1.0}) self.icon = _ba.newnode('image', owner=self._text_node, attrs={ 'position': (-130, self._vpos + 20), 'mask_texture': self._mask_texture, 'vr_depth': -10, 'attach': 'topCenter' }) animate_array(self.icon, 'scale', 2, {0: (0, 0), 0.1: (45, 45)}) # Set our initial name to '<choosing player>' in case anyone asks. self._sessionplayer.setname( Lstr(resource='choosingPlayerText').evaluate(), real=False) # Init these to our rando but they should get switched to the # selected profile (if any) right after. self._character_index = self._random_character_index self._color = self._random_color self._highlight = self._random_highlight self.update_from_profile() self.update_position() self._inited = True self._set_ready(False)
def _show_info(self) -> None: """Show the game description.""" from ba._gameutils import animate from bastd.actor.zoomtext import ZoomText name = self.get_instance_display_string() ZoomText(name, maxwidth=800, lifespan=2.5, jitter=2.0, position=(0, 180), flash=False, color=(0.93 * 1.25, 0.9 * 1.25, 1.0 * 1.25), trailcolor=(0.15, 0.05, 1.0, 0.0)).autoretain() _ba.timer(0.2, Call(_ba.playsound, _ba.getsound('gong'))) # The description can be either a string or a sequence with args # to swap in post-translation. desc_in = self.get_instance_description() desc_l: Sequence if isinstance(desc_in, str): desc_l = [desc_in] # handle simple string case else: desc_l = desc_in if not isinstance(desc_l[0], str): raise TypeError('Invalid format for instance description') subs = [] for i in range(len(desc_l) - 1): subs.append(('${ARG' + str(i + 1) + '}', str(desc_l[i + 1]))) translation = Lstr(translate=('gameDescriptions', desc_l[0]), subs=subs) # Do some standard filters (epic mode, etc). if self.settings_raw.get('Epic Mode', False): translation = Lstr(resource='epicDescriptionFilterText', subs=[('${DESCRIPTION}', translation)]) vrmode = _ba.app.vr_mode dnode = _ba.newnode('text', attrs={ 'v_attach': 'center', 'h_attach': 'center', 'h_align': 'center', 'color': (1, 1, 1, 1), 'shadow': 1.0 if vrmode else 0.5, 'flatness': 1.0 if vrmode else 0.5, 'vr_depth': -30, 'position': (0, 80), 'scale': 1.2, 'maxwidth': 700, 'text': translation }) cnode = _ba.newnode('combine', owner=dnode, attrs={ 'input0': 1.0, 'input1': 1.0, 'input2': 1.0, 'size': 4 }) cnode.connectattr('output', dnode, 'color') keys = {0.5: 0, 1.0: 1.0, 2.5: 1.0, 4.0: 0.0} animate(cnode, 'input3', keys) _ba.timer(4.0, dnode.delete)
def _show_scoreboard_info(self) -> None: """Create the game info display. This is the thing in the top left corner showing the name and short description of the game. """ # pylint: disable=too-many-locals from ba._freeforallsession import FreeForAllSession from ba._gameutils import animate from ba._nodeactor import NodeActor sb_name = self.get_instance_scoreboard_display_string() # The description can be either a string or a sequence with args # to swap in post-translation. sb_desc_in = self.get_instance_description_short() sb_desc_l: Sequence if isinstance(sb_desc_in, str): sb_desc_l = [sb_desc_in] # handle simple string case else: sb_desc_l = sb_desc_in if not isinstance(sb_desc_l[0], str): raise TypeError('Invalid format for instance description.') is_empty = (sb_desc_l[0] == '') subs = [] for i in range(len(sb_desc_l) - 1): subs.append(('${ARG' + str(i + 1) + '}', str(sb_desc_l[i + 1]))) translation = Lstr(translate=('gameDescriptions', sb_desc_l[0]), subs=subs) sb_desc = translation vrmode = _ba.app.vr_mode yval = -34 if is_empty else -20 yval -= 16 sbpos = ((15, yval) if isinstance(self.session, FreeForAllSession) else (15, yval)) self._game_scoreboard_name_text = NodeActor( _ba.newnode('text', attrs={ 'text': sb_name, 'maxwidth': 300, 'position': sbpos, 'h_attach': 'left', 'vr_depth': 10, 'v_attach': 'top', 'v_align': 'bottom', 'color': (1.0, 1.0, 1.0, 1.0), 'shadow': 1.0 if vrmode else 0.6, 'flatness': 1.0 if vrmode else 0.5, 'scale': 1.1 })) assert self._game_scoreboard_name_text.node animate(self._game_scoreboard_name_text.node, 'opacity', { 0: 0.0, 1.0: 1.0 }) descpos = (((17, -44 + 10) if isinstance(self.session, FreeForAllSession) else (17, -44 + 10))) self._game_scoreboard_description_text = NodeActor( _ba.newnode('text', attrs={ 'text': sb_desc, 'maxwidth': 480, 'position': descpos, 'scale': 0.7, 'h_attach': 'left', 'v_attach': 'top', 'v_align': 'top', 'shadow': 1.0 if vrmode else 0.7, 'flatness': 1.0 if vrmode else 0.8, 'color': (1, 1, 1, 1) if vrmode else (0.9, 0.9, 0.9, 1.0) })) assert self._game_scoreboard_description_text.node animate(self._game_scoreboard_description_text.node, 'opacity', { 0: 0.0, 1.0: 1.0 })
def get_team_display_string(cls, name: str) -> ba.Lstr: """Given a team name, returns a localized version of it.""" return Lstr(translate=('teamNames', name))
def in_progress_message() -> None: from ba._language import Lstr _ba.playsound(_ba.getsound('error')) _ba.screenmessage(Lstr(resource='getTicketsWindow.inProgressText'), color=(1, 0, 0))
def _setup_tournament_time_limit(self, duration: float) -> None: """ Create a tournament game time-limit given the provided duration in seconds. This will be displayed at the top of the screen. If the time-limit expires, end_game() will be called. """ from ba._nodeactor import NodeActor from ba._generated.enums import TimeType if duration <= 0.0: return self._tournament_time_limit = int(duration) # We want this timer to match the server's time as close as possible, # so lets go with base-time. Theoretically we should do real-time but # then we have to mess with contexts and whatnot since its currently # not available in activity contexts. :-/ self._tournament_time_limit_timer = _ba.Timer( 1.0, WeakCall(self._tournament_time_limit_tick), repeat=True, timetype=TimeType.BASE) self._tournament_time_limit_title_text = NodeActor( _ba.newnode('text', attrs={ 'v_attach': 'bottom', 'h_attach': 'left', 'h_align': 'center', 'v_align': 'center', 'vr_depth': 300, 'maxwidth': 100, 'color': (1.0, 1.0, 1.0, 0.5), 'position': (60, 50), 'flatness': 1.0, 'scale': 0.5, 'text': Lstr(resource='tournamentText') })) self._tournament_time_limit_text = NodeActor( _ba.newnode('text', attrs={ 'v_attach': 'bottom', 'h_attach': 'left', 'h_align': 'center', 'v_align': 'center', 'vr_depth': 300, 'maxwidth': 100, 'color': (1.0, 1.0, 1.0, 0.5), 'position': (60, 30), 'flatness': 1.0, 'scale': 0.9 })) self._tournament_time_limit_text_input = NodeActor( _ba.newnode('timedisplay', attrs={ 'timemin': 0, 'time2': self._tournament_time_limit * 1000 })) assert self._tournament_time_limit_text.node assert self._tournament_time_limit_text_input.node self._tournament_time_limit_text_input.node.connectattr( 'output', self._tournament_time_limit_text.node, 'text')
def on_activity_end(self, activity: ba.Activity, results: Any) -> None: """Method override for co-op sessions. Jumps between co-op games and score screens. """ # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements # pylint: disable=cyclic-import from ba._activitytypes import JoinActivity, TransitionActivity from ba._language import Lstr from ba._general import WeakCall from ba._coopgame import CoopGameActivity from ba._gameresults import GameResults from ba._score import ScoreType from ba._player import PlayerInfo from bastd.tutorial import TutorialActivity from bastd.activity.coopscore import CoopScoreScreen app = _ba.app # If we're running a TeamGameActivity we'll have a GameResults # as results. Otherwise its an old CoopGameActivity so its giving # us a dict of random stuff. if isinstance(results, GameResults): outcome = 'defeat' # This can't be 'beaten'. else: outcome = '' if results is None else results.get('outcome', '') # If we're running with a gui and at any point we have no # in-game players, quit out of the session (this can happen if # someone leaves in the tutorial for instance). if not _ba.app.headless_mode: active_players = [p for p in self.sessionplayers if p.in_game] if not active_players: self.end() return # If we're in a between-round activity or a restart-activity, # hop into a round. if (isinstance(activity, (JoinActivity, CoopScoreScreen, TransitionActivity))): if outcome == 'next_level': if self._next_game_instance is None: raise RuntimeError() assert self._next_game_level_name is not None self.campaign_level_name = self._next_game_level_name next_game = self._next_game_instance else: next_game = self._current_game_instance # Special case: if we're coming from a joining-activity # and will be going into onslaught-training, show the # tutorial first. if (isinstance(activity, JoinActivity) and self.campaign_level_name == 'Onslaught Training' and not (app.demo_mode or app.arcade_mode)): if self._tutorial_activity is None: raise RuntimeError('Tutorial not preloaded properly.') self.setactivity(self._tutorial_activity) self._tutorial_activity = None self._ran_tutorial_activity = True self._custom_menu_ui = [] # Normal case; launch the next round. else: # Reset stats for the new activity. self.stats.reset() for player in self.sessionplayers: # Skip players that are still choosing a team. if player.in_game: self.stats.register_sessionplayer(player) self.stats.setactivity(next_game) # Now flip the current activity.. self.setactivity(next_game) if not (app.demo_mode or app.arcade_mode): if self.tournament_id is not None: self._custom_menu_ui = [{ 'label': Lstr(resource='restartText'), 'resume_on_call': False, 'call': WeakCall(self._on_tournament_restart_menu_press) }] else: self._custom_menu_ui = [{ 'label': Lstr(resource='restartText'), 'call': WeakCall(self.restart) }] # If we were in a tutorial, just pop a transition to get to the # actual round. elif isinstance(activity, TutorialActivity): self.setactivity(_ba.newactivity(TransitionActivity)) else: playerinfos: list[ba.PlayerInfo] # Generic team games. if isinstance(results, GameResults): playerinfos = results.playerinfos score = results.get_sessionteam_score(results.sessionteams[0]) fail_message = None score_order = ('decreasing' if results.lower_is_better else 'increasing') if results.scoretype in (ScoreType.SECONDS, ScoreType.MILLISECONDS): scoretype = 'time' # ScoreScreen wants hundredths of a second. if score is not None: if results.scoretype is ScoreType.SECONDS: score *= 100 elif results.scoretype is ScoreType.MILLISECONDS: score //= 10 else: raise RuntimeError('FIXME') else: if results.scoretype is not ScoreType.POINTS: print(f'Unknown ScoreType:' f' "{results.scoretype}"') scoretype = 'points' # Old coop-game-specific results; should migrate away from these. else: playerinfos = results.get('playerinfos') score = results['score'] if 'score' in results else None fail_message = (results['fail_message'] if 'fail_message' in results else None) score_order = (results['score_order'] if 'score_order' in results else 'increasing') activity_score_type = (activity.get_score_type() if isinstance( activity, CoopGameActivity) else None) assert activity_score_type is not None scoretype = activity_score_type # Validate types. if playerinfos is not None: assert isinstance(playerinfos, list) assert (isinstance(i, PlayerInfo) for i in playerinfos) # Looks like we were in a round - check the outcome and # go from there. if outcome == 'restart': # This will pop up back in the same round. self.setactivity(_ba.newactivity(TransitionActivity)) else: self.setactivity( _ba.newactivity( CoopScoreScreen, { 'playerinfos': playerinfos, 'score': score, 'fail_message': fail_message, 'score_order': score_order, 'score_type': scoretype, 'outcome': outcome, 'campaign': self.campaign, 'level': self.campaign_level_name })) # No matter what, get the next 2 levels ready to go. self._update_on_deck_game_instances()
def _add_chosen_player(self, chooser: ba.Chooser) -> ba.SessionPlayer: from ba._team import SessionTeam sessionplayer = chooser.getplayer() assert sessionplayer in self.sessionplayers, ( 'SessionPlayer not found in session ' 'player-list after chooser selection.') activity = self._activity_weak() assert activity is not None # Reset the player's input here, as it is probably # referencing the chooser which could inadvertently keep it alive. sessionplayer.resetinput() # We can pass it to the current activity if it has already begun # (otherwise it'll get passed once begin is called). pass_to_activity = (activity.has_begun() and not activity.is_joining_activity) # However, if we're not allowing mid-game joins, don't actually pass; # just announce the arrival and say they'll partake next round. if pass_to_activity: if not self.allow_mid_activity_joins: pass_to_activity = False with _ba.Context(self): _ba.screenmessage( Lstr(resource='playerDelayedJoinText', subs=[('${PLAYER}', sessionplayer.getname(full=True))]), color=(0, 1, 0), ) # If we're a non-team session, each player gets their own team. # (keeps mini-game coding simpler if we can always deal with teams). if self.use_teams: sessionteam = chooser.sessionteam else: our_team_id = self._next_team_id self._next_team_id += 1 sessionteam = SessionTeam( team_id=our_team_id, color=chooser.get_color(), name=chooser.getplayer().getname(full=True, icon=False), ) # Add player's team to the Session. self.sessionteams.append(sessionteam) with _ba.Context(self): try: self.on_team_join(sessionteam) except Exception: print_exception(f'Error in on_team_join for {self}.') # Add player's team to the Activity. if pass_to_activity: activity.add_team(sessionteam) assert sessionplayer not in sessionteam.players sessionteam.players.append(sessionplayer) sessionplayer.setdata(team=sessionteam, character=chooser.get_character_name(), color=chooser.get_color(), highlight=chooser.get_highlight()) self.stats.register_sessionplayer(sessionplayer) if pass_to_activity: activity.add_player(sessionplayer) return sessionplayer
def _show_tip(self) -> None: # pylint: disable=too-many-locals from ba._gameutils import animate, GameTip from ba._generated.enums import SpecialChar # If there's any tips left on the list, display one. if self.tips: tip = self.tips.pop(random.randrange(len(self.tips))) tip_title = Lstr(value='${A}:', subs=[('${A}', Lstr(resource='tipText'))]) icon: Optional[ba.Texture] = None sound: Optional[ba.Sound] = None if isinstance(tip, GameTip): icon = tip.icon sound = tip.sound tip = tip.text assert isinstance(tip, str) # Do a few substitutions. tip_lstr = Lstr(translate=('tips', tip), subs=[('${PICKUP}', _ba.charstr(SpecialChar.TOP_BUTTON))]) base_position = (75, 50) tip_scale = 0.8 tip_title_scale = 1.2 vrmode = _ba.app.vr_mode t_offs = -350.0 tnode = _ba.newnode('text', attrs={ 'text': tip_lstr, 'scale': tip_scale, 'maxwidth': 900, 'position': (base_position[0] + t_offs, base_position[1]), 'h_align': 'left', 'vr_depth': 300, 'shadow': 1.0 if vrmode else 0.5, 'flatness': 1.0 if vrmode else 0.5, 'v_align': 'center', 'v_attach': 'bottom' }) t2pos = (base_position[0] + t_offs - (20 if icon is None else 82), base_position[1] + 2) t2node = _ba.newnode('text', owner=tnode, attrs={ 'text': tip_title, 'scale': tip_title_scale, 'position': t2pos, 'h_align': 'right', 'vr_depth': 300, 'shadow': 1.0 if vrmode else 0.5, 'flatness': 1.0 if vrmode else 0.5, 'maxwidth': 140, 'v_align': 'center', 'v_attach': 'bottom' }) if icon is not None: ipos = (base_position[0] + t_offs - 40, base_position[1] + 1) img = _ba.newnode('image', attrs={ 'texture': icon, 'position': ipos, 'scale': (50, 50), 'opacity': 1.0, 'vr_depth': 315, 'color': (1, 1, 1), 'absolute_scale': True, 'attach': 'bottomCenter' }) animate(img, 'opacity', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0}) _ba.timer(5.0, img.delete) if sound is not None: _ba.playsound(sound) combine = _ba.newnode('combine', owner=tnode, attrs={ 'input0': 1.0, 'input1': 0.8, 'input2': 1.0, 'size': 4 }) combine.connectattr('output', tnode, 'color') combine.connectattr('output', t2node, 'color') animate(combine, 'input3', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0}) _ba.timer(5.0, tnode.delete)
def submit_kill(self, showpoints: bool = True) -> None: """Submit a kill for this player entry.""" # FIXME Clean this up. # pylint: disable=too-many-statements from ba._language import Lstr from ba._general import Call self._multi_kill_count += 1 stats = self._stats() assert stats if self._multi_kill_count == 1: score = 0 name = None delay = 0.0 color = (0.0, 0.0, 0.0, 1.0) scale = 1.0 sound = None elif self._multi_kill_count == 2: score = 20 name = Lstr(resource='twoKillText') color = (0.1, 1.0, 0.0, 1) scale = 1.0 delay = 0.0 sound = stats.orchestrahitsound1 elif self._multi_kill_count == 3: score = 40 name = Lstr(resource='threeKillText') color = (1.0, 0.7, 0.0, 1) scale = 1.1 delay = 0.3 sound = stats.orchestrahitsound2 elif self._multi_kill_count == 4: score = 60 name = Lstr(resource='fourKillText') color = (1.0, 1.0, 0.0, 1) scale = 1.2 delay = 0.6 sound = stats.orchestrahitsound3 elif self._multi_kill_count == 5: score = 80 name = Lstr(resource='fiveKillText') color = (1.0, 0.5, 0.0, 1) scale = 1.3 delay = 0.9 sound = stats.orchestrahitsound4 else: score = 100 name = Lstr(resource='multiKillText', subs=[('${COUNT}', str(self._multi_kill_count))]) color = (1.0, 0.5, 0.0, 1) scale = 1.3 delay = 1.0 sound = stats.orchestrahitsound4 def _apply(name2: Lstr, score2: int, showpoints2: bool, color2: tuple[float, float, float, float], scale2: float, sound2: Optional[ba.Sound]) -> None: from bastd.actor.popuptext import PopupText # Only award this if they're still alive and we can get # a current position for them. our_pos: Optional[ba.Vec3] = None if self._sessionplayer: if self._sessionplayer.activityplayer is not None: try: our_pos = self._sessionplayer.activityplayer.position except NotFoundError: pass if our_pos is None: return # Jitter position a bit since these often come in clusters. our_pos = _ba.Vec3(our_pos[0] + (random.random() - 0.5) * 2.0, our_pos[1] + (random.random() - 0.5) * 2.0, our_pos[2] + (random.random() - 0.5) * 2.0) activity = self.getactivity() if activity is not None: PopupText(Lstr( value=(('+' + str(score2) + ' ') if showpoints2 else '') + '${N}', subs=[('${N}', name2)]), color=color2, scale=scale2, position=our_pos).autoretain() if sound2: _ba.playsound(sound2) self.score += score2 self.accumscore += score2 # Inform a running game of the score. if score2 != 0 and activity is not None: activity.handlemessage(PlayerScoredMessage(score=score2)) if name is not None: _ba.timer( 0.3 + delay, Call(_apply, name, score, showpoints, color, scale, sound)) # Keep the tally rollin'... # set a timer for a bit in the future. self._multi_kill_timer = _ba.Timer(1.0, self._end_multi_kill)
def error_message() -> None: from ba._language import Lstr _ba.playsound(_ba.getsound('error')) _ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))