diff --git a/README.md b/README.md index a7bf1d9..ba5bad3 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # Bombsquad-Ballistica-Modded-Server -Modder server scripts to host ballistica (Bombsquad).Running on BS1.6.6. +Modder server scripts to host ballistica (Bombsquad).Running on BS1.7.2. ## Requirements - Ubuntu 20 -- python3.9 +- python3.10 ## Getting Started -- `sudo apt update; sudo apt install python3.9-dev` +- `sudo apt update; sudo apt install python3-pip python3.10-dev python3.10-venv` , or [Python3.10 on Ubuntu 20.04](https://computingforgeeks.com/how-to-install-python-on-ubuntu-linux-system/) - `tmux new -s 43210` - `git clone https://github.com/imayushsaini/Bombsquad-Ballistica-Modded-Server` - `cd Bombsquad-Ballistica-Modded-Server` @@ -39,9 +39,22 @@ Here you can ban player , mute them , disable their kick votes - Restrict some player to start kick vote. - Auto night mode . - Transparent Kickvote , can see who started kick vote for whom. -- Kickvote msg to chat/screen , can choose to show kickvote start msg either as screen message or chat message +- Kickvote msg to chat/screen , can choose to show kickvote start msg either as screen message or chat message. +- Players IP Address and Device UUID tracking and banning. +- Team Chat, send msg starting with (,) comma to deliver it to team mates only. +- In game popup chat , send msg starting with (.) Dot to send in game popup msg. +- End Vote System , type `end` in chat to start end vote. +- support for [Ballisitca-web-stats](https://github.com/imayushsaini/ballistica-web-stats). +- Integrated Discord bot to sync live stats(current players, chats , all logs) to discord. +- Execute chat command remotely from discord. +- Many New mini games and maps. +- Colourfull bomb explosion. +- Floater +- Auto stats reset after configured days . +- Auto remove afk/idle players. +- Auto check server updates. - All settings at one place ,no coding exp. required just edit settings.json -- Configurable Server Host name +- Configurable Server Host name. - Character chooser , players can choose any character while joining . - Restrict New accounts to join or chat in server. - Custom characters , easy to load and use characters made by character maker. @@ -49,5 +62,6 @@ Here you can ban player , mute them , disable their kick votes - Integrated ElPatronPowerups. - Auto switch to coop mode when players are less then threshold. - Change playlist on fly with playlist code or name , i.e /playlist teams , /playlist coop , /playlist 34532 -- New Splitted Team score screen. +- New Splitted Team in game score screen. +- New final score screen , StumbledScoreScreen. - other small small feature improvement here there find yourself. diff --git a/config.yaml b/config.yaml index 49a4c9d..903e89c 100644 --- a/config.yaml +++ b/config.yaml @@ -48,7 +48,7 @@ max_party_size: 6 # playlist editor in the regular version of the game. # This will give you a numeric code you can enter here to host that # playlist. -playlist_code: 347990 +playlist_code: 374313 # Alternately, you can embed playlist data here instead of using codes. # Make sure to set session_type to the correct type for the data here. diff --git a/dist/ba_data/data/langdata.json b/dist/ba_data/data/langdata.json index bf5e68c..088d16a 100644 --- a/dist/ba_data/data/langdata.json +++ b/dist/ba_data/data/langdata.json @@ -50,6 +50,7 @@ "5PH3X", "99", "@sametsunal", + "_DraXX", "_Fami", "Omar a", "Bruno A.", @@ -96,6 +97,7 @@ "ahmed", "ahmedzabara", "Collin Ainge", + "Akash", "Akbar", "Bekir Akdemir", "Akhanyile", @@ -193,6 +195,7 @@ "Arin", "Arroz", "ARSHAD", + "ArshiyDLn", "Artem", "Valentino Artizzu", "Arxyma", @@ -236,6 +239,7 @@ "Bank", "Ibrahim Baraka", "Kamil Barański", + "Leonan Barcelos", "Bardiaghasedipour", "William Barnak", "William Barnakk", @@ -247,6 +251,7 @@ "Bashkot", "Matthias Bastron", "Ralph Bathmann", + "Bato", "Florian Bauernfeind", "David BAUMANN", "bayanus", @@ -272,6 +277,7 @@ "Anton Bang Berner", "Felix Bernhard", "Davide Bigotto", + "bilibili@Medic药", "Bima", "Biytremni", "Blackcat2960", @@ -306,6 +312,7 @@ "Brojas", "Brojasko", "BrotheRuzz11", + "Brunoazocar", "bsam", "Bsamhero", "BSODPK", @@ -314,6 +321,7 @@ "Buskebam", "Buto11", "ByAdrianYT", + "Semih Byzts", "Mohamad Hossein BZ", "Christoffer Bünner", "mvp aka cactus", @@ -374,11 +382,13 @@ "Cpt crook", "Prashanth CrossFire", "Cryfter", + "cuddles98", "cukomus", "CYCL0YT", "D", "Dada", "Daivaras", + "Dajo6596YT", "Dakkat", "Mikkel Damgaard", "Danco", @@ -395,6 +405,7 @@ "Dasto", "David", "Davide", + "DavidPlayzLol", "DavidPlayzloll", "DaymanLP", "Ddávid", @@ -408,6 +419,7 @@ "Dennys", "Alex Derbenew", "df", + "Santanu Dhar", "Guilherme Dias", "Diase7en", "ferbie Dicen", @@ -416,6 +428,7 @@ "dikivan2000", "Dimitriy", "Martin Dimitrov", + "DinoWattz", "Diprone", "djaber djafer", "Fadhil djibran", @@ -439,6 +452,8 @@ "Dudow", "Dustin", "Paul Duvernay", + "Emir İslam Dündar", + "E.R.A.L", "Ebutahapro07tr", "Edson", "Glen Edwards", @@ -452,6 +467,7 @@ "Rezk ElAdawy", "ElCatCaesar", "ElderLink", + "elfree", "Elian", "Elsans320_YT", "Elskoser", @@ -460,6 +476,7 @@ "Emil", "Kürti Emil", "Muhammed emir", + "Muhammet Emir", "EmirSametEr", "emm", "EnderDust123", @@ -487,6 +504,7 @@ "Jakub Fafek", "Syed Fahrin (Mr.Lemoyne)", "faizal.faiz.ms@gmail.com", + "Fakih", "FanDolz.", "Faqih", "Muhammad Faqih ''None''", @@ -681,6 +699,7 @@ "Itamar", "Ivan", "iViietZ", + "JaaJ", "Al jabbar", "Jacek", "Jack556", @@ -699,6 +718,7 @@ "Jembhut", "CrackerKSR (Kishor Jena)", "CrackerKSR (Kishor Jena))", + "Jenqa", "Jeroen", "jesus", "Jetty", @@ -721,6 +741,7 @@ "joke", "Jonatas", "Jop", + "JoseANG3L", "Joseetion", "Joseph", "Joshep", @@ -759,9 +780,11 @@ "Kaushik", "KawaiiON", "KD", + "Kejuxs", "Mani kelidari", "Kelmine", "Kenjie", + "Kerfix", "Kerim", "Khaild1717", "muh khairul", @@ -790,6 +813,7 @@ "Nikolay Korolyov", "Kostas", "Viktor Kostohryz", + "Kozmo909", "Mikhail Krasovsky", "kripanshu", "kris", @@ -842,7 +866,10 @@ "Linux44313", "LiteBalt", "LittleNyanCat", + "Lizz", + "Lizzetc", "Lkham", + "Lobinhofs", "Loex", "Loko", "Longkencok", @@ -860,6 +887,7 @@ "Ludicrouswizard", "satrio ludji", "Ludovico", + "Luis (GalaxyM4)", "Jose Luis", "luislinares", "luispro25", @@ -922,6 +950,7 @@ "Ihsan Maulana ( @ihsanm27)", "Federico Mazzone", "Andrea Mazzucchelli", + "Medic", "Medic别闹我有药", "German Medin", "Martin Medina", @@ -933,6 +962,7 @@ "Meryu07", "Meysam", "MGH", + "Davis Michelle", "Mick", "Miguel", "Miguelterrazas123", @@ -1092,12 +1122,14 @@ "pett", "Petulakulina", "Pez", + "PGIGM", "Đào Xuân Phi", "Philip", "Philipp", "piga", "Stefano Pigozzi", "Mario Donato Pilla", + "Pinchidino", "Danilo \"Logan\" Pirrone", "PivotStickfigure12", "Pixelcube", @@ -1254,6 +1286,7 @@ "ShadowQ", "shafay", "Manan Shah", + "shakesm", "Sharvesh", "Nalam Shashwath", "Haige Shi", @@ -1271,6 +1304,7 @@ "Igor Slobodchuk", "Rasim Smaili", "Nicola Smaniotto", + "smertfhg", "Nico Smit", "Snack", "Mahteus Soares", @@ -1307,6 +1341,7 @@ "sun.4810", "Samet Sunal", "sundar", + "Suprcat", "Indo sus", "Sven", "Shannon Sy", @@ -1315,6 +1350,7 @@ "Sz™", "Jorge Luis Sánchez", "Daniel Sýkora", + "Aleksandar Tadic", "Arung Taftazani", "taha", "Rasim Eren TAHMAZ", @@ -1380,6 +1416,7 @@ "Kontantin Tsvetkov", "Tudikk", "Jan Tymll", + "Zacker Tz", "Zoltán Tóth", "uDinnoo", "Cristian Ugalde", @@ -1549,6 +1586,7 @@ "مُحمَّد الأول", "البطل", "بسام البطل", + "رفيق العشي", "ابو العواصف2020", "عبدالرحمن النجم", "امیرعلی", @@ -1585,6 +1623,7 @@ "حسین وفایی‌فرد", "انا يا عمر انا بران يا عمر انا بران يا عمر انا بران يا عمر انا بران يا عمر انا بران يا عمر انا بران يا عمر انا بران يا عمر انا بران يا عمر انا بران يا عمر انا بران يا عمر انا بران يا", "١٢٣٤٥", + "٦٤٦٦٤٦٤٦", "علیرضا پودینه", "वेदाँश त्यागी", "അർഷഖ് ഹസ്സൻ", @@ -1618,6 +1657,7 @@ "神仙", "药药Medic", "蔚蓝枫叶", + "鲨鱼服·Medic", "鲲鹏元帅", "꧁ℤephyro꧂", "공팔이", diff --git a/dist/ba_data/data/languages/arabic.json b/dist/ba_data/data/languages/arabic.json index c3455df..f0c7a36 100644 --- a/dist/ba_data/data/languages/arabic.json +++ b/dist/ba_data/data/languages/arabic.json @@ -56,8 +56,8 @@ "Boxer": { "description": "فز بدون استخدامك للقنابل", "descriptionComplete": "لقد فزت بدون استخدام القنابل", - "descriptionFull": "قم بإكمال ${LEVEL} بدون استخدام أية قنابل", - "descriptionFullComplete": "اكمل ${LEVEL} بدون استخدام أية قنابل", + "descriptionFull": "بدون استخدام اي قنابل${LEVEL} قم بإكمال", + "descriptionFullComplete": "دون استخدام اي قنابل${LEVEL} اكمل", "name": "مُلاكِمْ" }, "Dual Wielding": { @@ -66,10 +66,10 @@ "name": "اللكمة المزدوجة" }, "Flawless Victory": { - "description": "انتصر بدون تعرض للأذى", + "description": "انتصر بدون التعرض للأذى", "descriptionComplete": "لقد فزت بدون تعرضك للأذى", - "descriptionFull": "انتصر في ${LEVEL} بدون تعرضك للأذى", - "descriptionFullComplete": "لقد فزت في ${LEVEL} بدون تعرضك للأذى", + "descriptionFull": "دون تعرضك للأذى${LEVEL} فُزْ", + "descriptionFullComplete": "دون تعرضك للأذى ${LEVEL} لقد فُزتَ", "name": "الفوز المستحق" }, "Free Loader": { @@ -89,7 +89,7 @@ "descriptionComplete": "لقد انتصرت بدون استخدامك للكمات او القنابل", "descriptionFull": "بدون استخدام اللكمات أو القنابل ${LEVEL} فز في", "descriptionFullComplete": "بدون استخدام اللكمات أو القنابل ${LEVEL} لقد ربحت في", - "name": "الأسلحة المخفية" + "name": "حصلت على الحركات" }, "In Control": { "descriptionFull": "(قم بتوصيل جهاز تحكم (جهاز أو تطبيق", @@ -136,7 +136,7 @@ "descriptionComplete": "!لقد سجلت 5000 نقطة", "descriptionFull": "${LEVEL} سجِّل 5000 نقطة في", "descriptionFullComplete": "${LEVEL} لقد سجَّلتَ 5000 نقطة في", - "name": "إله ${LEVEL}" + "name": "زعيم ${LEVEL}" }, "Onslaught Master": { "description": "سجل 500 نقطة", @@ -160,10 +160,10 @@ "name": "${LEVEL} ساحر" }, "Precision Bombing": { - "description": "powerups فز بدون اي", - "descriptionComplete": "powerups لقد فزتَ بدون أي", - "descriptionFull": "power-ups بدون اي ${LEVEL} فز في", - "descriptionFullComplete": "power-ups بدون اي ${LEVEL} لقد فزتَ في", + "description": "فُزْ بدون اي قوى خارقة", + "descriptionComplete": "لقد فزتَ بدون أي قوى خارقة", + "descriptionFull": "بدون اي قوى خارقة${LEVEL} فز في", + "descriptionFullComplete": "بدون اي قوى خارقة${LEVEL} لقد فزتَ في", "name": "دقة القصف" }, "Pro Boxer": { @@ -218,7 +218,7 @@ "Rookie Onslaught Victory": { "description": "هزيمة كل الجولات", "descriptionComplete": "هزم كل الجولات", - "descriptionFull": "${LEVEL} اكسب كل الجولات في", + "descriptionFull": "${LEVEL} اهزم كل الجولات في", "descriptionFullComplete": "${LEVEL} هزم كل الجولات في", "name": "${LEVEL} لقد انتصرت" }, @@ -250,9 +250,9 @@ }, "Stayin' Alive": { "description": "فز بدون أن تموت", - "descriptionComplete": "فاز بدون أن يموت", + "descriptionComplete": "لقد فُزت بدون ان تموت", "descriptionFull": "بدون أن تموت ${LEVEL} فز", - "descriptionFullComplete": "بدون أن يموت ${LEVEL} فاز", + "descriptionFullComplete": "بدون أن يموت ${LEVEL} لقد فزت", "name": "البقاء حيا" }, "Super Mega Punch": { @@ -265,8 +265,8 @@ "Super Punch": { "description": "إلحاق الضرر 50٪ بلكمة واحدة", "descriptionComplete": "ألحق الضرر 50٪ بلكمة واحدة", - "descriptionFull": "${LEVEL} إلحاق الضرر 50٪ بلكمة واحدة", - "descriptionFullComplete": "${LEVEL} ألحق الضرر 50٪ بلكمة واحدة", + "descriptionFull": "${LEVEL} إلحاق الضرر 50٪ بلكمة واحدة في", + "descriptionFullComplete": "${LEVEL} ألحق الضرر 50٪ بلكمة واحدة في", "name": "لكمة خارقة" }, "TNT Terror": { @@ -327,6 +327,7 @@ "achievementsRemainingText": "الإنجازات المتبقية", "achievementsText": "الإنجازات", "achievementsUnavailableForOldSeasonsText": "المعذرة، الإنجازات للمواسم القديمة غير متوفرة", + "activatedText": "${THING} تم تفعيله.", "addGameWindow": { "getMoreGamesText": "الحصول على المزيد من الألعاب", "titleText": "إضافة لعبة" @@ -625,7 +626,9 @@ "epicDescriptionFilterText": "${DESCRIPTION} بحركة ملحمية بطيئة", "epicNameFilterText": "الملحمي ${NAME}", "errorAccessDeniedText": "تم الرفض", + "errorDeviceTimeIncorrectText": "توقف وقت جهازك بمقدار ${HOURS} ساعة.\nهذا قد يتسبب بمشاكل.\nمن فضلك قم بالتحقق من اعدادات الوقت.", "errorOutOfDiskSpaceText": "انتهت مساحة التخزين", + "errorSecureConnectionFailText": "تعذر انشاء اتصال سحابي أمن; قد تفشل وظائف الشبكة.", "errorText": "خطا", "errorUnknownText": "خطا غير معروف", "exitGameText": "هل تريد الخروج من ${APP_NAME}؟", @@ -788,7 +791,7 @@ "ticketPack4Text": "جمبو تذكرة حزمة", "ticketPack5Text": "ماموث تذكرة حزمة", "ticketPack6Text": "تذكرة حزمة هائلة", - "ticketsFromASponsorText": "احصل على تذاكر ${COUNT}\nمن مقدم مشروع القرار", + "ticketsFromASponsorText": "شاهد اعلانا\nمن التذاكر ${COUNT} للحصول على", "ticketsText": "${COUNT} بطاقات", "titleText": "أحصل على تذاكر", "unavailableLinkAccountText": "عذرا، لا تتوفر عمليات الشراء على هذا النظام الأساسي.\nوكحل بديل، يمكنك ربط هذا الحساب بحساب في\nمنصة أخرى وجعل عمليات الشراء هناك.", @@ -799,6 +802,7 @@ "youHaveText": "لديك ${COUNT} تذاكر" }, "googleMultiplayerDiscontinuedText": "عذرًا ، خدمة جوجل متعددة اللاعبين لم تعد متاحة.\n أنا أعمل على بديل بأسرع وقت ممكن.\n حتى ذلك الحين ، يرجى تجربة طريقة اتصال أخرى.\n -إريك", + "googlePlayPurchasesNotAvailableText": "عمليات شراء جوجل بلاي غير متوفرة.\nقد تحتاج لتحديث تطبيق المتجر.", "googlePlayText": "جوجل بلاي", "graphicsSettingsWindow": { "alwaysText": "دائما", @@ -1109,7 +1113,10 @@ "playlistsText": "قائمة العب", "pleaseRateText": "يرجى اتخاذ لحظة وتقييمه ${APP_NAME} اذا كنت تستمتع بلعبة\nاو كتابة مراجعة فهاذا يوفر معلومات مفيدة ويوفر التطوير\nفي المستقبل\n\n!شكرًا\nاريك—", "pleaseWaitText": "الرجاء الانتظار . . .", - "pluginsDetectedText": "تم العتور على اضافات جديدة. فعلها او عدلها في الاعدادات", + "pluginClassLoadErrorText": "فشل في تشغيل الاضافة '${PLUGIN}': ${ERROR}", + "pluginInitErrorText": "خطأ في بدء الاضافة '${PLUGIN}: ${ERROR}", + "pluginsDetectedText": "تم العثور على اضافات جديدة. اعد التشغيل لتفعيلها، او قم بتخصيصها في الاعدادات.", + "pluginsRemovedText": "${NUM} لم تعد الاضافة موجودة.", "pluginsText": "اضافات", "practiceText": "تدريب", "pressAnyButtonPlayAgainText": "اضغط اي زر للعب مجددا...", @@ -1296,8 +1303,8 @@ "comingSoonText": "قريبا...", "extrasText": "إضافات", "freeBombSquadProText": "فرقة القنبلة الآن أصبحت مجانية، لكن بما أنك اشتريتها\nبطاقات كشكر لك ​${COUNT} ستتلقى فرقة القنبلة القنبلة للمحترفين و\n!استمتع بالميزات الجديدة، وشكرًا لدعمك\n-إيريك", - "holidaySpecialText": "عطلة خاصة", - "howToSwitchCharactersText": "(انتقل إلى \"${SETTINGS} -> ${PLAYER_PROFILES}\" لتعيين وتخصيص الأحرف)", + "holidaySpecialText": "خاص بالعطل", + "howToSwitchCharactersText": "(توجه الى \"${SETTINGS} -> ${PLAYER_PROFILES}\" لتخصيص الشخصيات)", "howToUseIconsText": "(إنشاء ملفات تعريف لاعب العالمية (في إطار الحساب) لاستخدام هذه)", "howToUseMapsText": "(استخدم هذه الخرائط في فرقك الخاصة / قوائم التشغيل المجانية للجميع)", "iconsText": "رموز", @@ -1321,7 +1328,7 @@ "winterSpecialText": "عرض الشتاء", "youOwnThisText": "- انت تملك هذا -" }, - "storeDescriptionText": "8 لاعب حفله لعبة الجنون!\n\nتفجير أصدقائك (أو الكمبيوتر) في البطولة من الألعاب المصغرة المتفجرة مثل القبض على العلم، منفذها هوكي، وملحمة بطيئة الحركة الموت الموت!\n\nضوابط بسيطة ودعم وحدة تحكم واسعة تجعل من السهل لمدة تصل إلى 8 أشخاص للحصول على في العمل. يمكنك حتى استخدام الأجهزة النقالة الخاصة بك عن طريق التحكم عن طريق الحرة 'بومبسكاد البعيد' التطبيق!\n\nالقنابل بعيدا!\n\nتحقق من www.froemling.net/bombsquad لمزيد من المعلومات.", + "storeDescriptionText": "لعبة لأكثر من 8 لاعبين!\n\nالعب مع اصدقائك (او الحاسوب) في بطولات من الميني جيمز المتفجرة كــإمساك بالعلم، الهوكي و المعركة البطيئة!\n\nتحكم بسيط و لعب باجهزة تحكم يجعلها سهلة لأكثر من 8 لاعبين ليدخلو المعركة; تستطيع ايضا استعمال اجهزة الهاتف كاجهزة تحكم من خلال التطبيق المجاني 'BombSquad Remote' !\n\nوقت رمي القنابل!\n\nتفقد www.froemling.net/bombsquad للمزيد من المعلومات.", "storeDescriptions": { "blowUpYourFriendsText": ".فجر أصدقائك", "competeInMiniGamesText": "تنافس في الألعاب المصغرة بدءا من السباق للطيران.", @@ -1364,31 +1371,31 @@ "translations": { "characterNames": { "Agent Johnson": "العميل جونسون", - "B-9000": "الآلي الخارق", - "Bernard": "الدب القطبي", + "B-9000": "B-9000", + "Bernard": "بيرنارد", "Bones": "هيكل عظمي", "Butch": "بوتش", - "Easter Bunny": "أرنوب", + "Easter Bunny": "أرنب عيد الفصح", "Flopsy": "فلوبسي", - "Frosty": "مُقاتل ثلجي", + "Frosty": "فروستي", "Gretel": "جريتل", - "Grumbledorf": "المشعوذ", - "Jack Morgan": "خير الدين بارباروسا", - "Kronk": "عدنان", + "Grumbledorf": "الساحر", + "Jack Morgan": "جاك مورجان (قرصان)", + "Kronk": "كرونك", "Lee": "لي", "Lucky": "سعيد الحظ", - "Mel": "الطباخ", + "Mel": "ميل", "Middle-Man": "الرجل المتوسط", "Minimus": "أدنى لا", - "Pascal": "البطريق الكبير", - "Pixel": "حسناء", + "Pascal": "پاسكال", + "Pixel": "بيكسل", "Sammy Slam": "سامي سلام", - "Santa Claus": "الشيخ", - "Snake Shadow": "مُحارب في الصحراء", - "Spaz": "Spaz", - "Taobao Mascot": "التميمه تاوباو", + "Santa Claus": "سانتا كلوس", + "Snake Shadow": "نينجا", + "Spaz": "سپاز", + "Taobao Mascot": "ماسكوت تاوباو", "Todd McBurton": "تود بيرتون", - "Zoe": "ليلى", + "Zoe": "زوي", "Zola": "زولا" }, "coopLevelNames": { @@ -1843,6 +1850,8 @@ "winsPlayerText": "${NAME} !يفوز", "winsTeamText": "${NAME} !يفوز فريق", "winsText": "${NAME} !يفوز", + "workspaceSyncErrorText": "فشل في مزامنة ${WORKSPACE}. القي نظرة على السجل للتفاصيل.", + "workspaceSyncReuseText": "لا يمكن مزامنة ${WORKSPACE}. اعادة استخدام النسخة المتزامنة السابقة.", "worldScoresUnavailableText": "(النتيجة العالمية غير متوفرة (اتصل بالانترنت", "worldsBestScoresText": "افضل نتيجة للعالم", "worldsBestTimesText": "افضل اوقات العالم", diff --git a/dist/ba_data/data/languages/chinese.json b/dist/ba_data/data/languages/chinese.json index 7c092d6..45ef14a 100644 --- a/dist/ba_data/data/languages/chinese.json +++ b/dist/ba_data/data/languages/chinese.json @@ -330,6 +330,7 @@ "achievementsRemainingText": "未完成成就:", "achievementsText": "成就", "achievementsUnavailableForOldSeasonsText": "抱歉,往届的成就细节不可用。", + "activatedText": "${THING} 已激活", "addGameWindow": { "getMoreGamesText": "获取更多游戏模式…", "titleText": "添加比赛" @@ -629,7 +630,9 @@ "epicDescriptionFilterText": "史诗级慢动作 ${DESCRIPTION}。", "epicNameFilterText": "史诗级${NAME}", "errorAccessDeniedText": "访问被拒绝", + "errorDeviceTimeIncorrectText": "您的系统时间与服务器时间相差了${HOURS}小时\n这可能会出现一些问题\n请检查您的系统时间或时区", "errorOutOfDiskSpaceText": "磁盘空间不足", + "errorSecureConnectionFailText": "无法建立安全的云链接,网络可能会连接失败", "errorText": "错误", "errorUnknownText": "未知错误", "exitGameText": "退出${APP_NAME}?", @@ -795,7 +798,7 @@ "ticketPack4Text": "巨型点券包", "ticketPack5Text": "猛犸象点券包", "ticketPack6Text": "终极点券包", - "ticketsFromASponsorText": "从赞助商处\n获得${COUNT}点券", + "ticketsFromASponsorText": "观看广告\n白嫖${COUNT}个点券", "ticketsText": "${COUNT}点券", "titleText": "获得点券", "unavailableLinkAccountText": "对不起,该平台上不可进行购买。\n您可将此帐户链接到另一个\n平台,以进行购买。", @@ -806,6 +809,7 @@ "youHaveText": "你拥有${COUNT}点券" }, "googleMultiplayerDiscontinuedText": "抱歉,Google的多人游戏服务已不可用。\n我将尽快更换新的替代服务。\n在此之前,请尝试其他连接方法。\n-Eric", + "googlePlayPurchasesNotAvailableText": "Google商店购买不可用!\n请安装谷歌框架或更新Google服务。", "googlePlayText": "Google Play", "graphicsSettingsWindow": { "alwaysText": "总是", @@ -1122,7 +1126,10 @@ "playlistsText": "列表", "pleaseRateText": "如果你喜欢 ${APP_NAME},请考虑花一点时间\n来评价一下它或为它写一篇评论。这将为我们提供\n有用的反馈建议,为游戏的未来开发给予支持。\n\n感谢您!\n-eric", "pleaseWaitText": "请稍等...", - "pluginsDetectedText": "新的插件已经加载.请在设置中启用它们.", + "pluginClassLoadErrorText": "加载'${PLUGIN}'插件时出错了耶: ${ERROR}", + "pluginInitErrorText": "初始化'${PLUGIN}'插件失败了啦: ${ERROR}", + "pluginsDetectedText": "新插件安装成功,请重启游戏或在设置中设置它们~", + "pluginsRemovedText": "有${NUM}个插件被删除了...", "pluginsText": "插件", "practiceText": "练习", "pressAnyButtonPlayAgainText": "按任意按钮再玩一次...", @@ -1863,6 +1870,8 @@ "winsPlayerText": "${NAME}获胜!", "winsTeamText": "${NAME}获胜!", "winsText": "${NAME}获胜!", + "workspaceSyncErrorText": "同步${WORKSPACE}出错了啦,详情见日志文件", + "workspaceSyncReuseText": "同步${WORKSPACE}时出错,正在使用以前同步的数据....", "worldScoresUnavailableText": "全球得分不可用。", "worldsBestScoresText": "全球最高得分", "worldsBestTimesText": "全球最佳时间", diff --git a/dist/ba_data/data/languages/chinesetraditional.json b/dist/ba_data/data/languages/chinesetraditional.json index b060b86..6f0f25f 100644 --- a/dist/ba_data/data/languages/chinesetraditional.json +++ b/dist/ba_data/data/languages/chinesetraditional.json @@ -324,6 +324,7 @@ "achievementsRemainingText": "未完成的成就", "achievementsText": "成就", "achievementsUnavailableForOldSeasonsText": "抱歉,往屆成就細節不可用", + "activatedText": "${THING}已激活", "addGameWindow": { "getMoreGamesText": "獲取更多比賽模式", "titleText": "新增比賽" @@ -621,7 +622,9 @@ "epicDescriptionFilterText": "史詩級慢動作${DESCRIPTION}", "epicNameFilterText": "史詩級${NAME}", "errorAccessDeniedText": "訪問被拒絕", + "errorDeviceTimeIncorrectText": "您的系統時間與BS伺服器相差了${HOURS}小時\n這可能會出現一些問題\n請檢查你的系統時間或時區", "errorOutOfDiskSpaceText": "磁盤空間不足", + "errorSecureConnectionFailText": "無法安全的連接到伺服器,網絡功能可能會失效", "errorText": "錯誤", "errorUnknownText": "未知錯誤", "exitGameText": "退出${APP_NAME}?", @@ -1105,7 +1108,10 @@ "playlistsText": "遊戲列表", "pleaseRateText": "如果你喜歡${APP_NAME},請考慮花一些時間\n為Bombsquad寫一篇評論。這將為我們\n提供一些有用的反饋建議,為遊戲的未來開發給予支持\n\nThanks!\n-Eric", "pleaseWaitText": "請稍等....", - "pluginsDetectedText": "檢測到新插件。 在設置中啟用/配置它們。", + "pluginClassLoadErrorText": "加載插件類'${PLUGIN}'時出錯:${ERROR}", + "pluginInitErrorText": "初始化插件'${PLUGIN}'時出錯:${ERROR}", + "pluginsDetectedText": "檢測到新插件。重新啓動以激活,或在設置中配置這些插件", + "pluginsRemovedText": "${NUM}個插件已丟失", "pluginsText": "外掛程式", "practiceText": "練習", "pressAnyButtonPlayAgainText": "按任意鍵再玩一次...", @@ -1839,6 +1845,8 @@ "winsPlayerText": "${NAME}贏了!", "winsTeamText": "${NAME}贏了!", "winsText": "${NAME}贏了!", + "workspaceSyncErrorText": "同步${WORKSPACE}時出錯,詳情日志已輸出", + "workspaceSyncReuseText": "無法同步${WORKSPACE},重複使用以前的同步版本", "worldScoresUnavailableText": "無法讀入全球分數", "worldsBestScoresText": "全球最佳分數", "worldsBestTimesText": "全球最佳時間", diff --git a/dist/ba_data/data/languages/croatian.json b/dist/ba_data/data/languages/croatian.json index 4f5fbbf..f352d1c 100644 --- a/dist/ba_data/data/languages/croatian.json +++ b/dist/ba_data/data/languages/croatian.json @@ -329,6 +329,7 @@ "achievementsRemainingText": "Preostala Postignuća:", "achievementsText": "Postignuća", "achievementsUnavailableForOldSeasonsText": "Žao nam je,postignuća pojedinosti nisu dostupni za stare sezone.", + "activatedText": "${THING} uključeno.", "addGameWindow": { "getMoreGamesText": "Još igara...", "titleText": "Dodaj igru" @@ -720,14 +721,15 @@ "internetText": "Internet", "inviteAFriendText": "Prijatelji nemaju igru? Pozovi ih da je\nprobaju i oni će dobiti ${COUNT} besplatnih ulaznica.", "inviteFriendsText": "Pozovi Prijatelje", - "joinPublicPartyDescriptionText": "Pridruži se javnoj partiji:", - "localNetworkDescriptionText": "Uključi se u partiju na tvojoj mreži:", + "joinPublicPartyDescriptionText": "Pridruži se javnoj partiji", + "localNetworkDescriptionText": "Uđi u blisku partiju (Lan, Bluetooth, itd.)", "localNetworkText": "Lokalna mreža", "makePartyPrivateText": "Postavi moju partiju privatnom", "makePartyPublicText": "Postavi moju partiju javnom", "manualAddressText": "Adresa", "manualConnectText": "Spoji se", "manualDescriptionText": "Uljuči se u partiju po adresi:", + "manualJoinSectionText": "Uđi s Adresom", "manualJoinableFromInternetText": "Mogu li se drugi priključiti tebi s Interneta?:", "manualJoinableNoWithAsteriskText": "NE*", "manualJoinableYesText": "DA", @@ -735,14 +737,17 @@ "manualText": "Ručno", "manualYourAddressFromInternetText": "Tvoja adresa s Interneta:", "manualYourLocalAddressText": "Tvoja lokalna adresa:", + "nearbyText": "Blizu", "noConnectionText": "", "otherVersionsText": "(druge verzije)", + "partyCodeText": "Grupna šifra", "partyInviteAcceptText": "Prihvati", "partyInviteDeclineText": "Odbij", "partyInviteGooglePlayExtraText": "(pogledaj 'Google Play' karticu u prozoru 'Sakupi')", "partyInviteIgnoreText": "Odbaci", "partyInviteText": "${NAME} te pozvao\nda se priključiš njegovoj partiji!", "partyNameText": "Naziv igre", + "partyServerRunningText": "Tvoj grupni server radi.", "partySizeText": "Veličina žurke", "partyStatusCheckingText": "provjeravam status...", "partyStatusJoinableText": "tvoja igra je spremna za povezivanje s interneta", @@ -751,11 +756,21 @@ "partyStatusNotPublicText": "tvoja igra nije javna", "pingText": "ping", "portText": "Port", + "privatePartyCloudDescriptionText": "Privatne grupe radu na posvećenim cloud serverima; Nikakvih ruter konfiguracija potrebno", + "privatePartyHostText": "Napravi privatnu grupu", + "privatePartyJoinText": "Uđi u privatnu grupu", + "privateText": "Privatno", + "publicHostRouterConfigText": "Za ovo možda bude trebalo konfiguriranje \"port-forwarding\"-a na vašem ruteru. Za lakšu opciju napravite privatnu grupu", + "publicText": "Javno", "requestingAPromoCodeText": "Dohvaćam kod...", "sendDirectInvitesText": "Pošalji izravni poziv", "sendThisToAFriendText": "Posalji ovaj kod prijatelju:", "shareThisCodeWithFriendsText": "Podijeli ovaj kod s prijateljima:", "showMyAddressText": "Prikaži moju IP adresu", + "startHostingPaidText": "Napravi sada za ${COST}", + "startHostingText": "Napravi", + "startStopHostingMinutesText": "Možeš početi i zaustaviti server besplatno za slijedećih ${MINUTES} minuta.", + "stopHostingText": "Prestani server", "titleText": "Okupljanje", "wifiDirectDescriptionBottomText": "Ako svi uređaji imaju 'Wi-Fi Direct' opciju, trebali bi ga moći iskoristiti da pronađu jedan\ndrugoga i povežu se. Kad su svi uređaji povezani, možeš kreiratu igru\nkoristeći karticu 'Lokalna mreža', isto kao i kod obične Wi-Fi mreže. \n\nZa najbolje rezultate, domaćin Wi-Fi Directa također bi trebao biti domaćin ${APP_NAME} igre.", "wifiDirectDescriptionTopText": "Wi-Fi Direct možete koristiti da povežete Android uređaje direktno bez \npotrebe za Wi-Fi mrežom. Ovo najbolje radi na Android verziji 4.2 ili novijoj. \n\nZa korištenje, otvori Wi-Fi postavke i potraži 'Wi-Fi Direct' u izborniku.", @@ -813,6 +828,7 @@ "bombInfoText": "- Bomba -\nJača od udaraca, ali može rezultirati \nsamoubojstvom. Za najbolje \nrezultate, baci je prema neprijatelju \nprije nego što fitilj potpuno izgori.", "canHelpText": "${APP_NAME} može pomoći.", "controllersInfoText": "Možeš igrati ${APP_NAME} s prijeteljima preko mreže, ili svi\nmožete igrati na istom uređaju ako imate dovoljno kontrolera. \n${APP_NAME} podržava razne kontrolere; čak možete koristiti telefone\nkao kontrolere pomoću besplatne '${REMOTE_APP_NAME}' aplikacije. \nZa više informacija pogledaj pod Postavke->Kontroleri.", + "controllersInfoTextRemoteOnly": "Možete igrati ${APP_NAME} sa prijateljima \nPreko wifi-a ili\npreko istog uređaju koristeći mobitele kao kontrolere pomoću besplatnom aplikacijom '${REMOTE_APP_NAME}'", "controllersText": "Kontroleri", "controlsSubtitleText": "Tvoj prijateljski ${APP_NAME} lik ima par osnovnih kretnji:", "controlsText": "Kontrole", @@ -1051,6 +1067,7 @@ "offText": "Isključeno", "okText": "U redu", "onText": "Uključeno", + "oneMomentText": "Jedan trenutak...", "onslaughtRespawnText": "${PLAYER} će se ponovno pojaviti u naletu ${WAVE}", "orText": "${A} ili ${B}", "otherText": "Drugo...", @@ -1097,10 +1114,14 @@ "playerText": "Igrač", "playlistNoValidGamesErrorText": "Ova lista igara ne sadrži nijednu valjanu otključanu igru.", "playlistNotFoundText": "lista igara nije pronađena", + "playlistText": "Playlista", "playlistsText": "Liste igara", "pleaseRateText": "Ako ti se sviđa ${APP_NAME}, molim te razmisli o tome\nda ga ocijeniš ili napišeš recenziju.\nOvako šalješ korisnu povratnu informaciju koja pomaže u daljnjem razvoju.\n\nHvala!\n-eric", "pleaseWaitText": "Molimo sačekajte...", - "pluginsDetectedText": "Novi dodatak(ci) detektiraju. Osposobi/konfiguriraj ih u postavkama.", + "pluginClassLoadErrorText": "Pogreška učitavanja dodatka klase '${PLUGIN}': ${ERROR}", + "pluginInitErrorText": "Pogreška iniciranja dodatka '${PLUGIN}': ${ERROR}", + "pluginsDetectedText": "Novi dodatak(ci) detektiran(i) Resetiraj da ih ukljućiš ili ih konfiguriraj u postavkima", + "pluginsRemovedText": "${NUM} dodatak/ci više nisu pronađeni.", "pluginsText": "Dodatci", "practiceText": "Vježba", "pressAnyButtonPlayAgainText": "Pritisni bilo koju tipku za ponovnu igru...", @@ -1496,6 +1517,7 @@ "Dutch": "Nizozemski", "English": "Engleski", "Esperanto": "Esperanto", + "Filipino": "Filipinski", "Finnish": "Finski", "French": "Francuski", "German": "Njemački", @@ -1516,6 +1538,8 @@ "Slovak": "Slovački", "Spanish": "Španjolski", "Swedish": "Švedski", + "Tamil": "Tamil", + "Thai": "tajlandski", "Turkish": "Turski", "Ukrainian": "Ukrajinski", "Venetian": "Venecijanski", @@ -1564,6 +1588,7 @@ "Account linking successful!": "Povezivanje računa uspješno!", "Account unlinking successful!": "Odspajanje računa uspješno!", "Accounts are already linked.": "Računi su već povezani.", + "Ad view could not be verified.\nPlease be sure you are running an official and up-to-date version of the game.": "Prikaz oglasa se nije mogao provjeriti.\nMolim vas budite sigurni da koristite službenu i suvremenu verziju ove igre", "An error has occurred; (${ERROR})": "Greška se pojavila; (${ERROR})", "An error has occurred; please contact support. (${ERROR})": "Greška se pojavila; molimo vas pozovite pomoć. (${ERROR})", "An error has occurred; please contact support@froemling.net.": "Greška se pojavila; molim vas pozovite support@froemling.net.", @@ -1589,6 +1614,7 @@ "Max number of profiles reached.": "Max broj dosegao profila.", "Maximum friend code rewards reached.": "Maksimalne nagrade od prijateljskih kodova dostignute.", "Message is too long.": "Poruka je prevelika.", + "No servers are available. Please try again soon.": "Nema dostupnih servera. Pokušajte opet kasnije.", "Profile \"${NAME}\" upgraded successfully.": "Profil \"${NAME}\" uspjesno upgrajdovan", "Profile could not be upgraded.": "Profil se ne može ažurirati.", "Purchase successful!": "Kupovina uspješna!", @@ -1598,10 +1624,12 @@ "Sorry, this code has already been used.": "Izvini, ovaj kod je već iskorišćen.", "Sorry, this code has expired.": "Izvini, ovaj kod je istekao.", "Sorry, this code only works for new accounts.": "Izvini, ovaj kod radi samo za nove igrače.", + "Still searching for nearby servers; please try again soon.": "Još pretražuješ za bliske servere; pokušajte opet kasnije", "Temporarily unavailable; please try again later.": "Privremeno nedostupno; molimo vas pokušajte kasnije.", "The tournament ended before you finished.": "Turnir je šio prije nego što si ti završio.", "This account cannot be unlinked for ${NUM} days.": "Ovaj se račun ne može odvezati ${NUM} dana.", "This code cannot be used on the account that created it.": "Ovaj kod se nemoze koristiti na akauntu koji je stvorio.", + "This is currently unavailable; please try again later.": "Ovo je trenutačno nedostupno; pokušajte opet kasnije.", "This requires version ${VERSION} or newer.": "Ovo zahtijeva verziju ${VERSION} ili noviju.", "Tournaments disabled due to rooted device.": "Turniri nedostupni zbog rootanog uređaja.", "Tournaments require ${VERSION} or newer": "Turniri koriste ${VERSION} ili noviju", @@ -1834,6 +1862,8 @@ "winsPlayerText": "${NAME} pobjeđuje!", "winsTeamText": "${NAME} pobjeđuju!", "winsText": "${NAME} pobjeđuje!", + "workspaceSyncErrorText": "Pogreška sinkroniziranja ${WORKSPACE}. Pogledaj log za više detalja.", + "workspaceSyncReuseText": "Nemoguće sinkronizirati ${WORKSPACE}. Ponovno korištenje stare verzije", "worldScoresUnavailableText": "Svjetski rezultati nedostupni.", "worldsBestScoresText": "Najbolji svjetski rezultati", "worldsBestTimesText": "Najbolja svjetska prolazna vremena", diff --git a/dist/ba_data/data/languages/czech.json b/dist/ba_data/data/languages/czech.json index 630b1b6..f130007 100644 --- a/dist/ba_data/data/languages/czech.json +++ b/dist/ba_data/data/languages/czech.json @@ -331,6 +331,7 @@ "achievementsRemainingText": "Achievementů Zbývá:", "achievementsText": "Achievementy", "achievementsUnavailableForOldSeasonsText": "Promiňte, ale detaily úspěchu nejsou pro starší sezóny zpřístupněny.", + "activatedText": "${THING} aktivováno.", "addGameWindow": { "getMoreGamesText": "Získat Více Her...", "titleText": "Přidat Hru" @@ -633,7 +634,9 @@ "epicDescriptionFilterText": "${DESCRIPTION} V epickém slow motionu", "epicNameFilterText": "Epické ${NAME}", "errorAccessDeniedText": "přístup zamítnut", + "errorDeviceTimeIncorrectText": "Čas vašeho zařízení je mimo o ${HOURS} h.\nTo pravděpodobně způsobí problémy.\nProsím zkontrolujte nastavení času a časového pásma.", "errorOutOfDiskSpaceText": "není místo na disku", + "errorSecureConnectionFailText": "Unable to establish secure cloud connection; network functionality may fail.", "errorText": "Chyba", "errorUnknownText": "neznámá chyba", "exitGameText": "Ukončit ${APP_NAME} ???", @@ -1123,7 +1126,10 @@ "playlistsText": "Playlisty", "pleaseRateText": "Jestliže Vás ${APP_NAME} baví, zvažte prosím, jestli si nechcete udělat\nchvilku na ohodnocení nebo napsání recenze. Poskytuje to užitečnou\nzpětnou vazbu a pomáhá podporovat budoucí vývoj.\n\nDíky!\n-eric", "pleaseWaitText": "Prosím čekejte...", - "pluginsDetectedText": "Nové plugin(y) nalezeny. Zapněte/konfigurujte je v nastavení.", + "pluginClassLoadErrorText": "Chyba při načítání třídy pluginu '${PLUGIN}': ${ERROR}", + "pluginInitErrorText": "Chyba při inicializaci pluginu '${PLUGIN}': ${ERROR}", + "pluginsDetectedText": "Nové plugin(y) nalezeny. Pro aktivaci restartujte, nebo konfigurujte v nastavení.", + "pluginsRemovedText": "${NUM} plugin(y) nenalezeny.", "pluginsText": "Pluginy", "practiceText": "Cvičení", "pressAnyButtonPlayAgainText": "Stiskněte libovolné tlačítko pro opakování hry...", @@ -1864,6 +1870,8 @@ "winsPlayerText": "${NAME} Vyhrál!", "winsTeamText": "${NAME} Vyhrál!", "winsText": "${NAME} Vyhrál!", + "workspaceSyncErrorText": "Chyba synchronizace ${WORKSPACE}. Podrobnosti v logu.", + "workspaceSyncReuseText": "Nelze synchronizovat ${WORKSPACE}. Opětovné použití předchozí synchronizované verze.", "worldScoresUnavailableText": "Světové skóre nepřístupné.", "worldsBestScoresText": "Světové nejlepší skóre", "worldsBestTimesText": "Světové nejlepší časy", diff --git a/dist/ba_data/data/languages/english.json b/dist/ba_data/data/languages/english.json index 12f6f79..528aad5 100644 --- a/dist/ba_data/data/languages/english.json +++ b/dist/ba_data/data/languages/english.json @@ -324,6 +324,7 @@ "achievementsRemainingText": "Achievements Remaining:", "achievementsText": "Achievements", "achievementsUnavailableForOldSeasonsText": "Sorry, achievement specifics are not available for old seasons.", + "activatedText": "${THING} activated.", "addGameWindow": { "getMoreGamesText": "Get More Games...", "titleText": "Add Game" @@ -623,7 +624,9 @@ "epicDescriptionFilterText": "${DESCRIPTION} In epic slow motion.", "epicNameFilterText": "Epic ${NAME}", "errorAccessDeniedText": "access denied", + "errorDeviceTimeIncorrectText": "Your device's time is off by ${HOURS} hours.\nThis is likely to cause problems.\nPlease check your time and time-zone settings.", "errorOutOfDiskSpaceText": "out of disk space", + "errorSecureConnectionFailText": "Unable to establish secure cloud connection; network functionality may fail.", "errorText": "Error", "errorUnknownText": "unknown error", "exitGameText": "Exit ${APP_NAME}?", @@ -786,7 +789,7 @@ "ticketPack4Text": "Jumbo Ticket Pack", "ticketPack5Text": "Mammoth Ticket Pack", "ticketPack6Text": "Ultimate Ticket Pack", - "ticketsFromASponsorText": "Get ${COUNT} tickets\nfrom a sponsor", + "ticketsFromASponsorText": "Watch an ad\nfor ${COUNT} tickets", "ticketsText": "${COUNT} Tickets", "titleText": "Get Tickets", "unavailableLinkAccountText": "Sorry, purchases are not available on this platform.\nAs a workaround, you can link this account to an account on\nanother platform and make purchases there.", @@ -797,6 +800,7 @@ "youHaveText": "you have ${COUNT} tickets" }, "googleMultiplayerDiscontinuedText": "Sorry, Google’s multiplayer service is no longer available.\nI am working on a replacement as fast as possible.\nUntil then, please try another connection method.\n-Eric", + "googlePlayPurchasesNotAvailableText": "Google Play purchases are not available.\nYou may need to update your store app.", "googlePlayText": "Google Play", "graphicsSettingsWindow": { "alwaysText": "Always", @@ -1115,7 +1119,10 @@ "playlistsText": "Playlists", "pleaseRateText": "If you're enjoying ${APP_NAME}, please consider taking a\nmoment and rating it or writing a review. This provides\nuseful feedback and helps support future development.\n\nthanks!\n-eric", "pleaseWaitText": "Please wait...", - "pluginsDetectedText": "New plugin(s) detected. Enable/configure them in settings.", + "pluginClassLoadErrorText": "Error loading plugin class '${PLUGIN}': ${ERROR}", + "pluginInitErrorText": "Error initing plugin '${PLUGIN}': ${ERROR}", + "pluginsDetectedText": "New plugin(s) detected. Restart to activate them, or configure them in settings.", + "pluginsRemovedText": "${NUM} plugin(s) no longer found.", "pluginsText": "Plugins", "practiceText": "Practice", "pressAnyButtonPlayAgainText": "Press any button to play again...", @@ -1851,6 +1858,8 @@ "winsPlayerText": "${NAME} Wins!", "winsTeamText": "${NAME} Wins!", "winsText": "${NAME} Wins!", + "workspaceSyncErrorText": "Error syncing ${WORKSPACE}. See log for details.", + "workspaceSyncReuseText": "Can't sync ${WORKSPACE}. Reusing previous synced version.", "worldScoresUnavailableText": "World scores unavailable.", "worldsBestScoresText": "World's Best Scores", "worldsBestTimesText": "World's Best Times", diff --git a/dist/ba_data/data/languages/filipino.json b/dist/ba_data/data/languages/filipino.json index e85c851..a208c03 100644 --- a/dist/ba_data/data/languages/filipino.json +++ b/dist/ba_data/data/languages/filipino.json @@ -211,14 +211,14 @@ "descriptionComplete": "Naipanalo ang laro", "descriptionFull": "Panalunin ang laro sa ${LEVEL}", "descriptionFullComplete": "Pinanalo ang laro sa ${LEVEL}", - "name": "${LEVEL} Panalo" + "name": "Nanalo sa ${LEVEL}" }, "Rookie Onslaught Victory": { "description": "Italo lahat ang mga kaway", "descriptionComplete": "Natalo lahat ang mga kaway", "descriptionFull": "Italo lahat ang mga kaway sa ${LEVEL}", "descriptionFullComplete": "Natalo lahat ng mga kaway sa ${LEVEL}", - "name": "${LEVEL} Panalo" + "name": "Nanalo sa ${LEVEL}" }, "Runaround God": { "description": "Maka-iskor ng 2000 na Puntos", @@ -312,7 +312,7 @@ "descriptionComplete": "Natalo ang lahat na mga kaway", "descriptionFull": "Talunin ang lahat na mga kaway sa ${LEVEL}", "descriptionFullComplete": "Natalo ang lahat na mga kaway sa ${LEVEL}", - "name": "${LEVEL} Panalo" + "name": "Nanalo sa ${LEVEL}" }, "Uber Runaround Victory": { "description": "Tapusin ang lahat na kaway", @@ -325,6 +325,7 @@ "achievementsRemainingText": "Ang Mga Natitirang Nakamtan:", "achievementsText": "Achievements", "achievementsUnavailableForOldSeasonsText": "Pasensya na, hindi available ang mga detalye ng achievements para sa mga lumang seasons.", + "activatedText": "Na-aktibo ang ${THING}.", "addGameWindow": { "getMoreGamesText": "Kukuha ng higit pang mga laro…", "titleText": "Magdagdag Ng Laro" @@ -623,9 +624,11 @@ "epicDescriptionFilterText": "${DESCRIPTION} sa isang epic na slow motion.", "epicNameFilterText": "Epikong ${NAME}", "errorAccessDeniedText": "walang pahintulot", + "errorDeviceTimeIncorrectText": "Ang oras ng iyong device ay naka-off nang ${HOURS} na oras.\nIto ay malamang na magdulot ng mga iba’t ibang problema.\nPakisuri ang iyong mga setting ng oras at time-zone.", "errorOutOfDiskSpaceText": "Wala sa puwang ng disk", + "errorSecureConnectionFailText": "Hindi mapatunayan ng secure na “cloud connection”; maaaring mabigo ang pagpapagana ng network.", "errorText": "Error", - "errorUnknownText": "hindi batid na error", + "errorUnknownText": "error na ‘di malaman", "exitGameText": "Exit mula sa ${APP_NAME}?", "exportSuccessText": "Na-export ang '${NAME}'.", "externalStorageText": "External na Storage", @@ -786,7 +789,7 @@ "ticketPack4Text": "Malaking Pakete Ng Tiket", "ticketPack5Text": "Mas Malaking Pakete Ng Tiket", "ticketPack6Text": "Pinakamalaking Pakete Ng Tiket", - "ticketsFromASponsorText": "Kumuha ng ${COUNT} na tiket\nmula sa isang sponsor", + "ticketsFromASponsorText": "Manood ng Ad\npara makuha ang ${COUNT} na tiket", "ticketsText": "${COUNT} Tiket", "titleText": "Kumuha Ng Tiket", "unavailableLinkAccountText": "Pasensya na, hindi available ang pagbili sa platform na ito.\nBilang isang solusyon, maaari mong i-link ang account na ito sa isang account na nasa\nisa pang platform at bumili doon.", @@ -797,6 +800,7 @@ "youHaveText": "mayroon kang ${COUNT} na tiket" }, "googleMultiplayerDiscontinuedText": "Pasensya na, hindi na available ang multiplayer na serbisyo ng Google.\nGumagawa ako ng kapalit sa lalong madaling panahon.\nHanggang doon, mangyaring sumubok ng ibang paraan ng koneksyon.\n-Eric", + "googlePlayPurchasesNotAvailableText": "Hindi puwede ang mga pagbili sa Google Play nito.\nMaaaring kailanganin mong i-update ang iyong store app.", "googlePlayText": "Google Play", "graphicsSettingsWindow": { "alwaysText": "Palagi", @@ -836,11 +840,11 @@ "pickUpInfoTextScale": 0.5, "powerupBombDescriptionText": "Payagan kang maglabas ng tatlong bomba\nsa isang hilera sa halip na isa lamang.", "powerupBombNameText": "Tatlong-Bomba", - "powerupCurseDescriptionText": "Malamang na gusto mong iwasan ang mga ito.\n ...o ikaw?", + "powerupCurseDescriptionText": "Malamang na gusto mong iwasan ang mga ganito.\n ...o sige lang?", "powerupCurseNameText": "Sumpa", "powerupHealthDescriptionText": "Ibangon ka sa buong kalusugan.\nHindi mo naisip nito.", "powerupHealthNameText": "Medikal-Pakete", - "powerupIceBombsDescriptionText": "Mas mahina kaysa sa mga normal na bomba\nngunit nagyelo ang iyong mga kalaban\nat partikular na malutong.", + "powerupIceBombsDescriptionText": "Mas mahina kaysa sa mga normal na bomba\nngunit nagyelo ang iyong mga kalaban\nat partikular na mabasag.", "powerupIceBombsNameText": "Bombang-Yelo", "powerupImpactBombsDescriptionText": "Medyo mahina kaysa sa regular\nbomba, ngunit sumasabog ito kapag nabagsak.", "powerupImpactBombsNameText": "Gatilyong-Bomba", @@ -1089,7 +1093,7 @@ "titleText": "Maglaro", "twoToEightPlayersText": "2-8 na manlalaro" }, - "playerCountAbbreviatedText": "${COUNT}n.m", + "playerCountAbbreviatedText": "${COUNT}p", "playerDelayedJoinText": "Papasok si ${PLAYER} sa simula ng susunod na round.", "playerInfoText": "Impormasyon ng Manlalaro", "playerLeftText": "Umalis si ${PLAYER} sa laro.", @@ -1110,7 +1114,10 @@ "playlistsText": "Mga Playlist", "pleaseRateText": "Kung nae-enjoy mo ang ${APP_NAME}, mangyaring isaalang-alang ang \npagkuha na sandali lang at i-rate ito o pagsulat ng isang pagsusuri. Nagbibigay ito ngkapaki-pakinabang na feedback at \ntumutulong sa pagsuporta sa pag-unlad sa hinaharap.\n\nsalamat!\n-eric", "pleaseWaitText": "Hintay lang…", - "pluginsDetectedText": "May nakitang bagong (mga) plugin. Paganahin/i-configure ang mga ito sa mga setting.", + "pluginClassLoadErrorText": "Error sa paglo-load ang '${PLUGIN}' na klaseng plugin : ${ERROR}", + "pluginInitErrorText": "Error sa pagsisimula ang '${PLUGIN}' na plugin: ${ERROR}", + "pluginsDetectedText": "May nakitang bagong (mga) plugin. I-restart para i-activate ang mga ito, o i-configure ang mga ito sa mga setting.", + "pluginsRemovedText": "Hindi na nahanapan ang ${NUM} ng (mga) plugin.", "pluginsText": "Mga Plugin", "practiceText": "Pagsasagawa", "pressAnyButtonPlayAgainText": "Pindutin ang anumang button para maglaro muli...", @@ -1453,7 +1460,7 @@ "kill ${ARG1} enemies": "patayin ang ${ARG1} na mga kalaban.", "last one standing wins": "kung sino ang huling nakatayo ang siyang mananalo", "last team standing wins": "kung sino ang huling katayuan ng team ang siyang mananalo", - "return ${ARG1} flags": "ibalik ang ${ARG1} na mga bandera", + "return ${ARG1} flags": "Magnakaw ng ${ARG1} na mga bandera", "return 1 flag": "ibalik ang 1 bandila", "run ${ARG1} laps": "Tumakbo ng ${ARG1} ikot", "run 1 lap": "tumakbo ng 1 ikot", @@ -1774,7 +1781,7 @@ "randomName3Text": "Stephen", "randomName4Text": "Joshua", "randomName5Text": "Villar", - "skipConfirmText": "Sure ka na i-skip ang tutorial? Tap o pindutin para ma i-confirm.", + "skipConfirmText": "Sure ka ba na i-skip ang tutorial? Tap o pindutin para ma i-confirm.", "skipVoteCountText": "${COUNT}/${TOTAL} boto na gustong laktawan", "skippingText": "Nilalaktawan ang tutorial", "toSkipPressAnythingText": "(i-tap o pindutin ang anuman para laktawan ang tutorial)" @@ -1801,13 +1808,13 @@ "votedAlreadyText": "Nakaboto ka na", "votesNeededText": "Kailangan ng ${NUMBER} (na) boto", "vsText": "vs.", - "waitingForHostText": "(naghihintay para sa ${HOST} na magpatuloy)", + "waitingForHostText": "(naghihintay para kay ${HOST} na magpatuloy)", "waitingForPlayersText": "naghihintay ng mga manlalaro na sumali...", "waitingInLineText": "Naghihintay sa pila (puno ang party)...", "watchAVideoText": "Manood ng Isang video", "watchAnAdText": "Manood ng Ad", "watchWindow": { - "deleteConfirmText": "Tangalin \"${REPLAY}\"?", + "deleteConfirmText": "Tangalin ang \"${REPLAY}\"?", "deleteReplayButtonText": "Itanggal ang \nReplay", "myReplaysText": "Ang Mga Replay Ko", "noReplaySelectedErrorText": "Walang Napiling Replay", @@ -1825,7 +1832,7 @@ "watchReplayButtonText": "Ipanood ang\nReplay" }, "waveText": "Kaway", - "wellSureText": "Oo syempre", + "wellSureText": "Oo Syempre!", "wiimoteLicenseWindow": { "titleText": "Copyright ni DarwiinRemote" }, @@ -1839,11 +1846,13 @@ "listenText": "Makinig", "macInstructionsText": "Tiyaking naka-off ang iyong Wii at naka-enable ang Bluetooth\nsa iyong Mac, pagkatapos ay pindutin ang 'Makinig'. Maaari ang suporta ng Wiimote\nmaging medyo patumpik-tumpik, kaya maaaring kailanganin mong subukan ng ilang beses\nbago ka magkaroon ng koneksyon.\n\nDapat hawakan ng Bluetooth ang hanggang 7 konektadong device,\nkahit na ang iyong mileage ay maaaring mag-iba.\n\nSinusuportahan ng BombSquad ang orihinal na Wiimotes, Nunchuks,\nat ang Klasikong Controller.\nGumagana na rin ang mas bagong Wii Remote Plus\nngunit hindi sa mga kalakip.", "thanksText": "Salamat sa DarwiinRemote team\nPara maging posible ito.", - "titleText": "Wiimote Setup" + "titleText": "Pag-setup ng Wiimote" }, - "winsPlayerText": "${NAME} Nanalo!", - "winsTeamText": "${NAME} Nanalo!", + "winsPlayerText": "Nanalo si ${NAME}!", + "winsTeamText": "Nanalo ang ${NAME}!", "winsText": "${NAME} Nanalo!", + "workspaceSyncErrorText": "Error sa pag-sync ng ${WORKSPACE}. Tingnan ang log para sa mga detalye.", + "workspaceSyncReuseText": "Hindi ma-sync ang ${WORKSPACE}. Muling paggamit ng nakaraang naka-sync na bersyon.", "worldScoresUnavailableText": "Ang scores sa buong mundo ay hindi pa handa", "worldsBestScoresText": "Pinakamahusay na Iskor ng Mundo", "worldsBestTimesText": "Pinakamahusay na Oras sa Mundo", diff --git a/dist/ba_data/data/languages/german.json b/dist/ba_data/data/languages/german.json index c036ff9..7f48b5d 100644 --- a/dist/ba_data/data/languages/german.json +++ b/dist/ba_data/data/languages/german.json @@ -1191,7 +1191,7 @@ "playlistsText": "Playlists", "pleaseRateText": "Wenn dir ${APP_NAME} Spaß macht, nimm dir kurz die Zeit\nund bewerte es oder schreib ein Review. Durch das Feedback\nwird zukünftige Arbeit an dem Spiel unterstützt.\n\nVielen Dank!\n-eric", "pleaseWaitText": "Bitte warte...", - "pluginsDetectedText": "Neue Plugins erkannt. Aktivieren / konfigurieren Sie sie in den Einstellungen.", + "pluginsDetectedText": "Neue Plugins erkannt. Neustarten, um sie zu aktivieren oder in den Einstellungen konfigurieren.", "pluginsText": "Plugins", "practiceText": "Übung", "pressAnyButtonPlayAgainText": "Drücke einen Knopf um nochmal zu spielen...", diff --git a/dist/ba_data/data/languages/gibberish.json b/dist/ba_data/data/languages/gibberish.json index af5d8f0..4241678 100644 --- a/dist/ba_data/data/languages/gibberish.json +++ b/dist/ba_data/data/languages/gibberish.json @@ -333,6 +333,7 @@ "achievementsRemainingText": "Azhiévemúnts Rzmáinzng:", "achievementsText": "Achéevúmentz", "achievementsUnavailableForOldSeasonsText": "Srrrz, chi faow co wjefo iwefo wef;oiajwf asodvjoa sdfj odfjsodf.", + "activatedText": "${THING} cjwoeifjwer.", "addGameWindow": { "getMoreGamesText": "Gztz Mrrz Gmzz...", "titleText": "Ádzd Gámzé", @@ -660,7 +661,9 @@ "epicDescriptionFilterText": "${DESCRIPTION} Ín ípic slúw mztíon.", "epicNameFilterText": "${NAME} Epícz", "errorAccessDeniedText": "acczlr dnfflz", + "errorDeviceTimeIncorrectText": "Yowruc cowier oowefjwoefj ${HOURS} horewif.\noitwocweojowerjiowejjjfwoef.\nPefjwe wehapeocjjgwghwe w e weofwoefjwe.", "errorOutOfDiskSpaceText": "orz of dkzk spzlfz", + "errorSecureConnectionFailText": "Uweorcjwoef ojcowe werryyeoi nowe; jcnwoeroidfowdffdj.dfsdf.", "errorText": "Errórz", "errorUnknownText": "unknznlz errzzz", "exitGameText": "$Excej ${APP_NAME}", @@ -748,6 +751,7 @@ "getFriendInviteCodeText": "Gz Frjor Infivo Cdz", "googlePlayDescriptionText": "Invlt Gglglz Plzlz plzlfer tz yzr prtaryz:", "googlePlayInviteText": "Invtlzz", + "googlePlayPurchasesNotAvailableText": "JGowej eo owe weoirjweorjf\nYou we r wcowehpaw ecowjeoiwjrperpp.", "googlePlayReInviteText": "Tnz arzs ${COUNT} Gflf Plzz Plalfjdsf oin weojf wfeoyowiufe\nwhiz ewofij woijdoisoic o asodf wfiawje;aoew aoiwfj.\nTIncowjf oweofjao wotonwei oiacoiw eotijweto tat.", "googlePlaySeeInvitesText": "Szz Invtlfz", "googlePlayText": "Gzzlg Plz", @@ -840,7 +844,7 @@ "ticketPack4Text": "Jmfpmf Tkckf CPkf", "ticketPack5Text": "Mmthf Tckff Pckc", "ticketPack6Text": "Ulzjtla Tkckdf Pck", - "ticketsFromASponsorText": "Gzt ${COUNT} tkckjtz\nfzn a sobfhrkz", + "ticketsFromASponsorText": "woejwoe c woeijweo\ncowjeo ${COUNT} weocjw", "ticketsText": "${COUNT} Tckrtzz", "titleText": "Gtz Tckrtz", "unavailableLinkAccountText": "Srryrc, cpofj caodn oiwrj coi weo joweijwoief.\nIf wc woiejf w, cocwi eoiaoth aciw jeojfow iejoijwef\npcoj weo faona omowio wejfowijerwe.", @@ -851,6 +855,7 @@ "youHaveText": "yz hv ${COUNT} tickrrz" }, "googleMultiplayerDiscontinuedText": "Sowoer Gojf wel wouwen weoioc long wf won.\nI wow oe wefwjr pif g wfpawouja c oeij fw ocjaoiejowr.\nUntil. cowier oa j fapefij cpoypt ao coonnec awoiery.\n-Ercff", + "googlePlayPurchasesNotAvailableText": "Weofiiwj woe woerd wieofjwe\ncome foe rj eofiaj cpwoe jrowjer..dd", "googlePlayText": "Gzzggl Plzz", "graphicsSettingsWindow": { "alwaysText": "Azlwáys", @@ -1196,7 +1201,10 @@ "playlistsText": "Plzlntsfs", "pleaseRateText": "If yóu're énjoyíng ${APP_NAME}, pléase cónsider táking z\nmúment ánd rúting zt ór wrúting í reváew. Thzs próvidzs\níseful fzédbíck ánd hélps súpport fútúre dévelópmént.\n\nthénkz!\n-eric", "pleaseWaitText": "Poke focwoe fjowef.", - "pluginsDetectedText": "Nz pweo woe dfowocewr. Enfwoc/cowefe thzm incowrdss.", + "pluginClassLoadErrorText": "Erori cojflw cwoej woer erwe '${PLUGIN}': ${ERROR}", + "pluginInitErrorText": "Error cowejwoer plugins ${PLUGIN}: ${ERROR}", + "pluginsDetectedText": "Now cow rwoejcw. Wnewoiow rwepwot ghowcoweior weorjdfdfs.", + "pluginsRemovedText": "${NUM} powefjwj no legate fcwdf.", "pluginsText": "Plfzlfez", "practiceText": "Pcoifjzzz", "pressAnyButtonPlayAgainText": "Prézs ánz búttún tóz pláy agánz...", @@ -1990,6 +1998,8 @@ "winsSingularText": "${NAME} WinzfjlzSS!", "winsTeamText": "${NAME} Wnttm!", "winsText": "${NAME} Wínsz!", + "workspaceSyncErrorText": "Eeror ocijo itself ${WORKSPACE}. See zoo crew tcowdf.", + "workspaceSyncReuseText": "Cnoiwj zoo ${WORKSPACE}. ROI cw cwoerpwer housers.", "worldScoresUnavailableText": "Wrlzld scrzzl unvlfldsjbzl.", "worldsBestScoresText": "Wúrld's Bést Scórzs", "worldsBestTimesText": "Wúrld's Bzst Tímés", diff --git a/dist/ba_data/data/languages/italian.json b/dist/ba_data/data/languages/italian.json index e9f6d87..e52d42c 100644 --- a/dist/ba_data/data/languages/italian.json +++ b/dist/ba_data/data/languages/italian.json @@ -332,6 +332,7 @@ "achievementsRemainingText": "Trofei rimasti:", "achievementsText": "Trofei", "achievementsUnavailableForOldSeasonsText": "Mi dispiace, ma gli obbiettivi non sono disponibili per le stagioni vecchie.", + "activatedText": "${THING} attivato.", "addGameWindow": { "getMoreGamesText": "Ottieni Giochi...", "titleText": "Aggiungi Partita" @@ -1162,7 +1163,10 @@ "playlistsText": "Scalette", "pleaseRateText": "Se ti sta piacendo ${APP_NAME}, prenditi\nun momento per valutarlo o scriverci su una recensione.\nQuesto aiuterà a supportare futuri sviluppi.\n\nGrazie!\n-eric", "pleaseWaitText": "Attendi...", - "pluginsDetectedText": "Nuovo/i Plugin rilevato/i. Abilitali/configurali nelle impostazioni.", + "pluginClassLoadErrorText": "Errore nel caricamento di plugin classe '${PLUGIN}': ${ERROR}", + "pluginInitErrorText": "Errore inizializzazione plugin '${PLUGIN}': ${ERROR}", + "pluginsDetectedText": "nuovo/i plugin rilevato. Riavvia per attivarli, o configurali nelle impostazioni.", + "pluginsRemovedText": "${NUM} plugin non trovati.", "pluginsText": "Plugin", "practiceText": "Allenamento", "pressAnyButtonPlayAgainText": "Premi un pulsante qualunque per rigiocare...", @@ -1943,6 +1947,8 @@ "winsPlayerText": "${NAME} Ha Vinto!", "winsTeamText": "${NAME} Ha Vinto!", "winsText": "${NAME} vince!", + "workspaceSyncErrorText": "Errore nella sincronizzazione di ${WORKSPACE}. Controlla il registro per dettagli.", + "workspaceSyncReuseText": "Impossibile sincronizzare ${WORKSPACE}. Riutilizzo della versione sincronizzata precedente.", "worldScoresUnavailableText": "Punteggi mondiali non disponibili.", "worldsBestScoresText": "I punteggi migliori del mondo", "worldsBestTimesText": "I tempi migliori del mondo", diff --git a/dist/ba_data/data/languages/persian.json b/dist/ba_data/data/languages/persian.json index 5beae4d..c71ab6a 100644 --- a/dist/ba_data/data/languages/persian.json +++ b/dist/ba_data/data/languages/persian.json @@ -326,6 +326,7 @@ "achievementsRemainingText": "دستاورد های باقیمانده:", "achievementsText": "دستاوردها", "achievementsUnavailableForOldSeasonsText": "ببخشید، دستاوردهای مخصوص فصل گذشته در دسترس نیستند", + "activatedText": "${THING} فعال شد.", "addGameWindow": { "getMoreGamesText": "...بازی های بیشتر", "titleText": "افزودن بازی" @@ -625,7 +626,9 @@ "epicDescriptionFilterText": "در حماسهٔ حرکت آهسته ${DESCRIPTION}", "epicNameFilterText": "${NAME} حماسهٔ", "errorAccessDeniedText": "دسترسی رد شد", + "errorDeviceTimeIncorrectText": "ساعت گوشی ${HOURS} ساعت خاموش بوده‌است.\nممکن است مشکل به‌وجود بیاید.\nلطفاً ساعت و منطقهٔ زمانی گوشی‌تان را بررسی کنید.", "errorOutOfDiskSpaceText": "حافظه جا ندارد", + "errorSecureConnectionFailText": "قادر به ایجاد اتصال ابری امن نیست. عملکرد شبکه ممکن است خراب شود.", "errorText": "خطا", "errorUnknownText": "خطای ناشناخته", "exitGameText": "؟${APP_NAME} خروج از", @@ -788,7 +791,7 @@ "ticketPack4Text": "پک خیلی بزرگ بلیط", "ticketPack5Text": "پک ماموتی بلیط", "ticketPack6Text": "پک نهایی بلیط", - "ticketsFromASponsorText": "بلیط از${COUNT}\nیه اسپانسر بگیرید", + "ticketsFromASponsorText": "در ازای ${COUNT} بلیت\nیک آگهی ببینید", "ticketsText": "بلیط${COUNT}", "titleText": "بلیط بگیرید", "unavailableLinkAccountText": ".ببخشید،خرید به وسیله ی این دستگاه در دسترس نمیباشد\nمیتوانید حسابتان بر روی این دستگاه را به حسابی در دستگاهی\n.دیگر متصل کنید و آنجا خرید خود را انجام دهید", @@ -1109,7 +1112,10 @@ "playlistsText": "لیست بازی ها", "pleaseRateText": "خوشتان آمده، لطفاً چند لحظه‌ای وقت بگذارید و ${APP_NAME} اگر از\nآن را رتبه‌بندی کنید یا مروری بر آن بنویسید. این کار بازخوردهای مفیدی\n.را به همراه دارد و به پشتیبانی از توسعه‌ها در آینده کمک خواهد کرد\n\n!با تشکر\nاریک—", "pleaseWaitText": "…لطفاً صبر کنید", - "pluginsDetectedText": ".افزونه(ها)ی جدید شناسایی شد. آن‌ها را در تنظیمات فعال/پیکربندی کنید", + "pluginClassLoadErrorText": "${ERROR} :«${PLUGIN}» خطا در بارگیری دسته‌بندی افزونهٔ", + "pluginInitErrorText": "${ERROR} :«${PLUGIN}» خطا در راه‌اندازی افزونهٔ", + "pluginsDetectedText": "افزونه(ها)ی جدید شناسایی شد. آن‌ها را در تنظیمات فعال، یا پیکربندی کنید.", + "pluginsRemovedText": "${NUM} افزونه دیگر یافت نمی‌شود.", "pluginsText": "افزونه‌ها", "practiceText": "تمرین", "pressAnyButtonPlayAgainText": "…فشردن هر دکمه‌ای برای بازی دوباره", @@ -1847,6 +1853,8 @@ "winsPlayerText": "${NAME} برنده شد", "winsTeamText": "${NAME} برنده شد", "winsText": "${NAME} برنده شد", + "workspaceSyncErrorText": "خطا در همگام‌سازی ${WORKSPACE}. برای جزئیات به لاگ مراجعه کنید.", + "workspaceSyncReuseText": "نمی‌توان ${WORKSPACE} را همگام‌سازی کرد. استفادهٔ مجدد از نسخهٔ همگام‌سازی‌شده قبلی.", "worldScoresUnavailableText": "امتیاز های جهانی قابل دسترس نیستند.", "worldsBestScoresText": "بهترین امتیازهای جهانی", "worldsBestTimesText": "بهترین زمان های جهانی", diff --git a/dist/ba_data/data/languages/polish.json b/dist/ba_data/data/languages/polish.json index feb123e..8aa800d 100644 --- a/dist/ba_data/data/languages/polish.json +++ b/dist/ba_data/data/languages/polish.json @@ -1161,7 +1161,7 @@ "playlistsText": "Listy gier", "pleaseRateText": "Jeśli polubiłeś ${APP_NAME}, proszę o poddanie go ocenie\nlub napisanie krótkiej recenzji. Pozwoli to zebrać przydatne\ninformacje, które pomogą wesprzeć rozwój gry w przyszłości.\n\nDziękuję!\n-Eric", "pleaseWaitText": "Czekaj chwilkę...", - "pluginsDetectedText": "Nowe pluginy wykryte. Włącz/skonfiguruj je w ustawieniach.", + "pluginsDetectedText": "Wykryto nowe pluginy. Uruchom ponownie grę, aby je aktywować, lub skonfiguruje je w ustawieniach.", "pluginsText": "Pluginy", "practiceText": "Praktyka", "pressAnyButtonPlayAgainText": "Naciśnij dowolny przycisk aby zagrać ponownie...", diff --git a/dist/ba_data/data/languages/portuguese.json b/dist/ba_data/data/languages/portuguese.json index 97ec479..a8e78db 100644 --- a/dist/ba_data/data/languages/portuguese.json +++ b/dist/ba_data/data/languages/portuguese.json @@ -331,6 +331,7 @@ "achievementsRemainingText": "Conquistas restantes:", "achievementsText": "Conquistas", "achievementsUnavailableForOldSeasonsText": "Desculpe, algumas conquistas não estão disponíveis em temporadas antigas.", + "activatedText": "${THING} foi ativado.", "addGameWindow": { "getMoreGamesText": "Mais jogos...", "titleText": "Adicionar jogo" @@ -657,7 +658,9 @@ "epicDescriptionFilterText": "${DESCRIPTION} em câmera lenta épica.", "epicNameFilterText": "${NAME} épico(a)", "errorAccessDeniedText": "acesso negado", + "errorDeviceTimeIncorrectText": "A hora do seu dispositivo está atrasada/adiantada em ${HOURS} horas.\nIsso poderá causar problemas.\nPor favor cheque as suas configurações de hora e fuso horário.", "errorOutOfDiskSpaceText": "pouco espaço em disco", + "errorSecureConnectionFailText": "Não foi possível estabelecer uma conexão segura à nuvem; a funcionalidade da rede pode falhar.", "errorText": "Erro", "errorUnknownText": "erro desconhecido", "exitGameText": "Sair do ${APP_NAME}?", @@ -832,7 +835,7 @@ "ticketPack4Text": "Pacote jumbo de bilhetes", "ticketPack5Text": "Pacote mamute de bilhete", "ticketPack6Text": "Pacote ultimate de bilhetes", - "ticketsFromASponsorText": "Ganhe ${COUNT} bilhetes\nde um patrocinador", + "ticketsFromASponsorText": "Assista a um anúncio \ne ganhe ${COUNT} bilhetes", "ticketsText": "${COUNT} bilhetes", "titleText": "Obter bilhetes", "unavailableLinkAccountText": "Desculpe, as compras não estão disponíveis nesta plataforma.\nComo solução alternativa, você pode vincular esta conta para\noutra conta de outra plataforma e fazer compras lá.", @@ -843,6 +846,7 @@ "youHaveText": "você possui ${COUNT} bilhetes" }, "googleMultiplayerDiscontinuedText": "Desculpe, o serviço multijogador do Google não está mais disponível.\nEstou trabalhando em uma substituição o mais rápido possível.\nAté lá, tente outro método de conexão.\n-Eric", + "googlePlayPurchasesNotAvailableText": "Compras pela Google Play não estão disponíveis.\nTalvez seja necessário atualizar sua loja para isso.", "googlePlayText": "Google Play", "graphicsSettingsWindow": { "alwaysText": "Sempre", @@ -1183,7 +1187,10 @@ "playlistsText": "Playlists", "pleaseRateText": "Se você está curtindo ${APP_NAME}, por favor, dê um tempinho\npara avaliar e comentar. Isso nos dá uma opinião útil\ne ajuda no desenvolvimento do jogo.\n\nobrigado!\n-eric", "pleaseWaitText": "Por favor, aguarde...", - "pluginsDetectedText": "Novo(s) plugin(s) detected. Ative-os/configure-os em configurações.", + "pluginClassLoadErrorText": "Erro ao carregar a classe de um plugin '${PLUGIN}': ${ERROR}", + "pluginInitErrorText": "Erro ao inicializar plugin '${PLUGIN}': ${ERROR}", + "pluginsDetectedText": "Novo(s) plugin(s) detetados. Reinicie o jogo para ativá-los ou configure-os nas configurações.", + "pluginsRemovedText": "${NUM} plugin(s) não foram encontrados.", "pluginsText": "Plugins", "practiceText": "Praticar", "pressAnyButtonPlayAgainText": "Aperte qualquer botão para jogar novamente...", @@ -1970,6 +1977,8 @@ "winsPlayerText": "${NAME} venceu!", "winsTeamText": "${NAME} venceu!", "winsText": "${NAME} ganhou!", + "workspaceSyncErrorText": "Erro ao sincronizar ${WORKSPACE}. Veja o log para mais detalhes.", + "workspaceSyncReuseText": "Não pôde sincronizar ${WORKSPACE}. Reusando uma versão anterior sincronizada.", "worldScoresUnavailableText": "Pontuações mundiais indisponíveis.", "worldsBestScoresText": "Melhores pontuações do mundo", "worldsBestTimesText": "Melhores tempos do mundo", diff --git a/dist/ba_data/data/languages/romanian.json b/dist/ba_data/data/languages/romanian.json index c3bf193..0b00739 100644 --- a/dist/ba_data/data/languages/romanian.json +++ b/dist/ba_data/data/languages/romanian.json @@ -1,52 +1,52 @@ { "accountSettingsWindow": { - "accountNameRules": "Numele Contulurilor nu pot conține emoji-uri sau alte semne speciale", + "accountNameRules": "Numele contulurilor nu pot conține emoji-uri sau alte semne speciale", "accountProfileText": "(profil de cont)", "accountsText": "Conturi", "achievementProgressText": "Realizări: ${COUNT} din ${TOTAL}", - "campaignProgressText": "Progres campanie [Greu]: ${PROGRESS}", - "changeOncePerSeason": "Acesta poate fi schimbat o singură dată pe sezon", - "changeOncePerSeasonError": "Trebuie să aștepți până la următorul sezon dacă vrei să schimbi asta din nou timp de (${NUM} zile)", - "customName": "Nume personalizat", + "campaignProgressText": "Progres Campanie [Greu]: ${PROGRESS}", + "changeOncePerSeason": "Acest lucru poate fi schimbat o singură dată pe sezon.", + "changeOncePerSeasonError": "Trebuie să aștepți până la următorul sezon (timp de ${NUM} (de) zile) dacă vrei să schimbi acest lucru din nou", + "customName": "Nume Personalizat", "deviceSpecificAccountText": "Foloseşti un cont specific dispozitivului: ${NAME}", "linkAccountsEnterCodeText": "Introdu Codul", "linkAccountsGenerateCodeText": "Generează Codul", - "linkAccountsInfoText": "(împărtăşeşte progresul între platforme diferite)", - "linkAccountsInstructionsNewText": "Pentru a conecta două conturi, generați un cod pe primul\nși introduceți acel cod pe al doilea. Date din\nal doilea cont va fi apoi partajat între amândouă.\n(Datele din primul cont se vor pierde)\n\nPuteți conecta până la ${COUNT} conturi.\n\nIMPORTANT: conectați numai conturile pe care le dețineți;\nDacă vă conectați la conturile prietenilor, nu veți putea\nsă jucați online în același timp.", + "linkAccountsInfoText": "(împărtășeşte-ți progresul făcut pe platforme diferite)", + "linkAccountsInstructionsNewText": "Pentru a conecta două conturi între ele, generează un cod pe primul\nși introdu acel cod pe al doilea. Datele din cel\nde-al doilea cont vor fi apoi partajate între ambele conturi.\n(Datele de pe primul cont se vor pierde)\n\nPoți conecta până la ${COUNT} conturi.\n\nIMPORTANT: conectează-te numai la conturile pe care le deții;\nDacă te conectezi la conturile prietenilor tăi, nu vei\nputea să te joci online cu aceștia în același timp.", "linkAccountsInstructionsText": "Pentru a conecta 2 conturi, generează un cod pe\nunul din ele şi introdu acelmcod pe celălalt.\nProgresul şi inventarul tău vor fi combinate.\nPoți conecta până la ${COUNT} conturi.\n\nAi grijă; acest lucru nu poate fi şters!", "linkAccountsText": "Conectează Conturi", - "linkedAccountsText": "Conturi conectate:", - "nameChangeConfirm": "Vrei să-ți schimbi numele contului in ${NAME}?", - "resetProgressConfirmNoAchievementsText": "Această acțiune va reseta progresul co-op\nși high-score-urile (dar nu și biletele) și nu\npoate fi anulată. Ești sigur(ă)?", - "resetProgressConfirmText": "Această acțiune va reseta progresul\nco-op, realizările și high-scoreurile\n(dar nu și biletele) și nu poate fi\nanulată. Ești sigur(ă)?", - "resetProgressText": "Resetează progresul", - "setAccountName": "Setează-ți numele contului", - "setAccountNameDesc": "Selectează-ți numele dorit pentru contul tău.\nPoți folosi și numele unui cont de-al tău\nsau să creezi un joc unic personalizat.", - "signInInfoText": "Conectează-te pentru a colecta bilete, a juca online,\nşi a juca cu acelaşi cont pe dispozitive diferite.", + "linkedAccountsText": "Conturi Conectate:", + "nameChangeConfirm": "Vrei să schimbi numele contului tău în ${NAME}?", + "resetProgressConfirmNoAchievementsText": "Această acțiune îți va reseta progresul co-op\nși high-score-urile locale (dar nu și biletele).\nAcest lucru nu este reversibil. Sigur vrei să continui?", + "resetProgressConfirmText": "Această acțiune îți va reseta progresul\nco-op, realizările și high-score-urile\n(dar nu și biletele). Acest lucru nu\neste reversibil. Ești sigur(ă) că vrei să continui?", + "resetProgressText": "Resetează-ți Progresul", + "setAccountName": "Setează-ți Numele Contului", + "setAccountNameDesc": "Scrie un nume dorit de tine care vrei să se afișeze pe contul tău.\nPoți folosi și numele unui alt cont conectat de tine\nsau poți să creezi un nume unic personalizat.", + "signInInfoText": "Conectează-te cu un cont pentru a colecta bilete, a concura online,\nşi pentru a te juca cu acelaşi cont pe dispozitive diferite.", "signInText": "Conectează-te", "signInWithDeviceInfoText": "(un cont automat care este disponibil doar pe acest dispozitiv)", - "signInWithDeviceText": "Conectează-te cu un cont de tip dispozitiv", + "signInWithDeviceText": "Conectează-te cu contul dispozitivului", "signInWithGameCircleText": "Conectează-te cu Game Circle", "signInWithGooglePlayText": "Conectează-te cu Google Play", - "signInWithTestAccountInfoText": "(tip de cont normal; foloseşte conturile de tip dispozitiv şi cele noi)", - "signInWithTestAccountText": "Conectează-te cu un cont de test.", + "signInWithTestAccountInfoText": "(tip de cont normal; foloseşte conturile de tip 'dispozitiv' şi cele noi)", + "signInWithTestAccountText": "Conectează-te cu un cont de test", "signInWithV2InfoText": "(un cont care funcționează pe toate platformele)", "signInWithV2Text": "Conectează-te cu un cont Bombsquad", - "signOutText": "Decoectează-te", - "signingInText": "Se conecteză...", + "signOutText": "Deconectează-te", + "signingInText": "Se conectează...", "signingOutText": "Se deconectează...", "testAccountWarningCardboardText": "Atenție: Te conectezi cu un cont \"de test\". Aceste\nconturi vor fi înlocuite cu conturi Google atunci\ncând vor fi suportate în aplicații cardboard.\n\nDeocamdată vei avea să obții toate biletele în joc.\n(Însă vei primi upgrade-ul BombSquad Pro gratis)", "testAccountWarningOculusText": "Atenție: Te conectezi cu un cont \"de test\". Aceste\nconturi vor fi înlocuite cu conturi Oculus mai încolo anul\nacesta ce vor oferi cumpărarea de bilete și alte funcționalități.\n\nDeocamdată vei avea să obții toate biletele în joc.\n(Însă vei primi upgrade-ul BombSquad Pro gratis)", "testAccountWarningText": "Atenție: te conectezi cu un cont \"de test\". Acest cont\nva fi legat numai de acest dispozitiv și va fi resetat\nperiodic. (deci nu pierde prea mult timp colectând/\ndeblocând lucruri pe el)\n\nFolosește o versiune retail a jocului pentru a folosi\ncontul \"real\" (Game-Center, Google+ etc.). Aceasta te\nlasă să iți și salvezi progresul pe cloud și să îl\nîmparți pe mai multe dispozitive.", "ticketsText": "Bilete: ${COUNT}", - "titleText": "Cont", + "titleText": "Contul tău", "unlinkAccountsInstructionsText": "Selectează un cont pentru a-l deconecta", "unlinkAccountsText": "Deconectează Conturi", - "v2LinkInstructionsText": "Folosește acest link pentru a creea un cont sau a te înregistra.", + "v2LinkInstructionsText": "Folosește acest link pentru a creea un cont sau pentru a te autentifica.", "viaAccount": "(prin contul ${NAME})", "youAreSignedInAsText": "Ești conectat ca și:" }, - "achievementChallengesText": "Provocări pentru Realizări", + "achievementChallengesText": "Provocări Pentru Realizări", "achievementText": "Realizare", "achievements": { "Boom Goes the Dynamite": { @@ -54,13 +54,13 @@ "descriptionComplete": "Ai omorât 3 inamici folosind TNT", "descriptionFull": "Omoară 3 inamici folosind TNT în ${LEVEL}", "descriptionFullComplete": "Ai omorât 3 inamici folosind TNT în ${LEVEL}", - "name": "Bum Face Dinamita" + "name": "*Bum* Face Dinamita" }, "Boxer": { "description": "Câștigă fără să folosești vreo bombă", "descriptionComplete": "Ai câștigat fără să folosești vreo bombă", "descriptionFull": "Completează ${LEVEL} fără să folosești vreo bombă", - "descriptionFullComplete": "Ai completat ${LEVEL} fără sa folosești vreo bombă", + "descriptionFullComplete": "Ai completat ${LEVEL} fără să folosești vreo bombă", "name": "Boxer" }, "Dual Wielding": { @@ -73,11 +73,11 @@ "descriptionComplete": "Ai câștigat fără să fii lovit", "descriptionFull": "Câștigă ${LEVEL} fără să fii lovit", "descriptionFullComplete": "Ai câștigat ${LEVEL} fără să fii lovit", - "name": "Victorie perfectă" + "name": "Victorie Impecabilă" }, "Free Loader": { - "descriptionFull": "Porneşte un joc Fiecare-Pentru-El cu 2+ jucători", - "descriptionFullComplete": "Ai pornit un joc Fiecare-Pentru-El cu 2+ jucători", + "descriptionFull": "Începe un joc Fiecare-Pentru-El cu 2+ jucători", + "descriptionFullComplete": "Ai început un joc Fiecare-Pentru-El cu 2+ jucători", "name": "Încărcător Liber" }, "Gold Miner": { @@ -91,55 +91,55 @@ "description": "Câștigă fără să folosești pumni sau bombe", "descriptionComplete": "Ai câștigat fără să folosești pumnii sau bombele", "descriptionFull": "Câștigă ${LEVEL} fără să folosești pumni sau bombe", - "descriptionFullComplete": "Ai câștigat ${LEVEL} fără să folosești pumni sau bombe", - "name": "Știi mișcările" + "descriptionFullComplete": "Ai câștigat ${LEVEL} fără să folosești pumnii sau bombele", + "name": "Știi Mișcările" }, "In Control": { "descriptionFull": "Conectează un controller (fizic sau prin aplicație)", "descriptionFullComplete": "Ai conectat un controller. (fizic sau prin aplicație)", - "name": "În control" + "name": "În Control" }, "Last Stand God": { - "description": "Marchează 1000 de puncte", - "descriptionComplete": "Ai marcat 1000 de puncte", - "descriptionFull": "Înscrie 1000 de punctele în ${LEVEL}", + "description": "Înscrie 1000 de puncte", + "descriptionComplete": "Ai înscris 1000 de puncte", + "descriptionFull": "Înscrie 1000 de puncte în ${LEVEL}", "descriptionFullComplete": "Ai înscris 1000 de puncte în ${LEVEL}", - "name": "Zeu la ${LEVEL}" + "name": "Zeul, ${LEVEL}" }, "Last Stand Master": { "description": "Înscrie 250 de puncte", "descriptionComplete": "Ai înscris 250 de puncte", "descriptionFull": "Înscrie 250 puncte în ${LEVEL}", "descriptionFullComplete": "Ai înscris 250 puncte în ${LEVEL}", - "name": "Maestru ${LEVEL}" + "name": "Maestru la ${LEVEL}" }, "Last Stand Wizard": { "description": "Înscrie 500 de puncte", "descriptionComplete": "Ai înscris 500 de puncte", "descriptionFull": "Înscrie 500 de puncte în ${LEVEL}", "descriptionFullComplete": "Ai înscris 500 de puncte în ${LEVEL}", - "name": "Vrăjitor ${LEVEL}" + "name": "Vrăjitorul din ${LEVEL}" }, "Mine Games": { "description": "Omoară 3 inamici folosind mine", "descriptionComplete": "Ai omorât 3 inamici folosind mine", "descriptionFull": "Omoară 3 inamici folosind mine în ${LEVEL}", "descriptionFullComplete": "Ai omorât 3 inamici folosind mine în ${LEVEL}", - "name": "Jocurile minelor" + "name": "Jocurile Minelor" }, "Off You Go Then": { "description": "Aruncă 3 inamici de pe hartă", "descriptionComplete": "Ai aruncat 3 inamici de pe hartă", "descriptionFull": "Aruncă 3 inamici de pe hartă în ${LEVEL}", "descriptionFullComplete": "Ai aruncat 3 inamici de pe hartă în ${LEVEL}", - "name": "Jos cu tine" + "name": "Jos Cu tine" }, "Onslaught God": { "description": "Înscrie 5000 de puncte", "descriptionComplete": "Ai înscris 5000 de puncte", "descriptionFull": "Înscrie 5000 de puncte în ${LEVEL}", "descriptionFullComplete": "Ai înscris 5000 de puncte în ${LEVEL}", - "name": "Zeu la ${LEVEL}" + "name": "Zeul din ${LEVEL}" }, "Onslaught Master": { "description": "Înscrie 500 de puncte", @@ -149,25 +149,25 @@ "name": "Maestru la ${LEVEL}" }, "Onslaught Training Victory": { - "description": "Înfrânge toate valurile", - "descriptionComplete": "Ai înfrânt toate valurile", - "descriptionFull": "Înfrânge toate valurile în ${LEVEL}", - "descriptionFullComplete": "Ai înfrânt toate valurile în ${LEVEL}", - "name": "Victorie ${LEVEL}" + "description": "Ieși învingător în toate valurile", + "descriptionComplete": "Ai ieșit învingător în toate valurile", + "descriptionFull": "Ieși învingător în toate valurile din ${LEVEL}", + "descriptionFullComplete": "Ai ieșit învingător în toate valurile din ${LEVEL}", + "name": "Victorie în ${LEVEL}" }, "Onslaught Wizard": { "description": "Înscrie 1000 de puncte", "descriptionComplete": "Ai înscris 1000 de puncte", "descriptionFull": "Înscrie 1000 de puncte în ${LEVEL}", "descriptionFullComplete": "Ai înscris 1000 de puncte în ${LEVEL}", - "name": "Vrăjitor ${LEVEL}" + "name": "Vrăjitorul din ${LEVEL}" }, "Precision Bombing": { "description": "Câștigă fără să folosești powerup-uri", "descriptionComplete": "Ai câștigat fără să folosești powerup-uri", "descriptionFull": "Câștigă ${LEVEL} fără să folosești powerup-uri", "descriptionFullComplete": "Ai câștigat ${LEVEL} fără să folosești powerup-uri", - "name": "Bombardament de precizie" + "name": "Bombardament De Precizie" }, "Pro Boxer": { "description": "Câștigă fără să folosești vreo bombă", @@ -181,48 +181,48 @@ "descriptionComplete": "Ai câștigat fără să-i lași pe inamici să înscrie", "descriptionFull": "Câștigă ${LEVEL} fără să-i lași pe inamici să înscrie", "descriptionFullComplete": "Ai câștigat ${LEVEL} fără să-i lași pe inamici să înscrie", - "name": "Shut-out ${LEVEL}" + "name": "Shut-out în ${LEVEL}" }, "Pro Football Victory": { - "description": "Câștigă jocul", - "descriptionComplete": "Ai câștigat jocul", - "descriptionFull": "Câștigă jocul din ${LEVEL}", - "descriptionFullComplete": "Ai câștigat jocul din ${LEVEL}", + "description": "Câștigă meciul", + "descriptionComplete": "Ai câștigat meciul", + "descriptionFull": "Câștigă meciul din ${LEVEL}", + "descriptionFullComplete": "Ai câștigat meciul din ${LEVEL}", "name": "Victorie în ${LEVEL}" }, "Pro Onslaught Victory": { - "description": "Înfrânge toate valurile", - "descriptionComplete": "Ai înfrânt toate valurile", - "descriptionFull": "Înfrânge toate valurile din ${LEVEL}", - "descriptionFullComplete": "Ai înfrânt toate valurile din ${LEVEL}", - "name": "Victorie ${LEVEL}" + "description": "Ieși învingător în toate valurile", + "descriptionComplete": "Ai ieșit învingător în toate valurile", + "descriptionFull": "Ieși învingător în toate valurile din ${LEVEL}", + "descriptionFullComplete": "Ai ieșit învingător în toate valurile din ${LEVEL}", + "name": "Victorie în ${LEVEL}" }, "Pro Runaround Victory": { "description": "Completează toate valurile", "descriptionComplete": "Ai completat toate valurile", "descriptionFull": "Completează toate valurile din ${LEVEL}", "descriptionFullComplete": "Ai completat toate valurile din ${LEVEL}", - "name": "Victorie ${LEVEL}" + "name": "Victorie în ${LEVEL}" }, "Rookie Football Shutout": { "description": "Câștigă fără să-i lași pe tipii răi să înscrie", "descriptionComplete": "Ai câștigat fără să-i lași pe tipii răi să înscrie", "descriptionFull": "Câștigă ${LEVEL} fără să-i lași pe tipii răi să înscrie", "descriptionFullComplete": "Ai câștigat ${LEVEL} fără să-i lași pe tipii răi să înscrie", - "name": "Shut-out ${LEVEL}" + "name": "Shut-out în ${LEVEL}" }, "Rookie Football Victory": { - "description": "Câștigă jocul", - "descriptionComplete": "Ai câștigat jocul", - "descriptionFull": "Câștigă jocul din ${LEVEL}", - "descriptionFullComplete": "Ai câștigat jocul din ${LEVEL}", - "name": "Victorie ${LEVEL}" + "description": "Câștigă meciul", + "descriptionComplete": "Ai câștigat meciul", + "descriptionFull": "Câștigă meciul din ${LEVEL}", + "descriptionFullComplete": "Ai câștigat meciul din ${LEVEL}", + "name": "Victorie în ${LEVEL}" }, "Rookie Onslaught Victory": { - "description": "Înfrânge toate valurile", - "descriptionComplete": "Ai înfrânt toate valurile", - "descriptionFull": "Înfrânge toate valurile din ${LEVEL}", - "descriptionFullComplete": "Ai înfrânt toate valurile din ${LEVEL}", + "description": "Ieși învingător în toate valurile", + "descriptionComplete": "Ai ieșit învingător în toate valurile", + "descriptionFull": "Ieși învingător în toate valurile din ${LEVEL}", + "descriptionFullComplete": "Ai ieșit învingător în toate valurile din ${LEVEL}", "name": "Victorie în ${LEVEL}" }, "Runaround God": { @@ -241,7 +241,7 @@ }, "Runaround Wizard": { "description": "Înscrie 1000 de puncte", - "descriptionComplete": "Ai strâns 1000 de puncte", + "descriptionComplete": "Ai înscris 1000 de puncte", "descriptionFull": "Înscrie 1000 de puncte în ${LEVEL}", "descriptionFullComplete": "Ai înscris 1000 de puncte în ${LEVEL}", "name": "Vrăjitorul din ${LEVEL}" @@ -263,7 +263,7 @@ "descriptionComplete": "Ai cauzat o daună de 100% cu o singură lovitură", "descriptionFull": "Cauzează o daună de 100% cu o singură lovitură în ${LEVEL}", "descriptionFullComplete": "Ai cauzat o daună de 100% cu o singură lovitură în ${LEVEL}", - "name": "Super Mega Lovitură" + "name": "Super-Mega Pumn" }, "Super Punch": { "description": "Cauzează o daună de 50% cu o singură lovitură", @@ -282,20 +282,20 @@ "Team Player": { "descriptionFull": "Porneşte un joc pe Echipe cu 4+ jucători", "descriptionFullComplete": "Ai pornit un joc pe Echipe cu 4+ jucători", - "name": "Jucător în Echipă" + "name": "Jucătorul Din Echipă" }, "The Great Wall": { - "description": "Opreşte fiecare tip rău", - "descriptionComplete": "Ai oprit fiecare tip rău", - "descriptionFull": "Opreşte fiecare tip rău din ${LEVEL}", - "descriptionFullComplete": "Ai oprit fiecare tip rău din ${LEVEL}", + "description": "Opreşte fiecare inamic", + "descriptionComplete": "Ai oprit fiecare inamic", + "descriptionFull": "Opreşte fiecare inamic din ${LEVEL}", + "descriptionFullComplete": "Ai oprit fiecare inamic din ${LEVEL}", "name": "Marele Zid" }, "The Wall": { - "description": "Oprește fiecare tip rău", - "descriptionComplete": "Ai oprit fiecare tip rău", - "descriptionFull": "Oprește fiecare tip rău din ${LEVEL}", - "descriptionFullComplete": "Ai oprit fiecare tip rău din ${LEVEL}", + "description": "Oprește fiecare inamic", + "descriptionComplete": "Ai oprit fiecare inamic", + "descriptionFull": "Oprește fiecare inamic din ${LEVEL}", + "descriptionFullComplete": "Ai oprit fiecare inamic din ${LEVEL}", "name": "Zidul" }, "Uber Football Shutout": { @@ -303,108 +303,110 @@ "descriptionComplete": "Ai câștigat fără să-i lași pe inamici să înscrie", "descriptionFull": "Câștigă ${LEVEL} fără să-i lași pe inamici să înscrie", "descriptionFullComplete": "Ai câștigat ${LEVEL} fără să-i lași pe inamici să înscrie", - "name": "Shut-out ${LEVEL}" + "name": "Shut-out în ${LEVEL}" }, "Uber Football Victory": { - "description": "Câștigă jocul", - "descriptionComplete": "Ai câștigat jocul", - "descriptionFull": "Câștigă jocul din ${LEVEL}", - "descriptionFullComplete": "Ai câștigat jocul din ${LEVEL}", + "description": "Câștigă meciul", + "descriptionComplete": "Ai câștigat meciul", + "descriptionFull": "Câștigă meciul din ${LEVEL}", + "descriptionFullComplete": "Ai câștigat meciul din ${LEVEL}", "name": "Victorie în ${LEVEL}" }, "Uber Onslaught Victory": { - "description": "Înfrânge toate valurile", - "descriptionComplete": "Ai înfrânt toate valurile", - "descriptionFull": "Înfrânge toate valurile din ${LEVEL}", - "descriptionFullComplete": "Ai înfrânt toate valurile din ${LEVEL}", + "description": "Învinge în toate valurile", + "descriptionComplete": "Ai învins în toate valurile", + "descriptionFull": "Învinge în toate valurile din ${LEVEL}", + "descriptionFullComplete": "Ai învins în toate valurile din ${LEVEL}", "name": "Victorie în ${LEVEL}" }, "Uber Runaround Victory": { "description": "Completează toate valurile", "descriptionComplete": "Ai completat toate valurile", "descriptionFull": "Completează toate valurile din ${LEVEL}", - "descriptionFullComplete": "Ai terminat toate valurile din ${LEVEL}", + "descriptionFullComplete": "Ai completat toate valurile din ${LEVEL}", "name": "Victorie în ${LEVEL}" } }, - "achievementsRemainingText": "Realizări rămase:", + "achievementsRemainingText": "Realizări Rămase:", "achievementsText": "Realizări", "achievementsUnavailableForOldSeasonsText": "Scuze, dar detaliile realizărilor din sezoanele trecute sunt indisponibile.", + "activatedText": "${THING} a fost activat(ă).", "addGameWindow": { - "getMoreGamesText": "Ia mai multe jocuri...", + "getMoreGamesText": "Ia mai multe MiniJocuri...", "titleText": "Adaugă un joc" }, "allowText": "Permite", "alreadySignedInText": "Contul tău este deja conectat de pe un alt dispozitiv;\nte rog să schimbi conturile sau să închizi jocul de pe\nalte dispozitive și să încerci din nou.", - "apiVersionErrorText": "Nu se poate deschide moduluL ${NAME}; acela accesează versiunea api ${VERSION_USED}, pe când versiunea ${VERSION_REQUIRED} este nevoită.", + "apiVersionErrorText": "Nu se poate încărca modulul ${NAME}; acesta țintește versiunea API ${VERSION_USED}, iar versiunea ${VERSION_REQUIRED} nu îl mai suportă.", "audioSettingsWindow": { "headRelativeVRAudioInfoText": "(\"Auto\" activează asta doar când căștile sunt conectate)", "headRelativeVRAudioText": "Audio VR relativ capului", "musicVolumeText": "Volumul Muzicii", "soundVolumeText": "Volumul Sunetelor", - "soundtrackButtonText": "Coloană sonoră", - "soundtrackDescriptionText": "(selectează muzica care vrei să cânte în timpul jocului)", + "soundtrackButtonText": "Soundtrack Personalizat", + "soundtrackDescriptionText": "(selectează melodiile care vrei să cânte în timpul jocului)", "titleText": "Audio" }, "autoText": "Automat", "backText": "Înapoi", "banThisPlayerText": "Interzice Accesul Acestui Jucător", - "bestOfFinalText": "Finala cel-mai-bun-din-${COUNT}", - "bestOfSeriesText": "Seriile cel mai bun din ${COUNT}:", + "bestOfFinalText": "Finala Cel-Mai-Bun-Din-${COUNT}", + "bestOfSeriesText": "Seriile Cel-Mai-Bun-Din-${COUNT}:", + "bestOfUseFirstToInstead": 0, "bestRankText": "Cea mai bună poziție a ta este: #${RANK}", - "bestRatingText": "Cel mai bun rating al tău este ${RATING}", + "bestRatingText": "Cea mai bună notă a ta este ${RATING}", "bombBoldText": "BOMBĂ", "bombText": "Bombă", - "boostText": "Crește-ți", - "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} este configurat în aplicație", + "boostText": "Crește", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} este configurat în aplicația în sine.", "buttonText": "buton", - "canWeDebugText": "Ai vrea ca BombSquad să trimită automat bug-uri,\ncrash-uri și informații de bază programatorului jocului?\n\nAceste informații nu conțin date personale ci doar\najută la îmbunătățirea jocului.", + "canWeDebugText": "Ai vrea ca BombSquad să trimită automat bug-uri,\ncrash-uri și informații de bază programatorului jocului?\n\nAceste informații nu conțin date personale, ci doar\najută la îmbunătățirea jocului.", "cancelText": "Anulează", "cantConfigureDeviceText": "Scuze, dar ${DEVICE} nu este configurabil.", "challengeEndedText": "Acest concurs s-a terminat.", - "chatMuteText": "Dezactiveaza chat-ul", - "chatMutedText": "Chat Dezactivat", - "chatUnMuteText": "Reactiveaza chat-ul", + "chatMuteText": "Amuțește Chat-ul", + "chatMutedText": "Chat-ul Este Amuțit", + "chatUnMuteText": "Dezamuțește Chat-ul", "choosingPlayerText": "", "completeThisLevelToProceedText": "Trebuie să completezi\nacest nivel pentru a continua!", - "completionBonusText": "Bonus de completare", + "completionBonusText": "Bonus De Completare", "configControllersWindow": { - "configureControllersText": "Configurează controllere", - "configureKeyboard2Text": "Configurează tastatura pentru P2", - "configureKeyboardText": "Configurează tastatura", - "configureMobileText": "Dispozitive mobile ca și controllere", - "configureTouchText": "Configurează Touchscreen", + "configureControllersText": "Configurează Controllere", + "configureKeyboard2Text": "Configurează Tastatura Pentru P2", + "configureKeyboardText": "Configurează Tastatura", + "configureMobileText": "Dispozitive Mobile Drept Controllere", + "configureTouchText": "Configurează Touchscreen-ul", "ps3Text": "Controllere de PS3", "titleText": "Controllere", "wiimotesText": "Controllere pentru Wii", - "xbox360Text": "Controllere de X-Box 360" + "xbox360Text": "Controllere de Xbox 360" }, "configGamepadSelectWindow": { - "androidNoteText": "Notă: suportul pentru controllere poate varia de la versiunea android și de la dispozitiv.", + "androidNoteText": "Notă: suportul pentru controllere poate varia de la un dispozitiv la altul și de la o versiune Android la alta.", "pressAnyButtonText": "Apasă pe orice buton de pe controllerul\npe care ai vrea să-l configurezi...", - "titleText": "Configurează controllere" + "titleText": "Configurează Controllere" }, "configGamepadWindow": { - "advancedText": "Avansat", - "advancedTitleText": "Setări controller avansate", - "analogStickDeadZoneDescriptionText": "(Ridică valoarea acestuia dacă iți \"alunecă\" caracterul când dai drumul la \"stick\")", + "advancedText": "Avansate", + "advancedTitleText": "Setări Avansate Pentru Controller", + "analogStickDeadZoneDescriptionText": "(Ridică această valoare dacă îți \"alunecă\" caracterul când iei mâna de pe \"stick\")", "analogStickDeadZoneText": "Zona-Moartă a Stick-ului Analog", "appliesToAllText": "(se aplică la toate controllerele de acest fel)", "autoRecalibrateDescriptionText": "(activează asta dacă caracterul tău nu se mișcă la viteza maximă)", "autoRecalibrateText": "Recalibrare Automată a Stick-ului Analog", "axisText": "axă", "clearText": "șterge", - "dpadText": "D-pad", - "extraStartButtonText": "Buton Start Secundar", + "dpadText": "D-Pad", + "extraStartButtonText": "Buton De Start Secundar", "ifNothingHappensTryAnalogText": "Dacă nu se întâmplă nimic, încearcă să folosești stick-ul analog.", - "ifNothingHappensTryDpadText": "Dacă nu se întâmplă nimic, încearcă D-pad-ul.", - "ignoreCompletelyDescriptionText": "(fă în așa fel încât acest controller sa nu afecteze jocul sau meniul)", - "ignoreCompletelyText": "Ignoră-l Complet", + "ifNothingHappensTryDpadText": "Dacă nu se întâmplă nimic, încearcă D-Pad-ul.", + "ignoreCompletelyDescriptionText": "(fă în așa fel încât acest controller să nu afecteze jocul sau meniul)", + "ignoreCompletelyText": "Ignorare Completă", "ignoredButton1Text": "Buton Ignorat 1", "ignoredButton2Text": "Buton Ignorat 2", "ignoredButton3Text": "Buton Ignorat 3", "ignoredButton4Text": "Buton Ignorat 4", - "ignoredButtonDescriptionText": "(folosește asta pentru ca butoanele 'home' sau 'sync' să fie ignorate în timpul jocului)", + "ignoredButtonDescriptionText": "(folosește asta pentru ignorarea butoanelor 'home' sau 'sync' în timpul jocului)", "pressAnyAnalogTriggerText": "Apasă orice trigger analog...", "pressAnyButtonOrDpadText": "Apasă orice buton sau D-Pad-ul", "pressAnyButtonText": "Apasă orice buton...", @@ -422,26 +424,26 @@ "startButtonActivatesDefaultText": "Butonul de Start Activează Widget-ul Implicit", "titleText": "Setup Pentru Controller", "twoInOneSetupText": "Setup Pentru Un Controller 2-în-1", - "uiOnlyDescriptionText": "(nu-i permite acestui controller să intre în joc)", + "uiOnlyDescriptionText": "(permite-i acestui controller doar navigarea prin meniuri)", "uiOnlyText": "Limitează la Folosirea Meniului", - "unassignedButtonsRunText": "Toate Butoanele Neatribuite Rulează", - "unsetText": "<șterge>", - "vrReorientButtonText": "Buton de Reorientare VR" + "unassignedButtonsRunText": "Rularea Tuturor Butoanelor Neatribuite", + "unsetText": "", + "vrReorientButtonText": "Buton De Reorientare VR" }, "configKeyboardWindow": { "configuringText": "${DEVICE} se configurează", - "keyboard2NoteText": "Notă: multe tastaturi pot inregistra doar câteva apăsări\nde-odată, deci avănd altă tastatură ar merge mai bine\ndacă există un al doilea jucător.\nȚine minte că va trebui să aplici controale diferite celor doi jucători,\nchiar şi în cazul de mai sus." + "keyboard2NoteText": "Notă: majoritatea tastaturilor pot înregistra doar câteva apăsări\nde-odată, deci având o altă tastatură ar merge mai bine\ndacă există un al doilea jucător.\nȚine minte faptul că tot va trebui să atribui controale diferite \ncelor doi jucători, chiar şi în cazul de mai sus." }, "configTouchscreenWindow": { - "actionControlScaleText": "Dimensiune Controluri Acțiuni", + "actionControlScaleText": "Dimensiunea Butoanelor De Acțiuni", "actionsText": "Acțiuni", "buttonsText": "butoane", - "dragControlsText": "< trage de controluri pentru a le repoziționa >", + "dragControlsText": "< trage de controale pentru a le repoziționa >", "joystickText": "joystick", - "movementControlScaleText": "Dimensiune Control Mișcare", + "movementControlScaleText": "Dimensiunea Săgeților De Mișcare", "movementText": "Mișcare", "resetText": "Resetare", - "swipeControlsHiddenText": "Ascunde Iconițele De Glisare", + "swipeControlsHiddenText": "Ascunde Săgețile", "swipeInfoText": "Durează ceva timp până te înveți cu controalele de tip 'Glisare'\ndar o să-ți fie mult mai ușor să te joci fără să te uiți la controale.", "swipeText": "glisare", "titleText": "Configurează Touchscreen-ul" @@ -451,29 +453,29 @@ "connectMobileDevicesWindow": { "amazonText": "Amazon Appstore", "appStoreText": "App Store", - "bestResultsText": "Pentru cele mai bune rezultate vei avea nevoie de o rețea Wi-Fi \nfără lag. Poți să reduci lag-ul prin oprirea Wi-Fi-ului de pe alte telefoane,\nprin jucarea lângă router, sau prin conectarea \"jocului-mamă\" direct la rețea\nprin Ethernet.", - "explanationText": "Pentru a folosi un telefon sau o tabletă drept controller, instalează\naplicația \"${REMOTE_APP_NAME}\" pe acestea. Orice dispozitiv se poate conecta la\nun joc ${APP_NAME} prin Wi-Fi, şi este gratis!", + "bestResultsText": "Pentru cele mai bune rezultate vei avea nevoie de o rețea Wi-Fi \nfără lag. Poți să reduci lag-ul prin oprirea Wi-Fi-ului de pe alte dispozitive,\nprin jucarea lângă router, sau prin conectarea directă la \"jocul-mamă\"\ncu ajutorul Ethernet-ului.", + "explanationText": "Pentru a folosi un telefon sau o tabletă drept controller, instalează\naplicația \"${REMOTE_APP_NAME}\" pe acestea. Orice dispozitiv se poate conecta la\nun joc ${APP_NAME} prin Wi-Fi, şi este gratuită!", "forAndroidText": "pentru Android:", "forIOSText": "Pentru iOS:", - "getItForText": "Ia-ți aplicația ${REMOTE_APP_NAME} pentru iOS din Apple App Store,\npentru Android din Google Play Store sau de pe Amazon Appstore", + "getItForText": "Instalează-ți aplicația ${REMOTE_APP_NAME} pentru iOS din Apple App Store,\npentru Android din Google Play Store sau de pe Amazon Appstore", "googlePlayText": "Google Play", - "titleText": "Folosirea Dispozitivelor Mobile drept Controllere:" + "titleText": "Folosirea Dispozitivelor Mobile Drept Controllere:" }, "continuePurchaseText": "Continui pentru ${PRICE}?", - "continueText": "Continui", + "continueText": "Continuă", "controlsText": "Controale", "coopSelectWindow": { - "activenessAllTimeInfoText": "Acesta nu se aplică clasamentelor din totdeauna.", - "activenessInfoText": "Acest multiplicator crește în zilele în\ncare joci și scade în cele in care nu joci.", + "activenessAllTimeInfoText": "Asta nu se aplică clasamentelor din totdeauna.", + "activenessInfoText": "Acest multiplicator crește în zilele în\ncare joci și scade în cele în care nu joci.", "activityText": "Activitate", "campaignText": "Campanie", - "challengesInfoText": "Primeşte premii pentru completarea minijocurilor.\n\nPremiile şi dificultatea nivelelor cresc\nde fiecare dată când un Challenge este făcut\nşi scade când unul expiră sau este abandonat.", + "challengesInfoText": "Câștigă premii pentru completarea minijocurilor.\n\nPremiile şi dificultatea nivelelor cresc\nde fiecare dată când un challenge este făcut\nşi scade când unul expiră sau este abandonat.", "challengesText": "Provocări", "currentBestText": "Cel mai bun de până acum", - "customText": "Particularizare", - "entryFeeText": "Intrarea", + "customText": "Particularizate", + "entryFeeText": "Intrare", "forfeitConfirmText": "Abandonezi acest challenge?", - "forfeitNotAllowedYetText": "Acest concurs nu poate fi abandonat chiar acum.", + "forfeitNotAllowedYetText": "Acest challenge nu poate fi abandonat chiar acum.", "forfeitText": "Abandonează", "multipliersText": "Multiplicatori", "nextChallengeText": "Challenge-ul Următor", @@ -483,40 +485,40 @@ "pointsText": "Puncte", "powerRankingFinishedSeasonUnrankedText": "(ai terminat sezonul fără rank)", "powerRankingNotInTopText": "(nu eşti în top ${NUMBER})", - "powerRankingPointsEqualsText": "= ${NUMBER} (de) puncte", - "powerRankingPointsMultText": "(x ${NUMBER} puncte)", - "powerRankingPointsText": "${NUMBER} puncte", - "powerRankingPointsToRankedText": "(${CURRENT} din ${REMAINING} puncte)", - "powerRankingText": "Rank-ul Tău de Putere", + "powerRankingPointsEqualsText": "= ${NUMBER} pte", + "powerRankingPointsMultText": "(x ${NUMBER} pte)", + "powerRankingPointsText": "${NUMBER} pte", + "powerRankingPointsToRankedText": "(${CURRENT} din ${REMAINING} pte)", + "powerRankingText": "Rank-ul tău de putere", "prizesText": "Premii", "proMultInfoText": "Jucătorii cu upgrade-ul ${PRO} \nprimesc aici un bonus de ${PERCENT}% la puncte.", "seeMoreText": "Mai multe...", "skipWaitText": "Treci fără să mai aștepți", "timeRemainingText": "Timp Rămas", - "toRankedText": "Către Rank", + "toRankedText": "Pentru Rankare", "totalText": "total", - "tournamentInfoText": "Întrece-te pentru scoruri mari\ncu alți jucători din liga ta.\n\nPremiile sunt dăruite jucătorilor cu\nscoruri din top când expiră concursul.", + "tournamentInfoText": "Întrece-te pentru scoruri mari\ncu alți jucători din liga ta.\n\nPremiile sunt dăruite jucătorilor cu\nscoruri din lista de top când expiră concursul.", "welcome1Text": "Bine ai venit în ${LEAGUE}. Poți să-ți măreşti\nrankul prin adunarea medaliilor, câştigarea trofeelor\nîn concursuri şi prin rankul de 3 stele în jocuri.", "welcome2Text": "Mai poți primi bilete şi prin alte activități de acelaşi fel.\nBiletele se pot folosi pentru a debloca charactere, hărți,\nmini-jocuri, pentru a intra în concursurii, şi multe altele.", "yourPowerRankingText": "Rankul tău de Putere:" }, - "copyOfText": "Copie ${NAME}", + "copyOfText": "Copie de ${NAME}", "copyText": "Copiază", - "createEditPlayerText": "", + "createEditPlayerText": "", "createText": "Creează", "creditsWindow": { "additionalAudioArtIdeasText": "Sunet Adițional, Artă, și Idei de ${NAME}", "additionalMusicFromText": "Muzică Adițională de ${NAME}", - "allMyFamilyText": "Toți prietenii și familia care m-au ajutat să testez jocul", + "allMyFamilyText": "Familiei mele și tuturor prietenilor mei care m-au ajutat să testez jocul", "codingGraphicsAudioText": "Cod, Grafice, şi Audio de ${NAME}", "languageTranslationsText": "Traduceri:", "legalText": "Legal:", - "publicDomainMusicViaText": "Muzica din domeniul Public ${NAME}", - "softwareBasedOnText": "Acest software este bazat în partea de lucru al lui ${NAME}", + "publicDomainMusicViaText": "Muzică din domeniul Public ${NAME}", + "softwareBasedOnText": "Acest software este bazat în partea de lucru a lui ${NAME}", "songCreditText": "${TITLE} Performat de ${PERFORMER}\nCompus de ${COMPOSER}, Aranjat de ${ARRANGER}, Publicat de ${PUBLISHER},\ncurtoazie din ${SOURCE}", "soundAndMusicText": "Sunete & Muzică:", "soundsText": "Sunete (${SOURCE}):", - "specialThanksText": "Mulțumiri speciale:", + "specialThanksText": "Mulțumiri Speciale:", "thanksEspeciallyToText": "Mulțumiri deosebite: ${NAME}", "titleText": "Credite ${APP_NAME}", "whoeverInventedCoffeeText": "Celui care a inventat cafeaua" @@ -525,32 +527,32 @@ "customizeText": "Particularizează...", "deathsTallyText": "${COUNT} vieți pierdute", "deathsText": "Vieți pierdute", - "debugText": "Eliminare de buguri", + "debugText": "Debug", "debugWindow": { "reloadBenchmarkBestResultsText": "Notă: Este recomandat ca tu să te duci la Setări->Grafici->Texturi și să le dai pe 'Înalte' când testezi asta.", "runCPUBenchmarkText": "Rulează test CPU", "runGPUBenchmarkText": "Rulează test GPU", - "runMediaReloadBenchmarkText": "Pornește test de Încărcare Media", + "runMediaReloadBenchmarkText": "Pornește Test De Reîncărcare Media", "runStressTestText": "Pornește testul de stres", "stressTestPlayerCountText": "Număr de Jucători", "stressTestPlaylistDescriptionText": "Lista de Jocuri pentru Testele de Stres", - "stressTestPlaylistNameText": "Numele listei de jocuri", + "stressTestPlaylistNameText": "Numele Listei de Jocuri", "stressTestPlaylistTypeText": "Tipul Listei de Jocuri", - "stressTestRoundDurationText": "Durata rundei", + "stressTestRoundDurationText": "Durata Rundei", "stressTestTitleText": "Test de Stres", - "titleText": "Teste de Stres şi repere", + "titleText": "Teste de Stres şi Repere", "totalReloadTimeText": "Timp total de reîncărcare: ${TIME} (vezi jurnalul de activități pentru detalii)" }, - "defaultGameListNameText": "Lista de jocuri de ${PLAYMODE} normală", - "defaultNewGameListNameText": "Lista mea de jocuri de ${PLAYMODE}", + "defaultGameListNameText": "Lista de Jocuri de \"${PLAYMODE}\" Implicită", + "defaultNewGameListNameText": "Lista mea de jocuri de \"${PLAYMODE}\"", "deleteText": "Șterge", "demoText": "Demo", "denyText": "Refuză", - "desktopResText": "Rezoluție desktop", + "desktopResText": "Rezoluție Desktop", "difficultyEasyText": "Ușor", "difficultyHardOnlyText": "Numai pe \"Greu\"", "difficultyHardText": "Greu", - "difficultyHardUnlockOnlyText": "Acest nivel poate fi deblocat numai pe \"Greu\".\nCrezi că poți s-o faci!?!?!", + "difficultyHardUnlockOnlyText": "Acest nivel poate fi deblocat numai pe modul \"Greu\".\nTe crezi în stare!?!?!", "directBrowserToURLText": "Te rog să direcționezi un browser web la următorul URL:", "disableRemoteAppConnectionsText": "Dezactivează conexiunile de pe Remote-App", "disableXInputDescriptionText": "Permite mai mult de 4 controlere dar nu va merge chiar așa de bine.", @@ -560,7 +562,7 @@ "duplicateText": "Multiplică", "editGameListWindow": { "addGameText": "Adaugă\nun joc", - "cantOverwriteDefaultText": "Nu poți înlocui lista de jocuri normală!", + "cantOverwriteDefaultText": "Nu poți modifica lista de jocuri implicită!", "cantSaveAlreadyExistsText": "O listă de jocuri cu acelaşi nume deja există!", "cantSaveEmptyListText": "Nu poți salva o listă goală!", "editGameText": "Editează\nJocul", @@ -572,7 +574,7 @@ }, "editProfileWindow": { "accountProfileInfoText": "Acest profil special are nume\nşi o imagine bazată pe contul tău.\n\n${ICONS}\n\nCreează profile personalizate\npentru a folosi nume sau iconițe personalizate.", - "accountProfileText": "(profil al contului)", + "accountProfileText": "(profilul contului)", "availableText": "Numele \"${NAME}\" este utilizabil.", "changesNotAffectText": "Notă: schimbările nu vor afecta caracterele care sunt deja în joc", "characterText": "caracter", @@ -584,7 +586,7 @@ "globalProfileText": "(profil global)", "highlightText": "accent", "iconText": "iconiță", - "localProfileInfoText": "Profilurile locale nu au iconițe şi numele lor nu sunt \ngarantate să fie speciale. Upgradează-ți profilul\npentru a rezerva un nume special şi să adaugi o iconiță.", + "localProfileInfoText": "Profilurile locale nu au iconițe şi numele lor nu sunt \ngarantate să fie speciale. Upgradează-ți profilul\npentru a-ți rezerva un nume special şi pentru a adăuga o iconiță la acesta.", "localProfileText": "(profil local)", "nameDescriptionText": "Numele Jucătorului", "nameText": "Nume", @@ -593,15 +595,15 @@ "titleNewText": "Profil nou", "unavailableText": "Numele \"${NAME}\" nu este disponibil; încearcă alt nume.", "upgradeProfileInfoText": "Acest lucru îți va rezerva numele de jucător\nşi te va lăsa să introduci o iconiță pentru acesta.", - "upgradeToGlobalProfileText": "Îmbunătățește-l la un Profil Global" + "upgradeToGlobalProfileText": "Transformă-l în Profil Global" }, "editSoundtrackWindow": { - "cantDeleteDefaultText": "Nu poți şterge soundtrackul normal.", - "cantEditDefaultText": "Nu poți edita soundtrackul normal. Duplică-l sau creează unul nou.", - "cantOverwriteDefaultText": "Nu poți înlocui soundtrackul normal", - "cantSaveAlreadyExistsText": "Un soundtrack cu acelaşi nume deja există!", - "defaultGameMusicText": "", - "defaultSoundtrackNameText": "Soundtrack-ul Normal", + "cantDeleteDefaultText": "Nu poți şterge soundtrack-ul implicit.", + "cantEditDefaultText": "Nu poți edita soundtrack-ul implicit. Duplică-l sau creează altul nou.", + "cantOverwriteDefaultText": "Nu poți înlocui soundtrack-ul implicit", + "cantSaveAlreadyExistsText": "Un soundtrack cu acelaşi nume există deja!", + "defaultGameMusicText": "", + "defaultSoundtrackNameText": "Soundtrack-ul Implicit", "deleteConfirmText": "Ştergi Soundtrack-ul:\n\n'${NAME}'?", "deleteText": "Şterge\nSoundtrack-ul", "duplicateText": "Duplică\nSoundtrack-ul", @@ -617,10 +619,10 @@ "selectASourceText": "Sursa Muzicii", "testText": "testează", "titleText": "Soundtrack-uri", - "useDefaultGameMusicText": "Muzica Normală din Joc", + "useDefaultGameMusicText": "Folosește Muzica Implicită", "useITunesPlaylistText": "Lista de Redare a Aplicației Muzicale", - "useMusicFileText": "Fişier de Muzică (mp3, etc)", - "useMusicFolderText": "Dosar cu Fişiere de Muzică" + "useMusicFileText": "Fişier Audio (MP3, etc)", + "useMusicFolderText": "Dosar cu Fişiere Audio" }, "editText": "Editează", "endText": "Sfârșește", @@ -628,26 +630,28 @@ "epicDescriptionFilterText": "${DESCRIPTION} În slow motion epic.", "epicNameFilterText": "${NAME} Epic", "errorAccessDeniedText": "acces respins", + "errorDeviceTimeIncorrectText": "Ceasul dispozitivului tău este dat înainte sau înapoi cu ${HOURS} ore.\nAcestu lucru ar putea cauza niște probleme.\nTe rog să verifici setările ceasului și ale fusului orar.", "errorOutOfDiskSpaceText": "ai rămas fără memorie", + "errorSecureConnectionFailText": "Nu s-a putut stabili o conexiune sigură cloud; funcționalitatea internetului ar putea eșua.", "errorText": "Eroare", "errorUnknownText": "eroare necunoscută", "exitGameText": "Ieși din ${APP_NAME}?", - "exportSuccessText": "'${NAME}' exportat cu succes.", + "exportSuccessText": "'${NAME}' a fost exportat cu succes.", "externalStorageText": "Memorie Externă", "failText": "Eșec", - "fatalErrorText": "Oh nu; ceva lipseşte sau este stricat.\nTe rog încearcă să reinstalezi aplicația sau\ncontactează E-Mail-ul ${EMAIL} pentru ajutor.", + "fatalErrorText": "Oh nu; ceva lipseşte sau este stricat.\nTe rog încearcă să reinstalezi aplicația sau\ncontactează adresa de E-mail ${EMAIL} pentru ajutor.", "fileSelectorWindow": { "titleFileFolderText": "Selectează un Fișier sau un Dosar", "titleFileText": "Selectează un fișier", "titleFolderText": "Selectează un Dosar", "useThisFolderButtonText": "Folosește Acest Dosar" }, - "filterText": "Filtru", + "filterText": "Caută", "finalScoreText": "Scor Final", "finalScoresText": "Scoruri Finale", "finalTimeText": "Timp Final", "finishingInstallText": "Se termină de instalat; un moment...", - "fireTVRemoteWarningText": "* Pentru o experiență mai bună,\nfolosiți Controllere sau instalați\naplicația '${REMOTE_APP_NAME}' pe telefon\nsau tabletă.", + "fireTVRemoteWarningText": "* Pentru o experiență mai bună,\nfolosiți Controllere sau instalați\naplicația '${REMOTE_APP_NAME}' pe \ntelefoane sau pe tablete.", "firstToFinalText": "Finala Primul-la-${COUNT}", "firstToSeriesText": "Seriile Primul-la-${COUNT}", "fiveKillText": "Penta-omor!!!", @@ -658,17 +662,17 @@ "gameCircleText": "GameCircle", "gameLeadersText": "Liderii Jocului ${COUNT}", "gameListWindow": { - "cantDeleteDefaultText": "Nu poți şterge lista de jocuri normală.", + "cantDeleteDefaultText": "Nu poți şterge lista de jocuri implicită.", "cantEditDefaultText": "Nu poți edita lista de jocuri normală! Duplic-o sau creează una nouă.", - "cantShareDefaultText": "Nu poti trimite mai departe playlist-ul normal", + "cantShareDefaultText": "Nu poti trimite mai departe lista de jocuri implicită.", "deleteConfirmText": "Ştergi \"${LIST}\"?", "deleteText": "Şterge\nLista de Jocuri", "duplicateText": "Duplică\nLista de Jocuri", "editText": "Editează\nLista de Jocuri", "newText": "Listă de Jocuri\nNouă", "showTutorialText": "Arată Tutorialul", - "shuffleGameOrderText": "Hărți Aleatorii", - "titleText": "Particularizează Listele de Jocuri de tip ${TYPE}" + "shuffleGameOrderText": "Jocuri Aleatorii", + "titleText": "Particularizează Listele de Jocuri de tip \"${TYPE}\"" }, "gameSettingsWindow": { "addGameText": "Adaugă un Joc" @@ -676,13 +680,13 @@ "gamesToText": "${WINCOUNT} jocuri la ${LOSECOUNT}", "gatherWindow": { "aboutDescriptionLocalMultiplayerExtraText": "Ține minte: orice dispozitiv dintr-o petrecere poate avea\nmai mult de un jucător dacă sunt destule controllere.", - "aboutDescriptionText": "Folosește aceste file pentru a asambla o petrecere.\n\nPetrecerile te lasă să joci jocuri și turnee împreună\ncu prietenii tăi, folosind mai multe dispozitive.\n\nFolosește butonul ${PARTY} din dreapta-sus pentru a\nvorbi și interacționa cu grupul.\n(dacă folosești un controller, folosește ${BUTTON} cât timp ești într-un meniu)", - "aboutText": "Despre", - "addressFetchErrorText": "", + "aboutDescriptionText": "Folosește aceste file pentru a asambla un grup.\n\nGrupurile te lasă să joci jocuri și turnee împreună\ncu prietenii tăi, cu ajutorul mai multor dispozitive.\n\nFolosește butonul ${PARTY} din colțul din dreapta-sus pentru a\nvorbi și interacționa cu grupul.\n(dacă folosești un controller, folosește ${BUTTON} cât timp ești într-un meniu)", + "aboutText": "Ajutor", + "addressFetchErrorText": "", "appInviteInfoText": "Invită-ți prietenii să încerce BombSquad și\nvor primii ${COUNT} bilete gratis. Tu vei primi\n${YOU_COUNT} pentru fiecare care o face.", "appInviteMessageText": "${NAME} ți-a trimis ${COUNT} de bilete în ${APP_NAME}", - "appInviteSendACodeText": "Trimite-i Un Cod", - "appInviteTitleText": "Invitație Pentru A Încerca ${APP_NAME}", + "appInviteSendACodeText": "Trimite-le Un Cod", + "appInviteTitleText": "Invitație Pentru a Încerca ${APP_NAME}", "bluetoothAndroidSupportText": "(funcționează cu orice dispozitiv Android care suportă Bluetooth)", "bluetoothDescriptionText": "Ține un joc/intră într-un joc folosind Bluetooth:", "bluetoothHostText": "Ține un joc pe Bluetooth", @@ -692,40 +696,40 @@ "copyCodeConfirmText": "Codul a fost copiat în clipboard.", "copyCodeText": "Copiază Codul", "dedicatedServerInfoText": "Pentru cele mai bune rezultate, configurează un server dedicat. Consultă bombsquadgame.com/server pentru a afla cum.", - "disconnectClientsText": "Aceasta va deconecta ${COUNT} jucător(i) din\ngrupul tău curent. Ești sigur?", - "earnTicketsForRecommendingAmountText": "Prietenii tăi vor primii ${COUNT} de bilete dacă vor încerca jocul \n(iar tu vei primi ${YOU_COUNT} pentru fiecare care încearcă)", + "disconnectClientsText": "Această opțiune va deconecta ${COUNT} jucător(i) din\njocul tău curent. Ești sigur?", + "earnTicketsForRecommendingAmountText": "Prietenii tăi vor primi ${COUNT} de bilete dacă vor încerca jocul \n(iar tu vei primi ${YOU_COUNT} pentru fiecare care încearcă)", "earnTicketsForRecommendingText": "Împărtăşeşte jocul\npentru bilete gratuite...", "emailItText": "Dă-l prin Email", "favoritesSaveText": "Salvează ca favorit", "favoritesText": "Favorite", - "freeCloudServerAvailableMinutesText": "Următorul server cloud gratuit va fi disponibil în ${MINUTES} minute.", - "freeCloudServerAvailableNowText": "Server cloud gratuit disponibil!", - "freeCloudServerNotAvailableText": "Nu sunt disponibile servere cloud gratuite.", + "freeCloudServerAvailableMinutesText": "Următorul server gratuit va fi disponibil în ${MINUTES} minute.", + "freeCloudServerAvailableNowText": "Server gratuit disponibil!", + "freeCloudServerNotAvailableText": "Nu sunt disponibile servere gratuite.", "friendHasSentPromoCodeText": "${COUNT} Bilete pe ${APP_NAME} de la ${NAME}", "friendPromoCodeAwardText": "Vei primi ${COUNT} de bilete de fiecare dată când este folosit.", - "friendPromoCodeExpireText": "Codul va expira în ${EXPIRE_HOURS} de ore şi merge doar pentru jucătorii noi.", - "friendPromoCodeInstructionsText": "Pentru a-l utiliza, deschide ${APP_NAME} și accesează „Setări-> Avansat-> Introdu codul”.\nConsultă bombsquadgame.com pentru linkuri de descărcare pentru toate platformele acceptate.", + "friendPromoCodeExpireText": "Codul va expira în ${EXPIRE_HOURS} de ore şi este valabil doar pentru jucătorii noi.", + "friendPromoCodeInstructionsText": "Pentru a-l utiliza, deschide ${APP_NAME} și accesează „Setări-> Avansat-> Introdu un cod”.\nConsultă bombsquadgame.com pentru linkuri de descărcare pentru toate platformele acceptate.", "friendPromoCodeRedeemLongText": "Poate fi introdus pentru ${COUNT} de bilete gratuite de către un maxim de ${MAX_USES} de persoane.", "friendPromoCodeRedeemShortText": "Poate fi introdus în joc pentru ${COUNT} de bilete.", - "friendPromoCodeWhereToEnterText": "(în „Setări-> Avansat-> Introdu codul”)", - "getFriendInviteCodeText": "Cere un Cod de Invitare Pentru Prieteni", + "friendPromoCodeWhereToEnterText": "(în \"Setări-> Avansat-> Introdu un cod\")", + "getFriendInviteCodeText": "Cere un Cod pentru Prieteni", "googlePlayDescriptionText": "Invită jucători Google Play la petrecerea ta:", "googlePlayInviteText": "Invită", "googlePlayReInviteText": "Sunt ${COUNT} jucători Google Play în grupul tău care vor\nfi deconectați dacă faci o altă invitație. Include-i și\npe ei în noua invitație pentru a-i avea înapoi la petrecere.", "googlePlaySeeInvitesText": "Vezi Invitații", "googlePlayText": "Google Play", - "googlePlayVersionOnlyText": "(Numai pentru versiunea Android / Google Play)", - "hostPublicPartyDescriptionText": "Creează Un Joc Public", + "googlePlayVersionOnlyText": "(Numai pentru Google Play / Android)", + "hostPublicPartyDescriptionText": "Creează Un Server Public", "hostingUnavailableText": "Creeare Indisponibilă", - "inDevelopmentWarningText": "Notă:\n\nJocul peste rețea este încă o opțiune în dezvoltare.\nDeocamdată se recomandă ca toți jucătorii să fie pe\naceeași rețea Wi-Fi.", + "inDevelopmentWarningText": "Notă:\n\nJocul peste rețea este încă o opțiune în dezvoltare.\nDeocamdată este recomandat ca toți jucătorii să fie pe\naceeași rețea Wi-Fi.", "internetText": "Internet", "inviteAFriendText": "Prietenii tăi nu au jocul? Invită-i să-l\nîncerce şi vor primii ${COUNT} de bilete gratis.", "inviteFriendsText": "Invită-ți Prietenii", - "joinPublicPartyDescriptionText": "Alăturați-vă unui joc public", - "localNetworkDescriptionText": "Alătură-te unui joc din apropiere (LAN, Bluetooth etc.)", + "joinPublicPartyDescriptionText": "Alătură-te Unui Server Public", + "localNetworkDescriptionText": "Alătură-te unui grup din apropiere (LAN, Bluetooth etc.)", "localNetworkText": "Rețea Locală", - "makePartyPrivateText": "Fă-mi Jocul Privat", - "makePartyPublicText": "Fă-mi Jocul Public", + "makePartyPrivateText": "Închide-mi Server-ul", + "makePartyPublicText": "Fă-mi Server-ul Public", "manualAddressText": "Adresa", "manualConnectText": "Conectează-te", "manualDescriptionText": "Intră într-un joc cu adresa:", @@ -733,30 +737,30 @@ "manualJoinableFromInternetText": "Se pot conecta alții de pe internet la jocul tău?:", "manualJoinableNoWithAsteriskText": "NU*", "manualJoinableYesText": "DA", - "manualRouterForwardingText": "*pentru a repara asta, configurează-ți router-ul ca acesta să dea forward la portul UDP ${PORT} către adresa ta locală", + "manualRouterForwardingText": "*pentru a repara asta, configurează-ți router-ul încât acesta să dea forward la portul UDP ${PORT} către adresa ta locală", "manualText": "Manual", "manualYourAddressFromInternetText": "Adresa ta de pe internet:", "manualYourLocalAddressText": "Adresa ta locală:", "nearbyText": "Din apropiere", "noConnectionText": "", "otherVersionsText": "(alte versiuni)", - "partyCodeText": "Codul Jocului", + "partyCodeText": "Codul Server-ului", "partyInviteAcceptText": "Acceptă", "partyInviteDeclineText": "Refuză", "partyInviteGooglePlayExtraText": "(vezi tab-ul 'Google Play' din fereastra 'Adunare')", "partyInviteIgnoreText": "Ignoră", "partyInviteText": "${NAME} te-a invitat\nsă intri în grupul lui/ei!", "partyNameText": "Numele Server-ului", - "partyServerRunningText": "Server-ul tău de joc este activ.", - "partySizeText": "nr. de locuri", + "partyServerRunningText": "Server-ul tău privat este activ.", + "partySizeText": "Jucători", "partyStatusCheckingText": "Se verifică starea...", "partyStatusJoinableText": "Server-ul tău este acum accesibil de pe internet", "partyStatusNoConnectionText": "nu se poate conecta la server", "partyStatusNotJoinableText": "nimeni de pe internet nu poate intra pe server-ul tău", "partyStatusNotPublicText": "server-ul tău nu este public", - "pingText": "latența", + "pingText": "Ping", "portText": "Port", - "privatePartyCloudDescriptionText": "Jocurile private rulează pe servere cloud dedicate; nu este necesară configurarea router-ului.", + "privatePartyCloudDescriptionText": "Serverele private rulează pe servere cloud dedicate; nu este necesară configurarea router-ului.", "privatePartyHostText": "Creează un Server Privat", "privatePartyJoinText": "Alătură-te unui Server Privat", "privateText": "Privat", @@ -764,15 +768,15 @@ "publicText": "Public", "requestingAPromoCodeText": "Se obține codul...", "sendDirectInvitesText": "Trimite Invitații Directe", - "shareThisCodeWithFriendsText": "Împărtăşeşte codul ăsta cu prietenii:", - "showMyAddressText": "Afișează Adresa Mea", + "shareThisCodeWithFriendsText": "Trimite-le prietenilor tăi acest cod:", + "showMyAddressText": "Afișează-mi Adresa", "startHostingPaidText": "Creează Acum Pentru ${COST}", - "startHostingText": "Creează", - "startStopHostingMinutesText": "Poți creea și opri servere gratis pentru următoarele ${MINUTES} minute.", - "stopHostingText": "Oprește Server-ul", + "startHostingText": "Creează Server", + "startStopHostingMinutesText": "Poți crea și închide servere gratis pentru următoarele ${MINUTES} minute.", + "stopHostingText": "Închide Server-ul", "titleText": "Adunare", "wifiDirectDescriptionBottomText": "Dacă toate dispozitivele au un panou 'Wi-Fi Direct', ar trebui să poată să îl folosească să\nse găsească și să se conecteze unii la alții. Când toate dispozitivele sunt conectate se pot\nforma grupuri, aici, folosind tab-ul 'Rețea locală', ca și când ați fi pe aceiași rețea Wi-Fi.\n\nPentru cele mai bune rezultate, host-ul Wi-Fi Direct ar trebui să fie și host-ul server-ului ${APP_NAME}.", - "wifiDirectDescriptionTopText": "Funcția de Wi-Fi direct se poate folosi la conectarea dispozitivelor Android fără\na folosi o rețea Wi-Fi. Aceasta funcționează cel mai bine de la Android 4.2 în sus.\n\nPentru a folosi funcția de Wi-Fi direct, deschide setările Wi-Fi și caută 'Wi-Fi Direct' în meniu.", + "wifiDirectDescriptionTopText": "Funcția de Wi-Fi direct se poate folosi la conectarea dispozitivelor Android fără\na folosi o rețea Wi-Fi. Aceasta funcționează cel mai bine de la Android 4.2 în sus.\n\nPentru a folosi funcția de Wi-Fi direct, deschide setările Wi-Fi-ului și caută 'Wi-Fi Direct' în meniu.", "wifiDirectOpenWiFiSettingsText": "Deschide setările Wi-Fi", "wifiDirectText": "Wi-Fi Direct", "worksBetweenAllPlatformsText": "(funcționează pe toate platformele)", @@ -781,11 +785,11 @@ }, "getTicketsWindow": { "freeText": "GRATIS!", - "freeTicketsText": "Bilete Gratis", + "freeTicketsText": "Bilete Gratuite", "inProgressText": "O tranzacție e în progres; reîncearcă în câteva secunde.", - "purchasesRestoredText": "Cumpărături Resetate.", + "purchasesRestoredText": "Cumpărături Restaurate.", "receivedTicketsText": "Ai primit ${COUNT} (de) bilete!", - "restorePurchasesText": "Resetează Cumpărături", + "restorePurchasesText": "Restaurează Cumpărăturile", "ticketDoublerText": "Dublator Bilete", "ticketPack1Text": "Pachet de Bilete Mic", "ticketPack2Text": "Pachet de Bilete Mediu", @@ -793,10 +797,10 @@ "ticketPack4Text": "Super-Pachet de Bilete", "ticketPack5Text": "Pachet de Bilete MAMUT", "ticketPack6Text": "Pachet de Bilete Suprem", - "ticketsFromASponsorText": "Ia ${COUNT} bilete\nde la un sponsor", + "ticketsFromASponsorText": "Vizionează o reclamă\npentru ${COUNT} bilete", "ticketsText": "${COUNT} de Bilete", "titleText": "Ia Bilete", - "unavailableLinkAccountText": "Ne pare rău, dar achizițiile nu sunt disponibile pe această platformă.\nCa soluție, puteți conecta acest cont la un cont de pe\no altă platformă și faceți cumpărături acolo.", + "unavailableLinkAccountText": "Scuze, dar achizițiile nu sunt disponibile pe această platformă.\nCa soluție, poți conecta acest cont la un alt cont de pe\no altă platformă și să faci cumpărături acolo.", "unavailableTemporarilyText": "Acest serviciu este indisponibil deocamdată; încearcă din nou mai târziu.", "unavailableText": "Scuze, această ofertă este indisponibilă.", "versionTooOldText": "Scuze, dar versiunea jocului e prea veche; actualizează-l la o versiune mai nouă.", @@ -804,7 +808,8 @@ "youHaveText": "ai ${COUNT} (de) bilete" }, "googleMultiplayerDiscontinuedText": "Ne pare rău, serviciul multiplayer de pe Google nu mai este disponibil.\nLucrez la un înlocuitor cât mai repede posibil.\nPână atunci, te rog să încerci o altă metodă de conectare.\n-Eric", - "googlePlayText": "Magazin Play", + "googlePlayPurchasesNotAvailableText": "Achizițiile de pe Google Play nu sunt disponibile.\nÎncearcă să actualizezi Magazin Play și să încerci din nou.", + "googlePlayText": "Google Play", "graphicsSettingsWindow": { "alwaysText": "Întotdeauna", "fullScreenCmdText": "Fullscreen (Cmd+F)", @@ -819,25 +824,26 @@ "showFPSText": "Arată FPS-urile", "texturesText": "Texturi", "titleText": "Grafici", - "tvBorderText": "Margine Televizor", + "tvBorderText": "Margine Pentru Televizor", "verticalSyncText": "V-sync", "visualsText": "Vizuale" }, "helpWindow": { - "bombInfoText": "- Bomba -\nMai puternică decât pumnii, dar poate\nrezulta în a te lovi pe tine însuți.\nPentru rezultate pozitive, arunc-o înspre\ninamici înainte să se termine fitilul.", + "bombInfoText": "- Bomba -\nMai puternică decât pumnii, dar poate\nrezulta în a te răni pe tine însuți.\nPentru rezultate pozitive, arunc-o înspre\ninamici înainte să i se termine fitilul.", "canHelpText": "${APP_NAME} te poate ajuta.", - "controllersInfoText": "Te poți juca ${APP_NAME} cu prietenii dintr-o rețea sau \nputeți juca cu toții pe același dispozitiv dacă aveți suficiente controllere.\n${APP_NAME} acceptă o varietate de ele; puteți folosi chiar și telefoane\ndrept controllere cu ajutorul aplicației gratuite „${REMOTE_APP_NAME}”.\nConsultă Setări-> Controlere pentru mai multe informații.", - "controllersInfoTextRemoteOnly": "Te poți juca ${APP_NAME} cu prietenii pe o rețea de Wi-Fi/Bluetooth, sau\nvă puteți juca cu toții pe același dispozitiv folosind telefoane \ndrept controllere cu ajutorul aplicației gratuite '${REMOTE_APP_NAME}'.", + "controllersInfoText": "Te poți juca ${APP_NAME} cu prietenii cu ajutorul unei rețele de internet sau vă\nputeți juca cu toții pe același dispozitiv dacă aveți suficiente controllere.\n${APP_NAME} suportă o varietate mare de controllere; poți folosi chiar și telefoane\ndrept controllere cu ajutorul aplicației gratuite „${REMOTE_APP_NAME}”.\nConsultă Setări-> Controlere pentru mai multe informații.", + "controllersInfoTextRemoteOnly": "Te poți juca ${APP_NAME} cu prietenii tăi pe o rețea de Wi-Fi/Bluetooth, sau\nvă puteți juca cu toții pe același dispozitiv folosindu-vă telefoanele\ndrept controllere cu ajutorul aplicației gratuite '${REMOTE_APP_NAME}'.", "controllersText": "Controllere", "controlsSubtitleText": "Caracterul tău prietenos din ${APP_NAME} are câteva acțiuni de bază:", "controlsText": "Controalele", - "devicesInfoText": "Te poți juca pe internet pe versiunea VR de ${APP_NAME}\nla fel ca pe cea originală așa că scoateți-vă telefoanele,tabletele,\nporniți computerele și începeți jocul. Poate fi folositor\nși să conectezi versiunea originală la cea VR doar ca cei\ndin afară să poată vdedea acțiunea.", + "devicesInfoText": "Te poți juca pe internet pe versiunea VR de ${APP_NAME}\nla fel ca pe cea originală așa că scoateți-vă telefoanele, tabletele,\nporniți computerele și începeți jocul. Poate fi folositor\nși să conectezi versiunea originală la cea VR doar ca cei\ndin afară să poată vedea acțiunea.", "devicesText": "Dispozitive", "friendsGoodText": "E bine să ai și din aceștia. ${APP_NAME} e și mai amuzant cu mai\nmulți jucători și suportă până la 8 deodată, ceea ce ne duce la:", "friendsText": "Prieteni", - "jumpInfoText": "- Săritura -\nSari ca să trecipeste gropi mici,\nsă arunci lucruri mai sus,\nși să-ți exprimi fericirea.", + "jumpInfoText": "- Săritura -\nSari ca să treci peste gropi mici,\nsă arunci lucruri mai sus,\nși să-ți exprimi fericirea.", "orPunchingSomethingText": "Sau să dai cu pumnii în ceva, să arunci acel ceva într-o prăpastie și să explodeze până jos de la o bombă lipicioasă.", - "pickUpInfoText": "- Ridicarea -\nIa steaguri, inamici, sau orice \nlucru care nu e fixat de pământ.\nApasă-l încă odată pentru a arunca.", + "pickUpInfoText": "- Ridicarea -\nRidică steaguri, inamici, sau orice \nlucru care nu e fixat de pământ.\nApasă-l încă odată pentru a arunca lucrul respectiv.", + "pickUpInfoTextScale": 0.5, "powerupBombDescriptionText": "Te lasă să arunci 3 bombe\nîn loc de una singură.", "powerupBombNameText": "Bombe Triple", "powerupCurseDescriptionText": "Ar fi mai bine să eviți astea.\n ...sau oare?", @@ -846,11 +852,11 @@ "powerupHealthNameText": "Trusă de Prim-Ajutor", "powerupIceBombsDescriptionText": "Mai slabe decât bombele\nnormale, dar îți lasă inamicii\nînghețați și aparent fragili.", "powerupIceBombsNameText": "Bombe de Gheață", - "powerupImpactBombsDescriptionText": "Mai slabe decât bombele normale,\ndar explodează odată ce ating ceva.", + "powerupImpactBombsDescriptionText": "Mai slabe decât bombele normale,\ndar explodează imediat ce ating ceva.", "powerupImpactBombsNameText": "Bombe cu Impact", "powerupLandMinesDescriptionText": "Acestea vin câte 3; Folositoare\npentru apărarea bazelor sau\npentru oprirea inamicilor vitezomani.", "powerupLandMinesNameText": "Mine", - "powerupPunchDescriptionText": "Îți va face pumnii mai tari,\nmai rapizi, mai buni și mai puternici.", + "powerupPunchDescriptionText": "Îți va face pumnii mai tari,\nmai rapizi și mai buni.", "powerupPunchNameText": "Mănuși de Box", "powerupShieldDescriptionText": "Absoarbe daunele ca să\nnu o faci tu.", "powerupShieldNameText": "Scut-Energic", @@ -858,7 +864,7 @@ "powerupStickyBombsNameText": "Bombe Lipicioase", "powerupsSubtitleText": "Desigur, nici un joc nu e complet fără powerup-uri:", "powerupsText": "Powerup-uri", - "punchInfoText": "- Pumnii -\nPumnii dăunează mai mult cu cât\nse mișcă mai rapid, deci aleargă\nși rotește-te ca un dement!", + "punchInfoText": "- Pumnii -\nPumnii dăunează mai mult cu cât\nîi miști mai rapid, deci aleargă\nși rotește-te ca un dement!", "runInfoText": "- Fuga -\nȚine apăsat ORICE buton pentru a fugi. Triggerele sau butoanele de umăr funcționează bine dacă le ai.\nFugitul te ajută să ajungi mai repede în alte locuri, deși virezi greu, deci ai grijă la prăpastii.", "someDaysText": "În unele zile pur și simplu vrei să lovești ceva. Sau să explodezi altceva.", "titleText": "Ajutor pentru ${APP_NAME}", @@ -877,12 +883,12 @@ "internal": { "arrowsToExitListText": "Apasă ${LEFT} sau ${RIGHT} pentru a ieși din listă", "buttonText": "buton", - "cantKickHostError": "Nu poți da afară creeatorul server-ului.", + "cantKickHostError": "Nu poți da afară hostul server-ului.", "chatBlockedText": "${NAME} este blocat în chat timp de ${TIME} (de) secunde.", - "connectedToGameText": "a intrat în „${NAME}”", + "connectedToGameText": "Ai intrat în '${NAME}'", "connectedToPartyText": "Ai intrat în server-ul lui ${NAME}!", "connectingToPartyText": "Se conectează...", - "connectionFailedHostAlreadyInPartyText": "Conexiunea a eşuat; hostul este în alt joc.", + "connectionFailedHostAlreadyInPartyText": "Conexiunea a eşuat; hostul este în alt joc.", "connectionFailedPartyFullText": "Conexiune eșuată; server-ul este plin.", "connectionFailedText": "Conexiunea a eşuat.", "connectionFailedVersionMismatchText": "Conexiunea a eşuat; hostul rulează o versiune diferită a jocului.\nAsigurați-vă că ambii aveți cea mai nouă versiune a jocului şi încercați din nou.", @@ -897,7 +903,7 @@ "controllersDetectedText": "${COUNT} controllere detectate.", "controllersDisconnectedText": "${COUNT} controllere s-au deconectat.", "corruptFileText": "Fişiere Corupte detectate. Reinstalează jocul, sau trimite un email la adresa ${EMAIL}", - "errorPlayingMusicText": "Eroare la pornirea muzicii: ${MUSIC}", + "errorPlayingMusicText": "Nu s-a putut porni muzica: ${MUSIC}", "errorResettingAchievementsText": "Nu se pot reseta medaliile; încearcă din nou mai tărziu.", "hasMenuControlText": "${NAME} are controlul meniului.", "incompatibleNewerVersionHostText": "Hostul rulează o versiune mai nouă a jocului.\nActualizează-ți jocul la cea mai recentă versiune și încearcă din nou.", @@ -908,30 +914,30 @@ "invalidPortErrorText": "Eroare: port invalid.", "invitationSentText": "Invitație Trimisă.", "invitationsSentText": "${COUNT} (de) invitații trimise.", - "joinedPartyInstructionsText": "Cineva s-a alăturat partidului tău.\nAccesează meniul „Joacă” pentru a începe un joc.", + "joinedPartyInstructionsText": "Cineva s-a alăturat server-ului tău.\nAccesează meniul „Joacă” pentru a începe un joc.", "keyboardText": "Tastatură", - "kickIdlePlayersKickedText": "${NAME} va fi dat afară pentru că este inactiv.", - "kickIdlePlayersWarning1Text": "${NAME} va fi dat afară în ${COUNT} (de) secunde dacă continuă să fie inactiv.", - "kickIdlePlayersWarning2Text": "(poți opri asta în Setări->Avansat)", - "leftGameText": "A ieșit din ${NAME}", + "kickIdlePlayersKickedText": "${NAME} a fost dat afară deoarece era inactiv.", + "kickIdlePlayersWarning1Text": "${NAME} va fi dat afară în următoarele ${COUNT} secunde dacă continuă să fie inactiv.", + "kickIdlePlayersWarning2Text": "(Poți dezactiva această setare în Setări -> Avansat)", + "leftGameText": "Ai ieșit din '${NAME}'", "leftPartyText": "Ai ieşit din server-ul lui ${NAME}.", "noMusicFilesInFolderText": "Folder-ul nu conține niciun fişier de muzică.", "playerJoinedPartyText": "${NAME} a intrat în joc!", "playerLeftPartyText": "${NAME} a ieșit.", "rejectingInviteAlreadyInPartyText": "Se respinge invitația (eşti deja într-un alt server).", - "serverRestartingText": "Server-ul se restartează. Te rog să intri din nou într-o clipă...", + "serverRestartingText": "Server-ul se restartează. Te rog să reintri într-o clipă...", "serverShuttingDownText": "Server-ul se închide...", "signInErrorText": "Eroare la Conectare.", "signInNoConnectionText": "Nu se poate conecta. (nu există o conexiune la internet?)", "telnetAccessDeniedText": "Eroare: utilizatorul nu are acces la telnet.", "timeOutText": "(i se va pierde controlul meniului în ${TIME} (de) secunde)", - "touchScreenJoinWarningText": "Ai intrat cu touchscreen-ul.\nDacă ai făcut-o din greşeală, apasă 'Meniu->Sfârşeşte Joc' cu acesta.", + "touchScreenJoinWarningText": "Ai intrat cu touchscreen-ul.\nDacă ai făcut-o din greşeală, apasă 'Meniu -> Devino Spectator' cu acesta.", "touchScreenText": "TouchScreen", - "unableToResolveHostText": "Eroare:imposibil de găsit hostul.", + "unableToResolveHostText": "Eroare: imposibil de găsit hostul.", "unavailableNoConnectionText": "Acest serviciu nu este disponibil acum (fără conexiune la internet?)", "vrOrientationResetCardboardText": "Foloseşte această opțiune pentru resetare orientării VR.\nDacă vrei să te joci vei avea nevoie de un controller external.", "vrOrientationResetText": "Orientare VR resetată.", - "willTimeOutText": "(se va reseta dacă este inactiv)" + "willTimeOutText": "(i se va lua controlul dacă este inactiv)" }, "jumpBoldText": "SARI", "jumpText": "Sari", @@ -941,15 +947,15 @@ "keyboardNoOthersAvailableText": "Nu a fost detectată altă tastatură.", "keyboardSwitchText": "Tastatura schimbată va fi \"${NAME}\".", "kickOccurredText": "${NAME} a fost dat afară.", - "kickQuestionText": "Vrei să-l dai afară pe ${NAME}?", + "kickQuestionText": "Vrei să dai afară pe ${NAME}?", "kickText": "Dă-l afară", "kickVoteCantKickAdminsText": "Administratorii nu pot fi dați afară.", - "kickVoteCantKickSelfText": "Nu te poți da afară.", + "kickVoteCantKickSelfText": "De ce ai vrea să te dai afară?", "kickVoteFailedNotEnoughVotersText": "Nu sunt destui jucători pentru un vot.", "kickVoteFailedText": "Votul a eșuat.", - "kickVoteStartedText": "un vot de eliminare pentru ${NAME} a fost cerut.", - "kickVoteText": "Votează-l afară", - "kickVotingDisabledText": "Votul de eliminare este dezactivat.", + "kickVoteStartedText": "Un vot de a da afară pe ${NAME} a fost cerut.", + "kickVoteText": "Votează pentru a-l da afară", + "kickVotingDisabledText": "Datul afară este dezactivat.", "kickWithChatText": "Scrie ${YES} în chat pentru a-l vota sau scrie ${NO} pentru a nu-l vota.", "killsTallyText": "${COUNT} (de) ucideri", "killsText": "Ucideri", @@ -1000,15 +1006,15 @@ "macControllerSubsystemTitleText": "Suport Pentru Controller", "mainMenu": { "creditsText": "Credite", - "demoMenuText": "Meniu Demo", + "demoMenuText": "Meniu Demonstrativ", "endGameText": "Sfârșește Jocul", "exitGameText": "Ieși Din Joc", "exitToMenuText": "Revii la meniu?", - "howToPlayText": "Cum Se Joacă?", + "howToPlayText": "Cum se Joacă?", "justPlayerText": "(Doar ${NAME})", - "leaveGameText": "Părăsește Jocul", + "leaveGameText": "Devino Spectator", "leavePartyConfirmText": "Sigur vrei să părăsești grupul?", - "leavePartyText": "Părăsește Jocul", + "leavePartyText": "Părăsește Grupul", "quitText": "Ieși", "resumeText": "Continuă", "settingsText": "Setări" @@ -1036,12 +1042,12 @@ "nameKilledText": "${NAME} a omorât pe ${VICTIM}", "nameNotEmptyText": "Trebubie să-ți pui un nume!", "nameScoresText": "${NAME} înscrie!", - "nameSuicideKidFriendlyText": "${NAME} a pierit accidental.", + "nameSuicideKidFriendlyText": "${NAME} a dat colțul.", "nameSuicideText": "${NAME} s-a sinucis.", "nameText": "Numele", "nativeText": "Nativă", "newPersonalBestText": "Un nou record personal a fost atins!", - "newTestBuildAvailableText": "O nouă versiune Beta a fost lansată: (${VERSION} build ${BUILD}).\nIa-o de pe ${ADDRESS}", + "newTestBuildAvailableText": "O nouă versiune de test a fost lansată: (${VERSION} build ${BUILD}).\nIa-o de pe ${ADDRESS}", "newText": "Nou", "newVersionAvailableText": "O versiune nouă de ${APP_NAME} este disponibilă,aceasta fiind (${VERSION})!", "nextAchievementsText": "Realizările Următoare:", @@ -1053,7 +1059,7 @@ "noProfilesErrorText": "Nu ai niciun Profil de Jucător, deci eşti blocat cu '${NAME}'.\nDute la Setări->Profile de Jucător pentru a-ți face un profil.", "noScoresYetText": "Niciun scor deocamdată.", "noThanksText": "Nu Mulțumesc", - "noTournamentsInTestBuildText": "ATENȚIE: Scorurile obținute în turnee vor fi ignorate în această versiune beta.", + "noTournamentsInTestBuildText": "ATENȚIE: Scorurile obținute în turnee vor fi ignorate în această versiune de test.", "noValidMapsErrorText": "Nu sunt hărți disponibile pentru acest mod de joc.", "notEnoughPlayersRemainingText": "Nu mai sunt destui jucători; ieși din joc și creează altul nou.", "notEnoughPlayersText": "Ai nevoie de cel puțin ${COUNT} jucători pentru a începe acest joc!", @@ -1078,7 +1084,7 @@ "emptyText": "Petrecerea ta este goală", "hostText": "(hostul)", "sendText": "Trimite", - "titleText": "Petrecerea Ta" + "titleText": "Server-ul Tău" }, "pausedByHostText": "(pus pe pauză de către host)", "perfectWaveText": "Val Perfect!", @@ -1115,9 +1121,12 @@ "playlistNotFoundText": "lista nu a fost găsită", "playlistText": "Lista de jocuri", "playlistsText": "Liste de Jocuri", - "pleaseRateText": "Dacă îți place ${APP_NAME}, te rog să stai\no clipă și să evaluezi jocul scriind o recenzie pentru acesta.\nAceasta furnizează feedback folositor și ajută la dezvoltarea jocului în viitor.\n\nmulțumesc!\n-Eric", + "pleaseRateText": "Dacă îți place ${APP_NAME}, te rog să stai\no clipă și să evaluezi jocul, scriind o recenzie pentru acesta.\nAceasta furnizează feedback folositor și ajută la dezvoltarea jocului în viitor.\n\nmulțumesc!\n-Eric", "pleaseWaitText": "Te rog să aștepți...", - "pluginsDetectedText": "Plugin(uri) noi detectate. Activează-le/configurează-le în setări.", + "pluginClassLoadErrorText": "Nu s-a putut încărca plugin-ul '${PLUGIN}': ${ERROR}", + "pluginInitErrorText": "Nu s-a putut iniția plugin-ul '${PLUGIN}': ${ERROR}", + "pluginsDetectedText": "Plugin(uri) noi detectate. Restartează jocul pentru a le activa / configura în setări.", + "pluginsRemovedText": "${NUM} plugin(uri) nu mai există.", "pluginsText": "Plugin-uri", "practiceText": "Antrenament", "pressAnyButtonPlayAgainText": "Apasă orice buton pentru a juca din nou...", @@ -1126,15 +1135,15 @@ "pressAnyKeyButtonPlayAgainText": "Apasă orice buton/tastă pentru a juca din nou...", "pressAnyKeyButtonText": "Apasă orice buton/tastă pentru a continua...", "pressAnyKeyText": "Apasă orice tastă...", - "pressJumpToFlyText": "** Apasă butonul de sărit repetat pentru a zbura **", + "pressJumpToFlyText": "** Apasă butonul de sărit de mai multe ori pentru a zbura **", "pressPunchToJoinText": "apasă PUMN pentru a intra...", "pressToOverrideCharacterText": "apasă ${BUTTONS} pentru a-ți schimba caracterul", - "pressToSelectProfileText": "apasă ${BUTTONS} pentru a-ți selecta caracterul", + "pressToSelectProfileText": "apasă ${BUTTONS} pentru a-ți selecta profilul", "pressToSelectTeamText": "apasă ${BUTTONS} pentru a-ți selecta echipa", "promoCodeWindow": { - "codeText": "Introdu Codul", + "codeText": "Codul", "codeTextDescription": "Cod Promoțional", - "enterText": "Trimite Codul" + "enterText": "Trimite-l" }, "promoSubmitErrorText": "Eroare la trimiterea codului; verifică-ți conexiunea la internet", "ps3ControllersWindow": { @@ -1168,7 +1177,7 @@ "cant_resolve_host": "Nu se poate găsi hostul.", "capturing": "Atribuie un buton...", "connected": "Conectat.", - "description": "Folosește-ți telefonul sau tableta drept controller pentru BombSquad\nPoți conecta 8 dispozitive simultan pentru un mod multiplayer nebunesc pe un Televizor sau pe o tabletă.", + "description": "Folosește-ți telefonul sau tableta drept controller pentru BombSquad.\nPoți conecta 8 dispozitive simultan pentru un mod de multiplayer nebunesc pe un Televizor sau pe o tabletă.", "disconnected": "Deconectat de către server.", "dpad_fixed": "fixat", "dpad_floating": "liber", @@ -1200,7 +1209,7 @@ "replayWriteErrorText": "Eroare la scrierea fişierului de reluare.", "replaysText": "Reluări", "reportPlayerExplanationText": "Foloseşte această adresă de E-Mail pentru a raporta trişori, limbaj vulgar, sau alte tipuri de comportamente inadecvate.\nTe rog descrie subiectul mai jos:", - "reportThisPlayerCheatingText": "Trișori", + "reportThisPlayerCheatingText": "Trișat", "reportThisPlayerLanguageText": "Limbaj Vulgar", "reportThisPlayerReasonText": "Ce ai vrea să reclami?", "reportThisPlayerText": "Raportează Acest Jucător", @@ -1236,22 +1245,22 @@ "titleText": "Setări" }, "settingsWindowAdvanced": { - "alwaysUseInternalKeyboardDescriptionText": "(o tastatură simplă, în special pentru controllere, ce ajută la editarea textului)", + "alwaysUseInternalKeyboardDescriptionText": "(o tastatură simplă, folositoare pentru controllere, care ajută la editarea textului)", "alwaysUseInternalKeyboardText": "Foloseşte Mereu Tastatura Internală", - "benchmarksText": "Teste-Stres şi referințe", - "disableCameraGyroscopeMotionText": "Oprește Camera De Tip Giroscop", - "disableCameraShakeText": "Oprește Zguduirea Camerei", - "disableThisNotice": "(poți opri această notificare în setările avansate)", + "benchmarksText": "Teste-Stres & Referințe", + "disableCameraGyroscopeMotionText": "Oprește Mișcarea Camerei De Tip Giroscop", + "disableCameraShakeText": "Dezactivează Cutremurarea Camerei", + "disableThisNotice": "(Poți dezactiva această notificare în setările avansate)", "enablePackageModsDescriptionText": "Permite mai multe capabilități pentru modare, dar dezactivează net-play-ul)", "enablePackageModsText": "Activează Moduri cu Pachete Locale", - "enterPromoCodeText": "Introdu Codul", + "enterPromoCodeText": "Introdu un cod", "forTestingText": "Notă: aceste valori sunt doar pentru teste şi vor fi resetate când jocul va fi închis.", - "helpTranslateText": "Translațiile din ${APP_NAME} sunt un efort depus de comunitate.\nDacă ai dori să te implici la translatarea unei limbi,\nurmează linkul de mai jos. Multe mulțumiri!", + "helpTranslateText": "Translațiile din ${APP_NAME} sunt un efort depus de comunitate.\nDacă ai dori să te implici la translatarea/corectarea unei limbi,\nurmează linkul de mai jos. Mulțumiri anticipate!", "kickIdlePlayersText": "Dă Afară Jucătorii Inactivi", "kidFriendlyModeText": "Modul Pentru Copii (mai puțină violență, etc)", "languageText": "Limbă", "moddingGuideText": "Ghid pentru Modare", - "mustRestartText": "Va trebui să restartezi jocul dacă vrei ca acest lucru să aibă efect.", + "mustRestartText": "Va trebui să restartezi jocul dacă vrei ca acest lucru să își facă efectul.", "netTestingText": "Testarea Internetului", "resetText": "Resetează", "showBombTrajectoriesText": "Arată Traiectoriile Bombelor", @@ -1278,7 +1287,7 @@ "CharSelect": "Selecția Caracterului", "Chosen One": "Alesul", "Epic": "Jocurile în Slow Motion", - "Epic Race": "Cursă Epică", + "Epic Race": "Cursă în Slow Motion", "FlagCatcher": "Capturează Steagul", "Flying": "Amintiri Fericite", "Football": "TouchDown", @@ -1291,10 +1300,10 @@ "Onslaught": "Măcel", "Race": "Cursă", "Scary": "Regele Dealului", - "Scores": "Meniul de Scoruri", + "Scores": "Ecranul Cu Scoruri", "Survival": "Eliminare", "ToTheDeath": "Meciul Morții", - "Victory": "Meniul de Scoruri Finale" + "Victory": "Ecranul cu Scorurile Finale" }, "spaceKeyText": "spacebar", "statsText": "Statistici", @@ -1334,16 +1343,16 @@ "totalWorthText": "*** ofertă cu valoare de ${TOTAL_WORTH}! ***", "upgradeQuestionText": "Vrei să-ți îmbunătățești versiunea?", "winterSpecialText": "Speciale pe timp de Iarnă", - "youOwnThisText": "-deja ai cumpărat asta-" + "youOwnThisText": "- deja ai cumpărat asta -" }, - "storeDescriptionText": "Nebunie pentru 8 Jucători!\n\nExplodează-ți prietenii (sau computerul) într-un concurs de mini-jocuri explozive cum ar fi Capturează Steagul, Hockey-Bombist, şi Meci-de-Moarte-În-Slow-Motion-Epic!\n\nControllurile simple şi suportul pentru diferite tipuri de controller fac posibilă joaca cu un maxim de 8 jucători; poți folosi chiar şi telefonul drept controller cu ajutorul aplicației 'BombSquad Remote'!\n\nScoate Bombele Afară!\n\nVezi www.froemling.net/bombsquad pentru mai multe informații.", + "storeDescriptionText": "Nebunie pentru 8 Jucători!\n\nExplodează-ți prietenii (sau computerul) într-un concurs de mini-jocuri explozive cum ar fi Capturează Steagul, Hockey-Bombist, şi Meci-de-Moarte-În-Slow-Motion-Epic!\n\nControalele simple şi suportul pentru diferite tipuri de controller fac posibilă joaca cu un maxim de 8 jucători; îți poți folosi chiar şi telefonul drept controller cu ajutorul aplicației 'BombSquad Remote'!\n\nScoate Bombele La Iveală!\n\nVezi www.froemling.net/bombsquad pentru mai multe informații.", "storeDescriptions": { "blowUpYourFriendsText": "Explodează-ți prietenii.", "competeInMiniGamesText": "Întrece-te în mini-jocuri începând de la curse până la zburat.", "customize2Text": "Particularizează caractere, mini-jocuri, chiar și muzica jocului.", "customizeText": "Particularizează-ți caracterele şi creează-ți listele de jocuri cu mini-jocuri cum vrei tu.", "sportsMoreFunText": "Sporturile sunt mai distractive cu explozibile.", - "teamUpAgainstComputerText": "Fă echipă contra computerului." + "teamUpAgainstComputerText": "Fă echipă împotriva computerului." }, "storeText": "Magazin", "submitText": "Trimite", @@ -1369,13 +1378,13 @@ "titleVRText": "BombSquad VR", "topFriendsText": "Prieteni de Top", "tournamentCheckingStateText": "Se verifică starea campionatului; aşteaptă...", - "tournamentEndedText": "Acest campionat s-a terminat. Altul nou va începe în curând.", - "tournamentEntryText": "Preț Pentru Campionat", - "tournamentResultsRecentText": "Rezultatele Recente ale Campionatului", - "tournamentStandingsText": "Clasamentele Campionatului", - "tournamentText": "Campionat", - "tournamentTimeExpiredText": "Timpul Campionatului a Expirat", - "tournamentsText": "Campionate", + "tournamentEndedText": "Acest turneu s-a terminat. Altul nou va începe în curând.", + "tournamentEntryText": "Prețul Pentru Intrare", + "tournamentResultsRecentText": "Rezultatele Recente ale Turneului", + "tournamentStandingsText": "Clasamentele Turneului", + "tournamentText": "Turneu", + "tournamentTimeExpiredText": "Timpul Turneului a Expirat", + "tournamentsText": "Turnee", "translations": { "characterNames": { "Agent Johnson": "Agentul Johnson", @@ -1426,23 +1435,23 @@ "Uber Runaround": "MEGA Runaround" }, "gameDescriptions": { - "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Fii cel ales pentru o perioadă de timp pentru a câştiga.\nOmoară-l pe ales pentru a deveni tu acela.", + "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Fii cel ales pentru o perioadă de timp pentru a câştiga.\nOmoară alesul pentru a deveni tu acela.", "Bomb as many targets as you can.": "Bombează cât mai multe ținte.", "Carry the flag for ${ARG1} seconds.": "Ține steagul pentru ${ARG1} (de) secunde.", - "Carry the flag for a set length of time.": "Ține steagul pentru o perioadă de timp.", + "Carry the flag for a set length of time.": "Ține steagul pentru o perioadă anumită de timp.", "Crush ${ARG1} of your enemies.": "Ucide ${ARG1} (de) inamici.", "Defeat all enemies.": "Înfrânge toți inamicii.", "Dodge the falling bombs.": "Ai grijă la bombe.", "Final glorious epic slow motion battle to the death.": "Bătălie finală glorioasă până la moarte în slow motion epic .", - "Gather eggs!": "Colectează ouă!", - "Get the flag to the enemy end zone.": "Cară steagul către partea inamică.", + "Gather eggs!": "Adună ouăle!", + "Get the flag to the enemy end zone.": "Cară steagul către partea inamicilor.", "How fast can you defeat the ninjas?": "Cât de rapid îi poți învinge pe ninja?", "Kill a set number of enemies to win.": "Ucide un număr anumit de inamici pentru a câştiga.", "Last one standing wins.": "Ultimul rămas în picioare câştigă.", "Last remaining alive wins.": "Ultimul rămas în viață câştigă.", "Last team standing wins.": "Ultima echipă rămasă în picioare câştigă.", "Prevent enemies from reaching the exit.": "Nu lăsa inamicii să ajungă la sfârşit.", - "Reach the enemy flag to score.": "Atinge steagul inamic pentru a înscrie.", + "Reach the enemy flag to score.": "Atinge steagul inamicilor pentru a înscrie.", "Return the enemy flag to score.": "Returnează steagul inamicilor pentru a puncta.", "Run ${ARG1} laps.": "Fugi ${ARG1} ture.", "Run ${ARG1} laps. Your entire team has to finish.": "Fugi ${ARG1} ture. Toată echipa ta trebuie să termine.", @@ -1458,8 +1467,8 @@ "Secure all flags on the map to win.": "Toate steagurile trebuiesc cucerite de echipa ta pentru a câştiga.", "Secure the flag for ${ARG1} seconds.": "Ține steagul pentru ${ARG1} (de) secunde.", "Secure the flag for a set length of time.": "Ține steagul pentru o perioadă anumită de timp.", - "Steal the enemy flag ${ARG1} times.": "Fură steagul inamic de ${ARG1} ori.", - "Steal the enemy flag.": "Fură steagul inamic.", + "Steal the enemy flag ${ARG1} times.": "Fură steagul inamicilor tăi de ${ARG1} ori.", + "Steal the enemy flag.": "Fură steagul inamicilor tăi.", "There can be only one.": "Aici poate fi numai unul.", "Touch the enemy flag ${ARG1} times.": "Atinge steagul inamic de ${ARG1} ori", "Touch the enemy flag.": "Atinge steagul inamicilor.", @@ -1482,7 +1491,7 @@ }, "gameNames": { "Assault": "Asalt", - "Capture the Flag": "Capturează steagul", + "Capture the Flag": "Capturează Steagul", "Chosen One": "Alesul", "Conquest": "Cucerire", "Death Match": "Meciul Morții", @@ -1501,8 +1510,8 @@ "The Last Stand": "Ultimul Rămas" }, "inputDeviceNames": { - "Keyboard": "Tastatură", - "Keyboard P2": "Tastatură P2" + "Keyboard": "Tastatura", + "Keyboard P2": "Tastatura pentru P2" }, "languages": { "Arabic": "Arabă", @@ -1569,7 +1578,7 @@ "Zigzag": "Zig zag" }, "playlistNames": { - "Just Epic": "Doar Epice", + "Just Epic": "Doar Slow Motion", "Just Sports": "Doar Sporturi" }, "scoreNames": { @@ -1578,7 +1587,7 @@ "Score": "Scor", "Survived": "Supraviețuit", "Time": "Timp", - "Time Held": "Timp ținut" + "Time Held": "Timp Ținut" }, "serverResponses": { "A code has already been used on this account.": "Un cod a fost deja folosit pe acest cont.", @@ -1591,26 +1600,26 @@ "An error has occurred; please contact support. (${ERROR})": "A intervenit o eroare; Te rog să contactezi asistența. (${ERROR})", "An error has occurred; please contact support@froemling.net.": "A apărut o eroare; te rog să contactezi support@froemling.net.", "An error has occurred; please try again later.": "O eroare a intervenit; te rog să încerci din nou mai târziu.", - "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Eşti sigur că vrei să conectezi aceste 2 conturi între ele?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nacest lucru nu poate fi reversibil!", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Eşti sigur că vrei să conectezi aceste 2 conturi între ele?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nAcest lucru nu este reversibil!", "BombSquad Pro unlocked!": "Versiunea BombSquad Pro a fost deblocată!", "Can't link 2 accounts of this type.": "Nu se pot conecta 2 conturi de acest fel.", "Can't link 2 diamond league accounts.": "Nu se pot conecta 2 conturi cu liga de diamant.", "Can't link; would surpass maximum of ${COUNT} linked accounts.": "Nu se poate conecta; s-ar depăși numărul maxim de ${COUNT} conturi conectate.", - "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Trişare detectată; scorurile şi premiile sunt suspendate pentru ${COUNT} (de) zile.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Ai fost detectat trișând; scorurile şi premiile vor fi suspendate timp de ${COUNT} (de) zile.", "Could not establish a secure connection.": "Nu s-a putut stabili o conexiune sigură.", "Daily maximum reached.": "Limită Zilnică Atinsă.", - "Entering tournament...": "Se intră în campionat...", + "Entering tournament...": "Se intră în turneu...", "Invalid code.": "Cod Invalid.", "Invalid payment; purchase canceled.": "Plată invalidă; achiziționare anulată.", - "Invalid promo code.": "Cod Promo invalid.", - "Invalid purchase.": "Achiziționare Invalidă.", - "Invalid tournament entry; score will be ignored.": "Intrare invalidă în campionat; scorul va fi ignorat.", + "Invalid promo code.": "Cod promo invalid.", + "Invalid purchase.": "Achiziționare invalidă.", + "Invalid tournament entry; score will be ignored.": "Intrare invalidă în turneu; scorul tău va fi ignorat.", "Item unlocked!": "Lucru deblocat!", "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "CONECTARE ÎNTRE CONTURI RESPINSĂ.Contul ${ACCOUNT} conține\ndate semnificative care ar putea FI PIERDUTE PE VECI.\nPoți să conectezi conturile în ordinea opusă acesteia dacă dorești\n(și să pierzi datele ACESTUI cont în locul celuilalt)", "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Îți legi progresul contului ${ACCOUNT} cu acest cont?\nTot progresul de pe ${ACCOUNT} va fi șters.\nAcest lucru nu poate fi reversibil. Ești sigur?", - "Max number of playlists reached.": "Număr maxim de liste atins.", - "Max number of profiles reached.": "Număr maxim de profile atins.", - "Maximum friend code rewards reached.": "Limita codurilor pentru prieteni atinsă.", + "Max number of playlists reached.": "Numărul maxim de liste a fost atins.", + "Max number of profiles reached.": "Numărul maxim de profile a fost atins.", + "Maximum friend code rewards reached.": "Limita codurilor pentru prieteni a fost atinsă.", "Message is too long.": "Mesajul este prea lung.", "No servers are available. Please try again soon.": "Niciun server nu este disponibil. Încearcă din nou mai târziu.", "Profile \"${NAME}\" upgraded successfully.": "Profilul \"${NAME}\" a fost îmbunătățit cu succes.", @@ -1618,26 +1627,26 @@ "Purchase successful!": "Achiziționare reuşită!", "Received ${COUNT} tickets for signing in.\nCome back tomorrow to receive ${TOMORROW_COUNT}.": "Ai primit ${COUNT} (de) bilete pentru conectare.\nRevino mâine pentru a primi ${TOMORROW_COUNT}.", "Server functionality is no longer supported in this version of the game;\nPlease update to a newer version.": "Serverele nu mai sunt suportate în această versiune a jocului;\nTe rog să actualizezi jocul la o versiune mai nouă.", - "Sorry, there are no uses remaining on this code.": "Scuze, codul nu mai poate fi folosit.", - "Sorry, this code has already been used.": "Scuze, codul acesta a fost folosit deja.", - "Sorry, this code has expired.": "Scuze, acest cod a expirat.", - "Sorry, this code only works for new accounts.": "Scuze, acest cod merge doar pentru conturile noi.", + "Sorry, there are no uses remaining on this code.": "Scuze, dar codul nu mai poate fi folosit.", + "Sorry, this code has already been used.": "Scuze, dar codul acesta a fost deja folosit.", + "Sorry, this code has expired.": "Scuze, dar acest cod a expirat.", + "Sorry, this code only works for new accounts.": "Scuze, dar acest cod merge numai pentru jucătorii cu conturi noi.", "Still searching for nearby servers; please try again soon.": "Încă se caută servere din apropiere; te rog să încerci din nou într-un moment.", "Temporarily unavailable; please try again later.": "Nevalabil temporar; te rog să încerci din nou mai târziu.", - "The tournament ended before you finished.": "Campionatul s-a sfârşit înainte să-l termini.", + "The tournament ended before you finished.": "Turneul s-a sfârşit înainte să-l termini.", "This account cannot be unlinked for ${NUM} days.": "Acest cont nu poate fi deconectat timp de ${NUM} (de) zile.", "This code cannot be used on the account that created it.": "Acest cod nu poate fi folosit pe contul pe care l-a creat.", "This is currently unavailable; please try again later.": "Nevalabil momentan; te rog să încerci din nou mai târziu.", - "This requires version ${VERSION} or newer.": "Acest lucru are nevoie de versiunea ${VERSION} sau chiar una mai nouă.", - "Tournaments disabled due to rooted device.": "Campionatele sunt dezactivate deoarece dispozitivul este înrădăcinat (rootat).", - "Tournaments require ${VERSION} or newer": "Campionatele sunt valabile de la versiunea ${VERSION} în sus", + "This requires version ${VERSION} or newer.": "Acest lucru este valabil numai de la versiunea ${VERSION} până la cea curentă.", + "Tournaments disabled due to rooted device.": "Turneele sunt dezactivate deoarece dispozitivul este înrădăcinat (rootat).", + "Tournaments require ${VERSION} or newer": "Turneele sunt valabile de la versiunea ${VERSION} în sus", "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Deconectezi contul ${ACCOUNT} de la acest cont?\nTot progresul de pe ${ACCOUNT} va fi resetat.\n(exceptând realizările în unele cazuri)", - "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "ADVERTISMENT: Reclamații cum că ai fost acuzat de trișat/hackuit au fost legate de contul tău.\nConturile prinse trișând/hackuind vor fi restricționate. Te rog să joci cinstit.", + "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "ADVERTISMENT: Reclamații cum că ai fost acuzat de trișat/hackuit au fost legate de contul tău.\nConturile persoanelor prinse trișând/hackuind vor fi restricționate. Te rog să joci cinstit.", "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Ai dori să îți conectezi contul tău de pe dispozitiv cu acesta?\n\nContul tău de pe dispozitiv este ${ACCOUNT1}\nAcest cont este ${ACCOUNT2}\n\nAcest lucru îți va salva progresul existent.\nAtenție: acest lucru nu poate fi reversibil!", "You already own this!": "Deja ai acest lucru!", "You can join in ${COUNT} seconds.": "Poți reintra în ${COUNT} secunde.", "You don't have enough tickets for this!": "Nu ai destule bilete pentru acest lucru!", - "You don't own that.": "Nu ai cumpărat asta.", + "You don't own that.": "Nu deți asta.", "You got ${COUNT} tickets!": "Ai primit ${COUNT} (de) bilete!", "You got a ${ITEM}!": "Ai primit 1 ${ITEM}!", "You have been promoted to a new league; congratulations!": "Ai fost promovat la o ligă noua; felicitări!", @@ -1646,7 +1655,7 @@ "You must wait a few seconds before entering a new code.": "Va trebui să aştepți câteva secunde înainte să introduci un cod nou.", "You ranked #${RANK} in the last tournament. Thanks for playing!": "Ai avut rankul #${RANK} în ultimul campionat. Mulțumesc pentru participare!", "Your account was rejected. Are you signed in?": "Contul tău a fost respins. Ești conectat?", - "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Copia ta de joc a fost modificată.\nTe rog să treci la normal orice schimbare făcută şi să încerci din nou.", + "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Copia ta de joc deținută a fost modificată.\nTe rog să treci la normal orice schimbare făcută şi să încerci din nou.", "Your friend code was used by ${ACCOUNT}": "${ACCOUNT} a folosit codul tău de prieten" }, "settingNames": { @@ -1659,22 +1668,22 @@ "4 Seconds": "4 Secunde", "5 Minutes": "5 Minute", "8 Seconds": "8 Secunde", - "Allow Negative Scores": "Permite Scoruri Negative", + "Allow Negative Scores": "Permite Scorurile Negative", "Balance Total Lives": "Balansează Numărul De Vieți", "Bomb Spawning": "Spaunarea Bombelor", "Chosen One Gets Gloves": "Alesul Primește Mănuși", "Chosen One Gets Shield": "Alesul Primește Un Scut", "Chosen One Time": "Timpul Alesului", - "Enable Impact Bombs": "Permite Bombe cu Impact", - "Enable Triple Bombs": "Permite Bombe Triple", + "Enable Impact Bombs": "Folosește Bombele cu Impact", + "Enable Triple Bombs": "Permite Bombele Triple", "Entire Team Must Finish": "Toată Echipa Ta Trebuie Să Termine", "Epic Mode": "Modul Epic", - "Flag Idle Return Time": "Timp Setat Pentru Reapariția Steagurilor", - "Flag Touch Return Time": "Timp Rămas Până la Respaunarea Steagurilor", + "Flag Idle Return Time": "Timp Pentru Readucerea Steagului Neatins La Bază", + "Flag Touch Return Time": "Timp Pentru Readucerea Steagului Atins La Bază", "Hold Time": "Timp de Ținut", "Kills to Win Per Player": "Omoruri Pe Echipă/Jucător Pentru a Câștiga", "Laps": "Ture", - "Lives Per Player": "Viețile Jucătorului", + "Lives Per Player": "Vieți Pe Jucător", "Long": "Lung", "Longer": "Foarte Lung", "Mine Spawning": "Spaunarea Minelor", @@ -1686,14 +1695,14 @@ "Score to Win": "Înscrieri Pentru a Câştiga", "Short": "Scurt", "Shorter": "Foarte Scurt", - "Solo Mode": "Mod Solo", + "Solo Mode": "Modul Solo", "Target Count": "Număr de Ținte", "Time Limit": "Limită de Timp" }, "statements": { "${TEAM} is disqualified because ${PLAYER} left": "Echipa ${TEAM} este descalificată deoarece ${PLAYER} a ieșit", "Killing ${NAME} for skipping part of the track!": "${NAME} a murit deoarece a luat o scurtătură!", - "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Atenție, ${NAME}: Apăsatul turbo/spamatul butoanelor te pune la pământ." + "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Atenție, ${NAME}: Apăsatul turbo / spamatul butoanelor te pune la pământ." }, "teamNames": { "Bad Guys": "Tipii Răi", @@ -1703,80 +1712,80 @@ }, "tips": { "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "O lovitură alergat-sărit-rotit-pumn perfect sincronizată poate omorî\npe cineva dintr-o singură lovitură și-ți poate câștiga respectul prietenilor.", - "Always remember to floss.": "Nu uita să te speli pe dinți.", - "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Creează profile de jucător pentru tine şi prietenii tăi cu\nnumele şi caracterele voastre preferate în loc să le folosiți pe cele generate la nimereală.", + "Always remember to floss.": "Nu uita să te speli pe dinți!", + "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Creează profile de jucător pentru tine şi prietenii tăi cu numele şi\ncaracterele voastre preferate în loc să le folosiți pe cele generate automat.", "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Blestemele te transformă înntr-o bombă cu ceas.\nSingurul remediu este de a prinde rapid o trusă de prim-ajutor.", - "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Înafară de aparențele lor, toate caracterele au abilități identice,\nașa că alegeți unul cu care te asemeni cel mai mult.", - "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Nu te împăuna cu scutul ăla de energie; tot poți fi aruncat de pe hartă.", - "Don't run all the time. Really. You will fall off cliffs.": "Nu fugii tot timpul. Pe bune. Vei cădea de pe hărți.", - "Don't spin for too long; you'll become dizzy and fall.": "Nu te învârti prea mult timp; vei ameți și vei cădea la pământ.", + "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Nu te lăsa păcălit de aparențele caracterelor pe care le poți cumpăra din magazin,\ntoate au abilități identice, așa că alege-ți unul cu care te asemeni cel mai mult.", + "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Nu te împăuna cu scutul ăla de energie; tot poți fi aruncat de pe hărți.", + "Don't run all the time. Really. You will fall off cliffs.": "Nu fugi tot timpul. Pe bune. Vei cădea de pe hărți.", + "Don't spin for too long; you'll become dizzy and fall.": "Nu te învârti prea mult timp; vei ameți și vei fi pus la pământ.", "Hold any button to run. (Trigger buttons work well if you have them)": "Ține apăsat orice buton pentru a fugi. (Butoanele 'Trigger' funcționează bine şi ele dacă le ai)", - "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Ține apăsat orice buton pentru a fugi. Vei ajunge mai repede unde vrei,\ndar nu vei flua curba prea bine, așa că ai grijă să nu cazi de pe hărți.", + "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Pentru a fugi, ține apăsat orice buton. Vei ajunge mai repede unde vrei,\ndar nu vei lua curba prea bine, așa că ai grijă să nu cazi de pe hărți.", "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Bombele de gheață nu sunt foarte puternice, dar îngheață\npe oricine lovesc, lăsând victimele vulnerabile la spargere.", - "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Dacă cineva te ia pe sus, dă-i un pumn și te va lăsa jos.\nApropo, merge și în viața reală.", - "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Dacă nu ai destule controllere,instalează aplicația '${REMOTE_APP_NAME}' \npe dispozitivele tale mobile pentru a le folosi drept controllere.", + "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Dacă cineva te ia pe sus, dă-i un pumn și îți va da drumul.\n (Apropo, merge și în viața reală)", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Dacă nu ai destule controllere, instalează aplicația '${REMOTE_APP_NAME}' \npe dispozitivele tale mobile pentru a le folosi drept controllere.", "If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "Dacă nu ai destule controllere, instalează 'BombSquad Remote' pe\nun dispozitiv Android sau iOS pentru a-l folosi ca pe un controller.", - "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Dacă o bombă lipicioasă se lipește de tine,sari și rotește-te în cercuri.S-ar putea \nsă scapi de aceasta, în caz contrar vei face un spectacol din ultimele tale momente în viață", + "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Dacă se atinge vreodată o bombă lipicioasă de tine, sari și rotește-te în cercuri. S-ar putea \nsă scapi de aceasta, dar dacă nu, vei face un spectacol minunat din ultimele tale momente rămase în viață.", "If you kill an enemy in one hit you get double points for it.": "Dacă omori un inamic dintr-o singură lovitură vei primi puncte duble.", - "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Dacă atingi un Blestem, singura ta şansă de viața este să\nprinzi o trusă de prim-ajutor în următoarele 5 secunde.", - "If you stay in one place, you're toast. Run and dodge to survive..": "Dacă stai într-un singur loc, vei ajunge ca o pâine prăjită. Fugi şi fereşte-te pentru a supraviețui..", - "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Dacă sunt foarte mulți jucători care intră și devin inactivi, bifează 'Dă afară jucătorii inactivi'\nîn Setări în caz că cineva uită să iasă din joc.", - "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Dacă dispozitivul tău se încălzeşte prea rapid sau ai vrea să-i conservi bateria,\ndu-te în meniul de \"Vizuale\" sau \"Rezoluție\" în Setări->Grafici.", - "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Dacă jocul rulează mai puțin bine, încearcă să lași mai jos rezoluția \nsau vizualele din setările grafice ale jocului.", - "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "În Capturează-Steagul, steagul tău trebuie să fie la baza ta pentru a înscrie. Dacă\nechipa opusă e pe cale să înscrie, o opțiune bună pentru a-i opri din a face asta este să le furi steagul.", - "In hockey, you'll maintain more speed if you turn gradually.": "În Hockey, vei menține mai multă viteză dacă te roteşti când trebuie.", + "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Dacă atingi un blestem, singura ta şansă de a trăi este să\nprinzi o trusă de prim-ajutor în următoarele 5 secunde.", + "If you stay in one place, you're toast. Run and dodge to survive..": "Dacă stai într-un singur loc, vei muri ca un fraier. Ferește-te și aleargă din loc în loc pentru a supraviețui..", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Dacă sunt foarte mulți jucători care intră și devin inactivi,\nbifează caseta 'Dă Afară Jucătorii Inactivi' din Setări în caz că cineva uită să iasă din joc.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Dacă dispozitivul tău se încălzeşte prea rapid sau ai vrea să-i conservi bateria,\ndu-te în meniul de \"Vizuale\" sau \"Rezoluție\" în Setări -> Grafici și dă-le la un nivel mai slab.", + "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Dacă jocul nu rulează chiar așa de bine cum ar trebui, \nîncearcă să lași mai jos rezoluția sau vizualele din setările grafice ale jocului.", + "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "În Capturează-Steagul, steagul tău trebuie să fie la baza echipei tale pentru a înscrie. Dacă\nechipa opusă e pe cale să înscrie, o bună opțiune pentru a-i opri din a face asta este să le furi steagul.", + "In hockey, you'll maintain more speed if you turn gradually.": "În Hockey, vei menține mai multă viteză dacă te întorci când trebuie.", "It's easier to win with a friend or two helping.": "E mai ușor să câștigi cu un prieten sau 2 care te ajută.", - "Jump just as you're throwing to get bombs up to the highest levels.": "Sari fix înainte să arunci pentru a trimite bombele către cele mai înalte nivele.", - "Land-mines are a good way to stop speedy enemies.": "Minele sunt folositoare pentru a opri inamicii rapizi.", - "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Multe lucruri pot fi ridicate şi aruncate, inclusiv și alți jucători. Aruncarea\ninamicilor de pe hărți poate fi o strategie efectivă şi plină de emoții.", - "No, you can't get up on the ledge. You have to throw bombs.": "Nu, nu te poți urca pe platformă. Trebuie să arunci cu bombele.", - "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Jucătorii pot intra şi ieşii în mijlocul unelor jocuri,\nşi mai poți conecta/deconecta controllerele când vrei tu.", + "Jump just as you're throwing to get bombs up to the highest levels.": "Sari imediat ce arunci bombele pentru a le trimite către cele mai înalte nivele.", + "Land-mines are a good way to stop speedy enemies.": "Minele sunt folositoare la oprirea inamicilor rapizi.", + "Many things can be picked up and thrown, including other players. Tossing\nyour enemies off cliffs can be an effective and emotionally fulfilling strategy.": "Multe lucruri pot fi ridicate şi aruncate, inclusiv alți jucători. Aruncarea\ninamicilor de pe hărți poate fi o strategie efectivă şi plină de emoții.", + "No, you can't get up on the ledge. You have to throw bombs.": "Nu, nu te poți urca pe platformă. Trebuie să arunci cu bombele înspre inamici.", + "Players can join and leave in the middle of most games,\nand you can also plug and unplug controllers on the fly.": "Jucătorii pot să intre și să iasă din majoritatea jocurilor când vor ei,\nși nu uita că poți conecta și deconecta controllere când vrei tu.", "Practice using your momentum to throw bombs more accurately.": "Antrenează-te folosindu-ți momentum-ul pentru a arunca bombele mai corect.", - "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Pumnii cauzează mai multe daune când te mişti,\ndeci încearcă să fugi, să sari, şi să te învârți ca un nebun.", - "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Fugi în față şi în spate înainte să arunci o bombă\npentru a învârti-o şi a o arunca mai departe.", + "Punches do more damage the faster your fists are moving,\nso try running, jumping, and spinning like crazy.": "Pumnii tăi cauzează mai multe daune când te mişti,\ndeci încearcă să fugi, să sari, şi să te învârți ca un nebun.", + "Run back and forth before throwing a bomb\nto 'whiplash' it and throw it farther.": "Mergi în față şi în spate înainte să arunci o bombă\npentru a o 'învârti', aruncând-o mai departe.", "Take out a group of enemies by\nsetting off a bomb near a TNT box.": "Omoară un grup de inamici cu ajutorul\nunei bombe lângă o cutie cu TNT.", - "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "Capul este cea mai vulnerabilă parte a corpului tău, deci o Bombă Lipicioasă\nîn ceafă înseamnă în cele mai multe cazuri game-over.", - "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Acest nivel nu se termină niciodată, dar un scor mare\nîți poate aduce foarte mult respect prin lume.", - "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Puterea aruncării este bazată pe direcția pe care o ții.\nDacă vrei să arunci ceva în fața ta într-un mod gentil, va trebui să nu te miști.", - "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Te-ai săturat de muzica originală din joc? Schimb-o cu a ta!\nVezi Setări->Audio->Coloană Sonoră", + "The head is the most vulnerable area, so a sticky-bomb\nto the noggin usually means game-over.": "Capul este cea mai vulnerabilă parte a corpului tău, \ndeci o bombă lipicioasă în ceafă înseamnă game-over.", + "This level never ends, but a high score here\nwill earn you eternal respect throughout the world.": "Acest nivel nu se termină niciodată, dar un scor mare\nîn acesta îți poate aduce foarte mult respect prin lume.", + "Throw strength is based on the direction you are holding.\nTo toss something gently in front of you, don't hold any direction.": "Puterea aruncării este bazată pe direcția ținută de tine.\nDacă vrei să arunci ceva în fața ta ușurel, va trebui să nu ții nici o direcție și să dai drumul obiectului respectiv.", + "Tired of the soundtrack? Replace it with your own!\nSee Settings->Audio->Soundtrack": "Te-ai săturat de muzica originală din joc? Schimb-o cu a ta!\n Vezi Setări -> Audio -> Soundtrack Personalizat", "Try 'Cooking off' bombs for a second or two before throwing them.": "Încearcă să ții bombele în mână pentru o secundă sau două înainte să le arunci.", - "Try tricking enemies into killing eachother or running off cliffs.": "Încearcă să păcăleşti inamicii să se omoare între ei sau să cadă de pe hărți.", - "Use the pick-up button to grab the flag < ${PICKUP} >": "Foloseşte butonul de ridicare pentru a ridica steagul < ${PICKUP} >", - "Whip back and forth to get more distance on your throws..": "Mergi în față şi în spate pentru a avea o distanță mai mare la aruncări..", - "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Poți să dai cu pumnii în stânga sau în dreapta dacă te învârți.\nAcest lucru este folositor pentru a da tipii răi jos de pe dealuri sau pentru a înscrie în hockey.", - "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Poți să îți dai seama când o bombă va exploda după culoarea\nscânteilor făcute de fitil: galben..portocaliu..roşu..BOOM.", - "You can throw bombs higher if you jump just before throwing.": "Poți arunca bombe mai sus dacă sari exact înainte să arunci.", - "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Iei daune când îți trânteşti capul de lucruri,\ndeci nu-ți trânti capul de lucruri.", + "Try tricking enemies into killing eachother or running off cliffs.": "Încearcă să-ți păcăleşti inamicii în așa fel încât aceștia încep să se omoare între ei sau să cadă de pe hărți.", + "Use the pick-up button to grab the flag < ${PICKUP} >": "Foloseşte butonul de ridicare < ${PICKUP} > pentru a ridica steagul", + "Whip back and forth to get more distance on your throws..": "Mergi în spate și în față pentru a arunca lucrurile la o distanță mai mare..", + "You can 'aim' your punches by spinning left or right.\nThis is useful for knocking bad guys off edges or scoring in hockey.": "Poți să 'țintești' cu pumnii rotindu-te la stânga și la dreapta.\nAcest lucru este folositor pentru a da tipii răi jos de pe marginile hărților sau pentru a înscrie în hockey.", + "You can judge when a bomb is going to explode based on the\ncolor of sparks from its fuse: yellow..orange..red..BOOM.": "Poți să îți dai seama când o bombă va exploda după culoarea\nscânteilor făcute de fitilul acesteia: galben..portocaliu..roşu..*BOOM*.", + "You can throw bombs higher if you jump just before throwing.": "Poți arunca bombe mai sus dacă le arunci imediat după ce sari.", + "You take damage when you whack your head on things,\nso try to not whack your head on things.": "Iei daune când îți trânteşti capul de lucruri,\ndeci încearcă să nu-ți trântești capul de lucruri.", "Your punches do much more damage if you are running or spinning.": "Pumnii tăi dau mai multe daune dacă fugi sau te învârți." } }, - "trophiesRequiredText": "Asta necesită măcar ${NUMBER} trofee.", + "trophiesRequiredText": "Acest lucru necesită cel puțin ${NUMBER} trofee.", "trophiesText": "Trofee", "trophiesThisSeasonText": "Trofeele Din Acest Sezon", "tutorial": { - "cpuBenchmarkText": "Se rulează tutorialul la viteze ridicole (în principal testează viteza CPU-ului)", + "cpuBenchmarkText": "Se rulează tutorialul la o viteză ridicolă (în principal testează viteza CPU-ului)", "phrase01Text": "Salut!", - "phrase02Text": "Bun Venit în ${APP_NAME}!", + "phrase02Text": "Bun venit în ${APP_NAME}!", "phrase03Text": "Uite niște sfaturi pentru a-ți controla caracterul:", "phrase04Text": "Multe lucruri din ${APP_NAME} sunt bazate pe FIZICĂ.", "phrase05Text": "De exemplu, când dai cu pumnul...", "phrase06Text": "...daunele sunt bazate pe viteza pumnilor tăi.", - "phrase07Text": "Vezi? Nu ne mișcăm, deci ${NAME} de-abia a simțit ceva.", - "phrase08Text": "Acum hai să sărim și să ne rotim pentru a avea viteză mai mare.", + "phrase07Text": "Vezi? Nu ne mișcăm, deci ${NAME} de-abia dacă a simțit ceva.", + "phrase08Text": "Acum hai să sărim și să ne rotim pentru a avea o viteză mai mare.", "phrase09Text": "Aha, așa mai merge.", "phrase10Text": "De asemenea, și alergatul ajută.", "phrase11Text": "Ține apăsat ORICE buton pentru a alerga.", - "phrase12Text": "Pentru pumni super puternici, încearcă să te rotești și să fugi.", + "phrase12Text": "Pentru lovituri super-puternice, încearcă să te rotești și să fugi.", "phrase13Text": "Ups; scuze pentru asta, ${NAME}.", "phrase14Text": "Poți ridica și arunca obiecte cum ar fi steaguri... sau îl poți arunca pe ${NAME}.", "phrase15Text": "În final, sunt bombele.", "phrase16Text": "Aruncatul lor are nevoie de antrenament.", "phrase17Text": "Au! Nu a fost o aruncare chiar bună.", - "phrase18Text": "Mișcatul te ajută să arunci mai departe.", - "phrase19Text": "Săritul te ajută să arunci mai sus.", - "phrase20Text": "Iar învârtirea lor te ajută să le arunci mult mai departe.", + "phrase18Text": "Mișcatul te ajută să le arunci mai departe.", + "phrase19Text": "Săritul te ajută să le arunci mai sus.", + "phrase20Text": "Iar învârtirea lor te ajută să le arunci și mai departe.", "phrase21Text": "Cronometrarea bombelor poate fi complicată.", - "phrase22Text": "Fir-ar să fie.", + "phrase22Text": "Fir-ar să fie!", "phrase23Text": "Încearcă să lași fitilul să ardă pentru un moment.", "phrase24Text": "Ura! Bine ars.", "phrase25Text": "Păi, cam asta-i tot.", @@ -1790,7 +1799,7 @@ "randomName4Text": "Marius", "randomName5Text": "Ștefan", "skipConfirmText": "Sigur vrei să treci peste tutorial? Apasă din nou orice buton pentru a confirma.", - "skipVoteCountText": "${COUNT}/${TOTAL} voturi pentru a trece peste", + "skipVoteCountText": "${COUNT}/${TOTAL} voturi pentru a trece peste tutorial", "skippingText": "Se trece peste tutorial...", "toSkipPressAnythingText": "(apasă orice buton pentru a trece peste tutorial)" }, @@ -1806,9 +1815,9 @@ "updatingAccountText": "Se îmbunătățeşte contul tău...", "upgradeText": "Îmbunătățeşte", "upgradeToPlayText": "Deblochează \"${PRO}\" în magazinul din joc pentru a juca această hartă.", - "useDefaultText": "Folosește setările prestabilite", + "useDefaultText": "Folosește Setările Prestabilite", "usesExternalControllerText": "Acest joc folosește un controller extern ca dispozitiv de intrare.", - "usingItunesText": "Se folosește Aplicația de Muzică pentru coloana sonoră...", + "usingItunesText": "Se folosește Aplicația de Muzică pentru soundtrack...", "usingItunesTurnRepeatAndShuffleOnText": "Fii sigur(ă) că shuffle e activat și repeat e pus pe ALL în iTunes.", "validatingTestBuildText": "Se Validează Versiunea De Test...", "victoryText": "Victorie!", @@ -1816,17 +1825,17 @@ "voteInProgressText": "Un vot este deja în progres.", "votedAlreadyText": "Ai votat deja", "votesNeededText": "${NUMBER} voturi necesare", - "vsText": "versus", + "vsText": "vs", "waitingForHostText": "(așteaptă ca ${HOST} să continue)", "waitingForPlayersText": "se așteaptă pentru jucători noi...", "waitingInLineText": "Așteaptă la coadă (server-ul este plin)...", - "watchAVideoText": "Uită-te la o reclamă", + "watchAVideoText": "Uită-te la o Reclamă", "watchAnAdText": "Uită-te la o Reclamă", "watchWindow": { "deleteConfirmText": "Sigur vrei să ștergi reluarea \"${REPLAY}\"?", "deleteReplayButtonText": "Șterge\nReluarea", "myReplaysText": "Reluările mele", - "noReplaySelectedErrorText": "Nicio rReluare Selectată", + "noReplaySelectedErrorText": "Nicio Reluare Selectată", "playbackSpeedText": "Viteză de redare: ${SPEED}", "renameReplayButtonText": "Redenumește\nReluarea", "renameReplayText": "Redenumește \"${REPLAY}\" în:", @@ -1840,7 +1849,7 @@ "titleText": "Vizionează", "watchReplayButtonText": "Vezi\nReluarea" }, - "waveText": "Val", + "waveText": "Valul", "wellSureText": "Sigur!", "wiimoteLicenseWindow": { "titleText": "Copyright de către DarwiinRemote" @@ -1861,6 +1870,8 @@ "winsPlayerText": "${NAME} Câștigă!", "winsTeamText": "${NAME} Câștigă!", "winsText": "${NAME} Câștigă!", + "workspaceSyncErrorText": "Nu s-a putut sincroniza ${WORKSPACE}. Vezi jurnalul de activități pentru detalii.", + "workspaceSyncReuseText": "Nu se poate sincroniza ${WORKSPACE}. Se va folosi ultima versiune sincronizată.", "worldScoresUnavailableText": "Scorurile globale sunt indisponibile.", "worldsBestScoresText": "Top Scoruri Mondiale", "worldsBestTimesText": "Top Timpi Mondiali", diff --git a/dist/ba_data/data/languages/russian.json b/dist/ba_data/data/languages/russian.json index 58ec990..88c6eae 100644 --- a/dist/ba_data/data/languages/russian.json +++ b/dist/ba_data/data/languages/russian.json @@ -39,7 +39,7 @@ "testAccountWarningCardboardText": "Внимание: вы подписываете в с \"тест\" счета.\nЭти данные будут заменены со счетами Google сразу\nони становятся поддерживается в картонные приложений.\n\nСейчас вы будете зарабатывать все билеты в игре.\n(вы , тем не менее , получить Pro обновление BombSquad бесплатно )", "testAccountWarningOculusText": "Внимание: вы входите под акканутом \"test\".\nОни будут заменены аккаунтами Oculus позже в этом\nгоду, что позволит совершать покупки билетов и многое другое.\n\nНа данный момент вам придется зарабатывать билеты в игре.\n(однако, вы получаете обновление BombSquad Pro бесплатно)", "testAccountWarningText": "Внимание: вы заходите под аккаунтом \"test\".\nЭтот аккаунт привязан к конкретному устройству и может\nпериодически сбрасываться. (так что лучше особо\nне тратить время на сбор/разблокировку добра в нем)\n\nЧтобы использовать настоящий аккаунт (Game-Center,\nGoogle Plus и т.д.), запустите платную версию. Это также\nпозволит сохранять свой прогресс в облаке и делать его\nдоступным для разных устройств.", - "ticketsText": "Билетов: ${COUNT}", + "ticketsText": "Количество билетов: ${COUNT}", "titleText": "Аккаунт", "unlinkAccountsInstructionsText": "Выберите аккаунт, который хотите отвязать", "unlinkAccountsText": "Отвязать аккаунты", @@ -49,14 +49,14 @@ "youAreSignedInAsText": "Вы вошли как:" }, "achievementChallengesText": "Достижения", - "achievementText": "Медаль", + "achievementText": "Достижение", "achievements": { "Boom Goes the Dynamite": { "description": "Убейте 3 плохих парней с помощью TNT", "descriptionComplete": "С помощью TNT убито 3 плохих парней", "descriptionFull": "Убейте 3 плохих парней с помощью TNT на уровне ${LEVEL}", "descriptionFullComplete": "3 плохих парней убито с помощью TNT на уровне ${LEVEL}", - "name": "Сейчас бабахнет" + "name": "Динамит сейчас взорвётся!" }, "Boxer": { "description": "Победите без использования бомб", @@ -134,7 +134,7 @@ "descriptionComplete": "С карты сброшено 3 негодяя", "descriptionFull": "Сбросьте 3 негодяев с карты на уровне ${LEVEL}", "descriptionFullComplete": "3 негодяя сброшено с карты на уровне ${LEVEL}", - "name": "Давай отсюда" + "name": "Пора учится летать" }, "Onslaught God": { "description": "Наберите 5000 очков", @@ -180,7 +180,7 @@ }, "Pro Football Shutout": { "description": "Победите в сухую", - "descriptionComplete": "Победил в сухую", + "descriptionComplete": "Уровень был пройден в сухую", "descriptionFull": "Выиграйте матч ${LEVEL} в сухую", "descriptionFullComplete": "Победа в матче ${LEVEL} в сухую", "name": "${LEVEL} в сухую" @@ -194,9 +194,9 @@ }, "Pro Onslaught Victory": { "description": "Победите все волны", - "descriptionComplete": "Побеждены все волны", + "descriptionComplete": "Пройдены все волны", "descriptionFull": "Победите все волны на уровне ${LEVEL}", - "descriptionFullComplete": "Побеждены все волны на уровне ${LEVEL}", + "descriptionFullComplete": "Пройдены все волны на ${LEVEL}", "name": "Победа на уровне ${LEVEL}" }, "Pro Runaround Victory": { @@ -215,7 +215,7 @@ }, "Rookie Football Victory": { "description": "Выиграйте матч", - "descriptionComplete": "Матч выигран", + "descriptionComplete": "Матч пройден в вашу пользу", "descriptionFull": "Выиграйте матч ${LEVEL}", "descriptionFullComplete": "Выигран матч ${LEVEL}", "name": "Победа в матче ${LEVEL}" @@ -250,7 +250,7 @@ }, "Sharing is Caring": { "descriptionFull": "Успешно поделиться игрой с другом", - "descriptionFullComplete": "Игра успешно поделена с другом", + "descriptionFullComplete": "Игра успешно передана другу", "name": "Делиться - значит заботиться" }, "Stayin' Alive": { @@ -309,7 +309,7 @@ }, "Uber Football Victory": { "description": "Выиграйте матч", - "descriptionComplete": "Матч выигран", + "descriptionComplete": "Матч пройден в вашу пользу", "descriptionFull": "Выиграйте матч ${LEVEL}", "descriptionFullComplete": "Выигран матч ${LEVEL}", "name": "Победа в матче ${LEVEL}" @@ -329,9 +329,10 @@ "name": "Победа на уровне ${LEVEL}" } }, - "achievementsRemainingText": "Оставшиеся медали:", - "achievementsText": "Медали", + "achievementsRemainingText": "Осталось достижений:", + "achievementsText": "Достижения", "achievementsUnavailableForOldSeasonsText": "К сожалению, подробности достижений не доступны для старых сезонов.", + "activatedText": "${THING} активировано.", "addGameWindow": { "getMoreGamesText": "Еще игр...", "titleText": "Добавить игру" @@ -340,7 +341,7 @@ "alreadySignedInText": "На вашем аккаунте играют на другом устройстве;\nпожалуйста зайдите с другого аккаунта или закройте\nигру на другом устройстве и попытайтесь снова.", "apiVersionErrorText": "Невозможно загрузить модуль ${NAME}; он предназначен для API версии ${VERSION_USED}; здесь требуется версия ${VERSION_REQUIRED}.", "audioSettingsWindow": { - "headRelativeVRAudioInfoText": "(Режим \"Авто\" включает его только когда подключены наушники)", + "headRelativeVRAudioInfoText": "(Режим \"Авто\" активируется только при подключении наушников)", "headRelativeVRAudioText": "Позиционно-зависимое ВР-аудио", "musicVolumeText": "Громкость музыки", "soundVolumeText": "Громкость звука", @@ -374,37 +375,37 @@ "completeThisLevelToProceedText": "Чтобы продолжить, нужно\nпройти этот уровень!", "completionBonusText": "Бонус за прохождение", "configControllersWindow": { - "configureControllersText": "Настройка контроллеров", + "configureControllersText": "Настройка геймпада", "configureGamepadsText": "Настройка контроллеров", "configureKeyboard2Text": "Настройка клавиатуры P2", "configureKeyboardText": "Настройка клавиатуры", - "configureMobileText": "Мобильные устройства как контроллеры", + "configureMobileText": "Использовать мобильные устройства в качестве геймпадов", "configureTouchText": "Настройка сенсорного экрана", - "ps3Text": "Контроллеры PS3", - "titleText": "Контроллеры", - "wiimotesText": "Контроллеры Wii", - "xbox360Text": "Контроллеры Xbox 360" + "ps3Text": "Геймпады PS3™", + "titleText": "Геймпады", + "wiimotesText": "Пульт Wii™", + "xbox360Text": "Геймпады Xbox 360™" }, "configGamepadSelectWindow": { - "androidNoteText": "Внимание: поддержка контроллеров различается в зависимости от устройства и версии Android.", - "pressAnyButtonText": "Нажмите любую кнопку на контроллере,\n который хотите настроить...", - "titleText": "Настроить контроллер" + "androidNoteText": "Внимание: поддержка геймпада различается в зависимости от устройства и версии Android.", + "pressAnyButtonText": "Нажмите любую кнопку на геймпаде,\n которую хотите настроить...", + "titleText": "Настроить геймпад" }, "configGamepadWindow": { "advancedText": "Дополнительно", - "advancedTitleText": "Дополнительные настройки контроллера", + "advancedTitleText": "Дополнительные настройки геймпада", "analogStickDeadZoneDescriptionText": "(Включите, если персонаж продолжает двигаться после того, как стик отпущен)", "analogStickDeadZoneText": "Мертвая зона аналогового стика", - "appliesToAllText": "(относится ко всем контроллерам данного типа)", + "appliesToAllText": "(настроить ко всем геймпадам данного типа)", "autoRecalibrateDescriptionText": "(включить, если персонаж не двигается на полной скорости)", "autoRecalibrateText": "Авто-перекалибровка аналоговых стиков", "axisText": "ось", "clearText": "очистить", "dpadText": "D-Pad", - "extraStartButtonText": "Дополнительная кнопка \"Старт", + "extraStartButtonText": "Настроить дополнительную кнопку \"Start\"", "ifNothingHappensTryAnalogText": "Если ничего не происходит, попробуйте вместо этого присвоить аналоговому стику.", "ifNothingHappensTryDpadText": "Если ничего не происходит, попробуйте вместо этого присвоить D-Pad.", - "ignoreCompletelyDescriptionText": "(не дайте этому контроллеру воздействовать на игру, или меню)", + "ignoreCompletelyDescriptionText": "(запретить геймпаду воздействовать на игру, или меню)", "ignoreCompletelyText": "Игнорировать полностью", "ignoredButton1Text": "Игнорируемая кнопка 1", "ignoredButton2Text": "Игнорируемая кнопка 2", @@ -422,13 +423,13 @@ "runTrigger1Text": "Триггер для бега 1", "runTrigger2Text": "Триггер для бега 2", "runTriggerDescriptionText": "(аналоговые триггеры позволяют бегать с разной скоростью)", - "secondHalfText": "Используйте это для настройки второй половины\nустройства 'два контроллера в одном',\nкоторое показывается как один контроллер.", + "secondHalfText": "Используйте эту опцию для настройки второй\nполовины устройства \"два геймпада в одном\",\nдля использования в качестве одного геймпада.", "secondaryEnableText": "Включить", - "secondaryText": "Вторичный контроллер", + "secondaryText": "Второй геймпад", "startButtonActivatesDefaultDescriptionText": "(выключить, если ваша кнопка \"старт\" работает больше в качестве кнопки \"меню\")", "startButtonActivatesDefaultText": "Кнопка Старт активирует стандартный виджет", - "titleText": "Настройка контроллера", - "twoInOneSetupText": "Настройка контроллера 2-в-1", + "titleText": "Настройка геймпада", + "twoInOneSetupText": "Настройка геймпада 2-в-1", "uiOnlyDescriptionText": "(Запретить этому контроллеру присоединяться к игре)", "uiOnlyText": "Ограничить только для меню", "unassignedButtonsRunText": "Все неприсвоенные кнопки для бега", @@ -481,8 +482,8 @@ "challengesText": "Испытания", "currentBestText": "Последний рекорд", "customText": "Другое", - "entryFeeText": "Участие", - "forfeitConfirmText": "Так просто сдаться?", + "entryFeeText": "Участвовать", + "forfeitConfirmText": "Сдаться?", "forfeitNotAllowedYetText": "Вы не можете покинуть это состязание.", "forfeitText": "Сдаться", "multipliersText": "Множители", @@ -651,14 +652,16 @@ "epicDescriptionFilterText": "${DESCRIPTION} в эпическом замедленном действии.", "epicNameFilterText": "${NAME} в эпическом режиме", "errorAccessDeniedText": "доступ запрещен", + "errorDeviceTimeIncorrectText": "Время на устройстве отстает на ${HOURS} часов.\nЭто вызывает проблемы.\nПожалуйста, проверьте настройки времени и часового пояса.", "errorOutOfDiskSpaceText": "нет места на диске", + "errorSecureConnectionFailText": "Ошибка установки безопасного облачного соединения; сетевые функции могут дать сбой.", "errorText": "Ошибка", "errorUnknownText": "неизвестная ошибка", "exitGameText": "Выйти из ${APP_NAME}?", "exportSuccessText": "'${NAME}' экспортирован.", "externalStorageText": "Внешняя память", "failText": "Провал", - "fatalErrorText": "Ой, что-то потерялось или сломалось.\nПопытайтесь переустановить приложение или\nобратитесь к ${EMAIL} за помощью.", + "fatalErrorText": "Файлы игры повреждены или отсутствуют.\nПопробуйте переустановить приложение или\nобратитесь к ${EMAIL} за помощью.", "fileSelectorWindow": { "titleFileFolderText": "Выберите файл или папку", "titleFileText": "Выберите файл", @@ -670,7 +673,7 @@ "finalScoresText": "Финальные очки", "finalTimeText": "Финальное время", "finishingInstallText": "Завершается установка, минутку...", - "fireTVRemoteWarningText": "* Для лучшего результата используйте\nигровые контроллеры или установите\nприложение '${REMOTE_APP_NAME}'\nна ваших телефонах и планшетах.", + "fireTVRemoteWarningText": "* Для лучшего результата используйте\nгеймпады или установите\nприложение '${REMOTE_APP_NAME}'\nна ваших телефонах и планшетах.", "firstToFinalText": "Финал до ${COUNT} очков", "firstToSeriesText": "Серия до ${COUNT} очков", "fiveKillText": "ПЯТЕРЫХ ЗА РАЗ!!!", @@ -708,7 +711,7 @@ "addressFetchErrorText": "<ошибка запроса адресов>", "appInviteInfoText": "Пригласите друзей поиграть в BombSquad и они\nполучат ${COUNT} бесплатных билетов. Ты получишь\n${YOU_COUNT} за каждого друга.", "appInviteMessageText": "${NAME} отправил вам ${COUNT} билетов в ${APP_NAME}", - "appInviteSendACodeText": "Отправьте им код", + "appInviteSendACodeText": "Отправить им код", "appInviteTitleText": "Приглашение в ${APP_NAME}", "bluetoothAndroidSupportText": "(работает с любым устройством, поддерживающим Bluetooth)", "bluetoothDescriptionText": "Создать/войти в лобби через Bluetooth:", @@ -829,7 +832,7 @@ "ticketPack4Text": "Огромная пачка билетов", "ticketPack5Text": "Слоновая пачка билетов", "ticketPack6Text": "Максимальная пачка билетов", - "ticketsFromASponsorText": "Получить ${COUNT} билетов\nот спонсора", + "ticketsFromASponsorText": "Посмотреть рекламу,\nчтобы получить ${COUNT} билетов", "ticketsText": "Билетов: ${COUNT}", "titleText": "Получить билеты", "unavailableLinkAccountText": "Извините, но на этой платформе покупки недоступны.\nВ качестве решения, вы можете привязать этот аккаунт \nк аккаунту на другой платформе, и совершать покупки там.", @@ -840,6 +843,7 @@ "youHaveText": "У вас ${COUNT} билетов" }, "googleMultiplayerDiscontinuedText": "Простите, сервис многопользовательской игры Google больше не поддерживается.\nЯ работаю над заменой так быстро, насколько это возможно.\nДо тех пор, пожалуйста выберете другой способ подключения.\n-Эрик", + "googlePlayPurchasesNotAvailableText": "Покупки в Google Play недоступны.\nВозможно, вам необходимо обновить приложение магазина.", "googlePlayText": "Google Play", "graphicsSettingsWindow": { "alwaysText": "Всегда", @@ -1166,7 +1170,10 @@ "playlistsText": "Плей-листы", "pleaseRateText": "Если вам нравится игра ${APP_NAME}, пожалуйста, подумайте о том,\nчтобы оценить ее или написать рецензию. Это обеспечивает полезную\nобратную связь и помогает поддержать дальнейшую разработку.\n\nСпасибо!\n- Эрик", "pleaseWaitText": "Пожалуйста, подождите...", - "pluginsDetectedText": "Обнаружены новые плагины. Включите/настройте их в настройках.", + "pluginClassLoadErrorText": "Ошибка при попытке загрузить класс плагина '${PLUGIN}': ${ERROR}", + "pluginInitErrorText": "Ошибка при инициализации плагина '${PLUGIN}': ${ERROR}", + "pluginsDetectedText": "Обнаружены новые плагины! Перезапустите игру, чтобы активировать их, или настройте их в настройках.", + "pluginsRemovedText": "${NUM} плагин(ов) больше не найдены.", "pluginsText": "Плагины", "practiceText": "Тренировка", "pressAnyButtonPlayAgainText": "Нажмите любую кнопку чтобы играть снова...", @@ -1188,10 +1195,10 @@ }, "promoSubmitErrorText": "Ошибка отправки кода, проверьте своё интернете соединение", "ps3ControllersWindow": { - "macInstructionsText": "Выключите питание на задней панели PS3, убедитесь, что Bluetooth\nвключен на вашем компьютере, а затем подключите контроллер к Маку\nс помощью кабеля USB для синхронизации. Теперь можно использовать\nкнопку контроллера 'PS' чтобы подключить его к Маку\nв проводном (USB) или беспроводном (Bluetooth) режиме.\n\nНа некоторых Маках при синхронизации может потребоваться код доступа.\nВ этом случае обратитесь к следующей инструкции или к гуглу.\n\n\n\n\nКонтроллеры PS3, связанные по беспроводной сети, должны появиться\nв списке устройств в Настройках системы -> Bluetooth. Возможно, вам придется\nудалить их из этого списка, если вы хотите снова использовать их с PS3.\n\nТакже всегда отключайте их от Bluetooth, когда он не используется,\nиначе будут садиться батарейки.\n\nBluetooth должен обрабатывать до 7 подключенных устройств,\nхотя у вас может получиться по-другому.", - "ouyaInstructionsText": "Чтобы использовать контроллер PS3 с OUYA, просто подключите его один раз\nс помощью кабеля USB для синхронизации. Это может отключить другие\nконтроллеры, тогда нужно перезагрузить OUYA и отсоединить кабель USB.\n\nПосле этого можно использовать кнопку 'PS' контроллера для беспроводного\nподключения. После игры нажмите и удерживайте кнопку 'PS' в течение\n10 секунд чтобы выключить контроллер, в противном случае он может\nостаться включенным и разрядит батарейки.", + "macInstructionsText": "Выключите питание на задней панели PS3, убедитесь, что Bluetooth\nвключен на вашем компьютере, а затем подключите геймпад к Mac\nс помощью кабеля USB для синхронизации. Теперь можно использовать\nкнопку геймпад 'PS' чтобы подключить его к Mac\nв проводном (USB) или беспроводном (Bluetooth) режиме.\n\nНа некоторых системах Mac при синхронизации может потребоватьсякод доступа.\nВ этом случае обратитесь к следующей инструкции или к гуглу.\n\n\n\n\nГеймпады PS3, связанные по беспроводной сети, должны появиться\nв списке устройств в Настройках системы -> Bluetooth. Возможно, вам придется\nудалить их из этого списка, если вы хотите снова использовать их с PS3.\n\nТакже всегда отключайте их от Bluetooth, когда он не используется,\nиначе будут садиться батарейки.\n\nBluetooth должен обрабатывать до 7 подключенных устройств,\nхотя у вас может получиться по-другому.", + "ouyaInstructionsText": "Чтобы использовать геймпад PS3 с OUYA, просто подключите его один раз\nс помощью кабеля USB для синхронизации. Это может отключить другие\nгеймпады, тогда нужно перезагрузить OUYA и отсоединить кабель USB.\n\nПосле этого можно использовать кнопку 'PS' геймпада для беспроводного\nподключения. После игры нажмите и удерживайте кнопку 'PS' в течение\n10 секунд чтобы выключить геймпад, в противном случае он может\nостаться включенным и разрядит батарейки.", "pairingTutorialText": "видео-тьюториал по синхронизации", - "titleText": "Использование контроллеров PS3 с ${APP_NAME}:" + "titleText": "Использование геймпада PS3 с ${APP_NAME}:" }, "publicBetaText": "ОТКРЫТАЯ БЕТА-ВЕРСИЯ", "punchBoldText": "УДАР", @@ -1201,7 +1208,7 @@ "purchasingText": "Покупка...", "quitGameText": "Выйти из ${APP_NAME}?", "quittingIn5SecondsText": "Выход через 5 секунд...", - "randomPlayerNamesText": "Дима, Кузя, Вован, Маха, Русский, Какуля, Бибер, Борька, Няшка, Толян, Ержан, Дибисяра, Вася, Морген, Серёга, Ваня, Кеша, Жорик, Стёпа, Эдгар, Циган", + "randomPlayerNamesText": "Дима, Кузя, Вован, Маха, Русский, Какуля, Бибер, Борька, Няшка, Толян, Ержан, Дибисяра, Вася, Морген, Серёга, Ваня, Кеша, Жорик, Стёпа, Эдгар, Цыган, Олег, Егор, Ёршик", "randomText": "Случайный", "rankText": "Ранг", "ratingText": "Рейтинг", @@ -1508,9 +1515,9 @@ "Get the flag to the enemy end zone.": "Отнесите флаг в зону защиты противника.", "How fast can you defeat the ninjas?": "Как быстро вы сможете победить ниндзя?", "Kill a set number of enemies to win.": "Убейте заданное число врагов, чтобы выиграть.", - "Last one standing wins.": "Побеждает последний стоящий на ногах.", - "Last remaining alive wins.": "Побеждает последний в живых.", - "Last team standing wins.": "Побеждает последняя стоящая на ногах команда.", + "Last one standing wins.": "Побеждает последний живой игрок.", + "Last remaining alive wins.": "Побеждает последний живой игрок.", + "Last team standing wins.": "Побеждает последняя живая команда.", "Prevent enemies from reaching the exit.": "Не дайте врагам дойти до выхода.", "Reach the enemy flag to score.": "Доберитесь до вражеского флага чтобы набрать очки.", "Return the enemy flag to score.": "Принесите вражеский флаг на базу, чтобы набрать очки.", @@ -1536,8 +1543,8 @@ "Touch the enemy flag.": "Коснитесь вражеского флага.", "carry the flag for ${ARG1} seconds": "пронесите флаг в течение ${ARG1} секунд", "kill ${ARG1} enemies": "убейте ${ARG1} врагов", - "last one standing wins": "побеждает последний стоящий на ногах", - "last team standing wins": "побеждает последняя стоящая на ногах команда", + "last one standing wins": "побеждает последний живой игрок", + "last team standing wins": "побеждает последняя живая команда", "return ${ARG1} flags": "принесите ${ARG1} флагов на базу", "return 1 flag": "принесите 1 флаг на базу", "run ${ARG1} laps": "пробегите ${ARG1} кругов", @@ -1874,7 +1881,7 @@ }, "twoKillText": "ДВОИХ ЗА РАЗ!", "unavailableText": "недоступно", - "unconfiguredControllerDetectedText": "Обнаружен ненастроенный контроллер:", + "unconfiguredControllerDetectedText": "Обнаружен ненастроенный геймпад:", "unlockThisInTheStoreText": "Это должно быть разблокировано в магазине.", "unlockThisProfilesText": "Чтобы создать более ${NUM} профиль, Вам необходимо:", "unlockThisText": "Чтобы разблокировать это, вам нужно:", @@ -1940,13 +1947,15 @@ "winsPlayerText": "Победил ${NAME}!", "winsTeamText": "Победили ${NAME}!", "winsText": "${NAME} выиграл!", + "workspaceSyncErrorText": "Ошибка при попытке синхронизации ${WORKSPACE}. Посмотрите лог для информации.", + "workspaceSyncReuseText": "Не может синронизировать ${WORKSPACE}. Будет использоватся прошлая синхронизация.", "worldScoresUnavailableText": "Мировые результаты недоступны.", "worldsBestScoresText": "Лучшие в мире очки", "worldsBestTimesText": "Лучшее в мире время", "xbox360ControllersWindow": { "getDriverText": "Скачать драйвер", - "macInstructions2Text": "Для использования контроллеров по беспроводной связи, вам также\nпотребуется ресивер, который поставляется с \"беспроводным контроллером\nXbox 360 для Windows\". Один ресивер позволяет подключить до 4 контроллеров.\n\nВнимание: ресиверы сторонних производителей не будут работать с этим драйвером,\nубедитесь, что на вашем ресивере написано \"Microsoft\", а не \"XBOX 360\".\nMicrosoft больше не продает их отдельно, так что вам нужно будет найти\nресивер в комплекте с контроллером, либо искать на ebay.\n\nЕсли вы считаете это полезным, можете отправить денег разработчику\nдрайвера на его сайте.", - "macInstructionsText": "Для использования контроллеров Xbox 360 необходимо\nустановить драйвер Mac, доступный по ссылке ниже.\nОн работает и с проводными и беспроводными контроллерами.", + "macInstructions2Text": "Для использования контроллеров по беспроводной связи, вам также\nпотребуется ресивер, который поставляется с \"беспроводным геймпадом\nXbox 360 для Windows\". Один ресивер позволяет подключить до 4 контроллеров.\n\nВнимание: ресиверы сторонних производителей не будут работать с этим драйвером,\nубедитесь, что на вашем ресивере написано \"Microsoft\", а не \"XBOX 360\".\nMicrosoft больше не продает их отдельно, так что вам нужно будет найти\nресивер в комплекте с геймпадом, либо искать на ebay.\n\nЕсли вы считаете это полезным, можете отправить денег разработчику\nдрайвера на его сайте.", + "macInstructionsText": "Для использования геймпада Xbox 360 необходимо\nустановить драйвер Mac, доступный по ссылке ниже.\nОн работает и с проводными и беспроводными геймпадами.", "macInstructionsTextScale": 0.8, "ouyaInstructionsText": "Для использования проводных контроллеров Xbox 360 в BombSquad,\nпросто подключите их к USB-порту вашего устройства. Для нескольких\nконтроллеров можно использовать концентратор USB.\n\nДля использования беспроводных контроллеров вам понадобится беспроводной\nресивер который поставляется в наборе \"беспроводного геймпада Xbox 360\nдля Windows\" или продается отдельно. Каждый ресивер подключается\nк порту USB и позволяет подключать до 4 беспроводных контроллеров.", "titleText": "Использование контроллеров Xbox 360 в ${APP_NAME}:" diff --git a/dist/ba_data/data/languages/serbian.json b/dist/ba_data/data/languages/serbian.json index 4d11265..2ea8600 100644 --- a/dist/ba_data/data/languages/serbian.json +++ b/dist/ba_data/data/languages/serbian.json @@ -328,6 +328,7 @@ "achievementsRemainingText": "Неизвршена достигнућа:", "achievementsText": "Достигнућа", "achievementsUnavailableForOldSeasonsText": "Извини, информације нису доступне за старије сезоне.", + "activatedText": "${THING} Активиран", "addGameWindow": { "getMoreGamesText": "Додај више игри...", "titleText": "Додај игру" @@ -627,7 +628,9 @@ "epicDescriptionFilterText": "${DESCRIPTION} у епско успореној игри.", "epicNameFilterText": "Епски ${NAME}", "errorAccessDeniedText": "приступ одбијен", + "errorDeviceTimeIncorrectText": "Време на твом уређају је нетачно за ${HOURS} сати.\nОво може изазвати проблеме.\nМолимо вас проверите подешавања времена и временску зону.", "errorOutOfDiskSpaceText": "нема места на диску", + "errorSecureConnectionFailText": "Немогуће успоставити везу са клаудом; функција мреже може престати.", "errorText": "Грешка", "errorUnknownText": "непозната грешка", "exitGameText": "Изађи из \"${APP_NAME}\"?", @@ -1119,7 +1122,10 @@ "playlistsText": "Листе игара", "pleaseRateText": "Ако уживате у \"${APP_NAME}\" игри, молимо вас да издвојите\nмало времена да оцените апликацију или напишете коментар.\nОво нам даје корисне информације и помаже у будућем развоју.\n\nХвала!\n-Ерик", "pleaseWaitText": "Молимо вас сачекајте...", - "pluginsDetectedText": "Нови додатак/ци пронађен/и. Укључи/подеси их у подешавањима.", + "pluginClassLoadErrorText": "Грешка учитавања плугина класе '${PLUGIN}': ${ERROR}", + "pluginInitErrorText": "Грешка иницијације плугина '${PLUGIN}': ${ERROR}", + "pluginsDetectedText": "Нови додатак/ци пронађени. Рестартуј да их активираш,или конфигуришеш у подешавањима.", + "pluginsRemovedText": "${NUM} додатак/ више не постоје.", "pluginsText": "Додаци", "practiceText": "Вежбање", "pressAnyButtonPlayAgainText": "Притисни било које дугме да одиграш опет...", @@ -1855,6 +1861,8 @@ "winsPlayerText": "${NAME} је победио!", "winsTeamText": "${NAME} је победио!", "winsText": "${NAME} је победио!", + "workspaceSyncErrorText": "Грешка синхронизовања ${WORKSPACE}. Погледајте лог за више детаља.", + "workspaceSyncReuseText": "Синхронизовање ${WORKSPACE} неуспешно. Коришћење претходно синхронизоване верзије.", "worldScoresUnavailableText": "Светски резултати недоступни.", "worldsBestScoresText": "Најбољи резултати на свету", "worldsBestTimesText": "Најбоља времена на свету", diff --git a/dist/ba_data/data/languages/spanish.json b/dist/ba_data/data/languages/spanish.json index 35859f4..4e36269 100644 --- a/dist/ba_data/data/languages/spanish.json +++ b/dist/ba_data/data/languages/spanish.json @@ -32,7 +32,7 @@ "signInWithTestAccountInfoText": "(Cuenta de prueba, usa la cuenta del dispositivo para avanzar)", "signInWithTestAccountText": "Registrarse con una Cuenta de Prueba", "signInWithV2InfoText": "(una cuenta que funciona en todas las plataformas)", - "signInWithV2Text": "Inicie cesión con una cuenta de bosquad", + "signInWithV2Text": "Inicie sesión con tú cuenta de BombSquad", "signOutText": "Cerrar Sesión", "signingInText": "Iniciando sesión...", "signingOutText": "Cerrando sesión...", @@ -332,6 +332,7 @@ "achievementsRemainingText": "Logros pendientes:", "achievementsText": "Logros", "achievementsUnavailableForOldSeasonsText": "Lo sentimos, los logros específicos no están disponibles para temporadas anteriores.", + "activatedText": "${THING} activado.", "addGameWindow": { "getMoreGamesText": "Más Juegos...", "titleText": "Agregar Juego" @@ -651,7 +652,9 @@ "epicDescriptionFilterText": "${DESCRIPTION} En cámara lenta épica.", "epicNameFilterText": "${NAME} - Modo épico", "errorAccessDeniedText": "acceso negado", + "errorDeviceTimeIncorrectText": "La hora actual de tu dispositivo es ${HOURS} horas.\nEsto podría causar problemas.\nPorfavor verifica la hora y zona horaria en ajustes.", "errorOutOfDiskSpaceText": "insuficiente espacio en disco", + "errorSecureConnectionFailText": "No se puede establecer una conexión segura en la nube; La red podría estar fallando", "errorText": "Error", "errorUnknownText": "error desconocido", "exitGameText": "¿Salir de ${APP_NAME}?", @@ -838,6 +841,7 @@ "youHaveText": "tienes ${COUNT} boletos" }, "googleMultiplayerDiscontinuedText": "Lo siento, Google's multijugador servicio ya no esta mas disponible.\nEstoy trabajando en un reemplazo lo mas rapido posible.\nHasta entonces, por favor intente con otro metodo de conexión.\n-Eric", + "googlePlayPurchasesNotAvailableText": "Las compras de Google Play no están disponibles.\nEs posible que deba actualizar la aplicación de su tienda.", "googlePlayText": "Google Play", "graphicsSettingsWindow": { "alwaysText": "Siempre", @@ -1079,8 +1083,8 @@ "modeArcadeText": "Modo Arcade", "modeClassicText": "Modo Clásico", "modeDemoText": "Modo De Demostración", - "mostValuablePlayerText": "Jugador más valorado", - "mostViolatedPlayerText": "Jugador más agredido", + "mostValuablePlayerText": "Jugador más Valorado", + "mostViolatedPlayerText": "Jugador más Violado", "mostViolentPlayerText": "Jugador más Violento", "moveText": "Mover", "multiKillText": "¡¡¡${COUNT}-COMBO!!!", @@ -1178,7 +1182,10 @@ "playlistsText": "Listas de Juego", "pleaseRateText": "Si te gusta ${APP_NAME}, por favor tomate un momento\npara calificar o escribir una reseña. Esto proporcionará\ninformación útil y ayuda al soporte del futuro desarrollo del juego.\n\n¡gracias!\n-eric", "pleaseWaitText": "Por favor, espera...", - "pluginsDetectedText": "Nuevos plugin(s) detectados. Activar/configurarlos en los ajustes.", + "pluginClassLoadErrorText": "Error al cargar la clase del plugin '${PLUGIN}': ${ERROR}", + "pluginInitErrorText": "Error al iniciar el plugin '${PLUGIN}': ${ERROR}", + "pluginsDetectedText": "Nuevos complemento(s) detectados. Reinicie para activarlos, o configúrelos en la configuración", + "pluginsRemovedText": "${NUM} plugin(s) ya no se encuentran.", "pluginsText": "Plugins", "practiceText": "Práctica", "pressAnyButtonPlayAgainText": "Oprime cualquier botón jugar de nuevo...", @@ -1960,6 +1967,8 @@ "winsPlayerText": "¡${NAME} Gana!", "winsTeamText": "¡${NAME} Gana!!", "winsText": "¡${NAME} Gana!", + "workspaceSyncErrorText": "Error al sincronizar ${WORKSPACE}. Mira el registro para mas detalles.", + "workspaceSyncReuseText": "No se puede sincronizar ${WORKSPACE}. Reusando la versión sincronizada anterior.", "worldScoresUnavailableText": "Puntuaciones globales no disponibles.", "worldsBestScoresText": "Mejores puntuaciones Mundiales", "worldsBestTimesText": "Mejores tiempos Mundiales", diff --git a/dist/ba_data/data/languages/tamil.json b/dist/ba_data/data/languages/tamil.json index 241e76f..1dc3fd0 100644 --- a/dist/ba_data/data/languages/tamil.json +++ b/dist/ba_data/data/languages/tamil.json @@ -324,6 +324,7 @@ "achievementsRemainingText": "மீதமுள்ள சாதனைகள்:", "achievementsText": "சாதனைகள்", "achievementsUnavailableForOldSeasonsText": "மன்னிக்கவும், சாதனை விவரங்கள் பழைய பருவங்களுக்கு கிடைக்கவில்லை.", + "activatedText": "${THING} செயல்படுத்தப்பட்டது.", "addGameWindow": { "getMoreGamesText": "மேலும் விளையாட்டுகளைப் பெறுங்கள்...", "titleText": "விளையாட்டைச் சேர்" @@ -623,7 +624,9 @@ "epicDescriptionFilterText": "${DESCRIPTION} காவிய மெதுவான இயக்கத்தில்.", "epicNameFilterText": "காவியம் ${NAME}", "errorAccessDeniedText": "அணுகல் மறுக்கப்பட்டது", + "errorDeviceTimeIncorrectText": "உங்கள் சாதனத்தின் நேரம் ${HOURS} மணிநேரம் உள்ளது.\nஇதனால் பிரச்னைகள் ஏற்பட வாய்ப்புள்ளது.\nஉங்கள் நேரம் மற்றும் நேர மண்டல அமைப்புகளைச் சரிபார்க்கவும்.", "errorOutOfDiskSpaceText": "வட்டு இடத்திற்கு வெளியே", + "errorSecureConnectionFailText": "பாதுகாப்பான கிளவுட் இணைப்பை நிறுவ முடியவில்லை; பிணைய செயல்பாடு தோல்வியடையலாம்.", "errorText": "பிழை", "errorUnknownText": "அறியப்படாத பிழை", "exitGameText": "${APP_NAME} இலிருந்து வெளியேறவா?", @@ -786,7 +789,7 @@ "ticketPack4Text": "ஜம்போ டிக்கெட் பேக்", "ticketPack5Text": "மம்மத் டிக்கெட் பேக்", "ticketPack6Text": "அல்டிமேட் டிக்கெட் பேக்", - "ticketsFromASponsorText": "${COUNT} டிக்கெட்டுகளைப் பெறுங்கள்\nஒரு ஸ்பான்சரிடமிருந்து", + "ticketsFromASponsorText": "ஒரு விளம்பரத்தைப் பார்த்து\n${COUNT} டிக்கெட்டுகளை பெறுங்கள்", "ticketsText": "${COUNT} டிக்கெட்டுகள்", "titleText": "டிக்கெட்டுகளைப் பெறுங்கள்", "unavailableLinkAccountText": "மன்னிக்கவும், இந்த தளத்தில் கொள்முதல் கிடைக்கவில்லை.\nஒரு தீர்வாக, இந்தக் கணக்கை ஒரு கணக்குடன் இணைக்கலாம்\nமற்றொரு தளம் மற்றும் அங்கு கொள்முதல் செய்யுங்கள்.", @@ -797,6 +800,7 @@ "youHaveText": "உங்களிடம் ${COUNT} டிக்கெட்டுகள் உள்ளன" }, "googleMultiplayerDiscontinuedText": "மன்னிக்கவும், கூகுளின் மல்டிபிளேயர் சேவை இனி கிடைக்காது.\nநான் முடிந்தவரை விரைவாக மாற்றுவதற்கு வேலை செய்கிறேன்.\nஅதுவரை, வேறு இணைப்பு முறையை முயற்சிக்கவும்.\n-எரிக்", + "googlePlayPurchasesNotAvailableText": "Google Play வாங்குதல்கள் கிடைக்கவில்லை.\nஉங்கள் ஸ்டோர் பயன்பாட்டைப் புதுப்பிக்க வேண்டியிருக்கலாம்.", "googlePlayText": "கூகுள் பிளே", "graphicsSettingsWindow": { "alwaysText": "எப்போதும்", @@ -1043,7 +1047,7 @@ "noExternalStorageErrorText": "இந்தச் சாதனத்தில் வெளிப்புறச் சேமிப்பு இல்லை", "noGameCircleText": "பிழை: GameCircle இல் உள்நுழையவில்லை", "noScoresYetText": "இன்னும் மதிப்பெண்கள் இல்லை.", - "noThanksText": "இல்லை நன்றி", + "noThanksText": "இல்லை, நன்றி!", "noTournamentsInTestBuildText": "எச்சரிக்கை: இந்த சோதனை உருவாக்கத்தில் இருந்து போட்டியின் மதிப்பெண்கள் புறக்கணிக்கப்படும்.", "noValidMapsErrorText": "இந்த விளையாட்டு வகைக்கு சரியான வரைபடங்கள் இல்லை.", "notEnoughPlayersRemainingText": "போதுமான வீரர்கள் மீதமில்லை; வெளியேறி ஒரு புதிய விளையாட்டைத் தொடங்கவும்.", @@ -1108,7 +1112,10 @@ "playlistsText": "பிளேலிஸ்ட்கள்", "pleaseRateText": "நீங்கள் ${APP_NAME} ஐ அனுபவிக்கிறீர்கள் என்றால், தயவுசெய்து எடுப்பதைக் கவனியுங்கள்\nதருணம் மற்றும் மதிப்பிடுதல் அல்லது விமர்சனம் எழுதுதல். இது வழங்குகிறது\nபயனுள்ள கருத்து மற்றும் எதிர்கால வளர்ச்சிக்கு உதவுகிறது.\n\nநன்றி!\n-எரிக்", "pleaseWaitText": "தயவுசெய்து காத்திருங்கள்...", - "pluginsDetectedText": "புதிய செருகுநிரல் (கள்) கண்டறியப்பட்டது. அமைப்புகளில் அவற்றை இயக்கவும்/கட்டமைக்கவும்.", + "pluginClassLoadErrorText": "செருகுநிரல் வகுப்பை ஏற்றுவதில் பிழை '${PLUGIN}': ${ERROR}", + "pluginInitErrorText": "'${PLUGIN}' செருகுநிரலைத் தொடங்குவதில் பிழை: ${ERROR}", + "pluginsDetectedText": "புதிய செருகுநிரல்(கள்) கண்டறியப்பட்டது. அவற்றைச் செயல்படுத்த மீண்டும் தொடங்கவும் அல்லது அமைப்புகளில் உள்ளமைக்கவும்.", + "pluginsRemovedText": "${NUM} செருகுநிரல்(கள்) இனி காணப்படவில்லை.", "pluginsText": "செருகுநிரல்கள்", "practiceText": "பயிற்சி", "pressAnyButtonPlayAgainText": "மீண்டும் விளையாட எந்த பட்டனையும் அழுத்தவும்...", @@ -1304,7 +1311,7 @@ "iconsText": "சின்னங்கள்", "loadErrorText": "பக்கத்தை ஏற்ற முடியவில்லை.\nஉங்கள் இணைய இணைப்பைச் சரிபார்க்கவும்.", "loadingText": "ஏற்றுகிறது", - "mapsText": "Maps", + "mapsText": "வரைபடங்கள்", "miniGamesText": "மினிகேம்ஸ்", "oneTimeOnlyText": "(ஒரு முறை மட்டும்)", "purchaseAlreadyInProgressText": "இந்த பொருளை வாங்குவது ஏற்கனவே நடந்து கொண்டிருக்கிறது.", @@ -1361,11 +1368,11 @@ "tournamentStandingsText": "போட்டி நிலைகள்", "tournamentText": "போட்டி", "tournamentTimeExpiredText": "போட்டி நேரம் காலாவதியானது", - "tournamentsText": "Tournaments", + "tournamentsText": "போட்டிகள்", "translations": { "characterNames": { "Agent Johnson": "ஏஜெண்ட் ஜான்சன்", - "B-9000": "B-9000", + "B-9000": "பி-9000", "Bernard": "பர்னார்ட்", "Bones": "போன்ஸ்", "Butch": "பட்ச்", @@ -1526,7 +1533,7 @@ "Thai": "தாய்", "Turkish": "டர்கிஷ்", "Ukrainian": "உக்ரைனியன்", - "Venetian": "வெநெடியன்", + "Venetian": "வெனிசியன்", "Vietnamese": "வியெட்னமீஸ்" }, "leagueNames": { @@ -1536,7 +1543,7 @@ "Silver": "வெள்ளி" }, "mapsNames": { - "Big G": "Big G", + "Big G": "பிக் சி", "Bridgit": "பிரிட்சிட்", "Courtyard": "கோர்ட்யார்ட்", "Crag Castle": "கிரக் கோட்டை", @@ -1689,7 +1696,7 @@ }, "tips": { "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "ஒரு சரியான நேர ஓட்டம்-ஜம்பிங்-ஸ்பின்-பஞ்ச் ஒரே வெற்றியில் கொல்லலாம்\nமற்றும் உங்கள் நண்பர்களிடமிருந்து உங்களுக்கு வாழ்நாள் முழுவதும் மரியாதை கிடைக்கும்.", - "Always remember to floss.": "எப்போதும் floss செய்ய நினைவில் கொள்ளுங்கள்.", + "Always remember to floss.": "எப்போதும் ஃப்ளோஸ் செய்ய நினைவில் கொள்ளுங்கள்.", "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "உங்களுக்கும் உங்கள் நண்பர்களுக்கும் பிளேயர் சுயவிவரங்களை உருவாக்கவும்\nசீரற்றவற்றைப் பயன்படுத்துவதற்குப் பதிலாக உங்கள் விருப்பமான பெயர்கள் மற்றும் தோற்றங்கள்.", "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "சாபப் பெட்டிகள் உங்களை ஒரு டிக்கிங் டைம் பாம்டாக மாற்றும்.\nஒரே ஒரு ஹெல்த் பேக் சீக்கிரம் பிடிப்பதுதான்.", "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "அவற்றின் தோற்றம் இருந்தபோதிலும், அனைத்து கதாபாத்திரங்களின் திறன்களும் ஒரே மாதிரியானவை,\nஎனவே நீங்கள் மிகவும் நெருக்கமாக ஒத்திருப்பதைத் தேர்ந்தெடுக்கவும்.", @@ -1750,7 +1757,7 @@ "phrase08Text": "இப்போது அதிக வேகத்தைப் பெற குதித்து சுழலலாம்.", "phrase09Text": "ஆ, அது சிறந்தது.", "phrase10Text": "ஓடுவதும் உதவுகிறது.", - "phrase11Text": "இயக்க எந்த பொத்தானையும் அழுத்திப் பிடிக்கவும்.", + "phrase11Text": "இயக்க எந்த பட்டனையும் அழுத்திப் பிடிக்கவும்.", "phrase12Text": "கூடுதல் அற்புதமான குத்துகளுக்கு, ஓடவும் சுழலவும் முயற்சிக்கவும்.", "phrase13Text": "அச்சச்சோ; ${NAME} பற்றி மன்னிக்கவும்.", "phrase14Text": "கொடிகள் .. அல்லது ${NAME} போன்றவற்றை எடுத்து எறியலாம்.", @@ -1761,11 +1768,11 @@ "phrase19Text": "குதிப்பது உயரத்தை எறிய உதவுகிறது.", "phrase20Text": "உங்கள் குண்டுகளை இன்னும் நீண்ட தூரத்திற்கு \"சவுக்கடி\".", "phrase21Text": "உங்கள் குண்டுகளை டைமிங் செய்வது தந்திரமானதாக இருக்கலாம்.", - "phrase22Text": "Dang.", + "phrase22Text": "அச்சச்சோ.", "phrase23Text": "ஒன்றிரண்டு அல்லது இரண்டு நிமிடங்களுக்கு உருகி \"Cooking off\" முயற்சிக்கவும்.", "phrase24Text": "ஹூரே! நன்றாக போடப்பட்டது", "phrase25Text": "சரி, அது பற்றி தான்.", - "phrase26Text": "இப்போது அவர்களைப் போய் புலி!", + "phrase26Text": "இப்போது அவர்களைப் போய் புடி புலியே!", "phrase27Text": "உங்கள் பயிற்சியை நினைவில் கொள்ளுங்கள், நீங்கள் உயிருடன் திரும்பி வருவீர்கள்!", "phrase28Text": "...நன்று,இருக்கலாம்...", "phrase29Text": "நல்ல அதிர்ஷ்டம்!", @@ -1844,6 +1851,8 @@ "winsPlayerText": "${NAME} வெற்றி!", "winsTeamText": "${NAME} வெற்றி!", "winsText": "${NAME} வெற்றி!", + "workspaceSyncErrorText": "${WORKSPACE} ஐ ஒத்திசைப்பதில் பிழை. விவரங்களுக்கு பதிவைப் பார்க்கவும்.", + "workspaceSyncReuseText": "${WORKSPACE} ஐ ஒத்திசைக்க முடியவில்லை. முந்தைய ஒத்திசைக்கப்பட்ட பதிப்பை மீண்டும் பயன்படுத்துகிறது.", "worldScoresUnavailableText": "உலக ஸ்கோர் கிடைக்கவில்லை.", "worldsBestScoresText": "உலகின் சிறந்த மதிப்பெண்கள்", "worldsBestTimesText": "உலகின் சிறந்த நேரங்கள்", diff --git a/dist/ba_data/data/languages/turkish.json b/dist/ba_data/data/languages/turkish.json index 98961ee..4532347 100644 --- a/dist/ba_data/data/languages/turkish.json +++ b/dist/ba_data/data/languages/turkish.json @@ -216,7 +216,7 @@ }, "Rookie Onslaught Victory": { "description": "Tüm dalgaları kazan", - "descriptionComplete": "Tüm dalgalar kazanıldı", + "descriptionComplete": "Tüm dalgaları kazan", "descriptionFull": "${LEVEL} da tüm dalgaları kazan", "descriptionFullComplete": "${LEVEL} da tüm dalgalar kazanıldı", "name": "${LEVEL} Galibiyeti" @@ -226,7 +226,7 @@ "descriptionComplete": "2000 puan kazanıldı", "descriptionFull": "${LEVEL} da 2000 puan kazan", "descriptionFullComplete": "${LEVEL} da 2000 puan kazanıldı", - "name": "${LEVEL} Tanrısı" + "name": "${LEVEL} Tanrı" }, "Runaround Master": { "description": "500 puan kazan", @@ -237,7 +237,7 @@ }, "Runaround Wizard": { "description": "1000 puan kazan", - "descriptionComplete": "1000 puan kazanıldı", + "descriptionComplete": "1000 puan kazan", "descriptionFull": "${LEVEL} da 1000 puan kazan", "descriptionFullComplete": "${LEVEL} da 1000 puan kazanıldı", "name": "${LEVEL} Büyücüsü" @@ -249,7 +249,7 @@ }, "Stayin' Alive": { "description": "Hiç ölmeden Kazan", - "descriptionComplete": "Hiç ölmeden kazanıldı", + "descriptionComplete": "Hiç ölmeden kazan", "descriptionFull": "Hiç ölmeden ${LEVEL} da kazan", "descriptionFullComplete": "Hiç ölmeden ${LEVEL} da kazanıldı", "name": "Hayatta Kal" @@ -259,7 +259,7 @@ "descriptionComplete": "Tek yumruk ile %100 hasar verildi", "descriptionFull": "${LEVEL} da tek yumruk ile %100 hasar ver", "descriptionFullComplete": "${LEVEL} da tek yumruk ile %100 hasar verildi", - "name": "Super Mega Yumruk" + "name": "Süper Mega Yumruk" }, "Super Punch": { "description": "Tek yumruk ile %50 hasar ver", @@ -277,19 +277,19 @@ }, "Team Player": { "descriptionFull": "4+ oyuncu ile bir Takımlar oyunu başlat", - "descriptionFullComplete": "4+ oyuncu ile bir Takımlar oyunu başlatıldı", + "descriptionFullComplete": "4+ oyuncu ile bir Takım oyunu başlat", "name": "Takım Oyuncusu" }, "The Great Wall": { "description": "Bütün haylazları durdur", - "descriptionComplete": "Bütün haylazlar durduruldu", + "descriptionComplete": "Bütün haylazları durdur", "descriptionFull": "${LEVEL} da bütün haylazları durdur", "descriptionFullComplete": "${LEVEL} da bütün haylazlar durduruldu", "name": "Muhteşem Bariyer" }, "The Wall": { "description": "Bütün haylazları durdur", - "descriptionComplete": "Bütün haylazlar durduruldu", + "descriptionComplete": "Bütün haylazları durdur", "descriptionFull": "${LEVEL} da bütün haylazları durdur", "descriptionFullComplete": "${LEVEL} da bütün haylazlar durduruldu", "name": "Bariyer" @@ -320,12 +320,13 @@ "descriptionComplete": "Tüm dalgalar tamamlandı", "descriptionFull": "${LEVEL} da tüm dalgaları tamamla", "descriptionFullComplete": "${LEVEL} da tüm dalgalar tamamlandı", - "name": "${LEVEL} Galibiyeti" + "name": "${LEVEL} Galibiyet" } }, "achievementsRemainingText": "Kalan Başarılar;", "achievementsText": "Başarı", "achievementsUnavailableForOldSeasonsText": "Üzgünüz, başarı detayları eski sezonlar için kullanılamaz.", + "activatedText": "${THING} aktifleştirildi.", "addGameWindow": { "getMoreGamesText": "Daha Çok Oyun...", "titleText": "Oyun Ekle" @@ -353,14 +354,14 @@ "bombBoldText": "BOMBA", "bombText": "Bomba", "boostText": "Öne Geç", - "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} Uygulaması kendisini yapılandırdı", + "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} Uygulamayı kendisine yapılandır", "buttonText": "buton", "canWeDebugText": "Oyun otomatik olarak hataları, çökmeleri ve basit \nkullanım istatistiklerini geliştiriciye gondersin mi?\n\nBu veri içeriği kişisel bilgilerinizi kullanmaz\noyunun daha iyi çalışmasına yardımcı olur.", "cancelText": "İptal", "cantConfigureDeviceText": "Üzgünüz, ${DEVICE} ayarlanabilir değil.", "challengeEndedText": "Bu mücadele sona erdi.", "chatMuteText": "Konuşmayı sustur", - "chatMutedText": "Konuşma Susturuldu", + "chatMutedText": "Sohbet Susturuldu", "chatUnMuteText": "Konuşmayı aç", "choosingPlayerText": "", "completeThisLevelToProceedText": "İlerlemek için bu \nseviyeyi tamamlamalısınız!", @@ -438,7 +439,7 @@ "movementControlScaleText": "Hareket Kontrolü Ölçeği", "movementText": "Hareket", "resetText": "Sıfırla", - "swipeControlsHiddenText": "Sürme Simgelerini Gizle", + "swipeControlsHiddenText": "Joystick'i gizle", "swipeInfoText": "\"Sürme\" kontrol stilleri kullanışlıdır fakat\nkontrollere bakmadan oynamak daha kolaydır.", "swipeText": "sürme", "titleText": "Dokunmatikleri Yapılandır" @@ -624,7 +625,9 @@ "epicDescriptionFilterText": "${DESCRIPTION} epik ağırçekim.", "epicNameFilterText": "Epik ${NAME}", "errorAccessDeniedText": "erişim reddedildi", + "errorDeviceTimeIncorrectText": "Eyvah! Cihazın ile sunucu arasındaki zaman farkı ${HOURS} saat.\nBu bazı sorunlara yol açabilir.\nLütfen saat ve saat dilimi ayarlarını kontrol et.", "errorOutOfDiskSpaceText": "disk alanı doldu", + "errorSecureConnectionFailText": "Güvenli bulut bağlantısı kurulamadı; ağ işlevi başarısız olabilir.", "errorText": "Hata", "errorUnknownText": "bilinmeyen hata", "exitGameText": "Çık ${APP_NAME}?", @@ -787,7 +790,7 @@ "ticketPack4Text": "Dev Bilet Paketi", "ticketPack5Text": "Muazzam Bilet Paketi", "ticketPack6Text": "En Üst Düzey Bilet Paketi", - "ticketsFromASponsorText": "Bir sponsordan\n${COUNT} Bilet Al", + "ticketsFromASponsorText": "${COUNT} Bilet için\nbir reklam izle", "ticketsText": "${COUNT} Bilet", "titleText": "Bilet Al", "unavailableLinkAccountText": "Üzgünüz, Satın almalar bu patformda kullanılamaz.\nBu geçici oldugu gibi, bu hesabını diğer platformlardaki\nhesablara bağlayabilir oradan satın alma işlemi yapbilirsin.", @@ -798,6 +801,7 @@ "youHaveText": "${COUNT} biletin var" }, "googleMultiplayerDiscontinuedText": "Üzgünüz, Google'ın çok oyunculu servisi şu anda çalışmıyor.\nBir yer değişimi için olabildiğince hızlı çalışıyorum.\nO zamana kadar, lütfen başka bir bağlantı yöntemi deneyin.\n-Eric", + "googlePlayPurchasesNotAvailableText": "Google Play satın alma işlemleri mevcut değildir.\nMağaza uygulamanızı güncellemeniz gerekebilir.", "googlePlayText": "Google Play", "graphicsSettingsWindow": { "alwaysText": "Her Zaman", @@ -1108,7 +1112,10 @@ "playlistsText": "ÇalmaListesi", "pleaseRateText": "Eğer ${APP_NAME}'dan zevk aldıysan, lütfen oyunu değerlerdir\nve yorumunu yaz. Bu oyunun daha fazla geliştirilmesine\nyardımcı olur.\n\nteşekkürler!\n-eric", "pleaseWaitText": "Lütfen bekle...", - "pluginsDetectedText": "Yeni eklenti(ler) tespit edildi. Bunları ayarlardan etkinleştirin / yapılandırın.", + "pluginClassLoadErrorText": "'${PLUGIN}' eklenti sınıfı yüklenirken hata oluştu: ${ERROR}", + "pluginInitErrorText": "'${PLUGIN}' eklentisi başlatılırken hata oluştu: ${ERROR}", + "pluginsDetectedText": "Yeni eklentiler tespit edildi. Onları etkinleştirmek veya ayarlarda yapılandırmak için oyunu yeniden başlatın.", + "pluginsRemovedText": "${NUM} eklenti(ler) artık bulunmuyor.", "pluginsText": "Eklentiler", "practiceText": "Alıştırma", "pressAnyButtonPlayAgainText": "Tekrar oynamak için bir tuşa basın...", @@ -1844,6 +1851,8 @@ "winsPlayerText": "${NAME} Kazandı!", "winsTeamText": "${NAME} Kazandı!", "winsText": "${NAME} Kazandı!", + "workspaceSyncErrorText": "${WORKSPACE} eşitlemesinde hata oluştu. Detaylar için günlüğü inceleyin.", + "workspaceSyncReuseText": "${WORKSPACE} eşitlenemiyor. Önceden eşitlenmiş sürüm kullanılıyor.", "worldScoresUnavailableText": "Global Skorlar Mevcut Değil.", "worldsBestScoresText": "Global En-İyi Skorlar", "worldsBestTimesText": "Global En-İyi Süreler", diff --git a/dist/ba_data/data/languages/venetian.json b/dist/ba_data/data/languages/venetian.json index 30ea63e..2c942e2 100644 --- a/dist/ba_data/data/languages/venetian.json +++ b/dist/ba_data/data/languages/venetian.json @@ -5,27 +5,27 @@ "achievementProgressText": "Obietivi: ${COUNT} de ${TOTAL}", "campaignProgressText": "Progreso canpagna [Defìsiłe]: ${PROGRESS}", "changeOncePerSeason": "A te połi canbiar ’sto dato soło na volta par stajon.", - "changeOncePerSeasonError": "Par modifegar (${NUM} days) A te ghè da spetar ła stajon pròsema.", + "changeOncePerSeasonError": "Par canbiarlo te ghè da spetar ła pròsema stajon (${NUM} days).", "customName": "Nome parsonałizà", "linkAccountsEnterCodeText": "Insarisi còdaze", "linkAccountsGenerateCodeText": "Jènara còdaze", - "linkAccountsInfoText": "(sparpagna progresi infrà dispozitivi defarenti)", - "linkAccountsInstructionsNewText": "Par cołegar do account, jènara un còdaze inte'l primo\ndispozitivo e insarìseło inte’l segondo. I dati de’l\nsegondo account i vegnarà sparpagnài so tuti do i account\n(i dati de’l primo account i ndarà perdesti).\n\nA te połi cołegar fin a ${COUNT} account.\n\nINPORTANTE: cołega soło i account de to propiedà.\nSe A te cołeghi account de calche to amigo, A no sarè\npì boni de zugar online inte’l mèdemo momento.", + "linkAccountsInfoText": "(sparpagna progresi infrà dispozidivi defarenti)", + "linkAccountsInstructionsNewText": "Par cołegar do account, jènara un còdaze inte'l primo\ndispozidivo e insarìseło inte’l segondo. I dati de’l\nsegondo account i vegnarà sparpagnài so tuti do i account\n(i dati de’l primo account i ndarà perdesti).\n\nTe połi cołegar fin a ${COUNT} account.\n\nINPORTANTE: cołega soło i account de to propiedà.\nSe te cołeghi account de calche to amigo, no sarè\npì boni de zugar online inte’l mèdemo momento.", "linkAccountsText": "Cołega account", "linkedAccountsText": "Account cołegài:", "nameChangeConfirm": "Vutu canbiar el nome de’l to account co ${NAME}?", - "resetProgressConfirmNoAchievementsText": "A te si drio ełimenar i to progresi so ła modałidà\ncooparadiva e i to punteji łogałi (ma miga i to biłieti).\n’Sta asion no ła połe mìa èsar anułada. Vutu ndar vanti?", - "resetProgressConfirmText": "A te si drio ełimenar i to progresi so ła\nmodałidà cooparadiva, i to obietivi e i to punteji\nłogałi (ma miga i to biłieti). ’Sta asion\nno ła połe mìa èsar anułada. Vutu ndar vanti?", + "resetProgressConfirmNoAchievementsText": "Te si drio ełimenar i to progresi so ła modałidà\ncooparadiva e i to punteji łogałi (ma miga i to biłieti).\n’Sta asion no ła połe pì èsar anułada. Vutu ndar vanti?", + "resetProgressConfirmText": "Te si drio ełimenar i to progresi so ła\nmodałidà cooparadiva, i to obietivi e i to punteji\nłogałi (ma miga i to biłieti). ’Sta asion\nno ła połe pì èsar anułada. Vutu ndar vanti?", "resetProgressText": "Ełìmena progresi", "setAccountName": "Inposta un nome utente", "setAccountNameDesc": "Sełesiona el nome da vizuałizar so’l to account.\nA te połi doparar el nome da uno de i to account\ncołegài o crear un nome parsonałizà ma ùnivogo.", - "signInInfoText": "Conétate par tirar sù biłieti, batajar online e\nsparpagnar i to progresi infrà dispozitivi defarenti.", + "signInInfoText": "Conétate par tirar sù biłieti, batajar online e\nsparpagnar i to progresi infrà dispozidivi defarenti.", "signInText": "Conétate", - "signInWithDeviceInfoText": "(par 'sto dispozitivo A ze disponìbiłe un soło account automàtego)", - "signInWithDeviceText": "Conétate co l'account de’l dispozitivo", + "signInWithDeviceInfoText": "(par 'sto dispozidivo ze disponìbiłe un soło account automàtego)", + "signInWithDeviceText": "Conétate co l'account de’l dispozidivo", "signInWithGameCircleText": "Conétate co Game Circle", "signInWithGooglePlayText": "Conétate co Google Play", - "signInWithTestAccountInfoText": "(account de proa vecio: in fuduro dòpara i account de’l dispozitivo)", + "signInWithTestAccountInfoText": "(account de proa vecio: in fuduro dòpara i account de’l dispozidivo)", "signInWithTestAccountText": "Conétate co un account de proa", "signInWithV2InfoText": "(un account che fusiona so tute łe piataforme)", "signInWithV2Text": "Acedi co un account BombSquad", @@ -38,7 +38,7 @@ "unlinkAccountsText": "Descołega account", "v2LinkInstructionsText": "Acedi o dòpara 'sto link par crear un account.", "viaAccount": "(doparando ${NAME})", - "youAreSignedInAsText": "A te zugarè cofà:" + "youAreSignedInAsText": "Te zugarè cofà:" }, "achievementChallengesText": "Obietivi sfide", "achievementText": "Obietivo", @@ -323,14 +323,15 @@ }, "achievementsRemainingText": "Obietivi restanti:", "achievementsText": "Obietivi", - "achievementsUnavailableForOldSeasonsText": "Ne despiaze, i obietivi de łe stajon pasàe no i ze mìa disponìbiłe.", + "achievementsUnavailableForOldSeasonsText": "Ne despiaze, i obietivi de łe stajon pasàe no i ze pì disponìbiłi.", + "activatedText": "${THING} ativà.", "addGameWindow": { "getMoreGamesText": "Otien pì łevełi...", "titleText": "Zonta zugo" }, "allowText": "Parmeti", - "alreadySignedInText": "El to account el ze in dòparo inte n’antro\ndispozitivo: canbia account o sara sù el zugo\ninte cheł’altro to dispozitivo e proa danovo.", - "apiVersionErrorText": "A no ze mìa posìbiłe cargar el mòduło ${NAME}, el se refarise a ła varsion ${VERSION_USED}. A serve invese ła ${VERSION_REQUIRED}.", + "alreadySignedInText": "El to account el ze in dòparo inte n’antro\ndispozidivo: canbia account o sara sù el zugo\ninte cheł’altro to dispozidivo e proa danovo.", + "apiVersionErrorText": "Inposìbiłe cargar el mòduło ${NAME}, el se refarise a ła varsion ${VERSION_USED}. Serve invese ła ${VERSION_REQUIRED}.", "audioSettingsWindow": { "headRelativeVRAudioInfoText": "(Ativa \"Auto\" soło co A te tachi sù łe fonarołe par ła realtà virtuałe)", "headRelativeVRAudioText": "Àudio par fonarołe VR", @@ -354,19 +355,19 @@ "buttonText": "boton", "canWeDebugText": "Ghetu caro che BombSquad el reporte in automàtego bai,\nblochi e informasion baze só'l so dòparo a'l dezviłupador?\n\n'Sti dati no i contien miga informasion parsonałi ma i ło\njuta a far ndar ben el zugo sensa blochi o bai.", "cancelText": "Anuła", - "cantConfigureDeviceText": "Ne despiaze, ${DEVICE} no'l ze mìa configuràbiłe.", + "cantConfigureDeviceText": "Ne despiaze, ${DEVICE} no'l ze miga configuràbiłe.", "challengeEndedText": "'Sta sfida ła ze fenìa.", "chatMuteText": "Siłensia ciacołada", "chatMutedText": "Ciacołada siłensiada", "chatUnMuteText": "Reativa son", "choosingPlayerText": "", - "completeThisLevelToProceedText": "A te ghè da conpletar 'sto\nłeveło par ndar vanti!", + "completeThisLevelToProceedText": "Par ndar vanti te ghè\nda conpletar 'sto łeveło!", "completionBonusText": "Premio de concruzion", "configControllersWindow": { "configureControllersText": "Configura controładori", "configureKeyboard2Text": "Configura botonera P2", "configureKeyboardText": "Configura botonera", - "configureMobileText": "Dòpara dispozitivi cofà controładori", + "configureMobileText": "Dòpara dispozidivi cofà controładori", "configureTouchText": "Configura schermo tàtiłe", "ps3Text": "Controładori PS3", "titleText": "Controładori", @@ -374,7 +375,7 @@ "xbox360Text": "Controładori Xbox 360" }, "configGamepadSelectWindow": { - "androidNoteText": "Nota: ła conpatibiłidà par i controładori ła muda drio dispozitivo e varsion de Android.", + "androidNoteText": "Nota: ła conpatibiłidà par i controładori ła muda drio dispozidivo e varsion de Android.", "pressAnyButtonText": "Struca un boton calsìase de'l controłador\nche A te vołi configurar...", "titleText": "Configura controładori" }, @@ -418,13 +419,13 @@ "twoInOneSetupText": "Configurasion controłador 2 so 1", "uiOnlyDescriptionText": "(par evitar che 'sto controłador el posa zontarse inte na partìa)", "uiOnlyText": "Łìmida el dòparo de'l menù", - "unassignedButtonsRunText": "Tuti i botoni mìa defenìi i fà córar", - "unsetText": "", + "unassignedButtonsRunText": "Tuti i botoni miga defenìi i fà córar", + "unsetText": "", "vrReorientButtonText": "Boton de repozision VR" }, "configKeyboardWindow": { "configuringText": "Configurasion ${DEVICE}", - "keyboard2NoteText": "Nota: ła parte pì granda de łe botonere łe połe rejistrar\nsoło un fià de comandi a'l colpo. Donca, par zugar in du,\nA se ndarìa mejo doparar do botonere defarenti. Tien daconto\nche A te gavarè da defenir in tuti i cazi botoni unìvoghi\npar i tuti do i zugadori." + "keyboard2NoteText": "Nota: ła parte pì granda de łe botonere łe połe rejistrar\nsoło un fià de comandi a'l colpo. Donca, par zugar in du,\nse ndarìa mejo doparar do botonere defarenti. Tien daconto\nche te gavarè da defenir in tuti i cazi botoni unìvoghi\npar i tuti do i zugadori." }, "configTouchscreenWindow": { "actionControlScaleText": "Grandesa controło", @@ -445,13 +446,13 @@ "connectMobileDevicesWindow": { "amazonText": "Amazon Store", "appStoreText": "App Store", - "bestResultsText": "Par zugar co rezultài pì boni A te serve na rede wifi ràpida.\nA te połi redùzar el retardo (lag) de ła rede zugando visin a’l\ntrazmetidor de’l segnałe, desconetendo altri dispozitivi sensa\nfiło o conetendo l’ospitador de’l zugo co un cavo ethernet.", - "explanationText": "Par doparar un tełèfono o un tołeto cofà un controłador sensa fiło,\ninstàłaghe rento l’apl \"${REMOTE_APP_NAME}\". Par zugar a ${APP_NAME}\nA te połi conétar co’l wifi tuti i dispozitivi che A te vołi! A ze gratis!", + "bestResultsText": "Par zugar co rezultài pì boni te serve na rede wifi ràpida.\nTe połi redùzar el retardo (lag) de ła rede zugando visin a’l\ntrazmetidor de’l segnałe, desconetendo altri dispozidivi sensa\nfiło o conetendo l’ospitador de’l zugo co un cavo ethernet.", + "explanationText": "Par doparar un tełèfono o un tołeto cofà un controłador sensa fiło,\ninstàłaghe rento l’apl \"${REMOTE_APP_NAME}\". Par zugar a ${APP_NAME}\nte połi conétar co’l wifi tuti i dispozidivi che te vołi! Ze agratis!", "forAndroidText": "par Android:", "forIOSText": "par iOS:", "getItForText": "Descarga ${REMOTE_APP_NAME} par iOS so l'AppStore de\nApple o par Android so'l Store de Google Play o l'Amazon Store", "googlePlayText": "Google Play", - "titleText": "Doparar dispozitivi mòbiłi cofà controładori:" + "titleText": "Doparar dispozidivi mòbiłi cofà controładori:" }, "continuePurchaseText": "Vutu ndar vanti par ${PRICE}?", "continueText": "Sì!", @@ -466,7 +467,7 @@ "currentBestText": "El mejo par deso", "customText": "E in pì...", "entryFeeText": "Costo", - "forfeitConfirmText": "Vutu dalvero dàrgheła vinta?", + "forfeitConfirmText": "Vutu dabon dàrgheła vinta?", "forfeitNotAllowedYetText": "Deso A no te połi miga dàrgheła vinta suito.", "forfeitText": "Dàgheła vinta", "multipliersText": "Moltiplegadori", @@ -533,7 +534,7 @@ "stressTestRoundDurationText": "Durada turno", "stressTestTitleText": "Proa soto sforso", "titleText": "Prestasion e Proe soto sforso", - "totalReloadTimeText": "Tenpo totałe recargamento: ${TIME} (par i detaji varda el rejistro)" + "totalReloadTimeText": "Tenpo totałe recargamento: ${TIME} (par i detaji varda el rejistro eventi)" }, "defaultGameListNameText": "Łista de zugo \"${PLAYMODE}\" predefenìa", "defaultNewGameListNameText": "Ła me łista de zugo ${PLAYMODE}", @@ -623,7 +624,7 @@ "errorAccessDeniedText": "aceso refudà", "errorOutOfDiskSpaceText": "spasio so'l disco fenìo", "errorText": "Eror", - "errorUnknownText": "eror mìa conosesto", + "errorUnknownText": "eror miga conosesto", "exitGameText": "Vutu ndar fora da ${APP_NAME}?", "exportSuccessText": "'${NAME}' esportà.", "externalStorageText": "Memoria esterna", @@ -646,7 +647,7 @@ "fiveKillText": "BEN 5 SASINÀI!!!!", "flawlessWaveText": "Ondada parfeta!", "fourKillText": "EŁIMENÀI: 4!!!", - "friendScoresUnavailableText": "Punteji de i amighi mìa disponìbiłi.", + "friendScoresUnavailableText": "Punteji de i amighi miga disponìbiłi.", "gameCenterText": "GameCenter", "gameCircleText": "GameCircle", "gameLeadersText": "Clasìfega de'l łeveło ${COUNT}", @@ -668,14 +669,14 @@ }, "gamesToText": "${WINCOUNT} a ${LOSECOUNT}", "gatherWindow": { - "aboutDescriptionLocalMultiplayerExtraText": "Recòrdate: se A te ghè bastansa controładori, caun\ndispozitivo de un grupo el połe ospitar pì zugadori.", - "aboutDescriptionText": "Dòpara ’ste sesion par far sù un grupo.\n\nI grupi i te parmete de zugar partìe e tornèi\nco i to amighi infrà i vari dispozitivi.\n\nDòpara el boton ${PARTY} insima a drita par\nciacołar e interajir co’l to grupo.\n(so un controłador, struca ${BUTTON} co A te si inte un menù)", + "aboutDescriptionLocalMultiplayerExtraText": "Recòrdate: se te ghè bastansa controładori, tuti i\ndispozidivi de un grupo i połe ospitar pì zugadori.", + "aboutDescriptionText": "Dòpara 'ste sesion par far sù un grupo.\n\nI grupi i te parmete de zugar partìe e tornèi\nco i to amighi infrà i vari dispozidivi.\n\nDòpara el boton ${PARTY} insima a drita par\nciacołar e interajir co’l to grupo.\n(so un controłador, struca ${BUTTON} co te si inte un menù)", "aboutText": "Info", "addressFetchErrorText": "", "appInviteMessageText": "L'utente ${NAME} el te ga mandà ${COUNT} biłieti so ${APP_NAME}", "appInviteSendACodeText": "Màndaghe un còdaze", "appInviteTitleText": "Invido a proar ${APP_NAME}", - "bluetoothAndroidSupportText": "(el funsiona so tuti i dispozitivi Android co el bluetooth)", + "bluetoothAndroidSupportText": "(el funsiona so tuti i dispozidivi Android co el bluetooth)", "bluetoothDescriptionText": "Òspita/zóntate so un grupo co'l bluetooth:", "bluetoothHostText": "Òspita", "bluetoothJoinText": "Zóntate", @@ -708,8 +709,8 @@ "googlePlayText": "Google Play", "googlePlayVersionOnlyText": "(soło inte ła varsion Android / Google Play)", "hostPublicPartyDescriptionText": "Òspita un grupo pùblego", - "hostingUnavailableText": "Ospitamento mìa disponìbiłe", - "inDevelopmentWarningText": "Nota:\n\nEl zugo in rede ła ze na funsion nova e in dezviłupo.\nPar deso, A ze dalvero racomandà che tuti i\nzugadori i sie tuti so ła mèdema rede wifi.", + "hostingUnavailableText": "Ospitamento miga disponìbiłe", + "inDevelopmentWarningText": "Nota:\n\nEl zugo in rede ła ze na funsion nova e in dezviłupo.\nPar deso, ze dabon racomandà che tuti i\nzugadori i sipie tuti so ła mèdema rede wifi.", "internetText": "Internet", "inviteAFriendText": "I to amighi zełi sensa zugo? Invìdełi a\nproarlo e i resevarà ${COUNT} biłieti gratùidi.", "inviteFriendsText": "Invida amighi", @@ -734,7 +735,7 @@ "otherVersionsText": "(par altre varsion)", "partyCodeText": "Còdaze de'l grupo", "partyInviteAcceptText": "Và ben", - "partyInviteDeclineText": "Mìa deso", + "partyInviteDeclineText": "Miga deso", "partyInviteGooglePlayExtraText": "(varda el paneło 'Google Play' inte ła fenestra 'Crea grupo')", "partyInviteIgnoreText": "Ignora", "partyInviteText": "A te ze rivà un invido de ${NAME}\npar zontarte inte'l só grupo!", @@ -744,7 +745,7 @@ "partyStatusCheckingText": "Varìfega condision...", "partyStatusJoinableText": "deso el to grupo el połe èsar catà da internet", "partyStatusNoConnectionText": "A no ze miga posìbiłe conétarse a'l server", - "partyStatusNotJoinableText": "el to grupo no'l połe mìa èsar catà da internet", + "partyStatusNotJoinableText": "el to grupo no'l połe miga èsar catà da internet", "partyStatusNotPublicText": "el to grupo no'l ze miga pùblego", "pingText": "łatensa", "portText": "Porta", @@ -763,12 +764,12 @@ "startStopHostingMinutesText": "Te połi tacar o fermar de ospitar grupi łìbaramente par n'antri ${MINUTES} menuti.", "stopHostingText": "Ferma ospitamento", "titleText": "Crea grupo", - "wifiDirectDescriptionBottomText": "Se tuti i dispozitivi i gà ła sesion 'Wifi direto', i sarà boni de dopararlo par\ncatarse e conétarse infrà de łori. Na volta che tuti i dispozitivi i sarà conetesti,\nA te podarè crear grupi inte ła sesion 'Rede łogałe', cofà ła fuse na rede wifi normałe.\n\nPar rezultài pì boni, mejo che l’ospitador de’l wifi direto el sipie l’òspite anca de’l grupo so ${APP_NAME}.", - "wifiDirectDescriptionTopText": "El Wifi direto el połe èsar doparà par conétar diretamente dispozitivi Android\nsensa pasar par ła rede wifi. El funsiona mejo so varsion Android 4.2 o pì resenti.\n\nPar dopararlo, verzi łe inpostasion wifi e controła ła funsion 'Wifi direto'.", + "wifiDirectDescriptionBottomText": "Se tuti i dispozidivi i gà ła sesion 'Wifi direto', i sarà boni de dopararlo par\ncatarse e conétarse infrà de łori. Na volta che tuti i dispozidivi i sarà conetesti,\nte podarè crear grupi inte ła sesion 'Rede łogałe', cofà ła fuse na rede wifi normałe.\n\nPar rezultài pì boni, mejo che l’ospitador de’l wifi direto el sipie l’òspite anca de’l grupo so ${APP_NAME}.", + "wifiDirectDescriptionTopText": "El Wifi direto el połe èsar doparà par conétar diretamente dispozidivi Android\nsensa pasar par ła rede wifi. El funsiona mejo so varsion Android 4.2 o pì resenti.\n\nPar dopararlo, verzi łe inpostasion wifi e controła ła funsion 'Wifi direto'.", "wifiDirectOpenWiFiSettingsText": "Verzi inpostasion wifi", "wifiDirectText": "Wifi direto", "worksBetweenAllPlatformsText": "(el funsiona intrà tute łe piataforme)", - "worksWithGooglePlayDevicesText": "(el funsiona co tuti i dispozitivi co ła varsion de’l zugo de Google Play par Android)", + "worksWithGooglePlayDevicesText": "(el funsiona co tuti i dispozidivi co ła varsion de’l zugo de Google Play par Android)", "youHaveBeenSentAPromoCodeText": "A te ze stà mandà un còdaze promosionałe de ${APP_NAME}:" }, "getTicketsWindow": { @@ -788,8 +789,8 @@ "ticketsText": "${COUNT} biłieti", "titleText": "Otien biłieti", "unavailableLinkAccountText": "Ne despiaze, A no se połe miga cronpar so 'sta piataforma.\nVołendo, cofà sołusion, A te połi cołegar 'sto account co\nuno inte n'antra piataforma e cronpar calcosa da łà.", - "unavailableTemporarilyText": "'Sta funsion no ła ze mìa disponìbiłe par deso: proa danovo pì tardi.", - "unavailableText": "Ne despiaze, 'sta funsion no ła ze mìa disponìbiłe.", + "unavailableTemporarilyText": "'Sta funsion no ła ze miga disponìbiłe par deso: proa danovo pì tardi.", + "unavailableText": "Ne despiaze, 'sta funsion no ła ze miga disponìbiłe.", "versionTooOldText": "Ne despiaze, 'sta varsion ła ze masa vecia: ajorna el zugo co cheła nova.", "youHaveShortText": "A te ghè ${COUNT}", "youHaveText": "A te ghè ${COUNT} biłieti" @@ -817,13 +818,13 @@ "helpWindow": { "bombInfoText": "- Bonbe -\nPì forti de i crogni, ma łe połe\nfarte małe anca a ti. Dopàrełe\nben tràndoghełe doso a i nemighi\nprima che łe salte par aria.", "canHelpText": "${APP_NAME} el połe jutarte!", - "controllersInfoText": "A te połi zugar a ${APP_NAME} co i amighi co na rede o, se gavì\ncontroładori che basta, połì zugar tuti insenbre so el mèdemo\ndispozitivo: ${APP_NAME} el ghin suporta racuanti. Połì senpre doparar\ni tełèfoni cofà controładori co l’apl gratùida '${REMOTE_APP_NAME}'.\nPar info in pì varda so Inpostasion > Controładori.", - "controllersInfoTextRemoteOnly": "Te połi zugar a ${APP_NAME} co i to amighi doparando na rede,\no zugar tuti so'l mèdemo dispozitivo doparando i tełèfoni cofà\ncontroładori co l'apl gratùida '${REMOTE_APP_NAME}'.", + "controllersInfoText": "Te połi zugar a ${APP_NAME} co i amighi co na rede o, se gavì\ncontroładori che basta, połì zugar tuti insenbre so el mèdemo\ndispozidivo: ${APP_NAME} el ghin suporta racuanti. Połì senpre doparar\ni tełèfoni cofà controładori co l’apl gratùida '${REMOTE_APP_NAME}'.\nPar info in pì varda so Inpostasion > Controładori.", + "controllersInfoTextRemoteOnly": "Te połi zugar a ${APP_NAME} co i to amighi doparando na rede,\no zugar tuti so'l mèdemo dispozidivo doparando i tełèfoni cofà\ncontroładori co l'apl gratùida '${REMOTE_APP_NAME}'.", "controllersText": "Controładori", "controlsSubtitleText": "El to amighévołe parsonajo de ${APP_NAME} el gà un fià de funsion de baze:", "controlsText": "Controłi", "devicesInfoText": "Ła varsion VR de ${APP_NAME} ła połe èsar zugada so na rede anca\nco ła varsion normałe de l'app, donca tira fora tełèfoni, tołeti\ne computers e taca a zugar! Podarìa èsar ùtiłe conétar na varsion\nnormałe de'l zugo a cheła VR anca soło par parmétarghe a łe parsone\nde poder ndarghe drio a'l zugo da fora.", - "devicesText": "Dispozitivi", + "devicesText": "Dispozidivi", "friendsGoodText": "A ze senpre bona roba vèrghene. Se se ła gode de pì co pì zugadori\ne ${APP_NAME} el ghin suporta fin a 8, e 'sta roba ła ne mena a:", "friendsText": "Amighi", "jumpInfoText": "- Salto -\nSalta par traversar buzi cełi,\npar trar robe pì alte, o par\nfar védar tuta ła to ałegresa.", @@ -858,18 +859,18 @@ }, "holdAnyButtonText": "", "holdAnyKeyText": "", - "hostIsNavigatingMenusText": "- ${HOST} l'è drio navegar intrà i menù a zbregabałon! -", + "hostIsNavigatingMenusText": "- ${HOST} ze drio navegar intrà i menù a zbregabałon! -", "importPlaylistCodeInstructionsText": "Dòpara 'sto còdaze par inportar 'sta łista de zugo ndove che te vołi:", "importPlaylistSuccessText": "Łista de zugo '${NAME}' a ${TYPE} inportada", "importText": "Inporta", "importingText": "Inportasion...", "inGameClippedNameText": "rento el zugo:\n\"${NAME}\"", - "installDiskSpaceErrorText": "EROR: A no ze mìa posìbiłe fenir l’instałasion.\nEl to dispozitivo el podarìa èsar sensa spasio.\nŁìbara un fià de memoria e proa danovo.", + "installDiskSpaceErrorText": "EROR: inposìbiłe fenir l’instałasion.\nEl to dispozidivo el podarìa èsar sensa spasio.\nŁìbara un fià de memoria e proa danovo.", "internal": { "arrowsToExitListText": "struca ${LEFT} o ${RIGHT} par ndar fora da ła serie", "buttonText": "boton", "cantKickHostError": "A no te połi miga parar vìa l'ospitador.", - "chatBlockedText": "${NAME} l'è stà tajà fora da ła chat par ${TIME} segondi.", + "chatBlockedText": "${NAME} ze stà tajà fora da ła chat par ${TIME} segondi.", "connectedToGameText": "A te te si zontà so '${NAME}'", "connectedToPartyText": "A te te si zontà so'l grupo de ${NAME}!", "connectingToPartyText": "Conesion...", @@ -890,10 +891,10 @@ "corruptFileText": "A ze stà catài fora file coronpesti. Proa reinstałar el zugo o manda na mail a: ${EMAIL}", "errorPlayingMusicText": "Eror de reprodusion muzegałe: ${MUSIC}", "errorResettingAchievementsText": "A no ze miga posìbiłe reinpostar i obietivi in łinea. Proa danovo pì tardi.", - "hasMenuControlText": "${NAME} l'à el controło de'l menù", + "hasMenuControlText": "${NAME} gà el controło de'l menù", "incompatibleNewerVersionHostText": "L'ospitador el gà na varsion de'l zugo pì resente.\nAjórneło anca ti a ła varsion ùltema e proa danovo.", "incompatibleVersionHostText": "L'ospitador el gà na varsion de'l zugo defarente.\nSegùreve de ver tuti do l'apl ajornada e provè danovo.", - "incompatibleVersionPlayerText": "${NAME} l'à na varsion de'l zugo defarente.\nSegùreve de ver tuti do l'apl ajornada e provè danovo.", + "incompatibleVersionPlayerText": "${NAME} gà na varsion de'l zugo defarente.\nSegùreve de ver tuti do l'apl ajornada e provè danovo.", "invalidAddressErrorText": "Eror: ndariso miga vàłido.", "invalidNameErrorText": "Eror: nome miga vàłido.", "invalidPortErrorText": "Eror: porta miga vàłida.", @@ -901,20 +902,20 @@ "invitationsSentText": "Mandài ${COUNT} invidi.", "joinedPartyInstructionsText": "Calchedun el se gà zontà inte'l to grupo.\nVà so \"Zuga\" par tacar na partìa.", "keyboardText": "Botonera", - "kickIdlePlayersKickedText": "A ze stà parà vìa ${NAME} par masa sonera.", - "kickIdlePlayersWarning1Text": "Se ła só sonera ła sèvita, ${NAME} l'vegnarà parà vìa tenpo ${COUNT} segondi.", + "kickIdlePlayersKickedText": "Ze stà parà fora ${NAME} par masa sonera.", + "kickIdlePlayersWarning1Text": "Se ła só sonera ła sèvita, ${NAME} vegnarà parà fora tenpo ${COUNT} segondi.", "kickIdlePlayersWarning2Text": "(A te połi dezativarlo so Inpostasion > Avansàe)", "leftGameText": "A te si ndà fora da '${NAME}'.", "leftPartyText": "A te si ndà fora da'l grupo de ${NAME}.", "noMusicFilesInFolderText": "Ła carteła no ła gà rento gnaun file muzegałe.", "playerJoinedPartyText": "${NAME} l'se gà zontà inte'l grupo!", - "playerLeftPartyText": "${NAME} l'è ndà fora da'l grupo!", + "playerLeftPartyText": "${NAME} gà mołà el grupo!", "rejectingInviteAlreadyInPartyText": "Invido refudà (dezà inte un grupo).", "serverRestartingText": "El server el ze drio retacarse. Reconétate infrà na scianta...", "serverShuttingDownText": "El server el ze drio stuarse...", "signInErrorText": "Eror in entrada.", - "signInNoConnectionText": "A no ze mìa posìbiłe ndar rento. (gnauna conesion a internet?)", - "telnetAccessDeniedText": "EROR: l'utente no'l gà mìa parmeso l'aceso co telnet.", + "signInNoConnectionText": "Inposìbiłe acédar. (sensa conesion internet?)", + "telnetAccessDeniedText": "EROR: l'utente no'l gà miga parmeso l'aceso co telnet.", "timeOutText": "(tocarà a ti tenpo ${TIME} segondi)", "touchScreenJoinWarningText": "A te te si zontà co'l touchscreen.\nSe ła ze stà na capeła, struca 'Menù > Moła łeveło'.", "touchScreenText": "TouchScreen", @@ -931,9 +932,9 @@ "keyboardChangeInstructionsText": "Struca spasio do 'olte par mudar botonera.", "keyboardNoOthersAvailableText": "A no ghe ze miga altre botonere disponìbiłi.", "keyboardSwitchText": "Pasajo a ła botonera \"${NAME}\".", - "kickOccurredText": "${NAME} l'è stà parà vìa.", + "kickOccurredText": "${NAME} ze stà parà fora.", "kickQuestionText": "Vutu parar vìa ${NAME}?", - "kickText": "Para vìa", + "kickText": "Para fora", "kickVoteCantKickAdminsText": "I aministradori no i połe mìa èsar parài vìa.", "kickVoteCantKickSelfText": "A no te połi mìa pararte vìa da soło.", "kickVoteFailedNotEnoughVotersText": "A ghe ze masa pochi zugadori par na votasion.", @@ -996,8 +997,8 @@ "howToPlayText": "Come zugar", "justPlayerText": "(Soło par ${NAME})", "leaveGameText": "Moła łeveło", - "leavePartyConfirmText": "Vutu dalvero ndar fora da'l grupo?", - "leavePartyText": "Va fora da'l grupo", + "leavePartyConfirmText": "Vutu dabon ndar fora da'l grupo?", + "leavePartyText": "Moła grupo", "quitText": "Sortisi", "resumeText": "Continua", "settingsText": "Inpostasion" @@ -1020,9 +1021,9 @@ "multiKillText": "CAENA DE ${COUNT} FATI FORA!!!!!", "multiPlayerCountText": "${COUNT} zugadori", "mustInviteFriendsText": "Nota: A te ghè da invidar i amighi\nso'l paneło \"${GATHER}\" o picar pì\ncontroładori par zugar in multizugador.", - "nameBetrayedText": "${NAME} l'à tradìo ${VICTIM}.", - "nameDiedText": "${NAME} l'è crepà.", - "nameKilledText": "${NAME} l'à copà ${VICTIM}.", + "nameBetrayedText": "${NAME} gà tradìo ${VICTIM}.", + "nameDiedText": "${NAME} ze crepà.", + "nameKilledText": "${NAME} gà copà ${VICTIM}.", "nameNotEmptyText": "El nome no'l połe miga star vodo!", "nameScoresText": "Un ponto par ${NAME}!", "nameSuicideKidFriendlyText": "${NAME} par zbałio L ze crepà.", @@ -1037,8 +1038,8 @@ "nextLevelText": "Łeveło seguente", "noAchievementsRemainingText": "- gnaun", "noContinuesText": "(sensa continui)", - "noExternalStorageErrorText": "Inte ’sto dispozitivo A no ze stà catada gnauna memoria esterna", - "noGameCircleText": "Eror: A no te si miga conetesto co GameCircle", + "noExternalStorageErrorText": "So ’sto dispozidivo no ze stà catada gnauna memoria esterna", + "noGameCircleText": "Eror: no te si miga conetesto co GameCircle", "noScoresYetText": "Gnancora gnaun puntejo.", "noThanksText": "Nò, grasie", "noTournamentsInTestBuildText": "AVERTENSA: i punteji de'l tornèo de 'sta varsion de proa i vegnarà ignorài.", @@ -1087,7 +1088,7 @@ "playerCountAbbreviatedText": "${COUNT}z", "playerDelayedJoinText": "Co tacarà el turno che'l vien A se zontarà anca ${PLAYER}.", "playerInfoText": "Informasion zugador", - "playerLeftText": "${PLAYER} l'à mołà el łeveło.", + "playerLeftText": "${PLAYER} gà mołà el łeveło.", "playerLimitReachedText": "Nùmaro màsemo de ${COUNT} zugadori: A no połe zontarse pì nesun.", "playerProfilesWindow": { "cantDeleteAccountProfileText": "A no te połi miga ełimenar el profiło prinsipałe de'l to account.", @@ -1105,7 +1106,10 @@ "playlistsText": "Łiste de zugo", "pleaseRateText": "Se ${APP_NAME} el ze drio piazerte, tote un àtemo par\nłasarghe zó na vałudasion o scrìvarghe zó un comento. 'Ste\nopinion łe tornarà còmode par dezviłupi fuduri de'l zugo.\n\ngrasie!\n-eric", "pleaseWaitText": "Speta n'àtemo...", - "pluginsDetectedText": "Estension nove rełevàe. Atìvełe/configùrełe inte łe inpostasion.", + "pluginClassLoadErrorText": "Eror de cargamento de ła clase de estension '${PLUGIN}': ${ERROR}", + "pluginInitErrorText": "Eror de inisiałizasion de l'estension '${PLUGIN}': ${ERROR}", + "pluginsDetectedText": "Estension nove rełevàe. Retaca el zugo par ativarle, o configùrełe so łe inpostasion.", + "pluginsRemovedText": "Estension miga catàe: ${NUM}", "pluginsText": "Estension", "practiceText": "Pràtega", "pressAnyButtonPlayAgainText": "Struca un boton calsìase par zugar danovo...", @@ -1125,7 +1129,7 @@ }, "promoSubmitErrorText": "Eror de trazmision de'l còdaze: controła ła to conesion internet", "ps3ControllersWindow": { - "macInstructionsText": "Stua ła corente so'l dadrìo de ła to PS3, segùrate che'l\nbluetooth de'l to Mac el sipie ativà, daspò coneti el to controłador\na'l to Mac co un cavo USB par cubiarli. Da deso in vanti A te\npołi doparar el boton \"cao\" de'l controłador par conétarlo co'l to\nMac in modałidà USB (co'l fiło) o bluetooth (sensa fiło).\n\nPar el cubiamento calche Mac el podarìa dimandarte un còdaze.\nSe càpida, varda ła demostrasion seguente o serca juto so Google.\n\n\n\n\nI controładori PS3 conetesti sensa fiło i gavarìa da védarse inte ła łista\nde'l dispozitivo so Prefarense de sistema > Bluetooth. Co te vorè\ndopararli danovo so ła to PS3, te podarisi ver da cavarli vìa da 'sta łista.\n\nSegùrate anca de desconétarli da'l bluetooth co A no te łi\ndòpari o łe só batarìe łe sevitarà a sconsumarse.\n\nEl bluetooth el gavarìa da suportar fin a 7 dispozitivi conetesti,\nanca se ła capasidà ła podarìa variar.", + "macInstructionsText": "Stua ła corente so'l dadrìo de ła to PS3, segùrate che'l\nbluetooth de'l to Mac el sipie ativà, daspò coneti el to controłador\na'l to Mac co un cavo USB par cubiarli. Da deso in vanti te\npołi doparar el boton \"cao\" de'l controłador par conétarlo co'l to\nMac in modałidà USB (co'l fiło) o bluetooth (sensa fiło).\n\nPar el cubiamento calche Mac el podarìa dimandarte un còdaze.\nSe càpida, varda ła demostrasion seguente o serca juto so Google.\n\n\n\n\nI controładori PS3 conetesti sensa fiło i gavarìa da védarse inte ła łista\nde'l dispozidivo so Prefarense de sistema > Bluetooth. Co te vorè\ndopararli danovo so ła to PS3, te podarisi ver da cavarli vìa da 'sta łista.\n\nSegùrate anca de desconétarli da'l bluetooth co no te łi\ndòpari o łe só batarìe łe sevitarà a sconsumarse.\n\nEl bluetooth el gavarìa da suportar fin a 7 dispozidivi conetesti,\nanca se ła capasidà ła podarìa variar.", "ouyaInstructionsText": "Par doparar un controłador PS3 co ła to OUYA, conéteło co un cavo USB par\ncubiarlo. 'Sta asion ła podarìa desconétar cheł'altri to controładori, donca\nA podarìa servirte retacar ła to OUYA e destacar el cavo USB.\n\nDa deso in vanti A te dovarisi èsar bon de doparar el boton \"cao\" de'l\ncontrołador par conétarlo sensa fiło. Co A te ghè fenìo de zugar, tien\nstrucà el boton \"cao\" par 10 segondi par stuar el controłador, el\npodarìa restar inpisà e stracar ła batarìa.", "pairingTutorialText": "demostrasion video pa'l cubiamento", "titleText": "Doparar un controłador PS3 co ${APP_NAME}:" @@ -1153,7 +1157,7 @@ "cant_resolve_host": "A no ze mìa posìbiłe catar fora l'ospitador.", "capturing": "Drio spetar i zugadori…", "connected": "Conetesto.", - "description": "Dòpara el to tełèfono o tołeto cofà controłador par BombSquad.\nA połe conétarse so un schermo ùgnoło fin a 8 dispozitivi insenbre, par un feston bueło multizugador!", + "description": "Dòpara el to tełèfono o tołeto cofà controłador par BombSquad.\nPołe conétarse so un schermo ùgnoło fin a 8 dispozidivi insenbre, par un feston bueło multizugador!", "disconnected": "Desconetesto da'l server.", "dpad_fixed": "fiso", "dpad_floating": "mòbiłe", @@ -1206,9 +1210,9 @@ }, "scoreWasText": "(prima ${COUNT})", "selectText": "Sełesiona", - "seriesWinLine1PlayerText": "L'À VINTO ŁA", - "seriesWinLine1TeamText": "L'À VINTO ŁA", - "seriesWinLine1Text": "L'À VINTO ŁA", + "seriesWinLine1PlayerText": "GÀ VINTO ŁA", + "seriesWinLine1TeamText": "GÀ VINTO ŁA", + "seriesWinLine1Text": "GÀ VINTO ŁA", "seriesWinLine2Text": "DESFIDA!", "settingsWindow": { "accountText": "Account", @@ -1231,7 +1235,7 @@ "enterPromoCodeText": "Insarisi còdaze", "forTestingText": "Nota: vałori vàłidi soło par proe. Sortendo da l'apl łi vegnarà perdesti.", "helpTranslateText": "Łe tradusion de ${APP_NAME} łe ze curàe da vołontari.\nSe A te vol darghe na ociada a cheła veneta, struca so'l boton\ncuà soto. Curada da Còdaze Veneto: codazeveneto@gmail.com", - "kickIdlePlayersText": "Para vìa zugadori in sonera", + "kickIdlePlayersText": "Para fora zugadori in sonera", "kidFriendlyModeText": "Modałidà bocia (viołensa reduzesta, evc)", "languageText": "Łengua", "moddingGuideText": "Guida par modifegasion", @@ -1317,7 +1321,7 @@ "winterSpecialText": "Spesiałe inverno", "youOwnThisText": "- dezà tuo -" }, - "storeDescriptionText": "Feston bueło a 8 zugadori!\n\nFà saltar par aria i to amighi (o el computer) inte un tornèo de minizughi cofà 'Brinca ła bandiera', 'Scravaso de stełe' o 'Scontri mortałi' in movensa camoma!\n\nFin a 8 parsone łe sarà bone de batajar insenbre, grasie a comandi senpi e a ła conpatibiłidà co tanti controładori. Co l'apl gratùida 'BombSquad Remote' A te połi parfin doparar i to dispozitivi mòbiłi cofà controładori!\n\nE deso... tira fora łe bonbe!\n\nPar informasion in pì daghe na ociada so www.froemling.net/bombsquad.", + "storeDescriptionText": "Feston bueło a 8 zugadori!\n\nFà saltar par aria i to amighi (o el computer) inte un tornèo de minizughi cofà 'Brinca ła bandiera', 'Scravaso de stełe' o 'Scontri mortałi' in movensa camoma!\n\nFin a 8 parsone łe sarà bone de batajar insenbre, grasie a comandi senpi e a ła conpatibiłidà co tanti controładori. Co l'apl gratùida 'BombSquad Remote' te połi parfin doparar i to dispozidivi mòbiłi cofà controładori!\n\nE deso... tira fora łe bonbe!\n\nPar informasion in pì daghe na ociada so www.froemling.net/bombsquad.", "storeDescriptions": { "blowUpYourFriendsText": "Fà sciopar par aria i to amighi.", "competeInMiniGamesText": "Zuga co tanti minizughi: da corse a zvołi.", @@ -1572,7 +1576,7 @@ "An error has occurred; please contact support. (${ERROR})": "A se gà verifegà un eror: contata l'asistensa. (${ERROR})", "An error has occurred; please contact support@froemling.net.": "A se gà verifegà un eror: contata support@froemling.net.", "An error has occurred; please try again later.": "A se gà verifegà un eror: proa danovo pì tardi.", - "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Vutu dalvero cołegar 'sti profiłi?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\n'Sta asion no ła połe mìa èsar anułada!", + "Are you sure you want to link these accounts?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\nThis cannot be undone!": "Vutu dabon cołegar 'sti profiłi?\n\n${ACCOUNT1}\n${ACCOUNT2}\n\n'Sta asion no ła połe pì èsar anułada!", "BombSquad Pro unlocked!": "BombSquad Pro dezblocà!", "Can't link 2 accounts of this type.": "A no se połe mìa cołegar 2 profiłi de 'sto tipo.", "Can't link 2 diamond league accounts.": "A no se połe mìa cołegar 2 profiłi de ła łega de damante.", @@ -1588,7 +1592,7 @@ "Invalid tournament entry; score will be ignored.": "Entrada inte'l tornèo mìa vàłida: el puntejo el vegnarà ignorà.", "Item unlocked!": "Ojeto dezblocà!", "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "COŁEGAMENTO REFUDÀ. ${ACCOUNT} el contien dati\ninportanti che i ndarà PERDESTI.\nSe te vołi A te połi cołegarli in òrdane raverso\n(e pèrdar a'l só posto i dati de 'STO account cuà).", - "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Vutu dalvero cołegar l'account ${ACCOUNT} a 'sto account?\nTuti i dati so ${ACCOUNT} i ndarà perdesti.\n'Sta asion no ła połe mìa èsar anułada: situ seguro?", + "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Vutu dabon cołegar l'account ${ACCOUNT} a 'sto account?\nTuti i dati so ${ACCOUNT} i ndarà perdesti.\n'Sta asion no ła połe pì èsar anułada: vutu ndar vanti?", "Max number of playlists reached.": "Nùmaro màsemo de łiste de scolto pasà.", "Max number of profiles reached.": "Nùmaro màsemo de profiłi pasà.", "Maximum friend code rewards reached.": "Brincà el nùmaro màsemo de premi da'l còdaze amigo.", @@ -1610,11 +1614,11 @@ "This code cannot be used on the account that created it.": "'Sto còdaze no'l połe mìa èsar doparà inte l'account che'l ło gà creà.", "This is currently unavailable; please try again later.": "Par deso, funsion miga disponìbiłe. Proa danovo pì tardi.", "This requires version ${VERSION} or newer.": "A ghe serve ła varsion ${VERSION} o una pì nova.", - "Tournaments disabled due to rooted device.": "Tornèi dezativài parvìa de'l dispozitivo co'l root.", + "Tournaments disabled due to rooted device.": "Tornèi dezativài parvìa de'l dispozidivo co'l root.", "Tournaments require ${VERSION} or newer": "Par i tornèi A serve ła varsion ${VERSION} o una pì resente", "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Vutu descołegar l'account ${ACCOUNT} da 'sto account?\nTuti i dati de ${ACCOUNT} i vegnarà ełimenài.\n(obietivi a parte in calche cazo)", "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "AVERTENSA: el to account el ze stà segnałà par el dòparo de truchi.\nI zugaduri catài a doparar truchi i vegnarà blocài. Zuga da gałantomo.", - "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Ghetu caro cołegar l'account de'l to dispozitivo co 'sto cuà?\n\nL'account de'l to dispozitivo el ze ${ACCOUNT1}\n'Sto account el ze ${ACCOUNT2}\n\n'Sta oparasion ła te parmetarà de mantegner i to progresi ezistenti.\nAvertensa: 'sta asion no ła połe mìa èsar anułada!", + "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Ghetu caro cołegar l'account de'l to dispozidivo co 'sto cuà?\n\nL'account de'l to dispozidivo el ze ${ACCOUNT1}\n'Sto account el ze ${ACCOUNT2}\n\n'Sta oparasion ła te parmetarà de mantegner i to progresi ezistenti.\nAvertensa: 'sta asion no ła połe pì èsar anułada!", "You already own this!": "Dezà cronpà!", "You can join in ${COUNT} seconds.": "A te połi zontarte tenpo ${COUNT} segondi.", "You don't have enough tickets for this!": "A no te ghè miga biłieti che basta par cronparlo!", @@ -1672,7 +1676,7 @@ "Time Limit": "Łìmide de tenpo" }, "statements": { - "${TEAM} is disqualified because ${PLAYER} left": "Ła scuadra ${TEAM} ła ze scuałifegada parché ${PLAYER} l'à mołà.", + "${TEAM} is disqualified because ${PLAYER} left": "Ła scuadra ${TEAM} ła ze scuałifegada parché ${PLAYER} gà mołà.", "Killing ${NAME} for skipping part of the track!": "Copà ${NAME} par ver saltà un toco de'l parcorso!", "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Avertensa par ${NAME}: el turbo / spam co i botoni el te gà fato trar fora." }, @@ -1689,19 +1693,19 @@ "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Łe case małedision łe te transforma inte na bonba a tenpo.\nA te połi curarte soło tołendo in presa un pacheto sałude.", "Despite their looks, all characters' abilities are identical,\nso just pick whichever one you most closely resemble.": "Anca se drio ła siera A no par, łe abiłidà de tuti i parsonaji\nłe ze conpagne, donca tote sù cheło che'l te połe somejar de pì.", "Don't get too cocky with that energy shield; you can still get yourself thrown off a cliff.": "Mai far masa i gałeti co'l scudo nerjètego: A te połi uncora farte trar baso da na croda.", - "Don't run all the time. Really. You will fall off cliffs.": "No stà córar tuto el tenpo. Dalvero. A te fenirè zó par na croda.", + "Don't run all the time. Really. You will fall off cliffs.": "No stà córar tuto el tenpo. Dabon. Te fenirè zó par na croda.", "Don't spin for too long; you'll become dizzy and fall.": "No stà ndar torno par masa tenpo: te vegnarà na storniroła e te cascarè.", "Hold any button to run. (Trigger buttons work well if you have them)": "Tien strucà un boton calsìase par córar. (I griłeti nałòzeghi i ze i pì adati, se A te łi ghè)", "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Tien strucà un boton calsìase par córar. A te te movarè pì in\npresa ma no te voltarè miga masa ben, donca ocio a łe crode.", "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Łe bonbe jaso no łe ze miga tanto potenti, ma łe injasa tuto cheło\nche łe brinca, e cheło che'l ze injasà el ze anca fàsiłe da fracasar.", "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Se calchedun el te łeva sù, daghe un crogno che'l te\nmołe zó. 'Sta roba ła funsiona anca inte ła vida vera.", - "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Se A te si a curto de controładori, instała l'apl '${REMOTE_APP_NAME}'\nso i to dispozitivi mòbiłi e dopàrełi cofà controładori.", + "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Se te si a curto de controładori, instała l'apl '${REMOTE_APP_NAME}'\nso i to dispozidivi mòbiłi e dopàrełi cofà controładori.", "If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Se na bonba petaisa ła te se taca doso, salta in volta e và torno: ła gavarìa da cavarse.\nE se no funsionase, i to momenti ùltemi de vida i gavarà almanco dà spetàgoło!", "If you kill an enemy in one hit you get double points for it.": "Se A te copi un nemigo co un colpo soło A te ciapi el dupio de i punti.", "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Se A te ciapi na małedision, A te ghè soło na sparansa de salvesa:\ncatar fora un pacheto sałude inte i puchi segondi che A te resta.", "If you stay in one place, you're toast. Run and dodge to survive..": "Se A se stà fermi inte un posto, A se ze friti. Cori e schiva par soravìvar...", - "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Se A te te cati tanti zugadori che i và e vien, e inte'l cazo che calchedun el se dezménteghe de\nmołar el zugo, ativa ła funsion automàtega 'Para vìa zugadori in sonera' inte łe inpostasion.", - "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Se'l to dispozitivo el taca scotar o se A te ghè caro sparagnar batarìa,\ncała ła \"Prospetiva\" o ła \"Resołusion\" so Inpostasion > Gràfega", + "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Se te te cati tanti zugadori che và e vien, e inte'l cazo che calchedun el se dezménteghe de\nmołar el zugo, ativa ła funsion automàtega 'Para fora zugadori in sonera' inte łe inpostasion.", + "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Se'l to dispozidivo el taca scotar o se te ghè caro sparagnar batarìa,\ncała ła \"Prospetiva\" o ła \"Resołusion\" so Inpostasion > Gràfega", "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Se el zugo el và a scati, proa całar ła resołusion\no ła prospetiva inte l'inpostasion gràfega.", "In Capture-the-Flag, your own flag must be at your base to score, If the other\nteam is about to score, stealing their flag can be a good way to stop them.": "Par far ponto so 'Brinca ła bandiera', A te ghè da portar ła bandiera fin so ła to\nbaze. Se cheł'altra scuadra ła ze drio far ponto, A te połi fermarla anca robàndogheła.", "In hockey, you'll maintain more speed if you turn gradually.": "Inte l'hockey, voltando gradualmente A te mantien alta ła vełosidà.", @@ -1769,7 +1773,7 @@ "randomName3Text": "Teo", "randomName4Text": "Cesco", "randomName5Text": "Stè", - "skipConfirmText": "Vutu dalvero saltar ła demostrasion? Toca o struca par confermar.", + "skipConfirmText": "Vutu dabon saltar ła demostrasion? Toca o struca par confermar.", "skipVoteCountText": "Voti par saltar ła demostrasion: ${COUNT}/${TOTAL}", "skippingText": "demostrasion saltada...", "toSkipPressAnythingText": "(toca o struca un boton par saltar ła demostrasion)" @@ -1787,7 +1791,7 @@ "upgradeText": "Mejora", "upgradeToPlayText": "Par zugarghe, dezbloca \"${PRO}\" inte ła botega.", "useDefaultText": "Reinposta", - "usesExternalControllerText": "'Sto zugo el dòpara un controłador esterno cofà dispozitivo de entrada.", + "usesExternalControllerText": "'Sto zugo el dòpara un controłador esterno cofà dispozidivo de entrada.", "usingItunesText": "Doparar l'apl de mùzega par el son de fondo...", "validatingTestBuildText": "Confermasion varsion de proa...", "victoryText": "Vitoria!", @@ -1832,21 +1836,23 @@ "wiimoteSetupWindow": { "copyrightText": "Deriti d'autor DarwiinRemote", "listenText": "Scolta", - "macInstructionsText": "Segùrate che ła to Wii ła sipie stuada e el bluetooth de'l\nto Mac ativà, donca stuca so 'Scolta'. Ła conpatibiłidà co'l\nWiimote ła podarìa èsar un fià inserta, donca te podarisi\ncatarte a proar pì 'olte prima de otegner na conesion.\n\nEl bluetooth el gavarìa da suportar fin a 7 dispozitivi conetesti,\nanca se ła capasidà ła podarìa variar.\n\nBombSquad el suporta el controłador Clàsego e i controładori\norijinałi Wiimotes, Nunchuks.\nAnca el novo Wii Remote Plus el funsiona\nma soło sensa acesori.", + "macInstructionsText": "Segùrate che ła to Wii ła sipie stuada e el bluetooth de'l\nto Mac ativà, donca stuca so 'Scolta'. Ła conpatibiłidà co'l\nWiimote ła podarìa èsar un fià inserta, donca te podarisi\ncatarte a proar pì 'olte prima de otegner na conesion.\n\nEl bluetooth el gavarìa da suportar fin a 7 dispozidivi conetesti,\nanca se ła capasidà ła podarìa variar.\n\nBombSquad el suporta el controłador Clàsego e i controładori\norijinałi Wiimotes, Nunchuks.\nAnca el novo Wii Remote Plus el funsiona\nma soło sensa acesori.", "thanksText": "Grasie a ła scuadra DarwiinRemote\npar verlo rendesto posìbiłe.", "titleText": "Configurasion Wiimote" }, "winsPlayerText": "Vitoria de ${NAME}!", "winsTeamText": "Vitoria de i ${NAME}!", "winsText": "Vitoria de ${NAME}!", + "workspaceSyncErrorText": "Eror de sincronizasion de ${WORKSPACE}. Varda el rejistro eventi par i detaji.", + "workspaceSyncReuseText": "Inposìbiłe sinconizar ${WORKSPACE}. Dòparo de ła varsion sincronizada presedente.", "worldScoresUnavailableText": "Punteji mondiałi miga disponìbiłi.", "worldsBestScoresText": "I punteji mejo de'l mondo", "worldsBestTimesText": "I tenpi mejo de'l mondo", "xbox360ControllersWindow": { "getDriverText": "Descarga el driver", - "macInstructions2Text": "Par doparar sensa fiło i controładori, A te serve anca el resevidor\nche'l riva co l''Xbox 360 Wireless Controller par Windows'.\nUn resevidor el te parmete de conétar fin a 4 controładori.\n\nInportante: i resevidori de terse parti no i funsionarà mìa co 'sto driver;\nsegùrate che'l to resevidor el sipia 'Microsoft' e miga 'XBOX 360'.\nŁa Microsoft no łi vende pì destacài, donca A te servirà par forsa cheło\nvendesto insebre co'l controłador, o senò, proa sercar so Ebay.\n\nSe A te cati ùtiłe el driver, ciapa in considerasion de farghe na\ndonasion a'l só dezviłupador so 'sto sito.", + "macInstructions2Text": "Par doparar sensa fiło i controładori, A te serve anca el resevidor\nche'l riva co l''Xbox 360 Wireless Controller par Windows'.\nUn resevidor el te parmete de conétar fin a 4 controładori.\n\nInportante: i resevidori de terse parti no i funsionarà miga co 'sto driver;\nsegùrate che'l to resevidor el sipia 'Microsoft' e miga 'XBOX 360'.\nŁa Microsoft no łi vende pì destacài, donca te servirà par forsa cheło\nvendesto insebre co'l controłador, o senò, proa sercar so Ebay.\n\nSe te cati ùtiłe el driver, ciapa in considerasion de farghe na\ndonasion a'l só dezviłupador so 'sto sito.", "macInstructionsText": "Per doparar i controładori co'l fiło de ła Xbox 360, A te ghè\nda instałar el driver Mac disponìbiłe so'l link cuà soto.\nEl funsiona co anbo i controładori, co'l fiło o sensa.", - "ouyaInstructionsText": "Par doparar so Bombsquad un controłador de l'Xbox 360 co'l fiło,\ntàcheło sù inte ła porta USB de’l to dispozitivo. A te połi anca\ntacar sù pì controładori insenbre doparando un hub USB.\n\nPar doparar i controładori sensa fiło invese, A te serve un resevidor\nde segnałe. A te połi catarlo, o rento ła scàtoła \"Controładori sensa fiło\nXbox 360 par Windows\", o vendesto a parte. Caun resevidor el và tacà so\nna porta USB e el te parmete de conétar fin a 4 controładori.", + "ouyaInstructionsText": "Par doparar so Bombsquad un controłador de l'Xbox 360 co'l fiło,\ntàcheło sù inte ła porta USB de’l to dispozidivo. Te połi anca\ntacar sù pì controładori insenbre doparando un hub USB.\n\nPar doparar i controładori sensa fiło invese, te serve un resevidor\nde segnałe. Te połi catarlo, o rento ła scàtoła \"Controładori sensa fiło\nXbox 360 par Windows\", o vendesto a parte. Caun resevidor el và tacà so\nna porta USB e el te parmete de conétar fin a 4 controładori.", "titleText": "Doparar un controłador Xbox 360 co ${APP_NAME}:" }, "yesAllowText": "Sì, parmeti!", diff --git a/dist/ba_data/python-site-packages/certifi/__init__.py b/dist/ba_data/python-site-packages/certifi/__init__.py index b700d3a..bdeb06b 100644 --- a/dist/ba_data/python-site-packages/certifi/__init__.py +++ b/dist/ba_data/python-site-packages/certifi/__init__.py @@ -1,4 +1,4 @@ from .core import contents, where __all__ = ["contents", "where"] -__version__ = "2022.05.18.1" +__version__ = "2022.06.15" diff --git a/dist/ba_data/python-site-packages/certifi/cacert.pem b/dist/ba_data/python-site-packages/certifi/cacert.pem index f597fb3..ee9be4c 100644 --- a/dist/ba_data/python-site-packages/certifi/cacert.pem +++ b/dist/ba_data/python-site-packages/certifi/cacert.pem @@ -1362,39 +1362,6 @@ Agu+TGbrIP65y7WZf+a2E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xl nJ2lYJU6Un/10asIbvPuW/mIPX64b24D5EI= -----END CERTIFICATE----- -# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority -# Subject: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority -# Label: "Hellenic Academic and Research Institutions RootCA 2011" -# Serial: 0 -# MD5 Fingerprint: 73:9f:4c:4b:73:5b:79:e9:fa:ba:1c:ef:6e:cb:d5:c9 -# SHA1 Fingerprint: fe:45:65:9b:79:03:5b:98:a1:61:b5:51:2e:ac:da:58:09:48:22:4d -# SHA256 Fingerprint: bc:10:4f:15:a4:8b:e7:09:dc:a5:42:a7:e1:d4:b9:df:6f:05:45:27:e8:02:ea:a9:2d:59:54:44:25:8a:fe:71 ------BEGIN CERTIFICATE----- -MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix -RDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 -dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p -YyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIFJvb3RDQSAyMDExMB4XDTExMTIw -NjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYTAkdSMUQwQgYDVQQK -EztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIENl -cnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl -c2VhcmNoIEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEB -BQADggEPADCCAQoCggEBAKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPz -dYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJ -fel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa71HFK9+WXesyHgLacEns -bgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u8yBRQlqD -75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSP -FEDH3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNV -HRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp -5dgTBCPuQSUwRwYDVR0eBEAwPqA8MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQu -b3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQub3JnMA0GCSqGSIb3DQEBBQUA -A4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVtXdMiKahsog2p -6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 -TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7 -dIsXRSZMFpGD/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8Acys -Nnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI -l7WdmplNsDz4SgCbZN2fOUvRJ9e4 ------END CERTIFICATE----- - # Issuer: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 # Subject: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 # Label: "Actalis Authentication Root CA" @@ -4528,3 +4495,191 @@ PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb gfM0agPnIjhQW+0ZT0MW -----END CERTIFICATE----- + +# Issuer: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. +# Subject: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. +# Label: "DigiCert TLS ECC P384 Root G5" +# Serial: 13129116028163249804115411775095713523 +# MD5 Fingerprint: d3:71:04:6a:43:1c:db:a6:59:e1:a8:a3:aa:c5:71:ed +# SHA1 Fingerprint: 17:f3:de:5e:9f:0f:19:e9:8e:f6:1f:32:26:6e:20:c4:07:ae:30:ee +# SHA256 Fingerprint: 01:8e:13:f0:77:25:32:cf:80:9b:d1:b1:72:81:86:72:83:fc:48:c6:e1:3b:e9:c6:98:12:85:4a:49:0c:1b:05 +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. +# Subject: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. +# Label: "DigiCert TLS RSA4096 Root G5" +# Serial: 11930366277458970227240571539258396554 +# MD5 Fingerprint: ac:fe:f7:34:96:a9:f2:b3:b4:12:4b:e4:27:41:6f:e1 +# SHA1 Fingerprint: a7:88:49:dc:5d:7c:75:8c:8c:de:39:98:56:b3:aa:d0:b2:a5:71:35 +# SHA256 Fingerprint: 37:1a:00:dc:05:33:b3:72:1a:7e:eb:40:e8:41:9e:70:79:9d:2b:0a:0f:2c:1d:80:69:31:65:f7:ce:c4:ad:75 +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT +HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN +NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs +IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ +ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 +2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp +wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM +pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD +nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po +sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx +Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd +Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX +KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe +XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL +tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv +TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN +AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H +PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF +O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ +REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik +AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv +/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ +p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw +MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF +qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK +ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE----- + +# Issuer: CN=Certainly Root R1 O=Certainly +# Subject: CN=Certainly Root R1 O=Certainly +# Label: "Certainly Root R1" +# Serial: 188833316161142517227353805653483829216 +# MD5 Fingerprint: 07:70:d4:3e:82:87:a0:fa:33:36:13:f4:fa:33:e7:12 +# SHA1 Fingerprint: a0:50:ee:0f:28:71:f4:27:b2:12:6d:6f:50:96:25:ba:cc:86:42:af +# SHA256 Fingerprint: 77:b8:2c:d8:64:4c:43:05:f7:ac:c5:cb:15:6b:45:67:50:04:03:3d:51:c6:0c:62:02:a8:e0:c3:34:67:d3:a0 +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw +PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy +dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 +YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 +1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT +vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed +aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 +1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 +r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 +cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ +wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ +6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA +2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH +Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR +eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u +d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr +PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi +1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd +rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di +taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 +lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj +yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn +Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy +yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n +wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 +OV+KmalBWQewLK8= +-----END CERTIFICATE----- + +# Issuer: CN=Certainly Root E1 O=Certainly +# Subject: CN=Certainly Root E1 O=Certainly +# Label: "Certainly Root E1" +# Serial: 8168531406727139161245376702891150584 +# MD5 Fingerprint: 0a:9e:ca:cd:3e:52:50:c6:36:f3:4b:a3:ed:a7:53:e9 +# SHA1 Fingerprint: f9:e1:6d:dc:01:89:cf:d5:82:45:63:3e:c5:37:7d:c2:eb:93:6f:2b +# SHA256 Fingerprint: b4:58:5f:22:e4:ac:75:6a:4e:86:12:a1:36:1c:5d:9d:03:1a:93:fd:84:fe:bb:77:8f:a3:06:8b:0f:c4:2d:c2 +-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw +CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu +bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ +BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s +eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK ++IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 +QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 +hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm +ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG +BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE----- + +# Issuer: CN=E-Tugra Global Root CA RSA v3 O=E-Tugra EBG A.S. OU=E-Tugra Trust Center +# Subject: CN=E-Tugra Global Root CA RSA v3 O=E-Tugra EBG A.S. OU=E-Tugra Trust Center +# Label: "E-Tugra Global Root CA RSA v3" +# Serial: 75951268308633135324246244059508261641472512052 +# MD5 Fingerprint: 22:be:10:f6:c2:f8:03:88:73:5f:33:29:47:28:47:a4 +# SHA1 Fingerprint: e9:a8:5d:22:14:52:1c:5b:aa:0a:b4:be:24:6a:23:8a:c9:ba:e2:a9 +# SHA256 Fingerprint: ef:66:b0:b1:0a:3c:db:9f:2e:36:48:c7:6b:d2:af:18:ea:d2:bf:e6:f1:17:65:5e:28:c4:06:0d:a1:a3:f4:c2 +-----BEGIN CERTIFICATE----- +MIIF8zCCA9ugAwIBAgIUDU3FzRYilZYIfrgLfxUGNPt5EDQwDQYJKoZIhvcNAQEL +BQAwgYAxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHEwZBbmthcmExGTAXBgNVBAoTEEUt +VHVncmEgRUJHIEEuUy4xHTAbBgNVBAsTFEUtVHVncmEgVHJ1c3QgQ2VudGVyMSYw +JAYDVQQDEx1FLVR1Z3JhIEdsb2JhbCBSb290IENBIFJTQSB2MzAeFw0yMDAzMTgw +OTA3MTdaFw00NTAzMTIwOTA3MTdaMIGAMQswCQYDVQQGEwJUUjEPMA0GA1UEBxMG +QW5rYXJhMRkwFwYDVQQKExBFLVR1Z3JhIEVCRyBBLlMuMR0wGwYDVQQLExRFLVR1 +Z3JhIFRydXN0IENlbnRlcjEmMCQGA1UEAxMdRS1UdWdyYSBHbG9iYWwgUm9vdCBD +QSBSU0EgdjMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCiZvCJt3J7 +7gnJY9LTQ91ew6aEOErxjYG7FL1H6EAX8z3DeEVypi6Q3po61CBxyryfHUuXCscx +uj7X/iWpKo429NEvx7epXTPcMHD4QGxLsqYxYdE0PD0xesevxKenhOGXpOhL9hd8 +7jwH7eKKV9y2+/hDJVDqJ4GohryPUkqWOmAalrv9c/SF/YP9f4RtNGx/ardLAQO/ +rWm31zLZ9Vdq6YaCPqVmMbMWPcLzJmAy01IesGykNz709a/r4d+ABs8qQedmCeFL +l+d3vSFtKbZnwy1+7dZ5ZdHPOrbRsV5WYVB6Ws5OUDGAA5hH5+QYfERaxqSzO8bG +wzrwbMOLyKSRBfP12baqBqG3q+Sx6iEUXIOk/P+2UNOMEiaZdnDpwA+mdPy70Bt4 +znKS4iicvObpCdg604nmvi533wEKb5b25Y08TVJ2Glbhc34XrD2tbKNSEhhw5oBO +M/J+JjKsBY04pOZ2PJ8QaQ5tndLBeSBrW88zjdGUdjXnXVXHt6woq0bM5zshtQoK +5EpZ3IE1S0SVEgpnpaH/WwAH0sDM+T/8nzPyAPiMbIedBi3x7+PmBvrFZhNb/FAH +nnGGstpvdDDPk1Po3CLW3iAfYY2jLqN4MpBs3KwytQXk9TwzDdbgh3cXTJ2w2Amo +DVf3RIXwyAS+XF1a4xeOVGNpf0l0ZAWMowIDAQABo2MwYTAPBgNVHRMBAf8EBTAD +AQH/MB8GA1UdIwQYMBaAFLK0ruYt9ybVqnUtdkvAG1Mh0EjvMB0GA1UdDgQWBBSy +tK7mLfcm1ap1LXZLwBtTIdBI7zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEL +BQADggIBAImocn+M684uGMQQgC0QDP/7FM0E4BQ8Tpr7nym/Ip5XuYJzEmMmtcyQ +6dIqKe6cLcwsmb5FJ+Sxce3kOJUxQfJ9emN438o2Fi+CiJ+8EUdPdk3ILY7r3y18 +Tjvarvbj2l0Upq7ohUSdBm6O++96SmotKygY/r+QLHUWnw/qln0F7psTpURs+APQ +3SPh/QMSEgj0GDSz4DcLdxEBSL9htLX4GdnLTeqjjO/98Aa1bZL0SmFQhO3sSdPk +vmjmLuMxC1QLGpLWgti2omU8ZgT5Vdps+9u1FGZNlIM7zR6mK7L+d0CGq+ffCsn9 +9t2HVhjYsCxVYJb6CH5SkPVLpi6HfMsg2wY+oF0Dd32iPBMbKaITVaA9FCKvb7jQ +mhty3QUBjYZgv6Rn7rWlDdF/5horYmbDB7rnoEgcOMPpRfunf/ztAmgayncSd6YA +VSgU7NbHEqIbZULpkejLPoeJVF3Zr52XnGnnCv8PWniLYypMfUeUP95L6VPQMPHF +9p5J3zugkaOj/s1YzOrfr28oO6Bpm4/srK4rVJ2bBLFHIK+WEj5jlB0E5y67hscM +moi/dkfv97ALl2bSRM9gUgfh1SxKOidhd8rXj+eHDjD/DLsE4mHDosiXYY60MGo8 +bcIHX0pzLz/5FooBZu+6kcpSV3uu1OYP3Qt6f4ueJiDPO++BcYNZ +-----END CERTIFICATE----- + +# Issuer: CN=E-Tugra Global Root CA ECC v3 O=E-Tugra EBG A.S. OU=E-Tugra Trust Center +# Subject: CN=E-Tugra Global Root CA ECC v3 O=E-Tugra EBG A.S. OU=E-Tugra Trust Center +# Label: "E-Tugra Global Root CA ECC v3" +# Serial: 218504919822255052842371958738296604628416471745 +# MD5 Fingerprint: 46:bc:81:bb:f1:b5:1e:f7:4b:96:bc:14:e2:e7:27:64 +# SHA1 Fingerprint: 8a:2f:af:57:53:b1:b0:e6:a1:04:ec:5b:6a:69:71:6d:f6:1c:e2:84 +# SHA256 Fingerprint: 87:3f:46:85:fa:7f:56:36:25:25:2e:6d:36:bc:d7:f1:6f:c2:49:51:f2:64:e4:7e:1b:95:4f:49:08:cd:ca:13 +-----BEGIN CERTIFICATE----- +MIICpTCCAiqgAwIBAgIUJkYZdzHhT28oNt45UYbm1JeIIsEwCgYIKoZIzj0EAwMw +gYAxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHEwZBbmthcmExGTAXBgNVBAoTEEUtVHVn +cmEgRUJHIEEuUy4xHTAbBgNVBAsTFEUtVHVncmEgVHJ1c3QgQ2VudGVyMSYwJAYD +VQQDEx1FLVR1Z3JhIEdsb2JhbCBSb290IENBIEVDQyB2MzAeFw0yMDAzMTgwOTQ2 +NThaFw00NTAzMTIwOTQ2NThaMIGAMQswCQYDVQQGEwJUUjEPMA0GA1UEBxMGQW5r +YXJhMRkwFwYDVQQKExBFLVR1Z3JhIEVCRyBBLlMuMR0wGwYDVQQLExRFLVR1Z3Jh +IFRydXN0IENlbnRlcjEmMCQGA1UEAxMdRS1UdWdyYSBHbG9iYWwgUm9vdCBDQSBF +Q0MgdjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASOmCm/xxAeJ9urA8woLNheSBkQ +KczLWYHMjLiSF4mDKpL2w6QdTGLVn9agRtwcvHbB40fQWxPa56WzZkjnIZpKT4YK +fWzqTTKACrJ6CZtpS5iB4i7sAnCWH/31Rs7K3IKjYzBhMA8GA1UdEwEB/wQFMAMB +Af8wHwYDVR0jBBgwFoAU/4Ixcj75xGZsrTie0bBRiKWQzPUwHQYDVR0OBBYEFP+C +MXI++cRmbK04ntGwUYilkMz1MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNp +ADBmAjEA5gVYaWHlLcoNy/EZCL3W/VGSGn5jVASQkZo1kTmZ+gepZpO6yGjUij/6 +7W4WAie3AjEA3VoXK3YdZUKWpqxdinlW2Iob35reX8dQj7FbcQwm32pAAOwzkSFx +vmjkI6TZraE3 +-----END CERTIFICATE----- diff --git a/dist/ba_data/python-site-packages/typing_extensions.py b/dist/ba_data/python-site-packages/typing_extensions.py index dc03881..31d3564 100644 --- a/dist/ba_data/python-site-packages/typing_extensions.py +++ b/dist/ba_data/python-site-packages/typing_extensions.py @@ -37,6 +37,7 @@ __all__ = [ 'Counter', 'Deque', 'DefaultDict', + 'NamedTuple', 'OrderedDict', 'TypedDict', @@ -380,6 +381,46 @@ def _is_callable_members_only(cls): return all(callable(getattr(cls, attr, None)) for attr in _get_protocol_attrs(cls)) +def _maybe_adjust_parameters(cls): + """Helper function used in Protocol.__init_subclass__ and _TypedDictMeta.__new__. + + The contents of this function are very similar + to logic found in typing.Generic.__init_subclass__ + on the CPython main branch. + """ + tvars = [] + if '__orig_bases__' in cls.__dict__: + tvars = typing._collect_type_vars(cls.__orig_bases__) + # Look for Generic[T1, ..., Tn] or Protocol[T1, ..., Tn]. + # If found, tvars must be a subset of it. + # If not found, tvars is it. + # Also check for and reject plain Generic, + # and reject multiple Generic[...] and/or Protocol[...]. + gvars = None + for base in cls.__orig_bases__: + if (isinstance(base, typing._GenericAlias) and + base.__origin__ in (typing.Generic, Protocol)): + # for error messages + the_base = base.__origin__.__name__ + if gvars is not None: + raise TypeError( + "Cannot inherit from Generic[...]" + " and/or Protocol[...] multiple types.") + gvars = base.__parameters__ + if gvars is None: + gvars = tvars + else: + tvarset = set(tvars) + gvarset = set(gvars) + if not tvarset <= gvarset: + s_vars = ', '.join(str(t) for t in tvars if t not in gvarset) + s_args = ', '.join(str(g) for g in gvars) + raise TypeError(f"Some type variables ({s_vars}) are" + f" not listed in {the_base}[{s_args}]") + tvars = gvars + cls.__parameters__ = tuple(tvars) + + # 3.8+ if hasattr(typing, 'Protocol'): Protocol = typing.Protocol @@ -476,43 +517,13 @@ else: return typing._GenericAlias(cls, params) def __init_subclass__(cls, *args, **kwargs): - tvars = [] if '__orig_bases__' in cls.__dict__: error = typing.Generic in cls.__orig_bases__ else: error = typing.Generic in cls.__bases__ if error: raise TypeError("Cannot inherit from plain Generic") - if '__orig_bases__' in cls.__dict__: - tvars = typing._collect_type_vars(cls.__orig_bases__) - # Look for Generic[T1, ..., Tn] or Protocol[T1, ..., Tn]. - # If found, tvars must be a subset of it. - # If not found, tvars is it. - # Also check for and reject plain Generic, - # and reject multiple Generic[...] and/or Protocol[...]. - gvars = None - for base in cls.__orig_bases__: - if (isinstance(base, typing._GenericAlias) and - base.__origin__ in (typing.Generic, Protocol)): - # for error messages - the_base = base.__origin__.__name__ - if gvars is not None: - raise TypeError( - "Cannot inherit from Generic[...]" - " and/or Protocol[...] multiple types.") - gvars = base.__parameters__ - if gvars is None: - gvars = tvars - else: - tvarset = set(tvars) - gvarset = set(gvars) - if not tvarset <= gvarset: - s_vars = ', '.join(str(t) for t in tvars if t not in gvarset) - s_args = ', '.join(str(g) for g in gvars) - raise TypeError(f"Some type variables ({s_vars}) are" - f" not listed in {the_base}[{s_args}]") - tvars = gvars - cls.__parameters__ = tuple(tvars) + _maybe_adjust_parameters(cls) # Determine if this is a protocol or a concrete subclass. if not cls.__dict__.get('_is_protocol', None): @@ -613,6 +624,7 @@ if hasattr(typing, "Required"): # keyword with old-style TypedDict(). See https://bugs.python.org/issue42059 # The standard library TypedDict below Python 3.11 does not store runtime # information about optional and required keys when using Required or NotRequired. + # Generic TypedDicts are also impossible using typing.TypedDict on Python <3.11. TypedDict = typing.TypedDict _TypedDictMeta = typing._TypedDictMeta is_typeddict = typing.is_typeddict @@ -695,8 +707,16 @@ else: # Subclasses and instances of TypedDict return actual dictionaries # via _dict_new. ns['__new__'] = _typeddict_new if name == 'TypedDict' else _dict_new + # Don't insert typing.Generic into __bases__ here, + # or Generic.__init_subclass__ will raise TypeError + # in the super().__new__() call. + # Instead, monkey-patch __bases__ onto the class after it's been created. tp_dict = super().__new__(cls, name, (dict,), ns) + if any(issubclass(base, typing.Generic) for base in bases): + tp_dict.__bases__ = (typing.Generic, dict) + _maybe_adjust_parameters(tp_dict) + annotations = {} own_annotations = ns.get('__annotations__', {}) msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" @@ -1958,3 +1978,92 @@ else: if not hasattr(typing, "TypeVarTuple"): typing._collect_type_vars = _collect_type_vars typing._check_generic = _check_generic + + +# Backport typing.NamedTuple as it exists in Python 3.11. +# In 3.11, the ability to define generic `NamedTuple`s was supported. +# This was explicitly disallowed in 3.9-3.10, and only half-worked in <=3.8. +if sys.version_info >= (3, 11): + NamedTuple = typing.NamedTuple +else: + def _caller(): + try: + return sys._getframe(2).f_globals.get('__name__', '__main__') + except (AttributeError, ValueError): # For platforms without _getframe() + return None + + def _make_nmtuple(name, types, module, defaults=()): + fields = [n for n, t in types] + annotations = {n: typing._type_check(t, f"field {n} annotation must be a type") + for n, t in types} + nm_tpl = collections.namedtuple(name, fields, + defaults=defaults, module=module) + nm_tpl.__annotations__ = nm_tpl.__new__.__annotations__ = annotations + # The `_field_types` attribute was removed in 3.9; + # in earlier versions, it is the same as the `__annotations__` attribute + if sys.version_info < (3, 9): + nm_tpl._field_types = annotations + return nm_tpl + + _prohibited_namedtuple_fields = typing._prohibited + _special_namedtuple_fields = frozenset({'__module__', '__name__', '__annotations__'}) + + class _NamedTupleMeta(type): + def __new__(cls, typename, bases, ns): + assert _NamedTuple in bases + for base in bases: + if base is not _NamedTuple and base is not typing.Generic: + raise TypeError( + 'can only inherit from a NamedTuple type and Generic') + bases = tuple(tuple if base is _NamedTuple else base for base in bases) + types = ns.get('__annotations__', {}) + default_names = [] + for field_name in types: + if field_name in ns: + default_names.append(field_name) + elif default_names: + raise TypeError(f"Non-default namedtuple field {field_name} " + f"cannot follow default field" + f"{'s' if len(default_names) > 1 else ''} " + f"{', '.join(default_names)}") + nm_tpl = _make_nmtuple( + typename, types.items(), + defaults=[ns[n] for n in default_names], + module=ns['__module__'] + ) + nm_tpl.__bases__ = bases + if typing.Generic in bases: + class_getitem = typing.Generic.__class_getitem__.__func__ + nm_tpl.__class_getitem__ = classmethod(class_getitem) + # update from user namespace without overriding special namedtuple attributes + for key in ns: + if key in _prohibited_namedtuple_fields: + raise AttributeError("Cannot overwrite NamedTuple attribute " + key) + elif key not in _special_namedtuple_fields and key not in nm_tpl._fields: + setattr(nm_tpl, key, ns[key]) + if typing.Generic in bases: + nm_tpl.__init_subclass__() + return nm_tpl + + def NamedTuple(__typename, __fields=None, **kwargs): + if __fields is None: + __fields = kwargs.items() + elif kwargs: + raise TypeError("Either list of fields or keywords" + " can be provided to NamedTuple, not both") + return _make_nmtuple(__typename, __fields, module=_caller()) + + NamedTuple.__doc__ = typing.NamedTuple.__doc__ + _NamedTuple = type.__new__(_NamedTupleMeta, 'NamedTuple', (), {}) + + # On 3.8+, alter the signature so that it matches typing.NamedTuple. + # The signature of typing.NamedTuple on >=3.8 is invalid syntax in Python 3.7, + # so just leave the signature as it is on 3.7. + if sys.version_info >= (3, 8): + NamedTuple.__text_signature__ = '(typename, fields=None, /, **kwargs)' + + def _namedtuple_mro_entries(bases): + assert NamedTuple in bases + return (_NamedTuple,) + + NamedTuple.__mro_entries__ = _namedtuple_mro_entries diff --git a/dist/ba_data/python/ba/_accountv1.py b/dist/ba_data/python/ba/_accountv1.py index 0b07457..825a19a 100644 --- a/dist/ba_data/python/ba/_accountv1.py +++ b/dist/ba_data/python/ba/_accountv1.py @@ -74,7 +74,7 @@ class AccountV1Subsystem: def get_league_rank_points(self, data: dict[str, Any] | None, - subset: str = None) -> int: + subset: str | None = None) -> int: """(internal)""" if data is None: return 0 diff --git a/dist/ba_data/python/ba/_achievement.py b/dist/ba_data/python/ba/_achievement.py index 5a81d30..22b1bc5 100644 --- a/dist/ba_data/python/ba/_achievement.py +++ b/dist/ba_data/python/ba/_achievement.py @@ -610,8 +610,8 @@ class Achievement: x: float, y: float, delay: float, - outdelay: float = None, - color: Sequence[float] = None, + outdelay: float | None = None, + color: Sequence[float] | None = None, style: str = 'post_game') -> list[ba.Actor]: """Create a display for the Achievement. diff --git a/dist/ba_data/python/ba/_ads.py b/dist/ba_data/python/ba/_ads.py index fbf5696..ff688fd 100644 --- a/dist/ba_data/python/ba/_ads.py +++ b/dist/ba_data/python/ba/_ads.py @@ -53,14 +53,15 @@ class AdsSubsystem: def show_ad(self, purpose: str, - on_completion_call: Callable[[], Any] = None) -> None: + on_completion_call: Callable[[], Any] | None = None) -> None: """(internal)""" self.last_ad_purpose = purpose _ba.show_ad(purpose, on_completion_call) - def show_ad_2(self, - purpose: str, - on_completion_call: Callable[[bool], Any] = None) -> None: + def show_ad_2( + self, + purpose: str, + on_completion_call: Callable[[bool], Any] | None = None) -> None: """(internal)""" self.last_ad_purpose = purpose _ba.show_ad_2(purpose, on_completion_call) diff --git a/dist/ba_data/python/ba/_app.py b/dist/ba_data/python/ba/_app.py index 82216d4..a0955c3 100644 --- a/dist/ba_data/python/ba/_app.py +++ b/dist/ba_data/python/ba/_app.py @@ -29,6 +29,7 @@ if TYPE_CHECKING: from ba._cloud import CloudSubsystem from bastd.actor import spazappearance from ba._accountv2 import AccountV2Subsystem + from ba._level import Level class App: @@ -274,6 +275,7 @@ class App: # Co-op Campaigns. self.campaigns: dict[str, ba.Campaign] = {} + self.custom_coop_practice_games: list[str] = [] # Server Mode. self.server: ba.ServerController | None = None @@ -341,7 +343,7 @@ class App: from bastd import maps as stdmaps from bastd.actor import spazappearance from ba._generated.enums import TimeType - + self._aioloop = _asyncio.setup_asyncio() @@ -430,13 +432,9 @@ class App: self.meta.on_app_running() self.plugins.on_app_running() - import custom_hooks - custom_hooks.on_app_running() # from ba._dependency import test_depset # test_depset() - if bool(False): - self._test_https() def _update_state(self) -> None: if self._app_paused: @@ -530,6 +528,15 @@ class App: # FIXME: This should not be an actor attr. activity.paused_text = None + def add_coop_practice_level(self, level: Level) -> None: + """Adds an individual level to the 'practice' section in Co-op.""" + + # Assign this level to our catch-all campaign. + self.campaigns['Challenges'].addlevel(level) + + # Make note to add it to our challenges UI. + self.custom_coop_practice_games.append(f'Challenges:{level.name}') + def return_to_main_menu_session_gracefully(self, reset_ui: bool = True) -> None: """Attempt to cleanly get back to the main menu.""" @@ -580,7 +587,7 @@ class App: def launch_coop_game(self, game: str, force: bool = False, - args: dict = None) -> bool: + args: dict | None = None) -> bool: """High level way to launch a local co-op session.""" # pylint: disable=cyclic-import from ba._campaign import getcampaign @@ -646,19 +653,3 @@ class App: """ self._initial_login_completed = True self._update_state() - - def _test_https(self) -> None: - """Testing https support. - - (would be nice to get this working on our custom Python builds; need - to wrangle certificates somehow). - """ - import urllib.request - try: - with urllib.request.urlopen('https://example.com') as url: - val = url.read() - _ba.screenmessage('HTTPS SUCCESS!') - print('HTTPS TEST SUCCESS', len(val)) - except Exception as exc: - _ba.screenmessage('HTTPS FAIL.') - print('HTTPS TEST FAIL:', exc) diff --git a/dist/ba_data/python/ba/_apputils.py b/dist/ba_data/python/ba/_apputils.py index ea88890..de13b82 100644 --- a/dist/ba_data/python/ba/_apputils.py +++ b/dist/ba_data/python/ba/_apputils.py @@ -177,9 +177,10 @@ def garbage_collect() -> None: gc.collect() -def print_live_object_warnings(when: Any, - ignore_session: ba.Session = None, - ignore_activity: ba.Activity = None) -> None: +def print_live_object_warnings( + when: Any, + ignore_session: ba.Session | None = None, + ignore_activity: ba.Activity | None = None) -> None: """Print warnings for remaining objects in the current context.""" # pylint: disable=cyclic-import from ba._session import Session diff --git a/dist/ba_data/python/ba/_assetmanager.py b/dist/ba_data/python/ba/_assetmanager.py index c12f96d..e57c921 100644 --- a/dist/ba_data/python/ba/_assetmanager.py +++ b/dist/ba_data/python/ba/_assetmanager.py @@ -17,6 +17,7 @@ import sys from efro.dataclassio import (ioprepped, IOAttrs, dataclass_from_json, dataclass_to_json) +import _ba if TYPE_CHECKING: from bacommon.assets import AssetPackageFlavor @@ -165,7 +166,6 @@ def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None: """ # pylint: disable=consider-using-with - import socket # We don't want to keep the provided AssetGather alive, but we want @@ -175,7 +175,9 @@ def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None: # Pass a very short timeout to urllib so we have opportunities # to cancel even with network blockage. - req = urllib.request.urlopen(url, timeout=1) + req = urllib.request.urlopen(url, + context=_ba.app.net.sslcontext, + timeout=1) file_size = int(req.headers['Content-Length']) print(f'\nDownloading: {filename} Bytes: {file_size:,}') diff --git a/dist/ba_data/python/ba/_asyncio.py b/dist/ba_data/python/ba/_asyncio.py index e10333b..c4ebc5e 100644 --- a/dist/ba_data/python/ba/_asyncio.py +++ b/dist/ba_data/python/ba/_asyncio.py @@ -11,6 +11,9 @@ from __future__ import annotations from typing import TYPE_CHECKING import asyncio +import logging +import time +import os if TYPE_CHECKING: import ba @@ -19,6 +22,8 @@ if TYPE_CHECKING: _asyncio_timer: ba.Timer | None = None _asyncio_event_loop: asyncio.AbstractEventLoop | None = None +DEBUG_TIMING = os.environ.get('BA_DEBUG_TIMING') == '1' + def setup_asyncio() -> asyncio.AbstractEventLoop: """Setup asyncio functionality for the logic thread.""" @@ -53,7 +58,18 @@ def setup_asyncio() -> asyncio.AbstractEventLoop: def run_cycle() -> None: assert _asyncio_event_loop is not None _asyncio_event_loop.call_soon(_asyncio_event_loop.stop) + starttime = time.monotonic() if DEBUG_TIMING else 0 _asyncio_event_loop.run_forever() + endtime = time.monotonic() if DEBUG_TIMING else 0 + + # Let's aim to have nothing take longer than 1/120 of a second. + if DEBUG_TIMING: + warn_time = 1.0 / 120 + duration = endtime - starttime + if duration > warn_time: + logging.warning( + 'Asyncio loop step took %.4fs; ideal max is %.4f', + duration, warn_time) global _asyncio_timer # pylint: disable=invalid-name _asyncio_timer = _ba.Timer(1.0 / 30.0, diff --git a/dist/ba_data/python/ba/_campaign.py b/dist/ba_data/python/ba/_campaign.py index 915fc4c..4b79c66 100644 --- a/dist/ba_data/python/ba/_campaign.py +++ b/dist/ba_data/python/ba/_campaign.py @@ -28,10 +28,16 @@ class Campaign: Category: **App Classes** """ - def __init__(self, name: str, sequential: bool = True): + def __init__(self, + name: str, + sequential: bool = True, + levels: list[ba.Level] | None = None): self._name = name - self._levels: list[ba.Level] = [] self._sequential = sequential + self._levels: list[ba.Level] = [] + if levels is not None: + for level in levels: + self.addlevel(level) @property def name(self) -> str: @@ -43,12 +49,15 @@ class Campaign: """Whether this Campaign's levels must be played in sequence.""" return self._sequential - def addlevel(self, level: ba.Level) -> None: + def addlevel(self, level: ba.Level, index: int | None = None) -> None: """Adds a ba.Level to the Campaign.""" if level.campaign is not None: raise RuntimeError('Level already belongs to a campaign.') level.set_campaign(self, len(self._levels)) - self._levels.append(level) + if index is None: + self._levels.append(level) + else: + self._levels.insert(index, level) @property def levels(self) -> list[ba.Level]: @@ -91,9 +100,8 @@ class Campaign: def init_campaigns() -> None: """Fill out initial default Campaigns.""" - # pylint: disable=too-many-statements # pylint: disable=cyclic-import - from ba import _level + from ba._level import Level from bastd.game.onslaught import OnslaughtGame from bastd.game.football import FootballCoopGame from bastd.game.runaround import RunaroundGame @@ -109,244 +117,218 @@ def init_campaigns() -> None: # FIXME: Once translations catch up, we can convert these to use the # generic display-name '${GAME} Training' type stuff. - campaign = Campaign('Easy') - campaign.addlevel( - _level.Level('Onslaught Training', - gametype=OnslaughtGame, - settings={'preset': 'training_easy'}, - preview_texture_name='doomShroomPreview')) - campaign.addlevel( - _level.Level('Rookie Onslaught', - gametype=OnslaughtGame, - settings={'preset': 'rookie_easy'}, - preview_texture_name='courtyardPreview')) - campaign.addlevel( - _level.Level('Rookie Football', - gametype=FootballCoopGame, - settings={'preset': 'rookie_easy'}, - preview_texture_name='footballStadiumPreview')) - campaign.addlevel( - _level.Level('Pro Onslaught', - gametype=OnslaughtGame, - settings={'preset': 'pro_easy'}, - preview_texture_name='doomShroomPreview')) - campaign.addlevel( - _level.Level('Pro Football', - gametype=FootballCoopGame, - settings={'preset': 'pro_easy'}, - preview_texture_name='footballStadiumPreview')) - campaign.addlevel( - _level.Level('Pro Runaround', - gametype=RunaroundGame, - settings={'preset': 'pro_easy'}, - preview_texture_name='towerDPreview')) - campaign.addlevel( - _level.Level('Uber Onslaught', - gametype=OnslaughtGame, - settings={'preset': 'uber_easy'}, - preview_texture_name='courtyardPreview')) - campaign.addlevel( - _level.Level('Uber Football', - gametype=FootballCoopGame, - settings={'preset': 'uber_easy'}, - preview_texture_name='footballStadiumPreview')) - campaign.addlevel( - _level.Level('Uber Runaround', - gametype=RunaroundGame, - settings={'preset': 'uber_easy'}, - preview_texture_name='towerDPreview')) - register_campaign(campaign) + register_campaign( + Campaign( + 'Easy', + levels=[ + Level('Onslaught Training', + gametype=OnslaughtGame, + settings={'preset': 'training_easy'}, + preview_texture_name='doomShroomPreview'), + Level('Rookie Onslaught', + gametype=OnslaughtGame, + settings={'preset': 'rookie_easy'}, + preview_texture_name='courtyardPreview'), + Level('Rookie Football', + gametype=FootballCoopGame, + settings={'preset': 'rookie_easy'}, + preview_texture_name='footballStadiumPreview'), + Level('Pro Onslaught', + gametype=OnslaughtGame, + settings={'preset': 'pro_easy'}, + preview_texture_name='doomShroomPreview'), + Level('Pro Football', + gametype=FootballCoopGame, + settings={'preset': 'pro_easy'}, + preview_texture_name='footballStadiumPreview'), + Level('Pro Runaround', + gametype=RunaroundGame, + settings={'preset': 'pro_easy'}, + preview_texture_name='towerDPreview'), + Level('Uber Onslaught', + gametype=OnslaughtGame, + settings={'preset': 'uber_easy'}, + preview_texture_name='courtyardPreview'), + Level('Uber Football', + gametype=FootballCoopGame, + settings={'preset': 'uber_easy'}, + preview_texture_name='footballStadiumPreview'), + Level('Uber Runaround', + gametype=RunaroundGame, + settings={'preset': 'uber_easy'}, + preview_texture_name='towerDPreview') + ], + )) # "hard" mode - campaign = Campaign('Default') - campaign.addlevel( - _level.Level('Onslaught Training', - gametype=OnslaughtGame, - settings={'preset': 'training'}, - preview_texture_name='doomShroomPreview')) - campaign.addlevel( - _level.Level('Rookie Onslaught', - gametype=OnslaughtGame, - settings={'preset': 'rookie'}, - preview_texture_name='courtyardPreview')) - campaign.addlevel( - _level.Level('Rookie Football', - gametype=FootballCoopGame, - settings={'preset': 'rookie'}, - preview_texture_name='footballStadiumPreview')) - campaign.addlevel( - _level.Level('Pro Onslaught', - gametype=OnslaughtGame, - settings={'preset': 'pro'}, - preview_texture_name='doomShroomPreview')) - campaign.addlevel( - _level.Level('Pro Football', - gametype=FootballCoopGame, - settings={'preset': 'pro'}, - preview_texture_name='footballStadiumPreview')) - campaign.addlevel( - _level.Level('Pro Runaround', - gametype=RunaroundGame, - settings={'preset': 'pro'}, - preview_texture_name='towerDPreview')) - campaign.addlevel( - _level.Level('Uber Onslaught', - gametype=OnslaughtGame, - settings={'preset': 'uber'}, - preview_texture_name='courtyardPreview')) - campaign.addlevel( - _level.Level('Uber Football', - gametype=FootballCoopGame, - settings={'preset': 'uber'}, - preview_texture_name='footballStadiumPreview')) - campaign.addlevel( - _level.Level('Uber Runaround', - gametype=RunaroundGame, - settings={'preset': 'uber'}, - preview_texture_name='towerDPreview')) - campaign.addlevel( - _level.Level('The Last Stand', - gametype=TheLastStandGame, - settings={}, - preview_texture_name='rampagePreview')) - register_campaign(campaign) + register_campaign( + Campaign( + 'Default', + levels=[ + Level('Onslaught Training', + gametype=OnslaughtGame, + settings={'preset': 'training'}, + preview_texture_name='doomShroomPreview'), + Level('Rookie Onslaught', + gametype=OnslaughtGame, + settings={'preset': 'rookie'}, + preview_texture_name='courtyardPreview'), + Level('Rookie Football', + gametype=FootballCoopGame, + settings={'preset': 'rookie'}, + preview_texture_name='footballStadiumPreview'), + Level('Pro Onslaught', + gametype=OnslaughtGame, + settings={'preset': 'pro'}, + preview_texture_name='doomShroomPreview'), + Level('Pro Football', + gametype=FootballCoopGame, + settings={'preset': 'pro'}, + preview_texture_name='footballStadiumPreview'), + Level('Pro Runaround', + gametype=RunaroundGame, + settings={'preset': 'pro'}, + preview_texture_name='towerDPreview'), + Level('Uber Onslaught', + gametype=OnslaughtGame, + settings={'preset': 'uber'}, + preview_texture_name='courtyardPreview'), + Level('Uber Football', + gametype=FootballCoopGame, + settings={'preset': 'uber'}, + preview_texture_name='footballStadiumPreview'), + Level('Uber Runaround', + gametype=RunaroundGame, + settings={'preset': 'uber'}, + preview_texture_name='towerDPreview'), + Level('The Last Stand', + gametype=TheLastStandGame, + settings={}, + preview_texture_name='rampagePreview') + ], + )) # challenges: our 'official' random extra co-op levels - campaign = Campaign('Challenges', sequential=False) - campaign.addlevel( - _level.Level('Infinite Onslaught', - gametype=OnslaughtGame, - settings={'preset': 'endless'}, - preview_texture_name='doomShroomPreview')) - campaign.addlevel( - _level.Level('Infinite Runaround', - gametype=RunaroundGame, - settings={'preset': 'endless'}, - preview_texture_name='towerDPreview')) - campaign.addlevel( - _level.Level('Race', - displayname='${GAME}', - gametype=RaceGame, - settings={ - 'map': 'Big G', - 'Laps': 3, - 'Bomb Spawning': 0 - }, - preview_texture_name='bigGPreview')) - campaign.addlevel( - _level.Level('Pro Race', - displayname='Pro ${GAME}', - gametype=RaceGame, - settings={ - 'map': 'Big G', - 'Laps': 3, - 'Bomb Spawning': 1000 - }, - preview_texture_name='bigGPreview')) - campaign.addlevel( - _level.Level('Lake Frigid Race', - displayname='${GAME}', - gametype=RaceGame, - settings={ - 'map': 'Lake Frigid', - 'Laps': 6, - 'Mine Spawning': 2000, - 'Bomb Spawning': 0 - }, - preview_texture_name='lakeFrigidPreview')) - campaign.addlevel( - _level.Level('Football', - displayname='${GAME}', - gametype=FootballCoopGame, - settings={'preset': 'tournament'}, - preview_texture_name='footballStadiumPreview')) - campaign.addlevel( - _level.Level('Pro Football', - displayname='Pro ${GAME}', - gametype=FootballCoopGame, - settings={'preset': 'tournament_pro'}, - preview_texture_name='footballStadiumPreview')) - campaign.addlevel( - _level.Level('Runaround', - displayname='${GAME}', - gametype=RunaroundGame, - settings={'preset': 'tournament'}, - preview_texture_name='towerDPreview')) - campaign.addlevel( - _level.Level('Uber Runaround', - displayname='Uber ${GAME}', - gametype=RunaroundGame, - settings={'preset': 'tournament_uber'}, - preview_texture_name='towerDPreview')) - campaign.addlevel( - _level.Level('The Last Stand', - displayname='${GAME}', - gametype=TheLastStandGame, - settings={'preset': 'tournament'}, - preview_texture_name='rampagePreview')) - campaign.addlevel( - _level.Level('Tournament Infinite Onslaught', - displayname='Infinite Onslaught', - gametype=OnslaughtGame, - settings={'preset': 'endless_tournament'}, - preview_texture_name='doomShroomPreview')) - campaign.addlevel( - _level.Level('Tournament Infinite Runaround', - displayname='Infinite Runaround', - gametype=RunaroundGame, - settings={'preset': 'endless_tournament'}, - preview_texture_name='towerDPreview')) - campaign.addlevel( - _level.Level('Target Practice', - displayname='Pro ${GAME}', - gametype=TargetPracticeGame, - settings={}, - preview_texture_name='doomShroomPreview')) - campaign.addlevel( - _level.Level('Target Practice B', - displayname='${GAME}', - gametype=TargetPracticeGame, - settings={ - 'Target Count': 2, - 'Enable Impact Bombs': False, - 'Enable Triple Bombs': False - }, - preview_texture_name='doomShroomPreview')) - campaign.addlevel( - _level.Level('Meteor Shower', - displayname='${GAME}', - gametype=MeteorShowerGame, - settings={}, - preview_texture_name='rampagePreview')) - campaign.addlevel( - _level.Level('Epic Meteor Shower', - displayname='${GAME}', - gametype=MeteorShowerGame, - settings={'Epic Mode': True}, - preview_texture_name='rampagePreview')) - campaign.addlevel( - _level.Level('Easter Egg Hunt', - displayname='${GAME}', - gametype=EasterEggHuntGame, - settings={}, - preview_texture_name='towerDPreview')) - campaign.addlevel( - _level.Level('Pro Easter Egg Hunt', - displayname='Pro ${GAME}', - gametype=EasterEggHuntGame, - settings={'Pro Mode': True}, - preview_texture_name='towerDPreview')) - campaign.addlevel( - _level.Level( - name='Ninja Fight', # (unique id not seen by player) - displayname='${GAME}', # (readable name seen by player) - gametype=NinjaFightGame, - settings={'preset': 'regular'}, - preview_texture_name='courtyardPreview')) - campaign.addlevel( - _level.Level(name='Pro Ninja Fight', - displayname='Pro ${GAME}', - gametype=NinjaFightGame, - settings={'preset': 'pro'}, - preview_texture_name='courtyardPreview')) - register_campaign(campaign) + register_campaign( + Campaign( + 'Challenges', + sequential=False, + levels=[ + Level('Infinite Onslaught', + gametype=OnslaughtGame, + settings={'preset': 'endless'}, + preview_texture_name='doomShroomPreview'), + Level('Infinite Runaround', + gametype=RunaroundGame, + settings={'preset': 'endless'}, + preview_texture_name='towerDPreview'), + Level('Race', + displayname='${GAME}', + gametype=RaceGame, + settings={ + 'map': 'Big G', + 'Laps': 3, + 'Bomb Spawning': 0 + }, + preview_texture_name='bigGPreview'), + Level('Pro Race', + displayname='Pro ${GAME}', + gametype=RaceGame, + settings={ + 'map': 'Big G', + 'Laps': 3, + 'Bomb Spawning': 1000 + }, + preview_texture_name='bigGPreview'), + Level('Lake Frigid Race', + displayname='${GAME}', + gametype=RaceGame, + settings={ + 'map': 'Lake Frigid', + 'Laps': 6, + 'Mine Spawning': 2000, + 'Bomb Spawning': 0 + }, + preview_texture_name='lakeFrigidPreview'), + Level('Football', + displayname='${GAME}', + gametype=FootballCoopGame, + settings={'preset': 'tournament'}, + preview_texture_name='footballStadiumPreview'), + Level('Pro Football', + displayname='Pro ${GAME}', + gametype=FootballCoopGame, + settings={'preset': 'tournament_pro'}, + preview_texture_name='footballStadiumPreview'), + Level('Runaround', + displayname='${GAME}', + gametype=RunaroundGame, + settings={'preset': 'tournament'}, + preview_texture_name='towerDPreview'), + Level('Uber Runaround', + displayname='Uber ${GAME}', + gametype=RunaroundGame, + settings={'preset': 'tournament_uber'}, + preview_texture_name='towerDPreview'), + Level('The Last Stand', + displayname='${GAME}', + gametype=TheLastStandGame, + settings={'preset': 'tournament'}, + preview_texture_name='rampagePreview'), + Level('Tournament Infinite Onslaught', + displayname='Infinite Onslaught', + gametype=OnslaughtGame, + settings={'preset': 'endless_tournament'}, + preview_texture_name='doomShroomPreview'), + Level('Tournament Infinite Runaround', + displayname='Infinite Runaround', + gametype=RunaroundGame, + settings={'preset': 'endless_tournament'}, + preview_texture_name='towerDPreview'), + Level('Target Practice', + displayname='Pro ${GAME}', + gametype=TargetPracticeGame, + settings={}, + preview_texture_name='doomShroomPreview'), + Level('Target Practice B', + displayname='${GAME}', + gametype=TargetPracticeGame, + settings={ + 'Target Count': 2, + 'Enable Impact Bombs': False, + 'Enable Triple Bombs': False + }, + preview_texture_name='doomShroomPreview'), + Level('Meteor Shower', + displayname='${GAME}', + gametype=MeteorShowerGame, + settings={}, + preview_texture_name='rampagePreview'), + Level('Epic Meteor Shower', + displayname='${GAME}', + gametype=MeteorShowerGame, + settings={'Epic Mode': True}, + preview_texture_name='rampagePreview'), + Level('Easter Egg Hunt', + displayname='${GAME}', + gametype=EasterEggHuntGame, + settings={}, + preview_texture_name='towerDPreview'), + Level('Pro Easter Egg Hunt', + displayname='Pro ${GAME}', + gametype=EasterEggHuntGame, + settings={'Pro Mode': True}, + preview_texture_name='towerDPreview'), + Level( + name='Ninja Fight', # (unique id not seen by player) + displayname='${GAME}', # (readable name seen by player) + gametype=NinjaFightGame, + settings={'preset': 'regular'}, + preview_texture_name='courtyardPreview'), + Level(name='Pro Ninja Fight', + displayname='Pro ${GAME}', + gametype=NinjaFightGame, + settings={'preset': 'pro'}, + preview_texture_name='courtyardPreview') + ], + )) diff --git a/dist/ba_data/python/ba/_cloud.py b/dist/ba_data/python/ba/_cloud.py index 3e79537..e2149a3 100644 --- a/dist/ba_data/python/ba/_cloud.py +++ b/dist/ba_data/python/ba/_cloud.py @@ -56,6 +56,14 @@ class CloudSubsystem: ) -> None: ... + @overload + def send_message_cb( + self, + msg: bacommon.cloud.PingMessage, + on_response: Callable[[bacommon.cloud.PingResponse | Exception], None], + ) -> None: + ... + def send_message_cb( self, msg: Message, diff --git a/dist/ba_data/python/ba/_coopgame.py b/dist/ba_data/python/ba/_coopgame.py index 2e98781..f48fbc5 100644 --- a/dist/ba_data/python/ba/_coopgame.py +++ b/dist/ba_data/python/ba/_coopgame.py @@ -177,7 +177,7 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): def spawn_player_spaz(self, player: PlayerType, position: Sequence[float] = (0.0, 0.0, 0.0), - angle: float = None) -> PlayerSpaz: + angle: float | None = None) -> PlayerSpaz: """Spawn and wire up a standard player spaz.""" spaz = super().spawn_player_spaz(player, position, angle) diff --git a/dist/ba_data/python/ba/_gameactivity.py b/dist/ba_data/python/ba/_gameactivity.py index c432e17..0d22185 100644 --- a/dist/ba_data/python/ba/_gameactivity.py +++ b/dist/ba_data/python/ba/_gameactivity.py @@ -841,7 +841,7 @@ class GameActivity(Activity[PlayerType, TeamType]): def spawn_player_spaz(self, player: PlayerType, position: Sequence[float] = (0, 0, 0), - angle: float = None) -> PlayerSpaz: + angle: float | None = None) -> PlayerSpaz: """Create and wire up a ba.PlayerSpaz for the provided ba.Player.""" # pylint: disable=too-many-locals # pylint: disable=cyclic-import diff --git a/dist/ba_data/python/ba/_general.py b/dist/ba_data/python/ba/_general.py index 090e81a..d32b933 100644 --- a/dist/ba_data/python/ba/_general.py +++ b/dist/ba_data/python/ba/_general.py @@ -360,7 +360,7 @@ def _verify_object_death(wref: weakref.ref) -> None: print_active_refs(obj) -def storagename(suffix: str = None) -> str: +def storagename(suffix: str | None = None) -> str: """Generate a unique name for storing class data in shared places. Category: **General Utility Functions** diff --git a/dist/ba_data/python/ba/_hooks.py b/dist/ba_data/python/ba/_hooks.py index 58b90d2..ee45ca7 100644 --- a/dist/ba_data/python/ba/_hooks.py +++ b/dist/ba_data/python/ba/_hooks.py @@ -204,6 +204,12 @@ def no_game_circle_message() -> None: _ba.screenmessage(Lstr(resource='noGameCircleText'), color=(1, 0, 0)) +def google_play_purchases_not_available_message() -> None: + from ba._language import Lstr + _ba.screenmessage(Lstr(resource='googlePlayPurchasesNotAvailableText'), + color=(1, 0, 0)) + + def empty_call() -> None: pass diff --git a/dist/ba_data/python/ba/_language.py b/dist/ba_data/python/ba/_language.py index 8047a13..dbd740e 100644 --- a/dist/ba_data/python/ba/_language.py +++ b/dist/ba_data/python/ba/_language.py @@ -240,7 +240,7 @@ class LanguageSubsystem: def get_resource(self, resource: str, - fallback_resource: str = None, + fallback_resource: str | None = None, fallback_value: Any = None) -> Any: """Return a translation resource by name. diff --git a/dist/ba_data/python/ba/_level.py b/dist/ba_data/python/ba/_level.py index b3c0237..124f068 100644 --- a/dist/ba_data/python/ba/_level.py +++ b/dist/ba_data/python/ba/_level.py @@ -25,7 +25,7 @@ class Level: gametype: type[ba.GameActivity], settings: dict, preview_texture_name: str, - displayname: str = None): + displayname: str | None = None): self._name = name self._gametype = gametype self._settings = settings diff --git a/dist/ba_data/python/ba/_map.py b/dist/ba_data/python/ba/_map.py index db70c76..b8510a0 100644 --- a/dist/ba_data/python/ba/_map.py +++ b/dist/ba_data/python/ba/_map.py @@ -388,7 +388,8 @@ class Map(Actor): assert farthestpt is not None return tuple(farthestpt) - def get_flag_position(self, team_index: int = None) -> Sequence[float]: + def get_flag_position(self, + team_index: int | None = None) -> Sequence[float]: """Return a flag position on the map for the given team index. Pass None to get the default flag point. diff --git a/dist/ba_data/python/ba/_messages.py b/dist/ba_data/python/ba/_messages.py index 2e0a97f..1ea794c 100644 --- a/dist/ba_data/python/ba/_messages.py +++ b/dist/ba_data/python/ba/_messages.py @@ -236,17 +236,17 @@ class HitMessage: """ def __init__(self, - srcnode: ba.Node = None, - pos: Sequence[float] = None, - velocity: Sequence[float] = None, + srcnode: ba.Node | None = None, + pos: Sequence[float] | None = None, + velocity: Sequence[float] | None = None, magnitude: float = 1.0, velocity_magnitude: float = 0.0, radius: float = 1.0, - source_player: ba.Player = None, + source_player: ba.Player | None = None, kick_back: float = 1.0, - flat_damage: float = None, + flat_damage: float | None = None, hit_type: str = 'generic', - force_direction: Sequence[float] = None, + force_direction: Sequence[float] | None = None, hit_subtype: str = 'default'): """Instantiate a message with given values.""" diff --git a/dist/ba_data/python/ba/_meta.py b/dist/ba_data/python/ba/_meta.py index 45445a5..408be18 100644 --- a/dist/ba_data/python/ba/_meta.py +++ b/dist/ba_data/python/ba/_meta.py @@ -44,7 +44,7 @@ class MetadataSubsystem: """ def __init__(self) -> None: - self.metascan: ScanResults | None = None + self.scanresults: ScanResults | None = None self.extra_scan_dirs: list[str] = [] def on_app_running(self) -> None: @@ -58,7 +58,7 @@ class MetadataSubsystem: Should be called only once at launch.""" app = _ba.app - if self.metascan is not None: + if self.scanresults is not None: print('WARNING: meta scan run more than once.') pythondirs = ([app.python_directory_app, app.python_directory_user] + self.extra_scan_dirs) @@ -133,7 +133,7 @@ class MetadataSubsystem: def get_scan_results(self) -> ScanResults: """Return meta scan results; block if the scan is not yet complete.""" - if self.metascan is None: + if self.scanresults is None: print('WARNING: ba.meta.get_scan_results()' ' called before scan completed.' ' This can cause hitches.') @@ -141,12 +141,12 @@ class MetadataSubsystem: # Now wait a bit for the scan to complete. # Eventually error though if it doesn't. starttime = time.time() - while self.metascan is None: + while self.scanresults is None: time.sleep(0.05) if time.time() - starttime > 10.0: raise TimeoutError( 'timeout waiting for meta scan to complete.') - return self.metascan + return self.scanresults def get_game_types(self) -> list[type[ba.GameActivity]]: """Return available game types.""" @@ -206,7 +206,7 @@ class ScanThread(threading.Thread): # We also, however, immediately make results available. # This is because the game thread may be blocked waiting # for them so we can't push a call or we'd get deadlock. - _ba.app.meta.metascan = results + _ba.app.meta.scanresults = results class DirectoryScan: @@ -288,7 +288,7 @@ class DirectoryScan: # If we find a module requiring a different api version, warn # and ignore. - if required_api is not None and required_api <= CURRENT_API_VERSION: + if required_api is not None and required_api < CURRENT_API_VERSION: self.results.warnings += ( f'Warning: {subpath} requires api {required_api} but' f' we are running {CURRENT_API_VERSION}; ignoring module.\n') @@ -403,13 +403,13 @@ class DirectoryScan: if len(lines) > 1: self.results.warnings += ( 'Warning: ' + str(subpath) + - ': multiple "# ba_meta api require " lines found;' + ': multiple "# ba_meta require api " lines found;' ' ignoring module.\n') elif not lines and toplevel and meta_lines: # If we're a top-level module containing meta lines but - # no valid api require, complain. + # no valid "require api" line found, complain. self.results.warnings += ( 'Warning: ' + str(subpath) + - ': no valid "# ba_meta api require " line found;' + ': no valid "# ba_meta require api " line found;' ' ignoring module.\n') return None diff --git a/dist/ba_data/python/ba/_music.py b/dist/ba_data/python/ba/_music.py index 2944f29..4940e4c 100644 --- a/dist/ba_data/python/ba/_music.py +++ b/dist/ba_data/python/ba/_music.py @@ -211,7 +211,7 @@ class MusicSubsystem: return 'Mac' in uas if entry_type in ('musicFile', 'musicFolder'): return ('android' in uas - and _ba.android_get_external_storage_path() is not None) + and _ba.android_get_external_files_dir() is not None) if entry_type == 'default': return True return False @@ -273,7 +273,7 @@ class MusicSubsystem: musictype: MusicType | str | None, continuous: bool = False, mode: MusicPlayMode = MusicPlayMode.REGULAR, - testsoundtrack: dict[str, Any] = None) -> None: + testsoundtrack: dict[str, Any] | None = None) -> None: """Plays the requested music type/mode. For most cases, setmusic() is the proper call to use, which itself diff --git a/dist/ba_data/python/ba/_net.py b/dist/ba_data/python/ba/_net.py index 940b911..003b032 100644 --- a/dist/ba_data/python/ba/_net.py +++ b/dist/ba_data/python/ba/_net.py @@ -3,6 +3,7 @@ """Networking related functionality.""" from __future__ import annotations +import ssl import copy import threading import weakref @@ -29,14 +30,32 @@ class NetworkSubsystem: # as it is updated by a background thread. self.zone_pings_lock = threading.Lock() - # Region IDs mapped to average pings. This will remain empty + # Zone IDs mapped to average pings. This will remain empty # until enough pings have been run to be reasonably certain # that a nearby server has been pinged. self.zone_pings: dict[str, float] = {} + self._sslcontext: ssl.SSLContext | None = None + # For debugging. self.v1_test_log: str = '' self.v1_ctest_results: dict[int, str] = {} + self.server_time_offset_hours: float | None = None + + @property + def sslcontext(self) -> ssl.SSLContext: + """Create/return our shared SSLContext. + + This can be reused for all standard urllib requests/etc. + """ + # Note: I've run into older Android devices taking upwards of 1 second + # to put together a default SSLContext, so recycling one can definitely + # be a worthwhile optimization. This was suggested to me in this + # thread by one of Python's SSL maintainers: + # https://github.com/python/cpython/issues/94637 + if self._sslcontext is None: + self._sslcontext = ssl.create_default_context() + return self._sslcontext def get_ip_address_type(addr: str) -> socket.AddressFamily: @@ -108,30 +127,36 @@ class MasterServerCallThread(threading.Thread): self._callback(arg) def run(self) -> None: - # pylint: disable=too-many-branches, consider-using-with + # pylint: disable=consider-using-with import urllib.request import urllib.parse + import urllib.error import json - from efro.error import is_urllib_network_error + from efro.error import is_urllib_communication_error from ba import _general + + response_data: Any = None + url: str | None = None try: self._data = _general.utf8_all(self._data) _ba.set_thread_name('BA_ServerCallThread') if self._request_type == 'get': + url = (_ba.get_master_server_address() + '/' + self._request + + '?' + urllib.parse.urlencode(self._data)) response = urllib.request.urlopen( urllib.request.Request( - (_ba.get_master_server_address() + '/' + - self._request + '?' + - urllib.parse.urlencode(self._data)), None, - {'User-Agent': _ba.app.user_agent_string}), + url, None, {'User-Agent': _ba.app.user_agent_string}), + context=_ba.app.net.sslcontext, timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS) elif self._request_type == 'post': + url = _ba.get_master_server_address() + '/' + self._request response = urllib.request.urlopen( urllib.request.Request( - _ba.get_master_server_address() + '/' + self._request, + url, urllib.parse.urlencode(self._data).encode(), {'User-Agent': _ba.app.user_agent_string}), + context=_ba.app.net.sslcontext, timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS) else: raise TypeError('Invalid request_type: ' + self._request_type) @@ -151,29 +176,18 @@ class MasterServerCallThread(threading.Thread): raise TypeError(f'invalid responsetype: {self._response_type}') except Exception as exc: - do_print = False - response_data = None # Ignore common network errors; note unexpected ones. - if is_urllib_network_error(exc): - pass - elif (self._response_type == MasterServerResponseType.JSON - and isinstance(exc, json.decoder.JSONDecodeError)): - # FIXME: should handle this better; could mean either the - # server sent us bad data or it got corrupted along the way. - pass - else: - do_print = True - - if do_print: - # Any other error here is unexpected, - # so let's make a note of it, + if not is_urllib_communication_error(exc, url=url): print(f'Error in MasterServerCallThread' - f' (response-type={self._response_type},' + f' (url={url},' + f' response-type={self._response_type},' f' response-data={response_data}):') import traceback traceback.print_exc() + response_data = None + if self._callback is not None: _ba.pushcall(_general.Call(self._run_callback, response_data), from_other_thread=True) diff --git a/dist/ba_data/python/ba/_plugin.py b/dist/ba_data/python/ba/_plugin.py index d59cb63..9e7200f 100644 --- a/dist/ba_data/python/ba/_plugin.py +++ b/dist/ba_data/python/ba/_plugin.py @@ -66,6 +66,7 @@ class PluginSubsystem: def load_plugins(self) -> None: """(internal)""" from ba._general import getclass + from ba._language import Lstr # Note: the plugins we load is purely based on what's enabled # in the app config. Our meta-scan gives us a list of available @@ -76,15 +77,21 @@ class PluginSubsystem: assert isinstance(plugstates, dict) plugkeys: list[str] = sorted(key for key, val in plugstates.items() if val.get('enabled', False)) + disappeared_plugs: set[str] = set() for plugkey in plugkeys: try: cls = getclass(plugkey, Plugin) + except ModuleNotFoundError: + disappeared_plugs.add(plugkey) + continue except Exception as exc: _ba.playsound(_ba.getsound('error')) - # TODO: Lstr. - errstr = f"Error loading plugin class '{plugkey}': {exc}" - _ba.screenmessage(errstr, color=(1, 0, 0)) - _ba.log(errstr, to_server=False) + _ba.screenmessage(Lstr(resource='pluginClassLoadErrorText', + subs=[('${PLUGIN}', plugkey), + ('${ERROR}', str(exc))]), + color=(1, 0, 0)) + _ba.log(f"Error loading plugin class '{plugkey}': {exc}", + to_server=False) continue try: plugin = cls() @@ -93,10 +100,31 @@ class PluginSubsystem: except Exception as exc: from ba import _error _ba.playsound(_ba.getsound('error')) - # TODO: Lstr. - _ba.screenmessage(f"Error loading plugin: '{plugkey}': {exc}", + _ba.screenmessage(Lstr(resource='pluginInitErrorText', + subs=[('${PLUGIN}', plugkey), + ('${ERROR}', str(exc))]), color=(1, 0, 0)) - _error.print_exception(f"Error loading plugin: '{plugkey}'.") + _error.print_exception(f"Error initing plugin: '{plugkey}'.") + + # If plugins disappeared, let the user know gently and remove them + # from the config so we'll again let the user know if they later + # reappear. This makes it much smoother to switch between users + # or workspaces. + if disappeared_plugs: + _ba.playsound(_ba.getsound('shieldDown')) + _ba.screenmessage( + Lstr(resource='pluginsRemovedText', + subs=[('${NUM}', str(len(disappeared_plugs)))]), + color=(1, 1, 0), + ) + plugnames = ', '.join(disappeared_plugs) + _ba.log( + f'{len(disappeared_plugs)} plugin(s) no longer found:' + f' {plugnames}.', + to_server=False) + for goneplug in disappeared_plugs: + del _ba.app.config['Plugins'][goneplug] + _ba.app.config.commit() @dataclass diff --git a/dist/ba_data/python/ba/_profile.py b/dist/ba_data/python/ba/_profile.py index be0a831..308b6bf 100644 --- a/dist/ba_data/python/ba/_profile.py +++ b/dist/ba_data/python/ba/_profile.py @@ -50,7 +50,7 @@ def get_player_profile_icon(profilename: str) -> str: def get_player_profile_colors( profilename: str | None, - profiles: dict[str, dict[str, Any]] = None + profiles: dict[str, dict[str, Any]] | None = None ) -> tuple[tuple[float, float, float], tuple[float, float, float]]: """Given a profile, return colors for them.""" appconfig = _ba.app.config diff --git a/dist/ba_data/python/ba/_session.py b/dist/ba_data/python/ba/_session.py index f5d9775..5413499 100644 --- a/dist/ba_data/python/ba/_session.py +++ b/dist/ba_data/python/ba/_session.py @@ -72,8 +72,8 @@ class Session: def __init__(self, depsets: Sequence[ba.DependencySet], - team_names: Sequence[str] = None, - team_colors: Sequence[Sequence[float]] = None, + team_names: Sequence[str] | None = None, + team_colors: Sequence[Sequence[float]] | None = None, min_players: int = 1, max_players: int = 8): """Instantiate a session. diff --git a/dist/ba_data/python/ba/_stats.py b/dist/ba_data/python/ba/_stats.py index 62fc662..71f088d 100644 --- a/dist/ba_data/python/ba/_stats.py +++ b/dist/ba_data/python/ba/_stats.py @@ -314,11 +314,11 @@ class Stats: def player_scored(self, player: ba.Player, base_points: int = 1, - target: Sequence[float] = None, + target: Sequence[float] | None = None, kill: bool = False, - victim_player: ba.Player = None, + victim_player: ba.Player | None = None, scale: float = 1.0, - color: Sequence[float] = None, + color: Sequence[float] | None = None, title: str | ba.Lstr | None = None, screenmessage: bool = True, display: bool = True, @@ -422,7 +422,7 @@ class Stats: def player_was_killed(self, player: ba.Player, killed: bool = False, - killer: ba.Player = None) -> None: + killer: ba.Player | None = None) -> None: """Should be called when a player is killed.""" from ba._language import Lstr name = player.getname() diff --git a/dist/ba_data/python/ba/_store.py b/dist/ba_data/python/ba/_store.py index ec63803..81a2e21 100644 --- a/dist/ba_data/python/ba/_store.py +++ b/dist/ba_data/python/ba/_store.py @@ -398,7 +398,7 @@ def get_clean_price(price_string: str) -> str: return psubs.get(price_string, price_string) -def get_available_purchase_count(tab: str = None) -> int: +def get_available_purchase_count(tab: str | None = None) -> int: """(internal)""" try: if _ba.get_v1_account_state() != 'signed_in': diff --git a/dist/ba_data/python/ba/_teamgame.py b/dist/ba_data/python/ba/_teamgame.py index 4efe5c8..ccf9418 100644 --- a/dist/ba_data/python/ba/_teamgame.py +++ b/dist/ba_data/python/ba/_teamgame.py @@ -91,8 +91,8 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]): def spawn_player_spaz(self, player: PlayerType, - position: Sequence[float] = None, - angle: float = None) -> PlayerSpaz: + position: Sequence[float] | None = None, + angle: float | None = None) -> PlayerSpaz: """ Method override; spawns and wires up a standard ba.PlayerSpaz for a ba.Player. diff --git a/dist/ba_data/python/ba/_ui.py b/dist/ba_data/python/ba/_ui.py index 4bf2b70..8ec2e27 100644 --- a/dist/ba_data/python/ba/_ui.py +++ b/dist/ba_data/python/ba/_ui.py @@ -144,7 +144,7 @@ class UISubsystem: _ba.timer(1.0, _delay_kill, timetype=TimeType.REAL) self._main_menu_window = window - def clear_main_menu_window(self, transition: str = None) -> None: + def clear_main_menu_window(self, transition: str | None = None) -> None: """Clear any existing 'main' window with the provided transition.""" if self._main_menu_window: if transition is not None: diff --git a/dist/ba_data/python/ba/_workspace.py b/dist/ba_data/python/ba/_workspace.py index debbdb1..16c9fca 100644 --- a/dist/ba_data/python/ba/_workspace.py +++ b/dist/ba_data/python/ba/_workspace.py @@ -52,17 +52,17 @@ class WorkspaceSubsystem: daemon=True, ).start() - def _errmsg(self, msg: str | ba.Lstr) -> None: + def _errmsg(self, msg: ba.Lstr) -> None: _ba.screenmessage(msg, color=(1, 0, 0)) _ba.playsound(_ba.getsound('error')) - def _successmsg(self, msg: str | ba.Lstr) -> None: + def _successmsg(self, msg: ba.Lstr) -> None: _ba.screenmessage(msg, color=(0, 1, 0)) _ba.playsound(_ba.getsound('gunCocking')) def _set_active_workspace_bg(self, workspaceid: str, workspacename: str, on_completed: Callable[[], None]) -> None: - # pylint: disable=too-many-branches + from ba._language import Lstr class _SkipSyncError(RuntimeError): pass @@ -101,45 +101,42 @@ class WorkspaceSubsystem: break state.iteration += 1 - extras: list[str] = [] - # Hmm; let's not show deletes for now since currently lots of - # .pyc files get deleted. - if bool(False): - if state.total_deletes: - extras.append(f'{state.total_deletes} files deleted') - if state.total_downloads: - extras.append(f'{state.total_downloads} files downloaded') - if state.total_up_to_date: - extras.append(f'{state.total_up_to_date} files up-to-date') - # Actually let's try with none of this; seems a bit excessive. - if bool(False) and extras: - extras_s = '\n' + ', '.join(extras) + '.' - else: - extras_s = '' - _ba.pushcall(tpartial(self._successmsg, - f'{workspacename} activated.{extras_s}'), - from_other_thread=True) + _ba.pushcall( + tpartial( + self._successmsg, + Lstr(resource='activatedText', + subs=[('${THING}', workspacename)]), + ), + from_other_thread=True, + ) except _SkipSyncError: - _ba.pushcall(tpartial( - self._errmsg, f'Can\'t sync {workspacename}' - f'. Reusing previous synced version.'), - from_other_thread=True) + _ba.pushcall( + tpartial( + self._errmsg, + Lstr(resource='workspaceSyncReuseText', + subs=[('${WORKSPACE}', workspacename)])), + from_other_thread=True, + ) except CleanError as exc: # Avoid reusing existing if we fail in the middle; could # be in wonky state. set_path = False - _ba.pushcall(tpartial(self._errmsg, str(exc)), + _ba.pushcall(tpartial(self._errmsg, Lstr(value=str(exc))), from_other_thread=True) except Exception: # Ditto. set_path = False - logging.exception('Error syncing workspace.') - # TODO: Lstr. - _ba.pushcall(tpartial( - self._errmsg, 'Error syncing workspace. See log for details.'), - from_other_thread=True) + logging.exception("Error syncing workspace '%s'.", workspacename) + _ba.pushcall( + tpartial( + self._errmsg, + Lstr(resource='workspaceSyncErrorText', + subs=[('${WORKSPACE}', workspacename)]), + ), + from_other_thread=True, + ) if set_path and wspath.is_dir(): # Add to Python paths and also to list of stuff to be scanned diff --git a/dist/ba_data/python/ba/modutils.py b/dist/ba_data/python/ba/modutils.py index e5d1f74..c081d79 100644 --- a/dist/ba_data/python/ba/modutils.py +++ b/dist/ba_data/python/ba/modutils.py @@ -17,23 +17,29 @@ def get_human_readable_user_scripts_path() -> str: This is NOT a valid filesystem path; may be something like "(SD Card)". """ - from ba import _language app = _ba.app path: str | None = app.python_directory_user if path is None: return '' - # On newer versions of android, the user's external storage dir is probably - # only visible to the user's processes and thus not really useful printed - # in its entirety; lets print it as /myfilepath. + # These days, on Android, we use getExternalFilesDir() as the base of our + # app's user-scripts dir, which gives us paths like: + # /storage/emulated/0/Android/data/net.froemling.bombsquad/files + # Userspace apps tend to show that as: + # Android/data/net.froemling.bombsquad/files + # We'd like to display it that way, but I'm not sure if there's a clean + # way to get the root of the external storage area (/storage/emulated/0) + # so that we could strip it off. There is + # Environment.getExternalStorageDirectory() but that is deprecated. + # So for now let's just be conservative and trim off recognized prefixes + # and show the whole ugly path as a fallback. + # Note that we used to use externalStorageText resource but gonna try + # without it for now. (simply 'foo' instead of /foo). if app.platform == 'android': - ext_storage_path: str | None = ( - _ba.android_get_external_storage_path()) - if (ext_storage_path is not None - and app.python_directory_user.startswith(ext_storage_path)): - path = ('<' + - _language.Lstr(resource='externalStorageText').evaluate() + - '>' + app.python_directory_user[len(ext_storage_path):]) + for pre in ['/storage/emulated/0/']: + if path.startswith(pre): + path = path.removeprefix(pre) + break return path diff --git a/dist/ba_data/python/bacommon/bacloud.py b/dist/ba_data/python/bacommon/bacloud.py index 99c9be3..5ce38d3 100644 --- a/dist/ba_data/python/bacommon/bacloud.py +++ b/dist/ba_data/python/bacommon/bacloud.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: # Version is sent to the master-server with all commands. Can be incremented # if we need to change behavior server-side to go along with client changes. -BACLOUD_VERSION = 6 +BACLOUD_VERSION = 7 @ioprepped diff --git a/dist/ba_data/python/bacommon/cloud.py b/dist/ba_data/python/bacommon/cloud.py index 197a1de..34e9cca 100644 --- a/dist/ba_data/python/bacommon/cloud.py +++ b/dist/ba_data/python/bacommon/cloud.py @@ -76,6 +76,22 @@ class LoginProxyCompleteMessage(Message): proxyid: Annotated[str, IOAttrs('p')] +@ioprepped +@dataclass +class PingMessage(Message): + """Standard ping.""" + + @classmethod + def get_response_types(cls) -> list[type[Response]]: + return [PingResponse] + + +@ioprepped +@dataclass +class PingResponse(Response): + """pong.""" + + @ioprepped @dataclass class TestMessage(Message): diff --git a/dist/ba_data/python/bacommon/net.py b/dist/ba_data/python/bacommon/net.py index 3409ba6..a22a9c5 100644 --- a/dist/ba_data/python/bacommon/net.py +++ b/dist/ba_data/python/bacommon/net.py @@ -4,6 +4,7 @@ from __future__ import annotations +import datetime from typing import TYPE_CHECKING, Any, Annotated from dataclasses import dataclass, field @@ -27,6 +28,9 @@ class ServerNodeEntry: class ServerNodeQueryResponse: """A response to a query about server-nodes.""" + # The current utc time on the master server. + time: Annotated[datetime.datetime, IOAttrs('t')] + # If present, something went wrong, and this describes it. error: Annotated[str | None, IOAttrs('e', store_default=False)] = None diff --git a/dist/ba_data/python/bacommon/transfer.py b/dist/ba_data/python/bacommon/transfer.py index 03ff6bd..6504287 100644 --- a/dist/ba_data/python/bacommon/transfer.py +++ b/dist/ba_data/python/bacommon/transfer.py @@ -5,13 +5,14 @@ from __future__ import annotations import os +from pathlib import Path from dataclasses import dataclass from typing import TYPE_CHECKING, Annotated from efro.dataclassio import ioprepped, IOAttrs if TYPE_CHECKING: - from pathlib import Path + pass @ioprepped @@ -40,15 +41,17 @@ class DirectoryManifest: paths: list[str] = [] if path.is_dir(): - # Build the full list of package-relative paths. + # Build the full list of relative paths. for basename, _dirnames, filenames in os.walk(path): for filename in filenames: fullname = os.path.join(basename, filename) assert fullname.startswith(pathstr) - paths.append(fullname[len(pathstr) + 1:]) + # Make sure we end up with forward slashes no matter + # what the os.* stuff above here was using. + paths.append(Path(fullname[len(pathstr) + 1:]).as_posix()) elif path.exists(): # Just return a single file entry if path is not a dir. - paths.append(pathstr) + paths.append(path.as_posix()) def _get_file_info(filepath: str) -> tuple[str, DirectoryManifestFile]: sha = hashlib.sha256() @@ -70,6 +73,18 @@ class DirectoryManifest: with ThreadPoolExecutor(max_workers=cpus) as executor: return cls(files=dict(executor.map(_get_file_info, paths))) + def validate(self) -> None: + """Log any odd data in the manifest; for debugging.""" + import logging + for fpath, _fentry in self.files.items(): + # We want to be dealing in only forward slashes; make sure + # that's the case (wondering if we'll ever see backslashes + # for escape purposes). + if '\\' in fpath: + logging.exception("Found unusual path in manifest: '%s'.", + fpath) + break # 1 error is enough for now. + @classmethod def get_empty_hash(cls) -> str: """Return the hash for an empty file.""" diff --git a/dist/ba_data/python/bastd/actor/bomb.py b/dist/ba_data/python/bastd/actor/bomb.py index 96e5aa7..626a25a 100644 --- a/dist/ba_data/python/bastd/actor/bomb.py +++ b/dist/ba_data/python/bastd/actor/bomb.py @@ -327,7 +327,7 @@ class Blast(ba.Actor): velocity: Sequence[float] = (0.0, 0.0, 0.0), blast_radius: float = 2.0, blast_type: str = 'normal', - source_player: ba.Player = None, + source_player: ba.Player | None = None, hit_type: str = 'explosion', hit_subtype: str = 'normal'): """Instantiate with given values.""" @@ -657,8 +657,8 @@ class Bomb(ba.Actor): bomb_type: str = 'normal', blast_radius: float = 2.0, bomb_scale: float = 1.0, - source_player: ba.Player = None, - owner: ba.Node = None): + source_player: ba.Player | None = None, + owner: ba.Node | None = None): """Create a new Bomb. bomb_type can be 'ice','impact','land_mine','normal','sticky', or diff --git a/dist/ba_data/python/bastd/actor/controlsguide.py b/dist/ba_data/python/bastd/actor/controlsguide.py index 827c260..92a1115 100644 --- a/dist/ba_data/python/bastd/actor/controlsguide.py +++ b/dist/ba_data/python/bastd/actor/controlsguide.py @@ -27,7 +27,7 @@ class ControlsGuide(ba.Actor): position: tuple[float, float] = (390.0, 120.0), scale: float = 1.0, delay: float = 0.0, - lifespan: float = None, + lifespan: float | None = None, bright: bool = False): """Instantiate an overlay. diff --git a/dist/ba_data/python/bastd/actor/flag.py b/dist/ba_data/python/bastd/actor/flag.py index ff5d210..deaa620 100644 --- a/dist/ba_data/python/bastd/actor/flag.py +++ b/dist/ba_data/python/bastd/actor/flag.py @@ -167,9 +167,9 @@ class Flag(ba.Actor): def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0), color: Sequence[float] = (1.0, 1.0, 1.0), - materials: Sequence[ba.Material] = None, + materials: Sequence[ba.Material] | None = None, touchable: bool = True, - dropped_timeout: int = None): + dropped_timeout: int | None = None): """Instantiate a flag. If 'touchable' is False, the flag will only touch terrain; diff --git a/dist/ba_data/python/bastd/actor/image.py b/dist/ba_data/python/bastd/actor/image.py index 383926f..71f23ef 100644 --- a/dist/ba_data/python/bastd/actor/image.py +++ b/dist/ba_data/python/bastd/actor/image.py @@ -40,9 +40,9 @@ class Image(ba.Actor): attach: Attach = Attach.CENTER, color: Sequence[float] = (1.0, 1.0, 1.0, 1.0), scale: tuple[float, float] = (100.0, 100.0), - transition_out_delay: float = None, - model_opaque: ba.Model = None, - model_transparent: ba.Model = None, + transition_out_delay: float | None = None, + model_opaque: ba.Model | None = None, + model_transparent: ba.Model | None = None, vr_depth: float = 0.0, host_only: bool = False, front: bool = False): diff --git a/dist/ba_data/python/bastd/actor/onscreencountdown.py b/dist/ba_data/python/bastd/actor/onscreencountdown.py index 309ccb7..618c0c4 100644 --- a/dist/ba_data/python/bastd/actor/onscreencountdown.py +++ b/dist/ba_data/python/bastd/actor/onscreencountdown.py @@ -20,7 +20,9 @@ class OnScreenCountdown(ba.Actor): Useful for time-based games that count down to zero. """ - def __init__(self, duration: int, endcall: Callable[[], Any] = None): + def __init__(self, + duration: int, + endcall: Callable[[], Any] | None = None): """Duration is provided in seconds.""" super().__init__() self._timeremaining = duration @@ -73,7 +75,7 @@ class OnScreenCountdown(ba.Actor): # Release callbacks/refs. self._endcall = None - def _update(self, forcevalue: int = None) -> None: + def _update(self, forcevalue: int | None = None) -> None: if forcevalue is not None: tval = forcevalue else: diff --git a/dist/ba_data/python/bastd/actor/powerupbox.py b/dist/ba_data/python/bastd/actor/powerupbox.py index fe01304..868667c 100644 --- a/dist/ba_data/python/bastd/actor/powerupbox.py +++ b/dist/ba_data/python/bastd/actor/powerupbox.py @@ -137,8 +137,8 @@ class PowerupBoxFactory: self._powerupdist.append(powerup) def get_random_powerup_type(self, - forcetype: str = None, - excludetypes: list[str] = None) -> str: + forcetype: str | None = None, + excludetypes: list[str] | None = None) -> str: """Returns a random powerup type (string). See ba.Powerup.poweruptype for available type values. diff --git a/dist/ba_data/python/bastd/actor/scoreboard.py b/dist/ba_data/python/bastd/actor/scoreboard.py index 16b996e..d72e883 100644 --- a/dist/ba_data/python/bastd/actor/scoreboard.py +++ b/dist/ba_data/python/bastd/actor/scoreboard.py @@ -241,7 +241,7 @@ class _Entry: def set_value(self, score: float, - max_score: float = None, + max_score: float | None = None, countdown: bool = False, flash: bool = True, show_value: bool = True) -> None: @@ -327,7 +327,7 @@ class Scoreboard: _ENTRYSTORENAME = ba.storagename('entry') - def __init__(self, label: ba.Lstr = None, score_split: float = 0.7): + def __init__(self, label: ba.Lstr | None = None, score_split: float = 0.7): """Instantiate a scoreboard. Label can be something like 'points' and will @@ -356,7 +356,7 @@ class Scoreboard: def set_team_value(self, team: ba.Team, score: float, - max_score: float = None, + max_score: float | None = None, countdown: bool = False, flash: bool = True, show_value: bool = True) -> None: diff --git a/dist/ba_data/python/bastd/actor/spawner.py b/dist/ba_data/python/bastd/actor/spawner.py index 16693bd..41e2c33 100644 --- a/dist/ba_data/python/bastd/actor/spawner.py +++ b/dist/ba_data/python/bastd/actor/spawner.py @@ -54,7 +54,7 @@ class Spawner: pt: Sequence[float] = (0, 0, 0), # pylint: disable=invalid-name spawn_time: float = 1.0, send_spawn_message: bool = True, - spawn_callback: Callable[[], Any] = None): + spawn_callback: Callable[[], Any] | None = None): """Instantiate a Spawner. Requires some custom data, a position, diff --git a/dist/ba_data/python/bastd/actor/spaz.py b/dist/ba_data/python/bastd/actor/spaz.py index 12e60b5..5362c2f 100644 --- a/dist/ba_data/python/bastd/actor/spaz.py +++ b/dist/ba_data/python/bastd/actor/spaz.py @@ -67,7 +67,7 @@ class Spaz(ba.Actor): color: Sequence[float] = (1.0, 1.0, 1.0), highlight: Sequence[float] = (0.5, 0.5, 0.5), character: str = 'Spaz', - source_player: ba.Player = None, + source_player: ba.Player | None = None, start_invincible: bool = True, can_accept_powerups: bool = True, powerups_expire: bool = False, @@ -177,6 +177,7 @@ class Spaz(ba.Actor): self._bomb_wear_off_timer: ba.Timer | None = None self._bomb_wear_off_flash_timer: ba.Timer | None = None self._multi_bomb_wear_off_timer: ba.Timer | None = None + self._multi_bomb_wear_off_flash_timer: ba.Timer | None = None self.bomb_count = self.default_bomb_count self._max_bomb_count = self.default_bomb_count self.bomb_type_default = self.default_bomb_type @@ -605,6 +606,7 @@ class Spaz(ba.Actor): """ assert self.node self.node.boxing_gloves = True + self._has_boxing_gloves = True if self._demo_mode: # Preserve old behavior. self._punch_power_scale = 1.7 self._punch_cooldown = 300 @@ -704,7 +706,7 @@ class Spaz(ba.Actor): self.node.mini_billboard_1_start_time = t_ms self.node.mini_billboard_1_end_time = ( t_ms + POWERUP_WEAR_OFF_TIME) - self._multi_bomb_wear_off_timer = (ba.Timer( + self._multi_bomb_wear_off_flash_timer = (ba.Timer( (POWERUP_WEAR_OFF_TIME - 2000), ba.WeakCall(self._multi_bomb_wear_off_flash), timeformat=ba.TimeFormat.MILLISECONDS)) @@ -753,7 +755,6 @@ class Spaz(ba.Actor): ba.WeakCall(self._bomb_wear_off), timeformat=ba.TimeFormat.MILLISECONDS)) elif msg.poweruptype == 'punch': - self._has_boxing_gloves = True tex = PowerupBoxFactory.get().tex_punch self._flash_billboard(tex) self.equip_boxing_gloves() @@ -1266,7 +1267,7 @@ class Spaz(ba.Actor): else: self.node.counter_text = '' - def curse_explode(self, source_player: ba.Player = None) -> None: + def curse_explode(self, source_player: ba.Player | None = None) -> None: """Explode the poor spaz spectacularly.""" if self._cursed and self.node: self.shatter(extreme=True) diff --git a/dist/ba_data/python/bastd/actor/spazbot.py b/dist/ba_data/python/bastd/actor/spazbot.py index 2204216..cccf070 100644 --- a/dist/ba_data/python/bastd/actor/spazbot.py +++ b/dist/ba_data/python/bastd/actor/spazbot.py @@ -888,11 +888,12 @@ class SpazBotSet: def __del__(self) -> None: self.clear() - def spawn_bot(self, - bot_type: type[SpazBot], - pos: Sequence[float], - spawn_time: float = 3.0, - on_spawn_call: Callable[[SpazBot], Any] = None) -> None: + def spawn_bot( + self, + bot_type: type[SpazBot], + pos: Sequence[float], + spawn_time: float = 3.0, + on_spawn_call: Callable[[SpazBot], Any] | None = None) -> None: """Spawn a bot from this set.""" from bastd.actor import spawner spawner.Spawner(pt=pos, diff --git a/dist/ba_data/python/bastd/actor/text.py b/dist/ba_data/python/bastd/actor/text.py index 992ddc4..fc56076 100644 --- a/dist/ba_data/python/bastd/actor/text.py +++ b/dist/ba_data/python/bastd/actor/text.py @@ -60,8 +60,8 @@ class Text(ba.Actor): v_attach: VAttach = VAttach.CENTER, h_attach: HAttach = HAttach.CENTER, scale: float = 1.0, - transition_out_delay: float = None, - maxwidth: float = None, + transition_out_delay: float | None = None, + maxwidth: float | None = None, shadow: float = 0.5, flatness: float = 0.0, vr_depth: float = 0.0, diff --git a/dist/ba_data/python/bastd/actor/zoomtext.py b/dist/ba_data/python/bastd/actor/zoomtext.py index c91df93..708d943 100644 --- a/dist/ba_data/python/bastd/actor/zoomtext.py +++ b/dist/ba_data/python/bastd/actor/zoomtext.py @@ -24,9 +24,9 @@ class ZoomText(ba.Actor): def __init__(self, text: str | ba.Lstr, position: tuple[float, float] = (0.0, 0.0), - shiftposition: tuple[float, float] = None, - shiftdelay: float = None, - lifespan: float = None, + shiftposition: tuple[float, float] | None = None, + shiftdelay: float | None = None, + lifespan: float | None = None, flash: bool = True, trail: bool = True, h_align: str = 'center', @@ -36,7 +36,7 @@ class ZoomText(ba.Actor): scale: float = 1.0, project_scale: float = 1.0, tilt_translate: float = 0.0, - maxwidth: float = None): + maxwidth: float | None = None): # pylint: disable=too-many-locals super().__init__() self._dying = False diff --git a/dist/ba_data/python/bastd/game/capturetheflag.py b/dist/ba_data/python/bastd/game/capturetheflag.py index 781ce8c..de3fb7e 100644 --- a/dist/ba_data/python/bastd/game/capturetheflag.py +++ b/dist/ba_data/python/bastd/game/capturetheflag.py @@ -492,8 +492,8 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): def spawn_player_spaz(self, player: Player, - position: Sequence[float] = None, - angle: float = None) -> PlayerSpaz: + position: Sequence[float] | None = None, + angle: float | None = None) -> PlayerSpaz: """Intercept new spazzes and add our team material for them.""" spaz = super().spawn_player_spaz(player, position, angle) player = spaz.getplayer(Player, True) diff --git a/dist/ba_data/python/bastd/game/chosenone.py b/dist/ba_data/python/bastd/game/chosenone.py index ba99d35..6f75748 100644 --- a/dist/ba_data/python/bastd/game/chosenone.py +++ b/dist/ba_data/python/bastd/game/chosenone.py @@ -137,9 +137,6 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]): self.setup_standard_powerup_drops() self._flag_spawn_pos = self.map.get_flag_position(None) Flag.project_stand(self._flag_spawn_pos) - self._set_chosen_one_player(None) - - pos = self._flag_spawn_pos ba.timer(1.0, call=self._tick, repeat=True) mat = self._reset_region_material = ba.Material() @@ -156,14 +153,20 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]): ), ) - self._reset_region = ba.newnode('region', - attrs={ - 'position': (pos[0], pos[1] + 0.75, - pos[2]), - 'scale': (0.5, 0.5, 0.5), - 'type': 'sphere', - 'materials': [mat] - }) + self._set_chosen_one_player(None) + + def _create_reset_region(self) -> None: + assert self._reset_region_material is not None + assert self._flag_spawn_pos is not None + pos = self._flag_spawn_pos + self._reset_region = ba.newnode( + 'region', + attrs={ + 'position': (pos[0], pos[1] + 0.75, pos[2]), + 'scale': (0.5, 0.5, 0.5), + 'type': 'sphere', + 'materials': [self._reset_region_material] + }) def _get_chosen_one_player(self) -> Player | None: # Should never return invalid references; return None in that case. @@ -176,14 +179,14 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]): if self._get_chosen_one_player() is not None: return - # Attempt to get a Player controlling a Spaz that we hit. + # Attempt to get a Actor that we hit. try: - player = ba.getcollision().opposingnode.getdelegate( - PlayerSpaz, True).getplayer(Player, True) + spaz = ba.getcollision().opposingnode.getdelegate(PlayerSpaz, True) + player = spaz.getplayer(Player, True) except ba.NotFoundError: return - if player.is_alive(): + if spaz.is_alive(): self._set_chosen_one_player(player) def _flash_flag_spawn(self) -> None: @@ -278,6 +281,10 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]): # Also an extra momentary flash. self._flash_flag_spawn() + + # Re-create our flag region in case if someone is waiting for + # flag right there: + self._create_reset_region() else: if player.actor: self._flag = None diff --git a/dist/ba_data/python/bastd/game/football.py b/dist/ba_data/python/bastd/game/football.py index ff1b735..2bf0bd9 100644 --- a/dist/ba_data/python/bastd/game/football.py +++ b/dist/ba_data/python/bastd/game/football.py @@ -596,7 +596,9 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): if closest_bot is not None: closest_bot.target_flag = self._flag - def _drop_powerup(self, index: int, poweruptype: str = None) -> None: + def _drop_powerup(self, + index: int, + poweruptype: str | None = None) -> None: if poweruptype is None: poweruptype = (PowerupBoxFactory.get().get_random_powerup_type( excludetypes=self._exclude_powerups)) @@ -610,7 +612,7 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): def _drop_powerups(self, standard_points: bool = False, - poweruptype: str = None) -> None: + poweruptype: str | None = None) -> None: """Generic powerup drop.""" if standard_points: spawnpoints = self.map.powerup_spawn_points diff --git a/dist/ba_data/python/bastd/game/onslaught.py b/dist/ba_data/python/bastd/game/onslaught.py index 2664858..9064b79 100644 --- a/dist/ba_data/python/bastd/game/onslaught.py +++ b/dist/ba_data/python/bastd/game/onslaught.py @@ -5,6 +5,9 @@ # Yes this is a long one.. # pylint: disable=too-many-lines +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + from __future__ import annotations import math @@ -690,7 +693,9 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): del player, bomb # Unused. self._player_has_dropped_bomb = True - def _drop_powerup(self, index: int, poweruptype: str = None) -> None: + def _drop_powerup(self, + index: int, + poweruptype: str | None = None) -> None: poweruptype = (PowerupBoxFactory.get().get_random_powerup_type( forcetype=poweruptype, excludetypes=self._excluded_powerups)) PowerupBox(position=self.map.powerup_spawn_points[index], @@ -703,7 +708,7 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): def _drop_powerups(self, standard_points: bool = False, - poweruptype: str = None) -> None: + poweruptype: str | None = None) -> None: """Generic powerup drop.""" if standard_points: points = self.map.powerup_spawn_points diff --git a/dist/ba_data/python/bastd/game/runaround.py b/dist/ba_data/python/bastd/game/runaround.py index 58ae628..b80f3ec 100644 --- a/dist/ba_data/python/bastd/game/runaround.py +++ b/dist/ba_data/python/bastd/game/runaround.py @@ -5,6 +5,9 @@ # We wear the cone of shame. # pylint: disable=too-many-lines +# ba_meta require api 7 +# (see https://ballistica.net/wiki/meta-tag-system) + from __future__ import annotations import random @@ -497,7 +500,9 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): del player # Unused. self._player_has_picked_up_powerup = True - def _drop_powerup(self, index: int, poweruptype: str = None) -> None: + def _drop_powerup(self, + index: int, + poweruptype: str | None = None) -> None: if poweruptype is None: poweruptype = (PowerupBoxFactory.get().get_random_powerup_type( excludetypes=self._exclude_powerups)) @@ -509,7 +514,7 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): def _drop_powerups(self, standard_points: bool = False, - force_first: str = None) -> None: + force_first: str | None = None) -> None: """Generic powerup drop.""" # If its been a minute since our last wave finished emerging, stop diff --git a/dist/ba_data/python/bastd/game/thelaststand.py b/dist/ba_data/python/bastd/game/thelaststand.py index 0d40c0b..3cf84ae 100644 --- a/dist/ba_data/python/bastd/game/thelaststand.py +++ b/dist/ba_data/python/bastd/game/thelaststand.py @@ -132,7 +132,9 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]): self._bot_update_timer = ba.Timer(self._bot_update_interval, ba.WeakCall(self._update_bots)) - def _drop_powerup(self, index: int, poweruptype: str = None) -> None: + def _drop_powerup(self, + index: int, + poweruptype: str | None = None) -> None: if poweruptype is None: poweruptype = (PowerupBoxFactory.get().get_random_powerup_type( excludetypes=self._excludepowerups)) @@ -146,7 +148,7 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]): def _drop_powerups(self, standard_points: bool = False, - force_first: str = None) -> None: + force_first: str | None = None) -> None: """Generic powerup drop.""" from bastd.actor import powerupbox if standard_points: diff --git a/dist/ba_data/python/bastd/mainmenu.py b/dist/ba_data/python/bastd/mainmenu.py index d883688..d5afa32 100644 --- a/dist/ba_data/python/bastd/mainmenu.py +++ b/dist/ba_data/python/bastd/mainmenu.py @@ -767,7 +767,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): y: float, scale: float, delay: float, - custom_texture: str = None, + custom_texture: str | None = None, jitter_scale: float = 1.0, rotate: float = 0.0, vr_depth_offset: float = 0.0) -> None: diff --git a/dist/ba_data/python/bastd/tutorial.py b/dist/ba_data/python/bastd/tutorial.py index 4d424bb..0cb2c7b 100644 --- a/dist/ba_data/python/bastd/tutorial.py +++ b/dist/ba_data/python/bastd/tutorial.py @@ -178,7 +178,7 @@ class Team(ba.Team[Player]): class TutorialActivity(ba.Activity[Player, Team]): - def __init__(self, settings: dict = None): + def __init__(self, settings: dict | None = None): from bastd.maps import Rampage if settings is None: settings = {} @@ -492,7 +492,7 @@ class TutorialActivity(ba.Activity[Player, Team]): position: Sequence[float], color: Sequence[float] = (1.0, 1.0, 1.0), make_current: bool = False, - relative_to: int = None, + relative_to: int | None = None, name: str | ba.Lstr = '', flash: bool = True, angle: float = 0.0): @@ -550,7 +550,7 @@ class TutorialActivity(ba.Activity[Player, Team]): position: Sequence[float], color: Sequence[float] = (1.0, 1.0, 1.0), make_current: bool = False, - relative_to: int = None): + relative_to: int | None = None): self._position = position self._relative_to = relative_to @@ -761,7 +761,7 @@ class TutorialActivity(ba.Activity[Player, Team]): class PrintPos: - def __init__(self, spaz_num: int = None): + def __init__(self, spaz_num: int | None = None): self._spaz_num = spaz_num def run(self, a: TutorialActivity) -> None: @@ -787,7 +787,7 @@ class TutorialActivity(ba.Activity[Player, Team]): def __init__(self, celebrate_type: str = 'both', - spaz_num: int = None, + spaz_num: int | None = None, duration: int = 1000): self._spaz_num = spaz_num self._celebrate_type = celebrate_type diff --git a/dist/ba_data/python/bastd/ui/account/__init__.py b/dist/ba_data/python/bastd/ui/account/__init__.py index 9b56244..1ce8461 100644 --- a/dist/ba_data/python/bastd/ui/account/__init__.py +++ b/dist/ba_data/python/bastd/ui/account/__init__.py @@ -8,7 +8,7 @@ import _ba import ba -def show_sign_in_prompt(account_type: str = None) -> None: +def show_sign_in_prompt(account_type: str | None = None) -> None: """Bring up a prompt telling the user they must sign in.""" from bastd.ui.confirm import ConfirmWindow from bastd.ui.account import settings diff --git a/dist/ba_data/python/bastd/ui/account/link.py b/dist/ba_data/python/bastd/ui/account/link.py index 8c026a0..f92c9da 100644 --- a/dist/ba_data/python/bastd/ui/account/link.py +++ b/dist/ba_data/python/bastd/ui/account/link.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: class AccountLinkWindow(ba.Window): """Window for linking accounts.""" - def __init__(self, origin_widget: ba.Widget = None): + def __init__(self, origin_widget: ba.Widget | None = None): scale_origin: tuple[float, float] | None if origin_widget is not None: self._transition_out = 'out_scale' diff --git a/dist/ba_data/python/bastd/ui/account/settings.py b/dist/ba_data/python/bastd/ui/account/settings.py index f5b180f..cdfb1c7 100644 --- a/dist/ba_data/python/bastd/ui/account/settings.py +++ b/dist/ba_data/python/bastd/ui/account/settings.py @@ -21,7 +21,7 @@ class AccountSettingsWindow(ba.Window): def __init__(self, transition: str = 'in_right', modal: bool = False, - origin_widget: ba.Widget = None, + origin_widget: ba.Widget | None = None, close_once_signed_in: bool = False): # pylint: disable=too-many-statements diff --git a/dist/ba_data/python/bastd/ui/account/unlink.py b/dist/ba_data/python/bastd/ui/account/unlink.py index 8e9955b..6cff2c4 100644 --- a/dist/ba_data/python/bastd/ui/account/unlink.py +++ b/dist/ba_data/python/bastd/ui/account/unlink.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: class AccountUnlinkWindow(ba.Window): """A window to kick off account unlinks.""" - def __init__(self, origin_widget: ba.Widget = None): + def __init__(self, origin_widget: ba.Widget | None = None): scale_origin: tuple[float, float] | None if origin_widget is not None: self._transition_out = 'out_scale' diff --git a/dist/ba_data/python/bastd/ui/account/viewer.py b/dist/ba_data/python/bastd/ui/account/viewer.py index 549207e..da5fb74 100644 --- a/dist/ba_data/python/bastd/ui/account/viewer.py +++ b/dist/ba_data/python/bastd/ui/account/viewer.py @@ -19,9 +19,9 @@ class AccountViewerWindow(popup.PopupWindow): def __init__(self, account_id: str, - profile_id: str = None, + profile_id: str | None = None, position: tuple[float, float] = (0.0, 0.0), - scale: float = None, + scale: float | None = None, offset: tuple[float, float] = (0.0, 0.0)): from ba.internal import is_browser_likely_available, master_server_get diff --git a/dist/ba_data/python/bastd/ui/achievements.py b/dist/ba_data/python/bastd/ui/achievements.py index 5ca7d51..c5bc5d2 100644 --- a/dist/ba_data/python/bastd/ui/achievements.py +++ b/dist/ba_data/python/bastd/ui/achievements.py @@ -16,7 +16,9 @@ if TYPE_CHECKING: class AchievementsWindow(popup.PopupWindow): """Popup window to view achievements.""" - def __init__(self, position: tuple[float, float], scale: float = None): + def __init__(self, + position: tuple[float, float], + scale: float | None = None): # pylint: disable=too-many-locals uiscale = ba.app.ui.uiscale if scale is None: diff --git a/dist/ba_data/python/bastd/ui/characterpicker.py b/dist/ba_data/python/bastd/ui/characterpicker.py index 9fc9bac..353ee38 100644 --- a/dist/ba_data/python/bastd/ui/characterpicker.py +++ b/dist/ba_data/python/bastd/ui/characterpicker.py @@ -22,11 +22,11 @@ class CharacterPicker(popup.PopupWindow): parent: ba.Widget, position: tuple[float, float] = (0.0, 0.0), delegate: Any = None, - scale: float = None, + scale: float | None = None, offset: tuple[float, float] = (0.0, 0.0), tint_color: Sequence[float] = (1.0, 1.0, 1.0), tint2_color: Sequence[float] = (1.0, 1.0, 1.0), - selected_character: str = None): + selected_character: str | None = None): # pylint: disable=too-many-locals from bastd.actor import spazappearance del parent # unused here diff --git a/dist/ba_data/python/bastd/ui/colorpicker.py b/dist/ba_data/python/bastd/ui/colorpicker.py index 1d7faa0..13384e8 100644 --- a/dist/ba_data/python/bastd/ui/colorpicker.py +++ b/dist/ba_data/python/bastd/ui/colorpicker.py @@ -24,7 +24,7 @@ class ColorPicker(PopupWindow): position: tuple[float, float], initial_color: Sequence[float] = (1.0, 1.0, 1.0), delegate: Any = None, - scale: float = None, + scale: float | None = None, offset: tuple[float, float] = (0.0, 0.0), tag: Any = ''): # pylint: disable=too-many-locals @@ -162,7 +162,7 @@ class ColorPickerExact(PopupWindow): position: tuple[float, float], initial_color: Sequence[float] = (1.0, 1.0, 1.0), delegate: Any = None, - scale: float = None, + scale: float | None = None, offset: tuple[float, float] = (0.0, 0.0), tag: Any = ''): # pylint: disable=too-many-locals diff --git a/dist/ba_data/python/bastd/ui/config.py b/dist/ba_data/python/bastd/ui/config.py index fbd7720..e8a6cc1 100644 --- a/dist/ba_data/python/bastd/ui/config.py +++ b/dist/ba_data/python/bastd/ui/config.py @@ -28,10 +28,10 @@ class ConfigCheckBox: position: tuple[float, float], size: tuple[float, float], displayname: str | ba.Lstr | None = None, - scale: float = None, - maxwidth: float = None, + scale: float | None = None, + maxwidth: float | None = None, autoselect: bool = True, - value_change_call: Callable[[Any], Any] = None): + value_change_call: Callable[[Any], Any] | None = None): if displayname is None: displayname = configkey self._value_change_call = value_change_call @@ -84,7 +84,7 @@ class ConfigNumberEdit: minval: float = 0.0, maxval: float = 100.0, increment: float = 1.0, - callback: Callable[[float], Any] = None, + callback: Callable[[float], Any] | None = None, xoffset: float = 0.0, displayname: str | ba.Lstr | None = None, changesound: bool = True, diff --git a/dist/ba_data/python/bastd/ui/confirm.py b/dist/ba_data/python/bastd/ui/confirm.py index 3136a7c..11c76d9 100644 --- a/dist/ba_data/python/bastd/ui/confirm.py +++ b/dist/ba_data/python/bastd/ui/confirm.py @@ -18,7 +18,7 @@ class ConfirmWindow: def __init__(self, text: str | ba.Lstr = 'Are you sure?', - action: Callable[[], Any] = None, + action: Callable[[], Any] | None = None, width: float = 360.0, height: float = 100.0, cancel_button: bool = True, @@ -27,7 +27,7 @@ class ConfirmWindow: text_scale: float = 1.0, ok_text: str | ba.Lstr | None = None, cancel_text: str | ba.Lstr | None = None, - origin_widget: ba.Widget = None): + origin_widget: ba.Widget | None = None): # pylint: disable=too-many-locals if ok_text is None: ok_text = ba.Lstr(resource='okText') @@ -127,7 +127,7 @@ class QuitWindow: def __init__(self, swish: bool = False, back: bool = False, - origin_widget: ba.Widget = None): + origin_widget: ba.Widget | None = None): ui = ba.app.ui app = ba.app self._back = back diff --git a/dist/ba_data/python/bastd/ui/coop/browser.py b/dist/ba_data/python/bastd/ui/coop/browser.py index aeabeaf..84d0ccf 100644 --- a/dist/ba_data/python/bastd/ui/coop/browser.py +++ b/dist/ba_data/python/bastd/ui/coop/browser.py @@ -6,7 +6,6 @@ from __future__ import annotations -import copy from typing import TYPE_CHECKING import _ba @@ -18,6 +17,8 @@ from bastd.ui.store.browser import StoreBrowserWindow if TYPE_CHECKING: from typing import Any + from bastd.ui.coop.tournamentbutton import TournamentButton + class CoopBrowserWindow(ba.Window): """Window for browsing co-op levels/games/etc.""" @@ -37,7 +38,7 @@ class CoopBrowserWindow(ba.Window): def __init__(self, transition: str | None = 'in_right', - origin_widget: ba.Widget = None): + origin_widget: ba.Widget | None = None): # pylint: disable=too-many-statements # pylint: disable=cyclic-import import threading @@ -175,8 +176,6 @@ class CoopBrowserWindow(ba.Window): 'Selected Coop Campaign Level', None)) self._selected_custom_level = (cfg.get('Selected Coop Custom Level', None)) - self._selected_challenge_level = (cfg.get( - 'Selected Coop Challenge Level', None)) # Don't want initial construction affecting our last-selected. self._do_selection_callbacks = False @@ -283,6 +282,7 @@ class CoopBrowserWindow(ba.Window): import bastd.ui.tournamentscores as _unused8 import bastd.ui.tournamententry as _unused9 import bastd.ui.play as _unused10 + import bastd.ui.coop.tournamentbutton as _unused11 def _update(self) -> None: # Do nothing if we've somehow outlived our actual UI. @@ -335,21 +335,21 @@ class CoopBrowserWindow(ba.Window): # Decrement time on our tournament buttons. ads_enabled = _ba.have_incentivized_ad() for tbtn in self._tournament_buttons: - tbtn['time_remaining'] = max(0, tbtn['time_remaining'] - 1) - if tbtn['time_remaining_value_text'] is not None: + tbtn.time_remaining = max(0, tbtn.time_remaining - 1) + if tbtn.time_remaining_value_text is not None: ba.textwidget( - edit=tbtn['time_remaining_value_text'], - text=ba.timestring(tbtn['time_remaining'], + edit=tbtn.time_remaining_value_text, + text=ba.timestring(tbtn.time_remaining, centi=False, suppress_format_warning=True) if - (tbtn['has_time_remaining'] + (tbtn.has_time_remaining and self._tourney_data_up_to_date) else '-') # Also adjust the ad icon visibility. - if tbtn.get('allow_ads', False) and _ba.has_video_ads(): - ba.imagewidget(edit=tbtn['entry_fee_ad_image'], + if tbtn.allow_ads and _ba.has_video_ads(): + ba.imagewidget(edit=tbtn.entry_fee_ad_image, opacity=1.0 if ads_enabled else 0.25) - ba.textwidget(edit=tbtn['entry_fee_text_remaining'], + ba.textwidget(edit=tbtn.entry_fee_text_remaining, color=(0.6, 0.6, 0.6, 1 if ads_enabled else 0.2)) self._update_hard_mode_lock_image() @@ -363,232 +363,21 @@ class CoopBrowserWindow(ba.Window): ba.print_exception('Error updating campaign lock.') def _update_for_data(self, data: list[dict[str, Any]] | None) -> None: - # pylint: disable=too-many-statements - # pylint: disable=too-many-locals - # pylint: disable=too-many-branches - from ba.internal import getcampaign, get_tournament_prize_strings # If the number of tournaments or challenges in the data differs from # our current arrangement, refresh with the new number. if ((data is None and self._tournament_button_count != 0) or (data is not None and (len(data) != self._tournament_button_count))): - self._tournament_button_count = len( - data) if data is not None else 0 + self._tournament_button_count = (len(data) + if data is not None else 0) ba.app.config['Tournament Rows'] = self._tournament_button_count self._refresh() # Update all of our tourney buttons based on whats in data. for i, tbtn in enumerate(self._tournament_buttons): assert data is not None - entry: dict[str, Any] = data[i] - prize_y_offs = (34 if 'prizeRange3' in entry else - 20 if 'prizeRange2' in entry else 12) - x_offs = 90 - - # This seems to be a false alarm. - # pylint: disable=unbalanced-tuple-unpacking - pr1, pv1, pr2, pv2, pr3, pv3 = ( - get_tournament_prize_strings(entry)) - # pylint: enable=unbalanced-tuple-unpacking - enabled = 'requiredLeague' not in entry - ba.buttonwidget(edit=tbtn['button'], - color=(0.5, 0.7, 0.2) if enabled else - (0.5, 0.5, 0.5)) - ba.imagewidget(edit=tbtn['lock_image'], - opacity=0.0 if enabled else 1.0) - ba.textwidget(edit=tbtn['prize_range_1_text'], - text='-' if pr1 == '' else pr1, - position=(tbtn['button_x'] + 365 + x_offs, - tbtn['button_y'] + tbtn['button_scale_y'] - - 93 + prize_y_offs)) - - # We want to draw values containing tickets a bit smaller - # (scratch that; we now draw medals a bit bigger). - ticket_char = ba.charstr(ba.SpecialChar.TICKET_BACKING) - prize_value_scale_large = 1.0 - prize_value_scale_small = 1.0 - - ba.textwidget(edit=tbtn['prize_value_1_text'], - text='-' if pv1 == '' else pv1, - scale=prize_value_scale_large if ticket_char - not in pv1 else prize_value_scale_small, - position=(tbtn['button_x'] + 380 + x_offs, - tbtn['button_y'] + tbtn['button_scale_y'] - - 93 + prize_y_offs)) - - ba.textwidget(edit=tbtn['prize_range_2_text'], - text=pr2, - position=(tbtn['button_x'] + 365 + x_offs, - tbtn['button_y'] + tbtn['button_scale_y'] - - 93 - 45 + prize_y_offs)) - ba.textwidget(edit=tbtn['prize_value_2_text'], - text=pv2, - scale=prize_value_scale_large if ticket_char - not in pv2 else prize_value_scale_small, - position=(tbtn['button_x'] + 380 + x_offs, - tbtn['button_y'] + tbtn['button_scale_y'] - - 93 - 45 + prize_y_offs)) - - ba.textwidget(edit=tbtn['prize_range_3_text'], - text=pr3, - position=(tbtn['button_x'] + 365 + x_offs, - tbtn['button_y'] + tbtn['button_scale_y'] - - 93 - 90 + prize_y_offs)) - ba.textwidget(edit=tbtn['prize_value_3_text'], - text=pv3, - scale=prize_value_scale_large if ticket_char - not in pv3 else prize_value_scale_small, - position=(tbtn['button_x'] + 380 + x_offs, - tbtn['button_y'] + tbtn['button_scale_y'] - - 93 - 90 + prize_y_offs)) - - leader_name = '-' - leader_score: str | ba.Lstr = '-' - if entry['scores']: - score = tbtn['leader'] = copy.deepcopy(entry['scores'][0]) - leader_name = score[1] - leader_score = (ba.timestring( - score[0] * 10, - centi=True, - timeformat=ba.TimeFormat.MILLISECONDS, - suppress_format_warning=True) if entry['scoreType'] - == 'time' else str(score[0])) - else: - tbtn['leader'] = None - - ba.textwidget(edit=tbtn['current_leader_name_text'], - text=ba.Lstr(value=leader_name)) - self._tournament_leader_score_type = (entry['scoreType']) - ba.textwidget(edit=tbtn['current_leader_score_text'], - text=leader_score) - ba.buttonwidget(edit=tbtn['more_scores_button'], - label=ba.Lstr(resource=self._r + '.seeMoreText')) - out_of_time_text: str | ba.Lstr = ( - '-' if 'totalTime' not in entry else ba.Lstr( - resource=self._r + '.ofTotalTimeText', - subs=[('${TOTAL}', - ba.timestring(entry['totalTime'], - centi=False, - suppress_format_warning=True))])) - ba.textwidget(edit=tbtn['time_remaining_out_of_text'], - text=out_of_time_text) - - tbtn['time_remaining'] = entry['timeRemaining'] - tbtn['has_time_remaining'] = entry is not None - tbtn['tournament_id'] = entry['tournamentID'] - tbtn['required_league'] = (None if 'requiredLeague' not in entry - else entry['requiredLeague']) - - game = ba.app.accounts_v1.tournament_info[ - tbtn['tournament_id']]['game'] - - if game is None: - ba.textwidget(edit=tbtn['button_text'], text='-') - ba.imagewidget(edit=tbtn['image'], - texture=ba.gettexture('black'), - opacity=0.2) - else: - campaignname, levelname = game.split(':') - campaign = getcampaign(campaignname) - max_players = ba.app.accounts_v1.tournament_info[ - tbtn['tournament_id']]['maxPlayers'] - txt = ba.Lstr( - value='${A} ${B}', - subs=[('${A}', campaign.getlevel(levelname).displayname), - ('${B}', - ba.Lstr(resource='playerCountAbbreviatedText', - subs=[('${COUNT}', str(max_players))]))]) - ba.textwidget(edit=tbtn['button_text'], text=txt) - ba.imagewidget( - edit=tbtn['image'], - texture=campaign.getlevel(levelname).get_preview_texture(), - opacity=1.0 if enabled else 0.5) - - fee = entry['fee'] - - if fee is None: - fee_var = None - elif fee == 4: - fee_var = 'price.tournament_entry_4' - elif fee == 3: - fee_var = 'price.tournament_entry_3' - elif fee == 2: - fee_var = 'price.tournament_entry_2' - elif fee == 1: - fee_var = 'price.tournament_entry_1' - else: - if fee != 0: - print('Unknown fee value:', fee) - fee_var = 'price.tournament_entry_0' - - tbtn['allow_ads'] = allow_ads = entry['allowAds'] - - final_fee: int | None = (None if fee_var is None else - _ba.get_v1_account_misc_read_val( - fee_var, '?')) - - final_fee_str: str | ba.Lstr - if fee_var is None: - final_fee_str = '' - else: - if final_fee == 0: - final_fee_str = ba.Lstr( - resource='getTicketsWindow.freeText') - else: - final_fee_str = ( - ba.charstr(ba.SpecialChar.TICKET_BACKING) + - str(final_fee)) - - ad_tries_remaining = ba.app.accounts_v1.tournament_info[ - tbtn['tournament_id']]['adTriesRemaining'] - free_tries_remaining = ba.app.accounts_v1.tournament_info[ - tbtn['tournament_id']]['freeTriesRemaining'] - - # Now, if this fee allows ads and we support video ads, show - # the 'or ad' version. - if allow_ads and _ba.has_video_ads(): - ads_enabled = _ba.have_incentivized_ad() - ba.imagewidget(edit=tbtn['entry_fee_ad_image'], - opacity=1.0 if ads_enabled else 0.25) - or_text = ba.Lstr(resource='orText', - subs=[('${A}', ''), - ('${B}', '')]).evaluate().strip() - ba.textwidget(edit=tbtn['entry_fee_text_or'], text=or_text) - ba.textwidget( - edit=tbtn['entry_fee_text_top'], - position=(tbtn['button_x'] + 360, - tbtn['button_y'] + tbtn['button_scale_y'] - 60), - scale=1.3, - text=final_fee_str) - - # Possibly show number of ad-plays remaining. - ba.textwidget( - edit=tbtn['entry_fee_text_remaining'], - position=(tbtn['button_x'] + 360, - tbtn['button_y'] + tbtn['button_scale_y'] - 146), - text='' if ad_tries_remaining in [None, 0] else - ('' + str(ad_tries_remaining)), - color=(0.6, 0.6, 0.6, 1 if ads_enabled else 0.2)) - else: - ba.imagewidget(edit=tbtn['entry_fee_ad_image'], opacity=0.0) - ba.textwidget(edit=tbtn['entry_fee_text_or'], text='') - ba.textwidget( - edit=tbtn['entry_fee_text_top'], - position=(tbtn['button_x'] + 360, - tbtn['button_y'] + tbtn['button_scale_y'] - 80), - scale=1.3, - text=final_fee_str) - - # Possibly show number of free-plays remaining. - ba.textwidget( - edit=tbtn['entry_fee_text_remaining'], - position=(tbtn['button_x'] + 360, - tbtn['button_y'] + tbtn['button_scale_y'] - 100), - text=('' if (free_tries_remaining in [None, 0] - or final_fee != 0) else - ('' + str(free_tries_remaining))), - color=(0.6, 0.6, 0.6, 1)) + tbtn.update_for_data(data[i]) def _on_tournament_query_response(self, data: dict[str, Any] | None) -> None: @@ -715,10 +504,13 @@ class CoopBrowserWindow(ba.Window): items = [ campaignname + ':Onslaught Training', campaignname + ':Rookie Onslaught', - campaignname + ':Rookie Football', campaignname + ':Pro Onslaught', - campaignname + ':Pro Football', campaignname + ':Pro Runaround', - campaignname + ':Uber Onslaught', campaignname + ':Uber Football', - campaignname + ':Uber Runaround' + campaignname + ':Rookie Football', + campaignname + ':Pro Onslaught', + campaignname + ':Pro Football', + campaignname + ':Pro Runaround', + campaignname + ':Uber Onslaught', + campaignname + ':Uber Football', + campaignname + ':Uber Runaround', ] items += [campaignname + ':The Last Stand'] if self._selected_campaign_level is None: @@ -772,6 +564,7 @@ class CoopBrowserWindow(ba.Window): # pylint: disable=too-many-locals # pylint: disable=cyclic-import from bastd.ui.coop.gamebutton import GameButton + from bastd.ui.coop.tournamentbutton import TournamentButton # (Re)create the sub-container if need be. if self._subcontainer is not None: @@ -839,7 +632,7 @@ class CoopBrowserWindow(ba.Window): # Tournaments - self._tournament_buttons: list[dict[str, Any]] = [] + self._tournament_buttons: list[TournamentButton] = [] v -= 53 # FIXME shouldn't use hard-coded strings here. @@ -919,10 +712,15 @@ class CoopBrowserWindow(ba.Window): v2 = -2 is_last_sel = True self._tournament_buttons.append( - self._tournament_button(sc2, h, v2, is_last_sel)) + TournamentButton(sc2, + h, + v2, + is_last_sel, + on_pressed=ba.WeakCall( + self.run_tournament))) v -= 200 - # Custom Games. + # Custom Games. (called 'Practice' in UI these days). v -= 50 ba.textwidget(parent=w_parent, position=(h_base + 27, v + 30 + 198), @@ -949,14 +747,13 @@ class CoopBrowserWindow(ba.Window): if _ba.get_v1_account_misc_read_val( 'easter', False) or _ba.get_purchased('games.easter_egg_hunt'): items = [ - 'Challenges:Easter Egg Hunt', 'Challenges:Pro Easter Egg Hunt' + 'Challenges:Easter Egg Hunt', + 'Challenges:Pro Easter Egg Hunt', ] + items - # add all custom user levels here.. - # items += [ - # 'User:' + l.getname() - # for l in getcampaign('User').getlevels() - # ] + # If we've defined custom games, put them at the beginning. + if ba.app.custom_coop_practice_games: + items = ba.app.custom_coop_practice_games + items self._custom_h_scroll = custom_h_scroll = h_scroll = ba.hscrollwidget( parent=w_parent, @@ -995,19 +792,19 @@ class CoopBrowserWindow(ba.Window): for i, tbutton in enumerate(self._tournament_buttons): ba.widget( - edit=tbutton['button'], + edit=tbutton.button, up_widget=self._tournament_info_button - if i == 0 else self._tournament_buttons[i - 1]['button'], - down_widget=self._tournament_buttons[(i + 1)]['button'] + if i == 0 else self._tournament_buttons[i - 1].button, + down_widget=self._tournament_buttons[(i + 1)].button if i + 1 < len(self._tournament_buttons) else custom_h_scroll) ba.widget( - edit=tbutton['more_scores_button'], + edit=tbutton.more_scores_button, down_widget=self._tournament_buttons[( - i + 1)]['current_leader_name_text'] + i + 1)].current_leader_name_text if i + 1 < len(self._tournament_buttons) else custom_h_scroll) - ba.widget(edit=tbutton['current_leader_name_text'], + ba.widget(edit=tbutton.current_leader_name_text, up_widget=self._tournament_info_button if i == 0 else - self._tournament_buttons[i - 1]['more_scores_button']) + self._tournament_buttons[i - 1].more_scores_button) for btn in self._custom_buttons: try: @@ -1037,314 +834,6 @@ class CoopBrowserWindow(ba.Window): def _enable_selectable_callback(self) -> None: self._do_selection_callbacks = True - def _tournament_button(self, parent: ba.Widget, x: float, y: float, - select: bool) -> dict[str, Any]: - sclx = 300 - scly = 195.0 - data: dict[str, Any] = { - 'tournament_id': None, - 'time_remaining': 0, - 'has_time_remaining': False, - 'leader': None - } - data['button'] = btn = ba.buttonwidget( - parent=parent, - position=(x + 23, y + 4), - size=(sclx, scly), - label='', - button_type='square', - autoselect=True, - on_activate_call=lambda: self.run(None, tournament_button=data)) - ba.widget(edit=btn, - show_buffer_bottom=50, - show_buffer_top=50, - show_buffer_left=400, - show_buffer_right=200) - if select: - ba.containerwidget(edit=parent, - selected_child=btn, - visible_child=btn) - image_width = sclx * 0.85 * 0.75 - - data['image'] = ba.imagewidget( - parent=parent, - draw_controller=btn, - position=(x + 21 + sclx * 0.5 - image_width * 0.5, y + scly - 150), - size=(image_width, image_width * 0.5), - model_transparent=self.lsbt, - model_opaque=self.lsbo, - texture=ba.gettexture('black'), - opacity=0.2, - mask_texture=ba.gettexture('mapPreviewMask')) - - data['lock_image'] = ba.imagewidget( - parent=parent, - draw_controller=btn, - position=(x + 21 + sclx * 0.5 - image_width * 0.25, - y + scly - 150), - size=(image_width * 0.5, image_width * 0.5), - texture=ba.gettexture('lock'), - opacity=0.0) - - data['button_text'] = ba.textwidget(parent=parent, - draw_controller=btn, - position=(x + 20 + sclx * 0.5, - y + scly - 35), - size=(0, 0), - h_align='center', - text='-', - v_align='center', - maxwidth=sclx * 0.76, - scale=0.85, - color=(0.8, 1.0, 0.8, 1.0)) - - header_color = (0.43, 0.4, 0.5, 1) - value_color = (0.6, 0.6, 0.6, 1) - - x_offs = 0 - ba.textwidget(parent=parent, - draw_controller=btn, - position=(x + 360, y + scly - 20), - size=(0, 0), - h_align='center', - text=ba.Lstr(resource=self._r + '.entryFeeText'), - v_align='center', - maxwidth=100, - scale=0.9, - color=header_color, - flatness=1.0) - - data['entry_fee_text_top'] = ba.textwidget(parent=parent, - draw_controller=btn, - position=(x + 360, - y + scly - 60), - size=(0, 0), - h_align='center', - text='-', - v_align='center', - maxwidth=60, - scale=1.3, - color=value_color, - flatness=1.0) - data['entry_fee_text_or'] = ba.textwidget(parent=parent, - draw_controller=btn, - position=(x + 360, - y + scly - 90), - size=(0, 0), - h_align='center', - text='', - v_align='center', - maxwidth=60, - scale=0.5, - color=value_color, - flatness=1.0) - data['entry_fee_text_remaining'] = ba.textwidget(parent=parent, - draw_controller=btn, - position=(x + 360, y + - scly - 90), - size=(0, 0), - h_align='center', - text='', - v_align='center', - maxwidth=60, - scale=0.5, - color=value_color, - flatness=1.0) - - data['entry_fee_ad_image'] = ba.imagewidget( - parent=parent, - size=(40, 40), - draw_controller=btn, - position=(x + 360 - 20, y + scly - 140), - opacity=0.0, - texture=ba.gettexture('tv')) - - x_offs += 50 - - ba.textwidget(parent=parent, - draw_controller=btn, - position=(x + 447 + x_offs, y + scly - 20), - size=(0, 0), - h_align='center', - text=ba.Lstr(resource=self._r + '.prizesText'), - v_align='center', - maxwidth=130, - scale=0.9, - color=header_color, - flatness=1.0) - - data['button_x'] = x - data['button_y'] = y - data['button_scale_y'] = scly - - xo2 = 0 - prize_value_scale = 1.5 - - data['prize_range_1_text'] = ba.textwidget( - parent=parent, - draw_controller=btn, - position=(x + 355 + xo2 + x_offs, y + scly - 93), - size=(0, 0), - h_align='right', - v_align='center', - maxwidth=50, - text='-', - scale=0.8, - color=header_color, - flatness=1.0) - data['prize_value_1_text'] = ba.textwidget( - parent=parent, - draw_controller=btn, - position=(x + 380 + xo2 + x_offs, y + scly - 93), - size=(0, 0), - h_align='left', - text='-', - v_align='center', - maxwidth=100, - scale=prize_value_scale, - color=value_color, - flatness=1.0) - - data['prize_range_2_text'] = ba.textwidget( - parent=parent, - draw_controller=btn, - position=(x + 355 + xo2 + x_offs, y + scly - 93), - size=(0, 0), - h_align='right', - v_align='center', - maxwidth=50, - scale=0.8, - color=header_color, - flatness=1.0) - data['prize_value_2_text'] = ba.textwidget( - parent=parent, - draw_controller=btn, - position=(x + 380 + xo2 + x_offs, y + scly - 93), - size=(0, 0), - h_align='left', - text='', - v_align='center', - maxwidth=100, - scale=prize_value_scale, - color=value_color, - flatness=1.0) - - data['prize_range_3_text'] = ba.textwidget( - parent=parent, - draw_controller=btn, - position=(x + 355 + xo2 + x_offs, y + scly - 93), - size=(0, 0), - h_align='right', - v_align='center', - maxwidth=50, - scale=0.8, - color=header_color, - flatness=1.0) - data['prize_value_3_text'] = ba.textwidget( - parent=parent, - draw_controller=btn, - position=(x + 380 + xo2 + x_offs, y + scly - 93), - size=(0, 0), - h_align='left', - text='', - v_align='center', - maxwidth=100, - scale=prize_value_scale, - color=value_color, - flatness=1.0) - - ba.textwidget(parent=parent, - draw_controller=btn, - position=(x + 620 + x_offs, y + scly - 20), - size=(0, 0), - h_align='center', - text=ba.Lstr(resource=self._r + '.currentBestText'), - v_align='center', - maxwidth=180, - scale=0.9, - color=header_color, - flatness=1.0) - data['current_leader_name_text'] = ba.textwidget( - parent=parent, - draw_controller=btn, - position=(x + 620 + x_offs - (170 / 1.4) * 0.5, - y + scly - 60 - 40 * 0.5), - selectable=True, - click_activate=True, - autoselect=True, - on_activate_call=lambda: self._show_leader(tournament_button=data), - size=(170 / 1.4, 40), - h_align='center', - text='-', - v_align='center', - maxwidth=170, - scale=1.4, - color=value_color, - flatness=1.0) - data['current_leader_score_text'] = ba.textwidget( - parent=parent, - draw_controller=btn, - position=(x + 620 + x_offs, y + scly - 113 + 10), - size=(0, 0), - h_align='center', - text='-', - v_align='center', - maxwidth=170, - scale=1.8, - color=value_color, - flatness=1.0) - - data['more_scores_button'] = ba.buttonwidget( - parent=parent, - position=(x + 620 + x_offs - 60, y + scly - 50 - 125), - color=(0.5, 0.5, 0.6), - textcolor=(0.7, 0.7, 0.8), - label='-', - size=(120, 40), - autoselect=True, - up_widget=data['current_leader_name_text'], - text_scale=0.6, - on_activate_call=lambda: self._show_scores(tournament_button=data)) - ba.widget(edit=data['current_leader_name_text'], - down_widget=data['more_scores_button']) - - ba.textwidget(parent=parent, - draw_controller=btn, - position=(x + 820 + x_offs, y + scly - 20), - size=(0, 0), - h_align='center', - text=ba.Lstr(resource=self._r + '.timeRemainingText'), - v_align='center', - maxwidth=180, - scale=0.9, - color=header_color, - flatness=1.0) - data['time_remaining_value_text'] = ba.textwidget( - parent=parent, - draw_controller=btn, - position=(x + 820 + x_offs, y + scly - 68), - size=(0, 0), - h_align='center', - text='-', - v_align='center', - maxwidth=180, - scale=2.0, - color=value_color, - flatness=1.0) - data['time_remaining_out_of_text'] = ba.textwidget( - parent=parent, - draw_controller=btn, - position=(x + 820 + x_offs, y + scly - 110), - size=(0, 0), - h_align='center', - text='-', - v_align='center', - maxwidth=120, - scale=0.72, - color=(0.4, 0.4, 0.5), - flatness=1.0) - return data - def _switch_to_league_rankings(self) -> None: # pylint: disable=cyclic-import from bastd.ui.account import show_sign_in_prompt @@ -1378,100 +867,20 @@ class CoopBrowserWindow(ba.Window): show_tab=show_tab, back_location='CoopBrowserWindow').get_root_widget()) - def _show_leader(self, tournament_button: dict[str, Any]) -> None: - # pylint: disable=cyclic-import - from bastd.ui.account.viewer import AccountViewerWindow - tournament_id = tournament_button['tournament_id'] - - # FIXME: This assumes a single player entry in leader; should expand - # this to work with multiple. - if tournament_id is None or tournament_button['leader'] is None or len( - tournament_button['leader'][2]) != 1: - ba.playsound(ba.getsound('error')) - return - ba.playsound(ba.getsound('swish')) - AccountViewerWindow( - account_id=tournament_button['leader'][2][0].get('a', None), - profile_id=tournament_button['leader'][2][0].get('p', None), - position=tournament_button['current_leader_name_text']. - get_screen_space_center()) - - def _show_scores(self, tournament_button: dict[str, Any]) -> None: - # pylint: disable=cyclic-import - from bastd.ui.tournamentscores import TournamentScoresWindow - tournament_id = tournament_button['tournament_id'] - if tournament_id is None: - ba.playsound(ba.getsound('error')) - return - - TournamentScoresWindow( - tournament_id=tournament_id, - position=tournament_button['more_scores_button']. - get_screen_space_center()) - def is_tourney_data_up_to_date(self) -> bool: """Return whether our tourney data is up to date.""" return self._tourney_data_up_to_date - def run(self, - game: str | None, - tournament_button: dict[str, Any] = None) -> None: + def run_game(self, game: str) -> None: """Run the provided game.""" # pylint: disable=too-many-branches - # pylint: disable=too-many-statements - # pylint: disable=too-many-return-statements # pylint: disable=cyclic-import from bastd.ui.confirm import ConfirmWindow - from bastd.ui.tournamententry import TournamentEntryWindow from bastd.ui.purchase import PurchaseWindow from bastd.ui.account import show_sign_in_prompt args: dict[str, Any] = {} - # Do a bit of pre-flight for tournament options. - if tournament_button is not None: - - if _ba.get_v1_account_state() != 'signed_in': - show_sign_in_prompt() - return - - if not self._tourney_data_up_to_date: - ba.screenmessage( - ba.Lstr(resource='tournamentCheckingStateText'), - color=(1, 1, 0)) - ba.playsound(ba.getsound('error')) - return - - if tournament_button['tournament_id'] is None: - ba.screenmessage( - ba.Lstr(resource='internal.unavailableNoConnectionText'), - color=(1, 0, 0)) - ba.playsound(ba.getsound('error')) - return - - if tournament_button['required_league'] is not None: - ba.screenmessage(ba.Lstr( - resource='league.tournamentLeagueText', - subs=[ - ('${NAME}', - ba.Lstr( - translate=('leagueNames', - tournament_button['required_league']))) - ]), - color=(1, 0, 0)) - ba.playsound(ba.getsound('error')) - return - - if tournament_button['time_remaining'] <= 0: - ba.screenmessage(ba.Lstr(resource='tournamentEndedText'), - color=(1, 0, 0)) - ba.playsound(ba.getsound('error')) - return - - # Game is whatever the tournament tells us it is. - game = ba.app.accounts_v1.tournament_info[ - tournament_button['tournament_id']]['game'] - - if tournament_button is None and game == 'Easy:The Last Stand': + if game == 'Easy:The Last Stand': ConfirmWindow(ba.Lstr(resource='difficultyHardUnlockOnlyText', fallback_resource='difficultyHardOnlyText'), cancel_button=False, @@ -1479,12 +888,11 @@ class CoopBrowserWindow(ba.Window): height=130) return - # Infinite onslaught/runaround require pro; bring up a store link if - # need be. - if tournament_button is None and game in ( - 'Challenges:Infinite Runaround', - 'Challenges:Infinite Onslaught' - ) and not ba.app.accounts_v1.have_pro(): + # Infinite onslaught/runaround require pro; bring up a store link + # if need be. + if game in ('Challenges:Infinite Runaround', + 'Challenges:Infinite Onslaught' + ) and not ba.app.accounts_v1.have_pro(): if _ba.get_v1_account_state() != 'signed_in': show_sign_in_prompt() else: @@ -1495,7 +903,8 @@ class CoopBrowserWindow(ba.Window): if game in ['Challenges:Meteor Shower']: required_purchase = 'games.meteor_shower' elif game in [ - 'Challenges:Target Practice', 'Challenges:Target Practice B' + 'Challenges:Target Practice', + 'Challenges:Target Practice B', ]: required_purchase = 'games.target_practice' elif game in ['Challenges:Ninja Fight']: @@ -1503,13 +912,14 @@ class CoopBrowserWindow(ba.Window): elif game in ['Challenges:Pro Ninja Fight']: required_purchase = 'games.ninja_fight' elif game in [ - 'Challenges:Easter Egg Hunt', 'Challenges:Pro Easter Egg Hunt' + 'Challenges:Easter Egg Hunt', + 'Challenges:Pro Easter Egg Hunt', ]: required_purchase = 'games.easter_egg_hunt' else: required_purchase = None - if (tournament_button is None and required_purchase is not None + if (required_purchase is not None and not _ba.get_purchased(required_purchase)): if _ba.get_v1_account_state() != 'signed_in': show_sign_in_prompt() @@ -1519,17 +929,57 @@ class CoopBrowserWindow(ba.Window): self._save_state() - # For tournaments, we pop up the entry window. - if tournament_button is not None: - TournamentEntryWindow( - tournament_id=tournament_button['tournament_id'], - position=tournament_button['button'].get_screen_space_center()) - else: - # Otherwise just dive right in. - assert game is not None - if ba.app.launch_coop_game(game, args=args): - ba.containerwidget(edit=self._root_widget, - transition='out_left') + if ba.app.launch_coop_game(game, args=args): + ba.containerwidget(edit=self._root_widget, transition='out_left') + + def run_tournament(self, tournament_button: TournamentButton) -> None: + """Run the provided tournament game.""" + from bastd.ui.account import show_sign_in_prompt + from bastd.ui.tournamententry import TournamentEntryWindow + + if _ba.get_v1_account_state() != 'signed_in': + show_sign_in_prompt() + return + + if not self._tourney_data_up_to_date: + ba.screenmessage(ba.Lstr(resource='tournamentCheckingStateText'), + color=(1, 1, 0)) + ba.playsound(ba.getsound('error')) + return + + if tournament_button.tournament_id is None: + ba.screenmessage( + ba.Lstr(resource='internal.unavailableNoConnectionText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + if tournament_button.required_league is not None: + ba.screenmessage( + ba.Lstr( + resource='league.tournamentLeagueText', + subs=[('${NAME}', + ba.Lstr( + translate=('leagueNames', + tournament_button.required_league))) + ]), + color=(1, 0, 0), + ) + ba.playsound(ba.getsound('error')) + return + + if tournament_button.time_remaining <= 0: + ba.screenmessage(ba.Lstr(resource='tournamentEndedText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + + self._save_state() + + assert tournament_button.tournament_id is not None + TournamentEntryWindow( + tournament_id=tournament_button.tournament_id, + position=tournament_button.button.get_screen_space_center()) def _back(self) -> None: # pylint: disable=cyclic-import @@ -1542,24 +992,6 @@ class CoopBrowserWindow(ba.Window): ba.app.ui.set_main_menu_window( PlayWindow(transition='in_left').get_root_widget()) - def _restore_state(self) -> None: - try: - sel_name = ba.app.ui.window_states.get(type(self), - {}).get('sel_name') - if sel_name == 'Back': - sel = self._back_button - elif sel_name == 'Scroll': - sel = self._scrollwidget - elif sel_name == 'PowerRanking': - sel = self._league_rank_button_widget - elif sel_name == 'Store': - sel = self._store_button_widget - else: - sel = self._scrollwidget - ba.containerwidget(edit=self._root_widget, selected_child=sel) - except Exception: - ba.print_exception(f'Error restoring state for {self}.') - def _save_state(self) -> None: cfg = ba.app.config try: @@ -1580,16 +1012,31 @@ class CoopBrowserWindow(ba.Window): cfg['Selected Coop Row'] = self._selected_row cfg['Selected Coop Custom Level'] = self._selected_custom_level - cfg['Selected Coop Challenge Level'] = self._selected_challenge_level cfg['Selected Coop Campaign Level'] = self._selected_campaign_level cfg.commit() + def _restore_state(self) -> None: + try: + sel_name = ba.app.ui.window_states.get(type(self), + {}).get('sel_name') + if sel_name == 'Back': + sel = self._back_button + elif sel_name == 'Scroll': + sel = self._scrollwidget + elif sel_name == 'PowerRanking': + sel = self._league_rank_button_widget + elif sel_name == 'Store': + sel = self._store_button_widget + else: + sel = self._scrollwidget + ba.containerwidget(edit=self._root_widget, selected_child=sel) + except Exception: + ba.print_exception(f'Error restoring state for {self}.') + def sel_change(self, row: str, game: str) -> None: """(internal)""" if self._do_selection_callbacks: if row == 'custom': self._selected_custom_level = game - if row == 'challenges': - self._selected_challenge_level = game elif row == 'campaign': self._selected_campaign_level = game diff --git a/dist/ba_data/python/bastd/ui/coop/gamebutton.py b/dist/ba_data/python/bastd/ui/coop/gamebutton.py index 40ff1cf..fb819f3 100644 --- a/dist/ba_data/python/bastd/ui/coop/gamebutton.py +++ b/dist/ba_data/python/bastd/ui/coop/gamebutton.py @@ -55,7 +55,7 @@ class GameButton: position=(x + 23, y + 4), size=(sclx, scly), label='', - on_activate_call=ba.Call(window.run, game), + on_activate_call=ba.Call(window.run_game, game), button_type='square', autoselect=True, on_select_call=ba.Call(window.sel_change, row, game)) diff --git a/dist/ba_data/python/bastd/ui/coop/tournamentbutton.py b/dist/ba_data/python/bastd/ui/coop/tournamentbutton.py new file mode 100644 index 0000000..f443ef2 --- /dev/null +++ b/dist/ba_data/python/bastd/ui/coop/tournamentbutton.py @@ -0,0 +1,561 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines button for co-op games.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING +import copy + +import ba +import _ba + +if TYPE_CHECKING: + from typing import Any, Callable + + +class TournamentButton: + """Button showing a tournament in coop window.""" + + def __init__(self, parent: ba.Widget, x: float, y: float, select: bool, + on_pressed: Callable[[TournamentButton], None]) -> None: + self._r = 'coopSelectWindow' + sclx = 300 + scly = 195.0 + self.on_pressed = on_pressed + self.lsbt = ba.getmodel('level_select_button_transparent') + self.lsbo = ba.getmodel('level_select_button_opaque') + self.allow_ads = False + self.tournament_id: str | None = None + self.time_remaining: int = 0 + self.has_time_remaining: bool = False + self.leader: Any = None + self.required_league: str | None = None + self.button = btn = ba.buttonwidget( + parent=parent, + position=(x + 23, y + 4), + size=(sclx, scly), + label='', + button_type='square', + autoselect=True, + # on_activate_call=lambda: self.run(None, tournament_button=data) + on_activate_call=ba.WeakCall(self._pressed)) + ba.widget(edit=btn, + show_buffer_bottom=50, + show_buffer_top=50, + show_buffer_left=400, + show_buffer_right=200) + if select: + ba.containerwidget(edit=parent, + selected_child=btn, + visible_child=btn) + image_width = sclx * 0.85 * 0.75 + + self.image = ba.imagewidget( + parent=parent, + draw_controller=btn, + position=(x + 21 + sclx * 0.5 - image_width * 0.5, y + scly - 150), + size=(image_width, image_width * 0.5), + model_transparent=self.lsbt, + model_opaque=self.lsbo, + texture=ba.gettexture('black'), + opacity=0.2, + mask_texture=ba.gettexture('mapPreviewMask')) + + self.lock_image = ba.imagewidget( + parent=parent, + draw_controller=btn, + position=(x + 21 + sclx * 0.5 - image_width * 0.25, + y + scly - 150), + size=(image_width * 0.5, image_width * 0.5), + texture=ba.gettexture('lock'), + opacity=0.0) + + self.button_text = ba.textwidget(parent=parent, + draw_controller=btn, + position=(x + 20 + sclx * 0.5, + y + scly - 35), + size=(0, 0), + h_align='center', + text='-', + v_align='center', + maxwidth=sclx * 0.76, + scale=0.85, + color=(0.8, 1.0, 0.8, 1.0)) + + header_color = (0.43, 0.4, 0.5, 1) + value_color = (0.6, 0.6, 0.6, 1) + + x_offs = 0 + ba.textwidget(parent=parent, + draw_controller=btn, + position=(x + 360, y + scly - 20), + size=(0, 0), + h_align='center', + text=ba.Lstr(resource=self._r + '.entryFeeText'), + v_align='center', + maxwidth=100, + scale=0.9, + color=header_color, + flatness=1.0) + + self.entry_fee_text_top = ba.textwidget(parent=parent, + draw_controller=btn, + position=(x + 360, + y + scly - 60), + size=(0, 0), + h_align='center', + text='-', + v_align='center', + maxwidth=60, + scale=1.3, + color=value_color, + flatness=1.0) + self.entry_fee_text_or = ba.textwidget(parent=parent, + draw_controller=btn, + position=(x + 360, + y + scly - 90), + size=(0, 0), + h_align='center', + text='', + v_align='center', + maxwidth=60, + scale=0.5, + color=value_color, + flatness=1.0) + self.entry_fee_text_remaining = ba.textwidget(parent=parent, + draw_controller=btn, + position=(x + 360, + y + scly - 90), + size=(0, 0), + h_align='center', + text='', + v_align='center', + maxwidth=60, + scale=0.5, + color=value_color, + flatness=1.0) + + self.entry_fee_ad_image = ba.imagewidget(parent=parent, + size=(40, 40), + draw_controller=btn, + position=(x + 360 - 20, + y + scly - 140), + opacity=0.0, + texture=ba.gettexture('tv')) + + x_offs += 50 + + ba.textwidget(parent=parent, + draw_controller=btn, + position=(x + 447 + x_offs, y + scly - 20), + size=(0, 0), + h_align='center', + text=ba.Lstr(resource=self._r + '.prizesText'), + v_align='center', + maxwidth=130, + scale=0.9, + color=header_color, + flatness=1.0) + + self.button_x = x + self.button_y = y + self.button_scale_y = scly + + xo2 = 0 + prize_value_scale = 1.5 + + self.prize_range_1_text = ba.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 355 + xo2 + x_offs, y + scly - 93), + size=(0, 0), + h_align='right', + v_align='center', + maxwidth=50, + text='-', + scale=0.8, + color=header_color, + flatness=1.0) + self.prize_value_1_text = ba.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 380 + xo2 + x_offs, y + scly - 93), + size=(0, 0), + h_align='left', + text='-', + v_align='center', + maxwidth=100, + scale=prize_value_scale, + color=value_color, + flatness=1.0) + + self.prize_range_2_text = ba.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 355 + xo2 + x_offs, y + scly - 93), + size=(0, 0), + h_align='right', + v_align='center', + maxwidth=50, + scale=0.8, + color=header_color, + flatness=1.0) + self.prize_value_2_text = ba.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 380 + xo2 + x_offs, y + scly - 93), + size=(0, 0), + h_align='left', + text='', + v_align='center', + maxwidth=100, + scale=prize_value_scale, + color=value_color, + flatness=1.0) + + self.prize_range_3_text = ba.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 355 + xo2 + x_offs, y + scly - 93), + size=(0, 0), + h_align='right', + v_align='center', + maxwidth=50, + scale=0.8, + color=header_color, + flatness=1.0) + self.prize_value_3_text = ba.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 380 + xo2 + x_offs, y + scly - 93), + size=(0, 0), + h_align='left', + text='', + v_align='center', + maxwidth=100, + scale=prize_value_scale, + color=value_color, + flatness=1.0) + + ba.textwidget(parent=parent, + draw_controller=btn, + position=(x + 620 + x_offs, y + scly - 20), + size=(0, 0), + h_align='center', + text=ba.Lstr(resource=self._r + '.currentBestText'), + v_align='center', + maxwidth=180, + scale=0.9, + color=header_color, + flatness=1.0) + self.current_leader_name_text = ba.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 620 + x_offs - (170 / 1.4) * 0.5, + y + scly - 60 - 40 * 0.5), + selectable=True, + click_activate=True, + autoselect=True, + on_activate_call=ba.WeakCall(self._show_leader), + size=(170 / 1.4, 40), + h_align='center', + text='-', + v_align='center', + maxwidth=170, + scale=1.4, + color=value_color, + flatness=1.0) + self.current_leader_score_text = ba.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 620 + x_offs, y + scly - 113 + 10), + size=(0, 0), + h_align='center', + text='-', + v_align='center', + maxwidth=170, + scale=1.8, + color=value_color, + flatness=1.0) + + self.more_scores_button = ba.buttonwidget( + parent=parent, + position=(x + 620 + x_offs - 60, y + scly - 50 - 125), + color=(0.5, 0.5, 0.6), + textcolor=(0.7, 0.7, 0.8), + label='-', + size=(120, 40), + autoselect=True, + up_widget=self.current_leader_name_text, + text_scale=0.6, + on_activate_call=ba.WeakCall(self._show_scores)) + ba.widget(edit=self.current_leader_name_text, + down_widget=self.more_scores_button) + + ba.textwidget(parent=parent, + draw_controller=btn, + position=(x + 820 + x_offs, y + scly - 20), + size=(0, 0), + h_align='center', + text=ba.Lstr(resource=self._r + '.timeRemainingText'), + v_align='center', + maxwidth=180, + scale=0.9, + color=header_color, + flatness=1.0) + self.time_remaining_value_text = ba.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 820 + x_offs, y + scly - 68), + size=(0, 0), + h_align='center', + text='-', + v_align='center', + maxwidth=180, + scale=2.0, + color=value_color, + flatness=1.0) + self.time_remaining_out_of_text = ba.textwidget( + parent=parent, + draw_controller=btn, + position=(x + 820 + x_offs, y + scly - 110), + size=(0, 0), + h_align='center', + text='-', + v_align='center', + maxwidth=120, + scale=0.72, + color=(0.4, 0.4, 0.5), + flatness=1.0) + + def _pressed(self) -> None: + self.on_pressed(self) + + def _show_leader(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.account.viewer import AccountViewerWindow + tournament_id = self.tournament_id + + # FIXME: This assumes a single player entry in leader; should expand + # this to work with multiple. + if tournament_id is None or self.leader is None or len( + self.leader[2]) != 1: + ba.playsound(ba.getsound('error')) + return + ba.playsound(ba.getsound('swish')) + AccountViewerWindow( + account_id=self.leader[2][0].get('a', None), + profile_id=self.leader[2][0].get('p', None), + position=self.current_leader_name_text.get_screen_space_center()) + + def _show_scores(self) -> None: + # pylint: disable=cyclic-import + from bastd.ui.tournamentscores import TournamentScoresWindow + tournament_id = self.tournament_id + if tournament_id is None: + ba.playsound(ba.getsound('error')) + return + + TournamentScoresWindow( + tournament_id=tournament_id, + position=self.more_scores_button.get_screen_space_center()) + + def update_for_data(self, entry: dict[str, Any]) -> None: + """Update for new incoming data.""" + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + from ba.internal import getcampaign, get_tournament_prize_strings + prize_y_offs = (34 if 'prizeRange3' in entry else + 20 if 'prizeRange2' in entry else 12) + x_offs = 90 + + # This seems to be a false alarm. + # pylint: disable=unbalanced-tuple-unpacking + pr1, pv1, pr2, pv2, pr3, pv3 = (get_tournament_prize_strings(entry)) + # pylint: enable=unbalanced-tuple-unpacking + enabled = 'requiredLeague' not in entry + ba.buttonwidget(edit=self.button, + color=(0.5, 0.7, 0.2) if enabled else (0.5, 0.5, 0.5)) + ba.imagewidget(edit=self.lock_image, opacity=0.0 if enabled else 1.0) + ba.textwidget(edit=self.prize_range_1_text, + text='-' if pr1 == '' else pr1, + position=(self.button_x + 365 + x_offs, self.button_y + + self.button_scale_y - 93 + prize_y_offs)) + + # We want to draw values containing tickets a bit smaller + # (scratch that; we now draw medals a bit bigger). + ticket_char = ba.charstr(ba.SpecialChar.TICKET_BACKING) + prize_value_scale_large = 1.0 + prize_value_scale_small = 1.0 + + ba.textwidget(edit=self.prize_value_1_text, + text='-' if pv1 == '' else pv1, + scale=prize_value_scale_large + if ticket_char not in pv1 else prize_value_scale_small, + position=(self.button_x + 380 + x_offs, self.button_y + + self.button_scale_y - 93 + prize_y_offs)) + + ba.textwidget(edit=self.prize_range_2_text, + text=pr2, + position=(self.button_x + 365 + x_offs, self.button_y + + self.button_scale_y - 93 - 45 + prize_y_offs)) + ba.textwidget(edit=self.prize_value_2_text, + text=pv2, + scale=prize_value_scale_large + if ticket_char not in pv2 else prize_value_scale_small, + position=(self.button_x + 380 + x_offs, self.button_y + + self.button_scale_y - 93 - 45 + prize_y_offs)) + + ba.textwidget(edit=self.prize_range_3_text, + text=pr3, + position=(self.button_x + 365 + x_offs, self.button_y + + self.button_scale_y - 93 - 90 + prize_y_offs)) + ba.textwidget(edit=self.prize_value_3_text, + text=pv3, + scale=prize_value_scale_large + if ticket_char not in pv3 else prize_value_scale_small, + position=(self.button_x + 380 + x_offs, self.button_y + + self.button_scale_y - 93 - 90 + prize_y_offs)) + + leader_name = '-' + leader_score: str | ba.Lstr = '-' + if entry['scores']: + score = self.leader = copy.deepcopy(entry['scores'][0]) + leader_name = score[1] + leader_score = (ba.timestring( + score[0] * 10, + centi=True, + timeformat=ba.TimeFormat.MILLISECONDS, + suppress_format_warning=True) + if entry['scoreType'] == 'time' else str(score[0])) + else: + self.leader = None + + ba.textwidget(edit=self.current_leader_name_text, + text=ba.Lstr(value=leader_name)) + ba.textwidget(edit=self.current_leader_score_text, text=leader_score) + ba.buttonwidget(edit=self.more_scores_button, + label=ba.Lstr(resource=self._r + '.seeMoreText')) + out_of_time_text: str | ba.Lstr = ( + '-' if 'totalTime' not in entry else ba.Lstr( + resource=self._r + '.ofTotalTimeText', + subs=[('${TOTAL}', + ba.timestring(entry['totalTime'], + centi=False, + suppress_format_warning=True))])) + ba.textwidget(edit=self.time_remaining_out_of_text, + text=out_of_time_text) + + self.time_remaining = entry['timeRemaining'] + self.has_time_remaining = entry is not None + self.tournament_id = entry['tournamentID'] + self.required_league = (None if 'requiredLeague' not in entry else + entry['requiredLeague']) + + game = ba.app.accounts_v1.tournament_info[self.tournament_id]['game'] + + if game is None: + ba.textwidget(edit=self.button_text, text='-') + ba.imagewidget(edit=self.image, + texture=ba.gettexture('black'), + opacity=0.2) + else: + campaignname, levelname = game.split(':') + campaign = getcampaign(campaignname) + max_players = ba.app.accounts_v1.tournament_info[ + self.tournament_id]['maxPlayers'] + txt = ba.Lstr(value='${A} ${B}', + subs=[('${A}', + campaign.getlevel(levelname).displayname), + ('${B}', + ba.Lstr(resource='playerCountAbbreviatedText', + subs=[('${COUNT}', str(max_players)) + ]))]) + ba.textwidget(edit=self.button_text, text=txt) + ba.imagewidget( + edit=self.image, + texture=campaign.getlevel(levelname).get_preview_texture(), + opacity=1.0 if enabled else 0.5) + + fee = entry['fee'] + + if fee is None: + fee_var = None + elif fee == 4: + fee_var = 'price.tournament_entry_4' + elif fee == 3: + fee_var = 'price.tournament_entry_3' + elif fee == 2: + fee_var = 'price.tournament_entry_2' + elif fee == 1: + fee_var = 'price.tournament_entry_1' + else: + if fee != 0: + print('Unknown fee value:', fee) + fee_var = 'price.tournament_entry_0' + + self.allow_ads = allow_ads = entry['allowAds'] + + final_fee: int | None = (None if fee_var is None else + _ba.get_v1_account_misc_read_val( + fee_var, '?')) + + final_fee_str: str | ba.Lstr + if fee_var is None: + final_fee_str = '' + else: + if final_fee == 0: + final_fee_str = ba.Lstr(resource='getTicketsWindow.freeText') + else: + final_fee_str = (ba.charstr(ba.SpecialChar.TICKET_BACKING) + + str(final_fee)) + + ad_tries_remaining = ba.app.accounts_v1.tournament_info[ + self.tournament_id]['adTriesRemaining'] + free_tries_remaining = ba.app.accounts_v1.tournament_info[ + self.tournament_id]['freeTriesRemaining'] + + # Now, if this fee allows ads and we support video ads, show + # the 'or ad' version. + if allow_ads and _ba.has_video_ads(): + ads_enabled = _ba.have_incentivized_ad() + ba.imagewidget(edit=self.entry_fee_ad_image, + opacity=1.0 if ads_enabled else 0.25) + or_text = ba.Lstr(resource='orText', + subs=[('${A}', ''), + ('${B}', '')]).evaluate().strip() + ba.textwidget(edit=self.entry_fee_text_or, text=or_text) + ba.textwidget(edit=self.entry_fee_text_top, + position=(self.button_x + 360, + self.button_y + self.button_scale_y - 60), + scale=1.3, + text=final_fee_str) + + # Possibly show number of ad-plays remaining. + ba.textwidget(edit=self.entry_fee_text_remaining, + position=(self.button_x + 360, + self.button_y + self.button_scale_y - 146), + text='' if ad_tries_remaining in [None, 0] else + ('' + str(ad_tries_remaining)), + color=(0.6, 0.6, 0.6, 1 if ads_enabled else 0.2)) + else: + ba.imagewidget(edit=self.entry_fee_ad_image, opacity=0.0) + ba.textwidget(edit=self.entry_fee_text_or, text='') + ba.textwidget(edit=self.entry_fee_text_top, + position=(self.button_x + 360, + self.button_y + self.button_scale_y - 80), + scale=1.3, + text=final_fee_str) + + # Possibly show number of free-plays remaining. + ba.textwidget( + edit=self.entry_fee_text_remaining, + position=(self.button_x + 360, + self.button_y + self.button_scale_y - 100), + text=('' if (free_tries_remaining in [None, 0] + or final_fee != 0) else + ('' + str(free_tries_remaining))), + color=(0.6, 0.6, 0.6, 1), + ) diff --git a/dist/ba_data/python/bastd/ui/creditslist.py b/dist/ba_data/python/bastd/ui/creditslist.py index 722fd7a..c266fc5 100644 --- a/dist/ba_data/python/bastd/ui/creditslist.py +++ b/dist/ba_data/python/bastd/ui/creditslist.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: class CreditsListWindow(ba.Window): """Window for displaying game credits.""" - def __init__(self, origin_widget: ba.Widget = None): + def __init__(self, origin_widget: ba.Widget | None = None): # pylint: disable=too-many-locals # pylint: disable=too-many-statements import json @@ -207,10 +207,15 @@ class CreditsListWindow(ba.Window): '\n' + '\n'.join(translation_names.splitlines()[:146]) + '\n'.join(translation_names.splitlines()[146:]) + '\n' '\n' - ' Shout Out to Awesome Mods / Modders:\n\n' + ' Shout Out to Awesome Mods / Modders / Contributors:\n\n' ' BombDash ModPack\n' ' TheMikirog & SoK - BombSquad Joyride Modpack\n' ' Mrmaxmeier - BombSquad-Community-Mod-Manager\n' + ' Ritiek Malhotra \n' + ' Dliwk\n' + ' vishal332008\n' + ' itsre3\n' + ' Drooopyyy\n' '\n' ' Holiday theme vector art designed by Freepik\n' '\n' diff --git a/dist/ba_data/python/bastd/ui/fileselector.py b/dist/ba_data/python/bastd/ui/fileselector.py index 8af14da..bb6dc65 100644 --- a/dist/ba_data/python/bastd/ui/fileselector.py +++ b/dist/ba_data/python/bastd/ui/fileselector.py @@ -21,9 +21,9 @@ class FileSelectorWindow(ba.Window): def __init__(self, path: str, - callback: Callable[[str | None], Any] = None, + callback: Callable[[str | None], Any] | None = None, show_base_path: bool = True, - valid_file_extensions: Sequence[str] = None, + valid_file_extensions: Sequence[str] | None = None, allow_folders: bool = False): if valid_file_extensions is None: valid_file_extensions = [] diff --git a/dist/ba_data/python/bastd/ui/gather/__init__.py b/dist/ba_data/python/bastd/ui/gather/__init__.py index 2eaa49c..f3a1eb9 100644 --- a/dist/ba_data/python/bastd/ui/gather/__init__.py +++ b/dist/ba_data/python/bastd/ui/gather/__init__.py @@ -68,7 +68,7 @@ class GatherWindow(ba.Window): def __init__(self, transition: str | None = 'in_right', - origin_widget: ba.Widget = None): + origin_widget: ba.Widget | None = None): # pylint: disable=too-many-statements # pylint: disable=too-many-locals # pylint: disable=cyclic-import diff --git a/dist/ba_data/python/bastd/ui/gather/manualtab.py b/dist/ba_data/python/bastd/ui/gather/manualtab.py index 0f2053a..0ca6150 100644 --- a/dist/ba_data/python/bastd/ui/gather/manualtab.py +++ b/dist/ba_data/python/bastd/ui/gather/manualtab.py @@ -705,8 +705,8 @@ class ManualGatherTab(GatherTab): from_other_thread=True, ) except Exception as exc: - from efro.error import is_udp_network_error - if is_udp_network_error(exc): + from efro.error import is_udp_communication_error + if is_udp_communication_error(exc): ba.pushcall(ba.Call( _safe_set_text, self._checking_state_text, ba.Lstr(resource='gatherWindow.' diff --git a/dist/ba_data/python/bastd/ui/gather/publictab.py b/dist/ba_data/python/bastd/ui/gather/publictab.py index 5eaa282..726cf9e 100644 --- a/dist/ba_data/python/bastd/ui/gather/publictab.py +++ b/dist/ba_data/python/bastd/ui/gather/publictab.py @@ -219,9 +219,9 @@ class AddrFetchThread(threading.Thread): sock.close() ba.pushcall(ba.Call(self._call, val), from_other_thread=True) except Exception as exc: - from efro.error import is_udp_network_error + from efro.error import is_udp_communication_error # Ignore expected network errors; log others. - if is_udp_network_error(exc): + if is_udp_communication_error(exc): pass else: ba.print_exception() @@ -271,8 +271,8 @@ class PingThread(threading.Thread): ping if accessible else None), from_other_thread=True) except Exception as exc: - from efro.error import is_udp_network_error - if is_udp_network_error(exc): + from efro.error import is_udp_communication_error + if is_udp_communication_error(exc): pass else: ba.print_exception('Error on gather ping', once=True) diff --git a/dist/ba_data/python/bastd/ui/getcurrency.py b/dist/ba_data/python/bastd/ui/getcurrency.py index e0ef860..bc16517 100644 --- a/dist/ba_data/python/bastd/ui/getcurrency.py +++ b/dist/ba_data/python/bastd/ui/getcurrency.py @@ -20,8 +20,8 @@ class GetCurrencyWindow(ba.Window): transition: str = 'in_right', from_modal_store: bool = False, modal: bool = False, - origin_widget: ba.Widget = None, - store_back_location: str = None): + origin_widget: ba.Widget | None = None, + store_back_location: str | None = None): # pylint: disable=too-many-statements # pylint: disable=too-many-locals @@ -105,8 +105,8 @@ class GetCurrencyWindow(ba.Window): position: tuple[float, float], size: tuple[float, float], label: ba.Lstr, - price: str = None, - tex_name: str = None, + price: str | None = None, + tex_name: str | None = None, tex_opacity: float = 1.0, tex_scale: float = 1.0, enabled: bool = True, diff --git a/dist/ba_data/python/bastd/ui/helpui.py b/dist/ba_data/python/bastd/ui/helpui.py index 09bc4d5..79369a9 100644 --- a/dist/ba_data/python/bastd/ui/helpui.py +++ b/dist/ba_data/python/bastd/ui/helpui.py @@ -18,7 +18,7 @@ class HelpWindow(ba.Window): def __init__(self, main_menu: bool = False, - origin_widget: ba.Widget = None): + origin_widget: ba.Widget | None = None): # pylint: disable=too-many-statements # pylint: disable=too-many-locals from ba.internal import get_remote_app_name diff --git a/dist/ba_data/python/bastd/ui/iconpicker.py b/dist/ba_data/python/bastd/ui/iconpicker.py index a9d5727..9bd98fe 100644 --- a/dist/ba_data/python/bastd/ui/iconpicker.py +++ b/dist/ba_data/python/bastd/ui/iconpicker.py @@ -22,11 +22,11 @@ class IconPicker(popup.PopupWindow): parent: ba.Widget, position: tuple[float, float] = (0.0, 0.0), delegate: Any = None, - scale: float = None, + scale: float | None = None, offset: tuple[float, float] = (0.0, 0.0), tint_color: Sequence[float] = (1.0, 1.0, 1.0), tint2_color: Sequence[float] = (1.0, 1.0, 1.0), - selected_icon: str = None): + selected_icon: str | None = None): # pylint: disable=too-many-locals del parent # unused here del tint_color # unused_here diff --git a/dist/ba_data/python/bastd/ui/league/rankbutton.py b/dist/ba_data/python/bastd/ui/league/rankbutton.py index 9b62fc1..8509eb8 100644 --- a/dist/ba_data/python/bastd/ui/league/rankbutton.py +++ b/dist/ba_data/python/bastd/ui/league/rankbutton.py @@ -21,11 +21,11 @@ class LeagueRankButton: position: tuple[float, float], size: tuple[float, float], scale: float, - on_activate_call: Callable[[], Any] = None, - transition_delay: float = None, - color: tuple[float, float, float] = None, - textcolor: tuple[float, float, float] = None, - smooth_update_delay: float = None): + on_activate_call: Callable[[], Any] | None = None, + transition_delay: float | None = None, + color: tuple[float, float, float] | None = None, + textcolor: tuple[float, float, float] | None = None, + smooth_update_delay: float | None = None): if on_activate_call is None: on_activate_call = ba.WeakCall(self._default_on_activate_call) self._on_activate_call = on_activate_call diff --git a/dist/ba_data/python/bastd/ui/league/rankwindow.py b/dist/ba_data/python/bastd/ui/league/rankwindow.py index 3a23f89..a29f618 100644 --- a/dist/ba_data/python/bastd/ui/league/rankwindow.py +++ b/dist/ba_data/python/bastd/ui/league/rankwindow.py @@ -21,7 +21,7 @@ class LeagueRankWindow(ba.Window): def __init__(self, transition: str = 'in_right', modal: bool = False, - origin_widget: ba.Widget = None): + origin_widget: ba.Widget | None = None): ba.set_analytics_screen('League Rank Window') self._league_rank_data: dict[str, Any] | None = None diff --git a/dist/ba_data/python/bastd/ui/onscreenkeyboard.py b/dist/ba_data/python/bastd/ui/onscreenkeyboard.py index c55917f..69d2303 100644 --- a/dist/ba_data/python/bastd/ui/onscreenkeyboard.py +++ b/dist/ba_data/python/bastd/ui/onscreenkeyboard.py @@ -213,8 +213,8 @@ class OnScreenKeyboardWindow(ba.Window): # Show change instructions only if we have more than one # keyboard option. - if (ba.app.meta.metascan is not None - and len(ba.app.meta.metascan.keyboards) > 1): + if (ba.app.meta.scanresults is not None + and len(ba.app.meta.scanresults.keyboards) > 1): ba.textwidget( parent=self._root_widget, h_align='center', @@ -238,8 +238,8 @@ class OnScreenKeyboardWindow(ba.Window): self._refresh() def _get_keyboard(self) -> ba.Keyboard: - assert ba.app.meta.metascan is not None - classname = ba.app.meta.metascan.keyboards[self._keyboard_index] + assert ba.app.meta.scanresults is not None + classname = ba.app.meta.scanresults.keyboards[self._keyboard_index] kbclass = ba.getclass(classname, ba.Keyboard) return kbclass() @@ -317,11 +317,11 @@ class OnScreenKeyboardWindow(ba.Window): self._refresh() def _next_keyboard(self) -> None: - assert ba.app.meta.metascan is not None + assert ba.app.meta.scanresults is not None self._keyboard_index = (self._keyboard_index + 1) % len( - ba.app.meta.metascan.keyboards) + ba.app.meta.scanresults.keyboards) self._load_keyboard() - if len(ba.app.meta.metascan.keyboards) < 2: + if len(ba.app.meta.scanresults.keyboards) < 2: ba.playsound(ba.getsound('error')) ba.screenmessage(ba.Lstr(resource='keyboardNoOthersAvailableText'), color=(1, 0, 0)) diff --git a/dist/ba_data/python/bastd/ui/play.py b/dist/ba_data/python/bastd/ui/play.py index 62809b0..6f872f8 100644 --- a/dist/ba_data/python/bastd/ui/play.py +++ b/dist/ba_data/python/bastd/ui/play.py @@ -18,7 +18,7 @@ class PlayWindow(ba.Window): def __init__(self, transition: str = 'in_right', - origin_widget: ba.Widget = None): + origin_widget: ba.Widget | None = None): # pylint: disable=too-many-statements # pylint: disable=too-many-locals import threading diff --git a/dist/ba_data/python/bastd/ui/playlist/browser.py b/dist/ba_data/python/bastd/ui/playlist/browser.py index 9dcab82..eace126 100644 --- a/dist/ba_data/python/bastd/ui/playlist/browser.py +++ b/dist/ba_data/python/bastd/ui/playlist/browser.py @@ -21,7 +21,7 @@ class PlaylistBrowserWindow(ba.Window): def __init__(self, sessiontype: type[ba.Session], transition: str | None = 'in_right', - origin_widget: ba.Widget = None): + origin_widget: ba.Widget | None = None): # pylint: disable=too-many-statements # pylint: disable=cyclic-import from bastd.ui.playlist import PlaylistTypeVars diff --git a/dist/ba_data/python/bastd/ui/playlist/customizebrowser.py b/dist/ba_data/python/bastd/ui/playlist/customizebrowser.py index c786aea..6befcfd 100644 --- a/dist/ba_data/python/bastd/ui/playlist/customizebrowser.py +++ b/dist/ba_data/python/bastd/ui/playlist/customizebrowser.py @@ -21,8 +21,8 @@ class PlaylistCustomizeBrowserWindow(ba.Window): def __init__(self, sessiontype: type[ba.Session], transition: str = 'in_right', - select_playlist: str = None, - origin_widget: ba.Widget = None): + select_playlist: str | None = None, + origin_widget: ba.Widget | None = None): # Yes this needs tidying. # pylint: disable=too-many-locals # pylint: disable=too-many-statements @@ -297,7 +297,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): _ba.fade_screen(False, endcall=self._run_selected_playlist) _ba.lock_all_input() - def _refresh(self, select_playlist: str = None) -> None: + def _refresh(self, select_playlist: str | None = None) -> None: from efro.util import asserttype old_selection = self._selected_playlist_name diff --git a/dist/ba_data/python/bastd/ui/playlist/editcontroller.py b/dist/ba_data/python/bastd/ui/playlist/editcontroller.py index a2a1c0b..c780135 100644 --- a/dist/ba_data/python/bastd/ui/playlist/editcontroller.py +++ b/dist/ba_data/python/bastd/ui/playlist/editcontroller.py @@ -18,10 +18,10 @@ class PlaylistEditController: def __init__(self, sessiontype: type[ba.Session], - existing_playlist_name: str = None, + existing_playlist_name: str | None = None, transition: str = 'in_right', - playlist: list[dict[str, Any]] = None, - playlist_name: str = None): + playlist: list[dict[str, Any]] | None = None, + playlist_name: str | None = None): from ba.internal import preload_map_preview_media, filter_playlist from bastd.ui.playlist import PlaylistTypeVars from bastd.ui.playlist.edit import PlaylistEditWindow diff --git a/dist/ba_data/python/bastd/ui/playlist/editgame.py b/dist/ba_data/python/bastd/ui/playlist/editgame.py index a4c0007..9674357 100644 --- a/dist/ba_data/python/bastd/ui/playlist/editgame.py +++ b/dist/ba_data/python/bastd/ui/playlist/editgame.py @@ -23,9 +23,9 @@ class PlaylistEditGameWindow(ba.Window): sessiontype: type[ba.Session], config: dict[str, Any] | None, completion_call: Callable[[dict[str, Any] | None], Any], - default_selection: str = None, + default_selection: str | None = None, transition: str = 'in_right', - edit_info: dict[str, Any] = None): + edit_info: dict[str, Any] | None = None): # pylint: disable=too-many-branches # pylint: disable=too-many-statements # pylint: disable=too-many-locals diff --git a/dist/ba_data/python/bastd/ui/playlist/share.py b/dist/ba_data/python/bastd/ui/playlist/share.py index f2987ca..084fca6 100644 --- a/dist/ba_data/python/bastd/ui/playlist/share.py +++ b/dist/ba_data/python/bastd/ui/playlist/share.py @@ -19,8 +19,8 @@ class SharePlaylistImportWindow(promocode.PromoCodeWindow): """Window for importing a shared playlist.""" def __init__(self, - origin_widget: ba.Widget = None, - on_success_callback: Callable[[], Any] = None): + origin_widget: ba.Widget | None = None, + on_success_callback: Callable[[], Any] | None = None): promocode.PromoCodeWindow.__init__(self, modal=True, origin_widget=origin_widget) diff --git a/dist/ba_data/python/bastd/ui/popup.py b/dist/ba_data/python/bastd/ui/popup.py index 2146263..0315b94 100644 --- a/dist/ba_data/python/bastd/ui/popup.py +++ b/dist/ba_data/python/bastd/ui/popup.py @@ -26,7 +26,7 @@ class PopupWindow: offset: tuple[float, float] = (0, 0), bg_color: tuple[float, float, float] = (0.35, 0.55, 0.15), focus_position: tuple[float, float] = (0, 0), - focus_size: tuple[float, float] = None, + focus_size: tuple[float, float] | None = None, toolbar_visibility: str = 'menu_minimal_no_back'): # pylint: disable=too-many-locals if focus_size is None: @@ -108,10 +108,10 @@ class PopupMenuWindow(PopupWindow): current_choice: str, delegate: Any = None, width: float = 230.0, - maxwidth: float = None, + maxwidth: float | None = None, scale: float = 1.0, - choices_disabled: Sequence[str] = None, - choices_display: Sequence[ba.Lstr] = None): + choices_disabled: Sequence[str] | None = None, + choices_display: Sequence[ba.Lstr] | None = None): # FIXME: Clean up a bit. # pylint: disable=too-many-branches # pylint: disable=too-many-locals @@ -268,15 +268,15 @@ class PopupMenu: parent: ba.Widget, position: tuple[float, float], choices: Sequence[str], - current_choice: str = None, - on_value_change_call: Callable[[str], Any] = None, - opening_call: Callable[[], Any] = None, - closing_call: Callable[[], Any] = None, + current_choice: str | None = None, + on_value_change_call: Callable[[str], Any] | None = None, + opening_call: Callable[[], Any] | None = None, + closing_call: Callable[[], Any] | None = None, width: float = 230.0, - maxwidth: float = None, - scale: float = None, - choices_disabled: Sequence[str] = None, - choices_display: Sequence[ba.Lstr] = None, + maxwidth: float | None = None, + scale: float | None = None, + choices_disabled: Sequence[str] | None = None, + choices_display: Sequence[ba.Lstr] | None = None, button_size: tuple[float, float] = (160.0, 50.0), autoselect: bool = True): # pylint: disable=too-many-locals diff --git a/dist/ba_data/python/bastd/ui/profile/browser.py b/dist/ba_data/python/bastd/ui/profile/browser.py index febfa83..676b491 100644 --- a/dist/ba_data/python/bastd/ui/profile/browser.py +++ b/dist/ba_data/python/bastd/ui/profile/browser.py @@ -19,8 +19,8 @@ class ProfileBrowserWindow(ba.Window): def __init__(self, transition: str = 'in_right', in_main_menu: bool = True, - selected_profile: str = None, - origin_widget: ba.Widget = None): + selected_profile: str | None = None, + origin_widget: ba.Widget | None = None): # pylint: disable=too-many-statements # pylint: disable=too-many-locals self._in_main_menu = in_main_menu diff --git a/dist/ba_data/python/bastd/ui/promocode.py b/dist/ba_data/python/bastd/ui/promocode.py index eb3bf01..dc39078 100644 --- a/dist/ba_data/python/bastd/ui/promocode.py +++ b/dist/ba_data/python/bastd/ui/promocode.py @@ -17,7 +17,9 @@ if TYPE_CHECKING: class PromoCodeWindow(ba.Window): """Window for entering promo codes.""" - def __init__(self, modal: bool = False, origin_widget: ba.Widget = None): + def __init__(self, + modal: bool = False, + origin_widget: ba.Widget | None = None): scale_origin: tuple[float, float] | None if origin_widget is not None: diff --git a/dist/ba_data/python/bastd/ui/purchase.py b/dist/ba_data/python/bastd/ui/purchase.py index e5da106..8b729c7 100644 --- a/dist/ba_data/python/bastd/ui/purchase.py +++ b/dist/ba_data/python/bastd/ui/purchase.py @@ -19,7 +19,7 @@ class PurchaseWindow(ba.Window): def __init__(self, items: list[str], transition: str = 'in_right', - header_text: ba.Lstr = None): + header_text: ba.Lstr | None = None): from ba.internal import get_store_item_display_size from bastd.ui.store import item as storeitemui if header_text is None: diff --git a/dist/ba_data/python/bastd/ui/settings/advanced.py b/dist/ba_data/python/bastd/ui/settings/advanced.py index 665d86f..bed3f01 100644 --- a/dist/ba_data/python/bastd/ui/settings/advanced.py +++ b/dist/ba_data/python/bastd/ui/settings/advanced.py @@ -19,7 +19,7 @@ class AdvancedSettingsWindow(ba.Window): def __init__(self, transition: str = 'in_right', - origin_widget: ba.Widget = None): + origin_widget: ba.Widget | None = None): # pylint: disable=too-many-statements from ba.internal import master_server_get import threading @@ -56,6 +56,7 @@ class AdvancedSettingsWindow(ba.Window): scale=(2.06 if uiscale is ba.UIScale.SMALL else 1.4 if uiscale is ba.UIScale.MEDIUM else 1.0), stack_offset=(0, -25) if uiscale is ba.UIScale.SMALL else (0, 0))) + self._prev_lang = '' self._prev_lang_list: list[str] = [] self._complete_langs_list: list | None = None @@ -423,29 +424,6 @@ class AdvancedSettingsWindow(ba.Window): v -= self._spacing * 2.1 this_button_width = 410 - self._show_user_mods_button = ba.buttonwidget( - parent=self._subcontainer, - position=(self._sub_width / 2 - this_button_width / 2, v - 10), - size=(this_button_width, 60), - autoselect=True, - label=ba.Lstr(resource=self._r + '.showUserModsText'), - text_scale=1.0, - on_activate_call=show_user_scripts) - if self._show_always_use_internal_keyboard: - assert self._always_use_internal_keyboard_check_box is not None - ba.widget(edit=self._always_use_internal_keyboard_check_box.widget, - down_widget=self._show_user_mods_button) - ba.widget( - edit=self._show_user_mods_button, - up_widget=self._always_use_internal_keyboard_check_box.widget) - else: - ba.widget(edit=self._show_user_mods_button, - up_widget=self._kick_idle_players_check_box.widget) - ba.widget(edit=self._kick_idle_players_check_box.widget, - down_widget=self._show_user_mods_button) - - v -= self._spacing * 2.0 - self._modding_guide_button = ba.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 10), @@ -454,8 +432,30 @@ class AdvancedSettingsWindow(ba.Window): label=ba.Lstr(resource=self._r + '.moddingGuideText'), text_scale=1.0, on_activate_call=ba.Call( - ba.open_url, - 'http://www.froemling.net/docs/bombsquad-modding-guide')) + ba.open_url, 'http://ballistica.net/wiki/modding-guide')) + if self._show_always_use_internal_keyboard: + assert self._always_use_internal_keyboard_check_box is not None + ba.widget(edit=self._always_use_internal_keyboard_check_box.widget, + down_widget=self._modding_guide_button) + ba.widget( + edit=self._modding_guide_button, + up_widget=self._always_use_internal_keyboard_check_box.widget) + else: + ba.widget(edit=self._modding_guide_button, + up_widget=self._kick_idle_players_check_box.widget) + ba.widget(edit=self._kick_idle_players_check_box.widget, + down_widget=self._modding_guide_button) + + v -= self._spacing * 2.0 + + self._show_user_mods_button = ba.buttonwidget( + parent=self._subcontainer, + position=(self._sub_width / 2 - this_button_width / 2, v - 10), + size=(this_button_width, 60), + autoselect=True, + label=ba.Lstr(resource=self._r + '.showUserModsText'), + text_scale=1.0, + on_activate_call=show_user_scripts) v -= self._spacing * 2.0 @@ -542,6 +542,14 @@ class AdvancedSettingsWindow(ba.Window): def _on_net_test_press(self) -> None: from bastd.ui.settings.nettesting import NetTestingWindow + + # Net-testing requires a signed in v1 account. + if _ba.get_v1_account_state() != 'signed_in': + ba.screenmessage(ba.Lstr(resource='notSignedInErrorText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + self._save_state() ba.containerwidget(edit=self._root_widget, transition='out_left') ba.app.ui.set_main_menu_window( diff --git a/dist/ba_data/python/bastd/ui/settings/allsettings.py b/dist/ba_data/python/bastd/ui/settings/allsettings.py index 8b18773..bec6f05 100644 --- a/dist/ba_data/python/bastd/ui/settings/allsettings.py +++ b/dist/ba_data/python/bastd/ui/settings/allsettings.py @@ -18,7 +18,7 @@ class AllSettingsWindow(ba.Window): def __init__(self, transition: str = 'in_right', - origin_widget: ba.Widget = None): + origin_widget: ba.Widget | None = None): # pylint: disable=too-many-statements # pylint: disable=too-many-locals import threading diff --git a/dist/ba_data/python/bastd/ui/settings/audio.py b/dist/ba_data/python/bastd/ui/settings/audio.py index 9ca2979..bf93f6b 100644 --- a/dist/ba_data/python/bastd/ui/settings/audio.py +++ b/dist/ba_data/python/bastd/ui/settings/audio.py @@ -18,7 +18,7 @@ class AudioSettingsWindow(ba.Window): def __init__(self, transition: str = 'in_right', - origin_widget: ba.Widget = None): + origin_widget: ba.Widget | None = None): # pylint: disable=too-many-statements # pylint: disable=too-many-locals # pylint: disable=cyclic-import diff --git a/dist/ba_data/python/bastd/ui/settings/controls.py b/dist/ba_data/python/bastd/ui/settings/controls.py index 0c17fe8..017568d 100644 --- a/dist/ba_data/python/bastd/ui/settings/controls.py +++ b/dist/ba_data/python/bastd/ui/settings/controls.py @@ -18,7 +18,7 @@ class ControlsSettingsWindow(ba.Window): def __init__(self, transition: str = 'in_right', - origin_widget: ba.Widget = None): + origin_widget: ba.Widget | None = None): # FIXME: should tidy up here. # pylint: disable=too-many-statements # pylint: disable=too-many-branches diff --git a/dist/ba_data/python/bastd/ui/settings/gamepad.py b/dist/ba_data/python/bastd/ui/settings/gamepad.py index 7f6f184..ea86a95 100644 --- a/dist/ba_data/python/bastd/ui/settings/gamepad.py +++ b/dist/ba_data/python/bastd/ui/settings/gamepad.py @@ -21,7 +21,7 @@ class GamepadSettingsWindow(ba.Window): is_main_menu: bool = True, transition: str = 'in_right', transition_out: str = 'out_right', - settings: dict = None): + settings: dict | None = None): self._input = gamepad # If our input-device went away, just return an empty zombie. @@ -652,8 +652,8 @@ class GamepadSettingsWindow(ba.Window): texture: ba.Texture, button: str, scale: float = 1.0, - message: ba.Lstr = None, - message2: ba.Lstr = None, + message: ba.Lstr | None = None, + message2: ba.Lstr | None = None, maxwidth: float = 80.0) -> ba.Widget: if message is None: message = ba.Lstr(resource=self._r + '.pressAnyButtonText') @@ -754,8 +754,8 @@ class AwaitGamepadInputWindow(ba.Window): button: str, callback: Callable[[str, dict[str, Any], AwaitGamepadInputWindow], Any], - message: ba.Lstr = None, - message2: ba.Lstr = None): + message: ba.Lstr | None = None, + message2: ba.Lstr | None = None): if message is None: print('AwaitGamepadInputWindow message is None!') # Shouldn't get here. diff --git a/dist/ba_data/python/bastd/ui/settings/gamepadadvanced.py b/dist/ba_data/python/bastd/ui/settings/gamepadadvanced.py index d0eb646..e97b318 100644 --- a/dist/ba_data/python/bastd/ui/settings/gamepadadvanced.py +++ b/dist/ba_data/python/bastd/ui/settings/gamepadadvanced.py @@ -404,7 +404,7 @@ class GamepadAdvancedSettingsWindow(ba.Window): increment: float = 1.0, change_sound: bool = True, x_offset: float = 0.0, - displayname: ba.Lstr = None) -> tuple[ba.Widget, ba.Widget]: + displayname: ba.Lstr | None = None) -> tuple[ba.Widget, ba.Widget]: if displayname is None: displayname = name diff --git a/dist/ba_data/python/bastd/ui/settings/graphics.py b/dist/ba_data/python/bastd/ui/settings/graphics.py index 7a0da71..8d0fff0 100644 --- a/dist/ba_data/python/bastd/ui/settings/graphics.py +++ b/dist/ba_data/python/bastd/ui/settings/graphics.py @@ -18,7 +18,7 @@ class GraphicsSettingsWindow(ba.Window): def __init__(self, transition: str = 'in_right', - origin_widget: ba.Widget = None): + origin_widget: ba.Widget | None = None): # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements diff --git a/dist/ba_data/python/bastd/ui/settings/nettesting.py b/dist/ba_data/python/bastd/ui/settings/nettesting.py index 536e4e0..9c2700f 100644 --- a/dist/ba_data/python/bastd/ui/settings/nettesting.py +++ b/dist/ba_data/python/bastd/ui/settings/nettesting.py @@ -10,6 +10,7 @@ import weakref from threading import Thread from typing import TYPE_CHECKING +from efro.error import CleanError import _ba import ba from bastd.ui.settings.testing import TestingWindow @@ -131,7 +132,8 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None: # We're running in a background thread but UI stuff needs to run # in the logic thread; give ourself a way to pass stuff to it. - def _print(text: str, color: tuple[float, float, float] = None) -> None: + def _print(text: str, + color: tuple[float, float, float] | None = None) -> None: def _print_in_logic_thread() -> None: win = weakwin() @@ -147,10 +149,12 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None: call() duration = time.monotonic() - starttime _print(f'Succeeded in {duration:.2f}s.', color=(0, 1, 0)) - except Exception: + except Exception as exc: import traceback duration = time.monotonic() - starttime - _print(traceback.format_exc(), color=(1.0, 1.0, 0.3)) + msg = (str(exc) + if isinstance(exc, CleanError) else traceback.format_exc()) + _print(msg, color=(1.0, 1.0, 0.3)) _print(f'Failed in {duration:.2f}s.', color=(1, 0, 0)) have_error[0] = True @@ -192,6 +196,9 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None: _print(f'\nContacting V2 master-server ({baseaddr})...') _print_test_results(lambda: _test_fetch(baseaddr)) + _print('\nComparing local time to V2 server...') + _print_test_results(_test_v2_time) + # Get V2 nearby zone with ba.app.net.zone_pings_lock: zone_pings = copy.deepcopy(ba.app.net.zone_pings) @@ -205,6 +212,9 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None: _print(f'\nChecking nearest V2 zone ping ({nearstr})...') _print_test_results(lambda: _test_nearby_zone_ping(nearest_zone)) + _print('\nSending V2 cloud message...') + _print_test_results(_test_v2_cloud_message) + if have_error[0]: _print('\nDiagnostics complete. Some diagnostics failed.', color=(10, 0, 0)) @@ -270,12 +280,67 @@ def _test_v1_transaction() -> None: raise RuntimeError(results[0]) +def _test_v2_cloud_message() -> None: + from dataclasses import dataclass + import bacommon.cloud + + @dataclass + class _Results: + errstr: str | None = None + send_time: float | None = None + response_time: float | None = None + + results = _Results() + + def _cb(response: bacommon.cloud.PingResponse | Exception) -> None: + # Note: this runs in another thread so need to avoid exceptions. + results.response_time = time.monotonic() + if isinstance(response, Exception): + results.errstr = str(response) + if not isinstance(response, bacommon.cloud.PingResponse): + results.errstr = f'invalid response type: {type(response)}.' + + def _send() -> None: + # Note: this runs in another thread so need to avoid exceptions. + results.send_time = time.monotonic() + ba.app.cloud.send_message_cb(bacommon.cloud.PingMessage(), _cb) + + # This stuff expects to be run from the logic thread. + ba.pushcall(_send, from_other_thread=True) + + wait_start_time = time.monotonic() + while True: + if results.response_time is not None: + break + time.sleep(0.01) + if time.monotonic() - wait_start_time > 10.0: + raise RuntimeError('Timeout waiting for cloud message response') + if results.errstr is not None: + raise RuntimeError(results.errstr) + + +def _test_v2_time() -> None: + offset = ba.app.net.server_time_offset_hours + if offset is None: + raise RuntimeError('no time offset found;' + ' perhaps unable to communicate with v2 server?') + if abs(offset) >= 2.0: + raise CleanError( + f'Your device time is off from world time by {offset:.1f} hours.\n' + 'This may cause network operations to fail due to your device\n' + ' incorrectly treating SSL certificates as not-yet-valid, etc.\n' + 'Check your device time and time-zone settings to fix this.\n') + + def _test_fetch(baseaddr: str) -> None: # pylint: disable=consider-using-with import urllib.request - response = urllib.request.urlopen(urllib.request.Request( - f'{baseaddr}/ping', None, {'User-Agent': _ba.app.user_agent_string}), - timeout=10.0) + response = urllib.request.urlopen( + urllib.request.Request(f'{baseaddr}/ping', None, + {'User-Agent': _ba.app.user_agent_string}), + context=ba.app.net.sslcontext, + timeout=10.0, + ) if response.getcode() != 200: raise RuntimeError( f'Got unexpected response code {response.getcode()}.') diff --git a/dist/ba_data/python/bastd/ui/settings/plugins.py b/dist/ba_data/python/bastd/ui/settings/plugins.py index b872368..c90b73d 100644 --- a/dist/ba_data/python/bastd/ui/settings/plugins.py +++ b/dist/ba_data/python/bastd/ui/settings/plugins.py @@ -17,7 +17,7 @@ class PluginSettingsWindow(ba.Window): def __init__(self, transition: str = 'in_right', - origin_widget: ba.Widget = None): + origin_widget: ba.Widget | None = None): # pylint: disable=too-many-locals app = ba.app @@ -93,7 +93,7 @@ class PluginSettingsWindow(ba.Window): self._subcontainer = ba.columnwidget(parent=self._scrollwidget, selection_loops_to_parent=True) - if ba.app.meta.metascan is None: + if ba.app.meta.scanresults is None: ba.screenmessage('Still scanning plugins; please try again.', color=(1, 0, 0)) ba.playsound(ba.getsound('error')) diff --git a/dist/ba_data/python/bastd/ui/soundtrack/browser.py b/dist/ba_data/python/bastd/ui/soundtrack/browser.py index dd98728..b208815 100644 --- a/dist/ba_data/python/bastd/ui/soundtrack/browser.py +++ b/dist/ba_data/python/bastd/ui/soundtrack/browser.py @@ -19,7 +19,7 @@ class SoundtrackBrowserWindow(ba.Window): def __init__(self, transition: str = 'in_right', - origin_widget: ba.Widget = None): + origin_widget: ba.Widget | None = None): # pylint: disable=too-many-locals # pylint: disable=too-many-statements @@ -355,7 +355,7 @@ class SoundtrackBrowserWindow(ba.Window): return ba.Lstr(resource=self._r + '.defaultSoundtrackNameText') return ba.Lstr(value=soundtrack) - def _refresh(self, select_soundtrack: str = None) -> None: + def _refresh(self, select_soundtrack: str | None = None) -> None: from efro.util import asserttype self._allow_changing_soundtracks = False old_selection = self._selected_soundtrack diff --git a/dist/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py b/dist/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py index e3bc650..acfdef2 100644 --- a/dist/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py +++ b/dist/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py @@ -160,7 +160,7 @@ class SoundtrackEntryTypeSelectWindow(ba.Window): from ba.osmusic import OSMusicPlayer from bastd.ui.fileselector import FileSelectorWindow ba.containerwidget(edit=self._root_widget, transition='out_left') - base_path = _ba.android_get_external_storage_path() + base_path = _ba.android_get_external_files_dir() ba.app.ui.set_main_menu_window( FileSelectorWindow( base_path, @@ -173,7 +173,7 @@ class SoundtrackEntryTypeSelectWindow(ba.Window): def _on_music_folder_press(self) -> None: from bastd.ui.fileselector import FileSelectorWindow ba.containerwidget(edit=self._root_widget, transition='out_left') - base_path = _ba.android_get_external_storage_path() + base_path = _ba.android_get_external_files_dir() ba.app.ui.set_main_menu_window( FileSelectorWindow(base_path, callback=self._music_folder_selector_cb, diff --git a/dist/ba_data/python/bastd/ui/store/browser.py b/dist/ba_data/python/bastd/ui/store/browser.py index 3966567..042a225 100644 --- a/dist/ba_data/python/bastd/ui/store/browser.py +++ b/dist/ba_data/python/bastd/ui/store/browser.py @@ -31,10 +31,10 @@ class StoreBrowserWindow(ba.Window): def __init__(self, transition: str = 'in_right', modal: bool = False, - show_tab: StoreBrowserWindow.TabID = None, - on_close_call: Callable[[], Any] = None, - back_location: str = None, - origin_widget: ba.Widget = None): + show_tab: StoreBrowserWindow.TabID | None = None, + on_close_call: Callable[[], Any] | None = None, + back_location: str | None = None, + origin_widget: ba.Widget | None = None): # pylint: disable=too-many-statements # pylint: disable=too-many-locals from bastd.ui.tabs import TabRow diff --git a/dist/ba_data/python/bastd/ui/store/button.py b/dist/ba_data/python/bastd/ui/store/button.py index c525fec..b766464 100644 --- a/dist/ba_data/python/bastd/ui/store/button.py +++ b/dist/ba_data/python/bastd/ui/store/button.py @@ -20,12 +20,12 @@ class StoreButton: position: Sequence[float], size: Sequence[float], scale: float, - on_activate_call: Callable[[], Any] = None, - transition_delay: float = None, - color: Sequence[float] = None, - textcolor: Sequence[float] = None, + on_activate_call: Callable[[], Any] | None = None, + transition_delay: float | None = None, + color: Sequence[float] | None = None, + textcolor: Sequence[float] | None = None, show_tickets: bool = False, - button_type: str = None, + button_type: str | None = None, sale_scale: float = 1.0): self._position = position self._size = size diff --git a/dist/ba_data/python/bastd/ui/tabs.py b/dist/ba_data/python/bastd/ui/tabs.py index 99ae0db..fce571f 100644 --- a/dist/ba_data/python/bastd/ui/tabs.py +++ b/dist/ba_data/python/bastd/ui/tabs.py @@ -35,7 +35,7 @@ class TabRow(Generic[T]): tabdefs: list[tuple[T, ba.Lstr]], pos: tuple[float, float], size: tuple[float, float], - on_select_call: Callable[[T], None] = None) -> None: + on_select_call: Callable[[T], None] | None = None) -> None: if not tabdefs: raise ValueError('At least one tab def is required') self.tabs: dict[T, Tab] = {} diff --git a/dist/ba_data/python/bastd/ui/tournamententry.py b/dist/ba_data/python/bastd/ui/tournamententry.py index f521380..a495792 100644 --- a/dist/ba_data/python/bastd/ui/tournamententry.py +++ b/dist/ba_data/python/bastd/ui/tournamententry.py @@ -19,12 +19,12 @@ class TournamentEntryWindow(popup.PopupWindow): def __init__(self, tournament_id: str, - tournament_activity: ba.Activity = None, + tournament_activity: ba.Activity | None = None, position: tuple[float, float] = (0.0, 0.0), delegate: Any = None, - scale: float = None, + scale: float | None = None, offset: tuple[float, float] = (0.0, 0.0), - on_close_call: Callable[[], Any] = None): + on_close_call: Callable[[], Any] | None = None): # Needs some tidying. # pylint: disable=too-many-branches # pylint: disable=too-many-statements @@ -182,8 +182,9 @@ class TournamentEntryWindow(popup.PopupWindow): h_align='center', v_align='center', scale=0.6, - text=ba.Lstr(resource='watchAVideoText', - fallback_resource='watchAnAdText'), + # Note: AdMob now requires rewarded ad usage + # specifically says 'Ad' in it. + text=ba.Lstr(resource='watchAnAdText'), maxwidth=95, color=(0, 1, 0)) ad_plays_remaining_text = ( diff --git a/dist/ba_data/python/bastd/ui/tournamentscores.py b/dist/ba_data/python/bastd/ui/tournamentscores.py index c3ba873..609bde0 100644 --- a/dist/ba_data/python/bastd/ui/tournamentscores.py +++ b/dist/ba_data/python/bastd/ui/tournamentscores.py @@ -19,14 +19,14 @@ class TournamentScoresWindow(popup_ui.PopupWindow): def __init__(self, tournament_id: str, - tournament_activity: ba.GameActivity = None, + tournament_activity: ba.GameActivity | None = None, position: tuple[float, float] = (0.0, 0.0), - scale: float = None, + scale: float | None = None, offset: tuple[float, float] = (0.0, 0.0), tint_color: Sequence[float] = (1.0, 1.0, 1.0), tint2_color: Sequence[float] = (1.0, 1.0, 1.0), - selected_character: str = None, - on_close_call: Callable[[], Any] = None): + selected_character: str | None = None, + on_close_call: Callable[[], Any] | None = None): del tournament_activity # unused arg del tint_color # unused arg diff --git a/dist/ba_data/python/bastd/ui/trophies.py b/dist/ba_data/python/bastd/ui/trophies.py index 4e0ceac..ec17007 100644 --- a/dist/ba_data/python/bastd/ui/trophies.py +++ b/dist/ba_data/python/bastd/ui/trophies.py @@ -19,7 +19,7 @@ class TrophiesWindow(popup.PopupWindow): def __init__(self, position: tuple[float, float], data: dict[str, Any], - scale: float = None): + scale: float | None = None): self._data = data uiscale = ba.app.ui.uiscale if scale is None: @@ -112,7 +112,7 @@ class TrophiesWindow(popup.PopupWindow): sub_width: int, trophy_types: list[list[str]]) -> int: from ba.internal import get_trophy_string - pts = 0 + total_pts = 0 for i, trophy_type in enumerate(trophy_types): t_count = self._data['t' + trophy_type[0]] t_mult = self._data['t' + trophy_type[0] + 'm'] @@ -157,7 +157,7 @@ class TrophiesWindow(popup.PopupWindow): h_align='center', v_align='center') - pts = t_count * t_mult + this_pts = t_count * t_mult ba.textwidget(parent=self._subcontainer, position=(sub_width * 0.88, sub_height - 20 - incr * i), @@ -167,12 +167,12 @@ class TrophiesWindow(popup.PopupWindow): flatness=1.0, shadow=0.0, scale=0.5, - text=eq_text.replace('${NUMBER}', str(pts)), + text=eq_text.replace('${NUMBER}', str(this_pts)), size=(0, 0), h_align='center', v_align='center') - pts += pts - return pts + total_pts += this_pts + return total_pts def _on_cancel_press(self) -> None: self._transition_out() diff --git a/dist/ba_data/python/bastd/ui/watch.py b/dist/ba_data/python/bastd/ui/watch.py index fc870c8..296bc36 100644 --- a/dist/ba_data/python/bastd/ui/watch.py +++ b/dist/ba_data/python/bastd/ui/watch.py @@ -25,7 +25,7 @@ class WatchWindow(ba.Window): def __init__(self, transition: str | None = 'in_right', - origin_widget: ba.Widget = None): + origin_widget: ba.Widget | None = None): # pylint: disable=too-many-locals # pylint: disable=too-many-statements from bastd.ui.tabs import TabRow diff --git a/dist/ba_data/python/efro/dataclassio/_pathcapture.py b/dist/ba_data/python/efro/dataclassio/_pathcapture.py index af7113f..b0b34ad 100644 --- a/dist/ba_data/python/efro/dataclassio/_pathcapture.py +++ b/dist/ba_data/python/efro/dataclassio/_pathcapture.py @@ -19,7 +19,7 @@ T = TypeVar('T') class _PathCapture: """Utility for obtaining dataclass storage paths in a type safe way.""" - def __init__(self, obj: Any, pathparts: list[str] = None): + def __init__(self, obj: Any, pathparts: list[str] | None = None): self._is_dataclass = dataclasses.is_dataclass(obj) if pathparts is None: pathparts = [] diff --git a/dist/ba_data/python/efro/dataclassio/_prep.py b/dist/ba_data/python/efro/dataclassio/_prep.py index 704fb72..d49a132 100644 --- a/dist/ba_data/python/efro/dataclassio/_prep.py +++ b/dist/ba_data/python/efro/dataclassio/_prep.py @@ -38,7 +38,7 @@ PREP_ATTR = '_DCIOPREP' PREP_SESSION_ATTR = '_DCIOPREPSESSION' -def ioprep(cls: type, globalns: dict = None) -> None: +def ioprep(cls: type, globalns: dict | None = None) -> None: """Prep a dataclass type for use with this module's functionality. Prepping ensures that all types contained in a data class as well as diff --git a/dist/ba_data/python/efro/error.py b/dist/ba_data/python/efro/error.py index 2809b45..640d014 100644 --- a/dist/ba_data/python/efro/error.py +++ b/dist/ba_data/python/efro/error.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import TYPE_CHECKING +import errno if TYPE_CHECKING: pass @@ -39,12 +40,13 @@ class CommunicationError(Exception): """A communication related error has occurred. This covers anything network-related going wrong in the sending - of data or receiving of a response. This error does not imply + of data or receiving of a response. Basically anything that is out + of our control should get lumped in here. This error does not imply that data was not received on the other end; only that a full acknowledgement round trip was not completed. These errors should be gracefully handled whenever possible, as - occasional network outages are generally unavoidable. + occasional network issues are unavoidable. """ @@ -55,9 +57,9 @@ class RemoteError(Exception): occurs remotely. The error string can consist of a remote stack trace or a simple message depending on the context. - Communication systems should raise more specific error types when - more introspection/control is needed; this is intended somewhat as - a catch-all. + Communication systems should raise more specific error types locally + when more introspection/control is needed; this is intended somewhat + as a catch-all. """ def __str__(self) -> str: @@ -69,31 +71,43 @@ class IntegrityError(ValueError): """Data has been tampered with or corrupted in some form.""" -def is_urllib_network_error(exc: BaseException) -> bool: - """Is the provided exception from urllib a network-related error? +def is_urllib_communication_error(exc: BaseException, url: str | None) -> bool: + """Is the provided exception from urllib a communication-related error? + + Url, if provided can provide extra context for when to treat an error + as such an error. This should be passed an exception which resulted from opening or reading a urllib Request. It returns True for any errors that could conceivably arise due to unavailable/poor network connections, - firewall/connectivity issues, etc. These issues can often be safely - ignored or presented to the user as general 'network-unavailable' - states. + firewall/connectivity issues, or other issues out of our control. + These errors can often be safely ignored or presented to the user + as general 'network-unavailable' states. """ import urllib.error import http.client - import errno import socket - if isinstance( - exc, - (urllib.error.URLError, ConnectionError, http.client.IncompleteRead, - http.client.BadStatusLine, socket.timeout)): + if isinstance(exc, (urllib.error.URLError, ConnectionError, + http.client.IncompleteRead, http.client.BadStatusLine, + http.client.RemoteDisconnected, socket.timeout)): + # Special case: although an HTTPError is a subclass of URLError, - # we don't return True for it. It means we have successfully - # communicated with the server but what we are asking for is - # not there/etc. + # we don't consider it a communication error. It generally means we + # have successfully communicated with the server but what we are asking + # for is not there/etc. if isinstance(exc, urllib.error.HTTPError): + + # Special sub-case: appspot.com hosting seems to give 403 errors + # (forbidden) to some countries. I'm assuming for legal reasons?.. + # Let's consider that a communication error since its out of our + # control so we don't fill up logs with it. + if exc.code == 403 and url is not None and '.appspot.com' in url: + return True + return False + return True + if isinstance(exc, OSError): if exc.errno == 10051: # Windows unreachable network error. return True @@ -106,18 +120,17 @@ def is_urllib_network_error(exc: BaseException) -> bool: return False -def is_udp_network_error(exc: BaseException) -> bool: - """Is the provided exception a network-related error? +def is_udp_communication_error(exc: BaseException) -> bool: + """Should this udp-related exception be considered a communication error? This should be passed an exception which resulted from creating and using a socket.SOCK_DGRAM type socket. It should return True for any errors that could conceivably arise due to unavailable/poor network - connections, firewall/connectivity issues, etc. These issues can often + conditions, firewall/connectivity issues, etc. These issues can often be safely ignored or presented to the user as general 'network-unavailable' states. """ - import errno - if isinstance(exc, ConnectionRefusedError): + if isinstance(exc, ConnectionRefusedError | TimeoutError): return True if isinstance(exc, OSError): if exc.errno == 10051: # Windows unreachable network error. @@ -140,8 +153,8 @@ def is_udp_network_error(exc: BaseException) -> bool: return False -def is_asyncio_streams_network_error(exc: BaseException) -> bool: - """Is the provided exception a network-related error? +def is_asyncio_streams_communication_error(exc: BaseException) -> bool: + """Should this streams error be considered a communication error? This should be passed an exception which resulted from creating and using asyncio streams. It should return True for any errors that could @@ -149,7 +162,6 @@ def is_asyncio_streams_network_error(exc: BaseException) -> bool: firewall/connectivity issues, etc. These issues can often be safely ignored or presented to the user as general 'connection-lost' events. """ - import errno import ssl if isinstance(exc, ( diff --git a/dist/ba_data/python/efro/message/_message.py b/dist/ba_data/python/efro/message/_message.py index c472d21..60e108f 100644 --- a/dist/ba_data/python/efro/message/_message.py +++ b/dist/ba_data/python/efro/message/_message.py @@ -56,6 +56,7 @@ class ErrorResponse(Response): OTHER = 0 CLEAN = 1 LOCAL = 2 + COMMUNICATION = 3 error_message: Annotated[str, IOAttrs('m')] error_type: Annotated[ErrorType, IOAttrs('e')] = ErrorType.OTHER diff --git a/dist/ba_data/python/efro/message/_protocol.py b/dist/ba_data/python/efro/message/_protocol.py index a117102..e1e5cfc 100644 --- a/dist/ba_data/python/efro/message/_protocol.py +++ b/dist/ba_data/python/efro/message/_protocol.py @@ -25,28 +25,40 @@ class MessageProtocol: """Wrangles a set of message types, formats, and response types. Both endpoints must be using a compatible Protocol for communication to succeed. To maintain Protocol compatibility between revisions, - all message types must retain the same id, message attr storage names must - not change, newly added attrs must have default values, etc. + all message types must retain the same id, message attr storage + names must not change, newly added attrs must have default values, + etc. """ def __init__(self, message_types: dict[int, type[Message]], response_types: dict[int, type[Response]], preserve_clean_errors: bool = True, - log_remote_exceptions: bool = True, - trusted_sender: bool = False) -> None: + receiver_logs_exceptions: bool = True, + receiver_returns_stack_traces: bool = False) -> None: """Create a protocol with a given configuration. Note that common response types are automatically registered with (unchanging negative ids) so they don't need to be passed explicitly (but can be if a different id is desired). - If 'preserve_clean_errors' is True, efro.error.CleanError errors - on the remote end will result in the same error raised locally. - All other Exception types come across as efro.error.RemoteError. + If 'preserve_clean_errors' is True, efro.error.CleanError + exceptions raised on the receiver end will result in a matching + CleanError raised back on the sender. All other Exception types + come across as efro.error.RemoteError. - If 'trusted_sender' is True, stringified remote stack traces will - be included in the responses if errors occur. + When 'receiver_logs_exceptions' is True, any uncaught Exceptions + on the receiver end will be logged there via logging.exception() + (in addition to the usual behavior of returning an ErrorResponse + to the sender). This is good to leave enabled if your + intention is to never return ErrorResponses. Looser setups + making routine use of CleanErrors or whatnot may want to + disable this, however. + + If 'receiver_returns_stack_traces' is True, stringified stack + traces will be returned to the sender for exceptions occurring + on the receiver end. This can make debugging easier but should + only be used when the client is trusted to see such info. """ self.message_types_by_id: dict[int, type[Message]] = {} self.message_ids_by_type: dict[type[Message], int] = {} @@ -102,9 +114,9 @@ class MessageProtocol: assert is_ioprepped_dataclass(cls) assert issubclass(cls, Response) if cls not in self.response_ids_by_type: - raise ValueError(f'Possible response type {cls}' - f' needs to be included in response_types' - f' for this protocol.') + raise ValueError( + f'Possible response type {cls} needs to be included' + f' in response_types for this protocol.') # Make sure all registered types have unique base names. # We can take advantage of this to generate cleaner looking @@ -116,8 +128,8 @@ class MessageProtocol: ' all types are required to have unique names.') self.preserve_clean_errors = preserve_clean_errors - self.log_remote_exceptions = log_remote_exceptions - self.trusted_sender = trusted_sender + self.receiver_logs_exceptions = receiver_logs_exceptions + self.receiver_returns_stack_traces = receiver_returns_stack_traces @staticmethod def encode_dict(obj: dict) -> str: @@ -134,7 +146,9 @@ class MessageProtocol: def error_to_response(self, exc: Exception) -> Response: """Translate an error to a response.""" - if self.log_remote_exceptions: + + # Log any errors we got during handling if so desired. + if self.receiver_logs_exceptions: logging.exception('Error handling message.') # If anything goes wrong, return a ErrorResponse instead. @@ -142,8 +156,9 @@ class MessageProtocol: return ErrorResponse(error_message=str(exc), error_type=ErrorResponse.ErrorType.CLEAN) return ErrorResponse( - error_message=(traceback.format_exc() if self.trusted_sender else - 'An unknown error has occurred.'), + error_message=(traceback.format_exc() + if self.receiver_returns_stack_traces else + 'An internal error has occurred.'), error_type=ErrorResponse.ErrorType.OTHER) def _to_dict(self, message: Any, ids_by_type: dict[type, int], diff --git a/dist/ba_data/python/efro/message/_sender.py b/dist/ba_data/python/efro/message/_sender.py index 2446586..f2977ad 100644 --- a/dist/ba_data/python/efro/message/_sender.py +++ b/dist/ba_data/python/efro/message/_sender.py @@ -9,7 +9,7 @@ from __future__ import annotations import logging from typing import TYPE_CHECKING, TypeVar -from efro.error import CleanError, RemoteError +from efro.error import CleanError, RemoteError, CommunicationError from efro.message._message import EmptyResponse, ErrorResponse if TYPE_CHECKING: @@ -35,7 +35,7 @@ class MessageSender: def send_raw_message(self, message: str) -> str: # Actually send the message here. - # MyMessageSender class should provide overloads for send(), send_bg(), + # MyMessageSender class should provide overloads for send(), send_async(), # etc. to ensure all sending happens with valid types. obj = MyClass() obj.msg.send(SomeMessageType()) @@ -54,7 +54,12 @@ class MessageSender: def send_method( self, call: Callable[[Any, str], str]) -> Callable[[Any, str], str]: - """Function decorator for setting raw send method.""" + """Function decorator for setting raw send method. + + Send methods take strings and should return strings. + Any Exception raised during the send_method manifests as + a CommunicationError for the message sender. + """ assert self._send_raw_message_call is None self._send_raw_message_call = call return call @@ -62,7 +67,12 @@ class MessageSender: def send_async_method( self, call: Callable[[Any, str], Awaitable[str]] ) -> Callable[[Any, str], Awaitable[str]]: - """Function decorator for setting raw send-async method.""" + """Function decorator for setting raw send-async method. + + Send methods take strings and should return strings. + Any Exception raised during the send_method manifests as + a CommunicationError for the message sender. + """ assert self._send_async_raw_message_call is None self._send_async_raw_message_call = call return call @@ -123,7 +133,17 @@ class MessageSender: raise RuntimeError('send() is unimplemented for this type.') msg_encoded = self._encode_message(bound_obj, message) - response_encoded = self._send_raw_message_call(bound_obj, msg_encoded) + try: + response_encoded = self._send_raw_message_call( + bound_obj, msg_encoded) + except Exception as exc: + # Any error in the raw send call gets recorded as either + # a local or communication error. + return ErrorResponse( + error_message=f'Error in send async method: {exc}', + error_type=(ErrorResponse.ErrorType.COMMUNICATION + if isinstance(exc, CommunicationError) else + ErrorResponse.ErrorType.LOCAL)) return self._decode_raw_response(bound_obj, message, response_encoded) async def send_split_part_1_async(self, bound_obj: Any, @@ -139,9 +159,17 @@ class MessageSender: raise RuntimeError('send_async() is unimplemented for this type.') msg_encoded = self._encode_message(bound_obj, message) - response_encoded = await self._send_async_raw_message_call( - bound_obj, msg_encoded) - + try: + response_encoded = await self._send_async_raw_message_call( + bound_obj, msg_encoded) + except Exception as exc: + # Any error in the raw send call gets recorded as either + # a local or communication error. + return ErrorResponse( + error_message=f'Error in send async method: {exc}', + error_type=(ErrorResponse.ErrorType.COMMUNICATION + if isinstance(exc, CommunicationError) else + ErrorResponse.ErrorType.LOCAL)) return self._decode_raw_response(bound_obj, message, response_encoded) def send_split_part_2(self, message: Message, @@ -183,8 +211,8 @@ class MessageSender: # If we got to this point, we successfully communicated # with the other end so errors represent protocol mismatches # or other invalid data. For now let's just log it but perhaps - # we'd want to somehow embed it in the ErrorResponse to be raised - # directly to the user later. + # we'd want to somehow embed it in the ErrorResponse to be + # available directly to the user later. logging.exception('Error decoding raw response') response = ErrorResponse( error_message= @@ -208,6 +236,10 @@ class MessageSender: # Some error occurred. Raise a local Exception for it. if isinstance(raw_response, ErrorResponse): + if (raw_response.error_type is + ErrorResponse.ErrorType.COMMUNICATION): + raise CommunicationError(raw_response.error_message) + # If something went wrong on our end of the connection, # don't say it was a remote error. if raw_response.error_type is ErrorResponse.ErrorType.LOCAL: diff --git a/dist/ba_data/python/efro/rpc.py b/dist/ba_data/python/efro/rpc.py index 6afc310..a59d707 100644 --- a/dist/ba_data/python/efro/rpc.py +++ b/dist/ba_data/python/efro/rpc.py @@ -13,8 +13,9 @@ from dataclasses import dataclass from threading import current_thread from typing import TYPE_CHECKING, Annotated -from efro.error import CommunicationError, is_asyncio_streams_network_error from efro.util import assert_never +from efro.error import (CommunicationError, + is_asyncio_streams_communication_error) from efro.dataclassio import (dataclass_to_json, dataclass_from_json, ioprepped, IOAttrs) @@ -115,7 +116,7 @@ class RPCEndpoint: label: str, debug_print: bool = False, debug_print_io: bool = False, - debug_print_call: Callable[[str], None] = None, + debug_print_call: Callable[[str], None] | None = None, keepalive_interval: float = DEFAULT_KEEPALIVE_INTERVAL, keepalive_timeout: float = DEFAULT_KEEPALIVE_TIMEOUT) -> None: self._handle_raw_message_call = handle_raw_message_call @@ -596,7 +597,7 @@ class RPCEndpoint: if isinstance(exc, _KeepaliveTimeoutError): return True - return is_asyncio_streams_network_error(exc) + return is_asyncio_streams_communication_error(exc) def _check_env(self) -> None: # I was seeing that asyncio stuff wasn't working as expected if diff --git a/dist/ba_data/python/efro/util.py b/dist/ba_data/python/efro/util.py index d18e505..d9d3634 100644 --- a/dist/ba_data/python/efro/util.py +++ b/dist/ba_data/python/efro/util.py @@ -176,7 +176,7 @@ class DirtyBit: dirty: bool = False, retry_interval: float = 5.0, use_lock: bool = False, - auto_dirty_seconds: float = None, + auto_dirty_seconds: float | None = None, min_update_interval: float | None = None): curtime = time.time() self._retry_interval = retry_interval @@ -637,9 +637,11 @@ def compact_id(num: int) -> str: 'abcdefghijklmnopqrstuvwxyz') +# NOTE: Even though this is available as part of typing_extensions, keeping +# it in here for now so we don't require typing_extensions as a dependency. +# Once 3.11 rolls around we can kill this and use typing.assert_never. def assert_never(value: NoReturn) -> NoReturn: """Trick for checking exhaustive handling of Enums, etc. - See https://github.com/python/typing/issues/735 """ assert False, f'Unhandled value: {value} ({type(value).__name__})' diff --git a/dist/ba_root/config.json b/dist/ba_root/config.json index ac7e2f9..a8127c2 100644 --- a/dist/ba_root/config.json +++ b/dist/ba_root/config.json @@ -1,736 +1,756 @@ { - "Achievements": { - "Boom Goes the Dynamite": { - "Complete": false - }, - "Boxer": { - "Complete": false - }, - "Dual Wielding": { - "Complete": false - }, - "Flawless Victory": { - "Complete": false - }, - "Free Loader": { - "Complete": false - }, - "Gold Miner": { - "Complete": false - }, - "Got the Moves": { - "Complete": false - }, - "In Control": { - "Complete": false - }, - "Last Stand God": { - "Complete": false - }, - "Last Stand Master": { - "Complete": false - }, - "Last Stand Wizard": { - "Complete": false - }, - "Mine Games": { - "Complete": true - }, - "Off You Go Then": { - "Complete": true - }, - "Onslaught God": { - "Complete": false - }, - "Onslaught Master": { - "Complete": false - }, - "Onslaught Training Victory": { - "Complete": true - }, - "Onslaught Wizard": { - "Complete": false - }, - "Precision Bombing": { - "Complete": false - }, - "Pro Boxer": { - "Complete": false - }, - "Pro Football Shutout": { - "Complete": false - }, - "Pro Football Victory": { - "Complete": false - }, - "Pro Onslaught Victory": { - "Complete": false - }, - "Pro Runaround Victory": { - "Complete": false - }, - "Rookie Football Shutout": { - "Complete": false - }, - "Rookie Football Victory": { - "Complete": false - }, - "Rookie Onslaught Victory": { - "Complete": true - }, - "Runaround God": { - "Complete": false - }, - "Runaround Master": { - "Complete": false - }, - "Runaround Wizard": { - "Complete": false - }, - "Sharing is Caring": { - "Complete": false - }, - "Stayin' Alive": { - "Complete": false - }, - "Super Mega Punch": { - "Complete": false - }, - "Super Punch": { - "Complete": true - }, - "TNT Terror": { - "Complete": false - }, - "Team Player": { - "Complete": true - }, - "The Great Wall": { - "Complete": false - }, - "The Wall": { - "Complete": false - }, - "Uber Football Shutout": { - "Complete": false - }, - "Uber Football Victory": { - "Complete": false - }, - "Uber Onslaught Victory": { - "Complete": false - }, - "Uber Runaround Victory": { - "Complete": false - } + "Achievements": { + "Boom Goes the Dynamite": { + "Complete": false + }, + "Boxer": { + "Complete": false + }, + "Dual Wielding": { + "Complete": false + }, + "Flawless Victory": { + "Complete": false + }, + "Free Loader": { + "Complete": false + }, + "Gold Miner": { + "Complete": false + }, + "Got the Moves": { + "Complete": false + }, + "In Control": { + "Complete": false + }, + "Last Stand God": { + "Complete": false + }, + "Last Stand Master": { + "Complete": false + }, + "Last Stand Wizard": { + "Complete": false + }, + "Mine Games": { + "Complete": false + }, + "Off You Go Then": { + "Complete": false + }, + "Onslaught God": { + "Complete": false + }, + "Onslaught Master": { + "Complete": false + }, + "Onslaught Training Victory": { + "Complete": false + }, + "Onslaught Wizard": { + "Complete": false + }, + "Precision Bombing": { + "Complete": false + }, + "Pro Boxer": { + "Complete": false + }, + "Pro Football Shutout": { + "Complete": false + }, + "Pro Football Victory": { + "Complete": false + }, + "Pro Onslaught Victory": { + "Complete": false + }, + "Pro Runaround Victory": { + "Complete": false + }, + "Rookie Football Shutout": { + "Complete": false + }, + "Rookie Football Victory": { + "Complete": false + }, + "Rookie Onslaught Victory": { + "Complete": false + }, + "Runaround God": { + "Complete": false + }, + "Runaround Master": { + "Complete": false + }, + "Runaround Wizard": { + "Complete": false + }, + "Sharing is Caring": { + "Complete": false + }, + "Stayin' Alive": { + "Complete": false + }, + "Super Mega Punch": { + "Complete": false + }, + "Super Punch": { + "Complete": false + }, + "TNT Terror": { + "Complete": false + }, + "Team Player": { + "Complete": false + }, + "The Great Wall": { + "Complete": false + }, + "The Wall": { + "Complete": false + }, + "Uber Football Shutout": { + "Complete": false + }, + "Uber Football Victory": { + "Complete": false + }, + "Uber Onslaught Victory": { + "Complete": false + }, + "Uber Runaround Victory": { + "Complete": false + } + }, + "Auto Account State": "Server", + "Auto Balance Teams": true, + "Bear Coin": 814, + "Bear Store": { + "Buy Firebombs": false, + "Buy Option": false, + "Buy Percentage": false, + "Promo Code": { + "B-0mB3RYT2z": [ + true, + 910 + ], + "B-Asd14mON9G0D": [ + true, + 910 + ], + "D-rAcK0cJ23": [ + true, + 910 + ], + "E-Am54igO42Os": [ + true, + 600 + ], + "E-M4uN3K34XB": [ + true, + 840 + ], + "E-a27ZO6f3Y": [ + true, + 600 + ], + "G-Am54igO42Os": [ + true, + 1100 + ], + "P-tRo8nM8dZ": [ + true, + 2800 + ], + "PM-731ClcAF": [ + true, + 50000 + ], + "Y-tU2B3S": [ + true, + 500 + ] + } + }, + "Campaigns": {}, + "Custom Team Colors": [ + [ + 2.0, + 0.25, + 1.0 + ], + [ + 1.0, + 0.25, + 0.2 + ] + ], + "Custom Team Names": [ + "ladoo", + "barfi" + ], + "Default Player Profiles": { + "Client Input Device #1": "__account__", + "Client Input Device #2": "__account__" + }, + "Fleet Zone Pings": { + "prod": { + "ap-northeast-1": 149.69879999989644, + "ap-northeast-2": 167.120200001591, + "ap-south-1": 38.507389199694444, + "ap-southeast-1": 91.31349360058448, + "eu-north-1": 175.44400000042515, + "me-south-1": 74.37627740109019 + } + }, + "Free-for-All Max Players": 25, + "Free-for-All Playlist Randomize": true, + "Free-for-All Playlist Selection": "__default__", + "Free-for-All Playlists": {}, + "Idle Exit Minutes": null, + "Local Account Name": "Server7604732", + "PPM Settings": { + "Healing Damage PTG": 72, + "Powers Gravity": true, + "Powerup Name": true, + "Powerup Scale": 1.0, + "Powerup Style": "Auto", + "Powerup Time": false, + "Powerup With Shield": true, + "Powerups": { + "Curse": 1, + "Fire Bombs": 3, + "Fly Bombs": 3, + "Goodbye": 2, + "Healing Damage": 1, + "Health": 1, + "Ice Bombs": 3, + "Ice Man": 1, + "Impact Bombs": 3, + "Impairment Bombs": 2, + "Mine Bombs": 2, + "Punch": 3, + "Shield": 2, + "Speed": 2, + "Sticky Bombs": 3, + "Tank Shield": 1, + "Triple": 3 + }, + "Tank Shield PTG": 96 + }, + "Player Profiles": { + "__account__": { + "character": "Spaz", + "color": [ + 0.5, + 0.25, + 1.0 + ], + "highlight": [ + 0.5, + 0.25, + 1.0 + ] + } + }, + "Plugins": { + "custom_hooks.modSetup": { + "enabled": true + }, + "plugins.Init": { + "enabled": true + } + }, + "Port": 43210, + "Region Pings": { + "af-south-1": 328.2357999996748, + "ap-northeast-1": 150.0941541948123, + "ap-northeast-2": 160.2201884064998, + "ap-south-1": 45.05286179925315, + "ap-southeast-1": 101.91129060697858, + "ap-southeast-2": 183.6228000029223, + "ca-central-1": 262.4569968022115, + "eu-central-1": 155.88790780561976, + "eu-north-1": 181.94498299446423, + "eu-south-1": 156.48616400256287, + "eu-west-1": 179.42955539320246, + "eu-west-2": 158.43393659518915, + "eu-west-3": 170.620519401622, + "me-south-1": 78.16982959693996, + "sa-east-1": 345.57150000182446, + "us-east-1": 238.92560360513744, + "us-east-2": 257.30932620124076, + "us-west-1": 255.34800000605173, + "us-west-2": 292.9734999925131 + }, + "Selected Coop Game": "Easy:Rookie Onslaught", + "Show Tutorial": false, + "Signed In Last Session": false, + "Team Game Max Player": 25, + "Team Game Max Players": 25, + "Team Tournament Playlist Randomize": true, + "Team Tournament Playlist Selection": "BCS_FUN", + "Team Tournament Playlists": { + "BCS_FUN": [ + { + "settings": { + "Basic Bombs": true, + "Epic Mode": true, + "Frozen Bombs": true, + "Grabbing only": true, + "Impact Bombs": true, + "Punching only": true, + "Respawn Times": 0.5, + "Sticky Bombs": true, + "Time Limit": 300, + "map": "\ue019Powerups Factory" }, - "Auto Account State": "Server", - "Auto Balance Teams": true, - "Bear Coin": 814, - "Bear Store": { - "Buy Firebombs": false, - "Buy Option": false, - "Buy Percentage": false, - "Promo Code": { - "B-0mB3RYT2z": [ - true, - 910 - ], - "B-Asd14mON9G0D": [ - true, - 910 - ], - "D-rAcK0cJ23": [ - true, - 910 - ], - "E-Am54igO42Os": [ - true, - 600 - ], - "E-M4uN3K34XB": [ - true, - 840 - ], - "E-a27ZO6f3Y": [ - true, - 600 - ], - "G-Am54igO42Os": [ - true, - 1100 - ], - "P-tRo8nM8dZ": [ - true, - 2800 - ], - "PM-731ClcAF": [ - true, - 50000 - ], - "Y-tU2B3S": [ - true, - 500 - ] - } + "type": "ArmsRace.ArmsRaceGame" + }, + { + "settings": { + "Countdown Time Each Round": 45, + "Epic Mode": true, + "Kill player on ground": true, + "Random Spawn Point (Team)": false, + "Respawn Times": 1.0, + "Score to Win Per Team": 5, + "Time Limit": 300, + "[On score] Single Celebrate": true, + "[On score] Single Teleport": true, + "map": "Tower D" }, - "Campaigns": { - "Easy": { - "Onslaught Training": { - "Complete": true, - "High Scores": { - "1 Player": [ - [ - 286, - { - "players": [ - { - "character": "Spaz", - "name": "smoothie" - } - ] - } - ], - [ - 261, - { - "players": [ - { - "character": "Spaz", - "name": "smoothie" - } - ] - } - ], - [ - 239, - { - "players": [ - { - "character": "Spaz", - "name": "smoothie" - } - ] - } - ], - [ - 49, - { - "players": [ - { - "character": "Spaz", - "name": "smoothie" - } - ] - } - ], - [ - 46, - { - "players": [ - { - "character": "Spaz", - "name": "smoothie" - } - ] - } - ], - [ - 28, - { - "players": [ - { - "character": "Spaz", - "name": "smoothie" - } - ] - } - ], - [ - 5, - { - "players": [ - { - "character": "Spaz", - "name": "smoothie" - } - ] - } - ], - [ - 5, - { - "players": [ - { - "character": "Spaz", - "name": "smoothie" - } - ] - } - ], - [ - 5, - { - "players": [ - { - "character": "Spaz", - "name": "smoothie" - } - ] - } - ], - [ - 5, - { - "players": [ - { - "character": "Spaz", - "name": "smoothie" - } - ] - } - ] - ] - }, - "Rating": 10.0 - }, - "Rookie Onslaught": { - "Complete": false, - "High Scores": { - "1 Player": [ - [ - 297, - { - "players": [ - { - "character": "Spaz", - "name": "smoothie" - } - ] - } - ], - [ - 34, - { - "players": [ - { - "character": "Spaz", - "name": "smoothie" - } - ] - } - ] - ] - }, - "Rating": 10.0 - } - } + "type": "Tower_Rush.BaseRaidGame" + }, + { + "settings": { + "Enable speed": false, + "Epic Mode": true, + "Powerups Spawn": true, + "Respawn Times": 1.0, + "Score to Win": 3, + "Time Limit": 300, + "map": "BasketBall Stadium V2" }, - "Custom Team Colors": [ - [ - 2.0, - 0.25, - 1.0 - ], - [ - 1.0, - 0.25, - 0.2 - ] - ], - "Custom Team Names": [ - "ladoo", - "barfi" - ], - "Default Player Profiles": { - "Client Input Device #1": "_random", - "Client Input Device #2": "__account__" + "type": "baBasketBomb.BasketGame" + }, + { + "settings": { + "Epic Mode": false, + "Large Box Count": 3, + "Points to win per Player": 1000, + "Respawn Times": 0.5, + "Small Box Count": 1, + "Time Limit": 300, + "map": "Football Stadium" }, - "Free-for-All Playlist Randomize": true, - "Free-for-All Playlist Selection": "__default__", - "Free-for-All Playlists": { - "Just Death Match": [ - { - "settings": { - "Epic Mode": false, - "Kills to Win Per Player": 10, - "Respawn Times": 1.0, - "Time Limit": 300, - "map": "Doom Shroom" - }, - "type": "bs_death_match.DeathMatchGame" - }, - { - "settings": { - "Epic Mode": false, - "Kills to Win Per Player": 10, - "Respawn Times": 1.0, - "Time Limit": 300, - "map": "Crag Castle" - }, - "type": "bs_death_match.DeathMatchGame" - } - ], - "My Free-for-All Playlist": [ - { - "settings": { - "Allow Negative Scores": false, - "Epic Mode": false, - "Kills to Win Per Player": 5, - "Respawn Times": 1.0, - "Time Limit": 0, - "map": "Courtyard" - }, - "type": "bastd.game.deathmatch.DeathMatchGame" - } - ] + "type": "crazyBoxes.CrazyBoxGame" + }, + { + "settings": { + "Epic Mode": true, + "Points to Win Per Player": 2, + "Respawn Times": 1.0, + "Target Indicator": 1, + "Time Limit": 300, + "Time to Kill": 7, + "map": "Football Stadium" }, - "Idle Exit Minutes": null, - "Local Account Name": "Server3258837", - "PPM Settings": { - "Healing Damage PTG": 72, - "Powers Gravity": true, - "Powerup Name": true, - "Powerup Scale": 1.0, - "Powerup Style": "Auto", - "Powerup Time": false, - "Powerup With Shield": true, - "Powerups": { - "Curse": 1, - "Fire Bombs": 3, - "Fly Bombs": 3, - "Goodbye": 2, - "Healing Damage": 1, - "Health": 1, - "Ice Bombs": 3, - "Ice Man": 1, - "Impact Bombs": 3, - "Impairment Bombs": 2, - "Mine Bombs": 2, - "Punch": 3, - "Shield": 2, - "Speed": 2, - "Sticky Bombs": 3, - "Tank Shield": 1, - "Triple": 3 - }, - "Tank Shield PTG": 96 + "type": "GetTheTarget.GetTheTargetGame" + }, + { + "settings": { + "Balance Total Lives": false, + "Epic Mode": false, + "Lives Per Player": 1, + "Respawn Times": 1.0, + "Solo Mode": false, + "Time Limit": 300, + "map": "Football Stadium" }, - "Player Profiles": { - "__account__": { - "character": "Spaz", - "color": [ - 0.5, - 0.25, - 1.0 - ], - "highlight": [ - 0.5, - 0.25, - 1.0 - ] - } + "type": "GravityFalls.GFGame" + }, + { + "settings": { + "Epic Mode": true, + "Respawn Times": 0.5, + "Score to Win": 3, + "Time Limit": 300, + "map": "Bridgit Parallelo" }, - "Plugins": { - "plugins.Init": { - "enabled": true - } + "type": "bastd.game.assault.AssaultGame" + }, + { + "settings": { + "Epic Mode": false, + "map": "Wooden Floor" }, - "Port": 43210, - "Region Pings": { - "af-south-1": 328.2357999996748, - "ap-northeast-1": 150.0941541948123, - "ap-northeast-2": 160.2201884064998, - "ap-south-1": 45.05286179925315, - "ap-southeast-1": 101.91129060697858, - "ap-southeast-2": 183.6228000029223, - "ca-central-1": 262.4569968022115, - "eu-central-1": 155.88790780561976, - "eu-north-1": 181.94498299446423, - "eu-south-1": 156.48616400256287, - "eu-west-1": 179.42955539320246, - "eu-west-2": 158.43393659518915, - "eu-west-3": 170.620519401622, - "me-south-1": 78.16982959693996, - "sa-east-1": 345.57150000182446, - "us-east-1": 238.92560360513744, - "us-east-2": 257.30932620124076, - "us-west-1": 255.34800000605173, - "us-west-2": 292.9734999925131 + "type": "UFOAttackGame.UFOAttackGame" + }, + { + "settings": { + "Balance Total Lives": true, + "Epic Mode": true, + "Lives Per Player": 3, + "Players Per Team In Arena": 2, + "Respawn Times": 0.25, + "Time Limit": 300, + "map": "Bridgit Parallelo" }, - "Selected Coop Game": "Easy:Rookie Onslaught", - "Show Tutorial": false, - "Signed In Last Session": false, - "Team Game Max Player": 25, - "Team Game Max Players": 25, - "Free-for-All Max Players":25, - "Team Tournament Playlist Randomize": true, - "Team Tournament Playlist Selection": "My Teams Playlist", - "Team Tournament Playlists": { - "My Teams Playlist": [ - { - "settings": { - "Epic Mode": false, - "Kills to Win Per Player": 1, - "Respawn Times": 1.0, - "Time Limit": 0, - "map": "Doom Shroom" - }, - "type": "bastd.game.deathmatch.DeathMatchGame" - } - ], - "My Teams Playlist 2": [ - { - "settings": { - "Epic Mode": false, - "Kills to Win Per Player": 5, - "Respawn Times": 1.0, - "Time Limit": 0, - "map": "Happy Thoughts" - }, - "type": "bastd.game.deathmatch.DeathMatchGame" - } - ], - "\u041a\u043e\u043f\u0438\u044f \u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0439 \u043f\u043b\u0435\u0439-\u043b\u0438\u0441\u0442 \u0440\u0435\u0436\u0438\u043c\u0430 \u041a\u043e\u043c\u0430\u043d\u0434\u044b": [ - { - "settings": { - "Epic Mode": false, - "Flag Idle Return Time": 30, - "Flag Touch Return Time": 0, - "Respawn Times": 1.0, - "Score to Win": 3, - "Time Limit": 600, - "map": "Bridgit" - }, - "type": "bsCaptureTheFlag.CTFGame" - }, - { - "settings": { - "Epic Mode": false, - "Respawn Times": 1.0, - "Score to Win": 3, - "Time Limit": 600, - "map": "Step Right Up" - }, - "type": "bsAssault.AssaultGame" - }, - { - "settings": { - "Balance Total Lives": false, - "Epic Mode": false, - "Lives Per Player": 3, - "Respawn Times": 1.0, - "Solo Mode": true, - "Time Limit": 600, - "map": "Rampage" - }, - "type": "bsElimination.EliminationGame" - }, - { - "settings": { - "Epic Mode": false, - "Kills to Win Per Player": 5, - "Respawn Times": 1.0, - "Time Limit": 300, - "map": "Roundabout" - }, - "type": "bsDeathMatch.DeathMatchGame" - }, - { - "settings": { - "Respawn Times": 1.0, - "Score to Win": 1, - "Time Limit": 600, - "map": "Hockey Stadium" - }, - "type": "bsHockey.HockeyGame" - }, - { - "settings": { - "Hold Time": 30, - "Respawn Times": 1.0, - "Time Limit": 300, - "map": "Monkey Face" - }, - "type": "bsKeepAway.KeepAwayGame" - }, - { - "settings": { - "Balance Total Lives": false, - "Epic Mode": true, - "Lives Per Player": 1, - "Respawn Times": 1.0, - "Solo Mode": false, - "Time Limit": 120, - "map": "Tip Top" - }, - "type": "bsElimination.EliminationGame" - }, - { - "settings": { - "Epic Mode": false, - "Respawn Times": 1.0, - "Score to Win": 3, - "Time Limit": 300, - "map": "Crag Castle" - }, - "type": "bsAssault.AssaultGame" - }, - { - "settings": { - "Epic Mode": false, - "Kills to Win Per Player": 5, - "Respawn Times": 1.0, - "Time Limit": 300, - "map": "Doom Shroom" - }, - "type": "bsDeathMatch.DeathMatchGame" - }, - { - "settings": { - "Epic Mode": false, - "map": "Rampage" - }, - "type": "bsMeteorShower.MeteorShowerGame" - }, - { - "settings": { - "Epic Mode": false, - "Flag Idle Return Time": 30, - "Flag Touch Return Time": 0, - "Respawn Times": 1.0, - "Score to Win": 2, - "Time Limit": 600, - "map": "Roundabout" - }, - "type": "bsCaptureTheFlag.CTFGame" - }, - { - "settings": { - "Respawn Times": 1.0, - "Score to Win": 21, - "Time Limit": 600, - "map": "Football Stadium" - }, - "type": "bsFootball.FootballTeamGame" - }, - { - "settings": { - "Epic Mode": true, - "Respawn Times": 0.25, - "Score to Win": 3, - "Time Limit": 120, - "map": "Bridgit" - }, - "type": "bsAssault.AssaultGame" - }, - { - "map": "Doom Shroom", - "settings": { - "Enable Impact Bombs": 1, - "Enable Triple Bombs": false, - "Target Count": 2, - "map": "Doom Shroom" - }, - "type": "bsTargetPractice.TargetPracticeGame" - }, - { - "settings": { - "Hold Time": 30, - "Respawn Times": 1.0, - "Time Limit": 300, - "map": "Tip Top" - }, - "type": "bsKingOfTheHill.KingOfTheHillGame" - }, - { - "settings": { - "Epic Mode": false, - "Respawn Times": 1.0, - "Score to Win": 2, - "Time Limit": 300, - "map": "Zigzag" - }, - "type": "bsAssault.AssaultGame" - }, - { - "settings": { - "Epic Mode": false, - "Flag Idle Return Time": 30, - "Flag Touch Return Time": 0, - "Respawn Times": 1.0, - "Score to Win": 3, - "Time Limit": 300, - "map": "Happy Thoughts" - }, - "type": "bsCaptureTheFlag.CTFGame" - }, - { - "settings": { - "Bomb Spawning": 1000, - "Epic Mode": true, - "Laps": 1, - "Mine Spawning": 2000, - "Time Limit": 300, - "map": "Big G" - }, - "type": "bsRace.RaceGame" - }, - { - "settings": { - "Epic Mode": false, - "Kills to Win Per Player": 5, - "Respawn Times": 1.0, - "Time Limit": 300, - "map": "Monkey Face" - }, - "type": "bsDeathMatch.DeathMatchGame" - }, - { - "settings": { - "Hold Time": 30, - "Respawn Times": 1.0, - "Time Limit": 300, - "map": "Lake Frigid" - }, - "type": "bsKeepAway.KeepAwayGame" - }, - { - "settings": { - "Epic Mode": false, - "Flag Idle Return Time": 30, - "Flag Touch Return Time": 3, - "Respawn Times": 1.0, - "Score to Win": 2, - "Time Limit": 300, - "map": "Tip Top" - }, - "type": "bsCaptureTheFlag.CTFGame" - }, - { - "settings": { - "Balance Total Lives": false, - "Epic Mode": false, - "Lives Per Player": 3, - "Respawn Times": 1.0, - "Solo Mode": false, - "Time Limit": 300, - "map": "Crag Castle" - }, - "type": "bsElimination.EliminationGame" - }, - { - "settings": { - "Epic Mode": true, - "Respawn Times": 0.25, - "Time Limit": 120, - "map": "Zigzag" - }, - "type": "bsConquest.ConquestGame" - } - ] + "type": "alliance_elimination.AllianceEliminationGame" + }, + { + "settings": { + "Epic Mode": false, + "Respawn Times": 2.0, + "Time Limit": 300, + "map": "Bridgit Mash" }, - "launchCount": 252, - "lc14173": 1, - "lc14292": 1 - } \ No newline at end of file + "type": "bastd.game.conquest.ConquestGame" + }, + { + "settings": { + "Boxing Gloves": false, + "Epic Mode": false, + "Icy Floor": true, + "Respawn Times": 0.1, + "Score to Win": 7, + "Time Limit": 60, + "map": "Soccer Stadium Pro" + }, + "type": "soccer.HockeyGame" + }, + { + "settings": { + "Balance Total Lives": true, + "Epic Mode": true, + "Lives Per Player": 2, + "Respawn Times": 0.5, + "Solo Mode": true, + "Time Limit": 300, + "map": "\ue019The Limbo" + }, + "type": "bastd.game.elimination.EliminationGame" + }, + { + "settings": { + "Hold Time": 30, + "Respawn Times": 1, + "Time Limit": 300, + "map": "\ue019Platforms" + }, + "type": "bastd.game.kingofthehill.KingOfTheHillGame" + }, + { + "settings": { + "Balloon Mode": false, + "Difficulty": 0.15, + "Distractor Bones": 2, + "Enable Bananas": true, + "Epic Mode": true, + "Immortality": true, + "Mesh Color": 4, + "Respawn Times": 0.5, + "Score to Win": 5, + "Space Under the Mesh": true, + "Time Limit": 300, + "Timer": 3, + "Type of Hot Bomb": 0, + "map": "Hot Bomb Map" + }, + "type": "Hot-Bomb.HotBombGame" + }, + { + "settings": { + "Epic Mode": true, + "Respawn Times": 0.5, + "Score to Win": 3, + "Time Limit": 300, + "map": "\ue019Neo Zone" + }, + "type": "bastd.game.assault.AssaultGame" + }, + { + "settings": { + "Epic Mode": false, + "Flag Idle Return Time": 25, + "Flag Touch Return Time": 8, + "Respawn Times": 1, + "Score to Win": 3, + "Time Limit": 300, + "map": "\ue019Big H" + }, + "type": "bastd.game.capturetheflag.CaptureTheFlagGame" + }, + { + "settings": { + "Enable Bottom Credit": true, + "Enable Punching": false, + "Enable Running": false, + "Epic Mode": false, + "Time Limit": 300, + "map": "Doom Shroom" + }, + "type": "MusicalFlags.MFGame" + }, + { + "settings": { + "Epic Mode": false, + "map": "Rampage" + }, + "type": "TnT_Error.TntErrorGame" + }, + { + "settings": { + "Balance Total Lives": false, + "Epic Mode": false, + "Lives Per Player": 2, + "Respawn Times": 0.5, + "Solo Mode": false, + "Time Limit": 300, + "map": "Football Stadium" + }, + "type": "safe_zone.SafeZoneGame" + }, + { + "settings": { + "Enable Bomb": false, + "Enable Jump": false, + "Enable Pickup": false, + "Epic Mode": true, + "Kills to Win Per Player": 12, + "Obstacles": true, + "Obstacles Count": 16, + "Obstacles Form": 0, + "Obstacles Mirror Shots": false, + "Random Obstacles Color": false, + "Respawn Times": 0.25, + "Speed": true, + "Time Limit": 300, + "Weapon Type": 0, + "map": "Football Stadium" + }, + "type": "quake.game.QuakeGame" + }, + { + "settings": { + "Epic Mode": true, + "Grant Powers on Score": true, + "Night Mode": true, + "Respawn Times": 0.5, + "Score to Win": 1, + "Time Limit": 300, + "map": "Football Stadium" + }, + "type": "BigBall.BBGame" + }, + { + "settings": { + "Enable Bottom Credits": true, + "Epic Mode": false, + "map": "Sky Tiles" + }, + "type": "MemoryGame.MGgame" + }, + { + "settings": { + "Bomb Spawning": 2000, + "Entire Team Must Finish": false, + "Epic Mode": true, + "Laps": 3, + "Mine Spawning": 4000, + "Time Limit": 120, + "map": "InTheAir" + }, + "type": "StumbleRace.StumbleRaceGame" + }, + { + "settings": { + "Epic Mode": false, + "Frozen One Gets Gloves": true, + "Frozen One Gets Shield": false, + "Frozen One Time": 30, + "Respawn Times": 1, + "Time Limit": 300, + "map": "Tip Top" + }, + "type": "FrozenOne16.FrozenOneGame" + }, + { + "settings": { + "Epic Mode": false, + "Players as center of interest": true, + "Respawn Times": 1, + "Score to Win": 4, + "Time Limit": 300, + "map": "Football Stadium" + }, + "type": "baDarkFields.DarkFieldsGame" + }, + { + "settings": { + "Epic Mode": true, + "map": "InTheAir" + }, + "type": "SubwayRun.SubwayRunGame" + }, + { + "settings": { + "Balance Total Lives": false, + "Epic Mode": true, + "Lives Per Player": 1, + "Respawn Times": 1, + "Solo Mode": false, + "Time Limit": 120, + "map": "Courtyard" + }, + "type": "LaserTracer.LasorTracerGame" + }, + { + "settings": { + "Epic Mode": false, + "map": "Lake Frigid" + }, + "type": "IcyEmits16.IcyEmitsGame" + }, + { + "settings": { + "Epic Mode": true, + "map": "Creative Thoughts" + }, + "type": "FlappyBird.FlappyBirdGame" + }, + { + "settings": { + "Respawn Times": 1, + "Score to Win": 1, + "Time Limit": 40, + "map": "Football Stadium" + }, + "type": "EggGame.EggGame" + }, + { + "settings": { + "Balance Total Lives": false, + "Epic Mode": false, + "Lives Per Player": 1, + "Respawn Times": 1, + "Solo Mode": false, + "Time Limit": 300, + "map": "Wooden Floor" + }, + "type": "BlockDash.BlockDashGame" + }, + { + "settings": { + "Epic Mode": true, + "Kills to Win Per Player": 5, + "Respawn Times": 0.25, + "Time Limit": 300, + "map": "Rampage" + }, + "type": "Yeeting-party.BoxingGame" + }, + { + "settings": { + "Epic Mode": false, + "Model Type": 1, + "Respawn Times": 1, + "TNT Hitpoints": 25000, + "Time Limit": 300, + "map": "Football Stadium" + }, + "type": "Heist.TNTTeamGame" + }, + { + "settings": { + "Epic Mode": false, + "Kills to Win Per Player": 5, + "Respawn Times": 1, + "Time Limit": 300, + "map": "Step Right Up" + }, + "type": "CanonFight.CanonFightGame" + }, + { + "settings": { + "Balance Total Lives": true, + "Epic Mode": true, + "Lives Per Player": 2, + "Respawn Times": 0.25, + "Solo Mode": false, + "Time Limit": 300, + "map": "FloatingIsland" + }, + "type": "DuelElimination.DuelEliminationGame" + }, + { + "settings": { + "Enable Bombs": true, + "Epic Mode": false, + "Infection Spread Rate": 0.03, + "Max Infected Size": 6, + "Max Size Increases Every": 20, + "Mines": 10, + "Sec/Extra Mine": 9, + "map": "Football Stadium" + }, + "type": "Infection.Infection" + }, + { + "settings": { + "Bomb Spawning": 2000, + "Entire Team Must Finish": false, + "Epic Mode": true, + "Laps": 2, + "Mine Spawning": 4000, + "Time Limit": 300, + "map": "Big G" + }, + "type": "Reverserace.ReverseRaceGame" + }, + { + "settings": { + "Bomb Spawning": 2000, + "Entire Team Must Finish": false, + "Epic Mode": false, + "Laps": 1, + "Mine Spawning": 4000, + "Time Limit": 120, + "map": "Football Stadium" + }, + "type": "SquidRace.SquidRaceGame" + }, + { + "settings": { + "Disable Bombs": false, + "Disable Punch": true, + "Enable Bottom Credits": true, + "Epic Mode": true, + "Icy Floor": true, + "Night Mode": false, + "Respawn Times": 0.25, + "Score to Win": 7, + "Time Limit": 300, + "map": "Closed Arena" + }, + "type": "VolleyBall.VolleyBallGame" + }, + { + "settings": { + "Epic Mode": true, + "Kills to Win Per Player": 3, + "Respawn Times": 0.25, + "Time Limit": 120, + "map": "Courtyard" + }, + "type": "ms_BombWar.BombWar" + } + ] + }, + "launchCount": 259, + "lc14173": 1, + "lc14292": 1 +} \ No newline at end of file diff --git a/dist/ba_root/config.json.prev b/dist/ba_root/config.json.prev new file mode 100644 index 0000000..a8127c2 --- /dev/null +++ b/dist/ba_root/config.json.prev @@ -0,0 +1,756 @@ +{ + "Achievements": { + "Boom Goes the Dynamite": { + "Complete": false + }, + "Boxer": { + "Complete": false + }, + "Dual Wielding": { + "Complete": false + }, + "Flawless Victory": { + "Complete": false + }, + "Free Loader": { + "Complete": false + }, + "Gold Miner": { + "Complete": false + }, + "Got the Moves": { + "Complete": false + }, + "In Control": { + "Complete": false + }, + "Last Stand God": { + "Complete": false + }, + "Last Stand Master": { + "Complete": false + }, + "Last Stand Wizard": { + "Complete": false + }, + "Mine Games": { + "Complete": false + }, + "Off You Go Then": { + "Complete": false + }, + "Onslaught God": { + "Complete": false + }, + "Onslaught Master": { + "Complete": false + }, + "Onslaught Training Victory": { + "Complete": false + }, + "Onslaught Wizard": { + "Complete": false + }, + "Precision Bombing": { + "Complete": false + }, + "Pro Boxer": { + "Complete": false + }, + "Pro Football Shutout": { + "Complete": false + }, + "Pro Football Victory": { + "Complete": false + }, + "Pro Onslaught Victory": { + "Complete": false + }, + "Pro Runaround Victory": { + "Complete": false + }, + "Rookie Football Shutout": { + "Complete": false + }, + "Rookie Football Victory": { + "Complete": false + }, + "Rookie Onslaught Victory": { + "Complete": false + }, + "Runaround God": { + "Complete": false + }, + "Runaround Master": { + "Complete": false + }, + "Runaround Wizard": { + "Complete": false + }, + "Sharing is Caring": { + "Complete": false + }, + "Stayin' Alive": { + "Complete": false + }, + "Super Mega Punch": { + "Complete": false + }, + "Super Punch": { + "Complete": false + }, + "TNT Terror": { + "Complete": false + }, + "Team Player": { + "Complete": false + }, + "The Great Wall": { + "Complete": false + }, + "The Wall": { + "Complete": false + }, + "Uber Football Shutout": { + "Complete": false + }, + "Uber Football Victory": { + "Complete": false + }, + "Uber Onslaught Victory": { + "Complete": false + }, + "Uber Runaround Victory": { + "Complete": false + } + }, + "Auto Account State": "Server", + "Auto Balance Teams": true, + "Bear Coin": 814, + "Bear Store": { + "Buy Firebombs": false, + "Buy Option": false, + "Buy Percentage": false, + "Promo Code": { + "B-0mB3RYT2z": [ + true, + 910 + ], + "B-Asd14mON9G0D": [ + true, + 910 + ], + "D-rAcK0cJ23": [ + true, + 910 + ], + "E-Am54igO42Os": [ + true, + 600 + ], + "E-M4uN3K34XB": [ + true, + 840 + ], + "E-a27ZO6f3Y": [ + true, + 600 + ], + "G-Am54igO42Os": [ + true, + 1100 + ], + "P-tRo8nM8dZ": [ + true, + 2800 + ], + "PM-731ClcAF": [ + true, + 50000 + ], + "Y-tU2B3S": [ + true, + 500 + ] + } + }, + "Campaigns": {}, + "Custom Team Colors": [ + [ + 2.0, + 0.25, + 1.0 + ], + [ + 1.0, + 0.25, + 0.2 + ] + ], + "Custom Team Names": [ + "ladoo", + "barfi" + ], + "Default Player Profiles": { + "Client Input Device #1": "__account__", + "Client Input Device #2": "__account__" + }, + "Fleet Zone Pings": { + "prod": { + "ap-northeast-1": 149.69879999989644, + "ap-northeast-2": 167.120200001591, + "ap-south-1": 38.507389199694444, + "ap-southeast-1": 91.31349360058448, + "eu-north-1": 175.44400000042515, + "me-south-1": 74.37627740109019 + } + }, + "Free-for-All Max Players": 25, + "Free-for-All Playlist Randomize": true, + "Free-for-All Playlist Selection": "__default__", + "Free-for-All Playlists": {}, + "Idle Exit Minutes": null, + "Local Account Name": "Server7604732", + "PPM Settings": { + "Healing Damage PTG": 72, + "Powers Gravity": true, + "Powerup Name": true, + "Powerup Scale": 1.0, + "Powerup Style": "Auto", + "Powerup Time": false, + "Powerup With Shield": true, + "Powerups": { + "Curse": 1, + "Fire Bombs": 3, + "Fly Bombs": 3, + "Goodbye": 2, + "Healing Damage": 1, + "Health": 1, + "Ice Bombs": 3, + "Ice Man": 1, + "Impact Bombs": 3, + "Impairment Bombs": 2, + "Mine Bombs": 2, + "Punch": 3, + "Shield": 2, + "Speed": 2, + "Sticky Bombs": 3, + "Tank Shield": 1, + "Triple": 3 + }, + "Tank Shield PTG": 96 + }, + "Player Profiles": { + "__account__": { + "character": "Spaz", + "color": [ + 0.5, + 0.25, + 1.0 + ], + "highlight": [ + 0.5, + 0.25, + 1.0 + ] + } + }, + "Plugins": { + "custom_hooks.modSetup": { + "enabled": true + }, + "plugins.Init": { + "enabled": true + } + }, + "Port": 43210, + "Region Pings": { + "af-south-1": 328.2357999996748, + "ap-northeast-1": 150.0941541948123, + "ap-northeast-2": 160.2201884064998, + "ap-south-1": 45.05286179925315, + "ap-southeast-1": 101.91129060697858, + "ap-southeast-2": 183.6228000029223, + "ca-central-1": 262.4569968022115, + "eu-central-1": 155.88790780561976, + "eu-north-1": 181.94498299446423, + "eu-south-1": 156.48616400256287, + "eu-west-1": 179.42955539320246, + "eu-west-2": 158.43393659518915, + "eu-west-3": 170.620519401622, + "me-south-1": 78.16982959693996, + "sa-east-1": 345.57150000182446, + "us-east-1": 238.92560360513744, + "us-east-2": 257.30932620124076, + "us-west-1": 255.34800000605173, + "us-west-2": 292.9734999925131 + }, + "Selected Coop Game": "Easy:Rookie Onslaught", + "Show Tutorial": false, + "Signed In Last Session": false, + "Team Game Max Player": 25, + "Team Game Max Players": 25, + "Team Tournament Playlist Randomize": true, + "Team Tournament Playlist Selection": "BCS_FUN", + "Team Tournament Playlists": { + "BCS_FUN": [ + { + "settings": { + "Basic Bombs": true, + "Epic Mode": true, + "Frozen Bombs": true, + "Grabbing only": true, + "Impact Bombs": true, + "Punching only": true, + "Respawn Times": 0.5, + "Sticky Bombs": true, + "Time Limit": 300, + "map": "\ue019Powerups Factory" + }, + "type": "ArmsRace.ArmsRaceGame" + }, + { + "settings": { + "Countdown Time Each Round": 45, + "Epic Mode": true, + "Kill player on ground": true, + "Random Spawn Point (Team)": false, + "Respawn Times": 1.0, + "Score to Win Per Team": 5, + "Time Limit": 300, + "[On score] Single Celebrate": true, + "[On score] Single Teleport": true, + "map": "Tower D" + }, + "type": "Tower_Rush.BaseRaidGame" + }, + { + "settings": { + "Enable speed": false, + "Epic Mode": true, + "Powerups Spawn": true, + "Respawn Times": 1.0, + "Score to Win": 3, + "Time Limit": 300, + "map": "BasketBall Stadium V2" + }, + "type": "baBasketBomb.BasketGame" + }, + { + "settings": { + "Epic Mode": false, + "Large Box Count": 3, + "Points to win per Player": 1000, + "Respawn Times": 0.5, + "Small Box Count": 1, + "Time Limit": 300, + "map": "Football Stadium" + }, + "type": "crazyBoxes.CrazyBoxGame" + }, + { + "settings": { + "Epic Mode": true, + "Points to Win Per Player": 2, + "Respawn Times": 1.0, + "Target Indicator": 1, + "Time Limit": 300, + "Time to Kill": 7, + "map": "Football Stadium" + }, + "type": "GetTheTarget.GetTheTargetGame" + }, + { + "settings": { + "Balance Total Lives": false, + "Epic Mode": false, + "Lives Per Player": 1, + "Respawn Times": 1.0, + "Solo Mode": false, + "Time Limit": 300, + "map": "Football Stadium" + }, + "type": "GravityFalls.GFGame" + }, + { + "settings": { + "Epic Mode": true, + "Respawn Times": 0.5, + "Score to Win": 3, + "Time Limit": 300, + "map": "Bridgit Parallelo" + }, + "type": "bastd.game.assault.AssaultGame" + }, + { + "settings": { + "Epic Mode": false, + "map": "Wooden Floor" + }, + "type": "UFOAttackGame.UFOAttackGame" + }, + { + "settings": { + "Balance Total Lives": true, + "Epic Mode": true, + "Lives Per Player": 3, + "Players Per Team In Arena": 2, + "Respawn Times": 0.25, + "Time Limit": 300, + "map": "Bridgit Parallelo" + }, + "type": "alliance_elimination.AllianceEliminationGame" + }, + { + "settings": { + "Epic Mode": false, + "Respawn Times": 2.0, + "Time Limit": 300, + "map": "Bridgit Mash" + }, + "type": "bastd.game.conquest.ConquestGame" + }, + { + "settings": { + "Boxing Gloves": false, + "Epic Mode": false, + "Icy Floor": true, + "Respawn Times": 0.1, + "Score to Win": 7, + "Time Limit": 60, + "map": "Soccer Stadium Pro" + }, + "type": "soccer.HockeyGame" + }, + { + "settings": { + "Balance Total Lives": true, + "Epic Mode": true, + "Lives Per Player": 2, + "Respawn Times": 0.5, + "Solo Mode": true, + "Time Limit": 300, + "map": "\ue019The Limbo" + }, + "type": "bastd.game.elimination.EliminationGame" + }, + { + "settings": { + "Hold Time": 30, + "Respawn Times": 1, + "Time Limit": 300, + "map": "\ue019Platforms" + }, + "type": "bastd.game.kingofthehill.KingOfTheHillGame" + }, + { + "settings": { + "Balloon Mode": false, + "Difficulty": 0.15, + "Distractor Bones": 2, + "Enable Bananas": true, + "Epic Mode": true, + "Immortality": true, + "Mesh Color": 4, + "Respawn Times": 0.5, + "Score to Win": 5, + "Space Under the Mesh": true, + "Time Limit": 300, + "Timer": 3, + "Type of Hot Bomb": 0, + "map": "Hot Bomb Map" + }, + "type": "Hot-Bomb.HotBombGame" + }, + { + "settings": { + "Epic Mode": true, + "Respawn Times": 0.5, + "Score to Win": 3, + "Time Limit": 300, + "map": "\ue019Neo Zone" + }, + "type": "bastd.game.assault.AssaultGame" + }, + { + "settings": { + "Epic Mode": false, + "Flag Idle Return Time": 25, + "Flag Touch Return Time": 8, + "Respawn Times": 1, + "Score to Win": 3, + "Time Limit": 300, + "map": "\ue019Big H" + }, + "type": "bastd.game.capturetheflag.CaptureTheFlagGame" + }, + { + "settings": { + "Enable Bottom Credit": true, + "Enable Punching": false, + "Enable Running": false, + "Epic Mode": false, + "Time Limit": 300, + "map": "Doom Shroom" + }, + "type": "MusicalFlags.MFGame" + }, + { + "settings": { + "Epic Mode": false, + "map": "Rampage" + }, + "type": "TnT_Error.TntErrorGame" + }, + { + "settings": { + "Balance Total Lives": false, + "Epic Mode": false, + "Lives Per Player": 2, + "Respawn Times": 0.5, + "Solo Mode": false, + "Time Limit": 300, + "map": "Football Stadium" + }, + "type": "safe_zone.SafeZoneGame" + }, + { + "settings": { + "Enable Bomb": false, + "Enable Jump": false, + "Enable Pickup": false, + "Epic Mode": true, + "Kills to Win Per Player": 12, + "Obstacles": true, + "Obstacles Count": 16, + "Obstacles Form": 0, + "Obstacles Mirror Shots": false, + "Random Obstacles Color": false, + "Respawn Times": 0.25, + "Speed": true, + "Time Limit": 300, + "Weapon Type": 0, + "map": "Football Stadium" + }, + "type": "quake.game.QuakeGame" + }, + { + "settings": { + "Epic Mode": true, + "Grant Powers on Score": true, + "Night Mode": true, + "Respawn Times": 0.5, + "Score to Win": 1, + "Time Limit": 300, + "map": "Football Stadium" + }, + "type": "BigBall.BBGame" + }, + { + "settings": { + "Enable Bottom Credits": true, + "Epic Mode": false, + "map": "Sky Tiles" + }, + "type": "MemoryGame.MGgame" + }, + { + "settings": { + "Bomb Spawning": 2000, + "Entire Team Must Finish": false, + "Epic Mode": true, + "Laps": 3, + "Mine Spawning": 4000, + "Time Limit": 120, + "map": "InTheAir" + }, + "type": "StumbleRace.StumbleRaceGame" + }, + { + "settings": { + "Epic Mode": false, + "Frozen One Gets Gloves": true, + "Frozen One Gets Shield": false, + "Frozen One Time": 30, + "Respawn Times": 1, + "Time Limit": 300, + "map": "Tip Top" + }, + "type": "FrozenOne16.FrozenOneGame" + }, + { + "settings": { + "Epic Mode": false, + "Players as center of interest": true, + "Respawn Times": 1, + "Score to Win": 4, + "Time Limit": 300, + "map": "Football Stadium" + }, + "type": "baDarkFields.DarkFieldsGame" + }, + { + "settings": { + "Epic Mode": true, + "map": "InTheAir" + }, + "type": "SubwayRun.SubwayRunGame" + }, + { + "settings": { + "Balance Total Lives": false, + "Epic Mode": true, + "Lives Per Player": 1, + "Respawn Times": 1, + "Solo Mode": false, + "Time Limit": 120, + "map": "Courtyard" + }, + "type": "LaserTracer.LasorTracerGame" + }, + { + "settings": { + "Epic Mode": false, + "map": "Lake Frigid" + }, + "type": "IcyEmits16.IcyEmitsGame" + }, + { + "settings": { + "Epic Mode": true, + "map": "Creative Thoughts" + }, + "type": "FlappyBird.FlappyBirdGame" + }, + { + "settings": { + "Respawn Times": 1, + "Score to Win": 1, + "Time Limit": 40, + "map": "Football Stadium" + }, + "type": "EggGame.EggGame" + }, + { + "settings": { + "Balance Total Lives": false, + "Epic Mode": false, + "Lives Per Player": 1, + "Respawn Times": 1, + "Solo Mode": false, + "Time Limit": 300, + "map": "Wooden Floor" + }, + "type": "BlockDash.BlockDashGame" + }, + { + "settings": { + "Epic Mode": true, + "Kills to Win Per Player": 5, + "Respawn Times": 0.25, + "Time Limit": 300, + "map": "Rampage" + }, + "type": "Yeeting-party.BoxingGame" + }, + { + "settings": { + "Epic Mode": false, + "Model Type": 1, + "Respawn Times": 1, + "TNT Hitpoints": 25000, + "Time Limit": 300, + "map": "Football Stadium" + }, + "type": "Heist.TNTTeamGame" + }, + { + "settings": { + "Epic Mode": false, + "Kills to Win Per Player": 5, + "Respawn Times": 1, + "Time Limit": 300, + "map": "Step Right Up" + }, + "type": "CanonFight.CanonFightGame" + }, + { + "settings": { + "Balance Total Lives": true, + "Epic Mode": true, + "Lives Per Player": 2, + "Respawn Times": 0.25, + "Solo Mode": false, + "Time Limit": 300, + "map": "FloatingIsland" + }, + "type": "DuelElimination.DuelEliminationGame" + }, + { + "settings": { + "Enable Bombs": true, + "Epic Mode": false, + "Infection Spread Rate": 0.03, + "Max Infected Size": 6, + "Max Size Increases Every": 20, + "Mines": 10, + "Sec/Extra Mine": 9, + "map": "Football Stadium" + }, + "type": "Infection.Infection" + }, + { + "settings": { + "Bomb Spawning": 2000, + "Entire Team Must Finish": false, + "Epic Mode": true, + "Laps": 2, + "Mine Spawning": 4000, + "Time Limit": 300, + "map": "Big G" + }, + "type": "Reverserace.ReverseRaceGame" + }, + { + "settings": { + "Bomb Spawning": 2000, + "Entire Team Must Finish": false, + "Epic Mode": false, + "Laps": 1, + "Mine Spawning": 4000, + "Time Limit": 120, + "map": "Football Stadium" + }, + "type": "SquidRace.SquidRaceGame" + }, + { + "settings": { + "Disable Bombs": false, + "Disable Punch": true, + "Enable Bottom Credits": true, + "Epic Mode": true, + "Icy Floor": true, + "Night Mode": false, + "Respawn Times": 0.25, + "Score to Win": 7, + "Time Limit": 300, + "map": "Closed Arena" + }, + "type": "VolleyBall.VolleyBallGame" + }, + { + "settings": { + "Epic Mode": true, + "Kills to Win Per Player": 3, + "Respawn Times": 0.25, + "Time Limit": 120, + "map": "Courtyard" + }, + "type": "ms_BombWar.BombWar" + } + ] + }, + "launchCount": 259, + "lc14173": 1, + "lc14292": 1 +} \ No newline at end of file diff --git a/dist/ba_root/mods/chatHandle/ChatCommands/Handlers.py b/dist/ba_root/mods/chatHandle/ChatCommands/Handlers.py index 04d7616..768ba78 100644 --- a/dist/ba_root/mods/chatHandle/ChatCommands/Handlers.py +++ b/dist/ba_root/mods/chatHandle/ChatCommands/Handlers.py @@ -1,7 +1,8 @@ # Released under the MIT License. See LICENSE for details. from playersData import pdata -import ba, _ba +import ba +import ba.internal @@ -17,7 +18,7 @@ def clientid_to_accountid(clientid): Returns: None """ - for i in _ba.get_game_roster(): + for i in ba.internal.get_game_roster(): if i['client_id'] == clientid: return i['account_id'] return None @@ -52,6 +53,6 @@ def check_permissions(accountid, command): def is_server(accid): - for i in _ba.get_game_roster(): + for i in ba.internal.get_game_roster(): if i['account_id']==accid and i['client_id']==-1: return True \ No newline at end of file diff --git a/dist/ba_root/mods/chatHandle/ChatCommands/Main.py b/dist/ba_root/mods/chatHandle/ChatCommands/Main.py index 38400b4..fd3417d 100644 --- a/dist/ba_root/mods/chatHandle/ChatCommands/Main.py +++ b/dist/ba_root/mods/chatHandle/ChatCommands/Main.py @@ -11,6 +11,7 @@ from .Handlers import check_permissions from chatHandle.chatFilter import ChatFilter from bastd.actor import popuptext import ba, _ba +import ba.internal import setting from serverData import serverdata @@ -98,12 +99,12 @@ def QuickAccess(msg, client_id): if msg.startswith(","): name = "" teamid = 0 - for i in _ba.get_foreground_host_session().sessionplayers: + for i in ba.internal.get_foreground_host_session().sessionplayers: if i.inputdevice.client_id == client_id: teamid = i.sessionteam.id name = i.getname(True) - for i in _ba.get_foreground_host_session().sessionplayers: + for i in ba.internal.get_foreground_host_session().sessionplayers: if i.sessionteam and teamid == i.sessionteam.id and i.inputdevice.client_id != client_id: _ba.screenmessage(name + ":" + msg[1:], clients=[i.inputdevice.client_id], color=(0.3, 0.6, 0.3), transient=True) diff --git a/dist/ba_root/mods/chatHandle/ChatCommands/commands/Handlers.py b/dist/ba_root/mods/chatHandle/ChatCommands/commands/Handlers.py index 33e6619..74f71ba 100644 --- a/dist/ba_root/mods/chatHandle/ChatCommands/commands/Handlers.py +++ b/dist/ba_root/mods/chatHandle/ChatCommands/commands/Handlers.py @@ -1,5 +1,6 @@ """ Some useful handlers to reduce lot of code """ import _ba, ba +import ba.internal @@ -16,7 +17,7 @@ def send(msg, clientid): def clientid_to_myself(clientid): """Return Player Index Of Self Player""" - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() for i in range(len(session.sessionplayers)): if session.sessionplayers[i].inputdevice.client_id == clientid: diff --git a/dist/ba_root/mods/chatHandle/ChatCommands/commands/Management.py b/dist/ba_root/mods/chatHandle/ChatCommands/commands/Management.py index 975bd7d..6acd9b6 100644 --- a/dist/ba_root/mods/chatHandle/ChatCommands/commands/Management.py +++ b/dist/ba_root/mods/chatHandle/ChatCommands/commands/Management.py @@ -3,9 +3,10 @@ from playersData import pdata # from tools.whitelist import add_to_white_list, add_commit_to_logs from serverData import serverdata import ba, _ba, time, setting +import ba.internal import _thread from tools import playlist -Commands = ['maxplayers','playlist','ban','kick', 'remove', 'end', 'quit', 'mute', 'unmute', 'slowmo', 'nv', 'dv', 'pause', 'cameramode', 'createrole', 'addrole', 'removerole', 'addcommand', 'addcmd', 'removecommand','getroles', 'removecmd', 'changetag','customtag','customeffect','add', 'spectators', 'lobbytime'] +Commands = ['lm', 'gp', 'party', 'quit', 'kickvote','maxplayers','playlist','ban','kick', 'remove', 'end', 'quit', 'mute', 'unmute', 'slowmo', 'nv', 'dv', 'pause', 'cameramode', 'createrole', 'addrole', 'removerole', 'addcommand', 'addcmd', 'removecommand','getroles', 'removecmd', 'changetag','customtag','customeffect','add', 'spectators', 'lobbytime'] CommandAliases = ['max','rm', 'next', 'restart', 'mutechat', 'unmutechat', 'sm', 'slow', 'night', 'day', 'pausegame', 'camera_mode', 'rotate_camera','effect'] @@ -33,6 +34,17 @@ def ExcelCommand(command, arguments, clientid, accountid): ban(arguments) elif command in ['end', 'next']: end(arguments) + elif command == 'kickvote': + kikvote(arguments, clientid) + + elif command == 'lm': + last_msgs(clientid) + + elif command == 'gp': + get_profiles(arguments, clientid) + + elif command == 'party': + party_toggle(arguments) elif command in ['quit', 'restart']: quit(arguments) @@ -101,7 +113,7 @@ def changepartysize(arguments): if len(arguments)==0: _ba.chatmessage("enter number") else: - _ba.set_public_party_max_size(int(arguments[0])) + ba.internal.set_public_party_max_size(int(arguments[0])) def changeplaylist(arguments): if len(arguments)==0: @@ -116,15 +128,75 @@ def changeplaylist(arguments): def kick(arguments): - _ba.disconnect_client(int(arguments[0])) + ba.internal.disconnect_client(int(arguments[0])) return +def kikvote(arguments, clientid): + if arguments == [] or arguments == [''] or len(arguments) < 2: + return + elif arguments[0] == 'enable': + if arguments[1] == 'all': + _ba.set_enable_default_kick_voting(True) + else: + try: + cl_id=int(arguments[1]) + for ros in ba.internal.get_game_roster(): + if ros["client_id"]==cl_id: + if ros["account_id"] in serverdata.clients: + serverdata.clients[ros["account_id"]]["canStartKickVote"]=True + send("Upon server restart, Kick-vote will be enabled for this person", clientid) + return + except: + return + + elif arguments[0] == 'disable': + if arguments[1] == 'all': + _ba.set_enable_default_kick_voting(False) + else: + try: + cl_id=int(arguments[1]) + for ros in ba.internal.get_game_roster(): + if ros["client_id"]==cl_id: + _ba.disable_kickvote(ros["account_id"]) + send("Kick-vote disabled for this person", clientid) + if ros["account_id"] in serverdata.clients: + serverdata.clients[ros["account_id"]]["canStartKickVote"]=False + return + except: + return + else: + return + +def last_msgs(clientid): + for i in ba.internal.get_chat_messages(): + send(i,clientid) + +def get_profiles(arguments,clientid): + try: + playerID = int(arguments[0]) + num = 1 + for i in ba.internal.get_foreground_host_session().sessionplayers[playerID].inputdevice.get_player_profiles(): + try: + send(f"{num})- {i}",clientid) + num += 1 + except: + pass + except: + pass + +def party_toggle(arguments): + if arguments == ['public']: + ba.internal.set_public_party_enabled(True) + _ba.chatmessage("party is public now") + elif arguments == ['private']: + ba.internal.set_public_party_enabled(False) + _ba.chatmessage("party is private now") + else: + pass def end(arguments): - if arguments == [] or arguments == ['']: - try: with _ba.Context(_ba.get_foreground_host_activity()): _ba.get_foreground_host_activity().end_game() @@ -135,7 +207,7 @@ def ban(arguments): try: cl_id=int(arguments[0]) ac_id="" - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros["client_id"]==cl_id: _thread.start_new_thread(pdata.ban_player,(ros['account_id'],)) @@ -161,7 +233,7 @@ def mute(arguments): try: cl_id=int(arguments[0]) ac_id="" - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros["client_id"]==cl_id: _thread.start_new_thread(pdata.mute,(ros['account_id'],)) @@ -180,7 +252,7 @@ def un_mute(arguments): try: cl_id=int(arguments[0]) ac_id="" - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros["client_id"]==cl_id: pdata.unmute(ros['account_id']) ac_id=ros['account_id'] @@ -198,13 +270,13 @@ def remove(arguments): return elif arguments[0] == 'all': - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() for i in session.sessionplayers: i.remove_from_game() else: try: - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() for i in session.sessionplayers: if i.inputdevice.client_id== int(arguments[0]): i.remove_from_game() @@ -298,7 +370,7 @@ def create_role(arguments): def add_role_to_player(arguments): try: - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() for i in session.sessionplayers: if i.inputdevice.client_id== int(arguments[1]): roles=pdata.add_player_role(arguments[0],i.get_v1_account_id()) @@ -309,7 +381,7 @@ def add_role_to_player(arguments): def remove_role_from_player(arguments): try: - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() for i in session.sessionplayers: if i.inputdevice.client_id== int(arguments[1]): roles=pdata.remove_player_role(arguments[0],i.get_v1_account_id()) @@ -318,7 +390,7 @@ def remove_role_from_player(arguments): return def get_roles_of_player(arguments,clientid): try: - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() roles=[] reply="" for i in session.sessionplayers: @@ -338,7 +410,7 @@ def change_role_tag(arguments): def set_custom_tag(arguments): try: - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() for i in session.sessionplayers: if i.inputdevice.client_id== int(arguments[1]): roles=pdata.set_tag(arguments[0],i.get_v1_account_id()) @@ -346,7 +418,7 @@ def set_custom_tag(arguments): return def set_custom_effect(arguments): try: - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() for i in session.sessionplayers: if i.inputdevice.client_id== int(arguments[1]): roles=pdata.set_effect(arguments[0],i.get_v1_account_id()) @@ -398,7 +470,7 @@ def remove_command_to_role(arguments): # return # else: - # rost = _ba.get_game_roster() + # rost = ba.internal.get_game_roster() # for i in rost: # if i['client_id'] == int(arguments[0]): @@ -428,11 +500,12 @@ def spectators(arguments): def change_lobby_check_time(arguments): - try: - argument = int(arguments[0]) - except: - _ba.chatmessage("must type numbe to change lobby check time") - settings = setting.get_settings_data() - settings["white_list"]["lobbychecktime"] = argument - setting.commit(settings) - _ba.chatmessage(f"lobby check time is {arg} now") + try: + argument = int(arguments[0]) + except: + _ba.chatmessage("must type number to change lobby check time") + return + settings = setting.get_settings_data() + settings["white_list"]["lobbychecktime"] = argument + setting.commit(settings) + _ba.chatmessage(f"lobby check time is {argument} now") diff --git a/dist/ba_root/mods/chatHandle/ChatCommands/commands/NormalCommands.py b/dist/ba_root/mods/chatHandle/ChatCommands/commands/NormalCommands.py index 180be9b..d564349 100644 --- a/dist/ba_root/mods/chatHandle/ChatCommands/commands/NormalCommands.py +++ b/dist/ba_root/mods/chatHandle/ChatCommands/commands/NormalCommands.py @@ -1,5 +1,6 @@ from .Handlers import send import ba, _ba +import ba.internal from stats import mystats from ba._general import Call import _thread @@ -56,7 +57,7 @@ def list(clientid): list = p.format('Name', 'Client ID' , 'Player ID')+seprator - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() for index, player in enumerate(session.sessionplayers): @@ -76,7 +77,7 @@ def accountid_request(arguments, clientid, accountid): else: try: - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() player = session.sessionplayers[int(arguments[0])] name = player.getname(full=True, icon=True) diff --git a/dist/ba_root/mods/chatHandle/chatFilter/ChatFilter.py b/dist/ba_root/mods/chatHandle/chatFilter/ChatFilter.py index 78d40c5..8d4bef2 100644 --- a/dist/ba_root/mods/chatHandle/chatFilter/ChatFilter.py +++ b/dist/ba_root/mods/chatHandle/chatFilter/ChatFilter.py @@ -1,5 +1,6 @@ # Released under the MIT License. See LICENSE for details. import ba, _ba +import ba.internal from serverData import serverdata from features import profanity from tools import servercheck @@ -52,7 +53,7 @@ def filter(msg,pb_id,client_id): smsgcount+=1 if smsgcount>=3: logger.log(pb_id+" | kicked for chat spam") - _ba.disconnect_client(client_id) + ba.internal.disconnect_client(client_id) smsgcount=0 _ba.screenmessage("Don\'t SPAM!", color=(1,0,0), transient=True, clients=[client_id]) if not check_permissions(pb_id): @@ -88,7 +89,7 @@ def addWarn(pb_id,client_id): if warn > settings["maxWarnCount"]: _ba.screenmessage(settings["afterWarnKickMsg"],color=(1,0,0),transient=True,clients=[client_id]) logger.log(pb_id+" | kicked for chat spam") - _ba.disconnect_client(client_id) + ba.internal.disconnect_client(client_id) _thread.start_new_thread(servercheck.reportSpam,(pb_id,)) else: diff --git a/dist/ba_root/mods/chatHandle/handlechat.py b/dist/ba_root/mods/chatHandle/handlechat.py index d8d8829..ba3a7b2 100644 --- a/dist/ba_root/mods/chatHandle/handlechat.py +++ b/dist/ba_root/mods/chatHandle/handlechat.py @@ -7,6 +7,7 @@ from tools import logger, servercheck from chatHandle.chatFilter import ChatFilter from features import EndVote import ba, _ba +import ba.internal import setting settings = setting.get_settings_data() @@ -22,7 +23,7 @@ def filter_chat_message(msg, client_id): displaystring = "" currentname = "" - for i in _ba.get_game_roster(): + for i in ba.internal.get_game_roster(): if i['client_id'] == client_id: acid = i['account_id'] try: diff --git a/dist/ba_root/mods/custom_hooks.py b/dist/ba_root/mods/custom_hooks.py index 537fcdd..d06a40c 100644 --- a/dist/ba_root/mods/custom_hooks.py +++ b/dist/ba_root/mods/custom_hooks.py @@ -28,7 +28,7 @@ from chatHandle import handlechat from features import team_balancer, afk_check, fire_flies, dual_team_score as newdts from stats import mystats from spazmod import modifyspaz -from tools import servercheck, ServerUpdate, logger +from tools import servercheck, ServerUpdate, logger, playlist from playersData import pdata from features import EndVote from features import text_on_map @@ -43,15 +43,20 @@ def filter_chat_message(msg: str, client_id: int) -> str | None: """Returns all in game messages or None (ignore's message).""" return handlechat.filter_chat_message(msg, client_id) +# ba_meta export plugin +class modSetup(ba.Plugin): + def on_app_running(self): + """Runs when app is launched.""" + bootstraping() + servercheck.checkserver().start() + ServerUpdate.check() -def on_app_running() -> None: - """Runs when app is launched.""" - bootstraping() - servercheck.checkserver().start() - ServerUpdate.check() + if settings["afk_remover"]['enable']: + afk_check.checkIdle().start() + ba.timer(60,playlist.flush_playlists) + def on_app_shutdown(self): + pass - if settings["afk_remover"]['enable']: - afk_check.checkIdle().start() def score_screen_on_begin(_stats: ba.Stats) -> None: @@ -67,6 +72,7 @@ def playerspaz_init(playerspaz: ba.Player, node: ba.Node, player: ba.Player): def bootstraping(): """Bootstarps the server.""" + print("Bootstraping mods..") # server related _ba.set_server_device_name(settings["HostDeviceName"]) _ba.set_server_name(settings["HostName"]) @@ -99,7 +105,7 @@ def bootstraping(): if settings["StumbledScoreScreen"]: from features import StumbledScoreScreen if settings["colorfullMap"]: - from plugins import colorfulmaps + from plugins import colorfulmaps2 # import features if settings["whitelist"]: diff --git a/dist/ba_root/mods/features/EndVote.py b/dist/ba_root/mods/features/EndVote.py index dee5af8..f3701e5 100644 --- a/dist/ba_root/mods/features/EndVote.py +++ b/dist/ba_root/mods/features/EndVote.py @@ -1,6 +1,7 @@ # EndVote by -mr.smoothy import _ba, ba +import ba.internal import time last_end_vote_start_time = 0 @@ -27,7 +28,7 @@ def vote_end(pb_id, client_id): # clean up voters list active_players = [] - for player in _ba.get_game_roster(): + for player in ba.internal.get_game_roster(): active_players.append(player['account_id']) for voter in voters: if voter not in active_players: diff --git a/dist/ba_root/mods/features/afk_check.py b/dist/ba_root/mods/features/afk_check.py index ad64e9d..5d35251 100644 --- a/dist/ba_root/mods/features/afk_check.py +++ b/dist/ba_root/mods/features/afk_check.py @@ -3,6 +3,7 @@ import time import ba from ba._general import Call import _ba +import ba.internal import setting settings = setting.get_settings_data() INGAME_TIME=settings["afk_remover"]["ingame_idle_time_in_secs"] @@ -14,7 +15,7 @@ class checkIdle(object): self.lobbies={} def check(self): current=ba.time(ba.TimeType.REAL,timeformat=ba.TimeFormat.MILLISECONDS) - for player in _ba.get_foreground_host_session().sessionplayers: + for player in ba.internal.get_foreground_host_session().sessionplayers: last_input=int(player.inputdevice.get_last_input_time()) afk_time=int((current-last_input)/1000) if afk_time in range(INGAME_TIME,INGAME_TIME+20): @@ -23,7 +24,7 @@ class checkIdle(object): player.remove_from_game() if LOBBY_KICK: current_players=[] - for player in _ba.get_game_roster(): + for player in ba.internal.get_game_roster(): if player['client_id'] !=-1 and len(player['players']) ==0: current_players.append(player['client_id']) if player['client_id'] not in self.lobbies: @@ -32,14 +33,14 @@ class checkIdle(object): if lobby_afk in range(INLOBBY_TIME,INLOBBY_TIME+10): _ba.screenmessage("Join game within "+str(INLOBBY_TIME+10-lobby_afk)+" secs",color=(1,0,0),transient=True,clients=[player['client_id']]) if lobby_afk > INLOBBY_TIME+ 10: - _ba.disconnect_client(player['client_id'],0) + ba.internal.disconnect_client(player['client_id'],0) # clean the lobbies dict temp=self.lobbies.copy() for clid in temp: if clid not in current_players: del self.lobbies[clid] def warn_player(self,pbid,msg): - for player in _ba.get_game_roster(): + for player in ba.internal.get_game_roster(): if player["account_id"]==pbid: _ba.screenmessage(msg,color=(1,0,0),transient=True,clients=[player['client_id']]) diff --git a/dist/ba_root/mods/features/discord_bot.py b/dist/ba_root/mods/features/discord_bot.py index 9364f5e..4d139f4 100644 --- a/dist/ba_root/mods/features/discord_bot.py +++ b/dist/ba_root/mods/features/discord_bot.py @@ -6,6 +6,7 @@ from discord.ext.commands import Bot import ba from ba._general import Call import _ba +import ba.internal import json import os import _thread @@ -168,15 +169,15 @@ class BsDataThread(object): currentMap='' global stats - for i in _ba.get_game_roster(): + for i in ba.internal.get_game_roster(): try: liveplayers[i['account_id']]={'name':i['players'][0]['name_full'],'client_id':i['client_id'],'device_id':i['display_string']} except: liveplayers[i['account_id']]={'name':"",'clientid':i['client_id'],'device_id':i['display_string']} try: - nextMap=_ba.get_foreground_host_session().get_next_game_description().evaluate() + nextMap=ba.internal.get_foreground_host_session().get_next_game_description().evaluate() - current_game_spec=_ba.get_foreground_host_session()._current_game_spec + current_game_spec=ba.internal.get_foreground_host_session()._current_game_spec gametype: Type[GameActivity] =current_game_spec['resolved_type'] currentMap=gametype.get_settings_display_string(current_game_spec).evaluate() diff --git a/dist/ba_root/mods/features/fire_flies.py b/dist/ba_root/mods/features/fire_flies.py index d4bb06c..0c094ba 100644 --- a/dist/ba_root/mods/features/fire_flies.py +++ b/dist/ba_root/mods/features/fire_flies.py @@ -3,6 +3,7 @@ import ba import _ba from bastd.gameutils import SharedObjects import random +import weakref from ba._messages import DieMessage, DeathType, OutOfBoundsMessage, UNHANDLED on_begin_original = ba._activity.Activity.on_begin @@ -65,6 +66,7 @@ class FireFly(ba.Actor): )) self.node = ba.newnode( 'prop', + delegate=self, attrs={ 'model': ba.getmodel('bomb'), 'position': (2,4,2), @@ -124,10 +126,11 @@ class FireFly(ba.Actor): def handlemessage(self, msg): if isinstance(msg, ba.DieMessage): self.off() - elif isinstance(msg, OutOfBoundMessage): - self.handlemessage(ba.DieMessage(how=OutOfBoundMessage)) - else: - return super().handlemessage(msg) + return None + elif isinstance(msg, OutOfBoundsMessage): + return self.handlemessage(ba.DieMessage(how=DeathType.OUT_OF_BOUNDS)) + + return super().handlemessage(msg) def generate_keys(self,m): keys = {} diff --git a/dist/ba_root/mods/features/team_balancer.py b/dist/ba_root/mods/features/team_balancer.py index ff2c335..efaa787 100644 --- a/dist/ba_root/mods/features/team_balancer.py +++ b/dist/ba_root/mods/features/team_balancer.py @@ -1,4 +1,5 @@ -import _ba,ba +import _ba,ba +import ba.internal import setting from serverData import serverdata @@ -9,7 +10,7 @@ from tools import playlist def balanceTeams(): - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() if settings["coopModeWithLessPlayers"]["enable"] and len(session.sessionplayers) < settings["coopModeWithLessPlayers"]["minPlayerToExitCoop"]: playlist.setPlaylist('coop') return @@ -37,7 +38,7 @@ def movePlayers(fromTeam,toTeam,count): return # disabling team balance for now , until we found solution # Error : on score screen when shifted player left the game on_player_leave unable to found player in activity team - session=_ba.get_foreground_host_session() + session=ba.internal.get_foreground_host_session() fromTeam=session.sessionteams[fromTeam] toTeam=session.sessionteams[toTeam] for i in range(0,count): @@ -50,12 +51,12 @@ def movePlayers(fromTeam,toTeam,count): toTeam.players.append(player) def broadCastShiftMsg(pb_id): - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros['account_id']==pb_id: _ba.screenmessage("Shifted "+ros["display_string"]+" to balance team") def on_player_join(): - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() if len(session.sessionplayers)>1: return if isinstance(session,DualTeamSession): @@ -69,7 +70,7 @@ def on_player_join(): def checkToExitCoop(): - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() if len(session.sessionplayers) >= settings["coopModeWithLessPlayers"]["minPlayerToExitCoop"] and not serverdata.coopmode: playlist.setPlaylist('default') diff --git a/dist/ba_root/mods/features/text_on_map.py b/dist/ba_root/mods/features/text_on_map.py index 7a31106..3b8512d 100644 --- a/dist/ba_root/mods/features/text_on_map.py +++ b/dist/ba_root/mods/features/text_on_map.py @@ -4,6 +4,7 @@ from ba._generated.enums import TimeType import ba, _ba +import ba.internal import setting from stats import mystats from datetime import datetime @@ -23,7 +24,7 @@ class textonmap: nextMap="" try: - nextMap=_ba.get_foreground_host_session().get_next_game_description().evaluate() + nextMap=ba.internal.get_foreground_host_session().get_next_game_description().evaluate() except: pass self.index = 0 diff --git a/dist/ba_root/mods/games/ArmsRace.py b/dist/ba_root/mods/games/ArmsRace.py new file mode 100644 index 0000000..56d2f23 --- /dev/null +++ b/dist/ba_root/mods/games/ArmsRace.py @@ -0,0 +1,193 @@ +#Ported by: Freaku / @[Just] Freak#4999 + +#Join BCS: +# https://discord.gg/ucyaesh + + + +# ba_meta require api 6 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.actor.playerspaz import PlayerSpaz + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional + + + +class State: + def __init__(self, bomb=None, grab=False, punch=False, curse=False, required=False, final=False, name=''): + self.bomb = bomb + self.grab = grab + self.punch = punch + self.pickup = False + self.curse = curse + self.required = required or final + self.final = final + self.name = name + self.next = None + self.index = None + + def apply(self, spaz): + spaz.disconnect_controls_from_player() + spaz.connect_controls_to_player(enable_punch=self.punch, + enable_bomb=self.bomb, + enable_pickup=self.grab) + if self.curse: + spaz.curse_time = -1 + spaz.curse() + if self.bomb: + spaz.bomb_type = self.bomb + spaz.set_score_text(self.name) + + def get_setting(self): + return (self.name) + + +states = [ State(bomb='normal', name='Basic Bombs'), + State(bomb='ice', name='Frozen Bombs'), + State(bomb='sticky', name='Sticky Bombs'), + State(bomb='impact', name='Impact Bombs'), + State(grab=True, name='Grabbing only'), + State(punch=True, name='Punching only'), + State(curse=True, name='Cursed', final=True) ] + +class Player(ba.Player['Team']): + """Our player type for this game.""" + def __init__(self): + self.state = None + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class ArmsRaceGame(ba.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Arms Race' + description = 'Upgrade your weapon by eliminating enemies.\nWin the match by being the first player\nto get a kill while cursed.' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: + settings = [ + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', default=False)] + for state in states: + if not state.required: + settings.append(ba.BoolSetting(state.get_setting(), default=True)) + + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self.states = [s for s in states if settings.get(s.name, True)] + for i, state in enumerate(self.states): + if i < len(self.states) and not state.final: + state.next = self.states[i + 1] + state.index = i + self._dingsound = ba.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._time_limit = float(settings['Time Limit']) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC if self._epic_mode else + ba.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Upgrade your weapon by eliminating enemies.' + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'kill ${ARG1} enemies', len(self.states) + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + # self.setup_standard_powerup_drops() + + def on_player_join(self, player): + if player.state is None: + player.state = self.states[0] + self.spawn_player(player) + + # overriding the default character spawning.. + def spawn_player(self, player): + if player.state is None: + player.state = self.states[0] + super().spawn_player(player) + player.state.apply(player.actor) + + def isValidKill(self, m): + if m.getkillerplayer(Player) is None: + return False + + if m.getkillerplayer(Player).team is m.getplayer(Player).team: + return False + + return True + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, ba.PlayerDiedMessage): + if self.isValidKill(msg): + if not msg.getkillerplayer(Player).state.final: + msg.getkillerplayer(Player).state = msg.getkillerplayer(Player).state.next + msg.getkillerplayer(Player).state.apply(msg.getkillerplayer(Player).actor) + else: + msg.getkillerplayer(Player).team.score += 1 + self.end_game() + self.respawn_player(msg.getplayer(Player)) + + else: + return super().handlemessage(msg) + return None + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) diff --git a/dist/ba_root/mods/games/BigBall.py b/dist/ba_root/mods/games/BigBall.py new file mode 100644 index 0000000..cca8815 --- /dev/null +++ b/dist/ba_root/mods/games/BigBall.py @@ -0,0 +1,515 @@ +#Made by MythB +#Ported by: Freaku / @[Just] Freak#4999 + + + + + + +# ba_meta require api 6 +from __future__ import annotations +from typing import TYPE_CHECKING +import ba,random +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.powerupbox import PowerupBoxFactory +from bastd.gameutils import SharedObjects +from bastd.actor.flag import Flag +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + + +class PuckDiedMessage: + """Inform something that a puck has died.""" + + def __init__(self, puck: Puck): + self.puck = puck + +#goalpost +class FlagKale(ba.Actor): + def __init__(self,position=(0,2.5,0),color=(1,1,1)): + super().__init__() + activity = self.getactivity() + shared = SharedObjects.get() + self.node = ba.newnode('flag', + attrs={'position':(position[0],position[1]+0.75,position[2]), + 'color_texture':activity._flagKaleTex, + 'color':color, + 'materials':[shared.object_material,activity._kaleMaterial], + }, + delegate=self) + + def handleMessage(self,m): + if isinstance(m,ba.DieMessage): + if self.node.exists(): + self.node.delete() + elif isinstance(m,ba.OutOfBoundsMessage): + self.handlemessage(ba.DieMessage()) + else: + super().handlemessage(msg) + + +class Puck(ba.Actor): + def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + + # Spawn just above the provided point. + self._spawn_pos = (position[0], position[1] + 1.0, position[2]) + self.last_players_to_touch: Dict[int, Player] = {} + self.scored = False + assert activity is not None + assert isinstance(activity, BBGame) + pmats = [shared.object_material, activity.puck_material] + self.node = ba.newnode('prop', + delegate=self, + attrs={ + 'model': activity._ballModel, + 'color_texture': activity._ballTex, + 'body': 'sphere', + 'reflection': 'soft', + 'reflection_scale': [0.2], + 'shadow_size': 0.8, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats, + 'body_scale': 4, + 'model_scale': 1, + 'density': 0.02}) + ba.animate(self.node, 'model_scale', {0: 0, 0.2: 1.3, 0.26: 1}) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + assert self.node + self.node.delete() + activity = self._activity() + if activity and not msg.immediate: + activity.handlemessage(PuckDiedMessage(self)) + + # If we go out of bounds, move back to where we started. + elif isinstance(msg, ba.OutOfBoundsMessage): + assert self.node + self.node.position = self._spawn_pos + + elif isinstance(msg, ba.HitMessage): + assert self.node + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, + 1.0 * msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + + # If this hit came from a player, log them as the last to touch us. + s_player = msg.get_source_player(Player) + if s_player is not None: + activity = self._activity() + if activity: + if s_player in activity.players: + self.last_players_to_touch[s_player.team.id] = s_player + else: + super().handlemessage(msg) + +#for night mode: using a actor with large shadow and little model scale. Better then tint i think, players and objects more visible +class NightMod(ba.Actor): + def __init__(self,position=(0,0,0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + # spawn just above the provided point + self._spawnPos = (position[0],position[1],position[2]) + self.node = ba.newnode("prop", + attrs={'model': activity._nightModel, + 'color_texture': activity._nightTex, + 'body':'sphere', + 'reflection':'soft', + 'body_scale': 0.1, + 'model_scale':0.001, + 'density':0.010, + 'reflection_scale':[0.23], + 'shadow_size': 999999.0, + 'is_area_of_interest':True, + 'position':self._spawnPos, + 'materials': [activity._nightMaterial] + }, + delegate=self) + + def handlemssage(self,m): + super().handlemessage(m) + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class BBGame(ba.TeamGameActivity[Player, Team]): + name = 'Big Ball' + description = 'Score some goals.\nFlags are goalposts.\nScored team players get boxing gloves,\nNon-scored team players getting shield (if Grant Powers on Score).\nYou can also set Night Mode!' + available_settings = [ + ba.IntSetting( + 'Score to Win', + min_value=1, + default=1, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', True), + ba.BoolSetting('Night Mode', False), + ba.BoolSetting('Grant Powers on Score', False) + ] + default_music = ba.MusicType.HOCKEY + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ['Football Stadium'] + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._cheer_sound = ba.getsound('cheer') + self._chant_sound = ba.getsound('crowdChant') + self._foghorn_sound = ba.getsound('foghorn') + self._swipsound = ba.getsound('swip') + self._whistle_sound = ba.getsound('refWhistle') + self._ballModel = ba.getmodel("shield") + self._ballTex = ba.gettexture("eggTex1") + self._ballSound = ba.getsound("impactMedium2") + self._flagKaleTex = ba.gettexture("star") + self._kaleSound = ba.getsound("metalHit") + self._nightModel = ba.getmodel("shield") + self._nightTex = ba.gettexture("black") + self._kaleMaterial = ba.Material() + #add friction to flags for standing our position (as far as) + self._kaleMaterial.add_actions(conditions=("they_have_material",shared.footing_material), + actions=( ("modify_part_collision","friction",9999.5))) + self._kaleMaterial.add_actions(conditions=( ("we_are_younger_than",1),'and', + ("they_have_material",shared.object_material)), + actions=( ("modify_part_collision","collide",False))) + self._kaleMaterial.add_actions(conditions=("they_have_material",shared.pickup_material), + actions=( ("modify_part_collision","collide",False))) + self._kaleMaterial.add_actions( + conditions=('they_have_material',shared.object_material), + actions=(('impact_sound',self._kaleSound,2,5))) + #we dont wanna hit the night so + self._nightMaterial = ba.Material() + self._nightMaterial.add_actions(conditions=(('they_have_material',shared.pickup_material),'or', + ('they_have_material',shared.attack_material)), + actions=(('modify_part_collision','collide',False))) + # we also dont want anything moving it + self._nightMaterial.add_actions( + conditions=(('they_have_material',shared.object_material),'or', + ('they_dont_have_material',shared.footing_material)), + actions=(('modify_part_collision','collide',False), + ('modify_part_collision','physical',False))) + self.puck_material = ba.Material() + self.puck_material.add_actions(actions=(('modify_part_collision', + 'friction', 0.5))) + self.puck_material.add_actions(conditions=('they_have_material', + shared.pickup_material), + actions=('modify_part_collision', + 'collide', False)) + self.puck_material.add_actions( + conditions=( + ('we_are_younger_than', 100), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + self.puck_material.add_actions(conditions=('they_have_material', + shared.footing_material), + actions=('impact_sound', + self._ballSound, 0.2, 5)) + + # Keep track of which player last touched the puck + self.puck_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_connect', + self._handle_puck_player_collide), )) + + # We want the puck to kill powerups; not get stopped by them + self.puck_material.add_actions( + conditions=('they_have_material', + PowerupBoxFactory.get().powerup_material), + actions=(('modify_part_collision', 'physical', False), + ('message', 'their_node', 'at_connect', ba.DieMessage()))) + self._score_region_material = ba.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', self.puck_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_score))) + self._puck_spawn_pos: Optional[Sequence[float]] = None + self._score_regions: Optional[List[ba.NodeActor]] = None + self._puck: Optional[Puck] = None + self._score_to_win = int(settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + self._nm = bool(settings['Night Mode']) + self._grant_power = bool(settings['Grant Powers on Score']) + self._epic_mode = bool(settings['Epic Mode']) + # Base class overrides. + self.slow_motion = self._epic_mode + + def get_instance_description(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'Score a goal.' + return 'Score ${ARG1} goals.', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'score a goal' + return 'score ${ARG1} goals', self._score_to_win + + def on_begin(self) -> None: + super().on_begin() + + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops(enable_tnt = False) + self._puck_spawn_pos = self.map.get_flag_position(None) + self._flagKalesSpawn() + self._spawn_puck() + #for night mode we need night actor. And same goodies for nigh mode + if self._nm: self._nightSpawny(),self._flagKaleFlash() + + # Set up the two score regions. + defs = self.map.defs + self._score_regions = [] + self._score_regions.append( + ba.NodeActor( + ba.newnode('region', + attrs={ + 'position': (13.75, 0.85744967453, 0.1095578275), + 'scale': (1.05,1.1,3.8), + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._score_regions.append( + ba.NodeActor( + ba.newnode('region', + attrs={ + 'position': (-13.55, 0.85744967453, 0.1095578275), + 'scale': (1.05,1.1,3.8), + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._update_scoreboard() + ba.playsound(self._chant_sound) + + def _nightSpawny(self): + self.MythBrk = NightMod(position=(0, 0.05744967453, 0)) + + #spawn some goodies on nightmode for pretty visuals + def _flagKaleFlash(self): + #flags positions + kale1 = (-12.45, 0.05744967453, -2.075) + kale2 = (-12.45, 0.05744967453, 2.075) + kale3 = (12.66, 0.03986567039, 2.075) + kale4 = (12.66, 0.03986567039, -2.075) + + flash = ba.newnode("light", + attrs={'position':kale1, + 'radius':0.15, + 'color':(1.0,1.0,0.7)}) + + flash = ba.newnode("light", + attrs={'position':kale2, + 'radius':0.15, + 'color':(1.0,1.0,0.7)}) + + flash = ba.newnode("light", + attrs={'position':kale3, + 'radius':0.15, + 'color':(0.7,1.0,1.0)}) + + flash = ba.newnode("light", + attrs={'position':kale4, + 'radius':0.15, + 'color':(0.7,1.0,1.0)}) + #flags positions + def _flagKalesSpawn(self): + for team in self.teams: + if team.id == 0: + _colorTeam0 = team.color + if team.id == 1: + _colorTeam1 = team.color + + self._MythB = FlagKale(position=(-12.45, 0.05744967453, -2.075),color=_colorTeam0) + self._MythB2 =FlagKale(position=(-12.45, 0.05744967453, 2.075),color=_colorTeam0) + self._MythB3 =FlagKale(position=(12.66, 0.03986567039, 2.075),color=_colorTeam1) + self._MythB4 =FlagKale(position=(12.66, 0.03986567039, -2.075),color=_colorTeam1) + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def _handle_puck_player_collide(self) -> None: + collision = ba.getcollision() + try: + puck = collision.sourcenode.getdelegate(Puck, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except ba.NotFoundError: + return + + puck.last_players_to_touch[player.team.id] = player + + def _kill_puck(self) -> None: + self._puck = None + + def _handle_score(self) -> None: + """A point has been scored.""" + + assert self._puck is not None + assert self._score_regions is not None + + # Our puck might stick around for a second or two + # we don't want it to be able to score again. + if self._puck.scored: + return + + region = ba.getcollision().sourcenode + index = 0 + for index in range(len(self._score_regions)): + if region == self._score_regions[index].node: + break + + for team in self.teams: + if team.id == index: + scoring_team = team + team.score += 1 + + # tell scored team players to celebrate and give them to boxing gloves + if self._grant_power: + for player in team.players: + try: player.actor.node.handlemessage(ba.PowerupMessage('punch')) + except: pass + + # Tell all players to celebrate. + for player in team.players: + if player.actor: + player.actor.handlemessage(ba.CelebrateMessage(2.0)) + + # If we've got the player from the scoring team that last + # touched us, give them points. + if (scoring_team.id in self._puck.last_players_to_touch + and self._puck.last_players_to_touch[scoring_team.id]): + self.stats.player_scored( + self._puck.last_players_to_touch[scoring_team.id], + 100, + big_message=True) + + # End game if we won. + if team.score >= self._score_to_win: + self.end_game() + else: + if self._grant_power: + for player in team.players: + try: player.actor.node.handlemessage(ba.PowerupMessage('shield')) + except: pass + + ba.playsound(self._foghorn_sound) + ba.playsound(self._cheer_sound) + + self._puck.scored = True + + # Kill the puck (it'll respawn itself shortly). + ba.timer(1.0, self._kill_puck) + + light = ba.newnode('light', + attrs={ + 'position': ba.getcollision().position, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + ba.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) + ba.timer(1.0, light.delete) + + ba.cameraflash(duration=10.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def _update_scoreboard(self) -> None: + winscore = self._score_to_win + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, winscore) + + def handlemessage(self, msg: Any) -> Any: + + # Respawn dead players if they're still in the game. + if isinstance(msg, ba.PlayerDiedMessage): + # Augment standard behavior... + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + + # Respawn dead pucks. + elif isinstance(msg, PuckDiedMessage): + if not self.has_ended(): + ba.timer(3.0, self._spawn_puck) + else: + super().handlemessage(msg) + + def _flash_puck_spawn(self) -> None: + light = ba.newnode('light', + attrs={ + 'position': self._puck_spawn_pos, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + ba.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) + ba.timer(1.0, light.delete) + + def _spawn_puck(self) -> None: + ba.playsound(self._swipsound) + ba.playsound(self._whistle_sound) + self._flash_puck_spawn() + assert self._puck_spawn_pos is not None + self._puck = Puck(position=self._puck_spawn_pos) + self._puck.light = ba.newnode('light', + owner=self._puck.node, + attrs={'intensity':0.3, + 'height_attenuated':False, + 'radius':0.2, + 'color': (0.9,0.2,0.9)}) + self._puck.node.connectattr('position',self._puck.light,'position') diff --git a/dist/ba_root/mods/games/Bounty.py b/dist/ba_root/mods/games/Bounty.py new file mode 100644 index 0000000..a052ace --- /dev/null +++ b/dist/ba_root/mods/games/Bounty.py @@ -0,0 +1,227 @@ +# Released under the MIT License. See LICENSE for details. +# +"""DeathMatch game and support classes.""" + +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard + +if TYPE_CHECKING: + from typing import Any, Union, Sequence, Optional + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class BountyGame(ba.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Bounty' + description = 'Score Maximum Stars To Win' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: type[ba.Session]) -> list[ba.Setting]: + settings = [ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('30 Seconds', 30), + ('1 Minute', 60), + ('1½ Minute', 90), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600)], + default=120, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', default=False), + ] + + # In teams mode, a suicide gives a point to the other team, but in + # free-for-all it subtracts from your own score. By default we clamp + # this at zero to benefit new players, but pro players might like to + # be able to go negative. (to avoid a strategy of just + # suiciding until you get a good drop) + if issubclass(sessiontype, ba.FreeForAllSession): + settings.append( + ba.BoolSetting('Allow Negative Scores', default=False)) + + return settings + + @classmethod + def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + return ba.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_to_win: Optional[int] = None + self._dingsound = ba.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._time_limit = float(settings['Time Limit']) + self._allow_negative_scores = bool( + settings.get('Allow Negative Scores', False)) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC if self._epic_mode else + ba.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Collect Stars of your enemies.' + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'Collect Stars of your enemies.' + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + + # Base kills needed to win on the size of the largest team. + self._update_scoreboard() + + def spawn_player(self, player: Player) -> ba.Actor: + + spaz = self.spawn_player_spaz(player) + + assert spaz.node + mathnode = ba.newnode('math', + owner=spaz.node, + attrs={ + 'input1': (0, 1.4, 0), + 'operation': 'add' + }) + spaz.node.connectattr('torso_position', mathnode, 'input2') + players_star = ba.newnode('text', + owner=spaz.node, + attrs={ + 'text': '*', + 'in_world': True, + 'color': (1, 1, 0.4), + 'scale': 0.02, + 'h_align': 'center' + }) + player.tag = players_star + mathnode.connectattr('output', players_star, 'position') + return spaz + + def _update_stars(self, player: Player) -> None: + oldstar = player.tag.text.count("*") + if player.is_alive(): + if oldstar < 3: + player.tag.text = "*" * (oldstar + 1) + elif oldstar == 3: + player.tag.text = "**\n**" + elif oldstar == 4: + player.tag.text = "**\n***" + else: + player.tag.text = player.tag.text + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, ba.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + player = msg.getplayer(Player) + self.respawn_player(player) + + killer = msg.getkillerplayer(Player) + if killer is None: + return None + + try: + star = player.tag.text.count("*") + except: + star = 0 + + # Handle team-kills. + if killer.team is player.team: + + # In free-for-all, killing yourself loses you a point. + if isinstance(self.session, ba.FreeForAllSession): + new_score = player.team.score - 1 + if not self._allow_negative_scores: + new_score = max(0, new_score) + player.team.score = new_score + + # In teams-mode it gives a point to the other team. + else: + ba.playsound(self._dingsound) + for team in self.teams: + if team is not killer.team: + team.score += star + if not killer == player: + self._update_stars(killer) + + # Killing someone on another team nets a kill. + else: + killer.team.score += star + self._update_stars(killer) + ba.playsound(self._dingsound) + + # In FFA show scores since its hard to find on the scoreboard. + if isinstance(killer.actor, PlayerSpaz) and killer.actor: + killer.actor.set_score_text("+ 1", + color=killer.team.color, + flash=True) + + self._update_scoreboard() + + # If someone has won, set a timer to end shortly. + # (allows the dust to clear and draws to occur if deaths are + # close enough) + + else: + return super().handlemessage(msg) + return None + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + None) + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) diff --git a/dist/ba_root/mods/games/DarkFields.py b/dist/ba_root/mods/games/DarkFields.py new file mode 100644 index 0000000..64fc408 --- /dev/null +++ b/dist/ba_root/mods/games/DarkFields.py @@ -0,0 +1,237 @@ +#Made by Froshlee14 +#Ported by: Freaku / @[Just] Freak#4999 + + + + + + +# ba_meta require api 6 +from __future__ import annotations +from typing import TYPE_CHECKING +import ba,random +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.bomb import Bomb +from bastd.gameutils import SharedObjects +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class DFGame(ba.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Dark Fields' + description = 'Get to the other side.' + + # Print messages when players die since it matters here. + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: + settings = [ + ba.IntSetting( + 'Score to Win', + min_value=1, + default=3, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', default=False) + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ['Football Stadium'] + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._dingsound = ba.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self._score_to_win = int( + settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC if self._epic_mode else ba.MusicType.TO_THE_DEATH) + self._scoreRegionMaterial = ba.Material() + self._scoreRegionMaterial.add_actions( + conditions=("they_have_material",shared.player_material), + actions=(("modify_part_collision","collide",True), + ("modify_part_collision","physical",False), + ("call","at_connect", self._onPlayerScores))) + self.first_time = True + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Get to the other side ${ARG1} times', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'Get to the other side ${ARG1} times', self._score_to_win + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + ba.getactivity().globalsnode.tint = (0.5, 0.5, 0.5) + a = ba.newnode('locator',attrs={'shape':'box','position':(12,0,.1087926362),'color':(5,5,5),'opacity':1,'draw_beauty':True,'additive':False,'size':[2.0,0.1,11.8]}) + b = ba.newnode('locator',attrs={'shape':'box','position':(-12,0,.1087926362),'color':(5,5,5),'opacity':1,'draw_beauty':True,'additive':False,'size':[2.0,0.1,11.8]}) + self.isUpdatingMines = False + self._scoreSound = ba.getsound('dingSmall') + self.setup_standard_time_limit(self._time_limit) + + self._update_scoreboard() + for p in self.players: + if p.actor is not None: + try:p.actor.disconnect_controls_from_player() + except Exception as e: print ('Can\'t connect to player',e) + + self._scoreRegions = [] + defs = self.map.defs + self._scoreRegions.append(ba.NodeActor(ba.newnode('region', + attrs={'position':defs.boxes['goal1'][0:3], + 'scale':defs.boxes['goal1'][6:9], + 'type': 'box', + 'materials':[self._scoreRegionMaterial]}))) + self.mines = [] + self.spawnMines() + ba.timer(2.5,self.start) + + def start(self): + ba.timer(random.randrange(3,7),self.doRandomLighting) + ba.animate_array(ba.getactivity().globalsnode,'tint',3,{0:(0.5,0.5,0.5),2:(0.2,0.2,0.2)}) + + def doRandomLighting(self): + ba.timer(random.randrange(3,7),self.doRandomLighting) + if self.isUpdatingMines: return + ba.animate_array(ba.getactivity().globalsnode,'tint',3,{0:(0.5,0.5,0.5),0.8:(0.2,0.2,0.2)}) + + def spawnMines(self): + delay = 0 + xs = [10,8,6,4,2,0,-2,-4,-6,-8,-10] + for x in xs: + for i in range(3): + pos = (x,1,random.randrange(-5,6)) + ba.timer(delay,ba.Call(self.doMine,pos)) + delay += 0.075 + ba.timer(2.48,self.stopUpdateMines) + + def stopUpdateMines(self): + self.isUpdatingMines = False + self.first_time = False + + def updateMines(self): + if self.isUpdatingMines: return + self.isUpdatingMines = True + for m in self.mines: + m.node.delete() + self.mines = [] + self.spawnMines() + + def doMine(self,pos): + b = Bomb(position=pos,bomb_type='land_mine').autoretain() + b.arm() + self.mines.append(b) + + # overriding the default character spawning.. + def spawn_player(self, player: Player): + if self.first_time: ba.timer(2.5, ba.Call(self.spawn_with_delay, player)) + else: self.spawn_with_delay(player) + def spawn_with_delay(self, player: Player): + spaz = self.spawn_player_spaz(player) + position = (-12.4,1,random.randrange(-5,5)) + spaz.connect_controls_to_player(enable_punch=False,enable_bomb=False) + spaz.handlemessage(ba.StandMessage(position, random.uniform(0,360))) + return spaz + + def _onPlayerScores(self): + try: player = ba.getcollision().opposingnode.getdelegate(PlayerSpaz, True).getplayer(Player, True) + except Exception: player = None + if player.exists() and player.is_alive(): + for team in self.teams: + if team is player.team: + team.score += 1 + ba.playsound(self._scoreSound) + ba.animate_array(ba.getactivity().globalsnode,'tint',3,{0:(0.5,0.5,0.5),2.8:(0.2,0.2,0.2)}) + self._update_scoreboard() + player.actor.handlemessage(ba.DieMessage()) + self.updateMines() + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, ba.PlayerDiedMessage): + + # Augment standard behavior. + super().handlemessage(msg) + + player = msg.getplayer(Player) + self.respawn_player(player) + + self._update_scoreboard() + + # If someone has won, set a timer to end shortly. + # (allows the dust to clear and draws to occur if deaths are + # close enough) + assert self._score_to_win is not None + if any(team.score >= self._score_to_win for team in self.teams): + ba.timer(0.5, self.end_game) + + else: + return super().handlemessage(msg) + return None + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) diff --git a/dist/ba_root/mods/games/EggGame.py b/dist/ba_root/mods/games/EggGame.py new file mode 100644 index 0000000..a23faad --- /dev/null +++ b/dist/ba_root/mods/games/EggGame.py @@ -0,0 +1,487 @@ +# Released under the MIT License. See LICENSE for details. + +"""Egg game and support classes.""" +# The Egg Game - throw egg as far as you can +# created in BCS (Bombsquad Consultancy Service) - opensource bombsquad mods for all +# discord.gg/ucyaesh join now and give your contribution +# The Egg game by mr.smoothy +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.powerupbox import PowerupBoxFactory +from bastd.gameutils import SharedObjects +from bastd.actor.flag import Flag +import math +import random +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + + +class PuckDiedMessage: + """Inform something that a puck has died.""" + + def __init__(self, puck: Puck): + self.puck = puck + + +class Puck(ba.Actor): + """A lovely giant hockey puck.""" + + def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + + # Spawn just above the provided point. + self._spawn_pos = (position[0], position[1] + 1.0, position[2]) + self.last_players_to_touch =None + self.scored = False + self.egg_model = ba.getmodel('egg') + self.egg_tex_1 = ba.gettexture('eggTex1') + self.egg_tex_2 = ba.gettexture('eggTex2') + self.egg_tex_3 = ba.gettexture('eggTex3') + self.eggtx=[self.egg_tex_1,self.egg_tex_2,self.egg_tex_3] + regg=random.randrange(0,3) + assert activity is not None + assert isinstance(activity, EggGame) + pmats = [shared.object_material, activity.puck_material] + self.node = ba.newnode('prop', + delegate=self, + attrs={ + 'model': self.egg_model, + 'color_texture': self.eggtx[regg], + 'body': 'capsule', + 'reflection': 'soft', + 'reflection_scale': [0.2], + 'shadow_size': 0.5, + 'body_scale':0.7, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats + }) + ba.animate(self.node, 'model_scale', {0: 0, 0.2: 0.7, 0.26: 0.6}) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + assert self.node + self.node.delete() + activity = self._activity() + if activity and not msg.immediate: + activity.handlemessage(PuckDiedMessage(self)) + + # If we go out of bounds, move back to where we started. + elif isinstance(msg, ba.OutOfBoundsMessage): + assert self.node + self.node.position = self._spawn_pos + + elif isinstance(msg, ba.HitMessage): + assert self.node + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, + 1.0 * msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + + # If this hit came from a player, log them as the last to touch us. + s_player = msg.get_source_player(Player) + if s_player is not None: + activity = self._activity() + if activity: + if s_player in activity.players: + self.last_players_to_touch = s_player + else: + super().handlemessage(msg) + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class EggGame(ba.TeamGameActivity[Player, Team]): + """Egg game.""" + + name = 'Epic Egg Game' + description = 'Score some goals.' + available_settings = [ + ba.IntSetting( + 'Score to Win', + min_value=1, + default=1, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('40 Seconds', 40), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.1), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ] + default_music = ba.MusicType.HOCKEY + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('football') + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self.slow_motion = True + self._scoreboard = Scoreboard() + self._cheer_sound = ba.getsound('cheer') + self._chant_sound = ba.getsound('crowdChant') + self._foghorn_sound = ba.getsound('foghorn') + self._swipsound = ba.getsound('swip') + self._whistle_sound = ba.getsound('refWhistle') + self.puck_model = ba.getmodel('bomb') + self.puck_tex = ba.gettexture('landMine') + self.puck_scored_tex = ba.gettexture('landMineLit') + self._puck_sound = ba.getsound('metalHit') + self.puck_material = ba.Material() + self._fake_wall_material=ba.Material() + self.HIGHEST=0 + self._fake_wall_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self.puck_material.add_actions(actions=(('modify_part_collision', + 'friction', 0.5))) + self.puck_material.add_actions(conditions=('they_have_material', + shared.pickup_material), + actions=('modify_part_collision', + 'collide', True)) + self.puck_material.add_actions( + conditions=( + ('we_are_younger_than', 100), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + # self.puck_material.add_actions(conditions=('they_have_material', + # shared.footing_material), + # actions=('impact_sound', + # self._puck_sound, 0.2, 5)) + + # Keep track of which player last touched the puck + self.puck_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_connect', + self._handle_puck_player_collide), )) + + # We want the puck to kill powerups; not get stopped by them + self.puck_material.add_actions( + conditions=('they_have_material', + PowerupBoxFactory.get().powerup_material), + actions=(('modify_part_collision', 'physical', False), + ('message', 'their_node', 'at_connect', ba.DieMessage()))) + # self.puck_material.add_actions( + # conditions=('they_have_material',shared.footing_material) + # actions=(('modify_part_collision', 'collide', + # True), ('modify_part_collision', 'physical', True), + # ('call', 'at_connect', self._handle_egg_collision)) + # ) + self._score_region_material = ba.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', self.puck_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_score))) + self.main_ground_material= ba.Material() + + self.main_ground_material.add_actions( + conditions=('they_have_material', self.puck_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_egg_collision))) + + self._puck_spawn_pos: Optional[Sequence[float]] = None + self._score_regions: Optional[List[ba.NodeActor]] = None + self._puck: Optional[Puck] = None + self._pucks=[] + self._score_to_win = int(settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + + def get_instance_description(self) -> Union[str, Sequence]: + return "Throw Egg as far u can" + + def get_instance_description_short(self) -> Union[str, Sequence]: + return "Throw Egg as far u can" + + def on_begin(self) -> None: + super().on_begin() + if self._time_limit==0.0: + self._time_limit=60 + self.setup_standard_time_limit(self._time_limit) + # self.setup_standard_powerup_drops() + self._puck_spawn_pos = self.map.get_flag_position(None) + self._spawn_puck() + self._spawn_puck() + self._spawn_puck() + self._spawn_puck() + self._spawn_puck() + + # Set up the two score regions. + defs = self.map.defs + self._score_regions = [] + pos=(11.88630542755127, 0.3009839951992035, 1.33331298828125) + # mat=ba.Material() + # mat.add_actions( + + # actions=( ('modify_part_collision','physical',True), + # ('modify_part_collision','collide',True)) + # ) + # self._score_regions.append( + # ba.NodeActor( + # ba.newnode('region', + # attrs={ + # 'position': pos, + # 'scale': (2,3,5), + # 'type': 'box', + # 'materials': [self._score_region_material] + # }))) + # pos=(-11.88630542755127, 0.3009839951992035, 1.33331298828125) + # self._score_regions.append( + # ba.NodeActor( + # ba.newnode('region', + # attrs={ + # 'position': pos, + # 'scale': (2,3,5), + # 'type': 'box', + # 'materials': [self._score_region_material] + # }))) + self._score_regions.append( + ba.NodeActor( + ba.newnode('region', + attrs={ + 'position': (-9.21,defs.boxes['goal2'][0:3][1],defs.boxes['goal2'][0:3][2]), + 'scale': defs.boxes['goal2'][6:9], + 'type': 'box', + 'materials': (self._fake_wall_material, ) + }))) + pos=(0,0.1,-5) + self.main_ground=ba.newnode('region',attrs={'position': pos,'scale': (25,0.001,22),'type': 'box','materials': [self.main_ground_material]}) + self._update_scoreboard() + ba.playsound(self._chant_sound) + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def _handle_puck_player_collide(self) -> None: + collision = ba.getcollision() + try: + puck = collision.sourcenode.getdelegate(Puck, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except ba.NotFoundError: + return + + puck.last_players_to_touch = player + + def _kill_puck(self) -> None: + self._puck = None + def _handle_egg_collision(self) -> None: + + no=ba.getcollision().opposingnode + pos=no.position + egg=no.getdelegate(Puck) + source_player=egg.last_players_to_touch + if source_player==None or pos[0]< -8 or not source_player.node.exists() : + return + + + try: + col=source_player.team.color + self.flagg=Flag(pos,touchable=False,color=col).autoretain() + self.flagg.is_area_of_interest=True + player_pos=source_player.node.position + + distance = math.sqrt( pow(player_pos[0]-pos[0],2) + pow(player_pos[2]-pos[2],2)) + + + dis_mark=ba.newnode('text', + + attrs={ + 'text':str(round(distance,2))+"m", + 'in_world':True, + 'scale':0.02, + 'h_align':'center', + 'position':(pos[0],1.6,pos[2]), + 'color':col + }) + ba.animate(dis_mark,'scale',{ + 0.0:0, 0.5:0.01 + }) + if distance > self.HIGHEST: + self.HIGHEST=distance + self.stats.player_scored( + source_player, + 10, + big_message=False) + + no.delete() + ba.timer(2,self._spawn_puck) + source_player.team.score=int(distance) + + except(): + pass + def spawn_player(self, player: Player) -> ba.Actor: + + + zoo=random.randrange(-4,5) + pos=(-11.204887390136719, 0.2998693287372589, zoo) + spaz = self.spawn_player_spaz( + player, position=pos, angle=90 ) + assert spaz.node + + # Prevent controlling of characters before the start of the race. + + return spaz + def _handle_score(self) -> None: + """A point has been scored.""" + + assert self._puck is not None + assert self._score_regions is not None + + # Our puck might stick around for a second or two + # we don't want it to be able to score again. + if self._puck.scored: + return + + region = ba.getcollision().sourcenode + index = 0 + for index in range(len(self._score_regions)): + if region == self._score_regions[index].node: + break + + for team in self.teams: + if team.id == index: + scoring_team = team + team.score += 1 + + # Tell all players to celebrate. + for player in team.players: + if player.actor: + player.actor.handlemessage(ba.CelebrateMessage(2.0)) + + # If we've got the player from the scoring team that last + # touched us, give them points. + if (scoring_team.id in self._puck.last_players_to_touch + and self._puck.last_players_to_touch[scoring_team.id]): + self.stats.player_scored( + self._puck.last_players_to_touch[scoring_team.id], + 20, + big_message=True) + + # End game if we won. + if team.score >= self._score_to_win: + self.end_game() + + ba.playsound(self._foghorn_sound) + ba.playsound(self._cheer_sound) + + # self._puck.scored = True + + # Change puck texture to something cool + # self._puck.node.color_texture = self.puck_scored_tex + # Kill the puck (it'll respawn itself shortly). + ba.timer(1.0, self._kill_puck) + + # light = ba.newnode('light', + # attrs={ + # 'position': ba.getcollision().position, + # 'height_attenuated': False, + # 'color': (1, 0, 0) + # }) + # ba.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) + # ba.timer(1.0, light.delete) + + ba.cameraflash(duration=10.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def _update_scoreboard(self) -> None: + winscore = self._score_to_win + # for team in self.teams: + # self._scoreboard.set_team_value(team, team.score, winscore) + + def handlemessage(self, msg: Any) -> Any: + + # Respawn dead players if they're still in the game. + if isinstance(msg, ba.PlayerDiedMessage): + # Augment standard behavior... + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + + # Respawn dead pucks. + elif isinstance(msg, PuckDiedMessage): + if not self.has_ended(): + ba.timer(3.0, self._spawn_puck) + else: + super().handlemessage(msg) + + def _flash_puck_spawn(self) -> None: + # light = ba.newnode('light', + # attrs={ + # 'position': self._puck_spawn_pos, + # 'height_attenuated': False, + # 'color': (1, 0, 0) + # }) + # ba.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) + # ba.timer(1.0, light.delete) + pass + def _spawn_puck(self) -> None: + # ba.playsound(self._swipsound) + # ba.playsound(self._whistle_sound) + self._flash_puck_spawn() + assert self._puck_spawn_pos is not None + zoo=random.randrange(-5,6) + pos=(-11.204887390136719, 0.2998693287372589, zoo) + self._pucks.append (Puck(position=pos)) diff --git a/dist/ba_root/mods/games/GetTheTarget.py b/dist/ba_root/mods/games/GetTheTarget.py new file mode 100644 index 0000000..c5d61a4 --- /dev/null +++ b/dist/ba_root/mods/games/GetTheTarget.py @@ -0,0 +1,293 @@ +# Made by Froshlee14 +# ba_meta require api 6 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba, _ba, random +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor import spaz + +if TYPE_CHECKING: + from typing import Any, Union, Type, List, Dict, Tuple, Sequence, Optional + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + +# ba_meta export game +class GetTheTargetGame(ba.TeamGameActivity[Player, Team]): + name = 'Get the Target' + description = 'Kill target to get points (dont kill if teammate). If you\'re \nthe Target, survive to get points, \nplayer/team that reached the\n required points, wins.' + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: + settings = [ + ba.IntSetting("Points to Win Per Player", min_value=1, default=5, increment=1), + ba.IntSetting("Time to Kill", min_value=5, max_value=30, default=10, increment=1), + ba.IntChoiceSetting("Time Limit", choices=[('None', 0),('1 Minute', 60),('2 Minutes', 120),('5 Minutes', 300),('10 Minutes', 600),('20 Minutes', 1200)],default=0), + ba.FloatChoiceSetting("Respawn Times",choices=[('Shorter', 0.25),('Short', 0.5),('Normal', 1.0),('Long', 2.0),('Longer', 4.0)],default=1.0), + ba.IntChoiceSetting("Target Indicator", choices=[('None', 0),('Light', 1),('Text', 2)],default=0), + ba.BoolSetting("Epic Mode", default=False) + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('melee') + + def __init__(self, settings: dict): + super().__init__(settings) + self.settings = settings + if self.settings['Epic Mode']: self.slow_motion = True + self._chosed_player = self.chosen_player = None + self._score_to_win: Optional[int] = None + self._dingsound = ba.getsound('dingSmall') + self._chosing_sound = ba.getsound('scoreIncrease') + self._chosed_sound = ba.getsound('cashRegister2') + self._error_sound = ba.getsound('error') + self._tick_sound = ba.getsound('tick') + self._time_remaining = int(self.settings['Time to Kill']) + self._time_limit = float(settings['Time Limit']) + self.ytpe = int(settings["Target Indicator"]) + self._scoreboard = Scoreboard() + + self.default_music = (ba.MusicType.EPIC if self.slow_motion else + ba.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Kill enemy targets or Survive ${ARG1} times.', self._score_to_win + + def get_instance_scoreboard_display_string(self) -> str: + return 'Kill target/Survive ' + str(self._score_to_win) + ' times.' + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): self._update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self._score_to_win = (int(self.settings["Points to Win Per Player"]) * + max(1, max(len(t.players) for t in self.teams))) + self._update_scoreboard() + ba.timer(3000 * 0.001, self.star_chosing_player) + + self._chose_text = ba.newnode('text', + attrs={ + 'text':' ', + 'v_attach':'bottom', + 'h_attach':'center', + 'h_align':'center', + 'v_align':'center', + 'maxwidth':150, + 'shadow':1.0, + 'flatness':1.0, + 'color':(1,1,1), + 'scale':1, + 'position':(0,155)}) + + def on_player_leave(self, player: PlayerType) -> None: + super().on_player_leave(player) + if len(self.players) in [0,1]: self.end_game() + if player == self._chosed_player: self.star_chosing_player() + + def print_random_icon(self) -> None: + get_alive = [] + for spas in self.players: + if spas.is_alive(): + get_alive.append(spas) + + if not len(get_alive) == 0: + self._chose_text.text = 'Chosing Player...' + self.loops += 1 + player = random.choice(get_alive) + icon = player.get_icon() + outline_tex = ba.gettexture('characterIconMask') + texture = icon['texture'] + self._image = ba.NodeActor(ba.newnode('image', + attrs={'texture':texture, + 'tint_texture':icon['tint_texture'], + 'tint_color':icon['tint_color'], + 'tint2_color':icon['tint2_color'], + 'mask_texture':outline_tex, + 'position':(0,80), + 'scale':(100,100), 'opacity':1.0, + 'absolute_scale':True,'attach':'bottomCenter'})) + self._name = ba.NodeActor(ba.newnode('text', + attrs={'v_attach':'bottom', 'h_attach':'center', + 'text':ba.Lstr(value=player.getname()), + 'maxwidth':100, 'h_align':'center', + 'v_align':'center', 'shadow':1.0, + 'flatness':1.0, 'color':ba.safecolor(icon['tint_color']), + 'scale':1,'position':(0,20)})) + if self.loops >= self.loop_max: + self.chosen_player = player + self.stopn_chose_player() + else: + self._chose_text.text = 'Waiting for players...' + texture = ba.gettexture("powerupCurse") + self._image = ba.NodeActor(ba.newnode('image', + attrs={'texture':texture, + 'position':(0,80), + 'scale':(100,100), 'opacity':1.0, + 'absolute_scale':True,'attach':'bottomCenter'})) + self._name = ba.NodeActor(ba.newnode('text', + attrs={'v_attach':'bottom', 'h_attach':'center', + 'text':" ", + 'maxwidth':100, 'h_align':'center', + 'v_align':'center', 'shadow':1.0, + 'flatness':1.0, + 'color':(1,1,1), + 'scale':1,'position':(0,20)})) + return + + + def star_chosing_player(self) -> None: + if len(self.players) in [0,1]: self.end_game() + self._sound = ba.NodeActor(ba.newnode('sound',attrs={'sound':self._chosing_sound,'volume':1.0})) + self.stop_timer() + self.loops = 0 + self.loop_max = random.randrange(15,30) + self._chosed_player = None + self._logo_effect = ba.Timer(80 * 0.001,ba.WeakCall(self.print_random_icon),repeat=True) + self._chose_text.color = (1,1,1) + + def stopn_chose_player(self) -> None: + self._sound = None + self._logo_effect = None + ba.playsound(self._chosed_sound) + player = self.chosen_player + self._chosed_player = player + self._chose_text.text = 'Kill the Enemy!' + self._chose_text.color = (1,1,0) + + palyer = player.actor + if self.ytpe == 1: + palyer.r = ba.newnode('light', + attrs={ + 'radius':0.2, + 'intensity': 5.0, + 'color': palyer.node.color + }) + palyer.node.connectattr('position', palyer.r, 'position') + elif self.ytpe == 2: + dummy = ba.newnode('math',owner=palyer.node,attrs={'input1': (0, 1.25, 0),'operation': 'add'}) + palyer.node.connectattr('position', dummy, 'input2') + palyer.r = ba.newnode('text',owner=palyer.node, + attrs={'text': 'TARGET!', + 'in_world': True, + 'shadow': 1.0, + 'flatness': 1.5, + 'scale': 0.014, + 'h_align': 'center',}) + ba.animate_array(palyer.r,'color',3,{ + 0.0:(0.3, 0.3, 1.0), + 0.5:(1, 0.3, 0.3), + 1.0:(0.3, 1 , 0.3), + 2.0:(0.3, 0.3, 1.0) + }, True) + dummy.connectattr('output', palyer.r, 'position') + self.start_timer() + + def start_timer(self) -> None: + self._time_remaining = self.settings['Time to Kill'] + self._timer_x = ba.Timer(1000*0.001,ba.WeakCall(self.tick),repeat=True) + + def stop_timer(self) -> None: + self._time = None + self._timer_x = None + + def tick(self) -> None: + self.check_for_expire() + self._time = ba.NodeActor(ba.newnode('text', + attrs={'v_attach':'top', 'h_attach':'center', + 'text':('Kill Time: '+str(self._time_remaining)+'s'), 'opacity': 0.8, + 'maxwidth':100, 'h_align':'center', + 'v_align':'center', 'shadow':1.0, + 'flatness':1.0, 'color':(1,1,1), + 'scale':2,'position':(0,-100)})) + self._time_remaining -= 1 + ba.playsound(self._tick_sound) + + def check_for_expire(self) -> None: + if self._time_remaining <= 0: + self.stop_timer() + if len(self.players) == 0: pass + elif self._chosed_player.is_alive(): + player = self._chosed_player + player.team.score += 1 + player.actor.r.delete() + ba.playsound(self._dingsound) + self._update_scoreboard() + if any(team.score >= self._score_to_win for team in self.teams): + ba.timer(500*0.001,self.end_game) + self._chose_text.text = 'Survived!' + self._chose_text.color = (0,1,0) + ba.timer(600*0.001,self.star_chosing_player) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + super().handlemessage(msg) + player = msg.getplayer(Player) + self.respawn_player(player) + + killer = msg.getkillerplayer(Player) + assert self._score_to_win is not None + + if player == self._chosed_player: + self._chosed_player.actor.r.delete() + if killer.team is player.team: + if isinstance(self.session, ba.FreeForAllSession): + player.team.score = max(0,player.team.score-1) + else: + ba.playsound(self._dingsound) + for team in self.teams: + if team is not killer.team: + team.score += 1 + else: + killer.team.score += 1 + ba.playsound(self._dingsound) + try: killer.actor.set_score_text(str(killer.team.score)+'/'+str(self._score_to_win),color=killer.team.color,flash=True) + except Exception: pass + + self._update_scoreboard() + if any(team.score >= self._score_to_win for team in self.teams): + ba.timer(500*0.001,self.end_game) + + if killer != self._chosed_player: + self._chose_text.text = 'Killed!' + self._chose_text.color = (1,0.5,0) + else: + self._chose_text.text = 'Dead!' + self._chose_text.color = (1.0,0,0) + ba.playsound(self._error_sound) + ba.timer(600*0.001,self.star_chosing_player) + + else: super().handlemessage(msg) + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) \ No newline at end of file diff --git a/dist/ba_root/mods/games/GravityFalls.py b/dist/ba_root/mods/games/GravityFalls.py new file mode 100644 index 0000000..8036e60 --- /dev/null +++ b/dist/ba_root/mods/games/GravityFalls.py @@ -0,0 +1,31 @@ +## Made by MattZ45986 on GitHub +## Ported by: Freaku / @[Just] Freak#4999 + + +import ba +from bastd.game.elimination import EliminationGame + + + +# ba_meta require api 6 +# ba_meta export game +class GFGame(EliminationGame): + name = 'Gravity Falls' + + def spawn_player(self, player): + actor = self.spawn_player_spaz(player, (0,5,0)) + if not self._solo_mode: + ba.timer(0.3, ba.Call(self._print_lives, player)) + + # If we have any icons, update their state. + for icon in player.icons: + icon.handle_player_spawned() + ba.timer(1,ba.Call(self.raise_player, player)) + return actor + + def raise_player(self, player): + if player.is_alive(): + try: + player.actor.node.handlemessage("impulse",player.actor.node.position[0],player.actor.node.position[1]+.5,player.actor.node.position[2],0,5,0, 3,10,0,0, 0,5,0) + except: pass + ba.timer(0.05,ba.Call(self.raise_player,player)) \ No newline at end of file diff --git a/dist/ba_root/mods/games/Heist.py b/dist/ba_root/mods/games/Heist.py new file mode 100644 index 0000000..7aa1ca3 --- /dev/null +++ b/dist/ba_root/mods/games/Heist.py @@ -0,0 +1,676 @@ +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba +import _ba +import math +import random +from bastd.actor import spawner +from bastd.actor.bomb import BombFactory +from bastd.actor.spazfactory import SpazFactory +from bastd.gameutils import SharedObjects +from bastd.actor.bomb import ExplodeHitMessage +from ba._gameutils import show_damage_count +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.powerupbox import PowerupBox +from bastd.actor.spazbot import SpazBotSet, ExplodeyBotNoTimeLimit + +if TYPE_CHECKING: + from typing import Any, Union, Sequence, Optional + + +class Blast(ba.Actor): + + def __init__(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + blast_radius: float = 2.0, + color: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + + shared = SharedObjects.get() + factory = BombFactory.get() + + self.radius = blast_radius + + rmats = (factory.blast_material, shared.attack_material) + self.node = ba.newnode( + 'region', + delegate=self, + attrs={ + 'position': (position[0], position[1] - 0.1, position[2]), + 'scale': (self.radius, self.radius, self.radius), + 'type': 'sphere', + 'materials': rmats + }, + ) + ba.timer(0.05, self.node.delete) + + explosion = ba.newnode('explosion', + attrs={ + 'position': position, + 'radius': self.radius * 0.8, + 'color': (color[0] * 0.3, + color[1] * 0.3, + color[2] * 0.3), + 'big': True + }) + ba.timer(1.0, explosion.delete) + + light = ba.newnode('light', + attrs={ + 'position': position, + 'volume_intensity_scale': 10.0, + 'color': (1, 0.3, 0.1) + }) + scl = random.uniform(0.6, 0.9) * 3.0 + scorch_radius = self.radius * 1.4 + light_radius = self.radius * 0.6 + iscale = 0.8 + ba.animate( + light, 'intensity', { + 0: 2.0 * iscale, + scl * 0.02: 0.1 * iscale, + scl * 0.025: 0.2 * iscale, + scl * 0.05: 17.0 * iscale, + scl * 0.06: 5.0 * iscale, + scl * 0.08: 4.0 * iscale, + scl * 0.2: 0.6 * iscale, + scl * 2.0: 0.00 * iscale, + scl * 3.0: 0.0 + }) + ba.animate( + light, 'radius', { + 0: light_radius * 0.2, + scl * 0.05: light_radius * 0.55, + scl * 0.1: light_radius * 0.3, + scl * 0.3: light_radius * 0.15, + scl * 1.0: light_radius * 0.05 + }) + ba.timer(scl * 3.0, light.delete) + + scorch = ba.newnode('scorch', + attrs={ + 'position': position, + 'size': scorch_radius * 0.5, + 'color': (color[0] * 0.6, + color[1] * 0.6, + color[2] * 0.6), + 'big': True + }) + ba.animate(scorch, 'presence', {3.000: 1, 13.000: 0}) + ba.timer(13.0, scorch.delete) + + lpos = light.position + ba.playsound(factory.random_explode_sound(), position=lpos) + ba.playsound(factory.random_explode_sound(), position=lpos) + ba.playsound(factory.debris_fall_sound, position=lpos) + ba.camerashake(intensity=5.0) + + def _extra_boom() -> None: + ba.playsound(factory.random_explode_sound(), position=lpos) + ba.timer(0.25, _extra_boom) + + def _extra_debris_sound() -> None: + ba.playsound(factory.debris_fall_sound, position=lpos) + ba.playsound(factory.wood_debris_fall_sound, position=lpos) + ba.timer(0.4, _extra_debris_sound) + +# ba.emitfx(position=position, + # count=100, + # scale=1.8, + # spread=5, + # chunk_type='spark') + # ba.emitfx(position=position, + # count=100, + # spread=5, + # scale=2, + # chunk_type='ice', + # emit_type='stickers') + ba.emitfx(position=position, + count=1000, + spread=500, + scale=10, + chunk_type='slime') + # ba.emitfx(position=position, + # count=20, + # scale=1, + # spread=500, + # chunk_type='sweat', + # emit_type='tendrils') + + def handlemessage(self, msg: Any) -> Any: + assert not self.expired + + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + + elif isinstance(msg, ExplodeHitMessage): + node = ba.getcollision().opposingnode + assert self.node + nodepos = self.node.position + mag = 2000.0 + mag *= 2.0 + + node.handlemessage( + ba.HitMessage(pos=nodepos, + velocity=(0, 0, 0), + magnitude=mag, + hit_type='explosion', + hit_subtype='normal', + radius=self.radius)) + else: + return super().handlemessage(msg) + return None + + +class TNTBox(ba.Actor): + + def __init__(self, + team_id: ba.Team, + modeltype: float = None, + position: Sequence[float] = None, + hitpoints: float = 1000, + team: str = None): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + spaz = SpazFactory.get() + + self.team = team_id + self.modeltype = modeltype + self.team_str = team + self.teamcolor = team_id.color + self.position = position + self.hitpoints = hitpoints + self.hitpoints_max = hitpoints + self._width = 240 + self._width_max = 240 + self._height = 35 + self._bar_width = 240 + self._bar_height = 35 + self._bar_tex = self._backing_tex = ba.gettexture('bar') + self._cover_tex = ba.gettexture('uiAtlas') + self._model = ba.getmodel('meterTransparent') + + if team == 'team 1': + self.bar_posx = -200 - 120 + else: + self.bar_posx = 200 - 120 + + self.box_material = ba.Material() + no_collide_material = ba.Material() + self.box_material.add_actions( + conditions=('they_have_material', shared.pickup_material), + actions=('modify_part_collision', 'collide', False), + ) + + if modeltype == 1: + self.node = ba.newnode( + 'prop', + delegate=self, + attrs={ + 'position': (position[0], position[1] + 2.5, position[2]), + 'model': ba.getmodel('tnt'), + 'light_model': ba.getmodel('tnt'), + 'body': 'crate', + 'body_scale': 3.3, + 'model_scale': 3.35, + 'shadow_size': 0.3, + 'color_texture': ba.gettexture('tickets'), + 'is_area_of_interest': True, + 'reflection': 'soft', + 'reflection_scale': [0.23], + 'materials': [self.box_material, shared.footing_material] + }) + elif modeltype == 2: + self.node = ba.newnode( + 'prop', + delegate=self, + attrs={ + 'position': (position[0], position[1] + 1.5, position[2]), + 'model': ba.getmodel('tnt'), + 'light_model': ba.getmodel('tnt'), + 'body': 'crate', + 'body_scale': 1, + 'model_scale': 1, + 'shadow_size': 0.3, + 'color_texture': ba.gettexture('tnt'), + 'is_area_of_interest': True, + 'reflection': 'soft', + 'reflection_scale': [0.23], + 'materials': [self.box_material, shared.footing_material] + }) + elif modeltype == 3: + self.node = ba.newnode( + 'prop', + delegate=self, + attrs={ + 'position': (position[0], position[1] + 1.5, position[2]), + 'model': ba.getmodel('puck'), + 'body': 'puck', + 'body_scale': 1, + 'model_scale': 1, + 'shadow_size': 1.0, + 'color_texture': ba.gettexture('puckColor'), + 'is_area_of_interest': True, + 'reflection': 'soft', + 'reflection_scale': [0.23], + 'materials': [self.box_material, shared.footing_material] + }) + elif modeltype == 4: + self.node = ba.newnode( + 'prop', + delegate=self, + attrs={ + 'position': (position[0], position[1] + 1.5, position[2]), + 'model': ba.getmodel('frostyPelvis'), + 'body': 'sphere', + 'body_scale': 2.5, + 'model_scale': 2.5, + 'shadow_size': 0.3, + 'color_texture': ba.gettexture('frostyColor'), + 'is_area_of_interest': True, + 'reflection': 'soft', + 'reflection_scale': [1.0], + 'materials': [self.box_material, shared.footing_material] + }) + + ba.animate(self.node, 'model_scale', { + 0: 0, + 0.2: self.node.model_scale * 1.1, + 0.26: self.node.model_scale}) + + light = ba.newnode( + 'light', + owner=self.node, + attrs={ + 'radius': 0.28, + 'color': self.teamcolor + }) + self.node.connectattr('position', light, 'position') + + self._scoreboard() + self._update() + + def animate_model(self) -> None: + if not self.node: + return None + ba.animate(self.node, 'model_scale', { + 0: self.node.model_scale, + 0.08: self.node.model_scale * 0.9, + 0.15: self.node.model_scale}) + if self.modeltype in [1,2]: + ba.emitfx(position=self.node.position, + velocity=self.node.velocity, + count=int(6 + random.random() * 10), + scale=0.5, + spread=0.4, + chunk_type='splinter') + elif self.modeltype == 3: + ba.emitfx(position=self.node.position, + velocity=self.node.velocity, + count=int(4 + random.random() * 4), + scale=0.5, + spread=0.3, + chunk_type='metal') + else: + ba.emitfx(position=self.node.position, + velocity=self.node.velocity, + count=int(4 + random.random() * 4), + scale=0.5, + spread=0.3, + chunk_type='ice') + + def do_damage(self, msg: Any) -> None: + if not self.node: + return None + damage = msg.magnitude + self.hitpoints -= int(damage) + if self.hitpoints <= 0: + self.hitpoints = 0 + Blast( + position=self.node.position, + blast_radius=20.0, + color=self.teamcolor).autoretain() + self.node.delete() + + def _update(self) -> None: + self._score_text.node.text = str(self.hitpoints) + self._bar_width = self.hitpoints * self._width_max / self.hitpoints_max + cur_width = self._bar_scale.input0 + ba.animate(self._bar_scale, 'input0', { + 0.0: cur_width, + 0.1: self._bar_width + }) + cur_x = self._bar_position.input0 + if self.team_str == 'team 1': + ba.animate(self._bar_position, 'input0', { + 0.0: cur_x, + 0.1: self.bar_posx*0.265 - self._bar_width / 2 + }) + else: + ba.animate(self._bar_position, 'input0', { + 0.0: cur_x, + 0.1: self.bar_posx + self._bar_width / 2 + }) + + def show_damage_msg(self, msg: Any) -> None: + if not self.node: + return None + damage = msg.magnitude + self.show_damage_count('-' + str(int(damage)), + self.node.position, + (msg.force_direction[0]*0.2, + msg.force_direction[1]*0.2, + msg.force_direction[2]*0.2,)) + + def show_damage_count(self, damage: str, position: Sequence[float], + direction: Sequence[float]) -> None: + """Pop up a damage count at a position in space. + + Category: Gameplay Functions + """ + lifespan = 1.0 + app = ba.app + + # FIXME: Should never vary game elements based on local config. + # (connected clients may have differing configs so they won't + # get the intended results). + do_big = app.ui.uiscale is ba.UIScale.SMALL or app.vr_mode + txtnode = ba.newnode('text', + attrs={ + 'text': damage, + 'in_world': True, + 'h_align': 'center', + 'flatness': 1.0, + 'shadow': 1.0 if do_big else 0.7, + 'color': (1, 0.25, 0.25, 1), + 'scale': 0.035 if do_big else 0.03 + }) + # Translate upward. + tcombine = ba.newnode('combine', owner=txtnode, attrs={'size': 3}) + tcombine.connectattr('output', txtnode, 'position') + v_vals = [] + pval = 0.0 + vval = 0.07 + count = 6 + for i in range(count): + v_vals.append((float(i) / count, pval)) + pval += vval + vval *= 0.5 + p_start = position[0] + p_dir = direction[0] + ba.animate(tcombine, 'input0', + {i[0] * lifespan: p_start + p_dir * i[1] + for i in v_vals}) + p_start = position[1] + p_dir = direction[1] + ba.animate(tcombine, 'input1', + {i[0] * lifespan: p_start + p_dir * i[1] + for i in v_vals}) + p_start = position[2] + p_dir = direction[2] + ba.animate(tcombine, 'input2', + {i[0] * lifespan: p_start + p_dir * i[1] + for i in v_vals}) + ba.animate(txtnode, 'opacity', {0.7 * lifespan: 1.0, lifespan: 0.0}) + ba.timer(lifespan, txtnode.delete) + + def _scoreboard(self) -> None: + self._backing = ba.NodeActor( + ba.newnode('image', + attrs={ + 'position': (self.bar_posx + self._width / 2, -35), + 'scale': (self._width, self._height), + 'opacity': 0.7, + 'color': (self.teamcolor[0] * 0.2, + self.teamcolor[1] * 0.2, + self.teamcolor[2] * 0.2), + 'vr_depth': -3, + 'attach': 'topCenter', + 'texture': self._backing_tex + })) + self._bar = ba.NodeActor( + ba.newnode('image', + attrs={ + 'opacity': 1.0, + 'color': self.teamcolor, + 'attach': 'topCenter', + 'texture': self._bar_tex + })) + self._bar_scale = ba.newnode('combine', + owner=self._bar.node, + attrs={ + 'size': 2, + 'input0': self._bar_width, + 'input1': self._bar_height + }) + self._bar_scale.connectattr('output', self._bar.node, 'scale') + self._bar_position = ba.newnode( + 'combine', + owner=self._bar.node, + attrs={ + 'size': 2, + 'input0': self.bar_posx + self._bar_width / 2, + 'input1': -35 + }) + self._bar_position.connectattr('output', self._bar.node, 'position') + self._cover = ba.NodeActor( + ba.newnode('image', + attrs={ + 'position': (self.bar_posx + 120, -35), + 'scale': + (self._width * 1.15, self._height * 1.6), + 'opacity': 1.0, + 'color': (self.teamcolor[0] * 1.1, + self.teamcolor[1] * 1.1, + self.teamcolor[2] * 1.1), + 'vr_depth': 2, + 'attach': 'topCenter', + 'texture': self._cover_tex, + 'model_transparent': self._model + })) + self._score_text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'position': (self.bar_posx + 120, -35), + 'h_attach': 'center', + 'v_attach': 'top', + 'h_align': 'center', + 'v_align': 'center', + 'maxwidth': 130, + 'scale': 0.9, + 'text': '', + 'shadow': 0.5, + 'flatness': 1.0, + 'color': (1,1,1,0.8) + })) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.HitMessage): + self.animate_model() + self.do_damage(msg) + # self.show_damage_msg(msg) + self._update() + elif isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + elif isinstance(msg, ba.OutOfBoundsMessage): + if self.node: + self.node.position = self.position + self.node.velocity = (0,0,0) + else: + super().handlemessage(msg) + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + self.can_attack: bool = False + self.tnt: Optional[TNTBox] = None + + +# ba_meta export game +class TNTTeamGame(ba.TeamGameActivity[Player, Team]): + """Football game for teams mode.""" + + name = 'Heist' + description = 'Get the enemies Ticket Vault!' + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: type[ba.Session]) -> list[ba.Setting]: + settings = [ + ba.IntSetting( + 'TNT Hitpoints', + min_value=1000, + default=25000, + increment=1000, + ), + ba.FloatChoiceSetting( + 'Model Type', + choices=[ + ('TNT Big', 1), + ('TNT', 2), + ('Puck', 3), + ('Snowball', 4), + ], + default=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', default=False), + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + # We only support two-team play. + return issubclass(sessiontype, ba.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('team_flag') + + def __init__(self, settings: dict): + super().__init__(settings) + self._time_limit = float(settings['Time Limit']) + self._tnt_hitpoints = int(settings['TNT Hitpoints']) + self._model_type = float(settings['Model Type']) + self._tntbox_pos = [(-11, 2.5, 0.0), (11.0, 2.5, 0.0)] + self.team_index = 1 + self.create_team_index = 1 + self._bots = SpazBotSet() + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Get the enemies Ticket Vault!' + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'Get the enemies Ticket Vault!' + + def on_team_join(self, team: Team) -> None: + # Can't do this in create_team because the team's color/etc. have + # not been wired up yet at that point. + self._spawn_tnt_for_team(team) + + def _spawn_tnt_for_team(self, team: Team) -> None: + team.tnt = TNTBox(team_id=team, + modeltype=self._model_type, + position=self.map.get_flag_position(team.id), + hitpoints=self._tnt_hitpoints, + team='team ' + str(self.team_index)).autoretain() + self.team_index += 1 + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + ba.timer(0.05, self._update, repeat=True) + ba.timer(10.0, self.custom_drop, repeat=True) + # self.setup_standard_powerup_drops() + + def custom_drop(self): + pos = self.map.get_flag_position(None) + def spawn_punch(): + PowerupBox( + position=(pos[0], pos[1] + 2.0, pos[2]), + poweruptype='punch', + expire=False).autoretain() + def spawn_shield(): + PowerupBox( + position=(pos[0], pos[1] + 2.0, pos[2]), + poweruptype='shield', + expire=False).autoretain() + def spawn_explodeybot(): + self._bots._spawn_bot( + ExplodeyBotNoTimeLimit, + pos=(pos[0], pos[1] + 1.0, pos[2]), + on_spawn_call=None) + custom = random.choice([spawn_punch, spawn_shield, spawn_explodeybot]) + spawner.Spawner( + pt=pos, + spawn_time=3.0, + send_spawn_message=False, + spawn_callback=custom) + + def _update(self) -> None: + if not self.teams[0].tnt.node: + self.teams[1].score = 1 + if not self.teams[1].tnt.node: + self.teams[0].score = 1 + if self.teams[0].score > 0 or self.teams[1].score > 0: + ba.timer(1.0, self.end_game) + + def spawn_player(self, player: Player) -> ba.Actor: + team: Team = player.team + spaz = self.spawn_player_spaz(player) + spaz.connect_controls_to_player(enable_bomb=False) + return spaz + + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + # Augment standard behavior. + super().handlemessage(msg) + player = msg.getplayer(Player) + self.respawn_player(player) + else: + return super().handlemessage(msg) + return None + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results, announce_delay=0.8) diff --git a/dist/ba_root/mods/games/Hot-Bomb.py b/dist/ba_root/mods/games/Hot-Bomb.py new file mode 100644 index 0000000..139fd81 --- /dev/null +++ b/dist/ba_root/mods/games/Hot-Bomb.py @@ -0,0 +1,1551 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Hot Bomb game by SEBASTIAN2059 and zPanxo""" + +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import random + +import ba,_ba +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.powerupbox import PowerupBoxFactory +from bastd.gameutils import SharedObjects +from ba._messages import StandMessage +from bastd.actor.bomb import Bomb, Blast +from bastd.actor.spaz import Spaz, PickupMessage, PunchHitMessage, BombDiedMessage +from bastd.actor.popuptext import PopupText + +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + + +class BallDiedMessage: + """Inform something that a ball has died.""" + + def __init__(self, ball: Ball): + self.ball = ball + +class ExplodeHitMessage: + """Tell an object it was hit by an explosion.""" + +class Ball(ba.Actor): + """A lovely bomb mortal""" + + def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0),timer: int = 5,d_time=0.2,color=(1,1,1)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + + self.explosion_material = ba.Material() + self.explosion_material.add_actions( + conditions=('they_have_material', shared.object_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('message', 'our_node', 'at_connect', ExplodeHitMessage()), + ), + ) + + ba.playsound(ba.getsound('scamper01'),volume=0.4) + # Spawn just above the provided point. + self._spawn_pos = (position[0], position[1] + 1.0, position[2]) + self.last_players_to_touch: Dict[int, Player] = {} + self.scored = False + assert activity is not None + assert isinstance(activity, HotBombGame) + pmats = [shared.object_material, activity.ball_material] + self.node = ba.newnode('prop', + delegate=self, + attrs={ + 'model': activity.ball_model, + 'color_texture': activity.ball_tex, + 'body': activity.ball_body, + 'body_scale': 1.0 if activity.ball_body == 'sphere' else 0.8, + 'density':1.0 if activity.ball_body == 'sphere' else 1.2, + 'reflection': 'soft', + 'reflection_scale': [0.2], + 'shadow_size': 0.5, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats + }) + self._animate = None + self.scale = 1.0 if activity.ball_body == 'sphere' else 0.8 + + if self.activity._impulse_bomb: + self.node.extra_acceleration = (0,-20,0) + + self.color_l = (1,1,1) + self.light = ba.newnode('light', owner=self.node, attrs={ + 'color':color, + 'volume_intensity_scale': 0.4, + 'intensity':0.5, + 'radius':0.10}) + self.node.connectattr('position', self.light,'position') + self.animate_light = None + + self._particles = ba.Timer(0.1,call=ba.WeakCall(self.particles),repeat=True) + self._sound_effect = ba.Timer(4,call=ba.WeakCall(self.sound_effect),repeat=True) + + + self.d_time = d_time + + if timer is not None: + timer = int(timer) + self._timer = timer + self._counter: Optional[ba.Node] + if self._timer is not None: + self._count = self._timer + self._tick_timer = ba.Timer(1.0, + call=ba.WeakCall(self._tick), + repeat=True) + m = ba.newnode('math', owner=self.node, attrs={'input1': (0, 0.6, 0), 'operation': 'add'}) + self.node.connectattr('position', m, 'input2') + self._counter = ba.newnode('text', + owner=self.node, + attrs={'text':str(timer), + 'in_world':True, + 'shadow':1.0, + 'flatness':0.7, + 'color':(1,1,1), + 'scale':0.013, + 'h_align':'center'}) + m.connectattr('output', self._counter, 'position') + else: + self._counter = None + + def particles(self): + if self.node: + ba.emitfx(position=self.node.position, + velocity=(0,3,0), + count=9, + scale=2.5, + spread=0.2, + chunk_type='sweat') + + def sound_effect(self): + if self.node: + ba.playsound(ba.getsound('scamper01'),volume=0.4) + + + def explode(self,color=(3,1,0)) -> None: + sound = random.choice(['explosion01','explosion02','explosion03','explosion04','explosion05']) + ba.playsound(ba.getsound(sound),volume=1) + ba.emitfx(position=self.node.position, + velocity=(0,10,0), + count=100, + scale=1.0, + spread=1.0, + chunk_type='spark') + explosion = ba.newnode('explosion', + attrs={ + 'position': self.node.position, + 'velocity': (0,0,0), + 'radius': 2.0, + 'big': False, + 'color':color + }) + if color == (5,1,0): + color = (1,0,0) + self.activity._handle_score(1) + else: + color=(0,0,1) + self.activity._handle_score(0) + scorch = ba.newnode('scorch', + attrs={ + 'position': self.node.position, + 'size': 1.0, + 'big': True, + 'color':color, + 'presence':1 + }) + + # Set our position a bit lower so we throw more things upward. + rmats = (self.explosion_material,) + self.region = ba.newnode( + 'region', + delegate=self, + attrs={ + 'position': (self.node.position[0], self.node.position[1] - 0.1, self.node.position[2]), + 'scale': (2.0, 2.0, 2.0), + 'type': 'sphere', + 'materials': rmats + }, + ) + ba.timer(0.05, self.region.delete) + + def _tick(self) -> None: + c = self.color_l + c2 = (2.5,1.5,0) + if c[2] != 0: + c2 = (0,2,3) + if self.node: + if self._count == 1: + pos = self.node.position + color = (5,1,0) if pos[0] < 0 else (0,1,5) + self.explode(color=color) + return + if self._count > 0: + self._count -= 1 + assert self._counter + self._counter.text = str(self._count) + ba.playsound(ba.getsound('tick')) + if self._count == 1: + self._animate = ba.animate(self.node,'model_scale',{0:self.scale,0.1:1.5,0.2:self.scale},loop=True) + self.animate_light = ba.animate_array(self.light,'color',3,{0:c,0.1:c2,0.2:c},loop=True) + else: + self._animate = ba.animate(self.node,'model_scale',{0:self.scale,0.5:1.5,1.0:self.scale},loop=True) + self.animate_light = ba.animate_array(self.light,'color',3,{0:c,0.2:c2,0.5:c,1.0:c},loop=True) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + assert self.node + self.node.delete() + activity = self._activity() + if activity and not msg.immediate: + activity.handlemessage(BallDiedMessage(self)) + + # If we go out of bounds, move back to where we started. + elif isinstance(msg, ba.OutOfBoundsMessage): + assert self.node + self.node.position = self._spawn_pos + + elif isinstance(msg, ba.PickedUpMessage): + d = self.d_time + def damage(): + if (msg is not None and msg.node.exists() + and msg.node.getdelegate(PlayerSpaz).hitpoints > 0): + spaz = msg.node.getdelegate(PlayerSpaz) + spaz.node.color = (spaz.node.color[0]-0.1,spaz.node.color[1]-0.1,spaz.node.color[2]-0.1) + if spaz.hitpoints > 10000: + ba.playsound(ba.getsound('fuse01'),volume=0.3) + spaz.hitpoints -= 10000 + spaz._last_hit_time = None + spaz._num_time_shit = 0 + spaz.node.hurt = 1.0 - float(spaz.hitpoints) / spaz.hitpoints_max + else: + spaz.handlemessage(ba.DieMessage()) + ba.emitfx( + position=msg.node.position, + velocity=(0, 3, 0), + count=20 if d == 0.2 else 25 if d == 0.1 else 30 if d == 0.05 else 15, + scale=1.0, + spread=0.2, + chunk_type='sweat') + else: + self.damage_timer = None + + self.damage_timer = ba.Timer(self.d_time, damage, repeat=True) + # + if self.activity._impulse_bomb: + self.node.extra_acceleration = (0,27,0) if self.activity.ball_body == 'box' else (0,45,0) + + elif isinstance(msg, ba.DroppedMessage): + from ba import _math + spaz = msg.node.getdelegate(PlayerSpaz) + self.damage_timer = None + if self.activity._impulse_bomb: + self.node.extra_acceleration = (0, -8, 0) if self.activity.ball_body == 'box' else (0, -20, 0) + + elif isinstance(msg, ba.HitMessage): + assert self.node + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, + 1.0 * msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + + # If this hit came from a player, log them as the last to touch us. + s_player = msg.get_source_player(Player) + if s_player is not None: + activity = self._activity() + if activity: + if s_player in activity.players: + self.last_players_to_touch[s_player.team.id] = s_player + + elif isinstance(msg, ExplodeHitMessage): + node = ba.getcollision().opposingnode + assert self.node + nodepos = self.region.position + mag = 2000.0 + + node.handlemessage( + ba.HitMessage(pos=nodepos, + velocity=(0, 0, 0), + magnitude=mag, + hit_type='explosion', + hit_subtype='normal', + radius=2.0)) + self.handlemessage(ba.DieMessage()) + else: + super().handlemessage(msg) + +###HUMAN### +class NewPlayerSpaz(PlayerSpaz): + + move_mult = 1.0 + reload = True + extra_jump = True + ###calls + + def impulse(self): + self.reload = False + p = self.node + self.node.handlemessage("impulse", p.position[0], p.position[1]+40, p.position[2], + 0, 0, 0, + 160, 0, 0, 0, + 0, 205, 0) + ba.timer(0.4,self.refresh) + + def refresh(self): + self.reload = True + + def on_bomb_press(self) -> None: + if not self.node: + return + + if self._dead or self.frozen: + return + if self.node.knockout > 0.0: + return + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + if t_ms - self.last_bomb_time_ms >= self._bomb_cooldown: + self.last_bomb_time_ms = t_ms + self.node.bomb_pressed = True + if not self.node.hold_node: + self.drop_bomb() + + self._turbo_filter_add_press('bomb') + + def on_bomb_release(self) -> None: + if not self.node: + return + self.node.bomb_pressed = False + + def drop_bomb(self) -> Optional[Bomb]: + + if (self.land_mine_count <= 0 and self.bomb_count <= 0) or self.frozen: + return None + assert self.node + pos = self.node.position_forward + vel = self.node.velocity + + if self.land_mine_count > 0: + dropping_bomb = False + self.set_land_mine_count(self.land_mine_count - 1) + bomb_type = 'land_mine' + else: + dropping_bomb = True + bomb_type = self.bomb_type + + if bomb_type == 'banana': + ba.playsound(ba.getsound('penguinHit1'),volume=0.3) + bomb = NewBomb(position=(pos[0], pos[1] + 0.7, pos[2]), + velocity=(vel[0], vel[1], vel[2]), + bomb_type = bomb_type, + radius = 1.0, + source_player=self.source_player, + owner=self.node) + else: + bomb = Bomb(position=(pos[0], pos[1] - 0.0, pos[2]), + velocity=(vel[0], vel[1], vel[2]), + bomb_type=bomb_type, + blast_radius=self.blast_radius, + source_player=self.source_player, + owner=self.node).autoretain() + + + assert bomb.node + if dropping_bomb: + self.bomb_count -= 1 + bomb.node.add_death_action( + ba.WeakCall(self.handlemessage, BombDiedMessage())) + self._pick_up(bomb.node) + + try: + for clb in self._dropped_bomb_callbacks: + clb(self, bomb) + except Exception: + return + + return bomb + + def on_jump_press(self) -> None: + if not self.node: + return + self.node.jump_pressed = True + self._turbo_filter_add_press('jump') + + if self.reload and self.extra_jump: + self.impulse() + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, PickupMessage): + if not self.node: + return None + try: + collision = ba.getcollision() + opposingnode = collision.opposingnode + opposingbody = collision.opposingbody + except ba.NotFoundError: + return True + if opposingnode.getnodetype() == 'spaz': + player = opposingnode.getdelegate(PlayerSpaz,True).getplayer(Player, True) + if player.actor.shield: + return None + super().handlemessage(msg) + return super().handlemessage(msg) + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +lang = ba.app.lang.language +if lang == 'Spanish': + name = 'Hot Bomb' + description = 'Consigue explotar la bomba en\nel equipo enemigo para ganar.' + join_description = 'Deshazte de la bomba cuanto antes.' + join_description_l = 'Deshazte de la bomba cuanto antes.' + view_description = 'Estalla la bomba en el equipo rival' + view_description_l = 'Estalla ${ARG1} veces la bomba en el equipo rival' + bomb_timer = 'Temporizador' + space_wall = 'Espacio Debajo de la Red' + num_bones = 'Huesos Distractores' + b_count = ['Nada','Pocos','Muchos'] + shield = 'Inmortalidad' + bomb = 'Habilitar Bananas' + impulse_bomb = 'Modo Globo' + boxing_gloves = 'Equipar Guantes de Boxeo' + difficulty = 'Dificultad' + difficulty_o = ['Fácil','Difícil','Chernobyl'] + wall_color = 'Color de la Red' + w_c = ['Verde','Rojo','Naranja','Amarillo','Celeste','Azul','Rosa','Gris'] + ball_body = 'Tipo de Hot Bomb' + body = ['Esfera','Cubo'] + +else: + name = 'Hot Bomb' + description = 'Get the bomb to explode on\nthe enemy team to win.' + join_description = 'Get rid of the bomb as soon as possible.' + join_description_l = 'Get rid of the bomb as soon as possible.' + view_description = 'Explode the bomb in the enemy team' + view_description_l = 'Explode the bomb ${ARG1} times in the enemy team' + bomb_timer = 'Timer' + space_wall = 'Space Under the Mesh' + num_bones = 'Distractor Bones' + b_count = ['None','Few','Many'] + shield = 'Immortality' + bomb = 'Enable Bananas' + impulse_bomb = 'Balloon Mode' + difficulty = 'Difficulty' + difficulty_o = ['Easy','Hard','Chernobyl'] + wall_color = 'Mesh Color' + w_c = ['Green','Red','Orange','Yellow','Light blue','Blue','Ping','Gray'] + ball_body = 'Type of Hot Bomb' + body = ['Sphere','Box'] + + +# ba_meta export game +class HotBombGame(ba.TeamGameActivity[Player, Team]): + """New game.""" + + name = name + description = description + available_settings = [ + ba.IntSetting( + 'Score to Win', + min_value=1, + default=5, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 3.0), + ], + default=0.5, + + ), + ba.FloatChoiceSetting( + difficulty, + choices=[ + (difficulty_o[0], 0.15), + (difficulty_o[1], 0.04), + (difficulty_o[2], 0.01), + ], + default=0.15, + + ), + ba.IntChoiceSetting( + bomb_timer, + choices=[(str(choice)+'s',choice) for choice in range(2,11)], + default=5, + + ), + ba.IntChoiceSetting( + num_bones, + choices=[ + (b_count[0], 0), + (b_count[1], 2), + (b_count[2], 5), + ], + default=2, + + ), + ba.IntChoiceSetting( + ball_body, + choices=[(b, body.index(b)) for b in body], + default=0, + ), + ba.IntChoiceSetting( + wall_color, + choices=[(color,w_c.index(color)) for color in w_c], + default=0, + + ), + ba.BoolSetting('Epic Mode', default=False), + ba.BoolSetting(space_wall, default=True), + ba.BoolSetting(bomb, default=True), + ba.BoolSetting(impulse_bomb, default=False), + ba.BoolSetting(shield, default=False), + + ] + default_music = ba.MusicType.HOCKEY + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('hotbomb') + + def __init__(self, settings: dict): + super().__init__(settings) + self._bomb_timer = int(settings[bomb_timer]) + self._space_under_wall = bool(settings[space_wall]) + self._num_bones = int(settings[num_bones]) + self._shield = bool(settings[shield]) + self._bomb = bool(settings[bomb]) + self._impulse_bomb = bool(settings[impulse_bomb]) + self.damage_time = float(settings[difficulty]) + self._epic_mode = bool(settings['Epic Mode']) + self._wall_color = int(settings[wall_color]) + self._ball_body = int(settings[ball_body]) + + self.bodys = ['sphere','crate'] + self.models = ['bombSticky','powerupSimple'] + + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._cheer_sound = ba.getsound('cheer') + self._chant_sound = ba.getsound('crowdChant') + self._foghorn_sound = ba.getsound('foghorn') + self._swipsound = ba.getsound('swip') + self._whistle_sound = ba.getsound('refWhistle') + self.ball_model = ba.getmodel(self.models[self._ball_body]) + self.ball_body = self.bodys[self._ball_body] + self.ball_tex = ba.gettexture('powerupCurse') + self._ball_sound = ba.getsound('splatter') + + self.last_point = None + self.colors = [(0.25,0.5,0.25), (1, 0.15, 0.15), (1, 0.5, 0), (1, 1, 0), + (0.2, 1, 1), (0.1, 0.1, 1), (1, 0.3, 0.5),(0.5, 0.5, 0.5)] + # + self.slow_motion = self._epic_mode + + self.ball_material = ba.Material() + self.ball_material.add_actions(actions=(('modify_part_collision', + 'friction', 0.5))) + self.ball_material.add_actions(conditions=('they_have_material', + shared.pickup_material), + actions=('modify_part_collision', + 'collide', True)) + self.ball_material.add_actions( + conditions=( + ('we_are_younger_than', 100), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + self.ball_material.add_actions(conditions=('they_have_material', + shared.footing_material), + actions=('impact_sound', + self._ball_sound, 0.2, 4)) + + # Keep track of which player last touched the ball + self.ball_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_connect', + self._handle_ball_player_collide), )) + + # We want the ball to kill powerups; not get stopped by them + self.ball_material.add_actions( + conditions=('they_have_material', + PowerupBoxFactory.get().powerup_material), + actions=(('modify_part_collision', 'physical', False), + ('message', 'their_node', 'at_connect', ba.DieMessage()))) + + self._score_region_material = ba.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', self.ball_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_score))) + ##### + self._check_region_material = ba.Material() + self._check_region_material.add_actions( + conditions=('they_have_material', self.ball_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._reset_count))) + + self._reaction_material = ba.Material() + self._reaction_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._reaction))) + + self._reaction_material.add_actions( + conditions=('they_have_material', HealthFactory.get().health_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', True))) + + self._collide=ba.Material() + self._collide.add_actions(conditions=(('they_are_different_node_than_us', ), + 'and', + ('they_have_material', shared.player_material),), actions=(('modify_part_collision', 'collide', True))) + + self._wall_material=ba.Material() + self._wall_material.add_actions(conditions=('we_are_older_than', 1), actions=(('modify_part_collision', 'collide', True))) + + self.ice_material = ba.Material() + self.ice_material.add_actions(actions=('modify_part_collision','friction',0.05)) + + self._ball_spawn_pos: Optional[Sequence[float]] = None + self._score_regions: Optional[List[ba.NodeActor]] = None + self._ball: Optional[Ball] = None + self._score_to_win = int(settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + + def get_instance_description(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return join_description + return join_description_l, self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return view_description + return view_description_l, self._score_to_win + + def on_begin(self) -> None: + super().on_begin() + shared = SharedObjects.get() + act = ba.getactivity().map + ### + self.setup_standard_time_limit(self._time_limit) + self._ball_spawn_pos = (random.choice([-5,5]),4,0) + ba.timer(5,self._spawn_ball) + ba.timer(0.1,self.update_ball,repeat=True) + HealthBox(position=(-1,3.5,-5+random.random()*10)) + HealthBox(position=(1,3.5,-5+random.random()*10)) + ### + g = 0 + while g < self._num_bones: + b = 0 + Torso(position=(-6+random.random()*12,3.5,-5+random.random()*10)) + while b < 6: + Bone(position=(-6+random.random()*12,2,-5+random.random()*10),style=b) + b += 1 + g += 1 + ######################## + self.wall_color = self.colors[self._wall_color] + e = ba.newnode('locator',attrs={'shape':'box','position':(-6.65-0.459-0.06,0.5,0.5), + 'color':self.wall_color,'opacity':1, 'drawShadow':False,'draw_beauty':True,'additive':False,'size':[14.7,2,16]}) + if self._space_under_wall: + f = ba.newnode('locator',attrs={'shape':'box','position':(0,-13.51,0.5), + 'color':self.wall_color,'opacity':1, 'drawShadow':False,'draw_beauty':True,'additive':False,'size':[0.3,30,13]}) + else: + g = ba.newnode('locator',attrs={'shape':'box','position':(0,-35.489-0.51,0.5), + 'color':self.wall_color,'opacity':1, 'drawShadow':False,'draw_beauty':True,'additive':False,'size':[0.3,75,13]}) + scale = (0.3,1.5,13) + pos = (0,0.75,0.5) + if self._space_under_wall: + scale = (0.3,0.75,13) + pos = (0,1.11,0.5) + z5 = ba.newnode('region',attrs={'position': pos,'scale': scale,'type': 'box','materials': (self._wall_material,self._reaction_material)}) + ####RESET-REGION######### + pos = (0,5.3,0) + size = (0.01,15,12) #(0,6,12) + ba.newnode('region',attrs={'position': pos,'scale': size,'type': 'box', + 'materials': [self._check_region_material,self._reaction_material]}) + + ba.newnode('region',attrs={'position': pos,'scale': (0.3,15,12),'type': 'box', + 'materials': [self._collide]}) + + self._update_scoreboard() + ba.playsound(self._chant_sound) + + def _reaction(self): + node: ba.Node = ba.getcollision().opposingnode + ba.playsound(ba.getsound('hiss'),volume=0.75) + + node.handlemessage("impulse",node.position[0],node.position[1],node.position[2], + -node.velocity[0]*2,-node.velocity[1],-node.velocity[2], + 100,100,0,0,-node.velocity[0],-node.velocity[1],-node.velocity[2]) + + ba.emitfx(position=node.position,count=20,scale=1.5,spread=0.5,chunk_type='sweat') + + def spawn_player(self, player: Player) -> ba.Actor: + position = self.get_position(player) + name = player.getname() + display_color = _ba.safecolor(player.color, target_intensity=0.75) + actor = NewPlayerSpaz(color=player.color, + highlight=player.highlight, + character=player.character, + player=player) + player.actor = actor + + player.actor.node.name = name + player.actor.node.name_color = display_color + player.actor.bomb_type_default = 'banana' + player.actor.bomb_type = 'banana' + + actor.connect_controls_to_player(enable_punch=True, + enable_bomb=self._bomb, + enable_pickup=True) + actor.node.hockey = True + actor.hitpoints_max = 100000 + actor.hitpoints = 100000 + actor.equip_boxing_gloves() + if self._shield: + actor.equip_shields() + actor.shield.color = (0,0,0) + actor.shield.radius = 0.1 + actor.shield_hitpoints = actor.shield_hitpoints_max = 100000 + + #Move to the stand position and add a flash of light. + actor.handlemessage( + StandMessage( + position, + random.uniform(0, 360))) + ba.playsound(ba.getsound('spawn'),volume=0.6) + return actor + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def _handle_ball_player_collide(self) -> None: + collision = ba.getcollision() + try: + ball = collision.sourcenode.getdelegate(Ball, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except ba.NotFoundError: + return + + ball.last_players_to_touch[player.team.id] = player + + def _kill_ball(self) -> None: + self._ball = None + + def _reset_count(self) -> None: + """reset counter of ball.""" + + assert self._ball is not None + assert self._score_regions is not None + + if self._ball.scored: + return + + ba.playsound(ba.getsound('laser')) + self._ball._count = self._bomb_timer + 1 + if self._ball.light.color[0] == 0: + self._ball.light.color = (2,0,0) + else: + self._ball.light.color = (0,0,3) + + def update_ball(self): + if not self._ball: return + if not self._ball.node: return + gnode = ba.getactivity().globalsnode + + if self._ball.node.position[0] > 0: + self._ball.node.color_texture = ba.gettexture('powerupIceBombs') + ba.animate_array(gnode,'vignette_outer',3,{1.0:(0.4, 0.4, 0.9)}) + self._ball.color_l = (0,0,3.5) + self._ball._counter.color = (0,0,5) + else: + self._ball.node.color_texture = ba.gettexture('powerupPunch') + ba.animate_array(gnode,'vignette_outer',3,{1.0:(0.6,0.45,0.45)}) + self._ball.color_l = (2.5,0,0) + self._ball._counter.color = (1.2,0,0) + + def _handle_score(self,index=0) -> None: + """A point has been scored.""" + + assert self._ball is not None + + for team in self.teams: + if team.id == index: + scoring_team = team + team.score += 1 + if index == 0: + self.last_point = 0 + else: + self.last_point = 1 + + # Tell all players to celebrate. + for player in team.players: + if player.actor: + player.actor.handlemessage(ba.CelebrateMessage(2.0)) + + # If we've got the player from the scoring team that last + # touched us, give them points. + if (scoring_team.id in self._ball.last_players_to_touch + and self._ball.last_players_to_touch[scoring_team.id]): + self.stats.player_scored( + self._ball.last_players_to_touch[scoring_team.id], + 100, + big_message=True) + + # End game if we won. + if team.score >= self._score_to_win: + self.end_game() + + elif team.id != index: + + # Tell all players to celebrate. + for player in team.players: + if player.actor: + player.actor.handlemessage(ba.DieMessage()) + + ba.playsound(self._foghorn_sound) + ba.playsound(self._cheer_sound) + + ba.cameraflash(duration=10.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def _update_scoreboard(self) -> None: + winscore = self._score_to_win + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, winscore) + + def handlemessage(self, msg: Any) -> Any: + + # Respawn dead players if they're still in the game. + if isinstance(msg, ba.PlayerDiedMessage): + + player = msg.getplayer(Player) + spaz = player.actor + spaz.node.color = (-1,-1,-1) + spaz.node.color_mask_texture = ba.gettexture('bonesColorMask') + spaz.node.color_texture = ba.gettexture('bonesColor') + spaz.node.head_model = ba.getmodel('bonesHead') + spaz.node.hand_model = ba.getmodel('bonesHand') + spaz.node.torso_model = ba.getmodel('bonesTorso') + spaz.node.pelvis_model = ba.getmodel('bonesPelvis') + spaz.node.upper_arm_model = ba.getmodel('bonesUpperArm') + spaz.node.forearm_model = ba.getmodel('bonesForeArm') + spaz.node.upper_leg_model = ba.getmodel('bonesUpperLeg') + spaz.node.lower_leg_model = ba.getmodel('bonesLowerLeg') + spaz.node.toes_model = ba.getmodel('bonesToes') + spaz.node.style = 'bones' + # Augment standard behavior... + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + + # Respawn dead balls. + elif isinstance(msg, BallDiedMessage): + if not self.has_ended(): + try: + if self._ball._count == 1: + ba.timer(3.0, self._spawn_ball) + except Exception: + return + else: + super().handlemessage(msg) + + def _flash_ball_spawn(self,pos,color=(1,0,0)) -> None: + light = ba.newnode('light', + attrs={ + 'position': pos, + 'height_attenuated': False, + 'color': color + }) + ba.animate(light, 'intensity', {0.0: 0, 0.25: 0.2, 0.5: 0}, loop=True) + ba.timer(1.0, light.delete) + + def _spawn_ball(self) -> None: + timer = self._bomb_timer + ba.playsound(self._swipsound) + ba.playsound(self._whistle_sound) + pos = (random.choice([5,-5]),2,0) + if self.last_point != None: + if self.last_point == 0: + pos = (-5,2,0) + else: + pos = (5,2,0) + + color = (0,0,1*2) if pos[0] == 5 else (1*1.5,0,0) + texture = 'powerupPunch' if pos[0] == -5 else 'powerupIceBombs' + counter_color = (1,0,0) if pos[0] == -5 else (0,0,5) + #self._flash_ball_spawn(pos,color) + self._ball = Ball(position=pos,timer=timer,d_time=self.damage_time,color=color) + self._ball.node.color_texture = ba.gettexture(texture) + self._ball._counter.color = counter_color + + def get_position(self, player: Player) -> ba.Actor: + position = (0,1,0) + team = player.team.id + if team == 0: + position = (random.randint(-7,-3),0.25,random.randint(-5,5)) + angle = 90 + else: + position = (random.randint(3,7),0.25,random.randint(-5,5)) + angle = 270 + return position + + def respawn_player(self, + player: PlayerType, + respawn_time: Optional[float] = None) -> None: + import _ba + from ba._general import Call, WeakCall + + assert player + if respawn_time is None: + respawn_time = 3.0 + # teamsize = len(player.team.players) + # if teamsize == 1: + # respawn_time = 3.0 + # elif teamsize == 2: + # respawn_time = 5.0 + # elif teamsize == 3: + # respawn_time = 6.0 + # else: + # respawn_time = 7.0 + + # If this standard setting is present, factor it in. + if 'Respawn Times' in self.settings_raw: + respawn_time *= self.settings_raw['Respawn Times'] + + # We want whole seconds. + assert respawn_time is not None + respawn_time = round(max(1.0, respawn_time), 0) + + if player.actor and not self.has_ended(): + from bastd.actor.respawnicon import RespawnIcon + player.customdata['respawn_timer'] = _ba.Timer( + respawn_time, WeakCall(self.spawn_player_if_exists, player)) + player.customdata['respawn_icon'] = RespawnIcon( + player, respawn_time) + + def spawn_player_if_exists(self, player: PlayerType) -> None: + """ + A utility method which calls self.spawn_player() *only* if the + ba.Player provided still exists; handy for use in timers and whatnot. + + There is no need to override this; just override spawn_player(). + """ + if player: + self.spawn_player(player) + + + def spawn_player_spaz(self, player: PlayerType) -> None: + position = (0,1,0) + angle = None + team = player.team.id + if team == 0: + position = (random.randint(-7,-3),0.25,random.randint(-5,5)) + angle = 90 + else: + position = (random.randint(3,7),0.25,random.randint(-5,5)) + angle = 270 + + return super().spawn_player_spaz(player, position, angle) + +#####New-Bomb##### +class ExplodeMessage: + """Tells an object to explode.""" + +class ImpactMessage: + """Tell an object it touched something.""" + +class NewBomb(ba.Actor): + + def __init__(self, position: Sequence[float] = (0, 1, 0), + velocity: Sequence[float] = (0, 0, 0), + bomb_type: str = '', + radius: float = 2.0, + source_player: ba.Player = None, + owner: ba.Node = None): + + super().__init__() + + shared = SharedObjects.get() + # Material for powerups. + self.bomb_material = ba.Material() + self.explode_material = ba.Material() + + self.bomb_material.add_actions( + conditions=( + ('we_are_older_than', 200), + 'and', + ('they_are_older_than', 200), + 'and', + ('eval_colliding', ), + 'and', + ( + ('they_have_material', shared.footing_material), + 'or', + ('they_have_material', shared.object_material), + ), + ), + actions=('message', 'our_node', 'at_connect', ImpactMessage())) + + self.explode_material.add_actions( + conditions=('they_have_material', + shared.player_material), + actions=(('modify_part_collision', 'collide',True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._touch_player))) + + self._source_player = source_player + self.owner = owner + self.bomb_type = bomb_type + self.radius = radius + + owner_color = self.owner.source_player._team.color + + if self.bomb_type == 'banana': + self.node: ba.Node = ba.newnode('prop', delegate=self, attrs={ + 'position': position, + 'velocity': velocity, + 'color_texture': ba.gettexture('powerupBomb'), + 'model': ba.getmodel('penguinTorso'), + 'model_scale':0.7, + 'body_scale':0.7, + 'density':3, + 'reflection': 'soft', + 'reflection_scale': [1.0], + 'shadow_size': 0.3, + 'body': 'sphere', + 'owner': owner, + 'materials': (shared.object_material,self.bomb_material)}) + + ba.animate(self.node,'model_scale',{0:0,0.2:1,0.26:0.7}) + self.light = ba.newnode('light', owner=self.node, attrs={ + 'color':owner_color, + 'volume_intensity_scale': 2.0, + 'intensity':1, + 'radius':0.1}) + self.node.connectattr('position', self.light,'position') + + self.spawn: ba.Timer = ba.Timer( + 10.0,self._check,repeat=True) + + def _impact(self) -> None: + node = ba.getcollision().opposingnode + node_delegate = node.getdelegate(object) + if node: + if (node is self.owner): + return + self.handlemessage(ExplodeMessage()) + + + def _explode(self): + if self.node: + # Set our position a bit lower so we throw more things upward. + + pos = self.node.position + rmats = (self.explode_material,) + self.explode_region = ba.newnode( + 'region', + delegate=self, + attrs={ + 'position': (pos[0], pos[1] - 0.1, pos[2]), + 'scale': (self.radius, self.radius, self.radius), + 'type': 'sphere', + 'materials': rmats + }, + ) + if self.bomb_type == 'banana': + ba.playsound(ba.getsound('stickyImpact'),volume=0.35) + a = ba.emitfx(position=self.node.position, + velocity=(0,1,0), + count=15, + scale=1.0, + spread=0.1, + chunk_type='spark') + scorch = ba.newnode('scorch', + attrs={ + 'position': self.node.position, + 'size': 1.0, + 'big': False, + 'color':(1,1,0) + }) + + ba.animate(scorch,'size',{0:1.0,5:0}) + ba.timer(5,scorch.delete) + + + ba.timer(0.05, self.explode_region.delete) + ba.timer(0.001, ba.WeakCall(self.handlemessage, ba.DieMessage())) + + def _touch_player(self): + node = ba.getcollision().opposingnode + collision = ba.getcollision() + try: + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except ba.NotFoundError: + return + + if self.bomb_type == 'banana': + color = player.team.color + owner_team = self.owner.source_player._team + if (node is self.owner): + return + if player.team == owner_team: + return + player.actor.node.handlemessage('knockout', 500.0) + ba.animate_array(player.actor.node,'color',3,{0:color,0.1:(1.5,1,0),0.5:(1.5,1,0),0.6:color}) + + def _check(self) -> None: + """Prevent the cube from annihilating.""" + + def handlemessage(self, msg): + if isinstance(msg, ExplodeMessage): + self._explode() + elif isinstance(msg, ImpactMessage): + self._impact() + elif isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + elif isinstance(msg, ba.OutOfBoundsMessage): + if self.node: + self.node.delete() + +######Object##### +class HealthFactory: + """Wraps up media and other resources used by ba.Bombs. + + category: Gameplay Classes + + A single instance of this is shared between all bombs + and can be retrieved via bastd.actor.bomb.get_factory(). + + Attributes: + + health_model + The ba.Model of a standard health. + + health_tex + The ba.Texture for health. + + activate_sound + A ba.Sound for an activating ??. + + health_material + A ba.Material applied to health. + """ + + _STORENAME = ba.storagename() + + @classmethod + def get(cls) -> HealthFactory: + """Get/create a shared EggFactory object.""" + activity = ba.getactivity() + factory = activity.customdata.get(cls._STORENAME) + if factory is None: + factory = HealthFactory() + activity.customdata[cls._STORENAME] = factory + assert isinstance(factory, HealthFactory) + return factory + + + + def __init__(self) -> None: + """Instantiate a BombFactory. + + You shouldn't need to do this; call get_factory() + to get a shared instance. + """ + shared = SharedObjects.get() + + self.health_model = ba.getmodel('egg') + + self.health_tex = ba.gettexture('eggTex1') + + self.health_sound = ba.getsound('activateBeep') + + # Set up our material so new bombs don't collide with objects + # that they are initially overlapping. + self.health_material = ba.Material() + + self.health_material.add_actions( + conditions=( + ( + ('we_are_younger_than', 100), + 'or', + ('they_are_younger_than', 100), + ), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + + # We want pickup materials to always hit us even if we're currently + # not colliding with their node. (generally due to the above rule) + self.health_material.add_actions( + conditions=('they_have_material', shared.pickup_material), + actions=('modify_part_collision', 'use_node_collide', False), + ) + + self.health_material.add_actions(actions=('modify_part_collision', + 'friction', 0.3)) + +class HealthBox(ba.Actor): + + def __init__(self, position: Sequence[float] = (0, 1, 0), + velocity: Sequence[float] = (0, 0, 0), + texture: str = 'powerupHealth'): + super().__init__() + + shared = SharedObjects.get() + factory = HealthFactory.get() + self.healthbox_material = ba.Material() + self.healthbox_material.add_actions(conditions=('they_are_different_node_than_us', ),actions=(('modify_part_collision', 'collide', True))) + self.node: ba.Node = ba.newnode('prop', delegate=self, attrs={ + 'position': position, + 'velocity': velocity, + 'color_texture': ba.gettexture(texture), + 'model': ba.getmodel('powerup'), + 'light_model':ba.getmodel('powerupSimple'), + 'model_scale':1, + 'body': 'crate', + 'body_scale':1, + 'density':1, + 'damping':0, + 'gravity_scale':1, + 'reflection': 'powerup', + 'reflection_scale': [0.5], + 'shadow_size': 0.0, + 'materials': (shared.object_material,self.healthbox_material,factory.health_material)}) + + self.light = ba.newnode('light', owner=self.node, attrs={ + 'color':(1,1,1), + 'volume_intensity_scale': 0.4, + 'intensity':0.7, + 'radius':0.0}) + self.node.connectattr('position', self.light,'position') + + self.spawn: ba.Timer = ba.Timer( + 10.0,self._check,repeat=True) + + def _check(self) -> None: + """Prevent the cube from annihilating.""" + + def handlemessage(self, msg): + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + + elif isinstance(msg, ba.OutOfBoundsMessage): + if self.node: + self.node.delete() + elif isinstance(msg, ba.HitMessage): + try: + spaz = msg._source_player + spaz.actor.node.handlemessage(ba.PowerupMessage(poweruptype='health')) + t_color = spaz.team.color + spaz.actor.node.color = t_color + ba.playsound(ba.getsound('healthPowerup'),volume=0.5) + ba.animate(self.light,'radius',{0:0.0,0.1:0.2,0.7:0}) + except: + pass + + elif isinstance(msg, ba.DroppedMessage): + spaz = msg.node.getdelegate(PlayerSpaz) + self.regen_timer = None + +class Torso(ba.Actor): + + def __init__(self, position: Sequence[float] = (0, 1, 0), + velocity: Sequence[float] = (0, 0, 0), + texture: str = 'bonesColor'): + super().__init__() + + shared = SharedObjects.get() + + self.node: ba.Node = ba.newnode('prop', delegate=self, attrs={ + 'position': position, + 'velocity': velocity, + 'color_texture': ba.gettexture(texture), + 'model': ba.getmodel('bonesTorso'), + 'model_scale':1, + 'body': 'sphere', + 'body_scale':0.5, + 'density':6, + 'damping':0, + 'gravity_scale':1, + 'reflection': 'soft', + 'reflection_scale': [0], + 'shadow_size': 0.0, + 'materials': (shared.object_material,)}) + + self.spawn: ba.Timer = ba.Timer( + 10.0,self._check,repeat=True) + + def _check(self) -> None: + """Prevent the cube from annihilating.""" + + def handlemessage(self, msg): + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + + elif isinstance(msg, ba.OutOfBoundsMessage): + if self.node: + self.node.delete() + +class Bone(ba.Actor): + + def __init__(self, position: Sequence[float] = (0, 1, 0), + velocity: Sequence[float] = (0, 0, 0), + texture: str = 'bonesColor', + style: int = 0): + super().__init__() + + shared = SharedObjects.get() + models = ['bonesUpperArm','bonesUpperLeg','bonesForeArm','bonesPelvis','bonesToes','bonesHand'] + bone = None + model = 0 + for i in models: + if model == style: + bone = models[model] + else: + model += 1 + self.node: ba.Node = ba.newnode('prop', delegate=self, attrs={ + 'position': position, + 'velocity': velocity, + 'color_texture': ba.gettexture(texture), + 'model': ba.getmodel(bone), + 'model_scale':1.5, + 'body': 'crate', + 'body_scale':0.6, + 'density':2, + 'damping':0, + 'gravity_scale':1, + 'reflection': 'soft', + 'reflection_scale': [0], + 'shadow_size': 0.0, + 'materials': (shared.object_material,)}) + + self.spawn: ba.Timer = ba.Timer( + 10.0,self._check,repeat=True) + + def _check(self) -> None: + """Prevent the cube from annihilating.""" + + def handlemessage(self, msg): + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + + elif isinstance(msg, ba.OutOfBoundsMessage): + if self.node: + self.node.delete() + +######Object##### +class Box(ba.Actor): + + def __init__(self, position: Sequence[float] = (0, 1, 0), + velocity: Sequence[float] = (0, 0, 0), + texture: str = 'powerupCurse'): + super().__init__() + + shared = SharedObjects.get() + self.dont_collide=ba.Material() + self.dont_collide.add_actions(conditions=('they_are_different_node_than_us', ),actions=(('modify_part_collision', 'collide', False))) + + self.node: ba.Node = ba.newnode('prop', delegate=self, attrs={ + 'position': position, + 'velocity': velocity, + 'color_texture': ba.gettexture(texture), + 'model': ba.getmodel('powerup'), + 'light_model': ba.getmodel('powerupSimple'), + 'model_scale':4, + 'body': 'box', + 'body_scale':3, + 'density':9999, + 'damping':9999, + 'gravity_scale':0, + 'reflection': 'soft', + 'reflection_scale': [0.25], + 'shadow_size': 0.0, + 'materials': [self.dont_collide,]}) + +###Map### +class HotBombMap(ba.Map): + """Stadium map for football games.""" + from bastd.mapdata import football_stadium as defs + + name = 'Hot Bomb Map' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['hotbomb'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'footballStadiumPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('footballStadium'), + 'vr_fill_model': ba.getmodel('footballStadiumVRFill'), + 'collide_model': ba.getcollidemodel('footballStadiumCollide'), + 'tex': ba.gettexture('footballStadium') + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'model': self.preloaddata['model'], + 'collide_model': self.preloaddata['collide_model'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['vr_fill_model'], + 'lighting': False, + 'vr_only': True, + 'background': True, + 'color_texture': self.preloaddata['tex'] + }) + + #TEXT + text = ba.newnode('text', + attrs={'position':(0,2.5,-6), + 'text':'Hot Bomb by\nSEBASTIAN2059 and zPanxo', + 'in_world':True, + 'shadow':1.0, + 'flatness':0.7, + 'color':(1.91,1.31,0.59), + 'opacity':0.25-0.15, + 'scale':0.013+0.007, + 'h_align':'center'}) + + #################################### + + self._wall_material=ba.Material() + self._wall_material.add_actions(conditions=('we_are_older_than', 1), actions=(('modify_part_collision', 'collide', True))) + + self._reaction_material = ba.Material() + self._reaction_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('modify_part_collision', 'collide', + True), ('call', 'at_connect', self._reaction))) + self._check_region_material = ba.Material() + # self._check_region_material.add_actions( + # conditions=('they_have_material', shared.object_material), + # actions=(('modify_part_collision', 'collide', + # True), ('modify_part_collision', 'physical', False), + # ('call', 'at_connect', self._reset_count))) + #################################### + z = ba.newnode('region',attrs={'position': (11,5.5,0),'scale': (4.5,11,13),'type': 'box','materials': (self._wall_material,)}) + z1 = ba.newnode('region',attrs={'position': (-11,5.5,0),'scale': (4.5,11,13),'type': 'box','materials': (self._wall_material,)}) + z2 = ba.newnode('region',attrs={'position': (0,5.5,-6.1),'scale': (19,11,1),'type': 'box','materials': (self._wall_material,)}) + z3 = ba.newnode('region',attrs={'position': (0,5.5,6.5),'scale': (19,11,1),'type': 'box','materials': (self._wall_material,)}) + #################################### + for i in [-5,-2.5,0,2.5,5]: + pos = (11,6.5,0) + Box(position=(pos[0]-0.5,pos[1]-5.5,pos[2]+i),texture='powerupPunch') + Box(position=(pos[0]-0.5,pos[1]-3,pos[2]+i),texture='powerupPunch') + Box(position=(pos[0]-0.5,pos[1]-0.5,pos[2]+i),texture='powerupPunch') + pos = (-11,6.5,0) + Box(position=(pos[0]+0.5,pos[1]-5.5,pos[2]+i),texture='powerupIceBombs') + Box(position=(pos[0]+0.5,pos[1]-3,pos[2]+i),texture='powerupIceBombs') + Box(position=(pos[0]+0.5,pos[1]-0.5,pos[2]+i),texture='powerupIceBombs') + #################################### + gnode = ba.getactivity().globalsnode + gnode.tint = (1.3, 1.2, 1.0) + gnode.ambient_color = (1.3, 1.2, 1.0) + gnode.vignette_outer = (0.57, 0.57, 0.57) + gnode.vignette_inner = (0.9, 0.9, 0.9) + gnode.vr_camera_offset = (0, -0.8, -1.1) + gnode.vr_near_clip = 0.5 + + def _reaction(self): + node: ba.Node = ba.getcollision().opposingnode + ba.playsound(ba.getsound('hiss'),volume=0.75) + + node.handlemessage("impulse",node.position[0],node.position[1],node.position[2], + -node.velocity[0],-node.velocity[1],-node.velocity[2], + 100,100,0,0,-node.velocity[0],-node.velocity[1],-node.velocity[2]) + + ba.emitfx(position=node.position,count=20,scale=1.5,spread=0.5,chunk_type='sweat') + + def is_point_near_edge(self, + point: ba.Vec3, + running: bool = False) -> bool: + box_position = self.defs.boxes['edge_box'][0:3] + box_scale = self.defs.boxes['edge_box'][6:9] + xpos = (point.x - box_position[0]) / box_scale[0] + zpos = (point.z - box_position[2]) / box_scale[2] + return xpos < -0.5 or xpos > 0.5 or zpos < -0.5 or zpos > 0.5 + +ba._map.register_map(HotBombMap) \ No newline at end of file diff --git a/dist/ba_root/mods/games/LaserTracer.so b/dist/ba_root/mods/games/LaserTracer.so new file mode 100644 index 0000000..0131b9d Binary files /dev/null and b/dist/ba_root/mods/games/LaserTracer.so differ diff --git a/dist/ba_root/mods/games/MemoryGame.py b/dist/ba_root/mods/games/MemoryGame.py index b04d9cb..1460421 100644 --- a/dist/ba_root/mods/games/MemoryGame.py +++ b/dist/ba_root/mods/games/MemoryGame.py @@ -936,65 +936,3 @@ class MGgame(ba.TeamGameActivity[Player, Team]): - - - - - - -class MGdefs(): - points = {} - boxes = {} - points['flag_default'] = (0.17358, 3.75764, 1.99124) - boxes['area_of_interest_bounds'] = (0.3544110667, 4.493562578, -2.518391331) + (0.0, 0.0, 0.0) + (16.64754831, 8.06138989, 18.5029888) - boxes['map_bounds'] = (0.2608783669, 4.899663734, -3.543675157) + (0.0, 0.0, 0.0) + (29.23565494, 14.19991443, 29.92689344) - -class MGmap(ba.Map): - defs = MGdefs() - name = 'Sky Tiles' - - @classmethod - def get_play_types(cls) -> List[str]: - """Return valid play types for this map.""" - return [] - - @classmethod - def get_preview_texture_name(cls) -> str: - return 'achievementOffYouGo' - - @classmethod - def on_preload(cls) -> Any: - data: Dict[str, Any] = { - 'bgtex': ba.gettexture('menuBG'), - 'bgmodel': ba.getmodel('thePadBG') - } - return data - - def __init__(self) -> None: - super().__init__() - shared = SharedObjects.get() - self.node = ba.newnode( - 'terrain', - attrs={ - 'model': self.preloaddata['bgmodel'], - 'lighting': False, - 'background': True, - 'color_texture': self.preloaddata['bgtex'] - }) - gnode = ba.getactivity().globalsnode - gnode.tint = (1.3, 1.2, 1.0) - gnode.ambient_color = (1.3, 1.2, 1.0) - gnode.vignette_outer = (0.57, 0.57, 0.57) - gnode.vignette_inner = (0.9, 0.9, 0.9) - gnode.vr_camera_offset = (0, -0.8, -1.1) - gnode.vr_near_clip = 0.5 - - - - -# Plugin only for our map UwU - -# ba_meta export plugin -class byFreaku(ba.Plugin): - def on_app_launch(self): - ba._map.register_map(MGmap) \ No newline at end of file diff --git a/dist/ba_root/mods/games/MusicalFlags.py b/dist/ba_root/mods/games/MusicalFlags.py new file mode 100644 index 0000000..53fc80b --- /dev/null +++ b/dist/ba_root/mods/games/MusicalFlags.py @@ -0,0 +1,233 @@ +## Made by MattZ45986 on GitHub +## Ported by: Freaku / @[Just] Freak#4999 + + +#Bug Fixes & Improvements as well... + +#Join BCS: +# https://discord.gg/ucyaesh + + + + +from __future__ import annotations +from typing import TYPE_CHECKING +import _ba,ba,random,math +from bastd.actor.flag import Flag,FlagPickedUpMessage +from bastd.actor.playerspaz import PlayerSpaz +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional + + + +class Player(ba.Player['Team']): + def __init__(self) -> None: + self.done: bool = False + self.survived: bool = True + +class Team(ba.Team[Player]): + def __init__(self) -> None: + self.score = 0 + + +# ba_meta require api 6 +# ba_meta export game +class MFGame(ba.TeamGameActivity[Player, Team]): + name = 'Musical Flags' + description = "Don't be the one stuck without a flag!" + + @classmethod + def get_available_settings( + cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: + settings = [ + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.BoolSetting('Epic Mode', default=False), + ba.BoolSetting('Enable Running', default=True), + ba.BoolSetting('Enable Punching', default=False), + ba.BoolSetting('Enable Bottom Credit', True) + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ['Doom Shroom'] + + def __init__(self, settings: dict): + super().__init__(settings) + self.nodes = [] + self._dingsound = ba.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self.credit_text = bool(settings['Enable Bottom Credit']) + self._time_limit = float(settings['Time Limit']) + self.is_punch = bool(settings['Enable Punching']) + self.is_run = bool(settings['Enable Running']) + + self._textRound = ba.newnode('text', + attrs={'text': '', + 'position': (0, -38), + 'scale': 1, + 'shadow': 1.0, + 'flatness': 1.0, + 'color': (1.0, 0.0, 1.0), + 'opacity': 1, + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'v_align': 'center'}) + + self.slow_motion = self._epic_mode + # A cool music, matching our gamemode theme + self.default_music = ba.MusicType.FLAG_CATCHER + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Catch Flag for yourself' + + def get_instance_description_short(self) -> Union[str, Sequence]: + return 'Catch Flag for yourself' + + def on_player_join(self, player: Player) -> None: + if self.has_begun(): + ba.screenmessage( + ba.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.getname(full=True))]), + color=(0, 1, 0),transient=True) + player.survived = False + return + self.spawn_player(player) + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + # A departing player may trigger game-over. + self.checkEnd() + + def on_begin(self) -> None: + super().on_begin() + self.roundNum = 0 + self.numPickedUp = 0 + self.nodes = [] + self.flags = [] + self.spawned = [] + self.setup_standard_time_limit(self._time_limit) + import base64 + exec(base64.b64decode("aWYgc2VsZi5jcmVkaXRfdGV4dDoKICAgICMjIFBlb3BsZSBzdGVhbGVkIGNyZWRpdHMgc28gdGhhdHMgd2h5IEkgZW5jb2RlZCB0aGlzLi4uCiAgICAjIyBFdmVuIHRobyB0aGVyZSBpcyBhIG9wdGlvbiwgdGhleSBjaGFuZ2VkIGNyZWF0ZWQgYnkKICAgICMjIGxpa2Ugd3RmIGlzIHRoaWVyIHByb2JsZW0/PwoKICAgICMjIEFueXdheXMgaGF2ZSBhIGdvb2QgZGF5IQogICAgdCA9IGJhLm5ld25vZGUoJ3RleHQnLAogICAgICAgICAgICAgICBhdHRycz17ICd0ZXh0JzoiUG9ydGVkIGJ5IO6BiEZyZWFrdVxuTWFkZSBieSBNYXR0WjQ1OTg2IiwgIyMgRGlzYWJsZSAnRW5hYmxlIEJvdHRvbSBDcmVkaXRzJyB3aGVuIG1ha2luZyBwbGF5bGlzdCwgTm8gbmVlZCB0byBlZGl0IHRoaXMgbG92ZWx5Li4uCiAgICAgICAgJ3NjYWxlJzowLjcsCiAgICAgICAgJ3Bvc2l0aW9uJzooMCwwKSwKICAgICAgICAnc2hhZG93JzowLjUsCiAgICAgICAgJ2ZsYXRuZXNzJzoxLjIsCiAgICAgICAgJ2NvbG9yJzooMSwgMSwgMSksCiAgICAgICAgJ2hfYWxpZ24nOidjZW50ZXInLAogICAgICAgICd2X2F0dGFjaCc6J2JvdHRvbSd9KQ==").decode('UTF-8')) + self.makeRound() + self._textRound.text = 'Round ' + str(self.roundNum) + ba.timer(5, self.checkEnd) + + def makeRound(self): + for player in self.players: + if player.survived: player.team.score += 1 + self.roundNum += 1 + self._textRound.text = 'Round ' + str(self.roundNum) + self.flags = [] + self.spawned = [] + angle = random.randint(0,359) + c=0 + for player in self.players: + if player.survived: c+=1 + spacing = 10 + for player in self.players: + player.done = False + if player.survived: + if not player.is_alive(): + self.spawn_player(player,(.5,5,-4)) + self.spawned.append(player) + try: spacing = 360 // (c) + except: self.checkEnd() + colors = [(1,0,0),(0,1,0),(0,0,1),(1,1,0),(1,0,1),(0,1,1),(0,0,0),(0.5,0.8,0),(0,0.8,0.5),(0.8,0.25,0.7),(0,0.27,0.55),(2,2,0.6),(0.4,3,0.85)] + # Smart Mathematics: + # All Flags spawn same distance from the players + for i in range(c-1): + angle += spacing + angle %= 360 + x=6 * math.sin(math.degrees(angle)) + z=6 * math.cos(math.degrees(angle)) + flag = Flag(position=(x+.5,5,z-4), color=colors[i]).autoretain() + self.flags.append(flag) + + def killRound(self): + self.numPickedUp = 0 + for player in self.players: + if player.is_alive(): player.actor.handlemessage(ba.DieMessage()) + for flag in self.flags: flag.node.delete() + for light in self.nodes: light.delete() + + def spawn_player(self, player: Player, pos: tuple = (0,0,0)) -> ba.Actor: + spaz = self.spawn_player_spaz(player) + if pos == (0,0,0): + pos = (-.5+random.random()*2,3+random.random()*2,-5+random.random()*2) + spaz.connect_controls_to_player(enable_punch=self.is_punch, enable_bomb=False, enable_run=self.is_run) + spaz.handlemessage(ba.StandMessage(pos)) + return spaz + + def check_respawn(self, player): + if not player.done and player.survived: + self.respawn_player(player, 2.5) + + def handlemessage(self, msg: Any) -> Any: + + if isinstance(msg, ba.PlayerDiedMessage): + super().handlemessage(msg) + player = msg.getplayer(Player) + ba.timer(0.1, ba.Call(self.check_respawn, player)) + ba.timer(0.5, self.checkEnd) + elif isinstance(msg, FlagPickedUpMessage): + self.numPickedUp += 1 + msg.node.getdelegate(PlayerSpaz, True).getplayer(Player, True).done = True + l = ba.newnode('light', + owner=None, + attrs={'color':msg.node.color, + 'position':(msg.node.position_center), + 'intensity':1}) + self.nodes.append(l) + msg.flag.handlemessage(ba.DieMessage()) + msg.node.handlemessage(ba.DieMessage()) + msg.node.delete() + if self.numPickedUp == len(self.flags): + for player in self.spawned: + if not player.done: + try: + player.survived = False + ba.screenmessage("No Flag? "+player.getname()) + player.actor.handlemessage(ba.StandMessage((0,3,-2))) + ba.timer(0.5,ba.Call(player.actor.handlemessage, ba.FreezeMessage())) + ba.timer(3,ba.Call(player.actor.handlemessage, ba.ShouldShatterMessage())) + except: pass + ba.timer(3.5,self.killRound) + ba.timer(3.55,self.makeRound) + else: + return super().handlemessage(msg) + return None + + def checkEnd(self): + i = 0 + for player in self.players: + if player.survived: + i+=1 + if i <= 1: + for player in self.players: + if player.survived: + player.team.score += 10 + ba.timer(2.5, self.end_game) + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) diff --git a/dist/ba_root/mods/games/Reverserace.py b/dist/ba_root/mods/games/Reverserace.py new file mode 100644 index 0000000..f85e82c --- /dev/null +++ b/dist/ba_root/mods/games/Reverserace.py @@ -0,0 +1,722 @@ + +# Released under the MIT License. See LICENSE for details. +# +"""Defines Race mini-game.""" +# Created inside BCS (Bombsquad consultancy services) +# discord.gg/ucyaesh +# https://bombsquad.ga +# - Reverse Race by Mr.Smoothy +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING +from dataclasses import dataclass + +import ba +from bastd.actor.bomb import Bomb +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import (Any, Type, Tuple, List, Sequence, Optional, Dict, + Union) + from bastd.actor.onscreentimer import OnScreenTimer + + +@dataclass +class RaceMine: + """Holds info about a mine on the track.""" + point: Sequence[float] + mine: Optional[Bomb] + + +class RaceRegion(ba.Actor): + """Region used to track progress during a race.""" + + def __init__(self, pt: Sequence[float], index: int): + super().__init__() + activity = self.activity + assert isinstance(activity, ReverseRaceGame) + self.pos = pt + self.index = index + self.node = ba.newnode( + 'region', + delegate=self, + attrs={ + 'position': pt[:3], + 'scale': (pt[3] * 2.0, pt[4] * 2.0, pt[5] * 2.0), + 'type': 'box', + 'materials': [activity.race_region_material] + }) + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.distance_txt: Optional[ba.Node] = None + self.last_region = 0 + self.lap = 0 + self.distance = 0.0 + self.finished = False + self.rank: Optional[int] = None + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.time: Optional[float] = None + self.lap = 0 + self.finished = False + + +# ba_meta export game +class ReverseRaceGame(ba.TeamGameActivity[Player, Team]): + """Game of racing around a track.""" + + name = 'Reverse Race' + description = 'Run real fast! \n but in reverse direction' + scoreconfig = ba.ScoreConfig(label='Time', + lower_is_better=True, + scoretype=ba.ScoreType.MILLISECONDS) + + @classmethod + def get_available_settings( + cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: + settings = [ + ba.IntSetting('Laps', min_value=1, default=3, increment=1), + ba.IntChoiceSetting( + 'Time Limit', + default=0, + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + ), + ba.IntChoiceSetting( + 'Mine Spawning', + default=4000, + choices=[ + ('No Mines', 0), + ('8 Seconds', 8000), + ('4 Seconds', 4000), + ('2 Seconds', 2000), + ], + ), + ba.IntChoiceSetting( + 'Bomb Spawning', + choices=[ + ('None', 0), + ('8 Seconds', 8000), + ('4 Seconds', 4000), + ('2 Seconds', 2000), + ('1 Second', 1000), + ], + default=2000, + ), + ba.BoolSetting('Epic Mode', default=False), + ] + + # We have some specific settings in teams mode. + if issubclass(sessiontype, ba.DualTeamSession): + settings.append( + ba.BoolSetting('Entire Team Must Finish', default=False)) + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.MultiTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ba.getmaps('race') + + def __init__(self, settings: dict): + self._race_started = False + super().__init__(settings) + self._scoreboard = Scoreboard() + self._score_sound = ba.getsound('score') + self._swipsound = ba.getsound('swip') + self._last_team_time: Optional[float] = None + self._front_race_region: Optional[int] = None + self._nub_tex = ba.gettexture('nub') + self._beep_1_sound = ba.getsound('raceBeep1') + self._beep_2_sound = ba.getsound('raceBeep2') + self.race_region_material: Optional[ba.Material] = None + self._regions: List[RaceRegion] = [] + self._team_finish_pts: Optional[int] = None + self._time_text: Optional[ba.Actor] = None + self._timer: Optional[OnScreenTimer] = None + self._race_mines: Optional[List[RaceMine]] = None + self._race_mine_timer: Optional[ba.Timer] = None + self._scoreboard_timer: Optional[ba.Timer] = None + self._player_order_update_timer: Optional[ba.Timer] = None + self._start_lights: Optional[List[ba.Node]] = None + self._bomb_spawn_timer: Optional[ba.Timer] = None + self._laps = int(settings['Laps']) + self._entire_team_must_finish = bool( + settings.get('Entire Team Must Finish', False)) + self._time_limit = float(settings['Time Limit']) + self._mine_spawning = int(settings['Mine Spawning']) + self._bomb_spawning = int(settings['Bomb Spawning']) + self._epic_mode = bool(settings['Epic Mode']) + + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC_RACE + if self._epic_mode else ba.MusicType.RACE) + + def get_instance_description(self) -> Union[str, Sequence]: + if (isinstance(self.session, ba.DualTeamSession) + and self._entire_team_must_finish): + t_str = ' Your entire team has to finish.' + else: + t_str = '' + + if self._laps > 1: + return 'Run ${ARG1} laps.' + t_str, self._laps + return 'Run 1 lap.' + t_str + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._laps > 1: + return 'run ${ARG1} laps in reverse direction', self._laps + return 'run 1 lap' + + def on_transition_in(self) -> None: + super().on_transition_in() + shared = SharedObjects.get() + pts = self.map.get_def_points('race_point') + mat = self.race_region_material = ba.Material() + mat.add_actions(conditions=('they_have_material', + shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', + self._handle_race_point_collide), + )) + self._regions.append(RaceRegion(pts[0], len(self._regions))) + + pts.reverse() + for rpt in pts[:-1]: + self._regions.append(RaceRegion(rpt, len(self._regions))) + + def _flash_player(self, player: Player, scale: float) -> None: + assert isinstance(player.actor, PlayerSpaz) + assert player.actor.node + pos = player.actor.node.position + light = ba.newnode('light', + attrs={ + 'position': pos, + 'color': (1, 1, 0), + 'height_attenuated': False, + 'radius': 0.4 + }) + ba.timer(0.5, light.delete) + ba.animate(light, 'intensity', {0: 0, 0.1: 1.0 * scale, 0.5: 0}) + + def _handle_race_point_collide(self) -> None: + # FIXME: Tidy this up. + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + # pylint: disable=too-many-nested-blocks + collision = ba.getcollision() + try: + region = collision.sourcenode.getdelegate(RaceRegion, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except ba.NotFoundError: + return + + last_region = player.last_region + this_region = region.index + + if last_region != this_region: + + # If a player tries to skip regions, smite them. + # Allow a one region leeway though (its plausible players can get + # blown over a region, etc). + if this_region > last_region + 2: + if player.is_alive(): + assert player.actor + player.actor.handlemessage(ba.DieMessage()) + ba.screenmessage(ba.Lstr( + translate=('statements', 'Killing ${NAME} for' + ' skipping part of the track!'), + subs=[('${NAME}', player.getname(full=True))]), + color=(1, 0, 0)) + else: + # If this player is in first, note that this is the + # front-most race-point. + if player.rank == 0: + self._front_race_region = this_region + + player.last_region = this_region + if last_region >= len(self._regions) - 2 and this_region == 0: + team = player.team + player.lap = min(self._laps, player.lap + 1) + + # In teams mode with all-must-finish on, the team lap + # value is the min of all team players. + # Otherwise its the max. + if isinstance(self.session, ba.DualTeamSession + ) and self._entire_team_must_finish: + team.lap = min([p.lap for p in team.players]) + else: + team.lap = max([p.lap for p in team.players]) + + # A player is finishing. + if player.lap == self._laps: + + # In teams mode, hand out points based on the order + # players come in. + if isinstance(self.session, ba.DualTeamSession): + assert self._team_finish_pts is not None + if self._team_finish_pts > 0: + self.stats.player_scored(player, + self._team_finish_pts, + screenmessage=False) + self._team_finish_pts -= 25 + + # Flash where the player is. + self._flash_player(player, 1.0) + player.finished = True + assert player.actor + player.actor.handlemessage( + ba.DieMessage(immediate=True)) + + # Makes sure noone behind them passes them in rank + # while finishing. + player.distance = 9999.0 + + # If the whole team has finished the race. + if team.lap == self._laps: + ba.playsound(self._score_sound) + player.team.finished = True + assert self._timer is not None + elapsed = ba.time() - self._timer.getstarttime() + self._last_team_time = player.team.time = elapsed + self._check_end_game() + + # Team has yet to finish. + else: + ba.playsound(self._swipsound) + + # They've just finished a lap but not the race. + else: + ba.playsound(self._swipsound) + self._flash_player(player, 0.3) + + # Print their lap number over their head. + try: + assert isinstance(player.actor, PlayerSpaz) + mathnode = ba.newnode('math', + owner=player.actor.node, + attrs={ + 'input1': (0, 1.9, 0), + 'operation': 'add' + }) + player.actor.node.connectattr( + 'torso_position', mathnode, 'input2') + tstr = ba.Lstr(resource='lapNumberText', + subs=[('${CURRENT}', + str(player.lap + 1)), + ('${TOTAL}', str(self._laps)) + ]) + txtnode = ba.newnode('text', + owner=mathnode, + attrs={ + 'text': tstr, + 'in_world': True, + 'color': (1, 1, 0, 1), + 'scale': 0.015, + 'h_align': 'center' + }) + mathnode.connectattr('output', txtnode, 'position') + ba.animate(txtnode, 'scale', { + 0.0: 0, + 0.2: 0.019, + 2.0: 0.019, + 2.2: 0 + }) + ba.timer(2.3, mathnode.delete) + except Exception: + ba.print_exception('Error printing lap.') + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def on_player_leave(self, player: Player) -> None: + super().on_player_leave(player) + + # A player leaving disqualifies the team if 'Entire Team Must Finish' + # is on (otherwise in teams mode everyone could just leave except the + # leading player to win). + if (isinstance(self.session, ba.DualTeamSession) + and self._entire_team_must_finish): + ba.screenmessage(ba.Lstr( + translate=('statements', + '${TEAM} is disqualified because ${PLAYER} left'), + subs=[('${TEAM}', player.team.name), + ('${PLAYER}', player.getname(full=True))]), + color=(1, 1, 0)) + player.team.finished = True + player.team.time = None + player.team.lap = 0 + ba.playsound(ba.getsound('boo')) + for otherplayer in player.team.players: + otherplayer.lap = 0 + otherplayer.finished = True + try: + if otherplayer.actor is not None: + otherplayer.actor.handlemessage(ba.DieMessage()) + except Exception: + ba.print_exception('Error sending DieMessage.') + + # Defer so team/player lists will be updated. + ba.pushcall(self._check_end_game) + + def _update_scoreboard(self) -> None: + for team in self.teams: + distances = [player.distance for player in team.players] + if not distances: + teams_dist = 0.0 + else: + if (isinstance(self.session, ba.DualTeamSession) + and self._entire_team_must_finish): + teams_dist = min(distances) + else: + teams_dist = max(distances) + self._scoreboard.set_team_value( + team, + teams_dist, + self._laps, + flash=(teams_dist >= float(self._laps)), + show_value=False) + + def on_begin(self) -> None: + from bastd.actor.onscreentimer import OnScreenTimer + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + self._team_finish_pts = 100 + + # Throw a timer up on-screen. + self._time_text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'color': (1, 1, 0.5, 1), + 'flatness': 0.5, + 'shadow': 0.5, + 'position': (0, -50), + 'scale': 1.4, + 'text': '' + })) + self._timer = OnScreenTimer() + + if self._mine_spawning != 0: + self._race_mines = [ + RaceMine(point=p, mine=None) + for p in self.map.get_def_points('race_mine') + ] + if self._race_mines: + self._race_mine_timer = ba.Timer(0.001 * self._mine_spawning, + self._update_race_mine, + repeat=True) + + self._scoreboard_timer = ba.Timer(0.25, + self._update_scoreboard, + repeat=True) + self._player_order_update_timer = ba.Timer(0.25, + self._update_player_order, + repeat=True) + + if self.slow_motion: + t_scale = 0.4 + light_y = 50 + else: + t_scale = 1.0 + light_y = 150 + lstart = 7.1 * t_scale + inc = 1.25 * t_scale + + ba.timer(lstart, self._do_light_1) + ba.timer(lstart + inc, self._do_light_2) + ba.timer(lstart + 2 * inc, self._do_light_3) + ba.timer(lstart + 3 * inc, self._start_race) + + self._start_lights = [] + for i in range(4): + lnub = ba.newnode('image', + attrs={ + 'texture': ba.gettexture('nub'), + 'opacity': 1.0, + 'absolute_scale': True, + 'position': (-75 + i * 50, light_y), + 'scale': (50, 50), + 'attach': 'center' + }) + ba.animate( + lnub, 'opacity', { + 4.0 * t_scale: 0, + 5.0 * t_scale: 1.0, + 12.0 * t_scale: 1.0, + 12.5 * t_scale: 0.0 + }) + ba.timer(13.0 * t_scale, lnub.delete) + self._start_lights.append(lnub) + + self._start_lights[0].color = (0.2, 0, 0) + self._start_lights[1].color = (0.2, 0, 0) + self._start_lights[2].color = (0.2, 0.05, 0) + self._start_lights[3].color = (0.0, 0.3, 0) + + def _do_light_1(self) -> None: + assert self._start_lights is not None + self._start_lights[0].color = (1.0, 0, 0) + ba.playsound(self._beep_1_sound) + + def _do_light_2(self) -> None: + assert self._start_lights is not None + self._start_lights[1].color = (1.0, 0, 0) + ba.playsound(self._beep_1_sound) + + def _do_light_3(self) -> None: + assert self._start_lights is not None + self._start_lights[2].color = (1.0, 0.3, 0) + ba.playsound(self._beep_1_sound) + + def _start_race(self) -> None: + assert self._start_lights is not None + self._start_lights[3].color = (0.0, 1.0, 0) + ba.playsound(self._beep_2_sound) + for player in self.players: + if player.actor is not None: + try: + assert isinstance(player.actor, PlayerSpaz) + player.actor.connect_controls_to_player() + except Exception: + ba.print_exception('Error in race player connects.') + assert self._timer is not None + self._timer.start() + + if self._bomb_spawning != 0: + self._bomb_spawn_timer = ba.Timer(0.001 * self._bomb_spawning, + self._spawn_bomb, + repeat=True) + + self._race_started = True + + def _update_player_order(self) -> None: + + # Calc all player distances. + for player in self.players: + pos: Optional[ba.Vec3] + try: + pos = player.position + except ba.NotFoundError: + pos = None + if pos is not None: + r_index = player.last_region + rg1 = self._regions[r_index] + r1pt = ba.Vec3(rg1.pos[:3]) + rg2 = self._regions[0] if r_index == len( + self._regions) - 1 else self._regions[r_index + 1] + r2pt = ba.Vec3(rg2.pos[:3]) + r2dist = (pos - r2pt).length() + amt = 1.0 - (r2dist / (r2pt - r1pt).length()) + amt = player.lap + (r_index + amt) * (1.0 / len(self._regions)) + player.distance = amt + + # Sort players by distance and update their ranks. + p_list = [(player.distance, player) for player in self.players] + + p_list.sort(reverse=True, key=lambda x: x[0]) + for i, plr in enumerate(p_list): + plr[1].rank = i + if plr[1].actor: + node = plr[1].distance_txt + if node: + node.text = str(i + 1) if plr[1].is_alive() else '' + + def _spawn_bomb(self) -> None: + if self._front_race_region is None: + return + region = (self._front_race_region + 3) % len(self._regions) + pos = self._regions[region].pos + + # Don't use the full region so we're less likely to spawn off a cliff. + region_scale = 0.8 + x_range = ((-0.5, 0.5) if pos[3] == 0 else + (-region_scale * pos[3], region_scale * pos[3])) + z_range = ((-0.5, 0.5) if pos[5] == 0 else + (-region_scale * pos[5], region_scale * pos[5])) + pos = (pos[0] + random.uniform(*x_range), pos[1] + 1.0, + pos[2] + random.uniform(*z_range)) + ba.timer(random.uniform(0.0, 2.0), + ba.WeakCall(self._spawn_bomb_at_pos, pos)) + + def _spawn_bomb_at_pos(self, pos: Sequence[float]) -> None: + if self.has_ended(): + return + Bomb(position=pos, bomb_type='normal').autoretain() + + def _make_mine(self, i: int) -> None: + assert self._race_mines is not None + rmine = self._race_mines[i] + rmine.mine = Bomb(position=rmine.point[:3], bomb_type='land_mine') + rmine.mine.arm() + + def _flash_mine(self, i: int) -> None: + assert self._race_mines is not None + rmine = self._race_mines[i] + light = ba.newnode('light', + attrs={ + 'position': rmine.point[:3], + 'color': (1, 0.2, 0.2), + 'radius': 0.1, + 'height_attenuated': False + }) + ba.animate(light, 'intensity', {0.0: 0, 0.1: 1.0, 0.2: 0}, loop=True) + ba.timer(1.0, light.delete) + + def _update_race_mine(self) -> None: + assert self._race_mines is not None + m_index = -1 + rmine = None + for _i in range(3): + m_index = random.randrange(len(self._race_mines)) + rmine = self._race_mines[m_index] + if not rmine.mine: + break + assert rmine is not None + if not rmine.mine: + self._flash_mine(m_index) + ba.timer(0.95, ba.Call(self._make_mine, m_index)) + + def spawn_player(self, player: Player) -> ba.Actor: + if player.team.finished: + # FIXME: This is not type-safe! + # This call is expected to always return an Actor! + # Perhaps we need something like can_spawn_player()... + # noinspection PyTypeChecker + return None # type: ignore + pos = self._regions[player.last_region].pos + + # Don't use the full region so we're less likely to spawn off a cliff. + region_scale = 0.8 + x_range = ((-0.5, 0.5) if pos[3] == 0 else + (-region_scale * pos[3], region_scale * pos[3])) + z_range = ((-0.5, 0.5) if pos[5] == 0 else + (-region_scale * pos[5], region_scale * pos[5])) + pos = (pos[0] + random.uniform(*x_range), pos[1], + pos[2] + random.uniform(*z_range)) + spaz = self.spawn_player_spaz( + player, position=pos, angle=270 if not self._race_started else None) + assert spaz.node + + # Prevent controlling of characters before the start of the race. + if not self._race_started: + spaz.disconnect_controls_from_player() + + mathnode = ba.newnode('math', + owner=spaz.node, + attrs={ + 'input1': (0, 1.4, 0), + 'operation': 'add' + }) + spaz.node.connectattr('torso_position', mathnode, 'input2') + + distance_txt = ba.newnode('text', + owner=spaz.node, + attrs={ + 'text': '', + 'in_world': True, + 'color': (1, 1, 0.4), + 'scale': 0.02, + 'h_align': 'center' + }) + player.distance_txt = distance_txt + mathnode.connectattr('output', distance_txt, 'position') + return spaz + + def _check_end_game(self) -> None: + + # If there's no teams left racing, finish. + teams_still_in = len([t for t in self.teams if not t.finished]) + if teams_still_in == 0: + self.end_game() + return + + # Count the number of teams that have completed the race. + teams_completed = len( + [t for t in self.teams if t.finished and t.time is not None]) + + if teams_completed > 0: + session = self.session + + # In teams mode its over as soon as any team finishes the race + + # FIXME: The get_ffa_point_awards code looks dangerous. + if isinstance(session, ba.DualTeamSession): + self.end_game() + else: + # In ffa we keep the race going while there's still any points + # to be handed out. Find out how many points we have to award + # and how many teams have finished, and once that matches + # we're done. + assert isinstance(session, ba.FreeForAllSession) + points_to_award = len(session.get_ffa_point_awards()) + if teams_completed >= points_to_award - teams_completed: + self.end_game() + return + + def end_game(self) -> None: + + # Stop updating our time text, and set it to show the exact last + # finish time if we have one. (so users don't get upset if their + # final time differs from what they see onscreen by a tiny amount) + assert self._timer is not None + if self._timer.has_started(): + self._timer.stop( + endtime=None if self._last_team_time is None else ( + self._timer.getstarttime() + self._last_team_time)) + + results = ba.GameResults() + + for team in self.teams: + if team.time is not None: + # We store time in seconds, but pass a score in milliseconds. + results.set_team_score(team, int(team.time * 1000.0)) + else: + results.set_team_score(team, None) + + # We don't announce a winner in ffa mode since its probably been a + # while since the first place guy crossed the finish line so it seems + # odd to be announcing that now. + self.end(results=results, + announce_winning_team=isinstance(self.session, + ba.DualTeamSession)) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + # Augment default behavior. + super().handlemessage(msg) + player = msg.getplayer(Player) + if not player.finished: + self.respawn_player(player, respawn_time=1) + else: + super().handlemessage(msg) diff --git a/dist/ba_root/mods/games/Tower_Rush.py b/dist/ba_root/mods/games/Tower_Rush.py new file mode 100644 index 0000000..961d50c --- /dev/null +++ b/dist/ba_root/mods/games/Tower_Rush.py @@ -0,0 +1,395 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Defines assault minigame.""" + +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba, random +from math import cos, sin +from bastd.actor.popuptext import PopupText +from bastd.actor.powerupbox import PowerupBox, PowerupBoxFactory +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Sequence, Union + +class Player(ba.Player['Team']): + ... + +class Team(ba.Team[Player]): + def __init__(self) -> None: + self.score = 0 + self.stored_score = 0 + self.role: str = '' + self.spot: str = '' + + +# ba_meta export game +class BaseRaidGame(ba.TeamGameActivity[Player, Team]): + + name = 'Tower Rush' + description = """If "score" is shown on your character,\ngo through inside the enemy team's castle-like structure\nto score. Otherwise if "defend" is shown\nprevent the enemy team from entering.""" + available_settings = [ + ba.IntSetting('Score to Win Per Team', min_value=1, default=10), + ba.IntChoiceSetting('Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200)], + default=300), + ba.IntSetting('Countdown Time Each Round', min_value = 10, increment=5 ,default = 60), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0)], + default=1.0), + ba.BoolSetting('Epic Mode', default=False), + ba.BoolSetting('Kill player on ground', default=True), + ba.BoolSetting('Random Spawn Point (Team)', default=False), + ba.BoolSetting('[On score] Single Teleport', default=True), + ba.BoolSetting('[On score] Single Celebrate', default=True)] + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ['Tower D'] + + def get_instance_scoreboard_display_string(self) -> ba.Lstr: + return "Score " + str(self._score_to_win) + " Points" + + def get_instance_description(self) -> Union[str, Sequence]: + return "if Score, score by reaching through the foe's structure\nif Defend, prevent enemy from scoring." + + def __init__(self, settings: dict): + super().__init__(settings) + self.get_s = settings + self._scoreboard = Scoreboard() + self._last_score_time = 0.0 + self._score_sound = ba.getsound('score') + self._epic_mode = bool(settings['Epic Mode']) + self._score_to_win = int(settings['Score to Win Per Team']) + self._time_limit = float(settings['Time Limit']) + self.time = int(settings['Countdown Time Each Round']) + self.change_spawn_points = bool(settings['Random Spawn Point (Team)']) + self.ground = bool(settings['Kill player on ground']) + self.loop_timer = self.knocker = None + + self.disable_controls = bool(True) + + # Base class overrides + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC if self._epic_mode else + ba.MusicType.FORWARD_MARCH) + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def on_player_join(self, player: PlayerType) -> None: + if not self.disable_controls: + self.spawn_player(player) + else: + pass + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_powerup_drops() + palyer = SharedObjects.get().player_material + + death_material = ba.Material(); box_1 = ba.Material(); box_2 = ba.Material() + death_material.add_actions( + conditions=('they_have_material', palyer), + actions=(('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', ba.WeakCall(self.handle_region, 'ground')))) + box_1.add_actions( + conditions=('they_have_material', palyer), + actions=(('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', ba.WeakCall(self.handle_region, 'turret_1')))) + box_2.add_actions( + conditions=('they_have_material', palyer), + actions=(('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', False), + ('call', 'at_connect', ba.WeakCall(self.handle_region, 'turret_2')))) + + self.r: List[ba.NodeActor] = [] + self.r.append(ba.NodeActor(ba.newnode('region', + attrs={'position': (-8.450964, 4.784977, 0.300695), + 'scale': (0.2, 3.45, 1.28), + 'type': 'box', + 'materials':[box_1]}))) + self.r.append(ba.NodeActor(ba.newnode('region', + attrs={'position': (7.470666, 3.429754, 0.328592), + 'scale': (0.2, 2.47, 1.39), + 'type': 'box', + 'materials':[box_2]}))) + if self.ground: + self.r.append(ba.NodeActor(ba.newnode('region', + attrs={'position': (-0.588583, 1.569668, 4.049047), + 'scale': (18.0, 0.1, 5.0), + 'type': 'box', + 'materials':[death_material]}))) + self.r.append(ba.NodeActor(ba.newnode('region', + attrs={'position': (-0.128583, 1.569668, -1.670954), + 'scale': (5.0, 0.1, 7.0), + 'type': 'box', + 'materials':[death_material]}))) + + self.r_timer = ba.newnode('text', + attrs={ + 'v_attach': 'top', + 'h_attach': 'center', + 'h_align': 'center', + 'color': (0.7,0.88,1.0,1.1), + 'shadow': 1.0, + 'flatness': 1.0, + 'position': (0, -120), + 'scale': 1.7, + 'text': "..."}) + self.setup_standard_time_limit(self._time_limit) + ba.timer(4.5, ba.WeakCall(self.selection, first_run = True)) + teams = [] + for t in self.teams: + teams.append(t) + roles = ['raid', 'defend'] + if self.change_spawn_points: + spots = ['spot2','spot1'] + + teams[0].spot = spots[random.randint(0,1)] + spots.remove(teams[0].spot) + teams[1].spot = spots[0] + else: + teams[0].spot = 'spot1' + teams[1].spot = 'spot2' + + + teams[0].role = roles[random.randint(0, 1)] + roles.remove(teams[0].role) + teams[1].role = roles[0] + def launch(): + for t in self.teams: + if t.role == 'raid': + ba.screenmessage(t.name.evaluate() + " Team, will be the goaler", color=t.color) + elif t.role == 'defend': + ba.screenmessage(t.name.evaluate() + " Team, will be the defender", color=t.color) + ba.timer(1.9, launch) + + def knock_players(self) -> None: + for p in self.players: + if p.is_alive(): + p.actor.node.handlemessage('knockout', 800.0) + + def decrease(self) -> None: + if int(self.time) != 0: + self.time -= 1 + self.r_timer.text = "Round Time: " + str(self.time) + else: + self.loop_timer = None + self.disable_controls = True + self.r_timer.text = "Round Ended" + self.knock_players() + self.knocker = ba.Timer(0.8, ba.Call(self.knock_players), repeat=True) + ba.timer(1, lambda: self.selection()) + + def get_pos(self, player: Player, center: bool = False) -> None: + + ang = random.randint(0, 360) + rad = random.uniform(0, 2.0) if not center else 0.0 + y = random.uniform(0.9, 1.16) + if player.team.spot == 'spot1': + x = cos(ang) * rad + z = sin(ang) * rad + pos = (-5.786582 + x, 2.8651068 + y, -0.7751054 + z) + elif player.team.spot == 'spot2': + ang = random.randint(0, 360) + rad = random.uniform(0, 2.0) + x = cos(ang) * rad + z = sin(ang) * rad + pos = (4.37031 + x, 2.51977 + y, -0.1901849 + z) + return pos + + + def selection(self, first_run: bool = False) -> None: + + def scene(): + self.disable_controls = True + teams = [] + for team in self.teams: + teams.append(team) + + spots = ['spot1', 'spot2'] + + if self.change_spawn_points: + teams[0].spot = spots[random.randint(0, 1)] + spots.remove(teams[0].spot) + teams[1].spot = spots[0] + + if not first_run: + temp2 = teams[0].role + teams[0].role = teams[1].role + + teams[1].role = temp2 + + def func(): + for t in self.teams: + if not first_run: + if t.role == 'raid': + ba.screenmessage( + "It's now " + t.name.evaluate() + " Team's turn, to score. ", + color=(0.5, 0.8, 0.8)) + break + ba.timer(0.926, func) + + def scene_2(): + for t in self.teams: + t.stored_score = 0 + for p in self.players: + if p.actor is None: + self.spawn_player(p) + elif p.is_alive(): + p.actor.handlemessage(ba.StandMessage(position=self.get_pos(p), angle=random.uniform(0, 360))) + + def delay(): + for get_s in self.players: + if get_s.is_alive(): + if get_s.team.role == 'raid': + PopupText(position=get_s.actor.node.position, text="Score!", color=get_s.team.color, scale=1.5).autoretain() + else: + PopupText(position=get_s.actor.node.position, text="Defend!", color=get_s.team.color, scale=1.5).autoretain() + + self.r_timer.text = "Round Time: " + str(self.time) + self.disable_controls = False + def a(): self.knocker = None + ba.timer(0.9, a) + ba.timer(0.488 if first_run else 0.67, delay) + + self.time = self.get_s['Countdown Time Each Round'] + self.loop_timer = ba.Timer(1, ba.Call(self.decrease), repeat=True) + + def text(): + for team in self.teams: + if not first_run: + if team.role == 'raid': + ba.screenmessage( + team.name.evaluate() + " Team secured " + str(team.stored_score) + " goals in " + str(self.get_s["Countdown Time Each Round"]) + " seconds.", + color=(0.4, 0.6, 1.0) + ) + break + + ba.timer(1.033, text) + ba.timer(2, scene) + ba.timer(4.858 if not first_run else 3, scene_2) + + def spawn_player(self, player: Player) -> ba.Actor: + spaz = self.spawn_player_spaz(player, position=self.get_pos(player)) + return spaz + + def teleport(self, player: Player) -> None: + if self.get_s["[On score] Single Teleport"]: + for this in player.team.players: + if this.actor: + this.actor.handlemessage(ba.StandMessage(position=self.get_pos(this), angle=random.uniform(0, 360))) + else: + if player.actor: + player.actor.handlemessage(ba.StandMessage(position=self.get_pos(player), angle=random.uniform(0, 360))) + + if self.get_s['[On score] Single Celebrate']: + for this in player.team.players: + if this.actor: + this.actor.handlemessage(ba.CelebrateMessage(2.0)) + else: + if player.actor: + player.actor.handlemessage(ba.CelebrateMessage(2.0)) + self.stats.player_scored(player, 10, big_message=True) + + def handle_region(self, region: str) -> None: + try: + player = ba.getcollision().opposingnode.getdelegate( + PlayerSpaz, True).getplayer(Player, True) + except: return + + if region == 'ground' and not self.disable_controls: + def delay(): + player.actor.handlemessage(ba.DieMessage()) + ba.timer(10*0.001, delay) + + elif region == 'turret_2': + if player.is_alive(): + if player.team.role == 'raid' and player.team.spot != 'spot2': + if ba.time() != self._last_score_time: + self._last_score_time = ba.time() + ba.playsound(self._score_sound) + player.team.score += 1 + player.team.stored_score += 1 + self._update_scoreboard() + self.teleport(player) + else: + return + + elif region == 'turret_1': + if player.is_alive(): + if player.team.role == 'raid' and player.team.spot != 'spot1': + if ba.time() != self._last_score_time: + self._last_score_time = ba.time() + ba.playsound(self._score_sound) + player.team.score += 1 + player.team.stored_score += 1 + self._update_scoreboard() + self.teleport(player) + + def _standard_drop_powerup(self, index: int, expire: bool = True) -> None: + self.i = [(6.1527314, 4.9323297, -7.8393626), + (-6.8911242, 4.4702456, -7.8909187), + (-5.3111238, 3.930246, -6.170918), + (3.9788754, 3.8102467, -5.9009175), + (-4.926642821, 2.397645612, 2.876782366), + (-1.964335546, 2.397645612, 3.751374716), + (1.64883201, 2.397645612, 3.751374716), + (4.398865293, 2.397645612, 2.877618924)] + self.i = ([p[:3] for p in self.i]) + PowerupBox( + position=self.i[index], + poweruptype=PowerupBoxFactory.get().get_random_powerup_type(), + expire=expire).autoretain() + + def _standard_drop_powerups(self) -> None: + for i in range(4 if self.ground else 8): + ba.timer(i * 0.4, ba.WeakCall(self._standard_drop_powerup, i)) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + self.respawn_player(msg.getplayer(Player)) + else: + super().handlemessage(msg) + return None + + def end_game(self) -> None: + self.loop_timer = self.knocker = None + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + if team.score >= self._score_to_win: + self.end_game() \ No newline at end of file diff --git a/dist/ba_root/mods/games/VolleyBall.py b/dist/ba_root/mods/games/VolleyBall.py new file mode 100644 index 0000000..7d2ecf5 --- /dev/null +++ b/dist/ba_root/mods/games/VolleyBall.py @@ -0,0 +1,537 @@ +from __future__ import annotations + +#Volley Ball (updated) +#Made for 1.6 by your friend: @[Just] Freak#4999 + +"""Defines Volley Ball mini-game""" + + + + +## This thing was originally made for 1.4 by someone (but had errors/bugs/missing features) so I edited/ported to 1.6/added all useful features :D +## I suggest to join this discord servers: +## https://discord.gg/ucyaesh +## https://discord.gg/NCvfPG2N9A + + +## Made on Android with the help of Terminal.py +## https://github.com/FreakOPP/BombSquad-Mods-byFREAK/blob/main/Utilities/Terminal.py + + +## MY (Freak's) comments are double-tagged btw ## + +## UPDATED: +""" +- Fixed Puck's mass/size/positions/texture/effects +- Fixed Goal positions +- Better center wall +- Added 1 more map +- Added more customisable options +- Map lights locators are now looped (thus reducing the size of the file and lengthy work...) +- Merged map & minigame in one file +- Puck spawns according to scored team (if right team scored, ball will spawn on left side) +- Also puck now spawns in airrr +- Server support added :) +- Fixed **LOTS** of errors/bugs +""" + + + + + + + + + + +# ba_meta require api 6 + +from typing import TYPE_CHECKING + +import bastd, _ba, ba, random +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.powerupbox import PowerupBoxFactory +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + + + +class PuckDiedMessage: + """Inform something that a puck has died.""" + + def __init__(self, puck: Puck): + self.puck = puck + + +class Puck(ba.Actor): + """A lovely giant hockey puck.""" + + def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + + # Spawn just above the provided point. + self._spawn_pos = (position[0], position[1] + 1.8, position[2]) + self.last_players_to_touch: Dict[int, Player] = {} + self.scored = False + assert activity is not None + assert isinstance(activity, VolleyBallGame) + pmats = [shared.object_material, activity.puck_material] + self.node = ba.newnode('prop', + delegate=self, + attrs={ + 'model': activity.puck_model, + 'color_texture': activity.puck_tex, + 'body': 'sphere', + 'reflection': 'soft', + 'reflection_scale': [0.2], + 'shadow_size': 0.6, + 'model_scale': 0.4, + 'body_scale': 1.07, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats + }) + + ## Since it rolls on spawn, lets make gravity + ## to 0, and when another node (bomb/spaz) + ## touches it. It'll act back as our normie puck! + ba.animate(self.node, 'gravity_scale', {0:-0.1, 2:1}, False) + ## When other node touches, it realises its new gravity_scale + ## Jugad for now... + + + + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + assert self.node + self.node.delete() + activity = self._activity() + if activity and not msg.immediate: + activity.handlemessage(PuckDiedMessage(self)) + + # If we go out of bounds, move back to where we started. + elif isinstance(msg, ba.OutOfBoundsMessage): + assert self.node + self.node.position = self._spawn_pos + + elif isinstance(msg, ba.HitMessage): + assert self.node + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, + 1.0 * msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + + # If this hit came from a player, log them as the last to touch us. + s_player = msg.get_source_player(Player) + if s_player is not None: + activity = self._activity() + if activity: + if s_player in activity.players: + self.last_players_to_touch[s_player.team.id] = s_player + else: + super().handlemessage(msg) + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class VolleyBallGame(ba.TeamGameActivity[Player, Team]): + """Volley Ball game.""" + + name = 'Volley Ball' + description = 'Score some goals.\nbyFREAK' + available_settings = [ + ba.IntSetting( + 'Score to Win', + min_value=1, + default=1, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting('Epic Mode', True), + ba.BoolSetting('Night Mode', False), + ba.BoolSetting('Icy Floor', True), + ba.BoolSetting('Disable Punch', False), + ba.BoolSetting('Disable Bombs', False), + ba.BoolSetting('Enable Bottom Credits', True), + ] + default_music = ba.MusicType.HOCKEY + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ['Open Field','Closed Arena'] + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._cheer_sound = ba.getsound('cheer') + self._chant_sound = ba.getsound('crowdChant') + self._foghorn_sound = ba.getsound('foghorn') + self._swipsound = ba.getsound('swip') + self._whistle_sound = ba.getsound('refWhistle') + self.puck_model = ba.getmodel('shield') + self.puck_tex = ba.gettexture('gameCircleIcon') + self._puck_sound = ba.getsound('metalHit') + self.puck_material = ba.Material() + self.puck_material.add_actions(actions=(('modify_part_collision', + 'friction', 0.5))) + self.puck_material.add_actions(conditions=('they_have_material', + shared.pickup_material), + actions=('modify_part_collision', + 'collide', True)) + self.puck_material.add_actions( + conditions=( + ('we_are_younger_than', 100), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + self.puck_material.add_actions(conditions=('they_have_material', + shared.footing_material), + actions=('impact_sound', + self._puck_sound, 0.2, 5)) + + # Keep track of which player last touched the puck + self.puck_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_connect', + self._handle_puck_player_collide), )) + + # We want the puck to kill powerups; not get stopped by them + self.puck_material.add_actions( + conditions=('they_have_material', + PowerupBoxFactory.get().powerup_material), + actions=(('modify_part_collision', 'physical', False), + ('message', 'their_node', 'at_connect', ba.DieMessage()))) + self._score_region_material = ba.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', self.puck_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_score))) + + + self._wall_material=ba.Material() + self._fake_wall_material=ba.Material() + self._wall_material.add_actions( + + actions=( + ('modify_part_collision', 'friction', 100000), + )) + self._wall_material.add_actions( + conditions=('they_have_material', shared.pickup_material), + actions=( + ('modify_part_collision', 'collide', False), + )) + + self._wall_material.add_actions( + conditions=(('we_are_younger_than', 100), + 'and', + ('they_have_material',shared.object_material)), + actions=( + ('modify_part_collision', 'collide', False), + )) + self._wall_material.add_actions( + conditions=('they_have_material',shared.footing_material), + actions=( + ('modify_part_collision', 'friction', 9999.5), + )) + self._wall_material.add_actions( + conditions=('they_have_material', bastd.actor.bomb.BombFactory.get().blast_material), + actions=( + ('modify_part_collision', 'collide', False), + ('modify_part_collision', 'physical', False) + + )) + self._fake_wall_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self.blocks=[] + + + self._net_wall_material=ba.Material() + self._net_wall_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + + self._net_wall_material.add_actions( + conditions=('they_have_material', shared.object_material), + actions=( + ('modify_part_collision', 'collide', True), + )) + self._net_wall_material.add_actions( + conditions=('they_have_material', self.puck_material), + actions=( + ('modify_part_collision', 'collide', True), + )) + self._net_wall_material.add_actions( + conditions=('we_are_older_than', 1), + actions=( + ('modify_part_collision', 'collide', True), + )) + self.net_blocc=[] + + + self._puck_spawn_pos: Optional[Sequence[float]] = None + self._score_regions: Optional[List[ba.NodeActor]] = None + self._puck: Optional[Puck] = None + self._score_to_win = int(settings['Score to Win']) + self._punchie_ = bool(settings['Disable Punch']) + self._night_mode = bool(settings['Night Mode']) + self._bombies_ = bool(settings['Disable Bombs']) + self._time_limit = float(settings['Time Limit']) + self._icy_flooor = bool(settings['Icy Floor']) + self.credit_text = bool(settings['Enable Bottom Credits']) + self._epic_mode = bool(settings['Epic Mode']) + # Base class overrides. + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC if self._epic_mode else + ba.MusicType.TO_THE_DEATH) + + def get_instance_description(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'Score a goal.' + return 'Score ${ARG1} goals.', self._score_to_win + + def get_instance_description_short(self) -> Union[str, Sequence]: + if self._score_to_win == 1: + return 'score a goal' + return 'score ${ARG1} goals', self._score_to_win + + def on_begin(self) -> None: + super().on_begin() + + self.setup_standard_time_limit(self._time_limit) + if self._night_mode: + ba.getactivity().globalsnode.tint = (0.5, 0.7, 1) + else: + pass + self._puck_spawn_pos = self.map.get_flag_position(None) + self._spawn_puck() + + # Set up the two score regions. + defs = self.map.defs + self._score_regions = [] + self._score_regions.append( + ba.NodeActor( + ba.newnode('region', + attrs={ + 'position':(5,0,0), + 'scale': defs.boxes['goal1'], + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._score_regions.append( + ba.NodeActor( + ba.newnode('region', + attrs={ + 'position':(-5,0,0), + 'scale': defs.boxes['goal2'], + 'type': 'box', + 'materials': [self._score_region_material] + }))) + self._update_scoreboard() + ba.playsound(self._chant_sound) + import base64 + exec(base64.b64decode("aWYgc2VsZi5jcmVkaXRfdGV4dDoKICAgICMjIFBlb3BsZSBzdGVhbGVkIGNyZWRpdHMgc28gdGhhdHMgd2h5IEkgZW5jb2RlZCB0aGlzLi4uCiAgICAjIyBFdmVuIHRobyB0aGVyZSBpcyBhIG9wdGlvbiwgdGhleSBjaGFuZ2VkIGNyZWF0ZWQgYnkKICAgICMjIGxpa2Ugd3RmIGlzIHRoaWVyIHByb2JsZW0/PwogICAgCiAgICAjIyBBbnl3YXlzIGhhdmUgYSBnb29kIGRheSEKICAgIHQgPSBiYS5uZXdub2RlKCd0ZXh0JywKICAgICAgICAgICAgICAgYXR0cnM9eyAndGV4dCc6IkNyZWF0ZWQgYnkg7oGIRnJlYWt1XG5Wb2xsZXlCYWxsIDEuNiIsICMjIERpc2FibGUgJ0VuYWJsZSBCb3R0b20gQ3JlZGl0cycgd2hlbiBtYWtpbmcgcGxheWxpc3QsIE5vIG5lZWQgdG8gZWRpdCB0aGlzIGxvdmVseS4uLgogICAgICAgICdzY2FsZSc6MC43LAogICAgICAgICdwb3NpdGlvbic6KDAsMCksICNMZXRzIGhvcGUgaGUgdXNlcyBUViBib3JkZXIgb2Ygc2V0dGluZ3M+R3JhcGhpY3MKICAgICAgICAnc2hhZG93JzowLjUsCiAgICAgICAgJ2ZsYXRuZXNzJzoxLjIsCiAgICAgICAgJ2NvbG9yJzooMSwgMSwgMSksCiAgICAgICAgJ2hfYWxpZ24nOidjZW50ZXInLAogICAgICAgICd2X2F0dGFjaCc6J2JvdHRvbSd9KQ==").decode('UTF-8')) + shared = SharedObjects.get() + self.blocks.append(ba.NodeActor(ba.newnode('region',attrs={'position': (0,2.4,0),'scale': (1,6,20),'type': 'box','materials': (self._fake_wall_material, )}))) + + self.net_blocc.append(ba.NodeActor(ba.newnode('region',attrs={'position': (0,0,0),'scale': (0.6,2.4,20),'type': 'box','materials': (self._net_wall_material, )}))) + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def _handle_puck_player_collide(self) -> None: + collision = ba.getcollision() + try: + puck = collision.sourcenode.getdelegate(Puck, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except ba.NotFoundError: + return + + puck.last_players_to_touch[player.team.id] = player + + def _kill_puck(self) -> None: + self._puck = None + + def _handle_score(self) -> None: + """A point has been scored.""" + + assert self._puck is not None + assert self._score_regions is not None + + # Our puck might stick around for a second or two + # we don't want it to be able to score again. + if self._puck.scored: + return + + region = ba.getcollision().sourcenode + index = 0 + for index in range(len(self._score_regions)): + if region == self._score_regions[index].node: + break + + for team in self.teams: + if team.id == index: + scoring_team = team + team.score += 1 + + + + ## Spawn puck on opponent's Area + if team.id == 0: # left side scored + self._puck_spawn_pos= (5, -0.3, 0) + elif team.id == 1: # right side scored + self._puck_spawn_pos= (-5, -0.3, 0) + else: # incase, this dude is oversmart and using 3+ teams... + self._puck_spawn_pos= (0, 0.2, 0) + ## Easy pizzy + + + for player in team.players: + if player.actor: + player.actor.handlemessage(ba.CelebrateMessage(2.0)) + + # If we've got the player from the scoring team that last + # touched us, give them points. + if (scoring_team.id in self._puck.last_players_to_touch + and self._puck.last_players_to_touch[scoring_team.id]): + self.stats.player_scored( + self._puck.last_players_to_touch[scoring_team.id], + 50, + big_message=True) + + # End game if we won. + if team.score >= self._score_to_win: + self.end_game() + + ba.playsound(self._foghorn_sound) + ba.playsound(self._cheer_sound) + + self._puck.scored = True + + # Kill the puck (it'll respawn itself shortly). + ba.emitfx(position= ba.getcollision().position, count=int(6.0 + 7.0 * 12), scale=3, spread=0.5, chunk_type='spark') + ba.timer(0.7, self._kill_puck) + + + ba.cameraflash(duration=7.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def on_transition_in(self) -> None: + super().on_transition_in() + activity = ba.getactivity() + if self._icy_flooor: + activity.map.is_hockey = True + else: + return + + def _update_scoreboard(self) -> None: + winscore = self._score_to_win + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, winscore) + + # overriding the default character spawning.. + def spawn_player(self, player: Player) -> ba.Actor: + spaz = self.spawn_player_spaz(player) + + if self._bombies_: + ## We wantthe button to work, justno bombs... + spaz.bomb_count = 0 + ## Imagine not being able to swipe those colorful buttons ;( + + + if self._punchie_: + spaz.connect_controls_to_player(enable_punch=False) + + return spaz + + def handlemessage(self, msg: Any) -> Any: + + # Respawn dead players if they're still in the game. + if isinstance(msg, ba.PlayerDiedMessage): + # Augment standard behavior... + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + + # Respawn dead pucks. + elif isinstance(msg, PuckDiedMessage): + if not self.has_ended(): + ba.timer(2.2, self._spawn_puck) + else: + super().handlemessage(msg) + + def _flash_puck_spawn(self) -> None: + ## Effect >>>>>> Flashly + ba.emitfx(position= self._puck_spawn_pos, count=int(6.0 + 7.0 * 12), scale=1.7, spread=0.4, chunk_type='spark') + + def _spawn_puck(self) -> None: + ba.playsound(self._swipsound) + ba.playsound(self._whistle_sound) + self._flash_puck_spawn() + assert self._puck_spawn_pos is not None + self._puck = Puck(position=self._puck_spawn_pos) + diff --git a/dist/ba_root/mods/games/baBasketBomb.py b/dist/ba_root/mods/games/baBasketBomb.py new file mode 100644 index 0000000..6050269 --- /dev/null +++ b/dist/ba_root/mods/games/baBasketBomb.py @@ -0,0 +1,673 @@ +# Released under the MIT License. See LICENSE for details. +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba, _ba +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.powerupbox import PowerupBoxFactory +from bastd.gameutils import SharedObjects +from bastd.actor import playerspaz as ps +from bastd import maps + +if TYPE_CHECKING: + from typing import Any, Sequence, Dict, Type, List, Optional, Union + +bsuSpaz = None + +def getlanguage(text, sub: str = ''): + lang = _ba.app.lang.language + translate = { + "Name": + {"Spanish": "Baloncesto", + "English": "Basketbomb", + "Portuguese": "Basketbomb"}, + "Info": + {"Spanish": "Anota todas las canastas y sé el MVP.", + "English": "Score all the baskets and be the MVP.", + "Portuguese": "Marque cada cesta e seja o MVP."}, + "Info-Short": + {"Spanish": f"Anota {sub} canasta(s) para ganar", + "English": f"Score {sub} baskets to win", + "Portuguese": f"Cestas de {sub} pontos para ganhar"}, + "S: Powerups": + {"Spanish": "Aparecer Potenciadores", + "English": "Powerups Spawn", + "Portuguese": "Habilitar Potenciadores"}, + "S: Velocity": + {"Spanish": "Activar velocidad", + "English": "Enable speed", + "Portuguese": "Ativar velocidade"}, + } + + languages = ['Spanish','Portuguese','English'] + if lang not in languages: lang = 'English' + + if text not in translate: + return text + return translate[text][lang] + +class BallDiedMessage: + def __init__(self, ball: Ball): + self.ball = ball + +class Ball(ba.Actor): + def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + velocty = (0.0, 8.0, 0.0) + _scale = 1.2 + + self._spawn_pos = (position[0], position[1] + 0.5, position[2]) + self.last_players_to_touch: Dict[int, Player] = {} + self.scored = False + + assert activity is not None + assert isinstance(activity, BasketGame) + + pmats = [shared.object_material, activity.ball_material] + self.node = ba.newnode('prop', + delegate=self, + attrs={ + 'model': activity.ball_model, + 'color_texture': activity.ball_tex, + 'body': 'sphere', + 'reflection': 'soft', + 'body_scale': 1.0 * _scale, + 'reflection_scale': [1.3], + 'shadow_size': 1.0, + 'gravity_scale': 0.92, + 'density': max(0.4 * _scale, 0.3), + 'position': self._spawn_pos, + 'velocity': velocty, + 'materials': pmats}) + self.scale = scale = 0.25 * _scale + ba.animate(self.node, 'model_scale', {0: 0, 0.2: scale*1.3, 0.26: scale}) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + assert self.node + self.node.delete() + activity = self._activity() + if activity and not msg.immediate: + activity.handlemessage(BallDiedMessage(self)) + + elif isinstance(msg, ba.OutOfBoundsMessage): + assert self.node + self.node.position = self._spawn_pos + self.node.velocity = (0.0, 0.0, 0.0) + + elif isinstance(msg, ba.HitMessage): + assert self.node + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], 1.0 * msg.magnitude, + 1.0 * msg.velocity_magnitude, msg.radius, 0, + msg.force_direction[0], msg.force_direction[1], + msg.force_direction[2]) + + s_player = msg.get_source_player(Player) + if s_player is not None: + activity = self._activity() + if activity: + if s_player in activity.players: + self.last_players_to_touch[s_player.team.id] = s_player + else: + super().handlemessage(msg) + + +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + self.score = 0 + +class Points: + postes = dict() + postes['pal_0'] = (10.64702320098877, 0.0000000000000000, 0.0000000000000000) #10.736066818237305, 0.3002409040927887, 0.5281256437301636 + postes['pal_1'] = (-10.64702320098877, 0.0000000000000000, 0.0000000000000000) + +# ba_meta export game +class BasketGame(ba.TeamGameActivity[Player, Team]): + + name = getlanguage('Name') + description = getlanguage('Info') + available_settings = [ + ba.IntSetting( + 'Score to Win', + min_value=1, + default=1, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[ + ('None', 0), + ('1 Minute', 60), + ('2 Minutes', 120), + ('5 Minutes', 300), + ('10 Minutes', 600), + ('20 Minutes', 1200), + ], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[ + ('Shorter', 0.25), + ('Short', 0.5), + ('Normal', 1.0), + ('Long', 2.0), + ('Longer', 4.0), + ], + default=1.0, + ), + ba.BoolSetting(getlanguage('S: Powerups'), default=True), + ba.BoolSetting(getlanguage('S: Velocity'), default=False), + ba.BoolSetting('Epic Mode', default=False), + ] + default_music = ba.MusicType.HOCKEY + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.DualTeamSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ['BasketBall Stadium', 'BasketBall Stadium V2'] + + def __init__(self, settings: dict): + super().__init__(settings) + shared = SharedObjects.get() + self._scoreboard = Scoreboard() + self._cheer_sound = ba.getsound('cheer') + self._chant_sound = ba.getsound('crowdChant') + self._foghorn_sound = ba.getsound('foghorn') + self._swipsound = ba.getsound('swip') + self._whistle_sound = ba.getsound('refWhistle') + self.ball_model = ba.getmodel('shield') + self.ball_tex = ba.gettexture('fontExtras3') + self._ball_sound = ba.getsound('bunnyJump') + self._powerups = bool(settings[getlanguage('S: Powerups')]) + self._speed = bool(settings[getlanguage('S: Velocity')]) + self._epic_mode = bool(settings['Epic Mode']) + self.slow_motion = self._epic_mode + + self.ball_material = ba.Material() + self.ball_material.add_actions(actions=(('modify_part_collision', + 'friction', 0.5))) + self.ball_material.add_actions(conditions=('they_have_material', + shared.pickup_material), + actions=('modify_part_collision', + 'collide', True)) + self.ball_material.add_actions( + conditions=( + ('we_are_younger_than', 100), + 'and', + ('they_have_material', shared.object_material), + ), + actions=('modify_node_collision', 'collide', False), + ) + self.ball_material.add_actions(conditions=('they_have_material', + shared.footing_material), + actions=('impact_sound', + self._ball_sound, 0.2, 5)) + + # Keep track of which player last touched the ball + self.ball_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=(('call', 'at_connect', + self._handle_ball_player_collide), )) + + self._score_region_material = ba.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', self.ball_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._handle_score))) + self._ball_spawn_pos: Optional[Sequence[float]] = None + self._score_regions: Optional[List[ba.NodeActor]] = None + self._ball: Optional[Ball] = None + self._score_to_win = int(settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) + + def get_instance_description(self) -> Union[str, Sequence]: + return getlanguage('Info-Short', sub=self._score_to_win) + + def get_instance_description_short(self) -> Union[str, Sequence]: + return getlanguage('Info-Short', sub=self._score_to_win) + + def on_begin(self) -> None: + super().on_begin() + + self.setup_standard_time_limit(self._time_limit) + + if self._powerups: + self.setup_standard_powerup_drops() + + self._ball_spawn_pos = self.map.get_flag_position(None) + self._spawn_ball() + + defs = self.map.defs + self._score_regions = [] + self._score_regions.append( + ba.NodeActor( + ba.newnode('region', + attrs={ + 'position': defs.boxes['goal1'][0:3], + 'scale': defs.boxes['goal1'][6:9], + 'type': 'box', + 'materials': [] + }))) + self._score_regions.append( + ba.NodeActor( + ba.newnode('region', + attrs={ + 'position': defs.boxes['goal2'][0:3], + 'scale': defs.boxes['goal2'][6:9], + 'type': 'box', + 'materials': [] + }))) + self._update_scoreboard() + ba.playsound(self._chant_sound) + + for id, team in enumerate(self.teams): + self.postes(id) + + def on_team_join(self, team: Team) -> None: + self._update_scoreboard() + + def _handle_ball_player_collide(self) -> None: + collision = ba.getcollision() + try: + ball = collision.sourcenode.getdelegate(Ball, True) + player = collision.opposingnode.getdelegate(PlayerSpaz, + True).getplayer( + Player, True) + except ba.NotFoundError: + return + + ball.last_players_to_touch[player.team.id] = player + + def _kill_ball(self) -> None: + self._ball = None + + def _handle_score(self, team_index: int = None) -> None: + assert self._ball is not None + assert self._score_regions is not None + + if self._ball.scored: + return + + region = ba.getcollision().sourcenode + index = 0 + for index in range(len(self._score_regions)): + if region == self._score_regions[index].node: + break + + if team_index is not None: + index = team_index + + for team in self.teams: + if team.id == index: + scoring_team = team + team.score += 1 + + for player in team.players: + if player.actor: + player.actor.handlemessage(ba.CelebrateMessage(2.0)) + + if (scoring_team.id in self._ball.last_players_to_touch + and self._ball.last_players_to_touch[scoring_team.id]): + self.stats.player_scored( + self._ball.last_players_to_touch[scoring_team.id], + 100, big_message=True) + + if team.score >= self._score_to_win: + self.end_game() + + #ba.playsound(self._foghorn_sound) + ba.playsound(self._cheer_sound) + + self._ball.scored = True + + # Kill the ball (it'll respawn itself shortly). + ba.timer(1.0, self._kill_ball) + + light = ba.newnode('light', + attrs={ + 'position': ba.getcollision().position, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + ba.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) + ba.timer(1.0, light.delete) + + ba.cameraflash(duration=10.0) + self._update_scoreboard() + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) + + def _update_scoreboard(self) -> None: + winscore = self._score_to_win + for id, team in enumerate(self.teams): + self._scoreboard.set_team_value(team, team.score, winscore) + #self.postes(id) + + def spawn_player(self, player: Player) -> ba.Actor: + if bsuSpaz is None: + spaz = self.spawn_player_spaz(player) + else: + ps.PlayerSpaz = bsuSpaz.BskSpaz + spaz = self.spawn_player_spaz(player) + ps.PlayerSpaz = bsuSpaz.OldPlayerSpaz + + if self._speed: + spaz.node.hockey = True + return spaz + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + super().handlemessage(msg) + self.respawn_player(msg.getplayer(Player)) + elif isinstance(msg, BallDiedMessage): + if not self.has_ended(): + ba.timer(3.0, self._spawn_ball) + else: + super().handlemessage(msg) + + def postes(self, team_id: int): + if not hasattr(self._map, 'poste_'+str(team_id)): + setattr(self._map, 'poste_'+str(team_id), + Palos(team=team_id, + position=Points.postes['pal_' + + str(team_id)]).autoretain()) + + def _flash_ball_spawn(self) -> None: + light = ba.newnode('light', + attrs={ + 'position': self._ball_spawn_pos, + 'height_attenuated': False, + 'color': (1, 0, 0) + }) + ba.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) + ba.timer(1.0, light.delete) + + def _spawn_ball(self) -> None: + ba.playsound(self._swipsound) + ba.playsound(self._whistle_sound) + self._flash_ball_spawn() + assert self._ball_spawn_pos is not None + self._ball = Ball(position=self._ball_spawn_pos) + +class Aro(ba.Actor): + def __init__(self, team: int = 0, + position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + act = self.getactivity() + shared = SharedObjects.get() + setattr(self, 'team', team) + setattr(self, 'locs', []) + + # Material Para; Traspasar Objetos + self.no_collision = ba.Material() + self.no_collision.add_actions( + actions=(('modify_part_collision', 'collide', False))) + + self.collision = ba.Material() + self.collision.add_actions( + actions=(('modify_part_collision', 'collide', True))) + + # Score + self._score_region_material = ba.Material() + self._score_region_material.add_actions( + conditions=('they_have_material', act.ball_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False), + ('call', 'at_connect', self._annotation))) + + self._spawn_pos = (position[0], position[1], position[2]) + self._materials_region0 = [self.collision, + shared.footing_material] + + model = None + tex = ba.gettexture('null') + + pmats = [self.no_collision] + self.node = ba.newnode('prop', + delegate=self, + attrs={ + 'model': model, + 'color_texture': tex, + 'body': 'box', + 'reflection': 'soft', + 'reflection_scale': [1.5], + 'shadow_size': 0.1, + 'position': self._spawn_pos, + 'materials': pmats}) + + self.scale = scale = 1.4 + ba.animate(self.node, 'model_scale', {0: 0}) + + pos = (position[0], position[1]+0.6, position[2]) + self.regions: List[ba.Node] = [ + ba.newnode('region', + attrs={'position': position, + 'scale': (0.6, 0.05, 0.6), + 'type': 'box', + 'materials': self._materials_region0}), + + ba.newnode('region', + attrs={'position': pos, + 'scale': (0.5, 0.3, 0.9), + 'type': 'box', + 'materials': [self._score_region_material]}) + ] + self.regions[0].connectattr('position', self.node, 'position') + #self.regions[0].connectattr('position', self.regions[1], 'position') + + locs_count = 9 + pos = list(position) + + try: + id = 0 if team == 1 else 1 + color = act.teams[id].color + except: color = (1,1,1) + + while locs_count > 1: + scale = (1.5 * 0.1 * locs_count) + 0.8 + + self.locs.append(ba.newnode('locator', + owner=self.node, + attrs={'shape': 'circleOutline', + 'position': pos, + 'color': color, + 'opacity': 1.0, + 'size': [scale], + 'draw_beauty': True, + 'additive': False})) + + pos[1] -= 0.1 + locs_count -= 1 + + def _annotation(self): + assert len(self.regions) >= 2 + ball = self.getactivity()._ball + + if ball: + p = self.regions[0].position + ball.node.position = p + ball.node.velocity = (0.0, 0.0, 0.0) + + act = self.getactivity() + act._handle_score(self.team) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + if self.node.exists(): + self.node.delete() + else: + super().handlemessage(msg) + +class Cuadro(ba.Actor): + def __init__(self, team: int = 0, + position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + act = self.getactivity() + shared = SharedObjects.get() + setattr(self, 'locs', []) + + self.collision = ba.Material() + self.collision.add_actions( + actions=(('modify_part_collision', 'collide', True))) + + pos = (position[0], position[1]+0.9, position[2]+1.5) + self.region: ba.Node = ba.newnode('region', + attrs={'position': pos, + 'scale': (0.5, 2.7, 2.5), + 'type': 'box', + 'materials': [self.collision, + shared.footing_material]}) + + #self.shield = ba.newnode('shield', attrs={'radius': 1.0, 'color': (0,10,0)}) + #self.region.connectattr('position', self.shield, 'position') + + position = (position[0], position[1], position[2]+0.09) + pos = list(position) + oldpos = list(position) + old_count = 14 + + count = old_count + count_y = 9 + + try: + id = 0 if team == 1 else 1 + color = act.teams[id].color + except: color = (1,1,1) + + while(count_y != 1): + + while(count != 1): + pos[2] += 0.19 + + self.locs.append( + ba.newnode('locator', + owner=self.region, + attrs={'shape': 'circle', + 'position': pos, + 'size': [0.5], + 'color': color, + 'opacity': 1.0, + 'draw_beauty': True, + 'additive': False})) + count -= 1 + + + count = old_count + pos[1] += 0.2 + pos[2] = oldpos[2] + count_y -= 1 + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + if self.node.exists(): + self.node.delete() + else: + super().handlemessage(msg) + +class Palos(ba.Actor): + def __init__(self, team: int = 0, + position: Sequence[float] = (0.0, 1.0, 0.0)): + super().__init__() + shared = SharedObjects.get() + activity = self.getactivity() + self._pos = position + self.aro = None + self.cua = None + + # Material Para; Traspasar Objetos + self.no_collision = ba.Material() + self.no_collision.add_actions( + actions=(('modify_part_collision', 'collide', False))) + + # + self.collision = ba.Material() + self.collision.add_actions( + actions=(('modify_part_collision', 'collide', True))) + + # Spawn just above the provided point. + self._spawn_pos = (position[0], position[2]+2.5, position[2]) + + model = ba.getmodel('flagPole') + tex = ba.gettexture('flagPoleColor') + + pmats = [self.no_collision] + self.node = ba.newnode('prop', + delegate=self, + attrs={ + 'model': model, + 'color_texture': tex, + 'body': 'puck', + 'reflection': 'soft', + 'reflection_scale': [2.6], + 'shadow_size': 0, + 'is_area_of_interest': True, + 'position': self._spawn_pos, + 'materials': pmats + }) + self.scale = scale = 4.0 + ba.animate(self.node, 'model_scale', {0: scale}) + + self.loc = ba.newnode('locator', + owner=self.node, + attrs={'shape': 'circle', + 'position': position, + 'color': (1,1,0), + 'opacity': 1.0, + 'draw_beauty': False, + 'additive': True}) + + self._y = _y = 0.30 + _x = -0.25 if team == 1 else 0.25 + _pos = (position[0]+_x, position[1]-1.5 + _y, position[2]) + self.region = ba.newnode('region', + attrs={ + 'position': _pos, + 'scale': (0.4, 8, 0.4), + 'type': 'box', + 'materials': [self.collision]}) + self.region.connectattr('position', self.node, 'position') + + _y = self._y + position = self._pos + if team == 0: + pos = (position[0]-0.8, position[1] + 2.0 + _y, position[2]) + else: pos = (position[0]+0.8, position[1] + 2.0 + _y, position[2]) + + if self.aro is None: + self.aro = Aro(team, pos).autoretain() + + if self.cua is None: + pos = (position[0], position[1] + 1.8 + _y, position[2]-1.4) + self.cua = Cuadro(team, pos).autoretain() + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + if self.node.exists(): + self.node.delete() + else: + super().handlemessage(msg) diff --git a/dist/ba_root/mods/games/crazyBoxes.py b/dist/ba_root/mods/games/crazyBoxes.py new file mode 100644 index 0000000..e1ff4d5 --- /dev/null +++ b/dist/ba_root/mods/games/crazyBoxes.py @@ -0,0 +1,322 @@ +# ba_meta require api 6 +# (see https://ballistica.net/wiki/meta-tag-system) + +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar + +import ba, random, _ba +from math import cos, sin +from bastd.actor.bomb import Bomb, BombFactory, Blast, ExplodeMessage +from bastd.actor.playerspaz import PlayerSpaz +from bastd.actor.scoreboard import Scoreboard +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional, Callable + +class DisconnectMessage(): + pass +class ConnectMessage(): + pass + +class Box(ba.Actor): + # should create our own node instead + def __init__(self, + velocity: Sequence[float] = (0.0, 0.0, 0.0), + bomb_type: str = 'normal', + box_type: str = 'small', + bomb_scale: float = 1.0): + super().__init__() + shared = SharedObjects.get() + factory = BombFactory.get() + self.pos: Sequence[float] = ((-11.857367) + round(random.uniform(0, 23), 6), + round(random.uniform(2, 9), 2), + (-4.3599663) + round(random.uniform(0, 8), 6)) + self.random = self.force = self.ang = self.x = self.z = self.momentum = 0 + + self.typee = box_type + self._exploded, self.ground = False, False + self._explode_callbacks: List[Callable[[Bomb, Blast], Any]] = [] + + materials: Tuple[ba.Material, ...] + materials = (factory.bomb_material, shared.footing_material, + shared.object_material) + + if box_type == 'small': + self.data = { + 'walk_force': 9.35, + 'jump_force': 190, + 'walk_cd': 1474, + 'jump_cd': 1500, + 'light_color': (1, 0, 0.4), + 'points': 100, + 'texture': ba.gettexture('star'), + 'size': 0.9} + elif box_type == 'large': + self.data = { + 'walk_force': 13.3905, + 'jump_force': 260, + 'walk_cd': 900, + 'jump_cd': 3000, + 'light_color': (0, 0.2, 1.55), + 'points': 50, + 'texture': ba.gettexture('egg2'), + 'size': 1.2} + + self.node = ba.newnode('prop', + delegate=self, + attrs={ + 'position': self.pos, + 'velocity': velocity, + 'model': ba.getmodel('powerup'), + 'light_model': ba.getmodel('powerup'), + 'body': 'crate', + 'model_scale': self.data.get('size'), + 'body_scale': self.data.get('size'), + 'density':0.5, + 'gravity_scale': 0.5, + 'shadow_size': 0.5, + 'color_texture': self.data.get('texture'), + 'reflection': 'soft', + 'reflection_scale': [0.23], + 'materials': materials}) + self.light = ba.newnode('light', + attrs={ + 'color': self.data.get('light_color'), + 'radius': 0.4 + }) + self.node.connectattr('position',self.light,'position') + self.move_used = self.highjump_used = self.jump_used = False + ba.animate(self.node, 'model_scale', {0.0:0.0, 0.7:self.data.get('size')}) + ba.timer(1000*0.001, self.start_updating) + + def start_updating(self): + self.move_timer = ba.Timer(100*0.001, ba.WeakCall(self.update_ai), repeat=True) + def refresh(self, type_: str = 'move'): + if type_ == 'move': + self.move_used = False + elif type_ == 'jump': + self.jump_used = False + elif type_ == 'highjump': + self.highjump_used = False + + + def move(self): + try: + if self.node.exists() and not self.move_used and self.node.position[1] < 1.0: + self.move_used = True + ba.timer(self.data.get('walk_cd')*0.001, lambda: self.refresh('move')) + self.ang = random.randint(0,360) + self.x = cos(self.ang) * self.data.get('walk_force') + self.z = sin(self.ang) * self.data.get('walk_force') + self.node.velocity = (self.x, 0, self.z) + except: pass + def high_jump(self): + try: + if self.node.exists() and not self.highjump_used: + self.highjump_used = True + ba.timer(10000*0.001, lambda: self.refresh('highjump')) + self.node.handlemessage("impulse", self.node.position[0], self.node.position[1], self.node.position[2], + 0.0, 0.0, 0.0, + self.data.get('jump_force')+70,0,0,0,0,1,0) + except: pass + def jump(self): + try: + if self.node.exists() and not self.jump_used: + self.jump_used = True + ba.timer(self.data.get('jump_cd')*0.001, lambda: self.refresh('jump')) + self.node.handlemessage("impulse", self.node.position[0], self.node.position[1], self.node.position[2], + 0.0, 0.0, 0.0, + self.data.get('jump_force'),0,0,0,0,1,0) + except: pass + def act_crazy(self): + try: + if self.node.exists(): + self.node.velocity = (self.node.velocity[0]+round(random.uniform(-2.0, 2.0),2), random.random()*1.5, self.node.velocity[2]+round(random.uniform(-2.0, 2.0),2)) + self.node.extra_acceleration = (self.node.velocity[0]*1.3, self.node.velocity[1]*45, self.node.velocity[2]*1.3) + except: pass + + def update_ai(self): + if not self.node.exists(): + self.update_ai = None + return + for p in self.activity.players: + if p.actor.node.exists(): + if p.actor.node.hold_node == self.node: + self.act_crazy() + else: + self.node.extra_acceleration = (0, 0, 0) + self.random = random.randint(1,4) + if self.random == 1: self.move() + elif self.random == 2: self.jump() + elif self.random == 3 and self.typee == 'large': self.high_jump() + + def on_expire(self) -> None: + super().on_expire() + self._explode_callbacks = [] + def respawn(self) -> None: pass + + def _handle_die(self) -> None: + # self.respawn() + if self.node: + self.light.delete() + self.node.delete() + def _handle_oob(self) -> None: + self.handlemessage(ba.DieMessage()) + def add_explode_callback(self, call: Callable[[Bomb, Blast], Any]) -> None: + self._explode_callbacks.append(call) + def explode(self) -> None: + if self._exploded: + return + self._exploded = True + if self.node: + blast = Blast(position=self.node.position, + velocity=self.node.velocity, + blast_radius=2.4, + blast_type='tnt', + hit_type='normal', + hit_subtype='tnt').autoretain() + for callback in self._explode_callbacks: + callback(self, blast) + ba.timer(0.001, ba.WeakCall(self.handlemessage, ba.DieMessage())) + def _add_material(self, material: ba.Material) -> None: + if not self.node: + return + materials = self.node.materials + if material not in materials: + assert isinstance(materials, tuple) + self.node.materials = materials + (material, ) + + def _handle_hit(self, msg: ba.HitMessage) -> None: + if msg.hit_subtype == 'tnt': return + if not self._exploded and not msg.hit_type == 'punch': + self.update_ai = None + ba.timer(0.1 + random.random() * 0.1, + ba.WeakCall(self.handlemessage, ExplodeMessage())) + killer = msg.get_source_player(ba.Player) + if killer is not None: + assert killer.team is not None + self.activity.stats.player_scored(killer, self.data.get('points'), screenmessage=False, color=(killer.actor.node.color[0]+0.3, killer.actor.node.color[1]+0.3, killer.actor.node.color[2]+0.3)) + killer.team.score += self.data.get('points') + ba.playsound(self.activity._dingsound) + self.activity._update_scoreboard() + self.move_timer = None + if msg.srcnode: pass + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ExplodeMessage): + self.explode() + elif isinstance(msg, ba.HitMessage): + self._handle_hit(msg) + elif isinstance(msg, ba.DieMessage): + ba.timer(random.randrange(1500,5020,50)*0.001, lambda: Box(box_type=self.typee).autoretain()) + self._handle_die() + elif isinstance(msg, ba.OutOfBoundsMessage): + self._handle_oob() + else: + super().handlemessage(msg) + + +class Player(ba.Player['Team']): + ... +class Team(ba.Team[Player]): + + def __init__(self) -> None: + self.score = 0 + + +# ba_meta export game +class CrazyBoxGame(ba.TeamGameActivity[Player, Team]): + """A game type based on acquiring kills.""" + + name = 'Crazy Box' + description = 'Blow up the Crazy Boxes!' + announce_player_deaths = True + + @classmethod + def get_available_settings( + cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: + settings = [ + ba.IntSetting('Points to win per Player',min_value=50,default=800, increment=50), + ba.IntSetting('Small Box Count',min_value=1,default=1, increment=1), + ba.IntSetting('Large Box Count',min_value=1,default=2, increment=1), + ba.IntChoiceSetting('Time Limit', choices=[('None', 0),('1 Minute', 60),('2 Minutes', 120),('5 Minutes', 300),('10 Minutes', 600),('20 Minutes', 1200)],default=0), + ba.FloatChoiceSetting('Respawn Times',choices=[('Shorter', 0.25),('Short', 0.5),('Normal', 1.0),('Long', 2.0),('Longer', 4.0)], default=1.0), + ba.BoolSetting('Epic Mode', default=False) + ] + return settings + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return (issubclass(sessiontype, ba.DualTeamSession) + or issubclass(sessiontype, ba.FreeForAllSession)) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + return ['Football Stadium'] + + def __init__(self, settings: dict): + super().__init__(settings) + self.co = int(settings["Small Box Count"]) + self.c2 = int(settings["Large Box Count"]) + self._scoreboard = Scoreboard() + self._score_to_win: Optional[int] = None + self._dingsound = ba.getsound('dingSmall') + self._epic_mode = bool(settings['Epic Mode']) + self.boxes_to_win = int(settings['Points to win per Player']) + self._time_limit = float(settings['Time Limit']) + self.region: List[ba.NodeActor] = [] + self.block_box_mat = ba.Material() + self.block_box_mat.add_actions(conditions=('they_have_material', BombFactory.get().bomb_material), + actions=(('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True))) + + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC if self._epic_mode else + ba.MusicType.FORWARD_MARCH) + + def _setup_standard_tnt_drops(self) -> None: + pass + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Blow up boxes and score ${ARG1} points to win!', self.boxes_to_win + + def on_team_join(self, team: Team) -> None: + if self.has_begun(): + self._update_scoreboard() + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + for i in range(self.co): + Box().autoretain() + for a in range(self.c2): + Box(box_type='large').autoretain() + self.setup_standard_powerup_drops() + b = self.map.defs.boxes['map_bounds'] + self.region.append(ba.NodeActor(ba.newnode('region', attrs={'position':(b[0], b[1]+10, b[2]), 'type':'box', 'scale': (b[6], 1.0, b[8]), 'materials': [self.block_box_mat]}))) + + self._score_to_win = (self.boxes_to_win * + max(1, max(len(t.players) for t in self.teams))) + self._update_scoreboard() + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + super().handlemessage(msg) + player = msg.getplayer(Player) + self.respawn_player(player) + else: + return super().handlemessage(msg) + return None + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + if team.score >= self._score_to_win: ba.timer(500*0.001, self.end_game) + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + self.end(results=results) \ No newline at end of file diff --git a/dist/ba_root/mods/games/game.py b/dist/ba_root/mods/games/game.py new file mode 100644 index 0000000..230f7c2 --- /dev/null +++ b/dist/ba_root/mods/games/game.py @@ -0,0 +1,361 @@ +"""Quake Game Activity""" +# ba_meta require api 6 +from __future__ import annotations + +from typing import TYPE_CHECKING + +import random +import enum +import ba + +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.powerupbox import PowerupBox +from bastd.gameutils import SharedObjects + +import quake.rocket +import quake.railgun + +if TYPE_CHECKING: + from typing import Optional, List, Any, Type, Union, Sequence + + +class Player(ba.Player['Team']): + """Our player""" + + +class Team(ba.Team[Player]): + """Our team""" + def __init__(self) -> None: + self.score = 0 + + +class WeaponType(enum.Enum): + """Type of weapon""" + ROCKET = 0 + RAILGUN = 1 + + +class ObstaclesForm(enum.Enum): + """Obstacle form""" + CUBE = 0 + SPHERE = 1 + RANDOM = 2 + + +# ba_meta export game +class QuakeGame(ba.TeamGameActivity[Player, Team]): + """Quake Team Game Activity""" + name = 'Quake' + description = 'Kill a set number of enemies to win.' + available_settings = [ + ba.IntSetting( + 'Kills to Win Per Player', + default=15, + min_value=1, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[('None', 0), ('1 Minute', 60), ('2 Minutes', 120), + ('5 Minutes', 300), ('10 Minutes', 600), + ('20 Minutes', 1200)], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[('At once', 0.0), ('Shorter', 0.25), ('Short', 0.5), + ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)], + default=1.0, + ), + ba.BoolSetting( + 'Speed', + default=True, + ), + ba.BoolSetting( + 'Enable Jump', + default=True, + ), + ba.BoolSetting( + 'Enable Pickup', + default=True, + ), + ba.BoolSetting( + 'Enable Bomb', + default=False, + ), + ba.BoolSetting( + 'Obstacles', + default=True, + ), + ba.IntChoiceSetting( + 'Obstacles Form', + choices=[('Cube', ObstaclesForm.CUBE.value), + ('Sphere', ObstaclesForm.SPHERE.value), + ('Random', ObstaclesForm.RANDOM.value)], + default=0, + ), + ba.IntChoiceSetting( + 'Weapon Type', + choices=[('Rocket', WeaponType.ROCKET.value), + ('Railgun', WeaponType.RAILGUN.value)], + default=WeaponType.ROCKET.value, + ), + ba.BoolSetting( + 'Obstacles Mirror Shots', + default=False, + ), + ba.IntSetting( + 'Obstacles Count', + default=16, + min_value=0, + increment=2, + ), + ba.BoolSetting( + 'Random Obstacles Color', + default=True, + ), + ba.BoolSetting( + 'Epic Mode', + default=False, + ), + ] + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.MultiTeamSession) or issubclass( + sessiontype, ba.FreeForAllSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + # TODO add more maps + return ['Football Stadium', 'Monkey Face', 'Doom Shroom'] + + def __init__(self, settings) -> None: + super().__init__(settings) + self._epic_mode = self.settings_raw['Epic Mode'] + self._score_to_win = self.settings_raw['Kills to Win Per Player'] + self._time_limit = self.settings_raw['Time Limit'] + self._obstacles_enabled = self.settings_raw['Obstacles'] + self._obstacles_count = self.settings_raw['Obstacles Count'] + self._speed_enabled = self.settings_raw['Speed'] + self._bomb_enabled = self.settings_raw['Enable Bomb'] + self._pickup_enabled = self.settings_raw['Enable Pickup'] + self._jump_enabled = self.settings_raw['Enable Jump'] + self._weapon_type = WeaponType(self.settings_raw['Weapon Type']) + self.default_music = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.GRAND_ROMP) + self.slow_motion = self._epic_mode + + self.announce_player_deaths = True + self._scoreboard = Scoreboard() + self._ding_sound = ba.getsound('dingSmall') + + self._shield_dropper: Optional[ba.Timer] = None + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Kill ${ARG1} enemies.', self._score_to_win + + def on_team_join(self, team: Team) -> None: + team.score = 0 + if self.has_begun(): + self._update_scoreboard() + + def on_begin(self) -> None: + ba.TeamGameActivity.on_begin(self) + ba.getactivity().globalsnode.tint = (0.5, 0.7, 1) + self.drop_shield() + self._shield_dropper = ba.Timer(8, + ba.WeakCall(self.drop_shield), + repeat=True) + self.setup_standard_time_limit(self._time_limit) + if self._obstacles_enabled: + count = self._obstacles_count + gamemap = self.map.getname() + for i in range(count): # TODO: tidy up around here + if gamemap == 'Football Stadium': + radius = (random.uniform(-10, 1), + 6, + random.uniform(-4.5, 4.5)) \ + if i > count / 2 else ( + random.uniform(10, 1), 6, random.uniform(-4.5, 4.5)) + else: + radius = (random.uniform(-10, 1), + 6, + random.uniform(-8, 8)) \ + if i > count / 2 else ( + random.uniform(10, 1), 6, random.uniform(-8, 8)) + + Obstacle( + position=radius, + mirror=self.settings_raw['Obstacles Mirror Shots'], + form=self.settings_raw['Obstacles Form']).autoretain() + + self._update_scoreboard() + + def drop_shield(self) -> None: + """Drop a shield powerup in random place""" + # FIXME: should use map defs + shield = PowerupBox(poweruptype='shield', + position=(random.uniform(-10, 10), 6, + random.uniform(-5, 5))).autoretain() + + ba.playsound(self._ding_sound) + + p_light = ba.newnode('light', + owner=shield.node, + attrs={ + 'position': (0, 0, 0), + 'color': (0.3, 0.0, 0.4), + 'radius': 0.3, + 'intensity': 2, + 'volume_intensity_scale': 10.0 + }) + + shield.node.connectattr('position', p_light, 'position') + + ba.animate(p_light, 'intensity', {0: 2, 8: 0}) + + def spawn_player(self, player: Player) -> None: + spaz = self.spawn_player_spaz(player) + if self._weapon_type == WeaponType.ROCKET: + quake.rocket.RocketLauncher().give(spaz) + elif self._weapon_type == WeaponType.RAILGUN: + quake.railgun.Railgun().give(spaz) + spaz.connect_controls_to_player(enable_jump=self._jump_enabled, + enable_pickup=self._pickup_enabled, + enable_bomb=self._bomb_enabled, + enable_fly=False) + + spaz.node.hockey = self._speed_enabled + spaz.spaz_light = ba.newnode('light', + owner=spaz.node, + attrs={ + 'position': (0, 0, 0), + 'color': spaz.node.color, + 'radius': 0.12, + 'intensity': 1, + 'volume_intensity_scale': 10.0 + }) + + spaz.node.connectattr('position', spaz.spaz_light, 'position') + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + ba.TeamGameActivity.handlemessage(self, msg) + player = msg.getplayer(Player) + self.respawn_player(player) + killer = msg.getkillerplayer(Player) + if killer is None: + return + + # handle team-kills + if killer.team is player.team: + # in free-for-all, killing yourself loses you a point + if isinstance(self.session, ba.FreeForAllSession): + new_score = player.team.score - 1 + new_score = max(0, new_score) + player.team.score = new_score + # in teams-mode it gives a point to the other team + else: + ba.playsound(self._ding_sound) + for team in self.teams: + if team is not killer.team: + team.score += 1 + # killing someone on another team nets a kill + else: + killer.team.score += 1 + ba.playsound(self._ding_sound) + # in FFA show our score since its hard to find on + # the scoreboard + assert killer.actor is not None + # noinspection PyUnresolvedReferences + killer.actor.set_score_text(str(killer.team.score) + '/' + + str(self._score_to_win), + color=killer.team.color, + flash=True) + + self._update_scoreboard() + + # if someone has won, set a timer to end shortly + # (allows the dust to clear and draws to occur if + # deaths are close enough) + if any(team.score >= self._score_to_win for team in self.teams): + ba.timer(0.5, self.end_game) + + else: + ba.TeamGameActivity.handlemessage(self, msg) + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + + self.end(results=results) + + +class Obstacle(ba.Actor): + """Scene object""" + + def __init__(self, + position, + form=ObstaclesForm.CUBE, + mirror=False) -> None: + ba.Actor.__init__(self) + + if form == ObstaclesForm.CUBE: + model = 'tnt' + body = 'crate' + elif form == ObstaclesForm.SPHERE: + model = 'bomb' + body = 'sphere' + else: # ObstaclesForm.RANDOM: + model = random.choice(['tnt', 'bomb']) + body = 'sphere' if model == 'bomb' else 'crate' + + self.node = ba.newnode( + 'prop', + delegate=self, + attrs={ + 'position': + position, + 'model': + ba.getmodel(model), + 'body': + body, + 'body_scale': + 1.3, + 'model_scale': + 1.3, + 'reflection': + 'powerup', + 'reflection_scale': [0.7], + 'color_texture': + ba.gettexture('bunnyColor'), + 'materials': [SharedObjects.get().footing_material] + if mirror else [ + SharedObjects.get().object_material, + SharedObjects.get().footing_material + ] + }) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + + elif isinstance(msg, ba.OutOfBoundsMessage): + if self.node: + self.handlemessage(ba.DieMessage()) + + elif isinstance(msg, ba.HitMessage): + self.node.handlemessage('impulse', msg.pos[0], msg.pos[1], + msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], + msg.magnitude, msg.velocity_magnitude, + msg.radius, 0, msg.velocity[0], + msg.velocity[1], msg.velocity[2]) diff --git a/dist/ba_root/mods/games/ms_BombWar.py b/dist/ba_root/mods/games/ms_BombWar.py new file mode 100644 index 0000000..b40b6f9 --- /dev/null +++ b/dist/ba_root/mods/games/ms_BombWar.py @@ -0,0 +1,117 @@ +# ba_meta require api 6 + +""" + BombWar Mini game + + by : Mr.Smoothy + discord : mr.smoothy#5824 + download more , contribute : https://discord.gg/ucyaesh + + """ +from __future__ import annotations + +from typing import TYPE_CHECKING +import ba +from bastd.game.deathmatch import DeathMatchGame, Player, Team +from bastd.actor.bomb import Bomb +import bastd +if TYPE_CHECKING: + from typing import Sequence + +from bastd.gameutils import SharedObjects +# ba_meta export game +class BombWar(DeathMatchGame): + name = 'Bomb War' + + def __init__(self, settings: dict): + super().__init__(settings) + + + shared = SharedObjects.get() + self._wall_material=ba.Material() + self._fake_wall_material=ba.Material() + self._wall_material.add_actions( + + actions=( + ('modify_part_collision', 'friction', 100000), + )) + self._wall_material.add_actions( + conditions=('they_have_material', shared.pickup_material), + actions=( + ('modify_part_collision', 'collide', False), + )) + + self._wall_material.add_actions( + conditions=(('we_are_younger_than', 100), + 'and', + ('they_have_material',shared.object_material)), + actions=( + ('modify_part_collision', 'collide', False), + )) + self._wall_material.add_actions( + conditions=('they_have_material',shared.footing_material), + actions=( + ('modify_part_collision', 'friction', 9999.5), + )) + self._wall_material.add_actions( + conditions=('they_have_material', bastd.actor.bomb.BombFactory.get().blast_material), + actions=( + ('modify_part_collision', 'collide', False), + ('modify_part_collision', 'physical', False) + + )) + self._fake_wall_material.add_actions( + conditions=('they_have_material', shared.player_material), + actions=( + ('modify_part_collision', 'collide', True), + ('modify_part_collision', 'physical', True) + + )) + self.blocks=[] + + def on_begin(self) -> None: + super().on_begin() + self.setup_standard_time_limit(self._time_limit) + factory = bastd.actor.bomb.BombFactory.get() + + shared = SharedObjects.get() + for i in [-5.5,-4.5,-3.5,-2.5,-1.5,-0.5,0.5,1.5,2.5]: + + self.blocks.append(ba.newnode( + 'prop', + delegate=self, + attrs={ + 'body': 'puck', + 'position': (-0.1,3,i), + 'model': ba.getmodel('tnt'), + # 'light_model': factory.tnt_model, + 'shadow_size': 0.5, + 'gravity_scale':7.5, + 'color_texture': ba.gettexture('puckColor'), + 'reflection': 'soft', + 'reflection_scale': [1.0], + 'materials': (shared.object_material,self._wall_material,) + }) ) + for i in [-5.5,-4.5,-3.5,-2.5,-1.5,-0.5,0.5,1.5,2.3]: + + self.blocks.append(ba.newnode( + 'prop', + delegate=self, + attrs={ + 'body': 'puck', + 'position': (-0.1,5,i), + 'model': ba.getmodel('tnt'), + # 'light_model': factory.tnt_model, + 'shadow_size': 0.5, + 'gravity_scale':7.5, + 'color_texture': ba.gettexture('puckColor'), + 'reflection': 'soft', + 'reflection_scale': [1.0], + 'materials': (shared.object_material,self._wall_material,) + }) ) + self.blocks.append(ba.NodeActor(ba.newnode('region',attrs={'position': (0,3,-6),'scale': (2,5,20),'type': 'box','materials': (self._fake_wall_material, )}))) + + # Base kills needed to win on the size of the largest team. + self._score_to_win = (self._kills_to_win_per_player * + max(1, max(len(t.players) for t in self.teams))) + self._update_scoreboard() \ No newline at end of file diff --git a/dist/ba_root/mods/games/quake/__init__.py b/dist/ba_root/mods/games/quake/__init__.py new file mode 100644 index 0000000..490d8c7 --- /dev/null +++ b/dist/ba_root/mods/games/quake/__init__.py @@ -0,0 +1,3 @@ +"""Quake Game""" +# ba_meta require api 6 +from .game import QuakeGame diff --git a/dist/ba_root/mods/games/quake/game.py b/dist/ba_root/mods/games/quake/game.py new file mode 100644 index 0000000..f12444f --- /dev/null +++ b/dist/ba_root/mods/games/quake/game.py @@ -0,0 +1,360 @@ +"""Quake Game Activity""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import random +import enum +import ba + +from bastd.actor.scoreboard import Scoreboard +from bastd.actor.powerupbox import PowerupBox +from bastd.gameutils import SharedObjects + +import quake.rocket +import quake.railgun + +if TYPE_CHECKING: + from typing import Optional, List, Any, Type, Union, Sequence + + +class Player(ba.Player['Team']): + """Our player""" + + +class Team(ba.Team[Player]): + """Our team""" + def __init__(self) -> None: + self.score = 0 + + +class WeaponType(enum.Enum): + """Type of weapon""" + ROCKET = 0 + RAILGUN = 1 + + +class ObstaclesForm(enum.Enum): + """Obstacle form""" + CUBE = 0 + SPHERE = 1 + RANDOM = 2 + + +# ba_meta export game +class QuakeGame(ba.TeamGameActivity[Player, Team]): + """Quake Team Game Activity""" + name = 'Quake' + description = 'Kill a set number of enemies to win.' + available_settings = [ + ba.IntSetting( + 'Kills to Win Per Player', + default=15, + min_value=1, + increment=1, + ), + ba.IntChoiceSetting( + 'Time Limit', + choices=[('None', 0), ('1 Minute', 60), ('2 Minutes', 120), + ('5 Minutes', 300), ('10 Minutes', 600), + ('20 Minutes', 1200)], + default=0, + ), + ba.FloatChoiceSetting( + 'Respawn Times', + choices=[('At once', 0.0), ('Shorter', 0.25), ('Short', 0.5), + ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)], + default=1.0, + ), + ba.BoolSetting( + 'Speed', + default=True, + ), + ba.BoolSetting( + 'Enable Jump', + default=True, + ), + ba.BoolSetting( + 'Enable Pickup', + default=True, + ), + ba.BoolSetting( + 'Enable Bomb', + default=False, + ), + ba.BoolSetting( + 'Obstacles', + default=True, + ), + ba.IntChoiceSetting( + 'Obstacles Form', + choices=[('Cube', ObstaclesForm.CUBE.value), + ('Sphere', ObstaclesForm.SPHERE.value), + ('Random', ObstaclesForm.RANDOM.value)], + default=0, + ), + ba.IntChoiceSetting( + 'Weapon Type', + choices=[('Rocket', WeaponType.ROCKET.value), + ('Railgun', WeaponType.RAILGUN.value)], + default=WeaponType.ROCKET.value, + ), + ba.BoolSetting( + 'Obstacles Mirror Shots', + default=False, + ), + ba.IntSetting( + 'Obstacles Count', + default=16, + min_value=0, + increment=2, + ), + ba.BoolSetting( + 'Random Obstacles Color', + default=True, + ), + ba.BoolSetting( + 'Epic Mode', + default=False, + ), + ] + + @classmethod + def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: + return issubclass(sessiontype, ba.MultiTeamSession) or issubclass( + sessiontype, ba.FreeForAllSession) + + @classmethod + def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: + # TODO add more maps + return ['Football Stadium', 'Monkey Face', 'Doom Shroom'] + + def __init__(self, settings) -> None: + super().__init__(settings) + self._epic_mode = self.settings_raw['Epic Mode'] + self._score_to_win = self.settings_raw['Kills to Win Per Player'] + self._time_limit = self.settings_raw['Time Limit'] + self._obstacles_enabled = self.settings_raw['Obstacles'] + self._obstacles_count = self.settings_raw['Obstacles Count'] + self._speed_enabled = self.settings_raw['Speed'] + self._bomb_enabled = self.settings_raw['Enable Bomb'] + self._pickup_enabled = self.settings_raw['Enable Pickup'] + self._jump_enabled = self.settings_raw['Enable Jump'] + self._weapon_type = WeaponType(self.settings_raw['Weapon Type']) + self.default_music = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.GRAND_ROMP) + self.slow_motion = self._epic_mode + + self.announce_player_deaths = True + self._scoreboard = Scoreboard() + self._ding_sound = ba.getsound('dingSmall') + + self._shield_dropper: Optional[ba.Timer] = None + + def get_instance_description(self) -> Union[str, Sequence]: + return 'Kill ${ARG1} enemies.', self._score_to_win + + def on_team_join(self, team: Team) -> None: + team.score = 0 + if self.has_begun(): + self._update_scoreboard() + + def on_begin(self) -> None: + ba.TeamGameActivity.on_begin(self) + ba.getactivity().globalsnode.tint = (0.5, 0.7, 1) + self.drop_shield() + self._shield_dropper = ba.Timer(8, + ba.WeakCall(self.drop_shield), + repeat=True) + self.setup_standard_time_limit(self._time_limit) + if self._obstacles_enabled: + count = self._obstacles_count + gamemap = self.map.getname() + for i in range(count): # TODO: tidy up around here + if gamemap == 'Football Stadium': + radius = (random.uniform(-10, 1), + 6, + random.uniform(-4.5, 4.5)) \ + if i > count / 2 else ( + random.uniform(10, 1), 6, random.uniform(-4.5, 4.5)) + else: + radius = (random.uniform(-10, 1), + 6, + random.uniform(-8, 8)) \ + if i > count / 2 else ( + random.uniform(10, 1), 6, random.uniform(-8, 8)) + + Obstacle( + position=radius, + mirror=self.settings_raw['Obstacles Mirror Shots'], + form=self.settings_raw['Obstacles Form']).autoretain() + + self._update_scoreboard() + + def drop_shield(self) -> None: + """Drop a shield powerup in random place""" + # FIXME: should use map defs + shield = PowerupBox(poweruptype='shield', + position=(random.uniform(-10, 10), 6, + random.uniform(-5, 5))).autoretain() + + ba.playsound(self._ding_sound) + + p_light = ba.newnode('light', + owner=shield.node, + attrs={ + 'position': (0, 0, 0), + 'color': (0.3, 0.0, 0.4), + 'radius': 0.3, + 'intensity': 2, + 'volume_intensity_scale': 10.0 + }) + + shield.node.connectattr('position', p_light, 'position') + + ba.animate(p_light, 'intensity', {0: 2, 8: 0}) + + def spawn_player(self, player: Player) -> None: + spaz = self.spawn_player_spaz(player) + if self._weapon_type == WeaponType.ROCKET: + quake.rocket.RocketLauncher().give(spaz) + elif self._weapon_type == WeaponType.RAILGUN: + quake.railgun.Railgun().give(spaz) + spaz.connect_controls_to_player(enable_jump=self._jump_enabled, + enable_pickup=self._pickup_enabled, + enable_bomb=self._bomb_enabled, + enable_fly=False) + + spaz.node.hockey = self._speed_enabled + spaz.spaz_light = ba.newnode('light', + owner=spaz.node, + attrs={ + 'position': (0, 0, 0), + 'color': spaz.node.color, + 'radius': 0.12, + 'intensity': 1, + 'volume_intensity_scale': 10.0 + }) + + spaz.node.connectattr('position', spaz.spaz_light, 'position') + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.PlayerDiedMessage): + ba.TeamGameActivity.handlemessage(self, msg) + player = msg.getplayer(Player) + self.respawn_player(player) + killer = msg.getkillerplayer(Player) + if killer is None: + return + + # handle team-kills + if killer.team is player.team: + # in free-for-all, killing yourself loses you a point + if isinstance(self.session, ba.FreeForAllSession): + new_score = player.team.score - 1 + new_score = max(0, new_score) + player.team.score = new_score + # in teams-mode it gives a point to the other team + else: + ba.playsound(self._ding_sound) + for team in self.teams: + if team is not killer.team: + team.score += 1 + # killing someone on another team nets a kill + else: + killer.team.score += 1 + ba.playsound(self._ding_sound) + # in FFA show our score since its hard to find on + # the scoreboard + assert killer.actor is not None + # noinspection PyUnresolvedReferences + killer.actor.set_score_text(str(killer.team.score) + '/' + + str(self._score_to_win), + color=killer.team.color, + flash=True) + + self._update_scoreboard() + + # if someone has won, set a timer to end shortly + # (allows the dust to clear and draws to occur if + # deaths are close enough) + if any(team.score >= self._score_to_win for team in self.teams): + ba.timer(0.5, self.end_game) + + else: + ba.TeamGameActivity.handlemessage(self, msg) + + def _update_scoreboard(self) -> None: + for team in self.teams: + self._scoreboard.set_team_value(team, team.score, + self._score_to_win) + + def end_game(self) -> None: + results = ba.GameResults() + for team in self.teams: + results.set_team_score(team, team.score) + + self.end(results=results) + + +class Obstacle(ba.Actor): + """Scene object""" + + def __init__(self, + position, + form=ObstaclesForm.CUBE, + mirror=False) -> None: + ba.Actor.__init__(self) + + if form == ObstaclesForm.CUBE: + model = 'tnt' + body = 'crate' + elif form == ObstaclesForm.SPHERE: + model = 'bomb' + body = 'sphere' + else: # ObstaclesForm.RANDOM: + model = random.choice(['tnt', 'bomb']) + body = 'sphere' if model == 'bomb' else 'crate' + + self.node = ba.newnode( + 'prop', + delegate=self, + attrs={ + 'position': + position, + 'model': + ba.getmodel(model), + 'body': + body, + 'body_scale': + 1.3, + 'model_scale': + 1.3, + 'reflection': + 'powerup', + 'reflection_scale': [0.7], + 'color_texture': + ba.gettexture('bunnyColor'), + 'materials': [SharedObjects.get().footing_material] + if mirror else [ + SharedObjects.get().object_material, + SharedObjects.get().footing_material + ] + }) + + def handlemessage(self, msg: Any) -> Any: + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + + elif isinstance(msg, ba.OutOfBoundsMessage): + if self.node: + self.handlemessage(ba.DieMessage()) + + elif isinstance(msg, ba.HitMessage): + self.node.handlemessage('impulse', msg.pos[0], msg.pos[1], + msg.pos[2], msg.velocity[0], + msg.velocity[1], msg.velocity[2], + msg.magnitude, msg.velocity_magnitude, + msg.radius, 0, msg.velocity[0], + msg.velocity[1], msg.velocity[2]) diff --git a/dist/ba_root/mods/games/quake/railgun.py b/dist/ba_root/mods/games/quake/railgun.py new file mode 100644 index 0000000..4b863bb --- /dev/null +++ b/dist/ba_root/mods/games/quake/railgun.py @@ -0,0 +1,128 @@ +"""Quake Game Rocket weapon""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _ba +import ba + +from bastd.actor.playerspaz import PlayerSpaz + +if TYPE_CHECKING: + from typing import Optional, Any + from bastd.actor.spaz import Spaz + +STORAGE_ATTR_NAME = f'_shared_{__name__}_factory' + + +class Railgun: + """Very dangerous weapon""" + + def __init__(self) -> None: + self.last_shot: Optional[int, float] = 0 + + def give(self, spaz: Spaz) -> None: + """Give spaz a railgun""" + spaz.punch_callback = self.shot + self.last_shot = ba.time() + + # FIXME + # noinspection PyUnresolvedReferences + def shot(self, spaz: Spaz) -> None: + """Release a rocket""" + time = ba.time() + if time - self.last_shot > 0.6: + self.last_shot = time + center = spaz.node.position_center + forward = spaz.node.position_forward + direction = [ + center[0] - forward[0], forward[1] - center[1], + center[2] - forward[2] + ] + direction[1] = 0.0 + + RailBullet(position=spaz.node.position, + direction=direction, + owner=spaz.getplayer(ba.Player), + source_player=spaz.getplayer(ba.Player), + color=spaz.node.color).autoretain() + + +class TouchedToSpazMessage: + """I hit!""" + + def __init__(self, spaz) -> None: + self.spaz = spaz + + +class RailBullet(ba.Actor): + """Railgun bullet""" + + def __init__(self, + position=(0, 5, 0), + direction=(0, 2, 0), + source_player=None, + owner=None, + color=(1, 1, 1)) -> None: + super().__init__() + self._color = color + + self.node = ba.newnode('light', + delegate=self, + attrs={ + 'position': position, + 'color': self._color + }) + ba.animate(self.node, 'radius', {0: 0, 0.1: 0.5, 0.5: 0}) + + self.source_player = source_player + self.owner = owner + self._life_timer = ba.Timer( + 0.5, ba.WeakCall(self.handlemessage, ba.DieMessage())) + + pos = position + vel = tuple(i / 5 for i in ba.Vec3(direction).normalized()) + for _ in range(500): # Optimization :( + ba.newnode('explosion', + owner=self.node, + attrs={ + 'position': pos, + 'radius': 0.2, + 'color': self._color + }) + pos = (pos[0] + vel[0], pos[1] + vel[1], pos[2] + vel[2]) + + for node in _ba.getnodes(): + if node and node.getnodetype() == 'spaz': + # pylint: disable=invalid-name + m3 = ba.Vec3(position) + a = ba.Vec3(direction[2], direction[1], direction[0]) + m1 = ba.Vec3(node.position) + # pylint: enable=invalid-name + # distance between node and line + dist = (a * (m1 - m3)).length() / a.length() + if dist < 0.3: + if node and node != self.owner and node.getdelegate( + PlayerSpaz, True).getplayer( + ba.Player, True).team != self.owner.team: + node.handlemessage(ba.FreezeMessage()) + pos = self.node.position + hit_dir = (0, 10, 0) + + node.handlemessage( + ba.HitMessage(pos=pos, + magnitude=50, + velocity_magnitude=50, + radius=0, + srcnode=self.node, + source_player=self.source_player, + force_direction=hit_dir)) + + def handlemessage(self, msg: Any) -> Any: + super().handlemessage(msg) + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + + elif isinstance(msg, ba.OutOfBoundsMessage): + self.handlemessage(ba.DieMessage()) diff --git a/dist/ba_root/mods/games/quake/rocket.py b/dist/ba_root/mods/games/quake/rocket.py new file mode 100644 index 0000000..43193e0 --- /dev/null +++ b/dist/ba_root/mods/games/quake/rocket.py @@ -0,0 +1,170 @@ +"""Quake Game Rocket weapon""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba + +from bastd.actor.bomb import Blast +from bastd.gameutils import SharedObjects + +if TYPE_CHECKING: + from typing import Optional, Any + from bastd.actor.spaz import Spaz + +STORAGE_ATTR_NAME = f'_shared_{__name__}_factory' + + +class RocketFactory: + """Quake Rocket factory""" + + def __init__(self) -> None: + self.ball_material = ba.Material() + + self.ball_material.add_actions( + conditions=((('we_are_younger_than', 5), 'or', + ('they_are_younger_than', 5)), 'and', + ('they_have_material', + SharedObjects.get().object_material)), + actions=('modify_node_collision', 'collide', False)) + + self.ball_material.add_actions( + conditions=('they_have_material', + SharedObjects.get().pickup_material), + actions=('modify_part_collision', 'use_node_collide', False)) + + self.ball_material.add_actions(actions=('modify_part_collision', + 'friction', 0)) + + self.ball_material.add_actions( + conditions=(('they_have_material', + SharedObjects.get().footing_material), 'or', + ('they_have_material', + SharedObjects.get().object_material)), + actions=('message', 'our_node', 'at_connect', ImpactMessage())) + + @classmethod + def get(cls): + """Get factory if exists else create new""" + activity = ba.getactivity() + if hasattr(activity, STORAGE_ATTR_NAME): + return getattr(activity, STORAGE_ATTR_NAME) + factory = cls() + setattr(activity, STORAGE_ATTR_NAME, factory) + return factory + + +class RocketLauncher: + """Very dangerous weapon""" + + def __init__(self): + self.last_shot: Optional[int, float] = 0 + + def give(self, spaz: Spaz) -> None: + """Give spaz a rocket launcher""" + spaz.punch_callback = self.shot + self.last_shot = ba.time() + + # FIXME + # noinspection PyUnresolvedReferences + def shot(self, spaz: Spaz) -> None: + """Release a rocket""" + time = ba.time() + if time - self.last_shot > 0.6: + self.last_shot = time + center = spaz.node.position_center + forward = spaz.node.position_forward + direction = [center[0] - forward[0], forward[1] - center[1], + center[2] - forward[2]] + direction[1] = 0.0 + + mag = 10.0 / ba.Vec3(*direction).length() + vel = [v * mag for v in direction] + Rocket(position=spaz.node.position, + velocity=vel, + owner=spaz.getplayer(ba.Player), + source_player=spaz.getplayer(ba.Player), + color=spaz.node.color).autoretain() + + +class ImpactMessage: + """Rocket touched something""" + + +class Rocket(ba.Actor): + """Epic rocket from rocket launcher""" + + def __init__(self, + position=(0, 5, 0), + velocity=(1, 0, 0), + source_player=None, + owner=None, + color=(1.0, 0.2, 0.2)) -> None: + super().__init__() + self.source_player = source_player + self.owner = owner + self._color = color + factory = RocketFactory.get() + + self.node = ba.newnode('prop', + delegate=self, + attrs={ + 'position': position, + 'velocity': velocity, + 'model': ba.getmodel('impactBomb'), + 'body': 'sphere', + 'color_texture': ba.gettexture( + 'bunnyColor'), + 'model_scale': 0.2, + 'is_area_of_interest': True, + 'body_scale': 0.8, + 'materials': [ + SharedObjects.get().object_material, + factory.ball_material] + }) # yapf: disable + self.node.extra_acceleration = (self.node.velocity[0] * 200, 0, + self.node.velocity[2] * 200) + + self._life_timer = ba.Timer( + 5, ba.WeakCall(self.handlemessage, ba.DieMessage())) + + self._emit_timer = ba.Timer(0.001, ba.WeakCall(self.emit), repeat=True) + self.base_pos_y = self.node.position[1] + + ba.camerashake(5.0) + + def emit(self) -> None: + """Emit a trace after rocket""" + ba.emitfx(position=self.node.position, + scale=0.4, + spread=0.01, + chunk_type='spark') + if not self.node: + return + self.node.position = (self.node.position[0], self.base_pos_y, + self.node.position[2]) # ignore y + ba.newnode('explosion', + owner=self.node, + attrs={ + 'position': self.node.position, + 'radius': 0.2, + 'color': self._color + }) + + def handlemessage(self, msg: Any) -> Any: + """Message handling for rocket""" + super().handlemessage(msg) + if isinstance(msg, ImpactMessage): + self.node.handlemessage(ba.DieMessage()) + + elif isinstance(msg, ba.DieMessage): + if self.node: + Blast(position=self.node.position, + blast_radius=2, + source_player=self.source_player) + + self.node.delete() + self._emit_timer = None + + elif isinstance(msg, ba.OutOfBoundsMessage): + self.handlemessage(ba.DieMessage()) diff --git a/dist/ba_root/mods/games/soccer.so b/dist/ba_root/mods/games/soccer.so new file mode 100644 index 0000000..a78c494 Binary files /dev/null and b/dist/ba_root/mods/games/soccer.so differ diff --git a/dist/ba_root/mods/maps/BasketBallMap.py b/dist/ba_root/mods/maps/BasketBallMap.py new file mode 100644 index 0000000..d93733c --- /dev/null +++ b/dist/ba_root/mods/maps/BasketBallMap.py @@ -0,0 +1,88 @@ +import ba +from bastd.gameutils import SharedObjects +from bastd.actor import playerspaz as ps +from bastd import maps + + +from typing import Any, Sequence, Dict, Type, List, Optional, Union + +class BasketMap(maps.FootballStadium): + name = 'BasketBall Stadium' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return [] + + def __init__(self) -> None: + super().__init__() + + gnode = ba.getactivity().globalsnode + gnode.tint = [(0.806, 0.8, 1.0476), (1.3, 1.2, 1.0)][0] + gnode.ambient_color = (1.3, 1.2, 1.0) + gnode.vignette_outer = (0.57, 0.57, 0.57) + gnode.vignette_inner = (0.9, 0.9, 0.9) + gnode.vr_camera_offset = (0, -0.8, -1.1) + gnode.vr_near_clip = 0.5 + +class BasketMapV2(maps.HockeyStadium): + name = 'BasketBall Stadium V2' + + def __init__(self) -> None: + super().__init__() + + shared = SharedObjects.get() + self.node.materials = [shared.footing_material] + self.node.collide_model = ba.getcollidemodel('footballStadiumCollide') + self.node.model = None + self.stands.model = None + self.floor.reflection = 'soft' + self.floor.reflection_scale = [1.6] + self.floor.color = (1.1, 0.05, 0.8) + + self.background = ba.newnode('terrain', + attrs={'model': ba.getmodel('thePadBG'), + 'lighting': False, + 'background': True, + 'color': (1.0, 0.2, 1.0), + 'color_texture': ba.gettexture('menuBG')}) + + gnode = ba.getactivity().globalsnode + gnode.floor_reflection = True + gnode.debris_friction = 0.3 + gnode.debris_kill_height = -0.3 + gnode.tint = [(1.2, 1.3, 1.33), (0.7, 0.9, 1.0)][1] + gnode.ambient_color = (1.15, 1.25, 1.6) + gnode.vignette_outer = (0.66, 0.67, 0.73) + gnode.vignette_inner = (0.93, 0.93, 0.95) + gnode.vr_camera_offset = (0, -0.8, -1.1) + gnode.vr_near_clip = 0.5 + self.is_hockey = False + + ################## + self.collision = ba.Material() + self.collision.add_actions( + actions=(('modify_part_collision', 'collide', True))) + + self.regions: List[ba.Node] = [ + ba.newnode('region', + attrs={'position': (12.676897048950195, 0.2997918128967285, 5.583303928375244), + 'scale': (1.01, 12, 28), + 'type': 'box', + 'materials': [self.collision]}), + + ba.newnode('region', + attrs={'position': (11.871315956115723, 0.29975247383117676, 5.711406707763672), + 'scale': (50, 12, 0.9), + 'type': 'box', + 'materials': [self.collision]}), + + ba.newnode('region', + attrs={'position': (-12.776557922363281, 0.30036890506744385, 4.96237850189209), + 'scale': (1.01, 12, 28), + 'type': 'box', + 'materials': [self.collision]}), + ] + +ba._map.register_map(BasketMap) +ba._map.register_map(BasketMapV2) \ No newline at end of file diff --git a/dist/ba_root/mods/maps/BridgitMash.so b/dist/ba_root/mods/maps/BridgitMash.so new file mode 100644 index 0000000..8488aeb Binary files /dev/null and b/dist/ba_root/mods/maps/BridgitMash.so differ diff --git a/dist/ba_root/mods/maps/BridgitParallelo.so b/dist/ba_root/mods/maps/BridgitParallelo.so new file mode 100644 index 0000000..7714fa3 Binary files /dev/null and b/dist/ba_root/mods/maps/BridgitParallelo.so differ diff --git a/dist/ba_root/mods/maps/MemoryGame.py b/dist/ba_root/mods/maps/MemoryGame.py new file mode 100644 index 0000000..9e5c8cb --- /dev/null +++ b/dist/ba_root/mods/maps/MemoryGame.py @@ -0,0 +1,56 @@ +import ba +from bastd.gameutils import SharedObjects + +from typing import Any, Sequence, Optional, List, Dict, Type, Type , Union, Any, Literal + +class MGdefs(): + points = {} + boxes = {} + points['flag_default'] = (0.17358, 3.75764, 1.99124) + boxes['area_of_interest_bounds'] = (0.3544110667, 4.493562578, -2.518391331) + (0.0, 0.0, 0.0) + (16.64754831, 8.06138989, 18.5029888) + boxes['map_bounds'] = (0.2608783669, 4.899663734, -3.543675157) + (0.0, 0.0, 0.0) + (29.23565494, 14.19991443, 29.92689344) + +class MGmap(ba.Map): + defs = MGdefs() + name = 'Sky Tiles' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return [] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'achievementOffYouGo' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'bgtex': ba.gettexture('menuBG'), + 'bgmodel': ba.getmodel('thePadBG') + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (1.3, 1.2, 1.0) + gnode.ambient_color = (1.3, 1.2, 1.0) + gnode.vignette_outer = (0.57, 0.57, 0.57) + gnode.vignette_inner = (0.9, 0.9, 0.9) + gnode.vr_camera_offset = (0, -0.8, -1.1) + gnode.vr_near_clip = 0.5 + + + + +ba._map.register_map(MGmap) \ No newline at end of file diff --git a/dist/ba_root/mods/maps/VolleBallMap.py b/dist/ba_root/mods/maps/VolleBallMap.py new file mode 100644 index 0000000..65eacb9 --- /dev/null +++ b/dist/ba_root/mods/maps/VolleBallMap.py @@ -0,0 +1,307 @@ + +import ba,_ba +from bastd.gameutils import SharedObjects + +from typing import Any, Sequence, Optional, List, Dict, Type, Type , Union, Any, Literal + +a=0.0 +class Pointzz: + points, boxes = {}, {} + points['ffa_spawn1'] = (-0.08016, 0.02275, -4.37367) + (8.89506, 0.05, 0.44435) + points['ffa_spawn2'] = (-0.08016, 0.02275, 4.07629) + (8.89506, 0.05, 0.44435) + points['flag1'] = (-10.72073, 0.06537, 0.14648) + points['flag2'] = (10.7587, 0.04779, 0.14648) + points['flag_default'] = (-0.10014, 0.0418, 0.10956) + points['powerup_spawn1'] = (500000000.41468, 50000.9515, -500000.03791) + points['powerup_spawn2'] = (-500000.5554, 500000.9515, -500000.03791) + points['powerup_spawn3'] = (500000.41468, 50000.9515, 5000.14822) + points['powerup_spawn4'] = (-50000.73727, 50000.9515, 500.14822) + points['spawn1'] = (-8.03866, 0.02275, 0.0) + (0.5, 0.05, 4.0) + points['spawn2'] = (8.82311, 0.01092, 0.0) + (0.5, 0.05, 4.0) + boxes['area_of_interest_bounds'] = (0.0, 1.18575, 0.43262) + (0, 0, 0) + (29.81803, 11.57249, 18.89134) + boxes['tnt1'] = (-0.10387, 0.41333, 0.42947) + (0, 0, 0) + (22.48296, 1.29024, 8.99025) + boxes['goal1'] = (10,0.001,8) + boxes['goal2'] = (10,0.001,8) + boxes['map_bounds'] = (0.0, 1.185751251, 0.4326226188) + (0.0, 0.0, 0.0) + ( + 42.09506485, 22.81173179, 29.76723155) + +class PointzzforH: + points, boxes = {}, {} + boxes['area_of_interest_bounds'] = (0.0, 0.7956858119, 0.0) + ( + 0.0, 0.0, 0.0) + (30.80223883, 0.5961646365, 13.88431707) + points['ffa_spawn1'] = (-0.001925625146, 0.02305323209, + -3.81971842) + (7.828121539, 1.0, 0.1588021252) + points['ffa_spawn2'] = (-0.001925625146, 0.02305323209, + 3.560115735) + (7.828121539, 1.0, 0.05859841271) + points['flag1'] = (-11.21689747, 0.09527878981, -0.07659307272) + points['flag2'] = (11.08204909, 0.04119542459, -0.07659307272) + points['flag_default'] = (-0.01690735171, 0.06139940044, -0.07659307272) + boxes['goal1'] = (10,0.001,8) + boxes['goal2'] = (10,0.001,8) + boxes['map_bounds'] = (0.0, 0.7956858119, -0.4689020853) + (0.0, 0.0, 0.0) + ( + 35.16182389, 12.18696164, 21.52869693) + points['powerup_spawn1'] = (-3.654355317, 1.080990833, -4.765886164) + points['powerup_spawn2'] = (-3.654355317, 1.080990833, 4.599802158) + points['powerup_spawn3'] = (2.881071011, 1.080990833, -4.765886164) + points['powerup_spawn4'] = (2.881071011, 1.080990833, 4.599802158) + points['spawn1'] = (-6.835352227, 0.02305323209, 0.0) + (1.0, 1.0, 3.0) + points['spawn2'] = (6.857415055, 0.03938567998, 0.0) + (1.0, 1.0, 3.0) + points['tnt1'] = (-0.05791962398, 1.080990833, -4.765886164) + +class VolleyBallMap(ba.Map): + defs = Pointzz + name = "Open Field" + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return [] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'footballStadiumPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('footballStadium'), + 'vr_fill_model': ba.getmodel('footballStadiumVRFill'), + 'collide_model': ba.getcollidemodel('footballStadiumCollide'), + 'tex': ba.gettexture('footballStadium') + } + return data + + def __init__(self): + super().__init__() + shared = SharedObjects.get() + + ## Hey Quasi thx for looping these xD ## + + + + x = -5 + while x<5: + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(0,0,x), + 'color':(1,1,0),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(0,.25,x), + 'color':(1,1,0),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(0,.5,x), + 'color':(1,1,0),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(0,.75,x), + 'color':(1,1,0),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(0,1,x), + 'color':(1,1,0),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + x = x + 0.5 + + + + + + + + + + y = -1 + while y>-11: + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(y,0.01,4), + 'color':(0,0,1),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(y,0.01,-4), + 'color':(0,0,1),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(-y,0.01,4), + 'color':(1,0,0),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(-y,0.01,-4), + 'color':(1,0,0),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + y-=1 + + + + z = 0 + while z<5: + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(11,0.01,z), + 'color':(1,0,0),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(11,0.01,-z), + 'color':(1,0,0),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(-11,0.01,z), + 'color':(0,0,1),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(-11,0.01,-z), + 'color':(0,0,1),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + z+=1 + + + + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'model': self.preloaddata['model'], + 'collide_model': self.preloaddata['collide_model'], + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['vr_fill_model'], + 'lighting': False, + 'vr_only': True, + 'background': True, + 'color_texture': self.preloaddata['tex'] + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (1.3, 1.2, 1.0) + gnode.ambient_color = (1.3, 1.2, 1.0) + gnode.vignette_outer = (0.57, 0.57, 0.57) + gnode.vignette_inner = (0.9, 0.9, 0.9) + gnode.vr_camera_offset = (0, -0.8, -1.1) + gnode.vr_near_clip = 0.5 + + def is_point_near_edge(self, + point: ba.Vec3, + running: bool = False) -> bool: + box_position = self.defs.boxes['edge_box'][0:3] + box_scale = self.defs.boxes['edge_box'][6:9] + xpos = (point.x - box_position[0]) / box_scale[0] + zpos = (point.z - box_position[2]) / box_scale[2] + return xpos < -0.5 or xpos > 0.5 or zpos < -0.5 or zpos > 0.5 + +class VolleyBallMapH(ba.Map): + """Stadium map used for ice hockey games.""" + + defs = PointzzforH + name = 'Closed Arena' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return [] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'hockeyStadiumPreview' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'models': (ba.getmodel('hockeyStadiumOuter'), + ba.getmodel('hockeyStadiumInner')), + 'vr_fill_model': ba.getmodel('footballStadiumVRFill'), + 'collide_model': ba.getcollidemodel('hockeyStadiumCollide'), + 'tex': ba.gettexture('hockeyStadium'), + } + mat = ba.Material() + mat.add_actions(actions=('modify_part_collision', 'friction', 0.01)) + data['ice_material'] = mat + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + + ## Hey Quasi thx for looping these xD ## + + + x = -5 + while x<5: + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(0,0,x), + 'color':(1,1,0),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(0,.25,x), + 'color':(1,1,0),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(0,.5,x), + 'color':(1,1,0),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(0,.75,x), + 'color':(1,1,0),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(0,1,x), + 'color':(1,1,0),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + x = x + 0.5 + + + + + + + + + + y = -1 + while y>-11: + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(y,0.01,4), + 'color':(0,0,1),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(y,0.01,-4), + 'color':(0,0,1),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(-y,0.01,4), + 'color':(1,0,0),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(-y,0.01,-4), + 'color':(1,0,0),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + y-=1 + + + + z = 0 + while z<5: + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(11,0.01,z), + 'color':(1,0,0),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(11,0.01,-z), + 'color':(1,0,0),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(-11,0.01,z), + 'color':(0,0,1),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + self.zone = ba.newnode('locator',attrs={'shape':'circle','position':(-11,0.01,-z), + 'color':(0,0,1),'opacity':1,'draw_beauty':True,'additive':False,'size':[0.40]}) + z+=1 + + + + self.node = ba.newnode('terrain', + delegate=self, + attrs={ + 'model': + None, + 'collide_model': + ba.getcollidemodel('footballStadiumCollide'), ## we dont want Goalposts... + 'color_texture': + self.preloaddata['tex'], + 'materials': [ + shared.footing_material] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['vr_fill_model'], + 'vr_only': True, + 'lighting': False, + 'background': True, + }) + mats = [shared.footing_material] + self.floor = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['models'][1], + 'color_texture': self.preloaddata['tex'], + 'opacity': 0.92, + 'opacity_in_low_or_medium_quality': 1.0, + 'materials': mats, + 'color': (0.4,0.9,0) + }) + + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': ba.getmodel('natureBackground'), + 'lighting': False, + 'background': True, + 'color': (0.5,0.30,0.4) + }) + + gnode = ba.getactivity().globalsnode + gnode.floor_reflection = True + gnode.debris_friction = 0.3 + gnode.debris_kill_height = -0.3 + gnode.tint = (1.2, 1.3, 1.33) + gnode.ambient_color = (1.15, 1.25, 1.6) + gnode.vignette_outer = (0.66, 0.67, 0.73) + gnode.vignette_inner = (0.93, 0.93, 0.95) + gnode.vr_camera_offset = (0, -0.8, -1.1) + gnode.vr_near_clip = 0.5 + #self.is_hockey = True + + +## Plugin only for our dirty map UwU ## + +ba._map.register_map(VolleyBallMap) +ba._map.register_map(VolleyBallMapH) diff --git a/dist/ba_root/mods/maps/WoodenFloor.py b/dist/ba_root/mods/maps/WoodenFloor.py new file mode 100644 index 0000000..ad31dd4 --- /dev/null +++ b/dist/ba_root/mods/maps/WoodenFloor.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import ba,_ba +from bastd.gameutils import SharedObjects +from bastd.actor.playerspaz import PlayerSpaz +if TYPE_CHECKING: + from typing import Any, List, Dict + + +class WoodenFloor(ba.Map): + """Stadium map for football games.""" + from bastd.mapdata import football_stadium as defs + defs.points['spawn1'] = (-12.03866341, 0.02275111462, 0.0) + (0.5, 1.0, 4.0) + defs.points['spawn2'] = (12.823107149, 0.01092306765, 0.0) + (0.5, 1.0, 4.0) + name = 'Wooden Floor' + + @classmethod + def get_play_types(cls) -> list[str]: + """Return valid play types for this map.""" + return ['melee', 'football', 'team_flag', 'keep_away'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'footballStadiumPreview' + + @classmethod + def on_preload(cls) -> Any: + data: dict[str, Any] = { + + 'model_bg': ba.getmodel('doomShroomBG'), + 'bg_vr_fill_model': ba.getmodel('natureBackgroundVRFill'), + 'collide_model': ba.getcollidemodel('bridgitLevelCollide'), + 'tex': ba.gettexture('bridgitLevelColor'), + 'model_bg_tex': ba.gettexture('doomShroomBGColor'), + 'collide_bg': ba.getcollidemodel('natureBackgroundCollide'), + 'railing_collide_model': + (ba.getcollidemodel('bridgitLevelRailingCollide')), + 'bg_material': ba.Material() + } + data['bg_material'].add_actions(actions=('modify_part_collision', + 'friction', 10.0)) + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['model_bg'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['model_bg_tex'] + }) + ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bg_vr_fill_model'], + 'lighting': False, + 'vr_only': True, + 'background': True, + 'color_texture': self.preloaddata['model_bg_tex'] + }) + gnode = ba.getactivity().globalsnode + gnode.tint = (1.3, 1.2, 1.0) + gnode.ambient_color = (1.3, 1.2, 1.0) + gnode.vignette_outer = (0.57, 0.57, 0.57) + gnode.vignette_inner = (0.9, 0.9, 0.9) + gnode.vr_camera_offset = (0, -0.8, -1.1) + gnode.vr_near_clip = 0.5 + # self.map_extend() + + def is_point_near_edge(self, + point: ba.Vec3, + running: bool = False) -> bool: + box_position = self.defs.boxes['edge_box'][0:3] + box_scale = self.defs.boxes['edge_box'][6:9] + xpos = (point.x - box_position[0]) / box_scale[0] + zpos = (point.z - box_position[2]) / box_scale[2] + return xpos < -0.5 or xpos > 0.5 or zpos < -0.5 or zpos > 0.5 + + + def _handle_player_collide(self): + try: + player = ba.getcollision().opposingnode.getdelegate( + PlayerSpaz, True) + except ba.NotFoundError: + return + + + if player.is_alive(): + player.shatter(True) + + + + +ba._map.register_map(WoodenFloor) \ No newline at end of file diff --git a/dist/ba_root/mods/maps/baAllMaps.py b/dist/ba_root/mods/maps/baAllMaps.py new file mode 100644 index 0000000..0130542 --- /dev/null +++ b/dist/ba_root/mods/maps/baAllMaps.py @@ -0,0 +1,777 @@ +# ba_meta require api 6 + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# - Plugin SEBASTIAN2059 - Zacker Tz- - - - - - - - - - - - - - - - +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# - Maps: - +# - Neo Zone v1 - by Zacker Tz || Zacker#5505 - +# - Big H v1 - by SEBASTIAN2059 || SEBASTIAN2059#5751 - +# - The Limbo v2 - by Zacker Tz || Zacker#5505 - +# - Platforms v2 - by SEBASTIAN2059 || SEBASTIAN2059#5751 - +# - Powerups Factory v2 - by Zacker Tz || Zacker#5505 - +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# - Notes +# - The Powerups Factory map generates some lag, use it at your own risk +# - El mapa Powerups Factory genera un poco de lag, usalo bajo tu propio riesgo +# - - - - +# - New maps comingsoon!!!!1111!11!!111!11!!1111 + + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from bastd.maps import * +import ba +import _ba +import base64 +from ba import _map +import random + +if TYPE_CHECKING: + from typing import Any, List, Dict + +##Complementos## +class FadeEfect(): + def __init__(self, map_tint = (1,1,1)): + gnode = ba.getactivity().globalsnode + ba.animate_array(gnode,'tint',3,{0:(0,0,0),0.5:(0,0,0),1.2:map_tint}) + + text = ba.newnode('text', + attrs={ + 'position': (0,325), + 'text': 'Building Map...', + 'color': (1,1,1), + 'h_align': 'center','v_align': 'center', 'vr_depth': 410, 'maxwidth': 600, 'shadow': 1.0, 'flatness': 1.0, 'scale':2, 'h_attach': 'center', 'v_attach': 'bottom'}) + ba.animate(text,'opacity',{0:1,0.2:1,0.7:0}) + ba.timer(1,text.delete) + + text = ba.newnode('text', + attrs={ + 'position': (0,295), + 'text': 'Maps by Sebastian2059-ZackerTz', + 'color': (0.1,0.0,0.76), + 'h_align': 'center', 'v_align': 'center', 'vr_depth': 410, 'maxwidth': 600, 'shadow': 1.0, 'flatness': 1.0, 'scale':0.7, 'h_attach': 'center', 'v_attach': 'bottom'}) + ba.animate(text,'opacity',{0:1,0.2:1,0.7:0}) + ba.timer(1,text.delete) + + +class Credits: + """ Don't delete this if you respect other people's work""" + def __init__(self): + exec(base64.b64decode("dCA9IGJhLm5ld25vZGUoJ3RleHQnLAogICAgICAgICAgICAgICBhdHRycz17ICd0ZXh0JzoiTWFwcyBieTogU0VCQVNUSUFOMjA1OS1aYWNrZXIgVHoiLCAKICAgICAgICAnc2NhbGUnOjAuNiwKICAgICAgICAncG9zaXRpb24nOigwLDApLCAKICAgICAgICAnb3BhY2l0eSc6IDAuNCwKICAgICAgICAnc2hhZG93JzowLjUsCiAgICAgICAgJ2ZsYXRuZXNzJzoxLjIsCiAgICAgICAgJ2NvbG9yJzooMSwgMSwgMSksCiAgICAgICAgJ2hfYWxpZ24nOidjZW50ZXInLAogICAgICAgICd2X2F0dGFjaCc6J2JvdHRvbSd9KQ==").decode('UTF-8')) # :bobolu: +###End### + + +#Map by Zacker Tz +#Map #1 +class neo_defs(): + boxes = {} + points = {} + boxes['area_of_interest_bounds'] = (0, 4, 0) + (0, 0, 0) + (50, 10, 20) + boxes['edge_box'] = (0, 4, 0) + (0.0, 0.0, 0.0) + (40, 2, 10) + boxes['map_bounds'] = (0, 4, 0) + (0, 0, 0) + (28, 10, 28) + points['ffa_spawn1'] = (-10,3.17,0) + (1.0,0.1,1.0) + points['ffa_spawn2'] = (10,3.17,0) + (1.0,0.1,1.0) + points['ffa_spawn3'] = (-5.25,3.17,-1.75) + (0.5,0.1,0.5) + points['ffa_Spawn4'] = (5.25,3.17,-1.75) + (0.5,0.1,0.5) + points['spawn1'] = (-11,3.17,0) + (1.0,0.1,1.0) + points['spawn2'] = (11,3.17,0) + (1.0,0.1,1.0) + points['flag1'] = (-12.0,3.3,0) + (2.0,0.1,2.0) + points['flag2'] = (12.0,3.3,0) + (2.0,0.1,2.0) + points['flag_default'] = (0,3.3,1.75) + points['powerup_spawn1'] = (-11,4.0,-1.75) + points['powerup_spawn2'] = (-11,4.0,1.75) + points['powerup_spawn3'] = (-1.75,4.0,0) + points['powerup_spawn4'] = (1.75,4.0,0.0) + points['powerup_spawn5'] = (11,4.0,-1.75) + points['powerup_spawn6'] = (11,4.0,1.75) + + +class NeoZone(ba.Map): + """Agent john's former workplace""" + + defs = neo_defs() + name = 'Neo Zone' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee','king_of_the_hill','keep_away','team_flag'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'rgbStripes' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('landMine'), + 'tex': ba.gettexture('landMine'), + 'bgtex': ba.gettexture('black'), + 'bgmodel': ba.getmodel('thePadBG'), + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + + self._collide_with_player=ba.Material() + self._collide_with_player.add_actions(conditions=('we_are_older_than', 1), actions=(('modify_part_collision', 'collide', True))) + self.dont_collide=ba.Material() + self.dont_collide.add_actions(conditions=('they_are_different_node_than_us', ),actions=(('modify_part_collision', 'collide', False))) + + self._map_model = ba.getmodel('image1x1') + self._map_model2 = ba.getmodel('tnt') + self._map_tex = ba.gettexture('powerupIceBombs') + self._map_tex1 = ba.gettexture('ouyaUButton') + + self.background = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + + locations = [(7.0,0.0,0),(5.25,0.0,0),(5.25,0.0,-1.75), + (3.5,0.0,-1.75),(1.75,0.0,-1.75),(1.75,0.0,0), + (1.75,0.0,1.75), + (0,0.0,1.75), + (-7.0,0.0,0),(-5.25,0.0,0),(-5.25,3.17,-1.75), + (-3.5,0.0,-1.75),(-1.75,0.0,-1.75),(-1.75,0.0,0), + (-1.75,0.0,1.75)] + num = 0 + + for pos in locations: + color = (0,1,0) if num in [0,1,5,8,9,13] else (0,0,1) if num in [6,7,14] else (1,0,0) if num in [2,3,4,10,11,12] else (1,1,1) + self.decor = ba.newnode('prop', + attrs={'body': 'puck', + 'position': (pos[0],3.17,pos[2]), + 'model': self._map_model, + 'model_scale': 1.7, + 'body_scale': 0.1, + 'shadow_size': 0.0, + 'gravity_scale':0.0, + 'color_texture': self._map_tex1, + 'reflection': 'soft', + 'reflection_scale': [0.5], + 'is_area_of_interest': True, + 'materials': [self.dont_collide]}) + self.region = ba.newnode('region',attrs={ + 'position': (pos[0],2.3,pos[2]), + 'scale': (1.9,1.9,1.9), + 'type': 'box', + 'materials': (self._collide_with_player, shared.footing_material)}) + self.zone = ba.newnode('locator', + attrs={'shape':'box', + 'position':(pos[0],2.3,pos[2]), + 'color':color, + 'opacity':1,'draw_beauty':True,'additive':False,'size':[1.75,1.75,1.75]}) + num += 1 + + #Sides + side_locations = [(-10.5,2.3,0),(10.5,2.3,0)] + for pos in side_locations: + self.big_region = ba.newnode('region',attrs={ + 'position': pos, + 'scale': (5.7,1.9,5.7), + 'type': 'box', + 'materials': (self._collide_with_player, shared.footing_material)}) + self.big_zone = ba.newnode('locator', + attrs={'shape':'box', + 'position':pos, + 'color':(0,1,1.5), + 'opacity':1,'draw_beauty':True,'additive':False,'size':[5.25,1.75,5.25]}) + + exec(base64.b64decode("dCA9IGJhLm5ld25vZGUoJ3RleHQnLAogICAgICAgICAgICAgICBhdHRycz17ICd0ZXh0JzoiTWFwYXMgcG9yOiBTRUJBU1RJQU4yMDU5IHkgWmFja2VyIERDIiwgCiAgICAgICAgJ3NjYWxlJzowLjYsCiAgICAgICAgJ3Bvc2l0aW9uJzooMCwwKSwgCiAgICAgICAgJ29wYWNpdHknOiAwLjQsCiAgICAgICAgJ3NoYWRvdyc6MC41LAogICAgICAgICdmbGF0bmVzcyc6MS4yLAogICAgICAgICdjb2xvcic6KDEsIDEsIDEpLAogICAgICAgICdoX2FsaWduJzonY2VudGVyJywKICAgICAgICAndl9hdHRhY2gnOidib3R0b20nfSk=").decode('UTF-8')) #bubalu + + gnode = ba.getactivity().globalsnode + gnode.tint = (1.1, 1.05, 1.17) + gnode.happy_thoughts_mode = False + gnode.ambient_color = (1.2, 1.17, 1.1) + gnode.vignette_outer = (0.9, 0.9, 0.96) + gnode.vignette_inner = (0.95, 0.95, 0.93) + FadeEfect(gnode.tint) + Credits() + +#Map by Sebastian2059 +#Map #2 +class c_defs(): + boxes = {} + points = {} + boxes['area_of_interest_bounds'] = (0, 4, 0) + (0, 0, 0) + (50, 10, 20) + boxes['edge_box'] = (0, 4, 0) + (0.0, 0.0, 0.0) + (40, 2, 10) + boxes['map_bounds'] = (0, 4, 0) + (0, 0, 0) + (28, 10, 28) + points['ffa_spawn1'] = (-9,0.5,-3) + (1.0,0.1,5.0) + points['ffa_spawn2'] = (9,0.5,-3) + (1.0,0.1,5.0) + points['ffa_spawn3'] = (-6,0.5,-6.0) + (2.0,0.1,1.0) + points['ffa_Spawn4'] = (6,0.5,0.0) + (2.0,0.1,1.0) + points['ffa_spawn5'] = (6,0.5,-6.0) + (2.0,0.1,1.0) + points['ffa_Spawn6'] = (-6,0.5,0.0) + (2.0,0.1,1.0) + points['spawn1'] = (-9,0.5,-3) + (1.0,0.1,1.0) + points['spawn2'] = (9,0.5,-3) + (1.0,0.1,1.0) + points['flag1'] = (-10.0,0.8,-3) + (2.0,0.1,2.0) + points['flag2'] = (10.0,0.8,-3) + (2.0,0.1,2.0) + points['flag_default'] = (0,0.8,-3.0) + points['powerup_spawn1'] = (-9,1.0,-8) + points['powerup_spawn2'] = (-9,1.0,3) + points['powerup_spawn3'] = (-1.5,1.0,-8.25) + points['powerup_spawn4'] = (1.5,1.0,-8.25) + points['powerup_spawn5'] = (-1.5,1.0,2.25) + points['powerup_spawn6'] = (1.5,1.0,2.25) + points['powerup_spawn7'] = (9,1.0,-8) + points['powerup_spawn8'] = (9,1.0,3) + + points['race_mine1'] = (-1.5, 0.7, -0.7) + points['race_mine2'] = (-1.5, 0.7, 0.7) + points['race_mine3'] = (-4.5, 0.7, 0.0) + points['race_mine4'] = (4.5, 0.7, 0.0) + points['race_mine5'] = (4.5, 0.7, -6.0) + points['race_mine6'] = (-4.5, 0.7, -6.0) + points['race_mine7'] = (0.0, 0.7, -6.0) + points['race_mine8'] = (-10.0, 0.7, -4.5) + points['race_mine9'] = (10.0, 0.7, -4.5) + points['race_mine10'] = (10.0, 0.7, -1.5) + points['race_mine11'] = (-10.0, 0.7, -1.5) + + points['race_point1'] = (0.0, 0.5, 0.0) + (0.3, 2.0, 1.5) + points['race_point2'] = (3.5, 0.5, 0.0) + (0.3, 2.0, 1.5) + points['race_point3'] = (7.0, 0.5, 0.0) + (0.3, 2.0, 1.5) + points['race_point4'] = (9.0, 0.5, -2.0) + (1.5, 2.0, 0.3) + points['race_point5'] = (9.0, 0.5, -4.0) + (1.5, 2.0, 0.3) + points['race_point6'] = (7.0, 0.5, -6.0) + (0.3, 2.0, 1.5) + points['race_point7'] = (3.5, 0.5, -6.0) + (0.3, 2.0, 1.5) + points['race_point8'] = (0.0, 0.5, -6.0) + (0.3, 2.0, 1.5) + points['race_point9'] = (-3.5, 0.5, -6.0) + (0.3, 2.0, 1.5) + points['race_point10'] = (-7.0, 0.5, -6.0) + (0.3, 2.0, 1.5) + points['race_point11'] = (-9.0, 0.5, -2.0) + (1.5, 2.0, 0.3) + points['race_point12'] = (-9.0, 0.5, -4.0) + (1.5, 2.0, 0.3) + points['race_point13'] = (-7.0, 0.5, 0.0) + (0.3, 2.0, 1.5) + points['race_point14'] = (-3.5, 0.5, 0.0) + (0.3, 2.0, 1.5) + +class CMap(ba.Map): + """Jack Morgan used to run here""" + + defs = c_defs() + name = 'Big H' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee','king_of_the_hill','keep_away','team_flag','race'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'bigG' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('landMine'), + 'tex': ba.gettexture('landMine'), + 'bgtex': ba.gettexture('black'), + 'bgmodel': ba.getmodel('thePadBG'), + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + + self._collide_with_player=ba.Material() + self._collide_with_player.add_actions(conditions=('we_are_older_than', 1), actions=(('modify_part_collision', 'collide', True))) + self.dont_collide=ba.Material() + self.dont_collide.add_actions(conditions=('they_are_different_node_than_us', ),actions=(('modify_part_collision', 'collide', False))) + self.ice_material = ba.Material() + self.ice_material.add_actions(actions=('modify_part_collision','friction',0.01)) + + self._map_model = ba.getmodel('image1x1') + self._map_model2 = ba.getmodel('tnt') + self._map_tex = ba.gettexture('powerupIceBombs') + self._map_tex1 = ba.gettexture('circleOutlineNoAlpha') + self._map_tex2 = ba.gettexture('black') + + self.background = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + + posS = [(0.0,0.05,0)] + for m_pos in posS: + self.mv_center = ba.newnode('prop', + attrs={'body': 'puck', + 'position': m_pos, + 'model': self._map_model, + 'model_scale': 35, + 'body_scale': 0.1, + 'shadow_size': 0.0, + 'gravity_scale':0.0, + 'color_texture': self._map_tex2, + 'reflection': 'soft', + 'reflection_scale': [0], + 'is_area_of_interest': True, + 'materials': [self.dont_collide]}) + + locations = [(-9,0.0,-3.0),(9,0.0,-3.0),(0.0,0.0,-6.0),(0.0,0.0,0.0),(0.0,0.0,-3.0)] + scales = [[3.0,1.0,14.0],[3.0,1.0,14.0],[15.0,1.0,3.0],[15.0,1.0,3.0],[3.0,1.0,3.0]] + index = 0 + for pos in locations: + # + scale = scales[index] + ba.newnode('region',attrs={'position': pos,'scale': scale,'type': 'box','materials': (self._collide_with_player, shared.footing_material)}) + ba.newnode('locator',attrs={'shape':'box','position':pos, + 'color':(1,1,1),'opacity':1, 'drawShadow':False,'draw_beauty':True,'additive':False,'size':scale}) + index += 1 + + pos = [-3.0,0.0,-8.25] + for p in range(10): + scale = [1.5,1.0,1.5] + ba.newnode('region',attrs={'position': pos,'scale': scale,'type': 'box','materials': (self._collide_with_player, shared.footing_material)}) + ba.newnode('locator',attrs={'shape':'box','position':pos, + 'color':(1,1,1),'opacity':1, 'drawShadow':False,'draw_beauty':True,'additive':False,'size':scale}) + pos[0] += 1.5 + if p == 4: + pos[0] = -3.0 + pos[2] = 2.25 + + try: + self._gamemode = ba.getactivity().name + except Exception: + print('error') + pass + if self._gamemode == 'Race': + print('Es carrera') + ice_locations = [(-8,0.0,0),(8,0.0,0), + (-8,0.0,-6),(8,0.0,-6), + (-9,0.0,-3),(9,0.0,-3)] + + for pos in ice_locations: + scale = [3.0,1.025,3.0] + ba.newnode('region',attrs={'position': pos,'scale': scale,'type': 'box','materials': (self._collide_with_player, shared.footing_material, self.ice_material)}) + ba.newnode('locator',attrs={'shape':'box','position':pos, + 'color':(0,1,1),'opacity':1, 'drawShadow':False,'draw_beauty':True,'additive':False,'size':scale}) + + gnode = ba.getactivity().globalsnode + gnode.tint = (1.1, 1.05, 1.17) + gnode.happy_thoughts_mode = False + gnode.ambient_color = (1.2, 1.17, 1.1) + gnode.vignette_outer = (0.9, 0.9, 0.96) + gnode.vignette_inner = (0.95, 0.95, 0.93) + FadeEfect(gnode.tint) + Credits() + +#Map by Zaker DC [Inspiration from a map of Sebastian] +#Map 3# +class factory_defs: + boxes = {} + points = {} + boxes['area_of_interest_bounds'] = (0, 4, 0) + (0, 0, 0) + (50, 10, 20) + boxes['edge_box'] = (0, 4, 0) + (0.0, 0.0, 0.0) + (40, 2, 10) + boxes['map_bounds'] = (0, 4, 0) + (0, 0, 0) + (28, 10, 28) + + points['ffa_spawn1'] = (-8,3.5,0) + points['ffa_spawn2'] = (8,3.5,0) + points['ffa_spawn3'] = (3.4,3.75,3) + points['ffa_Spawn4'] = (-3.4,-0.75,-3) + + points['spawn1'] = (-8,3.5,0) + (1.0,0.1,1.0) + points['spawn2'] = (8,3.5,0) + (1.0,0.1,1.0) + + points['flag1'] = (-9.5,3.5,0) + (2.0,0.1,2.0) + points['flag2'] = (9.5,3.5,0) + (2.0,0.1,2.0) + points['flag_default'] = (0,3.7,0) + + points['powerup_spawn1'] = (4.8,3.65,3) + points['powerup_spawn2'] = (-4.8,3.65,-3) + points['powerup_spawn3'] = (-4.2,3.7,1.4) + points['powerup_spawn4'] = (4.1,3.7,-1.4) + +class FactoryMap(ba.Map): + """Grambledorf former experiment room""" + + defs = factory_defs + name = 'Powerups Factory' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee','king_of_the_hill','keep_away','team_flag'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'zigZagLevelColor' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('landMine'), + 'tex': ba.gettexture('landMine'), + 'bgtex': ba.gettexture('bg'), + 'bgmodel': ba.getmodel('thePadBG'), + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + + self._collide_with_player=ba.Material() + self._collide_with_player.add_actions(conditions=('we_are_older_than', 1), actions=(('modify_part_collision', 'collide', True))) + self.dont_collide=ba.Material() + self.dont_collide.add_actions(conditions=('they_are_different_node_than_us', ),actions=(('modify_part_collision', 'collide', False))) + + self._map_model = ba.getmodel('image1x1') + self._map_model2 = ba.getmodel('tnt') + self._map_tex1 = ba.gettexture('powerupImpactBombs') + self._map_tex2 = ba.gettexture('reflectionChar_-y') + self._map_tex3 = ba.gettexture('flagPoleColor') + + + self.background = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color': (1.3, 1.3, 1.3), + 'color_texture': self.preloaddata['bgtex'] + }) + + posXD = [(1.5,2.3,0),(-1.5,2.3,0),(0,2.3,0), (4.0,2.3,1.5),(4.0,2.3,-1.5),(4.0,2.3,0), (4.8,2.3,3),(3.4,2.3,3),(1.9,2.3,3), (-4.0,2.3,1.5),(-4.0,2.3,-1.5),(-4.0,2.3,0), (-4.8,2.3,-3),(-3.4,2.3,-3),(-1.9,2.3,-3) + ] + for m_pos in posXD: + self.mv_center = ba.newnode('prop', + attrs={'body': 'puck', + 'position': m_pos, + 'model': self._map_model2, + 'model_scale': 2.2, + 'body_scale': 0.1, + 'shadow_size': 0.0, + 'gravity_scale':0.0, + 'color_texture': self._map_tex1, + 'reflection': 'soft', + 'reflection_scale': [0.5], + 'is_area_of_interest': True, + 'materials': [self.dont_collide]}) + + posXD = [(-3.4,0.75,-3),(-3.4,-0.75,-3) + ] + for m_pos in posXD: + self.mc_center = ba.newnode('region',attrs={ 'position': m_pos, + 'scale': (1.5,1.5,1.5), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + + for m_pos in [(0,2.3,0)]: + self.mc_center = ba.newnode('region',attrs={'position': m_pos, + 'scale': (4.5,1.5,1.5), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + + for m_pos in [(4.0,2.3,0),(-4.0,2.3,0)]: + self.mc_center = ba.newnode('region',attrs={'position': m_pos, + 'scale': (1.5,1.5,4.5), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + + for m_pos in [(3.4,2.3,3),(-3.4,2.3,-3)]: + self.mc_center = ba.newnode('region',attrs={'position': m_pos, + 'scale': (4.5,1.5,1.5), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + + # Cajas Grandes Normales + for m_pos in [(8.7,1.72,0),(6.10,1.72,0),(-8.7,1.72,0),(-6.10,1.72,0)]: + self.mv_d2 = ba.newnode('prop', + attrs={'body': 'puck', 'position': m_pos, + 'model': self._map_model2, + 'model_scale': 3.8, + 'color_texture': self._map_tex1, + 'reflection_scale': [1.0], 'body_scale': 0.1, 'shadow_size': 0.0, 'gravity_scale':0.0, 'reflection': 'soft', 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + + for m_pos in [(7.45,1.72,0),(-7.45,1.72,0)]: + self.mc_d2 = ba.newnode('region',attrs={'position': m_pos, + 'scale': (5.25,2.7,2.7), 'type': 'box', 'materials': (self._collide_with_player, shared.footing_material)}) + + #Superficie + pos = [(-1.5,3.075,0),(0,3.075,0),(1.5,3.075,0), (4.0,3.075,1.5),(4.0,3.075,-1.5),(4.0,3.075,0), (-4.0,3.075,-1.5),(-4.0,3.075,1.5),(-4.0,3.075,0), (-4.8,3.075,-3),(-3.4,3.075,-3),(-1.9,3.075,-3), (4.8,3.075,3),(3.4,3.075,3),(1.9,3.075,3)] + for m_pos in pos: + self.mv_centera = ba.newnode('prop', + attrs={'body': 'puck', 'position': m_pos, + 'model': self._map_model, + 'color_texture': self._map_tex3, + 'model_scale': 1.5, 'body_scale': 0.1, 'shadow_size': 0.0, 'gravity_scale':0.0, 'reflection': 'soft', 'reflection_scale': [0.5],'is_area_of_interest': True, + 'materials': [self.dont_collide]}) + + gnode = ba.getactivity().globalsnode + gnode.tint = (1.1, 1.1, 1.17) + gnode.ambient_color = (1.2, 1.17, 1.1) + gnode.vignette_outer = (0.8, 0.7, 0.96) + gnode.vignette_inner = (0.95, 0.95, 0.93) + FadeEfect(gnode.tint) + Credits() + + +# Map by SEBASTIAN2059 +# Map 4# +class platforms_defs: + boxes = {} + points = {} + boxes['area_of_interest_bounds'] = (0, 4, 0) + (0, 0, 0) + (50, 10, 20) + boxes['edge_box'] = (0, 4, 0) + (0.0, 0.0, 0.0) + (40, 2, 10) + boxes['map_bounds'] = (0, 4, 0) + (0, 0, 0) + (28, 10, 28) + points['ffa_spawn1'] = (-10,3.5,0) + (2.0,0.1,2.0) + points['ffa_spawn2'] = (10,3.5,0) + (2.0,0.1,2.0) + points['ffa_spawn3'] = (0,3.5,1) + points['ffa_Spawn4'] = (0,3.5,-1) + points['spawn1'] = (-10,3.5,0) + (2.0,0.1,2.0) + points['spawn2'] = (10,3.5,0) + (2.0,0.1,2.0) + points['flag1'] = (-12,3.5,0) + (2.0,0.1,2.0) + points['flag2'] = (12,3.5,0) + (2.0,0.1,2.0) + points['flag_default'] = (0,3.5,0) + points['powerup_spawn1'] = (-11.8,4,-1.8) + points['powerup_spawn2'] = (-8.2,4,1.8) + points['powerup_spawn3'] = (8.2,4,-1.8) + points['powerup_spawn4'] = (11.8,4,1.8) + +class PlatformsMap(ba.Map): + """Plataforms!""" + defs = platforms_defs + name = 'Platforms' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee','king_of_the_hill','keep_away','team_flag'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'bridgitLevelColor' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'bgtex': ba.gettexture('bg'), + 'bgmodel': ba.getmodel('thePadBG'), + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + + self._collide_with_player=ba.Material() + self._collide_with_player.add_actions(conditions=('we_are_older_than', 1), actions=(('modify_part_collision', 'collide', True))) + self.dont_collide=ba.Material() + self.dont_collide.add_actions(conditions=('they_are_different_node_than_us', ),actions=(('modify_part_collision', 'collide', False))) + + self._map_model = ba.getmodel('image1x1') + self._map_tex = ba.gettexture('powerupIceBombs') + self._map_tex1 = ba.gettexture('powerupPunch') + self._map_tex2 = ba.gettexture('powerupImpactBombs') + + self.background = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + + for m_pos in [(-10,2.5,0),(10,2.5,0)]: + self.e_cnnt = ba.newnode('math', owner=self.node, attrs={'input1': (0, 0.5, 0), 'operation': 'add'}) + self.mc = ba.newnode('region',attrs={'position': m_pos,'scale': (5.0,1,5.0),'type': 'box','materials': (self._collide_with_player, shared.footing_material)}) + self.mv = ba.newnode('prop', owner=self.mc, + attrs={'body': 'puck','position': m_pos, 'model': self._map_model, 'model_scale': 5.0, 'body_scale': 0.1, 'shadow_size': 0.0, 'gravity_scale':0.0, 'color_texture': self._map_tex, 'reflection': 'soft', 'reflection_scale': [1.0], 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + self.mc.connectattr('position', self.e_cnnt, 'input2') + self.e_cnnt.connectattr('output', self.mv, 'position') + + for m_pos in [(0,2.5,1.35),(0,2.5,-1.35)]: + self.c_cnnt = ba.newnode('math', owner=self.node, attrs={'input1': (0, 0.5, 0), 'operation': 'add'}) + self.mc_center = ba.newnode('region',attrs={'position': m_pos,'scale': (2.7,1,2.7),'type': 'box','materials': (self._collide_with_player, shared.footing_material)}) + self.mv_center = ba.newnode('prop', owner=self.mc, + attrs={'body': 'puck','position': m_pos, 'model': self._map_model, 'model_scale': 2.7, 'body_scale': 0.1, 'shadow_size': 0.0, 'gravity_scale':0.0, 'color_texture': self._map_tex, 'reflection': 'soft', 'reflection_scale': [1.0], 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + self.mc_center.connectattr('position', self.c_cnnt, 'input2') + self.c_cnnt.connectattr('output', self.mv_center, 'position') + + for m_pos in [(1.1,3.01,0),(-1.1,3.01,0)]: + self.dec = ba.newnode('prop', owner=self.mc, + attrs={'body': 'puck','position': m_pos, 'model': self._map_model, 'model_scale': 0.5, 'body_scale': 0.1, 'shadow_size': 0.0, 'gravity_scale':0.0, 'color_texture': self._map_tex2, 'reflection': 'soft', 'reflection_scale': [1.0], 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + + pos = [(-5.9,2.5,1),(-3,2.5,-1),(3,2.5,1),(5.9,2.5,-1)] + for m_pos in pos: + self.m_cnnt = ba.newnode('math', owner=self.node, attrs={'input1': (0, 0.5, 0), 'operation': 'add'}) + self.mc_a = ba.newnode('region',attrs={'position': m_pos,'scale': (2.5,1,2.5),'type': 'box','materials': (self._collide_with_player, shared.footing_material)}) + self.mv_a = ba.newnode('prop', owner=self.mc, + attrs={'body': 'puck','position': m_pos, 'model': self._map_model, 'model_scale': 2.5, 'body_scale': 0.1, 'shadow_size': 0.0, 'gravity_scale':0.0, 'color_texture': self._map_tex1, 'reflection': 'soft', 'reflection_scale': [1.0], 'is_area_of_interest': True, 'materials': [self.dont_collide]}) + self.mc_a.connectattr('position', self.m_cnnt, 'input2') + self.m_cnnt.connectattr('output', self.mv_a, 'position') + if m_pos[2] == -1: + ba.animate_array(self.mc_a,'position',3,{0:m_pos,2:(m_pos[0],m_pos[1],m_pos[2]+2),3:(m_pos[0],m_pos[1],m_pos[2]+2),5:m_pos,6:m_pos},loop=True) + else: + ba.animate_array(self.mc_a,'position',3,{0:m_pos,2:(m_pos[0],m_pos[1],m_pos[2]-2),3:(m_pos[0],m_pos[1],m_pos[2]-2),5:m_pos,6:m_pos},loop=True) + + + gnode = ba.getactivity().globalsnode + gnode.tint = (1.1, 1.05, 1.17) + gnode.ambient_color = (1.2, 1.17, 1.1) + gnode.vignette_outer = (0.9, 0.9, 0.96) + gnode.vignette_inner = (0.95, 0.95, 0.93) + FadeEfect(gnode.tint) + Credits() + +#Map By Zacker +#Map 5# +class darkzone_defs: + boxes = {} + points = {} + boxes['area_of_interest_bounds'] = (0, 6, 0) + (0, 5, 0) + (17, 9, 5520) + boxes['map_bounds'] = (0, 0, 0) + (0, 0, 0) + (20.0, 23, 7.25) + points['flag_default'] = (0,5.1,0) + points['flag1'] = (-6.5,5.79,0.4) + points['spawn1'] = (-4.4,5,0) + points['flag2'] = (6.5,5.79,0.4) + points['spawn2'] = (4.4,5,0) + points['ffa_spawn1'] = (3,5.2,0) + points['ffa_spawn2'] = (-3,5.2,0) + points['ffa_spawn3'] = (4,5.2,0) + points['ffa_spawn4'] = (-4,5.2,0) + points['powerup_spawn1'] = (-5.5,7,0) + points['powerup_spawn2'] = (5.5,7,0) + +class DarkZone(ba.Map): + """Unknown city""" + defs = darkzone_defs + name = 'The Limbo' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee','king_of_the_hill','keep_away','team_flag'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'shrapnel1Color' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'bottom_model': ba.getmodel('rampageLevelBottom'), + 'tex': ba.gettexture('rampageLevelColor'), + 'bgmodel1': ba.getmodel('rampageBG'), + 'bgtex1': ba.gettexture('rampageBGColor'), + 'bgtex': ba.gettexture('shrapnel1Color'), + 'bgmodel': ba.getmodel('thePadBG'), + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + + self._collide_with_player=ba.Material() + self._collide_with_player.add_actions(conditions=('we_are_older_than', 1), actions=(('modify_part_collision', 'collide', True))) + self.dont_collide=ba.Material() + self.dont_collide.add_actions(conditions=('they_are_different_node_than_us', ),actions=(('modify_part_collision', 'collide', False))) + + self._map_model1 = ba.getmodel('image1x1') + self._map_model2 = ba.getmodel('tnt') + self._map_tex1 = ba.gettexture('black') + self._map_tex2 = ba.gettexture('reflectionChar_-y') + self._map_tex3 = ba.gettexture('bg') + self._map_tex4 = ba.gettexture('circleOutlineNoAlpha') + + self.background = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + + self.bg2 = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bgmodel1'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex1'] + }) + + self.zone = ba.newnode('locator', + attrs={'shape':'box', + 'position':(0,5,0), + 'color':(1,1,1), + 'opacity':1,'draw_beauty':True,'additive':False,'size':[15.5,0.05,5.3]}) + ba.animate_array(self.zone, 'color', 3,{0:(0,0,0), 1.5:(0,0,0), 2.00:(0,0,0), 2.05:(1,1,1), 2.1:(0,0,0), 2.15:(1,1,1), 2.2:(0,0,0), + 2.25:(1,1,1), 2.3:(0,0,0), 2.35:(1,1,1), 2.4:(0,0,0), 2.45:(0.7,0.7,0.7), 4.5: (0.7,0.7,0.7), 5: (0,0,0), 5.5: (1,1,1), 7: (0.4,0.4,0.4)},True) + + self.zone = ba.newnode('locator', + attrs={'shape':'box', + 'position':(0,3,0), + 'color':(1,1,1), + 'opacity':1,'draw_beauty':True,'additive':False,'size':[15.5,0.05,5.3]}) + ba.animate_array(self.zone, 'color', 3,{0:(0,0,0), 1.5:(0,0,0), 2.00:(0,0,0), 2.05:(1,1,1), 2.1:(0,0,0), 2.15:(1,1,1), 2.2:(0,0,0), + 2.25:(1,1,1), 2.3:(0,0,0), 2.35:(1,1,1), 2.4:(0,0,0), 2.45:(0.7,0.7,0.7), 4.5: (0.7,0.7,0.7), 5: (0,0,0), 5.5: (1,1,1), 7: (0.4,0.4,0.4)},True) + + self.zone = ba.newnode('locator', + attrs={'shape':'box', + 'position':(0,1,0), + 'color':(1,1,1), + 'opacity':1,'draw_beauty':True,'additive':False,'size':[15.5,0.05,5.3]}) + ba.animate_array(self.zone, 'color', 3,{0:(0,0,0), 1.5:(0,0,0), 2.00:(0,0,0), 2.05:(1,1,1), 2.1:(0,0,0), 2.15:(1,1,1), 2.2:(0,0,0), + 2.25:(1,1,1), 2.3:(0,0,0), 2.35:(1,1,1), 2.4:(0,0,0), 2.45:(0.7,0.7,0.7), 4.5: (0.7,0.7,0.7), 5: (0,0,0), 5.5: (1,1,1), 7: (0.4,0.4,0.4)},True) + + for m_pos1 in [(-5,3,0),(0,3,0),(5,3,0)]: + self.mv_center = ba.newnode('prop', + attrs={'body': 'puck', + 'position': m_pos1, + 'model': self._map_model2, + 'model_scale': 7.23, + 'body_scale': 0.1, + 'shadow_size': 0.0, + 'gravity_scale':0.0, + 'color_texture': self._map_tex3, + 'reflection': 'soft', + 'reflection_scale': [0.37], + 'is_area_of_interest': True, + 'materials': [self.dont_collide]}) + + for m_pos1 in [(0,3,0)]: + self.mc_center = ba.newnode('region',attrs={ + 'position': m_pos1, + 'scale': (15,5,5), + 'type': 'box', + 'materials': (self._collide_with_player, shared.footing_material)}) + + + for m_pos1 in [(-5,5.4,0),(0,5.4,0),(5,5.4,0)]: + self.mv_center = ba.newnode('prop', + attrs={'body': 'puck', + 'position': m_pos1, + 'model': self._map_model1, + 'model_scale': 4.00, + 'body_scale': 0.1, + 'shadow_size': 0.0, + 'gravity_scale':0.0, + 'color_texture': self._map_tex4, + 'reflection': 'soft', + 'reflection_scale': [0.0], + 'is_area_of_interest': True, + 'materials': [self.dont_collide]}) + + gnode = ba.getactivity().globalsnode + gnode.tint = (1.2,1.2,1.2) + gnode.ambient_color = (1.15,1.25,1.6) + gnode.vignette_outer = (0.5,-0.25,0.5) + gnode.vignette_inner = (0.93,0.93,0.95) + FadeEfect(gnode.tint) + Credits() + +#List Maps +zk2059 = [FactoryMap,PlatformsMap,DarkZone, #v2 + NeoZone,CMap #v1 + ] + +def register_maps(): + for new_map in zk2059: + _map.register_map(new_map) + + +register_maps() + \ No newline at end of file diff --git a/dist/ba_root/mods/maps/baMaps_v1.py b/dist/ba_root/mods/maps/baMaps_v1.py new file mode 100644 index 0000000..e973adb --- /dev/null +++ b/dist/ba_root/mods/maps/baMaps_v1.py @@ -0,0 +1,334 @@ +# ba_meta require api 6 + +# Plugin SEBASTIAN2059 - Zacker Tz +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# - Maps: - +# - Neo Zone - by Zacker Tz || Zacker#5505 - +# - Big H map - by SEBASTIAN2059 || SEBASTIAN2059#5751 - +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from bastd.maps import * +import ba +import _ba +import base64 +from ba import _map +import random + +if TYPE_CHECKING: + from typing import Any, List, Dict + +#Map by Zacker Tz +class neo_defs(): + boxes = {} + points = {} + boxes['area_of_interest_bounds'] = (0, 4, 0) + (0, 0, 0) + (50, 10, 20) + boxes['edge_box'] = (0, 4, 0) + (0.0, 0.0, 0.0) + (40, 2, 10) + boxes['map_bounds'] = (0, 4, 0) + (0, 0, 0) + (28, 10, 28) + points['ffa_spawn1'] = (-10,3.17,0) + (1.0,0.1,1.0) + points['ffa_spawn2'] = (10,3.17,0) + (1.0,0.1,1.0) + points['ffa_spawn3'] = (-5.25,3.17,-1.75) + (0.5,0.1,0.5) + points['ffa_Spawn4'] = (5.25,3.17,-1.75) + (0.5,0.1,0.5) + points['spawn1'] = (-11,3.17,0) + (1.0,0.1,1.0) + points['spawn2'] = (11,3.17,0) + (1.0,0.1,1.0) + points['flag1'] = (-12.0,3.3,0) + (2.0,0.1,2.0) + points['flag2'] = (12.0,3.3,0) + (2.0,0.1,2.0) + points['flag_default'] = (0,3.3,1.75) + points['powerup_spawn1'] = (-11,4.0,-1.75) + points['powerup_spawn2'] = (-11,4.0,1.75) + points['powerup_spawn3'] = (-1.75,4.0,0) + points['powerup_spawn4'] = (1.75,4.0,0.0) + points['powerup_spawn5'] = (11,4.0,-1.75) + points['powerup_spawn6'] = (11,4.0,1.75) + + +class NeoZone(ba.Map): + """Agent john's former workplace""" + + defs = neo_defs() + name = 'Neo Zone' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee','king_of_the_hill','keep_away','team_flag'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'rgbStripes' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('landMine'), + 'tex': ba.gettexture('landMine'), + 'bgtex': ba.gettexture('black'), + 'bgmodel': ba.getmodel('thePadBG'), + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + + self._collide_with_player=ba.Material() + self._collide_with_player.add_actions(conditions=('we_are_older_than', 1), actions=(('modify_part_collision', 'collide', True))) + self.dont_collide=ba.Material() + self.dont_collide.add_actions(conditions=('they_are_different_node_than_us', ),actions=(('modify_part_collision', 'collide', False))) + + self._map_model = ba.getmodel('image1x1') + self._map_model2 = ba.getmodel('tnt') + self._map_tex = ba.gettexture('powerupIceBombs') + self._map_tex1 = ba.gettexture('ouyaUButton') + + self.background = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + + locations = [(7.0,0.0,0),(5.25,0.0,0),(5.25,0.0,-1.75), + (3.5,0.0,-1.75),(1.75,0.0,-1.75),(1.75,0.0,0), + (1.75,0.0,1.75), + (0,0.0,1.75), + (-7.0,0.0,0),(-5.25,0.0,0),(-5.25,3.17,-1.75), + (-3.5,0.0,-1.75),(-1.75,0.0,-1.75),(-1.75,0.0,0), + (-1.75,0.0,1.75)] + num = 0 + + for pos in locations: + color = (0,1,0) if num in [0,1,5,8,9,13] else (0,0,1) if num in [6,7,14] else (1,0,0) if num in [2,3,4,10,11,12] else (1,1,1) + self.decor = ba.newnode('prop', + attrs={'body': 'puck', + 'position': (pos[0],3.17,pos[2]), + 'model': self._map_model, + 'model_scale': 1.7, + 'body_scale': 0.1, + 'shadow_size': 0.0, + 'gravity_scale':0.0, + 'color_texture': self._map_tex1, + 'reflection': 'soft', + 'reflection_scale': [0.5], + 'is_area_of_interest': True, + 'materials': [self.dont_collide]}) + self.region = ba.newnode('region',attrs={ + 'position': (pos[0],2.3,pos[2]), + 'scale': (1.9,1.9,1.9), + 'type': 'box', + 'materials': (self._collide_with_player, shared.footing_material)}) + self.zone = ba.newnode('locator', + attrs={'shape':'box', + 'position':(pos[0],2.3,pos[2]), + 'color':color, + 'opacity':1,'draw_beauty':True,'additive':False,'size':[1.75,1.75,1.75]}) + num += 1 + + #Sides + side_locations = [(-10.5,2.3,0),(10.5,2.3,0)] + for pos in side_locations: + self.big_region = ba.newnode('region',attrs={ + 'position': pos, + 'scale': (5.7,1.9,5.7), + 'type': 'box', + 'materials': (self._collide_with_player, shared.footing_material)}) + self.big_zone = ba.newnode('locator', + attrs={'shape':'box', + 'position':pos, + 'color':(0,1,1.5), + 'opacity':1,'draw_beauty':True,'additive':False,'size':[5.25,1.75,5.25]}) + + exec(base64.b64decode("dCA9IGJhLm5ld25vZGUoJ3RleHQnLAogICAgICAgICAgICAgICBhdHRycz17ICd0ZXh0JzoiTWFwYXMgcG9yOiBTRUJBU1RJQU4yMDU5IHkgWmFja2VyIERDIiwgCiAgICAgICAgJ3NjYWxlJzowLjYsCiAgICAgICAgJ3Bvc2l0aW9uJzooMCwwKSwgCiAgICAgICAgJ29wYWNpdHknOiAwLjQsCiAgICAgICAgJ3NoYWRvdyc6MC41LAogICAgICAgICdmbGF0bmVzcyc6MS4yLAogICAgICAgICdjb2xvcic6KDEsIDEsIDEpLAogICAgICAgICdoX2FsaWduJzonY2VudGVyJywKICAgICAgICAndl9hdHRhY2gnOidib3R0b20nfSk=").decode('UTF-8')) #bubalu + + gnode = ba.getactivity().globalsnode + gnode.tint = (1.1, 1.05, 1.17) + gnode.happy_thoughts_mode = False + gnode.ambient_color = (1.2, 1.17, 1.1) + gnode.vignette_outer = (0.9, 0.9, 0.96) + gnode.vignette_inner = (0.95, 0.95, 0.93) + +#Map by Sebastian2059 +class c_defs(): + boxes = {} + points = {} + boxes['area_of_interest_bounds'] = (0, 4, 0) + (0, 0, 0) + (50, 10, 20) + boxes['edge_box'] = (0, 4, 0) + (0.0, 0.0, 0.0) + (40, 2, 10) + boxes['map_bounds'] = (0, 4, 0) + (0, 0, 0) + (28, 10, 28) + points['ffa_spawn1'] = (-9,0.5,-3) + (1.0,0.1,5.0) + points['ffa_spawn2'] = (9,0.5,-3) + (1.0,0.1,5.0) + points['ffa_spawn3'] = (-6,0.5,-6.0) + (2.0,0.1,1.0) + points['ffa_Spawn4'] = (6,0.5,0.0) + (2.0,0.1,1.0) + points['ffa_spawn5'] = (6,0.5,-6.0) + (2.0,0.1,1.0) + points['ffa_Spawn6'] = (-6,0.5,0.0) + (2.0,0.1,1.0) + points['spawn1'] = (-9,0.5,-3) + (1.0,0.1,1.0) + points['spawn2'] = (9,0.5,-3) + (1.0,0.1,1.0) + points['flag1'] = (-10.0,0.8,-3) + (2.0,0.1,2.0) + points['flag2'] = (10.0,0.8,-3) + (2.0,0.1,2.0) + points['flag_default'] = (0,0.8,-3.0) + points['powerup_spawn1'] = (-9,1.0,-8) + points['powerup_spawn2'] = (-9,1.0,3) + points['powerup_spawn3'] = (-1.5,1.0,-8.25) + points['powerup_spawn4'] = (1.5,1.0,-8.25) + points['powerup_spawn5'] = (-1.5,1.0,2.25) + points['powerup_spawn6'] = (1.5,1.0,2.25) + points['powerup_spawn7'] = (9,1.0,-8) + points['powerup_spawn8'] = (9,1.0,3) + + points['race_mine1'] = (-1.5, 0.7, -0.7) + points['race_mine2'] = (-1.5, 0.7, 0.7) + points['race_mine3'] = (-4.5, 0.7, 0.0) + points['race_mine4'] = (4.5, 0.7, 0.0) + points['race_mine5'] = (4.5, 0.7, -6.0) + points['race_mine6'] = (-4.5, 0.7, -6.0) + points['race_mine7'] = (0.0, 0.7, -6.0) + points['race_mine8'] = (-10.0, 0.7, -4.5) + points['race_mine9'] = (10.0, 0.7, -4.5) + points['race_mine10'] = (10.0, 0.7, -1.5) + points['race_mine11'] = (-10.0, 0.7, -1.5) + + points['race_point1'] = (0.0, 0.5, 0.0) + (0.3, 2.0, 1.5) + points['race_point2'] = (3.5, 0.5, 0.0) + (0.3, 2.0, 1.5) + points['race_point3'] = (7.0, 0.5, 0.0) + (0.3, 2.0, 1.5) + points['race_point4'] = (9.0, 0.5, -2.0) + (1.5, 2.0, 0.3) + points['race_point5'] = (9.0, 0.5, -4.0) + (1.5, 2.0, 0.3) + points['race_point6'] = (7.0, 0.5, -6.0) + (0.3, 2.0, 1.5) + points['race_point7'] = (3.5, 0.5, -6.0) + (0.3, 2.0, 1.5) + points['race_point8'] = (0.0, 0.5, -6.0) + (0.3, 2.0, 1.5) + points['race_point9'] = (-3.5, 0.5, -6.0) + (0.3, 2.0, 1.5) + points['race_point10'] = (-7.0, 0.5, -6.0) + (0.3, 2.0, 1.5) + points['race_point11'] = (-9.0, 0.5, -2.0) + (1.5, 2.0, 0.3) + points['race_point12'] = (-9.0, 0.5, -4.0) + (1.5, 2.0, 0.3) + points['race_point13'] = (-7.0, 0.5, 0.0) + (0.3, 2.0, 1.5) + points['race_point14'] = (-3.5, 0.5, 0.0) + (0.3, 2.0, 1.5) + +class CMap(ba.Map): + """Jack Morgan used to run here""" + + defs = c_defs() + name = 'Big H' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee','king_of_the_hill','keep_away','team_flag','race'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'bigG' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('landMine'), + 'tex': ba.gettexture('landMine'), + 'bgtex': ba.gettexture('black'), + 'bgmodel': ba.getmodel('thePadBG'), + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + + self._collide_with_player=ba.Material() + self._collide_with_player.add_actions(conditions=('we_are_older_than', 1), actions=(('modify_part_collision', 'collide', True))) + self.dont_collide=ba.Material() + self.dont_collide.add_actions(conditions=('they_are_different_node_than_us', ),actions=(('modify_part_collision', 'collide', False))) + self.ice_material = ba.Material() + self.ice_material.add_actions(actions=('modify_part_collision','friction',0.01)) + + self._map_model = ba.getmodel('image1x1') + self._map_model2 = ba.getmodel('tnt') + self._map_tex = ba.gettexture('powerupIceBombs') + self._map_tex1 = ba.gettexture('circleOutlineNoAlpha') + self._map_tex2 = ba.gettexture('black') + + self.background = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + + posS = [(0.0,0.05,0)] + for m_pos in posS: + self.mv_center = ba.newnode('prop', + attrs={'body': 'puck', + 'position': m_pos, + 'model': self._map_model, + 'model_scale': 35, + 'body_scale': 0.1, + 'shadow_size': 0.0, + 'gravity_scale':0.0, + 'color_texture': self._map_tex2, + 'reflection': 'soft', + 'reflection_scale': [0], + 'is_area_of_interest': True, + 'materials': [self.dont_collide]}) + try: + self._gamemode = ba.getactivity().name + except Exception: + print('error') + pass + locations = [(-9,0.0,-3.0),(9,0.0,-3.0),(0.0,0.0,0.0),(0.0,0.0,-6.0),(0.0,0.0,-3.0)] + scales = [[3.0,1.0,14.0],[3.0,1.0,14.0],[15.0,1.0,3.0],[15.0,1.0,3.0],[3.0,1.0,3.0]] + index = 0 + for pos in locations: + # + scale = scales[index] + ba.newnode('region',attrs={'position': pos,'scale': scale,'type': 'box','materials': (self._collide_with_player, shared.footing_material)}) + ba.newnode('locator',attrs={'shape':'box','position':pos, + 'color':(1,1,1),'opacity':1, 'drawShadow':False,'draw_beauty':True,'additive':False,'size':scale}) + index += 1 + + pos = [-3.0,0.0,-8.25] + for p in range(10): + scale = [1.5,1.0,1.5] + ba.newnode('region',attrs={'position': pos,'scale': scale,'type': 'box','materials': (self._collide_with_player, shared.footing_material)}) + ba.newnode('locator',attrs={'shape':'box','position':pos, + 'color':(1,1,1),'opacity':1, 'drawShadow':False,'draw_beauty':True,'additive':False,'size':scale}) + pos[0] += 1.5 + if p == 4: + pos[0] = -3.0 + pos[2] = 2.25 + + if self._gamemode == 'Race': + ice_locations = [(-8,0.0,0),(8,0.0,0), + (-8,0.0,-6),(8,0.0,-6), + (-9,0.0,-3),(9,0.0,-3)] + + for pos in ice_locations: + scale = [3.0,1.025,3.0] + ba.newnode('region',attrs={'position': pos,'scale': scale,'type': 'box','materials': (self._collide_with_player, shared.footing_material, self.ice_material)}) + ba.newnode('locator',attrs={'shape':'box','position':pos, + 'color':(0,1,1),'opacity':1, 'drawShadow':False,'draw_beauty':True,'additive':False,'size':scale}) + + exec(base64.b64decode("dCA9IGJhLm5ld25vZGUoJ3RleHQnLAogICAgICAgICAgICAgICBhdHRycz17ICd0ZXh0JzoiTWFwYXMgcG9yOiBTRUJBU1RJQU4yMDU5IHkgWmFja2VyIERDIiwgCiAgICAgICAgJ3NjYWxlJzowLjYsCiAgICAgICAgJ3Bvc2l0aW9uJzooMCwwKSwgCiAgICAgICAgJ29wYWNpdHknOiAwLjQsCiAgICAgICAgJ3NoYWRvdyc6MC41LAogICAgICAgICdmbGF0bmVzcyc6MS4yLAogICAgICAgICdjb2xvcic6KDEsIDEsIDEpLAogICAgICAgICdoX2FsaWduJzonY2VudGVyJywKICAgICAgICAndl9hdHRhY2gnOidib3R0b20nfSk=").decode('UTF-8')) #saludo + + gnode = ba.getactivity().globalsnode + gnode.tint = (1.1, 1.05, 1.17) + gnode.happy_thoughts_mode = False + gnode.ambient_color = (1.2, 1.17, 1.1) + gnode.vignette_outer = (0.9, 0.9, 0.96) + gnode.vignette_inner = (0.95, 0.95, 0.93) + +#List Maps +zk2059 = [NeoZone,CMap] + +def register_maps(): + for new_map in zk2059: + _map.register_map(new_map) + +# ba_meta export plugin +class Zk2059(ba.Plugin): + def __init__(self): + if _ba.env().get("build_number", 0) >= 20258: + register_maps() + else: + print("new_maps.py only runs with BombSquad versions higher than 1.5.29.") + \ No newline at end of file diff --git a/dist/ba_root/mods/maps/new_maps.py b/dist/ba_root/mods/maps/new_maps.py new file mode 100644 index 0000000..683c6b1 --- /dev/null +++ b/dist/ba_root/mods/maps/new_maps.py @@ -0,0 +1,411 @@ +## DECODED FOR MODIFICATIONS OF MAP POINTS ## +## DON'T SHARE ## + + +# ba_meta require api 6 + +#Mod by Froshlee14 +#Updated by SEBASTIAN2059 +from __future__ import annotations + +from typing import TYPE_CHECKING + + +import ba +import _ba +import random +from bastd.gameutils import SharedObjects +from bastd.actor.powerupbox import PowerupBox, PowerupBoxFactory + +if TYPE_CHECKING: + from typing import Any, List, Dict + + +class mega_mine_defs: + boxes = {} + points = {} + boxes['area_of_interest_bounds'] = (0, 1, 0) + (0, 0, 0) + (0, 0, 0) + boxes['map_bounds'] = (0, 0, 0) + (0, 0, 0) + (20, 20, 20) + points['ffa_spawn1'] = (3,2,-2) + points['ffa_spawn2'] = (-3,2,-2) + points['ffa_spawn3'] = (3,2,2) + points['ffa_Spawn4'] = (-3,2,2) + points['powerup_spawn1'] = (-2.8,3,0) + points['powerup_spawn2'] = (2.8,3,0) + points['powerup_spawn3'] = (0,3,-2.8) + points['powerup_spawn4'] = (0,3,2.8) + + +class MegaMine(ba.Map): + """A giant mine!""" + + defs = mega_mine_defs + + name = 'Mega Mine' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'landMine' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('landMine'), + 'tex': ba.gettexture('landMine'), + 'bgtex': ba.gettexture('menuBG'), + 'bgmodel': ba.getmodel('thePadBG'), + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = ba.newnode('prop', + delegate=self, + attrs={'position':(0,1,0), + 'velocity':(0,0,0), + 'model':self.preloaddata['model'], + 'size':[25,13,4], + 'model_scale':14.6, + 'body_scale':14.3, + 'density':999999999999999999999, + 'damping':999999999999999999999, + 'gravity_scale':0, + 'body':'landMine', + 'reflection':'powerup', + 'reflection_scale':[1.0], + 'color_texture':self.preloaddata['tex'], + 'materials':[shared.footing_material]}) + + + self.background = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + + gnode = ba.getactivity().globalsnode + gnode.tint = (1.2, 1.17, 1.1) + gnode.ambient_color = (1.2, 1.17, 1.1) + gnode.vignette_outer = (0.6, 0.6, 0.64) + gnode.vignette_inner = (0.95, 0.95, 0.93) + + +class powerups_defs: + boxes = {} + points = {} + boxes['area_of_interest_bounds'] = (0, -2, -2) + (0, 0, 0) + (0, 15, 0) + boxes['map_bounds'] = (0, 1, 0) + (0, -3, 0) + (50, 30, 50) + points['ffa_spawn1'] = (3,2,-1.5) + points['ffa_spawn2'] = (-3,2,-1.5) + points['ffa_spawn3'] = (3,2,1.5) + points['ffa_spawn4'] = (-3,2,1.5) + points['powerup_spawn1'] = (-2,3,0) + points['powerup_spawn2'] = (2,3,0) + + +class PowerupMap(ba.Map): + """A Powerups!""" + + defs = powerups_defs + + name = 'Powerups' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'powerupShield' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'bgtex': ba.gettexture('menuBG'), + 'bgmodel': ba.getmodel('thePadBG'), + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + Box(position=(-3,-2,0),texture='powerupShield') + Box(position=(3,-2,0),texture='powerupSpeed') + Box(position=(-3,-7,0),texture='powerupHealth') + Box(position=(3,-7,0),texture='powerupCurse') + + self._new_region_material = ba.Material() + self._new_region_material.add_actions( + conditions=('they_have_material', shared.object_material), + actions=(('modify_part_collision', 'collide', + True), ('modify_part_collision', 'physical', False))) + + self._region = ba.newnode('region', + attrs={ + 'position': (2,-10.5,0), + 'scale': (18, 0, 12), + 'type': 'box', + 'materials': [self._new_region_material, shared.death_material] + }) + # a = ba.newnode('locator',attrs={'shape':'box','position':(0,-10.5,0), + # 'color':(1,1,1),'opacity':1, 'drawShadow':False,'draw_beauty':True,'additive':False,'size':[18,0,12]}) + + self._call_powerups = ba.timer(0.1,ba.Call(self.spawn_powerup),repeat=True) + + self.background = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + + gnode = ba.getactivity().globalsnode + gnode.tint = (1.0,1.0,1.0) + gnode.ambient_color = (1.1,1.1,1.0) + gnode.vignette_outer = (0.7,0.65,0.75) + gnode.vignette_inner = (0.95,0.95,0.93) + + def spawn_powerup(self): + pos = (-15+random.random()*30,-13,-15+random.random()*30) + if not random.random() > 0.9997: + p = PowerupBox(position=pos,poweruptype=PowerupBoxFactory().get_random_powerup_type(),expire=False).autoretain() + p.node.gravity_scale = -0.1 + + +class Box(ba.Actor): + def __init__(self,position=(0,0,0),texture=None): + ba.Actor.__init__(self) + shared = SharedObjects.get() + self.node = ba.newnode('prop', + delegate=self, + attrs={'position':position, + 'velocity':(0,0,0), + 'model':ba.getmodel('powerup'), + 'model_scale':8.6, + 'body_scale':7 , + 'density':999999999999999999999, + 'damping':999999999999999999999, + 'gravity_scale':0, + 'body':'crate', + 'reflection':'powerup', + 'reflection_scale':[0.3], + 'color_texture':ba.gettexture(texture), + 'materials':[shared.footing_material]}) + + + +class darkness_defs: + boxes = {} + points = {} + boxes['area_of_interest_bounds'] = (0, 1, 0) + (0, 0, 0) + (17, 0, 0) + boxes['map_bounds'] = (0, 0, 0) + (0, 0, 0) + (10.5, 20, 10.5) + points['flag_default'] = (0,1,0) + points['flag1'] = (-4.7,1,0) + points['spawn1'] = (-4.7,1,0) + points['flag2'] = (4.7,1,0) + points['spawn2'] = (4.7,1,0) + points['ffa_spawn1'] = (0,1,3) + points['ffa_spawn2'] = (0,1,-3) + points['ffa_spawn3'] = (3,1,0) + points['ffa_spawn4'] = (-3,1,0) + points['powerup_spawn1'] = (-3.5,2,-3.5) + points['powerup_spawn2'] = (-3.4,2,3.5) + points['powerup_spawn3'] = (3.5,2,-3.5) + points['powerup_spawn4'] = (3.5,2,3.5) + +class Dark(ba.Map): + defs = darkness_defs + name = 'Dark world' + + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee','king_of_the_hill','keep_away','team_flag'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'bg' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('footballStadium'), + 'collide_model': ba.getcollidemodel('footballStadiumCollide'), + 'tex': ba.gettexture('bg'), + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = ba.newnode('terrain', delegate=self, + attrs={ + 'model':self.preloaddata['model'], + 'collide_model':self.preloaddata['collide_model'], + 'color_texture':self.preloaddata['tex'], + 'materials':[shared.footing_material]}) + + self.zone = ba.newnode('locator', + attrs={'shape':'circleOutline','position':(0,0,0), + 'color':(1,1,1),'opacity':1,'draw_beauty':True,'additive':False,'size':[11]}) + + gnode = ba.getactivity().globalsnode + gnode.tint = (0.8,0.8,1) + gnode.ambient_color = (1.15,1.25,1.6) + gnode.vignette_outer = (0.66,0.67,0.73) + gnode.vignette_inner = (0.93,0.93,0.95) + + + +class SuperTntMap(ba.Map): + """A giant mine!""" + + defs = powerups_defs + + name = 'Super TNT' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'tnt' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('powerupSimple'), + 'tex': ba.gettexture('tnt'), + 'bgtex': ba.gettexture('menuBG'), + 'bgmodel': ba.getmodel('thePadBG'), + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = ba.newnode('prop', + delegate=self, + attrs={'position':(0,-3.2,0), + 'velocity':(0,0,0), + 'model':self.preloaddata['model'], + 'model_scale':18, + 'body_scale':15, + 'density':999999999999999999999, + 'damping':999999999999999999999, + 'gravity_scale':0, + 'body':'crate', + 'color_texture':self.preloaddata['tex'], + 'materials':[shared.footing_material]}) + + + self.background = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'background': True, + 'color_texture': self.preloaddata['bgtex'] + }) + + gnode = ba.getactivity().globalsnode + gnode.tint = (1.0, 1.0, 1.0) + gnode.ambient_color = (1.1, 1.1, 1.0) + gnode.vignette_outer = (0.7, 0.65, 0.75) + gnode.vignette_inner = (0.95, 0.95, 0.93) + + + +class GreenScreenMap(ba.Map): + """A giant mine!""" + + from bastd.mapdata import doom_shroom as defs + + name = 'Green Screen' + + @classmethod + def get_play_types(cls) -> List[str]: + """Return valid play types for this map.""" + return ['melee','keep_away','team_flag'] + + @classmethod + def get_preview_texture_name(cls) -> str: + return 'eggTex2' + + @classmethod + def on_preload(cls) -> Any: + data: Dict[str, Any] = { + 'model': ba.getmodel('doomShroomLevel'), + 'collide_model': ba.getcollidemodel('doomShroomLevelCollide'), + 'tex': ba.gettexture('white'), + 'bgtex': ba.gettexture('doomShroomBGColor'), + 'bgmodel': ba.getmodel('doomShroomBG'), + 'stem_model': ba.getmodel('doomShroomStem'), + 'collide_bg': ba.getcollidemodel('doomShroomStemCollide'), + } + return data + + def __init__(self) -> None: + super().__init__() + shared = SharedObjects.get() + self.node = ba.newnode( + 'terrain', + delegate=self, + attrs={ + 'collide_model': self.preloaddata['collide_model'], + 'model': self.preloaddata['model'], + 'color':(0,1,0), + 'color_texture': self.preloaddata['tex'], + 'materials': [shared.footing_material] + }) + self.background = ba.newnode( + 'terrain', + attrs={ + 'model': self.preloaddata['bgmodel'], + 'lighting': False, + 'color':(0,1,0), + 'background': True, + 'color_texture': self.preloaddata['tex'] + }) + self.stem = ba.newnode('terrain', + attrs={ + 'model': self.preloaddata['stem_model'], + 'lighting': False, + 'color':(0,1,0), + 'color_texture': self.preloaddata['tex'] + }) + self.bg_collide = ba.newnode( + 'terrain', + attrs={ + 'collide_model': self.preloaddata['collide_bg'], + 'materials': [shared.footing_material, shared.death_material] + }) + + gnode = ba.getactivity().globalsnode + gnode.tint = (0.82, 1.10, 1.15) + gnode.ambient_color = (0.9, 1.3, 1.1) + gnode.shadow_ortho = False + gnode.vignette_outer = (0.76, 0.76, 0.76) + gnode.vignette_inner = (0.95, 0.95, 0.99) + +##### List containing the maps to be registered ##### +MAPS = [MegaMine,PowerupMap,Dark,SuperTntMap,GreenScreenMap] + +def register_maps(): + for new_map in MAPS: + ba._map.register_map(new_map) + + +register_maps() \ No newline at end of file diff --git a/dist/ba_root/mods/playersData/pdata.py b/dist/ba_root/mods/playersData/pdata.py index ad1cd4b..a146183 100644 --- a/dist/ba_root/mods/playersData/pdata.py +++ b/dist/ba_root/mods/playersData/pdata.py @@ -12,7 +12,9 @@ import _thread from serverData import serverdata from tools.file_handle import OpenJson -import _ba # pylint: disable=import-error +# pylint: disable=import-error +import _ba +import ba.internal import json if TYPE_CHECKING: @@ -131,7 +133,7 @@ def add_profile( serverdata.clients[account_id]["rejoincount"] = 1 serverdata.clients[account_id]["lastJoin"] = time.time() cid = 113 - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros['account_id'] == account_id: cid = ros['client_id'] serverdata.clients[account_id]["lastIP"] = _ba.get_client_ip(cid) diff --git a/dist/ba_root/mods/playersData/profiles.json b/dist/ba_root/mods/playersData/profiles.json index cddd30e..2f5b7df 100644 --- a/dist/ba_root/mods/playersData/profiles.json +++ b/dist/ba_root/mods/playersData/profiles.json @@ -68,12 +68,12 @@ "lastSpam": 1655407521.4853249, "totaltimeplayer": 0, "warnCount": 0, - "lastWarned": 1655407521.4853408, + "lastWarned": 1658055515.14302, "verified": true, - "rejoincount": 2, - "lastJoin": 1655407536.1110733, - "lastIP": "axj~}j~~n`ai", - "deviceUUID": "\u0003\u0005^\u0005VFYYULL\u0007Z\u0005L@QU\u0004L\u0015QZ\u0002L\u0016XX" + "rejoincount": 1, + "lastJoin": 1658055515.143022, + "lastIP": "axj~|h~|j~~", + "deviceUUID": "c49fafb7d66d14198924c1b9dcc59e23fb838042" }, "pb-IF43U2cIVw==": { "display_string": "\ue063Smoothy", diff --git a/dist/ba_root/mods/playersData/profiles.json.backup b/dist/ba_root/mods/playersData/profiles.json.backup index cddd30e..2f5b7df 100644 --- a/dist/ba_root/mods/playersData/profiles.json.backup +++ b/dist/ba_root/mods/playersData/profiles.json.backup @@ -68,12 +68,12 @@ "lastSpam": 1655407521.4853249, "totaltimeplayer": 0, "warnCount": 0, - "lastWarned": 1655407521.4853408, + "lastWarned": 1658055515.14302, "verified": true, - "rejoincount": 2, - "lastJoin": 1655407536.1110733, - "lastIP": "axj~}j~~n`ai", - "deviceUUID": "\u0003\u0005^\u0005VFYYULL\u0007Z\u0005L@QU\u0004L\u0015QZ\u0002L\u0016XX" + "rejoincount": 1, + "lastJoin": 1658055515.143022, + "lastIP": "axj~|h~|j~~", + "deviceUUID": "c49fafb7d66d14198924c1b9dcc59e23fb838042" }, "pb-IF43U2cIVw==": { "display_string": "\ue063Smoothy", diff --git a/dist/ba_root/mods/playersData/roles.json b/dist/ba_root/mods/playersData/roles.json index 9055d7c..c566e59 100644 --- a/dist/ba_root/mods/playersData/roles.json +++ b/dist/ba_root/mods/playersData/roles.json @@ -71,8 +71,7 @@ "commands": [], "ids": [ "pb-IF4VAk4a", - "pb-IF4eUxk5KA==", - "pb-IF43U2cIVw==" + "pb-IF4RU2ECAg==" ] }, "bypass-warn": { diff --git a/dist/ba_root/mods/playersData/roles.json.backup b/dist/ba_root/mods/playersData/roles.json.backup index 9055d7c..c566e59 100644 --- a/dist/ba_root/mods/playersData/roles.json.backup +++ b/dist/ba_root/mods/playersData/roles.json.backup @@ -71,8 +71,7 @@ "commands": [], "ids": [ "pb-IF4VAk4a", - "pb-IF4eUxk5KA==", - "pb-IF43U2cIVw==" + "pb-IF4RU2ECAg==" ] }, "bypass-warn": { diff --git a/dist/ba_root/mods/plugins/CharacterChooser.py b/dist/ba_root/mods/plugins/CharacterChooser.py index aa6d8ab..0546ad0 100644 --- a/dist/ba_root/mods/plugins/CharacterChooser.py +++ b/dist/ba_root/mods/plugins/CharacterChooser.py @@ -35,6 +35,7 @@ from __future__ import annotations from typing import TYPE_CHECKING import ba,_ba +import ba.internal from bastd.actor.playerspaz import PlayerSpaz @@ -163,7 +164,7 @@ def _set_ready(self, ready: bool) -> None: # Give their input-device UI ownership too # (prevent someone else from snatching it in crowded games) - _ba.set_ui_input_device(self._sessionplayer.inputdevice) + ba.internal.set_ui_input_device(self._sessionplayer.inputdevice) return if ready==False: diff --git a/dist/ba_root/mods/plugins/bcs_plugin.py b/dist/ba_root/mods/plugins/bcs_plugin.py index 493978f..cc5b240 100644 --- a/dist/ba_root/mods/plugins/bcs_plugin.py +++ b/dist/ba_root/mods/plugins/bcs_plugin.py @@ -5,6 +5,7 @@ from typing import Optional, Any, Dict, List, Type, Sequence from ba._gameactivity import GameActivity import ba,_ba +import ba.internal import json import os import _thread @@ -53,7 +54,7 @@ class BsDataThread(object): nextMap='' currentMap='' global stats - for i in _ba.get_game_roster(): + for i in ba.internal.get_game_roster(): try: @@ -61,9 +62,9 @@ class BsDataThread(object): except: liveplayers[i['account_id']]={'name':"",'clientid':i['client_id'],'device_id':i['display_string']} try: - nextMap=_ba.get_foreground_host_session().get_next_game_description().evaluate() + nextMap=ba.internal.get_foreground_host_session().get_next_game_description().evaluate() - current_game_spec=_ba.get_foreground_host_session()._current_game_spec + current_game_spec=ba.internal.get_foreground_host_session()._current_game_spec gametype: Type[GameActivity] =current_game_spec['resolved_type'] currentMap=gametype.get_settings_display_string(current_game_spec).evaluate() @@ -74,7 +75,7 @@ class BsDataThread(object): system={'cpu':"null",'ram':'null'} stats['system']=system stats['roster']=liveplayers - stats['chats']=_ba.get_chat_messages() + stats['chats']=ba.internal.get_chat_messages() stats['playlist']=minigame stats['teamInfo']=self.getTeamInfo() @@ -83,7 +84,7 @@ class BsDataThread(object): def getTeamInfo(self): data={} - session=_ba.get_foreground_host_session() + session=ba.internal.get_foreground_host_session() data['sessionType']=type(session).__name__ teams=session.sessionteams for team in teams: @@ -94,7 +95,7 @@ class BsDataThread(object): } for player in team.players: teamplayer={'name':player.getname(), - 'device_id':player.inputdevice.get_account_name(True), + 'device_id':player.inputdevice.get_v1_account_name(True), 'inGame':player.in_game, 'character':player.character, 'account_id':player.get_v1_account_id() diff --git a/dist/ba_root/mods/plugins/colorfulmaps.py b/dist/ba_root/mods/plugins/colorfulmaps2.py similarity index 95% rename from dist/ba_root/mods/plugins/colorfulmaps.py rename to dist/ba_root/mods/plugins/colorfulmaps2.py index 98847f8..ccd292e 100644 --- a/dist/ba_root/mods/plugins/colorfulmaps.py +++ b/dist/ba_root/mods/plugins/colorfulmaps2.py @@ -2,12 +2,9 @@ # Just edit Config before starting server # by: Lirik # Further edited/Fixed by:Freak -# ba_meta require api 7 - import ba import random from random import choice - CONFIGS = { "Radius": 2.0, "Blinking": False, @@ -121,7 +118,5 @@ def Map___init__(func): return wrapper -# ba_meta export plugin -class MapColor(ba.Plugin): - def on_app_running(self): - ba.Map.__init__ = Map___init__(ba.Map.__init__) + +ba.Map.__init__ = Map___init__(ba.Map.__init__) diff --git a/dist/ba_root/mods/plugins/elPatronPowerups.py b/dist/ba_root/mods/plugins/elPatronPowerups.py index 24d6801..c3e5245 100644 --- a/dist/ba_root/mods/plugins/elPatronPowerups.py +++ b/dist/ba_root/mods/plugins/elPatronPowerups.py @@ -1,10 +1,2352 @@ # ba_meta require api 7 from __future__ import annotations from typing import TYPE_CHECKING -import base64 _sp_ = ('\n') -exec(base64.b64decode("CmltcG9ydCBiYSxfYmEscmFuZG9tLHRpbWUsZGF0ZXRpbWUsd2Vha3JlZixqc29uLG9zCmZyb20gYmFzdGQudWkucHJvZmlsZSBpbXBvcnQgYnJvd3Nlcgpmcm9tIGJhc3RkLmFjdG9yIGltcG9ydCBib21iCmZyb20gYmFzdGQuYWN0b3IgaW1wb3J0IHBvd2VydXBib3ggIGFzIHB1cGJveApmcm9tIGJhc3RkLmFjdG9yLnNwYXpib3QgaW1wb3J0IFNwYXpCb3QKZnJvbSBiYXN0ZC5hY3Rvci5ib21iIGltcG9ydCAoQm9tYixCbGFzdCkKZnJvbSBiYXN0ZC51aS5wb3B1cCBpbXBvcnQgKFBvcHVwV2luZG93LFBvcHVwTWVudVdpbmRvdyxQb3B1cE1lbnUpCmZyb20gYmFzdGQuYWN0b3Iuc3BheiBpbXBvcnQgKFNwYXosU3BhekZhY3RvcnksUGlja3VwTWVzc2FnZSwgUHVuY2hIaXRNZXNzYWdlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBDdXJzZUV4cGxvZGVNZXNzYWdlLCBCb21iRGllZE1lc3NhZ2UpCmZyb20gYmFzdGQubWFpbm1lbnUgaW1wb3J0IChNYWluTWVudUFjdGl2aXR5LE1haW5NZW51U2Vzc2lvbikKZnJvbSBiYXN0ZC5nYW1ldXRpbHMgaW1wb3J0IFNoYXJlZE9iamVjdHMKZnJvbSBiYXN0ZC5hY3Rvci5wb3dlcnVwYm94IGltcG9ydCBQb3dlcnVwQm94RmFjdG9yeQpmcm9tIGJhc3RkLmFjdG9yLnBvcHVwdGV4dCBpbXBvcnQgUG9wdXBUZXh0CmZyb20gYmFzdGQudWkuY29uZmlybSBpbXBvcnQgQ29uZmlybVdpbmRvdwpmcm9tIGJhc3RkLmFjdG9yLnNwYXogaW1wb3J0ICoKCmlmIFRZUEVfQ0hFQ0tJTkc6CiAgICBwYXNzCgojID09PSBNb2QgbWFkZSBieSBAUGF0cm9uX01vZHogPT09CgpkZWYgZ2V0bGFuZ3VhZ2UodGV4dCwgc3Viczogc3RyID0gTm9uZSwgYWxtYWNlbjogbGlzdCA9IFtdKToKICAgIGlmIGFsbWFjZW4gPT0gW106IGFsbWFjZW4gPSBsaXN0KHJhbmdlKDEwMDApKQogICAgbGFuZyA9IF9iYS5hcHAubGFuZy5sYW5ndWFnZQogICAgdHJhbnNsYXRlID0geyJSZXNldCI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiUmVpbmljaWFyIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJSZXNldCIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiUmVpbmljaWFyIn0sCiAgICAgICAgICAgICAgICAgIk5vdGhpbmciOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIlNpbiBwb3RlbmNpYWRvcmVzIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJObyBwb3dlcnVwcyIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiU2VtIHBvd2VydXBzIn0sCiAgICAgICAgICAgICAgICAgIkFjdGlvbiAxIjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6ICJQb3RlbmNpYWRvcmVzIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJQb3dlcnVwcyIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiUG93ZXJ1cHMifSwKICAgICAgICAgICAgICAgICAiQWN0aW9uIDIiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIkNvbmZpZ3VyYWNpw4PCs24iLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIlNldHRpbmdzIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJEZWZpbmnDg8Knw4PCtWVzIn0sCiAgICAgICAgICAgICAgICAgIkFjdGlvbiAzIjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6ICJFeHRyYXMiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIkV4dHJhcyIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiRXh0cmFzIn0sCiAgICAgICAgICAgICAgICAgIkFjdGlvbiA0IjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6ICJUaWVuZGEiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIlN0b3JlIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJMb2phIn0sCiAgICAgICAgICAgICAgICAgIkFjdGlvbiA1IjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6ICJDYW5qZWFyIGPDg8KzZGlnbyIsCiAgICAgICAgICAgICAgICAgICAgICAgIkVuZ2xpc2giOiAiRW50ZXIgQ29kZSIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiQ8ODwrNkaWdvIHByb21vY2lvbmFsIn0sCiAgICAgICAgICAgICAgICAgIkN1c3RvbSI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJDdXN0b21pemUiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIkN1c3RvbWl6YXIifSwKICAgICAgICAgICAgICAgICAiSW1wYWlybWVudCBCb21icyI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiQm9tYmFzIG1lbm9zY2FibyIsCiAgICAgICAgICAgICAgICAgICAgICAgIkVuZ2xpc2giOiAiSHlwZXJhY3RpdmUgYm9tYnMiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIkJvbWJhcyBoaXBlcmF0aXZhcyJ9LAogICAgICAgICAgICAgICAgICJTcGVlZCI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiVmVsb2NpZGFkIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJTcGVlZCIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiVmVsb2NpZGFkZSJ9LAogICAgICAgICAgICAgICAgICJGaXJlIEJvbWJzIjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6ICJCb21iYXMgZGUgZnVlZ28iLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIkZpcmUgQm9tYnMiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIkJvbWJhcyBkZSBmb2dvIn0sCiAgICAgICAgICAgICAgICAgIkljZSBNYW4iOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIkhvbWJyZSBkZSBoaWVsbyIsCiAgICAgICAgICAgICAgICAgICAgICAgIkVuZ2xpc2giOiAiSWNlIG1hbiIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiSG9tZW0gZGUgZ2VsbyJ9LAogICAgICAgICAgICAgICAgICJGbHkgQm9tYnMiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIkJvbWJhcyBleHBhbnNpdmFzIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJFeHBhbnNpdmUgYm9tYnMiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIkJvbWJhcyBleHBhbnNpdmFzIn0sCiAgICAgICAgICAgICAgICAgIkdvb2RieWUiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIsOCwqFIYXN0YSBsdWVnbyEiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIkdvb2RieWUhIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJBZGV1cyEifSwKICAgICAgICAgICAgICAgICAiSGVhbGluZyBEYW1hZ2UiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIkF1dG8tY3VyYWNpw4PCs24iLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIkhlYWxpbmcgRGFtYWdlIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJBdXRvLWN1cmEifSwKICAgICAgICAgICAgICAgICAiVGFuayBTaGllbGQiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIlPDg8K6cGVyIGJsaW5kYWplIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJSZWluZm9yY2VkIHNoaWVsZCIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiRXNjdWRvIHJlZm9yw4PCp2FkbyJ9LAogICAgICAgICAgICAgICAgICJUYW5rIFNoaWVsZCBQVEciOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIlBvcmNlbnRhamUgZGUgZGlzbWludWNpw4PCs24iLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIlBlcmNlbnRhZ2UgZGVjcmVhc2VkIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJQZXJjZW50dWFsIHJlZHV6aWRvIn0sCiAgICAgICAgICAgICAgICAgIkhlYWxpbmcgRGFtYWdlIFBURyI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiUG9yY2VudGFqZSBkZSByZWN1cGVyYWNpw4PCs24gZGUgc2FsdWQiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIlBlcmNlbnRhZ2Ugb2YgaGVhbHRoIHJlY292ZXJlZCIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiUG9yY2VudGFnZW0gZGUgcmVjdXBlcmHDg8Knw4PCo28gZGUgc2HDg8K6ZGUifSwKICAgICAgICAgICAgICAgICAiU1k6IEJBTEwiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIkVzZmVyYSIsCiAgICAgICAgICAgICAgICAgICAgICAgIkVuZ2xpc2giOiAiU3BoZXJlIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJFc2ZlcmEifSwKICAgICAgICAgICAgICAgICAiU1k6IEltcGFjdCI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiRXNwZWNpYWwiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIlNwZWNpYWwiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIkVzcGVjaWFsIn0sCiAgICAgICAgICAgICAgICAgIlNZOiBFZ2ciOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIkh1ZXZpdG8iLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIkVnZyBzaGFwZSIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiT3ZvIn0sCiAgICAgICAgICAgICAgICAgIlBvd2VydXAgU2NhbGUiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIlRhbWHDg8KxbyBkZWwgcG90ZW5jaWFkb3IiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIlBvd2VydXBzIHNpemUiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIlRhbWFuaG8gZGUgcG93ZXJ1cHMifSwKICAgICAgICAgICAgICAgICAiUG93ZXJ1cCBXaXRoIFNoaWVsZCI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiUG90ZW5jaWFkb3JlcyBjb24gZXNjdWRvIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJQb3dlcnVwcyB3aXRoIHNoaWVsZCIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiUG93ZXJ1cHMgY29tIGVzY3VkbyJ9LAogICAgICAgICAgICAgICAgICJQb3dlcnVwIFRpbWUiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIk1vc3RyYXIgVGVtcG9yaXphZG9yIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJTaG93IGVuZCB0aW1lIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJNb3N0cmFyIGNyb27Dg8K0bWV0cm8ifSwKICAgICAgICAgICAgICAgICAiUG93ZXJ1cCBTdHlsZSI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiRm9ybWEgZGUgbG9zIHBvdGVuY2lhZG9yZXMiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIlNoYXBlIG9mIHBvd2VydXAiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIkZvcm1hIGRlIHBvd2VydXAifSwKICAgICAgICAgICAgICAgICAiUG93ZXJ1cCBOYW1lIjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6ICJNb3N0cmFyIG5vbWJyZSBlbiBsb3MgcG90ZW5jaWFkb3JlcyIsCiAgICAgICAgICAgICAgICAgICAgICAgIkVuZ2xpc2giOiAiU2hvdyBuYW1lIG9uIHBvd2VydXBzIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJNb3N0cmFyIG5vbWUgZW0gcG93ZXJ1cHMifSwKICAgICAgICAgICAgICAgICAiUGVyY2VudGFnZSI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiUHJvYmFiaWxpZGFkIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJTaG93IHBlcmNlbnRhZ2UiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIk1vc3RyYXIgcG9yY2VudGFnZW0ifSwKICAgICAgICAgICAgICAgICAiT25seSBJdGVtcyI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiU8ODwrNsbyBBY2Nlc29yaW9zIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJPbmx5IHV0ZW5zaWxzIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJBcGVuYXMgdXRlbnNpbGlvcyJ9LAogICAgICAgICAgICAgICAgICJOZXciOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIk51ZXZvIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJOZXciLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIk5vdm8ifSwKICAgICAgICAgICAgICAgICAiT25seSBCb21icyI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiU8ODwrNsbyBCb21iYXMiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIk9ubHkgYm9tYnMiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIkFwZW5hcyBib21iYXMifSwKICAgICAgICAgICAgICAgICAiQ29pbnMgMCI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiTW9uZWRhcyBJbnN1ZmljaWVudGVzIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJJbnN1ZmZpY2llbnQgY29pbnMiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIk1vZWRhcyBpbnN1ZmljaWVudGVzIn0sCiAgICAgICAgICAgICAgICAgIlB1cmNoYXNlIjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6ICJDb21wcmEgcmVhbGl6YWRhIGNvcnJlY3RhbWVudGUiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIlN1Y2Nlc3NmdWwgcHVyY2hhc2UiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIkNvbXByYSBCZW0gU3VjZWRpZGEifSwKICAgICAgICAgICAgICAgICAiRG91YmxlIFByb2R1Y3QiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIllhIGhhcyBjb21wcmFkbyBlc3RlIGFydMODwq1jdWxvIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJZb3UndmUgYWxyZWFkeSBib3VnaHQgdGhpcyIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiVm9jZSBqYSBjb21wcm91IGlzdG8ifSwKICAgICAgICAgICAgICAgICAiQm91Z2h0IjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6ICJDb21wcmFkbyIsCiAgICAgICAgICAgICAgICAgICAgICAgIkVuZ2xpc2giOiAiQm91Z2h0IiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJDb21wcm91In0sCiAgICAgICAgICAgICAgICAgIkNvbmZpcm0gUHVyY2hhc2UiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogZidUaWVuZXMge3N1YnN9IG1vbmVkYXMuIHtfc3BffSDDgsK/RGVzZWFzIGNvbXByYXIgZXN0bz8nLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogZidZb3UgaGF2ZSB7c3Vic30gY29pbnMuIHtfc3BffSBEbyB5b3Ugd2FudCB0byBidXkgdGhpcz8nLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogZidWb2PDg8KqIHRlbSB7c3Vic30gbW9lZGFzLiB7X3NwX30gRGVzZWphIGNvbXByYXIgaXN0bz8nfSwKICAgICAgICAgICAgICAgICAiRmlyZUJvbWJzIFN0b3JlIjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6ICdCb21iYXMgZGUgZnVlZ28nLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogJ0ZpcmUgYm9tYnMnLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogJ0JvbWJhcyBkZSBpbmPDg8KqbmRpbyd9LAogICAgICAgICAgICAgICAgICJUaW1lciBTdG9yZSI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAnVGVtcG9yaXphZG9yJywKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICdUaW1lcicsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAnVGltZXInfSwKICAgICAgICAgICAgICAgICAiUGVyY2VudGFnZXMgU3RvcmUiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogJ0V4dHJhcycsCiAgICAgICAgICAgICAgICAgICAgICAgIkVuZ2xpc2giOiAnRXh0cmFzJywKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICdFeHRyYXMnfSwKICAgICAgICAgICAgICAgICAiQmxvY2sgT3B0aW9uIFN0b3JlIjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6IGYiVXV1cHMuLntfc3BffUVzdGEgb3BjacODwrNuIGVzdMODwqEgYmxvcXVlYWRhLntfc3BffSBQYXJhIGFjY2VkZXIgYSBlbGxhIHB1ZWRlcyB7X3NwX30gY29tcHJhcmxhIGVuIGxhIHRpZW5kYS57X3NwX30gR3JhY2lhcy4uLiIsCiAgICAgICAgICAgICAgICAgICAgICAgIkVuZ2xpc2giOiBmIk9vb29wcy4uLntfc3BffVRoaXMgb3B0aW9uIGlzIGJsb2NrZWQuIHtfc3BffSBUbyBhY2Nlc3MgaXQgeW91IGNhbiBidXkge19zcF99IGl0IGluIHRoZSBzdG9yZS57X3NwX30gVGhhbmsgeW91Li4uIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6IGYiT29vcHMuLi57X3NwX31Fc3RhIG9ww4PCp8ODwqNvIGVzdMODwqEgYmxvcXVlYWRhLiB7X3NwX30gUGFyYSBhY2Vzc8ODwqEtbG8sIHZvY8ODwqogcG9kZSB7X3NwX30gY29tcHLDg8KhLWxvIG5hIGxvamEue19zcF99IE9icmlnYWRvLi4uIn0sCiAgICAgICAgICAgICAgICAgIlRydWUgQ29kZSI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiw4LCoUPDg8KzZGlnbyBjYW5qZWFkbyEiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIlN1Y2Nlc3NmdWwgY29kZSEiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIsOCwqFDw4PCs2RpZ28gdsODwqFsaWRvISJ9LAogICAgICAgICAgICAgICAgICJGYWxzZSBDb2RlIjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6ICJDw4PCs2RpZ28geWEgY2FuamVhZG8iLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIkV4cGlyZWQgY29kZSIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiQ8ODwrNkaWdvIGV4cGlyYWRvIn0sCiAgICAgICAgICAgICAgICAgIkludmFsaWQgQ29kZSI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiQ8ODwrNkaWdvIGludsODwqFsaWRvIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJJbnZhbGlkIGNvZGUiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIkPDg8KzZGlnbyBpbnbDg8KhbGlkbyJ9LAogICAgICAgICAgICAgICAgICJSZXdhcmQgQ29kZSI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiBmIsOCwqFGZWxpY2l0YWNpb25lcyEgw4LCoUdhbmFzdGUge3N1YnN9IG1vbmVkYXMhIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6IGYiQ29uZ3JhdHVsYXRpb25zISBZb3UndmUge3N1YnN9IGNvaW5zIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6IGYiUGFyYWLDg8KpbnMhIFZvY8ODwqogdGVtIHtzdWJzfSBtb2VkYXMifSwKICAgICAgICAgICAgICAgICAiQ3JlYXRvciI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiTW9kIGNyZWFkbyBwb3IgQFBhdHLDg8Kzbk1vZHoiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIk1vZCBjcmVhdGVkIGJ5IEBQYXRyw4PCs25Nb2R6IiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJNb2QgY3JlYWRvIGJ5IEBQYXRyw4PCs25Nb2R6In0sCiAgICAgICAgICAgICAgICAgIk1vZCBJbmZvIjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6IGYiVW4gbW9kIGdlbmlhbCBxdWUgdGUgcGVybWl0ZSBnZXN0aW9uYXIge19zcF99IGxvcyBwb3RlbmNpYWRvcmVzIGEgdHUgYW50b2pvLiB7X3NwX30gdGFtYmnDg8KpbiBpbmNsdXllIDggcG90ZW5jaWFkb3JlcyBleHRyYXtfc3BffSBkZWphbmRvIDE3IGVuIHRvdGFsLi4uIMOCwqFHdWF5ISIsCiAgICAgICAgICAgICAgICAgICAgICAgIkVuZ2xpc2giOiBmIkEgY29vbCBtb2QgdGhhdCBhbGxvd3MgeW91IHRvIG1hbmFnZSB7X3NwX30gcG93ZXJ1cHMgYXQgeW91ciB3aGltcy4ge19zcF99IGFsc28gaW5jbHVkZXMgOCBleHRyYSBwb3dlcnVwc3tfc3BffSBsZWF2aW5nIDE3IGluIHRvdGFsLi4uIFdvdyEiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogZiJVbSBtb2QgbGVnYWwgcXVlIHBlcm1pdGUgcXVlIHZvY8ODwqogZ2VyZW5jaWUgb3N7X3NwX30gcG93ZXJ1cHMgZGUgZGUgYWNvcmRvIGNvbSBzZXVzIGNhcHJpY2hvcy4ge19zcF99IHRhbWLDg8KpbSBpbmNsdWkgOCBwb3dlcnVwcyBleHRyYXMse19zcF99IGRlaXhhbmRvIDE3IG5vIHRvdGFsLi4uIFVhdSEifSwKICAgICAgICAgICAgICAgICAiQ29pbnMgTWVzc2FnZSI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiBmIlJlY29tcGVuc2E6IHtzdWJzfSBNb25lZGFzIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6IGYiUmV3YXJkOiB7c3Vic30gQ29pbnMiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogZiJSZWNvbXBlbnNhOiB7c3Vic30gTW9lZGFzIn0sCiAgICAgICAgICAgICAgICAgIkNvaW5zIExpbWl0IE1lc3NhZ2UiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogZiJHYW5hc3RlIHthbG1hY2VuWzBdfSBNb25lZGFzLntfc3BffSBQZXJvIGhhcyBzdXBlcmFkbyBlbCBsw4PCrW1pdGUgZGUge2FsbWFjZW5bMV19IiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6IGYiWW91IHdvbiB7YWxtYWNlblswXX0gQ29pbnMuIHtfc3BffSBCdXQgeW91IGhhdmUgZXhjZWVkZWQgdGhlIGxpbWl0IG9mIHthbG1hY2VuWzFdfSIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiBmIlZvY8ODwqogZ2FuaG91IHthbG1hY2VuWzBdfSBNb2VkYXMuIHtfc3BffSBNYXMgdm9jw4PCqiBleGNlZGV1IG8gbGltaXRlIGRlIHthbG1hY2VuWzFdfSJ9LAogICAgICAgICAgICAgICAgIH0KICAgIGxhbmd1YWdlcyA9IFsnU3BhbmlzaCcsJ1BvcnR1Z3Vlc2UnLCdFbmdsaXNoJ10KICAgIGlmIGxhbmcgbm90IGluIGxhbmd1YWdlczogbGFuZyA9ICdFbmdsaXNoJwoKICAgIGlmIHRleHQgbm90IGluIHRyYW5zbGF0ZToKICAgICAgICByZXR1cm4gdGV4dAogICAgCiAgICByZXR1cm4gdHJhbnNsYXRlW3RleHRdW2xhbmddCgppbXBvcnQgc2V0dGluZwpzZXR0aW5ncz1zZXR0aW5nLmdldF9zZXR0aW5nc19kYXRhKCkKCmRlZiBzZXR0aW5nc19kaXN0cmlidXRpb24oKToKICAgIHJldHVybiBzZXR0aW5nc1siZWxQYXRyb25Qb3dlcnVwcyJdWyJzZXR0aW5ncyJdCgoKCmFwZyA9IGJhLmFwcC5jb25maWcKCmFwZ1snUFBNIFNldHRpbmdzJ10gPSBzZXR0aW5nc19kaXN0cmlidXRpb24oKQoKCmNvbmZpZyA9IGFwZ1snUFBNIFNldHRpbmdzJ10KCmRlZiBkZWZhdWx0X3Bvd2VydXBzKCk6CiAgICByZXR1cm4gc2V0dGluZ3NbImVsUGF0cm9uUG93ZXJ1cHMiXVsiUXVhbnRpdHkiXQoKCmNvbmZpZ1snUG93ZXJ1cHMnXSA9IGRlZmF1bHRfcG93ZXJ1cHMoKQoKCnBvd2VydXBzID0gY29uZmlnWydQb3dlcnVwcyddCgojID09PSBFWFRSQVMgPT09CgpHTE9CQUwgPSB7IlRhYiI6ICdBY3Rpb24gMScsCiAgICAgICAgICAiQ2xzIFBvd2VydXAiOiAwLAogICAgICAgICAgIkNvaW5zIE1lc3NhZ2UiOiBbXX0KCiMgPT09IFNUT1JFID09PQpkZWYgcHJvbW9fY29kZXMoKToKICAgIHJldHVybiB7IkctQW01NGlnTzQyT3MiOiBbVHJ1ZSwxMTAwXSwKICAgICAgICAgICAgIlAtdFJvOG5NOGRaIjogW1RydWUsMjgwMF0sCiAgICAgICAgICAgICJZLXRVMkIzUyI6IFtUcnVlLDUwMF0sCiAgICAgICAgICAgICJCLTBtQjNSWVQyeiI6IFtUcnVlLDkxMF0sCiAgICAgICAgICAgICJCLUFzZDE0bU9OOUcwRCI6IFtUcnVlLDkxMF0sCiAgICAgICAgICAgICJELXJBY0swY0oyMyI6IFtUcnVlLDkxMF0sCiAgICAgICAgICAgICJFLWEyN1pPNmYzWSI6IFtUcnVlLDYwMF0sCiAgICAgICAgICAgICJFLUFtNTRpZ080Mk9zIjogW1RydWUsNjAwXSwKICAgICAgICAgICAgIkUtTTR1TjNLMzRYQiI6IFtUcnVlLDg0MF0sCiAgICAgICAgICAgICJQTS03MzFDbGNBRiI6IFtUcnVlLDUwMDAwXX0KICAgICAgICAgICAgCmRlZiBzdG9yZV9pdGVtcygpOgogICAgcmV0dXJuIHsiQnV5IEZpcmVib21icyI6IEZhbHNlLAogICAgICAgICAgICAiQnV5IE9wdGlvbiI6IEZhbHNlLAogICAgICAgICAgICAiQnV5IFBlcmNlbnRhZ2UiOiBGYWxzZX0KCmlmIGFwZy5nZXQoJ0JlYXIgQ29pbicpIGlzIE5vbmU6CiAgICBhcGdbJ0JlYXIgQ29pbiddID0gMAogICAgYXBnLmFwcGx5X2FuZF9jb21taXQoKQogICAgCmlmIGFwZy5nZXQoJ0JlYXIgQ29pbicpIGlzIG5vdCBOb25lOgogICAgaWYgYXBnWydCZWFyIENvaW4nXSA8PSAwOgogICAgICAgIGFwZ1snQmVhciBDb2luJ10gPSAwCiAgICBhcGdbJ0JlYXIgQ29pbiddID0gaW50KGFwZ1snQmVhciBDb2luJ10pCgppZiBhcGcuZ2V0KCdCZWFyIFN0b3JlJykgaXMgTm9uZToKICAgIGFwZ1snQmVhciBTdG9yZSddID0ge30KICAgIApmb3IgaSxqIGluIHN0b3JlX2l0ZW1zKCkuaXRlbXMoKToKICAgIHN0b3JlICA9IGFwZ1snQmVhciBTdG9yZSddCiAgICBpZiBpIG5vdCBpbiBzdG9yZToKICAgICAgICBpZiBzdG9yZS5nZXQoaSkgaXMgTm9uZToKICAgICAgICAgICAgc3RvcmVbaV0gPSBqCiAgICBhcGcuYXBwbHlfYW5kX2NvbW1pdCgpCgpTVE9SRSA9IGFwZ1snQmVhciBTdG9yZSddCgppZiBTVE9SRS5nZXQoJ1Byb21vIENvZGUnKSBpcyBOb25lOgogICAgU1RPUkVbJ1Byb21vIENvZGUnXSA9IHByb21vX2NvZGVzKCkKCmZvciBpLHggaW4gcHJvbW9fY29kZXMoKS5pdGVtcygpOgogICAgcG1jb2RlID0gU1RPUkVbJ1Byb21vIENvZGUnXQogICAgaWYgaSBub3QgaW4gcG1jb2RlOgogICAgICAgIGlmIHBtY29kZS5nZXQoaSkgaXMgTm9uZToKICAgICAgICAgICAgcG1jb2RlW2ldID0geAoKYXBnLmFwcGx5X2FuZF9jb21taXQoKQoKY2xhc3MgQmVhclN0b3JlOgogICAgZGVmIF9faW5pdF9fKHNlbGYsCiAgICAgICAgICAgICAgICAgcHJpY2U6IGludCA9IDEwMDAsCiAgICAgICAgICAgICAgICAgdmFsdWU6IHN0ciA9ICcnLAogICAgICAgICAgICAgICAgIGNhbGxiYWNrOiBjYWxsID0gTm9uZSk6CiAgICAgICAgICAgICAgICAgICAgIAogICAgICAgIHNlbGYucHJpY2UgPSBwcmljZQogICAgICAgIHNlbGYudmFsdWUgPSB2YWx1ZQogICAgICAgIHNlbGYuc3RvcmUgPSBTVE9SRVt2YWx1ZV0KICAgICAgICBzZWxmLmNvaW5zID0gYXBnWydCZWFyIENvaW4nXQogICAgICAgIHNlbGYuY2FsbGJhY2sgPSBjYWxsYmFjawogICAgICAgICAgICAgICAgIAogICAgZGVmIGJ1eShzZWxmKToKICAgICAgICBpZiBub3Qgc2VsZi5zdG9yZToKICAgICAgICAgICAgaWYgc2VsZi5jb2lucyA+PSAoc2VsZi5wcmljZSk6CiAgICAgICAgICAgICAgICBkZWYgY29uZmlybSgpOgogICAgICAgICAgICAgICAgICAgIFNUT1JFW3NlbGYudmFsdWVdID0gVHJ1ZQogICAgICAgICAgICAgICAgICAgIGFwZ1snQmVhciBDb2luJ10gLT0gaW50KHNlbGYucHJpY2UpCiAgICAgICAgICAgICAgICAgICAgYmEuc2NyZWVubWVzc2FnZShnZXRsYW5ndWFnZSgnUHVyY2hhc2UnKSwoMCwxLDApKQogICAgICAgICAgICAgICAgICAgIGJhLnBsYXlzb3VuZChiYS5nZXRzb3VuZCgnY2FzaFJlZ2lzdGVyJykpCiAgICAgICAgICAgICAgICAgICAgYXBnLmFwcGx5X2FuZF9jb21taXQoKQogICAgICAgICAgICAgICAgICAgIHNlbGYuY2FsbGJhY2soKQogICAgICAgICAgICAgICAgQ29uZmlybVdpbmRvdyhnZXRsYW5ndWFnZSgnQ29uZmlybSBQdXJjaGFzZScsc3Vicz1zZWxmLmNvaW5zKSwKICAgICAgICAgICAgICAgICAgICAgIHdpZHRoPTQwMCwgaGVpZ2h0PTEyMCwgYWN0aW9uPWNvbmZpcm0sIG9rX3RleHQ9YmEuTHN0cihyZXNvdXJjZT0nb2tUZXh0JykpCiAgICAgICAgICAgIGVsc2U6CiAgICAgICAgICAgICAgICBiYS5zY3JlZW5tZXNzYWdlKGdldGxhbmd1YWdlKCdDb2lucyAwJyksKDEsMCwwKSkKICAgICAgICAgICAgICAgIGJhLnBsYXlzb3VuZChiYS5nZXRzb3VuZCgnZXJyb3InKSkKICAgICAgICBlbHNlOgogICAgICAgICAgICBiYS5zY3JlZW5tZXNzYWdlKGdldGxhbmd1YWdlKCdEb3VibGUgUHJvZHVjdCcpLCgxLDAsMCkpCiAgICAgICAgICAgIGJhLnBsYXlzb3VuZChiYS5nZXRzb3VuZCgnZXJyb3InKSkKCiAgICBkZWYgX19kZWxfXyhzZWxmKToKICAgICAgICBhcGdbJ0JlYXIgQ29pbiddID0gaW50KGFwZ1snQmVhciBDb2luJ10pCiAgICAgICAgYXBnLmFwcGx5X2FuZF9jb21taXQoKQoKY2xhc3MgUHJvbW9Db2RlOgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGNvZGU6IHN0ciA9ICcnKToKICAgICAgICBzZWxmLmNvZGUgPSBjb2RlCiAgICAgICAgc2VsZi5jb2Rlc19zdG9yZSA9IFNUT1JFWydQcm9tbyBDb2RlJ10KICAgICAgICBpZiBzZWxmLmNvZGUgaW4gc2VsZi5jb2Rlc19zdG9yZToKICAgICAgICAgICAgc2VsZi5jb2RlX3R5cGUgPSBTVE9SRVsnUHJvbW8gQ29kZSddW2NvZGVdCiAgICAgICAgICAgIHNlbGYucHJvbW9fY29kZV9leHBpcmUgPSBzZWxmLmNvZGVfdHlwZVswXQogICAgICAgICAgICBzZWxmLnByb21vX2NvZGVfYW1vdW50ID0gc2VsZi5jb2RlX3R5cGVbMV0KCiAgICBkZWYgX19kZWxfXyhzZWxmKToKICAgICAgICBhcGdbJ0JlYXIgQ29pbiddID0gaW50KGFwZ1snQmVhciBDb2luJ10pCiAgICAgICAgYXBnLmFwcGx5X2FuZF9jb21taXQoKQoKICAgIGRlZiBjb2RlX2NvbmZpcm1hdGlvbihzZWxmKToKICAgICAgICBpZiBzZWxmLmNvZGUgIT0gIiI6CiAgICAgICAgICAgIGJhLnNjcmVlbm1lc3NhZ2UoCiAgICAgICAgICAgICAgICBiYS5Mc3RyKHJlc291cmNlPSdzdWJtaXR0aW5nUHJvbW9Db2RlVGV4dCcpLCgwLDEsMCkpCiAgICAgICAgICAgIGJhLnRpbWVyKDIsYmEuQ2FsbChzZWxmLnZhbGlkYXRlX2NvZGUpKQoKICAgIGRlZiB2YWxpZGF0ZV9jb2RlKHNlbGYpOgogICAgICAgIGlmIHNlbGYuY29kZSBpbiBzZWxmLmNvZGVzX3N0b3JlOgogICAgICAgICAgICBpZiBzZWxmLnByb21vX2NvZGVfZXhwaXJlOgogICAgICAgICAgICAgICAgYmEudGltZXIoMS41LGJhLkNhbGwoc2VsZi5zdWNjZXNzZnVsX2NvZGUpKQogICAgICAgICAgICAgICAgYmEuc2NyZWVubWVzc2FnZShnZXRsYW5ndWFnZSgnVHJ1ZSBDb2RlJyksKDAsMSwwKSkKICAgICAgICAgICAgICAgIGJhLnBsYXlzb3VuZChiYS5nZXRzb3VuZCgnY2hlZXInKSkKICAgICAgICAgICAgICAgIHNlbGYuY29kZV90eXBlWzBdID0gRmFsc2UKICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgIGJhLnNjcmVlbm1lc3NhZ2UoZ2V0bGFuZ3VhZ2UoJ0ZhbHNlIENvZGUnKSwoMSwwLDApKQogICAgICAgICAgICAgICAgYmEucGxheXNvdW5kKGJhLmdldHNvdW5kKCdlcnJvcicpKQogICAgICAgIGVsc2U6CiAgICAgICAgICAgIGJhLnNjcmVlbm1lc3NhZ2UoZ2V0bGFuZ3VhZ2UoJ0ludmFsaWQgQ29kZScpLCgxLDAsMCkpCiAgICAgICAgICAgIGJhLnBsYXlzb3VuZChiYS5nZXRzb3VuZCgnZXJyb3InKSkKCiAgICBkZWYgc3VjY2Vzc2Z1bF9jb2RlKHNlbGYpOgogICAgICAgIGFwZ1snQmVhciBDb2luJ10gKz0gc2VsZi5wcm9tb19jb2RlX2Ftb3VudAogICAgICAgIGJhLnNjcmVlbm1lc3NhZ2UoZ2V0bGFuZ3VhZ2UoJ1Jld2FyZCBDb2RlJywKICAgICAgICAgICAgc3Vicz1zZWxmLnByb21vX2NvZGVfYW1vdW50KSwoMCwxLDApKQogICAgICAgIGJhLnBsYXlzb3VuZChiYS5nZXRzb3VuZCgnY2FzaFJlZ2lzdGVyMicpKQoKTWFpbk1lbnVBY3Rpdml0eS5zdXBlcl90cmFuc2l0aW9uX2luID0gTWFpbk1lbnVBY3Rpdml0eS5vbl90cmFuc2l0aW9uX2luCmRlZiBuZXdfb25fdHJhbnNpdGlvbl9pbihzZWxmKToKICAgIHNlbGYuc3VwZXJfdHJhbnNpdGlvbl9pbigpCiAgICBsaW1pdCA9IDg0MDAKICAgIGJlYXJfY29pbiA9IGFwZ1snQmVhciBDb2luJ10KICAgIGNvaW5zX21lc3NhZ2UgPSBHTE9CQUxbJ0NvaW5zIE1lc3NhZ2UnXQogICAgdHJ5OgogICAgICAgIGlmIG5vdCAoU1RPUkVbJ0J1eSBGaXJlYm9tYnMnXSBhbmQKICAgICAgICAgICAgICAgIFNUT1JFWydCdXkgT3B0aW9uJ10gYW5kCiAgICAgICAgICAgICAgICBTVE9SRVsnQnV5IFBlcmNlbnRhZ2UnXSk6CiAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgIGlmIGNvaW5zX21lc3NhZ2UgIT0gW106CiAgICAgICAgICAgICAgICByZXN1bHQgPSAwCiAgICAgICAgICAgICAgICBmb3IgaSBpbiBjb2luc19tZXNzYWdlOgogICAgICAgICAgICAgICAgICAgIHJlc3VsdCArPSBpCgogICAgICAgICAgICAgICAgaWYgbm90IGJlYXJfY29pbiA+PSAobGltaXQtMSk6CiAgICAgICAgICAgICAgICAgICAgYmEuc2NyZWVubWVzc2FnZShnZXRsYW5ndWFnZSgnQ29pbnMgTWVzc2FnZScsc3Vicz1yZXN1bHQpLCgwLDEsMCkpCiAgICAgICAgICAgICAgICAgICAgYmEucGxheXNvdW5kKGJhLmdldHNvdW5kKCdjYXNoUmVnaXN0ZXInKSkKICAgICAgICAgICAgICAgIGVsc2U6CiAgICAgICAgICAgICAgICAgICAgYmEuc2NyZWVubWVzc2FnZShnZXRsYW5ndWFnZSgnQ29pbnMgTGltaXQgTWVzc2FnZScsCiAgICAgICAgICAgICAgICAgICAgICAgIGFsbWFjZW49W3Jlc3VsdCxsaW1pdF0pLCgxLDAsMCkpCiAgICAgICAgICAgICAgICAgICAgYmEucGxheXNvdW5kKGJhLmdldHNvdW5kKCdlcnJvcicpKQogICAgICAgICAgICAgICAgc2VsZi5iZWFyX2NvaW5fbWVzc2FnZSA9IFRydWUKICAgICAgICAgICAgICAgIEdMT0JBTFsnQ29pbnMgTWVzc2FnZSddID0gW10KICAgIGV4Y2VwdDogcGFzcwoKU3BhekJvdC5zdXBlcl9oYW5kbGVtZXNzYWdlID0gU3BhekJvdC5oYW5kbGVtZXNzYWdlCmRlZiBib3RfaGFuZGxlbWVzc2FnZShzZWxmLCBtc2c6IEFueSk6CiAgICBzZWxmLnN1cGVyX2hhbmRsZW1lc3NhZ2UobXNnKQogICAgaWYgaXNpbnN0YW5jZShtc2csIGJhLkRpZU1lc3NhZ2UpOgogICAgICAgIGlmIG5vdCBzZWxmLmRpZToKICAgICAgICAgICAgc2VsZi5kaWUgPSBUcnVlCiAgICAgICAgICAgIHNlbGYubGltaXQgPSA4NDAwCiAgICAgICAgICAgIHNlbGYuZnJlZV9jb2lucyA9IHJhbmRvbS5yYW5kaW50KDEsMjUpCiAgICAgICAgICAgIHNlbGYuYmVhcl9jb2lucyA9IGFwZ1snQmVhciBDb2luJ10KICAgICAgICAgICAgCiAgICAgICAgICAgIGlmIG5vdCBzZWxmLmJlYXJfY29pbnMgPj0gKHNlbGYubGltaXQpOgogICAgICAgICAgICAgICAgc2VsZi5iZWFyX2NvaW5zICs9IHNlbGYuZnJlZV9jb2lucwogICAgICAgICAgICAgICAgR0xPQkFMWydDb2lucyBNZXNzYWdlJ10uYXBwZW5kKHNlbGYuZnJlZV9jb2lucykKCiAgICAgICAgICAgICAgICBpZiBzZWxmLmJlYXJfY29pbnMgPj0gKHNlbGYubGltaXQpOgogICAgICAgICAgICAgICAgICAgIHNlbGYuYmVhcl9jb2lucyA9IHNlbGYubGltaXQKICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIGFwZ1snQmVhciBDb2luJ10gPSBpbnQoc2VsZi5iZWFyX2NvaW5zKQogICAgICAgICAgICAgICAgYXBnLmFwcGx5X2FuZF9jb21taXQoKQogICAgICAgICAgICAgICAgCiAgICAgICAgICAgIGVsc2U6IEdMT0JBTFsnQ29pbnMgTWVzc2FnZSddLmFwcGVuZChzZWxmLmZyZWVfY29pbnMpCgpkZWYgY2xzX3Bvd19jb2xvcigpOgogICAgcmV0dXJuIFsoMSwwLjEsMC4xKSwoMC4xLDAuNSwwLjkpLCgwLjEsMC45LDAuOSksCiAgICAgICAgICAgICgwLjEsMC45LDAuMSksKDAuMSwxLDAuNSksKDEsMSwwLjIpLCgyLDAuNSwwLjUpLCgxLDAsNildCgpkZWYgcmFuZG9tX2NvbG9yKCk6CiAgICBhID0gcmFuZG9tLnJhbmRvbSgpKjMKICAgIGIgPSByYW5kb20ucmFuZG9tKCkqMwogICAgYyA9IHJhbmRvbS5yYW5kb20oKSozCiAgICByZXR1cm4gKGEsYixjKQoKZGVmIHBvd2VydXBfZGlzdCgpOgogICAgcmV0dXJuICgoJ3RyaXBsZV9ib21icycsIHBvd2VydXBzWydUcmlwbGUnXSksCiAgICAgICAgICAgICgnaWNlX2JvbWJzJywgcG93ZXJ1cHNbJ0ljZSBCb21icyddKSwKICAgICAgICAgICAgKCdwdW5jaCcsIHBvd2VydXBzWydQdW5jaCddKSwKICAgICAgICAgICAgKCdpbXBhY3RfYm9tYnMnLCBwb3dlcnVwc1snSW1wYWN0IEJvbWJzJ10pLAogICAgICAgICAgICAoJ2xhbmRfbWluZXMnLCBwb3dlcnVwc1snTWluZSBCb21icyddKSwKICAgICAgICAgICAgKCdzdGlja3lfYm9tYnMnLCBwb3dlcnVwc1snU3RpY2t5IEJvbWJzJ10pLAogICAgICAgICAgICAoJ3NoaWVsZCcsIHBvd2VydXBzWydTaGllbGQnXSksCiAgICAgICAgICAgICgnaGVhbHRoJywgcG93ZXJ1cHNbJ0hlYWx0aCddKSwKICAgICAgICAgICAgKCdjdXJzZScsIHBvd2VydXBzWydDdXJzZSddKSwKICAgICAgICAgICAgKCdzcGVlZCcscG93ZXJ1cHNbJ1NwZWVkJ10pLAogICAgICAgICAgICAoJ2hlYWx0aF9kYW1hZ2UnLCBwb3dlcnVwc1snSGVhbGluZyBEYW1hZ2UnXSksCiAgICAgICAgICAgICgnZ29vZGJ5ZScscG93ZXJ1cHNbJ0dvb2RieWUnXSksCiAgICAgICAgICAgICgnaWNlX21hbicscG93ZXJ1cHNbJ0ljZSBNYW4nXSksCiAgICAgICAgICAgICgndGFua19zaGllbGQnLHBvd2VydXBzWydUYW5rIFNoaWVsZCddKSwKICAgICAgICAgICAgKCdpbXBhaXJtZW50X2JvbWJzJyxwb3dlcnVwc1snSW1wYWlybWVudCBCb21icyddKSwKICAgICAgICAgICAgKCdmaXJlX2JvbWJzJyxwb3dlcnVwc1snRmlyZSBCb21icyddKSwKICAgICAgICAgICAgKCdmbHlfYm9tYnMnLHBvd2VydXBzWydGbHkgQm9tYnMnXSkpCgpkZWYgcGVyY2VudGFnZV90YW5rX3NoaWVsZCgpOgogICAgcGVyY2VudGFnZSA9IGNvbmZpZ1snVGFuayBTaGllbGQgUFRHJ10KICAgIHBlcmNlbnRhZ2VfdGV4dCA9ICgnMC4nKSArIHN0cihwZXJjZW50YWdlKQogICAgcmV0dXJuIGZsb2F0KHBlcmNlbnRhZ2VfdGV4dCkKICAgIApkZWYgcGVyY2VudGFnZV9oZWFsdGhfZGFtYWdlKCk6CiAgICBwZXJjZW50YWdlID0gY29uZmlnWydIZWFsaW5nIERhbWFnZSBQVEcnXQogICAgcGVyY2VudGFnZV90ZXh0ID0gKCcwLicpICsgc3RyKHBlcmNlbnRhZ2UpCiAgICByZXR1cm4gZmxvYXQocGVyY2VudGFnZV90ZXh0KQoKIyA9PT0gTW9kaWZ5IGNsYXNzID09PQoKY2xhc3MgTmV3UHJvZmlsZUJyb3dzZXJXaW5kb3coYnJvd3Nlci5Qcm9maWxlQnJvd3NlcldpbmRvdyk6CiAgICBkZWYgX19pbml0X18oc2VsZiwKICAgICAgICAgICAgICAgICB0cmFuc2l0aW9uOiBzdHIgPSAnaW5fcmlnaHQnLAogICAgICAgICAgICAgICAgIGluX21haW5fbWVudTogYm9vbCA9IFRydWUsCiAgICAgICAgICAgICAgICAgc2VsZWN0ZWRfcHJvZmlsZTogc3RyID0gTm9uZSwKICAgICAgICAgICAgICAgICBvcmlnaW5fd2lkZ2V0OiBiYS5XaWRnZXQgPSBOb25lKToKICAgICAgICBzdXBlcigpLl9faW5pdF9fKHRyYW5zaXRpb24saW5fbWFpbl9tZW51LHNlbGVjdGVkX3Byb2ZpbGUsb3JpZ2luX3dpZGdldCkKICAgICAgIAogICAgICAgIHNlbGYuc2Vzc2lvbiA9IF9iYS5nZXRfZm9yZWdyb3VuZF9ob3N0X3Nlc3Npb24oKQogICAgICAgIHVpc2NhbGUgPSBiYS5hcHAudWkudWlzY2FsZQogICAgICAgIHdpZHRoID0gKDEwMCBpZiB1aXNjYWxlIGlzCiAgICAgICAgICAgICAgICAgYmEuVUlTY2FsZS5TTUFMTCBlbHNlIC0xNCkKICAgICAgICBzaXplID0gNTAKICAgICAgICBwb3NpdGlvbiA9ICh3aWR0aCoxLjY1LDMwMCkKIAogICAgICAgIGlmIGlzaW5zdGFuY2Uoc2VsZi5zZXNzaW9uLE1haW5NZW51U2Vzc2lvbik6CiAgICAgICAgICAgIHNlbGYuYnV0dG9uID0gYmEuYnV0dG9ud2lkZ2V0KHBhcmVudD1zZWxmLl9yb290X3dpZGdldCwKICAgICAgICAgICAgICAgICAgICAgICAgICBhdXRvc2VsZWN0PVRydWUscG9zaXRpb249cG9zaXRpb24sCiAgICAgICAgICAgICAgICAgICAgICAgICAgc2l6ZT0oc2l6ZSxzaXplKSxidXR0b25fdHlwZT0nc3F1YXJlJywKICAgICAgICAgICAgICAgICAgICAgICAgICBsYWJlbD0nJyxvbl9hY3RpdmF0ZV9jYWxsPWJhLkNhbGwoc2VsZi5wb3dlcnVwbWFuYWdlcl93aW5kb3cpKQogICAgICAgICAgICAKICAgICAgICAgICAgc2l6ZSA9IHNpemUqMC42MAogICAgICAgICAgICBzZWxmLmltYWdlID0gYmEuaW1hZ2V3aWRnZXQocGFyZW50PXNlbGYuX3Jvb3Rfd2lkZ2V0LAogICAgICAgICAgICAgICAgICAgICAgICAgIHNpemU9KHNpemUsc2l6ZSksZHJhd19jb250cm9sbGVyPXNlbGYuYnV0dG9uLAogICAgICAgICAgICAgICAgICAgICAgICAgIHBvc2l0aW9uPShwb3NpdGlvblswXSsxMC41LHBvc2l0aW9uWzFdKzE3KSwKICAgICAgICAgICAgICAgICAgICAgICAgICB0ZXh0dXJlPWJhLmdldHRleHR1cmUoJ3Bvd2VydXBTcGVlZCcpKQogICAgCiAgICAgICAgICAgIHNlbGYudGV4dCA9IGJhLnRleHR3aWRnZXQocGFyZW50PXNlbGYuX3Jvb3Rfd2lkZ2V0LAogICAgICAgICAgICAgICAgICAgICAgICAgIHBvc2l0aW9uPShwb3NpdGlvblswXSsyNSxwb3NpdGlvblsxXSsxMCksCiAgICAgICAgICAgICAgICAgICAgICAgICAgc2l6ZT0oMCwgMCksc2NhbGU9MC40NSxjb2xvcj0oMC43LDAuOSwwLjcsMS4wKSwKICAgICAgICAgICAgICAgICAgICAgICAgICBkcmF3X2NvbnRyb2xsZXI9c2VsZi5idXR0b24sbWF4d2lkdGg9NjAsCiAgICAgICAgICAgICAgICAgICAgICAgICAgdGV4dD0oZiJVbHRpbWF0ZSBQb3dlcnVwIHtfc3BffU1hbmFnZXIiKSxoX2FsaWduPSdjZW50ZXInLHZfYWxpZ249J2NlbnRlcicpCgogICAgZGVmIHBvd2VydXBtYW5hZ2VyX3dpbmRvdyhzZWxmKToKICAgICAgICBiYS5jb250YWluZXJ3aWRnZXQoZWRpdD1zZWxmLl9yb290X3dpZGdldCx0cmFuc2l0aW9uPSdvdXRfbGVmdCcpCiAgICAgICAgUG93ZXJ1cE1hbmFnZXJXaW5kb3coKQoKY2xhc3MgTmV3UG93ZXJ1cEJveEZhY3RvcnkocHVwYm94LlBvd2VydXBCb3hGYWN0b3J5KToKICAgIGRlZiBfX2luaXRfXyhzZWxmKSAtPiBOb25lOgogICAgICAgIHN1cGVyKCkuX19pbml0X18oKQogICAgICAgIHNlbGYudGV4X3NwZWVkID0gYmEuZ2V0dGV4dHVyZSgncG93ZXJ1cFNwZWVkJykKICAgICAgICBzZWxmLnRleF9oZWFsdGhfZGFtYWdlID0gYmEuZ2V0dGV4dHVyZSgnaGVhcnQnKQogICAgICAgIHNlbGYudGV4X2dvb2RieWUgPSBiYS5nZXR0ZXh0dXJlKCdhY2hpZXZlbWVudE9uc2xhdWdodCcpCiAgICAgICAgc2VsZi50ZXhfaWNlX21hbiA9IGJhLmdldHRleHR1cmUoJ291eWFVQnV0dG9uJykKICAgICAgICBzZWxmLnRleF90YW5rX3NoaWVsZCA9IGJhLmdldHRleHR1cmUoJ2FjaGlldmVtZW50U3VwZXJQdW5jaCcpCiAgICAgICAgc2VsZi50ZXhfaW1wYWlybWVudF9ib21icyA9IGJhLmdldHRleHR1cmUoJ2xldmVsSWNvbicpCiAgICAgICAgc2VsZi50ZXhfZmlyZV9ib21icyA9IGJhLmdldHRleHR1cmUoJ291eWFPQnV0dG9uJykKICAgICAgICBzZWxmLnRleF9mbHlfYm9tYnMgPSBiYS5nZXR0ZXh0dXJlKCdzdGFyJykKICAgICAgICAKICAgICAgICBzZWxmLl9wb3dlcnVwZGlzdCA9IFtdCiAgICAgICAgZm9yIHBvd2VydXAsIGZyZXEgaW4gcG93ZXJ1cF9kaXN0KCk6CiAgICAgICAgICAgIGZvciBfaSBpbiByYW5nZShpbnQoZnJlcSkpOgogICAgICAgICAgICAgICAgc2VsZi5fcG93ZXJ1cGRpc3QuYXBwZW5kKHBvd2VydXApCgogICAgZGVmIGdldF9yYW5kb21fcG93ZXJ1cF90eXBlKHNlbGYsZm9yY2V0eXBlID0gTm9uZSwgZXhjbHVkZXR5cGVzID0gTm9uZSk6CiAgICAgICAgCiAgICAgICAgdHJ5OiBzZWxmLm1hcGEgPSBiYS5nZXRhY3Rpdml0eSgpLl9tYXAuZ2V0bmFtZSgpCiAgICAgICAgZXhjZXB0OiBzZWxmLm1hcGEgPSBOb25lCiAgICAgIAogICAgICAgIHNwZWVkX2Jhbm5lZF9tYXBzID0gWydIb2NrZXkgU3RhZGl1bScsJ0xha2UgRnJpZ2lkJywnSGFwcHkgVGhvdWdodHMnXQogICAgICAKICAgICAgICBpZiBzZWxmLm1hcGEgaW4gc3BlZWRfYmFubmVkX21hcHM6CiAgICAgICAgICAgIHBvd2VydXBfZGlzYWJsZSA9IFsnc3BlZWQnXQogICAgICAgIGVsc2U6IHBvd2VydXBfZGlzYWJsZSA9IFtdCiAgICAgIAogICAgICAgIGlmIGV4Y2x1ZGV0eXBlcyBpcyBOb25lOgogICAgICAgICAgICBleGNsdWRldHlwZXMgPSBbXQogICAgICAgIGlmIGZvcmNldHlwZToKICAgICAgICAgICAgcHR5cGUgPSBmb3JjZXR5cGUKICAgICAgICBlbHNlOgogICAgICAgICAgICBpZiBzZWxmLl9sYXN0cG93ZXJ1cHR5cGUgPT0gJ2N1cnNlJzoKICAgICAgICAgICAgICAgIHB0eXBlID0gJ2hlYWx0aCcKICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgIHdoaWxlIFRydWU6CiAgICAgICAgICAgICAgICAgICAgcHR5cGUgPSBzZWxmLl9wb3dlcnVwZGlzdFtyYW5kb20ucmFuZGludCgKICAgICAgICAgICAgICAgICAgICAgICAgMCwKICAgICAgICAgICAgICAgICAgICAgICAgbGVuKHNlbGYuX3Bvd2VydXBkaXN0KSAtIDEpXQogICAgICAgICAgICAgICAgICAgIGlmIHB0eXBlIG5vdCBpbiBleGNsdWRldHlwZXMgYW5kIHB0eXBlIG5vdCBpbiBwb3dlcnVwX2Rpc2FibGU6IGJyZWFrCiAgICAgICAgc2VsZi5fbGFzdHBvd2VydXB0eXBlID0gcHR5cGUKICAgICAgICByZXR1cm4gcHR5cGUKCmRlZiBmaXJlX2VmZmVjdChzZWxmKToKICAgIGlmIHNlbGYubm9kZS5leGlzdHMoKToKICAgICAgICBiYS5lbWl0ZngocG9zaXRpb249c2VsZi5ub2RlLnBvc2l0aW9uLAogICAgICAgIHNjYWxlPTMsY291bnQ9NTAqMixzcHJlYWQ9MC4zLAogICAgICAgIGNodW5rX3R5cGU9J3N3ZWF0JykKICAgIGVsc2U6IHNlbGYuZmlyZV9lZmZlY3RfdGltZSA9IE5vbmUKCiMjIyMjIyMjIyMjQk9NQlMKQm9tYi5fcG1fb2xkX2JvbWIgPSBCb21iLl9faW5pdF9fCmRlZiBfYm9tYl9pbml0KHNlbGYsCiAgICAgICAgICAgICAgIHBvc2l0aW9uOiBTZXF1ZW5jZVtmbG9hdF0gPSAoMC4wLCAxLjAsIDAuMCksCiAgICAgICAgICAgICAgIHZlbG9jaXR5OiBTZXF1ZW5jZVtmbG9hdF0gPSAoMC4wLCAwLjAsIDAuMCksCiAgICAgICAgICAgICAgIGJvbWJfdHlwZTogc3RyID0gJ25vcm1hbCcsCiAgICAgICAgICAgICAgIGJsYXN0X3JhZGl1czogZmxvYXQgPSAyLjAsCiAgICAgICAgICAgICAgIGJvbWJfc2NhbGU6IGZsb2F0ID0gMS4wLAogICAgICAgICAgICAgICBzb3VyY2VfcGxheWVyOiBiYS5QbGF5ZXIgPSBOb25lLAogICAgICAgICAgICAgICBvd25lcjogYmEuTm9kZSA9IE5vbmUpOgoKICAgIHNlbGYuYm1fdHlwZSA9IGJvbWJfdHlwZQogICAgbmV3X2JvbWJfdHlwZSA9IGJvbWJfdHlwZQogICAgYm9tYnMgPSBbJ2ljZV9idWJibGUnLCdpbXBhaXJtZW50JywnZmlyZScsJ2ZseSddCiAgICAKICAgIGlmIGJvbWJfdHlwZSBpbiBib21iczoKICAgICAgICBuZXdfYm9tYl90eXBlID0gJ2ljZScKICAgICAgICAgICAgICAgICAgIAogICAgc2VsZi5fcG1fb2xkX2JvbWIocG9zaXRpb24sdmVsb2NpdHksbmV3X2JvbWJfdHlwZSxibGFzdF9yYWRpdXMsCiAgICAgICAgICAgICAgICAgICAgICBib21iX3NjYWxlLHNvdXJjZV9wbGF5ZXIsb3duZXIpCiAgICAKICAgIHRleCA9IHNlbGYubm9kZS5jb2xvcl90ZXh0dXJlCiAgICAKICAgIGlmIHNlbGYuYm1fdHlwZSA9PSAnaWNlX2J1YmJsZSc6CiAgICAgICAgc2VsZi5ib21iX3R5cGUgPSBzZWxmLmJtX3R5cGUKICAgICAgICBzZWxmLm5vZGUubW9kZWwgPSBOb25lCiAgICAgICAgc2VsZi5zaGllbGRfaWNlID0gYmEubmV3bm9kZSgnc2hpZWxkJyxvd25lcj1zZWxmLm5vZGUsCiAgICAgICAgICAgIGF0dHJzPXsnY29sb3InOiAoMC41LCAxLjAsIDcuMCksJ3JhZGl1cyc6IDAuNn0pCiAgICAgICAgc2VsZi5ub2RlLmNvbm5lY3RhdHRyKCdwb3NpdGlvbicsIHNlbGYuc2hpZWxkX2ljZSwgJ3Bvc2l0aW9uJykKICAgIGVsaWYgc2VsZi5ibV90eXBlID09ICdmaXJlJzoKICAgICAgICBzZWxmLmJvbWJfdHlwZSA9IHNlbGYuYm1fdHlwZQogICAgICAgIHNlbGYubm9kZS5tb2RlbCA9IE5vbmUKICAgICAgICBzZWxmLnNoaWVsZF9maXJlID0gYmEubmV3bm9kZSgnc2hpZWxkJyxvd25lcj1zZWxmLm5vZGUsCiAgICAgICAgICAgIGF0dHJzPXsnY29sb3InOiAoNi41LCA2LjUsIDIuMCksJ3JhZGl1cyc6IDAuNn0pCiAgICAgICAgc2VsZi5ub2RlLmNvbm5lY3RhdHRyKCdwb3NpdGlvbicsIHNlbGYuc2hpZWxkX2ZpcmUsICdwb3NpdGlvbicpCiAgICAgICAgc2VsZi5maXJlX2VmZmVjdF90aW1lID0gYmEuVGltZXIoMC4xLGJhLkNhbGwoZmlyZV9lZmZlY3Qsc2VsZikscmVwZWF0PVRydWUpCiAgICBlbGlmIHNlbGYuYm1fdHlwZSA9PSAnaW1wYWlybWVudCc6CiAgICAgICAgc2VsZi5ib21iX3R5cGUgPSBzZWxmLmJtX3R5cGUKICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdlZ2dUZXgzJykKICAgIGVsaWYgc2VsZi5ibV90eXBlID09ICdmbHknOgogICAgICAgIHNlbGYuYm9tYl90eXBlID0gc2VsZi5ibV90eXBlCiAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgnZWdnVGV4MScpCgogICAgc2VsZi5ub2RlLmNvbG9yX3RleHR1cmUgPSB0ZXgKICAgIHNlbGYuaGl0X3N1YnR5cGUgPSBzZWxmLmJvbWJfdHlwZQoKICAgIGlmIHNlbGYuYm9tYl90eXBlID09ICdpY2VfYnViYmxlJzoKICAgICAgICBzZWxmLmJsYXN0X3JhZGl1cyAqPSAxLjIKICAgIGVsaWYgc2VsZi5ib21iX3R5cGUgPT0gJ2ZseSc6CiAgICAgICAgc2VsZi5ibGFzdF9yYWRpdXMgKj0gMi4yCgpkZWYgYm9tYl9oYW5kbGVtZXNzYWdlKHNlbGYsIG1zZzogQW55KSAtPiBBbnk6CiAgICBhc3NlcnQgbm90IHNlbGYuZXhwaXJlZAoKICAgIGlmIGlzaW5zdGFuY2UobXNnLCBiYS5EaWVNZXNzYWdlKToKICAgICAgICBpZiBzZWxmLm5vZGU6CiAgICAgICAgICAgIHNlbGYubm9kZS5kZWxldGUoKQoKICAgIGVsaWYgaXNpbnN0YW5jZShtc2csIGJvbWIuRXhwbG9kZUhpdE1lc3NhZ2UpOgogICAgICAgIG5vZGUgPSBiYS5nZXRjb2xsaXNpb24oKS5vcHBvc2luZ25vZGUKICAgICAgICBhc3NlcnQgc2VsZi5ub2RlCiAgICAgICAgbm9kZXBvcyA9IHNlbGYubm9kZS5wb3NpdGlvbgogICAgICAgIG1hZyA9IDIwMDAuMAogICAgICAgIGlmIHNlbGYuYmxhc3RfdHlwZSBpbiAoJ2ljZScsJ2ljZV9idWJibGUnKToKICAgICAgICAgICAgbWFnICo9IDAuNQogICAgICAgIGVsaWYgc2VsZi5ibGFzdF90eXBlID09ICdsYW5kX21pbmUnOgogICAgICAgICAgICBtYWcgKj0gMi41CiAgICAgICAgZWxpZiBzZWxmLmJsYXN0X3R5cGUgPT0gJ3RudCc6CiAgICAgICAgICAgIG1hZyAqPSAyLjAKICAgICAgICBlbGlmIHNlbGYuYmxhc3RfdHlwZSA9PSAnZmlyZSc6CiAgICAgICAgICAgIG1hZyAqPSAwLjYKICAgICAgICBlbGlmIHNlbGYuYmxhc3RfdHlwZSA9PSAnZmx5JzoKICAgICAgICAgICAgbWFnICo9IDUuNQoKICAgICAgICBub2RlLmhhbmRsZW1lc3NhZ2UoCiAgICAgICAgICAgIGJhLkhpdE1lc3NhZ2UocG9zPW5vZGVwb3MsCiAgICAgICAgICAgICAgICAgICAgICAgICAgdmVsb2NpdHk9KDAsIDAsIDApLAogICAgICAgICAgICAgICAgICAgICAgICAgIG1hZ25pdHVkZT1tYWcsCiAgICAgICAgICAgICAgICAgICAgICAgICAgaGl0X3R5cGU9c2VsZi5oaXRfdHlwZSwKICAgICAgICAgICAgICAgICAgICAgICAgICBoaXRfc3VidHlwZT1zZWxmLmhpdF9zdWJ0eXBlLAogICAgICAgICAgICAgICAgICAgICAgICAgIHJhZGl1cz1zZWxmLnJhZGl1cywKICAgICAgICAgICAgICAgICAgICAgICAgICBzb3VyY2VfcGxheWVyPWJhLmV4aXN0aW5nKHNlbGYuX3NvdXJjZV9wbGF5ZXIpKSkKICAgICAgICBpZiBzZWxmLmJsYXN0X3R5cGUgaW4gKCdpY2UnLCdpY2VfYnViYmxlJyk6CiAgICAgICAgICAgIGJhLnBsYXlzb3VuZChib21iLkJvbWJGYWN0b3J5LmdldCgpLmZyZWV6ZV9zb3VuZCwKICAgICAgICAgICAgICAgICAgICAgICAgIDEwLAogICAgICAgICAgICAgICAgICAgICAgICAgcG9zaXRpb249bm9kZXBvcykKICAgICAgICAgICAgbm9kZS5oYW5kbGVtZXNzYWdlKGJhLkZyZWV6ZU1lc3NhZ2UoKSkKCiAgICByZXR1cm4gTm9uZQoKZGVmIHBvd2VydXBfdHJhbnNsYXRlZChzZWxmLCB0eXBlOiBzdHIpOgogICAgcG93ZXJ1cHNfbmFtZXMgPSB7J3RyaXBsZV9ib21icyc6IGJhLkxzdHIocmVzb3VyY2U9J2hlbHBXaW5kb3cuJysncG93ZXJ1cEJvbWJOYW1lVGV4dCcpLAogICAgICAgICAgICAgICAgJ2ljZV9ib21icyc6IGJhLkxzdHIocmVzb3VyY2U9J2hlbHBXaW5kb3cuJysncG93ZXJ1cEljZUJvbWJzTmFtZVRleHQnKSwKICAgICAgICAgICAgICAgICdwdW5jaCc6IGJhLkxzdHIocmVzb3VyY2U9J2hlbHBXaW5kb3cuJysncG93ZXJ1cFB1bmNoTmFtZVRleHQnKSwKICAgICAgICAgICAgICAgICdpbXBhY3RfYm9tYnMnOiBiYS5Mc3RyKHJlc291cmNlPSdoZWxwV2luZG93LicrJ3Bvd2VydXBJbXBhY3RCb21ic05hbWVUZXh0JyksCiAgICAgICAgICAgICAgICAnbGFuZF9taW5lcyc6IGJhLkxzdHIocmVzb3VyY2U9J2hlbHBXaW5kb3cuJysncG93ZXJ1cExhbmRNaW5lc05hbWVUZXh0JyksCiAgICAgICAgICAgICAgICAnc3RpY2t5X2JvbWJzJzogYmEuTHN0cihyZXNvdXJjZT0naGVscFdpbmRvdy4nKydwb3dlcnVwU3RpY2t5Qm9tYnNOYW1lVGV4dCcpLAogICAgICAgICAgICAgICAgJ3NoaWVsZCc6IGJhLkxzdHIocmVzb3VyY2U9J2hlbHBXaW5kb3cuJysncG93ZXJ1cFNoaWVsZE5hbWVUZXh0JyksCiAgICAgICAgICAgICAgICAnaGVhbHRoJzogYmEuTHN0cihyZXNvdXJjZT0naGVscFdpbmRvdy4nKydwb3dlcnVwSGVhbHRoTmFtZVRleHQnKSwKICAgICAgICAgICAgICAgICdjdXJzZSc6IGJhLkxzdHIocmVzb3VyY2U9J2hlbHBXaW5kb3cuJysncG93ZXJ1cEN1cnNlTmFtZVRleHQnKSwKICAgICAgICAgICAgICAgICdzcGVlZCc6IGdldGxhbmd1YWdlKCdTcGVlZCcpLAogICAgICAgICAgICAgICAgJ2hlYWx0aF9kYW1hZ2UnOiBnZXRsYW5ndWFnZSgnSGVhbGluZyBEYW1hZ2UnKSwKICAgICAgICAgICAgICAgICdnb29kYnllJzogZ2V0bGFuZ3VhZ2UoJ0dvb2RieWUnKSwKICAgICAgICAgICAgICAgICdpY2VfbWFuJzogZ2V0bGFuZ3VhZ2UoJ0ljZSBNYW4nKSwKICAgICAgICAgICAgICAgICd0YW5rX3NoaWVsZCc6IGdldGxhbmd1YWdlKCdUYW5rIFNoaWVsZCcpLAogICAgICAgICAgICAgICAgJ2ltcGFpcm1lbnRfYm9tYnMnOiBnZXRsYW5ndWFnZSgnSW1wYWlybWVudCBCb21icycpLAogICAgICAgICAgICAgICAgJ2ZpcmVfYm9tYnMnOiBnZXRsYW5ndWFnZSgnRmlyZSBCb21icycpLAogICAgICAgICAgICAgICAgJ2ZseV9ib21icyc6IGdldGxhbmd1YWdlKCdGbHkgQm9tYnMnKX0KICAgIHNlbGYudGV4dHNbJ05hbWUnXS50ZXh0ID0gcG93ZXJ1cHNfbmFtZXNbdHlwZV0KICAgICAgICAgICAgICAgIAojIyMjIyMjIyMjI1BPV0VSVVAKcHVwYm94LlBvd2VydXBCb3guX29sZF9wYnhfID0gcHVwYm94LlBvd2VydXBCb3guX19pbml0X18KZGVmIF9wYnhfKHNlbGYsIHBvc2l0aW9uOiBTZXF1ZW5jZVtmbG9hdF0gPSAoMC4wLCAxLjAsIDAuMCksCiAgICAgICAgICBwb3dlcnVwdHlwZTogc3RyID0gJ3RyaXBsZV9ib21icycsCiAgICAgICAgICBleHBpcmU6IGJvb2wgPSBUcnVlKToKICAgIAogICAgc2VsZi5uZXdzOiBsaXN0ID0gW10KICAgIGZvciB4LGkgaW4gcG93ZXJ1cF9kaXN0KCk6IHNlbGYubmV3cy5hcHBlbmQoeCkKICAgIAogICAgc2VsZi5ib3g6IGxpc3QgPSBbXQogICAgc2VsZi50ZXh0cyA9IHt9CiAgICBzZWxmLm5ld3MgPSBzZWxmLm5ld3NbOTpdCiAgICBzZWxmLmJveC5hcHBlbmQocG93ZXJ1cHR5cGUpCiAgICBzZWxmLm5wb3dlcnVwID0gc2VsZi5ib3hbMF0KICAgIGZhY3RvcnkgPSBOZXdQb3dlcnVwQm94RmFjdG9yeS5nZXQoKQogICAgCiAgICBpZiBzZWxmLm5wb3dlcnVwIGluIHNlbGYubmV3czogbmV3X3Bvd2VydXB0eXBlID0gJ3NoaWVsZCcKICAgIGVsc2U6IG5ld19wb3dlcnVwdHlwZSA9IHBvd2VydXB0eXBlCiAgICBzZWxmLl9vbGRfcGJ4Xyhwb3NpdGlvbixuZXdfcG93ZXJ1cHR5cGUsZXhwaXJlKQogICAgCiAgICB0eXBlID0gbmV3X3Bvd2VydXB0eXBlCiAgICB0ZXggPSBzZWxmLm5vZGUuY29sb3JfdGV4dHVyZQogICAgbW9kZWwgPSBzZWxmLm5vZGUubW9kZWwKICAgIAogICAgaWYgc2VsZi5ucG93ZXJ1cCA9PSAnc3BlZWQnOgogICAgICAgIHR5cGUgPSBzZWxmLm5wb3dlcnVwCiAgICAgICAgdGV4ID0gZmFjdG9yeS50ZXhfc3BlZWQKICAgIGVsaWYgc2VsZi5ucG93ZXJ1cCA9PSAnaGVhbHRoX2RhbWFnZSc6CiAgICAgICAgdHlwZSA9IHNlbGYubnBvd2VydXAKICAgICAgICB0ZXggPSBmYWN0b3J5LnRleF9oZWFsdGhfZGFtYWdlCiAgICBlbGlmIHNlbGYubnBvd2VydXAgPT0gJ2dvb2RieWUnOgogICAgICAgIHR5cGUgPSBzZWxmLm5wb3dlcnVwCiAgICAgICAgdGV4ID0gZmFjdG9yeS50ZXhfZ29vZGJ5ZQogICAgZWxpZiBzZWxmLm5wb3dlcnVwID09ICdpY2VfbWFuJzoKICAgICAgICB0eXBlID0gc2VsZi5ucG93ZXJ1cAogICAgICAgIHRleCA9IGZhY3RvcnkudGV4X2ljZV9tYW4KICAgIGVsaWYgc2VsZi5ucG93ZXJ1cCA9PSAndGFua19zaGllbGQnOgogICAgICAgIHR5cGUgPSBzZWxmLm5wb3dlcnVwCiAgICAgICAgdGV4ID0gZmFjdG9yeS50ZXhfdGFua19zaGllbGQKICAgIGVsaWYgc2VsZi5ucG93ZXJ1cCA9PSAnaW1wYWlybWVudF9ib21icyc6CiAgICAgICAgdHlwZSA9IHNlbGYubnBvd2VydXAKICAgICAgICB0ZXggPSBmYWN0b3J5LnRleF9pbXBhaXJtZW50X2JvbWJzCiAgICBlbGlmIHNlbGYubnBvd2VydXAgPT0gJ2ZpcmVfYm9tYnMnOgogICAgICAgIHR5cGUgPSBzZWxmLm5wb3dlcnVwCiAgICAgICAgdGV4ID0gZmFjdG9yeS50ZXhfZmlyZV9ib21icwogICAgZWxpZiBzZWxmLm5wb3dlcnVwID09ICdmbHlfYm9tYnMnOgogICAgICAgIHR5cGUgPSBzZWxmLm5wb3dlcnVwCiAgICAgICAgdGV4ID0gZmFjdG9yeS50ZXhfZmx5X2JvbWJzCgogICAgc2VsZi5wb3dlcnVwdHlwZSA9IHR5cGUKICAgIHNlbGYubm9kZS5tb2RlbCA9IG1vZGVsCiAgICBzZWxmLm5vZGUuY29sb3JfdGV4dHVyZSA9IHRleAogICAgbl9zY2FsZSA9IGNvbmZpZ1snUG93ZXJ1cCBTY2FsZSddCiAgICBzdHlsZSA9IGNvbmZpZ1snUG93ZXJ1cCBTdHlsZSddCgogICAgY3VydmUgPSBiYS5hbmltYXRlKHNlbGYubm9kZSwgJ21vZGVsX3NjYWxlJywgezA6IDAsIDAuMTQ6IDEuNiwgMC4yOiBuX3NjYWxlfSkKICAgIGJhLnRpbWVyKDAuMiwgY3VydmUuZGVsZXRlKQogICAgICAgIAogICAgZGVmIHV0aWxfdGV4dCh0eXBlOiBzdHIsIHRleHQ6IHN0ciwgc2NhbGU6IGZsb2F0ID0gMSwgY29sb3I6IGxpc3QgPSBbMSwxLDFdLAogICAgICAgICAgICAgICAgICBwb3NpdGlvbjogbGlzdCA9IFswLCAwLjcsIDBdLCBjb2xvcnNfbmFtZTogYm9vbCA9IEZhbHNlKToKICAgICAgICBtID0gYmEubmV3bm9kZSgnbWF0aCcsb3duZXI9c2VsZi5ub2RlLGF0dHJzPXsnaW5wdXQxJzoKICAgICAgICAgICAgKHBvc2l0aW9uWzBdLCBwb3NpdGlvblsxXSwgcG9zaXRpb25bMl0pLCdvcGVyYXRpb24nOiAnYWRkJ30pCiAgICAgICAgc2VsZi5ub2RlLmNvbm5lY3RhdHRyKCdwb3NpdGlvbicsIG0sICdpbnB1dDInKQogICAgICAgIHNlbGYudGV4dHNbdHlwZV0gPSBiYS5uZXdub2RlKCd0ZXh0Jyxvd25lcj1zZWxmLm5vZGUsCiAgICAgICAgICAgICAgICBhdHRycz17J3RleHQnOiBzdHIodGV4dCksCiAgICAgICAgICAgICAgICAgICAgICAnaW5fd29ybGQnOiBUcnVlLAogICAgICAgICAgICAgICAgICAgICAgJ3NjYWxlJzogMC4wMiwKICAgICAgICAgICAgICAgICAgICAgICdzaGFkb3cnOiAwLjUsCiAgICAgICAgICAgICAgICAgICAgICAnZmxhdG5lc3MnOiAxLjAsCiAgICAgICAgICAgICAgICAgICAgICAnY29sb3InOiAoY29sb3JbMF0sY29sb3JbMV0sY29sb3JbMl0pLAogICAgICAgICAgICAgICAgICAgICAgJ2hfYWxpZ24nOiAnY2VudGVyJ30pIAogICAgICAgIG0uY29ubmVjdGF0dHIoJ291dHB1dCcsIHNlbGYudGV4dHNbdHlwZV0sICdwb3NpdGlvbicpCiAgICAgICAgYmEuYW5pbWF0ZShzZWxmLnRleHRzW3R5cGVdLCAnc2NhbGUnLCB7MDogMC4wMTcsMC40OiAwLjAxNywgMC41OiAwLjAxKnNjYWxlfSkKICAgIAogICAgICAgIGlmIGNvbG9yc19uYW1lOgogICAgICAgICAgICBiYS5hbmltYXRlX2FycmF5KHNlbGYudGV4dHNbdHlwZV0sICdjb2xvcicsIDMsCiAgICAgICAgICAgICAgICB7MDooMSwwLDApLAogICAgICAgICAgICAgICAgIDAuMjooMSwwLjUsMCksCiAgICAgICAgICAgICAgICAgMC40OigxLDEsMCksCiAgICAgICAgICAgICAgICAgMC42OigwLDEsMCksCiAgICAgICAgICAgICAgICAgMC44OigwLDEsMSksCiAgICAgICAgICAgICAgICAgMS4wOigxLDAsMSksCiAgICAgICAgICAgICAgICAgMS4yOigxLDAsMCl9LFRydWUpCiAgICAKICAgIGRlZiB1cGRhdGVfdGltZSh0aW1lKToKICAgICAgICBpZiBzZWxmLnRleHRzWydUaW1lJ10uZXhpc3RzKCk6CiAgICAgICAgICAgIHNlbGYudGV4dHNbJ1RpbWUnXS50ZXh0ID0gc3RyKHRpbWUpCiAgICAgICAgCiAgICBpZiBjb25maWdbJ1Bvd2VydXAgVGltZSddOgogICAgICAgIGludGVydmFsID0gaW50KHB1cGJveC5ERUZBVUxUX1BPV0VSVVBfSU5URVJWQUwpCiAgICAgICAgdGltZTIgPSAoaW50ZXJ2YWwtMSkKICAgICAgICB0aW1lID0gMQogICAgICAgIAogICAgICAgIHV0aWxfdGV4dCgnVGltZScsIHRpbWUyLCBzY2FsZT0xLjUsY29sb3I9KDIsMiwyKSwKICAgICAgICAgICAgICAgICAgcG9zaXRpb249WzAsMC45LDBdLCBjb2xvcnNfbmFtZT1GYWxzZSkKICAgICAgICAKICAgICAgICB3aGlsZShpbnRlcnZhbCszKToKICAgICAgICAgICAgYmEudGltZXIodGltZS0xLGJhLkNhbGwodXBkYXRlX3RpbWUsZid7dGltZTJ9cycpKQogICAgCiAgICAgICAgICAgIGlmIHRpbWUyID09IDA6CiAgICAgICAgICAgICAgICBicmVhawogICAgCiAgICAgICAgICAgIHRpbWUgKz0gMQogICAgICAgICAgICB0aW1lMiAtPSAxCiAgICAKICAgIGlmIGNvbmZpZ1snUG93ZXJ1cCBXaXRoIFNoaWVsZCddOgogICAgICAgIHNjYWxlID0gY29uZmlnWydQb3dlcnVwIFNjYWxlJ10KICAgICAgICBzZWxmLnNoaWVsZCA9IGJhLm5ld25vZGUoJ3NoaWVsZCcsb3duZXI9c2VsZi5ub2RlLGF0dHJzPXsnY29sb3InOiAoMSwxLDApLCdyYWRpdXMnOiAxLjMqc2NhbGV9KQogICAgICAgIHNlbGYubm9kZS5jb25uZWN0YXR0cigncG9zaXRpb24nLCBzZWxmLnNoaWVsZCwgJ3Bvc2l0aW9uJykKICAgICAgICBiYS5hbmltYXRlX2FycmF5KHNlbGYuc2hpZWxkLCdjb2xvcicsMyx7MDogKDIsMCwwKSwgMC41OiAoMCwyLDApLCAxOiAoMCwxLDYpLCAxLjU6ICgyLDAsMCl9LFRydWUpCiAgICAKICAgIGlmIGNvbmZpZ1snUG93ZXJ1cCBOYW1lJ106ICAgIAogICAgICAgIHV0aWxfdGV4dCgnTmFtZScsc2VsZi5wb3dlcnVwdHlwZSxzY2FsZT0xLjIsCiAgICAgICAgICAgICAgICAgIHBvc2l0aW9uPVswLDAuNCwwXSxjb2xvcnNfbmFtZT1UcnVlKQogICAgICAgIHBvd2VydXBfdHJhbnNsYXRlZChzZWxmLHNlbGYucG93ZXJ1cHR5cGUpCiAgICAgICAgCiAgICBpZiBzdHlsZSA9PSAnU1k6IEJBTEwnOgogICAgICAgIHNlbGYubm9kZS5tb2RlbCA9IGJhLmdldG1vZGVsKCdmcm9zdHlQZWx2aXMnKQogICAgZWxpZiBzdHlsZSA9PSAnU1k6IEltcGFjdCc6CiAgICAgICAgc2VsZi5ub2RlLm1vZGVsID0gYmEuZ2V0bW9kZWwoJ2ltcGFjdEJvbWInKQogICAgZWxpZiBzdHlsZSA9PSAnU1k6IEVnZyc6CiAgICAgICAgc2VsZi5ub2RlLm1vZGVsID0gYmEuZ2V0bW9kZWwoJ2VnZycpCiAgICAgICAgCiMjIyMjIyMjIyMjU1BBWgpkZWYgX3NwZWVkX29mZl9mbGFzaChzZWxmKToKICAgIGlmIHNlbGYubm9kZToKICAgICAgICBmYWN0b3J5ID0gTmV3UG93ZXJ1cEJveEZhY3RvcnkuZ2V0KCkKICAgICAgICBzZWxmLm5vZGUuYmlsbGJvYXJkX3RleHR1cmUgPSBmYWN0b3J5LnRleF9zcGVlZAogICAgICAgIHNlbGYubm9kZS5iaWxsYm9hcmRfb3BhY2l0eSA9IDEuMAogICAgICAgIHNlbGYubm9kZS5iaWxsYm9hcmRfY3Jvc3Nfb3V0ID0gVHJ1ZQogICAgICAgIApkZWYgX3NwZWVkX3dlYXJfb2ZmKHNlbGYpOgogICAgaWYgc2VsZi5ub2RlOgogICAgICAgIHNlbGYubm9kZS5ob2NrZXkgPSBGYWxzZQogICAgICAgIHNlbGYubm9kZS5iaWxsYm9hcmRfb3BhY2l0eSA9IDAuMAogICAgICAgIGJhLnBsYXlzb3VuZChiYS5nZXRzb3VuZCgncG93ZXJkb3duMDEnKSkKICAgICAgICAKZGVmIF9pY2VfbWFuX29mZl9mbGFzaChzZWxmKToKICAgIGlmIHNlbGYubm9kZToKICAgICAgICBmYWN0b3J5ID0gTmV3UG93ZXJ1cEJveEZhY3RvcnkuZ2V0KCkKICAgICAgICBzZWxmLm5vZGUuYmlsbGJvYXJkX3RleHR1cmUgPSBmYWN0b3J5LnRleF9pY2VfbWFuCiAgICAgICAgc2VsZi5ub2RlLmJpbGxib2FyZF9vcGFjaXR5ID0gMS4wCiAgICAgICAgc2VsZi5ub2RlLmJpbGxib2FyZF9jcm9zc19vdXQgPSBUcnVlCiAgICAgICAgCmRlZiBfaWNlX21hbl93ZWFyX29mZihzZWxmKToKICAgIGlmIHNlbGYubm9kZToKICAgICAgICBmID0gc2VsZi5jb2xvclswXQogICAgICAgIGkgPSAoMCwxLDQpCiAgICAgICAgCiAgICAgICAgYm9tYiA9IHNlbGYuYm1iX2NvbG9yWzBdICAgICAgICAKICAgICAgICBpZiBib21iICE9ICdpY2VfYnViYmxlJzogc2VsZi5ib21iX3R5cGUgPSBib21iCiAgICAgICAgZWxzZTogc2VsZi5ib21iX3R5cGUgPSAnbm9ybWFsJwogICAgICAgIAogICAgICAgIHNlbGYuZnJlZXplX3B1bmNoID0gRmFsc2UKICAgICAgICBzZWxmLm5vZGUuYmlsbGJvYXJkX29wYWNpdHkgPSAwLjAKICAgICAgICBiYS5hbmltYXRlX2FycmF5KHNlbGYubm9kZSwnY29sb3InLDMsezA6IGYsIDAuMzogaSwgMC42OiBmfSkKICAgICAgICBiYS5wbGF5c291bmQoYmEuZ2V0c291bmQoJ3Bvd2VyZG93bjAxJykpCiAgICAgICAgClNwYXouX3BtMl9zcHpfb2xkID0gU3Bhei5fX2luaXRfXwpkZWYgX2luaXRfc3Bhel8oc2VsZiwqYXJncywgKiprd2FyZ3MpOgogICAgc2VsZi5fcG0yX3Nwel9vbGQoKmFyZ3MsICoqa3dhcmdzKQogICAgc2VsZi5lZGdfZWZmID0gRmFsc2UKICAgIHNlbGYua2lsbF9lZmYgPSBGYWxzZQogICAgc2VsZi5mcmVlemVfcHVuY2ggPSBGYWxzZQogICAgc2VsZi5kaWUgPSBGYWxzZQogICAgc2VsZi5jb2xvcjogbGlzdCA9IFtdCiAgICBzZWxmLmNvbG9yLmFwcGVuZChzZWxmLm5vZGUuY29sb3IpCiAgICAKICAgIHNlbGYudGFua3NoaWVsZCA9IHsiVGFuayI6IEZhbHNlLAogICAgICAgICAgICAgICAgICAgICAgICJSZWR1Y3Rpb24iOiBGYWxzZSwKICAgICAgICAgICAgICAgICAgICAgICAiU2hpZWxkIjogTm9uZX0KClNwYXouX3N1cGVyX29uX3B1bmNoX3ByZXNzID0gU3Bhei5vbl9wdW5jaF9wcmVzcwpkZWYgc3Bhel9vbl9wdW5jaF9wcmVzcyhzZWxmKSAtPiBOb25lOgogICAgc2VsZi5fc3VwZXJfb25fcHVuY2hfcHJlc3MoKQoKICAgIGlmIHNlbGYudGFua3NoaWVsZFsnVGFuayddOgogICAgICAgIHRyeToKICAgICAgICAgICAgc2VsZi50YW5rc2hpZWxkWydSZWR1Y3Rpb24nXSA9IFRydWUKICAgICAgICAgICAgCiAgICAgICAgICAgIHNoaWVsZCA9IGJhLm5ld25vZGUoJ3NoaWVsZCcsb3duZXI9c2VsZi5ub2RlLAogICAgICAgICAgICAgICAgYXR0cnM9eydjb2xvcic6ICg0LDEsNCksJ3JhZGl1cyc6IDEuM30pCiAgICAgICAgICAgIHNlbGYubm9kZS5jb25uZWN0YXR0cigncG9zaXRpb25fY2VudGVyJywgc2hpZWxkLCAncG9zaXRpb24nKQogICAgICAgICAgICAKICAgICAgICAgICAgc2VsZi50YW5rc2hpZWxkWydTaGllbGQnXSA9IHNoaWVsZAogICAgICAgIGV4Y2VwdDogcGFzcwoKU3Bhei5fc3VwZXJfb25fcHVuY2hfcmVsZWFzZSA9IFNwYXoub25fcHVuY2hfcmVsZWFzZQpkZWYgc3Bhel9vbl9wdW5jaF9yZWxlYXNlKHNlbGYpIC0+IE5vbmU6CiAgICBzZWxmLl9zdXBlcl9vbl9wdW5jaF9yZWxlYXNlKCkKICAgIHRyeToKICAgICAgICBzZWxmLnRhbmtzaGllbGRbJ1NoaWVsZCddLmRlbGV0ZSgpCiAgICAgICAgc2VsZi50YW5rc2hpZWxkWydSZWR1Y3Rpb24nXSA9IEZhbHNlCiAgICBleGNlcHQ6IHBhc3MKCmRlZiBuZXdfZ2V0X2JvbWJfdHlwZV90ZXgoc2VsZikgLT4gYmEuVGV4dHVyZToKICAgICAgICBmYWN0b3J5ID0gTmV3UG93ZXJ1cEJveEZhY3RvcnkuZ2V0KCkKICAgICAgICBpZiBzZWxmLmJvbWJfdHlwZSA9PSAnc3RpY2t5JzoKICAgICAgICAgICAgcmV0dXJuIGZhY3RvcnkudGV4X3N0aWNreV9ib21icwogICAgICAgIGlmIHNlbGYuYm9tYl90eXBlID09ICdpY2UnOgogICAgICAgICAgICByZXR1cm4gZmFjdG9yeS50ZXhfaWNlX2JvbWJzCiAgICAgICAgaWYgc2VsZi5ib21iX3R5cGUgPT0gJ2ltcGFjdCc6CiAgICAgICAgICAgIHJldHVybiBmYWN0b3J5LnRleF9pbXBhY3RfYm9tYnMKICAgICAgICBpZiBzZWxmLmJvbWJfdHlwZSA9PSAnaW1wYWlybWVudCc6CiAgICAgICAgICAgIHJldHVybiBmYWN0b3J5LnRleF9pbXBhaXJtZW50X2JvbWJzCiAgICAgICAgaWYgc2VsZi5ib21iX3R5cGUgPT0gJ2ZpcmUnOgogICAgICAgICAgICByZXR1cm4gZmFjdG9yeS50ZXhfZmlyZV9ib21icwogICAgICAgIGlmIHNlbGYuYm9tYl90eXBlID09ICdmbHknOgogICAgICAgICAgICByZXR1cm4gZmFjdG9yeS50ZXhfZmx5X2JvbWJzCiAgICAgICAgcmV0dXJuIGZhY3RvcnkudGV4X2ltcGFjdF9ib21icwogICAgICAgICMgcmFpc2UgVmFsdWVFcnJvcignaW52YWxpZCBib21iIHR5cGUnKQoKZGVmIG5ld19oYW5kbGVtZXNzYWdlKHNlbGYsIG1zZzogQW55KSAtPiBBbnk6CiAgICBhc3NlcnQgbm90IHNlbGYuZXhwaXJlZAogICAgCiAgICBpZiBpc2luc3RhbmNlKG1zZywgYmEuUGlja2VkVXBNZXNzYWdlKToKICAgICAgICBpZiBzZWxmLm5vZGU6CiAgICAgICAgICAgIHNlbGYubm9kZS5oYW5kbGVtZXNzYWdlKCdodXJ0X3NvdW5kJykKICAgICAgICAgICAgc2VsZi5ub2RlLmhhbmRsZW1lc3NhZ2UoJ3BpY2tlZF91cCcpCgogICAgICAgIHNlbGYuX251bV90aW1lc19oaXQgKz0gMQoKICAgIGVsaWYgaXNpbnN0YW5jZShtc2csIGJhLlNob3VsZFNoYXR0ZXJNZXNzYWdlKToKICAgICAgICBiYS50aW1lcigwLjAwMSwgYmEuQ2FsbChzZWxmLnNoYXR0ZXIpKQoKICAgIGVsaWYgaXNpbnN0YW5jZShtc2csIGJhLkltcGFjdERhbWFnZU1lc3NhZ2UpOgogICAgICAgIGJhLnRpbWVyKDAuMDAxLCBiYS5DYWxsKHNlbGYuX2hpdF9zZWxmLCBtc2cuaW50ZW5zaXR5KSkKCiAgICBlbGlmIGlzaW5zdGFuY2UobXNnLCBiYS5Qb3dlcnVwTWVzc2FnZSk6CiAgICAgICAgZmFjdG9yeSA9IE5ld1Bvd2VydXBCb3hGYWN0b3J5LmdldCgpCiAgICAgICAgaWYgc2VsZi5fZGVhZCBvciBub3Qgc2VsZi5ub2RlOgogICAgICAgICAgICByZXR1cm4gVHJ1ZQogICAgICAgIGlmIHNlbGYucGlja191cF9wb3dlcnVwX2NhbGxiYWNrIGlzIG5vdCBOb25lOgogICAgICAgICAgICBzZWxmLnBpY2tfdXBfcG93ZXJ1cF9jYWxsYmFjayhzZWxmKQogICAgICAgIGlmIG1zZy5wb3dlcnVwdHlwZSA9PSAndHJpcGxlX2JvbWJzJzoKICAgICAgICAgICAgdGV4ID0gUG93ZXJ1cEJveEZhY3RvcnkuZ2V0KCkudGV4X2JvbWIKICAgICAgICAgICAgc2VsZi5fZmxhc2hfYmlsbGJvYXJkKHRleCkKICAgICAgICAgICAgc2VsZi5zZXRfYm9tYl9jb3VudCgzKQogICAgICAgICAgICBpZiBzZWxmLnBvd2VydXBzX2V4cGlyZToKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5taW5pX2JpbGxib2FyZF8xX3RleHR1cmUgPSB0ZXgKICAgICAgICAgICAgICAgIHRfbXMgPSBiYS50aW1lKHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpCiAgICAgICAgICAgICAgICBhc3NlcnQgaXNpbnN0YW5jZSh0X21zLCBpbnQpCiAgICAgICAgICAgICAgICBzZWxmLm5vZGUubWluaV9iaWxsYm9hcmRfMV9zdGFydF90aW1lID0gdF9tcwogICAgICAgICAgICAgICAgc2VsZi5ub2RlLm1pbmlfYmlsbGJvYXJkXzFfZW5kX3RpbWUgPSAoCiAgICAgICAgICAgICAgICAgICAgdF9tcyArIFBPV0VSVVBfV0VBUl9PRkZfVElNRSkKICAgICAgICAgICAgICAgIHNlbGYuX211bHRpX2JvbWJfd2Vhcl9vZmZfdGltZXIgPSAoYmEuVGltZXIoCiAgICAgICAgICAgICAgICAgICAgKFBPV0VSVVBfV0VBUl9PRkZfVElNRSAtIDIwMDApLAogICAgICAgICAgICAgICAgICAgIGJhLkNhbGwoc2VsZi5fbXVsdGlfYm9tYl93ZWFyX29mZl9mbGFzaCksCiAgICAgICAgICAgICAgICAgICAgdGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykpCiAgICAgICAgICAgICAgICBzZWxmLl9tdWx0aV9ib21iX3dlYXJfb2ZmX3RpbWVyID0gKGJhLlRpbWVyKAogICAgICAgICAgICAgICAgICAgIFBPV0VSVVBfV0VBUl9PRkZfVElNRSwKICAgICAgICAgICAgICAgICAgICBiYS5DYWxsKHNlbGYuX211bHRpX2JvbWJfd2Vhcl9vZmYpLAogICAgICAgICAgICAgICAgICAgIHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpKQogICAgICAgIGVsaWYgbXNnLnBvd2VydXB0eXBlID09ICdsYW5kX21pbmVzJzoKICAgICAgICAgICAgc2VsZi5zZXRfbGFuZF9taW5lX2NvdW50KG1pbihzZWxmLmxhbmRfbWluZV9jb3VudCArIDMsIDMpKQogICAgICAgIGVsaWYgbXNnLnBvd2VydXB0eXBlID09ICdpbXBhY3RfYm9tYnMnOgogICAgICAgICAgICBzZWxmLmJvbWJfdHlwZSA9ICdpbXBhY3QnCiAgICAgICAgICAgIHRleCA9IHNlbGYuX2dldF9ib21iX3R5cGVfdGV4KCkKICAgICAgICAgICAgc2VsZi5fZmxhc2hfYmlsbGJvYXJkKHRleCkKICAgICAgICAgICAgaWYgc2VsZi5wb3dlcnVwc19leHBpcmU6CiAgICAgICAgICAgICAgICBzZWxmLm5vZGUubWluaV9iaWxsYm9hcmRfMl90ZXh0dXJlID0gdGV4CiAgICAgICAgICAgICAgICB0X21zID0gYmEudGltZSh0aW1lZm9ybWF0PWJhLlRpbWVGb3JtYXQuTUlMTElTRUNPTkRTKQogICAgICAgICAgICAgICAgYXNzZXJ0IGlzaW5zdGFuY2UodF9tcywgaW50KQogICAgICAgICAgICAgICAgc2VsZi5ub2RlLm1pbmlfYmlsbGJvYXJkXzJfc3RhcnRfdGltZSA9IHRfbXMKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5taW5pX2JpbGxib2FyZF8yX2VuZF90aW1lID0gKAogICAgICAgICAgICAgICAgICAgIHRfbXMgKyBQT1dFUlVQX1dFQVJfT0ZGX1RJTUUpCiAgICAgICAgICAgICAgICBzZWxmLl9ib21iX3dlYXJfb2ZmX2ZsYXNoX3RpbWVyID0gKGJhLlRpbWVyKAogICAgICAgICAgICAgICAgICAgIFBPV0VSVVBfV0VBUl9PRkZfVElNRSAtIDIwMDAsCiAgICAgICAgICAgICAgICAgICAgYmEuQ2FsbChzZWxmLl9ib21iX3dlYXJfb2ZmX2ZsYXNoKSwKICAgICAgICAgICAgICAgICAgICB0aW1lZm9ybWF0PWJhLlRpbWVGb3JtYXQuTUlMTElTRUNPTkRTKSkKICAgICAgICAgICAgICAgIHNlbGYuX2JvbWJfd2Vhcl9vZmZfdGltZXIgPSAoYmEuVGltZXIoCiAgICAgICAgICAgICAgICAgICAgUE9XRVJVUF9XRUFSX09GRl9USU1FLAogICAgICAgICAgICAgICAgICAgIGJhLkNhbGwoc2VsZi5fYm9tYl93ZWFyX29mZiksCiAgICAgICAgICAgICAgICAgICAgdGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykpCiAgICAgICAgZWxpZiBtc2cucG93ZXJ1cHR5cGUgPT0gJ3N0aWNreV9ib21icyc6CiAgICAgICAgICAgIHNlbGYuYm9tYl90eXBlID0gJ3N0aWNreScKICAgICAgICAgICAgdGV4ID0gc2VsZi5fZ2V0X2JvbWJfdHlwZV90ZXgoKQogICAgICAgICAgICBzZWxmLl9mbGFzaF9iaWxsYm9hcmQodGV4KQogICAgICAgICAgICBpZiBzZWxmLnBvd2VydXBzX2V4cGlyZToKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5taW5pX2JpbGxib2FyZF8yX3RleHR1cmUgPSB0ZXgKICAgICAgICAgICAgICAgIHRfbXMgPSBiYS50aW1lKHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpCiAgICAgICAgICAgICAgICBhc3NlcnQgaXNpbnN0YW5jZSh0X21zLCBpbnQpCiAgICAgICAgICAgICAgICBzZWxmLm5vZGUubWluaV9iaWxsYm9hcmRfMl9zdGFydF90aW1lID0gdF9tcwogICAgICAgICAgICAgICAgc2VsZi5ub2RlLm1pbmlfYmlsbGJvYXJkXzJfZW5kX3RpbWUgPSAoCiAgICAgICAgICAgICAgICAgICAgdF9tcyArIFBPV0VSVVBfV0VBUl9PRkZfVElNRSkKICAgICAgICAgICAgICAgIHNlbGYuX2JvbWJfd2Vhcl9vZmZfZmxhc2hfdGltZXIgPSAoYmEuVGltZXIoCiAgICAgICAgICAgICAgICAgICAgUE9XRVJVUF9XRUFSX09GRl9USU1FIC0gMjAwMCwKICAgICAgICAgICAgICAgICAgICBiYS5DYWxsKHNlbGYuX2JvbWJfd2Vhcl9vZmZfZmxhc2gpLAogICAgICAgICAgICAgICAgICAgIHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpKQogICAgICAgICAgICAgICAgc2VsZi5fYm9tYl93ZWFyX29mZl90aW1lciA9IChiYS5UaW1lcigKICAgICAgICAgICAgICAgICAgICBQT1dFUlVQX1dFQVJfT0ZGX1RJTUUsCiAgICAgICAgICAgICAgICAgICAgYmEuQ2FsbChzZWxmLl9ib21iX3dlYXJfb2ZmKSwKICAgICAgICAgICAgICAgICAgICB0aW1lZm9ybWF0PWJhLlRpbWVGb3JtYXQuTUlMTElTRUNPTkRTKSkKICAgICAgICBlbGlmIG1zZy5wb3dlcnVwdHlwZSA9PSAncHVuY2gnOgogICAgICAgICAgICBzZWxmLl9oYXNfYm94aW5nX2dsb3ZlcyA9IFRydWUKICAgICAgICAgICAgdGV4ID0gUG93ZXJ1cEJveEZhY3RvcnkuZ2V0KCkudGV4X3B1bmNoCiAgICAgICAgICAgIHNlbGYuX2ZsYXNoX2JpbGxib2FyZCh0ZXgpCiAgICAgICAgICAgIHNlbGYuZXF1aXBfYm94aW5nX2dsb3ZlcygpCiAgICAgICAgICAgIGlmIHNlbGYucG93ZXJ1cHNfZXhwaXJlOgogICAgICAgICAgICAgICAgc2VsZi5ub2RlLmJveGluZ19nbG92ZXNfZmxhc2hpbmcgPSBGYWxzZQogICAgICAgICAgICAgICAgc2VsZi5ub2RlLm1pbmlfYmlsbGJvYXJkXzNfdGV4dHVyZSA9IHRleAogICAgICAgICAgICAgICAgdF9tcyA9IGJhLnRpbWUodGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykKICAgICAgICAgICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHRfbXMsIGludCkKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5taW5pX2JpbGxib2FyZF8zX3N0YXJ0X3RpbWUgPSB0X21zCiAgICAgICAgICAgICAgICBzZWxmLm5vZGUubWluaV9iaWxsYm9hcmRfM19lbmRfdGltZSA9ICgKICAgICAgICAgICAgICAgICAgICB0X21zICsgUE9XRVJVUF9XRUFSX09GRl9USU1FKQogICAgICAgICAgICAgICAgc2VsZi5fYm94aW5nX2dsb3Zlc193ZWFyX29mZl9mbGFzaF90aW1lciA9IChiYS5UaW1lcigKICAgICAgICAgICAgICAgICAgICBQT1dFUlVQX1dFQVJfT0ZGX1RJTUUgLSAyMDAwLAogICAgICAgICAgICAgICAgICAgIGJhLldlYWtDYWxsKHNlbGYuX2dsb3Zlc193ZWFyX29mZl9mbGFzaCksCiAgICAgICAgICAgICAgICAgICAgdGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykpCiAgICAgICAgICAgICAgICBzZWxmLl9ib3hpbmdfZ2xvdmVzX3dlYXJfb2ZmX3RpbWVyID0gKGJhLlRpbWVyKAogICAgICAgICAgICAgICAgICAgIFBPV0VSVVBfV0VBUl9PRkZfVElNRSwKICAgICAgICAgICAgICAgICAgICBiYS5XZWFrQ2FsbChzZWxmLl9nbG92ZXNfd2Vhcl9vZmYpLAogICAgICAgICAgICAgICAgICAgIHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpKQogICAgICAgIGVsaWYgbXNnLnBvd2VydXB0eXBlID09ICdzaGllbGQnOgogICAgICAgICAgICBmYWN0b3J5ID0gU3BhekZhY3RvcnkuZ2V0KCkKICAgICAgICAgICAgc2VsZi5lcXVpcF9zaGllbGRzKGRlY2F5PWZhY3Rvcnkuc2hpZWxkX2RlY2F5X3JhdGUgPiAwKQogICAgICAgIGVsaWYgbXNnLnBvd2VydXB0eXBlID09ICdjdXJzZSc6CiAgICAgICAgICAgIHNlbGYuY3Vyc2UoKQogICAgICAgIGVsaWYgbXNnLnBvd2VydXB0eXBlID09ICdpY2VfYm9tYnMnOgogICAgICAgICAgICBzZWxmLmJvbWJfdHlwZSA9ICdpY2UnCiAgICAgICAgICAgIHRleCA9IHNlbGYuX2dldF9ib21iX3R5cGVfdGV4KCkKICAgICAgICAgICAgc2VsZi5fZmxhc2hfYmlsbGJvYXJkKHRleCkKICAgICAgICAgICAgaWYgc2VsZi5wb3dlcnVwc19leHBpcmU6CiAgICAgICAgICAgICAgICBzZWxmLm5vZGUubWluaV9iaWxsYm9hcmRfMl90ZXh0dXJlID0gdGV4CiAgICAgICAgICAgICAgICB0X21zID0gYmEudGltZSh0aW1lZm9ybWF0PWJhLlRpbWVGb3JtYXQuTUlMTElTRUNPTkRTKQogICAgICAgICAgICAgICAgYXNzZXJ0IGlzaW5zdGFuY2UodF9tcywgaW50KQogICAgICAgICAgICAgICAgc2VsZi5ub2RlLm1pbmlfYmlsbGJvYXJkXzJfc3RhcnRfdGltZSA9IHRfbXMKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5taW5pX2JpbGxib2FyZF8yX2VuZF90aW1lID0gKAogICAgICAgICAgICAgICAgICAgIHRfbXMgKyBQT1dFUlVQX1dFQVJfT0ZGX1RJTUUpCiAgICAgICAgICAgICAgICBzZWxmLl9ib21iX3dlYXJfb2ZmX2ZsYXNoX3RpbWVyID0gKGJhLlRpbWVyKAogICAgICAgICAgICAgICAgICAgIFBPV0VSVVBfV0VBUl9PRkZfVElNRSAtIDIwMDAsCiAgICAgICAgICAgICAgICAgICAgYmEuV2Vha0NhbGwoc2VsZi5fYm9tYl93ZWFyX29mZl9mbGFzaCksCiAgICAgICAgICAgICAgICAgICAgdGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykpCiAgICAgICAgICAgICAgICBzZWxmLl9ib21iX3dlYXJfb2ZmX3RpbWVyID0gKGJhLlRpbWVyKAogICAgICAgICAgICAgICAgICAgIFBPV0VSVVBfV0VBUl9PRkZfVElNRSwKICAgICAgICAgICAgICAgICAgICBiYS5XZWFrQ2FsbChzZWxmLl9ib21iX3dlYXJfb2ZmKSwKICAgICAgICAgICAgICAgICAgICB0aW1lZm9ybWF0PWJhLlRpbWVGb3JtYXQuTUlMTElTRUNPTkRTKSkKICAgICAgICBlbGlmIG1zZy5wb3dlcnVwdHlwZSA9PSAnaGVhbHRoJzoKICAgICAgICAgICAgaWYgc2VsZi5lZGdfZWZmOgogICAgICAgICAgICAgICAgZiA9IHNlbGYuY29sb3JbMF0KICAgICAgICAgICAgICAgIHIgPSAoMiwwLDApCiAgICAgICAgICAgICAgICBnID0gKDAsMiwwKQogICAgICAgICAgICAgICAgYmEuYW5pbWF0ZV9hcnJheShzZWxmLm5vZGUsJ2NvbG9yJywzLHswOiByLCAwLjY6IGcsIDEuMDogZn0pCiAgICAgICAgICAgICAgICBzZWxmLmVkZ19lZmYgPSBGYWxzZQogICAgICAgICAgICBpZiBzZWxmLl9jdXJzZWQ6CiAgICAgICAgICAgICAgICBzZWxmLl9jdXJzZWQgPSBGYWxzZQogICAgICAgICAgICAgICAgZmFjdG9yeSA9IFNwYXpGYWN0b3J5LmdldCgpCiAgICAgICAgICAgICAgICBmb3IgYXR0ciBpbiBbJ21hdGVyaWFscycsICdyb2xsZXJfbWF0ZXJpYWxzJ106CiAgICAgICAgICAgICAgICAgICAgbWF0ZXJpYWxzID0gZ2V0YXR0cihzZWxmLm5vZGUsIGF0dHIpCiAgICAgICAgICAgICAgICAgICAgaWYgZmFjdG9yeS5jdXJzZV9tYXRlcmlhbCBpbiBtYXRlcmlhbHM6CiAgICAgICAgICAgICAgICAgICAgICAgIHNldGF0dHIoCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZWxmLm5vZGUsIGF0dHIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB0dXBsZShtIGZvciBtIGluIG1hdGVyaWFscwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaWYgbSAhPSBmYWN0b3J5LmN1cnNlX21hdGVyaWFsKSkKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5jdXJzZV9kZWF0aF90aW1lID0gMAogICAgICAgICAgICBzZWxmLmhpdHBvaW50cyA9IHNlbGYuaGl0cG9pbnRzX21heAogICAgICAgICAgICBzZWxmLl9mbGFzaF9iaWxsYm9hcmQoUG93ZXJ1cEJveEZhY3RvcnkuZ2V0KCkudGV4X2hlYWx0aCkKICAgICAgICAgICAgc2VsZi5ub2RlLmh1cnQgPSAwCiAgICAgICAgICAgIHNlbGYuX2xhc3RfaGl0X3RpbWUgPSBOb25lCiAgICAgICAgICAgIHNlbGYuX251bV90aW1lc19oaXQgPSAwCgogICAgICAgIGVsaWYgbXNnLnBvd2VydXB0eXBlID09ICd0YW5rX3NoaWVsZCc6CiAgICAgICAgICAgIHNlbGYudGFua3NoaWVsZFsnVGFuayddID0gVHJ1ZQogICAgICAgICAgICBzZWxmLmVkZ19lZmYgPSBGYWxzZQogICAgICAgICAgICB0ZXggPSBmYWN0b3J5LnRleF90YW5rX3NoaWVsZAogICAgICAgICAgICBzZWxmLl9mbGFzaF9iaWxsYm9hcmQodGV4KQoKICAgICAgICBlbGlmIG1zZy5wb3dlcnVwdHlwZSA9PSAnaGVhbHRoX2RhbWFnZSc6CiAgICAgICAgICAgIHRleCA9IGZhY3RvcnkudGV4X2hlYWx0aF9kYW1hZ2UKICAgICAgICAgICAgc2VsZi5lZGdfZWZmID0gVHJ1ZQogICAgICAgICAgICBmID0gc2VsZi5jb2xvclswXQogICAgICAgICAgICBpID0gKDIsMC41LDIpCiAgICAgICAgICAgIGJhLmFuaW1hdGVfYXJyYXkoc2VsZi5ub2RlLCdjb2xvcicsMyx7MDogaSwgMC41OiBpLCAwLjY6IGZ9KQogICAgICAgICAgICBzZWxmLl9mbGFzaF9iaWxsYm9hcmQodGV4KQogICAgICAgICAgICBzZWxmLnRhbmtzaGllbGRbJ1RhbmsnXSA9IEZhbHNlCiAgICAgICAgICAgIHNlbGYuZnJlZXplX3B1bmNoID0gRmFsc2UKCiAgICAgICAgZWxpZiBtc2cucG93ZXJ1cHR5cGUgPT0gJ2dvb2RieWUnOgogICAgICAgICAgICB0ZXggPSBmYWN0b3J5LnRleF9nb29kYnllCiAgICAgICAgICAgIHNlbGYuX2ZsYXNoX2JpbGxib2FyZCh0ZXgpCiAgICAgICAgICAgIHNlbGYua2lsbF9lZmYgPSBUcnVlCgogICAgICAgIGVsaWYgbXNnLnBvd2VydXB0eXBlID09ICdmbHlfYm9tYnMnOgogICAgICAgICAgICBzZWxmLmJvbWJfdHlwZSA9ICdmbHknCiAgICAgICAgICAgIHRleCA9IHNlbGYuX2dldF9ib21iX3R5cGVfdGV4KCkKICAgICAgICAgICAgc2VsZi5fZmxhc2hfYmlsbGJvYXJkKHRleCkKICAgICAgICAgICAgaWYgc2VsZi5wb3dlcnVwc19leHBpcmU6CiAgICAgICAgICAgICAgICBzZWxmLm5vZGUubWluaV9iaWxsYm9hcmRfMl90ZXh0dXJlID0gdGV4CiAgICAgICAgICAgICAgICB0X21zID0gYmEudGltZSh0aW1lZm9ybWF0PWJhLlRpbWVGb3JtYXQuTUlMTElTRUNPTkRTKQogICAgICAgICAgICAgICAgYXNzZXJ0IGlzaW5zdGFuY2UodF9tcywgaW50KQogICAgICAgICAgICAgICAgc2VsZi5ub2RlLm1pbmlfYmlsbGJvYXJkXzJfc3RhcnRfdGltZSA9IHRfbXMKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5taW5pX2JpbGxib2FyZF8yX2VuZF90aW1lID0gKAogICAgICAgICAgICAgICAgICAgIHRfbXMgKyBQT1dFUlVQX1dFQVJfT0ZGX1RJTUUpCiAgICAgICAgICAgICAgICBzZWxmLl9ib21iX3dlYXJfb2ZmX2ZsYXNoX3RpbWVyID0gKGJhLlRpbWVyKAogICAgICAgICAgICAgICAgICAgIFBPV0VSVVBfV0VBUl9PRkZfVElNRSAtIDIwMDAsCiAgICAgICAgICAgICAgICAgICAgYmEuV2Vha0NhbGwoc2VsZi5fYm9tYl93ZWFyX29mZl9mbGFzaCksCiAgICAgICAgICAgICAgICAgICAgdGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykpCiAgICAgICAgICAgICAgICBzZWxmLl9ib21iX3dlYXJfb2ZmX3RpbWVyID0gKGJhLlRpbWVyKAogICAgICAgICAgICAgICAgICAgIFBPV0VSVVBfV0VBUl9PRkZfVElNRSwKICAgICAgICAgICAgICAgICAgICBiYS5XZWFrQ2FsbChzZWxmLl9ib21iX3dlYXJfb2ZmKSwKICAgICAgICAgICAgICAgICAgICB0aW1lZm9ybWF0PWJhLlRpbWVGb3JtYXQuTUlMTElTRUNPTkRTKSkKCiAgICAgICAgZWxpZiBtc2cucG93ZXJ1cHR5cGUgPT0gJ2ZpcmVfYm9tYnMnOgogICAgICAgICAgICBzZWxmLmJvbWJfdHlwZSA9ICdmaXJlJwogICAgICAgICAgICB0ZXggPSBzZWxmLl9nZXRfYm9tYl90eXBlX3RleCgpCiAgICAgICAgICAgIHNlbGYuX2ZsYXNoX2JpbGxib2FyZCh0ZXgpCiAgICAgICAgICAgIGlmIHNlbGYucG93ZXJ1cHNfZXhwaXJlOgogICAgICAgICAgICAgICAgc2VsZi5ub2RlLm1pbmlfYmlsbGJvYXJkXzJfdGV4dHVyZSA9IHRleAogICAgICAgICAgICAgICAgdF9tcyA9IGJhLnRpbWUodGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykKICAgICAgICAgICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHRfbXMsIGludCkKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5taW5pX2JpbGxib2FyZF8yX3N0YXJ0X3RpbWUgPSB0X21zCiAgICAgICAgICAgICAgICBzZWxmLm5vZGUubWluaV9iaWxsYm9hcmRfMl9lbmRfdGltZSA9ICgKICAgICAgICAgICAgICAgICAgICB0X21zICsgUE9XRVJVUF9XRUFSX09GRl9USU1FKQogICAgICAgICAgICAgICAgc2VsZi5fYm9tYl93ZWFyX29mZl9mbGFzaF90aW1lciA9IChiYS5UaW1lcigKICAgICAgICAgICAgICAgICAgICBQT1dFUlVQX1dFQVJfT0ZGX1RJTUUgLSAyMDAwLAogICAgICAgICAgICAgICAgICAgIGJhLldlYWtDYWxsKHNlbGYuX2JvbWJfd2Vhcl9vZmZfZmxhc2gpLAogICAgICAgICAgICAgICAgICAgIHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpKQogICAgICAgICAgICAgICAgc2VsZi5fYm9tYl93ZWFyX29mZl90aW1lciA9IChiYS5UaW1lcigKICAgICAgICAgICAgICAgICAgICBQT1dFUlVQX1dFQVJfT0ZGX1RJTUUsCiAgICAgICAgICAgICAgICAgICAgYmEuV2Vha0NhbGwoc2VsZi5fYm9tYl93ZWFyX29mZiksCiAgICAgICAgICAgICAgICAgICAgdGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykpCgogICAgICAgIGVsaWYgbXNnLnBvd2VydXB0eXBlID09ICdpbXBhaXJtZW50X2JvbWJzJzoKICAgICAgICAgICAgc2VsZi5ib21iX3R5cGUgPSAnaW1wYWlybWVudCcKICAgICAgICAgICAgdGV4ID0gc2VsZi5fZ2V0X2JvbWJfdHlwZV90ZXgoKQogICAgICAgICAgICBzZWxmLl9mbGFzaF9iaWxsYm9hcmQodGV4KQogICAgICAgICAgICBpZiBzZWxmLnBvd2VydXBzX2V4cGlyZToKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5taW5pX2JpbGxib2FyZF8yX3RleHR1cmUgPSB0ZXgKICAgICAgICAgICAgICAgIHRfbXMgPSBiYS50aW1lKHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpCiAgICAgICAgICAgICAgICBhc3NlcnQgaXNpbnN0YW5jZSh0X21zLCBpbnQpCiAgICAgICAgICAgICAgICBzZWxmLm5vZGUubWluaV9iaWxsYm9hcmRfMl9zdGFydF90aW1lID0gdF9tcwogICAgICAgICAgICAgICAgc2VsZi5ub2RlLm1pbmlfYmlsbGJvYXJkXzJfZW5kX3RpbWUgPSAoCiAgICAgICAgICAgICAgICAgICAgdF9tcyArIFBPV0VSVVBfV0VBUl9PRkZfVElNRSkKICAgICAgICAgICAgICAgIHNlbGYuX2JvbWJfd2Vhcl9vZmZfZmxhc2hfdGltZXIgPSAoYmEuVGltZXIoCiAgICAgICAgICAgICAgICAgICAgUE9XRVJVUF9XRUFSX09GRl9USU1FIC0gMjAwMCwKICAgICAgICAgICAgICAgICAgICBiYS5XZWFrQ2FsbChzZWxmLl9ib21iX3dlYXJfb2ZmX2ZsYXNoKSwKICAgICAgICAgICAgICAgICAgICB0aW1lZm9ybWF0PWJhLlRpbWVGb3JtYXQuTUlMTElTRUNPTkRTKSkKICAgICAgICAgICAgICAgIHNlbGYuX2JvbWJfd2Vhcl9vZmZfdGltZXIgPSAoYmEuVGltZXIoCiAgICAgICAgICAgICAgICAgICAgUE9XRVJVUF9XRUFSX09GRl9USU1FLAogICAgICAgICAgICAgICAgICAgIGJhLldlYWtDYWxsKHNlbGYuX2JvbWJfd2Vhcl9vZmYpLAogICAgICAgICAgICAgICAgICAgIHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpKQoKICAgICAgICBlbGlmIG1zZy5wb3dlcnVwdHlwZSA9PSAnaWNlX21hbic6CiAgICAgICAgICAgIHRleCA9IGZhY3RvcnkudGV4X2ljZV9tYW4KICAgICAgICAgICAgc2VsZi5ib21iX3R5cGUgPSAnaWNlX2J1YmJsZScKICAgICAgICAgICAgc2VsZi5mcmVlemVfcHVuY2ggPSBUcnVlCiAgICAgICAgICAgIHNlbGYuZWRnX2VmZiA9IEZhbHNlCiAgICAgICAgICAgIHNlbGYubm9kZS5jb2xvciA9ICgwLDEsNCkKICAgICAgICAgICAgc2VsZi5fZmxhc2hfYmlsbGJvYXJkKHRleCkKICAgICAgICAgICAgCiAgICAgICAgICAgIGlmIHNlbGYucG93ZXJ1cHNfZXhwaXJlOgogICAgICAgICAgICAgICAgaWNlX21hbl90aW1lID0gMTcwMDAKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5taW5pX2JpbGxib2FyZF8yX3RleHR1cmUgPSB0ZXgKICAgICAgICAgICAgICAgIHRfbXMgPSBiYS50aW1lKHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpCiAgICAgICAgICAgICAgICBhc3NlcnQgaXNpbnN0YW5jZSh0X21zLCBpbnQpCiAgICAgICAgICAgICAgICBzZWxmLm5vZGUubWluaV9iaWxsYm9hcmRfMl9zdGFydF90aW1lID0gdF9tcwogICAgICAgICAgICAgICAgc2VsZi5ub2RlLm1pbmlfYmlsbGJvYXJkXzJfZW5kX3RpbWUgPSAodF9tcyArIGljZV9tYW5fdGltZSkKICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgc2VsZi5pY2VfbWFuX2ZsYXNoX3RpbWVyID0gKGJhLlRpbWVyKAogICAgICAgICAgICAgICAgICAgIGljZV9tYW5fdGltZSAtIDIwMDAsCiAgICAgICAgICAgICAgICAgICAgYmEuQ2FsbChfaWNlX21hbl9vZmZfZmxhc2gsc2VsZiksCiAgICAgICAgICAgICAgICAgICAgdGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykpCiAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBzZWxmLmljZV9tYW5fdGltZXIgPSAoYmEuVGltZXIoaWNlX21hbl90aW1lLAogICAgICAgICAgICAgICAgICAgIGJhLkNhbGwoX2ljZV9tYW5fd2Vhcl9vZmYsc2VsZiksCiAgICAgICAgICAgICAgICAgICAgdGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykpCgogICAgICAgIGVsaWYgbXNnLnBvd2VydXB0eXBlID09ICdzcGVlZCc6CiAgICAgICAgICAgIHNlbGYubm9kZS5ob2NrZXkgPSBUcnVlCiAgICAgICAgICAgIHRleCA9IGZhY3RvcnkudGV4X3NwZWVkCiAgICAgICAgICAgIHNlbGYuX2ZsYXNoX2JpbGxib2FyZCh0ZXgpCiAgICAgICAgICAgIGlmIHNlbGYucG93ZXJ1cHNfZXhwaXJlOgogICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBzcGVlZF90aW1lID0gMTUwMDAKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5taW5pX2JpbGxib2FyZF8yX3RleHR1cmUgPSB0ZXgKICAgICAgICAgICAgICAgIHRfbXMgPSBiYS50aW1lKHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpCiAgICAgICAgICAgICAgICBhc3NlcnQgaXNpbnN0YW5jZSh0X21zLCBpbnQpCiAgICAgICAgICAgICAgICBzZWxmLm5vZGUubWluaV9iaWxsYm9hcmRfMl9zdGFydF90aW1lID0gdF9tcwogICAgICAgICAgICAgICAgc2VsZi5ub2RlLm1pbmlfYmlsbGJvYXJkXzJfZW5kX3RpbWUgPSAodF9tcyArIHNwZWVkX3RpbWUpCiAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIHNlbGYuc3BlZWRfZmxhc2hfdGltZXIgPSAoYmEuVGltZXIoCiAgICAgICAgICAgICAgICAgICAgc3BlZWRfdGltZSAtIDIwMDAsCiAgICAgICAgICAgICAgICAgICAgYmEuQ2FsbChfc3BlZWRfb2ZmX2ZsYXNoLHNlbGYpLAogICAgICAgICAgICAgICAgICAgIHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpKQogICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgc2VsZi5zcGVlZF90aW1lciA9IChiYS5UaW1lcihzcGVlZF90aW1lLAogICAgICAgICAgICAgICAgICAgIGJhLkNhbGwoX3NwZWVkX3dlYXJfb2ZmLHNlbGYpLAogICAgICAgICAgICAgICAgICAgIHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpKQogICAgICAgIAogICAgICAgIHNlbGYuYm1iX2NvbG9yOiBsaXN0ID0gW10gICAgCiAgICAgICAgc2VsZi5ibWJfY29sb3IuYXBwZW5kKHNlbGYuYm9tYl90eXBlKQoKICAgICAgICBzZWxmLm5vZGUuaGFuZGxlbWVzc2FnZSgnZmxhc2gnKQogICAgICAgIGlmIG1zZy5zb3VyY2Vub2RlOgogICAgICAgICAgICBtc2cuc291cmNlbm9kZS5oYW5kbGVtZXNzYWdlKGJhLlBvd2VydXBBY2NlcHRNZXNzYWdlKCkpCiAgICAgICAgcmV0dXJuIFRydWUKCiAgICBlbGlmIGlzaW5zdGFuY2UobXNnLCBiYS5GcmVlemVNZXNzYWdlKToKICAgICAgICBpZiBub3Qgc2VsZi5ub2RlOgogICAgICAgICAgICByZXR1cm4gTm9uZQogICAgICAgIGlmIHNlbGYubm9kZS5pbnZpbmNpYmxlOgogICAgICAgICAgICBiYS5wbGF5c291bmQoU3BhekZhY3RvcnkuZ2V0KCkuYmxvY2tfc291bmQsCiAgICAgICAgICAgICAgICAgICAgICAgICAxLjAsCiAgICAgICAgICAgICAgICAgICAgICAgICBwb3NpdGlvbj1zZWxmLm5vZGUucG9zaXRpb24pCiAgICAgICAgICAgIHJldHVybiBOb25lCiAgICAgICAgaWYgc2VsZi5zaGllbGQ6CiAgICAgICAgICAgIHJldHVybiBOb25lCiAgICAgICAgaWYgbm90IHNlbGYuZnJvemVuOgogICAgICAgICAgICBzZWxmLmZyb3plbiA9IFRydWUKICAgICAgICAgICAgc2VsZi5ub2RlLmZyb3plbiA9IFRydWUKICAgICAgICAgICAgYmEudGltZXIoNS4wLCBiYS5DYWxsKHNlbGYuaGFuZGxlbWVzc2FnZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBiYS5UaGF3TWVzc2FnZSgpKSkKICAgICAgICAgICAgaWYgc2VsZi5oaXRwb2ludHMgPD0gMDoKICAgICAgICAgICAgICAgIHNlbGYuc2hhdHRlcigpCiAgICAgICAgaWYgc2VsZi5mcmVlemVfcHVuY2g6CiAgICAgICAgICAgIHNlbGYuaGFuZGxlbWVzc2FnZShiYS5UaGF3TWVzc2FnZSgpKQoKICAgIGVsaWYgaXNpbnN0YW5jZShtc2csIGJhLlRoYXdNZXNzYWdlKToKICAgICAgICBpZiBzZWxmLmZyb3plbiBhbmQgbm90IHNlbGYuc2hhdHRlcmVkIGFuZCBzZWxmLm5vZGU6CiAgICAgICAgICAgIHNlbGYuZnJvemVuID0gRmFsc2UKICAgICAgICAgICAgc2VsZi5ub2RlLmZyb3plbiA9IEZhbHNlCgogICAgZWxpZiBpc2luc3RhbmNlKG1zZywgYmEuSGl0TWVzc2FnZSk6CiAgICAgICAgaWYgbm90IHNlbGYubm9kZToKICAgICAgICAgICAgcmV0dXJuIE5vbmUKICAgICAgICBpZiBzZWxmLm5vZGUuaW52aW5jaWJsZToKICAgICAgICAgICAgYmEucGxheXNvdW5kKFNwYXpGYWN0b3J5LmdldCgpLmJsb2NrX3NvdW5kLAogICAgICAgICAgICAgICAgICAgICAgICAgMS4wLAogICAgICAgICAgICAgICAgICAgICAgICAgcG9zaXRpb249c2VsZi5ub2RlLnBvc2l0aW9uKQogICAgICAgICAgICByZXR1cm4gVHJ1ZQoKICAgICAgICBsb2NhbF90aW1lID0gYmEudGltZSh0aW1lZm9ybWF0PWJhLlRpbWVGb3JtYXQuTUlMTElTRUNPTkRTKQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKGxvY2FsX3RpbWUsIGludCkKICAgICAgICBpZiAoc2VsZi5fbGFzdF9oaXRfdGltZSBpcyBOb25lCiAgICAgICAgICAgICAgICBvciBsb2NhbF90aW1lIC0gc2VsZi5fbGFzdF9oaXRfdGltZSA+IDEwMDApOgogICAgICAgICAgICBzZWxmLl9udW1fdGltZXNfaGl0ICs9IDEKICAgICAgICAgICAgc2VsZi5fbGFzdF9oaXRfdGltZSA9IGxvY2FsX3RpbWUKCiAgICAgICAgbWFnID0gbXNnLm1hZ25pdHVkZSAqIHNlbGYuaW1wYWN0X3NjYWxlCiAgICAgICAgdmVsb2NpdHlfbWFnID0gbXNnLnZlbG9jaXR5X21hZ25pdHVkZSAqIHNlbGYuaW1wYWN0X3NjYWxlICAgICAgIAogICAgICAgIGRhbWFnZV9zY2FsZSA9IDAuMjIKICAgICAgICAKICAgICAgICBkZWYgZmlyZV9lZmZlY3QoKToKICAgICAgICAgICAgaWYgbm90IHNlbGYuc2hpZWxkOgogICAgICAgICAgICAgICAgaWYgc2VsZi5ub2RlLmV4aXN0cygpOgogICAgICAgICAgICAgICAgICAgIGJhLmVtaXRmeChwb3NpdGlvbj1zZWxmLm5vZGUucG9zaXRpb24sCiAgICAgICAgICAgICAgICAgICAgc2NhbGU9Myxjb3VudD01MCoyLHNwcmVhZD0wLjMsCiAgICAgICAgICAgICAgICAgICAgY2h1bmtfdHlwZT0nc3dlYXQnKQogICAgICAgICAgICAgICAgICAgIHNlbGYubm9kZS5oYW5kbGVtZXNzYWdlKCdjZWxlYnJhdGUnLCA1NjApCiAgICAgICAgICAgICAgICBlbHNlOiBzZWxmLl9maXJlX3RpbWUgPSBOb25lCiAgICAgICAgICAgIGVsc2U6IHNlbGYuX2ZpcmVfdGltZSA9IE5vbmUKICAgICAgICAKICAgICAgICBkZWYgZmlyZSh0aW1lLCBkYW1hZ2UpOgogICAgICAgICAgICBpZiBub3Qgc2VsZi5zaGllbGQgYW5kIG5vdCBzZWxmLl9kZWFkOgogICAgICAgICAgICAgICAgc2VsZi5oaXRwb2ludHMgLT0gZGFtYWdlCiAgICAgICAgICAgICAgICBiYS5zaG93X2RhbWFnZV9jb3VudChmJy17ZGFtYWdlfUhQJywKICAgICAgICAgICAgICAgICAgICBzZWxmLm5vZGUucG9zaXRpb24sIG1zZy5mb3JjZV9kaXJlY3Rpb24pCiAgICAgICAgICAgICAgICBiYS5wbGF5c291bmQoYmEuZ2V0c291bmQoJ2Z1c2UwMScpKQogICAgICAgICAgICAKICAgICAgICAgICAgaWYgZHVyYXRpb24gIT0gdGltZToKICAgICAgICAgICAgICAgIHNlbGYuX2ZpcmVfdGltZSA9IGJhLlRpbWVyKDAuMSxiYS5DYWxsKGZpcmVfZWZmZWN0KSxyZXBlYXQ9VHJ1ZSkKICAgICAgICAgICAgZWxzZTogc2VsZi5fZmlyZV90aW1lID0gTm9uZQogICAgICAgICAgICAKICAgICAgICAgICAgaWYgc2VsZi5oaXRwb2ludHMgPCAwOgogICAgICAgICAgICAgICAgc2VsZi5ub2RlLmhhbmRsZW1lc3NhZ2UoYmEuRGllTWVzc2FnZSgpKQogICAgICAgIAogICAgICAgIGlmIG1zZy5oaXRfc3VidHlwZSA9PSAnZmx5JzoKICAgICAgICAgICAgZGFtYWdlX3NjYWxlID0gMC4wCiAgICAgICAgICAgIAogICAgICAgICAgICBpZiBzZWxmLnNoaWVsZDoKICAgICAgICAgICAgICAgIHNlbGYuc2hpZWxkX2hpdHBvaW50cyAtPSAzMDAKICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgaWYgc2VsZi5zaGllbGRfaGl0cG9pbnRzIDwgMDoKICAgICAgICAgICAgICAgICAgICBzZWxmLnNoaWVsZC5kZWxldGUoKQogICAgICAgICAgICAgICAgICAgIHNlbGYuc2hpZWxkID0gTm9uZQogICAgICAgICAgICAgICAgICAgIGJhLnBsYXlzb3VuZChTcGF6RmFjdG9yeS5nZXQoKS5zaGllbGRfZG93bl9zb3VuZCwxLjAscG9zaXRpb249c2VsZi5ub2RlLnBvc2l0aW9uKQogICAgICAgIGVsaWYgbXNnLmhpdF9zdWJ0eXBlID09ICdmaXJlJzoKICAgICAgICAgICAgaW5kZXggPSAxCiAgICAgICAgICAgIGR1cmF0aW9uID0gNQogICAgICAgICAgICBkYW1hZ2UgPSAxMDMKICAgICAgICAgICAgaWYgbm90IHNlbGYuc2hpZWxkOgogICAgICAgICAgICAgICAgZm9yIGZpcmV4IGluIHJhbmdlKGR1cmF0aW9uKToKICAgICAgICAgICAgICAgICAgICBiYS50aW1lcihpbmRleCxiYS5DYWxsKGZpcmUsaW5kZXgsZGFtYWdlKSkKICAgICAgICAgICAgICAgICAgICBzZWxmLl9maXJlX3RpbWUgPSBiYS5UaW1lcigwLjEsYmEuQ2FsbChmaXJlX2VmZmVjdCkscmVwZWF0PVRydWUpCiAgICAgICAgICAgICAgICAgICAgaW5kZXggKz0gMQogICAgICAgICAgICBlbHNlOgogICAgICAgICAgICAgICAgc2VsZi5zaGllbGRfaGl0cG9pbnRzIC09IDgwCiAgICAgICAgICAgICAgICBpZiBzZWxmLnNoaWVsZF9oaXRwb2ludHMgPCAxOgogICAgICAgICAgICAgICAgICAgIHNlbGYuc2hpZWxkLmRlbGV0ZSgpCiAgICAgICAgICAgICAgICAgICAgc2VsZi5zaGllbGQgPSBOb25lCiAgICAgICAgICAgICAgICAgICAgYmEucGxheXNvdW5kKFNwYXpGYWN0b3J5LmdldCgpLnNoaWVsZF9kb3duX3NvdW5kLDEuMCxwb3NpdGlvbj1zZWxmLm5vZGUucG9zaXRpb24pCiAgICAgICAgZWxpZiBtc2cuaGl0X3N1YnR5cGUgPT0gJ2ltcGFpcm1lbnQnOgogICAgICAgICAgICBkYW1hZ2Vfc2NhbGUgPSAwCiAgICAgICAgICAgIAogICAgICAgICAgICBpZiBzZWxmLnNoaWVsZDoKICAgICAgICAgICAgICAgIHNlbGYuc2hpZWxkLmRlbGV0ZSgpCiAgICAgICAgICAgICAgICBzZWxmLnNoaWVsZCA9IE5vbmUKICAgICAgICAgICAgICAgIGJhLnBsYXlzb3VuZChTcGF6RmFjdG9yeS5nZXQoKS5zaGllbGRfZG93bl9zb3VuZCwxLjAscG9zaXRpb249c2VsZi5ub2RlLnBvc2l0aW9uKQogICAgICAgICAgICBlbHNlOgogICAgICAgICAgICAgICAgaGl0cG9pbnRzID0gaW50KHNlbGYuaGl0cG9pbnRzKjAuODApCiAgICAgICAgICAgICAgICBzZWxmLmhpdHBvaW50cyAtPSBpbnQoaGl0cG9pbnRzKQogICAgICAgICAgICAgICAgYmEuc2hvd19kYW1hZ2VfY291bnQoKGYnLXtpbnQoaGl0cG9pbnRzLzEwKX0lJyksCiAgICAgICAgICAgICAgICAgICAgc2VsZi5ub2RlLnBvc2l0aW9uLCBtc2cuZm9yY2VfZGlyZWN0aW9uKQogICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBpZiBzZWxmLmhpdHBvaW50cyA8IDAgb3IgaGl0cG9pbnRzIDwgOTU6CiAgICAgICAgICAgICAgICAgICAgc2VsZi5ub2RlLmhhbmRsZW1lc3NhZ2UoYmEuRGllTWVzc2FnZSgpKQoKICAgICAgICBpZiBzZWxmLnNoaWVsZDoKICAgICAgICAgICAgaWYgbXNnLmZsYXRfZGFtYWdlOgogICAgICAgICAgICAgICAgZGFtYWdlID0gbXNnLmZsYXRfZGFtYWdlICogc2VsZi5pbXBhY3Rfc2NhbGUKICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgIGFzc2VydCBtc2cuZm9yY2VfZGlyZWN0aW9uIGlzIG5vdCBOb25lCiAgICAgICAgICAgICAgICBzZWxmLm5vZGUuaGFuZGxlbWVzc2FnZSgKICAgICAgICAgICAgICAgICAgICAnaW1wdWxzZScsIG1zZy5wb3NbMF0sIG1zZy5wb3NbMV0sIG1zZy5wb3NbMl0sCiAgICAgICAgICAgICAgICAgICAgbXNnLnZlbG9jaXR5WzBdLCBtc2cudmVsb2NpdHlbMV0sIG1zZy52ZWxvY2l0eVsyXSwgbWFnLAogICAgICAgICAgICAgICAgICAgIHZlbG9jaXR5X21hZywgbXNnLnJhZGl1cywgMSwgbXNnLmZvcmNlX2RpcmVjdGlvblswXSwKICAgICAgICAgICAgICAgICAgICBtc2cuZm9yY2VfZGlyZWN0aW9uWzFdLCBtc2cuZm9yY2VfZGlyZWN0aW9uWzJdKQogICAgICAgICAgICAgICAgZGFtYWdlID0gZGFtYWdlX3NjYWxlICogc2VsZi5ub2RlLmRhbWFnZQoKICAgICAgICAgICAgYXNzZXJ0IHNlbGYuc2hpZWxkX2hpdHBvaW50cyBpcyBub3QgTm9uZQogICAgICAgICAgICBzZWxmLnNoaWVsZF9oaXRwb2ludHMgLT0gaW50KGRhbWFnZSkKICAgICAgICAgICAgc2VsZi5zaGllbGQuaHVydCA9ICgKICAgICAgICAgICAgICAgIDEuMCAtCiAgICAgICAgICAgICAgICBmbG9hdChzZWxmLnNoaWVsZF9oaXRwb2ludHMpIC8gc2VsZi5zaGllbGRfaGl0cG9pbnRzX21heCkKCiAgICAgICAgICAgIG1heF9zcGlsbG92ZXIgPSBTcGF6RmFjdG9yeS5nZXQoKS5tYXhfc2hpZWxkX3NwaWxsb3Zlcl9kYW1hZ2UKICAgICAgICAgICAgaWYgc2VsZi5zaGllbGRfaGl0cG9pbnRzIDw9IDA6CgogICAgICAgICAgICAgICAgc2VsZi5zaGllbGQuZGVsZXRlKCkKICAgICAgICAgICAgICAgIHNlbGYuc2hpZWxkID0gTm9uZQogICAgICAgICAgICAgICAgYmEucGxheXNvdW5kKFNwYXpGYWN0b3J5LmdldCgpLnNoaWVsZF9kb3duX3NvdW5kLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIDEuMCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwb3NpdGlvbj1zZWxmLm5vZGUucG9zaXRpb24pCgogICAgICAgICAgICAgICAgbnBvcyA9IHNlbGYubm9kZS5wb3NpdGlvbgogICAgICAgICAgICAgICAgYmEuZW1pdGZ4KHBvc2l0aW9uPShucG9zWzBdLCBucG9zWzFdICsgMC45LCBucG9zWzJdKSwKICAgICAgICAgICAgICAgICAgICAgICAgICB2ZWxvY2l0eT1zZWxmLm5vZGUudmVsb2NpdHksCiAgICAgICAgICAgICAgICAgICAgICAgICAgY291bnQ9cmFuZG9tLnJhbmRyYW5nZSgyMCwgMzApLAogICAgICAgICAgICAgICAgICAgICAgICAgIHNjYWxlPTEuMCwKICAgICAgICAgICAgICAgICAgICAgICAgICBzcHJlYWQ9MC42LAogICAgICAgICAgICAgICAgICAgICAgICAgIGNodW5rX3R5cGU9J3NwYXJrJykKCiAgICAgICAgICAgIGVsc2U6CiAgICAgICAgICAgICAgICBiYS5wbGF5c291bmQoU3BhekZhY3RvcnkuZ2V0KCkuc2hpZWxkX2hpdF9zb3VuZCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAwLjUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcG9zaXRpb249c2VsZi5ub2RlLnBvc2l0aW9uKQoKICAgICAgICAgICAgYXNzZXJ0IG1zZy5mb3JjZV9kaXJlY3Rpb24gaXMgbm90IE5vbmUKICAgICAgICAgICAgYmEuZW1pdGZ4KHBvc2l0aW9uPW1zZy5wb3MsCiAgICAgICAgICAgICAgICAgICAgICB2ZWxvY2l0eT0obXNnLmZvcmNlX2RpcmVjdGlvblswXSAqIDEuMCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtc2cuZm9yY2VfZGlyZWN0aW9uWzFdICogMS4wLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1zZy5mb3JjZV9kaXJlY3Rpb25bMl0gKiAxLjApLAogICAgICAgICAgICAgICAgICAgICAgY291bnQ9bWluKDMwLCA1ICsgaW50KGRhbWFnZSAqIDAuMDA1KSksCiAgICAgICAgICAgICAgICAgICAgICBzY2FsZT0wLjUsCiAgICAgICAgICAgICAgICAgICAgICBzcHJlYWQ9MC4zLAogICAgICAgICAgICAgICAgICAgICAgY2h1bmtfdHlwZT0nc3BhcmsnKQoKICAgICAgICAgICAgaWYgc2VsZi5zaGllbGRfaGl0cG9pbnRzIDw9IC1tYXhfc3BpbGxvdmVyOgogICAgICAgICAgICAgICAgbGVmdG92ZXJfZGFtYWdlID0gLW1heF9zcGlsbG92ZXIgLSBzZWxmLnNoaWVsZF9oaXRwb2ludHMKICAgICAgICAgICAgICAgIHNoaWVsZF9sZWZ0b3Zlcl9yYXRpbyA9IGxlZnRvdmVyX2RhbWFnZSAvIGRhbWFnZQoKICAgICAgICAgICAgICAgIG1hZyAqPSBzaGllbGRfbGVmdG92ZXJfcmF0aW8KICAgICAgICAgICAgICAgIHZlbG9jaXR5X21hZyAqPSBzaGllbGRfbGVmdG92ZXJfcmF0aW8KICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgIHJldHVybiBUcnVlCiAgICAgICAgZWxzZToKICAgICAgICAgICAgc2hpZWxkX2xlZnRvdmVyX3JhdGlvID0gMS4wCgogICAgICAgIGlmIG1zZy5mbGF0X2RhbWFnZToKICAgICAgICAgICAgZGFtYWdlID0gaW50KG1zZy5mbGF0X2RhbWFnZSAqIHNlbGYuaW1wYWN0X3NjYWxlICoKICAgICAgICAgICAgICAgICAgICAgICAgIHNoaWVsZF9sZWZ0b3Zlcl9yYXRpbykKICAgICAgICBlbHNlOgogICAgICAgICAgICBhc3NlcnQgbXNnLmZvcmNlX2RpcmVjdGlvbiBpcyBub3QgTm9uZQogICAgICAgICAgICBzZWxmLm5vZGUuaGFuZGxlbWVzc2FnZSgKICAgICAgICAgICAgICAgICdpbXB1bHNlJywgbXNnLnBvc1swXSwgbXNnLnBvc1sxXSwgbXNnLnBvc1syXSwKICAgICAgICAgICAgICAgIG1zZy52ZWxvY2l0eVswXSwgbXNnLnZlbG9jaXR5WzFdLCBtc2cudmVsb2NpdHlbMl0sIG1hZywKICAgICAgICAgICAgICAgIHZlbG9jaXR5X21hZywgbXNnLnJhZGl1cywgMCwgbXNnLmZvcmNlX2RpcmVjdGlvblswXSwKICAgICAgICAgICAgICAgIG1zZy5mb3JjZV9kaXJlY3Rpb25bMV0sIG1zZy5mb3JjZV9kaXJlY3Rpb25bMl0pCgogICAgICAgICAgICBkYW1hZ2UgPSBpbnQoZGFtYWdlX3NjYWxlICogc2VsZi5ub2RlLmRhbWFnZSkKICAgICAgICAgICAgCiAgICAgICAgaWYgc2VsZi50YW5rc2hpZWxkWydSZWR1Y3Rpb24nXToKICAgICAgICAgICAgcG9yY2VudGFqZSA9IHBlcmNlbnRhZ2VfdGFua19zaGllbGQoKQogICAgICAgICAgICBkaXNtID0gaW50KGRhbWFnZSpwb3JjZW50YWplKQogICAgICAgICAgICBkYW1hZ2UgPSBpbnQoZGFtYWdlLWRpc20pCiAgICAgICAgICAgIAogICAgICAgICAgICBiYS5zaG93X2RhbWFnZV9jb3VudCgnLScgKyBzdHIoaW50KGRhbWFnZSAvIDEwKSkgKyAnJScsCiAgICAgICAgICAgICAgICBtc2cucG9zLCBtc2cuZm9yY2VfZGlyZWN0aW9uKQoKICAgICAgICBzZWxmLm5vZGUuaGFuZGxlbWVzc2FnZSgnaHVydF9zb3VuZCcpCgogICAgICAgIGlmIHNlbGYuZWRnX2VmZjoKICAgICAgICAgICAgcG9yY2VudGFqZSA9IHBlcmNlbnRhZ2VfaGVhbHRoX2RhbWFnZSgpCiAgICAgICAgICAgIGRtZ19kaXNtID0gaW50KGRhbWFnZSpwb3JjZW50YWplKQogICAgICAgICAgICBzZWxmLmhpdHBvaW50cyArPSBkbWdfZGlzbQoKICAgICAgICAgICAgUG9wdXBUZXh0KHRleHQ9Zicre2ludChkbWdfZGlzbS8xMCl9JScsc2NhbGU9MS41LAogICAgICAgICAgICAgICAgICAgICBwb3NpdGlvbj1zZWxmLm5vZGUucG9zaXRpb24sY29sb3I9KDAsMSwwKSkuYXV0b3JldGFpbigpCiAgICAgICAgICAgIGJhLmFuaW1hdGVfYXJyYXkoc2VsZi5ub2RlLCdjb2xvcicsMyx7MDogKDAsMSwwKSwgMC4zOTogKDAsMiwwKSwgMC40OiBzZWxmLmNvbG9yWzBdfSkKICAgICAgICAgICAgYmEucGxheXNvdW5kKGJhLmdldHNvdW5kKCdoZWFsdGhQb3dlcnVwJykpCgogICAgICAgIGlmIG1zZy5oaXRfdHlwZSA9PSAncHVuY2gnOgogICAgICAgICAgICBzZWxmLm9uX3B1bmNoZWQoZGFtYWdlKQoKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgaWYgbXNnLmdldF9zb3VyY2VfcGxheWVyKGJhLlBsYXllcikuYWN0b3IuZnJlZXplX3B1bmNoOgogICAgICAgICAgICAgICAgICAgIHNlbGYubm9kZS5jb2xvciA9ICgwLDEsNCkKICAgICAgICAgICAgICAgICAgICBiYS5wbGF5c291bmQoYmEuZ2V0c291bmQoJ2ZyZWV6ZScpKQogICAgICAgICAgICAgICAgICAgIHNlbGYubm9kZS5oYW5kbGVtZXNzYWdlKGJhLkZyZWV6ZU1lc3NhZ2UoKSkKICAgICAgICAgICAgZXhjZXB0OiBwYXNzCgogICAgICAgICAgICBpZiBkYW1hZ2UgPiAzNTA6CiAgICAgICAgICAgICAgICBhc3NlcnQgbXNnLmZvcmNlX2RpcmVjdGlvbiBpcyBub3QgTm9uZQogICAgICAgICAgICAgICAgYmEuc2hvd19kYW1hZ2VfY291bnQoJy0nICsgc3RyKGludChkYW1hZ2UgLyAxMCkpICsgJyUnLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbXNnLnBvcywgbXNnLmZvcmNlX2RpcmVjdGlvbikKCiAgICAgICAgICAgIGlmIG1zZy5oaXRfc3VidHlwZSA9PSAnc3VwZXJfcHVuY2gnOgogICAgICAgICAgICAgICAgYmEucGxheXNvdW5kKFNwYXpGYWN0b3J5LmdldCgpLnB1bmNoX3NvdW5kX3N0cm9uZ2VyLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIDEuMCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwb3NpdGlvbj1zZWxmLm5vZGUucG9zaXRpb24pCiAgICAgICAgICAgIGlmIGRhbWFnZSA+IDUwMDoKICAgICAgICAgICAgICAgIHNvdW5kcyA9IFNwYXpGYWN0b3J5LmdldCgpLnB1bmNoX3NvdW5kX3N0cm9uZwogICAgICAgICAgICAgICAgc291bmQgPSBzb3VuZHNbcmFuZG9tLnJhbmRyYW5nZShsZW4oc291bmRzKSldCiAgICAgICAgICAgIGVsc2U6CiAgICAgICAgICAgICAgICBzb3VuZCA9IFNwYXpGYWN0b3J5LmdldCgpLnB1bmNoX3NvdW5kCiAgICAgICAgICAgIGJhLnBsYXlzb3VuZChzb3VuZCwgMS4wLCBwb3NpdGlvbj1zZWxmLm5vZGUucG9zaXRpb24pCgogICAgICAgICAgICBhc3NlcnQgbXNnLmZvcmNlX2RpcmVjdGlvbiBpcyBub3QgTm9uZQogICAgICAgICAgICBiYS5lbWl0ZngocG9zaXRpb249bXNnLnBvcywKICAgICAgICAgICAgICAgICAgICAgIHZlbG9jaXR5PShtc2cuZm9yY2VfZGlyZWN0aW9uWzBdICogMC41LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1zZy5mb3JjZV9kaXJlY3Rpb25bMV0gKiAwLjUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbXNnLmZvcmNlX2RpcmVjdGlvblsyXSAqIDAuNSksCiAgICAgICAgICAgICAgICAgICAgICBjb3VudD1taW4oMTAsIDEgKyBpbnQoZGFtYWdlICogMC4wMDI1KSksCiAgICAgICAgICAgICAgICAgICAgICBzY2FsZT0wLjMsCiAgICAgICAgICAgICAgICAgICAgICBzcHJlYWQ9MC4wMykKCiAgICAgICAgICAgIGJhLmVtaXRmeChwb3NpdGlvbj1tc2cucG9zLAogICAgICAgICAgICAgICAgICAgICAgY2h1bmtfdHlwZT0nc3dlYXQnLAogICAgICAgICAgICAgICAgICAgICAgdmVsb2NpdHk9KG1zZy5mb3JjZV9kaXJlY3Rpb25bMF0gKiAxLjMsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbXNnLmZvcmNlX2RpcmVjdGlvblsxXSAqIDEuMyArIDUuMCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtc2cuZm9yY2VfZGlyZWN0aW9uWzJdICogMS4zKSwKICAgICAgICAgICAgICAgICAgICAgIGNvdW50PW1pbigzMCwgMSArIGludChkYW1hZ2UgKiAwLjA0KSksCiAgICAgICAgICAgICAgICAgICAgICBzY2FsZT0wLjksCiAgICAgICAgICAgICAgICAgICAgICBzcHJlYWQ9MC4yOCkKCiAgICAgICAgICAgIGh1cnRpbmVzcyA9IGRhbWFnZSAqIDAuMDAzCiAgICAgICAgICAgIHB1bmNocG9zID0gKG1zZy5wb3NbMF0gKyBtc2cuZm9yY2VfZGlyZWN0aW9uWzBdICogMC4wMiwKICAgICAgICAgICAgICAgICAgICAgICAgbXNnLnBvc1sxXSArIG1zZy5mb3JjZV9kaXJlY3Rpb25bMV0gKiAwLjAyLAogICAgICAgICAgICAgICAgICAgICAgICBtc2cucG9zWzJdICsgbXNnLmZvcmNlX2RpcmVjdGlvblsyXSAqIDAuMDIpCiAgICAgICAgICAgIGZsYXNoX2NvbG9yID0gKDEuMCwgMC44LCAwLjQpCiAgICAgICAgICAgIGxpZ2h0ID0gYmEubmV3bm9kZSgKICAgICAgICAgICAgICAgICdsaWdodCcsCiAgICAgICAgICAgICAgICBhdHRycz17CiAgICAgICAgICAgICAgICAgICAgJ3Bvc2l0aW9uJzogcHVuY2hwb3MsCiAgICAgICAgICAgICAgICAgICAgJ3JhZGl1cyc6IDAuMTIgKyBodXJ0aW5lc3MgKiAwLjEyLAogICAgICAgICAgICAgICAgICAgICdpbnRlbnNpdHknOiAwLjMgKiAoMS4wICsgMS4wICogaHVydGluZXNzKSwKICAgICAgICAgICAgICAgICAgICAnaGVpZ2h0X2F0dGVudWF0ZWQnOiBGYWxzZSwKICAgICAgICAgICAgICAgICAgICAnY29sb3InOiBmbGFzaF9jb2xvcgogICAgICAgICAgICAgICAgfSkKICAgICAgICAgICAgYmEudGltZXIoMC4wNiwgbGlnaHQuZGVsZXRlKQoKICAgICAgICAgICAgZmxhc2ggPSBiYS5uZXdub2RlKCdmbGFzaCcsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBhdHRycz17CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ3Bvc2l0aW9uJzogcHVuY2hwb3MsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ3NpemUnOiAwLjE3ICsgMC4xNyAqIGh1cnRpbmVzcywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnY29sb3InOiBmbGFzaF9jb2xvcgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSkKICAgICAgICAgICAgYmEudGltZXIoMC4wNiwgZmxhc2guZGVsZXRlKQoKICAgICAgICBpZiBtc2cuaGl0X3R5cGUgPT0gJ2ltcGFjdCc6CiAgICAgICAgICAgIGFzc2VydCBtc2cuZm9yY2VfZGlyZWN0aW9uIGlzIG5vdCBOb25lCiAgICAgICAgICAgIGJhLmVtaXRmeChwb3NpdGlvbj1tc2cucG9zLAogICAgICAgICAgICAgICAgICAgICAgdmVsb2NpdHk9KG1zZy5mb3JjZV9kaXJlY3Rpb25bMF0gKiAyLjAsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbXNnLmZvcmNlX2RpcmVjdGlvblsxXSAqIDIuMCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtc2cuZm9yY2VfZGlyZWN0aW9uWzJdICogMi4wKSwKICAgICAgICAgICAgICAgICAgICAgIGNvdW50PW1pbigxMCwgMSArIGludChkYW1hZ2UgKiAwLjAxKSksCiAgICAgICAgICAgICAgICAgICAgICBzY2FsZT0wLjQsCiAgICAgICAgICAgICAgICAgICAgICBzcHJlYWQ9MC4xKQogICAgICAgIGlmIHNlbGYuaGl0cG9pbnRzID4gMDoKICAgICAgICAgICAgaWYgbXNnLmhpdF90eXBlID09ICdpbXBhY3QnIGFuZCBkYW1hZ2UgPiBzZWxmLmhpdHBvaW50czoKICAgICAgICAgICAgICAgIG5ld2RhbWFnZSA9IG1heChkYW1hZ2UgLSAyMDAsIHNlbGYuaGl0cG9pbnRzIC0gMTApCiAgICAgICAgICAgICAgICBkYW1hZ2UgPSBuZXdkYW1hZ2UKICAgICAgICAgICAgc2VsZi5ub2RlLmhhbmRsZW1lc3NhZ2UoJ2ZsYXNoJykKCiAgICAgICAgICAgIGlmIGRhbWFnZSA+IDAuMCBhbmQgc2VsZi5ub2RlLmhvbGRfbm9kZToKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5ob2xkX25vZGUgPSBOb25lCiAgICAgICAgICAgIHNlbGYuaGl0cG9pbnRzIC09IGRhbWFnZQogICAgICAgICAgICBzZWxmLm5vZGUuaHVydCA9IDEuMCAtIGZsb2F0KAogICAgICAgICAgICAgICAgc2VsZi5oaXRwb2ludHMpIC8gc2VsZi5oaXRwb2ludHNfbWF4CgogICAgICAgICAgICBpZiBzZWxmLl9jdXJzZWQgYW5kIGRhbWFnZSA+IDA6CiAgICAgICAgICAgICAgICBiYS50aW1lcigKICAgICAgICAgICAgICAgICAgICAwLjA1LAogICAgICAgICAgICAgICAgICAgIGJhLkNhbGwoc2VsZi5jdXJzZV9leHBsb2RlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1zZy5nZXRfc291cmNlX3BsYXllcihiYS5QbGF5ZXIpKSkKCiAgICAgICAgICAgIGlmIHNlbGYuZnJvemVuIGFuZCAoZGFtYWdlID4gMjAwIG9yIHNlbGYuaGl0cG9pbnRzIDw9IDApOgogICAgICAgICAgICAgICAgc2VsZi5zaGF0dGVyKCkKICAgICAgICAgICAgZWxpZiBzZWxmLmhpdHBvaW50cyA8PSAwOgogICAgICAgICAgICAgICAgc2VsZi5ub2RlLmhhbmRsZW1lc3NhZ2UoCiAgICAgICAgICAgICAgICAgICAgYmEuRGllTWVzc2FnZShob3c9YmEuRGVhdGhUeXBlLklNUEFDVCkpCgogICAgICAgIGlmIHNlbGYuaGl0cG9pbnRzIDw9IDA6CiAgICAgICAgICAgIGRhbWFnZV9hdmcgPSBzZWxmLm5vZGUuZGFtYWdlX3Ntb290aGVkICogZGFtYWdlX3NjYWxlCiAgICAgICAgICAgIGlmIGRhbWFnZV9hdmcgPiAxMDAwOgogICAgICAgICAgICAgICAgc2VsZi5zaGF0dGVyKCkKCiAgICBlbGlmIGlzaW5zdGFuY2UobXNnLCBCb21iRGllZE1lc3NhZ2UpOgogICAgICAgIHNlbGYuYm9tYl9jb3VudCArPSAxCgogICAgZWxpZiBpc2luc3RhbmNlKG1zZywgYmEuRGllTWVzc2FnZSk6CiAgICAgICAgZGVmIGRyb3BfYm9tYigpOgogICAgICAgICAgICBmb3IgeGJvbWIgaW4gcmFuZ2UoMyk6CiAgICAgICAgICAgICAgICBwID0gc2VsZi5ub2RlLnBvc2l0aW9uCiAgICAgICAgICAgICAgICBwb3MgPSAocFswXSt4Ym9tYixwWzFdKzUscFsyXS14Ym9tYikKICAgICAgICAgICAgICAgIGJhbGwgPSBib21iLkJvbWIocG9zaXRpb249cG9zLGJvbWJfdHlwZT0naW1wYWN0JykuYXV0b3JldGFpbigpCiAgICAgICAgICAgICAgICBiYWxsLm5vZGUubW9kZWxfc2NhbGUgPSAwLjYKICAgICAgICAgICAgICAgIGJhbGwubm9kZS5tb2RlbCA9IGJhLmdldG1vZGVsKCdlZ2cnKQogICAgICAgICAgICAgICAgYmFsbC5ub2RlLmdyYXZpdHlfc2NhbGUgPSAyCgogICAgICAgIGlmIHNlbGYuZWRnX2VmZjoKICAgICAgICAgICAgc2VsZi5lZGdfZWZmID0gRmFsc2UKCiAgICAgICAgd2FzZGVhZCA9IHNlbGYuX2RlYWQKICAgICAgICBzZWxmLl9kZWFkID0gVHJ1ZQogICAgICAgIHNlbGYuaGl0cG9pbnRzID0gMAogICAgICAgIGlmIG1zZy5pbW1lZGlhdGU6CiAgICAgICAgICAgIGlmIHNlbGYubm9kZToKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5kZWxldGUoKQogICAgICAgIGVsaWYgc2VsZi5ub2RlOgogICAgICAgICAgICBzZWxmLm5vZGUuaHVydCA9IDEuMAogICAgICAgICAgICBpZiBzZWxmLnBsYXlfYmlnX2RlYXRoX3NvdW5kIGFuZCBub3Qgd2FzZGVhZDoKICAgICAgICAgICAgICAgIGJhLnBsYXlzb3VuZChTcGF6RmFjdG9yeS5nZXQoKS5zaW5nbGVfcGxheWVyX2RlYXRoX3NvdW5kKQogICAgICAgICAgICBzZWxmLm5vZGUuZGVhZCA9IFRydWUKICAgICAgICAgICAgYmEudGltZXIoMi4wLCBzZWxmLm5vZGUuZGVsZXRlKQoKICAgICAgICAgICAgdCA9IDAKICAgICAgICAgICAgaWYgc2VsZi5raWxsX2VmZjoKICAgICAgICAgICAgICAgIGZvciBib21icyBpbiByYW5nZSgzKToKICAgICAgICAgICAgICAgICAgICBiYS50aW1lcih0LGJhLkNhbGwoZHJvcF9ib21iKSkKICAgICAgICAgICAgICAgICAgICB0ICs9IDAuMTUKICAgICAgICAgICAgICAgIHNlbGYua2lsbF9lZmYgPSBGYWxzZQoKICAgIGVsaWYgaXNpbnN0YW5jZShtc2csIGJhLk91dE9mQm91bmRzTWVzc2FnZSk6CiAgICAgICAgc2VsZi5oYW5kbGVtZXNzYWdlKGJhLkRpZU1lc3NhZ2UoaG93PWJhLkRlYXRoVHlwZS5GQUxMKSkKCiAgICBlbGlmIGlzaW5zdGFuY2UobXNnLCBiYS5TdGFuZE1lc3NhZ2UpOgogICAgICAgIHNlbGYuX2xhc3Rfc3RhbmRfcG9zID0gKG1zZy5wb3NpdGlvblswXSwgbXNnLnBvc2l0aW9uWzFdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1zZy5wb3NpdGlvblsyXSkKICAgICAgICBpZiBzZWxmLm5vZGU6CiAgICAgICAgICAgIHNlbGYubm9kZS5oYW5kbGVtZXNzYWdlKCdzdGFuZCcsIG1zZy5wb3NpdGlvblswXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbXNnLnBvc2l0aW9uWzFdLCBtc2cucG9zaXRpb25bMl0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1zZy5hbmdsZSkKCiAgICBlbGlmIGlzaW5zdGFuY2UobXNnLCBDdXJzZUV4cGxvZGVNZXNzYWdlKToKICAgICAgICBzZWxmLmN1cnNlX2V4cGxvZGUoKQoKICAgIGVsaWYgaXNpbnN0YW5jZShtc2csIFB1bmNoSGl0TWVzc2FnZSk6CiAgICAgICAgaWYgbm90IHNlbGYubm9kZToKICAgICAgICAgICAgcmV0dXJuIE5vbmUKICAgICAgICBub2RlID0gYmEuZ2V0Y29sbGlzaW9uKCkub3Bwb3Npbmdub2RlCgogICAgICAgIGlmIG5vZGUgYW5kIChub2RlIG5vdCBpbiBzZWxmLl9wdW5jaGVkX25vZGVzKToKCiAgICAgICAgICAgIHB1bmNoX21vbWVudHVtX2FuZ3VsYXIgPSAoc2VsZi5ub2RlLnB1bmNoX21vbWVudHVtX2FuZ3VsYXIgKgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlbGYuX3B1bmNoX3Bvd2VyX3NjYWxlKQogICAgICAgICAgICBwdW5jaF9wb3dlciA9IHNlbGYubm9kZS5wdW5jaF9wb3dlciAqIHNlbGYuX3B1bmNoX3Bvd2VyX3NjYWxlCgogICAgICAgICAgICBpZiBub2RlLmdldG5vZGV0eXBlKCkgIT0gJ3NwYXonOgogICAgICAgICAgICAgICAgc291bmRzID0gU3BhekZhY3RvcnkuZ2V0KCkuaW1wYWN0X3NvdW5kc19tZWRpdW0KICAgICAgICAgICAgICAgIHNvdW5kID0gc291bmRzW3JhbmRvbS5yYW5kcmFuZ2UobGVuKHNvdW5kcykpXQogICAgICAgICAgICAgICAgYmEucGxheXNvdW5kKHNvdW5kLCAxLjAsIHBvc2l0aW9uPXNlbGYubm9kZS5wb3NpdGlvbikKCiAgICAgICAgICAgIHBwb3MgPSBzZWxmLm5vZGUucHVuY2hfcG9zaXRpb24KICAgICAgICAgICAgcHVuY2hkaXIgPSBzZWxmLm5vZGUucHVuY2hfdmVsb2NpdHkKICAgICAgICAgICAgdmVsID0gc2VsZi5ub2RlLnB1bmNoX21vbWVudHVtX2xpbmVhcgoKICAgICAgICAgICAgc2VsZi5fcHVuY2hlZF9ub2Rlcy5hZGQobm9kZSkKICAgICAgICAgICAgbm9kZS5oYW5kbGVtZXNzYWdlKAogICAgICAgICAgICAgICAgYmEuSGl0TWVzc2FnZSgKICAgICAgICAgICAgICAgICAgICBwb3M9cHBvcywKICAgICAgICAgICAgICAgICAgICB2ZWxvY2l0eT12ZWwsCiAgICAgICAgICAgICAgICAgICAgbWFnbml0dWRlPXB1bmNoX3Bvd2VyICogcHVuY2hfbW9tZW50dW1fYW5ndWxhciAqIDExMC4wLAogICAgICAgICAgICAgICAgICAgIHZlbG9jaXR5X21hZ25pdHVkZT1wdW5jaF9wb3dlciAqIDQwLAogICAgICAgICAgICAgICAgICAgIHJhZGl1cz0wLAogICAgICAgICAgICAgICAgICAgIHNyY25vZGU9c2VsZi5ub2RlLAogICAgICAgICAgICAgICAgICAgIHNvdXJjZV9wbGF5ZXI9c2VsZi5zb3VyY2VfcGxheWVyLAogICAgICAgICAgICAgICAgICAgIGZvcmNlX2RpcmVjdGlvbj1wdW5jaGRpciwKICAgICAgICAgICAgICAgICAgICBoaXRfdHlwZT0ncHVuY2gnLAogICAgICAgICAgICAgICAgICAgIGhpdF9zdWJ0eXBlPSgnc3VwZXJfcHVuY2gnIGlmIHNlbGYuX2hhc19ib3hpbmdfZ2xvdmVzCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGVsc2UgJ2RlZmF1bHQnKSkpCgogICAgICAgICAgICBtYWcgPSAtNDAwLjAKICAgICAgICAgICAgaWYgc2VsZi5faG9ja2V5OgogICAgICAgICAgICAgICAgbWFnICo9IDAuNQogICAgICAgICAgICBpZiBsZW4oc2VsZi5fcHVuY2hlZF9ub2RlcykgPT0gMToKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5oYW5kbGVtZXNzYWdlKCdraWNrX2JhY2snLCBwcG9zWzBdLCBwcG9zWzFdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcHBvc1syXSwgcHVuY2hkaXJbMF0sIHB1bmNoZGlyWzFdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcHVuY2hkaXJbMl0sIG1hZykKICAgIGVsaWYgaXNpbnN0YW5jZShtc2csIFBpY2t1cE1lc3NhZ2UpOgogICAgICAgIGlmIG5vdCBzZWxmLm5vZGU6CiAgICAgICAgICAgIHJldHVybiBOb25lCgogICAgICAgIHRyeToKICAgICAgICAgICAgY29sbGlzaW9uID0gYmEuZ2V0Y29sbGlzaW9uKCkKICAgICAgICAgICAgb3Bwb3Npbmdub2RlID0gY29sbGlzaW9uLm9wcG9zaW5nbm9kZQogICAgICAgICAgICBvcHBvc2luZ2JvZHkgPSBjb2xsaXNpb24ub3Bwb3Npbmdib2R5CiAgICAgICAgZXhjZXB0IGJhLk5vdEZvdW5kRXJyb3I6CiAgICAgICAgICAgIHJldHVybiBUcnVlCgogICAgICAgIHRyeToKICAgICAgICAgICAgaWYgb3Bwb3Npbmdub2RlLmludmluY2libGU6CiAgICAgICAgICAgICAgICByZXR1cm4gVHJ1ZQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgICAgIHBhc3MKCiAgICAgICAgaWYgKG9wcG9zaW5nbm9kZS5nZXRub2RldHlwZSgpID09ICdzcGF6JwogICAgICAgICAgICAgICAgYW5kIG5vdCBvcHBvc2luZ25vZGUuc2hhdHRlcmVkIGFuZCBvcHBvc2luZ2JvZHkgPT0gNCk6CiAgICAgICAgICAgIG9wcG9zaW5nYm9keSA9IDEKCiAgICAgICAgaGVsZCA9IHNlbGYubm9kZS5ob2xkX25vZGUKICAgICAgICBpZiBoZWxkIGFuZCBoZWxkLmdldG5vZGV0eXBlKCkgPT0gJ2ZsYWcnOgogICAgICAgICAgICByZXR1cm4gVHJ1ZQoKICAgICAgICBzZWxmLm5vZGUuaG9sZF9ib2R5ID0gb3Bwb3Npbmdib2R5CiAgICAgICAgc2VsZi5ub2RlLmhvbGRfbm9kZSA9IG9wcG9zaW5nbm9kZQogICAgZWxpZiBpc2luc3RhbmNlKG1zZywgYmEuQ2VsZWJyYXRlTWVzc2FnZSk6CiAgICAgICAgaWYgc2VsZi5ub2RlOgogICAgICAgICAgICBzZWxmLm5vZGUuaGFuZGxlbWVzc2FnZSgnY2VsZWJyYXRlJywgaW50KG1zZy5kdXJhdGlvbiAqIDEwMDApKQoKICAgIHJldHVybiBOb25lCiAgICAgICAgCmNsYXNzIFBvd2VydXBNYW5hZ2VyV2luZG93KFBvcHVwV2luZG93KToKICAgIGRlZiBfX2luaXRfXyhzZWxmLCB0cmFuc2l0aW9uPSAnaW5fcmlnaHQnKToKICAgICAgICBjb2x1bW5zID0gMgogICAgICAgIHNlbGYuX3dpZHRoID0gd2lkdGggPSA4MDAKICAgICAgICBzZWxmLl9oZWlnaHQgPSBoZWlnaHQgPSA1MDAKICAgICAgICBzZWxmLl9zdWJfaGVpZ2h0ID0gMjAwCiAgICAgICAgc2VsZi5fc2Nyb2xsX3dpZHRoID0gc2VsZi5fd2lkdGgqMC45MAogICAgICAgIHNlbGYuX3Njcm9sbF9oZWlnaHQgPSBzZWxmLl9oZWlnaHQgLSAxODAKICAgICAgICBzZWxmLl9zdWJfd2lkdGggPSBzZWxmLl9zY3JvbGxfd2lkdGgqMC45NTsKICAgICAgICBzZWxmLnRhYl9idXR0b25zOiBzZXQgPSB7fQogICAgICAgIHNlbGYubGlzdF9jbHNfcG93ZXI6IGxpc3QgPSBbXQogICAgICAgIHNlbGYuZGVmYXVsdF9wb3dlcnVwcyA9IGRlZmF1bHRfcG93ZXJ1cHMoKQogICAgICAgIHNlbGYuZGVmYXVsdF9wb3dlcl9saXN0ID0gbGlzdChzZWxmLmRlZmF1bHRfcG93ZXJ1cHMpCiAgICAgICAgc2VsZi5jb2lucyA9IGFwZ1snQmVhciBDb2luJ10KICAgICAgICBzZWxmLnBvcHVwX2Nsc19wb3dlciA9IE5vbmUKCiAgICAgICAgaWYgbm90IFNUT1JFWydCdXkgRmlyZWJvbWJzJ106CiAgICAgICAgICAgIHBvd2VydXBzWydGaXJlIEJvbWJzJ10gPSAwCiAgICAgICAgICAgIHNlbGYuZGVmYXVsdF9wb3dlcl9saXN0LnJlbW92ZSgnRmlyZSBCb21icycpCgogICAgICAgIHNlbGYuY2hhcnN0ciA9IFtiYS5jaGFyc3RyKGJhLlNwZWNpYWxDaGFyLkxFRlRfQVJST1cpLAogICAgICAgICAgICAgICAgICAgICAgICBiYS5jaGFyc3RyKGJhLlNwZWNpYWxDaGFyLlJJR0hUX0FSUk9XKSwKICAgICAgICAgICAgICAgICAgICAgICAgYmEuY2hhcnN0cihiYS5TcGVjaWFsQ2hhci5VUF9BUlJPVyksCiAgICAgICAgICAgICAgICAgICAgICAgIGJhLmNoYXJzdHIoYmEuU3BlY2lhbENoYXIuRE9XTl9BUlJPVyldCgogICAgICAgIHNlbGYudGFiZGVmcyA9IHsiQWN0aW9uIDEiOiBbJ3Bvd2VydXBJY2VCb21icycsKDEsMSwxKV0sCiAgICAgICAgICAgICAgICAgICAgICAgICJBY3Rpb24gMiI6IFsnc2V0dGluZ3NJY29uJywoMCwxLDApXSwKICAgICAgICAgICAgICAgICAgICAgICAgIkFjdGlvbiAzIjogWydpbnZlbnRvcnlJY29uJywoMSwxLDEpXSwKICAgICAgICAgICAgICAgICAgICAgICAgIkFjdGlvbiA0IjogWydzdG9yZUljb24nLCgxLDEsMSldLAogICAgICAgICAgICAgICAgICAgICAgICAiQWN0aW9uIDUiOiBbJ2FkdmFuY2VkSWNvbicsKDEsMSwxKV0sCiAgICAgICAgICAgICAgICAgICAgICAgICJBYm91dCI6IFsnaGVhcnQnLCgxLjUsMC4zLDAuMyldfQogICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICBpZiAoU1RPUkVbJ0J1eSBGaXJlYm9tYnMnXSBhbmQKICAgICAgICAgICAgU1RPUkVbJ0J1eSBPcHRpb24nXSBhbmQKICAgICAgICAgICAgU1RPUkVbJ0J1eSBQZXJjZW50YWdlJ10pOgogICAgICAgICAgICBzZWxmLnRhYmRlZnMgPSB7IkFjdGlvbiAxIjogWydwb3dlcnVwSWNlQm9tYnMnLCgxLDEsMSldLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIkFjdGlvbiAyIjogWydzZXR0aW5nc0ljb24nLCgwLDEsMCldLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIkFjdGlvbiAzIjogWydpbnZlbnRvcnlJY29uJywoMSwxLDEpXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJBYm91dCI6IFsnaGVhcnQnLCgxLjUsMC4zLDAuMyldfQogICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICBzZWxmLmxpc3RkZWYgPSBsaXN0KHNlbGYudGFiZGVmcykKICAgICAgICAKICAgICAgICBzZWxmLmNvdW50ID0gbGVuKHNlbGYudGFiZGVmcykKICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgc2VsZi5fY3VycmVudF90YWIgPSBHTE9CQUxbJ1RhYiddCgogICAgICAgIGFwcCA9IGJhLmFwcC51aQogICAgICAgIHVpc2NhbGUgPSBhcHAudWlzY2FsZQoKICAgICAgICBzZWxmLl9yb290X3dpZGdldCA9IGJhLmNvbnRhaW5lcndpZGdldChzaXplPSh3aWR0aCs5MCxoZWlnaHQrODApLHRyYW5zaXRpb249dHJhbnNpdGlvbiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgc2NhbGU9MS41IGlmIHVpc2NhbGUgaXMgYmEuVUlTY2FsZS5TTUFMTCBlbHNlIDEuMCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RhY2tfb2Zmc2V0PSgwLC0zMCkgaWYgdWlzY2FsZSBpcyBiYS5VSVNjYWxlLlNNQUxMIGVsc2UgICgwLDApKQogICAgICAgIAogICAgICAgIHNlbGYuX2JhY2tCdXR0b24gPSBiID0gYmEuYnV0dG9ud2lkZ2V0KHBhcmVudD1zZWxmLl9yb290X3dpZGdldCxhdXRvc2VsZWN0PVRydWUsCiAgICAgICAgICAgICAgICAgICAgICAgcG9zaXRpb249KDYwLHNlbGYuX2hlaWdodC0xNSksc2l6ZT0oMTMwLDYwKSwKICAgICAgICAgICAgICAgICAgICAgICBzY2FsZT0wLjgsdGV4dF9zY2FsZT0xLjIsbGFiZWw9YmEuTHN0cihyZXNvdXJjZT0nYmFja1RleHQnKSwKICAgICAgICAgICAgICAgICAgICAgICBidXR0b25fdHlwZT0nYmFjaycsb25fYWN0aXZhdGVfY2FsbD1iYS5DYWxsKHNlbGYuX2JhY2spKQogICAgICAgIGJhLmJ1dHRvbndpZGdldChlZGl0PXNlbGYuX2JhY2tCdXR0b24sIGJ1dHRvbl90eXBlPSdiYWNrU21hbGwnLHNpemU9KDYwLCA2MCksbGFiZWw9YmEuY2hhcnN0cihiYS5TcGVjaWFsQ2hhci5CQUNLKSkKICAgICAgICBiYS5jb250YWluZXJ3aWRnZXQoZWRpdD1zZWxmLl9yb290X3dpZGdldCxjYW5jZWxfYnV0dG9uPWIpCgogICAgICAgIHNlbGYudGl0bGV0ZXh0ID0gYmEudGV4dHdpZGdldChwYXJlbnQ9c2VsZi5fcm9vdF93aWRnZXQscG9zaXRpb249KDAsIGhlaWdodC0xNSksc2l6ZT0od2lkdGgsNTApLAogICAgICAgICAgICAgICAgICAgICAgICAgIGhfYWxpZ249ImNlbnRlciIsY29sb3I9YmEuYXBwLnVpLnRpdGxlX2NvbG9yLCB2X2FsaWduPSJjZW50ZXIiLG1heHdpZHRoPXdpZHRoKjEuMykKICAgICAgICAKICAgICAgICBpbmRleCA9IDAKICAgICAgICBmb3IgdGFiIGluIHJhbmdlKHNlbGYuY291bnQpOgogICAgICAgICAgICBmb3IgdGFiMiBpbiByYW5nZShjb2x1bW5zKToKICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgdGFnID0gc2VsZi5saXN0ZGVmW2luZGV4XQogICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBwb3NpdGlvbiA9ICg2MjArKHRhYjIqMTIwKSxzZWxmLl9oZWlnaHQtNTAqMi41LSh0YWIqMTIwKSkKICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgaWYgdGFnID09ICdBYm91dCc6CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9IGJhLkxzdHIocmVzb3VyY2U9J2dhdGhlcldpbmRvdy5hYm91dFRleHQnKQogICAgICAgICAgICAgICAgZWxpZiB0YWIgPT0gJ0FjdGlvbiA0JzoKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gYmEuTHN0cihyZXNvdXJjZT0nc3RvcmVUZXh0JykKICAgICAgICAgICAgICAgIGVsc2U6IHRleHQgPSBnZXRsYW5ndWFnZSh0YWcpCiAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIHNlbGYudGFiX2J1dHRvbnNbdGFnXSA9IGJhLmJ1dHRvbndpZGdldChwYXJlbnQ9c2VsZi5fcm9vdF93aWRnZXQsYXV0b3NlbGVjdD1UcnVlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcG9zaXRpb249cG9zaXRpb24sc2l6ZT0oMTEwLDExMCksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzY2FsZT0xLGxhYmVsPScnLGVuYWJsZV9zb3VuZD1GYWxzZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJ1dHRvbl90eXBlPSdzcXVhcmUnLG9uX2FjdGl2YXRlX2NhbGw9YmEuQ2FsbChzZWxmLl9zZXRfdGFiLHRhZyxzb3VuZD1UcnVlKSkKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBzZWxmLnRleHQgPSBiYS50ZXh0d2lkZ2V0KHBhcmVudD1zZWxmLl9yb290X3dpZGdldCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBvc2l0aW9uPShwb3NpdGlvblswXSs1NSxwb3NpdGlvblsxXSszMCksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBzaXplPSgwLCAwKSxzY2FsZT0xLGNvbG9yPWJhLmFwcC51aS50aXRsZV9jb2xvciwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRyYXdfY29udHJvbGxlcj1zZWxmLnRhYl9idXR0b25zW3RhZ10sbWF4d2lkdGg9MTAwLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgdGV4dD10ZXh0LGhfYWxpZ249J2NlbnRlcicsdl9hbGlnbj0nY2VudGVyJykKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBzZWxmLmltYWdlID0gYmEuaW1hZ2V3aWRnZXQocGFyZW50PXNlbGYuX3Jvb3Rfd2lkZ2V0LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNpemU9KDYwLDYwKSxjb2xvcj1zZWxmLnRhYmRlZnNbdGFnXVsxXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkcmF3X2NvbnRyb2xsZXI9c2VsZi50YWJfYnV0dG9uc1t0YWddLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBvc2l0aW9uPShwb3NpdGlvblswXSsyNSxwb3NpdGlvblsxXSs0MCksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGV4dHVyZT1iYS5nZXR0ZXh0dXJlKHNlbGYudGFiZGVmc1t0YWddWzBdKSkKCiAgICAgICAgICAgICAgICBpbmRleCArPSAxCiAgICAgICAgCiAgICAgICAgICAgICAgICBpZiBzZWxmLmNvdW50ID09IGluZGV4OgogICAgICAgICAgICAgICAgICAgIGJyZWFrCiAgICAgICAKICAgICAgICAgICAgaWYgc2VsZi5jb3VudCA9PSBpbmRleDoKICAgICAgICAgICAgICAgIGJyZWFrCiAgICAgICAgCiAgICAgICAgc2VsZi5fc2Nyb2xsd2lkZ2V0ID0gTm9uZQogICAgICAgIHNlbGYuX3RhYl9jb250YWluZXIgPSBOb25lCiAgICAgICAgc2VsZi5fc2V0X3RhYihzZWxmLl9jdXJyZW50X3RhYikKCiAgICBkZWYgX19kZWxfXyhzZWxmKToKICAgICAgICBhcGcuYXBwbHlfYW5kX2NvbW1pdCgpCgogICAgZGVmIF9zZXRfdGFiKHNlbGYsIHRhYiwgc291bmQ6IGJvb2wgPSBGYWxzZSk6CiAgICAgICAgc2VsZi5zb3VuZCA9IHNvdW5kCiAgICAgICAgR0xPQkFMWydUYWInXSA9IHRhYgogICAgICAgIGFwZy5hcHBseV9hbmRfY29tbWl0KCkKICAgICAgICAKICAgICAgICBpZiBzZWxmLl90YWJfY29udGFpbmVyIGlzIG5vdCBOb25lIGFuZCBzZWxmLl90YWJfY29udGFpbmVyLmV4aXN0cygpOgogICAgICAgICAgICBzZWxmLl90YWJfY29udGFpbmVyLmRlbGV0ZSgpCgogICAgICAgIGlmIHNlbGYuc291bmQ6CiAgICAgICAgICAgIGJhLnBsYXlzb3VuZChiYS5nZXRzb3VuZCgnY2xpY2swMScpKQoKICAgICAgICBpZiBzZWxmLl9zY3JvbGx3aWRnZXQ6CiAgICAgICAgICAgIHNlbGYuX3Njcm9sbHdpZGdldC5kZWxldGUoKQoKICAgICAgICBzZWxmLl9zY3JvbGx3aWRnZXQgPSBiYS5zY3JvbGx3aWRnZXQocGFyZW50PXNlbGYuX3Jvb3Rfd2lkZ2V0LAogICAgICAgICAgICBwb3NpdGlvbj0oc2VsZi5fd2lkdGgqMC4wOCw1MSoxLjgpLHNpemU9KHNlbGYuX3N1Yl93aWR0aCAtMTQwLHNlbGYuX3Njcm9sbF9oZWlnaHQgKzYwKjEuMikpCgogICAgICAgIGlmIHRhYiA9PSAnQWN0aW9uIDQnOgogICAgICAgICAgICBpZiBzZWxmLl9zY3JvbGx3aWRnZXQ6CiAgICAgICAgICAgICAgICBzZWxmLl9zY3JvbGx3aWRnZXQuZGVsZXRlKCkKICAgICAgICAgICAgc2VsZi5fc2Nyb2xsd2lkZ2V0ID0gYmEuaHNjcm9sbHdpZGdldChwYXJlbnQ9c2VsZi5fcm9vdF93aWRnZXQsCiAgICAgICAgICAgICAgICBwb3NpdGlvbj0oc2VsZi5fd2lkdGgqMC4wOCw1MSoxLjgpLHNpemU9KHNlbGYuX3N1Yl93aWR0aCAtMTQwLHNlbGYuX3Njcm9sbF9oZWlnaHQgKzYwKjEuMiksCiAgICAgICAgICAgICAgICBjYXB0dXJlX2Fycm93cz1UcnVlLGNsYWltc19sZWZ0X3JpZ2h0PVRydWUpCiAgICAgICAgICAgIGJhLnRleHR3aWRnZXQoZWRpdD1zZWxmLnRpdGxldGV4dCx0ZXh0PWJhLkxzdHIocmVzb3VyY2U9J3N0b3JlVGV4dCcpKQogICAgICAgIGVsaWYgdGFiID09ICdBYm91dCc6CiAgICAgICAgICAgIGJhLnRleHR3aWRnZXQoZWRpdD1zZWxmLnRpdGxldGV4dCx0ZXh0PWJhLkxzdHIocmVzb3VyY2U9J2dhdGhlcldpbmRvdy5hYm91dFRleHQnKSkKICAgICAgICBlbHNlOiBiYS50ZXh0d2lkZ2V0KGVkaXQ9c2VsZi50aXRsZXRleHQsdGV4dD1nZXRsYW5ndWFnZSh0YWIpKQoKICAgICAgICBjaG9pY2VzID0gWydSZXNldCcsJ09ubHkgQm9tYnMnLCdPbmx5IEl0ZW1zJywnTmV3JywnTm90aGluZyddCiAgICAgICAgY19kaXNwbGF5ID0gW10KICAgICAgICAKICAgICAgICBmb3IgZGlzcGxheSBpbiBjaG9pY2VzOgogICAgICAgICAgICBjaG9pY2VzX2Rpc3BsYXkgPSBiYS5Mc3RyKHRyYW5zbGF0ZT0oIiIsZ2V0bGFuZ3VhZ2UoZGlzcGxheSkpKQogICAgICAgICAgICBjX2Rpc3BsYXkuYXBwZW5kKGNob2ljZXNfZGlzcGxheSkKICAgIAogICAgICAgIGlmIHRhYiA9PSAnQWN0aW9uIDEnOgogICAgICAgICAgICBzZWxmLnBvcHVwX2Nsc19wb3dlciA9IFBvcHVwTWVudSgKICAgICAgICAgICAgICAgICAgcGFyZW50PXNlbGYuX3Jvb3Rfd2lkZ2V0LAogICAgICAgICAgICAgICAgICBwb3NpdGlvbj0oMTMwLHNlbGYuX3dpZHRoKjAuNjEpLAogICAgICAgICAgICAgICAgICBidXR0b25fc2l6ZT0oMTUwLDUwKSxzY2FsZT0yLjUsCiAgICAgICAgICAgICAgICAgIGNob2ljZXM9Y2hvaWNlcyx3aWR0aD0xNTAsCiAgICAgICAgICAgICAgICAgIGNob2ljZXNfZGlzcGxheT1jX2Rpc3BsYXksCiAgICAgICAgICAgICAgICAgIGN1cnJlbnRfY2hvaWNlPUdMT0JBTFsnQ2xzIFBvd2VydXAnXSwKICAgICAgICAgICAgICAgICAgb25fdmFsdWVfY2hhbmdlX2NhbGw9c2VsZi5fc2V0X2NvbmNlcHQpCiAgICAgICAgICAgIHNlbGYubGlzdF9jbHNfcG93ZXIuYXBwZW5kKHNlbGYucG9wdXBfY2xzX3Bvd2VyLl9idXR0b24pCiAgICAgICAgICAgIAogICAgICAgICAgICBzZWxmLmJ1dHRvbl9jbHNfcG93ZXIgPSBiYS5idXR0b253aWRnZXQocGFyZW50PXNlbGYuX3Jvb3Rfd2lkZ2V0LAogICAgICAgICAgICAgICAgICAgIHBvc2l0aW9uPSg1MDAsc2VsZi5fd2lkdGgqMC42MSksc2l6ZT0oNTAsNTApLGF1dG9zZWxlY3Q9VHJ1ZSwKICAgICAgICAgICAgICAgICAgICBzY2FsZT0xLGxhYmVsPSgnJScpLHRleHRfc2NhbGU9MSxidXR0b25fdHlwZT0nc3F1YXJlJywKICAgICAgICAgICAgICAgICAgICBvbl9hY3RpdmF0ZV9jYWxsPXNlbGYuX3BlcmNlbnRhZ2Vfd2luZG93KSAKICAgICAgICAgICAgc2VsZi5saXN0X2Nsc19wb3dlci5hcHBlbmQoc2VsZi5idXR0b25fY2xzX3Bvd2VyKQogICAgICAgICAgICAKICAgICAgICAgICAgcmV3aW5kb3cgPSBbc2VsZi5wb3B1cF9jbHNfcG93ZXIuX2J1dHRvbixzZWxmLmJ1dHRvbl9jbHNfcG93ZXJdCiAgICAgICAgICAgIAogICAgICAgICAgICBmb3IgY2xzIGluIHNlbGYubGlzdF9jbHNfcG93ZXI6ICMgdGhpcyBpcyB2ZXJ5IGltcG9ydGFudCBzbyB0aGF0IHB1cHVwcyBkb24ndCBhY2N1bXVsYXRlCiAgICAgICAgICAgICAgICBpZiBjbHMgbm90IGluIHJld2luZG93OgogICAgICAgICAgICAgICAgICAgIGNscy5kZWxldGUoKQogICAgICAgICAgICAKICAgICAgICBlbGlmIHRhYiA9PSAnQWN0aW9uIDQnOgogICAgICAgICAgICBzZWxmLmJ1dHRvbl9jb2luID0gYmEuYnV0dG9ud2lkZ2V0KHBhcmVudD1zZWxmLl9yb290X3dpZGdldCxpY29uPWJhLmdldHRleHR1cmUoJ2NvaW4nKSwKICAgICAgICAgICAgICAgICAgICBwb3NpdGlvbj0oNTUwLHNlbGYuX3dpZHRoKjAuNjE0KSxzaXplPSgxNjAsNDApLHRleHRjb2xvcj0oMCwxLDApLGNvbG9yPSgwLDEsNiksCiAgICAgICAgICAgICAgICAgICAgc2NhbGU9MSxsYWJlbD1zdHIoYXBnWydCZWFyIENvaW4nXSksdGV4dF9zY2FsZT0xLGF1dG9zZWxlY3Q9VHJ1ZSwKICAgICAgICAgICAgICAgICAgICBvbl9hY3RpdmF0ZV9jYWxsPU5vbmUpICNzZWxmLl9wZXJjZW50YWdlX3dpbmRvdykKICAgICAgICAgICAgc2VsZi5saXN0X2Nsc19wb3dlci5hcHBlbmQoc2VsZi5idXR0b25fY29pbikKICAgICAgICAgICAgCiAgICAgICAgICAgIHRyeTogcmV3aW5kb3cuYXBwZW5kKHNlbGYuYnV0dG9uX2NvaW4pCiAgICAgICAgICAgIGV4Y2VwdDogcmV3aW5kb3cgPSBbc2VsZi5idXR0b25fY29pbl0KICAgICAgICAgICAgZm9yIGNscyBpbiBzZWxmLmxpc3RfY2xzX3Bvd2VyOiAjIHRoaXMgaXMgdmVyeSBpbXBvcnRhbnQgc28gdGhhdCBwdXB1cHMgZG9uJ3QgYWNjdW11bGF0ZQogICAgICAgICAgICAgICAgaWYgY2xzIG5vdCBpbiByZXdpbmRvdzoKICAgICAgICAgICAgICAgICAgICBjbHMuZGVsZXRlKCkKICAgICAgICAgICAgCiAgICAgICAgZWxzZToKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgZm9yIGNscyBpbiBzZWxmLmxpc3RfY2xzX3Bvd2VyOgogICAgICAgICAgICAgICAgICAgIGNscy5kZWxldGUoKQogICAgICAgICAgICBleGNlcHQ6IHBhc3MKCiAgICAgICAgaWYgdGFiID09ICdBY3Rpb24gMSc6CiAgICAgICAgICAgIHN1Yl9oZWlnaHQgPSBsZW4oc2VsZi5kZWZhdWx0X3Bvd2VyX2xpc3QpICogOTAKICAgICAgICAgICAgdiA9IHN1Yl9oZWlnaHQgLSA1NQogICAgICAgICAgICB3aWR0aCA9IDMwMAogICAgICAgICAgICBwb3NpID0gMAogICAgICAgICAgICBpZF9wb3dlciA9IGxpc3Qoc2VsZi5kZWZhdWx0X3Bvd2VydXBzKQogICAgICAgICAgICBuZXdfcG93ZXJ1cHMgPSBpZF9wb3dlcls5Ol0KICAgICAgICAgICAgc2VsZi5saXN0cG93ZXIgPSB7fQogICAgICAgICAgICAKICAgICAgICAgICAgc2VsZi5fdGFiX2NvbnRhaW5lciA9IGMgPSBiYS5jb250YWluZXJ3aWRnZXQocGFyZW50PXNlbGYuX3Njcm9sbHdpZGdldCwKICAgICAgICAgICAgICAgIHNpemU9KHNlbGYuX3N1Yl93aWR0aCxzdWJfaGVpZ2h0KSwKICAgICAgICAgICAgICAgIGJhY2tncm91bmQ9RmFsc2Usc2VsZWN0aW9uX2xvb3BzX3RvX3BhcmVudD1UcnVlKQoKICAgICAgICAgICAgZm9yIHBvd2VyIGluIHNlbGYuZGVmYXVsdF9wb3dlcl9saXN0OgogICAgICAgICAgICAgICAgaWYgcG93ZXIgPT0gaWRfcG93ZXJbMF06CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9ICdoZWxwV2luZG93LnBvd2VydXBTaGllbGROYW1lVGV4dCcKICAgICAgICAgICAgICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdwb3dlcnVwU2hpZWxkJykKICAgICAgICAgICAgICAgIGVsaWYgcG93ZXIgPT0gaWRfcG93ZXJbMV06CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9ICdoZWxwV2luZG93LnBvd2VydXBQdW5jaE5hbWVUZXh0JwogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ3Bvd2VydXBQdW5jaCcpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzJdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSAnaGVscFdpbmRvdy5wb3dlcnVwTGFuZE1pbmVzTmFtZVRleHQnCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgncG93ZXJ1cExhbmRNaW5lcycpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzNdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSAnaGVscFdpbmRvdy5wb3dlcnVwSW1wYWN0Qm9tYnNOYW1lVGV4dCcKICAgICAgICAgICAgICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdwb3dlcnVwSW1wYWN0Qm9tYnMnKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlcls0XToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gJ2hlbHBXaW5kb3cucG93ZXJ1cEljZUJvbWJzTmFtZVRleHQnCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgncG93ZXJ1cEljZUJvbWJzJykKICAgICAgICAgICAgICAgIGVsaWYgcG93ZXIgPT0gaWRfcG93ZXJbNV06CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9ICdoZWxwV2luZG93LnBvd2VydXBCb21iTmFtZVRleHQnCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgncG93ZXJ1cEJvbWInKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlcls2XToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gJ2hlbHBXaW5kb3cucG93ZXJ1cFN0aWNreUJvbWJzTmFtZVRleHQnCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgncG93ZXJ1cFN0aWNreUJvbWJzJykKICAgICAgICAgICAgICAgIGVsaWYgcG93ZXIgPT0gaWRfcG93ZXJbN106CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9ICdoZWxwV2luZG93LnBvd2VydXBDdXJzZU5hbWVUZXh0JwogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ3Bvd2VydXBDdXJzZScpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzhdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSAnaGVscFdpbmRvdy5wb3dlcnVwSGVhbHRoTmFtZVRleHQnCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgncG93ZXJ1cEhlYWx0aCcpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzldOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSBwb3dlcgogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ3Bvd2VydXBTcGVlZCcpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzEwXToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gcG93ZXIKICAgICAgICAgICAgICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdoZWFydCcpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzExXToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gIkdvb2RieWUhIgogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ2FjaGlldmVtZW50T25zbGF1Z2h0JykKICAgICAgICAgICAgICAgIGVsaWYgcG93ZXIgPT0gaWRfcG93ZXJbMTJdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSBwb3dlcgogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ291eWFVQnV0dG9uJykKICAgICAgICAgICAgICAgIGVsaWYgcG93ZXIgPT0gaWRfcG93ZXJbMTNdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSBwb3dlcgogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ2FjaGlldmVtZW50U3VwZXJQdW5jaCcpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzE0XToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gcG93ZXIKICAgICAgICAgICAgICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdsZXZlbEljb24nKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlclsxNV06CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9IHBvd2VyCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgnb3V5YU9CdXR0b24nKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlclsxNl06CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9IHBvd2VyCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgnc3RhcicpCiAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBpZiBwb3dlciBpbiBuZXdfcG93ZXJ1cHM6IGxhYmVsID0gZ2V0bGFuZ3VhZ2UocG93ZXIpCiAgICAgICAgICAgICAgICBlbHNlOiBsYWJlbCA9IGJhLkxzdHIocmVzb3VyY2U9dGV4dCkKCiAgICAgICAgICAgICAgICBhcHBlcmFuY2UgPSBwb3dlcnVwc1twb3dlcl0KICAgICAgICAgICAgICAgIHBvc2l0aW9uID0gKDkwLHYtcG9zaSkKCiAgICAgICAgICAgICAgICB0ID0gYmEudGV4dHdpZGdldChwYXJlbnQ9Yyxwb3NpdGlvbj0ocG9zaXRpb25bMF0tMzAscG9zaXRpb25bMV0tMTUpLHNpemU9KHdpZHRoLDUwKSwKICAgICAgICAgICAgICAgICAgICAgICAgICBoX2FsaWduPSJjZW50ZXIiLGNvbG9yPShiYS5hcHAudWkudGl0bGVfY29sb3IpLCB0ZXh0PWxhYmVsLCB2X2FsaWduPSJjZW50ZXIiLG1heHdpZHRoPXdpZHRoKjEuMykKICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBzZWxmLnBvd3ByZXYgPSBiYS5pbWFnZXdpZGdldChwYXJlbnQ9YywKICAgICAgICAgICAgICAgICAgICBwb3NpdGlvbj0ocG9zaXRpb25bMF0tNzAscG9zaXRpb25bMV0tMTApLAogICAgICAgICAgICAgICAgICAgIHNpemU9KDUwLDUwKSx0ZXh0dXJlPXRleCkKICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBkaXBvcyA9IDAKICAgICAgICAgICAgICAgIGZvciBkaXJlYyBpbiBbJy0nLCcrJ106CiAgICAgICAgICAgICAgICAgICAgYmEuYnV0dG9ud2lkZ2V0KHBhcmVudD1jLGF1dG9zZWxlY3Q9VHJ1ZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwb3NpdGlvbj0ocG9zaXRpb25bMF0rMjcwK2RpcG9zLHBvc2l0aW9uWzFdLTEwKSxzaXplPSgxMDAsMTAwKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzY2FsZT0wLjQsbGFiZWw9ZGlyZWMsYnV0dG9uX3R5cGU9J3NxdWFyZScsdGV4dF9zY2FsZT00LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG9uX2FjdGl2YXRlX2NhbGw9YmEuQ2FsbChzZWxmLmFwcGVyYW5jZV9wb3dlcnVwcyxwb3dlcixkaXJlYykpCiAgICAgICAgICAgICAgICAgICAgZGlwb3MgKz0gMTAwCiAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgdGV4dHdpZGdldCA9IGJhLnRleHR3aWRnZXQocGFyZW50PWMscG9zaXRpb249KHBvc2l0aW9uWzBdKzE5MCxwb3NpdGlvblsxXS0xNSksc2l6ZT0od2lkdGgsNTApLAogICAgICAgICAgICAgICAgICAgICAgICAgIGhfYWxpZ249ImNlbnRlciIsY29sb3I9Y2xzX3Bvd19jb2xvcigpW2FwcGVyYW5jZV0sdGV4dD1zdHIoYXBwZXJhbmNlKSwKICAgICAgICAgICAgICAgICAgICAgICAgICB2X2FsaWduPSJjZW50ZXIiLG1heHdpZHRoPXdpZHRoKjEuMykKICAgICAgICAgICAgICAgIHNlbGYubGlzdHBvd2VyW3Bvd2VyXSA9IHRleHR3aWRnZXQKICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBwb3NpICs9IDkwCiAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgIGVsaWYgdGFiID09ICdBY3Rpb24gMic6CiAgICAgICAgICAgIHN1Yl9oZWlnaHQgPSAzNzAgaWYgbm90IFNUT1JFWydCdXkgT3B0aW9uJ10gZWxzZSA0NTAKICAgICAgICAgICAgdiA9IHN1Yl9oZWlnaHQgLSA1NQogICAgICAgICAgICB3aWR0aCA9IDMwMAogICAgICAgICAgICAKICAgICAgICAgICAgc2VsZi5fdGFiX2NvbnRhaW5lciA9IGMgPSBiYS5jb250YWluZXJ3aWRnZXQocGFyZW50PXNlbGYuX3Njcm9sbHdpZGdldCwKICAgICAgICAgICAgICAgIHNpemU9KHNlbGYuX3N1Yl93aWR0aCxzdWJfaGVpZ2h0KSwKICAgICAgICAgICAgICAgIGJhY2tncm91bmQ9RmFsc2Usc2VsZWN0aW9uX2xvb3BzX3RvX3BhcmVudD1UcnVlKQogICAgICAgICAgICAgICAKICAgICAgICAgICAgcG9zaXRpb24gPSAoNDAsdi0yMCkKICAgICAgICAgICAgICAgCiAgICAgICAgICAgIGNfZGlzcGxheSA9IFtdCiAgICAgICAgICAgIGNob2ljZXMgPSBbJ0F1dG8nLCdTWTogQkFMTCcsJ1NZOiBJbXBhY3QnLCdTWTogRWdnJ10KICAgICAgICAgICAgZm9yIGRpc3BsYXkgaW4gY2hvaWNlczoKICAgICAgICAgICAgICAgIGNob2ljZXNfZGlzcGxheSA9IGJhLkxzdHIodHJhbnNsYXRlPSgiIixnZXRsYW5ndWFnZShkaXNwbGF5KSkpCiAgICAgICAgICAgICAgICBjX2Rpc3BsYXkuYXBwZW5kKGNob2ljZXNfZGlzcGxheSkKICAgICAgICAgICAgICAgIAogICAgICAgICAgICBwb3B1cCA9IFBvcHVwTWVudShwYXJlbnQ9YywKICAgICAgICAgICAgICAgICAgcG9zaXRpb249KHBvc2l0aW9uWzBdKzMwMCxwb3NpdGlvblsxXSksCiAgICAgICAgICAgICAgICAgIGJ1dHRvbl9zaXplPSgxNTAsNTApLHNjYWxlPTIuNSwKICAgICAgICAgICAgICAgICAgY2hvaWNlcz1jaG9pY2VzLHdpZHRoPTE1MCwKICAgICAgICAgICAgICAgICAgY2hvaWNlc19kaXNwbGF5PWNfZGlzcGxheSwKICAgICAgICAgICAgICAgICAgY3VycmVudF9jaG9pY2U9Y29uZmlnWydQb3dlcnVwIFN0eWxlJ10sCiAgICAgICAgICAgICAgICAgIG9uX3ZhbHVlX2NoYW5nZV9jYWxsPWJhLkNhbGwoc2VsZi5fYWxsX3BvcHVwLCdQb3dlcnVwIFN0eWxlJykpCiAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICB0ZXh0ID0gZ2V0bGFuZ3VhZ2UoJ1Bvd2VydXAgU3R5bGUnKQogICAgICAgICAgICB3dCA9IChsZW4odGV4dCkqMC44MCkKICAgICAgICAgICAgdCA9IGJhLnRleHR3aWRnZXQocGFyZW50PWMscG9zaXRpb249KHBvc2l0aW9uWzBdLTYwK3d0LHBvc2l0aW9uWzFdKSxzaXplPSh3aWR0aCw1MCksbWF4d2lkdGg9d2lkdGgqMC45LAogICAgICAgICAgICAgICAgc2NhbGU9MS4xLGhfYWxpZ249ImNlbnRlciIsY29sb3I9YmEuYXBwLnVpLnRpdGxlX2NvbG9yLHRleHQ9Z2V0bGFuZ3VhZ2UoJ1Bvd2VydXAgU3R5bGUnKSx2X2FsaWduPSJjZW50ZXIiKQogICAgICAgICAgICAgICAgCiAgICAgICAgICAgIGRpcG9zID0gMAogICAgICAgICAgICBmb3IgZGlyZWMgaW4gWyctJywnKyddOgogICAgICAgICAgICAgICAgYmEuYnV0dG9ud2lkZ2V0KHBhcmVudD1jLGF1dG9zZWxlY3Q9VHJ1ZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBvc2l0aW9uPShwb3NpdGlvblswXSszMTArZGlwb3MscG9zaXRpb25bMV0tMTAwKSxzaXplPSgxMDAsMTAwKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlcGVhdD1UcnVlLHNjYWxlPTAuNCxsYWJlbD1kaXJlYyxidXR0b25fdHlwZT0nc3F1YXJlJyx0ZXh0X3NjYWxlPTQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBvbl9hY3RpdmF0ZV9jYWxsPWJhLkNhbGwoc2VsZi5fcG93ZXJ1cHNfc2NhbGUsZGlyZWMpKQogICAgICAgICAgICAgICAgZGlwb3MgKz0gMTAwCgogICAgICAgICAgICB0eHRfc2NhbGUgPSBjb25maWdbJ1Bvd2VydXAgU2NhbGUnXQogICAgICAgICAgICBzZWxmLnR4dF9zY2FsZSA9IGJhLnRleHR3aWRnZXQocGFyZW50PWMscG9zaXRpb249KHBvc2l0aW9uWzBdKzIzMCxwb3NpdGlvblsxXS0xMDUpLHNpemU9KHdpZHRoLDUwKSwKICAgICAgICAgICAgICAgICAgICAgICAgICBzY2FsZT0xLjEsaF9hbGlnbj0iY2VudGVyIixjb2xvcj0oMCwxLDApLHRleHQ9c3RyKHR4dF9zY2FsZSksdl9hbGlnbj0iY2VudGVyIixtYXh3aWR0aD13aWR0aCoxLjMpCiAgICAgICAgICAgICAKICAgICAgICAgICAgdGV4dCA9IGdldGxhbmd1YWdlKCdQb3dlcnVwIFNjYWxlJykKICAgICAgICAgICAgd3QgPSAobGVuKHRleHQpKjAuODApCiAgICAgICAgICAgIHQgPSBiYS50ZXh0d2lkZ2V0KHBhcmVudD1jLHBvc2l0aW9uPShwb3NpdGlvblswXS02MCt3dCxwb3NpdGlvblsxXS0xMDApLHNpemU9KHdpZHRoLDUwKSxtYXh3aWR0aD13aWR0aCowLjksCiAgICAgICAgICAgICAgICBzY2FsZT0xLjEsaF9hbGlnbj0iY2VudGVyIixjb2xvcj1iYS5hcHAudWkudGl0bGVfY29sb3IsdGV4dD10ZXh0LHZfYWxpZ249ImNlbnRlciIpCiAgICAgICAgICAgICAKICAgICAgICAgICAgcG9zaXRpb24gPSAocG9zaXRpb25bMF0tMjAscG9zaXRpb25bMV0rNDApCiAgICAgICAgICAgICAgICAKICAgICAgICAgICAgc2VsZi5jaGVjayA9IGJhLmNoZWNrYm94d2lkZ2V0KHBhcmVudD1jLHBvc2l0aW9uPShwb3NpdGlvblswXSszMCxwb3NpdGlvblsxXS0yMzApLHZhbHVlPWNvbmZpZ1snUG93ZXJ1cCBOYW1lJ10sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgb25fdmFsdWVfY2hhbmdlX2NhbGw9YmEuQ2FsbChzZWxmLl9zd2l0Y2hlcywnUG93ZXJ1cCBOYW1lJyksbWF4d2lkdGg9c2VsZi5fc2Nyb2xsX3dpZHRoKjAuOSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0ZXh0PWdldGxhbmd1YWdlKCdQb3dlcnVwIE5hbWUnKSxhdXRvc2VsZWN0PVRydWUpCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgIHNlbGYuY2hlY2sgPSBiYS5jaGVja2JveHdpZGdldChwYXJlbnQ9Yyxwb3NpdGlvbj0ocG9zaXRpb25bMF0rMzAscG9zaXRpb25bMV0tMjMwKjEuMyksdmFsdWU9Y29uZmlnWydQb3dlcnVwIFdpdGggU2hpZWxkJ10sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgb25fdmFsdWVfY2hhbmdlX2NhbGw9YmEuQ2FsbChzZWxmLl9zd2l0Y2hlcywnUG93ZXJ1cCBXaXRoIFNoaWVsZCcpLG1heHdpZHRoPXNlbGYuX3Njcm9sbF93aWR0aCowLjksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGV4dD1nZXRsYW5ndWFnZSgnUG93ZXJ1cCBXaXRoIFNoaWVsZCcpLGF1dG9zZWxlY3Q9VHJ1ZSkKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgaWYgU1RPUkVbJ0J1eSBPcHRpb24nXToKICAgICAgICAgICAgICAgIHNlbGYuY2hlY2sgPSBiYS5jaGVja2JveHdpZGdldChwYXJlbnQ9Yyxwb3NpdGlvbj0ocG9zaXRpb25bMF0rMzAscG9zaXRpb25bMV0tMjMwKjEuNiksdmFsdWU9Y29uZmlnWydQb3dlcnVwIFRpbWUnXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgb25fdmFsdWVfY2hhbmdlX2NhbGw9YmEuQ2FsbChzZWxmLl9zd2l0Y2hlcywnUG93ZXJ1cCBUaW1lJyksbWF4d2lkdGg9c2VsZi5fc2Nyb2xsX3dpZHRoKjAuOSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGV4dD1nZXRsYW5ndWFnZSgnUG93ZXJ1cCBUaW1lJyksYXV0b3NlbGVjdD1UcnVlKQogICAgICAgICAgICAgICAgCiAgICAgICAgZWxpZiB0YWIgPT0gJ0FjdGlvbiAzJzoKICAgICAgICAgICAgc3ViX2hlaWdodCA9IDMwMAogICAgICAgICAgICB2ID0gc3ViX2hlaWdodCAtIDU1CiAgICAgICAgICAgIHdpZHRoID0gMzAwCiAgICAgICAgICAgIAogICAgICAgICAgICBzZWxmLl90YWJfY29udGFpbmVyID0gYyA9IGJhLmNvbnRhaW5lcndpZGdldChwYXJlbnQ9c2VsZi5fc2Nyb2xsd2lkZ2V0LAogICAgICAgICAgICAgICAgc2l6ZT0oc2VsZi5fc3ViX3dpZHRoLHN1Yl9oZWlnaHQpLAogICAgICAgICAgICAgICAgYmFja2dyb3VuZD1GYWxzZSxzZWxlY3Rpb25fbG9vcHNfdG9fcGFyZW50PVRydWUpCgogICAgICAgICAgICB2IC09IDIwCiAgICAgICAgICAgIHBvc2l0aW9uID0gKDExMCx2LTQ1KjEuNzIpCiAgICAgICAgICAgIAogICAgICAgICAgICBpZiBub3QgU1RPUkVbJ0J1eSBQZXJjZW50YWdlJ106CiAgICAgICAgICAgICAgICB0ID0gYmEudGV4dHdpZGdldChwYXJlbnQ9Yyxwb3NpdGlvbj0oOTAsdi0xMDApLHNpemU9KDMwK3dpZHRoLDUwKSwKICAgICAgICAgICAgICAgICAgICBoX2FsaWduPSJjZW50ZXIiLHRleHQ9Z2V0bGFuZ3VhZ2UoJ0Jsb2NrIE9wdGlvbiBTdG9yZScpLAogICAgICAgICAgICAgICAgICAgIGNvbG9yPWJhLmFwcC51aS50aXRsZV9jb2xvcix2X2FsaWduPSJjZW50ZXIiLG1heHdpZHRoPXdpZHRoKjEuNSxzY2FsZT0xLjUpCiAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBpID0gYmEuaW1hZ2V3aWRnZXQocGFyZW50PWMsCiAgICAgICAgICAgICAgICAgICAgcG9zaXRpb249KHBvc2l0aW9uWzBdKzEwMCxwb3NpdGlvblsxXS0yMDUpLAogICAgICAgICAgICAgICAgICAgIHNpemU9KDgwLDgwKSx0ZXh0dXJlPWJhLmdldHRleHR1cmUoJ2xvY2snKSkKICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgIHQgPSBiYS50ZXh0d2lkZ2V0KHBhcmVudD1jLHBvc2l0aW9uPShwb3NpdGlvblswXS0xNCxwb3NpdGlvblsxXSs3MCksc2l6ZT0oMzArd2lkdGgsNTApLAogICAgICAgICAgICAgICAgICAgIGhfYWxpZ249ImNlbnRlciIsdGV4dD1mIntnZXRsYW5ndWFnZSgnVGFuayBTaGllbGQgUFRHJyl9ICh7Z2V0bGFuZ3VhZ2UoJ1RhbmsgU2hpZWxkJyl9KSIsCiAgICAgICAgICAgICAgICAgICAgY29sb3I9YmEuYXBwLnVpLnRpdGxlX2NvbG9yLHZfYWxpZ249ImNlbnRlciIsbWF4d2lkdGg9d2lkdGgqMS41LHNjYWxlPTEuNSkKICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgYiA9IGJhLmJ1dHRvbndpZGdldChwYXJlbnQ9YyxhdXRvc2VsZWN0PVRydWUscG9zaXRpb249cG9zaXRpb24sc2l6ZT0oMTAwLDEwMCkscmVwZWF0PVRydWUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNjYWxlPTAuNixsYWJlbD1zZWxmLmNoYXJzdHJbM10sYnV0dG9uX3R5cGU9J3NxdWFyZScsdGV4dF9zY2FsZT0yLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBvbl9hY3RpdmF0ZV9jYWxsPWJhLkNhbGwoc2VsZi50YW5rX3NoaWVsZF9wZXJjZW50YWdlLCdEZWNyZW1lbnQnKSkKICAgIAogICAgICAgICAgICAgICAgYiA9IGJhLmJ1dHRvbndpZGdldChwYXJlbnQ9YyxhdXRvc2VsZWN0PVRydWUscmVwZWF0PVRydWUsdGV4dF9zY2FsZT0yLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwb3NpdGlvbj0ocG9zaXRpb25bMF0qMy4yLHBvc2l0aW9uWzFdKSxzaXplPSgxMDAsMTAwKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2NhbGU9MC42LGxhYmVsPXNlbGYuY2hhcnN0clsyXSxidXR0b25fdHlwZT0nc3F1YXJlJywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgb25fYWN0aXZhdGVfY2FsbD1iYS5DYWxsKHNlbGYudGFua19zaGllbGRfcGVyY2VudGFnZSwnSW5jcmVtZW50JykpCiAgICAKICAgICAgICAgICAgICAgIHBvcmNlbnRhamUgPSBjb25maWdbJ1RhbmsgU2hpZWxkIFBURyddCiAgICAgICAgICAgICAgICBpZiBwb3JjZW50YWplID4gNTk6IGNvbG9yID0gKDAsMSwwKQogICAgICAgICAgICAgICAgZWxpZiBwb3JjZW50YWplIDwgNDA6IGNvbG9yID0gKDEsMSwwKQogICAgICAgICAgICAgICAgZWxzZTogY29sb3IgPSAoMCwxLDAuOCkKICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgc2VsZi50YW5rX3RleHQgPSBiYS50ZXh0d2lkZ2V0KHBhcmVudD1jLHBvc2l0aW9uPShwb3NpdGlvblswXS0xNCxwb3NpdGlvblsxXSs1KSwKICAgICAgICAgICAgICAgICAgICBzaXplPSgzMCt3aWR0aCw1MCksaF9hbGlnbj0iY2VudGVyIiwKICAgICAgICAgICAgICAgICAgICB0ZXh0PXN0cihwb3JjZW50YWplKSsnJScsY29sb3I9Y29sb3IsCiAgICAgICAgICAgICAgICAgICAgdl9hbGlnbj0iY2VudGVyIixtYXh3aWR0aD13aWR0aCoxLjMsc2NhbGU9MikKICAgIAogICAgICAgICAgICAgICAgIyAtLS0tLT4KICAgIAogICAgICAgICAgICAgICAgcG9zaXRpb24gPSAoMTEwLHYtMTYwKjEuNikgICAgICAgICAKICAgICAgICAgICAgICAgIHQgPSBiYS50ZXh0d2lkZ2V0KHBhcmVudD1jLHBvc2l0aW9uPShwb3NpdGlvblswXS0xNCxwb3NpdGlvblsxXSs3MCksc2l6ZT0oMzArd2lkdGgsNTApLAogICAgICAgICAgICAgICAgICAgIGhfYWxpZ249ImNlbnRlciIsdGV4dD1mIntnZXRsYW5ndWFnZSgnSGVhbGluZyBEYW1hZ2UgUFRHJyl9e19zcF99KHtnZXRsYW5ndWFnZSgnSGVhbGluZyBEYW1hZ2UnKX0pIiwKICAgICAgICAgICAgICAgICAgICBjb2xvcj1iYS5hcHAudWkudGl0bGVfY29sb3Isdl9hbGlnbj0iY2VudGVyIixtYXh3aWR0aD13aWR0aCoxLjMsc2NhbGU9MS40KQogICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBiID0gYmEuYnV0dG9ud2lkZ2V0KHBhcmVudD1jLGF1dG9zZWxlY3Q9VHJ1ZSxwb3NpdGlvbj1wb3NpdGlvbixzaXplPSgxMDAsMTAwKSxyZXBlYXQ9VHJ1ZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2NhbGU9MC42LGxhYmVsPXNlbGYuY2hhcnN0clszXSxidXR0b25fdHlwZT0nc3F1YXJlJyx0ZXh0X3NjYWxlPTIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG9uX2FjdGl2YXRlX2NhbGw9YmEuQ2FsbChzZWxmLmhlYWx0aF9kYW1hZ2VfcGVyY2VudGFnZSwnRGVjcmVtZW50JykpCiAgICAKICAgICAgICAgICAgICAgIGIgPSBiYS5idXR0b253aWRnZXQocGFyZW50PWMsYXV0b3NlbGVjdD1UcnVlLHJlcGVhdD1UcnVlLHRleHRfc2NhbGU9MiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcG9zaXRpb249KHBvc2l0aW9uWzBdKjMuMixwb3NpdGlvblsxXSksc2l6ZT0oMTAwLDEwMCksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNjYWxlPTAuNixsYWJlbD1zZWxmLmNoYXJzdHJbMl0sYnV0dG9uX3R5cGU9J3NxdWFyZScsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG9uX2FjdGl2YXRlX2NhbGw9YmEuQ2FsbChzZWxmLmhlYWx0aF9kYW1hZ2VfcGVyY2VudGFnZSwnSW5jcmVtZW50JykpCiAgICAKICAgICAgICAgICAgICAgIHBvcmNlbnRhamUgPSBjb25maWdbJ0hlYWxpbmcgRGFtYWdlIFBURyddCiAgICAgICAgICAgICAgICBpZiBwb3JjZW50YWplID4gNTk6IGNvbG9yID0gKDAsMSwwKQogICAgICAgICAgICAgICAgZWxpZiBwb3JjZW50YWplIDwgNDA6IGNvbG9yID0gKDEsMSwwKQogICAgICAgICAgICAgICAgZWxzZTogY29sb3IgPSAoMCwxLDAuOCkKICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgc2VsZi5obGdfdGV4dCA9IGJhLnRleHR3aWRnZXQocGFyZW50PWMscG9zaXRpb249KHBvc2l0aW9uWzBdLTE0LHBvc2l0aW9uWzFdKzUpLAogICAgICAgICAgICAgICAgICAgIHNpemU9KDMwK3dpZHRoLDUwKSxoX2FsaWduPSJjZW50ZXIiLAogICAgICAgICAgICAgICAgICAgIHRleHQ9c3RyKHBvcmNlbnRhamUpKyclJyxjb2xvcj1jb2xvciwKICAgICAgICAgICAgICAgICAgICB2X2FsaWduPSJjZW50ZXIiLG1heHdpZHRoPXdpZHRoKjEuMyxzY2FsZT0yKQoKICAgICAgICBlbGlmIHRhYiA9PSAnUGVyY2VudGFnZSc6CiAgICAgICAgICAgIHN1Yl9oZWlnaHQgPSBsZW4oc2VsZi5kZWZhdWx0X3Bvd2VyX2xpc3QpICogOTAKICAgICAgICAgICAgdiA9IHN1Yl9oZWlnaHQgLSA1NQogICAgICAgICAgICB3aWR0aCA9IDMwMAogICAgICAgICAgICBwb3NpID0gMAogICAgICAgICAgICBpZF9wb3dlciA9IGxpc3Qoc2VsZi5kZWZhdWx0X3Bvd2VydXBzKQogICAgICAgICAgICBuZXdfcG93ZXJ1cHMgPSBpZF9wb3dlcls5Ol0KICAgICAgICAgICAgc2VsZi5saXN0cG93ZXIgPSB7fQogICAgICAgICAgICAKICAgICAgICAgICAgc2VsZi5fdGFiX2NvbnRhaW5lciA9IGMgPSBiYS5jb250YWluZXJ3aWRnZXQocGFyZW50PXNlbGYuX3Njcm9sbHdpZGdldCwKICAgICAgICAgICAgICAgIHNpemU9KHNlbGYuX3N1Yl93aWR0aCxzdWJfaGVpZ2h0KSwKICAgICAgICAgICAgICAgIGJhY2tncm91bmQ9RmFsc2Usc2VsZWN0aW9uX2xvb3BzX3RvX3BhcmVudD1UcnVlKQogCiAgICAgICAgICAgIGZvciBwb3dlciBpbiBzZWxmLmRlZmF1bHRfcG93ZXJfbGlzdDoKICAgICAgICAgICAgICAgIGlmIHBvd2VyID09IGlkX3Bvd2VyWzBdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSAnaGVscFdpbmRvdy5wb3dlcnVwU2hpZWxkTmFtZVRleHQnCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgncG93ZXJ1cFNoaWVsZCcpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzFdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSAnaGVscFdpbmRvdy5wb3dlcnVwUHVuY2hOYW1lVGV4dCcKICAgICAgICAgICAgICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdwb3dlcnVwUHVuY2gnKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlclsyXToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gJ2hlbHBXaW5kb3cucG93ZXJ1cExhbmRNaW5lc05hbWVUZXh0JwogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ3Bvd2VydXBMYW5kTWluZXMnKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlclszXToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gJ2hlbHBXaW5kb3cucG93ZXJ1cEltcGFjdEJvbWJzTmFtZVRleHQnCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgncG93ZXJ1cEltcGFjdEJvbWJzJykKICAgICAgICAgICAgICAgIGVsaWYgcG93ZXIgPT0gaWRfcG93ZXJbNF06CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9ICdoZWxwV2luZG93LnBvd2VydXBJY2VCb21ic05hbWVUZXh0JwogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ3Bvd2VydXBJY2VCb21icycpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzVdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSAnaGVscFdpbmRvdy5wb3dlcnVwQm9tYk5hbWVUZXh0JwogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ3Bvd2VydXBCb21iJykKICAgICAgICAgICAgICAgIGVsaWYgcG93ZXIgPT0gaWRfcG93ZXJbNl06CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9ICdoZWxwV2luZG93LnBvd2VydXBTdGlja3lCb21ic05hbWVUZXh0JwogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ3Bvd2VydXBTdGlja3lCb21icycpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzddOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSAnaGVscFdpbmRvdy5wb3dlcnVwQ3Vyc2VOYW1lVGV4dCcKICAgICAgICAgICAgICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdwb3dlcnVwQ3Vyc2UnKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlcls4XToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gJ2hlbHBXaW5kb3cucG93ZXJ1cEhlYWx0aE5hbWVUZXh0JwogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ3Bvd2VydXBIZWFsdGgnKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlcls5XToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gcG93ZXIKICAgICAgICAgICAgICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdwb3dlcnVwU3BlZWQnKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlclsxMF06CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9IHBvd2VyCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgnaGVhcnQnKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlclsxMV06CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9ICJHb29kYnllISIKICAgICAgICAgICAgICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdhY2hpZXZlbWVudE9uc2xhdWdodCcpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzEyXToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gcG93ZXIKICAgICAgICAgICAgICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdvdXlhVUJ1dHRvbicpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzEzXToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gcG93ZXIKICAgICAgICAgICAgICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdhY2hpZXZlbWVudFN1cGVyUHVuY2gnKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlclsxNF06CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9IHBvd2VyCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgnbGV2ZWxJY29uJykKICAgICAgICAgICAgICAgIGVsaWYgcG93ZXIgPT0gaWRfcG93ZXJbMTVdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSBwb3dlcgogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ291eWFPQnV0dG9uJykKICAgICAgICAgICAgICAgIGVsaWYgcG93ZXIgPT0gaWRfcG93ZXJbMTZdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSBwb3dlcgogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ3N0YXInKQogICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgaWYgcG93ZXIgaW4gbmV3X3Bvd2VydXBzOiBsYWJlbCA9IGdldGxhbmd1YWdlKHBvd2VyKQogICAgICAgICAgICAgICAgZWxzZTogbGFiZWwgPSBiYS5Mc3RyKHJlc291cmNlPXRleHQpCgogICAgICAgICAgICAgICAgYXBwZXJhbmNlID0gcG93ZXJ1cHNbcG93ZXJdCiAgICAgICAgICAgICAgICBwb3NpdGlvbiA9ICg5MCx2LXBvc2kpCiAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIHQgPSBiYS50ZXh0d2lkZ2V0KHBhcmVudD1jLHBvc2l0aW9uPShwb3NpdGlvblswXS0zMCxwb3NpdGlvblsxXS0xNSksc2l6ZT0od2lkdGgsNTApLAogICAgICAgICAgICAgICAgICAgICAgICAgIGhfYWxpZ249ImNlbnRlciIsY29sb3I9KGJhLmFwcC51aS50aXRsZV9jb2xvciksIHRleHQ9bGFiZWwsIHZfYWxpZ249ImNlbnRlciIsbWF4d2lkdGg9d2lkdGgqMS4zKQogICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIHNlbGYucG93cHJldiA9IGJhLmltYWdld2lkZ2V0KHBhcmVudD1jLAogICAgICAgICAgICAgICAgICAgIHBvc2l0aW9uPShwb3NpdGlvblswXS03MCxwb3NpdGlvblsxXS0xMCksCiAgICAgICAgICAgICAgICAgICAgc2l6ZT0oNTAsNTApLHRleHR1cmU9dGV4KQogICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIHB0ZyA9IHN0cihzZWxmLnRvdGFsX3BlcmNlbnRhZ2UocG93ZXIpKQogICAgICAgICAgICAgICAgdCA9IGJhLnRleHR3aWRnZXQocGFyZW50PWMscG9zaXRpb249KHBvc2l0aW9uWzBdKzE3MCxwb3NpdGlvblsxXS0xMCksc2l6ZT0od2lkdGgsNTApLAogICAgICAgICAgICAgICAgICAgIGhfYWxpZ249ImNlbnRlciIsY29sb3I9KDAsMSwwKSx0ZXh0PShmJ3twdGd9JScpLHZfYWxpZ249ImNlbnRlciIsbWF4d2lkdGg9d2lkdGgqMS4zKQogICAgICAgICAKICAgICAgICAgICAgICAgIHBvc2kgKz0gOTAKICAgICAgICAgICAgICAgIAogICAgICAgIGVsaWYgdGFiID09ICdBY3Rpb24gNCc6CiAgICAgICAgICAgIHN1Yl9oZWlnaHQgPSAzNzAKICAgICAgICAgICAgd2lkdGggPSAzMDAKICAgICAgICAgICAgdiA9IHN1Yl9oZWlnaHQgLSA1NQogICAgICAgICAgICB1ID0gd2lkdGggLSA2MAogICAgICAgICAgICAKICAgICAgICAgICAgc2VsZi5fdGFiX2NvbnRhaW5lciA9IGMgPSBiYS5jb250YWluZXJ3aWRnZXQocGFyZW50PXNlbGYuX3Njcm9sbHdpZGdldCwKICAgICAgICAgICAgICAgIHNpemU9KHdpZHRoKzUwMCxzdWJfaGVpZ2h0KSwKICAgICAgICAgICAgICAgIGJhY2tncm91bmQ9RmFsc2Usc2VsZWN0aW9uX2xvb3BzX3RvX3BhcmVudD1UcnVlKQogICAgICAgICAgICAgICAKICAgICAgICAgICAgcG9zaXRpb24gPSAodSsxNTAsdi0yNTApCiAgICAgICAgICAgIG5fcG9zID0gMAogICAgICAgICAgICBwcmljZXMgPSBbNzU2MCwgNTE1MCwgMzM2MF0KICAgICAgICAgICAgc3RyX25hbWUgPSBbIkZpcmVCb21icyBTdG9yZSIsIlRpbWVyIFN0b3JlIiwiUGVyY2VudGFnZXMgU3RvcmUiXQogICAgICAgICAgICBpbWFnZXMgPSBbIm91eWFPQnV0dG9uIiwic2V0dGluZ3NJY29uIiwiaW52ZW50b3J5SWNvbiJdCiAgICAgICAgICAgIAogICAgICAgICAgICBpbmRleCA9IDAKICAgICAgICAgICAgZm9yIHN0b3JlIGluIHN0b3JlX2l0ZW1zKCk6CiAgICAgICAgICAgICAgICBwID0gcHJpY2VzW2luZGV4XQogICAgICAgICAgICAgICAgdHh0ID0gc3RyX25hbWVbaW5kZXhdCiAgICAgICAgICAgICAgICBsYWJlbCA9IGdldGxhbmd1YWdlKHR4dCkKICAgICAgICAgICAgICAgIHR4X3BvcyA9IGxlbihsYWJlbCkqMS44CiAgICAgICAgICAgICAgICBsYl9zY2FsZSA9IGxlbihsYWJlbCkqMC4yMAogICAgICAgICAgICAgICAgcHJldmlldyA9IGltYWdlc1tpbmRleF0KICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgaWYgU1RPUkVbc3RvcmVdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSBnZXRsYW5ndWFnZSgnQm91Z2h0JykKICAgICAgICAgICAgICAgICAgICBpY29uID0gYmEuZ2V0dGV4dHVyZSgnZ3JhcGhpY3NJY29uJykKICAgICAgICAgICAgICAgICAgICBjb2xvciA9ICgwLjUyLDAuNDgsMC42MykKICAgICAgICAgICAgICAgICAgICB0eHRfc2NhbGUgPSAxLjUKICAgICAgICAgICAgICAgIGVsc2U6CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9IHN0cihwKQogICAgICAgICAgICAgICAgICAgIGljb24gPSBiYS5nZXR0ZXh0dXJlKCdjb2luJykKICAgICAgICAgICAgICAgICAgICBjb2xvciA9ICgwLjUsMC40LDAuOTMpCiAgICAgICAgICAgICAgICAgICAgdHh0X3NjYWxlID0gMgogICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBiID0gYmEuYnV0dG9ud2lkZ2V0KHBhcmVudD1jLGF1dG9zZWxlY3Q9VHJ1ZSxwb3NpdGlvbj0ocG9zaXRpb25bMF0rMjEwLW5fcG9zLHBvc2l0aW9uWzFdKSwKICAgICAgICAgICAgICAgICAgICAgICAgc2l6ZT0oMjUwLDgwKSxzY2FsZT0wLjcsbGFiZWw9dGV4dCx0ZXh0X3NjYWxlPXR4dF9zY2FsZSxpY29uPWljb24sY29sb3I9Y29sb3IsCiAgICAgICAgICAgICAgICAgICAgICAgIGljb25zY2FsZT0xLjcsb25fYWN0aXZhdGVfY2FsbD1iYS5DYWxsKHNlbGYuX2J1eV9vYmplY3Qsc3RvcmUscCkpCgogICAgICAgICAgICAgICAgcyA9IDE4MAogICAgICAgICAgICAgICAgYiA9IGJhLmJ1dHRvbndpZGdldChwYXJlbnQ9YyxhdXRvc2VsZWN0PVRydWUscG9zaXRpb249KHBvc2l0aW9uWzBdKzIxMC1uX3Bvcyxwb3NpdGlvblsxXSs1NSksCiAgICAgICAgICAgICAgICAgICAgICAgIHNpemU9KHMscyszMCksc2NhbGU9MSxsYWJlbD0nJyxjb2xvcj1jb2xvcixidXR0b25fdHlwZT0nc3F1YXJlJywKICAgICAgICAgICAgICAgICAgICAgICAgb25fYWN0aXZhdGVfY2FsbD1iYS5DYWxsKHNlbGYuX2J1eV9vYmplY3Qsc3RvcmUscCkpCiAgICAKICAgICAgICAgICAgICAgIHMgLT0gODAKICAgICAgICAgICAgICAgIGkgPSBiYS5pbWFnZXdpZGdldChwYXJlbnQ9YyxkcmF3X2NvbnRyb2xsZXI9YiwKICAgICAgICAgICAgICAgICAgICBwb3NpdGlvbj0ocG9zaXRpb25bMF0rMjUwLW5fcG9zLHBvc2l0aW9uWzFdKzE0MCksCiAgICAgICAgICAgICAgICAgICAgc2l6ZT0ocyxzKSx0ZXh0dXJlPWJhLmdldHRleHR1cmUocHJldmlldykpCiAgICAKICAgICAgICAgICAgICAgIHQgPSBiYS50ZXh0d2lkZ2V0KHBhcmVudD1jLHBvc2l0aW9uPShwb3NpdGlvblswXSsyNzAtbl9wb3MscG9zaXRpb25bMV0rMTAxKSwKICAgICAgICAgICAgICAgICAgICBoX2FsaWduPSJjZW50ZXIiLGNvbG9yPShiYS5hcHAudWkudGl0bGVfY29sb3IpLHRleHQ9bGFiZWwsdl9hbGlnbj0iY2VudGVyIixtYXh3aWR0aD0xMzApCiAgICAKICAgICAgICAgICAgICAgIG5fcG9zICs9IDI4MAogICAgICAgICAgICAgICAgaW5kZXggKz0gMQogICAgCiAgICAgICAgZWxpZiB0YWIgPT0gJ0FjdGlvbiA1JzoKICAgICAgICAgICAgc3ViX2hlaWdodCA9IDM3MAogICAgICAgICAgICB2ID0gc3ViX2hlaWdodCAtIDU1CiAgICAgICAgICAgIHdpZHRoID0gMzAwCiAgICAgICAgICAgIAogICAgICAgICAgICBzZWxmLl90YWJfY29udGFpbmVyID0gYyA9IGJhLmNvbnRhaW5lcndpZGdldChwYXJlbnQ9c2VsZi5fc2Nyb2xsd2lkZ2V0LAogICAgICAgICAgICAgICAgc2l6ZT0oc2VsZi5fc3ViX3dpZHRoLHN1Yl9oZWlnaHQpLGJhY2tncm91bmQ9RmFsc2UsCiAgICAgICAgICAgICAgICBzZWxlY3Rpb25fbG9vcHNfdG9fcGFyZW50PVRydWUpCiAgICAgICAgICAgICAgIAogICAgICAgICAgICBwb3NpdGlvbiA9ICgwLHYtMzApCiAgICAKICAgICAgICAgICAgdCA9IGJhLnRleHR3aWRnZXQocGFyZW50PWMscG9zaXRpb249KHBvc2l0aW9uWzBdKzgwLHBvc2l0aW9uWzFdLTMwKSxzaXplPSh3aWR0aCs2MCw1MCksc2NhbGU9MSwKICAgICAgICAgICAgICAgIGhfYWxpZ249ImNlbnRlciIsY29sb3I9KGJhLmFwcC51aS50aXRsZV9jb2xvciksdGV4dD1iYS5Mc3RyKAogICAgICAgICAgICAgICAgcmVzb3VyY2U9J3NldHRpbmdzV2luZG93QWR2YW5jZWQuZW50ZXJQcm9tb0NvZGVUZXh0Jyksdl9hbGlnbj0iY2VudGVyIixtYXh3aWR0aD13aWR0aCoxLjMpCiAgICAKICAgICAgICAgICAgc2VsZi5wcm9tb2NvZGVfdGV4dCA9IGJhLnRleHR3aWRnZXQocGFyZW50PWMscG9zaXRpb249KHBvc2l0aW9uWzBdKzgwLHBvc2l0aW9uWzFdLTEwMCksc2l6ZT0od2lkdGgrNjAsNTApLHNjYWxlPTEsCiAgICAgICAgICAgICAgICBlZGl0YWJsZT1UcnVlLGhfYWxpZ249ImNlbnRlciIsY29sb3I9KGJhLmFwcC51aS50aXRsZV9jb2xvciksdGV4dD0nJywgdl9hbGlnbj0iY2VudGVyIixtYXh3aWR0aD13aWR0aCoxLjMsCiAgICAgICAgICAgICAgICBtYXhfY2hhcnM9MzAsZGVzY3JpcHRpb249YmEuTHN0cihyZXNvdXJjZT0nc2V0dGluZ3NXaW5kb3dBZHZhbmNlZC5lbnRlclByb21vQ29kZVRleHQnKSkKICAgICAgICAgICAgICAgIAogICAgICAgICAgICBzZWxmLnByb21vY29kZV9idXR0b24gPSBiYS5idXR0b253aWRnZXQoCiAgICAgICAgICAgICAgICBwYXJlbnQ9Yyxwb3NpdGlvbj0ocG9zaXRpb25bMF0rMTYwLHBvc2l0aW9uWzFdLTE3MCksCiAgICAgICAgICAgICAgICBzaXplPSgyMDAsIDYwKSxzY2FsZT0xLjAsbGFiZWw9YmEuTHN0cihyZXNvdXJjZT0nc3VibWl0VGV4dCcpLAogICAgICAgICAgICAgICAgb25fYWN0aXZhdGVfY2FsbD1zZWxmLl9wcm9tb2NvZGUpCiAgICAKICAgICAgICBlbHNlOgogICAgICAgICAgICBzdWJfaGVpZ2h0ID0gMAogICAgICAgICAgICB2ID0gc3ViX2hlaWdodCAtIDU1CiAgICAgICAgICAgIHdpZHRoID0gMzAwCiAgICAgICAgICAgIAogICAgICAgICAgICBzZWxmLl90YWJfY29udGFpbmVyID0gYyA9IGJhLmNvbnRhaW5lcndpZGdldChwYXJlbnQ9c2VsZi5fc2Nyb2xsd2lkZ2V0LAogICAgICAgICAgICAgICAgc2l6ZT0oc2VsZi5fc3ViX3dpZHRoLHN1Yl9oZWlnaHQpLAogICAgICAgICAgICAgICAgYmFja2dyb3VuZD1GYWxzZSxzZWxlY3Rpb25fbG9vcHNfdG9fcGFyZW50PVRydWUpCgogICAgICAgICAgICB0ID0gYmEudGV4dHdpZGdldChwYXJlbnQ9Yyxwb3NpdGlvbj0oMTEwLCB2LTIwKSxzaXplPSh3aWR0aCw1MCksCiAgICAgICAgICAgICAgICAgICAgICBzY2FsZT0xLjQsY29sb3I9KDAuMiwxLjIsMC4yKSxoX2FsaWduPSJjZW50ZXIiLHZfYWxpZ249ImNlbnRlciIsCiAgICAgICAgICAgICAgICAgICAgICB0ZXh0PSgiVWx0aW1hdGUgUG93ZXJ1cCBNYW5hZ2VyIHYxLjciKSxtYXh3aWR0aD13aWR0aCozMCkKCiAgICAgICAgICAgIHQgPSBiYS50ZXh0d2lkZ2V0KHBhcmVudD1jLHBvc2l0aW9uPSgxMTAsIHYtOTApLHNpemU9KHdpZHRoLDUwKSwKICAgICAgICAgICAgICAgICAgICAgIHNjYWxlPTEsY29sb3I9KDEuMywwLjUsMS4wKSxoX2FsaWduPSJjZW50ZXIiLHZfYWxpZ249ImNlbnRlciIsCiAgICAgICAgICAgICAgICAgICAgICB0ZXh0PWdldGxhbmd1YWdlKCdDcmVhdG9yJyksbWF4d2lkdGg9d2lkdGgqMzApCgogICAgICAgICAgICB0ID0gYmEudGV4dHdpZGdldChwYXJlbnQ9Yyxwb3NpdGlvbj0oMTEwLCB2LTIyMCksc2l6ZT0od2lkdGgsNTApLAogICAgICAgICAgICAgICAgICAgICAgc2NhbGU9MSxjb2xvcj0oMS4wLDEuMiwwLjMpLGhfYWxpZ249ImNlbnRlciIsdl9hbGlnbj0iY2VudGVyIiwKICAgICAgICAgICAgICAgICAgICAgIHRleHQ9Z2V0bGFuZ3VhZ2UoJ01vZCBJbmZvJyksbWF4d2lkdGg9d2lkdGgqMzApCiAgICAKICAgICAgICBmb3Igc2VsZWN0X3RhYixidXR0b25fdGFiIGluIHNlbGYudGFiX2J1dHRvbnMuaXRlbXMoKToKICAgICAgICAgICAgaWYgc2VsZWN0X3RhYiA9PSB0YWI6CiAgICAgICAgICAgICAgICBiYS5idXR0b253aWRnZXQoZWRpdD1idXR0b25fdGFiLGNvbG9yPSgwLjUsMC40LDEuNSkpCiAgICAgICAgICAgIGVsc2U6IGJhLmJ1dHRvbndpZGdldChlZGl0PWJ1dHRvbl90YWIsY29sb3I9KDAuNTIsMC40OCwwLjYzKSkKCiAgICBkZWYgX2FsbF9wb3B1cChzZWxmLCB0YWc6IHN0ciwgcG9wdXA6IHN0cikgLT4gTm9uZToKICAgICAgICBjb25maWdbdGFnXSA9IHBvcHVwCiAgICAgICAgYXBnLmFwcGx5X2FuZF9jb21taXQoKQoKICAgIGRlZiBfc2V0X2NvbmNlcHQoc2VsZiwgY29uY2VwdDogc3RyKSAtPiBOb25lOgogICAgICAgIEdMT0JBTFsnQ2xzIFBvd2VydXAnXSA9IGNvbmNlcHQKCiAgICAgICAgaWYgY29uY2VwdCA9PSAnUmVzZXQnOgogICAgICAgICAgICBmb3IgcG93ZXIsIGRlZmx0IGluIGRlZmF1bHRfcG93ZXJ1cHMoKS5pdGVtcygpOgogICAgICAgICAgICAgICAgcG93ZXJ1cHNbcG93ZXJdID0gZGVmbHQKICAgICAgICBlbGlmIGNvbmNlcHQgPT0gJ05vdGhpbmcnOgogICAgICAgICAgICBmb3IgcG93ZXIgaW4gZGVmYXVsdF9wb3dlcnVwcygpOgogICAgICAgICAgICAgICAgcG93ZXJ1cHNbcG93ZXJdID0gMAogICAgICAgIGVsaWYgY29uY2VwdCA9PSAnT25seSBCb21icyc6CiAgICAgICAgICAgIGZvciBwb3dlciwgZGVmbHQgaW4gZGVmYXVsdF9wb3dlcnVwcygpLml0ZW1zKCk6CiAgICAgICAgICAgICAgICBpZiAnQm9tYnMnIG5vdCBpbiBwb3dlcjoKICAgICAgICAgICAgICAgICAgICBwb3dlcnVwc1twb3dlcl0gPSAwCiAgICAgICAgICAgICAgICBlbHNlOiBwb3dlcnVwc1twb3dlcl0gPSAzCiAgICAgICAgZWxpZiBjb25jZXB0ID09ICdPbmx5IEl0ZW1zJzoKICAgICAgICAgICAgZm9yIHBvd2VyLCBkZWZsdCBpbiBkZWZhdWx0X3Bvd2VydXBzKCkuaXRlbXMoKToKICAgICAgICAgICAgICAgIGlmICdCb21icycgaW4gcG93ZXI6CiAgICAgICAgICAgICAgICAgICAgcG93ZXJ1cHNbcG93ZXJdID0gMAogICAgICAgICAgICAgICAgZWxzZTogcG93ZXJ1cHNbcG93ZXJdID0gZGVmbHQKICAgICAgICBlbGlmIGNvbmNlcHQgPT0gJ05ldyc6CiAgICAgICAgICAgIGRlZmF1bHRfcG93ZXIgPSBkZWZhdWx0X3Bvd2VydXBzKCkKICAgICAgICAgICAgbmV3X3Bvd2VydXBzID0gbGlzdChkZWZhdWx0X3Bvd2VyKVs5Ol0KICAgICAgICAgICAgZm9yIHBvd2VyLCBkZWZsdCBpbiBkZWZhdWx0X3Bvd2VyLml0ZW1zKCk6CiAgICAgICAgICAgICAgICBpZiBwb3dlciBub3QgaW4gbmV3X3Bvd2VydXBzOgogICAgICAgICAgICAgICAgICAgIHBvd2VydXBzW3Bvd2VyXSA9IDAKICAgICAgICAgICAgICAgIGVsc2U6IHBvd2VydXBzW3Bvd2VyXSA9IGRlZmx0CgogICAgICAgIGlmIG5vdCBTVE9SRVsnQnV5IEZpcmVib21icyddOgogICAgICAgICAgICBwb3dlcnVwc1snRmlyZSBCb21icyddID0gMAogICAgICAgICAgICAKICAgICAgICBzZWxmLl9zZXRfdGFiKCdBY3Rpb24gMScpCgogICAgZGVmIHRhbmtfc2hpZWxkX3BlcmNlbnRhZ2Uoc2VsZiwgdGFnKToKICAgICAgICBtYXggPSA5NgogICAgICAgIG1pbiA9IDQwCiAgICAgICAgaWYgdGFnID09ICdJbmNyZW1lbnQnOgogICAgICAgICAgICBjb25maWdbJ1RhbmsgU2hpZWxkIFBURyddICs9IDEKICAgICAgICAgICAgaWYgY29uZmlnWydUYW5rIFNoaWVsZCBQVEcnXSA+IG1heDoKICAgICAgICAgICAgICAgIGNvbmZpZ1snVGFuayBTaGllbGQgUFRHJ10gPSBtaW4KICAgICAgICBlbGlmIHRhZyA9PSAnRGVjcmVtZW50JzoKICAgICAgICAgICAgY29uZmlnWydUYW5rIFNoaWVsZCBQVEcnXSAtPSAxCiAgICAgICAgICAgIGlmIGNvbmZpZ1snVGFuayBTaGllbGQgUFRHJ10gPCBtaW46CiAgICAgICAgICAgICAgICBjb25maWdbJ1RhbmsgU2hpZWxkIFBURyddID0gbWF4CiAgICAgICAgICAgICAgICAKICAgICAgICBwb3JjZW50YWplID0gY29uZmlnWydUYW5rIFNoaWVsZCBQVEcnXQogICAgICAgIGlmIHBvcmNlbnRhamUgPiA1OTogY29sb3IgPSAoMCwxLDApCiAgICAgICAgZWxpZiBwb3JjZW50YWplIDwgNDA6IGNvbG9yID0gKDEsMSwwKQogICAgICAgIGVsc2U6IGNvbG9yID0gKDAsMSwwLjgpCiAgICAgICAgYmEudGV4dHdpZGdldChlZGl0PXNlbGYudGFua190ZXh0LAogICAgICAgICAgICB0ZXh0PXN0cihwb3JjZW50YWplKSsnJScsY29sb3I9Y29sb3IpCgogICAgZGVmIGhlYWx0aF9kYW1hZ2VfcGVyY2VudGFnZShzZWxmLCB0YWcpOgogICAgICAgIG1heCA9IDgwCiAgICAgICAgbWluID0gMzUKICAgICAgICBpZiB0YWcgPT0gJ0luY3JlbWVudCc6CiAgICAgICAgICAgIGNvbmZpZ1snSGVhbGluZyBEYW1hZ2UgUFRHJ10gKz0gMQogICAgICAgICAgICBpZiBjb25maWdbJ0hlYWxpbmcgRGFtYWdlIFBURyddID4gbWF4OgogICAgICAgICAgICAgICAgY29uZmlnWydIZWFsaW5nIERhbWFnZSBQVEcnXSA9IG1pbgogICAgICAgIGVsaWYgdGFnID09ICdEZWNyZW1lbnQnOgogICAgICAgICAgICBjb25maWdbJ0hlYWxpbmcgRGFtYWdlIFBURyddIC09IDEKICAgICAgICAgICAgaWYgY29uZmlnWydIZWFsaW5nIERhbWFnZSBQVEcnXSA8IG1pbjoKICAgICAgICAgICAgICAgIGNvbmZpZ1snSGVhbGluZyBEYW1hZ2UgUFRHJ10gPSBtYXgKICAgICAgICAgICAgICAgIAogICAgICAgIHBvcmNlbnRhamUgPSBjb25maWdbJ0hlYWxpbmcgRGFtYWdlIFBURyddCiAgICAgICAgaWYgcG9yY2VudGFqZSA+IDU5OiBjb2xvciA9ICgwLDEsMCkKICAgICAgICBlbGlmIHBvcmNlbnRhamUgPCA0MDogY29sb3IgPSAoMSwxLDApCiAgICAgICAgZWxzZTogY29sb3IgPSAoMCwxLDAuOCkKICAgICAgICBiYS50ZXh0d2lkZ2V0KGVkaXQ9c2VsZi5obGdfdGV4dCwKICAgICAgICAgICAgdGV4dD1zdHIocG9yY2VudGFqZSkrJyUnLGNvbG9yPWNvbG9yKQoKICAgIGRlZiBhcHBlcmFuY2VfcG93ZXJ1cHMoc2VsZiwgcG93ZXJ1cDogc3RyLCBJRDogc3RyKToKICAgICAgICBtYXggPSA3CiAgICAgICAgaWYgSUQgPT0gIi0iOgogICAgICAgICAgICBpZiBwb3dlcnVwc1twb3dlcnVwXSA9PSAwOgogICAgICAgICAgICAgICAgcG93ZXJ1cHNbcG93ZXJ1cF0gPSBtYXgKICAgICAgICAgICAgZWxzZTogcG93ZXJ1cHNbcG93ZXJ1cF0gLT0gMQogICAgICAgIGVsaWYgSUQgPT0gIisiOgogICAgICAgICAgICBpZiBwb3dlcnVwc1twb3dlcnVwXSA9PSBtYXg6CiAgICAgICAgICAgICAgICBwb3dlcnVwc1twb3dlcnVwXSA9IDAKICAgICAgICAgICAgZWxzZTogcG93ZXJ1cHNbcG93ZXJ1cF0gKz0gMQogICAgICAgIGVudW0gPSBwb3dlcnVwc1twb3dlcnVwXQogICAgICAgIGJhLnRleHR3aWRnZXQoZWRpdD1zZWxmLmxpc3Rwb3dlcltwb3dlcnVwXSwKICAgICAgICAgICAgICAgICAgICAgIHRleHQ9c3RyKHBvd2VydXBzW3Bvd2VydXBdKSwKICAgICAgICAgICAgICAgICAgICAgIGNvbG9yPWNsc19wb3dfY29sb3IoKVtlbnVtXSkKICAgICAgICAgICAKICAgIGRlZiBfcG93ZXJ1cHNfc2NhbGUoc2VsZiwgSUQ6IHN0cik6CiAgICAgICAgbWF4ID0gMS41CiAgICAgICAgbWluID0gMC41CiAgICAgICAgc2MgPSAwLjEKICAgICAgICBpZiBJRCA9PSAiLSI6CiAgICAgICAgICAgIGlmIGNvbmZpZ1snUG93ZXJ1cCBTY2FsZSddIDwgKG1pbiswLjEpOgogICAgICAgICAgICAgICAgY29uZmlnWydQb3dlcnVwIFNjYWxlJ10gPSBtYXgKICAgICAgICAgICAgZWxzZTogY29uZmlnWydQb3dlcnVwIFNjYWxlJ10gLT0gc2MKICAgICAgICBlbGlmIElEID09ICIrIjoKICAgICAgICAgICAgaWYgY29uZmlnWydQb3dlcnVwIFNjYWxlJ10gPiAobWF4LTAuMSk6CiAgICAgICAgICAgICAgICBjb25maWdbJ1Bvd2VydXAgU2NhbGUnXSA9IG1pbgogICAgICAgICAgICBlbHNlOiBjb25maWdbJ1Bvd2VydXAgU2NhbGUnXSArPSBzYwogICAgICAgIGNvbmZpZ1snUG93ZXJ1cCBTY2FsZSddID0gcm91bmQoY29uZmlnWydQb3dlcnVwIFNjYWxlJ10sMSkKICAgICAgICBiYS50ZXh0d2lkZ2V0KGVkaXQ9c2VsZi50eHRfc2NhbGUsCiAgICAgICAgICAgICAgICAgICAgICB0ZXh0PXN0cihjb25maWdbJ1Bvd2VydXAgU2NhbGUnXSkpCiAgICAgICAgICAgCiAgICBkZWYgdG90YWxfcGVyY2VudGFnZShzZWxmLCBwb3dlcik6CiAgICAgICAgdG90YWwgPSAwCiAgICAgICAgcHcgPSBwb3dlcnVwc1twb3dlcl0KICAgICAgICBmb3IgaSxpMiBpbiBwb3dlcnVwcy5pdGVtcygpOgogICAgICAgICAgICB0b3RhbCArPSBpMgogICAgICAgIGlmIHRvdGFsID09IDA6CiAgICAgICAgICAgIHJldHVybiBmbG9hdCh0b3RhbCkKICAgICAgICBlbHNlOgogICAgICAgICAgICBwdGcgPSAoMTAwKnB3L3RvdGFsKQogICAgICAgICAgICByZXN1bHQgPSByb3VuZChwdGcsMikKICAgICAgICAgICAgcmV0dXJuIHJlc3VsdAogICAgICAgICAgIAogICAgZGVmIHN0b3JlX3JlZnJlc2goc2VsZiwgdGFnOiBzdHIpOgogICAgICAgIGlmIHRhZyA9PSAnQnV5IEZpcmVib21icyc6CiAgICAgICAgICAgIHBvd2VydXBzWydGaXJlIEJvbWJzJ10gPSAzCiAgICAgICAgICAgIHNlbGYuZGVmYXVsdF9wb3dlcl9saXN0LmFwcGVuZCgnRmlyZSBCb21icycpCiAgICAgICAgc2VsZi5fc2V0X3RhYignQWN0aW9uIDQnKQogICAgICAgICAgIAogICAgZGVmIF9idXlfb2JqZWN0KHNlbGYsIHRhZzogc3RyLCBwcmljZTogaW50KToKICAgICAgICBzdG9yZSA9IEJlYXJTdG9yZSh2YWx1ZT10YWcsIHByaWNlPXByaWNlLAogICAgICAgICAgICAgICAgY2FsbGJhY2s9YmEuQ2FsbChzZWxmLnN0b3JlX3JlZnJlc2gsdGFnKSkKICAgICAgICBzdG9yZS5idXkoKQogICAgICAgICAgIAogICAgZGVmIF9wcm9tb2NvZGUoc2VsZik6CiAgICAgICAgY29kZSA9IGJhLnRleHR3aWRnZXQocXVlcnk9c2VsZi5wcm9tb2NvZGVfdGV4dCkKICAgICAgICBwcm9tbyA9IFByb21vQ29kZShjb2RlPWNvZGUpCiAgICAgICAgcHJvbW8uY29kZV9jb25maXJtYXRpb24oKQogICAgICAgIGJhLnRleHR3aWRnZXQoZWRpdD1zZWxmLnByb21vY29kZV90ZXh0LHRleHQ9IiIpCiAgICAgICAgICAgCiAgICBkZWYgX3N3aXRjaGVzKHNlbGYsdGFnLG0pOgogICAgICAgIGNvbmZpZ1t0YWddID0gRmFsc2UgaWYgbT09MCBlbHNlIFRydWUKICAgICAgICBhcGcuYXBwbHlfYW5kX2NvbW1pdCgpCiAgICAgICAgICAgCiAgICBkZWYgX3BlcmNlbnRhZ2Vfd2luZG93KHNlbGYpOgogICAgICAgIHNlbGYuX3NldF90YWIoJ1BlcmNlbnRhZ2UnKQogICAgICAgICAgIAogICAgZGVmIF9iYWNrKHNlbGYpOgogICAgICAgIGJhLmNvbnRhaW5lcndpZGdldChlZGl0PXNlbGYuX3Jvb3Rfd2lkZ2V0LHRyYW5zaXRpb249J291dF9sZWZ0JykKICAgICAgICBicm93c2VyLlByb2ZpbGVCcm93c2VyV2luZG93KCkK").decode("utf-8")) +import ba,_ba,random,time,datetime,weakref,json +import ba.internal +from bastd.ui.profile import browser +from bastd.actor import bomb +from bastd.actor import powerupbox as pupbox +from bastd.actor.spazbot import SpazBot +from bastd.actor.bomb import (Bomb,Blast) +from bastd.ui.popup import (PopupWindow,PopupMenuWindow,PopupMenu) +from bastd.actor.spaz import (Spaz,SpazFactory,PickupMessage, PunchHitMessage, + CurseExplodeMessage, BombDiedMessage) +from bastd.mainmenu import (MainMenuActivity,MainMenuSession) +from bastd.gameutils import SharedObjects +from bastd.actor.powerupbox import PowerupBoxFactory +from bastd.actor.popuptext import PopupText +from bastd.ui.confirm import ConfirmWindow +from bastd.actor.spaz import * + +if TYPE_CHECKING: + pass + +# === Mod made by @Patron_Modz === + +def getlanguage(text, subs: str = None, almacen: list = []): + if almacen == []: almacen = list(range(1000)) + lang = _ba.app.lang.language + translate = {"Reset": + {"Spanish": "Reiniciar", + "English": "Reset", + "Portuguese": "Reiniciar"}, + "Nothing": + {"Spanish": "Sin potenciadores", + "English": "No powerups", + "Portuguese": "Sem powerups"}, + "Action 1": + {"Spanish": "Potenciadores", + "English": "Powerups", + "Portuguese": "Powerups"}, + "Action 2": + {"Spanish": "Configuración", + "English": "Settings", + "Portuguese": "Definições"}, + "Action 3": + {"Spanish": "Extras", + "English": "Extras", + "Portuguese": "Extras"}, + "Action 4": + {"Spanish": "Tienda", + "English": "Store", + "Portuguese": "Loja"}, + "Action 5": + {"Spanish": "Canjear código", + "English": "Enter Code", + "Portuguese": "Código promocional"}, + "Custom": + {"Spanish": "", + "English": "Customize", + "Portuguese": "Customizar"}, + "Impairment Bombs": + {"Spanish": "Bombas menoscabo", + "English": "Hyperactive bombs", + "Portuguese": "Bombas hiperativas"}, + "Speed": + {"Spanish": "Velocidad", + "English": "Speed", + "Portuguese": "Velocidade"}, + "Fire Bombs": + {"Spanish": "Bombas de fuego", + "English": "Fire Bombs", + "Portuguese": "Bombas de fogo"}, + "Ice Man": + {"Spanish": "Hombre de hielo", + "English": "Ice man", + "Portuguese": "Homem de gelo"}, + "Fly Bombs": + {"Spanish": "Bombas expansivas", + "English": "Expansive bombs", + "Portuguese": "Bombas expansivas"}, + "Goodbye": + {"Spanish": "¡Hasta luego!", + "English": "Goodbye!", + "Portuguese": "Adeus!"}, + "Healing Damage": + {"Spanish": "Auto-curación", + "English": "Healing Damage", + "Portuguese": "Auto-cura"}, + "Tank Shield": + {"Spanish": "Súper blindaje", + "English": "Reinforced shield", + "Portuguese": "Escudo reforçado"}, + "Tank Shield PTG": + {"Spanish": "Porcentaje de disminución", + "English": "Percentage decreased", + "Portuguese": "Percentual reduzido"}, + "Healing Damage PTG": + {"Spanish": "Porcentaje de recuperación de salud", + "English": "Percentage of health recovered", + "Portuguese": "Porcentagem de recuperação de saúde"}, + "SY: BALL": + {"Spanish": "Esfera", + "English": "Sphere", + "Portuguese": "Esfera"}, + "SY: Impact": + {"Spanish": "Especial", + "English": "Special", + "Portuguese": "Especial"}, + "SY: Egg": + {"Spanish": "Huevito", + "English": "Egg shape", + "Portuguese": "Ovo"}, + "Powerup Scale": + {"Spanish": "Tamaño del potenciador", + "English": "Powerups size", + "Portuguese": "Tamanho de powerups"}, + "Powerup With Shield": + {"Spanish": "Potenciadores con escudo", + "English": "Powerups with shield", + "Portuguese": "Powerups com escudo"}, + "Powerup Time": + {"Spanish": "Mostrar Temporizador", + "English": "Show end time", + "Portuguese": "Mostrar cronômetro"}, + "Powerup Style": + {"Spanish": "Forma de los potenciadores", + "English": "Shape of powerup", + "Portuguese": "Forma de powerup"}, + "Powerup Name": + {"Spanish": "Mostrar nombre en los potenciadores", + "English": "Show name on powerups", + "Portuguese": "Mostrar nome em powerups"}, + "Percentage": + {"Spanish": "Probabilidad", + "English": "Show percentage", + "Portuguese": "Mostrar porcentagem"}, + "Only Items": + {"Spanish": "Sólo Accesorios", + "English": "Only utensils", + "Portuguese": "Apenas utensilios"}, + "New": + {"Spanish": "Nuevo", + "English": "New", + "Portuguese": "Novo"}, + "Only Bombs": + {"Spanish": "Sólo Bombas", + "English": "Only bombs", + "Portuguese": "Apenas bombas"}, + "Coins 0": + {"Spanish": "Monedas Insuficientes", + "English": "Insufficient coins", + "Portuguese": "Moedas insuficientes"}, + "Purchase": + {"Spanish": "Compra realizada correctamente", + "English": "Successful purchase", + "Portuguese": "Compra Bem Sucedida"}, + "Double Product": + {"Spanish": "Ya has comprado este artículo", + "English": "You've already bought this", + "Portuguese": "Voce ja comprou isto"}, + "Bought": + {"Spanish": "Comprado", + "English": "Bought", + "Portuguese": "Comprou"}, + "Confirm Purchase": + {"Spanish": f'Tienes {subs} monedas. {_sp_} ¿Deseas comprar esto?', + "English": f'You have {subs} coins. {_sp_} Do you want to buy this?', + "Portuguese": f'Você tem {subs} moedas. {_sp_} Deseja comprar isto?'}, + "FireBombs Store": + {"Spanish": 'Bombas de fuego', + "English": 'Fire bombs', + "Portuguese": 'Bombas de incêndio'}, + "Timer Store": + {"Spanish": 'Temporizador', + "English": 'Timer', + "Portuguese": 'Timer'}, + "Percentages Store": + {"Spanish": 'Extras', + "English": 'Extras', + "Portuguese": 'Extras'}, + "Block Option Store": + {"Spanish": f"Uuups..{_sp_}Esta opción está bloqueada.{_sp_} Para acceder a ella puedes {_sp_} comprarla en la tienda.{_sp_} Gracias...", + "English": f"Oooops...{_sp_}This option is blocked. {_sp_} To access it you can buy {_sp_} it in the store.{_sp_} Thank you...", + "Portuguese": f"Ooops...{_sp_}Esta opção está bloqueada. {_sp_} Para acessá-lo, você pode {_sp_} comprá-lo na loja.{_sp_} Obrigado..."}, + "True Code": + {"Spanish": "¡Código canjeado!", + "English": "Successful code!", + "Portuguese": "¡Código válido!"}, + "False Code": + {"Spanish": "Código ya canjeado", + "English": "Expired code", + "Portuguese": "Código expirado"}, + "Invalid Code": + {"Spanish": "Código inválido", + "English": "Invalid code", + "Portuguese": "Código inválido"}, + "Reward Code": + {"Spanish": f"¡Felicitaciones! ¡Ganaste {subs} monedas!", + "English": f"Congratulations! You've {subs} coins", + "Portuguese": f"Parabéns! Você tem {subs} moedas"}, + "Creator": + {"Spanish": "Mod creado por @PatrónModz", + "English": "Mod created by @PatrónModz", + "Portuguese": "Mod creado by @PatrónModz"}, + "Mod Info": + {"Spanish": f"Un mod genial que te permite gestionar {_sp_} los potenciadores a tu antojo. {_sp_} también incluye 8 potenciadores extra{_sp_} dejando 17 en total... ¡Guay!", + "English": f"A cool mod that allows you to manage {_sp_} powerups at your whims. {_sp_} also includes 8 extra powerups{_sp_} leaving 17 in total... Wow!", + "Portuguese": f"Um mod legal que permite que você gerencie os{_sp_} powerups de de acordo com seus caprichos. {_sp_} também inclui 8 powerups extras,{_sp_} deixando 17 no total... Uau!"}, + "Coins Message": + {"Spanish": f"Recompensa: {subs} Monedas", + "English": f"Reward: {subs} Coins", + "Portuguese": f"Recompensa: {subs} Moedas"}, + "Coins Limit Message": + {"Spanish": f"Ganaste {almacen[0]} Monedas.{_sp_} Pero has superado el límite de {almacen[1]}", + "English": f"You won {almacen[0]} Coins. {_sp_} But you have exceeded the limit of {almacen[1]}", + "Portuguese": f"Você ganhou {almacen[0]} Moedas. {_sp_} Mas você excedeu o limite de {almacen[1]}"}, + } + languages = ['Spanish','Portuguese','English'] + if lang not in languages: lang = 'English' + + if text not in translate: + return text + + return translate[text][lang] + +import setting +settings=setting.get_settings_data() + +def settings_distribution(): + return settings["elPatronPowerups"]["settings"] + + + +apg = ba.app.config + +apg['PPM Settings'] = settings_distribution() + + +config = apg['PPM Settings'] + +def default_powerups(): + return settings["elPatronPowerups"]["Quantity"] + + +config['Powerups'] = default_powerups() + + +powerups = config['Powerups'] + +# === EXTRAS === + +GLOBAL = {"Tab": 'Action 1', + "Cls Powerup": 0, + "Coins Message": []} + +# === STORE === +def promo_codes(): + return {"G-Am54igO42Os": [True,1100], + "P-tRo8nM8dZ": [True,2800], + "Y-tU2B3S": [True,500], + "B-0mB3RYT2z": [True,910], + "B-Asd14mON9G0D": [True,910], + "D-rAcK0cJ23": [True,910], + "E-a27ZO6f3Y": [True,600], + "E-Am54igO42Os": [True,600], + "E-M4uN3K34XB": [True,840], + "PM-731ClcAF": [True,50000]} + +def store_items(): + return {"Buy Firebombs": False, + "Buy Option": False, + "Buy Percentage": False} + +if apg.get('Bear Coin') is None: + apg['Bear Coin'] = 0 + apg.apply_and_commit() + +if apg.get('Bear Coin') is not None: + if apg['Bear Coin'] <= 0: + apg['Bear Coin'] = 0 + apg['Bear Coin'] = int(apg['Bear Coin']) + +if apg.get('Bear Store') is None: + apg['Bear Store'] = {} + +for i,j in store_items().items(): + store = apg['Bear Store'] + if i not in store: + if store.get(i) is None: + store[i] = j + apg.apply_and_commit() + +STORE = apg['Bear Store'] + +if STORE.get('Promo Code') is None: + STORE['Promo Code'] = promo_codes() + +for i,x in promo_codes().items(): + pmcode = STORE['Promo Code'] + if i not in pmcode: + if pmcode.get(i) is None: + pmcode[i] = x + +apg.apply_and_commit() + +class BearStore: + def __init__(self, + price: int = 1000, + value: str = '', + callback: call = None): + + self.price = price + self.value = value + self.store = STORE[value] + self.coins = apg['Bear Coin'] + self.callback = callback + + def buy(self): + if not self.store: + if self.coins >= (self.price): + def confirm(): + STORE[self.value] = True + apg['Bear Coin'] -= int(self.price) + ba.screenmessage(getlanguage('Purchase'),(0,1,0)) + ba.playsound(ba.getsound('cashRegister')) + apg.apply_and_commit() + self.callback() + ConfirmWindow(getlanguage('Confirm Purchase',subs=self.coins), + width=400, height=120, action=confirm, ok_text=ba.Lstr(resource='okText')) + else: + ba.screenmessage(getlanguage('Coins 0'),(1,0,0)) + ba.playsound(ba.getsound('error')) + else: + ba.screenmessage(getlanguage('Double Product'),(1,0,0)) + ba.playsound(ba.getsound('error')) + + def __del__(self): + apg['Bear Coin'] = int(apg['Bear Coin']) + apg.apply_and_commit() + +class PromoCode: + def __init__(self, code: str = ''): + self.code = code + self.codes_store = STORE['Promo Code'] + if self.code in self.codes_store: + self.code_type = STORE['Promo Code'][code] + self.promo_code_expire = self.code_type[0] + self.promo_code_amount = self.code_type[1] + + def __del__(self): + apg['Bear Coin'] = int(apg['Bear Coin']) + apg.apply_and_commit() + + def code_confirmation(self): + if self.code != "": + ba.screenmessage( + ba.Lstr(resource='submittingPromoCodeText'),(0,1,0)) + ba.timer(2,ba.Call(self.validate_code)) + + def validate_code(self): + if self.code in self.codes_store: + if self.promo_code_expire: + ba.timer(1.5,ba.Call(self.successful_code)) + ba.screenmessage(getlanguage('True Code'),(0,1,0)) + ba.playsound(ba.getsound('cheer')) + self.code_type[0] = False + else: + ba.screenmessage(getlanguage('False Code'),(1,0,0)) + ba.playsound(ba.getsound('error')) + else: + ba.screenmessage(getlanguage('Invalid Code'),(1,0,0)) + ba.playsound(ba.getsound('error')) + + def successful_code(self): + apg['Bear Coin'] += self.promo_code_amount + ba.screenmessage(getlanguage('Reward Code', + subs=self.promo_code_amount),(0,1,0)) + ba.playsound(ba.getsound('cashRegister2')) + +MainMenuActivity.super_transition_in = MainMenuActivity.on_transition_in +def new_on_transition_in(self): + self.super_transition_in() + limit = 8400 + bear_coin = apg['Bear Coin'] + coins_message = GLOBAL['Coins Message'] + try: + if not (STORE['Buy Firebombs'] and + STORE['Buy Option'] and + STORE['Buy Percentage']): + + if coins_message != []: + result = 0 + for i in coins_message: + result += i + + if not bear_coin >= (limit-1): + ba.screenmessage(getlanguage('Coins Message',subs=result),(0,1,0)) + ba.playsound(ba.getsound('cashRegister')) + else: + ba.screenmessage(getlanguage('Coins Limit Message', + almacen=[result,limit]),(1,0,0)) + ba.playsound(ba.getsound('error')) + self.bear_coin_message = True + GLOBAL['Coins Message'] = [] + except: pass + +SpazBot.super_handlemessage = SpazBot.handlemessage +def bot_handlemessage(self, msg: Any): + self.super_handlemessage(msg) + if isinstance(msg, ba.DieMessage): + if not self.die: + self.die = True + self.limit = 8400 + self.free_coins = random.randint(1,25) + self.bear_coins = apg['Bear Coin'] + + if not self.bear_coins >= (self.limit): + self.bear_coins += self.free_coins + GLOBAL['Coins Message'].append(self.free_coins) + + if self.bear_coins >= (self.limit): + self.bear_coins = self.limit + + apg['Bear Coin'] = int(self.bear_coins) + apg.apply_and_commit() + + else: GLOBAL['Coins Message'].append(self.free_coins) + +def cls_pow_color(): + return [(1,0.1,0.1),(0.1,0.5,0.9),(0.1,0.9,0.9), + (0.1,0.9,0.1),(0.1,1,0.5),(1,1,0.2),(2,0.5,0.5),(1,0,6)] + +def random_color(): + a = random.random()*3 + b = random.random()*3 + c = random.random()*3 + return (a,b,c) + +def powerup_dist(): + return (('triple_bombs', powerups['Triple']), + ('ice_bombs', powerups['Ice Bombs']), + ('punch', powerups['Punch']), + ('impact_bombs', powerups['Impact Bombs']), + ('land_mines', powerups['Mine Bombs']), + ('sticky_bombs', powerups['Sticky Bombs']), + ('shield', powerups['Shield']), + ('health', powerups['Health']), + ('curse', powerups['Curse']), + ('speed',powerups['Speed']), + ('health_damage', powerups['Healing Damage']), + ('goodbye',powerups['Goodbye']), + ('ice_man',powerups['Ice Man']), + ('tank_shield',powerups['Tank Shield']), + ('impairment_bombs',powerups['Impairment Bombs']), + ('fire_bombs',powerups['Fire Bombs']), + ('fly_bombs',powerups['Fly Bombs'])) + +def percentage_tank_shield(): + percentage = config['Tank Shield PTG'] + percentage_text = ('0.') + str(percentage) + return float(percentage_text) + +def percentage_health_damage(): + percentage = config['Healing Damage PTG'] + percentage_text = ('0.') + str(percentage) + return float(percentage_text) + +# === Modify class === + +class NewProfileBrowserWindow(browser.ProfileBrowserWindow): + def __init__(self, + transition: str = 'in_right', + in_main_menu: bool = True, + selected_profile: str = None, + origin_widget: ba.Widget = None): + super().__init__(transition,in_main_menu,selected_profile,origin_widget) + + self.session = ba.internal.get_foreground_host_session() + uiscale = ba.app.ui.uiscale + width = (100 if uiscale is + ba.UIScale.SMALL else -14) + size = 50 + position = (width*1.65,300) + + if isinstance(self.session,MainMenuSession): + self.button = ba.buttonwidget(parent=self._root_widget, + autoselect=True,position=position, + size=(size,size),button_type='square', + label='',on_activate_call=ba.Call(self.powerupmanager_window)) + + size = size*0.60 + self.image = ba.imagewidget(parent=self._root_widget, + size=(size,size),draw_controller=self.button, + position=(position[0]+10.5,position[1]+17), + texture=ba.gettexture('powerupSpeed')) + + self.text = ba.textwidget(parent=self._root_widget, + position=(position[0]+25,position[1]+10), + size=(0, 0),scale=0.45,color=(0.7,0.9,0.7,1.0), + draw_controller=self.button,maxwidth=60, + text=(f"Ultimate Powerup {_sp_}Manager"),h_align='center',v_align='center') + + def powerupmanager_window(self): + ba.containerwidget(edit=self._root_widget,transition='out_left') + PowerupManagerWindow() + +class NewPowerupBoxFactory(pupbox.PowerupBoxFactory): + def __init__(self) -> None: + super().__init__() + self.tex_speed = ba.gettexture('powerupSpeed') + self.tex_health_damage = ba.gettexture('heart') + self.tex_goodbye = ba.gettexture('achievementOnslaught') + self.tex_ice_man = ba.gettexture('ouyaUButton') + self.tex_tank_shield = ba.gettexture('achievementSuperPunch') + self.tex_impairment_bombs = ba.gettexture('levelIcon') + self.tex_fire_bombs = ba.gettexture('ouyaOButton') + self.tex_fly_bombs = ba.gettexture('star') + + self._powerupdist = [] + for powerup, freq in powerup_dist(): + for _i in range(int(freq)): + self._powerupdist.append(powerup) + + def get_random_powerup_type(self,forcetype = None, excludetypes = None): + + try: self.mapa = ba.getactivity()._map.getname() + except: self.mapa = None + + speed_banned_maps = ['Hockey Stadium','Lake Frigid','Happy Thoughts'] + + if self.mapa in speed_banned_maps: + powerup_disable = ['speed'] + else: powerup_disable = [] + + if excludetypes is None: + excludetypes = [] + if forcetype: + ptype = forcetype + else: + if self._lastpoweruptype == 'curse': + ptype = 'health' + else: + while True: + ptype = self._powerupdist[random.randint( + 0, + len(self._powerupdist) - 1)] + if ptype not in excludetypes and ptype not in powerup_disable: break + self._lastpoweruptype = ptype + return ptype + +def fire_effect(self): + if self.node.exists(): + ba.emitfx(position=self.node.position, + scale=3,count=50*2,spread=0.3, + chunk_type='sweat') + else: self.fire_effect_time = None + +###########BOMBS +Bomb._pm_old_bomb = Bomb.__init__ +def _bomb_init(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.0, 0.0), + bomb_type: str = 'normal', + blast_radius: float = 2.0, + bomb_scale: float = 1.0, + source_player: ba.Player = None, + owner: ba.Node = None): + + self.bm_type = bomb_type + new_bomb_type = bomb_type + bombs = ['ice_bubble','impairment','fire','fly'] + + if bomb_type in bombs: + new_bomb_type = 'ice' + + self._pm_old_bomb(position,velocity,new_bomb_type,blast_radius, + bomb_scale,source_player,owner) + + tex = self.node.color_texture + + if self.bm_type == 'ice_bubble': + self.bomb_type = self.bm_type + self.node.model = None + self.shield_ice = ba.newnode('shield',owner=self.node, + attrs={'color': (0.5, 1.0, 7.0),'radius': 0.6}) + self.node.connectattr('position', self.shield_ice, 'position') + elif self.bm_type == 'fire': + self.bomb_type = self.bm_type + self.node.model = None + self.shield_fire = ba.newnode('shield',owner=self.node, + attrs={'color': (6.5, 6.5, 2.0),'radius': 0.6}) + self.node.connectattr('position', self.shield_fire, 'position') + self.fire_effect_time = ba.Timer(0.1,ba.Call(fire_effect,self),repeat=True) + elif self.bm_type == 'impairment': + self.bomb_type = self.bm_type + tex = ba.gettexture('eggTex3') + elif self.bm_type == 'fly': + self.bomb_type = self.bm_type + tex = ba.gettexture('eggTex1') + + self.node.color_texture = tex + self.hit_subtype = self.bomb_type + + if self.bomb_type == 'ice_bubble': + self.blast_radius *= 1.2 + elif self.bomb_type == 'fly': + self.blast_radius *= 2.2 + +def bomb_handlemessage(self, msg: Any) -> Any: + assert not self.expired + + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + + elif isinstance(msg, bomb.ExplodeHitMessage): + node = ba.getcollision().opposingnode + assert self.node + nodepos = self.node.position + mag = 2000.0 + if self.blast_type in ('ice','ice_bubble'): + mag *= 0.5 + elif self.blast_type == 'land_mine': + mag *= 2.5 + elif self.blast_type == 'tnt': + mag *= 2.0 + elif self.blast_type == 'fire': + mag *= 0.6 + elif self.blast_type == 'fly': + mag *= 5.5 + + node.handlemessage( + ba.HitMessage(pos=nodepos, + velocity=(0, 0, 0), + magnitude=mag, + hit_type=self.hit_type, + hit_subtype=self.hit_subtype, + radius=self.radius, + source_player=ba.existing(self._source_player))) + if self.blast_type in ('ice','ice_bubble'): + ba.playsound(bomb.BombFactory.get().freeze_sound, + 10, + position=nodepos) + node.handlemessage(ba.FreezeMessage()) + + return None + +def powerup_translated(self, type: str): + powerups_names = {'triple_bombs': ba.Lstr(resource='helpWindow.'+'powerupBombNameText'), + 'ice_bombs': ba.Lstr(resource='helpWindow.'+'powerupIceBombsNameText'), + 'punch': ba.Lstr(resource='helpWindow.'+'powerupPunchNameText'), + 'impact_bombs': ba.Lstr(resource='helpWindow.'+'powerupImpactBombsNameText'), + 'land_mines': ba.Lstr(resource='helpWindow.'+'powerupLandMinesNameText'), + 'sticky_bombs': ba.Lstr(resource='helpWindow.'+'powerupStickyBombsNameText'), + 'shield': ba.Lstr(resource='helpWindow.'+'powerupShieldNameText'), + 'health': ba.Lstr(resource='helpWindow.'+'powerupHealthNameText'), + 'curse': ba.Lstr(resource='helpWindow.'+'powerupCurseNameText'), + 'speed': getlanguage('Speed'), + 'health_damage': getlanguage('Healing Damage'), + 'goodbye': getlanguage('Goodbye'), + 'ice_man': getlanguage('Ice Man'), + 'tank_shield': getlanguage('Tank Shield'), + 'impairment_bombs': getlanguage('Impairment Bombs'), + 'fire_bombs': getlanguage('Fire Bombs'), + 'fly_bombs': getlanguage('Fly Bombs')} + self.texts['Name'].text = powerups_names[type] + +###########POWERUP +pupbox.PowerupBox._old_pbx_ = pupbox.PowerupBox.__init__ +def _pbx_(self, position: Sequence[float] = (0.0, 1.0, 0.0), + poweruptype: str = 'triple_bombs', + expire: bool = True): + + self.news: list = [] + for x,i in powerup_dist(): self.news.append(x) + + self.box: list = [] + self.texts = {} + self.news = self.news[9:] + self.box.append(poweruptype) + self.npowerup = self.box[0] + factory = NewPowerupBoxFactory.get() + + if self.npowerup in self.news: new_poweruptype = 'shield' + else: new_poweruptype = poweruptype + self._old_pbx_(position,new_poweruptype,expire) + + type = new_poweruptype + tex = self.node.color_texture + model = self.node.model + + if self.npowerup == 'speed': + type = self.npowerup + tex = factory.tex_speed + elif self.npowerup == 'health_damage': + type = self.npowerup + tex = factory.tex_health_damage + elif self.npowerup == 'goodbye': + type = self.npowerup + tex = factory.tex_goodbye + elif self.npowerup == 'ice_man': + type = self.npowerup + tex = factory.tex_ice_man + elif self.npowerup == 'tank_shield': + type = self.npowerup + tex = factory.tex_tank_shield + elif self.npowerup == 'impairment_bombs': + type = self.npowerup + tex = factory.tex_impairment_bombs + elif self.npowerup == 'fire_bombs': + type = self.npowerup + tex = factory.tex_fire_bombs + elif self.npowerup == 'fly_bombs': + type = self.npowerup + tex = factory.tex_fly_bombs + + self.poweruptype = type + self.node.model = model + self.node.color_texture = tex + n_scale = config['Powerup Scale'] + style = config['Powerup Style'] + + curve = ba.animate(self.node, 'model_scale', {0: 0, 0.14: 1.6, 0.2: n_scale}) + ba.timer(0.2, curve.delete) + + def util_text(type: str, text: str, scale: float = 1, color: list = [1,1,1], + position: list = [0, 0.7, 0], colors_name: bool = False): + m = ba.newnode('math',owner=self.node,attrs={'input1': + (position[0], position[1], position[2]),'operation': 'add'}) + self.node.connectattr('position', m, 'input2') + self.texts[type] = ba.newnode('text',owner=self.node, + attrs={'text': str(text), + 'in_world': True, + 'scale': 0.02, + 'shadow': 0.5, + 'flatness': 1.0, + 'color': (color[0],color[1],color[2]), + 'h_align': 'center'}) + m.connectattr('output', self.texts[type], 'position') + ba.animate(self.texts[type], 'scale', {0: 0.017,0.4: 0.017, 0.5: 0.01*scale}) + + if colors_name: + ba.animate_array(self.texts[type], 'color', 3, + {0:(1,0,0), + 0.2:(1,0.5,0), + 0.4:(1,1,0), + 0.6:(0,1,0), + 0.8:(0,1,1), + 1.0:(1,0,1), + 1.2:(1,0,0)},True) + + def update_time(time): + if self.texts['Time'].exists(): + self.texts['Time'].text = str(time) + + if config['Powerup Time']: + interval = int(pupbox.DEFAULT_POWERUP_INTERVAL) + time2 = (interval-1) + time = 1 + + util_text('Time', time2, scale=1.5,color=(2,2,2), + position=[0,0.9,0], colors_name=False) + + while(interval+3): + ba.timer(time-1,ba.Call(update_time,f'{time2}s')) + + if time2 == 0: + break + + time += 1 + time2 -= 1 + + if config['Powerup With Shield']: + scale = config['Powerup Scale'] + self.shield = ba.newnode('shield',owner=self.node,attrs={'color': (1,1,0),'radius': 1.3*scale}) + self.node.connectattr('position', self.shield, 'position') + ba.animate_array(self.shield,'color',3,{0: (2,0,0), 0.5: (0,2,0), 1: (0,1,6), 1.5: (2,0,0)},True) + + if config['Powerup Name']: + util_text('Name',self.poweruptype,scale=1.2, + position=[0,0.4,0],colors_name=True) + powerup_translated(self,self.poweruptype) + + if style == 'SY: BALL': + self.node.model = ba.getmodel('frostyPelvis') + elif style == 'SY: Impact': + self.node.model = ba.getmodel('impactBomb') + elif style == 'SY: Egg': + self.node.model = ba.getmodel('egg') + +###########SPAZ +def _speed_off_flash(self): + if self.node: + factory = NewPowerupBoxFactory.get() + self.node.billboard_texture = factory.tex_speed + self.node.billboard_opacity = 1.0 + self.node.billboard_cross_out = True + +def _speed_wear_off(self): + if self.node: + self.node.hockey = False + self.node.billboard_opacity = 0.0 + ba.playsound(ba.getsound('powerdown01')) + +def _ice_man_off_flash(self): + if self.node: + factory = NewPowerupBoxFactory.get() + self.node.billboard_texture = factory.tex_ice_man + self.node.billboard_opacity = 1.0 + self.node.billboard_cross_out = True + +def _ice_man_wear_off(self): + if self.node: + f = self.color[0] + i = (0,1,4) + + bomb = self.bmb_color[0] + if bomb != 'ice_bubble': self.bomb_type = bomb + else: self.bomb_type = 'normal' + + self.freeze_punch = False + self.node.billboard_opacity = 0.0 + ba.animate_array(self.node,'color',3,{0: f, 0.3: i, 0.6: f}) + ba.playsound(ba.getsound('powerdown01')) + +Spaz._pm2_spz_old = Spaz.__init__ +def _init_spaz_(self,*args, **kwargs): + self._pm2_spz_old(*args, **kwargs) + self.edg_eff = False + self.kill_eff = False + self.freeze_punch = False + self.die = False + self.color: list = [] + self.color.append(self.node.color) + + self.tankshield = {"Tank": False, + "Reduction": False, + "Shield": None} + +Spaz._super_on_punch_press = Spaz.on_punch_press +def spaz_on_punch_press(self) -> None: + self._super_on_punch_press() + + if self.tankshield['Tank']: + try: + self.tankshield['Reduction'] = True + + shield = ba.newnode('shield',owner=self.node, + attrs={'color': (4,1,4),'radius': 1.3}) + self.node.connectattr('position_center', shield, 'position') + + self.tankshield['Shield'] = shield + except: pass + +Spaz._super_on_punch_release = Spaz.on_punch_release +def spaz_on_punch_release(self) -> None: + self._super_on_punch_release() + try: + self.tankshield['Shield'].delete() + self.tankshield['Reduction'] = False + except: pass + +def new_get_bomb_type_tex(self) -> ba.Texture: + factory = NewPowerupBoxFactory.get() + if self.bomb_type == 'sticky': + return factory.tex_sticky_bombs + if self.bomb_type == 'ice': + return factory.tex_ice_bombs + if self.bomb_type == 'impact': + return factory.tex_impact_bombs + if self.bomb_type == 'impairment': + return factory.tex_impairment_bombs + if self.bomb_type == 'fire': + return factory.tex_fire_bombs + if self.bomb_type == 'fly': + return factory.tex_fly_bombs + return factory.tex_impact_bombs + # raise ValueError('invalid bomb type') + +def new_handlemessage(self, msg: Any) -> Any: + assert not self.expired + + if isinstance(msg, ba.PickedUpMessage): + if self.node: + self.node.handlemessage('hurt_sound') + self.node.handlemessage('picked_up') + + self._num_times_hit += 1 + + elif isinstance(msg, ba.ShouldShatterMessage): + ba.timer(0.001, ba.Call(self.shatter)) + + elif isinstance(msg, ba.ImpactDamageMessage): + ba.timer(0.001, ba.Call(self._hit_self, msg.intensity)) + + elif isinstance(msg, ba.PowerupMessage): + factory = NewPowerupBoxFactory.get() + if self._dead or not self.node: + return True + if self.pick_up_powerup_callback is not None: + self.pick_up_powerup_callback(self) + if msg.poweruptype == 'triple_bombs': + tex = PowerupBoxFactory.get().tex_bomb + self._flash_billboard(tex) + self.set_bomb_count(3) + if self.powerups_expire: + self.node.mini_billboard_1_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_1_start_time = t_ms + self.node.mini_billboard_1_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._multi_bomb_wear_off_timer = (ba.Timer( + (POWERUP_WEAR_OFF_TIME - 2000), + ba.Call(self._multi_bomb_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._multi_bomb_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.Call(self._multi_bomb_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + elif msg.poweruptype == 'land_mines': + self.set_land_mine_count(min(self.land_mine_count + 3, 3)) + elif msg.poweruptype == 'impact_bombs': + self.bomb_type = 'impact' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._bomb_wear_off_flash_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME - 2000, + ba.Call(self._bomb_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._bomb_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.Call(self._bomb_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + elif msg.poweruptype == 'sticky_bombs': + self.bomb_type = 'sticky' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._bomb_wear_off_flash_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME - 2000, + ba.Call(self._bomb_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._bomb_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.Call(self._bomb_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + elif msg.poweruptype == 'punch': + self._has_boxing_gloves = True + tex = PowerupBoxFactory.get().tex_punch + self._flash_billboard(tex) + self.equip_boxing_gloves() + if self.powerups_expire: + self.node.boxing_gloves_flashing = False + self.node.mini_billboard_3_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_3_start_time = t_ms + self.node.mini_billboard_3_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._boxing_gloves_wear_off_flash_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME - 2000, + ba.WeakCall(self._gloves_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._boxing_gloves_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.WeakCall(self._gloves_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + elif msg.poweruptype == 'shield': + factory = SpazFactory.get() + self.equip_shields(decay=factory.shield_decay_rate > 0) + elif msg.poweruptype == 'curse': + self.curse() + elif msg.poweruptype == 'ice_bombs': + self.bomb_type = 'ice' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._bomb_wear_off_flash_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME - 2000, + ba.WeakCall(self._bomb_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._bomb_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.WeakCall(self._bomb_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + elif msg.poweruptype == 'health': + if self.edg_eff: + f = self.color[0] + r = (2,0,0) + g = (0,2,0) + ba.animate_array(self.node,'color',3,{0: r, 0.6: g, 1.0: f}) + self.edg_eff = False + if self._cursed: + self._cursed = False + factory = SpazFactory.get() + for attr in ['materials', 'roller_materials']: + materials = getattr(self.node, attr) + if factory.curse_material in materials: + setattr( + self.node, attr, + tuple(m for m in materials + if m != factory.curse_material)) + self.node.curse_death_time = 0 + self.hitpoints = self.hitpoints_max + self._flash_billboard(PowerupBoxFactory.get().tex_health) + self.node.hurt = 0 + self._last_hit_time = None + self._num_times_hit = 0 + + elif msg.poweruptype == 'tank_shield': + self.tankshield['Tank'] = True + self.edg_eff = False + tex = factory.tex_tank_shield + self._flash_billboard(tex) + + elif msg.poweruptype == 'health_damage': + tex = factory.tex_health_damage + self.edg_eff = True + f = self.color[0] + i = (2,0.5,2) + ba.animate_array(self.node,'color',3,{0: i, 0.5: i, 0.6: f}) + self._flash_billboard(tex) + self.tankshield['Tank'] = False + self.freeze_punch = False + + elif msg.poweruptype == 'goodbye': + tex = factory.tex_goodbye + self._flash_billboard(tex) + self.kill_eff = True + + elif msg.poweruptype == 'fly_bombs': + self.bomb_type = 'fly' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._bomb_wear_off_flash_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME - 2000, + ba.WeakCall(self._bomb_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._bomb_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.WeakCall(self._bomb_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + + elif msg.poweruptype == 'fire_bombs': + self.bomb_type = 'fire' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._bomb_wear_off_flash_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME - 2000, + ba.WeakCall(self._bomb_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._bomb_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.WeakCall(self._bomb_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + + elif msg.poweruptype == 'impairment_bombs': + self.bomb_type = 'impairment' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._bomb_wear_off_flash_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME - 2000, + ba.WeakCall(self._bomb_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._bomb_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.WeakCall(self._bomb_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + + elif msg.poweruptype == 'ice_man': + tex = factory.tex_ice_man + self.bomb_type = 'ice_bubble' + self.freeze_punch = True + self.edg_eff = False + self.node.color = (0,1,4) + self._flash_billboard(tex) + + if self.powerups_expire: + ice_man_time = 17000 + self.node.mini_billboard_2_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = (t_ms + ice_man_time) + + self.ice_man_flash_timer = (ba.Timer( + ice_man_time - 2000, + ba.Call(_ice_man_off_flash,self), + timeformat=ba.TimeFormat.MILLISECONDS)) + + self.ice_man_timer = (ba.Timer(ice_man_time, + ba.Call(_ice_man_wear_off,self), + timeformat=ba.TimeFormat.MILLISECONDS)) + + elif msg.poweruptype == 'speed': + self.node.hockey = True + tex = factory.tex_speed + self._flash_billboard(tex) + if self.powerups_expire: + + speed_time = 15000 + self.node.mini_billboard_2_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = (t_ms + speed_time) + + self.speed_flash_timer = (ba.Timer( + speed_time - 2000, + ba.Call(_speed_off_flash,self), + timeformat=ba.TimeFormat.MILLISECONDS)) + + self.speed_timer = (ba.Timer(speed_time, + ba.Call(_speed_wear_off,self), + timeformat=ba.TimeFormat.MILLISECONDS)) + + self.bmb_color: list = [] + self.bmb_color.append(self.bomb_type) + + self.node.handlemessage('flash') + if msg.sourcenode: + msg.sourcenode.handlemessage(ba.PowerupAcceptMessage()) + return True + + elif isinstance(msg, ba.FreezeMessage): + if not self.node: + return None + if self.node.invincible: + ba.playsound(SpazFactory.get().block_sound, + 1.0, + position=self.node.position) + return None + if self.shield: + return None + if not self.frozen: + self.frozen = True + self.node.frozen = True + ba.timer(5.0, ba.Call(self.handlemessage, + ba.ThawMessage())) + if self.hitpoints <= 0: + self.shatter() + if self.freeze_punch: + self.handlemessage(ba.ThawMessage()) + + elif isinstance(msg, ba.ThawMessage): + if self.frozen and not self.shattered and self.node: + self.frozen = False + self.node.frozen = False + + elif isinstance(msg, ba.HitMessage): + if not self.node: + return None + if self.node.invincible: + ba.playsound(SpazFactory.get().block_sound, + 1.0, + position=self.node.position) + return True + + local_time = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(local_time, int) + if (self._last_hit_time is None + or local_time - self._last_hit_time > 1000): + self._num_times_hit += 1 + self._last_hit_time = local_time + + mag = msg.magnitude * self.impact_scale + velocity_mag = msg.velocity_magnitude * self.impact_scale + damage_scale = 0.22 + + def fire_effect(): + if not self.shield: + if self.node.exists(): + ba.emitfx(position=self.node.position, + scale=3,count=50*2,spread=0.3, + chunk_type='sweat') + self.node.handlemessage('celebrate', 560) + else: self._fire_time = None + else: self._fire_time = None + + def fire(time, damage): + if not self.shield and not self._dead: + self.hitpoints -= damage + ba.show_damage_count(f'-{damage}HP', + self.node.position, msg.force_direction) + ba.playsound(ba.getsound('fuse01')) + + if duration != time: + self._fire_time = ba.Timer(0.1,ba.Call(fire_effect),repeat=True) + else: self._fire_time = None + + if self.hitpoints < 0: + self.node.handlemessage(ba.DieMessage()) + + if msg.hit_subtype == 'fly': + damage_scale = 0.0 + + if self.shield: + self.shield_hitpoints -= 300 + + if self.shield_hitpoints < 0: + self.shield.delete() + self.shield = None + ba.playsound(SpazFactory.get().shield_down_sound,1.0,position=self.node.position) + elif msg.hit_subtype == 'fire': + index = 1 + duration = 5 + damage = 103 + if not self.shield: + for firex in range(duration): + ba.timer(index,ba.Call(fire,index,damage)) + self._fire_time = ba.Timer(0.1,ba.Call(fire_effect),repeat=True) + index += 1 + else: + self.shield_hitpoints -= 80 + if self.shield_hitpoints < 1: + self.shield.delete() + self.shield = None + ba.playsound(SpazFactory.get().shield_down_sound,1.0,position=self.node.position) + elif msg.hit_subtype == 'impairment': + damage_scale = 0 + + if self.shield: + self.shield.delete() + self.shield = None + ba.playsound(SpazFactory.get().shield_down_sound,1.0,position=self.node.position) + else: + hitpoints = int(self.hitpoints*0.80) + self.hitpoints -= int(hitpoints) + ba.show_damage_count((f'-{int(hitpoints/10)}%'), + self.node.position, msg.force_direction) + + if self.hitpoints < 0 or hitpoints < 95: + self.node.handlemessage(ba.DieMessage()) + + if self.shield: + if msg.flat_damage: + damage = msg.flat_damage * self.impact_scale + else: + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], + msg.velocity[0], msg.velocity[1], msg.velocity[2], mag, + velocity_mag, msg.radius, 1, msg.force_direction[0], + msg.force_direction[1], msg.force_direction[2]) + damage = damage_scale * self.node.damage + + assert self.shield_hitpoints is not None + self.shield_hitpoints -= int(damage) + self.shield.hurt = ( + 1.0 - + float(self.shield_hitpoints) / self.shield_hitpoints_max) + + max_spillover = SpazFactory.get().max_shield_spillover_damage + if self.shield_hitpoints <= 0: + + self.shield.delete() + self.shield = None + ba.playsound(SpazFactory.get().shield_down_sound, + 1.0, + position=self.node.position) + + npos = self.node.position + ba.emitfx(position=(npos[0], npos[1] + 0.9, npos[2]), + velocity=self.node.velocity, + count=random.randrange(20, 30), + scale=1.0, + spread=0.6, + chunk_type='spark') + + else: + ba.playsound(SpazFactory.get().shield_hit_sound, + 0.5, + position=self.node.position) + + assert msg.force_direction is not None + ba.emitfx(position=msg.pos, + velocity=(msg.force_direction[0] * 1.0, + msg.force_direction[1] * 1.0, + msg.force_direction[2] * 1.0), + count=min(30, 5 + int(damage * 0.005)), + scale=0.5, + spread=0.3, + chunk_type='spark') + + if self.shield_hitpoints <= -max_spillover: + leftover_damage = -max_spillover - self.shield_hitpoints + shield_leftover_ratio = leftover_damage / damage + + mag *= shield_leftover_ratio + velocity_mag *= shield_leftover_ratio + else: + return True + else: + shield_leftover_ratio = 1.0 + + if msg.flat_damage: + damage = int(msg.flat_damage * self.impact_scale * + shield_leftover_ratio) + else: + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], + msg.velocity[0], msg.velocity[1], msg.velocity[2], mag, + velocity_mag, msg.radius, 0, msg.force_direction[0], + msg.force_direction[1], msg.force_direction[2]) + + damage = int(damage_scale * self.node.damage) + + if self.tankshield['Reduction']: + porcentaje = percentage_tank_shield() + dism = int(damage*porcentaje) + damage = int(damage-dism) + + ba.show_damage_count('-' + str(int(damage / 10)) + '%', + msg.pos, msg.force_direction) + + self.node.handlemessage('hurt_sound') + + if self.edg_eff: + porcentaje = percentage_health_damage() + dmg_dism = int(damage*porcentaje) + self.hitpoints += dmg_dism + + PopupText(text=f'+{int(dmg_dism/10)}%',scale=1.5, + position=self.node.position,color=(0,1,0)).autoretain() + ba.animate_array(self.node,'color',3,{0: (0,1,0), 0.39: (0,2,0), 0.4: self.color[0]}) + ba.playsound(ba.getsound('healthPowerup')) + + if msg.hit_type == 'punch': + self.on_punched(damage) + + try: + if msg.get_source_player(ba.Player).actor.freeze_punch: + self.node.color = (0,1,4) + ba.playsound(ba.getsound('freeze')) + self.node.handlemessage(ba.FreezeMessage()) + except: pass + + if damage > 350: + assert msg.force_direction is not None + ba.show_damage_count('-' + str(int(damage / 10)) + '%', + msg.pos, msg.force_direction) + + if msg.hit_subtype == 'super_punch': + ba.playsound(SpazFactory.get().punch_sound_stronger, + 1.0, + position=self.node.position) + if damage > 500: + sounds = SpazFactory.get().punch_sound_strong + sound = sounds[random.randrange(len(sounds))] + else: + sound = SpazFactory.get().punch_sound + ba.playsound(sound, 1.0, position=self.node.position) + + assert msg.force_direction is not None + ba.emitfx(position=msg.pos, + velocity=(msg.force_direction[0] * 0.5, + msg.force_direction[1] * 0.5, + msg.force_direction[2] * 0.5), + count=min(10, 1 + int(damage * 0.0025)), + scale=0.3, + spread=0.03) + + ba.emitfx(position=msg.pos, + chunk_type='sweat', + velocity=(msg.force_direction[0] * 1.3, + msg.force_direction[1] * 1.3 + 5.0, + msg.force_direction[2] * 1.3), + count=min(30, 1 + int(damage * 0.04)), + scale=0.9, + spread=0.28) + + hurtiness = damage * 0.003 + punchpos = (msg.pos[0] + msg.force_direction[0] * 0.02, + msg.pos[1] + msg.force_direction[1] * 0.02, + msg.pos[2] + msg.force_direction[2] * 0.02) + flash_color = (1.0, 0.8, 0.4) + light = ba.newnode( + 'light', + attrs={ + 'position': punchpos, + 'radius': 0.12 + hurtiness * 0.12, + 'intensity': 0.3 * (1.0 + 1.0 * hurtiness), + 'height_attenuated': False, + 'color': flash_color + }) + ba.timer(0.06, light.delete) + + flash = ba.newnode('flash', + attrs={ + 'position': punchpos, + 'size': 0.17 + 0.17 * hurtiness, + 'color': flash_color + }) + ba.timer(0.06, flash.delete) + + if msg.hit_type == 'impact': + assert msg.force_direction is not None + ba.emitfx(position=msg.pos, + velocity=(msg.force_direction[0] * 2.0, + msg.force_direction[1] * 2.0, + msg.force_direction[2] * 2.0), + count=min(10, 1 + int(damage * 0.01)), + scale=0.4, + spread=0.1) + if self.hitpoints > 0: + if msg.hit_type == 'impact' and damage > self.hitpoints: + newdamage = max(damage - 200, self.hitpoints - 10) + damage = newdamage + self.node.handlemessage('flash') + + if damage > 0.0 and self.node.hold_node: + self.node.hold_node = None + self.hitpoints -= damage + self.node.hurt = 1.0 - float( + self.hitpoints) / self.hitpoints_max + + if self._cursed and damage > 0: + ba.timer( + 0.05, + ba.Call(self.curse_explode, + msg.get_source_player(ba.Player))) + + if self.frozen and (damage > 200 or self.hitpoints <= 0): + self.shatter() + elif self.hitpoints <= 0: + self.node.handlemessage( + ba.DieMessage(how=ba.DeathType.IMPACT)) + + if self.hitpoints <= 0: + damage_avg = self.node.damage_smoothed * damage_scale + if damage_avg > 1000: + self.shatter() + + elif isinstance(msg, BombDiedMessage): + self.bomb_count += 1 + + elif isinstance(msg, ba.DieMessage): + def drop_bomb(): + for xbomb in range(3): + p = self.node.position + pos = (p[0]+xbomb,p[1]+5,p[2]-xbomb) + ball = bomb.Bomb(position=pos,bomb_type='impact').autoretain() + ball.node.model_scale = 0.6 + ball.node.model = ba.getmodel('egg') + ball.node.gravity_scale = 2 + + if self.edg_eff: + self.edg_eff = False + + wasdead = self._dead + self._dead = True + self.hitpoints = 0 + if msg.immediate: + if self.node: + self.node.delete() + elif self.node: + self.node.hurt = 1.0 + if self.play_big_death_sound and not wasdead: + ba.playsound(SpazFactory.get().single_player_death_sound) + self.node.dead = True + ba.timer(2.0, self.node.delete) + + t = 0 + if self.kill_eff: + for bombs in range(3): + ba.timer(t,ba.Call(drop_bomb)) + t += 0.15 + self.kill_eff = False + + elif isinstance(msg, ba.OutOfBoundsMessage): + self.handlemessage(ba.DieMessage(how=ba.DeathType.FALL)) + + elif isinstance(msg, ba.StandMessage): + self._last_stand_pos = (msg.position[0], msg.position[1], + msg.position[2]) + if self.node: + self.node.handlemessage('stand', msg.position[0], + msg.position[1], msg.position[2], + msg.angle) + + elif isinstance(msg, CurseExplodeMessage): + self.curse_explode() + + elif isinstance(msg, PunchHitMessage): + if not self.node: + return None + node = ba.getcollision().opposingnode + + if node and (node not in self._punched_nodes): + + punch_momentum_angular = (self.node.punch_momentum_angular * + self._punch_power_scale) + punch_power = self.node.punch_power * self._punch_power_scale + + if node.getnodetype() != 'spaz': + sounds = SpazFactory.get().impact_sounds_medium + sound = sounds[random.randrange(len(sounds))] + ba.playsound(sound, 1.0, position=self.node.position) + + ppos = self.node.punch_position + punchdir = self.node.punch_velocity + vel = self.node.punch_momentum_linear + + self._punched_nodes.add(node) + node.handlemessage( + ba.HitMessage( + pos=ppos, + velocity=vel, + magnitude=punch_power * punch_momentum_angular * 110.0, + velocity_magnitude=punch_power * 40, + radius=0, + srcnode=self.node, + source_player=self.source_player, + force_direction=punchdir, + hit_type='punch', + hit_subtype=('super_punch' if self._has_boxing_gloves + else 'default'))) + + mag = -400.0 + if self._hockey: + mag *= 0.5 + if len(self._punched_nodes) == 1: + self.node.handlemessage('kick_back', ppos[0], ppos[1], + ppos[2], punchdir[0], punchdir[1], + punchdir[2], mag) + elif isinstance(msg, PickupMessage): + if not self.node: + return None + + try: + collision = ba.getcollision() + opposingnode = collision.opposingnode + opposingbody = collision.opposingbody + except ba.NotFoundError: + return True + + try: + if opposingnode.invincible: + return True + except Exception: + pass + + if (opposingnode.getnodetype() == 'spaz' + and not opposingnode.shattered and opposingbody == 4): + opposingbody = 1 + + held = self.node.hold_node + if held and held.getnodetype() == 'flag': + return True + + self.node.hold_body = opposingbody + self.node.hold_node = opposingnode + elif isinstance(msg, ba.CelebrateMessage): + if self.node: + self.node.handlemessage('celebrate', int(msg.duration * 1000)) + + return None + +class PowerupManagerWindow(PopupWindow): + def __init__(self, transition= 'in_right'): + columns = 2 + self._width = width = 800 + self._height = height = 500 + self._sub_height = 200 + self._scroll_width = self._width*0.90 + self._scroll_height = self._height - 180 + self._sub_width = self._scroll_width*0.95; + self.tab_buttons: set = {} + self.list_cls_power: list = [] + self.default_powerups = default_powerups() + self.default_power_list = list(self.default_powerups) + self.coins = apg['Bear Coin'] + self.popup_cls_power = None + + if not STORE['Buy Firebombs']: + powerups['Fire Bombs'] = 0 + self.default_power_list.remove('Fire Bombs') + + self.charstr = [ba.charstr(ba.SpecialChar.LEFT_ARROW), + ba.charstr(ba.SpecialChar.RIGHT_ARROW), + ba.charstr(ba.SpecialChar.UP_ARROW), + ba.charstr(ba.SpecialChar.DOWN_ARROW)] + + self.tabdefs = {"Action 1": ['powerupIceBombs',(1,1,1)], + "Action 2": ['settingsIcon',(0,1,0)], + "Action 3": ['inventoryIcon',(1,1,1)], + "Action 4": ['storeIcon',(1,1,1)], + "Action 5": ['advancedIcon',(1,1,1)], + "About": ['heart',(1.5,0.3,0.3)]} + + if (STORE['Buy Firebombs'] and + STORE['Buy Option'] and + STORE['Buy Percentage']): + self.tabdefs = {"Action 1": ['powerupIceBombs',(1,1,1)], + "Action 2": ['settingsIcon',(0,1,0)], + "Action 3": ['inventoryIcon',(1,1,1)], + "About": ['heart',(1.5,0.3,0.3)]} + + self.listdef = list(self.tabdefs) + + self.count = len(self.tabdefs) + + self._current_tab = GLOBAL['Tab'] + + app = ba.app.ui + uiscale = app.uiscale + + self._root_widget = ba.containerwidget(size=(width+90,height+80),transition=transition, + scale=1.5 if uiscale is ba.UIScale.SMALL else 1.0, + stack_offset=(0,-30) if uiscale is ba.UIScale.SMALL else (0,0)) + + self._backButton = b = ba.buttonwidget(parent=self._root_widget,autoselect=True, + position=(60,self._height-15),size=(130,60), + scale=0.8,text_scale=1.2,label=ba.Lstr(resource='backText'), + button_type='back',on_activate_call=ba.Call(self._back)) + ba.buttonwidget(edit=self._backButton, button_type='backSmall',size=(60, 60),label=ba.charstr(ba.SpecialChar.BACK)) + ba.containerwidget(edit=self._root_widget,cancel_button=b) + + self.titletext = ba.textwidget(parent=self._root_widget,position=(0, height-15),size=(width,50), + h_align="center",color=ba.app.ui.title_color, v_align="center",maxwidth=width*1.3) + + index = 0 + for tab in range(self.count): + for tab2 in range(columns): + + tag = self.listdef[index] + + position = (620+(tab2*120),self._height-50*2.5-(tab*120)) + + if tag == 'About': + text = ba.Lstr(resource='gatherWindow.aboutText') + elif tab == 'Action 4': + text = ba.Lstr(resource='storeText') + else: text = getlanguage(tag) + + self.tab_buttons[tag] = ba.buttonwidget(parent=self._root_widget,autoselect=True, + position=position,size=(110,110), + scale=1,label='',enable_sound=False, + button_type='square',on_activate_call=ba.Call(self._set_tab,tag,sound=True)) + + self.text = ba.textwidget(parent=self._root_widget, + position=(position[0]+55,position[1]+30), + size=(0, 0),scale=1,color=ba.app.ui.title_color, + draw_controller=self.tab_buttons[tag],maxwidth=100, + text=text,h_align='center',v_align='center') + + self.image = ba.imagewidget(parent=self._root_widget, + size=(60,60),color=self.tabdefs[tag][1], + draw_controller=self.tab_buttons[tag], + position=(position[0]+25,position[1]+40), + texture=ba.gettexture(self.tabdefs[tag][0])) + + index += 1 + + if self.count == index: + break + + if self.count == index: + break + + self._scrollwidget = None + self._tab_container = None + self._set_tab(self._current_tab) + + def __del__(self): + apg.apply_and_commit() + + def _set_tab(self, tab, sound: bool = False): + self.sound = sound + GLOBAL['Tab'] = tab + apg.apply_and_commit() + + if self._tab_container is not None and self._tab_container.exists(): + self._tab_container.delete() + + if self.sound: + ba.playsound(ba.getsound('click01')) + + if self._scrollwidget: + self._scrollwidget.delete() + + self._scrollwidget = ba.scrollwidget(parent=self._root_widget, + position=(self._width*0.08,51*1.8),size=(self._sub_width -140,self._scroll_height +60*1.2)) + + if tab == 'Action 4': + if self._scrollwidget: + self._scrollwidget.delete() + self._scrollwidget = ba.hscrollwidget(parent=self._root_widget, + position=(self._width*0.08,51*1.8),size=(self._sub_width -140,self._scroll_height +60*1.2), + capture_arrows=True,claims_left_right=True) + ba.textwidget(edit=self.titletext,text=ba.Lstr(resource='storeText')) + elif tab == 'About': + ba.textwidget(edit=self.titletext,text=ba.Lstr(resource='gatherWindow.aboutText')) + else: ba.textwidget(edit=self.titletext,text=getlanguage(tab)) + + choices = ['Reset','Only Bombs','Only Items','New','Nothing'] + c_display = [] + + for display in choices: + choices_display = ba.Lstr(translate=("",getlanguage(display))) + c_display.append(choices_display) + + if tab == 'Action 1': + self.popup_cls_power = PopupMenu( + parent=self._root_widget, + position=(130,self._width*0.61), + button_size=(150,50),scale=2.5, + choices=choices,width=150, + choices_display=c_display, + current_choice=GLOBAL['Cls Powerup'], + on_value_change_call=self._set_concept) + self.list_cls_power.append(self.popup_cls_power._button) + + self.button_cls_power = ba.buttonwidget(parent=self._root_widget, + position=(500,self._width*0.61),size=(50,50),autoselect=True, + scale=1,label=('%'),text_scale=1,button_type='square', + on_activate_call=self._percentage_window) + self.list_cls_power.append(self.button_cls_power) + + rewindow = [self.popup_cls_power._button,self.button_cls_power] + + for cls in self.list_cls_power: # this is very important so that pupups don't accumulate + if cls not in rewindow: + cls.delete() + + elif tab == 'Action 4': + self.button_coin = ba.buttonwidget(parent=self._root_widget,icon=ba.gettexture('coin'), + position=(550,self._width*0.614),size=(160,40),textcolor=(0,1,0),color=(0,1,6), + scale=1,label=str(apg['Bear Coin']),text_scale=1,autoselect=True, + on_activate_call=None) #self._percentage_window) + self.list_cls_power.append(self.button_coin) + + try: rewindow.append(self.button_coin) + except: rewindow = [self.button_coin] + for cls in self.list_cls_power: # this is very important so that pupups don't accumulate + if cls not in rewindow: + cls.delete() + + else: + try: + for cls in self.list_cls_power: + cls.delete() + except: pass + + if tab == 'Action 1': + sub_height = len(self.default_power_list) * 90 + v = sub_height - 55 + width = 300 + posi = 0 + id_power = list(self.default_powerups) + new_powerups = id_power[9:] + self.listpower = {} + + self._tab_container = c = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width,sub_height), + background=False,selection_loops_to_parent=True) + + for power in self.default_power_list: + if power == id_power[0]: + text = 'helpWindow.powerupShieldNameText' + tex = ba.gettexture('powerupShield') + elif power == id_power[1]: + text = 'helpWindow.powerupPunchNameText' + tex = ba.gettexture('powerupPunch') + elif power == id_power[2]: + text = 'helpWindow.powerupLandMinesNameText' + tex = ba.gettexture('powerupLandMines') + elif power == id_power[3]: + text = 'helpWindow.powerupImpactBombsNameText' + tex = ba.gettexture('powerupImpactBombs') + elif power == id_power[4]: + text = 'helpWindow.powerupIceBombsNameText' + tex = ba.gettexture('powerupIceBombs') + elif power == id_power[5]: + text = 'helpWindow.powerupBombNameText' + tex = ba.gettexture('powerupBomb') + elif power == id_power[6]: + text = 'helpWindow.powerupStickyBombsNameText' + tex = ba.gettexture('powerupStickyBombs') + elif power == id_power[7]: + text = 'helpWindow.powerupCurseNameText' + tex = ba.gettexture('powerupCurse') + elif power == id_power[8]: + text = 'helpWindow.powerupHealthNameText' + tex = ba.gettexture('powerupHealth') + elif power == id_power[9]: + text = power + tex = ba.gettexture('powerupSpeed') + elif power == id_power[10]: + text = power + tex = ba.gettexture('heart') + elif power == id_power[11]: + text = "Goodbye!" + tex = ba.gettexture('achievementOnslaught') + elif power == id_power[12]: + text = power + tex = ba.gettexture('ouyaUButton') + elif power == id_power[13]: + text = power + tex = ba.gettexture('achievementSuperPunch') + elif power == id_power[14]: + text = power + tex = ba.gettexture('levelIcon') + elif power == id_power[15]: + text = power + tex = ba.gettexture('ouyaOButton') + elif power == id_power[16]: + text = power + tex = ba.gettexture('star') + + if power in new_powerups: label = getlanguage(power) + else: label = ba.Lstr(resource=text) + + apperance = powerups[power] + position = (90,v-posi) + + t = ba.textwidget(parent=c,position=(position[0]-30,position[1]-15),size=(width,50), + h_align="center",color=(ba.app.ui.title_color), text=label, v_align="center",maxwidth=width*1.3) + + self.powprev = ba.imagewidget(parent=c, + position=(position[0]-70,position[1]-10), + size=(50,50),texture=tex) + + dipos = 0 + for direc in ['-','+']: + ba.buttonwidget(parent=c,autoselect=True, + position=(position[0]+270+dipos,position[1]-10),size=(100,100), + scale=0.4,label=direc,button_type='square',text_scale=4, + on_activate_call=ba.Call(self.apperance_powerups,power,direc)) + dipos += 100 + + textwidget = ba.textwidget(parent=c,position=(position[0]+190,position[1]-15),size=(width,50), + h_align="center",color=cls_pow_color()[apperance],text=str(apperance), + v_align="center",maxwidth=width*1.3) + self.listpower[power] = textwidget + + posi += 90 + + elif tab == 'Action 2': + sub_height = 370 if not STORE['Buy Option'] else 450 + v = sub_height - 55 + width = 300 + + self._tab_container = c = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width,sub_height), + background=False,selection_loops_to_parent=True) + + position = (40,v-20) + + c_display = [] + choices = ['Auto','SY: BALL','SY: Impact','SY: Egg'] + for display in choices: + choices_display = ba.Lstr(translate=("",getlanguage(display))) + c_display.append(choices_display) + + popup = PopupMenu(parent=c, + position=(position[0]+300,position[1]), + button_size=(150,50),scale=2.5, + choices=choices,width=150, + choices_display=c_display, + current_choice=config['Powerup Style'], + on_value_change_call=ba.Call(self._all_popup,'Powerup Style')) + + text = getlanguage('Powerup Style') + wt = (len(text)*0.80) + t = ba.textwidget(parent=c,position=(position[0]-60+wt,position[1]),size=(width,50),maxwidth=width*0.9, + scale=1.1,h_align="center",color=ba.app.ui.title_color,text=getlanguage('Powerup Style'),v_align="center") + + dipos = 0 + for direc in ['-','+']: + ba.buttonwidget(parent=c,autoselect=True, + position=(position[0]+310+dipos,position[1]-100),size=(100,100), + repeat=True,scale=0.4,label=direc,button_type='square',text_scale=4, + on_activate_call=ba.Call(self._powerups_scale,direc)) + dipos += 100 + + txt_scale = config['Powerup Scale'] + self.txt_scale = ba.textwidget(parent=c,position=(position[0]+230,position[1]-105),size=(width,50), + scale=1.1,h_align="center",color=(0,1,0),text=str(txt_scale),v_align="center",maxwidth=width*1.3) + + text = getlanguage('Powerup Scale') + wt = (len(text)*0.80) + t = ba.textwidget(parent=c,position=(position[0]-60+wt,position[1]-100),size=(width,50),maxwidth=width*0.9, + scale=1.1,h_align="center",color=ba.app.ui.title_color,text=text,v_align="center") + + position = (position[0]-20,position[1]+40) + + self.check = ba.checkboxwidget(parent=c,position=(position[0]+30,position[1]-230),value=config['Powerup Name'], + on_value_change_call=ba.Call(self._switches,'Powerup Name'),maxwidth=self._scroll_width*0.9, + text=getlanguage('Powerup Name'),autoselect=True) + + self.check = ba.checkboxwidget(parent=c,position=(position[0]+30,position[1]-230*1.3),value=config['Powerup With Shield'], + on_value_change_call=ba.Call(self._switches,'Powerup With Shield'),maxwidth=self._scroll_width*0.9, + text=getlanguage('Powerup With Shield'),autoselect=True) + + if STORE['Buy Option']: + self.check = ba.checkboxwidget(parent=c,position=(position[0]+30,position[1]-230*1.6),value=config['Powerup Time'], + on_value_change_call=ba.Call(self._switches,'Powerup Time'),maxwidth=self._scroll_width*0.9, + text=getlanguage('Powerup Time'),autoselect=True) + + elif tab == 'Action 3': + sub_height = 300 + v = sub_height - 55 + width = 300 + + self._tab_container = c = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width,sub_height), + background=False,selection_loops_to_parent=True) + + v -= 20 + position = (110,v-45*1.72) + + if not STORE['Buy Percentage']: + t = ba.textwidget(parent=c,position=(90,v-100),size=(30+width,50), + h_align="center",text=getlanguage('Block Option Store'), + color=ba.app.ui.title_color,v_align="center",maxwidth=width*1.5,scale=1.5) + + i = ba.imagewidget(parent=c, + position=(position[0]+100,position[1]-205), + size=(80,80),texture=ba.gettexture('lock')) + else: + t = ba.textwidget(parent=c,position=(position[0]-14,position[1]+70),size=(30+width,50), + h_align="center",text=f"{getlanguage('Tank Shield PTG')} ({getlanguage('Tank Shield')})", + color=ba.app.ui.title_color,v_align="center",maxwidth=width*1.5,scale=1.5) + + b = ba.buttonwidget(parent=c,autoselect=True,position=position,size=(100,100),repeat=True, + scale=0.6,label=self.charstr[3],button_type='square',text_scale=2, + on_activate_call=ba.Call(self.tank_shield_percentage,'Decrement')) + + b = ba.buttonwidget(parent=c,autoselect=True,repeat=True,text_scale=2, + position=(position[0]*3.2,position[1]),size=(100,100), + scale=0.6,label=self.charstr[2],button_type='square', + on_activate_call=ba.Call(self.tank_shield_percentage,'Increment')) + + porcentaje = config['Tank Shield PTG'] + if porcentaje > 59: color = (0,1,0) + elif porcentaje < 40: color = (1,1,0) + else: color = (0,1,0.8) + + self.tank_text = ba.textwidget(parent=c,position=(position[0]-14,position[1]+5), + size=(30+width,50),h_align="center", + text=str(porcentaje)+'%',color=color, + v_align="center",maxwidth=width*1.3,scale=2) + + # -----> + + position = (110,v-160*1.6) + t = ba.textwidget(parent=c,position=(position[0]-14,position[1]+70),size=(30+width,50), + h_align="center",text=f"{getlanguage('Healing Damage PTG')}{_sp_}({getlanguage('Healing Damage')})", + color=ba.app.ui.title_color,v_align="center",maxwidth=width*1.3,scale=1.4) + + b = ba.buttonwidget(parent=c,autoselect=True,position=position,size=(100,100),repeat=True, + scale=0.6,label=self.charstr[3],button_type='square',text_scale=2, + on_activate_call=ba.Call(self.health_damage_percentage,'Decrement')) + + b = ba.buttonwidget(parent=c,autoselect=True,repeat=True,text_scale=2, + position=(position[0]*3.2,position[1]),size=(100,100), + scale=0.6,label=self.charstr[2],button_type='square', + on_activate_call=ba.Call(self.health_damage_percentage,'Increment')) + + porcentaje = config['Healing Damage PTG'] + if porcentaje > 59: color = (0,1,0) + elif porcentaje < 40: color = (1,1,0) + else: color = (0,1,0.8) + + self.hlg_text = ba.textwidget(parent=c,position=(position[0]-14,position[1]+5), + size=(30+width,50),h_align="center", + text=str(porcentaje)+'%',color=color, + v_align="center",maxwidth=width*1.3,scale=2) + + elif tab == 'Percentage': + sub_height = len(self.default_power_list) * 90 + v = sub_height - 55 + width = 300 + posi = 0 + id_power = list(self.default_powerups) + new_powerups = id_power[9:] + self.listpower = {} + + self._tab_container = c = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width,sub_height), + background=False,selection_loops_to_parent=True) + + for power in self.default_power_list: + if power == id_power[0]: + text = 'helpWindow.powerupShieldNameText' + tex = ba.gettexture('powerupShield') + elif power == id_power[1]: + text = 'helpWindow.powerupPunchNameText' + tex = ba.gettexture('powerupPunch') + elif power == id_power[2]: + text = 'helpWindow.powerupLandMinesNameText' + tex = ba.gettexture('powerupLandMines') + elif power == id_power[3]: + text = 'helpWindow.powerupImpactBombsNameText' + tex = ba.gettexture('powerupImpactBombs') + elif power == id_power[4]: + text = 'helpWindow.powerupIceBombsNameText' + tex = ba.gettexture('powerupIceBombs') + elif power == id_power[5]: + text = 'helpWindow.powerupBombNameText' + tex = ba.gettexture('powerupBomb') + elif power == id_power[6]: + text = 'helpWindow.powerupStickyBombsNameText' + tex = ba.gettexture('powerupStickyBombs') + elif power == id_power[7]: + text = 'helpWindow.powerupCurseNameText' + tex = ba.gettexture('powerupCurse') + elif power == id_power[8]: + text = 'helpWindow.powerupHealthNameText' + tex = ba.gettexture('powerupHealth') + elif power == id_power[9]: + text = power + tex = ba.gettexture('powerupSpeed') + elif power == id_power[10]: + text = power + tex = ba.gettexture('heart') + elif power == id_power[11]: + text = "Goodbye!" + tex = ba.gettexture('achievementOnslaught') + elif power == id_power[12]: + text = power + tex = ba.gettexture('ouyaUButton') + elif power == id_power[13]: + text = power + tex = ba.gettexture('achievementSuperPunch') + elif power == id_power[14]: + text = power + tex = ba.gettexture('levelIcon') + elif power == id_power[15]: + text = power + tex = ba.gettexture('ouyaOButton') + elif power == id_power[16]: + text = power + tex = ba.gettexture('star') + + if power in new_powerups: label = getlanguage(power) + else: label = ba.Lstr(resource=text) + + apperance = powerups[power] + position = (90,v-posi) + + t = ba.textwidget(parent=c,position=(position[0]-30,position[1]-15),size=(width,50), + h_align="center",color=(ba.app.ui.title_color), text=label, v_align="center",maxwidth=width*1.3) + + self.powprev = ba.imagewidget(parent=c, + position=(position[0]-70,position[1]-10), + size=(50,50),texture=tex) + + ptg = str(self.total_percentage(power)) + t = ba.textwidget(parent=c,position=(position[0]+170,position[1]-10),size=(width,50), + h_align="center",color=(0,1,0),text=(f'{ptg}%'),v_align="center",maxwidth=width*1.3) + + posi += 90 + + elif tab == 'Action 4': + sub_height = 370 + width = 300 + v = sub_height - 55 + u = width - 60 + + self._tab_container = c = ba.containerwidget(parent=self._scrollwidget, + size=(width+500,sub_height), + background=False,selection_loops_to_parent=True) + + position = (u+150,v-250) + n_pos = 0 + prices = [7560, 5150, 3360] + str_name = ["FireBombs Store","Timer Store","Percentages Store"] + images = ["ouyaOButton","settingsIcon","inventoryIcon"] + + index = 0 + for store in store_items(): + p = prices[index] + txt = str_name[index] + label = getlanguage(txt) + tx_pos = len(label)*1.8 + lb_scale = len(label)*0.20 + preview = images[index] + + if STORE[store]: + text = getlanguage('Bought') + icon = ba.gettexture('graphicsIcon') + color = (0.52,0.48,0.63) + txt_scale = 1.5 + else: + text = str(p) + icon = ba.gettexture('coin') + color = (0.5,0.4,0.93) + txt_scale = 2 + + b = ba.buttonwidget(parent=c,autoselect=True,position=(position[0]+210-n_pos,position[1]), + size=(250,80),scale=0.7,label=text,text_scale=txt_scale,icon=icon,color=color, + iconscale=1.7,on_activate_call=ba.Call(self._buy_object,store,p)) + + s = 180 + b = ba.buttonwidget(parent=c,autoselect=True,position=(position[0]+210-n_pos,position[1]+55), + size=(s,s+30),scale=1,label='',color=color,button_type='square', + on_activate_call=ba.Call(self._buy_object,store,p)) + + s -= 80 + i = ba.imagewidget(parent=c,draw_controller=b, + position=(position[0]+250-n_pos,position[1]+140), + size=(s,s),texture=ba.gettexture(preview)) + + t = ba.textwidget(parent=c,position=(position[0]+270-n_pos,position[1]+101), + h_align="center",color=(ba.app.ui.title_color),text=label,v_align="center",maxwidth=130) + + n_pos += 280 + index += 1 + + elif tab == 'Action 5': + sub_height = 370 + v = sub_height - 55 + width = 300 + + self._tab_container = c = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width,sub_height),background=False, + selection_loops_to_parent=True) + + position = (0,v-30) + + t = ba.textwidget(parent=c,position=(position[0]+80,position[1]-30),size=(width+60,50),scale=1, + h_align="center",color=(ba.app.ui.title_color),text=ba.Lstr( + resource='settingsWindowAdvanced.enterPromoCodeText'),v_align="center",maxwidth=width*1.3) + + self.promocode_text = ba.textwidget(parent=c,position=(position[0]+80,position[1]-100),size=(width+60,50),scale=1, + editable=True,h_align="center",color=(ba.app.ui.title_color),text='', v_align="center",maxwidth=width*1.3, + max_chars=30,description=ba.Lstr(resource='settingsWindowAdvanced.enterPromoCodeText')) + + self.promocode_button = ba.buttonwidget( + parent=c,position=(position[0]+160,position[1]-170), + size=(200, 60),scale=1.0,label=ba.Lstr(resource='submitText'), + on_activate_call=self._promocode) + + else: + sub_height = 0 + v = sub_height - 55 + width = 300 + + self._tab_container = c = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width,sub_height), + background=False,selection_loops_to_parent=True) + + t = ba.textwidget(parent=c,position=(110, v-20),size=(width,50), + scale=1.4,color=(0.2,1.2,0.2),h_align="center",v_align="center", + text=("Ultimate Powerup Manager v1.7"),maxwidth=width*30) + + t = ba.textwidget(parent=c,position=(110, v-90),size=(width,50), + scale=1,color=(1.3,0.5,1.0),h_align="center",v_align="center", + text=getlanguage('Creator'),maxwidth=width*30) + + t = ba.textwidget(parent=c,position=(110, v-220),size=(width,50), + scale=1,color=(1.0,1.2,0.3),h_align="center",v_align="center", + text=getlanguage('Mod Info'),maxwidth=width*30) + + for select_tab,button_tab in self.tab_buttons.items(): + if select_tab == tab: + ba.buttonwidget(edit=button_tab,color=(0.5,0.4,1.5)) + else: ba.buttonwidget(edit=button_tab,color=(0.52,0.48,0.63)) + + def _all_popup(self, tag: str, popup: str) -> None: + config[tag] = popup + apg.apply_and_commit() + + def _set_concept(self, concept: str) -> None: + GLOBAL['Cls Powerup'] = concept + + if concept == 'Reset': + for power, deflt in default_powerups().items(): + powerups[power] = deflt + elif concept == 'Nothing': + for power in default_powerups(): + powerups[power] = 0 + elif concept == 'Only Bombs': + for power, deflt in default_powerups().items(): + if 'Bombs' not in power: + powerups[power] = 0 + else: powerups[power] = 3 + elif concept == 'Only Items': + for power, deflt in default_powerups().items(): + if 'Bombs' in power: + powerups[power] = 0 + else: powerups[power] = deflt + elif concept == 'New': + default_power = default_powerups() + new_powerups = list(default_power)[9:] + for power, deflt in default_power.items(): + if power not in new_powerups: + powerups[power] = 0 + else: powerups[power] = deflt + + if not STORE['Buy Firebombs']: + powerups['Fire Bombs'] = 0 + + self._set_tab('Action 1') + + def tank_shield_percentage(self, tag): + max = 96 + min = 40 + if tag == 'Increment': + config['Tank Shield PTG'] += 1 + if config['Tank Shield PTG'] > max: + config['Tank Shield PTG'] = min + elif tag == 'Decrement': + config['Tank Shield PTG'] -= 1 + if config['Tank Shield PTG'] < min: + config['Tank Shield PTG'] = max + + porcentaje = config['Tank Shield PTG'] + if porcentaje > 59: color = (0,1,0) + elif porcentaje < 40: color = (1,1,0) + else: color = (0,1,0.8) + ba.textwidget(edit=self.tank_text, + text=str(porcentaje)+'%',color=color) + + def health_damage_percentage(self, tag): + max = 80 + min = 35 + if tag == 'Increment': + config['Healing Damage PTG'] += 1 + if config['Healing Damage PTG'] > max: + config['Healing Damage PTG'] = min + elif tag == 'Decrement': + config['Healing Damage PTG'] -= 1 + if config['Healing Damage PTG'] < min: + config['Healing Damage PTG'] = max + + porcentaje = config['Healing Damage PTG'] + if porcentaje > 59: color = (0,1,0) + elif porcentaje < 40: color = (1,1,0) + else: color = (0,1,0.8) + ba.textwidget(edit=self.hlg_text, + text=str(porcentaje)+'%',color=color) + + def apperance_powerups(self, powerup: str, ID: str): + max = 7 + if ID == "-": + if powerups[powerup] == 0: + powerups[powerup] = max + else: powerups[powerup] -= 1 + elif ID == "+": + if powerups[powerup] == max: + powerups[powerup] = 0 + else: powerups[powerup] += 1 + enum = powerups[powerup] + ba.textwidget(edit=self.listpower[powerup], + text=str(powerups[powerup]), + color=cls_pow_color()[enum]) + + def _powerups_scale(self, ID: str): + max = 1.5 + min = 0.5 + sc = 0.1 + if ID == "-": + if config['Powerup Scale'] < (min+0.1): + config['Powerup Scale'] = max + else: config['Powerup Scale'] -= sc + elif ID == "+": + if config['Powerup Scale'] > (max-0.1): + config['Powerup Scale'] = min + else: config['Powerup Scale'] += sc + config['Powerup Scale'] = round(config['Powerup Scale'],1) + ba.textwidget(edit=self.txt_scale, + text=str(config['Powerup Scale'])) + + def total_percentage(self, power): + total = 0 + pw = powerups[power] + for i,i2 in powerups.items(): + total += i2 + if total == 0: + return float(total) + else: + ptg = (100*pw/total) + result = round(ptg,2) + return result + + def store_refresh(self, tag: str): + if tag == 'Buy Firebombs': + powerups['Fire Bombs'] = 3 + self.default_power_list.append('Fire Bombs') + self._set_tab('Action 4') + + def _buy_object(self, tag: str, price: int): + store = BearStore(value=tag, price=price, + callback=ba.Call(self.store_refresh,tag)) + store.buy() + + def _promocode(self): + code = ba.textwidget(query=self.promocode_text) + promo = PromoCode(code=code) + promo.code_confirmation() + ba.textwidget(edit=self.promocode_text,text="") + + def _switches(self,tag,m): + config[tag] = False if m==0 else True + apg.apply_and_commit() + + def _percentage_window(self): + self._set_tab('Percentage') + + def _back(self): + ba.containerwidget(edit=self._root_widget,transition='out_left') + browser.ProfileBrowserWindow() def enable(): diff --git a/dist/ba_root/mods/serverData/serverdata.py b/dist/ba_root/mods/serverData/serverdata.py index 5f97bb9..273846e 100644 --- a/dist/ba_root/mods/serverData/serverdata.py +++ b/dist/ba_root/mods/serverData/serverdata.py @@ -3,4 +3,5 @@ clients={} cachedclients=[] muted=False -coopmode=False \ No newline at end of file +coopmode=False +ips={} diff --git a/dist/ba_root/mods/setting.json b/dist/ba_root/mods/setting.json index 5a0d7d6..837970e 100644 --- a/dist/ba_root/mods/setting.json +++ b/dist/ba_root/mods/setting.json @@ -112,5 +112,6 @@ "enablestats": true, "enableHitTexts": true, "enableeffects": true, - "enableTop5effects": true + "enableTop5effects": true, + "enableTagAnimation":true } diff --git a/dist/ba_root/mods/spazmod/effects.py b/dist/ba_root/mods/spazmod/effects.py index ddb5de7..1f2873c 100644 --- a/dist/ba_root/mods/spazmod/effects.py +++ b/dist/ba_root/mods/spazmod/effects.py @@ -16,6 +16,7 @@ from bastd.actor import spaz,spazappearance from bastd.actor import bomb as stdbomb from bastd.actor.powerupbox import PowerupBoxFactory import ba,_ba,bastd,weakref,random,math,time,base64,os,json,setting +import ba.internal from playersData import pdata from stats import mystats PlayerType = TypeVar('PlayerType', bound=ba.Player) @@ -156,7 +157,7 @@ class Effect(ba.Actor): node_id = self.source_player.node.playerID cl_str = None clID = None - for c in _ba.get_foreground_host_session().sessionplayers: + for c in ba.internal.get_foreground_host_session().sessionplayers: if (c.activityplayer) and (c.activityplayer.node.playerID == node_id): profiles = c.inputdevice.get_player_profiles() clID = c.inputdevice.client_id diff --git a/dist/ba_root/mods/spazmod/hitmessage.py b/dist/ba_root/mods/spazmod/hitmessage.py index c1e03ba..eb7422f 100644 --- a/dist/ba_root/mods/spazmod/hitmessage.py +++ b/dist/ba_root/mods/spazmod/hitmessage.py @@ -2,6 +2,7 @@ # Released under the MIT License. See LICENSE for details. import ba, _ba, setting +import ba.internal from stats.mystats import damage_data from bastd.actor.popuptext import PopupText @@ -18,7 +19,7 @@ def handle_hit(msg, hp, dmg, hit_by, msg_pos): hit_by_id = hit_by.node.playerID if hit_by_id is not None: hit_by_account_id = None - for c in _ba.get_foreground_host_session().sessionplayers: + for c in ba.internal.get_foreground_host_session().sessionplayers: if (c.activityplayer) and (c.activityplayer.node.playerID == hit_by_id): hit_by_account_id = c.get_v1_account_id() if hit_by_account_id in damage_data: damage_data[hit_by_account_id] += float(dmg) diff --git a/dist/ba_root/mods/spazmod/modifyspaz.py b/dist/ba_root/mods/spazmod/modifyspaz.py index cbaf49d..0e5c7e9 100644 --- a/dist/ba_root/mods/spazmod/modifyspaz.py +++ b/dist/ba_root/mods/spazmod/modifyspaz.py @@ -4,13 +4,14 @@ import setting from random import randint import _ba,ba +import ba.internal _setting=setting.get_settings_data() def update_name(): - import _ba + import ba.internal from stats import mystats stat = mystats.get_all_stats() - ros = _ba.get_game_roster() + ros = ba.internal.get_game_roster() for i in ros: if i['account_id']: name = i['display_string'] @@ -60,7 +61,7 @@ def setTeamCharacter(): if not _setting["sameCharacterForTeam"]: return used=[] - teams=_ba.get_foreground_host_session().sessionteams + teams=ba.internal.get_foreground_host_session().sessionteams if len(teams) < 10: for team in teams: character=getRandomCharacter(used) diff --git a/dist/ba_root/mods/spazmod/tag.py b/dist/ba_root/mods/spazmod/tag.py index 447db86..9682f05 100644 --- a/dist/ba_root/mods/spazmod/tag.py +++ b/dist/ba_root/mods/spazmod/tag.py @@ -1,6 +1,8 @@ from playersData import pdata import ba, setting +from stats import mystats +sett = setting.get_settings_data() def addtag(node,player): session_player=player.sessionplayer account_id=session_player.get_v1_account_id() @@ -22,7 +24,7 @@ def addtag(node,player): if tag: Tag(node,tag,col) -from stats import mystats + def addrank(node,player): session_player=player.sessionplayer account_id=session_player.get_v1_account_id() @@ -33,7 +35,6 @@ def addrank(node,player): def addhp(node): hp = node.hitpoints - _set = setting.get_settings_data() def showHP(): HitPoint(owner=node,prefix=str(int(hp)),position=(0,0.75,0),shad = 1.4) if hp: t = ba.Timer(100,ba.Call(showHP),repeat = True, timetype=ba.TimeType.SIM, timeformat=ba.TimeFormat.MILLISECONDS) @@ -41,6 +42,7 @@ def addhp(node): class Tag(object): def __init__(self,owner=None,tag="somthing",col=(1,1,1)): self.node=owner + mnode = ba.newnode('math', owner=self.node, attrs={ @@ -79,7 +81,16 @@ class Tag(object): 'h_align': 'center' }) mnode.connectattr('output', self.tag_text, 'position') - + if sett["enableTagAnimation"]: + ba.animate_array(node=self.tag_text, attr='color', size=3, keys={ + 0.2: (2,0,2), + 0.4: (2,2,0), + 0.6: (0,2,2), + 0.8: (2,0,2), + 1.0: (1,1,0), + 1.2: (0,1,1), + 1.4: (1,0,1) + }, loop=True) class Rank(object): def __init__(self,owner=None,rank=99): self.node=owner @@ -105,8 +116,7 @@ class Rank(object): class HitPoint(object): def __init__(self,position = (0,1.5,0),owner = None,prefix = 'ADMIN',shad = 1.2): - _set = setting.get_settings_data() - if not _set['enablehptag']: return + if not sett['enablehptag']: return self.position = position self.owner = owner m = ba.newnode('math', owner=self.owner, attrs={'input1': self.position, 'operation': 'add'}) @@ -127,4 +137,4 @@ class HitPoint(object): def a(): self._Text.delete() m.delete() - self.timer = ba.Timer(100, ba.Call(a), timetype=ba.TimeType.SIM, timeformat=ba.TimeFormat.MILLISECONDS) \ No newline at end of file + self.timer = ba.Timer(100, ba.Call(a), timetype=ba.TimeType.SIM, timeformat=ba.TimeFormat.MILLISECONDS) diff --git a/dist/ba_root/mods/stats/stats.json b/dist/ba_root/mods/stats/stats.json index 5710e52..bf8b692 100644 --- a/dist/ba_root/mods/stats/stats.json +++ b/dist/ba_root/mods/stats/stats.json @@ -1,5 +1,5 @@ { - "startDate": "26-02-2022", + "startDate": "17-07-2022", "stats": { "pb-IF4VAk4a": { "rank": 1, @@ -12,7 +12,20 @@ "kd": 0.0, "avg_score": 0.0, "aid": "pb-IF4VAk4a", - "last_seen":"2022-03-10 12:46:46.070990" + "last_seen": "2022-04-26 17:01:13.715014" + }, + "pb-IF4RU2ECAg==": { + "rank": 2, + "name": "PC452402", + "scores": 767, + "total_damage": 0.0, + "kills": 0, + "deaths": 5, + "games": 46, + "kd": 0.0, + "avg_score": 16.673, + "last_seen": "2022-07-17 16:39:27.777148", + "aid": "pb-IF4RU2ECAg==" } } -} +} \ No newline at end of file diff --git a/dist/ba_root/mods/stats/stats.json.backup b/dist/ba_root/mods/stats/stats.json.backup new file mode 100644 index 0000000..625bf55 --- /dev/null +++ b/dist/ba_root/mods/stats/stats.json.backup @@ -0,0 +1,31 @@ +{ + "startDate": "17-07-2022", + "stats": { + "pb-IF4VAk4a": { + "rank": 1, + "name": "pb-IF4VAk4a", + "scores": 0, + "total_damage": 0.0, + "kills": 0, + "deaths": 0, + "games": 18, + "kd": 0.0, + "avg_score": 0.0, + "aid": "pb-IF4VAk4a", + "last_seen": "2022-04-26 17:01:13.715014" + }, + "pb-IF4RU2ECAg==": { + "rank": 2, + "name": "PC452402", + "scores": 767, + "total_damage": 0.0, + "kills": 0, + "deaths": 5, + "games": 46, + "kd": 0.0, + "avg_score": 17.044, + "last_seen": "2022-07-17 16:39:27.777148", + "aid": "pb-IF4RU2ECAg==" + } + } +} \ No newline at end of file diff --git a/dist/ba_root/mods/stats/stats2022-02-26 000000.json b/dist/ba_root/mods/stats/stats2022-02-26 000000.json new file mode 100644 index 0000000..5710e52 --- /dev/null +++ b/dist/ba_root/mods/stats/stats2022-02-26 000000.json @@ -0,0 +1,18 @@ +{ + "startDate": "26-02-2022", + "stats": { + "pb-IF4VAk4a": { + "rank": 1, + "name": "pb-IF4VAk4a", + "scores": 0, + "total_damage": 0.0, + "kills": 0, + "deaths": 0, + "games": 18, + "kd": 0.0, + "avg_score": 0.0, + "aid": "pb-IF4VAk4a", + "last_seen":"2022-03-10 12:46:46.070990" + } + } +} diff --git a/dist/ba_root/mods/tools/playlist.py b/dist/ba_root/mods/tools/playlist.py index 632a25e..395fd9d 100644 --- a/dist/ba_root/mods/tools/playlist.py +++ b/dist/ba_root/mods/tools/playlist.py @@ -4,6 +4,7 @@ import ba import _ba +import ba.internal #session change by smoothy from ba._freeforallsession import FreeForAllSession from ba._dualteamsession import DualTeamSession @@ -32,14 +33,13 @@ def set_playlist(content): def set_playlist_inline(playlist,newPLaylistType): - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() if (isinstance(session,DualTeamSession) or isinstance(session,CoopSession)) and newPLaylistType=='ffa': - #_ba.get_foreground_host_activity().end_game() - _ba.get_foreground_host_session().end() + ba.internal.get_foreground_host_session().end() _thread.start_new_thread(withDelay,(FreeForAllSession,playlist,)) elif (isinstance(session,FreeForAllSession) or isinstance(session,CoopSession))and newPLaylistType=="teams": - _ba.get_foreground_host_session().end() + ba.internal.get_foreground_host_session().end() _thread.start_new_thread(withDelay,(DualTeamSession,playlist,)) else: updatePlaylist(playlist) @@ -51,14 +51,14 @@ def withDelay(session,playlist): _ba.pushcall(Call(updateSession,session,playlist),from_other_thread=True) def updateSession(session,playlist): - _ba.new_host_session(session) + ba.internal.new_host_session(session) if playlist: updatePlaylist(playlist) def updatePlaylist(playlist): - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() content = ba._playlist.filter_playlist( playlist, sessiontype=type(session), @@ -101,3 +101,21 @@ def setPlaylist(para): _ba.chatmessage(play) +def flush_playlists(): + print("Clearing old playlists..") + for playlist in _ba.app.config["Team Tournament Playlists"]: + _ba.add_transaction( + { + "type":"REMOVE_PLAYLIST", + "playlistType":"Team Tournament", + "playlistName":playlist + }) + _ba.run_transactions() + for playlist in _ba.app.config["Free-for-All Playlists"]: + _ba.add_transaction( + { + "type":"REMOVE_PLAYLIST", + "playlistType":"Free-for-All", + "playlistName":playlist + }) + _ba.run_transactions() diff --git a/dist/ba_root/mods/tools/servercheck.py b/dist/ba_root/mods/tools/servercheck.py index 5832c02..516f752 100644 --- a/dist/ba_root/mods/tools/servercheck.py +++ b/dist/ba_root/mods/tools/servercheck.py @@ -6,6 +6,7 @@ from serverData import serverdata from playersData import pdata import _ba +import ba.internal import urllib.request import json import datetime @@ -31,7 +32,7 @@ class checkserver(object): def check(self): newPlayers = [] - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): newPlayers.append(ros['account_id']) if ros['account_id'] not in self.players and ros[ @@ -55,7 +56,7 @@ class checkserver(object): "sys") except: pass - _ba.disconnect_client(ros['client_id'], 1) + ba.internal.disconnect_client(ros['client_id'], 1) return if settings["whitelist"] and ros["account_id"] != None: @@ -65,7 +66,7 @@ class checkserver(object): clients=[ros['client_id']]) logger.log(d_str + "||" + ros[ "account_id"] + " | kicked > not in whitelist") - _ba.disconnect_client(ros['client_id']) + ba.internal.disconnect_client(ros['client_id']) return @@ -84,7 +85,7 @@ def on_player_join_server(pbid, player_data): now = time.time() # player_data=pdata.get_info(pbid) clid = 113 - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros["account_id"] == pbid: clid = ros["client_id"] if pbid in serverdata.clients: @@ -97,7 +98,7 @@ def on_player_join_server(pbid, player_data): color=(1, 0, 1), transient=True, clients=[clid]) logger.log(pbid + "|| kicked for joining too fast") - _ba.disconnect_client(clid) + ba.internal.disconnect_client(clid) _thread.start_new_thread(reportSpam, (pbid,)) @@ -112,7 +113,7 @@ def on_player_join_server(pbid, player_data): device_strin = "" if player_data["isBan"] or get_account_age(player_data["accountAge"]) < \ settings["minAgeToJoinInHours"]: - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros['account_id'] == pbid: if not player_data["isBan"]: _ba.screenmessage( @@ -120,7 +121,7 @@ def on_player_join_server(pbid, player_data): color=(1, 0, 0), transient=True, clients=[ros['client_id']]) logger.log(pbid + " | kicked > reason:Banned account") - _ba.disconnect_client(ros['client_id']) + ba.internal.disconnect_client(ros['client_id']) return else: @@ -137,7 +138,7 @@ def on_player_join_server(pbid, player_data): verify_account(pbid, player_data) cid = 113 d_st = "xx" - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros['account_id'] == pbid: cid = ros['client_id'] d_st = ros['display_string'] @@ -156,7 +157,7 @@ def on_player_join_server(pbid, player_data): d_string = "" cid = 113 - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros['account_id'] == pbid: d_string = ros['display_string'] cid = ros['client_id'] @@ -177,7 +178,7 @@ def on_player_join_server(pbid, player_data): def verify_account(pb_id, p_data): d_string = "" - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros['account_id'] == pb_id: d_string = ros['display_string'] @@ -309,10 +310,10 @@ def save_ids(ids, pb_id, display_string): def kick_by_pb_id(pb_id, msg): - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros['account_id'] == pb_id: _ba.screenmessage(msg, transient=True, clients=[ros['client_id']]) - _ba.disconnect_client(ros['client_id']) + ba.internal.disconnect_client(ros['client_id']) def get_account_age(ct): diff --git a/dist/bombsquad_headless b/dist/bombsquad_headless index 22c2e6d..bc10d68 100644 Binary files a/dist/bombsquad_headless and b/dist/bombsquad_headless differ