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 { Player } from "./models/player";
import { Unit } from "./models/unit";
@ -9,24 +10,32 @@ export enum BattleState {
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
}
interface PotencyEffects {
resistanceChange: PotencyStatus[]
accuracyChange: PotencyStatus[]
speedChange: PotencyStatus[]
damageChange: PotencyStatus[]
taunt: number
}
class BattleUnit {
constructor (unit: Unit) {
constructor (unit: Unit, battle: Battle) {
this.unit = unit;
this.battle = battle;
this.defaultHealth = unit.health;
this.health = unit.health;
this.speed = unit.speed;
@ -34,6 +43,7 @@ class BattleUnit {
}
unit: Unit
battle: Battle
defaultHealth: number
health: number
speed: number
@ -43,40 +53,41 @@ class BattleUnit {
regeneration: 0,
burn: 0,
confusion: 0,
stun: 0
}
potencyEffects: PotencyEffects = {
resistanceChange: [],
accuracyChange: [],
speedChange: [],
damageChange: []
stun: 0,
taunt: 0
}
potencyEffects: PotencyEffect[] = []
active = true
}
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.player1 = player1;
this.player2 = player2;
this.team1 = team1.map(unit => {return new BattleUnit(unit)}) as [BattleUnit, BattleUnit, BattleUnit];
this.team2 = team2.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, 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) {
@ -85,21 +96,26 @@ export class Battle {
unit.health -= change;
this.appendLog(`${unit.character.nameShort} took ${change} poison damage!`);
}
this.log += '\n';
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.log += '\n';
if (unit.statusEffects.burn > 0) {
unit.statusEffects.burn--
unit.health -= 5;
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[]) {
@ -107,75 +123,175 @@ export class Battle {
for (let i = 0; i < units.length; i++) {
const rolls = []
let modifiedSpeed = units[i].speed
for (const change of units[i].potencyEffects.speedChange) {
modifiedSpeed -= change.potency;
}
for (let o = 0; o < modifiedSpeed; o++) {
//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 ? 1 : 2
const target = team === 1 ? this.team2[Math.floor(Math.random()*this.team2.length)] : this.team1[Math.floor(Math.random()*this.team1.length)];
const allyTarget = team === 1 ? this.team1[Math.floor(Math.random()*this.team1.length)] : this.team2[Math.floor(Math.random()*this.team2.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);
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}!`);
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 === 1 ? this.team1 : this.team2);
break;
case Target.OneTeammate:
this.runEffect(effect, unit, [allyTarget]);
break;
case Target.OneOpponent:
this.runEffect(effect, unit, [target]);
break;
case Target.AllOpponents:
this.runEffect(effect, unit, team === 1 ? this.team2 : this.team1);
break;
case Target.TrueRandomOpponent:
this.runEffect(effect, unit, [team === 1 ? this.team2[Math.floor(Math.random()*this.team2.length)] : this.team1[Math.floor(Math.random()*this.team1.length)]]);
break;
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[]) {
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;
for (const statChange of self.potencyEffects.damageChange) {
Math.round(damage *= 1+(statChange.potency/100));
}
for (const statChange of target.potencyEffects.resistanceChange) {
Math.round(damage *= 1-(statChange.potency/100));
}
//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);
}
}
@ -196,13 +312,44 @@ export class Battle {
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>!`);
}
}
}

View File

@ -6,6 +6,7 @@ export enum Target {
Self = "self",
Team = "team",
OneTeammate = "oneTeammate",
TrueRandomTeammate = "trueRandomTeammate",
OneOpponent = "oneOpponent",
AllOpponents = "allOpponents",
TrueRandomOpponent = "trueRandomOpponent"
@ -26,11 +27,13 @@ const Effect = z.object({
damage: z.optional(z.number().int()),
heal: z.optional(z.number().int()),
cure: z.optional(z.boolean()),
dispel: z.optional(z.boolean()),
poison: z.optional(z.number().int()),
regeneration: z.optional(z.number().int()),
burn: z.optional(z.number().int()),
confusion: z.optional(z.number().int()),
stun: z.optional(z.number().int()),
taunt: z.optional(z.number().int()),
resistanceChange: z.optional(PotencyStatus),
accuracyChange: 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 you = await client.findOrCreatePlayer(int.user.id);
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({
content: 'gottem'