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,713 @@
1
+ const STATUSES = require('./statuses.json')
2
+
3
+ const getTeamGotchis = (team) => {
4
+ return [...team.formation.front, ...team.formation.back].filter(x => x)
5
+ }
6
+
7
+ // Get only alive gotchis in a team
8
+ const getAlive = (team, row) => {
9
+ if (row) {
10
+ return team.formation[row].filter(x => x).filter(x => x.health > 0)
11
+ }
12
+
13
+ return [...team.formation.front, ...team.formation.back].filter(x => x).filter(x => x.health > 0)
14
+ }
15
+
16
+ /**
17
+ * Get the formation position of a gotchi
18
+ * @param {Object} team1 An in-game team object
19
+ * @param {Object} team2 An in-game team object
20
+ * @param {Number} gotchiId The id of the gotchi
21
+ * @returns {Object} position The formation position of the gotchi
22
+ * @returns {Number} position.team The team the gotchi is on
23
+ * @returns {String} position.row The row the gotchi is on
24
+ * @returns {Number} position.position The position of the gotchi in the row
25
+ * @returns {null} position null if the gotchi is not found
26
+ **/
27
+ const getFormationPosition = (team1, team2, gotchiId) => {
28
+ const team1FrontIndex = team1.formation.front.findIndex(x => x && x.id === gotchiId)
29
+
30
+ if (team1FrontIndex !== -1) return {
31
+ team: 1,
32
+ row: 'front',
33
+ position: team1FrontIndex,
34
+ name: team1.formation.front[team1FrontIndex].name
35
+ }
36
+
37
+ const team1BackIndex = team1.formation.back.findIndex(x => x && x.id === gotchiId)
38
+
39
+ if (team1BackIndex !== -1) return {
40
+ team: 1,
41
+ row: 'back',
42
+ position: team1BackIndex,
43
+ name: team1.formation.back[team1BackIndex].name
44
+ }
45
+
46
+ const team2FrontIndex = team2.formation.front.findIndex(x => x && x.id === gotchiId)
47
+
48
+ if (team2FrontIndex !== -1) return {
49
+ team: 2,
50
+ row: 'front',
51
+ position: team2FrontIndex,
52
+ name: team2.formation.front[team2FrontIndex].name
53
+ }
54
+
55
+ const team2BackIndex = team2.formation.back.findIndex(x => x && x.id === gotchiId)
56
+
57
+ if (team2BackIndex !== -1) return {
58
+ team: 2,
59
+ row: 'back',
60
+ position: team2BackIndex,
61
+ name: team2.formation.back[team2BackIndex].name
62
+ }
63
+
64
+ return null
65
+ }
66
+
67
+ /**
68
+ * Get the leader gotchi of a team
69
+ * @param {Object} team An in-game team object
70
+ * @returns {Object} gotchi The leader gotchi
71
+ **/
72
+ const getLeaderGotchi = (team) => {
73
+ const leader = [...team.formation.front, ...team.formation.back].find(x => x && x.id === team.leader)
74
+
75
+ if (!leader) throw new Error('Leader not found')
76
+
77
+ return leader
78
+ }
79
+
80
+ /**
81
+ * Get the next gotchi to act
82
+ * @param {Object} team1 An in-game team object
83
+ * @param {Object} team2 An in-game team object
84
+ * @param {Function} rng The random number generator
85
+ * @returns {Object} position The formation position of the gotchi
86
+ **/
87
+ const getNextToAct = (team1, team2, rng) => {
88
+ const aliveGotchis = [...getAlive(team1), ...getAlive(team2)]
89
+
90
+ aliveGotchis.sort((a, b) => a.actionDelay - b.actionDelay)
91
+
92
+ let toAct = aliveGotchis.filter(gotchi => gotchi.actionDelay === aliveGotchis[0].actionDelay)
93
+
94
+ // If only one gotchi can act then return it
95
+ if (toAct.length === 1) return getFormationPosition(team1, team2, toAct[0].id)
96
+
97
+ // Lowest speeds win tiebreaker
98
+ toAct.sort((a, b) => a.speed - b.speed)
99
+ toAct = toAct.filter(gotchi => gotchi.speed === toAct[0].speed)
100
+
101
+ // If only one gotchi can act then return it
102
+
103
+ if (toAct.length === 1) return getFormationPosition(team1, team2, toAct[0].id)
104
+
105
+ // If still tied then randomly choose
106
+ const randomIndex = Math.floor(rng() * toAct.length)
107
+
108
+ if (!toAct[randomIndex]) throw new Error(`No gotchi found at index ${randomIndex}`)
109
+
110
+ toAct = toAct[randomIndex]
111
+ return getFormationPosition(team1, team2, toAct.id)
112
+ }
113
+
114
+ const getTarget = (defendingTeam, rng) => {
115
+ // Check for taunt gotchis
116
+ const taunt = [...getAlive(defendingTeam, 'front'), ...getAlive(defendingTeam, 'back')].filter(gotchi => gotchi.statuses && gotchi.statuses.includes('taunt'))
117
+
118
+ if (taunt.length) {
119
+ if (taunt.length === 1) return taunt[0]
120
+
121
+ // If multiple taunt gotchis then randomly choose one
122
+ return taunt[Math.floor(rng() * taunt.length)]
123
+ }
124
+
125
+ // Target gotchis in the front row first
126
+ const frontRow = getAlive(defendingTeam, 'front')
127
+
128
+ if (frontRow.length) {
129
+ return frontRow[Math.floor(rng() * frontRow.length)]
130
+ }
131
+
132
+ // If no gotchis in front row then target back row
133
+ const backRow = getAlive(defendingTeam, 'back')
134
+
135
+ if (backRow.length) {
136
+ return backRow[Math.floor(rng() * backRow.length)]
137
+ }
138
+
139
+ throw new Error('No gotchis to target')
140
+ }
141
+
142
+ /**
143
+ * Get a target from a target code
144
+ * @param {String} targetCode The target code
145
+ * @param {Object} attackingGotchi The attacking gotchi
146
+ * @param {Array} attackingTeam The attacking team
147
+ * @param {Array} defendingTeam The defending team
148
+ * @param {Function} rng The random number generator
149
+ * @returns {Array} targets An array of targets
150
+ **/
151
+ const getTargetsFromCode = (targetCode, attackingGotchi, attackingTeam, defendingTeam, rng) => {
152
+ /**
153
+ * [
154
+ { "code": "self", "description": "The casting Gotchi itself" },
155
+ { "code": "enemy_random", "description": "Random enemy" },
156
+ { "code": "enemy_back_row", "description": "Random enemy in the back row" },
157
+ { "code": "enemy_front_row", "description": "Random enemy in the front row" },
158
+ { "code": "enemy_row_largest", "description": "Random enemy in the row with most enemies" },
159
+ { "code": "all_enemies", "description": "All enemies" },
160
+
161
+ { "code": "ally_random", "description": "Random ally" },
162
+ { "code": "ally_back_row", "description": "Random ally in the back row" },
163
+ { "code": "ally_front_row", "description": "Random ally in the front row" },
164
+ { "code": "ally_row_largest", "description": "Random ally in the row with most allies" },
165
+ { "code": "all_allies", "description": "All allies" },
166
+
167
+ { "code": "same_as_attack", "description": "Targets exactly the same units as the special attack did" },
168
+ { "code": "all", "description": "All Gotchis on the battlefield (allies and enemies)" }
169
+ ]
170
+ */
171
+
172
+ let targets = []
173
+
174
+ switch (targetCode) {
175
+ case 'self':
176
+ targets.push(attackingGotchi)
177
+ break
178
+ case 'enemy_random':
179
+ targets.push(getTarget(defendingTeam, rng))
180
+ break
181
+ case 'enemy_back_row':
182
+ if (getAlive(defendingTeam, 'back').length) {
183
+ targets.push(getAlive(defendingTeam, 'back')[Math.floor(rng() * getAlive(defendingTeam, 'back').length)])
184
+ } else {
185
+ targets.push(getTarget(defendingTeam, rng))
186
+ }
187
+ break
188
+ case 'enemy_front_row':
189
+ if (getAlive(defendingTeam, 'front').length) {
190
+ targets.push(getAlive(defendingTeam, 'front')[Math.floor(rng() * getAlive(defendingTeam, 'front').length)])
191
+ } else {
192
+ targets.push(getTarget(defendingTeam, rng))
193
+ }
194
+ break
195
+ case 'enemy_row_largest': {
196
+ const row = getAlive(defendingTeam, 'front').length > getAlive(defendingTeam, 'back').length ? 'front' : 'back'
197
+ targets = getAlive(defendingTeam, row)
198
+ break
199
+ }
200
+ case 'all_enemies':
201
+ targets = getAlive(defendingTeam)
202
+ break
203
+ case 'ally_random':
204
+ targets.push(getTarget(attackingTeam, rng))
205
+ break
206
+ case 'ally_back_row':
207
+ if (getAlive(attackingTeam, 'back').length) {
208
+ targets.push(getAlive(attackingTeam, 'back')[Math.floor(rng() * getAlive(attackingTeam, 'back').length)])
209
+ } else {
210
+ targets.push(getTarget(attackingTeam, rng))
211
+ }
212
+ break
213
+ case 'ally_front_row':
214
+ if (getAlive(attackingTeam, 'front').length) {
215
+ targets.push(getAlive(attackingTeam, 'front')[Math.floor(rng() * getAlive(attackingTeam, 'front').length)])
216
+ } else {
217
+ targets.push(getTarget(attackingTeam, rng))
218
+ }
219
+ break
220
+ case 'ally_row_largest': {
221
+ const row = getAlive(attackingTeam, 'front').length > getAlive(attackingTeam, 'back').length ? 'front' : 'back'
222
+ targets = getAlive(attackingTeam, row)
223
+ break
224
+ }
225
+ case 'all_allies':
226
+ targets = getAlive(attackingTeam)
227
+ break
228
+ case 'same_as_attack':
229
+ throw new Error('same_as_attack is not implemented in getTargetsFromCode')
230
+ case 'all':
231
+ targets = [...getAlive(attackingTeam), ...getAlive(defendingTeam)]
232
+ break
233
+ default:
234
+ throw new Error(`Invalid target code: ${targetCode}`)
235
+ }
236
+
237
+ return targets
238
+ }
239
+
240
+ /**
241
+ * Get the damage of an attack
242
+ * @param {Object} attackingGotchi The gotchi attacking
243
+ * @param {Object} defendingGotchi The gotchi defending
244
+ * @param {Number} multiplier The damage multiplier
245
+ * @returns {Number} damage The damage of the attack
246
+ **/
247
+ const getDamage = (attackingGotchi, defendingGotchi, multiplier) => {
248
+
249
+ // Apply any status effects
250
+ const modifiedAttackingGotchi = getModifiedStats(attackingGotchi)
251
+ const modifiedDefendingGotchi = getModifiedStats(defendingGotchi)
252
+
253
+ // Calculate damage
254
+ let damage = Math.round((modifiedAttackingGotchi.attack / modifiedDefendingGotchi.defense) * 100)
255
+
256
+ // Apply multiplier
257
+ if (multiplier) damage = Math.round(damage * multiplier)
258
+
259
+ // check for environment effects
260
+ if (defendingGotchi.environmentEffects && defendingGotchi.environmentEffects.length > 0) {
261
+ damage = Math.round(damage * (1 + (defendingGotchi.environmentEffects.length * 0.5)))
262
+ }
263
+
264
+ return damage
265
+ }
266
+
267
+ const getHealFromMultiplier = (healingGotchi, multiplier) => {
268
+
269
+ const modifiedHealingGotchi = getModifiedStats(healingGotchi)
270
+
271
+ const amountToHeal = Math.round(modifiedHealingGotchi.resist * multiplier)
272
+
273
+ return amountToHeal
274
+ }
275
+
276
+ /**
277
+ * Apply status effects to a gotchi
278
+ * @param {Object} gotchi An in-game gotchi object
279
+ * @returns {Object} gotchi An in-game gotchi object with modified stats
280
+ */
281
+ const getModifiedStats = (gotchi) => {
282
+ const statMods = {}
283
+
284
+ const decimalStats = ['criticalRate', 'criticalDamage']
285
+
286
+ gotchi.statuses.forEach(statusCode => {
287
+ const statusStatMods = {}
288
+ const status = getStatusByCode(statusCode)
289
+
290
+ // Check if status is a stat modifier
291
+ if (status.category !== 'stat_modifier') {
292
+ return
293
+ }
294
+
295
+ status.statModifiers.forEach(statModifier => {
296
+ let statChange = 0
297
+
298
+ if (statModifier.valueType === 'flat') {
299
+ statChange = statModifier.value
300
+ } else if (statModifier.valueType === 'percent') {
301
+ statChange = gotchi[statModifier.statName] * (statModifier.value / 100)
302
+ } else {
303
+ throw new Error(`Invalid value type for status ${statusCode}: ${statModifier.valueType}`)
304
+ }
305
+
306
+ if (decimalStats.includes(statModifier.statName)) {
307
+ statChange = Math.round(statChange * 100) / 100
308
+ } else {
309
+ statChange = Math.round(statChange)
310
+ }
311
+
312
+ if (statusStatMods[statModifier.statName]) {
313
+ statusStatMods[statModifier.statName] = statusStatMods[statModifier.statName] + statChange
314
+ } else {
315
+ statusStatMods[statModifier.statName] = statChange
316
+ }
317
+ })
318
+
319
+ // apply status mods
320
+ Object.keys(statusStatMods).forEach(stat => {
321
+ statMods[stat] = statMods[stat] ? statMods[stat] + statusStatMods[stat] : statusStatMods[stat]
322
+ })
323
+ })
324
+
325
+ const modifiedGotchi = {
326
+ ...gotchi
327
+ }
328
+
329
+ // apply stat mods
330
+ Object.keys(statMods).forEach(stat => {
331
+ if (statMods[stat] < 0) {
332
+ modifiedGotchi[stat] = modifiedGotchi[stat] + statMods[stat] < 0 ? 0 : modifiedGotchi[stat] + statMods[stat]
333
+ } else {
334
+ modifiedGotchi[stat] += statMods[stat]
335
+ }
336
+ })
337
+
338
+ // Enforce practical lower bounds for certain stats regardless of whether they were modified by statuses
339
+ if (modifiedGotchi.defense < 1) {
340
+ modifiedGotchi.defense = 1
341
+ }
342
+ if (modifiedGotchi.speed < 1) {
343
+ modifiedGotchi.speed = 1
344
+ }
345
+
346
+ return modifiedGotchi
347
+ }
348
+
349
+ const calculateActionDelay = (gotchi) => {
350
+ // Calculate action delay and round to 3 decimal places
351
+ return Math.round(((100 / getModifiedStats(gotchi).speed) + Number.EPSILON) * 1000) / 1000
352
+ }
353
+
354
+ const getNewActionDelay = (gotchi) => {
355
+ // Calculate new action delay and round to 3 decimal places
356
+ return Math.round((gotchi.actionDelay + calculateActionDelay(gotchi) + Number.EPSILON) * 1000) / 1000
357
+ }
358
+
359
+ /**
360
+ * Simplify a team object for storage
361
+ * @param {Object} team An in-game team object
362
+ * @returns {Object} simplifiedTeam A simplified team object
363
+ */
364
+ const simplifyTeam = (team) => {
365
+ return {
366
+ name: team.name,
367
+ owner: team.owner,
368
+ leaderId: team.leader,
369
+ rows: [
370
+ {
371
+ slots: team.formation.front.map((x) => {
372
+ return {
373
+ isActive: x ? true : false,
374
+ id: x ? x.id : null
375
+ }
376
+ })
377
+ },
378
+ {
379
+ slots: team.formation.back.map((x) => {
380
+ return {
381
+ isActive: x ? true : false,
382
+ id: x ? x.id : null
383
+ }
384
+ })
385
+ }
386
+ ],
387
+ uiOrder: getUiOrder(team)
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Get the UI order of a team (used for the front end)
393
+ * @param {Object} team An in-game team object
394
+ * @returns {Array} uiOrder An array of gotchi ids in the order they should be displayed
395
+ **/
396
+ const getUiOrder = (team) => {
397
+ const uiOrder = []
398
+
399
+ if (team.formation.front[0]) uiOrder.push(team.formation.front[0].id)
400
+ if (team.formation.back[0]) uiOrder.push(team.formation.back[0].id)
401
+ if (team.formation.front[1]) uiOrder.push(team.formation.front[1].id)
402
+ if (team.formation.back[1]) uiOrder.push(team.formation.back[1].id)
403
+ if (team.formation.front[2]) uiOrder.push(team.formation.front[2].id)
404
+ if (team.formation.back[2]) uiOrder.push(team.formation.back[2].id)
405
+ if (team.formation.front[3]) uiOrder.push(team.formation.front[3].id)
406
+ if (team.formation.back[3]) uiOrder.push(team.formation.back[3].id)
407
+ if (team.formation.front[4]) uiOrder.push(team.formation.front[4].id)
408
+ if (team.formation.back[4]) uiOrder.push(team.formation.back[4].id)
409
+
410
+ return uiOrder
411
+ }
412
+
413
+ /**
414
+ * Add the leader statuses to a team
415
+ * @param {Object} team An in-game team object
416
+ * @param {Boolean} addStatuses Whether to add the leader statuses to the team
417
+ **/
418
+ const addLeaderToTeam = (team, addStatuses) => {
419
+ if (!addStatuses) return
420
+
421
+ // Add passive leader abilities
422
+ const teamLeader = getLeaderGotchi(team)
423
+ const leaderskill = teamLeader.leaderSkillExpanded
424
+
425
+ if (!leaderskill || !leaderskill.statuses) return
426
+
427
+ leaderskill.statuses.forEach(leaderSkillStatus => {
428
+ getAlive(team).forEach(x => {
429
+ addStatusToGotchi(x, leaderSkillStatus.status, leaderSkillStatus.stackCount)
430
+ })
431
+ })
432
+ }
433
+
434
+ /**
435
+ * Add a status to a gotchi
436
+ * @param {Object} gotchi An in-game gotchi object
437
+ * @param {String} status The status to add
438
+ * @param {Integer} count The number of the status to add
439
+ * @returns {Boolean} success A boolean to determine if the status was added
440
+ **/
441
+ const addStatusToGotchi = (gotchi, status, count) => {
442
+ if (!count) count = 1
443
+
444
+ for (let i = 0; i < count; i++) {
445
+ gotchi.statuses.push(status)
446
+ }
447
+
448
+ return true
449
+ }
450
+
451
+ const scrambleGotchiIds = (allAliveGotchis, team1, team2) => {
452
+ // check there's no duplicate gotchis
453
+ const gotchiIds = allAliveGotchis.map(x => x.id)
454
+
455
+ if (gotchiIds.length !== new Set(gotchiIds).size) {
456
+ // scramble gotchi ids
457
+ allAliveGotchis.forEach(x => {
458
+ const newId = Math.floor(Math.random() * 10000000)
459
+
460
+ // find gotchi in team1 or team2
461
+ const position = getFormationPosition(team1, team2, x.id)
462
+
463
+ // change gotchi id
464
+ if (position) {
465
+ if (position.team === 1) {
466
+ if (x.id === team1.leader) team1.leader = newId
467
+ team1.formation[position.row][position.position].id = newId
468
+ } else {
469
+ if (x.id === team2.leader) team2.leader = newId
470
+ team2.formation[position.row][position.position].id = newId
471
+ }
472
+ } else {
473
+ throw new Error('Gotchi not found in team1 or team2')
474
+ }
475
+ })
476
+
477
+ // check again
478
+ const newGotchiIds = allAliveGotchis.map(x => x.id)
479
+ if (newGotchiIds.length !== new Set(newGotchiIds).size) {
480
+ // Scramble again
481
+ scrambleGotchiIds(allAliveGotchis, team1, team2)
482
+ }
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Prepare teams for battle
488
+ * @param {Array} allAliveGotchis An array of all alive gotchis
489
+ * @param {Object} team1 An in-game team object
490
+ * @param {Object} team2 An in-game team object
491
+ **/
492
+ const prepareTeams = (allAliveGotchis, team1, team2) => {
493
+ // check there's no duplicate gotchis
494
+ scrambleGotchiIds(allAliveGotchis, team1, team2)
495
+
496
+ // Apply stat items
497
+ applyStatItems(allAliveGotchis)
498
+
499
+ allAliveGotchis.forEach(x => {
500
+ // Add statuses property to all gotchis
501
+ x.statuses = []
502
+
503
+ // Calculate initial action delay for all gotchis
504
+ x.actionDelay = calculateActionDelay(x)
505
+
506
+ // Set special specialBar
507
+ // gotchi.specialBar is the % the special bar is full. 100% is full. 0% is empty.
508
+ // We split into 6 sections, so the initial specialBar is the number of sections to fill.
509
+ x.specialBar = Math.round((100/6) * (6 - x.specialExpanded.initialCooldown))
510
+
511
+ // Handle Health
512
+ // add fullHealth property to all gotchis
513
+ x.fullHealth = x.health
514
+
515
+ // Add environmentEffects to all gotchis
516
+ x.environmentEffects = []
517
+
518
+ // Add stats to all gotchis
519
+ x.stats = {
520
+ dmgGiven: 0,
521
+ dmgReceived: 0,
522
+ healGiven: 0,
523
+ healReceived: 0,
524
+ crits: 0,
525
+ resists: 0,
526
+ focuses: 0,
527
+ counters: 0,
528
+ hits: 0
529
+ }
530
+ })
531
+
532
+ const teams = [team1, team2]
533
+
534
+ teams.forEach(team => {
535
+ if (team.startingState && team.startingState.length) {
536
+ team.startingState.forEach(gotchiState => {
537
+ // Find gotchi in allAliveGotchis
538
+ const gotchi = allAliveGotchis.find(x => x.id === gotchiState.id)
539
+
540
+ if (!gotchi) {
541
+ throw new Error(`Gotchi with id ${gotchiState.id} not found in allAliveGotchis`)
542
+ }
543
+
544
+ // Set Health and statuses
545
+ gotchi.health = gotchiState.health
546
+ gotchi.statuses = gotchiState.statuses
547
+ })
548
+
549
+ // Don't add leader passive statuses if we have a starting state
550
+ addLeaderToTeam(team, false)
551
+ } else {
552
+ // Add leader passives to team
553
+ addLeaderToTeam(team, true)
554
+ }
555
+ })
556
+ }
557
+
558
+ /**
559
+ * Get log gotchi object for battle logs
560
+ * @param {Array} allAliveGotchis An array of all alive gotchis
561
+ * @returns {Array} logGotchis An array of gotchi objects for logs
562
+ */
563
+ const getLogGotchis = (allAliveGotchis) => {
564
+ const logGotchis = JSON.parse(JSON.stringify(allAliveGotchis))
565
+
566
+ logGotchis.forEach(x => {
567
+ // Remove unnecessary properties to reduce log size
568
+ delete x.actionDelay
569
+ delete x.environmentEffects
570
+ delete x.stats
571
+ delete x.createdAt
572
+ delete x.updatedAt
573
+ })
574
+
575
+ return logGotchis
576
+ }
577
+
578
+ /**
579
+ * Apply stat items to gotchis
580
+ * @param {Array} gotchis An array of gotchis
581
+ */
582
+ const applyStatItems = (gotchis) => {
583
+ gotchis.forEach(gotchi => {
584
+ // Apply stat items
585
+ if (gotchi.item && gotchi.item.stat && gotchi.item.statValue) {
586
+ gotchi[gotchi.item.stat] += gotchi.item.statValue
587
+ }
588
+ })
589
+ }
590
+
591
+ /**
592
+ * Remove stat items from gotchis
593
+ * This is used when replaying a battle from logs where the stat items have already been applied
594
+ * @param {Array} gotchis An array of gotchis
595
+ */
596
+ const removeStatItems = (gotchis) => {
597
+ gotchis.forEach(gotchi => {
598
+ // Remove stat items
599
+ if (gotchi.item && gotchi.item.stat && gotchi.item.statValue) {
600
+ gotchi[gotchi.item.stat] -= gotchi.item.statValue
601
+ }
602
+ })
603
+ }
604
+
605
+ const getTeamStats = (team) => {
606
+ const teamStats = {}
607
+
608
+ const gotchis = getTeamGotchis(team)
609
+
610
+ gotchis.forEach(gotchi => {
611
+ Object.keys(gotchi.stats).forEach(stat => {
612
+ if (!teamStats[stat]) teamStats[stat] = 0
613
+ teamStats[stat] += gotchi.stats[stat]
614
+ })
615
+ })
616
+
617
+ return {
618
+ ...teamStats,
619
+ gotchis: gotchis.map(gotchi => {
620
+ return {
621
+ id: gotchi.id,
622
+ name: gotchi.name,
623
+ ...gotchi.stats
624
+ }
625
+ })
626
+ }
627
+ }
628
+
629
+ const getStatusByCode = (statusCode) => {
630
+ const status = STATUSES.find(status => status.code === statusCode)
631
+
632
+ if (!status) {
633
+ throw new Error(`Status with code ${statusCode} not found`)
634
+ }
635
+
636
+ return status
637
+ }
638
+
639
+ const getTeamSpecialBars = (team1, team2) => {
640
+ const specialBars = []
641
+
642
+ for (const gotchi of [...getTeamGotchis(team1), ...getTeamGotchis(team2)]) {
643
+ specialBars.push({
644
+ id: gotchi.id,
645
+ val: gotchi.specialBar
646
+ })
647
+ }
648
+
649
+ return specialBars
650
+ }
651
+
652
+ const focusCheck = (attackingTeam, attackingGotchi, targetGotchi, rng) => {
653
+ const modifiedAttackingGotchi = getModifiedStats(attackingGotchi)
654
+ const modifiedTargetGotchi = getModifiedStats(targetGotchi)
655
+
656
+ const attackingTeamGotchis = getTeamGotchis(attackingTeam)
657
+ // If the attacking gotchi is on the same team as the defending gotchi then always return true
658
+ if (attackingTeamGotchis.find(gotchi => gotchi.id === targetGotchi.id)) {
659
+ return true
660
+ } else {
661
+ // Status apply chance is clamp(0.5 + (FOC - RES) / 200, 0.15, 0.95)
662
+ const chance = Math.max(Math.min(0.5 + (modifiedAttackingGotchi.focus - modifiedTargetGotchi.resist) / 200, 0.95), 0.15)
663
+
664
+ const result = rng() < chance
665
+
666
+ if (result) {
667
+ // if attacking gotchi has beaten the focus check then add to stats
668
+ attackingGotchi.stats.focuses++
669
+ } else {
670
+ targetGotchi.stats.resists++
671
+ }
672
+
673
+ return result
674
+ }
675
+ }
676
+
677
+ const getCritMultiplier = (gotchi, rng) => {
678
+ const modifiedGotchi = getModifiedStats(gotchi)
679
+ const isCrit = rng() < Math.max(Math.min(modifiedGotchi.criticalRate / 100, 1), 0.05)
680
+ if (isCrit) {
681
+ return (modifiedGotchi.criticalDamage / 100) + 1
682
+ }
683
+ return 1
684
+ }
685
+
686
+ module.exports = {
687
+ getTeamGotchis,
688
+ getAlive,
689
+ getFormationPosition,
690
+ getLeaderGotchi,
691
+ getNextToAct,
692
+ getTarget,
693
+ getTargetsFromCode,
694
+ getDamage,
695
+ getHealFromMultiplier,
696
+ getModifiedStats,
697
+ calculateActionDelay,
698
+ getNewActionDelay,
699
+ simplifyTeam,
700
+ getUiOrder,
701
+ addLeaderToTeam,
702
+ addStatusToGotchi,
703
+ scrambleGotchiIds,
704
+ prepareTeams,
705
+ getLogGotchis,
706
+ applyStatItems,
707
+ removeStatItems,
708
+ getTeamStats,
709
+ getStatusByCode,
710
+ getTeamSpecialBars,
711
+ focusCheck,
712
+ getCritMultiplier
713
+ }