gotchi-battler-game-logic 1.0.0 → 2.0.1
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/.env.example +1 -0
- package/.vscode/settings.json +4 -4
- package/Dockerfile +10 -0
- package/README.md +49 -49
- package/cloudbuild.yaml +27 -0
- package/constants/tournamentManagerAbi.json +208 -208
- package/game-logic/index.js +6 -5
- package/game-logic/v1.4/constants.js +120 -120
- package/game-logic/v1.4/index.js +1366 -1353
- package/game-logic/v1.5/index.js +8 -8
- package/game-logic/v1.6/constants.js +129 -129
- package/game-logic/v1.6/index.js +1406 -1402
- package/game-logic/v1.7/constants.js +147 -0
- package/game-logic/v1.7/helpers.js +605 -0
- package/game-logic/v1.7/index.js +796 -0
- package/index.js +13 -6
- package/package.json +26 -22
- package/schemas/team.json +262 -203
- package/scripts/balancing/createCSV.js +126 -0
- package/scripts/balancing/fixTrainingGotchis.js +260 -0
- package/scripts/balancing/processSims.js +230 -0
- package/scripts/balancing/sims.js +278 -0
- package/scripts/balancing/v1.7/class_combos.js +44 -0
- package/scripts/balancing/v1.7/setTeamPositions.js +105 -0
- package/scripts/balancing/v1.7/training_gotchis.json +20162 -0
- package/scripts/balancing/v1.7/trait_combos.json +10 -0
- package/scripts/balancing/v1.7.1/class_combos.js +44 -0
- package/scripts/balancing/v1.7.1/setTeamPositions.js +122 -0
- package/scripts/balancing/v1.7.1/training_gotchis.json +22402 -0
- package/scripts/balancing/v1.7.1/trait_combos.json +10 -0
- package/scripts/data/team1.json +213 -200
- package/scripts/data/team2.json +200 -200
- package/scripts/data/tournaments.json +66 -66
- package/scripts/runBattle.js +18 -16
- package/scripts/validateBattle.js +70 -64
- package/scripts/validateTournament.js +101 -101
- package/utils/contracts.js +12 -12
- package/utils/errors.js +29 -29
- package/utils/transforms.js +88 -47
- package/utils/validations.js +39 -39
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
const seedrandom = require('seedrandom')
|
|
2
|
+
const ZSchema = require('z-schema')
|
|
3
|
+
const validator = new ZSchema()
|
|
4
|
+
const teamSchema = require('../../schemas/team.json')
|
|
5
|
+
|
|
6
|
+
const { GameError } = require('../../utils/errors')
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
PASSIVES,
|
|
10
|
+
DEBUFFS,
|
|
11
|
+
BUFFS,
|
|
12
|
+
MULTS
|
|
13
|
+
} = require('./constants')
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
getAlive,
|
|
17
|
+
getNextToAct,
|
|
18
|
+
getTarget,
|
|
19
|
+
getDamage,
|
|
20
|
+
getModifiedStats,
|
|
21
|
+
getNewActionDelay,
|
|
22
|
+
simplifyTeam,
|
|
23
|
+
getExpiredStatuses,
|
|
24
|
+
addStatusToGotchi,
|
|
25
|
+
prepareTeams,
|
|
26
|
+
getLogGotchis
|
|
27
|
+
} = require('./helpers')
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Run a battle between two teams
|
|
31
|
+
* @param {Object} team1 An in-game team object
|
|
32
|
+
* @param {Object} team2 An in-game team object
|
|
33
|
+
* @param {String} seed A seed for the random number generator
|
|
34
|
+
* @param {Boolean} debug A boolean to determine if the logs should include debug information
|
|
35
|
+
* @returns {Object} logs The battle logs
|
|
36
|
+
*/
|
|
37
|
+
const gameLoop = (team1, team2, seed, debug) => {
|
|
38
|
+
if (!team1) throw new Error("Team 1 not found")
|
|
39
|
+
if (!team2) throw new Error("Team 2 not found")
|
|
40
|
+
if (!seed) throw new Error("Seed not found")
|
|
41
|
+
|
|
42
|
+
// Validate team objects
|
|
43
|
+
const team1Validation = validator.validate(team1, teamSchema)
|
|
44
|
+
if (!team1Validation) {
|
|
45
|
+
console.error('Team 1 validation failed: ', JSON.stringify(validator.getLastErrors(), null, 2))
|
|
46
|
+
throw new Error(`Team 1 validation failed`)
|
|
47
|
+
}
|
|
48
|
+
const team2Validation = validator.validate(team2, teamSchema)
|
|
49
|
+
if (!team2Validation) {
|
|
50
|
+
console.error('Team 2 validation failed: ', JSON.stringify(validator.getLastErrors(), null, 2))
|
|
51
|
+
throw new Error(`Team 2 validation failed`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Make deep copy of team objects to avoid modifying the original objects
|
|
55
|
+
team1 = JSON.parse(JSON.stringify(team1))
|
|
56
|
+
team2 = JSON.parse(JSON.stringify(team2))
|
|
57
|
+
|
|
58
|
+
const rng = seedrandom(seed)
|
|
59
|
+
|
|
60
|
+
const allAliveGotchis = [...getAlive(team1), ...getAlive(team2)]
|
|
61
|
+
|
|
62
|
+
prepareTeams(allAliveGotchis, team1, team2)
|
|
63
|
+
|
|
64
|
+
const logs = {
|
|
65
|
+
gotchis: getLogGotchis(allAliveGotchis),
|
|
66
|
+
layout: {
|
|
67
|
+
teams: [
|
|
68
|
+
simplifyTeam(team1),
|
|
69
|
+
simplifyTeam(team2)
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
turns: []
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Used for turn by turn health and status summaries
|
|
76
|
+
// Deleted if not in development or no errors
|
|
77
|
+
logs.debug = []
|
|
78
|
+
|
|
79
|
+
let turnCounter = 0
|
|
80
|
+
let draw = false
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
while (getAlive(team1).length && getAlive(team2).length) {
|
|
84
|
+
// Check if turnCounter is ready for environment effects (99,149,199, etc)
|
|
85
|
+
let isEnvironmentTurn = [99, 149, 199, 249, 299].includes(turnCounter)
|
|
86
|
+
if (isEnvironmentTurn) {
|
|
87
|
+
allAliveGotchis.forEach(x => {
|
|
88
|
+
x.environmentEffects.push('damage_up')
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const turnLogs = executeTurn(team1, team2, rng)
|
|
93
|
+
|
|
94
|
+
// Check if turnCounter is ready for environment effects (99,149,199, etc)
|
|
95
|
+
if (isEnvironmentTurn) turnLogs.environmentEffects = ['damage_up']
|
|
96
|
+
|
|
97
|
+
if (MULTS.EXPIRE_LEADERSKILL) {
|
|
98
|
+
turnLogs.statusesExpired = [...turnLogs.statusesExpired, ...getExpiredStatuses(team1, team2)]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
logs.turns.push({index: turnCounter, ...turnLogs})
|
|
102
|
+
|
|
103
|
+
if (debug) {
|
|
104
|
+
logs.debug.push({
|
|
105
|
+
turn: turnCounter,
|
|
106
|
+
user: logs.turns[logs.turns.length - 1].action.user,
|
|
107
|
+
move: logs.turns[logs.turns.length - 1].action.name,
|
|
108
|
+
team1: getAlive(team1).map((x) => {
|
|
109
|
+
return `Id: ${x.id}, Name: ${x.name}, Health: ${x.health}, Statuses: ${x.statuses}`
|
|
110
|
+
}),
|
|
111
|
+
team2: getAlive(team2).map((x) => {
|
|
112
|
+
return `Id: ${x.id}, Name: ${x.name}, Health: ${x.health}, Statuses: ${x.statuses}`
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
turnCounter++
|
|
118
|
+
}
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.error(e)
|
|
121
|
+
throw new GameError('Game loop failed', logs)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (draw) {
|
|
125
|
+
logs.result = {
|
|
126
|
+
winner: 0,
|
|
127
|
+
loser: 0,
|
|
128
|
+
winningTeam: [],
|
|
129
|
+
numOfTurns: logs.turns.length
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
logs.result = {
|
|
133
|
+
winner: getAlive(team1).length ? 1 : 2,
|
|
134
|
+
loser: getAlive(team1).length ? 2 : 1,
|
|
135
|
+
winningTeam: getAlive(team1).length ? getAlive(team1) : getAlive(team2),
|
|
136
|
+
numOfTurns: logs.turns.length
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// trim winning team objects
|
|
140
|
+
logs.result.winningTeam = logs.result.winningTeam.map((gotchi) => {
|
|
141
|
+
return {
|
|
142
|
+
id: gotchi.id,
|
|
143
|
+
name: gotchi.name,
|
|
144
|
+
brs: gotchi.brs,
|
|
145
|
+
health: gotchi.health
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!debug) delete logs.debug
|
|
151
|
+
|
|
152
|
+
return logs
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Attack one or more gotchis. This mutates the defending gotchis health
|
|
157
|
+
* @param {Object} attackingGotchi The attacking gotchi object
|
|
158
|
+
* @param {Array} attackingTeam A team object for the attacking team
|
|
159
|
+
* @param {Array} defendingTeam A team object for the defending team
|
|
160
|
+
* @param {Array} defendingTargets An array of gotchis to attack
|
|
161
|
+
* @param {Function} rng The random number generator
|
|
162
|
+
* @param {Object} options An object of options
|
|
163
|
+
* @param {Boolean} options.ignoreArmor Ignore the defending gotchi's defense
|
|
164
|
+
* @param {Boolean} options.multiplier A multiplier to apply to the damage
|
|
165
|
+
* @param {Boolean} options.statuses An array of status effects to apply
|
|
166
|
+
* @param {Boolean} options.cannotBeEvaded A boolean to determine if the attack can be evaded
|
|
167
|
+
* @param {Boolean} options.cannotBeResisted A boolean to determine if the attack can be resisted
|
|
168
|
+
* @param {Boolean} options.cannotBeCountered A boolean to determine if the attack can be countered
|
|
169
|
+
* @param {Boolean} options.noPassiveStatuses A boolean to determine if passive statuses should be inflicted
|
|
170
|
+
* @param {Number} options.critMultiplier Override the crit multiplier
|
|
171
|
+
* @returns {Array} effects An array of effects to apply
|
|
172
|
+
*/
|
|
173
|
+
const attack = (attackingGotchi, attackingTeam, defendingTeam, defendingTargets, rng, options) => {
|
|
174
|
+
if (!options) options = {}
|
|
175
|
+
if (!options.ignoreArmor) options.ignoreArmor = false
|
|
176
|
+
if (!options.multiplier) options.multiplier = 1
|
|
177
|
+
if (!options.statuses) options.statuses = []
|
|
178
|
+
if (!options.cannotBeEvaded) options.cannotBeEvaded = false
|
|
179
|
+
if (!options.critCannotBeEvaded) options.critCannotBeEvaded = false
|
|
180
|
+
if (!options.cannotBeResisted) options.cannotBeResisted = false
|
|
181
|
+
if (!options.cannotBeCountered) options.cannotBeCountered = false
|
|
182
|
+
if (!options.noPassiveStatuses) options.noPassiveStatuses = false
|
|
183
|
+
if (!options.speedPenalty) options.speedPenalty = 0
|
|
184
|
+
if (!options.noResistSpeedPenalty) options.noResistSpeedPenalty = false
|
|
185
|
+
if (!options.critMultiplier) options.critMultiplier = null
|
|
186
|
+
|
|
187
|
+
// If passive statuses are allowed then add leaderPassive status effects to attackingGotchi
|
|
188
|
+
if (!options.noPassiveStatuses) {
|
|
189
|
+
// If attacking gotchi has 'sharp_blades' status, add 'bleed' to statuses
|
|
190
|
+
if (attackingGotchi.statuses.includes('sharp_blades')) {
|
|
191
|
+
if (rng() < MULTS.SHARP_BLADES_BLEED_CHANCE) options.statuses.push('bleed')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// If attacking gotchi has 'spread_the_fear' status, add 'fear' to statuses
|
|
195
|
+
if (attackingGotchi.statuses.includes('spread_the_fear')) {
|
|
196
|
+
// Reduce the chance to spread the fear if attacking gotchi has speed over 100
|
|
197
|
+
const spreadTheFearChance = attackingGotchi.speed > 100 ? MULTS.SPREAD_THE_FEAR_CHANCE - MULTS.SPREAD_THE_FEAR_SPEED_PENALTY : MULTS.SPREAD_THE_FEAR_CHANCE
|
|
198
|
+
if (rng() < spreadTheFearChance) options.statuses.push('fear')
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const effects = []
|
|
203
|
+
|
|
204
|
+
defendingTargets.forEach((defendingGotchi) => {
|
|
205
|
+
// Check attacking gotchi hasn't been killed by a counter
|
|
206
|
+
if (attackingGotchi.health <= 0) return
|
|
207
|
+
|
|
208
|
+
const modifiedAttackingGotchi = getModifiedStats(attackingGotchi)
|
|
209
|
+
const modifiedDefendingGotchi = getModifiedStats(defendingGotchi)
|
|
210
|
+
|
|
211
|
+
// Check for crit
|
|
212
|
+
const isCrit = rng() < modifiedAttackingGotchi.crit / 100
|
|
213
|
+
if (isCrit) {
|
|
214
|
+
if (options.critMultiplier) {
|
|
215
|
+
options.multiplier *= options.critMultiplier
|
|
216
|
+
} else {
|
|
217
|
+
// Apply different crit multipliers for -nrg and +nrg gotchis
|
|
218
|
+
if (attackingGotchi.speed <= 100) {
|
|
219
|
+
options.multiplier *= MULTS.CRIT_MULTIPLIER_SLOW
|
|
220
|
+
} else {
|
|
221
|
+
options.multiplier *= MULTS.CRIT_MULTIPLIER_FAST
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let canEvade = true
|
|
227
|
+
if (options.cannotBeEvaded) canEvade = false
|
|
228
|
+
if (isCrit && options.critCannotBeEvaded) canEvade = false
|
|
229
|
+
|
|
230
|
+
const damage = getDamage(attackingTeam, defendingTeam, attackingGotchi, defendingGotchi, options.multiplier, options.ignoreArmor, options.speedPenalty)
|
|
231
|
+
|
|
232
|
+
let effect = {
|
|
233
|
+
target: defendingGotchi.id,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check for miss
|
|
237
|
+
if (rng() > modifiedAttackingGotchi.accuracy / 100) {
|
|
238
|
+
effect.outcome = 'miss'
|
|
239
|
+
effects.push(effect)
|
|
240
|
+
} else if (canEvade && rng() < modifiedDefendingGotchi.evade / 100){
|
|
241
|
+
effect.outcome = 'evade'
|
|
242
|
+
effects.push(effect)
|
|
243
|
+
} else {
|
|
244
|
+
if (!options.cannotBeResisted) {
|
|
245
|
+
// Check for status effect from the move
|
|
246
|
+
options.statuses.forEach((status) => {
|
|
247
|
+
if (rng() > modifiedDefendingGotchi.resist / 100) {
|
|
248
|
+
// Attempt to add status to defending gotchi
|
|
249
|
+
if (addStatusToGotchi(defendingGotchi, status)) {
|
|
250
|
+
// If status added, add to effect
|
|
251
|
+
if (!effect.statuses) {
|
|
252
|
+
effect.statuses = [status]
|
|
253
|
+
} else {
|
|
254
|
+
effect.statuses.push(status)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Handle damage
|
|
262
|
+
defendingGotchi.health -= damage
|
|
263
|
+
effect.damage = damage
|
|
264
|
+
effect.outcome = isCrit ? 'critical' : 'success'
|
|
265
|
+
effects.push(effect)
|
|
266
|
+
|
|
267
|
+
// Check for counter attack
|
|
268
|
+
if (
|
|
269
|
+
defendingGotchi.statuses.includes('taunt')
|
|
270
|
+
&& defendingGotchi.health > 0
|
|
271
|
+
&& !options.cannotBeCountered) {
|
|
272
|
+
|
|
273
|
+
// Chance to counter based on speed over 100
|
|
274
|
+
let chanceToCounter = defendingGotchi.speed - 100
|
|
275
|
+
|
|
276
|
+
if (chanceToCounter < MULTS.COUNTER_CHANCE_MIN) chanceToCounter = MULTS.COUNTER_CHANCE_MIN
|
|
277
|
+
|
|
278
|
+
// Add chance if gotchi has fortify status
|
|
279
|
+
if (defendingGotchi.statuses.includes('fortify')) {
|
|
280
|
+
chanceToCounter += MULTS.FORTIFY_COUNTER_CHANCE
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (rng() < chanceToCounter / 100) {
|
|
284
|
+
const counterDamage = getDamage(defendingTeam, attackingTeam, defendingGotchi, attackingGotchi, MULTS.COUNTER_DAMAGE, false, 0)
|
|
285
|
+
|
|
286
|
+
attackingGotchi.health -= counterDamage
|
|
287
|
+
|
|
288
|
+
effects.push({
|
|
289
|
+
target: attackingGotchi.id,
|
|
290
|
+
source: defendingGotchi.id,
|
|
291
|
+
damage: counterDamage,
|
|
292
|
+
outcome: 'counter'
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
return effects
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Deal with start of turn status effects
|
|
303
|
+
const handleStatusEffects = (attackingGotchi, attackingTeam, defendingTeam, rng) => {
|
|
304
|
+
const statusEffects = []
|
|
305
|
+
const passiveEffects = []
|
|
306
|
+
|
|
307
|
+
const modifiedAttackingGotchi = getModifiedStats(attackingGotchi)
|
|
308
|
+
|
|
309
|
+
// Check for global status effects
|
|
310
|
+
const allAliveGotchis = [...getAlive(attackingTeam), ...getAlive(defendingTeam)]
|
|
311
|
+
|
|
312
|
+
allAliveGotchis.forEach((gotchi) => {
|
|
313
|
+
const modifiedGotchi = getModifiedStats(gotchi)
|
|
314
|
+
if (gotchi.statuses && gotchi.statuses.length) {
|
|
315
|
+
gotchi.statuses.forEach((status) => {
|
|
316
|
+
// Handle cleansing_aura (health regen)
|
|
317
|
+
if (status === 'cleansing_aura') {
|
|
318
|
+
let amountToHeal
|
|
319
|
+
|
|
320
|
+
// Check if healer
|
|
321
|
+
if (gotchi.special.id === 6) {
|
|
322
|
+
amountToHeal = Math.round(modifiedGotchi.resist * MULTS.CLEANSING_AURA_REGEN)
|
|
323
|
+
} else {
|
|
324
|
+
amountToHeal = MULTS.CLEANSING_AURA_NON_HEALER_REGEN
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Don't allow amountToHeal to be more than the difference between current health and max health
|
|
328
|
+
if (amountToHeal > gotchi.originalStats.health - gotchi.health) {
|
|
329
|
+
amountToHeal = gotchi.originalStats.health - gotchi.health
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// if amountToHeal > 0, add status effect
|
|
333
|
+
if (amountToHeal) {
|
|
334
|
+
// Add status effect
|
|
335
|
+
statusEffects.push({
|
|
336
|
+
target: gotchi.id,
|
|
337
|
+
status,
|
|
338
|
+
damage: -Math.abs(amountToHeal),
|
|
339
|
+
remove: false
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
gotchi.health += amountToHeal
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/*
|
|
347
|
+
* Handle damage effect at the bottom of the loop
|
|
348
|
+
*/
|
|
349
|
+
|
|
350
|
+
// Handle bleed
|
|
351
|
+
if (status === 'bleed') {
|
|
352
|
+
let damage = MULTS.BLEED_DAMAGE
|
|
353
|
+
|
|
354
|
+
gotchi.health -= damage
|
|
355
|
+
if (gotchi.health <= 0) gotchi.health = 0
|
|
356
|
+
|
|
357
|
+
// Add status effect
|
|
358
|
+
statusEffects.push({
|
|
359
|
+
target: gotchi.id,
|
|
360
|
+
status,
|
|
361
|
+
damage,
|
|
362
|
+
remove: false
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
let skipTurn = null
|
|
370
|
+
|
|
371
|
+
// Check if gotchi is dead
|
|
372
|
+
if (attackingGotchi.health <= 0) {
|
|
373
|
+
return {
|
|
374
|
+
statusEffects,
|
|
375
|
+
passiveEffects,
|
|
376
|
+
skipTurn: 'ATTACKER_DEAD'
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Check if a whole team is dead
|
|
381
|
+
if (getAlive(attackingTeam).length === 0 || getAlive(defendingTeam).length === 0) {
|
|
382
|
+
return {
|
|
383
|
+
statusEffects,
|
|
384
|
+
passiveEffects,
|
|
385
|
+
skipTurn: 'TEAM_DEAD'
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Check for turn skipping statuses
|
|
390
|
+
for (let i = 0; i < attackingGotchi.statuses.length; i++) {
|
|
391
|
+
const status = attackingGotchi.statuses[i]
|
|
392
|
+
// Fear - skip turn
|
|
393
|
+
if (status === 'fear') {
|
|
394
|
+
// Skip turn
|
|
395
|
+
statusEffects.push({
|
|
396
|
+
target: attackingGotchi.id,
|
|
397
|
+
status,
|
|
398
|
+
damage: 0,
|
|
399
|
+
remove: true
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
skipTurn = 'FEAR'
|
|
403
|
+
|
|
404
|
+
// Remove fear first instance of fear
|
|
405
|
+
attackingGotchi.statuses.splice(i, 1)
|
|
406
|
+
|
|
407
|
+
break
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
statusEffects,
|
|
413
|
+
passiveEffects,
|
|
414
|
+
skipTurn
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const executeTurn = (team1, team2, rng) => {
|
|
419
|
+
const nextToAct = getNextToAct(team1, team2, rng)
|
|
420
|
+
|
|
421
|
+
const attackingTeam = nextToAct.team === 1 ? team1 : team2
|
|
422
|
+
const defendingTeam = nextToAct.team === 1 ? team2 : team1
|
|
423
|
+
|
|
424
|
+
const attackingGotchi = attackingTeam.formation[nextToAct.row][nextToAct.position]
|
|
425
|
+
|
|
426
|
+
let { statusEffects, passiveEffects, skipTurn } = handleStatusEffects(attackingGotchi, attackingTeam, defendingTeam, rng)
|
|
427
|
+
let statusesExpired = []
|
|
428
|
+
|
|
429
|
+
let effects = []
|
|
430
|
+
if (skipTurn) {
|
|
431
|
+
// Increase actionDelay
|
|
432
|
+
attackingGotchi.actionDelay = getNewActionDelay(attackingGotchi)
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
skipTurn,
|
|
436
|
+
action: {
|
|
437
|
+
user: attackingGotchi.id,
|
|
438
|
+
name: 'auto',
|
|
439
|
+
effects
|
|
440
|
+
},
|
|
441
|
+
passiveEffects,
|
|
442
|
+
statusEffects,
|
|
443
|
+
statusesExpired
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let specialDone = false
|
|
448
|
+
// Check if special attack is ready
|
|
449
|
+
if (attackingGotchi.special.cooldown === 0) {
|
|
450
|
+
// Execute special attack
|
|
451
|
+
const specialResults = specialAttack(attackingGotchi, attackingTeam, defendingTeam, rng)
|
|
452
|
+
|
|
453
|
+
if (specialResults.specialNotDone) {
|
|
454
|
+
// Do nothing which will lead to an auto attack
|
|
455
|
+
} else {
|
|
456
|
+
specialDone = true
|
|
457
|
+
|
|
458
|
+
effects = specialResults.effects
|
|
459
|
+
statusesExpired = specialResults.statusesExpired
|
|
460
|
+
|
|
461
|
+
// Reset cooldown
|
|
462
|
+
attackingGotchi.special.cooldown = 2
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
} else {
|
|
466
|
+
// Decrease cooldown
|
|
467
|
+
attackingGotchi.special.cooldown--
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (!specialDone) {
|
|
471
|
+
// Do an auto attack
|
|
472
|
+
const target = getTarget(defendingTeam, rng)
|
|
473
|
+
|
|
474
|
+
effects = attack(attackingGotchi, attackingTeam, defendingTeam, [target], rng)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Increase actionDelay
|
|
478
|
+
attackingGotchi.actionDelay = getNewActionDelay(attackingGotchi)
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
skipTurn,
|
|
482
|
+
action: {
|
|
483
|
+
user: attackingGotchi.id,
|
|
484
|
+
name: specialDone ? attackingGotchi.special.name : 'auto',
|
|
485
|
+
effects
|
|
486
|
+
},
|
|
487
|
+
passiveEffects,
|
|
488
|
+
statusEffects,
|
|
489
|
+
statusesExpired
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Execute a special attack
|
|
495
|
+
* @param {Object} attackingGotchi The attacking gotchi object
|
|
496
|
+
* @param {Array} attackingTeam An array of gotchis to attack
|
|
497
|
+
* @param {Array} defendingTeam An array of gotchis to attack
|
|
498
|
+
* @param {Function} rng The random number generator
|
|
499
|
+
* @returns {Array} effects An array of effects to apply
|
|
500
|
+
**/
|
|
501
|
+
const specialAttack = (attackingGotchi, attackingTeam, defendingTeam, rng) => {
|
|
502
|
+
const specialId = attackingGotchi.special.id
|
|
503
|
+
let effects = []
|
|
504
|
+
let statusesExpired = []
|
|
505
|
+
let specialNotDone = false
|
|
506
|
+
|
|
507
|
+
const modifiedAttackingGotchi = getModifiedStats(attackingGotchi)
|
|
508
|
+
|
|
509
|
+
switch (specialId) {
|
|
510
|
+
case 1:
|
|
511
|
+
// Spectral Strike - ignore armor and appply bleed status
|
|
512
|
+
// get single target
|
|
513
|
+
const ssTarget = getTarget(defendingTeam, rng)
|
|
514
|
+
|
|
515
|
+
effects = attack(attackingGotchi, attackingTeam, defendingTeam, [ssTarget], rng, {
|
|
516
|
+
multiplier: MULTS.SPECTRAL_STRIKE_DAMAGE,
|
|
517
|
+
ignoreArmor: true,
|
|
518
|
+
statuses: ['bleed'],
|
|
519
|
+
cannotBeCountered: true,
|
|
520
|
+
cannotBeEvaded: true,
|
|
521
|
+
noPassiveStatuses: true,
|
|
522
|
+
noResistSpeedPenalty: true
|
|
523
|
+
})
|
|
524
|
+
break
|
|
525
|
+
case 2:
|
|
526
|
+
// Meditate - Boost own speed, magic, physical by 30%
|
|
527
|
+
// If gotchi already has 2 power_up statuses, do nothing
|
|
528
|
+
if (!addStatusToGotchi(attackingGotchi, 'power_up_2')) {
|
|
529
|
+
specialNotDone = true
|
|
530
|
+
break
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
effects = [
|
|
534
|
+
{
|
|
535
|
+
target: attackingGotchi.id,
|
|
536
|
+
outcome: 'success',
|
|
537
|
+
statuses: ['power_up_2']
|
|
538
|
+
}
|
|
539
|
+
]
|
|
540
|
+
|
|
541
|
+
// Check for leaderPassive 'Cloud of Zen'
|
|
542
|
+
if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
|
|
543
|
+
// Increase allies speed, magic and physical by 15% of the original value
|
|
544
|
+
|
|
545
|
+
const cloudOfZenGotchis = getAlive(attackingTeam)
|
|
546
|
+
|
|
547
|
+
cloudOfZenGotchis.forEach((gotchi) => {
|
|
548
|
+
if (addStatusToGotchi(gotchi, 'power_up_1')) {
|
|
549
|
+
effects.push({
|
|
550
|
+
target: gotchi.id,
|
|
551
|
+
outcome: 'success',
|
|
552
|
+
statuses: ['power_up_1']
|
|
553
|
+
})
|
|
554
|
+
}
|
|
555
|
+
})
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
break
|
|
559
|
+
case 3:
|
|
560
|
+
// Cleave - attack all enemies in a row (that have the most gotchis) for 75% damage
|
|
561
|
+
// Find row with most gotchis
|
|
562
|
+
const cleaveRow = getAlive(defendingTeam, 'front').length > getAlive(defendingTeam, 'back').length ? 'front' : 'back'
|
|
563
|
+
|
|
564
|
+
// Attack all gotchis in that row for 75% damage
|
|
565
|
+
effects = attack(attackingGotchi, attackingTeam, defendingTeam, getAlive(defendingTeam, cleaveRow), rng, {
|
|
566
|
+
multiplier: MULTS.CLEAVE_DAMAGE,
|
|
567
|
+
cannotBeCountered: true,
|
|
568
|
+
noPassiveStatuses: true
|
|
569
|
+
})
|
|
570
|
+
break
|
|
571
|
+
case 4:
|
|
572
|
+
// Taunt - add taunt status to self
|
|
573
|
+
|
|
574
|
+
// Check if gotchi already has taunt status
|
|
575
|
+
if (attackingGotchi.statuses.includes('taunt')) {
|
|
576
|
+
specialNotDone = true
|
|
577
|
+
break
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (!addStatusToGotchi(attackingGotchi, 'taunt')) {
|
|
581
|
+
specialNotDone = true
|
|
582
|
+
break
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
effects = [
|
|
586
|
+
{
|
|
587
|
+
target: attackingGotchi.id,
|
|
588
|
+
outcome: 'success',
|
|
589
|
+
statuses: ['taunt']
|
|
590
|
+
}
|
|
591
|
+
]
|
|
592
|
+
break
|
|
593
|
+
case 5:
|
|
594
|
+
// Curse - attack random enemy for 50% damage, apply fear status and remove all buffs
|
|
595
|
+
|
|
596
|
+
const curseTarget = getTarget(defendingTeam, rng)
|
|
597
|
+
|
|
598
|
+
const curseTargetStatuses = ['fear']
|
|
599
|
+
|
|
600
|
+
effects = attack(attackingGotchi, attackingTeam, defendingTeam, [curseTarget], rng, {
|
|
601
|
+
multiplier: MULTS.CURSE_DAMAGE,
|
|
602
|
+
statuses: curseTargetStatuses,
|
|
603
|
+
cannotBeCountered: true,
|
|
604
|
+
noPassiveStatuses: true,
|
|
605
|
+
speedPenalty: MULTS.CURSE_SPEED_PENALTY,
|
|
606
|
+
noResistSpeedPenalty: true
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
const removeRandomBuff = (target) => {
|
|
610
|
+
const modifiedTarget = getModifiedStats(target)
|
|
611
|
+
|
|
612
|
+
if (rng() > modifiedTarget.resist / 100) {
|
|
613
|
+
const buffsToRemove = target.statuses.filter((status) => BUFFS.includes(status))
|
|
614
|
+
|
|
615
|
+
if (buffsToRemove.length) {
|
|
616
|
+
const randomBuff = buffsToRemove[Math.floor(rng() * buffsToRemove.length)]
|
|
617
|
+
statusesExpired.push({
|
|
618
|
+
target: target.id,
|
|
619
|
+
status: randomBuff
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
// Remove first instance of randomBuff (there may be multiple)
|
|
623
|
+
const index = target.statuses.indexOf(randomBuff)
|
|
624
|
+
target.statuses.splice(index, 1)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (effects[0] && (effects[0].outcome === 'success' || effects[0].outcome === 'critical')) {
|
|
630
|
+
// 1 chance to remove a random buff
|
|
631
|
+
removeRandomBuff(curseTarget)
|
|
632
|
+
|
|
633
|
+
if (effects[0].outcome === 'critical') {
|
|
634
|
+
// 2 chances to remove a random buff
|
|
635
|
+
removeRandomBuff(curseTarget)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// heal attacking gotchi for % of damage dealt
|
|
639
|
+
let amountToHeal = Math.round(effects[0].damage * MULTS.CURSE_HEAL)
|
|
640
|
+
|
|
641
|
+
// Don't allow amountToHeal to be more than the difference between current health and max health
|
|
642
|
+
if (amountToHeal > attackingGotchi.originalStats.health - attackingGotchi.health) {
|
|
643
|
+
amountToHeal = attackingGotchi.originalStats.health - attackingGotchi.health
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (amountToHeal) {
|
|
647
|
+
attackingGotchi.health += amountToHeal
|
|
648
|
+
|
|
649
|
+
effects.push({
|
|
650
|
+
target: attackingGotchi.id,
|
|
651
|
+
outcome: effects[0].outcome,
|
|
652
|
+
damage: -Math.abs(amountToHeal)
|
|
653
|
+
})
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
break
|
|
658
|
+
case 6:
|
|
659
|
+
// Blessing - Heal all non-healer allies and remove all debuffs
|
|
660
|
+
|
|
661
|
+
// Get all alive non-healer allies on the attacking team
|
|
662
|
+
// const gotchisToHeal = getAlive(attackingTeam).filter(x => x.special.id !== 6)
|
|
663
|
+
const gotchisToHeal = getAlive(attackingTeam)
|
|
664
|
+
|
|
665
|
+
// Heal all allies for multiple of healers resistance
|
|
666
|
+
gotchisToHeal.forEach((gotchi) => {
|
|
667
|
+
let amountToHeal
|
|
668
|
+
|
|
669
|
+
// If gotchi has 'cleansing_aura' status, increase heal amount
|
|
670
|
+
if (attackingGotchi.statuses.includes('cleansing_aura')) {
|
|
671
|
+
amountToHeal = Math.round(modifiedAttackingGotchi.resist * MULTS.CLEANSING_AURA_HEAL)
|
|
672
|
+
} else {
|
|
673
|
+
amountToHeal = Math.round(modifiedAttackingGotchi.resist * MULTS.BLESSING_HEAL)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Check for crit
|
|
677
|
+
const isCrit = rng() < modifiedAttackingGotchi.crit / 100
|
|
678
|
+
if (isCrit) {
|
|
679
|
+
amountToHeal = Math.round(amountToHeal * MULTS.BLESSING_HEAL_CRIT_MULTIPLIER)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Apply speed penalty
|
|
683
|
+
let speedPenalty
|
|
684
|
+
if (attackingGotchi.statuses.includes('cleansing_aura')) {
|
|
685
|
+
speedPenalty = Math.round((modifiedAttackingGotchi.speed - 100) * MULTS.CLEANSING_AURA_HEAL_SPEED_PENALTY)
|
|
686
|
+
} else {
|
|
687
|
+
speedPenalty = Math.round((modifiedAttackingGotchi.speed - 100) * MULTS.BLESSING_HEAL_SPEED_PENALTY)
|
|
688
|
+
}
|
|
689
|
+
if (speedPenalty > 0) amountToHeal -= speedPenalty
|
|
690
|
+
|
|
691
|
+
// Don't allow amountToHeal to be more than the difference between current health and max health
|
|
692
|
+
if (amountToHeal > gotchi.originalStats.health - gotchi.health) {
|
|
693
|
+
amountToHeal = gotchi.originalStats.health - gotchi.health
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
gotchi.health += amountToHeal
|
|
697
|
+
|
|
698
|
+
if (amountToHeal) {
|
|
699
|
+
effects.push({
|
|
700
|
+
target: gotchi.id,
|
|
701
|
+
outcome: isCrit ? 'critical' : 'success',
|
|
702
|
+
damage: -Math.abs(amountToHeal)
|
|
703
|
+
})
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Remove all debuffs
|
|
707
|
+
// Add removed debuffs to statusesExpired
|
|
708
|
+
gotchi.statuses.forEach((status) => {
|
|
709
|
+
if (DEBUFFS.includes(status)) {
|
|
710
|
+
statusesExpired.push({
|
|
711
|
+
target: gotchi.id,
|
|
712
|
+
status
|
|
713
|
+
})
|
|
714
|
+
}
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
// Remove all debuffs from gotchi
|
|
718
|
+
gotchi.statuses = gotchi.statuses.filter((status) => !DEBUFFS.includes(status))
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
// If no allies have been healed and no debuffs removed, then special attack not done
|
|
722
|
+
if (!effects.length && !statusesExpired.length) {
|
|
723
|
+
specialNotDone = true
|
|
724
|
+
break
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
break
|
|
728
|
+
case 7:
|
|
729
|
+
// Thunder - Attack all enemies for 50% damage and apply stun status
|
|
730
|
+
|
|
731
|
+
const thunderTargets = getAlive(defendingTeam)
|
|
732
|
+
|
|
733
|
+
let stunStatuses = []
|
|
734
|
+
// Check if leader passive is 'channel_the_coven' then apply stun status
|
|
735
|
+
if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
|
|
736
|
+
if (rng() < MULTS.CHANNEL_THE_COVEN_STUN_CHANCE) stunStatuses.push('stun')
|
|
737
|
+
} else {
|
|
738
|
+
if (rng() < MULTS.THUNDER_STUN_CHANCE) stunStatuses.push('stun')
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
effects = attack(attackingGotchi, attackingTeam, defendingTeam, thunderTargets, rng, {
|
|
742
|
+
multiplier: MULTS.THUNDER_DAMAGE,
|
|
743
|
+
statuses: stunStatuses,
|
|
744
|
+
cannotBeCountered: true,
|
|
745
|
+
noPassiveStatuses: true,
|
|
746
|
+
critMultiplier: MULTS.THUNDER_CRIT_MULTIPLIER
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
break
|
|
750
|
+
case 8:
|
|
751
|
+
// Devestating Smash - Attack random enemy for 200% damage
|
|
752
|
+
|
|
753
|
+
const smashTarget = getTarget(defendingTeam, rng)
|
|
754
|
+
|
|
755
|
+
effects = attack(attackingGotchi, attackingTeam, defendingTeam, [smashTarget], rng, {
|
|
756
|
+
multiplier: MULTS.DEVESTATING_SMASH_DAMAGE,
|
|
757
|
+
cannotBeCountered: true,
|
|
758
|
+
noPassiveStatuses: true
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
let anotherAttack = false
|
|
762
|
+
if (attackingGotchi.statuses.includes(PASSIVES[specialId - 1])) {
|
|
763
|
+
if (rng() < MULTS.CLAN_MOMENTUM_CHANCE) anotherAttack = true
|
|
764
|
+
} else {
|
|
765
|
+
if (rng() < MULTS.DEVESTATING_SMASH_X2_CHANCE) anotherAttack = true
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (anotherAttack) {
|
|
769
|
+
// Check if any enemies are alive
|
|
770
|
+
const aliveEnemies = getAlive(defendingTeam)
|
|
771
|
+
|
|
772
|
+
if (aliveEnemies.length) {
|
|
773
|
+
// Do an extra devestating smash
|
|
774
|
+
const target = getTarget(defendingTeam, rng)
|
|
775
|
+
|
|
776
|
+
effects.push(...attack(attackingGotchi, attackingTeam, defendingTeam, [target], rng, {
|
|
777
|
+
multiplier: MULTS.DEVESTATING_SMASH_X2_DAMAGE,
|
|
778
|
+
cannotBeCountered: true,
|
|
779
|
+
noPassiveStatuses: true
|
|
780
|
+
}))
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
break
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
return {
|
|
788
|
+
effects,
|
|
789
|
+
statusesExpired,
|
|
790
|
+
specialNotDone
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
module.exports = {
|
|
795
|
+
gameLoop
|
|
796
|
+
}
|