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