index.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. const { Document } = require('camo')
  2. const battleAIs = require('../battle/ai')
  3. const Move = require('../battle/move')
  4. const log = require('../util/log')
  5. const gm = require('gm')
  6. const request = require('request')
  7. const tinygradient = require('tinygradient')
  8. const tinycolor = require('tinycolor2')
  9. const healthGradient = tinygradient(['#BA0020', '#ffbf00', '#77dd77'])
  10. const colorDead = '#36454f'
  11. const BACKUP_AVATAR_URL = 'https://discordapp.com/assets/6debd47ed13483642cf09e832ed0bc1b.png'
  12. // A Character refers to both a player and an enemy. The difference is in
  13. // their `ai` that controls their actions (for players, this is DiscordAI).
  14. class Character extends Document {
  15. constructor() {
  16. super()
  17. this.schema({
  18. discordID: {type: String, required: false},
  19. battleAI: {type: String, required: true, choices: Object.keys(battleAIs)},
  20. partyID: {type: String, required: false}, // Automatically generated
  21. health: {type: Number, default: 5, min: 0},
  22. maxHealth: {type: Number, default: 5, min: 1},
  23. moveIDs: {type: [String]},
  24. })
  25. }
  26. // Rather than a string (this.battleAI), returns the class itself.
  27. get BattleAI() {
  28. return battleAIs[this.battleAI]
  29. }
  30. async modHealth(by, guild) {
  31. this.health += by
  32. if (this.health > this.maxHealth) this.health = this.maxHealth
  33. if (this.health < 0) this.health = 0
  34. if (guild) await this.healthUpdate(guild)
  35. return this.healthState
  36. }
  37. async healthUpdate(guild) {
  38. await this.save()
  39. if (!this.discordID) return
  40. const member = guild.members.get(this.discordID)
  41. // Remove existing health role, if any
  42. const roleExisting = member.roles.find(role => role.name.endsWith(' health'))
  43. if (roleExisting) await roleExisting.delete()
  44. // Reuse/create a health role and grant it
  45. const roleName = `${this.health}/${this.maxHealth} health`
  46. const role = member.roles.find(role => role.name === roleName)
  47. || await guild.createRole({
  48. name: roleName,
  49. color: this.health === 0
  50. ? colorDead
  51. : tinycolor(healthGradient.rgbAt(this.health / this.maxHealth)).toHexString()
  52. })
  53. await member.addRole(role)
  54. }
  55. get healthState() {
  56. if (this.health === this.maxHealth) return 'healthy'
  57. else if (this.health === 1) return 'peril'
  58. else if (this.health === 0) return 'dead'
  59. else return 'injured'
  60. }
  61. getName(guild) {
  62. // FIXME: names for characters without accociated discord users (eg. ai enemies)
  63. return guild.members.get(this.discordID).displayName
  64. }
  65. // "Label" of a character, to be displayed in messages.
  66. async getLabel(game) {
  67. const emote = await this.getEmote(game)
  68. const role = await this.getRole(game)
  69. return `${emote} ${role}`
  70. }
  71. // Finds/creates a Discord role with the same name as the character and with
  72. // the same color as the character's party.
  73. async getRole(game, forceUpdate = false) {
  74. if (this._discordRole && !forceUpdate) {
  75. return this._discordRole
  76. }
  77. // Create the role
  78. const name = this.getName(game.guild)
  79. const party = await this.getParty()
  80. const role = await game.guild.createRole({name, color: party.colorHex})
  81. // Give this member the role
  82. const member = await game.guild.members.get(this.discordID)
  83. await member.addRole(role)
  84. return this._discordRole = role
  85. }
  86. // Returns a little icon emote based on the character's avatar.
  87. // TODO: implement this for characters that aren't discord-backed
  88. async getEmote(game) {
  89. if (!this.discordID) throw new Error() // FIXME
  90. try {
  91. const member = game.guild.members.get(this.discordID)
  92. const graphic = gm(request(member.user.avatarURL))
  93. .resize(48, 48)
  94. .stream('PNG')
  95. const emote = await game.emoteStore.get('char' + this._id, graphic)
  96. return emote
  97. } catch (err) {
  98. log.warning('Error while generating character emote:\n' + err.message)
  99. const graphic = gm(request(BACKUP_AVATAR_URL))
  100. .resize(48, 48)
  101. .stream('PNG')
  102. const emote = await game.emoteStore.get('char' + this._id, graphic)
  103. return emote
  104. //return _.sample(['❤️', '💛', '💙', '💜', '💚'])
  105. }
  106. }
  107. async getParty() {
  108. const Party = require('./party') // I don't like this any more than you do, but recursive requires DESTROY Node.js. Sorry.
  109. let party
  110. if (!this.partyID) {
  111. // Create a one-man party
  112. party = await Party.of([this])
  113. }
  114. return party || await Party.findOne({_id: this.partyID})
  115. }
  116. }
  117. module.exports = Character