battle logic
This commit is contained in:
parent
f257bd594b
commit
c41c5dca7c
269
src/battle.ts
269
src/battle.ts
@ -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>!`);
|
||||
}
|
||||
}
|
||||
}
|
@ -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),
|
||||
|
@ -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'
|
||||
|
Loading…
Reference in New Issue
Block a user