Expanded Achievements for QuestJS

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:

  1. Copy achievements.js to the QuestJS /lib directory
  2. Add settings.customLibraries.push({folder:'lib',files:['achievements']}) to settings.js
  3. Add achievements.persistAchievements() to the settings.setup function

You 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:

  • achievements.getAchievements

Takes a boolean, returns a list of completed achievements when passed true and uncompleted achievements when passed false

  • achievements.getAllAchievements

Returns a list of all possible achievements, sorted by name for uncompleted achievements and date achieved for completed achievements

  • achievements.listAchievements

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!


This topic is now closed. Topics are closed after 60 days of inactivity.

Support

Forums