mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2024-12-02 11:46:11 +00:00
lost-at-sea encounter - further progress
This commit is contained in:
parent
7164b0afe4
commit
c9cbdd7d44
@ -1,21 +1,12 @@
|
|||||||
import { Type } from "#app/data/type.js";
|
import { Type } from "#app/data/type.js";
|
||||||
|
import { Moves } from "#app/enums/moves.js";
|
||||||
import { Species } from "#app/enums/species.js";
|
import { Species } from "#app/enums/species.js";
|
||||||
import { PlayerPokemon } from "#app/field/pokemon.js";
|
import { PlayerPokemon } from "#app/field/pokemon.js";
|
||||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||||
import BattleScene from "../../../battle-scene";
|
import BattleScene from "../../../battle-scene";
|
||||||
import MysteryEncounter, {
|
import MysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier } from "../mystery-encounter";
|
||||||
MysteryEncounterBuilder,
|
import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option";
|
||||||
MysteryEncounterTier,
|
import { applyDamageToPokemon, leaveEncounterWithoutBattle, setEncounterExp } from "../mystery-encounter-utils";
|
||||||
} from "../mystery-encounter";
|
|
||||||
import {
|
|
||||||
EncounterOptionMode,
|
|
||||||
MysteryEncounterOptionBuilder,
|
|
||||||
} from "../mystery-encounter-option";
|
|
||||||
import {
|
|
||||||
applyDamageToPokemon,
|
|
||||||
leaveEncounterWithoutBattle,
|
|
||||||
setEncounterExp,
|
|
||||||
} from "../mystery-encounter-utils";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Damage percentage taken when wandering aimlessly.
|
* Damage percentage taken when wandering aimlessly.
|
||||||
@ -26,7 +17,7 @@ const DAMAGE_PERCENTAGE: number = 30; // 0 - 100
|
|||||||
/** The i18n namespace for the encounter */
|
/** The i18n namespace for the encounter */
|
||||||
const namepsace = "mysteryEncounter:lostAtSea";
|
const namepsace = "mysteryEncounter:lostAtSea";
|
||||||
|
|
||||||
let waterPkm: PlayerPokemon;
|
let surfablePkm: PlayerPokemon;
|
||||||
let flyingPkm: PlayerPokemon;
|
let flyingPkm: PlayerPokemon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,8 +25,7 @@ let flyingPkm: PlayerPokemon;
|
|||||||
* @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/9 | GitHub Issue #9}
|
* @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/9 | GitHub Issue #9}
|
||||||
* @see For biome requirements check [mysteryEncountersByBiome](../mystery-encounters.ts)
|
* @see For biome requirements check [mysteryEncountersByBiome](../mystery-encounters.ts)
|
||||||
*/
|
*/
|
||||||
export const LostAtSeaEncounter: MysteryEncounter =
|
export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.LOST_AT_SEA)
|
||||||
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.LOST_AT_SEA)
|
|
||||||
.withEncounterTier(MysteryEncounterTier.COMMON)
|
.withEncounterTier(MysteryEncounterTier.COMMON)
|
||||||
.withSceneWaveRangeRequirement(11, 179)
|
.withSceneWaveRangeRequirement(11, 179)
|
||||||
.withIntroSpriteConfigs([
|
.withIntroSpriteConfigs([
|
||||||
@ -50,32 +40,20 @@ export const LostAtSeaEncounter: MysteryEncounter =
|
|||||||
alpha: 0.25,
|
alpha: 0.25,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
.withIntroDialogue([
|
.withIntroDialogue([{ text: `${namepsace}:intro` }])
|
||||||
{
|
|
||||||
text: `${namepsace}:intro`,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.withOnInit((scene: BattleScene) => {
|
.withOnInit((scene: BattleScene) => {
|
||||||
const allowedPokemon = scene
|
// const allowedPokemon = scene.getParty().filter((p) => p.isAllowedInBattle());
|
||||||
.getParty()
|
|
||||||
.filter((p) => p.isAllowedInBattle());
|
|
||||||
const { mysteryEncounter } = scene.currentBattle;
|
const { mysteryEncounter } = scene.currentBattle;
|
||||||
|
|
||||||
mysteryEncounter.setDialogueToken(
|
mysteryEncounter.setDialogueToken("damagePercentage", String(DAMAGE_PERCENTAGE));
|
||||||
"damagePercentage",
|
|
||||||
String(DAMAGE_PERCENTAGE)
|
|
||||||
);
|
|
||||||
|
|
||||||
// check for water pokemon
|
// check for water pokemon
|
||||||
waterPkm = findPokemonByType(allowedPokemon, Type.WATER);
|
// surfablePkm = findPokemonThatCanLearnMove(allowedPokemon, Type.WATER);
|
||||||
mysteryEncounter.setDialogueToken("waterPkm", waterPkm?.name ?? "<NONE>");
|
// mysteryEncounter.setDialogueToken("waterPkm", surfablePkm?.name ?? "");
|
||||||
|
|
||||||
// check for flying pokemon
|
// check for flying pokemon
|
||||||
flyingPkm = findPokemonByType(allowedPokemon, Type.FLYING);
|
// flyingPkm = findPokemonThatCanLearnMove(allowedPokemon, Type.FLYING);
|
||||||
mysteryEncounter.setDialogueToken(
|
// mysteryEncounter.setDialogueToken("flyingPkm", flyingPkm?.name ?? "");
|
||||||
"flyingPkm",
|
|
||||||
flyingPkm?.name ?? "<NONE>"
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
@ -88,7 +66,7 @@ export const LostAtSeaEncounter: MysteryEncounter =
|
|||||||
* Receives EXP similar to defeating a Lapras
|
* Receives EXP similar to defeating a Lapras
|
||||||
*/
|
*/
|
||||||
new MysteryEncounterOptionBuilder()
|
new MysteryEncounterOptionBuilder()
|
||||||
.withPokemonTypeRequirement(Type.WATER, true, 1)
|
.withPokemonCanLearnMoveRequirement(Moves.SURF)
|
||||||
.withOptionMode(EncounterOptionMode.DISABLED_OR_DEFAULT)
|
.withOptionMode(EncounterOptionMode.DISABLED_OR_DEFAULT)
|
||||||
.withDialogue({
|
.withDialogue({
|
||||||
buttonLabel: `${namepsace}:option:1:label`,
|
buttonLabel: `${namepsace}:option:1:label`,
|
||||||
@ -99,9 +77,7 @@ export const LostAtSeaEncounter: MysteryEncounter =
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.withOptionPhase(async (scene: BattleScene) =>
|
.withOptionPhase(async (scene: BattleScene) => handleGuidingOptionPhase(scene, surfablePkm))
|
||||||
handleGuidingOptionPhase(scene, waterPkm)
|
|
||||||
)
|
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.withOption(
|
.withOption(
|
||||||
@ -121,9 +97,7 @@ export const LostAtSeaEncounter: MysteryEncounter =
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.withOptionPhase(async (scene: BattleScene) =>
|
.withOptionPhase(async (scene: BattleScene) => handleGuidingOptionPhase(scene, flyingPkm))
|
||||||
handleGuidingOptionPhase(scene, flyingPkm)
|
|
||||||
)
|
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.withSimpleOption(
|
.withSimpleOption(
|
||||||
@ -140,9 +114,7 @@ export const LostAtSeaEncounter: MysteryEncounter =
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
async (scene: BattleScene) => {
|
async (scene: BattleScene) => {
|
||||||
const allowedPokemon = scene
|
const allowedPokemon = scene.getParty().filter((p) => p.isAllowedInBattle());
|
||||||
.getParty()
|
|
||||||
.filter((p) => p.isAllowedInBattle());
|
|
||||||
|
|
||||||
allowedPokemon.forEach((pkm) => {
|
allowedPokemon.forEach((pkm) => {
|
||||||
const percentage = DAMAGE_PERCENTAGE / 100;
|
const percentage = DAMAGE_PERCENTAGE / 100;
|
||||||
@ -150,41 +122,26 @@ export const LostAtSeaEncounter: MysteryEncounter =
|
|||||||
return applyDamageToPokemon(pkm, damage);
|
return applyDamageToPokemon(pkm, damage);
|
||||||
});
|
});
|
||||||
leaveEncounterWithoutBattle(scene);
|
leaveEncounterWithoutBattle(scene);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a pokemon inside the given party by a given type
|
|
||||||
*
|
|
||||||
* @param party player pokemon party
|
|
||||||
* @param type type to search for
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function findPokemonByType(party: PlayerPokemon[], type: Type) {
|
|
||||||
return party.find((p) => p.getTypes(true).includes(type));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic handler for using a guiding pokemon to guide you back.
|
* Generic handler for using a guiding pokemon to guide you back.
|
||||||
*
|
*
|
||||||
* @param scene Battle scene
|
* @param scene Battle scene
|
||||||
* @param guidePokemon pokemon choosen as a guide
|
* @param guidePokemon pokemon choosen as a guide
|
||||||
*/
|
*/
|
||||||
function handleGuidingOptionPhase(
|
function handleGuidingOptionPhase(scene: BattleScene, guidePokemon: PlayerPokemon) {
|
||||||
scene: BattleScene,
|
|
||||||
guidePokemon: PlayerPokemon
|
|
||||||
) {
|
|
||||||
/** Base EXP value for guiding pokemon. Currently Lapras base-value */
|
/** Base EXP value for guiding pokemon. Currently Lapras base-value */
|
||||||
const baseExpValue: number = 187;
|
const baseExpValue: number = 187;
|
||||||
|
|
||||||
if (guidePokemon) {
|
if (guidePokemon) {
|
||||||
setEncounterExp(scene, guidePokemon.id, baseExpValue, true);
|
setEncounterExp(scene, guidePokemon.id, baseExpValue, true);
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn("Lost at sea: No guide pokemon found but pokemon guides player. huh!?");
|
||||||
"Lost at sea: No guide pokemon found but pokemon guides player. huh!?"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
leaveEncounterWithoutBattle(scene);
|
leaveEncounterWithoutBattle(scene);
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { OptionTextDisplay } from "#app/data/mystery-encounters/mystery-encounter-dialogue";
|
import { OptionTextDisplay } from "#app/data/mystery-encounters/mystery-encounter-dialogue";
|
||||||
|
import { Moves } from "#app/enums/moves.js";
|
||||||
import { PlayerPokemon } from "#app/field/pokemon";
|
import { PlayerPokemon } from "#app/field/pokemon";
|
||||||
import BattleScene from "../../battle-scene";
|
import BattleScene from "../../battle-scene";
|
||||||
import * as Utils from "../../utils";
|
import * as Utils from "../../utils";
|
||||||
import { Type } from "../type";
|
import { Type } from "../type";
|
||||||
import { EncounterPokemonRequirement, EncounterSceneRequirement, MoneyRequirement, TypeRequirement } from "./mystery-encounter-requirements";
|
import { EncounterPokemonRequirement, EncounterSceneRequirement, MoneyRequirement, TypeRequirement } from "./mystery-encounter-requirements";
|
||||||
|
import { CanLearnMoveRequirement, CanlearnMoveRequirementOptions } from "./requirements/can-learn-move-requirement";
|
||||||
|
|
||||||
export enum EncounterOptionMode {
|
export enum EncounterOptionMode {
|
||||||
/** Default style */
|
/** Default style */
|
||||||
@ -194,8 +196,18 @@ export class MysteryEncounterOptionBuilder implements Partial<MysteryEncounterOp
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
withPokemonTypeRequirement(type: Type | Type[], excludeFainted?: boolean, minNumberOfPokemon?: number, invertQuery?: boolean) {
|
withPokemonTypeRequirement(type: Type | Type[], excludeFainted?: boolean, minNumberOfPokemon?: number, invertQuery?: boolean) {
|
||||||
const types = Array.isArray(type) ? type : [type];
|
return this.withPrimaryPokemonRequirement(new TypeRequirement(type, excludeFainted, minNumberOfPokemon, invertQuery));
|
||||||
return this.withPrimaryPokemonRequirement(new TypeRequirement(types, excludeFainted, minNumberOfPokemon, invertQuery));
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Player is required to have a pokemon that can learn a certain move/moveset
|
||||||
|
*
|
||||||
|
* @param move the required move/moves
|
||||||
|
* @param options see {@linkcode CanlearnMoveRequirementOptions}
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
withPokemonCanLearnMoveRequirement(move: Moves | Moves[], options?: CanlearnMoveRequirementOptions) {
|
||||||
|
return this.withPrimaryPokemonRequirement(new CanLearnMoveRequirement(move, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
withSecondaryPokemonRequirement(requirement: EncounterPokemonRequirement, excludePrimaryFromSecondaryRequirements?: boolean): this & Required<Pick<MysteryEncounterOption, "secondaryPokemonRequirements">> {
|
withSecondaryPokemonRequirement(requirement: EncounterPokemonRequirement, excludePrimaryFromSecondaryRequirements?: boolean): this & Required<Pick<MysteryEncounterOption, "secondaryPokemonRequirements">> {
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import IMysteryEncounter from "./mystery-encounter";
|
import { Biome } from "#enums/biome";
|
||||||
|
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||||
import { DarkDealEncounter } from "./encounters/dark-deal-encounter";
|
import { DarkDealEncounter } from "./encounters/dark-deal-encounter";
|
||||||
|
import { DepartmentStoreSaleEncounter } from "./encounters/department-store-sale-encounter";
|
||||||
|
import { FieldTripEncounter } from "./encounters/field-trip-encounter";
|
||||||
|
import { FightOrFlightEncounter } from "./encounters/fight-or-flight-encounter";
|
||||||
|
import { LostAtSeaEncounter } from "./encounters/lost-at-sea-encounter";
|
||||||
import { MysteriousChallengersEncounter } from "./encounters/mysterious-challengers-encounter";
|
import { MysteriousChallengersEncounter } from "./encounters/mysterious-challengers-encounter";
|
||||||
import { MysteriousChestEncounter } from "./encounters/mysterious-chest-encounter";
|
import { MysteriousChestEncounter } from "./encounters/mysterious-chest-encounter";
|
||||||
import { FightOrFlightEncounter } from "./encounters/fight-or-flight-encounter";
|
|
||||||
import { TrainingSessionEncounter } from "./encounters/training-session-encounter";
|
|
||||||
import { Biome } from "#enums/biome";
|
|
||||||
import { SleepingSnorlaxEncounter } from "./encounters/sleeping-snorlax-encounter";
|
|
||||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
|
||||||
import { DepartmentStoreSaleEncounter } from "./encounters/department-store-sale-encounter";
|
|
||||||
import { ShadyVitaminDealerEncounter } from "./encounters/shady-vitamin-dealer-encounter";
|
import { ShadyVitaminDealerEncounter } from "./encounters/shady-vitamin-dealer-encounter";
|
||||||
import { LostAtSeaEncounter } from "./encounters/lost-at-sea-encounter";
|
import { SleepingSnorlaxEncounter } from "./encounters/sleeping-snorlax-encounter";
|
||||||
|
import { TrainingSessionEncounter } from "./encounters/training-session-encounter";
|
||||||
|
import IMysteryEncounter from "./mystery-encounter";
|
||||||
|
|
||||||
// Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * <number of missed spawns>) / 256
|
// Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * <number of missed spawns>) / 256
|
||||||
export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1;
|
export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1;
|
||||||
|
@ -7,7 +7,7 @@ import { EncounterPokemonRequirement } from "../mystery-encounter-requirements";
|
|||||||
/**
|
/**
|
||||||
* {@linkcode CanLearnMoveRequirement} options
|
* {@linkcode CanLearnMoveRequirement} options
|
||||||
*/
|
*/
|
||||||
interface Options {
|
export interface CanlearnMoveRequirementOptions {
|
||||||
excludeLevelMoves?: boolean;
|
excludeLevelMoves?: boolean;
|
||||||
excludeTmMoves?: boolean;
|
excludeTmMoves?: boolean;
|
||||||
excludeEggMoves?: boolean;
|
excludeEggMoves?: boolean;
|
||||||
@ -26,20 +26,11 @@ export class CanLearnMoveRequirement extends EncounterPokemonRequirement {
|
|||||||
private readonly excludeEggMoves?: boolean;
|
private readonly excludeEggMoves?: boolean;
|
||||||
private readonly includeFainted?: boolean;
|
private readonly includeFainted?: boolean;
|
||||||
|
|
||||||
constructor(requiredMoves: Moves | Moves[], options: Options = {}) {
|
constructor(requiredMoves: Moves | Moves[], options: CanlearnMoveRequirementOptions = {}) {
|
||||||
super();
|
super();
|
||||||
this.requiredMoves = Array.isArray(requiredMoves)
|
this.requiredMoves = Array.isArray(requiredMoves) ? requiredMoves : [requiredMoves];
|
||||||
? requiredMoves
|
|
||||||
: [requiredMoves];
|
|
||||||
|
|
||||||
const {
|
const { excludeLevelMoves, excludeTmMoves, excludeEggMoves, includeFainted, minNumberOfPokemon, invertQuery } = options;
|
||||||
excludeLevelMoves,
|
|
||||||
excludeTmMoves,
|
|
||||||
excludeEggMoves,
|
|
||||||
includeFainted,
|
|
||||||
minNumberOfPokemon,
|
|
||||||
invertQuery,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
this.excludeLevelMoves = excludeLevelMoves ?? false;
|
this.excludeLevelMoves = excludeLevelMoves ?? false;
|
||||||
this.excludeTmMoves = excludeTmMoves ?? false;
|
this.excludeTmMoves = excludeTmMoves ?? false;
|
||||||
@ -50,11 +41,7 @@ export class CanLearnMoveRequirement extends EncounterPokemonRequirement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override meetsRequirement(scene: BattleScene): boolean {
|
override meetsRequirement(scene: BattleScene): boolean {
|
||||||
const partyPokemon = scene
|
const partyPokemon = scene.getParty().filter((pkm) => (this.includeFainted ? pkm.isAllowed() : pkm.isAllowedInBattle()));
|
||||||
.getParty()
|
|
||||||
.filter((pkm) =>
|
|
||||||
this.includeFainted ? pkm.isAllowed() : pkm.isAllowedInBattle()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isNullOrUndefined(partyPokemon) || this?.requiredMoves?.length < 0) {
|
if (isNullOrUndefined(partyPokemon) || this?.requiredMoves?.length < 0) {
|
||||||
return false;
|
return false;
|
||||||
@ -67,26 +54,19 @@ export class CanLearnMoveRequirement extends EncounterPokemonRequirement {
|
|||||||
if (!this.invertQuery) {
|
if (!this.invertQuery) {
|
||||||
return partyPokemon.filter((pokemon) =>
|
return partyPokemon.filter((pokemon) =>
|
||||||
// every required move should be included
|
// every required move should be included
|
||||||
this.requiredMoves.every((requiredMove) =>
|
this.requiredMoves.every((requiredMove) => this.getAllPokemonMoves(pokemon).includes(requiredMove))
|
||||||
this.getAllPokemonMoves(pokemon).includes(requiredMove)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return partyPokemon.filter(
|
return partyPokemon.filter(
|
||||||
(pokemon) =>
|
(pokemon) =>
|
||||||
// none of the "required" moves should be included
|
// none of the "required" moves should be included
|
||||||
!this.requiredMoves.some((requiredMove) =>
|
!this.requiredMoves.some((requiredMove) => this.getAllPokemonMoves(pokemon).includes(requiredMove))
|
||||||
this.getAllPokemonMoves(pokemon).includes(requiredMove)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override getDialogueToken(
|
override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
|
||||||
_scene: BattleScene,
|
return pokemon ? ["pokemonCanLearnMove", pokemon.name] : null;
|
||||||
_pokemon?: PlayerPokemon
|
|
||||||
): [string, string] {
|
|
||||||
return ["requiredMoves", this.requiredMoves.join(", ")];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPokemonLevelMoves(pkm: PlayerPokemon): Moves[] {
|
private getPokemonLevelMoves(pkm: PlayerPokemon): Moves[] {
|
||||||
|
@ -6,10 +6,10 @@ export const lostAtSea = {
|
|||||||
query: "What will you do?",
|
query: "What will you do?",
|
||||||
option: {
|
option: {
|
||||||
1: {
|
1: {
|
||||||
label: "Use @ec{waterPkm}", // pkm has to be of type water
|
label: "Use @ec{pokemonCanLearnMove}", // pkm has to be of type water
|
||||||
tooltip:
|
tooltip:
|
||||||
"Use @ec{waterPkm} to guide you back. @ec{waterPkm} earns EXP as if having defeated a Lapras.",
|
"Use @ec{pokemonCanLearnMove} to guide you back. @ec{pokemonCanLearnMove} earns EXP as if having defeated a Lapras.",
|
||||||
selected: "@ec{waterPkm} guides you back and earns EXP.",
|
selected: "@ec{pokemonCanLearnMove} guides you back and earns EXP.",
|
||||||
},
|
},
|
||||||
2: {
|
2: {
|
||||||
label: "Use @ec{flyingPkm}", // pkm has to be of type flying
|
label: "Use @ec{flyingPkm}", // pkm has to be of type flying
|
||||||
|
Loading…
Reference in New Issue
Block a user