gotchi-battler-game-logic 2.0.8 → 4.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 (74) hide show
  1. package/.cursor/rules/cursor-rules.mdc +67 -0
  2. package/.cursor/rules/directory-structure.mdc +63 -0
  3. package/.cursor/rules/self-improvement.mdc +64 -0
  4. package/.cursor/rules/tech-stack.mdc +99 -0
  5. package/README.md +7 -3
  6. package/eslint.config.js +31 -0
  7. package/game-logic/index.js +2 -5
  8. package/game-logic/v1.4/constants.js +0 -23
  9. package/game-logic/v1.4/index.js +64 -56
  10. package/game-logic/v1.5/constants.js +0 -23
  11. package/game-logic/v1.5/index.js +27 -21
  12. package/game-logic/v1.6/constants.js +0 -23
  13. package/game-logic/v1.6/index.js +27 -21
  14. package/game-logic/v1.7/constants.js +0 -23
  15. package/game-logic/v1.7/helpers.js +18 -3
  16. package/game-logic/v1.7/index.js +24 -18
  17. package/game-logic/v1.8/constants.js +112 -0
  18. package/game-logic/v1.8/helpers.js +628 -0
  19. package/game-logic/v1.8/index.js +832 -0
  20. package/game-logic/v2.0/constants.js +112 -0
  21. package/game-logic/v2.0/helpers.js +713 -0
  22. package/game-logic/v2.0/index.js +782 -0
  23. package/game-logic/v2.0/statuses.json +439 -0
  24. package/package.json +11 -4
  25. package/schemas/crystal.js +14 -0
  26. package/schemas/effect.js +25 -0
  27. package/schemas/gotchi.js +53 -0
  28. package/schemas/ingameteam.js +14 -0
  29. package/schemas/item.js +13 -0
  30. package/schemas/leaderskill.js +15 -0
  31. package/schemas/leaderskillstatus.js +12 -0
  32. package/schemas/special.js +22 -0
  33. package/schemas/team.js +24 -0
  34. package/schemas/team.json +254 -116
  35. package/scripts/balancing/createCSV.js +1 -1
  36. package/scripts/balancing/createTrainingGotchis.js +267 -0
  37. package/scripts/balancing/extractOnchainTraits.js +61 -0
  38. package/scripts/balancing/fixTrainingGotchis.js +41 -41
  39. package/scripts/balancing/processSims.js +6 -6
  40. package/scripts/balancing/sims.js +10 -17
  41. package/scripts/balancing/v1.7/mapGotchi.js +119 -0
  42. package/scripts/balancing/v1.7/setTeamPositions.js +2 -2
  43. package/scripts/balancing/v1.7/training_gotchis_traits.json +520 -0
  44. package/scripts/balancing/v1.7.1/mapGotchi.js +119 -0
  45. package/scripts/balancing/v1.7.1/setTeamPositions.js +2 -2
  46. package/scripts/balancing/v1.7.1/training_gotchis_traits.json +520 -0
  47. package/scripts/balancing/v1.7.2/mapGotchi.js +157 -0
  48. package/scripts/balancing/v1.7.2/setTeamPositions.js +2 -2
  49. package/scripts/balancing/v1.7.2/training_gotchis_traits.json +520 -0
  50. package/scripts/balancing/v1.7.3/class_combos.js +44 -0
  51. package/scripts/balancing/v1.7.3/mapGotchi.js +164 -0
  52. package/scripts/balancing/v1.7.3/setTeamPositions.js +122 -0
  53. package/scripts/balancing/v1.7.3/training_gotchis.json +22402 -0
  54. package/scripts/balancing/v1.7.3/training_gotchis_traits.json +37 -0
  55. package/scripts/balancing/v1.7.3/trait_combos.json +10 -0
  56. package/scripts/data/dungeon_mob_1.json +87 -0
  57. package/scripts/data/dungeon_mob_2.json +87 -0
  58. package/scripts/data/immaterialTeam1.json +374 -0
  59. package/scripts/data/immaterialTeam2.json +365 -0
  60. package/scripts/data/tournaments.json +5 -0
  61. package/scripts/generateAllSpecialsLogs.js +93 -0
  62. package/scripts/generateSpecialLogs.js +94 -0
  63. package/scripts/runCampaignBattles.js +41 -0
  64. package/scripts/runLocalBattle.js +6 -3
  65. package/scripts/runLocalDungeon.js +52 -0
  66. package/scripts/runPvPBattle.js +16 -0
  67. package/scripts/runRealBattle.js +8 -8
  68. package/scripts/simRealBattle.js +8 -8
  69. package/scripts/validateBattle.js +23 -16
  70. package/scripts/validateTournament.js +9 -9
  71. package/tests/getModifiedStats.test.js +78 -0
  72. package/utils/errors.js +13 -13
  73. package/utils/transforms.js +2 -8
  74. package/scripts/output/.gitkeep +0 -0
@@ -0,0 +1,832 @@
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) => {
308
+ const statusEffects = []
309
+ const passiveEffects = []
310
+
311
+ // Check for global status effects
312
+ const allAliveGotchis = [...getAlive(attackingTeam), ...getAlive(defendingTeam)]
313
+
314
+ allAliveGotchis.forEach((gotchi) => {
315
+ const modifiedGotchi = getModifiedStats(gotchi)
316
+ if (gotchi.statuses && gotchi.statuses.length) {
317
+ gotchi.statuses.forEach((status) => {
318
+ // Handle cleansing_aura (health regen)
319
+ if (status === 'cleansing_aura') {
320
+ let amountToHeal
321
+
322
+ // Check if healer
323
+ if (gotchi.special.id === 6) {
324
+ amountToHeal = Math.round(modifiedGotchi.resist * MULTS.CLEANSING_AURA_REGEN)
325
+ } else {
326
+ amountToHeal = Math.round(modifiedGotchi.resist * MULTS.CLEANSING_AURA_NON_HEALER_REGEN)
327
+ }
328
+
329
+ // Don't allow amountToHeal to be more than the difference between current health and max health
330
+ if (amountToHeal > gotchi.originalStats.health - gotchi.health) {
331
+ amountToHeal = gotchi.originalStats.health - gotchi.health
332
+ }
333
+
334
+ // if amountToHeal > 0, add status effect
335
+ if (amountToHeal) {
336
+ // Add status effect
337
+ statusEffects.push({
338
+ target: gotchi.id,
339
+ status,
340
+ damage: -Math.abs(amountToHeal),
341
+ remove: false
342
+ })
343
+
344
+ gotchi.health += amountToHeal
345
+ }
346
+ }
347
+
348
+ /*
349
+ * Handle damage effect at the bottom of the loop
350
+ */
351
+
352
+ // Handle bleed
353
+ if (status === 'bleed') {
354
+ let damage = MULTS.BLEED_DAMAGE
355
+
356
+ gotchi.health -= damage
357
+ if (gotchi.health <= 0) gotchi.health = 0
358
+
359
+ // Add status effect
360
+ statusEffects.push({
361
+ target: gotchi.id,
362
+ status,
363
+ damage,
364
+ remove: false
365
+ })
366
+ }
367
+ })
368
+ }
369
+ })
370
+
371
+ let skipTurn = null
372
+
373
+ // Check if gotchi is dead
374
+ if (attackingGotchi.health <= 0) {
375
+ return {
376
+ statusEffects,
377
+ passiveEffects,
378
+ skipTurn: 'ATTACKER_DEAD'
379
+ }
380
+ }
381
+
382
+ // Check if a whole team is dead
383
+ if (getAlive(attackingTeam).length === 0 || getAlive(defendingTeam).length === 0) {
384
+ return {
385
+ statusEffects,
386
+ passiveEffects,
387
+ skipTurn: 'TEAM_DEAD'
388
+ }
389
+ }
390
+
391
+ // Check for turn skipping statuses
392
+ for (let i = 0; i < attackingGotchi.statuses.length; i++) {
393
+ const status = attackingGotchi.statuses[i]
394
+ // Fear - skip turn
395
+ if (status === 'fear') {
396
+ // Skip turn
397
+ statusEffects.push({
398
+ target: attackingGotchi.id,
399
+ status,
400
+ damage: 0,
401
+ remove: true
402
+ })
403
+
404
+ skipTurn = 'FEAR'
405
+
406
+ // Remove fear first instance of fear
407
+ attackingGotchi.statuses.splice(i, 1)
408
+
409
+ break
410
+ }
411
+ }
412
+
413
+ return {
414
+ statusEffects,
415
+ passiveEffects,
416
+ skipTurn
417
+ }
418
+ }
419
+
420
+ const executeTurn = (team1, team2, rng) => {
421
+ const nextToAct = getNextToAct(team1, team2, rng)
422
+
423
+ const attackingTeam = nextToAct.team === 1 ? team1 : team2
424
+ const defendingTeam = nextToAct.team === 1 ? team2 : team1
425
+
426
+ const attackingGotchi = attackingTeam.formation[nextToAct.row][nextToAct.position]
427
+
428
+ let { statusEffects, passiveEffects, skipTurn } = handleStatusEffects(attackingGotchi, attackingTeam, defendingTeam, rng)
429
+ let statusesExpired = []
430
+
431
+ let effects = []
432
+ if (skipTurn) {
433
+ // Increase actionDelay
434
+ attackingGotchi.actionDelay = getNewActionDelay(attackingGotchi)
435
+
436
+ return {
437
+ skipTurn,
438
+ action: {
439
+ user: attackingGotchi.id,
440
+ name: 'auto',
441
+ effects
442
+ },
443
+ passiveEffects,
444
+ statusEffects,
445
+ statusesExpired
446
+ }
447
+ }
448
+
449
+ let specialDone = false
450
+ // Check if special attack is ready
451
+ if (attackingGotchi.special.cooldown === 0) {
452
+ // Execute special attack
453
+ const specialResults = specialAttack(attackingGotchi, attackingTeam, defendingTeam, rng)
454
+
455
+ if (specialResults.specialNotDone) {
456
+ // Do nothing which will lead to an auto attack
457
+ } else {
458
+ specialDone = true
459
+
460
+ effects = specialResults.effects
461
+ statusesExpired = specialResults.statusesExpired
462
+
463
+ // Reset cooldown
464
+ attackingGotchi.special.cooldown = 2
465
+ }
466
+
467
+ } else {
468
+ // Decrease cooldown
469
+ attackingGotchi.special.cooldown--
470
+ }
471
+
472
+ if (!specialDone) {
473
+ // Do an auto attack
474
+ const target = getTarget(defendingTeam, rng)
475
+
476
+ effects = attack(attackingGotchi, attackingTeam, defendingTeam, [target], rng)
477
+ }
478
+
479
+ // Increase actionDelay
480
+ attackingGotchi.actionDelay = getNewActionDelay(attackingGotchi)
481
+
482
+ return {
483
+ skipTurn,
484
+ action: {
485
+ user: attackingGotchi.id,
486
+ name: specialDone ? attackingGotchi.special.name : 'auto',
487
+ effects
488
+ },
489
+ passiveEffects,
490
+ statusEffects,
491
+ statusesExpired
492
+ }
493
+ }
494
+
495
+ /**
496
+ * Execute a special attack
497
+ * @param {Object} attackingGotchi The attacking gotchi object
498
+ * @param {Array} attackingTeam An array of gotchis to attack
499
+ * @param {Array} defendingTeam An array of gotchis to attack
500
+ * @param {Function} rng The random number generator
501
+ * @returns {Array} effects An array of effects to apply
502
+ **/
503
+ const specialAttack = (attackingGotchi, attackingTeam, defendingTeam, rng) => {
504
+ const specialId = attackingGotchi.special.id
505
+ let effects = []
506
+ let statusesExpired = []
507
+ let specialNotDone = false
508
+
509
+ const modifiedAttackingGotchi = getModifiedStats(attackingGotchi)
510
+
511
+ switch (specialId) {
512
+ case 1: {
513
+ // Spectral Strike - ignore armor and appply bleed status
514
+ // get single target
515
+ const ssTarget = getTarget(defendingTeam, rng)
516
+
517
+ let statuses = ['bleed']
518
+
519
+ // Add another bleed if gotchi has 'sharp_blades' status
520
+ if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
521
+ statuses.push('bleed')
522
+ }
523
+
524
+ effects = attack(attackingGotchi, attackingTeam, defendingTeam, [ssTarget], rng, {
525
+ multiplier: MULTS.SPECTRAL_STRIKE_DAMAGE,
526
+ ignoreArmor: true,
527
+ statuses,
528
+ cannotBeCountered: true,
529
+ cannotBeEvaded: true,
530
+ noPassiveStatuses: true,
531
+ noResistSpeedPenalty: true
532
+ })
533
+ break
534
+ }
535
+ case 2: {
536
+ // Meditate - Boost own speed, magic, physical by 30%
537
+
538
+ // Check if gotchi already has power_up_2 status
539
+ if (attackingGotchi.statuses.includes('power_up_2')) {
540
+ specialNotDone = true
541
+ break
542
+ }
543
+
544
+ if (!addStatusToGotchi(attackingGotchi, 'power_up_2')) {
545
+ specialNotDone = true
546
+ break
547
+ }
548
+
549
+ effects = [
550
+ {
551
+ target: attackingGotchi.id,
552
+ outcome: 'success',
553
+ statuses: ['power_up_2']
554
+ }
555
+ ]
556
+ break
557
+ }
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
+ }
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
+ }
594
+ case 5: {
595
+ // Curse - attack random enemy for 50% damage, apply fear status and remove all buffs
596
+
597
+ const curseTarget = getTarget(defendingTeam, rng)
598
+
599
+ const curseTargetStatuses = ['fear']
600
+ let curseMultiplier = MULTS.CURSE_DAMAGE
601
+
602
+ // Check if leader passive is 'spread_the_fear' then apply fear status
603
+ if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
604
+ curseTargetStatuses.push('fear')
605
+ curseMultiplier = MULTS.SPREAD_THE_FEAR_CURSE_DAMAGE
606
+ }
607
+
608
+ effects = attack(attackingGotchi, attackingTeam, defendingTeam, [curseTarget], rng, {
609
+ multiplier: curseMultiplier,
610
+ statuses: curseTargetStatuses,
611
+ cannotBeCountered: true,
612
+ noPassiveStatuses: true,
613
+ speedPenalty: MULTS.CURSE_SPEED_PENALTY,
614
+ noResistSpeedPenalty: true
615
+ })
616
+
617
+ const removeRandomBuff = (target) => {
618
+ const modifiedTarget = getModifiedStats(target)
619
+
620
+ if (rng() > modifiedTarget.resist / 100) {
621
+ const buffsToRemove = target.statuses.filter((status) => BUFFS.includes(status))
622
+
623
+ if (buffsToRemove.length) {
624
+ const randomBuff = buffsToRemove[Math.floor(rng() * buffsToRemove.length)]
625
+ statusesExpired.push({
626
+ target: target.id,
627
+ status: randomBuff
628
+ })
629
+
630
+ // Remove first instance of randomBuff (there may be multiple)
631
+ const index = target.statuses.indexOf(randomBuff)
632
+ target.statuses.splice(index, 1)
633
+ }
634
+ }
635
+ }
636
+
637
+ if (effects[0] && (effects[0].outcome === 'success' || effects[0].outcome === 'critical')) {
638
+ // 1 chance to remove a random buff
639
+ removeRandomBuff(curseTarget)
640
+
641
+ // Add another chance if crit
642
+ if (effects[0].outcome === 'critical') {
643
+ removeRandomBuff(curseTarget)
644
+ }
645
+
646
+ // Add another chance if 'spread_the_fear' status
647
+ if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
648
+ removeRandomBuff(curseTarget)
649
+ }
650
+
651
+ // heal attacking gotchi for % of damage dealt
652
+ let amountToHeal = Math.round(effects[0].damage * MULTS.CURSE_HEAL)
653
+
654
+ // Don't allow amountToHeal to be more than the difference between current health and max health
655
+ if (amountToHeal > attackingGotchi.originalStats.health - attackingGotchi.health) {
656
+ amountToHeal = attackingGotchi.originalStats.health - attackingGotchi.health
657
+ }
658
+
659
+ if (amountToHeal) {
660
+ attackingGotchi.health += amountToHeal
661
+
662
+ effects.push({
663
+ target: attackingGotchi.id,
664
+ outcome: effects[0].outcome,
665
+ damage: -Math.abs(amountToHeal)
666
+ })
667
+ }
668
+ }
669
+
670
+ break
671
+ }
672
+ case 6: {
673
+ // Blessing - Heal all non-healer allies and remove all debuffs
674
+
675
+ // Get all alive non-healer allies on the attacking team
676
+ // const gotchisToHeal = getAlive(attackingTeam).filter(x => x.special.id !== 6)
677
+ const gotchisToHeal = getAlive(attackingTeam)
678
+
679
+ // Heal all allies for multiple of healers resistance
680
+ gotchisToHeal.forEach((gotchi) => {
681
+ let amountToHeal
682
+
683
+ // If gotchi has 'cleansing_aura' status, increase heal amount
684
+ if (attackingGotchi.statuses.includes('cleansing_aura')) {
685
+ amountToHeal = Math.round(modifiedAttackingGotchi.resist * MULTS.CLEANSING_AURA_HEAL)
686
+ } else {
687
+ amountToHeal = Math.round(modifiedAttackingGotchi.resist * MULTS.BLESSING_HEAL)
688
+ }
689
+
690
+ // Check for crit
691
+ const isCrit = rng() < modifiedAttackingGotchi.crit / 100
692
+ if (isCrit) {
693
+ amountToHeal = Math.round(amountToHeal * MULTS.BLESSING_HEAL_CRIT_MULTIPLIER)
694
+ }
695
+
696
+ // Apply speed penalty
697
+ let speedPenalty
698
+ if (attackingGotchi.statuses.includes('cleansing_aura')) {
699
+ speedPenalty = Math.round((modifiedAttackingGotchi.speed - 100) * MULTS.CLEANSING_AURA_HEAL_SPEED_PENALTY)
700
+ } else {
701
+ speedPenalty = Math.round((modifiedAttackingGotchi.speed - 100) * MULTS.BLESSING_HEAL_SPEED_PENALTY)
702
+ }
703
+ if (speedPenalty > 0) amountToHeal -= speedPenalty
704
+
705
+ // Don't allow amountToHeal to be more than the difference between current health and max health
706
+ if (amountToHeal > gotchi.originalStats.health - gotchi.health) {
707
+ amountToHeal = gotchi.originalStats.health - gotchi.health
708
+ }
709
+
710
+ gotchi.health += amountToHeal
711
+
712
+ if (amountToHeal) {
713
+ effects.push({
714
+ target: gotchi.id,
715
+ outcome: isCrit ? 'critical' : 'success',
716
+ damage: -Math.abs(amountToHeal)
717
+ })
718
+ }
719
+
720
+ // if gotchi has 'cleansing_aura' status, remove all debuffs
721
+ if (attackingGotchi.statuses.includes('cleansing_aura')) {
722
+ // Remove all debuffs
723
+ gotchi.statuses = gotchi.statuses.filter((status) => !DEBUFFS.includes(status))
724
+
725
+ // Add removed debuffs to statusesExpired
726
+ gotchi.statuses.forEach((status) => {
727
+ if (DEBUFFS.includes(status)) {
728
+ statusesExpired.push({
729
+ target: gotchi.id,
730
+ status
731
+ })
732
+ }
733
+ })
734
+ } else {
735
+ // Remove 1 random debuff
736
+ const debuffs = gotchi.statuses.filter((status) => DEBUFFS.includes(status))
737
+
738
+ if (debuffs.length) {
739
+ const randomDebuff = debuffs[Math.floor(rng() * debuffs.length)]
740
+ statusesExpired.push({
741
+ target: gotchi.id,
742
+ status: randomDebuff
743
+ })
744
+
745
+ // Remove first instance of randomDebuff (there may be multiple)
746
+ const index = gotchi.statuses.indexOf(randomDebuff)
747
+ gotchi.statuses.splice(index, 1)
748
+ }
749
+ }
750
+
751
+ })
752
+
753
+ // If no allies have been healed and no debuffs removed, then special attack not done
754
+ if (!effects.length && !statusesExpired.length) {
755
+ specialNotDone = true
756
+ break
757
+ }
758
+
759
+ break
760
+ }
761
+ case 7: {
762
+ // Thunder - Attack all enemies for 50% damage and apply stun status
763
+
764
+ const thunderTargets = getAlive(defendingTeam)
765
+
766
+ let stunStatuses = []
767
+ // Check if leader passive is 'channel_the_coven' then apply stun status
768
+ if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
769
+ if (rng() < MULTS.CHANNEL_THE_COVEN_STUN_CHANCE) stunStatuses.push('stun')
770
+ } else {
771
+ if (rng() < MULTS.THUNDER_STUN_CHANCE) stunStatuses.push('stun')
772
+ }
773
+
774
+ effects = attack(attackingGotchi, attackingTeam, defendingTeam, thunderTargets, rng, {
775
+ multiplier: MULTS.THUNDER_DAMAGE,
776
+ statuses: stunStatuses,
777
+ cannotBeCountered: true,
778
+ noPassiveStatuses: true
779
+ })
780
+
781
+ break
782
+ }
783
+ case 8: {
784
+ // Devestating Smash - Attack random enemy for 200% damage
785
+
786
+ const smashTarget = getTarget(defendingTeam, rng)
787
+
788
+ effects = attack(attackingGotchi, attackingTeam, defendingTeam, [smashTarget], rng, {
789
+ multiplier: MULTS.DEVESTATING_SMASH_DAMAGE,
790
+ cannotBeCountered: true,
791
+ noPassiveStatuses: true
792
+ })
793
+
794
+ let anotherAttack = false
795
+ if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
796
+ if (rng() < MULTS.CLAN_MOMENTUM_CHANCE) anotherAttack = true
797
+ } else {
798
+ if (rng() < MULTS.DEVESTATING_SMASH_X2_CHANCE) anotherAttack = true
799
+ }
800
+
801
+ if (anotherAttack) {
802
+ // Check if any enemies are alive
803
+ const aliveEnemies = getAlive(defendingTeam)
804
+
805
+ if (aliveEnemies.length) {
806
+ // Do an extra devestating smash
807
+ const target = getTarget(defendingTeam, rng)
808
+
809
+ effects.push(...attack(attackingGotchi, attackingTeam, defendingTeam, [target], rng, {
810
+ multiplier: MULTS.DEVESTATING_SMASH_X2_DAMAGE,
811
+ cannotBeCountered: true,
812
+ noPassiveStatuses: true
813
+ }))
814
+ }
815
+ }
816
+
817
+ break
818
+ }
819
+ }
820
+
821
+ return {
822
+ effects,
823
+ statusesExpired,
824
+ specialNotDone
825
+ }
826
+ }
827
+
828
+ module.exports = {
829
+ gameLoop,
830
+ attack,
831
+ specialAttack
832
+ }