This library is based on The Pixie's achievements feature in QuestJS 1.4. It's quite a good idea and implementation to reward the player, encourage exploration, and improve replayability. I wanted to do something a bit different, where the player could see a list of uncompleted achievements to help guide them, which didn't appear to be possible in the current system. Apart from that change, this library is otherwise very similar to the existing feature.
Instead of creating achievements when they are completed, this library requires them to be defined when the game loads. Somewhat like a normal QuestJS object, each achievement has a name
and analias
, with the former being used to identify the object and the later being used to display. Each achievement also has a details
and afterDetails
, which are displayed before and after the achievement is completed, respectively. Finally, each achievement has a condition
function, which should return a boolean, true if the achievement should be complete, false if otherwise.
Each turn, the condition functions for all uncompleted achievements are run. If any evaluate to true, a message is printed, and the date is saved to the achievement object.
Here is the current code for the library:
"use strict"
function createAchievement(args) {
if (!args.name || typeof args.name !== 'string') errormsg("Achievement created without name.")
if (!args.alias || typeof args.alias !== 'string') errormsg(`Achievement ${args.name}: created without alias.`)
if (!args.details || typeof args.details !== 'string') errormsg(`Achievement ${args.name}: created without details.`)
if (!args.afterDetails || typeof args.afterDetails !== 'string') errormsg(`Achievement ${args.name}: created without afterDetails.`)
if (!args.condition || typeof args.condition !== 'function') errormsg(`Achievement ${args.name}: created without condition.`)
achievements.achievements.push(args)
}
const achievements = {
achievements: [],
achievementsKey: "QJS:" + settings.title + ":achievements",
getAchievements(achieved) {
const achievementsJSON = localStorage.getItem(this.achievementsKey)
const achievements = achievementsJSON ? JSON.parse(achievementsJSON) : []
return achieved ? achievements.filter(ach => { return !!ach.achieved }) : achievements.filter(ach => { return !ach.achieved })
},
getAllAchievements() {
const achievementsJSON = localStorage.getItem(this.achievementsKey)
const achievements = achievementsJSON ? JSON.parse(achievementsJSON) : []
return achievements.sort(function (a, b) {
if (!a.achieved && !!b.achieved) return -1
if (!b.achieved && !!a.achieved) return 1
if (!!a.achieved && !!b.achieved) return a.achieved - b.achieved
return a.name.localeCompare(b.name)
})
},
listAchievements(achievements) {
achievements.forEach(ach => {
msg(`${ach.achieved ? "☑" : "☐"} ${ach.alias} - ${ach.achieved ? ach.afterDetails : ach.details}${ach.achieved ? " - " + new Date(ach.achieved).toDateString() : ''}`)
})
},
setAchievement(name) {
const achievementsJSON = localStorage.getItem(this.achievementsKey)
const achievements = achievementsJSON ? JSON.parse(achievementsJSON) : []
const achievement = achievements.find(ach => { return ach.name === name })
if (!achievement.achieved) {
achievement.achieved = Date.now()
localStorage.setItem(this.achievementsKey, JSON.stringify(achievements))
}
msg(`Achievement unlocked: ${achievement.alias} - ${achievement.details}`)
},
endTurn() {
this.getAchievements(false).forEach(ach => {
const achievement = this.achievements.find(match => { return match.name === ach.name })
if (achievement.condition()) this.setAchievement(ach.name)
})
},
persistAchievements() {
const achievementsJSON = localStorage.getItem(this.achievementsKey)
const achievements = achievementsJSON ? JSON.parse(achievementsJSON) : []
this.achievements.forEach(achievement => {
if (!achievements.find(ach => { return achievement.name === ach.name })) achievements.push(achievement)
})
localStorage.setItem(this.achievementsKey, JSON.stringify(achievements))
},
}
settings.modulesToEndTurn.push(achievements)
And here are the steps to install it:
achievements.js
to the QuestJS /lib
directorysettings.customLibraries.push({folder:'lib',files:['achievements']})
to settings.js
achievements.persistAchievements()
to the settings.setup
functionYou can create an achievement like this:
createAchievement({
name:"unique_achievement_id",
alias:"Cool Achievement Name!",
details:"Message to display before achievement is completed",
afterDetails:"Message to display after achievement is completed",
condition:function(){
return (w.player.did_a_cool_thing}))
}
})
You can use the following functions to retrieve and print a list of achievements:
Takes a boolean, returns a list of completed achievements when passed true
and uncompleted achievements when passed false
Returns a list of all possible achievements, sorted by name for uncompleted achievements and date achieved for completed achievements
Takes a list of achievements and prints them to the screen
☐ Uncompleted Achievement - This is what's in details!
☑ Completed Achievement - This is what's in afterDetails! - Sat Jul 29 2023
And here's a link to the github, in case I need to fix any bugs:
https://github.com/cellarderecho/derecho-quest-libs/tree/main/achievements
This is good. Would you be happy for it to be added (slightly edited) to QuestJS?
I think achievements.persistAchievements()
can go on the end of the achievements.js file, making it easier for authors, unless you know otherwise?
I ended up pretty much re-wring it. The new version combines the old and what you have above. The createAchievement
behaves just the same, so you should be able to swap pretty easily. You need to do settings.libraries.push('achievements')
to include the file. There is also an ACHIEVEMENTS command now.
More details here:
https://github.com/ThePix/QuestJS/wiki/Achievements
Nicely done! I had a similar idea to eliminate persistAchievements()
by combining it with createAchievement
, but your implementation is much better. There were two minor changes that I made after posting that could be included in your version, if you agree.
First, I updated createAchievement
to exit the function early if anything critical is missing. For your version, I think this would just be name
and details
.
Second, I updated localeCompare (line 46 in your version) to compare the alias
instead of the name
.
Thanks for taking the time to improve this, I hope other people find it useful!