From 1adb9e79aad566dba78d027f21de66c6b5572bb5 Mon Sep 17 00:00:00 2001 From: BroBordd Date: Mon, 17 Aug 2026 09:56:06 +0300 Subject: [PATCH 1/5] added movi v1.0 --- plugins/utilities/movi.py | 13924 ++++++++++++++++++++++++++++++++++++ 1 file changed, 13924 insertions(+) create mode 100644 plugins/utilities/movi.py diff --git a/plugins/utilities/movi.py b/plugins/utilities/movi.py new file mode 100644 index 00000000..6334ada5 --- /dev/null +++ b/plugins/utilities/movi.py @@ -0,0 +1,13924 @@ +# Copyright 2025 - Solely by BrotherBoard +# Intended for personal use only +# Bug? Feedback? Telegram >> @BroBordd + +""" +Movi v1.0 - Movie Maker + +A simple movie maker with native BRP replay export. +Includes timeline sequencing, node manipulation, +custom cameras, code injection, VFX presets, and more. +""" + +import babase as ba +import bauiv1 as bui +import _babase as _ba +import bascenev1 as bs + +from re import sub +from math import ceil +from colorsys import hsv_to_rgb +from os import listdir +from io import StringIO +from shutil import copy +from hashlib import md5 +from random import choice, uniform +from base64 import b85decode +from time import perf_counter +from datetime import datetime +from json import dumps, loads +from weakref import WeakMethod +from traceback import format_exc +from os.path import join, dirname +from threading import Thread, Event +from collections import defaultdict +from zlib import decompress, compress +from ctypes import pythonapi, c_long, py_object +from contextlib import redirect_stdout, redirect_stderr + +__version__ = '1.0' + +plugman = dict( + plugin_name="movi", + description=( + "A simple movie maker with native BRP replay export. " + "Includes timeline sequencing, node manipulation, " + "custom cameras, code injection, VFX presets, and more." + ), + external_url="https://BroBordd.github.io/byBordd", + authors=[ + { + "name": "BrotherBoard", + "email": "brobordd@gmail.com", + "discord": "BrotherBoard" + }, + ], + version=__version__, +) + +class Tracker: + def __init__(self): + self.active = {} + self.active_sounds = {} + self.active_timers = {} + self.active_codes = defaultdict(dict) + self.active_seeds = {} + self.internal_timers = [] + self.active_key_schedule = {} + +class Editor: + _shared = {'callbacks':[],'on_create':[]} + + @staticmethod + def ui_safe(f): + return lambda s,*a,**k: ( + hasattr(s,'root') and + s.root.exists() and + s.ui_on and f(s,*a,**k) + ) + + @staticmethod + def clickable(f): + return lambda s,*a,**k: ( + f(s,*a,**k) if s.ui_clickable else + None if not s.ui_on else + s.toast( + s.ui_clickable is None and + Strings.INFO_SLOW_DOWN or + Strings.ERROR_PAUSE_FIRST + ) or + Eval.SOUND(Const.BAD_SOUND).play() + ) + + @staticmethod + def _call(sig): + for callback_ref in Editor._shared['callbacks']: + callback = callback_ref() + callback(sig) + + def callback(s,cb): + bui.apptimer(Const.BA_LAG_SMALL,getattr(s,cb)) + + def __init__(s,map): + s.shared_callback = WeakMethod(s.callback) + type(s)._shared['callbacks'].append(s.shared_callback) + s.ui_on = False + s.ui_clickable = False + s.original_map = map + s.timeline = [] + s.timeline_index = 0 + s.active = {} + s.active_sounds = {} + s.active_timers = {} + s.active_key_schedule = {} + s.active_codes = defaultdict(dict) + s.active_seeds = {} + s.play_timer = None + s.playing = False + s.playhead = None + s.is_wide = False + s.can_toast = True + s.toast_zoom = None + s.toast_blink = None + s.last_toast = None + s.menu_root = None + s.menu_on = False + s.seed_on = False + s.menu_kids = [] + s.event_root = None + s.event_on = False + s.event_kids = {} + s.event_top = None + s.window_on = () + s.window_sub_on = None + s.window_kids = [] + s.window_trash = [] + s.magic_x = 5.5 + s.magic_y = 5 + s.magic_right = 0.925 + s.magic_left = 1.4 + s.entry_xs = 40 + s.entry_ys = 40 + s.entry_xs_real = s.entry_xs * s.magic_right + s.entry_ys_real = s.entry_ys * s.magic_right + s.stamp_kids = [] + s.stamp_timeline = [] + s.stamp_hack = 14 + s.entries_per_sec = 5 + s.object_duration = Settings.get('entry_duration') + s.memory = {} + s.widgets = {} + s.anims = defaultdict(dict) + s.in_anims = [] + s.out_anims = [] + s.pending = [] + s.controls = [] + s.controls_shown = False + s.tools = [] + s.tools_shown = False + s.camera_timer = None + s.camera_data = {} + s.autosave_timer = None + s.autosave_kids = [] + s.autosave_img_kids = [] + s.autosave_text_kids = [] + s.autosave_text_anim = {} + s.autosave_kill_timer = None + s.about_letter_kids = [] + s.sl = None + s.global_butter = 0.3 + s.can_do = False + s.blame = None + s.increment = 1 + s.info_fps_was_on = bui.app.config.get(Const.CONFIG_FPS_KEY,False) + s.info_dev_was_on = bui.app.config.get(Const.CONFIG_DEV_KEY,False) + Settings.apply_all() + s.grid_nodes = [] + s.aspect_bars = None + s.aspect_fill_bars = [] + s.schedule_on_ui( + s.on_ui_ready, + lag=0.23 + ) + + def schedule_on_ui(s,f,lag=0.3): + if s.ui_on: f() + else: s.pending.append((lag,f)) + + def universal_back(s): + if s.window_sub_on: + s.window_sub_on[2]() + return + if s.window_on or s.event_on: + s.event_button.activate() + else: s.square.activate() + + @ui_safe + def on_resize(s): + s.on_scroll() + s.wrap_all() + + @ui_safe + def on_rescale(s): + s.on_scroll() + s.wrap_all() + + def save_state(s): + Config.set('last',Eval.ENCODE(s.memory)) + + @ui_safe + def autosave(s): + if not s.memory: return + if not Settings.get('autosave_on'): return + s.save_state() + s.play_autosave_anim() + + def restart_autosave_timer(s): + """Called whenever Settings' 'autosave_interval' changes so + the live AppTimer picks up the new period immediately.""" + s.autosave_timer = None + s.autosave_timer = bui.AppTimer( + max(Settings.get('autosave_interval'),Const.AUTOSAVE_MIN_INTERVAL), + s.autosave, repeat=True + ) + + def build_grid(s): + """Populates the scene with spatial references while editing - + a flat 2D grid, a full 3D lattice, or both, per their own + independent settings. Purely visual - never touched by + playback or export. Safe to call repeatedly (always clears + the previous grid first). + + The 2D grid draws actual lines (thin red 'image' nodes tinted + over the plain 'white' texture) rather than a dot at every + intersection - the 3D grid stays as locator-box markers, + since a full lattice of lines would just be visual noise.""" + s.destroy_grid() + if s.playing: return + show_2d = Settings.get('show_grid_2d') + show_3d = Settings.get('show_grid_3d') + if not show_2d and not show_3d: return + try: + activity = bs.get_foreground_host_activity() + except Exception: + return + span = Const.GRID_SPAN + step = Const.GRID_STEP + n = int(span/step) + try: + with activity.context: + if show_2d: + rx,ry = bui.get_virtual_screen_size() + step2d = min(rx,ry)/Const.GRID_2D_DIVISIONS + nx2d = int((rx/2)/step2d) + ny2d = int((ry/2)/step2d) + total_len_x = 2*nx2d*step2d + total_len_y = 2*ny2d*step2d + thickness = Const.GRID_2D_THICKNESS + grid_opacity = min(Color.OPACITY*1.6,1) + for ix in range(-nx2d,nx2d+1): + s.grid_nodes.append(bs.newnode( + 'image', + attrs={ + 'texture':bs.gettexture('white'), + 'position':(ix*step2d,0.0), + 'scale':(thickness,total_len_y), + 'color':(1,0,0), + 'opacity':grid_opacity + } + )) + for iy in range(-ny2d,ny2d+1): + s.grid_nodes.append(bs.newnode( + 'image', + attrs={ + 'texture':bs.gettexture('white'), + 'position':(0.0,iy*step2d), + 'scale':(total_len_x,thickness), + 'color':(1,0,0), + 'opacity':grid_opacity + } + )) + if show_3d: + span_y = Const.GRID_SPAN_3D + step_y = Const.GRID_STEP_3D + ny = int(span_y/step_y) + for ix in range(-n,n+1): + for iy in range(-ny,ny+1): + for iz in range(-n,n+1): + if show_2d and iy == 0: continue + s.grid_nodes.append(bs.newnode( + 'locator', + attrs={ + 'position':(ix*step,iy*step_y,iz*step), + 'shape':'box', + 'size':[Const.GRID_MARK_SIZE]*3, + 'color':Color.WARM, + 'opacity':min(Color.OPACITY*1.6,1), + 'additive':False, + 'draw_beauty':True + } + )) + except Exception as e: + print(format_exc()) + s.destroy_grid() + + def destroy_grid(s): + for n in getattr(s,'grid_nodes',None) or []: + if n.exists(): n.delete() + s.grid_nodes = [] + + def play_autosave_anim(s): + for w in s.autosave_kids: + if w.exists(): w.delete() + s.autosave_kids.clear() + s.autosave_img_kids.clear() + s.autosave_text_kids.clear() + s.autosave_text_anim.clear() + + rx,ry = bui.get_virtual_screen_size() + area = Const.AUTOSAVE_AREA + marg = Const.AUTOSAVE_MARGIN + pad = Const.AUTOSAVE_BG_PADDING + fancy = Settings.get('fancy_autosave') + epic = Settings.get('epic_mode') + + icon_pad = pad if fancy else Const.AUTOSAVE_COMPACT_BG_PAD/2 + + # icon sits inset by `icon_pad` from the container's edges + area_bl = (marg+icon_pad,ry-marg-icon_pad-area) + area_cx = marg+icon_pad+area/2 + area_cy = ry-marg-icon_pad-area/2 + + def square(size,pos,opacity): + w = bui.imagewidget( + parent=s.root, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + position=pos, + size=size, + opacity=opacity + ) + s.autosave_kids.append(w) + s.autosave_img_kids.append(w) + return w + + p_dur = Const.AUTOSAVE_PARENT_DUR + + if fancy: + inner_w = Const.AUTOSAVE_BG_WIDTH + bg_w = inner_w+pad*2 + bg_h = area+pad*2 + else: + inner_w = area + bg_w = bg_h = area+icon_pad*2 + bg_end_pos = (marg,ry-marg-bg_h) + bg_cx = marg+bg_w/2 + bg_cy = ry-marg-bg_h/2 + pop = Const.AUTOSAVE_BG_POP_SCALE * (Const.EPIC_POP_MULT if epic else 1) + bg_start_size = (bg_w*pop,bg_h*pop) + bg_start_pos = (bg_cx-bg_start_size[0]/2,bg_cy-bg_start_size[1]/2) + + bg = square(bg_start_size,bg_start_pos,0) + Animate( + widget=bg, + duration=p_dur, + attrs={ + 'size':(bg_start_size,(bg_w,bg_h)), + 'position':(bg_start_pos,bg_end_pos), + 'opacity':(0,Color.OPACITY) + } + ) + + text_x = area_bl[0]+area-5 + text_w = inner_w-area-pad + + title = None + if fancy: + title = bui.textwidget( + parent=s.root, + text=choice(Const.AUTOSAVE_TITLES), + position=(text_x,bg_cy+14), + size=(text_w,20), + h_align='left', + v_align='center', + scale=0.9, + maxwidth=text_w, + color=Const.INVISIBLE + ) + s.autosave_kids.append(title) + s.autosave_text_kids.append(title) + s.autosave_text_anim[id(title)] = Animate( + widget=title, + duration=p_dur, + attrs={'color':(Const.INVISIBLE,(*Color.TEXT,Color.TEXT_OPACITY))} + ) + + tips = [] + if fancy: + for index,line in enumerate( + choice(Const.AUTOSAVE_TIPS).splitlines() + ): + tip = bui.textwidget( + parent=s.root, + text=line, + position=(text_x-30,bg_cy-10-(index*15)), + size=(text_w,30), + h_align='left', + v_align='center', + scale=0.6, + maxwidth=text_w, + color=Const.INVISIBLE + ) + s.autosave_kids.append(tip) + s.autosave_text_kids.append(tip) + s.autosave_text_anim[id(tip)] = Animate( + widget=tip, + duration=p_dur, + attrs={'color':(Const.INVISIBLE,(*Color.TEXT,Color.TEXT_OPACITY*0.7))} + ) + tips.append(tip) + + p_end_pos = (area_cx-area/2,area_cy-area/2) + + parent = square((area,area),p_end_pos,0) + Animate( + widget=parent, + duration=p_dur, + attrs={ + 'opacity':(0,Color.OPACITY) + } + ) + + swap_dur = Const.AUTOSAVE_CHILD_GROW_DUR + swap_wait = Const.AUTOSAVE_CHILD_WAIT1*2 + swap_count = Const.AUTOSAVE_SWAP_COUNT * (Const.EPIC_SWAP_MULT if epic else 1) + min_diff = Const.AUTOSAVE_MIN_SPLIT_DIFF + + area_tr_corner = (marg+icon_pad+area, ry-marg-icon_pad) + + def bl_pos(sz): + return area_bl + + def tr_pos(sz): + return (area_tr_corner[0]-sz, area_tr_corner[1]-sz) + + def rand_split(prev_pct=None): + while True: + pct = uniform(0.15,0.85) + if prev_pct is None or abs(pct-prev_pct) >= min_diff: + return pct + + pct = rand_split() + a_size,b_size = area*pct, area*(1-pct) + + # square A starts at bottom-left, square B at top-right + a = square((a_size,a_size),bl_pos(a_size),0) + b = square((b_size,b_size),tr_pos(b_size),0) + + Animate( + widget=a, + duration=p_dur, + attrs={'opacity':(0,Color.OPACITY)} + ) + Animate( + widget=b, + duration=p_dur, + attrs={'opacity':(0,Color.OPACITY)} + ) + + last_delay = 0 + anims_off = not Settings.get('ui_anim_on') + + if not anims_off: + for i in range(swap_count): + base = p_dur+i*(swap_dur+swap_wait) + a_from,b_from = a_size,b_size + pct = rand_split(pct) + a_size,b_size = area*pct, area*(1-pct) + a_to,b_to = a_size,b_size + + Animate( + widget=a, + delay=base, + duration=swap_dur, + attrs={ + 'size':((a_from,a_from),(a_to,a_to)), + 'position':(bl_pos(a_from),bl_pos(a_to)) + } + ) + Animate( + widget=b, + delay=base, + duration=swap_dur, + attrs={ + 'size':((b_from,b_from),(b_to,b_to)), + 'position':(tr_pos(b_from),tr_pos(b_to)) + } + ) + + last_delay = base+swap_dur+swap_wait + else: + last_delay = Const.AUTOSAVE_STATIC_HOLD + + def die(): + for w in (a,b,parent,title,*tips): + if w is not None and w.exists(): w.delete() + s.autosave_text_anim.pop(id(w),None) + Animate( + widget=bg, + duration=p_dur, + attrs={ + 'size':((bg_w,bg_h),bg_start_size), + 'position':(bg_end_pos,bg_start_pos), + 'opacity':(Color.OPACITY,0) + }, + on_finish=lambda: bg.exists() and bg.delete() + ) + s.autosave_kill_timer = bui.AppTimer(last_delay, die) + + def recreate(s): + type(s)._shared['callbacks'].remove(s.shared_callback) + Movi.recreate() + + @ui_safe + def on_scroll(s): + if s.event_on: + for kid in s.event_kids: + an = s.anims[id(kid)] + for _ in ['extra','to']: + (a:=an.get(_,None)) and a.cancel() + + def on_ui_ready(s): + if (last:=Config.get('last')): + try: memory = Eval.DECODE(last) + except Exception as e: + s.toast(Format.ERROR(e)) + Eval.SOUND(Const.BAD_SOUND).play() + return + s.load_memory(memory,shut=True) + bui.apptimer(Const.BA_LAG_BIG,s.on_rescale) + on_create = type(s)._shared['on_create'] + for call,args in on_create: call(s,*args) + on_create.clear() + s.autosave_timer = bui.AppTimer( + max(Settings.get('autosave_interval'),Const.AUTOSAVE_MIN_INTERVAL),s.autosave,repeat=True + ) + s.build_grid() + + def start_recording(s): + s.export_flag = True + s.play() + s.toast(Strings.INFO_RECORDING_NOW) + bui.buttonwidget( + s.controls[0], + label=Strings.STOP_RECORDING_LABEL + ) + + def render_export_filename(s,uid): + """Turns the 'export_filename_template' setting (e.g. + 'movi_{uuid}') into a real, filesystem-safe base filename - + no extension, that's added by the caller. Unknown {tokens} + are dropped instead of raising, and anything that isn't + alnum/underscore/dash collapses to '_' so a stray character + in the template can't produce a broken path.""" + now = datetime.now() + tokens = defaultdict(str,{ + 'uuid':uid, + 'date':now.strftime('%Y%m%d'), + 'time':now.strftime('%H%M%S'), + }) + template = Settings.get('export_filename_template') or Const.EXPORT_DEFAULT_TEMPLATE + try: + rendered = template.format_map(tokens) + except Exception: + rendered = Const.EXPORT_DEFAULT_TEMPLATE.format_map(tokens) + rendered = sub(r'[^A-Za-z0-9_\-]+','_',rendered).strip('_') + return rendered or Const.EXPORT_PREFIX+uid + + def export_replay(s,wait=True): + if wait: + bui.apptimer( + Const.BA_LAG_BIG, + bui.CallPartial( + s.export_replay, + wait=False + ) + ) + return + uid = md5( + dumps( + s.memory, + sort_keys=True + ).encode() + ).hexdigest()[:8] + base_name = s.render_export_filename(uid) + name = base_name+Const.EXPORT_SUFFIX + copy( + join( + Const.REPLAYS, + Const.STOCK_REPLAY + ), + join( + Const.REPLAYS, + name + ) + ) + if Settings.get('brp_text_export'): + text_name = base_name+Const.EXPORT_TEXT_SUFFIX + try: + with open(join(Const.REPLAYS,text_name),'w',encoding='utf-8') as f: + f.write(dumps(s.memory,indent=2,sort_keys=True)) + except Exception as e: + s.toast(Format.ERROR(e)) + s.toast(Format.SAVED_AS(name)) + Eval.SOUND(Const.GOOD_SOUND).play() + + def toast(s,inp=None,shut=1,extra=0): + shut or Eval.SOUND(Const.OK_SOUND).play() + if not s.can_toast and not shut: return + if s.can_do and extra<1: s.can_do = False + if s.toast_blink: + s.toast_blink.cancel() + s.toast_blink = None + if s.toast_zoom: s.toast_zoom.cancel() + s.can_toast = False + b = s.toast_bg + t,desc = inp or ('','') + if not s.blame: + s.blame = Eval.BLAME( + Const.BLAME(), + Const.BLAME_CHARSET + ) + desc and bui.buttonwidget( + b,on_activate_call=bui.CallPartial( + s.toast, + (desc,choice(s.blame)), + shut=0, + extra=extra-1 + ) + ) + text_width = t and Eval.STRING_WIDTH(t) or 0 + duration = 0.45 + end_size = dx,dy = (text_width+(t and 20 or 0),30) + start_size = (0,dy) + start_opacity = 0 + zero = 0.0001 + x,y = ox,oy = s.toast_position + end_pos = epx,epy = (ox-dx/2,oy) + rush = False + if (anim:=s.anims.get(id(b),None)): + start_size = stx,sty = anim.attrs_current['size'] + if ( + (int(stx) == int(dx)) and + (int(sty) == int(dy)) + ): rush = True + x,y = anim.attrs_current['position'] + start_opacity = anim.attrs_current['opacity'] + anim.cancel() + def enable(): s.can_toast = True + zoom_time = 0.2 + def zoom(): + s.toast_zoom = Animate( + widget=b, + attrs={ + 'size':( + end_size, + (dx*1.1,dy*1.1) + ), + 'position':( + end_pos, + (epx-dx*0.1/2,epy-dy*0.1/2) + ) + }, + duration=zoom_time, + on_finish=(enable,) + ) + start_textcolor = (*Color.TEXT,Color.TEXT_OPACITY) + blink_time = 0.2 + apply_text = bui.CallPartial( + bui.buttonwidget, + b, label=t + ) + skip_blink = s.last_toast == t + def blink(): + if (anim:=s.toast_blink): + anim.cancel() + s.toast_blink = Animate( + widget=b, + attrs={ + 'textcolor':( + start_textcolor, + skip_blink and start_textcolor or Const.INVISIBLE + ) + }, + duration=skip_blink and zero or blink_time, + on_finish=(None,), + on_reverse=apply_text, + on_cancel=apply_text + ) + blink() + s.anims[id(b)] = Animate( + widget=b, + attrs={ + 'size':(start_size,end_size), + 'opacity':( + start_opacity, + t and Color.OPACITY or 0 + ), + 'position':( + (x,y), + end_pos + ) + }, + duration=rush and zero or duration, + on_finish=zoom + ) + s.toast_timer = inp and bui.AppTimer( + max(len(t)*0.07,Settings.get('toast_duration')), + s.toast + ) + s.last_toast = t + + def reset_toast_position(s): + """toast()'s continuous-toast-chain animation deliberately + starts each new toast from the previous one's current position + (anim.attrs_current) instead of s.toast_position, so back-to- + back toasts flow into each other instead of jumping. But that + means once 'toast_top' changes, the very next toast still + inherits the old anim's last position and springs across the + screen to the new spot - only settling into place correctly + from then on, since by then attrs_current matches the new + s.toast_position. Dropping the stored anim here makes that + next toast fall through to s.toast_position immediately, + exactly like the very first toast of a session does.""" + if (anim := s.anims.pop(id(s.toast_bg),None)): + anim.cancel() + + def make(s): + s.root = bui.containerwidget( + parent=bui.get_special_widget('overlay_stack'), + background=False + ) + bui.containerwidget( + s.root, + cancel_button=( + bui.buttonwidget( + parent=s.root, + size=(0,0), + label='', + selectable=False, + enable_sound=False, + on_activate_call=s.universal_back, + texture=Eval.TEXTURE(Const.EMPTY) + ) + ) + ) + s.stamp_bg = bui.imagewidget( + parent=s.root, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + opacity=0 + ) + s.square = bui.buttonwidget( + parent=s.root, + texture=Eval.TEXTURE(Const.SKIN), + label=Eval.CHAR(Const.SQUARE), + color=Color.BASE, + textcolor=(*Color.TEXT,Color.TEXT_OPACITY), + enable_sound=False, + on_activate_call=s.on_square + ) + s.triangle = bui.buttonwidget( + parent=s.root, + texture=Eval.TEXTURE(Const.SKIN), + label=Eval.CHAR(Const.TRIANGLE), + color=Color.BASE, + textcolor=(*Color.TEXT,Color.TEXT_OPACITY), + enable_sound=False, + on_activate_call=s.on_triangle + ) + s.circle = bui.buttonwidget( + parent=s.root, + texture=Eval.TEXTURE(Const.SKIN), + label=Eval.CHAR(Const.CIRCLE), + color=Color.BASE, + textcolor=(*Color.TEXT,Color.TEXT_OPACITY), + enable_sound=False, + on_activate_call=s.on_circle + ) + s.circle_shadow = bui.imagewidget( + parent=s.root, + opacity=0, + texture=Eval.TEXTURE(Const.SHADOW), + color=Color.SHADOW + ) + s.about = bui.buttonwidget( + parent=s.root, + texture=Eval.TEXTURE(Const.SKIN), + label=Strings.ABOUT_LABEL, + color=Color.BASE, + textcolor=(*Color.TEXT,Color.TEXT_OPACITY), + enable_sound=False, + on_activate_call=s.on_about + ) + s.about_shadow = bui.imagewidget( + parent=s.root, + opacity=0, + texture=Eval.TEXTURE(Const.SHADOW), + color=Color.SHADOW + ) + s.stamp_scroll = bui.scrollwidget( + parent=s.root, + border_opacity=0, + color=Color.BASE, + on_select_call=s.on_scroll + ) + s.stamp_scroll_root = bui.containerwidget( + parent=s.stamp_scroll, + background=False + ) + s.stamp_hscroll = bui.hscrollwidget( + parent=s.stamp_scroll_root, + border_opacity=0, + color=Color.BASE + ) + s.stamp_hscroll_root = bui.containerwidget( + parent=s.stamp_hscroll, + background=False + ) + s.top_left_h = bui.textwidget( + parent=s.stamp_hscroll_root + ) + s.top_left_v = bui.textwidget( + parent=s.stamp_scroll_root + ) + s.bottom_left_h = bui.textwidget( + parent=s.stamp_hscroll_root, + position=(0,0), + size=(10,10) + ) + s.bottom_left_v = bui.textwidget( + parent=s.stamp_scroll_root, + position=(0,0) + ) + s.event_root = bui.imagewidget( + parent=s.root, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + opacity=0 + ) + s.event_button = bui.buttonwidget( + parent=s.root, + label=Strings.EVENT_BUTTON_OFF, + on_activate_call=s.toggle_event, + texture=Eval.TEXTURE(Const.EMPTY), + opacity=0, + textcolor=Const.INVISIBLE, + enable_sound=False + ) + for i,n in enumerate(Strings.EVENTS): + b = bui.buttonwidget( + parent=s.root, + label=n, + color=Color.BASE, + textcolor=Const.INVISIBLE, + texture=Eval.TEXTURE(Const.SKIN), + opacity=0, + enable_sound=False, + selectable=False + ) + sh = bui.imagewidget( + parent=s.root, + opacity=0, + texture=Eval.TEXTURE(Const.SHADOW), + color=Color.SHADOW + ) + s.event_kids[b] = {'shadow':sh} + s.edit_button = bui.buttonwidget( + parent=s.root, + label=Strings.EDIT_BUTTON, + on_activate_call=s.edit_window, + texture=Eval.TEXTURE(Const.SKIN), + opacity=0, + textcolor=Const.INVISIBLE, + enable_sound=False, + color=Color.BASE + ) + s.edit_button_shadow = bui.imagewidget( + parent=s.root, + opacity=0, + texture=Eval.TEXTURE(Const.SHADOW), + color=Color.SHADOW + ) + s.key_button = bui.buttonwidget( + parent=s.root, + label=Strings.KEYS, + on_activate_call=s.key_window, + texture=Eval.TEXTURE(Const.SKIN), + opacity=0, + textcolor=Const.INVISIBLE, + enable_sound=False, + color=Color.BASE + ) + s.key_button_shadow = bui.imagewidget( + parent=s.root, + opacity=0, + texture=Eval.TEXTURE(Const.SHADOW), + color=Color.SHADOW + ) + for i,t in enumerate(Const.TOOLS): + b = bui.buttonwidget( + parent=s.root, + color=Color.BASE, + opacity=0, + textcolor=Const.INVISIBLE, + enable_sound=False, + texture=Eval.TEXTURE(Const.SKIN), + label=Eval.CHAR(t), + on_activate_call=bui.CallPartial( + s.do_tool, i + ), + repeat=True + ) + s.tools.append(b) + for i,t in enumerate(Const.CONTROLS): + b = bui.buttonwidget( + parent=s.root, + color=Color.BASE, + opacity=0, + textcolor=Const.INVISIBLE, + enable_sound=False, + texture=Eval.TEXTURE(Const.SKIN), + label=( + isinstance(t,str) + and Eval.CHAR(t) + or Eval.CHAR(t[s.playing]) + ), + on_activate_call=bui.CallPartial( + s.do_control, i + ), + size=(0,0) + ) + s.controls.append(b) + s.toast_bg = bui.buttonwidget( + parent=s.root, + label='', + enable_sound=False, + selectable=False, + size=(0,0), + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE + ) + s.make_menu() + s.ui_clickable = True + s.wrap_all(init=True) + s.make_timeline(init=True) + s.wrap_timeline() + s.top_left() + bui.apptimer(0.3,s.toggle_ui) + for lag,call in s.pending: + bui.apptimer(lag,call) + s.pending.clear() + + def make_timeline(s,init=False): + for i,j in s.stamp_timeline: + i.delete() + j.delete() + s.stamp_timeline.clear() + eps = s.entries_per_sec + num_markers = int(s.stamp_deep_x / s.entry_xs_real) + 5 + for i in range(num_markers): + t = bui.textwidget( + parent=s.stamp_hscroll_root, + text=( + i%eps == 0 + and str(int(i/eps)) + or '.' + ), + h_align=Const.ALIGN, + v_align=Const.ALIGN, + size=(10,5), + scale=0.5, + color=( + Const.INVISIBLE if init else + (*Color.TEXT,Color.TEXT_OPACITY) + ) + ) + l = bui.imagewidget( + parent=s.stamp_hscroll_root, + texture=Eval.TEXTURE(Const.SKIN), + opacity=0 if init else Color.OPACITY/10, + size=(2,s.stamp_deep_y*2), + color=Color.TEXT + ) + s.stamp_timeline.append((t,l)) + + def build_timeline(s): + s.timeline = [] + for btn_id, mem in s.memory.items(): + btn = next( + (b for b in s.stamp_kids if id(b) == btn_id), + None + ) + if not btn: continue + s.timeline.append({ + 'time': mem['start'], + 'type': 'start', + 'button': btn, + 'memory': mem, + 'btn_id': btn_id + }) + s.timeline.append({ + 'time': mem['start'] + mem['duration'], + 'type': 'end', + 'button': btn, + 'memory': mem, + 'btn_id': btn_id + }) + s.timeline.sort(key=lambda x: x['time']) + s.max_time = s.timeline[-1]['time'] if s.timeline else 0 + + def wrap_timeline(s): + for i,g in enumerate(s.stamp_timeline): + t,l = g + px = i*s.entry_xs_real + py = s.stamp_deep_y-20 + bui.textwidget( + t, + position=(px,py) + ) + bui.imagewidget( + l, + position=(px+4,-s.stamp_deep_y/2) + ) + + def refresh_ui(s,message=None): + """Existing widgets don't repaint themselves just because a + live global (Color.OPACITY, Color.BASE/COLD/WARM/TEXT via a + theme swap, or a Strings.* lookup via a language swap) + changed underneath them - a hide/show cycle is what actually + rebuilds them onto whatever's current (animate_in() re-reads + every one of those fresh each time it runs). Shared by every + setting that needs a full repaint - opacity, theme, and + language all fall through to this one path. No-op if the UI + was already hidden - nothing visible to refresh, and flipping + it on would undo whatever the person hid it for.""" + if not s.ui_on: return + message and s.toast(message) + s.toggle_ui(on_finish=lambda: bui.apptimer(0.2,s.toggle_ui)) + + def refresh_opacity_via_ui_toggle(s): + s.refresh_ui(Strings.INFO_CHANGING_OPACITY) + + def repaint_theme(s): + """refresh_ui()/animate_in() only ever re-paints the small + handful of widgets wired into the hide/show fade - everything + else built once in make() (or added to it later, like + event_kids/stamp_kids) bakes in whatever Color.BASE was live + at creation and just keeps it forever, so a theme change only + ever reached that cherry-picked subset. This walks every + persistent widget instead and snaps its fill straight to the + current Color.BASE, so a theme switch actually repaints the + whole UI - regardless of ui_on, since these exist either way. + + Only ever touches `color` (the background fill), never + `textcolor`/`opacity` - several of these widgets (edit_button, + key_button, event_root/event_kids, tools, controls) have their + own separate show/hide state machine driving those, and + stomping them here would fight that instead of just fixing + the color underneath it. Shadow widgets get Color.SHADOW + instead of Color.BASE - they're a separate, always-dark tint + so a shadow still reads as a shadow on light/pale themes + instead of brightening along with the panel it's cast by. + + stamp_kids are the one exception to the color-only rule above: + unlike edit_button/tools/controls/etc, they don't have any + ongoing show/hide cycle of their own once created (fade in + once via add_entry/load_memory, then just sit on the timeline + for the rest of the session) - nothing else was ever going to + come back and refresh their textcolor, so it stayed pinned to + whatever theme was live when each one was created.""" + if not (hasattr(s,'root') and s.root.exists()): return + bui.imagewidget(s.stamp_bg,color=Color.BASE) + bui.buttonwidget(s.square,color=Color.BASE,textcolor=(*Color.TEXT,Color.TEXT_OPACITY)) + bui.buttonwidget(s.triangle,color=Color.BASE,textcolor=(*Color.TEXT,Color.TEXT_OPACITY)) + bui.buttonwidget(s.circle,color=Color.BASE,textcolor=(*Color.TEXT,Color.TEXT_OPACITY)) + bui.imagewidget(s.circle_shadow,color=Color.SHADOW) + if (anim := s.anims.get(id(s.circle),{}).get('window')): + anim.attrs_start['textcolor'] = (*Color.TEXT,Color.TEXT_OPACITY) + bui.buttonwidget(s.about,color=Color.BASE,textcolor=(*Color.TEXT,Color.TEXT_OPACITY)) + bui.imagewidget(s.about_shadow,color=Color.SHADOW) + if (anim := s.anims.get(id(s.about),{}).get('window')): + anim.attrs_start['textcolor'] = (*Color.TEXT,Color.TEXT_OPACITY) + bui.scrollwidget(s.stamp_scroll,color=Color.BASE) + bui.hscrollwidget(s.stamp_hscroll,color=Color.BASE) + bui.imagewidget(s.event_root,color=Color.BASE) + for b,data in s.event_kids.items(): + b.exists() and bui.buttonwidget(b,color=Color.BASE) + data['shadow'].exists() and bui.imagewidget(data['shadow'],color=Color.SHADOW) + bui.buttonwidget(s.edit_button,color=Color.BASE) + bui.imagewidget(s.edit_button_shadow,color=Color.SHADOW) + bui.buttonwidget(s.key_button,color=Color.BASE) + bui.imagewidget(s.key_button_shadow,color=Color.SHADOW) + for b in s.tools: + b.exists() and bui.buttonwidget(b,color=Color.BASE) + for b in s.controls: + b.exists() and bui.buttonwidget(b,color=Color.BASE) + bui.buttonwidget(s.toast_bg,color=Color.BASE) + for kid in s.stamp_kids: + kid.exists() and bui.buttonwidget( + kid,color=Color.BASE,textcolor=(*Color.TEXT,Color.TEXT_OPACITY) + ) + for t,l in s.stamp_timeline: + t.exists() and bui.textwidget(t,color=(*Color.TEXT,Color.TEXT_OPACITY)) + l.exists() and bui.imagewidget(l,color=Color.TEXT) + bui.imagewidget(s.menu_bg,color=Color.BASE) + for kid in s.menu_kids: + kid.exists() and bui.buttonwidget(kid,color=Color.BASE) + bui.textwidget(s.seed_input,color=(*Color.TEXT,Color.TEXT_OPACITY)) + + for w in s.autosave_img_kids: + w.exists() and bui.imagewidget(w,color=Color.BASE) + for w in s.autosave_text_kids: + if not w.exists(): continue + anim = s.autosave_text_anim.get(id(w)) + if not anim: continue + for attrs in (anim.attrs_start,anim.attrs_end): + c = attrs.get('color') + if c and c != Const.INVISIBLE and len(c) == 4: + attrs['color'] = (*Color.TEXT,c[3]) + cur = anim.attrs_current.get('color') + if cur and tuple(cur) != Const.INVISIBLE and len(cur) == 4: + new_cur = (*Color.TEXT,cur[3]) + anim.attrs_current['color'] = list(new_cur) + bui.textwidget(w,color=new_cur) + + for w in s.about_letter_kids: + if not w.exists(): continue + if id(w) in s.anims and 'about_sweep' in s.anims[id(w)]: + continue + bui.imagewidget(w,color=Color.TEXT,opacity=Color.TEXT_OPACITY) + + def refresh_theme(s): + Settings.apply_all() + s.repaint_theme() + s.refresh_ui(Strings.INFO_CHANGING_THEME) + + def repaint_language(s): + if not (hasattr(s,'root') and s.root.exists()): return + bui.buttonwidget(s.about,label=Strings.ABOUT_LABEL) + bui.buttonwidget(s.edit_button,label=Strings.EDIT_BUTTON) + bui.buttonwidget(s.key_button,label=Strings.KEYS) + bui.buttonwidget( + s.event_button, + label=s.event_on and Strings.EVENT_BUTTON_ON or Strings.EVENT_BUTTON_OFF + ) + for kid,label in zip(s.menu_kids,Strings.MENUS): + kid.exists() and bui.buttonwidget(kid,label=label) + + def refresh_language(s): + s.repaint_language() + s.refresh_ui(Strings.INFO_CHANGING_LANGUAGE) + + def toggle_ui(s,on_finish=None): + if s.ui_on: + s.animate_out(on_finish=on_finish) + s.ui_on = False + s.ui_clickable = False + else: + s.animate_in(on_finish=on_finish) + s.ui_on = True + s.ui_clickable = True + if s.ui_on: s.restore_infos() + else: s.set_infos(False) + + def set_infos(s,b): + bui.app.config[Const.CONFIG_FPS_KEY] = b + bui.app.config[Const.CONFIG_DEV_KEY] = b + bui.app.config.apply_and_commit() + + def restore_infos(s): + bui.app.config[Const.CONFIG_FPS_KEY] = s.info_fps_was_on + bui.app.config[Const.CONFIG_DEV_KEY] = s.info_dev_was_on + bui.app.config.apply_and_commit() + + def animate_in(s,on_finish=None): + bui.scrollwidget( + s.stamp_scroll, + size=s.stamp_size + ) + butter = s.global_butter * 2 + for anim in s.out_anims: + anim.cancel() + s.in_anims.clear() + s.out_anims.clear() + def cleanup(): + callable(on_finish) and on_finish() + a = Animate( + widget=s.stamp_bg, + duration=butter, + attrs={ + 'opacity':(0,Color.OPACITY) + }, + on_finish=cleanup + ) + s.in_anims.append(a) + for t,l in s.stamp_timeline: + a = Animate( + widget=t, + duration=butter, + attrs={ + 'color':( + Const.INVISIBLE, + (*Color.TEXT,Color.TEXT_OPACITY) + ) + } + ) + s.in_anims.append(a) + a = Animate( + widget=l, + duration=butter, + attrs={ + 'opacity':(0,Color.OPACITY/10) + } + ) + s.in_anims.append(a) + for kid in s.stamp_kids: + a = Animate( + widget=kid, + duration=butter, + attrs={ + 'opacity':(0,Color.OPACITY), + 'color':(Color.BASE,Color.BASE) + } + ) + s.in_anims.append(a) + a = Animate( + widget=s.event_button, + duration=butter, + attrs={ + 'textcolor':( + Const.INVISIBLE, + (*Color.TEXT,Color.TEXT_OPACITY) + ) + } + ) + s.in_anims.append(a) + a = Animate( + widget=s.event_root, + duration=butter, + attrs={ + 'opacity':(0,Color.OPACITY) + } + ) + s.in_anims.append(a) + a = Animate( + widget=s.edit_button, + duration=butter, + attrs={ + 'opacity':(0,Color.OPACITY), + 'textcolor':( + Const.INVISIBLE, + (*Color.TEXT,Color.TEXT_OPACITY) + ) + } + ) + s.in_anims.append(a) + a = Animate( + widget=s.key_button, + duration=butter, + attrs={ + 'opacity':(0,Color.OPACITY), + 'textcolor':( + Const.INVISIBLE, + (*Color.TEXT,Color.TEXT_OPACITY) + ) + } + ) + s.in_anims.append(a) + if len(s.memory): + if s.sl: s.show_tools() + else: s.show_controls() + + def animate_out(s,on_finish=None): + butter = s.global_butter*2 + s.collapse_all(hard=True) + bui.scrollwidget( + s.stamp_scroll, + size=(0,0) + ) + s.out_anims.clear() + for anim in s.in_anims: + a = anim.reverse( + duration=butter + ) + s.out_anims.append(a) + s.in_anims.clear() + callable(on_finish) and bui.apptimer(butter,on_finish) + + def collapse_all(s,hard=False): + if s.window_sub_on: + s.window_sub_on[2](instant=True) + if not s.event_on and s.window_on: + s.window_back(into_nothing=True) + if s.event_on and s.window_on: + s.window_back(into_nothing=True,skip=True) + s.toggle_event() + if s.event_on: + s.toggle_event() + if s.controls_shown and (hard or not s.memory): + s.hide_controls() + if s.tools_shown: + s.hide_tools() + + @clickable + def edit_window(s): + s.dismiss_window() + if not s.sl: + Eval.SOUND(Const.BAD_SOUND).play() + s.toast(Strings.ERROR_SELECT_SOMETHING) + return + Eval.SOUND(Const.OK_SOUND).play() + bui.buttonwidget( + s.edit_button, + on_activate_call=Const.DO_NOTHING, + selectable=False + ) + start_pos = s.event_on and s.edit_button_pos2 or s.edit_button_pos + end_pos = s.window_pos + start_size = s.edit_button_size + end_size = s.window_size + butter = s.global_butter*1.3 + s.anims[id(s.edit_button)]['window'] = Animate( + s.edit_button, + duration=butter, + attrs={ + 'position':(start_pos,end_pos), + 'size':(start_size,end_size), + 'textcolor':( + (*Color.TEXT,Color.TEXT_OPACITY), + Const.INVISIBLE + ) + } + ) + s.anims[id(s.edit_button)]['shadow'] = ( + Animate( + widget=s.edit_button_shadow, + attrs={ + 'opacity':(0,Color.SHADOW_OPACITY), + 'position':( + start_pos, + s.window_shadow_pos + ), + 'size':( + start_size, + s.window_shadow_size + ) + }, + duration=butter + ) + ) + b = s.sl + mem = s.memory[id(b)] + ret = s.make_window_kids( + mem['event'], edit=mem + ) + on_back = lambda: ( + callable(ret) and ret(), + s.toast(Strings.INFO_DISCARDED) + ) + s.window_on = (s.edit_button,s.edit_window,on_back) + + @clickable + def key_window(s): + if not s.sl: + Eval.SOUND(Const.BAD_SOUND).play() + s.toast(Strings.ERROR_SELECT_SOMETHING) + return + si = s.memory[id(s.sl)]['event'] + if not (keys:=Const.EVENT_KEYS.get(si,())): + Eval.SOUND(Const.BAD_SOUND).play() + s.toast(Strings.NO_ACTIONS) + return + if s.window_on: + s.dismiss_window() + Eval.SOUND(Const.OK_SOUND).play() + bui.buttonwidget( + s.key_button, + on_activate_call=Const.DO_NOTHING, + selectable=False + ) + start_pos = s.event_on and s.key_button_pos2 or s.key_button_pos + end_pos = s.window_pos + start_size = s.edit_button_size + end_size = s.window_size + butter = s.global_butter*1.3 + s.anims[id(s.key_button)]['window'] = Animate( + s.key_button, + duration=butter, + attrs={ + 'position':(start_pos,end_pos), + 'size':(start_size,end_size), + 'textcolor':( + (*Color.TEXT,Color.TEXT_OPACITY), + Const.INVISIBLE + ) + } + ) + s.anims[id(s.key_button)]['shadow'] = ( + Animate( + widget=s.key_button_shadow, + attrs={ + 'opacity':(0,Color.SHADOW_OPACITY), + 'position':( + start_pos, + s.window_shadow_pos + ), + 'size':( + start_size, + s.window_shadow_size + ) + }, + duration=butter + ) + ) + ret = s.make_key_kids( + title=Strings.KEYS_ON.format( + list(Strings.EVENTS)[si] + ), + keys=keys + ) + on_back = lambda: ( + callable(ret) and ret(), + s.key_clean() + ) + s.window_on = (s.key_button,s.key_window,on_back) + + def make_key_kids(s,title,keys): + s.make_window_default(title=title) + s.make_key_default(keys) + s.wrap_window_kids() + s.animate_window_kids() + + def make_key_default(s,what): + x,y = s.window_pos + sx,sy = s.window_size + text_push = 15 + delay = 0.35 + cur = s.memory[id(s.sl)]['keys'] + pos = (s.window_marg-s.window_fix,sy/2+s.window_marg-54) + size = dx,dy = (150,sy/2-s.window_marg) + what_scroll = bui.scrollwidget( + parent=s.root, + position=pos, + color=Color.BASE, + border_opacity=Color.OPACITY + ) + s.window_kids.append((what_scroll,pos,text_push,delay, + ('size',((dx-130,dy),size)) + )) + what_root = bui.containerwidget( + parent=what_scroll, + size=(dx,30*len(what)), + background=False + ) + what_texts = [] + top = len(what)*30 + for j,i in enumerate(what,start=1): + w = bui.textwidget( + parent=what_root, + size=(dx,30), + position=(0,top-j*30), + color=(*Color.TEXT,Color.TEXT_OPACITY), + selectable=True, + click_activate=True, + on_activate_call=bui.CallPartial( + s.set_act, i + ), + text=Strings.ACTIONS[i], + glow_type=Const.GLOW + ) + what_texts.append(w) + pos = (s.window_marg-s.window_fix,s.window_marg-4) + size = dx,dy = (150,sy/2-(s.window_marg+50)) + cur_scroll = bui.scrollwidget( + parent=s.root, + position=pos, + color=Color.BASE, + border_opacity=Color.OPACITY + ) + s.window_kids.append((cur_scroll,pos,text_push,delay, + ('size',((dx-130,dy),size)) + )) + s.current_key_root = bui.containerwidget( + parent=cur_scroll, + size=(dx,30*len(cur)), + background=False + ) + s.fresh_current_key_texts() + pos = (sx*0.62,sy*0.43) + t = bui.textwidget( + parent=s.root, + text=Strings.ACTION_PLACEHOLDER, + position=pos, + color=Const.INVISIBLE, + h_align=Const.ALIGN, + v_align=Const.ALIGN + ) + s.window_kids.append((t,pos,70,delay+0.13, + ('color',( + Const.INVISIBLE, + (*Color.TEXT,Color.TEXT_OPACITY) + )) + )) + s.key_kids = [(t,0)] + s.window_trash = [what_texts,s.current_key_texts] + + def fresh_current_key_texts(s): + for _ in s.current_key_root.get_children(): _.delete() + s.current_key_texts = [] + cur = s.memory[id(s.sl)]['keys'] + top = len(cur)*30 + dx = 150 + for i,g in enumerate(cur.items(),start=1): + nam,c = g + w = bui.textwidget( + parent=s.current_key_root, + size=(dx,30), + position=(0,top-i*30), + color=(*Color.TEXT,Color.TEXT_OPACITY), + selectable=True, + click_activate=True, + maxwidth=dx-10, + on_activate_call=bui.CallPartial( + s.set_act, + c['action'], + data=c['data'], + nam=nam + ), + text=nam, + glow_type=Const.GLOW + ) + s.current_key_texts.append(w) + bui.containerwidget(s.current_key_root,size=(150,top)) + + def key_clean(s): + for k,_ in s.key_kids: + if _ == 1: + k.delete() + continue + s.anims[id(k)].reverse( + on_finish=k.delete + ) + s.key_kids.clear() + + def set_act(s,i,data=None,nam=''): + butter = s.global_butter + s.key_clean() + x,y = ( + s.window_pos[0]+150+s.window_marg*2, + s.window_pos[1]+s.window_marg + ) + sx,sy = ( + s.window_size[0]-150-s.window_marg, + s.window_size[1]-54 + ) + data = data or {} + if i == 0: + tx = Strings.ATTR + t = bui.textwidget( + parent=s.root, + position=(x,y+sy-37-2), + text=tx, + maxwidth=60, + color=Const.INVISIBLE + ) + s.key_kids.append((t,0)) + attr = bui.textwidget( + parent=s.root, + position=(x+70,y+sy-37-5), + glow_type=Const.GLOW, + editable=True, + size=(sx-80,35), + allow_clear_button=False, + description=tx, + v_align=Const.ALIGN, + color=(*Color.TEXT,Color.TEXT_OPACITY), + text=data.get('attr','') + ) + s.key_kids.append((attr,1)) + tx = Strings.EVAL + t = bui.textwidget( + parent=s.root, + position=(x,y+sy-37*2-2), + text=tx, + maxwidth=60, + color=Const.INVISIBLE + ) + s.key_kids.append((t,0)) + val = bui.textwidget( + parent=s.root, + position=(x+70,y+sy-37*2-5), + glow_type=Const.GLOW, + editable=True, + size=(sx-80,35), + allow_clear_button=False, + description=tx, + v_align=Const.ALIGN, + color=(*Color.TEXT,Color.TEXT_OPACITY), + text=data.get('eval','') + ) + s.key_kids.append((val,1)) + tx = Strings.NAME + t = bui.textwidget( + parent=s.root, + position=(x,y+sy-37*3-2), + text=tx, + maxwidth=60, + color=Const.INVISIBLE + ) + s.key_kids.append((t,0)) + name_inp = bui.textwidget( + parent=s.root, + position=(x+70,y+sy-37*3-5), + glow_type=Const.GLOW, + editable=True, + size=(sx-80,35), + allow_clear_button=False, + description=tx, + v_align=Const.ALIGN, + color=(*Color.TEXT,Color.TEXT_OPACITY), + text=nam + ) + s.key_kids.append((name_inp,1)) + bx,by = 100,40 + tx = Strings.OFFSET + t = bui.textwidget( + parent=s.root, + position=(x,y+sy-37*4-2), + text=tx, + maxwidth=60, + color=Const.INVISIBLE + ) + s.key_kids.append((t,0)) + time_inp = bui.textwidget( + parent=s.root, + position=(x+70,y+sy-37*4-5), + glow_type=Const.GLOW, + editable=True, + size=(sx-80,35), + allow_clear_button=False, + v_align=Const.ALIGN, + description=tx, + color=(*Color.TEXT,Color.TEXT_OPACITY), + text=data.get('offset','') + ) + s.key_kids.append((time_inp,1)) + _old_off = None + def _off_spy(): + nonlocal _old_off + if not time_inp.exists(): + s.off_spy = None + return + o = bui.textwidget(query=time_inp) + if ( + o.replace('.','',1).isdigit() + and (new:=float(o)) != _old_off + ): + mem = s.memory[id(s.sl)] + _old_off = new + mem.get('prev_off_wid') and s.widgets.pop(mem.pop('prev_off_wid')).delete() + if not (0 <= new <= mem['duration']): return + s.prev_off_wid = bui.imagewidget( + parent=s.stamp_hscroll_root, + position=( + s.magic_x + s.entry_xs_real * (mem['start'] + new) * s.entries_per_sec - s.entry_ys_real/4, + s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + ), + size=(s.entry_ys_real, s.entry_ys_real - s.magic_y), + texture=Eval.TEXTURE(Const.KEY), + color=Color.COLD, + opacity=Color.OPACITY + ) + mem['prev_off'] = new + mem['prev_off_wid'] = id(s.prev_off_wid) + s.widgets[id(s.prev_off_wid)] = s.prev_off_wid + s.off_spy = bui.AppTimer( + 0.02, _off_spy, repeat=True + ) + def do_done(): + a = bui.textwidget(query=attr) + ov = bui.textwidget(query=val) + nam = bui.textwidget(query=name_inp) + o = bui.textwidget(query=time_inp) + if not a: + s.toast(Format.ERROR_EMPTY(Strings.ATTR)) + Eval.SOUND(Const.BAD_SOUND).play() + return + if not ov: + s.toast(Format.ERROR_EMPTY(Strings.EVAL)) + Eval.SOUND(Const.BAD_SOUND).play() + return + if not nam: + s.toast(Format.ERROR_EMPTY(Strings.NAME)) + Eval.SOUND(Const.BAD_SOUND).play() + return + if not o: + s.toast(Format.ERROR_EMPTY(Strings.OFFSET)) + Eval.SOUND(Const.BAD_SOUND).play() + return + try: + with bs.get_foreground_host_activity().context: + eval(ov) + except Exception as e: + s.toast(Format.ERROR(e)) + Eval.SOUND(Const.BAD_SOUND).play() + return + try: + offset_val = float(o) + except ValueError: + s.toast(Format.INVALID(Strings.OFFSET)) + Eval.SOUND(Const.BAD_SOUND).play() + return + mem = s.memory[id(s.sl)] + if not (0 <= offset_val <= mem['duration']): + s.toast(Format.OUT_OF_RANGE(Strings.OFFSET)) + Eval.SOUND(Const.BAD_SOUND).play() + return + actual_time = mem['start'] + offset_val + key_x = s.magic_x + s.entry_xs_real * actual_time * s.entries_per_sec - s.entry_ys_real/4 + key_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + key_wid = bui.imagewidget( + parent=s.stamp_hscroll_root, + position=(key_x, key_y), + size=(s.entry_ys_real, s.entry_ys_real - s.magic_y), + texture=Eval.TEXTURE(Const.KEY), + color=Color.WARM, + opacity=Color.OPACITY + ) + existed = False + if nam in mem['keys']: + s.widgets.pop(mem['keys'][nam]['widget']).delete() + existed = True + mem['keys'][nam] = { + 'time':actual_time, + 'action':i, + 'data':{ + 'attr':a, + 'eval':ov, + 'offset':o + }, + 'widget':id(key_wid) + } + s.widgets[id(key_wid)] = key_wid + s.toast( + existed and + Strings.INFO_EDITED_KEY or + Strings.INFO_ADDED_KEY + ) + s.dismiss_window() + b = bui.buttonwidget( + parent=s.root, + position=(x+sx-(bx+5),y), + size=(bx,by), + texture=Eval.TEXTURE(Const.SKIN), + opacity=0, + on_activate_call=do_done, + enable_sound=False, + label=Strings.DONE, + color=Color.BASE, + textcolor=Const.INVISIBLE + ) + s.key_kids.append((b,2)) + def do_pop(): + n = bui.textwidget(query=name_inp) + if not n: + s.toast(Format.ERROR_EMPTY(Strings.NAME)) + Eval.SOUND(Const.BAD_SOUND).play() + return + mem = s.memory[id(s.sl)] + if n not in mem['keys']: + s.toast(Format.NOT_FOUND(n)) + Eval.SOUND(Const.BAD_SOUND).play() + return + s.widgets[mem['keys'].pop(n)['widget']].delete() + s.toast(Strings.INFO_POPPED(n)) + Eval.SOUND(Const.OK_SOUND).play() + s.fresh_current_key_texts() + b = bui.buttonwidget( + parent=s.root, + position=(x+10,y), + size=(bx,by), + texture=Eval.TEXTURE(Const.SKIN), + opacity=0, + on_activate_call=do_pop, + enable_sound=False, + label=Strings.POP, + color=Color.BASE, + textcolor=Const.INVISIBLE + ) + s.key_kids.append((b,2)) + elif i == 1: + if not s.event_on: + s.toggle_event(passive=True) + t = bui.textwidget( + parent=s.root, + position=(x+s.window_marg+5,y+sy-30), + text=Strings.EXTEND_CODE, + scale=1.5, + color=Const.INVISIBLE + ) + s.key_kids.append((t,0)) + t = bui.textwidget( + parent=s.root, + position=(x+s.window_marg-5,y+sy-70), + text=Strings.CODE_HELP, + maxwidth=sx-s.window_marg*2, + color=Const.INVISIBLE + ) + s.key_kids.append((t,0)) + tx = Strings.NAME + t = bui.textwidget( + parent=s.root, + position=(x,y+sy-37*4.5+8), + text=tx, + maxwidth=60, + color=Const.INVISIBLE + ) + s.key_kids.append((t,0)) + name_inp = bui.textwidget( + parent=s.root, + position=(x+70,y+sy-37*4.5+5), + glow_type=Const.GLOW, + editable=True, + size=(sx-80,35), + allow_clear_button=False, + v_align=Const.ALIGN, + description=tx, + color=(*Color.TEXT,Color.TEXT_OPACITY), + text=data.get('name','') + ) + s.key_kids.append((name_inp,1)) + tx = Strings.OFFSET + t = bui.textwidget( + parent=s.root, + position=(x,y+sy-37*5.5+8), + text=tx, + maxwidth=60, + color=Const.INVISIBLE + ) + s.key_kids.append((t,0)) + time_inp = bui.textwidget( + parent=s.root, + position=(x+70,y+sy-37*5.5+5), + glow_type=Const.GLOW, + editable=True, + size=(sx-80,35), + allow_clear_button=False, + v_align=Const.ALIGN, + description=tx, + color=(*Color.TEXT,Color.TEXT_OPACITY), + text=str(data.get('offset','')) + ) + s.key_kids.append((time_inp,1)) + _old_off = None + def _off_spy(): + nonlocal _old_off + if not time_inp.exists(): + s.off_spy = None + return + o = bui.textwidget(query=time_inp) + if ( + o.replace('.','',1).isdigit() + and (new:=float(o)) != _old_off + ): + mem = s.memory[id(s.sl)] + _old_off = new + mem.get('prev_off_wid') and s.widgets.pop(mem.pop('prev_off_wid')).delete() + if not (0 <= new <= mem['duration']): return + s.prev_off_wid = bui.imagewidget( + parent=s.stamp_hscroll_root, + position=( + s.magic_x + s.entry_xs_real * (mem['start'] + new) * s.entries_per_sec - s.entry_ys_real/4, + s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + ), + size=(s.entry_ys_real, s.entry_ys_real - s.magic_y), + texture=Eval.TEXTURE(Const.KEY), + color=Color.COLD, + opacity=Color.OPACITY + ) + mem['prev_off'] = new + mem['prev_off_wid'] = id(s.prev_off_wid) + s.widgets[id(s.prev_off_wid)] = s.prev_off_wid + s.off_spy = bui.AppTimer( + 0.02, _off_spy, repeat=True + ) + def do_open(): + o = bui.textwidget(query=time_inp) + n = bui.textwidget(query=name_inp) + if not n: + s.toast(Format.ERROR_EMPTY(Strings.NAME)) + Eval.SOUND(Const.BAD_SOUND).play() + return + if not o: + s.toast(Format.ERROR_EMPTY(Strings.OFFSET)) + Eval.SOUND(Const.BAD_SOUND).play() + return + try: + offset_val = float(o) + except ValueError: + s.toast(Format.INVALID(Strings.OFFSET)) + Eval.SOUND(Const.BAD_SOUND).play() + return + mem = s.memory[id(s.sl)] + if not (0 <= offset_val <= mem['duration']): + s.toast(Format.OUT_OF_RANGE(Strings.OFFSET)) + Eval.SOUND(Const.BAD_SOUND).play() + return + s.forgive_prev_off = True + s.event_window( + 6, + force_title=Strings.CODE_EDITOR, + on_done=lambda final: add_key(final,n,offset_val,mem), + initial_code=data.get('code') + ) + def add_key(final,n,off,mem): + actual_time = mem['start'] + off + key_x = s.magic_x + s.entry_xs_real * actual_time * s.entries_per_sec - s.entry_ys_real/4 + key_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + key_wid = bui.imagewidget( + parent=s.stamp_hscroll_root, + position=(key_x, key_y), + size=(s.entry_ys_real, s.entry_ys_real - s.magic_y), + texture=Eval.TEXTURE(Const.KEY), + color=Color.WARM, + opacity=Color.OPACITY + ) + existed = False + if n in mem['keys']: + s.widgets.pop(mem['keys'][n]['widget']).delete() + existed = True + mem['keys'][n] = { + 'time':actual_time, + 'action':i, + 'data':{ + 'offset':off, + 'code':final['code'], + 'name':n + }, + 'widget':id(key_wid) + } + s.widgets[id(key_wid)] = key_wid + s.toast( + existed and + Strings.INFO_EDITED_KEY or + Strings.INFO_ADDED_KEY + ) + bx,by = sx/2-s.window_marg*5,40 + b = bui.buttonwidget( + parent=s.root, + position=(x+bx+s.window_marg*6+4,y), + size=(bx,by), + texture=Eval.TEXTURE(Const.SKIN), + opacity=0, + on_activate_call=do_open, + enable_sound=False, + label=Strings.NEXT, + color=Color.BASE, + textcolor=Const.INVISIBLE + ) + s.key_kids.append((b,2)) + def do_pop(): + n = bui.textwidget(query=name_inp) + if not n: + s.toast(Format.ERROR_EMPTY(Strings.NAME)) + Eval.SOUND(Const.BAD_SOUND).play() + return + mem = s.memory[id(s.sl)] + if n not in mem['keys']: + s.toast(Format.NOT_FOUND(n)) + Eval.SOUND(Const.BAD_SOUND).play() + return + s.widgets[mem['keys'].pop(n)['widget']].delete() + s.toast(Strings.INFO_POPPED(n)) + Eval.SOUND(Const.OK_SOUND).play() + s.fresh_current_key_texts() + b = bui.buttonwidget( + parent=s.root, + position=(x+10,y), + size=(bx,by), + texture=Eval.TEXTURE(Const.SKIN), + opacity=0, + on_activate_call=do_pop, + enable_sound=False, + label=Strings.POP, + color=Color.BASE, + textcolor=Const.INVISIBLE + ) + s.key_kids.append((b,2)) + elif i == 2: + tx = Strings.VALUE + t = bui.textwidget( + parent=s.root, + position=(x,y+sy-37-2), + text=tx, + maxwidth=60, + color=Const.INVISIBLE + ) + s.key_kids.append((t,0)) + vol_inp = bui.textwidget( + parent=s.root, + position=(x+70,y+sy-37-5), + glow_type=Const.GLOW, + editable=True, + size=(sx-80,35), + allow_clear_button=False, + v_align=Const.ALIGN, + description=tx, + color=(*Color.TEXT,Color.TEXT_OPACITY), + text=str(data.get('volume','')) + ) + s.key_kids.append((vol_inp,1)) + bx,by = 100,40 + tx = Strings.OFFSET + t = bui.textwidget( + parent=s.root, + position=(x,y+sy-37*2-2), + text=tx, + maxwidth=60, + color=Const.INVISIBLE + ) + s.key_kids.append((t,0)) + time_inp = bui.textwidget( + parent=s.root, + position=(x+70,y+sy-37*2-5), + glow_type=Const.GLOW, + editable=True, + size=(sx-80,35), + allow_clear_button=False, + v_align=Const.ALIGN, + description=tx, + color=(*Color.TEXT,Color.TEXT_OPACITY), + text=str(data.get('offset','')) + ) + s.key_kids.append((time_inp,1)) + tx = Strings.NAME + t = bui.textwidget( + parent=s.root, + position=(x,y+sy-37*3-2), + text=tx, + maxwidth=60, + color=Const.INVISIBLE + ) + s.key_kids.append((t,0)) + name_inp = bui.textwidget( + parent=s.root, + position=(x+70,y+sy-37*3-5), + glow_type=Const.GLOW, + editable=True, + size=(sx-80,35), + allow_clear_button=False, + v_align=Const.ALIGN, + description=tx, + color=(*Color.TEXT,Color.TEXT_OPACITY), + text=str(data.get('name','')) + ) + s.key_kids.append((name_inp,1)) + _old_off = None + def _off_spy(): + nonlocal _old_off + if not time_inp.exists(): + s.off_spy = None + return + o = bui.textwidget(query=time_inp) + if ( + o.replace('.','',1).isdigit() + and (new:=float(o)) != _old_off + ): + mem = s.memory[id(s.sl)] + _old_off = new + mem.get('prev_off_wid') and s.widgets.pop(mem.pop('prev_off_wid')).delete() + if not (0 <= new <= mem['duration']): return + s.prev_off_wid = bui.imagewidget( + parent=s.stamp_hscroll_root, + position=( + s.magic_x + s.entry_xs_real * (mem['start'] + new) * s.entries_per_sec - s.entry_ys_real/4, + s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + ), + size=(s.entry_ys_real, s.entry_ys_real - s.magic_y), + texture=Eval.TEXTURE(Const.KEY), + color=Color.COLD, + opacity=Color.OPACITY + ) + mem['prev_off'] = new + mem['prev_off_wid'] = id(s.prev_off_wid) + s.widgets[id(s.prev_off_wid)] = s.prev_off_wid + s.off_spy = bui.AppTimer( + 0.02, _off_spy, repeat=True + ) + def do_done(): + v = bui.textwidget(query=vol_inp) + o = bui.textwidget(query=time_inp) + nam = bui.textwidget(query=name_inp) + if not v: + s.toast(Format.ERROR_EMPTY(Strings.VALUE)) + Eval.SOUND(Const.BAD_SOUND).play() + return + if not o: + s.toast(Format.ERROR_EMPTY(Strings.OFFSET)) + Eval.SOUND(Const.BAD_SOUND).play() + return + if not nam: + s.toast(Format.ERROR_EMPTY(Strings.NAME)) + Eval.SOUND(Const.BAD_SOUND).play() + return + try: + volume_val = round(float(v),2) + except ValueError: + s.toast(Format.INVALID(Strings.VOLUME)) + Eval.SOUND(Const.BAD_SOUND).play() + return + try: + offset_val = float(o) + except ValueError: + s.toast(Format.INVALID(Strings.OFFSET)) + Eval.SOUND(Const.BAD_SOUND).play() + return + mem = s.memory[id(s.sl)] + if not (0 <= offset_val <= mem['duration']): + s.toast(Format.OUT_OF_RANGE(Strings.OFFSET)) + Eval.SOUND(Const.BAD_SOUND).play() + return + actual_time = mem['start'] + offset_val + key_x = s.magic_x + s.entry_xs_real * actual_time * s.entries_per_sec - s.entry_ys_real/4 + key_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + key_wid = bui.imagewidget( + parent=s.stamp_hscroll_root, + position=(key_x, key_y), + size=(s.entry_ys_real, s.entry_ys_real - s.magic_y), + texture=Eval.TEXTURE(Const.KEY), + color=Color.WARM, + opacity=Color.OPACITY + ) + existed = False + if nam in mem['keys']: + existed = True + s.widgets.pop(mem['keys'][nam]['widget']).delete() + mem['keys'][nam] = { + 'time':actual_time, + 'action':i, + 'data':{ + 'volume':volume_val, + 'offset':offset_val, + 'name':nam + }, + 'widget':id(key_wid) + } + s.widgets[id(key_wid)] = key_wid + s.toast( + existed and + Strings.INFO_EDITED_KEY or + Strings.INFO_ADDED_KEY + ) + s.dismiss_window() + b = bui.buttonwidget( + parent=s.root, + position=(x+sx-(bx+5),y), + size=(bx,by), + texture=Eval.TEXTURE(Const.SKIN), + opacity=0, + on_activate_call=do_done, + enable_sound=False, + label=Strings.DONE, + color=Color.BASE, + textcolor=Const.INVISIBLE + ) + s.key_kids.append((b,2)) + def do_pop(): + t = bui.textwidget(query=name_inp) + mem = s.memory[id(s.sl)] + if not t: + s.toast(Strings.INFO_POP_WHAT) + Eval.SOUND(Const.BAD_SOUND).play() + return + if t not in mem['keys']: + s.toast(Format.NOT_FOUND(t)) + Eval.SOUND(Const.BAD_SOUND).play() + return + s.widgets.pop(mem['keys'].pop(t)['widget']).delete() + s.toast(Strings.INFO_POPPED(t)) + Eval.SOUND(Const.OK_SOUND).play() + s.fresh_current_key_texts() + b = bui.buttonwidget( + parent=s.root, + position=(x+(sx/2-145),y), + size=(bx,by), + texture=Eval.TEXTURE(Const.SKIN), + opacity=0, + on_activate_call=do_pop, + enable_sound=False, + label=Strings.POP, + color=Color.BASE, + textcolor=Const.INVISIBLE + ) + s.key_kids.append((b,2)) + elif i == 3: + tx = Strings.TEXT + t = bui.textwidget( + parent=s.root, + position=(x,y+sy-37-2), + text=tx, + maxwidth=60, + color=Const.INVISIBLE + ) + s.key_kids.append((t,0)) + text_inp = bui.textwidget( + parent=s.root, + position=(x+70,y+sy-37-5), + glow_type=Const.GLOW, + editable=True, + size=(sx-80,35), + allow_clear_button=False, + description=tx, + v_align=Const.ALIGN, + color=(*Color.TEXT,Color.TEXT_OPACITY), + text=str(data.get('text','Hello!')) + ) + s.key_kids.append((text_inp,1)) + tx = Strings.COLOR + t = bui.textwidget( + parent=s.root, + position=(x,y+sy-37*2-2), + text=tx, + maxwidth=60, + color=Const.INVISIBLE + ) + s.key_kids.append((t,0)) + color_inp = bui.textwidget( + parent=s.root, + position=(x+70,y+sy-37*2-5), + glow_type=Const.GLOW, + editable=True, + size=(sx-80,35), + allow_clear_button=False, + description=tx, + v_align=Const.ALIGN, + color=(*Color.TEXT,Color.TEXT_OPACITY), + text=str(data.get('color','(1,1,1)')) + ) + s.key_kids.append((color_inp,1)) + tx = Strings.TIME + t = bui.textwidget( + parent=s.root, + position=(x,y+sy-37*3-2), + text=tx, + maxwidth=60, + color=Const.INVISIBLE + ) + s.key_kids.append((t,0)) + btime_inp = bui.textwidget( + parent=s.root, + position=(x+70,y+sy-37*3-5), + glow_type=Const.GLOW, + editable=True, + size=(sx-80,35), + allow_clear_button=False, + description=tx, + v_align=Const.ALIGN, + color=(*Color.TEXT,Color.TEXT_OPACITY), + text=str(data.get('time',4)) + ) + s.key_kids.append((btime_inp,1)) + tx = Strings.NAME + t = bui.textwidget( + parent=s.root, + position=(x,y+sy-37*4-2), + text=tx, + maxwidth=60, + color=Const.INVISIBLE + ) + s.key_kids.append((t,0)) + name_inp = bui.textwidget( + parent=s.root, + position=(x+70,y+sy-37*4-5), + glow_type=Const.GLOW, + editable=True, + size=(sx-80,35), + allow_clear_button=False, + description=tx, + v_align=Const.ALIGN, + color=(*Color.TEXT,Color.TEXT_OPACITY), + text=nam + ) + s.key_kids.append((name_inp,1)) + bx,by = 100,40 + tx = Strings.OFFSET + t = bui.textwidget( + parent=s.root, + position=(x,y+sy-37*5-2), + text=tx, + maxwidth=60, + color=Const.INVISIBLE + ) + s.key_kids.append((t,0)) + time_inp = bui.textwidget( + parent=s.root, + position=(x+70,y+sy-37*5-5), + glow_type=Const.GLOW, + editable=True, + size=(sx-80,35), + allow_clear_button=False, + v_align=Const.ALIGN, + description=tx, + color=(*Color.TEXT,Color.TEXT_OPACITY), + text=str(data.get('offset','')) + ) + s.key_kids.append((time_inp,1)) + _old_off = None + def _off_spy(): + nonlocal _old_off + if not time_inp.exists(): + s.off_spy = None + return + o = bui.textwidget(query=time_inp) + if ( + o.replace('.','',1).isdigit() + and (new:=float(o)) != _old_off + ): + mem = s.memory[id(s.sl)] + _old_off = new + mem.get('prev_off_wid') and s.widgets.pop(mem.pop('prev_off_wid')).delete() + if not (0 <= new <= mem['duration']): return + s.prev_off_wid = bui.imagewidget( + parent=s.stamp_hscroll_root, + position=( + s.magic_x + s.entry_xs_real * (mem['start'] + new) * s.entries_per_sec - s.entry_ys_real/4, + s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + ), + size=(s.entry_ys_real, s.entry_ys_real - s.magic_y), + texture=Eval.TEXTURE(Const.KEY), + color=Color.COLD, + opacity=Color.OPACITY + ) + mem['prev_off'] = new + mem['prev_off_wid'] = id(s.prev_off_wid) + s.widgets[id(s.prev_off_wid)] = s.prev_off_wid + s.off_spy = bui.AppTimer( + 0.02, _off_spy, repeat=True + ) + def do_done(): + txt = bui.textwidget(query=text_inp) + col = bui.textwidget(query=color_inp) + bt = bui.textwidget(query=btime_inp) + o = bui.textwidget(query=time_inp) + nam = bui.textwidget(query=name_inp) + if not txt: + s.toast(Format.ERROR_EMPTY(Strings.TEXT)) + Eval.SOUND(Const.BAD_SOUND).play() + return + if not col: + s.toast(Format.ERROR_EMPTY(Strings.COLOR)) + Eval.SOUND(Const.BAD_SOUND).play() + return + if not bt: + s.toast(Format.ERROR_EMPTY(Strings.TIME)) + Eval.SOUND(Const.BAD_SOUND).play() + return + if not nam: + s.toast(Format.ERROR_EMPTY(Strings.NAME)) + Eval.SOUND(Const.BAD_SOUND).play() + return + if not o: + s.toast(Format.ERROR_EMPTY(Strings.OFFSET)) + Eval.SOUND(Const.BAD_SOUND).play() + return + try: + with bs.get_foreground_host_activity().context: + eval(col) + except Exception as e: + s.toast(Format.ERROR(e)) + Eval.SOUND(Const.BAD_SOUND).play() + return + try: + time_val = float(bt) + except ValueError: + s.toast(Format.INVALID(Strings.TIME)) + Eval.SOUND(Const.BAD_SOUND).play() + return + try: + offset_val = float(o) + except ValueError: + s.toast(Format.INVALID(Strings.OFFSET)) + Eval.SOUND(Const.BAD_SOUND).play() + return + mem = s.memory[id(s.sl)] + if not (0 <= offset_val <= mem['duration']): + s.toast(Format.OUT_OF_RANGE(Strings.OFFSET)) + Eval.SOUND(Const.BAD_SOUND).play() + return + actual_time = mem['start'] + offset_val + key_x = s.magic_x + s.entry_xs_real * actual_time * s.entries_per_sec - s.entry_ys_real/4 + key_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + key_wid = bui.imagewidget( + parent=s.stamp_hscroll_root, + position=(key_x, key_y), + size=(s.entry_ys_real, s.entry_ys_real - s.magic_y), + texture=Eval.TEXTURE(Const.KEY), + color=Color.WARM, + opacity=Color.OPACITY + ) + existed = False + if nam in mem['keys']: + s.widgets.pop(mem['keys'][nam]['widget']).delete() + existed = True + mem['keys'][nam] = { + 'time':actual_time, + 'action':i, + 'data':{ + 'text':txt, + 'color':col, + 'time':time_val, + 'offset':o, + 'name':nam + }, + 'widget':id(key_wid) + } + s.widgets[id(key_wid)] = key_wid + s.toast( + existed and + Strings.INFO_EDITED_KEY or + Strings.INFO_ADDED_KEY + ) + s.dismiss_window() + b = bui.buttonwidget( + parent=s.root, + position=(x+sx-(bx+5),y), + size=(bx,by), + texture=Eval.TEXTURE(Const.SKIN), + opacity=0, + on_activate_call=do_done, + enable_sound=False, + label=Strings.DONE, + color=Color.BASE, + textcolor=Const.INVISIBLE + ) + s.key_kids.append((b,2)) + def do_pop(): + n = bui.textwidget(query=name_inp) + if not n: + s.toast(Format.ERROR_EMPTY(Strings.NAME)) + Eval.SOUND(Const.BAD_SOUND).play() + return + mem = s.memory[id(s.sl)] + if n not in mem['keys']: + s.toast(Format.NOT_FOUND(n)) + Eval.SOUND(Const.BAD_SOUND).play() + return + s.widgets[mem['keys'].pop(n)['widget']].delete() + s.toast(Strings.INFO_POPPED(n)) + Eval.SOUND(Const.OK_SOUND).play() + s.fresh_current_key_texts() + b = bui.buttonwidget( + parent=s.root, + position=(x+10,y), + size=(bx,by), + texture=Eval.TEXTURE(Const.SKIN), + opacity=0, + on_activate_call=do_pop, + enable_sound=False, + label=Strings.POP, + color=Color.BASE, + textcolor=Const.INVISIBLE + ) + s.key_kids.append((b,2)) + for k,_ in s.key_kids: + if _ == 1: continue + attrs = _ == 2 and { + 'opacity':(0,Color.OPACITY), + 'textcolor':( + Const.INVISIBLE, + (*Color.TEXT,Color.TEXT_OPACITY) + ) + } or { + 'color':( + Const.INVISIBLE, + (*Color.TEXT,Color.TEXT_OPACITY) + ) + } + s.anims[id(k)] = Animate( + widget=k, + attrs=attrs, + duration=butter + ) + + def clear_memory(s): + Config.set('last', None) + + def load_memory(s,memory,shut=False): + sorted_entries = sorted( + memory.items(), + key=lambda x: x[1]['order'] + ) + + total_count = len(sorted_entries) + + for idx, (old_id, mem_data) in enumerate(sorted_entries): + data = mem_data['data'].copy() + smol = mem_data.get('smol', False) + + if smol: + size = ( + s.entry_ys_real, + s.entry_ys_real - s.magic_y + ) + else: + size = ( + s.entry_xs_real * ( + mem_data['duration'] * s.entries_per_sec + ) * s.magic_right, + s.entry_ys_real - s.magic_y + ) + + btn = bui.buttonwidget( + parent=s.stamp_hscroll_root, + texture=Eval.TEXTURE(Const.SKIN), + label=data['name'], + textcolor=(*Color.TEXT, Color.TEXT_OPACITY), + color=Color.BASE, + opacity=0, + enable_sound=False, + size=size, + button_type='square' + ) + + bui.buttonwidget( + btn, + on_activate_call=bui.CallPartial(s.select, btn) + ) + + s.stamp_kids.append(btn) + + new_mem = { + 'order': idx, + 'event': mem_data['event'], + 'data': data, + 'duration': mem_data['duration'], + 'start': mem_data['start'], + 'keys': {}, + 'smol': smol + } + + s.memory[id(btn)] = new_mem + + y_pos = s.entry_ys_real * (total_count - idx - 1) + + for key_name, key_data_orig in mem_data.get('keys', {}).items(): + key_data = key_data_orig.copy() + + if 'widget' in key_data: + del key_data['widget'] + + key_time = key_data['time'] + key_x = s.magic_x + s.entry_xs_real * key_time * s.entries_per_sec - s.entry_ys_real/4 + + key_wid = bui.imagewidget( + parent=s.stamp_hscroll_root, + position=(key_x, y_pos), + size=(s.entry_ys_real, s.entry_ys_real - s.magic_y), + texture=Eval.TEXTURE(Const.KEY), + color=Color.WARM, + opacity=Color.OPACITY + ) + + new_mem['keys'][key_name] = { + **key_data, + 'widget': id(key_wid) + } + s.widgets[id(key_wid)] = key_wid + + width_in_steps = mem_data['duration'] * s.entries_per_sec + x_pos = s.magic_x + s.entry_xs_real * mem_data['start'] * s.entries_per_sec + ( + width_in_steps * s.magic_left + ) + + bui.buttonwidget(btn, position=(x_pos, y_pos)) + + s.build_timeline() + + s.wrap([1, 2, 3], on_finish=s.bottom_left) + s.make_timeline() + s.wrap_timeline() + + if len(s.memory): + s.show_controls() + + s.top_left() + if shut: return + Eval.SOUND(Const.OK_SOUND).play() + s.toast(Format.LOADED_ENTRIES(len(memory))) + + def wrap(s,what=0,on_finish=None,init=False): + rx,ry = s.real = bui.get_virtual_screen_size() + sx,sy = s.stamp_size = (rx,150) + smoly = sy-s.stamp_hack + old_deep_y = getattr(s,'stamp_deep_y',smoly) + s.stamp_deep_y = max(s.entry_ys_real*(len(s.memory)+1),smoly) + smolx = sx-s.stamp_hack + old_deep_x = getattr(s,'stamp_deep_x',smolx) + if hasattr(s, 'timeline') and s.timeline: + s.max_time = s.timeline[-1]['time'] + else: + times = [ + _['start'] + _['duration'] + for _ in s.memory.values() + ] or [0] + s.max_time = max(times) + rightmost_edge = ( + s.max_time * s.entries_per_sec * s.entry_xs_real + ) + s.stamp_deep_x = max(rightmost_edge + s.entry_xs_real * 1, smolx) + y_off = 70 + xoff, = Eval.SCALE_REAL(25) + one, = Eval.SCALE_REAL(1) + s.event_kid_ts = one + one_ba, = Eval.SCALE_BA(1) + s.window_size = wx,wy = 450,300 + s.window_pos = Eval.OFFSET(rx,ry,-wx/2,-wy/2,0,-y_off*2) + ( + s.window_shadow_pos, + s.window_shadow_size + ) = Eval.SHADOW( + *s.window_pos, + *s.window_size + ) + s.event_button_size = dx,dy = Eval.SCALE_REAL(100,40) + s.event_kid_off, = Eval.SCALE_REAL(40) + num_events = len(Strings.EVENTS) + button_height = 40 + spacing = 10 + menu_height = button_height * (num_events + 1) + spacing * (num_events + 2) + ex,ey = s.event_menu_size = Eval.SCALE_REAL(300, menu_height) + s.event_kid_size = (ex-s.event_kid_off,dy) + + s.edit_button_xoff, = Eval.SCALE_REAL(200) + s.edit_button_xtra, = Eval.SCALE_REAL(10) + s.edit_button_pos = pos = ( + dx+s.edit_button_xtra, + sy+6.5 + ) + s.edit_button_pos2 = ( + pos[0]+ex-dx, + pos[1] + ) + s.edit_button_size = (dx-4,dy-3) + s.key_button_pos = pos = ( + (dx+s.edit_button_xtra)*2, + sy+6.5 + ) + s.key_button_pos2 = ( + pos[0]+ex-dx, + pos[1] + ) + s.control_off, = Eval.SCALE_REAL(5) + s.control_size = conx,cony = Eval.SCALE_REAL(50,50) + s.control_pos = lambda i:( + sx-conx*(i+1)-s.control_off*i-2,sy+s.control_off + ) + s.tool_off, = Eval.SCALE_REAL(5) + s.tool_size = tx,ty = Eval.SCALE_REAL(50,50) + s.tool_pos = lambda i:( + sx-tx*(i+1)-s.tool_off*i-2,sy+s.tool_off + ) + if not isinstance(what,list): what = [what] + yes = 0 in what + if yes or 1 in what: + bui.containerwidget( + s.root, + size=s.stamp_size, + stack_offset=Eval.OFFSET(-rx,-ry,sx/2,sy/2) + ) + s.toast_position = ( + sx/2, + ry-40 if Settings.get('toast_top') else sy+10 + ) + bui.imagewidget(s.stamp_bg,size=s.stamp_size) + bx, = Eval.SCALE_BA(55) + px1,_ = Eval.OFFSET( + rx, ry, *bui.get_special_widget( + 'menu_button' + ).get_screen_space_center(), bx, bx + ) + px2,py = Eval.OFFSET( + rx, ry, *bui.get_special_widget( + 'squad_button' + ).get_screen_space_center(), bx, bx + ) + bui.buttonwidget( + s.square, + position=(px1,py), + size=(bx,bx), + text_scale=one_ba + ) + bui.buttonwidget( + s.triangle, + position=(px2,py), + size=(bx,bx), + text_scale=one_ba + ) + s.circle_pos = (px2-bx,py) + s.circle_size = (bx,bx) + s.about_pos = (px2-bx*2-s.control_off,py) + s.about_size = (bx,bx) + win = s.circle in s.window_on + bui.buttonwidget( + s.circle, + position=( + win and s.window_pos + or s.circle_pos + ), + size=( + win and s.window_size + or s.circle_size + ), + text_scale=one_ba + ) + if win: + a = s.anims[id(s.circle)]['window'].attrs_start + a['position'] = s.circle_pos + a['size'] = s.circle_size + a = s.anims[id(s.circle)]['shadow'].attrs_start + a['position'] = s.circle_pos + a['size'] = s.circle_size + about_win = s.about in s.window_on + bui.buttonwidget( + s.about, + position=( + about_win and s.window_pos + or s.about_pos + ), + size=( + about_win and s.window_size + or s.about_size + ), + text_scale=one_ba + ) + if about_win: + a = s.anims[id(s.about)]['window'].attrs_start + a['position'] = s.about_pos + a['size'] = s.about_size + a = s.anims[id(s.about)]['shadow'].attrs_start + a['position'] = s.about_pos + a['size'] = s.about_size + bui.textwidget( + s.top_left_h, + position=(0,s.stamp_deep_y) + ) + bui.textwidget( + s.top_left_v, + position=(0,s.stamp_deep_y) + ) + if yes or 2 in what: + bui.scrollwidget( + s.stamp_scroll, + size=s.stamp_size + ) + + height_changed = old_deep_y != s.stamp_deep_y + width_changed = old_deep_x != s.stamp_deep_x + + if height_changed or width_changed: + butter = s.global_butter/2 + + def fix_timeline_after_resize(): + for i, (t, l) in enumerate(s.stamp_timeline): + px = i * s.entry_xs_real + py = s.stamp_deep_y - 20 + bui.textwidget(t, position=(px, py)) + bui.imagewidget( + l, + position=(px + 4, -s.stamp_deep_y / 2), + size=(2, s.stamp_deep_y * 2) + ) + if callable(on_finish): + on_finish() + + s.anims[id(s.stamp_scroll_root)] = Animate( + widget=s.stamp_scroll_root, + attrs={ + 'size':( + (sx,old_deep_y), + (sx,s.stamp_deep_y) + ) + }, + duration=butter + ) + s.anims[id(s.stamp_hscroll)] = Animate( + widget=s.stamp_hscroll, + attrs={ + 'size':( + (sx,old_deep_y), + (sx,s.stamp_deep_y) + ) + }, + duration=butter + ) + s.anims[id(s.stamp_hscroll_root)] = Animate( + widget=s.stamp_hscroll_root, + attrs={ + 'size':( + (old_deep_x,old_deep_y), + (s.stamp_deep_x,s.stamp_deep_y) + ) + }, + duration=butter, + on_finish=fix_timeline_after_resize + ) + else: + bui.containerwidget( + s.stamp_scroll_root, + size=(sx,s.stamp_deep_y) + ) + bui.hscrollwidget( + s.stamp_hscroll, + size=(sx,s.stamp_deep_y) + ) + bui.containerwidget( + s.stamp_hscroll_root, + size=(s.stamp_deep_x,s.stamp_deep_y) + ) + if callable(on_finish): on_finish() + if yes or 3 in what: + if not init: s.wrap_timeline() + if yes or 4 in what: + dx,dy = ( + s.event_on and + s.event_menu_size or + s.event_button_size + ) + bui.imagewidget( + s.event_root, + size=(dx,dy), + position=(0,sy+5) + ) + if s.event_on: + a = s.anims[id(s.event_root)].attrs_end + a['size'] = s.event_menu_size + a = s.anims[id(s.event_root)].attrs_start + a['size'] = s.event_button_size + a = s.anims[id(s.event_root)].attrs_current + a['size'] = s.event_menu_size + bui.buttonwidget( + s.event_button, + size=s.event_button_size, + position=(0,sy+5), + text_scale=one + ) + s.event_top = sy+ey+5 + s.ev_mult = s.event_button_size[1]+Eval.SCALE_REAL(10)[0] + s.ev_x, = Eval.SCALE_REAL(20) + for i,g in enumerate(s.event_kids.items(),start=1): + kid,dat = g + win = kid in s.window_on + pos = (s.ev_x,s.event_top-s.ev_mult*i) + size = s.event_kid_size + bui.buttonwidget( + kid, + position=( + win and s.window_pos + or pos + ), + size=( + win and s.window_size + or size + ), + text_scale=one + ) + bui.imagewidget( + dat['shadow'], + position=( + win and s.window_shadow_pos + or pos + ), + size=( + win and s.window_shadow_size + or size + ) + ) + if win: + a = s.anims[id(kid)]['window'].attrs_end + a['position'] = s.window_pos + a['size'] = s.window_size + a = s.anims[id(kid)]['window'].attrs_start + a['position'] = pos + a['size'] = size + a = s.anims[id(kid)]['window'].attrs_current + a['position'] = s.window_pos + a['size'] = s.window_size + a = s.anims[id(kid)]['shadow'].attrs_end + a['position'] = s.window_shadow_pos + a['size'] = s.window_shadow_size + a = s.anims[id(kid)]['shadow'].attrs_start + a['position'] = pos + a['size'] = size + a = s.anims[id(kid)]['shadow'].attrs_current + a['position'] = s.window_shadow_pos + a['size'] = s.window_shadow_size + if yes or 5 in what: + win = s.edit_button in s.window_on + pos = ( + s.event_on and + s.edit_button_pos2 or + s.edit_button_pos + ) + size = s.edit_button_size + bui.buttonwidget( + s.edit_button, + size=( + win and s.window_size or + s.edit_button_size + ), + position=( + win and s.window_pos + or pos + ), + text_scale=one + ) + if win: + a = s.anims[id(s.edit_button)]['window'].attrs_start + a['position'] = pos + a['size'] = size + a = s.anims[id(s.edit_button)]['shadow'].attrs_start + a['position'] = pos + a['size'] = size + if yes or 6 in what: + win = s.key_button in s.window_on + pos = ( + s.event_on and + s.key_button_pos2 or + s.key_button_pos + ) + size = s.edit_button_size + bui.buttonwidget( + s.key_button, + size=( + win and s.window_size or + s.edit_button_size + ), + position=( + win and s.window_pos + or pos + ), + text_scale=one + ) + if win: + a = s.anims[id(s.key_button)]['window'].attrs_start + a['position'] = pos + a['size'] = size + a = s.anims[id(s.key_button)]['shadow'].attrs_start + a['position'] = pos + a['size'] = size + if yes or 9 in what: + win = s.circle in s.window_on + pos = s.circle_pos + size = s.circle_size + bui.buttonwidget( + s.circle, + size=( + win and s.window_size or + s.circle_size + ), + position=( + win and s.window_pos + or pos + ), + text_scale=one + ) + if win: + a = s.anims[id(s.circle)]['window'].attrs_start + a['position'] = pos + a['size'] = size + a = s.anims[id(s.circle)]['shadow'].attrs_start + a['position'] = pos + a['size'] = size + about_win = s.about in s.window_on + about_pos = s.about_pos + about_size = s.about_size + bui.buttonwidget( + s.about, + size=( + about_win and s.window_size or + s.about_size + ), + position=( + about_win and s.window_pos + or about_pos + ), + text_scale=one + ) + if about_win: + a = s.anims[id(s.about)]['window'].attrs_start + a['position'] = about_pos + a['size'] = about_size + a = s.anims[id(s.about)]['shadow'].attrs_start + a['position'] = about_pos + a['size'] = about_size + if yes or 7 in what: + for i,b in enumerate(s.controls): + bui.buttonwidget( + b, + size=( + (0,0) if init or not + s.controls_shown else + s.control_size + ), + position=s.control_pos(i), + text_scale=one + ) + if yes or 8 in what: + for i,b in enumerate(s.tools): + bui.buttonwidget( + b, + size=init and (0,0) or s.tool_size, + position=s.tool_pos(i), + text_scale=one + ) + + def bottom_left(s,dry=False): + if not dry: + bui.containerwidget( + s.stamp_hscroll_root, + visible_child=s.bottom_left_h + ) + bui.containerwidget( + s.stamp_scroll_root, + visible_child=s.bottom_left_v + ) + cx,cy = s.bottom_left_h.get_screen_space_center() + rx,ry = s.real + return ( + cx+rx/2-5+s.magic_x, + cy+ry/2-5 + ) + + def top_left(s): + bui.containerwidget( + s.stamp_hscroll_root, + visible_child=s.top_left_h + ) + bui.containerwidget( + s.stamp_scroll_root, + visible_child=s.top_left_v + ) + + def on_square(s): + if s.ui_clickable is None: + s.toast(Strings.INFO_SLOW_DOWN) + Eval.SOUND(Const.BAD_SOUND) + return + s.toggle_menu() + + def on_triangle(s): + bui.get_special_widget('squad_button').activate() + + @clickable + def on_circle(s): + if s.window_on: + s.dismiss_window() + Eval.SOUND(Const.OK_SOUND).play() + bui.buttonwidget( + s.circle, + on_activate_call=Const.DO_NOTHING, + selectable=False + ) + start_pos = s.circle_pos + end_pos = s.window_pos + start_size = s.circle_size + end_size = s.window_size + butter = s.global_butter*1.3 + s.anims[id(s.circle)]['window'] = Animate( + s.circle, + duration=butter, + attrs={ + 'position':(start_pos,end_pos), + 'size':(start_size,end_size), + 'opacity':(1,Color.OPACITY), + 'textcolor':( + (*Color.TEXT,Color.TEXT_OPACITY), + Const.INVISIBLE + ) + } + ) + s.anims[id(s.circle)]['shadow'] = ( + Animate( + widget=s.circle_shadow, + attrs={ + 'opacity':(0,Color.SHADOW_OPACITY), + 'position':( + start_pos, + s.window_shadow_pos + ), + 'size':( + start_size, + s.window_shadow_size + ) + }, + duration=butter + ) + ) + s.make_window_kids_settings() + def settings_on_back(): + if s.settings_opacity_dirty: + s.settings_opacity_dirty = False + s.refresh_opacity_via_ui_toggle() + s.window_on = (s.circle,s.on_circle,settings_on_back) + + @clickable + def on_about(s): + if s.window_on: + s.dismiss_window() + Eval.SOUND(Const.OK_SOUND).play() + bui.buttonwidget( + s.about, + on_activate_call=Const.DO_NOTHING, + selectable=False + ) + start_pos = s.about_pos + end_pos = s.window_pos + start_size = s.about_size + end_size = s.window_size + butter = s.global_butter*1.3 + s.anims[id(s.about)]['window'] = Animate( + s.about, + duration=butter, + attrs={ + 'position':(start_pos,end_pos), + 'size':(start_size,end_size), + 'opacity':(1,Color.OPACITY), + 'textcolor':( + (*Color.TEXT,Color.TEXT_OPACITY), + Const.INVISIBLE + ) + } + ) + s.anims[id(s.about)]['shadow'] = ( + Animate( + widget=s.about_shadow, + attrs={ + 'opacity':(0,Color.SHADOW_OPACITY), + 'position':( + start_pos, + s.window_shadow_pos + ), + 'size':( + start_size, + s.window_shadow_size + ) + }, + duration=butter + ) + ) + s.make_window_kids_about() + s.window_on = (s.about,s.on_about,None) + + def open_side_picker(s,btn,btn_screen_pos,btn_size,shadow,options,get_current,on_pick,label=None,option_color=None,option_textcolor=None): + """Generic 'grow a button into a side list' picker - the same + interaction make_node_window uses for its node-type picker, + pulled out so any button anywhere (settings' Theme/Language + cycles included) can open one without re-implementing the + grow/shrink/stagger/close_sub machinery. Traps on_back the + same way: wires s.window_sub_on so universal_back and every + other close-this-window path closes the picker first instead + of falling through past it. + + btn: the button widget that grows into the picker panel. + btn_screen_pos: that button's *screen-space* position (i.e. + window position + its local offset) - needed because the + grow animation interpolates from here, not from wherever + the button's local (window-relative) position says. + btn_size: the button's current (shrunk) size. + shadow: an imagewidget used purely for the picker's drop + shadow while open (mirrors type_btn_shadow). + options: list of label strings, offered top-to-bottom. + get_current: zero-arg callable returning whichever option is + currently selected (drawn highlighted) - a callable + rather than a fixed value so reopening after a pick + always highlights the up-to-date choice. + on_pick(value): called with the chosen option; picker closes + right after. + label: display text per option; defaults to the option itself + (callers needing e.g. capitalized labels pass this). + option_color(opt): optional - returns the swatch color for + that specific option (e.g. the theme picker passes each + theme's own 'base' tuple, so every row previews what it + will actually set Color.BASE to). Defaults to None, which + keeps the old behavior of every row sharing Color.BASE + with only the current pick tinted Color.COLD. + option_textcolor(opt): optional, same idea for the row's own + label color (theme picker passes each theme's 'text'). + Defaults to None, which keeps using the live Color.TEXT + for every row.""" + if s.window_sub_on: + s.window_sub_on[2]() + Eval.SOUND(Const.OK_SOUND).play() + bui.buttonwidget(btn,on_activate_call=Const.DO_NOTHING,selectable=False) + + wx,wy = s.window_pos + wsx,wsy = s.window_size + picker_x,picker_sx = 150,160 + picker_pos = (wx+wsx+100,wy) + picker_size = (picker_sx,wsy) + ( + picker_shadow_pos, + picker_shadow_size + ) = Eval.SHADOW(*picker_pos,*picker_size) + + btn_start_pos,btn_start_size = btn_screen_pos,btn_size + butter = s.global_butter*1.3 + + grow_anim = Animate( + widget=btn, + duration=butter, + attrs={ + 'position':(btn_start_pos,picker_pos), + 'size':(btn_start_size,picker_size), + 'textcolor':( + (*Color.TEXT,Color.TEXT_OPACITY), + Const.INVISIBLE + ) + } + ) + shadow_anim = Animate( + widget=shadow, + attrs={ + 'opacity':(0,Color.SHADOW_OPACITY), + 'position':(btn_start_pos,picker_shadow_pos), + 'size':(btn_start_size,picker_shadow_size) + }, + duration=butter + ) + + child_start_progress = 0.35 + child_delay = butter*child_start_progress + child_duration = butter*(1-child_start_progress)+0.05 + + picker_kids = [] + kid_anims = [] + + picker_scroll = bui.scrollwidget( + parent=s.root, + position=picker_pos, + size=(0,picker_size[1]), + color=Color.COLD, + border_opacity=0 + ) + picker_kids.append(picker_scroll) + kid_anims.append(Animate( + widget=picker_scroll, + duration=child_duration, + delay=child_delay, + attrs={ + 'size':((0,picker_size[1]),picker_size), + 'border_opacity':(0,Color.OPACITY), + 'color':(Color.COLD,Color.BASE) + } + )) + + row_h = 35 + picker_root = bui.containerwidget( + parent=picker_scroll, + background=False, + size=(picker_x,row_h*len(options)) + ) + picker_kids.append(picker_root) + + def close_sub(instant=False): + s.window_sub_on = None + for anim in kid_anims: anim.cancel() + kid_anims.clear() + for w in picker_kids: + if w.exists(): w.delete() + picker_kids.clear() + dur = instant and 0.0001 or butter + grow_anim.reverse(duration=dur) + shadow_anim.reverse(duration=dur) + bui.buttonwidget(btn,on_activate_call=reopen,selectable=True) + if not instant: + Eval.SOUND(Const.OK_SOUND).play() + + def pick(v): + Eval.SOUND(Const.OK_SOUND).play() + on_pick(v) + close_sub() + + def reopen(): + s.open_side_picker( + btn,btn_screen_pos,btn_size,shadow, + options,get_current,on_pick,label,option_color,option_textcolor + ) + + current = get_current() + row_off = 15 + for i,opt in enumerate(options): + y_pos = row_h*(len(options)-1-i) + txt = label(opt) if callable(label) else str(opt) + if option_color: + cr,cg,cb = option_color(opt) + luma = (cr+cg+cb)/3 + nudge = 1.6 if luma < 1.0 else 0.75 + swatch = (cr*nudge,cg*nudge,cb*nudge) if opt == current else (cr,cg,cb) + else: + swatch = Color.BASE if opt != current else Color.COLD + row_text = option_textcolor(opt) if option_textcolor else Color.TEXT + b = bui.buttonwidget( + parent=picker_root, + position=(row_off,y_pos), + size=(picker_x,row_h), + label=txt, + color=swatch, + textcolor=Const.INVISIBLE, + opacity=0, + texture=Eval.TEXTURE(Const.SKIN), + enable_sound=False, + on_activate_call=bui.CallPartial(pick,opt) + ) + picker_kids.append(b) + stagger = 0.02*i + kid_anims.append(Animate( + widget=b, + duration=child_duration, + delay=child_delay+stagger, + attrs={ + 'position':((row_off,y_pos),(0,y_pos)), + 'opacity':(0,Color.OPACITY), + 'textcolor':( + Const.INVISIBLE, + (*row_text,Color.OPACITY) + ) + } + )) + + s.window_sub_on = (btn,reopen,close_sub) + + def make_window_kids_settings(s): + s.making_window_kids = True + s.make_window_default(title=Strings.SETTINGS,reverse_off=True) + s.settings_opacity_dirty = False + + sx,sy = s.window_size + delay = 0.35 + row_h = Const.SETTINGS_ROW_H + step = row_h+Const.SETTINGS_ROW_GAP + ctrl_w = Const.SETTINGS_CYCLE_W + num_w = Const.SETTINGS_NUMERIC_W + + tb_size = (35,35) + tb_w,tb_h = tb_size + tb_gap = 8 + tb_inset = s.window_marg-s.window_fix + top_y = sy-tb_h-s.window_marg + lang_x = sx-tb_w-tb_inset + theme_x = lang_x-tb_gap-tb_w + + size = dx,dy = (sx-s.window_marg*2-s.window_fix+10, sy-s.window_marg*9) + pos = px,py = (s.window_marg-s.window_fix, s.window_marg-s.window_fix) + scroll = bui.scrollwidget( + parent=s.root, + position=pos, + color=Color.BASE, + size=(0,0), + border_opacity=0 + ) + s.window_kids.append((scroll,pos,20,delay, + ('size',((dx*2/3,dy*2/3),size)), + ('border_opacity',(0,Color.OPACITY)), + ('color',(Color.COLD,Color.BASE)) + )) + root = bui.containerwidget(parent=scroll,background=False) + + trash = [] + label_w = dx-ctrl_w-40 + + + def add_label(y,text): + trash.append(bui.textwidget( + parent=root, + position=(10,y), + size=(label_w,row_h), + text=text, + h_align='left', + v_align='center', + color=(*Color.TEXT,Color.TEXT_OPACITY), + maxwidth=label_w + )) + + def add_checkbox(y,text,key,on_change=None): + add_label(y,text) + def do(v): + Settings.set(key,v) + callable(on_change) and on_change(v) + trash.append(bui.checkboxwidget( + parent=root, + position=(dx-54,y+row_h/2-15), + size=(30,30), + text='', + value=Settings.get(key), + color=Color.BASE, + textcolor=Const.INVISIBLE, + on_value_change_call=do + )) + + live_fields = [] + + def add_numeric(y,text,key,fmt='{:.2f}',minv=None,maxv=None,on_change=None): + add_label(y,text) + field = bui.textwidget( + parent=root, + position=(dx-num_w-10,y+2), + size=(num_w,row_h-4), + text=fmt.format(Settings.get(key)), + editable=True, + allow_clear_button=False, + h_align='center', + v_align='center', + color=(*Color.TEXT,Color.TEXT_OPACITY) + ) + trash.append(field) + live_fields.append({ + 'kind':'numeric','field':field,'key':key,'fmt':fmt, + 'minv':minv,'maxv':maxv,'on_change':on_change, + 'last':fmt.format(Settings.get(key)) + }) + + def add_text(y,text,key,width,on_change=None): + add_label(y,text) + eat = 40 + field = bui.textwidget( + parent=root, + position=(dx-(width-eat)-10,y+2), + size=(width-eat,row_h-4), + text=str(Settings.get(key)), + editable=True, + allow_clear_button=False, + h_align='center', + v_align='center', + color=(*Color.TEXT,Color.TEXT_OPACITY) + ) + trash.append(field) + live_fields.append({ + 'kind':'text','field':field,'key':key, + 'on_change':on_change,'last':str(Settings.get(key)), + 'fallback':Settings.DEFAULTS.get(key,'') + }) + + def poll_live_fields(): + if not root.exists(): + s.settings_live_timer = None + return + for lf in live_fields: + field = lf['field'] + if not field.exists(): continue + raw = bui.textwidget(query=field) + if raw == lf['last']: continue + + if lf['kind'] == 'text': + value = raw.strip() or lf['fallback'] + if value != Settings.get(lf['key']): + Settings.set(lf['key'],value) + callable(lf['on_change']) and lf['on_change'](value) + lf['last'] = raw + continue + + try: + v = float(raw) + except Exception: + lf['last'] = raw + continue + clamped = v + if lf['minv'] is not None: clamped = max(lf['minv'],clamped) + if lf['maxv'] is not None: clamped = min(lf['maxv'],clamped) + if clamped != Settings.get(lf['key']): + Settings.set(lf['key'],clamped) + callable(lf['on_change']) and lf['on_change'](clamped) + shown = lf['fmt'].format(clamped) + if shown != raw: + bui.textwidget(field,text=shown) + lf['last'] = shown + s.settings_live_timer = bui.AppTimer(0.1,poll_live_fields,repeat=True) + + def add_cycle(y,text,key,options,on_change=None): + add_label(y,text) + def do(): + cur = Settings.get(key) + i = options.index(cur) if cur in options else 0 + nxt = options[(i+1) % len(options)] + Settings.set(key,nxt) + bui.buttonwidget(btn,label=str(nxt)) + Eval.SOUND(Const.OK_SOUND).play() + callable(on_change) and on_change(nxt) + btn = bui.buttonwidget( + parent=root, + position=(dx-ctrl_w-10,y+2), + size=(ctrl_w,row_h-4), + label=str(Settings.get(key)), + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + textcolor=(*Color.TEXT,Color.TEXT_OPACITY), + enable_sound=False, + on_activate_call=do + ) + trash.append(btn) + + def add_top_picker(x,char_label,key,options,label=None,on_change=None,option_color=None,option_textcolor=None): + pos = (x,top_y) + btn = bui.buttonwidget( + parent=s.root, + position=pos, + size=tb_size, + label=char_label, + enable_sound=False, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + textcolor=Const.INVISIBLE, + opacity=0, + selectable=True + ) + s.window_kids.append((btn,pos,-50,delay)) + shadow = bui.imagewidget( + parent=s.root, + opacity=0, + texture=Eval.TEXTURE(Const.SHADOW), + color=Color.SHADOW + ) + trash.append(shadow) + + def do_pick(v): + Settings.set(key,v) + callable(on_change) and on_change(v) + + def open_picker(): + wx,wy = s.window_pos + btn_screen_pos = (wx+pos[0],wy+pos[1]) + s.open_side_picker( + btn,btn_screen_pos,tb_size,shadow, + options,lambda: Settings.get(key),do_pick,label,option_color,option_textcolor + ) + bui.buttonwidget(btn,on_activate_call=open_picker,selectable=True) + + def add_button(y,text,on_press): + def do(): + Eval.SOUND(Const.OK_SOUND).play() + on_press() + trash.append(bui.buttonwidget( + parent=root, + position=(10,y+2), + size=(dx-20,row_h-4), + label=text, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + textcolor=(*Color.TEXT,Color.TEXT_OPACITY), + enable_sound=False, + on_activate_call=do + )) + + def add_header(y,text): + trash.append(bui.textwidget( + parent=root, + position=(0,y), + size=(dx,row_h), + text=text, + h_align='center', + v_align='center', + scale=0.85, + color=(*Color.TEMP,Color.OPACITY) + )) + + add_top_picker(theme_x,Eval.CHAR(Const.THEME_ICON),'theme',tuple(Settings.THEMES), + on_change=lambda v: s.refresh_theme(), + option_color=lambda v: Settings.THEMES[v]['base'], + option_textcolor=lambda v: Settings.THEMES[v]['text']) + add_top_picker(lang_x,'A\u3042','language',tuple(Strings.LANGUAGES), + label=lambda v: Strings.LANGUAGE_NAMES.get(v,v), + on_change=lambda v: s.refresh_language()) + + general_rows = 16 + debug_rows = 6 + total_slots = general_rows+1+debug_rows # +1 for the "Debug" header + rsy = max(total_slots*step+20, dy-15) + y = rsy-row_h + + add_numeric(y,Strings.SETTING_ENTRY_DURATION,'entry_duration', + minv=0.05,maxv=60, + on_change=lambda v: setattr(s,'object_duration',v)); y -= step + add_numeric(y,Strings.SETTING_ANIM_SPEED,'anim_speed', + minv=0.1,maxv=5); y -= step + add_numeric(y,Strings.SETTING_BASE_OPACITY,'base_opacity', + minv=0.05,maxv=1, + on_change=lambda v: ( + Settings.apply_all(), + setattr(s,'settings_opacity_dirty',True) + )); y -= step + add_numeric(y,Strings.SETTING_TEXT_OPACITY,'text_opacity', + minv=0.05,maxv=1, + on_change=lambda v: ( + Settings.apply_all(), + setattr(s,'settings_opacity_dirty',True) + )); y -= step + add_checkbox(y,Strings.SETTING_AUTOSAVE_ON,'autosave_on'); y -= step + add_numeric(y,Strings.SETTING_AUTOSAVE_INTERVAL,'autosave_interval', + fmt='{:.0f}',minv=Const.AUTOSAVE_MIN_INTERVAL,maxv=600, + on_change=lambda v: s.restart_autosave_timer()); y -= step + add_checkbox(y,Strings.SETTING_UI_ANIM_ON,'ui_anim_on'); y -= step + add_checkbox(y,Strings.SETTING_SFX_EDITOR,'sfx_editor_on'); y -= step + add_checkbox(y,Strings.SETTING_SFX_UI,'sfx_ui_on'); y -= step + add_checkbox(y,Strings.SETTING_IGNORE_PLAYBACK_ERRORS,'ignore_playback_errors'); y -= step + add_checkbox(y,Strings.SETTING_BRP_TEXT_EXPORT,'brp_text_export'); y -= step + add_text(y,Strings.SETTING_EXPORT_FILENAME,'export_filename_template', + dx-100); y -= step + add_checkbox(y,Strings.SETTING_TOAST_TOP,'toast_top', + on_change=lambda v: (s.wrap([1]),s.reset_toast_position())); y -= step + add_checkbox(y,Strings.SETTING_FANCY_AUTOSAVE,'fancy_autosave'); y -= step + add_numeric(y,Strings.SETTING_TOAST_DURATION,'toast_duration', + fmt='{:.1f}',minv=0.5,maxv=15); y -= step + add_checkbox(y,Strings.SETTING_EPIC_MODE,'epic_mode', + on_change=lambda v: s.toast( + Strings.INFO_EPIC_ON if v else Strings.INFO_EPIC_OFF + )); y -= step + + add_header(y,Strings.SETTING_DEBUG_HEADER); y -= step + + add_checkbox(y,Strings.SETTING_SHOW_GRID_2D,'show_grid_2d', + on_change=lambda v: s.build_grid()); y -= step + add_checkbox(y,Strings.SETTING_SHOW_GRID_3D,'show_grid_3d', + on_change=lambda v: s.build_grid()); y -= step + add_button(y,Strings.SETTING_DUMP_MEMORY,lambda: ( + print('=== MOVI DEBUG: s.memory ==='), + print(s.memory), + s.toast(Strings.INFO_DUMPED_MEMORY) + )); y -= step + add_button(y,Strings.SETTING_DUMP_TIMELINE,lambda: ( + print('=== MOVI DEBUG: s.timeline ==='), + print(s.timeline), + s.toast(Strings.INFO_DUMPED_TIMELINE) + )); y -= step + add_cycle(y,Strings.SETTING_ASPECT_RATIO,'aspect_ratio',Settings.ASPECT_RATIOS, + on_change=lambda v: s.wrap_aspect_bars()); y -= step + add_checkbox(y,Strings.SETTING_FILL_ASPECT_RATIO,'fill_aspect_ratio', + on_change=lambda v: s.wrap_aspect_bars()); y -= step + + bui.containerwidget(root,size=(dx,rsy)) + s.window_trash = [trash] + + s.wrap_window_kids() + s.animate_window_kids() + s.making_window_kids = False + + ABOUT_FONT_5X7 = { + 'M': ('10001','11011','10101','10001','10001','10001','10001'), + 'O': ('01110','10001','10001','10001','10001','10001','01110'), + 'V': ('10001','10001','10001','10001','10001','01010','00100'), + 'I': ('11111','00100','00100','00100','00100','00100','11111'), + } + + def make_window_kids_about(s): + """About window - same grow-from-button/back-button/scroll + shell as make_window_kids_settings (make_window_default is + shared with it), just a static bit of content instead of a + settings list: a big "MOVI" logo built out of small square + imagewidgets (a dot-matrix glyph per letter, see + ABOUT_FONT_5X7) that does one hue sweep - each letter phase- + shifted from its neighbour so the sweep reads as a single + band of color walking left-to-right across the whole word - + before settling on the theme's own text color, followed by + the version, copyright, and a thank-you line.""" + s.making_window_kids = True + s.make_window_default(title=Strings.ABOUT_TITLE,reverse_off=True) + + sx,sy = s.window_size + x,y = s.window_pos + delay = 0.35 + trash = [] + + title_text = 'MOVI' + cell = 11 + gap_px = 1 + letter_gap = 16 + cols,rows = 5,7 + letter_w = cols*cell + logo_w = len(title_text)*letter_w+(len(title_text)-1)*letter_gap + start_x = sx/2-logo_w/2 + + bottom_margin = 28 + text_h = 24 + thanks_y = bottom_margin+text_h/2 + tagline_y = thanks_y+26 + version_y = tagline_y+32 + logo_bottom_y = version_y+50 + logo_top_y = logo_bottom_y+rows*cell + + letter_pixel_groups = [] + pixel_widgets = [] + for li,ch in enumerate(title_text): + pattern = s.ABOUT_FONT_5X7[ch] + letter_x = start_x+li*(letter_w+letter_gap) + pixels = [] + for r,row in enumerate(pattern): + for c,bit in enumerate(row): + if bit != '1': continue + px = x+letter_x+c*cell + py = y+logo_top_y-(r+1)*cell + w = bui.imagewidget( + parent=s.root, + texture=Eval.TEXTURE(Const.SKIN), + position=(px,py), + size=(cell-gap_px,cell-gap_px), + color=Color.BASE, + opacity=0 + ) + trash.append(w) + pixels.append(w) + pixel_widgets.append(w) + letter_pixel_groups.append(pixels) + + hue_span = 0.85 + fine_steps = 48 + step_dur = 0.035 + + def hue_color(h): + r,g,b = hsv_to_rgb(h%1.0,1.0,1.0) + return (r*2.3,g*2.3,b*2.3) + + def make_stops(hue_start): + return tuple( + hue_color(hue_start+hue_span*(i/(fine_steps-1))) + for i in range(fine_steps) + ) + + def settle_color(w): + s.anims[id(w)].pop('about_sweep',None) + if not w.exists(): return + bui.imagewidget(w,color=Color.TEXT,opacity=Color.TEXT_OPACITY) + + def sweep_pixel(w,prev_color,stops,step_dur): + if not w.exists(): + s.anims[id(w)].pop('about_sweep',None) + return + if not stops: + settle_color(w) + return + nxt,*rest = stops + anim = Animate( + widget=w, + duration=step_dur, + attrs={'color':(prev_color,nxt)}, + on_finish=lambda: sweep_pixel(w,nxt,rest,step_dur) + ) + s.anims[id(w)]['about_sweep'] = anim + + letter_hue_offset = 0.05 + letter_stagger = 0.05 + for li,pixels in enumerate(letter_pixel_groups): + stops = make_stops(li*letter_hue_offset) + first,*rest = stops + letter_delay = delay+li*letter_stagger + for w in pixels: + anim = Animate( + widget=w, + duration=step_dur, + delay=letter_delay, + attrs={ + 'opacity':(0,Color.TEXT_OPACITY), + 'color':(Color.BASE,first) + }, + on_finish=lambda w=w,rest=rest,first=first: sweep_pixel(w,first,rest,step_dur) + ) + s.anims[id(w)]['about_sweep'] = anim + + s.about_letter_kids = pixel_widgets + + version = bui.textwidget( + parent=s.root, + text=Strings.ABOUT_VERSION.format(__version__), + position=(0,version_y), + size=(sx,24), + h_align=Const.ALIGN, + v_align=Const.ALIGN, + scale=0.75, + color=Const.INVISIBLE + ) + trash.append(version) + s.window_kids.append((version,(0,version_y),50,delay+0.15)) + + tagline_w = bui.textwidget( + parent=s.root, + text=Strings.ABOUT_TAGLINE, + position=(0,tagline_y), + size=(sx,24), + h_align=Const.ALIGN, + v_align=Const.ALIGN, + scale=0.68, + color=Const.INVISIBLE + ) + trash.append(tagline_w) + s.window_kids.append((tagline_w,(0,tagline_y),50,delay+0.2)) + + thanks_w = bui.textwidget( + parent=s.root, + text=Strings.ABOUT_THANKS, + position=(0,thanks_y), + size=(sx,24), + h_align=Const.ALIGN, + v_align=Const.ALIGN, + scale=0.68, + color=Const.INVISIBLE + ) + trash.append(thanks_w) + s.window_kids.append((thanks_w,(0,thanks_y),50,delay+0.25)) + + s.window_trash = [trash] + + s.wrap_window_kids() + s.animate_window_kids() + s.making_window_kids = False + + def kill(s,on_kill=None): + def finish(): + callable(on_kill) and on_kill() + s.hard_cleanup() + s.animate_out( + on_finish=finish + ) + s.ui_on = False + s.ui_clickable = False + bs.get_foreground_host_activity().globalsnode.area_of_interest_bounds = Const.EXIT_BOUNDS + + def hard_cleanup(s): + for attr in s.__dict__.copy(): + isinstance(attr,list) and attr.clear() + delattr(s,attr) + + def make_menu(s): + s.menu_bg = bui.imagewidget( + parent=s.root, + opacity=0, + color=Color.BASE, + texture=Eval.TEXTURE(Const.SKIN) + ) + s.menu_kids = [] + for t in Strings.MENUS: + w = bui.buttonwidget( + parent=s.root, + enable_sound=False, + label=t, + size=(0,0), + opacity=0, + textcolor=Const.INVISIBLE, + color=Color.BASE, + texture=Eval.TEXTURE(Const.SKIN) + ) + s.menu_kids.append(w) + s.seed_input = bui.textwidget( + parent=s.root, + position=(0,0), + size=(0,0), + editable=True, + allow_clear_button=False, + color=(*Color.TEXT,Color.TEXT_OPACITY), + v_align=Const.ALIGN, + glow_type=Const.GLOW, + description=Strings.SEED + ) + + def complete_all(s): + for widget_id, anim_dict in s.anims.items(): + if isinstance(anim_dict,dict): + for anim in list(anim_dict.values()): + anim.complete() + else: anim_dict.complete() + + def wrap_all(s,autofix=True,init=False): + s.complete_all() + s.wrap(init=init) + s.wrap_menu() + s.wrap_window_kids() + s.wrap_aspect_bars() + autofix and bui.apptimer( + Const.BA_LAG, bui.CallPartial( + s.wrap_all, autofix=False + )) + + def wrap_aspect_bars(s): + """Letterbox/pillarbox preview: draws flat black bars over + whatever's outside the chosen aspect ratio, so you can see + what a recording would crop before it's too late. + + These are scene 'image' nodes attached to screen corners + (bascenev1), not UI widgets - they belong on the scene screen + itself so they sit under/over the world the same way a real + letterbox would, independent of whichever editor window + happens to be open. + + If 'fill_aspect_ratio' is on, four more bars are added behind + these same four, each massively overscanned past the safe + window's own edge (see Const.FILL_ASPECT_OVERSCAN) - like + painting the wall around a window frame, not just the window + itself, so whatever real device margin exists outside + get_virtual_screen_size()'s safe box reads as part of the + letterbox too instead of showing raw unpainted screen.""" + name = Settings.get('aspect_ratio') + ratio = Const.ASPECT_RATIO_VALUES.get(name) + if not ratio: + s.destroy_aspect_bars() + return + rx,ry = bui.get_virtual_screen_size() + target_w,target_h = rx,rx/ratio + if target_h > ry: + target_h,target_w = ry,ry*ratio + bar_h = max((ry-target_h)/2,0) + bar_w = max((rx-target_w)/2,0) + fill = Settings.get('fill_aspect_ratio') + over = Const.FILL_ASPECT_OVERSCAN + try: + activity = bs.get_foreground_host_activity() + except Exception: + return + try: + with activity.context: + if not s.aspect_bars: + s.aspect_bars = [ + bs.newnode('image',attrs={ + 'texture':bs.gettexture(Const.SKIN), + 'color':(0,0,0), + 'opacity':1, + 'attach':'bottomLeft' + }) for _ in range(4) + ] + if fill and not s.aspect_fill_bars: + s.aspect_fill_bars = [ + bs.newnode('image',attrs={ + 'texture':bs.gettexture(Const.SKIN), + 'color':(0,0,0), + 'opacity':1, + 'attach':'bottomLeft' + }) for _ in range(4) + ] + elif not fill and s.aspect_fill_bars: + for n in s.aspect_fill_bars: + if n.exists(): n.delete() + s.aspect_fill_bars = [] + + top,bottom,left,right = s.aspect_bars + for n,pos,scale in ( + (top, (rx/2,ry-bar_h/2), (rx,bar_h)), + (bottom,(rx/2,bar_h/2), (rx,bar_h)), + (left, (bar_w/2,ry/2), (bar_w,ry)), + (right, (rx-bar_w/2,ry/2), (bar_w,ry)), + ): + n.position = pos + n.scale = scale + + if fill and s.aspect_fill_bars: + ftop,fbottom,fleft,fright = s.aspect_fill_bars + over_h = ry*over + over_w = rx*over + for n,pos,scale in ( + (ftop, (rx/2,ry-bar_h+over_h/2), (rx+over_w*2,over_h)), + (fbottom,(rx/2,bar_h-over_h/2), (rx+over_w*2,over_h)), + (fleft, (bar_w-over_w/2,ry/2), (over_w,ry+over_h*2)), + (fright, (rx-bar_w+over_w/2,ry/2), (over_w,ry+over_h*2)), + ): + n.position = pos + n.scale = scale + except Exception: + print(format_exc()) + s.destroy_aspect_bars() + + def destroy_aspect_bars(s): + for n in getattr(s,'aspect_bars',None) or []: + if n.exists(): n.delete() + s.aspect_bars = None + for n in getattr(s,'aspect_fill_bars',None) or []: + if n.exists(): n.delete() + s.aspect_fill_bars = [] + + def wrap_menu(s): + rx,ry = bui.get_virtual_screen_size() + sx,sy = s.menu_size = Eval.SCALE_REAL(240,370) + s.menu_start_size = (sx*0.8,sy*0.8) + s.menu_yoff, = Eval.SCALE_REAL(62) + s.menu_marg, = Eval.SCALE_REAL(10) + x,y = s.menu_pos = rx-sx+2,ry-sy-s.menu_yoff + s.menu_start_pos = ( + rx-s.menu_start_size[0], + ry-s.menu_yoff-s.menu_start_size[1] + ) + bx = sx-s.menu_marg*4 + by, = Eval.SCALE_REAL(40) + one, = Eval.SCALE_REAL(1) + s.menu_kid_size = bx,by + s.menu_kid_start_size = (bx/2,by) + s.menu_button_xp = x+s.menu_marg*2 + s.menu_kid_yp = lambda i: ( + y+s.menu_marg*1.5+(by+s.menu_marg)*i + ) + s.menu_kid_start_pos = lambda i:( + s.menu_button_xp+bx/2, + s.menu_kid_yp(i) + ) + s.menu_kid_pos = lambda i:( + s.menu_button_xp, + s.menu_kid_yp(i) + ) + s.seed_button_shrunk_size = (s.menu_kid_size[0]*0.3, s.menu_kid_size[1]) + s.seed_input_size = (s.menu_kid_size[0]*0.7-s.menu_marg, s.menu_kid_size[1]) + s.seed_input_pos = lambda i: ( + s.menu_button_xp, + s.menu_kid_yp(i) + ) + bui.imagewidget( + s.menu_bg, + position=s.menu_pos, + size=s.menu_size + ) + if s.menu_on: + a = s.anims[id(s.menu_bg)].attrs_start + a['size'] = s.menu_start_size + a['position'] = s.menu_start_pos + a = s.anims[id(s.menu_bg)].attrs_end + a['size'] = s.menu_size + a['position'] = s.menu_pos + a = s.anims[id(s.menu_bg)].attrs_current + a['size'] = s.menu_size + a['position'] = s.menu_pos + for i,kid in enumerate(s.menu_kids): + if i == 2 and s.seed_on: + kid_size = s.seed_button_shrunk_size + kid_pos = ( + s.menu_button_xp + (s.menu_kid_size[0] - s.seed_button_shrunk_size[0]) + 5, + s.menu_kid_yp(i) + ) + else: + kid_size = (bx, by) + kid_pos = ( + s.menu_button_xp, + s.menu_kid_yp(i) + ) + + bui.buttonwidget( + kid, + size=kid_size, + position=kid_pos, + text_scale=one + ) + + if s.menu_on: + a = s.anims[id(kid)]['main'].attrs_start + a['size'] = s.menu_kid_start_size + a['position'] = s.menu_kid_start_pos(i) + a = s.anims[id(kid)]['main'].attrs_current + a['size'] = kid_size + a['position'] = kid_pos + a = s.anims[id(kid)]['main'].attrs_end + a['size'] = kid_size + a['position'] = kid_pos + bui.textwidget( + s.seed_input, + position=s.seed_input_pos(2), + size=s.seed_on and s.seed_input_size or (0,0) + ) + + if s.menu_on and (anim := s.anims.get(id(s.seed_input))): + if s.seed_on: + try: + anim.attrs_end['position'] = s.seed_input_pos(2) + anim.attrs_current['position'] = s.seed_input_pos(2) + except NameError: + return + + def toggle_menu(s,on_finish=None,shut=False): + shut or Eval.SOUND(Const.OK_SOUND).play() + delay = 0.1 + butter = s.global_butter*0.7 + if s.menu_on: + s.menu_on = False + anim = s.anims[id(s.menu_bg)] + s.anims[id(s.menu_bg)] = anim.reverse( + duration=butter + ) + (victim:=s.anims[id(s.menu_kids[2])].pop('seed',None)) and victim.cancel() + (victim:=s.anims[id(s.seed_input)]) and s.seed_on and victim.cancel() + bui.textwidget(s.seed_input,size=(0,0)) + s.seed_on = False + bui.buttonwidget( + s.menu_kids[2], + label=Strings.MENUS[2] + ) + for i,kid in enumerate(s.menu_kids): + anim = s.anims[id(kid)]['main'] + s.anims[id(kid)]['main'] = anim.reverse( + duration=butter*0.7 + ) + bui.buttonwidget( + kid, on_activate_call=Const.DO_NOTHING + ) + return + s.menu_on = True + if (anim:=s.anims[id(s.menu_bg)]): + anim.cancel() + s.anims[id(s.menu_bg)] = Animate( + widget=s.menu_bg, + duration=butter, + attrs={ + 'opacity':(0,Color.OPACITY), + 'position':( + s.menu_start_pos, + s.menu_pos + ), + 'size':( + s.menu_start_size, + s.menu_size + ) + } + ) + def menu_action(i): + if i == 0: + Eval.SOUND(Const.OK_SOUND).play() + s.toast(Strings.BYE) + s.farewell() + if i == 1: + if s.can_do != 'nuke': + s.toast(Strings.INFO_CONFIRM_CLEAR,extra=2) + Eval.SOUND(Const.OK_SOUND).play() + s.can_do = 'nuke' + return + if s.playing: s.stop() + s.clear_memory() + type(s)._shared['on_create'].append(( + lambda z: ( + z.toast(Strings.INFO_MEMORY_CLEARED) or + Eval.SOUND(Const.ACTION_SOUND).play() + ), () + )) + s.recreate() + if i == 2: + s.seed_on = not s.seed_on + target_label = Strings.DONE if s.seed_on else Strings.MENUS[2] + + target_size = s.seed_button_shrunk_size + target_pos = ( + s.menu_button_xp + (s.menu_kid_size[0] - s.seed_button_shrunk_size[0]) + 5, + s.menu_kid_yp(2) + ) + + if s.seed_on: + anim = s.anims[id(s.menu_kids[1])].get('main') + start_size = s.menu_kid_size + start_pos = (s.menu_button_xp, s.menu_kid_yp(2)) + if anim and not anim.finished: + start_size = anim.attrs_current['size'] + start_pos = anim.attrs_current['position'] + anim.cancel() + + def change_label(): + bui.buttonwidget(s.menu_kids[2], label=target_label) + Animate( + widget=s.menu_kids[2], + attrs={ + 'textcolor': ( + Const.INVISIBLE, + (*Color.TEXT, Color.TEXT_OPACITY) + ) + }, + duration=s.global_butter / 2 + ) + + s.anims[id(s.menu_kids[2])]['seed'] = Animate( + widget=s.menu_kids[2], + attrs={ + 'size': (start_size, target_size), + 'position': (start_pos, target_pos), + 'textcolor': ( + (*Color.TEXT, Color.TEXT_OPACITY), + Const.INVISIBLE + ) + }, + duration=s.global_butter / 2, + on_finish=change_label + ) + else: + bui.buttonwidget(s.menu_kids[2], label=target_label) + + if s.seed_on: + anim_input = s.anims.get(id(s.seed_input)) + start_input_size = (0, s.menu_kid_size[1]) + if anim_input and not anim_input.finished: + start_input_size = anim_input.attrs_current['size'] + anim_input.cancel() + + s.anims[id(s.seed_input)] = Animate( + widget=s.seed_input, + attrs={ + 'size': (start_input_size, s.seed_input_size), + 'color': (Const.INVISIBLE, (*Color.TEXT,Color.TEXT_OPACITY)) + }, + duration=s.global_butter + ) + else: + bui.textwidget(s.seed_input, size=(0, s.menu_kid_size[1]), color=Const.INVISIBLE) + + if not s.seed_on: + seed_text = bui.textwidget(query=s.seed_input) + s.toggle_menu(shut=True) + if not seed_text: + s.toast(Format.ERROR_EMPTY(Strings.SEED)) + Eval.SOUND(Const.BAD_SOUND).play() + return + bui.textwidget(s.seed_input, text='') + try: memory = Eval.DECODE(seed_text) + except Exception as e: + s.toast(Format.ERROR(e)) + Eval.SOUND(Const.BAD_SOUND).play() + return + s.clear_memory() + type(s)._shared['on_create'].append(( + lambda z,mem: z.load_memory(mem), + (memory,) + )) + s.recreate() + Eval.SOUND(Const.OK_SOUND).play() + if i == 3: + try: seed = Eval.ENCODE(s.memory) + except Exception as e: + s.toast(Format.ERROR(e)) + Eval.SOUND(Const.BAD_SOUND).play() + return + bui.clipboard_set_text(str(seed)) + Eval.SOUND(Const.GOOD_SOUND).play() + s.toast(Strings.INFO_COPIED) + s.toggle_menu() + if i == 4: + s.toggle_ui() + s.toggle_menu() + Eval.SOUND(Const.OK_SOUND).play() + if i == 5: + if not s.stamp_kids: + s.toast(Strings.ERROR_NO_MEMORY) + Eval.SOUND(Const.BAD_SOUND).play() + return + Eval.SOUND(Const.OK_SOUND).play() + s.save_state() + type(s)._shared['on_create'].append(( + lambda z: z.start_recording(), + () + )) + s.recreate() + if i == 6: + if not s.ui_on: + s.toast(Strings.ERROR_ALREADY_WIDE) + Eval.SOUND(Const.BAD_SOUND).play() + return + Eval.SOUND(Const.OK_SOUND).play() + s.play() + s.wrap_controls() + s.is_wide = True + s.toggle_ui() + s.toggle_menu() + for i,kid in enumerate(s.menu_kids): + if (anim:=s.anims[id(kid)].get('main')): + anim.cancel() + s.anims[id(kid)]['main'] = Animate( + widget=kid, + delay=delay+0.08-0.03*i, + duration=butter, + attrs={ + 'size':( + s.menu_kid_start_size, + s.menu_kid_size + ), + 'opacity':(0,Color.OPACITY), + 'textcolor':( + Const.INVISIBLE, + (*Color.TEXT,Color.TEXT_OPACITY) + ), + 'position':( + s.menu_kid_start_pos(i), + s.menu_kid_pos(i) + ) + } + ) + bui.buttonwidget( + kid, on_activate_call=bui.CallPartial( + menu_action, i + ) + ) + + def farewell(s): + s.restore_infos() + s.toggle_menu() + def on_kill(): + bui.app.classic.return_to_main_menu_session_gracefully(reset_ui=False) + bui.apptimer( + Const.BA_LAG+Const.BA_LAG_SMALL*16, + bui.CallPartial( + _ba.set_camera_manual, + False + ) + ) + s.kill(on_kill=on_kill) + Eval.SOUND(Const.OK_SOUND).play() + s.save_state() + + @clickable + def toggle_event(s,passive=False): + if s.window_on and not passive: + s.dismiss_window() + return + Eval.SOUND(Const.OK_SOUND).play() + for kid in s.event_kids: + if (fix_anim := s.anims[id(kid)].get('fix', None)): + fix_anim.cancel() + s.anims[id(kid)].pop('fix', None) + + def push(): + w = s.edit_button + ex,ey = s.edit_button_pos + start,end = s.edit_button_pos, s.edit_button_pos2 + if (anim:=s.anims[id(w)].get('push',None)): + anim.cancel() + start_pos = anim.attrs_current['position'] + else: start_pos = s.event_on and end or start + end_pos = s.event_on and start or end + s.anims[id(w)]['push'] = Animate( + widget=w, + attrs={ + 'position':(start_pos,end_pos) + }, + duration=s.global_butter, + delay=s.event_on and 0.07 or 0 + ) + w = s.key_button + ex,ey = s.key_button_pos + start,end = s.key_button_pos, s.key_button_pos2 + if (anim:=s.anims[id(w)].get('push',None)): + anim.cancel() + start_pos = anim.attrs_current['position'] + else: start_pos = s.event_on and end or start + end_pos = s.event_on and start or end + if s.key_window in s.window_on: + s.anims[id(w)]['window'].attrs_start['position'] = end_pos + s.anims[id(w)]['shadow'].attrs_start['position'] = end_pos + if 'push' in s.anims[id(w)]: + s.anims[id(w)]['push'].attrs_start['position'] = start_pos + s.anims[id(w)]['push'].attrs_end['position'] = end_pos + s.anims[id(w)]['push'].attrs_current['position'] = end_pos + return + s.anims[id(w)]['push'] = Animate( + widget=w, + attrs={ + 'position':(start_pos,end_pos) + }, + duration=s.global_butter, + delay=s.event_on and 0.07 or 0 + ) + push() + dur = s.global_butter*1.5 + old_anim = s.anims.get(id(s.event_root),None) + if s.event_on: + s.event_on = False + bui.buttonwidget(s.event_button, label=Strings.EVENT_BUTTON_OFF) + + s.anims[id(s.event_root)] = old_anim.reverse( + duration=dur + ) + for kid,d in s.event_kids.items(): + an = s.anims[id(kid)] + if (anim:=an.pop('window',None)): + anim.cancel() + if (anim:=an.pop('extra',None)): + anim.cancel() + old = an.get('main',None) + if old: + old.cancel() + s.anims[id(kid)]['main'] = old.reverse( + duration=dur/4 + ) + bui.buttonwidget( + kid, + on_activate_call=Const.DO_NOTHING, + selectable=False + ) + return + + s.event_on = True + bui.buttonwidget(s.event_button, label=Strings.EVENT_BUTTON_ON) + + rx, ry = s.real + sx, sy = s.event_menu_size + dx, dy = s.event_button_size + + child_start_progress = 0.2 + child_delay = dur * child_start_progress + child_duration = dur * (1 - child_start_progress) + + mx = sx - 40 + + if old_anim: old_anim.cancel() + s.anims[id(s.event_root)] = Animate( + widget=s.event_root, + + attrs={ + 'size': ((dx, dy), (sx, sy)) + }, + duration=dur + ) + + num = len(Strings.EVENTS) + parent_width_progress = dx + (sx - dx) * child_start_progress + start_width_ratio = (parent_width_progress - 40) / mx + + times = [] + for i,b in enumerate(s.event_kids): + stagger = 0.02 * (num-i) + s.anims[id(b)]['main'] = ( + Animate( + widget=b, + attrs={ + 'opacity': (0, Color.OPACITY), + 'textcolor': ( + Const.INVISIBLE, + (*Color.TEXT, Color.TEXT_OPACITY) + ), + 'size': ((mx * start_width_ratio, dy), s.event_kid_size) + }, + duration=child_duration, + delay=child_delay + stagger + ) + ) + times.append(child_delay + stagger) + bui.buttonwidget( + b, + on_activate_call=bui.CallPartial( + s.event_window,i + ), + position=( + s.ev_x, + s.event_top-s.ev_mult*(i+1) + ) + ) + + @clickable + def event_window(s,i,edit={},load=False,passive=False,**kw): + if getattr(s,'making_window_kids',False): + s.toast(Strings.INFO_SLOW_DOWN) + Eval.SOUND(Const.BAD_SOUND).play() + return + if s.window_on and not passive: s.dismiss_window() + else: Eval.SOUND(Const.OK_SOUND).play() + b = list(s.event_kids)[i] + call = bui.CallPartial(s.event_window,i) + s.window_on = [b,call,None] + bui.buttonwidget( + b, + on_activate_call=Const.DO_NOTHING, + selectable=False + ) + s.event_kid_pos = (s.ev_x,s.event_top-s.ev_mult*(i+1)) + s.last_window_i = i + sx,sy = s.window_size + dx,dy = s.event_kid_size + butter = 0.5 + s.anims[id(b)]['window'] = ( + Animate( + widget=b, + duration=butter, + attrs={ + 'position':( + s.event_kid_pos, + s.window_pos + ), + 'size':( + s.event_kid_size, + s.window_size + ), + 'textcolor':( + (*Color.TEXT, Color.TEXT_OPACITY), + (*Color.TEXT, 0) + ) + } + ) + ) + s.anims[id(b)]['shadow'] = ( + Animate( + widget=s.event_kids[b]['shadow'], + attrs={ + 'opacity':(0,Color.SHADOW_OPACITY), + 'position':( + s.event_kid_pos, + s.window_shadow_pos + ), + 'size':( + s.event_kid_size, + s.window_shadow_size + ) + }, + duration=butter + ) + ) + s.window_on[2] = s.make_window_kids(i,edit=edit,load=load,**kw) + + def make_window_kids(s,i,edit={},load=False,**kw): + s.making_window_kids = True + s.make_window_default( + title=kw.pop('force_title',None) or ( + edit and not load and Strings.EDIT.format( + edit['data']['name'] + ) or list(Strings.EVENTS.values())[i] + ) + ) + func = ( + i == 0 and s.make_node_window or + i == 1 and s.make_camera_window or + i == 2 and s.make_sound_window or + i == 3 and s.make_fx_window or + i == 4 and s.make_map_window or + i == 5 and s.make_preset_window or + i == 6 and s.make_code_window or + i == 7 and s.make_seed_window or + (lambda *a,**k:s.toast(Strings.COMING_SOON)) + ) + wait = 0 + fin = lambda: ( + s.wrap_window_kids(), + s.animate_window_kids(extra_delay=-wait), + setattr(s,'making_window_kids',False) + ) + r = func(edit,load,**kw) + if isinstance(r,tuple): + wait,r = r + bui.apptimer(wait,fin) + else: fin() + return r + + def animate_window_kids(s,extra_delay=0): + x,y = s.window_pos + for _,g in enumerate(s.window_kids): + w,pos,off,delay,*extra = g + px,py = pos + extra = dict(extra) + attrs = { + 'position':( + (x+px-off,y+py), + (x+px,y+py) + ), + **extra + } + ty = w.get_widget_type() + if ty in ['button','checkbox']: + attrs.update({ + 'textcolor':( + Const.INVISIBLE, + (*Color.TEXT,Color.TEXT_OPACITY) + ) + }) + if ty in ['text']: + attrs.update({ + 'color':( + Const.INVISIBLE, + (*Color.TEXT,Color.TEXT_OPACITY) + ) + }) + if ty in ['image','button']: + attrs.update({ + 'opacity':(0,Color.OPACITY) + }) + s.anims[id(w)] = Animate( + widget=w, + attrs=attrs, + duration=0.18, + delay=delay+extra_delay + ) + + def wrap_window_kids(s): + x,y = s.window_pos + for kid in s.window_kids: + w,p = kid[0:2] + px,py = p + Eval.WIDGET(w)(w,position=(x+px,y+py)) + + def make_window_default(s,title,reverse_off=False): + x,y = s.window_pos + sx,sy = s.window_size + def bye(): + if s.window_sub_on: + s.window_sub_on[2]() + return + s.window_clean() + s.window_back() + s.window_marg = 5 + s.window_fix = 8 + dx,dy = 35,35 + off = reverse_off and -50 or 50 + + pos = (s.window_marg-s.window_fix,sy-dy-s.window_marg) + back = bui.buttonwidget( + parent=s.root, + size=(dx,dy), + enable_sound=False, + label=Eval.CHAR(Const.BACK), + on_activate_call=bye, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + textcolor=Const.INVISIBLE, + opacity=0 + ) + s.window_back_btn = back + s.window_kids.append((back,pos,off,0.35)) + + pos = (sx/2-s.window_marg*4,sy-s.window_marg-32.5) + w = bui.textwidget( + parent=s.root, + text=title, + color=Const.INVISIBLE, + h_align=Const.ALIGN, + v_align=Const.ALIGN, + maxwidth=sx-s.window_marg*3-dx + ) + s.window_kids.append((w,pos,off,0.35)) + + def add_entry(s,final,smol=False): + nam = final['name'] + end_size = smol and ( + s.entry_ys_real, + s.entry_ys_real - s.magic_y + ) or ( + s.entry_xs_real * ( + s.entries_per_sec * + s.object_duration + )*s.magic_right, + s.entry_ys_real-s.magic_y + ) + btn = bui.buttonwidget( + parent=s.stamp_hscroll_root, + texture=Eval.TEXTURE(Const.SKIN), + label=nam, + textcolor=Const.INVISIBLE, + color=Color.BASE, + opacity=0, + enable_sound=False, + size=end_size, + button_type='square' + ) + bui.buttonwidget( + btn, + on_activate_call=bui.CallPartial( + s.select, btn + ) + ) + s.stamp_kids.append(btn) + s.memory[id(btn)] = { + 'order':len(s.memory), + 'event':s.last_window_i, + 'data':final, + 'duration':s.object_duration/(smol and s.entries_per_sec or 1), + 'start':0.0, + 'keys':{}, + 'smol':smol + } + s.build_timeline() + def push(): + for i,kid in enumerate( + reversed(s.stamp_kids) + ): + mem = s.memory[id(kid)] + width_in_steps = mem['duration'] * s.entries_per_sec + old_x = s.magic_x + s.entry_xs_real*mem['start']*s.entries_per_sec + (width_in_steps * s.magic_left) + end_pos = ( + old_x, + s.entry_ys_real*i + ) + s.anims[kid]['push'] = Animate( + widget=kid, + + attrs={ + 'position':( + (old_x,s.entry_ys_real*(i-1)), + end_pos + ) + }, + duration=s.global_butter + ) + for kid in reversed(s.stamp_kids): + kid_mem = s.memory[id(kid)] + + for key_data in kid_mem.get('keys', {}).values(): + if 'widget' in key_data and s.widgets[key_data['widget']].exists(): + wid = s.widgets[key_data['widget']] + wid_id = id(wid) + key_time = key_data['time'] + + new_x = s.magic_x + s.entry_xs_real * key_time * s.entries_per_sec - s.entry_ys_real/4 + new_y = s.entry_ys_real * (len(s.memory) - kid_mem['order'] - 1) + + old_y = s.entry_ys_real * (len(s.memory) - kid_mem['order'] - 2) + + if wid_id not in s.anims: + s.anims[wid_id] = {} + + s.anims[wid_id]['push'] = Animate( + widget=wid, + attrs={ + 'position': ((new_x, old_y), (new_x, new_y)) + }, + duration=s.global_butter + ) + push() + s.wrap([1,2,3],on_finish=s.bottom_left) + def appear(): + bui.buttonwidget( + btn, + textcolor=( + *Color.TEXT, + Color.OPACITY + ), + opacity=Color.OPACITY + ) + if not s.tools_shown: + s.show_controls() + s.wrap() + half_size = hx,hy = tuple(_/2 for _ in s.window_size) + half_pos = (hx*3,hy*2.5) + ( + half_shadow_pos, + half_shadow_size + ) = Eval.SHADOW( + *half_pos, + *half_size, + d=0.18 + ) + opacity = Color.OPACITY + half_opacity = opacity/2 + wait = 0.4 + width_in_steps = s.object_duration * s.entries_per_sec + + where_to = lambda: ( + (bl:=s.bottom_left(dry=True)) and ( + bl[0]+( + width_in_steps * + s.magic_left + ), bl[1] + ) + ) + s.window_back( + to=lambda:{ + 'position':( + half_pos, + where_to() + ), + 'size':( + half_size, + end_size + ), + 'text_scale':(s.event_kid_ts,1) + }, + shadow_to=lambda:{ + 'opacity':(half_opacity,0), + 'position':( + half_shadow_pos, + where_to() + ), + 'size':( + half_shadow_size, + end_size + ) + }, + on_fix=appear, + wait=wait, + instant={ + 'label':nam + }, + extra={ + 'textcolor':( + Const.INVISIBLE, + (*Color.TEXT,Color.TEXT_OPACITY) + ), + 'size':( + s.window_size, + half_size + ), + 'position':( + s.window_pos, + half_pos + ) + }, + shadow_extra={ + 'size':( + s.window_shadow_size, + half_shadow_size + ), + 'position':( + s.window_shadow_pos, + half_shadow_pos + ), + 'opacity':( + opacity, + half_opacity + ) + } + ) + + NODE_DEFAULT_ATTRS = { + 'prop': {'mesh': 'tnt', 'color_texture': 'tnt', 'body': 'crate', 'gravity_scale': '1.0', 'reflection': 'soft'}, + 'text': {'text': 'Hello!', 'color': '(1, 1, 1)', 'scale': '0.02', 'in_world': 'True', 'h_align': 'center'}, + 'light': {'color': '(1, 1, 1)', 'radius': '1.0', 'intensity': '1.0'}, + 'math': {'input1': '(0, 0, 0)', 'operation': 'add'}, + 'spaz': { + 'character': 'Spaz', 'color': '(1, 1, 1)', 'highlight': '(1, 1, 1)', + 'materials': '[_factory.spaz_material, _shared.object_material, _shared.player_material]', + 'roller_materials': '[_factory.roller_material, _shared.player_material]', + 'punch_materials': '[_factory.punch_material, _shared.attack_material]', + 'pickup_materials': '[_factory.pickup_material, _shared.pickup_material]', + }, + 'bomb': {'fuse_length': '3.0'}, + 'combine': {'size': '3', 'input0': '0.0', 'input1': '0.0', 'input2': '0.0'}, + 'explosion': {'radius': '2.0', 'color': '(1, 1, 1)', 'big': 'False'}, + 'flag': {'color_texture': 'flagColor', 'color': '(1, 1, 1)', 'lightWeight': 'False'}, + 'flash': {'size': '1.0', 'color': '(1, 1, 1)'}, + 'image': {'texture': 'light', 'opacity': '1.0', 'color': '(1, 1, 1)', 'attach': 'center'}, + 'locator': {'shape': 'box', 'size': '(1, 1, 1)', 'color': '(1, 1, 1)', 'opacity': '1.0'}, + 'region': {'type': 'box', 'scale': '(1, 1, 1)'}, + 'scorch': {'size': '1.0', 'presence': '1.0', 'color': '(0, 0, 0)'}, + 'shield': {'radius': '1.0', 'color': '(1, 0.25, 0.25)'}, + 'sound': {'sound': 'ding', 'volume': '1.0', 'loop': 'False', 'positional': 'True'}, + 'terrain': {'mesh': 'thePad', 'color_texture': 'thePadColor', 'collision_mesh': 'thePadCollide', 'color': '(1, 1, 1)'}, + 'texture_sequence': {'rate': '30'}, + 'time_display': {'timemin': '0', 'timemax': '0', 'time1': '0', 'time2': '0'}, + 'anim_curve': {'times': '(0, 1000)', 'values': '(0.0, 1.0)', 'loop': 'False'} + } + + NODE_TYPES = [ + 'prop', 'text', 'light', 'math', 'spaz', 'bomb', 'combine', + 'explosion', 'flag', 'flash', 'image', 'locator', 'region', + 'scorch', 'shield', 'sound', 'terrain', 'texture_sequence', + 'time_display', 'anim_curve' + ] + + def make_node_window(s, edit=None, load=False): + x, y = s.window_pos + sx, sy = s.window_size + text_push = 15 + delay = 0.35 + data = edit and edit['data'] + + s.current_node_type = data.get('type', 'prop') if data else 'prop' + + init_pos = (0, 0, 0) + s.current_attrs = {} + + def to_friendly(k, v): + val = str(v).strip() + for prefix in ['bs.getmesh("', "bs.getmesh('", 'bs.gettexture("', "bs.gettexture('", 'bs.getsound("', "bs.getsound('"]: + if val.startswith(prefix): return val[len(prefix):-2] + if val.startswith('"') and val.endswith('"'): return val[1:-1] + if val.startswith("'") and val.endswith("'"): return val[1:-1] + return val + + def to_eval(k, v): + v = str(v).strip() + if k in ['mesh', 'collision_mesh', 'light_mesh', 'mesh_opaque', 'mesh_transparent']: return f'bs.getmesh("{v}")' + if k in ['color_texture', 'color_mask_texture', 'texture', 'tint_texture', 'mask_texture']: return f'bs.gettexture("{v}")' + if k == 'sound': return f'bs.getsound("{v}")' + if k in ['name', 'body', 'reflection', 'text', 'h_align', 'v_align', 'chunk_type', 'emit_type', 'operation', 'attach', 'shape', 'type', 'character', 'style', 'counter_text']: return f'"{v}"' + return v + + if data and 'attrs' in data: + for k, v in data['attrs'].items(): + if k == 'position': + try: init_pos = eval(v) if isinstance(v, str) else v + except: pass + else: + s.current_attrs[k] = to_friendly(k, v) + else: + for k, v in s.NODE_DEFAULT_ATTRS[s.current_node_type].items(): + s.current_attrs[k] = v + + left_x = 10 + + pos_lbl = (left_x, sy - 85) + w = bui.textwidget(parent=s.root, position=pos_lbl, size=(50, 30), text=Strings.TYPE, color=Const.INVISIBLE, maxwidth=50, h_align='left') + s.window_kids.append((w, pos_lbl, text_push, delay)) + + pos_inp = (left_x + 60, sy - 90) + type_btn = bui.buttonwidget( + parent=s.root, + position=pos_inp, + size=(0, 0), + label=s.current_node_type.capitalize(), + color=Color.BASE, + textcolor=Const.INVISIBLE, + texture=Eval.TEXTURE(Const.SKIN), + on_activate_call=lambda: open_type_picker(), + enable_sound=False + ) + s.window_kids.append((type_btn, pos_inp, text_push, delay, ('size', ((0, 35), (90, 35))))) + + type_btn_pos = (x + pos_inp[0], y + pos_inp[1]) + type_btn_shadow = bui.imagewidget( + parent=s.root, + opacity=0, + texture=Eval.TEXTURE(Const.SHADOW), + color=Color.SHADOW + ) + s.window_trash.append([type_btn_shadow]) + + def apply_type(new_type): + s.current_node_type = new_type + bui.buttonwidget(edit=type_btn, label=s.current_node_type.capitalize()) + + s.current_attrs.clear() + for k, v in s.NODE_DEFAULT_ATTRS[s.current_node_type].items(): + s.current_attrs[k] = v + refresh_right_pane(initial=False) + + def open_type_picker(): + if s.window_sub_on: + s.window_sub_on[2]() + Eval.SOUND(Const.OK_SOUND).play() + bui.buttonwidget( + type_btn, + on_activate_call=Const.DO_NOTHING, + selectable=False + ) + + wx, wy = s.window_pos + wsx, wsy = s.window_size + picker_x, picker_sx = 120, 130 + picker_pos = (wx + wsx + 100, wy) + picker_size = (picker_sx, wsy) + ( + picker_shadow_pos, + picker_shadow_size + ) = Eval.SHADOW(*picker_pos, *picker_size) + + btn_start_pos, btn_start_size = type_btn_pos, (90, 35) + butter = s.global_butter * 1.3 + + grow_anim = Animate( + widget=type_btn, + duration=butter, + attrs={ + 'position': (btn_start_pos, picker_pos), + 'size': (btn_start_size, picker_size), + 'textcolor': ( + (*Color.TEXT, Color.TEXT_OPACITY), + Const.INVISIBLE + ) + } + ) + shadow_anim = Animate( + widget=type_btn_shadow, + attrs={ + 'opacity': (0, Color.SHADOW_OPACITY), + 'position': (btn_start_pos, picker_shadow_pos), + 'size': (btn_start_size, picker_shadow_size) + }, + duration=butter + ) + + child_start_progress = 0.35 + child_delay = butter * child_start_progress + child_duration = butter * (1 - child_start_progress) + 0.05 + + picker_kids = [] + kid_anims = [] + + picker_scroll = bui.scrollwidget( + parent=s.root, + position=picker_pos, + size=(0, picker_size[1]), + color=Color.COLD, + border_opacity=0 + ) + picker_kids.append(picker_scroll) + kid_anims.append(Animate( + widget=picker_scroll, + duration=child_duration, + delay=child_delay, + attrs={ + 'size': ((0, picker_size[1]), picker_size), + 'border_opacity': (0, Color.OPACITY), + 'color': (Color.COLD, Color.BASE) + } + )) + + row_h = 35 + types = s.NODE_TYPES + picker_root = bui.containerwidget( + parent=picker_scroll, + background=False, + size=(picker_x, row_h * len(types)) + ) + picker_kids.append(picker_root) + + def close_sub(instant=False): + s.window_sub_on = None + for anim in kid_anims: anim.cancel() + kid_anims.clear() + for w in picker_kids: + if w.exists(): w.delete() + picker_kids.clear() + dur = instant and 0.0001 or butter + grow_anim.reverse(duration=dur) + shadow_anim.reverse(duration=dur) + bui.buttonwidget( + type_btn, + on_activate_call=open_type_picker, + selectable=True + ) + if not instant: + Eval.SOUND(Const.OK_SOUND).play() + + def pick(t): + Eval.SOUND(Const.OK_SOUND).play() + apply_type(t) + close_sub() + + row_off = 15 + for i, t in enumerate(types): + y_pos = row_h * (len(types) - 1 - i) + btn = bui.buttonwidget( + parent=picker_root, + position=(row_off, y_pos), + size=(picker_x, row_h), + label=t.capitalize(), + color=Color.BASE if t != s.current_node_type else Color.COLD, + textcolor=Const.INVISIBLE, + opacity=0, + texture=Eval.TEXTURE(Const.SKIN), + enable_sound=False, + on_activate_call=bui.CallPartial(pick, t) + ) + picker_kids.append(btn) + stagger = 0.02 * i + kid_anims.append(Animate( + widget=btn, + duration=child_duration, + delay=child_delay + stagger, + attrs={ + 'position': ((row_off, y_pos), (0, y_pos)), + 'opacity': (0, Color.OPACITY), + 'textcolor': ( + Const.INVISIBLE, + (*Color.TEXT, Color.TEXT_OPACITY) + ) + } + )) + + s.window_sub_on = (type_btn, open_type_picker, close_sub) + + pos_lbl = (left_x, sy - 130) + w = bui.textwidget(parent=s.root, position=pos_lbl, size=(50, 30), text=Strings.NAME, color=Const.INVISIBLE, maxwidth=50, h_align='left') + s.window_kids.append((w, pos_lbl, text_push, delay + 0.05)) + + pos_inp = (left_x + 60, sy - 130) + incr = f'Node {s.increment}' + name_text = bui.textwidget( + parent=s.root, + position=pos_inp, + editable=True, + allow_clear_button=False, + size=(0, 0), + maxwidth=80, + description=Strings.NAME, + color=Const.INVISIBLE, + v_align=Const.ALIGN, + glow_type=Const.GLOW, + text=data.get('name', incr) if data else incr + ) + s.window_kids.append((name_text, pos_inp, text_push, delay+0.05, ('size', ((0, 35), (90, 35))))) + + pos_sep = (left_x + 2, sy - 133) + w = bui.imagewidget( + parent=s.root, + position=pos_sep, + texture=Eval.TEXTURE(Const.SKIN), + size=(0, 0), + opacity=0, + color=Color.COLD + ) + s.window_kids.append((w, pos_sep, text_push, delay + 0.1, ('size', ((0, 2), (145, 2))))) + + s.pos_inputs = [] + labels = ['X', 'Y', 'Z'] + + for i, lbl in enumerate(labels): + lbl_y = sy - 165 - (i * 40) + inp_y = sy - 170 - (i * 40) + + pos_lbl = (left_x, lbl_y) + w = bui.textwidget(parent=s.root, position=pos_lbl, size=(50, 30), text=f'Pos {lbl}', color=Const.INVISIBLE, maxwidth=50, h_align='left') + s.window_kids.append((w, pos_lbl, text_push, delay + 0.15 + i*0.05)) + + pos_inp = (left_x + 60, inp_y) + inp = bui.textwidget( + parent=s.root, + position=pos_inp, + editable=True, + allow_clear_button=False, + size=(0, 0), + description=f'Pos {lbl}', + color=Const.INVISIBLE, + maxwidth=80, + v_align=Const.ALIGN, + glow_type=Const.GLOW, + text=str(init_pos[i] if len(init_pos) > i else 0) + ) + s.pos_inputs.append(inp) + s.window_kids.append((inp, pos_inp, text_push, delay+0.15+i*0.05, ('size', ((0, 35), (90, 35))))) + + scroll_x = 170 + scroll_y = 50 + scroll_w = sx - scroll_x - 10 + scroll_h = 195 + + attr_scroll = bui.scrollwidget( + parent=s.root, + position=(scroll_x, scroll_y), + color=Color.BASE, + size=(scroll_w/2, 0), + border_opacity=0 + ) + s.window_kids.append((attr_scroll, (scroll_x, scroll_y), 20, delay, + ('size', ((0, scroll_h), (scroll_w, scroll_h))), + ('border_opacity', (0, Color.OPACITY)), + ('color', (Color.COLD, Color.BASE)) + )) + + s.attr_root = bui.containerwidget(parent=attr_scroll, background=False) + + s.attr_widgets = [] + s.current_inputs = {} + + def anim_scroll_kid(w, px, py, dt): + if (anim := s.anims.get(id(w), {}).get('scroll')): anim.cancel() + ty = w.get_widget_type() + attrs = {'position': ((px + 50, py), (px, py))} + if ty == 'text': + attrs['color'] = (Const.INVISIBLE, (*Color.TEXT, Color.TEXT_OPACITY)) + elif ty == 'button': + attrs['opacity'] = (0, Color.OPACITY) + attrs['textcolor'] = (Const.INVISIBLE, (*Color.TEXT, Color.TEXT_OPACITY)) + s.anims[id(w)]['scroll'] = Animate(widget=w, attrs=attrs, duration=0.18, delay=dt) + + def sync_edits(): + for key, wid in s.current_inputs.items(): + s.current_attrs[key] = bui.textwidget(query=wid) + + def add_custom_attr(k_wid, v_wid): + sync_edits() + k = bui.textwidget(query=k_wid).strip() + v = bui.textwidget(query=v_wid).strip() + if not k: + s.toast(Format.ERROR_EMPTY(Strings.ATTR)) + Eval.SOUND(Const.BAD_SOUND).play() + return + + try: + with bs.get_foreground_host_activity().context: + _shared = None + _factory = None + if s.current_node_type == 'spaz': + from bascenev1lib.gameutils import SharedObjects + from bascenev1lib.actor.spazfactory import SpazFactory + _shared = SharedObjects.get() + _factory = SpazFactory.get() + eval(to_eval(k, v)) + except Exception as e: + s.toast(Format.ERROR(e)) + Eval.SOUND(Const.BAD_SOUND).play() + return + + s.current_attrs[k] = v + Eval.SOUND(Const.OK_SOUND).play() + refresh_right_pane() + + def delete_attr(k): + sync_edits() + if k in s.current_attrs: + del s.current_attrs[k] + Eval.SOUND(Const.OK_SOUND).play() + refresh_right_pane() + + def refresh_right_pane(initial=False): + for w in s.attr_widgets: w.delete() + s.attr_widgets.clear() + s.current_inputs.clear() + + row_h = 35 + num_attrs = len(s.current_attrs) + content_h = (num_attrs + 1) * row_h + + bui.containerwidget(edit=s.attr_root, size=(250, max(content_h, scroll_h))) + + keys = list(s.current_attrs.keys()) + for i, k in enumerate(keys): + y_pos = max(content_h, scroll_h) - (i + 1) * row_h + + lbl = bui.textwidget( + parent=s.attr_root, position=(0, y_pos), size=(85, row_h), + text=k, h_align='left', v_align='center', maxwidth=80, + color=Const.INVISIBLE if initial else (*Color.TEXT, Color.TEXT_OPACITY) + ) + s.attr_widgets.append(lbl) + + val = str(s.current_attrs[k]) + inp = bui.textwidget( + parent=s.attr_root, position=(90, y_pos+5), size=(120, row_h-5), + text=val, editable=True, h_align='left', v_align='center', + glow_type=Const.GLOW, allow_clear_button=False, + color=Const.INVISIBLE if initial else (*Color.TEXT, Color.TEXT_OPACITY) + ) + s.attr_widgets.append(inp) + s.current_inputs[k] = inp + + del_btn = bui.buttonwidget( + parent=s.attr_root, position=(215, y_pos+5), size=(30, row_h-5), + label='-', on_activate_call=bui.CallPartial(delete_attr, k), + button_type='square', enable_sound=False, + texture=Eval.TEXTURE(Const.SKIN), color=Color.BASE, + opacity=0 if initial else Color.OPACITY, + textcolor=Const.INVISIBLE if initial else (*Color.TEXT, Color.TEXT_OPACITY) + ) + s.attr_widgets.append(del_btn) + + if initial: + anim_scroll_kid(lbl, 0, y_pos, delay + 0.1 + i*0.03) + anim_scroll_kid(inp, 90, y_pos+5, delay + 0.1 + i*0.03) + anim_scroll_kid(del_btn, 215, y_pos+5, delay + 0.1 + i*0.03) + + y_pos = max(content_h, scroll_h) - (num_attrs + 1) * row_h + new_k = bui.textwidget( + parent=s.attr_root, position=(0, y_pos+5), size=(85, row_h-5), + text="new_attr", editable=True, h_align='left', v_align='center', + maxwidth=80, glow_type=Const.GLOW, allow_clear_button=False, + color=Const.INVISIBLE if initial else (*Color.TEXT, Color.TEXT_OPACITY) + ) + s.attr_widgets.append(new_k) + + new_v = bui.textwidget( + parent=s.attr_root, position=(90, y_pos+5), size=(120, row_h-5), + text="value", editable=True, h_align='left', v_align='center', + glow_type=Const.GLOW, allow_clear_button=False, + color=Const.INVISIBLE if initial else (*Color.TEXT, Color.TEXT_OPACITY) + ) + s.attr_widgets.append(new_v) + + add_btn = bui.buttonwidget( + parent=s.attr_root, position=(215, y_pos+5), size=(30, row_h-5), + label='+', on_activate_call=lambda: add_custom_attr(new_k, new_v), + button_type='square', enable_sound=False, + texture=Eval.TEXTURE(Const.SKIN), color=Color.BASE, + opacity=0 if initial else Color.OPACITY, + textcolor=Const.INVISIBLE if initial else (*Color.TEXT, Color.TEXT_OPACITY) + ) + s.attr_widgets.append(add_btn) + + if initial: + anim_scroll_kid(new_k, 0, y_pos+5, delay + 0.1 + num_attrs*0.03) + anim_scroll_kid(new_v, 90, y_pos+5, delay + 0.1 + num_attrs*0.03) + anim_scroll_kid(add_btn, 215, y_pos+5, delay + 0.1 + num_attrs*0.03) + + refresh_right_pane(initial=True) + + prv_on = False + prv_node = None + + def do_preview(): + nonlocal prv_on, prv_node + if prv_on: + prv_on = False + bui.buttonwidget(prv_btn, label=Strings.PREVIEW) + if prv_node and prv_node.exists(): + prv_node.delete() + prv_node = None + Eval.SOUND(Const.OK_SOUND).play() + return + + try: + pos_tuple = ( + float(bui.textwidget(query=s.pos_inputs[0]) or '0'), + float(bui.textwidget(query=s.pos_inputs[1]) or '0'), + float(bui.textwidget(query=s.pos_inputs[2]) or '0') + ) + except Exception: + s.toast("Invalid Position!") + Eval.SOUND(Const.BAD_SOUND).play() + return + + sync_edits() + try: + with bs.get_foreground_host_activity().context: + _shared = None + _factory = None + if s.current_node_type == 'spaz': + from bascenev1lib.gameutils import SharedObjects + from bascenev1lib.actor.spazfactory import SpazFactory + _shared = SharedObjects.get() + _factory = SpazFactory.get() + kw = {} if s.current_node_type == 'spaz' else {'position': pos_tuple} + for key, val in s.current_attrs.items(): + if val: + kw[key] = eval(to_eval(key, val)) + if s.current_node_type == 'spaz': + from bascenev1lib.actor.spaz import Spaz + for extraneous in ('materials','roller_materials','punch_materials','pickup_materials','style'): + kw.pop(extraneous, None) + prv_actor = Spaz( + character=kw.pop('character', None) or 'Spaz', + color=kw.pop('color', (1, 1, 1)), + highlight=kw.pop('highlight', (1, 1, 1)), + start_invincible=False + ).autoretain() + for k, v in kw.items(): + setattr(prv_actor.node, k, v) + prv_node = prv_actor.node + bs.timer( + 0, + lambda a=prv_actor: a.node.exists() and a.handlemessage(bs.StandMessage(pos_tuple, 0.0)) + ) + else: + prv_node = bs.newnode(s.current_node_type, attrs=kw) + except Exception as e: + s.toast(Format.ERROR(e)) + Eval.SOUND(Const.BAD_SOUND).play() + return + + Eval.SOUND(Const.OK_SOUND).play() + bui.buttonwidget(prv_btn, label=Strings.STOP) + prv_on = True + + pos = (left_x - 3, s.window_marg) + size = (145, 40) + prv_btn = bui.buttonwidget( + parent=s.root, + size=(0, 0), + position=pos, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + enable_sound=False, + label=Strings.PREVIEW, + textcolor=Const.INVISIBLE, + on_activate_call=do_preview + ) + s.window_kids.append((prv_btn, pos, 50, delay + 0.1, ('size', ((0, size[1]), size)))) + + def do_done(): + nam = bui.textwidget(query=name_text) + if not nam: + s.toast(Format.ERROR_EMPTY(Strings.NAME)) + Eval.SOUND(Const.BAD_SOUND).play() + return + + try: + pos_tuple = ( + float(bui.textwidget(query=s.pos_inputs[0]) or '0'), + float(bui.textwidget(query=s.pos_inputs[1]) or '0'), + float(bui.textwidget(query=s.pos_inputs[2]) or '0') + ) + except Exception: + s.toast("Invalid Position!") + Eval.SOUND(Const.BAD_SOUND).play() + return + + sync_edits() + final_attrs = {'position': str(pos_tuple)} + + try: + with bs.get_foreground_host_activity().context: + _shared = None + _factory = None + if s.current_node_type == 'spaz': + from bascenev1lib.gameutils import SharedObjects + from bascenev1lib.actor.spazfactory import SpazFactory + _shared = SharedObjects.get() + _factory = SpazFactory.get() + for key, val in s.current_attrs.items(): + if val: + eval_str = to_eval(key, val) + eval(eval_str) + final_attrs[key] = eval_str + except Exception as e: + s.toast(Format.ERROR(e)) + Eval.SOUND(Const.BAD_SOUND).play() + return + + final_data = { + 'type': s.current_node_type, + 'name': nam, + 'attrs': final_attrs + } + + Eval.SOUND(Const.OK_SOUND).play() + if edit and not load: + data.update(final_data) + bui.buttonwidget(s.stamp_kids[edit['order']], label=nam) + s.dismiss_window() + s.toast(Strings.INFO_SAVED) + else: + s.add_entry(final_data) + s.increment += 1 + + done_pos = (sx - 105, s.window_marg) + done_btn = bui.buttonwidget( + parent=s.root, + size=(0, 0), + position=done_pos, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + enable_sound=False, + label=Strings.DONE, + textcolor=Const.INVISIBLE, + on_activate_call=do_done + ) + s.window_kids.append((done_btn, done_pos, 50, delay+0.1, ('size', ((0, 40), (95, 40))))) + + s.window_trash = [s.attr_widgets] + + return lambda: prv_node and prv_node.exists() and prv_node.delete() + + def make_camera_window(s,edit=None,load=False): + x,y = s.window_pos + x += 1 + sx,sy = s.window_size + bx,by = sx/3-s.window_marg*4,40 + text_push = 15 + delay = 0.35 + off = s.window_marg*5+bx + yoff = by+s.window_marg + data = edit and edit['data'] + + prv_on = False + chks = [True,True,False] + last_pos = _ba.get_camera_position() + last_tar = _ba.get_camera_target() + old_pos = list(last_pos) + old_tar = list(last_tar) + virgin = True + current_manual = False + + prev_pos_node = None + prev_tar_node = None + + def do_chk(i,v): + chks[i] = v + + pos = (s.window_marg-3,by+s.window_marg*5) + pos_chk = bui.checkboxwidget( + parent=s.root, + text=Strings.CAMERA_POSITION_CHECK, + position=pos, + color=Color.BASE, + textcolor=Const.INVISIBLE, + scale=0, + maxwidth=bx-s.window_marg, + value=chks[0], + on_value_change_call=bui.CallPartial(do_chk,0) + ) + s.window_kids.append((pos_chk,pos,text_push,delay+0.12, + ('size',((bx/2,by),(bx,by))), + ('scale',(0,1)) + )) + + pos_texts = [] + top = yoff*4.5 + for i,o in enumerate(old_pos): + pos = (s.window_marg-3,top-yoff*i) + w = bui.textwidget( + parent=s.root, + position=pos, + editable=True, + allow_clear_button=False, + color=Const.INVISIBLE, + size=(0,0), + text=str(round(o,2)) + ) + pos_texts.append(w) + s.window_kids.append((w,pos,text_push,delay+(0.24-0.06*i), + ('size',((bx/2,by),(bx,by))) + )) + + pos = (off+s.window_marg*2-2,by+s.window_marg*5) + tar_chk = bui.checkboxwidget( + parent=s.root, + text=Strings.CAMERA_TARGET_CHECK, + position=pos, + color=Color.BASE, + textcolor=Const.INVISIBLE, + scale=0, + maxwidth=bx-s.window_marg, + value=chks[1], + on_value_change_call=bui.CallPartial(do_chk,1) + ) + s.window_kids.append((tar_chk,pos,text_push,delay+0.12, + ('size',((bx/2,by),(bx,by))), + ('scale',(0,1)) + )) + + target_texts = [] + for i,o in enumerate(old_tar): + pos = (off+s.window_marg-3,top-yoff*i) + w = bui.textwidget( + parent=s.root, + position=pos, + editable=True, + allow_clear_button=False, + color=Const.INVISIBLE, + size=(0,0), + text=str(round(o,2)) + ) + target_texts.append(w) + s.window_kids.append((w,pos,text_push,delay+(0.24-0.06*i), + ('size',((bx/2,by),(bx,by))) + )) + + pos = (off*2+s.window_marg*2-2,by+s.window_marg*5) + man_chk = bui.checkboxwidget( + parent=s.root, + text=Strings.CAMERA_MANUAL_CHECK, + position=pos, + color=Color.BASE, + textcolor=Const.INVISIBLE, + scale=0, + maxwidth=bx-s.window_marg, + value=chks[2], + on_value_change_call=bui.CallPartial(do_chk,2) + ) + s.window_kids.append((man_chk,pos,text_push,delay+0.12, + ('size',((bx/2,by),(bx,by))), + ('scale',(0,1)) + )) + + def collect_pos(): + nonlocal last_pos + last_pos = [float(bui.textwidget(query=w) or '0') for w in pos_texts] + + def collect_tar(): + nonlocal last_tar + last_tar = [float(bui.textwidget(query=w) or '0') for w in target_texts] + + def collect(): + collect_pos() + collect_tar() + + def kill_prev(): + nonlocal prev_pos_node, prev_tar_node + if prev_pos_node and prev_pos_node.exists(): + prev_pos_node.delete() + prev_pos_node = None + if prev_tar_node and prev_tar_node.exists(): + prev_tar_node.delete() + prev_tar_node = None + + def do_see(): + nonlocal prev_pos_node, prev_tar_node, current_manual + + if chks[2] != current_manual: + _ba.set_camera_manual(chks[2]) + current_manual = chks[2] + + if chks[0]: + collect_pos() + _ba.set_camera_position(*last_pos) + + if prev_pos_node and prev_pos_node.exists(): + prev_pos_node.text = bui.charstr(getattr(bui.SpecialChar,Const.PIN_POINT)) + ' ' + Strings.POSITION + prev_pos_node.position = last_pos + else: + with bs.get_foreground_host_activity().context: + prev_pos_node = bs.newnode( + 'text', + attrs={ + 'text':bui.charstr(getattr(bui.SpecialChar,Const.PIN_POINT)) + ' ' + Strings.POSITION, + 'position':last_pos, + 'in_world':True, + 'scale':0.01, + 'flatness':1, + 'color':Color.WARM, + 'shadow':1.0 + } + ) + + if chks[1]: + collect_tar() + _ba.set_camera_target(*last_tar) + + if prev_tar_node and prev_tar_node.exists(): + prev_tar_node.text = bui.charstr(getattr(bui.SpecialChar,Const.PIN_POINT)) + ' ' + Strings.TARGET + prev_tar_node.position = last_tar + else: + with bs.get_foreground_host_activity().context: + prev_tar_node = bs.newnode( + 'text', + attrs={ + 'text':bui.charstr(getattr(bui.SpecialChar,Const.PIN_POINT)) + ' ' + Strings.TARGET, + 'position':last_tar, + 'in_world':True, + 'scale':0.01, + 'flatness':1, + 'color':Color.COLD, + 'shadow':1.0 + } + ) + + see_timer = None + def start_preview(): + nonlocal see_timer, prv_on, virgin, current_manual + prv_on = True + virgin = False + current_manual = chks[2] + if chks[2]: + _ba.set_camera_manual(True) + see_timer = bui.AppTimer(0.02, do_see, repeat=True) + + def stop_preview(): + nonlocal see_timer, prv_on, current_manual + prv_on = False + see_timer = None + kill_prev() + + if current_manual: + _ba.set_camera_manual(False) + current_manual = False + + def enforce(): + for w,d in zip(pos_texts,last_pos): + bui.textwidget(w,text=str(d)) + for w,d in zip(target_texts,last_tar): + bui.textwidget(w,text=str(d)) + for w,b in zip((pos_chk,tar_chk,man_chk),chks): + bui.checkboxwidget(w,value=b) + + mod = 0 + stp = 1 + def add(*d): + for i,w in enumerate(mod and target_texts or pos_texts): + old = bui.textwidget(query=w) or '0' + z = round(float(old)+d[i]*stp,2) + bui.textwidget(w,text=str(z)) + collect() + + def action(n): + nonlocal mod + if n == 0: add(0,0,1) + if n == 1: add(-1,0,0) + if n == 2: + mod = 0 + s.toast(Strings.INFO_POSITION_MODE) + if n == 3: add(0,-1,0) + if n == 4: pass + if n == 5: add(0,1,0) + if n == 6: add(0,0,-1) + if n == 7: add(1,0,0) + if n == 8: + mod = 1 + s.toast(Strings.INFO_TARGET_MODE) + Eval.SOUND(Const.OK_SOUND).play() + + for i in range(3): + for j in range(3): + n = i*3+j + pos = (sx-(by+s.window_marg)*(3-i),(by+s.window_marg+1)*(2.5+j)) + t = Const.CAMERA_TOOLS[n] + t = len(t) > 1 and Eval.CHAR(t) or t + w = bui.buttonwidget( + parent=s.root, + position=pos, + size=(0,0), + opacity=0, + label=t, + color=Color.BASE, + textcolor=Const.INVISIBLE, + texture=Eval.TEXTURE(Const.SKIN), + enable_sound=False, + repeat=True, + on_activate_call=bui.CallPartial(action,n) + ) + s.window_kids.append((w,pos,text_push,delay+0.15+0.02*n, + ('size',((by/2,by),(by,by))) + )) + + pos = (s.window_marg-3,by+s.window_marg*4) + size = (sx-s.window_marg,2) + w = bui.imagewidget( + parent=s.root, + position=pos, + texture=Eval.TEXTURE(Const.SKIN), + size=(0,0), + opacity=0, + color=Color.COLD + ) + s.window_kids.append((w,pos,text_push,delay+0.1, + ('size',((0,size[1]),size)) + )) + + def do_reset(shut=0): + Eval.SOUND(Const.OK_SOUND).play() + shut or s.toast(Strings.INFO_RESETTED) + + if prv_on: + do_preview(1) + else: + nonlocal virgin + virgin = True + kill_prev() + + if not shut: + for i in range(3): + bui.textwidget(pos_texts[i],text=str(round(old_pos[i],2))) + bui.textwidget(target_texts[i],text=str(round(old_tar[i],2))) + + pos = (s.window_marg,s.window_marg) + w = bui.buttonwidget( + parent=s.root, + size=(0,0), + position=pos, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + enable_sound=False, + label=Strings.CAMERA_RESET_BUTTON, + textcolor=Const.INVISIBLE, + on_activate_call=do_reset + ) + s.window_kids.append((w,pos,50,delay+0.23, + ('size',((bx/2,by),(bx,by))) + )) + + def do_preview(shut=0): + nonlocal prv_on + prv_on = not prv_on + bui.buttonwidget( + prv_button, + label=prv_on and Strings.STOP or Strings.PREVIEW + ) + shut or ( + s.toast(prv_on and Strings.INFO_PREVIEW_ON or Strings.PREVIEW_OFF) or + Eval.SOUND(Const.OK_SOUND).play() + ) + if prv_on: + start_preview() + else: + stop_preview() + + pos = (s.window_marg+off,s.window_marg) + prv_button = bui.buttonwidget( + parent=s.root, + size=(0,0), + position=pos, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + enable_sound=False, + label=Strings.PREVIEW, + textcolor=Const.INVISIBLE, + on_activate_call=do_preview + ) + s.window_kids.append((prv_button,pos,50,delay+0.23, + ('size',((bx/2,by),(bx,by))) + )) + + def do_done(): + collect() + nam = Strings.CAMERA_ENTRY + final = { + 'chks':chks, + 'name':nam, + 'position':last_pos, + 'target':last_tar + } + Eval.SOUND(Const.OK_SOUND).play() + if edit and not load: + data.update(final) + s.dismiss_window() + s.toast(Strings.INFO_SAVED) + else: + s.add_entry(final) + + pos = (s.window_marg+off*2,s.window_marg) + w = bui.buttonwidget( + parent=s.root, + size=(0,0), + position=pos, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + enable_sound=False, + label=Strings.DONE, + textcolor=Const.INVISIBLE, + on_activate_call=do_done + ) + s.window_kids.append((w,pos,50,delay+0.23, + ('size',((bx/2,by),(bx,by))) + )) + + if edit: + last_pos = data['position'] + last_tar = data['target'] + chks = data['chks'] + enforce() + + return lambda: (kill_prev(), stop_preview()) if not virgin else kill_prev() + + def make_sound_window(s, edit=None, load=False, wait=0.43): + s.prv_sound = None + if wait: + bui.apptimer( + wait, bui.CallPartial( + s.make_sound_window, + edit, wait=False + ) + ) + return (wait,lambda: s.prv_sound and s.prv_sound.delete()) + x, y = s.window_pos + sx, sy = s.window_size + text_push = 15 + delay = 0.35 + data = edit and edit['data'] + + sound_files = sorted(listdir(join(Const.BA_DATA,'audio')),reverse=True) + + current_sound = data and data['file'] or None + + def format_name(filename): + name = filename.replace('.ogg', '') + spaced = sub(r'([a-z])([A-Z])', r'\1 \2', name) + return spaced.title() + + def stop_current(): + if s.prv_sound: + s.prv_sound.delete() + + size = dx, dy = (sx/2 - s.window_marg*3, sy - s.window_marg*9 - 55) + pos = px, py = (s.window_marg - s.window_fix, s.window_marg - s.window_fix + 55) + sound_scroll = bui.scrollwidget( + parent=s.root, + position=pos, + color=Color.BASE, + size=(dx/2, 0), + border_opacity=0 + ) + s.window_kids.append((sound_scroll, pos, 20, delay + 0, + ('size', ((0, size[1]), size)), + ('border_opacity', (0, Color.OPACITY)), + ('color', (Color.COLD, Color.BASE)) + )) + + sound_root = bui.containerwidget( + parent=sound_scroll, + background=False + ) + + chks = data.get('chks',[False,False]) + def do_chk(i,v): + chks[i] = v + bx, by = 80, 40 + pos = (s.window_marg-8,s.window_marg) + pos_chk = bui.checkboxwidget( + parent=s.root, + text=Strings.EVERYWHERE, + position=pos, + color=Color.BASE, + textcolor=Const.INVISIBLE, + scale=0, + maxwidth=bx-s.window_marg, + value=chks[0], + on_value_change_call=bui.CallPartial(do_chk,0) + ) + s.window_kids.append((pos_chk,pos,text_push,delay+0.12, + ('size',((bx/2,by),(bx,by))), + ('scale',(0,1)) + )) + + bx, by = 80, 40 + pos = (s.window_marg-3+bx+40,s.window_marg) + loop_chk = bui.checkboxwidget( + parent=s.root, + text=Strings.LOOP, + position=pos, + color=Color.BASE, + textcolor=Const.INVISIBLE, + scale=0, + maxwidth=bx/2, + value=chks[1], + on_value_change_call=bui.CallPartial(do_chk,1) + ) + s.window_kids.append((loop_chk,pos,text_push,delay+0.12, + ('size',((bx/2,by),(bx,by))), + ('scale',(0,1)) + )) + + text_y = 30 + sound_texts = [] + + for i, filename in enumerate(sound_files): + w = bui.textwidget( + parent=sound_root, + size=(dx, text_y), + position=(0, i * text_y), + maxwidth=dx - 15, + selectable=True, + glow_type=Const.GLOW, + click_activate=True, + text=filename, + color=Const.INVISIBLE, + v_align=Const.ALIGN + ) + sound_texts.append(w) + + bui.containerwidget( + sound_root, + size=(dx, max(len(sound_files) * text_y, dy - 15)) + ) + + title_pos = (sx/2 + s.window_marg, sy - s.window_marg - 80) + title_text = bui.textwidget( + parent=s.root, + position=title_pos, + text=current_sound and format_name(current_sound) or Strings.SOUND_PLACEHOLDER, + color=Const.INVISIBLE, + v_align=Const.ALIGN, + maxwidth=sx/2 - s.window_marg*2, + scale=1.2 + ) + s.window_kids.append((title_text, title_pos, text_push, delay + 0.1)) + + input_width = (sx/2 - s.window_marg*5) / 3 + 5 + input_height = 35 + input_y = sy - s.window_marg - 150 + labels = ['X', 'Y', 'Z'] + position_inputs = [] + + for idx, label in enumerate(labels): + label_pos = (sx/2 + s.window_marg + idx * (input_width + s.window_marg), input_y + input_height) + label_widget = bui.textwidget( + parent=s.root, + position=label_pos, + text=label, + color=Const.INVISIBLE, + scale=0.8 + ) + s.window_kids.append((label_widget, label_pos, text_push, delay + 0.12)) + + input_pos = (sx/2 + s.window_marg + idx * (input_width + s.window_marg) - 5, input_y) + input_widget = bui.textwidget( + parent=s.root, + position=input_pos, + editable=True, + allow_clear_button=False, + color=Const.INVISIBLE, + size=(0, 0), + text=data and str(data.get(label.lower(), 0)) or '0', + glow_type=Const.GLOW, + v_align=Const.ALIGN + ) + position_inputs.append(input_widget) + s.window_kids.append((input_widget, input_pos, text_push, delay + 0.13, + ('size', ((input_width/2, input_height), (input_width, input_height))) + )) + + vol_label_pos = (sx/2, input_y - 36) + vol_label = bui.textwidget( + parent=s.root, + position=vol_label_pos, + text='Volume', + color=Const.INVISIBLE, + scale=0.8 + ) + s.window_kids.append((vol_label, vol_label_pos, text_push, delay + 0.15)) + + vol_input_pos = (sx/2 + s.window_marg + 90, input_y - 40) + volume_input = bui.textwidget( + parent=s.root, + position=vol_input_pos, + editable=True, + allow_clear_button=False, + color=Const.INVISIBLE, + size=(0, 0), + text=data and str(data.get('volume', 1.0)) or '1.0', + glow_type=Const.GLOW, + v_align=Const.ALIGN + ) + s.window_kids.append((volume_input, vol_input_pos, text_push, delay + 0.16, + ('size', ((input_width/2, input_height), (input_width * 1.8, input_height))) + )) + + def select_sound(filename): + nonlocal current_sound + stop_current() + current_sound = filename + bui.textwidget(title_text, text=format_name(filename)) + + all_delay = (delay-0.35) + 0.05 + len(sound_files) * 0.01 + for i, (filename, w) in enumerate(zip(sound_files, sound_texts)): + bui.textwidget( + w, + on_activate_call=bui.CallPartial(select_sound, filename) + ) + butter = s.global_butter + s.anims[id(w)] = Animate( + widget=w, + attrs={ + 'color': ( + Const.INVISIBLE, + (*Color.TEXT, Color.TEXT_OPACITY) + ), + 'position': ( + (50, i * text_y), + (0, i * text_y) + ) + }, + duration=butter, + delay=all_delay - i * 0.01 + ) + + s.window_trash = [sound_texts] + + button_x = sx - (dx + s.window_marg*2) + button_y = s.window_marg + + def do_play(): + if not current_sound: + s.toast(Strings.ERROR_NO_SOUND_SELECTED) + Eval.SOUND(Const.BAD_SOUND).play() + return + stop_current() + sound_name = current_sound.replace('.ogg', '') + try: pos = collect_pos() + except: + s.toast(Strings.ERROR_INVALID.format(Strings.POSITION)) + Eval.SOUND(Const.BAD_SOUND).play() + return + try: + vol = float(bui.textwidget(query=volume_input) or '1.0') + except Exception as e: + Eval.SOUND(Const.BAD_SOUND).play() + Format.ERROR(e) + return + with bs.get_foreground_host_activity().context: + s.prv_sound = bs.newnode( + 'sound', + attrs={ + 'sound':bs.getsound(sound_name), + 'position':pos, + 'volume':vol, + 'positional':not chks[0], + 'loop':chks[1] + } + ) + + play_pos = (button_x-5, button_y*3+by) + play_button = bui.buttonwidget( + parent=s.root, + size=(0, 0), + position=play_pos, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + enable_sound=False, + label=Eval.CHAR(Const.PLAY_BUTTON), + textcolor=Const.INVISIBLE, + on_activate_call=do_play + ) + s.window_kids.append((play_button, play_pos, 50, delay + 0.15, + ('size', ((0, by), (dx/2 - s.window_marg/2, by))) + )) + + def do_stop(): + stop_current() + + stop_pos = (button_x+dx/2+s.window_marg*2, button_y*3+by) + stop_button = bui.buttonwidget( + parent=s.root, + size=(0, 0), + position=stop_pos, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + enable_sound=False, + label=Eval.CHAR(Const.PAUSE_BUTTON), + textcolor=Const.INVISIBLE, + on_activate_call=do_stop + ) + s.window_kids.append((stop_button, stop_pos, 50, delay + 0.15, + ('size', ((0, by), (dx/2 - s.window_marg/2+2, by))) + )) + def collect_pos(): + return [ + float(bui.textwidget(query=i) or '0') + for i in position_inputs + ] + def do_done(): + if not current_sound: + s.toast(Strings.ERROR_NO_SOUND_SELECTED) + Eval.SOUND(Const.BAD_SOUND).play() + return + + stop_current() + + try: + x_val,y_val,z_val = collect_pos() + except ValueError: + s.toast(Strings.ERROR_INVALID.format(Strings.POSITION)) + Eval.SOUND(Const.BAD_SOUND).play() + return + try: + vol_val = float(bui.textwidget(query=volume_input) or '1.0') + except Exception as e: + Eval.SOUND(Const.BAD_SOUND).play() + Format.ERROR(e) + return + + Eval.SOUND(Const.OK_SOUND).play() + + final = { + 'name': format_name(current_sound), + 'file': current_sound, + 'x': x_val, + 'y': y_val, + 'z': z_val, + 'volume': vol_val, + 'chks':chks.copy(), + } + + if edit and not load: + data.update(final) + bui.buttonwidget( + s.stamp_kids[edit['order']], + label=final['name'] + ) + s.dismiss_window() + s.toast(Strings.INFO_SAVED) + else: + s.add_entry(final) + + done_pos = (sx - (dx + s.window_marg*2), button_y) + done_button = bui.buttonwidget( + parent=s.root, + size=(0, 0), + position=done_pos, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + enable_sound=False, + label=Strings.DONE, + textcolor=Const.INVISIBLE, + on_activate_call=do_done + ) + s.window_kids.append((done_button, done_pos, 50, delay + 0.2, + ('size', ((0, by), (sx/2 - s.window_marg*2, by))) + )) + + FX_TYPES = ['spark', 'impact', 'sticky', 'ice', 'custom'] + FX_DEFAULT_ATTRS = { + 'spark': { + 'count': '20', 'scale': '1.0', 'spread': '0.6', + 'chunk_type': 'spark', + }, + 'impact': { + 'count': '8', 'scale': '0.8', 'chunk_type': 'metal', + }, + 'sticky': { + 'count': '15', 'scale': '0.6', 'chunk_type': 'slime', + 'emit_type': 'stickers', + }, + 'ice': { + 'count': '20', 'scale': '0.5', 'spread': '0.2', + 'chunk_type': 'ice', + }, + 'custom': { + 'count': '20', 'emit_type': 'distortion', + }, + } + + def make_fx_window(s, edit=None, load=False): + x, y = s.window_pos + sx, sy = s.window_size + text_push = 15 + delay = 0.35 + data = edit and edit['data'] + + VECTOR_KEYS = ('position',) + + def guess_type(attrs): + ct = attrs.get('chunk_type') + et = attrs.get('emit_type') + if ct == 'spark' and et != 'stickers': return 'spark' + if ct == 'metal': return 'impact' + if et == 'stickers': return 'sticky' + if ct == 'ice': return 'ice' + return 'custom' + + s.current_fx_type = guess_type(data['attrs']) if data else 'spark' + + init_pos = (0, 0, 0) + s.current_fx_attrs = {} + + def to_friendly(k, v): + val = str(v).strip() + if val.startswith('"') and val.endswith('"'): return val[1:-1] + if val.startswith("'") and val.endswith("'"): return val[1:-1] + return val + + def to_eval(k, v): + v = str(v).strip() + if k in ('chunk_type', 'emit_type', 'tendril_type'): + return f'"{v}"' + return v + + if data and 'attrs' in data: + for k, v in data['attrs'].items(): + if k == 'position': + try: init_pos = eval(v) if isinstance(v, str) else v + except Exception: pass + else: + s.current_fx_attrs[k] = to_friendly(k, v) + else: + for k, v in s.FX_DEFAULT_ATTRS[s.current_fx_type].items(): + s.current_fx_attrs[k] = v + + left_x = 10 + + pos_lbl = (left_x, sy - 85) + w = bui.textwidget(parent=s.root, position=pos_lbl, size=(50, 30), text=Strings.TYPE, color=Const.INVISIBLE, maxwidth=50, h_align='left') + s.window_kids.append((w, pos_lbl, text_push, delay)) + + pos_inp = (left_x + 60, sy - 90) + type_btn = bui.buttonwidget( + parent=s.root, + position=pos_inp, + size=(0, 0), + label=s.current_fx_type.capitalize(), + color=Color.BASE, + textcolor=Const.INVISIBLE, + texture=Eval.TEXTURE(Const.SKIN), + on_activate_call=lambda: open_type_picker(), + enable_sound=False + ) + s.window_kids.append((type_btn, pos_inp, text_push, delay, ('size', ((0, 35), (90, 35))))) + + type_btn_pos = (x + pos_inp[0], y + pos_inp[1]) + type_btn_shadow = bui.imagewidget( + parent=s.root, + opacity=0, + texture=Eval.TEXTURE(Const.SHADOW), + color=Color.SHADOW + ) + s.window_trash.append([type_btn_shadow]) + + def apply_type(new_type): + s.current_fx_type = new_type + bui.buttonwidget(edit=type_btn, label=s.current_fx_type.capitalize()) + + s.current_fx_attrs.clear() + for k, v in s.FX_DEFAULT_ATTRS[s.current_fx_type].items(): + s.current_fx_attrs[k] = v + refresh_right_pane(initial=False) + + def open_type_picker(): + if s.window_sub_on: + s.window_sub_on[2]() + Eval.SOUND(Const.OK_SOUND).play() + bui.buttonwidget( + type_btn, + on_activate_call=Const.DO_NOTHING, + selectable=False + ) + + wx, wy = s.window_pos + wsx, wsy = s.window_size + picker_x, picker_sx = 120, 130 + picker_pos = (wx + wsx + 100, wy) + picker_size = (picker_sx, wsy) + ( + picker_shadow_pos, + picker_shadow_size + ) = Eval.SHADOW(*picker_pos, *picker_size) + + btn_start_pos, btn_start_size = type_btn_pos, (90, 35) + butter = s.global_butter * 1.3 + + grow_anim = Animate( + widget=type_btn, + duration=butter, + attrs={ + 'position': (btn_start_pos, picker_pos), + 'size': (btn_start_size, picker_size), + 'textcolor': ( + (*Color.TEXT, Color.TEXT_OPACITY), + Const.INVISIBLE + ) + } + ) + shadow_anim = Animate( + widget=type_btn_shadow, + attrs={ + 'opacity': (0, Color.SHADOW_OPACITY), + 'position': (btn_start_pos, picker_shadow_pos), + 'size': (btn_start_size, picker_shadow_size) + }, + duration=butter + ) + + child_start_progress = 0.35 + child_delay = butter * child_start_progress + child_duration = butter * (1 - child_start_progress) + 0.05 + + picker_kids = [] + kid_anims = [] + + picker_scroll = bui.scrollwidget( + parent=s.root, + position=picker_pos, + size=(0, picker_size[1]), + color=Color.COLD, + border_opacity=0 + ) + picker_kids.append(picker_scroll) + kid_anims.append(Animate( + widget=picker_scroll, + duration=child_duration, + delay=child_delay, + attrs={ + 'size': ((0, picker_size[1]), picker_size), + 'border_opacity': (0, Color.OPACITY), + 'color': (Color.COLD, Color.BASE) + } + )) + + row_h = 35 + types = s.FX_TYPES + picker_root = bui.containerwidget( + parent=picker_scroll, + background=False, + size=(picker_x, row_h * len(types)) + ) + picker_kids.append(picker_root) + + def close_sub(instant=False): + s.window_sub_on = None + for anim in kid_anims: anim.cancel() + kid_anims.clear() + for w in picker_kids: + if w.exists(): w.delete() + picker_kids.clear() + dur = instant and 0.0001 or butter + grow_anim.reverse(duration=dur) + shadow_anim.reverse(duration=dur) + bui.buttonwidget( + type_btn, + on_activate_call=open_type_picker, + selectable=True + ) + if not instant: + Eval.SOUND(Const.OK_SOUND).play() + + def pick(t): + Eval.SOUND(Const.OK_SOUND).play() + apply_type(t) + close_sub() + + row_off = 15 + for i, t in enumerate(types): + y_pos = row_h * (len(types) - 1 - i) + btn = bui.buttonwidget( + parent=picker_root, + position=(row_off, y_pos), + size=(picker_x, row_h), + label=t.capitalize(), + color=Color.BASE if t != s.current_fx_type else Color.COLD, + textcolor=Const.INVISIBLE, + opacity=0, + texture=Eval.TEXTURE(Const.SKIN), + enable_sound=False, + on_activate_call=bui.CallPartial(pick, t) + ) + picker_kids.append(btn) + stagger = 0.02 * i + kid_anims.append(Animate( + widget=btn, + duration=child_duration, + delay=child_delay + stagger, + attrs={ + 'position': ((row_off, y_pos), (0, y_pos)), + 'opacity': (0, Color.OPACITY), + 'textcolor': ( + Const.INVISIBLE, + (*Color.TEXT, Color.TEXT_OPACITY) + ) + } + )) + + s.window_sub_on = (type_btn, open_type_picker, close_sub) + + pos_lbl = (left_x, sy - 130) + w = bui.textwidget(parent=s.root, position=pos_lbl, size=(50, 30), text=Strings.NAME, color=Const.INVISIBLE, maxwidth=50, h_align='left') + s.window_kids.append((w, pos_lbl, text_push, delay + 0.05)) + + pos_inp = (left_x + 60, sy - 130) + name_text = bui.textwidget( + parent=s.root, + position=pos_inp, + editable=True, + allow_clear_button=False, + size=(0, 0), + maxwidth=80, + description=Strings.FX_NAME_HELP, + color=Const.INVISIBLE, + v_align=Const.ALIGN, + glow_type=Const.GLOW, + text=(data and data['name'] or list(Strings.EVENTS)[3]) + ) + s.window_kids.append((name_text, pos_inp, text_push, delay + 0.05, ('size', ((0, 35), (90, 35))))) + + pos_sep = (left_x + 2, sy - 133) + w = bui.imagewidget( + parent=s.root, + position=pos_sep, + texture=Eval.TEXTURE(Const.SKIN), + size=(0, 0), + opacity=0, + color=Color.COLD + ) + s.window_kids.append((w, pos_sep, text_push, delay + 0.1, ('size', ((0, 2), (145, 2))))) + + s.fx_pos_inputs = [] + labels = ['X', 'Y', 'Z'] + for i, lbl in enumerate(labels): + lbl_y = sy - 165 - (i * 40) + inp_y = sy - 170 - (i * 40) + + pos_lbl = (left_x, lbl_y) + w = bui.textwidget(parent=s.root, position=pos_lbl, size=(50, 30), text=f'Pos {lbl}', color=Const.INVISIBLE, maxwidth=50, h_align='left') + s.window_kids.append((w, pos_lbl, text_push, delay + 0.15 + i * 0.05)) + + pos_inp = (left_x + 60, inp_y) + inp = bui.textwidget( + parent=s.root, + position=pos_inp, + editable=True, + allow_clear_button=False, + size=(0, 0), + description=f'Pos {lbl}', + color=Const.INVISIBLE, + maxwidth=80, + v_align=Const.ALIGN, + glow_type=Const.GLOW, + text=str(init_pos[i] if len(init_pos) > i else 0) + ) + s.fx_pos_inputs.append(inp) + s.window_kids.append((inp, pos_inp, text_push, delay + 0.15 + i * 0.05, ('size', ((0, 35), (90, 35))))) + + + scroll_x = 170 + scroll_y = 50 + scroll_w = sx - scroll_x - 10 + scroll_h = 195 + + attr_scroll = bui.scrollwidget( + parent=s.root, + position=(scroll_x, scroll_y), + color=Color.BASE, + size=(scroll_w / 2, 0), + border_opacity=0 + ) + s.window_kids.append((attr_scroll, (scroll_x, scroll_y), 20, delay, + ('size', ((0, scroll_h), (scroll_w, scroll_h))), + ('border_opacity', (0, Color.OPACITY)), + ('color', (Color.COLD, Color.BASE)) + )) + + s.fx_attr_root = bui.containerwidget(parent=attr_scroll, background=False) + + s.fx_attr_widgets = [] + s.fx_current_inputs = {} + + def anim_scroll_kid(w, px, py, dt): + if (anim := s.anims.get(id(w), {}).get('scroll')): anim.cancel() + ty = w.get_widget_type() + attrs = {'position': ((px + 50, py), (px, py))} + if ty == 'text': + attrs['color'] = (Const.INVISIBLE, (*Color.TEXT, Color.TEXT_OPACITY)) + elif ty == 'button': + attrs['opacity'] = (0, Color.OPACITY) + attrs['textcolor'] = (Const.INVISIBLE, (*Color.TEXT, Color.TEXT_OPACITY)) + s.anims[id(w)]['scroll'] = Animate(widget=w, attrs=attrs, duration=0.18, delay=dt) + + def sync_edits(): + for key, wid in s.fx_current_inputs.items(): + s.current_fx_attrs[key] = bui.textwidget(query=wid) + + def add_custom_attr(k_wid, v_wid): + sync_edits() + k = bui.textwidget(query=k_wid).strip() + v = bui.textwidget(query=v_wid).strip() + if not k: + s.toast(Format.ERROR_EMPTY(Strings.ATTR)) + Eval.SOUND(Const.BAD_SOUND).play() + return + if k in VECTOR_KEYS: + s.toast(Format.ERROR(f'Use the {k.capitalize()} fields on the left')) + Eval.SOUND(Const.BAD_SOUND).play() + return + + try: + with bs.get_foreground_host_activity().context: + eval(to_eval(k, v)) + except Exception as e: + s.toast(Format.ERROR(e)) + Eval.SOUND(Const.BAD_SOUND).play() + return + + s.current_fx_attrs[k] = v + Eval.SOUND(Const.OK_SOUND).play() + refresh_right_pane() + + def delete_attr(k): + sync_edits() + if k in s.current_fx_attrs: + del s.current_fx_attrs[k] + Eval.SOUND(Const.OK_SOUND).play() + refresh_right_pane() + + def refresh_right_pane(initial=False): + for w in s.fx_attr_widgets: w.delete() + s.fx_attr_widgets.clear() + s.fx_current_inputs.clear() + + row_h = 35 + num_attrs = len(s.current_fx_attrs) + content_h = (num_attrs + 1) * row_h + + bui.containerwidget(edit=s.fx_attr_root, size=(250, max(content_h, scroll_h))) + + keys = list(s.current_fx_attrs.keys()) + for i, k in enumerate(keys): + y_pos = max(content_h, scroll_h) - (i + 1) * row_h + + lbl = bui.textwidget( + parent=s.fx_attr_root, position=(0, y_pos), size=(85, row_h), + text=k, h_align='left', v_align='center', maxwidth=80, + color=Const.INVISIBLE if initial else (*Color.TEXT, Color.TEXT_OPACITY) + ) + s.fx_attr_widgets.append(lbl) + + val = str(s.current_fx_attrs[k]) + inp = bui.textwidget( + parent=s.fx_attr_root, position=(90, y_pos + 5), size=(120, row_h - 5), + text=val, editable=True, h_align='left', v_align='center', + maxwidth=100, glow_type=Const.GLOW, allow_clear_button=False, + color=Const.INVISIBLE if initial else (*Color.TEXT, Color.TEXT_OPACITY) + ) + s.fx_attr_widgets.append(inp) + s.fx_current_inputs[k] = inp + + del_btn = bui.buttonwidget( + parent=s.fx_attr_root, position=(215, y_pos + 5), size=(30, row_h - 5), + label='-', on_activate_call=bui.CallPartial(delete_attr, k), + button_type='square', enable_sound=False, + texture=Eval.TEXTURE(Const.SKIN), color=Color.BASE, + opacity=0 if initial else Color.OPACITY, + textcolor=Const.INVISIBLE if initial else (*Color.TEXT, Color.TEXT_OPACITY) + ) + s.fx_attr_widgets.append(del_btn) + + if initial: + anim_scroll_kid(lbl, 0, y_pos, delay + 0.1 + i * 0.03) + anim_scroll_kid(inp, 90, y_pos + 5, delay + 0.1 + i * 0.03) + anim_scroll_kid(del_btn, 215, y_pos + 5, delay + 0.1 + i * 0.03) + + y_pos = max(content_h, scroll_h) - (num_attrs + 1) * row_h + new_k = bui.textwidget( + parent=s.fx_attr_root, position=(0, y_pos + 5), size=(85, row_h - 5), + text="new_attr", editable=True, h_align='left', v_align='center', + maxwidth=80, glow_type=Const.GLOW, allow_clear_button=False, + color=Const.INVISIBLE if initial else (*Color.TEXT, Color.TEXT_OPACITY) + ) + s.fx_attr_widgets.append(new_k) + + new_v = bui.textwidget( + parent=s.fx_attr_root, position=(90, y_pos + 5), size=(120, row_h - 5), + text="value", editable=True, h_align='left', v_align='center', + maxwidth=100, glow_type=Const.GLOW, allow_clear_button=False, + color=Const.INVISIBLE if initial else (*Color.TEXT, Color.TEXT_OPACITY) + ) + s.fx_attr_widgets.append(new_v) + + add_btn = bui.buttonwidget( + parent=s.fx_attr_root, position=(215, y_pos + 5), size=(30, row_h - 5), + label='+', on_activate_call=lambda: add_custom_attr(new_k, new_v), + button_type='square', enable_sound=False, + texture=Eval.TEXTURE(Const.SKIN), color=Color.BASE, + opacity=0 if initial else Color.OPACITY, + textcolor=Const.INVISIBLE if initial else (*Color.TEXT, Color.TEXT_OPACITY) + ) + s.fx_attr_widgets.append(add_btn) + + if initial: + anim_scroll_kid(new_k, 0, y_pos + 5, delay + 0.1 + num_attrs * 0.03) + anim_scroll_kid(new_v, 90, y_pos + 5, delay + 0.1 + num_attrs * 0.03) + anim_scroll_kid(add_btn, 215, y_pos + 5, delay + 0.1 + num_attrs * 0.03) + + refresh_right_pane(initial=True) + + def collect_final_attrs(): + sync_edits() + pos_tuple = ( + float(bui.textwidget(query=s.fx_pos_inputs[0]) or '0'), + float(bui.textwidget(query=s.fx_pos_inputs[1]) or '0'), + float(bui.textwidget(query=s.fx_pos_inputs[2]) or '0') + ) + final_attrs = {'position': str(pos_tuple)} + for key, val in s.current_fx_attrs.items(): + if val: + final_attrs[key] = to_eval(key, val) + return final_attrs + + prv_on = False + + def do_prv(): + raw_attrs = collect_final_attrs() + try: + with bs.get_foreground_host_activity().context: + kw = {k: eval(v) for k, v in raw_attrs.items()} + bs.emitfx(**kw) + except Exception as e: + if prv_on: do_preview() + s.toast(Format.ERROR(e)) + Eval.SOUND(Const.BAD_SOUND).play() + + def do_preview(): + nonlocal prv_on + if prv_on: + prv_on = False + bui.buttonwidget(prv_btn, label=Strings.PREVIEW) + s.prv_fx = None + return + s.prv_fx = bui.AppTimer(0.1, do_prv, repeat=True) + bui.buttonwidget(prv_btn, label=Strings.STOP) + prv_on = True + + pos = (left_x - 3, s.window_marg) + size = (145, 40) + prv_btn = bui.buttonwidget( + parent=s.root, + size=(0, 0), + position=pos, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + enable_sound=False, + label=Strings.PREVIEW, + textcolor=Const.INVISIBLE, + on_activate_call=do_preview + ) + s.window_kids.append((prv_btn, pos, 50, delay + 0.1, ('size', ((0, size[1]), size)))) + + def do_done(): + nam = bui.textwidget(query=name_text) + if not nam: + s.toast(Format.ERROR_EMPTY(Strings.NAME)) + Eval.SOUND(Const.BAD_SOUND).play() + return + + try: + final_attrs = collect_final_attrs() + with bs.get_foreground_host_activity().context: + for key, val in final_attrs.items(): + eval(val) + except Exception as e: + s.toast(Format.ERROR(e)) + Eval.SOUND(Const.BAD_SOUND).play() + return + + final_data = {'name': nam, 'attrs': final_attrs} + + Eval.SOUND(Const.OK_SOUND).play() + if edit and not load: + data.update(final_data) + bui.buttonwidget(s.stamp_kids[edit['order']], label=nam) + s.dismiss_window() + s.toast(Strings.INFO_SAVED) + else: + s.add_entry(final_data) + + done_pos = (sx - 105, s.window_marg) + done_btn = bui.buttonwidget( + parent=s.root, + size=(0, 0), + position=done_pos, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + enable_sound=False, + label=Strings.DONE, + textcolor=Const.INVISIBLE, + on_activate_call=do_done + ) + s.window_kids.append((done_btn, done_pos, 50, delay + 0.1, ('size', ((0, 40), (95, 40))))) + + s.window_trash = [s.fx_attr_widgets] + return lambda: setattr(s, 'prv_fx', None) + + def error(s,e): + Eval.SOUND(Const.BAD_SOUND).play() + s.toast(Format.ERROR(e)) + + MAP_GNODE_DEFAULTS = { + 'Hockey Stadium': { + 'floor_reflection': True, 'debris_friction': 0.3, 'debris_kill_height': -0.3, + 'tint': (1.2, 1.3, 1.33), 'ambient_color': (1.15, 1.25, 1.6), + 'vignette_outer': (0.66, 0.67, 0.73), 'vignette_inner': (0.93, 0.93, 0.95), + 'vr_camera_offset': (0, -0.8, -1.1), 'vr_near_clip': 0.5, + }, + 'Football Stadium': { + 'tint': (1.3, 1.2, 1.0), 'ambient_color': (1.3, 1.2, 1.0), + 'vignette_outer': (0.57, 0.57, 0.57), 'vignette_inner': (0.9, 0.9, 0.9), + 'vr_camera_offset': (0, -0.8, -1.1), 'vr_near_clip': 0.5, + }, + 'Bridgit': { + 'tint': (1.1, 1.2, 1.3), 'ambient_color': (1.1, 1.2, 1.3), + 'vignette_outer': (0.65, 0.6, 0.55), 'vignette_inner': (0.9, 0.9, 0.93), + }, + 'Big G': { + 'tint': (0.75, 0.8, 0.85), 'ambient_color': (0.75, 0.8, 0.85), + 'vignette_outer': (0.5, 0.48, 0.45), 'vignette_inner': (0.85, 0.85, 0.88), + }, + 'Roundabout': { + 'tint': (1.0, 1.05, 1.1), 'ambient_color': (1.0, 1.05, 1.1), 'shadow_ortho': True, + 'vignette_outer': (0.63, 0.65, 0.7), 'vignette_inner': (0.97, 0.95, 0.93), + }, + 'Monkey Face': { + 'tint': (1.1, 1.2, 1.2), 'ambient_color': (1.2, 1.3, 1.3), + 'vignette_outer': (0.60, 0.62, 0.66), 'vignette_inner': (0.97, 0.95, 0.93), + 'vr_camera_offset': (-1.4, 0, 0), + }, + 'Zigzag': { + 'tint': (1.0, 1.15, 1.15), 'ambient_color': (1.0, 1.15, 1.15), + 'vignette_outer': (0.57, 0.59, 0.63), 'vignette_inner': (0.97, 0.95, 0.93), + 'vr_camera_offset': (-1.5, 0, 0), + }, + 'The Pad': { + 'tint': (1.1, 1.1, 1.0), 'ambient_color': (1.1, 1.1, 1.0), + 'vignette_outer': (0.7, 0.65, 0.75), 'vignette_inner': (0.95, 0.95, 0.93), + }, + 'Doom Shroom': { + 'tint': (0.82, 1.10, 1.15), 'ambient_color': (0.9, 1.3, 1.1), 'shadow_ortho': False, + 'vignette_outer': (0.76, 0.76, 0.76), 'vignette_inner': (0.95, 0.95, 0.99), + }, + 'Lake Frigid': { + 'tint': (1, 1, 1), 'ambient_color': (1, 1, 1), 'shadow_ortho': True, + 'vignette_outer': (0.86, 0.86, 0.86), 'vignette_inner': (0.95, 0.95, 0.99), + 'vr_near_clip': 0.5, + }, + 'Tip Top': { + 'tint': (0.8, 0.9, 1.3), 'ambient_color': (0.8, 0.9, 1.3), + 'vignette_outer': (0.79, 0.79, 0.69), 'vignette_inner': (0.97, 0.97, 0.99), + }, + 'Crag Castle': { + 'shadow_ortho': True, 'shadow_offset': (0, 0, -5.0), + 'tint': (1.15, 1.05, 0.75), 'ambient_color': (1.15, 1.05, 0.75), + 'vignette_outer': (0.6, 0.65, 0.6), 'vignette_inner': (0.95, 0.95, 0.95), + 'vr_near_clip': 1.0, + }, + 'Tower D': { + 'tint': (1.15, 1.11, 1.03), 'ambient_color': (1.2, 1.1, 1.0), + 'vignette_outer': (0.7, 0.73, 0.7), 'vignette_inner': (0.95, 0.95, 0.95), + }, + 'Happy Thoughts': { + 'happy_thoughts_mode': True, 'shadow_offset': (0.0, 8.0, 5.0), + 'tint': (1.3, 1.23, 1.0), 'ambient_color': (1.3, 1.23, 1.0), + 'vignette_outer': (0.64, 0.59, 0.69), 'vignette_inner': (0.95, 0.95, 0.93), + 'vr_near_clip': 1.0, + }, + 'Step Right Up': { + 'tint': (1.2, 1.1, 1.0), 'ambient_color': (1.2, 1.1, 1.0), + 'vignette_outer': (0.7, 0.65, 0.75), 'vignette_inner': (0.95, 0.95, 0.93), + }, + 'Courtyard': { + 'tint': (1.2, 1.17, 1.1), 'ambient_color': (1.2, 1.17, 1.1), + 'vignette_outer': (0.6, 0.6, 0.64), 'vignette_inner': (0.95, 0.95, 0.93), + }, + 'Rampage': { + 'tint': (1.2, 1.1, 0.97), 'ambient_color': (1.3, 1.2, 1.03), + 'vignette_outer': (0.62, 0.64, 0.69), 'vignette_inner': (0.97, 0.95, 0.93), + }, + } + + def change_map(s, ma, extra={}): + from bascenev1lib.gameutils import SharedObjects + _act = bs.get_foreground_host_activity() + old_map = _act.map + cls = bs.get_map_class(ma) + with _act.context: + if type(old_map) in _act.preloads: + del _act.preloads[type(old_map)] + for attr in dir(old_map): + if not attr.startswith('_') and attr not in ['node', 'activity', 'getactivity', 'handlemessage', 'on_expire', 'autoretain', 'is_alive', 'exists']: + try: + val = getattr(old_map, attr) + if not callable(val): + if hasattr(val, 'delete') and hasattr(val, 'exists'): + try: + if val.exists(): val.delete() + except Exception: + pass + delattr(old_map, attr) + except Exception: + pass + _act.preloads[cls] = cls.on_preload() + preload = _act.preloads[cls] + shared = SharedObjects.get() + for node in bs.getnodes(): + if node.getnodetype() == 'terrain' and node != old_map.node: + try: + node.delete() + except Exception: + pass + for attr in ['bottom', 'floor', 'stands', 'background', 'railing', 'bg_collide', 'stem', 'player_wall', 'bg2', 'node_bottom']: + if hasattr(old_map, attr): + delattr(old_map, attr) + if hasattr(old_map, 'node') and old_map.node: + old_map.node.mesh = preload.get('mesh') or preload.get('mesh_top') or preload.get('meshes', [None])[0] + old_map.node.color_texture = preload['tex'] + old_map.node.collision_mesh = preload['collision_mesh'] + if 'ice_material' in preload: + old_map.node.materials = [shared.footing_material, preload['ice_material']] + else: + old_map.node.materials = [shared.footing_material] + if 'mesh_bottom' in preload or 'bottom_mesh' in preload: + old_map.bottom = bs.newnode('terrain', attrs={'mesh': preload.get('mesh_bottom') or preload.get('bottom_mesh'), 'lighting': False, 'color_texture': preload['tex']}) + if 'meshes' in preload and len(preload['meshes']) > 1: + mats = [shared.footing_material, preload['ice_material']] if 'ice_material' in preload else [shared.footing_material] + old_map.floor = bs.newnode('terrain', attrs={'mesh': preload['meshes'][1], 'color_texture': preload['tex'], 'opacity': 0.92, 'opacity_in_low_or_medium_quality': 1.0, 'materials': mats}) + if 'meshes' in preload and len(preload['meshes']) > 2: + old_map.stands = bs.newnode('terrain', attrs={'mesh': preload['meshes'][2], 'visible_in_reflections': False, 'color_texture': preload.get('stands_tex', preload['tex'])}) + if 'mesh_bg' in preload or 'bgmesh' in preload: + old_map.background = bs.newnode('terrain', attrs={'mesh': preload.get('mesh_bg') or preload.get('bgmesh'), 'lighting': False, 'background': True, 'color_texture': preload.get('mesh_bg_tex') or preload.get('bgtex')}) + if 'bgmesh2' in preload: + old_map.bg2 = bs.newnode('terrain', attrs={'mesh': preload['bgmesh2'], 'lighting': False, 'background': True, 'color_texture': preload.get('bgtex2', preload.get('tex'))}) + if 'railing_collision_mesh' in preload or 'bumper_collision_mesh' in preload: + old_map.railing = bs.newnode('terrain', attrs={'collision_mesh': preload.get('railing_collision_mesh') or preload.get('bumper_collision_mesh'), 'materials': [shared.railing_material], 'bumper': True}) + if 'collide_bg' in preload: + old_map.bg_collide = bs.newnode('terrain', attrs={'collision_mesh': preload['collide_bg'], 'materials': [shared.footing_material, preload.get('bg_material'), shared.death_material]}) + if 'stem_mesh' in preload: + old_map.stem = bs.newnode('terrain', attrs={'mesh': preload['stem_mesh'], 'lighting': False, 'color_texture': preload['tex']}) + if 'player_wall_collision_mesh' in preload and isinstance(bs.getsession(), bs.CoopSession): + old_map.player_wall = bs.newnode('terrain', attrs={ + 'collision_mesh': preload['player_wall_collision_mesh'], + 'affect_bg_dynamics': False, + 'materials': [preload['player_wall_material']], + }) + old_map.preloaddata = preload + old_map.defs = cls.defs + old_map.is_hockey = hasattr(cls, 'is_hockey') or cls.name in ['Hockey Stadium', 'Lake Frigid'] + old_map.is_flying = cls.name == 'Happy Thoughts' + gnode = _act.globalsnode + aoi_bounds = old_map.get_def_bound_box('area_of_interest_bounds') or (-1, -1, -1, 1, 1, 1) + gnode.area_of_interest_bounds = aoi_bounds + map_bounds = old_map.get_def_bound_box('map_bounds') or (-30, -10, -30, 30, 100, 30) + bs.set_map_bounds(map_bounds) + if not hasattr(s, '_gnode_engine_defaults'): + s._gnode_engine_defaults = {} + all_gnode_keys = set() + for d in s.MAP_GNODE_DEFAULTS.values(): + all_gnode_keys.update(d.keys()) + for key in all_gnode_keys: + try: + s._gnode_engine_defaults[key] = getattr(gnode, key) + except Exception: + pass + for key, default_val in s._gnode_engine_defaults.items(): + try: + setattr(gnode, key, default_val) + except Exception: + pass + for attr, val in s.MAP_GNODE_DEFAULTS.get(cls.name, {}).items(): + try: + setattr(gnode, attr, val) + except Exception: + pass + for key, val in extra.items(): + try: + v = eval(val) + if hasattr(old_map, key): + setattr(old_map, key, v) + elif hasattr(gnode, key): + setattr(gnode, key, v) + elif old_map.node and hasattr(old_map.node, key): + setattr(old_map.node, key, v) + else: + raise AttributeError( + f"'{key}' is not a valid attribute on the map, " + f"globals, or map node" + ) + except Exception as e: + bui.pushcall( + bui.CallPartial( + s.error, e + ), raw=True + ) + + def make_map_window(s, edit=None, load=False): + x, y = s.window_pos + sx, sy = s.window_size + text_push = 15 + delay = 0.35 + data = edit and edit['data'] + _act = bs.get_foreground_host_activity() + + assert bui.app.classic is not None + map_names = sorted(bui.app.classic.maps.keys()) + + old_ma = s.original_map + try: + start_ma = bs.get_filtered_map_name(data['map'] if data else old_ma) + except Exception: + start_ma = old_ma + if start_ma not in map_names and map_names: + start_ma = map_names[0] + s.current_map_name = start_ma + + s.current_map_attrs = {} + if data and 'attrs' in data: + s.current_map_attrs = {k: str(v) for k, v in data['attrs'].items()} + else: + start_cls = bs.get_map_class(start_ma) + s.current_map_attrs = { + k: str(v) for k, v in s.MAP_GNODE_DEFAULTS.get(start_cls.name, {}).items() + } + + left_x = 10 + + pos_lbl = (left_x, sy - 85) + w = bui.textwidget(parent=s.root, position=pos_lbl, size=(50, 30), text=list(Strings.EVENTS)[4], color=Const.INVISIBLE, maxwidth=140, h_align='left') + s.window_kids.append((w, pos_lbl, text_push, delay)) + + list_x = left_x + list_y = 50 + list_w = 150 + list_h = sy - 145 + + map_scroll = bui.scrollwidget( + parent=s.root, + position=(list_x, list_y), + color=Color.BASE, + size=(list_w / 2, 0), + border_opacity=0 + ) + s.window_kids.append((map_scroll, (list_x, list_y), 20, delay, + ('size', ((0, list_h), (list_w, list_h))), + ('border_opacity', (0, Color.OPACITY)), + ('color', (Color.COLD, Color.BASE)) + )) + s.map_list_root = bui.containerwidget(parent=map_scroll, background=False) + + row_h = 32 + s.map_list_buttons = {} + prv_state = {'on': False} + + def select_map(name): + Eval.SOUND(Const.OK_SOUND).play() + s.current_map_name = name + for nm, btn in s.map_list_buttons.items(): + bui.buttonwidget(edit=btn, color=Color.BASE if nm != name else Color.COLD) + + cls = bs.get_map_class(name) + s.current_map_attrs = { + k: str(v) for k, v in s.MAP_GNODE_DEFAULTS.get(cls.name, {}).items() + } + refresh_right_pane(initial=False) + if prv_state['on']: + sync_edits() + try: + s.change_map(s.current_map_name, extra=s.current_map_attrs) + except Exception as e: + Eval.SOUND(Const.BAD_SOUND).play() + s.toast(Format.ERROR(e)) + + content_h = max(len(map_names) * row_h, list_h) + bui.containerwidget(edit=s.map_list_root, size=(list_w, content_h)) + for i, name in enumerate(map_names): + y_pos = content_h - (i + 1) * row_h + btn = bui.buttonwidget( + parent=s.map_list_root, + position=(0, y_pos), + size=(list_w - 10, row_h - 4), + label=name, + color=Color.COLD if name == s.current_map_name else Color.BASE, + textcolor=(*Color.TEXT, Color.TEXT_OPACITY), + texture=Eval.TEXTURE(Const.SKIN), + enable_sound=False, + opacity=Color.OPACITY, + on_activate_call=bui.CallPartial(select_map, name) + ) + s.map_list_buttons[name] = btn + + scroll_x = 170 + scroll_y = 50 + scroll_w = sx - scroll_x - 10 + scroll_h = 195 + + attr_scroll = bui.scrollwidget( + parent=s.root, + position=(scroll_x, scroll_y), + color=Color.BASE, + size=(scroll_w / 2, 0), + border_opacity=0 + ) + s.window_kids.append((attr_scroll, (scroll_x, scroll_y), 20, delay, + ('size', ((0, scroll_h), (scroll_w, scroll_h))), + ('border_opacity', (0, Color.OPACITY)), + ('color', (Color.COLD, Color.BASE)) + )) + s.map_attr_root = bui.containerwidget(parent=attr_scroll, background=False) + + s.map_attr_widgets = [] + s.map_current_inputs = {} + + def anim_scroll_kid(w, px, py, dt): + if (anim := s.anims.get(id(w), {}).get('scroll')): anim.cancel() + ty = w.get_widget_type() + attrs = {'position': ((px + 50, py), (px, py))} + if ty == 'text': + attrs['color'] = (Const.INVISIBLE, (*Color.TEXT, Color.TEXT_OPACITY)) + elif ty == 'button': + attrs['opacity'] = (0, Color.OPACITY) + attrs['textcolor'] = (Const.INVISIBLE, (*Color.TEXT, Color.TEXT_OPACITY)) + s.anims[id(w)]['scroll'] = Animate(widget=w, attrs=attrs, duration=0.18, delay=dt) + + def sync_edits(): + for key, wid in s.map_current_inputs.items(): + s.current_map_attrs[key] = bui.textwidget(query=wid) + + def add_custom_attr(k_wid, v_wid): + sync_edits() + k = bui.textwidget(query=k_wid).strip() + v = bui.textwidget(query=v_wid).strip() + if not k: + s.toast(Format.ERROR_EMPTY(Strings.ATTR)) + Eval.SOUND(Const.BAD_SOUND).play() + return + try: + with bs.get_foreground_host_activity().context: + eval(v) + except Exception as e: + s.toast(Format.ERROR(e)) + Eval.SOUND(Const.BAD_SOUND).play() + return + s.current_map_attrs[k] = v + Eval.SOUND(Const.OK_SOUND).play() + refresh_right_pane() + + def delete_attr(k): + sync_edits() + if k in s.current_map_attrs: + del s.current_map_attrs[k] + Eval.SOUND(Const.OK_SOUND).play() + refresh_right_pane() + + def refresh_right_pane(initial=False): + for w in s.map_attr_widgets: w.delete() + s.map_attr_widgets.clear() + s.map_current_inputs.clear() + + row_h2 = 35 + num_attrs = len(s.current_map_attrs) + content_h2 = (num_attrs + 1) * row_h2 + + bui.containerwidget(edit=s.map_attr_root, size=(250, max(content_h2, scroll_h))) + + keys = list(s.current_map_attrs.keys()) + for i, k in enumerate(keys): + y_pos = max(content_h2, scroll_h) - (i + 1) * row_h2 + + lbl = bui.textwidget( + parent=s.map_attr_root, position=(0, y_pos), size=(85, row_h2), + text=k, h_align='left', v_align='center', maxwidth=80, + color=Const.INVISIBLE if initial else (*Color.TEXT, Color.TEXT_OPACITY) + ) + s.map_attr_widgets.append(lbl) + + val = str(s.current_map_attrs[k]) + inp = bui.textwidget( + parent=s.map_attr_root, position=(90, y_pos + 5), size=(120, row_h2 - 5), + text=val, editable=True, h_align='left', v_align='center', + maxwidth=100, glow_type=Const.GLOW, allow_clear_button=False, + color=Const.INVISIBLE if initial else (*Color.TEXT, Color.TEXT_OPACITY) + ) + s.map_attr_widgets.append(inp) + s.map_current_inputs[k] = inp + + del_btn = bui.buttonwidget( + parent=s.map_attr_root, position=(215, y_pos + 5), size=(30, row_h2 - 5), + label='-', on_activate_call=bui.CallPartial(delete_attr, k), + button_type='square', enable_sound=False, + texture=Eval.TEXTURE(Const.SKIN), color=Color.BASE, + opacity=0 if initial else Color.OPACITY, + textcolor=Const.INVISIBLE if initial else (*Color.TEXT, Color.TEXT_OPACITY) + ) + s.map_attr_widgets.append(del_btn) + + if initial: + anim_scroll_kid(lbl, 0, y_pos, delay + 0.1 + i * 0.03) + anim_scroll_kid(inp, 90, y_pos + 5, delay + 0.1 + i * 0.03) + anim_scroll_kid(del_btn, 215, y_pos + 5, delay + 0.1 + i * 0.03) + + y_pos = max(content_h2, scroll_h) - (num_attrs + 1) * row_h2 + new_k = bui.textwidget( + parent=s.map_attr_root, position=(0, y_pos + 5), size=(85, row_h2 - 5), + text="new_attr", editable=True, h_align='left', v_align='center', + maxwidth=80, glow_type=Const.GLOW, allow_clear_button=False, + color=Const.INVISIBLE if initial else (*Color.TEXT, Color.TEXT_OPACITY) + ) + s.map_attr_widgets.append(new_k) + + new_v = bui.textwidget( + parent=s.map_attr_root, position=(90, y_pos + 5), size=(120, row_h2 - 5), + text="value", editable=True, h_align='left', v_align='center', + maxwidth=100, glow_type=Const.GLOW, allow_clear_button=False, + color=Const.INVISIBLE if initial else (*Color.TEXT, Color.TEXT_OPACITY) + ) + s.map_attr_widgets.append(new_v) + + add_btn = bui.buttonwidget( + parent=s.map_attr_root, position=(215, y_pos + 5), size=(30, row_h2 - 5), + label='+', on_activate_call=lambda: add_custom_attr(new_k, new_v), + button_type='square', enable_sound=False, + texture=Eval.TEXTURE(Const.SKIN), color=Color.BASE, + opacity=0 if initial else Color.OPACITY, + textcolor=Const.INVISIBLE if initial else (*Color.TEXT, Color.TEXT_OPACITY) + ) + s.map_attr_widgets.append(add_btn) + + if initial: + anim_scroll_kid(new_k, 0, y_pos + 5, delay + 0.1 + num_attrs * 0.03) + anim_scroll_kid(new_v, 90, y_pos + 5, delay + 0.1 + num_attrs * 0.03) + anim_scroll_kid(add_btn, 215, y_pos + 5, delay + 0.1 + num_attrs * 0.03) + + refresh_right_pane(initial=True) + + def do_preview(): + if prv_state['on']: + prv_state['on'] = False + bui.buttonwidget(prv_btn, label=Strings.PREVIEW) + s.change_map(old_ma) + Eval.SOUND(Const.OK_SOUND).play() + return + sync_edits() + try: + s.change_map(s.current_map_name, extra=s.current_map_attrs) + except Exception as e: + Eval.SOUND(Const.BAD_SOUND).play() + s.toast(Format.ERROR(e)) + return + Eval.SOUND(Const.OK_SOUND).play() + bui.buttonwidget(prv_btn, label=Strings.STOP) + prv_state['on'] = True + + pos = (left_x - 3, s.window_marg) + size = (145, 40) + prv_btn = bui.buttonwidget( + parent=s.root, + size=(0, 0), + position=pos, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + enable_sound=False, + label=Strings.PREVIEW, + textcolor=Const.INVISIBLE, + on_activate_call=do_preview + ) + s.window_kids.append((prv_btn, pos, 50, delay + 0.1, ('size', ((0, size[1]), size)))) + + def do_done(): + sync_edits() + Eval.SOUND(Const.OK_SOUND).play() + final_data = { + 'map': s.current_map_name, + 'attrs': dict(s.current_map_attrs), + 'name': list(Strings.EVENTS)[4] + } + if edit and not load: + data.update(final_data) + s.dismiss_window() + s.toast(Strings.INFO_SAVED) + else: + s.add_entry(final_data, smol=True) + + done_pos = (sx - 105, s.window_marg) + done_btn = bui.buttonwidget( + parent=s.root, + size=(0, 0), + position=done_pos, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + enable_sound=False, + label=Strings.DONE, + textcolor=Const.INVISIBLE, + on_activate_call=do_done + ) + s.window_kids.append((done_btn, done_pos, 50, delay + 0.1, ('size', ((0, 40), (95, 40))))) + + s.window_trash = [s.map_attr_widgets, s.map_list_buttons] + + return lambda: s.change_map(old_ma) + + def make_preset_window(s,edit=None,load=False): + x, y = s.window_pos + sx, sy = s.window_size + text_push = 15 + delay = 0.35 + + size = dx, dy = (sx/2 - s.window_marg*3, sy - s.window_marg*9) + pos = px, py = (s.window_marg - s.window_fix, s.window_marg - s.window_fix) + preset_scroll = bui.scrollwidget( + parent=s.root, + position=pos, + color=Color.BASE, + size=(dx/2, 0), + border_opacity=0 + ) + s.window_kids.append((preset_scroll, pos, 20, delay + 0, + ('size', ((0, size[1]), size)), + ('border_opacity', (0, Color.OPACITY)), + ('color', (Color.COLD, Color.BASE)) + )) + + preset_root = bui.containerwidget( + parent=preset_scroll, + background=False + ) + + sl = None + def do_select(i,j,nam,dsc,data): + nonlocal sl + sl = (j,data) + bui.textwidget( + title_text, + text=nam + ) + bui.textwidget( + desc_text, + text=dsc + ) + + text_y = 30 + preset_texts = [] + + presets = get_presets() + rsy = max(len(presets) * text_y, dy - 15) + for i,g in enumerate(presets,start=1): + j,nam,dsc,data = g + w = bui.textwidget( + parent=preset_root, + size=(dx, text_y), + position=(0, rsy - i * text_y), + maxwidth=dx - 25, + selectable=True, + glow_type=Const.GLOW, + click_activate=True, + text=nam, + color=(*Color.TEXT,Color.TEXT_OPACITY), + v_align=Const.ALIGN, + on_activate_call=bui.CallPartial( + do_select, i, j, nam, dsc, data + ) + ) + preset_texts.append(w) + + bui.containerwidget( + preset_root, + size=(dx, rsy) + ) + + title_pos = (sx/2 + s.window_marg, sy - s.window_marg - 80) + title_text = bui.textwidget( + parent=s.root, + position=title_pos, + text=Strings.PRESET_PLACEHOLDER, + color=Const.INVISIBLE, + maxwidth=sx/2 - s.window_marg*2, + scale=1.2 + ) + s.window_kids.append((title_text, title_pos, text_push, delay + 0.1)) + + desc_pos = (sx/2 + s.window_marg, sy - s.window_marg - 120) + desc_text = bui.textwidget( + parent=s.root, + position=desc_pos, + text=Strings.DESCRIPTION_HERE, + color=Const.INVISIBLE, + maxwidth=sx/2 - s.window_marg*2, + ) + s.window_kids.append((desc_text, desc_pos, text_push, delay + 0.1)) + + by = 40 + def do_load(): + Eval.SOUND(Const.OK_SOUND).play() + s.event_window(sl[0],edit=sl[1],load=True) + load_pos = (sx - (dx + s.window_marg*2), s.window_marg) + load_button = bui.buttonwidget( + parent=s.root, + size=(0, 0), + position=load_pos, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + enable_sound=False, + label=Strings.LOAD, + textcolor=Const.INVISIBLE, + on_activate_call=do_load + ) + s.window_kids.append((load_button, load_pos, 50, delay + 0.2, + ('size', ((0, by), (sx/2 - s.window_marg*2, by))) + )) + + s.window_trash = [preset_texts] + + def make_code_window(s,edit=None,load=False,on_done=None,initial_code=None): + x, y = s.window_pos + sx, sy = s.window_size + delay = 0.35 + by = 40 + data = edit and edit['data'] + + size = dx, dy = (sx+4, sy - s.window_marg*13-by) + pos = px, py = (s.window_marg - s.window_fix, s.window_marg*4 - s.window_fix + by) + code_scroll = bui.scrollwidget( + parent=s.root, + position=pos, + color=Color.BASE, + size=(dx/2, 0), + border_opacity=0 + ) + s.window_kids.append((code_scroll, pos, 20, delay + 0, + ('size', ((0, size[1]), size)), + ('border_opacity', (0, Color.OPACITY)), + ('color', (Color.COLD, Color.BASE)) + )) + + code_root = bui.containerwidget( + parent=code_scroll, + background=False + ) + + text_y = 30 + code_texts = [] + def make_text(**k): + code_texts.append( + bui.textwidget( + parent=code_root, + editable=True, + v_align=Const.ALIGN, + allow_clear_button=False, + size=(dx,text_y), + maxwidth=dx-20, + **k + ) + ) + sync_texts() + + def sync_texts(): + ry = len(code_texts)*text_y + for i,w in enumerate(code_texts,start=1): + bui.textwidget(w,position=(0,ry-i*text_y)) + bui.containerwidget(code_root,size=(dx,ry),visible_child=w) + + make_text() + + def code_spy(): + t1 = bui.textwidget(query=code_texts[-1]) + t2 = bui.textwidget(query=code_texts[-2]) if len(code_texts) > 1 else True + if t1: make_text() + if not t1 and not t2: + code_texts.pop(-1).delete() + sync_texts() + + start_spy = lambda: bui.AppTimer( + 0.02, code_spy, repeat=True + ) + code_timer = start_spy() + + def get_code(): + code = '\n'.join( + bui.textwidget( + query=t + ) + for t in code_texts + ) + if not code: + Eval.SOUND(Const.BAD_SOUND).play() + s.toast(Strings.ERROR_EMPTY_CODE) + return code + def load_code(code): + if not code: return + code = code.split('\n') + tail = code_texts[-1] + if not bui.textwidget(query=tail): + bui.textwidget(tail,text=code.pop(0)) + for l in code: + make_text(text=l) + running = False + runner = None + final = None + def do_btn(i,btn): + if i == 0: + if (code:=get_code()): + bui.clipboard_set_text(code) + Eval.SOUND(Const.GOOD_SOUND).play() + s.toast(Strings.INFO_COPIED) + if i == 1: + if not (t:=ba.clipboard_get_text()): + Eval.SOUND(Const.BAD_SOUND).play() + s.toast(Strings.INFO_NO_CLIPBOARD) + return + load_code(t) + Eval.SOUND(Const.ACTION_SOUND).play() + s.toast(Strings.INFO_PASTED) + if i == 2: + nonlocal running, runner + if running: + running = False + bui.buttonwidget(btn,label=Strings.RUN) + runner.on_end() + runner = None + Eval.SOUND(Const.OK_SOUND).play() + return + elif (code:=get_code()): + running = True + bui.buttonwidget(btn,label=Strings.STOP) + runner = CodeRunner( + on_error=lambda e:( + s.toast( + Format.ERROR(e) + ) + ) + ) + runner.on_start(code) + Eval.SOUND(Const.OK_SOUND).play() + if i == 3: + if not (code:=get_code()): return + runner and runner.on_end() + head = bui.textwidget(query=code_texts[0]) + na = Strings.CODE + if head.startswith(Const.CONFIG_HEAD): + na = head.split(Const.CONFIG_HEAD,1)[1] or na + nonlocal final + final = { + 'code':code, + 'name':na + } + if edit and not load: + data.update(final) + bui.buttonwidget( + s.stamp_kids[edit['order']], + label=na + ) + s.dismiss_window() + s.toast(Strings.INFO_SAVED) + elif not callable(on_done): s.add_entry(final) + else: + s.dismiss_window() + on_done(final) + for i,t in enumerate(( + Strings.COPY, + Strings.PASTE, + Strings.RUN, + Strings.DONE + )): + btn_pos = (s.window_marg - 4 + (sx/4 + s.window_marg)*i, s.window_marg) + btn = bui.buttonwidget( + parent=s.root, + size=(0, 0), + position=btn_pos, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + enable_sound=False, + label=t, + textcolor=Const.INVISIBLE + ) + bui.buttonwidget( + btn, + on_activate_call=bui.CallPartial( + do_btn, i, btn + ) + ) + s.window_kids.append((btn, btn_pos, 50, delay + i*0.1, + ('size', ((0, by), (sx/4 - s.window_marg*4, by))) + )) + + s.window_trash = [code_texts] + if data: + load_code(data.get('code')) + elif initial_code: + load_code(initial_code) + def cleanup(): + nonlocal code_timer + code_timer = None + runner and runner.on_end() + return cleanup + + def make_seed_window(s,edit=None,load=False): + x, y = s.window_pos + sx, sy = s.window_size + text_push = 15 + delay = 0.35 + data = edit and edit['data'] + bx = sx - s.window_marg*8 + by = 40 + + pos = (s.window_marg,sy-(by+s.window_marg*10)) + tip = bui.textwidget( + parent=s.root, + position=pos, + maxwidth=sx-s.window_marg*4, + max_height=sy-(by*2+s.window_marg*14), + text=Strings.SEED_TIP, + color=Const.INVISIBLE + ) + s.window_kids.append((tip,pos,text_push,delay+0)) + + pos = (s.window_marg*2+2,s.window_marg*4+by) + seed_inp = bui.textwidget( + parent=s.root, + position=pos, + editable=True, + allow_clear_button=False, + size=(0,0), + maxwidth=(bx+20)-s.window_marg*2, + description=Strings.SEED_HELP, + color=Const.INVISIBLE, + v_align=Const.ALIGN, + glow_type=Const.GLOW, + text=data and data['seed'] or '' + ) + s.window_kids.append((seed_inp,pos,text_push,delay+0, + ('size',((bx/2,by),(bx+20,by))) + )) + + def do_done(): + seed = bui.textwidget(query=seed_inp) + if not seed: + Eval.SOUND(Const.BAD_SOUND).play() + s.toast(Format.ERROR_EMPTY(Strings.SEED)) + return + final = { + 'seed':seed, + 'name':Strings.SEED + } + if edit: + data.update(final) + s.dismiss_window() + s.toast(Strings.INFO_SAVED) + else: s.add_entry(final) + done_pos = (s.window_marg*4, s.window_marg) + done_button = bui.buttonwidget( + parent=s.root, + size=(0, 0), + position=done_pos, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.BASE, + enable_sound=False, + label=Strings.DONE, + textcolor=Const.INVISIBLE, + on_activate_call=do_done + ) + s.window_kids.append((done_button, done_pos, 50, delay + 0.2, + ('size', ((bx/2, by), (bx, by))) + )) + + def window_clean(s): + for w,*_ in s.window_kids: + s.anims[id(w)].reverse( + duration=0.1, + on_finish=w.delete, + on_cancel=w.delete + ) + s.window_kids.clear() + for l in s.window_trash: + for w in ( + l if isinstance(l,list) + else l.values() + ): w.delete() + s.window_trash.clear() + if getattr(s,'forgive_prev_off',False): + s.forgive_prev_off = False + else: + getattr(s,'prev_off_wid',None) and s.prev_off_wid.delete() + + def dismiss_window(s,**kw): + """Fully closes whatever window is open, popup included, in + one call. window_back() on its own only peels off one layer - + if a popup is open it closes *that* and returns, leaving + window_on untouched, which is exactly the right feel for an + actual back-button press (first press dismisses the popup, + second press leaves the window). But every call site that + means to open something else next - a different window, a + select/delete action, toggling event view - wants the whole + trap gone unconditionally. Calling window_back() alone there + closes the popup but silently leaves window_on non-empty, so + the window itself never gets torn down even though nothing + appears to be covering it anymore. Route all of those through + here instead so a live popup can never survive an attempt to + open (or otherwise move past) a window.""" + if s.window_sub_on: + s.window_sub_on[2](instant=True) + if s.window_on: + s.window_back(**kw) + + def window_back(s,to=None,shadow_to=None,on_fix=None,wait=0,extra={},shadow_extra={},instant={},into_nothing=False,skip=False): + if s.window_sub_on: + s.window_sub_on[2]() + return + b,call,on_back = s.window_on + butter = s.global_butter*1.66 + anim = s.anims[id(b)]['window'] + s.window_on = () + callable(on_back) and on_back() + def enable(): + bui.buttonwidget( + b, + on_activate_call=call, + selectable=True + ) + Eval.SOUND(Const.OK_SOUND).play() + s.window_clean() + if to: + last_i = s.last_window_i + def fix(): + for _ in ['extra','to','shadow']: + anim = s.anims[id(b)].pop(_,None) + if not anim: continue + anim.cancel() + if s.event_on: + ox,oy = (s.ev_x, s.event_top - s.ev_mult * (last_i+1)) + anim = Animate( + widget=b, + duration=s.global_butter, + attrs={ + 'textcolor':( + Const.INVISIBLE, + (*Color.TEXT,Color.TEXT_OPACITY) + ), + 'opacity':(0,Color.OPACITY), + 'position':( + (ox-50,oy), + (ox,oy) + ), + } + ) + s.anims[id(b)]['fix'] = anim + enable() + bui.buttonwidget( + b, + size=s.event_kid_size, + opacity=0, + textcolor=Const.INVISIBLE, + label=list(Strings.EVENTS)[last_i], + text_scale=s.event_kid_ts + ) + bui.imagewidget( + s.event_kids[b]['shadow'], + opacity=0 + ) + if callable(on_fix): on_fix() + def do_anim(): + anim = Animate( + widget=b, + attrs=to(), + duration=butter, + on_finish=fix, + on_cancel=fix + ) + s.anims[id(b)]['to'] = anim + s.anims[id(b)]['shadow'] = Animate( + widget=s.event_kids[b]['shadow'], + + attrs=shadow_to(), + duration=butter + ) + if wait: + s.after_scroll_t = bui.AppTimer(wait,do_anim) + else: do_anim() + if extra: + def nevermind(): + s.after_scroll_t = None + anim.cancel() + fix() + s.anims[id(b)]['extra'] = Animate( + widget=b, + duration=wait, + attrs=extra, + on_cancel=nevermind + ) + if shadow_extra: + s.anims[id(b)]['shadow'] = Animate( + widget=s.event_kids[b]['shadow'], + duration=wait, + attrs=shadow_extra + ) + instant and bui.buttonwidget( + b, **instant + ) + else: + zero = 0.0001 + s.anims[id(b)]['window'] = anim.reverse( + duration=skip and zero or butter + ) + anim = s.anims[id(b)]['shadow'] + s.anims[id(b)]['shadow'] = anim.reverse( + duration=skip and zero or butter + ) + enable() + + def show_controls(s,up=False): + if s.controls_shown: return + s.controls_shown = True + if up: + dx,dy = s.control_size + end_size = (dx,0) + for i,b in enumerate(s.controls): + for a in s.anims[id(b)].values(): + a.cancel() + px,py = s.control_pos(i) + bui.buttonwidget( + b, + position=(px,py+dy), + size=end_size, + textcolor=Const.INVISIBLE, + opacity=0 + ) + attrs = { + 'size':( + end_size, + s.control_size + ), + 'textcolor':( + Const.INVISIBLE, + (*Color.TEXT,Color.TEXT_OPACITY) + ), + 'opacity':(0,Color.OPACITY), + 'position':( + (px,py+dy), + (px,py) + ) + } + s.anims[id(b)][up] = Animate( + widget=b, + duration=s.global_butter, + attrs=attrs + ) + else: + dx,dy = s.control_size + sx,sy = s.stamp_size + start_size = (dx,dy/4) + for i,b in enumerate(s.controls): + bui.buttonwidget( + b, position=s.control_pos(i) + ) + for a in s.anims[id(b)].values(): + a.cancel() + attrs = { + 'size':( + start_size, + s.control_size + ), + 'textcolor':( + Const.INVISIBLE, + (*Color.TEXT,Color.TEXT_OPACITY) + ), + 'opacity':(0,Color.OPACITY) + } + s.anims[id(b)][up] = Animate( + widget=b, + duration=s.global_butter, + attrs=attrs + ) + + @ui_safe + def hide_controls(s,up=False): + if not s.controls_shown: return + s.controls_shown = False + if up: + dx,dy = s.control_size + sx,sy = s.stamp_size + end_size = (dx,0) + for i,b in enumerate(s.controls): + for a in s.anims[id(b)].values(): + a.cancel() + px,py = s.control_pos(i) + attrs = { + 'size':( + s.control_size, + end_size + ), + 'textcolor':( + (*Color.TEXT,Color.TEXT_OPACITY), + Const.INVISIBLE + ), + 'opacity':(Color.OPACITY,0), + 'position':( + (px,py), + (px,py+dy) + ) + } + s.anims[id(b)][up] = Animate( + widget=b, + duration=s.global_butter, + attrs=attrs + ) + else: + for b in s.controls: + s.anims[id(b)][up].reverse() + + def do_control(s,i): + if ( + not s.ui_on or + s.tools_shown or + not s.controls_shown + ): return + r = None + if i == 0: s.toggle_play() + if i == 1: r = s.stop() + Eval.SOUND( + r is None and Const.OK_SOUND + or Const.BAD_SOUND + ).play() + + def toggle_play(s): + if s.playing: s.pause() + else: s.play() + s.toast( + s.playing + and Strings.INFO_PLAYING + or Strings.INFO_PAUSED + ) + s.wrap_controls() + + def wrap_controls(s): + bui.buttonwidget( + s.controls[0], + label=Eval.CHAR( + Const.CONTROLS[0][s.playing] + ) + ) + + def pause(s): + s.playing = False + s.pause_start = perf_counter() + s.freeze_scene() + for sound in s.active_sounds: + sound.volume = 0 + + def freeze_scene(s,b=True): + bs.get_foreground_host_activity().globalsnode.paused = b + + def stop(s,shut=0): + if not s.play_timer: + s.toast(Strings.ERROR_NOT_PLAYING) + return False + s.playing = False + s.play_timer = None + s.ui_clickable = True + s.kill_playhead() + s.wrap_play() + s.wrap_controls() + + def clean_tracker(tracker): + for _ in tracker.active.values(): + if _.exists(): _.delete() + tracker.active.clear() + + for _ in tracker.active_sounds: + if _.exists(): _.delete() + tracker.active_sounds.clear() + + tracker.active_timers.clear() + + for t in getattr(tracker, 'internal_timers', []): + t = None + if hasattr(tracker, 'internal_timers'): + tracker.internal_timers.clear() + + for _ in tracker.active_codes.values(): + (main:=_.get('main')) and main.on_end() + (children:=_.get('children')) and [ + child.on_end() for child in children + ] + tracker.active_codes.clear() + + for seed_tracker in tracker.active_seeds.values(): + clean_tracker(seed_tracker) + tracker.active_seeds.clear() + + clean_tracker(s) + + shut or s.toast(Strings.INFO_FINISHED) + s.freeze_scene(False) + s.change_map(s.original_map) + if s.camera_data: + s.camera_timer = None + s.camera_data.clear() + _ba.set_camera_manual(False) + if getattr(s,'export_flag',False): + s.export_flag = False + s.export_replay() + if s.is_wide: + s.is_wide = False + if not s.ui_on: + s.toggle_ui() + s.build_grid() + + def wrap_play(s,init=False): + s.pause_start = None + s.play_start = perf_counter() if init else None + s.play_elapsed = 0 + s.paused_time = 0 + s.timeline_index = 0 + + def play(s): + s.freeze_scene(False) + s.playing = True + if s.play_timer: + s.paused_time += perf_counter() - s.pause_start + s.pause_start = None + for sound,vol in s.active_sounds.items(): + setattr(sound,'volume',vol) + return + s.destroy_grid() + s.collapse_all() + s.ui_clickable = False + s.make_playhead() + s.wrap_play(init=True) + s.play_timer = bui.AppTimer( + 0.01, s.do_play, repeat=True + ) + s.wrap_playhead() + + def do_play(s): + if not s.playing: return + s.play_elapsed = ( + s.pause_start - s.play_start - s.paused_time + ) if s.pause_start else ( + perf_counter() - s.play_start - s.paused_time + ) + + while ( + s.timeline_index < len(s.timeline) and + s.timeline[s.timeline_index]['time'] <= s.play_elapsed + ): + event = s.timeline[s.timeline_index] + try: s.execute_event(event) + except Exception as e: + print(format_exc()) + t = event['memory']['data']['name'] + s.toast(Strings.ERROR_EVENT(t,e)) + Eval.SOUND(Const.BAD_SOUND).play() + if not Settings.get('ignore_playback_errors'): + s.stop(shut=1) + return + s.timeline_index += 1 + continue + s.timeline_index += 1 + + for btn_id, keys in list(s.active_key_schedule.items()): + for key_info in keys[:]: + if s.play_elapsed >= key_info['time']: + try: + s.execute_key( + key_info['data'], + key_info['btn_id'], + key_info['event_type'] + ) + except Exception as e: + mem = s.memory.get(btn_id) + name = mem['data']['name'] if mem else 'Unknown' + s.toast(Strings.ERROR_EVENT(f"{name} (Key)", e)) + Eval.SOUND(Const.BAD_SOUND).play() + + keys.remove(key_info) + + if not keys: + del s.active_key_schedule[btn_id] + + if s.play_elapsed >= s.max_time: + s.stop() + return + + s.move_playhead() + + def execute_event(s,e,tracker=None): + tracker = tracker or s + + mem = e['memory'] + key = e['btn_id'] + start = e['type'] == 'start' + what = mem['event'] + data = mem['data'] + call = None + + if what == 0: + if start: + with bs.get_foreground_host_activity().context: + _shared = None + _factory = None + if data['type'] == 'spaz': + from bascenev1lib.gameutils import SharedObjects + from bascenev1lib.actor.spazfactory import SpazFactory + from bascenev1lib.actor.spaz import Spaz + _shared = SharedObjects.get() + _factory = SpazFactory.get() + attrs = {} + for attr, val in data['attrs'].items(): + attrs[attr] = eval(val) + if data['type'] == 'spaz': + for extraneous in ('materials','roller_materials','punch_materials','pickup_materials','style'): + attrs.pop(extraneous, None) + position = attrs.pop('position', None) + actor = Spaz( + character=attrs.pop('character', None) or 'Spaz', + color=attrs.pop('color', (1, 1, 1)), + highlight=attrs.pop('highlight', (1, 1, 1)), + start_invincible=False + ).autoretain() + for k, v in attrs.items(): + setattr(actor.node, k, v) + tracker.active[key] = n = actor.node + if position is not None: + activity = bs.get_foreground_host_activity() + def call(activity=activity,position=position,actor=actor): + with activity.context: + bs.timer( + 0, + lambda: actor.node.exists() and actor.handlemessage(bs.StandMessage(position, 0.0)) + ) + else: + tracker.active[key] = n = bs.newnode( + type=data['type'], + name=data['name'], + attrs=attrs + ) + else: + if key in tracker.active: + tracker.active.pop(key).delete() + + if what == 1: + if start: + has_pos,has_tar,man = data['chks'] + s.camera_data[key] = ( + has_pos and data['position'], + has_tar and data['target'], + man + ) + if not s.camera_timer: + def apply(): + if not s.camera_data: return + pos,tar,man = next( + reversed( + s.camera_data.values() + ) + ) + if apply.last_man != man: + apply.last_man = man + _ba.set_camera_manual(man) + pos and _ba.set_camera_position( + *pos + ) + pos and _ba.set_camera_target( + *tar + ) + apply.last_man = False + s.camera_timer = bui.AppTimer( + 0.02, apply, repeat=True + ) + apply() + else: + if key in s.camera_data: + man = s.camera_data.pop(key)[2] + if not s.camera_data: + s.camera_timer = None + man and _ba.set_camera_manual(False) + if what == 2: + if start: + sound_name = data['file'].replace('.ogg', '') + position = (data['x'], data['y'], data['z']) + volume = data['volume'] + with bs.get_foreground_host_activity().context: + tracker.active[key] = n = bs.newnode( + 'sound', + attrs={ + 'position':position, + 'sound':bs.getsound(sound_name), + 'volume':volume, + 'positional':not data['chks'][0], + 'loop':data['chks'][1] + } + ) + tracker.active_sounds[n] = volume + else: + if key in tracker.active: + n = tracker.active.pop(key) + if n in tracker.active_sounds: + tracker.active_sounds.pop(n) + n.delete() + + if what == 3: + if start: + at = { + attr:eval(val) + for attr,val in data['attrs'].items() + } + delay = at.pop('delay',0.1) + def _emit(): + with bs.get_foreground_host_activity().context: + bs.emitfx(**at) + tracker.active_timers[key] = bs.AppTimer( + delay, _emit, repeat=True + ) + else: + if key in tracker.active_timers: + tracker.active_timers.pop(key) + + if what == 4: + if start: + s.change_map(data['map'], extra=data['attrs']) + + if what == 6: + if start: + tracker.active_codes[key]['main'] = runner = CodeRunner( + on_error=lambda e:( + s.toast( + Format.ERROR(e) + ) + ) + ) + runner.on_start(data['code']) + tracker.active_codes[key]['children'] = [] + else: + if key in tracker.active_codes: + codes = tracker.active_codes.pop(key) + codes['main'].on_end() + for child in codes['children']: + child.on_end() + + if what == 7: + if start: + try: + seed_mem = Eval.DECODE(data['seed']) + except Exception as ex: + s.toast(Format.ERROR(ex)) + return + + sub_tracker = Tracker() + tracker.active_seeds[key] = sub_tracker + + for seed_key, seed_entry in seed_mem.items(): + start_ev = { + 'memory': seed_entry, + 'btn_id': seed_key, + 'type': 'start' + } + end_ev = { + 'memory': seed_entry, + 'btn_id': seed_key, + 'type': 'end' + } + + t_start = bui.AppTimer( + seed_entry['start'], + bui.CallPartial(s.execute_event, start_ev, tracker=sub_tracker) + ) + sub_tracker.internal_timers.append(t_start) + + t_end = bui.AppTimer( + seed_entry['start'] + seed_entry['duration'], + bui.CallPartial(s.execute_event, end_ev, tracker=sub_tracker) + ) + sub_tracker.internal_timers.append(t_end) + + for k_name, k_data in seed_entry.get('keys', {}).items(): + t_key = bui.AppTimer( + k_data['time'], + bui.CallPartial( + s.execute_key, + k_data, + seed_key, + seed_entry['event'], + sub_tracker + ) + ) + sub_tracker.internal_timers.append(t_key) + + else: + if key in tracker.active_seeds: + sub_tracker = tracker.active_seeds.pop(key) + + sub_tracker.internal_timers.clear() + + def kill_scope(t): + for _ in t.active.values(): + if _.exists(): _.delete() + t.active.clear() + for _ in t.active_sounds: + if _.exists(): _.delete() + t.active_sounds.clear() + t.active_timers.clear() + for _ in t.active_codes.values(): + (m:=_.get('main')) and m.on_end() + (c:=_.get('children')) and [x.on_end() for x in c] + t.active_codes.clear() + for k, v in t.active_seeds.items(): + kill_scope(v) + t.active_seeds.clear() + + kill_scope(sub_tracker) + + if e['type'] == 'start' and tracker == s: + tracker.active_key_schedule[key] = [] + for key_name, key_data in mem.get('keys', {}).items(): + tracker.active_key_schedule[key].append({ + 'time': key_data['time'], + 'data': key_data, + 'btn_id': key, + 'event_type': what + }) + + callable(call) and call() + + def execute_key(s, key_data, btn_id, event_type, tracker=None): + tracker = tracker or s + action = key_data['action'] + + if action == 0: + da = key_data['data'] + attr_name, attr_eval = da['attr'],da['eval'] + + if btn_id in tracker.active: + node = tracker.active[btn_id] + if node.exists(): + try: + _shared = None + _factory = None + if attr_name in ( + 'materials', 'roller_materials', + 'punch_materials', 'pickup_materials' + ): + from bascenev1lib.gameutils import SharedObjects + from bascenev1lib.actor.spazfactory import SpazFactory + _shared = SharedObjects.get() + _factory = SpazFactory.get() + setattr(node, attr_name, eval(attr_eval)) + except Exception as e: + s.toast(Format.ERROR(e)) + Eval.SOUND(Const.BAD_SOUND).play() + + elif action == 1: + if btn_id in tracker.active_codes: + parent_runner = tracker.active_codes[btn_id]['main'] + child_runner = CodeRunner( + on_error=lambda e: s.toast(Format.ERROR(e)), + parent_runner=parent_runner + ) + child_runner.on_start(key_data['data']['code']) + tracker.active_codes[btn_id]['children'].append(child_runner) + + elif action == 2: + da = key_data['data'] + v = da['volume'] + if btn_id in tracker.active: + node = tracker.active[btn_id] + if node.exists(): + if not s.paused: + try: node.volume = v + except Exception as e: + s.toast(Format.ERROR(e)) + Eval.SOUND(Const.BAD_SOUND).play() + tracker.active_sounds[node] = v + + elif action == 3: + da = key_data['data'] + if btn_id in tracker.active: + node = tracker.active[btn_id] + if node.exists(): + try: + with bs.get_foreground_host_activity().context: + Bubble( + node, + text=da['text'], + color=eval(da['color']), + time=da['time'] + ) + except Exception as e: + s.toast(Format.ERROR(e)) + Eval.SOUND(Const.BAD_SOUND).play() + + def make_playhead(s): + s.playhead and s.playhead.delete() + s.playhead = bui.imagewidget( + parent=s.stamp_hscroll_root, + texture=Eval.TEXTURE(Const.SKIN), + color=Color.WARM, + opacity=Color.OPACITY + ) + + def move_playhead(s): + bui.imagewidget( + s.playhead, + position=s.playhead_pos(s.play_elapsed) + ) + bui.containerwidget( + s.stamp_hscroll_root, + visible_child=s.playhead + ) + + def wrap_playhead(s): + s.playhead_pos = lambda i:( + i*(s.entries_per_sec*s.entry_xs_real)+4, + -s.stamp_deep_y/2 + ) + s.playhead_size = (2,s.stamp_deep_y*2) + bui.imagewidget( + s.playhead, + size=s.playhead_size + ) + s.move_playhead() + + def kill_playhead(s,instant=False): + if instant: + s.playhead.delete() + return + px,py = s.playhead_pos(s.play_elapsed) + sx,sy = s.playhead_size + end_sx = sx*100 + s.anims[id(s.playhead)] = Animate( + widget=s.playhead, + attrs={ + 'size':( + (sx,sy), + (end_sx,sy) + ), + 'opacity':( + Color.OPACITY*0.6, + 0 + ), + 'position':( + (px,py), + (px-end_sx/2,py) + ) + }, + duration=s.global_butter*2, + on_finish=s.playhead.delete + ) + + @clickable + def select(s,b): + Eval.SOUND(Const.OK_SOUND).play() + if s.window_on and s.window_on[1] in ( + s.edit_window, + s.key_window + ): s.dismiss_window() + sl = b + yes = lambda: bui.buttonwidget( + b,color=Color.COLD + ) + no = lambda: bui.buttonwidget( + s.sl,color=Color.BASE + ) + if s.sl == sl: + no() + s.hide_tools() + s.show_controls(up=True) + s.sl = None + return + if s.sl: no() + s.hide_controls(up=True) + s.show_tools() + s.sl = sl + yes() + + def show_tools(s): + if s.tools_shown: return + s.tools_shown = True + xs,ys = s.tool_size + start_size = (xs,ys/4) + start_tc = Const.INVISIBLE + start_op = 0 + for i,b in enumerate(s.tools): + if (a:=s.anims.get(id(b),None)): + a.cancel() + s.anims[id(b)] = Animate( + widget=b, + duration=s.global_butter, + attrs={ + 'size':( + start_size, + s.tool_size + ), + 'textcolor':( + start_tc, + (*Color.TEXT,Color.TEXT_OPACITY) + ), + 'opacity':(start_op,Color.OPACITY) + } + ) + + @ui_safe + def hide_tools(s): + if not s.tools_shown: return + s.tools_shown = False + for b in s.tools: + s.anims[id(b)].reverse( + duration=s.global_butter, + on_finish=bui.CallPartial( + bui.buttonwidget, + b, size=(0,0) + ) + ) + + @clickable + def do_tool(s,which): + if not s.tools_shown: return + if not s.sl: return + b =s.sl + mem = s.memory[id(b)] + if mem.get('smol', False) and which in [2, 3]: + Eval.SOUND(Const.BAD_SOUND).play() + s.toast(Strings.ERROR_SMOL_NO_RESIZE) + return + new = {} + scroll_butter = s.global_butter/2 + restamp = lambda:( + s.wrap(2), + s.make_timeline(), + s.wrap_timeline() + ) + start_size = Eval.ENTRY_SIZE(s,mem) + start_pos = Eval.ENTRY_POS(s,mem) + + def animate_key_widgets(anim_key, key_old_positions): + for key_data in mem.get('keys', {}).values(): + if 'widget' in key_data and s.widgets[key_data['widget']].exists(): + wid = s.widgets[key_data['widget']] + wid_id = id(wid) + key_time = key_data['time'] + + new_x = s.magic_x + s.entry_xs_real * key_time * s.entries_per_sec - s.entry_ys_real/4 + new_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + + start_wid_pos = key_old_positions.get(wid_id, (new_x, new_y)) + + if wid_id in s.anims and anim_key in s.anims[wid_id]: + if (anim := s.anims[wid_id].get(anim_key, None)) and not anim.finished: + start_wid_pos = anim.attrs_current['position'] + anim.cancel() + + if wid_id not in s.anims: + s.anims[wid_id] = {} + + s.anims[wid_id].pop(anim_key, None) + + s.anims[wid_id][anim_key] = Animate( + widget=wid, + attrs={'position': (start_wid_pos, (new_x, new_y))}, + duration=s.global_butter + ) + + if which == 0: + for key in [1, 2, 3]: + if (anim := s.anims[id(b)].get(key, None)): + anim.cancel() + s.anims[id(b)].pop(key, None) + + if (anim := s.anims[id(b)].get(0, None)) and not anim.finished: + start_pos = anim.attrs_current['position'] + anim.cancel() + + s.anims[id(b)].pop(0, None) + + mem['start'] += 1/s.entries_per_sec + + key_old_positions = {} + for key_data in mem.get('keys', {}).values(): + if 'widget' in key_data and s.widgets[key_data['widget']].exists(): + wid = s.widgets[key_data['widget']] + wid_id = id(wid) + + if wid_id in s.anims and 1 in s.anims[wid_id]: + if (anim := s.anims[wid_id].get(1, None)) and not anim.finished: + key_old_positions[wid_id] = anim.attrs_current['position'] + anim.cancel() + s.anims[wid_id].pop(1, None) + else: + old_x = s.magic_x + s.entry_xs_real * key_data['time'] * s.entries_per_sec - s.entry_ys_real/4 + old_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + key_old_positions[wid_id] = (old_x, old_y) + else: + old_x = s.magic_x + s.entry_xs_real * key_data['time'] * s.entries_per_sec - s.entry_ys_real/4 + old_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + key_old_positions[wid_id] = (old_x, old_y) + + for key_data in mem.get('keys', {}).values(): + key_data['time'] += 1/s.entries_per_sec + + end_pos = Eval.ENTRY_POS(s,mem) + new['position'] = (start_pos, end_pos) + + animate_key_widgets(0, key_old_positions) + + if mem.get('prev_off_wid') and s.widgets[mem['prev_off_wid']].exists(): + wid_id = mem['prev_off_wid'] + + for key in [1, 4, 5]: + if (anim := s.anims.get(wid_id, {}).get(key, None)): + anim.cancel() + s.anims[wid_id].pop(key, None) + + old_wid_x = s.magic_x + s.entry_xs_real * (mem['start'] - 1/s.entries_per_sec + mem['prev_off']) * s.entries_per_sec - s.entry_ys_real/4 + new_wid_x = s.magic_x + s.entry_xs_real * (mem['start'] + mem['prev_off']) * s.entries_per_sec - s.entry_ys_real/4 + wid_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + + start_wid_pos = (old_wid_x, wid_y) + + if (anim := s.anims.get(wid_id, {}).get(0, None)) and not anim.finished: + start_wid_pos = anim.attrs_current['position'] + anim.cancel() + + if wid_id in s.anims: + s.anims[wid_id].pop(0, None) + else: + s.anims[wid_id] = {} + + s.anims[wid_id][0] = Animate( + widget=s.widgets[mem['prev_off_wid']], + attrs={ + 'position': (start_wid_pos, (new_wid_x, wid_y)) + }, + duration=s.global_butter + ) + + restamp() + + if which == 1: + if mem['start'] < 0.01: + Eval.SOUND(Const.BAD_SOUND).play() + s.toast(Strings.ERROR_REACHED_ZERO) + return + + for key in [0, 2, 3]: + if (anim := s.anims[id(b)].get(key, None)): + anim.cancel() + s.anims[id(b)].pop(key, None) + + if (anim := s.anims[id(b)].get(1, None)) and not anim.finished: + start_pos = anim.attrs_current['position'] + anim.cancel() + + s.anims[id(b)].pop(1, None) + + mem['start'] -= 1/s.entries_per_sec + + key_old_positions = {} + for key_data in mem.get('keys', {}).values(): + if 'widget' in key_data and s.widgets[key_data['widget']].exists(): + wid = s.widgets[key_data['widget']] + wid_id = id(wid) + + if wid_id in s.anims and 0 in s.anims[wid_id]: + if (anim := s.anims[wid_id].get(0, None)) and not anim.finished: + key_old_positions[wid_id] = anim.attrs_current['position'] + anim.cancel() + s.anims[wid_id].pop(0, None) + else: + old_x = s.magic_x + s.entry_xs_real * key_data['time'] * s.entries_per_sec - s.entry_ys_real/4 + old_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + key_old_positions[wid_id] = (old_x, old_y) + else: + old_x = s.magic_x + s.entry_xs_real * key_data['time'] * s.entries_per_sec - s.entry_ys_real/4 + old_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + key_old_positions[wid_id] = (old_x, old_y) + + for key_data in mem.get('keys', {}).values(): + key_data['time'] -= 1/s.entries_per_sec + + end_pos = Eval.ENTRY_POS(s, mem) + new['position'] = (start_pos, end_pos) + + animate_key_widgets(1, key_old_positions) + + if mem.get('prev_off_wid') and s.widgets[mem['prev_off_wid']].exists(): + wid_id = mem['prev_off_wid'] + + for key in [0, 4, 5]: + if (anim := s.anims.get(wid_id, {}).get(key, None)): + anim.cancel() + s.anims[wid_id].pop(key, None) + + old_wid_x = s.magic_x + s.entry_xs_real * (mem['start'] + 1/s.entries_per_sec + mem['prev_off']) * s.entries_per_sec - s.entry_ys_real/4 + new_wid_x = s.magic_x + s.entry_xs_real * (mem['start'] + mem['prev_off']) * s.entries_per_sec - s.entry_ys_real/4 + wid_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + + start_wid_pos = (old_wid_x, wid_y) + + if (anim := s.anims.get(wid_id, {}).get(1, None)) and not anim.finished: + start_wid_pos = anim.attrs_current['position'] + anim.cancel() + + if wid_id in s.anims: + s.anims[wid_id].pop(1, None) + else: + s.anims[wid_id] = {} + + s.anims[wid_id][1] = Animate( + widget=s.widgets[mem['prev_off_wid']], + attrs={ + 'position': (start_wid_pos, (new_wid_x, wid_y)) + }, + duration=s.global_butter + ) + + restamp() + + if which == 2: + if (shrink := s.anims[id(b)].get(3, None)): + shrink.cancel() + s.anims[id(b)].pop(3, None) + + if (anim := s.anims[id(b)].get(2, None)) and not anim.finished: + start_size = anim.attrs_current['size'] + start_pos = anim.attrs_current['position'] + anim.cancel() + + s.anims[id(b)].pop(2, None) + + mem['duration'] += 1 / s.entries_per_sec + + end_size = Eval.ENTRY_SIZE(s,mem) + end_pos = Eval.ENTRY_POS(s,mem) + + new['size'] = (start_size, end_size) + new['position'] = (start_pos, end_pos) + + restamp() + + for key_data in mem.get('keys', {}).values(): + if 'widget' in key_data: + key_offset = key_data['time'] - mem['start'] + if key_offset <= mem['duration'] and not s.widgets[key_data['widget']].exists(): + key_x = s.magic_x + s.entry_xs_real * (mem['start'] + key_offset) * s.entries_per_sec - s.entry_ys_real/4 + key_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + + key_w = bui.imagewidget( + parent=s.stamp_hscroll_root, + position=(key_x, key_y), + size=(s.entry_ys_real, s.entry_ys_real - s.magic_y), + texture=Eval.TEXTURE(Const.KEY), + color=Color.WARM, + opacity=Color.OPACITY + ) + key_data['widget'] = id(key_w) + s.widgets[id(key_w)] = key_w + + if mem.get('prev_off') is not None and s.window_on and s.window_on[1] == s.key_window: + old_duration = mem['duration'] - 1/s.entries_per_sec + new_duration = mem['duration'] + + if old_duration < mem['prev_off'] and new_duration >= mem['prev_off']: + wid_x = s.magic_x + s.entry_xs_real * (mem['start'] + mem['prev_off']) * s.entries_per_sec - s.entry_ys_real/4 + wid_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + + s.prev_off_wid = bui.imagewidget( + parent=s.stamp_hscroll_root, + position=(wid_x, wid_y), + size=(s.entry_ys_real, s.entry_ys_real - s.magic_y), + texture=Eval.TEXTURE(Const.KEY), + color=Color.WARM, + opacity=Color.OPACITY + ) + mem['prev_off_wid'] = id(s.prev_off_wid) + s.widgets[id(s.prev_off_wid)] = s.prev_off_wid + + if which == 3: + current_ticks = round(mem['duration'] * s.entries_per_sec) + if current_ticks <= 1: + Eval.SOUND(Const.BAD_SOUND).play() + s.toast(Strings.ERROR_SMALLEST) + return + + if (expand := s.anims[id(b)].get(2, None)): + expand.cancel() + s.anims[id(b)].pop(2, None) + + if (anim := s.anims[id(b)].get(3, None)) and not anim.finished: + start_size = anim.attrs_current['size'] + start_pos = anim.attrs_current['position'] + anim.cancel() + + s.anims[id(b)].pop(3, None) + + mem['duration'] -= 1 / s.entries_per_sec + + new_width_steps = mem['duration'] * s.entries_per_sec + end_size = ( + s.entry_xs_real * new_width_steps * s.magic_right, + s.entry_ys_real - s.magic_y + ) + end_x = s.magic_x + s.entry_xs_real * mem['start']*s.entries_per_sec + (new_width_steps * s.magic_left) + end_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + end_pos = (end_x, end_y) + + new['size'] = (start_size, end_size) + new['position'] = (start_pos, end_pos) + + restamp() + + for key_data in mem.get('keys', {}).values(): + key_time = key_data['time'] - mem['start'] + if key_time > mem['duration']: + if 'widget' in key_data and s.widgets[key_data['widget']].exists(): + s.widgets.pop(key_data['widget']).delete() + + if mem.get('prev_off') is not None and s.window_on and s.window_on[1] == s.key_window: + old_duration = mem['duration'] + 1/s.entries_per_sec + new_duration = mem['duration'] + + if old_duration >= mem['prev_off'] and new_duration < mem['prev_off']: + if mem.get('prev_off_wid') and s.widgets[mem['prev_off_wid']].exists(): + s.widgets.pop(mem.pop('prev_off_wid')).delete() + + if which == 4: + current_list_index = s.stamp_kids.index(b) + if current_list_index == 0: + Eval.SOUND(Const.BAD_SOUND).play() + s.toast(Strings.ERROR_AT_TOP) + return + + Eval.SOUND(Const.OK_SOUND).play() + + target_list_index = current_list_index - 1 + other_btn = s.stamp_kids[target_list_index] + other_mem = s.memory[id(other_btn)] + + start_pos_b = Eval.ENTRY_POS(s,mem) + start_pos_other = Eval.ENTRY_POS(s,other_mem) + new_y_up = Eval.ENTRY_Y(s,other_mem) + new_y_down = Eval.ENTRY_Y(s,mem) + + key_old_positions = {} + for key_data in mem.get('keys', {}).values(): + if 'widget' in key_data and s.widgets[key_data['widget']].exists(): + wid = s.widgets[key_data['widget']] + wid_id = id(wid) + + found = False + for check_key in [4, 5]: + if wid_id in s.anims and check_key in s.anims[wid_id]: + if (anim := s.anims[wid_id].get(check_key, None)) and not anim.finished: + key_old_positions[wid_id] = anim.attrs_current['position'] + anim.cancel() + s.anims[wid_id].pop(check_key, None) + found = True + break + + if not found: + old_x = s.magic_x + s.entry_xs_real * key_data['time'] * s.entries_per_sec - s.entry_ys_real/4 + old_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + key_old_positions[wid_id] = (old_x, old_y) + + other_key_old_positions = {} + for key_data in other_mem.get('keys', {}).values(): + if 'widget' in key_data and s.widgets[key_data['widget']].exists(): + wid = s.widgets[key_data['widget']] + wid_id = id(wid) + + found = False + for check_key in [4, 5]: + if wid_id in s.anims and check_key in s.anims[wid_id]: + if (anim := s.anims[wid_id].get(check_key, None)) and not anim.finished: + other_key_old_positions[wid_id] = anim.attrs_current['position'] + anim.cancel() + s.anims[wid_id].pop(check_key, None) + found = True + break + if not found: + old_x = s.magic_x + s.entry_xs_real * key_data['time'] * s.entries_per_sec - s.entry_ys_real/4 + old_y = s.entry_ys_real * (len(s.memory) - other_mem['order'] - 1) + other_key_old_positions[wid_id] = (old_x, old_y) + + mem['order'],other_mem['order'] = other_mem['order'],mem['order'] + + s.stamp_kids[current_list_index] = other_btn + s.stamp_kids[target_list_index] = b + + if (down := s.anims[id(b)].get(5, None)): + down.cancel() + s.anims[id(b)].pop(5, None) + + if (anim := s.anims[id(b)].get(4, None)) and not anim.finished: + start_pos_b = anim.attrs_current['position'] + anim.cancel() + + s.anims[id(b)].pop(4, None) + + end_pos_b = (start_pos_b[0], new_y_up) + s.anims[id(b)][4] = Animate( + widget=b, + duration=s.global_butter, + attrs={'position': (start_pos_b, end_pos_b)} + ) + + if (down := s.anims[id(other_btn)].get(5, None)): + down.cancel() + s.anims[id(other_btn)].pop(5, None) + + if (anim := s.anims[id(other_btn)].get(4, None)) and not anim.finished: + start_pos_other = anim.attrs_current['position'] + anim.cancel() + + s.anims[id(other_btn)].pop(4, None) + + end_pos_other = (start_pos_other[0], new_y_down) + s.anims[id(other_btn)][4] = Animate( + widget=other_btn, + duration=s.global_butter, + attrs={'position': (start_pos_other, end_pos_other)} + ) + + animate_key_widgets(4, key_old_positions) + + for key_data in other_mem.get('keys', {}).values(): + if 'widget' in key_data and s.widgets[key_data['widget']].exists(): + wid = s.widgets[key_data['widget']] + wid_id = id(wid) + key_time = key_data['time'] + + new_x = s.magic_x + s.entry_xs_real * key_time * s.entries_per_sec - s.entry_ys_real/4 + new_y = s.entry_ys_real * (len(s.memory) - other_mem['order'] - 1) + + start_wid_pos = other_key_old_positions.get(wid_id, (new_x, new_y)) + + if wid_id in s.anims and 4 in s.anims[wid_id]: + if (anim := s.anims[wid_id].get(4, None)) and not anim.finished: + start_wid_pos = anim.attrs_current['position'] + anim.cancel() + + if wid_id not in s.anims: + s.anims[wid_id] = {} + + s.anims[wid_id].pop(4, None) + + s.anims[wid_id][4] = Animate( + widget=wid, + attrs={'position': (start_wid_pos, (new_x, new_y))}, + duration=s.global_butter + ) + + if mem.get('prev_off_wid') and s.widgets[mem['prev_off_wid']].exists(): + wid_id = mem['prev_off_wid'] + + for key in [5, 0, 1]: + if (anim := s.anims.get(wid_id, {}).get(key, None)): + anim.cancel() + s.anims[wid_id].pop(key, None) + + wid_x = s.magic_x + s.entry_xs_real * (mem['start'] + mem['prev_off']) * s.entries_per_sec - s.entry_ys_real/4 + old_wid_y = s.entry_ys_real * (len(s.memory) - other_mem['order'] - 1) + new_wid_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + + start_wid_pos = (wid_x, old_wid_y) + + if (anim := s.anims.get(wid_id, {}).get(4, None)) and not anim.finished: + start_wid_pos = anim.attrs_current['position'] + anim.cancel() + + if wid_id in s.anims: + s.anims[wid_id].pop(4, None) + else: + s.anims[wid_id] = {} + + s.anims[wid_id][4] = Animate( + widget=s.widgets[mem['prev_off_wid']], + attrs={ + 'position': (start_wid_pos, (wid_x, new_wid_y)) + }, + duration=s.global_butter + ) + + if other_mem.get('prev_off_wid') and s.widgets[other_mem['prev_off_wid']].exists(): + wid_id = other_mem['prev_off_wid'] + + for key in [5, 0, 1]: + if (anim := s.anims.get(wid_id, {}).get(key, None)): + anim.cancel() + s.anims[wid_id].pop(key, None) + + wid_x = s.magic_x + s.entry_xs_real * (other_mem['start'] + other_mem['prev_off']) * s.entries_per_sec - s.entry_ys_real/4 + old_wid_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + new_wid_y = s.entry_ys_real * (len(s.memory) - other_mem['order'] - 1) + + start_wid_pos = (wid_x, old_wid_y) + + if (anim := s.anims.get(wid_id, {}).get(4, None)) and not anim.finished: + start_wid_pos = anim.attrs_current['position'] + anim.cancel() + + if wid_id in s.anims: + s.anims[wid_id].pop(4, None) + else: + s.anims[wid_id] = {} + + s.anims[wid_id][4] = Animate( + widget=s.widgets[other_mem['prev_off_wid']], + attrs={ + 'position': (start_wid_pos, (wid_x, new_wid_y)) + }, + duration=s.global_butter + ) + + if which == 5: + current_list_index = s.stamp_kids.index(b) + max_list_index = len(s.stamp_kids) - 1 + if current_list_index == max_list_index: + Eval.SOUND(Const.BAD_SOUND).play() + s.toast(Strings.ERROR_AT_BOTTOM) + return + + Eval.SOUND(Const.OK_SOUND).play() + + target_list_index = current_list_index + 1 + other_btn = s.stamp_kids[target_list_index] + other_mem = s.memory[id(other_btn)] + + start_pos_b = Eval.ENTRY_POS(s,mem) + start_pos_other = Eval.ENTRY_POS(s,other_mem) + new_y_down = Eval.ENTRY_Y(s,other_mem) + new_y_up = Eval.ENTRY_Y(s,mem) + + key_old_positions = {} + for key_data in mem.get('keys', {}).values(): + if 'widget' in key_data and s.widgets[key_data['widget']].exists(): + wid = s.widgets[key_data['widget']] + wid_id = id(wid) + + found = False + for check_key in [4, 5]: + if wid_id in s.anims and check_key in s.anims[wid_id]: + if (anim := s.anims[wid_id].get(check_key, None)) and not anim.finished: + key_old_positions[wid_id] = anim.attrs_current['position'] + anim.cancel() + s.anims[wid_id].pop(check_key, None) + found = True + break + + if not found: + old_x = s.magic_x + s.entry_xs_real * key_data['time'] * s.entries_per_sec - s.entry_ys_real/4 + old_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + key_old_positions[wid_id] = (old_x, old_y) + + other_key_old_positions = {} + for key_data in other_mem.get('keys', {}).values(): + if 'widget' in key_data and s.widgets[key_data['widget']].exists(): + wid = s.widgets[key_data['widget']] + wid_id = id(wid) + + found = False + for check_key in [4, 5]: + if wid_id in s.anims and check_key in s.anims[wid_id]: + if (anim := s.anims[wid_id].get(check_key, None)) and not anim.finished: + other_key_old_positions[wid_id] = anim.attrs_current['position'] + anim.cancel() + s.anims[wid_id].pop(check_key, None) + found = True + break + + if not found: + old_x = s.magic_x + s.entry_xs_real * key_data['time'] * s.entries_per_sec - s.entry_ys_real/4 + old_y = s.entry_ys_real * (len(s.memory) - other_mem['order'] - 1) + other_key_old_positions[wid_id] = (old_x, old_y) + + mem['order'],other_mem['order'] = other_mem['order'],mem['order'] + + s.stamp_kids[current_list_index] = other_btn + s.stamp_kids[target_list_index] = b + + if (up := s.anims[id(b)].get(4, None)): + up.cancel() + s.anims[id(b)].pop(4, None) + + if (anim := s.anims[id(b)].get(5, None)) and not anim.finished: + start_pos_b = anim.attrs_current['position'] + anim.cancel() + + s.anims[id(b)].pop(5, None) + + end_pos_b = (start_pos_b[0], new_y_down) + s.anims[id(b)][5] = Animate( + widget=b, + duration=s.global_butter, + attrs={'position': (start_pos_b, end_pos_b)} + ) + + if (up := s.anims[id(other_btn)].get(4, None)): + up.cancel() + s.anims[id(other_btn)].pop(4, None) + + if (anim := s.anims[id(other_btn)].get(5, None)) and not anim.finished: + start_pos_other = anim.attrs_current['position'] + anim.cancel() + + s.anims[id(other_btn)].pop(5, None) + + end_pos_other = (start_pos_other[0], new_y_up) + s.anims[id(other_btn)][5] = Animate( + widget=other_btn, + duration=s.global_butter, + attrs={'position': (start_pos_other, end_pos_other)} + ) + + animate_key_widgets(5, key_old_positions) + + for key_data in other_mem.get('keys', {}).values(): + if 'widget' in key_data and s.widgets[key_data['widget']].exists(): + wid = s.widgets[key_data['widget']] + wid_id = id(wid) + key_time = key_data['time'] + + new_x = s.magic_x + s.entry_xs_real * key_time * s.entries_per_sec - s.entry_ys_real/4 + new_y = s.entry_ys_real * (len(s.memory) - other_mem['order'] - 1) + + start_wid_pos = other_key_old_positions.get(wid_id, (new_x, new_y)) + + if wid_id in s.anims and 5 in s.anims[wid_id]: + if (anim := s.anims[wid_id].get(5, None)) and not anim.finished: + start_wid_pos = anim.attrs_current['position'] + anim.cancel() + + if wid_id not in s.anims: + s.anims[wid_id] = {} + + s.anims[wid_id].pop(5, None) + + s.anims[wid_id][5] = Animate( + widget=wid, + attrs={'position': (start_wid_pos, (new_x, new_y))}, + duration=s.global_butter + ) + + if mem.get('prev_off_wid') and s.widgets[mem['prev_off_wid']].exists(): + wid_id = mem['prev_off_wid'] + + for key in [4, 0, 1]: + if (anim := s.anims.get(wid_id, {}).get(key, None)): + anim.cancel() + s.anims[wid_id].pop(key, None) + + wid_x = s.magic_x + s.entry_xs_real * (mem['start'] + mem['prev_off']) * s.entries_per_sec - s.entry_ys_real/4 + old_wid_y = s.entry_ys_real * (len(s.memory) - other_mem['order'] - 1) + new_wid_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + + start_wid_pos = (wid_x, old_wid_y) + + if (anim := s.anims.get(wid_id, {}).get(5, None)) and not anim.finished: + start_wid_pos = anim.attrs_current['position'] + anim.cancel() + + if wid_id in s.anims: + s.anims[wid_id].pop(5, None) + else: + s.anims[wid_id] = {} + + s.anims[wid_id][5] = Animate( + widget=s.widgets[mem['prev_off_wid']], + attrs={ + 'position': (start_wid_pos, (wid_x, new_wid_y)) + }, + duration=s.global_butter + ) + + if other_mem.get('prev_off_wid') and s.widgets[other_mem['prev_off_wid']].exists(): + wid_id = other_mem['prev_off_wid'] + + for key in [4, 0, 1]: + if (anim := s.anims.get(wid_id, {}).get(key, None)): + anim.cancel() + s.anims[wid_id].pop(key, None) + + wid_x = s.magic_x + s.entry_xs_real * (other_mem['start'] + other_mem['prev_off']) * s.entries_per_sec - s.entry_ys_real/4 + old_wid_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + new_wid_y = s.entry_ys_real * (len(s.memory) - other_mem['order'] - 1) + + start_wid_pos = (wid_x, old_wid_y) + + if (anim := s.anims.get(wid_id, {}).get(5, None)) and not anim.finished: + start_wid_pos = anim.attrs_current['position'] + anim.cancel() + + if wid_id in s.anims: + s.anims[wid_id].pop(5, None) + else: + s.anims[wid_id] = {} + + s.anims[wid_id][5] = Animate( + widget=s.widgets[other_mem['prev_off_wid']], + attrs={ + 'position': (start_wid_pos, (wid_x, new_wid_y)) + }, + duration=s.global_butter + ) + + if which == 6: + Eval.SOUND(Const.OK_SOUND).play() + if s.can_do != which: + s.toast(Strings.CONFIRM_DUPLICATE( + mem['data']['name'] + ), extra=2) + s.can_do = which + return + s.on_scroll() + + for kid in s.stamp_kids: + if (anim := s.anims[id(kid)].get(6, None)): + anim.cancel() + s.anims[id(kid)].pop(6, None) + bui.buttonwidget( + kid, + opacity=Color.OPACITY, + textcolor=(*Color.TEXT, Color.TEXT_OPACITY) + ) + + original_data = mem.copy() + original_event = original_data['event'] + original_duration = original_data['duration'] + original_start = original_data['start'] + original_order = original_data['order'] + node_data = { + i:( + isinstance(j,(list,dict)) + and j.copy() or j + ) for i,j in original_data['data'].copy().items() + } + + size = ( + s.entry_xs_real * ( + original_duration * + s.entries_per_sec + ) * s.magic_right, + s.entry_ys_real - s.magic_y + ) + btn = bui.buttonwidget( + parent=s.stamp_hscroll_root, + texture=Eval.TEXTURE(Const.SKIN), + label=node_data['name'], + textcolor=Const.INVISIBLE, + color=Color.BASE, + opacity=0, + enable_sound=False, + size=size, + button_type='square' + ) + + original_list_index = s.stamp_kids.index(b) + s.stamp_kids.insert(original_list_index + 1, btn) + + new_order = original_order + 1 + + call = bui.CallPartial( + s.select, btn + ) + bui.buttonwidget(btn, on_activate_call=call) + + new_keys = {} + for nam, key_data in original_data.get('keys', {}).items(): + new_key_data = key_data.copy() + key_time = new_key_data['time'] + key_x = s.magic_x + s.entry_xs_real * key_time * s.entries_per_sec - s.entry_ys_real/4 + key_y = s.entry_ys_real * (len(s.memory) - new_order) + + new_key_wid = bui.imagewidget( + parent=s.stamp_hscroll_root, + position=(key_x, key_y), + size=(s.entry_ys_real, s.entry_ys_real - s.magic_y), + texture=Eval.TEXTURE(Const.KEY), + color=Color.WARM, + opacity=0 + ) + new_key_data['widget'] = id(new_key_wid) + s.widgets[id(new_key_wid)] = new_key_wid + new_keys[nam] = new_key_data + + s.memory[id(btn)] = { + 'order': new_order, + 'event': original_event, + 'data': node_data, + 'duration': original_duration, + 'start': original_start, + 'keys': new_keys, + 'prev_off': original_data.get('prev_off'), + 'prev_off_wid': None + } + + for kid in s.stamp_kids[original_list_index + 2:]: + s.memory[id(kid)]['order'] += 1 + + s.wrap([1, 2, 3]) + + final_x = Eval.ENTRY_X(s, {'start': original_start, 'duration': original_duration}) + orig_y = Eval.ENTRY_Y(s, {'order': original_order}) + final_y = Eval.ENTRY_Y(s, {'order': new_order}) + + bui.buttonwidget(btn, position=(final_x, orig_y)) + + for kid in s.stamp_kids[:original_list_index + 1]: + kid_mem = s.memory[id(kid)] + kid_x = Eval.ENTRY_X(s, kid_mem) + + old_y = s.entry_ys_real * (len(s.memory) - kid_mem['order'] - 2) + new_y = s.entry_ys_real * (len(s.memory) - kid_mem['order'] - 1) + + s.anims[id(kid)][which] = Animate( + widget=kid, + attrs={ + 'position': ((kid_x, old_y), (kid_x, new_y)) + }, + duration=s.global_butter + ) + + for kid in s.stamp_kids[:original_list_index + 1]: + kid_mem = s.memory[id(kid)] + kid_width_steps = kid_mem['duration'] * s.entries_per_sec + kid_x = s.magic_x + s.entry_xs_real * kid_mem['start']*s.entries_per_sec + (kid_width_steps * s.magic_left) + + old_y = s.entry_ys_real * (len(s.memory) - kid_mem['order'] - 2) + new_y = s.entry_ys_real * (len(s.memory) - kid_mem['order'] - 1) + + s.anims[id(kid)][which] = Animate( + widget=kid, + attrs={ + 'position': ((kid_x, old_y), (kid_x, new_y)) + }, + duration=s.global_butter + ) + + for key_data in kid_mem.get('keys', {}).values(): + if 'widget' in key_data and s.widgets[key_data['widget']].exists(): + wid = s.widgets[key_data['widget']] + wid_id = id(wid) + key_time = key_data['time'] + + key_x = s.magic_x + s.entry_xs_real * key_time * s.entries_per_sec - s.entry_ys_real/4 + old_key_y = s.entry_ys_real * (len(s.memory) - kid_mem['order'] - 2) + new_key_y = s.entry_ys_real * (len(s.memory) - kid_mem['order'] - 1) + + if wid_id not in s.anims: + s.anims[wid_id] = {} + + s.anims[wid_id][which] = Animate( + widget=wid, + attrs={ + 'opacity': (Color.OPACITY, Color.OPACITY), + 'position': ((key_x, old_key_y), (key_x, new_key_y)) + }, + duration=s.global_butter + ) + + s.anims[id(btn)][which] = Animate( + widget=btn, + attrs={ + 'opacity': (0, Color.OPACITY), + 'textcolor': ( + Const.INVISIBLE, + (*Color.TEXT, Color.TEXT_OPACITY) + ), + 'position': ((final_x, orig_y), (final_x, final_y)) + }, + duration=s.global_butter + ) + + for key_data in new_keys.values(): + if 'widget' in key_data and s.widgets[key_data['widget']].exists(): + wid = s.widgets[key_data['widget']] + wid_id = id(wid) + key_time = key_data['time'] + + key_x = s.magic_x + s.entry_xs_real * key_time * s.entries_per_sec - s.entry_ys_real/4 + key_y = s.entry_ys_real * (len(s.memory) - new_order - 1) + + if wid_id not in s.anims: + s.anims[wid_id] = {} + + s.anims[wid_id][which] = Animate( + widget=wid, + attrs={ + 'opacity': (0, Color.OPACITY), + 'position': ((key_x, orig_y + 0), (key_x, key_y)) + }, + duration=s.global_butter + ) + + s.scroll_to_timer = bui.AppTimer( + s.global_butter / 2, + bui.CallPartial(s.scroll_to, btn) + ) + + call() + s.toast(Strings.INFO_DUPLICATED(node_data["name"])) + s.build_timeline() + bui.apptimer(s.global_butter,s.wrap) + return + + if which == 7: + Eval.SOUND(Const.OK_SOUND).play() + if s.can_do != which: + s.toast(Strings.CONFIRM_DELETE( + mem['data']['name'] + ), extra=2) + s.can_do = which + return + + if s.window_on and s.window_on[1] == s.edit_window: + s.dismiss_window() + + node_name = mem['data']['name'] + deleted_order = mem['order'] + + deleted_key_old_positions = {} + for key_data in mem.get('keys', {}).values(): + if 'widget' in key_data and s.widgets[key_data['widget']].exists(): + wid = s.widgets[key_data['widget']] + wid_id = id(wid) + key_time = key_data['time'] + + old_x = s.magic_x + s.entry_xs_real * key_time * s.entries_per_sec - s.entry_ys_real/4 + old_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) + deleted_key_old_positions[wid_id] = (old_x, old_y) + + def cleanup(): + for key_data in mem.get('keys', {}).values(): + if 'widget' in key_data and s.widgets[key_data['widget']].exists(): + wid = s.widgets.pop(key_data['widget']) + wid_id = id(wid) + + start_pos = deleted_key_old_positions.get(wid_id, (0, 0)) + + if wid_id in s.anims and which in s.anims[wid_id]: + if (anim := s.anims[wid_id].get(which, None)) and not anim.finished: + start_pos = anim.attrs_current['position'] + anim.cancel() + + if wid_id not in s.anims: + s.anims[wid_id] = {} + + s.anims[wid_id].pop(which, None) + + s.anims[wid_id][which] = Animate( + widget=wid, + attrs={ + 'opacity': (Color.OPACITY, 0), + 'position': (start_pos, start_pos) + }, + duration=s.global_butter / 2, + on_finish=wid.delete + ) + + if mem.get('prev_off_wid') and s.widgets[mem['prev_off_wid']].exists(): + wid = s.widgets.pop(mem['prev_off_wid']) + wid_id = id(wid) + + if wid_id not in s.anims: + s.anims[wid_id] = {} + + s.anims[wid_id][which] = Animate( + widget=wid, + attrs={'opacity': (Color.OPACITY, 0)}, + duration=s.global_butter / 2, + on_finish=wid.delete + ) + mem.pop('prev_off_wid') + + del s.memory[id(b)] + s.stamp_kids.remove(b) + + if b.exists(): + b.delete() + + for kid in s.stamp_kids: + kid_mem = s.memory[id(kid)] + if kid_mem['order'] > deleted_order: + kid_mem['order'] -= 1 + + s.wrap([1,2,3]) + + for idx, kid in enumerate(reversed(s.stamp_kids)): + kid_mem = s.memory[id(kid)] + if kid_mem['order'] >= deleted_order: + continue + + old_x = Eval.ENTRY_X(s, kid_mem) + current_y = s.entry_ys_real * (idx + 1) + end_pos = (old_x, s.entry_ys_real * idx) + + s.anims[id(kid)][which] = Animate( + widget=kid, + attrs={ + 'position': ((old_x, current_y), end_pos) + }, + duration=s.global_butter + ) + + for key_data in kid_mem.get('keys', {}).values(): + if 'widget' in key_data and s.widgets[key_data['widget']].exists(): + wid = s.widgets[key_data['widget']] + wid_id = id(wid) + key_time = key_data['time'] + + new_x = s.magic_x + s.entry_xs_real * key_time * s.entries_per_sec - s.entry_ys_real/4 + old_key_y = s.entry_ys_real * (idx + 1) + new_key_y = s.entry_ys_real * idx + + start_wid_pos = (new_x, old_key_y) + + if wid_id in s.anims and which in s.anims[wid_id]: + if (anim := s.anims[wid_id].get(which, None)) and not anim.finished: + start_wid_pos = anim.attrs_current['position'] + anim.cancel() + + if wid_id not in s.anims: + s.anims[wid_id] = {} + + s.anims[wid_id].pop(which, None) + + s.anims[wid_id][which] = Animate( + widget=wid, + attrs={ + 'position': (start_wid_pos, (new_x, new_key_y)) + }, + duration=s.global_butter + ) + + if kid_mem.get('prev_off_wid') and s.widgets[kid_mem['prev_off_wid']].exists(): + wid_id = kid_mem['prev_off_wid'] + + for key in [4, 5, 0, 1]: + if (anim := s.anims.get(wid_id, {}).get(key, None)): + anim.cancel() + s.anims[wid_id].pop(key, None) + + wid_x = s.magic_x + s.entry_xs_real * (kid_mem['start'] + kid_mem['prev_off']) * s.entries_per_sec - s.entry_ys_real/4 + old_wid_y = s.entry_ys_real * (idx + 1) + new_wid_y = s.entry_ys_real * idx + + start_wid_pos = (wid_x, old_wid_y) + + if (anim := s.anims.get(wid_id, {}).get(which, None)) and not anim.finished: + start_wid_pos = anim.attrs_current['position'] + anim.cancel() + + if wid_id in s.anims: + s.anims[wid_id].pop(which, None) + else: + s.anims[wid_id] = {} + + s.anims[wid_id][which] = Animate( + widget=s.widgets[kid_mem['prev_off_wid']], + attrs={ + 'position': (start_wid_pos, (wid_x, new_wid_y)) + }, + duration=s.global_butter + ) + + s.sl = None + s.hide_tools() + if len(s.memory): + s.show_controls(up=True) + + s.toast(Strings.INFO_DELETED(node_name)) + s.build_timeline() + + s.anims[id(b)][which] = Animate( + widget=b, + attrs={ + 'opacity': (Color.OPACITY, 0), + 'textcolor': ( + (*Color.TEXT, Color.TEXT_OPACITY), + Const.INVISIBLE + ) + }, + duration=s.global_butter / 2, + on_finish=cleanup + ) + + s.wrap(2) + return + + s.build_timeline() + which not in [2,3] and bui.apptimer( + scroll_butter, + bui.CallPartial(s.scroll_to,b) + ) + butter = s.global_butter + if new: + s.anims[id(b)][which] = Animate( + widget=b, + duration=butter, + attrs=new + ) + Eval.SOUND(Const.OK_SOUND).play() + + def scroll_to(s,b): + bui.containerwidget( + s.stamp_hscroll_root, + visible_child=b + ) + rx,ry = s.real + bx,by = b.get_screen_space_center() + dx,dy = s.bottom_left_h.get_screen_space_center() + to = Eval.RELATIVE( + rx/2,ry/2, + dx,dy, + bx,by + ) + temp = bui.textwidget( + parent=s.stamp_scroll_root, + position=to, + ) + bui.containerwidget( + s.stamp_scroll_root, + visible_child=temp + ) + temp.delete() + s.on_scroll() + +class Bubble: + __mem__ = {} + def __init__( + s, + node: 'bs.Node', + text: str = 'Hello!', + color: tuple = (1,1,1), + time: float | int = 4, + mode: int = 0, + res: list = [('█'),('▼')] + ) -> None: + if not 0 <= mode <= 5 : raise ValueError(f'mode can be an integer from 0 to 5, not {mode}') + if not mode: mode = choice([1,2,3,4,5]) + s.ans,s.kids,s.mats,s.time = [],[],[],time + s.node,s.dead,s.text = node,False,text + s.color,s.mode,s.res = color,mode,res + s.mem = lambda: s.__class__.__mem__ + m = s.mem() + o = m.get(node,0) + if not getattr(o,'dead',1): bs.timer(0.2,bs.CallPartial(o.delete,force=True)) + s.show() + m[node] = s + def show(s): + q,l,r = s.mats,s.kids,s.ans + m = bs.newnode( + 'math', + owner=s.node, + attrs={ + 'input1': (0,1.65,0), + 'operation': 'add' + } + ) + q.append(m) + c = list(s.color) + w = Eval.STRING_WIDTH(s.res[0]) + b = bs.newnode( + 'text', + owner=m, + attrs={ + 'text': f'{ceil((Eval.STRING_WIDTH(s.text)+2*w)/w)*s.res[0]}\n{s.res[1]}', + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': (c[0],c[1],c[2],0.2), + 'scale': 0.01, + 'h_align': 'center' + } + ) + l.append(b) + txt = [] + mat = [] + kek = -Eval.STRING_WIDTH(s.text)/185 + sf = 0 + for i in range(len(s.text)): + j = s.text[i] + x = Eval.STRING_WIDTH(j)/95.0 + p1 = bs.newnode( + 'text', + owner=m, + attrs={ + 'text': j, + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': s.color, + 'scale': 0.01, + 'h_align': 'left' + } + ) + txt.append(p1) + ok = kek+sf + p2 = bs.newnode( + 'math', + owner=m, + attrs={ + 'input1': (ok,1.65,0), + 'operation': 'add' + } + ) + mat.append([p2,ok]) + s.node.connectattr('position',p2,'input2') + p2.connectattr('output',p1,'position') + sf += x + l += txt + q += [mat[i][0] for i in range(len(mat))] + s.node.connectattr('position',m,'input2') + m.connectattr('output',b,'position') + z = s.time + a = bs.animate( + b, + 'scale', + { + 0:0, + z*0.041: 0.014, + z*0.154: 0.014, + z*0.167: 0.010, + z*0.98: 0.010, + z:0 + }, + ) + r.append(a) + a = bs.animate_array( + m, + 'input1', + 3, + { + 0:(0,1.2,0), + z*0.04:(0,1.65,0), + z*0.98:(0,1.65,0), + z:(0,1.2,0) + } + ) + r.append(a) + r += [ + bs.animate( + txt[i], + 'scale', + { + 0:0, + z*0.041: 0.015, + z*0.154: 0.015, + z*0.167: 0.010, + z*0.98: 0.010, + z:0 + }, + ) + for i in range(len(mat)) + ] if s.mode in [1,4] else [] + r += [ + bs.animate_array( + mat[i][0], + 'input1', + 3, + { + 0:(mat[i][1]/4,1.2,0), + z*0.04:(mat[i][1]*1.5,1.65,0), + z*0.154:(mat[i][1]*1.5,1.65,0), + z*0.167:(mat[i][1],1.65,0), + z*0.98:(mat[i][1],1.65,0), + z:(mat[i][1]/4,1.2,0) + } + ) + for i in range(len(mat)) + ] if s.mode in [1,4] else [] + ok = (z*0.04*1.6) + hm = [0.03,0.05][s.mode==2] + r += [ + bs.animate_array( + j[0], + 'input1', + 3, + { + 0.5+i*hm:(j[1],1.4,0), + 0.5+i*hm+(ok*0.6):(j[1],1.9,0), + 0.5+i*hm+ok:(j[1],1.65,0), + (z-(z*0.02)):(j[1],1.65,0), + z:(j[1],1.2,0) + } + ) + for i,j in enumerate(mat) + ] if s.mode in [2,5] else [] + r += [ + bs.animate( + txt[i], + 'opacity', + { + 0.5+i*hm:0, + (0.5+i*hm+ok)*0.98:1, + z*0.9:1, + z:0 + } + ) + for i in range(len(mat)) + ] if s.mode in [2,4,5] else [] + r += [ + bs.animate( + txt[i], + 'scale', + { + 0:0, + z*0.154: 0, + z*0.167: 0.010, + z*0.98: 0.010, + z:0 + }, + ) + for i in range(len(mat)) + ] if s.mode == 3 else [] + bs.timer(z,s.delete) + + def delete(s,force=False): + if s.dead: return + s.dead = True + [i.delete() for i in s.ans if hasattr(i,'delete')] + bs.timer(0.2,lambda:[i.delete() for i in s.kids+s.mats if hasattr(i,'delete')]) + if not force: return + [bs.animate( + i, + 'opacity', + { + 0:i.opacity, + 0.2:0 + } + ) for i in s.kids] + +class Animate: + def __init__(s, widget, attrs, duration, on_start=None, on_finish=None, on_cancel=None, delay=0, condition=None, on_reverse=None): + """ + Dynamic animation system. + + Args: + widget: The widget to animate + func: The function to call (e.g., bui.imagewidget, bui.buttonwidget) + attrs: Dict of attributes to animate, format: + {'attr_name': (start_value, end_value), ...} + Examples: + - {'opacity': (0, 1)} + - {'position': ((0,0), (100,200))} + - {'size': ((50,50), (200,300)), 'opacity': (0, 0.5)} + duration: Animation duration in seconds + on_start: Optional callback when animation starts + on_finish: Optional callback when animation completes + delay: Delay in seconds before starting animation + condition: Optional callable that must return True + """ + s.widget = widget + s.on_start = on_start + s.on_finish = ( + isinstance(on_finish,tuple) and bui.CallPartial( + s.reverse, + on_finish=on_finish[0] + ) or on_finish + ) + s.on_reverse = on_reverse + s.on_cancel = on_cancel + s.cancelled = False + s.finished = False + s.delay = delay + s.delay_timer = None + s.timer = None + s.condition = condition + if not widget.exists(): return + s.func = Eval.WIDGET(widget) + + s.attrs_start = {} + s.attrs_end = {} + s.attrs_current = {} + + for attr_name, (start_val, end_val) in attrs.items(): + s.attrs_start[attr_name] = start_val + s.attrs_end[attr_name] = end_val + if isinstance(start_val, (list, tuple)): + s.attrs_current[attr_name] = list(start_val) + else: + s.attrs_current[attr_name] = start_val + + anim_speed = max(Settings.get('anim_speed') or 1.0, 0.05) + if not Settings.get('ui_anim_on'): + duration = Const.ANIM_INSTANT + s.delay = 0 + else: + duration = duration/anim_speed + + s.duration = duration + s.start_time = None + + if s.delay > 0: + s.delay_timer = bui.AppTimer(s.delay, s.start_animation) + else: + s.start_animation() + + def __del__(s): + s.cancel() + + def start_animation(s): + """Start the actual animation after delay.""" + if s.cancelled: return + if callable(s.condition) and not s.condition(): return + s.delay_timer = None + s.start_time = perf_counter() + s.timer = bui.AppTimer(0.008, s.tick, repeat=True) + if callable(s.on_start): s.on_start() + + def lerp(s, a, b, t): + """Linear interpolation for single values or tuples/lists.""" + if isinstance(a, (list, tuple)): + return [s.lerp(av, bv, t) for av, bv in zip(a, b)] + return a + (b - a) * t + + def tick(s): + if s.cancelled: + s.timer = None + return s.finish() + + if not s.widget.exists(): + s.timer = None + s.delay_timer = None + s.cancelled = True + s._release_callbacks() + return + + elapsed = perf_counter() - s.start_time + progress = min(elapsed / s.duration, 1.0) + + t = s.ease_out(progress) + + kwargs = {} + for attr_name in s.attrs_start: + start_val = s.attrs_start[attr_name] + end_val = s.attrs_end[attr_name] + + current_val = s.lerp(start_val, end_val, t) + + s.attrs_current[attr_name] = current_val + + if isinstance(current_val, list): + current_val = tuple(current_val) + + kwargs[attr_name] = current_val + + if not s.widget.exists(): + s.timer = None + s.delay_timer = None + s.cancelled = True + s._release_callbacks() + return + try: s.func(s.widget, **kwargs) + except: + s.timer = None + s.delay_timer = None + s.cancelled = True + s._release_callbacks() + return + + if progress >= 1.0: + s.timer = None + s.finish() + + def ease_out(s, t): + return 1 - (1 - t) ** 3 + + def _release_callbacks(s): + """ + Drop callback references once they're no longer needed. + on_finish in particular can hold a self-referencing CallPartial + (see the tuple-on_finish handling in __init__, which wraps + s.reverse -- a bound method of this very instance), so leaving + it set after it's already fired keeps this instance alive as a + reference cycle that only the periodic cyclic gc pass can + collect, instead of being freed immediately by refcounting once + nothing else references it. + + on_reverse is deliberately NOT cleared here -- it's only ever + read inside reverse() itself (including synchronously, when a + tuple on_finish auto-triggers reverse() from within finish()), + so it's cleared there instead, right after it's actually used. + """ + s.on_finish = None + s.on_start = None + s.on_cancel = None + s.condition = None + + def finish(s): + s.finished = True + on_finish = s.on_finish + should_call = callable(on_finish) and not s.cancelled + s._release_callbacks() + if should_call: + on_finish() + + def complete(s): + """Immediately complete the animation by applying final values.""" + if s.cancelled or s.finished: + return + + on_finish = s.on_finish + s.cancel() + + if s.widget.exists(): + kwargs = {} + for attr_name, end_val in s.attrs_end.items(): + if isinstance(end_val, list): + end_val = tuple(end_val) + kwargs[attr_name] = end_val + s.attrs_current[attr_name] = end_val + + s.func(s.widget, **kwargs) + + s.finished = True + if callable(on_finish): + on_finish() + + def cancel(s): + s.cancelled = True + s.timer = None + if s.delay_timer: + s.delay_timer = None + on_cancel = s.on_cancel + should_call = callable(on_cancel) and not s.finished + exists = s.widget and s.widget.exists() + s._release_callbacks() + if not exists: return + if should_call: + on_cancel() + + def get_state(s): + """Returns current animation state for all attributes.""" + return { + 'current': s.attrs_current.copy(), + 'start': s.attrs_start.copy(), + 'end': s.attrs_end.copy() + } + + def reverse(s,**kwargs): + """ + Create and return a new animation that reverses this one. + Uses current values as start and original start as end. + + Args: + Anything __init__ accepts. + + Returns: + New Animate instance with reversed animation + """ + callable(s.on_reverse) and s.on_reverse() + s.on_reverse = None + s.cancel() + + reversed_attrs = {} + for attr_name in s.attrs_current: + current = s.attrs_current[attr_name] + original_start = s.attrs_start[attr_name] + + if isinstance(current, list): + current = tuple(current) + if isinstance(original_start, list): + original_start = tuple(original_start) + + reversed_attrs[attr_name] = (current, original_start) + + new = { + 'duration':s.duration + } + new.update(kwargs) + return Animate( + widget=s.widget, + attrs=reversed_attrs, + **new + ) + +# hardcoded stuff + +class _StringsEN: + MAP_TITLE = 'Movi' + MAP_DESCRIPTION = 'Movie Maker' + INSTANCE_DESCRIPTION = 'Three Two One Action!' + INSTANCE_DESCRIPTION_SHORT = f'Version {__version__}' + MENUS = ( + 'Save & Exit', + 'Clear Session', + 'Load Seed', + 'Copy Seed', + 'Toggle Editor', + 'Record BRP', + 'Wide Preview' + ) + EDIT_BUTTON = 'Edit' + SETTINGS = 'Settings' + EVENT_BUTTON_OFF = 'Event' + EVENT_BUTTON_ON = 'Back' + EVENTS = { + 'Node':'Make a scene node', + 'Camera':'Tune the camera', + 'Sound':'Play a sound', + 'FX':'Emit an effect', + 'Map':'Control the map', + 'Preset':'Load a preset', + 'Code':'Custom code', + 'Seed':'Project seed' + } + LOADED_ENTRIES = 'Loaded {} entries!' + LOADED_ENTRIES_HELP = 'Seeds are useful aren\'t they?' + SAVED_AS = 'Saved as {}' + SAVED_AS_HELP = 'Full path: {}' + CODE = 'Code' + CODE_HELP = "Keyframes continue from the\nevent's code. All variables and\nstate are shared." + EXTEND_CODE = 'Parallel Code' + CODE_EDITOR = 'Code Editor' + COPY = 'Copy' + RUN = 'Run' + SEED = 'Seed' + PASTE = 'Paste' + ATTR = 'Attr' + NEXT = 'Next' + NODE_ATTR_HELP = 'The node\'s attribute name in attr dict\nbascenev1.newnode(attrs={\'THIS\':value})\nEnter' + FX_ATTR_HELP = 'The FX\'s attribute name in attr dict\nbascenev1.emitfx(THIS=value)\nEnter' + MAP_ATTR_HELP = 'The Sound\'s attribute name in attr dict\nsetattr(bascenev1.getactivity().map.node,\'THIS\',value)\nEnter' + EVAL = 'Eval' + EVAL_HELP = 'The node\'s attr value in attr dict (evaluated)\nbascenev1.newnode(attrs={\'attr\':THIS})\nEnter' + OFFSET = 'Offset' + TEXT = 'Text' + COLOR = 'Color' + TIME = 'Time' + BUBBLE_TEXT_HELP = 'The bubble\'s message\nEnter' + BUBBLE_COLOR_HELP = 'The bubble\'s color (evaluated tuple)\nEnter' + BUBBLE_TIME_HELP = 'How long the bubble stays, in seconds\nEnter' + TYPE = 'Type' + TYPE_HELP = 'The node\'s type kwarg\nbascenev1.newnode(type=\'THIS\')\nEnter' + SEED_HELP = 'The Movi\'s project seed. Get it from Square -> Copy Seed\nEnter' + NAME = 'Name' + NODE_NAME_HELP = 'The node\'s name kwarg\nbascenev1.newnode(name=\'THIS\')\nEnter' + SEED_TIP = 'A Movi seed contains all the memory of\nthe project, which is basically a dict of\nentry data. Get your seed by pressing on\nCopy Seed option in the square menu.' + SEED = 'Seed' + FX_NAME_HELP = 'The FX name, used only for recognition\nEnter' + SET = 'Set' + POP = 'Pop' + KEYS = 'Keys' + DONE = 'Done' + TARGET = 'Traget' + POSITION = 'Position' + VALUE = 'Value' + EVERYWHERE = 'Everywhere' + LOOP = 'Loop' + LOAD = 'Load' + ACTIONS = [ + 'Attribute', + 'Code', + 'Volume', + 'Bubble' + ] + ACTION_PLACEHOLDER = 'Select an action\nNice UI appears here' + SOUND_PLACEHOLDER = 'Select a sound' + PRESET_PLACEHOLDER = 'Select a preset' + CAMERA_RESET_BUTTON = 'Reset' + PREVIEW = 'Preview' + STOP = 'Stop' + CAMERA_POSITION_CHECK = 'Position' + CAMERA_TARGET_CHECK = 'Target' + CAMERA_MANUAL_CHECK = 'Manual' + CAMERA_ENTRY = 'Camera' + ERROR_ALREADY_WIDE = ( + 'Already wide!', + 'UI is already collapsed blud' + ) + ERROR_NO_MEMORY = ( + 'There\'s nothing to record!', + 'Unless you want a blank BRP lol' + ) + ERROR_NO_SOUND_SELECTED = ( + 'No sound selected!', + 'Pick one from the list first' + ) + ERROR_SMOL_NO_RESIZE = ( + 'Not resizable!', + 'This is an instant action blud' + ) + ERROR_EMPTY_CODE = ( + 'Empty code!', + 'Enter something bud' + ) + ERROR_INVALID = 'Invalid {}!' + ERROR_INVALID_HELP = 'Check your input pal' + ERROR_OUT_OF_RANGE = '{} out of range!' + ERROR_OUT_OF_RANGE_HELP = 'You went too far' + ERROR_EMPTY = 'Empty {}!' + ERROR_EMPTY_HELP = 'Stop leaving empty text boxes around' + ERROR = 'Error!' + ERROR_E = 'Error: {}' + ERROR_HELP = 'You\'re on your own pal' + ERROR_EVENT = lambda t,e: ( + f'{t}: {e}', + 'Your fault, not mine.' + ) + ERROR_NOT_FOUND = 'Nothing here is called {}' + ERROR_NOT_FOUND_HELP = 'Yeah, nothing happened' + ERROR_REACHED_ZERO = ( + 'Reached zero!', + 'Yeah I can\'t move it past that' + ) + ERROR_AT_TOP = ( + 'Already at the top!', + 'No entries above to swap' + ) + ERROR_AT_BOTTOM = ( + 'Hit the bottom!', + 'No entries below to swap' + ) + ERROR_SMALLEST = ( + 'Already at smallest size!', + 'Yeah it can\'t be smaller' + ) + ERROR_SELECT_SOMETHING = ( + 'Select something!', + 'Press on an entry to select it' + ) + ERROR_NOT_PLAYING = ( + 'Not even playing!', + 'There is no playhead to hide' + ) + ERROR_PAUSE_FIRST = ( + 'Stop playback first!', + 'The playhead is watching, I can\'t.' + ) + NO_ACTIONS = ( + 'No actions available!', + 'We\'re stuck with it as is' + ) + INFO_CONFIRM_CLEAR = ( + 'Really clear all memory?', + 'Press again to remove everything' + ) + INFO_MEMORY_CLEARED = ( + 'Memory Cleared!', + 'I\'m clean now, regret.' + ) + INFO_RECORDING_NOW = ( + 'Recording now! Press OK to finish recording', + 'Just watch your movie silently' + ) + STOP_RECORDING_LABEL = 'OK' + ABOUT_LABEL = '!' + ABOUT_TITLE = 'About' + ABOUT_VERSION = 'Version {}' + ABOUT_TAGLINE = 'Made with love and tea.' + ABOUT_THANKS = 'Special thanks to: YOU for trying Movi!' + INFO_RECORDING_SAVED = ( + 'Recording Saved!', + 'A wild shiny BRP file was created' + ) + INFO_NO_CLIPBOARD = ( + 'Empty clipboard!', + 'What are you trying to do exactly?' + ) + INFO_COPIED = ( + 'Copied to clipboard!', + 'Hope it\'s in good hands now' + ) + INFO_PASTED = ( + 'Pasted!', + 'Let\'s hope you did\'t click this by mistake' + ) + INFO_SLOW_DOWN = ( + 'Slow down poke', + 'I know you\'re spamming the ui' + ) + INFO_POP_WHAT = ( + 'Pop what?', + 'Buddy write some name' + ) + INFO_SAVED = ( + 'Saved changes!', + 'Go look at it' + ) + INFO_DISCARDED = ( + 'Discarded changes!', + 'Because you changed your mind' + ) + INFO_DELETED = lambda n:( + f'Deleted "{n}"', + 'Now it\'s gone forever' + ) + INFO_DUPLICATED = lambda n:( + f'Duplicated "{n}"', + 'Now there\'s two of them. This is getting out of hand.' + ) + INFO_ASSIGNED = lambda a:( + f'Assigned new attribute {a}', + 'Use the same attr name to overwrite it later' + ) + INFO_UPDATED = lambda a:( + f'Updated existing attribute {a}', + 'Since you used the same attr name' + ) + INFO_POPPED = lambda n:( + f'Popped "{n}"', + 'It\'s in a better place now.' + ) + INFO_TARGET_MODE = ( + 'Target mode', + 'Now arrows apply to target boxes' + ) + INFO_POSITION_MODE = ( + 'Position mode', + 'Now arrows apply to position boxes' + ) + INFO_PREVIEW_ON = ( + 'Preview on', + 'Camera changes are previewed live' + ) + PREVIEW_OFF = ( + 'Preview off', + 'Forget I said anything' + ) + INFO_RESETTED = ( + 'Resetted', + 'Everything is clean once again' + ) + INFO_PLAYING = ( + 'Now playing!', + 'What else should\'ve happened?' + ) + INFO_PAUSED = ( + 'Playback paused!', + 'You just froze time' + ) + INFO_FINISHED = ( + 'Playback finished!', + 'The playhead is gone' + ) + INFO_ADDED_KEY = ( + 'Key added!', + 'Yeah, that red dot' + ) + INFO_EDITED_KEY = ( + 'Key edited!', + 'Now it does something else' + ) + CONFIRM_DUPLICATE = lambda t:( + f'Make another "{t}"?', + f'Press {Eval.CHAR(Const.TOOLS[6])} again to confirm' + ) + CONFIRM_DELETE = lambda t:( + f'Really delete "{t}"?', + f'Press {Eval.CHAR(Const.TOOLS[7])} again to confirm' + ) + KEYS_ON = 'Keys on {}' + BYE = ( + 'That\'s a wrap!', + 'How fast can you read this' + ) + EDIT = 'Edit {}' + WELCOME = '{} joined the studio! Press for more' + DESCRIPTION_HERE = 'Description here' + WELCOME_HELP = 'Movi v{}, what could go wrong?' + COMING_SOON = ( + 'Coming soon!', + 'Aka not implemented yet lmao' + ) + + SETTING_ENTRY_DURATION = 'Default Entry Duration' + SETTING_ANIM_SPEED = 'UI Animation Speed' + SETTING_BASE_OPACITY = 'Base Opacity' + SETTING_TEXT_OPACITY = 'Text Opacity' + SETTING_THEME = 'Theme' + SETTING_LANGUAGE = 'Language' + SETTING_AUTOSAVE_ON = 'Autosave' + SETTING_AUTOSAVE_INTERVAL = 'Autosave Interval (s)' + SETTING_UI_ANIM_ON = 'UI Animations' + SETTING_SFX_EDITOR = 'Editor SFX' + SETTING_SFX_UI = 'UI SFX' + SETTING_IGNORE_PLAYBACK_ERRORS = 'Ignore Errors During Playback' + SETTING_SHOW_GRID_2D = 'Show 2D Grid' + SETTING_SHOW_GRID_3D = 'Show 3D Grid' + SETTING_BRP_TEXT_EXPORT = 'Also Export Memory JSON' + SETTING_EXPORT_FILENAME = 'Filename' + SETTING_TOAST_TOP = 'Toast at Top' + SETTING_FANCY_AUTOSAVE = 'Fancy Autosave' + SETTING_TOAST_DURATION = 'Toast Duration (s)' + SETTING_EPIC_MODE = 'Epic Mode' + SETTING_DEBUG_HEADER = 'Debug' + SETTING_DUMP_MEMORY = 'Dump Memory to Log' + SETTING_DUMP_TIMELINE = 'Dump Timeline to Log' + SETTING_ASPECT_RATIO = 'Aspect Ratio' + INFO_DUMPED_MEMORY = ( + 'Memory dumped!', + 'Check your console/log' + ) + INFO_DUMPED_TIMELINE = ( + 'Timeline dumped!', + 'Check your console/log' + ) + INFO_EPIC_ON = ( + 'Epic mode engaged!', + 'The autosave panel is showing off now' + ) + INFO_EPIC_OFF = ( + 'Epic mode disengaged', + 'Back to being humble' + ) + INFO_CHANGING_OPACITY = ( + 'Changing opacity...', + 'Refreshing the timeline' + ) + INFO_CHANGING_THEME = ( + 'Changing theme...', + 'Refreshing the timeline' + ) + INFO_CHANGING_LANGUAGE = ( + 'Changing language...', + 'Refreshing the timeline' + ) + SETTING_FILL_ASPECT_RATIO = 'Fill Outside Frame' + + +class _StringsMeta(type): + def __getattr__(cls,name): + table = _TRANSLATIONS.get(Settings.get('language')) + if table and name in table: + return table[name] + return getattr(_StringsEN,name) + +class Strings(metaclass=_StringsMeta): + """Dynamic, language-aware replacement for the old flat Strings + class. Every Strings.FOO lookup resolves against the current + language's translation table, falling back to English (via + _StringsEN) for anything not yet translated. See _StringsMeta.""" + LANGUAGES = ('English','Arabic','Japanese','French','Chinese', + 'Spanish','German','Portuguese','Russian','Korean', + 'Hindi','Italian','Bruh') + LANGUAGE_NAMES = { + 'English': 'English', + 'Arabic': 'العربية', + 'Japanese': '日本語', + 'French': 'Français', + 'Chinese': '中文', + 'Spanish': 'Español', + 'German': 'Deutsch', + 'Portuguese': 'Português', + 'Russian': 'Русский', + 'Korean': '한국어', + 'Hindi': 'हिन्दी', + 'Italian': 'Italiano', + 'Bruh': 'bruh 💀', + } + +_TRANSLATIONS = { + 'Arabic': { + 'SETTINGS':'الإعدادات','EDIT_BUTTON':'تعديل', + 'EVENT_BUTTON_OFF':'حدث','EVENT_BUTTON_ON':'رجوع', + 'DONE':'تم','LOAD':'تحميل','SET':'ضبط','POP':'إزالة', + 'KEYS':'مفاتيح','NAME':'الاسم','TYPE':'النوع','VALUE':'القيمة', + 'POSITION':'الموضع','TARGET':'الهدف','PREVIEW':'معاينة', + 'STOP':'إيقاف','COPY':'نسخ','RUN':'تشغيل','PASTE':'لصق', + 'SEED':'البذرة','CODE':'الكود','NEXT':'التالي','LOOP':'تكرار', + 'EVERYWHERE':'في كل مكان','ERROR':'خطأ!', + 'MENUS':('حفظ وخروج','مسح الجلسة','تحميل بذرة','نسخ البذرة', + 'تبديل المحرر','تسجيل BRP','معاينة عريضة'), + 'EVENTS':{'Node':'إنشاء عقدة مشهد','Camera':'ضبط الكاميرا', + 'Sound':'تشغيل صوت','FX':'إطلاق تأثير', + 'Map':'التحكم بالخريطة','Preset':'تحميل إعداد مسبق', + 'Code':'كود مخصص','Seed':'بذرة المشروع'}, + 'SETTING_ENTRY_DURATION':'مدة الإدخال الافتراضية', + 'SETTING_ANIM_SPEED':'سرعة حركة الواجهة', + 'SETTING_BASE_OPACITY':'الشفافية الأساسية', + 'SETTING_TEXT_OPACITY':'شفافية النص', + 'SETTING_THEME':'السمة','SETTING_LANGUAGE':'اللغة', + 'SETTING_AUTOSAVE_ON':'الحفظ التلقائي', + 'SETTING_AUTOSAVE_INTERVAL':'فاصل الحفظ التلقائي (ث)', + 'SETTING_UI_ANIM_ON':'حركات الواجهة', + 'SETTING_SFX_EDITOR':'أصوات المحرر', + 'SETTING_SFX_UI':'أصوات الواجهة', + 'SETTING_IGNORE_PLAYBACK_ERRORS':'تجاهل الأخطاء أثناء التشغيل', + 'SETTING_SHOW_GRID_2D':'إظهار الشبكة ثنائية الأبعاد', + 'SETTING_SHOW_GRID_3D':'إظهار الشبكة ثلاثية الأبعاد', + 'SETTING_BRP_TEXT_EXPORT':'تصدير JSON للذاكرة أيضاً', + 'SETTING_EXPORT_FILENAME':'اسم الملف', + 'SETTING_TOAST_TOP':'الإشعارات في الأعلى', + 'SETTING_FANCY_AUTOSAVE':'حفظ تلقائي مزخرف', + 'SETTING_TOAST_DURATION':'مدة الإشعار (ث)', + 'SETTING_EPIC_MODE':'الوضع الملحمي', + 'SETTING_DEBUG_HEADER':'التصحيح', + 'SETTING_DUMP_MEMORY':'تفريغ الذاكرة إلى السجل', + 'SETTING_DUMP_TIMELINE':'تفريغ الخط الزمني إلى السجل', + 'SETTING_ASPECT_RATIO':'نسبة العرض إلى الارتفاع', + 'SETTING_FILL_ASPECT_RATIO':'ملء ما وراء الإطار', + 'COMING_SOON':('قريباً!','لم يتم تنفيذه بعد'), + }, + 'Japanese': { + 'SETTINGS':'設定','EDIT_BUTTON':'編集', + 'EVENT_BUTTON_OFF':'イベント','EVENT_BUTTON_ON':'戻る', + 'DONE':'完了','LOAD':'読み込み','SET':'設定','POP':'削除', + 'KEYS':'キー','NAME':'名前','TYPE':'タイプ','VALUE':'値', + 'POSITION':'位置','TARGET':'ターゲット','PREVIEW':'プレビュー', + 'STOP':'停止','COPY':'コピー','RUN':'実行','PASTE':'貼り付け', + 'SEED':'シード','CODE':'コード','NEXT':'次へ','LOOP':'ループ', + 'EVERYWHERE':'どこでも','ERROR':'エラー!', + 'MENUS':('保存して終了','セッションをクリア','シードを読み込む', + 'シードをコピー','エディタ切替','BRPを録画', + 'ワイドプレビュー'), + 'EVENTS':{'Node':'シーンノードを作成','Camera':'カメラを調整', + 'Sound':'サウンドを再生','FX':'エフェクトを発生', + 'Map':'マップを操作','Preset':'プリセットを読み込む', + 'Code':'カスタムコード','Seed':'プロジェクトシード'}, + 'SETTING_ENTRY_DURATION':'デフォルトの継続時間', + 'SETTING_ANIM_SPEED':'UIアニメーション速度', + 'SETTING_BASE_OPACITY':'基本の不透明度', + 'SETTING_TEXT_OPACITY':'テキストの不透明度', + 'SETTING_THEME':'テーマ','SETTING_LANGUAGE':'言語', + 'SETTING_AUTOSAVE_ON':'自動保存', + 'SETTING_AUTOSAVE_INTERVAL':'自動保存間隔(秒)', + 'SETTING_UI_ANIM_ON':'UIアニメーション', + 'SETTING_SFX_EDITOR':'エディタ効果音', + 'SETTING_SFX_UI':'UI効果音', + 'SETTING_IGNORE_PLAYBACK_ERRORS':'再生中のエラーを無視', + 'SETTING_SHOW_GRID_2D':'2Dグリッドを表示', + 'SETTING_SHOW_GRID_3D':'3Dグリッドを表示', + 'SETTING_BRP_TEXT_EXPORT':'メモリJSONも書き出す', + 'SETTING_EXPORT_FILENAME':'ファイル名', + 'SETTING_TOAST_TOP':'通知を上部に表示', + 'SETTING_FANCY_AUTOSAVE':'装飾された自動保存', + 'SETTING_TOAST_DURATION':'通知の表示時間(秒)', + 'SETTING_EPIC_MODE':'エピックモード', + 'SETTING_DEBUG_HEADER':'デバッグ', + 'SETTING_DUMP_MEMORY':'メモリをログに出力', + 'SETTING_DUMP_TIMELINE':'タイムラインをログに出力', + 'SETTING_ASPECT_RATIO':'アスペクト比', + 'SETTING_FILL_ASPECT_RATIO':'フレーム外を塗りつぶす', + 'COMING_SOON':('近日公開!','まだ実装されていません'), + }, + 'French': { + 'SETTINGS':'Paramètres','EDIT_BUTTON':'Modifier', + 'EVENT_BUTTON_OFF':'Événement','EVENT_BUTTON_ON':'Retour', + 'DONE':'Terminé','LOAD':'Charger','SET':'Définir', + 'POP':'Retirer','KEYS':'Clés','NAME':'Nom','TYPE':'Type', + 'VALUE':'Valeur','POSITION':'Position','TARGET':'Cible', + 'PREVIEW':'Aperçu','STOP':'Arrêter','COPY':'Copier', + 'RUN':'Exécuter','PASTE':'Coller','SEED':'Graine', + 'CODE':'Code','NEXT':'Suivant','LOOP':'Boucle', + 'EVERYWHERE':'Partout','ERROR':'Erreur !', + 'MENUS':('Enregistrer et quitter','Effacer la session', + 'Charger une graine','Copier la graine', + "Basculer l'éditeur",'Enregistrer BRP', + 'Aperçu large'), + 'EVENTS':{'Node':'Créer un nœud de scène', + 'Camera':'Régler la caméra','Sound':'Jouer un son', + 'FX':'Émettre un effet','Map':'Contrôler la carte', + 'Preset':'Charger un préréglage', + 'Code':'Code personnalisé','Seed':'Graine du projet'}, + 'SETTING_ENTRY_DURATION':"Durée d'entrée par défaut", + 'SETTING_ANIM_SPEED':"Vitesse d'animation UI", + 'SETTING_BASE_OPACITY':'Opacité de base', + 'SETTING_TEXT_OPACITY':'Opacité du texte', + 'SETTING_THEME':'Thème','SETTING_LANGUAGE':'Langue', + 'SETTING_AUTOSAVE_ON':'Enregistrement automatique', + 'SETTING_AUTOSAVE_INTERVAL':'Intervalle auto (s)', + 'SETTING_UI_ANIM_ON':"Animations de l'UI", + 'SETTING_SFX_EDITOR':"Sons de l'éditeur", + 'SETTING_SFX_UI':"Sons de l'UI", + 'SETTING_IGNORE_PLAYBACK_ERRORS': + 'Ignorer les erreurs pendant la lecture', + 'SETTING_SHOW_GRID_2D':'Afficher la grille 2D', + 'SETTING_SHOW_GRID_3D':'Afficher la grille 3D', + 'SETTING_BRP_TEXT_EXPORT': + 'Exporter aussi le JSON mémoire', + 'SETTING_EXPORT_FILENAME':'Nom de fichier', + 'SETTING_TOAST_TOP':'Notification en haut', + 'SETTING_FANCY_AUTOSAVE':'Auto-save élaboré', + 'SETTING_TOAST_DURATION':'Durée de notification (s)', + 'SETTING_EPIC_MODE':'Mode épique', + 'SETTING_DEBUG_HEADER':'Débogage', + 'SETTING_DUMP_MEMORY':'Vider la mémoire dans le journal', + 'SETTING_DUMP_TIMELINE': + 'Vider la chronologie dans le journal', + 'SETTING_ASPECT_RATIO':"Format d'image", + 'SETTING_FILL_ASPECT_RATIO':'Remplir hors du cadre', + 'COMING_SOON':('Bientôt disponible !', + "Pas encore implémenté"), + }, + 'Chinese': { + 'SETTINGS':'设置','EDIT_BUTTON':'编辑', + 'EVENT_BUTTON_OFF':'事件','EVENT_BUTTON_ON':'返回', + 'DONE':'完成','LOAD':'加载','SET':'设置','POP':'移除', + 'KEYS':'关键帧','NAME':'名称','TYPE':'类型','VALUE':'值', + 'POSITION':'位置','TARGET':'目标','PREVIEW':'预览', + 'STOP':'停止','COPY':'复制','RUN':'运行','PASTE':'粘贴', + 'SEED':'种子','CODE':'代码','NEXT':'下一个','LOOP':'循环', + 'EVERYWHERE':'所有位置','ERROR':'错误!', + 'MENUS':('保存并退出','清除会话','加载种子','复制种子', + '切换编辑器','录制BRP','宽屏预览'), + 'EVENTS':{'Node':'创建场景节点','Camera':'调整摄像机', + 'Sound':'播放声音','FX':'触发特效', + 'Map':'控制地图','Preset':'加载预设', + 'Code':'自定义代码','Seed':'项目种子'}, + 'SETTING_ENTRY_DURATION':'默认条目时长', + 'SETTING_ANIM_SPEED':'界面动画速度', + 'SETTING_BASE_OPACITY':'基础不透明度', + 'SETTING_TEXT_OPACITY':'文字不透明度', + 'SETTING_THEME':'主题','SETTING_LANGUAGE':'语言', + 'SETTING_AUTOSAVE_ON':'自动保存', + 'SETTING_AUTOSAVE_INTERVAL':'自动保存间隔(秒)', + 'SETTING_UI_ANIM_ON':'界面动画', + 'SETTING_SFX_EDITOR':'编辑器音效', + 'SETTING_SFX_UI':'界面音效', + 'SETTING_IGNORE_PLAYBACK_ERRORS':'播放时忽略错误', + 'SETTING_SHOW_GRID_2D':'显示二维网格', + 'SETTING_SHOW_GRID_3D':'显示三维网格', + 'SETTING_BRP_TEXT_EXPORT':'同时导出内存JSON', + 'SETTING_EXPORT_FILENAME':'文件名', + 'SETTING_TOAST_TOP':'提示显示在顶部', + 'SETTING_FANCY_AUTOSAVE':'精美自动保存', + 'SETTING_TOAST_DURATION':'提示持续时间(秒)', + 'SETTING_EPIC_MODE':'史诗模式', + 'SETTING_DEBUG_HEADER':'调试', + 'SETTING_DUMP_MEMORY':'将内存转储到日志', + 'SETTING_DUMP_TIMELINE':'将时间线转储到日志', + 'SETTING_ASPECT_RATIO':'宽高比', + 'SETTING_FILL_ASPECT_RATIO':'填充画面外区域', + 'COMING_SOON':('即将推出!','还没实现呢'), + }, + 'Spanish': { + 'SETTINGS':'Ajustes','EDIT_BUTTON':'Editar', + 'EVENT_BUTTON_OFF':'Evento','EVENT_BUTTON_ON':'Volver', + 'DONE':'Hecho','LOAD':'Cargar','SET':'Fijar','POP':'Quitar', + 'KEYS':'Claves','NAME':'Nombre','TYPE':'Tipo','VALUE':'Valor', + 'POSITION':'Posición','TARGET':'Objetivo','PREVIEW':'Vista previa', + 'STOP':'Detener','COPY':'Copiar','RUN':'Ejecutar', + 'PASTE':'Pegar','SEED':'Semilla','CODE':'Código', + 'NEXT':'Siguiente','LOOP':'Bucle','EVERYWHERE':'En todas partes', + 'ERROR':'¡Error!', + 'MENUS':('Guardar y salir','Borrar sesión','Cargar semilla', + 'Copiar semilla','Cambiar editor','Grabar BRP', + 'Vista previa amplia'), + 'EVENTS':{'Node':'Crear nodo de escena', + 'Camera':'Ajustar cámara','Sound':'Reproducir sonido', + 'FX':'Disparar efecto','Map':'Controlar mapa', + 'Preset':'Cargar preajuste','Code':'Código personalizado', + 'Seed':'Semilla del proyecto'}, + 'SETTING_ENTRY_DURATION':'Duración de entrada predeterminada', + 'SETTING_ANIM_SPEED':'Velocidad de animación UI', + 'SETTING_BASE_OPACITY':'Opacidad base', + 'SETTING_TEXT_OPACITY':'Opacidad del texto', + 'SETTING_THEME':'Tema','SETTING_LANGUAGE':'Idioma', + 'SETTING_AUTOSAVE_ON':'Autoguardado', + 'SETTING_AUTOSAVE_INTERVAL':'Intervalo de autoguardado (s)', + 'SETTING_UI_ANIM_ON':'Animaciones de UI', + 'SETTING_SFX_EDITOR':'SFX del editor', + 'SETTING_SFX_UI':'SFX de la UI', + 'SETTING_IGNORE_PLAYBACK_ERRORS': + 'Ignorar errores durante la reproducción', + 'SETTING_SHOW_GRID_2D':'Mostrar cuadrícula 2D', + 'SETTING_SHOW_GRID_3D':'Mostrar cuadrícula 3D', + 'SETTING_BRP_TEXT_EXPORT':'También exportar JSON de memoria', + 'SETTING_EXPORT_FILENAME':'Nombre de archivo', + 'SETTING_TOAST_TOP':'Notificación arriba', + 'SETTING_FANCY_AUTOSAVE':'Autoguardado elegante', + 'SETTING_TOAST_DURATION':'Duración de notificación (s)', + 'SETTING_EPIC_MODE':'Modo épico', + 'SETTING_DEBUG_HEADER':'Depuración', + 'SETTING_DUMP_MEMORY':'Volcar memoria al registro', + 'SETTING_DUMP_TIMELINE':'Volcar línea de tiempo al registro', + 'SETTING_ASPECT_RATIO':'Relación de aspecto', + 'SETTING_FILL_ASPECT_RATIO':'Rellenar fuera del cuadro', + 'COMING_SOON':('¡Próximamente!','Aún no implementado'), + }, + 'German': { + 'SETTINGS':'Einstellungen','EDIT_BUTTON':'Bearbeiten', + 'EVENT_BUTTON_OFF':'Ereignis','EVENT_BUTTON_ON':'Zurück', + 'DONE':'Fertig','LOAD':'Laden','SET':'Setzen', + 'POP':'Entfernen','KEYS':'Schlüssel','NAME':'Name', + 'TYPE':'Typ','VALUE':'Wert','POSITION':'Position', + 'TARGET':'Ziel','PREVIEW':'Vorschau','STOP':'Stopp', + 'COPY':'Kopieren','RUN':'Ausführen','PASTE':'Einfügen', + 'SEED':'Seed','CODE':'Code','NEXT':'Weiter','LOOP':'Schleife', + 'EVERYWHERE':'Überall','ERROR':'Fehler!', + 'MENUS':('Speichern und beenden','Sitzung löschen', + 'Seed laden','Seed kopieren','Editor wechseln', + 'BRP aufnehmen','Breite Vorschau'), + 'EVENTS':{'Node':'Szenenknoten erstellen', + 'Camera':'Kamera anpassen','Sound':'Sound abspielen', + 'FX':'Effekt auslösen','Map':'Karte steuern', + 'Preset':'Voreinstellung laden', + 'Code':'Eigener Code','Seed':'Projekt-Seed'}, + 'SETTING_ENTRY_DURATION':'Standard-Eintragsdauer', + 'SETTING_ANIM_SPEED':'UI-Animationsgeschwindigkeit', + 'SETTING_BASE_OPACITY':'Basisdeckkraft', + 'SETTING_TEXT_OPACITY':'Textdeckkraft', + 'SETTING_THEME':'Thema','SETTING_LANGUAGE':'Sprache', + 'SETTING_AUTOSAVE_ON':'Autospeichern', + 'SETTING_AUTOSAVE_INTERVAL':'Autospeicher-Intervall (s)', + 'SETTING_UI_ANIM_ON':'UI-Animationen', + 'SETTING_SFX_EDITOR':'Editor-SFX', + 'SETTING_SFX_UI':'UI-SFX', + 'SETTING_IGNORE_PLAYBACK_ERRORS': + 'Fehler während der Wiedergabe ignorieren', + 'SETTING_SHOW_GRID_2D':'2D-Raster anzeigen', + 'SETTING_SHOW_GRID_3D':'3D-Raster anzeigen', + 'SETTING_BRP_TEXT_EXPORT':'Auch Speicher-JSON exportieren', + 'SETTING_EXPORT_FILENAME':'Dateiname', + 'SETTING_TOAST_TOP':'Benachrichtigung oben', + 'SETTING_FANCY_AUTOSAVE':'Schickes Autospeichern', + 'SETTING_TOAST_DURATION':'Benachrichtigungsdauer (s)', + 'SETTING_EPIC_MODE':'Epischer Modus', + 'SETTING_DEBUG_HEADER':'Debug', + 'SETTING_DUMP_MEMORY':'Speicher ins Log schreiben', + 'SETTING_DUMP_TIMELINE':'Zeitleiste ins Log schreiben', + 'SETTING_ASPECT_RATIO':'Seitenverhältnis', + 'SETTING_FILL_ASPECT_RATIO':'Außerhalb des Rahmens füllen', + 'COMING_SOON':('Demnächst!','Noch nicht implementiert'), + }, + 'Portuguese': { + 'SETTINGS':'Configurações','EDIT_BUTTON':'Editar', + 'EVENT_BUTTON_OFF':'Evento','EVENT_BUTTON_ON':'Voltar', + 'DONE':'Concluído','LOAD':'Carregar','SET':'Definir', + 'POP':'Remover','KEYS':'Chaves','NAME':'Nome','TYPE':'Tipo', + 'VALUE':'Valor','POSITION':'Posição','TARGET':'Alvo', + 'PREVIEW':'Pré-visualização','STOP':'Parar','COPY':'Copiar', + 'RUN':'Executar','PASTE':'Colar','SEED':'Semente', + 'CODE':'Código','NEXT':'Próximo','LOOP':'Loop', + 'EVERYWHERE':'Em todo lugar','ERROR':'Erro!', + 'MENUS':('Salvar e sair','Limpar sessão','Carregar semente', + 'Copiar semente','Trocar editor','Gravar BRP', + 'Pré-visualização ampla'), + 'EVENTS':{'Node':'Criar nó de cena','Camera':'Ajustar câmera', + 'Sound':'Tocar som','FX':'Disparar efeito', + 'Map':'Controlar mapa','Preset':'Carregar predefinição', + 'Code':'Código personalizado','Seed':'Semente do projeto'}, + 'SETTING_ENTRY_DURATION':'Duração padrão da entrada', + 'SETTING_ANIM_SPEED':'Velocidade de animação da UI', + 'SETTING_BASE_OPACITY':'Opacidade base', + 'SETTING_TEXT_OPACITY':'Opacidade do texto', + 'SETTING_THEME':'Tema','SETTING_LANGUAGE':'Idioma', + 'SETTING_AUTOSAVE_ON':'Salvamento automático', + 'SETTING_AUTOSAVE_INTERVAL':'Intervalo de salvamento (s)', + 'SETTING_UI_ANIM_ON':'Animações da UI', + 'SETTING_SFX_EDITOR':'SFX do editor', + 'SETTING_SFX_UI':'SFX da UI', + 'SETTING_IGNORE_PLAYBACK_ERRORS': + 'Ignorar erros durante a reprodução', + 'SETTING_SHOW_GRID_2D':'Mostrar grade 2D', + 'SETTING_SHOW_GRID_3D':'Mostrar grade 3D', + 'SETTING_BRP_TEXT_EXPORT':'Também exportar JSON de memória', + 'SETTING_EXPORT_FILENAME':'Nome do arquivo', + 'SETTING_TOAST_TOP':'Notificação no topo', + 'SETTING_FANCY_AUTOSAVE':'Salvamento automático chique', + 'SETTING_TOAST_DURATION':'Duração da notificação (s)', + 'SETTING_EPIC_MODE':'Modo épico', + 'SETTING_DEBUG_HEADER':'Depuração', + 'SETTING_DUMP_MEMORY':'Despejar memória no log', + 'SETTING_DUMP_TIMELINE':'Despejar linha do tempo no log', + 'SETTING_ASPECT_RATIO':'Proporção', + 'SETTING_FILL_ASPECT_RATIO':'Preencher fora do quadro', + 'COMING_SOON':('Em breve!','Ainda não implementado'), + }, + 'Russian': { + 'SETTINGS':'Настройки','EDIT_BUTTON':'Изменить', + 'EVENT_BUTTON_OFF':'Событие','EVENT_BUTTON_ON':'Назад', + 'DONE':'Готово','LOAD':'Загрузить','SET':'Задать', + 'POP':'Удалить','KEYS':'Ключи','NAME':'Имя','TYPE':'Тип', + 'VALUE':'Значение','POSITION':'Позиция','TARGET':'Цель', + 'PREVIEW':'Превью','STOP':'Стоп','COPY':'Копировать', + 'RUN':'Запуск','PASTE':'Вставить','SEED':'Сид', + 'CODE':'Код','NEXT':'Далее','LOOP':'Цикл', + 'EVERYWHERE':'Везде','ERROR':'Ошибка!', + 'MENUS':('Сохранить и выйти','Очистить сессию', + 'Загрузить сид','Копировать сид', + 'Сменить редактор','Записать BRP', + 'Широкий просмотр'), + 'EVENTS':{'Node':'Создать узел сцены', + 'Camera':'Настроить камеру','Sound':'Воспроизвести звук', + 'FX':'Запустить эффект','Map':'Управлять картой', + 'Preset':'Загрузить пресет','Code':'Свой код', + 'Seed':'Сид проекта'}, + 'SETTING_ENTRY_DURATION':'Длительность записи по умолчанию', + 'SETTING_ANIM_SPEED':'Скорость анимации интерфейса', + 'SETTING_BASE_OPACITY':'Базовая непрозрачность', + 'SETTING_TEXT_OPACITY':'Непрозрачность текста', + 'SETTING_THEME':'Тема','SETTING_LANGUAGE':'Язык', + 'SETTING_AUTOSAVE_ON':'Автосохранение', + 'SETTING_AUTOSAVE_INTERVAL':'Интервал автосохранения (с)', + 'SETTING_UI_ANIM_ON':'Анимации интерфейса', + 'SETTING_SFX_EDITOR':'Звуки редактора', + 'SETTING_SFX_UI':'Звуки интерфейса', + 'SETTING_IGNORE_PLAYBACK_ERRORS': + 'Игнорировать ошибки при воспроизведении', + 'SETTING_SHOW_GRID_2D':'Показать 2D-сетку', + 'SETTING_SHOW_GRID_3D':'Показать 3D-сетку', + 'SETTING_BRP_TEXT_EXPORT':'Также экспортировать JSON памяти', + 'SETTING_EXPORT_FILENAME':'Имя файла', + 'SETTING_TOAST_TOP':'Уведомление сверху', + 'SETTING_FANCY_AUTOSAVE':'Красивое автосохранение', + 'SETTING_TOAST_DURATION':'Длительность уведомления (с)', + 'SETTING_EPIC_MODE':'Эпичный режим', + 'SETTING_DEBUG_HEADER':'Отладка', + 'SETTING_DUMP_MEMORY':'Сбросить память в лог', + 'SETTING_DUMP_TIMELINE':'Сбросить таймлайн в лог', + 'SETTING_ASPECT_RATIO':'Соотношение сторон', + 'SETTING_FILL_ASPECT_RATIO':'Заполнять за пределами кадра', + 'COMING_SOON':('Скоро!','Пока не реализовано'), + }, + 'Korean': { + 'SETTINGS':'설정','EDIT_BUTTON':'편집', + 'EVENT_BUTTON_OFF':'이벤트','EVENT_BUTTON_ON':'뒤로', + 'DONE':'완료','LOAD':'불러오기','SET':'설정','POP':'제거', + 'KEYS':'키','NAME':'이름','TYPE':'유형','VALUE':'값', + 'POSITION':'위치','TARGET':'대상','PREVIEW':'미리보기', + 'STOP':'정지','COPY':'복사','RUN':'실행','PASTE':'붙여넣기', + 'SEED':'시드','CODE':'코드','NEXT':'다음','LOOP':'반복', + 'EVERYWHERE':'모든 곳','ERROR':'오류!', + 'MENUS':('저장 후 종료','세션 지우기','시드 불러오기', + '시드 복사','에디터 전환','BRP 녹화', + '와이드 미리보기'), + 'EVENTS':{'Node':'씬 노드 생성','Camera':'카메라 조정', + 'Sound':'사운드 재생','FX':'이펙트 실행', + 'Map':'맵 제어','Preset':'프리셋 불러오기', + 'Code':'사용자 코드','Seed':'프로젝트 시드'}, + 'SETTING_ENTRY_DURATION':'기본 항목 길이', + 'SETTING_ANIM_SPEED':'UI 애니메이션 속도', + 'SETTING_BASE_OPACITY':'기본 불투명도', + 'SETTING_TEXT_OPACITY':'텍스트 불투명도', + 'SETTING_THEME':'테마','SETTING_LANGUAGE':'언어', + 'SETTING_AUTOSAVE_ON':'자동 저장', + 'SETTING_AUTOSAVE_INTERVAL':'자동 저장 간격(초)', + 'SETTING_UI_ANIM_ON':'UI 애니메이션', + 'SETTING_SFX_EDITOR':'에디터 효과음', + 'SETTING_SFX_UI':'UI 효과음', + 'SETTING_IGNORE_PLAYBACK_ERRORS':'재생 중 오류 무시', + 'SETTING_SHOW_GRID_2D':'2D 그리드 표시', + 'SETTING_SHOW_GRID_3D':'3D 그리드 표시', + 'SETTING_BRP_TEXT_EXPORT':'메모리 JSON도 내보내기', + 'SETTING_EXPORT_FILENAME':'파일 이름', + 'SETTING_TOAST_TOP':'알림을 상단에 표시', + 'SETTING_FANCY_AUTOSAVE':'화려한 자동 저장', + 'SETTING_TOAST_DURATION':'알림 지속 시간(초)', + 'SETTING_EPIC_MODE':'에픽 모드', + 'SETTING_DEBUG_HEADER':'디버그', + 'SETTING_DUMP_MEMORY':'메모리를 로그로 덤프', + 'SETTING_DUMP_TIMELINE':'타임라인을 로그로 덤프', + 'SETTING_ASPECT_RATIO':'화면 비율', + 'SETTING_FILL_ASPECT_RATIO':'프레임 밖 채우기', + 'COMING_SOON':('곧 공개!','아직 구현되지 않음'), + }, + 'Hindi': { + 'SETTINGS':'सेटिंग्स','EDIT_BUTTON':'संपादित करें', + 'EVENT_BUTTON_OFF':'इवेंट','EVENT_BUTTON_ON':'वापस', + 'DONE':'हो गया','LOAD':'लोड करें','SET':'सेट करें', + 'POP':'हटाएं','KEYS':'कीज़','NAME':'नाम','TYPE':'प्रकार', + 'VALUE':'मान','POSITION':'स्थिति','TARGET':'लक्ष्य', + 'PREVIEW':'पूर्वावलोकन','STOP':'रोकें','COPY':'कॉपी करें', + 'RUN':'चलाएं','PASTE':'पेस्ट करें','SEED':'सीड', + 'CODE':'कोड','NEXT':'अगला','LOOP':'लूप', + 'EVERYWHERE':'हर जगह','ERROR':'त्रुटि!', + 'MENUS':('सेव करके बाहर निकलें','सत्र साफ़ करें', + 'सीड लोड करें','सीड कॉपी करें','एडिटर बदलें', + 'BRP रिकॉर्ड करें','वाइड पूर्वावलोकन'), + 'EVENTS':{'Node':'सीन नोड बनाएं','Camera':'कैमरा समायोजित करें', + 'Sound':'ध्वनि चलाएं','FX':'इफ़ेक्ट चलाएं', + 'Map':'मैप नियंत्रित करें','Preset':'प्रीसेट लोड करें', + 'Code':'कस्टम कोड','Seed':'प्रोजेक्ट सीड'}, + 'SETTING_ENTRY_DURATION':'डिफ़ॉल्ट एंट्री अवधि', + 'SETTING_ANIM_SPEED':'UI एनिमेशन गति', + 'SETTING_BASE_OPACITY':'आधार अपारदर्शिता', + 'SETTING_TEXT_OPACITY':'टेक्स्ट अपारदर्शिता', + 'SETTING_THEME':'थीम','SETTING_LANGUAGE':'भाषा', + 'SETTING_AUTOSAVE_ON':'ऑटोसेव', + 'SETTING_AUTOSAVE_INTERVAL':'ऑटोसेव अंतराल (से)', + 'SETTING_UI_ANIM_ON':'UI एनिमेशन', + 'SETTING_SFX_EDITOR':'एडिटर SFX', + 'SETTING_SFX_UI':'UI SFX', + 'SETTING_IGNORE_PLAYBACK_ERRORS': + 'प्लेबैक के दौरान त्रुटियां अनदेखा करें', + 'SETTING_SHOW_GRID_2D':'2D ग्रिड दिखाएं', + 'SETTING_SHOW_GRID_3D':'3D ग्रिड दिखाएं', + 'SETTING_BRP_TEXT_EXPORT':'मेमोरी JSON भी एक्सपोर्ट करें', + 'SETTING_EXPORT_FILENAME':'फ़ाइल नाम', + 'SETTING_TOAST_TOP':'सूचना ऊपर दिखाएं', + 'SETTING_FANCY_AUTOSAVE':'स्टाइलिश ऑटोसेव', + 'SETTING_TOAST_DURATION':'सूचना अवधि (से)', + 'SETTING_EPIC_MODE':'एपिक मोड', + 'SETTING_DEBUG_HEADER':'डीबग', + 'SETTING_DUMP_MEMORY':'मेमोरी को लॉग में डंप करें', + 'SETTING_DUMP_TIMELINE':'टाइमलाइन को लॉग में डंप करें', + 'SETTING_ASPECT_RATIO':'आस्पेक्ट रेशियो', + 'SETTING_FILL_ASPECT_RATIO':'फ्रेम के बाहर भरें', + 'COMING_SOON':('जल्द आ रहा है!','अभी लागू नहीं हुआ'), + }, + 'Italian': { + 'SETTINGS':'Impostazioni','EDIT_BUTTON':'Modifica', + 'EVENT_BUTTON_OFF':'Evento','EVENT_BUTTON_ON':'Indietro', + 'DONE':'Fatto','LOAD':'Carica','SET':'Imposta', + 'POP':'Rimuovi','KEYS':'Chiavi','NAME':'Nome','TYPE':'Tipo', + 'VALUE':'Valore','POSITION':'Posizione','TARGET':'Obiettivo', + 'PREVIEW':'Anteprima','STOP':'Ferma','COPY':'Copia', + 'RUN':'Esegui','PASTE':'Incolla','SEED':'Seed', + 'CODE':'Codice','NEXT':'Avanti','LOOP':'Ciclo', + 'EVERYWHERE':'Ovunque','ERROR':'Errore!', + 'MENUS':('Salva ed esci','Cancella sessione','Carica seed', + 'Copia seed','Cambia editor','Registra BRP', + 'Anteprima ampia'), + 'EVENTS':{'Node':'Crea nodo scena','Camera':'Regola camera', + 'Sound':'Riproduci suono','FX':'Avvia effetto', + 'Map':'Controlla mappa','Preset':'Carica preset', + 'Code':'Codice personalizzato','Seed':'Seed del progetto'}, + 'SETTING_ENTRY_DURATION':'Durata voce predefinita', + 'SETTING_ANIM_SPEED':'Velocità animazioni UI', + 'SETTING_BASE_OPACITY':'Opacità base', + 'SETTING_TEXT_OPACITY':'Opacità del testo', + 'SETTING_THEME':'Tema','SETTING_LANGUAGE':'Lingua', + 'SETTING_AUTOSAVE_ON':'Salvataggio automatico', + 'SETTING_AUTOSAVE_INTERVAL':'Intervallo autosalvataggio (s)', + 'SETTING_UI_ANIM_ON':'Animazioni UI', + 'SETTING_SFX_EDITOR':'SFX editor', + 'SETTING_SFX_UI':'SFX UI', + 'SETTING_IGNORE_PLAYBACK_ERRORS': + "Ignora errori durante la riproduzione", + 'SETTING_SHOW_GRID_2D':'Mostra griglia 2D', + 'SETTING_SHOW_GRID_3D':'Mostra griglia 3D', + 'SETTING_BRP_TEXT_EXPORT':'Esporta anche JSON di memoria', + 'SETTING_EXPORT_FILENAME':'Nome file', + 'SETTING_TOAST_TOP':'Notifica in alto', + 'SETTING_FANCY_AUTOSAVE':'Autosalvataggio elegante', + 'SETTING_TOAST_DURATION':'Durata notifica (s)', + 'SETTING_EPIC_MODE':'Modalità epica', + 'SETTING_DEBUG_HEADER':'Debug', + 'SETTING_DUMP_MEMORY':'Scarica memoria nel log', + 'SETTING_DUMP_TIMELINE':'Scarica timeline nel log', + 'SETTING_ASPECT_RATIO':'Proporzioni', + 'SETTING_FILL_ASPECT_RATIO':"Riempi fuori dall'inquadratura", + 'COMING_SOON':('Prossimamente!','Non ancora implementato'), + }, + 'Bruh': { + 'SETTINGS':'settings ig','EDIT_BUTTON':'edit', + 'EVENT_BUTTON_OFF':'event','EVENT_BUTTON_ON':'bye', + 'DONE':'done ✅','LOAD':'load','SET':'set','POP':'yeet it', + 'KEYS':'keys','NAME':'name','TYPE':'type','VALUE':'value', + 'POSITION':'position','TARGET':'target','PREVIEW':'preview', + 'STOP':'stop','COPY':'copy','RUN':'run it','PASTE':'paste', + 'SEED':'seed','CODE':'code','NEXT':'next','LOOP':'loop', + 'EVERYWHERE':'literally everywhere','ERROR':'bro it broke 💀', + 'MENUS':('save n dip','wipe the session lol', + 'load a seed ig','copy the seed', + 'swap editors','record some brp fr', + 'wide preview (chefs kiss)'), + 'EVENTS':{'Node':'spawn a scene node ig', + 'Camera':'nudge the camera bro', + 'Sound':'blast a sound', + 'FX':'send it (fx edition)', + 'Map':'mess with the map', + 'Preset':'load a preset fr', + 'Code':'ur cursed custom code', + 'Seed':'the project seed no cap'}, + 'SETTING_ENTRY_DURATION':'default entry duration ig', + 'SETTING_ANIM_SPEED':'ui anim speed (zoomies)', + 'SETTING_BASE_OPACITY':'base opacity fr fr', + 'SETTING_TEXT_OPACITY':'text opacity but make it hit different', + 'SETTING_THEME':'theme (pick ur vibe)', + 'SETTING_LANGUAGE':'language (u already found it lol)', + 'SETTING_AUTOSAVE_ON':'autosave (trust)', + 'SETTING_AUTOSAVE_INTERVAL':'autosave interval (s) bro', + 'SETTING_UI_ANIM_ON':'ui animations', + 'SETTING_SFX_EDITOR':'editor sfx go brrr', + 'SETTING_SFX_UI':'ui sfx', + 'SETTING_IGNORE_PLAYBACK_ERRORS': + 'ignore errors during playback, we ball', + 'SETTING_SHOW_GRID_2D':'show that 2d grid', + 'SETTING_SHOW_GRID_3D':'show that 3d grid', + 'SETTING_BRP_TEXT_EXPORT':'also export the memory json ez', + 'SETTING_EXPORT_FILENAME':'filename ig', + 'SETTING_TOAST_TOP':'toast goes up top', + 'SETTING_FANCY_AUTOSAVE':'fancy autosave (it slaps)', + 'SETTING_TOAST_DURATION':'toast duration (s)', + 'SETTING_EPIC_MODE':'epic mode 😳', + 'SETTING_DEBUG_HEADER':'debug (send logs to the group chat)', + 'SETTING_DUMP_MEMORY':'dump memory to the log, no cap', + 'SETTING_DUMP_TIMELINE':'dump the timeline to the log too', + 'SETTING_ASPECT_RATIO':'aspect ratio', + 'SETTING_FILL_ASPECT_RATIO':'fill outside the frame ig', + 'COMING_SOON':('soon (tm)', + 'aka not done yet, screenshot this and send to the telegram group'), + }, +} + +class Const: + BA_DATA = join( + dirname( + bui.app.env.cache_directory + ), 'ballistica_files', 'ba_data' + ) + REPLAYS = join( + dirname( + dirname( + bui.app.env.cache_directory + ) + ), 'files', 'bombsquad_config', 'replays' + ) + STOCK_REPLAY = '__lastReplay.brp' + EXPORT_PREFIX = 'movi_' + EXPORT_SUFFIX = '.brp' + CONFIG_HEAD = '# MOVI ' + CONFIG_PREFIX = 'movi_' + CONFIG_DEV_KEY = 'Show Dev Console Button' + CONFIG_FPS_KEY = 'Show FPS' + EXIT_BOUNDS = (0,0,0,0,35,0) + BA_LAG_BIG = 1.5 + BA_LAG = 0.04 + BA_LAG_SMALL = 0.01 + INVISIBLE = (0,0,0,0) + SCALE_BA = { + bui.UIScale.SMALL: 1.275, + bui.UIScale.MEDIUM: 1, + bui.UIScale.LARGE: 0.764 + } + SCALE_REAL = { + bui.UIScale.SMALL: 0.93, + bui.UIScale.MEDIUM: 0.9, + bui.UIScale.LARGE: 0.7 + } + SKIN = 'white' + EMPTY = 'empty' + SHADOW = 'softRect' + GLOW = 'uniform' + ALIGN = 'center' + KEY = 'circleZigZag' + PLAY_BUTTON = 'PLAY_BUTTON' + PAUSE_BUTTON = 'PAUSE_BUTTON' + CONTROLS = ( + ('PLAY_BUTTON','PAUSE_BUTTON'), + 'BACK' + ) + PIN_POINT = 'PLAY_STATION_CROSS_BUTTON' + TOOLS = ( + 'RIGHT_ARROW', + 'LEFT_ARROW', + 'FAST_FORWARD_BUTTON', + 'REWIND_BUTTON', + 'UP_ARROW', + 'DOWN_ARROW', + 'DPAD_CENTER_BUTTON', + 'PLAY_STATION_CROSS_BUTTON' + ) + EVENT_KEYS = { + 0: (0,3), + 2: (2,), + 6: (1,) + } + CAMERA_TOOLS = ( + '-', + 'LEFT_ARROW', + 'LEFT_BUTTON', + 'DOWN_ARROW', + 'DPAD_CENTER_BUTTON', + 'UP_ARROW', + '+', + 'RIGHT_ARROW', + 'RIGHT_BUTTON' + ) + OK_SOUND = 'deek' + BAD_SOUND = 'block' + ACTION_SOUND = 'gunCocking' + GOOD_SOUND = 'dingSmall' + TRIANGLE = 'PLAY_STATION_TRIANGLE_BUTTON' + SQUARE = 'PLAY_STATION_SQUARE_BUTTON' + CIRCLE = 'PLAY_STATION_CIRCLE_BUTTON' + BACK = 'BACK' + THEME_ICON = 'LOGO_FLAT' + BLAME_CHARSET = " ()',?ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + DO_NOTHING = lambda:None + BLAME = lambda: ( + '{Wp48S^xk9=GL@E0stWa8~^|S5YJf5;0J63A6)B{gpqX' + 'dk#@9?UBPn;2%V#jMSPv_1XgyAJ^ZvYCOsN&;+crKe;d^f*1&xO5^OsH3p{1PWZr2DvZX#' + 'Q(asi+>+1LJ3~Qw$fLY?PWe0Gz8+_A7Di@~MP;^#5AkfTHb%IeI)Caw!BokT0(B=(PBwFM' + 'J`y4mX1FWa8Uc<=Zf?~Q^yB!g9w1-Ks>f=OJA!hqouOgMGKK+K<}ReB;Y*XYGvVu?pxK;0' + '%D0$KY!tzmUe!969j2GtKACX6T0Rc4wuI|fIo}Q1>mcXg)cg(+@UNr_t_FI@@' + 'K-z`vn!j^;vk3jjV~X?AF#}P5-lM@%Cm(E@*d0FbkeodJ^wCY=H}(suiou^zL}})0AxqiM' + 'f9o>9`gZTUUMFRuXQ1^80Sed9lNtj1TBkASgF`=Ob9Ll~2$YYYb#|Q;&`99IjGc~in__wo' + 'iPNw?$28MM-FVrS%b&Y$h9=Oi+t0%a%YIX}>Gr?}vc^toT;Juz3GE5ivr@NgNaO&Jl5l2R;2eGynkua*2)$i2kE1SDu&' + '{fQ=D7ZZ?YLF;Krit7YFA*F^Ze6aAM--qkd2lcLC{U{DR)fW57r#Z@fs1Nb(eQY`_!vq?shD>Bk5E?lhEEM' + 'F*ga;#^a?rEOgo?)Hl*mC@H&L_`LBwqpgQQ934D*00FxQ?G69{MaZMAvBYQl0ssI200dcD' + ) + FONT_METRICS = loads(decompress(b85decode( + 'c$`&KXG0Y+5d13|QHmH-F6}H>Y4+Zpf)znQl&Xlo-DI+P+&5&WpGSo5T81f7Sh9X0Wp`O{i#L#6{d4-{?49y`kM@PSwqgGi4)b' + '&hx85~{14a6B!>@eacPxlszN#-^#*EZF8Ow=89E^wj641?4y{Uxl4!uPW1' + 'lf>5@^aSH;(OoDSTNUoXb&RwX_qpH$4j)2ESsL*OtfiHC46-?U!jJfrcRm' + '}Q_&GQ#d;ve5s~h_gl%Y^x4GUi*r$mKs#-|;PL?5LO)bn;syh8(B@(MPS-' + 'oN5&`dt6_N2g+p)WnHcNI_Oz1YuD1' + '-C|73{&HV;B5Z_Bx-M39-W81smf|hN65 1 else pal['text'] + Color.SHADOW = pal['shadow'] + base_luma = sum(pal['base'])/3 + Color.SHADOW_OPACITY = max(0.10,min(0.5,0.5-base_luma*0.42)) + Color.TEMP = (2.2,1.35,0.15) if base_luma < 0.5 else (0.55,0.32,0.02) + + @staticmethod + def apply_all(): + """Push persisted settings into the live globals. Safe to + call as often as needed - cheap, idempotent.""" + Color.OPACITY = Settings.get('base_opacity') + Color.TEXT_OPACITY = Settings.get('text_opacity') + Settings.apply_theme() + +# ba_meta export bascenev1.GameActivity +class Movi(bs.GameActivity[bs.Player,bs.Team]): + name = Strings.MAP_TITLE + description = Strings.MAP_DESCRIPTION + get_availabe_settings = lambda s:[] + supports_session_type = lambda s:True + get_supported_maps = lambda s:bs.app.classic.getmaps('melee') + get_instance_description = lambda s: Strings.INSTANCE_DESCRIPTION + get_instance_description_short = lambda s: Strings.INSTANCE_DESCRIPTION_SHORT + + @classmethod + def recreate(cls): + bs.new_host_session(cls.sessiontype) + session = bs.get_foreground_host_session() + with session.context: + act = bs.newactivity(Movi,cls.settings) + session.setactivity(act) + + def __init__(s, settings): + super().__init__(settings) + type(s).settings = settings + session = bs.get_foreground_host_session() + type(s).sessiontype = ( + session.use_teams and + bs.DualTeamSession or + bs.FreeForAllSession + ) + s.default_music = None + s.editor = None + + def ensure(s): + if not s.editor: + s.editor = type(s).INS = Editor( + map=type(s).settings['map'] + ) + s.make_ui() + + def on_player_join(s,p): + s.ensure() + s.editor and s.editor.schedule_on_ui( + lambda: s.editor.toast( + Format.WELCOME( + p.sessionplayer.getname() + ) + ) + ) + + def on_begin(s): + s.ensure() + + def make_ui(s): + ba.pushcall(ba.CallPartial( + s.editor.make + ),raw=True) + + def kill_ui(s): + ba.pushcall(s.editor.kill,raw=True) + +class MoviSubsystem(ba.AppSubsystem): + def on_screen_size_change(s): + Editor._call('on_resize') + def on_ui_scale_change(s): + Editor._call('on_rescale') + +# brobord collide grass +# ba_meta require api 9 +# ba_meta export babase.Plugin +class byBordd(ba.Plugin): + def __init__(s): + ba.app.register_subsystem(MoviSubsystem()) + +class _BSWrapper: + def __init__(self, original_module, newnode_func): + self._original = original_module + self.newnode = newnode_func + + def __getattr__(self, name): + return getattr(self._original, name) + + def __setattr__(self, name, value): + if name in ('_original', 'newnode'): + object.__setattr__(self, name, value) + else: + setattr(self._original, name, value) + + +def _make_tracked_spaz(OriginalSpaz, runner): + class TrackedSpaz(OriginalSpaz): + def __init__(inner_self, *args, **kwargs): + super().__init__(*args, **kwargs) + runner.created_actors.append(inner_self) + return TrackedSpaz + + +class CodeRunner: + _SHARED = {} + + def __init__(self, on_error=None, parent_runner=None): + self.on_error = on_error + self.parent_runner = parent_runner + + if parent_runner: + self.namespace = parent_runner.namespace + self.created_nodes = parent_runner.created_nodes + self.created_actors = parent_runner.created_actors + self._tracked_spaz = None + else: + self.namespace = {} + self.created_nodes = [] + self.created_actors = [] + self._tracked_spaz = None + + self.stdout_capture = StringIO() + self.stderr_capture = StringIO() + self.running = False + self.stop_flag = Event() + self.main_thread = None + self.children = [] + + def _terminate_thread(self, thread): + if not thread.is_alive(): + return + try: + exc = py_object(SystemExit) + res = pythonapi.PyThreadState_SetAsyncExc(c_long(thread.ident), exc) + if res > 1: + pythonapi.PyThreadState_SetAsyncExc(thread.ident, None) + except: + pass + + def _execute_code(self, code_string): + import bascenev1 as bs + import bascenev1lib as bsl + from bascenev1lib.actor.spaz import Spaz as OriginalSpaz + import bauiv1 as bui + import babase as ba + import _babase as _ba + import math + import random + + original_newnode = bs.newnode + + def tracked_newnode(*args, **kwargs): + node = original_newnode(*args, **kwargs) + self.created_nodes.append(node) + return node + + root = self.parent_runner or self + if root._tracked_spaz is None: + root._tracked_spaz = _make_tracked_spaz(OriginalSpaz, root) + TrackedSpaz = root._tracked_spaz + + bs_wrapped = _BSWrapper(bs, tracked_newnode) + + self.namespace.update({ + '__stop_flag__': self.stop_flag, + 'bascenev1': bs_wrapped, + 'bascenev1lib': bsl, + 'bauiv1': bui, + 'babase': ba, + '_babase': _ba, + 'bs': bs_wrapped, + 'bsl': bsl, + 'bui': bui, + 'ba': ba, + '_ba': _ba, + 'Spaz': TrackedSpaz, + 'math': math, + 'random': random, + 'Bubble': Bubble, + '_SHARED': CodeRunner._SHARED + }) + + try: + with bs.get_foreground_host_activity().context: + with redirect_stdout(self.stdout_capture), redirect_stderr(self.stderr_capture): + exec(code_string, self.namespace) + except Exception as e: + if callable(self.on_error): + self.on_error(e) + else: + print(f"Error in user code: {e}") + print(format_exc()) + + def _cleanup_all(self): + """Delete all tracked actors and nodes within the activity context""" + import bascenev1 as bs + + try: + with bs.get_foreground_host_activity().context: + for actor in self.created_actors: + try: + if actor.is_alive(): + actor.handlemessage(bs.DieMessage(immediate=True)) + except: + pass + self.created_actors.clear() + + for node in self.created_nodes: + try: + if node.exists(): + node.delete() + except: + pass + self.created_nodes.clear() + except: + self.created_actors.clear() + self.created_nodes.clear() + + def _runner(self, code_string): + try: + bs.pushcall(lambda: self._execute_code(code_string), from_other_thread=True) + except SystemExit: + pass + except Exception as e: + if callable(self.on_error): + self.on_error(e) + else: + print(f"Error in user code: {e}") + print(format_exc()) + finally: + self.running = False + + def on_start(self, code_string): + if not self.parent_runner: + self.namespace = {} + self.created_nodes = [] + self.created_actors = [] + + self.stdout_capture = StringIO() + self.stderr_capture = StringIO() + self.stop_flag.clear() + self.running = True + self.main_thread = Thread(target=self._runner, args=(code_string,), daemon=True) + self.main_thread.start() + + def spawn_child(self, code_string): + """Create and start a child runner that shares this namespace""" + child = CodeRunner( + on_error=self.on_error, + parent_runner=self + ) + self.children.append(child) + child.on_start(code_string) + return child + + def on_end(self): + if not self.running and self.main_thread is None and not self.children: + return + + self.stop_flag.set() + + if self.main_thread and self.main_thread.is_alive(): + self.main_thread.join(timeout=0.1) + if self.main_thread and self.main_thread.is_alive(): + self._terminate_thread(self.main_thread) + self.main_thread.join(timeout=0.5) + + for child in self.children: + child.on_end() + self.children.clear() + + self._tracked_spaz = None + + if not self.parent_runner: + bs.pushcall(self._cleanup_all) + + for value in list(self.namespace.values()): + if hasattr(value, 'close') and callable(value.close): + try: + value.close() + except: + pass + + self.namespace.clear() + + self.main_thread = None + self.running = False + +def get_presets(): + """ + Return a list of Movi presets. + + Each entry is a tuple: + (event_index, display_name, description, edit_like_dict) + + Where edit_like_dict is shaped exactly like the 'edit' parameter + expected by the corresponding make_*_window function: + { + 'data': { ...fields... } + } + + event_index legend (matches Strings.EVENTS order): + 0 = Node 1 = Camera 2 = Sound 3 = FX + 4 = Map 6 = Code + + Rebuilt from scratch: every sound name, map name, character name, + and chunk/emit type below was cross-checked against the actual + BombSquad engine source, so nothing here should silently no-op or + error on a bad asset name. Code presets that fire many sounds use + a single seq=[...] list + for-loop instead of hand-unrolled + bs.AppTimer blocks, so they're much shorter and easy to edit. + """ + presets = [] + + presets.append(( + 0, + 'World Anchor', + 'Invisible dummy node at\norigin, handy to parent\nother nodes/effects to.', + { + 'data': { + 'type': 'math', + 'name': 'WorldAnchor', + 'attrs': {'input1': (0, 0, 0), 'operation': 'add'} + } + } + )) + + presets.append(( + 0, + 'Offstage Anchor', + 'Dummy node placed off to\nthe side, out of frame -\nuseful as a parking spot.', + { + 'data': { + 'type': 'math', + 'name': 'OffstageAnchor', + 'attrs': {'input1': (20, 0, 0), 'operation': 'add'} + } + } + )) + + presets.append(( + 0, + 'Basic Spaz', + 'Standard blue Spaz character\nat center stage - the safest\nstarting point.', + { + 'data': { + 'type': 'spaz', + 'name': 'Spaz', + 'attrs': { + 'character': '"Spaz"', + 'color': '(0.4, 0.5, 0.8)', + 'highlight': '(1.0, 1.0, 1.0)', + 'materials': '[_factory.spaz_material, _shared.object_material, _shared.player_material]', + 'roller_materials': '[_factory.roller_material, _shared.player_material]', + 'punch_materials': '[_factory.punch_material, _shared.attack_material]', + 'pickup_materials': '[_factory.pickup_material, _shared.pickup_material]', + 'position': '(0, 1, 0)' + } + } + } + )) + + presets.append(( + 0, + 'Neon Assassin', + 'Stealthy ninja with electric\npurple glow.', + { + 'data': { + 'type': 'spaz', + 'name': 'Shadow Strike', + 'attrs': { + 'character': '"Snake Shadow"', + 'color': '(2.2, 0.0, 0.4)', + 'highlight': '(2.5, 0.0, 4.0)', + 'materials': '[_factory.spaz_material, _shared.object_material, _shared.player_material]', + 'roller_materials': '[_factory.roller_material, _shared.player_material]', + 'punch_materials': '[_factory.punch_material, _shared.attack_material]', + 'pickup_materials': '[_factory.pickup_material, _shared.pickup_material]', + 'position': '(0, 1, 0)' + } + } + } + )) + + presets.append(( + 0, + 'Cyber Warrior', + 'Futuristic agent with intense\ncyan energy.', + { + 'data': { + 'type': 'spaz', + 'name': 'NetRunner', + 'attrs': { + 'character': '"Agent Johnson"', + 'color': '(0.0, 0.1, 0.2)', + 'highlight': '(0.0, 3.5, 5.0)', + 'materials': '[_factory.spaz_material, _shared.object_material, _shared.player_material]', + 'roller_materials': '[_factory.roller_material, _shared.player_material]', + 'punch_materials': '[_factory.punch_material, _shared.attack_material]', + 'pickup_materials': '[_factory.pickup_material, _shared.pickup_material]', + 'position': '(2, 1, 0)' + } + } + } + )) + + presets.append(( + 0, + 'Plasma Knight', + 'Medieval warrior radiating\nblue-white plasma.', + { + 'data': { + 'type': 'spaz', + 'name': 'Sir Voltage', + 'attrs': { + 'character': '"Kronk"', + 'color': '(0.1, 0.15, 0.3)', + 'highlight': '(1.5, 2.5, 6.0)', + 'materials': '[_factory.spaz_material, _shared.object_material, _shared.player_material]', + 'roller_materials': '[_factory.roller_material, _shared.player_material]', + 'punch_materials': '[_factory.punch_material, _shared.attack_material]', + 'pickup_materials': '[_factory.pickup_material, _shared.pickup_material]', + 'position': '(-2, 1, 0)' + } + } + } + )) + + presets.append(( + 0, + 'Toxic Menace', + 'Radioactive character with\nsickly green glow.', + { + 'data': { + 'type': 'spaz', + 'name': 'Biohazard', + 'attrs': { + 'character': '"Mel"', + 'color': '(0.1, 0.2, 0.0)', + 'highlight': '(2.0, 5.0, 0.0)', + 'materials': '[_factory.spaz_material, _shared.object_material, _shared.player_material]', + 'roller_materials': '[_factory.roller_material, _shared.player_material]', + 'punch_materials': '[_factory.punch_material, _shared.attack_material]', + 'pickup_materials': '[_factory.pickup_material, _shared.pickup_material]', + 'position': '(0, 1, -2)' + } + } + } + )) + + presets.append(( + 0, + 'Frosty Sentinel', + 'Icy pale character, good for\na winter/frozen-map scene.', + { + 'data': { + 'type': 'spaz', + 'name': 'Permafrost', + 'attrs': { + 'character': '"Frosty"', + 'color': '(0.5, 0.7, 0.9)', + 'highlight': '(0.8, 1.0, 1.3)', + 'materials': '[_factory.spaz_material, _shared.object_material, _shared.player_material]', + 'roller_materials': '[_factory.roller_material, _shared.player_material]', + 'punch_materials': '[_factory.punch_material, _shared.attack_material]', + 'pickup_materials': '[_factory.pickup_material, _shared.pickup_material]', + 'position': '(0, 1, 2)' + } + } + } + )) + + presets.append(( + 0, + 'Skeleton Crew', + "Bones's default look, works\nwell for a spooky scene.", + { + 'data': { + 'type': 'spaz', + 'name': 'Rattler', + 'attrs': { + 'character': '"Bones"', + 'color': '(0.8, 0.8, 0.75)', + 'highlight': '(1.0, 1.0, 1.0)', + 'materials': '[_factory.spaz_material, _shared.object_material, _shared.player_material]', + 'roller_materials': '[_factory.roller_material, _shared.player_material]', + 'punch_materials': '[_factory.punch_material, _shared.attack_material]', + 'pickup_materials': '[_factory.pickup_material, _shared.pickup_material]', + 'position': '(-2, 1, 2)' + } + } + } + )) + + presets.append(( + 0, + 'Old Wizard', + 'Grumbledorf tinted purple for\na mystical caster look.', + { + 'data': { + 'type': 'spaz', + 'name': 'The Archmage', + 'attrs': { + 'character': '"Grumbledorf"', + 'color': '(0.4, 0.1, 0.5)', + 'highlight': '(0.9, 0.5, 1.4)', + 'materials': '[_factory.spaz_material, _shared.object_material, _shared.player_material]', + 'roller_materials': '[_factory.roller_material, _shared.player_material]', + 'punch_materials': '[_factory.punch_material, _shared.attack_material]', + 'pickup_materials': '[_factory.pickup_material, _shared.pickup_material]', + 'position': '(0, 1, 0)' + } + } + } + )) + + presets.append(( + 0, + 'Jungle Explorer', + 'Zoe recolored earthy green,\ngood for an adventurer role.', + { + 'data': { + 'type': 'spaz', + 'name': 'Trailblazer', + 'attrs': { + 'character': '"Zoe"', + 'color': '(0.3, 0.5, 0.2)', + 'highlight': '(0.9, 1.0, 0.6)', + 'materials': '[_factory.spaz_material, _shared.object_material, _shared.player_material]', + 'roller_materials': '[_factory.roller_material, _shared.player_material]', + 'punch_materials': '[_factory.punch_material, _shared.attack_material]', + 'pickup_materials': '[_factory.pickup_material, _shared.pickup_material]', + 'position': '(2, 1, -2)' + } + } + } + )) + + presets.append(( + 0, + 'Deep Sea Diver', + 'Pixel recolored deep blue,\nfits an underwater/submarine\nscene.', + { + 'data': { + 'type': 'spaz', + 'name': 'Fathom', + 'attrs': { + 'character': '"Pixel"', + 'color': '(0.0, 0.2, 0.5)', + 'highlight': '(0.3, 0.6, 1.2)', + 'materials': '[_factory.spaz_material, _shared.object_material, _shared.player_material]', + 'roller_materials': '[_factory.roller_material, _shared.player_material]', + 'punch_materials': '[_factory.punch_material, _shared.attack_material]', + 'pickup_materials': '[_factory.pickup_material, _shared.pickup_material]', + 'position': '(0, 1, 0)' + } + } + } + )) + + presets.append(( + 0, + 'Old-Timer Gunslinger', + 'Jack Morgan recolored dusty\nbrown for a western scene.', + { + 'data': { + 'type': 'spaz', + 'name': 'Doc Ironside', + 'attrs': { + 'character': '"Jack Morgan"', + 'color': '(0.4, 0.3, 0.2)', + 'highlight': '(0.9, 0.8, 0.6)', + 'materials': '[_factory.spaz_material, _shared.object_material, _shared.player_material]', + 'roller_materials': '[_factory.roller_material, _shared.player_material]', + 'punch_materials': '[_factory.punch_material, _shared.attack_material]', + 'pickup_materials': '[_factory.pickup_material, _shared.pickup_material]', + 'position': '(-2, 1, -2)' + } + } + } + )) + + presets.append(( + 0, + 'Butler Bernard', + 'Bernard kept close to natural\ncolors, calm supporting-cast\nlook.', + { + 'data': { + 'type': 'spaz', + 'name': 'Bernard', + 'attrs': { + 'character': '"Bernard"', + 'color': '(0.5, 0.45, 0.4)', + 'highlight': '(1.0, 1.0, 0.95)', + 'materials': '[_factory.spaz_material, _shared.object_material, _shared.player_material]', + 'roller_materials': '[_factory.roller_material, _shared.player_material]', + 'punch_materials': '[_factory.punch_material, _shared.attack_material]', + 'pickup_materials': '[_factory.pickup_material, _shared.pickup_material]', + 'position': '(0, 1, 0)' + } + } + } + )) + + presets.append(( + 0, + 'Mascot Todd', + 'Todd McBurton with a bright,\nfriendly palette - good for a\ncomic-relief role.', + { + 'data': { + 'type': 'spaz', + 'name': 'Big Todd', + 'attrs': { + 'character': '"Todd McBurton"', + 'color': '(0.9, 0.6, 0.2)', + 'highlight': '(1.2, 1.0, 0.6)', + 'materials': '[_factory.spaz_material, _shared.object_material, _shared.player_material]', + 'roller_materials': '[_factory.roller_material, _shared.player_material]', + 'punch_materials': '[_factory.punch_material, _shared.attack_material]', + 'pickup_materials': '[_factory.pickup_material, _shared.pickup_material]', + 'position': '(2, 1, 2)' + } + } + } + )) + + presets.append(( + 0, + 'Soft Fill Light', + 'Gentle omnidirectional fill\nabove center. Good default\nfor any scene.', + { + 'data': { + 'type': 'light', + 'name': 'SoftFill', + 'attrs': {'intensity': 1.3, 'radius': 3.0, 'color': (1.0, 0.95, 0.9), 'position': (0, 2.0, 0), 'volume_intensity_scale': 0.0} + } + } + )) + + presets.append(( + 0, + 'Warm Key Light', + 'Strong warm key light,\nthe main light source for\na portrait-style shot.', + { + 'data': { + 'type': 'light', + 'name': 'WarmKey', + 'attrs': {'intensity': 1.8, 'radius': 3.5, 'color': (1.3, 1.0, 0.7), 'position': (2.5, 3.5, -2.0), 'volume_intensity_scale': 0.0} + } + } + )) + + presets.append(( + 0, + 'Cool Key Light', + 'Cold blue-toned key light\nfor a tense or sci-fi mood.', + { + 'data': { + 'type': 'light', + 'name': 'CoolKey', + 'attrs': {'intensity': 1.6, 'radius': 3.5, 'color': (0.6, 0.8, 1.4), 'position': (-2.5, 3.5, -2.0), 'volume_intensity_scale': 0.0} + } + } + )) + + presets.append(( + 0, + 'Rim Light Left', + 'Colored rim light from the\nleft to separate a subject\nfrom the background.', + { + 'data': { + 'type': 'light', + 'name': 'RimLeft', + 'attrs': {'intensity': 1.0, 'radius': 4.0, 'color': (0.3, 0.5, 1.2), 'position': (-4, 3, 0), 'volume_intensity_scale': 0.0} + } + } + )) + + presets.append(( + 0, + 'Rim Light Right', + 'Mirror of Rim Light Left -\nuse both together for a\nclean two-sided glow.', + { + 'data': { + 'type': 'light', + 'name': 'RimRight', + 'attrs': {'intensity': 1.0, 'radius': 4.0, 'color': (1.2, 0.5, 0.3), 'position': (4, 3, 0), 'volume_intensity_scale': 0.0} + } + } + )) + + presets.append(( + 0, + 'Top Spotlight', + 'A strong spotlight directly\nabove origin, good for a\nreveal moment.', + { + 'data': { + 'type': 'light', + 'name': 'TopSpot', + 'attrs': {'intensity': 2.2, 'radius': 2.5, 'color': (1.0, 1.0, 1.0), 'position': (0, 5, 0), 'volume_intensity_scale': 0.0} + } + } + )) + + presets.append(( + 0, + 'Dramatic Underlight', + 'Uplight from below for a\nspooky or villainous look.', + { + 'data': { + 'type': 'light', + 'name': 'Underlight', + 'attrs': {'intensity': 1.5, 'radius': 2.0, 'color': (0.9, 0.2, 0.9), 'position': (0, -0.5, 0), 'volume_intensity_scale': 0.0} + } + } + )) + + presets.append(( + 0, + 'Candle Glow', + 'Small warm practical light,\nlow radius, subtle flicker\nfeel for close-up scenes.', + { + 'data': { + 'type': 'light', + 'name': 'CandleGlow', + 'attrs': {'intensity': 0.7, 'radius': 1.2, 'color': (1.4, 0.9, 0.5), 'position': (0, 1.0, 0.5), 'volume_intensity_scale': 0.0} + } + } + )) + + presets.append(( + 0, + 'Color Wash Red', + 'Big saturated red wash for\nalarm or danger beats.', + { + 'data': { + 'type': 'light', + 'name': 'WashRed', + 'attrs': {'intensity': 1.4, 'radius': 6.0, 'color': (2.0, 0.1, 0.1), 'position': (0, 4, 0), 'volume_intensity_scale': 0.0} + } + } + )) + + presets.append(( + 0, + 'Color Wash Blue', + 'Big saturated blue wash for\ncalm, sad, or night beats.', + { + 'data': { + 'type': 'light', + 'name': 'WashBlue', + 'attrs': {'intensity': 1.4, 'radius': 6.0, 'color': (0.1, 0.3, 2.0), 'position': (0, 4, 0), 'volume_intensity_scale': 0.0} + } + } + )) + + presets.append(( + 0, + 'Title Card', + 'Center title text hovering\nin the world.', + { + 'data': { + 'type': 'text', + 'name': 'Title', + 'attrs': {'text': 'MOVI PRESENTS', 'position': (0, 2.2, 0), 'in_world': True, 'shadow': 1.0, 'flatness': 0.8, 'scale': 0.02, 'color': (2.0, 2.0, 2.0), 'h_align': 'center'} + } + } + )) + + presets.append(( + 0, + 'Lower Third Caption', + 'Small caption text near the\nbottom, good for names or\nsubtitles.', + { + 'data': { + 'type': 'text', + 'name': 'LowerThird', + 'attrs': {'text': 'Caption goes here', 'position': (0, 0.6, 0), 'in_world': True, 'shadow': 1.0, 'flatness': 0.9, 'scale': 0.012, 'color': (1.8, 1.8, 1.8), 'h_align': 'center'} + } + } + )) + + presets.append(( + 0, + 'Chapter Marker', + 'Large bold marker text for\nswitching scenes/chapters.', + { + 'data': { + 'type': 'text', + 'name': 'ChapterMarker', + 'attrs': {'text': 'CHAPTER ONE', 'position': (0, 3.0, 0), 'in_world': True, 'shadow': 1.0, 'flatness': 0.7, 'scale': 0.025, 'color': (2.2, 2.2, 1.0), 'h_align': 'center'} + } + } + )) + + presets.append(( + 0, + 'The End Card', + 'Simple closing card text for\nthe last frame of a movie.', + { + 'data': { + 'type': 'text', + 'name': 'EndCard', + 'attrs': {'text': 'THE END', 'position': (0, 2.2, 0), 'in_world': True, 'shadow': 1.0, 'flatness': 0.8, 'scale': 0.022, 'color': (2.0, 2.0, 2.0), 'h_align': 'center'} + } + } + )) + + presets.append(( + 1, + 'Side Shot', + 'Side-on camera, slightly\nzoomed, focused on center.', + { + 'data': { + 'name': 'Camera', + 'chks': [True, True, True], + 'position': [8.0, 3.0, 0.0], + 'target': [0.0, 1.5, 0.0] + } + } + )) + + presets.append(( + 1, + 'Hero Shot', + 'Low front angle, looking\nup at center - makes the\nsubject look powerful.', + { + 'data': { + 'name': 'Camera', + 'chks': [True, True, True], + 'position': [0.0, 1.0, -7.5], + 'target': [0.0, 2.5, 0.0] + } + } + )) + + presets.append(( + 1, + 'Top Down', + 'Top-down overview of the\nwhole map.', + { + 'data': { + 'name': 'Camera', + 'chks': [True, True, True], + 'position': [0.0, 13.0, 0.01], + 'target': [0.0, 0.5, 0.0] + } + } + )) + + presets.append(( + 1, + 'Over Shoulder', + 'Over-the-shoulder shot from\nbehind origin.', + { + 'data': { + 'name': 'Camera', + 'chks': [True, True, True], + 'position': [0.0, 2.2, -4.5], + 'target': [0.0, 1.5, 2.0] + } + } + )) + + presets.append(( + 1, + 'Wide Establishing', + 'Pulled far back to show\nthe whole set - good as\na scene opener.', + { + 'data': { + 'name': 'Camera', + 'chks': [True, True, True], + 'position': [0.0, 6.0, -14.0], + 'target': [0.0, 1.0, 0.0] + } + } + )) + + presets.append(( + 1, + 'Extreme Close-Up', + 'Very tight on center,\nfor an intense reaction\nbeat.', + { + 'data': { + 'name': 'Camera', + 'chks': [True, True, True], + 'position': [0.0, 1.3, -1.6], + 'target': [0.0, 1.4, 0.0] + } + } + )) + + presets.append(( + 1, + 'Three-Quarter Shot', + 'Angled 3/4 view, a natural\ndefault for dialogue.', + { + 'data': { + 'name': 'Camera', + 'chks': [True, True, True], + 'position': [5.0, 2.2, -3.0], + 'target': [0.0, 1.5, 0.0] + } + } + )) + + presets.append(( + 1, + "Bird's Eye Corner", + 'High angle from a corner,\ngood for showing scale.', + { + 'data': { + 'name': 'Camera', + 'chks': [True, True, True], + 'position': [10.0, 10.0, -10.0], + 'target': [0.0, 0.0, 0.0] + } + } + )) + + presets.append(( + 1, + 'Low Diagonal Drama', + 'Low, off-center angle for\na dramatic villain reveal.', + { + 'data': { + 'name': 'Camera', + 'chks': [True, True, True], + 'position': [-3.0, 0.6, -5.0], + 'target': [0.0, 2.0, 0.0] + } + } + )) + + presets.append(( + 1, + 'Symmetric Front', + 'Dead-center front-on shot,\nclean and formal.', + { + 'data': { + 'name': 'Camera', + 'chks': [True, True, True], + 'position': [0.0, 2.0, -8.0], + 'target': [0.0, 1.5, 0.0] + } + } + )) + + presets.append(( + 1, + 'Profile Shot', + 'Pure side profile at eye\nheight.', + { + 'data': { + 'name': 'Camera', + 'chks': [True, True, True], + 'position': [6.5, 1.5, 0.0], + 'target': [0.0, 1.5, 0.0] + } + } + )) + + presets.append(( + 1, + 'Reverse Angle', + 'Opposite side of Side Shot -\nuse both to cut back and\nforth in a conversation.', + { + 'data': { + 'name': 'Camera', + 'chks': [True, True, True], + 'position': [-8.0, 3.0, 0.0], + 'target': [0.0, 1.5, 0.0] + } + } + )) + + presets.append(( + 1, + 'Crane High Wide', + 'High sweeping wide angle,\nfeels cinematic for an\nending shot.', + { + 'data': { + 'name': 'Camera', + 'chks': [True, True, True], + 'position': [12.0, 9.0, -6.0], + 'target': [0.0, 1.0, 0.0] + } + } + )) + + presets.append(( + 1, + 'Ground Level Macro', + 'Camera almost at ground\nlevel looking up - great\nfor a small-object focus.', + { + 'data': { + 'name': 'Camera', + 'chks': [True, True, True], + 'position': [1.0, 0.15, -1.5], + 'target': [0.0, 0.4, 0.0] + } + } + )) + + presets.append(( + 1, + 'Push-In Close', + 'Slightly closer version of\nThree-Quarter Shot - swap\nbetween the two for a fake\ndolly-in cut.', + { + 'data': { + 'name': 'Camera', + 'chks': [True, True, True], + 'position': [3.0, 1.8, -1.8], + 'target': [0.0, 1.5, 0.0] + } + } + )) + + presets.append(( + 2, + 'Crowd Cheer', + 'Positive crowd cheer,\ngreat after a win moment.', + { + 'data': { + 'name': 'Crowd Cheer', + 'file': 'cheer.ogg', + 'x': 0, + 'y': 1, + 'z': 0, + 'volume': 1.0, + 'chks': [False, False] + } + } + )) + + presets.append(( + 2, + 'Crowd Boo', + 'Negative crowd reaction for\na villain or fail beat.', + { + 'data': { + 'name': 'Crowd Boo', + 'file': 'boo.ogg', + 'x': 0, + 'y': 1, + 'z': 0, + 'volume': 1.0, + 'chks': [False, False] + } + } + )) + + presets.append(( + 2, + 'Crowd Chant Loop', + 'Looping ambient crowd chant\nfor a stadium scene.', + { + 'data': { + 'name': 'Crowd Chant Loop', + 'file': 'crowdChant.ogg', + 'x': 0, + 'y': 0, + 'z': 0, + 'volume': 0.6, + 'chks': [False, True] + } + } + )) + + presets.append(( + 2, + 'Foghorn Sting', + 'Big blaring foghorn hit,\ngood comedic sting.', + { + 'data': { + 'name': 'Foghorn Sting', + 'file': 'foghorn.ogg', + 'x': 0, + 'y': 1, + 'z': 0, + 'volume': 1.0, + 'chks': [True, False] + } + } + )) + + presets.append(( + 2, + 'Referee Whistle', + 'Sharp whistle to mark the\nstart/stop of an action.', + { + 'data': { + 'name': 'Referee Whistle', + 'file': 'refWhistle.ogg', + 'x': 0, + 'y': 1, + 'z': 0, + 'volume': 1.0, + 'chks': [True, False] + } + } + )) + + presets.append(( + 2, + 'Cash Register Ka-Ching', + 'Cash register sound for a\nscore or reward beat.', + { + 'data': { + 'name': 'Cash Register Ka-Ching', + 'file': 'cashRegister.ogg', + 'x': 0, + 'y': 1, + 'z': 0, + 'volume': 1.0, + 'chks': [True, False] + } + } + )) + + presets.append(( + 2, + 'Error Buzz', + 'Harsh error/negative buzz,\ngood for a mistake beat.', + { + 'data': { + 'name': 'Error Buzz', + 'file': 'error.ogg', + 'x': 0, + 'y': 1, + 'z': 0, + 'volume': 1.0, + 'chks': [True, False] + } + } + )) + + presets.append(( + 2, + 'Warning Beep Loop', + 'Looping warning beep for a\ntense countdown scene.', + { + 'data': { + 'name': 'Warning Beep Loop', + 'file': 'warnBeeps.ogg', + 'x': 0, + 'y': 1.5, + 'z': 0, + 'volume': 0.7, + 'chks': [False, True] + } + } + )) + + presets.append(( + 2, + 'Drum Roll Buildup', + 'Classic drum roll before a\nbig reveal.', + { + 'data': { + 'name': 'Drum Roll Buildup', + 'file': 'drumRoll.ogg', + 'x': 0, + 'y': 1, + 'z': 0, + 'volume': 0.9, + 'chks': [True, False] + } + } + )) + + presets.append(( + 2, + 'Gong Hit', + 'Big resonant gong hit for a\ndramatic entrance.', + { + 'data': { + 'name': 'Gong Hit', + 'file': 'gong.ogg', + 'x': 0, + 'y': 1.5, + 'z': 0, + 'volume': 1.1, + 'chks': [True, False] + } + } + )) + + presets.append(( + 2, + 'Boxing Bell', + 'Boxing ring bell, perfect\nfor a match start/end.', + { + 'data': { + 'name': 'Boxing Bell', + 'file': 'boxingBell.ogg', + 'x': 0, + 'y': 1, + 'z': 0, + 'volume': 1.0, + 'chks': [True, False] + } + } + )) + + presets.append(( + 2, + 'Achievement Ding', + 'Positive achievement chime\nfor unlocking something.', + { + 'data': { + 'name': 'Achievement Ding', + 'file': 'achievement.ogg', + 'x': 0, + 'y': 1, + 'z': 0, + 'volume': 0.9, + 'chks': [False, False] + } + } + )) + + presets.append(( + 2, + 'Distant Explosion', + 'Single positional boom off\nto the far right.', + { + 'data': { + 'name': 'Distant Explosion', + 'file': 'explosion01.ogg', + 'x': 12.0, + 'y': 0.5, + 'z': 0.0, + 'volume': 1.1, + 'chks': [False, False] + } + } + )) + + presets.append(( + 2, + 'Cinematic Hit', + 'Short musical hit at\ncenter, non-positional.', + { + 'data': { + 'name': 'Cinematic Hit', + 'file': 'impactHard.ogg', + 'x': 0.0, + 'y': 1.0, + 'z': 0.0, + 'volume': 1.4, + 'chks': [True, False] + } + } + )) + + presets.append(( + 2, + 'Underground Rumble Loop', + 'Low looping rumble from\nbelow the arena.', + { + 'data': { + 'name': 'Underground Rumble Loop', + 'file': 'bigImpact2.ogg', + 'x': 0.0, + 'y': -4.0, + 'z': 0.0, + 'volume': 0.9, + 'chks': [False, True] + } + } + )) + + presets.append(( + 2, + 'Power-Up Chime', + 'Bright pickup chime, works\nwell layered under a\nreward FX.', + { + 'data': { + 'name': 'Power-Up Chime', + 'file': 'powerup01.ogg', + 'x': 0, + 'y': 1, + 'z': 0, + 'volume': 1.0, + 'chks': [True, False] + } + } + )) + + presets.append(( + 2, + 'Freeze Sting', + 'Icy freeze sound effect for\na time-stop or slow-mo\nbeat.', + { + 'data': { + 'name': 'Freeze Sting', + 'file': 'freeze.ogg', + 'x': 0, + 'y': 1, + 'z': 0, + 'volume': 1.0, + 'chks': [True, False] + } + } + )) + + presets.append(( + 2, + 'Fuse Burning Loop', + 'Looping fuse hiss for\nbuilding suspense.', + { + 'data': { + 'name': 'Fuse Burning Loop', + 'file': 'fuse01.ogg', + 'x': 0, + 'y': 1, + 'z': 0, + 'volume': 0.7, + 'chks': [True, True] + } + } + )) + + presets.append(( + 3, + 'Spark Shower', + 'Burst of sparks at the\ncenter - a solid all-\npurpose impact effect.', + { + 'data': { + 'name': 'Spark Shower', + 'attrs': {'position': (0, 1.2, 0), 'velocity': (0, 3, 0), 'count': 40, 'scale': 1.0, 'spread': 1.0, 'chunk_type': 'spark', 'emit_type': 'stickers'} + } + } + )) + + presets.append(( + 3, + 'Magic Ring', + 'Pulsing distortion ring\naround center, good for a\nspell or teleport.', + { + 'data': { + 'name': 'Magic Ring', + 'attrs': {'position': (0, 1.0, 0), 'chunk_type': 'sweat', 'emit_type': 'distortion', 'count': 20, 'scale': 1.2, 'spread': 0.2} + } + } + )) + + presets.append(( + 3, + 'Soft Smoke Column', + "Rising smoke plume. This is\nemit_type='tendrils' with\ntendril_type='smoke' - chunk_\ntype is unused by tendrils but\nstill a required argument.", + { + 'data': { + 'name': 'Soft Smoke Column', + 'attrs': {'position': (0, 0.1, 0), 'velocity': (0, 2.5, 0), 'count': 25, 'scale': 1.1, 'chunk_type': 'rock', 'emit_type': 'tendrils', 'tendril_type': 'smoke', 'spread': 0.4} + } + } + )) + + presets.append(( + 3, + 'Thin Wispy Smoke', + 'Same as Soft Smoke Column but\nusing tendril_type=thin_smoke\nfor a lighter, less dense\ntrail.', + { + 'data': { + 'name': 'Thin Wispy Smoke', + 'attrs': {'position': (0, 0.1, 0), 'velocity': (0, 2.0, 0), 'count': 18, 'scale': 0.9, 'chunk_type': 'rock', 'emit_type': 'tendrils', 'tendril_type': 'thin_smoke', 'spread': 0.3} + } + } + )) + + presets.append(( + 3, + 'Icy Vapor Trail', + 'Cold breath / icy vapor using\ntendril_type=ice - good for a\nfrozen-map or dragon-breath\nmoment.', + { + 'data': { + 'name': 'Icy Vapor Trail', + 'attrs': {'position': (0, 1.4, 0), 'velocity': (0, 1.0, 0), 'count': 15, 'scale': 0.8, 'chunk_type': 'ice', 'emit_type': 'tendrils', 'tendril_type': 'ice', 'spread': 0.5} + } + } + )) + + presets.append(( + 3, + 'Fairy Dust Trail', + "Uses the dedicated emit_\ntype='fairydust' - a light\nmagical sparkle trail, softer\nthan a spark burst.", + { + 'data': { + 'name': 'Fairy Dust Trail', + 'attrs': {'position': (0, 1.5, 0), 'velocity': (0, 1.5, 0), 'chunk_type': 'spark', 'emit_type': 'fairydust', 'count': 25, 'scale': 1.0, 'spread': 1.0} + } + } + )) + + presets.append(( + 3, + 'Standard Debris Chunks', + "The engine's own default\nemit_type ('chunks') - plain\nphysical debris, the most\nneutral all-purpose break FX.", + { + 'data': { + 'name': 'Standard Debris Chunks', + 'attrs': {'position': (0, 1.0, 0), 'velocity': (0, 2.0, 0), 'chunk_type': 'rock', 'emit_type': 'chunks', 'count': 24, 'scale': 1.0, 'spread': 1.0} + } + } + )) + + presets.append(( + 3, + 'Small Blast', + 'Compact explosive burst -\nsize down for a bomb or\nfootstep impact.', + { + 'data': { + 'name': 'Small Blast', + 'attrs': {'position': (0, 1.0, 0), 'chunk_type': 'rock', 'emit_type': 'stickers', 'count': 18, 'scale': 0.9, 'spread': 0.6} + } + } + )) + + presets.append(( + 3, + 'Big Explosion', + 'Large violent burst for a\nfinal boom / climax beat.', + { + 'data': { + 'name': 'Big Explosion', + 'attrs': {'position': (0, 1.5, 0), 'velocity': (0, 5, 0), 'chunk_type': 'rock', 'emit_type': 'stickers', 'count': 60, 'scale': 2.0, 'spread': 1.5} + } + } + )) + + presets.append(( + 3, + 'Metal Shrapnel', + 'Sharp metallic burst, good\nfor a robot/machine break.', + { + 'data': { + 'name': 'Metal Shrapnel', + 'attrs': {'position': (0, 1.0, 0), 'chunk_type': 'metal', 'emit_type': 'stickers', 'count': 30, 'scale': 1.0, 'spread': 0.8} + } + } + )) + + presets.append(( + 3, + 'Ice Shatter', + 'Icy shards bursting outward,\nfor a freeze/shatter beat.', + { + 'data': { + 'name': 'Ice Shatter', + 'attrs': {'position': (0, 1.0, 0), 'velocity': (0, 1, 0), 'chunk_type': 'ice', 'emit_type': 'stickers', 'count': 35, 'scale': 1.1, 'spread': 1.0} + } + } + )) + + presets.append(( + 3, + 'Slime Splat', + 'Goopy slime splash, fun for\na comedic gross-out beat.', + { + 'data': { + 'name': 'Slime Splat', + 'attrs': {'position': (0, 1.0, 0), 'chunk_type': 'slime', 'emit_type': 'stickers', 'count': 22, 'scale': 0.9, 'spread': 0.7} + } + } + )) + + presets.append(( + 3, + 'Wood Splinter Burst', + 'Wooden debris burst for a\ncrate/prop breaking.', + { + 'data': { + 'name': 'Wood Splinter Burst', + 'attrs': {'position': (0, 0.8, 0), 'chunk_type': 'splinter', 'emit_type': 'stickers', 'count': 20, 'scale': 0.8, 'spread': 0.6} + } + } + )) + + presets.append(( + 3, + 'Energy Pulse', + 'Fast expanding distortion\npulse, reads like a shock-\nwave or sonic blast.', + { + 'data': { + 'name': 'Energy Pulse', + 'attrs': {'position': (0, 1.5, 0), 'chunk_type': 'spark', 'emit_type': 'distortion', 'count': 45, 'scale': 1.6, 'spread': 1.2} + } + } + )) + + presets.append(( + 3, + 'Gentle Sparkle', + "Light, slow sparkle drift -\nsubtle magical ambiance,\nlow count so it doesn't\noverpower a shot.", + { + 'data': { + 'name': 'Gentle Sparkle', + 'attrs': {'position': (0, 2.0, 0), 'velocity': (0, 0.5, 0), 'chunk_type': 'spark', 'emit_type': 'tendrils', 'count': 10, 'scale': 0.6, 'spread': 1.5} + } + } + )) + + presets.append(( + 3, + 'Sweat Drip Panic', + 'Comedic sweat drops, useful\nfor a nervous character\nmoment.', + { + 'data': { + 'name': 'Sweat Drip Panic', + 'attrs': {'position': (0, 1.6, 0), 'velocity': (0, -1, 0), 'chunk_type': 'sweat', 'emit_type': 'stickers', 'count': 8, 'scale': 0.7, 'spread': 0.3} + } + } + )) + + presets.append(( + 4, + 'Hockey Stadium (Bright)', + 'Hockey Stadium with crisp,\nslightly colder lighting.', + { + 'data': { + 'map': 'Hockey Stadium', + 'name': 'Map', + 'attrs': {'ambient_color': (0.6, 0.7, 1.0), 'tint': (1.1, 1.2, 1.3), 'vignette_outer': (0.1, 0.1, 0.2), 'vignette_inner': (0.8, 0.9, 1.0)} + } + } + )) + + presets.append(( + 4, + 'Bridgit (Warm Sunset)', + 'Bridgit map with warm\nsunset-tinted lighting.', + { + 'data': { + 'map': 'Bridgit', + 'name': 'Map', + 'attrs': {'ambient_color': (1.1, 0.8, 0.7), 'tint': (1.3, 1.0, 0.8), 'vignette_outer': (0.2, 0.1, 0.05), 'vignette_inner': (0.9, 0.8, 0.7)} + } + } + )) + + presets.append(( + 4, + 'Happy Thoughts (Dreamy)', + 'Extra dreamy, pastel-heavy\nHappy Thoughts mood.', + { + 'data': { + 'map': 'Happy Thoughts', + 'name': 'Map', + 'attrs': {'ambient_color': (1.4, 1.0, 1.4), 'tint': (1.5, 1.0, 1.5), 'happy_thoughts_mode': True} + } + } + )) + + presets.append(( + 4, + 'Doom Shroom (Ominous)', + 'Doom Shroom pushed darker\nand greener for a horror\nfeel.', + { + 'data': { + 'map': 'Doom Shroom', + 'name': 'Map', + 'attrs': {'ambient_color': (0.5, 0.7, 0.4), 'tint': (0.8, 1.0, 0.7), 'vignette_outer': (0.0, 0.05, 0.0), 'vignette_inner': (0.6, 0.7, 0.6)} + } + } + )) + + presets.append(( + 4, + 'Lake Frigid (Ice Blue)', + 'Lake Frigid pushed toward a\ncold, icy blue palette.', + { + 'data': { + 'map': 'Lake Frigid', + 'name': 'Map', + 'attrs': {'ambient_color': (0.7, 0.85, 1.2), 'tint': (0.9, 1.0, 1.3), 'vignette_outer': (0.05, 0.08, 0.15), 'vignette_inner': (0.8, 0.9, 1.0)} + } + } + )) + + presets.append(( + 4, + 'Tip Top (High Noon)', + 'Tip Top with harsh bright\noverhead daylight.', + { + 'data': { + 'map': 'Tip Top', + 'name': 'Map', + 'attrs': {'ambient_color': (1.2, 1.2, 1.1), 'tint': (1.2, 1.2, 1.1), 'vignette_outer': (0.15, 0.15, 0.1), 'vignette_inner': (1.0, 1.0, 0.95)} + } + } + )) + + presets.append(( + 4, + 'Crag Castle (Torchlit)', + 'Crag Castle warmed up like\nit is lit by torches.', + { + 'data': { + 'map': 'Crag Castle', + 'name': 'Map', + 'attrs': {'ambient_color': (1.2, 0.8, 0.5), 'tint': (1.3, 0.9, 0.6), 'vignette_outer': (0.1, 0.05, 0.0), 'vignette_inner': (0.9, 0.7, 0.5)} + } + } + )) + + presets.append(( + 4, + 'Tower D (Neon Night)', + 'Tower D pushed into a\nsaturated neon night look.', + { + 'data': { + 'map': 'Tower D', + 'name': 'Map', + 'attrs': {'ambient_color': (0.6, 0.4, 1.2), 'tint': (0.8, 0.6, 1.4), 'vignette_outer': (0.05, 0.0, 0.15), 'vignette_inner': (0.7, 0.5, 1.0)} + } + } + )) + + presets.append(( + 4, + 'Football Stadium (Overcast)', + 'Football Stadium under a\nflat, cloudy grey sky.', + { + 'data': { + 'map': 'Football Stadium', + 'name': 'Map', + 'attrs': {'ambient_color': (0.8, 0.8, 0.85), 'tint': (0.9, 0.9, 0.95), 'vignette_outer': (0.1, 0.1, 0.12), 'vignette_inner': (0.85, 0.85, 0.9)} + } + } + )) + + presets.append(( + 4, + 'Big G (Golden Hour)', + 'Big G with a rich golden-\nhour glow.', + { + 'data': { + 'map': 'Big G', + 'name': 'Map', + 'attrs': {'ambient_color': (1.3, 1.0, 0.6), 'tint': (1.4, 1.05, 0.7), 'vignette_outer': (0.15, 0.08, 0.0), 'vignette_inner': (1.0, 0.85, 0.6)} + } + } + )) + + presets.append(( + 4, + 'Roundabout (Cool Studio)', + 'Roundabout in a neutral,\ncool studio-lit look.', + { + 'data': { + 'map': 'Roundabout', + 'name': 'Map', + 'attrs': {'ambient_color': (0.9, 0.95, 1.05), 'tint': (0.95, 1.0, 1.05), 'vignette_outer': (0.1, 0.1, 0.12), 'vignette_inner': (0.9, 0.92, 0.95)} + } + } + )) + + presets.append(( + 4, + 'Monkey Face (Jungle Haze)', + 'Monkey Face with a hazy\ngreen jungle-light feel.', + { + 'data': { + 'map': 'Monkey Face', + 'name': 'Map', + 'attrs': {'ambient_color': (0.9, 1.1, 0.7), 'tint': (0.95, 1.1, 0.75), 'vignette_outer': (0.05, 0.1, 0.0), 'vignette_inner': (0.8, 0.95, 0.65)} + } + } + )) + + presets.append(( + 4, + 'Zigzag (Late Afternoon)', + 'Zigzag with a soft late-\nafternoon amber cast.', + { + 'data': { + 'map': 'Zigzag', + 'name': 'Map', + 'attrs': {'ambient_color': (1.15, 0.95, 0.75), 'tint': (1.2, 1.0, 0.8), 'vignette_outer': (0.12, 0.08, 0.02), 'vignette_inner': (0.95, 0.85, 0.7)} + } + } + )) + + presets.append(( + 4, + 'The Pad (Moody Purple)', + 'The Pad tinted moody purple\nfor a stylish lounge feel.', + { + 'data': { + 'map': 'The Pad', + 'name': 'Map', + 'attrs': {'ambient_color': (0.9, 0.6, 1.1), 'tint': (1.0, 0.7, 1.2), 'vignette_outer': (0.1, 0.0, 0.15), 'vignette_inner': (0.85, 0.6, 0.95)} + } + } + )) + + presets.append(( + 4, + 'Step Right Up (Carnival Bright)', + 'Step Right Up boosted for a\nfun, colorful carnival pop.', + { + 'data': { + 'map': 'Step Right Up', + 'name': 'Map', + 'attrs': {'ambient_color': (1.2, 1.05, 0.9), 'tint': (1.25, 1.1, 0.95), 'vignette_outer': (0.15, 0.1, 0.05), 'vignette_inner': (1.0, 0.9, 0.8)} + } + } + )) + + presets.append(( + 4, + 'Courtyard (Soft Overcast)', + 'Courtyard with gentle,\ndiffused daylight.', + { + 'data': { + 'map': 'Courtyard', + 'name': 'Map', + 'attrs': {'ambient_color': (0.95, 0.95, 0.9), 'tint': (1.0, 1.0, 0.95), 'vignette_outer': (0.1, 0.1, 0.08), 'vignette_inner': (0.9, 0.9, 0.85)} + } + } + )) + + presets.append(( + 4, + 'Rampage (Industrial Grey)', + 'Rampage pushed toward a\ncold industrial grey tone.', + { + 'data': { + 'map': 'Rampage', + 'name': 'Map', + 'attrs': {'ambient_color': (0.75, 0.78, 0.8), 'tint': (0.85, 0.87, 0.9), 'vignette_outer': (0.08, 0.08, 0.1), 'vignette_inner': (0.8, 0.82, 0.85)} + } + } + )) + + presets.append(( + 6, + 'Grand Entrance', + 'One-shot hero intro: spotlight\nfades up, a drum roll builds,\nand a Spaz steps into frame.\nSaves wiring 3 separate\npresets together.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Grand Entrance', + 'timers = []', + 'bs.newnode("light", attrs={', + ' "intensity": 2.0, "radius": 3.0, "color": (1.0, 1.0, 1.0),', + ' "position": (0, 5, 0), "volume_intensity_scale": 0.0', + '})', + 'timers.append(bs.AppTimer(0.0, lambda: bs.newnode("sound", attrs={', + ' "sound": bs.getsound("drumRoll"), "volume": 0.9, "loop": False', + '})))', + 'def _spawn_hero():', + ' bot = Spaz(character="Spaz", start_invincible=False,', + ' color=(0.4, 0.5, 0.8), highlight=(1.0, 1.0, 1.0))', + ' bot.handlemessage(bs.StandMessage((0, 1, 0), 0))', + 'timers.append(bs.AppTimer(1.6, _spawn_hero))', + )) + } + } + )) + + presets.append(( + 6, + 'Instant Boss Reveal', + 'Dark red wash + gong hit +\noversized villain step-in, all\nin one preset for a fast boss\nintro.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Instant Boss Reveal', + 'timers = []', + 'bs.newnode("light", attrs={', + ' "intensity": 1.6, "radius": 5.0, "color": (2.0, 0.1, 0.1),', + ' "position": (0, 3, 0), "volume_intensity_scale": 0.0', + '})', + 'timers.append(bs.AppTimer(0.0, lambda: bs.newnode("sound", attrs={', + ' "sound": bs.getsound("gong"), "volume": 1.1, "loop": False', + '})))', + 'def _spawn_boss():', + ' bot = Spaz(character="Grumbledorf", start_invincible=False,', + ' color=(0.3, 0.0, 0.0), highlight=(1.5, 0.0, 0.0))', + ' bot.handlemessage(bs.StandMessage((0, 1, 0), 0))', + 'timers.append(bs.AppTimer(0.8, _spawn_boss))', + )) + } + } + )) + + presets.append(( + 6, + 'Victory Podium', + 'Cheer + achievement chime +\nspark shower + "WINNER" text,\nready to drop on a win screen.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Victory Podium', + 'timers = []', + 'bs.newnode("text", attrs={', + ' "text": "WINNER", "position": (0, 2.4, 0), "in_world": True,', + ' "shadow": 1.0, "flatness": 0.8, "scale": 0.024,', + ' "color": (2.0, 2.0, 0.4), "h_align": "center"', + '})', + 'timers.append(bs.AppTimer(0.0, lambda: bs.newnode("sound", attrs={', + ' "sound": bs.getsound("cheer"), "volume": 1.0, "loop": False', + '})))', + 'timers.append(bs.AppTimer(0.2, lambda: bs.newnode("sound", attrs={', + ' "sound": bs.getsound("achievement"), "volume": 0.9, "loop": False', + '})))', + 'timers.append(bs.AppTimer(0.2, lambda: bs.emitfx(', + ' position=(0, 1.2, 0), velocity=(0, 3, 0), count=40, scale=1.0,', + ' spread=1.0, chunk_type="spark", emit_type="stickers"', + ')))', + )) + } + } + )) + + presets.append(( + 6, + 'Countdown Launch', + 'Five-to-one voice countdown\nthat ends in an explosion -\nthe whole beat in one preset.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Countdown Launch', + 'timers = []', + 'announces = ["Five", "Four", "Three", "Two", "One"]', + 'for i, num_name in enumerate(announces):', + ' t = i * 1.0', + ' sound_name = f"announce{num_name}"', + ' timers.append(bs.AppTimer(t, lambda s=sound_name: bs.newnode(', + ' "sound", attrs={"sound": bs.getsound(s), "volume": 0.9, "loop": False}', + ' )))', + 'timers.append(bs.AppTimer(5.0, lambda: bs.newnode("sound", attrs={', + ' "sound": bs.getsound("explosion05"), "volume": 1.0, "loop": False', + '})))', + 'timers.append(bs.AppTimer(5.0, lambda: bs.emitfx(', + ' position=(0, 1.5, 0), velocity=(0, 5, 0), chunk_type="rock",', + ' emit_type="stickers", count=60, scale=2.0, spread=1.5', + ')))', + )) + } + } + )) + + presets.append(( + 6, + 'Quick Two-Character Duel', + 'Spawns two ready-to-go Spaz\ncharacters facing each other -\nsaves setting up both by hand.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Quick Two-Character Duel', + 'left = Spaz(character="Spaz", start_invincible=False,', + ' color=(0.4, 0.5, 0.8), highlight=(1.0, 1.0, 1.0))', + 'left.handlemessage(bs.StandMessage((-2, 1, 0), 1.57))', + 'right = Spaz(character="Snake Shadow", start_invincible=False,', + ' color=(2.2, 0.0, 0.4), highlight=(2.5, 0.0, 4.0))', + 'right.handlemessage(bs.StandMessage((2, 1, 0), -1.57))', + )) + } + } + )) + + presets.append(( + 6, + 'Suspense Build', + 'Burning fuse loop + a dim red\nlight + a warning beep - a\nready-made tension bed.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Suspense Build', + 'timers = []', + 'bs.newnode("light", attrs={', + ' "intensity": 0.8, "radius": 2.0, "color": (1.2, 0.1, 0.1),', + ' "position": (0, 1.5, 0), "volume_intensity_scale": 0.0', + '})', + 'timers.append(bs.AppTimer(0.0, lambda: bs.newnode("sound", attrs={', + ' "sound": bs.getsound("fuse01"), "volume": 0.7, "loop": True', + '})))', + 'timers.append(bs.AppTimer(1.5, lambda: bs.newnode("sound", attrs={', + ' "sound": bs.getsound("warnBeep"), "volume": 0.8, "loop": False', + '})))', + )) + } + } + )) + + presets.append(( + 6, + 'Comedy Fail', + 'Error buzz + crowd boo + a\nslime splat FX for a fast\nslapstick fail beat.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Comedy Fail', + 'timers = []', + 'timers.append(bs.AppTimer(0.0, lambda: bs.newnode("sound", attrs={', + ' "sound": bs.getsound("error"), "volume": 1.0, "loop": False', + '})))', + 'timers.append(bs.AppTimer(0.3, lambda: bs.newnode("sound", attrs={', + ' "sound": bs.getsound("boo"), "volume": 0.9, "loop": False', + '})))', + 'timers.append(bs.AppTimer(0.3, lambda: bs.emitfx(', + ' position=(0, 1.0, 0), chunk_type="slime", emit_type="stickers",', + ' count=22, scale=0.9, spread=0.7', + ')))', + )) + } + } + )) + + presets.append(( + 6, + 'Sports Intro', + 'Boxing bell + looping crowd\nchant + a referee whistle -\nready for a match-start scene.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Sports Intro', + 'timers = []', + 'timers.append(bs.AppTimer(0.0, lambda: bs.newnode("sound", attrs={', + ' "sound": bs.getsound("crowdChant"), "volume": 0.6, "loop": True', + '})))', + 'timers.append(bs.AppTimer(0.5, lambda: bs.newnode("sound", attrs={', + ' "sound": bs.getsound("boxingBell"), "volume": 1.0, "loop": False', + '})))', + 'timers.append(bs.AppTimer(1.0, lambda: bs.newnode("sound", attrs={', + ' "sound": bs.getsound("refWhistle"), "volume": 1.0, "loop": False', + '})))', + )) + } + } + )) + + presets.append(( + 6, + 'Retro Arcade Blips', + 'Quick ascending arcade-style\nblip run - good for a menu or\nscore-tick moment.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Retro Arcade Blips', + 'timers = []', + 'seq = [', + " (0.0, 'ding', 0.7),", + " (0.15, 'dingSmall', 0.6),", + " (0.3, 'dingSmallHigh', 0.6),", + " (0.45, 'ding', 0.7),", + " (0.6, 'dingSmallHigh', 0.8),", + ']', + 'for t, nm, vol in seq:', + ' timers.append(bs.AppTimer(t, lambda nm=nm, vol=vol: bs.newnode(', + ' "sound", attrs={"sound": bs.getsound(nm), "volume": vol, "loop": False}', + ' )))', + )) + } + } + )) + + presets.append(( + 6, + 'Suspense Rising Tension', + 'Alarm pulse followed by rising\ndings - simple building tension\nbed.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Suspense Rising Tension', + 'timers = []', + 'seq = [', + " (0.0, 'alarm', 0.4),", + " (0.8, 'dingSmall', 0.3),", + " (1.6, 'dingSmallHigh', 0.35),", + " (2.4, 'bellHigh', 0.4),", + " (3.2, 'bellHigh', 0.5),", + ']', + 'for t, nm, vol in seq:', + ' timers.append(bs.AppTimer(t, lambda nm=nm, vol=vol: bs.newnode(', + ' "sound", attrs={"sound": bs.getsound(nm), "volume": vol, "loop": False}', + ' )))', + )) + } + } + )) + + presets.append(( + 6, + 'Bell Chime Cascade', + 'Low-to-high bell run, works\nwell for a peaceful or magical\nmoment.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Bell Chime Cascade', + 'timers = []', + 'seq = [', + " (0.0, 'bellLow', 0.6),", + " (0.4, 'bellMed', 0.6),", + " (0.8, 'bellHigh', 0.6),", + " (1.4, 'bellLow', 0.4),", + " (1.8, 'bellHigh', 0.5),", + ']', + 'for t, nm, vol in seq:', + ' timers.append(bs.AppTimer(t, lambda nm=nm, vol=vol: bs.newnode(', + ' "sound", attrs={"sound": bs.getsound(nm), "volume": vol, "loop": False}', + ' )))', + )) + } + } + )) + + presets.append(( + 6, + 'Explosive Climax', + 'Three layered explosions plus\na big particle burst, all on\none beat - a ready-made\nfinale moment.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Explosive Climax', + 'timers = []', + 'seq = [', + " (0.0, 'explosion05', 1.0),", + " (0.0, 'explosion04', 0.9),", + " (0.05, 'bigImpact', 0.8),", + ']', + 'for t, nm, vol in seq:', + ' timers.append(bs.AppTimer(t, lambda nm=nm, vol=vol: bs.newnode(', + ' "sound", attrs={"sound": bs.getsound(nm), "volume": vol, "loop": False}', + ' )))', + '', + 'fx_events = [', + " (0.0, {'position': (0, 2, 0), 'velocity': (0, 10, 0), 'count': 150, 'scale': 3.5, 'spread': 3.0, 'chunk_type': 'spark', 'emit_type': 'distortion'}),", + ']', + 'for t, kwargs in fx_events:', + ' timers.append(bs.AppTimer(t, lambda kwargs=kwargs: bs.emitfx(**kwargs)))', + )) + } + } + )) + + presets.append(( + 6, + 'Score Combo Ding', + 'Five quick rising dings for a\ncombo/score-multiplier feel.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Score Combo Ding', + 'timers = []', + 'seq = [', + " (0.0, 'dingSmallHigh', 0.4),", + " (0.12, 'dingSmallHigh', 0.5),", + " (0.24, 'dingSmallHigh', 0.6),", + " (0.36, 'dingSmallHigh', 0.75),", + " (0.48, 'scoreHit02', 1.0),", + ']', + 'for t, nm, vol in seq:', + ' timers.append(bs.AppTimer(t, lambda nm=nm, vol=vol: bs.newnode(', + ' "sound", attrs={"sound": bs.getsound(nm), "volume": vol, "loop": False}', + ' )))', + )) + } + } + )) + + presets.append(( + 6, + 'Robotic Glitch Sequence', + 'Power up/down and error\nsounds layered for a glitching\nrobot/machine feel.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Robotic Glitch Sequence', + 'timers = []', + 'seq = [', + " (0.0, 'powerup01', 0.7),", + " (0.3, 'error', 0.5),", + " (0.6, 'powerdown01', 0.6),", + " (0.9, 'powerup01', 0.8),", + " (1.1, 'click01', 0.5),", + ']', + 'for t, nm, vol in seq:', + ' timers.append(bs.AppTimer(t, lambda nm=nm, vol=vol: bs.newnode(', + ' "sound", attrs={"sound": bs.getsound(nm), "volume": vol, "loop": False}', + ' )))', + )) + } + } + )) + + presets.append(( + 6, + 'Impact Barrage', + 'Layered punch sounds for a\nfast, punchy fight montage\nbeat.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Impact Barrage', + 'timers = []', + 'seq = [', + " (0.0, 'punch01', 0.8),", + " (0.2, 'punchWeak01', 0.6),", + " (0.4, 'punchStrong01', 0.9),", + " (0.6, 'punchStrong02', 1.0),", + " (0.9, 'superPunch', 1.0),", + ']', + 'for t, nm, vol in seq:', + ' timers.append(bs.AppTimer(t, lambda nm=nm, vol=vol: bs.newnode(', + ' "sound", attrs={"sound": bs.getsound(nm), "volume": vol, "loop": False}', + ' )))', + )) + } + } + )) + + presets.append(( + 6, + 'Crowd Hype Wave', + 'Cheer building into a looping\nchant with a bell hit - good\nfor a stadium hype moment.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Crowd Hype Wave', + 'timers = []', + 'seq = [', + " (0.0, 'cheer', 0.9),", + " (0.4, 'crowdChant', 0.6),", + " (1.2, 'boxingBell', 0.8),", + ']', + 'for t, nm, vol in seq:', + ' timers.append(bs.AppTimer(t, lambda nm=nm, vol=vol: bs.newnode(', + ' "sound", attrs={"sound": bs.getsound(nm), "volume": vol, "loop": False}', + ' )))', + )) + } + } + )) + + presets.append(( + 6, + 'Footstep Walk Cycle', + 'Alternating footstep sounds\ntimed for a walk cycle - handy\nfor syncing to a character\nwalking on-screen.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Footstep Walk Cycle', + 'timers = []', + 'seq = [', + " (0.0, 'footImpact01', 0.6),", + " (0.4, 'footImpact02', 0.6),", + " (0.8, 'footImpact03', 0.6),", + " (1.2, 'footImpact01', 0.6),", + " (1.6, 'footImpact02', 0.6),", + ']', + 'for t, nm, vol in seq:', + ' timers.append(bs.AppTimer(t, lambda nm=nm, vol=vol: bs.newnode(', + ' "sound", attrs={"sound": bs.getsound(nm), "volume": vol, "loop": False}', + ' )))', + )) + } + } + )) + + presets.append(( + 6, + 'Glass Break Sequence', + 'Shatter, splatter, and\ndebris fall layered for a full\nglass-breaking beat.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Glass Break Sequence', + 'timers = []', + 'seq = [', + " (0.0, 'shatter', 1.0),", + " (0.05, 'splatter', 0.6),", + " (0.15, 'debrisFall', 0.7),", + ']', + 'for t, nm, vol in seq:', + ' timers.append(bs.AppTimer(t, lambda nm=nm, vol=vol: bs.newnode(', + ' "sound", attrs={"sound": bs.getsound(nm), "volume": vol, "loop": False}', + ' )))', + )) + } + } + )) + + presets.append(( + 6, + 'Metal Clang Rhythm', + 'Rhythmic metal hits for a\nmachine-shop or forge feel.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Metal Clang Rhythm', + 'timers = []', + 'seq = [', + " (0.0, 'metalHit', 0.8),", + " (0.5, 'metalSkid', 0.5),", + " (1.0, 'metalHit', 0.8),", + " (1.5, 'metalHit', 0.6),", + " (2.0, 'metalSkid', 0.5),", + ']', + 'for t, nm, vol in seq:', + ' timers.append(bs.AppTimer(t, lambda nm=nm, vol=vol: bs.newnode(', + ' "sound", attrs={"sound": bs.getsound(nm), "volume": vol, "loop": False}', + ' )))', + )) + } + } + )) + + presets.append(( + 6, + 'Ticking Clock Loop', + 'Steady ticking that speeds up\ninto a final impact - great\nfor a time-pressure beat.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Ticking Clock Loop', + 'timers = []', + 'seq = [', + " (0.0, 'tick', 0.5),", + " (0.6, 'tick', 0.5),", + " (1.1, 'tick', 0.5),", + " (1.5, 'tick', 0.5),", + " (1.8, 'ticking', 0.6),", + " (2.1, 'ticking', 0.6),", + " (2.3, 'bigImpact', 1.0),", + ']', + 'for t, nm, vol in seq:', + ' timers.append(bs.AppTimer(t, lambda nm=nm, vol=vol: bs.newnode(', + ' "sound", attrs={"sound": bs.getsound(nm), "volume": vol, "loop": False}', + ' )))', + )) + } + } + )) + + presets.append(( + 6, + 'Race Start Sequence', + 'Two warning beeps then a go-\nsignal and revving engine -\nready for a race start.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Race Start Sequence', + 'timers = []', + 'seq = [', + " (0.0, 'raceBeep1', 0.8),", + " (0.8, 'raceBeep1', 0.8),", + " (1.6, 'raceBeep2', 1.0),", + " (1.6, 'revUp', 0.9),", + ']', + 'for t, nm, vol in seq:', + ' timers.append(bs.AppTimer(t, lambda nm=nm, vol=vol: bs.newnode(', + ' "sound", attrs={"sound": bs.getsound(nm), "volume": vol, "loop": False}', + ' )))', + )) + } + } + )) + + presets.append(( + 6, + 'Cash Reward Jingle', + 'Powerup chime into a cash\nregister hit - a satisfying\nreward stinger.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Cash Reward Jingle', + 'timers = []', + 'seq = [', + " (0.0, 'healthPowerup', 0.7),", + " (0.3, 'powerup01', 0.7),", + " (0.6, 'cashRegister', 1.0),", + ']', + 'for t, nm, vol in seq:', + ' timers.append(bs.AppTimer(t, lambda nm=nm, vol=vol: bs.newnode(', + ' "sound", attrs={"sound": bs.getsound(nm), "volume": vol, "loop": False}', + ' )))', + )) + } + } + )) + + presets.append(( + 6, + 'Freeze Frame Flash', + 'Freeze sound + a bright white\nflash light + "FREEZE!" text -\ngood for a slow-mo/pause gag.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Freeze Frame Flash', + 'bs.newnode("sound", attrs={', + ' "sound": bs.getsound("freeze"), "volume": 1.0, "loop": False', + '})', + 'bs.newnode("light", attrs={', + ' "intensity": 3.0, "radius": 6.0, "color": (2.0, 2.0, 2.2),', + ' "position": (0, 3, 0), "volume_intensity_scale": 0.0', + '})', + 'bs.newnode("text", attrs={', + ' "text": "FREEZE!", "position": (0, 2.4, 0), "in_world": True,', + ' "shadow": 1.0, "flatness": 0.8, "scale": 0.024,', + ' "color": (1.6, 1.9, 2.2), "h_align": "center"', + '})', + )) + } + } + )) + + presets.append(( + 6, + 'Chapter Title Sequence', + 'A gong hit paired with a\ncentered chapter title - a\nfast way to mark a new scene.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Chapter Title Sequence', + 'bs.newnode("sound", attrs={', + ' "sound": bs.getsound("gong"), "volume": 1.0, "loop": False', + '})', + 'bs.newnode("text", attrs={', + ' "text": "CHAPTER ONE", "position": (0, 3.0, 0), "in_world": True,', + ' "shadow": 1.0, "flatness": 0.7, "scale": 0.025,', + ' "color": (2.2, 2.2, 1.0), "h_align": "center"', + '})', + )) + } + } + )) + + presets.append(( + 6, + 'Slow-Mo Sting', + 'Freeze sound + a dim blue\nlight, useful right before a\nslow-motion replay beat.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Slow-Mo Sting', + 'bs.newnode("sound", attrs={', + ' "sound": bs.getsound("freeze"), "volume": 0.8, "loop": False', + '})', + 'bs.newnode("light", attrs={', + ' "intensity": 0.9, "radius": 4.0, "color": (0.4, 0.6, 1.4),', + ' "position": (0, 2, 0), "volume_intensity_scale": 0.0', + '})', + )) + } + } + )) + + presets.append(( + 6, + 'Credits Roll Setup', + 'Orchestra hit stinger + a\ncentered "THE END" card - a\nready-made closer.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Credits Roll Setup', + 'bs.newnode("sound", attrs={', + ' "sound": bs.getsound("orchestraHit"), "volume": 1.0, "loop": False', + '})', + 'bs.newnode("text", attrs={', + ' "text": "THE END", "position": (0, 2.2, 0), "in_world": True,', + ' "shadow": 1.0, "flatness": 0.8, "scale": 0.022,', + ' "color": (2.0, 2.0, 2.0), "h_align": "center"', + '})', + )) + } + } + )) + + presets.append(( + 6, + 'Confetti Celebration', + 'Gold spark burst + a cheer -\na quick celebratory moment\nwithout needing separate FX\nand Sound presets.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Confetti Celebration', + 'bs.newnode("sound", attrs={', + ' "sound": bs.getsound("cheer"), "volume": 1.0, "loop": False', + '})', + 'bs.emitfx(', + ' position=(0, 2.0, 0), velocity=(0, 4, 0), count=70, scale=1.3,', + ' spread=2.0, chunk_type="spark", emit_type="stickers"', + ')', + )) + } + } + )) + + presets.append(( + 6, + 'Random Sound Roulette', + 'Fires a random sound from a\ncurated safe list every 0.4s -\na fun chaos/easter-egg preset\nfor testing or a silly montage.', + { + 'data': { + 'code': '\n'.join(( + '# MOVI Random Sound Roulette', + 'timers = []', + 'pool = ["ding", "boo", "cheer", "error", "gong", "achievement",', + ' "powerup01", "punchWeak01", "click01", "dripity"]', + 'for i in range(8):', + ' t = i * 0.4', + ' timers.append(bs.AppTimer(t, lambda: bs.newnode("sound", attrs={', + ' "sound": bs.getsound(random.choice(pool)), "volume": 0.8, "loop": False', + ' })))', + )) + } + } + )) + + + return presets + + From ae99ccc7b238fe4d56809ba1faa45270ddff7bce Mon Sep 17 00:00:00 2001 From: BroBordd Date: Mon, 17 Aug 2026 10:01:09 +0300 Subject: [PATCH 2/5] updated header --- plugins/utilities/movi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/utilities/movi.py b/plugins/utilities/movi.py index 6334ada5..a359bc5e 100644 --- a/plugins/utilities/movi.py +++ b/plugins/utilities/movi.py @@ -1,4 +1,4 @@ -# Copyright 2025 - Solely by BrotherBoard +# Copyright 2026 - Solely by BrotherBoard # Intended for personal use only # Bug? Feedback? Telegram >> @BroBordd From 8d66176000fad2c45334345a14bd12189d573da2 Mon Sep 17 00:00:00 2001 From: BroBordd Date: Tue, 18 Aug 2026 06:03:37 +0300 Subject: [PATCH 3/5] bugfix --- plugins/utilities/movi.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/plugins/utilities/movi.py b/plugins/utilities/movi.py index a359bc5e..411ac44c 100644 --- a/plugins/utilities/movi.py +++ b/plugins/utilities/movi.py @@ -1738,7 +1738,7 @@ def do_done(): ) existed = False if nam in mem['keys']: - s.widgets.pop(mem['keys'][nam]['widget']).delete() + (wid := s.widgets.pop(mem['keys'][nam].get('widget'), None)) and wid.delete() existed = True mem['keys'][nam] = { 'time':actual_time, @@ -1781,7 +1781,7 @@ def do_pop(): s.toast(Format.NOT_FOUND(n)) Eval.SOUND(Const.BAD_SOUND).play() return - s.widgets[mem['keys'].pop(n)['widget']].delete() + (wid := s.widgets.pop(mem['keys'].pop(n).get('widget', object()), None)) and wid.delete() s.toast(Strings.INFO_POPPED(n)) Eval.SOUND(Const.OK_SOUND).play() s.fresh_current_key_texts() @@ -1936,7 +1936,7 @@ def add_key(final,n,off,mem): ) existed = False if n in mem['keys']: - s.widgets.pop(mem['keys'][n]['widget']).delete() + (wid := s.widgets.pop(mem['keys'][n].get('widget'), None)) and wid.delete() existed = True mem['keys'][n] = { 'time':actual_time, @@ -1979,7 +1979,7 @@ def do_pop(): s.toast(Format.NOT_FOUND(n)) Eval.SOUND(Const.BAD_SOUND).play() return - s.widgets[mem['keys'].pop(n)['widget']].delete() + (wid := s.widgets.pop(mem['keys'].pop(n).get('widget', object()), None)) and wid.delete() s.toast(Strings.INFO_POPPED(n)) Eval.SOUND(Const.OK_SOUND).play() s.fresh_current_key_texts() @@ -2143,7 +2143,7 @@ def do_done(): existed = False if nam in mem['keys']: existed = True - s.widgets.pop(mem['keys'][nam]['widget']).delete() + (wid := s.widgets.pop(mem['keys'][nam].get('widget'), None)) and wid.delete() mem['keys'][nam] = { 'time':actual_time, 'action':i, @@ -2185,7 +2185,7 @@ def do_pop(): s.toast(Format.NOT_FOUND(t)) Eval.SOUND(Const.BAD_SOUND).play() return - s.widgets.pop(mem['keys'].pop(t)['widget']).delete() + (wid := s.widgets.pop(mem['keys'].pop(t).get('widget', object()), None)) and wid.delete() s.toast(Strings.INFO_POPPED(t)) Eval.SOUND(Const.OK_SOUND).play() s.fresh_current_key_texts() @@ -2409,7 +2409,7 @@ def do_done(): ) existed = False if nam in mem['keys']: - s.widgets.pop(mem['keys'][nam]['widget']).delete() + (wid := s.widgets.pop(mem['keys'][nam].get('widget'), None)) and wid.delete() existed = True mem['keys'][nam] = { 'time':actual_time, @@ -2454,7 +2454,7 @@ def do_pop(): s.toast(Format.NOT_FOUND(n)) Eval.SOUND(Const.BAD_SOUND).play() return - s.widgets[mem['keys'].pop(n)['widget']].delete() + (wid := s.widgets.pop(mem['keys'].pop(n).get('widget', object()), None)) and wid.delete() s.toast(Strings.INFO_POPPED(n)) Eval.SOUND(Const.OK_SOUND).play() s.fresh_current_key_texts() @@ -8640,9 +8640,10 @@ def animate_key_widgets(anim_key, key_old_positions): restamp() for key_data in mem.get('keys', {}).values(): - if 'widget' in key_data: - key_offset = key_data['time'] - mem['start'] - if key_offset <= mem['duration'] and not s.widgets[key_data['widget']].exists(): + key_offset = key_data['time'] - mem['start'] + if key_offset <= mem['duration']: + existing = key_data.get('widget') + if existing is None or existing not in s.widgets or not s.widgets[existing].exists(): key_x = s.magic_x + s.entry_xs_real * (mem['start'] + key_offset) * s.entries_per_sec - s.entry_ys_real/4 key_y = s.entry_ys_real * (len(s.memory) - mem['order'] - 1) @@ -8713,8 +8714,10 @@ def animate_key_widgets(anim_key, key_old_positions): for key_data in mem.get('keys', {}).values(): key_time = key_data['time'] - mem['start'] if key_time > mem['duration']: - if 'widget' in key_data and s.widgets[key_data['widget']].exists(): - s.widgets.pop(key_data['widget']).delete() + if 'widget' in key_data: + wid = s.widgets.pop(key_data['widget'], None) + wid and wid.exists() and wid.delete() + del key_data['widget'] if mem.get('prev_off') is not None and s.window_on and s.window_on[1] == s.key_window: old_duration = mem['duration'] + 1/s.entries_per_sec From 8628c976fcd3d277d6116e0f82756ea25d21ae51 Mon Sep 17 00:00:00 2001 From: BroBordd Date: Tue, 18 Aug 2026 12:50:04 +0300 Subject: [PATCH 4/5] finder: bugfix --- plugins/utilities/finder.py | 1033 +++++++++++++++++++++++------------ 1 file changed, 677 insertions(+), 356 deletions(-) diff --git a/plugins/utilities/finder.py b/plugins/utilities/finder.py index 50d16471..13b2202b 100644 --- a/plugins/utilities/finder.py +++ b/plugins/utilities/finder.py @@ -1,323 +1,630 @@ # Copyright 2025 - Solely by BrotherBoard # Intended for personal use only -# Bug? Feedback? Telegram >> GalaxyA14user +# Bug? Feedback? Telegram >> @BroBordd """ -Finder v1.0 - Find anyone +Finder v4.1 - Find anyone -Experimental. Feedback is appreciated. -Useful if you are looking for someone, or just messing around. +Refactored with proper packet structure and optimized for high-concurrency scanning. +Uses the same elegant enum system as Proto for maintainability and correctness. Features: -- Fetch servers: Pings all servers, then sorts them by lowest -- Ability to cycle through x servers to collect users -- Ability to connect to servers by player name there - -Combine with Power plugin for better control. +- Proper packet structure using enums (readable and maintainable) +- Optimized lightweight join/roster/leave flow +- High-concurrency scanning via ThreadPoolExecutor +- Correct disconnect packets +- Sniffs out players without actually joining servers """ -from socket import socket, SOCK_DGRAM -from random import uniform as uf -from babase import Plugin, app -from threading import Thread +from json import dumps, loads +from concurrent.futures import ThreadPoolExecutor from time import time, sleep +from enum import IntEnum +from random import randint, choice as CH, uniform as uf +from socket import socket, SOCK_DGRAM +from uuid import uuid4 + +from bascenev1 import connect_to_party as CON, protocol_version as PT from bauiv1 import ( - get_ip_address_type as IPT, - clipboard_set_text as COPY, - get_special_widget as zw, - containerwidget as ocw, - screenmessage as push, - buttonwidget as obw, - scrollwidget as sw, - imagewidget as iw, - textwidget as tw, - gettexture as gt, - apptimer as teck, - getsound as gs, - getmesh as gm, - Call + get_ip_address_type as IPT, clipboard_set_text as COPY, + get_special_widget as zw, containerwidget as ocw, + screenmessage as push, buttonwidget as obw, + scrollwidget as sw, imagewidget as iw, + SpecialChar as sc, textwidget as tw, + gettexture as gt, apptimer as teck, + AppTimer as tuck, getsound as gs, + getmesh as gm, charstr as cs, + CallPartial ) -from bascenev1 import ( - disconnect_from_host as BYE, - connect_to_party as CON, - protocol_version as PT, - get_game_roster as GGR +from babase import Plugin, app + +plugman = dict( + plugin_name="finder", + description=( + "Find anyone. Experimental. Useful if you are " + "looking for someone, or just messing around. " + "For full features, either check first lines " + "of py file, or check source. Combine with " + "Power plugin for better control." + ), + external_url="https://BroBordd.github.io/byBordd", + authors=[ + { + "name": "BrotherBoard", + "email": "brobordd@gmail.com", + "discord": "BrotherBoard" + }, + ], + version='4.1', ) +# ============================================================================ +# PACKET ENUMS - Same elegant system as Proto +# ============================================================================ + +class PackEnum(IntEnum): + """Base enum for all packet types""" + @classmethod + def get(cls): + return [p for p in cls.__members__] + + def to_bytes(self): + return bytes([self.value]) + +class Packet(PackEnum): + """Main packet types""" + P_SIMPLE_PING = 11 + P_SIMPLE_PONG = 12 + P_CLIENT_REQUEST = 24 + P_CLIENT_ACCEPT = 25 + P_CLIENT_DENY = 26 + P_CLIENT_DENY_VERSION_MISMATCH = 27 + P_CLIENT_DENY_ALREADY_IN_PARTY = 28 + P_CLIENT_DENY_PARTY_FULL = 29 + P_DISCONNECT_FROM_CLIENT_REQUEST = 32 + P_CLIENT_GAMEPACKET_COMPRESSED = 36 + P_HOST_GAMEPACKET_COMPRESSED = 37 + +class ScenePacket(PackEnum): + """Scene packet types""" + SP_HANDSHAKE_RESPONSE = 16 + SP_MESSAGE = 17 + SP_DISCONNECT = 19 + +class Message(PackEnum): + """Message types""" + M_NULL = 3 + M_PARTY_ROSTER = 9 + M_MULTIPART = 13 + M_MULTIPART_END = 14 + M_CLIENT_INFO = 18 + M_CLIENT_PLAYER_PROFILES_JSON = 21 + +class Extra(PackEnum): + """Extra constants""" + PROTOCOL_VERSION_LOW = 33 + PROTOCOL_VERSION_HIGH = 0 + ACK_EXTRA = 0 + DUMMY_MN_LOW = 240 + DUMMY_MN_HIGH = 255 + DUMMY_ACK_LOW = 240 + DUMMY_ACK_HIGH = 255 + +# ============================================================================ +# OPTIMIZED PACKET BUILDER +# ============================================================================ + +class PacketBuilder: + """Optimized packet construction for high-performance scanning""" + + def __init__(self): + # Pre-build static data to avoid repeated encoding + self._spec_data = self._build_spec() + self._auth_data = self._build_auth() + self._empty_profiles = self._build_empty_profiles() + + def _build_spec(self): + """Build spec packet data once""" + return dumps({ + 's': dumps({ + 'n': 'Finder', + 'a': '', + 'sn': '' + }, separators=(',', ':')), + 'd': '69' * 20 + }, separators=(',', ':')).encode('utf-8') + + def _build_auth(self): + """Build auth packet data once""" + return dumps({ + 'b': app.env.engine_build_number, + 'tk': '', + 'ph': '' + }, separators=(',', ':')).encode('utf-8') + + def _build_empty_profiles(self): + """Build empty profiles packet data once""" + return dumps({}, separators=(',', ':')).encode('utf-8') + + def handshake_request(self, my_id: str) -> bytes: + """Build client handshake request""" + return ( + Packet.P_CLIENT_REQUEST.to_bytes() + + Extra.PROTOCOL_VERSION_LOW.to_bytes() + + Extra.PROTOCOL_VERSION_HIGH.to_bytes() + + bytes.fromhex(my_id) + + str(uuid4()).encode() + ) + + def handshake_response(self, server_id: str) -> bytes: + """Build handshake response with spec""" + return ( + Packet.P_CLIENT_GAMEPACKET_COMPRESSED.to_bytes() + + bytes.fromhex(server_id) + + ScenePacket.SP_HANDSHAKE_RESPONSE.to_bytes() + + Extra.PROTOCOL_VERSION_LOW.to_bytes() + + Extra.PROTOCOL_VERSION_HIGH.to_bytes() + + self._spec_data + ) + + def auth_message(self, server_id: str) -> bytes: + """Build auth message""" + return ( + Packet.P_CLIENT_GAMEPACKET_COMPRESSED.to_bytes() + + bytes.fromhex(server_id) + + ScenePacket.SP_MESSAGE.to_bytes() + + Extra.DUMMY_MN_LOW.to_bytes() + + Extra.DUMMY_MN_HIGH.to_bytes() + + Extra.DUMMY_ACK_LOW.to_bytes() + + Extra.DUMMY_ACK_HIGH.to_bytes() + + Extra.ACK_EXTRA.to_bytes() + + Message.M_CLIENT_INFO.to_bytes() + + self._auth_data + ) + + def profiles_message(self, server_id: str) -> bytes: + """Build empty profiles message""" + return ( + Packet.P_CLIENT_GAMEPACKET_COMPRESSED.to_bytes() + + bytes.fromhex(server_id) + + ScenePacket.SP_MESSAGE.to_bytes() + + Extra.DUMMY_MN_LOW.to_bytes() + + Extra.DUMMY_MN_HIGH.to_bytes() + + Extra.DUMMY_ACK_LOW.to_bytes() + + Extra.DUMMY_ACK_HIGH.to_bytes() + + Extra.ACK_EXTRA.to_bytes() + + Message.M_CLIENT_PLAYER_PROFILES_JSON.to_bytes() + + self._empty_profiles + ) + + def null_message(self, server_id: str) -> bytes: + """Build null message (final handshake)""" + return ( + Packet.P_CLIENT_GAMEPACKET_COMPRESSED.to_bytes() + + bytes.fromhex(server_id) + + ScenePacket.SP_MESSAGE.to_bytes() + + Extra.DUMMY_MN_LOW.to_bytes() + + Extra.DUMMY_MN_HIGH.to_bytes() + + Extra.DUMMY_ACK_LOW.to_bytes() + + Extra.DUMMY_ACK_HIGH.to_bytes() + + Extra.ACK_EXTRA.to_bytes() + + Message.M_NULL.to_bytes() + ) + + def disconnect(self, server_id: str) -> bytes: + """Build proper disconnect packet""" + return ( + Packet.P_DISCONNECT_FROM_CLIENT_REQUEST.to_bytes() + + bytes.fromhex(server_id) + ) + +# ============================================================================ +# OPTIMIZED SCANNER +# ============================================================================ + +def scan_server(address: str, port: int, packet_builder: PacketBuilder, index: int) -> tuple: + """ + Lightweight scanner: ping -> handshake -> grab roster -> disconnect + + Optimized for scanning hundreds of servers in seconds: + - Minimal socket operations + - Pre-built packets + - Fast timeout handling + - Proper disconnect + + Returns: (index, ping_ms, roster_list) + """ + ping_ms = 999 + roster = [] + sock = None + + try: + # Create socket with tight timeout + sock = socket(IPT(address), SOCK_DGRAM) + sock.settimeout(2.5) + + addr_tuple = (address, port) + + # ---- PING ---- + ping_start = time() + sock.sendto(Packet.P_SIMPLE_PING.to_bytes(), addr_tuple) + + data, recv_addr = sock.recvfrom(10) + if data != Packet.P_SIMPLE_PONG.to_bytes() or recv_addr[0] != address: + return (index, 999, []) + + ping_ms = (time() - ping_start) * 1000 + + # ---- HANDSHAKE ---- + my_id = f'{(71 + randint(0, 150)):02x}' + sock.sendto(packet_builder.handshake_request(my_id), addr_tuple) + + # Wait for accept + shake = sock.recvfrom(1024)[0] + if not shake.startswith(Packet.P_CLIENT_ACCEPT.to_bytes()): + return (index, ping_ms, []) + + server_id = f'{shake[1]:02x}' + + # Flush host info + sock.recvfrom(1024) + + # ---- MINIMAL JOIN SEQUENCE ---- + # Send only what's needed to get roster + sock.sendto(packet_builder.handshake_response(server_id), addr_tuple) + sock.sendto(packet_builder.auth_message(server_id), addr_tuple) + sock.sendto(packet_builder.profiles_message(server_id), addr_tuple) + sock.sendto(packet_builder.null_message(server_id), addr_tuple) + + # Flush acks + sock.recvfrom(1024) + sock.recvfrom(9) + + # ---- GRAB ROSTER ---- + roster_buffer = bytearray() + collecting_multipart = False + listen_start = time() + + while time() - listen_start < 1.5: # Short timeout for roster + try: + packet = sock.recvfrom(2048)[0] + + if len(packet) < 9: + continue + + # Check for host game packet with message + if (packet[0] == Packet.P_HOST_GAMEPACKET_COMPRESSED.value and + packet[2] == ScenePacket.SP_MESSAGE.value): + + msg_type = packet[8] + msg_data = packet[9:] + + # Direct roster message + if msg_type == Message.M_PARTY_ROSTER.value: + roster = loads(msg_data.rstrip(b'\x00').decode('utf-8')) + break + + # Multipart roster start + elif msg_type == Message.M_MULTIPART.value: + if msg_data and msg_data[0] == Message.M_PARTY_ROSTER.value: + collecting_multipart = True + roster_buffer.clear() + roster_buffer.extend(msg_data[1:]) + elif collecting_multipart: + roster_buffer.extend(msg_data) + + # Multipart roster end + elif msg_type == Message.M_MULTIPART_END.value and collecting_multipart: + roster_buffer.extend(msg_data) + roster = loads(roster_buffer.rstrip(b'\x00').decode('utf-8')) + break + + except: + break + + # ---- PROPER DISCONNECT ---- + sock.sendto(packet_builder.disconnect(server_id), addr_tuple) + + except: + pass + + finally: + if sock: + sock.close() + + return (index, ping_ms, roster) + +# ============================================================================ +# FINDER UI +# ============================================================================ class Finder: + VER = '4.0' COL1 = (0, 0.3, 0.3) COL2 = (0, 0.55, 0.55) COL3 = (0, 0.7, 0.7) COL4 = (0, 1, 1) COL5 = (1, 1, 0) - MAX = 0.3 - TOP = 15 - VER = '1.0' - MEM = [] - BST = [] - SL = None - + + # Class state + PRO, MEM, ART, KIDS, IKIDS = [], [], [], [], [] + P2 = ARTT = SL = TIP = None + BUSY = False + FLT = '' + def __init__(s, src): - s.thr = [] - s.ikids = [] - s.busy = False + s.sust = None s.s1 = s.snd('powerup01') c = s.__class__ - # parent z = (460, 400) - s.p = cw( + + c.P = cw( scale_origin_stack_offset=src.get_screen_space_center(), size=z, oac=s.bye )[0] - # footing - sw( - parent=s.p, - size=z, - border_opacity=0 - ) - # fetch - tw( - parent=s.p, - text='Fetch Servers', - color=s.COL4, - position=(19, 359) - ) + + sw(parent=c.P, size=z, border_opacity=0) + + tw(parent=c.P, text='Fetch all servers', color=s.COL4, position=(19, 359)) + bw( - parent=s.p, - position=(360, 343), - size=(80, 39), - label='Fetch', - color=s.COL2, - textcolor=s.COL4, + parent=c.P, position=(360, 343), size=(80, 39), + label='Fetch', color=s.COL2, textcolor=s.COL4, oac=s.fresh ) + tw( - parent=s.p, - text='Fetches, pings, and sorts public servers.', - color=s.COL3, - scale=0.8, - position=(15, 330), - maxwidth=320 + parent=c.P, text='Sniff out players without joining', + color=s.COL3, scale=0.8, position=(15, 330), maxwidth=320 ) - # separator + iw( - parent=s.p, - size=(429, 1), - position=(17, 330), - texture=gt('white'), - color=s.COL2 + parent=c.P, size=(429, 1), position=(17, 330), + texture=gt('white'), color=s.COL2 ) - # cycle - tw( - parent=s.p, - text='Cycle Servers', - color=s.COL4, - position=(19, 294) + + c.ARTT = tw( + parent=c.P, + text='' if c.ART else f'Finder v{c.VER}\n{CH(lmao())}', + maxwidth=430, max_height=125, + h_align='center', v_align='top', + color=s.COL4, position=(205, 295) ) - bw( - parent=s.p, - position=(360, 278), - size=(80, 39), - label='Cycle', - color=s.COL2, - textcolor=s.COL4, - oac=s.find - ) - tw( - parent=s.p, - text='Cycles through best servers and saves their players.', - color=s.COL3, - scale=0.8, - position=(15, 265), - maxwidth=320, - v_align='center' - ) - # separator + iw( - parent=s.p, - size=(429, 1), - position=(17, 265), - texture=gt('white'), - color=s.COL2 - ) - # top - tw( - parent=s.p, - text='Server Cycle Limit', - color=s.COL4, - position=(19, 230) + parent=c.P, size=(429, 1), position=(17, 200), + texture=gt('white'), color=s.COL2 ) - s.top = tw( - parent=s.p, - position=(398, 228), - size=(80, 50), - text=str(c.TOP), + + c.FT = tw( + parent=c.P, position=(23, 150), size=(201, 35), + text=c.FLT, editable=True, glow_type='uniform', + allow_clear_button=False, v_align='center', color=s.COL4, - editable=True, - h_align='center', - v_align='center', - corner_scale=0.1, - scale=10, - allow_clear_button=False, - shadow=0, - flatness=1, + description='Raw search - Matches wildcard to all strings' ) - tw( - parent=s.p, - text='Maximum number of servers to cycle.', - color=s.COL3, - scale=0.8, - position=(15, 201), - maxwidth=320 - ) - # separator - iw( - parent=s.p, - size=(429, 1), - position=(17, 200), - texture=gt('white'), - color=s.COL2 - ) - # players - pl = s.plys() - sy = max(len(pl)*30, 140) + + s.ft2 = tw(parent=c.P, position=(26, 153), text='Search', color=s.COL3) + p1 = sw( - parent=s.p, - position=(20, 18), - size=(205, 172), - border_opacity=0.4 + parent=c.P, position=(20, 18), size=(205, 122), + border_opacity=0.4, color=s.COL4 ) - p2 = ocw( - parent=p1, - size=(205, sy), - background=False + + c.P2 = ocw(parent=p1, size=(205, 1), background=False) + + s.pltip = tw( + parent=c.P, position=(90, 100), + text='Sniff some servers\nto collect players\nResults vary by\ntime and connection', + color=s.COL4, maxwidth=175, h_align='center' ) - 0 if pl else tw( - parent=s.p, - position=(90, 100), - text='Cycle some servers\nto collect players', - color=s.COL4, - maxwidth=175, - h_align='center' - ) - s.kids = [] - for _, g in enumerate(pl): - p, a = g - s.kids.append(tw( - parent=p2, - size=(200, 30), - selectable=True, - click_activate=True, - color=s.COL3, - text=p, - position=(0, sy-30-30*_), - maxwidth=175, - on_activate_call=Call(s.hl, _, p), - v_align='center' - )) - # info + iw( - parent=s.p, - position=(235, 18), - size=(205, 172), + parent=c.P, position=(235, 18), size=(205, 172), texture=gt('scrollWidget'), mesh_transparent=gm('softEdgeOutside'), opacity=0.4 ) - s.tip = tw( - parent=s.p, - position=(310, 98), - text='Select something to\nview server info', - color=s.COL4, - maxwidth=170, - h_align='center' - ) if c.SL is None else 0 - + + s.tip = 'Select something to\nview server info' + c.TIP = tw( + parent=c.P, position=(310, 98), + text=s.tip, color=s.COL4, + maxwidth=170, h_align='center' + ) + + s.draw() if c.ART else 0 + s.up() + c.SL and s.info(c.SL) + c.FL = tuck(0.1, s.flup, repeat=True) + + def flup(s): + c = s.__class__ + if not s.ft2.exists(): + c.FL = None + return + ct = tw(query=c.FT) + tw(s.ft2, text=['Search', ''][bool(ct)]) + if ct != s.FLT: + c.FLT = ct + s.up() + def hl(s, _, p): - [tw(t, color=s.COL3) for t in s.kids] - tw(s.kids[_], color=s.COL4) + c = s.__class__ + c.SL = p + for w in c.KIDS: + tw(w, color=s.COL3) + w = c.KIDS[_] + tw(w, color=s.COL4) + ocw(c.P2, visible_child=w) s.info(p) - + def info(s, p): - [_.delete() for _ in s.ikids] - s.ikids.clear() - s.tip and s.tip.delete() - bst = s.__class__.BST - for _ in bst: - for r in _['roster']: - if r['display_string'] == p: - i = _ - break + c = s.__class__ + for _ in c.IKIDS: + _.delete() + c.IKIDS.clear() + tw(c.TIP, text='') + + i = None + for _ in c.MEM: + for r in _.get('roster', []): + try: + # Safely decode spec with error handling for control characters + spec_raw = r.get('spec', '') + if isinstance(spec_raw, bytes): + # Decode bytes, replacing invalid characters + spec_str = spec_raw.decode('utf-8', errors='replace') + else: + spec_str = str(spec_raw) + + # Remove control characters before parsing + import re + # Remove all control characters except newline, carriage return, tab + spec_str = re.sub(r'[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]', '', spec_str) + + # Parse the cleaned JSON + spec = loads(spec_str) + + if spec.get('n') == p: + i = _ + pz = r['p'] + break + except (ValueError, KeyError, TypeError) as e: + # Skip malformed entries + continue + + if i is None: + c.SL = None + tw(c.TIP, text=s.tip) + return + for _ in range(3): t = str(i['nap'[_]]) - s.ikids.append(tw( - parent=s.p, - position=(250, 155-40*_), - h_align='center', - v_align='center', - maxwidth=175, - text=t, - color=s.COL4, - size=(175, 30), - selectable=True, - click_activate=True, - on_activate_call=Call(s.copy, t) + px = [250, 245, 375][_] + py = [155, 115][bool(_)] + sx = [175, 115, 55][_] + c.IKIDS.append(tw( + parent=c.P, position=(px, py), + h_align='center', v_align='center', + maxwidth=sx, text=t, color=s.COL4, + size=(sx, 30), selectable=True, + click_activate=True, glow_type='uniform', + on_activate_call=CallPartial(s.copy, t) )) - s.ikids.append(bw( - parent=s.p, - position=(253, 30), - size=(166, 30), - label='Connect', - color=s.COL2, - textcolor=s.COL4, - oac=Call(CON, i['a'], i['p'], False) + + c.IKIDS.append(bw( + parent=c.P, position=(253, 65), size=(166, 30), + label=p, color=s.COL2, textcolor=s.COL4, + oac=CallPartial( + s.oke, + '\n'.join([' | '.join([str(j) for j in _.values()]) for _ in pz]) or 'Nothing' + ) )) - + + c.IKIDS.append(bw( + parent=c.P, position=(253, 30), size=(166, 30), + label='Connect', color=s.COL2, textcolor=s.COL4, + oac=CallPartial(CON, i['a'], i['p'], False) + )) + + def oke(s, t): + TIP(t) + s.ding(1, 1) + def copy(s, t): s.ding(1, 1) TIP('Copied to clipboard!') COPY(t) - + def plys(s): z = [] - me = app.plus.get_v1_account_name() - me = [me, '\ue063'+me] - for _ in s.__class__.BST: + c = s.__class__ + for _ in c.MEM: a = _['a'] if (r := _.get('roster', {})): for p in r: - ds = p['display_string'] - 0 if ds in me else z.append((ds, a)) - return sorted(z, key=lambda _: _[0].startswith('\ue030Server')) - + try: + # Safely decode spec + spec_raw = p.get('spec', '') + if isinstance(spec_raw, bytes): + spec_str = spec_raw.decode('utf-8', errors='replace') + else: + spec_str = str(spec_raw) + + # Remove control characters + import re + spec_str = re.sub(r'[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]', '', spec_str) + + ds = loads(spec_str)['n'] + except (ValueError, KeyError, TypeError): + continue + + 0 if (ds == 'Finder' or (c.FLT and not s.chk(r))) else z.append((ds, a)) + return sorted(z, key=lambda _: _[0].startswith('Server')) + + def chk(s, r): + t = s.__class__.FLT.lower() + for _ in r: + try: + # Safely decode spec + spec_raw = _.get('spec', '') + if isinstance(spec_raw, bytes): + spec_str = spec_raw.decode('utf-8', errors='replace') + else: + spec_str = str(spec_raw) + + # Remove control characters + import re + spec_str = re.sub(r'[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]', '', spec_str) + + n = loads(spec_str)['n'] + if n != 'Finder' and t in n.lower(): + return True + except (ValueError, KeyError, TypeError): + continue + + try: + for p in _.get('p', []): + if t in p.get('nf', '').lower(): + return True + except (AttributeError, TypeError): + continue + return False + def snd(s, t): l = gs(t) l.play() teck(uf(0.14, 0.18), l.stop) return l - + def bye(s): s.s1.stop() - ocw(s.p, transition='out_scale') + c = s.__class__ + ocw(c.P, transition='out_scale') l = s.snd('laser') - def f(): return teck(0.01, f) if s.p else l.stop() + f = lambda: teck(0.01, f) if c.P else l.stop() f() - - def ding(s, i, j): + + def ding(s, *z): a = ['Small', ''] - x, y = a[i], a[j] - s.snd('ding'+x) - teck(0.1, gs('ding'+y).play) - + for i, _ in enumerate(z): + h = 'ding' + a[_] + teck(i / 10, CallPartial(s.snd, h) if i < (len(z) - 1) else gs(h).play) + def fresh(s): - if s.busy: - BTW("Still busy!") + c = s.__class__ + if c.BUSY: + TIP("Still busy!") + s.ding(0, 0) return - TIP('Fetching servers...') + + TIP('Scanning servers!\nThis should take a few seconds!\nYou can close this window.') + c.ST = time() s.ding(1, 0) - s.busy = True + c.BUSY = True + p = app.plus p.add_v1_account_transaction( { @@ -325,121 +632,117 @@ def fresh(s): 'proto': PT(), 'lang': 'English' }, - callback=s.kang, + callback=s.kang ) p.run_v1_account_transactions() - + def kang(s, r): c = s.__class__ c.MEM = r['l'] - s.thr = [] - for _ in s.__class__.MEM: - t = Thread(target=Call(s.ping, _)) - s.thr.append(t) - t.start() - teck(s.MAX*4, s.join) - - def join(s): + c.ART = [cs(sc.OUYA_BUTTON_U)] * len(c.MEM) + c.PRO.clear() + + # Create packet builder once for all scans + packet_builder = PacketBuilder() + + # High concurrency for fast scanning + executor = ThreadPoolExecutor(max_workers=256) + + c.THR = [ + executor.submit( + scan_server, + _['a'], + _['p'], + packet_builder, + i + ) for i, _ in enumerate(c.MEM) + ] + + s.sus_starter() + + def sus_starter(s): + if not s.sust: + s.sust = tuck(0.01, s.sus, repeat=True) + + def sus(s): c = s.__class__ - [t.join() for t in s.thr] - far = s.MAX*3000 - c.MEM = [_ for _ in c.MEM if _['ping']] - c.MEM.sort(key=lambda _: _['ping']) - s.thr.clear() - TIP(f'Loaded {len(c.MEM)} servers!') - s.ding(0, 1) - s.busy = False - - def find(s): - if s.busy: - BTW("Still busy!") - return - c = s.__class__ - if not c.MEM: - BTW('Fetch some servers first!') - return - t = tw(query=s.top) - if not t.isdigit(): - BTW('Invalid cycle limit!') - return - top = int(t) - if not (0 < top < len(c.MEM)): - BTW('Cycle count is too '+['big', 'small'][top <= 0]+'!') - return - c.TOP = top - s.ding(1, 0) - TIP('Starting cycle...') - s.busy = True - s.ci = s.lr = 0 - c.BST = c.MEM[:top] - s.cycle() - - def cycle(s): - _ = s.__class__.BST[s.ci] - s.ca = _['a'] - CON(s.ca, _['p'], False) - s.wait() - - def wait(s, i=5): - r = GGR() - if (r != s.lr) and r: - s.__class__.BST[s.ci]['roster'] = s.lr = r - return s.next() - if not i: - s.__class__.BST[s.ci]['roster'] = [] - return s.next() - teck(0.1, Call(s.wait, i-1)) - - def next(s): - s.ci += 1 - if s.ci >= len(s.__class__.BST): - BYE() - teck(0.5, s.yay) - return - s.cycle() - - def yay(s): - TIP('Cycle finished!') - s.ding(0, 1) - s.busy = False - zw('squad_button').activate() - teck(0.3, byBordd.up) - - def ping(s, _): - sock = ping = None - a, p = _['a'], _['p'] - sock = socket(IPT(a), SOCK_DGRAM) - try: - sock.connect((a, p)) - except: - ping = None - else: - st = time() - sock.settimeout(s.MAX) - yes = False - for _i in range(3): + + # Process completed scans + for future in c.THR[:]: + if future.done(): try: - sock.send(b'\x0b') - r = sock.recv(10) + index, ping, roster = future.result() + c.MEM[index]['ping'] = ping + c.MEM[index]['roster'] = roster + + # Update art + c.ART[index] = ( + cs(sc.OUYA_BUTTON_A) if ping == 999 else + cs(sc.OUYA_BUTTON_O) if ping < 100 else + cs(sc.OUYA_BUTTON_Y) + ) + + c.THR.remove(future) + s.draw() if c.ARTT.exists() else None except: - r = None - if r == b'\x0c': - yes = True - break - sleep(s.MAX) - ping = (time()-st)*1000 if yes else None - finally: - _['ping'] = ping - sock.close() + pass + + # Check if all done + if not c.THR: + s.sust = None + s.done() + + def draw(s): + c = s.__class__ + tw(c.ARTT, text=('\n'.join(''.join(c.ART[i:i + 40]) for i in range(0, len(s.ART), 40)))) + s.up() + + def up(s): + c = s.__class__ + [_.delete() for _ in c.KIDS] + c.KIDS.clear() + pl = s.plys() + s.pltip.delete() if pl else 0 + sy = max(len(pl) * 30, 90) + ocw(c.P2, size=(205, sy)) + dun = 0 + for _, g in enumerate(pl): + p, a = g + tt = tw( + parent=c.P2, size=(200, 30), + selectable=True, click_activate=True, + glow_type='uniform', + color=[s.COL3, s.COL4][p == c.SL and not dun], + text=p, position=(0, sy - 30 - 30 * _), + maxwidth=175, + on_activate_call=CallPartial(s.hl, _, p), + v_align='center' + ) + if not dun and p == c.SL: + ocw(c.P2, visible_child=tt) + dun = 1 + c.KIDS.append(tt) + + def done(s): + c = s.__class__ + s.ding(0, 1) + tt = time() - c.ST + ln = len(s.MEM) + ab = int(ln / tt) + TIP(f'Finished!\nScanned {ln} servers in {round(tt, 2)} seconds!\nAbout {ab} server{"s" if ab != 1 else ""}/sec') + s.__class__.BUSY = False +# ============================================================================ +# UI HELPERS +# ============================================================================ -# Patches bw = lambda *, oac=None, **k: obw( texture=gt('white'), on_activate_call=oac, enable_sound=False, **k ) + cw = lambda *, size=None, oac=None, **k: (p := ocw( parent=zw('overlay_stack'), background=False, @@ -450,8 +753,8 @@ def ping(s, _): )) and (p, iw( parent=p, texture=gt('softRect'), - size=(size[0]*1.2, size[1]*1.2), - position=(-size[0]*0.1, -size[1]*0.1), + size=(size[0] * 1.2, size[1] * 1.2), + position=(-size[0] * 0.1, -size[1] * 0.1), opacity=0.55, color=(0, 0, 0) ), iw( @@ -461,38 +764,56 @@ def ping(s, _): color=Finder.COL1 )) -# Global - - -def BTW(t): return (push(t, color=(1, 1, 0)), gs('block').play()) -def TIP(t): return push(t, Finder.COL3) +TIP = lambda t: push(t, Finder.COL3) + +lmao = lambda: [ + 'Who are we looking for this time?', + 'Press on Fetch, and I\'ll do the rest.', + 'Let\'s legally stalk all servers!', + 'Let\'s list them all!', + 'Relax. We can find them.', + 'Lost your friend? Let\'s find them!', + 'Looking for players? I can help!', + 'Cool art appears here. Fetch already!', + 'Let\'s hear some "How did u find me!?"', + 'Ready as ever. Press on Fetch!', + 'Let\'s sniff out some packets!', + 'Who\'s there? I\'ll see myself!', + 'They can\'t hide!! Muahahaha-', + 'Why did I put a random tip here?', + 'We\'re having rosters for dinner!' +] + +# ============================================================================ +# PLUGIN EXPORT +# ============================================================================ # ba_meta require api 9 # ba_meta export babase.Plugin - - class byBordd(Plugin): BTN = None - + has_settings_ui = lambda s: True + show_settings_ui = lambda s, w: Finder(w) + @classmethod def up(c): c.BTN.activate() if c.BTN.exists() else None - + def __init__(s): from bauiv1lib import party p = party.PartyWindow a = '__init__' o = getattr(p, a) setattr(p, a, lambda z, *a, **k: (o(z, *a, **k), s.make(z))[0]) - + def make(s, z): sz = (80, 30) p = z._root_widget - x, y = (-60, z._height-45) + x, y = (-60, z._height - 45) iw( parent=p, - size=(sz[0]*1.34, sz[1]*1.4), - position=(x-sz[0]*0.14, y-sz[1]*0.20), + size=(sz[0] * 1.34, sz[1] * 1.4), + position=(x - sz[0] * 0.14, y - sz[1] * 0.20), texture=gt('softRect'), opacity=0.2, color=(0, 0, 0) From 9588673f7d6e17d5c12d3e3f8ad7e8e74ef03ba6 Mon Sep 17 00:00:00 2001 From: BroBordd Date: Tue, 18 Aug 2026 14:53:33 +0300 Subject: [PATCH 5/5] added coolbox v1.0 --- plugins/utilities/coolbox.py | 8101 ++++++++++++++++++++++++++++++++++ 1 file changed, 8101 insertions(+) create mode 100755 plugins/utilities/coolbox.py diff --git a/plugins/utilities/coolbox.py b/plugins/utilities/coolbox.py new file mode 100755 index 00000000..1c78d29b --- /dev/null +++ b/plugins/utilities/coolbox.py @@ -0,0 +1,8101 @@ +# Copyright 2026 - Solely by BrotherBoard +# Intended for personal use only +# Bug? Feedback? Telegram >> @GalaxyA14user + +""" +Coolbox v1.0 - Own the scene. + +Experimental - Feedback is appreciated. +Coolbox is an advanced menu that can do almost everything! +Access the menu from pause menu. +This is a clean refactored version of Sandbox. Written from scratch. +The code itself acts like a closed environment. +Coolbox uses a lot of scene features for the sake of appearance. +""" + +import bauiv1lib.ingamemenu as bui_igm +import babase as ba +import _babase as _ba +import bauiv1 as bui +import bascenev1 as bs +import random +import bascenev1lib.actor.powerupbox as bs_powerupbox +import bascenev1lib.actor.playerspaz as bs_playerspaz +import bascenev1lib.gameutils as bs_gameutils +import bascenev1lib.actor.bomb as bs_bomb +import bascenev1lib.actor.spazbot as bs_spazbot +import bascenev1lib.actor.spaz as bs_spaz +import traceback +import zlib +import inspect +import os.path +import math +import weakref +import time +import json +import os +import uuid + +plugman = dict( + plugin_name="coolbox", + description=( + "Coolbox is an advanced menu that can do almost everything! " + "Access the menu from pause menu. " + "This is a clean refactored version of Sandbox. Written from scratch." + ), + external_url="https://BroBordd.github.io/byBordd", + authors=[ + { + "name": "BrotherBoard", + "email": "brobordd@gmail.com", + "discord": "BrotherBoard" + }, + ], + version='1.0', +) + +caught = {} +_rgb_cycle_combines = [] + +def catch(i,j): + with bs.get_foreground_host_activity().context: + caught[bs.time()] = (i,j) + print(i,j) + +def record(): + caught.clear() + me = getme(1) + getme().resetinput() + LN({ + 'JUMP_PRESS': lambda: catch(0,1), + 'BOMB_PRESS': lambda: catch(1,1), + 'PICK_UP_PRESS': lambda: catch(2,1), + 'PUNCH_PRESS': lambda: catch(3,1), + 'JUMP_RELEASE': lambda: catch(0,0), + 'BOMB_RELEASE': lambda: catch(1,0), + 'PICK_UP_RELEASE': lambda: catch(2,0), + 'PUNCH_RELEASE': lambda: catch(3,0), + }) + +_last_time = -1 +def omg(): + global _last_time + with bs.get_foreground_host_activity().context: + t = bs.time() + if _last_time > t: + record() + _last_time = t + +_gaygay = None +def gaygay(): + global _gaygay + _gaygay = bs.AppTimer(0.01,omg,repeat=True) +#bs.apptimer(3,gaygay) + +class Coolbox: + @classmethod + def state(s,t=0): + bui.apptimer(0 if t else 0.13, bs.CallPartial(bui.buttonwidget, s.in_source, icon=bui.gettexture(f"chest{'Open' if t else ''}Icon"))) + def __init__(s, fresh=True, in_source=None, fb=None, fake=False, extra=None): + # safe + if not bs.get_foreground_host_activity(): btw(random.choice(NAH())); return + if STATE(): btw('Coolbox is already running!'); return + if in_source: + s.__class__.in_source = in_source + s.state(1) + z = icw( + title='Coolbox', + shadow=True, + show_back=False, + show_nuke=not fake, + show_tog=not fake, + source_cls=s.__class__, + nuke_anim='out_scale' if in_source else 'out_right', + transition='in_scale' if fresh and in_source else 'in_right', + in_source=in_source, + out_source=in_source, + note='CoolBox is a clean version of SandBox, written from scratch.', + note_scale=0.5 + ) + # Buttons + x,y = z.size + w = z.widget + mem = globals() + kids = [] + for k in range(5): + for j in range(3): + l,i = s.get(j,k) + b = bw( + p=w, + label=l, + pos=(47+171.5*j,300-64*k), + size=(173,64), + icon=bui.gettexture(i), + ) + bcls = mem[l] + args = (w,b,i,in_source,s.__class__,extra) + if not fake: bw(b,oac=bs.CallPartial(bcls, *args)) + if fb == bcls.__name__: bui.apptimer(0.5, bs.CallPartial(bcls, *args)) + kids.append(b) + """Icons and names""" + def get(s,j,k): + return ( + ('Spawn','cuteSpaz'), + ('Modify','advancedIcon'), + ('Control','controllerIcon'), + ('Effect','graphicsIcon'), + ('Deploy','star'), + ('Listen','audioIcon'), + ('Build','cursor'), + ('Tweak','menuIcon'), + ('Tune','settingsIcon'), + ('Gather','achievementTeamPlayer'), + ('Load','inventoryIcon'), + ('Quick','nextLevelIcon'), + ('Scene','nub'), + ('Camera','tv'), + ('About','heart') + )[k*3+j] + +class Bot(bs_spazbot.SpazBot): + """A Bombsquad Bot""" + def __init__( + s, + char=None, + ignore=False, + name='Bot', + name_color=(1,1,1), + mesh=[], + ctex=None, + ctex2=None + ) -> None: + mem = COLS() + bs_spaz.Spaz.__init__( + s, + color=var(mem[0]), + highlight=var(mem[1]), + character=char or var('char'), + start_invincible=False, + can_accept_powerups=True + ) + n = s.node + n.name, n.name_color = name, name_color + n.color_texture, n.color_mask_texture = bs.gettexture(ctex), bs.gettexture(ctex2) + a = DIR('mesh') + for i,m in enumerate(mesh): + if not m: continue + setattr(n,a[i],bs.getmesh(m)) + s.act = [] + s.uid = None + s.group_id = var('lastid') + s.held_count = 0 + s.last_player_attacked_by = None + s.last_player_held_by = None + ignore if ignore else bs.timer(0.1,s.load) + def move(s,x=32767,y=32767): + s.on_move_left_right(x) + s.on_move_up_down(y) + def stop(s): + s.move(0,0) + def move_to(s,t,min=0.7,time=10,chain=None): + nah = 0 + def f(b=False): + nonlocal nah + if nah: s.stop(); return + try: p = s.node.position + except: return + if t == p: return + dx = t[0]-p[0] + dz = p[2]-t[2] + if b: + m = (dx**2+dz**2)**0.5 + try: s.move(dx/m,dz/m) + except ZeroDivisionError: pass + if math.dist((p[0],p[2]),(t[0],t[2])) < min: + nah = 1 + s.stop() + if (chain is not None): s.wait(0.2,chain) + return + bs.timer(0.01,f) + def g(): + nonlocal nah + if (not nah) and (chain is not None): s.wait(0.2,chain) + nah = 1 + f(True) + bs.timer(time,g) + def follow(s,n,min=0.7,time=10,chain=None): + nah = 0 + def f(): + nonlocal nah + if nah or not n.exists(): s.stop(); return + try: + p = s.node.position + t = n.position + except: return + if t == p: return + dx = t[0]-p[0] + dz = p[2]-t[2] + m = (dx**2+dz**2)**0.5 + if (min is not None) and math.dist((p[0],p[2]),(t[0],t[2])) < min: + s.stop() + nah = 1 + if (chain is not None): s.wait(0.2,chain) + return + else: + try: s.move(dx/m,dz/m) + except ZeroDivisionError: pass + bs.timer(0.01, f) + def g(): + nonlocal nah + if (not nah) and (chain is not None): s.wait(0.2,chain) + nah = 1 + f() if n is not None else n + if time is not None: bs.timer(time,g) + def wait(s,i,chain=None): + if chain is not None: bs.timer(i,lambda: s.load(chain+1)) + def key(s,i,chain=None): + [getattr(s,f"on_{['jump','bomb','pickup','punch'][i]}_{['press','release'][j]}")() for j in [0,1]] + if chain is not None: s.wait(0.2,chain) + def say(s,t,u,chain=None): + if not s.node.exists(): return + bs.timer( + [0,0.1][not chain], + lambda: Bubble( + node=s.node, + text=t, + time=int(u), + color=s.node.color, + ) + ) + if chain is not None: s.wait(0.2,chain) + def load(s,i=None): + if i is None: s.act = var('act').copy(); i = 0 + try: a = s.act[i] + except: return + t = a[0] + if t == 0: s.say(*a[1:],i) + elif t == 1: s.move_to(*a[1:],i) + elif t == 2: s.wait(a[1],i) + elif t == 3: + j = a[1]-1 + k = a[2]-1 + if k < 0: s.load(i+1); return + s.act[i] = [t,j+1,k-1] + s.load(j) + elif t == 4: s.key(a[1],i) + elif t == 5: + who = a[1] + if who == 'me': n = getme(1).node + else: + n = None + for j in bs.getnodes(): + if str(j) == who: n = j; break + s.follow(n,*a[2:],i) + +class Bubble: + __doc__ = (""" + Highly customizable floating bubble + + Arguments: + - node: bascenev1.Node to follow (*required) + - text: the text to show in bubble + - color: the color of bubble + - time: how long to show bubble (in seconds) + - mode: how should the text animate + + Supported modes: + - 0: random (default) + - 1: text pops up with bubble + - 2: text slides letter by letter + - 3: text fades in letter by letter + - 4: text comes up from bubble + - 5: text waves in letter by letter + + Besides copy pasting, inline usage varies: + >>> from bubble import Bubble + >>> bub = Bubble( + ... text='Kill me please', + ... color=(0.2,0.8,0.9), + ... node=bot.node, + ... time=6 + ... ) + >>> + """) + __mem__ = {} + def __init__( + s, + node: 'bascenev1.Node', + text: str = 'Hello!', + color: tuple = (1,1,1), + time: float | int = 4, + mode: int = 0, + res: list = [('█'),('▼')] + ) -> None: + if not 0 <= mode <= 5 : raise ValueError(f'mode can be an integer from 0 to 5, not {mode}') + if not mode: mode = random.choice([1,2,3,4,5]) + s.ans,s.kids,s.mats,s.time = [],[],[],time + s.node,s.dead,s.text = node,False,text + s.color,s.mode,s.res = color,mode,res + # destroy existing bubbles if possible + s.mem = lambda: s.__class__.__mem__ + m = s.mem() + o = m.get(node,0) + if not getattr(o,'dead',1): bs.timer(0.2,bs.CallPartial(o.delete,force=True)) + s.show() + m[node] = s + def show(s): + q,l,r = s.mats,s.kids,s.ans + # offset + m = bs.newnode( + 'math', + owner=s.node, + attrs={ + 'input1': (0,1.65,0), + 'operation': 'add' + } + ) + q.append(m) + # the bubble + c = list(s.color) + w = GSW(s.res[0]) + b = bs.newnode( + 'text', + owner=m, + attrs={ + 'text': f'{math.ceil((GSW(s.text)+2*w)/w)*s.res[0]}\n{s.res[1]}', + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': (c[0],c[1],c[2],0.2), + 'scale': 0.01, + 'h_align': 'center' + } + ) + l.append(b) + # the text + txt = [] + mat = [] + kek = -GSW(s.text)/185 + sf = 0 + for i in range(len(s.text)): + j = s.text[i] + x = GSW(j)/95.0 + p1 = bs.newnode( + 'text', + owner=m, + attrs={ + 'text': j, + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': s.color, + 'scale': 0.01, + 'h_align': 'left' + } + ) + txt.append(p1) + ok = kek+sf + p2 = bs.newnode( + 'math', + owner=m, + attrs={ + 'input1': (ok,1.65,0), + 'operation': 'add' + } + ) + mat.append([p2,ok]) + s.node.connectattr('position',p2,'input2') + p2.connectattr('output',p1,'position') + sf += x + l += txt + q += [mat[i][0] for i in range(len(mat))] + # connect + s.node.connectattr('position',m,'input2') + m.connectattr('output',b,'position') + # hardcoded animators + # conditionally used based on animation + z = s.time + # scale bubble in out + a = bs.animate( + b, + 'scale', + { + 0:0, + z*0.041: 0.014, + z*0.154: 0.014, + z*0.167: 0.010, + z*0.98: 0.010, + z:0 + }, + ) + r.append(a) + # move bubble up down + a = bs.animate_array( + m, + 'input1', + 3, + { + 0:(0,1.2,0), + z*0.04:(0,1.65,0), + z*0.98:(0,1.65,0), + z:(0,1.2,0) + } + ) + r.append(a) + # scale text in out + r += [ + bs.animate( + txt[i], + 'scale', + { + 0:0, + z*0.041: 0.015, + z*0.154: 0.015, + z*0.167: 0.010, + z*0.98: 0.010, + z:0 + }, + ) + for i in range(len(mat)) + ] if s.mode in [1,4] else [] + # move text up down + r += [ + bs.animate_array( + mat[i][0], + 'input1', + 3, + { + 0:(mat[i][1]/4,1.2,0), + z*0.04:(mat[i][1]*1.5,1.65,0), + z*0.154:(mat[i][1]*1.5,1.65,0), + z*0.167:(mat[i][1],1.65,0), + z*0.98:(mat[i][1],1.65,0), + z:(mat[i][1]/4,1.2,0) + } + ) + for i in range(len(mat)) + ] if s.mode in [1,4] else [] + # slide in overshoot letter by letter + ok = (z*0.04*1.6) + hm = [0.03,0.05][s.mode==2] + r += [ + bs.animate_array( + j[0], + 'input1', + 3, + { + 0.5+i*hm:(j[1],1.4,0), + 0.5+i*hm+(ok*0.6):(j[1],1.9,0), + 0.5+i*hm+ok:(j[1],1.65,0), + (z-(z*0.02)):(j[1],1.65,0), + z:(j[1],1.2,0) + } + ) + for i,j in enumerate(mat) + ] if s.mode in [2,5] else [] + # fade in letter by letter + r += [ + bs.animate( + txt[i], + 'opacity', + { + 0.5+i*hm:0, + (0.5+i*hm+ok)*0.98:1, + z*0.9:1, + z:0 + } + ) + for i in range(len(mat)) + ] if s.mode in [2,4,5] else [] + # scale slide up text + r += [ + bs.animate( + txt[i], + 'scale', + { + 0:0, + z*0.154: 0, + z*0.167: 0.010, + z*0.98: 0.010, + z:0 + }, + ) + for i in range(len(mat)) + ] if s.mode == 3 else [] + # autokill + bs.timer(z,s.delete) + def delete(s,force=False): + if s.dead: return + s.dead = True + [i.delete() for i in s.ans if hasattr(i,'delete')] + bs.timer(0.2,lambda:[i.delete() for i in s.kids+s.mats if hasattr(i,'delete')]) + if not force: return + [bs.animate( + i, + 'opacity', + { + 0:i.opacity, + 0.2:0 + } + ) for i in s.kids] + +class icw: + """Instant container maker""" + INS = [] + @classmethod + def on_resume(cls): + bui.apptimer(0.01,lambda: [i.update() for i in cls.INS]) + def __del__(s): + s.__class__.INS.delete(s) + def snd(s,t): + l = bui.getsound(t) + l.play() + bui.apptimer(random.uniform(0.14,0.18),l.stop) + return l + def __init__( + s, title, shadow=False, icon=None, show_tog=False, + back_anim='out_right', nuke_anim='out_scale', + on_back=lambda: None, on_nuke=lambda: None, + show_nuke=True, show_back=True, in_source=None, + out_source=None, cls=None, source_cls=None, + outside_action=None, note=None, note_scale=0.7, + note_pos=(50,15), extra=None, auto_offset=True, + *a, **k + ) -> None: + s.snd('powerup01') if in_source else 0 + s.__class__.INS.append(s) + r = bui.get_virtual_screen_size() + k.update({ + 'parent':gos(), + 'transition':'in_scale' if in_source else 'in_right', + 'size':k.get('size',0) or (600,450), + 'on_outside_click_call':lambda:s.nuke(True) if outside_action == 'nuke' else s.back(True) if outside_action == 'back' else lambda:None, + 'scale':[1.5,1.1,0.8][UIS()], + 'scale_origin_stack_offset':gc(in_source) if in_source else (0,0), + 'position':(r[0]*0.8,r[1]*0.05) + }) + s.size = k['size'] + s.ouis = bui.app.ui_v1.uiscale + x,y = s.size + s.widget = w = cw(*a,**k) + s.ao = auto_offset + s.cls = cls(w,x,y,in_source,source_cls,extra) if cls else None + s.on_back = on_back + s.on_nuke = on_nuke + s.nuke_anim = nuke_anim + s.back_anim = back_anim + s.icon = icon + s.in_source = in_source + s.out_source = out_source + s.source_cls = source_cls + # Exit buttons + c = var('bg') + if show_back: + b = sbw( + p=w, + button_type='backSmall', + pos=([30,55][s.size[0]>400],y-70), + oac=s.back, + label=bui.charstr(bui.SpecialChar.BACK), + ) + cw(w,cancel_button=b) + s.back_button = b + if show_nuke: + b = bw( + p=w, + pos=(510,y-70), + oac=s.nuke, + size=(43,43), + label=bui.charstr(bui.SpecialChar.CLOSE), + ) + if not show_back: cw(w,cancel_button=b) + s.nuke_button = b + if show_tog: + b = bw( + p=w, + size=(43,43), + pos=(455,y-70), + oac=s.toggle, + ) + s.tog_button = b + s.toggle(True) + # Icon shadow title + c = var('t') + for i in range(2): + if not i and not shadow: continue + tw( + p=w, + text=title, + scale=1.5, + h_align='left' if icon else 'center', + v_align='center', + color=c if i else darken(c), + pos=(x/2.2+i*5,y-65+i*2), + maxwidth=x/1.8, + ) + if icon: + bui.imagewidget( + parent=w, + position=(215,y-75), + size=(50,50), + texture=bui.gettexture(icon) + ) + if note: + tw( + p=w, + pos=note_pos, + color=(1,1,1,0.2), + text='* '+note, + scale=note_scale + ) + s.update() + """Update""" + def update(s): + if not s.widget.exists() or s.widget.transitioning_out: return + r = bui.get_virtual_screen_size() + i = UIS() + j = UIS(1) + if s.ouis != j: + s.ouis = j + [bui.apptimer(z,bs.CallPartial(bui.app.set_ui_scale,j)) for z in [0.01,0.1]] + a,b = [(0.2,0.007),(0.3,-0.0034),(0.33,0.0026)][i] + ef = s.in_source + of = gc(ef) if ef and ef.exists() else None + cw( + s.widget, + stack_offset=( + (a*s.size[0]/600)*r[0], + r[1]*(b*s.size[1]/450) + ) if s.ao else of, + scale=[1.5,1.1,0.8][i], + scale_origin_stack_offset=of + ) + """Class specific""" + def prekill(s): + s.snd('laser') + s.cls.back() if s.cls and hasattr(s.cls,'back') else None + """On back""" + def back(s, by_button=False): + if by_button: + try: s.back_button.activate() + except: pass + else: + s.prekill() + s.on_back() if callable(s.on_back) else None + cw(s.widget, transition=s.back_anim if s.in_source else 'out_right') + """On nuke""" + def nuke(s,by_button=False): + if not hasm(): pause(0) + if by_button: + try: s.nuke_button.activate() + except: pass + else: + if s.out_source: forbtn(s.widget,s.out_source,60) + s.prekill() + s.on_nuke() + cw(s.widget, transition=s.nuke_anim if s.out_source else 'out_right') + if s.source_cls and s.out_source: + s.source_cls.state() + """Toggle pause""" + def toggle(s,dry=False): + assert hasattr(s,'tog_button') + i = [[1,0],[0,1]][dry][pause()] + bw(s.tog_button, label=bui.charstr([bui.SpecialChar.PAUSE_BUTTON,bui.SpecialChar.PLAY_BUTTON][i])) + if dry: return + bui.getsound('deek').play() + pause(i) + +class itw: + """Indent text widget""" + def __init__(s,*a,**k) -> None: + s.a = a + s.k = k + k.update({'h_align':'center', + 'v_align':'center'}) + s.position = k.get('position',(0,0)) + s.text = k.get('text','') + s.h = k.get('size',(0,30))[1] + s.kids = [] + s.set_text(s.text) + """Set text""" + def set_text(s,text) -> None: + s.text = text + z = len(text) + p = list(s.position) + m = s.max = max(text.replace('\\n','') or [''],key=GSW) + l = s.max_len = GSW(str(m))/1.2 + s.total = l*len(max(text.split('\\n') or [''],key=len)) + [k.delete() for k in s.kids] + s.kids.clear() + x = 0 + for i in range(z): + p[0] += [l,0][i==0] + if x: x = 0; continue + j = text[i] + k = text[i+1] if (i+1) < z else j + if j == '\\' and k == 'n': + p[0] = s.position[0]-l*2 + p[1] = p[1] - s.h + x = 1 + continue + s.k.update({'position':(p[0],p[1]), + 'text':j, + 'size':(l,s.h)}) + s.kids.append(tw(*s.a,**s.k)) + """Set color""" + def set_color(s,color): + [tw(t,color=color) for t in s.kids] + +class ctw: + """Conditional text widget""" + def __init__( + s,*a,allow=[],tint=True,hint='', + bad_image=None,conf=None,type=None, + on_conf=(lambda *a: None),flash=False, + on_edit=(lambda *a: None),blank=False, + **k + ): + k.update({ + 'editable':True, + 'v_align':'center', + 'h_align':'center', + 'allow_clear_button':False, + 'color':k.get('color',(1,1,1)), + 'description':k.get('description',hint) or 'Enter', + 'glow_type':'uniform' + }) + s.color = k['color'] + s.widget = t = tw(*a,**k) + k.update({ + 'editable': False, + 'text': hint, + 'color': (0.5,0.5,0.5), + 'maxwidth':k.get('size')[0] + }) + s.widget2 = u = tw(*a,**k) + s.type = type + s.conf = conf if isinstance(conf,tuple) else (conf,conf) + s.forgive = s.silent = s.bad = False + s.on_edit = on_edit + s.blank = blank + s.on_conf = on_conf + s.hint = hint + s.tint = tint + s.allow = allow + s.flash = flash + s.bad_image = bad_image + s.old = s.get_text() + s.spy() + """Blink""" + def blink(s, c=(0,1,0)): + oc = s.color + tw(s.widget, color=c) + def f(): + try: tw(s.widget, color=oc) + except: pass + bui.apptimer(0.3,f) + """Get current text""" + def get_text(s): + return tw(query=s.widget) + """Set text gracefully""" + def set_text(s,t,silent=False): + if t is None: return + if s.get_text() == t: return + s.forgive = True + tw(s.widget, text=t) + tw(s.widget, color=s.color) + if silent: s.silent = True + """Spy on input""" + def spy(s): + if not s.widget.exists(): return + v = s.get_text() + t = s.widget + u = s.widget2 + tw(u,color=(0.5,0.5,0.5,0 if v else 1)) + bool = s.allow if s.allow is True else not v.translate({ord(c): None for c in s.allow}) + if bool and s.type == 'encoding': + try: CHECK(v) + except: bool = False + if v and not bool: + if not s.bad: + push(f'Invalid {s.hint}',color=(1,0,0)) + bui.getsound('error').play() + s.bad = True + if s.tint: tw(t,color=(1,0,0)) + if s.bad_image: bui.imagewidget(s.bad_image,opacity=1) + elif s.bad: + s.bad = False + if s.tint: tw(t,color=(1,1,1)) + if s.bad_image: bui.imagewidget(s.bad_image,opacity=0) + if bool and v != s.old: + if s.flash and s.old: s.blink() + s.old = v + if s.forgive: s.forgive = False + else: s.on_edit(s.hint, v) + if s.conf[0]: + var(s.conf[0],v if (v or s.blank) else var(s.conf[1])) + s.on_conf() + if s.type == 'encoding' and not s.silent: + gun() + elif s.silent: s.silent = False + bui.apptimer(0.1, s.spy) + +class SoundManager: + """Sound manager""" + def __init__(s,source=None,on_back=None,cols=None,prf=''): + w = icw( + title='Sound', + outside_action='back', + size=(350,400), + show_nuke=False, + on_back=on_back, + in_source=source, + back_anim='out_scale' if source else 'out_right' + ).widget + s.kids = [] + s.prf = prf + s.D = D() + s.cols = cols or COLS + mem = DIR('sounds') + for i in range(6): + y = 270-50*i + j = mem[i] + b = bw( + p=w, + label=j.split('_')[0], + size=(200,45), + pos=(50,y) + ) + bw(b,oac=bs.CallPartial(SoundPipe,pipe=s.fresh,source=b,action=i,prf=prf)) + b = pbw( + p=w, + pos=(260,y+7), + size=(25.5,25.5), + tex=s.cols() + ) + s.kids.append(b) + s.update() + sbw( + p=w, + icon=bui.gettexture('backIcon'), + pos=(275,330), + color=darken(var('bg')), + oac=s.reset + ) + """Update""" + def update(s): + for o in range(len(DIR('sounds'))): + v = var(f'{s.prf}sound{o}') or [0,0] + t = v[1] + i = v[0] + if t == 1: + ah = AUDIO() + t = ah[i] + bw(s.kids[o],texture=bui.gettexture('audioIcon'),oac=bs.CallPartial(bs.broadcastmessage,t),color=(1,1,1),tint_texture=bui.gettexture('black')) + else: + char = list(s.D.values())[i] + bw(s.kids[o],texture=bui.gettexture(char.icon_texture),tint_texture=bui.gettexture(char.icon_mask_texture),oac=bs.CallPartial(bs.broadcastmessage,list(s.D)[i],color=var(s.cols()[2])),color=(1,1,1)) + """Reset""" + def reset(s): + c = var(f'{s.prf}char') + fixsounds(c,prf=s.prf) + bui.getsound('block').play() + push('Restored default sounds!',color=(0.5,0.5,1)) + s.update() + """Fresh""" + def fresh(s,i,j): + o, t = j + var(f'{s.prf}sound{o}',[i,t]) + s.update() + +class SoundPipe: + """Sound pipe""" + def __init__(s,pipe=lambda:None,source=None,action=None,prf=''): + s.pipe = pipe + s.z = icw( + title=DIR('sounds')[action], + show_nuke=False, + in_source=source, + outside_action='back', + size=(400,200), + back_anim='out_scale' + ) + w = s.z.widget + t = var(f'{prf}sound{action}') or [0,0] + t = AUDIO()[t[0]] if t[1] else NAME()[t[0]] + s.t = tw( + p=w, + pos=(175,85), + h_align='center', + text=t + ) + b = bw( + p=w, + pos=(50,25), + size=(150,50), + label='Characters' + ) + bw(b,oac=bs.CallPartial(CharPicker,pipe=s.pick,source=b,what='Select a sound',extra=[action,0],prf=None)) + b = bw( + p=w, + pos=(200,25), + size=(150,50), + label='All Sounds' + ) + bw(b,oac=bs.CallPartial(SoundPicker,source=b,pipe=s.pick,extra=[action,1])) + """Redirect all""" + def pick(s,i,j): + s.z.back() + s.pipe(i,j) + +class SoundPicker: + """Sound picker""" + def __init__(s,source=None,pipe=lambda a:None,what='Select a sound',extra=None): + s.pipe = pipe + s.extra = extra + size = (600,500) + al = s.al = AUDIO() + s.z = z = icw( + title=what, + in_source=source, + size=size, + show_nuke=False, + on_back=s.stop, + back_anim='out_scale' + ) + w = z.widget + v = len(al) + ss = 30 * v + sv = bui.scrollwidget( + parent=w, + size=(210,360), + position=(60,50), + color=var('bg') + ) + cv = cw( + parent=sv, + background=False, + size=(280,ss) + ) + s.texts = [] + # List sounds + s.n = lambda: var('sp') or 0 + j = {'parent':cv,'size':(280,30)} + for i in range(v): + p = (10,(ss-30)-i*30) + t = tw( + pos=p, + maxwidth=210, + text=al[i], + v_align='center', + **j + ) + bw( + pos=p, + oac=bs.CallPartial(s.preview,i,extra), + texture=bui.gettexture('empty'), + **j + ) + s.texts.append(t) + s.t = tw( + p=w, + pos=(300,370), + maxwidth=290, + text=al[s.n()] + ) + s.up = lambda: cw(cv,visible_child=s.texts[s.n()]) + bw( + p=w, + label='Pick', + size=(100,50), + pos=(460,50), + oac=lambda: s.pick(s.n(),extra) + ) + for i in range(4): + j = ['ouyaOButton','ouyaAButton','upButton','downButton'][i] + k = [s.play,s.stop,s.prev,s.next][i] + sbw( + p=w, + icon=bui.gettexture(j), + pos=(300+50*i,300), + repeat=i>1, + oac=k + ) + c = chk( + parent=w, + on_value_change_call=lambda auto: var('spa',auto), + position=(300,50), + size=(100,50), + value=var('spa') or False, + text='Autoplay' + ) + chk(c,color=darken(var('bg')),textcolor=(1,1,1)) + s.update(True) + """Prev""" + def prev(s): + n = s.n() - 1 + if n < 0: n = len(s.al) - 1 + var('sp',n) + s.update() + """Next""" + def next(s): + n = s.n() + 1 + if n >= len(s.al): n = 0 + var('sp',n) + s.update() + """Play""" + def play(s): + s.stop() + s.v = bui.getsound(s.al[s.n()]) + s.v.play() + """Stop""" + def stop(s): + try: s.v.stop() + except: pass + """Update""" + def update(s,first=False): + for i in range(len(s.texts)): + tw(s.texts[i],color=(0,1,0) if s.n() == i else (1,1,1)) + s.up() + if var('spa') and not first: s.play() + """Preview""" + def preview(s,n,e): + s.stop() + t = s.al[n] + var('sp',n) + tw(s.t, text=t) + s.update() + """Redirect""" + def pick(s,i,j): + s.stop() + gun() + s.pipe(i,j) + s.z.back() + +class MeshManager: + """Mesh manager""" + def __init__(s,source=None,on_back=None,prf='',cols=None): + w = icw( + title='Mesh', + size=(400,530), + show_nuke=False, + in_source=source, + outside_action='back', + on_back=on_back, + back_anim='out_scale' if source else 'out_right', + note_scale=0.55, + note_pos=(25,15), + note='Some characters use kronk parts, it\'s not a bug.' + ).widget + s.kids = [] + s.mem = MESH() + s.on_back = on_back + s.cols = cols or COLS + mem = DIR('mesh') + s.prf = prf + for i in range(len(mem)): + px = [55,55,110,111,111,111,110,55,111][i] + py = [245,190,350,90,189,45,237,300,140][i] + sx = [50,50,100,100,100,100,100,50,100][i] + sy = [50,50,100,45,50,40,120,50,45][i] + l = mem[i][:-5] + b = bw( + p=w, + pos=(100+px,py), + size=(sx,sy), + text_scale=[1,1.5][sx<51], + label=l.split('_')[0] + ) + bw(b,oac=bs.CallPartial(MeshPipe,pipe=s.fresh,source=b,part=i,title=l,prf=prf)) + tx = [105,105,325,325,325,325,325,105,325][i] + ty = [253,199,385,97,199,49,282,307,147][i] + t = pbw( + p=w, + pos=(tx,ty), + size=(25.5,25.5), + tex=s.cols() + ) + s.kids.append(t) + sbw( + p=w, + icon=bui.gettexture('backIcon'), + pos=(320,461), + color=darken(var('bg')), + oac=s.reset + ) + s.img = [] + for i in range(2): + b = bw( + p=w, + pos=(50,115-73*i), + size=(160,70) + ) + j = ['Texture','Mask'][i] + bw(b,oac=bs.CallPartial(TexPipe,source=b,pipe=s.update,prf=prf,mask=i,what=j)) + tw( + p=w, + pos=(120,135-73*i), + h_align='center', + text=j + ) + s.img.append(bui.imagewidget( + parent=w, + position=(68,135-73*i), + draw_controller=b, + size=(30,30) + )) + b = bw( + p=w, + pos=(50,350), + label='Select All', + text_scale=1.3, + size=(160,100) + ) + bw(b,oac=lambda: CharPicker(pipe=s.all,source=b,note='To avoid confusion, character will be fully changed.',prf=None)) + s.update() + """Set all""" + def all(s,i,e=None): + na = NAME() + n = na[i] + s.reset(n) + if var(f'{s.prf}tchar') in na: var(f'{s.prf}tchar',n) + var(f'{s.prf}char',n) + push('Character changed!',color=(1,1,0)) + s.on_back() if callable(s.on_back) else None + """Reset""" + def reset(s,c=None): + if not c: + c = var(f'{s.prf}char') + bui.getsound('block').play() + push('Restored default mesh!',color=(0.5,0.5,1)) + fixmesh(c,prf=s.prf) + s.update() + """Update previews""" + def update(s): + for i in range(len(DIR('mesh'))): + g = var(f'{s.prf}mesh{i}') + bw(s.kids[i],texture=bui.gettexture('menuIcon'),color=(1,1,1),oac=bs.CallPartial(bs.broadcastmessage,g)) + bui.imagewidget(s.img[0],texture=bui.gettexture(var(f'{s.prf}ctex') or get_ctex(var('char')))) + bui.imagewidget(s.img[1],texture=bui.gettexture(var(f'{s.prf}ctex2') or get_ctex2(var('char')))) + """Fresh""" + def fresh(s,j,n): + var(f'{s.prf}mesh{j}',n) + s.update() + +class MeshPicker: + """Mesh picker""" + def __init__(s,source=None,pipe=lambda a:None,what='Select a mesh',mem=None,extra=None): + s.pipe = pipe + size = (420,500) + al = s.al = mem or MESH() + s.z = z = icw( + title=what, + in_source=source, + size=size, + show_nuke=False, + back_anim='out_scale', + outside_action='back' + ) + w = z.widget + v = len(al) + ss = 30 * v + sv = bui.scrollwidget( + parent=w, + size=(310,360), + position=(60,50), + color=var('bg') + ) + cv = cw( + parent=sv, + background=False, + size=(380,ss) + ) + s.texts = [] + # List meshes + s.n = lambda: var('mp') or 0 + for i in range(v): + p = (10,(ss-30)-i*30) + j = al[i] + t = tw( + pos=p, + maxwidth=290, + v_align='center', + text=j, + p=cv, + size=(280,30), + click_activate=True, + selectable=True, + on_activate_call=bs.CallPartial(s.fresh,i,j) + ) + s.texts.append(t) + s.up = lambda: cw(cv,visible_child=s.texts[s.n()]) + bw( + p=w, + label='Pick', + size=(100,50), + pos=(270,0), + oac=bs.CallPartial(s.pick,extra) + ) + s.fresh() + """Refresh""" + def fresh(s,i=None,j=None): + if i is not None: + var('mp',i) + s.m = j + [tw(t,color=(1,1,1)) for t in s.texts] + else: i = s.n() + tw(s.texts[i],color=(0,1,0)) + s.up() + """Redirect""" + def pick(s,e): + n = s.n() + try: + with bs.get_foreground_host_activity().context: bs.getmesh(s.m) + except: btw("Can't use this mesh!"); return + gun() + s.pipe(n,e) + s.z.back() + +class MeshPipe: + """Mesh pipe""" + def __init__(s,pipe=lambda:None,source=None,part=None,prf='',title=None): + s.pipe = pipe + s.part = part + s.mem = MESH() + s.title = title + s.z = icw( + title=title, + show_nuke=False, + in_source=source, + outside_action='back', + size=(400,200), + back_anim='out_scale' + ) + w = s.z.widget + t = var(f'{prf}mesh{part}') + s.t = tw( + p=w, + pos=(175,85), + h_align='center', + text=t + ) + b = bw( + p=w, + pos=(50,25), + size=(150,50), + label='Characters' + ) + bw(b,oac=bs.CallPartial(CharPicker,pipe=s.pick,source=b,what=f'Get {title} from',extra=0,prf=None)) + b = bw( + p=w, + pos=(200,25), + size=(150,50), + label='All Meshes' + ) + bw(b,oac=bs.CallPartial(MeshPicker,source=b,pipe=s.pick,extra=1,mem=s.mem,what=f'Pick a mesh')) + """Redirect all""" + def pick(s,i,j): + s.z.back() + m = s.mem[i] if j else getattr(SPAZ()[i],s.title+'_mesh') + s.pipe(s.part,m) + +class TexPipe: + """Texture pipe""" + def __init__(s,pipe=lambda:None,source=None,prf='',mask=False,what='Texture'): + s.pipe = pipe + s.mask = mask + s.z = icw( + title=what, + show_nuke=False, + in_source=source, + outside_action='back', + size=(400,200), + back_anim='out_scale' + ) + w = s.z.widget + s.prf = prf + s.vs = f"{s.prf}ctex{['','2'][s.mask]}" + s.vf = [get_ctex,get_ctex2][mask] + v = var(s.vs) or s.vf(var('char')) + s.t = tw( + p=w, + pos=(175,85), + h_align='center', + text=v + ) + b = bw( + p=w, + pos=(50,25), + size=(150,50), + label='Characters' + ) + bw(b,oac=bs.CallPartial(CharPicker,pipe=bs.CallPartial(s.pick,0),source=b,what='Get texture from',prf=None)) + b = bw( + p=w, + pos=(200,25), + size=(150,50), + label='All Textures' + ) + bw(b,oac=bs.CallPartial(TexPicker,source=b,pipe=bs.CallPartial(s.pick,1))) + """On pick""" + def pick(s,n,p,e=None): + s.z.back() + var(s.vs,ALL()[p] if n else s.vf(NAME()[p])) + s.pipe() + +class TexPicker: + """Texture picker""" + def __init__(s,source=None,pipe=lambda a:None,what='Select a texture'): + s.pipe = pipe + size = (600,500) + al = s.al = ALL() + s.z = z = icw( + title=what, + in_source=source, + size=size, + outside_action='back', + show_nuke=False, + back_anim='out_scale' + ) + w = z.widget + h = 6 + v = len(al) // h + ss = 100 * v + sv = bui.scrollwidget( + parent=w, + size=(510,360), + position=(60,50), + color=var('bg') + ) + cv = cw( + parent=sv, + background=False, + size=(480,ss) + ) + # List textures + txt = {} + for i in range(v): + for j in range(h): + n = (i*h)+j + b = bw( + p=cv, + size=(65,65), + pos=(j*85,(ss-65)-i*100), + texture=bui.gettexture(al[n]), + ) + bw(b, color=(1,1,1), oac=bs.CallPartial(s.pick,n)) + t = al[n] + m = 15 + if len(t) > m: t = t[:m]+'\n'+t[m:] + o = tw(p=cv, + maxwidth=65, + max_height=25, + pos=(j*85,(ss-100)-i*100), + v_align='top', + text=t + ) + txt[al[n]] = o + """Redirect""" + def pick(s,n): + gun() + s.pipe(n) + s.z.back() + +class CharPicker: + """Character picker""" + def __init__( + s,pipe,source=None,what='Select a character', + note='',extra=None,cols=None,prf='' + ): + s.pipe = pipe + size = (600,500) + ah = s.ah = SPAZ() + v = 1+len(ah) // 3 + s.z = z = icw( + title=what, + in_source=source, + size=size, + outside_action='back', + show_nuke=False, + note=note, + back_anim='out_scale' + ) + w = z.widget + sv = bui.scrollwidget( + parent=w, + color=var('bg'), + size=(510,360), + position=(60,50)) + cv = cw( + parent=sv, + background=False, + size=(480,v*150+180)) + # List characters + txt = {} + hm = NAME() + mem = [COLS,cols][bool(cols)]() + for i in range(v): + for j in range(3): + n = (i*3)+j + if n >= len(ah): continue + xp = 30+j*150 + yp = 1160-(i*180)-40 + pbw( + p=cv, + size=(120,120), + pos=(xp,yp-20), + texture=bui.gettexture(ah[n].icon_texture), + tint_texture=bui.gettexture(ah[n].icon_mask_texture), + oac=bs.CallPartial(s.pick,n,extra), + tex=mem + ) + t = tw( + p=cv, + maxwidth=120, + pos=(xp+35,yp-52), + h_align='center', + v_align='center', + text=hm[n], + color=var(mem[2]) + ) + txt[hm[n]] = t + if prf is not None: cw(cv, visible_child=txt[var(f'{prf}char')]) + """Redirect""" + def pick(s,n,e): + gun() + s.z.back() + s.pipe(n,*([e] if e is not None else [])) + +class SpazPicker: + """Spaz picker""" + def __init__( + s, + source, + pipe, + what='Select a spaz', + deny=[], + deny_msg="You can't pick that spaz!", + note=None + ): + s.pipe = pipe + s.deny = [deny,deny_msg] + with bs.get_foreground_host_activity().context: all = [_ for _ in KIDS() if getattr(_,'exists',lambda:False)() and not getattr(_.getdelegate(object),'_dead',True)] + l = len(all) + v = l//3+[1,0][l%3<1] + yl = v*185 + s.z = z = icw( + title=what, + in_source=source, + size=(600,500), + outside_action='back', + show_nuke=False, + back_anim='out_scale', + note=note + ) + w = z.widget + sv = bui.scrollwidget( + color=var('bg'), + parent=w, + size=(510,360), + position=(60,50) + ) + cv = cw( + parent=sv, + background=False, + size=(480,yl) + ) + yl -= 150 + # List characters + tex = ALL() + for i in range(v): + for j in range(3): + try: k = (i*3)+j; n = all[k] + except: return + ui = NTEX(n,tex) + pbw( + p=cv, + size=(120,120), + pos=(155*j+30,yl-185*i), + oac=bs.CallPartial(s.pick,n), + **ui[0] + ) + tw( + p=cv, + maxwidth=120, + pos=(155*j+65,yl-185*i-35), + h_align='center', + color=ui[1], + text=n.name or 'Unnamed' + ) + """Clean up""" + def pick(s,n): + if n in s.deny[0]: btw(s.deny[1]); return + s.z.back() + s.pipe(n) + +class ColorPicker: + """Color picker""" + def __init__( + s, + in_source: bui.buttonwidget, + id: str, + what: str, + chr: str, + on_back: callable, + id2: str = None, + mode: int = 0, + ): + s.mode = mode + s.color = (var(what) or (1,1,1)) + s.what = what + s.on_back = on_back + s.in_source = in_source + size = (450, 570) + x,y = s.x,s.y = size + s.rgb = ['Red','Green','Blue'] + z = icw( + show_nuke=False, + in_source=in_source, + back_anim='out_scale', + on_back=s.back, + title=f'{what} color', + size=size, + outside_action='back', + note='To support neon colors, a 12-point hex is used.', + note_scale=0.5, + note_pos=(50,6) + ) + w = s.w = z.widget + s.kids = {} + s.rans = [] + s.shad = [] + char = D()[var(chr)] + # Color preview + pre = MK(char,id,id2)[mode>1] + tex = bui.imagewidget( + parent=w, + color=(1,1,1), + size=(165,165), + position=(45,320), + **pre + ) + if s.mode == 2: bui.imagewidget(tex, color=s.color) + s.pre = tex + # RGBH editables + for i in range(4): + e = [*s.rgb,'Hex'][i] + c = [(1,0,0),(0,1,0),(0,0,1),s.color][i] + t = str(float(s.color[i]))[:5] if i < 3 else c2h(s.color) + v = ['-#abcdef0123456789','-0.123456789'][i<3] + xp = 220 + yp = 437-i*33 + bi = bui.imagewidget( + parent=w, + position=(xp+194,yp), + size=(33,33), + opacity=0, + color=(30,0,0), + texture=bui.gettexture('crossOut'), + ) + o = ctw( + parent=w, + allow=v, + on_edit=s.on_edit, + tint=False, + bad_image=bi, + hint=e, + color=c, + size=(195,33), + text=t, + pos=(xp,yp) + ) + s.kids[e] = o + # Random button + bw( + p=w, + size=(50,50), + scale=0.9, + color=darken(var('bg')), + pos=(380,500), + oac=s.randomize, + icon=bui.gettexture('replayIcon'), + button_type='regular', + repeat=True, + iconscale=1.2 + ) + # Color choices + for i in range(3): + for j in range(5): + r = bw( + p=s.w, + size=(68,68), + pos=(56+j*73,237-i*69) + ) + s.rans.append(r) + # Shade row + for j in range(5): + r = bw( + p=s.w, + size=(68,68), + pos=(56+j*73,30) + ) + s.shad.append(r) + s.shade() + s.randomize(True) + """Shade""" + def shade(s): + c = s.color + for i in range(5): + h = (i+0.5)/2 + d = (c[0]*h,c[1]*h,c[2]*h) + bw( + s.shad[i], + color=d, + oac=bs.CallPartial(s.set,d) + ) + """Randomize""" + def randomize(s,silent=False): + min = -1 + max = 1 + ran = [[tuple([random.uniform(min+k/4,max+k/2) for i in range(3)]) for j in range(5)] for k in range(3)] + lol = (i for i in s.rans) + for i in range(3): + for j in range(5): + c = ran[i][j] + r = bw( + next(lol), + color=c, + oac=bs.CallPartial(s.set,c), + ) + None if silent else bui.getsound('cashRegister2').play() + """Set color""" + def set(s,c): + bui.getsound('tap').play() + for i in range(3): + var(s.what+s.rgb[i], c[i]) + w = s.kids[s.rgb[i]].widget + tw(w, text=str(c[i])[:5]) + tw(w, color=(1,0,0) if i < 1 else \ + (0,1,0) if i < 2 else \ + (0,0,1)) + var(s.what+'Hex', c2h(c)) + s.fresh(None) + """Obtain rgb""" + def get_rgb(s): + a = [] + for i in s.rgb: + o = var(s.what+i) + v = float(o if o not in [None,''] else 1.0) + a.append(v) + return a + """Obtain hex""" + def get_hex(s): + return c2h(s.get_rgb()) + """On edit""" + def on_edit(s,k,v): + if v == '' and k == 'Hex': return + var(s.what+k,v) + if k in s.rgb: + if not '.' in v and v: + v = v+".0" + s.kids[k].set_text(v) + var(s.what+'Hex',s.get_hex()) + elif k == 'Hex': + if not v.startswith('#') and len(v) < 13: + v = '#'+v + s.kids['Hex'].set_text(v) + if len(v.replace('#','')) < 12: + for i in range(12-len(v)): v += '0' + try: c = h2c(v) + except: return + [var(s.what+s.rgb[i],c[i]) for i in range(len(s.rgb))] + s.fresh(k) + """Refresh values""" + def fresh(s,k): + if k == 'Hex': + for i in s.rgb: + v = var(s.what+i) + if v == '': continue + s.kids[i].set_text(str(v)[:5]) + else: + s.kids['Hex'].set_text(var(s.what+'Hex')) + c = s.get_rgb() + s.color = c + s.shade() + if not s.mode: bui.imagewidget(s.pre, tint_color=c) + elif s.mode < 2: bui.imagewidget(s.pre, tint2_color=c) + else: bui.imagewidget(s.pre, color=c) + tw(s.kids['Hex'].widget, color=s.color) + """On back""" + def back(s): + var(s.what, s.color) + bw(s.in_source, color=s.color) + s.on_back() if callable(s.on_back) else None + +class NodeManager: + """Node manager""" + def __init__(s,node,source=None,pipe=None) -> None: + s.node = node + z = icw( + in_source=source, + title='Manage', + show_nuke=False, + size=(450,300), + back_anim='out_scale', + out_source=source, + on_back=pipe + ) + w = z.widget + s.t = atw( + p=w, + obj=node, + pos=(70,65), + scale=1.3, + size=(280,30) + ) + s.btns = [] + for i in range(3): + j = ['CallPartial','Random','Modify'][i] + m = ['startButton','replayIcon','settingsIcon'][i] + o = [s.call,s.random,s.modify][i] + b = bw( + p=w, + label=j, + icon=bui.gettexture(m), + size=(130,35), + oac=bs.CallPartial(s.t.insp,f=o), + pos=(30+130*i,20) + ) + s.btns.append(b) + """Random""" + def random(s,a): + t = s.t.typ.__name__ + o = None + if t == 'tuple': o = tuple([round(random.uniform(0,1),1) for _ in [0,0,0]]) + if t in ['float','int']: o = random.randrange(0,100) + if o is None: btw('Unrecognized type!'); return + Modder( + obj=s.node, + source=s.btns[1], + attr=s.t.text, + old=o, + title='Random', + label='Apply' + ) + """CallPartial""" + def call(s,a): + if not callable(a): btw('Not callable!'); return + CallPartialer(obj=a,source=s.btns[0]) + """Modify""" + def modify(s,a): + if s.t.dead: btw('Object is dead!'); return + if callable(a): btw("You can't modify a callable!"); return + Modder( + obj=s.node, + source=s.btns[2], + attr=s.t.text, + old=a + ) + +class Confirm: + """Action confirmer""" + def __init__(s,source,pipe,title,label): + z = icw( + size=(280,150), + title=title, + auto_offset=False, + back_anim='out_scale', + in_source=source, + outside_action='back', + show_nuke=False + ) + bw( + p=z.widget, + pos=(20,10), + size=(240,50), + label=label, + oac=lambda:(pipe(),z.back()) + ) + +class CallPartialer: + """Object caller""" + def __init__(s,source,obj): + z = icw( + in_source=source, + show_nuke=False, + back_anim='out_scale', + size=(450,500), + title='CallPartial' + ) + w = z.widget + s.obj = obj + tw( + p=w, + text=str(obj), + color=(0,1,0), + pos=(50,390), + v_align='center', + maxwidth=380 + ) + tw( + p=w, + text=GETSIG(obj), + color=(0,0.6,1), + pos=(50,362), + v_align='center', + maxwidth=380 + ) + s.at = ctw( + allow=True, + p=w, + pos=(50,330), + size=(380,30), + description='Arguments will be passed to target method (eg. "True, 37, DieMessage"). Leave blank to pass nothing. Any input here will be evaluated, which means you can use args defined in Coolbox too! Enter', + hint='Args separated by comma' + ) + d = bui.hscrollwidget( + parent=w, + color=var('bg'), + position=(44,70), + size=(385,250) + ) + s.c = cw( + parent=d, + size=(385,250), + background=False + ) + s.log = tw( + p=s.c, + pos=(0,210), + max_height=250 + ) + s.log2 = itw( + parent=s.c, + position=(0,210), + max_height=250 + ) + ok = chk( + parent=w, + position=(50,45), + text='Same character width (beta)', + textcolor=(1,1,1), + scale=0.7, + value=var('icall') + ) + chk(ok,on_value_change_call=lambda v:(var('icall',v),chk(ok,value=v))) + bw( + p=w, + pos=(325,20), + icon=bui.gettexture('startButton'), + label='CallPartial', + size=(100,40), + oac=s.call + ) + """Real call""" + def call(s): + try: o = str(s.obj(*eval(f"[{s.at.get_text()}]"))) + except: bui.getsound('error').play(); o = traceback.format_exc(); c = (1,0,0) + else: bui.getsound('ding').play(); c = (0,1,0) + if var('icall'): + tw(s.log,text='') + s.log2.set_text(o.replace('\n','\\n')) + s.log2.set_color((1,1,0)) + bui.apptimer(0.5,lambda:s.log2.set_color(c)) + m = s.log2.total+20 + else: + m = GSW(max(o.splitlines(),key=len))+20 + tw(s.log,text=o,color=(1,1,0)) + s.log2.set_text('') + bui.apptimer(0.5,lambda:tw(s.log,color=c)) + cw(s.c,visible_child=s.log,size=([m,385][m<385],250)) + +class Modder: + """Object modifier""" + def __init__(s,source,obj,attr,old,title='Modify',label='Modify'): + s.obj = obj + s.attr = attr + s.z = icw( + title=title, + size=(450,180), + show_nuke=False, + back_anim='out_scale', + in_source=source + ) + w = s.z.widget + s.t = ctw( + allow=True, + hint='New value (eg. True or "string")', + description='Any input here will be evaluated. Which means you have to use quotes for strings (eg. "Name"), functions defined in coolbox.py can be used here! eg. gbt(\'agentColor\'). Enter', + size=(400,30), + text=f'"{old}"' if type(old) is str else str(old), + p=w, + pos=(28,68) + ) + b = bw( + p=w, + pos=(330,20), + icon=bui.gettexture('settingsIcon'), + label=label, + size=(100,40), + oac=s.mod + ) + """Actually modify""" + def mod(s): + v = s.t.get_text() + try: + with bs.get_foreground_host_activity().context: + v = eval(v) + if s.attr == 'position' and isinstance(v, (tuple, list)) and len(v) == 3: + v = tuple(round(c, 2) for c in v) + setattr(s.obj,s.attr,v) + except Exception as E: err(str(E)) + else: + bui.getsound('ding').play() + s.z.back() + push('Success!',color=(0,1,0)) + if Zoom.__ob__ == s.obj: return + Zoom(s.obj) if len(getattr(s.obj,'position',(0,0))) == 3 else None + +class atw: + """Attribute text widget""" + def __init__(s,scale,obj,p,pos,size,*a,**k): + k.update({ + 'position':pos, + 'editable':True, + 'description':'Start typing to see matches. Write a star "*" to list all attrs. Enter', + 'parent':p, + 'maxwidth':size[0]+50, + 'scale':scale, + 'size':size + }) + s.widget = tw(*a,**k) + k.update({ + 'editable':False, + 'text':'try "color" or "*"' + }) + s.widget2 = tw(*a,**k) + bw( + p=p, + pos=(400,pos[1]), + size=(35,35), + label=bui.charstr(bui.SpecialChar.UP_ARROW), + oac=lambda:bui.getsound('tap').play() + ) + s.kids = [] + for i in range(4): + j = ['Type','Value','Name',str(obj)][i] + c = [(1,1,0),(0,1,1),(0,1,0),(1,0,1)][i] + s.kids.append(tw(p=p, + text=j, + maxwidth=400, + color=c, + v_align='center', + pos=(25,100+(30*i)))) + s.parent = p + s.pos = pos + s.obj = obj + s.type = getattr(obj,'getnodetype',lambda:'idk')() + s.typ = 'Type' + s.x, s.y = size + s.attrs = dir(obj) + s.scale = scale + s.prev = [] + s.root = s.text = s.old = None + s.dropped = s.active = s.dead = False + s.ty = 30 + s.cnt = 8 + s.spy() + s.tip(True) + """Inspect""" + def insp(s,f): + if s.valid(): return f(s.get(s.text)) + btw('Attribute does not exist!') + """Get""" + def get(s,t): + return getattr(s.obj,t,None) if not (s.type == 'spaz' and t in BAD()) else (0,0,0) + """Control hint""" + def tip(s,b): + tw(s.widget2,color=(0.5,0.5,0.5,1 if b else 0)) + """Drop height""" + def ht(s): + l = len(s.prev) + return (s.ty * (s.cnt if l > s.cnt else l) + 10) + """Get text""" + def get_text(s): + if s.parent.transitioning_out or not s.parent.exists(): return + return tw(query=s.widget) + """Set text""" + def set_text(s,t): + ot = s.get_text() + if ot == t: s.clear(False) + tw(s.widget,text=t) + """Validate""" + def valid(s): + for i in s.prev: + if i == s.text: return True + """Spy""" + def spy(s): + if not s.dead and not s.obj.exists(): s.dead = True + try: + if s.parent.transitioning_out or not s.parent.exists(): raise + s.text = s.get_text() + s.active = s.parent.get_selected_child() in [s.widget,s.root] + except: return + if s.text: + s.apply(s.valid()) + if (s.old != s.text) and s.active: + s.old = s.text + s.refine() + s.drop() + if (not s.text and s.dropped) or (not s.active and s.dropped): s.clear(False) + bui.apptimer(0.1, s.spy) + """Apply for granted""" + def apply(s,d): + a = s.text if d else 'Name' + v = s.get(a) if d else 'Value' + t = type(v) if d else 'Type' + [tw(s.kids[i],color=[(1,1,0),(0,1,1),(0,1,0)][i],text=f'{[t,v,a][i]}') for i in range(3)] + s.typ = t + """Refine prev""" + def refine(s): + s.prev = [] + [s.prev.append(p) if s.text in "*"+p else None for p in s.attrs] + """Clear drop""" + def clear(s,b): + s.dropped = b + if not b: s.anim(s.ht(),-1,True) + elif s.root: s.root.delete() + s.tip((not b) and (not s.text)) + """Dropdown""" + def drop(s): + s.px = s.pos[0]-52 + s.py = s.pos[1]-4 + b = s.dropped + s.clear(True) + l = len(s.prev)*s.ty + s.root = bui.scrollwidget( + parent=s.parent, + size=(s.x+60,s.ht() if s.dropped else 0), + highlight=False, + color=var('bg'), + border_opacity=0.0, + position=(s.px,s.py-s.ht()) + ) + c = cw( + parent=s.root, + size=(s.x+60,l), + background=False + ) + bui.imagewidget( + parent=c, + size=(s.x+1500,l+1000), + texture=bui.gettexture('black'), + opacity=0.7, + position=(-300,-500) + ) + r = bui.hscrollwidget( + parent=c, + size=(s.x+40,l+200), + color=var('bg'), + border_opacity=0.0, + highlight=False, + position=(0,s.py-60) + ) + c = cw( + parent=r, + size=(s.x,s.x), + background=False + ) + tl = tw( + p=c, + size=(1,1), + text=" ", + pos=(0,l)) + top = s.x + for i in range(len(s.prev)): + u = s.prev[i] + y = l-(i+1)*s.ty + t = tw( + p=c, + text=u, + size=(s.x,s.ty), + color=(0.6,0.6,0.6), + pos=(0,y), + selectable=True, + click_activate=True, + on_activate_call=bs.CallPartial(s.set_text,u) + ) + txt = type(s.get(u)).__name__ + uw = GSW(u+" ") + tw( + p=c, + text=txt, + size=(s.x,s.ty), + color=(1,1,0), + pos=(uw,y) + ) + new = GSW(u+" "+txt) + if new > top: top = new + o = None + for j in range(len(u)): + f = u.find(s.text,j) + if f < 0: break + if o == f: continue + o = f + tw( + p=c, + text=s.text, + size=(s.x,s.ty), + color=(0,1,0), + pos=(GSW(u[:o]),y) + ) + if not b: s.anim(0,1) + cw( + c, + size=(top+20,l+20), + visible_child=tl + ) + """Animate""" + def anim(s,i,r,nuke=False): + if (i > s.ht()) or (i < 0): + if nuke: s.root.delete(); s.old = None + return + try: bui.scrollwidget(s.root, size=(s.x+60,i), position=(s.px,s.py-i)) + except: return + bui.apptimer(0.0005, bs.CallPartial(s.anim,i+(1*r),r,nuke)) + +class NodePicker: + """Node picker""" + def __init__(s,pipe,source=None,which='node',allow='all',note=''): + s.zins = None + s.op = pause() + s.z = icw( + in_source=source, + title='Select a '+which, + back_anim='out_scale', + size=(600,600), + on_back=lambda:(npause(s.op),s.zins.x() if s.zins else None), + show_nuke=False, + note=note, + note_scale=0.6 + ) + w = s.w = s.z.widget + s.pipe = pipe + s.allow = allow + s.kids = [] + s.nuds = [] + s.sl = None + s.blind = False + sv = bui.scrollwidget( + parent=w, + size=(500,350), + color=var('bg'), + position=(50,50) + ) + s.cv = cw( + parent=sv, + background=False + ) + c = tuple([0.4 for i in range(3)]) + for i in range(3): + j = ['Type','Position','Extra'][i] + k = [65,195,430][i] + tw( + p=w, + text=j, + size=(40,20), + color=c, + pos=(k,410) + ) + v = var('npp') + s.btns = [] + for i in range(6): + j = ['', 'Delete', 'Refresh','Manage','Clone','Pick'][i] + k = ['black', 'textClearButton', 'replayIcon', 'menuIcon', 'achievementFreeLoader', 'ouyaOButton'][i] + l = ([40,210,380]*2)[i] + o = [s.toggle, s.delete, s.refresh, s.advanced, s.clone, s.pick][i] + b = bw( + p=w, + pos=(l,[430,480][i<3]), + label=j, + size=(170,45), + icon=bui.gettexture(k), + oac=o + ) + s.btns.append(b) + s.up() + s.toggle(0,False) + """Advanced""" + def advanced(s): + n = s.now() + if not n: return + n = s.nuds[s.sl] + NodeManager(source=s.btns[3],node=n) + """Toggle pause""" + def toggle(s,f=1,d=True): + v = var('npp') + if f: var('npp',[1,0][v]); v = not v + if d: bui.getsound('deek').play() + bw(s.btns[0], label=['Pause','Resume'][v], icon=bui.gettexture(['achievementFootballShutout','startButton'][v])) + npause() + """Confirm safely""" + def confirm(s,what,f): + Confirm( + source=s.btns[1], + title=what, + label='Confirm (Risky!)', + pipe=lambda:(f(),z.back()) + ) + """Locate node""" + def delete(s): + n = s.now() + if not n: return + def f(): + s.sl = None + n.delete() + s.confirm('Delete',f) + """Refresh nodes""" + def refresh(s): + ding() + s.sl = None + s.up() + """Obtain safely""" + def now(s): + if s.sl is None: btw('Select a node!'); return + return s.nuds[s.sl] + """Clone node""" + def clone(s): + n = s.now() + if not n: return + def f(): + d = dir(n) + all = {} + for i in d: + a = getattr(n,i) + if i.startswith('_') or callable(a): continue + all[i] = a + with bs.get_foreground_host_activity().context: + try: c = bs.newnode(n.getnodetype(),attrs=all) + except: return + SPARK(c.position) + push('Cloned!', color=(0,1,0)) + s.confirm('Clone',f) + """Pick""" + def pick(s): + n = s.now() + if not n: return + if s.allow == '3D': + try: + p = n.position + if len(p) != 3: raise + except: btw('Must be a 3D node!'); return + s.blind = True + s.pipe(n) + npause(0) + s.z.back() + """Update""" + def up(s): + with bs.get_foreground_host_activity().context: a = bs.getnodes() + l = len(a) + y = l * 30 + [[o.delete() for o in j] for j in s.kids] + s.kids.clear() + s.nuds.clear() + for i in range(l): + n = a[i] + p = n.position if hasattr(n,'position') else () + q = len(p) if p else 0 + t = n.getnodetype() + s.kids.append([]) + f = y-30*(i+1) + for j in range(3): + x = [10,140,375][j] + k = [t,str(rnd(p)).replace('()','N/A'),bui.charstr(bui.SpecialChar.LOGO_FLAT) if hasattr(n,'color') else bui.charstr(bui.SpecialChar.DPAD_CENTER_BUTTON)][j] + m = [120,200,150][j] + t = tw( + p=s.cv, + text=k, + maxwidth=m, + size=(m,25), + h_align='left', + color=n.color if hasattr(n,'color') and j == 2 else (1,1,1), + v_align='center', + selectable=True, + click_activate=True, + on_activate_call=bs.CallPartial(s.select,i), + pos=(x,f) + ) + s.kids[-1].append(t) + t = str(getattr(n,'color_texture',0) or getattr(n,'texture','"empty"')) + p1 = t.find('"')+1 + p2 = t[p1:t.find('"',p1)] + try: t = bui.gettexture(p2) + except: t = bui.gettexture('empty') + g = bui.imagewidget( + parent=s.cv, + size=(25,25), + position=(x+40,f), + texture=t + ) + s.nuds.append(n) + cw(s.cv, size=(500,y)) + s.eye() + """Check for existence""" + def eye(s): + z = 0 + for k in s.kids: + n = s.nuds[z] + if not n.exists(): + for t in k: + try: + if s.w.exists and s.w.transitioning_out: return + if t.exists: tw(t, color=(1,0,0)) + except: return + if s.sl is not None and s.sl == z: + btw('Selected node has died just now!') + s.sl = None + z += 1 + if not s.blind: bui.apptimer(0.1,s.eye) + """Highlight""" + def hl(s, sl): + if (sl is None): return + try: n = s.nuds[sl] + except: return + if not n.exists(): + btw('Node is dead!') + return False + c = (1,1,1) + try: + k = s.kids[s.sl] + [tw(k[i],color=c) for i in range(2)] + except: pass + c = (0,1,0) + try: + k = s.kids[sl] + [tw(k[i],color=c) for i in range(2)] + cw(s.cv, visible_child=k[0]) + except IndexError: return + if len(getattr(n,'position',(0,0))) > 2: + s.zins = Zoom(n) + return sl + """Select""" + def select(s,i): + r = s.hl(i) + if r is not False: s.sl = r + +class Zoom: + """Smooth zoomer""" + __up__ = False + __ob__ = None + def __init__(s,n): + s.n = n + c = s.__class__ + s.u = lambda: c.__up__ + s.v = lambda b: setattr(c,'__up__',b) + s.w = lambda b: setattr(c,'__ob__',b) + s.x = lambda: (s.v(False),_ba.set_camera_manual(False),s.w(None)) + s.step = 0 + if s.u(): s.x(); bui.apptimer(0.1,s.focus) + else: s.focus(); s.w(n) + def focus(s): + s.v(True) + s.w(s.n) + s.step = 1 + s._focus() + bui.apptimer(1.5,bs.CallPartial(setattr,s,'step',2)) + def _focus(s): + if not s.u() or not s.n.exists(): s.x(); return + if s.step == 2: + _ba.set_camera_manual(True) + s.zoom() + return + if s.step == 4: s.x(); return + _ba.set_camera_target(*s.n.position) + bui.apptimer(0.01,s._focus) + def zoom(s): + if not s.u() or not s.n.exists(): s.x(); return + p = _ba.get_camera_position() + q = s.n.position + d = math.dist(p,q) + if d < 5: + s.step = 3 + s._focus() + bui.apptimer(2,bs.CallPartial(setattr,s,'step',4)) + return + e = 0.01+d*0.005 + e = min(e,0.8) + dx = q[0]-p[0] + dy = q[1]-p[1] + dz = q[2]-p[2] + sx = dx*e + sy = dy*e + sz = dz*e + _ba.set_camera_position(*(p[0]+sx,p[1]+sy,p[2]+sz)) + _ba.set_camera_target(*q) + bui.apptimer(0.0081,s.zoom) + +class ActionManager: + """Action manager""" + actions = lambda c=0: ['Say','MoveTo','Wait','GoTo','Key','Follow'] + @classmethod + def classes(cls): + g = globals() + return [g['Action'+i] for i in cls.actions()] + @classmethod + def which(cls,s): + n = s.__class__ + i = cls.classes().index(n) + return (cls.actions()[i],i) + def __init__(s,source=None): + s.z = icw( + in_source=source, + show_nuke=False, + back_anim='out_scale', + title='Actions', + note='Advanced' + ) + w = s.z.widget + sv = bui.scrollwidget(parent=w, + size=(400,310), + color=var('bg'), + position=(60,50)) + s.cv = cw(parent=sv, + background=False, + size=(400,0)) + b = bw(p=w, + size=(100,50), + pos=(470,310), + label='Add', + icon=bui.gettexture('powerupHealth')) + bw(b,oac=bs.CallPartial(ActionPipe,source=b,pipe=s.add,what='Add')) + s.kids = [] + for i in range(3): + j = [s.edit,s.replace,s.delete][i] + k = ['Edit','Replace','Delete'][i] + l = [256,202,148][i] + m = ['settingsIcon','replayIcon','crossOut'][i] + b = bw(p=w, + size=(100,50), + pos=(470,l), + label=k, + icon=bui.gettexture(m), + oac=j) + s.kids.append(b) + for i in [-1,1]: + j = ['Up','Down'][i<0] + k = [94,40][i<0] + bw(p=w, + size=(100,50), + pos=(470,k), + label=j, + icon=bui.gettexture(f'{j.lower()}Button'), + repeat=True, + oac=bs.CallPartial(s.nav,-i)) + s.texts = [] + s.sl = None + s.update() + """Index control""" + def nav(s,i): + l = len(var('act'))-1 + if s.sl is None: i = [0,l][i<0] + else: i = s.sl + i + if i < 0 or i > l: bui.getsound('block').play(); return + s.select(i) + bui.getsound('tap').play() + """Conditional collect""" + def collect(s,i): + if i == 0: a = [0,var('say'),var('tsay')] + elif i == 1: a = [i,[round(float(var(f'move{i}')),3) for i in range(3)],float(var('smove')),float(var('tmove'))] + elif i == 2: a = [i,round(float(var('wait')),3)] + elif i == 3: a = [i,int(var('goto')),int(var('egoto'))] + elif i == 4: a = [i,var('lastkey')] + elif i == 5: a = [i,var('follow'),float(var('sfollow')),float(var('tfollow'))] + else: bs.broadcastmessage('nah '+str(i)); return + return a + """Add action""" + def add(s,i): + o = var('act') + a = s.collect(i) + o.append(a) + var('act',o) + s.update() + """Edit action""" + def edit(s): + if s.sl is None: btw('Select an action to edit!'); return + o = var('act') + v = o[s.sl] + i = v[0] + def f(): + a = s.collect(i) + o[s.sl] = a + var('act',o) + s.update() + gun() + s.classes()[i](source=s.kids[0],pipe=lambda z: f()) + """Replace action""" + def replace(s): + if s.sl is None: btw('Select an action to replace!'); return + o = var('act') + def f(i): + a = s.collect(i) + o[s.sl] = a + var('act',o) + s.update() + gun() + ActionPipe(source=s.kids[1],pipe=lambda i: f(i),what='Replace') + """Update""" + def update(s): + v = var('act') + l = len(v) + [t.delete() for t in s.texts] + s.texts.clear() + w = s.cv + y = l*30+10 + cw(w,size=(400,y)) + with bs.get_foreground_host_activity().context: + nn = bs.getnodes() + ns = [str(nn[i]) for i in range(len(nn))] + del nn + mem = s.actions() + for i in range(len(v)): + a = v[i] + t = tw( + p=w, + text=f'{i+1} {mem[a[0]]} {a[1:]}', + size=(350,30), + maxwidth=350, + selectable=True, + click_activate=True, + on_activate_call=bs.CallPartial(s.select,i), + v_align='center', + pos=(20,(y-40)-i*30) + ) + s.texts.append(t) + if a[0] == 5 and a[1] != 'me' and not a[1] in ns: + tw(t, color=(1,0,0), on_activate_call=bs.CallPartial(s.select,i,dead=True)) + s.sl = None + """Update selection""" + def up(s): + [tw(s.texts[i],color=[(1,1,1),(0,1,0)][i==s.sl]) for i in range(len(s.texts))] + cw(s.cv,visible_child=s.texts[s.sl]) + """Select""" + def select(s,i,dead=False): + s.sl = i + s.up() + if dead: btw('Follow node is dead!\nAction will be ignored. Pick the node again.') + """Delete selected""" + def delete(s): + if s.sl is not None: var('act').pop(s.sl); s.update(); bui.getsound('pop01').play() + else: btw('Select an action to delete!') + +class ActionPipe: + """Action pipe""" + def __init__(s,pipe,source=None,what='Action'): + s.pipe = pipe + ah = ActionManager.actions() + eh = ActionManager.classes() + l = len(ah) + y = 130+60*l + s.z = icw(in_source=source, + show_nuke=False, + back_anim='out_scale', + size=(300,y), + title=what) + w = s.z.widget + for i in range(l): + b = bw(p=w, + pos=(25,(y-150)-60*i), + size=(250,55), + label=ah[i]) + bw(b,oac=bs.CallPartial(eh[i],s.pick,b)) + """Redirect""" + def pick(s,i): + s.z.back() + s.pipe(i) + +class ActionSay: + """Action Say""" + def __init__(s,pipe,source=None): + s.which = ActionManager.which(s) + s.pipe = pipe + s.z = icw( + in_source=source, + title=s.which[0], + size=(450,230), + back_anim='out_scale', + show_nuke=False, + note='The more the duration, the slower the animation.', + note_pos=(15,10), + note_scale=0.6 + ) + w = s.z.widget + s.txt = [] + for i in range(2): + j = ['Say','And last for duration'][i] + k = [120,90][i] + l = ['say','tsay'][i] + m = [True,'0123456789'][i] + n = [80,310][i] + o = [330,100][i] + p = ['text','seconds'][i] + tw(p=w,text=j,pos=(30,k)) + s.txt.append(ctw(conf=l,text=var(l),allow=m,hint=p,pos=(n,k),p=w,size=(o,30),h_align='left')) + bw( + p=w, + pos=(330,30), + size=(80,50), + oac=s.pick, + label='Done' + ) + """Pick""" + def pick(s): + for t in s.txt: + if t.bad or not t.get_text(): err(f'Invalid {t.hint}!\nFix your input!'); return + [var(['say','tsay'][i],s.txt[i].get_text()) for i in [0,1]] + s.z.back() + gun() + s.pipe(s.which[1]) + +class ActionFollow: + """Action follow""" + def __init__(s,pipe,source=None): + s.which = ActionManager.which(s) + s.z = icw( + in_source=source, + title=s.which[0], + size=(450,230), + back_anim='out_scale', + show_nuke=False, + note="Keep in mind that dist is relevant to target's center", + note_scale=0.6, + note_pos=(15,5) + ) + w = s.z.widget + s.pipe = pipe + for i in range(2): + j = ['Me','Pick'][i] + b = bw(p=w, + label=j, + pos=(25,90-55*i), + size=(100,50)) + o = [bs.CallPartial(s.pick,'me'),bs.CallPartial(NodePicker,s.pick,b,allow='3D',note='Coolbox stores picked nodes as strings for portability')][i] + bw(b,oac=o) + for i in range(3): + j = ['Stop following when:','- Distance is less than','- Time is more than'][i] + k = [105,70,40][i] + l = [300,200][i>1] + tw(p=w, + pos=(135,k), + maxwidth=l, + text=j) + for i in range(2): + j = [380,340][i] + k = [67,37][i] + l = ['sfollow','tfollow'][i] + m = ['Bot might keep following until timeout if distance is too narrow or zero, enter','Time starts from the moment action is played, stop following when time is more than'][i] + ctw(p=w, + pos=(j,k), + size=(65,30), + allow='0.123456789', + conf=l, + description=m, + text=var(l)) + """Redirect""" + def pick(s,n): + var('follow',str(n)) + s.pipe(s.which[1]) + s.z.back() + gun() + +class ActionKey: + """Action send key""" + def __init__(s,pipe,source=None): + s.which = ActionManager.which(s) + s.z = icw( + in_source=source, + title=s.which[0], + size=(250,300), + back_anim='out_scale', + show_nuke=False + ) + w = s.z.widget + s.pipe = pipe + for i in range(4): + j = [105,155,105,55][i] + k = [50,100,150,100][i] + l = ['Jump','Bomb','PickUp','Punch'][i] + sbw( + p=w, + pos=(j,k), + icon=bui.gettexture('button'+l), + oac=bs.CallPartial(s.set,i) + ) + """Initial set""" + def set(s,i): + var('lastkey',i) + s.pick() + """Pick""" + def pick(s): + s.z.back() + s.pipe(s.which[1]) + gun() + +class ActionGoTo: + """Action go to""" + def __init__(s,pipe,source=None): + s.which = ActionManager.which(s) + s.z = icw(in_source=source, + title=s.which[0], + size=(500,200), + back_anim='out_scale', + show_nuke=False) + w = s.z.widget + s.pipe = pipe + t = tw(pos=(60,90), + p=w, + text='Go to action with index') + for i in range(2): + j = ['Usable for','times'][i] + t = tw(pos=(60+190*i,50), + p=w, + text=j) + s.t = ctw(p=w, + text=var('goto'), + pos=(315,85), + size=(65,40), + conf='goto', + description='Simulate a loop by going to a previous action again, or skip specific next actions. Enter', + hint='num', + allow='01223456789') + s.u = ctw(p=w, + text=var('egoto'), + pos=(180,45), + size=(65,40), + conf='egoto', + description='Expire after being used for', + hint='num', + allow='01223456789') + bw(p=w, + pos=(390,30), + size=(80,40), + oac=s.pick, + label='Done') + """Pick""" + def pick(s): + t = s.t.get_text() + u = s.u.get_text() + if s.t.bad or (not t) or (not u): err('Fix your input!'); return + if ((int(t) <= 0) or (int(t) >= len(var('act')))): + err('No such action exists!') + return + if (int(u) <= 0): + btw("At least repeat it 1 time!") + return + s.z.back() + s.pipe(s.which[1]) + gun() + +class ActionWait: + """Action wait""" + def __init__(s,pipe,source=None): + s.which = ActionManager.which(s) + s.z = icw(in_source=source, + title=s.which[0], + size=(500,200), + back_anim='out_scale', + show_nuke=False) + w = s.z.widget + s.pipe = pipe + for i in range(2): + j = ['Keep waiting for','seconds'][i] + t = tw(pos=(60+260*i,80), + p=w, + text=j) + s.t = ctw(p=w, + text=var('wait'), + pos=(248,75), + size=(65,40), + conf='wait', + description='Do nothing for', + hint='num', + allow='0.1223456789') + bw(p=w, + pos=(390,30), + size=(80,40), + oac=s.pick, + label='Done') + """Pick""" + def pick(s): + if s.t.bad: err('Fix your input!'); return + s.z.back() + s.pipe(s.which[1]) + gun() + +class ActionMoveTo: + """Action move to""" + def __init__(s,pipe,source=None): + s.which = ActionManager.which(s) + s.z = icw(in_source=source, + title=s.which[0], + size=(500,230), + back_anim='out_scale', + show_nuke=False) + w = s.z.widget + s.pipe = pipe + s.pos = [] + tw(pos=(30,120), + p=w, + text='Directly move towards') + tw(pos=(30,80), + p=w, + text='Stop when distance is less than') + tw(pos=(30,40), + p=w, + text='Or when time is more than') + for i in range(3): + j = ['X','Y','Z'][i] + t = ctw(p=w, + text=var(f'move{i}'), + pos=(290+60*i,115), + size=(55,40), + conf=f'move{i}', + hint=j+' position', + allow='-0.1223456789') + s.pos.append(t) + s.t = ctw(p=w, + text=var('smove'), + pos=(380,75), + size=(60,40), + conf='smove', + description='Minimum distance between bot and target', + hint='Dist', + allow='0.1223456789') + s.u = ctw(p=w, + text=var('tmove'), + pos=(325,35), + size=(60,40), + conf='tmove', + description='Timeout before force stopping if dist is not met', + hint='Time', + allow='0.1223456789') + bw(p=w, + pos=(390,30), + size=(80,40), + oac=s.pick, + label='Done') + """Pick""" + def pick(s): + for t in s.pos+[s.t,s.u]: + if t.bad: err('Fix your input!'); return + s.z.back() + s.pipe(s.which[1]) + gun() + +class ConPipe: + """Conditional pipe""" + cons = lambda: ['Has Group ID','In Team'] + def __init__(s,source,pipe): + s.pipe = pipe + mem = s.__class__.cons() + l = len(mem) + y = 130+60*l + s.z = icw( + in_source=source, + show_nuke=False, + back_anim='out_scale', + size=(300,y), + note='Ignored until found', + note_pos=(15,10), + note_scale=0.6, + title='Condition', + outside_action='back', + auto_offset=False + ) + w = s.z.widget + g = globals() + for i in range(l): + j = mem[i] + b = bw( + p=w, + pos=(25,(y-150)-60*i), + size=(250,55), + label=j + ) + bw(b,oac=bs.CallPartial(s.collect,b,i)) + """Collect""" + def collect(s,b,i): + s.extra = i + Collector(source=b,pipe=s.pick,allow=['01223456789',True][i]) + """Redirect""" + def pick(s,t): + s.z.back() + s.pipe([s.extra,t]) + +class Collector: + """Value collector""" + def __init__( + s, + source, + pipe, + allow=True, + title='Input', + first='text', + double='', + two=False, + dallow=True, + raw=False + ): + s.pipe = pipe + s.two = two + s.raw = raw + s.z = icw( + in_source=source, + show_nuke=False, + back_anim='out_scale', + auto_offset=False, + size=(300,150+[0,30][two]), + title=title + ) + w = s.z.widget + s.t = ctw( + p=w, + allow=allow, + hint=first, + size=(200,30), + pos=(30,30) + ) + s.t2 = ctw( + p=w, + allow=dallow, + hint=double, + size=(200,30), + pos=(30,65) + ) if two else None + bw( + label='Done', + p=w, + pos=(230,30), + size=(50,30), + oac=bs.CallPartial(s.pick) + ) + def pick(s): + t = s.t.get_text() + if s.t.bad or (not t and not s.raw): err(f'Invalid {"Input" if s.raw else s.t.hint}!\nFix your input!'); return + if s.two: + t2 = s.t2.get_text() + if s.raw and t == "": v = '' + elif s.raw: + try: v = eval(t) + except Exception as e: + err(str(e)) + return + else: v = t + r = (t2,v) + else: r = t + s.z.back() + s.pipe(r) + +class Overlay: + """Controls overlay""" + def __init__(s,build=False): + s.colors = [ + [(0.2,0.6,0.2),(0.4,1,0.4)], + [(0.6,0,0),(1,0,0)], + [(0.2,0.6,0.6),(0.4,1,1)], + [(0.6,0.6,0.2),(1,1,0.4)], + [(0.3,0.23,0.5),(0.2,0.13,0.3)] + ] + s.pics = [] + s.texts = [] + s.pos = [] + s.nub = [] + s.old = [0,0,0] + with bs.get_foreground_host_activity().context: + # This is a bascenev1 scene overlay, not a bauiv1 window - it + # has to use the scene 'attach' node attr (like Scoreboard and + # RespawnIcon do) to anchor to a screen corner. bui.get_virtual_ + # screen_size() is a bauiv1-only call and means nothing here, + # which is why it went invisible/out of bounds. 'attach': + # 'topRight' + small negative offsets keeps this pinned to the + # bottom-right corner regardless of resolution. + for i in range(4): + j = ['Jump','Bomb','PickUp','Punch'][i] + k = [-210,-160,-210,-260][i] + l = [-175,-125,-75,-125][i] + c = s.colors[i][0] + n = bs.newnode( + 'image', + attrs={ + 'texture': bs.gettexture('button'+j), + 'absolute_scale': True, + 'attach': 'topRight', + 'position': (k,l), + 'scale': (60,60), + 'color': c + } + ) + s.pics.append(n) + j = (['Down','Exit','Up','Build'] if build else ['Down','Pick','Up','Boost'])[i] + k = [-210,-130,-210,-295][i] + l = [-230,-125,-20,-125][i] + h = ['center','left','center','right'][i] + v = ['bottom','center','top','center'][i] + n = bs.newnode( + 'text', + attrs={ + 'text': j, + 'h_attach': 'right', + 'v_attach': 'top', + 'position': (k,l), + 'color': c, + 'h_align': h, + 'v_align': v + } + ) + s.texts.append(n) + for i in range(3): + c = s.colors[[1,0,2][i]][0] + n = bs.newnode( + 'text', + attrs={ + 'text': '0', + 'h_attach': 'right', + 'v_attach': 'top', + 'position': (-170,[-190,-220,-250][i]), + 'color': c, + 'h_align': 'left' + } + ) + s.pos.append(n) + s.np = (-20,-205) + for i in [0,1]: + j = [110,60][i] + n = bs.newnode( + 'image', + attrs={ + 'texture': bs.gettexture('nub'), + 'absolute_scale': True, + 'attach': 'topRight', + 'position': s.np, + 'scale': (j,j), + 'color': s.colors[4][i] + } + ) + s.nub.append(n) + s.fade() + """Color overlays""" + def set(s,i,c): + s.pics[i].color = s.texts[i].color = c + """Color position""" + def pset(s,i,c): + s.pos[i].color = c + """Simulate pressed""" + def press(s,i): + s.set(i,s.colors[i][1]) + s.pics[i].opacity = 1.0 + """Simulate released""" + def release(s,i): + s.set(i,s.colors[i][0]) + s.pics[i].opacity = 0.7 + """Get all nodes""" + def nodes(s): + return s.pics+s.texts+s.pos+s.nub + """Update position""" + def up(s,x,y,z,lr,ud): + new = [x,y,z] + for i in range(3): + c = s.colors[[1,0,2][i]] + if s.old[i] == new[i]: s.pset(i,c[0]); continue + t = s.pos[i] + t.text = str(round(new[i],5)) + s.pset(i,c[1]) + s.old = new + [setattr(s.nub[i],'opacity',[[0.5,0.2],[0.7,0.3]][bool(lr or ud)][i]) for i in [0,1]] + p = s.np + m = math.sqrt(lr**2+ud**2) or 1 + d = 25*min(math.sqrt(lr**2+ud**2),1) + lr /= m + ud /= m + s.nub[1].position = (p[0]+lr*d,p[1]+ud*d) + """Fade""" + def fade(s,i=0): + [bs.timer(1, bs.animate(n,'opacity',{0:i,0.5:abs(i-0.7)}).delete) for n in s.nodes()] + """Destroy overlay""" + def destroy(s): + with bs.get_foreground_host_activity().context: + bs.timer(0.2,lambda:s.fade(0.7)) + bs.timer(1,lambda: [n.delete() for n in s.nodes()]) + +class Mapper: + """In-Game position mapper""" + last = 0 + def __init__(s, pipe, pos=None, on_exit=None, build=False) -> None: + s.tired = time.time_ns() - s.__class__.last < 10**9 + if s.tired: btw('Cool down!'); return + p = pos or getpos() + s.pipe = pipe + s.on_exit = on_exit + s.build_mode = build + s.tex = 'achievementCrossHair' + s.btex = 'achievementSuperPunch' + with bs.get_foreground_host_activity().context: + M = bs.Material() + M.add_actions( + conditions=(('they_are_older_than', 0)), + actions=( + ('modify_part_collision', 'collide', False), + ('modify_part_collision', 'physical', False), + ('modify_part_collision', 'friction', 0), + ('modify_part_collision', 'stiffness', 0), + ('modify_part_collision', 'damping', 0) + ) + ) + n = bs.newnode( + 'prop', + delegate=s, + attrs={ + 'mesh': bs.getmesh('tnt'), + 'color_texture': bs.gettexture(s.tex), + 'body': 'crate', + 'reflection': 'soft', + 'density': 4.0, + 'reflection_scale': [1.5], + 'shadow_size': 0.6, + 'position': p, + 'gravity_scale': 0, + 'materials': [M], + 'is_area_of_interest': True + } + ) + bs.timer(0.1, bs.animate(n,'mesh_scale',{0:2,0.1:0.5}).delete) + SND('laser',p) + s.safe = None + def f(): s.safe = s.node.exists() + bui.apptimer(1,f) + s.step = s.ostep = 0.008 + l = len(KIDS()) + if l > 15: s.step = s.ostep = 0.008 + l/200 + s.node = n + s.wait = 0.001 + s.bstep = s.step * (20/8) + s.mode = 4 + s.llr = s.lud = 0.0 + s.overlay = Overlay(build=s.build_mode) + binds = { + 'UP_DOWN': lambda a: s.manage(a), + 'LEFT_RIGHT': lambda a: s.manage(a,1), + 'PICK_UP_PRESS': lambda: s.start(2), + 'JUMP_PRESS': lambda: s.start(0), + 'PICK_UP_RELEASE': lambda: s.stop(2), + 'JUMP_RELEASE': lambda: s.stop(0), + } + if s.build_mode: + binds['BOMB_PRESS'] = s.exit + binds['PUNCH_PRESS'] = s.build + else: + binds['BOMB_PRESS'] = s.pick + binds['PUNCH_PRESS'] = s.boost + binds['PUNCH_RELEASE'] = lambda: s.boost(0) + LN(binds) + STATE(True) + s.move() + """Handle events""" + def handlemessage(s, m): + if isinstance(m, bs.OutOfBoundsMessage): s.destroy() + """Destroy""" + def destroy(s): + with bs.get_foreground_host_activity().context: + n = s.node + MESS(n.position) + s.mode = 2 + n.delete() + bs.timer(1, s.reset) + """Reset input""" + def reset(s): + me = getme() + if not me: return + me.resetinput() + with bs.get_foreground_host_activity().context: me.actor.connect_controls_to_player() + """Manage movement""" + def manage(s,a,lr=0): + if lr: s.llr = a; return + s.lud = a + """Move""" + def move(s): + m = getme(1) + if (not m) or m._dead: s.destroy(); bs.broadcastmessage('nah2') + try: p = s.getpos() + except: + if STATE(): + STATE(False) + if bs.get_foreground_host_activity() != bs.get_foreground_host_activity(): return + bui.apptimer(1,s.fail) + return + s.setpos((p[0]+s.llr*s.step,p[1],p[2]-s.lud*s.step)) + s.overlay.up(*p,s.llr,s.lud) + _ba.set_camera_target(*p) + bui.apptimer(s.wait,s.move) + """Start elevating""" + def start(s,i): + s.overlay.press(i) + s.mode = i + s.loop(i) + """Keep elevating""" + def loop(s,i): + if s.mode != i: return + try: p = list(s.node.position) + except: return + p[1] += s.step if i else -s.step + s.node.position = tuple(p) + bui.apptimer(s.wait, lambda: s.loop(i)) + """Stop elevating""" + def stop(s,i): + s.overlay.release(i) + s.mode = 4 + """Get position""" + def getpos(s): + return s.node.position + """Set position""" + def setpos(s,p): + s.node.position = p + """Pick position (single-shot: exits mapper immediately)""" + def pick(s): + s.overlay.press(1) + s.overlay.destroy() + try: p = s.node.position + except: return + with bs.get_foreground_host_activity().context: + SND('powerup01',p) + bs.timer(0.1, bs.animate(s.node,'mesh_scale',{0:0.5,0.1:1.5}).delete) + bs.timer(0.1,lambda: (SPARK(p),s.node.delete())) + STATE(False) + s.pipe(p) + bui.apptimer(1,s.reset) + s.__class__.last = time.time_ns() + """Boost""" + def boost(s,i=1): + s.step = s.bstep if i else s.ostep + s.overlay.press(3) if i else s.overlay.release(3) + if i: + try: SND('punch01',s.node.position,2) + except: return + with bs.get_foreground_host_activity().context: + try: s.node.color_texture = bs.gettexture(s.btex if i else s.tex) + except: return + """Build at current position (repeatable - does not exit)""" + def build(s): + s.overlay.press(3) + try: p = s.node.position + except: return + with bs.get_foreground_host_activity().context: + bs.timer(0.1, lambda: s.overlay.release(3)) + SND('gunCocking',p) + bs.timer(0.2, bs.animate(s.node,'mesh_scale',{0:0.5,0.1:0.3,0.2:0.5}).delete) + SPARK(p) + s.pipe(p) + """Exit mapper without building (graceful pick-style animation, no shatter)""" + def exit(s): + s.overlay.press(1) + s.overlay.destroy() + try: p = s.node.position + except: p = None + with bs.get_foreground_host_activity().context: + if p: + SND('powerup01',p) + bs.timer(0.1, bs.animate(s.node,'mesh_scale',{0:0.5,0.1:1.5}).delete) + bs.timer(0.1,lambda: (SPARK(p),s.node.delete())) + else: + s.node.delete() + STATE(False) + s.__class__.last = time.time_ns() + if s.on_exit and p: bui.apptimer(1, bs.CallPartial(s.on_exit, p)) + bui.apptimer(1, s.reset) + """Complain""" + def complain(s): + push('You destroyed the mapper!',color=(1,0,0)) + bui.getsound('swip').play() + s.overlay.destroy() + None if s.safe else btw('Mapper was destroyed too early.\nFix your positions!') + """Handle move() losing the node (e.g. quitting mid-map): only + call pipe() if it's the no-arg pick-style callback - build mode's + pipe (dispatch) requires a real position and must not be called + with none.""" + def fail(s): + if not s.build_mode: s.pipe() + s.complain() + +def NEW(f): + """New class-based entry decorator""" + def g(w,b,i,in_source,source_cls,extra): + if hasm(): RESUME() + cw(w,transition="out_right") + cls = icw( + in_source=b, + out_source=in_source, + icon=i, + title=f.__name__, + cls=f, + show_tog=True, + extra=extra, + nuke_anim='out_scale' if in_source else 'out_right', + source_cls=source_cls, + on_back=lambda:Coolbox(fresh=False, in_source=in_source) + ).cls + return cls + g.__name__ = f.__name__ + g.__cls__ = f + return g + +@NEW +class Spawn: + """Spawn bots to game""" + def __init__(s,*a): + s.args = a + xoff = 45 + yoff = 45 + bx = 170 + by = 80 + s.mapper = None + s.can_ran = s.can_blud = True + ex = a[5] + # Main buttons + s.b = pbw( + p=a[0], + pos=(58.5,225), + size=(136,136), + tex=COLS() + ) + c = lambda: CharPicker(pipe=s.fresh,source=s.b) + bw(s.b, oac=c, color=(1,1,1)) + # Text + s.t = tw( + p=a[0], + pos=(100,193), + h_align='center', + maxwidth=170 + ) + tw( + p=a[0], + pos=(280, 197), + text='Position', + h_align='center' + ) + t = tw( + p=a[0], + pos=(455, 197), + text='Seed', + h_align='center' + ) + # Positioning + s.pos = [] + tx = 38 + for i in range(3): + e = ['X','Y','Z'][i] + t = ctw( + flash=True, + p=a[0], + size=(150,45), + conf=f'pos{i}', + hint=e+' Position', + allow='-0.1223456789', + pos=(230, 315-45*i), + ).widget + s.pos.append(t) + # Position smalls + for j in range(3): + i = ['scorch','slash','touchArrowsActions'][j] + c = [s.dot,s.invert,s.same][j] + sbw( + p=a[0], + pos=(230+50*j, 150), + size=(43,43), + icon=bui.gettexture(i), + icon_tint=-8, + cons=[None,CONS()[0]][j==2], + oac=c + ) + # Position buttons + for i in range(2): + bw( + p=a[0], + pos=(222,30+60*i), + icon=bui.gettexture(['touchArrows','cursor'][i]), + size=(160,55), + label=['Teleport','Map'][i], + cons={ + **CONS()[0], + **[{},CONS()[1]][i] + }, + oac=[s.teleport,s.map][i] + ) + # Character smalls + s.csm = {} + e = COLS() + for i in range(3): + f = e[i] + t = bw( + p=a[0], + pos=(60+50*i,150), + size=(38,38), + ) + oac = bs.CallPartial( + ColorPicker, + chr='char', + in_source=t, + on_back=s.fresh, + id=e[0], + id2=e[1], + what=f, + mode=i, + ) + c = var(f) + bw(t, oac=oac, color=c) + s.csm[f] = t + # Character buttons + for i in range(2): + j = ['Sound','Mesh'][i] + k = ['audioIcon','menuIcon'][i] + b = bw( + p=a[0], + pos=(48,30+60*i), + icon=bui.gettexture(k), + size=(160,55), + label=j + ) + bw(b,oac=bs.CallPartial([SoundManager,MeshManager][i],source=b,on_back=s.fresh)) + # Attributes + s.t2 = ctw( + p=a[0], + pos=(408,315), + size=(150,45), + allow=True, + text=var('tchar'), + conf=('tchar','char'), + description='Name', + on_conf=s.fresh + ).widget + s.t3 = ctw( + p=a[0], + pos=(408,270), + size=(150,45), + allow="01223456789", + text=var('lastid'), + conf='lastid', + flash=True, + hint='Group ID' + ).widget + s.seed_t = ctw( + p=a[0], + pos=(410,225), + size=(150,45), + allow=True, + conf='seed', + type='encoding', + text=var('seed'), + on_conf=s.apply, + hint='Seed', + description='Seed includes name, character, colors, mesh, sounds and custom name. Sharing seed will share all these. Enter' + ) + for i in range(3): + j = ['replayIcon','file','logIcon'][i] + k = [s.randomize,s.copy,s.paste][i] + sbw( + p=a[0], + pos=(410+50*i,150), + icon=bui.gettexture(j), + oac=k + ) + s.sbtn = [] + for i in range(2): + j = ['Spawn','Actions'][i] + k = ['downButton','trophy'][i] + l = [ + s.spawn, + lambda: ActionManager(source=s.sbtn[1]) + ][i] + b = bw( + p=a[0], + pos=(398,30+60*i), + icon=bui.gettexture(k), + size=(160,55), + label=j, + oac=l + ) + s.sbtn.append(b) + # Resume + s.fresh() + [tw(s.pos[i],text=var(f'pos{i}')) for i in range(3)] + if ex and rnd(ex) != getpos(): + bui.apptimer(0.2, lambda: (s.setpos(ex), gun())) + """Copy seed""" + def copy(s): + bui.clipboard_set_text(var('seed')) + bui.getsound('ding').play() + s.seed_t.blink() + push('Copied seed to clipboard!',color=(0,1,0)) + """Paste seed""" + def paste(s): + s.seed_t.set_text(PASTE(),silent=True) + gun() + s.seed_t.blink((0,1,1)) + push('Pasted seed from clipboard!',color=(0,1,1)) + """Randomize seed""" + def randomize(s): + if not s.can_ran: (setattr(s,'can_blud',False),btw(random.choice(SLOWDOWN())),bui.apptimer(5,bs.CallPartial(setattr,s,'can_blud',True))) if s.can_blud else bui.getsound('block').play(); return + s.can_ran = False + s.seed_t.set_text(RANDOM(),silent=True) + bui.getsound('cashRegister2').play() + push('Randomized seed!',color=(1,1,0)) + bui.apptimer(0.2,lambda:setattr(s,'can_ran',True)) + """Refresh previews""" + def fresh(s,i=None,ms=True,silent=True): + if i is not None: c = cc = NAME()[i]; var('char',c); var('tchar',c); fixall() + else: c = var('char'); cc = var('tchar') + mem = COLS() + n = var(mem[2]) or (1,1,1) + [tw(t,text=cc,color=n) for t in [s.t,s.t2]] + char = D()[c] + tc = var(mem[0]) or (1,1,1) + tc2 = var(mem[1]) or (1,1,1) + bw( + s.b, + texture=bui.gettexture(char.icon_texture), + color=(1,1,1), + tint_texture=bui.gettexture(char.icon_mask_texture), + tint_color=tc, + tint2_color=tc2 + ) + [bw(s.csm[z],color=var(z) or (1,1,1)) for z in list(s.csm.keys())] + # char, tchar, main, hl, name, (*mesh,ctex,ctex2), (*sounds) + if ms: + e = SEED(c,cc,tuple(tc),tuple(tc2),tuple(n)) + var('seed',e) + s.seed_t.set_text(e,silent=silent) + s.seed_t.blink((1,1,0)) + """Apply seed""" + def apply(s): + try: a = CHECK(var('seed')) + except: push('Found a bad seed in memory'); return + mem = COLS() + var('char',a[0]) + var('tchar',a[1]) + var(mem[0],a[2]) + var(mem[1],a[3]) + var(mem[2],a[4]) + arr = list(a[5]) + for i in range(len(DIR('mesh'))): var(f'mesh{i}',arr[i]) + var('ctex',arr[-2]) + var('ctex2',arr[-1]) + [var(f'sound{i}',a[6][i]) for i in range(len(DIR('sounds')))] + s.fresh(ms=False) + s.seed_t.blink((1,1,0)) + """Actually spawn""" + def spawn(s): + a = bs.get_foreground_host_activity() + with a.context: + i = Bot( + name=var('tchar'), + name_color=var('Name'), + mesh=[var(f'mesh{i}') for i in range(len(DIR('mesh')))], + ctex=var('ctex'), + ctex2=var('ctex2') + ) + id = UUID() + i.uid = id + a.customdata[id] = i + s.on_spawn(i) + return id + """On spawn""" + def on_spawn(s, b): + n = b.node + pos = getpos() + bs.getsound('spawn').play(position=pos) + n.handlemessage('flash') + n.is_area_of_interest = True + b.handlemessage(bs.StandMessage(pos,0)) + FOCUS(n,2,1) + # Apply sounds + set_sounds(n) + """Teleport""" + def teleport(s): + me = getme(1) + n = me.node + FOCUS(n,2,1) + p = getpos() + me.handlemessage(bs.StandMessage(p, 0)) + n.handlemessage('flash') + SND('spawn',p) + """Draw dot""" + def dot(s): + p = getpos() + LOOK(p, on_found=lambda: push('Located positon!', color=(1,1,1))) + """Invert position""" + def invert(s): + p = getpos() + s.setpos((-p[0],p[1],-p[2])) + bui.getsound('deek').play() + push('Inverted position!',color=(0.5,0.7,1)) + """Same player position""" + def same(s): + s.setpos(getme(1).node.position) + gun() + push('Used your position!', color=(1,0.7,0.2)) + """Set position""" + def setpos(s,p): + p = rnd(p) + [tw(s.pos[i],text=str(p[i])) for i in range(3)] + """Map position""" + def map(s): + s.mapper = Mapper(pipe=s.pick,pos=getpos()) + None if s.mapper.tired else cw(s.args[0], transition='out_right') + """Pick position""" + def pick(s,p=None): + Coolbox(fb=s.__class__.__name__, fake=True, extra=(p or getpos())) + +class shadow: + """Widget blocker""" + def __init__(s,p,pos,size,conf): + s.add = s.pro = 0 + s.conf = conf + s.size = size + s.a = bui.imagewidget( + texture=bui.gettexture('black'), + parent=p, + size=size, + position=pos, + ) + s.b = bui.scrollwidget( + parent=p, + color=var('bg'), + size=size, + position=pos, + border_opacity=0 + ) + s.fresh() + """Refresh""" + def fresh(s): + v = var(s.conf) + bui.scrollwidget(s.b,size=([s.size,(0,0)][v])) + s.add = [0.01,-0.01][v] + def f(j): + if s.add != j: return + if s.pro > 0.8: s.pro = 0.8; return + if s.pro < 0: s.pro = 0; return + s.pro += j + bui.imagewidget(s.a,opacity=s.pro) + bui.apptimer(0.001,bs.CallPartial(f,j)) + f(s.add) + +@NEW +class Modify: + """Modify in-game players and bots""" + def __init__(s,*a): + s.a = a + s.mem = WHAT() + w = a[0] + q = bui.scrollwidget( + parent=w, + color=var('bg'), + size=(150,270), + position=(50,100) + ) + s.c = cw( + parent=q, + size=(150,100), + background=False + ) + for i in range(3): + tw( + text=['Position','Template','Who?'][i], + pos=([435,250,75][i],[70,200][i<1]), + h_align='center', + size=(100,30), + p=w + ) + b = bw( + p=w, + pos=(53+50*i,25), + label=[bui.charstr(bui.SpecialChar.UP_ARROW),'?','&'][i], + text_scale=[1.2,1.4][i>0], + size=(40,40), + **([{},{'cons':CONS()[0]}][i>1]) + ) + o = [ + bs.CallPartial(SpazPicker,source=b,pipe=s.add,note="It's recommended to modify one spaz at a time"), + bs.CallPartial(ConPipe,source=b,pipe=s.add), + bs.CallPartial(s.kang,source=b) + ][i] + bw(b, oac=o) + # Char name + s.ct = tw( + p=w, + pos=(274,197), + maxwidth=150, + h_align='center', + text=var('mchar') + ) + b = bw( + p=w, + pos=(227,25), + icon=bui.gettexture('achievementOutline'), + label='Load', + size=(150,40) + ) + bw(b,oac=bs.CallPartial(SpazPicker,source=b,pipe=s.load,what='Copy who?')) + bw( + p=w, + pos=(410,153), + icon=bui.gettexture('cursor'), + label='Map', + size=(150,40), + oac=s.map, + cons=CONS()[1] + ) + # Position editables + s.pos = [] + for i in range(3): + j = ['X','Y','Z'][i]+' Position' + t = ctw( + parent=w, + pos=(410,310-40*i), + size=(150,40), + allow='-0.1223456789', + hint=j, + conf=f'mpos{i}', + text=var(f'mpos{i}') + ) + s.pos.append(t) + s.cols = lambda: ['New '+i for i in COLS()] + e = s.cols() + s.cbs = [] + # Color smalls + for i in range(3): + f = e[i] + t = bw( + p=a[0], + pos=(230+50*i,153), + size=(40,40) + ) + oac = bs.CallPartial( + ColorPicker, + in_source=t, + on_back=s.fresh, + id=e[0], + id2=e[1], + what=f, + mode=i, + chr='mchar' + ) + bw(t, oac=oac, color=var(f) or (1,1,1)) + s.cbs.append(t) + b = sbw( + p=a[0], + pos=(230+50*i,103), + icon=bui.gettexture(['audioIcon','menuIcon','textClearButton'][i]) + ) + bw(b,oac=[ + bs.CallPartial(SoundManager,source=b,prf='m',cols=s.cols), + bs.CallPartial(MeshManager,source=b,prf='m',cols=s.cols), + bs.CallPartial(s.creset,source=b) + ][i]) + # Character preview + b = s.pre = bw( + p=a[0], + pos=(235,230), + color=(1,1,1), + size=(130,130) + ) + bw(b,oac=bs.CallPartial(CharPicker,source=s.pre,pipe=s.pipe,cols=s.cols,prf='m')) + s.targets = [] + s.kids = [] + # Name editable + s.nm = ctw( + p=a[0], + allow=True, + hint='Name', + conf='mtchar', + text=var('mtchar'), + pos=(410,109), + size=(150,35) + ) + s.ct2 = s.nm.widget + # Healh editable + s.ht = ctw( + p=a[0], + hint='Health', + description='Health ranges from 1 to 1000. Enter', + conf='mhp', + text=var('mhp'), + pos=(410,72), + allow='0.1223456789', + size=(150,35) + ) + # Modify button + bw( + p=a[0], + oac=s.modify, + label='Modify', + icon=bui.gettexture('settingsIcon'), + pos=(410,25), + size=(150,40) + ) + # What button + b = sbw( + p=a[0], + icon=bui.gettexture('menuButton'), + pos=(102,380), + size=(40,40) + ) + bw(b,oac=bs.CallPartial(s.what,b)) + # Attr button + b = sbw( + p=a[0], + icon=bui.gettexture('file'), + pos=(152,380), + size=(40,40), + ) + bw(b,oac=bs.CallPartial(s.attr,b)) + # Shadows + s.shads = [] + for i in range(7): + j = [ + (225,200),(225,150), + (225,100),(275,100), + (400,195),(405,107), + (405,70) + ][i] + k = [ + (150,170),(150,50), + (50,50),(50,50), + (170,160),(160,40), + (160,40) + ][i] + s.shads.append( + shadow( + p=a[0], + pos=j, + size=k, + conf=f'what{i}' + ) + ) + # Resume + ex = a[5] + if ex: + s.targets = ex[1] + s.fresh() + s.setpos(ex[0]) + s.pipe(var('mchar')) + s.spy() + """Edit attrs""" + def attr(s,b): + a = s.targets + if not len(a): btw('Select a target first!'); return + if len(a) > 1: btw('This only works on one target!'); return + t = a[0] + NodeManager(source=b,node=t,pipe=s.fresh) + """Optional modify""" + def what(s,source=None): + z = icw( + in_source=source, + title='Modify what?', + back_anim='out_scale', + size=(300,335), + show_nuke=False, + outside_action='back' + ) + w = z.widget + for i in range(7): + j = s.mem[i] + k = f'what{i}' + chk( + text=j, + value=var(k), + on_value_change_call=bs.CallPartial(s.opipe,k,i), + parent=w, + size=(200,20), + position=(30,235-35*i) + ) + """Optional pipe""" + def opipe(s,k,i,b): + var(k,b) + s.shads[i].fresh() + """Actually modify""" + def modify(s): + # node | [0,gid] | [1,tem] + l = [] + with bs.get_foreground_host_activity().context: mem = KIDS() + for t in s.targets: + if isinstance(t,list): + if t[0]: + for i in mem: + j = i.source_player + l.append(i) if j and j.team.name.evaluate() == t[1] else None + else: [l.append(i) if getattr(i.getdelegate(object),'group_id',0) == t[1] else None for i in mem] + else: l.append(t) + if not len(l): + btw('No targets found!') + return + co = s.cols() + for n in l: + [setattr(n,['color','highlight','name_color'][i],var(co[i]) or (1,1,1)) for i in range(3)] if var('what1') else None + t = s.nm.get_text(); setattr(n,'name',t) if t and var('what5') else None + t = s.ht.get_text(); [setattr(n.getdelegate(object),'hitpoints',float(t)),setattr(n,'hurt',1-(float(t)/1000))] if t and var('what6') else None + n.handlemessage(bs.StandMessage(s.getpos(),0)) if var('what4') else None + nam,spa = NAME(),SPAZ() + mem = spa[nam.index(var('mchar'))] + ok = ['color_texture','color_mask_texture'] + ok2 = DIR('mesh'); z = range(len(ok2)) + tex = [var(f'mmesh{i}') for i in z] + act = bs.get_foreground_host_activity() + with act.context: + mem = [bs.gettexture(var('mctex')), bs.gettexture(getattr(mem,ok[1])), mem.style] + tex = [bs.getmesh(tex[i]) for i in z] + [[setattr(n,ok[i],mem[i]) for i in range(2)] for n in l] if var('what0') else None + [[setattr(n,ok2[i],tex[i]) for i in z] for n in l] if var('what3') else None + [set_sounds(n,prf='m',spa=spa) for n in l] if var(f'what2') else None + SND('gunCocking',l[0].position) + # style + st = mem[2] + mem = BRUH() + for n in l: + ot = n.style + n.style = st + if (ot in mem[0] and st not in mem[0]) or st in mem[1]: + sp = n.source_player + if sp: pass # TODO: recreate player when needed + else: + b = Bot(NAME()[0],ignore=True) + b.uid = UUID() + bn = b.node + act.customdata[b.uid] = b + mem = BAD() + for i in dir(bn): + if i == 'node': continue + if i.startswith('_') or i in mem: continue + try: j = getattr(n,i) + except AttributeError: continue + if callable(j): continue + try: setattr(bn,i,j) + except RuntimeError: continue + p = n.position + p = (p[0],p[1]-0.6,p[2]) + b.handlemessage(bs.StandMessage(p,0)) + s.targets[s.targets.index(n)] = bn + c = Control.__cls__ + if c.__n__ == n: c.__n__ = bn; c.__in__.ln(b) + n.delete() + n.handlemessage('flash') + s.fresh(False) + ding() + """Kang hold node""" + def kang(s,source=None): + n = getme(1).node.hold_node + if not n or n.getnodetype() != 'spaz': btw('You are not holding a spaz!'); return + Confirm( + source=source, + title='Held player', + label='Add as target', + pipe=bs.CallPartial(s.add,n) + ) + """Confirm reset""" + def creset(s,source=None): + Confirm( + source=source, + title='Sure?', + label='Reset template', + pipe=s.reset + ) + """Reset""" + def reset(s): + o = NAME()[0] + var('mchar',o) + [var(c,(1,1,1)) for c in s.cols()] + [bw(b,color=(1,1,1)) for b in s.cbs] + tw(s.ct2,color=(1,1,1)) + fixall(o,prf='m') + s.fresh() + bui.getsound('swip').play() + """Load spaz""" + def load(s,n): + ui = NTEX(n) + var('mtchar',n.name) + c = s.cols() + var(c[0],ui[0]['tint_color']) + var(c[1],ui[0]['tint2_color']) + var(c[2],ui[1]) + [bw(s.cbs[i],color=var(c[i])) for i in range(3)] + s.pipe(ICONS().index(ui[2]+'Icon')) + o = n.getdelegate(object) + s.ht.set_text(str(round(o.hitpoints,1))) + gun() + """Character pipe""" + def pipe(s,i): + if isinstance(i,str): i = NAME().index(i) + else: + ok = NAME()[i] + var('mchar',ok) + var('mhp','1000.0') + fixall(prf='m') + c = s.cols() + ui = MK(SPAZ()[i],c[0],c[1]) + tw(s.ct,text=var('mchar'),color=var(c[2])) + tw(s.ct2,text=var('mtchar'),color=var(c[2])) + s.ht.set_text('1000.0') + bw(s.pre,**ui[0]) + """Set position""" + def setpos(s,p): + p = rnd(p) + h = [] + for i in range(3): + t = s.pos[i] + o = t.get_text() + n = str(p[i]) + h.append(t) if o != n else None + t.set_text(n) + bui.apptimer(0.2,lambda:([t.blink() for t in h],gun())) if h else None + """Get position""" + def getpos(s): + return rnd(tuple([float(var(f'mpos{i}')) for i in range(3)])) + """Map""" + def map(s): + s.mapper = Mapper(pipe=s.pick,pos=s.getpos()) + None if s.mapper.tired else cw(s.a[0], transition='out_right') + """Pick""" + def pick(s,p=None): + Coolbox(fb=s.__class__.__name__, fake=True, extra=[p or s.getpos(),s.targets]) + """Add target""" + def add(s,t): + if t in s.targets: btw('Target already exists!'); return + s.targets.append(t) + gun() + s.fresh(pipe=False) + """Remove target""" + def remove(s,n): + s.targets.remove(n) + bui.getsound('pop01').play() + s.fresh() + """Refresh""" + def fresh(s,pipe=True): + a = s.targets + l = len(a) + [k.delete() for k in s.kids] + tex = ALL() + for i in range(l): + n = a[i] + b = not isinstance(n,list) + if b: + ui = NTEX(n,tex) + t = getattr(n,'name','') or f'Target {i+1}' + else: + ui = {'color':(1,1,1),'texture':bui.gettexture(['achievementSharingIsCaring','achievementTeamPlayer'][n[0]])},(1,1,1) + t = n[1] + k1 = [bw,pbw][b]( + p=s.c, + pos=(10,i*155+29), + size=(110,110), + oac=bs.CallPartial(s.remove,n), + **ui[0] + ) + k2 = tw( + p=s.c, + pos=(40,i*155-5), + h_align='center', + maxwidth=105, + max_height=40, + text=t, + color=ui[1] + ) + [s.kids.append(k) for k in [k1,k2]] + cw(s.c, size=(100,l*155)) + v = var('mchar') + if pipe and v: s.pipe(NAME().index(v)) + """Spy""" + def spy(s): + if not s.c.exists() or s.c.transitioning_out: return + for n in s.targets: + if isinstance(n,list) or n.exists(): continue + btw(f'Target no. {s.targets.index(n)+1} is dead!') + s.remove(n) + bui.apptimer(0.1,s.spy) + +@NEW +class Control: + """Control anyone""" + __up__ = False + __in__ = None + __n__ = None + @classmethod + def spy(c): + if not c.__up__: return + i = c.__in__ + n = c.__n__ + m = getme(1) + if not m or m._dead and i: i.reset(n,True); c.__in__ = None; return + if not n or not n.exists() or n.getdelegate(object)._dead and i: i.reset(n); c.__in__ = None; return + bui.apptimer(0.1,c.spy) + def __init__(s,*a): + w = s.w = a[0] + s.n = None + # Target preview + s.b = bw( + p=w, + color=(1,1,1), + pos=(63.5,225), + size=(136,136), + texture=bui.gettexture('achievementEmpty') + ) + bw(s.b,oac=s.pick) + s.t = tw( + p=w, + text='Who?', + maxwidth=170, + pos=(105,193), + h_align='center' + ) + c = [ + (0.4,1,0.4), + (1,0,0), + (0.4,1,1), + (1,1,0.4), + [(0.3,0.23,0.5),(0.4,0.26,0.6)] + ] + bw( + p=w, + oac=s.apply, + label='Apply', + icon=bui.gettexture('settingsIcon'), + pos=(410,15), + size=(150,40) + ) + b = bw( + p=w, + label='Hold', + icon=bui.gettexture('achievementOutline'), + pos=(235,15), + size=(150,40) + ) + bw(b,oac=bs.CallPartial(s.hold,b)) + bw( + p=w, + label='Right arm', + icon=bui.gettexture('rightButton'), + pos=(235,155), + size=(150,40), + oac=bs.CallPartial(s.yay,'_r'), + repeat=True + ) + bw( + p=w, + label='Left arm', + icon=bui.gettexture('leftButton'), + pos=(235,108), + size=(150,40), + oac=bs.CallPartial(s.yay,'_l'), + repeat=True + ) + bw( + p=w, + label='Both arms', + icon=bui.gettexture('upButton'), + pos=(235,60), + size=(150,40), + oac=s.yay, + repeat=True + ) + cls = s.__class__ + cls.__in__ = s + b = cls.__up__ + s.sb = bw( + p=w, + oac=s.start, + pos=(58.5,15), + size=(150,40), + cons=CONS()[0] + ) + s.start(dry=True) + for i in range(5): + j = ['Jump','Bomb','PickUp','Punch','Move'][i] + y = 175-30*i + x = 410 + l = c[i] + t = tw( + p=w, + text=j, + pos=(x+35,y) + ) + if i<4: + bui.imagewidget( + parent=w, + size=(30,30), + color=c[i], + position=(x+4,y), + texture=bui.gettexture(f'button{j}') + ) + else: + [bui.imagewidget( + parent=w, + color=l[k], + texture=bui.gettexture('nub'), + size=[(35,35),(20,20)][k], + position=(x+1+7*k,y-2+7*k) + ) for k in [0,1]] + l = l[1] + tw(t,color=l) + v = f'cont{i}' + b = var(v) + bb = sbw( + parent=w, + textcolor=l, + size=(20,20), + text_scale=0.7, + position=(x+115,y+5), + label=['',bui.charstr(bui.SpecialChar.DPAD_CENTER_BUTTON)][b], + ) + bw(bb,oac=bs.CallPartial(s.check,bb,i,b)) + for i in range(4): + j = [ + 'Block him', + 'Close this', + 'Invincible', + 'Block you' + ][i] + v = f'cconf{i}' + chk( + parent=s.w, + text='', + position=(63.5,155-33*i), + value=var(v), + size=(170,30), + on_value_change_call=bs.CallPartial(s.cconf,v) + ) + tw( + p=s.w, + text=j, + maxwidth=95, + position=(100.5,158-33*i), + ) + for i in range(4): + j = [460,505,460,415][i] + k = [218,265,312,265][i] + l = ['Jump','Bomb','PickUp','Punch'][i] + sbw( + p=s.w, + pos=(j,k), + color=c[i], + size=(50,50), + oac=bs.CallPartial(s.key,i), + texture=bui.gettexture('button'+l) + ) + s.mbs = [] + s.mb = None + s.la = 0 + h = '_ARROW' + for i in range(3): + for j in range(3): + ij = i*3+j + k = [ + '','DOWN'+h,'', + 'LEFT'+h,'DPAD_CENTER_BUTTON','RIGHT'+h, + '','UP'+h,'' + ][ij] + b = bw( + p=s.w, + label=bui.charstr(getattr(bui.SpecialChar,k)) if k else k, + repeat=True, + size=(40,40), + position=(240+50*j,225+50*i) + ) + bw(b,oac=bs.CallPartial(s.move,i,j,b)) + s.mbs.append(b) + for i in range(4): + j = [ + (237.5,215), + (360,190), + (385,312.5), + (262.5,337.5) + ][i] + tw( + parent=s.w, + position=j, + text=bui.charstr(bui.SpecialChar.LEFT_ARROW), + rotate=45+90*i + ) + tw( + text='Long press to run', + color=(0.2,0.2,0.2), + position=(235,195), + parent=s.w, + scale=0.71 + ) + n = cls.__n__ + s.fresh(n,False) if n else n + """Celebrate""" + def yay(s,pr=''): + if s.nah(): return + s.n.handlemessage('celebrate'+pr,100) + """Hold""" + def hold(s,b): + if s.nah(): return + if s.n.hold_node: btw("Target's already holding something!"); return + NodePicker(pipe=lambda n: (bs.broadcastmessage(random.choice(HOLDSELF())) if s.n == n else None,bui.apptimer(0.2,lambda:(bs.broadcastmessage('Now resume the game to see changes') if pause() else None) if s.holds(n) else None)),source=b,allow='3D') + """Safe hold""" + def holds(s,w): + if w.getnodetype() in HOLDABLE(): + s.n.hold_node = w + return True + btw("You can't hold that!") + """Move target""" + def move(s,i,j,b): + if s.nah(): return + o = s.n.getdelegate(object) + if s.mb != b: + s.mb = b + bw(b,color=(0,1,0)) if i*3+j != 4 else None + [bw(_,color=var('bg')) for _ in s.mbs if _ != b] + else: + with bs.get_foreground_host_activity().context: + o.on_run(1) + s.ls = time.time_ns() + bs.timer(0.1,s.stop) + v = 32767; a = [-v,0,v] + x,y = a[j],a[i] + o.on_move_left_right(x) + o.on_move_up_down(y) + """Stop running""" + def stop(s): + i = time.time_ns() - s.ls + if i < 0.8*10**8: return + else: s.n.getdelegate(object).on_run(0) + """Back gracefully""" + def back(s): + if s.mb and s.mbs.index(s.mb) != 4: s.move(1,1,s.mbs[4]) + """Control config""" + def cconf(s,v,b): + var(v,b) + if s.__class__.__up__ and getattr(s,'tcconf',True): + s.tcconf = False + bs.broadcastmessage('Restart control to apply changes') + bui.apptimer(5,bs.CallPartial(setattr,s,'tcconf',True)) + """Send key""" + def key(s,i): + if s.nah(): return + with bs.get_foreground_host_activity().context: [getattr(s.n.getdelegate(object),f"on_{['jump','bomb','pickup','punch'][i]}_{['press','release'][j]}")() for j in [0,1]] + """Custom check""" + def check(s,bb,i,b): + b = not b + var(f'cont{i}',b) + bw(bb,label=['',bui.charstr(bui.SpecialChar.DPAD_CENTER_BUTTON)][b],oac=bs.CallPartial(s.check,bb,i,b)) + bui.getsound('deek').play() + """Conditional CharPicker""" + def pick(s): + SpazPicker(source=s.b,pipe=s.fresh,deny=[s.n],deny_msg="Already picked!") + """Verify""" + def nah(s): + if s.n is None: btw('No target selected!'); return 1 + elif (getattr(s.n,'exists',lambda:False)() and s.n.getdelegate(object)._dead) or (not s.n.exists()): btw('Selected target is dead!'); return 1 + """Apply inputs""" + def apply(s,n=None,l=None): + if not n: + if s.nah(): return + p = s.n.source_player + if not p: btw("Not an actual player!"); return + n = s.n + else: p = n.source_player + o = n.getdelegate(object) + p.resetinput() + l = l or [var(f'cont{i}') for i in range(5)] + p.customdata['cont'] = (l,n) + d = {'RUN': o.on_run} + z = lambda a,i=0: getattr(o,f"on_{a}_{['press','release'][i]}") + d.update({ + 'UP_DOWN': o.on_move_up_down, + 'LEFT_RIGHT': o.on_move_left_right + }) if l[4] else None + d.update({ + 'PICK_UP_PRESS': z('pickup'), + 'PICK_UP_RELEASE': z('pickup',1) + }) if l[2] else None + d.update({ + 'JUMP_PRESS': z('jump'), + 'JUMP_RELEASE': z('jump',1) + }) if l[0] else None + d.update({ + 'BOMB_PRESS': z('bomb'), + 'BOMB_RELEASE': z('bomb',1) + }) if l[1] else None + d.update({ + 'PUNCH_PRESS': z('punch'), + 'PUNCH_RELEASE': z('punch',1) + }) if l[3] else None + with bs.get_foreground_host_activity().context: LN(d,p) + ding() + push('Applied controls!',color=(0,1,0)) + n.handlemessage('flash') + """Start control""" + def start(s,dry=False,shut=False,tran=False,hm=False): + if not dry and s.nah(): return + me = getme(1).node + c = s.__class__ + if s.n == me and not shut: + bui.getsound('block').play() + if not getattr(s,'lmao1',True): return + s.lmao1 = False + bs.broadcastmessage(random.choice(["Now that's some self control lmao","Trying to control yourself?","Anyone but that.","But this is YOU!"])) + bui.apptimer(5,bs.CallPartial(setattr,s,'lmao1',True)) + return + b = s.__class__.__up__ + if not dry: s.__class__.__up__ = b = not b + bw(s.sb,label=['Start','Stop'][b],icon=bui.gettexture(f"ouya{['O','A'][b]}Button")) + if dry: return + if not b: s.reset(tran=tran,hm=hm); return + s.__class__.__n__ = s.n + o = s.n.getdelegate(object) + cw(s.w,transition='out_right') if var('cconf1') else None + z = lambda a,i=0: getattr(o,f"on_{a}_{['press','release'][i]}") + gun() + bs.broadcastmessage(f"Now {s.n.name or 'This spaz'} moves "+['like','instead of'][var('cconf3')]+' you!', color=(0,1,0)) + s.__class__.spy() + if var('cconf2'): me.invincible = True + with bs.get_foreground_host_activity().context: + s.ln(o) + s.n.handlemessage('flash') + FOCUS(s.n,2,1) + sp = s.n.source_player + if sp: + if var('cconf0'): sp.resetinput() + return + Bubble( + node=s.n, + time=4, + text=bui.charstr(getattr(bui.SpecialChar,random.choice([ + 'DPAD_CENTER_BUTTON', + 'LOGO_FLAT' + ])))+' '+random.choice(CONSTR()), + color=s.n.color + ) + p = s.n.position + bs.timer(1,lambda:(o.on_jump_press(),o.on_jump_release()) if math.dist(p,o.node.position) < 0.2 else None) + """Link""" + def ln(s,o): + m = getme(1) + v = var('cconf3') + a = lambda i: lambda e=None: (getattr(o,i)(*([e] if e is not None else [])),getattr(m,i)(*([e] if e is not None else [])) if not v else None) + LN({ + 'UP_DOWN': a('on_move_up_down'), + 'LEFT_RIGHT': a('on_move_left_right'), + 'PICK_UP_PRESS': a('on_pickup_press'), + 'PICK_UP_RELEASE': a('on_pickup_release'), + 'JUMP_PRESS': a('on_jump_press'), + 'JUMP_RELEASE': a('on_jump_release'), + 'BOMB_PRESS': a('on_bomb_press'), + 'BOMB_RELEASE': a('on_bomb_release'), + 'PUNCH_PRESS': a('on_punch_press'), + 'PUNCH_RELEASE': a('on_punch_release'), + 'RUN': a('on_run') + }) + """Reset""" + def reset(s,n=None,host=False,tran=False,hm=False): + b = n is not None + me = getme() + c = s.__class__ + o = n.getdelegate(object) if b else None + if b and n != s.__class__.__n__: return # denied + if b and o._dead: bs.broadcastmessage('Target died!'+random.choice([' LOL','','','']),color=(1,1,0)) + elif b and host: btw('You died!'); me.resetinput(); o.on_move_up_down(0.0); o.on_move_left_right(0.0) + else: push(['Stopped','Transferred'][tran and hm]+' control!',color=(1,1,0)); tran if tran else ding() + sp = c.__n__.source_player + if sp: + l = sp.customdata.get('cont',0) + if l and l[1].exists(): s.apply(n,l[0]) + else: sp.actor.connect_controls_to_player() + c.__n__ = None + c.__up__ = False + cw(s.w,transition='out_right') if s.w.exists() and not s.w.transitioning_out and b and o._dead else None + if host: return # host died + me.resetinput() + with bs.get_foreground_host_activity().context: me.actor.connect_controls_to_player() + h = me.actor.node + h.handlemessage('flash') + if h.invincible: h.invincible = False + """Refresh""" + def fresh(s,n,b=True): + s.move(1,1,s.mbs[4]) if s.n else None + s.n = n + ui = NTEX(n) + tw(s.t,text=getattr(n,'name','') or 'Unnamed',color=ui[1]) + bw(s.b,**ui[0],mask_texture=bui.gettexture('characterIconMask')) + if not b: return + gun() + c = s.__class__ + if c.__up__: + bub = Bubble.__mem__.get(c.__n__,0) + hm = n != getme(1).node + s.start(shut=True,tran=True,hm=hm) + s.start() if hm else None + if not bub: return + with bs.get_foreground_host_activity().context: bub.delete(force=True) + +@NEW +class Effect: + """Apply an effect to anyone""" + MEM = {} + def __init__(s,w,*a): + s.n = None + s.kids = [] + s.b = bw( + p=w, + color=(1,1,1), + pos=(60,225), + size=(136,136), + texture=bui.gettexture('achievementEmpty') + ) + bw(s.b,oac=s.pick) + s.t = tw( + p=w, + text='Who?', + maxwidth=170, + pos=(101.5,193), + h_align='center' + ) + s1 = bui.scrollwidget( + parent=w, + position=(52, 52), + color=var('bg'), + size=(150.0, 140.0) + ) + s.p1 = cw( + parent=s1, + background=False, + size=(150,0) + ) + s2 = bui.scrollwidget( + color=var('bg'), + parent=w, + position=(231.1, 52), + size=(150.0, 320.0) + ) + res = s.res() + p2 = cw( + parent=s2, + background=False, + size=(150,155*len(res)) + ) + tw( + parent=w, + position=(75.0, 22.4), + size=(100, 30), + text='Active', + h_align='center' + ) + tw( + parent=w, + position=(255.0, 22.4), + size=(100, 30), + text='What', + h_align='center' + ) + tw( + parent=w, + position=(425.0, 22.4), + size=(100, 30), + text='More', + h_align='center' + ) + s.more = [] + r1 = s.res(1) + for y in range(5): + for x in range(2): + _ = y*2+x + i,j = r1[_] + f = bs.CallPartial(s.pup,j) if _ else lambda: Collector(source=s.more[0],pipe=lambda t: wga(lambda: Bubble(s.n,t,s.n.color))) + s.more.append(bw( + size=(60,60), + position=(405+x*70,315-65*y), + parent=w, + icon=bui.gettexture(i), + oac=bs.CallPartial(s.nah,f), + iconscale=1.2 + )) + for _,g in enumerate(res): + t,x = g + tw( + parent=p2, + maxwidth=140, + position=(40,0+_*155), + text=t, + max_height=30, + h_align='center' + ) + bw( + parent=p2, + size=(90,90), + position=(22,18+20+_*155), + texture=bui.gettexture(x), + color=(1,1,1), + oac=bs.CallPartial(s.nah,bs.CallPartial(s.add,_)) + ) + def pup(s,j): + s.n.handlemessage(bs.PowerupMessage(j)) + bui.getsound('powerup01').play() + def add(s,_): + if _ in s.MEM.get(s.n,[]): btw('Already applied!'); return + bui.getsound('powerup01').play() + s.man(_) + s.check() + def man(s,_,a=True): + e = lambda z:_==z + o = s.MEM.get(s.n,[]) + [o.remove,o.append][a](_) + s.MEM[s.n] = o + if e(0): + v = [1.2,15][a] + t = '_punch_power_scale' + b = DLG(s.n) + h = bs.CallPartial(setattr,b,t,v) + h() + if not a: return + g = bs.CallPartial(getattr,b,t) + f = lambda: s.n and (_ in s.MEM[s.n]) and (h() if g() != v else 0,bs.timer(0.2,f)) + with bs.get_foreground_host_activity().context: f() + if e(1): + v = [400,0][a] + t = '_punch_cooldown' + b = DLG(s.n) + h = bs.CallPartial(setattr,b,t,v) + h() + if not a: return + g = bs.CallPartial(getattr,b,t) + f = lambda: s.n and (_ in s.MEM[s.n]) and (h() if g() != v else 0,bs.timer(0.2,f)) + with bs.get_foreground_host_activity().context: f() + if e(2): s.set(hockey=a) + if e(3): + if not a: return + f = lambda: s.n and (_ in s.MEM.get(s.n,[])) and (s.n.handlemessage('knockout',500),bs.timer(0.4,f)) + with bs.get_foreground_host_activity().context: f(); Bubble(time=4,node=s.n,color=s.n.color,text=random.choice(ZZZ())) + if e(4): s.set(invincible=a) + if e(5): s.set(shattered=a) + if e(6): s.set(frozen=a) + s.n.handlemessage('flash') + def rem(s,i): + if not i in s.MEM.get(s.n,[]): btw('Effect isn\'t applied... hmm?'); return + bui.getsound('pop01').play() + s.man(i,False) + s.check() + def check(s): + [_.delete() for _ in s.kids]; s.kids.clear() + res = [_[0] for _ in s.res()] + mem = s.MEM.get(s.n,[]) + for _,i in enumerate(mem): + t = tw( + parent=s.p1, + size=(150,30), + click_activate=True, + selectable=True, + position=(0,_*35), + text=res[i], + on_activate_call=bs.CallPartial(s.nah,bs.CallPartial(s.rem,i)) + ) + s.kids.append(t) + cw(s.p1,size=(150,len(mem)*35)) + def res(s,i=0): + return [[ + ('Super punch','achievementSuperPunch'), + ('Fast punch','nextLevelIcon'), + ('Super speed','achievementGotTheMoves'), + ('Good night','achievementOffYouGo'), + ('Invincible','star'), + ('Shatter','achievementCrossHair'), + ('Freeze','ouyaUButton') + ],[ + ('achievementOutline','none'), + ('powerupHealth','health'), + ('powerupCurse','curse'), + ('powerupStickyBombs','sticky_bombs'), + ('powerupImpactBombs','impact_bombs'), + ('powerupIceBombs','ice_bombs'), + ('powerupBomb','triple_bombs'), + ('powerupLandMines','land_mines'), + ('powerupShield','shield'), + ('powerupPunch','punch') + ]][i] + def sleep(s): + k = lambda: s.n and (s.n.handlemessage('knockout',1000),bs.timer(0.9,k)) + with bs.get_foreground_host_activity().context: k() + def set(s,obj=False,**k): + n = s.n.getdelegate(object) if obj else s.n + [setattr(n,a,v) for a,v in k.items()] + def pick(s): + SpazPicker(source=s.b,pipe=s.fresh,deny=[s.n],deny_msg="Already picked!") + def fresh(s,n): + gun() + s.n = n + ui = NTEX(n) + tw(s.t,text=getattr(n,'name',0) or 'Unnamed',color=ui[1]) + bw(s.b,**ui[0],mask_texture=bui.gettexture('characterIconMask')) + s.check() + def nah(s,f): + if s.n is None: btw('No target selected!'); return 1 + elif (getattr(s.n,'exists',lambda:False)() and getattr(s.n.getdelegate(object),'_dead',True)) or (not s.n.exists()): btw('Selected node is dead!\nPick a new target'); return 1 + f(); return True + +@NEW +class Deploy: + """Obtain any object or powerup in game""" + @classmethod + def get(c): + with bs.get_foreground_host_activity().context: + f = bs_bomb.BombFactory.get() + o = bs_gameutils.SharedObjects.get() + return { + ('Safe Bomb','spinner0'):[ + 'bomb', + { + 'mesh':f.bomb_mesh, + 'color_texture':f.regular_tex, + 'shadow_size':0.5, + 'materials':[ + o.object_material, + o.footing_material, + f.bomb_material + ] + } + ], + ('Safe TNT','tnt'):[ + 'prop', + { + 'mesh':f.tnt_mesh, + 'body':'crate', + 'color_texture':f.tnt_tex, + 'shadow_size':0.5, + 'materials':[ + o.object_material, + o.footing_material, + f.bomb_material + ] + } + ], + ('Light Ball','nub'):[ + 'prop', + { + 'mesh':f.bomb_mesh, + 'body':'sphere', + 'color_texture':GA(lambda:bs.gettexture('white')), + 'shadow_size':0.6, + 'gravity_scale':0.5, + 'materials':[ + o.object_material, + o.footing_material, + f.bomb_material + ] + } + ], + ('Gold Coin','coin'):[ + 'prop', + { + 'mesh':GA(lambda:bs.getmesh('puck')), + 'body':'puck', + 'color_texture':GA(lambda:bs.gettexture('tokens4')), + 'reflection':'sharper', + 'reflection_scale':[5,5,5], + 'materials':[ + o.object_material, + o.footing_material + ] + } + ] + } + def __init__(s,w,*a): + s.w = w + s.data = {'attrs':{}} + s.atkids = [] + s.edbs = [] + s.pos = [] + ex = a[4] + prp1 = bui.scrollwidget( + parent=w, + position=(58.8, 59.5), + color=var('bg'), + size=(150.0, 310.0) + ) + mem = s.__class__.get() + py = 150*len(mem) + s.prp = cw( + parent=prp1, + background=False, + size=(150,py) + ) + s.vc = ex[2] if ex else 0 + for _,g in enumerate(mem.items()): + tx,at = g + y = py-150-150*_ + b = bw( + parent=s.prp, + size=(100,100), + texture=bui.gettexture(tx[1]), + position=(15,y+50), + color=(1,1,1), + oac=bs.CallPartial(s.load,at,_) + ) + t = tw( + parent=s.prp, + maxwidth=140, + text=tx[0], + position=(40,y+15), + h_align='center', + v_align='center' + ) + if _ == s.vc: cw(s.prp,visible_child=t) + tw( + parent=w, + position=(82.0, 26.7), + size=(100, 30), + text='Preset', + h_align='center' + ) + v = 'dplprop' + e = var(v) + s.propt = ctw( + parent=w, + position=(299.4, 335.2), + size=(260.0, 30), + text=e, + conf=v, + v_align='center', + allow=True, + hint='prop', + blank=True + ) + v = 'dplname' + e = var(v) + s.namet = ctw( + parent=w, + position=(309.4, 260.2), + size=(250.0, 30), + allow=True, + hint='None', + text=e, + conf=v, + blank=True + ) + tw( + parent=w, + position=(220.4, 298.9), + size=(100, 30), + text='owner =' + ) + s.ownert = tw( + parent=w, + position=(316.4, 298.9), + size=(150.0, 30), + text='None', + color=(0.5,0.5,0.5), + maxwidth=150 + ) + tw( + parent=w, + position=(220.4, 261.9), + size=(90.0, 30), + text='name =' + ) + tw( + parent=w, + position=(220.4, 224.9), + size=(70.0, 30), + text='pos =' + ) + tw( + parent=w, + position=(220.4, 335.9), + size=(70.0, 30), + text='type =' + ) + s.pickb = bw( + size=(82.0, 20.0), + position=(477.3, 302.1), + label='Pick', + parent=w, + oac=s.pick + ) + for _ in range(3): + h = ['X','Y','Z'][_] + s.pos.append(ctw( + parent=w, + position=(293.3, 222.9-37*_), + size=(270.0, 30), + hint=h, + conf=f'dpl{_}', + allow='-0.1223456789', + text=var(f'dpl{_}') + )) + bw( + size=(60.0, 60.0), + position=(217.1, 153.8), + parent=w, + icon=bui.gettexture('cursor'), + iconscale=1.2, + oac=s.map + ) + atp1 = bui.scrollwidget( + parent=w, + position=(218.9, 60.2), + color=var('bg'), + size=(260.0, 80.0) + ) + s.atp = cw( + parent=atp1, + background=False + ) + bw( + size=(70.0, 82.0), + position=(492.4, 59.6), + parent=w, + iconscale=1.55, + icon=bui.gettexture('downButton'), + oac=s.make + ) + tw( + parent=w, + position=(298.3, 29.4), + size=(100, 30), + text='Attrs', + h_align='center' + ) + tw( + parent=w, + position=(477, 29.4), + size=(100, 30), + text='Make', + h_align='center' + ) + # finally + s.fattr() + s.chk(ex) + def load(s,at,vc): + s.vc = vc + ty,da = at + s.propt.set_text(ty) + s.data['attrs'] = da + s.fattr() + def chk(s,ex): + if not ex: return + p,da,_ = ex + if p and p != s.getpos(): + bui.apptimer(0.2, lambda:s.setpos(p)) + s.data = da + s.fattr() + s.fresh() + def make(s): + p = s.getpos() + d = s.data + a = d['attrs'].copy() + if not a: + btw('No attrs!') + return + a.update({'position':p}) + with bs.get_foreground_host_activity().context: + try: + bs.newnode( + type=var('dplprop') or 'prop', + owner=d.get('owner',None), + name=var('dplname'), + attrs=a + ) + except Exception as e: + err(str(e)) + SND('spawn',p) + def _fattr(s,t): + a,v = t + if not a: + bs.broadcastmessage('Cancelled!') + return + if a in s.data['attrs']: + bs.broadcastmessage('Updated existing attribute!') + s.data['attrs'][a] = v + s.fattr() + def fattr(s): + i = len(s.data['attrs']) + cw(s.atp,size=(260,i*70+35)) + [_.delete() for _ in s.atkids] + s.atkids.clear() + s.edbs.clear() + # attrs + for _,g in enumerate(s.data['attrs'].items()): + a,v = g + y = 35+_*70 + s.atkids.append(tw( + parent=s.atp, + text=a, + v_align='center', + maxwidth=210, + position=(5,y+35) + )) + dv = f"'{v}'" if isinstance(v,str) else str(v) + edb = bw( + position=(200,y+35), + label='!', + parent=s.atp, + oac=bs.CallPartial(s.eattr,a,dv,old=(a,v),_=_), + size=(30,30) + ) + s.atkids.append(edb) + s.edbs.append(edb) + s.atkids.append(tw( + parent=s.atp, + text=dv, + v_align='center', + maxwidth=190, + position=(5,y+4), + color=(0.7,0.7,0.7) + )) + s.atkids.append(bw( + position=(200,y+4), + size=(30,26), + label='-', + parent=s.atp, + oac=bs.CallPartial(s.dattr,a) + )) + s.atkids.append(tw( + text='Add an attribute', + position=(5,0), + parent=s.atp, + v_align='center', + maxwidth=210 + )) + s.aattrb = bw( + label='+', + position=(200,0), + size=(30,30), + parent=s.atp, + oac=s.aattr + ) + s.atkids.append(s.aattrb) + def dattr(s,a): + s.data['attrs'].pop(a) + bui.getsound('pop01').play() + s.fattr() + def eattr(s,a,dv,old,_): + Collector( + source=s.edbs[_], + first=dv, + double=a, + two=True, + title='Edit', + pipe=bs.CallPartial(s._eattr,old=old), + raw=True + ) + def _eattr(s,t,old): + a,v = t + s.data['attrs'].pop(old[0]) + s.data['attrs'][a or old[0]] = v if v != "" else old[1] + s.fattr() + def aattr(s): + Collector( + source=s.aattrb, + first='Value', + double='Attribute', + two=True, + title='Add', + pipe=s._fattr, + raw=True + ) + def pick(s): + NodePicker( + source=s.pickb, + pipe=s.fresh, + allow='3D' + ) + def fresh(s,n=None): + if n: s.data['owner'] = n + else: n = s.data.get('owner',None) + if not n: return + t = str(n)[1:][:-1] + t = t[t.find('Node ')+5:t.find("'")]+n.getnodetype() + tw(s.ownert,text=t,color=(1,1,1)) + def map(s): + s.mapper = Mapper(pipe=s.mup,pos=s.getpos()) + None if s.mapper.tired else cw(s.w,transition='out_right') + def mup(s,p=None): + Coolbox(fb=s.__class__.__name__, fake=True, extra=(p or s.getpos(),s.data,s.vc)) + def getpos(s): + return rnd(tuple([float(var(f'dpl{i}')) for i in range(3)])) + def setpos(s,p): + p = rnd(p) + h = [] + for i in range(3): + t = s.pos[i] + o = t.get_text() + n = str(p[i]) + h.append(t) if o != n else None + t.set_text(n) + bui.apptimer(0.2,lambda:([t.blink() for t in h],gun())) if h else None + +@NEW +class Listen: + """Music player and manager""" + + def __init__(s, *a): + w = s.w = a[0] + s.sl = None + + s.m_types = sorted([m for m in dir(bs.MusicType) if not m.startswith('_') and m.isupper()]) + s.M_NAME = [" ".join(word.capitalize() for word in m.split("_")) for m in s.m_types] + + s.tex_map = { + "CHAR_SELECT": "neoSpazIcon", "CHOSEN_ONE": "achievementSuperPunch", + "EPIC": "tipTopPreview", "EPIC_RACE": "bigGPreview", "FLAG_CATCHER": "bridgitPreview", + "FLYING": "alwaysLandPreview", "FOOTBALL": "achievementFootballVictory", + "FORWARD_MARCH": "cragCastlePreview", "GRAND_ROMP": "achievementFlawlessVictory", + "HOCKEY": "hockeyStadiumPreview", "KEEP_AWAY": "thePadPreview", + "MARCHING": "achievementRunaround", "MENU": "logo", + "ONSLAUGHT": "doomShroomPreview", "RACE": "lakeFrigidPreview", + "RUN_AWAY": "monkeyFacePreview", "SCARY": "powerupCurse", + "SCORES": "achievementFootballShutout", "SPORTS": "footballStadiumPreview", + "SURVIVAL": "rampagePreview", "TO_THE_DEATH": "achievementOnslaught", + "VICTORY": "achievementMedalLarge" + } + + sv = bui.scrollwidget( + parent=w, + color=var('bg'), + position=(40, 50), + size=(250, 320) + ) + + count = len(s.M_NAME) + ss = max(320, count * 35) + s.cv = cw( + parent=sv, + background=False, + size=(230, ss) + ) + + s.texts = [] + for i in range(count): + p_y = ss - 35 - (i * 35) + t = tw( + p=s.cv, + text=s.M_NAME[i], + pos=(10, p_y), + size=(210, 30), + maxwidth=200, + v_align='center', + selectable=True, + click_activate=True, + on_activate_call=bs.CallPartial(s.select, i) + ) + s.texts.append(t) + + c_x = 440 + + s.img = bui.imagewidget( + parent=w, + position=(c_x - 60, 220), + size=(120, 120), + texture=bui.gettexture('audioIcon'), + mask_texture=bui.gettexture('characterIconMask'), + color=(1, 1, 1) + ) + + s.t_name = tw( + p=w, + pos=(c_x - 100, 180), + size=(200, 30), + text='Select a Track', + h_align='center', + v_align='center', + maxwidth=200, + color=var('t') or (1, 1, 1), + scale=1.2 + ) + + s.t_desc = tw( + p=w, + pos=(c_x - 100, 150), + size=(200, 30), + text='...', + h_align='center', + v_align='center', + maxwidth=200, + color=(0.5, 0.5, 0.5), + scale=0.8 + ) + + s.b_play = bw( + p=w, + pos=(c_x - 110, 80), + size=(100, 45), + label='Play', + icon=bui.gettexture('startButton'), + oac=s.play + ) + + s.b_def = bw( + p=w, + pos=(c_x + 10, 80), + size=(100, 45), + label='Default', + icon=bui.gettexture('replayIcon'), + oac=s.play_def + ) + + tw( + p=w, + pos=(c_x - 125, 40), + size=(250, 30), + text='* Non-hosts will hear tracks locally.', + h_align='center', + v_align='center', + maxwidth=250, + color=(1, 1, 1, 0.3), + scale=0.6 + ) + + if count > 0: + s.select(0) + + def select(s, i): + if s.sl != i and s.sl is not None: + bui.getsound('tap').play() + s.sl = i + + for idx, t in enumerate(s.texts): + tw(t, color=(0, 1, 0) if idx == i else var('t') or (1, 1, 1)) + + tw(s.t_name, text=s.M_NAME[i]) + m_val = s.m_types[i] + tw(s.t_desc, text=f"MusicType.{m_val}") + + tex_name = s.tex_map.get(m_val, 'audioIcon') + try: + tex = bui.gettexture(tex_name) + except: + tex = bui.gettexture('audioIcon') + bui.imagewidget(s.img, texture=tex) + + def play(s): + if s.sl is None: + btw('Select a track to play!') + return + + enum_name = s.m_types[s.sl] + + try: + m_type = getattr(bs.MusicType, enum_name) + with bs.get_foreground_host_activity().context: + bs.setmusic(m_type) + push(f"Now Playing: {s.M_NAME[s.sl]}", color=(0, 1, 0)) + ding() + except Exception as e: + err(f"Failed to play track: {e}") + + def play_def(s): + try: + with bs.get_foreground_host_activity().context: + def_music = bs.get_foreground_host_activity().default_music + bs.setmusic(def_music) + push("Restored default music!", color=(0, 1, 0)) + ding() + except Exception as e: + err("Failed to restore default music.") + +@NEW +class Tune: + """The nice settings window""" + + def __init__(s, *a): + w = s.w = a[0] + + sv = bui.scrollwidget( + parent=w, + position=(50, 50), + color=var('bg'), + size=(500, 320) + ) + + items = [ + ('npp', 'Auto-Pause', 'Automatically pause the match when Coolbox opens.'), + ('icall', 'Fixed Width Logs', 'Use fixed-width fonts for CallPartial logs to align text.'), + ('spa', 'Autoplay Sounds', 'Automatically play sounds when scrolling through the Sound Picker.'), + ('cconf0', 'Control: Block Host', 'Block your normal player inputs when remote Control starts.'), + ('cconf1', 'Control: Auto-Close', 'Close the Coolbox UI automatically when Control starts.'), + ('cconf2', 'Control: Invincibility', 'Make your original spaz invincible while controlling a bot.'), + ('cconf3', 'Control: Intercept', 'Prevent the remote bot from performing its original AI actions.') + ] + + count = len(items) + ss = max(320, count * 60 + 20) + + s.cv = cw( + parent=sv, + background=False, + size=(480, ss) + ) + + for i, (key, title, desc) in enumerate(items): + p_y = ss - 60 - (i * 60) + + chk( + parent=s.cv, + position=(20, p_y + 25), + size=(200, 30), + value=bool(var(key)), + text=title, + on_value_change_call=bs.CallPartial(s.toggle, key) + ) + + tw( + p=s.cv, + position=(55, p_y + 5), + text=desc, + color=(0.5, 0.5, 0.5), + scale=0.65, + maxwidth=400 + ) + + def toggle(s, key, val): + var(key, val) + if key == 'npp': + npause(1 if val else 0) + bui.getsound('click01').play() + +@NEW +class Quick: + + _disco_timer = None + _disco_on = False + + def __init__(s, *a): + w = s.w = a[0] + + tw( + p=w, pos=(40, 310), size=(220, 30), text='World Overrides', + h_align='center', v_align='center', scale=1.2, color=(0.1, 0.7, 1) + ) + tw( + p=w, pos=(320, 310), size=(250, 30), text='Mass Modifiers', + h_align='center', v_align='center', scale=1.2, color=(0.1, 0.7, 1) + ) + + chk( + parent=w, position=(40, 250), size=(200, 30), + value=bool(var('epic_mode')), text='Epic Slow-Motion', + on_value_change_call=s.epic + ) + + chk( + parent=w, position=(40, 185), size=(200, 30), + value=bool(type(s)._disco_on), text='Disco Lighting', + on_value_change_call=s.disco + ) + + bw( + p=w, pos=(40, 105), size=(220, 55), + label='Meteor Shower', icon=bui.gettexture('powerupBomb'), oac=s.meteors + ) + + bw( + p=w, pos=(40, 40), size=(220, 55), + label='Earthquake', icon=bui.gettexture('achievementOnslaught'), oac=s.quake + ) + + b_list = [ + ('Shield All', 'powerupShield', 'shield', (320, 235)), + ('Glove All', 'powerupPunch', 'punch', (450, 235)), + ('Heal All', 'powerupHealth', 'health', (320, 170)), + ('Curse All', 'powerupCurse', 'curse', (450, 170)), + ('Ice Rink', 'powerupIceBombs', 'ice', (320, 105)), + ('Sleep All', 'achievementOffYouGo', 'sleep', (450, 105)), + ('Shatter All', 'achievementCrossHair', 'shatter', (320, 40)), + ('God Mode', 'star', 'god', (450, 40)) + ] + + for lbl, ic, act, p in b_list: + bw(p=w, pos=p, size=(120, 55), label=lbl, icon=bui.gettexture(ic), oac=bs.CallPartial(s.mass, act)) + + def epic(s, v): + var('epic_mode', v) + with bs.get_foreground_host_activity().context: + bs.get_foreground_host_activity().globalsnode.slow_motion = v + bui.getsound('click01').play() + + def disco(s, v): + type(s)._disco_on = v + bui.getsound('click01').play() + if v: + s.__class__._disco_timer = bui.AppTimer(0.3, s.step_disco, repeat=True) + else: + s.__class__._disco_timer = None + with bs.get_foreground_host_activity().context: + bs.get_foreground_host_activity().globalsnode.tint = (1.3, 1.2, 1) + + def step_disco(s): + c = (random.random(), random.random(), random.random()) + with bs.get_foreground_host_activity().context: + bs.get_foreground_host_activity().globalsnode.tint = c + + def meteors(s): + def drop(): + if not s.w.exists(): + return + p = (random.uniform(-9, 9), 12, random.uniform(-6, 6)) + with bs.get_foreground_host_activity().context: + bs_bomb.Bomb(position=p, bomb_type='normal').autoretain() + + for i in range(25): + bui.apptimer(i * 0.15, drop) + + push("Meteors Incoming!", color=(1, 0.5, 0)) + bui.getsound('shieldDown').play() + + def quake(s): + with bs.get_foreground_host_activity().context: + bs.camerashake(10) + push("Earthquake!", color=(0.8, 0.4, 0)) + bui.getsound('block').play() + + def mass(s, act): + with bs.get_foreground_host_activity().context: + for p in bs.get_foreground_host_activity().players: + a = p.actor + if not a or not a.node.exists(): + continue + if act in ('shield', 'punch', 'health', 'curse'): + a.node.handlemessage(bs.PowerupMessage(act)) + elif act == 'ice': + a.node.hockey = not a.node.hockey + elif act == 'sleep': + a.node.handlemessage('knockout', 4000) + elif act == 'shatter': + a.shatter(True) + elif act == 'god': + a.node.invincible = True + a._punch_cooldown = 0 + a._punch_power_scale = 10 + ding() + push(f"Applied {act}!", color=(0, 1, 0)) + +@NEW +class Tweak: + + def __init__(s, *a): + w = s.w = a[0] + + btn_defs = [ + ('Globals Node', 'settingsIcon', s.glob_node), + ('Map Node', 'star', s.map_node), + ('Disco Nodes', 'achievementGotTheMoves', s.disco_nodes), + + ('Pick & Bubble', 'achievementOutline', s.pick_bubble), + ('Pick & Smite', 'powerupBomb', s.pick_smite), + ('Pick & Teleport', 'achievementOffYouGo', s.pick_tp), + + ('Find by Name', 'textClearButton', s.find_name), + ('Find by Type', 'logo', s.find_type), + ('Reset Camera', 'replayIcon', s.res_cam), + + ('Clear Orphans', 'crossOut', s.clear_orphans), + ('Draw Line', 'graphicsIcon', s.draw_line), + ('Map Coord', 'cursor', s.map_coord), + + ('Moon Gravity', 'upButton', s.moon_grav), + ('Shiny World', 'achievementFlawlessVictory', s.shiny_world), + ('RGB Cycle', 'powerupShield', s.rgb_cycle) + ] + + for i, (lbl, ic, oac) in enumerate(btn_defs): + col = i % 3 + row = i // 3 + bx = 45 + col * 175 + by = 315 - row * 65 + + b = bw( + p=w, pos=(bx, by), size=(160, 55), + label=lbl, icon=bui.gettexture(ic), + text_scale=1.0, iconscale=1.0 + ) + bw(b, oac=bs.CallPartial(oac, b)) + + def glob_node(s, b): + NodeManager(node=bs.get_foreground_host_activity().globalsnode, source=b) + + def map_node(s, b): + try: + NodeManager(node=bs.get_foreground_host_activity().map.node, source=b) + except Exception: + btw('Map has no main node!') + + def disco_nodes(s, b): + c = 0 + with bs.get_foreground_host_activity().context: + for n in bs.getnodes(): + if hasattr(n, 'color') and n.getnodetype() != 'globals': + try: + n.color = (random.random(), random.random(), random.random()) + c += 1 + except Exception: + pass + ding() + push(f'Randomized colors for {c} nodes!', color=(0, 1, 0)) + + def pick_bubble(s, b): + NodePicker(source=b, allow='3D', pipe=bs.CallPartial(s._bubble_prompt, b)) + + def _bubble_prompt(s, b, n): + Collector(source=b, pipe=bs.CallPartial(s._apply_bubble, n), first='Message', title='Bubble Text') + + def _apply_bubble(s, n, txt): + if not n.exists(): + return + with bs.get_foreground_host_activity().context: + Bubble(node=n, text=txt, time=10, color=(1, 1, 0)) + ding() + + def pick_smite(s, b): + NodePicker(source=b, allow='3D', pipe=s._smite) + + def _smite(s, n): + if not n.exists(): + return + p = n.position + with bs.get_foreground_host_activity().context: + bmb = bs_bomb.Bomb(position=p, bomb_type='tnt').autoretain() + bs.timer(0.1, bmb.explode) + ding() + + def pick_tp(s, b): + NodePicker(source=b, allow='3D', pipe=s._tp) + + def _tp(s, n): + try: + getme(1).node.handlemessage(bs.StandMessage(n.position, 0)) + ding() + except Exception as e: + err(f"Teleport failed: {e}") + + def find_name(s, b): + Collector(source=b, pipe=bs.CallPartial(s._f_name, b), first='Node name', title='Find by Name') + + def _f_name(s, b, t): + with bs.get_foreground_host_activity().context: + for n in bs.getnodes(): + if getattr(n, 'name', '') == t: + NodeManager(node=n, source=b) + return + btw('Node not found!') + + def find_type(s, b): + Collector(source=b, pipe=bs.CallPartial(s._f_type, b), first='Node type', title='Find by Type') + + def _f_type(s, b, t): + with bs.get_foreground_host_activity().context: + for n in bs.getnodes(): + if n.getnodetype() == t: + NodeManager(node=n, source=b) + return + btw('Node not found!') + + def res_cam(s, b): + _ba.set_camera_manual(False) + ding() + push('Camera Reset!', color=(0, 1, 0)) + + def clear_orphans(s, b): + c = 0 + with bs.get_foreground_host_activity().context: + for n in bs.getnodes(): + if n.getnodetype() in ('prop', 'light', 'math', 'text') and not n.getdelegate(object): + n.delete() + c += 1 + push(f'Cleared {c} orphaned nodes!', color=(1, 0.5, 0)) + bui.getsound('shieldDown').play() + + def draw_line(s, b): + NodePicker(source=b, allow='3D', pipe=lambda n1: NodePicker(source=b, allow='3D', pipe=lambda n2: s._draw(n1, n2))) + + def _draw(s, n1, n2, count=30): + if count <= 0 or not n1.exists() or not n2.exists(): + return + + p1 = n1.position + p2 = n2.position + d = math.dist(p1, p2) + steps = max(2, int(d * 5)) + + with bs.get_foreground_host_activity().context: + for i in range(steps): + t = i / (steps - 1) + p = ( + p1[0] + t * (p2[0] - p1[0]), + p1[1] + t * (p2[1] - p1[1]), + p1[2] + t * (p2[2] - p1[2]) + ) + bs.emitfx(position=p, chunk_type='spark', count=1, scale=0.5) + + if count == 30: + ding() + + bui.apptimer(0.1, lambda: s._draw(n1, n2, count - 1)) + + def map_coord(s, b): + NodePicker(source=b, allow='3D', pipe=s._map) + + def _map(s, n): + s.mapper = Mapper(pipe=lambda p2: Coolbox(fake=True, extra=p2), pos=n.position) + cw(s.w, transition='out_right') + + def moon_grav(s, b): + v = var('t_moongrav') + nv = 0.2 if not v else 1.0 + var('t_moongrav', not v) + c = 0 + with bs.get_foreground_host_activity().context: + for n in bs.getnodes(): + if hasattr(n, 'gravity_scale'): + try: + n.gravity_scale = nv + c += 1 + except Exception: + pass + ding() + push(f'Moon Gravity {"On" if not v else "Off"} ({c} nodes)!', color=(0.5, 0.5, 1)) + + def shiny_world(s, b): + v = var('t_shiny') + nr = [3.0] if not v else [1.0] + nrf = 'soft' if not v else 'powerup' + var('t_shiny', not v) + c = 0 + with bs.get_foreground_host_activity().context: + for n in bs.getnodes(): + if hasattr(n, 'reflection_scale'): + try: + n.reflection_scale = nr + n.reflection = nrf + c += 1 + except Exception: + pass + ding() + push(f'Shiny World {"On" if not v else "Off"} ({c} nodes)!', color=(1, 1, 0)) + + def rgb_cycle(s, b): + global _rgb_cycle_combines + v = var('t_rgbcycle') + if not v: + c = 0 + with bs.get_foreground_host_activity().context: + gnode = bs.get_foreground_host_activity().globalsnode + keys = {0: (1, 0, 0), 1: (0, 1, 0), 2: (0, 0, 1), 3: (1, 0, 0)} + items = sorted(keys.items()) + for n in bs.getnodes(): + if hasattr(n, 'color') and n.getnodetype() != 'globals': + try: + combine = bs.newnode('combine', owner=n, attrs={'size': 3}) + for i in range(3): + curve = bs.newnode('animcurve', owner=n) + gnode.connectattr('time', curve, 'in') + curve.times = [int(1000 * t) for t, _ in items] + curve.values = [val[i] for _, val in items] + curve.loop = True + curve.connectattr('out', combine, f'input{i}') + combine.connectattr('output', n, 'color') + _rgb_cycle_combines.append(combine) + c += 1 + except Exception: + pass + var('t_rgbcycle', True) + ding() + push(f'RGB Cycle On ({c} nodes)!', color=(0, 1, 0)) + else: + with bs.get_foreground_host_activity().context: + for combine in _rgb_cycle_combines: + try: + if combine.exists(): combine.delete() + except Exception: + pass + _rgb_cycle_combines = [] + var('t_rgbcycle', False) + ding() + push('RGB Cycle Off!', color=(1, 1, 0)) + +@NEW +class Gather: + """Modify party and team based stuff""" + + def __init__(s, *a): + w = s.w = a[0] + s.sl_t = None + + # Left Panel: Team List + s.tsv = bui.scrollwidget(parent=w, color=var('bg'), position=(30, 65), size=(180, 305)) + s.tcv = cw(parent=s.tsv, background=False) + + # Add Team Button + bw( + p=w, pos=(30, 20), size=(180, 40), + label='Create Team', icon=bui.gettexture('achievementTeamPlayer'), + oac=s.add_team + ) + + # Right Panel: Team & Player Details + s.dsv = bui.scrollwidget(parent=w, color=var('bg'), position=(220, 20), size=(350, 350)) + s.dcv = cw(parent=s.dsv, background=False) + + s.refresh() + s.spy() + + def spy(s): + if not s.w.exists(): return + try: + teams = bs.get_foreground_host_activity().teams + c_t = len(teams) + c_p = sum(len(t.players) for t in teams) + if c_t != getattr(s, 'last_t', -1) or c_p != getattr(s, 'last_p', -1): + s.last_t = c_t + s.last_p = c_p + s.refresh() + except: + pass + bui.apptimer(1.0, s.spy) + + def add_team(s): + Collector(source=s.w, pipe=s._add_team, first='Team Name', title='Create Team') + + def _add_team(s, name): + tid = var('g_tid') or 2 + try: + c = (random.random(), random.random(), random.random()) + t = bs.SessionTeam(team_id=tid, name=name, color=c) + bs.get_foreground_host_activity().add_team(t) + var('g_tid', tid + 1) + ding() + s.refresh() + except Exception as e: + err(f"Failed: {e}") + + def refresh(s): + if not s.w.exists(): return + [k.delete() for k in s.tcv.get_children()] + try: + teams = bs.get_foreground_host_activity().teams + except: + return + + ss = max(305, len(teams) * 45) + cw(s.tcv, size=(160, ss)) + + valid_sl = False + for i, t in enumerate(teams): + if t == s.sl_t: valid_sl = True + y = ss - 40 - (i * 45) + n = t.name.evaluate() if hasattr(t.name, 'evaluate') else t.name + sl_color = tuple(min(1.0, ch * 1.4 + 0.15) for ch in t.color) if t == s.sl_t else t.color + b = bw( + p=s.tcv, pos=(5, y), size=(150, 35), label=n, color=sl_color, + textcolor=var('t'), + oac=bs.CallPartial(s.show_team, t) + ) + + if not valid_sl: s.sl_t = None + s.show_team(s.sl_t) + + def show_team(s, t): + s.sl_t = t + [k.delete() for k in s.dcv.get_children()] + if not t: return + + players = t.players + count = len(players) + ss = max(350, 180 + count * 45) + cw(s.dcv, size=(330, ss)) + + n = t.name.evaluate() if hasattr(t.name, 'evaluate') else t.name + + tw(p=s.dcv, pos=(10, ss - 40), size=(310, 30), text=n, color=t.color, scale=1.2, h_align='center', v_align='center') + + bw(p=s.dcv, pos=(10, ss - 85), size=(100, 35), label='Rename', oac=bs.CallPartial(s.rename_team, t)) + + c_btn = bw(p=s.dcv, pos=(115, ss - 85), size=(100, 35), label='Color', color=t.color) + var('t_col', t.color) + bw(c_btn, oac=bs.CallPartial(ColorPicker, in_source=c_btn, id='t_col', what='t_col', id2='t_col', chr='char', on_back=bs.CallPartial(s.recolor_team, t), mode=2)) + + bw(p=s.dcv, pos=(220, ss - 85), size=(100, 35), label='Nuke', icon=bui.gettexture('crossOut'), oac=bs.CallPartial(s.nuke_team, t)) + + bw(p=s.dcv, pos=(10, ss - 130), size=(100, 35), label='Gather', icon=bui.gettexture('achievementTeamPlayer'), oac=bs.CallPartial(s.gather_team, t)) + bw(p=s.dcv, pos=(115, ss - 130), size=(100, 35), label='Smite', icon=bui.gettexture('achievementOffYouGo'), oac=bs.CallPartial(s.smite_team, t)) + bw(p=s.dcv, pos=(220, ss - 130), size=(100, 35), label='Heal', icon=bui.gettexture('powerupHealth'), oac=bs.CallPartial(s.heal_team, t)) + + tw(p=s.dcv, pos=(10, ss - 170), text=f'Players ({count}):', color=(0.5, 0.5, 0.5), scale=0.8) + + for i, p in enumerate(players): + y = ss - 210 - (i * 45) + p_name = p.getname() + + tw(p=s.dcv, pos=(10, y + 5), text=p_name, maxwidth=150, color=getattr(p, 'color', (1,1,1))) + + bw(p=s.dcv, pos=(170, y), size=(70, 35), label='Fetch', oac=bs.CallPartial(s.fetch_player, p)) + bw(p=s.dcv, pos=(245, y), size=(70, 35), label='Goto', oac=bs.CallPartial(s.goto_player, p)) + + def _refresh_scoreboard(s, t): + """Force the current game's Scoreboard actor (if any) to rebuild + this team's entry, since it bakes name/color in once at creation + and never updates them live otherwise.""" + try: + act = bs.get_foreground_host_activity() + sb = getattr(act, '_scoreboard', None) or getattr(act, 'scoreboard', None) + entries = getattr(sb, '_entries', None) + if sb is not None and entries is not None and t.id in entries: + sb.remove_team(t.id) + sb._add_team(t) + except Exception: + pass + + def rename_team(s, t): + Collector(source=s.w, pipe=bs.CallPartial(s._rename_team, t), first='New Name', title='Rename Team') + + def _rename_team(s, t, name): + try: + with bs.get_foreground_host_activity().context: + t.name = name + try: + t.sessionteam.name = name + except Exception: + pass + s._refresh_scoreboard(t) + ding() + s.refresh() + except Exception as e: + err(f'Failed to rename team: {e}') + + def recolor_team(s, t): + try: + c = var('t_col') + with bs.get_foreground_host_activity().context: + t.color = c + try: + t.sessionteam.color = c + except Exception: + pass + # Update everyone currently on the team: their cached + # color (so respawns pick it up) and their live spaz + # node color (so it changes instantly if they're alive). + for p in t.players: + p.color = c + try: + p.sessionplayer.color = c + except Exception: + pass + try: + if p.actor and p.actor.node.exists(): + p.actor.node.color = c + p.actor.node.name_color = bs.safecolor(c, target_intensity=0.75) + except Exception: + pass + s._refresh_scoreboard(t) + s.refresh() + except Exception as e: + err(f'Failed to recolor team: {e}') + + def nuke_team(s, t): + try: + bs.get_foreground_host_activity().remove_team(t.sessionteam) + s.sl_t = None + ding() + s.refresh() + except Exception as e: + err(f"Failed: {e}") + + def gather_team(s, t): + me = getme(1) + if not me or not me.node.exists(): return + pos = me.node.position + for p in t.players: + if p.actor and p.actor.node.exists(): + p.actor.node.handlemessage(bs.StandMessage(pos, 0)) + ding() + push('Team gathered!') + + def smite_team(s, t): + for p in t.players: + if p.actor and p.actor.node.exists(): + p.actor.node.handlemessage(bs.DieMessage()) + ding() + + def heal_team(s, t): + for p in t.players: + if p.actor and p.actor.node.exists(): + p.actor.node.handlemessage(bs.PowerupMessage('health')) + ding() + + def fetch_player(s, p): + me = getme(1) + if not me or not me.node.exists(): return + if p.actor and p.actor.node.exists(): + p.actor.node.handlemessage(bs.StandMessage(me.node.position, 0)) + ding() + + def goto_player(s, p): + me = getme(1) + if not me or not me.node.exists(): return + if p.actor and p.actor.node.exists(): + me.node.handlemessage(bs.StandMessage(p.actor.node.position, 0)) + ding() + +@NEW +class Load: + + def __init__(s, *a): + w = s.w = a[0] + s.sl = None + + s.presets = [ + # === SPAWN === + { + 'n': 'Kronk Buddy', + 'd': 'A friendly blue Kronk ready to help you win.', + 't': 'Spawn', + 'i': 'kronkIcon', + 'v': {'char': 'Kronk', 'tchar': 'Kronk Buddy', 'Main': (0, 1, 1), 'Highlight': (0, 0, 1), 'Name': (0, 0.5, 1)} + }, + { + 'n': 'Golden God', + 'd': 'A shiny golden skeleton. Wealthy and terrifying.', + 't': 'Spawn', + 'i': 'bonesIcon', + 'v': {'char': 'Bones', 'tchar': 'Golden God', 'Main': (1, 0.8, 0), 'Highlight': (1, 1, 0), 'Name': (1, 0.9, 0.2)} + }, + { + 'n': 'Shadow Ninja', + 'd': 'Silent, deadly, and extremely edgy.', + 't': 'Spawn', + 'i': 'ninjaIcon', + 'v': {'char': 'Snake Shadow', 'tchar': 'Shadow Ninja', 'Main': (0.1, 0.1, 0.1), 'Highlight': (1, 0, 0), 'Name': (1, 0, 0)} + }, + { + 'n': 'Cyborg Unit', + 'd': 'Cold steel. No feelings. Probably.', + 't': 'Spawn', + 'i': 'cyborgIcon', + 'v': {'char': 'Cyborg', 'tchar': 'Cyborg Unit', 'Main': (0.4, 0.4, 0.5), 'Highlight': (0, 0.8, 1), 'Name': (0.5, 0.9, 1)} + }, + { + 'n': 'Frosty', + 'd': 'A chilly snowman. Cool as ice.', + 't': 'Spawn', + 'i': 'frostyIcon', + 'v': {'char': 'Frosty', 'tchar': 'Frosty', 'Main': (0.5, 0.8, 1), 'Highlight': (1, 1, 1), 'Name': (0.7, 0.95, 1)} + }, + { + 'n': 'Santa', + 'd': 'The big man himself. Ho ho ho.', + 't': 'Spawn', + 'i': 'santaIcon', + 'v': {'char': 'Santa Claus', 'tchar': 'Santa', 'Main': (0.9, 0.1, 0.1), 'Highlight': (1, 1, 1), 'Name': (1, 0.8, 0.8)} + }, + { + 'n': 'Wizard', + 'd': 'A mysterious spellcaster in purple.', + 't': 'Spawn', + 'i': 'wizardIcon', + 'v': {'char': 'Wizard', 'tchar': 'Wizard', 'Main': (0.5, 0.1, 0.8), 'Highlight': (0.9, 0.5, 1), 'Name': (0.8, 0.4, 1)} + }, + { + 'n': 'Evil Bones', + 'd': 'A skeleton painted in crimson rage.', + 't': 'Spawn', + 'i': 'bonesIcon', + 'v': {'char': 'Bones', 'tchar': 'Evil Bones', 'Main': (0.8, 0, 0), 'Highlight': (0.2, 0, 0), 'Name': (1, 0.2, 0.2)} + }, + { + 'n': 'Toxic Kronk', + 'd': 'He has been through some things.', + 't': 'Spawn', + 'i': 'kronkIcon', + 'v': {'char': 'Kronk', 'tchar': 'Toxic Kronk', 'Main': (0.2, 0.9, 0.1), 'Highlight': (0.5, 0.1, 0.8), 'Name': (0.3, 1, 0.3)} + }, + { + 'n': 'Pink Ninja', + 'd': 'Deadly. Also pink.', + 't': 'Spawn', + 'i': 'ninjaIcon', + 'v': {'char': 'Snake Shadow', 'tchar': 'Pink Ninja', 'Main': (1, 0.4, 0.7), 'Highlight': (1, 0.8, 0.9), 'Name': (1, 0.6, 0.8)} + }, + { + 'n': 'Rainbow Bones', + 'd': 'Born to be wild and colorful.', + 't': 'Spawn', + 'i': 'bonesIcon', + 'v': {'char': 'Bones', 'tchar': 'Rainbow Bones', 'Main': (1, 0.2, 0.8), 'Highlight': (0.2, 1, 0.4), 'Name': (0.4, 0.8, 1)} + }, + { + 'n': 'Agent Zero', + 'd': 'No name. No face. No mercy.', + 't': 'Spawn', + 'i': 'agentIcon', + 'v': {'char': 'Secret Agent', 'tchar': 'Agent Zero', 'Main': (0.15, 0.15, 0.15), 'Highlight': (0.8, 0.8, 0.8), 'Name': (0.6, 0.6, 0.6)} + }, + { + 'n': 'Galaxy Penguin', + 'd': 'Waddling across the cosmos.', + 't': 'Spawn', + 'i': 'penguinIcon', + 'v': {'char': 'Penguin', 'tchar': 'Galaxy Penguin', 'Main': (0.1, 0, 0.5), 'Highlight': (0.6, 0.2, 1), 'Name': (0.7, 0.5, 1)} + }, + { + 'n': 'Lava Wizard', + 'd': 'He studied the blade. Then set it on fire.', + 't': 'Spawn', + 'i': 'wizardIcon', + 'v': {'char': 'Wizard', 'tchar': 'Lava Wizard', 'Main': (1, 0.2, 0), 'Highlight': (1, 0.6, 0), 'Name': (1, 0.4, 0)} + }, + { + 'n': 'Blueberry Bear', + 'd': 'Round. Soft. Slightly dangerous.', + 't': 'Spawn', + 'i': 'bearIcon', + 'v': {'char': 'B-9000', 'tchar': 'Blueberry Bear', 'Main': (0.2, 0.4, 1), 'Highlight': (0.5, 0.7, 1), 'Name': (0.6, 0.8, 1)} + }, + { + 'n': 'Void Walker', + 'd': 'Came from nowhere. Going back.', + 't': 'Spawn', + 'i': 'cyborgIcon', + 'v': {'char': 'Cyborg', 'tchar': 'Void Walker', 'Main': (0, 0, 0), 'Highlight': (0.3, 0, 0.5), 'Name': (0.5, 0, 1)} + }, + { + 'n': 'Sun God', + 'd': 'Blinding. Warm. Absolutely unhinged.', + 't': 'Spawn', + 'i': 'wizardIcon', + 'v': {'char': 'Wizard', 'tchar': 'Sun God', 'Main': (1, 0.9, 0), 'Highlight': (1, 0.5, 0), 'Name': (1, 1, 0.5)} + }, + { + 'n': 'Midnight Agent', + 'd': 'Operates exclusively at 3am.', + 't': 'Spawn', + 'i': 'agentIcon', + 'v': {'char': 'Secret Agent', 'tchar': 'Midnight Agent', 'Main': (0.05, 0.05, 0.15), 'Highlight': (0.2, 0.2, 0.5), 'Name': (0.4, 0.4, 1)} + }, + { + 'n': 'Cursed Frosty', + 'd': 'Do NOT let him near the thermostat.', + 't': 'Spawn', + 'i': 'frostyIcon', + 'v': {'char': 'Frosty', 'tchar': 'Cursed Frosty', 'Main': (0.3, 0, 0.1), 'Highlight': (0.8, 0, 0.3), 'Name': (1, 0, 0.4)} + }, + { + 'n': 'Neon Jack', + 'd': 'He glows. You will too eventually.', + 't': 'Spawn', + 'i': 'jackIcon', + 'v': {'char': 'Jack Morgan', 'tchar': 'Neon Jack', 'Main': (0, 1, 0.5), 'Highlight': (0, 0.5, 1), 'Name': (0, 1, 0.8)} + }, + { + 'n': 'Inferno Ali', + 'd': 'Float like a butterfly, burn like the sun.', + 't': 'Spawn', + 'i': 'aliIcon', + 'v': {'char': 'Ali', 'tchar': 'Inferno Ali', 'Main': (1, 0.3, 0), 'Highlight': (1, 0.7, 0), 'Name': (1, 0.5, 0.1)} + }, + { + 'n': 'Ocean Mel', + 'd': 'Deep, mysterious, slightly salty.', + 't': 'Spawn', + 'i': 'melIcon', + 'v': {'char': 'Mel', 'tchar': 'Ocean Mel', 'Main': (0, 0.4, 0.8), 'Highlight': (0, 0.8, 1), 'Name': (0.3, 0.9, 1)} + }, + { + 'n': 'Pixel Zoe', + 'd': 'She runs at exactly 8 frames per second.', + 't': 'Spawn', + 'i': 'zoeColorMask', + 'v': {'char': 'Zoe', 'tchar': 'Pixel Zoe', 'Main': (0.9, 0.5, 0.9), 'Highlight': (0.5, 0.9, 0.5), 'Name': (1, 0.8, 1)} + }, + { + 'n': 'Rust Bot', + 'd': 'Outdated firmware. Still dangerous.', + 't': 'Spawn', + 'i': 'cyborgIcon', + 'v': {'char': 'Cyborg', 'tchar': 'Rust Bot', 'Main': (0.6, 0.3, 0.1), 'Highlight': (0.4, 0.2, 0), 'Name': (0.8, 0.5, 0.2)} + }, + { + 'n': 'Plague Bear', + 'd': 'Unclear what disease. Very clear it works.', + 't': 'Spawn', + 'i': 'bearIcon', + 'v': {'char': 'B-9000', 'tchar': 'Plague Bear', 'Main': (0.3, 0.5, 0.1), 'Highlight': (0.6, 0.8, 0), 'Name': (0.5, 1, 0.2)} + }, + { + 'n': 'Demon Santa', + 'd': 'He sees you when you\'re sleeping. Still.', + 't': 'Spawn', + 'i': 'santaIcon', + 'v': {'char': 'Santa Claus', 'tchar': 'Demon Santa', 'Main': (0.5, 0, 0), 'Highlight': (0.1, 0, 0), 'Name': (1, 0, 0)} + }, + # === BUILD === + { + 'n': 'Bouncy TNT', + 'd': 'A highly reflective TNT block that barely obeys gravity.', + 't': 'Build', + 'i': 'tnt', + 'v': {'b_mode': 0, 'b_mesh': 'tnt', 'b_tex': 'tnt', 'b_body': 'crate', 'b_grav': '0.3'} + }, + { + 'n': 'Giant Coin', + 'd': 'A massive golden puck that bounces around.', + 't': 'Build', + 'i': 'coin', + 'v': {'b_mode': 0, 'b_mesh': 'puck', 'b_tex': 'tokens4', 'b_body': 'puck', 'b_grav': '1.0'} + }, + { + 'n': 'Sticky Bomb', + 'd': 'Clings to surfaces like it has something to prove.', + 't': 'Build', + 'i': 'bombStickyColor', + 'v': {'b_mode': 0, 'b_mesh': 'bomb', 'b_tex': 'bombStickyColor', 'b_body': 'sphere', 'b_grav': '1.0'} + }, + { + 'n': 'Ice Ball', + 'd': 'A slippery ice bomb that glides around.', + 't': 'Build', + 'i': 'bombColorIce', + 'v': {'b_mode': 0, 'b_mesh': 'bomb', 'b_tex': 'bombColorIce', 'b_body': 'sphere', 'b_grav': '0.5'} + }, + { + 'n': 'Floating Orb', + 'd': 'Weightless. Pointless. Beautiful.', + 't': 'Build', + 'i': 'powerupShield', + 'v': {'b_mode': 0, 'b_mesh': 'bomb', 'b_tex': 'powerupShield', 'b_body': 'sphere', 'b_grav': '0.0'} + }, + { + 'n': 'Moon Rock', + 'd': 'Crate that floats like it forgot gravity exists.', + 't': 'Build', + 'i': 'tnt', + 'v': {'b_mode': 0, 'b_mesh': 'crate', 'b_tex': 'tnt', 'b_body': 'crate', 'b_grav': '0.15'} + }, + { + 'n': 'Speed Puck', + 'd': 'Puck with basically no friction with reality.', + 't': 'Build', + 'i': 'coin', + 'v': {'b_mode': 0, 'b_mesh': 'puck', 'b_tex': 'puckColor', 'b_body': 'puck', 'b_grav': '0.6'} + }, + { + 'n': 'Cursed Crate', + 'd': 'Crate shaped. Power unknown. Do not open.', + 't': 'Build', + 'i': 'achievementTNT', + 'v': {'b_mode': 0, 'b_mesh': 'crate', 'b_tex': 'powerupCurse', 'b_body': 'crate', 'b_grav': '1.0'} + }, + { + 'n': 'Neon Text', + 'd': 'Bright glowing text to mark your territory.', + 't': 'Build', + 'i': 'achievementOutline', + 'v': {'b_mode': 1, 'b_txt': 'Danger!', 'Text': (1, 0, 0), 'b_scl': '0.02', 'b_inw': True} + }, + { + 'n': 'Warning Sign', + 'd': 'For when something is definitely about to go wrong.', + 't': 'Build', + 'i': 'achievementTNT', + 'v': {'b_mode': 1, 'b_txt': 'WARNING!', 'Text': (1, 0.2, 0), 'b_scl': '0.025', 'b_inw': True} + }, + { + 'n': 'Name Tag', + 'd': 'A floating name tag. Edit text to personalize.', + 't': 'Build', + 'i': 'achievementOutline', + 'v': {'b_mode': 1, 'b_txt': 'Player 1', 'Text': (0.4, 1, 0.4), 'b_scl': '0.015', 'b_inw': True} + }, + { + 'n': 'Touch Grass', + 'd': 'Friendly reminder.', + 't': 'Build', + 'i': 'achievementOutline', + 'v': {'b_mode': 1, 'b_txt': 'Touch grass.', 'Text': (0.3, 0.9, 0.3), 'b_scl': '0.018', 'b_inw': True} + }, + { + 'n': 'Skill Issue', + 'd': 'Post this wherever appropriate.', + 't': 'Build', + 'i': 'achievementSuperPunch', + 'v': {'b_mode': 1, 'b_txt': 'Skill Issue', 'Text': (1, 0.4, 0.1), 'b_scl': '0.022', 'b_inw': True} + }, + { + 'n': 'GG Sign', + 'd': 'Place after winning. Or losing. Whatever.', + 't': 'Build', + 'i': 'achievementMedalLarge', + 'v': {'b_mode': 1, 'b_txt': 'GG!', 'Text': (0.2, 1, 0.5), 'b_scl': '0.03', 'b_inw': True} + }, + { + 'n': 'Chaos Box', + 'd': 'Unknown mesh. Unknown body. You\'ll see.', + 't': 'Build', + 'i': 'powerupBomb', + 'v': {'b_mode': 0, 'b_mesh': 'tnt', 'b_tex': 'explosion', 'b_body': 'sphere', 'b_grav': '0.05'} + }, + { + 'n': 'Ice Puck', + 'd': 'Hockey puck made of ice. Slide forever.', + 't': 'Build', + 'i': 'bombColorIce', + 'v': {'b_mode': 0, 'b_mesh': 'puck', 'b_tex': 'bombColorIce', 'b_body': 'puck', 'b_grav': '0.8'} + }, + # === QUICK === + { + 'n': 'Cinematic Mode', + 'd': 'Epic slow motion with auto-pause disabled.', + 't': 'Quick', + 'i': 'settingsIcon', + 'v': {'epic_mode': True, 'npp': 0} + }, + { + 'n': 'Disco Time', + 'd': 'RGB global tint cycle for a party vibe.', + 't': 'Quick', + 'i': 'achievementGotTheMoves', + 'v': {'disco': True} + }, + { + 'n': 'Moon Physics', + 'd': 'Low gravity for all nodes in the scene.', + 't': 'Quick', + 'i': 'upButton', + 'v': {'t_moongrav': True} + }, + { + 'n': 'Shiny World', + 'd': 'Crank up reflections on everything.', + 't': 'Quick', + 'i': 'achievementFlawlessVictory', + 'v': {'t_shiny': True} + }, + { + 'n': 'God Mode Setup', + 'd': 'Open Quick and smash God Mode immediately.', + 't': 'Quick', + 'i': 'star', + 'v': {} + }, + { + 'n': 'Freeze Frame', + 'd': 'Slow motion on, auto pause off. For clips.', + 't': 'Quick', + 'i': 'replayIcon', + 'v': {'epic_mode': True, 'npp': 0} + }, + # === TUNE === + { + 'n': 'Chill Setup', + 'd': 'Autoplay sounds on, fixed logs off. Relaxed vibes.', + 't': 'Tune', + 'i': 'audioIcon', + 'v': {'spa': True, 'icall': False, 'npp': 1} + }, + { + 'n': 'Dev Mode', + 'd': 'Fixed width logs, no auto pause, autoplay off.', + 't': 'Tune', + 'i': 'logIcon', + 'v': {'icall': True, 'spa': False, 'npp': 0} + }, + { + 'n': 'Control Freak', + 'd': 'Block host, auto-close UI, intercept inputs.', + 't': 'Tune', + 'i': 'controllerIcon', + 'v': {'cconf0': True, 'cconf1': True, 'cconf2': False, 'cconf3': True} + }, + { + 'n': 'Ghost Control', + 'd': 'Control without blocking host inputs. Sneaky.', + 't': 'Tune', + 'i': 'controllerIcon', + 'v': {'cconf0': False, 'cconf1': False, 'cconf2': False, 'cconf3': False} + }, + { + 'n': 'Invincible Driver', + 'd': 'Invincibility on while controlling. No dying.', + 't': 'Tune', + 'i': 'star', + 'v': {'cconf2': True, 'cconf0': True, 'cconf3': True} + }, + ] + + s.img = bui.imagewidget( + parent=w, + position=(50, 270), + size=(90, 90), + texture=bui.gettexture('empty'), + mask_texture=bui.gettexture('empty') + ) + + s.t_name = tw( + p=w, pos=(185, 315), size=(1, 35), text='', + scale=1.2, h_align='left', v_align='center', maxwidth=215 + ) + + s.t_desc = tw( + p=w, pos=(185, 270), size=(1, 55), text='', + scale=0.78, h_align='left', v_align='top', maxwidth=215, + color=(0.6, 0.6, 0.6) + ) + + s.b_load = bw( + p=w, pos=(420, 280), size=(130, 50), + label='Load', icon=bui.gettexture('inventoryIcon'), + oac=s.load_preset + ) + + sv = bui.scrollwidget( + parent=w, + position=(40, 40), + size=(520, 200), + color=var('bg') + ) + + count = len(s.presets) + rows = math.ceil(count / 2) + ss = max(200, rows * 45) + + s.cv = cw( + parent=sv, + background=False, + size=(500, ss) + ) + + s.texts = [] + for i, p in enumerate(s.presets): + col = i % 2 + row = i // 2 + x = 10 + col * 245 + y = ss - 45 - row * 45 + t = tw( + p=s.cv, pos=(x, y), size=(235, 40), + text=p['n'], color=var('t'), + selectable=True, click_activate=True, maxwidth=230, + on_activate_call=bs.CallPartial(s.select, i), + h_align='center', v_align='center' + ) + s.texts.append(t) + + if count > 0: + s.select(0) + + def select(s, i): + if s.sl != i and s.sl is not None: + bui.getsound('tap').play() + s.sl = i + + for idx, t in enumerate(s.texts): + tw(t, color=(0, 1, 0) if idx == i else var('t')) + + p = s.presets[i] + tw(s.t_name, text=p['n'], color=p['v'].get('Main', var('t'))) + tw(s.t_desc, text=p['d']) + + if p['t'] == 'Spawn': + c_name = p['v'].get('char', NAME()[0]) + valid_char = c_name if c_name in NAME() else NAME()[0] + char_obj = SPAZ()[NAME().index(valid_char)] + bui.imagewidget( + edit=s.img, + texture=bui.gettexture(char_obj.icon_texture), + mask_texture=bui.gettexture('characterIconMask'), + tint_texture=bui.gettexture(char_obj.icon_mask_texture), + color=(1, 1, 1), + tint_color=p['v'].get('Main', (1, 1, 1)), + tint2_color=p['v'].get('Highlight', (1, 1, 1)) + ) + else: + try: + tex = bui.gettexture(p.get('i') or 'logo') + except Exception: + tex = bui.gettexture('logo') + bui.imagewidget( + edit=s.img, + texture=tex, + mask_texture=bui.gettexture('empty'), + tint_texture=bui.gettexture('empty'), + color=(1, 1, 1), + tint_color=(1, 1, 1), + tint2_color=(1, 1, 1) + ) + + def load_preset(s): + if s.sl is None: + btw('Select a preset first!') + return + + p = s.presets[s.sl] + + for k, v in p['v'].items(): + var(k, v) + + if p['t'] == 'Spawn': + c_name = p['v'].get('char', NAME()[0]) + valid_char = c_name if c_name in NAME() else NAME()[0] + fixall(valid_char) + + ding() + push(f"Loaded {p['n']}!", color=(0, 1, 0)) + + cw(s.w, transition='out_right') + bui.apptimer(0.1, lambda: Coolbox(fb=p['t'])) + +class Mapper2D: + + last = 0 + + def __init__(s, pipe, pos=None, on_exit=None): + s.tired = time.time_ns() - s.__class__.last < 10**9 + if s.tired: btw('Cool down!'); return + + p = pos if pos and len(pos) == 2 else (0, 0) + if pos and len(pos) == 3: + p = (pos[0], pos[1]) + + s.pipe = pipe + s.on_exit = on_exit + + with bs.get_foreground_host_activity().context: + s.node = bs.newnode('image', attrs={ + 'texture': bs.gettexture('achievementCrossHair'), + 'absolute_scale': True, + 'scale': (50, 50), + 'position': p, + 'color': (0, 1, 0) + }) + bui.getsound('laser').play() + + s.safe = None + def f(): s.safe = s.node.exists() + bui.apptimer(1, f) + + s.step = s.ostep = 3.0 + + s.wait = 0.01 + s.bstep = s.step * 3 + s.llr = s.lud = 0.0 + + s.overlay = Overlay(build=True) + + LN({ + 'UP_DOWN': lambda a: s.manage(a, 0), + 'LEFT_RIGHT': lambda a: s.manage(a, 1), + 'BOMB_PRESS': s.exit, + 'PUNCH_PRESS': s.build, + }) + STATE(True) + s.move() + + def manage(s, a, lr=0): + if lr: s.llr = a + else: s.lud = a + + def move(s): + m = getme(1) + if not m or m._dead: s.destroy(); return + try: p = s.node.position + except: + if STATE(): + STATE(False) + bui.apptimer(1, s.fail) + return + + new_p = (p[0] + s.llr * s.step, p[1] + s.lud * s.step) + s.node.position = new_p + s.overlay.up(new_p[0], new_p[1], 0, s.llr, s.lud) + + bui.apptimer(s.wait, s.move) + + def destroy(s): + with bs.get_foreground_host_activity().context: + n = s.node + bui.getsound('shatter').play() + n.delete() + bs.timer(1, s.reset) + + def reset(s): + me = getme() + if not me: return + me.resetinput() + with bs.get_foreground_host_activity().context: + me.actor.connect_controls_to_player() + + """Build at current position (repeatable - does not exit)""" + def build(s): + s.overlay.press(3) + try: p = s.node.position + except: return + + with bs.get_foreground_host_activity().context: + bs.timer(0.1, lambda: s.overlay.release(3)) + bui.getsound('gunCocking').play() + bs.timer(0.2, bs.animate(s.node,'scale',{0:(50,50),0.1:(30,30),0.2:(50,50)}).delete) + + s.pipe((p[0], p[1], 0.0)) + + """Exit mapper without building (graceful, no shatter)""" + def exit(s): + s.overlay.press(1) + s.overlay.destroy() + try: p = s.node.position + except: p = None + with bs.get_foreground_host_activity().context: + if p: + bui.getsound('powerup01').play() + bs.animate(s.node,'scale',{0:(50,50),0.1:(0,0)}) + bs.timer(0.1, s.node.delete) + else: + s.node.delete() + STATE(False) + s.__class__.last = time.time_ns() + if s.on_exit and p: bui.apptimer(1, bs.CallPartial(s.on_exit, (p[0],p[1],0.0))) + bui.apptimer(1, s.reset) + + def complain(s): + push('You destroyed the mapper!', color=(1, 0, 0)) + bui.getsound('swip').play() + s.overlay.destroy() + None if s.safe else btw('Mapper destroyed too early.') + + """Mapper2D is always build mode (dispatch needs a real position), + so on a lost node just complain - never call pipe() with none.""" + def fail(s): + s.complain() + + +@NEW +class Build: + + def __init__(s, *a): + w = s.w = a[0] + s.ex = a[5] + + con('b_mesh', 'tnt') + con('b_tex', 'tnt') + con('b_body', 'crate') + con('b_grav', '1.0') + con('b_mscale', '1.0') + con('b_bscale', '1.0') + con('b_txt', 'Coolbox!') + con('b_scl', '0.01') + con('b_iscal', '100.0') + con('Text', (1, 1, 1)) + con('Light', (1, 1, 1)) + con('Image', (1, 1, 1)) + con('b_rad', '1.0') + con('b_int', '1.0') + con('b_mode', 0) + con('b_inw', True) + + sv = bui.scrollwidget( + parent=w, + position=(30, 50), + color=var('bg'), + size=(140, 320) + ) + + s.cv = cw( + parent=sv, + background=False, + size=(120, 280) + ) + + s.rp = cw( + parent=w, + position=(180, 50), + size=(390, 320), + background=False + ) + + s.modes = ['Prop Builder', 'Text Builder', 'Light Builder', 'Image Builder', 'Node Tools', 'Fun Tools'] + s.m_btns = [] + for i, m in enumerate(s.modes): + b = bw( + p=s.cv, + pos=(5, 240 - 45 * i), + size=(110, 40), + label=m, + oac=bs.CallPartial(s.set_mode, i) + ) + s.m_btns.append(b) + + s.kids = [] + s.set_mode(var('b_mode')) + + if s.ex: + p, btype = s.ex + for i in range(3): + var(f'pos{i}', str(p[i])) + + def clear(s): + for k in s.kids: + if hasattr(k, 'delete'): + k.delete() + s.kids.clear() + + def set_mode(s, m): + var('b_mode', m) + s.clear() + + for i, b in enumerate(s.m_btns): + bw(b, color=(0, 1, 0) if i == m else var('bg')) + + s.kids.append(tw( + p=s.rp, pos=(195, 290), text=s.modes[m], + h_align='center', scale=1.3, color=(0.1, 0.7, 1) + )) + + bx = 100 + bw_sz = 250 + bh = 35 + tx = 20 + lw = bx - tx - 5 + + r0y = 220 + r1y = 175 + r2y = 130 + r3y = 85 + + def lbl(txt, ypos): + s.kids.append(tw(p=s.rp, pos=(tx, ypos), size=(lw, bh), text=txt, v_align='center', h_align='left', maxwidth=lw)) + + if m == 0: + p0y,p1y,p2y,p3y,p4y,p5y = 245,210,175,140,105,70 + + lbl('Mesh:', p0y) + bm = bw(p=s.rp, pos=(bx, p0y), size=(bw_sz, bh), label=var('b_mesh')) + bw(bm, oac=bs.CallPartial(MeshPicker, source=bm, pipe=s.pick_mesh, extra=bm)) + s.kids.append(bm) + + lbl('Texture:', p1y) + bt = bw(p=s.rp, pos=(bx, p1y), size=(bw_sz, bh), label=var('b_tex')) + bw(bt, oac=bs.CallPartial(TexPicker, source=bt, pipe=s.pick_tex)) + s.b_tex_btn = bt + s.kids.append(bt) + + lbl('Body:', p2y) + bb = ctw( + p=s.rp, pos=(bx, p2y), size=(bw_sz, bh), allow=True, + hint='crate/sphere/puck', conf='b_body', text=var('b_body') + ) + s.kids.extend([bb.widget, bb.widget2]) + + lbl('Gravity:', p3y) + bg = ctw( + p=s.rp, pos=(bx, p3y), size=(bw_sz, bh), allow='-0.123456789', + conf='b_grav', text=var('b_grav'), hint='Float' + ) + s.kids.extend([bg.widget, bg.widget2]) + + lbl('Mesh Scale:', p4y) + bms = ctw( + p=s.rp, pos=(bx, p4y), size=(bw_sz, bh), allow='0.123456789', + conf='b_mscale', text=var('b_mscale'), hint='Float' + ) + s.kids.extend([bms.widget, bms.widget2]) + + lbl('Body Scale:', p5y) + bbs = ctw( + p=s.rp, pos=(bx, p5y), size=(bw_sz, bh), allow='0.123456789', + conf='b_bscale', text=var('b_bscale'), hint='Float' + ) + s.kids.extend([bbs.widget, bbs.widget2]) + + bp = bw( + p=s.rp, pos=(115, 25), size=(160, 45), label='Map & Build', + icon=bui.gettexture('cursor'), oac=bs.CallPartial(s.map_it, 'prop') + ) + s.kids.append(bp) + + elif m == 1: + lbl('Text:', r0y) + btxt = ctw( + p=s.rp, pos=(bx, r0y), size=(bw_sz, bh), allow=True, + conf='b_txt', text=var('b_txt'), hint='String' + ) + s.kids.extend([btxt.widget, btxt.widget2]) + + lbl('Color:', r1y) + bc = bw(p=s.rp, pos=(bx, r1y), size=(50, bh), color=var('Text')) + bw(bc, oac=bs.CallPartial( + ColorPicker, in_source=bc, id='Text', what='Text', id2='Text', + chr='char', on_back=lambda: None, mode=2 + )) + s.kids.append(bc) + + lbl('Scale:', r2y) + bscl = ctw( + p=s.rp, pos=(bx, r2y), size=(bw_sz, bh), allow='0.123456789', + conf='b_scl', text=var('b_scl'), hint='Float' + ) + s.kids.extend([bscl.widget, bscl.widget2]) + + ck = chk( + parent=s.rp, position=(bx, r3y + 5), size=(150, 30), + value=bool(var('b_inw')), text='In World (3D)', + on_value_change_call=bs.CallPartial(s.toggle_inw) + ) + s.kids.append(ck) + + bp = bw( + p=s.rp, pos=(115, 25), size=(160, 45), label='Map & Build', + icon=bui.gettexture('cursor'), oac=bs.CallPartial(s.map_it, 'text') + ) + s.kids.append(bp) + + elif m == 2: + lbl('Color:', r0y) + bc = bw(p=s.rp, pos=(bx, r0y), size=(50, bh), color=var('Light')) + bw(bc, oac=bs.CallPartial( + ColorPicker, in_source=bc, id='Light', what='Light', id2='Light', + chr='char', on_back=lambda: None, mode=2 + )) + s.kids.append(bc) + + lbl('Radius:', r1y) + brad = ctw( + p=s.rp, pos=(bx, r1y), size=(bw_sz, bh), allow='0.123456789', + conf='b_rad', text=var('b_rad'), hint='Float' + ) + s.kids.extend([brad.widget, brad.widget2]) + + lbl('Intensity:', r2y) + bint = ctw( + p=s.rp, pos=(bx, r2y), size=(bw_sz, bh), allow='0.123456789', + conf='b_int', text=var('b_int'), hint='Float' + ) + s.kids.extend([bint.widget, bint.widget2]) + + bp = bw( + p=s.rp, pos=(115, 25), size=(160, 45), label='Map & Build', + icon=bui.gettexture('cursor'), oac=bs.CallPartial(s.map_it, 'light') + ) + s.kids.append(bp) + + elif m == 3: + lbl('Texture:', r0y) + bt = bw(p=s.rp, pos=(bx, r0y), size=(bw_sz, bh), label=var('b_tex')) + bw(bt, oac=bs.CallPartial(TexPicker, source=bt, pipe=s.pick_tex)) + s.b_tex_btn = bt + s.kids.append(bt) + + lbl('Color:', r1y) + bc = bw(p=s.rp, pos=(bx, r1y), size=(50, bh), color=var('Image')) + bw(bc, oac=bs.CallPartial( + ColorPicker, in_source=bc, id='Image', what='Image', id2='Image', + chr='char', on_back=lambda: None, mode=2 + )) + s.kids.append(bc) + + lbl('Scale:', r2y) + bscl = ctw( + p=s.rp, pos=(bx, r2y), size=(bw_sz, bh), allow='0.123456789', + conf='b_iscal', text=var('b_iscal'), hint='Float' + ) + s.kids.extend([bscl.widget, bscl.widget2]) + + bp = bw( + p=s.rp, pos=(115, 25), size=(160, 45), label='Build (2D)', + icon=bui.gettexture('cursor'), oac=bs.CallPartial(s.map_it, 'image') + ) + s.kids.append(bp) + + elif m == 4: + b1 = bw( + p=s.rp, pos=(95, 215), size=(200, 45), label='Pick & Edit', + icon=bui.gettexture('menuIcon') + ) + bw(b1, oac=bs.CallPartial(NodePicker, source=b1, pipe=lambda n: NodeManager(node=n, source=b1))) + s.kids.append(b1) + + b2 = bw( + p=s.rp, pos=(95, 160), size=(200, 45), label='Pick & Delete', + icon=bui.gettexture('crossOut') + ) + bw(b2, oac=bs.CallPartial( + NodePicker, source=b2, + pipe=lambda n: (n.delete(), ding(), push('Node Deleted!', color=(1, 0, 0))) + )) + s.kids.append(b2) + + b3 = bw( + p=s.rp, pos=(95, 105), size=(200, 45), label='Pick & Attach Light', + icon=bui.gettexture('star') + ) + bw(b3, oac=bs.CallPartial(NodePicker, source=b3, pipe=s.attach_light)) + s.kids.append(b3) + + b4 = bw( + p=s.rp, pos=(95, 50), size=(200, 45), label='Pick & Scale Up', + icon=bui.gettexture('upButton') + ) + bw(b4, oac=bs.CallPartial(NodePicker, source=b4, pipe=s.scale_up)) + s.kids.append(b4) + + elif m == 5: + b1 = bw( + p=s.rp, pos=(95, 215), size=(200, 45), label='Rain Current Prop', + icon=bui.gettexture('downButton') + ) + bw(b1, oac=s.rain_props) + s.kids.append(b1) + + b2 = bw( + p=s.rp, pos=(95, 160), size=(200, 45), label='Clear All Props', + icon=bui.gettexture('crossOut') + ) + bw(b2, oac=bs.CallPartial(s.clear_nodes, 'prop')) + s.kids.append(b2) + + b3 = bw( + p=s.rp, pos=(95, 105), size=(200, 45), label='Clear All Texts', + icon=bui.gettexture('crossOut') + ) + bw(b3, oac=bs.CallPartial(s.clear_nodes, 'text')) + s.kids.append(b3) + + b4 = bw( + p=s.rp, pos=(95, 50), size=(200, 45), label='Clear All Images', + icon=bui.gettexture('crossOut') + ) + bw(b4, oac=bs.CallPartial(s.clear_nodes, 'image')) + s.kids.append(b4) + + def toggle_inw(s, val): + var('b_inw', val) + + def pick_mesh(s, idx, btn): + m = MESH()[idx] + var('b_mesh', m) + bw(btn, label=m) + + def pick_tex(s, idx): + t = ALL()[idx] + var('b_tex', t) + if hasattr(s, 'b_tex_btn') and s.b_tex_btn.exists(): + bw(s.b_tex_btn, label=t) + + def map_it(s, btype): + if btype == 'image' or (btype == 'text' and not var('b_inw')): + s.mapper = Mapper2D( + pipe=bs.CallPartial(s.dispatch, btype), + pos=getpos()[:2], + on_exit=bs.CallPartial(s.reopen, btype) + ) + else: + s.mapper = Mapper( + pipe=bs.CallPartial(s.dispatch, btype), + pos=getpos(), + on_exit=bs.CallPartial(s.reopen, btype), + build=True + ) + None if s.mapper.tired else cw(s.w, transition='out_right') + + """Reopen the Build window at the position mapping ended on, once + the mapper is dismissed via bomb.""" + def reopen(s, btype, p): + Coolbox(fb='Build', fake=True, extra=(p, btype)) + + """Build directly at a mapped position - no menu/window round-trip, + so this is safe to call repeatedly (e.g. once per punch) without + tripping the Coolbox-already-running guard.""" + def dispatch(s, btype, p): + { + 'prop': s.do_prop, + 'text': s.do_text, + 'light': s.do_light, + 'image': s.do_image, + }[btype](p) + + def do_prop(s, pos, silent=False): + with bs.get_foreground_host_activity().context: + o = bs_gameutils.SharedObjects.get() + try: + bs.newnode('prop', attrs={ + 'position': pos, + 'mesh': bs.getmesh(var('b_mesh')), + 'color_texture': bs.gettexture(var('b_tex')), + 'body': var('b_body'), + 'materials': [o.object_material, o.footing_material], + 'reflection': 'soft', + 'reflection_scale': [0.5], + 'gravity_scale': float(var('b_grav')), + 'mesh_scale': float(var('b_mscale')), + 'body_scale': float(var('b_bscale')) + }) + if not silent: + SPARK(pos) + ding() + push('Prop Built!', color=(0, 1, 0)) + except Exception as e: + err(f"Build Failed: {e}") + + def do_text(s, pos): + in_w = var('b_inw') + with bs.get_foreground_host_activity().context: + try: + n = bs.newnode('text', attrs={ + 'text': var('b_txt'), + 'color': var('Text'), + 'scale': float(var('b_scl')), + 'in_world': in_w, + 'h_align': 'center' + }) + if in_w: + n.position = pos + SPARK(pos) + else: + n.position = (pos[0], pos[1]) + ding() + push('Text Built!', color=(0, 1, 0)) + except Exception as e: + err(f"Build Failed: {e}") + + def do_light(s, pos): + with bs.get_foreground_host_activity().context: + try: + bs.newnode('light', attrs={ + 'position': pos, + 'color': var('Light'), + 'radius': float(var('b_rad')), + 'intensity': float(var('b_int')) + }) + SPARK(pos) + ding() + push('Light Built!', color=(0, 1, 0)) + except Exception as e: + err(f"Build Failed: {e}") + + def do_image(s, pos): + try: + scl = float(var('b_iscal')) + with bs.get_foreground_host_activity().context: + bs.newnode('image', attrs={ + 'position': (pos[0], pos[1]), + 'texture': bs.gettexture(var('b_tex')), + 'color': var('Image'), + 'scale': (scl, scl), + 'absolute_scale': True + }) + ding() + push('Image Built!', color=(0, 1, 0)) + except Exception as e: + err(f"Build Failed: {e}") + + def attach_light(s, n): + with bs.get_foreground_host_activity().context: + l = bs.newnode('light', owner=n, attrs={ + 'color': (random.random(), random.random(), random.random()), + 'radius': 1.2, + 'intensity': 1.0 + }) + n.connectattr('position', l, 'position') + ding() + push('Light Attached!', color=(1, 1, 0)) + + def scale_up(s, n): + with bs.get_foreground_host_activity().context: + if hasattr(n, 'mesh_scale'): + v = n.mesh_scale + n.mesh_scale = [i * 1.5 for i in v] if isinstance(v, (list, tuple)) else v * 1.5 + if hasattr(n, 'scale'): + v = n.scale + n.scale = [i * 1.5 for i in v] if isinstance(v, (list, tuple)) else v * 1.5 + ding() + push('Node Scaled Up!', color=(0, 1, 0)) + + def clear_nodes(s, t): + c = 0 + with bs.get_foreground_host_activity().context: + for n in bs.getnodes(): + if n and n.getnodetype() == t and n != bs.get_foreground_host_activity().globalsnode: + n.delete() + c += 1 + bui.getsound('shieldDown').play() + push(f'Cleared {c} {t}s!', color=(1, 0.5, 0)) + + def rain_props(s): + p = getpos() + def drop(): + if not s.w.exists(): + return + rp = (p[0] + random.uniform(-6, 6), p[1] + 15, p[2] + random.uniform(-6, 6)) + s.do_prop(rp, True) + + for i in range(25): + bui.apptimer(i * 0.1, drop) + + bui.getsound('spawn').play() + push('Raining Props!', color=(0, 1, 1)) + +@NEW +class Camera: + """Camera control panel""" + + def __init__(s, *a): + w = s.w = a[0] + s.live = False + s.live_timer = None + s.manual = False + + # --- Position inputs --- + tw(p=w, pos=(50, 335), text='Position', h_align='center', size=(150, 25)) + s.pos_inputs = [] + for i, label in enumerate(['X', 'Y', 'Z']): + tw(p=w, pos=(50, 305 - i*40), text=label, size=(20, 30), v_align='center') + t = ctw( + p=w, pos=(75, 298 - i*40), size=(120, 35), + allow='-0.123456789', hint=f'Pos {label}', + conf=f'cam_pos{i}', text=str(round(_ba.get_camera_position()[i], 2)) + ) + s.pos_inputs.append(t) + + # --- Target inputs --- + tw(p=w, pos=(230, 335), text='Target', h_align='center', size=(150, 25)) + s.tar_inputs = [] + for i, label in enumerate(['X', 'Y', 'Z']): + tw(p=w, pos=(230, 305 - i*40), text=label, size=(20, 30), v_align='center') + t = ctw( + p=w, pos=(255, 298 - i*40), size=(120, 35), + allow='-0.123456789', hint=f'Tar {label}', + conf=f'cam_tar{i}', text=str(round(_ba.get_camera_target()[i], 2)) + ) + s.tar_inputs.append(t) + + # --- Manual mode checkbox --- + s.man_chk = chk( + parent=w, position=(410, 340), + text='Manual', size=(150, 30), + value=False, + on_value_change_call=s.set_manual + ) + + # --- Live preview toggle --- + s.b_live = bw( + p=w, pos=(410, 290), size=(150, 40), + label='Live Preview', icon=bui.gettexture('replayIcon'), + oac=s.toggle_live + ) + + # --- Arrow controls --- + # XZ plane (position) + arrows = [ + ('Z-', (460, 245), (0, 0, -1)), + ('X-', (410, 195), (-1, 0, 0)), + ('·', (460, 195), None), + ('X+', (510, 195), (1, 0, 0)), + ('Z+', (460, 145), (0, 0, 1)), + ('Y+', (560, 195), (0, 1, 0)), + ('Y-', (560, 145), (0, -1, 0)), + ] + labels = ['↑', '←', '·', '→', '↓', 'Y↑', 'Y↓'] + for (lbl, pos, delta), txt in zip(arrows, labels): + if delta is None: + tw(p=w, pos=pos, text='✛', h_align='center', size=(40, 40), v_align='center', color=(0.4, 0.4, 0.4)) + continue + bw( + p=w, pos=pos, size=(40, 40), + label=txt, text_scale=1.1, + oac=bs.CallPartial(s.nudge, delta), + repeat=True + ) + + # Step size + tw(p=w, pos=(50, 155), text='Step:', size=(50, 30), v_align='center') + s.step_t = ctw( + p=w, pos=(100, 148), size=(90, 35), + allow='0.123456789', hint='Step', + conf='cam_step', text=var('cam_step') or '0.5' + ) + + # Target mode toggle + s.tar_mode = False + s.b_mode = bw( + p=w, pos=(50, 105), size=(150, 35), + label='Moving: Pos', oac=s.toggle_mode + ) + + # --- Apply / Reset / Sync --- + bw(p=w, pos=(220, 105), size=(120, 35), + label='Apply', icon=bui.gettexture('settingsIcon'), + oac=s.apply) + + bw(p=w, pos=(350, 105), size=(120, 35), + label='Sync', icon=bui.gettexture('replayIcon'), + oac=s.sync) + + bw(p=w, pos=(50, 55), size=(150, 35), + label='Reset Camera', icon=bui.gettexture('replayIcon'), + oac=s.reset) + + bw(p=w, pos=(220, 55), size=(250, 35), + label='Pick Node → Focus', icon=bui.gettexture('star'), + oac=s.pick_focus) + + # init sync + s.sync() + + def get_pos(s): + return tuple(float(s.pos_inputs[i].get_text() or '0') for i in range(3)) + + def get_tar(s): + return tuple(float(s.tar_inputs[i].get_text() or '0') for i in range(3)) + + def set_manual(s, v): + s.manual = v + _ba.set_camera_manual(v) + + def toggle_mode(s): + s.tar_mode = not s.tar_mode + bw(s.b_mode, label=f"Moving: {'Tar' if s.tar_mode else 'Pos'}") + bui.getsound('deek').play() + + def toggle_live(s): + s.live = not s.live + bw(s.b_live, color=(0, 0.6, 0) if s.live else var('bg')) + if s.live: + s.live_timer = bui.AppTimer(0.02, s._live_tick, repeat=True) + bui.getsound('ding').play() + else: + s.live_timer = None + bui.getsound('tap').play() + + def _live_tick(s): + if not s.w.exists() or s.w.transitioning_out: + s.live_timer = None + return + try: + p = s.get_pos() + t = s.get_tar() + if s.manual: + _ba.set_camera_position(*p) + _ba.set_camera_target(*t) + except Exception: + pass + + def nudge(s, delta): + step = float(s.step_t.get_text() or '0.5') + inputs = s.tar_inputs if s.tar_mode else s.pos_inputs + for i in range(3): + if delta[i] == 0: + continue + cur = float(inputs[i].get_text() or '0') + inputs[i].set_text(str(round(cur + delta[i] * step, 3))) + if s.live: + s._live_tick() + + def apply(s): + try: + p = s.get_pos() + t = s.get_tar() + except ValueError: + btw('Invalid input!') + return + if s.manual: + _ba.set_camera_position(*p) + _ba.set_camera_target(*t) + ding() + push('Camera applied!', color=(0, 1, 0)) + + def sync(s): + p = _ba.get_camera_position() + t = _ba.get_camera_target() + for i in range(3): + s.pos_inputs[i].set_text(str(round(p[i], 3)), silent=True) + s.tar_inputs[i].set_text(str(round(t[i], 3)), silent=True) + bui.getsound('tap').play() + + def reset(s): + _ba.set_camera_manual(False) + chk(s.man_chk, value=False) + s.manual = False + if s.live: + s.toggle_live() + s.sync() + ding() + push('Camera reset!', color=(0, 1, 0)) + + def pick_focus(s): + NodePicker( + source=s.w, + pipe=s._focus_on, + allow='3D' + ) + + def _focus_on(s, n): + try: + p = n.position + except Exception: + btw('Node has no position!') + return + for i in range(3): + s.tar_inputs[i].set_text(str(round(p[i], 3)), silent=True) + _ba.set_camera_target(*p) + FOCUS(p, 2) + ding() + + +@NEW +class Scene: + """Scene control: map, tint presets""" + + def __init__(s, *a): + w = s.w = a[0] + + # MAP (left: x=40..290) + assert bui.app.classic is not None + maps = sorted(bui.app.classic.maps.keys()) + + s.sel_map = None + s.map_btns = [] + ROW = 32 + content_h = max(260, len(maps) * ROW) + + s._scroll = bui.scrollwidget( + parent=w, position=(40, 110), + size=(245, 260), color=var('bg'), + border_opacity=0.4 + ) + s._cv = cw(parent=s._scroll, background=False, + size=(225, content_h)) + + for i, m in enumerate(maps): + y = content_h - ROW - i * ROW + b = bw( + p=s._cv, pos=(0, y), size=(225, ROW - 2), + label=m, text_scale=0.75, + oac=bs.CallPartial(s.select_map, i, m) + ) + s.map_btns.append(b) + + s.b_switch = bw( + p=w, pos=(40, 60), size=(118, 42), + label='Switch Map', icon=bui.gettexture('levelIcon'), + oac=s.do_map + ) + s.b_mapnode = bw( + p=w, pos=(166, 60), size=(118, 42), + label='Map Node', icon=bui.gettexture('settingsIcon'), + oac=s.map_node + ) + + # ── TINT (right top: x=305..555) ───────────────────── + bw(p=w, pos=(305, 332), size=(120, 40), + label='× 1.1', text_scale=0.9, + oac=bs.CallPartial(s.mult_tint, 1.1)) + bw(p=w, pos=(433, 332), size=(120, 40), + label='÷ 1.1', text_scale=0.9, + oac=bs.CallPartial(s.mult_tint, 1 / 1.1)) + + bw(p=w, pos=(305, 284), size=(250, 40), + label='Reset Tint', icon=bui.gettexture('replayIcon'), + oac=s.reset_tint) + + # ── TINT PRESETS (right bottom: x=305..555) ────────── + presets = [ + ('Neutral', (1.0, 1.0, 1.0)), + ('Warm', (1.3, 1.0, 0.8)), + ('Cold', (0.8, 0.9, 1.3)), + ('Dusk', (1.2, 0.8, 0.6)), + ('Horror', (0.5, 0.7, 0.5)), + ('Void', (0.3, 0.3, 0.5)), + ('Sunset', (1.3, 0.85, 0.6)), + ('Frost', (0.85, 1.0, 1.2)), + ('Toxic', (0.6, 1.2, 0.5)), + ] + s.preset_btns = [] + for i, (lbl, col) in enumerate(presets): + b = bw( + p=w, + pos=(305 + (i % 3) * 84, 238 - (i // 3) * 44), + size=(78, 38), label=lbl, + color=col, oac=bs.CallPartial(s.set_tint, col) + ) + s.preset_btns.append(b) + + s.b_custom_tint = bw( + p=w, pos=(305, 60), size=(250, 42), + label='Custom Tint', icon=bui.gettexture('settingsIcon'), + oac=s.open_tint + ) + + # ── helpers ─────────────────────────────────────────────── + + def _glob(s): + return bs.get_foreground_host_activity().globalsnode + + # tint + + def open_tint(s): + try: + c = s._glob().tint + except Exception: + c = (1.0, 1.0, 1.0) + var('scene_tint', c) + var('scene_tintRed', str(round(c[0], 4))) + var('scene_tintGreen', str(round(c[1], 4))) + var('scene_tintBlue', str(round(c[2], 4))) + ColorPicker( + in_source=s.b_custom_tint, + id='scene_tint', what='scene_tint', + id2='scene_tint', chr='char', mode=2, + on_back=s.apply_tint + ) + + def apply_tint(s): + c = var('scene_tint') or (1.0, 1.0, 1.0) + with bs.get_foreground_host_activity().context: + s._glob().tint = c + ding() + push('Tint applied!', color=(0, 1, 0)) + + def mult_tint(s, factor): + with bs.get_foreground_host_activity().context: + c = s._glob().tint + nc = tuple(c[i] * factor for i in range(3)) + s._glob().tint = nc + ding() + + def reset_tint(s): + with bs.get_foreground_host_activity().context: + s._glob().tint = (1.0, 1.0, 1.0) + ding() + push('Tint reset!', color=(0, 1, 0)) + + def set_tint(s, col): + try: + with bs.get_foreground_host_activity().context: + s._glob().tint = col + except Exception as e: + err(str(e)) + return + ding() + push('Tint set!', color=col) + + # map + + def select_map(s, i, name): + for idx, b in enumerate(s.map_btns): + bw(b, color=(0.0, 0.45, 0.0) if idx == i else var('bg')) + s.sel_map = name + bui.getsound('tap').play() + + def do_map(s): + if not s.sel_map: + btw('Pick a map first!') + return + try: + s._change_map(s.sel_map) + ding() + push(f'Map → {s.sel_map}!', color=(0, 1, 0)) + except Exception as e: + err(str(e)) + + def map_node(s): + try: + NodeManager( + node=bs.get_foreground_host_activity().map.node, + source=s.b_mapnode + ) + except Exception: + btw('Map has no main node!') + + def _change_map(s, ma): + from bascenev1lib.gameutils import SharedObjects + _act = bs.get_foreground_host_activity() + old_map = _act.map + cls = bs.get_map_class(ma) + with _act.context: + if type(old_map) in _act.preloads: + del _act.preloads[type(old_map)] + for attr in dir(old_map): + if not attr.startswith('_') and attr not in [ + 'node', 'activity', 'getactivity', 'handlemessage', + 'on_expire', 'autoretain', 'is_alive', 'exists' + ]: + try: + val = getattr(old_map, attr) + if not callable(val): + if hasattr(val, 'delete') and hasattr(val, 'exists'): + try: + if val.exists(): val.delete() + except Exception: + pass + delattr(old_map, attr) + except Exception: + pass + _act.preloads[cls] = preload = cls.on_preload() + shared = SharedObjects.get() + for node in bs.getnodes(): + if node.getnodetype() == 'terrain' and node != old_map.node: + try: node.delete() + except Exception: pass + for attr in ['bottom', 'floor', 'stands', 'background', 'railing', + 'bg_collide', 'stem', 'player_wall', 'bg2', 'node_bottom']: + if hasattr(old_map, attr): + delattr(old_map, attr) + if hasattr(old_map, 'node') and old_map.node: + old_map.node.mesh = ( + preload.get('mesh') or + preload.get('mesh_top') or + preload.get('meshes', [None])[0] + ) + old_map.node.color_texture = preload['tex'] + old_map.node.collision_mesh = preload['collision_mesh'] + old_map.node.materials = ( + [shared.footing_material, preload['ice_material']] + if 'ice_material' in preload + else [shared.footing_material] + ) + if 'mesh_bottom' in preload or 'bottom_mesh' in preload: + old_map.bottom = bs.newnode('terrain', attrs={ + 'mesh': preload.get('mesh_bottom') or preload.get('bottom_mesh'), + 'lighting': False, 'color_texture': preload['tex'] + }) + if 'meshes' in preload and len(preload['meshes']) > 1: + mats = ( + [shared.footing_material, preload['ice_material']] + if 'ice_material' in preload else [shared.footing_material] + ) + old_map.floor = bs.newnode('terrain', attrs={ + 'mesh': preload['meshes'][1], + 'color_texture': preload['tex'], + 'opacity': 0.92, + 'opacity_in_low_or_medium_quality': 1.0, + 'materials': mats + }) + if 'meshes' in preload and len(preload['meshes']) > 2: + old_map.stands = bs.newnode('terrain', attrs={ + 'mesh': preload['meshes'][2], + 'visible_in_reflections': False, + 'color_texture': preload.get('stands_tex', preload['tex']) + }) + if 'mesh_bg' in preload or 'bgmesh' in preload: + old_map.background = bs.newnode('terrain', attrs={ + 'mesh': preload.get('mesh_bg') or preload.get('bgmesh'), + 'lighting': False, 'background': True, + 'color_texture': preload.get('mesh_bg_tex') or preload.get('bgtex') + }) + if 'bgmesh2' in preload: + old_map.bg2 = bs.newnode('terrain', attrs={ + 'mesh': preload['bgmesh2'], + 'lighting': False, 'background': True, + 'color_texture': preload.get('bgtex2', preload.get('tex')) + }) + if 'railing_collision_mesh' in preload or 'bumper_collision_mesh' in preload: + old_map.railing = bs.newnode('terrain', attrs={ + 'collision_mesh': ( + preload.get('railing_collision_mesh') or + preload.get('bumper_collision_mesh') + ), + 'materials': [shared.railing_material], + 'bumper': True + }) + if 'collide_bg' in preload: + old_map.bg_collide = bs.newnode('terrain', attrs={ + 'collision_mesh': preload['collide_bg'], + 'materials': [ + shared.footing_material, + preload.get('bg_material'), + shared.death_material + ] + }) + if 'stem_mesh' in preload: + old_map.stem = bs.newnode('terrain', attrs={ + 'mesh': preload['stem_mesh'], + 'lighting': False, 'color_texture': preload['tex'] + }) + if 'player_wall_collision_mesh' in preload and isinstance(bs.getsession(), bs.CoopSession): + old_map.player_wall = bs.newnode('terrain', attrs={ + 'collision_mesh': preload['player_wall_collision_mesh'], + 'affect_bg_dynamics': False, + 'materials': [preload['player_wall_material']], + }) + old_map.preloaddata = preload + old_map.defs = cls.defs + old_map.is_hockey = ( + hasattr(cls, 'is_hockey') or + cls.name in ['Hockey Stadium', 'Lake Frigid'] + ) + old_map.is_flying = cls.name == 'Happy Thoughts' + gnode = _act.globalsnode + gnode.area_of_interest_bounds = ( + old_map.get_def_bound_box('area_of_interest_bounds') or + (-1, -1, -1, 1, 1, 1) + ) + bs.set_map_bounds( + old_map.get_def_bound_box('map_bounds') or + (-30, -10, -30, 30, 100, 30) + ) + + # old_map keeps its cached point lists from whichever map it was + # first built for (Map.__init__ only computes these once) -- with + # defs swapped to the new map's defs those lists are now stale, + # so recompute them the same way Map.__init__ does or players will + # spawn/teleport into the old map's (possibly void) coordinates. + old_map.spawn_points = old_map.get_def_points('spawn') or [(0, 0, 0, 0, 0, 0)] + old_map.ffa_spawn_points = old_map.get_def_points('ffa_spawn') or [(0, 0, 0, 0, 0, 0)] + old_map.spawn_by_flag_points = old_map.get_def_points('spawn_by_flag') or [(0, 0, 0, 0, 0, 0)] + flag_pts = old_map.get_def_points('flag') or [(0, 0, 0)] + old_map.flag_points = [p[:3] for p in flag_pts] + old_map.flag_points_default = old_map.get_def_point('flag_default') or (0, 1, 0) + pu_pts = old_map.get_def_points('powerup_spawn') or [(0, 0, 0)] + old_map.powerup_spawn_points = [p[:3] for p in pu_pts] + old_map.tnt_points = [p[:3] for p in (old_map.get_def_points('tnt') or [])] + + # Teleport every live player to a fresh spawn point on the new + # map so nobody is left standing over what is now empty space. + for i, p in enumerate(_act.players): + if p.actor and p.actor.node.exists(): + pos = old_map.get_start_position(i) + p.actor.node.handlemessage(bs.StandMessage(pos, 0)) + + # Also StandMessage every bot spawned via Coolbox (tracked in + # activity.customdata), so they don't stay stuck in the void. + for i, v in enumerate(_act.customdata.values()): + if isinstance(v, Bot) and v.node and v.node.exists(): + pos = old_map.get_start_position(i) + v.node.handlemessage(bs.StandMessage(pos, 0)) + +@NEW +class About: + """About Coolbox""" + def __init__(s, *a): + w, x, y = a[0], a[1], a[2] + + tw( + p=w, pos=(x/2, y-170), size=(0,0), text='COOLBOX', + h_align='center', v_align='center', scale=3.2, + color=(0.1,0.7,1), big=True + ) + + tw( + p=w, pos=(x/2, y-270), size=(0,0), text='version 1.0', + h_align='center', v_align='center', scale=0.9, + color=var('t') + ) + + tw( + p=w, pos=(x/2, y-305), size=(0,0), text='made with love and tea', + h_align='center', v_align='center', scale=0.9, + color=var('t') + ) + + tw( + p=w, pos=(x/2, y-355), size=(0,0), text='special thanks to: YOU for trying coolbox!', + h_align='center', v_align='center', scale=0.8, maxwidth=x*0.85, + color=(1,0.85,0.3) + ) + +# Dynamic Resources +# Stored as callabes and only called when needed +# Very beneficial for performance and memory +def D(): d = bui.app.classic.spaz_appearances; [d.pop(i) for i in d.copy() if i != 'Pascal' and d[i].default_color == (0.3,0.5,0.8)]; return d +_INIT_CWD = os.getcwd() +def BASE(): + app_py_dir = getattr(bui.app.env, "python_directory_app", None) + if app_py_dir: + base_from_app = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(app_py_dir))), + "ba_data" + ) + if os.path.exists(base_from_app): + return base_from_app + + base_from_cwd = os.path.join(_INIT_CWD, "ba_data") + if os.path.exists(base_from_cwd): + return base_from_cwd + + return os.path.join( + os.path.dirname(bui.app.env.cache_directory), + "ballistica_files", + "ba_data" + ) +#BASE = lambda: os.path.join(os.path.dirname(bui.app.env.cache_directory),'ballistica_files','ba_data') +NAME = lambda: list(D()) +SPAZ = lambda: list(D().values()) +KIDS = lambda: [i for i in bs.getnodes() if i.getnodetype() == 'spaz'] +ALL = lambda: sorted([i[:-4] for i in os.listdir(os.path.join(BASE(),'textures'))]) +AUDIO = lambda: sorted([i[:-4] for i in os.listdir(os.path.join(BASE(),'audio'))]) +MESH = lambda: sorted([i[:-4] for i in os.listdir(os.path.join(BASE(),'meshes'))]) +ICONS = lambda: [i.icon_texture for i in SPAZ()] +CTEX = lambda: [i.color_texture for i in SPAZ()] +def DIR(t): a = dir(SPAZ()[0]); b = []; [b.append(i) if i.endswith('_'+t) else None for i in a]; return b +CONS = lambda: [ + {lambda:getme(1):lambda:btw('Join the game first!')}, + {lambda:not pause():lambda:btw('Resume the game first!')} +] +def BRUH(): mem = SPAZ(); return [[mem[i].style for i in [8,11,12,13]],mem[1].style] + +# String Resources +# Stored in disk, not in memory +# Lambda is called on runtime when needed +COLS = lambda: ['Main','Highlight','Name'] +WHAT = lambda: ['Character','Colors','Sound','Mesh','Position','Name','Health'] +BAD = lambda: ['punch_velocity','punch_position','punch_momentum_linear'] +HOLDABLE = lambda: ['prop','spaz','flag'] +ZZZ = lambda: [ + 'zzz', + 'I sleep', + 'naah bro I\'m out', + 'Goonight', + 'I\'ll lie down here a bit', + 'U know what? zzz', + 'Eyes closed', + 'Do not disturb', + 'I\'ll take a nap', + 'Sleep. Here and now.', + 'x y zzzzzz...', + 'Good feeling, sleeping is.', + 'Too much for one day. I sleep.', + 'No. Just no. gn.', + 'Don\'t wake me up.', + 'Z for Zleep. zzz.' +] +SLOWDOWN = lambda: [ + 'Take it easy blud', + 'Cool down pal', + 'At least look at it', + 'You don\'t have to spam', + 'But you just clicked me', + 'OK OK just stop spamming', + 'Slow down bullet-kun', + 'You like the sound huh?', + 'I disencourage spamming', + 'Nou I just randomized', + 'The cooldown is 0.2 sec, and you surpassed it', + 'You can\'t wait 0.2 seconds can you?', + 'Wait 0.2 seconds I know that\'s a lot to ask', + 'Go to spammer jail', + 'If you really like the sound then go play it in Listen menu', + 'Dun (that means cool down)', + 'Too fast this is.', + 'Hello bullet I\'m cool down', + 'Knock knock - no, cool down.', + 'So you think clicking faster is better?', + 'The delay between your clicks is less than 200ms\nYou must be proud', + 'Keep spamming, and I will keep blocking.', + 'Too fast eh', + 'A 0.2s delay is too much to ask?' +] +HOLDSELF = lambda: [ + 'You sure like holding yourself lmfao', + 'You look funny that way', + 'Holding your own neck LOL', + 'Is that a new form of suicide', + 'What are you doing lmao' +] +CONSTR = lambda: [ + 'Yes!', + 'Where should I go?', + 'Let\'s get this done', + 'Yessir', + 'Just don\'t get me killed', + 'What next?', + 'Initiate!', + 'Why me?', + 'Can\'t you pick someone else', + 'Fine. Where are we going?', + 'Ready as ever!', + 'Sir yessir!', + 'I don\'t have a good feeling about this', + 'Ready for some moves!', + 'You move, I move.', + 'Controls linked!' +] +NAH = lambda: [ + "You're not the host!", + "Nice try, but you're not in charge here.", + "Host privileges required.", + "This doesn't open for just anyone.", + "Sorry, locked for non-hosts.", + "Access denied. Not yours to open.", + "Only the host gets to play with the cool toys.", + "You're not cool enough (not the host).", + "Error 403: Hosts only.", + "Host status required.", + "Back away, citizen.", + "You don't have the key.", + "Host-exclusive. Sorry!", + "You need host powers for this.", + "Refuses to cooperate with non-hosts.", + "Nice try. Host only though!", + "Secrets are for the host's eyes only.", + "Host authentication failed.", + "You're missing the magic word: Host permissions.", + "*clicks shut* Not for you!", + "Only responds to the host.", + "Permission denied. Host privileges not detected.", + "This isn't for everyone... just the host.", + "You wish you could open this, huh?", + "Nope! Host access only.", + "Admin powers not found.", + "Unauthorized. Move along.", + "The host would disapprove.", + "Not happening without host status." +] + +# Config +# Our database is babase.app.config +def var(s, v=None, cb=True): + cfg = bui.app.config + if cb: s = 'cb_'+s + if v is None: return cfg.get(s,v) + else: + cfg[s] = v + cfg.commit() +def con(n,v): var(n,v) if var(n) is None else None +def reset_conf(): cfg = bui.app.config; [(cfg.pop(c) if c.startswith('cb_') else None) for c in cfg.copy()]; cfg.commit() + +# Patches +# These few lines save huge amounts of lines later +f = ba.PluginSubsystem.on_screen_size_change; ba.PluginSubsystem.on_screen_size_change = lambda *a,**k: (icw.on_resume(),f(*a,**k)) +f = ba.PluginSubsystem.on_ui_scale_change; ba.PluginSubsystem.on_ui_scale_change = lambda *a,**k: (icw.on_resume(),f(*a,**k)) +gos = lambda: bui.get_special_widget("overlay_stack") +chk = lambda *a,**k: (k.update({'color':var('bg'),'textcolor':var('t')}),bui.checkboxwidget(*a,**k))[1] +def bw(*a,cons={},oac=None,pos=None,p=None,**k): + if cons: + o = oac + oac = lambda: None if None in [True if c() else [cons[c](),None][1] for c in cons] else o() + None if len(a) else k.update({ + 'label':k.get('label',''), + 'button_type':k.get('button_type','square'), + 'color':k.get('color',var('bg')), + 'textcolor':k.get('textcolor',var('t')), + 'enable_sound':k.get('nable_sound',False) + }) + if pos: k['position'] = pos + if p: k['parent'] = p + if oac: k['on_activate_call'] = oac + return bui.buttonwidget(*a,**k) +def cw(*a,**k): + only(k,'color',var('bg')) + only(k,'parent', gos()) + return bui.containerwidget(*a,**k) +def tw(*a,p=None,pos=None,**k): + only(k,'color',var('t')) + if p: k['parent'] = p + if pos: k['position'] = pos + return bui.textwidget(*a,**k) +def pbw(*a,tex=None,**k): + b = bw(*a,**k) + if tex: bw(b,tint_color=var(tex[0]) or (1,1,1),tint2_color=var(tex[1]) or (1,1,1)) + bw(b,color=(1,1,1),mask_texture=bui.gettexture('characterIconMask')) + return b +def sbw(*a,**k): + k.update({ + 'label':k.get('label',''), + 'size':k.get('size',(43,43)), + 'iconscale':k.get('iconscale',1.2), + 'text_scale':k.get('text_scale',1.4) + }) + b = bw(*a,**k) + return b + +# Mini tools +# May look dirty, but they're extremely useful +err = lambda t: (bui.getsound('error').play(),bs.broadcastmessage(t,color=(1,0,0))) +btw = lambda t: (bui.getsound('block').play(),bs.broadcastmessage(t,color=(1,1,0))) +darken = lambda c: (c[0]/2,c[1]/2,c[2]/2) +gc = lambda w: w.get_screen_space_center() +smol = lambda c: [i/255 for i in c] +huge = lambda c: [int(i*255) for i in c] +gun = lambda: bui.getsound('gunCocking').play() +ding = lambda: bui.getsound('dingSmallHigh').play() +UUID = lambda: str(uuid.uuid4())[:5] +def GA(f): + with bs.get_foreground_host_activity().context: return f() + +# CJK-packed seed system (ported from movi.py) - denser than plain digit strings +CJK_SEED_RANGES = ( + (0x3400, 0x4DBF), # CJK Unified Ideographs Extension A + (0x4E00, 0x9FFF), # CJK Unified Ideographs (basic) + (0xF900, 0xFAFF), # CJK Compatibility Ideographs + (0x2F800, 0x2FA1F), # CJK Compatibility Ideographs Supplement + (0x20000, 0x2A6DF), # CJK Unified Ideographs Extension B + (0x2A700, 0x2B73F), # CJK Unified Ideographs Extension C + (0x2B740, 0x2B81F), # CJK Unified Ideographs Extension D + (0x2B820, 0x2CEAF), # CJK Unified Ideographs Extension E + (0x2CEB0, 0x2EBEF), # CJK Unified Ideographs Extension F + (0x2EBF0, 0x2EE5F), # CJK Unified Ideographs Extension I + (0x30000, 0x3134F), # CJK Unified Ideographs Extension G + (0x31350, 0x323AF), # CJK Unified Ideographs Extension H + (0x323B0, 0x3347F), # CJK Unified Ideographs Extension J +) +def _cjk_build_table(): + table = [] + cum = 0 + for lo,hi in CJK_SEED_RANGES: + size = hi-lo+1 + table.append((cum,cum+size-1,lo)) + cum += size + return table,cum +CJK_SEED_TABLE,CJK_SEED_BASE = _cjk_build_table() +def cjk_digit_to_char(d): + for start,end,lo in CJK_SEED_TABLE: + if start <= d <= end: + return chr(lo+(d-start)) + raise ValueError(f'seed digit out of range: {d}') +def cjk_char_to_digit(c): + cp = ord(c) + cum = 0 + for lo,hi in CJK_SEED_RANGES: + size = hi-lo+1 + if lo <= cp <= hi: + return cum+(cp-lo) + cum += size + raise ValueError(f'not a valid seed character: {c!r}') +def cjk_encode_int(n): + if n == 0: return cjk_digit_to_char(0) + digits = [] + while n: + n,r = divmod(n,CJK_SEED_BASE) + digits.append(r) + return ''.join(cjk_digit_to_char(d) for d in reversed(digits)) +def cjk_decode_int(s): + n = 0 + for c in s: + n = n*CJK_SEED_BASE + cjk_char_to_digit(c) + return n +def ENCODE(a): + c=zlib.compress(json.dumps(a,separators=(',',':'),sort_keys=True).encode('utf-8')) + return cjk_encode_int(int.from_bytes(c,'big')) +def DECODE(s): + n=cjk_decode_int(s) + b=n.to_bytes((n.bit_length()+7)//8,'big') + return json.loads(zlib.decompress(b).decode('utf-8')) +hasm = lambda: bui.app.ui_v1.has_main_window() +c2h = lambda c: '#{:04x}{:04x}{:04x}'.format(*huge(c)) +DLG = lambda n: n.getdelegate(object) +def h2c(h): + h = h.lstrip('#') + return smol([int(h[i:i+4],16) for i in [0,4,8]]) +def push(t,**k): + v = var('lpush') or [] + if t in v: t = f'{t} [x{v[1]}]'; v = [v[0],v[1]+1] + else: v = [t,2] + var('lpush',v) + bs.broadcastmessage(t,top=True,**k) +def UIS(j=0): + i = bui.app.ui_v1.uiscale + return [[[2,1][i==bui.UIScale.MEDIUM],0][i==bui.UIScale.SMALL],i][j] +def only(k,v,d): k[v] = r = k.get(v,d); return r +def forbtn(w,b,xoff=0): + p = gc(b) + cw(w, scale_origin_stack_offset=(p[0]-xoff,p[1])) +def PASTE(): + try: return ba.clipboard_get_text() + except: pass +def CHECK(v): d = DECODE(v); d[4]; return d +def RANDOM(): + mem = NAME() + n = random.choice(mem) + c = [[round(random.uniform(0,2),2) for i in range(3)] for i in range(3)] + return SEED(n,n if n != mem[0] else random.choice(bs.get_random_names()),tuple(c[0]),tuple(c[1]),tuple(c[2]),fix=n) +def getme(actor=0): + for p in bs.get_foreground_host_activity().players: + if p.sessionplayer.inputdevice.client_id == -1: + return p.actor if actor else p +def RESUME(): + u = bui.app.ui_v1 + c = bui.app.classic + c.resume() + u.clear_main_window() + [z() for z in c.main_menu_resume_callbacks] + c.main_menu_resume_callbacks.clear() +def npause(i=None): bs.get_foreground_host_activity().globalsnode.paused = i if i is not None else var('npp') +def pause(i=None): + g = bs.get_foreground_host_activity().globalsnode + if i is None: return g.paused + g.paused = i +def FOCUS(p,i,node=0): + FOC = 0 + def f(): + nonlocal FOC + if FOC: return + try: _ba.set_camera_target(*(p.position if node else p)) + except: return + bui.apptimer(0.01,f) + def g(): + nonlocal FOC + FOC = 1 + f(); bui.apptimer(i, g) +def SPARK(p): + with bs.get_foreground_host_activity().context: + SND('ding',p) + bs.emitfx(position=p, + scale=1, + count=70, + chunk_type='spark') +def MESS(p): + with bs.get_foreground_host_activity().context: + SND('shatter',p) + bs.emitfx( + position=p, + scale=1, + count=30, + spread=0.1, + chunk_type='ice' + ) +def LOOK(p,on_found=lambda: None): + d = math.dist(p,_ba.get_camera_target())/17 + h = bui.getsound('shieldUp') + FOCUS(p,2) + h.play() if d > 0.3 else None + bui.apptimer(d, lambda: (None if pause() else SPARK(p), h.stop(), on_found())) +def getpos(): return rnd(tuple([float(var(f'pos{i}')) for i in range(3)])) +def rnd(p): return tuple([round(i,3) for i in p]) +def SND(s,p,v=3): + with bs.get_foreground_host_activity().context: bs.getsound(s).play(v,position=p) +def STATE(b=None): + if b is None: return var('active') + var('active',b) +def fixsounds(c=None,prf=''): + c = c or var(f'{prf}char') + n = NAME() + [var(f'{prf}sound{i}',[n.index(c),0]) for i in range(len(DIR('sounds')))] +def fixmesh(c=None,prf=''): + c = SPAZ()[NAME().index(c or var(f'{prf}char'))] + mem = DIR('mesh'); z = range(len(mem)) + [var(f'{prf}mesh{i}',getattr(c,mem[i])) for i in z] + var(f'{prf}ctex',c.color_texture) + var(f'{prf}ctex2',c.color_mask_texture) +def fixall(c=None,prf=''): fixmesh(c,prf); fixsounds(c,prf) +def get_ctex(c): return SPAZ()[NAME().index(c)].color_texture +def get_ctex2(c): return SPAZ()[NAME().index(c)].color_mask_texture +def set_sounds(n,prf='',dr=None,aud=None,spa=None): + a = dr or DIR(f'sounds') + aud = aud or AUDIO() + spa = spa or SPAZ() + for i in range(len(a)): + j = var(f'{prf}sound{i}') + if not j: continue + if j[1]: v = [bs.getsound(aud[j[0]])] + else: v = [bs.getsound(k) for k in getattr(spa[j[0]],a[i])] + setattr(n,a[i],v) +def mesh_seed(): + r = [] + mem = SPAZ()[NAME().index(var('char'))] + ok = DIR('mesh') + for i in range(len(ok)): + c = var(f'mesh{i}') or getattr(mem,ok[i]) + r.append(c) + return r+[var('ctex') or get_ctex(var('char')),var('ctex2') or get_ctex2(var('char'))] +def sound_seed(): return [var(f'sound{i}') for i in range(len(DIR('sounds')))] +def SEED(char,tchar,main,hl,name,fix=None): fixall(fix) if fix else None; return ENCODE([char,tchar,main,hl,name,mesh_seed(),sound_seed()]) +def LN(d,o=None): me = o or getme(); [me.assigninput(getattr(ba.InputType,k), d[k]) for k in d] +def GSW(t): return _ba.get_string_width(t,suppress_warning=True) +def GETSIG(o): + try: return f"Supported args: {list(inspect.signature(o).parameters.keys())}" + except: return 'No signature found :(' +def NTEX(n,tex=None): + t = str(getattr(n,'color_texture','"black"')) + f = t.find('"')+1 + c = t[f:t.find('"',f)].replace('Color','') + b = c+'Icon' in (tex or ALL()) + return { + 'texture':bui.gettexture(c+['','Icon'][b]), + 'tint_texture':bui.gettexture(c+['','IconColorMask'][b]), + 'tint_color':getattr(n,'color',(1,1,1)), + 'tint2_color':getattr(n,'highlight',(1,1,1)) + },getattr(n,'name_color',(1,1,1)),c +def MK(ch,f,g): + j = {'tint_color':var(f)} + return [{ + 'tint2_color':var(g), + 'mask_texture':bui.gettexture('characterIconMask'), + 'tint_texture':bui.gettexture(ch.icon_mask_texture), + 'texture':bui.gettexture(ch.icon_texture), + **j + },{'texture':bui.gettexture('buttonSquare'),**j}] +def clone(n,a): + sp = n.source_player + b = bs_playerspaz.PlayerSpaz(player=sp) if sp else Bot(NAME()[0]) + bn = b.node + a.customdata[UUID()] = b + mem = BAD() + for i in dir(bn): + if i.startswith('_') or i in mem: continue + try: j = getattr(n,i) + except AttributeError: continue + if callable(j): continue + try: setattr(bn,i,j) + except RuntimeError: continue + p = n.position + p = (p[0],p[1]-0.6,p[2]) + b.handlemessage(bs.StandMessage(p,0)) + if sp: + b.connect_controls_to_player() + sp.actor = b + print (sp.actor == b, sp.actor.node == bn) + return [sp,bn] +def wga(f): + with bs.get_foreground_host_activity().context: f() + +# Init +# Our default values on first run +c = NAME()[0] +d = { + 'first': True, + 'char':c, + 'tchar':c, + 'mchar':c, + 't':(1,1,1), + 'bg':(0.13,0.13,0.13), + 'act':[], + 'egoto':'1', + 'wait':'1', + 'smove':'1.1', + 'sfollow':'1.1', + 'tmove':'10', + 'tfollow':'10', + 'goto':'1', + 'icall':0, + 'uis':None, + 'npp':1, + 'say':'hi please kill me', + 'tsay':'3', + 'dplprop':'prop', + 'dplname':'', + 'cconf3':1 +}; [con(i,d[i]) for i in d] +for i in range(3): + con(f'pos{i}','0') + con(f'mpos{i}','0') + con(f'move{i}','0') + con(f'dpl{i}','0') + con(f'bld{i}','0') + con(f'cconf{i}',[1,1,0,1][i]) +[con(f'cont{i}',True) for i in range(5)] +[con(f'what{i}',True) for i in range(len(WHAT()))] +var('active',False) +var('lpush','') +if var('first'): var('first',False); fixall(prf='m') +del c,d + +# brobord collide grass +# ba_meta require api 9 +# ba_meta export babase.Plugin +class byBordd(ba.Plugin): + def __init__(s): + og = bui_igm.InGameMenuWindow._refresh_in_game + def entry(s,*a,**k): + r = og(s,*a,**k) + w = bw( + p=s._root_widget, + pos=(-80, s._height-50), + label="Coolbox", + icon=bui.gettexture('chestIcon'), + iconscale=0.8, + scale=1.0, + size=(100, 40) + ) + bw(w, oac=lambda: Coolbox(fresh=True, in_source=w)) + return r + bui_igm.InGameMenuWindow._refresh_in_game = entry