+5
-3
@@ -110,11 +110,13 @@ public final class CharacterCommand extends AbstractCommandCollection {
|
||||
}
|
||||
try {
|
||||
String name = context.get(nameArg);
|
||||
CharacterService.CharacterCreationResult result;
|
||||
if (name == null || name.isBlank()) {
|
||||
name = characters.nextDefaultName(playerRef.getUuid());
|
||||
result = characters.createDefaultCharacter(
|
||||
playerRef.getUuid(), playerRef.getUsername());
|
||||
} else {
|
||||
result = characters.createCharacter(playerRef.getUuid(), name);
|
||||
}
|
||||
CharacterService.CharacterCreationResult result =
|
||||
characters.createCharacter(playerRef.getUuid(), name);
|
||||
if (result.ok() && result.profile() != null) {
|
||||
context.sendMessage(Message.raw(
|
||||
"Personnage créé : " + result.profile().getDisplayName()));
|
||||
|
||||
+9
@@ -109,6 +109,15 @@ public final class CharacterRepository {
|
||||
return profile;
|
||||
}
|
||||
|
||||
public boolean delete(@Nonnull UUID accountUuid, @Nonnull UUID characterId) throws SQLException {
|
||||
String sql = "DELETE FROM characters WHERE id = ? AND account_uuid = ?";
|
||||
try (PreparedStatement statement = connection().prepareStatement(sql)) {
|
||||
statement.setString(1, characterId.toString());
|
||||
statement.setString(2, accountUuid.toString());
|
||||
return statement.executeUpdate() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private List<PlayerProfile> findByQuery(@Nonnull String whereClause, @Nonnull String... params)
|
||||
throws SQLException {
|
||||
|
||||
+93
-3
@@ -87,6 +87,25 @@ public final class CharacterService {
|
||||
if (name == null) {
|
||||
return CharacterCreationResult.invalidName();
|
||||
}
|
||||
return createCharacterInternal(accountUuid, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a character named after the player's account username.
|
||||
* Uses {@code "pseudo 2"}, {@code "pseudo 3"}, … when the base name is already taken.
|
||||
*/
|
||||
@Nonnull
|
||||
public CharacterCreationResult createDefaultCharacter(
|
||||
@Nonnull UUID accountUuid,
|
||||
@Nonnull String playerUsername) throws SQLException {
|
||||
String name = defaultCharacterName(accountUuid, playerUsername);
|
||||
return createCharacterInternal(accountUuid, name);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private CharacterCreationResult createCharacterInternal(
|
||||
@Nonnull UUID accountUuid,
|
||||
@Nonnull String name) throws SQLException {
|
||||
if (characters.countByAccount(accountUuid) >= config.getMaxCharactersPerAccount()) {
|
||||
return CharacterCreationResult.limitReached(config.getMaxCharactersPerAccount());
|
||||
}
|
||||
@@ -105,9 +124,54 @@ public final class CharacterService {
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String nextDefaultName(@Nonnull UUID accountUuid) throws SQLException {
|
||||
int count = characters.countByAccount(accountUuid);
|
||||
return "Perso " + (count + 1);
|
||||
public String defaultCharacterName(
|
||||
@Nonnull UUID accountUuid,
|
||||
@Nonnull String playerUsername) throws SQLException {
|
||||
String base = sanitizeCharacterName(playerUsername);
|
||||
if (!characters.nameExists(accountUuid, base)) {
|
||||
return base;
|
||||
}
|
||||
for (int suffix = 2; suffix <= config.getMaxCharactersPerAccount() + 1; suffix++) {
|
||||
String suffixText = " " + suffix;
|
||||
int maxBaseLength = MAX_NAME_LENGTH - suffixText.length();
|
||||
String truncatedBase = base.length() > maxBaseLength
|
||||
? base.substring(0, Math.max(1, maxBaseLength)).trim()
|
||||
: base;
|
||||
String candidate = truncatedBase + suffixText;
|
||||
if (!characters.nameExists(accountUuid, candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return base + " " + System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CharacterDeletionResult deleteCharacter(
|
||||
@Nonnull UUID accountUuid,
|
||||
@Nonnull UUID characterId) throws SQLException {
|
||||
Optional<PlayerProfile> found = characters.findById(characterId);
|
||||
if (found.isEmpty() || !found.get().getAccountUuid().equals(accountUuid)) {
|
||||
return CharacterDeletionResult.notFound();
|
||||
}
|
||||
|
||||
PlayerAccount account = accounts.findByUuid(accountUuid).orElse(null);
|
||||
if (account != null && characterId.equals(account.getActiveCharacterId())) {
|
||||
accounts.setActiveCharacter(accountUuid, null);
|
||||
account.setActiveCharacterId(null);
|
||||
account.touch();
|
||||
accounts.save(account);
|
||||
}
|
||||
|
||||
if (!characters.delete(accountUuid, characterId)) {
|
||||
return CharacterDeletionResult.notFound();
|
||||
}
|
||||
|
||||
logger.at(Level.INFO).log(
|
||||
"Deleted character %s (%s) for account %s",
|
||||
found.get().getDisplayName(),
|
||||
characterId,
|
||||
accountUuid);
|
||||
return CharacterDeletionResult.success(found.get().getDisplayName());
|
||||
}
|
||||
|
||||
public void openCharacterSelector(
|
||||
@@ -224,6 +288,18 @@ public final class CharacterService {
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String sanitizeCharacterName(@Nonnull String rawName) {
|
||||
String name = rawName.trim();
|
||||
if (name.isEmpty()) {
|
||||
return "Joueur";
|
||||
}
|
||||
if (name.length() > MAX_NAME_LENGTH) {
|
||||
name = name.substring(0, MAX_NAME_LENGTH);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String normalizeName(@Nonnull String rawName) {
|
||||
String name = rawName.trim();
|
||||
@@ -269,4 +345,18 @@ public final class CharacterService {
|
||||
false, null, "Limite de " + max + " personnages atteinte.");
|
||||
}
|
||||
}
|
||||
|
||||
public record CharacterDeletionResult(
|
||||
boolean ok,
|
||||
@Nullable String deletedName,
|
||||
@Nullable String errorMessage) {
|
||||
|
||||
static CharacterDeletionResult success(@Nonnull String deletedName) {
|
||||
return new CharacterDeletionResult(true, deletedName, null);
|
||||
}
|
||||
|
||||
static CharacterDeletionResult notFound() {
|
||||
return new CharacterDeletionResult(false, null, "Personnage introuvable.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+104
-7
@@ -29,9 +29,6 @@ import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Mandatory character selector shown on join before gameplay begins.
|
||||
* <p>
|
||||
* Character rows are predefined in {@code MmorpgCharacterSelect.ui} — {@code appendInline}
|
||||
* only supports simple widgets (e.g. {@code Label}), not nested {@code Group}/{@code TextButton}.
|
||||
*/
|
||||
public final class CharacterSelectPage extends InteractiveCustomUIPage<CharacterSelectPage.UiEvent> {
|
||||
|
||||
@@ -44,6 +41,11 @@ public final class CharacterSelectPage extends InteractiveCustomUIPage<Character
|
||||
private final CharacterService characters;
|
||||
private final int maxCharacters;
|
||||
|
||||
@Nullable
|
||||
private UUID pendingDeleteId;
|
||||
@Nullable
|
||||
private String pendingDeleteName;
|
||||
|
||||
public CharacterSelectPage(
|
||||
@Nonnull PlayerRef playerRef,
|
||||
@Nonnull UUID accountUuid,
|
||||
@@ -67,6 +69,7 @@ public final class CharacterSelectPage extends InteractiveCustomUIPage<Character
|
||||
ui.set("#Subtitle.Text", escape("Compte : " + playerRef.getUsername()));
|
||||
bindActions(events);
|
||||
renderCharacterList(ui, events, loadCharacters());
|
||||
applyConfirmPanel(ui, events);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -80,17 +83,36 @@ public final class CharacterSelectPage extends InteractiveCustomUIPage<Character
|
||||
}
|
||||
|
||||
if ("refresh".equals(event.action())) {
|
||||
clearPendingDelete();
|
||||
refresh(ref, store);
|
||||
return;
|
||||
}
|
||||
|
||||
if ("create".equals(event.action())) {
|
||||
clearPendingDelete();
|
||||
handleCreate(ref, store);
|
||||
return;
|
||||
}
|
||||
|
||||
if ("select".equals(event.action()) && event.characterId() != null) {
|
||||
clearPendingDelete();
|
||||
handleSelect(ref, store, event.characterId());
|
||||
return;
|
||||
}
|
||||
|
||||
if ("delete".equals(event.action()) && event.characterId() != null) {
|
||||
handleDeleteRequest(ref, store, event.characterId());
|
||||
return;
|
||||
}
|
||||
|
||||
if ("confirm_delete".equals(event.action())) {
|
||||
handleConfirmDelete(ref, store);
|
||||
return;
|
||||
}
|
||||
|
||||
if ("cancel_delete".equals(event.action())) {
|
||||
clearPendingDelete();
|
||||
refresh(ref, store);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,16 +123,16 @@ public final class CharacterSelectPage extends InteractiveCustomUIPage<Character
|
||||
setStatus(ref, store, "Limite de " + maxCharacters + " personnages atteinte.");
|
||||
return;
|
||||
}
|
||||
String name = characters.nextDefaultName(accountUuid);
|
||||
CharacterService.CharacterCreationResult result = characters.createCharacter(accountUuid, name);
|
||||
CharacterService.CharacterCreationResult result =
|
||||
characters.createDefaultCharacter(accountUuid, playerRef.getUsername());
|
||||
if (!result.ok()) {
|
||||
setStatus(ref, store, result.errorMessage() != null ? result.errorMessage() : "Creation impossible.");
|
||||
setStatus(ref, store, result.errorMessage() != null ? result.errorMessage() : "Création impossible.");
|
||||
return;
|
||||
}
|
||||
refresh(ref, store);
|
||||
} catch (SQLException e) {
|
||||
LOGGER.log(Level.WARNING, "Character creation failed", e);
|
||||
setStatus(ref, store, "Erreur lors de la creation du personnage.");
|
||||
setStatus(ref, store, "Erreur lors de la création du personnage.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,11 +148,52 @@ public final class CharacterSelectPage extends InteractiveCustomUIPage<Character
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDeleteRequest(
|
||||
@Nonnull Ref<EntityStore> ref,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull UUID characterId) {
|
||||
try {
|
||||
var found = characters.findCharacter(accountUuid, characterId.toString());
|
||||
if (found.isEmpty()) {
|
||||
setStatus(ref, store, "Personnage introuvable.");
|
||||
return;
|
||||
}
|
||||
pendingDeleteId = characterId;
|
||||
pendingDeleteName = found.get().getDisplayName();
|
||||
refresh(ref, store);
|
||||
} catch (SQLException e) {
|
||||
LOGGER.log(Level.WARNING, "Character delete request failed", e);
|
||||
setStatus(ref, store, "Erreur lors de la préparation de la suppression.");
|
||||
}
|
||||
}
|
||||
|
||||
private void handleConfirmDelete(@Nonnull Ref<EntityStore> ref, @Nonnull Store<EntityStore> store) {
|
||||
if (pendingDeleteId == null) {
|
||||
refresh(ref, store);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
CharacterService.CharacterDeletionResult result =
|
||||
characters.deleteCharacter(accountUuid, pendingDeleteId);
|
||||
clearPendingDelete();
|
||||
if (result.ok()) {
|
||||
setStatus(ref, store, "Personnage supprimé : " + result.deletedName());
|
||||
} else {
|
||||
setStatus(ref, store, result.errorMessage() != null ? result.errorMessage() : "Suppression impossible.");
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.log(Level.WARNING, "Character deletion failed", e);
|
||||
clearPendingDelete();
|
||||
setStatus(ref, store, "Erreur lors de la suppression du personnage.");
|
||||
}
|
||||
}
|
||||
|
||||
private void refresh(@Nonnull Ref<EntityStore> ref, @Nonnull Store<EntityStore> store) {
|
||||
UICommandBuilder ui = new UICommandBuilder();
|
||||
UIEventBuilder events = new UIEventBuilder();
|
||||
bindActions(events);
|
||||
renderCharacterList(ui, events, loadCharacters());
|
||||
applyConfirmPanel(ui, events);
|
||||
sendUpdate(ui, events, false);
|
||||
}
|
||||
|
||||
@@ -142,6 +205,7 @@ public final class CharacterSelectPage extends InteractiveCustomUIPage<Character
|
||||
UIEventBuilder events = new UIEventBuilder();
|
||||
bindActions(events);
|
||||
renderCharacterList(ui, events, loadCharacters());
|
||||
applyConfirmPanel(ui, events);
|
||||
ui.set("#FooterHint.Text", escape(message));
|
||||
sendUpdate(ui, events, false);
|
||||
}
|
||||
@@ -184,9 +248,37 @@ public final class CharacterSelectPage extends InteractiveCustomUIPage<Character
|
||||
EventData.of("Action", "select")
|
||||
.append("CharacterId", profile.getCharacterId().toString()),
|
||||
false);
|
||||
events.addEventBinding(
|
||||
CustomUIEventBindingType.Activating,
|
||||
"#CharDelete" + slot,
|
||||
EventData.of("Action", "delete")
|
||||
.append("CharacterId", profile.getCharacterId().toString()),
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyConfirmPanel(@Nonnull UICommandBuilder ui, @Nonnull UIEventBuilder events) {
|
||||
if (pendingDeleteId == null || pendingDeleteName == null) {
|
||||
ui.set("#ConfirmPanel.Visible", false);
|
||||
return;
|
||||
}
|
||||
|
||||
ui.set("#ConfirmPanel.Visible", true);
|
||||
ui.set(
|
||||
"#ConfirmMessage.Text",
|
||||
escape("Supprimer « " + pendingDeleteName + " » ?\nInventaire et progression seront perdus."));
|
||||
events.addEventBinding(
|
||||
CustomUIEventBindingType.Activating,
|
||||
"#ConfirmDeleteButton",
|
||||
EventData.of("Action", "confirm_delete"),
|
||||
false);
|
||||
events.addEventBinding(
|
||||
CustomUIEventBindingType.Activating,
|
||||
"#CancelDeleteButton",
|
||||
EventData.of("Action", "cancel_delete"),
|
||||
false);
|
||||
}
|
||||
|
||||
private void bindActions(@Nonnull UIEventBuilder events) {
|
||||
events.addEventBinding(
|
||||
CustomUIEventBindingType.Activating,
|
||||
@@ -200,6 +292,11 @@ public final class CharacterSelectPage extends InteractiveCustomUIPage<Character
|
||||
false);
|
||||
}
|
||||
|
||||
private void clearPendingDelete() {
|
||||
pendingDeleteId = null;
|
||||
pendingDeleteName = null;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static String formatSummary(@Nonnull PlayerProfile profile) {
|
||||
PlayerClass playerClass = ClassCatalog.byId(profile.getClassId());
|
||||
|
||||
+60
-5
@@ -3,6 +3,7 @@
|
||||
@MmorpgAccent = #f5c842;
|
||||
@MmorpgText = #e8e8ec;
|
||||
@MmorpgMuted = #878e9c;
|
||||
@MmorpgDanger = #c9302c;
|
||||
|
||||
Group {
|
||||
LayoutMode: Center;
|
||||
@@ -49,9 +50,14 @@ Group {
|
||||
FlexWeight: 1;
|
||||
}
|
||||
|
||||
TextButton #CharDelete0 {
|
||||
Text: "Suppr.";
|
||||
Anchor: (Width: 72, Right: 6);
|
||||
}
|
||||
|
||||
TextButton #CharSelect0 {
|
||||
Text: "Jouer";
|
||||
Anchor: (Width: 100);
|
||||
Anchor: (Width: 88);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,9 +73,14 @@ Group {
|
||||
FlexWeight: 1;
|
||||
}
|
||||
|
||||
TextButton #CharDelete1 {
|
||||
Text: "Suppr.";
|
||||
Anchor: (Width: 72, Right: 6);
|
||||
}
|
||||
|
||||
TextButton #CharSelect1 {
|
||||
Text: "Jouer";
|
||||
Anchor: (Width: 100);
|
||||
Anchor: (Width: 88);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,9 +96,14 @@ Group {
|
||||
FlexWeight: 1;
|
||||
}
|
||||
|
||||
TextButton #CharDelete2 {
|
||||
Text: "Suppr.";
|
||||
Anchor: (Width: 72, Right: 6);
|
||||
}
|
||||
|
||||
TextButton #CharSelect2 {
|
||||
Text: "Jouer";
|
||||
Anchor: (Width: 100);
|
||||
Anchor: (Width: 88);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,9 +119,14 @@ Group {
|
||||
FlexWeight: 1;
|
||||
}
|
||||
|
||||
TextButton #CharDelete3 {
|
||||
Text: "Suppr.";
|
||||
Anchor: (Width: 72, Right: 6);
|
||||
}
|
||||
|
||||
TextButton #CharSelect3 {
|
||||
Text: "Jouer";
|
||||
Anchor: (Width: 100);
|
||||
Anchor: (Width: 88);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,9 +142,43 @@ Group {
|
||||
FlexWeight: 1;
|
||||
}
|
||||
|
||||
TextButton #CharDelete4 {
|
||||
Text: "Suppr.";
|
||||
Anchor: (Width: 72, Right: 6);
|
||||
}
|
||||
|
||||
TextButton #CharSelect4 {
|
||||
Text: "Jouer";
|
||||
Anchor: (Width: 100);
|
||||
Anchor: (Width: 88);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Group #ConfirmPanel {
|
||||
LayoutMode: Top;
|
||||
Visible: false;
|
||||
Background: #2a1515(0.95);
|
||||
Padding: (Full: 12);
|
||||
Anchor: (Bottom: 8);
|
||||
|
||||
Label #ConfirmMessage {
|
||||
Style: (FontSize: 14, TextColor: @MmorpgText, Wrap: true, HorizontalAlignment: Center);
|
||||
Text: "Confirmer la suppression ?";
|
||||
Anchor: (Bottom: 10);
|
||||
}
|
||||
|
||||
Group #ConfirmActions {
|
||||
LayoutMode: Left;
|
||||
|
||||
TextButton #ConfirmDeleteButton {
|
||||
Text: "Confirmer la suppression";
|
||||
FlexWeight: 1;
|
||||
Anchor: (Right: 8);
|
||||
}
|
||||
|
||||
TextButton #CancelDeleteButton {
|
||||
Text: "Annuler";
|
||||
FlexWeight: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user