# Руководство с примерами кода
- Включение режима разработчика
- Автоматическое объявление файлов
- Автоматическое объявление файлов (для начинающих)
- Автоматическое объявление персонажей и интересные плюшки
- Эффект падающих частиц
- Отображение музыки, играющей в данный момент
- Открытие файла
- Создание собственной карты
- Создание собственной карты (для начинающих)
- Замена интерфейса
- Создание галереи
- Перевод мода
- Интеграция Live2D
- Интеграция Python модулей
- Показ всех объявленных персонажей
# Включение режима разработчика
Будут доступны команды:
Shift + D
- меню разработчикаShift + O
- консоль
WARNING
Для того, чтобы игра распознала сочетания клавиш, нужно переключиться на латинскую раскладку клавиатуры.
init:
$ config.developer = True
$ config.console = True
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()
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 в качестве корневой директории мода.
2
При необходимости можем добавить постфикс для объявленных файлов или вовсе вместо объявления записывать их в отдельный файл autoinit_assets.rpy
(чтобы объявления ресурсов, записанных в файл, сработало, необходимо перезагрузить БЛ, чтобы rpy
файл скомпилировался).
init:
$ autoinitialization_mymod = autoInitialization("mymod", "myPostfix")
2
init:
$ autoinitialization_mymod = autoInitialization("mymod", write_into_file=True)
2
# Изображения
show bg ext_square_sunset # Показ изображения ext_square_sunset из папки bg
# Изображения (с префиксом)
show bg ext_square_sunset_myPostfix # Показ изображения ext_square_sunset из папки bg с префиксом mymod
# Спрайты
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
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
2
3
4
5
6
# Аудио
play sound mymusic # Воспроизведение файла mymusic на канале sound
# Аудио (с префиксом)
play sound mymusic_myPostfix # Воспроизведение файла mymusic на канале sound и постфиксом myPostfix
# Заключение
Если необходима дополнительная информация, каждый метод класса содержит подробные комментарии работы с примерами вызова объявленных ресурсов мода.
# Автоматическое объявление файлов (для начинающих)
Данный отрезок кода автоматически объявляет все изображения и звуки Вашего мода.
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
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')
2
3
4
# Объявление ресурсов с покраской спрайтов
init:
# Если мод находится в папке `my_mod`,
# а спрайты - в `my_mod/images/sprites`, то:
$ define_assets('my_mod', sprites_folder='images/sprites')
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"]
}
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)
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 "Я — новый персонаж!"
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()
2
3
4
5
6
7
8
# Использование
label mymod:
new "Говорю в ADV-режиме."
window hide
$ set_mode_mymod(nvl)
pause(1) # Пауза для плавного перехода
window show
new "Говорю в NVL-режиме."
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()
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 "Цвет моего имени изменился!"
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()
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
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))
В примере выше указаны параметры, заданные по умолчанию. Вы можете изменить их по вашему желанию:
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
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
, то появится его название (которое мы указали в словаре).
- Объявляем словарь, где ключ — путь до файла, а значение — его название, которое будет выводиться.
init python:
music_data = {"sound/music/pile.ogg": "Between August and December - Pile"}
# Словарь с музыкой
# Ключ словаря - путь до трека. Значение - название трека
# Ключ — 'sound/music/pile.ogg', Значение — 'Between August and December - Pile'
2
3
4
5
- Создаём функцию, что будет отвечать за вывод на экран текста.
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 # Если музыка не играет, то мы возвращаем пустой текст
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
- Объявляем изображение как DynamicDisplayable.
init python:
renpy.image("playing_music", DynamicDisplayable(show_music)) # Объявляем изображение
# Теперь это изображение является нашей функцией, что будет показывать название трека, который играет в данный момент.
# Рекомендуется использование более уникальных имён для словаря с музыкой, названий функции для треков и т.п, ибо возможны конфликты.
2
3
4
TIP
Данный способ объявления работает в init python
. Для обычного init используйте:
init:
image playing_music = DynamicDisplayable(show_music)
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 # Останавливаем воспроизведение музыки
"И если музыка не играет, то мы ничего не увидим"
2
3
4
5
6
7
8
TIP
Если вы хотите добавить отображение играемой музыки в экран, то можно сделать так:
add 'playing_music' # Будет добавлять в экран заранее объявленый DynamicDisplayable
# Открытие файла
Данный код позволяет открыть необходимый файл во время игры.
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])
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`, продолжается игра."
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
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
"А вот и лейбл нашего домика."
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
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
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 позволяет делать кнопку чувствительной, пока действует какое-то условие.
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 позволяет делать кнопку чувствительной, пока действует какое-то условие.
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:
'Я забрел куда-то не туда.'
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()
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)() # Выходим в главное меню.
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)]
# Создание галереи
Код представляет собой полноценную галерею, поделённую на 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
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
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
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)))
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()
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
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
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
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"] = {}
2
3
Затем создаём внутри значения с нашем именем создаём ещё два: одно для русского перевода, второе — для английского.
translation_new["translator"]["name"][None] = u"Переводчик"
translation_new["translator"]["name"]["english"] = "Translator"
2
Теперь объявляем сам мод, но с именем, что будет брать значение из нашего словаря с переводом в зависимости от установленного языка игры.
mods["translator_mod"] = translation_new["translator"]["name"][_preferences.language]
# Перевод персонажа
Теперь переведём персонажа. Для этого создаём в нашем словаре значение для персонажей, а внутри него — ещё одно значение с нашим персонажем.
translation_new["translator"]["characters"] = {}
translation_new["translator"]["characters"]["samantha"] = {}
2
Создаём значения с переводом на русский и английский язык.
translation_new["translator"]["characters"]["samantha"]["english"] = "Samantha"
translation_new["translator"]["characters"]["samantha"][None] = "Саманта"
2
И объявляем нашего персонажа со значением для имени, что будет браться из установленного языка игры.
translator_sam = Character(translation_new["translator"]["characters"]["samantha"][_preferences.language])
# Перевод текста
Прежде всего делаем проверку на то, имеется ли в списке store
наша будущая переменная для перевода текста. Если нет, то добавляем и ставим по умолчанию русский язык.
if not hasattr(store, "persistent.translate_text_lang"):
persistent.translate_text_lang = "ru"
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 [ ]
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"
2
TIP
Как вариант, можно создать кнопку в меню мода, что будет переключать язык повествования.
if persistent.translate_text_lang == "ru":
textbutton "Язык повествования (Русский)":
action Function(translate_toggle_lang())
else:
textbutton "Язык повествования (Английский)":
action Function(translate_toggle_lang())
2
3
4
5
6
Объявляем нашли тэги.
config.custom_text_tags["en"] = translate_en_tag
config.custom_text_tags["ru"] = translate_ru_tag
2
Пример написания перевода текста представлен ниже.
label translator_mod:
translator_sam "{en}Hello!{/en}{ru}Привет!{/ru}" # Саманта (или Samantha, если установлен английский язык игры) произносит "Привет!", если переменная равна "ru", если же равно "en", то "Hello!"
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]
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)
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
2
3
4
5
6
# Пример использования
image eileen moving = MyLive2D("Путь до корневой папки Live2D спрайта", fallback="eileen happy") # При возможности воспроизвести будет использоваться Live2D версия спрайта, если же невозможно, то будет использован статичный спрайт `eileen happy`. Если `fallback` не заполнять, то вместо Live2D спрайта будет выводиться текст о том, что невозможно воспроизвести Live2D.
# Использование анимаций
Движения хранятся в папке motions, эмоции в папке expressions. Названия движений и эмоций берутся из файлов Live2D, затем вводятся в нижний регистр, и если они начинаются с имени спрайта, за которым следует подчеркивание, то этот префикс удаляется.
Название файла движения - Epsilon_idle_01.motion3.json
, следовательно, название движения - idle_01
Название файла эмоции - Angry.exp3.json
, название эмоции - angry
# Движение
show Epsilon idle_01
# Эмоция
show Epsilon angry
# Движение и эмоция одновременно
show Epsilon idle_01 angry
# Изменение названия анимации
Для удобства при объявлении Live2D персонажа Вы можете с помощью параметра aliases
изменить название анимации/анимаций на более удобное.
init:
image hiyori = Live2D("Resources/Hiyori", base=.6, aliases={"idle" : "m01"})
label mymod:
show hiyori idle # эквивалент show hiyori m01
2
3
4
5
# Плавная смена анимаций
RenPy поддерживает плавную смену анимации при работе с Live2D. Обычно, когда Ren'Py переходит от одной анимации к другой, переход происходит резко - одна анимация останавливается, а другая запускается.
Live2D поддерживает другую модель, в которой старая анимация может плавно переходить в новую, с интерполяцией параметров. Считайте, что персонаж перемещает свои руки в нужное положение перед началом следующей анимации, а не резко переходит из одной анимации в другую.
Затухание движения контролируется с помощью:
- параметра
fade
при объявлении персонажа. ЕслиTrue
, используется затухание анимации, а еслиFalse
, то происходит резкая смена анимации.
image hiyori = Live2D("Resources/Hiyori", base=.6, fade=True)
- переменной
_live2d_fade
init:
$ _live2d_fade = True
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))
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
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" самого БЛ
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")
2
3
4
5
6
7
8
9
10