gotchi-battler-game-logic 2.0.7 → 2.0.8

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 (50) 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 +6 -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 +595 -593
  13. package/game-logic/v1.7/index.js +802 -795
  14. package/index.js +12 -12
  15. package/package.json +26 -26
  16. package/schemas/team.json +349 -343
  17. package/scripts/balancing/createCSV.js +126 -126
  18. package/scripts/balancing/fixTrainingGotchis.js +155 -259
  19. package/scripts/balancing/processSims.js +229 -229
  20. package/scripts/balancing/sims.js +278 -278
  21. package/scripts/balancing/v1.7/class_combos.js +43 -43
  22. package/scripts/balancing/v1.7/setTeamPositions.js +105 -105
  23. package/scripts/balancing/v1.7/training_gotchis.json +20161 -20161
  24. package/scripts/balancing/v1.7/trait_combos.json +9 -9
  25. package/scripts/balancing/v1.7.1/class_combos.js +43 -43
  26. package/scripts/balancing/v1.7.1/setTeamPositions.js +122 -122
  27. package/scripts/balancing/v1.7.1/training_gotchis.json +22401 -22401
  28. package/scripts/balancing/v1.7.1/trait_combos.json +9 -9
  29. package/scripts/balancing/v1.7.2/class_combos.js +44 -0
  30. package/scripts/balancing/v1.7.2/setTeamPositions.js +122 -0
  31. package/scripts/balancing/v1.7.2/training_gotchis.json +22402 -0
  32. package/scripts/balancing/v1.7.2/trait_combos.json +10 -0
  33. package/scripts/data/team1.json +213 -213
  34. package/scripts/data/team2.json +200 -200
  35. package/scripts/data/tournaments.json +66 -66
  36. package/scripts/{runBattle.js → runLocalBattle.js} +18 -18
  37. package/scripts/runRealBattle.js +52 -0
  38. package/scripts/simRealBattle.js +121 -0
  39. package/scripts/validateBattle.js +74 -70
  40. package/scripts/validateTournament.js +101 -101
  41. package/utils/contracts.js +12 -12
  42. package/utils/errors.js +29 -29
  43. package/utils/mapGotchi.js +119 -0
  44. package/utils/transforms.js +89 -88
  45. package/utils/validations.js +39 -39
  46. package/debug.log +0 -2
  47. package/game-logic/v1.6/debug.log +0 -1
  48. package/game-logic/v1.7/debug.log +0 -3
  49. package/scripts/data/debug.log +0 -2
  50. package/scripts/debug.log +0 -1
@@ -1,594 +1,596 @@
1
- const {
2
- PASSIVES,
3
- BUFF_MULT_EFFECTS,
4
- BUFF_FLAT_EFFECTS,
5
- DEBUFF_MULT_EFFECTS,
6
- DEBUFF_FLAT_EFFECTS,
7
- MULTS
8
- } = require('./constants')
9
-
10
- // Get only alive gotchis in a team
11
- const getAlive = (team, row) => {
12
- if (row) {
13
- return team.formation[row].filter(x => x).filter(x => x.health > 0)
14
- }
15
-
16
- return [...team.formation.front, ...team.formation.back].filter(x => x).filter(x => x.health > 0)
17
- }
18
-
19
- /**
20
- * Get the formation position of a gotchi
21
- * @param {Object} team1 An in-game team object
22
- * @param {Object} team2 An in-game team object
23
- * @param {Number} gotchiId The id of the gotchi
24
- * @returns {Object} position The formation position of the gotchi
25
- * @returns {Number} position.team The team the gotchi is on
26
- * @returns {String} position.row The row the gotchi is on
27
- * @returns {Number} position.position The position of the gotchi in the row
28
- * @returns {null} position null if the gotchi is not found
29
- **/
30
- const getFormationPosition = (team1, team2, gotchiId) => {
31
- const team1FrontIndex = team1.formation.front.findIndex(x => x && x.id === gotchiId)
32
-
33
- if (team1FrontIndex !== -1) return {
34
- team: 1,
35
- row: 'front',
36
- position: team1FrontIndex,
37
- name: team1.formation.front[team1FrontIndex].name
38
- }
39
-
40
- const team1BackIndex = team1.formation.back.findIndex(x => x && x.id === gotchiId)
41
-
42
- if (team1BackIndex !== -1) return {
43
- team: 1,
44
- row: 'back',
45
- position: team1BackIndex,
46
- name: team1.formation.back[team1BackIndex].name
47
- }
48
-
49
- const team2FrontIndex = team2.formation.front.findIndex(x => x && x.id === gotchiId)
50
-
51
- if (team2FrontIndex !== -1) return {
52
- team: 2,
53
- row: 'front',
54
- position: team2FrontIndex,
55
- name: team2.formation.front[team2FrontIndex].name
56
- }
57
-
58
- const team2BackIndex = team2.formation.back.findIndex(x => x && x.id === gotchiId)
59
-
60
- if (team2BackIndex !== -1) return {
61
- team: 2,
62
- row: 'back',
63
- position: team2BackIndex,
64
- name: team2.formation.back[team2BackIndex].name
65
- }
66
-
67
- return null
68
- }
69
-
70
- /**
71
- * Get the leader gotchi of a team
72
- * @param {Object} team An in-game team object
73
- * @returns {Object} gotchi The leader gotchi
74
- * @returns {Number} leader.id The id of the gotchi
75
- * @returns {String} leader.special The special object of the gotchi
76
- * @returns {String} leader.special.class The class of the special
77
- **/
78
- const getLeaderGotchi = (team) => {
79
- const leader = [...team.formation.front, ...team.formation.back].find(x => x && x.id === team.leader)
80
-
81
- if (!leader) throw new Error('Leader not found')
82
-
83
- return leader
84
- }
85
-
86
- /**
87
- * Get the next gotchi to act
88
- * @param {Object} team1 An in-game team object
89
- * @param {Object} team2 An in-game team object
90
- * @param {Function} rng The random number generator
91
- * @returns {Object} position The formation position of the gotchi
92
- **/
93
- const getNextToAct = (team1, team2, rng) => {
94
- const aliveGotchis = [...getAlive(team1), ...getAlive(team2)]
95
-
96
- aliveGotchis.sort((a, b) => a.actionDelay - b.actionDelay)
97
-
98
- let toAct = aliveGotchis.filter(gotchi => gotchi.actionDelay === aliveGotchis[0].actionDelay)
99
-
100
- // If only one gotchi can act then return it
101
- if (toAct.length === 1) return getFormationPosition(team1, team2, toAct[0].id)
102
-
103
- // Lowest speeds win tiebreaker
104
- toAct.sort((a, b) => a.speed - b.speed)
105
- toAct = toAct.filter(gotchi => gotchi.speed === toAct[0].speed)
106
-
107
- // If only one gotchi can act then return it
108
-
109
- if (toAct.length === 1) return getFormationPosition(team1, team2, toAct[0].id)
110
-
111
- // If still tied then randomly choose
112
- const randomIndex = Math.floor(rng() * toAct.length)
113
-
114
- if (!toAct[randomIndex]) throw new Error(`No gotchi found at index ${randomIndex}`)
115
-
116
- toAct = toAct[randomIndex]
117
- return getFormationPosition(team1, team2, toAct.id)
118
- }
119
-
120
- const getTarget = (defendingTeam, rng) => {
121
- // Check for taunt gotchis
122
- const taunt = [...getAlive(defendingTeam, 'front'), ...getAlive(defendingTeam, 'back')].filter(gotchi => gotchi.statuses && gotchi.statuses.includes("taunt"))
123
-
124
- if (taunt.length) {
125
- if (taunt.length === 1) return taunt[0]
126
-
127
- // If multiple taunt gotchis then randomly choose one
128
- return taunt[Math.floor(rng() * taunt.length)]
129
- }
130
-
131
- // Target gotchis in the front row first
132
- const frontRow = getAlive(defendingTeam, 'front')
133
-
134
- if (frontRow.length) {
135
- return frontRow[Math.floor(rng() * frontRow.length)]
136
- }
137
-
138
- // If no gotchis in front row then target back row
139
- const backRow = getAlive(defendingTeam, 'back')
140
-
141
- if (backRow.length) {
142
- return backRow[Math.floor(rng() * backRow.length)]
143
- }
144
-
145
- throw new Error('No gotchis to target')
146
- }
147
-
148
- const applySpeedPenalty = (gotchi, penalty) => {
149
- const speedPenalty = (gotchi.speed - 100) * penalty
150
-
151
- return {
152
- ...gotchi,
153
- magic: gotchi.magic - speedPenalty,
154
- physical: gotchi.physical - speedPenalty
155
- }
156
- }
157
-
158
- /**
159
- * Get the damage of an attack
160
- * @param {Object} attackingTeam The attacking team
161
- * @param {Object} defendingTeam The defending team
162
- * @param {Object} attackingGotchi The gotchi attacking
163
- * @param {Object} defendingGotchi The gotchi defending
164
- * @param {Number} multiplier The damage multiplier
165
- * @param {Boolean} ignoreArmor Whether to ignore armor
166
- * @param {Number} speedPenalty The speed penalty to apply
167
- * @returns {Number} damage The damage of the attack
168
- **/
169
- const getDamage = (attackingTeam, defendingTeam, attackingGotchi, defendingGotchi, multiplier, ignoreArmor, speedPenalty) => {
170
-
171
- const attackerWithSpeedPenalty = speedPenalty ? applySpeedPenalty(attackingGotchi, speedPenalty) : attackingGotchi
172
-
173
- // Apply any status effects
174
- const modifiedAttackingGotchi = getModifiedStats(attackerWithSpeedPenalty)
175
- const modifiedDefendingGotchi = getModifiedStats(defendingGotchi)
176
-
177
- let attackValue = modifiedAttackingGotchi.attack === 'magic' ? modifiedAttackingGotchi.magic : modifiedAttackingGotchi.physical
178
-
179
- // If attacking gotchi is in the front row then apply front row attack bonus
180
- if (getFormationPosition(attackingTeam, defendingTeam, attackingGotchi.id).row === 'front') {
181
- attackValue = Math.round(attackValue * MULTS.FRONT_ROW_ATK_BONUS)
182
- }
183
-
184
- let defenseValue = modifiedAttackingGotchi.attack === 'magic' ? modifiedDefendingGotchi.magic : modifiedDefendingGotchi.physical
185
-
186
- // If defending gotchi is in the front row then apply front row defence penalty
187
- if (getFormationPosition(attackingTeam, defendingTeam, defendingGotchi.id).row === 'front') {
188
- defenseValue = Math.round(defenseValue * MULTS.FRONT_ROW_DEF_NERF)
189
- }
190
-
191
- // Add armor to defense value
192
- if (!ignoreArmor) defenseValue += modifiedDefendingGotchi.armor
193
-
194
- // Calculate damage
195
- let damage = Math.round((attackValue / defenseValue) * 100)
196
-
197
- // Apply multiplier
198
- if (multiplier) damage = Math.round(damage * multiplier)
199
-
200
- // check for environment effects
201
- if (defendingGotchi.environmentEffects && defendingGotchi.environmentEffects.length > 0) {
202
- damage = Math.round(damage * (1 + (defendingGotchi.environmentEffects.length * 0.5)))
203
- }
204
-
205
- return damage
206
- }
207
-
208
- /**
209
- * Apply status effects to a gotchi
210
- * @param {Object} gotchi An in-game gotchi object
211
- * @returns {Object} gotchi An in-game gotchi object with modified stats
212
- */
213
- const getModifiedStats = (gotchi) => {
214
- const statMods = {}
215
-
216
- gotchi.statuses.forEach(status => {
217
- const statusStatMods = {}
218
-
219
- // apply any modifier from BUFF_MULT_EFFECTS
220
- if (BUFF_MULT_EFFECTS[status]) {
221
- Object.keys(BUFF_MULT_EFFECTS[status]).forEach(stat => {
222
- const modifier = Math.round(gotchi[stat] * BUFF_MULT_EFFECTS[status][stat])
223
-
224
- statusStatMods[stat] = modifier
225
- })
226
- }
227
-
228
- // apply any modifier from BUFF_FLAT_EFFECTS
229
- if (BUFF_FLAT_EFFECTS[status]) {
230
- Object.keys(BUFF_FLAT_EFFECTS[status]).forEach(stat => {
231
- if (statusStatMods[stat]) {
232
- // If a mod for this status already exists, only add if the new mod is greater
233
- if (BUFF_FLAT_EFFECTS[status][stat] > statusStatMods[stat]) statusStatMods[stat] = BUFF_FLAT_EFFECTS[status][stat]
234
- } else {
235
- statusStatMods[stat] = BUFF_FLAT_EFFECTS[status][stat]
236
- }
237
- })
238
- }
239
-
240
- // apply any modifier from DEBUFF_MULT_EFFECTS
241
- if (DEBUFF_MULT_EFFECTS[status]) {
242
- Object.keys(DEBUFF_MULT_EFFECTS[status]).forEach(stat => {
243
- const modifier = Math.round(gotchi[stat] * DEBUFF_MULT_EFFECTS[status][stat])
244
-
245
- statusStatMods[stat] = -modifier
246
- })
247
- }
248
-
249
- // apply any modifier from DEBUFF_FLAT_EFFECTS
250
- if (DEBUFF_FLAT_EFFECTS[status]) {
251
- Object.keys(DEBUFF_FLAT_EFFECTS[status]).forEach(stat => {
252
- if (statusStatMods[stat]) {
253
- // If a mod for this status already exists, only add if the new mod is greater
254
- if (DEBUFF_FLAT_EFFECTS[status][stat] < statusStatMods[stat]) statusStatMods[stat] = DEBUFF_FLAT_EFFECTS[status][stat]
255
- } else {
256
- statusStatMods[stat] = -DEBUFF_FLAT_EFFECTS[status][stat]
257
- }
258
- })
259
- }
260
-
261
- // apply status mods
262
- Object.keys(statusStatMods).forEach(stat => {
263
- statMods[stat] = statMods[stat] ? statMods[stat] + statusStatMods[stat] : statusStatMods[stat]
264
- })
265
- })
266
-
267
- const modifiedGotchi = {
268
- ...gotchi
269
- }
270
-
271
- // apply stat mods
272
- Object.keys(statMods).forEach(stat => {
273
- if (statMods[stat] < 0) {
274
- modifiedGotchi[stat] = modifiedGotchi[stat] + statMods[stat] < 0 ? 0 : modifiedGotchi[stat] + statMods[stat]
275
- } else {
276
- modifiedGotchi[stat] += statMods[stat]
277
- }
278
-
279
- })
280
-
281
- // Recalculate attack type
282
- modifiedGotchi.attack = modifiedGotchi.magic > modifiedGotchi.physical ? 'magic' : 'physical'
283
-
284
- return modifiedGotchi
285
- }
286
-
287
- const calculateActionDelay = (gotchi) => {
288
- // Calculate action delay and round to 3 decimal places
289
- return Math.round(((100 / getModifiedStats(gotchi).speed) + Number.EPSILON) * 1000) / 1000
290
- }
291
-
292
- const getNewActionDelay = (gotchi) => {
293
- // Calculate new action delay and round to 3 decimal places
294
- return Math.round((gotchi.actionDelay + calculateActionDelay(gotchi) + Number.EPSILON) * 1000) / 1000
295
- }
296
-
297
- /**
298
- * Simplify a team object for storage
299
- * @param {Object} team An in-game team object
300
- * @returns {Object} simplifiedTeam A simplified team object
301
- */
302
- const simplifyTeam = (team) => {
303
- return {
304
- name: team.name,
305
- owner: team.owner,
306
- leaderId: team.leader,
307
- rows: [
308
- {
309
- slots: team.formation.front.map((x) => {
310
- return {
311
- isActive: x ? true : false,
312
- id: x ? x.id : null
313
- }
314
- })
315
- },
316
- {
317
- slots: team.formation.back.map((x) => {
318
- return {
319
- isActive: x ? true : false,
320
- id: x ? x.id : null
321
- }
322
- })
323
- }
324
- ],
325
- uiOrder: getUiOrder(team)
326
- }
327
- }
328
-
329
- /**
330
- * Get the UI order of a team (used for the front end)
331
- * @param {Object} team An in-game team object
332
- * @returns {Array} uiOrder An array of gotchi ids in the order they should be displayed
333
- **/
334
- const getUiOrder = (team) => {
335
- const uiOrder = []
336
-
337
- if (team.formation.front[0]) uiOrder.push(team.formation.front[0].id)
338
- if (team.formation.back[0]) uiOrder.push(team.formation.back[0].id)
339
- if (team.formation.front[1]) uiOrder.push(team.formation.front[1].id)
340
- if (team.formation.back[1]) uiOrder.push(team.formation.back[1].id)
341
- if (team.formation.front[2]) uiOrder.push(team.formation.front[2].id)
342
- if (team.formation.back[2]) uiOrder.push(team.formation.back[2].id)
343
- if (team.formation.front[3]) uiOrder.push(team.formation.front[3].id)
344
- if (team.formation.back[3]) uiOrder.push(team.formation.back[3].id)
345
- if (team.formation.front[4]) uiOrder.push(team.formation.front[4].id)
346
- if (team.formation.back[4]) uiOrder.push(team.formation.back[4].id)
347
-
348
- return uiOrder
349
- }
350
-
351
- /**
352
- * Add the leader statuses to a team
353
- * @param {Object} team An in-game team object
354
- **/
355
- const addLeaderToTeam = (team) => {
356
- // Add passive leader abilities
357
- const teamLeader = getLeaderGotchi(team)
358
-
359
- team.leaderPassive = teamLeader.special.id
360
-
361
- // Apply leader passive statuses
362
- switch (team.leaderPassive) {
363
- case 1:
364
- // Sharpen blades - all allies gain 'sharp_blades' status
365
- getAlive(team).forEach(x => {
366
- x.statuses.push(PASSIVES[team.leaderPassive - 1])
367
- })
368
- break
369
- case 2:
370
- // Cloud of Zen - Leader get 'cloud_of_zen' status
371
- teamLeader.statuses.push(PASSIVES[team.leaderPassive - 1])
372
- break
373
- case 3:
374
- // Frenzy - all allies get 'frenzy' status
375
- getAlive(team).forEach(x => {
376
- x.statuses.push(PASSIVES[team.leaderPassive - 1])
377
- })
378
- break
379
- case 4:
380
- // All allies get 'fortify' status
381
- getAlive(team).forEach(x => {
382
- x.statuses.push(PASSIVES[team.leaderPassive - 1])
383
- })
384
-
385
- break
386
- case 5:
387
- // Spread the fear - all allies get 'spread_the_fear' status
388
- getAlive(team).forEach(x => {
389
- x.statuses.push(PASSIVES[team.leaderPassive - 1])
390
- })
391
- break
392
- case 6:
393
- // Cleansing aura - every healer ally and every tank ally gets 'cleansing_aura' status
394
- getAlive(team).forEach(x => {
395
- if (x.special.id === 6 || x.special.id === 4) x.statuses.push(PASSIVES[team.leaderPassive - 1])
396
- })
397
- break
398
- case 7:
399
- // All allies get 'channel_the_coven' status
400
- getAlive(team).forEach(x => {
401
- x.statuses.push(PASSIVES[team.leaderPassive - 1])
402
- })
403
- break
404
- case 8:
405
- // All allies get 'clan_momentum' status
406
- getAlive(team).forEach(x => {
407
- x.statuses.push(PASSIVES[team.leaderPassive - 1])
408
- })
409
- break
410
- }
411
- }
412
-
413
- const removeLeaderPassivesFromTeam = (team) => {
414
- let statusesRemoved = []
415
- if (!team.leaderPassive) return statusesRemoved
416
-
417
- // Remove leader passive statuses from team
418
- getAlive(team).forEach(x => {
419
- // add effects for each status removed
420
- x.statuses.forEach(status => {
421
- if (status === PASSIVES[team.leaderPassive - 1]) {
422
- statusesRemoved.push({
423
- target: x.id,
424
- status: status
425
- })
426
- }
427
- })
428
-
429
- x.statuses = x.statuses.filter(x => x !== PASSIVES[team.leaderPassive - 1])
430
- })
431
-
432
- team.leaderPassive = null
433
-
434
- return statusesRemoved
435
- }
436
-
437
- const getExpiredStatuses = (team1, team2) => {
438
- // If leader is dead, remove leader passive
439
- let statusesExpired = []
440
- if (team1.leaderPassive && !getAlive(team1).find(x => x.id === team1.leader)) {
441
- // Remove leader passive statuses
442
- statusesExpired = removeLeaderPassivesFromTeam(team1)
443
- }
444
- if (team2.leaderPassive && !getAlive(team2).find(x => x.id === team2.leader)) {
445
- // Remove leader passive statuses
446
- statusesExpired = removeLeaderPassivesFromTeam(team2)
447
- }
448
-
449
- return statusesExpired
450
- }
451
-
452
- /**
453
- * Add a status to a gotchi
454
- * @param {Object} gotchi An in-game gotchi object
455
- * @param {String} status The status to add
456
- * @returns {Boolean} success A boolean to determine if the status was added
457
- **/
458
- const addStatusToGotchi = (gotchi, status) => {
459
- // Check that gotchi doesn't already have max number of statuses
460
- if (gotchi.statuses.filter(item => item === status).length >= MULTS.MAX_STATUSES) return false
461
-
462
- gotchi.statuses.push(status)
463
-
464
- return true
465
- }
466
-
467
- const scrambleGotchiIds = (allAliveGotchis, team1, team2) => {
468
- // check there's no duplicate gotchis
469
- const gotchiIds = allAliveGotchis.map(x => x.id)
470
-
471
- if (gotchiIds.length !== new Set(gotchiIds).size) {
472
- // scramble gotchi ids
473
- allAliveGotchis.forEach(x => {
474
- const newId = Math.floor(Math.random() * 10000000)
475
-
476
- // find gotchi in team1 or team2
477
- const position = getFormationPosition(team1, team2, x.id)
478
-
479
- // change gotchi id
480
- if (position) {
481
- if (position.team === 1) {
482
- if (x.id === team1.leader) team1.leader = newId
483
- team1.formation[position.row][position.position].id = newId
484
- } else {
485
- if (x.id === team2.leader) team2.leader = newId
486
- team2.formation[position.row][position.position].id = newId
487
- }
488
- } else {
489
- throw new Error('Gotchi not found in team1 or team2')
490
- }
491
- })
492
-
493
- // check again
494
- const newGotchiIds = allAliveGotchis.map(x => x.id)
495
- if (newGotchiIds.length !== new Set(newGotchiIds).size) {
496
- // Scramble again
497
- scrambleGotchiIds(allAliveGotchis, team1, team2)
498
- }
499
- }
500
- }
501
-
502
- /**
503
- * Prepare teams for battle
504
- * @param {Array} allAliveGotchis An array of all alive gotchis
505
- * @param {Object} team1 An in-game team object
506
- * @param {Object} team2 An in-game team object
507
- **/
508
- const prepareTeams = (allAliveGotchis, team1, team2) => {
509
- // check there's no duplicate gotchis
510
- scrambleGotchiIds(allAliveGotchis, team1, team2);
511
-
512
- // Apply stat items
513
- applyStatItems(allAliveGotchis)
514
-
515
- allAliveGotchis.forEach(x => {
516
- // Add statuses property to all gotchis
517
- x.statuses = []
518
-
519
- // Calculate initial action delay for all gotchis
520
- x.actionDelay = calculateActionDelay(x)
521
-
522
- // Calculate attack type
523
- x.attack = x.magic > x.physical ? 'magic' : 'physical'
524
-
525
- // Add original stats to all gotchis
526
- // Do a deep copy of the gotchi object to avoid modifying the original object
527
- x.originalStats = JSON.parse(JSON.stringify(x))
528
-
529
- // Add environmentEffects to all gotchis
530
- x.environmentEffects = []
531
- })
532
-
533
- // Add leader passive to team
534
- addLeaderToTeam(team1)
535
- addLeaderToTeam(team2)
536
- }
537
-
538
- /**
539
- * Get log gotchi object for battle logs
540
- * @param {Array} allAliveGotchis An array of all alive gotchis
541
- * @returns {Array} logGotchis An array of gotchi objects for logs
542
- */
543
- const getLogGotchis = (allAliveGotchis) => {
544
- const logGotchis = JSON.parse(JSON.stringify(allAliveGotchis))
545
-
546
- logGotchis.forEach(x => {
547
- // Change gotchi.special.class to gotchi.special.gotchiClass to avoid conflicts with class keyword
548
- x.special.gotchiClass = x.special.class
549
-
550
- // Remove unnecessary properties to reduce log size
551
- delete x.special.class
552
- delete x.actionDelay
553
- delete x.attack
554
- delete x.originalStats
555
- delete x.environmentEffects
556
- })
557
-
558
- return logGotchis
559
- }
560
-
561
- /**
562
- * Apply stat items to gotchis
563
- * @param {Array} gotchis An array of gotchis
564
- */
565
- const applyStatItems = (gotchis) => {
566
- gotchis.forEach(gotchi => {
567
- // Apply stat items
568
- if (gotchi.item && gotchi.item.stat && gotchi.item.statValue) {
569
- gotchi[gotchi.item.stat] += gotchi.item.statValue
570
- }
571
- })
572
- }
573
-
574
- module.exports = {
575
- getAlive,
576
- getFormationPosition,
577
- getLeaderGotchi,
578
- getNextToAct,
579
- getTarget,
580
- getDamage,
581
- getModifiedStats,
582
- calculateActionDelay,
583
- getNewActionDelay,
584
- simplifyTeam,
585
- getUiOrder,
586
- addLeaderToTeam,
587
- removeLeaderPassivesFromTeam,
588
- getExpiredStatuses,
589
- addStatusToGotchi,
590
- scrambleGotchiIds,
591
- prepareTeams,
592
- getLogGotchis,
593
- applyStatItems
1
+ const {
2
+ PASSIVES,
3
+ BUFF_MULT_EFFECTS,
4
+ BUFF_FLAT_EFFECTS,
5
+ DEBUFF_MULT_EFFECTS,
6
+ DEBUFF_FLAT_EFFECTS,
7
+ MULTS
8
+ } = require('./constants')
9
+
10
+ // Get only alive gotchis in a team
11
+ const getAlive = (team, row) => {
12
+ if (row) {
13
+ return team.formation[row].filter(x => x).filter(x => x.health > 0)
14
+ }
15
+
16
+ return [...team.formation.front, ...team.formation.back].filter(x => x).filter(x => x.health > 0)
17
+ }
18
+
19
+ /**
20
+ * Get the formation position of a gotchi
21
+ * @param {Object} team1 An in-game team object
22
+ * @param {Object} team2 An in-game team object
23
+ * @param {Number} gotchiId The id of the gotchi
24
+ * @returns {Object} position The formation position of the gotchi
25
+ * @returns {Number} position.team The team the gotchi is on
26
+ * @returns {String} position.row The row the gotchi is on
27
+ * @returns {Number} position.position The position of the gotchi in the row
28
+ * @returns {null} position null if the gotchi is not found
29
+ **/
30
+ const getFormationPosition = (team1, team2, gotchiId) => {
31
+ const team1FrontIndex = team1.formation.front.findIndex(x => x && x.id === gotchiId)
32
+
33
+ if (team1FrontIndex !== -1) return {
34
+ team: 1,
35
+ row: 'front',
36
+ position: team1FrontIndex,
37
+ name: team1.formation.front[team1FrontIndex].name
38
+ }
39
+
40
+ const team1BackIndex = team1.formation.back.findIndex(x => x && x.id === gotchiId)
41
+
42
+ if (team1BackIndex !== -1) return {
43
+ team: 1,
44
+ row: 'back',
45
+ position: team1BackIndex,
46
+ name: team1.formation.back[team1BackIndex].name
47
+ }
48
+
49
+ const team2FrontIndex = team2.formation.front.findIndex(x => x && x.id === gotchiId)
50
+
51
+ if (team2FrontIndex !== -1) return {
52
+ team: 2,
53
+ row: 'front',
54
+ position: team2FrontIndex,
55
+ name: team2.formation.front[team2FrontIndex].name
56
+ }
57
+
58
+ const team2BackIndex = team2.formation.back.findIndex(x => x && x.id === gotchiId)
59
+
60
+ if (team2BackIndex !== -1) return {
61
+ team: 2,
62
+ row: 'back',
63
+ position: team2BackIndex,
64
+ name: team2.formation.back[team2BackIndex].name
65
+ }
66
+
67
+ return null
68
+ }
69
+
70
+ /**
71
+ * Get the leader gotchi of a team
72
+ * @param {Object} team An in-game team object
73
+ * @returns {Object} gotchi The leader gotchi
74
+ * @returns {Number} leader.id The id of the gotchi
75
+ * @returns {String} leader.special The special object of the gotchi
76
+ * @returns {String} leader.special.class The class of the special
77
+ **/
78
+ const getLeaderGotchi = (team) => {
79
+ const leader = [...team.formation.front, ...team.formation.back].find(x => x && x.id === team.leader)
80
+
81
+ if (!leader) throw new Error('Leader not found')
82
+
83
+ return leader
84
+ }
85
+
86
+ /**
87
+ * Get the next gotchi to act
88
+ * @param {Object} team1 An in-game team object
89
+ * @param {Object} team2 An in-game team object
90
+ * @param {Function} rng The random number generator
91
+ * @returns {Object} position The formation position of the gotchi
92
+ **/
93
+ const getNextToAct = (team1, team2, rng) => {
94
+ const aliveGotchis = [...getAlive(team1), ...getAlive(team2)]
95
+
96
+ aliveGotchis.sort((a, b) => a.actionDelay - b.actionDelay)
97
+
98
+ let toAct = aliveGotchis.filter(gotchi => gotchi.actionDelay === aliveGotchis[0].actionDelay)
99
+
100
+ // If only one gotchi can act then return it
101
+ if (toAct.length === 1) return getFormationPosition(team1, team2, toAct[0].id)
102
+
103
+ // Lowest speeds win tiebreaker
104
+ toAct.sort((a, b) => a.speed - b.speed)
105
+ toAct = toAct.filter(gotchi => gotchi.speed === toAct[0].speed)
106
+
107
+ // If only one gotchi can act then return it
108
+
109
+ if (toAct.length === 1) return getFormationPosition(team1, team2, toAct[0].id)
110
+
111
+ // If still tied then randomly choose
112
+ const randomIndex = Math.floor(rng() * toAct.length)
113
+
114
+ if (!toAct[randomIndex]) throw new Error(`No gotchi found at index ${randomIndex}`)
115
+
116
+ toAct = toAct[randomIndex]
117
+ return getFormationPosition(team1, team2, toAct.id)
118
+ }
119
+
120
+ const getTarget = (defendingTeam, rng) => {
121
+ // Check for taunt gotchis
122
+ const taunt = [...getAlive(defendingTeam, 'front'), ...getAlive(defendingTeam, 'back')].filter(gotchi => gotchi.statuses && gotchi.statuses.includes("taunt"))
123
+
124
+ if (taunt.length) {
125
+ if (taunt.length === 1) return taunt[0]
126
+
127
+ // If multiple taunt gotchis then randomly choose one
128
+ return taunt[Math.floor(rng() * taunt.length)]
129
+ }
130
+
131
+ // Target gotchis in the front row first
132
+ const frontRow = getAlive(defendingTeam, 'front')
133
+
134
+ if (frontRow.length) {
135
+ return frontRow[Math.floor(rng() * frontRow.length)]
136
+ }
137
+
138
+ // If no gotchis in front row then target back row
139
+ const backRow = getAlive(defendingTeam, 'back')
140
+
141
+ if (backRow.length) {
142
+ return backRow[Math.floor(rng() * backRow.length)]
143
+ }
144
+
145
+ throw new Error('No gotchis to target')
146
+ }
147
+
148
+ const applySpeedPenalty = (gotchi, penalty) => {
149
+ const speedPenalty = (gotchi.speed - 100) * penalty
150
+
151
+ return {
152
+ ...gotchi,
153
+ magic: gotchi.magic - speedPenalty,
154
+ physical: gotchi.physical - speedPenalty
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Get the damage of an attack
160
+ * @param {Object} attackingTeam The attacking team
161
+ * @param {Object} defendingTeam The defending team
162
+ * @param {Object} attackingGotchi The gotchi attacking
163
+ * @param {Object} defendingGotchi The gotchi defending
164
+ * @param {Number} multiplier The damage multiplier
165
+ * @param {Boolean} ignoreArmor Whether to ignore armor
166
+ * @param {Number} speedPenalty The speed penalty to apply
167
+ * @returns {Number} damage The damage of the attack
168
+ **/
169
+ const getDamage = (attackingTeam, defendingTeam, attackingGotchi, defendingGotchi, multiplier, ignoreArmor, speedPenalty) => {
170
+
171
+ const attackerWithSpeedPenalty = speedPenalty ? applySpeedPenalty(attackingGotchi, speedPenalty) : attackingGotchi
172
+
173
+ // Apply any status effects
174
+ const modifiedAttackingGotchi = getModifiedStats(attackerWithSpeedPenalty)
175
+ const modifiedDefendingGotchi = getModifiedStats(defendingGotchi)
176
+
177
+ let attackValue = modifiedAttackingGotchi.attack === 'magic' ? modifiedAttackingGotchi.magic : modifiedAttackingGotchi.physical
178
+
179
+ // If attacking gotchi is in the front row then apply front row attack bonus
180
+ if (getFormationPosition(attackingTeam, defendingTeam, attackingGotchi.id).row === 'front') {
181
+ attackValue = Math.round(attackValue * MULTS.FRONT_ROW_ATK_BONUS)
182
+ }
183
+
184
+ let defenseValue = modifiedAttackingGotchi.attack === 'magic' ? modifiedDefendingGotchi.magic : modifiedDefendingGotchi.physical
185
+
186
+ // If defending gotchi is in the front row then apply front row defence penalty
187
+ if (getFormationPosition(attackingTeam, defendingTeam, defendingGotchi.id).row === 'front') {
188
+ defenseValue = Math.round(defenseValue * MULTS.FRONT_ROW_DEF_NERF)
189
+ }
190
+
191
+ // Add armor to defense value
192
+ if (!ignoreArmor) defenseValue += modifiedDefendingGotchi.armor
193
+
194
+ // Calculate damage
195
+ let damage = Math.round((attackValue / defenseValue) * 100)
196
+
197
+ // Apply multiplier
198
+ if (multiplier) damage = Math.round(damage * multiplier)
199
+
200
+ // check for environment effects
201
+ if (defendingGotchi.environmentEffects && defendingGotchi.environmentEffects.length > 0) {
202
+ damage = Math.round(damage * (1 + (defendingGotchi.environmentEffects.length * 0.5)))
203
+ }
204
+
205
+ return damage
206
+ }
207
+
208
+ /**
209
+ * Apply status effects to a gotchi
210
+ * @param {Object} gotchi An in-game gotchi object
211
+ * @returns {Object} gotchi An in-game gotchi object with modified stats
212
+ */
213
+ const getModifiedStats = (gotchi) => {
214
+ const statMods = {}
215
+
216
+ gotchi.statuses.forEach(status => {
217
+ const statusStatMods = {}
218
+
219
+ // apply any modifier from BUFF_MULT_EFFECTS
220
+ if (BUFF_MULT_EFFECTS[status]) {
221
+ Object.keys(BUFF_MULT_EFFECTS[status]).forEach(stat => {
222
+ const modifier = Math.round(gotchi[stat] * BUFF_MULT_EFFECTS[status][stat])
223
+
224
+ statusStatMods[stat] = modifier
225
+ })
226
+ }
227
+
228
+ // apply any modifier from BUFF_FLAT_EFFECTS
229
+ if (BUFF_FLAT_EFFECTS[status]) {
230
+ Object.keys(BUFF_FLAT_EFFECTS[status]).forEach(stat => {
231
+ if (statusStatMods[stat]) {
232
+ // If a mod for this status already exists, only add if the new mod is greater
233
+ if (BUFF_FLAT_EFFECTS[status][stat] > statusStatMods[stat]) statusStatMods[stat] = BUFF_FLAT_EFFECTS[status][stat]
234
+ } else {
235
+ statusStatMods[stat] = BUFF_FLAT_EFFECTS[status][stat]
236
+ }
237
+ })
238
+ }
239
+
240
+ // apply any modifier from DEBUFF_MULT_EFFECTS
241
+ if (DEBUFF_MULT_EFFECTS[status]) {
242
+ Object.keys(DEBUFF_MULT_EFFECTS[status]).forEach(stat => {
243
+ const modifier = Math.round(gotchi[stat] * DEBUFF_MULT_EFFECTS[status][stat])
244
+
245
+ statusStatMods[stat] = -modifier
246
+ })
247
+ }
248
+
249
+ // apply any modifier from DEBUFF_FLAT_EFFECTS
250
+ if (DEBUFF_FLAT_EFFECTS[status]) {
251
+ Object.keys(DEBUFF_FLAT_EFFECTS[status]).forEach(stat => {
252
+ if (statusStatMods[stat]) {
253
+ // If a mod for this status already exists, only add if the new mod is greater
254
+ if (DEBUFF_FLAT_EFFECTS[status][stat] < statusStatMods[stat]) statusStatMods[stat] = DEBUFF_FLAT_EFFECTS[status][stat]
255
+ } else {
256
+ statusStatMods[stat] = -DEBUFF_FLAT_EFFECTS[status][stat]
257
+ }
258
+ })
259
+ }
260
+
261
+ // apply status mods
262
+ Object.keys(statusStatMods).forEach(stat => {
263
+ statMods[stat] = statMods[stat] ? statMods[stat] + statusStatMods[stat] : statusStatMods[stat]
264
+ })
265
+ })
266
+
267
+ const modifiedGotchi = {
268
+ ...gotchi
269
+ }
270
+
271
+ // apply stat mods
272
+ Object.keys(statMods).forEach(stat => {
273
+ if (statMods[stat] < 0) {
274
+ modifiedGotchi[stat] = modifiedGotchi[stat] + statMods[stat] < 0 ? 0 : modifiedGotchi[stat] + statMods[stat]
275
+ } else {
276
+ modifiedGotchi[stat] += statMods[stat]
277
+ }
278
+
279
+ })
280
+
281
+ // Recalculate attack type
282
+ modifiedGotchi.attack = modifiedGotchi.magic > modifiedGotchi.physical ? 'magic' : 'physical'
283
+
284
+ return modifiedGotchi
285
+ }
286
+
287
+ const calculateActionDelay = (gotchi) => {
288
+ // Calculate action delay and round to 3 decimal places
289
+ return Math.round(((100 / getModifiedStats(gotchi).speed) + Number.EPSILON) * 1000) / 1000
290
+ }
291
+
292
+ const getNewActionDelay = (gotchi) => {
293
+ // Calculate new action delay and round to 3 decimal places
294
+ return Math.round((gotchi.actionDelay + calculateActionDelay(gotchi) + Number.EPSILON) * 1000) / 1000
295
+ }
296
+
297
+ /**
298
+ * Simplify a team object for storage
299
+ * @param {Object} team An in-game team object
300
+ * @returns {Object} simplifiedTeam A simplified team object
301
+ */
302
+ const simplifyTeam = (team) => {
303
+ return {
304
+ name: team.name,
305
+ owner: team.owner,
306
+ leaderId: team.leader,
307
+ rows: [
308
+ {
309
+ slots: team.formation.front.map((x) => {
310
+ return {
311
+ isActive: x ? true : false,
312
+ id: x ? x.id : null
313
+ }
314
+ })
315
+ },
316
+ {
317
+ slots: team.formation.back.map((x) => {
318
+ return {
319
+ isActive: x ? true : false,
320
+ id: x ? x.id : null
321
+ }
322
+ })
323
+ }
324
+ ],
325
+ uiOrder: getUiOrder(team)
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Get the UI order of a team (used for the front end)
331
+ * @param {Object} team An in-game team object
332
+ * @returns {Array} uiOrder An array of gotchi ids in the order they should be displayed
333
+ **/
334
+ const getUiOrder = (team) => {
335
+ const uiOrder = []
336
+
337
+ if (team.formation.front[0]) uiOrder.push(team.formation.front[0].id)
338
+ if (team.formation.back[0]) uiOrder.push(team.formation.back[0].id)
339
+ if (team.formation.front[1]) uiOrder.push(team.formation.front[1].id)
340
+ if (team.formation.back[1]) uiOrder.push(team.formation.back[1].id)
341
+ if (team.formation.front[2]) uiOrder.push(team.formation.front[2].id)
342
+ if (team.formation.back[2]) uiOrder.push(team.formation.back[2].id)
343
+ if (team.formation.front[3]) uiOrder.push(team.formation.front[3].id)
344
+ if (team.formation.back[3]) uiOrder.push(team.formation.back[3].id)
345
+ if (team.formation.front[4]) uiOrder.push(team.formation.front[4].id)
346
+ if (team.formation.back[4]) uiOrder.push(team.formation.back[4].id)
347
+
348
+ return uiOrder
349
+ }
350
+
351
+ /**
352
+ * Add the leader statuses to a team
353
+ * @param {Object} team An in-game team object
354
+ **/
355
+ const addLeaderToTeam = (team) => {
356
+ // Add passive leader abilities
357
+ const teamLeader = getLeaderGotchi(team)
358
+
359
+ team.leaderPassive = teamLeader.special.id
360
+
361
+ // Apply leader passive statuses
362
+ switch (team.leaderPassive) {
363
+ case 1:
364
+ // Sharpen blades - all allies gain 'sharp_blades' status
365
+ getAlive(team).forEach(x => {
366
+ x.statuses.push(PASSIVES[team.leaderPassive - 1])
367
+ })
368
+ break
369
+ case 2:
370
+ // Cloud of Zen - All enlightened allies get 'cloud_of_zen' status
371
+ getAlive(team).forEach(x => {
372
+ if (x.special.id === 2) x.statuses.push(PASSIVES[team.leaderPassive - 1])
373
+ })
374
+ break
375
+ case 3:
376
+ // Frenzy - all allies get 'frenzy' status
377
+ getAlive(team).forEach(x => {
378
+ x.statuses.push(PASSIVES[team.leaderPassive - 1])
379
+ })
380
+ break
381
+ case 4:
382
+ // All allies get 'fortify' status
383
+ getAlive(team).forEach(x => {
384
+ x.statuses.push(PASSIVES[team.leaderPassive - 1])
385
+ })
386
+
387
+ break
388
+ case 5:
389
+ // Spread the fear - all allies get 'spread_the_fear' status
390
+ getAlive(team).forEach(x => {
391
+ x.statuses.push(PASSIVES[team.leaderPassive - 1])
392
+ })
393
+ break
394
+ case 6:
395
+ // Cleansing aura - every healer ally and every tank ally gets 'cleansing_aura' status
396
+ getAlive(team).forEach(x => {
397
+ if (x.special.id === 6 || x.special.id === 4) x.statuses.push(PASSIVES[team.leaderPassive - 1])
398
+ })
399
+ break
400
+ case 7:
401
+ // All allies get 'channel_the_coven' status
402
+ getAlive(team).forEach(x => {
403
+ x.statuses.push(PASSIVES[team.leaderPassive - 1])
404
+ })
405
+ break
406
+ case 8:
407
+ // All allies get 'clan_momentum' status
408
+ getAlive(team).forEach(x => {
409
+ x.statuses.push(PASSIVES[team.leaderPassive - 1])
410
+ })
411
+ break
412
+ }
413
+ }
414
+
415
+ const removeLeaderPassivesFromTeam = (team) => {
416
+ let statusesRemoved = []
417
+ if (!team.leaderPassive) return statusesRemoved
418
+
419
+ // Remove leader passive statuses from team
420
+ getAlive(team).forEach(x => {
421
+ // add effects for each status removed
422
+ x.statuses.forEach(status => {
423
+ if (status === PASSIVES[team.leaderPassive - 1]) {
424
+ statusesRemoved.push({
425
+ target: x.id,
426
+ status: status
427
+ })
428
+ }
429
+ })
430
+
431
+ x.statuses = x.statuses.filter(x => x !== PASSIVES[team.leaderPassive - 1])
432
+ })
433
+
434
+ team.leaderPassive = null
435
+
436
+ return statusesRemoved
437
+ }
438
+
439
+ const getExpiredStatuses = (team1, team2) => {
440
+ // If leader is dead, remove leader passive
441
+ let statusesExpired = []
442
+ if (team1.leaderPassive && !getAlive(team1).find(x => x.id === team1.leader)) {
443
+ // Remove leader passive statuses
444
+ statusesExpired = removeLeaderPassivesFromTeam(team1)
445
+ }
446
+ if (team2.leaderPassive && !getAlive(team2).find(x => x.id === team2.leader)) {
447
+ // Remove leader passive statuses
448
+ statusesExpired = removeLeaderPassivesFromTeam(team2)
449
+ }
450
+
451
+ return statusesExpired
452
+ }
453
+
454
+ /**
455
+ * Add a status to a gotchi
456
+ * @param {Object} gotchi An in-game gotchi object
457
+ * @param {String} status The status to add
458
+ * @returns {Boolean} success A boolean to determine if the status was added
459
+ **/
460
+ const addStatusToGotchi = (gotchi, status) => {
461
+ // Check that gotchi doesn't already have max number of statuses
462
+ if (gotchi.statuses.filter(item => item === status).length >= MULTS.MAX_STATUSES) return false
463
+
464
+ gotchi.statuses.push(status)
465
+
466
+ return true
467
+ }
468
+
469
+ const scrambleGotchiIds = (allAliveGotchis, team1, team2) => {
470
+ // check there's no duplicate gotchis
471
+ const gotchiIds = allAliveGotchis.map(x => x.id)
472
+
473
+ if (gotchiIds.length !== new Set(gotchiIds).size) {
474
+ // scramble gotchi ids
475
+ allAliveGotchis.forEach(x => {
476
+ const newId = Math.floor(Math.random() * 10000000)
477
+
478
+ // find gotchi in team1 or team2
479
+ const position = getFormationPosition(team1, team2, x.id)
480
+
481
+ // change gotchi id
482
+ if (position) {
483
+ if (position.team === 1) {
484
+ if (x.id === team1.leader) team1.leader = newId
485
+ team1.formation[position.row][position.position].id = newId
486
+ } else {
487
+ if (x.id === team2.leader) team2.leader = newId
488
+ team2.formation[position.row][position.position].id = newId
489
+ }
490
+ } else {
491
+ throw new Error('Gotchi not found in team1 or team2')
492
+ }
493
+ })
494
+
495
+ // check again
496
+ const newGotchiIds = allAliveGotchis.map(x => x.id)
497
+ if (newGotchiIds.length !== new Set(newGotchiIds).size) {
498
+ // Scramble again
499
+ scrambleGotchiIds(allAliveGotchis, team1, team2)
500
+ }
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Prepare teams for battle
506
+ * @param {Array} allAliveGotchis An array of all alive gotchis
507
+ * @param {Object} team1 An in-game team object
508
+ * @param {Object} team2 An in-game team object
509
+ **/
510
+ const prepareTeams = (allAliveGotchis, team1, team2) => {
511
+ // check there's no duplicate gotchis
512
+ scrambleGotchiIds(allAliveGotchis, team1, team2);
513
+
514
+ // Apply stat items
515
+ applyStatItems(allAliveGotchis)
516
+
517
+ allAliveGotchis.forEach(x => {
518
+ // Add statuses property to all gotchis
519
+ x.statuses = []
520
+
521
+ // Calculate initial action delay for all gotchis
522
+ x.actionDelay = calculateActionDelay(x)
523
+
524
+ // Calculate attack type
525
+ x.attack = x.magic > x.physical ? 'magic' : 'physical'
526
+
527
+ // Add original stats to all gotchis
528
+ // Do a deep copy of the gotchi object to avoid modifying the original object
529
+ x.originalStats = JSON.parse(JSON.stringify(x))
530
+
531
+ // Add environmentEffects to all gotchis
532
+ x.environmentEffects = []
533
+ })
534
+
535
+ // Add leader passive to team
536
+ addLeaderToTeam(team1)
537
+ addLeaderToTeam(team2)
538
+ }
539
+
540
+ /**
541
+ * Get log gotchi object for battle logs
542
+ * @param {Array} allAliveGotchis An array of all alive gotchis
543
+ * @returns {Array} logGotchis An array of gotchi objects for logs
544
+ */
545
+ const getLogGotchis = (allAliveGotchis) => {
546
+ const logGotchis = JSON.parse(JSON.stringify(allAliveGotchis))
547
+
548
+ logGotchis.forEach(x => {
549
+ // Change gotchi.special.class to gotchi.special.gotchiClass to avoid conflicts with class keyword
550
+ x.special.gotchiClass = x.special.class
551
+
552
+ // Remove unnecessary properties to reduce log size
553
+ delete x.special.class
554
+ delete x.actionDelay
555
+ delete x.attack
556
+ delete x.originalStats
557
+ delete x.environmentEffects
558
+ })
559
+
560
+ return logGotchis
561
+ }
562
+
563
+ /**
564
+ * Apply stat items to gotchis
565
+ * @param {Array} gotchis An array of gotchis
566
+ */
567
+ const applyStatItems = (gotchis) => {
568
+ gotchis.forEach(gotchi => {
569
+ // Apply stat items
570
+ if (gotchi.item && gotchi.item.stat && gotchi.item.statValue) {
571
+ gotchi[gotchi.item.stat] += gotchi.item.statValue
572
+ }
573
+ })
574
+ }
575
+
576
+ module.exports = {
577
+ getAlive,
578
+ getFormationPosition,
579
+ getLeaderGotchi,
580
+ getNextToAct,
581
+ getTarget,
582
+ getDamage,
583
+ getModifiedStats,
584
+ calculateActionDelay,
585
+ getNewActionDelay,
586
+ simplifyTeam,
587
+ getUiOrder,
588
+ addLeaderToTeam,
589
+ removeLeaderPassivesFromTeam,
590
+ getExpiredStatuses,
591
+ addStatusToGotchi,
592
+ scrambleGotchiIds,
593
+ prepareTeams,
594
+ getLogGotchis,
595
+ applyStatItems
594
596
  }