diff --git a/public/images/ui/icon_spliced.png b/public/images/ui/icon_spliced.png new file mode 100644 index 000000000..612da16db Binary files /dev/null and b/public/images/ui/icon_spliced.png differ diff --git a/src/arena.ts b/src/arena.ts index 79f7ca85e..792f2569b 100644 --- a/src/arena.ts +++ b/src/arena.ts @@ -33,7 +33,7 @@ export class Arena { randomSpecies(waveIndex: integer, level: integer, attempt?: integer): PokemonSpecies { const isBoss = waveIndex % 10 === 0 && !!this.pokemonPool[BiomePoolTier.BOSS].length - && (this.biomeType !== Biome.END || this.scene.gameMode !== GameMode.ENDLESS || waveIndex % 250 === 0); + && (this.biomeType !== Biome.END || this.scene.gameMode === GameMode.CLASSIC || waveIndex % 250 === 0); const tierValue = Utils.randSeedInt(!isBoss ? 512 : 64); let tier = !isBoss ? tierValue >= 156 ? BiomePoolTier.COMMON : tierValue >= 32 ? BiomePoolTier.UNCOMMON : tierValue >= 6 ? BiomePoolTier.RARE : tierValue >= 1 ? BiomePoolTier.SUPER_RARE : BiomePoolTier.ULTRA_RARE @@ -103,7 +103,7 @@ export class Arena { randomTrainerType(waveIndex: integer): TrainerType { const isBoss = waveIndex > 20 && !(waveIndex % 30) && !!this.trainerPool[BiomePoolTier.BOSS].length - && (this.biomeType !== Biome.END || this.scene.gameMode !== GameMode.ENDLESS || waveIndex % 250 === 0); + && (this.biomeType !== Biome.END || this.scene.gameMode === GameMode.CLASSIC || waveIndex % 250 === 0); const tierValue = Utils.randSeedInt(!isBoss ? 512 : 64); let tier = !isBoss ? tierValue >= 156 ? BiomePoolTier.COMMON : tierValue >= 32 ? BiomePoolTier.UNCOMMON : tierValue >= 6 ? BiomePoolTier.RARE : tierValue >= 1 ? BiomePoolTier.SUPER_RARE : BiomePoolTier.ULTRA_RARE diff --git a/src/battle-phases.ts b/src/battle-phases.ts index 362bdc81e..68ea9cc8a 100644 --- a/src/battle-phases.ts +++ b/src/battle-phases.ts @@ -472,7 +472,7 @@ export class SelectBiomePhase extends BattlePhase { if (this.scene.gameMode === GameMode.CLASSIC && this.scene.currentBattle.waveIndex === this.scene.finalWave - 9) setNextBiome(Biome.END); - else if (this.scene.gameMode === GameMode.ENDLESS) { + else if (this.scene.gameMode !== GameMode.CLASSIC) { if (this.scene.currentBattle.waveIndex % 50 === 0) setNextBiome(Biome.END); else { @@ -2152,10 +2152,10 @@ export class VictoryPhase extends PokemonPhase { this.scene.pushPhase(new BattleEndPhase(this.scene)); if (this.scene.currentBattle.battleType === BattleType.TRAINER) this.scene.pushPhase(new TrainerVictoryPhase(this.scene)); - if (this.scene.gameMode === GameMode.ENDLESS || this.scene.currentBattle.waveIndex < this.scene.finalWave) { + if (this.scene.gameMode !== GameMode.CLASSIC || this.scene.currentBattle.waveIndex < this.scene.finalWave) { if (this.scene.currentBattle.waveIndex > 30 || this.scene.currentBattle.waveIndex % 10) { this.scene.pushPhase(new SelectModifierPhase(this.scene)); - if (this.scene.gameMode === GameMode.ENDLESS && !(this.scene.currentBattle.waveIndex % 50)) + if (this.scene.gameMode !== GameMode.CLASSIC && !(this.scene.currentBattle.waveIndex % 50)) this.scene.pushPhase(new SelectEnemyBuffModifierPhase(this.scene)); } else this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.GOLDEN_EXP_CHARM)) @@ -2259,6 +2259,8 @@ export class GameOverPhase extends BattlePhase { if (this.victory) { if (!this.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]) this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.ENDLESS_MODE)); + if (this.scene.getParty().filter(p => p.fusionSpecies).length && !this.scene.gameData.unlocks[Unlockables.SPLICED_ENDLESS_MODE]) + this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.SPLICED_ENDLESS_MODE)); if (!this.scene.gameData.unlocks[Unlockables.MINI_BLACK_HOLE]) this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.MINI_BLACK_HOLE)); } diff --git a/src/battle-scene.ts b/src/battle-scene.ts index fc217cd1e..3e002127d 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -197,6 +197,7 @@ export default class BattleScene extends Phaser.Scene { this.loadImage('ability_bar', 'ui'); this.loadImage('party_exp_bar', 'ui'); this.loadImage('shiny_star', 'ui', 'shiny.png'); + this.loadImage('icon_spliced', 'ui'); this.loadImage('pb_tray_overlay_player', 'ui'); this.loadImage('pb_tray_overlay_enemy', 'ui'); @@ -617,7 +618,7 @@ export default class BattleScene extends Phaser.Scene { if (newTrainer) this.field.add(newTrainer); } else { - if (this.gameMode === GameMode.ENDLESS) + if (this.gameMode !== GameMode.CLASSIC) newBattleType = BattleType.WILD; else if (battleType === undefined) { if (newWaveIndex > 20 && !(newWaveIndex % 30)) @@ -918,7 +919,7 @@ export default class BattleScene extends Phaser.Scene { playBgm(bgmName?: string, fadeOut?: boolean): void { if (bgmName === undefined) - bgmName = this.currentBattle.getBgmOverride() || this.arena.bgm; + bgmName = this.currentBattle.getBgmOverride(this) || this.arena.bgm; if (this.bgm && bgmName === this.bgm.key) { if (!this.bgm.isPlaying) { this.bgm.play({ diff --git a/src/battle.ts b/src/battle.ts index be511ec1a..70c5c956f 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -1,11 +1,12 @@ -import BattleScene, { PokeballCounts } from "./battle-scene"; +import BattleScene from "./battle-scene"; import { EnemyPokemon, PlayerPokemon, QueuedMove } from "./pokemon"; import { Command } from "./ui/command-ui-handler"; import * as Utils from "./utils"; import Trainer from "./trainer"; import { Species } from "./data/species"; import { Moves } from "./data/move"; -import { TrainerConfig, TrainerType } from "./data/trainer-type"; +import { TrainerType } from "./data/trainer-type"; +import { GameMode } from "./game-mode"; export enum BattleType { WILD, @@ -94,7 +95,7 @@ export default class Battle { this.playerParticipantIds.delete(playerPokemon.id); } - getBgmOverride(): string { + getBgmOverride(scene: BattleScene): string { const battlers = this.enemyParty.slice(0, this.getBattlerCount()); if (this.battleType === BattleType.TRAINER) { if (!this.started && this.trainer.config.encounterBgm && this.trainer.config.encounterMessages.length) @@ -113,7 +114,7 @@ export default class Battle { } } - if (this.waveIndex <= 4) + if (scene.gameMode === GameMode.CLASSIC && this.waveIndex <= 4) return 'battle_wild'; return null; diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index d24156f83..747df0ab6 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -356,6 +356,21 @@ export default class PokemonSpecies extends PokemonSpeciesForm { return prevolutionLevels; } + getCompatibleFusionSpeciesFilter(): PokemonSpeciesFilter { + const hasEvolution = pokemonEvolutions.hasOwnProperty(this.speciesId); + const hasPrevolution = pokemonPrevolutions.hasOwnProperty(this.speciesId); + const pseudoLegendary = this.pseudoLegendary; + const legendary = this.legendary; + const mythical = this.mythical; + return species => { + return pokemonEvolutions.hasOwnProperty(species.speciesId) === hasEvolution + && pokemonPrevolutions.hasOwnProperty(species.speciesId) === hasPrevolution + && species.pseudoLegendary === pseudoLegendary + && species.legendary === legendary + && species.mythical === mythical; + }; + } + getFormSpriteKey(formIndex?: integer) { return this.forms?.length ? this.forms[formIndex || 0].formKey diff --git a/src/game-mode.ts b/src/game-mode.ts index 0247e024e..430a6d1b3 100644 --- a/src/game-mode.ts +++ b/src/game-mode.ts @@ -1,4 +1,11 @@ export enum GameMode { CLASSIC, - ENDLESS -} \ No newline at end of file + ENDLESS, + SPLICED_ENDLESS +} + +export const gameModeNames = { + [GameMode.CLASSIC]: 'Classic', + [GameMode.ENDLESS]: 'Endless', + [GameMode.SPLICED_ENDLESS]: 'Endless (Spliced)' +}; \ No newline at end of file diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 47ba5ea7c..79e4ba4f1 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -779,6 +779,7 @@ const modifierPool = { new WeightedModifierType(modifierTypes.TM_GREAT, 2), new WeightedModifierType(modifierTypes.EXP_SHARE, 1), new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3), + new WeightedModifierType(modifierTypes.REVERSE_DNA_SPLICERS, (party: Pokemon[]) => party[0].scene.gameMode === GameMode.SPLICED_ENDLESS && party.filter(p => p.fusionSpecies).length ? 4 : 0), ].map(m => { m.setTier(ModifierTier.GREAT); return m; }), [ModifierTier.ULTRA]: [ new WeightedModifierType(modifierTypes.ULTRA_BALL, 8), @@ -799,12 +800,13 @@ const modifierPool = { new WeightedModifierType(modifierTypes.OVAL_CHARM, 2), new WeightedModifierType(modifierTypes.ABILITY_CHARM, 2), new WeightedModifierType(modifierTypes.EXP_BALANCE, 1), - new WeightedModifierType(modifierTypes.REVERSE_DNA_SPLICERS, (party: Pokemon[]) => party.filter(p => p.fusionSpecies).length > 1 ? 3 : 0), + new WeightedModifierType(modifierTypes.REVERSE_DNA_SPLICERS, (party: Pokemon[]) => party[0].scene.gameMode !== GameMode.SPLICED_ENDLESS && party.filter(p => p.fusionSpecies).length ? 3 : 0), + new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => party[0].scene.gameMode === GameMode.SPLICED_ENDLESS && party.filter(p => !p.fusionSpecies).length > 1 ? 6 : 0), ].map(m => { m.setTier(ModifierTier.ULTRA); return m; }), [ModifierTier.MASTER]: [ new WeightedModifierType(modifierTypes.MASTER_BALL, 3), new WeightedModifierType(modifierTypes.SHINY_CHARM, 2), - new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => party.filter(p => !p.fusionSpecies).length > 1 ? 1 : 0), + new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => party[0].scene.gameMode !== GameMode.SPLICED_ENDLESS && party.filter(p => !p.fusionSpecies).length > 1 ? 1 : 0), new WeightedModifierType(modifierTypes.MINI_BLACK_HOLE, (party: Pokemon[]) => party[0].scene.gameData.unlocks[Unlockables.MINI_BLACK_HOLE] ? 1 : 0), ].map(m => { m.setTier(ModifierTier.MASTER); return m; }), [ModifierTier.LUXURY]: [ diff --git a/src/pokemon.ts b/src/pokemon.ts index f30db3a3c..9df362391 100644 --- a/src/pokemon.ts +++ b/src/pokemon.ts @@ -28,6 +28,7 @@ import { BattlerIndex } from './battle'; import { Mode } from './ui/ui'; import PartyUiHandler, { PartyOption, PartyUiMode } from './ui/party-ui-handler'; import SoundFade from 'phaser3-rex-plugins/plugins/soundfade'; +import { GameMode } from './game-mode'; export enum FieldPosition { CENTER, @@ -148,6 +149,23 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.winCount = 0; this.pokerus = false; + + if (scene.gameMode === GameMode.SPLICED_ENDLESS) { + this.fusionSpecies = scene.randomSpecies(scene.currentBattle?.waveIndex || 0, level, this.species.getCompatibleFusionSpeciesFilter(), false); + this.fusionAbilityIndex = (this.fusionSpecies.abilityHidden && hasHiddenAbility ? this.fusionSpecies.ability2 ? 2 : 1 : this.fusionSpecies.ability2 ? randAbilityIndex : 0); + this.fusionFormIndex = scene.getSpeciesFormIndex(this.fusionSpecies); + this.fusionShiny = this.shiny; + + if (this.getFusionSpeciesForm().malePercent === null) + this.fusionGender = Gender.GENDERLESS; + else { + const genderChance = (this.id % 256) * 0.390625; + if (genderChance < this.getFusionSpeciesForm().malePercent) + this.fusionGender = Gender.MALE; + else + this.fusionGender = Gender.FEMALE; + } + } } if (!species.isObtainable()) diff --git a/src/system/game-data.ts b/src/system/game-data.ts index fb6e2a57d..28807bf6d 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -87,7 +87,8 @@ export class GameData { this.secretId = Utils.randSeedInt(65536); this.unlocks = { [Unlockables.ENDLESS_MODE]: false, - [Unlockables.MINI_BLACK_HOLE]: false + [Unlockables.MINI_BLACK_HOLE]: false, + [Unlockables.SPLICED_ENDLESS_MODE]: false }; this.initDexData(); this.loadSystem(); diff --git a/src/system/unlockables.ts b/src/system/unlockables.ts index c5e254572..23c4b1b2b 100644 --- a/src/system/unlockables.ts +++ b/src/system/unlockables.ts @@ -1,13 +1,18 @@ +import { GameMode, gameModeNames } from "../game-mode"; + export enum Unlockables { ENDLESS_MODE, - MINI_BLACK_HOLE + MINI_BLACK_HOLE, + SPLICED_ENDLESS_MODE } export function getUnlockableName(unlockable: Unlockables) { switch (unlockable) { case Unlockables.ENDLESS_MODE: - return 'Endless Mode'; + return gameModeNames[GameMode.ENDLESS]; case Unlockables.MINI_BLACK_HOLE: return 'Mini Black Hole'; + case Unlockables.SPLICED_ENDLESS_MODE: + return gameModeNames[GameMode.SPLICED_ENDLESS]; } } \ No newline at end of file diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts index 8553a33ad..ce52dde50 100644 --- a/src/ui/battle-info.ts +++ b/src/ui/battle-info.ts @@ -24,6 +24,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { private nameText: Phaser.GameObjects.Text; private genderText: Phaser.GameObjects.Text; private ownedIcon: Phaser.GameObjects.Sprite; + private splicedIcon: Phaser.GameObjects.Sprite; private statusIndicator: Phaser.GameObjects.Sprite; private levelContainer: Phaser.GameObjects.Container; private hpBar: Phaser.GameObjects.Image; @@ -69,6 +70,13 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.add(this.ownedIcon); } + this.splicedIcon = this.scene.add.sprite(0, 0, 'icon_spliced'); + this.splicedIcon.setVisible(false); + this.splicedIcon.setOrigin(0, 0); + this.splicedIcon.setPositionRelative(this.nameText, 0, 2); + this.splicedIcon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 5, 7), Phaser.Geom.Rectangle.Contains); + this.add(this.splicedIcon); + this.statusIndicator = this.scene.add.sprite(0, 0, 'statuses'); this.statusIndicator.setVisible(false); this.statusIndicator.setOrigin(0, 0); @@ -113,6 +121,13 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.genderText.setColor(getGenderColor(pokemon.gender)); this.genderText.setPositionRelative(this.nameText, nameTextWidth, 0); + this.splicedIcon.setPositionRelative(this.nameText, nameTextWidth + this.genderText.displayWidth + 1, 1); + this.splicedIcon.setVisible(!!pokemon.fusionSpecies); + if (this.splicedIcon.visible) { + this.splicedIcon.on('pointerover', () => (this.scene as BattleScene).ui.showTooltip(null, `Spliced with ${pokemon.fusionSpecies.name}`)); + this.splicedIcon.on('pointerout', () => (this.scene as BattleScene).ui.hideTooltip()); + } + if (!this.player) { const speciesOwned = !!pokemon.scene.gameData.getDefaultDexEntry(pokemon.species)?.entry?.caught; this.ownedIcon.setVisible(speciesOwned); diff --git a/src/ui/game-mode-select-ui-handler.ts b/src/ui/game-mode-select-ui-handler.ts index d76c9f7b8..3efd0cb51 100644 --- a/src/ui/game-mode-select-ui-handler.ts +++ b/src/ui/game-mode-select-ui-handler.ts @@ -1,4 +1,6 @@ -import BattleScene from "../battle-scene"; +import BattleScene, { Button } from "../battle-scene"; +import { GameMode, gameModeNames } from "../game-mode"; +import { Unlockables } from "../system/unlockables"; import OptionSelectUiHandler from "./option-select-ui-handler"; import { Mode } from "./ui"; @@ -9,14 +11,53 @@ export default class GameModeSelectUiHandler extends OptionSelectUiHandler { } getWindowWidth(): integer { - return 64; + return 104; } getWindowHeight(): number { - return 64; + return (this.getOptions().length + 1) * 16; } getOptions(): string[] { - return [ 'Classic', 'Endless', 'Cancel' ]; + const ret = [ gameModeNames[GameMode.CLASSIC] ]; + if (this.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]) { + ret.push(gameModeNames[GameMode.ENDLESS]); + if (this.scene.gameData.unlocks[Unlockables.SPLICED_ENDLESS_MODE]) + ret.push(gameModeNames[GameMode.SPLICED_ENDLESS]); + } + ret.push('Cancel'); + return ret; + } + + show(args: any[]) { + if (args.length === 2 && args[0] instanceof Function && args[1] instanceof Function) { + this.setupOptions(); + + super.show(args); + + this.handlers = args as Function[]; + + this.optionSelectContainer.setVisible(true); + this.setCursor(0); + } + } + + processInput(button: Button) { + const ui = this.getUi(); + + const options = this.getOptions(); + + if (button === Button.ACTION || button === Button.CANCEL) { + if (button === Button.CANCEL) + this.setCursor(options.length - 1); + if (this.cursor < options.length - 1) { + const gameMode = Object.values(gameModeNames).indexOf(options[this.cursor]) as GameMode; + this.handlers[0](gameMode); + } else + this.handlers[1](); + this.clear(); + ui.playSelect(); + } else + return super.processInput(button); } } \ No newline at end of file diff --git a/src/ui/option-select-ui-handler.ts b/src/ui/option-select-ui-handler.ts index 06ae9a25c..86ba48505 100644 --- a/src/ui/option-select-ui-handler.ts +++ b/src/ui/option-select-ui-handler.ts @@ -8,6 +8,7 @@ export default abstract class OptionSelectUiHandler extends UiHandler { protected optionSelectContainer: Phaser.GameObjects.Container; protected optionSelectBg: Phaser.GameObjects.NineSlice; + protected optionSelectText: Phaser.GameObjects.Text; private cursorObj: Phaser.GameObjects.Image; @@ -24,22 +25,31 @@ export default abstract class OptionSelectUiHandler extends UiHandler { setup() { const ui = this.getUi(); - this.optionSelectContainer = this.scene.add.container((this.scene.game.canvas.width / 6) - (this.getWindowWidth() + 1), -(this.getWindowWidth() + 1)); + this.optionSelectContainer = this.scene.add.container((this.scene.game.canvas.width / 6) - 1, -48); this.optionSelectContainer.setVisible(false); ui.add(this.optionSelectContainer); this.optionSelectBg = this.scene.add.nineslice(0, 0, 'window', null, this.getWindowWidth(), this.getWindowHeight(), 6, 6, 6, 6); - this.optionSelectBg.setOrigin(0, 1); + this.optionSelectBg.setOrigin(1, 1); this.optionSelectContainer.add(this.optionSelectBg); + this.setupOptions(); + this.setCursor(0); + } + + protected setupOptions() { const options = this.getOptions(); - const optionSelectText = addTextObject(this.scene, 0, 0, options.join('\n'), TextStyle.WINDOW, { maxLines: options.length }); - optionSelectText.setPositionRelative(this.optionSelectBg, 16, 9); - optionSelectText.setLineSpacing(12); - this.optionSelectContainer.add(optionSelectText); + if (this.optionSelectText) + this.optionSelectText.destroy(); - this.setCursor(0); + this.optionSelectText = addTextObject(this.scene, 0, 0, options.join('\n'), TextStyle.WINDOW, { maxLines: options.length }); + this.optionSelectText.setPositionRelative(this.optionSelectBg, 16, 9); + this.optionSelectText.setLineSpacing(12); + this.optionSelectContainer.add(this.optionSelectText); + + this.optionSelectBg.width = Math.max(this.optionSelectText.displayWidth + 24, this.getWindowWidth()); + this.optionSelectBg.height = this.getWindowHeight(); } show(args: any[]) { diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index a5df541e5..9a0a3a734 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -371,9 +371,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { }; if (this.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]) { ui.setMode(Mode.STARTER_SELECT); - ui.showText('Select a game mode.', null, () => { - ui.setModeWithoutClear(Mode.GAME_MODE_SELECT, () => startRun(GameMode.CLASSIC), () => startRun(GameMode.ENDLESS), cancel); - }); + ui.showText('Select a game mode.', null, () => ui.setModeWithoutClear(Mode.GAME_MODE_SELECT, startRun, cancel)); } else startRun(GameMode.CLASSIC); }, cancel); diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 7de118a19..29e77d7fd 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -146,10 +146,12 @@ export default class UI extends Phaser.GameObjects.Container { showTooltip(title: string, content: string, overlap?: boolean): void { this.tooltipContainer.setVisible(true); - this.tooltipTitle.setText(title); + this.tooltipTitle.setText(title || ''); const wrappedContent = this.tooltipContent.runWordWrap(content); this.tooltipContent.setText(wrappedContent); - this.tooltipBg.height = 31 + 10.5 * (wrappedContent.split('\n').length - 1); + this.tooltipContent.y = title ? 16 : 4; + this.tooltipBg.width = Math.min(Math.max(this.tooltipTitle.displayWidth, this.tooltipContent.displayWidth) + 12, 684); + this.tooltipBg.height = (title ? 31 : 19) + 10.5 * (wrappedContent.split('\n').length - 1); if (overlap) (this.scene as BattleScene).uiContainer.moveAbove(this.tooltipContainer, this); else