battle logic

This commit is contained in:
Hexugory 2023-03-31 00:44:33 -05:00
parent f257bd594b
commit c41c5dca7c
3 changed files with 212 additions and 62 deletions

View File

@ -1,4 +1,5 @@
import { Message, TextBasedChannel } from "discord.js"; import { Message, TextBasedChannel, TextChannel } from "discord.js";
import { CBClient } from "./cbclient";
import { Character, CHARACTERS, Effect, PotencyStatus, Target } from "./characters"; import { Character, CHARACTERS, Effect, PotencyStatus, Target } from "./characters";
import { Player } from "./models/player"; import { Player } from "./models/player";
import { Unit } from "./models/unit"; import { Unit } from "./models/unit";
@ -9,24 +10,32 @@ export enum BattleState {
Team2Win Team2Win
} }
enum EffectType {
Resistance = 'resistance',
Accuracy = 'accuracy',
Speed = 'speed',
Damage = 'damage'
}
interface PotencyEffect {
type: EffectType
duration: number
potency: number
}
interface StatusEffects { interface StatusEffects {
poison: number poison: number
regeneration: number regeneration: number
burn: number burn: number
confusion: number confusion: number
stun: number stun: number
} taunt: number
interface PotencyEffects {
resistanceChange: PotencyStatus[]
accuracyChange: PotencyStatus[]
speedChange: PotencyStatus[]
damageChange: PotencyStatus[]
} }
class BattleUnit { class BattleUnit {
constructor (unit: Unit) { constructor (unit: Unit, battle: Battle) {
this.unit = unit; this.unit = unit;
this.battle = battle;
this.defaultHealth = unit.health; this.defaultHealth = unit.health;
this.health = unit.health; this.health = unit.health;
this.speed = unit.speed; this.speed = unit.speed;
@ -34,6 +43,7 @@ class BattleUnit {
} }
unit: Unit unit: Unit
battle: Battle
defaultHealth: number defaultHealth: number
health: number health: number
speed: number speed: number
@ -43,40 +53,41 @@ class BattleUnit {
regeneration: 0, regeneration: 0,
burn: 0, burn: 0,
confusion: 0, confusion: 0,
stun: 0 stun: 0,
} taunt: 0
potencyEffects: PotencyEffects = {
resistanceChange: [],
accuracyChange: [],
speedChange: [],
damageChange: []
} }
potencyEffects: PotencyEffect[] = []
active = true active = true
} }
export class Battle { export class Battle {
constructor (channel: TextBasedChannel, player1: Player, player2: Player, team1: [Unit, Unit, Unit], team2: [Unit, Unit, Unit]) { constructor (client: CBClient, channel: TextBasedChannel, player1: Player, player2: Player, team1: [Unit, Unit, Unit], team2: [Unit, Unit, Unit]) {
this.client = client;
this.channel = channel; this.channel = channel;
this.player1 = player1; this.player1 = player1;
this.player2 = player2; this.player2 = player2;
this.team1 = team1.map(unit => {return new BattleUnit(unit)}) as [BattleUnit, BattleUnit, BattleUnit]; this.team1 = team1.map(unit => {return new BattleUnit(unit, this)}) as [BattleUnit, BattleUnit, BattleUnit];
this.team2 = team2.map(unit => {return new BattleUnit(unit)}) 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]; this.units = this.team1.concat(this.team2) as [BattleUnit, BattleUnit, BattleUnit, BattleUnit, BattleUnit, BattleUnit];
} }
simulateRound (): BattleState { simulateRound (): BattleState {
//reset log each round
this.log = ''; this.log = '';
this.tickStatuses(this.units); this.tickStatuses(this.units);
this.checkAlive(this.units); this.checkAlive(this.units);
this.useSkills(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; 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; 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; else return BattleState.Ongoing;
} }
tickStatuses (units: BattleUnit[]) { tickStatuses (units: BattleUnit[]) {
for (const unit of units) { for (const unit of units) {
//dead touhous tell no tales
if (!unit.active) continue; if (!unit.active) continue;
if (unit.statusEffects.poison > 0) { if (unit.statusEffects.poison > 0) {
@ -85,21 +96,26 @@ export class Battle {
unit.health -= change; unit.health -= change;
this.appendLog(`${unit.character.nameShort} took ${change} poison damage!`); this.appendLog(`${unit.character.nameShort} took ${change} poison damage!`);
} }
this.log += '\n';
if (unit.statusEffects.regeneration > 0) { if (unit.statusEffects.regeneration > 0) {
unit.statusEffects.regeneration-- unit.statusEffects.regeneration--
const change = Math.floor(unit.defaultHealth/10) const change = Math.floor(unit.defaultHealth/10)
unit.health += change; unit.health += change;
this.appendLog(`${unit.character.nameShort} regenerated ${change} health!`); this.appendLog(`${unit.character.nameShort} regenerated ${change} health!`);
} }
this.log += '\n';
if (unit.statusEffects.burn > 0) { if (unit.statusEffects.burn > 0) {
unit.statusEffects.burn-- unit.statusEffects.burn--
unit.health -= 5; unit.health -= 5;
this.appendLog(`${unit.character.nameShort} took 5 burn damage!`); this.appendLog(`${unit.character.nameShort} took 5 burn damage!`);
} }
this.log += '\n'; 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[]) { useSkills (units: BattleUnit[]) {
@ -107,75 +123,175 @@ export class Battle {
for (let i = 0; i < units.length; i++) { for (let i = 0; i < units.length; i++) {
const rolls = [] const rolls = []
let modifiedSpeed = units[i].speed //modify speed by speedChange effects
for (const change of units[i].potencyEffects.speedChange) { const affectedSpeed = units[i].speed
modifiedSpeed -= change.potency; + units[i].potencyEffects
} .filter(status => {return status.type === 'speed'})
for (let o = 0; o < modifiedSpeed; o++) { .reduce((a, b) => a + b.potency, 0);
//roll n
for (let o = 0; o < affectedSpeed; o++) {
rolls.push(Math.random()); rolls.push(Math.random());
} }
//pick max
const initiative = Math.max(...rolls); const initiative = Math.max(...rolls);
order.push([i, initiative]); order.push([i, initiative]);
} }
//sort turn order
order.sort((a, b) => {return b[1] - a[1]}); order.sort((a, b) => {return b[1] - a[1]});
console.debug(order); console.debug(order);
for (const initiative of order) { for (const initiative of order) {
//unit is self
const unit = units[initiative[0]]; const unit = units[initiative[0]];
//dead touhous tell no tales
if (!unit.active) continue; if (!unit.active) continue;
//pick random skill
const skill = unit.character.skills[Math.floor(Math.random()*unit.character.skills.length)]; const skill = unit.character.skills[Math.floor(Math.random()*unit.character.skills.length)];
const team = initiative[0] < 3 ? 1 : 2 const team = initiative[0] < 3 ? this.team1 : this.team2;
const target = team === 1 ? this.team2[Math.floor(Math.random()*this.team2.length)] : this.team1[Math.floor(Math.random()*this.team1.length)]; const opponentTeam = initiative[0] < 3 ? this.team2 : this.team1;
const allyTarget = team === 1 ? this.team1[Math.floor(Math.random()*this.team1.length)] : this.team2[Math.floor(Math.random()*this.team2.length)]; //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);
if (skill.accuracy && Math.random()*100 > skill.accuracy) { //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!`); this.appendLog(`${unit.character.nameShort} used ${skill.name}, but missed!`);
} }
else {
this.appendLog(`${unit.character.nameShort} used ${skill.name}!`); this.appendLog(`${unit.character.nameShort} used ${skill.name}!`);
for (const effect of skill.effects) { for (const effect of skill.effects) {
switch (effect.target){ switch (effect.target){
case Target.Self: case Target.Self:
this.runEffect(effect, unit, [unit]); this.runEffect(effect, unit, [unit]);
break; break;
case Target.Team: case Target.Team:
this.runEffect(effect, unit, team === 1 ? this.team1 : this.team2); this.runEffect(effect, unit, team);
break; break;
case Target.OneTeammate: case Target.OneTeammate:
this.runEffect(effect, unit, [allyTarget]); this.runEffect(effect, unit, [allyTarget]);
break; break;
case Target.OneOpponent: case Target.TrueRandomTeammate:
this.runEffect(effect, unit, [target]); this.runEffect(effect, unit, [team[Math.floor(Math.random()*team.length)]]);
break; break;
case Target.AllOpponents: case Target.OneOpponent:
this.runEffect(effect, unit, team === 1 ? this.team2 : this.team1); this.runEffect(effect, unit, [target]);
break; break;
case Target.TrueRandomOpponent: case Target.AllOpponents:
this.runEffect(effect, unit, [team === 1 ? this.team2[Math.floor(Math.random()*this.team2.length)] : this.team1[Math.floor(Math.random()*this.team1.length)]]); this.runEffect(effect, unit, opponentTeam);
break; break;
case Target.TrueRandomOpponent:
this.runEffect(effect, unit, [opponentTeam[Math.floor(Math.random()*opponentTeam.length)]]);
break;
}
} }
} }
this.checkAlive(this.units); this.checkAlive(this.units);
this.appendLog('');
} }
} }
runEffect (effect: Effect, self: BattleUnit, targets: BattleUnit[]) { runEffect (effect: Effect, self: BattleUnit, targets: BattleUnit[]) {
if (effect.damage) { for (const target of targets) {
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; let damage = effect.damage;
for (const statChange of self.potencyEffects.damageChange) { //multiply by damageChange sum
Math.round(damage *= 1+(statChange.potency/100)); damage *= self.potencyEffects
} .filter(status => {return status.type === 'damage'})
for (const statChange of target.potencyEffects.resistanceChange) { .reduce((a, b) => a + (b.potency/100), 1);
Math.round(damage *= 1-(statChange.potency/100)); //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!`); this.appendLog(`${target.character.nameShort} took ${damage} damage!`);
target.health -= 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);
} }
} }
@ -196,13 +312,44 @@ export class Battle {
return this.log; 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 message?: Message
log = "" log = ""
client: CBClient
channel: TextBasedChannel channel: TextBasedChannel
player1: Player player1: Player
player2: Player player2: Player
team1: [BattleUnit, BattleUnit, BattleUnit] team1: [BattleUnit, BattleUnit, BattleUnit]
team2: [BattleUnit, BattleUnit, BattleUnit] team2: [BattleUnit, BattleUnit, BattleUnit]
units: [BattleUnit, BattleUnit, BattleUnit, 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>!`);
}
}
} }

View File

@ -6,6 +6,7 @@ export enum Target {
Self = "self", Self = "self",
Team = "team", Team = "team",
OneTeammate = "oneTeammate", OneTeammate = "oneTeammate",
TrueRandomTeammate = "trueRandomTeammate",
OneOpponent = "oneOpponent", OneOpponent = "oneOpponent",
AllOpponents = "allOpponents", AllOpponents = "allOpponents",
TrueRandomOpponent = "trueRandomOpponent" TrueRandomOpponent = "trueRandomOpponent"
@ -26,11 +27,13 @@ const Effect = z.object({
damage: z.optional(z.number().int()), damage: z.optional(z.number().int()),
heal: z.optional(z.number().int()), heal: z.optional(z.number().int()),
cure: z.optional(z.boolean()), cure: z.optional(z.boolean()),
dispel: z.optional(z.boolean()),
poison: z.optional(z.number().int()), poison: z.optional(z.number().int()),
regeneration: z.optional(z.number().int()), regeneration: z.optional(z.number().int()),
burn: z.optional(z.number().int()), burn: z.optional(z.number().int()),
confusion: z.optional(z.number().int()), confusion: z.optional(z.number().int()),
stun: z.optional(z.number().int()), stun: z.optional(z.number().int()),
taunt: z.optional(z.number().int()),
resistanceChange: z.optional(PotencyStatus), resistanceChange: z.optional(PotencyStatus),
accuracyChange: z.optional(PotencyStatus), accuracyChange: z.optional(PotencyStatus),
speedChange: z.optional(PotencyStatus), speedChange: z.optional(PotencyStatus),

View File

@ -19,7 +19,7 @@ export class BattleTestCommand implements SlashCommand {
const client = int.client as CBClient; const client = int.client as CBClient;
const you = await client.findOrCreatePlayer(int.user.id); const you = await client.findOrCreatePlayer(int.user.id);
const units = (await Unit.findAll()); const units = (await Unit.findAll());
const battle = new Battle(int.channel!, you, you, units.slice(0, 3) as [Unit, Unit, Unit], units.slice(3, 6) as [Unit, Unit, Unit]); const battle = new Battle(client, int.channel!, you, you, units.slice(0, 3) as [Unit, Unit, Unit], units.slice(3, 6) as [Unit, Unit, Unit]);
await int.reply({ await int.reply({
content: 'gottem' content: 'gottem'