-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
/
Copy pathgame-mode.ts
379 lines (349 loc) · 12.3 KB
/
game-mode.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
import i18next from "i18next";
import type { FixedBattleConfigs } from "./battle";
import { classicFixedBattles, FixedBattleConfig } from "./battle";
import type { Challenge } from "./data/challenge";
import { allChallenges, applyChallenges, ChallengeType, copyChallenge } from "./data/challenge";
import type PokemonSpecies from "./data/pokemon-species";
import { allSpecies } from "./data/pokemon-species";
import type { Arena } from "./field/arena";
import Overrides from "#app/overrides";
import * as Utils from "./utils";
import { Biome } from "#enums/biome";
import { Species } from "#enums/species";
import { Challenges } from "./enums/challenges";
import { globalScene } from "#app/global-scene";
import { getDailyStartingBiome } from "./data/daily-run";
export enum GameModes {
CLASSIC,
ENDLESS,
SPLICED_ENDLESS,
DAILY,
CHALLENGE
}
interface GameModeConfig {
isClassic?: boolean;
isEndless?: boolean;
isDaily?: boolean;
hasTrainers?: boolean;
hasNoShop?: boolean;
hasShortBiomes?: boolean;
hasRandomBiomes?: boolean;
hasRandomBosses?: boolean;
isSplicedOnly?: boolean;
isChallenge?: boolean;
hasMysteryEncounters?: boolean;
}
// Describes min and max waves for MEs in specific game modes
export const CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES: [number, number] = [ 10, 180 ];
export const CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES: [number, number] = [ 10, 180 ];
export class GameMode implements GameModeConfig {
public modeId: GameModes;
public isClassic: boolean;
public isEndless: boolean;
public isDaily: boolean;
public hasTrainers: boolean;
public hasNoShop: boolean;
public hasShortBiomes: boolean;
public hasRandomBiomes: boolean;
public hasRandomBosses: boolean;
public isSplicedOnly: boolean;
public isChallenge: boolean;
public challenges: Challenge[];
public battleConfig: FixedBattleConfigs;
public hasMysteryEncounters: boolean;
public minMysteryEncounterWave: number;
public maxMysteryEncounterWave: number;
constructor(modeId: GameModes, config: GameModeConfig, battleConfig?: FixedBattleConfigs) {
this.modeId = modeId;
this.challenges = [];
Object.assign(this, config);
if (this.isChallenge) {
this.challenges = allChallenges.map(c => copyChallenge(c));
}
this.battleConfig = battleConfig || {};
}
/**
* Helper function to see if a GameMode has a specific challenge type
* @param challenge the Challenges it looks for
* @returns true if the game mode has that challenge
*/
hasChallenge(challenge: Challenges): boolean {
return this.challenges.some(c => c.id === challenge && c.value !== 0);
}
/**
* Helper function to see if the game mode is using fresh start
* @returns true if a fresh start challenge is being applied
*/
isFreshStartChallenge(): boolean {
return this.hasChallenge(Challenges.FRESH_START);
}
/**
* @returns either:
* - override from overrides.ts
* - 20 for Daily Runs
* - 5 for all other modes
*/
getStartingLevel(): number {
if (Overrides.STARTING_LEVEL_OVERRIDE) {
return Overrides.STARTING_LEVEL_OVERRIDE;
}
switch (this.modeId) {
case GameModes.DAILY:
return 20;
default:
return 5;
}
}
/**
* @returns either:
* - override from overrides.ts
* - 1000
*/
getStartingMoney(): number {
return Overrides.STARTING_MONEY_OVERRIDE || 1000;
}
/**
* @returns either:
* - random biome for Daily mode
* - override from overrides.ts
* - Town
*/
getStartingBiome(): Biome {
switch (this.modeId) {
case GameModes.DAILY:
return getDailyStartingBiome();
default:
return Overrides.STARTING_BIOME_OVERRIDE || Biome.TOWN;
}
}
getWaveForDifficulty(waveIndex: number, ignoreCurveChanges: boolean = false): number {
switch (this.modeId) {
case GameModes.DAILY:
return waveIndex + 30 + (!ignoreCurveChanges ? Math.floor(waveIndex / 5) : 0);
default:
return waveIndex;
}
}
/**
* Determines whether or not to generate a trainer
* @param waveIndex the current floor the player is on (trainer sprites fail to generate on X1 floors)
* @param arena the current {@linkcode Arena}
* @returns `true` if a trainer should be generated, `false` otherwise
*/
isWaveTrainer(waveIndex: number, arena: Arena): boolean {
/**
* Daily spawns trainers on floors 5, 15, 20, 25, 30, 35, 40, and 45
*/
if (this.isDaily) {
return waveIndex % 10 === 5 || (!(waveIndex % 10) && waveIndex > 10 && !this.isWaveFinal(waveIndex));
}
if ((waveIndex % 30) === (globalScene.offsetGym ? 0 : 20) && !this.isWaveFinal(waveIndex)) {
return true;
} else if (waveIndex % 10 !== 1 && waveIndex % 10) {
/**
* Do not check X1 floors since there's a bug that stops trainer sprites from appearing
* after a X0 full party heal
*/
const trainerChance = arena.getTrainerChance();
let allowTrainerBattle = true;
if (trainerChance) {
const waveBase = Math.floor(waveIndex / 10) * 10;
// Stop generic trainers from spawning in within 3 waves of a trainer battle
for (let w = Math.max(waveIndex - 3, waveBase + 2); w <= Math.min(waveIndex + 3, waveBase + 9); w++) {
if (w === waveIndex) {
continue;
}
if ((w % 30) === (globalScene.offsetGym ? 0 : 20) || this.isFixedBattle(w)) {
allowTrainerBattle = false;
break;
} else if (w < waveIndex) {
globalScene.executeWithSeedOffset(() => {
const waveTrainerChance = arena.getTrainerChance();
if (!Utils.randSeedInt(waveTrainerChance)) {
allowTrainerBattle = false;
}
}, w);
if (!allowTrainerBattle) {
break;
}
}
}
}
return Boolean(allowTrainerBattle && trainerChance && !Utils.randSeedInt(trainerChance));
}
return false;
}
isTrainerBoss(waveIndex: number, biomeType: Biome, offsetGym: boolean): boolean {
switch (this.modeId) {
case GameModes.DAILY:
return waveIndex > 10 && waveIndex < 50 && !(waveIndex % 10);
default:
return (waveIndex % 30) === (offsetGym ? 0 : 20) && (biomeType !== Biome.END || this.isClassic || this.isWaveFinal(waveIndex));
}
}
getOverrideSpecies(waveIndex: number): PokemonSpecies | null {
if (this.isDaily && this.isWaveFinal(waveIndex)) {
const allFinalBossSpecies = allSpecies.filter(s => (s.subLegendary || s.legendary || s.mythical)
&& s.baseTotal >= 600 && s.speciesId !== Species.ETERNATUS && s.speciesId !== Species.ARCEUS);
return Utils.randSeedItem(allFinalBossSpecies);
}
return null;
}
/**
* Checks if wave provided is the final for current or specified game mode
* @param waveIndex
* @param modeId game mode
* @returns if the current wave is final for classic or daily OR a minor boss in endless
*/
isWaveFinal(waveIndex: number, modeId: GameModes = this.modeId): boolean {
switch (modeId) {
case GameModes.CLASSIC:
case GameModes.CHALLENGE:
return waveIndex === 200;
case GameModes.ENDLESS:
case GameModes.SPLICED_ENDLESS:
return !(waveIndex % 250);
case GameModes.DAILY:
return waveIndex === 50;
}
}
/**
* Every 10 waves is a boss battle
* @returns true if waveIndex is a multiple of 10
*/
isBoss(waveIndex: number): boolean {
return waveIndex % 10 === 0;
}
/**
* @returns `true` if the current battle is against classic mode's final boss
*/
isBattleClassicFinalBoss(waveIndex: number): boolean {
return (this.modeId === GameModes.CLASSIC || this.modeId === GameModes.CHALLENGE) && this.isWaveFinal(waveIndex);
}
/**
* Every 50 waves of an Endless mode is a boss
* At this time it is paradox pokemon
* @returns true if waveIndex is a multiple of 50 in Endless
*/
isEndlessBoss(waveIndex: number): boolean {
return waveIndex % 50 === 0 &&
(this.modeId === GameModes.ENDLESS || this.modeId === GameModes.SPLICED_ENDLESS);
}
/**
* Every 250 waves of an Endless mode is a minor boss
* At this time it is Eternatus
* @returns true if waveIndex is a multiple of 250 in Endless
*/
isEndlessMinorBoss(waveIndex: number): boolean {
return waveIndex % 250 === 0 &&
(this.modeId === GameModes.ENDLESS || this.modeId === GameModes.SPLICED_ENDLESS);
}
/**
* Every 1000 waves of an Endless mode is a major boss
* At this time it is Eternamax Eternatus
* @returns true if waveIndex is a multiple of 1000 in Endless
*/
isEndlessMajorBoss(waveIndex: number): boolean {
return waveIndex % 1000 === 0 &&
(this.modeId === GameModes.ENDLESS || this.modeId === GameModes.SPLICED_ENDLESS);
}
/**
* Checks whether there is a fixed battle on this gamemode on a given wave.
* @param {number} waveIndex The wave to check.
* @returns {boolean} If this game mode has a fixed battle on this wave
*/
isFixedBattle(waveIndex: number): boolean {
const dummyConfig = new FixedBattleConfig();
return this.battleConfig.hasOwnProperty(waveIndex) || applyChallenges(this, ChallengeType.FIXED_BATTLES, waveIndex, dummyConfig);
}
/**
* Returns the config for the fixed battle for a particular wave.
* @param {number} waveIndex The wave to check.
* @returns {boolean} The fixed battle for this wave.
*/
getFixedBattle(waveIndex: number): FixedBattleConfig {
const challengeConfig = new FixedBattleConfig();
if (applyChallenges(this, ChallengeType.FIXED_BATTLES, waveIndex, challengeConfig)) {
return challengeConfig;
} else {
return this.battleConfig[waveIndex];
}
}
getClearScoreBonus(): number {
switch (this.modeId) {
case GameModes.CLASSIC:
case GameModes.CHALLENGE:
return 5000;
case GameModes.DAILY:
return 2500;
default:
return 0;
}
}
getEnemyModifierChance(isBoss: boolean): number {
switch (this.modeId) {
case GameModes.CLASSIC:
case GameModes.CHALLENGE:
case GameModes.DAILY:
return !isBoss ? 18 : 6;
case GameModes.ENDLESS:
case GameModes.SPLICED_ENDLESS:
return !isBoss ? 12 : 4;
}
}
getName(): string {
switch (this.modeId) {
case GameModes.CLASSIC:
return i18next.t("gameMode:classic");
case GameModes.ENDLESS:
return i18next.t("gameMode:endless");
case GameModes.SPLICED_ENDLESS:
return i18next.t("gameMode:endlessSpliced");
case GameModes.DAILY:
return i18next.t("gameMode:dailyRun");
case GameModes.CHALLENGE:
return i18next.t("gameMode:challenge");
}
}
/**
* Returns the wave range where MEs can spawn for the game mode [min, max]
*/
getMysteryEncounterLegalWaves(): [number, number] {
switch (this.modeId) {
default:
return [ 0, 0 ];
case GameModes.CLASSIC:
return CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES;
case GameModes.CHALLENGE:
return CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES;
}
}
static getModeName(modeId: GameModes): string {
switch (modeId) {
case GameModes.CLASSIC:
return i18next.t("gameMode:classic");
case GameModes.ENDLESS:
return i18next.t("gameMode:endless");
case GameModes.SPLICED_ENDLESS:
return i18next.t("gameMode:endlessSpliced");
case GameModes.DAILY:
return i18next.t("gameMode:dailyRun");
case GameModes.CHALLENGE:
return i18next.t("gameMode:challenge");
}
}
}
export function getGameMode(gameMode: GameModes): GameMode {
switch (gameMode) {
case GameModes.CLASSIC:
return new GameMode(GameModes.CLASSIC, { isClassic: true, hasTrainers: true, hasMysteryEncounters: true }, classicFixedBattles);
case GameModes.ENDLESS:
return new GameMode(GameModes.ENDLESS, { isEndless: true, hasShortBiomes: true, hasRandomBosses: true });
case GameModes.SPLICED_ENDLESS:
return new GameMode(GameModes.SPLICED_ENDLESS, { isEndless: true, hasShortBiomes: true, hasRandomBosses: true, isSplicedOnly: true });
case GameModes.DAILY:
return new GameMode(GameModes.DAILY, { isDaily: true, hasTrainers: true, hasNoShop: true });
case GameModes.CHALLENGE:
return new GameMode(GameModes.CHALLENGE, { isClassic: true, hasTrainers: true, isChallenge: true, hasMysteryEncounters: true }, classicFixedBattles);
}
}