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
package/game-logic/v1.4/index.js
CHANGED
|
@@ -1,1367 +1,1367 @@
|
|
|
1
|
-
const seedrandom = require('seedrandom')
|
|
2
|
-
const ZSchema = require('z-schema')
|
|
3
|
-
const validator = new ZSchema()
|
|
4
|
-
const { GameError } = require('../../utils/errors')
|
|
5
|
-
|
|
6
|
-
let {
|
|
7
|
-
PASSIVES,
|
|
8
|
-
BUFF_MULT_EFFECTS,
|
|
9
|
-
BUFF_FLAT_EFFECTS,
|
|
10
|
-
DEBUFF_MULT_EFFECTS,
|
|
11
|
-
DEBUFF_FLAT_EFFECTS,
|
|
12
|
-
DEBUFFS,
|
|
13
|
-
BUFFS,
|
|
14
|
-
MULTS
|
|
15
|
-
} = require('./constants')
|
|
16
|
-
|
|
17
|
-
// Get only alive gotchis in a team
|
|
18
|
-
const getAlive = (team, row) => {
|
|
19
|
-
if (row) {
|
|
20
|
-
return team.formation[row].filter(x => x).filter(x => x.health > 0)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return [...team.formation.front, ...team.formation.back].filter(x => x).filter(x => x.health > 0)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Get the formation position of a gotchi
|
|
28
|
-
* @param {Object} team1 An in-game team object
|
|
29
|
-
* @param {Object} team2 An in-game team object
|
|
30
|
-
* @param {Number} gotchiId The id of the gotchi
|
|
31
|
-
* @returns {Object} position The formation position of the gotchi
|
|
32
|
-
* @returns {Number} position.team The team the gotchi is on
|
|
33
|
-
* @returns {String} position.row The row the gotchi is on
|
|
34
|
-
* @returns {Number} position.position The position of the gotchi in the row
|
|
35
|
-
* @returns {null} position null if the gotchi is not found
|
|
36
|
-
**/
|
|
37
|
-
const getFormationPosition = (team1, team2, gotchiId) => {
|
|
38
|
-
const team1FrontIndex = team1.formation.front.findIndex(x => x && x.id === gotchiId)
|
|
39
|
-
|
|
40
|
-
if (team1FrontIndex !== -1) return {
|
|
41
|
-
team: 1,
|
|
42
|
-
row: 'front',
|
|
43
|
-
position: team1FrontIndex,
|
|
44
|
-
name: team1.formation.front[team1FrontIndex].name
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const team1BackIndex = team1.formation.back.findIndex(x => x && x.id === gotchiId)
|
|
48
|
-
|
|
49
|
-
if (team1BackIndex !== -1) return {
|
|
50
|
-
team: 1,
|
|
51
|
-
row: 'back',
|
|
52
|
-
position: team1BackIndex,
|
|
53
|
-
name: team1.formation.back[team1BackIndex].name
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const team2FrontIndex = team2.formation.front.findIndex(x => x && x.id === gotchiId)
|
|
57
|
-
|
|
58
|
-
if (team2FrontIndex !== -1) return {
|
|
59
|
-
team: 2,
|
|
60
|
-
row: 'front',
|
|
61
|
-
position: team2FrontIndex,
|
|
62
|
-
name: team2.formation.front[team2FrontIndex].name
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const team2BackIndex = team2.formation.back.findIndex(x => x && x.id === gotchiId)
|
|
66
|
-
|
|
67
|
-
if (team2BackIndex !== -1) return {
|
|
68
|
-
team: 2,
|
|
69
|
-
row: 'back',
|
|
70
|
-
position: team2BackIndex,
|
|
71
|
-
name: team2.formation.back[team2BackIndex].name
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return null
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Get the leader gotchi of a team
|
|
79
|
-
* @param {Object} team An in-game team object
|
|
80
|
-
* @returns {Object} gotchi The leader gotchi
|
|
81
|
-
* @returns {Number} leader.id The id of the gotchi
|
|
82
|
-
* @returns {String} leader.special The special object of the gotchi
|
|
83
|
-
* @returns {String} leader.special.class The class of the special
|
|
84
|
-
**/
|
|
85
|
-
const getLeaderGotchi = (team) => {
|
|
86
|
-
const leader = [...team.formation.front, ...team.formation.back].find(x => x && x.id === team.leader)
|
|
87
|
-
|
|
88
|
-
if (!leader) throw new Error('Leader not found')
|
|
89
|
-
|
|
90
|
-
return leader
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Get the next gotchi to act
|
|
95
|
-
* @param {Object} team1 An in-game team object
|
|
96
|
-
* @param {Object} team2 An in-game team object
|
|
97
|
-
* @param {Function} rng The random number generator
|
|
98
|
-
* @returns {Object} position The formation position of the gotchi
|
|
99
|
-
**/
|
|
100
|
-
const getNextToAct = (team1, team2, rng) => {
|
|
101
|
-
const aliveGotchis = [...getAlive(team1), ...getAlive(team2)]
|
|
102
|
-
|
|
103
|
-
aliveGotchis.sort((a, b) => a.actionDelay - b.actionDelay)
|
|
104
|
-
|
|
105
|
-
let toAct = aliveGotchis.filter(gotchi => gotchi.actionDelay === aliveGotchis[0].actionDelay)
|
|
106
|
-
|
|
107
|
-
// If only one gotchi can act then return it
|
|
108
|
-
if (toAct.length === 1) return getFormationPosition(team1, team2, toAct[0].id)
|
|
109
|
-
|
|
110
|
-
// Lowest speeds win tiebreaker
|
|
111
|
-
toAct.sort((a, b) => a.speed - b.speed)
|
|
112
|
-
toAct = toAct.filter(gotchi => gotchi.speed === toAct[0].speed)
|
|
113
|
-
|
|
114
|
-
// If only one gotchi can act then return it
|
|
115
|
-
|
|
116
|
-
if (toAct.length === 1) return getFormationPosition(team1, team2, toAct[0].id)
|
|
117
|
-
|
|
118
|
-
// If still tied then randomly choose
|
|
119
|
-
const randomIndex = Math.floor(rng() * toAct.length)
|
|
120
|
-
|
|
121
|
-
if (!toAct[randomIndex]) throw new Error(`No gotchi found at index ${randomIndex}`)
|
|
122
|
-
|
|
123
|
-
toAct = toAct[randomIndex]
|
|
124
|
-
return getFormationPosition(team1, team2, toAct.id)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const getTarget = (defendingTeam, rng) => {
|
|
128
|
-
// Check for taunt gotchis
|
|
129
|
-
const taunt = [...getAlive(defendingTeam, 'front'), ...getAlive(defendingTeam, 'back')].filter(gotchi => gotchi.statuses && gotchi.statuses.includes("taunt"))
|
|
130
|
-
|
|
131
|
-
if (taunt.length) {
|
|
132
|
-
if (taunt.length === 1) return taunt[0]
|
|
133
|
-
|
|
134
|
-
// If multiple taunt gotchis then randomly choose one
|
|
135
|
-
return taunt[Math.floor(rng() * taunt.length)]
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Target gotchis in the front row first
|
|
139
|
-
const frontRow = getAlive(defendingTeam, 'front')
|
|
140
|
-
|
|
141
|
-
if (frontRow.length) {
|
|
142
|
-
return frontRow[Math.floor(rng() * frontRow.length)]
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// If no gotchis in front row then target back row
|
|
146
|
-
const backRow = getAlive(defendingTeam, 'back')
|
|
147
|
-
|
|
148
|
-
if (backRow.length) {
|
|
149
|
-
return backRow[Math.floor(rng() * backRow.length)]
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
throw new Error('No gotchis to target')
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const applySpeedPenalty = (gotchi, penalty) => {
|
|
156
|
-
const speedPenalty = (gotchi.speed - 100) * penalty
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
...gotchi,
|
|
160
|
-
magic: gotchi.magic - speedPenalty,
|
|
161
|
-
physical: gotchi.physical - speedPenalty
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Get the damage of an attack
|
|
167
|
-
* @param {Object} attackingTeam The attacking team
|
|
168
|
-
* @param {Object} defendingTeam The defending team
|
|
169
|
-
* @param {Object} attackingGotchi The gotchi attacking
|
|
170
|
-
* @param {Object} defendingGotchi The gotchi defending
|
|
171
|
-
* @param {Number} multiplier The damage multiplier
|
|
172
|
-
* @param {Boolean} ignoreArmor Whether to ignore armor
|
|
173
|
-
* @param {Number} speedPenalty The speed penalty to apply
|
|
174
|
-
* @returns {Number} damage The damage of the attack
|
|
175
|
-
**/
|
|
176
|
-
const getDamage = (attackingTeam, defendingTeam, attackingGotchi, defendingGotchi, multiplier, ignoreArmor, speedPenalty) => {
|
|
177
|
-
|
|
178
|
-
const attackerWithSpeedPenalty = speedPenalty ? applySpeedPenalty(attackingGotchi, speedPenalty) : attackingGotchi
|
|
179
|
-
|
|
180
|
-
// Apply any status effects
|
|
181
|
-
const modifiedAttackingsGotchi = getModifiedStats(attackerWithSpeedPenalty)
|
|
182
|
-
const modifiedDefendingGotchi = getModifiedStats(defendingGotchi)
|
|
183
|
-
|
|
184
|
-
let attackValue = attackingGotchi.attack === 'magic' ? modifiedAttackingsGotchi.magic : modifiedAttackingsGotchi.physical
|
|
185
|
-
|
|
186
|
-
// If attacking gotchi is in the front row and physical attack then apply front row physical attack bonus
|
|
187
|
-
if (getFormationPosition(attackingTeam, defendingTeam, attackingGotchi.id).row === 'front' && attackingGotchi.attack === 'physical') {
|
|
188
|
-
attackValue = Math.round(attackValue * MULTS.FRONT_ROW_PHY_ATK)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
let defenseValue = attackingGotchi.attack === 'magic' ? modifiedDefendingGotchi.magic : modifiedDefendingGotchi.physical
|
|
192
|
-
|
|
193
|
-
// If defending gotchi is in the front row and the attack is physical then apply front row physical defence penalty
|
|
194
|
-
if (getFormationPosition(attackingTeam, defendingTeam, defendingGotchi.id).row === 'front' && attackingGotchi.attack === 'physical') {
|
|
195
|
-
defenseValue = Math.round(defenseValue * MULTS.FRONT_ROW_PHY_DEF)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Add armor to defense value
|
|
199
|
-
if (!ignoreArmor) defenseValue += modifiedDefendingGotchi.armor
|
|
200
|
-
|
|
201
|
-
// Calculate damage
|
|
202
|
-
let damage = Math.round((attackValue / defenseValue) * 100)
|
|
203
|
-
|
|
204
|
-
// Apply multiplier
|
|
205
|
-
if (multiplier) damage = Math.round(damage * multiplier)
|
|
206
|
-
|
|
207
|
-
// check for environment effects
|
|
208
|
-
if (defendingGotchi.environmentEffects && defendingGotchi.environmentEffects.length > 0) {
|
|
209
|
-
damage = Math.round(damage * (1 + (defendingGotchi.environmentEffects.length * 0.5)))
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return damage
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Apply status effects to a gotchi
|
|
217
|
-
* @param {Object} gotchi An in-game gotchi object
|
|
218
|
-
* @returns {Object} gotchi An in-game gotchi object with modified stats
|
|
219
|
-
*/
|
|
220
|
-
const getModifiedStats = (gotchi) => {
|
|
221
|
-
const statMods = {}
|
|
222
|
-
|
|
223
|
-
gotchi.statuses.forEach(status => {
|
|
224
|
-
const statusStatMods = {}
|
|
225
|
-
|
|
226
|
-
// apply any modifier from BUFF_MULT_EFFECTS
|
|
227
|
-
if (BUFF_MULT_EFFECTS[status]) {
|
|
228
|
-
Object.keys(BUFF_MULT_EFFECTS[status]).forEach(stat => {
|
|
229
|
-
const modifier = Math.round(gotchi[stat] * BUFF_MULT_EFFECTS[status][stat])
|
|
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
|
-
const modifier = Math.round(gotchi[stat] * DEBUFF_MULT_EFFECTS[status][stat])
|
|
251
|
-
|
|
252
|
-
statusStatMods[stat] = -modifier
|
|
253
|
-
})
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// apply any modifier from DEBUFF_FLAT_EFFECTS
|
|
257
|
-
if (DEBUFF_FLAT_EFFECTS[status]) {
|
|
258
|
-
Object.keys(DEBUFF_FLAT_EFFECTS[status]).forEach(stat => {
|
|
259
|
-
if (statusStatMods[stat]) {
|
|
260
|
-
// If a mod for this status already exists, only add if the new mod is greater
|
|
261
|
-
if (DEBUFF_FLAT_EFFECTS[status][stat] < statusStatMods[stat]) statusStatMods[stat] = DEBUFF_FLAT_EFFECTS[status][stat]
|
|
262
|
-
} else {
|
|
263
|
-
statusStatMods[stat] = -DEBUFF_FLAT_EFFECTS[status][stat]
|
|
264
|
-
}
|
|
265
|
-
})
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// apply status mods
|
|
269
|
-
Object.keys(statusStatMods).forEach(stat => {
|
|
270
|
-
statMods[stat] = statMods[stat] ? statMods[stat] + statusStatMods[stat] : statusStatMods[stat]
|
|
271
|
-
})
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
const modifiedGotchi = {
|
|
275
|
-
...gotchi
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// apply stat mods
|
|
279
|
-
Object.keys(statMods).forEach(stat => {
|
|
280
|
-
if (statMods[stat] < 0) {
|
|
281
|
-
modifiedGotchi[stat] = modifiedGotchi[stat] + statMods[stat] < 0 ? 0 : modifiedGotchi[stat] + statMods[stat]
|
|
282
|
-
} else {
|
|
283
|
-
modifiedGotchi[stat] += statMods[stat]
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
return modifiedGotchi
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const calculateActionDelay = (gotchi) => {
|
|
292
|
-
// Calculate action delay and round to 3 decimal places
|
|
293
|
-
return Math.round(((100 / getModifiedStats(gotchi).speed) + Number.EPSILON) * 1000) / 1000
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const getNewActionDelay = (gotchi) => {
|
|
297
|
-
// Calculate new action delay and round to 3 decimal places
|
|
298
|
-
return Math.round((gotchi.actionDelay + calculateActionDelay(gotchi) + Number.EPSILON) * 1000) / 1000
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Simplify a team object for storage
|
|
303
|
-
* @param {Object} team An in-game team object
|
|
304
|
-
* @returns {Object} simplifiedTeam A simplified team object
|
|
305
|
-
*/
|
|
306
|
-
const simplifyTeam = (team) => {
|
|
307
|
-
return {
|
|
308
|
-
name: team.name,
|
|
309
|
-
owner: team.owner,
|
|
310
|
-
leaderId: team.leader,
|
|
311
|
-
rows: [
|
|
312
|
-
{
|
|
313
|
-
slots: team.formation.front.map((x) => {
|
|
314
|
-
return {
|
|
315
|
-
isActive: x ? true : false,
|
|
316
|
-
id: x ? x.id : null
|
|
317
|
-
}
|
|
318
|
-
})
|
|
319
|
-
},
|
|
320
|
-
{
|
|
321
|
-
slots: team.formation.back.map((x) => {
|
|
322
|
-
return {
|
|
323
|
-
isActive: x ? true : false,
|
|
324
|
-
id: x ? x.id : null
|
|
325
|
-
}
|
|
326
|
-
})
|
|
327
|
-
}
|
|
328
|
-
],
|
|
329
|
-
uiOrder: getUiOrder(team)
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Get the UI order of a team (used for the front end)
|
|
335
|
-
* @param {Object} team An in-game team object
|
|
336
|
-
* @returns {Array} uiOrder An array of gotchi ids in the order they should be displayed
|
|
337
|
-
**/
|
|
338
|
-
const getUiOrder = (team) => {
|
|
339
|
-
const uiOrder = []
|
|
340
|
-
|
|
341
|
-
if (team.formation.front[0]) uiOrder.push(team.formation.front[0].id)
|
|
342
|
-
if (team.formation.back[0]) uiOrder.push(team.formation.back[0].id)
|
|
343
|
-
if (team.formation.front[1]) uiOrder.push(team.formation.front[1].id)
|
|
344
|
-
if (team.formation.back[1]) uiOrder.push(team.formation.back[1].id)
|
|
345
|
-
if (team.formation.front[2]) uiOrder.push(team.formation.front[2].id)
|
|
346
|
-
if (team.formation.back[2]) uiOrder.push(team.formation.back[2].id)
|
|
347
|
-
if (team.formation.front[3]) uiOrder.push(team.formation.front[3].id)
|
|
348
|
-
if (team.formation.back[3]) uiOrder.push(team.formation.back[3].id)
|
|
349
|
-
if (team.formation.front[4]) uiOrder.push(team.formation.front[4].id)
|
|
350
|
-
if (team.formation.back[4]) uiOrder.push(team.formation.back[4].id)
|
|
351
|
-
|
|
352
|
-
return uiOrder
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Add the leader statuses to a team
|
|
357
|
-
* @param {Object} team An in-game team object
|
|
358
|
-
**/
|
|
359
|
-
const addLeaderToTeam = (team) => {
|
|
360
|
-
// Add passive leader abilities
|
|
361
|
-
const teamLeader = getLeaderGotchi(team)
|
|
362
|
-
|
|
363
|
-
team.leaderPassive = teamLeader.special.id
|
|
364
|
-
|
|
365
|
-
// Apply leader passive statuses
|
|
366
|
-
switch (team.leaderPassive) {
|
|
367
|
-
case 1:
|
|
368
|
-
// Sharpen blades - all allies gain 'sharp_blades' status
|
|
369
|
-
getAlive(team).forEach(x => {
|
|
370
|
-
x.statuses.push(PASSIVES[team.leaderPassive - 1])
|
|
371
|
-
})
|
|
372
|
-
break
|
|
373
|
-
case 2:
|
|
374
|
-
// Cloud of Zen - Leader get 'cloud_of_zen' status
|
|
375
|
-
teamLeader.statuses.push(PASSIVES[team.leaderPassive - 1])
|
|
376
|
-
break
|
|
377
|
-
case 3:
|
|
378
|
-
// Frenzy - all allies get 'frenzy' status
|
|
379
|
-
getAlive(team).forEach(x => {
|
|
380
|
-
x.statuses.push(PASSIVES[team.leaderPassive - 1])
|
|
381
|
-
})
|
|
382
|
-
break
|
|
383
|
-
case 4:
|
|
384
|
-
// All allies get 'fortify' status
|
|
385
|
-
getAlive(team).forEach(x => {
|
|
386
|
-
x.statuses.push(PASSIVES[team.leaderPassive - 1])
|
|
387
|
-
})
|
|
388
|
-
|
|
389
|
-
break
|
|
390
|
-
case 5:
|
|
391
|
-
// Spread the fear - all allies get 'spread_the_fear' status
|
|
392
|
-
getAlive(team).forEach(x => {
|
|
393
|
-
x.statuses.push(PASSIVES[team.leaderPassive - 1])
|
|
394
|
-
})
|
|
395
|
-
break
|
|
396
|
-
case 6:
|
|
397
|
-
// Cleansing aura - every healer ally and every tank ally gets 'cleansing_aura' status
|
|
398
|
-
getAlive(team).forEach(x => {
|
|
399
|
-
if (x.special.id === 6 || x.special.id === 4) x.statuses.push(PASSIVES[team.leaderPassive - 1])
|
|
400
|
-
})
|
|
401
|
-
break
|
|
402
|
-
case 7:
|
|
403
|
-
// Arcane thunder - every mage ally gets 'arcane_thunder' status
|
|
404
|
-
getAlive(team).forEach(x => {
|
|
405
|
-
if (x.special.id === 7) x.statuses.push(PASSIVES[team.leaderPassive - 1])
|
|
406
|
-
})
|
|
407
|
-
break
|
|
408
|
-
case 8:
|
|
409
|
-
// Clan momentum - every Troll ally gets 'clan_momentum' status
|
|
410
|
-
getAlive(team).forEach(x => {
|
|
411
|
-
if (x.special.id === 8) x.statuses.push(PASSIVES[team.leaderPassive - 1])
|
|
412
|
-
})
|
|
413
|
-
break
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const removeLeaderPassivesFromTeam = (team) => {
|
|
418
|
-
let statusesRemoved = []
|
|
419
|
-
if (!team.leaderPassive) return statusesRemoved
|
|
420
|
-
|
|
421
|
-
// Remove leader passive statuses from team
|
|
422
|
-
getAlive(team).forEach(x => {
|
|
423
|
-
// add effects for each status removed
|
|
424
|
-
x.statuses.forEach(status => {
|
|
425
|
-
if (status === PASSIVES[team.leaderPassive - 1]) {
|
|
426
|
-
statusesRemoved.push({
|
|
427
|
-
target: x.id,
|
|
428
|
-
status: status
|
|
429
|
-
})
|
|
430
|
-
}
|
|
431
|
-
})
|
|
432
|
-
|
|
433
|
-
x.statuses = x.statuses.filter(x => x !== PASSIVES[team.leaderPassive - 1])
|
|
434
|
-
})
|
|
435
|
-
|
|
436
|
-
team.leaderPassive = null
|
|
437
|
-
|
|
438
|
-
return statusesRemoved
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const getExpiredStatuses = (team1, team2) => {
|
|
442
|
-
// If leader is dead, remove leader passive
|
|
443
|
-
let statusesExpired = []
|
|
444
|
-
if (team1.leaderPassive && !getAlive(team1).find(x => x.id === team1.leader)) {
|
|
445
|
-
// Remove leader passive statuses
|
|
446
|
-
statusesExpired = removeLeaderPassivesFromTeam(team1)
|
|
447
|
-
}
|
|
448
|
-
if (team2.leaderPassive && !getAlive(team2).find(x => x.id === team2.leader)) {
|
|
449
|
-
// Remove leader passive statuses
|
|
450
|
-
statusesExpired = removeLeaderPassivesFromTeam(team2)
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
return statusesExpired
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Add a status to a gotchi
|
|
458
|
-
* @param {Object} gotchi An in-game gotchi object
|
|
459
|
-
* @param {String} status The status to add
|
|
460
|
-
* @returns {Boolean} success A boolean to determine if the status was added
|
|
461
|
-
**/
|
|
462
|
-
const addStatusToGotchi = (gotchi, status) => {
|
|
463
|
-
// Check that gotchi doesn't already have max number of statuses
|
|
464
|
-
if (gotchi.statuses.filter(item => item === status).length >= MULTS.MAX_STATUSES) return false
|
|
465
|
-
|
|
466
|
-
gotchi.statuses.push(status)
|
|
467
|
-
|
|
468
|
-
return true
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
const scrambleGotchiIds = (allAliveGotchis, team1, team2) => {
|
|
472
|
-
// check there's no duplicate gotchis
|
|
473
|
-
const gotchiIds = allAliveGotchis.map(x => x.id)
|
|
474
|
-
|
|
475
|
-
if (gotchiIds.length !== new Set(gotchiIds).size) {
|
|
476
|
-
// scramble gotchi ids
|
|
477
|
-
allAliveGotchis.forEach(x => {
|
|
478
|
-
const newId = Math.floor(Math.random() * 10000000)
|
|
479
|
-
|
|
480
|
-
// find gotchi in team1 or team2
|
|
481
|
-
const position = getFormationPosition(team1, team2, x.id)
|
|
482
|
-
|
|
483
|
-
// change gotchi id
|
|
484
|
-
if (position) {
|
|
485
|
-
if (position.team === 1) {
|
|
486
|
-
if (x.id === team1.leader) team1.leader = newId
|
|
487
|
-
team1.formation[position.row][position.position].id = newId
|
|
488
|
-
} else {
|
|
489
|
-
if (x.id === team2.leader) team2.leader = newId
|
|
490
|
-
team2.formation[position.row][position.position].id = newId
|
|
491
|
-
}
|
|
492
|
-
} else {
|
|
493
|
-
throw new Error('Gotchi not found in team1 or team2')
|
|
494
|
-
}
|
|
495
|
-
})
|
|
496
|
-
|
|
497
|
-
// check again
|
|
498
|
-
const newGotchiIds = allAliveGotchis.map(x => x.id)
|
|
499
|
-
if (newGotchiIds.length !== new Set(newGotchiIds).size) {
|
|
500
|
-
// Scramble again
|
|
501
|
-
scrambleGotchiIds(allAliveGotchis, team1, team2)
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/**
|
|
507
|
-
* Prepare teams for battle
|
|
508
|
-
* @param {Array} allAliveGotchis An array of all alive gotchis
|
|
509
|
-
* @param {Object} team1 An in-game team object
|
|
510
|
-
* @param {Object} team2 An in-game team object
|
|
511
|
-
**/
|
|
512
|
-
const prepareTeams = (allAliveGotchis, team1, team2) => {
|
|
513
|
-
// check there's no duplicate gotchis
|
|
514
|
-
scrambleGotchiIds(allAliveGotchis, team1, team2);
|
|
515
|
-
|
|
516
|
-
allAliveGotchis.forEach(x => {
|
|
517
|
-
// Add statuses property to all gotchis
|
|
518
|
-
x.statuses = []
|
|
519
|
-
|
|
520
|
-
// Calculate initial action delay for all gotchis
|
|
521
|
-
x.actionDelay = calculateActionDelay(x)
|
|
522
|
-
|
|
523
|
-
// Calculate attack type
|
|
524
|
-
x.attack = x.magic > x.physical ? 'magic' : 'physical'
|
|
525
|
-
|
|
526
|
-
// Add original stats to all gotchis
|
|
527
|
-
// Do a deep copy of the gotchi object to avoid modifying the original object
|
|
528
|
-
x.originalStats = JSON.parse(JSON.stringify(x))
|
|
529
|
-
|
|
530
|
-
// Add environmentEffects to all gotchis
|
|
531
|
-
x.environmentEffects = []
|
|
532
|
-
})
|
|
533
|
-
|
|
534
|
-
// Add leader passive to team
|
|
535
|
-
addLeaderToTeam(team1)
|
|
536
|
-
addLeaderToTeam(team2);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
/**
|
|
540
|
-
* Get log gotchi object for battle logs
|
|
541
|
-
* @param {Array} allAliveGotchis An array of all alive gotchis
|
|
542
|
-
* @returns {Array} logGotchis An array of gotchi objects for logs
|
|
543
|
-
*/
|
|
544
|
-
const getLogGotchis = (allAliveGotchis) => {
|
|
545
|
-
const logGotchis = JSON.parse(JSON.stringify(allAliveGotchis))
|
|
546
|
-
|
|
547
|
-
logGotchis.forEach(x => {
|
|
548
|
-
// Change gotchi.special.class to gotchi.special.gotchiClass to avoid conflicts with class keyword
|
|
549
|
-
x.special.gotchiClass = x.special.class
|
|
550
|
-
|
|
551
|
-
// Remove unnecessary properties to reduce log size
|
|
552
|
-
delete x.special.class
|
|
553
|
-
delete x.snapshotBlock
|
|
554
|
-
delete x.onchainId
|
|
555
|
-
delete x.brs
|
|
556
|
-
delete x.nrg
|
|
557
|
-
delete x.agg
|
|
558
|
-
delete x.spk
|
|
559
|
-
delete x.brn
|
|
560
|
-
delete x.eyc
|
|
561
|
-
delete x.eys
|
|
562
|
-
delete x.kinship
|
|
563
|
-
delete x.xp
|
|
564
|
-
delete x.actionDelay
|
|
565
|
-
delete x.attack
|
|
566
|
-
delete x.originalStats
|
|
567
|
-
delete x.environmentEffects
|
|
568
|
-
})
|
|
569
|
-
|
|
570
|
-
return logGotchis
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* Run a battle between two teams
|
|
575
|
-
* @param {Object} team1 An in-game team object
|
|
576
|
-
* @param {Object} team2 An in-game team object
|
|
577
|
-
* @param {String} seed A seed for the random number generator
|
|
578
|
-
* @param {Boolean} debug A boolean to determine if the logs should include debug information
|
|
579
|
-
* @returns {Object} logs The battle logs
|
|
580
|
-
*/
|
|
581
|
-
const gameLoop = (team1, team2, seed, debug) => {
|
|
582
|
-
if (!team1) throw new Error("Team 1 not found")
|
|
583
|
-
if (!team2) throw new Error("Team 2 not found")
|
|
584
|
-
if (!seed) throw new Error("Seed not found")
|
|
585
|
-
|
|
586
|
-
// Validate team objects
|
|
587
|
-
const team1Validation = validator.validate(team1, teamSchema)
|
|
588
|
-
if (!team1Validation) {
|
|
589
|
-
console.error('Team 1 validation failed: ', JSON.stringify(validator.getLastErrors(), null, 2))
|
|
590
|
-
throw new Error(`Team 1 validation failed`)
|
|
591
|
-
}
|
|
592
|
-
const team2Validation = validator.validate(team2, teamSchema)
|
|
593
|
-
if (!team2Validation) {
|
|
594
|
-
console.error('Team 2 validation failed: ', JSON.stringify(validator.getLastErrors(), null, 2))
|
|
595
|
-
throw new Error(`Team 2 validation failed`)
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
const rng = seedrandom(seed)
|
|
599
|
-
|
|
600
|
-
const allAliveGotchis = [...getAlive(team1), ...getAlive(team2)]
|
|
601
|
-
|
|
602
|
-
prepareTeams(allAliveGotchis, team1, team2)
|
|
603
|
-
|
|
604
|
-
const logs = {
|
|
605
|
-
gotchis: getLogGotchis(allAliveGotchis),
|
|
606
|
-
layout: {
|
|
607
|
-
teams: [
|
|
608
|
-
simplifyTeam(team1),
|
|
609
|
-
simplifyTeam(team2)
|
|
610
|
-
]
|
|
611
|
-
},
|
|
612
|
-
turns: []
|
|
613
|
-
};
|
|
614
|
-
|
|
615
|
-
// Used for turn by turn health and status summaries
|
|
616
|
-
// Deleted if not in development or no errors
|
|
617
|
-
logs.debug = []
|
|
618
|
-
|
|
619
|
-
let turnCounter = 0
|
|
620
|
-
let draw = false
|
|
621
|
-
|
|
622
|
-
try {
|
|
623
|
-
while (getAlive(team1).length && getAlive(team2).length) {
|
|
624
|
-
// Check if turnCounter is ready for environment effects (99,149,199, etc)
|
|
625
|
-
let isEnvironmentTurn = [99, 149, 199, 249, 299].includes(turnCounter)
|
|
626
|
-
if (isEnvironmentTurn) {
|
|
627
|
-
allAliveGotchis.forEach(x => {
|
|
628
|
-
x.environmentEffects.push('damage_up')
|
|
629
|
-
})
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
const turnLogs = executeTurn(team1, team2, rng)
|
|
633
|
-
|
|
634
|
-
// Check if turnCounter is ready for environment effects (99,149,199, etc)
|
|
635
|
-
if (isEnvironmentTurn) turnLogs.environmentEffects = ['damage_up']
|
|
636
|
-
|
|
637
|
-
if (MULTS.EXPIRE_LEADERSKILL) {
|
|
638
|
-
turnLogs.statusesExpired = [...turnLogs.statusesExpired, ...getExpiredStatuses(team1, team2)]
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
logs.turns.push({index: turnCounter, ...turnLogs})
|
|
642
|
-
|
|
643
|
-
if (debug) {
|
|
644
|
-
logs.debug.push({
|
|
645
|
-
turn: turnCounter,
|
|
646
|
-
user: logs.turns[logs.turns.length - 1].action.user,
|
|
647
|
-
move: logs.turns[logs.turns.length - 1].action.name,
|
|
648
|
-
team1: getAlive(team1).map((x) => {
|
|
649
|
-
return `Id: ${x.id}, Name: ${x.name}, Health: ${x.health}, Statuses: ${x.statuses}`
|
|
650
|
-
}),
|
|
651
|
-
team2: getAlive(team2).map((x) => {
|
|
652
|
-
return `Id: ${x.id}, Name: ${x.name}, Health: ${x.health}, Statuses: ${x.statuses}`
|
|
653
|
-
})
|
|
654
|
-
})
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
turnCounter++
|
|
658
|
-
}
|
|
659
|
-
} catch (e) {
|
|
660
|
-
console.error(e)
|
|
661
|
-
throw new GameError('Game loop failed', logs)
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
if (draw) {
|
|
665
|
-
logs.result = {
|
|
666
|
-
winner: 0,
|
|
667
|
-
loser: 0,
|
|
668
|
-
winningTeam: [],
|
|
669
|
-
numOfTurns: logs.turns.length
|
|
670
|
-
}
|
|
671
|
-
} else {
|
|
672
|
-
logs.result = {
|
|
673
|
-
winner: getAlive(team1).length ? 1 : 2,
|
|
674
|
-
loser: getAlive(team1).length ? 2 : 1,
|
|
675
|
-
winningTeam: getAlive(team1).length ? getAlive(team1) : getAlive(team2),
|
|
676
|
-
numOfTurns: logs.turns.length
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// trim winning team objects
|
|
680
|
-
logs.result.winningTeam = logs.result.winningTeam.map((gotchi) => {
|
|
681
|
-
return {
|
|
682
|
-
id: gotchi.id,
|
|
683
|
-
name: gotchi.name,
|
|
684
|
-
brs: gotchi.brs,
|
|
685
|
-
health: gotchi.health
|
|
686
|
-
}
|
|
687
|
-
})
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
if (!debug) delete logs.debug
|
|
691
|
-
|
|
692
|
-
return logs
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
/**
|
|
696
|
-
* Attack one or more gotchis. This mutates the defending gotchis health
|
|
697
|
-
* @param {Object} attackingGotchi The attacking gotchi object
|
|
698
|
-
* @param {Array} attackingTeam A team object for the attacking team
|
|
699
|
-
* @param {Array} defendingTeam A team object for the defending team
|
|
700
|
-
* @param {Array} defendingTargets An array of gotchis to attack
|
|
701
|
-
* @param {Function} rng The random number generator
|
|
702
|
-
* @param {Object} options An object of options
|
|
703
|
-
* @param {Boolean} options.ignoreArmor Ignore the defending gotchi's defense
|
|
704
|
-
* @param {Boolean} options.multiplier A multiplier to apply to the damage
|
|
705
|
-
* @param {Boolean} options.statuses An array of status effects to apply
|
|
706
|
-
* @param {Boolean} options.cannotBeEvaded A boolean to determine if the attack can be evaded
|
|
707
|
-
* @param {Boolean} options.cannotBeResisted A boolean to determine if the attack can be resisted
|
|
708
|
-
* @param {Boolean} options.cannotBeCountered A boolean to determine if the attack can be countered
|
|
709
|
-
* @param {Boolean} options.inflictPassiveStatuses A boolean to determine if passive statuses should be inflicted
|
|
710
|
-
* @returns {Array} effects An array of effects to apply
|
|
711
|
-
*/
|
|
712
|
-
const attack = (attackingGotchi, attackingTeam, defendingTeam, defendingTargets, rng, options = {
|
|
713
|
-
ignoreArmor: false,
|
|
714
|
-
multiplier: 1,
|
|
715
|
-
statuses: [],
|
|
716
|
-
cannotBeEvaded: false,
|
|
717
|
-
critCannotBeEvaded: false,
|
|
718
|
-
cannotBeResisted: false,
|
|
719
|
-
cannotBeCountered: false,
|
|
720
|
-
inflictPassiveStatuses: true,
|
|
721
|
-
speedPenalty: 0,
|
|
722
|
-
noResistSpeedPenalty: false
|
|
723
|
-
}) => {
|
|
724
|
-
const effects = []
|
|
725
|
-
if (!options.ignoreArmor) options.ignoreArmor = false
|
|
726
|
-
if (!options.multiplier) options.multiplier = 1
|
|
727
|
-
if (!options.statuses) options.statuses = []
|
|
728
|
-
if (!options.cannotBeEvaded) options.cannotBeEvaded = false
|
|
729
|
-
if (!options.critCannotBeEvaded) options.critCannotBeEvaded = false
|
|
730
|
-
if (!options.cannotBeResisted) options.cannotBeResisted = false
|
|
731
|
-
if (!options.cannotBeCountered) options.cannotBeCountered = false
|
|
732
|
-
if (!options.inflictPassiveStatuses) options.inflictPassiveStatuses = false
|
|
733
|
-
if (!options.speedPenalty) options.speedPenalty = 0
|
|
734
|
-
if (!options.noResistSpeedPenalty) options.noResistSpeedPenalty = false
|
|
735
|
-
|
|
736
|
-
// If inflictPassiveStatuses then add leaderPassive status effects to attackingGotchi
|
|
737
|
-
if (options.inflictPassiveStatuses) {
|
|
738
|
-
// If attacking gotchi has 'sharp_blades' status, add 'bleed' to statuses
|
|
739
|
-
if (attackingGotchi.statuses.includes('sharp_blades')) {
|
|
740
|
-
if (rng() < MULTS.SHARP_BLADES_BLEED_CHANCE) options.statuses.push('bleed')
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
// If attacking gotchi has 'spread_the_fear' status, add 'fear' to statuses
|
|
744
|
-
if (attackingGotchi.statuses.includes('spread_the_fear')) {
|
|
745
|
-
// Reduce the chance to spread the fear if attacking gotchi has speed over 100
|
|
746
|
-
const spreadTheFearChance = attackingGotchi.speed > 100 ? MULTS.SPREAD_THE_FEAR_CHANCE - MULTS.SPREAD_THE_FEAR_SPEED_PENALTY : MULTS.SPREAD_THE_FEAR_CHANCE
|
|
747
|
-
if (rng() < spreadTheFearChance) options.statuses.push('fear')
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
defendingTargets.forEach((defendingGotchi) => {
|
|
752
|
-
// Check attacking gotchi hasn't been killed by a counter
|
|
753
|
-
if (attackingGotchi.health <= 0) return
|
|
754
|
-
|
|
755
|
-
const modifiedAttackingGotchi = getModifiedStats(attackingGotchi)
|
|
756
|
-
const modifiedDefendingGotchi = getModifiedStats(defendingGotchi)
|
|
757
|
-
|
|
758
|
-
// Check for crit
|
|
759
|
-
const isCrit = rng() < modifiedAttackingGotchi.crit / 100
|
|
760
|
-
if (isCrit) {
|
|
761
|
-
// Apply different crit multipliers for -nrg and +nrg gotchis
|
|
762
|
-
if (attackingGotchi.speed <= 100) {
|
|
763
|
-
options.multiplier *= MULTS.CRIT_MULTIPLIER_SLOW
|
|
764
|
-
} else {
|
|
765
|
-
options.multiplier *= MULTS.CRIT_MULTIPLIER_FAST
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
let canEvade = true
|
|
770
|
-
if (options.cannotBeEvaded) canEvade = false
|
|
771
|
-
if (isCrit && options.critCannotBeEvaded) canEvade = false
|
|
772
|
-
|
|
773
|
-
const damage = getDamage(attackingTeam, defendingTeam, attackingGotchi, defendingGotchi, options.multiplier, options.ignoreArmor, options.speedPenalty)
|
|
774
|
-
|
|
775
|
-
let effect = {
|
|
776
|
-
target: defendingGotchi.id,
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
// Check for miss
|
|
780
|
-
if (rng() > modifiedAttackingGotchi.accuracy / 100) {
|
|
781
|
-
effect.outcome = 'miss'
|
|
782
|
-
effects.push(effect)
|
|
783
|
-
} else if (canEvade && rng() < modifiedDefendingGotchi.evade / 100){
|
|
784
|
-
effect.outcome = 'evade'
|
|
785
|
-
effects.push(effect)
|
|
786
|
-
} else {
|
|
787
|
-
if (!options.cannotBeResisted) {
|
|
788
|
-
// Check for status effect from the move
|
|
789
|
-
options.statuses.forEach((status) => {
|
|
790
|
-
if (rng() > modifiedDefendingGotchi.resist / 100) {
|
|
791
|
-
// Attempt to add status to defending gotchi
|
|
792
|
-
if (addStatusToGotchi(defendingGotchi, status)) {
|
|
793
|
-
// If status added, add to effect
|
|
794
|
-
if (!effect.statuses) {
|
|
795
|
-
effect.statuses = [status]
|
|
796
|
-
} else {
|
|
797
|
-
effect.statuses.push(status)
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
})
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
// Handle damage
|
|
805
|
-
defendingGotchi.health -= damage
|
|
806
|
-
effect.damage = damage
|
|
807
|
-
effect.outcome = isCrit ? 'critical' : 'success'
|
|
808
|
-
effects.push(effect)
|
|
809
|
-
|
|
810
|
-
// Check for counter attack
|
|
811
|
-
if (
|
|
812
|
-
defendingGotchi.statuses.includes('taunt')
|
|
813
|
-
&& defendingGotchi.health > 0
|
|
814
|
-
&& !options.cannotBeCountered) {
|
|
815
|
-
|
|
816
|
-
// Chance to counter based on speed over 100
|
|
817
|
-
let chanceToCounter = defendingGotchi.speed - 100
|
|
818
|
-
|
|
819
|
-
if (chanceToCounter < MULTS.COUNTER_CHANCE_MIN) chanceToCounter = MULTS.COUNTER_CHANCE_MIN
|
|
820
|
-
|
|
821
|
-
// Add a higher chance to counter if gotchi has 'fortify' status
|
|
822
|
-
if (defendingGotchi.statuses.includes('fortify')) chanceToCounter += MULTS.FORTIFY_COUNTER_CHANCE
|
|
823
|
-
|
|
824
|
-
if (rng() < chanceToCounter / 100) {
|
|
825
|
-
const counterDamage = getDamage(defendingTeam, attackingTeam, defendingGotchi, attackingGotchi, MULTS.COUNTER_DAMAGE, false, 0)
|
|
826
|
-
|
|
827
|
-
attackingGotchi.health -= counterDamage
|
|
828
|
-
|
|
829
|
-
effects.push({
|
|
830
|
-
target: attackingGotchi.id,
|
|
831
|
-
source: defendingGotchi.id,
|
|
832
|
-
damage: counterDamage,
|
|
833
|
-
outcome: 'counter'
|
|
834
|
-
})
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
})
|
|
839
|
-
|
|
840
|
-
return effects
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
// Deal with start of turn status effects
|
|
844
|
-
const handleStatusEffects = (attackingGotchi, attackingTeam, defendingTeam, rng) => {
|
|
845
|
-
const statusEffects = []
|
|
846
|
-
const passiveEffects = []
|
|
847
|
-
|
|
848
|
-
const modifiedAttackingGotchi = getModifiedStats(attackingGotchi)
|
|
849
|
-
|
|
850
|
-
// Check for cleansing_aura
|
|
851
|
-
// if (attackingGotchi.statuses.includes('cleansing_aura')) {
|
|
852
|
-
// // Remove all debuffs from all allies
|
|
853
|
-
// const aliveAllies = getAlive(attackingTeam)
|
|
854
|
-
// aliveAllies.forEach((ally) => {
|
|
855
|
-
// ally.statuses.forEach((status) => {
|
|
856
|
-
// if (DEBUFFS.includes(status)) {
|
|
857
|
-
// passiveEffects.push({
|
|
858
|
-
// source: attackingGotchi.id,
|
|
859
|
-
// target: ally.id,
|
|
860
|
-
// status,
|
|
861
|
-
// damage: 0,
|
|
862
|
-
// remove: true
|
|
863
|
-
// })
|
|
864
|
-
// }
|
|
865
|
-
// })
|
|
866
|
-
|
|
867
|
-
// // Remove status effects
|
|
868
|
-
// ally.statuses = ally.statuses.filter((status) => !DEBUFFS.includes(status))
|
|
869
|
-
// })
|
|
870
|
-
// }
|
|
871
|
-
|
|
872
|
-
// Check for global status effects
|
|
873
|
-
const allAliveGotchis = [...getAlive(attackingTeam), ...getAlive(defendingTeam)]
|
|
874
|
-
|
|
875
|
-
allAliveGotchis.forEach((gotchi) => {
|
|
876
|
-
if (gotchi.statuses && gotchi.statuses.length) {
|
|
877
|
-
gotchi.statuses.forEach((status) => {
|
|
878
|
-
// Handle cleansing_aura (health regen)
|
|
879
|
-
if (status === 'cleansing_aura') {
|
|
880
|
-
let amountToHeal
|
|
881
|
-
|
|
882
|
-
// Check if healer
|
|
883
|
-
if (gotchi.special.id === 6) {
|
|
884
|
-
amountToHeal = Math.round(gotchi.resist * MULTS.CLEANSING_AURA_REGEN)
|
|
885
|
-
} else {
|
|
886
|
-
amountToHeal = MULTS.CLEANSING_AURA_NON_HEALER_REGEN
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// Don't allow amountToHeal to be more than the difference between current health and max health
|
|
890
|
-
if (amountToHeal > gotchi.originalStats.health - gotchi.health) {
|
|
891
|
-
amountToHeal = gotchi.originalStats.health - gotchi.health
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// if amountToHeal > 0, add status effect
|
|
895
|
-
if (amountToHeal) {
|
|
896
|
-
// Add status effect
|
|
897
|
-
statusEffects.push({
|
|
898
|
-
target: gotchi.id,
|
|
899
|
-
status,
|
|
900
|
-
damage: -Math.abs(amountToHeal),
|
|
901
|
-
remove: false
|
|
902
|
-
})
|
|
903
|
-
|
|
904
|
-
gotchi.health += amountToHeal
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
/*
|
|
909
|
-
* Handle damage effect at the bottom of the loop
|
|
910
|
-
*/
|
|
911
|
-
|
|
912
|
-
// Handle bleed
|
|
913
|
-
if (status === 'bleed') {
|
|
914
|
-
let damage = MULTS.BLEED_DAMAGE
|
|
915
|
-
|
|
916
|
-
gotchi.health -= damage
|
|
917
|
-
if (gotchi.health <= 0) gotchi.health = 0
|
|
918
|
-
|
|
919
|
-
// Add status effect
|
|
920
|
-
statusEffects.push({
|
|
921
|
-
target: gotchi.id,
|
|
922
|
-
status,
|
|
923
|
-
damage,
|
|
924
|
-
remove: false
|
|
925
|
-
})
|
|
926
|
-
}
|
|
927
|
-
})
|
|
928
|
-
}
|
|
929
|
-
})
|
|
930
|
-
|
|
931
|
-
let skipTurn = null
|
|
932
|
-
|
|
933
|
-
// Check if gotchi is dead
|
|
934
|
-
if (attackingGotchi.health <= 0) {
|
|
935
|
-
return {
|
|
936
|
-
statusEffects,
|
|
937
|
-
passiveEffects,
|
|
938
|
-
skipTurn: 'ATTACKER_DEAD'
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
// Check if a whole team is dead
|
|
943
|
-
if (getAlive(attackingTeam).length === 0 || getAlive(defendingTeam).length === 0) {
|
|
944
|
-
return {
|
|
945
|
-
statusEffects,
|
|
946
|
-
passiveEffects,
|
|
947
|
-
skipTurn: 'TEAM_DEAD'
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
// Check for turn skipping statuses
|
|
952
|
-
for (let i = 0; i < attackingGotchi.statuses.length; i++) {
|
|
953
|
-
const status = attackingGotchi.statuses[i]
|
|
954
|
-
// Fear - skip turn
|
|
955
|
-
if (status === 'fear') {
|
|
956
|
-
// Skip turn
|
|
957
|
-
statusEffects.push({
|
|
958
|
-
target: attackingGotchi.id,
|
|
959
|
-
status,
|
|
960
|
-
damage: 0,
|
|
961
|
-
remove: true
|
|
962
|
-
})
|
|
963
|
-
|
|
964
|
-
skipTurn = 'FEAR'
|
|
965
|
-
|
|
966
|
-
// Remove fear first instance of fear
|
|
967
|
-
attackingGotchi.statuses.splice(i, 1)
|
|
968
|
-
|
|
969
|
-
break
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
// Stun
|
|
973
|
-
if (status === 'stun') {
|
|
974
|
-
// Skip turn
|
|
975
|
-
statusEffects.push({
|
|
976
|
-
target: attackingGotchi.id,
|
|
977
|
-
status,
|
|
978
|
-
damage: 0,
|
|
979
|
-
remove: true
|
|
980
|
-
})
|
|
981
|
-
|
|
982
|
-
skipTurn = 'STUN'
|
|
983
|
-
|
|
984
|
-
// Remove first instance of stun
|
|
985
|
-
attackingGotchi.statuses.splice(i, 1)
|
|
986
|
-
|
|
987
|
-
break
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
return {
|
|
992
|
-
statusEffects,
|
|
993
|
-
passiveEffects,
|
|
994
|
-
skipTurn
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
const executeTurn = (team1, team2, rng) => {
|
|
999
|
-
const nextToAct = getNextToAct(team1, team2, rng)
|
|
1000
|
-
|
|
1001
|
-
const attackingTeam = nextToAct.team === 1 ? team1 : team2
|
|
1002
|
-
const defendingTeam = nextToAct.team === 1 ? team2 : team1
|
|
1003
|
-
|
|
1004
|
-
const attackingGotchi = attackingTeam.formation[nextToAct.row][nextToAct.position]
|
|
1005
|
-
|
|
1006
|
-
let { statusEffects, passiveEffects, skipTurn } = handleStatusEffects(attackingGotchi, attackingTeam, defendingTeam, rng)
|
|
1007
|
-
let statusesExpired = []
|
|
1008
|
-
|
|
1009
|
-
let effects = []
|
|
1010
|
-
if (skipTurn) {
|
|
1011
|
-
// Increase actionDelay
|
|
1012
|
-
attackingGotchi.actionDelay = getNewActionDelay(attackingGotchi)
|
|
1013
|
-
|
|
1014
|
-
return {
|
|
1015
|
-
skipTurn,
|
|
1016
|
-
action: {
|
|
1017
|
-
user: attackingGotchi.id,
|
|
1018
|
-
name: 'auto',
|
|
1019
|
-
effects
|
|
1020
|
-
},
|
|
1021
|
-
passiveEffects,
|
|
1022
|
-
statusEffects,
|
|
1023
|
-
statusesExpired
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
let specialDone = false
|
|
1028
|
-
// Check if special attack is ready
|
|
1029
|
-
if (attackingGotchi.special.cooldown === 0) {
|
|
1030
|
-
// TODO: Check if special attack should be used
|
|
1031
|
-
|
|
1032
|
-
// Execute special attack
|
|
1033
|
-
const specialResults = specialAttack(attackingGotchi, attackingTeam, defendingTeam, rng)
|
|
1034
|
-
|
|
1035
|
-
effects = specialResults.effects
|
|
1036
|
-
statusesExpired = specialResults.statusesExpired
|
|
1037
|
-
|
|
1038
|
-
// Reset cooldown
|
|
1039
|
-
attackingGotchi.special.cooldown = 2
|
|
1040
|
-
|
|
1041
|
-
if (specialResults.specialNotDone) {
|
|
1042
|
-
// Do nothing which will lead to an auto attack
|
|
1043
|
-
} else {
|
|
1044
|
-
specialDone = true
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
} else {
|
|
1048
|
-
// Decrease cooldown
|
|
1049
|
-
attackingGotchi.special.cooldown--
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
if (!specialDone) {
|
|
1053
|
-
// Do an auto attack
|
|
1054
|
-
const target = getTarget(defendingTeam, rng)
|
|
1055
|
-
|
|
1056
|
-
effects = attack(attackingGotchi, attackingTeam, defendingTeam, [target], rng)
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
// Increase actionDelay
|
|
1060
|
-
attackingGotchi.actionDelay = getNewActionDelay(attackingGotchi)
|
|
1061
|
-
|
|
1062
|
-
return {
|
|
1063
|
-
skipTurn,
|
|
1064
|
-
action: {
|
|
1065
|
-
user: attackingGotchi.id,
|
|
1066
|
-
name: specialDone ? attackingGotchi.special.name : 'auto',
|
|
1067
|
-
effects
|
|
1068
|
-
},
|
|
1069
|
-
passiveEffects,
|
|
1070
|
-
statusEffects,
|
|
1071
|
-
statusesExpired
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
/**
|
|
1076
|
-
* Execute a special attack
|
|
1077
|
-
* @param {Object} attackingGotchi The attacking gotchi object
|
|
1078
|
-
* @param {Array} attackingTeam An array of gotchis to attack
|
|
1079
|
-
* @param {Array} defendingTeam An array of gotchis to attack
|
|
1080
|
-
* @param {Function} rng The random number generator
|
|
1081
|
-
* @returns {Array} effects An array of effects to apply
|
|
1082
|
-
**/
|
|
1083
|
-
const specialAttack = (attackingGotchi, attackingTeam, defendingTeam, rng) => {
|
|
1084
|
-
const specialId = attackingGotchi.special.id
|
|
1085
|
-
let effects = []
|
|
1086
|
-
let statusesExpired = []
|
|
1087
|
-
let specialNotDone = false
|
|
1088
|
-
|
|
1089
|
-
const modifiedAttackingGotchi = getModifiedStats(attackingGotchi)
|
|
1090
|
-
|
|
1091
|
-
switch (specialId) {
|
|
1092
|
-
case 1:
|
|
1093
|
-
// Spectral Strike - ignore armor and appply bleed status
|
|
1094
|
-
// get single target
|
|
1095
|
-
const ssTarget = getTarget(defendingTeam, rng)
|
|
1096
|
-
|
|
1097
|
-
effects = attack(attackingGotchi, attackingTeam, defendingTeam, [ssTarget], rng, {
|
|
1098
|
-
multiplier: MULTS.SPECTRAL_STRIKE_DAMAGE,
|
|
1099
|
-
ignoreArmor: true,
|
|
1100
|
-
statuses: ['bleed'],
|
|
1101
|
-
cannotBeCountered: true,
|
|
1102
|
-
cannotBeEvaded: true,
|
|
1103
|
-
inflictPassiveStatuses: false,
|
|
1104
|
-
noResistSpeedPenalty: true
|
|
1105
|
-
})
|
|
1106
|
-
break
|
|
1107
|
-
case 2:
|
|
1108
|
-
// Meditate - Boost own speed, magic, physical by 30%
|
|
1109
|
-
// If gotchi already has 2 power_up statuses, do nothing
|
|
1110
|
-
if (!addStatusToGotchi(attackingGotchi, 'power_up_2')) {
|
|
1111
|
-
specialNotDone = true
|
|
1112
|
-
break
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
effects = [
|
|
1116
|
-
{
|
|
1117
|
-
target: attackingGotchi.id,
|
|
1118
|
-
outcome: 'success',
|
|
1119
|
-
statuses: ['power_up_2']
|
|
1120
|
-
}
|
|
1121
|
-
]
|
|
1122
|
-
|
|
1123
|
-
// Check for leaderPassive 'Cloud of Zen'
|
|
1124
|
-
if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
|
|
1125
|
-
// Increase allies speed, magic and physical by 15% of the original value
|
|
1126
|
-
|
|
1127
|
-
const cloudOfZenGotchis = getAlive(attackingTeam)
|
|
1128
|
-
|
|
1129
|
-
cloudOfZenGotchis.forEach((gotchi) => {
|
|
1130
|
-
if (addStatusToGotchi(gotchi, 'power_up_1')) {
|
|
1131
|
-
effects.push({
|
|
1132
|
-
target: gotchi.id,
|
|
1133
|
-
outcome: 'success',
|
|
1134
|
-
statuses: ['power_up_1']
|
|
1135
|
-
})
|
|
1136
|
-
}
|
|
1137
|
-
})
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
break
|
|
1141
|
-
case 3:
|
|
1142
|
-
// Cleave - attack all enemies in a row (that have the most gotchis) for 75% damage
|
|
1143
|
-
// Find row with most gotchis
|
|
1144
|
-
const cleaveRow = getAlive(defendingTeam, 'front').length > getAlive(defendingTeam, 'back').length ? 'front' : 'back'
|
|
1145
|
-
|
|
1146
|
-
// Attack all gotchis in that row for 75% damage
|
|
1147
|
-
effects = attack(attackingGotchi, attackingTeam, defendingTeam, getAlive(defendingTeam, cleaveRow), rng, {
|
|
1148
|
-
multiplier: MULTS.CLEAVE_DAMAGE,
|
|
1149
|
-
cannotBeCountered: true,
|
|
1150
|
-
inflictPassiveStatuses: false
|
|
1151
|
-
})
|
|
1152
|
-
break
|
|
1153
|
-
case 4:
|
|
1154
|
-
// Taunt - add taunt status to self
|
|
1155
|
-
|
|
1156
|
-
// Check if gotchi already has taunt status
|
|
1157
|
-
if (attackingGotchi.statuses.includes('taunt')) {
|
|
1158
|
-
specialNotDone = true
|
|
1159
|
-
break
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
if (!addStatusToGotchi(attackingGotchi, 'taunt')) {
|
|
1163
|
-
specialNotDone = true
|
|
1164
|
-
break
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
effects = [
|
|
1168
|
-
{
|
|
1169
|
-
target: attackingGotchi.id,
|
|
1170
|
-
outcome: 'success',
|
|
1171
|
-
statuses: ['taunt']
|
|
1172
|
-
}
|
|
1173
|
-
]
|
|
1174
|
-
break
|
|
1175
|
-
case 5:
|
|
1176
|
-
// Curse - attack random enemy for 50% damage, apply fear status and remove all buffs
|
|
1177
|
-
|
|
1178
|
-
const curseTarget = getTarget(defendingTeam, rng)
|
|
1179
|
-
|
|
1180
|
-
const curseTargetStatuses = ['fear']
|
|
1181
|
-
|
|
1182
|
-
effects = attack(attackingGotchi, attackingTeam, defendingTeam, [curseTarget], rng, {
|
|
1183
|
-
multiplier: MULTS.CURSE_DAMAGE,
|
|
1184
|
-
statuses: curseTargetStatuses,
|
|
1185
|
-
cannotBeCountered: true,
|
|
1186
|
-
inflictPassiveStatuses: false,
|
|
1187
|
-
speedPenalty: MULTS.CURSE_SPEED_PENALTY,
|
|
1188
|
-
noResistSpeedPenalty: true
|
|
1189
|
-
})
|
|
1190
|
-
|
|
1191
|
-
const removeRandomBuff = (target) => {
|
|
1192
|
-
const modifiedTarget = getModifiedStats(target)
|
|
1193
|
-
|
|
1194
|
-
if (rng() > modifiedTarget.resist / 100) {
|
|
1195
|
-
const buffsToRemove = target.statuses.filter((status) => BUFFS.includes(status))
|
|
1196
|
-
|
|
1197
|
-
if (buffsToRemove.length) {
|
|
1198
|
-
const randomBuff = buffsToRemove[Math.floor(rng() * buffsToRemove.length)]
|
|
1199
|
-
statusesExpired.push({
|
|
1200
|
-
target: target.id,
|
|
1201
|
-
status: randomBuff
|
|
1202
|
-
})
|
|
1203
|
-
|
|
1204
|
-
// Remove first instance of randomBuff (there may be multiple)
|
|
1205
|
-
const index = target.statuses.indexOf(randomBuff)
|
|
1206
|
-
target.statuses.splice(index, 1)
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
if (effects[0] && effects[0].outcome === 'success') {
|
|
1212
|
-
// 1 chance to remove a random buff
|
|
1213
|
-
removeRandomBuff(curseTarget)
|
|
1214
|
-
|
|
1215
|
-
} else if (effects[0] && effects[0].outcome === 'critical') {
|
|
1216
|
-
// 2 chances to remove a random buff
|
|
1217
|
-
removeRandomBuff(curseTarget)
|
|
1218
|
-
removeRandomBuff(curseTarget)
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
break
|
|
1222
|
-
case 6:
|
|
1223
|
-
// Blessing - Heal all non-healer allies and remove all debuffs
|
|
1224
|
-
|
|
1225
|
-
// Get all alive non-healer allies on the attacking team
|
|
1226
|
-
const gotchisToHeal = getAlive(attackingTeam).filter(x => x.special.id !== 6)
|
|
1227
|
-
|
|
1228
|
-
// Heal all allies for multiple of healers resistance
|
|
1229
|
-
gotchisToHeal.forEach((gotchi) => {
|
|
1230
|
-
let amountToHeal
|
|
1231
|
-
|
|
1232
|
-
// If gotchi has 'cleansing_aura' status, increase heal amount
|
|
1233
|
-
if (attackingGotchi.statuses.includes('cleansing_aura')) {
|
|
1234
|
-
amountToHeal = Math.round(modifiedAttackingGotchi.resist * MULTS.CLEANSING_AURA_HEAL)
|
|
1235
|
-
} else {
|
|
1236
|
-
amountToHeal = Math.round(modifiedAttackingGotchi.resist * MULTS.BLESSING_HEAL)
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
// Check for crit
|
|
1240
|
-
const isCrit = rng() < modifiedAttackingGotchi.crit / 100
|
|
1241
|
-
if (isCrit) {
|
|
1242
|
-
amountToHeal = Math.round(amountToHeal * MULTS.BLESSING_HEAL_CRIT_MULTIPLIER)
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
// Apply speed penalty
|
|
1246
|
-
const speedPenalty = (modifiedAttackingGotchi.speed - 100) * MULTS.BLESSING_HEAL_SPEED_PENALTY
|
|
1247
|
-
if (speedPenalty > 0) amountToHeal -= speedPenalty
|
|
1248
|
-
|
|
1249
|
-
// Don't allow amountToHeal to be more than the difference between current health and max health
|
|
1250
|
-
if (amountToHeal > gotchi.originalStats.health - gotchi.health) {
|
|
1251
|
-
amountToHeal = gotchi.originalStats.health - gotchi.health
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
gotchi.health += amountToHeal
|
|
1255
|
-
|
|
1256
|
-
if (amountToHeal) {
|
|
1257
|
-
effects.push({
|
|
1258
|
-
target: gotchi.id,
|
|
1259
|
-
outcome: isCrit ? 'critical' : 'success',
|
|
1260
|
-
damage: -Math.abs(amountToHeal)
|
|
1261
|
-
})
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
// Remove all debuffs
|
|
1265
|
-
// Add removed debuffs to statusesExpired
|
|
1266
|
-
gotchi.statuses.forEach((status) => {
|
|
1267
|
-
if (DEBUFFS.includes(status)) {
|
|
1268
|
-
statusesExpired.push({
|
|
1269
|
-
target: gotchi.id,
|
|
1270
|
-
status
|
|
1271
|
-
})
|
|
1272
|
-
}
|
|
1273
|
-
})
|
|
1274
|
-
|
|
1275
|
-
// Remove all debuffs from gotchi
|
|
1276
|
-
gotchi.statuses = gotchi.statuses.filter((status) => !DEBUFFS.includes(status))
|
|
1277
|
-
})
|
|
1278
|
-
|
|
1279
|
-
// If no allies have been healed and no debuffs removed, then special attack not done
|
|
1280
|
-
if (!effects.length && !statusesExpired.length) {
|
|
1281
|
-
specialNotDone = true
|
|
1282
|
-
break
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
break
|
|
1286
|
-
case 7:
|
|
1287
|
-
// Thunder - Attack all enemies for 50% damage and apply stun status
|
|
1288
|
-
|
|
1289
|
-
const thunderTargets = getAlive(defendingTeam)
|
|
1290
|
-
|
|
1291
|
-
// Check if leader passive is 'arcane_thunder' then apply stun status
|
|
1292
|
-
if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
|
|
1293
|
-
const stunStatuses = ['stun']
|
|
1294
|
-
|
|
1295
|
-
effects = attack(attackingGotchi, attackingTeam, defendingTeam, thunderTargets, rng, {
|
|
1296
|
-
multiplier: modifiedAttackingGotchi.speed > 100 ? MULTS.CHANNEL_THE_COVEN_DAMAGE_FAST : MULTS.CHANNEL_THE_COVEN_DAMAGE_SLOW,
|
|
1297
|
-
statuses: stunStatuses,
|
|
1298
|
-
cannotBeCountered: true,
|
|
1299
|
-
inflictPassiveStatuses: false
|
|
1300
|
-
})
|
|
1301
|
-
} else {
|
|
1302
|
-
effects = attack(attackingGotchi, attackingTeam, defendingTeam, thunderTargets, rng, {
|
|
1303
|
-
multiplier: modifiedAttackingGotchi.speed > 100 ? MULTS.THUNDER_DAMAGE_FAST : MULTS.THUNDER_DAMAGE_SLOW,
|
|
1304
|
-
cannotBeCountered: true,
|
|
1305
|
-
inflictPassiveStatuses: false
|
|
1306
|
-
})
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
break
|
|
1310
|
-
case 8:
|
|
1311
|
-
// Devestating Smash - Attack random enemy for 200% damage
|
|
1312
|
-
|
|
1313
|
-
const smashTarget = getTarget(defendingTeam, rng)
|
|
1314
|
-
|
|
1315
|
-
effects = attack(attackingGotchi, attackingTeam, defendingTeam, [smashTarget], rng, {
|
|
1316
|
-
multiplier: MULTS.DEVESTATING_SMASH_DAMAGE,
|
|
1317
|
-
cannotBeCountered: true,
|
|
1318
|
-
inflictPassiveStatuses: false
|
|
1319
|
-
})
|
|
1320
|
-
|
|
1321
|
-
// If crit then attack again
|
|
1322
|
-
if (effects[0].outcome === 'critical') {
|
|
1323
|
-
const aliveEnemies = getAlive(defendingTeam)
|
|
1324
|
-
|
|
1325
|
-
if (aliveEnemies.length) {
|
|
1326
|
-
const target = getTarget(defendingTeam, rng)
|
|
1327
|
-
|
|
1328
|
-
effects.push(...attack(attackingGotchi, attackingTeam, defendingTeam, [target], rng, {
|
|
1329
|
-
multiplier: MULTS.DEVESTATING_SMASH_DAMAGE,
|
|
1330
|
-
cannotBeCountered: true,
|
|
1331
|
-
inflictPassiveStatuses: false
|
|
1332
|
-
}))
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
// If leader passive is 'Clan momentum', attack again
|
|
1337
|
-
if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
|
|
1338
|
-
// Check if any enemies are alive
|
|
1339
|
-
const aliveEnemies = getAlive(defendingTeam)
|
|
1340
|
-
|
|
1341
|
-
if (aliveEnemies.length) {
|
|
1342
|
-
// Do an extra devestating smash
|
|
1343
|
-
const target = getTarget(defendingTeam, rng)
|
|
1344
|
-
|
|
1345
|
-
effects.push(...attack(attackingGotchi, attackingTeam, defendingTeam, [target], rng, {
|
|
1346
|
-
multiplier: MULTS.CLAN_MOMENTUM_DAMAGE,
|
|
1347
|
-
cannotBeCountered: true,
|
|
1348
|
-
inflictPassiveStatuses: false
|
|
1349
|
-
}))
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
break
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
return {
|
|
1357
|
-
effects,
|
|
1358
|
-
statusesExpired,
|
|
1359
|
-
specialNotDone
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
module.exports = {
|
|
1364
|
-
getFormationPosition,
|
|
1365
|
-
getModifiedStats,
|
|
1366
|
-
gameLoop
|
|
1
|
+
const seedrandom = require('seedrandom')
|
|
2
|
+
const ZSchema = require('z-schema')
|
|
3
|
+
const validator = new ZSchema()
|
|
4
|
+
const { GameError } = require('../../utils/errors')
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
PASSIVES,
|
|
8
|
+
BUFF_MULT_EFFECTS,
|
|
9
|
+
BUFF_FLAT_EFFECTS,
|
|
10
|
+
DEBUFF_MULT_EFFECTS,
|
|
11
|
+
DEBUFF_FLAT_EFFECTS,
|
|
12
|
+
DEBUFFS,
|
|
13
|
+
BUFFS,
|
|
14
|
+
MULTS
|
|
15
|
+
} = require('./constants')
|
|
16
|
+
|
|
17
|
+
// Get only alive gotchis in a team
|
|
18
|
+
const getAlive = (team, row) => {
|
|
19
|
+
if (row) {
|
|
20
|
+
return team.formation[row].filter(x => x).filter(x => x.health > 0)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return [...team.formation.front, ...team.formation.back].filter(x => x).filter(x => x.health > 0)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get the formation position of a gotchi
|
|
28
|
+
* @param {Object} team1 An in-game team object
|
|
29
|
+
* @param {Object} team2 An in-game team object
|
|
30
|
+
* @param {Number} gotchiId The id of the gotchi
|
|
31
|
+
* @returns {Object} position The formation position of the gotchi
|
|
32
|
+
* @returns {Number} position.team The team the gotchi is on
|
|
33
|
+
* @returns {String} position.row The row the gotchi is on
|
|
34
|
+
* @returns {Number} position.position The position of the gotchi in the row
|
|
35
|
+
* @returns {null} position null if the gotchi is not found
|
|
36
|
+
**/
|
|
37
|
+
const getFormationPosition = (team1, team2, gotchiId) => {
|
|
38
|
+
const team1FrontIndex = team1.formation.front.findIndex(x => x && x.id === gotchiId)
|
|
39
|
+
|
|
40
|
+
if (team1FrontIndex !== -1) return {
|
|
41
|
+
team: 1,
|
|
42
|
+
row: 'front',
|
|
43
|
+
position: team1FrontIndex,
|
|
44
|
+
name: team1.formation.front[team1FrontIndex].name
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const team1BackIndex = team1.formation.back.findIndex(x => x && x.id === gotchiId)
|
|
48
|
+
|
|
49
|
+
if (team1BackIndex !== -1) return {
|
|
50
|
+
team: 1,
|
|
51
|
+
row: 'back',
|
|
52
|
+
position: team1BackIndex,
|
|
53
|
+
name: team1.formation.back[team1BackIndex].name
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const team2FrontIndex = team2.formation.front.findIndex(x => x && x.id === gotchiId)
|
|
57
|
+
|
|
58
|
+
if (team2FrontIndex !== -1) return {
|
|
59
|
+
team: 2,
|
|
60
|
+
row: 'front',
|
|
61
|
+
position: team2FrontIndex,
|
|
62
|
+
name: team2.formation.front[team2FrontIndex].name
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const team2BackIndex = team2.formation.back.findIndex(x => x && x.id === gotchiId)
|
|
66
|
+
|
|
67
|
+
if (team2BackIndex !== -1) return {
|
|
68
|
+
team: 2,
|
|
69
|
+
row: 'back',
|
|
70
|
+
position: team2BackIndex,
|
|
71
|
+
name: team2.formation.back[team2BackIndex].name
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get the leader gotchi of a team
|
|
79
|
+
* @param {Object} team An in-game team object
|
|
80
|
+
* @returns {Object} gotchi The leader gotchi
|
|
81
|
+
* @returns {Number} leader.id The id of the gotchi
|
|
82
|
+
* @returns {String} leader.special The special object of the gotchi
|
|
83
|
+
* @returns {String} leader.special.class The class of the special
|
|
84
|
+
**/
|
|
85
|
+
const getLeaderGotchi = (team) => {
|
|
86
|
+
const leader = [...team.formation.front, ...team.formation.back].find(x => x && x.id === team.leader)
|
|
87
|
+
|
|
88
|
+
if (!leader) throw new Error('Leader not found')
|
|
89
|
+
|
|
90
|
+
return leader
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get the next gotchi to act
|
|
95
|
+
* @param {Object} team1 An in-game team object
|
|
96
|
+
* @param {Object} team2 An in-game team object
|
|
97
|
+
* @param {Function} rng The random number generator
|
|
98
|
+
* @returns {Object} position The formation position of the gotchi
|
|
99
|
+
**/
|
|
100
|
+
const getNextToAct = (team1, team2, rng) => {
|
|
101
|
+
const aliveGotchis = [...getAlive(team1), ...getAlive(team2)]
|
|
102
|
+
|
|
103
|
+
aliveGotchis.sort((a, b) => a.actionDelay - b.actionDelay)
|
|
104
|
+
|
|
105
|
+
let toAct = aliveGotchis.filter(gotchi => gotchi.actionDelay === aliveGotchis[0].actionDelay)
|
|
106
|
+
|
|
107
|
+
// If only one gotchi can act then return it
|
|
108
|
+
if (toAct.length === 1) return getFormationPosition(team1, team2, toAct[0].id)
|
|
109
|
+
|
|
110
|
+
// Lowest speeds win tiebreaker
|
|
111
|
+
toAct.sort((a, b) => a.speed - b.speed)
|
|
112
|
+
toAct = toAct.filter(gotchi => gotchi.speed === toAct[0].speed)
|
|
113
|
+
|
|
114
|
+
// If only one gotchi can act then return it
|
|
115
|
+
|
|
116
|
+
if (toAct.length === 1) return getFormationPosition(team1, team2, toAct[0].id)
|
|
117
|
+
|
|
118
|
+
// If still tied then randomly choose
|
|
119
|
+
const randomIndex = Math.floor(rng() * toAct.length)
|
|
120
|
+
|
|
121
|
+
if (!toAct[randomIndex]) throw new Error(`No gotchi found at index ${randomIndex}`)
|
|
122
|
+
|
|
123
|
+
toAct = toAct[randomIndex]
|
|
124
|
+
return getFormationPosition(team1, team2, toAct.id)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const getTarget = (defendingTeam, rng) => {
|
|
128
|
+
// Check for taunt gotchis
|
|
129
|
+
const taunt = [...getAlive(defendingTeam, 'front'), ...getAlive(defendingTeam, 'back')].filter(gotchi => gotchi.statuses && gotchi.statuses.includes("taunt"))
|
|
130
|
+
|
|
131
|
+
if (taunt.length) {
|
|
132
|
+
if (taunt.length === 1) return taunt[0]
|
|
133
|
+
|
|
134
|
+
// If multiple taunt gotchis then randomly choose one
|
|
135
|
+
return taunt[Math.floor(rng() * taunt.length)]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Target gotchis in the front row first
|
|
139
|
+
const frontRow = getAlive(defendingTeam, 'front')
|
|
140
|
+
|
|
141
|
+
if (frontRow.length) {
|
|
142
|
+
return frontRow[Math.floor(rng() * frontRow.length)]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// If no gotchis in front row then target back row
|
|
146
|
+
const backRow = getAlive(defendingTeam, 'back')
|
|
147
|
+
|
|
148
|
+
if (backRow.length) {
|
|
149
|
+
return backRow[Math.floor(rng() * backRow.length)]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
throw new Error('No gotchis to target')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const applySpeedPenalty = (gotchi, penalty) => {
|
|
156
|
+
const speedPenalty = (gotchi.speed - 100) * penalty
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
...gotchi,
|
|
160
|
+
magic: gotchi.magic - speedPenalty,
|
|
161
|
+
physical: gotchi.physical - speedPenalty
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get the damage of an attack
|
|
167
|
+
* @param {Object} attackingTeam The attacking team
|
|
168
|
+
* @param {Object} defendingTeam The defending team
|
|
169
|
+
* @param {Object} attackingGotchi The gotchi attacking
|
|
170
|
+
* @param {Object} defendingGotchi The gotchi defending
|
|
171
|
+
* @param {Number} multiplier The damage multiplier
|
|
172
|
+
* @param {Boolean} ignoreArmor Whether to ignore armor
|
|
173
|
+
* @param {Number} speedPenalty The speed penalty to apply
|
|
174
|
+
* @returns {Number} damage The damage of the attack
|
|
175
|
+
**/
|
|
176
|
+
const getDamage = (attackingTeam, defendingTeam, attackingGotchi, defendingGotchi, multiplier, ignoreArmor, speedPenalty) => {
|
|
177
|
+
|
|
178
|
+
const attackerWithSpeedPenalty = speedPenalty ? applySpeedPenalty(attackingGotchi, speedPenalty) : attackingGotchi
|
|
179
|
+
|
|
180
|
+
// Apply any status effects
|
|
181
|
+
const modifiedAttackingsGotchi = getModifiedStats(attackerWithSpeedPenalty)
|
|
182
|
+
const modifiedDefendingGotchi = getModifiedStats(defendingGotchi)
|
|
183
|
+
|
|
184
|
+
let attackValue = attackingGotchi.attack === 'magic' ? modifiedAttackingsGotchi.magic : modifiedAttackingsGotchi.physical
|
|
185
|
+
|
|
186
|
+
// If attacking gotchi is in the front row and physical attack then apply front row physical attack bonus
|
|
187
|
+
if (getFormationPosition(attackingTeam, defendingTeam, attackingGotchi.id).row === 'front' && attackingGotchi.attack === 'physical') {
|
|
188
|
+
attackValue = Math.round(attackValue * MULTS.FRONT_ROW_PHY_ATK)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let defenseValue = attackingGotchi.attack === 'magic' ? modifiedDefendingGotchi.magic : modifiedDefendingGotchi.physical
|
|
192
|
+
|
|
193
|
+
// If defending gotchi is in the front row and the attack is physical then apply front row physical defence penalty
|
|
194
|
+
if (getFormationPosition(attackingTeam, defendingTeam, defendingGotchi.id).row === 'front' && attackingGotchi.attack === 'physical') {
|
|
195
|
+
defenseValue = Math.round(defenseValue * MULTS.FRONT_ROW_PHY_DEF)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Add armor to defense value
|
|
199
|
+
if (!ignoreArmor) defenseValue += modifiedDefendingGotchi.armor
|
|
200
|
+
|
|
201
|
+
// Calculate damage
|
|
202
|
+
let damage = Math.round((attackValue / defenseValue) * 100)
|
|
203
|
+
|
|
204
|
+
// Apply multiplier
|
|
205
|
+
if (multiplier) damage = Math.round(damage * multiplier)
|
|
206
|
+
|
|
207
|
+
// check for environment effects
|
|
208
|
+
if (defendingGotchi.environmentEffects && defendingGotchi.environmentEffects.length > 0) {
|
|
209
|
+
damage = Math.round(damage * (1 + (defendingGotchi.environmentEffects.length * 0.5)))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return damage
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Apply status effects to a gotchi
|
|
217
|
+
* @param {Object} gotchi An in-game gotchi object
|
|
218
|
+
* @returns {Object} gotchi An in-game gotchi object with modified stats
|
|
219
|
+
*/
|
|
220
|
+
const getModifiedStats = (gotchi) => {
|
|
221
|
+
const statMods = {}
|
|
222
|
+
|
|
223
|
+
gotchi.statuses.forEach(status => {
|
|
224
|
+
const statusStatMods = {}
|
|
225
|
+
|
|
226
|
+
// apply any modifier from BUFF_MULT_EFFECTS
|
|
227
|
+
if (BUFF_MULT_EFFECTS[status]) {
|
|
228
|
+
Object.keys(BUFF_MULT_EFFECTS[status]).forEach(stat => {
|
|
229
|
+
const modifier = Math.round(gotchi[stat] * BUFF_MULT_EFFECTS[status][stat])
|
|
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
|
+
const modifier = Math.round(gotchi[stat] * DEBUFF_MULT_EFFECTS[status][stat])
|
|
251
|
+
|
|
252
|
+
statusStatMods[stat] = -modifier
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// apply any modifier from DEBUFF_FLAT_EFFECTS
|
|
257
|
+
if (DEBUFF_FLAT_EFFECTS[status]) {
|
|
258
|
+
Object.keys(DEBUFF_FLAT_EFFECTS[status]).forEach(stat => {
|
|
259
|
+
if (statusStatMods[stat]) {
|
|
260
|
+
// If a mod for this status already exists, only add if the new mod is greater
|
|
261
|
+
if (DEBUFF_FLAT_EFFECTS[status][stat] < statusStatMods[stat]) statusStatMods[stat] = DEBUFF_FLAT_EFFECTS[status][stat]
|
|
262
|
+
} else {
|
|
263
|
+
statusStatMods[stat] = -DEBUFF_FLAT_EFFECTS[status][stat]
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// apply status mods
|
|
269
|
+
Object.keys(statusStatMods).forEach(stat => {
|
|
270
|
+
statMods[stat] = statMods[stat] ? statMods[stat] + statusStatMods[stat] : statusStatMods[stat]
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
const modifiedGotchi = {
|
|
275
|
+
...gotchi
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// apply stat mods
|
|
279
|
+
Object.keys(statMods).forEach(stat => {
|
|
280
|
+
if (statMods[stat] < 0) {
|
|
281
|
+
modifiedGotchi[stat] = modifiedGotchi[stat] + statMods[stat] < 0 ? 0 : modifiedGotchi[stat] + statMods[stat]
|
|
282
|
+
} else {
|
|
283
|
+
modifiedGotchi[stat] += statMods[stat]
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
return modifiedGotchi
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const calculateActionDelay = (gotchi) => {
|
|
292
|
+
// Calculate action delay and round to 3 decimal places
|
|
293
|
+
return Math.round(((100 / getModifiedStats(gotchi).speed) + Number.EPSILON) * 1000) / 1000
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const getNewActionDelay = (gotchi) => {
|
|
297
|
+
// Calculate new action delay and round to 3 decimal places
|
|
298
|
+
return Math.round((gotchi.actionDelay + calculateActionDelay(gotchi) + Number.EPSILON) * 1000) / 1000
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Simplify a team object for storage
|
|
303
|
+
* @param {Object} team An in-game team object
|
|
304
|
+
* @returns {Object} simplifiedTeam A simplified team object
|
|
305
|
+
*/
|
|
306
|
+
const simplifyTeam = (team) => {
|
|
307
|
+
return {
|
|
308
|
+
name: team.name,
|
|
309
|
+
owner: team.owner,
|
|
310
|
+
leaderId: team.leader,
|
|
311
|
+
rows: [
|
|
312
|
+
{
|
|
313
|
+
slots: team.formation.front.map((x) => {
|
|
314
|
+
return {
|
|
315
|
+
isActive: x ? true : false,
|
|
316
|
+
id: x ? x.id : null
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
slots: team.formation.back.map((x) => {
|
|
322
|
+
return {
|
|
323
|
+
isActive: x ? true : false,
|
|
324
|
+
id: x ? x.id : null
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
],
|
|
329
|
+
uiOrder: getUiOrder(team)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Get the UI order of a team (used for the front end)
|
|
335
|
+
* @param {Object} team An in-game team object
|
|
336
|
+
* @returns {Array} uiOrder An array of gotchi ids in the order they should be displayed
|
|
337
|
+
**/
|
|
338
|
+
const getUiOrder = (team) => {
|
|
339
|
+
const uiOrder = []
|
|
340
|
+
|
|
341
|
+
if (team.formation.front[0]) uiOrder.push(team.formation.front[0].id)
|
|
342
|
+
if (team.formation.back[0]) uiOrder.push(team.formation.back[0].id)
|
|
343
|
+
if (team.formation.front[1]) uiOrder.push(team.formation.front[1].id)
|
|
344
|
+
if (team.formation.back[1]) uiOrder.push(team.formation.back[1].id)
|
|
345
|
+
if (team.formation.front[2]) uiOrder.push(team.formation.front[2].id)
|
|
346
|
+
if (team.formation.back[2]) uiOrder.push(team.formation.back[2].id)
|
|
347
|
+
if (team.formation.front[3]) uiOrder.push(team.formation.front[3].id)
|
|
348
|
+
if (team.formation.back[3]) uiOrder.push(team.formation.back[3].id)
|
|
349
|
+
if (team.formation.front[4]) uiOrder.push(team.formation.front[4].id)
|
|
350
|
+
if (team.formation.back[4]) uiOrder.push(team.formation.back[4].id)
|
|
351
|
+
|
|
352
|
+
return uiOrder
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Add the leader statuses to a team
|
|
357
|
+
* @param {Object} team An in-game team object
|
|
358
|
+
**/
|
|
359
|
+
const addLeaderToTeam = (team) => {
|
|
360
|
+
// Add passive leader abilities
|
|
361
|
+
const teamLeader = getLeaderGotchi(team)
|
|
362
|
+
|
|
363
|
+
team.leaderPassive = teamLeader.special.id
|
|
364
|
+
|
|
365
|
+
// Apply leader passive statuses
|
|
366
|
+
switch (team.leaderPassive) {
|
|
367
|
+
case 1:
|
|
368
|
+
// Sharpen blades - all allies gain 'sharp_blades' status
|
|
369
|
+
getAlive(team).forEach(x => {
|
|
370
|
+
x.statuses.push(PASSIVES[team.leaderPassive - 1])
|
|
371
|
+
})
|
|
372
|
+
break
|
|
373
|
+
case 2:
|
|
374
|
+
// Cloud of Zen - Leader get 'cloud_of_zen' status
|
|
375
|
+
teamLeader.statuses.push(PASSIVES[team.leaderPassive - 1])
|
|
376
|
+
break
|
|
377
|
+
case 3:
|
|
378
|
+
// Frenzy - all allies get 'frenzy' status
|
|
379
|
+
getAlive(team).forEach(x => {
|
|
380
|
+
x.statuses.push(PASSIVES[team.leaderPassive - 1])
|
|
381
|
+
})
|
|
382
|
+
break
|
|
383
|
+
case 4:
|
|
384
|
+
// All allies get 'fortify' status
|
|
385
|
+
getAlive(team).forEach(x => {
|
|
386
|
+
x.statuses.push(PASSIVES[team.leaderPassive - 1])
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
break
|
|
390
|
+
case 5:
|
|
391
|
+
// Spread the fear - all allies get 'spread_the_fear' status
|
|
392
|
+
getAlive(team).forEach(x => {
|
|
393
|
+
x.statuses.push(PASSIVES[team.leaderPassive - 1])
|
|
394
|
+
})
|
|
395
|
+
break
|
|
396
|
+
case 6:
|
|
397
|
+
// Cleansing aura - every healer ally and every tank ally gets 'cleansing_aura' status
|
|
398
|
+
getAlive(team).forEach(x => {
|
|
399
|
+
if (x.special.id === 6 || x.special.id === 4) x.statuses.push(PASSIVES[team.leaderPassive - 1])
|
|
400
|
+
})
|
|
401
|
+
break
|
|
402
|
+
case 7:
|
|
403
|
+
// Arcane thunder - every mage ally gets 'arcane_thunder' status
|
|
404
|
+
getAlive(team).forEach(x => {
|
|
405
|
+
if (x.special.id === 7) x.statuses.push(PASSIVES[team.leaderPassive - 1])
|
|
406
|
+
})
|
|
407
|
+
break
|
|
408
|
+
case 8:
|
|
409
|
+
// Clan momentum - every Troll ally gets 'clan_momentum' status
|
|
410
|
+
getAlive(team).forEach(x => {
|
|
411
|
+
if (x.special.id === 8) x.statuses.push(PASSIVES[team.leaderPassive - 1])
|
|
412
|
+
})
|
|
413
|
+
break
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const removeLeaderPassivesFromTeam = (team) => {
|
|
418
|
+
let statusesRemoved = []
|
|
419
|
+
if (!team.leaderPassive) return statusesRemoved
|
|
420
|
+
|
|
421
|
+
// Remove leader passive statuses from team
|
|
422
|
+
getAlive(team).forEach(x => {
|
|
423
|
+
// add effects for each status removed
|
|
424
|
+
x.statuses.forEach(status => {
|
|
425
|
+
if (status === PASSIVES[team.leaderPassive - 1]) {
|
|
426
|
+
statusesRemoved.push({
|
|
427
|
+
target: x.id,
|
|
428
|
+
status: status
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
x.statuses = x.statuses.filter(x => x !== PASSIVES[team.leaderPassive - 1])
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
team.leaderPassive = null
|
|
437
|
+
|
|
438
|
+
return statusesRemoved
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const getExpiredStatuses = (team1, team2) => {
|
|
442
|
+
// If leader is dead, remove leader passive
|
|
443
|
+
let statusesExpired = []
|
|
444
|
+
if (team1.leaderPassive && !getAlive(team1).find(x => x.id === team1.leader)) {
|
|
445
|
+
// Remove leader passive statuses
|
|
446
|
+
statusesExpired = removeLeaderPassivesFromTeam(team1)
|
|
447
|
+
}
|
|
448
|
+
if (team2.leaderPassive && !getAlive(team2).find(x => x.id === team2.leader)) {
|
|
449
|
+
// Remove leader passive statuses
|
|
450
|
+
statusesExpired = removeLeaderPassivesFromTeam(team2)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return statusesExpired
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Add a status to a gotchi
|
|
458
|
+
* @param {Object} gotchi An in-game gotchi object
|
|
459
|
+
* @param {String} status The status to add
|
|
460
|
+
* @returns {Boolean} success A boolean to determine if the status was added
|
|
461
|
+
**/
|
|
462
|
+
const addStatusToGotchi = (gotchi, status) => {
|
|
463
|
+
// Check that gotchi doesn't already have max number of statuses
|
|
464
|
+
if (gotchi.statuses.filter(item => item === status).length >= MULTS.MAX_STATUSES) return false
|
|
465
|
+
|
|
466
|
+
gotchi.statuses.push(status)
|
|
467
|
+
|
|
468
|
+
return true
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const scrambleGotchiIds = (allAliveGotchis, team1, team2) => {
|
|
472
|
+
// check there's no duplicate gotchis
|
|
473
|
+
const gotchiIds = allAliveGotchis.map(x => x.id)
|
|
474
|
+
|
|
475
|
+
if (gotchiIds.length !== new Set(gotchiIds).size) {
|
|
476
|
+
// scramble gotchi ids
|
|
477
|
+
allAliveGotchis.forEach(x => {
|
|
478
|
+
const newId = Math.floor(Math.random() * 10000000)
|
|
479
|
+
|
|
480
|
+
// find gotchi in team1 or team2
|
|
481
|
+
const position = getFormationPosition(team1, team2, x.id)
|
|
482
|
+
|
|
483
|
+
// change gotchi id
|
|
484
|
+
if (position) {
|
|
485
|
+
if (position.team === 1) {
|
|
486
|
+
if (x.id === team1.leader) team1.leader = newId
|
|
487
|
+
team1.formation[position.row][position.position].id = newId
|
|
488
|
+
} else {
|
|
489
|
+
if (x.id === team2.leader) team2.leader = newId
|
|
490
|
+
team2.formation[position.row][position.position].id = newId
|
|
491
|
+
}
|
|
492
|
+
} else {
|
|
493
|
+
throw new Error('Gotchi not found in team1 or team2')
|
|
494
|
+
}
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
// check again
|
|
498
|
+
const newGotchiIds = allAliveGotchis.map(x => x.id)
|
|
499
|
+
if (newGotchiIds.length !== new Set(newGotchiIds).size) {
|
|
500
|
+
// Scramble again
|
|
501
|
+
scrambleGotchiIds(allAliveGotchis, team1, team2)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Prepare teams for battle
|
|
508
|
+
* @param {Array} allAliveGotchis An array of all alive gotchis
|
|
509
|
+
* @param {Object} team1 An in-game team object
|
|
510
|
+
* @param {Object} team2 An in-game team object
|
|
511
|
+
**/
|
|
512
|
+
const prepareTeams = (allAliveGotchis, team1, team2) => {
|
|
513
|
+
// check there's no duplicate gotchis
|
|
514
|
+
scrambleGotchiIds(allAliveGotchis, team1, team2);
|
|
515
|
+
|
|
516
|
+
allAliveGotchis.forEach(x => {
|
|
517
|
+
// Add statuses property to all gotchis
|
|
518
|
+
x.statuses = []
|
|
519
|
+
|
|
520
|
+
// Calculate initial action delay for all gotchis
|
|
521
|
+
x.actionDelay = calculateActionDelay(x)
|
|
522
|
+
|
|
523
|
+
// Calculate attack type
|
|
524
|
+
x.attack = x.magic > x.physical ? 'magic' : 'physical'
|
|
525
|
+
|
|
526
|
+
// Add original stats to all gotchis
|
|
527
|
+
// Do a deep copy of the gotchi object to avoid modifying the original object
|
|
528
|
+
x.originalStats = JSON.parse(JSON.stringify(x))
|
|
529
|
+
|
|
530
|
+
// Add environmentEffects to all gotchis
|
|
531
|
+
x.environmentEffects = []
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
// Add leader passive to team
|
|
535
|
+
addLeaderToTeam(team1)
|
|
536
|
+
addLeaderToTeam(team2);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Get log gotchi object for battle logs
|
|
541
|
+
* @param {Array} allAliveGotchis An array of all alive gotchis
|
|
542
|
+
* @returns {Array} logGotchis An array of gotchi objects for logs
|
|
543
|
+
*/
|
|
544
|
+
const getLogGotchis = (allAliveGotchis) => {
|
|
545
|
+
const logGotchis = JSON.parse(JSON.stringify(allAliveGotchis))
|
|
546
|
+
|
|
547
|
+
logGotchis.forEach(x => {
|
|
548
|
+
// Change gotchi.special.class to gotchi.special.gotchiClass to avoid conflicts with class keyword
|
|
549
|
+
x.special.gotchiClass = x.special.class
|
|
550
|
+
|
|
551
|
+
// Remove unnecessary properties to reduce log size
|
|
552
|
+
delete x.special.class
|
|
553
|
+
delete x.snapshotBlock
|
|
554
|
+
delete x.onchainId
|
|
555
|
+
delete x.brs
|
|
556
|
+
delete x.nrg
|
|
557
|
+
delete x.agg
|
|
558
|
+
delete x.spk
|
|
559
|
+
delete x.brn
|
|
560
|
+
delete x.eyc
|
|
561
|
+
delete x.eys
|
|
562
|
+
delete x.kinship
|
|
563
|
+
delete x.xp
|
|
564
|
+
delete x.actionDelay
|
|
565
|
+
delete x.attack
|
|
566
|
+
delete x.originalStats
|
|
567
|
+
delete x.environmentEffects
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
return logGotchis
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Run a battle between two teams
|
|
575
|
+
* @param {Object} team1 An in-game team object
|
|
576
|
+
* @param {Object} team2 An in-game team object
|
|
577
|
+
* @param {String} seed A seed for the random number generator
|
|
578
|
+
* @param {Boolean} debug A boolean to determine if the logs should include debug information
|
|
579
|
+
* @returns {Object} logs The battle logs
|
|
580
|
+
*/
|
|
581
|
+
const gameLoop = (team1, team2, seed, debug) => {
|
|
582
|
+
if (!team1) throw new Error("Team 1 not found")
|
|
583
|
+
if (!team2) throw new Error("Team 2 not found")
|
|
584
|
+
if (!seed) throw new Error("Seed not found")
|
|
585
|
+
|
|
586
|
+
// Validate team objects
|
|
587
|
+
const team1Validation = validator.validate(team1, teamSchema)
|
|
588
|
+
if (!team1Validation) {
|
|
589
|
+
console.error('Team 1 validation failed: ', JSON.stringify(validator.getLastErrors(), null, 2))
|
|
590
|
+
throw new Error(`Team 1 validation failed`)
|
|
591
|
+
}
|
|
592
|
+
const team2Validation = validator.validate(team2, teamSchema)
|
|
593
|
+
if (!team2Validation) {
|
|
594
|
+
console.error('Team 2 validation failed: ', JSON.stringify(validator.getLastErrors(), null, 2))
|
|
595
|
+
throw new Error(`Team 2 validation failed`)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const rng = seedrandom(seed)
|
|
599
|
+
|
|
600
|
+
const allAliveGotchis = [...getAlive(team1), ...getAlive(team2)]
|
|
601
|
+
|
|
602
|
+
prepareTeams(allAliveGotchis, team1, team2)
|
|
603
|
+
|
|
604
|
+
const logs = {
|
|
605
|
+
gotchis: getLogGotchis(allAliveGotchis),
|
|
606
|
+
layout: {
|
|
607
|
+
teams: [
|
|
608
|
+
simplifyTeam(team1),
|
|
609
|
+
simplifyTeam(team2)
|
|
610
|
+
]
|
|
611
|
+
},
|
|
612
|
+
turns: []
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// Used for turn by turn health and status summaries
|
|
616
|
+
// Deleted if not in development or no errors
|
|
617
|
+
logs.debug = []
|
|
618
|
+
|
|
619
|
+
let turnCounter = 0
|
|
620
|
+
let draw = false
|
|
621
|
+
|
|
622
|
+
try {
|
|
623
|
+
while (getAlive(team1).length && getAlive(team2).length) {
|
|
624
|
+
// Check if turnCounter is ready for environment effects (99,149,199, etc)
|
|
625
|
+
let isEnvironmentTurn = [99, 149, 199, 249, 299].includes(turnCounter)
|
|
626
|
+
if (isEnvironmentTurn) {
|
|
627
|
+
allAliveGotchis.forEach(x => {
|
|
628
|
+
x.environmentEffects.push('damage_up')
|
|
629
|
+
})
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const turnLogs = executeTurn(team1, team2, rng)
|
|
633
|
+
|
|
634
|
+
// Check if turnCounter is ready for environment effects (99,149,199, etc)
|
|
635
|
+
if (isEnvironmentTurn) turnLogs.environmentEffects = ['damage_up']
|
|
636
|
+
|
|
637
|
+
if (MULTS.EXPIRE_LEADERSKILL) {
|
|
638
|
+
turnLogs.statusesExpired = [...turnLogs.statusesExpired, ...getExpiredStatuses(team1, team2)]
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
logs.turns.push({index: turnCounter, ...turnLogs})
|
|
642
|
+
|
|
643
|
+
if (debug) {
|
|
644
|
+
logs.debug.push({
|
|
645
|
+
turn: turnCounter,
|
|
646
|
+
user: logs.turns[logs.turns.length - 1].action.user,
|
|
647
|
+
move: logs.turns[logs.turns.length - 1].action.name,
|
|
648
|
+
team1: getAlive(team1).map((x) => {
|
|
649
|
+
return `Id: ${x.id}, Name: ${x.name}, Health: ${x.health}, Statuses: ${x.statuses}`
|
|
650
|
+
}),
|
|
651
|
+
team2: getAlive(team2).map((x) => {
|
|
652
|
+
return `Id: ${x.id}, Name: ${x.name}, Health: ${x.health}, Statuses: ${x.statuses}`
|
|
653
|
+
})
|
|
654
|
+
})
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
turnCounter++
|
|
658
|
+
}
|
|
659
|
+
} catch (e) {
|
|
660
|
+
console.error(e)
|
|
661
|
+
throw new GameError('Game loop failed', logs)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (draw) {
|
|
665
|
+
logs.result = {
|
|
666
|
+
winner: 0,
|
|
667
|
+
loser: 0,
|
|
668
|
+
winningTeam: [],
|
|
669
|
+
numOfTurns: logs.turns.length
|
|
670
|
+
}
|
|
671
|
+
} else {
|
|
672
|
+
logs.result = {
|
|
673
|
+
winner: getAlive(team1).length ? 1 : 2,
|
|
674
|
+
loser: getAlive(team1).length ? 2 : 1,
|
|
675
|
+
winningTeam: getAlive(team1).length ? getAlive(team1) : getAlive(team2),
|
|
676
|
+
numOfTurns: logs.turns.length
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// trim winning team objects
|
|
680
|
+
logs.result.winningTeam = logs.result.winningTeam.map((gotchi) => {
|
|
681
|
+
return {
|
|
682
|
+
id: gotchi.id,
|
|
683
|
+
name: gotchi.name,
|
|
684
|
+
brs: gotchi.brs,
|
|
685
|
+
health: gotchi.health
|
|
686
|
+
}
|
|
687
|
+
})
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (!debug) delete logs.debug
|
|
691
|
+
|
|
692
|
+
return logs
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Attack one or more gotchis. This mutates the defending gotchis health
|
|
697
|
+
* @param {Object} attackingGotchi The attacking gotchi object
|
|
698
|
+
* @param {Array} attackingTeam A team object for the attacking team
|
|
699
|
+
* @param {Array} defendingTeam A team object for the defending team
|
|
700
|
+
* @param {Array} defendingTargets An array of gotchis to attack
|
|
701
|
+
* @param {Function} rng The random number generator
|
|
702
|
+
* @param {Object} options An object of options
|
|
703
|
+
* @param {Boolean} options.ignoreArmor Ignore the defending gotchi's defense
|
|
704
|
+
* @param {Boolean} options.multiplier A multiplier to apply to the damage
|
|
705
|
+
* @param {Boolean} options.statuses An array of status effects to apply
|
|
706
|
+
* @param {Boolean} options.cannotBeEvaded A boolean to determine if the attack can be evaded
|
|
707
|
+
* @param {Boolean} options.cannotBeResisted A boolean to determine if the attack can be resisted
|
|
708
|
+
* @param {Boolean} options.cannotBeCountered A boolean to determine if the attack can be countered
|
|
709
|
+
* @param {Boolean} options.inflictPassiveStatuses A boolean to determine if passive statuses should be inflicted
|
|
710
|
+
* @returns {Array} effects An array of effects to apply
|
|
711
|
+
*/
|
|
712
|
+
const attack = (attackingGotchi, attackingTeam, defendingTeam, defendingTargets, rng, options = {
|
|
713
|
+
ignoreArmor: false,
|
|
714
|
+
multiplier: 1,
|
|
715
|
+
statuses: [],
|
|
716
|
+
cannotBeEvaded: false,
|
|
717
|
+
critCannotBeEvaded: false,
|
|
718
|
+
cannotBeResisted: false,
|
|
719
|
+
cannotBeCountered: false,
|
|
720
|
+
inflictPassiveStatuses: true,
|
|
721
|
+
speedPenalty: 0,
|
|
722
|
+
noResistSpeedPenalty: false
|
|
723
|
+
}) => {
|
|
724
|
+
const effects = []
|
|
725
|
+
if (!options.ignoreArmor) options.ignoreArmor = false
|
|
726
|
+
if (!options.multiplier) options.multiplier = 1
|
|
727
|
+
if (!options.statuses) options.statuses = []
|
|
728
|
+
if (!options.cannotBeEvaded) options.cannotBeEvaded = false
|
|
729
|
+
if (!options.critCannotBeEvaded) options.critCannotBeEvaded = false
|
|
730
|
+
if (!options.cannotBeResisted) options.cannotBeResisted = false
|
|
731
|
+
if (!options.cannotBeCountered) options.cannotBeCountered = false
|
|
732
|
+
if (!options.inflictPassiveStatuses) options.inflictPassiveStatuses = false
|
|
733
|
+
if (!options.speedPenalty) options.speedPenalty = 0
|
|
734
|
+
if (!options.noResistSpeedPenalty) options.noResistSpeedPenalty = false
|
|
735
|
+
|
|
736
|
+
// If inflictPassiveStatuses then add leaderPassive status effects to attackingGotchi
|
|
737
|
+
if (options.inflictPassiveStatuses) {
|
|
738
|
+
// If attacking gotchi has 'sharp_blades' status, add 'bleed' to statuses
|
|
739
|
+
if (attackingGotchi.statuses.includes('sharp_blades')) {
|
|
740
|
+
if (rng() < MULTS.SHARP_BLADES_BLEED_CHANCE) options.statuses.push('bleed')
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// If attacking gotchi has 'spread_the_fear' status, add 'fear' to statuses
|
|
744
|
+
if (attackingGotchi.statuses.includes('spread_the_fear')) {
|
|
745
|
+
// Reduce the chance to spread the fear if attacking gotchi has speed over 100
|
|
746
|
+
const spreadTheFearChance = attackingGotchi.speed > 100 ? MULTS.SPREAD_THE_FEAR_CHANCE - MULTS.SPREAD_THE_FEAR_SPEED_PENALTY : MULTS.SPREAD_THE_FEAR_CHANCE
|
|
747
|
+
if (rng() < spreadTheFearChance) options.statuses.push('fear')
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
defendingTargets.forEach((defendingGotchi) => {
|
|
752
|
+
// Check attacking gotchi hasn't been killed by a counter
|
|
753
|
+
if (attackingGotchi.health <= 0) return
|
|
754
|
+
|
|
755
|
+
const modifiedAttackingGotchi = getModifiedStats(attackingGotchi)
|
|
756
|
+
const modifiedDefendingGotchi = getModifiedStats(defendingGotchi)
|
|
757
|
+
|
|
758
|
+
// Check for crit
|
|
759
|
+
const isCrit = rng() < modifiedAttackingGotchi.crit / 100
|
|
760
|
+
if (isCrit) {
|
|
761
|
+
// Apply different crit multipliers for -nrg and +nrg gotchis
|
|
762
|
+
if (attackingGotchi.speed <= 100) {
|
|
763
|
+
options.multiplier *= MULTS.CRIT_MULTIPLIER_SLOW
|
|
764
|
+
} else {
|
|
765
|
+
options.multiplier *= MULTS.CRIT_MULTIPLIER_FAST
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
let canEvade = true
|
|
770
|
+
if (options.cannotBeEvaded) canEvade = false
|
|
771
|
+
if (isCrit && options.critCannotBeEvaded) canEvade = false
|
|
772
|
+
|
|
773
|
+
const damage = getDamage(attackingTeam, defendingTeam, attackingGotchi, defendingGotchi, options.multiplier, options.ignoreArmor, options.speedPenalty)
|
|
774
|
+
|
|
775
|
+
let effect = {
|
|
776
|
+
target: defendingGotchi.id,
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Check for miss
|
|
780
|
+
if (rng() > modifiedAttackingGotchi.accuracy / 100) {
|
|
781
|
+
effect.outcome = 'miss'
|
|
782
|
+
effects.push(effect)
|
|
783
|
+
} else if (canEvade && rng() < modifiedDefendingGotchi.evade / 100){
|
|
784
|
+
effect.outcome = 'evade'
|
|
785
|
+
effects.push(effect)
|
|
786
|
+
} else {
|
|
787
|
+
if (!options.cannotBeResisted) {
|
|
788
|
+
// Check for status effect from the move
|
|
789
|
+
options.statuses.forEach((status) => {
|
|
790
|
+
if (rng() > modifiedDefendingGotchi.resist / 100) {
|
|
791
|
+
// Attempt to add status to defending gotchi
|
|
792
|
+
if (addStatusToGotchi(defendingGotchi, status)) {
|
|
793
|
+
// If status added, add to effect
|
|
794
|
+
if (!effect.statuses) {
|
|
795
|
+
effect.statuses = [status]
|
|
796
|
+
} else {
|
|
797
|
+
effect.statuses.push(status)
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
})
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Handle damage
|
|
805
|
+
defendingGotchi.health -= damage
|
|
806
|
+
effect.damage = damage
|
|
807
|
+
effect.outcome = isCrit ? 'critical' : 'success'
|
|
808
|
+
effects.push(effect)
|
|
809
|
+
|
|
810
|
+
// Check for counter attack
|
|
811
|
+
if (
|
|
812
|
+
defendingGotchi.statuses.includes('taunt')
|
|
813
|
+
&& defendingGotchi.health > 0
|
|
814
|
+
&& !options.cannotBeCountered) {
|
|
815
|
+
|
|
816
|
+
// Chance to counter based on speed over 100
|
|
817
|
+
let chanceToCounter = defendingGotchi.speed - 100
|
|
818
|
+
|
|
819
|
+
if (chanceToCounter < MULTS.COUNTER_CHANCE_MIN) chanceToCounter = MULTS.COUNTER_CHANCE_MIN
|
|
820
|
+
|
|
821
|
+
// Add a higher chance to counter if gotchi has 'fortify' status
|
|
822
|
+
if (defendingGotchi.statuses.includes('fortify')) chanceToCounter += MULTS.FORTIFY_COUNTER_CHANCE
|
|
823
|
+
|
|
824
|
+
if (rng() < chanceToCounter / 100) {
|
|
825
|
+
const counterDamage = getDamage(defendingTeam, attackingTeam, defendingGotchi, attackingGotchi, MULTS.COUNTER_DAMAGE, false, 0)
|
|
826
|
+
|
|
827
|
+
attackingGotchi.health -= counterDamage
|
|
828
|
+
|
|
829
|
+
effects.push({
|
|
830
|
+
target: attackingGotchi.id,
|
|
831
|
+
source: defendingGotchi.id,
|
|
832
|
+
damage: counterDamage,
|
|
833
|
+
outcome: 'counter'
|
|
834
|
+
})
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
return effects
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Deal with start of turn status effects
|
|
844
|
+
const handleStatusEffects = (attackingGotchi, attackingTeam, defendingTeam, rng) => {
|
|
845
|
+
const statusEffects = []
|
|
846
|
+
const passiveEffects = []
|
|
847
|
+
|
|
848
|
+
const modifiedAttackingGotchi = getModifiedStats(attackingGotchi)
|
|
849
|
+
|
|
850
|
+
// Check for cleansing_aura
|
|
851
|
+
// if (attackingGotchi.statuses.includes('cleansing_aura')) {
|
|
852
|
+
// // Remove all debuffs from all allies
|
|
853
|
+
// const aliveAllies = getAlive(attackingTeam)
|
|
854
|
+
// aliveAllies.forEach((ally) => {
|
|
855
|
+
// ally.statuses.forEach((status) => {
|
|
856
|
+
// if (DEBUFFS.includes(status)) {
|
|
857
|
+
// passiveEffects.push({
|
|
858
|
+
// source: attackingGotchi.id,
|
|
859
|
+
// target: ally.id,
|
|
860
|
+
// status,
|
|
861
|
+
// damage: 0,
|
|
862
|
+
// remove: true
|
|
863
|
+
// })
|
|
864
|
+
// }
|
|
865
|
+
// })
|
|
866
|
+
|
|
867
|
+
// // Remove status effects
|
|
868
|
+
// ally.statuses = ally.statuses.filter((status) => !DEBUFFS.includes(status))
|
|
869
|
+
// })
|
|
870
|
+
// }
|
|
871
|
+
|
|
872
|
+
// Check for global status effects
|
|
873
|
+
const allAliveGotchis = [...getAlive(attackingTeam), ...getAlive(defendingTeam)]
|
|
874
|
+
|
|
875
|
+
allAliveGotchis.forEach((gotchi) => {
|
|
876
|
+
if (gotchi.statuses && gotchi.statuses.length) {
|
|
877
|
+
gotchi.statuses.forEach((status) => {
|
|
878
|
+
// Handle cleansing_aura (health regen)
|
|
879
|
+
if (status === 'cleansing_aura') {
|
|
880
|
+
let amountToHeal
|
|
881
|
+
|
|
882
|
+
// Check if healer
|
|
883
|
+
if (gotchi.special.id === 6) {
|
|
884
|
+
amountToHeal = Math.round(gotchi.resist * MULTS.CLEANSING_AURA_REGEN)
|
|
885
|
+
} else {
|
|
886
|
+
amountToHeal = MULTS.CLEANSING_AURA_NON_HEALER_REGEN
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Don't allow amountToHeal to be more than the difference between current health and max health
|
|
890
|
+
if (amountToHeal > gotchi.originalStats.health - gotchi.health) {
|
|
891
|
+
amountToHeal = gotchi.originalStats.health - gotchi.health
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// if amountToHeal > 0, add status effect
|
|
895
|
+
if (amountToHeal) {
|
|
896
|
+
// Add status effect
|
|
897
|
+
statusEffects.push({
|
|
898
|
+
target: gotchi.id,
|
|
899
|
+
status,
|
|
900
|
+
damage: -Math.abs(amountToHeal),
|
|
901
|
+
remove: false
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
gotchi.health += amountToHeal
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/*
|
|
909
|
+
* Handle damage effect at the bottom of the loop
|
|
910
|
+
*/
|
|
911
|
+
|
|
912
|
+
// Handle bleed
|
|
913
|
+
if (status === 'bleed') {
|
|
914
|
+
let damage = MULTS.BLEED_DAMAGE
|
|
915
|
+
|
|
916
|
+
gotchi.health -= damage
|
|
917
|
+
if (gotchi.health <= 0) gotchi.health = 0
|
|
918
|
+
|
|
919
|
+
// Add status effect
|
|
920
|
+
statusEffects.push({
|
|
921
|
+
target: gotchi.id,
|
|
922
|
+
status,
|
|
923
|
+
damage,
|
|
924
|
+
remove: false
|
|
925
|
+
})
|
|
926
|
+
}
|
|
927
|
+
})
|
|
928
|
+
}
|
|
929
|
+
})
|
|
930
|
+
|
|
931
|
+
let skipTurn = null
|
|
932
|
+
|
|
933
|
+
// Check if gotchi is dead
|
|
934
|
+
if (attackingGotchi.health <= 0) {
|
|
935
|
+
return {
|
|
936
|
+
statusEffects,
|
|
937
|
+
passiveEffects,
|
|
938
|
+
skipTurn: 'ATTACKER_DEAD'
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Check if a whole team is dead
|
|
943
|
+
if (getAlive(attackingTeam).length === 0 || getAlive(defendingTeam).length === 0) {
|
|
944
|
+
return {
|
|
945
|
+
statusEffects,
|
|
946
|
+
passiveEffects,
|
|
947
|
+
skipTurn: 'TEAM_DEAD'
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Check for turn skipping statuses
|
|
952
|
+
for (let i = 0; i < attackingGotchi.statuses.length; i++) {
|
|
953
|
+
const status = attackingGotchi.statuses[i]
|
|
954
|
+
// Fear - skip turn
|
|
955
|
+
if (status === 'fear') {
|
|
956
|
+
// Skip turn
|
|
957
|
+
statusEffects.push({
|
|
958
|
+
target: attackingGotchi.id,
|
|
959
|
+
status,
|
|
960
|
+
damage: 0,
|
|
961
|
+
remove: true
|
|
962
|
+
})
|
|
963
|
+
|
|
964
|
+
skipTurn = 'FEAR'
|
|
965
|
+
|
|
966
|
+
// Remove fear first instance of fear
|
|
967
|
+
attackingGotchi.statuses.splice(i, 1)
|
|
968
|
+
|
|
969
|
+
break
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Stun
|
|
973
|
+
if (status === 'stun') {
|
|
974
|
+
// Skip turn
|
|
975
|
+
statusEffects.push({
|
|
976
|
+
target: attackingGotchi.id,
|
|
977
|
+
status,
|
|
978
|
+
damage: 0,
|
|
979
|
+
remove: true
|
|
980
|
+
})
|
|
981
|
+
|
|
982
|
+
skipTurn = 'STUN'
|
|
983
|
+
|
|
984
|
+
// Remove first instance of stun
|
|
985
|
+
attackingGotchi.statuses.splice(i, 1)
|
|
986
|
+
|
|
987
|
+
break
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
return {
|
|
992
|
+
statusEffects,
|
|
993
|
+
passiveEffects,
|
|
994
|
+
skipTurn
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const executeTurn = (team1, team2, rng) => {
|
|
999
|
+
const nextToAct = getNextToAct(team1, team2, rng)
|
|
1000
|
+
|
|
1001
|
+
const attackingTeam = nextToAct.team === 1 ? team1 : team2
|
|
1002
|
+
const defendingTeam = nextToAct.team === 1 ? team2 : team1
|
|
1003
|
+
|
|
1004
|
+
const attackingGotchi = attackingTeam.formation[nextToAct.row][nextToAct.position]
|
|
1005
|
+
|
|
1006
|
+
let { statusEffects, passiveEffects, skipTurn } = handleStatusEffects(attackingGotchi, attackingTeam, defendingTeam, rng)
|
|
1007
|
+
let statusesExpired = []
|
|
1008
|
+
|
|
1009
|
+
let effects = []
|
|
1010
|
+
if (skipTurn) {
|
|
1011
|
+
// Increase actionDelay
|
|
1012
|
+
attackingGotchi.actionDelay = getNewActionDelay(attackingGotchi)
|
|
1013
|
+
|
|
1014
|
+
return {
|
|
1015
|
+
skipTurn,
|
|
1016
|
+
action: {
|
|
1017
|
+
user: attackingGotchi.id,
|
|
1018
|
+
name: 'auto',
|
|
1019
|
+
effects
|
|
1020
|
+
},
|
|
1021
|
+
passiveEffects,
|
|
1022
|
+
statusEffects,
|
|
1023
|
+
statusesExpired
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
let specialDone = false
|
|
1028
|
+
// Check if special attack is ready
|
|
1029
|
+
if (attackingGotchi.special.cooldown === 0) {
|
|
1030
|
+
// TODO: Check if special attack should be used
|
|
1031
|
+
|
|
1032
|
+
// Execute special attack
|
|
1033
|
+
const specialResults = specialAttack(attackingGotchi, attackingTeam, defendingTeam, rng)
|
|
1034
|
+
|
|
1035
|
+
effects = specialResults.effects
|
|
1036
|
+
statusesExpired = specialResults.statusesExpired
|
|
1037
|
+
|
|
1038
|
+
// Reset cooldown
|
|
1039
|
+
attackingGotchi.special.cooldown = 2
|
|
1040
|
+
|
|
1041
|
+
if (specialResults.specialNotDone) {
|
|
1042
|
+
// Do nothing which will lead to an auto attack
|
|
1043
|
+
} else {
|
|
1044
|
+
specialDone = true
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
} else {
|
|
1048
|
+
// Decrease cooldown
|
|
1049
|
+
attackingGotchi.special.cooldown--
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (!specialDone) {
|
|
1053
|
+
// Do an auto attack
|
|
1054
|
+
const target = getTarget(defendingTeam, rng)
|
|
1055
|
+
|
|
1056
|
+
effects = attack(attackingGotchi, attackingTeam, defendingTeam, [target], rng)
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Increase actionDelay
|
|
1060
|
+
attackingGotchi.actionDelay = getNewActionDelay(attackingGotchi)
|
|
1061
|
+
|
|
1062
|
+
return {
|
|
1063
|
+
skipTurn,
|
|
1064
|
+
action: {
|
|
1065
|
+
user: attackingGotchi.id,
|
|
1066
|
+
name: specialDone ? attackingGotchi.special.name : 'auto',
|
|
1067
|
+
effects
|
|
1068
|
+
},
|
|
1069
|
+
passiveEffects,
|
|
1070
|
+
statusEffects,
|
|
1071
|
+
statusesExpired
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Execute a special attack
|
|
1077
|
+
* @param {Object} attackingGotchi The attacking gotchi object
|
|
1078
|
+
* @param {Array} attackingTeam An array of gotchis to attack
|
|
1079
|
+
* @param {Array} defendingTeam An array of gotchis to attack
|
|
1080
|
+
* @param {Function} rng The random number generator
|
|
1081
|
+
* @returns {Array} effects An array of effects to apply
|
|
1082
|
+
**/
|
|
1083
|
+
const specialAttack = (attackingGotchi, attackingTeam, defendingTeam, rng) => {
|
|
1084
|
+
const specialId = attackingGotchi.special.id
|
|
1085
|
+
let effects = []
|
|
1086
|
+
let statusesExpired = []
|
|
1087
|
+
let specialNotDone = false
|
|
1088
|
+
|
|
1089
|
+
const modifiedAttackingGotchi = getModifiedStats(attackingGotchi)
|
|
1090
|
+
|
|
1091
|
+
switch (specialId) {
|
|
1092
|
+
case 1:
|
|
1093
|
+
// Spectral Strike - ignore armor and appply bleed status
|
|
1094
|
+
// get single target
|
|
1095
|
+
const ssTarget = getTarget(defendingTeam, rng)
|
|
1096
|
+
|
|
1097
|
+
effects = attack(attackingGotchi, attackingTeam, defendingTeam, [ssTarget], rng, {
|
|
1098
|
+
multiplier: MULTS.SPECTRAL_STRIKE_DAMAGE,
|
|
1099
|
+
ignoreArmor: true,
|
|
1100
|
+
statuses: ['bleed'],
|
|
1101
|
+
cannotBeCountered: true,
|
|
1102
|
+
cannotBeEvaded: true,
|
|
1103
|
+
inflictPassiveStatuses: false,
|
|
1104
|
+
noResistSpeedPenalty: true
|
|
1105
|
+
})
|
|
1106
|
+
break
|
|
1107
|
+
case 2:
|
|
1108
|
+
// Meditate - Boost own speed, magic, physical by 30%
|
|
1109
|
+
// If gotchi already has 2 power_up statuses, do nothing
|
|
1110
|
+
if (!addStatusToGotchi(attackingGotchi, 'power_up_2')) {
|
|
1111
|
+
specialNotDone = true
|
|
1112
|
+
break
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
effects = [
|
|
1116
|
+
{
|
|
1117
|
+
target: attackingGotchi.id,
|
|
1118
|
+
outcome: 'success',
|
|
1119
|
+
statuses: ['power_up_2']
|
|
1120
|
+
}
|
|
1121
|
+
]
|
|
1122
|
+
|
|
1123
|
+
// Check for leaderPassive 'Cloud of Zen'
|
|
1124
|
+
if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
|
|
1125
|
+
// Increase allies speed, magic and physical by 15% of the original value
|
|
1126
|
+
|
|
1127
|
+
const cloudOfZenGotchis = getAlive(attackingTeam)
|
|
1128
|
+
|
|
1129
|
+
cloudOfZenGotchis.forEach((gotchi) => {
|
|
1130
|
+
if (addStatusToGotchi(gotchi, 'power_up_1')) {
|
|
1131
|
+
effects.push({
|
|
1132
|
+
target: gotchi.id,
|
|
1133
|
+
outcome: 'success',
|
|
1134
|
+
statuses: ['power_up_1']
|
|
1135
|
+
})
|
|
1136
|
+
}
|
|
1137
|
+
})
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
break
|
|
1141
|
+
case 3:
|
|
1142
|
+
// Cleave - attack all enemies in a row (that have the most gotchis) for 75% damage
|
|
1143
|
+
// Find row with most gotchis
|
|
1144
|
+
const cleaveRow = getAlive(defendingTeam, 'front').length > getAlive(defendingTeam, 'back').length ? 'front' : 'back'
|
|
1145
|
+
|
|
1146
|
+
// Attack all gotchis in that row for 75% damage
|
|
1147
|
+
effects = attack(attackingGotchi, attackingTeam, defendingTeam, getAlive(defendingTeam, cleaveRow), rng, {
|
|
1148
|
+
multiplier: MULTS.CLEAVE_DAMAGE,
|
|
1149
|
+
cannotBeCountered: true,
|
|
1150
|
+
inflictPassiveStatuses: false
|
|
1151
|
+
})
|
|
1152
|
+
break
|
|
1153
|
+
case 4:
|
|
1154
|
+
// Taunt - add taunt status to self
|
|
1155
|
+
|
|
1156
|
+
// Check if gotchi already has taunt status
|
|
1157
|
+
if (attackingGotchi.statuses.includes('taunt')) {
|
|
1158
|
+
specialNotDone = true
|
|
1159
|
+
break
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
if (!addStatusToGotchi(attackingGotchi, 'taunt')) {
|
|
1163
|
+
specialNotDone = true
|
|
1164
|
+
break
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
effects = [
|
|
1168
|
+
{
|
|
1169
|
+
target: attackingGotchi.id,
|
|
1170
|
+
outcome: 'success',
|
|
1171
|
+
statuses: ['taunt']
|
|
1172
|
+
}
|
|
1173
|
+
]
|
|
1174
|
+
break
|
|
1175
|
+
case 5:
|
|
1176
|
+
// Curse - attack random enemy for 50% damage, apply fear status and remove all buffs
|
|
1177
|
+
|
|
1178
|
+
const curseTarget = getTarget(defendingTeam, rng)
|
|
1179
|
+
|
|
1180
|
+
const curseTargetStatuses = ['fear']
|
|
1181
|
+
|
|
1182
|
+
effects = attack(attackingGotchi, attackingTeam, defendingTeam, [curseTarget], rng, {
|
|
1183
|
+
multiplier: MULTS.CURSE_DAMAGE,
|
|
1184
|
+
statuses: curseTargetStatuses,
|
|
1185
|
+
cannotBeCountered: true,
|
|
1186
|
+
inflictPassiveStatuses: false,
|
|
1187
|
+
speedPenalty: MULTS.CURSE_SPEED_PENALTY,
|
|
1188
|
+
noResistSpeedPenalty: true
|
|
1189
|
+
})
|
|
1190
|
+
|
|
1191
|
+
const removeRandomBuff = (target) => {
|
|
1192
|
+
const modifiedTarget = getModifiedStats(target)
|
|
1193
|
+
|
|
1194
|
+
if (rng() > modifiedTarget.resist / 100) {
|
|
1195
|
+
const buffsToRemove = target.statuses.filter((status) => BUFFS.includes(status))
|
|
1196
|
+
|
|
1197
|
+
if (buffsToRemove.length) {
|
|
1198
|
+
const randomBuff = buffsToRemove[Math.floor(rng() * buffsToRemove.length)]
|
|
1199
|
+
statusesExpired.push({
|
|
1200
|
+
target: target.id,
|
|
1201
|
+
status: randomBuff
|
|
1202
|
+
})
|
|
1203
|
+
|
|
1204
|
+
// Remove first instance of randomBuff (there may be multiple)
|
|
1205
|
+
const index = target.statuses.indexOf(randomBuff)
|
|
1206
|
+
target.statuses.splice(index, 1)
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
if (effects[0] && effects[0].outcome === 'success') {
|
|
1212
|
+
// 1 chance to remove a random buff
|
|
1213
|
+
removeRandomBuff(curseTarget)
|
|
1214
|
+
|
|
1215
|
+
} else if (effects[0] && effects[0].outcome === 'critical') {
|
|
1216
|
+
// 2 chances to remove a random buff
|
|
1217
|
+
removeRandomBuff(curseTarget)
|
|
1218
|
+
removeRandomBuff(curseTarget)
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
break
|
|
1222
|
+
case 6:
|
|
1223
|
+
// Blessing - Heal all non-healer allies and remove all debuffs
|
|
1224
|
+
|
|
1225
|
+
// Get all alive non-healer allies on the attacking team
|
|
1226
|
+
const gotchisToHeal = getAlive(attackingTeam).filter(x => x.special.id !== 6)
|
|
1227
|
+
|
|
1228
|
+
// Heal all allies for multiple of healers resistance
|
|
1229
|
+
gotchisToHeal.forEach((gotchi) => {
|
|
1230
|
+
let amountToHeal
|
|
1231
|
+
|
|
1232
|
+
// If gotchi has 'cleansing_aura' status, increase heal amount
|
|
1233
|
+
if (attackingGotchi.statuses.includes('cleansing_aura')) {
|
|
1234
|
+
amountToHeal = Math.round(modifiedAttackingGotchi.resist * MULTS.CLEANSING_AURA_HEAL)
|
|
1235
|
+
} else {
|
|
1236
|
+
amountToHeal = Math.round(modifiedAttackingGotchi.resist * MULTS.BLESSING_HEAL)
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// Check for crit
|
|
1240
|
+
const isCrit = rng() < modifiedAttackingGotchi.crit / 100
|
|
1241
|
+
if (isCrit) {
|
|
1242
|
+
amountToHeal = Math.round(amountToHeal * MULTS.BLESSING_HEAL_CRIT_MULTIPLIER)
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// Apply speed penalty
|
|
1246
|
+
const speedPenalty = (modifiedAttackingGotchi.speed - 100) * MULTS.BLESSING_HEAL_SPEED_PENALTY
|
|
1247
|
+
if (speedPenalty > 0) amountToHeal -= speedPenalty
|
|
1248
|
+
|
|
1249
|
+
// Don't allow amountToHeal to be more than the difference between current health and max health
|
|
1250
|
+
if (amountToHeal > gotchi.originalStats.health - gotchi.health) {
|
|
1251
|
+
amountToHeal = gotchi.originalStats.health - gotchi.health
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
gotchi.health += amountToHeal
|
|
1255
|
+
|
|
1256
|
+
if (amountToHeal) {
|
|
1257
|
+
effects.push({
|
|
1258
|
+
target: gotchi.id,
|
|
1259
|
+
outcome: isCrit ? 'critical' : 'success',
|
|
1260
|
+
damage: -Math.abs(amountToHeal)
|
|
1261
|
+
})
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Remove all debuffs
|
|
1265
|
+
// Add removed debuffs to statusesExpired
|
|
1266
|
+
gotchi.statuses.forEach((status) => {
|
|
1267
|
+
if (DEBUFFS.includes(status)) {
|
|
1268
|
+
statusesExpired.push({
|
|
1269
|
+
target: gotchi.id,
|
|
1270
|
+
status
|
|
1271
|
+
})
|
|
1272
|
+
}
|
|
1273
|
+
})
|
|
1274
|
+
|
|
1275
|
+
// Remove all debuffs from gotchi
|
|
1276
|
+
gotchi.statuses = gotchi.statuses.filter((status) => !DEBUFFS.includes(status))
|
|
1277
|
+
})
|
|
1278
|
+
|
|
1279
|
+
// If no allies have been healed and no debuffs removed, then special attack not done
|
|
1280
|
+
if (!effects.length && !statusesExpired.length) {
|
|
1281
|
+
specialNotDone = true
|
|
1282
|
+
break
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
break
|
|
1286
|
+
case 7:
|
|
1287
|
+
// Thunder - Attack all enemies for 50% damage and apply stun status
|
|
1288
|
+
|
|
1289
|
+
const thunderTargets = getAlive(defendingTeam)
|
|
1290
|
+
|
|
1291
|
+
// Check if leader passive is 'arcane_thunder' then apply stun status
|
|
1292
|
+
if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
|
|
1293
|
+
const stunStatuses = ['stun']
|
|
1294
|
+
|
|
1295
|
+
effects = attack(attackingGotchi, attackingTeam, defendingTeam, thunderTargets, rng, {
|
|
1296
|
+
multiplier: modifiedAttackingGotchi.speed > 100 ? MULTS.CHANNEL_THE_COVEN_DAMAGE_FAST : MULTS.CHANNEL_THE_COVEN_DAMAGE_SLOW,
|
|
1297
|
+
statuses: stunStatuses,
|
|
1298
|
+
cannotBeCountered: true,
|
|
1299
|
+
inflictPassiveStatuses: false
|
|
1300
|
+
})
|
|
1301
|
+
} else {
|
|
1302
|
+
effects = attack(attackingGotchi, attackingTeam, defendingTeam, thunderTargets, rng, {
|
|
1303
|
+
multiplier: modifiedAttackingGotchi.speed > 100 ? MULTS.THUNDER_DAMAGE_FAST : MULTS.THUNDER_DAMAGE_SLOW,
|
|
1304
|
+
cannotBeCountered: true,
|
|
1305
|
+
inflictPassiveStatuses: false
|
|
1306
|
+
})
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
break
|
|
1310
|
+
case 8:
|
|
1311
|
+
// Devestating Smash - Attack random enemy for 200% damage
|
|
1312
|
+
|
|
1313
|
+
const smashTarget = getTarget(defendingTeam, rng)
|
|
1314
|
+
|
|
1315
|
+
effects = attack(attackingGotchi, attackingTeam, defendingTeam, [smashTarget], rng, {
|
|
1316
|
+
multiplier: MULTS.DEVESTATING_SMASH_DAMAGE,
|
|
1317
|
+
cannotBeCountered: true,
|
|
1318
|
+
inflictPassiveStatuses: false
|
|
1319
|
+
})
|
|
1320
|
+
|
|
1321
|
+
// If crit then attack again
|
|
1322
|
+
if (effects[0].outcome === 'critical') {
|
|
1323
|
+
const aliveEnemies = getAlive(defendingTeam)
|
|
1324
|
+
|
|
1325
|
+
if (aliveEnemies.length) {
|
|
1326
|
+
const target = getTarget(defendingTeam, rng)
|
|
1327
|
+
|
|
1328
|
+
effects.push(...attack(attackingGotchi, attackingTeam, defendingTeam, [target], rng, {
|
|
1329
|
+
multiplier: MULTS.DEVESTATING_SMASH_DAMAGE,
|
|
1330
|
+
cannotBeCountered: true,
|
|
1331
|
+
inflictPassiveStatuses: false
|
|
1332
|
+
}))
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// If leader passive is 'Clan momentum', attack again
|
|
1337
|
+
if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
|
|
1338
|
+
// Check if any enemies are alive
|
|
1339
|
+
const aliveEnemies = getAlive(defendingTeam)
|
|
1340
|
+
|
|
1341
|
+
if (aliveEnemies.length) {
|
|
1342
|
+
// Do an extra devestating smash
|
|
1343
|
+
const target = getTarget(defendingTeam, rng)
|
|
1344
|
+
|
|
1345
|
+
effects.push(...attack(attackingGotchi, attackingTeam, defendingTeam, [target], rng, {
|
|
1346
|
+
multiplier: MULTS.CLAN_MOMENTUM_DAMAGE,
|
|
1347
|
+
cannotBeCountered: true,
|
|
1348
|
+
inflictPassiveStatuses: false
|
|
1349
|
+
}))
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
break
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
return {
|
|
1357
|
+
effects,
|
|
1358
|
+
statusesExpired,
|
|
1359
|
+
specialNotDone
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
module.exports = {
|
|
1364
|
+
getFormationPosition,
|
|
1365
|
+
getModifiedStats,
|
|
1366
|
+
gameLoop
|
|
1367
1367
|
}
|