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,782 @@
|
|
|
1
|
+
const seedrandom = require('seedrandom')
|
|
2
|
+
const { InGameTeamSchema } = require('../../schemas/ingameteam')
|
|
3
|
+
const { GameError } = require('../../utils/errors')
|
|
4
|
+
|
|
5
|
+
const STATUSES = require('./statuses.json')
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
getTeamGotchis,
|
|
9
|
+
getAlive,
|
|
10
|
+
getNextToAct,
|
|
11
|
+
getTargetsFromCode,
|
|
12
|
+
getDamage,
|
|
13
|
+
getHealFromMultiplier,
|
|
14
|
+
getNewActionDelay,
|
|
15
|
+
simplifyTeam,
|
|
16
|
+
addStatusToGotchi,
|
|
17
|
+
prepareTeams,
|
|
18
|
+
getLogGotchis,
|
|
19
|
+
getTeamStats,
|
|
20
|
+
getStatusByCode,
|
|
21
|
+
getTeamSpecialBars,
|
|
22
|
+
focusCheck,
|
|
23
|
+
getCritMultiplier,
|
|
24
|
+
getModifiedStats
|
|
25
|
+
} = require('./helpers')
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Run a battle between two teams
|
|
29
|
+
* @param {Object} team1 An in-game team object
|
|
30
|
+
* @param {Object} team2 An in-game team object
|
|
31
|
+
* @param {String} seed A seed for the random number generator
|
|
32
|
+
* @param {Object} options An object containing options for the game loop
|
|
33
|
+
* @param {Boolean} options.debug A boolean to determine if the logs should include debug information
|
|
34
|
+
* @param {String} options.type A string to determine the type of the game loop
|
|
35
|
+
* @param {Object} options.campaign An object containing the campaign information
|
|
36
|
+
* @param {String} options.isBoss A boolean to determine if team2 is a boss
|
|
37
|
+
* @returns {Object} logs The battle logs
|
|
38
|
+
*/
|
|
39
|
+
const gameLoop = (team1, team2, seed, options = { debug: false, type: 'pve', campaign: {}, isBoss: false }) => {
|
|
40
|
+
if (!seed) throw new Error('Seed not found')
|
|
41
|
+
|
|
42
|
+
// Validate team objects
|
|
43
|
+
team1 = InGameTeamSchema.parse(team1)
|
|
44
|
+
team2 = InGameTeamSchema.parse(team2)
|
|
45
|
+
|
|
46
|
+
const rng = seedrandom(seed)
|
|
47
|
+
|
|
48
|
+
const allAliveGotchis = [...getAlive(team1), ...getAlive(team2)]
|
|
49
|
+
|
|
50
|
+
prepareTeams(allAliveGotchis, team1, team2)
|
|
51
|
+
|
|
52
|
+
const logs = {
|
|
53
|
+
meta: {
|
|
54
|
+
seed,
|
|
55
|
+
timestamp: new Date(),
|
|
56
|
+
type: options.type || 'pve',
|
|
57
|
+
campaign: options.campaign || {},
|
|
58
|
+
isBoss: options.isBoss || false
|
|
59
|
+
},
|
|
60
|
+
gotchis: getLogGotchis(allAliveGotchis),
|
|
61
|
+
layout: {
|
|
62
|
+
teams: [
|
|
63
|
+
simplifyTeam(team1),
|
|
64
|
+
simplifyTeam(team2)
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
turns: [],
|
|
68
|
+
result: {},
|
|
69
|
+
debug: []
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let turnCounter = 0
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
while (getAlive(team1).length && getAlive(team2).length) {
|
|
76
|
+
// Check if turnCounter is ready for environment effects (99,149,199, etc)
|
|
77
|
+
let isEnvironmentTurn = [99, 149, 199, 249, 299].includes(turnCounter)
|
|
78
|
+
if (isEnvironmentTurn) {
|
|
79
|
+
allAliveGotchis.forEach(x => {
|
|
80
|
+
x.environmentEffects.push('damage_up')
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const turnLogs = executeTurn(team1, team2, rng)
|
|
85
|
+
|
|
86
|
+
turnLogs.specialBars = getTeamSpecialBars(team1, team2)
|
|
87
|
+
|
|
88
|
+
// Check if turnCounter is ready for environment effects (99,149,199, etc)
|
|
89
|
+
if (isEnvironmentTurn) turnLogs.environmentEffects = ['damage_up']
|
|
90
|
+
|
|
91
|
+
logs.turns.push({ index: turnCounter, ...turnLogs })
|
|
92
|
+
|
|
93
|
+
if (options.debug) {
|
|
94
|
+
logs.debug.push({
|
|
95
|
+
turn: turnCounter,
|
|
96
|
+
user: logs.turns[logs.turns.length - 1].action.user,
|
|
97
|
+
move: logs.turns[logs.turns.length - 1].action.name,
|
|
98
|
+
team1: getAlive(team1).map((x) => {
|
|
99
|
+
return `Id: ${x.id}, Name: ${x.name}, Health: ${x.health}, Statuses: ${x.statuses}`
|
|
100
|
+
}),
|
|
101
|
+
team2: getAlive(team2).map((x) => {
|
|
102
|
+
return `Id: ${x.id}, Name: ${x.name}, Health: ${x.health}, Statuses: ${x.statuses}`
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
turnCounter++
|
|
108
|
+
}
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.error(e)
|
|
111
|
+
throw new GameError('Game loop failed', logs)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Add stats to logs
|
|
115
|
+
logs.result.stats = {
|
|
116
|
+
numOfTurns: turnCounter,
|
|
117
|
+
team1: getTeamStats(team1),
|
|
118
|
+
team2: getTeamStats(team2)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
logs.result.winner = getAlive(team1).length ? 1 : 2
|
|
122
|
+
logs.result.winningTeam = getTeamGotchis(team1).length ? getTeamGotchis(team1) : getTeamGotchis(team2)
|
|
123
|
+
logs.result.winningTeam = logs.result.winningTeam.map((gotchi) => {
|
|
124
|
+
return {
|
|
125
|
+
id: gotchi.id,
|
|
126
|
+
name: gotchi.name,
|
|
127
|
+
health: gotchi.health,
|
|
128
|
+
statuses: gotchi.statuses,
|
|
129
|
+
originalStats: {
|
|
130
|
+
speed: gotchi.speed,
|
|
131
|
+
attack: gotchi.attack,
|
|
132
|
+
defense: gotchi.defense,
|
|
133
|
+
criticalRate: gotchi.criticalRate,
|
|
134
|
+
criticalDamage: gotchi.criticalDamage,
|
|
135
|
+
resist: gotchi.resist,
|
|
136
|
+
focus: gotchi.focus
|
|
137
|
+
},
|
|
138
|
+
modifiedStats: {
|
|
139
|
+
speed: getModifiedStats(gotchi).speed,
|
|
140
|
+
attack: getModifiedStats(gotchi).attack,
|
|
141
|
+
defense: getModifiedStats(gotchi).defense,
|
|
142
|
+
criticalRate: getModifiedStats(gotchi).criticalRate,
|
|
143
|
+
criticalDamage: getModifiedStats(gotchi).criticalDamage,
|
|
144
|
+
resist: getModifiedStats(gotchi).resist,
|
|
145
|
+
focus: getModifiedStats(gotchi).focus
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
if (!options.debug) delete logs.debug
|
|
151
|
+
|
|
152
|
+
return logs
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const executeTurn = (team1, team2, rng) => {
|
|
156
|
+
const nextToAct = getNextToAct(team1, team2, rng)
|
|
157
|
+
|
|
158
|
+
const attackingTeam = nextToAct.team === 1 ? team1 : team2
|
|
159
|
+
const defendingTeam = nextToAct.team === 1 ? team2 : team1
|
|
160
|
+
|
|
161
|
+
const attackingGotchi = attackingTeam.formation[nextToAct.row][nextToAct.position]
|
|
162
|
+
|
|
163
|
+
let { statusEffects, skipTurn } = handleStatusEffects(attackingGotchi, attackingTeam, defendingTeam, rng)
|
|
164
|
+
let statusesExpired = []
|
|
165
|
+
|
|
166
|
+
let actionEffects = []
|
|
167
|
+
let additionalEffects = []
|
|
168
|
+
if (skipTurn) {
|
|
169
|
+
// Increase actionDelay
|
|
170
|
+
attackingGotchi.actionDelay = getNewActionDelay(attackingGotchi)
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
skipTurn,
|
|
174
|
+
action: {
|
|
175
|
+
user: attackingGotchi.id,
|
|
176
|
+
name: 'auto',
|
|
177
|
+
actionEffects,
|
|
178
|
+
additionalEffects
|
|
179
|
+
},
|
|
180
|
+
statusEffects,
|
|
181
|
+
statusesExpired
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let actionName = 'auto'
|
|
186
|
+
let repeatAttack = false
|
|
187
|
+
// Check if special attack is ready
|
|
188
|
+
if (attackingGotchi.specialBar === 100) {
|
|
189
|
+
// Execute special attack
|
|
190
|
+
actionName = attackingGotchi.specialExpanded.code
|
|
191
|
+
const specialResults = attack(attackingGotchi, attackingTeam, defendingTeam, rng, true)
|
|
192
|
+
|
|
193
|
+
actionEffects = specialResults.actionEffects
|
|
194
|
+
additionalEffects = specialResults.additionalEffects
|
|
195
|
+
statusesExpired = specialResults.statusesExpired
|
|
196
|
+
|
|
197
|
+
if (specialResults.repeatAttack) {
|
|
198
|
+
// Don't reset specialBar, just repeat the attack
|
|
199
|
+
repeatAttack = true
|
|
200
|
+
} else {
|
|
201
|
+
// Reset specialBar
|
|
202
|
+
attackingGotchi.specialBar = Math.round((100/6) * (6 - attackingGotchi.specialExpanded.cooldown))
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
// Do an auto attack
|
|
206
|
+
const attackResults = attack(attackingGotchi, attackingTeam, defendingTeam, rng)
|
|
207
|
+
|
|
208
|
+
actionEffects = attackResults.actionEffects
|
|
209
|
+
additionalEffects = attackResults.additionalEffects
|
|
210
|
+
statusesExpired = attackResults.statusesExpired
|
|
211
|
+
|
|
212
|
+
// Increase specialBar by 1/6th
|
|
213
|
+
attackingGotchi.specialBar = Math.round(attackingGotchi.specialBar + (100/6))
|
|
214
|
+
if (attackingGotchi.specialBar > 100) attackingGotchi.specialBar = 100
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Increase actionDelay
|
|
218
|
+
if (!repeatAttack) {
|
|
219
|
+
attackingGotchi.actionDelay = getNewActionDelay(attackingGotchi)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
skipTurn,
|
|
224
|
+
action: {
|
|
225
|
+
user: attackingGotchi.id,
|
|
226
|
+
name: actionName,
|
|
227
|
+
actionEffects,
|
|
228
|
+
additionalEffects
|
|
229
|
+
},
|
|
230
|
+
statusEffects,
|
|
231
|
+
statusesExpired
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Deal with start of turn status effects
|
|
236
|
+
const handleStatusEffects = (attackingGotchi, attackingTeam, defendingTeam) => {
|
|
237
|
+
const statusEffects = []
|
|
238
|
+
let skipTurn = null
|
|
239
|
+
|
|
240
|
+
// Check for global status effects
|
|
241
|
+
const allAliveGotchis = [...getAlive(attackingTeam), ...getAlive(defendingTeam)]
|
|
242
|
+
|
|
243
|
+
allAliveGotchis.forEach((gotchi) => {
|
|
244
|
+
// Get all statuses that have turnEffects
|
|
245
|
+
const turnEffectStatuses = gotchi.statuses.filter(status => {
|
|
246
|
+
const statusEffect = getStatusByCode(status)
|
|
247
|
+
return statusEffect.turnEffects
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
turnEffectStatuses.forEach((turnEffectStatus) => {
|
|
251
|
+
|
|
252
|
+
const status = getStatusByCode(turnEffectStatus)
|
|
253
|
+
|
|
254
|
+
const turnEffects = status.turnEffects
|
|
255
|
+
|
|
256
|
+
turnEffects.forEach((turnEffect) => {
|
|
257
|
+
switch (turnEffect.type) {
|
|
258
|
+
case 'heal': {
|
|
259
|
+
let amountToHeal = turnEffect.value
|
|
260
|
+
|
|
261
|
+
if (turnEffect.valueType === 'percent') {
|
|
262
|
+
amountToHeal = Math.round(gotchi.fullHealth * (amountToHeal / 100))
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Don't allow amountToHeal to be more than the difference between current health and max health
|
|
266
|
+
if (amountToHeal > gotchi.fullHealth - gotchi.health) {
|
|
267
|
+
amountToHeal = gotchi.fullHealth - gotchi.health
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (amountToHeal > 0) {
|
|
271
|
+
// Add status effect
|
|
272
|
+
statusEffects.push({
|
|
273
|
+
target: gotchi.id,
|
|
274
|
+
status: status.code,
|
|
275
|
+
damage: -amountToHeal,
|
|
276
|
+
remove: false
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
gotchi.health += amountToHeal
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
break
|
|
283
|
+
}
|
|
284
|
+
case 'damage': {
|
|
285
|
+
const damage = turnEffect.value
|
|
286
|
+
|
|
287
|
+
gotchi.health -= damage
|
|
288
|
+
if (gotchi.health <= 0) gotchi.health = 0
|
|
289
|
+
|
|
290
|
+
// Add status effect
|
|
291
|
+
statusEffects.push({
|
|
292
|
+
target: gotchi.id,
|
|
293
|
+
status: status.code,
|
|
294
|
+
damage: damage,
|
|
295
|
+
remove: false
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
gotchi.stats.dmgReceived += damage
|
|
299
|
+
|
|
300
|
+
break
|
|
301
|
+
}
|
|
302
|
+
case 'skip_turn': {
|
|
303
|
+
// Do nothing here and handle after damage/heal
|
|
304
|
+
break
|
|
305
|
+
}
|
|
306
|
+
default: {
|
|
307
|
+
throw new Error(`Invalid turn status effect type: ${turnEffect.type}`)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
// Check if gotchi is dead
|
|
315
|
+
if (attackingGotchi.health <= 0) {
|
|
316
|
+
return {
|
|
317
|
+
statusEffects,
|
|
318
|
+
skipTurn: 'attacker_dead'
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Check if a whole team is dead
|
|
323
|
+
if (getAlive(attackingTeam).length === 0 || getAlive(defendingTeam).length === 0) {
|
|
324
|
+
return {
|
|
325
|
+
statusEffects,
|
|
326
|
+
skipTurn: 'team_dead'
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Check for turn skipping statuses
|
|
331
|
+
for (let i = 0; i < attackingGotchi.statuses.length; i++) {
|
|
332
|
+
const status = getStatusByCode(attackingGotchi.statuses[i])
|
|
333
|
+
|
|
334
|
+
if (status.turnEffects) {
|
|
335
|
+
// Get first instance of a turn effect that is a skip_turn
|
|
336
|
+
const skipTurnEffect = status.turnEffects.find(turnEffect => turnEffect.type === 'skip_turn')
|
|
337
|
+
if (skipTurnEffect) {
|
|
338
|
+
statusEffects.push({
|
|
339
|
+
target: attackingGotchi.id,
|
|
340
|
+
status: status.code,
|
|
341
|
+
damage: 0,
|
|
342
|
+
remove: true
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
skipTurn = status.code
|
|
346
|
+
|
|
347
|
+
// Remove status
|
|
348
|
+
attackingGotchi.statuses.splice(i, 1)
|
|
349
|
+
|
|
350
|
+
break
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
statusEffects,
|
|
357
|
+
skipTurn
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Attack one or more gotchis. This mutates the defending gotchis health
|
|
363
|
+
* @param {Object} attackingGotchi The attacking gotchi object
|
|
364
|
+
* @param {Array} attackingTeam A team object for the attacking team
|
|
365
|
+
* @param {Array} defendingTeam A team object for the defending team
|
|
366
|
+
* @param {Function} rng The random number generator
|
|
367
|
+
* @param {Boolean} isSpecial A boolean to determine if the attack is a special attack
|
|
368
|
+
* @returns {Object} results The results of the attack
|
|
369
|
+
* @returns {Array} results.effects An array of effects to apply
|
|
370
|
+
* @returns {Array} results.statusesExpired An array of statuses that expired
|
|
371
|
+
*/
|
|
372
|
+
const attack = (attackingGotchi, attackingTeam, defendingTeam, rng, isSpecial = false) => {
|
|
373
|
+
const action = isSpecial ? attackingGotchi.specialExpanded.actionType : 'attack'
|
|
374
|
+
|
|
375
|
+
const targetCode = isSpecial ? attackingGotchi.specialExpanded.target : 'enemy_random'
|
|
376
|
+
const targets = getTargetsFromCode(targetCode, attackingGotchi, attackingTeam, defendingTeam, rng)
|
|
377
|
+
|
|
378
|
+
let actionMultipler = isSpecial ? attackingGotchi.specialExpanded.actionMultiplier : 1
|
|
379
|
+
|
|
380
|
+
const actionEffects = []
|
|
381
|
+
const additionalEffects = []
|
|
382
|
+
const statusesExpired = []
|
|
383
|
+
let repeatAttack = false
|
|
384
|
+
|
|
385
|
+
targets.forEach((target) => {
|
|
386
|
+
// The effect for the main action of the attack
|
|
387
|
+
let targetActionEffect
|
|
388
|
+
|
|
389
|
+
// For an additional effects that come for the special attack e.g. heals
|
|
390
|
+
const targetAdditionalEffects = []
|
|
391
|
+
|
|
392
|
+
// Handle action first
|
|
393
|
+
if (action === 'attack') {
|
|
394
|
+
const critMultiplier = getCritMultiplier(attackingGotchi, rng)
|
|
395
|
+
const isCrit = critMultiplier > 1
|
|
396
|
+
actionMultipler *= critMultiplier
|
|
397
|
+
|
|
398
|
+
const damage = getDamage(attackingGotchi, target, actionMultipler)
|
|
399
|
+
|
|
400
|
+
targetActionEffect = {
|
|
401
|
+
target: target.id,
|
|
402
|
+
statuses: [],
|
|
403
|
+
damage,
|
|
404
|
+
outcome: isCrit ? 'critical' : 'success'
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Handle damage
|
|
408
|
+
target.health -= damage
|
|
409
|
+
|
|
410
|
+
// Handle stats
|
|
411
|
+
if (isCrit) attackingGotchi.stats.crits++
|
|
412
|
+
attackingGotchi.stats.hits++
|
|
413
|
+
attackingGotchi.stats.dmgGiven += damage
|
|
414
|
+
target.stats.dmgReceived += damage
|
|
415
|
+
|
|
416
|
+
} else if (action === 'heal') {
|
|
417
|
+
const amountToHeal = getHealFromMultiplier(attackingGotchi, actionMultipler)
|
|
418
|
+
|
|
419
|
+
targetActionEffect = {
|
|
420
|
+
target: target.id,
|
|
421
|
+
statuses: [],
|
|
422
|
+
damage: -amountToHeal,
|
|
423
|
+
outcome: 'success'
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Handle healing
|
|
427
|
+
target.health += amountToHeal
|
|
428
|
+
|
|
429
|
+
// Handle stats
|
|
430
|
+
attackingGotchi.stats.healGiven += amountToHeal
|
|
431
|
+
target.stats.healReceived += amountToHeal
|
|
432
|
+
|
|
433
|
+
} else if (action === 'none') {
|
|
434
|
+
// Do nothing
|
|
435
|
+
} else {
|
|
436
|
+
// Check we actually have a valid action
|
|
437
|
+
throw new Error(`Invalid action: ${action}`)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// If it's a special attack then handle the special effects with target 'same_as_attack'
|
|
441
|
+
if (isSpecial) {
|
|
442
|
+
attackingGotchi.specialExpanded.effects.forEach((specialEffect) => {
|
|
443
|
+
// Only handle special effects here that have a target code of 'same_as_attack'
|
|
444
|
+
// Handle the rest after the action is done
|
|
445
|
+
// This is to ensure that these effects are not applied multiple times
|
|
446
|
+
// e.g. if the target is 'all_enemies' then we don't want to apply that here for every target
|
|
447
|
+
|
|
448
|
+
if (specialEffect.target === 'same_as_attack') {
|
|
449
|
+
// Handle the effect
|
|
450
|
+
const specialEffectResults = handleSpecialEffects(attackingTeam, attackingGotchi, target, specialEffect, rng)
|
|
451
|
+
|
|
452
|
+
if (specialEffectResults) {
|
|
453
|
+
if (specialEffectResults.actionEffect) {
|
|
454
|
+
if (targetActionEffect) {
|
|
455
|
+
// If target is same as the actionEffect then just add statuses to the actionEffect
|
|
456
|
+
if (targetActionEffect.target && targetActionEffect.target === specialEffectResults.actionEffect.target) {
|
|
457
|
+
targetActionEffect.statuses.push(...specialEffectResults.actionEffect.statuses)
|
|
458
|
+
} else {
|
|
459
|
+
// If not then add to additionalEffects
|
|
460
|
+
targetAdditionalEffects.push(specialEffectResults.actionEffect)
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
targetActionEffect = specialEffectResults.actionEffect
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (specialEffectResults.additionalEffects) {
|
|
468
|
+
targetAdditionalEffects.push(...specialEffectResults.additionalEffects)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (specialEffectResults.statusesExpired) {
|
|
472
|
+
statusesExpired.push(...specialEffectResults.statusesExpired)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (specialEffectResults.repeatAttack) {
|
|
476
|
+
repeatAttack = true
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
})
|
|
481
|
+
} else {
|
|
482
|
+
// If it's an auto attack then handle all the statuses that have attackEffects
|
|
483
|
+
const attackEffects = attackingGotchi.statuses.filter(status => {
|
|
484
|
+
const statusEffect = getStatusByCode(status)
|
|
485
|
+
return statusEffect.attackEffects
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
attackEffects.forEach((attackEffect) => {
|
|
489
|
+
if (attackEffect.effectChance && attackEffect.effectChance < 1 && rng() > attackEffect.effectChance) {
|
|
490
|
+
return
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// 'apply_status', 'gain_status', 'remove_buff', 'cleanse_target', 'cleanse_self'
|
|
494
|
+
switch (attackEffect.type) {
|
|
495
|
+
case 'apply_status': {
|
|
496
|
+
if (focusCheck(attackingTeam, attackingGotchi, target, rng)) {
|
|
497
|
+
addStatusToGotchi(target, attackEffect.status)
|
|
498
|
+
targetActionEffect.statuses.push(attackEffect.status)
|
|
499
|
+
}
|
|
500
|
+
break
|
|
501
|
+
}
|
|
502
|
+
case 'gain_status': {
|
|
503
|
+
addStatusToGotchi(attackingGotchi, attackEffect.status)
|
|
504
|
+
targetAdditionalEffects.push({
|
|
505
|
+
target: attackingGotchi.id,
|
|
506
|
+
status: attackEffect.status,
|
|
507
|
+
outcome: 'success'
|
|
508
|
+
})
|
|
509
|
+
break
|
|
510
|
+
}
|
|
511
|
+
case 'remove_buff': {
|
|
512
|
+
if (focusCheck(attackingTeam, attackingGotchi, target, rng)) {
|
|
513
|
+
const buffs = target.statuses.filter(status => STATUSES[status].isBuff)
|
|
514
|
+
|
|
515
|
+
if (buffs.length) {
|
|
516
|
+
const randomBuff = buffs[Math.floor(rng() * buffs.length)]
|
|
517
|
+
statusesExpired.push({
|
|
518
|
+
target: target.id,
|
|
519
|
+
status: randomBuff
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
// Remove first instance of randomBuff (there may be multiple)
|
|
523
|
+
const index = target.statuses.indexOf(randomBuff)
|
|
524
|
+
target.statuses.splice(index, 1)
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
break
|
|
528
|
+
}
|
|
529
|
+
case 'cleanse_target': {
|
|
530
|
+
const debuffs = target.statuses.filter(status => STATUSES[status].isDebuff)
|
|
531
|
+
|
|
532
|
+
if (debuffs.length) {
|
|
533
|
+
const randomDebuff = debuffs[Math.floor(rng() * debuffs.length)]
|
|
534
|
+
statusesExpired.push({
|
|
535
|
+
target: target.id,
|
|
536
|
+
status: randomDebuff
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
// Remove first instance of randomDebuff (there may be multiple)
|
|
540
|
+
const index = target.statuses.indexOf(randomDebuff)
|
|
541
|
+
target.statuses.splice(index, 1)
|
|
542
|
+
}
|
|
543
|
+
break
|
|
544
|
+
}
|
|
545
|
+
case 'cleanse_self': {
|
|
546
|
+
const debuffs = attackingGotchi.statuses.filter(status => STATUSES[status].isDebuff)
|
|
547
|
+
|
|
548
|
+
if (debuffs.length) {
|
|
549
|
+
const randomDebuff = debuffs[Math.floor(rng() * debuffs.length)]
|
|
550
|
+
statusesExpired.push({
|
|
551
|
+
target: attackingGotchi.id,
|
|
552
|
+
status: randomDebuff
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
// Remove first instance of randomDebuff (there may be multiple)
|
|
556
|
+
const index = attackingGotchi.statuses.indexOf(randomDebuff)
|
|
557
|
+
attackingGotchi.statuses.splice(index, 1)
|
|
558
|
+
}
|
|
559
|
+
break
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
// Check for counter attack
|
|
565
|
+
if (target.statuses.includes('taunt') && target.health > 0) {
|
|
566
|
+
const counterDamageMultiplier = 0.5
|
|
567
|
+
const counterDamage = getDamage(target, attackingGotchi, counterDamageMultiplier)
|
|
568
|
+
|
|
569
|
+
attackingGotchi.health -= counterDamage
|
|
570
|
+
|
|
571
|
+
targetAdditionalEffects.push({
|
|
572
|
+
target: attackingGotchi.id,
|
|
573
|
+
source: target.id,
|
|
574
|
+
damage: counterDamage,
|
|
575
|
+
outcome: 'counter'
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
// Add to stats
|
|
579
|
+
target.stats.counters++
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// If the actionType is 'none' then there may not be an actionEffect
|
|
584
|
+
if (targetActionEffect) {
|
|
585
|
+
actionEffects.push(targetActionEffect)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Add additional effects to the effects array
|
|
589
|
+
additionalEffects.push(...targetAdditionalEffects)
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
// Handle specialEffects that are not 'same_as_attack'
|
|
593
|
+
if (isSpecial) {
|
|
594
|
+
attackingGotchi.specialExpanded.effects.forEach((specialEffect) => {
|
|
595
|
+
if (specialEffect.target !== 'same_as_attack') {
|
|
596
|
+
const targets = getTargetsFromCode(specialEffect.target, attackingGotchi, attackingTeam, defendingTeam, rng)
|
|
597
|
+
|
|
598
|
+
targets.forEach((target) => {
|
|
599
|
+
const specialEffectResults = handleSpecialEffects(attackingTeam, attackingGotchi, target, specialEffect, rng)
|
|
600
|
+
|
|
601
|
+
if (specialEffectResults) {
|
|
602
|
+
if (specialEffectResults.actionEffect) {
|
|
603
|
+
additionalEffects.push(specialEffectResults.actionEffect)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (specialEffectResults.additionalEffects) {
|
|
607
|
+
additionalEffects.push(...specialEffectResults.additionalEffects)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (specialEffectResults.statusesExpired) {
|
|
611
|
+
statusesExpired.push(...specialEffectResults.statusesExpired)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (specialEffectResults.repeatAttack) {
|
|
615
|
+
repeatAttack = true
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
})
|
|
619
|
+
}
|
|
620
|
+
})
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return {
|
|
624
|
+
actionEffects,
|
|
625
|
+
additionalEffects,
|
|
626
|
+
statusesExpired,
|
|
627
|
+
repeatAttack
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const handleSpecialEffects = (attackingTeam, attackingGotchi, target, specialEffect, rng) => {
|
|
632
|
+
if (specialEffect.chance && specialEffect.chance < 1 && rng() > specialEffect.chance) {
|
|
633
|
+
return false
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const actionEffect = {
|
|
637
|
+
target: target.id,
|
|
638
|
+
statuses: [],
|
|
639
|
+
outcome: 'success'
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const additionalEffects = []
|
|
643
|
+
const statusesExpired = []
|
|
644
|
+
let repeatAttack = false
|
|
645
|
+
|
|
646
|
+
switch (specialEffect.effectType) {
|
|
647
|
+
case 'status': {
|
|
648
|
+
// Focus/resistance check if target is not on the same team as the attacking gotchi
|
|
649
|
+
if (focusCheck(attackingTeam, attackingGotchi, target, rng)) {
|
|
650
|
+
addStatusToGotchi(target, specialEffect.status)
|
|
651
|
+
actionEffect.statuses.push(specialEffect.status)
|
|
652
|
+
}
|
|
653
|
+
break
|
|
654
|
+
}
|
|
655
|
+
case 'heal': {
|
|
656
|
+
const amountToHeal = getHealFromMultiplier(target, specialEffect.actionMultiplier)
|
|
657
|
+
target.health += amountToHeal
|
|
658
|
+
|
|
659
|
+
// Add another effect for the healing
|
|
660
|
+
additionalEffects.push({
|
|
661
|
+
target: target.id,
|
|
662
|
+
source: attackingGotchi.id,
|
|
663
|
+
damage: -amountToHeal,
|
|
664
|
+
outcome: 'success'
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
// Handle stats
|
|
668
|
+
attackingGotchi.stats.healGiven += amountToHeal
|
|
669
|
+
target.stats.healReceived += amountToHeal
|
|
670
|
+
break
|
|
671
|
+
}
|
|
672
|
+
case 'remove_buff': {
|
|
673
|
+
// Focus/resistance check if target is not on the same team as the attacking gotchi
|
|
674
|
+
if (focusCheck(attackingTeam, attackingGotchi, target, rng)) {
|
|
675
|
+
const buffs = target.statuses.filter(statusCode => {
|
|
676
|
+
const status = getStatusByCode(statusCode)
|
|
677
|
+
return status.isBuff
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
if (buffs.length) {
|
|
681
|
+
const randomBuff = buffs[Math.floor(rng() * buffs.length)]
|
|
682
|
+
statusesExpired.push({
|
|
683
|
+
target: target.id,
|
|
684
|
+
status: randomBuff
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
// Remove first instance of randomBuff (there may be multiple)
|
|
688
|
+
const index = target.statuses.indexOf(randomBuff)
|
|
689
|
+
target.statuses.splice(index, 1)
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
break
|
|
694
|
+
}
|
|
695
|
+
case 'remove_debuff': {
|
|
696
|
+
const debuffs = target.statuses.filter(statusCode => {
|
|
697
|
+
const status = getStatusByCode(statusCode)
|
|
698
|
+
return !status.isBuff
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
if (debuffs.length) {
|
|
702
|
+
const randomDebuff = debuffs[Math.floor(rng() * debuffs.length)]
|
|
703
|
+
statusesExpired.push({
|
|
704
|
+
target: target.id,
|
|
705
|
+
status: randomDebuff
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
// Remove first instance of randomDebuff (there may be multiple)
|
|
709
|
+
const index = target.statuses.indexOf(randomDebuff)
|
|
710
|
+
target.statuses.splice(index, 1)
|
|
711
|
+
}
|
|
712
|
+
break
|
|
713
|
+
}
|
|
714
|
+
case 'remove_all_buffs': {
|
|
715
|
+
// Focus/resistance check if target is not on the same team as the attacking gotchi
|
|
716
|
+
if (focusCheck(attackingTeam, attackingGotchi, target, rng)) {
|
|
717
|
+
const buffsToRemove = target.statuses.filter(statusCode => {
|
|
718
|
+
const status = getStatusByCode(statusCode)
|
|
719
|
+
return status.isBuff
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
buffsToRemove.forEach((buff) => {
|
|
723
|
+
statusesExpired.push({
|
|
724
|
+
target: target.id,
|
|
725
|
+
status: buff
|
|
726
|
+
})
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
if (buffsToRemove.length) {
|
|
730
|
+
// Filter statuses so only debuffs remain
|
|
731
|
+
target.statuses = target.statuses.filter(statusCode => {
|
|
732
|
+
const status = getStatusByCode(statusCode)
|
|
733
|
+
return !status.isBuff
|
|
734
|
+
})
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
break
|
|
739
|
+
}
|
|
740
|
+
case 'remove_all_debuffs': {
|
|
741
|
+
const debuffsToRemove = target.statuses.filter(statusCode => {
|
|
742
|
+
const status = getStatusByCode(statusCode)
|
|
743
|
+
return !status.isBuff
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
debuffsToRemove.forEach((debuff) => {
|
|
747
|
+
statusesExpired.push({
|
|
748
|
+
target: target.id,
|
|
749
|
+
status: debuff
|
|
750
|
+
})
|
|
751
|
+
})
|
|
752
|
+
|
|
753
|
+
if (debuffsToRemove.length) {
|
|
754
|
+
// Filter statuses so only buffs remain
|
|
755
|
+
target.statuses = target.statuses.filter(statusCode => {
|
|
756
|
+
const status = getStatusByCode(statusCode)
|
|
757
|
+
return status.isBuff
|
|
758
|
+
})
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
break
|
|
762
|
+
}
|
|
763
|
+
case 'repeat_attack': {
|
|
764
|
+
repeatAttack = true
|
|
765
|
+
break
|
|
766
|
+
}
|
|
767
|
+
default:
|
|
768
|
+
throw new Error(`Invalid special effect type: ${specialEffect.effectType}`)
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return {
|
|
772
|
+
actionEffect,
|
|
773
|
+
additionalEffects,
|
|
774
|
+
statusesExpired,
|
|
775
|
+
repeatAttack
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
module.exports = {
|
|
780
|
+
gameLoop,
|
|
781
|
+
attack
|
|
782
|
+
}
|