diff --git a/game/achievement_backend.rpy b/game/achievement_backend.rpy new file mode 100644 index 0000000..b4f405e --- /dev/null +++ b/game/achievement_backend.rpy @@ -0,0 +1,492 @@ +################################################################################ +## +## Achievements for Ren'Py by Feniks (feniksdev.itch.io / feniksdev.com) +## +################################################################################ +## This file contains code for an achievement system in Ren'Py. It is designed +## as a wrapper to the built-in achievement system, so it hooks into the +## Steam backend as well if you set up your achievement IDs the same as in +## the Steam backend. +## +## If you use this code in your projects, credit me as Feniks @ feniksdev.com +## +## If you'd like to see how to use this tool, check the other file, +## achievements.rpy! +## +## Leave a comment on the tool page on itch.io or an issue on the GitHub +## if you run into any issues. +################################################################################ +init -50 python: + import datetime, time + from re import sub as re_sub + + TAG_ALPHABET = "abcdefghijklmnopqrstuvwxyz" + def get_random_screen_tag(k=4): + """Generate a random k-letter word out of alphabet letters.""" + + # Shuffle the list and pop k items from the front + alphabet = list(store.TAG_ALPHABET) + renpy.random.shuffle(alphabet) + ## Add the time onto the end so there are no duplicates + return ''.join(alphabet[:k] + [str(time.time())]) + + + class Achievement(): + """ + A class with information on in-game achievements which can be extended + to use with other systems (e.g. Steam achievements). + + Attributes: + ----------- + name : string + The human-readable name of this achievement. May have spaces, + apostrophes, dashes, etc. + id : string + The code-friendly name of this achievement (which can be used for + things like the Steam backend). Should only include letters, + numbers, and underscores. If not provided, name will be sanitized + for this purpose. + description : string + A longer description for this achievement. Optional. + unlocked_image : Displayable + A displayable to use when this achievement is unlocked. + locked_image : Displayable + A displayable to use when this achievement is locked. If not + provided, requires an image named "locked_achievement" to be + declared somewhere. + stat_max : int + If provided, an integer corresponding to the maximum progress of + an achievement, if the achievement can be partially completed + (e.g. your game has 24 chapters and you want this to tick up + after every chapter, thus, stat_max is 24). The achievement is + unlocked when it reaches this value. + stat_progress : int + The current progress for the stat. + stat_modulo : int + The formula (stat_progress % stat_modulo) is applied whenever + achievement progress is updated. If the result is 0, the + progress is shown to the user. By default this is 0 so all updates + to stat_progress are shown. Useful if, for the supposed 24-chapter + game progress stat, you only wanted to show updates every time the + player got through a quarter of the chapters. In this case, + stat_modulo would be 6 (24//4). + hidden : bool + True if this achievement's description and name should be hidden + from the player. + hide_description : bool + True if this achievement's description should be hidden from the + player. Can be set separately from hidden, e.g. with hidden=True + and hide_description=False, the player will see the name but not + the description. + timestamp : Datetime + The time this achievement was unlocked at. + """ + ## A list of all the achievements that exist in this game, + ## to loop over in the achievements screen. + all_achievements = [ ] + achievement_dict = dict() + def __init__(self, name, id=None, description=None, unlocked_image=None, + locked_image=None, stat_max=None, stat_modulo=None, hidden=False, + stat_update_percent=1, hide_description=None): + + self._name = name + # Try to sanitize the name for an id, if possible + self.id = id or re_sub(r'\W+', '', name) + + self._description = description or "" + self.unlocked_image = unlocked_image or None + self.locked_image = locked_image or "locked_achievement" + + self.stat_max = stat_max + self.stat_modulo = stat_modulo + if stat_update_percent != 1 and stat_modulo != 0: + raise Exception("Achievement {} has both stat_update_percent and stat_modulo set. Please only set one.".format(self.name)) + ## Figure out the modulo based on the percent + if stat_update_percent > 1: + ## Basically, if stat_max % stat_modulo == 0, then it updates. + ## So if it updates every 10%, then stat_max / stat_modulo = 10 + self.stat_modulo = int(stat_max * (stat_update_percent / 100.0)) + + self.hidden = hidden + if hide_description is None: + self.hide_description = hidden + else: + self.hide_description = hide_description + + # Add to list of all achievements + self.all_achievements.append(self) + # Add to the dictionary for a quick lookup + self.achievement_dict[self.id] = self + + # Register with backends + achievement.register(self.id, stat_max=stat_max, + stat_modulo=stat_modulo or None) + + def get_timestamp(self, format="%b %d, %Y @ %I:%M %p"): + """ + Return the timestamp when this achievement was granted, + using the provided string format. + """ + if self.has(): + return datetime.datetime.fromtimestamp( + self._timestamp).strftime(format) + else: + return "" + + @property + def _timestamp(self): + if store.persistent.achievement_timestamp is not None: + return store.persistent.achievement_timestamp.get(self.id, None) + else: + return None + + @property + def timestamp(self): + """Return the timestamp when this achievement was granted.""" + if self.has(): + try: + ts = datetime.datetime.fromtimestamp(self._timestamp) + except TypeError: + if config.developer: + print("WARNING: Could not find timestamp for achievement with ID {}".format(self.id)) + return "" + return __("Unlocked ") + ts.strftime(__( + "%b %d, %Y @ %I:%M %p{#achievement_timestamp}")) + else: + return "" + + @_timestamp.setter + def _timestamp(self, value): + """Set the timestamp for this achievement.""" + if store.persistent.achievement_timestamp is not None: + store.persistent.achievement_timestamp[self.id] = value + + @property + def idle_img(self): + """Return the idle image based on its locked status.""" + if self.has(): + return self.unlocked_image + else: + return self.locked_image + + @property + def name(self): + """ + Returns the name of the achievement based on whether it's + hidden or not. + """ + if self.hidden and not self.has(): + return _("???{#hidden_achievement_name}") + else: + return self._name + + @property + def description(self): + """ + Returns the description of the achievement based on whether it's + hidden or not. + """ + if self.hide_description and not self.has(): + if self.hide_description is True: + return _("???{#hidden_achievement_description}") + else: + return self.hide_description + else: + return self._description + + @property + def stat_progress(self): + """Return this achievement's progress stat.""" + return self.get_progress() + + def add_progress(self, amount=1): + """ + Increment the progress towards this achievement by amount. + """ + self.progress(min(self.stat_progress+amount, self.stat_max)) + + ## Wrappers for various achievement functionality + def clear(self): + """Clear this achievement from memory.""" + return achievement.clear(self.id) + + def get_progress(self): + """Return this achievement's progress.""" + return achievement.get_progress(self.id) + + def grant(self): + """ + Grant the player this achievement, and show a popup if this is + the first time they've gotten it. + """ + has_achievement = self.has() + x = achievement.grant(self.id) + if not has_achievement: + # First time this was granted + self.achievement_popup() + # Save the timestamp + self._timestamp = time.time() + # Callback + if myconfig.ACHIEVEMENT_CALLBACK is not None: + renpy.run(myconfig.ACHIEVEMENT_CALLBACK, self) + # Double check achievement sync + achievement.sync() + return x + + def has(self): + """Return True if the player has achieved this achievement.""" + return achievement.has(self.id) + + def progress(self, complete): + """ + A plugin to the original Achievement class. Sets the current + achievement progress to "complete". + """ + has_achievement = self.has() + x = achievement.progress(self.id, complete) + if not has_achievement and self.has(): + # First time this was granted + self.achievement_popup() + # Save the timestamp + self._timestamp = time.time() + # Callback + if myconfig.ACHIEVEMENT_CALLBACK is not None: + renpy.run(myconfig.ACHIEVEMENT_CALLBACK, self) + return x + + def achievement_popup(self): + """ + A function which shows an achievement screen to the user + to indicate they were granted an achievement. + """ + + if renpy.is_init_phase(): + ## This is init time; we don't show a popup screen + return + elif not self.has(): + # Don't have this achievement, so it doesn't get a popup. + return + elif not myconfig.SHOW_ACHIEVEMENT_POPUPS: + # Popups are disabled + return + + if achievement.steamapi and not myconfig.INGAME_POPUP_WITH_STEAM: + # Steam is detected and popups shouldn't appear in-game. + return + + # Otherwise, show the achievement screen + for i in range(10): + if store.onscreen_achievements.get(i, None) is None: + store.onscreen_achievements[i] = True + break + # Generate a random tag for this screen + tag = get_random_screen_tag(6) + ## Play a sound, if provided + if myconfig.ACHIEVEMENT_SOUND: + renpy.music.play(myconfig.ACHIEVEMENT_SOUND, + channel=myconfig.ACHIEVEMENT_CHANNEL) + renpy.show_screen('achievement_popup', a=self, tag=tag, num=i, + _tag=tag) + + def AddProgress(self, amount=1): + """Add amount of progress to this achievement.""" + return Function(self.add_progress, amount=amount) + + def Progress(self, amount): + """Set this achievement's progress to amount.""" + return Function(self.progress, amount) + + def Toggle(self): + """ + A developer action to easily toggle the achieved status + of a particular achievement. + """ + return [SelectedIf(self.has()), + If(self.has(), + Function(self.clear), + Function(self.grant))] + + def Grant(self): + """ + An action to easily achieve a particular achievement. + """ + return Function(self.grant) + + @classmethod + def reset(self): + """ + A class method which resets all achievements and clears all their + progress. + """ + for achievement in self.all_achievements: + achievement.clear() + + @classmethod + def Reset(self): + """ + A class method which resets all achievements and clears all their + progress. This is a button action rather than a function. + """ + return Function(self.reset) + + @classmethod + def num_earned(self): + """ + A class property which returns the number of unlocked achievements. + """ + return len([a for a in self.all_achievements if a.has()]) + + @classmethod + def num_total(self): + """ + A class property which returns the total number of achievements. + """ + return len(self.all_achievements) + + + class LinkedAchievement(): + """ + A class which can be used as part of an achievement callback to + trigger an achievement when some subset of achievements is unlocked. + + Attributes: + ----------- + links : dict + A dictionary of the form {achievement.id : [list of final + achievement ids]}. This is a reverse of the dictionary passed in + to the constructor and is used to look up what final achievements + are tied to a given achievement. + final_to_list : dict + A dictionary of the form {final_achievement.id : [list of + achievement ids to check]}. This is the same as the dictionary + passed in to the constructor, and is used to look up what + achievements are needed to unlock a given final achievement. + unlock_after_all : string + If this is set to an achievement ID, then that achievement is + unlocked after all other achievements are unlocked. + """ + def __init__(self, **links): + """ + Create a LinkedAchievement to be used as a callback. + + Parameters: + ---------- + links : dict + A dictionary of the form {final_achievement.id : [list of + achievement ids to check]}. When all of the achievements in the + list are unlocked, the final achievement is unlocked. + """ + ## links comes in the form of + ## {final_achievement.id : [list of achievement ids to check]} + self.links = dict() + + values = links.values() + if len(values) == 1 and 'all' in values: + ## Special case for an achievement that's achieved after + ## getting all achievements + self.unlock_after_all = ''.join(links.keys()) + self.final_to_list = links + return + else: + self.unlock_after_all = False + + ## Reverse-engineer a dictionary which corresponds to the things + ## that are checked, and what they tie back to. + for final_achievement, check_achievements in links.items(): + for check_achievement in check_achievements: + if check_achievement == final_achievement: + continue + if check_achievement not in links: + self.links[check_achievement] = [final_achievement] + else: + self.links[check_achievement].append(final_achievement) + + self.final_to_list = links + + def __call__(self, the_achievement): + """ + A method which is called when an achievement is unlocked. + It checks if the achievement is part of a list of achievements + which are needed to unlock a given final achievement, and if the + conditions needed to unlock that final achievement are met. + If so, it grants that achievement. + + Parameters: + ----------- + the_achievement : Achievement + The achievement which was just granted. + """ + if self.unlock_after_all: + ## This unlocks after all achievements are earned + if all([a.has() for a in Achievement.all_achievements + if a.id != self.unlock_after_all]): + fa = Achievement.achievement_dict.get(self.unlock_after_all) + if fa is not None: + fa.grant() + return + + ## Find which final achievements this is attached to + final_achievements = self.links.get(the_achievement.id, None) + if not final_achievements: + return + + ## Otherwise, see if this was the last achievement which was needed + ## to unlock a given final_achievement. + for final_achievement in final_achievements: + lst = self.final_to_list.get(final_achievement, None) + if lst is None: + continue + ## Check if all the achievements in the list are unlocked + if all([achievement.has(a) for a in lst]): + fa = Achievement.achievement_dict.get(final_achievement) + if fa is not None: + fa.grant() + return + +## Note: DO NOT change these configuration values in this block! See +## `achievements.rpy` for how to change them. This is just for setup so they +## exist in the game, and then you can modify them with `define` in a different +## file. +init -999 python in myconfig: + _constant = True + ## This is a configuration value which determines whether the in-game + ## achievement popup should appear when Steam is detected. Since Steam + ## already has its own built-in popup, you may want to set this to False + ## if you don't want to show the in-game popup alongside it. + ## The in-game popup will still work on non-Steam builds, such as builds + ## released DRM-free on itch.io. + INGAME_POPUP_WITH_STEAM = True + ## The length of time the in-game popup spends hiding itself (see + ## transform achievement_popout in achievements.rpy). + ACHIEVEMENT_HIDE_TIME = 1.0 + ## True if the game should show in-game achievement popups when an + ## achievement is earned. You can set this to False if you just want an + ## achievement gallery screen and don't want any popups. + SHOW_ACHIEVEMENT_POPUPS = True + ## A callback, or list of callbacks, which are called when an achievement + ## is granted. It is called with one argument, the achievement which + ## was granted. It is only called if the achievement has not previously + ## been earned. + ACHIEVEMENT_CALLBACK = None + ## A sound to play when the achievement is granted + ACHIEVEMENT_SOUND = None + ACHIEVEMENT_CHANNEL = "audio" + +## Track the time each achievement was earned at +default persistent.achievement_timestamp = dict() +## Tracks the number of onscreen achievements, for offsetting when +## multiple achievements are earned at once +default onscreen_achievements = dict() +## Required for older Ren'Py versions so the vpgrid doesn't complain about +## uneven numbers of achievements, but True by default in later Ren'Py versions. +define config.allow_underfull_grids = True + +# This, coupled with the timer on the popup screen, ensures that the achievement +# is properly hidden before another achievement can be shown in that "slot". +# If this was done as part of the timer in the previous screen, then it would +# consider that slot empty during the 1 second the achievement is hiding itself. +# That's why this timer is 1 second long. +screen finish_animating_achievement(num): + timer myconfig.ACHIEVEMENT_HIDE_TIME: + action [SetDict(onscreen_achievements, num, None), Hide()] + diff --git a/game/achievements.rpy b/game/achievements.rpy new file mode 100644 index 0000000..5dbed7f --- /dev/null +++ b/game/achievements.rpy @@ -0,0 +1,446 @@ +################################################################################ +## +## Achievements for Ren'Py by Feniks (feniksdev.itch.io / feniksdev.com) +## +################################################################################ +## This file contains code for an achievement system in Ren'Py. It is designed +## as a wrapper to the built-in achievement system, so it hooks into the +## Steam backend as well if you set up your achievement IDs the same as in +## the Steam backend. +## +## You don't have to register the achievements or anything with the backend, +## or worry about syncing - that's all automatically done for you when the +## achievements are declared and granted. +## +## To get started, declare a few achievements below. Some samples are included. +## You may also replace the `image locked_achievement` image with something +## appropriate - this image is used as the default "Locked" image for all your +## achievements unless you specify something else. +## +## Then you can make a button to go to your achievement gallery screen, e.g. +# textbutton _("Achievements") action ShowMenu("achievement_gallery") +## This will show the achievement gallery screen, declared below. You can +## further customize it however you like. +## If you click on an achievement in the gallery during development (this will +## not happen in a release build), it will toggle the achievement on/off. +## This will also let you see the achievement popup screen, similarly declared +## below. It can be customized however you like. +## +## If you use this code in your projects, credit me as Feniks @ feniksdev.com +## +## Leave a comment on the tool page on itch.io or an issue on the GitHub +## if you run into any issues. +################################################################################ + +################################################################################ +## CONFIGURATION +################################################################################ +## This is a configuration value which determines whether the in-game +## achievement popup should appear when Steam is detected. Since Steam +## already has its own built-in popup, you may want to set this to False +## if you don't want to show the in-game popup alongside it. +## The in-game popup will still work on non-Steam builds, such as builds +## released DRM-free on itch.io. +define myconfig.INGAME_POPUP_WITH_STEAM = True +## The length of time the in-game popup spends hiding itself (see +## transform achievement_popout below). +define myconfig.ACHIEVEMENT_HIDE_TIME = 1.0 +## True if the game should show in-game achievement popups when an +## achievement is earned. You can set this to False if you just want an +## achievement gallery screen and don't want any popups. +define myconfig.SHOW_ACHIEVEMENT_POPUPS = True +## This can be set to a sound that plays when the achievement popup appears. +## None does not play a sound. +define myconfig.ACHIEVEMENT_SOUND = None # "audio/sfx/achievement.ogg" +## If the sound plays, this sets the channel it will play on. The audio +## channel plays on the sfx mixer, and can play overlapping sounds if multiple +## achievements are earned at once. +define myconfig.ACHIEVEMENT_CHANNEL = "audio" + +## A callback, or list of callbacks, which are called when an achievement +## is granted. It is called with one argument, the achievement which +## was granted. It is only called if the achievement has not previously +## been earned. See the README for more information. +define myconfig.ACHIEVEMENT_CALLBACK = [ + ## This first example is an achievement which unlocks after two other + ## achievements have been granted ("hidden_achievement" and + ## "hidden_description"). + #LinkedAchievement(hidden3=['hidden_achievement', 'hidden_description']), + ## The second example is an achievement which unlocks after all achievements + ## have been granted. This is a special case. + # LinkedAchievement(platinum_achievement='all'), +] + +init python: + ## This is a built-in configuration value. It will set the position of + ## the Steam popup. You can change this to any of the following: + ## "top_left", "top_right", "bottom_left", "bottom_right" + ## You may want to use this to ensure any Steam notifications don't conflict + ## with the position of the built-in notification, if you're using both. + achievement.steam_position = None + +################################################################################ +## DEFINING ACHIEVEMENTS +################################################################################ + +define kicked_out = Achievement( + name=_("Kicked Out"), + id="kicked_out", + description=_("You left willingly and easily."), + locked_image=Transform("gui/window_icon.png", matrixcolor=SepiaMatrix()), + unlocked_image=Transform("gui/window_icon.png", matrixcolor=SepiaMatrix()), + hidden=True, +) + +define scare_tactics = Achievement( + name=_("Scare Tactics"), + id="scare_tactics", + description=_("You frightened your students off."), + locked_image=Transform("gui/window_icon.png", matrixcolor=SepiaMatrix()), + unlocked_image=Transform("gui/window_icon.png", matrixcolor=SepiaMatrix()), + hidden=True, +) + +define very_loud_quitting = Achievement( + name=_("Very Loud Quitting"), + id="very_loud_quitting", + description=_("You incited all the faculty into quitting."), + locked_image=Transform("gui/window_icon.png", matrixcolor=SepiaMatrix()), + unlocked_image=Transform("gui/window_icon.png", matrixcolor=SepiaMatrix()), + hidden=True, +) + +define she_has_your_back = Achievement( + name=_("She Has Your Back"), + id="she_has_your_back", + description=_("Either Reimu, Marisa, or Alice had your back."), + locked_image=Transform("gui/window_icon.png", matrixcolor=SepiaMatrix()), + unlocked_image=Transform("gui/window_icon.png", matrixcolor=SepiaMatrix()), + hidden=True, +) + +define they_have_your_back = Achievement( + name=_("They Have Your Back"), + id="they_have_your_back", + description=_("Your class had your back."), + locked_image=Transform("gui/window_icon.png", matrixcolor=SepiaMatrix()), + unlocked_image=Transform("gui/window_icon.png", matrixcolor=SepiaMatrix()), + hidden=True, +) + +define hedgehog_dilemma_solved = Achievement( + name=_("Hedgehog's Dilemma Solved"), + id="hedgehog_dilemma_solved", + description=_("You were proud and stubborn until the end."), + locked_image=Transform("gui/window_icon.png", matrixcolor=SepiaMatrix()), + unlocked_image=Transform("gui/window_icon.png", matrixcolor=SepiaMatrix()), + hidden=True, +) + + +# ## Replace this with whatever locked image you want to use as the default +# ## for a locked achievement. +# image locked_achievement = Text("?") + +# ## Example 1 ################################################################### +# ## This is how you declare achievements. You will use `define` and NOT +# ## `default`, so you can update the achievements later (you wouldn't want the +# ## description to be tied to a specific save file, for example). +# ## The order you declare achievements in is the order they will appear in the +# ## achievement gallery, by default. +# define sample_achievement = Achievement( +# ## The human-readable name, as it'll appear in the popup and in the gallery. +# name=_("Sample Achievement"), +# ## The id is used for Steam integration, and should match whatever ID +# ## you have set up in the Steam backend (if using). +# id="sample_achievement", +# ## Description. +# description=_("This is a sample achievement."), +# ## The image used in the popup and in the gallery once this achievement +# ## is unlocked. +# unlocked_image="gui/window_icon.png", +# ## By default all achievements use the "locked_achievement" image (declared +# ## above), but if you wanted to provide a different image, this is how +# ## you would specify it. It's used in the achievement gallery when the +# ## achievement is locked. +# locked_image="locked_achievement", +# ## All achievements are hidden=False by default, but you can change it to +# ## hidden=True if you'd like the title/description to show as ??? in the +# ## achievement gallery. See Examples 3 and 4 for examples of this. +# hidden=False, +# ) +# ## You can grant an achievement in-game with `$ sample_achievement.grant()` + +# ## Example 2 ################################################################### +# define progress_achievement = Achievement( +# name=_("Progress Achievement"), +# id="progress_achievement", +# description=_("This is an achievement with a progress bar."), +# unlocked_image=Transform("gui/window_icon.png", matrixcolor=InvertMatrix()), +# ## To record progress, you need to specify a stat_max. This means you can +# ## show a progress bar with % completion towards the achievement. It is +# ## useful if, for example, you have an achievement counting how many +# ## chapters the player has completed which unlocks when they have seen all +# ## the chapters. +# stat_max=12, +# ## You can also provide a stat_modulo, which means the achievement is only +# ## updated in the Steam backend every time the stat reaches a multiple of +# ## the modulo. +# ## Alternatively, this system also lets you set stat_update_percent instead, +# ## so if you want it to update every 10% it progresses, you can set +# # stat_update_percent=10 +# ## This is most useful for achievements with a large number of steps, +# ## like a general % completion achievement. Maybe there are 600 things to +# ## complete for the achievement, but obviously 0.1% increments are pretty +# ## meaningless so you can either set stat_modulo=6 or stat_update_percent=1 +# ## and it will update Steam every 6 steps or every 1%. +# ) +# ## To update progress towards completion of this achievement, you can use +# # $ progress_achievement.add_progress(1) +# ## where 1 is how much progress is added to the stat (so, the first time it +# ## is called for the above example it'd be 1/12, the second it'd be 2/12, etc). +# ## +# ## Alternatively, you can directly set the progress like: +# # $ progress_achievement.progress(5) +# ## This will directly set progress to 5, making the above example 5/12 for +# ## example. This can be useful if you're doing something like using a set to +# ## track unique progress towards the achievement e.g. +# # $ persistent.seen_endings.add("end1") +# # $ ending_achievement.progress(len(persistent.seen_endings)) +# ## This will prevent the achievement from being added to multiple times if the +# ## player sees the same ending multiple times. + +# ## Example 3 ################################################################### +# ## This achievement is "hidden", that is, its name and description appear as +# ## ??? in the achievement gallery until it is unlocked. +# define hidden_achievement = Achievement( +# name=_("Hidden Achievement"), +# id="hidden_achievement", +# description=_("This hidden achievement hides both the name and description."), +# unlocked_image=Transform("gui/window_icon.png", matrixcolor=BrightnessMatrix(-1.0)), +# hidden=True, ## The important bit that hides the name and description +# ) + +# ## Example 4 ################################################################### +# define hidden_description = Achievement( +# name=_("Hidden Description"), +# id="hidden_description", +# description=_("This hidden achievement hides only the description."), +# unlocked_image=Transform("gui/window_icon.png", matrixcolor=SepiaMatrix()), +# hide_description=True, ## The important bit that hides only the description +# ) + +# ## Example 5 ################################################################### +# ## This achievement unlocks automatically when the other two hidden achievements +# ## are unlocked. This is set up via myconfig.ACHIEVEMENT_CALLBACK earlier in +# ## the file. +# define hidden_double_unlock = Achievement( +# name=_("You found it"), +# id="hidden3", +# description=_("This achievement unlocks automatically when the other two hidden achievements are unlocked."), +# unlocked_image=Transform("gui/window_icon.png", matrixcolor=ContrastMatrix(0.0)), +# hidden=True, +# ## Besides just setting hide_description=True to set it to "???", you can +# ## optionally provide your own custom description here, which is only +# ## shown until the achievement is unlocked. +# hide_description=_("Try unlocking the other two hidden achievements before this one."), +# ) +# ## Example 6 ################################################################### +# ## This -2 makes sure it's declared before the other achievements. This is +# ## so it shows up first in the list even though it's defined all the way down +# ## here. +# define -2 all_achievements = Achievement( +# name=_("Platinum Achievement"), +# id="platinum_achievement", +# description=_("Congrats! You unlocked every achievement!"), +# unlocked_image=Transform("gui/window_icon.png", matrixcolor=BrightnessMatrix(1.0)), +# hide_description=_("Get all other achievements."), +# ) + +################################################################################ +## SCREENS +################################################################################ +## POPUP SCREEN +################################################################################ +## A screen which shows a popup for an achievement the first time +## it is obtained. You may modify this however you like. +## The relevant information is: +## a.name = the human-readable name of the achievement +## a.description = the description +## a.unlocked_image = the image of the achievement, now that it's unlocked +## a.timestamp = the time the achievement was unlocked at +screen achievement_popup(a, tag, num): + + zorder 190 + + ## Allows multiple achievements to be slightly offset from each other. + ## This number should be at least as tall as one achievement. + default achievement_yoffset = num*170 + + frame: + style_prefix 'achieve_popup' + ## The transform that makes it pop out + at achievement_popout() + ## Offsets the achievement down if there are multiple + yoffset achievement_yoffset + has hbox + add a.unlocked_image: + ## Make sure the image is within a certain size. Useful because + ## often popups are smaller than the full gallery image. + ## In this case it will not exceed 95 pixels tall but will retain + ## its dimensions. + fit "contain" ysize 95 align (0.5, 0.5) + vbox: + text a.name + text a.description size 25 + + ## Hide the screen after 5 seconds. You can change the time but shouldn't + ## change the action. + timer 5.0 action [Hide("achievement_popup"), + Show('finish_animating_achievement', num=num, _tag=tag+"1")] + +style achieve_popup_frame: + is confirm_frame + align (0.0, 0.0) +style achieve_popup_hbox: + spacing 10 +style achieve_popup_vbox: + spacing 2 + + +## A transform that pops the achievement out from the left side of +## the screen and bounces it slightly into place, then does the +## reverse when the achievement is hidden. +transform achievement_popout(): + ## The `on show` event occurs when the screen is first shown. + on show: + ## Align it off-screen at the left. Note that no y information is + ## given, as that is handled on the popup screen. + xpos 0.0 xanchor 1.0 + ## Ease it on-screen + easein_back 1.0 xpos 0.0 xanchor 0.0 + ## The `on hide, replaced` event occurs when the screen is hidden. + on hide, replaced: + ## Ease it off-screen again. + ## This uses the hide time above so it supports displaying multiple + ## achievements at once. + easeout_back myconfig.ACHIEVEMENT_HIDE_TIME xpos 0.0 xanchor 1.0 + +################################################################################ +## ACHIEVEMENT GALLERY SCREEN +################################################################################ +## The screen displaying a list of the achievements the player has earned. +## Feel free to update the styling for this however you like; this is just one +## way to display the various information. +screen ending_gallery(): + tag menu + + add VBox(Transform("#292835", ysize=110), "#21212db2") # Background + + ############################################################################ + ## Version 1 ############################################################### + ## If you're using a default template/typical Ren'Py layout, uncomment + ## the following: + # use game_menu(_("Achievement Gallery"), scroll='viewport'): + ############################################################################ + ## Version 2 ############################################################### + ## Otherwise, if you'd like this to be independent of the game menu, + ## use the following: + if main_menu: + textbutton _("Return") action Return() align (1.0, 1.0) + + viewport: + mousewheel True draggable True pagekeys True + scrollbars "vertical" + xalign 0.5 yalign 0.5 + xsize int(config.screen_width*0.6) ysize int(config.screen_height*0.7) + xfill False yfill False + has vbox + spacing 20 + ############################################################################ + ## Version 3 ############################################################### + ## You might also consider a vpgrid layout like so: + # textbutton _("Return") action Return() align (1.0, 1.0) + # vpgrid: + # cols 2 + # mousewheel True draggable True pagekeys True + # scrollbars "vertical" + # xalign 0.5 yalign 0.5 + # xsize 1500 ysize int(config.screen_height*0.7) + # yspacing 70 xspacing 50 + ############################################################################ + ## This list contains every achievement you declared. You can also + ## create your own lists to iterate over, if desired. That would be + ## useful if you wanted to group achievements by category, for example. + for a in Achievement.all_achievements: + button: + style_prefix 'achievement' + ## During development, you can click on achievements in the + ## gallery and they will toggle on/off. + if config.developer: + action a.Toggle() + has hbox + if a.idle_img: + fixed: + align (0.5, 0.5) + xysize (155, 155) + add a.idle_img fit "scale-down" ysize 155 align (0.5, 0.5) + else: + null width -10 + vbox: + label a.name + text a.description + if a.has(): + ## There are two ways to display the timestamp. The + ## first is automatically formatted like + ## Unlocked Sep 14, 2023 @ 6:45 PM + text a.timestamp size 22 + ## If you want to format it yourself, you can use + ## the get_timestamp method: + # text __("Achieved at ") + a.get_timestamp(__("%H:%M on %Y/%m/%d")) + ## The above example would display the timestamp like: + ## Achieved at 18:45 on 2023/09/14 + ## See https://strftime.org/ for formatting + ## Note also the double underscores for translation. + elif a.stat_max: + # Has a bar to show stat progress. + ## NOTE: If you don't want to show the progress *bar*, + ## you can remove this entire block (or potentially just + ## keep the text and not the bar if you like). + fixed: + fit_first True + bar value a.stat_progress range a.stat_max: + style 'achievement_bar' + text "[a.stat_progress]/[a.stat_max]": + style_suffix "progress_text" + + ## So there's a bit of space at the bottom after scrolling all the way. + null height 100 + + ## A header that shows how many achievements you've earned, out of + ## the total number of achievements in the game. Feel free to remove + ## or relocate this. + label __("Endings: ") + "{earned}/{total}".format( + earned=Achievement.num_earned(), total=Achievement.num_total()): + text_size 52 xalign 0.5 text_color "#f93c3e" top_padding 15 + + ## This is an example of a button you might have during development which + ## will reset all achievement progress at once. It can also be provided + ## to players if you'd like them to be able to reset their achievement + ## progress. + if main_menu: + textbutton "Reset Progress" action Achievement.Reset() align (1.0, 0.0) + +style achievement_button: + size_group 'achievement' + xmaximum 750 +style achievement_label: + padding (2, 2) +style achievement_label_text: + size 40 color "#ff8335" +style achievement_hbox: + spacing 10 +style achievement_vbox: + spacing 2 +style achievement_bar: + xmaximum 600 \ No newline at end of file diff --git a/game/screens/main_menu.rpy b/game/screens/main_menu.rpy index b3dfbcb..d950323 100644 --- a/game/screens/main_menu.rpy +++ b/game/screens/main_menu.rpy @@ -27,6 +27,8 @@ screen main_menu(): textbutton _("Load") action ShowMenu("load") + textbutton _("Endings") action ShowMenu("ending_gallery") + textbutton _("Preferences") action ShowMenu("preferences") textbutton _("About") action ShowMenu("about") diff --git a/game/script.rpy b/game/script.rpy index b302904..6f9e757 100644 --- a/game/script.rpy +++ b/game/script.rpy @@ -23,32 +23,78 @@ image reimu happy: label start: - scene bg room + label presentation_begins: - show yuuka happy: - xalign 0.2 - yalign 0.99 - - show reimu happy: - xalign 0.8 - yalign 0.99 + scene bg auditorium - yuuka "Good evening." - - yuuka "You all may know me as the substitute this past week for Professor Okazaki." - - yuuka "I've been teaching Botany, a topic on which I am incredibly overqualified for, to idiots younger than you but still very much like you." + show yuuka happy: + xalign 0.5 + yalign 0.99 + + python: # <- Remove + ''' + show reimu happy: + xalign 0.8 + yalign 0.99 + ''' - yuuka "But enough of the customary ribbing and teasing, I am here because Professor Okazaki is returning today." - - yuuka "As is stated in the faculty handbook she wrote and doodled over, because clearly not even she could have handwriting THAT horrendous," - - yuuka "I must give a presentation to report to the faculty and administration exactly what I did as subsitute for Professor Okazaki." + yuuka "Good evening." + + yuuka "You all may know me as the substitute this past week for Professor Okazaki." + + yuuka "I've been teaching Botany, a topic on which I am {sc}incredibly{/sc} overqualified for, to idiots younger than you, but still very much similar to you." - yuuka "This is that presentation." + narrator "{i}A few nervous chuckles break throughout the auditorium.{/i}" - yuuka "You might also be wondering why I've invited select students to attend this presentation." - - yuuka "I will be getting to that, please do not leave your seats through out this presentation. I will be brief." + yuuka "But enough of the customary good-natured teasing, I am here because Professor Okazaki is returning today." + + yuuka "As is stated in the faculty handbook she wrote and doodled over, because clearly not even she could have handwriting THAT horrendous," + + yuuka "I must give a presentation to report to the faculty and administration exactly what I did as subsitute for Professor Okazaki." - return \ No newline at end of file + yuuka "This is that presentation: a postmortem to my short time here, a retrospective of sorts." + + yuuka "You might also be wondering why I've invited select students to attend this presentation." + + yuuka "I will be getting to that, please do not leave your seats throughout this presentation. This will be brief." + + jump intro + + label intro: + scene bg entrance with fade + + show yuuka happy: + xalign 0.5 + yalign 0.99 + + yuuka "{i}There I was, one week ago, naive, unsure of what I was looking at.{/i}" (cb_name="") + + yuuka "What the hell am I looking at?" + + jump ending + +label ending: + + narrator "This is the part where the ending goes." + + narrator "You understand?" + + $ kicked_out.grant() + # $ scare_tactics.grant() + # $ very_loud_quitting.grant() + # $ she_has_your_back.grant() + # $ they_have_your_back.grant() + # $ hedgehog_dilemma_solved.grant() + + show screen ending_gallery + + jump pause_loop + + return + +label pause_loop: + + window hide + $ renpy.pause() + + jump pause_loop \ No newline at end of file