Files
limoka/SunnexGB/Heroku-Modules/everlastingsummer.py
2026-06-12 08:36:40 +00:00

487 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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": "<b>Пролог</b>",
"disclaimer": (
"Игра является плодом фантазии её разработчиков\n"
"и не ставит перед собой цели затронуть или иным\n"
"образом оскорбить кого-либо по религиозному,расовому,\n"
"социальному, экономическому или видовому признаку.\n"
"Также любое ущемление чувства прекрасного, активной\n"
"гражданской позиции или иных высоких душевных порывов\n"
"игроков разработчики оставляют на их совести.\n"
"Совпадения героев с вашими реальными (и воображаемыми)\n"
"знакомыми,соседями,коллегами, тульпами считать случайным.\n"
"Все героини достигли восемнадцатилетнего возраста,\n"
"и они дали письменное согласие на участие в игре\n"
"(выписка из истории болезни сценариста предоставляется по требованию).\n"
"При разработке не пострадало ни одного маскота, животного или человека. Приятной игры!"
),
"bad": "<b>Не удалось загрузить сценарий</b>",
"end": "<b>{}</b>",
"save_header": "<b>Сохранения</b>",
"load_header": "<b>Загрузить игру</b>",
"default_route_question": "<b>Что выберете?</b>",
"or_game": "Игра",
"or_character": "Персонаж",
"cutscene_text": "<tg-emoji emoji-id=5332542518955374404>💫</tg-emoji>",
"opening_title": "<b>Опенинг</b>",
"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"<b>{who}:</b>\n<blockquote>{txt}</blockquote>"
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)