gotchi-battler-game-logic 2.0.7 → 3.0.0

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 (67) hide show
  1. package/.vscode/settings.json +4 -4
  2. package/Dockerfile +9 -9
  3. package/README.md +49 -49
  4. package/cloudbuild.yaml +27 -27
  5. package/constants/tournamentManagerAbi.json +208 -208
  6. package/game-logic/index.js +7 -6
  7. package/game-logic/v1.4/constants.js +114 -114
  8. package/game-logic/v1.4/index.js +1366 -1366
  9. package/game-logic/v1.6/constants.js +123 -123
  10. package/game-logic/v1.6/index.js +1406 -1406
  11. package/game-logic/v1.7/constants.js +142 -140
  12. package/game-logic/v1.7/helpers.js +610 -593
  13. package/game-logic/v1.7/index.js +802 -795
  14. package/game-logic/v1.8/constants.js +135 -0
  15. package/game-logic/v1.8/helpers.js +628 -0
  16. package/game-logic/v1.8/index.js +826 -0
  17. package/index.js +12 -12
  18. package/package.json +26 -26
  19. package/schemas/team.json +349 -343
  20. package/scripts/balancing/createCSV.js +126 -126
  21. package/scripts/balancing/createTrainingGotchis.js +267 -0
  22. package/scripts/balancing/extractOnchainTraits.js +61 -0
  23. package/scripts/balancing/fixTrainingGotchis.js +155 -259
  24. package/scripts/balancing/processSims.js +229 -229
  25. package/scripts/balancing/sims.js +278 -278
  26. package/scripts/balancing/v1.7/class_combos.js +43 -43
  27. package/scripts/balancing/v1.7/mapGotchi.js +119 -0
  28. package/scripts/balancing/v1.7/setTeamPositions.js +105 -105
  29. package/scripts/balancing/v1.7/training_gotchis.json +20161 -20161
  30. package/scripts/balancing/v1.7/training_gotchis_traits.json +520 -0
  31. package/scripts/balancing/v1.7/trait_combos.json +9 -9
  32. package/scripts/balancing/v1.7.1/class_combos.js +43 -43
  33. package/scripts/balancing/v1.7.1/mapGotchi.js +119 -0
  34. package/scripts/balancing/v1.7.1/setTeamPositions.js +122 -122
  35. package/scripts/balancing/v1.7.1/training_gotchis.json +22401 -22401
  36. package/scripts/balancing/v1.7.1/training_gotchis_traits.json +520 -0
  37. package/scripts/balancing/v1.7.1/trait_combos.json +9 -9
  38. package/scripts/balancing/v1.7.2/class_combos.js +44 -0
  39. package/scripts/balancing/v1.7.2/mapGotchi.js +157 -0
  40. package/scripts/balancing/v1.7.2/setTeamPositions.js +122 -0
  41. package/scripts/balancing/v1.7.2/training_gotchis.json +22402 -0
  42. package/scripts/balancing/v1.7.2/training_gotchis_traits.json +520 -0
  43. package/scripts/balancing/v1.7.2/trait_combos.json +10 -0
  44. package/scripts/balancing/v1.7.3/class_combos.js +44 -0
  45. package/scripts/balancing/v1.7.3/mapGotchi.js +164 -0
  46. package/scripts/balancing/v1.7.3/setTeamPositions.js +122 -0
  47. package/scripts/balancing/v1.7.3/training_gotchis.json +22402 -0
  48. package/scripts/balancing/v1.7.3/training_gotchis_traits.json +37 -0
  49. package/scripts/balancing/v1.7.3/trait_combos.json +10 -0
  50. package/scripts/data/team1.json +213 -213
  51. package/scripts/data/team2.json +200 -200
  52. package/scripts/data/tournaments.json +71 -66
  53. package/scripts/{runBattle.js → runLocalBattle.js} +18 -18
  54. package/scripts/runRealBattle.js +52 -0
  55. package/scripts/simRealBattle.js +121 -0
  56. package/scripts/validateBattle.js +83 -70
  57. package/scripts/validateTournament.js +101 -101
  58. package/utils/contracts.js +12 -12
  59. package/utils/errors.js +29 -29
  60. package/utils/mapGotchi.js +119 -0
  61. package/utils/transforms.js +89 -88
  62. package/utils/validations.js +39 -39
  63. package/debug.log +0 -2
  64. package/game-logic/v1.6/debug.log +0 -1
  65. package/game-logic/v1.7/debug.log +0 -3
  66. package/scripts/data/debug.log +0 -2
  67. package/scripts/debug.log +0 -1
@@ -0,0 +1,826 @@
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
+ // Calculate what the multiplier should be if the attack is a crit
205
+ let critMultiplier = options.multiplier
206
+ if (options.critMultiplier) {
207
+ critMultiplier *= options.critMultiplier
208
+ } else {
209
+ // Apply different crit multipliers for -nrg and +nrg gotchis
210
+ if (attackingGotchi.speed <= 100) {
211
+ critMultiplier *= MULTS.CRIT_MULTIPLIER_SLOW
212
+ } else {
213
+ critMultiplier *= MULTS.CRIT_MULTIPLIER_FAST
214
+ }
215
+ }
216
+
217
+ defendingTargets.forEach((defendingGotchi) => {
218
+ // Check attacking gotchi hasn't been killed by a counter
219
+ if (attackingGotchi.health <= 0) return
220
+
221
+ const modifiedAttackingGotchi = getModifiedStats(attackingGotchi)
222
+ const modifiedDefendingGotchi = getModifiedStats(defendingGotchi)
223
+
224
+ // Check for crit
225
+ let damageMultiplier = options.multiplier
226
+ const isCrit = rng() < modifiedAttackingGotchi.crit / 100
227
+ if (isCrit) {
228
+ damageMultiplier = critMultiplier
229
+ }
230
+
231
+ let canEvade = true
232
+ if (options.cannotBeEvaded) canEvade = false
233
+ if (isCrit && options.critCannotBeEvaded) canEvade = false
234
+
235
+ const damage = getDamage(attackingTeam, defendingTeam, attackingGotchi, defendingGotchi, damageMultiplier, options.ignoreArmor, options.speedPenalty)
236
+
237
+ let effect = {
238
+ target: defendingGotchi.id,
239
+ }
240
+
241
+ // Check for miss
242
+ if (rng() > modifiedAttackingGotchi.accuracy / 100) {
243
+ effect.outcome = 'miss'
244
+ effects.push(effect)
245
+ } else if (canEvade && rng() < modifiedDefendingGotchi.evade / 100){
246
+ effect.outcome = 'evade'
247
+ effects.push(effect)
248
+ } else {
249
+ if (!options.cannotBeResisted) {
250
+ // Check for status effect from the move
251
+ options.statuses.forEach((status) => {
252
+ if (rng() > modifiedDefendingGotchi.resist / 100) {
253
+ // Attempt to add status to defending gotchi
254
+ if (addStatusToGotchi(defendingGotchi, status)) {
255
+ // If status added, add to effect
256
+ if (!effect.statuses) {
257
+ effect.statuses = [status]
258
+ } else {
259
+ effect.statuses.push(status)
260
+ }
261
+ }
262
+ }
263
+ })
264
+ }
265
+
266
+ // Handle damage
267
+ defendingGotchi.health -= damage
268
+ effect.damage = damage
269
+ effect.outcome = isCrit ? 'critical' : 'success'
270
+ effects.push(effect)
271
+
272
+ // Check for counter attack
273
+ if (
274
+ defendingGotchi.statuses.includes('taunt')
275
+ && defendingGotchi.health > 0
276
+ && !options.cannotBeCountered) {
277
+
278
+ // Chance to counter based on speed over 100
279
+ let chanceToCounter = Math.round((modifiedDefendingGotchi.speed - 100) * MULTS.COUNTER_SPEED_MULTIPLIER)
280
+
281
+ // Add chance if gotchi has fortify status
282
+ if (defendingGotchi.statuses.includes('fortify')) {
283
+ chanceToCounter += MULTS.FORTIFY_COUNTER_CHANCE
284
+ }
285
+
286
+ if (rng() < chanceToCounter / 100) {
287
+ const counterDamageMultiplier = defendingGotchi.statuses.includes('fortify') ? MULTS.FORTIFY_COUNTER_DAMAGE : MULTS.COUNTER_DAMAGE
288
+ const counterDamage = getDamage(defendingTeam, attackingTeam, defendingGotchi, attackingGotchi, counterDamageMultiplier, false, 0)
289
+
290
+ attackingGotchi.health -= counterDamage
291
+
292
+ effects.push({
293
+ target: attackingGotchi.id,
294
+ source: defendingGotchi.id,
295
+ damage: counterDamage,
296
+ outcome: 'counter'
297
+ })
298
+ }
299
+ }
300
+ }
301
+ })
302
+
303
+ return effects
304
+ }
305
+
306
+ // Deal with start of turn status effects
307
+ const handleStatusEffects = (attackingGotchi, attackingTeam, defendingTeam, rng) => {
308
+ const statusEffects = []
309
+ const passiveEffects = []
310
+
311
+ const modifiedAttackingGotchi = getModifiedStats(attackingGotchi)
312
+
313
+ // Check for global status effects
314
+ const allAliveGotchis = [...getAlive(attackingTeam), ...getAlive(defendingTeam)]
315
+
316
+ allAliveGotchis.forEach((gotchi) => {
317
+ const modifiedGotchi = getModifiedStats(gotchi)
318
+ if (gotchi.statuses && gotchi.statuses.length) {
319
+ gotchi.statuses.forEach((status) => {
320
+ // Handle cleansing_aura (health regen)
321
+ if (status === 'cleansing_aura') {
322
+ let amountToHeal
323
+
324
+ // Check if healer
325
+ if (gotchi.special.id === 6) {
326
+ amountToHeal = Math.round(modifiedGotchi.resist * MULTS.CLEANSING_AURA_REGEN)
327
+ } else {
328
+ amountToHeal = Math.round(modifiedGotchi.resist * MULTS.CLEANSING_AURA_NON_HEALER_REGEN)
329
+ }
330
+
331
+ // Don't allow amountToHeal to be more than the difference between current health and max health
332
+ if (amountToHeal > gotchi.originalStats.health - gotchi.health) {
333
+ amountToHeal = gotchi.originalStats.health - gotchi.health
334
+ }
335
+
336
+ // if amountToHeal > 0, add status effect
337
+ if (amountToHeal) {
338
+ // Add status effect
339
+ statusEffects.push({
340
+ target: gotchi.id,
341
+ status,
342
+ damage: -Math.abs(amountToHeal),
343
+ remove: false
344
+ })
345
+
346
+ gotchi.health += amountToHeal
347
+ }
348
+ }
349
+
350
+ /*
351
+ * Handle damage effect at the bottom of the loop
352
+ */
353
+
354
+ // Handle bleed
355
+ if (status === 'bleed') {
356
+ let damage = MULTS.BLEED_DAMAGE
357
+
358
+ gotchi.health -= damage
359
+ if (gotchi.health <= 0) gotchi.health = 0
360
+
361
+ // Add status effect
362
+ statusEffects.push({
363
+ target: gotchi.id,
364
+ status,
365
+ damage,
366
+ remove: false
367
+ })
368
+ }
369
+ })
370
+ }
371
+ })
372
+
373
+ let skipTurn = null
374
+
375
+ // Check if gotchi is dead
376
+ if (attackingGotchi.health <= 0) {
377
+ return {
378
+ statusEffects,
379
+ passiveEffects,
380
+ skipTurn: 'ATTACKER_DEAD'
381
+ }
382
+ }
383
+
384
+ // Check if a whole team is dead
385
+ if (getAlive(attackingTeam).length === 0 || getAlive(defendingTeam).length === 0) {
386
+ return {
387
+ statusEffects,
388
+ passiveEffects,
389
+ skipTurn: 'TEAM_DEAD'
390
+ }
391
+ }
392
+
393
+ // Check for turn skipping statuses
394
+ for (let i = 0; i < attackingGotchi.statuses.length; i++) {
395
+ const status = attackingGotchi.statuses[i]
396
+ // Fear - skip turn
397
+ if (status === 'fear') {
398
+ // Skip turn
399
+ statusEffects.push({
400
+ target: attackingGotchi.id,
401
+ status,
402
+ damage: 0,
403
+ remove: true
404
+ })
405
+
406
+ skipTurn = 'FEAR'
407
+
408
+ // Remove fear first instance of fear
409
+ attackingGotchi.statuses.splice(i, 1)
410
+
411
+ break
412
+ }
413
+ }
414
+
415
+ return {
416
+ statusEffects,
417
+ passiveEffects,
418
+ skipTurn
419
+ }
420
+ }
421
+
422
+ const executeTurn = (team1, team2, rng) => {
423
+ const nextToAct = getNextToAct(team1, team2, rng)
424
+
425
+ const attackingTeam = nextToAct.team === 1 ? team1 : team2
426
+ const defendingTeam = nextToAct.team === 1 ? team2 : team1
427
+
428
+ const attackingGotchi = attackingTeam.formation[nextToAct.row][nextToAct.position]
429
+
430
+ let { statusEffects, passiveEffects, skipTurn } = handleStatusEffects(attackingGotchi, attackingTeam, defendingTeam, rng)
431
+ let statusesExpired = []
432
+
433
+ let effects = []
434
+ if (skipTurn) {
435
+ // Increase actionDelay
436
+ attackingGotchi.actionDelay = getNewActionDelay(attackingGotchi)
437
+
438
+ return {
439
+ skipTurn,
440
+ action: {
441
+ user: attackingGotchi.id,
442
+ name: 'auto',
443
+ effects
444
+ },
445
+ passiveEffects,
446
+ statusEffects,
447
+ statusesExpired
448
+ }
449
+ }
450
+
451
+ let specialDone = false
452
+ // Check if special attack is ready
453
+ if (attackingGotchi.special.cooldown === 0) {
454
+ // Execute special attack
455
+ const specialResults = specialAttack(attackingGotchi, attackingTeam, defendingTeam, rng)
456
+
457
+ if (specialResults.specialNotDone) {
458
+ // Do nothing which will lead to an auto attack
459
+ } else {
460
+ specialDone = true
461
+
462
+ effects = specialResults.effects
463
+ statusesExpired = specialResults.statusesExpired
464
+
465
+ // Reset cooldown
466
+ attackingGotchi.special.cooldown = 2
467
+ }
468
+
469
+ } else {
470
+ // Decrease cooldown
471
+ attackingGotchi.special.cooldown--
472
+ }
473
+
474
+ if (!specialDone) {
475
+ // Do an auto attack
476
+ const target = getTarget(defendingTeam, rng)
477
+
478
+ effects = attack(attackingGotchi, attackingTeam, defendingTeam, [target], rng)
479
+ }
480
+
481
+ // Increase actionDelay
482
+ attackingGotchi.actionDelay = getNewActionDelay(attackingGotchi)
483
+
484
+ return {
485
+ skipTurn,
486
+ action: {
487
+ user: attackingGotchi.id,
488
+ name: specialDone ? attackingGotchi.special.name : 'auto',
489
+ effects
490
+ },
491
+ passiveEffects,
492
+ statusEffects,
493
+ statusesExpired
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Execute a special attack
499
+ * @param {Object} attackingGotchi The attacking gotchi object
500
+ * @param {Array} attackingTeam An array of gotchis to attack
501
+ * @param {Array} defendingTeam An array of gotchis to attack
502
+ * @param {Function} rng The random number generator
503
+ * @returns {Array} effects An array of effects to apply
504
+ **/
505
+ const specialAttack = (attackingGotchi, attackingTeam, defendingTeam, rng) => {
506
+ const specialId = attackingGotchi.special.id
507
+ let effects = []
508
+ let statusesExpired = []
509
+ let specialNotDone = false
510
+
511
+ const modifiedAttackingGotchi = getModifiedStats(attackingGotchi)
512
+
513
+ switch (specialId) {
514
+ case 1:
515
+ // Spectral Strike - ignore armor and appply bleed status
516
+ // get single target
517
+ const ssTarget = getTarget(defendingTeam, rng)
518
+
519
+ let statuses = ['bleed']
520
+
521
+ // Add another bleed if gotchi has 'sharp_blades' status
522
+ if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
523
+ statuses.push('bleed')
524
+ }
525
+
526
+ effects = attack(attackingGotchi, attackingTeam, defendingTeam, [ssTarget], rng, {
527
+ multiplier: MULTS.SPECTRAL_STRIKE_DAMAGE,
528
+ ignoreArmor: true,
529
+ statuses,
530
+ cannotBeCountered: true,
531
+ cannotBeEvaded: true,
532
+ noPassiveStatuses: true,
533
+ noResistSpeedPenalty: true
534
+ })
535
+ break
536
+ case 2:
537
+ // Meditate - Boost own speed, magic, physical by 30%
538
+
539
+ // Check if gotchi already has power_up_2 status
540
+ if (attackingGotchi.statuses.includes('power_up_2')) {
541
+ specialNotDone = true
542
+ break
543
+ }
544
+
545
+ if (!addStatusToGotchi(attackingGotchi, 'power_up_2')) {
546
+ specialNotDone = true
547
+ break
548
+ }
549
+
550
+ effects = [
551
+ {
552
+ target: attackingGotchi.id,
553
+ outcome: 'success',
554
+ statuses: ['power_up_2']
555
+ }
556
+ ]
557
+ break
558
+ case 3:
559
+ // Cleave - attack all enemies in a row (that have the most gotchis) for 75% damage
560
+ // Find row with most gotchis
561
+ const cleaveRow = getAlive(defendingTeam, 'front').length > getAlive(defendingTeam, 'back').length ? 'front' : 'back'
562
+
563
+ // Attack all gotchis in that row for 75% damage
564
+ effects = attack(attackingGotchi, attackingTeam, defendingTeam, getAlive(defendingTeam, cleaveRow), rng, {
565
+ multiplier: MULTS.CLEAVE_DAMAGE,
566
+ cannotBeCountered: true,
567
+ noPassiveStatuses: true
568
+ })
569
+ break
570
+ case 4:
571
+ // Taunt - add taunt status to self
572
+
573
+ // Check if gotchi already has taunt status
574
+ if (attackingGotchi.statuses.includes('taunt')) {
575
+ specialNotDone = true
576
+ break
577
+ }
578
+
579
+ if (!addStatusToGotchi(attackingGotchi, 'taunt')) {
580
+ specialNotDone = true
581
+ break
582
+ }
583
+
584
+ effects = [
585
+ {
586
+ target: attackingGotchi.id,
587
+ outcome: 'success',
588
+ statuses: ['taunt']
589
+ }
590
+ ]
591
+ break
592
+ case 5:
593
+ // Curse - attack random enemy for 50% damage, apply fear status and remove all buffs
594
+
595
+ const curseTarget = getTarget(defendingTeam, rng)
596
+
597
+ const curseTargetStatuses = ['fear']
598
+ let curseMultiplier = MULTS.CURSE_DAMAGE
599
+
600
+ // Check if leader passive is 'spread_the_fear' then apply fear status
601
+ if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
602
+ curseTargetStatuses.push('fear')
603
+ curseMultiplier = MULTS.SPREAD_THE_FEAR_CURSE_DAMAGE
604
+ }
605
+
606
+ effects = attack(attackingGotchi, attackingTeam, defendingTeam, [curseTarget], rng, {
607
+ multiplier: curseMultiplier,
608
+ statuses: curseTargetStatuses,
609
+ cannotBeCountered: true,
610
+ noPassiveStatuses: true,
611
+ speedPenalty: MULTS.CURSE_SPEED_PENALTY,
612
+ noResistSpeedPenalty: true
613
+ })
614
+
615
+ const removeRandomBuff = (target) => {
616
+ const modifiedTarget = getModifiedStats(target)
617
+
618
+ if (rng() > modifiedTarget.resist / 100) {
619
+ const buffsToRemove = target.statuses.filter((status) => BUFFS.includes(status))
620
+
621
+ if (buffsToRemove.length) {
622
+ const randomBuff = buffsToRemove[Math.floor(rng() * buffsToRemove.length)]
623
+ statusesExpired.push({
624
+ target: target.id,
625
+ status: randomBuff
626
+ })
627
+
628
+ // Remove first instance of randomBuff (there may be multiple)
629
+ const index = target.statuses.indexOf(randomBuff)
630
+ target.statuses.splice(index, 1)
631
+ }
632
+ }
633
+ }
634
+
635
+ if (effects[0] && (effects[0].outcome === 'success' || effects[0].outcome === 'critical')) {
636
+ // 1 chance to remove a random buff
637
+ removeRandomBuff(curseTarget)
638
+
639
+ // Add another chance if crit
640
+ if (effects[0].outcome === 'critical') {
641
+ removeRandomBuff(curseTarget)
642
+ }
643
+
644
+ // Add another chance if 'spread_the_fear' status
645
+ if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
646
+ removeRandomBuff(curseTarget)
647
+ }
648
+
649
+ // heal attacking gotchi for % of damage dealt
650
+ let amountToHeal = Math.round(effects[0].damage * MULTS.CURSE_HEAL)
651
+
652
+ // Don't allow amountToHeal to be more than the difference between current health and max health
653
+ if (amountToHeal > attackingGotchi.originalStats.health - attackingGotchi.health) {
654
+ amountToHeal = attackingGotchi.originalStats.health - attackingGotchi.health
655
+ }
656
+
657
+ if (amountToHeal) {
658
+ attackingGotchi.health += amountToHeal
659
+
660
+ effects.push({
661
+ target: attackingGotchi.id,
662
+ outcome: effects[0].outcome,
663
+ damage: -Math.abs(amountToHeal)
664
+ })
665
+ }
666
+ }
667
+
668
+ break
669
+ case 6:
670
+ // Blessing - Heal all non-healer allies and remove all debuffs
671
+
672
+ // Get all alive non-healer allies on the attacking team
673
+ // const gotchisToHeal = getAlive(attackingTeam).filter(x => x.special.id !== 6)
674
+ const gotchisToHeal = getAlive(attackingTeam)
675
+
676
+ // Heal all allies for multiple of healers resistance
677
+ gotchisToHeal.forEach((gotchi) => {
678
+ let amountToHeal
679
+
680
+ // If gotchi has 'cleansing_aura' status, increase heal amount
681
+ if (attackingGotchi.statuses.includes('cleansing_aura')) {
682
+ amountToHeal = Math.round(modifiedAttackingGotchi.resist * MULTS.CLEANSING_AURA_HEAL)
683
+ } else {
684
+ amountToHeal = Math.round(modifiedAttackingGotchi.resist * MULTS.BLESSING_HEAL)
685
+ }
686
+
687
+ // Check for crit
688
+ const isCrit = rng() < modifiedAttackingGotchi.crit / 100
689
+ if (isCrit) {
690
+ amountToHeal = Math.round(amountToHeal * MULTS.BLESSING_HEAL_CRIT_MULTIPLIER)
691
+ }
692
+
693
+ // Apply speed penalty
694
+ let speedPenalty
695
+ if (attackingGotchi.statuses.includes('cleansing_aura')) {
696
+ speedPenalty = Math.round((modifiedAttackingGotchi.speed - 100) * MULTS.CLEANSING_AURA_HEAL_SPEED_PENALTY)
697
+ } else {
698
+ speedPenalty = Math.round((modifiedAttackingGotchi.speed - 100) * MULTS.BLESSING_HEAL_SPEED_PENALTY)
699
+ }
700
+ if (speedPenalty > 0) amountToHeal -= speedPenalty
701
+
702
+ // Don't allow amountToHeal to be more than the difference between current health and max health
703
+ if (amountToHeal > gotchi.originalStats.health - gotchi.health) {
704
+ amountToHeal = gotchi.originalStats.health - gotchi.health
705
+ }
706
+
707
+ gotchi.health += amountToHeal
708
+
709
+ if (amountToHeal) {
710
+ effects.push({
711
+ target: gotchi.id,
712
+ outcome: isCrit ? 'critical' : 'success',
713
+ damage: -Math.abs(amountToHeal)
714
+ })
715
+ }
716
+
717
+ // if gotchi has 'cleansing_aura' status, remove all debuffs
718
+ if (attackingGotchi.statuses.includes('cleansing_aura')) {
719
+ // Remove all debuffs
720
+ gotchi.statuses = gotchi.statuses.filter((status) => !DEBUFFS.includes(status))
721
+
722
+ // Add removed debuffs to statusesExpired
723
+ gotchi.statuses.forEach((status) => {
724
+ if (DEBUFFS.includes(status)) {
725
+ statusesExpired.push({
726
+ target: gotchi.id,
727
+ status
728
+ })
729
+ }
730
+ })
731
+ } else {
732
+ // Remove 1 random debuff
733
+ const debuffs = gotchi.statuses.filter((status) => DEBUFFS.includes(status))
734
+
735
+ if (debuffs.length) {
736
+ const randomDebuff = debuffs[Math.floor(rng() * debuffs.length)]
737
+ statusesExpired.push({
738
+ target: gotchi.id,
739
+ status: randomDebuff
740
+ })
741
+
742
+ // Remove first instance of randomDebuff (there may be multiple)
743
+ const index = gotchi.statuses.indexOf(randomDebuff)
744
+ gotchi.statuses.splice(index, 1)
745
+ }
746
+ }
747
+
748
+ })
749
+
750
+ // If no allies have been healed and no debuffs removed, then special attack not done
751
+ if (!effects.length && !statusesExpired.length) {
752
+ specialNotDone = true
753
+ break
754
+ }
755
+
756
+ break
757
+ case 7:
758
+ // Thunder - Attack all enemies for 50% damage and apply stun status
759
+
760
+ const thunderTargets = getAlive(defendingTeam)
761
+
762
+ let stunStatuses = []
763
+ // Check if leader passive is 'channel_the_coven' then apply stun status
764
+ if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
765
+ if (rng() < MULTS.CHANNEL_THE_COVEN_STUN_CHANCE) stunStatuses.push('stun')
766
+ } else {
767
+ if (rng() < MULTS.THUNDER_STUN_CHANCE) stunStatuses.push('stun')
768
+ }
769
+
770
+ effects = attack(attackingGotchi, attackingTeam, defendingTeam, thunderTargets, rng, {
771
+ multiplier: MULTS.THUNDER_DAMAGE,
772
+ statuses: stunStatuses,
773
+ cannotBeCountered: true,
774
+ noPassiveStatuses: true
775
+ })
776
+
777
+ break
778
+ case 8:
779
+ // Devestating Smash - Attack random enemy for 200% damage
780
+
781
+ const smashTarget = getTarget(defendingTeam, rng)
782
+
783
+ effects = attack(attackingGotchi, attackingTeam, defendingTeam, [smashTarget], rng, {
784
+ multiplier: MULTS.DEVESTATING_SMASH_DAMAGE,
785
+ cannotBeCountered: true,
786
+ noPassiveStatuses: true
787
+ })
788
+
789
+ let anotherAttack = false
790
+ if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
791
+ if (rng() < MULTS.CLAN_MOMENTUM_CHANCE) anotherAttack = true
792
+ } else {
793
+ if (rng() < MULTS.DEVESTATING_SMASH_X2_CHANCE) anotherAttack = true
794
+ }
795
+
796
+ if (anotherAttack) {
797
+ // Check if any enemies are alive
798
+ const aliveEnemies = getAlive(defendingTeam)
799
+
800
+ if (aliveEnemies.length) {
801
+ // Do an extra devestating smash
802
+ const target = getTarget(defendingTeam, rng)
803
+
804
+ effects.push(...attack(attackingGotchi, attackingTeam, defendingTeam, [target], rng, {
805
+ multiplier: MULTS.DEVESTATING_SMASH_X2_DAMAGE,
806
+ cannotBeCountered: true,
807
+ noPassiveStatuses: true
808
+ }))
809
+ }
810
+ }
811
+
812
+ break
813
+ }
814
+
815
+ return {
816
+ effects,
817
+ statusesExpired,
818
+ specialNotDone
819
+ }
820
+ }
821
+
822
+ module.exports = {
823
+ gameLoop,
824
+ attack,
825
+ specialAttack
826
+ }