diff --git a/src/battle-scene.ts b/src/battle-scene.ts index af42d74db1b..b21ae7b29df 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -468,8 +468,8 @@ export default class BattleScene extends Phaser.Scene { } launchBattle() { - this.arenaBg = this.addFieldSprite(0, 0, 'plains_bg'); - this.arenaBgTransition = this.addFieldSprite(0, 0, `plains_bg`); + this.arenaBg = this.addFieldSprite(0, 0, 'plains_bg', null, 0); + this.arenaBgTransition = this.addFieldSprite(0, 0, `plains_bg`, null, 1); [ this.arenaBgTransition, this.arenaBg ].forEach(a => { a.setScale(6); @@ -920,6 +920,7 @@ export default class BattleScene extends Phaser.Scene { const biomeKey = getBiomeKey(biome); this.arenaBg.setTexture(`${biomeKey}_bg`); + this.arenaBg.pipelineData['terrainColorRatio'] = this.arena.getBgTerrainColorRatioForBiome(); this.arenaBgTransition.setTexture(`${biomeKey}_bg`); this.arenaPlayer.setBiome(biome); this.arenaPlayerTransition.setBiome(biome); @@ -1077,9 +1078,11 @@ export default class BattleScene extends Phaser.Scene { Phaser.Math.RND.state(state); } - addFieldSprite(x: number, y: number, texture: string | Phaser.Textures.Texture, frame?: string | number): Phaser.GameObjects.Sprite { + addFieldSprite(x: number, y: number, texture: string | Phaser.Textures.Texture, frame?: string | number, terrainColorRatio: number = 0): Phaser.GameObjects.Sprite { const ret = this.add.sprite(x, y, texture, frame); ret.setPipeline(this.fieldSpritePipeline); + if (terrainColorRatio) + ret.pipelineData['terrainColorRatio'] = terrainColorRatio; return ret; } diff --git a/src/data/ability.ts b/src/data/ability.ts index 3deef28b476..fee7eab9f72 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2,7 +2,7 @@ import Pokemon, { HitResult, PokemonMove } from "../field/pokemon"; import { Type } from "./type"; import * as Utils from "../utils"; import { BattleStat, getBattleStatName } from "./battle-stat"; -import { ObtainStatusEffectPhase, PokemonHealPhase, ShowAbilityPhase, StatChangePhase } from "../phases"; +import { PokemonHealPhase, ShowAbilityPhase, StatChangePhase } from "../phases"; import { getPokemonMessage } from "../messages"; import { Weather, WeatherType } from "./weather"; import { BattlerTag } from "./battler-tags"; @@ -368,7 +368,7 @@ export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr { applyPostDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, hitResult: HitResult, args: any[]): boolean { if (move.getMove().checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && pokemon.randSeedInt(100) < this.chance && !pokemon.status) { const effect = this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randSeedInt(this.effects.length)]; - pokemon.scene.unshiftPhase(new ObtainStatusEffectPhase(pokemon.scene, attacker.getBattlerIndex(), effect)); + return pokemon.trySetStatus(effect, true); } return false; @@ -1261,7 +1261,8 @@ export function applyPreStatChangeAbAttrs(attrType: { new(...args: any[]): PreSt export function applyPreSetStatusAbAttrs(attrType: { new(...args: any[]): PreSetStatusAbAttr }, pokemon: Pokemon, effect: StatusEffect, cancelled: Utils.BooleanHolder, ...args: any[]): Promise { - return applyAbAttrsInternal(attrType, pokemon, attr => attr.applyPreSetStatus(pokemon, effect, cancelled, args)); + const simulated = args.length > 1 && args[1]; + return applyAbAttrsInternal(attrType, pokemon, attr => attr.applyPreSetStatus(pokemon, effect, cancelled, args), false, false, !simulated); } export function applyPreApplyBattlerTagAbAttrs(attrType: { new(...args: any[]): PreApplyBattlerTagAbAttr }, diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index c6ca43722d9..87a2d201ef1 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -4,9 +4,8 @@ import * as Utils from "../utils"; import { allMoves } from "./move"; import { getPokemonMessage } from "../messages"; import Pokemon, { HitResult, PokemonMove } from "../field/pokemon"; -import { DamagePhase, MoveEffectPhase, ObtainStatusEffectPhase } from "../phases"; +import { MoveEffectPhase } from "../phases"; import { StatusEffect } from "./status-effect"; -import { BattlerTagType } from "./enums/battler-tag-type"; import { BattlerIndex } from "../battle"; import { Moves } from "./enums/moves"; import { ArenaTagType } from "./enums/arena-tag-type"; @@ -168,7 +167,7 @@ class SpikesTag extends ArenaTrapTag { } activateTrap(pokemon: Pokemon): boolean { - if ((!pokemon.isOfType(Type.FLYING) || pokemon.getTag(BattlerTagType.IGNORE_FLYING) || pokemon.scene.arena.getTag(ArenaTagType.GRAVITY))) { + if (pokemon.isGrounded()) { const damageHpRatio = 1 / (10 - 2 * this.layers); const damage = Math.ceil(pokemon.getMaxHp() * damageHpRatio); @@ -194,12 +193,9 @@ class ToxicSpikesTag extends ArenaTrapTag { } activateTrap(pokemon: Pokemon): boolean { - if (!pokemon.status && (!pokemon.isOfType(Type.FLYING) || pokemon.getTag(BattlerTagType.IGNORE_FLYING) || pokemon.scene.arena.getTag(ArenaTagType.GRAVITY))) { + if (!pokemon.status && pokemon.isGrounded()) { const toxic = this.layers > 1; - - pokemon.scene.unshiftPhase(new ObtainStatusEffectPhase(pokemon.scene, pokemon.getBattlerIndex(), - !toxic ? StatusEffect.POISON : StatusEffect.TOXIC, null, `the ${this.getMoveName()}`)); - return true; + return pokemon.trySetStatus(!toxic ? StatusEffect.POISON : StatusEffect.TOXIC, true, null, `the ${this.getMoveName()}`); } return false; diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 8ed038c2956..28e54454080 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1,5 +1,5 @@ import { CommonAnim, CommonBattleAnim } from "./battle-anims"; -import { CommonAnimPhase, DamagePhase, MovePhase, ObtainStatusEffectPhase, PokemonHealPhase, ShowAbilityPhase } from "../phases"; +import { CommonAnimPhase, MovePhase, PokemonHealPhase, ShowAbilityPhase } from "../phases"; import { getPokemonMessage } from "../messages"; import Pokemon, { MoveResult, HitResult } from "../field/pokemon"; import { Stat } from "./pokemon-stat"; @@ -10,6 +10,7 @@ import { ChargeAttr, allMoves } from "./move"; import { Type } from "./type"; import { Abilities, FlinchEffectAbAttr, applyAbAttrs } from "./ability"; import { BattlerTagType } from "./enums/battler-tag-type"; +import { TerrainType } from "./terrain"; export enum BattlerTagLapseType { FAINT, @@ -148,6 +149,10 @@ export class ConfusedTag extends BattlerTag { super(BattlerTagType.CONFUSED, BattlerTagLapseType.MOVE, turnCount, sourceMove); } + canAdd(pokemon: Pokemon): boolean { + return pokemon.scene.arena.terrain?.terrainType !== TerrainType.MISTY || !pokemon.isGrounded(); + } + onAdd(pokemon: Pokemon): void { super.onAdd(pokemon); @@ -427,6 +432,10 @@ export class DrowsyTag extends BattlerTag { super(BattlerTagType.DROWSY, BattlerTagLapseType.TURN_END, 2, Moves.YAWN); } + canAdd(pokemon: Pokemon): boolean { + return pokemon.scene.arena.terrain?.terrainType !== TerrainType.ELECTRIC || !pokemon.isGrounded(); + } + onAdd(pokemon: Pokemon): void { super.onAdd(pokemon); @@ -435,7 +444,7 @@ export class DrowsyTag extends BattlerTag { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { if (!super.lapse(pokemon, lapseType)) { - pokemon.scene.unshiftPhase(new ObtainStatusEffectPhase(pokemon.scene, pokemon.getBattlerIndex(), StatusEffect.SLEEP)); + pokemon.trySetStatus(StatusEffect.SLEEP, true); return false; } diff --git a/src/data/move.ts b/src/data/move.ts index b09d821428e..99d4e40da96 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1,12 +1,12 @@ import { Moves } from "./enums/moves"; import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims"; -import { BattleEndPhase, DamagePhase, MovePhase, NewBattlePhase, ObtainStatusEffectPhase, PokemonHealPhase, StatChangePhase, SwitchSummonPhase } from "../phases"; +import { BattleEndPhase, MovePhase, NewBattlePhase, PokemonHealPhase, StatChangePhase, SwitchSummonPhase } from "../phases"; import { BattleStat, getBattleStatName } from "./battle-stat"; import { EncoreTag } from "./battler-tags"; import { BattlerTagType } from "./enums/battler-tag-type"; import { getPokemonMessage } from "../messages"; import Pokemon, { AttackMoveResult, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon"; -import { StatusEffect, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect"; +import { StatusEffect, getStatusEffectHealText } from "./status-effect"; import { Type } from "./type"; import * as Utils from "../utils"; import { WeatherType } from "./weather"; @@ -16,6 +16,7 @@ import { Abilities, ProtectAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAb import { PokemonHeldItemModifier } from "../modifier/modifier"; import { BattlerIndex } from "../battle"; import { Stat } from "./pokemon-stat"; +import { TerrainType } from "./terrain"; export enum MoveCategory { PHYSICAL, @@ -767,10 +768,8 @@ export class StatusEffectAttr extends MoveEffectAttr { else return false; } - if (!pokemon.status || (pokemon.status.effect === this.effect && move.chance < 0)) { - user.scene.unshiftPhase(new ObtainStatusEffectPhase(user.scene, pokemon.getBattlerIndex(), this.effect, this.cureTurn)); - return true; - } + if (!pokemon.status || (pokemon.status.effect === this.effect && move.chance < 0)) + return pokemon.trySetStatus(this.effect, true); } return false; } @@ -902,6 +901,46 @@ export class ClearWeatherAttr extends MoveEffectAttr { } } +export class TerrainChangeAttr extends MoveEffectAttr { + private terrainType: TerrainType; + + constructor(terrainType: TerrainType) { + super(); + + this.terrainType = terrainType; + } + + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + return user.scene.arena.trySetTerrain(this.terrainType, true, true); + } + + getCondition(): MoveConditionFunc { + return (user, target, move) => !user.scene.arena.terrain || (user.scene.arena.terrain.terrainType !== this.terrainType); + } + + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { + // TODO: Expand on this + return user.scene.arena.terrain ? 0 : 6; + } +} + +export class ClearTerrainAttr extends MoveEffectAttr { + private terrainType: TerrainType; + + constructor(terrainType: TerrainType) { + super(); + + this.terrainType = terrainType; + } + + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + if (user.scene.arena.terrain?.terrainType === this.terrainType) + return user.scene.arena.trySetTerrain(TerrainType.NONE, true, true); + + return false; + } +} + export class OneHitKOAttr extends MoveAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { if (target.isBossImmune()) @@ -979,9 +1018,9 @@ export class ChargeAttr extends OverrideMoveEffectAttr { } } -export class SolarBeamChargeAttr extends ChargeAttr { - constructor() { - super(ChargeAnim.SOLAR_BEAM_CHARGING, 'took\nin sunlight!'); +export class SunlightChargeAttr extends ChargeAttr { + constructor(chargeAnim: ChargeAnim, chargeText: string) { + super(chargeAnim, chargeText); } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { @@ -1347,7 +1386,7 @@ export class TurnDamagedDoublePowerAttr extends VariablePowerAttr { } } -export class SolarBeamPowerAttr extends VariablePowerAttr { +export class AntiSunlightPowerDecreaseAttr extends VariablePowerAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { if (!user.scene.arena.weather?.isEffectSuppressed(user.scene)) { const power = args[0] as Utils.NumberHolder; @@ -2491,8 +2530,8 @@ export function initMoves() { .slicingMove() .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.SOLAR_BEAM, "Solar Beam", Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, 168, "In this two-turn attack, the user gathers light, then blasts a bundled beam on the next turn.", -1, 0, 1) - .attr(SolarBeamChargeAttr) - .attr(SolarBeamPowerAttr) + .attr(SunlightChargeAttr, ChargeAnim.SOLAR_BEAM_CHARGING, 'took\nin sunlight!') + .attr(AntiSunlightPowerDecreaseAttr) .ignoresVirtual(), new StatusMove(Moves.POISON_POWDER, "Poison Powder", Type.POISON, 75, 35, -1, "The user scatters a cloud of poisonous dust that poisons the target.", -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.POISON) @@ -3677,9 +3716,11 @@ export function initMoves() { .target(MoveTarget.USER_SIDE), new StatusMove(Moves.FLOWER_SHIELD, "Flower Shield (N)", Type.FAIRY, -1, 10, -1, "The user raises the Defense stats of all Grass-type Pokémon in battle with a mysterious power.", 100, 0, 6) .target(MoveTarget.ALL), - new StatusMove(Moves.GRASSY_TERRAIN, "Grassy Terrain (N)", Type.GRASS, -1, 10, -1, "The user turns the ground to grass for five turns. This restores the HP of Pokémon on the ground a little every turn and powers up Grass-type moves.", -1, 0, 6) + new StatusMove(Moves.GRASSY_TERRAIN, "Grassy Terrain", Type.GRASS, -1, 10, -1, "The user turns the ground to grass for five turns. This restores the HP of Pokémon on the ground a little every turn and powers up Grass-type moves.", -1, 0, 6) + .attr(TerrainChangeAttr, TerrainType.GRASSY) .target(MoveTarget.BOTH_SIDES), - new StatusMove(Moves.MISTY_TERRAIN, "Misty Terrain (N)", Type.FAIRY, -1, 10, -1, "This protects Pokémon on the ground from status conditions and halves damage from Dragon-type moves for five turns.", -1, 0, 6) + new StatusMove(Moves.MISTY_TERRAIN, "Misty Terrain", Type.FAIRY, -1, 10, -1, "This protects Pokémon on the ground from status conditions and halves damage from Dragon-type moves for five turns.", -1, 0, 6) + .attr(TerrainChangeAttr, TerrainType.MISTY) .target(MoveTarget.BOTH_SIDES), new StatusMove(Moves.ELECTRIFY, "Electrify (N)", Type.ELECTRIC, -1, 20, -1, "If the target is electrified before it uses a move during that turn, the target's move becomes Electric type.", -1, 0, 6), new AttackMove(Moves.PLAY_ROUGH, "Play Rough", Type.FAIRY, MoveCategory.PHYSICAL, 90, 90, 10, -1, "The user plays rough with the target and attacks it. This may also lower the target's Attack stat.", 10, 0, 6) @@ -3733,7 +3774,8 @@ export function initMoves() { .condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS].find(a => a === p.getAbility().id))), new StatusMove(Moves.HAPPY_HOUR, "Happy Hour (N)", Type.NORMAL, -1, 30, -1, "Using Happy Hour doubles the amount of prize money received after battle.", -1, 0, 6) // No animation .target(MoveTarget.USER_SIDE), - new StatusMove(Moves.ELECTRIC_TERRAIN, "Electric Terrain (N)", Type.ELECTRIC, -1, 10, -1, "The user electrifies the ground for five turns, powering up Electric-type moves. Pokémon on the ground no longer fall asleep.", -1, 0, 6) + new StatusMove(Moves.ELECTRIC_TERRAIN, "Electric Terrain", Type.ELECTRIC, -1, 10, -1, "The user electrifies the ground for five turns, powering up Electric-type moves. Pokémon on the ground no longer fall asleep.", -1, 0, 6) + .attr(TerrainChangeAttr, TerrainType.ELECTRIC) .target(MoveTarget.BOTH_SIDES), new AttackMove(Moves.DAZZLING_GLEAM, "Dazzling Gleam", Type.FAIRY, MoveCategory.SPECIAL, 80, 100, 10, -1, "The user damages opposing Pokémon by emitting a powerful flash.", -1, 0, 6) .target(MoveTarget.ALL_NEAR_ENEMIES), @@ -3831,7 +3873,8 @@ export function initMoves() { new StatusMove(Moves.STRENGTH_SAP, "Strength Sap (P)", Type.GRASS, 100, 10, -1, "The user restores its HP by the same amount as the target's Attack stat. It also lowers the target's Attack stat.", 100, 0, 7) .attr(StatChangeAttr, BattleStat.ATK, -1), new AttackMove(Moves.SOLAR_BLADE, "Solar Blade", Type.GRASS, MoveCategory.PHYSICAL, 125, 100, 10, -1, "In this two-turn attack, the user gathers light and fills a blade with the light's energy, attacking the target on the next turn.", -1, 0, 7) - .attr(ChargeAttr, ChargeAnim.SOLAR_BLADE_CHARGING, "is glowing!") + .attr(SunlightChargeAttr, ChargeAnim.SOLAR_BLADE_CHARGING, "is glowing!") + .attr(AntiSunlightPowerDecreaseAttr) .slicingMove(), new AttackMove(Moves.LEAFAGE, "Leafage", Type.GRASS, MoveCategory.PHYSICAL, 40, 100, 40, -1, "The user attacks by pelting the target with leaves.", -1, 0, 7), new StatusMove(Moves.SPOTLIGHT, "Spotlight (N)", Type.NORMAL, -1, 15, -1, "The user shines a spotlight on the target so that only the target will be attacked during the turn.", -1, 3, 7), @@ -3848,7 +3891,8 @@ export function initMoves() { .ballBombMove(), new AttackMove(Moves.ANCHOR_SHOT, "Anchor Shot", Type.STEEL, MoveCategory.PHYSICAL, 80, 100, 20, -1, "The user entangles the target with its anchor chain while attacking. The target becomes unable to flee.", -1, 0, 7) .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1), - new StatusMove(Moves.PSYCHIC_TERRAIN, "Psychic Terrain (N)", Type.PSYCHIC, -1, 10, -1, "This protects Pokémon on the ground from priority moves and powers up Psychic-type moves for five turns.", -1, 0, 7) + new StatusMove(Moves.PSYCHIC_TERRAIN, "Psychic Terrain", Type.PSYCHIC, -1, 10, -1, "This protects Pokémon on the ground from priority moves and powers up Psychic-type moves for five turns.", -1, 0, 7) + .attr(TerrainChangeAttr, TerrainType.PSYCHIC) .target(MoveTarget.BOTH_SIDES), new AttackMove(Moves.LUNGE, "Lunge", Type.BUG, MoveCategory.PHYSICAL, 80, 100, 15, -1, "The user makes a lunge at the target, attacking with full force. This also lowers the target's Attack stat.", 100, 0, 7) .attr(StatChangeAttr, BattleStat.ATK, -1), diff --git a/src/data/terrain.ts b/src/data/terrain.ts new file mode 100644 index 00000000000..c9814dbd705 --- /dev/null +++ b/src/data/terrain.ts @@ -0,0 +1,56 @@ +import { Type } from "./type"; + +export enum TerrainType { + NONE, + MISTY, + ELECTRIC, + GRASSY, + PSYCHIC +} + +export class Terrain { + public terrainType: TerrainType; + public turnsLeft: integer; + + constructor(terrainType: TerrainType, turnsLeft?: integer) { + this.terrainType = terrainType; + this.turnsLeft = turnsLeft || 0; + } + + lapse(): boolean { + if (this.turnsLeft) + return !!--this.turnsLeft; + + return true; + } + + getAttackTypeMultiplier(attackType: Type): number { + switch (this.terrainType) { + case TerrainType.GRASSY: + if (attackType === Type.GRASS) + return 1.3; + break; + case TerrainType.PSYCHIC: + if (attackType === Type.PSYCHIC) + return 1.3; + break; + } + + return 1; + } +} + +export function getTerrainColor(terrainType: TerrainType): [ integer, integer, integer ] { + switch (terrainType) { + case TerrainType.GRASSY: + return [ 120, 200, 80 ]; + case TerrainType.MISTY: + return [ 232, 136, 200 ]; + case TerrainType.ELECTRIC: + return [ 248, 248, 120 ]; + case TerrainType.PSYCHIC: + return [ 160, 64, 160 ]; + } + + return [ 0, 0, 0 ]; +} \ No newline at end of file diff --git a/src/data/weather.ts b/src/data/weather.ts index 6d467b36016..398bee6d1fc 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -6,6 +6,7 @@ import Move, { AttackMove } from "./move"; import * as Utils from "../utils"; import BattleScene from "../battle-scene"; import { SuppressWeatherEffectAbAttr, applyPreWeatherEffectAbAttrs } from "./ability"; +import { TerrainType } from "./terrain"; export enum WeatherType { NONE, @@ -112,7 +113,7 @@ export class Weather { } } -export function getWeatherStartMessage(weatherType: WeatherType) { +export function getWeatherStartMessage(weatherType: WeatherType): string { switch (weatherType) { case WeatherType.SUNNY: return 'The sunlight got bright!'; @@ -135,7 +136,7 @@ export function getWeatherStartMessage(weatherType: WeatherType) { return null; } -export function getWeatherLapseMessage(weatherType: WeatherType) { +export function getWeatherLapseMessage(weatherType: WeatherType): string { switch (weatherType) { case WeatherType.SUNNY: return 'The sunlight is strong.'; @@ -158,7 +159,7 @@ export function getWeatherLapseMessage(weatherType: WeatherType) { return null; } -export function getWeatherDamageMessage(weatherType: WeatherType, pokemon: Pokemon) { +export function getWeatherDamageMessage(weatherType: WeatherType, pokemon: Pokemon): string { switch (weatherType) { case WeatherType.SANDSTORM: return getPokemonMessage(pokemon, ' is buffeted\nby the sandstorm!'); @@ -169,7 +170,7 @@ export function getWeatherDamageMessage(weatherType: WeatherType, pokemon: Pokem return null; } -export function getWeatherClearMessage(weatherType: WeatherType) { +export function getWeatherClearMessage(weatherType: WeatherType): string { switch (weatherType) { case WeatherType.SUNNY: return 'The sunlight faded.'; @@ -192,6 +193,18 @@ export function getWeatherClearMessage(weatherType: WeatherType) { return null; } +export function getTerrainStartMessage(terrainType: TerrainType): string { + return terrainType + ? `The terrain became ${Utils.toReadableString(TerrainType[terrainType])}!` + : null; +} + +export function getTerrainClearMessage(terrainType: TerrainType): string { + return terrainType + ? `The ${Utils.toReadableString(TerrainType[terrainType])} terrain faded.` + : null; +} + interface WeatherPoolEntry { weatherType: WeatherType; weight: integer; diff --git a/src/field/arena.ts b/src/field/arena.ts index ffecd77b42f..d4c63600828 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -4,7 +4,7 @@ import { Biome } from "../data/enums/biome"; import * as Utils from "../utils"; import PokemonSpecies, { getPokemonSpecies } from "../data/pokemon-species"; import { Species } from "../data/enums/species"; -import { Weather, WeatherType, getWeatherClearMessage, getWeatherStartMessage } from "../data/weather"; +import { Weather, WeatherType, getTerrainClearMessage, getTerrainStartMessage, getWeatherClearMessage, getWeatherStartMessage } from "../data/weather"; import { CommonAnimPhase } from "../phases"; import { CommonAnim } from "../data/battle-anims"; import { Type } from "../data/type"; @@ -16,6 +16,7 @@ import { TrainerType } from "../data/enums/trainer-type"; import { BattlerIndex } from "../battle"; import { Moves } from "../data/enums/moves"; import { TimeOfDay } from "../data/enums/time-of-day"; +import { Terrain, TerrainType, getTerrainColor } from "../data/terrain"; const WEATHER_OVERRIDE = WeatherType.NONE; @@ -23,9 +24,9 @@ export class Arena { public scene: BattleScene; public biomeType: Biome; public weather: Weather; + public terrain: Terrain; public tags: ArenaTag[]; public bgm: string; - private lastTimeOfDay: TimeOfDay; private pokemonPool: PokemonPools; @@ -230,6 +231,17 @@ export class Arena { } } + getBgTerrainColorRatioForBiome(): number { + switch (this.biomeType) { + case Biome.SPACE: + return 1; + case Biome.END: + return 0; + } + + return 83 / 132; + } + trySetWeatherOverride(weather: WeatherType): boolean { this.weather = new Weather(weather, 0); this.scene.unshiftPhase(new CommonAnimPhase(this.scene, undefined, undefined, CommonAnim.SUNNY + (weather - 1))); @@ -259,15 +271,52 @@ export class Arena { return true; } + trySetTerrain(terrain: TerrainType, viaMove: boolean, ignoreAnim: boolean = false): boolean { + if (this.terrain?.terrainType === (terrain || undefined)) + return false; + + const oldTerrainType = this.terrain?.terrainType || TerrainType.NONE; + + this.terrain = terrain ? new Terrain(terrain, viaMove ? 5 : 0) : null; + + if (this.terrain) { + if (!ignoreAnim) + this.scene.unshiftPhase(new CommonAnimPhase(this.scene, undefined, undefined, CommonAnim.MISTY_TERRAIN + (terrain - 1))); + this.scene.queueMessage(getTerrainStartMessage(terrain)); + } else + this.scene.queueMessage(getTerrainClearMessage(oldTerrainType)); + /*[ + this.scene.arenaBg, + this.scene.arenaBgTransition, + this.scene.arenaPlayer.base, + this.scene.arenaPlayer.props, + this.scene.arenaPlayerTransition.base, + this.scene.arenaPlayerTransition.props, + this.scene.arenaEnemy.base, + this.scene.arenaEnemy.props, + this.scene.arenaNextEnemy.base, + this.scene.arenaNextEnemy.props + ] + .flat() + .map(a => a.pipelineData['terrainColor'] = getTerrainColor());*/ + + return true; + } + isMoveWeatherCancelled(move: Move) { return this.weather && !this.weather.isEffectSuppressed(this.scene) && this.weather.isMoveWeatherCancelled(move); } - getAttackTypeMultiplier(attackType: Type): number { - if (!this.weather || this.weather.isEffectSuppressed(this.scene)) - return 1; + getAttackTypeMultiplier(attackType: Type, grounded: boolean): number { + let weatherMultiplier = 1; + if (this.weather && !this.weather.isEffectSuppressed(this.scene)) + weatherMultiplier = this.weather.getAttackTypeMultiplier(attackType); - return this.weather.getAttackTypeMultiplier(attackType); + let terrainMultiplier = 1; + if (this.terrain && !grounded) + terrainMultiplier = this.terrain.getAttackTypeMultiplier(attackType); + + return weatherMultiplier * terrainMultiplier; } getTrainerChance(): integer { @@ -572,12 +621,12 @@ export class ArenaBase extends Phaser.GameObjects.Container { this.player = player; - this.base = scene.addFieldSprite(0, 0, 'plains_a'); + this.base = scene.addFieldSprite(0, 0, 'plains_a', null, 1); this.base.setOrigin(0, 0); this.props = !player ? new Array(3).fill(null).map(() => { - const ret = scene.addFieldSprite(0, 0, 'plains_b'); + const ret = scene.addFieldSprite(0, 0, 'plains_b', null, 1); ret.setOrigin(0, 0); ret.setVisible(false); return ret; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index a248dcf75d3..a62011b7d68 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2,7 +2,7 @@ import Phaser from 'phaser'; import BattleScene, { AnySound } from '../battle-scene'; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from '../ui/battle-info'; import { Moves } from "../data/enums/moves"; -import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariablePowerAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, MultiHitAttr, StatusMoveTypeImmunityAttr } from "../data/move"; +import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariablePowerAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, MultiHitAttr, StatusMoveTypeImmunityAttr, MoveTarget } from "../data/move"; import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies } from '../data/pokemon-species'; import * as Utils from '../utils'; import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from '../data/type'; @@ -15,7 +15,7 @@ import { initMoveAnim, loadMoveAnimAssets } from '../data/battle-anims'; import { Status, StatusEffect } from '../data/status-effect'; import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition } from '../data/pokemon-evolutions'; import { reverseCompatibleTms, tmSpecies } from '../data/tms'; -import { DamagePhase, FaintPhase, LearnMovePhase, StatChangePhase, SwitchSummonPhase } from '../phases'; +import { DamagePhase, FaintPhase, LearnMovePhase, ObtainStatusEffectPhase, StatChangePhase, SwitchSummonPhase } from '../phases'; import { BattleStat } from '../data/battle-stat'; import { BattlerTag, BattlerTagLapseType, EncoreTag, TypeBoostTag, getBattlerTag } from '../data/battler-tags'; import { BattlerTagType } from "../data/enums/battler-tag-type"; @@ -39,6 +39,7 @@ import { DexAttr, StarterMoveset } from '../system/game-data'; import { QuantizerCelebi, argbFromRgba, rgbaFromArgb } from '@material/material-color-utilities'; import { Nature, getNatureStatMultiplier } from '../data/nature'; import { SpeciesFormChange, SpeciesFormChangeActiveTrigger, SpeciesFormChangeMoveLearnedTrigger, SpeciesFormChangeMoveUsedTrigger, SpeciesFormChangeStatusEffectTrigger } from '../data/pokemon-forms'; +import { TerrainType } from '../data/terrain'; export enum FieldPosition { CENTER, @@ -664,8 +665,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return types; } - isOfType(type: Type) { - return this.getTypes(true).indexOf(type) > -1; + isOfType(type: Type): boolean { + return !!this.getTypes(true).find(t => t === type); } getAbility(ignoreOverride?: boolean): Ability { @@ -708,6 +709,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this.getTeraType() !== Type.UNKNOWN; } + isGrounded(): boolean { + return !this.isOfType(Type.FLYING); + } + getAttackMoveEffectiveness(source: Pokemon, move: PokemonMove): TypeDamageMultiplier { const typeless = !!move.getMove().getAttrs(TypelessAttr).length; const typeMultiplier = new Utils.NumberHolder(this.getAttackTypeEffectiveness(move.getMove().type)); @@ -1042,7 +1047,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { else { if (source.findTag(t => t instanceof TypeBoostTag && (t as TypeBoostTag).boostedType === move.type)) power.value *= 1.5; - const weatherTypeMultiplier = this.scene.arena.getAttackTypeMultiplier(move.type); + const arenaAttackTypeMultiplier = this.scene.arena.getAttackTypeMultiplier(move.type, this.isGrounded()); + if (this.scene.arena.terrain?.terrainType === TerrainType.GRASSY && this.isGrounded() && move.type === Type.GROUND && move.moveTarget === MoveTarget.ALL_NEAR_OTHERS) + power.value /= 2; applyMoveAttrs(VariablePowerAttr, source, this, move, power); if (!typeless) { this.scene.arena.applyTags(WeakenMoveTypeTag, move.type, power); @@ -1065,7 +1072,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const sourceAtk = source.getBattleStat(isPhysical ? Stat.ATK : Stat.SPATK, this); const targetDef = this.getBattleStat(isPhysical ? Stat.DEF : Stat.SPDEF, source); const criticalMultiplier = isCritical ? 2 : 1; - const isTypeImmune = (typeMultiplier.value * weatherTypeMultiplier) === 0; + const isTypeImmune = (typeMultiplier.value * arenaAttackTypeMultiplier) === 0; const sourceTypes = source.getTypes(); const matchesSourceType = sourceTypes[0] === move.type || (sourceTypes.length > 1 && sourceTypes[1] === move.type); let stabMultiplier = new Utils.NumberHolder(1); @@ -1080,7 +1087,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { stabMultiplier.value = Math.min(stabMultiplier.value + 0.5, 2.25); if (!isTypeImmune) { - damage.value = Math.ceil(((((2 * source.level / 5 + 2) * power.value * sourceAtk / targetDef) / 50) + 2) * stabMultiplier.value * typeMultiplier.value * weatherTypeMultiplier * ((this.scene.currentBattle.randSeedInt(15) + 85) / 100)) * criticalMultiplier; + damage.value = Math.ceil(((((2 * source.level / 5 + 2) * power.value * sourceAtk / targetDef) / 50) + 2) * stabMultiplier.value * typeMultiplier.value * arenaAttackTypeMultiplier * ((this.scene.currentBattle.randSeedInt(15) + 85) / 100)) * criticalMultiplier; if (isPhysical && source.status && source.status.effect === StatusEffect.BURN) damage.value = Math.floor(damage.value / 2); move.getAttrs(HitsTagAttr).map(hta => hta as HitsTagAttr).filter(hta => hta.doubleDamage).forEach(hta => { @@ -1089,6 +1096,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { }); } + if (this.scene.arena.terrain?.terrainType === TerrainType.MISTY && this.isGrounded() && move.type === Type.DRAGON) + damage.value = Math.floor(damage.value / 2); + const fixedDamage = new Utils.IntegerHolder(0); applyMoveAttrs(FixedDamageAttr, source, this, move, fixedDamage); if (!isTypeImmune && fixedDamage.value) { @@ -1528,9 +1538,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this.gender !== Gender.GENDERLESS && pokemon.gender === (this.gender === Gender.MALE ? Gender.FEMALE : Gender.MALE); } - trySetStatus(effect: StatusEffect): boolean { - if (this.status && effect !== StatusEffect.FAINT) + canSetStatus(effect: StatusEffect, quiet: boolean = false): boolean { + if (effect !== StatusEffect.FAINT && this.status) return false; + if (this.isGrounded() && this.scene.arena.terrain?.terrainType === TerrainType.MISTY) + return false; + switch (effect) { case StatusEffect.POISON: case StatusEffect.TOXIC: @@ -1541,6 +1554,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (this.isOfType(Type.ELECTRIC)) return false; break; + case StatusEffect.SLEEP: + if (this.isGrounded() && this.scene.arena.terrain?.terrainType === TerrainType.ELECTRIC) + return false; case StatusEffect.FREEZE: if (this.isOfType(Type.ICE)) return false; @@ -1552,21 +1568,33 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } const cancelled = new Utils.BooleanHolder(false); - applyPreSetStatusAbAttrs(StatusEffectImmunityAbAttr, this, effect, cancelled); + applyPreSetStatusAbAttrs(StatusEffectImmunityAbAttr, this, effect, cancelled, quiet); if (cancelled.value) return false; - let cureTurn: Utils.IntegerHolder; + return true; + } + + trySetStatus(effect: StatusEffect, asPhase: boolean = false, cureTurn: integer = 0, sourceText: string = null): boolean { + if (!this.canSetStatus(effect, asPhase)) + return false; + + if (asPhase) { + this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, cureTurn, sourceText)); + return true; + } + + let statusCureTurn: Utils.IntegerHolder; if (effect === StatusEffect.SLEEP) { - cureTurn = new Utils.IntegerHolder(this.randSeedIntRange(2, 4)); - applyAbAttrs(ReduceStatusEffectDurationAbAttr, this, null, effect, cureTurn); + statusCureTurn = new Utils.IntegerHolder(this.randSeedIntRange(2, 4)); + applyAbAttrs(ReduceStatusEffectDurationAbAttr, this, null, effect, statusCureTurn); this.setFrameRate(4); } - this.status = new Status(effect, 0, cureTurn?.value); + this.status = new Status(effect, 0, statusCureTurn?.value); if (effect !== StatusEffect.FAINT) this.scene.triggerPokemonFormChange(this, SpeciesFormChangeStatusEffectTrigger, true); diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index a14df487971..5bed79ab9aa 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -1,5 +1,5 @@ import * as ModifierTypes from './modifier-type'; -import { LearnMovePhase, LevelUpPhase, ObtainStatusEffectPhase, PokemonHealPhase } from "../phases"; +import { LearnMovePhase, LevelUpPhase, PokemonHealPhase } from "../phases"; import BattleScene from "../battle-scene"; import { getLevelTotalExp } from "../data/exp"; import { PokeballType } from "../data/pokeball"; @@ -13,12 +13,11 @@ import { getPokemonMessage } from '../messages'; import * as Utils from "../utils"; import { TempBattleStat } from '../data/temp-battle-stat'; import { BerryType, getBerryEffectFunc, getBerryPredicate } from '../data/berry'; -import { StatusEffect, getStatusEffectDescriptor, getStatusEffectHealText } from '../data/status-effect'; +import { StatusEffect, getStatusEffectHealText } from '../data/status-effect'; import { MoneyAchv, achvs } from '../system/achv'; import { VoucherType } from '../system/voucher'; import { PreventBerryUseAbAttr, applyAbAttrs } from '../data/ability'; import { FormChangeItem, SpeciesFormChangeItemTrigger } from '../data/pokemon-forms'; -import { ModifierTier } from './modifier-tier'; type ModifierType = ModifierTypes.ModifierType; export type ModifierPredicate = (modifier: Modifier) => boolean; @@ -1924,10 +1923,8 @@ export class EnemyAttackStatusEffectChanceModifier extends EnemyPersistentModifi apply(args: any[]): boolean { const target = (args[0] as Pokemon); - if (Phaser.Math.RND.realInRange(0, 1) < (this.chance * this.getStackCount())) { - target.scene.unshiftPhase(new ObtainStatusEffectPhase(target.scene, target.getBattlerIndex(), this.effect)); - return true; - } + if (Phaser.Math.RND.realInRange(0, 1) < (this.chance * this.getStackCount())) + return target.trySetStatus(this.effect, true); return false; } diff --git a/src/phases.ts b/src/phases.ts index f2186c4adfe..4d800231ff3 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -1744,6 +1744,11 @@ export class TurnEndPhase extends FieldPhase { this.scene.applyModifier(EnemyStatusEffectHealChanceModifier, false, pokemon); } + if (this.scene.arena.terrain && pokemon.isGrounded()) { + this.scene.unshiftPhase(new PokemonHealPhase(this.scene, pokemon.getBattlerIndex(), + Math.max(pokemon.getMaxHp() >> 4, 1), getPokemonMessage(pokemon, ' regained\nhealth from the Grassy Terrain!'), true)); + } + applyPostTurnAbAttrs(PostTurnAbAttr, pokemon); this.scene.applyModifiers(TurnHeldItemTransferModifier, pokemon.isPlayer(), pokemon); diff --git a/src/pipelines/field-sprite.ts b/src/pipelines/field-sprite.ts index 5fdf8f438c6..822bbd733ae 100644 --- a/src/pipelines/field-sprite.ts +++ b/src/pipelines/field-sprite.ts @@ -1,4 +1,5 @@ import BattleScene from "../battle-scene"; +import { TerrainType, getTerrainColor } from "../data/terrain"; import * as Utils from "../utils"; const spriteFragShader = ` @@ -21,6 +22,8 @@ uniform int isOutside; uniform vec3 dayTint; uniform vec3 duskTint; uniform vec3 nightTint; +uniform vec3 terrainColor; +uniform float terrainColorRatio; float blendOverlay(float base, float blend) { return base<0.5?(2.0*base*blend):(1.0-2.0*(1.0-base)*(1.0-blend)); @@ -34,6 +37,89 @@ vec3 blendHardLight(vec3 base, vec3 blend) { return blendOverlay(blend, base); } +float hue2rgb(float f1, float f2, float hue) { + if (hue < 0.0) + hue += 1.0; + else if (hue > 1.0) + hue -= 1.0; + float res; + if ((6.0 * hue) < 1.0) + res = f1 + (f2 - f1) * 6.0 * hue; + else if ((2.0 * hue) < 1.0) + res = f2; + else if ((3.0 * hue) < 2.0) + res = f1 + (f2 - f1) * ((2.0 / 3.0) - hue) * 6.0; + else + res = f1; + return res; +} + +vec3 rgb2hsl(vec3 color) { + vec3 hsl; + + float fmin = min(min(color.r, color.g), color.b); + float fmax = max(max(color.r, color.g), color.b); + float delta = fmax - fmin; + + hsl.z = (fmax + fmin) / 2.0; + + if (delta == 0.0) { + hsl.x = 0.0; + hsl.y = 0.0; + } else { + if (hsl.z < 0.5) + hsl.y = delta / (fmax + fmin); + else + hsl.y = delta / (2.0 - fmax - fmin); + + float deltaR = (((fmax - color.r) / 6.0) + (delta / 2.0)) / delta; + float deltaG = (((fmax - color.g) / 6.0) + (delta / 2.0)) / delta; + float deltaB = (((fmax - color.b) / 6.0) + (delta / 2.0)) / delta; + + if (color.r == fmax ) + hsl.x = deltaB - deltaG; + else if (color.g == fmax) + hsl.x = (1.0 / 3.0) + deltaR - deltaB; + else if (color.b == fmax) + hsl.x = (2.0 / 3.0) + deltaG - deltaR; + + if (hsl.x < 0.0) + hsl.x += 1.0; + else if (hsl.x > 1.0) + hsl.x -= 1.0; + } + + return hsl; +} + +vec3 hsl2rgb(vec3 hsl) { + vec3 rgb; + + if (hsl.y == 0.0) + rgb = vec3(hsl.z); + else { + float f2; + + if (hsl.z < 0.5) + f2 = hsl.z * (1.0 + hsl.y); + else + f2 = (hsl.z + hsl.y) - (hsl.y * hsl.z); + + float f1 = 2.0 * hsl.z - f2; + + rgb.r = hue2rgb(f1, f2, hsl.x + (1.0/3.0)); + rgb.g = hue2rgb(f1, f2, hsl.x); + rgb.b= hue2rgb(f1, f2, hsl.x - (1.0/3.0)); + } + + return rgb; +} + +vec3 blendHue(vec3 base, vec3 blend) { + vec3 baseHSL = rgb2hsl(base); + return hsl2rgb(vec3(rgb2hsl(blend).r, baseHSL.g, baseHSL.b)); +} + void main() { vec4 texture; @@ -77,6 +163,12 @@ void main() { color = vec4(blendHardLight(color.rgb, dayNightTint), color.a); } + if (terrainColorRatio > 0.0 && 1.0 - terrainColorRatio < outTexCoord.y) { + if (color.a > 0.0 && terrainColor.r > 0.0 && terrainColor.g > 0.0 && terrainColor.b > 0.0) { + color.rgb = mix(color.rgb, blendHue(color.rgb, terrainColor), 1.0); + } + } + gl_FragColor = color; } `; @@ -127,6 +219,8 @@ export default class FieldSpritePipeline extends Phaser.Renderer.WebGL.Pipelines onPreRender(): void { this.set1f('time', 0); this.set1i('ignoreTimeTint', 0); + this.set1f('terrainColorRatio', 0); + this.set3fv('terrainColor', [ 0, 0, 0 ]); } onBind(gameObject: Phaser.GameObjects.GameObject): void { @@ -137,6 +231,7 @@ export default class FieldSpritePipeline extends Phaser.Renderer.WebGL.Pipelines const data = sprite.pipelineData; const ignoreTimeTint = data['ignoreTimeTint'] as boolean; + const terrainColorRatio = data['terrainColorRatio'] as number || 0; let time = scene.currentBattle?.waveIndex ? ((scene.currentBattle.waveIndex + scene.getWaveCycleOffset()) % 40) / 40 // ((new Date().getSeconds() * 1000 + new Date().getMilliseconds()) % 10000) / 10000 @@ -147,6 +242,8 @@ export default class FieldSpritePipeline extends Phaser.Renderer.WebGL.Pipelines this.set3fv('dayTint', scene.arena.getDayTint().map(c => c / 255)); this.set3fv('duskTint', scene.arena.getDuskTint().map(c => c / 255)); this.set3fv('nightTint', scene.arena.getNightTint().map(c => c / 255)); + this.set3fv('terrainColor', getTerrainColor(scene.arena.terrain?.terrainType || TerrainType.NONE).map(c => c / 255)); + this.set1f('terrainColorRatio', terrainColorRatio); } onBatch(gameObject: Phaser.GameObjects.GameObject): void {