Structures_UserCard.js

'use strict'
const Discord = require('discord.js')
const { BaseClass } = require('./BaseClass')
const { ErrorCodes } = require('../Errors/ErrorCodes')
const { MissingValue, InvalidValue } = require('../Errors/LME')
const { AchievementStatus } = require('../Interfaces')
const { isNotPositiveInteger } = require('../Utils/ValidateInteger')

/** @typedef {import('../../typings').Rank} Rank */
/** @typedef {import('../../typings').Achievement} Achievement */
/** @typedef {import('../../typings').UserEntry} UserEntry */

/**
 * Represents a user in the database/cache.
 * @extends BaseClass
 */
class UserCard extends BaseClass {
  /** @type {Discord.Client} - Discord Client */
  client
  /** @type {Discord.Snowflake} - Users Id */
  id
  /** @type {String} - Users username */
  username
  /** @type {Number} - Users actual xp */
  xp
  /** @type {Number} Users level */
  level
  /** @type {Number} Users xp needed to level up */
  maxXpToLevelUp
  /** @type {Rank} - Users rank */
  rank
  /** @type {Achievement[]} - Users achievements */
  achievements

  /**
   * @param {Discord.Client} client - Discord client
   * @param {UserEntry} data - Data extracted from the database/cache
   */
  constructor (client, data) {
    super(client)

    this.#checkData(data)

    this.client = client

    this.id = data.id

    this.username = data.username

    this.xp = data.xp

    this.level = data.level

    this.maxXpToLevelUp = data.maxXpToLevelUp

    this.rank = data.rank

    this.achievements = data.achievements
  }

  /**
   * Evaluates if the user has all the required data.
   * @param {UserEntry} data - Data extracted from the database/cache
   * @throws {MissingValue} - If a property is missing.
   * @throws {InvalidValue} - If a property is not valid.
   * @private
   */
  #checkData (data) {
    if (!data) throw new MissingValue(ErrorCodes.MissingArgument, 'data')

    if (typeof data !== 'object') throw new InvalidValue(ErrorCodes.InvalidValue, 'data', 'an object')

    if (!data.id) throw new MissingValue(ErrorCodes.MissingProperty, 'id')

    if (typeof data.id !== 'string') throw new InvalidValue(ErrorCodes.InvalidValue, 'id', 'a string')

    if (!data.username) throw new MissingValue(ErrorCodes.MissingProperty, 'username')

    if (typeof data.username !== 'string') throw new InvalidValue(ErrorCodes.InvalidValue, 'username', 'a string')

    if (!data.xp && data.xp !== 0) throw new MissingValue(ErrorCodes.MissingProperty, 'xp')

    if (typeof data.xp !== 'number') throw new InvalidValue(ErrorCodes.InvalidValue, 'xp', 'a positive integer')

    if (!data.level && data.level !== 0) throw new MissingValue(ErrorCodes.MissingProperty, 'level')

    if (typeof data.level !== 'number') throw new InvalidValue(ErrorCodes.InvalidValue, 'level', 'a positive integer')

    if (!data.maxXpToLevelUp) throw new MissingValue(ErrorCodes.MissingProperty, 'maxXpToLevelUp')

    if (typeof data.maxXpToLevelUp !== 'number') throw new InvalidValue(ErrorCodes.InvalidValue, 'maxXpToLevelUp', 'a positive integer')

    if (!data.rank) throw new MissingValue(ErrorCodes.MissingProperty, 'rank')

    if (typeof data.rank !== 'object') throw new InvalidValue(ErrorCodes.InvalidValue, 'rank', 'an object')

    if (!data.achievements) throw new MissingValue(ErrorCodes.MissingProperty, 'achievements')

    if (!Array.isArray(data.achievements)) throw new InvalidValue(ErrorCodes.InvalidValue, 'achievements', 'an array')
  }

  /**
   * Updates a achievement progress and unlocks it if the progress is equal or greater than the achievement's max progress.
   *
   * The new progress will overwrite the old progress so if you want add more progress to the achievement you need to add the old progress to the new progress.
   *
   * @example
   *  user.achievementProgress(achievement, achievement.progress[0] + 10)
   *
   * @param {Achievement} achievement - Achievement to update.
   * @param {Number} progress - Progress to update.
   * @returns {Boolean} - If the achievement was unlocked.
   */
  achievementProgress (achievement, progress) {
    // Check params
    if (!achievement) throw new MissingValue(ErrorCodes.MissingArgument, 'achievement')

    if (achievement === null || achievement === undefined || typeof achievement !== 'object') throw new InvalidValue(ErrorCodes.InvalidValue, 'achievement', 'an object')

    if (progress === null || progress === undefined || typeof progress !== 'number' || isNotPositiveInteger(progress)) throw new InvalidValue(ErrorCodes.InvalidValue, 'progress', 'a positive integer')

    if (achievement.progress[1] < progress) throw new InvalidValue(ErrorCodes.InvalidValue, 'progress', `a positive integer less than ${achievement.progress[1]}`)

    // Check if the achievement is already unlocked
    if (achievement.progress[0] === achievement.progress[1]) return false
    else achievement.status = AchievementStatus.Locked

    // Update progress
    achievement.progress[0] = progress

    // Check if the achievement was unlocked
    if (achievement.progress[0] >= achievement.progress[1]) {
      achievement.status = AchievementStatus.Unlocked
      achievement.progress[0] = achievement.progress[1]

      return true
    }

    return false
  }

  /**
   * Returns the user as a JSON object.
   * @returns {UserEntry}
   */
  toJSON () {
    return {
      id: this.id,
      username: this.username,
      xp: this.xp,
      level: this.level,
      maxXpToLevelUp: this.maxXpToLevelUp,
      rank: this.rank,
      achievements: this.achievements
    }
  }
}

module.exports = { UserCard }