355 lines
14 KiB
TypeScript
355 lines
14 KiB
TypeScript
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>!`);
|
|
}
|
|
}
|
|
} |