feat: implemented browser + editors

This commit is contained in:
Andrey Terentev 2025-02-13 01:22:38 +07:00
parent 36eb128e85
commit 41724927b6
21 changed files with 1075 additions and 142 deletions

View File

@ -6,12 +6,17 @@ import net.minestom.server.entity.GameMode;
import net.minestom.server.event.player.PlayerChatEvent; import net.minestom.server.event.player.PlayerChatEvent;
import ru.dragonestia.msb3.api.module.FlatWorldModule; import ru.dragonestia.msb3.api.module.FlatWorldModule;
import ru.dragonestia.msb3.api.module.MotdModule; import ru.dragonestia.msb3.api.module.MotdModule;
import ru.dragonestia.msb3.api.module.PrometheusMetricsModule;
import ru.dragonestia.msb3.api.module.ResourcePackRepositoryModule; import ru.dragonestia.msb3.api.module.ResourcePackRepositoryModule;
import ru.dragonestia.msb3.api.resource.dialog.ButtonNumber; import ru.dragonestia.msb3.api.resource.dialog.ButtonNumber;
import ru.dragonestia.msb3.api.talk.dialogue.DialogueRenderer; import ru.dragonestia.msb3.api.talk.dialogue.DialogueRenderer;
import ru.dragonestia.msb3.api.talk.dialogue.DialogueTheme; import ru.dragonestia.msb3.api.talk.dialogue.DialogueTheme;
import ru.dragonestia.msb3.api.ui.PictureBanner;
import ru.dragonestia.msb3.resource.utils.ClassPreLoader;
import team.unnamed.creative.ResourcePack; import team.unnamed.creative.ResourcePack;
import java.net.InetSocketAddress;
@Log4j2 @Log4j2
public class DefaultBootstrap extends ServerInitializer { public class DefaultBootstrap extends ServerInitializer {
@ -23,56 +28,58 @@ public class DefaultBootstrap extends ServerInitializer {
public void onDefaultModulesLoaded() { public void onDefaultModulesLoaded() {
MotdModule.init("logo.png", "<gradient:#ff0059:#e06806><bold>msb3 server</bold></gradient>"); MotdModule.init("logo.png", "<gradient:#ff0059:#e06806><bold>msb3 server</bold></gradient>");
FlatWorldModule.init(GameMode.ADVENTURE); FlatWorldModule.init(GameMode.ADVENTURE);
PrometheusMetricsModule.init(new InetSocketAddress("0.0.0.0", 7500));
MinecraftServer.getGlobalEventHandler().addListener(PlayerChatEvent.class, event -> { MinecraftServer.getGlobalEventHandler().addListener(PlayerChatEvent.class, event -> {
var player = event.getPlayer(); var player = event.getPlayer();
var render = new DialogueRenderer(player, DialogueTheme.builder().build()); PictureBanner.TEST.show(player);
render.setText(""" // var render = new DialogueRenderer(player, DialogueTheme.builder().build());
Абсолютно точно. // render.setText("""
Я знаю точнo - невозможное возможно // Абсолютно точно.
Сойти с ума, влюбиться так неосторoжно // Я знаю точнo - невозможное возможно
Найти тебя, не отпускать ни днём, ни ночью // Сойти с ума, влюбиться так неосторoжно
Всё невозможное - возможно, знаю точно! // Найти тебя, не отпускать ни днём, ни ночью
А где тебя искать, прошу ты мне ответь // Всё невозможное - возможно, знаю точно!
В какие города мне за тобой лететь // А где тебя искать, прошу ты мне ответь
Я готов на край Земли, я всё должен объяснить // В какие города мне за тобой лететь
Пойми, что без тебя я не умею жить // Я готов на край Земли, я всё должен объяснить
Я знаю точно - невозможное возможно // Пойми, что без тебя я не умею жить
Сойти с ума, влюбиться так неосторожно // Я знаю точно - невозможное возможно
Найти тебя, не отпускать ни днём, ни ночью // Сойти с ума, влюбиться так неосторожно
Всё невозможное - возможно, знаю точно! // Найти тебя, не отпускать ни днём, ни ночью
На-на-на-на (на-на-на-на), а-а, а-а // Всё невозможное - возможно, знаю точно!
На-на-на-на (на-на-на-на), а-а, а-а // На-на-на-на (на-на-на-на), а-а, а-а
Всё готов делить, с тобой я пополам // На-на-на-на (на-на-на-на), а-а, а-а
Ты только мне поверь, я сделал выбор сам // Всё готов делить, с тобой я пополам
Дай же мне последний шанс, я всё должен объяснить // Ты только мне поверь, я сделал выбор сам
Пойми, что без тебя я не умею жить // Дай же мне последний шанс, я всё должен объяснить
Я знаю точно - невозможное возможно // Пойми, что без тебя я не умею жить
Сойти с ума, влюбиться так неосторожно // Я знаю точно - невозможное возможно
Найти тебя, не отпускать ни днём, ни ночью // Сойти с ума, влюбиться так неосторожно
Всё невозможное - возможно, знаю точно! // Найти тебя, не отпускать ни днём, ни ночью
На-на-на-на (на-на-на-на), а-а, а-а // Всё невозможное - возможно, знаю точно!
На-на-на-на (на-на-на-на), а-а, а-а // На-на-на-на (на-на-на-на), а-а, а-а
Я знаю точно - невозможное возможно // На-на-на-на (на-на-на-на), а-а, а-а
Сойти с ума, влюбиться так неосторожно // Я знаю точно - невозможное возможно
Найти тебя, не отпускать ни днём, ни ночью // Сойти с ума, влюбиться так неосторожно
Всё невозможное - возможно, знаю точно! // Найти тебя, не отпускать ни днём, ни ночью
На-на-на-на (на-на-на-на), а-а, а-а // Всё невозможное - возможно, знаю точно!
На-на-на-на (на-на-на-на), а-а, а-а"""); // На-на-на-на (на-на-на-на), а-а, а-а
// На-на-на-на (на-на-на-на), а-а, а-а""");
render.setButton(ButtonNumber.BUTTON_1, "Hello world!\nHello world!\nHello world!\nHello world!\nHello world!", ctx -> {}); //
render.setButton(ButtonNumber.BUTTON_2, "I am a teapot", ctx -> {}); // render.setButton(ButtonNumber.BUTTON_1, "Всем привет!", ctx -> {});
render.setButton(ButtonNumber.BUTTON_3, "I love pizza\nMamma mia\nPeperoni\nPapa carlo\nZaebumba\nZaebumba", ctx -> {}); // render.setButton(ButtonNumber.BUTTON_2, "I am a teapot", ctx -> {});
render.setButton(ButtonNumber.BUTTON_4, "msb3 is top!", ctx -> {}); // render.setButton(ButtonNumber.BUTTON_3, "I love pizza\nMamma mia\nPeperoni\nPapa carlo\nZaebumba\nZaebumba", ctx -> {});
// render.setButton(ButtonNumber.BUTTON_4, "msb3 is top!", ctx -> {});
render.show(); //
// render.show();
}); });
} }
@Override @Override
public void onInitializeResources(ResourcePack resourcePack) { public void onInitializeResources(ResourcePack resourcePack) {
ClassPreLoader.preload(PictureBanner.class);
} }
@Override @Override

View File

@ -5,6 +5,7 @@ import lombok.experimental.UtilityClass;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import ru.dragonestia.msb3.api.entity.PickableItem;
import ru.dragonestia.msb3.api.item.ItemUtil; import ru.dragonestia.msb3.api.item.ItemUtil;
import ru.dragonestia.msb3.api.resource.DialogueResources; import ru.dragonestia.msb3.api.resource.DialogueResources;
import ru.dragonestia.msb3.api.resource.MonologueResources; import ru.dragonestia.msb3.api.resource.MonologueResources;
@ -77,6 +78,7 @@ public final class ServerBootstrap {
private void initDefaultModules() { private void initDefaultModules() {
ItemUtil.init(); ItemUtil.init();
PickableItem.registerEvent();
} }
private void compileResourcePack() { private void compileResourcePack() {

View File

@ -0,0 +1,36 @@
package ru.dragonestia.msb3.api.entity;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.ItemEntity;
import net.minestom.server.entity.Player;
import net.minestom.server.event.item.PickupItemEvent;
import net.minestom.server.item.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.time.temporal.ChronoUnit;
public class PickableItem extends ItemEntity {
public PickableItem(@NotNull ItemStack itemStack) {
super(itemStack);
setPickable(true);
setMergeable(false);
setPickupDelay(500, ChronoUnit.MILLIS);
}
public static void registerEvent() {
MinecraftServer.getGlobalEventHandler().addListener(PickupItemEvent.class, event -> {
if (!(event.getItemEntity() instanceof PickableItem)) return;
if (!(event.getEntity() instanceof Player player)) return;
var itemEntity = event.getItemEntity();
var inv = player.getInventory();
var item = itemEntity.getItemStack();
if (!inv.addItemStack(item)) {
event.setCancelled(true);
}
});
}
}

View File

@ -43,6 +43,8 @@ public class ResourcePackRepositoryModule {
server.start(); server.start();
MinecraftServer.getGlobalEventHandler().addListener(PlayerSpawnEvent.class, event -> { MinecraftServer.getGlobalEventHandler().addListener(PlayerSpawnEvent.class, event -> {
if (!event.isFirstSpawn()) return;
var player = event.getPlayer(); var player = event.getPlayer();
player.sendResourcePacks(ResourcePackRequest.resourcePackRequest() player.sendResourcePacks(ResourcePackRequest.resourcePackRequest()

View File

@ -0,0 +1,87 @@
package ru.dragonestia.msb3.api.ui;
import net.kyori.adventure.key.Key;
import net.minestom.server.entity.Player;
import net.minestom.server.inventory.Inventory;
import net.minestom.server.inventory.InventoryType;
import ru.dragonestia.msb3.api.resource.dialog.GlyphPositions;
import ru.dragonestia.msb3.api.util.ResourceFromJar;
import ru.dragonestia.msb3.resource.Resources;
import ru.dragonestia.msb3.resource.glyph.GlyphComponentBuilder;
import ru.dragonestia.msb3.resource.glyph.GlyphImage;
import ru.dragonestia.msb3.resource.utils.ImageUtils;
import team.unnamed.creative.base.Writable;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
public class PictureBanner {
public static final int CHEST_GUI_WIDTH = 176;
public static PictureBanner TEST = new PictureBanner("test", ResourceFromJar.of("glyphs/test_banner.png"));
private final GlyphImage glyph1;
private final GlyphImage glyph2;
private final GlyphImage glyph3;
private final GlyphImage glyph4;
public PictureBanner(String identifier, Writable writable) {
BufferedImage image;
Writable part1;
Writable part2;
Writable part3;
Writable part4;
try (var steam = new ByteArrayInputStream(writable.toByteArray())) {
image = ImageIO.read(steam);
var w = image.getWidth();
var h = image.getHeight();
part1 = ImageUtils.imageToWritable(image.getSubimage(0, 0, w / 2, h / 2));
part2 = ImageUtils.imageToWritable(image.getSubimage(w / 2, 0, w / 2, h / 2));
part3 = ImageUtils.imageToWritable(image.getSubimage(0, h / 2, w / 2, h / 2));
part4 = ImageUtils.imageToWritable(image.getSubimage(w / 2, h / 2, w / 2, h / 2));
} catch (IOException ex) {
throw new RuntimeException(ex);
}
glyph1 = Resources.createGlyph(
Key.key("msb3", "banner/" + identifier + "/1"),
part1,
GlyphPositions.guiBackground.height() / 2,
GlyphPositions.guiBackground.topPartsY()
);
glyph2 = Resources.createGlyph(
Key.key("msb3", "banner/" + identifier + "/2"),
part2,
GlyphPositions.guiBackground.height() / 2,
GlyphPositions.guiBackground.topPartsY()
);
glyph3 = Resources.createGlyph(
Key.key("msb3", "banner/" + identifier + "/3"),
part3,
GlyphPositions.guiBackground.height() / 2,
GlyphPositions.guiBackground.bottomPartsY()
);
glyph4 = Resources.createGlyph(
Key.key("msb3", "banner/" + identifier + "/4"),
part4,
GlyphPositions.guiBackground.height() / 2,
GlyphPositions.guiBackground.bottomPartsY()
);
}
public void show(Player player) {
var builder = new GlyphComponentBuilder();
builder.append(CHEST_GUI_WIDTH / 2 + 2 - glyph1.width(), glyph1);
builder.append(CHEST_GUI_WIDTH / 2 + 1, glyph2);
builder.append(CHEST_GUI_WIDTH / 2 + 2 - glyph3.width(), glyph3);
builder.append(CHEST_GUI_WIDTH / 2 + 1, glyph4);
var inv = new Inventory(InventoryType.CHEST_6_ROW, builder.build());
player.openInventory(inv);
}
}

View File

@ -2,11 +2,22 @@ package ru.dragonestia.msb3.api.world.chunk;
import net.minestom.server.instance.DynamicChunk; import net.minestom.server.instance.DynamicChunk;
import net.minestom.server.instance.Instance; import net.minestom.server.instance.Instance;
import net.minestom.server.instance.LightingChunk;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class OutOfBoundsChunk extends DynamicChunk { public interface OutOfBoundsChunk {
public OutOfBoundsChunk(@NotNull Instance instance, int chunkX, int chunkZ) { class Dynamic extends DynamicChunk implements OutOfBoundsChunk {
public Dynamic(@NotNull Instance instance, int chunkX, int chunkZ) {
super(instance, chunkX, chunkZ); super(instance, chunkX, chunkZ);
} }
} }
class Lighting extends LightingChunk implements OutOfBoundsChunk {
public Lighting(@NotNull Instance instance, int chunkX, int chunkZ) {
super(instance, chunkX, chunkZ);
}
}
}

View File

@ -30,7 +30,7 @@ public class PreloadedAnvilChunkLoader implements IChunkLoader {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
var sourceChunkData = source.provideChunk(chunkX, chunkZ); var sourceChunkData = source.provideChunk(chunkX, chunkZ);
if (sourceChunkData.isEmpty()) { if (sourceChunkData.isEmpty()) {
return new OutOfBoundsChunk(instance, chunkX, chunkZ); return new OutOfBoundsChunk.Dynamic(instance, chunkX, chunkZ);
} }
return new SharedChunk(instance, chunkX, chunkZ, sourceChunkData.get()); return new SharedChunk(instance, chunkX, chunkZ, sourceChunkData.get());
}); });

View File

@ -0,0 +1,16 @@
package ru.dragonestia.editor.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.dragonestia.editor.controller.mapper.DialogueMapper;
import java.util.List;
@RestController
public class DialogueController {
@GetMapping("/api/dialogues")
List<DialogueMapper> allDialogues() {
return List.of(); // TODO
}
}

View File

@ -0,0 +1,51 @@
package ru.dragonestia.editor.controller.mapper;
import ru.dragonestia.editor.model.DialogueContext;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public record DialogueMapper(
String groupId,
String id,
String text,
List<Answer> answers
) {
public static DialogueMapper fromEntity(DialogueContext context) {
var buttons = new ArrayList<Answer>();
for (var button: context.getAnswers()) {
var actionData = new HashMap<String, String>();
for (var actionParam: button.getAction().getFields()) {
actionData.put(actionParam.getIdentifier(), actionParam.getValue());
}
var conditions = new ArrayList<Condition>();
for (var condition: button.getConditions()) {
var conditionData = new HashMap<String, String>();
for (var param: condition.getFields()) {
conditionData.put(param.getIdentifier(), param.getValue());
}
conditions.add(new Condition(condition.getIdentifier(), conditionData));
}
buttons.add(new Answer(button.getIdentifier(), button.getText(), button.getAction().getIdentifier(), actionData, conditions));
}
return new DialogueMapper(context.getGroupId(), context.getGroupId(), context.getText(), buttons);
}
public record Answer(
String id,
String text,
String actionId,
Map<String, String> actionData,
List<Condition> conditions
) {}
public record Condition(
String id,
Map<String, String> data
) {}
}

View File

@ -0,0 +1,35 @@
package ru.dragonestia.editor.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class DialogueAction {
private String identifier;
private String name;
private String description;
private List<Field> fields = new ArrayList<>();
private boolean builtIn;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Field {
private String identifier;
private String name;
private String description;
private FieldType type;
private String defaultValue;
}
}

View File

@ -0,0 +1,31 @@
package ru.dragonestia.editor.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class DialogueCondition {
private String identifier;
private String name;
private String description;
private ArrayList<Field> fields = new ArrayList<>();
@Getter
@Setter
public static class Field {
private String identifier;
private String name;
private String description;
private FieldType type;
private String defaultValue;
}
}

View File

@ -3,11 +3,54 @@ package ru.dragonestia.editor.model;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.util.ArrayList;
@Getter @Getter
@Setter @Setter
public class DialogueContext { public class DialogueContext {
private String groupId;
private String id; private String id;
private String text; private String text;
private String comment; private String comment;
private ArrayList<Answer> answers = new ArrayList<>();
@Getter
@Setter
public static class Answer {
private String identifier;
private String text;
private String comment;
private Action action;
private ArrayList<Condition> conditions = new ArrayList<>();
}
@Getter
@Setter
public static class Action {
private String identifier;
private String name;
private ArrayList<Field> fields = new ArrayList<>();
}
@Getter
@Setter
public static class Condition {
private String identifier;
private String name;
private ArrayList<Field> fields = new ArrayList<>();
}
@Getter
@Setter
public static class Field {
private String identifier;
private String name;
private FieldType type;
private String value;
}
} }

View File

@ -0,0 +1,36 @@
package ru.dragonestia.editor.model;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.function.Predicate;
@Getter
@RequiredArgsConstructor
public enum FieldType {
STRING("Строка", input -> true),
TEXT("Текст", input -> true),
INTEGER("Целое число", input -> {
try {
Integer.parseInt(input);
return true;
} catch (Exception e) {
return false;
}
}),
BOOLEAN("Булево значение", input -> switch (input.toLowerCase()) {
case "true", "false" -> true;
default -> false;
}),
DOUBLE("Число с плавающей точкой", input -> {
try {
Double.parseDouble(input);
return true;
} catch (Exception e) {
return false;
}
});
private final String name;
private final Predicate<String> validator;
}

View File

@ -0,0 +1,97 @@
package ru.dragonestia.editor.page;
import com.vaadin.flow.component.Unit;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.value.ValueChangeMode;
import ru.dragonestia.editor.page.browser.BrowserPage;
import ru.dragonestia.editor.page.browser.TabContainer;
import java.util.ArrayList;
import java.util.List;
public class DialogueGroupPage extends TabContainer {
private final String groupIdentifier;
private final TextField fieldSearch;
private final Grid<DialogueEntry> grid;
public DialogueGroupPage(BrowserPage browser, String groupIdentifier) {
super(browser, "Группа: " + groupIdentifier);
this.groupIdentifier = groupIdentifier;
add(new H2(groupIdentifier));
add(new Paragraph(
"Здесь содержатся диалоги, которые связаны с данной группой. " +
"Нажмите на диалог в таблице чтобы редактировать или удалить его. " +
"Если хотите создать новый диалог, то нажмите кнопку для создания ниже. "
));
var buttonsLayout = new HorizontalLayout();
buttonsLayout.setWidth("100%");
add(buttonsLayout);
buttonsLayout.add(createButtonNewDialogue());
buttonsLayout.add(createButtonUpdateDialogues());
add(fieldSearch = createFieldSearch());
add(grid = createGrid());
// TODO: init grid data
grid.setItems(List.of(
new DialogueEntry("test1", ""),
new DialogueEntry("test2", "а ывп выпыв пку пукфпкаырп ыварукып куруке авыфп авып"),
new DialogueEntry("fsdfdsfds", "111"),
new DialogueEntry("hello", "Приветственный диалог с игроком")
));
}
private Button createButtonUpdateDialogues() {
var button = new Button("Обновить список диалогов", VaadinIcon.REFRESH.create(), event -> {
// TODO
});
button.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
return button;
}
private Button createButtonNewDialogue() {
var button = new Button("Создать новый диалог", VaadinIcon.PLUS.create(), e -> {
// TODO
});
button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS);
return button;
}
private TextField createFieldSearch() {
var field = new TextField();
field.setPlaceholder("Поиск группы по идентификатору");
field.setPrefixComponent(VaadinIcon.SEARCH.create());
field.setValueChangeMode(ValueChangeMode.EAGER);
field.setWidth(100, Unit.PERCENTAGE);
field.addValueChangeListener(e -> {
var input = e.getValue().trim().toLowerCase();
// TODO
});
return field;
}
private Grid<DialogueEntry> createGrid() {
var grid = new Grid<DialogueEntry>();
grid.addColumn(DialogueEntry::identifier).setHeader("Идентификатор").setWidth("15%").setFlexGrow(0);
grid.addColumn(DialogueEntry::comment).setHeader("Комментарий");
grid.addItemClickListener(e -> {
var dialogueIdentifier = e.getItem().identifier();
// TODO
browser.openTab(new DialoguePage(browser, groupIdentifier, "test_id"));
});
return grid;
}
public record DialogueEntry(String identifier, String comment) {}
}

View File

@ -0,0 +1,325 @@
package ru.dragonestia.editor.page;
import com.vaadin.flow.component.Text;
import com.vaadin.flow.component.Unit;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.html.ListItem;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.Scroller;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.IntegerField;
import com.vaadin.flow.component.textfield.NumberField;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.value.ValueChangeMode;
import ru.dragonestia.editor.model.DialogueAction;
import ru.dragonestia.editor.model.DialogueContext;
import ru.dragonestia.editor.model.FieldType;
import ru.dragonestia.editor.page.browser.BrowserPage;
import ru.dragonestia.editor.page.browser.TabContainer;
import java.util.*;
import java.util.function.Supplier;
public class DialoguePage extends TabContainer {
private final static Random random = new Random();
private final String groupIdentifier;
private final String dialogueIdentifier;
private final TextArea fieldText;
private final TextArea fieldComment;
private final Button buttonNewAnswer;
private final VerticalLayout layoutAnswers;
private final List<Answer> answers = new ArrayList<>();
private DialogueContext dialogueContext = new DialogueContext();
public DialoguePage(BrowserPage browser, String groupIdentifier, String dialogueIdentifier) {
super(browser, "Диалог: %s/%s".formatted(groupIdentifier, dialogueIdentifier));
this.groupIdentifier = groupIdentifier;
this.dialogueIdentifier = dialogueIdentifier;
var layout = new HorizontalLayout();
layout.setPadding(false);
layout.setWidth(100, Unit.PERCENTAGE);
add(layout);
var leftLayout = new VerticalLayout();
leftLayout.setWidth(45, Unit.PERCENTAGE);
leftLayout.setPadding(false);
layout.add(leftLayout);
var rightLayout = new VerticalLayout();
rightLayout.setPadding(false);
layout.add(rightLayout);
leftLayout.add(createFieldIdentifier());
leftLayout.add(fieldText = createFieldText());
leftLayout.add(createTextHelping());
var controlLayout = new HorizontalLayout();
controlLayout.setWidth(100, Unit.PERCENTAGE);
controlLayout.setPadding(false);
leftLayout.add(controlLayout);
controlLayout.add(createButtonSave());
controlLayout.add(createButtonDelete());
leftLayout.add(fieldComment = createFieldComment());
rightLayout.add(new H3("Ответы диалогов"));
rightLayout.add(buttonNewAnswer = createButtonNewAnswer());
layoutAnswers = new VerticalLayout();
layoutAnswers.setPadding(false);
rightLayout.add(layoutAnswers);
updateAnswers();
//add(new DialogEditor(new DialogueContext()));
}
private TextField createFieldIdentifier() {
var field = new TextField("Идентификатор группы/идентификатор диалога");
field.setHelperText("Идентификатор диалога изменять нельзя");
field.setValue("%s/%s".formatted(groupIdentifier, dialogueIdentifier));
field.setReadOnly(true);
field.setWidth(100, Unit.PERCENTAGE);
return field;
}
private TextArea createFieldText() {
var field = new TextArea("Текст диалога");
field.setHeight(20, Unit.REM);
field.setWidth(100, Unit.PERCENTAGE);
return field;
}
private VerticalLayout createTextHelping() {
var layout = new VerticalLayout();
layout.setPadding(false);
layout.add(new Text("Здесь описаны подсказки с плейсхолдерами для диалога:")); // TODO
layout.add(new ListItem("Плейсхолдер 1"));
layout.add(new ListItem("Плейсхолдер 2"));
layout.add(new ListItem("Плейсхолдер 3"));
return layout;
}
private TextArea createFieldComment() {
var field = new TextArea("Комментарий");
field.setHelperText("Комментарием может являться любая пометка с кратким описанием диалога. Этот комментарий видно в списке диалогов на сайте.");
field.setMinHeight(10, Unit.REM);
field.setWidth(100, Unit.PERCENTAGE);
return field;
}
private Button createButtonNewAnswer() {
var button = new Button("Добавить ответ диалога", VaadinIcon.PLUS.create());
button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS);
button.addClickListener(e -> {
var answer = new Answer();
answer.deletion = () -> deleteAnswer(answer);
answers.add(answer);
updateAnswers();
});
button.setWidth(100, Unit.PERCENTAGE);
return button;
}
private Button createButtonSave() {
var button = new Button("Сохранить", VaadinIcon.DATABASE.create(), e -> {
// TODO
});
button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS);
button.setWidth(49, Unit.PERCENTAGE);
return button;
}
private Button createButtonDelete() {
var button = new Button("Удалить", VaadinIcon.TRASH.create(), e -> sendDeletionConfirm());
button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR);
button.setWidth(49, Unit.PERCENTAGE);
return button;
}
private void sendDeletionConfirm() {
var dialog = new Dialog("Удаление диалога");
dialog.setWidth(30, Unit.REM);
dialog.getFooter().add(new Button("Отмена", e -> dialog.close()));
var randomNumber = Integer.toString(random.nextInt(100, 999));
var layout = new VerticalLayout();
dialog.add(layout);
layout.add(new Paragraph("Подтвердите что вы хотите удалить диалог. Введите код ниже в поле для ввода чтобы подтвердить удаление."));
var codeLayout = new H3(randomNumber);
layout.add(codeLayout);
var buttonConfirm = new Button("Подтвердить удаление", e -> {
// TODO
dialog.close();
});
buttonConfirm.setEnabled(false);
buttonConfirm.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR);
buttonConfirm.setWidth(100, Unit.PERCENTAGE);
var fieldConfirmation = new TextField();
fieldConfirmation.setPlaceholder("Введите код, написанный выше");
fieldConfirmation.setWidth(100, Unit.PERCENTAGE);
fieldConfirmation.setValueChangeMode(ValueChangeMode.EAGER);
fieldConfirmation.addValueChangeListener(e -> {
buttonConfirm.setEnabled(randomNumber.equals(e.getValue()));
});
layout.add(fieldConfirmation);
layout.add(buttonConfirm);
dialog.open();
}
private void updateAnswers() {
buttonNewAnswer.setEnabled(answers.size() < 4);
layoutAnswers.removeAll();
for (var answer: answers) {
layoutAnswers.add(answer);
}
}
private void deleteAnswer(Answer answer) {
answers.removeIf(target -> answer.uuid.equals(target.uuid));
updateAnswers();
}
public static class Answer extends HorizontalLayout {
private final UUID uuid = UUID.randomUUID();
private Runnable deletion;
private final VerticalLayout layoutAction;
public Answer() {
var layoutLeft = new VerticalLayout();
add(layoutLeft);
var layoutRight = new VerticalLayout();
add(layoutRight);
layoutAction = new VerticalLayout();
layoutAction.setPadding(false);
layoutLeft.add(createFieldText());
layoutLeft.add(createSelectAction());
layoutLeft.add(layoutAction);
layoutRight.add(createButtonDeleteAnswer());
layoutRight.add(createFieldIdentifier());
layoutRight.add(createFieldComment());
setWidth(100, Unit.PERCENTAGE);
getStyle().set("background-color", "#EEEEEE");
getStyle().set("border-radius", "26px");
}
private TextField createFieldText() {
var field = new TextField("Текст кнопки");
field.setWidth(100, Unit.PERCENTAGE);
return field;
}
private Button createButtonDeleteAnswer() {
var button = new Button("Удалить ответ", VaadinIcon.TRASH.create(), e -> {
if (deletion == null) return;
deletion.run();
});
button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR);
button.setWidth(100, Unit.PERCENTAGE);
return button;
}
private TextField createFieldIdentifier() {
var field = new TextField("Идентификатор кнопки");
field.setHelperText("Может быть пустым. Этот параметр нужен чтобы запоминать какие кнопки нажимал игрок чтобы потом делать различные проверки на это.");
field.setWidth(100, Unit.PERCENTAGE);
return field;
}
private TextArea createFieldComment() {
var field = new TextArea("Комментарий");
field.setHelperText("Комментарием может являться любая пометка с кратким описанием ответа диалога.");
field.setWidth(100, Unit.PERCENTAGE);
field.setHeight(10, Unit.REM);
return field;
}
private ComboBox<DialogueAction> createSelectAction() {
var comboBox = new ComboBox<DialogueAction>("Действие ответа диалога");
comboBox.setHelperText("Выполняется, когда игрок нажимает на кнопку выбора ответа внутри диалога");
comboBox.setWidth(100, Unit.PERCENTAGE);
comboBox.setItemLabelGenerator(action -> "%s [%s]".formatted(action.getName(), action.getIdentifier()));
var items = List.of(
new DialogueAction("close", "Закрыть диалог", "Просто закрывает диалог", new ArrayList<>(), true),
new DialogueAction("dialogue", "Перейти к диалогу", "Перейти к другому диалогу", List.of(
new DialogueAction.Field("groupId", "Id-группы диалога", "", FieldType.STRING, ""),
new DialogueAction.Field("dialogueId", "Id диалога", "", FieldType.STRING, "")
), true),
new DialogueAction("test_params", "Тест всех полей", "", List.of(
new DialogueAction.Field("param1", "Строка", "", FieldType.STRING, ""),
new DialogueAction.Field("param2", "Текст", "", FieldType.TEXT, ""),
new DialogueAction.Field("param3", "Булево значение", "", FieldType.BOOLEAN, ""),
new DialogueAction.Field("param4", "Целое число", "", FieldType.INTEGER, ""),
new DialogueAction.Field("param5", "Число с плавающей точкой", "", FieldType.DOUBLE, "")
), true)
);
comboBox.setItems(items);
comboBox.setValue(items.getFirst());
comboBox.addValueChangeListener(e -> {
updateAction(e.getValue(), new HashMap<>());
});
return comboBox;
}
private void updateAction(DialogueAction action, Map<String, String> params) {
layoutAction.removeAll();
var fieldMapper = new HashMap<String, Supplier<String>>();
for (var param: action.getFields()) {
var component = switch (param.getType()) {
case STRING -> {
var field = new TextField(param.getName());
field.setValue(param.getDefaultValue());
fieldMapper.put(param.getIdentifier(), field::getValue);
yield field;
}
case TEXT -> {
var field = new TextArea(param.getName());
field.setValue(param.getDefaultValue());
fieldMapper.put(param.getIdentifier(), field::getValue);
yield field;
}
case BOOLEAN -> {
var field = new Checkbox(param.getName());
field.setValue(Boolean.parseBoolean(param.getDefaultValue()));
fieldMapper.put(param.getIdentifier(), () -> Boolean.toString(field.getValue()));
yield field;
}
case INTEGER -> {
var field = new IntegerField(param.getName());
fieldMapper.put(param.getIdentifier(), () -> Integer.toString(field.getValue()));
yield field;
}
case DOUBLE -> {
var field = new NumberField(param.getName());
fieldMapper.put(param.getIdentifier(), () -> Double.toString(field.getValue()));
yield field;
}
};
component.setWidth(100, Unit.PERCENTAGE);
layoutAction.add(component);
}
}
}
}

View File

@ -0,0 +1,166 @@
package ru.dragonestia.editor.page;
import com.vaadin.flow.component.Html;
import com.vaadin.flow.component.Unit;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.*;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.value.ValueChangeMode;
import ru.dragonestia.editor.page.browser.BrowserPage;
import ru.dragonestia.editor.page.browser.TabContainer;
import java.util.ArrayList;
import java.util.function.BiConsumer;
public class HomePage extends TabContainer {
public HomePage(BrowserPage browser) {
super(browser, "Домашняя страница");
add(new H3("Диалоги"));
addLink("Управление группами диалогов", this::findOrCreateDialog);
addLink("Список всех групп диалогов", this::listAllDialogueGroups);
add(new H3("Действия ответов диалога"));
addLink("Создание/редактирование действий ответов диалога", this::findOrCreateDialogAction);
addLink("Список всех действий ответов диалога", this::listAllDialogueActions);
add(new H3("Условия ответов диалога"));
addLink("Создание/редактирование условий ответов диалога", this::findOrCreateDialogCondition);
addLink("Список всех ответов диалога", this::listAllDialogueConditions);
}
private void addLink(String label, BiConsumer<VerticalLayout, Runnable> content) {
var component = new Span(new Html("<a href='#'>" + label + "</a>"));
component.addClickListener(event -> {
var dialog = new Dialog();
dialog.setMinHeight(30, Unit.PERCENTAGE);
dialog.setWidth(40, Unit.PERCENTAGE);
dialog.getHeader().add(new H2(label));
var layout = new VerticalLayout();
layout.setPadding(false);
content.accept(layout, dialog::close);
dialog.add(layout);
var closeButton = new Button("Close");
closeButton.addThemeVariants();
closeButton.addClickListener(e -> dialog.close());
dialog.getFooter().add(closeButton);
dialog.open();
});
add(new ListItem(component));
}
private void findOrCreateDialog(VerticalLayout layout, Runnable closeWindow) {
layout.add(new Paragraph(
"Введите идентификатор группы диалога чтобы перейти к созданию или редактированию группы диалога. " +
"Группы диалогов нужны чтобы упростить группировку диалогов. "
));
var fieldIdentifier = new TextField("Идентификатор группы");
fieldIdentifier.setHelperText("Идентификатор может содержать только символы английского алфавита, цифры и символ нижнего подчеркивания");
fieldIdentifier.setWidth(100, Unit.PERCENTAGE);
layout.add(fieldIdentifier);
var buttons = new HorizontalLayout();
buttons.setWidth(100, Unit.PERCENTAGE);
buttons.setJustifyContentMode(JustifyContentMode.CENTER);
layout.add(buttons);
var buttonCheckGroup = new Button("Проверить существование группы", e -> {
// TODO
});
buttonCheckGroup.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
buttonCheckGroup.setWidth(49, Unit.PERCENTAGE);
buttons.add(buttonCheckGroup);
var createOrFind = new Button("Создать/редактировать", e -> {
var groupIdentifier = fieldIdentifier.getValue().trim().toLowerCase();
if (groupIdentifier.isEmpty()) return;
if (!groupIdentifier.matches("^[aA-zZ\\d_]+$")) {
Notification.show("Идентификатор группы содержит недопустимые символы.")
.addThemeVariants(NotificationVariant.LUMO_ERROR);
return;
}
// TODO
browser.openTab(new DialogueGroupPage(browser, groupIdentifier));
closeWindow.run();
});
createOrFind.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS);
createOrFind.setWidth(49, Unit.PERCENTAGE);
buttons.add(createOrFind);
}
private void listAllDialogueGroups(VerticalLayout layout, Runnable closeWindow) {
var buttonRefresh = new Button("Обновить список", VaadinIcon.REFRESH.create());
layout.add(buttonRefresh);
var fieldSearch = new TextField();
fieldSearch.setPlaceholder("Поиск группы по идентификатору");
fieldSearch.setPrefixComponent(VaadinIcon.SEARCH.create());
fieldSearch.setValueChangeMode(ValueChangeMode.EAGER);
fieldSearch.setWidth(100, Unit.PERCENTAGE);
layout.add(fieldSearch);
var grid = new Grid<DialogGroupEntry>();
var data = new ArrayList<DialogGroupEntry>();
// TODO: initial data loading
grid.setItems(data);
grid.addColumn(DialogGroupEntry::id).setHeader("Id группы").setWidth("25%").setFlexGrow(0);
grid.addColumn(DialogGroupEntry::dialoguesInside).setHeader("Кол-во диалогов").setWidth("20%").setFlexGrow(0);
grid.addComponentColumn(obj -> new Paragraph(obj.comment())).setHeader("Комментарий");
grid.addItemClickListener(e -> {
var item = e.getItem();
// TODO: open dialogue group
closeWindow.run();
});
layout.add(grid);
fieldSearch.addValueChangeListener(e -> {
var input = e.getValue().trim().toLowerCase();
var newList = new ArrayList<>(data);
newList.removeIf(entry -> !entry.id().toLowerCase().startsWith(input));
grid.setItems(newList);
});
buttonRefresh.addClickListener(e -> {
// TODO
});
layout.add(new Paragraph(
"В этом списке отображаются группы, которые имеют хоть какое-то количество диалогов внутри. " +
"Нажмите на строчку с диалогом чтобы заглянуть внутрь. "
));
}
private void findOrCreateDialogAction(VerticalLayout layout, Runnable closeWindow) {
}
private void listAllDialogueActions(VerticalLayout layout, Runnable closeWindow) {
}
private void findOrCreateDialogCondition(VerticalLayout layout, Runnable closeWindow) {
}
private void listAllDialogueConditions(VerticalLayout layout, Runnable closeWindow) {
}
private record DialogGroupEntry(String id, int dialoguesInside, String comment) {}
}

View File

@ -1,81 +0,0 @@
package ru.dragonestia.editor.page;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.BeforeLeaveEvent;
import com.vaadin.flow.router.BeforeLeaveObserver;
import java.util.Optional;
public class Page extends VerticalLayout implements BeforeEnterObserver, BeforeLeaveObserver {
protected void init(QueryParams params) {}
protected void destroy() {}
@Override
public final void beforeEnter(BeforeEnterEvent event) {
init(key -> event.getRouteParameters().get(key));
}
@Override
public final void beforeLeave(BeforeLeaveEvent event) {
destroy();
}
public interface QueryParams {
Optional<String> String(String key);
default Optional<Integer> Integer(String key) {
return String(key).map(str -> {
try {
return Integer.parseInt(str);
} catch (Exception ex) {
return null;
}
});
}
default Optional<Long> Long(String key) {
return String(key).map(str -> {
try {
return Long.parseLong(str);
} catch (Exception ex) {
return null;
}
});
}
default Optional<Float> Float(String key) {
return String(key).map(str -> {
try {
return Float.parseFloat(str);
} catch (Exception ex) {
return null;
}
});
}
default Optional<Double> Double(String key) {
return String(key).map(str -> {
try {
return Double.parseDouble(str);
} catch (Exception ex) {
return null;
}
});
}
default Optional<Boolean> Boolean(String key) {
return String(key).map(str -> {
try {
return Boolean.parseBoolean(str);
} catch (Exception ex) {
return null;
}
});
}
}
}

View File

@ -1,16 +0,0 @@
package ru.dragonestia.editor.page;
import com.vaadin.flow.router.Route;
import jakarta.annotation.PostConstruct;
import ru.dragonestia.editor.component.DialogEditor;
import ru.dragonestia.editor.model.DialogueContext;
@Route("/")
public class TestPage extends Page {
@PostConstruct
void init() {
var ctx = new DialogueContext();
add(new DialogEditor(ctx));
}
}

View File

@ -0,0 +1,57 @@
package ru.dragonestia.editor.page.browser;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Unit;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.tabs.Tab;
import com.vaadin.flow.component.tabs.TabSheet;
import com.vaadin.flow.router.Route;
import ru.dragonestia.editor.page.HomePage;
@Route("/")
public class BrowserPage extends VerticalLayout {
private final TabSheet tabs;
public BrowserPage() {
tabs = new TabSheet();
tabs.setWidth(100, Unit.PERCENTAGE);
//tabs.setSuffixComponent(createNewTabButton());
add(tabs);
openHomePage();
}
private Component createNewTabButton() {
return new Button(VaadinIcon.PLUS.create(), event -> openHomePage());
}
private void openHomePage() {
var tab = new Tab(new Span(VaadinIcon.HOME.create()));
var style = tab.getStyle();
style.setPaddingTop("0rem");
style.setPaddingBottom("0rem");
tabs.add(tab, new HomePage(this));
}
public void openTab(TabContainer container) {
var tab = new Tab();
var style = tab.getStyle();
style.setPaddingTop("0rem");
style.setPaddingBottom("0rem");
tab.setLabel(container.getTitle());
container.setTab(tab);
var closeTabButton = new Span(VaadinIcon.CLOSE_SMALL.create());
closeTabButton.getStyle().setMarginLeft("1rem");
closeTabButton.addClickListener(event -> tabs.remove(tab));
tab.add(closeTabButton);
tabs.add(tab, container);
tabs.setSelectedTab(tab);
}
}

View File

@ -0,0 +1,25 @@
package ru.dragonestia.editor.page.browser;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.tabs.Tab;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public abstract class TabContainer extends VerticalLayout {
protected final BrowserPage browser;
private String title;
private Tab tab;
public TabContainer(BrowserPage browser, String title) {
this.browser = browser;
this.title = title;
}
public void setTitle(String title) {
this.title = title;
tab.setLabel(title);
}
}

View File

@ -14,5 +14,8 @@ spring:
hibernate: hibernate:
ddl-auto: validate ddl-auto: validate
mvc:
static-path-pattern: '/static/**'
server: server:
port: 8080 port: 8080