diff --git a/SunnexGB/Heroku-Modules/ASCII.py b/SunnexGB/Heroku-Modules/ASCII.py new file mode 100644 index 0000000..7dc49ab --- /dev/null +++ b/SunnexGB/Heroku-Modules/ASCII.py @@ -0,0 +1,97 @@ +# requires: Pillow numpy +# meta developer: @SunnexGB +# meta banner: https://i.pinimg.com/control1/1200x/24/8d/40/248d40b6afa5bd3c3764556b50635691.jpg +__version__ = (1, 0, 0) + +import io +import logging +from herokutl.types import Message +from .. import loader, utils + +logger = logging.getLogger(__name__) + +@loader.tds +class ASCII(loader.Module): + """Convert images to braille ASCII""" + + strings = { + "name": "ASCII", + "no_lib": "🚫 | Library not loaded", + "no_image": "⚠️ | Reply to image", + "processing": "®️ | Processing...", + "empty": "🤬 | Empty result", + "result": "
{art}
", + "Failed_to_load_library": "Failed to load library", + "Conversion_error": "Conversion error", + } + + strings_ru = { + "_cls_doc": "Конвертирует картинку в braille ASCII", + "no_lib": "🚫 | Библиотека не была загружена", + "no_image": "⚠️ | Ответьте на картинку", + "processing": "®️ | Обработка...", + "empty": "🤬 | Пустой результат", + "result": "
{art}
", + "Failed_to_load_library": "Не удалось загрузить библиотеку", + "Conversion_error": "Ошибка конвертации", + } + + def __init__(self): + self.config = loader.ModuleConfig( + loader.ConfigValue("width", 50), + loader.ConfigValue("threshold", 0.65), + loader.ConfigValue("contrast", 2.0), + loader.ConfigValue("chars", 464), + loader.ConfigValue("invert", False), + ) + self.lib = None + + async def client_ready(self): + try: + self.lib = await self.import_lib("https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/refs/heads/main/Assets/ASCII/ascii-lib.py", suspend_on_error=True) + except Exception: + logger.exception(self.strings["Failed_to_load_library"]) + self.lib = None + + @loader.command(ru_doc="- Отрисовать ASCII-ART (аргумент -f, отправляет файлом)") + async def dotcmd(self, message: Message): + """- Draw ASCII-ART (argument -f, sends as a file)""" + if not self.lib: + return await utils.answer(message, self.strings["no_lib"]) + args = utils.get_args_raw(message) + force_file = "-f" in args.lower() + reply = await message.get_reply_message() or message + if not reply or not ( + reply.photo + or ( + reply.document + and str(getattr(reply.document, "mime_type", "")).startswith("image/") + ) + ): + return await utils.answer(message, self.strings["no_image"]) + msg = await utils.answer(message, self.strings["processing"]) + + try: + image_bytes = await reply.download_media(bytes) + art = self.lib.convert( + image_bytes, + width=self.config["width"], + threshold=self.config["threshold"], + contrast_boost=self.config["contrast"], + invert=self.config["invert"], + target_chars=self.config["chars"], + ) + + except Exception as e: + logger.exception(self.strings["Conversion_error"]) + return await utils.answer(msg, f"
{e}
") + if not art or not art.strip(): + return await utils.answer(msg, self.strings["empty"]) + formatted_art = self.strings("result").format(art=art) + if force_file or len(formatted_art) > 4096: + file = io.BytesIO(art.encode("utf-8")) + file.name = "ascii.txt" + await message.client.send_file(message.peer_id, file) + await msg.delete() + else: + await utils.answer(msg, formatted_art) \ No newline at end of file diff --git a/SunnexGB/Heroku-Modules/Assets/ASCII/ascii-lib.py b/SunnexGB/Heroku-Modules/Assets/ASCII/ascii-lib.py new file mode 100644 index 0000000..457093b --- /dev/null +++ b/SunnexGB/Heroku-Modules/Assets/ASCII/ascii-lib.py @@ -0,0 +1,110 @@ +# requires: Pillow numpy +# Дикие оправдания по поводу именно этого ассета а точнее кода в нем,честно я не знаю что сказать была попытка переписать JS на Py и как бы особых проблем не было, +# до момента пост-обработки на помощь я позвал Claude и он не решил мою проблему от слова совсем,так как в целом я своего рода призираю пилоу,а модуль мне хотелось +# написать я примерно вайб-кодил около 50 минут и я уверен из за этого будет возможно много проблем,в итоге благодаря немного копанию в коде,я нашел проблему и уже +# начал ее решать,НО я опять же вообще не понимал как сделать то что мне нужно,в интернете были сюрсы но будто бы тот или иной мне не подходили? Я не знаю почему я +# дропнул эту идею. Потом я стал искать в JS-е что там вообще можно сделать,в итоге я там импортировал модель какую то блядскую не нужную и опять впустую время +# потратил,думал что тут определено есть решение и снова пошел к ии,вывод опятьь 0 помощи,я не знаю почему я так вцепился лишь в 1 идею.Как бы я мог упростить все, +# даже наверное просто попросив какую то флагмен ии написать модуль и переписать его,но я уже на тот момент по моему мнению сделал много и не хотел ни каким образом +# оставлять это,поэтому через время я нашел сайты которые в целом давали возможность настраивать фильтр,была переделана логика(в целом ее переделал на 60 процентов +# клод,я просто убирал мусор который он испражнял.И вот дальше точно бред я убил более дня на решение проблем которые были решены мной,но результат мне не нравился +# И ОПЯТЬ я пошел просить помощи у гугла,потом понял что возможно даже будет легко(по факту легко,но я ленивый) пока искал,мне перехотелось и я уже потом пытался +# сделать режимы в модуле,что оказалось ужасом ведь они работали,но при возможности гармонично вписать их в код были конфликты И Я В ОЧЕРЕДНОЙ РАЗ ПОШЕЛ К ИИ,спойлер +# он не смог написать лучше чем я,в итоге я отбросил эту идею и думаю в целом никак больше не апдейтать модуль по крупному. +# Да это были оправдания,но зато какие! +import io +import numpy as np +from PIL import Image, ImageFilter, ImageEnhance, ImageOps +from .. import loader + +BASE = 0x2800 +INVERT_MAP = {chr(BASE + c): chr(BASE + (c ^ 0xFF)) for c in range(256)} + + +class AsciiLib(loader.Library): + developer = "@SunnexGB" + + def resize(self, img): + if img.width > 768: + img = img.resize((768, int(img.height * 768 / img.width)), Image.LANCZOS) + w = img.width - img.width % 4 + h = img.height - img.height % 4 + if w != img.width or h != img.height: + img = img.resize((w, h), Image.LANCZOS) + return img + + def mode(self, img, threshold, contrast): + gray = img.convert("L") + edges = ImageOps.invert(gray.filter(ImageFilter.FIND_EDGES)) + contrast_img = ImageEnhance.Contrast(img).enhance(contrast).convert("L") + e = np.array(edges, dtype=np.float32) / 255.0 + c = np.array(contrast_img, dtype=np.float32) / 255.0 + blended = Image.fromarray((e * c * 255).astype(np.uint8), "L") + t = int(threshold * 255) + processed = blended.point(lambda p: 255 if p > t else 0, "L") + return processed, t + + def braille(self, img, threshold, width): + cw = width * 2 + o = -(-round(cw * img.height / img.width) // 4) + ch = 4 * o + px = np.array(img.resize((cw, ch), Image.LANCZOS).convert("L")) + order = [(0,0),(1,0),(2,0),(0,1),(1,1),(2,1),(3,0),(3,1)] + rows = [] + for rs in range(0, ch, 4): + line = [] + for cs in range(0, cw, 2): + grays = [ + int(px[rs+dy, cs+dx]) if (rs+dy < ch and cs+dx < cw) else 255 + for dy, dx in order + ] + bits = list(reversed([1 if g < threshold else 0 for g in grays])) + code = int("".join(str(b) for b in bits), 2) + line.append(chr(BASE + code)) + rows.append("".join(line)) + return rows + + def trim(self, lines): + blank = "\u2800" + while lines and all(c == blank for c in lines[0]): + lines = lines[1:] + while lines and all(c == blank for c in lines[-1]): + lines = lines[:-1] + if not lines: + return lines + left = min(next((i for i,c in enumerate(r) if c!=blank), len(r)) for r in lines) + right = min(next((i for i,c in enumerate(reversed(r)) if c!=blank), len(r)) for r in lines) + return [r[left: len(r)-right if right else len(r)] for r in lines] + + def invert(self, lines): + return ["".join(INVERT_MAP.get(c,c) for c in l) for l in lines] + + def fit(self, img, threshold, chars, width): + lo, hi = 5, 200 + best = "" + for _ in range(14): + mid = (lo + hi)//2 + lines = self.trim(self.braille(img, threshold, mid)) + art = "\n".join(lines) + if len(art) <= chars: + best = art + lo = mid + 1 + else: + hi = mid - 1 + return best + + def convert(self, data, width=50, threshold=0.65, contrast_boost=2.0, invert=False, target_chars=0): + buf = io.BytesIO(data) + img = Image.open(buf) + img.load() + buf.close() + img = img.convert("RGB") + img = self.resize(img) + processed, t = self.mode(img, threshold, contrast_boost) + if target_chars > 0: + art = self.fit(processed, t, target_chars, width) + else: + art = "\n".join(self.trim(self.braille(processed, t, width))) + if invert and art: + art = "\n".join(self.invert(art.split("\n"))) + return art diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/ddialogs/prologue_only.json b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/ddialogs/prologue_only.json new file mode 100644 index 0000000..6dda73a --- /dev/null +++ b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/ddialogs/prologue_only.json @@ -0,0 +1,832 @@ +{ + "prologue": [ + { + "type": "label", + "name": "prologue" + }, + { + "type": "scene", + "kind": "anim", + "name": "prolog_1", + "action": "load_asset", + "location": "anim/prolog_1", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_1.jpg?raw=true" + }, + { + "type": "narration", + "text": "Мне опять снился сон." + }, + { + "type": "narration", + "text": "Этот сон..." + }, + { + "type": "narration", + "text": "Каждую ночь одно и то же." + }, + { + "type": "narration", + "text": "Но наутро, как обычно, всё забудется." + }, + { + "type": "narration", + "text": "Может быть, оно и к лучшему..." + }, + { + "type": "narration", + "text": "Останутся только туманные воспоминания о приоткрытых, словно приглашающих куда-то воротах, рядом с которыми в камне застыли два пионера." + }, + { + "type": "narration", + "text": "А ещё странная девочка...{w} которая постоянно спрашивает:" + }, + { + "type": "scene", + "kind": "bg", + "name": "anim_prolog1_off", + "action": "load_asset", + "location": "anim/anim_prolog1_off", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/anim_prolog1_off.gif?raw=true" + }, + { + "type": "dialogue", + "char_id": "dreamgirl", + "character": "...", + "text": "Ты пойдёшь со мной?" + }, + { + "type": "narration", + "text": "Пойду?.." + }, + { + "type": "narration", + "text": "Но куда?" + }, + { + "type": "narration", + "text": "И зачем?.." + }, + { + "type": "narration", + "text": "Да и где я вообще нахожусь?" + }, + { + "type": "narration", + "text": "Конечно, случись всё на самом деле, наяву, стоило бы непременно испугаться." + }, + { + "type": "narration", + "text": "Как же иначе!" + }, + { + "type": "narration", + "text": "Но это – всего лишь сон.{w} Тот самый, который я вижу каждую ночь." + }, + { + "type": "narration", + "text": "А ведь всё это неспроста!" + }, + { + "type": "narration", + "text": "Необязательно знать где и почему, чтобы понять – что-то происходит." + }, + { + "type": "narration", + "text": "Нечто, отчаянно требующее моего внимания." + }, + { + "type": "narration", + "text": "Ведь всё окружающее меня здесь – реально!" + }, + { + "type": "narration", + "text": "Реально настолько, насколько реальны вещи в моей квартире; я бы мог открыть ворота, услышать скрип петель, смахнуть рукой осыпающуюся ржавчину, потянуть носом свежий прохладный воздух и поёжиться от холода." + }, + { + "type": "narration", + "text": "Мог бы, но для этого надо сдвинуться с места, сделать шаг, пошевелить рукой..." + }, + { + "type": "narration", + "text": "А ведь это сон – я понимаю, но что дальше, что изменит моё понимание?" + }, + { + "type": "narration", + "text": "Ведь здесь – словно по ту сторону потрескавшегося экрана старого телевизора, который из последних сил борется с помехами и силится показать зрителям всё, не упустив ни малейшей детали." + }, + { + "type": "narration", + "text": "Но вот картинка теряет чёткость...{w} Наверное, скоро просыпаться." + }, + { + "type": "narration", + "text": "..." + }, + { + "type": "narration", + "text": "Может быть, спросить у неё что-то?{w} У девочки." + }, + { + "type": "narration", + "text": "Как же её зовут..." + }, + { + "type": "narration", + "text": "Например про звёзды..." + }, + { + "type": "narration", + "text": "Хотя почему про звёзды?" + }, + { + "type": "narration", + "text": "Можно же спросить про ворота!{w} Да, про ворота!" + }, + { + "type": "narration", + "text": "Вот она удивится." + }, + { + "type": "narration", + "text": "Или лучше про букву ё." + }, + { + "type": "narration", + "text": "Хорошая была буква..." + }, + { + "type": "narration", + "text": "Как будто её больше нет!" + }, + { + "type": "narration", + "text": "И какое отношение буквы, ворота и звёзды имеют к этому месту?" + }, + { + "type": "narration", + "text": "Ведь если мне каждую ночь снится этот сон, который потом всё равно забудется, надо искать разгадку здесь и сейчас!" + }, + { + "type": "narration", + "text": "А вот, если присмотреться, можно увидеть Магелланово Облако..." + }, + { + "type": "narration", + "text": "Словно попал в южное полушарие!" + }, + { + "type": "narration", + "text": "..." + }, + { + "type": "narration", + "text": "Во сне всегда больше волнуют мелочи: неестественный цвет травы, невозможная кривизна прямых или своё перекошенное отражение – а реальная опасность, готовая оборвать всё здесь и сейчас, кажется пустяком." + }, + { + "type": "narration", + "text": "Естественно, ведь здесь нельзя умереть." + }, + { + "type": "narration", + "text": "Я точно знаю – я делал это сотни раз." + }, + { + "type": "narration", + "text": "Но если нельзя умереть, нет смысла жить?" + }, + { + "type": "narration", + "text": "Надо будет спросить у девочки: она местная – должна знать!" + }, + { + "type": "narration", + "text": "Да, именно!{w} Спросить, например, про сову." + }, + { + "type": "narration", + "text": "Больно уж птица странная..." + }, + { + "type": "narration", + "text": "А впрочем, неважно..." + }, + { + "type": "narration", + "text": "..." + }, + { + "type": "dialogue", + "char_id": "dreamgirl", + "character": "...", + "text": "Ты пойдёшь со мной?" + }, + { + "type": "narration", + "text": "И каждый раз надо отвечать." + }, + { + "type": "narration", + "text": "Иначе никак, иначе сон не закончится, а я – не проснусь." + }, + { + "type": "route", + "id": "prologue_choice_1" + }, + { + "type": "narration", + "text": "Каждый раз так сложно решить, что же ответить." + }, + { + "type": "narration", + "text": "Где я, что я здесь делаю, кто она такая?" + }, + { + "type": "narration", + "text": "И почему от ответа на этот вопрос зависит так много в моей жизни?" + }, + { + "type": "narration", + "text": "Или не зависит?.." + }, + { + "type": "narration", + "text": "Ведь это просто сон..." + }, + { + "type": "narration", + "text": "Просто сон..." + }, + { + "type": "scene", + "kind": "bg", + "name": "black", + "action": "load_asset", + "location": "bg/black", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/bg/black.png?raw=true", + "duration": null + }, + { + "type": "scene", + "kind": "anim", + "name": "1_prologue", + "action": "load_asset", + "location": "cg/p_kb_1", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/cg/p_kb_1.png?raw=true", + "duration": null + }, + { + "type": "scene", + "kind": "anim", + "name": "2_prologue", + "action": "load_asset", + "location": "cg/p_kb_2", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/cg/p_kb_2.png?raw=true", + "duration": null + }, + { + "type": "scene", + "kind": "anim", + "name": "3_prologue", + "action": "load_asset", + "location": "cg/p_kb_3", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/cg/p_kb_3.png?raw=true", + "duration": null + }, + { + "type": "scene", + "kind": "anim", + "name": "4_prologue", + "action": "load_asset", + "location": "cg/p_kb_4", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/cg/p_kb_4.png?raw=true", + "duration": null + }, + { + "type": "scene", + "kind": "anim", + "name": "5_prologue", + "action": "load_asset", + "location": "cg/p_kb_5", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/cg/p_kb_5.png?raw=true" + }, + { + "type": "narration", + "text": "Экран монитора смотрел на меня словно живой." + }, + { + "type": "narration", + "text": "Иногда мне правда казалось, что он обладает сознанием, своими мыслями и желаниями, стремлениями; умеет чувствовать, любить и страдать." + }, + { + "type": "narration", + "text": "Словно в наших отношениях инструмент не он – неодушевлённый кусок пластика и текстолита, – а я." + }, + { + "type": "narration", + "text": "Наверное, в этом есть доля правды, ведь компьютер на 90% обеспечивает моё общение с внешним миром." + }, + { + "type": "narration", + "text": "Анонимные имиджборды, иногда какие-то чаты, редко – аська или джаббер, ещё реже – форумы." + }, + { + "type": "narration", + "text": "А людей, сидящих по ту сторону сетевого кабеля, попросту не существует!" + }, + { + "type": "narration", + "text": "Все они – всего лишь плод его больной фантазии, ошибка в программном коде или баг ядра, зажившего собственной жизнью." + }, + { + "type": "scene", + "kind": "anim", + "name": "prolog_15", + "action": "load_asset", + "location": "anim/prolog_15", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_15.png?raw=true", + "duration": null + }, + { + "type": "scene", + "kind": "anim", + "name": "prolog_3", + "action": "load_asset", + "location": "anim/prolog_3", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_3.png?raw=true", + "duration": null + }, + { + "type": "scene", + "kind": "anim", + "name": "prolog_4", + "action": "load_asset", + "location": "anim/prolog_4", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_4.png?raw=true" + }, + { + "type": "narration", + "text": "Если посмотреть со стороны на моё существование, то такие мысли покажутся не столь уж бредовыми, а какой-нибудь психолог наверняка поставит мне кучу заумных диагнозов и, возможно, выпишет направление в жёлтый дом." + }, + { + "type": "scene", + "kind": "anim", + "name": "prolog_5", + "action": "load_asset", + "location": "anim/prolog_5", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_5.jpg?raw=true", + "duration": null + }, + { + "type": "scene", + "kind": "anim", + "name": "prolog_14", + "action": "load_asset", + "location": "anim/prolog_14", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_14.jpg?raw=true", + "duration": null + }, + { + "type": "scene", + "kind": "anim", + "name": "prolog_11", + "action": "load_asset", + "location": "anim/prolog_11", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_11.jpg?raw=true" + }, + { + "type": "narration", + "text": "Маленькая квартирка без следов какого бы то ни было ремонта или даже подобия порядка, и вечно одинаковый вид из окна на серый, день и ночь куда-то бегущий мегаполис, – вот условия моей жизни." + }, + { + "type": "scene", + "kind": "anim", + "name": "prolog_2", + "action": "load_asset", + "location": "anim/prolog_2", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_2.jpg?raw=true" + }, + { + "type": "narration", + "text": "Конечно, всё начиналось не так..." + }, + { + "type": "narration", + "text": "Я родился, пошёл в школу, закончил её – всё как у людей." + }, + { + "type": "narration", + "text": "Поступил в институт, где кое-как промучился полтора курса." + }, + { + "type": "narration", + "text": "Работал на паре-тройке разных работ.{w} Иногда даже и неплохо, иногда даже получая за это достойные деньги." + }, + { + "type": "narration", + "text": "Однако всё это казалось чужим, словно списанным с биографии другого человека." + }, + { + "type": "narration", + "text": "Я не ощущал полноту жизни – она словно зациклилась и продолжала идти по кругу.{w} Как в фильме «День сурка»." + }, + { + "type": "narration", + "text": "Только у меня не было выбора, как именно провести этот день, и каждый раз всё повторялось по одной и той же схеме.{w} Схеме пустоты, уныния и отчаяния." + }, + { + "type": "narration", + "text": "Последние несколько лет я просто целыми днями сидел за компьютером." + }, + { + "type": "narration", + "text": "Иногда подворачивались какие-то халтурки, иногда помогали родители." + }, + { + "type": "narration", + "text": "В общем, на жизнь хватало." + }, + { + "type": "narration", + "text": "Это и немудрено, ведь потребности у меня небольшие." + }, + { + "type": "narration", + "text": "На улицу я практически не выхожу, а всё моё общение с людьми сводится к интернет-переписке с анонимами, у которых нет ни реального имени, ни пола, ни возраста." + }, + { + "type": "narration", + "text": "Короче говоря, достаточно типичная жизнь достаточно типичного асоциального человека своего времени.{w} Этакий Обломов XXI века." + }, + { + "type": "narration", + "text": "Может быть, маститый писатель напишет обо мне роман, который станет классикой современной литературы.{w} Или напишу я сам…" + }, + { + "type": "narration", + "text": "Впрочем нет, что себя обманывать – уже не раз пытался, но меня не хватало даже на короткий рассказ." + }, + { + "type": "narration", + "text": "Изучал я и множество других вещей." + }, + { + "type": "narration", + "text": "Рисовать – не дано от природы.{w} Программирование – надоело.{w} Иностранные языки – долго и скучно…" + }, + { + "type": "narration", + "text": "Любил я разве что читать, но даже при этом никогда бы не назвал себя эрудированным человеком." + }, + { + "type": "narration", + "text": "Возможно, я был асом в просмотре аниме и гроссмейстером неумелых шуточек в интернете." + }, + { + "type": "narration", + "text": "Плати мне за это деньги, я бы обрадовался (да и заработал неплохо), но вряд ли так просто можно заполнить пустоту в душе." + }, + { + "type": "scene", + "kind": "bg", + "name": "semen_room_window", + "action": "load_asset", + "location": "bg/semen_room_window", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/bg/semen_room_window.jpg?raw=true" + }, + { + "type": "narration", + "text": "Сегодня очередной типичный день моей типичной жизни типичного неудачника." + }, + { + "type": "narration", + "text": "И именно сегодня мне нужно ехать на встречу институтских товарищей." + }, + { + "type": "narration", + "text": "По правде говоря, совершенно не хотелось." + }, + { + "type": "narration", + "text": "Да и какой смысл, если вместе с ними я отучился всего ничего?" + }, + { + "type": "narration", + "text": "Однако меня всё же уговорил друг, бывший одногруппник, один из немногих, с кем я поддерживал контакт не только в интернете." + }, + { + "type": "scene", + "kind": "anim", + "name": "intro_1", + "action": "load_asset", + "location": "anim/intro_1", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_1.jpg?raw=true", + "duration": null + }, + { + "type": "scene", + "kind": "anim", + "name": "intro_2", + "action": "load_asset", + "location": "anim/intro_2", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_2.jpg?raw=true", + "duration": null + }, + { + "type": "scene", + "kind": "anim", + "name": "intro_3", + "action": "load_asset", + "location": "anim/intro_3", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_3.jpg?raw=true", + "duration": null + }, + { + "type": "scene", + "kind": "anim", + "name": "intro_4", + "action": "load_asset", + "location": "anim/intro_4", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_4.jpg?raw=true", + "duration": null + }, + { + "type": "scene", + "kind": "anim", + "name": "intro_5", + "action": "load_asset", + "location": "anim/intro_5", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_5.jpg?raw=true", + "duration": null + }, + { + "type": "scene", + "kind": "anim", + "name": "intro_6", + "action": "load_asset", + "location": "anim/intro_6", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_6.jpg?raw=true", + "duration": null + }, + { + "type": "scene", + "kind": "anim", + "name": "intro_8", + "action": "load_asset", + "location": "anim/intro_8", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_8.jpg?raw=true", + "duration": null + }, + { + "type": "scene", + "kind": "anim", + "name": "intro_7", + "action": "load_asset", + "location": "anim/intro_7", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_7.jpg?raw=true", + "duration": null + }, + { + "type": "scene", + "kind": "bg", + "name": "bus_stop", + "action": "load_asset", + "location": "bg/bus_stop", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/bg/bus_stop.jpg?raw=true" + }, + { + "type": "narration", + "text": "Вечер. Мороз.{w} Остановка и ожидание автобуса." + }, + { + "type": "narration", + "text": "Я никогда не любил зиму.{w} Впрочем, и жаркое лето – тоже не моя стихия." + }, + { + "type": "narration", + "text": "Просто не вижу смысла выделять какое-то одно время года – не столь важно, какая погода на улице, если ты целыми днями сидишь дома." + }, + { + "type": "narration", + "text": "Автобус сегодня задерживался так сильно, что я уже был готов плюнуть на всё и потратить последнюю пару сотен на такси (совсем не ехать мне почему-то в голову не пришло)." + }, + { + "type": "narration", + "text": "В мозгу, как всегда, роились миллионы мыслей, из которых совершенно невозможно выудить хотя бы одну стоящую." + }, + { + "type": "narration", + "text": "Такую, которую можно закончить, привести в порядок, облечь в форму идеи и претворить в жизнь." + }, + { + "type": "narration", + "text": "Может быть, заняться бизнесом?{w} Но откуда я возьму деньги?" + }, + { + "type": "narration", + "text": "Или пойти опять работать в офис?{w} Нет уж!" + }, + { + "type": "narration", + "text": "Может, стоит попробовать фриланс?{w} Да что я умею, и кому я нужен…" + }, + { + "type": "scene", + "kind": "anim", + "name": "prolog_2", + "action": "load_asset", + "location": "anim/prolog_2", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_2.jpg?raw=true" + }, + { + "type": "narration", + "text": "Вдруг мне вспомнилось детство…{w} Или скорее юношество – 15-17 лет." + }, + { + "type": "narration", + "text": "Почему именно это время?{w} Не знаю." + }, + { + "type": "narration", + "text": "Наверное, потому что тогда всё было проще." + }, + { + "type": "narration", + "text": "Было проще принимать такие сложные сейчас и такие простые тогда решения." + }, + { + "type": "narration", + "text": "Проснувшись с утра, я чётко знал, как пройдёт мой день, а выходных ждал с нетерпением – смогу отдохнуть, заняться любимыми делами: компьютер, футбол, встречи с друзьями." + }, + { + "type": "narration", + "text": "А потом, когда наступит новая неделя, вновь примусь за учёбу." + }, + { + "type": "narration", + "text": "Ведь раньше не возникало этих мучительных вопросов «зачем», «кому это надо», «что изменится, если я это сделаю» или «что не изменится»." + }, + { + "type": "narration", + "text": "Простой поток жизни, такой привычный для любого нормального человека и такой чуждый для меня теперешнего." + }, + { + "type": "narration", + "text": "Время беззаботного детства…{w} Тогда же я и встретил свою первую любовь." + }, + { + "type": "narration", + "text": "Стёрлись из памяти её внешность, характер." + }, + { + "type": "narration", + "text": "Как строчка из профиля в социальной сети осталось лишь имя, да те чувства, которые захлёстывали меня, когда я был с ней.{w} Теплота, нежность, желание заботиться, защитить…" + }, + { + "type": "narration", + "text": "Жаль, что это продолжалось так недолго." + }, + { + "type": "narration", + "text": "Сейчас я уже с трудом могу себе представить что-то подобное." + }, + { + "type": "narration", + "text": "Наверное, и хочется познакомиться с девушкой, только не знаю, как начать диалог, о чём вообще с ней говорить, чем её заинтересовать." + }, + { + "type": "narration", + "text": "Да и подходящих девушек я давно не встречал.{w} Хотя где мне их встретить…" + }, + { + "type": "scene", + "kind": "anim", + "name": "intro_9", + "action": "load_asset", + "location": "anim/intro_9", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_9.jpg" + }, + { + "type": "narration", + "text": "Звук работающего двигателя вернул меня к реальности." + }, + { + "type": "narration", + "text": "Подъехал автобус." + }, + { + "type": "narration", + "text": "«Какой-то он не такой» – мелькнула мысль." + }, + { + "type": "narration", + "text": "Впрочем, какая разница – по этому маршруту ходит только 410-ый." + }, + { + "type": "scene", + "kind": "anim", + "name": "intro_10", + "action": "load_asset", + "location": "anim/intro_10", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_10.jpg?raw=true", + "duration": null + }, + { + "type": "scene", + "kind": "anim", + "name": "intro_11", + "action": "load_asset", + "location": "anim/intro_11", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_11.jpg?raw=true", + "duration": null + }, + { + "type": "scene", + "kind": "anim", + "name": "intro_13", + "action": "load_asset", + "location": "anim/intro_13", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_13.jpg?raw=true", + "duration": null + }, + { + "type": "scene", + "kind": "bg", + "name": "intro_xx", + "action": "load_asset", + "location": "bg/intro_xx", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/bg/intro_xx.jpg?raw=true" + }, + { + "type": "narration", + "text": "Огни пролетают мимо, их холодный свет словно зажигает внутри давно погасшие чувства." + }, + { + "type": "narration", + "text": "Или не зажигает, а просто пробуждает…" + }, + { + "type": "narration", + "text": "Ведь «они» уже давно живут во мне, то затихая, то просыпаясь вновь." + }, + { + "type": "narration", + "text": "Какая-то очень известная мелодия играла в радиоприёмнике у водителя.{w} Но я её не слушал." + }, + { + "type": "narration", + "text": "Я смотрел в запотевшее окно автобуса на проезжающие мимо машины." + }, + { + "type": "narration", + "text": "Ведь люди куда-то спешат, ведь им что-то нужно, и, погружённые в свои дела, они не задумываются о вопросах, мучающих меня." + }, + { + "type": "narration", + "text": "Наверное, у них тоже есть свои серьёзные проблемы, а может, им живётся куда легче." + }, + { + "type": "narration", + "text": "Знать наверняка нельзя, так как все люди разные.{w} Или не разные?" + }, + { + "type": "narration", + "text": "Бывает, поступки человека легко предсказуемы, но, пытаясь заглянуть к нему в душу, видишь лишь непроглядную тьму." + }, + { + "type": "narration", + "text": "..." + }, + { + "type": "narration", + "text": "Автобус приближался к центру, и мои мысли прервал яркий свет огней большого города." + }, + { + "type": "narration", + "text": "Сотни рекламных вывесок, тысячи машин, миллионы людей." + }, + { + "type": "narration", + "text": "Я смотрел на это светопреставление, и мне почему-то безумно захотелось спать." + }, + { + "type": "narration", + "text": "Глаза закрылись всего на полсекунды и…" + }, + { + "type": "scene", + "kind": "bg", + "name": "black", + "action": "load_asset", + "location": "bg/black", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/bg/int_bus_black.jpg?raw=true" + }, + { + "type": "opening", + "kind": "opening", + "name": "opening", + "action": "load_asset", + "location": "opening/opening", + "raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/opening/opening.mp4?raw=true" + } + ] +} \ No newline at end of file diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/ddialogs/routes_prologue.json b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/ddialogs/routes_prologue.json new file mode 100644 index 0000000..0563376 --- /dev/null +++ b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/ddialogs/routes_prologue.json @@ -0,0 +1,62 @@ +{ + "prologue_choice_1": { + "question": "Иначе никак, иначе сон не закончится, а я – не проснусь. — что выбрать?", + "chapter": "prologue", + "options": { + "Да, я пойду с тобой": { + "effects": {} + }, + "Нет, я останусь здесь": { + "effects": {} + } + } + }, + "endings": { + "labels": [ + "main_good_ending", + "main_bad_ending", + "sl_good_ending", + "sl_bad_ending", + "dv_good_ending", + "dv_bad_ending", + "un_good_ending", + "un_bad_ending", + "us_good_ending", + "us_bad_ending", + "mi_ending", + "uv_ending", + "harem_ending" + ], + "routes": { + "sl": { + "good": "sl_good_ending", + "bad": "sl_bad_ending", + "point_key": "sl_points" + }, + "dv": { + "good": "dv_good_ending", + "bad": "dv_bad_ending", + "point_key": "dv_points" + }, + "un": { + "good": "un_good_ending", + "bad": "un_bad_ending", + "point_key": "un_points" + }, + "us": { + "good": "us_good_ending", + "bad": "us_bad_ending", + "point_key": "us_points" + }, + "mi": { + "single": "mi_ending", + "point_key": "mi_points" + }, + "uv": { + "single": "uv_ending", + "point_key": "uv_points" + } + }, + "fallback": "main_bad_ending" + } +} diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/anim_prolog1_off.gif b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/anim_prolog1_off.gif new file mode 100644 index 0000000..776d00b Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/anim_prolog1_off.gif differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_1.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_1.jpg new file mode 100644 index 0000000..cef9501 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_1.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_10.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_10.jpg new file mode 100644 index 0000000..15a162e Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_10.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_11.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_11.jpg new file mode 100644 index 0000000..54ceb2f Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_11.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_13.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_13.jpg new file mode 100644 index 0000000..1098426 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_13.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_2.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_2.jpg new file mode 100644 index 0000000..acc384b Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_2.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_3.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_3.jpg new file mode 100644 index 0000000..c37580f Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_3.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_4.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_4.jpg new file mode 100644 index 0000000..581db49 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_4.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_5.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_5.jpg new file mode 100644 index 0000000..0782559 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_5.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_6.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_6.jpg new file mode 100644 index 0000000..cb835fc Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_6.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_7.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_7.jpg new file mode 100644 index 0000000..85ac729 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_7.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_8.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_8.jpg new file mode 100644 index 0000000..f418f23 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_8.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_9.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_9.jpg new file mode 100644 index 0000000..4d16e85 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/intro_9.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_1.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_1.jpg new file mode 100644 index 0000000..5a45e62 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_1.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_11.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_11.jpg new file mode 100644 index 0000000..6e6e5cb Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_11.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_14.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_14.jpg new file mode 100644 index 0000000..21d49c4 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_14.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_15.png b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_15.png new file mode 100644 index 0000000..54c7614 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_15.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_2.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_2.jpg new file mode 100644 index 0000000..c525295 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_2.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_3.png b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_3.png new file mode 100644 index 0000000..179872c Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_3.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_4.png b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_4.png new file mode 100644 index 0000000..57d7345 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_4.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_5.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_5.jpg new file mode 100644 index 0000000..c59767e Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/anim/prolog_5.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/bg/black.png b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/bg/black.png new file mode 100644 index 0000000..e382139 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/bg/black.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/bg/bus_stop.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/bg/bus_stop.jpg new file mode 100644 index 0000000..51bdfa7 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/bg/bus_stop.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/bg/int_bus_black.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/bg/int_bus_black.jpg new file mode 100644 index 0000000..46ef32f Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/bg/int_bus_black.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/bg/intro_xx.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/bg/intro_xx.jpg new file mode 100644 index 0000000..6af1f8e Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/bg/intro_xx.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/bg/semen_room_window.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/bg/semen_room_window.jpg new file mode 100644 index 0000000..63f95ac Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/bg/semen_room_window.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/cg/p_kb_1.png b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/cg/p_kb_1.png new file mode 100644 index 0000000..693cd17 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/cg/p_kb_1.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/cg/p_kb_2.png b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/cg/p_kb_2.png new file mode 100644 index 0000000..f182134 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/cg/p_kb_2.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/cg/p_kb_3.png b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/cg/p_kb_3.png new file mode 100644 index 0000000..6d708e4 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/cg/p_kb_3.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/cg/p_kb_4.png b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/cg/p_kb_4.png new file mode 100644 index 0000000..4e8fbba Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/cg/p_kb_4.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/cg/p_kb_5.png b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/cg/p_kb_5.png new file mode 100644 index 0000000..d847f0e Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/cg/p_kb_5.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/in_telegram_images/Save_Menu.png b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/in_telegram_images/Save_Menu.png new file mode 100644 index 0000000..15e51fc Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/in_telegram_images/Save_Menu.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/in_telegram_images/Start_Menu.jpg b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/in_telegram_images/Start_Menu.jpg new file mode 100644 index 0000000..856fcf2 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/in_telegram_images/Start_Menu.jpg differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/in_telegram_images/logo.webp b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/in_telegram_images/logo.webp new file mode 100644 index 0000000..79bd64a Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/images/1920/in_telegram_images/logo.webp differ diff --git a/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/opening/opening.mp4 b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/opening/opening.mp4 new file mode 100644 index 0000000..fabbee1 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Everlasting_Summer/opening/opening.mp4 differ diff --git a/SunnexGB/Heroku-Modules/Assets/Hangman/1.png b/SunnexGB/Heroku-Modules/Assets/Hangman/1.png new file mode 100644 index 0000000..5aec2db Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Hangman/1.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Hangman/10.png b/SunnexGB/Heroku-Modules/Assets/Hangman/10.png new file mode 100644 index 0000000..d3726a9 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Hangman/10.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Hangman/2.png b/SunnexGB/Heroku-Modules/Assets/Hangman/2.png new file mode 100644 index 0000000..be79b1f Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Hangman/2.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Hangman/3.png b/SunnexGB/Heroku-Modules/Assets/Hangman/3.png new file mode 100644 index 0000000..d62c480 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Hangman/3.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Hangman/4.png b/SunnexGB/Heroku-Modules/Assets/Hangman/4.png new file mode 100644 index 0000000..f4805d6 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Hangman/4.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Hangman/5.png b/SunnexGB/Heroku-Modules/Assets/Hangman/5.png new file mode 100644 index 0000000..dd9e6f0 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Hangman/5.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Hangman/6.png b/SunnexGB/Heroku-Modules/Assets/Hangman/6.png new file mode 100644 index 0000000..f4f9d28 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Hangman/6.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Hangman/7.png b/SunnexGB/Heroku-Modules/Assets/Hangman/7.png new file mode 100644 index 0000000..4f84d6d Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Hangman/7.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Hangman/8.png b/SunnexGB/Heroku-Modules/Assets/Hangman/8.png new file mode 100644 index 0000000..8ad057b Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Hangman/8.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Hangman/9.png b/SunnexGB/Heroku-Modules/Assets/Hangman/9.png new file mode 100644 index 0000000..a1da503 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Hangman/9.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Hangman/full_hp.png b/SunnexGB/Heroku-Modules/Assets/Hangman/full_hp.png new file mode 100644 index 0000000..c133063 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/Hangman/full_hp.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/Hangman/words.txt b/SunnexGB/Heroku-Modules/Assets/Hangman/words.txt new file mode 100644 index 0000000..3ad2cb6 --- /dev/null +++ b/SunnexGB/Heroku-Modules/Assets/Hangman/words.txt @@ -0,0 +1,1488 @@ +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА +КОШКА +СОБАКА +ПИТОН +ПРИВЕТ +СОЛНЦЕ +ЛУНА +ЗВЕЗДА +ЗЕМЛЯ +ВОДА +ОГОНЬ +ВЕТЕР +ДЕРЕВО +КАМЕНЬ +НЕБО +АЛМАЗ +БЕРЕЗА +ГРОЗА +ДОРОГА +ЕЖЕВИКА +ЖЕЛЕЗО +ЗЕРКАЛО +ИСКРА +ЙОГУРТ +КАРАНДАШ +ЛАБИРИНТ +МОЛОКО +НОЧЬ +ОБЛАКО +ПЕСОК +РАДУГА +СНЕЖИНКА \ No newline at end of file diff --git a/SunnexGB/Heroku-Modules/Assets/Mikuru/cultural_words_en.txt b/SunnexGB/Heroku-Modules/Assets/Mikuru/cultural_words_en.txt new file mode 100644 index 0000000..397d620 --- /dev/null +++ b/SunnexGB/Heroku-Modules/Assets/Mikuru/cultural_words_en.txt @@ -0,0 +1,400 @@ +2g1c +2 girls 1 cup +acrotomophilia +alabama hot pocket +alaskan pipeline +anal +anilingus +anus +apeshit +arsehole +ass +asshole +assmunch +auto erotic +autoerotic +babeland +baby batter +baby juice +ball gag +ball gravy +ball kicking +ball licking +ball sack +ball sucking +bangbros +bangbus +bareback +barely legal +barenaked +bastard +bastardo +bastinado +bbw +bdsm +beaner +beaners +beaver cleaver +beaver lips +beastiality +bestiality +big black +big breasts +big knockers +big tits +bimbos +birdlock +bitch +bitches +black cock +blonde action +blonde on blonde action +blowjob +blow job +blow your load +blue waffle +blumpkin +bollocks +bondage +boner +boob +boobs +booty call +brown showers +brunette action +bukkake +bulldyke +bullet vibe +bullshit +bung hole +bunghole +busty +butt +buttcheeks +butthole +camel toe +camgirl +camslut +camwhore +carpet muncher +carpetmuncher +chocolate rosebuds +cialis +circlejerk +cleveland steamer +clit +clitoris +clover clamps +clusterfuck +cock +cocks +coprolagnia +coprophilia +cornhole +coon +coons +creampie +cum +cumming +cumshot +cumshots +cunnilingus +cunt +darkie +date rape +daterape +deep throat +deepthroat +dendrophilia +dick +dildo +dingleberry +dingleberries +dirty pillows +dirty sanchez +doggie style +doggiestyle +doggy style +doggystyle +dog style +dolcett +domination +dominatrix +dommes +donkey punch +double dong +double penetration +dp action +dry hump +dvda +eat my ass +ecchi +ejaculation +erotic +erotism +escort +eunuch +fag +faggot +fecal +felch +fellatio +feltch +female squirting +femdom +figging +fingerbang +fingering +fisting +foot fetish +footjob +frotting +fuck +fuck buttons +fuckin +fucking +fucktards +fudge packer +fudgepacker +futanari +gangbang +gang bang +gay sex +genitals +giant cock +girl on +girl on top +girls gone wild +goatcx +goatse +god damn +gokkun +golden shower +goodpoop +goo girl +goregasm +grope +group sex +g-spot +guro +hand job +handjob +hard core +hardcore +hentai +homoerotic +honkey +hooker +horny +hot carl +hot chick +how to kill +how to murder +huge fat +humping +incest +intercourse +jack off +jail bait +jailbait +jelly donut +jerk off +jigaboo +jiggaboo +jiggerboo +jizz +juggs +kike +kinbaku +kinkster +kinky +knobbing +leather restraint +leather straight jacket +lemon party +livesex +lolita +lovemaking +make me come +male squirting +masturbate +masturbating +masturbation +menage a trois +milf +missionary position +mong +motherfucker +mound of venus +mr hands +muff diver +muffdiving +nambla +nawashi +negro +neonazi +nigga +nigger +nig nog +nimphomania +nipple +nipples +nsfw +nsfw images +nude +nudity +nutten +nympho +nymphomania +octopussy +omorashi +one cup two girls +one guy one jar +orgasm +orgy +paedophile +paki +panties +panty +pedobear +pedophile +pegging +penis +phone sex +piece of shit +pikey +pissing +piss pig +pisspig +playboy +pleasure chest +pole smoker +ponyplay +poof +poon +poontang +punany +poop chute +poopchute +porn +porno +pornography +prince albert piercing +pthc +pubes +pussy +queaf +queef +quim +raghead +raging boner +rape +raping +rapist +rectum +reverse cowgirl +rimjob +rimming +rosy palm +rosy palm and her 5 sisters +rusty trombone +sadism +santorum +scat +schlong +scissoring +semen +sex +sexcam +sexo +sexy +sexual +sexually +sexuality +shaved beaver +shaved pussy +shemale +shibari +shit +shitblimp +shitty +shota +shrimping +skeet +slanteye +slut +s&m +smut +snatch +snowballing +sodomize +sodomy +spastic +spic +splooge +splooge moose +spooge +spread legs +spunk +strap on +strapon +strappado +strip club +style doggy +suck +sucks +suicide girls +sultry women +swastika +swinger +tainted love +taste my +tea bagging +threesome +throating +thumbzilla +tied up +tight white +tit +tits +titties +titty +tongue in a +topless +tosser +towelhead +tranny +tribadism +tub girl +tubgirl +tushy +twat +twink +twinkie +two girls one cup +undressing +upskirt +urethra play +urophilia +vagina +venus mound +viagra +vibrator +violet wand +vorarephilia +voyeur +voyeurweb +voyuer +vulva +wank +wetback +wet dream +white power +whore +worldsex +wrapping men +wrinkled starfish +yaoi +yellow showers +yiffy +zoophilia diff --git a/SunnexGB/Heroku-Modules/Assets/Mikuru/cultural_words_ru.txt b/SunnexGB/Heroku-Modules/Assets/Mikuru/cultural_words_ru.txt new file mode 100644 index 0000000..b7c5618 --- /dev/null +++ b/SunnexGB/Heroku-Modules/Assets/Mikuru/cultural_words_ru.txt @@ -0,0 +1,2187 @@ +анафемовоёбый +архипиздоит +архипиздрит +безмудовый +безмудый +беспезды +беспиздая +бздение +бздеть +бздех +бзднуть +бздун +бздунья +бздюх +бздюха +блежник +блёжник +блудовместилище +блудодей +блудодейский +блудугонище +бля +блябу +блябуду +бляд +блядва +блядво +блядевина +блядей +блядепиздопроёбище +блядеха +бляди +блядиада +блядина +блядистка +блядистость +блядища +блядище +блядки +бляднота +блядование +блядовать +блядовитый +блядовозка +блядогандонский +блядогон +блядодерьмо +блядок +блядолиз +блядопростервозная +блядословник +блядоход +блядоящерица +блядская +блядский +блядского +блядское +блядство +блядствовать +блядун +блядунья +блядь +блядюга +блядюра +блядюшка +блядюшник +блядями +блять +бляхомудия +болтохуярой +бордель +вафлеотстойник +вафлист +вертохуй +вжопить +вжопиться +взбляд +вздрачивание +вздрачивать +вздрачиваться +вздрочить +вздрочиться +вздрючивание +вздрючивать +вздрючить +вз'ебывать +взъебка +взъёбка +взъебнуть +взъебщик +взъёбывать +взьебка +взьебывать +вислозадая +воспиздозаолупоклинившаяся +восьмиблядское +восьмихуй +впиздить +впиздиться +впиздохать +впиздохивать +впиздохиваться +впиздошиваться +впиздошить +впиздронивать +впиздрониваться +впиздюливать +впиздюливаться +впиздюлить +впиздюривать +впиздюриваться +впиздюрить +впиздякать +впиздякивать +впиздяривать +впиздяриваться +впиздярить +впиздяхать +впиздяхивать +впиздяхиваться +впиздячивать +впиздячил +впиздячила +впиздячить +впиздяшивать +впиздяшиваться +впиздяшить +впизживать +впизживаться +всхуяренный +вхуематери +вхуйкать +вхуйнуть +вхуйнуться +вхуя +вхуякать +вхуякаться +вхуякивать +вхуякиваться +вхуякнуть +вхуякнуться +вхуяривание +вхуяривать +вхуяриваться +вхуярить +вхуяриться +вхуячивание +вхуячивать +вхуячиваться +вхуячить +вхуячиться +вхуяшивать +вхуяшиваться +вхуяшить +вхуяшиться +въебать +въебаться +въебашивать +въебашиваться +въебашить +въебашиться +въебенивать +въебениваться +въебенить +въебениться +въёбывать +выблядовал +выблядовала +выблядовать +выблядодерьмо +выблядок +выблядыш +выебанный +выебат +выебать +выебаться +выебок +выебон +выебопропиздище +выебу +выёбывается +выебываешся +выёбывать +выёбываться +выёбываются +выперданный +выпердоватое +выпердоговенный +выпердопроссаная +выпиздеться +выпиздить +высераться +высраная +высраномудоватое +высранохуевое +высрать +высраться +выссаногнойное +выссанохуеплёт +выссать +выссаться +выссереть +выхуякивание +выхуякивать +выхуяривание +выхуяривать +выхуячивание +выхуячивать +гавнопрягопизд +гандон +гандонный +гандоноскотская +гандоносрака +гандоносучий +гандонохеровая +гандонский +глупизди +глыбоебливое +гнидоблядун +гнидопаскудная +гнидское +гнобилище +гноепадла +гноепромандоватое +говенный +говнецо +говнистый +говниться +говно +говновоз +говнодав +говнодерьмище +говноеб +говноёб +говноебский +говноед +говнозалупский +говномер +говномес +говноперданный +говносерка +говночист +говнюк +говняк +говняный +голоёбица +голожопая +голомудый +голопиздая +гомик +гомосек +гондон +гонореей +гонорея +греблядь +гужеёб +гусоёб +давалка +дароёб +двужопник +двужопостворчатый +дерьмо +дерьмовый +дерьмоед +дерьмопрозалупское +дерьмохеропиздократ +дерьмохеропиздократия +дерьмохеропиздократка +дерьмохеропиздократы +дилдо +додрочить +додрочиться +доебалась +доебались +доебался +доебать +доебаться +доебенивать +доебениваться +доебенить +доебениться +доёбывать +долбоеб +долбоёб +долбоебатина +долбоебическая +допиздеться +допиздить +допиздиться +допиздоболивать +допиздоболиваться +допиздоболиться +допиздовать +допиздоваться +допиздовывать +допиздовываться +допиздохать +допиздохаться +допиздохивать +допиздохиваться +допиздошивать +допиздошиваться +допиздошить +допиздошиться +допиздюкать +допиздюкаться +допиздюкивать +допиздюкиваться +допиздюливать +допиздюливаться +допиздюлить +допиздюлиться +допиздюривать +допиздюриваться +допиздюрить +допиздюриться +допиздюхать +допиздюхаться +допиздюхивать +допиздюхиваться +допиздякать +допиздякаться +допиздякивать +допиздякиваться +допиздяривать +допиздяриваться +допиздярить +допиздяриться +допиздяхать +допиздяхаться +допиздяхивать +допиздяхиваться +допиздячивать +допиздячиваться +допиздячить +допиздячиться +допиздяшивать +допиздяшиваться +допиздяшить +допиздяшиться +допизживать +дотрахать +дотрахаться +дохуйнуть +дохуякать +дохуякаться +дохуякивать +дохуякиваться +дохуяривать +дохуяриваться +дохуярить +дохуяриться +дохуячивать +дохуячиваться +дохуячить +дохуячиться +дохуяшивать +дохуяшиваться +дохуяшить +драноебливая +дрисня +дристать +дристун +дристуха +дрочена +дроченная +дроченье +дрочепиздище +дрочепроговенное +дрочепропердоватая +дрочепросволота +дроческотина +дрочила +дрочилка +дрочилыцик +дрочить +дрочиться +дрочка +дрючить +дрючиться +дурапиздия +дуроеб +дуроёб +дядеёб +ёб +ёбака +ебаквакнутый +ебал +ебалка +ебало +ебалово +ебалом +ебальник +ебальные +ебальный +ебанагандонный +ебанатик +ебанашка +ебаная +ебандей +ебанёшься +ебанная +ебанул +ебанула +ебанулась +ебанулись +ебанулось +ебанулся +ебанутые +ебанутый +ебануть +ебануться +ебаный +ёбаный +ебаных +ебанько +ебапира +ебаришка +ебарь +ёбарь +ебат +ебаторий +ебатория +ебатый +ебать +ебатьс +ебаться +ебашит +ебеня +ебёт +ебец +еби +ебиблядская +ебистика +ебись +ебитесь +ёбкость +еблан +ебланистый +ебланить +ебливая +ебливый +еблище +ебло +еблом +еблоухий +еблысь +ебля +ёбля +ёбнул +ебнутый +ёбнутый +ебнуть +ёбнуть +ебнуться +ёбнуться +ебня +ебоблядище +ебоватая +еболожье +ёбс +ебу +ебукентий +ебун +ебунище +ебучее +ебучий +ебущегося +елда +елдак +елдачить +елдище +елду +елупень +женщина-заебись +жидоёб +жидоёбка +жидоёбский +жирнозадый +жопа +жопастая +жопенци +жопища +жопка +жопник +жоповатое +жопоеб +жопой +жополиз +жополизание +жопоногий +жопопроебина +жопосер +жопочка +жопочник +жопство +жопу +забздеть +заблядовать +заблядоваться +задница +задрачивать +задрачиваться +задристать +задрока +задроченное тримандище +задроченный +задрочепидер +задрочепрогнидище +задрочепроговно +задроческоготическое +задроческотское +задрочила +задрочить +задрочиться +задрочун +задрючить +задрючиться +заёб +заеба +заебал +заебала +заебали +заебанец +заебанный +заёбанный +заебательская +заебать +заебаться +заебашивать +заебашиваться +заебашить +заебашиться +заебенивать +заебениваться +заебенить +заебениться +заебёшь +заебись +заебла +заебучестью +заебцовый +заебываться +залупа +залупаться +залупенец +залупенить +залупень +залупистый +залупить +залупиться +залупляться +залупоглазая +залупоглазое +залупоголовая +залуполиз +залупская +залупский +залупу +замудоебина +замудохаться +запиздадуриная +запиздарить +запизденевать +запизденелый +запизденная +запиздеть +запиздие +запиздить +запиздиться +запиздоболивать +запиздоболиваться +запиздоболить +запиздоболиться +запиздовать +запиздоваться +запиздовывать +запиздовываться +запиздомедузь +запиздохать +запиздошивать +запиздошиваться +запиздошить +запиздошиться +запиздюкать +запиздюкаться +запиздюкивать +запиздюкиваться +запиздюливать +запиздюливаться +запиздюлить +запиздюлиться +запиздюривать +запиздюриваться +запиздюрить +запиздюриться +запиздюхать +запиздюхаться +запиздюхивать +запиздюхиваться +запиздючивать +запиздючиваться +запиздючить +запиздючиться +запизживаться +засерать +засеря +засондряченный +засракомондохуй +засранец +засранка +засранный +засратый +засрать +засраться +засрун +зассать +затраханный +затрахать +затрахаться +затрахивать +затрахиваться +захуить +захуйнуть +захуйнуться +захуякать +захуякаться +захуякивать +захуякиваться +захуялась +захуяривать +захуяриваться +захуярить +захуяриться +захуяченный +захуячивать +захуячиваться +захуячить +захуячиться +захуяшивать +захуяшиваться +захуяшить +захуяшиться +злоебучая +злоебучее +злоебучести +злоебучий +злоебучим +злопиздии +изговнять +изговняться +издрочиться +измандить +измандиться +измандовать +измандоваться +измандовывать +измандовываться +изъеб +изъебать +изъебаться +изъебашивать +изъебашиваться +изъебашить +изъебашиться +изъебенивать +изъебениваться +изъебенить +изъебениться +изъебнулась +изъебнулся +изъебнуться +илда +испизделась +испизделся +испиздеться +испиздить +испражнение +испражняться +исхуякать +исхуякаться +исхуякивать +исхуякиваться +исхуяривать +исхуярить +исхуяриться +исхуячить +какать +какашка +кастрат +кастрировать +клитор +клоака +клоповыёбистый +кнахт +козлоблядской +козлоёб +козлоёбина +козлоёбиться +козлоёбище +козоёб +козоёбиться +коноёб +коноёбиться +кончить +коростоебина +косоёбится +косоебить +косоебиться +кривохуй +курва +курвенный +курвиный +куроёб +лахудра +лох +лохматка +лохудра +малоебущий +манда +мандавоха +мандавошка +мандавошный +мандей +мандеть +мандилищи +мандить +мандиться +мандища +мандище +мандоватая +мандовать +мандогнидища +мандопроблядоватая +мандопродерьмище +мандопроеб +мандопростерва +мандопроушечная +мандопроушина +мандоскотина +мандохать +мандохаться +мандохвост +мандохивать +мандохиваться +мандошить +мандюк +мастурбатор +минет +минетить +минетка +минетчик +минетчица +многоблядопропадловый +многоблядопросволота +многоблядская +многоблядь +многоебоватая +многоебоный +многоебошлюхская +многоебучий +многожопа +многомудоватое +многопизда +многопиздище +многопиздная +многопиздоблядун +многопиздопропадла +многостерводерьмо +многостервопропидрила +многостервохеровый +многостервохуенная +мозгоеб +мозгоёб +мозгоебатель +мозгоебать +мозгоебка +мокрожопый +мокропиздая +мондавошь +мондило +мондозалупленной +мордоблядина +мочиться +мудаблядин +мудагавнопердь +мудак +мудами +мудах +мудаханная +мудачьё +мудашвили +муде +мудеть +мудила +мудило +мудильщик +мудистое +мудистый +мудить +мудище +мудня +мудоблядскую +мудовафлоебище +мудодей +мудоеб +мудоёб +мудоеблю +мудозвон +мудой +мудопроеб +мудопроебное +мудопрошлюхское +мудорваней +мудями +муйня +набздеть +наблядовал +наблядоваться +надристать +надроченный +надрочивать +надрочить +надрочиться +наебалово +наебанный +наебать +наебаться +наебашился +наебениться +наебка +наёбка +наебнулась +наебнулся +наебнуть +наебнуться +наебщик +наёбывает +наебывать +наебываться +наебыш +найвыебенищее +накакать +накакаться +накакивать +напиздеть +напиздить +напиздошить +напиздюрить +напиздюриться +насрать +насраться +нассать +нассаться +настоебать +натрахать +натрахаться +натрахивать +натрахиваться +нахуевертеть +нахуй +нахуйник +нахуякать +нахуякаться +нахуякивать +нахуякиваться +нахуяривать +нахуяриваться +нахуярить +нахуяриться +нахуячивать +нахуячиваться +нахуячить +нахуячиться +нахуяшить +невъебения +невъебенной +невъебенный +невъебенным +невьебенно +недоеба +недоебанная +недоебанный +недоебок +недоносок +неебущий +нехуёвый +нехуй +нищеебство +обдристанный +обдристать +обдристаться +обдрочиться +обебать +оберблядь +облядипизденелости +облямудевшая +облямуденный +обосранец +обосранная +обосраный +обосрать +обосраться +обоссанец +обоссаный +обоссать +обоссаться +обоссывать +обоссываться +обпиздить +обпиздиться +обпиздовать +обпиздоваться +обпиздовывать +обпиздовываться +обпиздохать +обпиздохаться +обпиздохивать +обпиздохиваться +обпиздошить +обтрахать +обтрахаться +обтрахивать +обтрахиваться +обхуярить +обхуяриться +обхуячить +объебал +объебала +объебалово +объебательство +объебать +объебаться +объебенить +объебешь +объебнуть +объебон +объебос +одинхуй +однапизда +однохуйственно +оебать +оебашивать +оебашить +оебенивать +оебенить +оебыват +опедерастить +опизде +опизденевать +опизденеть +опизденно +опизденный +опиздеть +опиздить +опиздихуительный +опиздоумел +оскотоёбился +ослоёб +ослоёбиться +остоебал +остоебала +остоебали +остоебать +остоебенило +остоебенить +остоебеть +остопиздело +остопиздеть +остохуело +остохуеть +отдрачивать +отдрачиваться +отдрочить +отдрочиться +отпиздить +отпизднутый +отпиздошить +отпиздячить +отпиздяшивание +отпиздяшивать +отпиздяшиваться +отпиздяшить +отпиздяшиться +отсандаленный +отсасывать +отсасываться +отсосать +отсосаться +оттраханная +оттрахать +оттрахаться +оттрахивать +оттрахиваться +отхерачить +отхуякать +отхуякаться +отхуякивать +отхуякиваться +отхуяривать +отхуяриваться +отхуярить +отхуяриться +отхуячивать +отхуячиваться +отхуячить +отхуячиться +отхуяшивать +отхуяшиваться +отхуяшить +отхуяшиться +отшмаренная +отъебать +отъебаться +отъебашивание +отъебашивать +отъебашиваться +отъебашить +отъебенивать +отъебениваться +отъебенить +отъебениться +отъебись +отъебнуть +отъебывание +отъебывать +отъебываться +отьебаться +отьебашиться +отьебенивание +отьебись +отьебнуться +охлуебень +охуебаннейшая +охуевательский +охуевать +охуевающая +охуевающее +охуевающий +охуевшее +охуевший +охуение +охуения +охуенно +охуенные +охуенный +охуеть +охуительно +охуительный +охуякать +охуякаться +охуякивать +охуякиваться +охуякнуть +охуякнуться +охуяривать +охуяриваться +охуярить +охуяриться +охуячивать +охуячиваться +охуячить +охуячиться +охуяшивать +охуяшиваться +охуяшить +охуяшиться +оххуетительно +очко +падла +падловый +падлопроскотское +падлопросраное +падлопрошлюха +падлюка +педераст +педерастина +педерастический +педерастия +педик +педрик +педрило +пежить +пезды +пёзды +пердеж +пердеть +перднуть +пердодроченное +пердомудоватый +пердопрогнойная +пердопромудище +пердун +пердунец +пердь +перебздеть +передрачивать +передрочить +передрочиться +переёб +переёба +переебать +переебаться +переебашить +перекосоебленным +перемондоебленная +перепиздрюченный +перепиздюханный +перетраханной +перетрахать +перетрахаться +перетрахивать +перетрахиваться +перехуйнуть +перехуйнуться +перехуякать +перехуякаться +перехуякивать +перехуякиваться +перехуякнуть +перехуякнуться +перехуяривать +перехуяриваться +перехуярить +перехуяриться +перехуячивать +перехуячить +перехуячиться +пидарас +пидарастической +пидарасы +пидероперданное +пидор +пидорас +пидорестической +пидористический +пидрасня +пидрила +пизввда +пизда +пиздабол +пиздавлетины +пиздагрыз +пиздадавленный +пиздаёб +пиздакнутый +пиздакрыл +пиздалон +пизданутая +пиздануть +пиздануться +пиздапроебина +пиздарванка +пиздасер +пиздатая +пиздато +пиздатое +пиздатый +пиздаш +пизде +пиздёж +пизделиться +пиздельник +пизделякает +пизделякать +пизденея +пизденка +пизденочка +пизденыш +пиздёныш +пиздень +пиздеть +пиздец +пиздецкая +пиздецкий +пиздецкое +пиздий +пиздилища +пиздилищный +пиздиной +пиздину +пиздить +пиздища +пиздоаллюренный +пиздобездонная +пиздобесины +пиздоблошка +пиздоблятское +пиздобол +пиздобрат +пиздобратия +пиздобрюхой +пиздобузина +пиздоварнаковый +пиздоватый +пиздовать +пиздоверзилище +пиздовладелец +пиздоворот +пиздовыдло +пиздовый +пиздоглазая +пиздоглист +пиздогрыз +пиздогундливый +пиздодержец +пиздодраченая +пиздодрачильник +пиздодрачливище +пиздодушие +пиздодырищи +пиздодырная +пиздодырявина +пиздоебательный +пиздоёбищность +пиздоебливая +пиздоеблю +пиздожал +пиздожоп +пиздожопская +пиздозолотарь +пиздой +пиздокач +пиздоквашни +пиздокопатель +пиздолет +пиздолетная +пиздолиз +пиздоломина +пиздомаздливый +пиздомания +пиздомахалово +пиздомол +пиздомотина +пиздоногая +пиздоноздря +пиздообрез +пиздопертая +пиздопляска +пиздопроeбище +пиздопроговенная +пиздопроебина +пиздопроебинами +пиздопрозоид +пиздопролазный +пиздопроситель +пиздопроушина +пиздорванец +пиздорванка +пиздорвань +пиздосей +пиздосербало +пиздоскал +пиздосов +пиздосос +пиздострадалец +пиздострадания +пиздострадатель +пиздостремливое +пиздотень +пиздотёрый +пиздотолковища +пиздотрескучий +пиздотыренный +пиздохайловый +пиздоход +пиздохочь +пиздохуй +пиздочет +пиздошевка +пиздошить +пиздоямина +пиздрик +пизду +пиздуй +пиздулия +пиздун +пиздуянистый +пизды +пиздырь +пиздьей +пиздью +пиздюга +пиздюк +пиздюкать +пиздюкаться +пиздюлей +пиздюли +пиздюлина +пиздюлька +пиздюля +пиздюрить +пиздюхать +пиздюшка +пиздюшки +пиздюшник +пиздя +пиздякать +пиздятина +пиздятиной +пиздятины +пиздячий +пиздячина +пиздячить +пиздячья +писька +писюлек +плоскозадая +поблудить +поблядовать +поблядушка +подговнять +подзаебать +подзаебенить +подзалупная +поднаебнуть +поднаебнуться +поднаёбывать +подосрать +подосраться +подоссать +подпёздывать +подпиздить +подпиздовать +подпиздоваться +подпиздовывать +подпиздовываться +подпиздок +подпиздохать +подпиздохаться +подпиздохивать +подпиздохиваться +подпиздошивать +подпиздошить +подпиздошиться +подпиздывает +подпиздывать +подпиздякать +подпиздякаться +подпиздякивать +подпиздякиваться +подпиздяривать +подпиздяриваться +подпиздярить +подпиздяриться +подпиздяхать +подпиздяхаться +подпиздяхивать +подпиздяхиваться +подпиздячивать +подпиздячиваться +подпиздячить +подпиздячиться +подпиздяшивать +подпиздяшиваться +подпиздяшить +подпиздяшиться +подристывать +подрочить +подсирать +подхуякать +подхуякаться +подхуякивать +подхуякиваться +подхуякнуть +подхуякнуться +подхуяривать +подхуяриваться +подхуярить +подхуяриться +подхуячивать +подхуячиваться +подхуячиться +подхуяшивать +подхуяшиваться +подхуяшить +подхуяшиться +подъеб +подъебайка +подъебала +подъебалка +подъебать +подъебаться +подъебашить +подъебка +подъёбка +подъёбки +подъебнуть +подъебнуться +подъебывать +подъёбывать +подъябывать +поебанный +поебать +поебаться +поебень +поебистика +поебон +поебончик +поебочка +поебывать +поебываться +попердеть +попердеться +попердывать +попизденная +попиздеть +попиздили +попиздистее +попиздить +попиздиться +попиздоболивать +попиздоболиваться +попиздоболить +попиздоболиться +попиздоватей +попиздовать +попиздоваться +попиздовывать +попиздовываться +попиздохать +попиздохаться +попиздохивать +попиздохиваться +попиздошивать +попиздошиваться +попиздошить +попиздошиться +попиздюкать +попиздюкаться +попиздюкивать +попиздюкиваться +попиздюливать +попиздюливаться +попиздюлить +попиздюлиться +попиздюривать +попиздюриваться +попиздюрить +попиздюриться +попиздюхать +попиздюхаться +попиздюхивать +попиздюхиваться +попиздякать +попиздякаться +попиздякивать +попиздякиваться +попиздяривать +попиздяриваться +попиздярить +попиздяриться +попиздяхать +попиздяхаться +попиздяхивать +попиздяхиваться +попиздячивать +попиздячиваться +попиздячить +попиздячиться +попиздяшивать +попиздяшиваться +попиздяшить +попиздяшиться +попизживать +попизживаться +посрать +потаскун +потаскуха +потраханная +потрахать +потрахаться +потрахивать +потрахиваться +похер +похуист +похуй +похую +похуякать +похуякаться +похуякивать +похуякиваться +похуяривать +похуяриваться +похуярили +похуярить +похуяриться +похуячивать +похуячиваться +похуячить +похуячиться +похуяшивать +похуяшиваться +похуяшить +похуяшиться +поц +пошмариться +поябывать +приебать +приебаться +приебашивать +приебашиваться +приебашить +приебашиться +приебенивать +приебениваться +приебенить +приебениться +приебехать +приебехаться +приебехивать +приебехиваться +приебистый +приебуривать +приебуриваться +приебурить +приебуриться +приебывать +приебываться +прижопить +прижопывать +прикинуть +примавдовывать +примандехать +примандехаться +примандехивать +примандехиваться +примандить +примандиться +примандовать +примандоваться +примандовываться +примандохать +примандохаться +примандохивать +примандохиваться +примандошивать +примандошиваться +примандошить +примандошиться +примандюкать +примандюкаться +примандюкивать +примандюкиваться +примандюливать +примандюливаться +примандюлить +примандюлиться +примандюривать +примандюриваться +примандюрить +примандюриться +примандякать +примандякаться +примандякивать +примандякиваться +примандяривать +примандяриваться +примандярить +примандяриться +примандяхать +примандяхаться +примандяхивать +примандяхиваться +примандячивать +примандячиваться +примандячить +примандячиться +примандяшивать +примандяшиваться +примандяшить +примандяшиться +примудохать +примудохаться +примудохивать +примудохиваться +припизденный +припиздень +припиздеть +припиздить +припиздиться +припиздовать +припиздоваться +припиздовывать +припиздовываться +припиздохать +припиздохаться +припиздохивать +припиздохиваться +припиздошивать +припиздошиваться +припиздошить +припиздошиться +припиздронивать +припиздрониваться +припиздронить +припиздрониться +припиздывать +припиздываться +припиздюкать +припиздюкаться +припиздюкивать +припиздюкиваться +припиздюливать +припиздюливаться +припиздюлить +припиздюлиться +припиздюривать +припиздюриваться +припиздюрить +припиздюриться +припиздюхать +припиздюхаться +припиздюхивать +припиздюхиваться +припиздякать +припиздякаться +припиздякивать +припиздякиваться +припиздяривать +припиздяриваться +припиздярить +припиздяриться +припиздяхать +припиздяхаться +припиздяхивать +припиздяхиваться +припиздячивать +припиздячиваться +припиздячить +припиздячиться +припиздяшивать +припиздяшиваться +припиздяшить +припиздяшиться +припизживать +припизживаться +притрахаться +прихуевать +прихуеть +прихуякать +прихуякаться +прихуякивать +прихуякиваться +прихуяривать +прихуяриваться +прихуярить +прихуяриться +прихуячивать +прихуячиваться +прихуячить +прихуячиться +прихуяшивать +прихуяшиваться +прихуяшить +прихуяшиться +пробиздоблядская +пробиздом +проблядовать +проблядушка +проблядь +прогандонский +прогнидопрохуевающее +прогнидская +продрачивать +продрачиваться +продрочить +продрочиться +проёб +проёбанной +проебать +проебаться +проебашивать +проебашиваться +проебашить +проебашиться +проебенить +проебениться +проебом +проебывать +проебываться +промандеть +промандище +промандоговенная +промонодблядская +промудак +промудеть +промудище +промудоватая +промудопердун +промудохуеблядская +промудохуина +проперданутое +пропердок +пропердомандища +пропиздеть +пропиздить +пропиздиться +пропиздоболивать +пропиздоболиваться +пропиздоболить +пропиздоболиться +пропиздовать +пропиздоваться +пропиздовывать +пропиздовываться +пропиздон +пропиздохать +пропиздохаться +пропиздохивать +пропиздохиваться +пропиздошивать +пропиздошиваться +пропиздошить +пропиздошиться +пропиздюкать +пропиздюкаться +пропиздюкивать +пропиздюкиваться +пропиздюливать +пропиздюливаться +пропиздюлить +пропиздюлиться +пропиздюривать +пропиздюриваться +пропиздюрить +пропиздюриться +пропиздюхать +пропиздюхаться +пропиздюхивать +пропиздюхиваться +пропиздякать +пропиздякаться +пропиздякивать +пропиздякиваться +пропиздяривать +пропиздяриваться +пропиздярить +пропиздяриться +пропиздяхать +пропиздяхивать +пропиздяхиваться +пропиздячивать +пропиздячиваться +пропиздячить +пропиздячиться +пропиздяшивать +пропиздяшиваться +пропиздяшить +пропиздяшиться +пропизживать +пропизживаться +просволота +просволотопрохуевое +просволотопрошлюхский +просволотохуеватое +просволотская +проскотошлюха +просраноперданутая +просраносука +проссаное +проссаномудище +простервоблядовое +простервопрохерун +прохуякать +прохуякаться +прохуякивать +прохуякиваться +прохуяривать +прохуяриваться +прохуярить +прохуяриться +прохуячивать +прохуячиваться +прохуячить +прохуячиться +прохуяшивать +прохуяшиваться +прохуяшить +прохуяшиться +прошмандовочный +пятихуй +разблядоваться +раздрочить +раздрочиться +разёбанный +раззалупаться +разнохуйственно +разъеба +разъёба +разъебай +разъёбанное +разъёбанный +разъебать +разъебаться +разъебашивать +разъебашиваться +разъебашить +разъебашиться +разъебенивать +разъебениваться +разъебенить +разъебениться +разъебись +распиздаеб +распиздаёб +распиздай +распиздеться +распизди +распиздить +распиздиться +распиздоблятство +распиздовать +распиздоваться +распиздовывать +распиздовываться +распиздон +распиздохать +распиздохаться +распиздохивать +распиздохиваться +распиздошивать +распиздошиваться +распиздошил +распиздошила +распиздошить +распиздошиться +распиздюляченный +распиздяй +распиздяйка +распиздяйство +распроеб +расхуюжить +расхуяривать +расхуяриваться +расхуярить +расхуяриться +расхуячивать +расхуячиваться +расхуячить +расхуячиться +свиноёб +свиноёбиться +сволотопродерьмо +сволотосука +сговнять +сдрочить +семиблядским +сестроеб +сетсроёб +сифилитик +сифилюга +склипездень +скотложец +скотоёб +скотоёбина +скотопродерьмища +скурвиться +смандить +смандиться +сосихуйская +сосихуйский +сперматоблятская +сперматозавр +спермоблевотина +спиздеть +спиздил +спиздила +спиздить +сраногнойная +сранье +ссанопиздище +стерва +стервоза +стервопиздище +стервопроблядское +стоебучее +страпиздихуюлина +страхоёбище +страхопиздище +страхопизднутый +сука +суки +сукин +сукины +суходрочка +суходрочкой +сухопиздая +сучара +сучий +сучка +сучье +сучьемудища +сучьескотина +схуёженный +схуякать +схуякаться +схуякивать +схуякиваться +схуяли +схуяривать +схуяриваться +схуярить +схуяриться +схуячивать +схуячить +схуячиться +сцать +сциха +сцыха +съебать +съебаться +съебашивать +съебашиваться +съебашить +съебашиться +съебенивать +съебенить +съебениться +съебывать +съебываться +тварь +толстожопый +толстозадая +торчило +траханье +трахать +трахаться +трахнуть +трахнуться +трепак +трепездон +трепездонит +тригнидопроговно +тригнидопрохерище +тригнидская +триговноперданутое +триеблоостомондовевшая +триебучим +тризлоебучим +тримандаблядская +тримандоблядская +тримандопростервозный +тримондоеби +тримудище +трипездоклятый +трипердоватая +трипиздие +трипиздоблядская +трипиздоблядское +трипиздоблятский +трипиздодрочун +трипиздопроманда +трипиздопроскотложец +трипиздохерище +трипиздыпроебина +триппер +трипперных +трисраное +триссака +триссанохуина +триссаный +трисука +трисучьепадловая +трихломидозопиздоеблохуеблядеперепиздическая +трихуемандаблядский +троебучим +троепиздием +троепиздище +туебень +тупиздень +убить +ублюдоёб +ублюдок +уебалась +уебался +уебальник +уебанский +уебать +уебаться +уебашивать +уебашить +уебенить +уебище +уёбище +уёбищенски +уёбок +уёбывает +уебывать +уёбывать +уебываться +уебыш +упиздить +урод +усраться +усрачка +уссать +уссаться +ухуякать +ухуякаться +ухуякивать +ухуякиваться +ухуяривать +ухуяриваться +ухуярить +ухуяриться +ухуячивать +ухуячиваться +ухуячить +ухуячиться +ухуяшивать +ухуяшиваться +ухуяшить +ухуяшиться +фаллос +фекал +фекалии +фекалий +фуй +хер +херами +херня +херов +херовина +херовый +хероговнюк +херопроскотское +херун +хитровыебанная +хитровыебанный +хитрожопый +хлюха +хрен +хреново +хреновое +хреновый +худоебина +худоёбина +хуе +хуебарный +хуебень +хуеблища +хуеблядипиздожабья +хуеблядская +хуеблядский +хуебратия +хуебун +хуебур +хуебыдловый +хуев +хуёв +хуева +хуеватенький +хуевато +хуеватый +хуеверзоха +хуевина +хуёвина +хуёвищный +хуёвничать +хуево +хуёво +хуеворот +хуевотараканево +хуевый +хуёвый +хуег +хуеглот +хуеговно +хуеголовое +хуеголовый +хуегрыз +хуедав +хуедин +хуедрыга +хуезагнутие +хуездоватый +хуек +хуелептический +хуелес +хуеловица +хуем +хуём +хуеман +хуемандовина +хуемудрие +хуемырло +хуемыслие +хуеньки +хуеоглобель +хуепедераст +хуепиздрическое +хуеплет +хуеплёт +хуепромышленник +хуепропадла +хуепуполо +хуепутало +хуерик +хуероломом +хуерык +хуерыльная +хуесос +хуесосина +хуессаная +хуесучий +хуета +хуетень +хуетертое +хуеть +хуец +хуечек +хуи +хуидло +хуила +хуилище +хуило +хуиный +хуистая +хуистый +хуишко +хуище +хуй +хуйло +хуйнуть +хуйню +хуйня +хуйрик +хуйство +хули +хую +хуюга +хуюживать +хуюживаться +хуюжить +хуюжиться +хуюла +хуюльское +хуюшки +хуя +хуяк +хуяка +хуякать +хуякнуть +хуяли +хуяляга +хуям +хуями +хуярез +хуярить +хуяриться +хуясе +хуястый +хуях +хуяха +хуяция +хуячий +хуячить +хуячиться +хуячья +хуяшить +целка +целку +целочка +черножопые +чернозадый +член +шалава +шароёбится +шелудивая +шелупина +широкопиздая +шлюха +шлюхский +шлюхское +шмара +шмарить +шмариться +ялдак +анус +аборт +блудилище +вагина +влагалище +дурак +моча +пенис \ No newline at end of file diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/icons/first.png b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/first.png new file mode 100644 index 0000000..660babf Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/first.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/icons/last.png b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/last.png new file mode 100644 index 0000000..cd4f641 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/last.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/icons/menu-flip.svg b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/menu-flip.svg new file mode 100644 index 0000000..843d710 --- /dev/null +++ b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/menu-flip.svg @@ -0,0 +1,3 @@ + + + diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/icons/menu-history.svg b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/menu-history.svg new file mode 100644 index 0000000..87d94b2 --- /dev/null +++ b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/menu-history.svg @@ -0,0 +1,3 @@ + + + diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/icons/menu-share.svg b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/menu-share.svg new file mode 100644 index 0000000..50b34dd --- /dev/null +++ b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/menu-share.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/icons/next.png b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/next.png new file mode 100644 index 0000000..61eefd1 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/next.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/icons/pgn_moves.svg b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/pgn_moves.svg new file mode 100644 index 0000000..72ba596 --- /dev/null +++ b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/pgn_moves.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/icons/prev.png b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/prev.png new file mode 100644 index 0000000..0e4e744 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/prev.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/icons/result-crown.svg b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/result-crown.svg new file mode 100644 index 0000000..c4cc119 --- /dev/null +++ b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/result-crown.svg @@ -0,0 +1,4 @@ + + + + diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/icons/result-draw.svg b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/result-draw.svg new file mode 100644 index 0000000..25d67ed --- /dev/null +++ b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/result-draw.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/icons/result-flag.svg b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/result-flag.svg new file mode 100644 index 0000000..219b8e7 --- /dev/null +++ b/SunnexGB/Heroku-Modules/Assets/NoChess/icons/result-flag.svg @@ -0,0 +1,4 @@ + + + + diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/mr_granstandercleang.otf b/SunnexGB/Heroku-Modules/Assets/NoChess/mr_granstandercleang.otf new file mode 100644 index 0000000..554bd15 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/NoChess/mr_granstandercleang.otf differ diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/other/bg.png b/SunnexGB/Heroku-Modules/Assets/NoChess/other/bg.png new file mode 100644 index 0000000..e7b9a2b Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/NoChess/other/bg.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/other/noise.png b/SunnexGB/Heroku-Modules/Assets/NoChess/other/noise.png new file mode 100644 index 0000000..315084a Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/NoChess/other/noise.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-bishop.png b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-bishop.png new file mode 100644 index 0000000..d450ca0 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-bishop.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-king.png b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-king.png new file mode 100644 index 0000000..fa27585 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-king.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-knight.png b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-knight.png new file mode 100644 index 0000000..76bb3cc Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-knight.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-pawn.png b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-pawn.png new file mode 100644 index 0000000..a67597e Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-pawn.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-queen.png b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-queen.png new file mode 100644 index 0000000..9a23ce1 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-queen.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-rook.png b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-rook.png new file mode 100644 index 0000000..53c1a0a Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-rook.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-bishop.png b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-bishop.png new file mode 100644 index 0000000..d65e581 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-bishop.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-king.png b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-king.png new file mode 100644 index 0000000..8e93abc Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-king.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-knight.png b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-knight.png new file mode 100644 index 0000000..4d807d1 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-knight.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-pawn.png b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-pawn.png new file mode 100644 index 0000000..ad84cc0 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-pawn.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-queen.png b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-queen.png new file mode 100644 index 0000000..d41bd92 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-queen.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-rook.png b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-rook.png new file mode 100644 index 0000000..bfa7343 Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-rook.png differ diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/raw_assets/index.html b/SunnexGB/Heroku-Modules/Assets/NoChess/raw_assets/index.html new file mode 100644 index 0000000..f9d7af4 --- /dev/null +++ b/SunnexGB/Heroku-Modules/Assets/NoChess/raw_assets/index.html @@ -0,0 +1,113 @@ + + + + + + + NoChess + + +
+
+
+
+
+
+
+
+
+ first +
+
+ prev +
+
+ next +
+
+ last +
+
+
+
+ +
+
+
+
+
guest-acc
+
+
+
+ + + + + + + + + + + + + +
+
--:--
+
+ + + + +
+
--:--
+
+ + + + + + + + + + + + + +
+
+
+
Sunnex <3
+
+
+
+ + + + + + diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/raw_assets/javascript.js b/SunnexGB/Heroku-Modules/Assets/NoChess/raw_assets/javascript.js new file mode 100644 index 0000000..e978f20 --- /dev/null +++ b/SunnexGB/Heroku-Modules/Assets/NoChess/raw_assets/javascript.js @@ -0,0 +1,1675 @@ +const board_size = 8; +const block_size = 107; +const theme_config = window.nochess_theme || {}; +const block_light = theme_config.block_light || '#D8E3E7'; +const block_dark = theme_config.block_dark || '#7699AF'; +const select_block = theme_config.select_block || '#FFDF5A'; +const block_select_alpha_light = 0.34; +const block_select_alpha_dark = 0.58; +const move_pieces_color = theme_config.move_pieces_color || '#58B4FF'; +const block_move_from_alpha = 0.56; +const block_move_to_alpha = 0.62; +const move_highlight_delay_ms = 25; +const result_overlay_duration_ms = 300; +const result_win = theme_config.result_win || '#00BE16'; +const result_lose = theme_config.result_lose || '#BE0000'; +const result_draw = theme_config.result_draw || '#434343'; +const result_overlay_alpha = 0.64; +const arrow_color = theme_config.arrow_color || '#BD3667'; +const asset_root = window.nochess_asset_root || 'https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess'; +const noise_src = `${asset_root}/other/noise.png`; +const start_fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; + +const chess_pieces = { + wP: `${asset_root}/pieces/white-pawn.png`, + wR: `${asset_root}/pieces/white-rook.png`, + wN: `${asset_root}/pieces/white-knight.png`, + wB: `${asset_root}/pieces/white-bishop.png`, + wQ: `${asset_root}/pieces/white-queen.png`, + wK: `${asset_root}/pieces/white-king.png`, + bP: `${asset_root}/pieces/black-pawn.png`, + bR: `${asset_root}/pieces/black-rook.png`, + bN: `${asset_root}/pieces/black-knight.png`, + bB: `${asset_root}/pieces/black-bishop.png`, + bQ: `${asset_root}/pieces/black-queen.png`, + bK: `${asset_root}/pieces/black-king.png` +}; + +const sound_urls = { + game_start: 'https://images.chesscomfiles.com/chess-themes/sounds/_MP3_/default/game-start.mp3', + game_end: 'https://images.chesscomfiles.com/chess-themes/sounds/_MP3_/default/game-end.mp3', + capture: 'https://images.chesscomfiles.com/chess-themes/sounds/_MP3_/default/capture.mp3', + castle: 'https://images.chesscomfiles.com/chess-themes/sounds/_MP3_/default/castle.mp3', + premove: 'https://images.chesscomfiles.com/chess-themes/sounds/_MP3_/default/premove.mp3', + move_self: 'https://images.chesscomfiles.com/chess-themes/sounds/_MP3_/default/move-self.mp3', + move_opponent: 'https://images.chesscomfiles.com/chess-themes/sounds/_MP3_/default/move-opponent.mp3', + move_check: 'https://images.chesscomfiles.com/chess-themes/sounds/_MP3_/default/move-check.mp3', + promote: 'https://images.chesscomfiles.com/chess-themes/sounds/_MP3_/default/promote.mp3' +}; + +const ui_text = { + no_history: 'No history', + unknown_date: 'Unknown date', + chess_library_not_ready: 'Chess library not ready', + pgn_empty: 'Paste PGN text first', + pgn_invalid: 'Invalid PGN format', + pgn_loaded: 'PGN loaded', + pgn_copied: 'PGN copied', + copy_failed: 'Copy failed' +}; + +const crown_icon_src = `${asset_root}/icons/result-crown.svg`; +const flag_icon_src = `${asset_root}/icons/result-flag.svg`; +const draw_icon_src = `${asset_root}/icons/result-draw.svg`; + +const canvas = document.getElementById('chessBoard'); +const ctx = canvas.getContext('2d'); +canvas.width = board_size * block_size; +canvas.height = board_size * block_size; + +const moves_scroll = document.querySelector('.moves_list_mobile'); +const pgn_moves_board = document.querySelector('.pgn_moves_board'); +const timer_black = document.querySelector('.timer_black'); +const timer_white = document.querySelector('.timer_white'); +const player_name_white = document.querySelector('.player_name_white'); +const player_name_black = document.querySelector('.player_name_black'); +const avatar_white = document.querySelector('.avatar_white'); +const avatar_black = document.querySelector('.avatar_black'); +const history_btn = document.getElementById('history_btn'); +const share_btn = document.getElementById('share_btn'); +const more_btn = document.getElementById('more_btn'); +const app_layout = document.querySelector('.web_chess'); +const first_move_icon = document.querySelector('.first_move_btn img'); +const submenu_overlay = document.getElementById('submenu_overlay'); +const history_panel = document.getElementById('history_panel'); +const share_panel = document.getElementById('share_panel'); +const history_games = document.getElementById('history_games'); +const share_pgn_text = document.getElementById('share_pgn_text'); +const load_pgn_btn = document.getElementById('load_pgn_btn'); +const copy_pgn_btn = document.getElementById('copy_pgn_btn'); +const share_status = document.getElementById('share_status'); + +let board = []; +let marked_cells = new Set(); +let arrows = []; +let piece_images = {}; +let noise_image = null; +let right_drag_start = null; +let is_flipped = false; +let parsed_games = []; +let current_game_index = 0; +let current_ply = 0; +let share_status_timeout = null; +let chess_ready = false; +let highlighted_move_cells = null; +let piece_animation = null; +let highlight_delay_timeout = null; +let result_markers = null; +let result_animation = null; +let nochess_profile = window.nochess_profile || null; +const mobile_breakpoint = window.matchMedia('(max-width: 572px)'); +let was_mobile_layout = false; +const crown_icon_image = new Image(); +const flag_icon_image = new Image(); +const draw_icon_image = new Image(); +crown_icon_image.src = crown_icon_src; +flag_icon_image.src = flag_icon_src; +draw_icon_image.src = draw_icon_src; + +function hexToRgba(hex, alpha) { + const value = hex.replace('#', ''); + const full = value.length === 3 + ? value.split('').map((c) => c + c).join('') + : value; + const int = Number.parseInt(full, 16); + const r = (int >> 16) & 255; + const g = (int >> 8) & 255; + const b = int & 255; + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} + +function loadScript(src) { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = src; + script.async = true; + script.onload = () => resolve(true); + script.onerror = () => reject(new Error(`failed: ${src}`)); + document.head.appendChild(script); + }); +} + +async function loadChessLibrary() { + if (typeof window.Chess !== 'undefined') { + return true; + } + + const classic_sources = [ + 'https://cdn.jsdelivr.net/npm/chess.js@0.13.4/chess.min.js', + 'https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.13.4/chess.min.js' + ]; + + for (const src of classic_sources) { + try { + await loadScript(src); + if (typeof window.Chess !== 'undefined') { + return true; + } + } catch (error) { + } + } + + try { + const module = await import('https://cdn.jsdelivr.net/npm/chess.js@1.4.0/dist/esm/chess.js'); + window.Chess = module.Chess || module.default; + return typeof window.Chess !== 'undefined'; + } catch (error) { + return false; + } +} + +function createChess() { + if (typeof Chess === 'undefined') { + return null; + } + return new Chess(); +} + +function loadPgnToChess(chess, pgn_text) { + if (!chess) { + return false; + } + + if (typeof chess.load_pgn === 'function') { + let loaded = chess.load_pgn(pgn_text, { newline_char: '\n', sloppy: true }); + if (!loaded) { + loaded = chess.load_pgn(pgn_text, { sloppy: true }); + } + if (!loaded) { + loaded = chess.load_pgn(pgn_text); + } + return loaded; + } + + if (typeof chess.loadPgn === 'function') { + try { + chess.loadPgn(pgn_text, { strict: false }); + return true; + } catch (error) { + try { + chess.loadPgn(pgn_text); + return true; + } catch (second_error) { + return false; + } + } + } + + return false; +} + +function moveOnChess(chess, move_text) { + if (!chess || !move_text) { + return null; + } + + try { + const sloppy_move = chess.move(move_text, { sloppy: true }); + if (sloppy_move) { + return sloppy_move; + } + } catch (error) { + } + + try { + return chess.move(move_text); + } catch (error) { + return null; + } +} + +const sound_players = Object.fromEntries(Object.entries(sound_urls).map(([name, url]) => { + const base = new Audio(url); + base.preload = 'auto'; + return [name, () => { + const sound = base.cloneNode(true); + sound.volume = 0.9; + sound.play().catch(() => {}); + }]; +})); + +function updateScrollShadow() { + const at_bottom = moves_scroll.scrollTop + moves_scroll.clientHeight >= moves_scroll.scrollHeight - 5; + const has_overflow = moves_scroll.scrollHeight > moves_scroll.clientHeight; + pgn_moves_board.classList.toggle('has_scroll', has_overflow && !at_bottom); +} + +function parseHeaders(pgn_text) { + const headers = {}; + const header_regex = /^\[(\w+)\s+"([^"]*)"\]$/gm; + let match = header_regex.exec(pgn_text); + while (match) { + headers[match[1]] = match[2]; + match = header_regex.exec(pgn_text); + } + return headers; +} + +function normalizeClock(clock) { + if (!clock) { + return null; + } + const parts = clock.split(':'); + if (parts.length === 3) { + const hours = Number(parts[0]); + const minutes = Number(parts[1]); + const seconds = Number(parts[2]); + if (!Number.isNaN(hours) && !Number.isNaN(minutes) && !Number.isNaN(seconds)) { + if (hours === 0) { + return `${minutes}:${String(seconds).padStart(2, '0')}`; + } + return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; + } + } + if (parts.length === 2) { + const minutes = Number(parts[0]); + const seconds = Number(parts[1]); + if (!Number.isNaN(minutes) && !Number.isNaN(seconds)) { + return `${minutes}:${String(seconds).padStart(2, '0')}`; + } + } + return clock; +} + +function isMoveToken(token) { + return /^(O-O(-O)?|[KQRBN]?[a-h]?[1-8]?x?[a-h][1-8](=[QRBN])?[+#]?|[a-h]x[a-h][1-8](=[QRBN])?[+#]?|[a-h][1-8](=[QRBN])?[+#]?)$/.test(token); +} + +function extractMoveClocks(pgn_text) { + const cleaned = pgn_text.replace(/^\[[^\]]*\]\s*$/gm, ''); + const tokens = cleaned.match(/\{[^}]*\}|[^\s]+/g) || []; + const clocks_by_ply = {}; + let ply = 0; + let last_move_ply = 0; + + for (const token of tokens) { + if (!token) { + continue; + } + + if (token.startsWith('{')) { + const clk_match = token.match(/\[%clk\s+([^\]]+)\]/i); + if (clk_match && last_move_ply > 0) { + clocks_by_ply[last_move_ply] = normalizeClock(clk_match[1].trim()); + } + continue; + } + + if (/^\d+\.{1,3}$/.test(token)) { + continue; + } + + if (token === '1-0' || token === '0-1' || token === '1/2-1/2' || token === '*') { + continue; + } + + if (isMoveToken(token)) { + ply += 1; + last_move_ply = ply; + } + } + + return clocks_by_ply; +} + +function fenCharToPiece(char) { + const is_white = char === char.toUpperCase(); + const color = is_white ? 'w' : 'b'; + const type = char.toUpperCase(); + if (!'PRNBQK'.includes(type)) { + return null; + } + return `${color}${type}`; +} + +function boardFromFen(fen) { + const rows = fen.split(' ')[0].split('/'); + return rows.map((row) => { + const parsed = []; + for (const char of row) { + const count = Number(char); + if (!Number.isNaN(count) && count > 0) { + for (let i = 0; i < count; i += 1) { + parsed.push(null); + } + } else { + parsed.push(fenCharToPiece(char)); + } + } + return parsed; + }); +} + +function squareToCoords(square) { + if (!square || square.length < 2) { + return null; + } + const file = square.charCodeAt(0) - 97; + const rank = Number(square[1]); + if (file < 0 || file > 7 || Number.isNaN(rank) || rank < 1 || rank > 8) { + return null; + } + return { row: 8 - rank, col: file }; +} + +function setHighlightedMove(from_square, to_square) { + const from = squareToCoords(from_square); + const to = squareToCoords(to_square); + highlighted_move_cells = from && to ? { from, to } : null; +} + +function scheduleHighlightedMove(from_square, to_square) { + if (highlight_delay_timeout) { + clearTimeout(highlight_delay_timeout); + highlight_delay_timeout = null; + } + + highlighted_move_cells = null; + + if (!from_square || !to_square) { + return; + } + + highlight_delay_timeout = setTimeout(() => { + setHighlightedMove(from_square, to_square); + highlight_delay_timeout = null; + render(); + }, move_highlight_delay_ms); +} + +window.highlightMoveSquares = function highlightMoveSquares(from_square, to_square) { + setHighlightedMove(from_square, to_square); + render(); +}; + +function createAnimationState(from, to, piece_code) { + return { + from, + to, + piece_code, + start_time: performance.now(), + duration: 180 + }; +} + +function animateMoveTransition(animation_state) { + piece_animation = animation_state; + + function step() { + if (!piece_animation) { + return; + } + + const elapsed = performance.now() - piece_animation.start_time; + if (elapsed >= piece_animation.duration) { + piece_animation = null; + render(); + return; + } + + render(); + requestAnimationFrame(step); + } + + requestAnimationFrame(step); +} + +function parsePgnToGameState(pgn_text) { + if (!chess_ready || typeof Chess === 'undefined') { + return null; + } + + const parser = createChess(); + if (!parser) { + return null; + } + + const loaded = loadPgnToChess(parser, pgn_text); + + if (!loaded) { + const fallback = parseMovesWithSloppyParser(pgn_text); + if (!fallback) { + return null; + } + return fallback; + } + + const headers = parseHeaders(pgn_text); + const verbose_moves = parser.history({ verbose: true }); + const clocks_by_ply = extractMoveClocks(pgn_text); + const simulator = createChess(); + if (!simulator) { + return null; + } + const positions = [boardFromFen(simulator.fen())]; + + for (const move of verbose_moves) { + moveOnChess(simulator, move.san); + positions.push(boardFromFen(simulator.fen())); + } + + const moves = verbose_moves.map((move, index) => ({ + ...move, + clock: clocks_by_ply[index + 1] || null, + is_capture: move.flags.includes('c') || move.flags.includes('e'), + is_castle: move.flags.includes('k') || move.flags.includes('q'), + is_promotion: move.flags.includes('p'), + is_check: move.san.includes('+'), + is_mate: move.san.includes('#') + })); + + return { + pgn_text, + headers, + moves, + positions + }; +} + +function normalizeMoveToken(token) { + if (!token) { + return ''; + } + + let value = token.trim(); + value = value.replace(/^\d+\.\.\./, ''); + value = value.replace(/^\d+\./, ''); + value = value.replace(/[!?]+$/g, ''); + value = value.replace(/^0-0-0$/, 'O-O-O'); + value = value.replace(/^0-0$/, 'O-O'); + value = value.replace(/=([qrbn])/g, (_, p1) => `=${p1.toUpperCase()}`); + return value; +} + +function extractSanTokens(movetext) { + const regex = /(?:O-O-O|O-O|0-0-0|0-0|[KQRBN]?[a-h]?[1-8]?x?[a-h][1-8](?:=[QRBNqrbn])?|[a-h]x[a-h][1-8](?:=[QRBNqrbn])?|[a-h][1-8](?:=[QRBNqrbn])?)(?:[+#])?|1-0|0-1|1\/2-1\/2|\*/g; + return movetext.match(regex) || []; +} + +function tryApplySanMove(game, token) { + const first_try = moveOnChess(game, token); + if (first_try) { + return first_try; + } + + const without_suffix = token.replace(/[+#]$/, ''); + if (without_suffix !== token) { + const second_try = moveOnChess(game, without_suffix); + if (second_try) { + return second_try; + } + } + + return null; +} + +function parseMovesWithSloppyParser(pgn_text) { + if (!chess_ready || typeof Chess === 'undefined') { + return null; + } + + const headers = parseHeaders(pgn_text); + const clocks_by_ply = extractMoveClocks(pgn_text); + const move_section = pgn_text + .replace(/^\[[^\]]*\]\s*$/gm, '') + .replace(/\{[^}]*\}/g, ' ') + .replace(/\([^)]*\)/g, ' ') + .replace(/\$\d+/g, ' ') + .replace(/\r\n/g, '\n') + .trim(); + + const start_index = move_section.search(/\b\d+\./); + const mandatory_movetext = start_index >= 0 ? move_section.slice(start_index) : move_section; + const normalized_movetext = mandatory_movetext + .replace(/\s+/g, ' ') + .trim(); + + const raw_tokens = extractSanTokens(normalized_movetext); + const game = createChess(); + if (!game) { + return null; + } + const moves = []; + const positions = [boardFromFen(game.fen())]; + let ply = 0; + + for (const raw of raw_tokens) { + if (!raw || raw === '1-0' || raw === '0-1' || raw === '1/2-1/2' || raw === '*') { + continue; + } + + const split_tokens = raw.includes('.') ? raw.split('.').filter(Boolean) : [raw]; + for (const token of split_tokens) { + const clean_token = normalizeMoveToken(token); + if (!clean_token || /^\d+$/.test(clean_token)) { + continue; + } + + if (clean_token === '...' || /^\.+$/.test(clean_token)) { + continue; + } + + if (clean_token === '1-0' || clean_token === '0-1' || clean_token === '1/2-1/2' || clean_token === '*') { + continue; + } + + const move = tryApplySanMove(game, clean_token); + if (!move) { + return null; + } + + ply += 1; + moves.push({ + ...move, + clock: clocks_by_ply[ply] || null, + is_capture: move.flags.includes('c') || move.flags.includes('e'), + is_castle: move.flags.includes('k') || move.flags.includes('q'), + is_promotion: move.flags.includes('p'), + is_check: move.san.includes('+'), + is_mate: move.san.includes('#') + }); + positions.push(boardFromFen(game.fen())); + } + } + + if (moves.length === 0) { + return null; + } + + return { + pgn_text, + headers, + moves, + positions + }; +} + +function sanitizePgnInput(raw_text) { + if (!raw_text) { + return ''; + } + + let text = raw_text.replace(/^\uFEFF/, '').trim(); + const fenced = text.match(/^```(?:pgn)?\s*([\s\S]*?)\s*```$/i); + if (fenced) { + text = fenced[1].trim(); + } + + return text + .replace(/\u00A0/g, ' ') + .replace(/\r\n/g, '\n') + .replace(/\t/g, ' ') + .replace(/\n{3,}/g, '\n\n'); +} + +function autoResizeShareTextarea() { + share_pgn_text.style.height = 'auto'; + const next_height = Math.min(share_pgn_text.scrollHeight, Math.floor(window.innerHeight * 0.55)); + share_pgn_text.style.height = `${Math.max(next_height, 170)}px`; +} + +function setShareStatus(message, is_error) { + if (share_status_timeout) { + clearTimeout(share_status_timeout); + share_status_timeout = null; + } + + share_status.textContent = message || ''; + share_status.classList.remove('show', 'error'); + if (!message) { + return; + } + if (is_error) { + share_status.classList.add('error'); + } + requestAnimationFrame(() => { + share_status.classList.add('show'); + }); + + share_status_timeout = setTimeout(() => { + share_status.classList.remove('show', 'error'); + share_status.textContent = ''; + share_status_timeout = null; + }, 2200); +} + +function clearResultVisuals() { + result_markers = null; + result_animation = null; +} + +function isSameResultMarkers(first, second) { + if (!first && !second) { + return true; + } + if (!first || !second) { + return false; + } + return first.winner.row === second.winner.row + && first.winner.col === second.winner.col + && first.loser.row === second.loser.row + && first.loser.col === second.loser.col; +} + +function getResultOverlayProgress() { + if (!result_animation) { + return 1; + } + return Math.min(1, (performance.now() - result_animation.start_time) / result_animation.duration); +} + +function getResultIconScale(progress) { + const start = 0.52; + const overshoot = 1.08; + const ease_out_back = 1 + (2.1 * ((progress - 1) ** 3)) + (1.1 * ((progress - 1) ** 2)); + + if (progress < 0.78) { + return start + ((overshoot - start) * ease_out_back); + } + + const settle_progress = (progress - 0.78) / 0.22; + return overshoot + ((1 - overshoot) * settle_progress); +} + +function animateResultOverlay() { + function step() { + if (!result_animation) { + return; + } + + const progress = getResultOverlayProgress(); + render(); + + if (progress >= 1) { + result_animation = null; + return; + } + + requestAnimationFrame(step); + } + + requestAnimationFrame(step); +} + +function getWinnerFromResult(result) { + if (result === '1-0') { + return 'w'; + } + if (result === '0-1') { + return 'b'; + } + if (result === '1/2-1/2') { + return 'draw'; + } + return null; +} + +function updateResultVisuals() { + const previous_markers = result_markers; + clearResultVisuals(); + + const game = getCurrentGame(); + if (!game || current_ply !== game.moves.length) { + return; + } + + const winner = getWinnerFromResult(game.headers.Result || ''); + if (!winner) { + return; + } + + let winner_king = null; + let loser_king = null; + for (let row = 0; row < board_size; row += 1) { + for (let col = 0; col < board_size; col += 1) { + const piece = board[row][col]; + if (winner === 'draw') { + if (piece === 'wK') { + winner_king = { row, col }; + } else if (piece === 'bK') { + loser_king = { row, col }; + } + } else { + if (piece === (winner === 'w' ? 'wK' : 'bK')) { + winner_king = { row, col }; + } else if (piece === (winner === 'w' ? 'bK' : 'wK')) { + loser_king = { row, col }; + } + } + } + } + + if (winner_king && loser_king) { + const next_markers = { + type: winner === 'draw' ? 'draw' : 'winlose', + winner: winner_king, + loser: loser_king + }; + + result_markers = next_markers; + if (!isSameResultMarkers(previous_markers, next_markers)) { + result_animation = { + start_time: performance.now(), + duration: result_overlay_duration_ms + }; + animateResultOverlay(); + } + } +} + +async function copyTextToClipboard(text) { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text); + return; + } + + const helper = document.createElement('textarea'); + helper.value = text; + helper.setAttribute('readonly', ''); + helper.style.position = 'fixed'; + helper.style.top = '-1000px'; + helper.style.left = '-1000px'; + document.body.appendChild(helper); + helper.focus(); + helper.select(); + const success = document.execCommand('copy'); + document.body.removeChild(helper); + if (!success) { + throw new Error('copy-failed'); + } +} + +function getCurrentGame() { + return parsed_games[current_game_index] || null; +} + +function normalizePlayerName(value) { + return (value || '').toString().trim().toLowerCase(); +} + +function playerNameMatches(side_name, variants) { + const side = normalizePlayerName(side_name); + if (!side) { + return false; + } + + return variants.some((name) => { + const candidate = normalizePlayerName(name); + if (!candidate) { + return false; + } + return side === candidate || side.includes(candidate) || candidate.includes(side); + }); +} + +function detectMyColor(game) { + if (!game || !nochess_profile || !game.headers) { + return null; + } + + const names = [ + nochess_profile.name, + nochess_profile.username, + nochess_profile.first_name, + nochess_profile.last_name + ]; + + const white_match = playerNameMatches(game.headers.White, names); + const black_match = playerNameMatches(game.headers.Black, names); + + if (white_match && !black_match) { + return 'w'; + } + if (black_match && !white_match) { + return 'b'; + } + return null; +} + +function applyPlayerAvatars() { + if (!avatar_white || !avatar_black || !nochess_profile) { + return; + } + + const me_photo = nochess_profile.photo || ''; + const enemy_photo = nochess_profile.enemy_photo || ''; + if (!me_photo && !enemy_photo) { + return; + } + + const game = getCurrentGame(); + const my_color = detectMyColor(game) || 'w'; + const white_photo = my_color === 'b' ? enemy_photo : me_photo; + const black_photo = my_color === 'b' ? me_photo : enemy_photo; + + if (white_photo) { + avatar_white.style.backgroundImage = `url('${white_photo}')`; + } + if (black_photo) { + avatar_black.style.backgroundImage = `url('${black_photo}')`; + } +} + +function setNoChessProfile(profile) { + nochess_profile = profile || null; + applyPlayerAvatars(); +} + +window.setNoChessProfile = setNoChessProfile; + +function getTimerTextForColor(color) { + const game = getCurrentGame(); + if (!game) { + return '--:--'; + } + + let latest = null; + for (let i = 0; i < current_ply; i += 1) { + const move = game.moves[i]; + if (move.color === color && move.clock) { + latest = move.clock; + } + } + return latest || '--:--'; +} + +function updateTimers() { + timer_white.textContent = getTimerTextForColor('w'); + timer_black.textContent = getTimerTextForColor('b'); +} + +function updatePlayers() { + const game = getCurrentGame(); + if (!game) { + applyPlayerAvatars(); + return; + } + player_name_white.textContent = game.headers.White || 'White'; + player_name_black.textContent = game.headers.Black || 'Black'; + applyPlayerAvatars(); +} + +function renderMoves() { + const game = getCurrentGame(); + moves_scroll.innerHTML = ''; + + if (!game) { + updateScrollShadow(); + return; + } + + for (let i = 0; i < game.moves.length; i += 2) { + const move_number = Math.floor(i / 2) + 1; + const white = game.moves[i] || null; + const black = game.moves[i + 1] || null; + const row = document.createElement('div'); + row.className = 'move_row'; + + const number = document.createElement('div'); + number.className = 'move_number'; + number.textContent = `${move_number}.`; + + const white_cell = document.createElement('div'); + white_cell.className = 'move_white'; + white_cell.textContent = white ? white.san : ''; + if (white) { + white_cell.dataset.ply = String(i + 1); + } + + const black_cell = document.createElement('div'); + black_cell.className = 'move_black'; + black_cell.textContent = black ? black.san : ''; + if (black) { + black_cell.dataset.ply = String(i + 2); + } + + row.append(number, white_cell, black_cell); + moves_scroll.appendChild(row); + } + + moves_scroll.querySelectorAll('.move_white[data-ply], .move_black[data-ply]').forEach((cell) => { + cell.addEventListener('click', () => { + const next_ply = Number(cell.dataset.ply || '0'); + if (next_ply > 0) { + setPly(next_ply, true); + } + }); + }); + + updateCurrentMoveHighlight(); + updateScrollShadow(); +} + +function updateCurrentMoveHighlight() { + moves_scroll.querySelectorAll('.current_move').forEach((cell) => { + cell.classList.remove('current_move'); + }); + + if (current_ply <= 0) { + return; + } + + const active = moves_scroll.querySelector(`[data-ply="${current_ply}"]`); + if (!active) { + return; + } + + active.classList.add('current_move'); + active.scrollIntoView({ block: 'nearest' }); +} + +function playSound(name) { + const player = sound_players[name]; + if (player) { + player(); + } +} + +function playMoveSound(move) { + if (!move) { + return; + } + + if (move.is_mate) { + playSound('move_check'); + setTimeout(() => playSound('game_end'), 70); + return; + } + + if (move.is_promotion) { + playSound('promote'); + return; + } + + if (move.is_castle) { + playSound('castle'); + return; + } + + if (move.is_capture) { + playSound('capture'); + return; + } + + if (move.is_check) { + playSound('move_check'); + return; + } + + playSound(move.color === 'w' ? 'move_self' : 'move_opponent'); +} + +function setPly(next_ply, with_sound) { + const game = getCurrentGame(); + if (!game) { + return; + } + + const previous_ply = current_ply; + const previous_board = board.map((row) => [...row]); + const clamped = Math.max(0, Math.min(next_ply, game.moves.length)); + const delta = clamped - previous_ply; + const moved_forward_by_one = delta === 1; + current_ply = clamped; + + if (current_ply > 0) { + const move = game.moves[current_ply - 1]; + scheduleHighlightedMove(move.from, move.to); + } else { + scheduleHighlightedMove(null, null); + } + + board = game.positions[current_ply].map((row) => [...row]); + + if (Math.abs(delta) === 1) { + const move_index = delta > 0 ? current_ply - 1 : previous_ply - 1; + const move = game.moves[move_index]; + if (move) { + const from_square = delta > 0 ? move.from : move.to; + const to_square = delta > 0 ? move.to : move.from; + const from = squareToCoords(from_square); + const to = squareToCoords(to_square); + const piece_code = from ? previous_board[from.row][from.col] : null; + if (from && to && piece_code) { + animateMoveTransition(createAnimationState(from, to, piece_code)); + } else { + render(); + } + } else { + render(); + } + } else { + piece_animation = null; + render(); + } + + updateCurrentMoveHighlight(); + updateTimers(); + updateResultVisuals(); + + if (delta !== 0) { + marked_cells.clear(); + arrows = []; + } + + if (!with_sound || delta === 0) { + return; + } + + if (moved_forward_by_one && current_ply > 0) { + playMoveSound(game.moves[current_ply - 1]); + return; + } + + playSound('premove'); +} + +function loadGame(index) { + if (index < 0 || index >= parsed_games.length) { + return; + } + current_game_index = index; + current_ply = 0; + marked_cells.clear(); + arrows = []; + updatePlayers(); + renderMoves(); + setPly(0, false); + share_pgn_text.value = parsed_games[current_game_index].pgn_text; + playSound('game_start'); +} + +function buildHistoryList() { + history_games.innerHTML = ''; + + if (parsed_games.length === 0) { + history_games.classList.add('empty'); + const empty = document.createElement('div'); + empty.className = 'history_empty'; + empty.textContent = ui_text.no_history; + history_games.appendChild(empty); + return; + } + + history_games.classList.remove('empty'); + parsed_games.forEach((game, index) => { + const button = document.createElement('button'); + button.className = 'history_game_btn'; + button.type = 'button'; + const white = game.headers.White || 'White'; + const black = game.headers.Black || 'Black'; + const date = game.headers.Date || ui_text.unknown_date; + const result = game.headers.Result || '*'; + button.textContent = `${white} vs ${black} • ${result} • ${date}`; + button.addEventListener('click', () => { + closePanels(); + loadGame(index); + }); + history_games.appendChild(button); + }); +} + +function openPanel(panel) { + submenu_overlay.classList.add('open'); + history_panel.classList.remove('open'); + share_panel.classList.remove('open'); + panel.classList.add('open'); +} + +function closePanels() { + submenu_overlay.classList.remove('open'); + history_panel.classList.remove('open'); + share_panel.classList.remove('open'); +} + +function loadPieceImages(on_ready) { + let loaded = 0; + const total = Object.keys(chess_pieces).length; + + for (const [piece, src] of Object.entries(chess_pieces)) { + const img = new Image(); + img.onload = () => { + piece_images[piece] = img; + loaded += 1; + if (loaded === total) { + on_ready(); + } + }; + img.onerror = () => { + loaded += 1; + if (loaded === total) { + on_ready(); + } + }; + img.src = src; + } +} + +function loadNoise(on_ready) { + const img = new Image(); + img.onload = () => { + noise_image = img; + on_ready(); + }; + img.onerror = () => on_ready(); + img.src = noise_src; +} + +function toVisual(row, col) { + if (is_flipped) { + return { vrow: board_size - 1 - row, vcol: board_size - 1 - col }; + } + return { vrow: row, vcol: col }; +} + +function fromVisual(vrow, vcol) { + if (is_flipped) { + return { row: board_size - 1 - vrow, col: board_size - 1 - vcol }; + } + return { row: vrow, col: vcol }; +} + +function drawBoard() { + for (let vrow = 0; vrow < board_size; vrow += 1) { + for (let vcol = 0; vcol < board_size; vcol += 1) { + const { row, col } = fromVisual(vrow, vcol); + const is_light = (row + col) % 2 === 0; + const is_marked = marked_cells.has(`${row},${col}`); + const is_move_from = highlighted_move_cells + && highlighted_move_cells.from.row === row + && highlighted_move_cells.from.col === col; + const is_move_to = highlighted_move_cells + && highlighted_move_cells.to.row === row + && highlighted_move_cells.to.col === col; + + if (is_move_to) { + ctx.fillStyle = hexToRgba(move_pieces_color, block_move_to_alpha); + } else if (is_move_from) { + ctx.fillStyle = hexToRgba(move_pieces_color, block_move_from_alpha); + } else if (is_marked) { + const alpha = is_light ? block_select_alpha_light : block_select_alpha_dark; + ctx.fillStyle = hexToRgba(select_block, alpha); + } else { + ctx.fillStyle = is_light ? block_light : block_dark; + } + ctx.fillRect(vcol * block_size, vrow * block_size, block_size, block_size); + } + } + + if (noise_image) { + ctx.save(); + ctx.globalCompositeOperation = 'soft-light'; + ctx.drawImage(noise_image, 0, 0, canvas.width, canvas.height); + ctx.restore(); + } +} + +function drawResultOverlays() { + if (!result_markers) { + return; + } + + const progress = getResultOverlayProgress(); + const winner = toVisual(result_markers.winner.row, result_markers.winner.col); + const loser = toVisual(result_markers.loser.row, result_markers.loser.col); + + if (result_markers.type === 'draw') { + ctx.fillStyle = hexToRgba(result_draw, result_overlay_alpha * progress); + ctx.fillRect(winner.vcol * block_size, winner.vrow * block_size, block_size, block_size); + ctx.fillRect(loser.vcol * block_size, loser.vrow * block_size, block_size, block_size); + return; + } + + ctx.fillStyle = hexToRgba(result_win, result_overlay_alpha * progress); + ctx.fillRect(winner.vcol * block_size, winner.vrow * block_size, block_size, block_size); + + ctx.fillStyle = hexToRgba(result_lose, result_overlay_alpha * progress); + ctx.fillRect(loser.vcol * block_size, loser.vrow * block_size, block_size, block_size); +} + +function drawResultIcons() { + if (!result_markers) { + return; + } + + const progress = getResultOverlayProgress(); + const scale = getResultIconScale(progress); + + const draw_icon = (cell, icon) => { + if (!cell || !icon || !icon.complete) { + return; + } + const { vrow, vcol } = toVisual(cell.row, cell.col); + const max_size = 62; + const icon_ratio = (icon.naturalWidth || 61) / (icon.naturalHeight || 54); + const base_w = icon_ratio >= 1 ? max_size : max_size * icon_ratio; + const base_h = icon_ratio >= 1 ? max_size / icon_ratio : max_size; + const icon_w = base_w * scale; + const icon_h = base_h * scale; + const center_x = vcol * block_size + (block_size / 2); + const center_y = vrow * block_size + (block_size / 2); + const x = center_x - (icon_w / 2); + const y = center_y - (icon_h / 2); + ctx.save(); + ctx.globalAlpha = Math.min(1, progress * 1.15); + ctx.drawImage(icon, x, y, icon_w, icon_h); + ctx.restore(); + }; + + if (result_markers.type === 'draw') { + draw_icon(result_markers.winner, draw_icon_image); + draw_icon(result_markers.loser, draw_icon_image); + return; + } + + draw_icon(result_markers.winner, crown_icon_image); + draw_icon(result_markers.loser, flag_icon_image); +} + +function drawPiece(img, vcol, vrow) { + const max_size = block_size * 0.82; + const ratio = img.naturalWidth / img.naturalHeight; + const draw_w = ratio >= 1 ? max_size : max_size * ratio; + const draw_h = ratio >= 1 ? max_size / ratio : max_size; + const x = vcol * block_size + (block_size - draw_w) / 2; + const y = vrow * block_size + (block_size - draw_h) / 2; + ctx.drawImage(img, x, y, draw_w, draw_h); +} + +function drawPieces() { + const hide_row = piece_animation ? piece_animation.to.row : -1; + const hide_col = piece_animation ? piece_animation.to.col : -1; + + for (let row = 0; row < board_size; row += 1) { + for (let col = 0; col < board_size; col += 1) { + if (row === hide_row && col === hide_col) { + continue; + } + + const piece = board[row][col]; + if (!piece || !piece_images[piece]) { + continue; + } + const { vrow, vcol } = toVisual(row, col); + drawPiece(piece_images[piece], vcol, vrow); + } + } +} + +function drawAnimatedPiece() { + if (!piece_animation || !piece_images[piece_animation.piece_code]) { + return; + } + + const progress = Math.min(1, (performance.now() - piece_animation.start_time) / piece_animation.duration); + const eased = 1 - ((1 - progress) * (1 - progress)); + const row = piece_animation.from.row + ((piece_animation.to.row - piece_animation.from.row) * eased); + const col = piece_animation.from.col + ((piece_animation.to.col - piece_animation.from.col) * eased); + const visual = toVisual(row, col); + drawPiece(piece_images[piece_animation.piece_code], visual.vcol, visual.vrow); +} + +function drawArrow(from_col, from_row, to_col, to_row) { + const from = toVisual(from_row, from_col); + const to = toVisual(to_row, to_col); + const from_x = from.vcol * block_size + block_size / 2; + const from_y = from.vrow * block_size + block_size / 2; + const to_x = to.vcol * block_size + block_size / 2; + const to_y = to.vrow * block_size + block_size / 2; + const angle = Math.atan2(to_y - from_y, to_x - from_x); + const head_size = 28; + const line_end_x = to_x - Math.cos(angle) * (head_size * 0.6); + const line_end_y = to_y - Math.sin(angle) * (head_size * 0.6); + + ctx.save(); + ctx.strokeStyle = arrow_color; + ctx.fillStyle = arrow_color; + ctx.lineWidth = 10; + ctx.lineCap = 'round'; + ctx.globalAlpha = 0.85; + + ctx.beginPath(); + ctx.moveTo(from_x, from_y); + ctx.lineTo(line_end_x, line_end_y); + ctx.stroke(); + + ctx.translate(to_x, to_y); + ctx.rotate(angle); + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(-head_size, -head_size / 2.2); + ctx.lineTo(-head_size, head_size / 2.2); + ctx.closePath(); + ctx.fill(); + ctx.restore(); +} + +function isMobileLayout() { + return mobile_breakpoint.matches; +} + +function setMobileMovesOpen(is_open) { + if (!app_layout) { + return; + } + app_layout.classList.toggle('mobile_moves_open', is_open); +} + +function syncFirstMoveButtonIcon() { + if (!first_move_icon) { + return; + } + first_move_icon.src = isMobileLayout() + ? `${asset_root}/icons/pgn_moves.svg` + : `${asset_root}/icons/first.png`; +} + +function setMobileMenuOpen(is_open) { + if (!app_layout) { + return; + } + app_layout.classList.toggle('mobile_menu_open', is_open); + if (more_btn) { + more_btn.classList.toggle('active', is_open); + } +} + +function syncMobileUiState() { + const is_mobile = isMobileLayout(); + syncFirstMoveButtonIcon(); + if (!is_mobile) { + setMobileMovesOpen(false); + setMobileMenuOpen(false); + was_mobile_layout = false; + return; + } + + if (!was_mobile_layout) { + setMobileMovesOpen(true); + setMobileMenuOpen(false); + was_mobile_layout = true; + } +} + +function drawArrows() { + for (const arrow of arrows) { + drawArrow(arrow.from_col, arrow.from_row, arrow.to_col, arrow.to_row); + } +} + +function render() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawBoard(); + drawPieces(); + drawAnimatedPiece(); + drawResultOverlays(); + drawResultIcons(); + drawArrows(); +} + +function flipBoard() { + is_flipped = !is_flipped; + app_layout.classList.toggle('board_flipped', is_flipped); + applyPlayerAvatars(); + render(); +} + +function getCellFromEvent(event) { + const rect = canvas.getBoundingClientRect(); + const scale_x = canvas.width / rect.width; + const scale_y = canvas.height / rect.height; + const canvas_x = (event.clientX - rect.left) * scale_x; + const canvas_y = (event.clientY - rect.top) * scale_y; + const vcol = Math.floor(canvas_x / block_size); + const vrow = Math.floor(canvas_y / block_size); + return fromVisual(vrow, vcol); +} + +function setupNavigation() { + document.querySelector('.first_move_btn').addEventListener('click', () => { + if (isMobileLayout()) { + setMobileMovesOpen(!app_layout.classList.contains('mobile_moves_open')); + return; + } + setPly(0, true); + }); + document.querySelector('.prev_move_btn').addEventListener('click', () => setPly(current_ply - 1, true)); + document.querySelector('.next_move_btn').addEventListener('click', () => setPly(current_ply + 1, true)); + document.querySelector('.last_move_btn').addEventListener('click', () => { + const game = getCurrentGame(); + if (game) { + setPly(game.moves.length, true); + } + }); + + document.addEventListener('keydown', (event) => { + if (event.key === 'ArrowLeft') { + setPly(current_ply - 1, true); + } else if (event.key === 'ArrowRight') { + setPly(current_ply + 1, true); + } else if (event.key === 'Home') { + setPly(0, true); + } else if (event.key === 'End') { + const game = getCurrentGame(); + if (game) { + setPly(game.moves.length, true); + } + } + }); +} + +function setupMobileButtons() { + if (!more_btn) { + return; + } + + more_btn.addEventListener('click', () => { + if (!isMobileLayout()) { + return; + } + setMobileMenuOpen(!app_layout.classList.contains('mobile_menu_open')); + }); + + const mobile_menu_buttons = [ + document.getElementById('flip_board_btn'), + history_btn, + share_btn + ]; + + for (const button of mobile_menu_buttons) { + if (!button) { + continue; + } + button.addEventListener('click', () => { + if (isMobileLayout()) { + setMobileMenuOpen(false); + } + }); + } + + if (typeof mobile_breakpoint.addEventListener === 'function') { + mobile_breakpoint.addEventListener('change', syncMobileUiState); + } else { + mobile_breakpoint.addListener(syncMobileUiState); + } + + window.addEventListener('resize', syncMobileUiState); + syncMobileUiState(); +} + +function requestFullscreenIfPossible() { + if (document.fullscreenElement) { + return; + } + + const root = document.documentElement; + const request = root.requestFullscreen + || root.webkitRequestFullscreen + || root.msRequestFullscreen; + + if (typeof request === 'function') { + Promise.resolve(request.call(root)).catch(() => {}); + } +} + +function setupAutoFullscreen() { + requestFullscreenIfPossible(); + + const on_first_interaction = () => { + requestFullscreenIfPossible(); + window.removeEventListener('pointerdown', on_first_interaction); + window.removeEventListener('touchstart', on_first_interaction); + window.removeEventListener('click', on_first_interaction); + }; + + window.addEventListener('pointerdown', on_first_interaction, { once: true }); + window.addEventListener('touchstart', on_first_interaction, { once: true }); + window.addEventListener('click', on_first_interaction, { once: true }); +} + +function setupPanels() { + history_btn.addEventListener('click', () => openPanel(history_panel)); + share_btn.addEventListener('click', () => { + const game = getCurrentGame(); + share_pgn_text.value = game ? game.pgn_text : ''; + autoResizeShareTextarea(); + setShareStatus('', false); + openPanel(share_panel); + }); + submenu_overlay.addEventListener('click', closePanels); + + load_pgn_btn.addEventListener('click', () => { + if (!chess_ready) { + setShareStatus(ui_text.chess_library_not_ready, true); + return; + } + + const pgn_text = sanitizePgnInput(share_pgn_text.value); + if (!pgn_text) { + setShareStatus(ui_text.pgn_empty, true); + return; + } + + const parsed_game = parsePgnToGameState(pgn_text); + if (!parsed_game) { + setShareStatus(ui_text.pgn_invalid, true); + return; + } + + setShareStatus(ui_text.pgn_loaded, false); + share_pgn_text.value = pgn_text; + autoResizeShareTextarea(); + parsed_games.unshift(parsed_game); + buildHistoryList(); + closePanels(); + loadGame(0); + }); + + share_pgn_text.addEventListener('input', autoResizeShareTextarea); + window.addEventListener('resize', autoResizeShareTextarea); + + copy_pgn_btn.addEventListener('click', async () => { + try { + await copyTextToClipboard(share_pgn_text.value); + setShareStatus(ui_text.pgn_copied, false); + } catch (error) { + setShareStatus(ui_text.copy_failed, true); + } + }); +} + +function initializeGames() { + parsed_games = []; + current_game_index = 0; + current_ply = 0; + highlighted_move_cells = null; + piece_animation = null; + clearResultVisuals(); + buildHistoryList(); + board = boardFromFen(start_fen); + moves_scroll.innerHTML = ''; + render(); + updateTimers(); + app_layout.classList.remove('board_flipped'); + applyPlayerAvatars(); +} + +document.getElementById('flip_board_btn').addEventListener('click', flipBoard); +moves_scroll.addEventListener('scroll', updateScrollShadow); +new MutationObserver(updateScrollShadow).observe(moves_scroll, { childList: true, subtree: true }); +window.addEventListener('resize', updateScrollShadow); + +canvas.addEventListener('contextmenu', (event) => event.preventDefault()); + +canvas.addEventListener('mousedown', (event) => { + if (event.button !== 2) { + return; + } + right_drag_start = getCellFromEvent(event); +}); + +canvas.addEventListener('mouseup', (event) => { + if (event.button !== 2 || !right_drag_start) { + return; + } + + const { row, col } = getCellFromEvent(event); + + if (row === right_drag_start.row && col === right_drag_start.col) { + const key = `${row},${col}`; + if (marked_cells.has(key)) { + marked_cells.delete(key); + } else { + marked_cells.add(key); + } + } else { + const existing = arrows.findIndex((arrow) => ( + arrow.from_row === right_drag_start.row + && arrow.from_col === right_drag_start.col + && arrow.to_row === row + && arrow.to_col === col + )); + + if (existing >= 0) { + arrows.splice(existing, 1); + } else { + arrows.push({ + from_row: right_drag_start.row, + from_col: right_drag_start.col, + to_row: row, + to_col: col + }); + } + } + + right_drag_start = null; + render(); +}); + +canvas.addEventListener('click', () => { + marked_cells.clear(); + arrows = []; + render(); +}); + +loadNoise(() => { + loadPieceImages(async () => { + chess_ready = await loadChessLibrary(); + setupNavigation(); + setupMobileButtons(); + setupAutoFullscreen(); + setupPanels(); + initializeGames(); + updateScrollShadow(); + }); +}); diff --git a/SunnexGB/Heroku-Modules/Assets/NoChess/raw_assets/style.css b/SunnexGB/Heroku-Modules/Assets/NoChess/raw_assets/style.css new file mode 100644 index 0000000..6a5ef05 --- /dev/null +++ b/SunnexGB/Heroku-Modules/Assets/NoChess/raw_assets/style.css @@ -0,0 +1,967 @@ +@font-face { + font-family: 'mr_GranstanderCleanG'; + src: url('https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/mr_granstandercleang.otf') format('opentype'); + font-weight: 400; + font-style: normal; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background-color: #1A1224; + background-image: url('bg.png'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + min-height: 100vh; + font-family: 'mr_GranstanderCleanG', Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; +} + +.web_chess { + display: flex; + align-items: stretch; + gap: 20px; +} + +@media (min-width: 573px) and (max-width: 1562px) { + body { + overflow: hidden; + } + + .web_chess { + width: 1562px; + position: fixed; + left: 50%; + top: 50%; + margin: 0; + --desktop-scale: min(calc(100vw / 1562px), calc(100vh / 856px), 1); + transform-origin: center center; + transform: translate(-50%, -50%) scale(var(--desktop-scale)); + } +} + +.board-wrapper { + position: relative; + display: block; +} + +#chessBoard { + width: 856px; + height: 856px; + display: block; +} + +.pgn_moves_board { + width: 333px; + height: 856px; + position: relative; + overflow: hidden; +} + +.move_board_bg { + position: absolute; + inset: 0; + background: #1E1E1E; + border-radius: 15px; +} + +.move_board_bg::after { + content: ''; + position: absolute; + left: 0; + right: 0; + bottom: 80px; + height: 48px; + background: linear-gradient(180deg, #00000000 0%, #00000040 100%); + opacity: 0; + transition: opacity 0.2s; + pointer-events: none; +} + +.pgn_moves_board.has_scroll .move_board_bg::after { + opacity: 1; +} + +.moves_list { + position: absolute; + top: 20px; + bottom: 80px; + left: 0; + right: 0; + padding: 0 20px; +} + +.moves_list_mobile { + height: 100%; + overflow-y: auto; + display: flex; + flex-direction: column; + scrollbar-width: none; + -ms-overflow-style: none; +} + +.moves_list_mobile::-webkit-scrollbar { + width: 0; + height: 0; + display: none; +} + +.move_row { + display: grid; + grid-template-columns: 40px 1fr 1fr; + align-items: center; + min-height: 52px; + font-size: 20px; + color: #FFFFFF; +} + +.move_number { + opacity: 0.6; +} + +.move_white, +.move_black { + padding: 10px; + display: flex; + align-items: center; + border-radius: 8px; +} + +.current_move { + background: #B7B7B71A; + min-height: 52px; +} + +.move_white, +.move_black { + cursor: pointer; +} + +.move_white:empty, +.move_black:empty { + cursor: default; +} + +.moves_control_buttons { + position: absolute; + bottom: 10px; + left: 0; + width: 100%; + display: flex; + justify-content: space-around; +} + +.move_btn { + width: 75px; + height: 37px; + background: #2D2D2D; + border-radius: 8px; + box-shadow: 0px 4px 3.7px -1px #00000040; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background 0.2s, transform 0.2s; +} + +.move_btn img { + width: 20px; + height: 20px; + object-fit: contain; +} + +.move_btn:hover { + background: #3A3A3A; + transform: translateY(-2px); +} + +.move_btn:active { + background: #252525; + transform: translateY(0); +} + +.main_board { + width: 333px; + height: 856px; + position: relative; +} + +.main_board_bg { + position: absolute; + inset: 0; + background: #00000054; + border-radius: 15px; +} + +.player_block { + position: absolute; + width: 100%; + text-align: center; +} + +.player_black { + top: 20px; +} + +.player_white { + bottom: 20px; +} + +.web_chess.board_flipped .player_black { + top: auto; + bottom: 20px; + display: flex; + flex-direction: column; + align-items: center; +} + +.web_chess.board_flipped .player_white { + top: 20px; + bottom: auto; + display: flex; + flex-direction: column; + align-items: center; +} + +.web_chess.board_flipped .player_black .player_avatar { + order: 1; +} + +.web_chess.board_flipped .player_black .player_name_black { + order: 2; + margin-top: 10px; + margin-bottom: 0; +} + +.web_chess.board_flipped .player_white .player_name_white { + order: 1; + margin-top: 0; + margin-bottom: 10px; +} + +.web_chess.board_flipped .player_white .player_avatar { + order: 2; +} + +.player_avatar { + position: relative; + overflow: hidden; + width: 189px; + height: 189px; + aspect-ratio: 1 / 1; + margin: 0 auto; + background-size: cover; + background-repeat: no-repeat; + background-position: center; + border: 5px solid #FFFFFF; + border-radius: 50%; + clip-path: circle(50% at 50% 50%); +} + +.avatar_black { + background-image: url('https://i.pinimg.com/736x/6e/0a/0c/6e0a0cf688b30ba9de81b81bb32e49f9.jpg'); +} + +.avatar_white { + background-image: url('https://i.pinimg.com/736x/6e/0a/0c/6e0a0cf688b30ba9de81b81bb32e49f9.jpg'); +} + +.player_name_black { + margin-bottom: 10px; + font-size: 20px; + color: #FFFFFF; +} + +.player_name_white { + margin-top: 10px; + font-size: 20px; + color: #FFFFFF; +} + +.divider_line { + position: absolute; + left: 50%; + transform: translateX(-50%); +} + +.top_line { + top: 260px; +} + +.bottom_line { + bottom: 260px; +} + +.divider_line svg { + display: block; +} + +.timer { + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 139px; + height: 41px; + display: flex; + align-items: center; + justify-content: center; + background: #00000054; + border-radius: 15px; + color: #FFFFFF; + font-size: 20px; +} + +.timer_black { + top: 280px; +} + +.timer_white { + bottom: 280px; +} + +.web_chess.board_flipped .timer_black { + top: auto; + bottom: 280px; +} + +.web_chess.board_flipped .timer_white { + top: 280px; + bottom: auto; +} + +.board_menu { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + gap: 15px; +} + +.menu_btn { + width: 50px; + height: 42px; + background: #00000054; + border-radius: 15px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background 0.2s, transform 0.2s; +} + +.menu_btn:hover { + background: #FFFFFF1A; + transform: translateY(-2px); +} + +.menu_btn:active { + background: #00000080; + transform: translateY(0); +} + +.menu_btn img { + display: block; + width: 28px; + height: 24px; + object-fit: contain; +} + +.menu_more_btn { + display: none; +} + +.more_dots { + position: relative; + width: 22px; + height: 22px; +} + +.more_dot { + position: absolute; + left: 50%; + top: 50%; + width: 8px; + height: 8px; + border-radius: 50%; + background: #FFFFFF; + transform: translate(-50%, -50%); +} + +.more_dot_top { + transform: translate(-50%, calc(-50% - var(--dot-gap, 10px))); + transition: transform 0.32s ease; +} + +.more_dot_mid { + transition: transform 0.32s ease; +} + +.more_dot_bottom { + transform: translate(-50%, calc(-50% + var(--dot-gap, 10px))); + transition: transform 0.32s ease; +} + +.menu_more_btn.active .more_dot_top { + transform: translate(calc(-50% - var(--dot-gap, 10px)), -50%); +} + +.menu_more_btn.active .more_dot_bottom { + transform: translate(calc(-50% + var(--dot-gap, 10px)), -50%); +} + +.web_chess.mobile_menu_open .menu_more_btn .more_dot_mid { + transform: translate(-50%, -50%) scale(1.05); +} + +.submenu_overlay { + position: fixed; + inset: 0; + background: #00000080; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s; +} + +.submenu_overlay.open { + opacity: 1; + pointer-events: auto; +} + +.submenu_panel { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0.98); + width: min(540px, 92vw); + max-height: 80vh; + background: #1e1e1e; + border-radius: 15px; + border: 1px solid #FFFFFF14; + padding: 18px; + color: #FFFFFF; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s, transform 0.2s; + z-index: 20; +} + +.submenu_panel.open { + opacity: 1; + pointer-events: auto; + transform: translate(-50%, -50%) scale(1); +} + +.submenu_title { + font-size: 20px; + margin-bottom: 12px; +} + +.history_games { + display: flex; + flex-direction: column; + gap: 10px; +} + +.history_games.empty { + min-height: 120px; + justify-content: center; + align-items: center; +} + +.history_empty { + font-size: 24px; + font-weight: 700; + text-align: center; +} + +.history_game_btn { + width: 100%; + background: #2d2d2d; + color: #FFFFFF; + border: 1px solid #FFFFFF14; + border-radius: 10px; + padding: 10px 12px; + text-align: left; + cursor: pointer; +} + +.history_game_btn:hover { + background: #383838; +} + +#share_pgn_text { + width: 100%; + min-height: 170px; + max-height: 55vh; + border-radius: 10px; + border: 1px solid #FFFFFF1A; + background: #121212; + color: #FFFFFF; + padding: 10px; + resize: none; + overflow: auto; +} + +.share_actions { + margin-top: 12px; + display: flex; + gap: 10px; + justify-content: center; +} + +#load_pgn_btn, +#copy_pgn_btn { + height: 38px; + padding: 0 14px; + border-radius: 10px; + border: 0; + background: #2d2d2d; + color: #FFFFFF; + cursor: pointer; + min-width: 130px; +} + +#load_pgn_btn { + background: #2d2d2d; +} + +#load_pgn_btn:hover, +#copy_pgn_btn:hover { + filter: brightness(1.08); +} + +#share_status { + margin-top: 10px; + min-height: 20px; + color: #d9d9d9; + font-size: 14px; + text-align: center; + opacity: 0; + transform: translateY(4px); + transition: opacity 0.2s ease, transform 0.2s ease; +} + +#share_status.show { + opacity: 1; + transform: translateY(0); +} + +#share_status.error { + color: #ff8f8f; +} + +@media (max-width: 572px) { + body { + justify-content: center; + align-items: flex-start; + overflow: hidden; + } + + body::after { + content: ''; + position: fixed; + left: 0; + right: 0; + bottom: 0; + height: clamp(72px, 17.7vw, 101px); + background: linear-gradient(180deg, #00000000 0%, #0000008C 100%); + pointer-events: none; + z-index: 5; + } + + .web_chess { + --mobile-layout-width: 750px; + --mobile-board-size: 750px; + --mobile-board-top: 233px; + --mobile-player-gap: clamp(24px, 5vw, 32px); + --mobile-panel-height: clamp(64px, 17.13vw, 98px); + --mobile-timer-height: clamp(30px, 7.17vw, 41px); + --mobile-pgn-space: clamp(360px, 86vw, 492px); + --mobile-layout-height: calc(var(--mobile-board-top) + var(--mobile-board-size) + var(--mobile-pgn-space)); + --mobile-fit-scale: min(1, calc((100vw - 2px) / var(--mobile-layout-width)), calc((100dvh - 2px) / var(--mobile-layout-height))); + width: var(--mobile-layout-width); + height: var(--mobile-layout-height); + min-height: 0; + margin: 0; + position: fixed; + left: 50%; + top: 0; + display: block; + padding: 0; + overflow: hidden; + transform-origin: top center; + transform: translateX(-50%) scale(var(--mobile-fit-scale)); + } + + .web_chess::after { + content: none; + } + + .board-wrapper { + position: absolute; + left: 50%; + top: var(--mobile-board-top); + transform: translateX(-50%); + width: var(--mobile-board-size); + z-index: 2; + } + + #chessBoard { + width: 100%; + max-width: none; + height: auto; + } + + .main_board { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + z-index: 3; + pointer-events: none; + } + + .main_board_bg, + .divider_line { + display: none; + } + + .board_menu { + top: 4.2vw; + right: 3.1vw; + left: auto; + transform: none; + gap: 2.2vw; + align-items: center; + pointer-events: auto; + } + + .board_menu .menu_btn:not(.menu_more_btn) { + opacity: 1; + transform: translateX(0); + transition: transform 0.28s ease, opacity 0.28s ease, background 0.2s; + } + + .web_chess:not(.mobile_menu_open) .board_menu .menu_btn:not(.menu_more_btn) { + opacity: 0; + transform: translateX(11vw) scale(0.86); + pointer-events: none; + } + + .web_chess.mobile_menu_open .board_menu .menu_btn:not(.menu_more_btn) { + opacity: 1; + transform: translateX(0); + pointer-events: auto; + } + + .menu_btn { + width: 50px; + height: 42px; + border-radius: 15px; + } + + .menu_btn img { + width: 28px; + height: 24px; + } + + .menu_more_btn { + --dot-gap: 14px; + display: flex; + width: 80px; + height: 80px; + border-radius: 50%; + } + + .menu_more_btn .more_dots { + width: 34px; + height: 34px; + min-width: 34px; + min-height: 34px; + } + + .menu_more_btn .more_dot { + width: 9px; + height: 9px; + } + + .player_block { + width: calc(100% - 4.2vw); + left: 2.1vw; + height: var(--mobile-panel-height); + min-height: 64px; + border-radius: clamp(10px, 2.6vw, 15px); + display: flex; + align-items: center; + gap: clamp(10px, 2.8vw, 18px); + padding: 0 calc(clamp(96px, 24.3vw, 139px) + max(6vw, 22px)) 0 max(2.4vw, 12px); + text-align: left; + background: #00000054; + pointer-events: auto; + } + + .player_black { + top: calc(var(--mobile-board-top) - var(--mobile-player-gap) - var(--mobile-panel-height)); + } + + .player_black .player_avatar { + order: 1; + } + + .player_black .player_name_black { + order: 2; + text-align: left; + } + + .player_white { + top: calc(var(--mobile-board-top) + var(--mobile-board-size) + var(--mobile-player-gap)); + bottom: auto; + } + + .player_avatar { + width: min(13.99vw, 80px); + height: min(13.99vw, 80px); + margin: 0; + border-width: 2px; + flex-shrink: 0; + } + + .player_name_black, + .player_name_white { + margin: 0; + min-width: 0; + max-width: min(39vw, 220px); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: clamp(14px, 3.8vw, 20px); + color: #FFFFFF; + } + + .timer { + left: auto; + right: 6vw; + transform: none; + width: clamp(96px, 24.3vw, 139px); + height: var(--mobile-timer-height); + border-radius: 15px; + font-size: clamp(12px, 3.8vw, 20px); + background: #00000066; + color: #FFFFFF; + text-shadow: 0 1px 1px #0000004D; + z-index: 7; + pointer-events: auto; + } + + .timer_black { + top: calc(var(--mobile-board-top) - var(--mobile-player-gap) - var(--mobile-panel-height) + (var(--mobile-panel-height) - var(--mobile-timer-height)) / 2); + } + + .timer_white { + top: calc(var(--mobile-board-top) + var(--mobile-board-size) + var(--mobile-player-gap) + (var(--mobile-panel-height) - var(--mobile-timer-height)) / 2); + bottom: auto; + } + + .pgn_moves_board { + position: absolute; + top: calc(var(--mobile-board-top) + var(--mobile-board-size) + clamp(24px, 5vw, 32px) + var(--mobile-panel-height) + clamp(18px, 4vw, 28px)); + left: 50%; + width: calc(100% - clamp(10px, 2.7vw, 20px)); + height: clamp(180px, 56vw, 320px); + z-index: 3; + opacity: 1; + transform: translateX(-50%); + transition: opacity 0.24s ease; + } + + .move_board_bg { + display: none; + } + + .moves_list { + top: 0; + bottom: clamp(66px, 14vw, 84px); + left: 50%; + right: auto; + width: clamp(260px, 44vw, 360px); + transform: translateX(-50%); + padding: 0; + transition: opacity 0.24s ease; + } + + .moves_list::after { + content: none; + } + + .moves_list_mobile { + height: 140%; + gap: 0.6vw; + pointer-events: auto; + padding-right: 0; + align-items: center; + transform: translateY(clamp(10px, 2.2vh, 18px)); + scrollbar-width: none; + -ms-overflow-style: none; + } + + .moves_list_mobile::-webkit-scrollbar { + width: 0; + height: 0; + display: none; + } + + .move_row { + min-height: 9vw; + font-size: clamp(18px, 4.2vw, 24px); + width: max-content; + margin: 0 auto; + grid-template-columns: auto auto auto; + column-gap: clamp(10px, 2.8vw, 16px); + } + + .move_number { + opacity: 1; + } + + .move_white, + .move_black { + padding: 0; + min-height: 0; + width: max-content; + max-width: 100%; + justify-self: start; + justify-content: flex-start; + } + + .current_move { + min-height: 0; + width: max-content; + padding: clamp(4px, 1vw, 8px) clamp(6px, 1.4vw, 10px); + } + + .moves_control_buttons { + left: 2.6vw; + right: 2.6vw; + bottom: 0; + width: auto; + justify-content: flex-end; + gap: 1.5vw; + pointer-events: auto; + } + + .move_btn { + width: 80px; + height: 80px; + border-radius: 50%; + background: #00000054; + box-shadow: none; + } + + .menu_btn, + .move_btn { + transition: background 0.2s, transform 0.2s; + -webkit-tap-highlight-color: #00000000; + } + + .menu_btn:hover { + background: #FFFFFF1A; + transform: translateY(-2px); + } + + .move_btn:hover { + background-color: #00000054; + transform: none; + } + + .menu_btn:active { + background: #00000080; + transform: translateY(0); + } + + .menu_more_btn:hover { + background-color: #00000054; + transform: none; + } + + .menu_more_btn:active { + background-color: #00000054; + transform: scale(1.06); + } + + .move_btn:active { + background-color: #00000054; + transform: scale(1.06); + } + + .move_btn img { + width: 46%; + height: 46%; + } + + .first_move_btn { + margin-right: auto; + } + + .last_move_btn { + display: none; + } + + .web_chess:not(.mobile_moves_open) .moves_list { + opacity: 0; + pointer-events: none; + } + + .web_chess.board_flipped .player_black { + top: calc(var(--mobile-board-top) + var(--mobile-board-size) + var(--mobile-player-gap)); + bottom: auto; + flex-direction: row; + align-items: center; + justify-content: flex-start; + text-align: left; + } + + .web_chess.board_flipped .player_white { + top: calc(var(--mobile-board-top) - var(--mobile-player-gap) - var(--mobile-panel-height)); + bottom: auto; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + text-align: left; + } + + .web_chess.board_flipped .player_white .player_avatar { + order: 1; + } + + .web_chess.board_flipped .player_white .player_name_white { + order: 2; + margin: 0; + align-self: center; + line-height: 1.2; + } + + .web_chess.board_flipped .player_black .player_name_black { + margin: 0; + align-self: center; + line-height: 1.2; + } + + .web_chess.board_flipped .timer_black { + top: calc(var(--mobile-board-top) + var(--mobile-board-size) + var(--mobile-player-gap) + (var(--mobile-panel-height) - var(--mobile-timer-height)) / 2); + bottom: auto; + } + + .web_chess.board_flipped .timer_white { + top: calc(var(--mobile-board-top) - var(--mobile-player-gap) - var(--mobile-panel-height) + (var(--mobile-panel-height) - var(--mobile-timer-height)) / 2); + bottom: auto; + } +} diff --git a/SunnexGB/Heroku-Modules/Assets/XOCheat/Opening_book.txt b/SunnexGB/Heroku-Modules/Assets/XOCheat/Opening_book.txt new file mode 100644 index 0000000..45a1674 --- /dev/null +++ b/SunnexGB/Heroku-Modules/Assets/XOCheat/Opening_book.txt @@ -0,0 +1,1782 @@ +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.7 +2.4 +3.8 +4.5 +5.6 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.8 +3.1 +4.3 +5.7 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.6 +2.2 +3.8 +4.1 +5.7 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.0 +2.1 +3.6 +4.2 +5.3 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.2 +3.3 +4.6 +5.5 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.5 +2.7 +3.4 +4.6 +5.3 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.8 +2.0 +3.2 +4.4 +5.5 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.8 +2.3 +3.2 +4.0 +5.5 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.7 +3.8 +4.5 +5.0 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.1 +2.7 +3.0 +4.3 +5.2 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.8 +3.6 +4.7 +5.2 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.0 +3.3 +4.7 +5.5 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.0 +3.6 +4.7 +5.2 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.0 +2.3 +3.2 +4.4 +5.1 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.8 +2.4 +3.6 +4.1 +5.7 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.5 +3.8 +4.6 +5.0 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.5 +2.3 +3.2 +4.1 +5.8 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.0 +2.2 +3.8 +4.1 +5.4 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.2 +3.0 +4.1 +5.8 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.3 +3.0 +4.2 +5.8 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.1 +2.0 +3.4 +4.3 +5.7 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.0 +2.4 +3.1 +4.7 +5.2 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.6 +2.4 +3.0 +4.7 +5.3 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.6 +2.0 +3.2 +4.7 +5.4 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.8 +2.1 +3.6 +4.0 +5.7 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.2 +2.4 +3.1 +4.3 +5.0 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.2 +2.3 +3.4 +4.0 +5.6 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.2 +2.3 +3.0 +4.6 +5.1 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.8 +2.6 +3.5 +4.0 +5.2 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.2 +3.1 +4.6 +5.7 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.5 +2.3 +3.8 +4.0 +5.2 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.5 +3.1 +4.2 +5.7 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.7 +2.1 +3.8 +4.0 +5.6 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.7 +2.3 +3.6 +4.4 +5.8 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.6 +2.8 +3.3 +4.5 +5.0 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.1 +2.2 +3.4 +4.6 +5.7 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.1 +2.6 +3.0 +4.4 +5.2 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.0 +2.8 +3.2 +4.3 +5.1 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.2 +2.5 +3.1 +4.6 +5.0 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.7 +3.2 +4.0 +5.6 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.8 +3.2 +4.1 +5.6 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.7 +2.1 +3.6 +4.5 +5.8 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.1 +2.5 +3.2 +4.8 +5.0 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.2 +2.1 +3.5 +4.4 +5.8 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.3 +2.5 +3.0 +4.1 +5.6 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.0 +2.5 +3.2 +4.3 +5.1 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.5 +3.2 +4.1 +5.6 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.2 +3.5 +4.6 +5.3 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.8 +2.6 +3.0 +4.1 +5.4 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.3 +2.2 +3.0 +4.1 +5.6 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.3 +2.8 +3.6 +4.7 +5.0 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.0 +3.5 +4.7 +5.3 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.0 +2.3 +3.1 +4.4 +5.2 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.1 +2.8 +3.2 +4.4 +5.0 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.1 +3.5 +4.8 +5.3 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.8 +2.7 +3.4 +4.3 +5.0 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.0 +2.1 +3.6 +4.7 +5.3 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.7 +3.6 +4.0 +5.2 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.8 +3.6 +4.1 +5.2 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.8 +2.0 +3.6 +4.3 +5.7 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.3 +3.7 +4.2 +5.1 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.8 +2.6 +3.5 +4.4 +5.2 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.5 +3.1 +4.6 +5.7 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.0 +2.4 +3.3 +4.2 +5.6 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.6 +2.1 +3.4 +4.3 +5.2 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.6 +2.5 +3.0 +4.4 +5.3 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.2 +2.4 +3.5 +4.0 +5.8 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.8 +2.4 +3.2 +4.0 +5.5 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.2 +2.8 +3.6 +4.0 +5.4 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.1 +3.2 +4.3 +5.6 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.8 +2.3 +3.4 +4.1 +5.0 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.1 +3.6 +4.7 +5.2 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.8 +2.1 +3.4 +4.6 +5.0 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.5 +3.0 +4.2 +5.8 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.2 +2.0 +3.5 +4.6 +5.8 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.8 +3.5 +4.6 +5.3 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.0 +2.8 +3.6 +4.5 +5.3 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.5 +2.7 +3.4 +4.0 +5.3 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.1 +3.3 +4.8 +5.5 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.0 +2.7 +3.4 +4.5 +5.8 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.1 +2.2 +3.4 +4.0 +5.7 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.0 +2.5 +3.4 +4.6 +5.8 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.3 +3.1 +4.6 +5.7 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.3 +2.1 +3.4 +4.2 +5.5 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.8 +3.6 +4.5 +5.2 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.0 +3.6 +4.5 +5.2 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.6 +2.3 +3.8 +4.2 +5.7 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.0 +2.6 +3.1 +4.4 +5.2 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.6 +3.1 +4.2 +5.7 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.5 +2.8 +3.4 +4.7 +5.3 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.7 +3.0 +4.5 +5.8 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.2 +2.0 +3.8 +4.6 +5.5 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.8 +2.7 +3.2 +4.0 +5.5 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.8 +2.7 +3.5 +4.0 +5.2 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.3 +3.1 +4.2 +5.7 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.3 +3.8 +4.1 +5.0 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.0 +2.3 +3.4 +4.7 +5.8 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.5 +3.0 +4.6 +5.8 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.0 +3.3 +4.1 +5.5 + +#3x3 +[Author "Zero Engine"] +[Site "t.me/H_SunMods"] +[X "Zero Bot"] +[O "Zero Opp"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.4 +2.0 +3.7 +4.5 +5.1 + +#3x3 +[Author "@SunnexGB"] +[Site "t.me/H_SunMods"] +[X "Sunnex"] +[O "Tester"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.0 +2.4 +3.8 + +#3x3 +[Author "@SunnexGB"] +[Site "t.me/H_SunMods"] +[X "Sunnex"] +[O "Tester"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.0 +2.4 +3.8 + +#5x5 +[Author "@SunnexGB"] +[Site "t.me/H_SunMods"] +[X "Sunnex"] +[O "PRO"] +[Result "1/2-1/2"] +[Variant "down O"] +[Opening] +1.12 +2.8 +3.19 +4.17 + + +#5x5 +[Author "@SunnexGB"] +[Site "t.me/H_SunMods"] +[X "Sunnex"] +[O "PRO"] +[Result "1-0"] +[Variant "First X"] +[Opening] +1.12 +2.8 +3.19 +4.13 + +#5x5 +[Author "Zero"] +[Site "t.me/H_SunMods"] +[X "Player X"] +[O "Player O"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.12 +2.2 +3.8 +4.16 +5.6 +6.7 +7.18 +8.24 +9.0 + +#5x5 +[Author "Zero"] +[Site "t.me/H_SunMods"] +[X "Player X"] +[O "Player O"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.1 +2.12 +3.7 +4.13 +5.11 +6.6 +7.3 +8.15 +9.2 +10.0 +11.4 + +#5x5 +[Author "Zero"] +[Site "t.me/H_SunMods"] +[X "Player X"] +[O "Player O"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.6 +2.12 +3.18 +4.16 +5.8 +6.7 +7.13 +8.23 +9.3 + +#5x5 +[Author "Zero"] +[Site "t.me/H_SunMods"] +[X "Player X"] +[O "Player O"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.7 +2.17 +3.1 +4.11 +5.13 +6.19 +7.3 +8.16 +9.18 +10.8 +11.2 +12.4 +13.0 + +#5x5 +[Author "Zero"] +[Site "t.me/H_SunMods"] +[X "Player X"] +[O "Player O"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.8 +2.1 +3.13 +4.3 +5.2 +6.7 +7.18 +8.23 +9.16 +10.17 +11.12 +12.11 +13.6 +14.24 +15.0 + +#5x5 +[Author "Zero"] +[Site "t.me/H_SunMods"] +[X "Player X"] +[O "Player O"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.12 +2.8 +3.19 +4.17 +5.13 +6.7 +7.11 +8.14 +9.10 + +#5x5 +[Author "Zero"] +[Site "t.me/H_SunMods"] +[X "Player X"] +[O "Player O"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.12 +2.8 +3.19 +4.17 +5.13 +6.11 +7.7 +8.1 +9.6 +10.5 +11.23 +12.15 +13.10 +14.9 +15.14 +16.3 +17.2 +18.22 +19.21 +20.0 +21.24 +22.4 +23.20 +24.16 +25.18 + +#5x5 +[Author "Zero"] +[Site "t.me/H_SunMods"] +[X "Player X"] +[O "Player O"] +[Result "1/2-1/2"] +[Variant "zero index"] +[Opening] +1.12 +2.8 +3.19 +4.17 +5.13 +6.11 +7.7 +8.1 +9.6 +10.5 +11.23 +12.18 +13.16 +14.21 +15.15 +16.9 +17.3 +18.2 +19.10 +20.14 +21.22 +22.0 +23.24 +24.4 +25.20 + +#5x5 +[Author "Zero"] +[Site "t.me/H_SunMods"] +[X "Player X"] +[O "Player O"] +[Result "1/2-1/2"] +[Variant "zero index"] +[Opening] +1.6 +2.12 +3.17 +4.11 +5.13 +6.18 +7.8 +8.7 +9.19 +10.9 + +#5x5 +[Author "Zero"] +[Site "t.me/H_SunMods"] +[X "Player X"] +[O "Player O"] +[Result "1-0"] +[Variant "zero index"] +[Opening] +1.6 +2.12 +3.17 +4.11 +5.13 +6.18 +7.8 +8.7 +9.19 +10.5 +11.9 +12.14 +13.21 + +#5x5 +[Author "Zero"] +[Site "t.me/H_SunMods"] +[X "Player X"] +[O "Player O"] +[Result "1/2-1/2"] +[Variant "zero index"] +[Opening] +1.23 +2.12 +3.21 +4.22 +5.17 +6.16 + +#5x5 +[Author "Zero"] +[Site "t.me/H_SunMods"] +[X "Player X"] +[O "Player O"] +[Result "0-1"] +[Variant "zero index"] +[Opening] +1.0 +2.12 + +#5x5 +[Author "Zero"] +[Site "t.me/H_SunMods"] +[X "Player X"] +[O "Player O"] +[Result "0-1"] +[Variant "zero index"] +[Opening] +1.4 +2.12 + +#5x5 +[Author "Zero"] +[Site "t.me/H_SunMods"] +[X "Player X"] +[O "Player O"] +[Result "0-1"] +[Variant "zero index"] +[Opening] +1.20 +2.12 + +#5x5 +[Author "Zero"] +[Site "t.me/H_SunMods"] +[X "Player X"] +[O "Player O"] +[Result "0-1"] +[Variant "zero index"] +[Opening] +1.24 +2.12 + +#5x5 +[Author "Zero"] +[Site "t.me/H_SunMods"] +[X "Player X"] +[O "Player O"] +[Result "0-1"] +[Variant "zero index"] +[Opening] +1.2 +2.12 + +#5x5 +[Author "Zero"] +[Site "t.me/H_SunMods"] +[X "Player X"] +[O "Player O"] +[Result "0-1"] +[Variant "zero index"] +[Opening] +1.22 +2.12 + +#5x5 +[Author "Zero"] +[Site "t.me/H_SunMods"] +[X "Player X"] +[O "Player O"] +[Result "0-1"] +[Variant "zero index"] +[Opening] +1.10 +2.12 + +#5x5 +[Author "Zero"] +[Site "t.me/H_SunMods"] +[X "Player X"] +[O "Player O"] +[Result "0-1"] +[Variant "zero index"] +[Opening] +1.14 +2.12 \ No newline at end of file diff --git a/SunnexGB/Heroku-Modules/Assets/tmp_assets/chalkboardse.ttf b/SunnexGB/Heroku-Modules/Assets/tmp_assets/chalkboardse.ttf new file mode 100644 index 0000000..7e179af Binary files /dev/null and b/SunnexGB/Heroku-Modules/Assets/tmp_assets/chalkboardse.ttf differ diff --git a/SunnexGB/Heroku-Modules/DevStats.py b/SunnexGB/Heroku-Modules/DevStats.py new file mode 100644 index 0000000..620af1b --- /dev/null +++ b/SunnexGB/Heroku-Modules/DevStats.py @@ -0,0 +1,323 @@ +# meta developer: @H_SunMods +# meta banner: https://r2.fakecrime.bio/uploads/7c43eb05-4387-48f8-bbb2-20c5fad2f85f.jpg +# current ver +__version__ = (1, 0, 1) + +from .. import loader, utils +from herokutl.types import Message +from ..types import InlineCall +import asyncio +import aiohttp +import math + +FHETA_URL = "https://api.fixyres.com/grates" +VECTOR_URL = "https://vector-three-sooty.vercel.app/api/devstats" +VECTOR_TOPMOD_URL = "https://vector-three-sooty.vercel.app/api/usertopmod?users=" + +@loader.tds +class DevStats(loader.Module): + """developers stats module""" + + strings = { + "name": "DevStats", + "loading": "Loading...", + "no_data": "Failed to fetch data. Try again later.", + "dev_header": "Most popular developers:\n\n", + "devtop_not_found": "Your not found.", + "topmod_not_found": "No modules found.", + "no_usernames": "No usernames configured. Set them in .fcfg DevStats usernames @username", + "select_page": "Select page:", + "btn_prev": "◄", + "btn_next": "►", + "btn_back": "Back", + "btn_close": "Close", + "like_singl": "like", + "just_likes": "likes", + "just_dislikes": "dislikes", + "devtop_desc": "Your rank in developer leaderboard", + "topmod_desc": "Your most popular module and its rank", + } + + strings_ru = { + "_cls_doc": "Модуль статистики разработчиков", + "loading": "Загрузка...", + "no_data": "Не удалось получить данные. Попробуйте позже.", + "dev_header": "Самые популярные разработчики:\n\n", + "devtop_not_found": "Вы не были найдены.", + "topmod_not_found": "Модули не найдены.", + "no_usernames": "Юзернеймы не настроены. Укажите в .fcfg DevStats usernames @username", + "select_page": "Выберите страницу:", + "btn_prev": "◄", + "btn_next": "►", + "btn_back": "Назад", + "btn_close": "Закрыть", + "like_singl": "Лайк", + "just_likes": "Лайков", + "just_dislikes": "Дизлайков", + "devtop_desc": "Ваше место в рейтинге разработчиков", + "topmod_desc": "Ваш самый популярный модуль и его место в топе", + } + + def __init__(self): + self.config = loader.ModuleConfig( + loader.ConfigValue( + "provider", + "multi", + "Data source: multi (fheta + vector combined) | fheta | vector", + validator=loader.validators.Choice(["multi", "fheta", "vector"]), + ), + loader.ConfigValue( + "display_mode", + "likes", + "Display mode: likes | both", + validator=loader.validators.Choice(["likes", "both"]), + ), + loader.ConfigValue( + "usernames", + [], + "Your usernames with @ for placeholders", + validator=loader.validators.Series(loader.validators.String()), + ), + loader.ConfigValue( + "excluded_authors", + ["unknown"], + "Authors to exclude from leaderboard", + validator=loader.validators.Series(loader.validators.String()), + ), + loader.ConfigValue( + "rank1_emoji", + "👑", + "Emoji for rank №1", + ), + loader.ConfigValue( + "rank2_emoji", + "🌟", + "Emoji for rank №2", + ), + loader.ConfigValue( + "rank3_emoji", + "", + "Emoji for rank №3", + ), + ) + + async def client_ready(self, client, db): + utils.register_placeholder("devtop", self.placeholder_devtop, self.strings("devtop_desc")) + utils.register_placeholder("topmod", self.placeholder_topmod, self.strings("topmod_desc")) + + async def request_api(self, url: str, token: str = None): + headers = {"Authorization": token} if token else {} + try: + async with aiohttp.ClientSession() as session: + async with session.get( + url, + headers=headers, + timeout=aiohttp.ClientTimeout(total=15), + ) as resp: + return await resp.json() if resp.status == 200 else None + except Exception: + return None + + def aggregate_devs(self, data: dict) -> list: + excluded = {u.lower() for u in self.config["excluded_authors"]} + devs = {} + items = data.items() if isinstance(data, dict) else ( + (e.get("url", i), e) for i, e in enumerate(data) + ) + for _, info in items: + author = info.get("author", "").lstrip("@") + if not author or author.lower() in excluded: + continue + if author not in devs: + devs[author] = {"likes": 0, "dislikes": 0} + devs[author]["likes"] += int(info.get("likes", 0) or 0) + devs[author]["dislikes"] += int(info.get("dislikes", 0) or 0) + return sorted(devs.items(), key=lambda x: x[1]["likes"], reverse=True) + + def aggregate_vector(self, data: list) -> list: + excluded = {u.lower() for u in self.config["excluded_authors"]} + devs = {} + for entry in data: + author = entry.get("author", "").lstrip("@") + if not author or author.lower() in excluded: + continue + if author not in devs: + devs[author] = {"likes": 0, "dislikes": 0} + devs[author]["likes"] += int(entry.get("likes", 0) or 0) + devs[author]["dislikes"] += int(entry.get("dislikes", 0) or 0) + return sorted(devs.items(), key=lambda x: x[1]["likes"], reverse=True) + + def merge_sources(self, fheta_devs: list, vector_devs: list) -> list: + merged = {} + for username, stats in fheta_devs: + merged[username.lower()] = {"name": username, "likes": stats["likes"], "dislikes": stats["dislikes"]} + for username, stats in vector_devs: + key = username.lower() + if key in merged: + merged[key]["likes"] += stats["likes"] + merged[key]["dislikes"] += stats["dislikes"] + else: + merged[key] = {"name": username, "likes": stats["likes"], "dislikes": stats["dislikes"]} + result = [(v["name"], {"likes": v["likes"], "dislikes": v["dislikes"]}) for v in merged.values()] + return sorted(result, key=lambda x: x[1]["likes"], reverse=True) + + async def fetch_sorted_devs(self) -> list: + provider = self.config["provider"] + if provider == "fheta": + data = await self.request_api(FHETA_URL) + return self.aggregate_devs(data) if data else [] + if provider == "vector": + data = await self.request_api(VECTOR_URL) + return self.aggregate_vector(data) if isinstance(data, list) else [] + # multi + fheta_data, vector_data = await asyncio.gather( + self.request_api(FHETA_URL), + self.request_api(VECTOR_URL), + ) + fheta_devs = self.aggregate_devs(fheta_data) if fheta_data else [] + vector_devs = self.aggregate_vector(vector_data) if isinstance(vector_data, list) else [] + if not fheta_devs and not vector_devs: + return [] + return self.merge_sources(fheta_devs, vector_devs) + + def extract_module_name(self, key: str) -> str: + return key.strip().split("/")[-1].removesuffix(".py") + + def format_stats(self, likes: int, dislikes: int) -> str: + mode = self.config["display_mode"] + lw = self.strings["like_singl"] if likes == 1 else self.strings["just_likes"] + if mode == "both": + return f"({likes} {lw} | {dislikes} {self.strings['just_dislikes']})" + return f"({likes} {lw})" + + def dev_entry(self, rank: int, username: str, likes: int, dislikes: int) -> str: + stats = self.format_stats(likes, dislikes) + emoji = self.config[f"rank{rank}_emoji"] if rank <= 3 else "" + safe = utils.escape_html(username) + if emoji: + return f"{rank}. @{safe} {stats} | {emoji}\n" + return f"{rank}. @{safe} {stats}\n" + + def kb_dev_page(self, sorted_devs: list, page: int) -> str: + start = page * 10 + text = self.strings["dev_header"] + for i, (username, stats) in enumerate(sorted_devs[start:start + 10]): + rank = start + i + 1 + text += self.dev_entry(rank, username, stats["likes"], stats["dislikes"]) + return text + + def nav_markup(self, page: int, total: int, on_prev, on_next, on_page) -> list: + return [ + [ + {"text": self.strings["btn_prev"], "callback": on_prev}, + {"text": f"{page + 1}/{total}", "callback": on_page}, + {"text": self.strings["btn_next"], "callback": on_next}, + ], + [{"text": self.strings["btn_close"], "action": "close"}], + ] + + def page_selector_markup(self, total: int, page_cb_factory, on_back) -> list: + buttons, row = [], [] + for p in range(total): + row.append({"text": str(p + 1), "callback": page_cb_factory(p)}) + if len(row) == 5: + buttons.append(row) + row = [] + if row: + buttons.append(row) + buttons.append([{"text": self.strings["btn_back"], "callback": on_back}]) + return buttons + + async def placeholder_devtop(self) -> str: + usernames = {u.lstrip("@").lower() for u in self.config["usernames"]} + if not usernames: + return self.strings["no_usernames"] + sorted_devs = await self.fetch_sorted_devs() + if not sorted_devs: + return self.strings["no_data"] + for rank, (username, _) in enumerate(sorted_devs, 1): + if username.lower() in usernames: + return f"{rank}" + return self.strings["devtop_not_found"] + + async def placeholder_topmod(self) -> str: + usernames = {u.lstrip("@").lower() for u in self.config["usernames"]} + if not usernames: + return self.strings["no_usernames"] + + provider = self.config["provider"] + joined_usernames = ",".join(sorted(usernames)) + + if provider in {"vector", "multi"}: + data = await self.request_api(f"{VECTOR_TOPMOD_URL}{joined_usernames}") + if isinstance(data, dict) and data.get("name") and data.get("rank"): + return f"{data['name']} ({data['rank']})" + if provider == "vector": + return self.strings["topmod_not_found"] if data else self.strings["no_data"] + + data = await self.request_api(FHETA_URL) + if not data: + return self.strings["no_data"] + all_sorted = sorted( + [(self.extract_module_name(k), v) for k, v in data.items()], + key=lambda x: int(x[1].get("likes", 0) or 0), + reverse=True, + ) + user_mods = [ + (name, val) + for name, val in all_sorted + if val.get("author", "").lstrip("@").lower() in usernames + ] + if not user_mods: + return self.strings["topmod_not_found"] + top_name = user_mods[0][0] + global_rank = next( + (i + 1 for i, (name, _) in enumerate(all_sorted) if name == top_name), + None, + ) + return ( + f"{top_name} ({global_rank})" + if global_rank + else self.strings["topmod_not_found"] + ) + + @loader.command(ru_doc="Статистика топ разработчиков") + async def devstats(self, message: Message): + """Top Developers statistics""" + await utils.answer(message, self.strings["loading"]) + sorted_devs = await self.fetch_sorted_devs() + if not sorted_devs: + return await utils.answer(message, self.strings["no_data"]) + total_pages = max(1, math.ceil(len(sorted_devs) / 10)) + state = {"page": 0} + + def markup(): + return self.nav_markup(state["page"], total_pages, on_prev, on_next, on_page) + + def render(): + return self.kb_dev_page(sorted_devs, state["page"]) + + async def on_prev(call: InlineCall): + state["page"] = max(0, state["page"] - 1) + await call.edit(render(), reply_markup=markup()) + + async def on_next(call: InlineCall): + state["page"] = min(total_pages - 1, state["page"] + 1) + await call.edit(render(), reply_markup=markup()) + + async def on_page(call: InlineCall): + await call.edit( + self.strings["select_page"], + reply_markup=self.page_selector_markup(total_pages, make_page_cb, on_back), + ) + + def make_page_cb(p): + async def go(call: InlineCall): + state["page"] = p + await call.edit(render(), reply_markup=markup()) + return go + + async def on_back(call: InlineCall): + await call.edit(render(), reply_markup=markup()) + + await utils.answer(message, render(), reply_markup=markup()) \ No newline at end of file diff --git a/SunnexGB/Heroku-Modules/ForkCircles.py b/SunnexGB/Heroku-Modules/ForkCircles.py new file mode 100644 index 0000000..88acfa2 --- /dev/null +++ b/SunnexGB/Heroku-Modules/ForkCircles.py @@ -0,0 +1,168 @@ +# requires: python-ffmpeg +# meta developer: @SunnexGB +# meta pic: https://r2.fakecrime.bio/uploads/ef6d3ed1-6378-4bc4-aaad-d2bdeeaa4bbd.jpg +# meta banner: https://r2.fakecrime.bio/uploads/ef6d3ed1-6378-4bc4-aaad-d2bdeeaa4bbd.jpg + +# Note +# This is a fork module from @KeyZenD. +# Here is a link to the original module: https://github.com/KeyZenD/modules/blob/master/Circles.py + +from .. import loader, utils +from PIL import Image, ImageDraw, ImageOps, ImageFilter +import io +from telethon.tl.types import DocumentAttributeFilename +import subprocess +import json +import os + +@loader.tds +class ForkCircles(loader.Module): + """rounds everything - reply to message""" + strings = { + "name": "ForkCircles", + "processing_image": "Processing image💬", + "processing_video": "Processing video💬", + "no_reply": "🤚|reply to image/sticker or video/gif!", + "download": "downloading💬", + "ffprobe_failed": "🤚|error`ffmpeg is installed?", + "ffmpeg_failed": "🤚|ffmpeg error`: {error}", + } + + strings_ru = { + "_cls_doc": "Округляет всё - ответом на сообщение", + "processing_image": "Обработка изображения💬", + "processing_video": "Обработка видео💬", + "no_reply": "🤚|ответьте на изображение/стикер или видео/gif!", + "download": "Скачивание💬", + "ffprobe_failed": "🤚|еррорь ffmpeg установил?", + "ffmpeg_failed": "🤚|ffmpeg еррорь: {error}", + } + + def __init__(self): + self.name = self.strings['name'] + + async def client_ready(self, client, db): + self.client = client + + @loader.sudo + async def roundcmd(self, message): + """""" + reply = None + if message.is_reply: + reply = await message.get_reply_message() + data = await check_media(reply) + if isinstance(data, bool): + await utils.answer(message, self.strings['no_reply']) + return + else: + await utils.answer(message, self.strings['no_reply']) + return + data, type = data + if type == "img": + await message.edit(self.strings['processing_image']) + img = io.BytesIO() + bytes = await message.client.download_file(data, img) + im = Image.open(img) + w, h = im.size + img = Image.new("RGBA", (w,h), (0,0,0,0)) + img.paste(im, (0, 0)) + m = min(w, h) + img = img.crop(((w-m)//2, (h-m)//2, (w+m)//2, (h+m)//2)) + w, h = img.size + mask = Image.new('L', (w, h), 0) + draw = ImageDraw.Draw(mask) + draw.ellipse((10, 10, w-10, h-10), fill=255) + mask = mask.filter(ImageFilter.GaussianBlur(2)) + img = ImageOps.fit(img, (w, h)) + img.putalpha(mask) + im = io.BytesIO() + im.name = "img.webp" + img.save(im) + im.seek(0) + await message.client.send_file(message.to_id, im, reply_to=reply) + else: + await message.edit(self.strings['processing_video']) + await message.client.download_file(data, "video.mp4") + try: + cmd = [ + 'ffprobe', '-v', 'error', '-select_streams', 'v:0', + '-show_entries', 'stream=width,height', '-of', 'json', 'video.mp4' + ] + proc = subprocess.run(cmd, capture_output=True, text=True) + if proc.returncode != 0: + return + info = json.loads(proc.stdout or '{}') + streams = info.get('streams', []) + if not streams: + return + w = int(streams[0].get('width', 0)) + h = int(streams[0].get('height', 0)) + m = min(w, h) + x = (w - m) // 2 + y = (h - m) // 2 + await message.edit(self.strings['download']) + crop_filter = f"crop={m}:{m}:{x}:{y}" + is_gif = getattr(reply, 'gif', False) or False + if is_gif: + cmd = [ + 'ffmpeg', '-y', '-i', 'video.mp4', + '-vf', crop_filter, + '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '23', + '-pix_fmt', 'yuv420p', '-an', + 'result.mp4' + ] + else: + cmd = [ + 'ffmpeg', '-y', '-i', 'video.mp4', + '-vf', crop_filter, + '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '23', + '-c:a', 'aac', '-strict', '-2', + 'result.mp4' + ] + proc = subprocess.run(cmd, capture_output=True, text=True) + if proc.returncode != 0: + err = proc.stderr or '' + lines = [l for l in err.splitlines() if l.strip()] + filtered = [] + for l in lines: + low = l.lower() + if low.startswith('ffmpeg version') or low.startswith('built with') or low.startswith('configuration:'): + continue + filtered.append(l) + if not filtered: + safe = err[:300] + else: + safe = '\n'.join(filtered[-6:]) + await utils.answer(message, self.strings['ffmpeg_failed'].format(error=safe)) + return + await message.client.send_file(message.to_id, 'result.mp4', video_note=(not is_gif), reply_to=reply) + finally: + if os.path.exists('video.mp4'): + os.remove('video.mp4') + if os.path.exists('result.mp4'): + os.remove('result.mp4') + await message.delete() + + +async def check_media(reply): + type = "img" + if reply and reply.media: + if reply.photo: + data = reply.photo + elif reply.document: + if DocumentAttributeFilename(file_name='AnimatedSticker.tgs') in reply.media.document.attributes: + return False + if reply.gif or reply.video: + type = "vid" + if reply.audio or reply.voice: + return False + data = reply.media.document + else: + return False + else: + return False + + if not data or data is None: + return False + else: + return (data, type) diff --git a/SunnexGB/Heroku-Modules/Hangman.py b/SunnexGB/Heroku-Modules/Hangman.py new file mode 100644 index 0000000..a213887 --- /dev/null +++ b/SunnexGB/Heroku-Modules/Hangman.py @@ -0,0 +1,171 @@ +# meta developer: @H_SunMods +# meta pic: https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/10.png +# meta banner: https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/10.png +# meta fhsdesc: Game, Игра, Hangman, Висилица +# крутой баннер да? + +#current version +__version__ = ("d", "i", "e") + +import random +import aiohttp +from .. import loader, utils +from herokutl.types import Message +from ..types import InlineCall + +words = "https://github.com/SunnexGB/Heroku-Modules/raw/refs/heads/main/Assets/Hangman/words.txt" + +@loader.tds +class Hangman(loader.Module): + """Висилица""" + + strings = { + "name": "Hangman", + "caption": "{word}", + "won": "{word}", + "over": "Игра окончена. Слово было: {word}", + "already": "Сосиски свои ебаные убрал от этой буквы!", + } + + HangmanLives = [ + "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/full_hp.png", + "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/1.png", + "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/2.png", + "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/3.png", + "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/4.png", + "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/5.png", + "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/6.png", + "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/7.png", + "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/8.png", + "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/9.png", + "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/10.png", + ] + + async def client_ready(self): + await self.load_words() + + async def load_words(self): + try: + async with aiohttp.ClientSession() as session: + async with session.get(words) as resp: + resp.raise_for_status() + text = await resp.text() + self.words = [ + word.strip().upper() + for word in text.splitlines() + if word.strip() + ] + except Exception: + self.words = ["СЛЕНДЕРМЕН", "КАЗИНО", "АЗАРТ"] + + def field_w_letters(self, word, guessed): + return " ".join(l if l in guessed else "_" for l in word) + + def caption(self, state): + return self.strings["caption"].format( + word=self.field_w_letters(state["word"], state["guessed"]), + ) + + def russian_latters(self, state, chat_id): + guessed = state["guessed"] + wrong = state["wrong"] + return [ + [ + {"text": "А", "callback": self.on_letter, "args": (chat_id, "А"), **({"style": "success"} if "А" in guessed else {"style": "danger"} if "А" in wrong else {})}, + {"text": "Б", "callback": self.on_letter, "args": (chat_id, "Б"), **({"style": "success"} if "Б" in guessed else {"style": "danger"} if "Б" in wrong else {})}, + {"text": "В", "callback": self.on_letter, "args": (chat_id, "В"), **({"style": "success"} if "В" in guessed else {"style": "danger"} if "В" in wrong else {})}, + {"text": "Г", "callback": self.on_letter, "args": (chat_id, "Г"), **({"style": "success"} if "Г" in guessed else {"style": "danger"} if "Г" in wrong else {})}, + {"text": "Д", "callback": self.on_letter, "args": (chat_id, "Д"), **({"style": "success"} if "Д" in guessed else {"style": "danger"} if "Д" in wrong else {})}, + {"text": "Е", "callback": self.on_letter, "args": (chat_id, "Е"), **({"style": "success"} if "Е" in guessed else {"style": "danger"} if "Е" in wrong else {})}, + {"text": "Ё", "callback": self.on_letter, "args": (chat_id, "Ё"), **({"style": "success"} if "Ё" in guessed else {"style": "danger"} if "Ё" in wrong else {})}, + {"text": "Ж", "callback": self.on_letter, "args": (chat_id, "Ж"), **({"style": "success"} if "Ж" in guessed else {"style": "danger"} if "Ж" in wrong else {})}, + ], + [ + {"text": "З", "callback": self.on_letter, "args": (chat_id, "З"), **({"style": "success"} if "З" in guessed else {"style": "danger"} if "З" in wrong else {})}, + {"text": "И", "callback": self.on_letter, "args": (chat_id, "И"), **({"style": "success"} if "И" in guessed else {"style": "danger"} if "И" in wrong else {})}, + {"text": "Й", "callback": self.on_letter, "args": (chat_id, "Й"), **({"style": "success"} if "Й" in guessed else {"style": "danger"} if "Й" in wrong else {})}, + {"text": "К", "callback": self.on_letter, "args": (chat_id, "К"), **({"style": "success"} if "К" in guessed else {"style": "danger"} if "К" in wrong else {})}, + {"text": "Л", "callback": self.on_letter, "args": (chat_id, "Л"), **({"style": "success"} if "Л" in guessed else {"style": "danger"} if "Л" in wrong else {})}, + {"text": "М", "callback": self.on_letter, "args": (chat_id, "М"), **({"style": "success"} if "М" in guessed else {"style": "danger"} if "М" in wrong else {})}, + {"text": "Н", "callback": self.on_letter, "args": (chat_id, "Н"), **({"style": "success"} if "Н" in guessed else {"style": "danger"} if "Н" in wrong else {})}, + {"text": "О", "callback": self.on_letter, "args": (chat_id, "О"), **({"style": "success"} if "О" in guessed else {"style": "danger"} if "О" in wrong else {})}, + ], + [ + {"text": "П", "callback": self.on_letter, "args": (chat_id, "П"), **({"style": "success"} if "П" in guessed else {"style": "danger"} if "П" in wrong else {})}, + {"text": "Р", "callback": self.on_letter, "args": (chat_id, "Р"), **({"style": "success"} if "Р" in guessed else {"style": "danger"} if "Р" in wrong else {})}, + {"text": "С", "callback": self.on_letter, "args": (chat_id, "С"), **({"style": "success"} if "С" in guessed else {"style": "danger"} if "С" in wrong else {})}, + {"text": "Т", "callback": self.on_letter, "args": (chat_id, "Т"), **({"style": "success"} if "Т" in guessed else {"style": "danger"} if "Т" in wrong else {})}, + {"text": "У", "callback": self.on_letter, "args": (chat_id, "У"), **({"style": "success"} if "У" in guessed else {"style": "danger"} if "У" in wrong else {})}, + {"text": "Ф", "callback": self.on_letter, "args": (chat_id, "Ф"), **({"style": "success"} if "Ф" in guessed else {"style": "danger"} if "Ф" in wrong else {})}, + {"text": "Х", "callback": self.on_letter, "args": (chat_id, "Х"), **({"style": "success"} if "Х" in guessed else {"style": "danger"} if "Х" in wrong else {})}, + {"text": "Ц", "callback": self.on_letter, "args": (chat_id, "Ц"), **({"style": "success"} if "Ц" in guessed else {"style": "danger"} if "Ц" in wrong else {})}, + ], + [ + {"text": "Ч", "callback": self.on_letter, "args": (chat_id, "Ч"), **({"style": "success"} if "Ч" in guessed else {"style": "danger"} if "Ч" in wrong else {})}, + {"text": "Ш", "callback": self.on_letter, "args": (chat_id, "Ш"), **({"style": "success"} if "Ш" in guessed else {"style": "danger"} if "Ш" in wrong else {})}, + {"text": "Щ", "callback": self.on_letter, "args": (chat_id, "Щ"), **({"style": "success"} if "Щ" in guessed else {"style": "danger"} if "Щ" in wrong else {})}, + {"text": "Ъ", "callback": self.on_letter, "args": (chat_id, "Ъ"), **({"style": "success"} if "Ъ" in guessed else {"style": "danger"} if "Ъ" in wrong else {})}, + {"text": "Ь", "callback": self.on_letter, "args": (chat_id, "Ь"), **({"style": "success"} if "Ь" in guessed else {"style": "danger"} if "Ь" in wrong else {})}, + {"text": "Ы", "callback": self.on_letter, "args": (chat_id, "Ы"), **({"style": "success"} if "Ы" in guessed else {"style": "danger"} if "Ы" in wrong else {})}, + {"text": "Э", "callback": self.on_letter, "args": (chat_id, "Э"), **({"style": "success"} if "Э" in guessed else {"style": "danger"} if "Э" in wrong else {})}, + {"text": "Ю", "callback": self.on_letter, "args": (chat_id, "Ю"), **({"style": "success"} if "Ю" in guessed else {"style": "danger"} if "Ю" in wrong else {})}, + ], + [ + {"text": "Я", "callback": self.on_letter, "args": (chat_id, "Я"), **({"style": "success"} if "Я" in guessed else {"style": "danger"} if "Я" in wrong else {})}, + ], + ] + + async def on_letter(self, call: InlineCall, chat_id: int, letter: str): + letter = letter.upper() + state = self.get(f"pidor_{chat_id}", None) + + if letter in state["guessed"] or letter in state["wrong"]: + await call.answer(self.strings["already"], show_alert=True) + return + + if letter in state["word"]: + state["guessed"].append(letter) + self.set(f"pidor_{chat_id}", state) + + if "_" not in self.field_w_letters(state["word"], state["guessed"]): + self.set(f"pidor_{chat_id}", None) + await call.edit(self.strings["won"].format(word=state["word"])) + return + + await call.edit(self.caption(state), reply_markup=self.russian_latters(state, chat_id)) + + else: + state["wrong"].append(letter) + self.set(f"pidor_{chat_id}", state) + + wrong_count = len(state["wrong"]) + stage = min(wrong_count, len(self.HangmanLives) - 1) + + if wrong_count >= 10: + self.set(f"pidor_{chat_id}", None) + await call.edit( + self.strings["over"].format(word=state["word"]), + photo=self.HangmanLives[stage], + ) + return + + await call.edit( + self.caption(state), + reply_markup=self.russian_latters(state, chat_id), + photo=self.HangmanLives[stage], + ) + + @loader.command(ru_doc="(.oleg) Начать висилицу", alias="oleg") + async def hangman(self, message: Message): + """(.oleg) Start hangman game""" + chat_id = message.chat_id + word = random.choice(self.words) + state = {"word": word, "guessed": [], "wrong": []} + self.set(f"pidor_{chat_id}", state) + + await self.inline.form( + message=message, + text=self.caption(state), + reply_markup=self.russian_latters(state, chat_id), + photo=self.HangmanLives[0], + ) \ No newline at end of file diff --git a/SunnexGB/Heroku-Modules/HerokuTime.py b/SunnexGB/Heroku-Modules/HerokuTime.py new file mode 100644 index 0000000..197bbd3 --- /dev/null +++ b/SunnexGB/Heroku-Modules/HerokuTime.py @@ -0,0 +1,50 @@ +# meta developer: @SunnexGB +# meta pic: https://r2.fakecrime.bio/uploads/e19c2179-d2e9-4206-b783-25d6c0eb72eb.jpg +# meta banner: https://r2.fakecrime.bio/uploads/e19c2179-d2e9-4206-b783-25d6c0eb72eb.jpg +# meta fhsdesc: Плейсхолдер, placeholder, Time, Время, Статистика, Stats +# крутой баннер да? +#current version +__version__ = (1, 0, 0) + +import time +from .. import loader, utils + +@loader.tds +class HerokuTime(loader.Module): + """shows how much heroku you use in total (since installing placeholder)""" + + strings = { + "name": "HerokuTime", + "sec": "sec", + "min": "min", + "hour": "h", + } + + strings_ru = { + "cls_doc": "показывает сколько вы используете всего хероку(с момента установки плейсхолдера)", + "sec": "сек", + "min": "мин", + "hour": "ч", + } + + async def client_ready(self): + if not self.get("start_time"): + self.set("start_time", int(time.time())) + utils.register_placeholder("alltime", self.get_uptime, "show heroku time usage") + + def format_time(self, seconds: int) -> str: + if seconds < 60: + return f"{seconds} {self.strings('sec')}" + minutes = seconds // 60 + if minutes < 60: + return f"{minutes} {self.strings('min')} {seconds % 60} {self.strings('sec')}" + hours = minutes // 60 + return f"{hours} {self.strings('hour')} {minutes % 60} {self.strings('min')} {seconds % 60} {self.strings('sec')}" + + async def get_uptime(self): + start_time = self.get("start_time") + if not start_time: + return f"0 {self.strings('sec')}" + now = int(time.time()) + uptime = now - start_time + return self.format_time(uptime) \ No newline at end of file diff --git a/SunnexGB/Heroku-Modules/Mikuru.py b/SunnexGB/Heroku-Modules/Mikuru.py new file mode 100644 index 0000000..8e9ae19 --- /dev/null +++ b/SunnexGB/Heroku-Modules/Mikuru.py @@ -0,0 +1,138 @@ +# requires: aiohttp +# meta banner: https://r2.fakecrime.bio/uploads/c389e9b5-9ef1-495d-a37a-e993ef819b4a.mp4 +# meta developer: @SunnexGB +__version__ = (1, 0, 0) + +from .. import loader, utils +import re +import aiohttp + +@loader.tds +class Mikuru(loader.Module): + """Censors words with phrase Mikuru Asahina""" + + strings = { + "name": "Mikuru", + "mikuru": "🤤 | I cant say this is for u, b-because this is c-classified information", + "adult_mikuru": "😥 | Y-you already k-know s-so much...", + "ignored": "😏 | Y-you can talk about c-classified information in this chat", + "unignored": "☺️ | You cant speak in this chat b-because c-classified information", + "classified_information": "classified information" + } + + strings_ru = { + "_cls_doc": "Цензурует слова фразой Микуру Асахины", + "mikuru": "🤤 | Я не могу сказать тебе это, п-п-потому что это с-секретные сведения", + "adult_mikuru": "😥 | Т-ты и так м-много з-знаешь... ", + "ignored": "😏 | Т-ты можешь говорить о с-секретных сведениях в этом чате", + "unignored": "☺️ | Ты не можешь говорить в этом чате п-потому что с-секретные сведения", + "classified_information": "секретные сведения" + } + + def __init__(self): + self.config = loader.ModuleConfig( + loader.ConfigValue( + "Ignored_chats", + [-1002410964167, + -1002341345589, + -1001697279580, + -1001554874075, + -1001984640085], + "Ignored chats", + validator=loader.validators.Series() + ), + ) + self.bad_words = None + + async def client_ready(self, client, db): + self.db = db + await self.load_words() + + async def load_words(self): + cultural_words = [ + "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/refs/heads/main/Assets/Mikuru/cultural_words_ru.txt", + "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/49f6883d03d1d2c15c82bad55ee4d31f708870ed/Assets/Mikuru/cultural_words_en.txt" + ] + + words = set() + + async with aiohttp.ClientSession() as session: + for url in cultural_words: + try: + async with session.get(url) as resp: + if resp.status != 200: + continue + text = await resp.text() + for line in text.splitlines(): + w = line.strip().lower() + if w: + words.add(w) + except Exception: + pass + + if words: + self.bad_words = re.compile( + r"\b(" + "|".join(map(re.escape, words)) + r")\b", + re.IGNORECASE + ) + else: + self.bad_words = None + + @loader.command(ru_doc="- Начать цензурирование") + async def mikuru(self, message): + """- lets go censoring""" + state = self.db.get(self.name, "mikuru_state", False) + if state: + self.db.set(self.name, "mikuru_state", False) + await utils.answer(message, self.strings("adult_mikuru")) + else: + self.db.set(self.name, "mikuru_state", True) + await utils.answer(message, self.strings("mikuru")) + + @loader.command(ru_doc="- Добавить в список игнорируемых чатов(не будет работать в этих чатах) ") + async def ignore(self, message): + """- Add to list ignored chats(will not work in these chats) """ + args = utils.get_args_raw(message) + if not args: + target = str(utils.get_chat_id(message)) + else: + target = args.strip() + ignored = list(self.config["Ignored_chats"]) + if target in ignored: + ignored.remove(target) + self.config["Ignored_chats"] = ignored + await utils.answer(message, self.strings("unignored")) + else: + ignored.append(target) + self.config["Ignored_chats"] = ignored + await utils.answer(message, self.strings("ignored")) + + async def watcher(self, message): + if not self.db.get(self.name, "mikuru_state", False): + return + if not message.text: + return + if not message.out: + return + + chat_id = str(utils.get_chat_id(message)) + user_id = str(getattr(message.sender_id, "id", message.sender_id)) + + if chat_id in self.config["Ignored_chats"] or user_id in self.config["Ignored_chats"]: + return + if not self.bad_words: + return + + if self.bad_words.search(message.text): + text = self.bad_words.sub( + self.strings("classified_information"), + message.text + ) + try: + await message.edit(text) + except Exception: + try: + await message.delete() + await message.respond(text) + except Exception: + pass \ No newline at end of file diff --git a/SunnexGB/Heroku-Modules/NoChess.py b/SunnexGB/Heroku-Modules/NoChess.py new file mode 100644 index 0000000..5a6ffc2 --- /dev/null +++ b/SunnexGB/Heroku-Modules/NoChess.py @@ -0,0 +1,487 @@ +# requires: aiohttp pyngrok +# meta developer: @H_SunMods +# meta banner: https://r2.fakecrime.bio/uploads/965a3206-4609-4dff-beb0-6831f8b90e12.jpg +# current ver +__version__ = (0, 1, 0) + +import json +import socket +import asyncio +import secrets +import logging +from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit +from aiohttp import ClientSession, ClientTimeout, web +from herokutl.types import Message +from pyngrok import conf, ngrok +from .. import loader, utils +from ..inline.types import InlineCall + +logging.getLogger("pyngrok").setLevel(logging.WARNING) +logging.getLogger("pyngrok.process").setLevel(logging.WARNING) +logging.getLogger("pyngrok.process.ngrok").setLevel(logging.WARNING) + +html_raw = "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/refs/heads/main/Assets/NoChess/raw_assets/index.html" +css_raw = "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/refs/heads/main/Assets/NoChess/raw_assets/style.css" +js_raw = "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/refs/heads/main/Assets/NoChess/raw_assets/javascript.js" +asset_root_raw = "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess" +botfather_photo_url = "https://r2.fakecrime.bio/uploads/d3e16245-15a2-43f1-b176-493b4d9f1f21.jpg" + +@loader.tds +class NoChess(loader.Module): + """NoChess - web module that allows u to launch a web page either as a functional HTML page or as a Telegram Mini-App. This is an add-on for Chess module by @nullmod""" + + # я пытался кароче сделать тут перевод делая реплейсы в зависимости от стрингов,но это не работает,поэтому да + strings = { + "name": "NoChess", + "starting": "( ノ・ェ・ )ノ Starting NoChess...", + "online": "(*˘︶˘*) NoChess is running", + "already_running": "ʕᵕᴥᵕʔ NoChess is already running", + "stopped": "・゚・(。>д<。)・゚・ NoChess stopped", + "not_running": "(✿╹◡╹) NoChess is not running", + "ngrok_missing": "Set a ngrok_token", + "ngrok_error": "Ngrok start error: {}", + "asset_read_error": "Failed to load web assets: {}", + "open_button": "Open mini-app", + "stop_button": "Stop", + "about_text": "Important read:\nSometimes the server won't lift cause there's enough processes running, for example on HikkaHost, for this I just rebooted the server\nNext is that cma setups the app by a template and it's rly crooked, so you'll have to set some web app config settings yourself\nAnd also:\n 1. First launch will start straight with a site link, not as a web app\n 2. Use nochess, and then cma to setup the web app\n 3. After that restart the process by typing nochess -kill and nochess again\nYeah it's hacky as hell, but I was so over doing stuff that I started dumping some routine like working with files on ai, which I didn't like so I decided to quick-release the module before it's too late\nWell and maybe soon I'll make an update, right now it's some pre-alpha version, that's why the version name is like this, later I'll change it to 1.0.0, if people actually dig the module as an idea", + "cma_start": "( ノ・ェ・ )ノ Creating mini app in BotFather...", + "cma_need_url": "Set mini app web URL first or run .nochess to get it.", + "cma_done": "(*˘︶˘*) Done.", + "cma_error": "Error: {}", + "RuntimeError": "inline bot username not found", + "not_supported_platform": "(┬┬_┬┬) Unfortunately, it is impossible to install this module on this platform.\n\n(〜^∇^)〜 This is not an error, please do not contact support." + } + + strings_ru = { + "_cls_doc": "NoChess - Веб модуль который позволяет запускать веб-пейдж,как HTML страницу с функционалом,так же в виде Telegram Mini-App. Является дополнением к модулю Chess от @nullmod", + "starting": "( ノ・ェ・ )ノ Запуск NoChess...", + "online": "(*˘︶˘*) NoChess запущен", + "already_running": "ʕᵕᴥᵕʔ NoChess уже запущен", + "stopped": "・゚・(。>д<。)・゚・ NoChess остановлен", + "not_running": "(✿╹◡╹) NoChess не запущен", + "ngrok_missing": "Укажи ngrok_token", + "ngrok_error": "Ошибка запуска ngrok: {}", + "asset_read_error": "Не удалось загрузить веб-ассеты: {}", + "open_button": "Открыть мини-приложение", + "stop_button": "Остановить", + "about_text": "Важно к прочтению:\nИногда сервер не может подниматься из за того что запущено достаточно процессов, например на HikkaHost,для этого я просто перезагружал сервер.\nДалее это то что cma сетапает приложение по шаблону и оч криво, поэтому вам придется выставлять некоторые настройки конфигурации веб приложения самим.\nА еще:\n 1. Первый запуск будет запускаться сразу ссылкой на сайт, а не как веб приложение.\n 2. Используйте nochess, а потом cma чтобы настроить веб приложение.\n 3. После чего перезапустите процесс написав nochess -kill и повторно nochess.\nДа это костыли, но мне уже настолько было в падлу что то делать что я уже стал спихивать рутину по типу работы с файлами на ии, что мне не понравилось и я решил быстро релизать модуль пока не стало поздно.\nНу и может быть в скором времени я уже сделаю апдейт, на данный момент это какая то пре-альфа версия, поэтому и название версии такое, в дальнейшем изменю на 1.0.0, если модуль вообще понравиться людям как идея.", + "cma_start": "( ノ・ェ・ )ノ Создаю эпку через BotFather...", + "cma_need_url": "Сначала укажи URL мини-эпки или запусти .nochess, чтобы получить его", + "cma_done": "(*˘︶˘*) Готово", + "cma_error": "Ошибка: {}", + "RuntimeError": "юз инлайн бота не найден", + "not_supported_platform": "(┬┬_┬┬) К сожалению, на эту платформу невозможно установить этот модуль.\n\n(〜^∇^)〜 Это не ошибка, пожалуйста, не обращайтесь в поддержку." + } + + async def client_ready(self): + platform = utils.get_named_platform() + if platform in ("HikkaHost"): + raise loader.LoadError(self.strings("not_supported_platform")) + + def __init__(self): + self.config = loader.ModuleConfig( + loader.ConfigValue( + "ngrok_token", + None, + "Token from ngrok.com | Токен полученый на ngrok.com", + validator=loader.validators.Hidden(), + ), + loader.ConfigValue( + "mini_app_url", + None, + "Mini app direct url | Директ ссылка на ваше мини приложение", + validator=loader.validators.String(), + ), + loader.ConfigValue( + "block_light", + "#D8E3E7", + "Light board block color | Цвет светлых полей на доске", + validator=loader.validators.String() + ), + loader.ConfigValue("block_dark", + "#7699AF", + "Dark board block color | Цвет тёмных полей на доске", + validator=loader.validators.String() + ), + loader.ConfigValue( + "select_block", + "#FF5A5A", + "Selected block color | Цвет для выделения полей на доске", + validator=loader.validators.String() + ), + loader.ConfigValue( + "move_pieces_color", + "#58B4FF", + "Move highlight color | Цвет подсвечиваниях перехода на другую позицию", + validator=loader.validators.String() + ), + loader.ConfigValue( + "result_win", + "#00BE16", + "Winner color | Блок цвета победителя", + validator=loader.validators.String() + ), + loader.ConfigValue( + "result_lose", + "#BE0000", + "Loser color | Блок цвета проигравшего", + validator=loader.validators.String() + ), + loader.ConfigValue( + "result_draw", + "#434343", + "Draw color | Блок цвета при ничьей", + validator=loader.validators.String() + ), + loader.ConfigValue( + "arrow_color", + "#BD3667", + "Arrow color | Цвет стрелки", + validator=loader.validators.String() + ), + ) + + self.runner = None + self.tunnel_url = None + self.access_token = None + self.games_cache = [] + self.games_dump = "" + + def theme_config_dict(self): + return { + "block_light": self.config["block_light"], + "block_dark": self.config["block_dark"], + "select_block": self.config["select_block"], + "move_pieces_color": self.config["move_pieces_color"], + "result_win": self.config["result_win"], + "result_lose": self.config["result_lose"], + "result_draw": self.config["result_draw"], + "arrow_color": self.config["arrow_color"], + } + + async def refresh_games_cache(self): + chess = self.lookup("chess") + if not chess or not getattr(chess, "games", None): + self.games_cache = [] + self.games_dump = "" + return + + chunks = [] + items = list(chess.games.items()) + + def sort_key(item): + key = str(item[0]) + return (0, int(key)) if key.isdigit() else (1, key) + + for _, game in sorted(items, key=sort_key, reverse=True): + node = None + + if isinstance(game, dict): + game_obj = game.get("game", {}) + if isinstance(game_obj, dict): + node = game_obj.get("root_node") or game_obj.get("node") + if node is None: + node = game.get("root_node") or game.get("node") + + if node is None and hasattr(game, "game"): + game_obj = getattr(game, "game", None) + if isinstance(game_obj, dict): + node = game_obj.get("root_node") or game_obj.get("node") + + if node is None and hasattr(game, "root_node"): + node = getattr(game, "root_node", None) + + if node is None and hasattr(game, "node"): + node = getattr(game, "node", None) + + if node: + chunks.append(str(node).strip()) + + self.games_cache = [x for x in chunks if x] + self.games_dump = "\n\n".join(self.games_cache) + + async def get_me_json(self): + me = await self.client.get_me() + fallback_photo = "https://i.pinimg.com/736x/6e/0a/0c/6e0a0cf688b30ba9de81b81bb32e49f9.jpg" + full_name = (getattr(me, "first_name", "") or "") + ( + (" " + getattr(me, "last_name", "")) if getattr(me, "last_name", None) else "" + ) + return { + "id": getattr(me, "id", None), + "username": getattr(me, "username", None), + "first_name": getattr(me, "first_name", None), + "last_name": getattr(me, "last_name", None), + "name": full_name.strip() or str(getattr(me, "id", "Unknown")), + "photo": fallback_photo, + "enemy_photo": fallback_photo, + } + + def check_access(self, request): + token = request.query.get("token") or request.cookies.get("nochess_token") + return bool(self.access_token and token == self.access_token) + + def ensure_access_token(self): + if self.access_token: + return self.access_token + self.access_token = self.get("access_token") + if not self.access_token: + self.access_token = secrets.token_urlsafe(32) + self.set("access_token", self.access_token) + return self.access_token + + async def read_remote_asset(self, url): + timeout = ClientTimeout(total=15) + async with ClientSession(timeout=timeout) as session: + async with session.get(url) as response: + if response.status != 200: + raise RuntimeError(f"HTTP {response.status}: {url}") + return await response.text() + + async def load_web_assets(self): + html = await self.read_remote_asset(html_raw) + css = await self.read_remote_asset(css_raw) + js = await self.read_remote_asset(js_raw) + return html, css, js + + def localication_script(self): + return ( + "" + ) + + def inject_runtime_config(self, html, css, js): + asset_root = asset_root_raw.rstrip("/") + if asset_root: + css = css.replace("url('bg.png')", f"url('{asset_root}/other/bg.png')") + theme_json = json.dumps(self.theme_config_dict(), ensure_ascii=False) + bootstrap = ( + "" + ) + html = html.replace('', f"") + html = html.replace('', bootstrap + f"") + return html + + async def handle_home(self, request): + try: + html, css, js = await self.load_web_assets() + except Exception as error: + return web.Response( + text=self.strings["asset_read_error"].format(utils.escape_html(str(error))), + status=500, + ) + html = self.inject_runtime_config(html, css, js) + html = html.replace("", self.localication_script() + "") + response = web.Response(text=html, content_type="text/html") + response.set_cookie( + "nochess_token", + self.access_token, + max_age=86400, + httponly=True, + samesite="Lax", + ) + return response + + async def handle_games(self, request): + if not self.check_access(request): + return web.json_response({"error": "Unauthorized"}, status=401) + if not self.games_cache: + await self.refresh_games_cache() + return web.json_response({"games_dump": self.games_dump, "games": list(self.games_cache)}) + + async def handle_me(self, request): + if not self.check_access(request): + return web.json_response({"error": "Unauthorized"}, status=401) + return web.json_response(await self.get_me_json()) + + async def stop_server(self): + was_running = bool(self.runner) + try: + ngrok.kill() + except Exception: + pass + if self.runner: + await self.runner.cleanup() + self.runner = None + self.tunnel_url = None + return was_running + + async def send_form(self, message, url): + await self.inline.form( + self.strings["online"], + message=message, + reply_markup=[ + [{"text": self.strings["open_button"], "url": url}], + [{"text": self.strings["stop_button"], "callback": self.stop_callback}], + ], + ) + + async def stop_callback(self, call: InlineCall): + was_running = await self.stop_server() + await call.answer( + self.strings["stopped"] if was_running else self.strings["not_running"], + show_alert=False, + ) + try: + await call.delete() + except Exception: + try: + await call.edit(self.strings["stopped"] if was_running else self.strings["not_running"]) + except Exception: + pass + + @loader.command(ru_doc="[-kill] Вызываь веб интерфейс для просмотра партии") + async def nochess(self, message: Message): + """[-kill] Call web interface to view chess game""" + try: + return await self.nochess_args(message) + except Exception as error: + await self.stop_server() + return await utils.answer( + message, + self.strings["ngrok_error"].format(utils.escape_html(str(error))), + ) + + async def nochess_args(self, message: Message): + args = (utils.get_args_raw(message) or "").strip().lower() + if args == "-kill": + was_running = await self.stop_server() + return await utils.answer(message, self.strings["stopped"] if was_running else self.strings["not_running"]) + mini_url = (self.config["mini_app_url"] or "").strip().rstrip("/") + is_tg_direct = mini_url.startswith("https://t.me/") + if self.runner: + if is_tg_direct: + access = mini_url + else: + base = (self.tunnel_url or "").rstrip("/") + access = f"{base}/?token={self.access_token}" if base and self.access_token else base + await utils.answer(message, self.strings["already_running"]) + if access: + await self.send_form(message, access) + return + if not self.config["ngrok_token"] and (not mini_url or is_tg_direct): + return await utils.answer(message, self.strings["ngrok_missing"]) + await self.refresh_games_cache() + await utils.answer(message, self.strings["starting"]) + self.ensure_access_token() + sock = socket.socket() + sock.bind(("", 0)) + port = sock.getsockname()[1] + sock.close() + app = web.Application() + app.router.add_get("/", self.handle_home) + app.router.add_get("/api/games", self.handle_games) + app.router.add_get("/api/me", self.handle_me) + self.runner = web.AppRunner(app) + await self.runner.setup() + await web.TCPSite(self.runner, "127.0.0.1", port).start() + try: + if self.config["ngrok_token"]: + conf.get_default().auth_token = self.config["ngrok_token"] + tunnel = ngrok.connect(port) + self.tunnel_url = tunnel.public_url.rstrip("/") + else: + self.tunnel_url = mini_url + except Exception as error: + await self.stop_server() + return await utils.answer( + message, + self.strings["ngrok_error"].format(utils.escape_html(str(error))), + ) + if is_tg_direct: + access_url = mini_url + else: + base = (self.tunnel_url or "").rstrip("/") + access_url = f"{base}/?token={self.access_token}" if base and self.access_token else base + await self.send_form(message, access_url) + + @loader.command(ru_doc="Создает и настраивает эпку") + async def cma(self, message: Message): + """Create and setup mini-app""" + raw_args = (utils.get_args_raw(message) or "").strip() + parts = raw_args.split() + web_url = "" + short_name = "NoChess" + if parts: + web_url = parts[0] + if len(parts) > 1: + short_name = parts[1] + if not web_url: + candidate = (self.tunnel_url or "").strip() + if not candidate: + candidate = (self.config["mini_app_url"] or "").strip() + if candidate.startswith("https://t.me/"): + candidate = "" + web_url = candidate + if not web_url: + return await utils.answer(message, self.strings["cma_need_url"]) + self.ensure_access_token() + if web_url.startswith("http") and "t.me/" not in web_url: + parsed = urlsplit(web_url) + query = dict(parse_qsl(parsed.query, keep_blank_values=True)) + query["token"] = self.access_token + web_url = urlunsplit((parsed.scheme, parsed.netloc, parsed.path, urlencode(query), parsed.fragment)) + await utils.answer(message, self.strings["cma_start"]) + try: + bot_username = (await self.inline.bot.get_me()).username + bot_username = (bot_username or "").strip().lstrip("@") + if not bot_username: + raise RuntimeError(self.strings["RuntimeError"]) + await self.client.send_message("@BotFather", "/cancel") + await asyncio.sleep(0.9) + + async with self.client.conversation("@BotFather", timeout=120) as conv: + await conv.send_message("/newapp") + await conv.get_response() + await asyncio.sleep(0.8) + await conv.send_message(f"@{bot_username}") + await conv.get_response() + await asyncio.sleep(0.8) + await conv.send_message("NoChessModule") + await conv.get_response() + await asyncio.sleep(0.8) + await conv.send_message("NoChess") + await conv.get_response() + await asyncio.sleep(0.8) + await conv.send_file(botfather_photo_url) + await conv.get_response() + await asyncio.sleep(0.8) + await conv.send_message("/empty") + await conv.get_response() + await asyncio.sleep(0.8) + await conv.send_message(web_url) + await conv.get_response() + await asyncio.sleep(0.8) + await conv.send_message(short_name) + await conv.get_response() + + direct_link = f"https://t.me/{bot_username}/{short_name}" + module_ref = None + try: + module_ref = self.lookup("NoChess") + except Exception: + module_ref = None + if module_ref: + module_ref.config["mini_app_url"] = direct_link + else: + self.config["mini_app_url"] = direct_link + await utils.answer(message, self.strings["cma_done"]) + except Exception as error: + await utils.answer(message, self.strings["cma_error"].format(utils.escape_html(str(error)))) + + @loader.command(ru_doc="ВАЖНО К ПРОЧТЕНИЮ") + async def about(self, message: Message): + """IMPORTANT READING""" + await utils.answer(message, self.strings["about_text"]) + async def on_unload(self): + await self.stop_server() diff --git a/SunnexGB/Heroku-Modules/Shazamio.py b/SunnexGB/Heroku-Modules/Shazamio.py new file mode 100644 index 0000000..d8fe5cc --- /dev/null +++ b/SunnexGB/Heroku-Modules/Shazamio.py @@ -0,0 +1,124 @@ +# meta pic: https://r2.fakecrime.bio/uploads/54b3c78d-38cb-4970-b925-18b7ec2b268d.jpg +# meta banner: https://r2.fakecrime.bio/uploads/54b3c78d-38cb-4970-b925-18b7ec2b268d.jpg +# requires: https://files.pythonhosted.org/packages/2f/66/31ecae67c373421db10f250a83d80653d6908f7d95080c46816102bd1fda/shazamio-0.8.1.tar.gz https://files.pythonhosted.org/packages/dd/4d/7ecffb341d646e016be76e36f5a42cb32f409c9ca21a57b68f067fad3fc7/python_ffmpeg-2.0.12.tar.gz +# meta developer: @SunnexGB +#current version +__version__ = (1, 0, 0) + +from .. import loader, utils +import os +import asyncio +from shazamio import Shazam + +@loader.tds +class Shazamio(loader.Module): + """Music recognition module""" + + strings = { + "name": "Shazamio", + "processing": "Processing 🫥", + "shazaming": "🔈| Shazaming...", + "no_reply": "🚫| Reply to a video message.", + "no_video": "🚫| Reply must be to a video message.", + "ffmpeg_error": "🚫| Failed to read audio. Make sure ffmpeg is installed.", + "not_found": "✖️| Sorry, could not recognize the song.", + "result": "🔈| Song recognized:\n\n" + "🔈Artist:{artist}\n" + "🚮Title:{title}", + "result_url": "〰️Song recognized:\n\n" + "🚮Artist:{artist}\n" + "🔈Title:{title}\n\n" + "🔗Listen on Shazam", + "shazam_history": "〰️| Your last 10 recognised songs", # i put it off for later and then forgot i wanted to implement it + "no_history": "〰️| What do you want to see here?", # i put it off for later and then forgot i wanted to implement it + } + + strings_ru = { + "name": "Shazamio", + "_cls_doc": "Модуль для распознования музыки", + "processing": "Обработка 🫥", + "shazaming": "🔈| Шазамлю...", + "no_reply": "🚫| Ответьте на сообщение с видео.", + "no_video": "🚫| Ответ должен быть на видео", + "ffmpeg_error": "🚫| Неудачное чтение аудио. Убедитесь что ffmpeg установлен.Инструкция по установке", + "not_found": "✖️| Простите, песня не была найдена.", + "result": "🔈| Песня найдена:\n\n" + "🔈Исполнитель:{artist}\n" + "🚮Название:{title}", + "result_url": "〰️Песня найдена:\n\n" + "🚮Исполнитель:{artist}\n" + "🔈Название:{title}\n\n" + "🔗Слушайте на Shazam", + "shazam_history": "〰️| Твои 10 последних распознаных треков", # на потом,я забыл что я хотел это реализовать + "no_history": "〰️| Ну и что ты тут хотел увидеть?", # на потом,я забыл что я хотел это реализовать + } + + def __init__(self): + self.config = loader.ModuleConfig( + "ffmpeg_path", + "ffmpeg", + "Path to ffmpeg executable", + ) + + @loader.command(ru_doc="Распознать музыку (Ответом на видео)") + async def shazam(self, message): + """Recognize music (Reply in video)""" + reply = await message.get_reply_message() + if not reply: + await utils.answer(message, self.strings["no_reply"]) + return + + if not reply.video: + await utils.answer(message, self.strings["no_video"]) + return + + await utils.answer(message, self.strings["processing"]) + downloaded_path = await message.client.download_media(reply.video) + video_path = os.path.abspath(downloaded_path) + base, _ = os.path.splitext(video_path) + audio_path = f"{base}.mp3" + + try: + cmd = ( + f'{self.config["ffmpeg_path"]} -i "{video_path}" ' + f'-y -vn -ab 128k -ar 44100 -f mp3 "{audio_path}"' + ) + proc = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + await proc.communicate() + + if not os.path.exists(audio_path): + await utils.answer(message, self.strings["ffmpeg_error"]) + return + + await utils.answer(message, self.strings["shazaming"]) + shazam = Shazam() + result = await shazam.recognize(audio_path) + + track = result.get("track") + if track: + title = track.get("title", "Unknown Title") + artist = track.get("subtitle", "Unknown Artist") + url = track.get("url") + + if url: + text = self.strings["result_url"].format( + title=title, artist=artist, url=url + ) + else: + text = self.strings["result"].format( + title=title, artist=artist + ) + + await utils.answer(message, text) + else: + await utils.answer(message, self.strings["not_found"]) + + finally: + if os.path.exists(video_path): + os.remove(video_path) + if os.path.exists(audio_path): + os.remove(audio_path) \ No newline at end of file diff --git a/SunnexGB/Heroku-Modules/SpotiSaver.py b/SunnexGB/Heroku-Modules/SpotiSaver.py new file mode 100644 index 0000000..f0ae61d --- /dev/null +++ b/SunnexGB/Heroku-Modules/SpotiSaver.py @@ -0,0 +1,191 @@ +# Спасибо: snfsx, кезу, а так же Gemini +# requires: httpx +# meta developer: @SunnexGB +# meta repo: https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/refs/heads/main/spotisaver.py +# meta pic: https://r2.fakecrime.bio/uploads/ddf03169-09fe-4eb1-8eea-bad1a4cc4ada.jpg +# meta banner: https://r2.fakecrime.bio/uploads/ddf03169-09fe-4eb1-8eea-bad1a4cc4ada.jpg +# meta fhsdesc: Spotify, downloader, music, музыка, спотифай,скачать музыку +# это не должно было быть в релизе,но ладно я потом пофикшу все и вся в говнокоде. +__version__ = (1, 1, 1) + +import asyncio +import httpx +import os +import re +import logging +from .. import loader, utils +from herokutl.types import Message + +logger = logging.getLogger(__name__) + +headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36", + "Accept": "application/json", + "Content-Type": "application/json", + "Origin": "https://spotmate.online", + "Referer": "https://spotmate.online/en1", +} + +@loader.tds +class SpotiSaver(loader.Module): + """Downloading music from Spotify""" + strings = { + "name": "SpotiSaver", + # "args": " link to song is not specified", + "downloading": "📥 Downloading: {}", + "error": " Error, see logs!", + "done": "✔️ Done!", + "no_spotifymod": "💢 SpotifyMod not found.", + "no_spotify": "😅 Nothing is playing on Spotify.", + "nf_id": " ID key not found!", + "nf_track": " Song not found.", + "timeout": " timeout! Try again.", + } + + strings_ru = { + "name": "SpotiSaver", + "_cls_doc": "Скачивание музыки из Spotify", + # "args": " Ссылка на песню не указана", + "downloading": "📥 Скачиваю: {}", + "error": " Ерорь, смотри логи!", + "done": "✔️ Готово!", + "no_spotifymod": "💢 SpotifyMod не найден.", + "no_spotify": "😅 В Spotify ничего не играет.", + "nf_id": " ID песни не найден", + "nf_track": " Песня не найдена", + "timeout": " Таймаут! Попробуй ещё раз.", + } + + def __init__(self): + self.config = loader.ModuleConfig( + loader.ConfigValue( + "TimeOut", + 60, + "Response timeout in seconds | Время ожидания ответа в секундах", + validator=loader.validators.Integer(minimum=30), + ) + ) + + async def get_session(self, client: httpx.AsyncClient) -> str: + res = await client.get("https://spotmate.online/en1", headers={ + "User-Agent": headers["User-Agent"], + "Accept": "text/html", + }, + timeout=self.config["TimeOut"]) + match = re.search(r'csrf-token[^>]*content="([^"]+)"', res.text) + if not match: + raise ValueError("CSRF token not found") + return match.group(1) + + async def get_current_spotify_url(self) -> str | None: + spotifymod = self.lookup("SpotifyMod") + if not spotifymod or not spotifymod.sp: + return None + current_playback = await asyncio.to_thread(spotifymod.sp.current_playback) + if not current_playback or not current_playback.get("is_playing"): + return None + track_id = current_playback["item"]["id"] + return f"https://open.spotify.com/track/{track_id}" + + @loader.command(ru_doc="<ссылка> — Скачать трек из Spotify") + async def spotsave(self, message: Message): + """ - Download track from Spotify""" + args = utils.get_args_raw(message) + if not args: + spotifymod = self.lookup("SpotifyMod") + if not spotifymod or not spotifymod.sp: + return await utils.answer(message, self.strings["no_spotifymod"]) + args = await self.get_current_spotify_url() + if not args: + return await utils.answer(message, self.strings["no_spotify"]) + if "track/" not in args: + return await utils.answer(message, self.strings["nf_id"]) + track_url = args.split("?")[0] + try: + async with httpx.AsyncClient(follow_redirects=True) as client: + csrf = await self.get_session(client) + hdrs = {**headers, "X-CSRF-TOKEN": csrf} + info_res = await client.post( + "https://spotmate.online/getTrackData", + headers=hdrs, + json={"spotify_url": track_url}, + timeout=self.config["TimeOut"], + ) + + info = info_res.json() + if info.get("type") != "track": + return await utils.answer(message, self.strings["nf_track"]) + track_name = info.get("name", "Unknown") + artists = ", ".join(a["name"] for a in info.get("artists", [])) + full_name = f"{artists} - {track_name}" + track_id = info.get("id", track_url.split("/")[-1]) + conv_res = await client.post( + "https://spotmate.online/convert", + headers=hdrs, + json={"urls": track_url}, + timeout=self.config["TimeOut"], + ) + conv = conv_res.json() + download_url = conv.get("url") or conv.get("download_url") + task_id = conv.get("task_id") or conv.get("taskId") + if not download_url and task_id: + for _ in range(40): + await asyncio.sleep(4.5) + task_res = await client.get( + f"https://spotmate.online/tasks/{task_id}", + headers={**hdrs, "Accept": "application/json"}, + timeout=self.config["TimeOut"], + ) + task = task_res.json() + if task.get("error"): + return await utils.answer(message, self.strings["error"]) + data = task.get("data") or task.get("result") or {} + status = str(data.get("status") or data.get("state") or "").lower() + if status == "finished": + download_url = ( + data.get("url") or data.get("download_url") + or (data.get("result") or {}).get("url") + or (data.get("result") or {}).get("download_url") + ) + break + + if status in ("failed", "error", "expired", "cancelled"): + return await utils.answer(message, self.strings["error"]) + + if not download_url: + return await utils.answer(message, self.strings["timeout"]) + + await utils.answer( + message, + self.strings["downloading"].format(utils.escape_html(full_name)), + ) + + file_res = await client.get( + download_url, + headers={"User-Agent": headers["User-Agent"], "Referer": "https://spotmate.online/en1"}, + timeout=self.config["TimeOut"], + ) + + filename = f"{track_id}.mp3" + with open(filename, "wb") as f: + f.write(file_res.content) + + await self.client.send_file( + message.chat_id, + filename, + caption=self.strings["done"], + reply_to=message.id, + attributes=( + [utils.get_audio_tag(filename, title=track_name, performer=artists)] + if hasattr(utils, "get_audio_tag") + else [] + ), + ) + + await message.delete() + if os.path.exists(filename): + os.remove(filename) + + except Exception: + logger.exception("Download failed") + await utils.answer(message, self.strings["error"]) \ No newline at end of file diff --git a/SunnexGB/Heroku-Modules/SpotifyLyrics.py b/SunnexGB/Heroku-Modules/SpotifyLyrics.py new file mode 100644 index 0000000..8c9fd4d --- /dev/null +++ b/SunnexGB/Heroku-Modules/SpotifyLyrics.py @@ -0,0 +1,285 @@ +# meta developer: @SunnexGB +# requires: aiohttp +# meta pic: https://r2.fakecrime.bio/uploads/f49a9294-36ad-4fc4-801f-48cb049111d6.jpg +# meta banner: https://r2.fakecrime.bio/uploads/f49a9294-36ad-4fc4-801f-48cb049111d6.jpg +# meta fhsdesc: Spotify, music, музыка, спотифай,Lyrics, слова, текст, трек, песня +# все же я не знаю трек или сонг, так что пусть будет трек, а не сонг потому что интуитивнее поняттнее,наверное? +# крутой баннер да? +#current version +__version__ = (1, 1, 2) + +from herokutl.types import Message +from .. import loader, utils +from ..types import InlineCall +import aiohttp +import asyncio +import re + + +@loader.tds +class SpotifyLyrics(loader.Module): + """life lyrics current song""" + + strings = { + "name": "SpotifyLyrics", + "no_spotifymod": "💢 SpotifyMod not found,but u can install it. You can also support developer: @ke_mods", + "no_auth": "⁉️ You not authorized in SpotifyMod, visit you Saved Messages.", + "no_spotify": "😅 Nothing is playing on Spotify.", + "no_lyrics": "💢 Lyrics not found for: {}", + "not_synced": "⚠️ Lyrics are not synchronized.\n\n", + "finished": "‼️ Playback ended or track changed.", + "header": "🎤 {} - {}\n\n", + "timeout": " Oopsi, looks like we've got a timeout here.", + } + + strings_ru = { + "cls_doc": "Лайв слова текущей песни.", + "no_spotifymod": "💢 SpotifyMod не найден,но его можно установить. Вы также можете поддержать разработчика: @ke_mods", + "no_auth": "⁉️ Вы не авторизированы в SpotifyMod, перейдите в Избранное.", + "no_spotify": "😅 В Spotify ничего не играет.", + "no_lyrics": "💢 Текст не найден для: {}", + "not_synced": "⚠️ Текст не синхронизирован.\n\n", + "finished": "‼️ Воспроизведение завершено или трек сменился.", + "header": "🎤 {} - {}\n\n", + "timeout": " Упси, похоже кто то словил таймаут..", + } + + def __init__(self): + self._active_tasks: dict = {} + self.config = loader.ModuleConfig( + loader.ConfigValue( + "emoji_current", + "🤯", + "Emoji for current line", + validator=loader.validators.String(), + ), + loader.ConfigValue( + "dot", + "♪", + "instrumental_emoji or text", + validator=loader.validators.String(), + ), + loader.ConfigValue( + "lyrics_delay", + 0.5, + "delay in switching to a new timing sector with words", + ), + loader.ConfigValue( + "request_timeout", + 12, + "timeout value", + ), + ) + + async def install_spotifymod(self, call: InlineCall): + mod_url = "https://raw.githubusercontent.com/radiocycle/Modules/refs/heads/master/SpotifyMod.py" + try: + m = self.lookup("Modules") or self.lookup("loader") + await m.download_and_install(mod_url) + await call.answer("SpotifyMod installed!", show_alert=True) + mod = self.lookup("SpotifyMod") + acs_tkn = mod.get("acs_tkn") if mod else None + if not acs_tkn: + await self.invoke("sauth", " ", "me") + await call.edit( + self.strings("no_auth"), + reply_markup=[[{"text": "Хорошо", "callback": self.close}]], + ) + else: + await call.delete() + except Exception as e: + await call.answer(f"Error! Check logs.\n{e}", show_alert=True) + + def close(self, call: InlineCall): + return call.delete() + + async def _get_lyrics(self, artist: str, track: str): + clean_track = re.sub(r"\(.*?\)|\[.*?\]", "", track).strip() + try: + async with aiohttp.ClientSession() as session: + async with session.get( + "https://lrclib.net/api/search", + params={"track_name": clean_track, "artist_name": artist}, + timeout=aiohttp.ClientTimeout(total=(self.config["request_timeout"])), + ) as resp: + if resp.status == 200: + res = await resp.json() + return res[0] if res else None + except asyncio.TimeoutError: + return {"timeout": True} + except Exception: + pass + return None + + def _parse_synced(self, synced_text: str) -> list: + lines = [] + for line in synced_text.split("\n"): + m = re.search(r"\[(\d+):(\d+\.\d+)\](.*)", line) + if m: + mins, secs, text = m.groups() + lines.append({ + "time": (int(mins) * 60 + float(secs)) * 1000, + "text": text.strip(), + }) + return lines + + def _build_content(self, artist, track, lines, plain, progress_ms, not_synced_str): + header = self.strings("header").format( + utils.escape_html(artist), + utils.escape_html(track), + ) + if lines: + curr_idx = 0 + for i, line in enumerate(lines): + if progress_ms >= line["time"]: + curr_idx = i + win_start = max(0, curr_idx - 1) + win_end = min(len(lines), curr_idx + 6) + rows = [] + for i in range(win_start, win_end): + t = lines[i]["text"] or self.config["dot"] + if i == curr_idx: + rows.append( + f"{self.config['emoji_current']} {utils.escape_html(t)}" + ) + else: + rows.append(f"{utils.escape_html(t)}") + return header + "\n".join(rows) + else: + return header + not_synced_str + f"
{utils.escape_html((plain or '')[:4000])}
" + + def _markup(self, song_url): + return [ + [{"text": "🔗 song.link", "url": song_url}], + [{"text": "❌ Close", "callback": self._close_cb}], + ] + + async def _close_cb(self, call): + for track_id, task in list(self._active_tasks.items()): + task.cancel() + self._active_tasks.pop(track_id, None) + try: + await call.answer() + await call.delete() + except Exception: + pass + + async def run_loop(self, form, mod, track_id, artist_name, track_name, song_url, lines, plain, not_synced_str): + last_display = "" + try: + while True: + pb = mod.sp.current_playback() + if not pb or not pb.get("item") or pb["item"]["id"] != track_id: + try: + await form.edit( + self.strings("finished"), + reply_markup=[[{"text": "❌ Close", "callback": self._close_cb}]], + ) + except Exception: + pass + break + + prog = pb.get("progress_ms", 0) + content = self._build_content( + artist_name, track_name, lines, plain, prog, not_synced_str + ) + + if content != last_display: + try: + await form.edit(content, reply_markup=self._markup(song_url)) + last_display = content + except Exception: + break + + if not lines: + break + + await asyncio.sleep(self.config["lyrics_delay"]) + + except asyncio.CancelledError: + raise + except Exception: + pass + finally: + self._active_tasks.pop(track_id, None) + + @loader.command(ru_doc="- показать синхронизированный текст песни") + async def snowlcmd(self, message: Message): + """- show synchronized lyrics for current Spotify track""" + mod = self.lookup("SpotifyMod") + if not mod: + form = await self.inline.form("⏳", message=message) + await form.edit( + self.strings("no_spotifymod"), + reply_markup=[[{"text": "Install SpotifyMod", "callback": self.install_spotifymod}]], + ) + return + + acs_tkn = mod.get("acs_tkn") + if not acs_tkn: + await self.invoke("sauth", " ", "me") + form = await self.inline.form("⏳", message=message) + await form.edit( + self.strings("no_auth"), + reply_markup=[[{"text": "Хорошо", "callback": self.close}]], + ) + return + + playback = mod.sp.current_playback() + if not playback or not playback.get("item"): + return await utils.answer(message, self.strings("no_spotify")) + + track = playback["item"] + track_id = track["id"] + artist_name = track["artists"][0]["name"] + track_name = track["name"] + song_url = f"https://song.link/s/{track_id}" + + old = self._active_tasks.pop(track_id, None) + if old: + old.cancel() + + data = await self._get_lyrics(artist_name, track_name) + if data and data.get("timeout"): + return utils.answer( + message, + self.strings["timeout"] + ) + if not data or data.get("instrumental"): + track_and_artist = f"{utils.escape_html(track_name)} - {utils.escape_html(artist_name)}" + return await utils.answer( + message, + self.strings("no_lyrics").format(track_and_artist), + ) + + synced_raw = data.get("syncedLyrics") + plain = data.get("plainLyrics", "") + + lines = self._parse_synced(synced_raw) if synced_raw else [] + not_synced_str = self.strings("not_synced") + + prog = playback.get("progress_ms", 0) + initial_content = self._build_content( + artist_name, track_name, lines, plain, prog, not_synced_str + ) + + form = await self.inline.form( + text=initial_content, + message=message, + reply_markup=self._markup(song_url), + ) + + task = asyncio.ensure_future( + self.run_loop( + form=form, + mod=mod, + track_id=track_id, + artist_name=artist_name, + track_name=track_name, + song_url=song_url, + lines=lines, + plain=plain, + not_synced_str=not_synced_str, + ) + ) + self._active_tasks[track_id] = task \ No newline at end of file diff --git a/SunnexGB/Heroku-Modules/YandexLyrics.py b/SunnexGB/Heroku-Modules/YandexLyrics.py new file mode 100644 index 0000000..d9d31aa --- /dev/null +++ b/SunnexGB/Heroku-Modules/YandexLyrics.py @@ -0,0 +1,352 @@ +# meta developer: @SunnexGB +# requires: aiohttp +# meta pic: https://r2.fakecrime.bio/uploads/ab42b5e2-91f1-4ed1-8002-51b3184e3839.jpg +# meta banner: https://r2.fakecrime.bio/uploads/ab42b5e2-91f1-4ed1-8002-51b3184e3839.jpg +# meta fhsdesc: YaMusic, music, музыка, яндекс музыка,Lyrics, слова, текст, трек, песня +# все же я не знаю трек или сонг, так что пусть будет трек, а не сонг потому что интуитивнее поняттнее,наверное? +# крутой баннер да? +#current version +__version__ = (1, 1, 2) + +from herokutl.types import Message +from .. import loader, utils +from ..types import InlineCall +import aiohttp +import asyncio +import re + + +@loader.tds +class YandexLyrics(loader.Module): + """life lyrics current song""" + + strings = { + "name": "YandexLyrics", + "no_YaMusicMod": "💢 YaMusicMod not found,but u can install it. You can also support developer: @codrago_m", + "no_auth": "⁉️ You not authorized in SpotifyMod, visit you Saved Messages and setup token to continue.", + "no_ym": "😅 Nothing is playing on YaMusic.", + "no_lyrics": "💢 Lyrics not found for: {}", + "not_synced": "⚠️ Lyrics are not synchronized.\n\n", + "finished": "‼️ Playback ended or track changed.", + "header": "🎤 {} - {}\n\n", + "timeout": " Oopsi, looks like we've got a timeout here.", + + } + + strings_ru = { + "cls_doc": "Лайв слова текущей песни.", + "no_YaMusicMod": "💢 YaMusicMod не найден,но его можно установить. Вы также можете поддержать разработчика: @codrago_m", + "no_auth": "⁉️ Вы не авторизированы в YaMusicMod, перейдите в Избранное и установите токен для продолжения работы.", + "no_ym": "😅 В YaMusic ничего не играет.", + "no_lyrics": "💢 Текст не найден для: {}", + "not_synced": "⚠️ Текст не синхронизирован.\n\n", + "finished": "‼️ Воспроизведение завершено или трек сменился.", + "header": "🎤 {} - {}\n\n", + "timeout": " Упси, похоже кто то словил таймаут..", + } + + + def __init__(self): + self._active_tasks: dict = {} + self.config = loader.ModuleConfig( + loader.ConfigValue( + "emoji_current", + "🤯", + "Emoji for current line", + validator=loader.validators.String(), + ), + loader.ConfigValue( + "dot", + "♪", + "instrumental_emoji or text", + validator=loader.validators.String(), + ), + loader.ConfigValue( + "lyrics_delay", + 0.5, + "delay in switching to a new timing sector with words", + ), + loader.ConfigValue( + "request_timeout", + 12, + "timeout value", + ), + ) + + async def install_yamusic(self, call: InlineCall): + mod_url = "https://raw.githubusercontent.com/coddrago/modules/main/YaMusic.py" + try: + m = self.lookup("Modules") or self.lookup("loader") + await m.download_and_install(mod_url) + await call.answer("YaMusicMod installed!", show_alert=True) + mod = self.lookup("YaMusicMod") + acs_tkn = mod.get("__config__")["token"] if mod else "****" + if not acs_tkn: + await self.invoke("yguide", " ", "me") + await call.edit( + self.strings("no_auth"), + reply_markup=[[{"text": "Хорошо", "callback": self.close}]], + ) + else: + await call.delete() + except Exception as e: + await call.answer(f"Error! Check logs.\n{e}", show_alert=True) + + def close(self, call: InlineCall): + return call.delete() + + async def _get_lyrics(self, artist: str, track: str): + clean_track = re.sub(r"\(.*?\)|\[.*?\]", "", track).strip() + try: + async with aiohttp.ClientSession() as session: + async with session.get( + "https://lrclib.net/api/search", + params={"track_name": clean_track, "artist_name": artist}, + timeout=aiohttp.ClientTimeout(total=(self.config["request_timeout"])), + ) as resp: + if resp.status == 200: + res = await resp.json() + return res[0] if res else None + except asyncio.TimeoutError: + return {"timeout": True} + except Exception: + pass + return None + + def _parse_synced(self, synced_text: str) -> list: + lines = [] + for line in synced_text.split("\n"): + m = re.search(r"\[(\d+):(\d+\.\d+)\](.*)", line) + if m: + mins, secs, text = m.groups() + lines.append({ + "time": (int(mins) * 60 + float(secs)) * 1000, + "text": text.strip(), + }) + return lines + + def _build_content(self, artist, track, lines, plain, progress_ms, not_synced_str): + header = self.strings("header").format( + utils.escape_html(artist), + utils.escape_html(track), + ) + if lines: + curr_idx = 0 + for i, line in enumerate(lines): + if progress_ms >= line["time"]: + curr_idx = i + win_start = max(0, curr_idx - 1) + win_end = min(len(lines), curr_idx + 6) + rows = [] + for i in range(win_start, win_end): + t = lines[i]["text"] or self.config["dot"] + if i == curr_idx: + rows.append( + f"{self.config['emoji_current']} {utils.escape_html(t)}" + ) + else: + rows.append(f"{utils.escape_html(t)}") + return header + "\n".join(rows) + else: + return header + not_synced_str + f"
{utils.escape_html((plain or '')[:4000])}
" + + def _markup(self, song_url): + return [ + [{"text": "🔗 song.link", "url": song_url}], + [{"text": "❌ Close", "callback": self._close_cb}], + ] + + async def _close_cb(self, call): + for track_id, task in list(self._active_tasks.items()): + task.cancel() + self._active_tasks.pop(track_id, None) + try: + await call.answer() + await call.delete() + except Exception: + pass + + async def run_loop(self, form, mod, track_id, artist_name, track_name, song_url, lines, plain, not_synced_str): + last_display = "" + try: + while True: + pb = await mod._YaMusicMod__get_now_playing() + if not pb or not pb.get("track") or pb["track"]["track_id"] != track_id: + try: + await form.edit( + self.strings("finished"), + reply_markup=[[{"text": "❌ Close", "callback": self._close_cb}]], + ) + except Exception: + pass + break + + prog = pb.get("progress_ms", 0) + content = self._build_content( + artist_name, track_name, lines, plain, prog, not_synced_str + ) + + if content != last_display: + try: + await form.edit(content, reply_markup=self._markup(song_url)) + last_display = content + except Exception: + break + + if not lines: + break + + await asyncio.sleep(self.config["lyrics_delay"]) + + except asyncio.CancelledError: + raise + except Exception: + pass + finally: + self._active_tasks.pop(track_id, None) + + @loader.command(ru_doc="- показать синхронизированный текст песни") + async def ynowlcmd(self, message: Message): + """- show synchronized lyrics for current YaMusic track""" + mod = self.lookup("YaMusic") + if not mod: + form = await self.inline.form("⏳", message=message) + await form.edit( + self.strings("no_YaMusicMod"), + reply_markup=[[{"text": "Install YaMusic", "callback": self.install_yamusic}]], + ) + return + + ya_token = mod.get("__config__")["token"] + if not ya_token: + await self.invoke("yguide", " ", "me") + form = await self.inline.form("⏳", message=message) + await form.edit( + self.strings("no_auth"), + reply_markup=[[{"text": "Хорошо", "callback": self.close}]], + ) + return + + playback = await mod._YaMusicMod__get_now_playing() + if not playback or not playback.get("track"): + return await utils.answer(message, self.strings("no_ym")) + + track = playback["track"] + track_id = track["track_id"] + artist_name = ", ".join(track["artist"]) + track_name = track["title"] + song_url = f"https://song.link/s/{track_id}" + + old = self._active_tasks.pop(track_id, None) + if old: + old.cancel() + + data = await self._get_lyrics(artist_name, track_name) + if data and data.get("timeout"): + return utils.answer( + message, + self.strings["timeout"] + ) + if not data or data.get("instrumental"): + track_and_artist = f"{utils.escape_html(track_name)} - {utils.escape_html(artist_name)}" + return await utils.answer( + message, + self.strings("no_lyrics").format(track_and_artist), + ) + + synced_raw = data.get("syncedLyrics") + plain = data.get("plainLyrics", "") + + lines = self._parse_synced(synced_raw) if synced_raw else [] + not_synced_str = self.strings("not_synced") + + prog = playback.get("progress_ms", 0) + initial_content = self._build_content( + artist_name, track_name, lines, plain, prog, not_synced_str + ) + + form = await self.inline.form( + text=initial_content, + message=message, + reply_markup=self._markup(song_url), + ) + + task = asyncio.create_task( + self.run_loop( + form=form, + mod=mod, + track_id=track_id, + artist_name=artist_name, + track_name=track_name, + song_url=song_url, + lines=lines, + plain=plain, + not_synced_str=not_synced_str, + ) + ) + self._active_tasks[track_id] = task + + # Fan-fantasizing + # Fa-fa-fa-fantasizing + # You and I-I-I + # When I close my eyes (my eyes) + # Nothings real + # Fantasizing (fantasizing) + # Bout you and I + # Cos you only hit my line + # When you wanna waste time + # I know you're so busy + # But trust me baby I'm not blind (blind) + # Uh oh, you and I + # We could never be + # Uh oh you and I + # Cos we will never be + # Uh oh, you and I + # No, we will never be + # That pretty picture that I painted in my mind (mind) + # So tell me what (tell me, tell me) + # The view is like + # With your head in the clouds + # And tell me what (tell me, tell me) + # It feels like to be right all the time + # You say that you love me + # But you don't even love yourself (no) + # Wanna get in my head + # But I ain't gonna let you close (no) + # Tryna control me + # But I ain't gon' play your game + # No more + # No I won't + # When I close (when I close) + # My eyes (my eyes) + # Nothing's real (no) + # Fantasizing (fantasize) + # bout you-you-you-you-you + # You-you-you-you + # You-you-you-you + # You-you-you + # And I + # You-you-you-you-you-you-you + # You-you-you-you-you-you-you + # You-you-you-you-you + # And I + # This is the last time I tell you + # Don't come round my way if you're just gon' waste my time + # And no, I won't be there for the long run + # No, not I + # But you never get (never get) + # The message, do you? + # You never seem (never seem) + # To grip an understanding + # That you emulate a ghost + # I pointed out all of your flaws + # But you still came up with excuses for em all + # So typical (so typical) + # You know it all + # So of course, I'm the one that's wrong (right?) + # When I close my eyes + # Nothings real + # Fan- fantasize + # Fa-fa-fa-fa + # Fantasizing + # Fa-fantasizing bout you and I \ No newline at end of file diff --git a/SunnexGB/Heroku-Modules/everlastingsummer.py b/SunnexGB/Heroku-Modules/everlastingsummer.py new file mode 100644 index 0000000..16dd3c4 --- /dev/null +++ b/SunnexGB/Heroku-Modules/everlastingsummer.py @@ -0,0 +1,486 @@ +# meta developer: @H_SunMods +#meta banner: https://i.ibb.co/LdN9FXjc/logo.webp +# __version__ +__version__ = ("alpha", "1.0", 0) + +import asyncio +import copy +import json +from urllib.request import Request, urlopen +from herokutl.types import Message +from .. import loader, utils +from ..types import InlineCall + + +prologue_dialogs_url = "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/ddialogs/prologue_only.json" +routes_url = "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/ddialogs/routes_prologue.json" +menu_background_url = "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/in_telegram_images/Start_Menu.jpg" +save_background_url = "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/in_telegram_images/Save_Menu.png" + + +@loader.tds +class EverlastingSummer(loader.Module): + """Встретив Семёна, главного героя игры, вы никогда бы не обратили на него внимания. Просто обычный молодой человек среди тысяч, даже сотен тысяч таких, как он, в каждом обычном городе. Но однажды с ним происходит нечто совершенно необычное: он засыпает в автобусе зимой и просыпается... посреди жаркого лета. Перед ним - "Совёнок" - пионерский лагерь, а за ним - его прежняя жизнь. Чтобы понять, что с ним произошло, Семёну придется познакомиться с местными жителями (и, возможно, даже найти любовь), сориентироваться в сложном лабиринте человеческих отношений и своих собственных проблем, а также разгадать тайны лагеря. И ответить на главный вопрос - как вернуться? Стоит ли ему возвращаться?""" + strings = { + "name": "EverlastingSummer", + "menu": "Пролог", + "disclaimer": ( + "Игра является плодом фантазии её разработчиков\n" + "и не ставит перед собой цели затронуть или иным\n" + "образом оскорбить кого-либо по религиозному,расовому,\n" + "социальному, экономическому или видовому признаку.\n" + "Также любое ущемление чувства прекрасного, активной\n" + "гражданской позиции или иных высоких душевных порывов\n" + "игроков разработчики оставляют на их совести.\n" + "Совпадения героев с вашими реальными (и воображаемыми)\n" + "знакомыми,соседями,коллегами, тульпами считать случайным.\n" + "Все героини достигли восемнадцатилетнего возраста,\n" + "и они дали письменное согласие на участие в игре\n" + "(выписка из истории болезни сценариста предоставляется по требованию).\n" + "При разработке не пострадало ни одного маскота, животного или человека. Приятной игры!" + ), + "bad": "Не удалось загрузить сценарий", + "end": "{}", + "save_header": "Сохранения", + "load_header": "Загрузить игру", + "default_route_question": "Что выберете?", + "or_game": "Игра", + "or_character": "Персонаж", + "cutscene_text": "💫", + "opening_title": "Опенинг", + "opening_next": "Пропустить опенинг", + "saved": "игра сохранена в слот № {}", + "loaded": "Сохранение № {} загружено", + "empty": "Слот {} пуст", + "rewrite": "Слот {} уже занят. Перезаписать?", + "state_slots_from_menu": "slots_from_menu", + "chapter_prologue": "prologue", + "save_action": "save", + "load_action": "load", + "mode_ask_rewrite": "ask_rewrite", + "mode_ended": "ended", + "mode_menu": "menu", + "mode_play": "play", + "mode_slots": "slots", + "type_label": "label", + "type_jump": "jump", + "type_scene": "scene", + "type_dialogue": "dialogue", + "type_narration": "narration", + "type_route": "route", + "type_opening": "opening", + } + + def __init__(self): + self.config = loader.ModuleConfig(loader.ConfigValue("cut_speed", 3)) + self.dialogs_url = prologue_dialogs_url + self.routes_url = routes_url + self.menu_image = menu_background_url + self.save_image = save_background_url + self.dialogs_data = None + self.routes_data = None + self.label_index = {} + + async def json_load(self, url: str): + last_error = None + for i in range(3): + try: + def run(): + req = Request(url, headers={"User-Agent": "Mozilla/5.0 HSunMods"}) + with urlopen(req, timeout=60) as x: + return json.loads(x.read().decode("utf-8")) + return await asyncio.to_thread(run) + except Exception as e: + last_error = e + if i < 2: + await asyncio.sleep(1.5 * (i + 1)) + raise last_error + + async def load_data(self, force: bool = False): + if self.dialogs_data is not None and self.routes_data is not None and not force: + return True + try: + self.dialogs_data = await self.json_load(self.dialogs_url) + self.routes_data = await self.json_load(self.routes_url) + except Exception: + return False + + prologue_nodes = self.dialogs_data.get(self.strings["chapter_prologue"]) + if not isinstance(prologue_nodes, list): + return False + self.dialogs_data = {self.strings["chapter_prologue"]: prologue_nodes} + + self.label_index = {} + for node_index, node in enumerate(prologue_nodes): + if isinstance(node, dict) and node.get("type") == self.strings["type_label"]: + self.label_index[node.get("name")] = (self.strings["chapter_prologue"], node_index) + return True + + def state_get(self): + return self.get( + "state", + { + "mode": self.strings["mode_menu"], + "chapter": self.strings["chapter_prologue"], + "idx": 0, + "pending": None, + "scene": {}, + "vars": {}, + }, + ) + + def state_set(self, state_data): + self.set("state", state_data) + + def slots_get(self): + return self.get("slots", {}) + + def slots_set(self, slots_data): + self.set("slots", slots_data) + + async def ui(self, target, text, kb=None, photo=None): + if isinstance(target, InlineCall): + if photo: + try: + return await target.edit(text, reply_markup=kb, photo=photo) + except TypeError: + try: + return await target.edit(text, reply_markup=kb, file=photo) + except Exception: + pass + except Exception: + pass + try: + return await target.edit(text, reply_markup=kb) + except Exception: + raise + if photo: + try: + return await utils.answer(target, text, reply_markup=kb, photo=photo) + except Exception: + pass + return await utils.answer(target, text, reply_markup=kb) + + def menu_kb(self): + return [ + [{"text": "Начать пролог", "callback": self.new_game}], + [{"text": "Сохранения", "callback": self.save_menu, "args": (self.strings["load_action"],)}], + [{"text": "Дисклеймер", "callback": self.disclaimer_msg}], + ] + + def start_kb(self): + return [ + [{"text": "➤", "callback": self.next_step}], + [{"text": "Сохранить", "callback": self.save_menu, "args": (self.strings["save_action"],)}], + [{"text": "Меню", "callback": self.menu}], + ] + + def save_kb(self, mode: str): + slots = self.slots_get() + row = [] + for i in range(1, 6): + k = str(i) + b = {"text": k, "callback": self.save_action, "args": (mode, i)} + if k in slots: + b["style"] = "success" + row.append(b) + return [row, [{"text": "Назад", "callback": self.back_from_saves}]] + + def choice_kb(self, route_id: str): + opts = self.routes_data.get(route_id, {}).get("options", {}) + rows = [] + for i, txt in enumerate(opts.keys()): + rows.append([{"text": txt, "callback": self.pick_option, "args": (route_id, i)}]) + rows.append([{"text": "Сохранить", "callback": self.save_menu, "args": (self.strings["save_action"],)}]) + rows.append([{"text": "Меню", "callback": self.menu}]) + return rows + + def opening_kb(self): + return [[{"text": self.strings["opening_next"], "callback": self.opening_done}]] + + def state_preservation(self, state_data): + return { + "chapter": state_data.get("chapter"), + "idx": state_data.get("idx", 0), + "part": state_data.get("part", 0), + "pending": copy.deepcopy(state_data.get("pending")), + "scene": copy.deepcopy(state_data.get("scene")), + "vars": copy.deepcopy(state_data.get("vars", {})), + "mode": self.strings["mode_play"], + } + + def scene_photo(self, state_data): + u = (state_data.get("scene") or {}).get("raw_url") + return u if isinstance(u, str) else None + + def wait_text(self, t: str): + return [x.strip() for x in t.split("{w}") if x.strip()] or [t] + + def render_dialogs(self, state_data): + pending_node = state_data.get("pending") or {} + if pending_node.get("type") == self.strings["type_dialogue"]: + who = (pending_node.get("character") or pending_node.get("char_id") or self.strings["or_character"]).strip() + txt = " ".join(pending_node.get("parts", [])[: pending_node.get("part", 1)]) + return f"{who}:\n
{txt}
" + if pending_node.get("type") == self.strings["type_narration"]: + return " ".join(pending_node.get("parts", [])[: pending_node.get("part", 1)]) + return "" + + def is_ending_label(self, name: str): + return name in self.routes_data.get("endings", {}).get("labels", []) + + async def go(self, target, state_data): + cut_scene_speed_fallback = self.config["cut_speed"] + while True: + chapter_nodes = self.dialogs_data.get(self.strings["chapter_prologue"], []) + node_index = state_data.get("idx", 0) + if node_index >= len(chapter_nodes): + ending_name = self.routes_data.get("endings", {}).get("fallback", "main_bad_ending") + state_data["mode"] = self.strings["mode_ended"] + state_data["ending"] = ending_name + self.state_set(state_data) + await self.ui(target, self.strings["end"].format(ending_name), self.menu_kb(), self.scene_photo(state_data)) + return + + current_node = chapter_nodes[node_index] + node_type = current_node.get("type") + + if node_type == self.strings["type_label"]: + if self.is_ending_label(current_node.get("name")): + state_data["mode"] = self.strings["mode_ended"] + state_data["ending"] = current_node.get("name") + self.state_set(state_data) + await self.ui(target, self.strings["end"].format(current_node.get("name")), self.menu_kb(), self.scene_photo(state_data)) + return + state_data["idx"] = node_index + 1 + continue + + if node_type == self.strings["type_jump"]: + jump_target = self.label_index.get(current_node.get("label")) + if jump_target: + state_data["chapter"], state_data["idx"] = jump_target + else: + state_data["idx"] = node_index + 1 + continue + + if node_type == self.strings["type_scene"]: + state_data["scene"] = { + "raw_url": current_node.get("raw_url"), + "location": current_node.get("location"), + "action": current_node.get("action"), + "kind": current_node.get("kind"), + "name": current_node.get("name"), + } + state_data["idx"] = node_index + 1 + next_node = chapter_nodes[state_data["idx"]] if state_data["idx"] < len(chapter_nodes) else None + if isinstance(next_node, dict) and next_node.get("type") == self.strings["type_scene"]: + scene_duration = current_node.get("duration") + if scene_duration is None: + if cut_scene_speed_fallback is None: + scene_delay_seconds = 0.0 + else: + try: + scene_delay_seconds = float(cut_scene_speed_fallback) + except Exception: + scene_delay_seconds = 0.0 + else: + try: + scene_delay_seconds = float(scene_duration) + except Exception: + scene_delay_seconds = 0.0 + if scene_delay_seconds < 0: + scene_delay_seconds = 0.0 + self.state_set(state_data) + await self.ui(target, self.strings["cutscene_text"], None, self.scene_photo(state_data)) + if scene_delay_seconds > 0: + await asyncio.sleep(scene_delay_seconds) + continue + continue + + if node_type in {self.strings["type_dialogue"], self.strings["type_narration"]}: + state_data["pending"] = { + "type": node_type, + "parts": self.wait_text(current_node.get("text", "")), + "part": 1, + "char_id": current_node.get("char_id"), + "character": current_node.get("character"), + } + state_data["mode"] = self.strings["mode_play"] + self.state_set(state_data) + await self.ui(target, self.render_dialogs(state_data), self.start_kb(), self.scene_photo(state_data)) + return + + if node_type == self.strings["type_route"]: + route_id = current_node.get("id") + route_question = self.routes_data.get(route_id, {}).get("question") or self.strings["default_route_question"] + state_data["pending"] = {"type": self.strings["type_route"], "id": route_id} + state_data["mode"] = self.strings["mode_play"] + self.state_set(state_data) + await self.ui(target, route_question, self.choice_kb(route_id), self.scene_photo(state_data)) + return + + if node_type == self.strings["type_opening"] or (node_type == self.strings["type_label"] and current_node.get("kind") == self.strings["type_opening"]): + state_data["scene"] = { + "raw_url": current_node.get("raw_url"), + "location": current_node.get("location"), + "action": current_node.get("action"), + "kind": current_node.get("kind") or self.strings["type_opening"], + "name": current_node.get("name") or self.strings["type_opening"], + } + state_data["pending"] = {"type": self.strings["type_opening"]} + state_data["mode"] = self.strings["mode_play"] + state_data["idx"] = node_index + 1 + self.state_set(state_data) + await self.ui(target, self.strings["opening_title"], self.opening_kb(), self.scene_photo(state_data)) + return + + state_data["idx"] = node_index + 1 + + async def menu(self, call: InlineCall): + state = self.state_get() + state["mode"] = self.strings["mode_menu"] + state["pending"] = None + self.state_set(state) + await self.ui(call, self.strings["menu"], self.menu_kb(), self.menu_image) + + async def disclaimer_msg(self, call: InlineCall): + await self.ui(call, self.strings["disclaimer"], [[{"text": "Назад", "callback": self.menu}]], self.menu_image) + + async def new_game(self, call: InlineCall): + ok = await self.load_data(force=False) + if not ok: + await call.answer(self.strings["bad"], show_alert=True) + return + state = self.state_get() + state.update( + { + "chapter": self.strings["chapter_prologue"], + "idx": 0, + "part": 0, + "pending": None, + "scene": {}, + "vars": {}, + "mode": self.strings["mode_play"], + } + ) + self.state_set(state) + await self.go(call, state) + + async def next_step(self, call: InlineCall): + state = self.state_get() + pending_node = state.get("pending") or {} + if pending_node.get("type") == self.strings["type_opening"]: + await self.menu(call) + return + if pending_node.get("type") in {self.strings["type_dialogue"], self.strings["type_narration"]}: + if pending_node.get("part", 1) < len(pending_node.get("parts", [])): + pending_node["part"] += 1 + state["pending"] = pending_node + self.state_set(state) + await self.ui(call, self.render_dialogs(state), self.start_kb(), self.scene_photo(state)) + return + state["idx"] += 1 + state["pending"] = None + self.state_set(state) + await self.go(call, state) + return + await self.go(call, state) + + async def opening_done(self, call: InlineCall): + state = self.state_get() + state["pending"] = None + state["mode"] = self.strings["mode_menu"] + self.state_set(state) + await self.menu(call) + + async def pick_option(self, call: InlineCall, route_id: str, option_index: int): + state = self.state_get() + option_items = list((self.routes_data.get(route_id, {}).get("options") or {}).items()) + if option_index < 0 or option_index >= len(option_items): + return + _, option_data = option_items[option_index] + jump_label = option_data.get("jump") + if jump_label and jump_label in self.label_index: + state["chapter"], state["idx"] = self.label_index[jump_label] + else: + state["idx"] += 1 + state["pending"] = None + self.state_set(state) + await self.go(call, state) + + async def save_menu(self, call: InlineCall, mode: str): + state = self.state_get() + state[self.strings["state_slots_from_menu"]] = state.get("mode") == self.strings["mode_menu"] + state["mode"] = self.strings["mode_slots"] + self.state_set(state) + title_text = self.strings["save_header"] if mode == self.strings["save_action"] else self.strings["load_header"] + await self.ui(call, title_text, self.save_kb(mode), self.save_image) + + async def back_from_saves(self, call: InlineCall): + state = self.state_get() + if state.get(self.strings["state_slots_from_menu"]): + state[self.strings["state_slots_from_menu"]] = False + state["mode"] = self.strings["mode_menu"] + state["pending"] = None + self.state_set(state) + await self.menu(call) + return + if state.get("chapter") and state.get("mode") != self.strings["mode_menu"]: + state["mode"] = self.strings["mode_play"] + self.state_set(state) + pending_node = state.get("pending") + if pending_node and pending_node.get("type") == self.strings["type_route"]: + route_id = pending_node.get("id") + question_text = self.routes_data.get(route_id, {}).get("question") or self.strings["default_route_question"] + await self.ui(call, question_text, self.choice_kb(route_id), self.scene_photo(state)) + return + await self.ui(call, self.render_dialogs(state) or self.strings["or_game"], self.start_kb(), self.scene_photo(state)) + return + await self.menu(call) + + async def save_action(self, call: InlineCall, mode: str, n: int): + slots = self.slots_get() + state = self.state_get() + slot_key = str(n) + if mode == self.strings["save_action"]: + if slot_key in slots: + state["mode"] = self.strings["mode_ask_rewrite"] + self.state_set(state) + kb = [ + [ + {"text": "Да", "callback": self.rewrite_true, "args": (n,)}, + {"text": "Нет", "callback": self.save_menu, "args": (self.strings["save_action"],)}, + ], + [{"text": "Назад", "callback": self.back_from_saves}], + ] + await self.ui(call, self.strings["rewrite"].format(n), kb, self.save_image) + return + slots[slot_key] = self.state_preservation(state) + self.slots_set(slots) + await call.answer(self.strings["saved"].format(n), show_alert=True) + await self.save_menu(call, self.strings["save_action"]) + return + if slot_key not in slots: + await call.answer(self.strings["empty"].format(n), show_alert=True) + return + loaded_state = copy.deepcopy(slots[slot_key]) + self.state_set(loaded_state) + await call.answer(self.strings["loaded"].format(n), show_alert=True) + await self.go(call, loaded_state) + + async def rewrite_true(self, call: InlineCall, n: int): + slots = self.slots_get() + state = self.state_get() + slots[str(n)] = self.state_preservation(state) + self.slots_set(slots) + await call.answer(self.strings["saved"].format(n), show_alert=True) + await self.save_menu(call, self.strings["save_action"]) + + @loader.command() + async def bl(self, message: Message): + """Запустить ваше бесконечное лето,нууу точнее пока что его пролог.""" + ok = await self.load_data() + if not ok: + await utils.answer(message, self.strings["bad"]) + return + await self.ui(message, self.strings["menu"], self.menu_kb(), self.menu_image) diff --git a/SunnexGB/Heroku-Modules/full.txt b/SunnexGB/Heroku-Modules/full.txt new file mode 100644 index 0000000..9598d80 --- /dev/null +++ b/SunnexGB/Heroku-Modules/full.txt @@ -0,0 +1,9 @@ +SpotiSaver +SpotifyLyrics +YandexLyrics +HerokuTime +Shazamio +ForkCircles +Mikuru +pairavatars +ASCII \ No newline at end of file diff --git a/SunnexGB/Heroku-Modules/pairavatars.py b/SunnexGB/Heroku-Modules/pairavatars.py new file mode 100644 index 0000000..da1cbd7 --- /dev/null +++ b/SunnexGB/Heroku-Modules/pairavatars.py @@ -0,0 +1,57 @@ +# requires: https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz +# meta banner: https://i.ibb.co/yFVJ6L5D/pairavs.webp +# meta developer: @SunnexGB +# я хочу красивый баннер,не осуждайте. +# add version +__version__ = (1, 0, 1) + +import io +from PIL import Image +from herokutl.types import Message +from .. import loader, utils + +@loader.tds +class pairavatars(loader.Module): + """Create pair avatars""" + strings = { + "name": "PairAvatars", + "no_reply": "🚫 | Reply to photo!", + "processing": "💫 | Processing...", + "error": "⚠️ | Error`" + } + + strings_ru = { + "_cls_doc": "Создаёт парные авы", + "no_reply": "🚫 | Ответь на фото!", + "processing": "💫 | Обработка...", + "error": "⚠️ | Еррорь" + } + + @loader.command(ru_doc="- Создать парные аватарки (команда работает ТОЛЬКО ответом на сообщение)", only_reply=True) + async def pairavs(self, message: Message): + """- Create pair avatars (command work ONLY reply message)""" + reply = await message.get_reply_message() + processing_msg = await utils.answer(message, self.strings["processing"]) + try: + tmp_data = await message.client.download_media(reply.photo, bytes) + img = Image.open(io.BytesIO(tmp_data)) + w, h = img.size + center = w // 2 + left_part = img.crop((0, 0, center, h)) + right_part = img.crop((center, 0, w, h)) + out_left, out_right = io.BytesIO(), io.BytesIO() + left_part.save(out_left, "JPEG", quality=100) + right_part.save(out_right, "JPEG", quality=100) + out_left.name, out_right.name = "left.jpg", "right.jpg" + out_left.seek(0) + out_right.seek(0) + + await message.client.send_file( + message.chat_id, + [out_left, out_right], + reply_to=reply.id + ) + + await processing_msg.delete() + except Exception: + await utils.answer(processing_msg, self.strings["error"]) \ No newline at end of file diff --git a/SunnexGB/Heroku-Modules/spotify_ph.py b/SunnexGB/Heroku-Modules/spotify_ph.py new file mode 100644 index 0000000..22634f5 --- /dev/null +++ b/SunnexGB/Heroku-Modules/spotify_ph.py @@ -0,0 +1,124 @@ +# meta developer: @H_SunMods +# meta banner: https://r2.fakecrime.bio/uploads/7103b4ca-5fb1-4512-8a70-e720780c29c8.jpg +# current ver +__version__ = (1, 0, 0) + +import logging +from .. import loader, utils + +logger = logging.getLogger(__name__) + +@loader.tds +class spotifyph(loader.Module): + """Progress bar current track in spotify""" + + strings = { + "name": "spotify_ph", + "start_duration": "🎶🎶", + "start_full_duration": "🎶🎶", + "mid_duration": "🎶", + "empty_mid": "🎶", + "end_duration": "🎶", + "end_duration_full": "🎶", + "empty_end": "🎶", + "no_prem_start_duration": "ᵔᴥᵔ [---", + "no_prem_start_full_duration": "ᵔᴥᵔ [~~~", + "no_prem_mid_duration": "~~~", + "no_prem_empty_mid": "---", + "no_prem_end_duration_full": "~~~]", + "no_prem_empty_end": "---]", + "not_installed": "SpotifyMod is not installed", + "nothing_plays": "Nothing plays", + "sp_duration_desc": "Progress bar", + "sp_track_desc": "Artist and song", + "err": "Error`" + } + + strings_ru = { + "_cls_doc": "Прогресс бар играющего трека в спотифай", + "not_installed": "SpotifyMod не установлен", + "nothing_plays": "Ничего не играет", + "sp_duration_desc": "Прогресс бар", + "sp_track_desc": "Автор и песня", + "err": "Еррорь" + } + + async def client_ready(self, client, db): + self._client = client + utils.register_placeholder("sp_duration", self.sp_duration, self.strings("sp_duration_desc")) + utils.register_placeholder("sp_track", self.get_sp_track, self.strings("sp_track_desc")) + + def __init__(self): + self.config = loader.ModuleConfig( + loader.ConfigValue( + "show_text_time", + True, + "show text time", + validator=loader.validators.Boolean(), + ) + ) + + async def get_sp_track(self): + try: + s = self.lookup("SpotifyMod") + if not s or not s.sp: + return self.strings("not_installed") + + p = s.sp.current_user_playing_track() + if not (p and p.get('item')): + return self.strings("nothing_plays") + + artist = p['item']['artists'][0]['name'] + track_name = p['item']['name'] + return utils.escape_html(f"{artist} — {track_name}") + except Exception as e: + logger.error(f"Error in sp_track: {e}") + return self.strings("err") + + async def sp_duration(self): + s = self.lookup("SpotifyMod") + if not s or not s.sp: + return self.strings("not_installed") + + playback = s.sp.current_playback() + if not playback or not playback.get("item"): + return self.strings("nothing_plays") + + prog_ms = playback.get("progress_ms", 0) + dur_ms = playback["item"].get("duration_ms", 0) + + if dur_ms == 0: + return "00:00 / 00:00" + + percent = (prog_ms / dur_ms) * 100 + filled_units = int(percent // 16.66) + + # Логика для нищих + user = getattr(self._client, "heroku_me", getattr(self._client, "me", None)) + is_premium = getattr(user, "premium", False) + pref = "" if is_premium else "no_prem_" + + if filled_units >= 1: + start_pos = f"{pref}start_full_duration" + else: + start_pos = f"{pref}start_duration" + bar = self.strings(start_pos) + + for i in range(2, 6): + if filled_units >= i: + mid_pos = f"{pref}mid_duration" + else: + mid_pos = f"{pref}empty_mid" + bar += self.strings(mid_pos) + + if filled_units >= 6: + end_key = f"{pref}end_duration_full" + else: + end_key = f"{pref}empty_end" + bar += self.strings(end_key) + + if self.config["show_text_time"]: + prog_t = f"{prog_ms//60000:02}:{(prog_ms//1000)%60:02}" + dur_t = f"{dur_ms//60000:02}:{(dur_ms//1000)%60:02}" + return f"{bar} {prog_t} / {dur_t}" + return bar \ No newline at end of file diff --git a/coddrago/modules/YaMusic.py b/coddrago/modules/YaMusic.py index 036e671..01becc2 100644 --- a/coddrago/modules/YaMusic.py +++ b/coddrago/modules/YaMusic.py @@ -26,7 +26,6 @@ from .. import loader, utils logger = logging.getLogger(__name__) - class Banners: def __init__( self, @@ -324,6 +323,16 @@ class YaMusicMod(loader.Module): "name": "YaMusic" } + duration_placeholder = { + "start_duration": "☀️☀️", + "start_full_duration": "☀️☀️", + "closed_duration": "☀️", + "empty_mid": "☀️", + "empty_closed_duration_duration": "☀️", + "end_duration_full": "☀️", + "empty_closed_duration": "☀️", + } + def __init__(self): self.config = loader.ModuleConfig( loader.ConfigValue( @@ -553,67 +562,19 @@ class YaMusicMod(loader.Module): return "0%" percent = (progress / duration) * 100 + fill_logic = int(percent // 16.66) - s_less_10 = ( - "" - "" - "" - "" - "" - "" - ) - - s_10_to_20 = ( - "" - "" - "" - "" - "" - "" - ) - - s_30_to_40 = ( - "" - "" - "" - "" - "" - "" - ) - - s_over_50 = ( - "" - "" - "" - "" - "" - "" - ) - - s_over_80 = ( - "" - "" - "" - "" - "" - "" - ) - - if percent < 10: - return s_less_10 - elif percent < 20: - return s_10_to_20 - elif percent < 30: - return s_10_to_20 - elif percent < 40: - return s_30_to_40 - elif percent < 50: - return s_30_to_40 - elif percent < 80: - return s_over_50 + bar = self.duration_placeholder["start_full_duration"] if fill_logic >= 1 else self.duration_placeholder["start_duration"] + for i in range(2, 6): + if fill_logic >= i: + bar += self.duration_placeholder["closed_duration"] + else: + bar += self.duration_placeholder["empty_mid"] + if fill_logic >= 6: + bar += self.duration_placeholder["end_duration_full"] else: - return s_over_80 - + bar += self.duration_placeholder["empty_closed_duration"] + return bar except Exception as e: return f"Error: {e}" diff --git a/radiocycle/Modules/SpotifyMod.py b/radiocycle/Modules/SpotifyMod.py index af176c6..cf2a30e 100644 --- a/radiocycle/Modules/SpotifyMod.py +++ b/radiocycle/Modules/SpotifyMod.py @@ -17,10 +17,10 @@ # ======================================= # # meta developer: @ke_mods -# requires: telethon spotipy pillow requests yt-dlp curl_cffi +# requires: telethon spotipy pillow requests httpx # scope: ffmpeg -__version__ = (1, 0) +__version__ = (1, 0, 2) import asyncio import contextlib @@ -35,6 +35,7 @@ import os from types import FunctionType import random +import httpx import requests import spotipy from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont, ImageOps @@ -48,6 +49,14 @@ from .. import loader, utils logger = logging.getLogger(__name__) logging.getLogger("spotipy").setLevel(logging.CRITICAL) +headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36", + "Accept": "application/json", + "Content-Type": "application/json", + "Origin": "https://spotmate.online", + "Referer": "https://spotmate.online/en1", +} + class Banners: def __init__( self, @@ -517,7 +526,6 @@ class SpotifyMod(loader.Module): "autobio": ( "🎧 Spotify autobio {}" ), - "no_ytdlp": " yt-dlp not found... Check config or install yt-dlp ({}terminal pip install yt-dlp)", "snowt_failed": "\n\n Download failed", "uploading_banner": "\n\n🕔 Uploading banner...", "downloading_track": "\n\n🕔 Downloading track...", @@ -531,6 +539,12 @@ class SpotifyMod(loader.Module): "playlist_deleted": " Playlist {} deleted.", "no_playlist_name": " Please specify a playlist name.", "device_select": "📄 Select playback device:", + "on-shuffle": ( + "🔀 Shuffle enabled." + ), + "off-shuffle": ( + "🔀 Shuffle disabled." + ), } strings_ru = { @@ -641,7 +655,6 @@ class SpotifyMod(loader.Module): "🎧 Обновление био" " включено {}" ), - "no_ytdlp": " yt-dlp не найден... Проверьте конфиг или установите yt-dlp ({}terminal pip install yt-dlp)", "snowt_failed": "\n\n Ошибка скачивания.", "uploading_banner": "\n\n🕔 Загрузка баннера...", "downloading_track": "\n\n🕔 Скачивание трека...", @@ -655,6 +668,12 @@ class SpotifyMod(loader.Module): "playlist_deleted": " Плейлист {} удален.", "no_playlist_name": " Пожалуйста, укажите название плейлиста.", "device_select": "📄 Выберите устройство для воспроизведения:", + "on-shuffle": ( + "🔀 Перемешивание включено." + ), + "off-shuffle": ( + "🔀 Перемешивание отключено." + ), } def __init__(self): @@ -700,16 +719,10 @@ class SpotifyMod(loader.Module): lambda: "Template for Spotify AutoBio, supports {artist}, {title}", ), loader.ConfigValue( - "ytdlp_path", - "", - "Path to ytdlp binary", - validator=loader.validators.String(), - ), - loader.ConfigValue( - "cookies_path", - "", - "Path to your cookies for yt-dlp", - validator=loader.validators.String(), + "TimeOut", + 60, + "Response timeout in seconds | Время ожидания ответа в секундах", + validator=loader.validators.Integer(minimum=30), ), loader.ConfigValue( "banner_version", @@ -944,36 +957,102 @@ class SpotifyMod(loader.Module): success = False try: - squery = query.replace('"', '').replace("'", "") - cookies = self.config["cookies_path"] - ytdlp_flags = '-x --audio-format mp3 --audio-quality 0 --add-metadata --format "bestaudio/best" --no-playlist' - cookies_flag = f"--cookies {cookies} " if cookies else "" - cmd = ( - f'{self.config["ytdlp_path"]} {ytdlp_flags} {cookies_flag}' - f'-o "{dl_dir}/%(title)s [%(id)s].%(ext)s" ' - f'"ytsearch1:{squery}"' - ) + track_url = (query or "").strip().split("?")[0] + if "spotify:track:" in track_url: + track_url = f"https://open.spotify.com/track/{track_url.split(':')[-1]}" - proc = await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - _, stderr = await proc.communicate() + if "track/" not in track_url: + results = await asyncio.to_thread( + self.sp.search, + q=query, + limit=1, + type="track", + ) + items = (results or {}).get("tracks", {}).get("items", []) + if not items: + logger.error("SpotifyMod: Spotify track not found for %r", log_context or query) + await send_text(self.strings["snowt_failed"]) + return False - if proc.returncode: - err_text = stderr.decode(errors="ignore").strip() if stderr else "yt-dlp failed" - logger.error("SpotifyMod: yt-dlp code %s for %r: %s", proc.returncode, log_context or query, err_text[-400:]) + track_data = items[0] + track_url = track_data.get("external_urls", {}).get("spotify") or f"https://open.spotify.com/track/{track_data['id']}" - files = [f for f in os.listdir(dl_dir) if f.endswith(".mp3")] - if files: - success = await send_file(os.path.join(dl_dir, files[0])) + async with httpx.AsyncClient(follow_redirects=True) as client: + csrf = await self.get_session(client) + hdrs = {**headers, "X-CSRF-TOKEN": csrf} + + info_res = await client.post( + "https://spotmate.online/getTrackData", + headers=hdrs, + json={"spotify_url": track_url}, + timeout=self.config["TimeOut"], + ) + info = info_res.json() + if info.get("type") != "track": + logger.error("SpotifyMod: spotmate returned no track for %r", log_context or query) + await send_text(self.strings["snowt_failed"]) + return False + + track_id = info.get("id", track_url.split("/")[-1]) + conv_res = await client.post( + "https://spotmate.online/convert", + headers=hdrs, + json={"urls": track_url}, + timeout=self.config["TimeOut"], + ) + conv = conv_res.json() + download_url = conv.get("url") or conv.get("download_url") + task_id = conv.get("task_id") or conv.get("taskId") + + if not download_url and task_id: + for _ in range(40): + await asyncio.sleep(4.5) + task_res = await client.get( + f"https://spotmate.online/tasks/{task_id}", + headers={**hdrs, "Accept": "application/json"}, + timeout=self.config["TimeOut"], + ) + task = task_res.json() + if task.get("error"): + logger.error("SpotifyMod: task error for %r", log_context or query) + await send_text(self.strings["dl_err"]) + return False + + data = task.get("data") or task.get("result") or {} + status = str(data.get("status") or data.get("state") or "").lower() + if status == "finished": + download_url = ( + data.get("url") + or data.get("download_url") + or (data.get("result") or {}).get("url") + or (data.get("result") or {}).get("download_url") + ) + break + + if status in ("failed", "error", "expired", "cancelled"): + logger.error("SpotifyMod: task failed for %r", log_context or query) + await send_text(self.strings["dl_err"]) + return False + + if not download_url: + logger.error("SpotifyMod: download timeout for %r", log_context or query) + await send_text(self.strings["snowt_failed"]) + return False + + file_res = await client.get( + download_url, + headers={"User-Agent": headers["User-Agent"], "Referer": "https://spotmate.online/en1"}, + timeout=self.config["TimeOut"], + ) + + file_path = os.path.join(dl_dir, f"{track_id}.mp3") + with open(file_path, "wb") as f: + f.write(file_res.content) + + success = await send_file(file_path) if not success: logger.error("SpotifyMod: failed to send %r (target=%s)", log_context or query, type(target).__name__) await send_text(self.strings["dl_err"]) - else: - logger.error("SpotifyMod: yt-dlp produced no mp3 for %r", log_context or query) - await send_text(self.strings["snowt_failed"]) except Exception as e: logger.error("Download track error (%s): %s", log_context or "no context", e, exc_info=True) @@ -986,6 +1065,20 @@ class SpotifyMod(loader.Module): return success + async def get_session(self, client: httpx.AsyncClient) -> str: + res = await client.get( + "https://spotmate.online/en1", + headers={ + "User-Agent": headers["User-Agent"], + "Accept": "text/html", + }, + timeout=self.config["TimeOut"], + ) + match = re.search(r'csrf-token[^>]*content="([^"]+)"', res.text) + if not match: + raise ValueError("CSRF token not found") + return match.group(1) + def _short_text(self, text: str, limit: int = 60) -> str: text = " ".join(text.split()) if len(text) <= limit: @@ -1126,7 +1219,13 @@ class SpotifyMod(loader.Module): tracks = results["tracks"]["items"] store_id = id(tracks) - self._sp_store[store_id] = [(t.get("name", "Unknown"), ", ".join(a.get("name", "") for a in t.get("artists", []) if a.get("name")) or "Unknown Artist") for t in tracks] + self._sp_store[store_id] = [ + ( + t.get("name", "Unknown"), + ", ".join(a.get("name", "") for a in t.get("artists", []) if a.get("name")) or "Unknown Artist", + ) + for t in tracks + ] entries = [] for i, track in enumerate(tracks): @@ -1154,7 +1253,7 @@ class SpotifyMod(loader.Module): async def ssearch(self, query): """ - search Spotify track""" return await self._inline_search_tracks(query) - + @error_handler @tokenized @loader.command( @@ -1427,6 +1526,26 @@ class SpotifyMod(loader.Module): self.sp.repeat("context") await utils.answer(message, self.strings["off-repeat"]) + @error_handler + @tokenized + @loader.command( + ru_doc="- 🔀 Включить перемешивание" + ) + async def sshufflecmd(self, message: Message): + """- 🔀 Enable shuffle""" + self.sp.shuffle(True) + await utils.answer(message, self.strings["on-shuffle"]) + + @error_handler + @tokenized + @loader.command( + ru_doc="- 🔀 Отключить перемешивание" + ) + async def sdeshufflecmd(self, message: Message): + """- 🔀 Disable shuffle""" + self.sp.shuffle(False) + await utils.answer(message, self.strings["off-shuffle"]) + @error_handler @tokenized @loader.command( @@ -1730,11 +1849,10 @@ class SpotifyMod(loader.Module): @error_handler @tokenized @loader.command( - ru_doc="| .sq - 🔍 Поиск треков.", - alias="sq" + ru_doc="- 🔍 Поиск треков." ) - async def ssearchcmd(self, message: Message): - """| .sq - 🔍 Search for tracks.""" + async def sqcmd(self, message: Message): + """- 🔍 Search for tracks.""" args = utils.get_args_raw(message) if not args: await utils.answer(message, self.strings["no_search_query"]) @@ -1800,6 +1918,13 @@ class SpotifyMod(loader.Module): ), ) + @error_handler + @tokenized + @loader.command(ru_doc="- 🔍 Поиск треков.") + async def ssearchcmd(self, message: Message): + """- 🔍 Search for tracks.""" + await self.sqcmd(message) + async def watcher(self, message: Message): """Watcher is used to update token""" if not self.sp: @@ -1855,4 +1980,5 @@ class SpotifyMod(loader.Module): refresh_token = await self.invoke("stokrefresh", "", self.inline.bot.id) await refresh_token.delete() else: - self.set("NextRefresh", time.time() + 300) \ No newline at end of file + self.set("NextRefresh", time.time() + 300) +# слендермен