gotchi-battler-game-logic 2.0.6 → 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.
- package/.vscode/settings.json +4 -4
- package/Dockerfile +9 -9
- package/README.md +49 -49
- package/cloudbuild.yaml +27 -27
- package/constants/tournamentManagerAbi.json +208 -208
- package/game-logic/index.js +6 -6
- package/game-logic/v1.4/constants.js +114 -114
- package/game-logic/v1.4/index.js +1366 -1366
- package/game-logic/v1.6/constants.js +123 -123
- package/game-logic/v1.6/index.js +1406 -1406
- package/game-logic/v1.7/constants.js +142 -140
- package/game-logic/v1.7/helpers.js +595 -593
- package/game-logic/v1.7/index.js +802 -795
- package/index.js +12 -12
- package/package.json +26 -26
- package/schemas/team.json +349 -343
- package/scripts/balancing/createCSV.js +126 -126
- package/scripts/balancing/fixTrainingGotchis.js +155 -259
- package/scripts/balancing/processSims.js +229 -229
- package/scripts/balancing/sims.js +278 -278
- package/scripts/balancing/v1.7/class_combos.js +43 -43
- package/scripts/balancing/v1.7/setTeamPositions.js +105 -105
- package/scripts/balancing/v1.7/training_gotchis.json +20161 -20161
- package/scripts/balancing/v1.7/trait_combos.json +9 -9
- package/scripts/balancing/v1.7.1/class_combos.js +43 -43
- package/scripts/balancing/v1.7.1/setTeamPositions.js +122 -122
- package/scripts/balancing/v1.7.1/training_gotchis.json +22401 -22401
- package/scripts/balancing/v1.7.1/trait_combos.json +9 -9
- package/scripts/balancing/v1.7.2/class_combos.js +44 -0
- package/scripts/balancing/v1.7.2/setTeamPositions.js +122 -0
- package/scripts/balancing/v1.7.2/training_gotchis.json +22402 -0
- package/scripts/balancing/v1.7.2/trait_combos.json +10 -0
- package/scripts/data/team1.json +213 -213
- package/scripts/data/team2.json +200 -200
- package/scripts/data/tournaments.json +66 -66
- package/scripts/{runBattle.js → runLocalBattle.js} +18 -18
- package/scripts/runRealBattle.js +52 -0
- package/scripts/simRealBattle.js +121 -0
- package/scripts/validateBattle.js +74 -70
- package/scripts/validateTournament.js +101 -101
- package/utils/contracts.js +12 -12
- package/utils/errors.js +29 -29
- package/utils/mapGotchi.js +119 -0
- package/utils/transforms.js +89 -88
- package/utils/validations.js +39 -39
|
@@ -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 -
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
*
|
|
456
|
-
* @
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
gotchi.statuses.
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
*
|
|
506
|
-
* @param {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
allAliveGotchis
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
*
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
logGotchis.
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
delete x.
|
|
554
|
-
delete x.
|
|
555
|
-
delete x.
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
}
|