gotchi-battler-game-logic 1.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/.env.example +1 -0
  2. package/.vscode/settings.json +4 -4
  3. package/Dockerfile +10 -0
  4. package/README.md +49 -49
  5. package/cloudbuild.yaml +27 -0
  6. package/constants/tournamentManagerAbi.json +208 -208
  7. package/game-logic/index.js +6 -5
  8. package/game-logic/v1.4/constants.js +120 -120
  9. package/game-logic/v1.4/index.js +1366 -1353
  10. package/game-logic/v1.5/index.js +8 -8
  11. package/game-logic/v1.6/constants.js +129 -129
  12. package/game-logic/v1.6/index.js +1406 -1402
  13. package/game-logic/v1.7/constants.js +147 -0
  14. package/game-logic/v1.7/helpers.js +605 -0
  15. package/game-logic/v1.7/index.js +796 -0
  16. package/index.js +13 -6
  17. package/package.json +26 -22
  18. package/schemas/team.json +262 -203
  19. package/scripts/balancing/createCSV.js +126 -0
  20. package/scripts/balancing/fixTrainingGotchis.js +260 -0
  21. package/scripts/balancing/processSims.js +230 -0
  22. package/scripts/balancing/sims.js +278 -0
  23. package/scripts/balancing/v1.7/class_combos.js +44 -0
  24. package/scripts/balancing/v1.7/setTeamPositions.js +105 -0
  25. package/scripts/balancing/v1.7/training_gotchis.json +20162 -0
  26. package/scripts/balancing/v1.7/trait_combos.json +10 -0
  27. package/scripts/balancing/v1.7.1/class_combos.js +44 -0
  28. package/scripts/balancing/v1.7.1/setTeamPositions.js +122 -0
  29. package/scripts/balancing/v1.7.1/training_gotchis.json +22402 -0
  30. package/scripts/balancing/v1.7.1/trait_combos.json +10 -0
  31. package/scripts/data/team1.json +213 -200
  32. package/scripts/data/team2.json +200 -200
  33. package/scripts/data/tournaments.json +66 -66
  34. package/scripts/runBattle.js +18 -16
  35. package/scripts/validateBattle.js +70 -64
  36. package/scripts/validateTournament.js +101 -101
  37. package/utils/contracts.js +12 -12
  38. package/utils/errors.js +29 -29
  39. package/utils/transforms.js +88 -47
  40. package/utils/validations.js +39 -39
@@ -0,0 +1,796 @@
1
+ const seedrandom = require('seedrandom')
2
+ const ZSchema = require('z-schema')
3
+ const validator = new ZSchema()
4
+ const teamSchema = require('../../schemas/team.json')
5
+
6
+ const { GameError } = require('../../utils/errors')
7
+
8
+ const {
9
+ PASSIVES,
10
+ DEBUFFS,
11
+ BUFFS,
12
+ MULTS
13
+ } = require('./constants')
14
+
15
+ const {
16
+ getAlive,
17
+ getNextToAct,
18
+ getTarget,
19
+ getDamage,
20
+ getModifiedStats,
21
+ getNewActionDelay,
22
+ simplifyTeam,
23
+ getExpiredStatuses,
24
+ addStatusToGotchi,
25
+ prepareTeams,
26
+ getLogGotchis
27
+ } = require('./helpers')
28
+
29
+ /**
30
+ * Run a battle between two teams
31
+ * @param {Object} team1 An in-game team object
32
+ * @param {Object} team2 An in-game team object
33
+ * @param {String} seed A seed for the random number generator
34
+ * @param {Boolean} debug A boolean to determine if the logs should include debug information
35
+ * @returns {Object} logs The battle logs
36
+ */
37
+ const gameLoop = (team1, team2, seed, debug) => {
38
+ if (!team1) throw new Error("Team 1 not found")
39
+ if (!team2) throw new Error("Team 2 not found")
40
+ if (!seed) throw new Error("Seed not found")
41
+
42
+ // Validate team objects
43
+ const team1Validation = validator.validate(team1, teamSchema)
44
+ if (!team1Validation) {
45
+ console.error('Team 1 validation failed: ', JSON.stringify(validator.getLastErrors(), null, 2))
46
+ throw new Error(`Team 1 validation failed`)
47
+ }
48
+ const team2Validation = validator.validate(team2, teamSchema)
49
+ if (!team2Validation) {
50
+ console.error('Team 2 validation failed: ', JSON.stringify(validator.getLastErrors(), null, 2))
51
+ throw new Error(`Team 2 validation failed`)
52
+ }
53
+
54
+ // Make deep copy of team objects to avoid modifying the original objects
55
+ team1 = JSON.parse(JSON.stringify(team1))
56
+ team2 = JSON.parse(JSON.stringify(team2))
57
+
58
+ const rng = seedrandom(seed)
59
+
60
+ const allAliveGotchis = [...getAlive(team1), ...getAlive(team2)]
61
+
62
+ prepareTeams(allAliveGotchis, team1, team2)
63
+
64
+ const logs = {
65
+ gotchis: getLogGotchis(allAliveGotchis),
66
+ layout: {
67
+ teams: [
68
+ simplifyTeam(team1),
69
+ simplifyTeam(team2)
70
+ ]
71
+ },
72
+ turns: []
73
+ };
74
+
75
+ // Used for turn by turn health and status summaries
76
+ // Deleted if not in development or no errors
77
+ logs.debug = []
78
+
79
+ let turnCounter = 0
80
+ let draw = false
81
+
82
+ try {
83
+ while (getAlive(team1).length && getAlive(team2).length) {
84
+ // Check if turnCounter is ready for environment effects (99,149,199, etc)
85
+ let isEnvironmentTurn = [99, 149, 199, 249, 299].includes(turnCounter)
86
+ if (isEnvironmentTurn) {
87
+ allAliveGotchis.forEach(x => {
88
+ x.environmentEffects.push('damage_up')
89
+ })
90
+ }
91
+
92
+ const turnLogs = executeTurn(team1, team2, rng)
93
+
94
+ // Check if turnCounter is ready for environment effects (99,149,199, etc)
95
+ if (isEnvironmentTurn) turnLogs.environmentEffects = ['damage_up']
96
+
97
+ if (MULTS.EXPIRE_LEADERSKILL) {
98
+ turnLogs.statusesExpired = [...turnLogs.statusesExpired, ...getExpiredStatuses(team1, team2)]
99
+ }
100
+
101
+ logs.turns.push({index: turnCounter, ...turnLogs})
102
+
103
+ if (debug) {
104
+ logs.debug.push({
105
+ turn: turnCounter,
106
+ user: logs.turns[logs.turns.length - 1].action.user,
107
+ move: logs.turns[logs.turns.length - 1].action.name,
108
+ team1: getAlive(team1).map((x) => {
109
+ return `Id: ${x.id}, Name: ${x.name}, Health: ${x.health}, Statuses: ${x.statuses}`
110
+ }),
111
+ team2: getAlive(team2).map((x) => {
112
+ return `Id: ${x.id}, Name: ${x.name}, Health: ${x.health}, Statuses: ${x.statuses}`
113
+ })
114
+ })
115
+ }
116
+
117
+ turnCounter++
118
+ }
119
+ } catch (e) {
120
+ console.error(e)
121
+ throw new GameError('Game loop failed', logs)
122
+ }
123
+
124
+ if (draw) {
125
+ logs.result = {
126
+ winner: 0,
127
+ loser: 0,
128
+ winningTeam: [],
129
+ numOfTurns: logs.turns.length
130
+ }
131
+ } else {
132
+ logs.result = {
133
+ winner: getAlive(team1).length ? 1 : 2,
134
+ loser: getAlive(team1).length ? 2 : 1,
135
+ winningTeam: getAlive(team1).length ? getAlive(team1) : getAlive(team2),
136
+ numOfTurns: logs.turns.length
137
+ }
138
+
139
+ // trim winning team objects
140
+ logs.result.winningTeam = logs.result.winningTeam.map((gotchi) => {
141
+ return {
142
+ id: gotchi.id,
143
+ name: gotchi.name,
144
+ brs: gotchi.brs,
145
+ health: gotchi.health
146
+ }
147
+ })
148
+ }
149
+
150
+ if (!debug) delete logs.debug
151
+
152
+ return logs
153
+ }
154
+
155
+ /**
156
+ * Attack one or more gotchis. This mutates the defending gotchis health
157
+ * @param {Object} attackingGotchi The attacking gotchi object
158
+ * @param {Array} attackingTeam A team object for the attacking team
159
+ * @param {Array} defendingTeam A team object for the defending team
160
+ * @param {Array} defendingTargets An array of gotchis to attack
161
+ * @param {Function} rng The random number generator
162
+ * @param {Object} options An object of options
163
+ * @param {Boolean} options.ignoreArmor Ignore the defending gotchi's defense
164
+ * @param {Boolean} options.multiplier A multiplier to apply to the damage
165
+ * @param {Boolean} options.statuses An array of status effects to apply
166
+ * @param {Boolean} options.cannotBeEvaded A boolean to determine if the attack can be evaded
167
+ * @param {Boolean} options.cannotBeResisted A boolean to determine if the attack can be resisted
168
+ * @param {Boolean} options.cannotBeCountered A boolean to determine if the attack can be countered
169
+ * @param {Boolean} options.noPassiveStatuses A boolean to determine if passive statuses should be inflicted
170
+ * @param {Number} options.critMultiplier Override the crit multiplier
171
+ * @returns {Array} effects An array of effects to apply
172
+ */
173
+ const attack = (attackingGotchi, attackingTeam, defendingTeam, defendingTargets, rng, options) => {
174
+ if (!options) options = {}
175
+ if (!options.ignoreArmor) options.ignoreArmor = false
176
+ if (!options.multiplier) options.multiplier = 1
177
+ if (!options.statuses) options.statuses = []
178
+ if (!options.cannotBeEvaded) options.cannotBeEvaded = false
179
+ if (!options.critCannotBeEvaded) options.critCannotBeEvaded = false
180
+ if (!options.cannotBeResisted) options.cannotBeResisted = false
181
+ if (!options.cannotBeCountered) options.cannotBeCountered = false
182
+ if (!options.noPassiveStatuses) options.noPassiveStatuses = false
183
+ if (!options.speedPenalty) options.speedPenalty = 0
184
+ if (!options.noResistSpeedPenalty) options.noResistSpeedPenalty = false
185
+ if (!options.critMultiplier) options.critMultiplier = null
186
+
187
+ // If passive statuses are allowed then add leaderPassive status effects to attackingGotchi
188
+ if (!options.noPassiveStatuses) {
189
+ // If attacking gotchi has 'sharp_blades' status, add 'bleed' to statuses
190
+ if (attackingGotchi.statuses.includes('sharp_blades')) {
191
+ if (rng() < MULTS.SHARP_BLADES_BLEED_CHANCE) options.statuses.push('bleed')
192
+ }
193
+
194
+ // If attacking gotchi has 'spread_the_fear' status, add 'fear' to statuses
195
+ if (attackingGotchi.statuses.includes('spread_the_fear')) {
196
+ // Reduce the chance to spread the fear if attacking gotchi has speed over 100
197
+ const spreadTheFearChance = attackingGotchi.speed > 100 ? MULTS.SPREAD_THE_FEAR_CHANCE - MULTS.SPREAD_THE_FEAR_SPEED_PENALTY : MULTS.SPREAD_THE_FEAR_CHANCE
198
+ if (rng() < spreadTheFearChance) options.statuses.push('fear')
199
+ }
200
+ }
201
+
202
+ const effects = []
203
+
204
+ defendingTargets.forEach((defendingGotchi) => {
205
+ // Check attacking gotchi hasn't been killed by a counter
206
+ if (attackingGotchi.health <= 0) return
207
+
208
+ const modifiedAttackingGotchi = getModifiedStats(attackingGotchi)
209
+ const modifiedDefendingGotchi = getModifiedStats(defendingGotchi)
210
+
211
+ // Check for crit
212
+ const isCrit = rng() < modifiedAttackingGotchi.crit / 100
213
+ if (isCrit) {
214
+ if (options.critMultiplier) {
215
+ options.multiplier *= options.critMultiplier
216
+ } else {
217
+ // Apply different crit multipliers for -nrg and +nrg gotchis
218
+ if (attackingGotchi.speed <= 100) {
219
+ options.multiplier *= MULTS.CRIT_MULTIPLIER_SLOW
220
+ } else {
221
+ options.multiplier *= MULTS.CRIT_MULTIPLIER_FAST
222
+ }
223
+ }
224
+ }
225
+
226
+ let canEvade = true
227
+ if (options.cannotBeEvaded) canEvade = false
228
+ if (isCrit && options.critCannotBeEvaded) canEvade = false
229
+
230
+ const damage = getDamage(attackingTeam, defendingTeam, attackingGotchi, defendingGotchi, options.multiplier, options.ignoreArmor, options.speedPenalty)
231
+
232
+ let effect = {
233
+ target: defendingGotchi.id,
234
+ }
235
+
236
+ // Check for miss
237
+ if (rng() > modifiedAttackingGotchi.accuracy / 100) {
238
+ effect.outcome = 'miss'
239
+ effects.push(effect)
240
+ } else if (canEvade && rng() < modifiedDefendingGotchi.evade / 100){
241
+ effect.outcome = 'evade'
242
+ effects.push(effect)
243
+ } else {
244
+ if (!options.cannotBeResisted) {
245
+ // Check for status effect from the move
246
+ options.statuses.forEach((status) => {
247
+ if (rng() > modifiedDefendingGotchi.resist / 100) {
248
+ // Attempt to add status to defending gotchi
249
+ if (addStatusToGotchi(defendingGotchi, status)) {
250
+ // If status added, add to effect
251
+ if (!effect.statuses) {
252
+ effect.statuses = [status]
253
+ } else {
254
+ effect.statuses.push(status)
255
+ }
256
+ }
257
+ }
258
+ })
259
+ }
260
+
261
+ // Handle damage
262
+ defendingGotchi.health -= damage
263
+ effect.damage = damage
264
+ effect.outcome = isCrit ? 'critical' : 'success'
265
+ effects.push(effect)
266
+
267
+ // Check for counter attack
268
+ if (
269
+ defendingGotchi.statuses.includes('taunt')
270
+ && defendingGotchi.health > 0
271
+ && !options.cannotBeCountered) {
272
+
273
+ // Chance to counter based on speed over 100
274
+ let chanceToCounter = defendingGotchi.speed - 100
275
+
276
+ if (chanceToCounter < MULTS.COUNTER_CHANCE_MIN) chanceToCounter = MULTS.COUNTER_CHANCE_MIN
277
+
278
+ // Add chance if gotchi has fortify status
279
+ if (defendingGotchi.statuses.includes('fortify')) {
280
+ chanceToCounter += MULTS.FORTIFY_COUNTER_CHANCE
281
+ }
282
+
283
+ if (rng() < chanceToCounter / 100) {
284
+ const counterDamage = getDamage(defendingTeam, attackingTeam, defendingGotchi, attackingGotchi, MULTS.COUNTER_DAMAGE, false, 0)
285
+
286
+ attackingGotchi.health -= counterDamage
287
+
288
+ effects.push({
289
+ target: attackingGotchi.id,
290
+ source: defendingGotchi.id,
291
+ damage: counterDamage,
292
+ outcome: 'counter'
293
+ })
294
+ }
295
+ }
296
+ }
297
+ })
298
+
299
+ return effects
300
+ }
301
+
302
+ // Deal with start of turn status effects
303
+ const handleStatusEffects = (attackingGotchi, attackingTeam, defendingTeam, rng) => {
304
+ const statusEffects = []
305
+ const passiveEffects = []
306
+
307
+ const modifiedAttackingGotchi = getModifiedStats(attackingGotchi)
308
+
309
+ // Check for global status effects
310
+ const allAliveGotchis = [...getAlive(attackingTeam), ...getAlive(defendingTeam)]
311
+
312
+ allAliveGotchis.forEach((gotchi) => {
313
+ const modifiedGotchi = getModifiedStats(gotchi)
314
+ if (gotchi.statuses && gotchi.statuses.length) {
315
+ gotchi.statuses.forEach((status) => {
316
+ // Handle cleansing_aura (health regen)
317
+ if (status === 'cleansing_aura') {
318
+ let amountToHeal
319
+
320
+ // Check if healer
321
+ if (gotchi.special.id === 6) {
322
+ amountToHeal = Math.round(modifiedGotchi.resist * MULTS.CLEANSING_AURA_REGEN)
323
+ } else {
324
+ amountToHeal = MULTS.CLEANSING_AURA_NON_HEALER_REGEN
325
+ }
326
+
327
+ // Don't allow amountToHeal to be more than the difference between current health and max health
328
+ if (amountToHeal > gotchi.originalStats.health - gotchi.health) {
329
+ amountToHeal = gotchi.originalStats.health - gotchi.health
330
+ }
331
+
332
+ // if amountToHeal > 0, add status effect
333
+ if (amountToHeal) {
334
+ // Add status effect
335
+ statusEffects.push({
336
+ target: gotchi.id,
337
+ status,
338
+ damage: -Math.abs(amountToHeal),
339
+ remove: false
340
+ })
341
+
342
+ gotchi.health += amountToHeal
343
+ }
344
+ }
345
+
346
+ /*
347
+ * Handle damage effect at the bottom of the loop
348
+ */
349
+
350
+ // Handle bleed
351
+ if (status === 'bleed') {
352
+ let damage = MULTS.BLEED_DAMAGE
353
+
354
+ gotchi.health -= damage
355
+ if (gotchi.health <= 0) gotchi.health = 0
356
+
357
+ // Add status effect
358
+ statusEffects.push({
359
+ target: gotchi.id,
360
+ status,
361
+ damage,
362
+ remove: false
363
+ })
364
+ }
365
+ })
366
+ }
367
+ })
368
+
369
+ let skipTurn = null
370
+
371
+ // Check if gotchi is dead
372
+ if (attackingGotchi.health <= 0) {
373
+ return {
374
+ statusEffects,
375
+ passiveEffects,
376
+ skipTurn: 'ATTACKER_DEAD'
377
+ }
378
+ }
379
+
380
+ // Check if a whole team is dead
381
+ if (getAlive(attackingTeam).length === 0 || getAlive(defendingTeam).length === 0) {
382
+ return {
383
+ statusEffects,
384
+ passiveEffects,
385
+ skipTurn: 'TEAM_DEAD'
386
+ }
387
+ }
388
+
389
+ // Check for turn skipping statuses
390
+ for (let i = 0; i < attackingGotchi.statuses.length; i++) {
391
+ const status = attackingGotchi.statuses[i]
392
+ // Fear - skip turn
393
+ if (status === 'fear') {
394
+ // Skip turn
395
+ statusEffects.push({
396
+ target: attackingGotchi.id,
397
+ status,
398
+ damage: 0,
399
+ remove: true
400
+ })
401
+
402
+ skipTurn = 'FEAR'
403
+
404
+ // Remove fear first instance of fear
405
+ attackingGotchi.statuses.splice(i, 1)
406
+
407
+ break
408
+ }
409
+ }
410
+
411
+ return {
412
+ statusEffects,
413
+ passiveEffects,
414
+ skipTurn
415
+ }
416
+ }
417
+
418
+ const executeTurn = (team1, team2, rng) => {
419
+ const nextToAct = getNextToAct(team1, team2, rng)
420
+
421
+ const attackingTeam = nextToAct.team === 1 ? team1 : team2
422
+ const defendingTeam = nextToAct.team === 1 ? team2 : team1
423
+
424
+ const attackingGotchi = attackingTeam.formation[nextToAct.row][nextToAct.position]
425
+
426
+ let { statusEffects, passiveEffects, skipTurn } = handleStatusEffects(attackingGotchi, attackingTeam, defendingTeam, rng)
427
+ let statusesExpired = []
428
+
429
+ let effects = []
430
+ if (skipTurn) {
431
+ // Increase actionDelay
432
+ attackingGotchi.actionDelay = getNewActionDelay(attackingGotchi)
433
+
434
+ return {
435
+ skipTurn,
436
+ action: {
437
+ user: attackingGotchi.id,
438
+ name: 'auto',
439
+ effects
440
+ },
441
+ passiveEffects,
442
+ statusEffects,
443
+ statusesExpired
444
+ }
445
+ }
446
+
447
+ let specialDone = false
448
+ // Check if special attack is ready
449
+ if (attackingGotchi.special.cooldown === 0) {
450
+ // Execute special attack
451
+ const specialResults = specialAttack(attackingGotchi, attackingTeam, defendingTeam, rng)
452
+
453
+ if (specialResults.specialNotDone) {
454
+ // Do nothing which will lead to an auto attack
455
+ } else {
456
+ specialDone = true
457
+
458
+ effects = specialResults.effects
459
+ statusesExpired = specialResults.statusesExpired
460
+
461
+ // Reset cooldown
462
+ attackingGotchi.special.cooldown = 2
463
+ }
464
+
465
+ } else {
466
+ // Decrease cooldown
467
+ attackingGotchi.special.cooldown--
468
+ }
469
+
470
+ if (!specialDone) {
471
+ // Do an auto attack
472
+ const target = getTarget(defendingTeam, rng)
473
+
474
+ effects = attack(attackingGotchi, attackingTeam, defendingTeam, [target], rng)
475
+ }
476
+
477
+ // Increase actionDelay
478
+ attackingGotchi.actionDelay = getNewActionDelay(attackingGotchi)
479
+
480
+ return {
481
+ skipTurn,
482
+ action: {
483
+ user: attackingGotchi.id,
484
+ name: specialDone ? attackingGotchi.special.name : 'auto',
485
+ effects
486
+ },
487
+ passiveEffects,
488
+ statusEffects,
489
+ statusesExpired
490
+ }
491
+ }
492
+
493
+ /**
494
+ * Execute a special attack
495
+ * @param {Object} attackingGotchi The attacking gotchi object
496
+ * @param {Array} attackingTeam An array of gotchis to attack
497
+ * @param {Array} defendingTeam An array of gotchis to attack
498
+ * @param {Function} rng The random number generator
499
+ * @returns {Array} effects An array of effects to apply
500
+ **/
501
+ const specialAttack = (attackingGotchi, attackingTeam, defendingTeam, rng) => {
502
+ const specialId = attackingGotchi.special.id
503
+ let effects = []
504
+ let statusesExpired = []
505
+ let specialNotDone = false
506
+
507
+ const modifiedAttackingGotchi = getModifiedStats(attackingGotchi)
508
+
509
+ switch (specialId) {
510
+ case 1:
511
+ // Spectral Strike - ignore armor and appply bleed status
512
+ // get single target
513
+ const ssTarget = getTarget(defendingTeam, rng)
514
+
515
+ effects = attack(attackingGotchi, attackingTeam, defendingTeam, [ssTarget], rng, {
516
+ multiplier: MULTS.SPECTRAL_STRIKE_DAMAGE,
517
+ ignoreArmor: true,
518
+ statuses: ['bleed'],
519
+ cannotBeCountered: true,
520
+ cannotBeEvaded: true,
521
+ noPassiveStatuses: true,
522
+ noResistSpeedPenalty: true
523
+ })
524
+ break
525
+ case 2:
526
+ // Meditate - Boost own speed, magic, physical by 30%
527
+ // If gotchi already has 2 power_up statuses, do nothing
528
+ if (!addStatusToGotchi(attackingGotchi, 'power_up_2')) {
529
+ specialNotDone = true
530
+ break
531
+ }
532
+
533
+ effects = [
534
+ {
535
+ target: attackingGotchi.id,
536
+ outcome: 'success',
537
+ statuses: ['power_up_2']
538
+ }
539
+ ]
540
+
541
+ // Check for leaderPassive 'Cloud of Zen'
542
+ if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
543
+ // Increase allies speed, magic and physical by 15% of the original value
544
+
545
+ const cloudOfZenGotchis = getAlive(attackingTeam)
546
+
547
+ cloudOfZenGotchis.forEach((gotchi) => {
548
+ if (addStatusToGotchi(gotchi, 'power_up_1')) {
549
+ effects.push({
550
+ target: gotchi.id,
551
+ outcome: 'success',
552
+ statuses: ['power_up_1']
553
+ })
554
+ }
555
+ })
556
+ }
557
+
558
+ break
559
+ case 3:
560
+ // Cleave - attack all enemies in a row (that have the most gotchis) for 75% damage
561
+ // Find row with most gotchis
562
+ const cleaveRow = getAlive(defendingTeam, 'front').length > getAlive(defendingTeam, 'back').length ? 'front' : 'back'
563
+
564
+ // Attack all gotchis in that row for 75% damage
565
+ effects = attack(attackingGotchi, attackingTeam, defendingTeam, getAlive(defendingTeam, cleaveRow), rng, {
566
+ multiplier: MULTS.CLEAVE_DAMAGE,
567
+ cannotBeCountered: true,
568
+ noPassiveStatuses: true
569
+ })
570
+ break
571
+ case 4:
572
+ // Taunt - add taunt status to self
573
+
574
+ // Check if gotchi already has taunt status
575
+ if (attackingGotchi.statuses.includes('taunt')) {
576
+ specialNotDone = true
577
+ break
578
+ }
579
+
580
+ if (!addStatusToGotchi(attackingGotchi, 'taunt')) {
581
+ specialNotDone = true
582
+ break
583
+ }
584
+
585
+ effects = [
586
+ {
587
+ target: attackingGotchi.id,
588
+ outcome: 'success',
589
+ statuses: ['taunt']
590
+ }
591
+ ]
592
+ break
593
+ case 5:
594
+ // Curse - attack random enemy for 50% damage, apply fear status and remove all buffs
595
+
596
+ const curseTarget = getTarget(defendingTeam, rng)
597
+
598
+ const curseTargetStatuses = ['fear']
599
+
600
+ effects = attack(attackingGotchi, attackingTeam, defendingTeam, [curseTarget], rng, {
601
+ multiplier: MULTS.CURSE_DAMAGE,
602
+ statuses: curseTargetStatuses,
603
+ cannotBeCountered: true,
604
+ noPassiveStatuses: true,
605
+ speedPenalty: MULTS.CURSE_SPEED_PENALTY,
606
+ noResistSpeedPenalty: true
607
+ })
608
+
609
+ const removeRandomBuff = (target) => {
610
+ const modifiedTarget = getModifiedStats(target)
611
+
612
+ if (rng() > modifiedTarget.resist / 100) {
613
+ const buffsToRemove = target.statuses.filter((status) => BUFFS.includes(status))
614
+
615
+ if (buffsToRemove.length) {
616
+ const randomBuff = buffsToRemove[Math.floor(rng() * buffsToRemove.length)]
617
+ statusesExpired.push({
618
+ target: target.id,
619
+ status: randomBuff
620
+ })
621
+
622
+ // Remove first instance of randomBuff (there may be multiple)
623
+ const index = target.statuses.indexOf(randomBuff)
624
+ target.statuses.splice(index, 1)
625
+ }
626
+ }
627
+ }
628
+
629
+ if (effects[0] && (effects[0].outcome === 'success' || effects[0].outcome === 'critical')) {
630
+ // 1 chance to remove a random buff
631
+ removeRandomBuff(curseTarget)
632
+
633
+ if (effects[0].outcome === 'critical') {
634
+ // 2 chances to remove a random buff
635
+ removeRandomBuff(curseTarget)
636
+ }
637
+
638
+ // heal attacking gotchi for % of damage dealt
639
+ let amountToHeal = Math.round(effects[0].damage * MULTS.CURSE_HEAL)
640
+
641
+ // Don't allow amountToHeal to be more than the difference between current health and max health
642
+ if (amountToHeal > attackingGotchi.originalStats.health - attackingGotchi.health) {
643
+ amountToHeal = attackingGotchi.originalStats.health - attackingGotchi.health
644
+ }
645
+
646
+ if (amountToHeal) {
647
+ attackingGotchi.health += amountToHeal
648
+
649
+ effects.push({
650
+ target: attackingGotchi.id,
651
+ outcome: effects[0].outcome,
652
+ damage: -Math.abs(amountToHeal)
653
+ })
654
+ }
655
+ }
656
+
657
+ break
658
+ case 6:
659
+ // Blessing - Heal all non-healer allies and remove all debuffs
660
+
661
+ // Get all alive non-healer allies on the attacking team
662
+ // const gotchisToHeal = getAlive(attackingTeam).filter(x => x.special.id !== 6)
663
+ const gotchisToHeal = getAlive(attackingTeam)
664
+
665
+ // Heal all allies for multiple of healers resistance
666
+ gotchisToHeal.forEach((gotchi) => {
667
+ let amountToHeal
668
+
669
+ // If gotchi has 'cleansing_aura' status, increase heal amount
670
+ if (attackingGotchi.statuses.includes('cleansing_aura')) {
671
+ amountToHeal = Math.round(modifiedAttackingGotchi.resist * MULTS.CLEANSING_AURA_HEAL)
672
+ } else {
673
+ amountToHeal = Math.round(modifiedAttackingGotchi.resist * MULTS.BLESSING_HEAL)
674
+ }
675
+
676
+ // Check for crit
677
+ const isCrit = rng() < modifiedAttackingGotchi.crit / 100
678
+ if (isCrit) {
679
+ amountToHeal = Math.round(amountToHeal * MULTS.BLESSING_HEAL_CRIT_MULTIPLIER)
680
+ }
681
+
682
+ // Apply speed penalty
683
+ let speedPenalty
684
+ if (attackingGotchi.statuses.includes('cleansing_aura')) {
685
+ speedPenalty = Math.round((modifiedAttackingGotchi.speed - 100) * MULTS.CLEANSING_AURA_HEAL_SPEED_PENALTY)
686
+ } else {
687
+ speedPenalty = Math.round((modifiedAttackingGotchi.speed - 100) * MULTS.BLESSING_HEAL_SPEED_PENALTY)
688
+ }
689
+ if (speedPenalty > 0) amountToHeal -= speedPenalty
690
+
691
+ // Don't allow amountToHeal to be more than the difference between current health and max health
692
+ if (amountToHeal > gotchi.originalStats.health - gotchi.health) {
693
+ amountToHeal = gotchi.originalStats.health - gotchi.health
694
+ }
695
+
696
+ gotchi.health += amountToHeal
697
+
698
+ if (amountToHeal) {
699
+ effects.push({
700
+ target: gotchi.id,
701
+ outcome: isCrit ? 'critical' : 'success',
702
+ damage: -Math.abs(amountToHeal)
703
+ })
704
+ }
705
+
706
+ // Remove all debuffs
707
+ // Add removed debuffs to statusesExpired
708
+ gotchi.statuses.forEach((status) => {
709
+ if (DEBUFFS.includes(status)) {
710
+ statusesExpired.push({
711
+ target: gotchi.id,
712
+ status
713
+ })
714
+ }
715
+ })
716
+
717
+ // Remove all debuffs from gotchi
718
+ gotchi.statuses = gotchi.statuses.filter((status) => !DEBUFFS.includes(status))
719
+ })
720
+
721
+ // If no allies have been healed and no debuffs removed, then special attack not done
722
+ if (!effects.length && !statusesExpired.length) {
723
+ specialNotDone = true
724
+ break
725
+ }
726
+
727
+ break
728
+ case 7:
729
+ // Thunder - Attack all enemies for 50% damage and apply stun status
730
+
731
+ const thunderTargets = getAlive(defendingTeam)
732
+
733
+ let stunStatuses = []
734
+ // Check if leader passive is 'channel_the_coven' then apply stun status
735
+ if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
736
+ if (rng() < MULTS.CHANNEL_THE_COVEN_STUN_CHANCE) stunStatuses.push('stun')
737
+ } else {
738
+ if (rng() < MULTS.THUNDER_STUN_CHANCE) stunStatuses.push('stun')
739
+ }
740
+
741
+ effects = attack(attackingGotchi, attackingTeam, defendingTeam, thunderTargets, rng, {
742
+ multiplier: MULTS.THUNDER_DAMAGE,
743
+ statuses: stunStatuses,
744
+ cannotBeCountered: true,
745
+ noPassiveStatuses: true,
746
+ critMultiplier: MULTS.THUNDER_CRIT_MULTIPLIER
747
+ })
748
+
749
+ break
750
+ case 8:
751
+ // Devestating Smash - Attack random enemy for 200% damage
752
+
753
+ const smashTarget = getTarget(defendingTeam, rng)
754
+
755
+ effects = attack(attackingGotchi, attackingTeam, defendingTeam, [smashTarget], rng, {
756
+ multiplier: MULTS.DEVESTATING_SMASH_DAMAGE,
757
+ cannotBeCountered: true,
758
+ noPassiveStatuses: true
759
+ })
760
+
761
+ let anotherAttack = false
762
+ if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
763
+ if (rng() < MULTS.CLAN_MOMENTUM_CHANCE) anotherAttack = true
764
+ } else {
765
+ if (rng() < MULTS.DEVESTATING_SMASH_X2_CHANCE) anotherAttack = true
766
+ }
767
+
768
+ if (anotherAttack) {
769
+ // Check if any enemies are alive
770
+ const aliveEnemies = getAlive(defendingTeam)
771
+
772
+ if (aliveEnemies.length) {
773
+ // Do an extra devestating smash
774
+ const target = getTarget(defendingTeam, rng)
775
+
776
+ effects.push(...attack(attackingGotchi, attackingTeam, defendingTeam, [target], rng, {
777
+ multiplier: MULTS.DEVESTATING_SMASH_X2_DAMAGE,
778
+ cannotBeCountered: true,
779
+ noPassiveStatuses: true
780
+ }))
781
+ }
782
+ }
783
+
784
+ break
785
+ }
786
+
787
+ return {
788
+ effects,
789
+ statusesExpired,
790
+ specialNotDone
791
+ }
792
+ }
793
+
794
+ module.exports = {
795
+ gameLoop
796
+ }