diff --git a/README.md b/README.md index a2c66b20..24766a32 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ This mod works in multiplayer, but may be considered cheating on some servers, s |----------------|-------------------------------------------------------------------------------------------------------------------------|--------------| | Toggle Freecam | Enables/disables Freecam | `F4` | | Config GUI | Opens the settings screen. | `Unbound` | +| Goto GUI | Opens a "goto" screen, which allows jumping Freecam to any player within range. | `G` | | Control Player | Transfers control back to your player, but retains your current perspective (Can only be used while Freecam is active.) | `Unbound` | | Reset Tripod | Resets a tripod\* camera when pressed in combination with any of the hotbar keys | `Unbound` | diff --git a/common/src/main/java/net/xolt/freecam/Freecam.java b/common/src/main/java/net/xolt/freecam/Freecam.java index a8779b3e..dcd7e81a 100644 --- a/common/src/main/java/net/xolt/freecam/Freecam.java +++ b/common/src/main/java/net/xolt/freecam/Freecam.java @@ -10,6 +10,7 @@ import net.minecraft.world.entity.Entity; import net.minecraft.world.level.ChunkPos; import net.xolt.freecam.config.ModConfig; +import net.xolt.freecam.gui.go.GotoScreen; import net.xolt.freecam.tripod.TripodRegistry; import net.xolt.freecam.tripod.TripodSlot; import net.xolt.freecam.util.FreeCamera; @@ -17,6 +18,9 @@ import net.xolt.freecam.variant.api.BuildVariant; import org.jetbrains.annotations.Nullable; +import java.util.Timer; +import java.util.TimerTask; + import static net.xolt.freecam.config.ModBindings.*; public class Freecam { @@ -94,6 +98,10 @@ else if (KEY_TOGGLE.consumeClick() || toggleKeyHeldTicks > 0) { switchControls(); } + while (KEY_GOTO_GUI.consumeClick()) { + mc.setScreen(new GotoScreen()); + } + while (KEY_CONFIG_GUI.consumeClick()) { mc.setScreen(AutoConfig.getConfigScreen(ModConfig.class, mc.screen).get()); } @@ -123,6 +131,35 @@ public static void toggle() { } } + public static void gotoPosition(FreecamPosition position, String name, boolean perspective) { + long notificationDelay = tripodEnabled || !freecamEnabled ? 1500 : 1; + + if (tripodEnabled) { + toggleTripod(activeTripod); + } + + if (!freecamEnabled) { + toggle(); + } + + // FIXME integrate with moveTo*() + freeCamera.applyPosition(position); + if (perspective) { + freeCamera.applyPerspective(ModConfig.INSTANCE.hidden.gotoPlayerPerspective, checkInitialCollision()); + } + + if (ModConfig.INSTANCE.notification.notifyGoto) { + new Timer().schedule(new TimerTask() { + @Override + public void run() { + if (freecamEnabled && MC.player != null) { + MC.player.displayClientMessage(Component.translatable("msg.freecam.gotoPosition", name), true); + } + } + }, notificationDelay); + } + } + private static void toggleTripod(TripodSlot tripod) { if (tripod == TripodSlot.NONE) { return; @@ -296,10 +333,11 @@ public static void moveToPlayer() { return; } freeCamera.copyPosition(MC.player); - freeCamera.applyPerspective( - ModConfig.INSTANCE.visual.perspective, - ModConfig.INSTANCE.collision.alwaysCheck || !(ModConfig.INSTANCE.collision.ignoreAll && BuildVariant.getInstance().cheatsPermitted()) - ); + freeCamera.applyPerspective(ModConfig.INSTANCE.visual.perspective, checkInitialCollision()); + } + + private static boolean checkInitialCollision() { + return ModConfig.INSTANCE.collision.alwaysCheck || !(ModConfig.INSTANCE.collision.ignoreAll && BuildVariant.getInstance().cheatsPermitted()); } public static FreeCamera getFreeCamera() { diff --git a/common/src/main/java/net/xolt/freecam/config/ModBindings.java b/common/src/main/java/net/xolt/freecam/config/ModBindings.java index c7cafba4..af0ce908 100644 --- a/common/src/main/java/net/xolt/freecam/config/ModBindings.java +++ b/common/src/main/java/net/xolt/freecam/config/ModBindings.java @@ -12,14 +12,14 @@ import java.util.Spliterator; import java.util.function.Consumer; -import static org.lwjgl.glfw.GLFW.GLFW_KEY_F4; -import static org.lwjgl.glfw.GLFW.GLFW_KEY_UNKNOWN; +import static org.lwjgl.glfw.GLFW.*; public enum ModBindings { KEY_TOGGLE("toggle", GLFW_KEY_F4), KEY_PLAYER_CONTROL("playerControl"), KEY_TRIPOD_RESET("tripodReset"), + KEY_GOTO_GUI("goto", GLFW_KEY_G), KEY_CONFIG_GUI("configGui"); private final Supplier lazyMapping; @@ -57,6 +57,14 @@ public boolean consumeClick() { return get().consumeClick(); } + /** + * @return the result of calling {@link KeyMapping#matches(int, int)} on the represented {@link KeyMapping}. + * @see KeyMapping#matches(int, int) + */ + public boolean matches(int keyCode, int scanCode) { + return get().matches(keyCode, scanCode); + } + /** * Reset whether the key was pressed. *

diff --git a/common/src/main/java/net/xolt/freecam/config/ModConfig.java b/common/src/main/java/net/xolt/freecam/config/ModConfig.java index 34cca815..9f840a8e 100644 --- a/common/src/main/java/net/xolt/freecam/config/ModConfig.java +++ b/common/src/main/java/net/xolt/freecam/config/ModConfig.java @@ -7,6 +7,8 @@ import me.shedaniel.autoconfig.annotation.ConfigEntry.Gui.EnumHandler.EnumDisplayOption; import me.shedaniel.autoconfig.serializer.JanksonConfigSerializer; import me.shedaniel.clothconfig2.gui.entries.SelectionListEntry; +import org.jetbrains.annotations.NotNull; + import net.xolt.freecam.variant.api.BuildVariant; @Config(name = "freecam") @@ -104,6 +106,15 @@ public static class NotificationConfig { @ConfigEntry.Gui.Tooltip public boolean notifyTripod = true; + + @ConfigEntry.Gui.Tooltip + public boolean notifyGoto = true; + } + + @ConfigEntry.Gui.Excluded + public Hidden hidden = new Hidden(); + public static class Hidden { + public Perspective gotoPlayerPerspective = Perspective.THIRD_PERSON; } public enum FlightMode implements SelectionListEntry.Translatable { diff --git a/common/src/main/java/net/xolt/freecam/gui/go/GotoScreen.java b/common/src/main/java/net/xolt/freecam/gui/go/GotoScreen.java new file mode 100644 index 00000000..7bfa32e3 --- /dev/null +++ b/common/src/main/java/net/xolt/freecam/gui/go/GotoScreen.java @@ -0,0 +1,234 @@ +package net.xolt.freecam.gui.go; + +import me.shedaniel.autoconfig.AutoConfig; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.CycleButton; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.layouts.FrameLayout; +import net.minecraft.client.gui.layouts.LinearLayout; +import net.minecraft.client.gui.navigation.CommonInputs; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.FastColor; +import net.xolt.freecam.Freecam; +import net.xolt.freecam.config.ModConfig; +import net.xolt.freecam.gui.Texture; +import net.xolt.freecam.util.FreeCamera; +import org.lwjgl.glfw.GLFW; + +import java.util.*; +import java.util.stream.Collectors; + +import static net.xolt.freecam.config.ModBindings.KEY_GOTO_GUI; + +public class GotoScreen extends Screen { + public static final int GRAY_COLOR = FastColor.ARGB32.color(255, 74, 74, 74); + public static final int WHITE_COLOR = FastColor.ARGB32.color(255, 255, 255, 255); + private static final int GUI_WIDTH = 236; + private static final int GUI_TOP = 50; + private static final int LIST_TOP = GUI_TOP + 8; + private static final int LIST_ITEM_HEIGHT = 36; + private static final ResourceLocation SEARCH_ICON_TEXTURE = new ResourceLocation("icon/search"); + private static final Component SEARCH_TEXT = Component.translatable("gui.recipebook.search_hint").withStyle(ChatFormatting.ITALIC).withStyle(ChatFormatting.GRAY); + private static final Texture JUMP_BACKGROUND = new Texture(new ResourceLocation(Freecam.MOD_ID, "textures/gui/goto_background.png")); + private static final Texture JUMP_LIST_BACKGROUND = new Texture(new ResourceLocation(Freecam.MOD_ID, "textures/gui/goto_list_background.png")); + + private ListWidget list; + private boolean initialized; + private Button buttonBack; + private Button buttonJump; + private CycleButton buttonPerspective; + private EditBox searchBox; + private String currentSearch; + + public GotoScreen() { + super(Component.translatable("gui.freecam.goto.title")); + } + + @Override + protected void init() { + super.init(); + + if (!this.initialized) { + this.list = new ListWidget(this, this.minecraft, 0, this.height, LIST_ITEM_HEIGHT); + this.searchBox = new EditBox(this.font, 0, 15, SEARCH_TEXT); + this.searchBox.setHint(SEARCH_TEXT); + this.searchBox.setMaxLength(16); + this.searchBox.setVisible(true); + this.searchBox.setTextColor(0xFFFFFF); + this.searchBox.setResponder(this::onSearchChange); + + this.buttonJump = Button.builder(Component.translatable("gui.freecam.goto.button.go"), button -> this.go()) + .tooltip(Tooltip.create(Component.translatable("gui.freecam.goto.button.go.@Tooltip"))) + .width(48) + .build(); + + this.buttonPerspective = CycleButton + .builder((ModConfig.Perspective value) -> Component.translatable(value.getKey())) + .withValues(ModConfig.Perspective.values()) + .withInitialValue(ModConfig.INSTANCE.hidden.gotoPlayerPerspective) + .withTooltip(value -> Tooltip.create(Component.translatable("gui.freecam.goto.button.perspective.@Tooltip"))) + .displayOnlyValue() + .create(0, 0, 80, 20, null, (button, value) -> { + ModConfig.INSTANCE.hidden.gotoPlayerPerspective = value; + AutoConfig.getConfigHolder(ModConfig.class).save(); + }); + + this.buttonBack = Button.builder(CommonComponents.GUI_BACK, button -> this.onClose()).width(48).build(); + } + + int listTop = LIST_TOP + 16; + int listBottom = this.getListBottom(); + int innerWidth = GUI_WIDTH - 10; + int innerX = (this.width - innerWidth) / 2; + + this.list.setY(listTop); + this.list.setSize(this.width, listBottom - listTop); + this.searchBox.setPosition(innerX + 20, LIST_TOP + 1); + this.searchBox.setWidth(this.list.getRowWidth() - 19); + + FrameLayout positioner = new FrameLayout(innerX, listBottom + 3, innerWidth, 0); + positioner.defaultChildLayoutSetting() + .alignVerticallyBottom() + .alignHorizontallyRight(); + LinearLayout layout = positioner.addChild(LinearLayout.horizontal()); + layout.defaultCellSetting() + .alignVerticallyBottom() + .paddingHorizontal(2); + + layout.addChild(this.buttonBack); + layout.addChild(this.buttonPerspective); + layout.addChild(this.buttonJump); + + positioner.arrangeElements(); + positioner.visitWidgets(this::addRenderableWidget); + + List.of(this.searchBox, this.list).forEach(this::addRenderableWidget); + this.setInitialFocus(this.list); + + this.initialized = true; + } + + @Override + public void renderBackground(GuiGraphics gfx, int mouseX, int mouseY, float delta) { + super.renderBackground(gfx, mouseX, mouseY, delta); + int left = (this.width - GUI_WIDTH) / 2; + JUMP_BACKGROUND.draw(gfx, left, GUI_TOP, 0, GUI_WIDTH, this.getGuiHeight()); + JUMP_LIST_BACKGROUND.draw(gfx, left + 7, LIST_TOP - 1, 0, this.list.getRowWidth() + 2, this.getListHeight() + 2); + gfx.blitSprite(SEARCH_ICON_TEXTURE, left + 10, LIST_TOP + 3, 12, 12); + } + + @Override + public void tick() { + super.tick(); + if (this.initialized) { + this.updateEntries(); + } + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (this.searchBox.isFocused()) { + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + this.magicalSpecialHackyFocus(null); + return true; + } + } else { + if (KEY_GOTO_GUI.matches(keyCode, scanCode)) { + this.onClose(); + return true; + } + } + if (this.list.getSelected() != null) { + if (CommonInputs.selected(keyCode)) { + this.go(); + return true; + } + if (this.list.keyPressed(keyCode, scanCode, modifiers)) { + return true; + } + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean isPauseScreen() { + return false; + } + + public void updateEntries() { + List entries = calculatePlayerEntries().stream() + .filter(entry -> this.currentSearch == null + || this.currentSearch.isEmpty() + || entry.matchesSearch(this.currentSearch)) + .sorted() + .toList(); + + // Update only if the list has changed + if (!Objects.equals(this.list.children(), entries)) { + this.list.updateEntries(entries); + } + } + + private List calculatePlayerEntries() { + // Store the existing entries in a UUID map for easy lookup + Map currentEntries = this.list.children() + .parallelStream() + .filter(PlayerListEntry.class::isInstance) + .map(PlayerListEntry.class::cast) + .collect(Collectors.toUnmodifiableMap(PlayerListEntry::getUUID, entry -> entry)); + + // Map the in-range players into PlayerListEntries + // Use existing entries if possible + return this.minecraft.level.players() + .parallelStream() + .filter(player -> !(player instanceof FreeCamera)) + .map(player -> Objects.requireNonNullElseGet( + currentEntries.get(player.getUUID()), + () -> new PlayerListEntry(this.minecraft, this, player))) + .map(ListEntry.class::cast) + .toList(); + } + + public void go() { + Optional.ofNullable(this.list.getSelected()) + .ifPresent(listEntry -> { + boolean perspective = this.buttonPerspective != null && this.buttonPerspective.active; + this.onClose(); + Freecam.gotoPosition(listEntry.getPosition(), listEntry.getName(), perspective); + }); + } + + public void select(ListEntry entry) { + this.list.setSelected(entry); + this.updateButtonState(); + } + + public void updateButtonState() { + ListEntry selected = this.list.getSelected(); + this.buttonJump.active = selected != null; + } + + private void onSearchChange(String search) { + this.currentSearch = search.toLowerCase(Locale.ROOT); + } + + // GUI height + private int getGuiHeight() { + return Math.max(52, this.height - (GUI_TOP * 2)); + } + + // List height including search bar + private int getListHeight() { + return this.getGuiHeight() - 29 - 8; + } + + private int getListBottom() { + return LIST_TOP + this.getListHeight(); + } +} diff --git a/common/src/main/java/net/xolt/freecam/gui/go/ListEntry.java b/common/src/main/java/net/xolt/freecam/gui/go/ListEntry.java new file mode 100644 index 00000000..5721f7d0 --- /dev/null +++ b/common/src/main/java/net/xolt/freecam/gui/go/ListEntry.java @@ -0,0 +1,63 @@ +package net.xolt.freecam.gui.go; + +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.ObjectSelectionList; +import net.minecraft.network.chat.Component; +import net.xolt.freecam.util.FreecamPosition; +import org.jetbrains.annotations.NotNull; + +import java.util.Locale; + +import static net.xolt.freecam.gui.go.GotoScreen.GRAY_COLOR; + +public abstract class ListEntry extends ObjectSelectionList.Entry implements Comparable { + + private static final long DOUBLE_CLICK_TIME = 250; + + protected final Minecraft client; + protected final GotoScreen screen; + private long lastClicked; + + protected ListEntry(Minecraft client, GotoScreen screen) { + this.client = client; + this.screen = screen; + } + + @Override + public void render(GuiGraphics gfx, int index, int y, int x, int fullEntryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + // We are passed a reduced entryHeight but the full entryWidth... + int entryWidth = fullEntryWidth - 4; + gfx.fill(x, y, x + entryWidth, y + entryHeight, GRAY_COLOR); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + long time = Util.getMillis(); + this.screen.select(this); + if (time - this.lastClicked < DOUBLE_CLICK_TIME) { + this.screen.go(); + } + this.lastClicked = time; + return false; + } + + @Override + public @NotNull Component getNarration() { + return null; + } + + public abstract FreecamPosition getPosition(); + + public abstract String getName(); + + public boolean matchesSearch(String string) { + return this.getName().toLowerCase(Locale.ROOT).contains(string); + } + + @Override + public int compareTo(@NotNull ListEntry entry) { + return this.getName().compareTo(entry.getName()); + } +} diff --git a/common/src/main/java/net/xolt/freecam/gui/go/ListWidget.java b/common/src/main/java/net/xolt/freecam/gui/go/ListWidget.java new file mode 100644 index 00000000..f029ecb0 --- /dev/null +++ b/common/src/main/java/net/xolt/freecam/gui/go/ListWidget.java @@ -0,0 +1,88 @@ +package net.xolt.freecam.gui.go; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.ObjectSelectionList; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.function.Predicate; + +public class ListWidget extends ObjectSelectionList { + private final GotoScreen screen; + + public ListWidget(GotoScreen screen, Minecraft client, int top, int height, int itemHeight) { + super(client, screen.width, height, top, itemHeight); + this.screen = screen; + this.setRenderBackground(false); + } + + public void updateEntries(List newEntries) { + ListEntry selection = migrateSelection(this.getSelected(), newEntries, this.children()); + this.replaceEntries(newEntries); + this.setSelected(selection); + } + + @Override + public void setSelected(@Nullable ListEntry entry) { + super.setSelected(entry); + this.screen.updateButtonState(); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + ListEntry entry = this.getSelected(); + return entry != null && entry.keyPressed(keyCode, scanCode, modifiers) || super.keyPressed(keyCode, scanCode, modifiers); + } + + private static @Nullable ListEntry migrateSelection(ListEntry selection, List newEntries, List oldEntries) { + // New list is empty, can't select anything + if (newEntries.isEmpty()) { + return null; + } + + // No previous selection existed, nothing to check + if (selection == null) { + return newEntries.get(0); + } + + int len = oldEntries.size(); + int index = oldEntries.indexOf(selection); + + // Helper function to check candidates + Predicate check = i -> { + ListEntry entry = oldEntries.get(i); + return newEntries.contains(entry); + }; + + // Check if the previous selection is still present + if (index >= 0 && index < len && check.test(index)) { + return selection; + } + + // Use a "nearest neighbor" style search when the selection is lost, + // to minimise GUI focus jumps and improve UX. + int dec = index; + int inc = index; + while (true) { + dec--; + inc++; + + // Failure: out-of-bounds in both directions + // terminate the loop + if (dec < 0 && inc >= len) { + return newEntries.get(0); + } + + // Check for a lower neighbor + if (dec >= 0 && check.test(dec)) { + return oldEntries.get(dec); + } + + // Check for a higher neighbor + if (inc < len && check.test(inc)) { + return oldEntries.get(inc); + } + + } + } +} diff --git a/common/src/main/java/net/xolt/freecam/gui/go/PlayerListEntry.java b/common/src/main/java/net/xolt/freecam/gui/go/PlayerListEntry.java new file mode 100644 index 00000000..4923402f --- /dev/null +++ b/common/src/main/java/net/xolt/freecam/gui/go/PlayerListEntry.java @@ -0,0 +1,92 @@ +package net.xolt.freecam.gui.go; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.PlayerFaceRenderer; +import net.minecraft.client.player.AbstractClientPlayer; +import net.minecraft.client.resources.PlayerSkin; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.xolt.freecam.util.FreecamPosition; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.UUID; +import java.util.function.Supplier; + +public class PlayerListEntry extends ListEntry { + + private final @Nullable Supplier skinSupplier; + private final Component name; + private final AbstractClientPlayer player; + + public PlayerListEntry(Minecraft mc, GotoScreen screen, AbstractClientPlayer player) { + super(mc, screen); + this.player = player; + + MutableComponent name = Component.literal(player.getScoreboardName()); + if (Objects.equals(player, mc.player)) { + name = Component.translatable("gui.freecam.goto.entry.player.you", name); + } + this.name = name; + + var playerInfo = mc.player.connection.getPlayerInfo(player.getUUID()); + this.skinSupplier = playerInfo == null ? null : playerInfo::getSkin; + } + + @Override + public void render(GuiGraphics gfx, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + super.render(gfx, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickDelta); + + int padding = 4; + int skinSize = 24; + boolean hasSkin = this.skinSupplier != null; + + if (hasSkin) { + int skinX = x + padding; + int skinY = y + (entryHeight - skinSize) / 2; + PlayerFaceRenderer.draw(gfx, this.skinSupplier.get(), skinX, skinY, skinSize); + } + + int textX = x + padding + (hasSkin ? skinSize + padding : 0); + int textY = y + (entryHeight - this.client.font.lineHeight) / 2; + gfx.drawString(this.client.font, this.name, textX, textY, GotoScreen.WHITE_COLOR, false); + } + + public UUID getUUID() { + return this.player.getUUID(); + } + + @Override + public String getName() { + return this.name.getString(); + } + + @Override + public FreecamPosition getPosition() { + return new FreecamPosition(this.player); + } + + @Override + public int compareTo(@NotNull ListEntry entry) { + // Sort before non-player entries + if (!(entry instanceof PlayerListEntry playerEntry)) { + return -1; + } + + // Sort mc.player before other players + if (Objects.equals(this.getUUID(), playerEntry.getUUID())) { + return 0; + } + if (Objects.equals(this.getUUID(), this.client.player.getUUID())) { + return -1; + } + if (Objects.equals(playerEntry.getUUID(), this.client.player.getUUID())) { + return 1; + } + + // Fallback to default comparison + return super.compareTo(entry); + } +} diff --git a/common/src/main/java/net/xolt/freecam/util/FreecamPosition.java b/common/src/main/java/net/xolt/freecam/util/FreecamPosition.java index f306ca6a..c10b0981 100644 --- a/common/src/main/java/net/xolt/freecam/util/FreecamPosition.java +++ b/common/src/main/java/net/xolt/freecam/util/FreecamPosition.java @@ -6,6 +6,9 @@ import org.joml.Quaternionf; import org.joml.Vector3f; +import java.text.DecimalFormat; +import java.text.NumberFormat; + public class FreecamPosition { public double x; public double y; @@ -66,6 +69,12 @@ public ChunkPos getChunkPos() { return new ChunkPos((int) (x / 16), (int) (z / 16)); } + // FIXME is this used? + public String coords() { + NumberFormat fmt = new DecimalFormat("0.##"); + return "x: %s, y: %s, z: %s".formatted(fmt.format(this.x), fmt.format(this.y), fmt.format(this.z)); + } + private static double getSwimmingY(Entity entity) { if (entity.getPose() == Pose.SWIMMING) { return entity.getY(); diff --git a/common/src/main/resources/assets/freecam/lang/en_us.json b/common/src/main/resources/assets/freecam/lang/en_us.json index 2c8460c0..cc274042 100644 --- a/common/src/main/resources/assets/freecam/lang/en_us.json +++ b/common/src/main/resources/assets/freecam/lang/en_us.json @@ -3,13 +3,19 @@ "key.freecam.toggle": "Toggle Freecam", "key.freecam.playerControl": "Control Player", "key.freecam.tripodReset": "Reset Tripod", + "key.freecam.goto": "Goto GUI", "key.freecam.configGui": "Config GUI", "msg.freecam.enable": "Freecam has been enabled.", "msg.freecam.disable": "Freecam has been disabled.", "msg.freecam.openTripod": "Opening camera %s", "msg.freecam.closeTripod": "Closing camera %s", "msg.freecam.tripodReset": "Reset camera %s", - "text.autoconfig.freecam.title": "Freecam Options", + "msg.freecam.gotoPosition": "Freecam jumped to %s.", + "gui.freecam.goto.title": "Freecam Goto...", + "gui.freecam.goto.button.go": "Go", + "gui.freecam.goto.button.go.@Tooltip": "Go to the selected target.", + "gui.freecam.goto.button.perspective.@Tooltip": "Initial perspective on target player.", + "gui.freecam.goto.entry.player.you": "§l%s§r§o (you)", "text.autoconfig.freecam.option.movement": "Movement Options", "text.autoconfig.freecam.option.movement.@Tooltip": "How the camera moves.", "text.autoconfig.freecam.option.movement.flightMode": "Flight Mode", @@ -71,5 +77,7 @@ "text.autoconfig.freecam.option.notification.notifyFreecam": "Freecam Notifications", "text.autoconfig.freecam.option.notification.notifyFreecam.@Tooltip": "Notifies you when entering/exiting freecam.", "text.autoconfig.freecam.option.notification.notifyTripod": "Tripod Notifications", - "text.autoconfig.freecam.option.notification.notifyTripod.@Tooltip": "Notifies you when entering/exiting tripod cameras." + "text.autoconfig.freecam.option.notification.notifyTripod.@Tooltip": "Notifies you when entering/exiting tripod cameras.", + "text.autoconfig.freecam.option.notification.notifyGoto": "Goto Notifications", + "text.autoconfig.freecam.option.notification.notifyGoto.@Tooltip": "Notifies you when using \"Goto\" to teleport the camera." } diff --git a/common/src/main/resources/assets/freecam/textures/gui/goto_background.png b/common/src/main/resources/assets/freecam/textures/gui/goto_background.png new file mode 100644 index 00000000..dff39ff9 Binary files /dev/null and b/common/src/main/resources/assets/freecam/textures/gui/goto_background.png differ diff --git a/common/src/main/resources/assets/freecam/textures/gui/goto_background.png.mcmeta b/common/src/main/resources/assets/freecam/textures/gui/goto_background.png.mcmeta new file mode 100644 index 00000000..234f4568 --- /dev/null +++ b/common/src/main/resources/assets/freecam/textures/gui/goto_background.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 9, + "height": 9, + "border": 4 + } + } +} diff --git a/common/src/main/resources/assets/freecam/textures/gui/goto_list_background.png b/common/src/main/resources/assets/freecam/textures/gui/goto_list_background.png new file mode 100644 index 00000000..0b4153a3 Binary files /dev/null and b/common/src/main/resources/assets/freecam/textures/gui/goto_list_background.png differ diff --git a/common/src/main/resources/assets/freecam/textures/gui/goto_list_background.png.mcmeta b/common/src/main/resources/assets/freecam/textures/gui/goto_list_background.png.mcmeta new file mode 100644 index 00000000..15c4cd9e --- /dev/null +++ b/common/src/main/resources/assets/freecam/textures/gui/goto_list_background.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 3, + "height": 3, + "border": 1 + } + } +}