From a4c03cef3cf9e7e13a2ffa48015177621d49c9cc Mon Sep 17 00:00:00 2001 From: Hexugory Date: Fri, 31 Mar 2023 23:27:00 -0500 Subject: [PATCH] the end of all --- package-lock.json | 14 ++ package.json | 1 + src/arpad.d.ts | 27 ++++ src/battle.ts | 284 +++++++++++++++++++++++++++-------- src/cbclient.ts | 116 +++++++++++--- src/characters.json | 62 ++++---- src/commands/commandlist.ts | 4 +- src/commands/instructions.ts | 21 +++ src/models/player.ts | 6 +- src/slash/challenge.ts | 2 +- src/slash/equip.ts | 14 +- src/slash/grant.ts | 8 +- src/slash/list.ts | 31 ++-- src/slash/roll.ts | 8 +- 14 files changed, 455 insertions(+), 143 deletions(-) create mode 100644 src/arpad.d.ts create mode 100644 src/commands/instructions.ts diff --git a/package-lock.json b/package-lock.json index 6057c72..8c88edb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "arpad": "^2.0.0", "discord.js": "^14.8.0", "sequelize": "^6.30.0", "sqlite3": "^5.1.6", @@ -275,6 +276,14 @@ "node": ">=10" } }, + "node_modules/arpad": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arpad/-/arpad-2.0.0.tgz", + "integrity": "sha512-zuGHvpR6yPHmvH4xZFPHy9mvLPOd4EZsOaAPC/ndoLrxOp4YhGilvqE8C0YtyJovlhxpmck0Zt43Ib5f09O3zQ==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1815,6 +1824,11 @@ "readable-stream": "^3.6.0" } }, + "arpad": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arpad/-/arpad-2.0.0.tgz", + "integrity": "sha512-zuGHvpR6yPHmvH4xZFPHy9mvLPOd4EZsOaAPC/ndoLrxOp4YhGilvqE8C0YtyJovlhxpmck0Zt43Ib5f09O3zQ==" + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", diff --git a/package.json b/package.json index 49dda7d..49d8397 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "typescript": "^5.0.2" }, "dependencies": { + "arpad": "^2.0.0", "discord.js": "^14.8.0", "sequelize": "^6.30.0", "sqlite3": "^5.1.6", diff --git a/src/arpad.d.ts b/src/arpad.d.ts new file mode 100644 index 0000000..c54d644 --- /dev/null +++ b/src/arpad.d.ts @@ -0,0 +1,27 @@ +declare module 'arpad' { + export default class Elo { + constructor(k_factor?: number, min?: number, max?: number) + + /** + * Calculates a new rating from an existing rating and opponents rating if the player won + * + * This is a convenience method which skips the score concept + * + * @param {Number} rating The existing rating of the player, e.g. 1200 + * @param {Number} opponent_rating The rating of the opponent, e.g. 1300 + * @return {Number} The new rating of the player, e.g. 1300 + */ + newRatingIfWon(rating: number, opponent_rating: number): number + + /** + * Calculates a new rating from an existing rating and opponents rating if the player lost + * + * This is a convenience method which skips the score concept + * + * @param {Number} rating The existing rating of the player, e.g. 1200 + * @param {Number} opponent_rating The rating of the opponent, e.g. 1300 + * @return {Number} The new rating of the player, e.g. 1180 + */ + newRatingIfLost(rating: number, opponent_rating: number): number + } +}; \ No newline at end of file diff --git a/src/battle.ts b/src/battle.ts index 537f5ff..ef0fd51 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -1,6 +1,6 @@ import { Message, TextBasedChannel } from "discord.js"; import { CBClient } from "./cbclient"; -import { CHARACTERS, Character, Effect, PotencyStatus, Target } from "./characters"; +import { CHARACTERS, Character, Effect, PotencyStatus, Skill, Target } from "./characters"; import { Player } from "./models/player"; import { Unit } from "./models/unit"; @@ -33,9 +33,10 @@ interface StatusEffects { } class BattleUnit { - constructor (unit: {user_id: string, character_id: number}, battle: Battle) { + constructor (unit: {user_id: string, character_id: number}, battle: Battle, team: 1 | 2) { this.unit = unit; this.battle = battle; + this.team = team this.character = CHARACTERS.get(unit.character_id)!; this.defaultHealth = this.character.health; this.health = this.character.health; @@ -44,6 +45,7 @@ class BattleUnit { unit: {user_id: string, character_id: number} battle: Battle + team: 1 | 2 defaultHealth: number health: number speed: number @@ -66,8 +68,8 @@ export class Battle { this.channel = channel; this.player1 = player1; this.player2 = player2; - this.team1 = team1.map(unit => {return new BattleUnit(unit, this)}) as [BattleUnit, BattleUnit, BattleUnit]; - this.team2 = team2.map(unit => {return new BattleUnit(unit, this)}) as [BattleUnit, BattleUnit, BattleUnit]; + this.team1 = team1.map(unit => {return new BattleUnit(unit, this, 1)}) as [BattleUnit, BattleUnit, BattleUnit]; + this.team2 = team2.map(unit => {return new BattleUnit(unit, this, 2)}) as [BattleUnit, BattleUnit, BattleUnit]; this.units = this.team1.concat(this.team2) as [BattleUnit, BattleUnit, BattleUnit, BattleUnit, BattleUnit, BattleUnit]; } @@ -94,18 +96,18 @@ export class Battle { unit.statusEffects.poison-- const change = Math.floor(unit.defaultHealth/12); unit.health -= change; - this.appendLog(`${unit.character.nameShort} took ${change} poison damage!`); + this.appendLog(`(${unit.team}) ${unit.character.nameShort} took ${change} poison damage!`); } if (unit.statusEffects.regeneration > 0) { unit.statusEffects.regeneration-- const change = Math.floor(unit.defaultHealth/10) unit.health += change; - this.appendLog(`${unit.character.nameShort} regenerated ${change} health!`); + this.appendLog(`(${unit.team}) ${unit.character.nameShort} regenerated ${change} health!`); } if (unit.statusEffects.burn > 0) { unit.statusEffects.burn-- unit.health -= 5; - this.appendLog(`${unit.character.nameShort} took 5 burn damage!`); + this.appendLog(`(${unit.team}) ${unit.character.nameShort} took 5 burn damage!`); } if (unit.statusEffects.confusion > 0) unit.statusEffects.confusion--; if (unit.statusEffects.stun > 0) unit.statusEffects.stun--; @@ -150,8 +152,8 @@ export class Battle { //pick random skill const skill = unit.character.skills[Math.floor(Math.random()*unit.character.skills.length)]; - const team = initiative[0] < 3 ? this.team1.filter(Battle.filterActive) : this.team2.filter(Battle.filterActive); - const opponentTeam = initiative[0] < 3 ? this.team2.filter(Battle.filterActive) : this.team1.filter(Battle.filterActive); + const team = unit.team === 1 ? this.team1.filter(Battle.filterActive) : this.team2.filter(Battle.filterActive); + const opponentTeam = unit.team === 1 ? this.team2.filter(Battle.filterActive) : this.team1.filter(Battle.filterActive); const activeUnits = this.units.filter(Battle.filterActive); //targets are chosen randomly between the units with the most taunt const opponentTargets = opponentTeam.filter(unit => { @@ -167,46 +169,18 @@ export class Battle { .reduce((a, b) => a + b.potency, 0); //chance to stun - if (unit.statusEffects.stun) this.appendLog(`${unit.character.nameShort} is stunned!`); + if (unit.statusEffects.stun) this.appendLog(`(${unit.team}) ${unit.character.nameShort} is stunned!`); //chance to confused else if (unit.statusEffects.confusion && Math.random()*100 > 75) { unit.health -= 5; - this.appendLog(`${unit.character.nameShort} hit herself in confusion!`); + this.appendLog(`(${unit.team}) ${unit.character.nameShort} hit herself in confusion!`); } //chance to miss else if (Math.random()*100 > affectedAccuracy) { - this.appendLog(`${unit.character.nameShort} used ${skill.name}, but missed!`); + this.appendLog(`(${unit.team}) ${unit.character.nameShort} used ${skill.name}, but missed!`); } else { - this.appendLog(`${unit.character.nameShort} used ${skill.name}!`); - for (const effect of skill.effects) { - switch (effect.target){ - case Target.Self: - this.runEffect(effect, unit, [unit]); - break; - case Target.Team: - this.runEffect(effect, unit, team); - break; - case Target.OneTeammate: - this.runEffect(effect, unit, [allyTarget]); - break; - case Target.TrueRandomTeammate: - this.runEffect(effect, unit, [team[Math.floor(Math.random()*team.length)]]); - break; - case Target.OneOpponent: - this.runEffect(effect, unit, [target]); - break; - case Target.AllOpponents: - this.runEffect(effect, unit, opponentTeam); - break; - case Target.TrueRandomOpponent: - this.runEffect(effect, unit, [opponentTeam[Math.floor(Math.random()*opponentTeam.length)]]); - break; - case Target.TrueRandom: - this.runEffect(effect, unit, [activeUnits[Math.floor(Math.random()*activeUnits.length)]]); - break; - } - } + this.useSkill(unit, skill, team, allyTarget, target, opponentTeam, activeUnits); } this.checkAlive(this.units); @@ -214,13 +188,45 @@ export class Battle { } } + useSkill (unit: BattleUnit, skill: Skill, team: BattleUnit[], allyTarget: BattleUnit, target: BattleUnit, opponentTeam: BattleUnit[], activeUnits: BattleUnit[]) { + this.appendLog(`(${unit.team}) ${unit.character.nameShort} used ${skill.name}!`); + for (const effect of skill.effects) { + switch (effect.target){ + case Target.Self: + this.runEffect(effect, unit, [unit]); + break; + case Target.Team: + this.runEffect(effect, unit, team); + break; + case Target.OneTeammate: + this.runEffect(effect, unit, [allyTarget]); + break; + case Target.TrueRandomTeammate: + this.runEffect(effect, unit, [team[Math.floor(Math.random()*team.length)]]); + break; + case Target.OneOpponent: + this.runEffect(effect, unit, [target]); + break; + case Target.AllOpponents: + this.runEffect(effect, unit, opponentTeam); + break; + case Target.TrueRandomOpponent: + this.runEffect(effect, unit, [opponentTeam[Math.floor(Math.random()*opponentTeam.length)]]); + break; + case Target.TrueRandom: + this.runEffect(effect, unit, [activeUnits[Math.floor(Math.random()*activeUnits.length)]]); + break; + } + } + } + runEffect (effect: Effect, self: BattleUnit, targets: BattleUnit[]) { if (!targets[0]) return; for (const target of targets) { //chance to miss if (effect.accuracy && Math.random()*100 > effect.accuracy) { - this.appendLog(`${target.character.nameShort} avoided the attack!`); + this.appendLog(`(${target.team}) ${target.character.nameShort} avoided the attack!`); continue; } @@ -236,15 +242,15 @@ export class Battle { .reduce((a, b) => a - (b.potency/100), 1); damage = Math.round(damage) - this.appendLog(`${target.character.nameShort} took ${damage} damage!`); + this.appendLog(`(${target.team}) ${target.character.nameShort} took ${damage} damage!`); target.health -= damage; } if (effect.heal) { - this.appendLog(`${target.character.nameShort} restored ${effect.heal} health!`); + this.appendLog(`(${target.team}) ${target.character.nameShort} restored ${effect.heal} health!`); target.health += effect.heal; } if (effect.cure) { - this.appendLog(`${target.character.nameShort} was cured of ailments!`); + this.appendLog(`(${target.team}) ${target.character.nameShort} was cured of ailments!`); target.statusEffects.burn = 0; target.statusEffects.confusion = 0; target.statusEffects.stun = 0; @@ -253,51 +259,51 @@ export class Battle { target.potencyEffects.filter(Battle.isBuff); } if (effect.dispel) { - this.appendLog(`${target.character.nameShort} was dispelled of buffs!`); + this.appendLog(`(${target.team}) ${target.character.nameShort} was dispelled of buffs!`); target.statusEffects.regeneration = 0; target.potencyEffects.filter(Battle.isDebuff); } if (effect.poison) { target.statusEffects.poison = Math.max(target.statusEffects.poison, effect.poison); - this.appendLog(`${target.character.nameShort} was poisoned!`); + this.appendLog(`(${target.team}) ${target.character.nameShort} was poisoned!`); } if (effect.regeneration) { target.statusEffects.regeneration = Math.max(target.statusEffects.regeneration, effect.regeneration); - this.appendLog(`${target.character.nameShort} began recovering!`); + this.appendLog(`(${target.team}) ${target.character.nameShort} began recovering!`); } if (effect.burn) { target.statusEffects.burn = Math.max(target.statusEffects.burn, effect.burn); - this.appendLog(`${target.character.nameShort} caught fire!`); + this.appendLog(`(${target.team}) ${target.character.nameShort} caught fire!`); } if (effect.confusion) { - target.statusEffects.confusion = Math.max(target.statusEffects.confusion, effect.confusion); - this.appendLog(`${target.character.nameShort} was confused!`); + target.statusEffects.confusion = Math.max(target.statusEffects.confusion+1, effect.confusion); + this.appendLog(`(${target.team}) ${target.character.nameShort} was confused!`); } if (effect.stun) { - target.statusEffects.stun = Math.max(target.statusEffects.stun, effect.stun); - this.appendLog(`${target.character.nameShort} was stunned!`); + target.statusEffects.stun = Math.max(target.statusEffects.stun+1, effect.stun); + this.appendLog(`(${target.team}) ${target.character.nameShort} was stunned!`); } if (effect.taunt) { - target.statusEffects.taunt = Math.max(target.statusEffects.taunt, effect.taunt); - this.appendLog(`${target.character.nameShort} started drawing aggression!`); + target.statusEffects.taunt = Math.max(target.statusEffects.taunt+1, effect.taunt); + this.appendLog(`(${target.team}) ${target.character.nameShort} started drawing aggression!`); } if (effect.resistanceChange) { target.potencyEffects.push(Battle.createPotencyEffect(effect.resistanceChange, EffectType.Resistance)); - this.appendLog(`${target.character.nameShort} ${effect.resistanceChange.potency >= 0 ? 'gained' : 'lost'} ${Math.abs(effect.resistanceChange.potency)}% resistance!`); + this.appendLog(`(${target.team}) ${target.character.nameShort} ${effect.resistanceChange.potency >= 0 ? 'gained' : 'lost'} ${Math.abs(effect.resistanceChange.potency)}% resistance!`); } if (effect.accuracyChange) { target.potencyEffects.push(Battle.createPotencyEffect(effect.accuracyChange, EffectType.Accuracy)); - this.appendLog(`${target.character.nameShort} ${effect.accuracyChange.potency >= 0 ? 'gained' : 'lost'} ${Math.abs(effect.accuracyChange.potency)}% accuracy!`); + this.appendLog(`(${target.team}) ${target.character.nameShort} ${effect.accuracyChange.potency >= 0 ? 'gained' : 'lost'} ${Math.abs(effect.accuracyChange.potency)}% accuracy!`); } if (effect.speedChange) { target.potencyEffects.push(Battle.createPotencyEffect(effect.speedChange, EffectType.Speed)); - this.appendLog(`${target.character.nameShort} ${effect.speedChange.potency >= 0 ? 'gained' : 'lost'} ${Math.abs(effect.speedChange.potency)} speed!`); + this.appendLog(`(${target.team}) ${target.character.nameShort} ${effect.speedChange.potency >= 0 ? 'gained' : 'lost'} ${Math.abs(effect.speedChange.potency)} speed!`); } if (effect.damageChange) { target.potencyEffects.push(Battle.createPotencyEffect(effect.damageChange, EffectType.Damage)); - this.appendLog(`${target.character.nameShort} ${effect.damageChange.potency >= 0 ? 'gained' : 'lost'} ${Math.abs(effect.damageChange.potency)}% damage!`); + this.appendLog(`(${target.team}) ${target.character.nameShort} ${effect.damageChange.potency >= 0 ? 'gained' : 'lost'} ${Math.abs(effect.damageChange.potency)}% damage!`); } - if (effect.function) Battle.skillFunctions[effect.function](target, this); + if (effect.function) this.skillFunctions[effect.function](self, target, this); } } @@ -307,7 +313,7 @@ export class Battle { console.debug(unit.health); if (unit.health > 0) continue; unit.active = false; - this.appendLog(`${unit.character.nameShort} has been defeated!`); + this.appendLog(`(${unit.team}) ${unit.character.nameShort} has been defeated!`); } } @@ -333,7 +339,7 @@ export class Battle { static createPotencyEffect (status: PotencyStatus, type: EffectType): PotencyEffect { return { type: type, - duration: status.duration, + duration: status.duration+1, potency: status.potency }; } @@ -353,13 +359,161 @@ export class Battle { team2: [BattleUnit, BattleUnit, BattleUnit] units: [BattleUnit, BattleUnit, BattleUnit, BattleUnit, BattleUnit, BattleUnit] - static skillFunctions: { [key: string]: (target: BattleUnit, battle: Battle) => void; } = { - bruh: (target: BattleUnit, battle: Battle) => { + skillFunctions: { [key: string]: (self: BattleUnit, target: BattleUnit, battle: Battle) => void; } = { + bruh: (self: BattleUnit, target: BattleUnit, battle: Battle) => { if (battle.channel.isDMBased()) return; const targetMember = battle.channel.guild.members.resolve(target.unit.user_id); if (!targetMember) return; targetMember.kick('april fools').catch(err => console.error); battle.appendLog(`${targetMember} was kicked by <@208460737180467200>!`); - } + }, + perfectFreeze: (self: BattleUnit, target: BattleUnit, battle: Battle) => { + target.potencyEffects = target.potencyEffects.filter(effect => {return effect.type != EffectType.Speed}); + target.potencyEffects.push({ + type: EffectType.Speed, + duration: 2, + potency: 1-target.speed + }); + + battle.appendLog(`${target.character.nameShort}'s speed dropped to 1!`); + }, + fireBird: (self: BattleUnit, target: BattleUnit, battle: Battle) => { + if (!target.statusEffects.burn) return; + + this.appendLog(`(${target.team}) ${target.character.nameShort} restored 30 health!`); + target.health += 30; + }, + aPoison: (self: BattleUnit, target: BattleUnit, battle: Battle) => { + if (!target.statusEffects.poison) return; + + this.appendLog(`(${target.team}) ${target.character.nameShort} restored 20 health!`); + target.health += 20; + }, + pathOfAvici: (self: BattleUnit, target: BattleUnit, battle: Battle) => { + target.potencyEffects = target.potencyEffects.filter(effect => {return effect.type != EffectType.Speed}); + target.potencyEffects.push({ + type: EffectType.Speed, + duration: 3, + potency: 1-target.speed + }); + + battle.appendLog(`${target.character.nameShort}'s speed dropped to 1!`); + }, + poorFate: (self: BattleUnit, target: BattleUnit, battle: Battle) => { + target.health = Math.round(target.health/2); + + battle.appendLog(`${target.character.nameShort}'s life was cut in half!`); + }, + terrifyingHypnotism: (self: BattleUnit, target: BattleUnit, battle: Battle) => { + const opponents = target.team === 1 ? battle.team1 : battle.team2; + const mimicTarget = opponents[Math.floor(Math.random()*opponents.length)]; + const skill = mimicTarget.character.skills[Math.floor(Math.random()*mimicTarget.character.skills.length)]; + const team = self.team === 1 ? this.team1.filter(Battle.filterActive) : this.team2.filter(Battle.filterActive); + const opponentTeam = self.team === 1 ? this.team2.filter(Battle.filterActive) : this.team1.filter(Battle.filterActive); + const activeUnits = this.units.filter(Battle.filterActive); + //pick random targets + const allyTarget = team[Math.floor(Math.random()*team.length)]; + + this.useSkill(self, skill, team, allyTarget, target, opponentTeam, activeUnits); + + battle.appendLog(`${target.character.nameShort}'s life was cut in half!`); + }, + risingSun: (self: BattleUnit, target: BattleUnit, battle: Battle) => { + target.potencyEffects = target.potencyEffects.filter(effect => {return effect.type != EffectType.Speed}); + target.potencyEffects.push({ + type: EffectType.Speed, + duration: 2, + potency: 5-target.speed + }); + + battle.appendLog(`${target.character.nameShort}'s speed increased to 5!`); + }, + overturnBuffs: (self: BattleUnit, target: BattleUnit, battle: Battle) => { + target.potencyEffects = target.potencyEffects.map(effect => { + effect.potency = Math.min(effect.potency, effect.potency*-1); + return effect; + }); + + battle.appendLog(`${target.character.nameShort}'s buffs were overturned!`); + }, + overturnDebuffs: (self: BattleUnit, target: BattleUnit, battle: Battle) => { + target.potencyEffects = target.potencyEffects.map(effect => { + effect.potency = Math.max(effect.potency, effect.potency*-1); + return effect; + }); + + battle.appendLog(`${target.character.nameShort}'s debuffs were overturned!`); + }, + overturnHealth: (self: BattleUnit, target: BattleUnit, battle: Battle) => { + target.health = Math.max(target.health, 35); + + battle.appendLog(`${target.character.nameShort} was brought down to size!`); + }, + reverseHierarchy: (self: BattleUnit, target: BattleUnit, battle: Battle) => { + let damage = 10+Math.floor(target.defaultHealth/10)*3; + //multiply by damageChange sum + damage *= self.potencyEffects + .filter(status => {return status.type === 'damage'}) + .reduce((a, b) => a + (b.potency/100), 1); + //multiply by resistanceChange sum + damage *= self.potencyEffects + .filter(status => {return status.type === 'resistance'}) + .reduce((a, b) => a - (b.potency/100), 1); + damage = Math.round(damage) + + this.appendLog(`(${target.team}) ${target.character.nameShort} took ${damage} damage!`); + target.health -= damage; + }, + issunBoushi: (self: BattleUnit, target: BattleUnit, battle: Battle) => { + let damage = Math.floor(target.defaultHealth/10)*5; + //multiply by damageChange sum + damage *= self.potencyEffects + .filter(status => {return status.type === 'damage'}) + .reduce((a, b) => a + (b.potency/100), 1); + //multiply by resistanceChange sum + damage *= self.potencyEffects + .filter(status => {return status.type === 'resistance'}) + .reduce((a, b) => a - (b.potency/100), 1); + damage = Math.round(damage) + + this.appendLog(`(${target.team}) ${target.character.nameShort} took ${damage} damage!`); + target.health -= damage; + }, + pureLight: (self: BattleUnit, target: BattleUnit, battle: Battle) => { + let damage = 17; + + this.appendLog(`(${target.team}) ${target.character.nameShort} took ${damage} damage!`); + target.health -= damage; + }, + pristineLunacy: (self: BattleUnit, target: BattleUnit, battle: Battle) => { + let damage = 15; + + this.appendLog(`(${target.team}) ${target.character.nameShort} took ${damage} damage!`); + target.health -= damage; + }, + pdh: (self: BattleUnit, target: BattleUnit, battle: Battle) => { + let damage = 8; + + this.appendLog(`(${target.team}) ${target.character.nameShort} took ${damage} damage!`); + target.health -= damage; + }, + buffBuff: (self: BattleUnit, target: BattleUnit, battle: Battle) => { + target.potencyEffects = target.potencyEffects.map(effect => { + if (effect.potency > 0) effect.potency *= 2; + + return effect; + }); + + battle.appendLog(`${target.character.nameShort}'s buffs were strengthened!`); + }, + absoluteLoser: (self: BattleUnit, target: BattleUnit, battle: Battle) => { + const swap1 = self.health; + const swap2 = target.health; + + self.health = swap2; + target.health = swap1; + + battle.appendLog(`${target.character.nameShort} swapped health with ${self.character.nameShort}!`); + }, } } \ No newline at end of file diff --git a/src/cbclient.ts b/src/cbclient.ts index e6d4473..6d9c546 100644 --- a/src/cbclient.ts +++ b/src/cbclient.ts @@ -1,10 +1,13 @@ -import { ClientOptions, Collection, Message, Snowflake } from "discord.js"; +import { ClientOptions, Collection, EmbedBuilder, Message, Snowflake } from "discord.js"; +import Elo from "arpad"; +import { Sequelize } from "sequelize"; +import { Battle, BattleState } from "./battle"; +import { CHARACTERS } from "./characters"; import { CommandClient } from "./commandclient"; import { Player } from "./models/player"; -import { Battle } from "./battle"; -import { Sequelize } from "sequelize"; import { Unit } from "./models/unit"; -import { CHARACTERS } from "./characters"; + +const ELO = new Elo(); export class Challenge { constructor (player1: Player, player2: Player, message: Message) { @@ -27,8 +30,57 @@ export class ActiveBattle { this.message = message; } - sendRound () { + async sendRound () { + try { + this.rounds++; + const outcome = this.battle.simulateRound(); + const log = this.battle.log.slice(0, 1024); + + await this.message.edit({ + embeds: [ + new EmbedBuilder() + .addFields([ + { name: 'Team 1', value: `${this.battle.team1[0].character.name} [${this.battle.team1[0].health}]\n${this.battle.team1[1].character.name} [${this.battle.team1[1].health}]\n${this.battle.team1[2].character.name} [${this.battle.team1[2].health}]`, inline: true }, + { name: 'Team 2', value: `${this.battle.team2[0].character.name} [${this.battle.team2[0].health}]\n${this.battle.team2[1].character.name} [${this.battle.team2[1].health}]\n${this.battle.team2[2].character.name} [${this.battle.team2[2].health}]`, inline: true }, + { name: 'Log', value: log } + ]) + .setColor(0x8c110b) + .setThumbnail('https://i.imgur.com/sMrWQWO.png') + ] + }).catch(this.logAndSelfDestruct); + if (outcome === BattleState.Ongoing && this.rounds < 20) {this.timeout = setTimeout(this.sendRound.bind(this), 4_000);return;} + + this.battle.client.ACTIVE_BATTLES.splice(this.battle.client.ACTIVE_BATTLES.indexOf(this), 1); + + if (this.battle.channel.isDMBased()) return; + let elo1 = this.battle.player1.elo; + let elo2 = this.battle.player1.elo; + const player1 = this.battle.channel.guild.members.resolve(this.battle.player1.user_id); + const player2 = this.battle.channel.guild.members.resolve(this.battle.player2.user_id); + + switch (outcome) { + case BattleState.Team1Win: + await this.battle.player1.update({ elo: ELO.newRatingIfWon(elo1, elo2) }); + await this.battle.player2.update({ elo: ELO.newRatingIfLost(elo2, elo1) }); + + await this.battle.channel.send({ + content: `${player1?.user.username || '████████'} has defeated ${player2?.user.username || '████████'}!\n\n${player1?.user.username || '████████'} +${this.battle.player1.elo-elo1}\n${player2?.user.username || '████████'} -${elo2-this.battle.player2.elo}` + }).catch(this.logAndSelfDestruct); + break; + case BattleState.Team2Win: + await this.battle.player2.update({ elo: ELO.newRatingIfWon(elo2, elo1) }); + await this.battle.player1.update({ elo: ELO.newRatingIfLost(elo1, elo2) }); + + await this.battle.channel.send({ + content: `${player2?.user.username || '████████'} has defeated ${player1?.user.username || '████████'}!\n\n${player2?.user.username || '████████'} +${this.battle.player2.elo-elo2}\n${player1?.user.username || '████████'} -${elo1-this.battle.player1.elo}` + }).catch(this.logAndSelfDestruct); + break; + } + } + catch (err) { + this.logAndSelfDestruct(err); + } } static async create (battle: Battle) { @@ -36,8 +88,15 @@ export class ActiveBattle { return activeBattle; } + logAndSelfDestruct (err: unknown) { + console.error(err); + this.battle.client.ACTIVE_BATTLES.splice(this.battle.client.ACTIVE_BATTLES.indexOf(this), 1); + } + + rounds = 0 battle: Battle message: Message + timeout = setTimeout(this.sendRound.bind(this), 2_000) } export class CBClient extends CommandClient { @@ -51,12 +110,18 @@ export class CBClient extends CommandClient { const challenge = this.CHALLENGES.get(int.customId); if (!challenge || challenge.created+60_000 < Date.now()) { - int.reply("That challenge has expired!"); + int.reply({ + content: "That challenge has expired!", + ephemeral: true + }); return; } if (int.user.id != challenge.player2.user_id) { - int.reply("You can't accept someone else's challenge!"); + int.reply({ + content: "You can't accept someone else's challenge!", + ephemeral: true + }); return; } @@ -64,7 +129,10 @@ export class CBClient extends CommandClient { return [challenge.player1.user_id, challenge.player2.user_id].includes(e.battle.player1.user_id) || [challenge.player1.user_id, challenge.player2.user_id].includes(e.battle.player2.user_id); })) { - int.reply("A participant is already in a battle!"); + int.reply({ + content: "A participant is already in a battle!", + ephemeral: true + }); return; } @@ -72,22 +140,31 @@ export class CBClient extends CommandClient { this.CHALLENGES.delete(challenge.id); const team1: [Unit, Unit, Unit] = [ - await Unit.findOne({ where: { id: challenge.player1.slot1 } }) || defaultUnit, - await Unit.findOne({ where: { id: challenge.player1.slot2 } }) || defaultUnit, - await Unit.findOne({ where: { id: challenge.player1.slot3 } }) || defaultUnit + await Unit.findOne({ where: { id: challenge.player1.slot1 || -1 } }) || defaultUnit, + await Unit.findOne({ where: { id: challenge.player1.slot2 || -1 } }) || defaultUnit, + await Unit.findOne({ where: { id: challenge.player1.slot3 || -1 } }) || defaultUnit ] const team2: [Unit, Unit, Unit] = [ - await Unit.findOne({ where: { id: challenge.player2.slot1 } }) || defaultUnit, - await Unit.findOne({ where: { id: challenge.player2.slot2 } }) || defaultUnit, - await Unit.findOne({ where: { id: challenge.player2.slot3 } }) || defaultUnit + await Unit.findOne({ where: { id: challenge.player2.slot1 || -1 } }) || defaultUnit, + await Unit.findOne({ where: { id: challenge.player2.slot2 || -1 } }) || defaultUnit, + await Unit.findOne({ where: { id: challenge.player2.slot3 || -1 } }) || defaultUnit ] const battle = new Battle(this, int.channel, challenge.player1, challenge.player2, team1, team2); const activeBattle = new ActiveBattle(battle, await int.channel.send({ - content: 'wooo' + embeds: [ + new EmbedBuilder() + .addFields([ + { name: 'Team 1', value: `${battle.team1[0].character.name} [${battle.team1[0].health}]\n${battle.team1[1].character.name} [${battle.team1[1].health}]\n${battle.team1[2].character.name} [${battle.team1[2].health}]`, inline: true }, + { name: 'Team 2', value: `${battle.team2[0].character.name} [${battle.team2[0].health}]\n${battle.team2[1].character.name} [${battle.team2[1].health}]\n${battle.team2[2].character.name} [${battle.team2[2].health}]`, inline: true }, + { name: 'Log', value: 'Starting...' } + ]) + .setColor(0x8c110b) + .setThumbnail('https://i.imgur.com/sMrWQWO.png') + ] } )); - this.ACTIVE_BATTLES.push(); + this.ACTIVE_BATTLES.push(activeBattle); }); } @@ -120,15 +197,16 @@ export class CBClient extends CommandClient { } static async spendOnPlayer(player: Player, amount: number) { - const currency = Date.now() - player.start - player.spent; + const currency = Date.now()/1000 - player.start - player.spent; + console.debug(Date.now()/1000, player.start, player.spent, currency); if (currency - amount < 0) return false; - await player.update("spent", player.spent+amount); + await player.update({ spent: player.spent+amount }); return true; } static async grantMoneyToPlayer(player: Player, amount: number) { - await player.update("spent", player.spent-amount); + await player.update({ spent: player.spent-amount }); return; } } \ No newline at end of file diff --git a/src/characters.json b/src/characters.json index 3e9d0eb..d7b3bc7 100644 --- a/src/characters.json +++ b/src/characters.json @@ -206,7 +206,7 @@ { "target": "oneOpponent", "damage": 10, - "function": "" + "function": "perfectFreeze" } ] }, @@ -1406,10 +1406,11 @@ "skills": [ { "name": "Mind of God \"Omoikane's Brain\"", + "accuracy": 9999, "effects": [ { "target": "oneOpponent", - "function": "" + "damage": 8 } ] }, @@ -1469,7 +1470,7 @@ }, { "target": "self", - "function": "" + "function": "fireBird" } ] }, @@ -1709,7 +1710,7 @@ }, { "target": "self", - "function": "" + "function": "aPoison" } ] }, @@ -1739,11 +1740,11 @@ "effects": [ { "target": "allOpponents", - "function": "" + "function": "pathOfAvici" }, { "target": "team", - "function": "" + "function": "pathOfAvici" } ] }, @@ -1752,7 +1753,7 @@ "effects": [ { "target": "oneOpponent", - "function": "" + "function": "poorFate" } ] }, @@ -2498,7 +2499,7 @@ "effects": [ { "target": "oneOpponent", - "function": "" + "function": "terrifyingHypnotism" } ] }, @@ -3449,7 +3450,7 @@ }, { "target": "self", - "function": "" + "function": "risingSun" } ] }, @@ -3957,11 +3958,11 @@ { "target": "oneOpponent", "damage": 15, - "function": "" + "function": "overturnBuffs" }, { "target": "team", - "function": "" + "function": "overturnDebuffs" } ] }, @@ -3970,7 +3971,7 @@ "effects": [ { "target": "oneOpponent", - "function": "" + "function": "overturnHealth" } ] }, @@ -3980,7 +3981,7 @@ { "target": "oneOpponent", "damage": 10, - "function": "" + "function": "reverseHierarchy" } ] } @@ -4015,12 +4016,15 @@ "name": "Mallet \"You Grow Bigger!\"", "effects": [ { - "target": "oneOpponent", + "target": "oneTeammate", "damageChange": { "duration": 1, "potency": 20 }, - "function": "" + "resistanceChange": { + "duration": 1, + "potency": 20 + } } ] }, @@ -4030,8 +4034,7 @@ "effects": [ { "target": "oneOpponent", - "damage": 7, - "function": "" + "function": "issunBoushi" } ] } @@ -4242,10 +4245,11 @@ }, { "name": "Gun Sign \"Lunatic Gun\"", + "accuracy": 9999, "effects": [ { "target": "oneOpponent", - "function": "" + "damage": 20 } ] } @@ -4491,7 +4495,7 @@ "effects": [ { "target": "oneOpponent", - "function": "" + "function": "pureLight" } ] }, @@ -4501,7 +4505,7 @@ { "target": "oneOpponent", "confusion": 3, - "function": "" + "function": "pristineLunacy" } ] }, @@ -4510,23 +4514,23 @@ "effects": [ { "target": "trueRandomOpponent", - "function": "" + "function": "pdh" }, { "target": "trueRandomOpponent", - "function": "" + "function": "pdh" }, { "target": "trueRandomOpponent", - "function": "" + "function": "pdh" }, { "target": "trueRandomOpponent", - "function": "" + "function": "pdh" }, { "target": "trueRandomOpponent", - "function": "" + "function": "pdh" } ] } @@ -4943,11 +4947,11 @@ "effects": [ { "target": "allOpponents", - "function": "" + "function": "buffBuff" }, { "target": "team", - "function": "" + "function": "buffBuff" } ] } @@ -4976,7 +4980,7 @@ "effects": [ { "target": "oneOpponent", - "function": "" + "function": "absoluteLoser" } ] }, @@ -5024,7 +5028,7 @@ "effects": [ { "target": "oneOpponent", - "function": "" + "function": "absoluteLoser" } ] }, diff --git a/src/commands/commandlist.ts b/src/commands/commandlist.ts index ef5d490..da385a5 100644 --- a/src/commands/commandlist.ts +++ b/src/commands/commandlist.ts @@ -2,6 +2,7 @@ import { BlacklistCommand } from "./blacklist"; import { Command } from "./command"; import { DeploySlashCommand } from "./deployslash"; import { GlobalBlacklistCommand } from "./gblacklist"; +import { InstructionsCommand } from "./instructions"; import { KillCommand } from "./kill"; import { RandomCaseCommand } from "./randomcase"; @@ -10,5 +11,6 @@ export const CommandList: Command[] = [ new BlacklistCommand(), new RandomCaseCommand(), new KillCommand(), - new DeploySlashCommand() + new DeploySlashCommand(), + new InstructionsCommand() ]; \ No newline at end of file diff --git a/src/commands/instructions.ts b/src/commands/instructions.ts new file mode 100644 index 0000000..d959331 --- /dev/null +++ b/src/commands/instructions.ts @@ -0,0 +1,21 @@ +import { Message, PermissionResolvable } from "discord.js"; +import { Command } from "./command"; + +export class InstructionsCommand implements Command { + name = 'instructions' + aliases = [] + description = 'Described how to use the bot' + usage = 'instructions' + permission = [] + guildOnly = false + ownerOnly = false + args = [] + + async execute(msg: Message) { + msg.channel.send(`Here's a list of commands: +\`/roll\`: Roll a new unit. You need points to roll. You earn points simply by being in the server. +\`/list\`: View all of your current units. +\`/equip\`: Equip a unit to use them in battles. Choose the numbered slot (1, 2, or 3) and the ID of the unit to equip. +\`/challenge\`: Challenge somebody to a battle. Select somebody to challenge. Both players must have units in all 3 slots.`) + } +}; diff --git a/src/models/player.ts b/src/models/player.ts index 07a667d..8f27531 100644 --- a/src/models/player.ts +++ b/src/models/player.ts @@ -5,7 +5,7 @@ export class Player extends Model, InferCreationAttribut declare start: number declare spent: CreationOptional declare elo: CreationOptional - declare slot1: CreationOptional - declare slot2: CreationOptional - declare slot3: CreationOptional + declare slot1: number | null + declare slot2: number | null + declare slot3: number | null } \ No newline at end of file diff --git a/src/slash/challenge.ts b/src/slash/challenge.ts index 44ace1d..e051a2c 100644 --- a/src/slash/challenge.ts +++ b/src/slash/challenge.ts @@ -12,7 +12,7 @@ export class ChallengeCommand implements SlashCommand { permission = [] ownerOnly = false guildOnly = true - guildID = "739645806100873328" //for testing + //guildID = "739645806100873328" //for testing args: ApplicationCommandOptionData[] = [ { name: 'opponent', diff --git a/src/slash/equip.ts b/src/slash/equip.ts index b8f3643..e78bdab 100644 --- a/src/slash/equip.ts +++ b/src/slash/equip.ts @@ -1,8 +1,7 @@ -import { ApplicationCommandOptionData, ApplicationCommandOptionType, CommandInteraction, User } from "discord.js" +import { ApplicationCommandOptionData, ApplicationCommandOptionType, CommandInteraction } from "discord.js" import { CBClient } from "../cbclient" -import { CHARACTERS } from "../characters" import { Unit } from "../models/unit" -import { createArgumentsObject, SlashCommand } from "./slash" +import { SlashCommand, createArgumentsObject } from "./slash" interface EquipArguments { slot: number @@ -15,7 +14,7 @@ export class EquipCommand implements SlashCommand { permission = [] ownerOnly = false guildOnly = false - guildID = "739645806100873328" //for testing + //guildID = "739645806100873328" //for testing args: ApplicationCommandOptionData[] = [ { name: 'slot', @@ -40,9 +39,10 @@ export class EquipCommand implements SlashCommand { content: `That isn't your unit!` }); - if (player.slot1 === args.unit) player.set({slot1:undefined}); - if (player.slot2 === args.unit) player.set({slot2:undefined}); - if (player.slot3 === args.unit) player.set({slot3:undefined}); + console.debug(player.slot1 === args.unit); + if (player.slot1 === args.unit) player.set({slot1:null}); + if (player.slot2 === args.unit) player.set({slot2:null}); + if (player.slot3 === args.unit) player.set({slot3:null}); switch (args.slot) { case 1: diff --git a/src/slash/grant.ts b/src/slash/grant.ts index 7c8e103..d98aebc 100644 --- a/src/slash/grant.ts +++ b/src/slash/grant.ts @@ -1,4 +1,4 @@ -import { ApplicationCommandOptionData, ApplicationCommandOptionType, CommandInteraction, User } from "discord.js" +import { ApplicationCommandOptionData, ApplicationCommandOptionType, CommandInteraction, PermissionFlagsBits, User } from "discord.js" import { CBClient } from "../cbclient" import { CHARACTERS } from "../characters" import { Unit } from "../models/unit" @@ -12,10 +12,10 @@ interface GrantArguments { export class GrantCommand implements SlashCommand { name = 'grant' description = 'Grant someone a character' - permission = [] - ownerOnly = true + permission = [PermissionFlagsBits.Administrator] + ownerOnly = false guildOnly = false - guildID = "739645806100873328" //for testing + //guildID = "739645806100873328" //for testing args: ApplicationCommandOptionData[] = [ { name: 'user', diff --git a/src/slash/list.ts b/src/slash/list.ts index dd9d81c..53fc17b 100644 --- a/src/slash/list.ts +++ b/src/slash/list.ts @@ -1,4 +1,4 @@ -import { ApplicationCommandOptionData, ApplicationCommandOptionType, CommandInteraction, EmbedBuilder, User } from "discord.js" +import { ApplicationCommandOptionData, ApplicationCommandOptionType, CommandInteraction, EmbedBuilder, GuildMember, User } from "discord.js" import { CBClient } from "../cbclient" import { CHARACTERS } from "../characters" import { Unit } from "../models/unit" @@ -6,6 +6,7 @@ import { createArgumentsObject, SlashCommand } from "./slash" interface ListArguments { page?: number + user?: GuildMember } export class ListCommand implements SlashCommand { @@ -13,14 +14,20 @@ export class ListCommand implements SlashCommand { description = 'List your units' permission = [] ownerOnly = false - guildOnly = false - guildID = "739645806100873328" //for testing + guildOnly = true + //guildID = "739645806100873328" //for testing args: ApplicationCommandOptionData[] = [ { name: 'page', type: ApplicationCommandOptionType.Integer, description: "The page to view", required: false + }, + { + name: 'user', + type: ApplicationCommandOptionType.User, + description: "The user to view", + required: false } ] @@ -28,9 +35,10 @@ export class ListCommand implements SlashCommand { const args = createArgumentsObject(int.options.data); if (!args.page) args.page = 1; args.page = Math.max(args.page, 1); - const player = await CBClient.findOrCreatePlayer(int.user.id); + const player = await CBClient.findOrCreatePlayer(args.user?.id || int.user.id); + const user = args.user?.user || int.user - const units = await Unit.findAll({ where: { user_id: int.user.id } }); + const units = await Unit.findAll({ where: { user_id: user.id } }); let liststr = ''; for (let i = (args.page-1)*10; i < Math.min(args.page*10, units.length); i++) { @@ -38,9 +46,9 @@ export class ListCommand implements SlashCommand { liststr += `${units[i].id}: ${character.name}\n`; } - const slot1 = await Unit.findOne({ where: { id: player.slot1 } }); - const slot2 = await Unit.findOne({ where: { id: player.slot2 } }); - const slot3 = await Unit.findOne({ where: { id: player.slot3 } }); + const slot1 = await Unit.findOne({ where: { id: player.slot1 || -1 } }); + const slot2 = await Unit.findOne({ where: { id: player.slot2 || -1 } }); + const slot3 = await Unit.findOne({ where: { id: player.slot3 || -1 } }); const character1 = CHARACTERS.get(slot1?.character_id || -1); const character2 = CHARACTERS.get(slot2?.character_id || -1); const character3 = CHARACTERS.get(slot3?.character_id || -1); @@ -48,9 +56,12 @@ export class ListCommand implements SlashCommand { return int.reply({ embeds: [ new EmbedBuilder() - .setTitle(`${int.user.username}'s Units`) + .setTitle(`[${player.elo}] ${user.username}'s Units (${args.page}/${Math.ceil(units.length/10)})`) .setDescription(liststr || 'Empty...') - .addFields({ name: 'Equipped', value: `1: ${character1 ? character1.name + ` (${slot1?.id})` : 'Empty'}\n2: ${character2 ? character2.name + ` (${slot2?.id})` : 'Empty'}\n3: ${character3 ? character3.name + ` (${slot3?.id})` : 'Empty'}` }) + .addFields( + { name: 'Equipped', value: `1: ${character1 ? character1.name + ` (${slot1?.id})` : 'Empty'}\n2: ${character2 ? character2.name + ` (${slot2?.id})` : 'Empty'}\n3: ${character3 ? character3.name + ` (${slot3?.id})` : 'Empty'}` }, + { name: `${Math.round((Date.now()/1000)-player.start-player.spent)} points`, value: `${Math.round((Date.now()/1000)-player.start-player.spent) > 3600 ? 'You can roll!' : 'Keep saving...'}` } + ) ] }); } diff --git a/src/slash/roll.ts b/src/slash/roll.ts index f19f6ca..c68eaa7 100644 --- a/src/slash/roll.ts +++ b/src/slash/roll.ts @@ -6,17 +6,17 @@ import { createArgumentsObject, SlashCommand } from "./slash" export class RollCommand implements SlashCommand { name = 'roll' - description = 'Pull a random character' + description = 'Pull a random character for 3600 points' permission = [] ownerOnly = false guildOnly = false - guildID = "739645806100873328" //for testing + //guildID = "739645806100873328" //for testing args: ApplicationCommandOptionData[] = [] async execute(int: CommandInteraction) { const player = await CBClient.findOrCreatePlayer(int.user.id); - if (!(await CBClient.spendOnPlayer(player, 3600_000))) return int.reply({ + if (!(await CBClient.spendOnPlayer(player, 3600))) return int.reply({ content: "You don't have the money!", ephemeral: true }); @@ -30,7 +30,7 @@ export class RollCommand implements SlashCommand { return int.reply({ embeds: [ new EmbedBuilder() - .setTitle(`You got **${character.name}**!`) + .setTitle(`${int.user.username}, you got **${character.name}**!`) .setImage(character.img) ] });