import { Message, TextBasedChannel, TextChannel } from "discord.js"; import { CBClient } from "./cbclient"; import { Character, CHARACTERS, Effect, PotencyStatus, Target } from "./characters"; import { Player } from "./models/player"; import { Unit } from "./models/unit"; export enum BattleState { Ongoing, Team1Win, Team2Win } enum EffectType { Resistance = 'resistance', Accuracy = 'accuracy', Speed = 'speed', Damage = 'damage' } interface PotencyEffect { type: EffectType duration: number potency: number } interface StatusEffects { poison: number regeneration: number burn: number confusion: number stun: number taunt: number } class BattleUnit { constructor (unit: Unit, battle: Battle) { this.unit = unit; this.battle = battle; this.defaultHealth = unit.health; this.health = unit.health; this.speed = unit.speed; this.character = CHARACTERS.get(unit.character_id)!; } unit: Unit battle: Battle defaultHealth: number health: number speed: number character: Character statusEffects: StatusEffects = { poison: 0, regeneration: 0, burn: 0, confusion: 0, stun: 0, taunt: 0 } potencyEffects: PotencyEffect[] = [] active = true } export class Battle { constructor (client: CBClient, channel: TextBasedChannel, player1: Player, player2: Player, team1: [Unit, Unit, Unit], team2: [Unit, Unit, Unit]) { this.client = client; 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.units = this.team1.concat(this.team2) as [BattleUnit, BattleUnit, BattleUnit, BattleUnit, BattleUnit, BattleUnit]; } simulateRound (): BattleState { //reset log each round this.log = ''; this.tickStatuses(this.units); this.checkAlive(this.units); this.useSkills(this.units); //check to see if a team has no active units if (!this.team1.filter(e => {return e.active})[0]) return BattleState.Team2Win; else if (!this.team2.filter(e => {return e.active})[0]) return BattleState.Team1Win; //let repeating rounds be handled by the invoker else return BattleState.Ongoing; } tickStatuses (units: BattleUnit[]) { for (const unit of units) { //dead touhous tell no tales if (!unit.active) continue; if (unit.statusEffects.poison > 0) { unit.statusEffects.poison-- const change = Math.floor(unit.defaultHealth/12); unit.health -= change; this.appendLog(`${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!`); } if (unit.statusEffects.burn > 0) { unit.statusEffects.burn-- unit.health -= 5; this.appendLog(`${unit.character.nameShort} took 5 burn damage!`); } if (unit.statusEffects.confusion > 0) unit.statusEffects.confusion--; if (unit.statusEffects.stun > 0) unit.statusEffects.stun--; if (unit.statusEffects.taunt > 0) unit.statusEffects.taunt--; for (const status of unit.potencyEffects) { status.duration--; } unit.potencyEffects.filter(Battle.buffIsActive); } this.appendLog(''); } useSkills (units: BattleUnit[]) { const order = []; for (let i = 0; i < units.length; i++) { const rolls = [] //modify speed by speedChange effects const affectedSpeed = units[i].speed + units[i].potencyEffects .filter(status => {return status.type === 'speed'}) .reduce((a, b) => a + b.potency, 0); //roll n for (let o = 0; o < affectedSpeed; o++) { rolls.push(Math.random()); } //pick max const initiative = Math.max(...rolls); order.push([i, initiative]); } //sort turn order order.sort((a, b) => {return b[1] - a[1]}); console.debug(order); for (const initiative of order) { //unit is self const unit = units[initiative[0]]; //dead touhous tell no tales if (!unit.active) continue; //pick random skill const skill = unit.character.skills[Math.floor(Math.random()*unit.character.skills.length)]; const team = initiative[0] < 3 ? this.team1 : this.team2; const opponentTeam = initiative[0] < 3 ? this.team2 : this.team1; //targets are chosen randomly between the units with the most taunt const opponentTargets = opponentTeam.filter(unit => { return unit.statusEffects.taunt >= Math.max(...opponentTeam.map(unit => {return unit.statusEffects.taunt})); }); //pick random targets const target = opponentTargets[Math.floor(Math.random()*opponentTargets.length)]; const allyTarget = team[Math.floor(Math.random()*team.length)]; //modify accuracy by accuracyChange effects const affectedAccuracy = (skill.accuracy || 100) + unit.potencyEffects .filter(status => {return status.type === 'accuracy'}) .reduce((a, b) => a + b.potency, 0); //chance to stun if (unit.statusEffects.stun) this.appendLog(`${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!`); } //chance to miss else if (Math.random()*100 > affectedAccuracy) { this.appendLog(`${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; } } } this.checkAlive(this.units); this.appendLog(''); } } runEffect (effect: Effect, self: BattleUnit, targets: BattleUnit[]) { for (const target of targets) { //chance to miss if (effect.accuracy && Math.random()*100 > effect.accuracy) { this.appendLog(`${target.character.nameShort} avoided the attack!`); continue; } if (effect.damage) { let damage = effect.damage; //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.character.nameShort} took ${damage} damage!`); target.health -= damage; } if (effect.heal) { this.appendLog(`${target.character.nameShort} restored ${effect.heal} health!`); target.health += effect.heal; } if (effect.cure) { this.appendLog(`${target.character.nameShort} was cured of ailments!`); target.statusEffects.burn = 0; target.statusEffects.confusion = 0; target.statusEffects.stun = 0; target.statusEffects.poison = 0; target.statusEffects.taunt = 0; target.potencyEffects.filter(Battle.isBuff); } if (effect.dispel) { this.appendLog(`${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!`); } if (effect.regeneration) { target.statusEffects.regeneration = Math.max(target.statusEffects.regeneration, effect.regeneration); this.appendLog(`${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!`); } if (effect.confusion) { target.statusEffects.confusion = Math.max(target.statusEffects.confusion, effect.confusion); this.appendLog(`${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!`); } if (effect.taunt) { target.statusEffects.taunt = Math.max(target.statusEffects.taunt, effect.taunt); this.appendLog(`${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'} ${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'} ${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'} ${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'} ${effect.damageChange.potency}% damage!`); } if (effect.function) Battle.skillFunctions[effect.function](target, this); } } checkAlive (units: BattleUnit[]) { for (const unit of units) { if (!unit.active) continue; console.debug(unit.health); if (unit.health > 0) continue; unit.active = false; this.appendLog(`${unit.character.nameShort} has been defeated!`); } } appendLog (str: string) { if (this.log) this.log += '\n'; this.log += str; console.debug(str); return this.log; } static isBuff (status: PotencyStatus) { return status.potency >= 0; } static isDebuff (status: PotencyStatus) { return status.potency < 0; } static buffIsActive (status: PotencyStatus) { return status.duration > 0; } static createPotencyEffect (status: PotencyStatus, type: EffectType): PotencyEffect { return { type: type, duration: status.duration, potency: status.potency }; } message?: Message log = "" client: CBClient channel: TextBasedChannel player1: Player player2: Player team1: [BattleUnit, BattleUnit, BattleUnit] 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) => { if (!(battle.channel instanceof TextChannel)) 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>!`); } } }