# 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)