pokerogue/src/field/mystery-encounter-intro.ts
ImperialSympathizer acb2b66be4
[Feature] Add Mystery Encounters to the game (#3938)
* add .github/workflows/mystery-event.yml

* update mystery-event.yml

* mystery encounters: resolve review comments:

Lost at Sea:
-fix typo in handlePokemonGuidingYouPhase function

Mysterious Chest:
- remove obsolete commented code

mystery-encounter.ts
- remove unused `onDone` field from MysteryEncounterBuilder

* fix typo in CanLearnMoveRequirementOptions

* remove redundance from Pokemon.isAllowedInBattle()

* chore: jsdoc formatting

* fix lost-at-sea tests

* add fallback for biomeMysteryEncounters if empty

* lost-at-sea-encounter: fix and extend tests

* move "battle:fainted" into `koPlayerPokemon`

* add retries to quick-draw tests

* fix lost-at-sea-encounter tests

* clean up battle animation logic

* Update and rename mystery-event.yml to mystery-events.yml

* Update mystery-events.yml

* Fix typo

* Update mystery-events.yml

Fix debug runs

* clean up unit tests and utils

* attach github issues to all encounter jsdocs

* start dialogue refactor

* update sleeping snorlax encounter

* migrate encounters dialogue to new format

* cleanup and add jsdocs

* finish fiery fallout encounter

* fix unit test breaks

* add skeleton tests to fiery fallout

* commit latest test changes

* finish unit tests for fiery fallout

* bug fix for empty modifier shop

* stash working changes

* stash changes

* Update src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* Update src/test/utils/overridesHelper.ts

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* Update src/test/utils/overridesHelper.ts

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* Update src/test/utils/overridesHelper.ts

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* Update src/test/utils/overridesHelper.ts

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* Update src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* Update src/data/battle-anims.ts

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* nit updates and cleanup

* Update src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* add jsdocs and more cleanup

* add more jsdoc

* add the strong stuff encounter

* add the strong stuff encounter and more unit tests

* cleanup container length checks in ME ui

* add retries to tests

* add retries to tests

* fix trainer wave disable override

* add shuckle juice modifier

* add dialogue bug fixes

* add dialogue bug fixes

* add pokemon salesman encounter and affects pokedex UI display

* add unit tests for pokemon salesman

* temp stash

* add offer you can't refuse

* add unit tests for offer you can't refuse encounter

* remove unnecessary prompt handlers

* add tests for disabled encounter options

* add delibird-y encounter

* add delibird-y encounter

* add absolute avarice encounter

* finish absolute avarice encounter

* add unit tests and enhancements for item overrides in tests

* fix unit test

* cleanup absolute avarice PR

* small bug fixes with latest sync from main

* update visuals loading for safari and stat trainer visuals

* update visuals loading for safari and stat trainer visuals

* update a trainer's test encounter and add unit tests

* add Trash to Treasure encounter

* clean up trash to treasure encounter

* clean up trash to treasure encounter

* add berries abound encounter

* start clowning around encounter

* first implementation pass at clowning around

* add unit tests for clowning around

* add unit tests for clowning around

* clean up ME unit tests

* clean up unit tests

* update unit tests

* add part timer and dancing lessons encounters

* add unit tests for Dancing Lessons and Part-Timer

* reordered biome list and adjusted redirection for project and labels

* Add Weird Dream encounter and slight reworks to Berries Abound/Fight or Flight

* adjusting yml to match new labels

* fix yml whoopsie

* Expanded 'Weird Dream' banlist and fixed a bug with the BST bump range

* adds Winstrate Challenge mystery encounter

* small cleanup for winstrates

* add unit tests for Winstrate Challenge

* fix pokemon not returning after winstrate battle

* commit latest beta merge updates

* fix ME null checks and unit tests with beta update

* fix ME null checks and unit tests with beta update

* MEs to pokerogue beta branch

* test dialogue changes

* test patch fix

* test patch fix

* test patch fix

* adds teleporting hijinks encounter

* add unit tests for Teleporting Hijinks

* small change to teleporting hijinks dialogue

* migrate ME translations to json

* add retries to berries-abound.Option1: should reward the player with X berries based on wave

* add missing ME dialogue back in

* revert template changes

* add ME unique trainer dialogue to both dialogue jsons

* fix hanging comma in json

* fix broken imports

* resolve lint issues

* fix flaky test

* balance tweaks to a few MEs, updates to bug superfan

* add unit tests for Bug-Type Superfan and clean up dialogue

* Adds Fun and Games mystery encounter

* add unit tests for Fun and Games encounter

* update jsdoc

* small ME balance changes

* small ME balance changes

* Adds Uncommon Breed ME and misc. ME bug fixes

* Update getFinalSessionData() to collect Mystery Encounter data

* adds GTS encounter

* various ME bug fixes and balance changes

* latest ME bug fixes

* clean up GTS Encounter and add unit tests

* small cleanup to MEs branch

* add BGM music names for ME music

* bug fixes and balance changes for MEs

* ME data schema updates

* balance changes and bug fixes to MEs

* balance changes and bug fixes to MEs

* update tests for MEs

* add jsdoc to party exp function

* dialogue updates and test fixes for MEs

* dialogue updates and test fixes for MEs

* PR suggestions and fixees

* stash PR feedback and bugfixes

* fix all tests for MEs and cleanup

* PR feedback

* update flaky ME test

* update tests, bug fix MEs, and sprite assets

* remove unintentional console log

* re-enable stubbed function for Phaser text styling

* handle undefined introVisuals properly

* PR feedback from NightKev

* disable Uncommon Breed tests

* locales updates and bug fixes for safari zone

* more PR feedback and update field trip with Rarer Candy

* fix unit test

* Change how reroll button gets disabled in Modifier Shop Phase

* update continue button text logic

* Update src/ui/modifier-select-ui-handler.ts

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* fix money formatting and some nits

* more nits

* more nits

* update ME tsdocs with links

* update ME tsdocs with links

---------

Co-authored-by: Felix Staud <felix.staud@headwire.com>
Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>
Co-authored-by: ImperialSympathizer <imperialsympathizer@gmail.com>
Co-authored-by: InnocentGameDev <asdargmng@gmail.com>
Co-authored-by: Mumble <171087428+frutescens@users.noreply.github.com>
2024-09-14 03:05:58 +01:00

457 lines
13 KiB
TypeScript

import { GameObjects } from "phaser";
import BattleScene from "../battle-scene";
import MysteryEncounter from "../data/mystery-encounters/mystery-encounter";
import { Species } from "#enums/species";
import { isNullOrUndefined } from "#app/utils";
import { getSpriteKeysFromSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import PlayAnimationConfig = Phaser.Types.Animations.PlayAnimationConfig;
type KnownFileRoot =
| "arenas"
| "battle_anims"
| "cg"
| "character"
| "effect"
| "egg"
| "events"
| "inputs"
| "items"
| "mystery-encounters"
| "pokeball"
| "pokemon"
| "pokemon/back"
| "pokemon/exp"
| "pokemon/female"
| "pokemon/icons"
| "pokemon/input"
| "pokemon/shiny"
| "pokemon/variant"
| "statuses"
| "trainer"
| "ui";
export class MysteryEncounterSpriteConfig {
/** The sprite key (which is the image file name). e.g. "ace_trainer_f" */
spriteKey: string;
/** Refer to [/public/images](../../public/images) directorty for all folder names */
fileRoot: KnownFileRoot & string | string;
/** Optional replacement for `spriteKey`/`fileRoot`. Just know this defaults to male/genderless, form 0, no shiny */
species?: Species;
/** Enable shadow. Defaults to `false` */
hasShadow?: boolean = false;
/** Disable animation. Defaults to `false` */
disableAnimation?: boolean = false;
/** Repeat the animation. Defaults to `false` */
repeat?: boolean = false;
/** What frame of the animation to start on. Defaults to 0 */
startFrame?: number = 0;
/** Hidden at start of encounter. Defaults to `false` */
hidden?: boolean = false;
/** Tint color. `0` - `1`. Higher means darker tint. */
tint?: number;
/** X offset */
x?: number;
/** Y offset */
y?: number;
/** Y shadow offset */
yShadow?: number;
/** Sprite scale. `0` - `n` */
scale?: number;
/** If you are using a Pokemon sprite, set to `true`. This will ensure variant, form, gender, shiny sprites are loaded properly */
isPokemon?: boolean;
/** If you are using an item sprite, set to `true` */
isItem?: boolean;
/** The sprites alpha. `0` - `1` The lower the number, the more transparent */
alpha?: number;
}
/**
* When a mystery encounter spawns, there are visuals (mainly sprites) tied to the field for the new encounter to inform the player of the type of encounter
* These slide in with the field as part of standard field change cycle, and will typically be hidden after the player has selected an option for the encounter
* Note: intro visuals are not "Trainers" or any other specific game object, though they may contain trainer sprites
*/
export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Container {
public encounter: MysteryEncounter;
public spriteConfigs: MysteryEncounterSpriteConfig[];
public enterFromRight: boolean;
constructor(scene: BattleScene, encounter: MysteryEncounter) {
super(scene, -72, 76);
this.encounter = encounter;
this.enterFromRight = encounter.enterIntroVisualsFromRight ?? false;
// Shallow copy configs to allow visual config updates at runtime without dirtying master copy of Encounter
this.spriteConfigs = encounter.spriteConfigs.map(config => {
const result = {
...config
};
if (!isNullOrUndefined(result.species)) {
const keys = getSpriteKeysFromSpecies(result.species!);
result.spriteKey = keys.spriteKey;
result.fileRoot = keys.fileRoot;
result.isPokemon = true;
}
return result;
});
if (!this.spriteConfigs) {
return;
}
const getSprite = (spriteKey: string, hasShadow?: boolean, yShadow?: number) => {
const ret = this.scene.addFieldSprite(0, 0, spriteKey);
ret.setOrigin(0.5, 1);
ret.setPipeline(this.scene.spritePipeline, { tone: [0.0, 0.0, 0.0, 0.0], hasShadow: !!hasShadow, yShadowOffset: yShadow ?? 0 });
return ret;
};
const getItemSprite = (spriteKey: string, hasShadow?: boolean, yShadow?: number) => {
const icon = this.scene.add.sprite(-19, 2, "items", spriteKey);
icon.setOrigin(0.5, 1);
icon.setPipeline(this.scene.spritePipeline, { tone: [0.0, 0.0, 0.0, 0.0], hasShadow: !!hasShadow, yShadowOffset: yShadow ?? 0 });
return icon;
};
// Depending on number of sprites added, should space them to be on the circular field sprite
const minX = -40;
const maxX = 40;
const origin = 4;
let n = 0;
// Sprites with custom X or Y defined will not count for normal spacing requirements
const spacingValue = Math.round((maxX - minX) / Math.max(this.spriteConfigs.filter(s => !s.x && !s.y).length, 1));
this.spriteConfigs?.forEach((config) => {
const { spriteKey, isItem, hasShadow, scale, x, y, yShadow, alpha } = config;
let sprite: GameObjects.Sprite;
let tintSprite: GameObjects.Sprite;
if (!isItem) {
sprite = getSprite(spriteKey, hasShadow, yShadow);
tintSprite = getSprite(spriteKey);
} else {
sprite = getItemSprite(spriteKey, hasShadow, yShadow);
tintSprite = getItemSprite(spriteKey);
}
sprite.setVisible(!config.hidden);
tintSprite.setVisible(false);
if (scale) {
sprite.setScale(scale);
tintSprite.setScale(scale);
}
// Sprite offset from origin
if (x || y) {
if (x) {
sprite.setPosition(origin + x, sprite.y);
tintSprite.setPosition(origin + x, tintSprite.y);
}
if (y) {
sprite.setPosition(sprite.x, sprite.y + y);
tintSprite.setPosition(tintSprite.x, tintSprite.y + y);
}
} else {
// Single sprite
if (this.spriteConfigs.length === 1) {
sprite.x = origin;
tintSprite.x = origin;
} else {
// Do standard sprite spacing (not including offset sprites)
sprite.x = minX + (n + 0.5) * spacingValue + origin;
tintSprite.x = minX + (n + 0.5) * spacingValue + origin;
n++;
}
}
if (!isNullOrUndefined(alpha)) {
sprite.setAlpha(alpha);
tintSprite.setAlpha(alpha);
}
this.add(sprite);
this.add(tintSprite);
});
}
/**
* Loads the assets that were defined on construction (async)
*/
loadAssets(): Promise<void> {
return new Promise(resolve => {
if (!this.spriteConfigs) {
resolve();
}
this.spriteConfigs.forEach((config) => {
if (config.isPokemon) {
this.scene.loadPokemonAtlas(config.spriteKey, config.fileRoot);
} else if (config.isItem) {
this.scene.loadAtlas("items", "");
} else {
this.scene.loadAtlas(config.spriteKey, config.fileRoot);
}
});
this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => {
this.spriteConfigs.every((config) => {
if (config.isItem) {
return true;
}
const originalWarn = console.warn;
// Ignore warnings for missing frames, because there will be a lot
console.warn = () => {
};
const frameNames = this.scene.anims.generateFrameNames(config.spriteKey, { zeroPad: 4, suffix: ".png", start: 1, end: 128 });
console.warn = originalWarn;
if (!(this.scene.anims.exists(config.spriteKey))) {
this.scene.anims.create({
key: config.spriteKey,
frames: frameNames,
frameRate: 12,
repeat: -1
});
}
return true;
});
resolve();
});
if (!this.scene.load.isLoading()) {
this.scene.load.start();
}
});
}
/**
* Sets the initial frames and tint of sprites after load
*/
initSprite(): void {
if (!this.spriteConfigs) {
return;
}
this.getSprites().map((sprite, i) => {
if (!this.spriteConfigs[i].isItem) {
sprite.setTexture(this.spriteConfigs[i].spriteKey).setFrame(0);
}
});
this.getTintSprites().map((tintSprite, i) => {
if (!this.spriteConfigs[i].isItem) {
tintSprite.setTexture(this.spriteConfigs[i].spriteKey).setFrame(0);
}
});
this.spriteConfigs.every((config, i) => {
if (!config.tint) {
return true;
}
const tintSprite = this.getAt(i * 2 + 1);
this.tint(tintSprite, 0, config.tint);
return true;
});
}
/**
* Attempts to animate a given set of {@linkcode Phaser.GameObjects.Sprite}
* @see {@linkcode Phaser.GameObjects.Sprite.play}
* @param sprite {@linkcode Phaser.GameObjects.Sprite} to animate
* @param tintSprite {@linkcode Phaser.GameObjects.Sprite} placed on top of the sprite to add a color tint
* @param animConfig {@linkcode Phaser.Types.Animations.PlayAnimationConfig} to pass to {@linkcode Phaser.GameObjects.Sprite.play}
* @returns true if the sprite was able to be animated
*/
tryPlaySprite(sprite: Phaser.GameObjects.Sprite, tintSprite: Phaser.GameObjects.Sprite, animConfig: Phaser.Types.Animations.PlayAnimationConfig): boolean {
// Show an error in the console if there isn't a texture loaded
if (sprite.texture.key === "__MISSING") {
console.error(`No texture found for '${animConfig.key}'!`);
return false;
}
// Don't try to play an animation when there isn't one
if (sprite.texture.frameTotal <= 1) {
console.warn(`No animation found for '${animConfig.key}'. Is this intentional?`);
return false;
}
sprite.play(animConfig);
tintSprite.play(animConfig);
return true;
}
/**
* For sprites with animation and that do not have animation disabled, will begin frame animation
*/
playAnim(): void {
if (!this.spriteConfigs) {
return;
}
const sprites = this.getSprites();
const tintSprites = this.getTintSprites();
this.spriteConfigs.forEach((config, i) => {
if (!config.disableAnimation) {
const trainerAnimConfig: PlayAnimationConfig = {
key: config.spriteKey,
repeat: config?.repeat ? -1 : 0,
startFrame: config?.startFrame ?? 0
};
this.tryPlaySprite(sprites[i], tintSprites[i], trainerAnimConfig);
}
});
}
/**
* Returns a Sprite/TintSprite pair
* @param index
*/
getSpriteAtIndex(index: number): Phaser.GameObjects.Sprite[] {
if (!this.spriteConfigs) {
return [];
}
const ret: Phaser.GameObjects.Sprite[] = [];
ret.push(this.getAt(index * 2)); // Sprite
ret.push(this.getAt(index * 2 + 1)); // Tint Sprite
return ret;
}
/**
* Gets all non-tint sprites (these are the "real" unmodified sprites)
*/
getSprites(): Phaser.GameObjects.Sprite[] {
if (!this.spriteConfigs) {
return [];
}
const ret: Phaser.GameObjects.Sprite[] = [];
this.spriteConfigs.forEach((config, i) => {
ret.push(this.getAt(i * 2));
});
return ret;
}
/**
* Gets all tint sprites (duplicate sprites that have different alpha and fill values)
*/
getTintSprites(): Phaser.GameObjects.Sprite[] {
if (!this.spriteConfigs) {
return [];
}
const ret: Phaser.GameObjects.Sprite[] = [];
this.spriteConfigs.forEach((config, i) => {
ret.push(this.getAt(i * 2 + 1));
});
return ret;
}
/**
* Tints a single sprite
* @param sprite
* @param color
* @param alpha
* @param duration
* @param ease
*/
private tint(sprite, color: number, alpha?: number, duration?: integer, ease?: string): void {
// const tintSprites = this.getTintSprites();
sprite.setTintFill(color);
sprite.setVisible(true);
if (duration) {
sprite.setAlpha(0);
this.scene.tweens.add({
targets: sprite,
alpha: alpha || 1,
duration: duration,
ease: ease || "Linear"
});
} else {
sprite.setAlpha(alpha);
}
}
/**
* Tints all sprites
* @param color
* @param alpha
* @param duration
* @param ease
*/
tintAll(color: number, alpha?: number, duration?: integer, ease?: string): void {
const tintSprites = this.getTintSprites();
tintSprites.map(tintSprite => {
this.tint(tintSprite, color, alpha, duration, ease);
});
}
/**
* Untints a single sprite over a duration
* @param sprite
* @param duration
* @param ease
*/
private untint(sprite, duration: integer, ease?: string): void {
if (duration) {
this.scene.tweens.add({
targets: sprite,
alpha: 0,
duration: duration,
ease: ease || "Linear",
onComplete: () => {
sprite.setVisible(false);
sprite.setAlpha(1);
}
});
} else {
sprite.setVisible(false);
sprite.setAlpha(1);
}
}
/**
* Untints all sprites
* @param sprite
* @param duration
* @param ease
*/
untintAll(duration: integer, ease?: string): void {
const tintSprites = this.getTintSprites();
tintSprites.map(tintSprite => {
this.untint(tintSprite, duration, ease);
});
}
/**
* Sets container and all child sprites to visible
* @param value - true for visible, false for hidden
*/
setVisible(value: boolean): this {
this.getSprites().forEach(sprite => {
sprite.setVisible(value);
});
return super.setVisible(value);
}
}
/**
* Interface is required so as not to override {@link Phaser.GameObjects.Container.scene}
*/
export default interface MysteryEncounterIntroVisuals {
scene: BattleScene
}