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