refactor(game): unifica renderitzat dels rètols de transició via TextScreenSpec
This commit is contained in:
1 parent
fa27308897
commit
2523661c8f
2 files changed
+127
-154
No files matched your search
+100
-154
@@ -1011,11 +1011,6 @@ void GameScene::renderRoomFade() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::renderRoomText() {
|
void GameScene::renderRoomText() {
|
||||||
// Orquestrador del rètol de transició de sala. La feina pesada
|
|
||||||
// (animació de X, animació d'altura del panel i pintat) la fan
|
|
||||||
// quatre helpers privats; aquesta funció només decideix quin
|
|
||||||
// text dibuixar i amb quines fases segons l'estat.
|
|
||||||
//
|
|
||||||
// Mapatge per estat (només els estats amb text actiu hi entren):
|
// Mapatge per estat (només els estats amb text actiu hi entren):
|
||||||
// ROOM_INTRO: phases 2..4 → enter / hold / exit; dues línies
|
// ROOM_INTRO: phases 2..4 → enter / hold / exit; dues línies
|
||||||
// (piràmide damunt, sala davall) que travessen la
|
// (piràmide damunt, sala davall) que travessen la
|
||||||
@@ -1024,82 +1019,35 @@ void GameScene::renderRoomText() {
|
|||||||
// oposades ("fase" / "completada"). Phase 3 és el
|
// oposades ("fase" / "completada"). Phase 3 és el
|
||||||
// fade-out i 4 la pausa en negre — cap text en cap
|
// fade-out i 4 la pausa en negre — cap text en cap
|
||||||
// de les dues.
|
// de les dues.
|
||||||
if (!room_text_font_) { return; }
|
|
||||||
|
|
||||||
constexpr int KERNING = 0;
|
|
||||||
// Centre vertical de la zona de joc (canvas sense la HUD). Reemplaça
|
|
||||||
// l'antic GameCanvas::CENTER_Y per descomptar la franja del marcador
|
|
||||||
// i tenir el rètol equilibrat sobre el playfield.
|
|
||||||
const int PLAY_CENTER_Y = (GameCanvas::HEIGHT + Map::hud_h) / 2;
|
|
||||||
// Colors del rètol via UiConfig (`data/config/ui.yaml > colors.room_text` i
|
|
||||||
// `room_text_outline`). Independents dels colors de notificació.
|
|
||||||
const Uint8 FILL = UiConfig::get().roomText();
|
|
||||||
const Uint8 OUTLINE = UiConfig::get().roomTextOutline();
|
|
||||||
const int LINE_HEIGHT = room_text_font_->getLineHeight();
|
|
||||||
|
|
||||||
const int PHASE = transition_.phase;
|
const int PHASE = transition_.phase;
|
||||||
const int ELAPSED = transition_.elapsed_ms;
|
const int ELAPSED = transition_.elapsed_ms;
|
||||||
if (state_ == RoomFsm::State::ROOM_INTRO && PHASE >= 2 && PHASE <= 4) {
|
if (state_ == RoomFsm::State::ROOM_INTRO && PHASE >= 2 && PHASE <= 4) {
|
||||||
const int DUR_ENTER = transition_config_.intro_text_enter_ms;
|
|
||||||
const int DUR_EXIT = transition_config_.intro_text_exit_ms;
|
|
||||||
// Panel primer (queda sota el text gràcies a l'ordre de blits).
|
|
||||||
drawRoomTextPanel(PLAY_CENTER_Y,
|
|
||||||
computeRoomTextPanelHeight(PHASE, ELAPSED, 2, 3, 4, DUR_ENTER, DUR_EXIT));
|
|
||||||
|
|
||||||
const auto& mgr = Levels::LevelManager::get();
|
const auto& mgr = Levels::LevelManager::get();
|
||||||
const std::string LINE_PYRAMID = RoomFsm::formatIntroPyramidLine(mgr.currentPyramid());
|
renderTextScreen(TextScreenSpec{
|
||||||
const std::string LINE_ROOM = RoomFsm::formatIntroRoomLine(mgr.currentRoom());
|
.line1 = RoomFsm::formatIntroPyramidLine(mgr.currentPyramid()),
|
||||||
// Direccions oposades: piràmide entra per l'esquerra i surt per
|
.line2 = RoomFsm::formatIntroRoomLine(mgr.currentRoom()),
|
||||||
// la dreta; sala fa el contrari.
|
.phase_enter = 2,
|
||||||
const int X_PYRAMID = computeRoomTextX(room_text_font_->length(LINE_PYRAMID, KERNING),
|
.phase_hold = 3,
|
||||||
|
.phase_exit = 4,
|
||||||
|
.dur_enter_ms = transition_config_.intro_text_enter_ms,
|
||||||
|
.dur_exit_ms = transition_config_.intro_text_exit_ms,
|
||||||
|
.line_gap_px = transition_config_.intro_text_line_gap_px,
|
||||||
|
},
|
||||||
PHASE,
|
PHASE,
|
||||||
ELAPSED,
|
ELAPSED);
|
||||||
2,
|
|
||||||
4,
|
|
||||||
DUR_ENTER,
|
|
||||||
DUR_EXIT,
|
|
||||||
/*from_right=*/false);
|
|
||||||
const int X_ROOM = computeRoomTextX(room_text_font_->length(LINE_ROOM, KERNING),
|
|
||||||
PHASE,
|
|
||||||
ELAPSED,
|
|
||||||
2,
|
|
||||||
4,
|
|
||||||
DUR_ENTER,
|
|
||||||
DUR_EXIT,
|
|
||||||
/*from_right=*/true);
|
|
||||||
// Bloc de dues línies centrat al voltant del PLAY_CENTER_Y. El
|
|
||||||
// gap (`intro_text_line_gap_px`) es reparteix simètricament:
|
|
||||||
// mig amunt a la piràmide, mig avall a la sala.
|
|
||||||
const int GAP = transition_config_.intro_text_line_gap_px;
|
|
||||||
drawRoomTextLine(LINE_PYRAMID, X_PYRAMID, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE);
|
|
||||||
drawRoomTextLine(LINE_ROOM, X_ROOM, PLAY_CENTER_Y + (GAP / 2), FILL, OUTLINE);
|
|
||||||
} else if (state_ == RoomFsm::State::ROOM_OUTRO && PHASE >= 0 && PHASE <= 2) {
|
} else if (state_ == RoomFsm::State::ROOM_OUTRO && PHASE >= 0 && PHASE <= 2) {
|
||||||
const int DUR_ENTER = transition_config_.outro_text_enter_ms;
|
renderTextScreen(TextScreenSpec{
|
||||||
const int DUR_EXIT = transition_config_.outro_text_exit_ms;
|
.line1 = Locale::get().text("game.room_complete_line1"),
|
||||||
drawRoomTextPanel(PLAY_CENTER_Y,
|
.line2 = Locale::get().text("game.room_complete_line2"),
|
||||||
computeRoomTextPanelHeight(PHASE, ELAPSED, 0, 1, 2, DUR_ENTER, DUR_EXIT));
|
.phase_enter = 0,
|
||||||
|
.phase_hold = 1,
|
||||||
const std::string LINE_1 = Locale::get().text("game.room_complete_line1");
|
.phase_exit = 2,
|
||||||
const std::string LINE_2 = Locale::get().text("game.room_complete_line2");
|
.dur_enter_ms = transition_config_.outro_text_enter_ms,
|
||||||
const int X_1 = computeRoomTextX(room_text_font_->length(LINE_1, KERNING),
|
.dur_exit_ms = transition_config_.outro_text_exit_ms,
|
||||||
|
.line_gap_px = transition_config_.outro_text_line_gap_px,
|
||||||
|
},
|
||||||
PHASE,
|
PHASE,
|
||||||
ELAPSED,
|
ELAPSED);
|
||||||
0,
|
|
||||||
2,
|
|
||||||
DUR_ENTER,
|
|
||||||
DUR_EXIT,
|
|
||||||
/*from_right=*/false);
|
|
||||||
const int X_2 = computeRoomTextX(room_text_font_->length(LINE_2, KERNING),
|
|
||||||
PHASE,
|
|
||||||
ELAPSED,
|
|
||||||
0,
|
|
||||||
2,
|
|
||||||
DUR_ENTER,
|
|
||||||
DUR_EXIT,
|
|
||||||
/*from_right=*/true);
|
|
||||||
const int GAP = transition_config_.outro_text_line_gap_px;
|
|
||||||
drawRoomTextLine(LINE_1, X_1, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE);
|
|
||||||
drawRoomTextLine(LINE_2, X_2, PLAY_CENTER_Y + (GAP / 2), FILL, OUTLINE);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1179,6 +1127,51 @@ void GameScene::drawRoomTextLine(const std::string& text, int x, int y, std::uin
|
|||||||
outline_color);
|
outline_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameScene::renderTextScreen(const TextScreenSpec& spec, int phase, int elapsed_ms) const {
|
||||||
|
// Coreografia comuna als quatre rètols (ROOM_INTRO, ROOM_OUTRO,
|
||||||
|
// CONTINUE?, GAME OVER): panel translúcid de fons + dues línies
|
||||||
|
// que entren/surten per costats oposats. Els quatre helpers
|
||||||
|
// existents (`drawRoomTextPanel`, `computeRoomTextPanelHeight`,
|
||||||
|
// `computeRoomTextX`, `drawRoomTextLine`) fan la feina pesada;
|
||||||
|
// aquesta funció només els encadena en el mateix ordre per als
|
||||||
|
// quatre casos. Una `lineN` buida es salta sense pintar — així
|
||||||
|
// CONTINUE? amaga la línia del countdown fora del hold sense
|
||||||
|
// que els callers hagin de duplicar la branca.
|
||||||
|
if (!room_text_font_) { return; }
|
||||||
|
|
||||||
|
constexpr int KERNING = 0;
|
||||||
|
const int PLAY_CENTER_Y = (GameCanvas::HEIGHT + Map::hud_h) / 2;
|
||||||
|
const Uint8 FILL = UiConfig::get().roomText();
|
||||||
|
const Uint8 OUTLINE = UiConfig::get().roomTextOutline();
|
||||||
|
const int LINE_HEIGHT = room_text_font_->getLineHeight();
|
||||||
|
|
||||||
|
drawRoomTextPanel(PLAY_CENTER_Y,
|
||||||
|
computeRoomTextPanelHeight(phase, elapsed_ms, spec.phase_enter, spec.phase_hold, spec.phase_exit, spec.dur_enter_ms, spec.dur_exit_ms));
|
||||||
|
|
||||||
|
if (!spec.line1.empty()) {
|
||||||
|
const int X_1 = computeRoomTextX(room_text_font_->length(spec.line1, KERNING),
|
||||||
|
phase,
|
||||||
|
elapsed_ms,
|
||||||
|
spec.phase_enter,
|
||||||
|
spec.phase_exit,
|
||||||
|
spec.dur_enter_ms,
|
||||||
|
spec.dur_exit_ms,
|
||||||
|
/*from_right=*/false);
|
||||||
|
drawRoomTextLine(spec.line1, X_1, PLAY_CENTER_Y - LINE_HEIGHT - (spec.line_gap_px / 2), FILL, OUTLINE);
|
||||||
|
}
|
||||||
|
if (!spec.line2.empty()) {
|
||||||
|
const int X_2 = computeRoomTextX(room_text_font_->length(spec.line2, KERNING),
|
||||||
|
phase,
|
||||||
|
elapsed_ms,
|
||||||
|
spec.phase_enter,
|
||||||
|
spec.phase_exit,
|
||||||
|
spec.dur_enter_ms,
|
||||||
|
spec.dur_exit_ms,
|
||||||
|
/*from_right=*/true);
|
||||||
|
drawRoomTextLine(spec.line2, X_2, PLAY_CENTER_Y + (spec.line_gap_px / 2), FILL, OUTLINE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GameScene::setGameOverState(GameOverFsm::State new_state) {
|
void GameScene::setGameOverState(GameOverFsm::State new_state) {
|
||||||
if (new_state == game_over_state_) { return; }
|
if (new_state == game_over_state_) { return; }
|
||||||
const auto PREV = game_over_state_;
|
const auto PREV = game_over_state_;
|
||||||
@@ -1315,104 +1308,57 @@ void GameScene::emitCountdownTickIfChanged() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::renderGameOverText() {
|
void GameScene::renderGameOverText() {
|
||||||
if (!room_text_font_) { return; }
|
// Mapatge per estat:
|
||||||
|
// CONTINUE_PROMPT: phases 0=enter, 1=hold (countdown visible),
|
||||||
|
// 2=exit. La línia 2 (número) només es pinta
|
||||||
|
// durant la fase 1 perquè els números no
|
||||||
|
// travessin la pantalla.
|
||||||
|
// GAME_OVER: phases 0=hold, 1=exit (sense enter anim — el
|
||||||
|
// panel ja és amunt del CONTINUE_PROMPT). Phase 2
|
||||||
|
// (wait) sense text ni panel.
|
||||||
if (game_over_state_ != GameOverFsm::State::CONTINUE_PROMPT && game_over_state_ != GameOverFsm::State::GAME_OVER) {
|
if (game_over_state_ != GameOverFsm::State::CONTINUE_PROMPT && game_over_state_ != GameOverFsm::State::GAME_OVER) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr int KERNING = 0;
|
|
||||||
const int PLAY_CENTER_Y = (GameCanvas::HEIGHT + Map::hud_h) / 2;
|
|
||||||
const Uint8 FILL = UiConfig::get().roomText();
|
|
||||||
const Uint8 OUTLINE = UiConfig::get().roomTextOutline();
|
|
||||||
const int LINE_HEIGHT = room_text_font_->getLineHeight();
|
|
||||||
const int PHASE = game_over_transition_.phase;
|
const int PHASE = game_over_transition_.phase;
|
||||||
const int ELAPSED = game_over_transition_.elapsed_ms;
|
const int ELAPSED = game_over_transition_.elapsed_ms;
|
||||||
|
|
||||||
if (game_over_state_ == GameOverFsm::State::CONTINUE_PROMPT) {
|
if (game_over_state_ == GameOverFsm::State::CONTINUE_PROMPT) {
|
||||||
const int DUR_ENTER = game_over_config_.continue_text_enter_ms;
|
|
||||||
const int DUR_EXIT = game_over_config_.continue_text_exit_ms;
|
|
||||||
// Phases del CONTINUE_PROMPT: 0=enter, 1=hold, 2=exit. El panel
|
|
||||||
// segueix la mateixa corba enter/hold/exit que el room text.
|
|
||||||
drawRoomTextPanel(PLAY_CENTER_Y,
|
|
||||||
computeRoomTextPanelHeight(PHASE, ELAPSED, 0, 1, 2, DUR_ENTER, DUR_EXIT));
|
|
||||||
|
|
||||||
const std::string LINE_1 = Locale::get().text("game.continue_prompt");
|
|
||||||
const int SECONDS = GameOverFsm::countdownSecondsRemaining(
|
const int SECONDS = GameOverFsm::countdownSecondsRemaining(
|
||||||
game_over_state_,
|
game_over_state_,
|
||||||
game_over_transition_,
|
game_over_transition_,
|
||||||
game_over_config_);
|
game_over_config_);
|
||||||
// Línia 2 només té text durant la fase de hold (countdown). En
|
renderTextScreen(TextScreenSpec{
|
||||||
// les fases d'enter i exit deixem només el "CONTINUE?" perquè
|
.line1 = Locale::get().text("game.continue_prompt"),
|
||||||
// l'animació dels números no quedi rara.
|
.line2 = (PHASE == 1) ? std::to_string(SECONDS) : std::string{},
|
||||||
const std::string LINE_2 = (PHASE == 1) ? std::to_string(SECONDS) : std::string{};
|
.phase_enter = 0,
|
||||||
|
.phase_hold = 1,
|
||||||
const int X_1 = computeRoomTextX(room_text_font_->length(LINE_1, KERNING),
|
.phase_exit = 2,
|
||||||
|
.dur_enter_ms = game_over_config_.continue_text_enter_ms,
|
||||||
|
.dur_exit_ms = game_over_config_.continue_text_exit_ms,
|
||||||
|
.line_gap_px = transition_config_.intro_text_line_gap_px,
|
||||||
|
},
|
||||||
PHASE,
|
PHASE,
|
||||||
ELAPSED,
|
ELAPSED);
|
||||||
0,
|
|
||||||
2,
|
|
||||||
DUR_ENTER,
|
|
||||||
DUR_EXIT,
|
|
||||||
/*from_right=*/false);
|
|
||||||
const int X_2 = computeRoomTextX(LINE_2.empty() ? 0 : room_text_font_->length(LINE_2, KERNING),
|
|
||||||
PHASE,
|
|
||||||
ELAPSED,
|
|
||||||
0,
|
|
||||||
2,
|
|
||||||
DUR_ENTER,
|
|
||||||
DUR_EXIT,
|
|
||||||
/*from_right=*/true);
|
|
||||||
const int GAP = transition_config_.intro_text_line_gap_px;
|
|
||||||
drawRoomTextLine(LINE_1, X_1, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE);
|
|
||||||
if (!LINE_2.empty()) {
|
|
||||||
drawRoomTextLine(LINE_2, X_2, PLAY_CENTER_Y + (GAP / 2), FILL, OUTLINE);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GAME_OVER: panel sempre amunt durant phase 0/1, decreix en phase 1
|
// GAME_OVER: phase 2 (wait) sense panel ni text — només temps mort
|
||||||
// (exit). Phase 2 (wait) sense panel ni text — només temps mort
|
// abans del fade. phase_enter=-1 desactiva l'animació d'entrada
|
||||||
// abans del fade.
|
// (el panel ja és amunt heretat del CONTINUE_PROMPT).
|
||||||
const int DUR_HOLD = game_over_config_.game_over_text_hold_ms;
|
|
||||||
const int DUR_EXIT = game_over_config_.game_over_text_exit_ms;
|
|
||||||
if (PHASE > 1) { return; }
|
if (PHASE > 1) { return; }
|
||||||
|
renderTextScreen(TextScreenSpec{
|
||||||
// Panel: phase 0 = ple, phase 1 = decreix. Reaprofitem
|
.line1 = Locale::get().text("game.game_over_line1"),
|
||||||
// computeRoomTextPanelHeight passant un phase_enter "fora de rang"
|
.line2 = Locale::get().text("game.game_over_line2"),
|
||||||
// (-1) perquè la branca d'enter no s'activi mai.
|
.phase_enter = -1,
|
||||||
drawRoomTextPanel(PLAY_CENTER_Y,
|
.phase_hold = 0,
|
||||||
computeRoomTextPanelHeight(PHASE, ELAPSED, /*phase_enter=*/-1,
|
.phase_exit = 1,
|
||||||
/*phase_hold=*/0,
|
.dur_enter_ms = 0,
|
||||||
/*phase_exit=*/1,
|
.dur_exit_ms = game_over_config_.game_over_text_exit_ms,
|
||||||
/*dur_enter=*/0,
|
.line_gap_px = transition_config_.outro_text_line_gap_px,
|
||||||
DUR_EXIT));
|
},
|
||||||
|
|
||||||
const std::string LINE_1 = Locale::get().text("game.game_over_line1");
|
|
||||||
const std::string LINE_2 = Locale::get().text("game.game_over_line2");
|
|
||||||
// Phase 0 (hold): text quiet al CENTER_X. Phase 1 (exit): el text
|
|
||||||
// marxa pels costats com el room outro. Reaprofitem computeRoomTextX
|
|
||||||
// amb phase_enter=-1 (no enter anim) i phase_exit=1.
|
|
||||||
const int X_1 = computeRoomTextX(room_text_font_->length(LINE_1, KERNING),
|
|
||||||
PHASE,
|
PHASE,
|
||||||
ELAPSED,
|
ELAPSED);
|
||||||
/*phase_enter=*/-1,
|
|
||||||
/*phase_exit=*/1,
|
|
||||||
/*dur_enter=*/0,
|
|
||||||
DUR_EXIT,
|
|
||||||
/*from_right=*/false);
|
|
||||||
const int X_2 = computeRoomTextX(room_text_font_->length(LINE_2, KERNING),
|
|
||||||
PHASE,
|
|
||||||
ELAPSED,
|
|
||||||
/*phase_enter=*/-1,
|
|
||||||
/*phase_exit=*/1,
|
|
||||||
/*dur_enter=*/0,
|
|
||||||
DUR_EXIT,
|
|
||||||
/*from_right=*/true);
|
|
||||||
const int GAP = transition_config_.outro_text_line_gap_px;
|
|
||||||
drawRoomTextLine(LINE_1, X_1, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE);
|
|
||||||
drawRoomTextLine(LINE_2, X_2, PLAY_CENTER_Y + (GAP / 2), FILL, OUTLINE);
|
|
||||||
|
|
||||||
(void)DUR_HOLD; // Reservat per a futurs efectes de hold (pulse, etc.).
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::renderGameOverFade() {
|
void GameScene::renderGameOverFade() {
|
||||||
|
|||||||
@@ -372,6 +372,33 @@ class GameScene : public Scene {
|
|||||||
void drawRoomTextPanel(int center_y, int panel_height) const;
|
void drawRoomTextPanel(int center_y, int panel_height) const;
|
||||||
void drawRoomTextLine(const std::string& text, int x, int y, std::uint8_t fill_color, std::uint8_t outline_color) const;
|
void drawRoomTextLine(const std::string& text, int x, int y, std::uint8_t fill_color, std::uint8_t outline_color) const;
|
||||||
|
|
||||||
|
// Especificació declarativa d'un rètol de transició de dues línies
|
||||||
|
// amb panel translúcid. ROOM_INTRO, ROOM_OUTRO, CONTINUE? i GAME OVER
|
||||||
|
// comparteixen la mateixa coreografia (panel + dues línies en
|
||||||
|
// direccions oposades) i només divergeixen en el text, els índexs
|
||||||
|
// de fase i les durades. `phase_enter = -1` desactiva l'animació
|
||||||
|
// d'entrada (cas GAME_OVER, que apareix amb el panel ja amunt).
|
||||||
|
// Una `lineN` buida indica que la línia no s'ha de pintar (cas
|
||||||
|
// CONTINUE? fora del hold, on la línia 2 mostra el countdown).
|
||||||
|
struct TextScreenSpec {
|
||||||
|
std::string line1;
|
||||||
|
std::string line2;
|
||||||
|
int phase_enter{0};
|
||||||
|
int phase_hold{0};
|
||||||
|
int phase_exit{0};
|
||||||
|
int dur_enter_ms{0};
|
||||||
|
int dur_exit_ms{0};
|
||||||
|
int line_gap_px{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Orquestrador unificat dels quatre rètols de transició. Pinta el
|
||||||
|
// panel + dues línies a partir de `spec`, deixant la decisió de
|
||||||
|
// QUÈ pintar (text, fases, durades) als callers `renderRoomText`
|
||||||
|
// i `renderGameOverText`. Sense estat propi: la fase i temps
|
||||||
|
// arriben pels paràmetres perquè la mateixa funció serveix les
|
||||||
|
// dues sub-FSMs (RoomFsm i GameOverFsm).
|
||||||
|
void renderTextScreen(const TextScreenSpec& spec, int phase, int elapsed_ms) const;
|
||||||
|
|
||||||
// Posició inicial dels jugadors derivada de `Map::door_col`. Tots
|
// Posició inicial dels jugadors derivada de `Map::door_col`. Tots
|
||||||
// els jugadors comparteixen Y (centre del corredor exterior superior);
|
// els jugadors comparteixen Y (centre del corredor exterior superior);
|
||||||
// X depèn del nombre actiu: amb un jugador queda centrat sota la
|
// X depèn del nombre actiu: amb un jugador queda centrat sota la
|
||||||
|
|||||||
Reference in new issue
Block a user