parent
d0a3798dd2
commit
630be43744
|
@ -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()]
|
||||
|
|
@ -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
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
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
|
Loading…
Reference in New Issue