# Руководство с примерами кода

# Включение режима разработчика

Будут доступны команды:

  • Shift + D - меню разработчика
  • Shift + O - консоль

WARNING

Для того, чтобы игра распознала сочетания клавиш, нужно переключиться на латинскую раскладку клавиатуры.

init:
    $ config.developer = True
    $ config.console = True
1
2
3

# Автоматическое объявление файлов

Автоматическое объявление всех изображений и звуков мода.

Скачать скрипт

Скачать мод-пример

WARNING

Поддерживается объявление как "резаных", так и "цельных" спрайтов.

"Резаным" спрайтом называется спрайт, у которого каждая часть спрайта (тело, одежда, эмоция и прочее) идёт отдельным изображением, как в БЛ.

"Цельным" спрайтом называется спрайт, у которого тело, одежда, эмоция и прочее идёт одним изображением.

WARNING

Для работы автообъявления необходимо соблюдать иерархию папок с ресурсами.

Место расположения аудиофайлов может быть любым. В корневой папки мода необходимо создать папку images, в которой будут храниться все изображения. Для изображений внутри images создаём ещё одну папку с любым названием (к примеру, bg) и в неё закидываем необходимые для объявления изображения. Поддерживается инициализация изображений в подпапках, так что можно будет внутри создать ещё одну папку (к примеру, subfolder) и туда добавить ещё изображения, вызвать можно по папка подпапка имя_изображения

Для объявления спрайтов внутри images создаём папку sprites. Затем, для каждой дистанции, создаём ещё по папке: normal, close и far соответственно. Если для объявляемых спрайтов есть лишь одна из дистанций, то ненужные папки просто удаляем. Для каждого спрайта создаём папку c любым названием (к примеру, ufo), затем, внутри, для каждой позы по ещё одной папке (цифрой). Если спрайт "резаный", то в папку необходимо поместить изображение с телом, в имени обязательно должно быть указано body (ufo_1_body.png или просто body.png). Для аксессуаров спрайта создаём папку acc, для одежды clothes, для эмоций emo. Название изображений любое.

Пример правильного пути к файлам - mymod\images\sprites\normal\ufo\1.

Параметры:

  • modID: string - название корневой папки мода. Если мод лежит в "mods", то добавить к modID в начале "mods/": "mods/mymod";
  • modPostfix: string, optional - постфикс к названиям объявлённых файлов при необходимости;
  • write_into_file: boolean - если равно True, вместо инициализации записывает ресурсы мода в отдельный файл. Для дальнейшей инициализации ресурсов мода из файла необходимо перезагрузить БЛ. Если равно False, ресурсы мода инициализируются в момент загрузки БЛ.
init python early:
    class autoInitialization:
        """
        Класс для автоматической инициализации файлов мода.
        Инициализирует аудио и изображения (включая спрайты).

        Параметры класса:

            :param modID: str
                название корневой папки Вашего мода
            :param modPostfix: str, optional, :default value: ""
                опциональный параметр для добавления постфикса к названиям объявлённых ресурсов.
            :param write_into_file: boolean, optional, :default value: False
                если равно True, вместо инициализации записывает ресурсы мода в отдельный файл. Для дальнейшей инициализации ресурсов мода из файла необходимо перезагрузить БЛ.
                если равно False, ресурсы мода инициализируются в момент загрузки БЛ.
        """
        def __init__(self, modID, modPostfix="", write_into_file=False):
            """
            Параметры класса:

                :param modID: str
                    название корневой папки Вашего мода
                :param modPostfix: str, optional, :default value: ""
                    опциональный параметр для добавления постфикса к названиям объявлённых ресурсов.
                :param write_into_file: boolean, optional, :default value: False
                    если равно True, вместо инициализации записывает ресурсы мода в отдельный файл. Для дальнейшей инициализации ресурсов мода из файла необходимо перезагрузить БЛ.
                    если равно False, ресурсы мода инициализируются в момент загрузки БЛ.
            """
            self.modID = modID
            self.modPostfix = ("_" + modPostfix if modPostfix else "")
            self.modFiles = []
            self.write_into_file = write_into_file
            self.modDist = self.process_distances()

            self.initialize()

        def count_file(self, type, file_name, file):
            """
            Добавляет название файла, сам файл и его тип в лист modFiles.

            :param type: str
                тип файла
            :param file_name: srt
                имя файла
            :param file: str
                путь до файла
            """
            self.modFiles.append([type, file_name, file])

        def process_mod_path(self):
            """
            Находит путь до папки мода.

            :return: str
            """
            for dir, fn in renpy.loader.listdirfiles(False):
                if self.modID in fn:
                    return os.path.join(dir, self.modID).replace("\\", "/")
                else:
                    for root, dirs, files in os.walk(dir):
                        if self.modID in dirs:
                            return os.path.join(root, self.modID).replace("\\", "/")

        def process_images_path(self):
            """
            Находит путь до папки изображений мода.

            :return: str
            """
            return os.path.join(self.process_mod_path(), 'images').replace("\\", "/")

        def process_distances(self):
            """
            Находит путь до папки sprites, строит названия дистанций по именам внутри (для normal дистанции имя будет "", как в самом БЛ), ищет изображение в каждой из папок с дистанциями, получает размер изображения и добавляет в словарь
            
            :return: dict

            Пример возврата функции:
            {
                "far": {"far", (675, 1080)},
                "normal": {"", (900, 1080)},
                "close": {"close", (1125, 1080)},
            }
            """
            folder_names = {}
            path = os.path.join(self.process_images_path(), "sprites")
            for name in os.listdir(path):
                full_path = os.path.join(path, name).replace("\\", "/")
                if os.path.isdir(full_path):
                    for root, dirs, files in os.walk(full_path):
                        for file in files:
                            image_path = os.path.join(root, file).replace("\\", "/")
                            image_size = renpy.image_size(image_path)
                            folder_names[name] = (name if name != "normal" else "", image_size)
                            break
                        else:
                            continue
                        break
            return folder_names

        def process_audio(self):
            """
            Обрабатывает аудио. Поддерживает расширения (".wav", ".mp2", ".mp3", ".ogg", ".opus")

            Имя аудио для вызова будет в формате:
            [имя][_постфикс]

            Пример:
            newmusic
            """
            audio_extensions = {".wav", ".mp2", ".mp3", ".ogg", ".opus"}
            for file in renpy.list_files():
                if self.modID in file:
                    file_name = os.path.splitext(os.path.basename(file))[0] + self.modPostfix
                    if file.endswith(tuple(audio_extensions)):
                        self.count_file("sound", file_name, file)
    
        def process_images(self):
            """
            Обрабатывает изображения. Поддерживает изображения в подпапках.

            Имя изображения для вызова будет в формате:
            [папка] [подпапка] [имя][_постфикс]

            Пример:
            bg background
            bg subfolder background
            bg subfolder subsubfolder background
            """
            mod_imgs_path = self.process_images_path()
            for folder in os.listdir(mod_imgs_path):
                path = os.path.join(mod_imgs_path, folder).replace("\\", "/")
                if folder != 'sprites':
                    for root, dirs, files in os.walk(path):
                        for file in files:
                            image_path = os.path.join(root, file).replace("\\", "/")
                            image_name = os.path.splitext(file)[0]
                            relative_path = os.path.relpath(root, mod_imgs_path) # Получаем полный путь к изображению и удаляем путь к корню
                            folder_structure = relative_path.split(os.sep) # Разделяем путь на компоненты и объединяем их в имя изображения
                            folder_index = folder_structure.index(folder)
                            folder_structure = folder_structure[folder_index:] + [image_name] # Оставляем только элементы после папки folder
                            image_name_with_folder = ' '.join(folder_structure).replace('/', '').replace('\\', '') + self.modPostfix
                            image_path = os.path.relpath(image_path, renpy.loader.listdirfiles(False)[0][0]).replace(os.sep, "/")
                            self.count_file("image", image_name_with_folder, image_path)
                else:
                    self.process_sprites(path)

        def process_sprite_clothes_emo_acc(self, emo_l, clothes_l, acc_l, who, file_body, dist):
            """Обрабатывает спрайт [тело] [эмоция] [одежда] [аксессуар]"""
            for emotion in emo_l:
                for clothes in clothes_l:
                    for acc in acc_l:
                        file_name = who + self.modPostfix + ' ' + emotion[0] + ' ' + clothes[0] + ' ' + acc[0] + ' ' + self.modDist[dist][0]
                        file = """
                            ConditionSwitch(
                                "persistent.sprite_time=='sunset'",
                                im.MatrixColor(im.Composite({0},
                                                            (0, 0), "{1}",
                                                            (0, 0), "{2}",
                                                            (0, 0), "{3}",
                                                            (0, 0), "{4}"),
                                                im.matrix.tint(0.94, 0.82, 1.0)
                                            ),
                                "persistent.sprite_time=='night'",
                                im.MatrixColor(im.Composite({0},
                                                            (0, 0), "{1}",
                                                            (0, 0), "{2}",
                                                            (0, 0), "{3}",
                                                            (0, 0), "{4}"),
                                                im.matrix.tint(0.63, 0.78, 0.82)
                                            ),
                                True,
                                im.Composite({0},
                                            (0, 0), "{1}",
                                            (0, 0), "{2}",
                                            (0, 0), "{3}",
                                            (0, 0), "{4}")
                            )
                        """.format(self.modDist[dist][1], file_body, clothes[1], emotion[1], acc[1])
                        self.count_file("sprite", file_name, file)

            self.process_sprite_clothes_emo(emo_l, clothes_l, who, file_body, dist)
            self.process_sprite_clothes_acc(clothes_l, acc_l, who, file_body, dist)
            self.process_sprite_emo_acc(emo_l, acc_l,  who, file_body, dist)
            self.process_sprite_emo(emo_l, who, file_body, dist)
            self.process_sprite_acc(acc_l, who, file_body, dist)
            self.process_sprite_clothes(clothes_l, who, file_body, dist)

        def process_sprite_clothes_emo(self, emo_l, clothes_l, who, file_body, dist):
            """Обрабатывает спрайт [тело] [эмоция] [одежда]"""
            for clothes in clothes_l:
                for emotion in emo_l:
                    file_name = who + self.modPostfix + ' ' + emotion[0] + ' ' + clothes[0] + ' ' + self.modDist[dist][0]
                    file = """
                        ConditionSwitch(
                            "persistent.sprite_time=='sunset'",
                            im.MatrixColor(im.Composite({0},
                                                        (0, 0), "{1}",
                                                        (0, 0), "{2}",
                                                        (0, 0), "{3}"),
                                            im.matrix.tint(0.94, 0.82, 1.0)
                                        ),
                            "persistent.sprite_time=='night'",
                            im.MatrixColor(im.Composite({0},
                                                        (0, 0), "{1}",
                                                        (0, 0), "{2}",
                                                        (0, 0), "{3}"),
                                            im.matrix.tint(0.63, 0.78, 0.82)
                                        ),
                            True,
                            im.Composite({0},
                                        (0, 0), "{1}",
                                        (0, 0), "{2}",
                                        (0, 0), "{3}")
                        )
                    """.format(self.modDist[dist][1], file_body, clothes[1], emotion[1])
                    self.count_file("sprite", file_name, file)
            self.process_sprite_clothes(clothes_l, who, file_body, dist)
            self.process_sprite_emo(emo_l, who, file_body, dist)

        def process_sprite_clothes_acc(self, clothes_l, acc_l, who, file_body, dist):
            """Обрабатывает спрайт [тело] [одежда] [аксессуар]"""
            for clothes in clothes_l:
                for acc in acc_l:
                    file_name = who + self.modPostfix + ' ' + clothes[0] + ' ' + acc[0] + ' ' + self.modDist[dist][0]
                    file = """
                        ConditionSwitch(
                            "persistent.sprite_time=='sunset'",
                            im.MatrixColor(im.Composite({0},
                                                        (0, 0), "{1}",
                                                        (0, 0), "{2}",
                                                        (0, 0), "{3}"),
                                            im.matrix.tint(0.94, 0.82, 1.0)
                                        ),
                            "persistent.sprite_time=='night'",
                            im.MatrixColor(im.Composite({0},
                                                        (0, 0), "{1}",
                                                        (0, 0), "{2}",
                                                        (0, 0), "{3}"),
                                            im.matrix.tint(0.63, 0.78, 0.82)
                                        ),
                            True,
                            im.Composite({0},
                                        (0, 0), "{1}",
                                        (0, 0), "{2}",
                                        (0, 0), "{3}")
                        )
                    """.format(self.modDist[dist][1], file_body, clothes[1], acc[1])
                    self.count_file("sprite", file_name, file)
            self.process_sprite_clothes(clothes_l, who, file_body, dist)
            self.process_sprite_acc(acc_l, who, file_body, dist)

        def process_sprite_emo_acc(self, emo_l, acc_l, who, file_body, dist):
            """Обрабатывает спрайт [тело] [эмоция] [аксессуар]"""
            for emotion in emo_l:
                for acc in acc_l:
                    file_name = who + self.modPostfix + ' ' + emotion[0] + ' ' + acc[0] + ' ' + self.modDist[dist][0]
                    file = """
                        ConditionSwitch(
                            "persistent.sprite_time=='sunset'",
                            im.MatrixColor(im.Composite({0},
                                                        (0, 0), "{1}",
                                                        (0, 0), "{2}",
                                                        (0, 0), "{3}"),
                                            im.matrix.tint(0.94, 0.82, 1.0)
                                        ),
                            "persistent.sprite_time=='night'",
                            im.MatrixColor(im.Composite({0},
                                                        (0, 0), "{1}",
                                                        (0, 0), "{2}",
                                                        (0, 0), "{3}"),
                                            im.matrix.tint(0.63, 0.78, 0.82)
                                        ),
                            True,
                            im.Composite({0},
                                        (0, 0), "{1}",
                                        (0, 0), "{2}",
                                        (0, 0), "{3}")
                        )
                    """.format(self.modDist[dist][1], file_body, emotion[1], acc[1])
                    self.count_file("sprite", file_name, file)
            self.process_sprite_emo(emo_l, who, file_body, dist)
            self.process_sprite_acc(acc_l, who, file_body, dist)

        def process_sprite_clothes(self, clothes_l, who, file_body, dist):
            """Обрабатывает спрайт [тело] [одежда]"""
            for clothes in clothes_l:
                file_name = who + self.modPostfix + ' ' + clothes[0] + ' ' + self.modDist[dist][0]
                file = """
                    ConditionSwitch(
                        "persistent.sprite_time=='sunset'",
                        im.MatrixColor(im.Composite({0},
                                                    (0, 0), "{1}",
                                                    (0, 0), "{2}"),
                                        im.matrix.tint(0.94, 0.82, 1.0)
                                    ),
                        "persistent.sprite_time=='night'",
                        im.MatrixColor(im.Composite({0},
                                                    (0, 0), "{1}",
                                                    (0, 0), "{2}"),
                                        im.matrix.tint(0.63, 0.78, 0.82)
                                    ),
                        True,
                        im.Composite({0},
                                    (0, 0), "{1}",
                                    (0, 0), "{2}")
                    )
                """.format(self.modDist[dist][1], file_body, clothes[1])
                self.count_file("sprite", file_name, file)

        def process_sprite_acc(self, acc_l, who, file_body, dist):
            """Обрабатывает спрайт [тело] [аксессуар]"""
            for acc in acc_l:
                file_name = who + self.modPostfix + ' ' + acc[0] + ' ' + self.modDist[dist][0]
                file = """
                    ConditionSwitch(
                        "persistent.sprite_time=='sunset'",
                        im.MatrixColor(im.Composite({0},
                                                    (0, 0), "{1}",
                                                    (0, 0), "{2}"),
                                        im.matrix.tint(0.94, 0.82, 1.0)
                                    ),
                        "persistent.sprite_time=='night'",
                        im.MatrixColor(im.Composite({0},
                                                    (0, 0), "{1}",
                                                    (0, 0), "{2}"),
                                        im.matrix.tint(0.63, 0.78, 0.82)
                                    ),
                        True,
                        im.Composite({0},
                                    (0, 0), "{1}",
                                    (0, 0), "{2}")
                    )
                """.format(self.modDist[dist][1], file_body, acc[1])
                self.count_file("sprite", file_name, file)

        def process_sprite_emo(self, emo_l, who, file_body, dist):
            """Обрабатывает спрайт [тело] [эмоция]"""
            for emotion in emo_l:
                file_name = who + self.modPostfix + ' ' + emotion[0] + ' ' + self.modDist[dist][0]
                file = """
                    ConditionSwitch(
                        "persistent.sprite_time=='sunset'",
                        im.MatrixColor(im.Composite({0},
                                                    (0, 0), "{1}",
                                                    (0, 0), "{2}"),
                                        im.matrix.tint(0.94, 0.82, 1.0)
                                    ),
                        "persistent.sprite_time=='night'",
                        im.MatrixColor(im.Composite({0},
                                                    (0, 0), "{1}",
                                                    (0, 0), "{2}"),
                                        im.matrix.tint(0.63, 0.78, 0.82)
                                    ),
                        True,
                        im.Composite({0},
                                    (0, 0), "{1}",
                                    (0, 0), "{2}")
                    )
                """.format(self.modDist[dist][1], file_body, emotion[1])
                self.count_file("sprite", file_name, file)

        def process_sprite(self, who, file_body, dist):
            """Обрабатывает спрайт [тело]"""
            file_name = "{}{} {}".format(who, self.modPostfix, self.modDist[dist][0])
            file = """
                ConditionSwitch(
                    "persistent.sprite_time=='sunset'",
                    im.MatrixColor(im.Composite({0},
                                                (0, 0), "{1}"),
                                    im.matrix.tint(0.94, 0.82, 1.0)
                                ),
                    "persistent.sprite_time=='night'",
                    im.MatrixColor(im.Composite({0},
                                                (0, 0), "{1}"),
                                    im.matrix.tint(0.63, 0.78, 0.82)
                                ),
                    True,
                    im.Composite({0},
                                (0, 0), "{1}")
                )
            """.format(self.modDist[dist][1], file_body)
            self.count_file("sprite", file_name, file)

        def process_sprites(self, path):
            """Обрабатывает спрайты и все их комбинации
            
            Имя спрайта для вызова будет в формате:
            [название спрайта][_постфикс]
            [название спрайта][_постфикс] [эмоция]
            [название спрайта][_постфикс] [эмоция] [одежда]
            [название спрайта][_постфикс] [эмоция] [одежда] [аксессуар]
            и любые другие комбинации.

            Пример:
            dv
            dv normal
            dv normal sport
            dv normal sport jewelry
            """
            for dist in os.listdir(path):
                who_path = os.path.join(path, dist).replace("\\", "/")
                for who in os.listdir(who_path):
                    who_path_num = os.path.join(who_path, who).replace("\\", "/")
                    for numb in os.listdir(who_path_num):
                        sprite_folders = os.listdir(os.path.join(who_path_num, numb).replace("\\", "/"))

                        for i in sprite_folders:
                            if 'body' in i:
                                file_body = os.path.relpath(os.path.join(who_path_num, numb, i).replace("\\", "/"), renpy.loader.listdirfiles(False)[0][0]).replace(os.sep, "/")
                                break
                        else:
                            file_body = im.Alpha("images/misc/soviet_games.png", 0.0) # Заглушка, если не нашли тело

                        clothes_l = []
                        emo_l = []
                        acc_l = []

                        if 'clothes' in sprite_folders:
                            clothes_l = [(os.path.splitext(clothes)[0].split('_'+numb+"_", 1)[-1], os.path.relpath(os.path.join(who_path_num, numb, 'clothes', clothes).replace("\\", "/"), renpy.loader.listdirfiles(False)[0][0]).replace(os.sep, "/")) for clothes in os.listdir(os.path.join(who_path_num, numb, 'clothes'))]

                        if 'emo' in sprite_folders:
                            emo_l = [(os.path.splitext(emo)[0].split('_'+numb+"_", 1)[-1], os.path.relpath(os.path.join(who_path_num, numb, 'emo', emo).replace("\\", "/"), renpy.loader.listdirfiles(False)[0][0]).replace(os.sep, "/")) for emo in os.listdir(os.path.join(who_path_num, numb, 'emo'))]

                        if 'acc' in sprite_folders:
                            acc_l = [(os.path.splitext(acc)[0].split('_'+numb+"_", 1)[-1], os.path.relpath(os.path.join(who_path_num, numb, 'acc', acc).replace("\\", "/"), renpy.loader.listdirfiles(False)[0][0]).replace(os.sep, "/")) for acc in os.listdir(os.path.join(who_path_num, numb, 'acc'))]

                        self.process_sprite(who, file_body, dist)
                        if clothes_l and emo_l and acc_l:
                            self.process_sprite_clothes_emo_acc(emo_l, clothes_l, acc_l, who, file_body, dist)
                        elif clothes_l and emo_l:
                            self.process_sprite_clothes_emo(emo_l, clothes_l, who, file_body, dist)
                        elif clothes_l and acc_l:
                            self.process_sprite_clothes_acc(clothes_l, acc_l, who, file_body, dist)
                        elif emo_l and acc_l:
                            self.process_sprite_emo_acc(emo_l, acc_l,  who, file_body, dist)
                        elif clothes_l:
                            self.process_sprite_clothes(clothes_l, who, file_body, dist)
                        elif acc_l:
                            self.process_sprite_acc(acc_l, who, file_body, dist)
                        elif emo_l:
                            self.process_sprite_emo(emo_l, who, file_body, dist)

        def process_files(self):
            """
            Обрабатывает файлы мода.

            Если write_into_file равно True, вместо инициализации записывает ресурсы мода в отдельный файл. Для дальнейшей инициализации ресурсов мода из файла необходимо перезагрузить БЛ.
            """
            if self.write_into_file:
                with open(self.process_mod_path() + "/autoinit_assets.rpy", "w") as log_file:
                    log_file.write("init python:\n    ")
                    for type, file_name, file in self.modFiles:
                        if type == "sound":
                            log_file.write("%s = \"%s\"\n    " % (file_name, file))
                        elif type == "image":
                            log_file.write("renpy.image(\"%s\", \"%s\")\n    " % (file_name, file))
                        if type == "sprite":
                            log_file.write("renpy.image(\"%s\", %s)\n    " % (file_name, file))
            else:
                for type, file_name, file in self.modFiles:
                    if type == "sound":
                        globals()[file_name] = file
                    elif type == "image":
                        renpy.image(file_name, file)
                    if type == "sprite":
                        renpy.image(file_name, eval(file))

        def initialize(self):
            """
            Инициализация ресурсов мода
            """
            self.process_audio()
            self.process_images()
            self.process_files()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474

# Пример использования

Прежде всего нам необходимо создать объект класса:

init:
    $ autoinitialization_mymod = autoInitialization("mymod") # Создаём объект класса с mymod в качестве корневой директории мода.
1
2

При необходимости можем добавить постфикс для объявленных файлов или вовсе вместо объявления записывать их в отдельный файл autoinit_assets.rpy (чтобы объявления ресурсов, записанных в файл, сработало, необходимо перезагрузить БЛ, чтобы rpy файл скомпилировался).

init:
    $ autoinitialization_mymod = autoInitialization("mymod", "myPostfix")
1
2
init:
    $ autoinitialization_mymod = autoInitialization("mymod", write_into_file=True)
1
2

# Изображения

show bg ext_square_sunset # Показ изображения ext_square_sunset из папки bg
1

# Изображения (с префиксом)

show bg ext_square_sunset_myPostfix # Показ изображения ext_square_sunset из папки bg с префиксом mymod
1

# Спрайты

show ufo dress smile jewelry far # Показ спрайта персонажа ufo в одежде dress, эмоцией smile, аксессуаром jewelry и дистанцией far
show ufo dress smile jewelry #  Показ спрайта персонажа ufo в одежде dress, эмоцией smile, аксессуаром jewelry и дистанцией normal
show ufo dress smile jewelry close # Показ спрайта персонажа ufo в одежде dress, эмоцией smile, аксессуаром jewelry и дистанцией close
show ufo dress smile # Показ спрайта персонажа ufo в одежде dress, эмоцией smile и дистанцией normal
show ufo dress # Показ спрайта персонажа ufo в одежде dress и дистанцией normal
show ufo # Показ спрайта персонажа ufo с дистанцией normal
1
2
3
4
5
6

# Спрайты (с префиксом)

show ufo_myPostfix dress smile jewelry far # Показ спрайта персонажа ufo в одежде dress, эмоцией smile, аксессуаром jewelry, дистанцией far и постфиксом myPostfix
show ufo_myPostfix dress smile jewelry #  Показ спрайта персонажа ufo в одежде dress, эмоцией smile, аксессуаром jewelry, дистанцией normal и постфиксом myPostfix
show ufo_myPostfix dress smile jewelry close # Показ спрайта персонажа ufo в одежде dress, эмоцией smile, аксессуаром jewelry, дистанцией close и постфиксом myPostfix
show ufo_myPostfix dress smile # Показ спрайта персонажа ufo в одежде dress, эмоцией smile, дистанцией normal и постфиксом myPostfix
show ufo_myPostfix dress # Показ спрайта персонажа ufo в одежде dress, дистанцией normal и постфиксом myPostfix
show ufo_myPostfix # Показ спрайта персонажа ufo с дистанцией normal и постфиксом myPostfix
1
2
3
4
5
6

# Аудио

play sound mymusic # Воспроизведение файла mymusic на канале sound
1

# Аудио (с префиксом)

play sound mymusic_myPostfix # Воспроизведение файла mymusic на канале sound и постфиксом myPostfix
1

# Заключение

Если необходима дополнительная информация, каждый метод класса содержит подробные комментарии работы с примерами вызова объявленных ресурсов мода.

# Автоматическое объявление файлов (для начинающих)

Данный отрезок кода автоматически объявляет все изображения и звуки Вашего мода.

Скачать скрипт

WARNING

Поддерживается объявление только "цельных" спрайтов.

"Цельным" спрайтом называется спрайт, у которого тело, одежда, эмоция и прочее идёт одним изображением, а не каждая часть спрайта отдельно, как в БЛ.

Параметры:

  • mod_folder: string - название папки, в которой находится мод;
  • sprites_folder: string - название папки, в которой хранятся спрайты, все спрайты в папке будут автоматически окрашены в зависимости от установленного времени суток (persistent.sprite_time).
init python:
    from os import path

    def define_assets(mod_folder, sprites_folder):
        for file in renpy.list_files():
            if mod_folder in file:
                file_name = path.splitext(path.basename(file))[0]

                if file.endswith((".png", ".jpg")):
                    if sprites_folder and '%s/%s' % (mod_folder, sprites_folder) in file:
                            renpy.image(
                                file_name,
                                ConditionSwitch(
                                    "persistent.sprite_time == 'sunset'",
                                    im.MatrixColor(
                                        file,
                                        im.matrix.tint(0.94, 0.82, 1.0)
                                    ),
                                    "persistent.sprite_time == 'night'",
                                    im.MatrixColor(
                                        file,
                                        im.matrix.tint(0.63, 0.78, 0.82)
                                    ),
                                    "True", file
                                )
                            )
                    else:
                        renpy.image(file_name, file)
                elif file.endswith((".wav", ".mp2", ".mp3", ".ogg", ".opus", ".webm", ".flv", ".vob")):
                    globals()[file_name] = file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# Пример использования

# Объявление ресурсов

init:
    # Первый параметр - папка, в которой хранится мод
    # Например, если название папки - `my_mod`, то:
    $ define_assets('my_mod')
1
2
3
4

# Объявление ресурсов с покраской спрайтов

init:
    # Если мод находится в папке `my_mod`,
    # а спрайты - в `my_mod/images/sprites`, то:
    $ define_assets('my_mod', sprites_folder='images/sprites')
1
2
3
4

# Автоматическое объявление персонажей и интересные плюшки

Позволяет автоматически объявить персонажей с БЛ-like стилем текста, исключая возможность создания конфликтов с другими модами. Не забудьте заменить mymod на свой вариант.

Скачать скрипт

Создаём словарь с персонажами, добавляем в него персонажей из оригинала, а затем добавляем своих.

Пример добавления: "переменная_персонажа":[u"Имя персонажа", "HEX цвет имени персонажа"]

init -1 python:
    characters_mymod = { # Словарь с персонажами
        # основные
        "narrator":[None, None],
        "th":[None, None],
        "me":[u"Семён", "#E1DD7D"],
        # персонажи оригинала
        "mi":[u"Мику", "#00DEFF"],
        "us":[u"Ульяна", "#FF3200"],
        "dv":[u"Алиса", "#FFAA00"],
        "mt":[u"Ольга Дмитриевна", "#00EA32"],
        "mz":[u"Женя", "#4A86FF"],
        "sh":[u"Шурик", "#FFF226"],
        "sl":[u"Славя", "#FFD200"],
        "el":[u"Электроник", "#FFFF00"],
        "un":[u"Лена", "#B956FF"],
        "cs":[u"Виола", "#A5A5FF"],
        "pi":[u"Пионер", "#E60000"],
        "uv":[u"Юля", "#4EFF00"],
        "voice":[u"Голос", "#e1dd7d"],
        # новые персонажи
        "new":[u"Новый персонаж", "#FF3200"],
        "new2":[u"Новый персонаж2", "#B956FF"]
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

Создаём функцию, объявляющую весь словарь с нашими персонажами

init python:
    def chars_define_mymod(kind=adv):
        gl = globals()
        if kind == nvl:
            who_suffix = ":"
            ctc = "ctc_animation_nvl"
        else:
            who_suffix = ""
            ctc = "ctc_animation"
        what_color = "#FFDD7D" # Цвет текста персонажа
        drop_shadow = (2, 2) # Наложение тени на текст
        for i, j in characters_mymod.items():
            if i == "narrator":
                gl[i] = Character(None, kind=kind, what_color=what_color, what_drop_shadow=drop_shadow, ctc=ctc, ctc_position="fixed")
            elif i == "th":
                gl[i] = Character(None, kind=kind, what_color=what_color, what_drop_shadow=drop_shadow, what_prefix="~ ", what_suffix=" ~", ctc=ctc, ctc_position="fixed")
            else:
                gl[i] = Character(j[0], kind=kind, who_color=j[1], who_drop_shadow=drop_shadow, who_suffix=who_suffix, what_color=what_color, what_drop_shadow=drop_shadow, ctc=ctc, ctc_position="fixed")
                # Добавлено дополнительное объявление персонажей, которые будут сохранять оригинальный цвет имени персонажа, но изменять его имя.
                # Полезно, когда ГГ в моде ещё не знаком с новыми пионерами, но забивать словарь мусором не хочется.
                # Пример использования - "new_v" - имя "Новый персонаж" меняется на "Голос", "new_pm" - "Пионер", "new_pg" - "Пионерка"
                gl[i+"_v"] = Character(u"Голос", kind=kind, who_color=j[1], who_drop_shadow=drop_shadow, who_suffix=who_suffix, what_color=what_color, what_drop_shadow=drop_shadow, ctc=ctc, ctc_position="fixed")
                gl[i+"_pm"] = Character(u"Пионер", kind=kind, who_color=j[1], who_drop_shadow=drop_shadow, who_suffix=who_suffix, what_color=what_color, what_drop_shadow=drop_shadow, ctc=ctc, ctc_position="fixed")
                gl[i+"_pg"] = Character(u"Пионерка", kind=kind, who_color=j[1], who_drop_shadow=drop_shadow, who_suffix=who_suffix, what_color=what_color, what_drop_shadow=drop_shadow, ctc=ctc, ctc_position="fixed")
            if renpy.mobile:
                colors[i] = {'night': j[1], 'sunset': j[1], 'day': j[1], 'prolog': j[1]}
                names[i] = j[0]
                store.names_list.append(i)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# Пример использования

label mymod:
    $ chars_define_mymod() # В самом начале первого лейбла мода объявляем персонажей во избежание конфликтов с персонажами из других модов
    new "Я — новый персонаж!"
1
2
3

# Добавление поддержки NVL-режима

    def set_mode_mymod(mode=adv): # Переключение между ADV и NVL режимами
        nvl_clear()
        chars_define_mymod(kind=mode)
        if renpy.mobile:
            if mode == adv:
                set_mode_adv()
            else:
                set_mode_nvl()
1
2
3
4
5
6
7
8

# Использование

label mymod:
    new "Говорю в ADV-режиме."

    window hide
    $ set_mode_mymod(nvl)
    pause(1) # Пауза для плавного перехода
    window show

    new "Говорю в NVL-режиме."
1
2
3
4
5
6
7
8
9

# Изменение имени и цвета персонажа во время игры

    def set_name_mymod(name, value, mode=adv): # Изменение имени персонажа
        characters_mymod[name][0] = value
        chars_define_mymod(mode)
        if renpy.mobile:
            if mode == nvl:
                set_mode_nvl()
            else:
                set_mode_adv()

    def set_char_color_mymod(name, value, mode=adv): # Изменение цвета имени персонажа
        characters_mymod[name][1] = value
        chars_define_mymod(mode)
        if renpy.mobile:
            if mode == nvl:
                set_mode_nvl()
            else:
                set_mode_adv()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Использование

label mymod:
    new "Моё имя - 'Новый Персонаж'"

    $ set_name_mymod("new", "Новое имя")

    new "Моё имя 'Новое имя'"

    $ set_char_color_mymod("new", "#4A86FF")

    new "Цвет моего имени изменился!"
1
2
3
4
5
6
7
8
9
10

# Полный вид кода

init -1 python:
    characters_mymod = { # Словарь с персонажами
        # основные
        "narrator":[None, None],
        "th":[None, None],
        "me":[u"Семён", "#E1DD7D"],
        # персонажи оригинала
        "mi":[u"Мику", "#00DEFF"],
        "us":[u"Ульяна", "#FF3200"],
        "dv":[u"Алиса", "#FFAA00"],
        "mt":[u"Ольга Дмитриевна", "#00EA32"],
        "mz":[u"Женя", "#4A86FF"],
        "sh":[u"Шурик", "#FFF226"],
        "sl":[u"Славя", "#FFD200"],
        "el":[u"Электроник", "#FFFF00"],
        "un":[u"Лена", "#B956FF"],
        "cs":[u"Виола", "#A5A5FF"],
        "pi":[u"Пионер", "#E60000"],
        "uv":[u"Юля", "#4EFF00"],
        "voice":[u"Голос", "#e1dd7d"],
        # новые персонажи
        "new":[u"Новый персонаж", "#FF3200"]
        }

init python:
    def chars_define_mymod(kind=adv):
        gl = globals()
        if kind == nvl:
            who_suffix = ":"
            ctc = "ctc_animation_nvl"
        else:
            who_suffix = ""
            ctc = "ctc_animation"
        what_color = "#FFDD7D" # Цвет текста персонажа
        drop_shadow = (2, 2) # Наложение тени на текст
        for i, j in characters_mymod.items():
            if i == "narrator":
                gl[i] = Character(None, kind=kind, what_color=what_color, what_drop_shadow=drop_shadow, ctc=ctc, ctc_position="fixed")
            elif i == "th":
                gl[i] = Character(None, kind=kind, what_color=what_color, what_drop_shadow=drop_shadow, what_prefix="~ ", what_suffix=" ~", ctc=ctc, ctc_position="fixed")
            else:
                gl[i] = Character(j[0], kind=kind, who_color=j[1], who_drop_shadow=drop_shadow, who_suffix=who_suffix, what_color=what_color, what_drop_shadow=drop_shadow, ctc=ctc, ctc_position="fixed")
                # Добавлено дополнительное объявление персонажей, которые будут сохранять оригинальный цвет имени персонажа, но изменять его имя.
                # Полезно, когда ГГ в моде ещё не знаком с новыми пионерами, но забивать словарь мусором не хочется.
                # Пример использования - "new_v" - имя "Новый персонаж" меняется на "Голос", "new_pm" - "Пионер", "new_pg" - "Пионерка"
                gl[i+"_v"] = Character(u"Голос", kind=kind, who_color=j[1], who_drop_shadow=drop_shadow, who_suffix=who_suffix, what_color=what_color, what_drop_shadow=drop_shadow, ctc=ctc, ctc_position="fixed")
                gl[i+"_pm"] = Character(u"Пионер", kind=kind, who_color=j[1], who_drop_shadow=drop_shadow, who_suffix=who_suffix, what_color=what_color, what_drop_shadow=drop_shadow, ctc=ctc, ctc_position="fixed")
                gl[i+"_pg"] = Character(u"Пионерка", kind=kind, who_color=j[1], who_drop_shadow=drop_shadow, who_suffix=who_suffix, what_color=what_color, what_drop_shadow=drop_shadow, ctc=ctc, ctc_position="fixed")
            if renpy.mobile:
                colors[i] = {'night': j[1], 'sunset': j[1], 'day': j[1], 'prolog': j[1]}
                names[i] = j[0]
                store.names_list.append(i)

    def set_mode_mymod(mode=adv): # Переключение между ADV и NVL режимами
        nvl_clear()
        chars_define_mymod(kind=mode)
        if renpy.mobile:
            if mode == adv:
                set_mode_adv()
            else:
                set_mode_nvl()

    def set_name_mymod(name, value, mode=adv): # Изменение имени персонажа
        characters_mymod[name][0] = value
        chars_define_mymod(mode)
        if renpy.mobile:
            if mode == nvl:
                set_mode_nvl()
            else:
                set_mode_adv()

    def set_char_color_mymod(name, value, mode=adv): # Изменение цвета имени персонажа
        characters_mymod[name][1] = value
        chars_define_mymod(mode)
        if renpy.mobile:
            if mode == nvl:
                set_mode_nvl()
            else:
                set_mode_adv()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

# Эффект падающих частиц

В коде игры уже предусмотрено использование частиц - снега. Имеются два варианта:

  • snow - image snow = Snow("images/anim/snow.png").
  • heavy_snow - image heavy_snow = Snow("images/anim/snow.png", max_particles=500).

Эти два варианта уже объявлены в игре глобально и могут использоваться как изображение (opens new window), объявленное с помощью Image Statement (opens new window).

Пример использования из игры:





 
 




 
 


label epilogue_sl:
    "..."
    window hide
    scene bg bus_stop
    show snow
    with fade2
    window show
    "..."
    window hide
    hide snow
    show heavy_snow
    with fade
    window show
1
2
3
4
5
6
7
8
9
10
11
12
13

Вы также можете создать изображение со своими частицами (например, каплями дождя):

define image rain = Snow("<путь к изображению>", max_particles=50, speed=150, wind=100, xborder=(0,100), yborder=(50,400))
1

В примере выше указаны параметры, заданные по умолчанию. Вы можете изменить их по вашему желанию:

  • image: String - путь к изображению, которое будет использоваться как частица.
  • max_particles: Int - максимальное количество частиц одновременно на экране.
  • speed: Float - скорость вертикального полёта частиц. Чем больше значение, тем быстрее частицы будут падать.
  • wind: Float - максимальная сила ветра, которая будет взаимодействовать с частицами.
  • xborder: (min: Int, max: Int): Tuple - горизонтальные границы, в которых будут случайно появляться частицы. По умолчанию - весь экран.
  • yborder: (min: Int, max: Int): Tuple - вертикальные границы, в которых будут случайно появляться частицы. По умолчанию - весь экран.
Реализация функции `Snow` в игре

Скачать скрипт

init python:
    import random

    random.seed()


    def Snow(
        image,
        max_particles=50,
        speed=150,
        wind=100,
        xborder=(0, 100),
        yborder=(50, 400),
        **kwargs
    ):
        """
        This creates the snow effect. You should use this function instead of instancing
        the SnowFactory directly (we'll, doesn't matter actually, but it saves typing if you're
        using the default values =D)

        @parm {image} image:
            The image used as the snowflakes. This should always be a image file or an im object,
            since we'll apply im transformations in it.

        @parm {int} max_particles:
            The maximum number of particles at once in the screen.

        @parm {float} speed:
            The base vertical speed of the particles. The higher the value, the faster particles will fall.
            Values below 1 will be changed to 1

        @parm {float} wind:
            The max wind force that'll be applyed to the particles.

        @parm {Tuple ({int} min, {int} max)} xborder:
            The horizontal border range. A random value between those two will be applyed when creating particles.

        @parm {Tuple ({int} min, {int} max)} yborder:
            The vertical border range. A random value between those two will be applyed when creating particles.
            The higher the values, the fartest from the screen they will be created.
        """
        return Particles(
            SnowFactory(image, max_particles, speed, wind, xborder, yborder, **kwargs)
        )


    class SnowFactory(object):
        """
        The factory that creates the particles we use in the snow effect.
        """

        def __init__(self, image, max_particles, speed, wind, xborder, yborder, **kwargs):
            """
            Initialize the factory. Parameters are the same as the Snow function.
            """

            self.max_particles = max_particles
            self.speed = speed
            self.wind = wind
            self.xborder = xborder
            self.yborder = yborder
            self.depth = kwargs.get("depth", 10)
            self.image = self.image_init(image)

        def create(self, particles, st):
            """
            This is internally called every frame by the Particles object to create new particles.
            We'll just create new particles if the number of particles on the screen is
            lower than the max number of particles we can have.
            """

            if particles is None or len(particles) < self.max_particles:
                depth = random.randint(1, self.depth)
                depth_speed = 1.5 - depth / (self.depth + 0.0)

                return [
                    SnowParticle(
                        self.image[depth - 1],
                        random.uniform(-self.wind, self.wind) * depth_speed,
                        self.speed * depth_speed,
                        random.randint(self.xborder[0], self.xborder[1]),
                        random.randint(self.yborder[0], self.yborder[1]),
                    )
                ]

        def image_init(self, image):
            """
            This is called internally to initialize the images.
            will create a list of images with different sizes, so we
            can predict them all and use the cached versions to make it more memory efficient.
            """
            rv = []

            for depth in range(self.depth):
                p = 1.1 - depth / (self.depth + 0.0)
                if p > 1:
                    p = 1.0

                rv.append(im.FactorScale(im.Alpha(image, p), p))

            return rv

        def predict(self):
            """
            This is called internally by the Particles object to predict the images the particles
            are using. It's expected to return a list of images to predict.
            """
            return self.image


    class SnowParticle(object):
        """
        Represents every particle in the screen.
        """

        def __init__(self, image, wind, speed, xborder, yborder):
            """
            Initializes the snow particle. This is called automatically when the object is created.
            """

            self.image = image

            if speed <= 0:
                speed = 1

            self.wind = wind
            self.speed = speed
            self.oldst = None
            self.xpos = random.uniform(0 - xborder, renpy.config.screen_width + xborder)
            self.ypos = -yborder

        def update(self, st):
            """
            Called internally in every frame to update the particle.
            """

            if self.oldst is None:
                self.oldst = st

            lag = st - self.oldst
            self.oldst = st

            self.xpos += lag * self.wind
            self.ypos += lag * self.speed

            if (
                self.ypos > renpy.config.screen_height
                or (self.wind < 0 and self.xpos < 0)
                or (self.wind > 0 and self.xpos > renpy.config.screen_width)
            ):

                return None

            return int(self.xpos), int(self.ypos), st, self.image
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154

# Отображение музыки, играющей в данный момент

Заполняем словарь music_data путём до трека и его названием, что будет отображаться при использовании DynamicDisplayable-функции. У нас примером выступит Between August and December - Pile.

Объявляем функцию как изображение, делая DynamicDisplayable. Теперь, если на канале music будет проигрываться какой-либо трек и путь до него будет указан в нашем словаре music_data, то появится его название (которое мы указали в словаре).

  1. Объявляем словарь, где ключ — путь до файла, а значение — его название, которое будет выводиться.
init python:
    music_data = {"sound/music/pile.ogg": "Between August and December - Pile"}
    # Словарь с музыкой
    # Ключ словаря - путь до трека. Значение - название трека
    # Ключ — 'sound/music/pile.ogg', Значение — 'Between August and December - Pile'
1
2
3
4
5
  1. Создаём функцию, что будет отвечать за вывод на экран текста.
init python:
    def show_music(text, time):
        """
        Функция показа играемого трека
        Две локальных переменных обязательны, чтобы могли возвращать текст с играемым треком и время ожидания перед повторным вызовом функции.
        В time будем возвращать .1, чтобы не было времени ожидания перед ещё одним вызовом функции.
        """
        music_is_play = renpy.music.is_playing(
            channel="music"
        )  # Узнаём, играет ли сейчас музыка в канале 'music'
        if music_is_play:  # Если играет, то…
            what_music_play = renpy.music.get_playing(
                channel="music"
            )  # …узнаём что играет (возвращает нам путь до трека)
            if (
                what_music_play not in music_data
            ):  # Проверяем, есть ли такой трек в словаре.
                return (
                    Text("Играет неизвестная словарю музыка"),
                    0.1,
                )  # Если его нет, появится эта строка вместо названия трека
            else:
                what_music_play = music_data[
                    what_music_play
                ]  # Если есть такой трек в словаре, то нашим выводом на экран станет значение словаря (то бишь, название трека)
                return (
                    Text("Сейчас играет:\n%s" % (what_music_play)),
                    0.1,
                )  # Возвращаем (показываем) название (или любой другой текст) песни, что сейчас играет
        else:
            return Text(""), 0.1  # Если музыка не играет, то мы возвращаем пустой текст
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  1. Объявляем изображение как DynamicDisplayable.
init python:
    renpy.image("playing_music", DynamicDisplayable(show_music)) # Объявляем изображение
    # Теперь это изображение является нашей функцией, что будет показывать название трека, который играет в данный момент.
    # Рекомендуется использование более уникальных имён для словаря с музыкой, названий функции для треков и т.п, ибо возможны конфликты.
1
2
3
4

TIP

Данный способ объявления работает в init python. Для обычного init используйте:

init:
    image playing_music = DynamicDisplayable(show_music)
1
2

# Пример использования

label playing_music:
    play music music_list["pile"] fadein 2 # Проигрываем музыку, что имеется в нашем словаре music_data
    show playing_music at truecenter # Показываем изображение-функцию по центру.
    "В центре экрана мы увидим что сейчас играет."
    play music music_list["what_do_you_think_of_me"] fadein 2 # Проигрываем музыку, что отсутствует в нашем словаре music_data
    "Если трека нет в словаре мы увидим заранее придуманный текст"
    stop music fadeout 2 # Останавливаем воспроизведение музыки
    "И если музыка не играет, то мы ничего не увидим"
1
2
3
4
5
6
7
8

TIP

Если вы хотите добавить отображение играемой музыки в экран, то можно сделать так:

    add 'playing_music' # Будет добавлять в экран заранее объявленый DynamicDisplayable
1

# Открытие файла

Данный код позволяет открыть необходимый файл во время игры.

init python:
    import os
    import sys
    import subprocess
    import platform

    def openFile(path):
        file = os.path.abspath(os.path.join(config.basedir, path))
        if sys.platform == "win32":
            os.startfile(file)
        elif platform.mac_ver()[0]:
            subprocess.Popen(["open", file])
        else:
            subprocess.Popen(["xdg-open", file])
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# Пример использования

label test_label:
    "Идёт некий текст."
    $ openFile("game/mods/myMod/file.txt")
    "Открывается файл `file.txt` по пути `game/mods/myMod/file.txt`, продолжается игра."
1
2
3
4

# Создание собственной карты

Если вам недостаточно мест в оригинальной карте или вам необходима своя карта для мода, то с помощью этого кода можно её создать. В архиве с ресурсами используется версия оригинальной карты со всеми зонами.

Скачать скрипт

Скачать архив с ресурсами карты

store.map_pics_mymod содержит в себе пути до default, idle и hover версий вашей карты.

  • bgpic_mymod - default-версия
  • avaliable_mymod - idle-версия
  • selected_mymod - hover-версия

store.map_zones_mymod содержит в себе список зон, при наведении на координаты которых будет сменяться idle версия на hover.

Пример заполнения списка:

  • "house_1" : String - название зоны
  • [766, 267, 803, 316] : Int (X верхнего левого угла), Int (Y верхнего левого угла), Int (X нижнего правого угла), Int (Y нижнего правого угла) - координаты зоны, при наведении на которые будет сменяться версия карты
  • u"Домик 1" : String - placeholder, занимающий место, если отсутствует "картинка" для зоны в её координатах. Выводится текстом.

Основные функции карты:

  • disable_all_zones_mymod() - отключает все зоны на карте.
  • enable_all_zones_mymod() - включает все зоны на карте.
  • set_zone_mymod(name, label) - включает одну зону на карте, указываем название зоны, а затем название лейбла, на который должен быть совершён прыжок при нажатии на зону.
  • reset_zone_mymod(name) - отключает одну зону на карте.
  • enable_empty_zone_mymod(name) - включает одну зону на карте, но при нажатии ничего не происходит.
  • reset_current_zone_mymod() - если мы выбрали зону и находимся на её лейбле, то при использовании включает эту зону, но при нажатии ничего не произойдёт.
  • disable_current_zone_mymod() - отключает зону, на лейбле которой мы находимся.
  • show_map_mymod() - перебрасывает на лейбл, показывающей карту. Считай, показывает карту.
  • init_map_zones_realization_mymod() - инициализирует карту.

WARNING

Инициализация карты должна происходить один раз за весь мод.

init python:
    import pygame
    import os
    import os.path
    import renpy.store as store
    from renpy.store import *
    from renpy.display.im import ImageBase, image, cache, Composite

    def bg_tmp_image(bgname):
        renpy.image(
            "text " + bgname,
            LiveComposite(
                (config.screen_width, config.screen_height),
                (0, 0),
                "#ffff7f",
                (50, 150),
                Text(u"А здесь будет фон про " + bgname, size=40, color="6A7183"),
            ),
        )
        return "text " + bgname

    store.map_pics_mymod = {
        "bgpic_mymod": "map/images/map_avaliable_mod.jpg",  # Путь до фона карты
        "avaliable_mymod": "map/images/map_avaliable_mod.jpg",  # Путь до версии карты с idle-версией
        "selected_mymod": "map/images/map_selected_mod.jpg",  # Путь до версии карты с hover-версией
    }

    store.map_zones_mymod = {
        "house_1": {
            "position": [766, 267, 803, 316],
            "default_bg": bg_tmp_image(u"Домик 1"),
        },
        "house_2": {
            "position": [808, 274, 844, 327],
            "default_bg": bg_tmp_image(u"Домик 2"),
        },
        "house_3": {
            "position": [842, 282, 892, 330],
            "default_bg": bg_tmp_image(u"Домик 3"),
        },
        "house_4": {
            "position": [888, 288, 928, 340],
            "default_bg": bg_tmp_image(u"Домик 4"),
        },
        "house_5": {
            "position": [964, 307, 999, 352],
            "default_bg": bg_tmp_image(u"Домик 5"),
        },
        "house_6": {
            "position": [1000, 303, 1038, 357],
            "default_bg": bg_tmp_image(u"Домик 6"),
        },
        "house_7": {
            "position": [790, 206, 829, 256],
            "default_bg": bg_tmp_image(u"Домик 7"),
        },
        "house_8": {
            "position": [835, 210, 873, 263],
            "default_bg": bg_tmp_image(u"Домик 8"),
        },
        "house_9": {
            "position": [905, 227, 939, 277],
            "default_bg": bg_tmp_image(u"Домик 9"),
        },
        "house_10": {
            "position": [945, 234, 981, 283],
            "default_bg": bg_tmp_image(u"Домик 10"),
        },
        "house_11": {
            "position": [988, 241, 1023, 290],
            "default_bg": bg_tmp_image(u"Домик 11"),
        },
        "house_12": {
            "position": [1024, 242, 1068, 303],
            "default_bg": bg_tmp_image(u"Домик 12"),
        },
        "house_13": {
            "position": [809, 143, 852, 200],
            "default_bg": bg_tmp_image(u"Домик 13"),
        },
        "house_14": {
            "position": [852, 150, 886, 205],
            "default_bg": bg_tmp_image(u"Домик 14"),
        },
        "house_15": {
            "position": [888, 158, 925, 209],
            "default_bg": bg_tmp_image(u"Домик 15"),
        },
        "house_16": {
            "position": [925, 166, 958, 228],
            "default_bg": bg_tmp_image(u"Домик 16"),
        },
        "house_17": {
            "position": [958, 168, 1020, 227],
            "default_bg": bg_tmp_image(u"Домик 17"),
        },
        "house_23": {
            "position": [715, 616, 763, 665],
            "default_bg": bg_tmp_image(u"Домик 23"),
        },
        "scene": {
            "position": [1062, 54, 1154, 139],
            "default_bg": bg_tmp_image(u"Эстрада"),
        },
        "square": {
            "position": [887, 360, 1001, 546],
            "default_bg": bg_tmp_image(u"Площадь"),
        },
        "musclub": {
            "position": [627, 255, 694, 340],
            "default_bg": bg_tmp_image(u"Музклуб"),
        },
        "dinning_hall": {
            "position": [1010, 456, 1144, 588],
            "default_bg": bg_tmp_image(u"Столовая"),
        },
        "sport_area": {
            "position": [1219, 376, 1584, 657],
            "default_bg": bg_tmp_image(u"Спорткомплекс"),
        },
        "beach": {"position": [1198, 674, 1490, 833], "default_bg": bg_tmp_image(u"Пляж")},
        "boathouse": {
            "position": [832, 801, 957, 855],
            "default_bg": bg_tmp_image(u"Лодочный причал"),
        },
        "booth": {"position": [905, 663, 949, 732], "default_bg": bg_tmp_image(u"Будка")},
        "clubs": {"position": [435, 437, 650, 605], "default_bg": bg_tmp_image(u"Клубы")},
        "library": {
            "position": [1158, 271, 1285, 360],
            "default_bg": bg_tmp_image(u"Библиотека"),
        },
        "infirmary": {
            "position": [1042, 360, 1188, 444],
            "default_bg": bg_tmp_image(u"Медпункт"),
        },
        "forest": {"position": [558, 58, 691, 194], "default_bg": bg_tmp_image(u"о. Лес")},
        "bus_stop": {
            "position": [286, 441, 414, 556],
            "default_bg": bg_tmp_image(u"Стоянка"),
        },
        "admin": {
            "position": [774, 348, 879, 449],
            "default_bg": bg_tmp_image(u"Админ. корпус"),
        },
        "shower_room": {
            "position": [695, 433, 791, 530],
            "default_bg": bg_tmp_image(u"Душевая"),
        },
        "old_building": {
            "position": [230, 1004, 337, 1073],
            "default_bg": bg_tmp_image(u"Старый корпус"),
        },
        "island_far": {
            "position": [873, 967, 1332, 1080],
            "default_bg": bg_tmp_image(u"Остров дальний"),
        },
        "island_close": {
            "position": [557, 935, 865, 1071],
            "default_bg": bg_tmp_image(u"Острова ближний"),
        },
        "storage": {
            "position": [1148, 481, 1215, 583],
            "default_bg": bg_tmp_image(u"Склад"),
        },
        "forest_r_u": {
            "position": [1757, 81, 1836, 203],
            "default_bg": bg_tmp_image(u"Лес верхний правый"),
        },
        "forest_r_d": {
            "position": [1777, 879, 1855, 998],
            "default_bg": bg_tmp_image(u"Лес нижний правый"),
        },
        "ws": {"position": [567, 355, 625, 405], "default_bg": bg_tmp_image(u"Туалет")},
    }

    global_map_result_mymod = "error"

    def init_map_zones_realization_mymod(zones_mymod, default):
        global global_zones_mymod
        global_zones_mymod = zones_mymod
        for i, data in global_zones_mymod.iteritems():
            data["label"] = default
            data["avaliable"] = True

    class Map_mymod(renpy.Displayable):
        def __init__(self, pics, default):
            renpy.Displayable.__init__(self)
            self.pics = pics
            self.default = default
            config.overlay_functions.append(self.overlay)

        def disable_all_zones(self):
            global global_zones_mymod
            for name, data in global_zones_mymod.iteritems():
                data["label"] = self.default
                data["avaliable"] = False

        def enable_all_zones(self):
            global global_zones_mymod
            for name, data in global_zones_mymod.iteritems():
                data["label"] = self.default
                data["avaliable"] = True

        def set_zone(self, name, label):
            global global_zones_mymod
            global_zones_mymod[name]["label"] = label
            global_zones_mymod[name]["avaliable"] = True

        def reset_zone(self, name):
            global global_zones_mymod
            global_zones_mymod[name]["label"] = self.default
            global_zones_mymod[name]["avaliable"] = False

        def enable_empty_zone(self, name):
            global global_zones_mymod
            self.set_zone(name, self.default)
            global_zones_mymod[name]["avaliable"] = True

        def reset_current_zone(self):
            self.enable_empty_zone(global_map_result_mymod)

        def disable_current_zone(self):
            global global_zones_mymod
            global_zones_mymod[global_map_result_mymod]["avaliable"] = False

        def event(self, ev, x, y, st):
            return

        def render(self, width, height, st, at):
            return renpy.Render(1, 1)

        def zoneclick(self, name):
            global global_zones_mymod
            global global_map_result_mymod
            store.map_enabled_mymod = False
            renpy.scene("mapoverlay")
            global_map_result_mymod = name
            renpy.hide("widget map_mymod")
            ui.jumps(global_zones_mymod[name]["label"])()

        def overlay(self):
            if store.map_enabled_mymod:
                global global_zones_mymod
                renpy.scene("mapoverlay")
                ui.layer("mapoverlay")
                for name, data in global_zones_mymod.iteritems():
                    if data["avaliable"]:
                        pos = data["position"]
                        print(name)
                        ui.imagebutton(
                            im.Crop(
                                self.pics["avaliable_mymod"],
                                pos[0],
                                pos[1],
                                pos[2] - pos[0],
                                pos[3] - pos[1],
                            ),
                            im.Crop(
                                self.pics["selected_mymod"],
                                pos[0],
                                pos[1],
                                pos[2] - pos[0],
                                pos[3] - pos[1],
                            ),
                            clicked=renpy.curry(self.zoneclick)(name),
                            xpos=pos[0],
                            ypos=pos[1],
                        )
                ui.close()

    store.map_mymod = Map_mymod(store.map_pics_mymod, default)

    store.map_enabled_mymod = False
    store.map_enabled_mymod_tmp = False

    def disable_stuff():
        store.map_enabled_mymod_tmp = store.map_enabled_mymod_tmp or store.map_enabled_mymod
        store.map_enabled_mymod = False

    def enable_stuff():
        store.map_enabled_mymod = store.map_enabled_mymod_tmp
        store.map_enabled_mymod_tmp = False

    config_session = False

    if not config_session:

        def disable_all_zones_mymod():
            store.map_mymod.disable_all_zones()

        def enable_all_zones_mymod():
            store.map_mymod.enable_all_zones()

        def set_zone_mymod(name, label):
            store.map_mymod.set_zone(name, label)

        def reset_zone_mymod(name):
            store.map_mymod.reset_zone(name)

        def enable_empty_zone_mymod(name):
            store.map_mymod.enable_empty_zone(name)

        def reset_current_zone_mymod():
            store.map_mymod.reset_current_zone()

        def disable_current_zone_mymod():
            store.map_mymod.disable_current_zone()

        def show_map_mymod():
            ui.jumps("_show_map_mymod")()

        def init_map_zones_mymod():
            init_map_zones_realization_mymod(store.map_zones_mymod, "nothing_here")

init:
    if not config_session:
        image widget map_mymod = "map/images/map_n_mod.jpg" # Путь до фона карты
        image bg map_mymod     = "map/images/map_avaliable_mod.jpg" # Путь до версии карты с idle-версией

label _show_map_mymod:
    show widget map_mymod
    $ store.map_enabled_mymod = True
    $ ui.interact()
    jump _show_map_mymod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324

# Пример использования

label test_map_mod:

    window hide

    $ init_map_zones_mymod() # Объявляем нашу карту.
    $ disable_all_zones_mymod() # Отключаем все зоны, если были ранее включены
    $ set_zone_mymod("house_1", "label_of_house")   # Выделяем на карте домик №1, при нажатии — прыжок на лейбл домика
                                                    # Название остальных мест можно взять из списка store.map_zones_mymod, что в map_mymod
    $ show_map_mymod() # Показываем нашу карту

label label_of_house:
    window show

    "А вот и лейбл нашего домика."
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# Создание собственной карты (для начинающих)

В исходном коде игры и во многих модах можно увидеть похожий код для использования карты внутри игры. Но этот метод достаточно сложен в понимании для новичка. Поэтому, далее будет показан пример кода для использования карты в вашей модификации.

Скачать скрипт

Перед началом, нам нужно будет изображение нашей карты в трёх состояниях:

  • idle - Состояние покоя.
  • hover - Состояние, когда курсор наведён на локацию.
  • insensitive - Состояние с отмеченными пройденными объектами на карте. При этом состоянии нельзя будет кликнуть на локацию.
init python:
    screen_map_condition = [False] * 7 # Можно сделать и словарь
    screen_map_count = 0
    screen_map_label = 'screen_map_after_walk'
    screen_map_need_count = 1
1
2
3
4
5

Сначала объявим нужные нам переменные:

  • screen_map_condition<List> - Список, состоящий из False. Кол-во False в списке определяет кол-во объектов, которые могут быть на карте.
  • screen_map_count<Int> - Число пройденных локаций. Изначально равно нулю.
  • screen_map_label<String> - Название лейбла, в который мы будем прыгать после прохождения карты.
  • screen_map_need_count<Int> - Число, определяющее сколько локаций нужно пройти. Изначально равно единице.

TIP

Стоит напомнить, что название переменных лучше придумывать более уникальными, чтобы избежать конфликтов с другими модификациями.

Теперь объявим нужные нам функции.

def screens_map_reset_condition():
    global screen_map_condition, screen_map_count, screen_map_need_count
    screen_map_condition = [False] * 7
    screen_map_count = 0
    screen_map_need_count = 1

def screens_map_set_condition(label,count):
    global screen_map_need_count, screen_map_label
    if label: # Проверяем, если аргумент label
        screen_map_label = label
    if count: # Проверяем, если аргумент count
        screen_map_need_count = count
1
2
3
4
5
6
7
8
9
10
11
12

Функция screens_map_reset_condition сбрасывает переменные, связанные с работой карты. А screens_map_set_condition принимает два аргумента label<String> и count<Int>. Устанавливает лейбл, к которому должны перейти после карты, и кол-во локаций.

Перейдем к написанию самой карты. Она будет представлять собой screen, принимающий в качестве аргумента словарь.

init:
    screen screen_map(condition={'screen_map_error_place' : [(414,467,200,200), screen_map_condition[0]]}): # Cтавим аргументу изначальное положение. На случай, если забудем вписать аргумент при вызове экрана.
        modal True
        imagemap:
            # Пропишем пути до состояний карты
            idle 'screens_map/map/old_map_idle.png'
            hover 'screens_map/map/old_map_hover.png'
            insensitive 'screens_map/map/old_map_insensitive.png'
            alpha True
            for label, lists in condition.items():
                # Циклом проходимся по словарю condition. И устанавливает чувствительные области в изображении.
                hotspot(lists[0][0], lists[0][1], lists[0][2], lists[0][3]) action [SensitiveIf(lists[1] == False), Jump(label)]
                # SensitiveIf позволяет делать кнопку чувствительной, пока действует какое-то условие.
1
2
3
4
5
6
7
8
9
10
11
12
13

Аргумент condition - словарь. Ключ этого словаря - название лейбла, к которому мы должны прыгнуть. Значение словаря - список. Первый элемент списка - кортеж (x, y, width, height) с координатами начала локации на изображении и её размеров по x и y. Второй элемент списка - какой-либо объект списка screen_map_condition.

init python:
    screen_map_condition = [False] * 7  # Можно сделать и словарь
    screen_map_count = 0
    screen_map_label = "screen_map_after_walk"
    screen_map_need_count = 1


    def screens_map_reset_condition():
        global screen_map_condition, screen_map_count, screen_map_need_count
        screen_map_condition = [False] * 7
        screen_map_count = 0
        screen_map_need_count = 1


    def screens_map_set_condition(label, count):
        global screen_map_need_count, screen_map_label
        if label:  # Проверяем, если аргумент label.
            screen_map_label = label
        if count:  # Проверяем, если аргумент count.
            screen_map_need_count = count

init:
    screen screen_map(condition={'screen_map_error_place' : [(414,467,200,200), screen_map_condition[0]]}): # Ставим аргументу изначальное положение. На случай если забудем вписать аргумент при вызове экрана.
        modal True
        imagemap:
            # Пропишем пути до состояний карты.
            idle 'screens_map/map/old_map_idle.png'
            hover 'screens_map/map/old_map_hover.png'
            insensitive 'screens_map/map/old_map_insensitive.png'
            alpha True
            for label, lists in condition.items():
                # Циклом проходимся по словарю condition. и устанавливает чувствительные области в изображении.
                hotspot(lists[0][0], lists[0][1], lists[0][2], lists[0][3]) action [SensitiveIf(lists[1] == False), Jump(label)]
                # SensitiveIf позволяет делать кнопку чувствительной, пока действует какое-то условие.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# Пример использования

label screen_map_start:
    window show

    'Сейчас перед нами должна появиться карта.'

    window hide
    $ screens_map_set_condition('screen_map_after_walk', 2) # Устанавливаем лейбл после прохождения карты и кол-во нужных пройденных локаций для этого.
    jump screen_map_walk

label screen_map_walk:
    # Проверяем, если кол-во пройденных локаций меньше кол-ва локаций, которых нужно пройти
    if screen_map_count < screen_map_need_count:
        # Если меньше, то вызываем наш экран и в него передаем словарь с нужными аргументами.
        call screen screen_map({'screen_map_place1' : [(414,467,200,200), screen_map_condition[0]],'screen_map_place_2' : [(1000,10,200,200), screen_map_condition[1]]})
    else:
        # Иначе сбрасываем переменные связанные с картой и прыгаем на заданный ранее лейбл.
        'Сбрасываем счетчик.'
        $ screens_map_reset_condition()
        jump screen_map_label

# Лейбл, связанный с локацией на карте.
label screen_map_place1:
    'Наш текст.'
    $ screen_map_count += 1 # Повышаем счётчик пройденных локаций.
    $ screen_map_condition[0] = True # Переключаем элемент списка в положение True.
    jump screen_map_walk # Прыгаем обратно в лейбл с нашей картой.

# Лейбл, связанный с локацией на карте
label screen_map_place_2:
    'Наш текст 2.'
    $ screen_map_count += 1 # Повышаем счётчик пройденных локаций.
    $ screen_map_condition[1] = True
    #переключаем элемент списка в положение True
    jump screen_map_walk # Прыгаем обратно в лейбл с нашей картой.

# После прохождения карты.
# История продолжается.
label screen_map_after_walk:
    'Мы прошли все места.'
    return

# Лейбл, в который ведет нас карта, если мы не установили аргумент condition
label screen_map_error_place:
    'Я забрел куда-то не туда.'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# Замена интерфейса

Под интерфейсом предполагаются внутриигровые экраны, с которыми взаимодействует пользователь, такие как:

  • say - Экран, где отображается текст вашей истории.
  • main_menu - Экран главного меню вашей модификации.
  • game_menu_selector - Экран игрового меню (меню быстрого доступа).
  • quit - Экран выхода.
  • preferences - Экран настроек.
  • save - Экран сохранения игры.
  • load- Экран загрузки сохранения.
  • nvl - NVL экран.
  • choice - Экран выбора.
  • text_history - Экран просмотра истории.
  • yesno_prompt - Экран подтверждения действия.
  • skip_indicator - Экран пропуска текста.
  • history - Экран прочитанного текста.

Данные экраны присутствуют в игре и их можно заменить. В этом примере мы не будем создавать экраны: предполагается, что у вас есть уже готовые экраны, которые вы хотели бы заменить.

В этом методе мы будем запускать мод с лейбла, который заменяет часть экранов и главное меню, после чего мы можем заменить их обратно при выходе из меню мода.

Скачать скрипт

Параметры:

  • my_mod - префикс. Замените его на префикс своего мода, чтобы избежать конфликтов.

WARNING

В данном случае название ваших экранов должно соответствовать виду: префикс мода + название экрана в оригинале.

Например, с main_menu:

  • префикс мода - my_mod
  • экран должен называться - my_mod_main_menu
init python:
    # Уберите из списка ненужные названия экранов, если не хотите их заменять.
    SCREENS = [
        "main_menu",
        "game_menu_selector",
        "quit",
        "say",
        "preferences",
        "save",
        "load",
        "nvl",
        "choice",
        "text_history_screen",
        "yesno_prompt",
        "skip_indicator",
        "history"
    ]

    def my_mod_screen_save():  # Функция сохранения экранов из оригинала.
        for name in SCREENS:
            renpy.display.screen.screens[
                ("my_mod_old_" + name, None)
            ] = renpy.display.screen.screens[(name, None)]


    def my_mod_screen_act():  # Функция замены экранов из оригинала на собственные.
        config.window_title = u"Мой мод"  # Здесь вводите название Вашего мода.
        for (
            name
        ) in (
            SCREENS
        ):
            renpy.display.screen.screens[(name, None)] = renpy.display.screen.screens[
                ("my_mod_" + name, None)
            ]
        config.mouse["default"] = [ ("images/misc/mouse/1.png", 0, 0) ]
        default_mouse = "default"
        # Две строчки сверху - замена курсора
        config.main_menu_music = (
            "mods/my_mod/music/main_menu.mp3"  # Вставьте ваш путь до музыки в главном меню.
        )


    def my_mod_screens_diact():  # Функция обратной замены.
        # Пытаемся заменить экраны.
        try:
            config.window_title = u"Бесконечное лето"
            for name in SCREENS:
                renpy.display.screen.screens[(name, None)] = renpy.display.screen.screens[
                    ("my_mod_old_" + name, None)
                ]
            config.mouse["default"] = [ ("images/misc/mouse/1.png", 0, 0) ]
            default_mouse = "default"
            config.main_menu_music = "sound/music/blow_with_the_fires.ogg"
        except:  # Если возникают ошибки, то мы выходим из игры, чтобы избежать Traceback
            renpy.quit()
    # Функция для автоматического включения кастомного интерфейса при загрузке сохранения с названием Вашего мода
    def my_mod_activate_after_load():
        global save_name
        if "MyMod" in save_name:
            my_mod_screen_save()
            my_mod_screen_act()

    # Добавляем функцию в Callback
    config.after_load_callbacks.append(my_mod_activate_after_load)

    # Объединяем функцию сохранения экранов и замены в одну.
    def my_mod_screens_save_act():
        my_mod_screen_save()
        my_mod_screen_act()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

# Пример использования

# Лейбл с которого будет запускаться мод.
label my_mod_index:
    window hide # Скрываем текстбокс.
    stop music fadeout 3 # Останавливаем музыку.
    scene bg black with fade2 # Переходим на сцену с чёрным экраном.
    $ my_mod_screens_save_act() # Сохраняем экраны из оригинала и заменяем на собственные.
    return # С помощью return попадаем в главное меню игры.


# Лейбл выхода из мода.
label my_mod_true_exit:
    window hide # Скрываем текстбокс.
    stop music fadeout 3 # Останавливаем музыку.
    scene black with fade # Переходим на сцену с чёрным экраном.
    $ my_mod_screens_diact() # Делаем обратную замену экранов мода на оригинальные.
    $ MainMenu(confirm=False)() # Выходим в главное меню.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

В нашем случае с лейбла my_mod_index должен запускаться мод. А лейбл my_mod_true_exit нужен для обратной замены экранов поэтому, чтобы выйти из мода, и выполнить обратную замену вы можете просто прыгнуть на этот лейбл.

TIP

Можно обойтись и без лейбла my_mod_true_exit: вы можете попробовать добавить к вашей кнопке выхода в главном меню следующее действие:

action [(Function(my_mod_screens_diact)), MainMenu(False)]
1

# Создание галереи

Код представляет собой полноценную галерею, поделённую на 2 раздела — иллюстрации (CG) и фоны (BG).

Скачать скрипт

Создаём init python блок, а внутри него — экземпляр класса Gallery(). Создаём переменные page и gallery_mode. Первая отвечает за страницы нашей галереи, вторая — за тип нашей галереи, который будет меняться при нажатии на кнопку для смены раздела.

Настраиваем наш экземпляр modGallery — изображение заблокированного (ещё не открытого) варианта картинки и отключаем навигацию.

init python:
    modGallery = Gallery()
    page = 0
    gallery_mode = "cg"

    modGallery.locked_button = get_image("gui/gallery/not_opened_idle.png")
    modGallery.navigation = False
1
2
3
4
5
6
7

Затем создаём словари, что будут содержать в себе иллюстрации и фоны, потом заполняем с помощью цикла нашу галерею.

    gallery_cg = [ # Заполняем ЦГ словарь
        "d1_food_normal",
        "d1_food_skolop",
        "d1_grasshopper",
        "d1_rena_sunset",
    ]

    gallery_bg = [ # Заполняем БГ словарь
        "bus_stop",
        "ext_aidpost_day",
        "ext_aidpost_night",
        "ext_bathhouse_night",
    ]

    # Создаём кнопки и их изображения, внезависимости от размера исходной картинки, будет масштабирование до 1920x1080
    for cg in gallery_cg:
        modGallery.button(cg)
        modGallery.image(im.Crop("images/cg/"+cg+".jpg" , (0, 0, 1920, 1080)))
        modGallery.unlock(cg)

    for bg in gallery_bg:
        modGallery.button(bg)
        modGallery.image(im.Crop("images/bg/"+bg+".jpg" , (0, 0, 1920, 1080)))
        modGallery.unlock(bg)
    # При нажатии на кнопку с изображением, будет происходить fade переход.
    modGallery.transition = fade
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

Разблокировка всех изображений

Если необходимо, можно создать специальную функцию, что позволяет нам открыть все изображения из нашей галереи.

def collect_all_ModGallery():
    s = [i for k in persistent._seen_images for i in k]

    for i in gallery_cg:
        if i not in s: return

    for i in gallery_bg:
        if i not in s: return
1
2
3
4
5
6
7
8

Теперь создаём сам экран с нашей галереей. Указываем количество ячеек для изображений, создаём список gallery_table, который будет заполняться иллюстрациями или фонами в зависимости от значения gallery_mode. Создаём переменную len_table, которая будет ссылаться на длину нашего списка. Создаём функцию, что позволит нам высчитать точное количество страниц галереи. В переменной pages, что отвечает за количество страниц галереи, высчитываем.

init:
    screen ModGallery_screen:
        modal True
        tag menu
        $ rows = 4
        $ cols = 3
        $ cells = rows * cols
        $ gallery_table = []
        if gallery_mode == "cg":
            $ gallery_table = gallery_cg
        else:
            $ gallery_table = gallery_bg
        $ len_table = len(gallery_table)
        python:
            def abc(n, k):
                l = float(n)/float(k)
                if l-int(l) > 0:
                    return int(l)+1
                else:
                    return l
        $ pages = str(page+1)+"/"+str(int(abc(len_table, cells)))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Создаём frame с фоном нашей галереи и кнопки для навигации по типам галереи.

frame background get_image("gui/settings/history_bg.jpg"):
    if gallery_mode == "cg":
        textbutton "Фоны":
            style "log_button"
            text_style "settings_link"
            xalign 0.98
            yalign 0.08
            action (SetVariable('gallery_mode', "bg"), SetVariable('page', 0), ShowMenu("ModGallery_screen"))
        hbox xalign 0.5 yalign 0.08:
            text "Иллюстрации" style "settings_link" yalign 0.5 color "#ffffff"
    elif gallery_mode == "bg":
        textbutton "Иллюстрации":
            style "log_button"
            text_style "settings_link"
            xalign 0.02
            yalign 0.08
            action (SetVariable('gallery_mode', "cg"), SetVariable('page', 0), ShowMenu("ModGallery_screen"))
        hbox xalign 0.5 yalign 0.08:
            text "Фоны":
                style "settings_link"
                yalign 0.5
                color "#ffffff"

    textbutton "Назад":
        style "log_button"
        text_style "settings_link"
        xalign 0.015
        yalign 0.92
        action Return()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

Создаём grid для отображения изображений в сетке. Производим вычисления, создаём превью-версии картинок для БГ и ЦГ, создаём сами кнопки.

grid rows cols xpos 0.09 ypos 0.18:
    $ cg_displayed = 0
    $ next_page = page + 1
    if next_page > int(len_table/cells):
        $ next_page = 0
    for n in range(0, len_table):
        if n < (page+1)*cells and n>=page*cells:
            python:
                if gallery_mode == "cg": # Превью для ЦГ
                    _t = im.Crop("images/cg/"+gallery_table[n]+".jpg" , (0, 0, 1920, 1080))
                elif gallery_mode == "bg": # Превью для БГ
                    _t = im.Crop("images/bg/"+gallery_table[n]+".jpg" , (0, 0, 1920, 1080))
                th = im.Scale(_t, 320, 180) # Само превью
                img = im.Composite((336, 196), (8, 8), im.Alpha(th, 0.9), (0, 0), im.Image(get_image("gui/gallery/thumbnail_idle.png"))) # idle-версия превью
                imgh = im.Composite((336, 196), (8, 8), th, (0, 0), im.Image(get_image("gui/gallery/thumbnail_hover.png"))) # hover-версия превью
            add g.make_button(gallery_table[n], get_image("gui/gallery/blank.png"), None, imgh, img, style="blank_button", bottom_margin=50, right_margin=50) # создаём кнопки
            $ cg_displayed += 1

            if n+1 == len_table:
                $ next_page = 0

    for j in range(0, cells-cg_displayed):
        null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

Финальные штрихи — создаём кнопки для навигации между страницами галереи, также ставим текст, что показывает текущую страницу и общее количество.

if page != 0:
    imagebutton:
        auto get_image("gui/dialogue_box/day/backward_%s.png")
        yalign 0.5
        xalign 0.01
        action (SetVariable('page', page-1), ShowMenu("ModGallery_screen"))
imagebutton:
    auto get_image("gui/dialogue_box/day/forward_%s.png")
    yalign 0.5
    xalign 0.99
    action (SetVariable('page', next_page), ShowMenu("ModGallery_screen"))

text pages:
    style "settings_link"
    xalign 0.985
    yalign 0.92
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# Заключение

Полный вариант кода выглядит так:

init python:
    modGallery = Gallery()
    page = 0
    gallery_mode = "cg"

    modGallery.locked_button = get_image("gui/gallery/not_opened_idle.png")
    modGallery.navigation = False

    gallery_cg = [ # Заполняем ЦГ словарь
        "d1_food_normal",
        "d1_food_skolop",
        "d1_grasshopper",
        "d1_rena_sunset",
    ]

    gallery_bg = [ # Заполняем БГ словарь
        "bus_stop",
        "ext_aidpost_day",
        "ext_aidpost_night",
        "ext_bathhouse_night",
    ]

    for cg in gallery_cg:
        modGallery.button(cg)
        modGallery.image(im.Crop("images/cg/"+cg+".jpg" , (0, 0, 1920, 1080)))
        modGallery.unlock(cg)

    for bg in gallery_bg:
        modGallery.button(bg)
        modGallery.image(im.Crop("images/bg/"+bg+".jpg" , (0, 0, 1920, 1080)))
        modGallery.unlock(bg)
    modGallery.transition = fade

    def collect_all_ModGallery():
        if persistent.collector:
            s = [i for k in persistent._seen_images for i in k]

            for i in gallery_cg:
                if i not in s: return

            for i in gallery_bg:
                if i not in s: return

init:
    screen ModGallery_screen:
        modal True
        tag menu
        $ rows = 4
        $ cols = 3
        $ cells = rows * cols
        $ gallery_table = []
        if gallery_mode == "cg":
            $ gallery_table = gallery_cg
        else:
            $ gallery_table = gallery_bg
        $ len_table = len(gallery_table)
        python:
            def abc(n, k):
                l = float(n)/float(k)
                if l-int(l) > 0:
                    return int(l)+1
                else:
                    return l
        $ pages = str(page+1)+"/"+str(int(abc(len_table, cells)))

        frame background get_image("gui/settings/history_bg.jpg"):
            if gallery_mode == "cg":
                textbutton "Фоны":
                    style "log_button"
                    text_style "settings_link"
                    xalign 0.98
                    yalign 0.08
                    action (SetVariable('gallery_mode', "bg"), SetVariable('page', 0), ShowMenu("ModGallery_screen"))
                hbox xalign 0.5 yalign 0.08:
                    text "Иллюстрации" style "settings_link" yalign 0.5 color "#ffffff"
            elif gallery_mode == "bg":
                textbutton "Иллюстрации":
                    style "log_button"
                    text_style "settings_link"
                    xalign 0.02
                    yalign 0.08
                    action (SetVariable('gallery_mode', "cg"), SetVariable('page', 0), ShowMenu("ModGallery_screen"))
                hbox xalign 0.5 yalign 0.08:
                    text "Фоны":
                        style "settings_link"
                        yalign 0.5
                        color "#ffffff"

            textbutton "Назад":
                style "log_button"
                text_style "settings_link"
                xalign 0.015
                yalign 0.92
                action Return()

            grid rows cols xpos 0.09 ypos 0.18:
                $ cg_displayed = 0
                $ next_page = page + 1
                if next_page > int(len_table/cells):
                    $ next_page = 0
                for n in range(0, len_table):
                    if n < (page+1)*cells and n>=page*cells:
                        python:
                            if gallery_mode == "cg": # Превью для ЦГ
                                _t = im.Crop("images/cg/"+gallery_table[n]+".jpg" , (0, 0, 1920, 1080))
                            elif gallery_mode == "bg": # Превью для БГ
                                _t = im.Crop("images/bg/"+gallery_table[n]+".jpg" , (0, 0, 1920, 1080))
                            th = im.Scale(_t, 320, 180)
                            img = im.Composite((336, 196), (8, 8), im.Alpha(th, 0.9), (0, 0), im.Image(get_image("gui/gallery/thumbnail_idle.png")))
                            imgh = im.Composite((336, 196), (8, 8), th, (0, 0), im.Image(get_image("gui/gallery/thumbnail_hover.png")))
                        add g.make_button(gallery_table[n], get_image("gui/gallery/blank.png"), None, imgh, img, style="blank_button", bottom_margin=50, right_margin=50)
                        $ cg_displayed += 1

                        if n+1 == len_table:
                            $ next_page = 0

                for j in range(0, cells-cg_displayed):
                    null

            if page != 0:
                imagebutton:
                    auto get_image("gui/dialogue_box/day/backward_%s.png")
                    yalign 0.5
                    xalign 0.01
                    action (SetVariable('page', page-1), ShowMenu("ModGallery_screen"))
            imagebutton:
                auto get_image("gui/dialogue_box/day/forward_%s.png")
                yalign 0.5
                xalign 0.99
                action (SetVariable('page', next_page), ShowMenu("ModGallery_screen"))

            text pages:
                style "settings_link"
                xalign 0.985
                yalign 0.92
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135

# Перевод мода

Нижеприведённый код позволит перевести ваш мод на другие языки. В примере показан перевод на английский.

Скачать скрипт

TIP

Существует возможность добавить перевод названия мода и персонажей на:

  • Английский | english
  • Русский | None
  • Испанский | spanish
  • Итальянский | italian
  • Китайский | chinese
  • Французский | french
  • Португальский | portuguese

# Перевод названия

Для начала переведём название нашего мода. Создаём словарь translator, где будет храниться перевод для названия мода (а впоследствии и перевод имён персонажей, о котором расскажем в следующем подразделе)

init python:
    translation_new["translator"] = {}
    translation_new["translator"]["name"] = {}
1
2
3

Затем создаём внутри значения с нашем именем создаём ещё два: одно для русского перевода, второе — для английского.

    translation_new["translator"]["name"][None] = u"Переводчик"
    translation_new["translator"]["name"]["english"] = "Translator"
1
2

Теперь объявляем сам мод, но с именем, что будет брать значение из нашего словаря с переводом в зависимости от установленного языка игры.

    mods["translator_mod"] = translation_new["translator"]["name"][_preferences.language]
1

# Перевод персонажа

Теперь переведём персонажа. Для этого создаём в нашем словаре значение для персонажей, а внутри него — ещё одно значение с нашим персонажем.

    translation_new["translator"]["characters"] = {}
    translation_new["translator"]["characters"]["samantha"] = {}
1
2

Создаём значения с переводом на русский и английский язык.

    translation_new["translator"]["characters"]["samantha"]["english"] = "Samantha"
    translation_new["translator"]["characters"]["samantha"][None] = "Саманта"
1
2

И объявляем нашего персонажа со значением для имени, что будет браться из установленного языка игры.

translator_sam = Character(translation_new["translator"]["characters"]["samantha"][_preferences.language])
1

# Перевод текста

Прежде всего делаем проверку на то, имеется ли в списке store наша будущая переменная для перевода текста. Если нет, то добавляем и ставим по умолчанию русский язык.

    if not hasattr(store, "persistent.translate_text_lang"):
        persistent.translate_text_lang = "ru"
1
2

Затем создаём сами тэги для перевода с помощью функций, что будут возвращать тот вариант текста, в зависимости от значения переменной persistent.translate_text_lang. Если значение ru, то показывает русский вариант текста, если en, то английский. Внутрь тэгов будем записывать русский и английский вариант текста.

    def translate_en_tag(tag, argument, contents):
        if persistent.translate_text_lang == "en":
            return contents
        else:
            return [ ]

    def translate_ru_tag(tag, argument, contents):
        if persistent.translate_text_lang == "ru":
            return contents
        else:
            return [ ]
1
2
3
4
5
6
7
8
9
10
11

Добавим функцию для переключения языка отображаемого текста

    def translate_toggle_lang():
        persistent.translate_text_lang = "ru" if persistent.translate_text_lang != "ru" else "en"
1
2

TIP

Как вариант, можно создать кнопку в меню мода, что будет переключать язык повествования.

if persistent.translate_text_lang == "ru":
    textbutton "Язык повествования (Русский)":
        action Function(translate_toggle_lang())
else:
    textbutton "Язык повествования (Английский)":
        action Function(translate_toggle_lang())
1
2
3
4
5
6

Объявляем нашли тэги.

    config.custom_text_tags["en"] = translate_en_tag
    config.custom_text_tags["ru"] = translate_ru_tag
1
2

Пример написания перевода текста представлен ниже.

label translator_mod:
    translator_sam "{en}Hello!{/en}{ru}Привет!{/ru}" # Саманта (или Samantha, если установлен английский язык игры) произносит "Привет!", если переменная равна "ru", если же равно "en", то "Hello!"
1
2

# Заключение

Полный вариант кода выглядит так:

init python:
    if not hasattr(store, "persistent.translate_text_lang"):
        persistent.translate_text_lang = "ru"

    def translate_en_tag(tag, argument, contents):
        if persistent.translate_text_lang == "en":
            return contents
        else:
            return [ ]

    def translate_ru_tag(tag, argument, contents):
        if persistent.translate_text_lang == "ru":
            return contents
        else:
            return [ ]

    def translate_toggle_lang():
        persistent.translate_text_lang = "ru" if persistent.translate_text_lang != "ru" else "en"

    config.custom_text_tags["en"] = translate_en_tag
    config.custom_text_tags["ru"] = translate_ru_tag

    translation_new["translator"] = {}
    translation_new["translator"]["name"] = {}
    translation_new["translator"]["characters"] = {}
    translation_new["translator"]["characters"]["samantha"] = {}

    translation_new["translator"]["name"]["english"] = "Translator"
    translation_new["translator"]["name"][None] = u"Переводчик"

    translation_new["translator"]["characters"]["samantha"]["english"] = "Samantha"
    translation_new["translator"]["characters"]["samantha"][None] = "Саманта"

    translator_sam = Character(translation_new["translator"]["characters"]["samantha"][_preferences.language])

    mods["translator_test"] = translation_new["translator"]["name"][_preferences.language]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# Интеграция Live2D

Позволяет интегрировать Live2D в БЛ без необходимости что-либо докачивать. DLL с Live2D автоматически устанавливается в папку Everlasting Summer/lib/Ваша_ОС. Поддерживается Windows, Linux, Mac, и, возможно, Android и WEB.

Скачать скрипт

Скачать мод-пример

# Объявление Live2D персонажа

image hiyori = Live2D("Resources/Hiyori", base=.6, loop=True)
1
  • base : float отвечает за нижнюю часть изображения, для определения размера. Это часть изображения, где 0.0 - верхняя часть, а 1.0 - нижняя. Это также становится значением yanchor по умолчанию.

  • loop : boolean отвечает за зацикливание анимаций персонажа

Полный список параметров при объявлении персонажа здесь (opens new window).

# Добавление поддержки устройств без Live2D

Имейте в виду, что устройство пользователя может быть неспособно инициализировать Live2D, в этом случае, необходимо создать функцию, которая будет показывать статичный вариант спрайта или текст-плейсхолдер при невозможности воспроизвести Live2D:

init python:
    def MyLive2D(*args, fallback=Placeholder(text="no live2d"), **kwargs):
        if renpy.has_live2d():
             return Live2D(*args, **kwargs)
        else:
             return fallback
1
2
3
4
5
6

# Пример использования

image eileen moving = MyLive2D("Путь до корневой папки Live2D спрайта", fallback="eileen happy") # При возможности воспроизвести будет использоваться Live2D версия спрайта, если же невозможно, то будет использован статичный спрайт `eileen happy`. Если `fallback` не заполнять, то вместо Live2D спрайта будет выводиться текст о том, что невозможно воспроизвести Live2D.
1

# Использование анимаций

Движения хранятся в папке motions, эмоции в папке expressions. Названия движений и эмоций берутся из файлов Live2D, затем вводятся в нижний регистр, и если они начинаются с имени спрайта, за которым следует подчеркивание, то этот префикс удаляется.

Название файла движения - Epsilon_idle_01.motion3.json, следовательно, название движения - idle_01 Название файла эмоции - Angry.exp3.json, название эмоции - angry

# Движение

show Epsilon idle_01
1

# Эмоция

show Epsilon angry
1

# Движение и эмоция одновременно

show Epsilon idle_01 angry
1

# Изменение названия анимации

Для удобства при объявлении Live2D персонажа Вы можете с помощью параметра aliases изменить название анимации/анимаций на более удобное.

init:
    image hiyori = Live2D("Resources/Hiyori", base=.6, aliases={"idle" : "m01"})

label mymod:
    show hiyori idle # эквивалент show hiyori m01
1
2
3
4
5

# Плавная смена анимаций

RenPy поддерживает плавную смену анимации при работе с Live2D. Обычно, когда Ren'Py переходит от одной анимации к другой, переход происходит резко - одна анимация останавливается, а другая запускается.

Live2D поддерживает другую модель, в которой старая анимация может плавно переходить в новую, с интерполяцией параметров. Считайте, что персонаж перемещает свои руки в нужное положение перед началом следующей анимации, а не резко переходит из одной анимации в другую.

Затухание движения контролируется с помощью:

  • параметра fade при объявлении персонажа. Если True, используется затухание анимации, а если False, то происходит резкая смена анимации.
image hiyori = Live2D("Resources/Hiyori", base=.6, fade=True)
1
  • переменной _live2d_fade
init:
    $ _live2d_fade = True
1
2

# Интеграция Python модулей

Позволяет устанавливать сторонние Python модули в Ваш мод.

WARNING

Поддерживаются не все модули!

Все однопапочные модули (где весь функционал в одной папке, кроме dist-info папок, там скриптов нет) работают нормально. Модули с зависимостями от других модулей тоже работают нормально, не забудьте только их тоже импортировать. Если модулю для работы необходимы ещё .exe или .dll файлы, то, скорее всего, они не будут работать.

WARNING

Не забудьте:

  • заменить _mymod на постфикс своего мода
  • создать папку "python-packages", если используете функцию копирования модуля (copy_module)

Скачать скрипт

init python:
    class moduleInstaller_mymod:
        """
        :doc: ModuleInstaller object

        Установщик Python модулей для модов Бесконечного Лета

        `mod_name` : str
            Название корневой директории мода
        """

        def __init__(self, mod_name):
            self.mod_name = mod_name
            self.mod_folder = self.find_mod_folder()
            self.renpy_python_packages_folder_path = self.create_renpy_package_folder()

        def create_renpy_package_folder(self):
            renpy_python_packages = config.gamedir + "/python-packages"
            if not os.path.exists(renpy_python_packages):
                os.mkdir(renpy_python_packages)
            return renpy_python_packages

        def find_mod_folder(self):
            import fnmatch
            import os
            try:
                if os.path.exists(config.gamedir + "/" + self.mod_name): # Если находит папку с именем мода в папке game
                    mod_folder = config.gamedir + "/" + self.mod_name # Путь до самого мода идёт через game
                else:
                    for root, dirnames, filenames in os.walk('../../workshop/content/331470'):
                        for x in dirnames:
                            if x.endswith(self.mod_name): # Если находит папки с именем мода в папке workshop
                                mod_folder = os.path.join(root, x) # Путь до самого мода идёт через workshop
                                break
                return mod_folder.replace("\\", "/")
            except Exception as e:
                renpy.error("Error while finding mod folder: {}".format(e))

        def find_mod_python_packages_folder_path(self):
            try:
                module_source_folder = self.mod_folder + "/python-packages/"
                return module_source_folder.replace("\\", "/")
            except Exception as e:
                renpy.error("Error while finding mod python-packages folder path: {}".format(e))

        def download_module(self, module_name):
            try:
                module_destination_folder = self.renpy_python_packages_folder_path + "/" + module_name + "/"
                if not os.path.exists(module_destination_folder):
                    os.system("pip install --target game/python-packages {}".format(module_name))
            except Exception as e:
                renpy.error("Error while downloading module: {}".format(e))

        def copy_folder(self, src, dst):
            if not os.path.exists(dst):
                os.makedirs(dst)

            for item in os.listdir(src):
                s = os.path.join(src, item)
                d = os.path.join(dst, item)

                if os.path.isdir(s):
                    self.copy_folder(s, d)
                else:
                    with open(s, 'rb') as f_in:
                        with open(d, 'wb') as f_out:
                            f_out.write(f_in.read())

        def copy_module(self, module_name):
            try:
                module_source_folder = self.find_mod_python_packages_folder_path() + module_name
                module_destination_folder = self.renpy_python_packages_folder_path + "/" + module_name + "/"

                if not os.path.exists(module_destination_folder):
                    os.makedirs(module_destination_folder)

                for item in os.listdir(module_source_folder):
                    s = os.path.join(module_source_folder, item)
                    d = os.path.join(module_destination_folder, item)

                    if os.path.isdir(s):
                        self.copy_folder(s, d)
                    else:
                        with open(s, 'rb') as f_in:
                            with open(d, 'wb') as f_out:
                                f_out.write(f_in.read())
            except Exception as e:
                renpy.error("Error while copying module: {}".format(e))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

# Установка модуля

Для установки модуля (как у Вас, так и у потенциального игрока Вашего мода) должен быть установлен pip (идёт в комплекте с Python (opens new window))

Пример установки модуля:

init python:
    ModuleInstaller_mymod = ModuleInstaller_mymod("testmod")
    moduleInstaller_mymod.download_module("pydub") # Модуль pydub скачивается с помощью pip
1
2
3

# Копирование модуля

Так как слишком запарно для пользователя ставить на его систему Python, чтобы мод мог докачать нужные ему модули, можно использовать копирование модуля вместо установки: разработчик заранее скачивает нужный ему модуль, закидывает его в созданную в корневой директории мода папку python-packages и с помощью метода copy_module скрипт автоматически скопирует модуль в директорию python-packages самого БЛ, без необходимости читателю что-либо докачивать, чтобы запустить мод.

Пример копирования модуля:

init python:
   ModuleInstaller_mymod = ModuleInstaller_mymod("testmod")
   moduleInstaller_mymod.copy_module("pydub") # Модуль pydub копируется из папки python-packages Вашего мода "testmod" в папку "python-packages" самого БЛ
1
2
3

# Показ всех объявленных персонажей

Записывает всех существующих персонажей в БЛ в файл characters.txt (который автоматически создаётся в главной директории БЛ) и открывает его.

WARNING

Если установлены сторонние модификации, то в списке будут и персонажи из модов

Скачать скрипт

init python:
    import os
    listCharacters = {}
    for entry in globals():
        if isinstance(globals()[entry], renpy.character.ADVCharacter):
            listCharacters[entry] = globals()[entry]
    with open("characters.txt", "w") as fileCharacters:
        for entry in sorted(listCharacters):
            fileCharacters.write("Переменная: " + str(entry) + "\nИмя: " + str(listCharacters[entry].name) + "\nСвойства стиля имени: " + str(listCharacters[entry].who_args) + "\n\n")
    os.startfile("characters.txt")
1
2
3
4
5
6
7
8
9
10