After Width: | Height: | Size: 81 KiB |
After Width: | Height: | Size: 262 KiB |
After Width: | Height: | Size: 226 KiB |
After Width: | Height: | Size: 246 KiB |
After Width: | Height: | Size: 204 KiB |
After Width: | Height: | Size: 167 KiB |
After Width: | Height: | Size: 183 KiB |
After Width: | Height: | Size: 198 KiB |
After Width: | Height: | Size: 199 KiB |
After Width: | Height: | Size: 266 KiB |
After Width: | Height: | Size: 214 KiB |
@ -0,0 +1,54 @@ |
||||
The game loads an alternate storyline.rpy, this allows you to control the flow of the game's storytelling |
||||
Examples include: |
||||
- You want to inject more stuff inbetween chapters, maybe you hate time skip writing |
||||
- You want to have more of an after story kind of ordeal, for example expanding Ending 2 |
||||
- You want to replace the entire story route |
||||
You can still call the vanilla game's chapters like the intro (call chapter_1) for example but you might want to either copy the vanilla scripts and mix in your edits to have full control |
||||
|
||||
You will need to learn bit of Ren'Py & bit of actual Python anyways but you don't have to think too hard in learning anything new other than familiarizing with this game's script.rpy's already defined Character objects and images, you can freely ignore most of the UI and so on for the inexperienced but passionate artist and/or writer. |
||||
|
||||
Textbox limitation: ~300 characters / four lines, the maximum in the vanilla game's script is around roughly ~278 and that only barely overflows to four lines, so <200 characters / three lines of text should be fine. |
||||
|
||||
--- Ideal file structure of your mod --- |
||||
In the root of the mods folder: |
||||
folder_of_your_mod_name |
||||
- name_of_storyline.rpy |
||||
-> asset_folder |
||||
- asset.png |
||||
-> script_folder |
||||
- script.rpy |
||||
|
||||
name_of_storyline.rpy |
||||
``` |
||||
init python: |
||||
# Modding Support variables |
||||
# All mod rpy files must have title of their mod (this shows up on a button) |
||||
# and finally the label that controls the flow of dialogue |
||||
|
||||
mod_menu_access += [{ |
||||
'Name': "Mod Name", |
||||
'Label': "mod_storyline" |
||||
}]; |
||||
|
||||
image template_sample = Image("mods/folder_of_your_mod_name/asset_folder/asset.png") |
||||
|
||||
label mod_storyline: |
||||
call chapter_1_new |
||||
``` |
||||
|
||||
script_folder/script.rpy |
||||
``` |
||||
label chapter_1_new: |
||||
show template_sample at scenter |
||||
"Sample Text" |
||||
|
||||
hide template_sample |
||||
play music 'audio/OST/Those Other Two Weirdos.ogg' |
||||
show anon neutral flip at aright with dissolve |
||||
A "Sample Text" |
||||
|
||||
return |
||||
``` |
||||
|
||||
The funny thing is I don't even like 'fanfictions' to begin with but this mod support allows these 'fanfictions', ironic. |
||||
|
@ -0,0 +1,11 @@ |
||||
|
||||
|
||||
label chapter_2_new: |
||||
show template_sample at scenter |
||||
"Sample Text" |
||||
|
||||
hide template_sample |
||||
play music 'audio/OST/Those Other Two Weirdos.ogg' |
||||
show anon neutral flip at aright with dissolve |
||||
A "Sample Text" |
||||
return |
After Width: | Height: | Size: 468 B |
@ -0,0 +1,15 @@ |
||||
|
||||
init python: |
||||
# Modding Support variables |
||||
# All mod rpy files must have title of their mod (this shows up on a button) |
||||
# and finally the label that controls the flow of dialogue |
||||
|
||||
mod_menu_access += [{ |
||||
'Name': "Example Mod Name", |
||||
'Label': "storyline_ex" |
||||
}]; |
||||
|
||||
image template_sample = Image("mods_example/template/img/sample.png") |
||||
|
||||
label storyline_ex: |
||||
call chapter_2_new |
@ -1,100 +1,292 @@ |
||||
init python: |
||||
|
||||
# CONST PARAMS |
||||
ALLOW_ZOOM = False |
||||
GALLERY_COLS = 3 |
||||
GALLERY_CGS_PER_PAGE = 6 |
||||
PREFERRED_WIDTH = 432 #px (1920 * 0.225) |
||||
PREFERRED_HEIGHT = 243 #px (1080 * 0.225) |
||||
PREFERRED_ASPECT_RATIO = 16.0/9.0 # 1.7777.. |
||||
DEFAULT_WIDTH_SCALE_RATIO = round(float(PREFERRED_WIDTH) / float(1920), 4) |
||||
DEFAULT_HEIGHT_SCALE_RATIO = round(float(PREFERRED_HEIGHT) / float(1080), 4) |
||||
NOT_UNLOCKED_COVER = im.FactorScale("gui/gallery/unlocked_cg_button_cover.png", DEFAULT_WIDTH_SCALE_RATIO, DEFAULT_HEIGHT_SCALE_RATIO) |
||||
ACCEPTED_EXTENSIONS = ["jpg", "png"] |
||||
CG_PATHS = "images/cgs/" |
||||
|
||||
# GALLERY OBJECT |
||||
# Handles unlockables via ren'py |
||||
g = Gallery() |
||||
g.transition = dissolve |
||||
g.locked_button = NOT_UNLOCKED_COVER |
||||
|
||||
# GALLERY ITEMS |
||||
# Data structure that holds the data for each cg and button |
||||
# item is the key in the Gallery |
||||
# ext is the file extension |
||||
# { item: string; cg: Displayable; ext: string }[] |
||||
galleryItems = [] |
||||
ACCEPTED_EXTENSIONS = ["jpg", "webm"] |
||||
CG_PATHS = [ |
||||
#CG doesn't really make sense |
||||
{ 'path': "images/cgs/", 'name': "CG", 'eval': None }, |
||||
{ 'path': "images/animations/", 'name': "Animations", 'eval': None }, |
||||
{ 'path': "images/NotForKids!/", 'name': "Lewd", |
||||
'eval': 'persistent.lewd == True' |
||||
} |
||||
] |
||||
#path: folder, name: shows up in gallery, eval: runs eval() on string |
||||
|
||||
""" |
||||
Data structure that holds the data for each cg and button |
||||
item is name, fn is fullpath |
||||
ext is the file extension |
||||
{ item: str; fn: str; cg: Displayable; ext: str; wh: [] }[] |
||||
(reference in this init python, actually used in screens) |
||||
""" |
||||
gallery_items = [] |
||||
|
||||
# key dict pair, cg <-> cgs' galleryitems [] |
||||
gallery_dic = {} # |
||||
for cp in CG_PATHS: |
||||
gallery_dic[cp['name']] = [] # |
||||
|
||||
# Make a scaled cg button |
||||
# (cg: string; ext: string; w: float; h: float; unlocked?: boolean): Displayable |
||||
def cg(fname, ext, w, h): |
||||
scaleFactor = getBoxNormalizerRatio(w, h) |
||||
return im.FactorScale(CG_PATHS + fname + "." + ext, scaleFactor["x"], scaleFactor["y"], False) |
||||
|
||||
# Create an object in g:Gallery, add to galleryItems |
||||
# (imageName: string; ext: string; w: float; h: float) -> None |
||||
def addGalleryItem(imageName, ext, w, h): |
||||
g.button(imageName) |
||||
g.image(imageName) |
||||
|
||||
horizontalPan = Pan((w - 1920, h - 1080), (0, h - 1080), 30.0) |
||||
verticalPan = Pan((w - 1920, h - 1080), (w - 1920, 0), 30.0) |
||||
g.transform(horizontalPan if w > h else verticalPan) #TODO: niceify |
||||
|
||||
str = "renpy.seen_image('"+imageName+"')" |
||||
g.condition(str) |
||||
|
||||
galleryItems.append({ |
||||
"item": imageName, |
||||
"cg": cg(imageName, ext, w, h), |
||||
"ext": ext |
||||
}) |
||||
return |
||||
# (cg: string; ext: string; w: float |
||||
def cg(fname, ext, w): |
||||
scale = PREFERRED_WIDTH * 100.0 / w / 100.0 |
||||
#scale = box_ratio(wh) |
||||
return im.FactorScale(fname, scale, scale, False) |
||||
|
||||
# Reads /images/cgs dir for all image files |
||||
# Populates g:Gallery and galleryItems |
||||
# Appends extra spaces at the end |
||||
# Populates galleryItems |
||||
# () -> None |
||||
def loadGallery(): |
||||
|
||||
list_img = renpy.list_images() |
||||
|
||||
#if ext is "webm": |
||||
# Add each image to the gallery |
||||
for str in list_img: |
||||
_str = CG_PATHS+str+"."+ACCEPTED_EXTENSIONS[0] |
||||
if renpy.loadable(_str): #brute force |
||||
image = renpy.image_size(Image(_str)) |
||||
addGalleryItem(str, ACCEPTED_EXTENSIONS[0], image[0], image[1]) |
||||
return |
||||
|
||||
for cp in CG_PATHS: |
||||
for ext in ACCEPTED_EXTENSIONS: |
||||
path = cp['path'] |
||||
_str = path+str+"."+ext |
||||
|
||||
# Returns what params to call im.FactorScale with for cg button size |
||||
# Basically the delta diff dimensions |
||||
# (w: int; h: int) -> { x: float; y: float } |
||||
def getBoxNormalizerRatio(w, h): |
||||
x = round(float(PREFERRED_WIDTH) / float(w), 4) |
||||
y = round(float(PREFERRED_HEIGHT) / float(h), 4) |
||||
if renpy.loadable(_str): #brute force |
||||
image = renpy.image_size(Image(_str)) |
||||
|
||||
return { "x": x, "y": y } |
||||
gallery_dic[cp['name']] += [{ |
||||
"item": str, |
||||
"fn": _str, |
||||
"cg": cg(_str, ext, image[0]), |
||||
"ext": ext, |
||||
"wh": image |
||||
}] |
||||
return |
||||
|
||||
# Call to loading the gallery |
||||
loadGallery() |
||||
|
||||
## CG Gallery screen ######################################################## |
||||
## A screen that shows the image gallery |
||||
screen cg_gallery(): |
||||
# hard code the webm because renpy is really dumb and doesn't add Movies properly until much later |
||||
fang_webm = 'images/animations/fang tail.webm' |
||||
gallery_dic['Animations'] = [{ |
||||
"item": 'fang tail', |
||||
"fn": fang_webm, |
||||
"cg": Movie(fang_webm),#cg(_str, 'webm', 1920), |
||||
"ext": 'webm', |
||||
"wh": [1920, 1080] |
||||
}] |
||||
|
||||
#for zooming in and out |
||||
zoom_arr = [0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0, 1.125, 1.25, 1.5, 1.75, 2.0] |
||||
|
||||
""" |
||||
for x in range(1,5): |
||||
_zoom = 1.0 |
||||
_zoom *= 1+(x*0.25) |
||||
zoom_arr.append(_zoom) |
||||
|
||||
for y in range(9,1,-1): |
||||
_zoom = 1.0 |
||||
_zoom *= (y*0.125) |
||||
zoom_arr.append(_zoom) |
||||
|
||||
zoom_arr.sort() |
||||
""" |
||||
""" |
||||
'Recursive' / Loopable / Roundtrip Screens |
||||
_0 <-> _1 |
||||
""" |
||||
#There is renpy.restart_interaction but since I wrote all this, it's too late |
||||
#screen cg_gallery(flag, __yoffset = 0, origin = 'CG'): |
||||
screen cg_gallery_0(__yoffset = 0, origin = 'CG'): |
||||
tag menu |
||||
use cg_gallery('1', __yoffset, origin) |
||||
screen cg_gallery_1( __yoffset = 0, origin = 'CG'): |
||||
tag menu |
||||
use cg_gallery('0', __yoffset, origin) |
||||
|
||||
#screen view_image(fn, _origin, zoom=1): |
||||
screen view_image_a(fn, _origin, zoom = zoom_arr.index(1.0)): |
||||
tag menu |
||||
use view_image(fn, _origin, zoom, 'b') |
||||
screen view_image_b(fn, _origin, zoom = zoom_arr.index(1.0)): |
||||
tag menu |
||||
use view_image(fn, _origin, zoom, 'a') |
||||
|
||||
""" |
||||
CG Gallery screen - A screen that shows the image gallery |
||||
Basically Gallery Object has terrible defaults, so I just wrote my own stuff |
||||
""" |
||||
screen cg_gallery(flag, __yoffset = 0, origin = 'CG'): |
||||
|
||||
if main_menu: |
||||
key "game_menu" action ShowMenu("main_menu") |
||||
|
||||
|
||||
frame: |
||||
pass |
||||
add gui.main_menu_background |
||||
add gui.game_menu_background |
||||
|
||||
tag menu |
||||
|
||||
python: |
||||
empty_spaces = gallery_rows = item_counter = 0 |
||||
|
||||
gallery_items = gallery_dic[origin] |
||||
items = len(gallery_items) |
||||
gallery_rows = (items / GALLERY_COLS) + 1 |
||||
empty_spaces = GALLERY_COLS - (items % GALLERY_COLS) |
||||
|
||||
|
||||
vbox: |
||||
transform: |
||||
zoom 0.95 |
||||
hbox: |
||||
style_prefix "navigation" |
||||
xalign 0.5 |
||||
|
||||
spacing gui.navigation_spacing |
||||
|
||||
for cp in CG_PATHS: |
||||
if cp['name'] == origin: |
||||
textbutton _(cp['name']) text_color gui.selected_color text_xalign 0.5 |
||||
else: |
||||
if cp['eval'] is None: |
||||
textbutton _(cp['name']) action ShowMenu('cg_gallery_'+flag, 0, cp['name']) text_xalign 0.5 |
||||
elif eval(cp['eval']): |
||||
textbutton _(cp['name']) action ShowMenu('cg_gallery_'+flag, 0, cp['name']) text_xalign 0.5 |
||||
else: |
||||
textbutton _(cp['name']) text_xalign 0.5 |
||||
textbutton _("Return") action ShowMenu('main_menu') text_xalign 0.5 |
||||
|
||||
if _in_replay: |
||||
textbutton _("End Replay") action EndReplay(confirm=True) |
||||
elif not main_menu: |
||||
textbutton _("Main Menu") action MainMenu() |
||||
|
||||
transform: |
||||
zoom 0.95 |
||||
xcenter 0.525 |
||||
ycenter 0.525 |
||||
|
||||
viewport: |
||||
yinitial __yoffset |
||||
scrollbars "vertical" |
||||
mousewheel True |
||||
draggable True |
||||
pagekeys True |
||||
xfill True |
||||
|
||||
grid GALLERY_COLS gallery_rows: |
||||
xcenter 0.5 |
||||
ycenter 0.5 |
||||
for item in gallery_items: |
||||
# Should properly fix with actual margin difference but good |
||||
# enough or the actual position |
||||
python: |
||||
item_counter += 1 |
||||
yoffset = item_counter / 3 * PREFERRED_HEIGHT * 1.15 |
||||
yoffset = int( yoffset + (PREFERRED_HEIGHT * 1.15)) |
||||
|
||||
use flag_button(item, yoffset, origin) |
||||
|
||||
for i in range(0, empty_spaces): |
||||
null height 20 |
||||
|
||||
|
||||
""" |
||||
if/else flow control & extra parameters for Buttons |
||||
""" |
||||
screen flag_button(item, yoffset, origin): |
||||
python: |
||||
items = len(galleryItems) |
||||
galleryRows = (items / GALLERY_COLS) + 1 |
||||
extraSpaces = GALLERY_COLS - (items % GALLERY_COLS) |
||||
flag = renpy.seen_image(item['item']) |
||||
|
||||
if flag: |
||||
button: |
||||
if item['ext'] == "webm": |
||||
action Replay('fang_movie')#ShowMenu('view_movie', item, ShowMenu('cg_gallery_0', yoffset, origin)) |
||||
else: |
||||
action ShowMenu('view_image_a', item, ShowMenu('cg_gallery_0', yoffset, origin)) |
||||
xcenter 0.5 ycenter 0.5 |
||||
padding (1,0,1,2) |
||||
vbox: |
||||
text item["item"] xalign 0.5 |
||||
add item["cg"] fit 'contain' xcenter 0.5 ycenter 0.5 size (PREFERRED_WIDTH, PREFERRED_HEIGHT) |
||||
else: |
||||
vbox: |
||||
ymaximum PREFERRED_HEIGHT |
||||
xcenter 0.5 ycenter 0.5 |
||||
text "? ? ?" xalign 0.5 |
||||
add NOT_UNLOCKED_COVER |
||||
|
||||
|
||||
screen view_movie(item, _origin): |
||||
tag menu |
||||
key "game_menu" action _origin |
||||
python: |
||||
renpy.movie_cutscene(item['item'], None, -1) |
||||
frame: |
||||
pass |
||||
#scene fang tail with fade |
||||
|
||||
|
||||
""" |
||||
view_image, Loads the image in fullscreen with viewport control. |
||||
""" |
||||
screen view_image(item, _origin, zoom = zoom_arr.index(1.0), flag='a'): |
||||
python: |
||||
zoom_a = zoom+1 |
||||
zoom_a_f = ShowMenu('view_image_'+flag, item, _origin, zoom_a) |
||||
zoom_b = zoom-1 |
||||
zoom_b_f = ShowMenu('view_image_'+flag, item, _origin, zoom_b) |
||||
|
||||
tag menu |
||||
use game_menu(_("Gallery"), scroll="viewport"): |
||||
grid GALLERY_COLS galleryRows: |
||||
spacing 8 |
||||
for item in galleryItems: |
||||
# vbox: |
||||
# text item["item"] size 8 |
||||
add g.make_button(item["item"], item["cg"], xalign = 0.5, yalign = 0.5) |
||||
# Add empty items to fill grid after last cg button |
||||
for i in range(0, extraSpaces): |
||||
null height 20 |
||||
key "game_menu" action _origin |
||||
|
||||
# mousewheel & insert+delete |
||||
if (ALLOW_ZOOM): |
||||
if zoom < len(zoom_arr)-1: #zoom in |
||||
key 'mousedown_4' action zoom_a_f |
||||
key 'K_INSERT' action zoom_a_f |
||||
if zoom > 0: #and (item['wh'][0] <= 1920 or item['wh'][1] <= 1080): |
||||
key 'mousedown_5' action zoom_b_f |
||||
key 'K_DELETE' action zoom_b_f |
||||
|
||||
viewport id "vie": |
||||
#Ren'Py isn't smart enough to not edgescroll while pressed, |
||||
#so we'll have to disable this for mobile |
||||
if renpy.variant("pc"): |
||||
edgescroll (300, 1800) |
||||
draggable True |
||||
arrowkeys True |
||||
pagekeys True |
||||
xfill False |
||||
yfill False |
||||
add item['fn'] zoom zoom_arr[zoom] anchor (0.55, 0.55) |
||||
|
||||
#Reuse quick buttons, Ren'Py handles touch input lazy, it doesn't have |
||||
#double finger pinch zoom, it translates taps as mouse events - have to use |
||||
#buttons |
||||
hbox: |
||||
style_prefix "quick" |
||||
xalign 0.5 |
||||
yalign 0.975 |
||||
if (ALLOW_ZOOM) and renpy.variant("small"): |
||||
use quick_buttons("gui/button/uioptionbuttons/template_idle.png", |
||||
[ |
||||
[ "+", zoom_a_f ], |
||||
[ "-", zoom_b_f ], |
||||
[ "Return", zoom_b_f ] |
||||
] ) |
||||
else: |
||||
use quick_buttons("gui/button/uioptionbuttons/template_idle.png", |
||||
[ |
||||
[ "Return", _origin ] |
||||
] ) |
||||
|
||||
|
@ -0,0 +1,69 @@ |
||||
# Mod Menu screen ############################################################ |
||||
## |
||||
## Handles jumping to the mods scripts |
||||
## Could be more lean but if this is going to one of last time I touch the UI, |
||||
## then fine by me |
||||
## |
||||
#similar to quick_button funcs |
||||
screen mod_menu_button(filename, label, function): |
||||
button: |
||||
xmaximum 600 |
||||
ymaximum 129 |
||||
action function |
||||
fixed: |
||||
add filename xalign 0.5 yalign 0.5 zoom 0.9 |
||||
text label xalign 0.5 yalign 0.5 xanchor 0.5 size 34 |
||||
|
||||
# arr is [{ |
||||
# 'Name': string (name that appears on the button) |
||||
# 'Label': string (jump label) |
||||
# }, { .. } ] |
||||
# Reuse the same image string and keep things 'neat'. |
||||
screen mod_menu_buttons(filename, arr): |
||||
for x in arr: |
||||
use mod_menu_button(filename, x['Name'], Start(x['Label'])) |
||||
|
||||
screen mod_menu(): |
||||
|
||||
tag menu |
||||
|
||||
style_prefix "main_menu" |
||||
|
||||
add gui.main_menu_background |
||||
|
||||
frame: |
||||
xsize 420 |
||||
yfill True |
||||
background "gui/overlay/main_menu.png" |
||||
|
||||
#side_yfill True |
||||
vbox: |
||||
xpos 1940 |
||||
yalign 0.03 |
||||
if persistent.splashtype == 1: |
||||
add "gui/sneedgame.png" |
||||
else: |
||||
add "gui/snootgame.png" |
||||
|
||||
viewport: |
||||
# this could be better but its ok for now |
||||
xpos 1885-540 |
||||
xmaximum 540 |
||||
ymaximum 0.8 |
||||
ypos 200 |
||||
yinitial 0 |
||||
scrollbars "vertical" |
||||
mousewheel True |
||||
draggable True |
||||
pagekeys True |
||||
vbox: |
||||
#xpos 1885 |
||||
spacing 18 |
||||
#yalign 0.98 |
||||
|
||||
#buttons are messed up but that's ok |
||||
use mod_menu_button("gui/button/menubuttons/template_idle.png", "Return", ShowMenu("main_menu")) |
||||
if len(mod_menu_access) is not 0: |
||||
use mod_menu_buttons("gui/button/menubuttons/template_idle.png", mod_menu_access ) |
||||
else: |
||||
use mod_menu_button("gui/button/menubuttons/template_idle.png", "You have no mods", None) |