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,628 @@
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
+ let modifier = gotchi[stat] * BUFF_MULT_EFFECTS[status][stat]
223
+
224
+ if (['speed', 'health', 'armor', 'resist', 'magic', 'physical', 'accuracy'].includes(stat)) {
225
+ modifier = Math.round(modifier)
226
+ } else {
227
+ // Round to 2 decimal places
228
+ modifier = Math.round((modifier) * 100) / 100
229
+ }
230
+
231
+ statusStatMods[stat] = modifier
232
+ })
233
+ }
234
+
235
+ // apply any modifier from BUFF_FLAT_EFFECTS
236
+ if (BUFF_FLAT_EFFECTS[status]) {
237
+ Object.keys(BUFF_FLAT_EFFECTS[status]).forEach(stat => {
238
+ if (statusStatMods[stat]) {
239
+ // If a mod for this status already exists, only add if the new mod is greater
240
+ if (BUFF_FLAT_EFFECTS[status][stat] > statusStatMods[stat]) statusStatMods[stat] = BUFF_FLAT_EFFECTS[status][stat]
241
+ } else {
242
+ statusStatMods[stat] = BUFF_FLAT_EFFECTS[status][stat]
243
+ }
244
+ })
245
+ }
246
+
247
+ // apply any modifier from DEBUFF_MULT_EFFECTS
248
+ if (DEBUFF_MULT_EFFECTS[status]) {
249
+ Object.keys(DEBUFF_MULT_EFFECTS[status]).forEach(stat => {
250
+ let modifier = gotchi[stat] * DEBUFF_MULT_EFFECTS[status][stat]
251
+
252
+ if (['speed', 'health', 'armor', 'resist', 'magic', 'physical', 'accuracy'].includes(stat)) {
253
+ modifier = Math.round(modifier)
254
+ } else {
255
+ // Round to 2 decimal places
256
+ modifier = Math.round((modifier) * 100) / 100
257
+ }
258
+
259
+ statusStatMods[stat] = -modifier
260
+ })
261
+ }
262
+
263
+ // apply any modifier from DEBUFF_FLAT_EFFECTS
264
+ if (DEBUFF_FLAT_EFFECTS[status]) {
265
+ Object.keys(DEBUFF_FLAT_EFFECTS[status]).forEach(stat => {
266
+ if (statusStatMods[stat]) {
267
+ // If a mod for this status already exists, only add if the new mod is greater
268
+ if (DEBUFF_FLAT_EFFECTS[status][stat] < statusStatMods[stat]) statusStatMods[stat] = DEBUFF_FLAT_EFFECTS[status][stat]
269
+ } else {
270
+ statusStatMods[stat] = -DEBUFF_FLAT_EFFECTS[status][stat]
271
+ }
272
+ })
273
+ }
274
+
275
+ // apply status mods
276
+ Object.keys(statusStatMods).forEach(stat => {
277
+ statMods[stat] = statMods[stat] ? statMods[stat] + statusStatMods[stat] : statusStatMods[stat]
278
+ })
279
+ })
280
+
281
+ const modifiedGotchi = {
282
+ ...gotchi
283
+ }
284
+
285
+ // apply stat mods
286
+ Object.keys(statMods).forEach(stat => {
287
+ if (statMods[stat] < 0) {
288
+ modifiedGotchi[stat] = modifiedGotchi[stat] + statMods[stat] < 0 ? 0 : modifiedGotchi[stat] + statMods[stat]
289
+ } else {
290
+ modifiedGotchi[stat] += statMods[stat]
291
+ }
292
+
293
+ })
294
+
295
+ // Recalculate attack type
296
+ modifiedGotchi.attack = modifiedGotchi.magic > modifiedGotchi.physical ? 'magic' : 'physical'
297
+
298
+ return modifiedGotchi
299
+ }
300
+
301
+ const calculateActionDelay = (gotchi) => {
302
+ // Calculate action delay and round to 3 decimal places
303
+ return Math.round(((100 / getModifiedStats(gotchi).speed) + Number.EPSILON) * 1000) / 1000
304
+ }
305
+
306
+ const getNewActionDelay = (gotchi) => {
307
+ // Calculate new action delay and round to 3 decimal places
308
+ return Math.round((gotchi.actionDelay + calculateActionDelay(gotchi) + Number.EPSILON) * 1000) / 1000
309
+ }
310
+
311
+ /**
312
+ * Simplify a team object for storage
313
+ * @param {Object} team An in-game team object
314
+ * @returns {Object} simplifiedTeam A simplified team object
315
+ */
316
+ const simplifyTeam = (team) => {
317
+ return {
318
+ name: team.name,
319
+ owner: team.owner,
320
+ leaderId: team.leader,
321
+ rows: [
322
+ {
323
+ slots: team.formation.front.map((x) => {
324
+ return {
325
+ isActive: x ? true : false,
326
+ id: x ? x.id : null
327
+ }
328
+ })
329
+ },
330
+ {
331
+ slots: team.formation.back.map((x) => {
332
+ return {
333
+ isActive: x ? true : false,
334
+ id: x ? x.id : null
335
+ }
336
+ })
337
+ }
338
+ ],
339
+ uiOrder: getUiOrder(team)
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Get the UI order of a team (used for the front end)
345
+ * @param {Object} team An in-game team object
346
+ * @returns {Array} uiOrder An array of gotchi ids in the order they should be displayed
347
+ **/
348
+ const getUiOrder = (team) => {
349
+ const uiOrder = []
350
+
351
+ if (team.formation.front[0]) uiOrder.push(team.formation.front[0].id)
352
+ if (team.formation.back[0]) uiOrder.push(team.formation.back[0].id)
353
+ if (team.formation.front[1]) uiOrder.push(team.formation.front[1].id)
354
+ if (team.formation.back[1]) uiOrder.push(team.formation.back[1].id)
355
+ if (team.formation.front[2]) uiOrder.push(team.formation.front[2].id)
356
+ if (team.formation.back[2]) uiOrder.push(team.formation.back[2].id)
357
+ if (team.formation.front[3]) uiOrder.push(team.formation.front[3].id)
358
+ if (team.formation.back[3]) uiOrder.push(team.formation.back[3].id)
359
+ if (team.formation.front[4]) uiOrder.push(team.formation.front[4].id)
360
+ if (team.formation.back[4]) uiOrder.push(team.formation.back[4].id)
361
+
362
+ return uiOrder
363
+ }
364
+
365
+ /**
366
+ * Add the leader statuses to a team
367
+ * @param {Object} team An in-game team object
368
+ **/
369
+ const addLeaderToTeam = (team) => {
370
+ // Add passive leader abilities
371
+ const teamLeader = getLeaderGotchi(team)
372
+
373
+ team.leaderPassive = teamLeader.special.id
374
+
375
+ // Apply leader passive statuses
376
+ switch (team.leaderPassive) {
377
+ case 1:
378
+ // Sharpen blades - all allies gain 'sharp_blades' status
379
+ getAlive(team).forEach(x => {
380
+ x.statuses.push(PASSIVES[team.leaderPassive - 1])
381
+ })
382
+ break
383
+ case 2:
384
+ // Cloud of Zen - all allies get 'cloud_of_zen' status
385
+ getAlive(team).forEach(x => {
386
+ x.statuses.push(PASSIVES[team.leaderPassive - 1])
387
+ })
388
+ break
389
+ case 3:
390
+ // Frenzy - all allies get 'frenzy' status
391
+ getAlive(team).forEach(x => {
392
+ x.statuses.push(PASSIVES[team.leaderPassive - 1])
393
+ })
394
+ break
395
+ case 4:
396
+ // All allies get 'fortify' status
397
+ getAlive(team).forEach(x => {
398
+ x.statuses.push(PASSIVES[team.leaderPassive - 1])
399
+ })
400
+
401
+ break
402
+ case 5:
403
+ // Spread the fear - all allies get 'spread_the_fear' status
404
+ getAlive(team).forEach(x => {
405
+ x.statuses.push(PASSIVES[team.leaderPassive - 1])
406
+ })
407
+ break
408
+ case 6:
409
+ // Cleansing aura - all allies get 'cleansing_aura' status
410
+ getAlive(team).forEach(x => {
411
+ x.statuses.push(PASSIVES[team.leaderPassive - 1])
412
+ })
413
+ break
414
+ case 7:
415
+ // All allies get 'channel_the_coven' status
416
+ getAlive(team).forEach(x => {
417
+ x.statuses.push(PASSIVES[team.leaderPassive - 1])
418
+ })
419
+ break
420
+ case 8:
421
+ // All allies get 'clan_momentum' status
422
+ getAlive(team).forEach(x => {
423
+ x.statuses.push(PASSIVES[team.leaderPassive - 1])
424
+ })
425
+ break
426
+ }
427
+ }
428
+
429
+ const removeLeaderPassivesFromTeam = (team) => {
430
+ let statusesRemoved = []
431
+ if (!team.leaderPassive) return statusesRemoved
432
+
433
+ // Remove leader passive statuses from team
434
+ getAlive(team).forEach(x => {
435
+ // add effects for each status removed
436
+ x.statuses.forEach(status => {
437
+ if (status === PASSIVES[team.leaderPassive - 1]) {
438
+ statusesRemoved.push({
439
+ target: x.id,
440
+ status: status
441
+ })
442
+ }
443
+ })
444
+
445
+ x.statuses = x.statuses.filter(x => x !== PASSIVES[team.leaderPassive - 1])
446
+ })
447
+
448
+ team.leaderPassive = null
449
+
450
+ return statusesRemoved
451
+ }
452
+
453
+ const getExpiredStatuses = (team1, team2) => {
454
+ // If leader is dead, remove leader passive
455
+ let statusesExpired = []
456
+ if (team1.leaderPassive && !getAlive(team1).find(x => x.id === team1.leader)) {
457
+ // Remove leader passive statuses
458
+ statusesExpired = removeLeaderPassivesFromTeam(team1)
459
+ }
460
+ if (team2.leaderPassive && !getAlive(team2).find(x => x.id === team2.leader)) {
461
+ // Remove leader passive statuses
462
+ statusesExpired = removeLeaderPassivesFromTeam(team2)
463
+ }
464
+
465
+ return statusesExpired
466
+ }
467
+
468
+ /**
469
+ * Add a status to a gotchi
470
+ * @param {Object} gotchi An in-game gotchi object
471
+ * @param {String} status The status to add
472
+ * @returns {Boolean} success A boolean to determine if the status was added
473
+ **/
474
+ const addStatusToGotchi = (gotchi, status) => {
475
+
476
+ const numOfStatus = gotchi.statuses.filter(item => item === status).length
477
+
478
+ // Check that gotchi doesn't already have max number of statuses
479
+ if (numOfStatus >= MULTS.MAX_STATUSES) return false
480
+
481
+ gotchi.statuses.push(status)
482
+
483
+ return true
484
+ }
485
+
486
+ const scrambleGotchiIds = (allAliveGotchis, team1, team2) => {
487
+ // check there's no duplicate gotchis
488
+ const gotchiIds = allAliveGotchis.map(x => x.id)
489
+
490
+ if (gotchiIds.length !== new Set(gotchiIds).size) {
491
+ // scramble gotchi ids
492
+ allAliveGotchis.forEach(x => {
493
+ const newId = Math.floor(Math.random() * 10000000)
494
+
495
+ // find gotchi in team1 or team2
496
+ const position = getFormationPosition(team1, team2, x.id)
497
+
498
+ // change gotchi id
499
+ if (position) {
500
+ if (position.team === 1) {
501
+ if (x.id === team1.leader) team1.leader = newId
502
+ team1.formation[position.row][position.position].id = newId
503
+ } else {
504
+ if (x.id === team2.leader) team2.leader = newId
505
+ team2.formation[position.row][position.position].id = newId
506
+ }
507
+ } else {
508
+ throw new Error('Gotchi not found in team1 or team2')
509
+ }
510
+ })
511
+
512
+ // check again
513
+ const newGotchiIds = allAliveGotchis.map(x => x.id)
514
+ if (newGotchiIds.length !== new Set(newGotchiIds).size) {
515
+ // Scramble again
516
+ scrambleGotchiIds(allAliveGotchis, team1, team2)
517
+ }
518
+ }
519
+ }
520
+
521
+ /**
522
+ * Prepare teams for battle
523
+ * @param {Array} allAliveGotchis An array of all alive gotchis
524
+ * @param {Object} team1 An in-game team object
525
+ * @param {Object} team2 An in-game team object
526
+ **/
527
+ const prepareTeams = (allAliveGotchis, team1, team2) => {
528
+ // check there's no duplicate gotchis
529
+ scrambleGotchiIds(allAliveGotchis, team1, team2)
530
+
531
+ // Apply stat items
532
+ applyStatItems(allAliveGotchis)
533
+
534
+ allAliveGotchis.forEach(x => {
535
+ // Add statuses property to all gotchis
536
+ x.statuses = []
537
+
538
+ // Calculate initial action delay for all gotchis
539
+ x.actionDelay = calculateActionDelay(x)
540
+
541
+ // Calculate attack type
542
+ x.attack = x.magic > x.physical ? 'magic' : 'physical'
543
+
544
+ // Add original stats to all gotchis
545
+ // Do a deep copy of the gotchi object to avoid modifying the original object
546
+ x.originalStats = JSON.parse(JSON.stringify(x))
547
+
548
+ // Add environmentEffects to all gotchis
549
+ x.environmentEffects = []
550
+ })
551
+
552
+ // Add leader passive to team
553
+ addLeaderToTeam(team1)
554
+ addLeaderToTeam(team2)
555
+ }
556
+
557
+ /**
558
+ * Get log gotchi object for battle logs
559
+ * @param {Array} allAliveGotchis An array of all alive gotchis
560
+ * @returns {Array} logGotchis An array of gotchi objects for logs
561
+ */
562
+ const getLogGotchis = (allAliveGotchis) => {
563
+ const logGotchis = JSON.parse(JSON.stringify(allAliveGotchis))
564
+
565
+ logGotchis.forEach(x => {
566
+ // Change gotchi.special.class to gotchi.special.gotchiClass to avoid conflicts with class keyword
567
+ x.special.gotchiClass = x.special.class
568
+
569
+ // Remove unnecessary properties to reduce log size
570
+ delete x.special.class
571
+ delete x.actionDelay
572
+ delete x.attack
573
+ delete x.originalStats
574
+ delete x.environmentEffects
575
+ })
576
+
577
+ return logGotchis
578
+ }
579
+
580
+ /**
581
+ * Apply stat items to gotchis
582
+ * @param {Array} gotchis An array of gotchis
583
+ */
584
+ const applyStatItems = (gotchis) => {
585
+ gotchis.forEach(gotchi => {
586
+ // Apply stat items
587
+ if (gotchi.item && gotchi.item.stat && gotchi.item.statValue) {
588
+ gotchi[gotchi.item.stat] += gotchi.item.statValue
589
+ }
590
+ })
591
+ }
592
+
593
+ /**
594
+ * Remove stat items from gotchis
595
+ * This is used when replaying a battle from logs where the stat items have already been applied
596
+ * @param {Array} gotchis An array of gotchis
597
+ */
598
+ const removeStatItems = (gotchis) => {
599
+ gotchis.forEach(gotchi => {
600
+ // Remove stat items
601
+ if (gotchi.item && gotchi.item.stat && gotchi.item.statValue) {
602
+ gotchi[gotchi.item.stat] -= gotchi.item.statValue
603
+ }
604
+ })
605
+ }
606
+
607
+ module.exports = {
608
+ getAlive,
609
+ getFormationPosition,
610
+ getLeaderGotchi,
611
+ getNextToAct,
612
+ getTarget,
613
+ getDamage,
614
+ getModifiedStats,
615
+ calculateActionDelay,
616
+ getNewActionDelay,
617
+ simplifyTeam,
618
+ getUiOrder,
619
+ addLeaderToTeam,
620
+ removeLeaderPassivesFromTeam,
621
+ getExpiredStatuses,
622
+ addStatusToGotchi,
623
+ scrambleGotchiIds,
624
+ prepareTeams,
625
+ getLogGotchis,
626
+ applyStatItems,
627
+ removeStatItems
628
+ }