Compare commits

...

9 Commits

Author SHA1 Message Date
flx-sta
7f4e3f6553
Merge branch 'beta' into bug/fix-762 2024-09-04 21:58:14 -07:00
innerthunder
31d3bec55e
[Test] Fix Rage Powder test failing randomly (#4038)
* Fix random failure in Rage Powder test

* Remove redundant override
2024-09-05 04:54:51 +00:00
Tempoanon
834255447d
Fix mbh not using user's nickname (#4033) 2024-09-05 04:02:45 +00:00
flx-sta
19e179d96c
Merge branch 'beta' into bug/fix-762 2024-09-04 20:49:21 -07:00
flx-sta
aadc0a8576 update isBetween docs. Remove .js imports 2024-09-04 20:30:46 -07:00
innerthunder
f3ced7e814
[Bug] Fix inconsistencies with move type resolution for some ability triggers (#3988)
* Fix inconsistencies with ability triggers on variable-type moves

* Fix aura effects not accounting for the move user

* Fix Wonder Guard evaluating move type as if the defender used the move

* Some additional test coverage for move-type-changing effects
2024-09-05 00:49:01 +01:00
Mumble
cebd449335
I have a brain made of cheese!!! (#4028)
Co-authored-by: frutescens <info@laptop>
2024-09-04 23:16:47 +00:00
chaosgrimmon
8835ae0299
[Sprite] Index egg skip UI (#4027)
* [Sprite] Index egg skip UI

* [Sprite] Index egg skip legacy UI
2024-09-04 20:46:33 +00:00
NightKev
a537113c8f
Re-add lost i18n strings (#4024) 2024-09-04 20:45:56 +00:00
16 changed files with 98 additions and 40 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 B

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 B

After

Width:  |  Height:  |  Size: 179 B

View File

@ -2755,12 +2755,18 @@ export default class BattleScene extends SceneBase {
keys.push("pkmn__" + p.species.getSpriteId(p.gender === Gender.FEMALE, p.species.formIndex, p.shiny, p.variant));
keys.push("pkmn__" + p.species.getSpriteId(p.gender === Gender.FEMALE, p.species.formIndex, p.shiny, p.variant, true));
keys.push("cry/" + p.species.getCryKey(p.species.formIndex));
if (p.fusionSpecies && p.getSpeciesForm() !== p.getFusionSpeciesForm()) {
keys.push("cry/"+p.getFusionSpeciesForm().getCryKey(p.fusionSpecies.formIndex));
}
});
// enemyParty has to be operated on separately from playerParty because playerPokemon =/= enemyPokemon
const enemyParty = this.getEnemyParty();
enemyParty.forEach(p => {
keys.push(p.species.getSpriteKey(p.gender === Gender.FEMALE, p.species.formIndex, p.shiny, p.variant));
keys.push("cry/" + p.species.getCryKey(p.species.formIndex));
if (p.fusionSpecies && p.getSpeciesForm() !== p.getFusionSpeciesForm()) {
keys.push("cry/"+p.getFusionSpeciesForm().getCryKey(p.fusionSpecies.formIndex));
}
});
return keys;
}

View File

@ -310,7 +310,7 @@ export class ReceivedMoveDamageMultiplierAbAttr extends PreDefendAbAttr {
export class ReceivedTypeDamageMultiplierAbAttr extends ReceivedMoveDamageMultiplierAbAttr {
constructor(moveType: Type, damageMultiplier: number) {
super((user, target, move) => move.type === moveType, damageMultiplier);
super((target, user, move) => user.getMoveType(move) === moveType, damageMultiplier);
}
}
@ -455,7 +455,7 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr {
}
applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean {
if (move instanceof AttackMove && pokemon.getAttackTypeEffectiveness(pokemon.getMoveType(move), attacker) < 2) {
if (move instanceof AttackMove && pokemon.getAttackTypeEffectiveness(attacker.getMoveType(move), attacker) < 2) {
cancelled.value = true; // Suppresses "No Effect" message
(args[0] as Utils.NumberHolder).value = 0;
return true;
@ -1462,7 +1462,7 @@ export class MovePowerBoostAbAttr extends VariableMovePowerAbAttr {
export class MoveTypePowerBoostAbAttr extends MovePowerBoostAbAttr {
constructor(boostedType: Type, powerMultiplier?: number) {
super((pokemon, defender, move) => move.type === boostedType, powerMultiplier || 1.5);
super((pokemon, defender, move) => pokemon?.getMoveType(move) === boostedType, powerMultiplier || 1.5);
}
}
@ -1546,7 +1546,7 @@ export class PreAttackFieldMoveTypePowerBoostAbAttr extends FieldMovePowerBoostA
* @param powerMultiplier - The multiplier to apply to the move's power, defaults to 1.5 if not provided.
*/
constructor(boostedType: Type, powerMultiplier?: number) {
super((pokemon, defender, move) => move.type === boostedType, powerMultiplier || 1.5);
super((pokemon, defender, move) => pokemon?.getMoveType(move) === boostedType, powerMultiplier || 1.5);
}
}
@ -5100,9 +5100,9 @@ export function initAbilities() {
.ignorable(),
new Ability(Abilities.TINTED_LENS, 4)
//@ts-ignore
.attr(DamageBoostAbAttr, 2, (user, target, move) => target.getAttackTypeEffectiveness(move.type, user) <= 0.5), // TODO: fix TS issues
.attr(DamageBoostAbAttr, 2, (user, target, move) => target?.getMoveEffectiveness(user, move) <= 0.5), // TODO: fix TS issues
new Ability(Abilities.FILTER, 4)
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2, 0.75)
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => target.getMoveEffectiveness(user, move) >= 2, 0.75)
.ignorable(),
new Ability(Abilities.SLOW_START, 4)
.attr(PostSummonAddBattlerTagAbAttr, BattlerTagType.SLOW_START, 5),
@ -5118,7 +5118,7 @@ export function initAbilities() {
.attr(PostWeatherLapseHealAbAttr, 1, WeatherType.HAIL, WeatherType.SNOW)
.partial(), // Healing not blocked by Heal Block
new Ability(Abilities.SOLID_ROCK, 4)
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2, 0.75)
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => target.getMoveEffectiveness(user, move) >= 2, 0.75)
.ignorable(),
new Ability(Abilities.SNOW_WARNING, 4)
.attr(PostSummonWeatherChangeAbAttr, WeatherType.SNOW)
@ -5236,10 +5236,13 @@ export function initAbilities() {
new Ability(Abilities.MOXIE, 5)
.attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1),
new Ability(Abilities.JUSTIFIED, 5)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.type === Type.DARK && move.category !== MoveCategory.STATUS, Stat.ATK, 1),
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === Type.DARK && move.category !== MoveCategory.STATUS, Stat.ATK, 1),
new Ability(Abilities.RATTLED, 5)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS && (move.type === Type.DARK || move.type === Type.BUG ||
move.type === Type.GHOST), Stat.SPD, 1)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => {
const moveType = user.getMoveType(move);
return move.category !== MoveCategory.STATUS
&& (moveType === Type.DARK || moveType === Type.BUG || moveType === Type.GHOST);
}, Stat.SPD, 1)
.attr(PostIntimidateStatStageChangeAbAttr, [Stat.SPD], 1),
new Ability(Abilities.MAGIC_BOUNCE, 5)
.ignorable()
@ -5313,7 +5316,7 @@ export function initAbilities() {
.attr(UnsuppressableAbilityAbAttr)
.attr(NoFusionAbilityAbAttr),
new Ability(Abilities.GALE_WINGS, 6)
.attr(ChangeMovePriorityAbAttr, (pokemon, move) => pokemon.isFullHp() && move.type === Type.FLYING, 1),
.attr(ChangeMovePriorityAbAttr, (pokemon, move) => pokemon.isFullHp() && pokemon.getMoveType(move) === Type.FLYING, 1),
new Ability(Abilities.MEGA_LAUNCHER, 6)
.attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.PULSE_MOVE), 1.5),
new Ability(Abilities.GRASS_PELT, 6)
@ -5368,7 +5371,7 @@ export function initAbilities() {
.condition(getSheerForceHitDisableAbCondition())
.unimplemented(),
new Ability(Abilities.WATER_COMPACTION, 7)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.type === Type.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2),
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === Type.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2),
new Ability(Abilities.MERCILESS, 7)
.attr(ConditionalCritAbAttr, (user, target, move) => target?.status?.effect === StatusEffect.TOXIC || target?.status?.effect === StatusEffect.POISON),
new Ability(Abilities.SHIELDS_DOWN, 7)
@ -5424,7 +5427,7 @@ export function initAbilities() {
.attr(NoFusionAbilityAbAttr)
// Add BattlerTagType.DISGUISE if the pokemon is in its disguised form
.conditionalAttr(pokemon => pokemon.formIndex === 0, PostSummonAddBattlerTagAbAttr, BattlerTagType.DISGUISE, 0, false)
.attr(FormBlockDamageAbAttr, (target, user, move) => !!target.getTag(BattlerTagType.DISGUISE) && target.getAttackTypeEffectiveness(move.type, user) > 0, 0, BattlerTagType.DISGUISE,
.attr(FormBlockDamageAbAttr, (target, user, move) => !!target.getTag(BattlerTagType.DISGUISE) && target.getMoveEffectiveness(user, move) > 0, 0, BattlerTagType.DISGUISE,
(pokemon, abilityName) => i18next.t("abilityTriggers:disguiseAvoidedDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName: abilityName }),
(pokemon) => Utils.toDmgValue(pokemon.getMaxHp() / 8))
.attr(PostBattleInitFormChangeAbAttr, () => 0)
@ -5469,7 +5472,7 @@ export function initAbilities() {
.attr(AllyMoveCategoryPowerBoostAbAttr, [MoveCategory.SPECIAL], 1.3),
new Ability(Abilities.FLUFFY, 7)
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT), 0.5)
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => move.type === Type.FIRE, 2)
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => user.getMoveType(move) === Type.FIRE, 2)
.ignorable(),
new Ability(Abilities.DAZZLING, 7)
.attr(FieldPriorityMoveImmunityAbAttr)
@ -5519,10 +5522,10 @@ export function initAbilities() {
new Ability(Abilities.SHADOW_SHIELD, 7)
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => target.isFullHp(), 0.5),
new Ability(Abilities.PRISM_ARMOR, 7)
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2, 0.75),
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => target.getMoveEffectiveness(user, move) >= 2, 0.75),
new Ability(Abilities.NEUROFORCE, 7)
//@ts-ignore
.attr(MovePowerBoostAbAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2, 1.25), // TODO: fix TS issues
.attr(MovePowerBoostAbAttr, (user, target, move) => target?.getMoveEffectiveness(user, move) >= 2, 1.25), // TODO: fix TS issues
new Ability(Abilities.INTREPID_SWORD, 8)
.attr(PostSummonStatStageChangeAbAttr, [ Stat.ATK ], 1, true)
.condition(getOncePerBattleCondition(Abilities.INTREPID_SWORD)),
@ -5553,7 +5556,11 @@ export function initAbilities() {
new Ability(Abilities.STALWART, 8)
.attr(BlockRedirectAbAttr),
new Ability(Abilities.STEAM_ENGINE, 8)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => (move.type === Type.FIRE || move.type === Type.WATER) && move.category !== MoveCategory.STATUS, Stat.SPD, 6),
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => {
const moveType = user.getMoveType(move);
return move.category !== MoveCategory.STATUS
&& (moveType === Type.FIRE || moveType === Type.WATER);
}, Stat.SPD, 6),
new Ability(Abilities.PUNK_ROCK, 8)
.attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.SOUND_BASED), 1.3)
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => move.hasFlag(MoveFlags.SOUND_BASED), 0.5)
@ -5653,7 +5660,7 @@ export function initAbilities() {
new Ability(Abilities.SEED_SOWER, 9)
.attr(PostDefendTerrainChangeAbAttr, TerrainType.GRASSY),
new Ability(Abilities.THERMAL_EXCHANGE, 9)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.type === Type.FIRE && move.category !== MoveCategory.STATUS, Stat.ATK, 1)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === Type.FIRE && move.category !== MoveCategory.STATUS, Stat.ATK, 1)
.attr(StatusEffectImmunityAbAttr, StatusEffect.BURN)
.ignorable(),
new Ability(Abilities.ANGER_SHELL, 9)

View File

@ -761,8 +761,7 @@ export default class Move implements Localizable {
.flat(),
);
for (const aura of fieldAuras) {
// The only relevant values are `move` and the `power` holder
aura.applyPreAttack(null, null, simulated, null, this, [power]);
aura.applyPreAttack(source, null, simulated, target, this, [power]);
}
const alliedField: Pokemon[] = source instanceof PlayerPokemon ? source.scene.getPlayerField() : source.scene.getEnemyField();

View File

@ -39,5 +39,6 @@
"matBlock": "Mat Block",
"craftyShield": "Crafty Shield",
"tailwind": "Tailwind",
"happyHour": "Happy Hour"
}
"happyHour": "Happy Hour",
"safeguard": "Safeguard"
}

View File

@ -47,5 +47,11 @@
"tailwindOnRemovePlayer": "Your team's Tailwind petered out!",
"tailwindOnRemoveEnemy": "The opposing team's Tailwind petered out!",
"happyHourOnAdd": "Everyone is caught up in the happy atmosphere!",
"happyHourOnRemove": "The atmosphere returned to normal."
"happyHourOnRemove": "The atmosphere returned to normal.",
"safeguardOnAdd": "The whole field is cloaked in a mystical veil!",
"safeguardOnAddPlayer": "Your team cloaked itself in a mystical veil!",
"safeguardOnAddEnemy": "The opposing team cloaked itself in a mystical veil!",
"safeguardOnRemove": "The field is no longer protected by Safeguard!",
"safeguardOnRemovePlayer": "Your team is no longer protected by Safeguard!",
"safeguardOnRemoveEnemy": "The opposing team is no longer protected by Safeguard!"
}

View File

@ -65,5 +65,6 @@
"suppressAbilities": "{{pokemonName}}'s ability\nwas suppressed!",
"revivalBlessing": "{{pokemonName}} was revived!",
"swapArenaTags": "{{pokemonName}} swapped the battle effects affecting each side of the field!",
"exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!"
}
"exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!",
"safeguard": "{{targetName}} is protected by Safeguard!"
}

View File

@ -2488,7 +2488,7 @@ export class TurnHeldItemTransferModifier extends HeldItemTransferModifier {
}
getTransferMessage(pokemon: Pokemon, targetPokemon: Pokemon, item: ModifierTypes.ModifierType): string {
return i18next.t("modifier:turnHeldItemTransferApply", { pokemonNameWithAffix: getPokemonNameWithAffix(targetPokemon), itemName: item.name, pokemonName: pokemon.name, typeName: this.type.name });
return i18next.t("modifier:turnHeldItemTransferApply", { pokemonNameWithAffix: getPokemonNameWithAffix(targetPokemon), itemName: item.name, pokemonName: pokemon.getNameToRender(), typeName: this.type.name });
}
getMaxHeldItemCount(pokemon: Pokemon): integer {

View File

@ -1,4 +1,5 @@
import { toDmgValue } from "#app/utils";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { StatusEffect } from "#app/data/status-effect";
@ -205,4 +206,22 @@ describe("Abilities - Disguise", () => {
expect(game.scene.getCurrentPhase()?.constructor.name).toBe("CommandPhase");
expect(game.scene.currentBattle.waveIndex).toBe(2);
}, TIMEOUT);
it("activates when Aerilate circumvents immunity to the move's base type", async () => {
game.override.ability(Abilities.AERILATE);
game.override.moveset([Moves.TACKLE]);
await game.classicMode.startBattle();
const mimikyu = game.scene.getEnemyPokemon()!;
const maxHp = mimikyu.getMaxHp();
const disguiseDamage = toDmgValue(maxHp / 8);
game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to("MoveEndPhase");
expect(mimikyu.formIndex).toBe(bustedForm);
expect(mimikyu.hp).toBe(maxHp - disguiseDamage);
}, TIMEOUT);
});

View File

@ -1,7 +1,6 @@
import { allAbilities } from "#app/data/ability";
import { allMoves } from "#app/data/move";
import { Abilities } from "#app/enums/abilities";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
@ -37,7 +36,7 @@ describe("Abilities - Steely Spirit", () => {
});
it("increases Steel-type moves' power used by the user and its allies by 50%", async () => {
await game.startBattle([Species.PIKACHU, Species.SHUCKLE]);
await game.classicMode.startBattle([Species.PIKACHU, Species.SHUCKLE]);
const boostSource = game.scene.getPlayerField()[1];
const enemyToCheck = game.scene.getEnemyPokemon()!;
@ -47,13 +46,13 @@ describe("Abilities - Steely Spirit", () => {
game.move.select(moveToCheck, 0, enemyToCheck.getBattlerIndex());
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to(MoveEffectPhase);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(allMoves[moveToCheck].calculateBattlePower).toHaveReturnedWith(ironHeadPower * steelySpiritMultiplier);
});
it("stacks if multiple users with this ability are on the field.", async () => {
await game.startBattle([Species.PIKACHU, Species.PIKACHU]);
await game.classicMode.startBattle([Species.PIKACHU, Species.PIKACHU]);
const enemyToCheck = game.scene.getEnemyPokemon()!;
game.scene.getPlayerField().forEach(p => {
@ -64,13 +63,13 @@ describe("Abilities - Steely Spirit", () => {
game.move.select(moveToCheck, 0, enemyToCheck.getBattlerIndex());
game.move.select(moveToCheck, 1, enemyToCheck.getBattlerIndex());
await game.phaseInterceptor.to(MoveEffectPhase);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(allMoves[moveToCheck].calculateBattlePower).toHaveReturnedWith(ironHeadPower * Math.pow(steelySpiritMultiplier, 2));
});
it("does not take effect when suppressed", async () => {
await game.startBattle([Species.PIKACHU, Species.SHUCKLE]);
await game.classicMode.startBattle([Species.PIKACHU, Species.SHUCKLE]);
const boostSource = game.scene.getPlayerField()[1];
const enemyToCheck = game.scene.getEnemyPokemon()!;
@ -84,8 +83,25 @@ describe("Abilities - Steely Spirit", () => {
game.move.select(moveToCheck, 0, enemyToCheck.getBattlerIndex());
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to(MoveEffectPhase);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(allMoves[moveToCheck].calculateBattlePower).toHaveReturnedWith(ironHeadPower);
});
it("affects variable-type moves if their resolved type is Steel", async () => {
game.override
.ability(Abilities.STEELY_SPIRIT)
.moveset([Moves.REVELATION_DANCE]);
const revelationDance = allMoves[Moves.REVELATION_DANCE];
vi.spyOn(revelationDance, "calculateBattlePower");
await game.classicMode.startBattle([Species.KLINKLANG]);
game.move.select(Moves.REVELATION_DANCE);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(revelationDance.calculateBattlePower).toHaveReturnedWith(revelationDance.power * 1.5);
});
});

View File

@ -1,8 +1,8 @@
import { Species } from "#app/enums/species";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import GameManager from "../utils/gameManager";
import { PokeballType } from "#app/enums/pokeball.js";
import BattleScene from "#app/battle-scene.js";
import { PokeballType } from "#app/enums/pokeball";
import BattleScene from "#app/battle-scene";
describe("Spec - Pokemon", () => {
let phaserGame: Phaser.Game;

View File

@ -25,7 +25,6 @@ describe("Moves - Rage Powder", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override.battleType("double");
game.override.starterSpecies(Species.AMOONGUSS);
game.override.enemySpecies(Species.SNORLAX);
game.override.startingLevel(100);
game.override.enemyLevel(100);
@ -68,6 +67,10 @@ describe("Moves - Rage Powder", () => {
game.move.select(Moves.QUICK_ATTACK, 0, BattlerIndex.ENEMY);
game.move.select(Moves.QUICK_ATTACK, 1, BattlerIndex.ENEMY_2);
await game.forceEnemyMove(Moves.RAGE_POWDER);
await game.forceEnemyMove(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase", false);
// If redirection was bypassed, both enemies should be damaged

View File

@ -585,11 +585,11 @@ export function getLocalizedSpriteKey(baseKey: string) {
}
/**
* Check if a number is between two other numbers
* Check if a number is **inclusive** between two numbers
* @param num the number to check
* @param min the minimum value
* @param max the maximum value
* @returns true if number is between min and max
* @param min the minimum value (included)
* @param max the maximum value (included)
* @returns true if number is **inclusive** between min and max
*/
export function isBetween(num: number, min: number, max: number): boolean {
return num >= min && num <= max;