def _get_ach_mult(include_pro_bonus: bool = False) -> int: """Return the multiplier for achievement pts. (just for display; changing this here won't affect actual rewards) """ from ba import _account val: int = _ba.get_account_misc_read_val('achAwardMult', 5) assert isinstance(val, int) if include_pro_bonus and _account.have_pro(): val *= 2 return val
def show_completion_banner(self, sound: bool = True) -> None: """Create the banner/sound for an acquired achievement announcement.""" from ba import _account from ba import _gameutils from bastd.actor.text import Text from bastd.actor.image import Image from ba._general import WeakCall from ba._lang import Lstr from ba._messages import DieMessage from ba._enums import TimeType, SpecialChar app = _ba.app app.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.achievement_completion_banner_slots: app.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 _account.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 get_available_sale_time(tab: str) -> Optional[int]: """(internal)""" # pylint: disable=too-many-branches # pylint: disable=too-many-nested-blocks # pylint: disable=too-many-locals try: import datetime from ba._account import have_pro from ba._enums import TimeType, TimeFormat app = _ba.app sale_times: List[Optional[int]] = [] # Calc time for our pro sale (old special case). if tab == 'extras': config = app.config if have_pro(): return None # If we haven't calced/loaded start times yet. if app.pro_sale_start_time is None: # If we've got a time-remaining in our config, start there. if 'PSTR' in config: app.pro_sale_start_time = int( _ba.time(TimeType.REAL, TimeFormat.MILLISECONDS)) app.pro_sale_start_val = config['PSTR'] else: # We start the timer once we get the duration from # the server. start_duration = _ba.get_account_misc_read_val( 'proSaleDurationMinutes', None) if start_duration is not None: app.pro_sale_start_time = int( _ba.time(TimeType.REAL, TimeFormat.MILLISECONDS)) app.pro_sale_start_val = (60000 * start_duration) # If we haven't heard from the server yet, no sale.. else: return None assert app.pro_sale_start_val is not None val: Optional[int] = max( 0, app.pro_sale_start_val - (_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS) - app.pro_sale_start_time)) # Keep the value in the config up to date. I suppose we should # write the config occasionally but it should happen often enough # for other reasons. config['PSTR'] = val if val == 0: val = None sale_times.append(val) # Now look for sales in this tab. sales_raw = _ba.get_account_misc_read_val('sales', {}) store_layout = get_store_layout() for section in store_layout[tab]: for item in section['items']: if item in sales_raw: if not _ba.get_purchased(item): to_end = ((datetime.datetime.utcfromtimestamp( sales_raw[item]['e']) - datetime.datetime.utcnow()).total_seconds()) if to_end > 0: sale_times.append(int(to_end * 1000)) # Return the smallest time i guess? return min(sale_times) if sale_times else None except Exception: from ba import _error _error.print_exception('error calcing sale time') return None
def call_after_ad(call: Callable[[], Any]) -> None: """Run a call after potentially showing an ad.""" # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals from ba._account import have_pro from ba._enums import TimeType import time app = _ba.app show = True # No ads without net-connections, etc. if not _ba.can_show_ad(): show = False if have_pro(): show = False # Pro disables interstitials. try: session = _ba.get_foreground_host_session() assert session is not None is_tournament = session.tournament_id is not None except Exception: is_tournament = False if is_tournament: show = False # Never show ads during tournaments. if show: interval: Optional[float] launch_count = app.config.get('launchCount', 0) # If we're seeing short ads we may want to space them differently. interval_mult = (_ba.get_account_misc_read_val( 'ads.shortIntervalMult', 1.0) if app.last_ad_was_short else 1.0) if app.ad_amt is None: if launch_count <= 1: app.ad_amt = _ba.get_account_misc_read_val( 'ads.startVal1', 0.99) else: app.ad_amt = _ba.get_account_misc_read_val( 'ads.startVal2', 1.0) interval = None else: # So far we're cleared to show; now calc our ad-show-threshold and # see if we should *actually* show (we reach our threshold faster # the longer we've been playing). base = 'ads' if _ba.has_video_ads() else 'ads2' min_lc = _ba.get_account_misc_read_val(base + '.minLC', 0.0) max_lc = _ba.get_account_misc_read_val(base + '.maxLC', 5.0) min_lc_scale = (_ba.get_account_misc_read_val( base + '.minLCScale', 0.25)) max_lc_scale = (_ba.get_account_misc_read_val( base + '.maxLCScale', 0.34)) min_lc_interval = (_ba.get_account_misc_read_val( base + '.minLCInterval', 360)) max_lc_interval = (_ba.get_account_misc_read_val( base + '.maxLCInterval', 300)) if launch_count < min_lc: lc_amt = 0.0 elif launch_count > max_lc: lc_amt = 1.0 else: lc_amt = ((float(launch_count) - min_lc) / (max_lc - min_lc)) incr = (1.0 - lc_amt) * min_lc_scale + lc_amt * max_lc_scale interval = ((1.0 - lc_amt) * min_lc_interval + lc_amt * max_lc_interval) app.ad_amt += incr assert app.ad_amt is not None if app.ad_amt >= 1.0: app.ad_amt = app.ad_amt % 1.0 app.attempted_first_ad = True # After we've reached the traditional show-threshold once, # try again whenever its been INTERVAL since our last successful show. elif (app.attempted_first_ad and (app.last_ad_completion_time is None or (interval is not None and _ba.time(TimeType.REAL) - app.last_ad_completion_time > (interval * interval_mult)))): # Reset our other counter too in this case. app.ad_amt = 0.0 else: show = False # If we're *still* cleared to show, actually tell the system to show. if show: # As a safety-check, set up an object that will run # the completion callback if we've returned and sat for 10 seconds # (in case some random ad network doesn't properly deliver its # completion callback). class _Payload: def __init__(self, pcall: Callable[[], Any]): self._call = pcall self._ran = False def run(self, fallback: bool = False) -> None: """Run the fallback call (and issues a warning about it).""" if not self._ran: if fallback: print(( 'ERROR: relying on fallback ad-callback! ' 'last network: ' + app.last_ad_network + ' (set ' + str(int(time.time() - app.last_ad_network_set_time)) + 's ago); purpose=' + app.last_ad_purpose)) _ba.pushcall(self._call) self._ran = True payload = _Payload(call) with _ba.Context('ui'): _ba.timer(5.0, lambda: payload.run(fallback=True), timetype=TimeType.REAL) show_ad('between_game', on_completion_call=payload.run) else: _ba.pushcall(call) # Just run the callback without the ad.