From 2aa1378a8f347d8c6717556825cedace1ec53dd9 Mon Sep 17 00:00:00 2001 From: I-No-oNe <145749961+I-No-oNe@users.noreply.github.com> Date: Sun, 14 Jun 2026 12:11:38 +0300 Subject: [PATCH 1/3] feat(ccs): add craft command and autocomplete/IDE fixes - craft : bulk-craft via recipe book, take result - autocomplete: suggest keybinds and item ids, match items without leading ":" - IDE: grey out // comment lines - IDE: Ctrl+/ toggles comments across full selection --- .../clickcrystals/ClickCrystals.java | 2 + .../common/interactive/TextFieldElement.java | 82 +++++++++--- .../scripts/ClickScriptAutocomplete.java | 52 +++++++- .../gui/screens/scripts/ClickScriptIDE.java | 1 + .../syntax/macros/inventory/CraftCmd.java | 126 ++++++++++++++++++ 5 files changed, 236 insertions(+), 27 deletions(-) create mode 100644 src/main/java/io/github/itzispyder/clickcrystals/scripting/syntax/macros/inventory/CraftCmd.java diff --git a/src/main/java/io/github/itzispyder/clickcrystals/ClickCrystals.java b/src/main/java/io/github/itzispyder/clickcrystals/ClickCrystals.java index d468f323..6702b3d6 100644 --- a/src/main/java/io/github/itzispyder/clickcrystals/ClickCrystals.java +++ b/src/main/java/io/github/itzispyder/clickcrystals/ClickCrystals.java @@ -52,6 +52,7 @@ import io.github.itzispyder.clickcrystals.scripting.syntax.macros.*; import io.github.itzispyder.clickcrystals.scripting.syntax.macros.camera.SnapToCmd; import io.github.itzispyder.clickcrystals.scripting.syntax.macros.camera.TurnToCmd; +import io.github.itzispyder.clickcrystals.scripting.syntax.macros.inventory.CraftCmd; import io.github.itzispyder.clickcrystals.scripting.syntax.macros.inventory.GuiDropCmd; import io.github.itzispyder.clickcrystals.scripting.syntax.macros.inventory.GuiQuickMoveCmd; import io.github.itzispyder.clickcrystals.scripting.syntax.macros.inventory.GuiSwapCmd; @@ -213,6 +214,7 @@ public void initClickScript() { ClickScript.register(new GuiSwapCmd()); ClickScript.register(new GuiDropCmd()); ClickScript.register(new GuiQuickMoveCmd()); + ClickScript.register(new CraftCmd()); ClickScript.register(new DamageCmd()); ClickScript.register(new InteractCmd()); ClickScript.register(new DefineCmd()); diff --git a/src/main/java/io/github/itzispyder/clickcrystals/gui/elements/common/interactive/TextFieldElement.java b/src/main/java/io/github/itzispyder/clickcrystals/gui/elements/common/interactive/TextFieldElement.java index 073b0ceb..efc6262e 100644 --- a/src/main/java/io/github/itzispyder/clickcrystals/gui/elements/common/interactive/TextFieldElement.java +++ b/src/main/java/io/github/itzispyder/clickcrystals/gui/elements/common/interactive/TextFieldElement.java @@ -97,6 +97,51 @@ private int selMax() { return Math.max(selectionStart, selectionEnd); } + // Comments or uncomments every line touched by the cursor or selection (Ctrl+A then Ctrl+/ covers all lines). + private boolean toggleComment() { + int from = selectedAll ? 0 : selMin(); + int to = selectedAll ? content.length() : selMax(); + int blockStart = lineStart(from); + int blockEnd = lineEnd(to); + boolean collapsed = !selectedAll && !hasRange(); + + String block = content.substring(blockStart, blockEnd); + String[] lines = block.split("\n", -1); + + // Uncomment only when every non-blank line is already commented; otherwise comment them all. + boolean allCommented = true; + for (String line : lines) + if (!line.isBlank() && !line.startsWith("//")) { + allCommented = false; + break; + } + + StringBuilder rebuilt = new StringBuilder(); + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + if (!line.isBlank()) + line = allCommented ? line.substring(line.startsWith("// ") ? 3 : 2) : "// " + line; + rebuilt.append(line); + if (i < lines.length - 1) rebuilt.append("\n"); + } + + pushUndo(); + int savedStart = selectionStart; + content = content.substring(0, blockStart) + rebuilt + content.substring(blockEnd); + + if (collapsed) { + int delta = rebuilt.length() - block.length(); + selectionStart = selectionEnd = MathUtils.clamp(savedStart + delta, blockStart, blockStart + rebuilt.length()); + } else { + selectionStart = blockStart; + selectionEnd = blockStart + rebuilt.length(); + } + styledContent = style(content); + updateSelection(); + preferredCol = -1; + return true; + } + private void deleteRange() { pushUndo(); int lo = selectedAll ? 0 : selMin(); @@ -270,28 +315,7 @@ private boolean handleKeyInner(int key, GuiScreen screen) { return true; } case GLFW.GLFW_KEY_SLASH -> { - int ls = lineStart(selectionStart); - int le = lineEnd(selectionStart); - String line = content.substring(ls, le); - pushUndo(); - String newLine; - int delta; - if (line.startsWith("// ")) { - newLine = line.substring(3); - delta = -3; - } else if (line.startsWith("//")) { - newLine = line.substring(2); - delta = -2; - } else { - newLine = "// " + line; - delta = 3; - } - content = content.substring(0, ls) + newLine + content.substring(le); - selectionStart = selectionEnd = MathUtils.clamp(selectionStart + delta, ls, ls + newLine.length()); - styledContent = style(content); - updateSelection(); - preferredCol = -1; - return true; + return toggleComment(); } case GLFW.GLFW_KEY_LEFT -> { selectionStart = selectionEnd = prevWordBoundary(selectionStart); @@ -772,6 +796,8 @@ public void clearUndoHistory() { public static class TextHighlighter { private List stringFactories = new ArrayList<>(); private ChatColor originalColor; + private String commentPrefix; + private ChatColor commentColor = ChatColor.GRAY; public TextHighlighter(ChatColor originalColor) { this.originalColor = originalColor; @@ -781,10 +807,22 @@ public TextHighlighter() { this(ChatColor.WHITE); } + // Renders whole lines starting with the given prefix in a single comment color. + public TextHighlighter comments(String prefix, ChatColor color) { + this.commentPrefix = prefix; + this.commentColor = color; + return this; + } + public String highlightText(String text) { String[] lines = text.lines().toArray(String[]::new); StringBuilder result = new StringBuilder(); for (int i = 0; i < lines.length; i++) { + if (commentPrefix != null && lines[i].stripLeading().startsWith(commentPrefix)) { + result.append("%s%s%s".formatted(commentColor, lines[i], originalColor)); + if (i < lines.length - 1) result.append("\n"); + continue; + } String[] words = lines[i].split(" "); for (int j = 0; j < words.length; j++) { String word = words[j]; diff --git a/src/main/java/io/github/itzispyder/clickcrystals/gui/screens/scripts/ClickScriptAutocomplete.java b/src/main/java/io/github/itzispyder/clickcrystals/gui/screens/scripts/ClickScriptAutocomplete.java index c752fac4..6b10201c 100644 --- a/src/main/java/io/github/itzispyder/clickcrystals/gui/screens/scripts/ClickScriptAutocomplete.java +++ b/src/main/java/io/github/itzispyder/clickcrystals/gui/screens/scripts/ClickScriptAutocomplete.java @@ -10,10 +10,12 @@ import io.github.itzispyder.clickcrystals.scripting.syntax.client.DefineCmd; import io.github.itzispyder.clickcrystals.scripting.syntax.client.ModuleCmd; import io.github.itzispyder.clickcrystals.scripting.syntax.logic.OnEventCmd; +import io.github.itzispyder.clickcrystals.modules.keybinds.Keybind; import io.github.itzispyder.clickcrystals.util.minecraft.render.RenderUtils; import io.github.itzispyder.clickcrystals.util.misc.Dimensions; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.chat.Component; import net.minecraft.util.Mth; import net.minecraft.world.level.GameType; @@ -52,7 +54,11 @@ public class ClickScriptAutocomplete implements Global { PACKET_TYPES, PACKET_C2S, PACKET_S2C, - CANCELABLE_INPUT_TYPES; + CANCELABLE_INPUT_TYPES, + KEY_NAMES, + ITEM_IDS, + HAND_OR_ITEM_TYPES, + SWITCH_TYPES; static { COMMANDS = sorted(Arrays.asList(ClickScript.collectNames())); @@ -76,6 +82,23 @@ public class ClickScriptAutocomplete implements Global { CANCELABLE_INPUT_TYPES = sorted(new ArrayList<>(INPUT_TYPES) {{ add("cancel"); }}); + KEY_NAMES = sorted(keyNames()); + ITEM_IDS = sorted(BuiltInRegistries.ITEM.keySet().stream().map(id -> ":" + id.getPath()).toList()); + HAND_OR_ITEM_TYPES = sorted(new ArrayList<>(ITEM_IDS) {{ + addAll(HAND_TYPES); + }}); + SWITCH_TYPES = sorted(new ArrayList<>(ITEM_IDS) {{ + add("back"); + }}); + } + + // Key names accepted by "on key_press"/"on key_release": the named keys plus the + // single-character glfw key names (letters and digits) reported at runtime. + private static List keyNames() { + List names = new ArrayList<>(Keybind.EXTENDED_NAMES.values()); + for (char c = 'a'; c <= 'z'; c++) names.add(String.valueOf(c)); + for (char c = '0'; c <= '9'; c++) names.add(String.valueOf(c)); + return names; } private final List suggestions = new ArrayList<>(); @@ -95,6 +118,18 @@ private static List sorted(List list) { return list.stream().sorted().toList(); } + // Matches a suggestion against the typed prefix. Identifier suggestions (":item") are also + // matched by their bare name, so the leading ":" is optional while typing. + private static boolean matches(String keyword, String prefix) { + if (keyword.equals(prefix)) return false; + if (keyword.startsWith(prefix)) return true; + return isSigil(keyword.charAt(0)) && !isSigil(prefix.charAt(0)) && keyword.regionMatches(1, prefix, 0, prefix.length()); + } + + private static boolean isSigil(char c) { + return c == ':' || c == '#'; + } + public void update(String line, int cursorCol) { suggestions.clear(); visible = false; @@ -114,7 +149,7 @@ public void update(String line, int cursorCol) { List pool = resolvePool(tokens, tokenIdx); for (String kw : pool) { - if (kw.startsWith(prefix) && !kw.equals(prefix)) { + if (matches(kw, prefix)) { suggestions.add(kw); if (suggestions.size() >= MAX_SUGGESTIONS) break; } @@ -151,6 +186,9 @@ private List resolveOnPool(String[] tokens, int tokenIdx) { default -> false; }; if (variable) { + // key events take a key name before the inline command + if ((event.equals("key_press") || event.equals("key_release")) && tokenIdx == 2) + return KEY_NAMES; for (int i = 2; i < tokenIdx; i++) { if (COMMAND_SET.contains(tokens[i].toLowerCase())) return firstArgPool(tokens[i].toLowerCase(), tokenIdx - i); @@ -224,8 +262,11 @@ private static List condArgPool(String cond, int argIdx) { case "facing", "target_block_face" -> argIdx == 0 ? DIRECTION_TYPES : List.of(); case "reference_entity" -> argIdx == 0 ? AS_TYPES : List.of(); case "dimension" -> argIdx == 0 ? DIMENSIONS : List.of(); - case "item_count", "item_durability", "item_cooldown", "inventory_count", "hotbar_count" -> - argIdx == 0 ? HAND_TYPES : List.of(); + case "item_count", "item_durability", "item_cooldown" -> + argIdx == 0 ? HAND_OR_ITEM_TYPES : List.of(); + case "holding", "off_holding", "inventory_has", "inventory_count", + "equipment_has", "hotbar_has", "hotbar_count", "cursor_item" -> + argIdx == 0 ? ITEM_IDS : List.of(); default -> List.of(); }; } @@ -242,7 +283,8 @@ private static List firstArgPool(String cmd, int idx) { case "hold_input", "toggle_input" -> CANCELABLE_INPUT_TYPES; case "interact", "damage" -> INTERACT_TYPES; case "cancel_packet", "uncancel_packet" -> PACKET_TYPES; - case "switch" -> List.of("back"); + case "switch" -> SWITCH_TYPES; + case "gui_quickmove", "gui_swap", "gui_switch", "gui_drop", "craft" -> ITEM_IDS; case "drop" -> List.of("all"); case "dimension" -> DIMENSIONS; case "as" -> AS_TYPES; diff --git a/src/main/java/io/github/itzispyder/clickcrystals/gui/screens/scripts/ClickScriptIDE.java b/src/main/java/io/github/itzispyder/clickcrystals/gui/screens/scripts/ClickScriptIDE.java index 6459fc39..3f4073e8 100644 --- a/src/main/java/io/github/itzispyder/clickcrystals/gui/screens/scripts/ClickScriptIDE.java +++ b/src/main/java/io/github/itzispyder/clickcrystals/gui/screens/scripts/ClickScriptIDE.java @@ -63,6 +63,7 @@ public class ClickScriptIDE extends DefaultBase { this.put(ChatColor.YELLOW, Arrays.stream(DefineCmd.Type.values()).map(e -> e.name().toLowerCase()).toList()); this.put(ChatColor.YELLOW, Arrays.stream(Dimensions.values()).map(e -> e.name().toLowerCase()).toList()); this.put(ChatColor.ORANGE, ClickScript.collectNames()); + this.comments("//", ChatColor.DARK_GRAY); }}; private static final List> KEYBIND_ENTRIES = List.of( diff --git a/src/main/java/io/github/itzispyder/clickcrystals/scripting/syntax/macros/inventory/CraftCmd.java b/src/main/java/io/github/itzispyder/clickcrystals/scripting/syntax/macros/inventory/CraftCmd.java new file mode 100644 index 00000000..62e3ca6f --- /dev/null +++ b/src/main/java/io/github/itzispyder/clickcrystals/scripting/syntax/macros/inventory/CraftCmd.java @@ -0,0 +1,126 @@ +package io.github.itzispyder.clickcrystals.scripting.syntax.macros.inventory; + +import io.github.itzispyder.clickcrystals.Global; +import io.github.itzispyder.clickcrystals.scripting.ScriptArgs; +import io.github.itzispyder.clickcrystals.scripting.ScriptCommand; +import io.github.itzispyder.clickcrystals.scripting.ScriptParser; +import io.github.itzispyder.clickcrystals.util.minecraft.InvUtils; +import io.github.itzispyder.clickcrystals.util.minecraft.PlayerUtils; +import net.minecraft.client.ClientRecipeBook; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.recipebook.RecipeCollection; +import net.minecraft.util.context.ContextMap; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.StackedItemContents; +import net.minecraft.world.inventory.AbstractCraftingMenu; +import net.minecraft.world.inventory.ContainerInput; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.display.RecipeDisplay; +import net.minecraft.world.item.crafting.display.RecipeDisplayEntry; +import net.minecraft.world.item.crafting.display.ShapedCraftingRecipeDisplay; +import net.minecraft.world.item.crafting.display.ShapelessCraftingRecipeDisplay; +import net.minecraft.world.item.crafting.display.SlotDisplayContext; + +import java.util.function.Predicate; + +// @Format craft ? +public class CraftCmd extends ScriptCommand implements Global { + + private static final int MIN_DELAY = 60, RAND_DELAY = 90; + private boolean crafting; + + public CraftCmd() { + super("craft"); + } + + @Override + public void onCommand(ScriptCommand command, String line, ScriptArgs args) { + if (PlayerUtils.invalid() || mc.level == null || crafting) + return; + + // require an actually-open crafting screen (2x2 inventory or a 3x3 table) + if (!(mc.screen instanceof AbstractContainerScreen) || !(mc.player.containerMenu instanceof AbstractCraftingMenu menu)) + return; + + String id = args.get(0).toString(); + Predicate target = ScriptParser.parseItemPredicate(id); + long delay = (long) (args.getSize() > 1 ? args.get(1).toDouble() * 1000L : 0L); + + int side = (int) Math.sqrt(menu.getInputGridSlots().size()); + ContextMap ctx = SlotDisplayContext.fromLevel(mc.level); + Search found = search(target, ctx, side); + + if (found.entry == null) { + if (!found.matchedAny) + throw new IllegalArgumentException("No crafting recipe produces an item matching \"" + id + "\"."); + if (!found.anyFits) + throw new IllegalArgumentException("Recipe for \"" + id + "\" doesn't fit the current " + side + "x" + side + " grid - open a crafting table."); + throw new IllegalArgumentException("Not enough materials in your inventory to craft \"" + id + "\"."); + } + + if (noRoomFor(found.entry.resultItems(ctx).get(0))) + throw new IllegalArgumentException("Your inventory is full - no room for the crafted \"" + id + "\"."); + + crafting = true; + // fill the grid with full stacks (recipe-book auto-fill), then bulk-craft the whole batch + mc.gameMode.handlePlaceRecipe(menu.containerId, found.entry.id(), true); + takeResult(menu, delay); + } + + // Shift-clicks the result after the requested wait plus a small reaction delay, crafting the entire batch at once. + private void takeResult(AbstractCraftingMenu menu, long baseDelay) { + long delay = baseDelay + MIN_DELAY + (long) (Math.random() * RAND_DELAY); + system.scheduler.runDelayedTask(() -> { + Slot result = menu.getResultSlot(); + if (!PlayerUtils.invalid() && mc.player.containerMenu == menu && result.hasItem()) + mc.gameMode.handleContainerInput(menu.containerId, result.index, 0, ContainerInput.QUICK_MOVE, mc.player); + crafting = false; + }, delay); + } + + // Scans the recipe book for a craftable crafting recipe whose result matches, tracking why a match may be unusable. + private Search search(Predicate target, ContextMap ctx, int side) { + StackedItemContents contents = new StackedItemContents(); + for (ItemStack stack : InvUtils.inv().getNonEquipmentItems()) + contents.accountStack(stack); + + boolean matchedAny = false, anyFits = false; + ClientRecipeBook book = mc.player.getRecipeBook(); + + for (RecipeCollection collection : book.getCollections()) { + for (RecipeDisplayEntry entry : collection.getRecipes()) { + if (!isCrafting(entry.display()) || entry.resultItems(ctx).stream().noneMatch(target)) + continue; + + matchedAny = true; + boolean fits = fitsGrid(entry.display(), side); + anyFits |= fits; + + if (fits && entry.canCraft(contents)) + return new Search(entry, true, true); + } + } + return new Search(null, matchedAny, anyFits); + } + + // True when the result can't be stored: no empty slot and no matching stack with room. + private static boolean noRoomFor(ItemStack result) { + Inventory inv = InvUtils.inv(); + return inv.getFreeSlot() == -1 && inv.getSlotWithRemainingSpace(result) == -1; + } + + private static boolean isCrafting(RecipeDisplay display) { + return display instanceof ShapedCraftingRecipeDisplay || display instanceof ShapelessCraftingRecipeDisplay; + } + + private static boolean fitsGrid(RecipeDisplay display, int side) { + if (display instanceof ShapedCraftingRecipeDisplay shaped) + return shaped.width() <= side && shaped.height() <= side; + if (display instanceof ShapelessCraftingRecipeDisplay shapeless) + return shapeless.ingredients().size() <= side * side; + return false; + } + + private record Search(RecipeDisplayEntry entry, boolean matchedAny, boolean anyFits) {} +} From dfd2c5cf040cfac075d7fe74f6a52bb1cca665ba Mon Sep 17 00:00:00 2001 From: I-No-oNe <145749961+I-No-oNe@users.noreply.github.com> Date: Sun, 14 Jun 2026 15:56:45 +0300 Subject: [PATCH 2/3] feat(ccs): add amount arg to craft command Omitting amount crafts everything; otherwise craft enough recipe sets to yield the requested item count. --- .../syntax/macros/inventory/CraftCmd.java | 55 +++++++++++++++---- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/src/main/java/io/github/itzispyder/clickcrystals/scripting/syntax/macros/inventory/CraftCmd.java b/src/main/java/io/github/itzispyder/clickcrystals/scripting/syntax/macros/inventory/CraftCmd.java index 62e3ca6f..3f91352f 100644 --- a/src/main/java/io/github/itzispyder/clickcrystals/scripting/syntax/macros/inventory/CraftCmd.java +++ b/src/main/java/io/github/itzispyder/clickcrystals/scripting/syntax/macros/inventory/CraftCmd.java @@ -18,13 +18,14 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.display.RecipeDisplay; import net.minecraft.world.item.crafting.display.RecipeDisplayEntry; +import net.minecraft.world.item.crafting.display.RecipeDisplayId; import net.minecraft.world.item.crafting.display.ShapedCraftingRecipeDisplay; import net.minecraft.world.item.crafting.display.ShapelessCraftingRecipeDisplay; import net.minecraft.world.item.crafting.display.SlotDisplayContext; import java.util.function.Predicate; -// @Format craft ? +// @Format craft ? ? public class CraftCmd extends ScriptCommand implements Global { private static final int MIN_DELAY = 60, RAND_DELAY = 90; @@ -46,6 +47,10 @@ public void onCommand(ScriptCommand command, String line, ScriptArgs args) { String id = args.get(0).toString(); Predicate target = ScriptParser.parseItemPredicate(id); long delay = (long) (args.getSize() > 1 ? args.get(1).toDouble() * 1000L : 0L); + boolean all = args.getSize() <= 2; // amount omitted -> craft everything + int amount = all ? 0 : args.get(2).toInt(); + if (!all && amount <= 0) + return; int side = (int) Math.sqrt(menu.getInputGridSlots().size()); ContextMap ctx = SlotDisplayContext.fromLevel(mc.level); @@ -59,24 +64,52 @@ public void onCommand(ScriptCommand command, String line, ScriptArgs args) { throw new IllegalArgumentException("Not enough materials in your inventory to craft \"" + id + "\"."); } - if (noRoomFor(found.entry.resultItems(ctx).get(0))) + ItemStack result = found.entry.resultItems(ctx).get(0); + if (noRoomFor(result)) throw new IllegalArgumentException("Your inventory is full - no room for the crafted \"" + id + "\"."); + // "all": repeatedly max-fill and bulk-craft until materials run out. + // amount: craft one recipe set per cycle, enough cycles to yield the requested amount. crafting = true; - // fill the grid with full stacks (recipe-book auto-fill), then bulk-craft the whole batch - mc.gameMode.handlePlaceRecipe(menu.containerId, found.entry.id(), true); - takeResult(menu, delay); + int cycles = all ? Integer.MAX_VALUE : (amount + result.getCount() - 1) / result.getCount(); + craftCycle(menu, found.entry.id(), all, cycles, delay); } - // Shift-clicks the result after the requested wait plus a small reaction delay, crafting the entire batch at once. - private void takeResult(AbstractCraftingMenu menu, long baseDelay) { - long delay = baseDelay + MIN_DELAY + (long) (Math.random() * RAND_DELAY); + // One fill-then-take cycle; reschedules itself until done, materials run out, or the inventory fills up. + private void craftCycle(AbstractCraftingMenu menu, RecipeDisplayId recipe, boolean all, int cyclesLeft, long delay) { system.scheduler.runDelayedTask(() -> { - Slot result = menu.getResultSlot(); - if (!PlayerUtils.invalid() && mc.player.containerMenu == menu && result.hasItem()) + if (aborted(menu)) + return; + mc.gameMode.handlePlaceRecipe(menu.containerId, recipe, all); + + system.scheduler.runDelayedTask(() -> { + if (aborted(menu)) + return; + Slot result = menu.getResultSlot(); + if (!result.hasItem() || noRoomFor(result.getItem())) { + crafting = false; + return; + } mc.gameMode.handleContainerInput(menu.containerId, result.index, 0, ContainerInput.QUICK_MOVE, mc.player); + + int left = all ? cyclesLeft : cyclesLeft - 1; + if (left > 0) craftCycle(menu, recipe, all, left, delay); + else crafting = false; + }, stepDelay(delay)); + }, stepDelay(delay)); + } + + // Stops the craft loop if the player left the menu or became invalid. + private boolean aborted(AbstractCraftingMenu menu) { + if (PlayerUtils.invalid() || mc.player.containerMenu != menu) { crafting = false; - }, delay); + return true; + } + return false; + } + + private static long stepDelay(long base) { + return base + MIN_DELAY + (long) (Math.random() * RAND_DELAY); } // Scans the recipe book for a craftable crafting recipe whose result matches, tracking why a match may be unusable. From feb90b99daefb43baae7b1432cabac692d072252 Mon Sep 17 00:00:00 2001 From: I-No-oNe <145749961+I-No-oNe@users.noreply.github.com> Date: Sun, 14 Jun 2026 17:40:50 +0300 Subject: [PATCH 3/3] feat(gui): add conditional setting visibility with animation - ModuleSetting.visibleWhen: hide settings until a condition holds, with an animated collapse/expand in the setting list - scroll panels re-stack so hidden settings close the gap and the view height shrinks - advanced: disable-script-chat-errors (errors go to logs only) - totem-chams: rising and vortex dolls; gravity/max-velocity shown per doll type - gui-cursor: totem options shown only in HOVER_TOTEM mode - craft: run steps on the main thread and never wedge the busy flag --- .../clickcrystals/client/system/Config.java | 9 +++ .../clickcrystals/gui/GuiElement.java | 5 ++ .../browsingmode/module/SettingElement.java | 62 +++++++++++++++- .../module/SettingSectionElement.java | 12 ++++ .../interactive/ScrollPanelElement.java | 26 +++++++ .../overviewmode/ModuleEditElement.java | 2 +- .../gui/screens/ModuleEditScreen.java | 2 +- .../settings/AdvancedSettingScreen.java | 13 +++- .../gui/screens/settings/KeybindScreen.java | 2 +- .../clickcrystals/modules/ModuleSetting.java | 12 ++++ .../modules/modules/misc/GuiCursor.java | 4 +- .../modules/modules/rendering/TotemChams.java | 44 +++++------- .../totemchams/RisingChamRagDoll.java | 39 ++++++++++ .../totemchams/VortexChamRagDoll.java | 40 +++++++++++ .../totemchams/parts/RisingChamPart.java | 72 +++++++++++++++++++ .../totemchams/parts/VortexChamPart.java | 71 ++++++++++++++++++ .../modules/settings/SettingBuilder.java | 11 +++ .../clickcrystals/scripting/ClickScript.java | 3 +- .../syntax/macros/inventory/CraftCmd.java | 38 +++++----- 19 files changed, 416 insertions(+), 51 deletions(-) create mode 100644 src/main/java/io/github/itzispyder/clickcrystals/modules/modules/rendering/totemchams/RisingChamRagDoll.java create mode 100644 src/main/java/io/github/itzispyder/clickcrystals/modules/modules/rendering/totemchams/VortexChamRagDoll.java create mode 100644 src/main/java/io/github/itzispyder/clickcrystals/modules/modules/rendering/totemchams/parts/RisingChamPart.java create mode 100644 src/main/java/io/github/itzispyder/clickcrystals/modules/modules/rendering/totemchams/parts/VortexChamPart.java diff --git a/src/main/java/io/github/itzispyder/clickcrystals/client/system/Config.java b/src/main/java/io/github/itzispyder/clickcrystals/client/system/Config.java index 33465ef7..c7919da3 100644 --- a/src/main/java/io/github/itzispyder/clickcrystals/client/system/Config.java +++ b/src/main/java/io/github/itzispyder/clickcrystals/client/system/Config.java @@ -33,6 +33,7 @@ public class Config implements JsonSerializable, Global { private boolean overviewMode; private boolean disableCustomLoading; private boolean disableModuleToggleBroadcast; + private boolean disableScriptChatErrors; private boolean modMenuIntegration; private boolean devMode; private int readAnnouncementCount; @@ -222,6 +223,14 @@ public void setDisableModuleToggleBroadcast(boolean disableModuleToggleBroadcast this.disableModuleToggleBroadcast = disableModuleToggleBroadcast; } + public boolean isDisableScriptChatErrors() { + return disableScriptChatErrors; + } + + public void setDisableScriptChatErrors(boolean disableScriptChatErrors) { + this.disableScriptChatErrors = disableScriptChatErrors; + } + public String getCustomPath() { return customPath; } diff --git a/src/main/java/io/github/itzispyder/clickcrystals/gui/GuiElement.java b/src/main/java/io/github/itzispyder/clickcrystals/gui/GuiElement.java index 4298197d..2f955c6d 100644 --- a/src/main/java/io/github/itzispyder/clickcrystals/gui/GuiElement.java +++ b/src/main/java/io/github/itzispyder/clickcrystals/gui/GuiElement.java @@ -255,6 +255,11 @@ public void setHeight(int height) { this.height = height; } + // Vertical space this element occupies in a parent's layout; may differ from the rendered height (e.g. while animating). + public int getLayoutHeight() { + return height; + } + public String getTooltip() { return tooltip; } diff --git a/src/main/java/io/github/itzispyder/clickcrystals/gui/elements/browsingmode/module/SettingElement.java b/src/main/java/io/github/itzispyder/clickcrystals/gui/elements/browsingmode/module/SettingElement.java index 136282e5..f7eaa5d8 100644 --- a/src/main/java/io/github/itzispyder/clickcrystals/gui/elements/browsingmode/module/SettingElement.java +++ b/src/main/java/io/github/itzispyder/clickcrystals/gui/elements/browsingmode/module/SettingElement.java @@ -4,20 +4,80 @@ import io.github.itzispyder.clickcrystals.gui.elements.common.AbstractElement; import io.github.itzispyder.clickcrystals.gui.misc.Shades; import io.github.itzispyder.clickcrystals.gui.misc.Tex; +import io.github.itzispyder.clickcrystals.gui.misc.animators.Animations; +import io.github.itzispyder.clickcrystals.gui.misc.animators.Animator; import io.github.itzispyder.clickcrystals.modules.ModuleSetting; +import io.github.itzispyder.clickcrystals.util.MathUtils; import io.github.itzispyder.clickcrystals.util.minecraft.TextUtils; import io.github.itzispyder.clickcrystals.util.minecraft.render.RenderUtils; import net.minecraft.client.gui.GuiGraphicsExtractor; public abstract class SettingElement> extends GuiElement { + private static final long VISIBILITY_ANIM_MS = 250; + protected final T setting; protected boolean shouldUnderline; + private final int slotHeight; + private Animator visibilityAnimator; + private boolean lastVisible; public SettingElement(T setting, int x, int y) { super(x, y, 270, 40); this.shouldUnderline = false; this.setting = setting; + this.slotHeight = height; + this.lastVisible = setting.isVisible(); + } + + // 0 = fully hidden, 1 = fully shown; animates when the setting's visibility condition flips. + private double visibility() { + boolean visible = setting.isVisible(); + if (visible != lastVisible) { + lastVisible = visible; + visibilityAnimator = new Animator(VISIBILITY_ANIM_MS, Animations.FADE_IN_AND_OUT); + visibilityAnimator.setReversed(!visible); + visibilityAnimator.reset(); + } + if (visibilityAnimator == null) + return visible ? 1 : 0; + if (visibilityAnimator.isFinished()) { + visibilityAnimator = null; + return visible ? 1 : 0; + } + return MathUtils.clamp(visibilityAnimator.getAnimation(), 0.0, 1.0); + } + + @Override + public int getLayoutHeight() { + return (int) (slotHeight * visibility()); + } + + @Override + public void render(GuiGraphicsExtractor context, int mouseX, int mouseY) { + double factor = visibility(); + if (factor <= 0.01) { + setRendering(false); + return; + } + setRendering(true); + + if (factor >= 0.99) { + super.render(context, mouseX, mouseY); + return; + } + + // collapsing/expanding: clip to the animated slot height (matches the reserved layout height) + int clip = Math.max(1, (int) (slotHeight * factor)); + context.enableScissor(x, y, x + width, y + clip); + super.render(context, mouseX, mouseY); + context.disableScissor(); + } + + @Override + public void mouseClicked(double mouseX, double mouseY, int button) { + if (setting.isVisible()) + super.mouseClicked(mouseX, mouseY, button); } public void renderSettingDetails(GuiGraphicsExtractor context) { @@ -66,6 +126,6 @@ public boolean isHovered(int mouseX, int mouseY) { int bxw = x + width - 5; int by = y + height / 2 - 4; int byh = y + height / 2 + 12; - return rendering && mouseX > bx && mouseX < bxw && mouseY > by && mouseY < byh; + return rendering && setting.isVisible() && mouseX > bx && mouseX < bxw && mouseY > by && mouseY < byh; } } diff --git a/src/main/java/io/github/itzispyder/clickcrystals/gui/elements/browsingmode/module/SettingSectionElement.java b/src/main/java/io/github/itzispyder/clickcrystals/gui/elements/browsingmode/module/SettingSectionElement.java index a26d47fe..af2d52b4 100644 --- a/src/main/java/io/github/itzispyder/clickcrystals/gui/elements/browsingmode/module/SettingSectionElement.java +++ b/src/main/java/io/github/itzispyder/clickcrystals/gui/elements/browsingmode/module/SettingSectionElement.java @@ -33,8 +33,20 @@ public SettingSectionElement(SettingSection settingSection, int x, int y) { height = caret - y; } + // Re-stacks settings each frame so hidden ones collapse and the rest slide to fill the gap. + private void layoutSettings() { + int caret = y + 30; + for (GuiElement child : getChildren()) { + if (child.y != caret) + child.move(0, caret - child.y); + caret += child.getLayoutHeight(); + } + height = caret - y; + } + @Override public void render(GuiGraphicsExtractor context, int mouseX, int mouseY) { + layoutSettings(); boolean isAnimating = animator != null && !animator.isFinished(); if (isAnimating) { context.pose().pushMatrix(); diff --git a/src/main/java/io/github/itzispyder/clickcrystals/gui/elements/common/interactive/ScrollPanelElement.java b/src/main/java/io/github/itzispyder/clickcrystals/gui/elements/common/interactive/ScrollPanelElement.java index 55fc1015..01a14f1c 100644 --- a/src/main/java/io/github/itzispyder/clickcrystals/gui/elements/common/interactive/ScrollPanelElement.java +++ b/src/main/java/io/github/itzispyder/clickcrystals/gui/elements/common/interactive/ScrollPanelElement.java @@ -15,6 +15,8 @@ public class ScrollPanelElement extends GuiElement { public static final int SCROLL_MULTIPLIER = 15; private int remainingUp, remainingDown, limitTop, limitBottom, scrollbarY, scrollbarHeight, prevDrag; private boolean scrolling; + private boolean verticalStack; + private int stackGap; private final Animator interpolation; private int interpolationLength; @@ -95,6 +97,29 @@ public void recalculatePositions() { } } + // Stack children vertically by their layout height, so collapsed/animating elements close the gap. + public ScrollPanelElement verticalStack(int gap) { + this.verticalStack = true; + this.stackGap = gap; + return this; + } + + // Anchors on the first child (which already tracks the scroll offset) and re-flows the rest below it. + private void restack() { + if (!verticalStack || getChildren().isEmpty()) + return; + + GuiElement first = getChildren().get(0); + int caret = first.y + first.getLayoutHeight() + stackGap; + for (int i = 1; i < getChildren().size(); i++) { + GuiElement child = getChildren().get(i); + if (child.y != caret) + child.move(0, caret - child.y); + caret += child.getLayoutHeight() + stackGap; + } + recalculatePositions(); + } + @Override public void onRender(GuiGraphicsExtractor context, int mouseX, int mouseY) { @@ -102,6 +127,7 @@ public void onRender(GuiGraphicsExtractor context, int mouseX, int mouseY) { @Override public void render(GuiGraphicsExtractor context, int mouseX, int mouseY) { + restack(); boolean bl = canRender(); float interpolatedDelta = (float)(interpolationLength * interpolation.getProgressClampedReversed()); diff --git a/src/main/java/io/github/itzispyder/clickcrystals/gui/elements/overviewmode/ModuleEditElement.java b/src/main/java/io/github/itzispyder/clickcrystals/gui/elements/overviewmode/ModuleEditElement.java index f810ba52..a8b0e7b0 100644 --- a/src/main/java/io/github/itzispyder/clickcrystals/gui/elements/overviewmode/ModuleEditElement.java +++ b/src/main/java/io/github/itzispyder/clickcrystals/gui/elements/overviewmode/ModuleEditElement.java @@ -21,7 +21,7 @@ public ModuleEditElement(GuiScreen parentScreen, Module module, int x, int y) { this.setDraggable(true); this.module = module; - ScrollPanelElement panel = new ScrollPanelElement(parentScreen, x + 5, y + 21, width - 5, height - 21); + ScrollPanelElement panel = new ScrollPanelElement(parentScreen, x + 5, y + 21, width - 5, height - 21).verticalStack(5); int caret = y + 25; for (SettingSection section : module.getData().getSettingSections()) { diff --git a/src/main/java/io/github/itzispyder/clickcrystals/gui/screens/ModuleEditScreen.java b/src/main/java/io/github/itzispyder/clickcrystals/gui/screens/ModuleEditScreen.java index c9a8d71a..19edfcb8 100644 --- a/src/main/java/io/github/itzispyder/clickcrystals/gui/screens/ModuleEditScreen.java +++ b/src/main/java/io/github/itzispyder/clickcrystals/gui/screens/ModuleEditScreen.java @@ -26,7 +26,7 @@ public ModuleEditScreen(Module module) { this.module = module; previousOpened = module; - ScrollPanelElement panel = new ScrollPanelElement(this, contentX + 5, contentY + 21, contentWidth - 5, contentHeight - 21); + ScrollPanelElement panel = new ScrollPanelElement(this, contentX + 5, contentY + 21, contentWidth - 5, contentHeight - 21).verticalStack(5); int caret = contentY + 25; for (SettingSection section : module.getData().getSettingSections()) { diff --git a/src/main/java/io/github/itzispyder/clickcrystals/gui/screens/settings/AdvancedSettingScreen.java b/src/main/java/io/github/itzispyder/clickcrystals/gui/screens/settings/AdvancedSettingScreen.java index 2cc3b08e..7881323b 100644 --- a/src/main/java/io/github/itzispyder/clickcrystals/gui/screens/settings/AdvancedSettingScreen.java +++ b/src/main/java/io/github/itzispyder/clickcrystals/gui/screens/settings/AdvancedSettingScreen.java @@ -62,6 +62,17 @@ public class AdvancedSettingScreen extends DefaultBase { }) .build() ); + public final ModuleSetting disableScriptChatErrors = scGui.add(scGui.createBoolSetting() + .name("disable-script-chat-errors") + .description("Don't print ClickScript errors to chat - show them only in the ClickCrystals logs") + .def(ClickCrystals.config.isDisableScriptChatErrors()) + .visibleWhen(() -> !ClickCrystals.config.isDisableModuleToggleBroadcast()) + .onSettingChange(setting -> { + ClickCrystals.config.setDisableScriptChatErrors(setting.getVal()); + ClickCrystals.config.save(); + }) + .build() + ); public final ModuleSetting debugMode = scGui.add(scGui.createBoolSetting() .name("debug-mode") .description("Useful while developing, for devs only ;)") @@ -76,7 +87,7 @@ public class AdvancedSettingScreen extends DefaultBase { public AdvancedSettingScreen() { super("Advanced Settings Screen"); - ScrollPanelElement panel = new ScrollPanelElement(this, contentX + 5, contentY + 21, contentWidth - 5, contentHeight - 21); + ScrollPanelElement panel = new ScrollPanelElement(this, contentX + 5, contentY + 21, contentWidth - 5, contentHeight - 21).verticalStack(5); int caret = contentY + 25; int margin = contentX + 5; diff --git a/src/main/java/io/github/itzispyder/clickcrystals/gui/screens/settings/KeybindScreen.java b/src/main/java/io/github/itzispyder/clickcrystals/gui/screens/settings/KeybindScreen.java index 30c7e995..7b481b0c 100644 --- a/src/main/java/io/github/itzispyder/clickcrystals/gui/screens/settings/KeybindScreen.java +++ b/src/main/java/io/github/itzispyder/clickcrystals/gui/screens/settings/KeybindScreen.java @@ -21,7 +21,7 @@ public class KeybindScreen extends DefaultBase { public KeybindScreen() { super("Keybinds Settings Screen"); - ScrollPanelElement panel = new ScrollPanelElement(this, contentX + 5, contentY + 21, contentWidth - 5, contentHeight - 21); + ScrollPanelElement panel = new ScrollPanelElement(this, contentX + 5, contentY + 21, contentWidth - 5, contentHeight - 21).verticalStack(5); int caret = contentY + 25; int margin = contentX + 5; diff --git a/src/main/java/io/github/itzispyder/clickcrystals/modules/ModuleSetting.java b/src/main/java/io/github/itzispyder/clickcrystals/modules/ModuleSetting.java index 7338d385..7588329e 100644 --- a/src/main/java/io/github/itzispyder/clickcrystals/modules/ModuleSetting.java +++ b/src/main/java/io/github/itzispyder/clickcrystals/modules/ModuleSetting.java @@ -5,9 +5,12 @@ import io.github.itzispyder.clickcrystals.modules.settings.SettingChangeCallback; import io.github.itzispyder.clickcrystals.util.StringUtils; +import java.util.function.Supplier; + public abstract class ModuleSetting { private SettingChangeCallback> changeAction; + private Supplier visibility; private final String name, id, description; protected T def, val; @@ -18,6 +21,7 @@ protected ModuleSetting(String name, String description, T def, T val) { this.def = def; this.val = val; this.changeAction = setting -> {}; + this.visibility = () -> true; } protected ModuleSetting(String name, String description, T val) { this(name, description, val, val); @@ -66,6 +70,14 @@ public void setChangeAction(SettingChangeCallback> changeAction this.changeAction = changeAction; } + public boolean isVisible() { + return visibility.get(); + } + + public void setVisibility(Supplier visibility) { + this.visibility = visibility != null ? visibility : () -> true; + } + public class Builder extends SettingBuilder> { @Override public ModuleSetting buildSetting() { diff --git a/src/main/java/io/github/itzispyder/clickcrystals/modules/modules/misc/GuiCursor.java b/src/main/java/io/github/itzispyder/clickcrystals/modules/modules/misc/GuiCursor.java index 36987476..ec95ffc2 100644 --- a/src/main/java/io/github/itzispyder/clickcrystals/modules/modules/misc/GuiCursor.java +++ b/src/main/java/io/github/itzispyder/clickcrystals/modules/modules/misc/GuiCursor.java @@ -40,12 +40,14 @@ public class GuiCursor extends Module implements Listener { public final ModuleSetting totemShiftHolder = scGeneral.add(createBoolSetting() .name("totem-shift-holder") .description("Holds down shift if the item you clicked on is a totem.") + .visibleWhen(()-> cursorAction.getVal() == Mode.HOVER_TOTEM) .def(false) .build() ); public final ModuleSetting closestTotemSlot = scGeneral.add(createBoolSetting() .name("closest-totem-slot") - .description("Picks the totem slot closest to the center of the inventory instead of the first one found. Requires cursor-action to be HOVER_TOTEM.") + .description("Picks the totem slot closest to the center of the inventory instead of the first one found.") + .visibleWhen(()-> cursorAction.getVal() == Mode.HOVER_TOTEM) .def(true) .build() ); diff --git a/src/main/java/io/github/itzispyder/clickcrystals/modules/modules/rendering/TotemChams.java b/src/main/java/io/github/itzispyder/clickcrystals/modules/modules/rendering/TotemChams.java index b22f4942..5414a902 100644 --- a/src/main/java/io/github/itzispyder/clickcrystals/modules/modules/rendering/TotemChams.java +++ b/src/main/java/io/github/itzispyder/clickcrystals/modules/modules/rendering/TotemChams.java @@ -12,8 +12,7 @@ import io.github.itzispyder.clickcrystals.modules.ModuleSetting; import io.github.itzispyder.clickcrystals.modules.modules.ListenerModule; import io.github.itzispyder.clickcrystals.modules.modules.rendering.totemchams.ChamRagDoll; -import io.github.itzispyder.clickcrystals.modules.modules.rendering.totemchams.ExplodingChamRagDoll; -import io.github.itzispyder.clickcrystals.modules.modules.rendering.totemchams.FadingChamRagDoll; +import io.github.itzispyder.clickcrystals.modules.modules.rendering.totemchams.*; import io.github.itzispyder.clickcrystals.modules.settings.SettingSection; import io.github.itzispyder.clickcrystals.util.minecraft.PlayerUtils; import net.minecraft.network.protocol.game.ClientboundEntityEventPacket; @@ -26,16 +25,11 @@ public class TotemChams extends ListenerModule { - boolean remove = false; - private final SettingSection scGeneral = getGeneralSection(); public final ModuleSetting ragDollState = scGeneral.add(createEnumSetting(RagDoll.class) .name("rag-doll-type") .description("How the rag doll should look like") .def(RagDoll.EXPLODING) - .onSettingChange(setting -> { - remove = setting.getVal() == RagDoll.FADING; - }) .build()); public final ModuleSetting showSelf = scGeneral.add(createBoolSetting() .name("show-self") @@ -46,6 +40,7 @@ public class TotemChams extends ListenerModule { public final ModuleSetting maxVelocity = scGeneral.add(createDoubleSetting() .name("max-velocity") .description("Max velocity of the flying parts") + .visibleWhen(()-> ragDollState.getVal().usesMaxVel()) .max(1.0) .min(0.0) .def(0.1) @@ -54,6 +49,7 @@ public class TotemChams extends ListenerModule { public final ModuleSetting gravity = scGeneral.add(createDoubleSetting() .name("gravity") .description("Gravity of the visuals") + .visibleWhen(() -> ragDollState.getVal().usesGravity()) .max(0.05) .min(0.0) .def(0.01) @@ -117,21 +113,6 @@ private void onEntityDamage(EntityDamageEvent e) { @EventHandler private void onTick(ClientTickEndEvent e) { - if (remove) { - scGeneral.remove(maxVelocity); - } - else { - boolean exists = false; - for (int i = 0; i < scGeneral.getSettings().size(); i++) { - if (scGeneral.getSettings().get(i) == maxVelocity) { - exists = true; - break; - } - } - if (!exists) - scGeneral.getSettings().add(2, maxVelocity); - } - if (PlayerUtils.invalid()) return; @@ -160,13 +141,26 @@ public int getColor() { } public enum RagDoll { - EXPLODING(ExplodingChamRagDoll.class), - FADING(FadingChamRagDoll.class); + EXPLODING(ExplodingChamRagDoll.class, true, true), + FADING(FadingChamRagDoll.class, true, false), + RISING(RisingChamRagDoll.class, false, false), + VORTEX(VortexChamRagDoll.class, false, false); private final Class> clazz; + private final boolean usesGravity, usesMaxVel; - RagDoll(Class> clazz) { + RagDoll(Class> clazz, boolean usesGravity, boolean usesMaxVel) { this.clazz = clazz; + this.usesGravity = usesGravity; + this.usesMaxVel = usesMaxVel; + } + + public boolean usesGravity() { + return usesGravity; + } + + public boolean usesMaxVel() { + return usesMaxVel; } public ChamRagDoll get(Player player) { diff --git a/src/main/java/io/github/itzispyder/clickcrystals/modules/modules/rendering/totemchams/RisingChamRagDoll.java b/src/main/java/io/github/itzispyder/clickcrystals/modules/modules/rendering/totemchams/RisingChamRagDoll.java new file mode 100644 index 00000000..882b4c73 --- /dev/null +++ b/src/main/java/io/github/itzispyder/clickcrystals/modules/modules/rendering/totemchams/RisingChamRagDoll.java @@ -0,0 +1,39 @@ +package io.github.itzispyder.clickcrystals.modules.modules.rendering.totemchams; + +import com.mojang.blaze3d.vertex.PoseStack; +import io.github.itzispyder.clickcrystals.modules.modules.rendering.totemchams.parts.ChamPart; +import io.github.itzispyder.clickcrystals.modules.modules.rendering.totemchams.parts.RisingChamPart; +import net.minecraft.world.entity.player.Player; + +public class RisingChamRagDoll extends ChamRagDoll { + + public RisingChamRagDoll(Player player, int maxAge) { + super(player, maxAge); + } + + @Override + protected void initializeParts(Player player) { + RisingChamPart head = new RisingChamPart(-ChamPart.B4, ChamPart.B12 + ChamPart.B12, -ChamPart.B4, ChamPart.B4, ChamPart.B12 + ChamPart.B12 + ChamPart.B8, ChamPart.B4, 0.0F); + head.pitch = player.getXRot(); + + parts.put("head", head); + parts.put("bod", new RisingChamPart(-ChamPart.B4, ChamPart.B12, -ChamPart.B2, ChamPart.B4, ChamPart.B12 + ChamPart.B12, ChamPart.B2, 0.05F)); + parts.put("leftArm", new RisingChamPart(-ChamPart.B8, ChamPart.B12, -ChamPart.B2, -ChamPart.B4, ChamPart.B12 + ChamPart.B12, ChamPart.B2, 0.1F)); + parts.put("rightArm", new RisingChamPart(ChamPart.B8, ChamPart.B12, -ChamPart.B2, ChamPart.B4, ChamPart.B12 + ChamPart.B12, ChamPart.B2, 0.1F)); + parts.put("leftLeg", new RisingChamPart(-ChamPart.B4, ChamPart.B0, -ChamPart.B2, ChamPart.B0, ChamPart.B12, ChamPart.B2, 0.15F)); + parts.put("rightLeg", new RisingChamPart(ChamPart.B4, ChamPart.B0, -ChamPart.B2, ChamPart.B0, ChamPart.B12, ChamPart.B2, 0.15F)); + } + + @Override + public void tick(float gravity, float maxVelocity) { + float ageDelta = getAgeDelta(); + for (RisingChamPart part : parts.values()) + part.tick(gravity, ageDelta); + age++; + } + + @Override + protected void renderPart(RisingChamPart part, PoseStack matrices, int color, float tickDelta) { + part.render(matrices, color, tickDelta, age); + } +} diff --git a/src/main/java/io/github/itzispyder/clickcrystals/modules/modules/rendering/totemchams/VortexChamRagDoll.java b/src/main/java/io/github/itzispyder/clickcrystals/modules/modules/rendering/totemchams/VortexChamRagDoll.java new file mode 100644 index 00000000..a6650343 --- /dev/null +++ b/src/main/java/io/github/itzispyder/clickcrystals/modules/modules/rendering/totemchams/VortexChamRagDoll.java @@ -0,0 +1,40 @@ +package io.github.itzispyder.clickcrystals.modules.modules.rendering.totemchams; + +import com.mojang.blaze3d.vertex.PoseStack; +import io.github.itzispyder.clickcrystals.modules.modules.rendering.totemchams.parts.ChamPart; +import io.github.itzispyder.clickcrystals.modules.modules.rendering.totemchams.parts.VortexChamPart; +import net.minecraft.world.entity.player.Player; + +public class VortexChamRagDoll extends ChamRagDoll { + + public VortexChamRagDoll(Player player, int maxAge) { + super(player, maxAge); + } + + @Override + protected void initializeParts(Player player) { + // spread the parts evenly around the vortex so they spiral out from different angles + VortexChamPart head = new VortexChamPart(-ChamPart.B4, ChamPart.B12 + ChamPart.B12, -ChamPart.B4, ChamPart.B4, ChamPart.B12 + ChamPart.B12 + ChamPart.B8, ChamPart.B4, 0F); + head.pitch = player.getXRot(); + + parts.put("head", head); + parts.put("bod", new VortexChamPart(-ChamPart.B4, ChamPart.B12, -ChamPart.B2, ChamPart.B4, ChamPart.B12 + ChamPart.B12, ChamPart.B2, 60F)); + parts.put("leftArm", new VortexChamPart(-ChamPart.B8, ChamPart.B12, -ChamPart.B2, -ChamPart.B4, ChamPart.B12 + ChamPart.B12, ChamPart.B2, 120F)); + parts.put("rightArm", new VortexChamPart(ChamPart.B8, ChamPart.B12, -ChamPart.B2, ChamPart.B4, ChamPart.B12 + ChamPart.B12, ChamPart.B2, 180F)); + parts.put("leftLeg", new VortexChamPart(-ChamPart.B4, ChamPart.B0, -ChamPart.B2, ChamPart.B0, ChamPart.B12, ChamPart.B2, 240F)); + parts.put("rightLeg", new VortexChamPart(ChamPart.B4, ChamPart.B0, -ChamPart.B2, ChamPart.B0, ChamPart.B12, ChamPart.B2, 300F)); + } + + @Override + public void tick(float gravity, float maxVelocity) { + float ageDelta = getAgeDelta(); + for (VortexChamPart part : parts.values()) + part.tick(gravity, ageDelta); + age++; + } + + @Override + protected void renderPart(VortexChamPart part, PoseStack matrices, int color, float tickDelta) { + part.render(matrices, color, tickDelta, age); + } +} diff --git a/src/main/java/io/github/itzispyder/clickcrystals/modules/modules/rendering/totemchams/parts/RisingChamPart.java b/src/main/java/io/github/itzispyder/clickcrystals/modules/modules/rendering/totemchams/parts/RisingChamPart.java new file mode 100644 index 00000000..65cb0d95 --- /dev/null +++ b/src/main/java/io/github/itzispyder/clickcrystals/modules/modules/rendering/totemchams/parts/RisingChamPart.java @@ -0,0 +1,72 @@ +package io.github.itzispyder.clickcrystals.modules.modules.rendering.totemchams.parts; + +import com.mojang.blaze3d.vertex.PoseStack; +import io.github.itzispyder.clickcrystals.util.MathUtils; +import io.github.itzispyder.clickcrystals.util.minecraft.render.RenderUtils3d; +import org.joml.Quaternionf; + +// Soul-like rise: the part drifts upward while gently swaying and slowly spinning. +public class RisingChamPart extends ChamPart { + + private final float delay, sway, swayPhase, riseSpeed; + private float progress; + + public RisingChamPart(float minX, float minY, float minZ, float maxX, float maxY, float maxZ, float delay) { + super(minX, minY, minZ, maxX, maxY, maxZ); + this.delay = delay; + this.sway = 0.01F + (float) Math.random() * 0.01F; + this.swayPhase = (float) (Math.random() * Math.PI * 2); + this.riseSpeed = 0.012F + (float) Math.random() * 0.008F; + } + + @Override + public void tick(float gravity, float ageDelta) { + progress = ageDelta; + if (ageDelta < delay) + return; + + prevX = x; + prevY = y; + prevZ = z; + prevPitch = pitch; + prevYaw = yaw; + + y += riseSpeed; + x += (float) Math.sin(ageDelta * Math.PI * 6 + swayPhase) * sway; + z += (float) Math.cos(ageDelta * Math.PI * 5 + swayPhase) * sway; + yaw += 1.5F; + pitch += 0.6F; + } + + public Quaternionf getRotation(float tickDelta) { + float pitch = (float) MathUtils.lerp(prevPitch, this.pitch, tickDelta); + float yaw = (float) MathUtils.lerp(prevYaw, this.yaw, tickDelta); + + Quaternionf qPitch = new Quaternionf().rotationX((float) Math.toRadians(pitch)); + Quaternionf qYaw = new Quaternionf().rotationY((float) Math.toRadians(yaw)); + return qYaw.mul(qPitch); + } + + @Override + public void render(PoseStack matrices, int color, float tickDelta, int age) { + float x = (float) MathUtils.lerp(prevX, this.x, tickDelta); + float y = (float) MathUtils.lerp(prevY, this.y, tickDelta); + float z = (float) MathUtils.lerp(prevZ, this.z, tickDelta); + + float cx = minX + x + (maxX - minX) / 2; + float cy = minY + y + (maxY - minY) / 2; + float cz = minZ + z + (maxZ - minZ) / 2; + + matrices.pushPose(); + matrices.rotateAround(getRotation(tickDelta), cx, cy, cz); + matrices.translate(x, y, z); + + RenderUtils3d.fillRectPrism(matrices, minX, minY, minZ, maxX, maxY, maxZ, color, true); + + int alpha = Math.min((color >> 24 & 0xFF) * 5, 0xFF); + int outline = (alpha << 24) | (color & 0x00FFFFFF); + RenderUtils3d.drawRectPrism(matrices, minX, minY, minZ, maxX, maxY, maxZ, outline, true); + + matrices.popPose(); + } +} diff --git a/src/main/java/io/github/itzispyder/clickcrystals/modules/modules/rendering/totemchams/parts/VortexChamPart.java b/src/main/java/io/github/itzispyder/clickcrystals/modules/modules/rendering/totemchams/parts/VortexChamPart.java new file mode 100644 index 00000000..a6e86ae6 --- /dev/null +++ b/src/main/java/io/github/itzispyder/clickcrystals/modules/modules/rendering/totemchams/parts/VortexChamPart.java @@ -0,0 +1,71 @@ +package io.github.itzispyder.clickcrystals.modules.modules.rendering.totemchams.parts; + +import com.mojang.blaze3d.vertex.PoseStack; +import io.github.itzispyder.clickcrystals.util.MathUtils; +import io.github.itzispyder.clickcrystals.util.minecraft.render.RenderUtils3d; +import org.joml.Quaternionf; + +// Vortex: the part spirals outward and upward around the doll's axis while spinning. +public class VortexChamPart extends ChamPart { + + private final float spinSpeed, riseSpeed; + private float angle, radius; + + public VortexChamPart(float minX, float minY, float minZ, float maxX, float maxY, float maxZ, float startAngleDeg) { + super(minX, minY, minZ, maxX, maxY, maxZ); + this.angle = (float) Math.toRadians(startAngleDeg); + this.spinSpeed = 0.18F + (float) Math.random() * 0.06F; + this.riseSpeed = 0.012F + (float) Math.random() * 0.006F; + } + + @Override + public void tick(float gravity, float ageDelta) { + prevX = x; + prevY = y; + prevZ = z; + prevPitch = pitch; + prevYaw = yaw; + + angle += spinSpeed; + radius += 0.012F; + + x = (float) (Math.cos(angle) * radius); + z = (float) (Math.sin(angle) * radius); + y += riseSpeed; + + yaw += 15F; + pitch += 8F; + } + + public Quaternionf getRotation(float tickDelta) { + float pitch = (float) MathUtils.lerp(prevPitch, this.pitch, tickDelta); + float yaw = (float) MathUtils.lerp(prevYaw, this.yaw, tickDelta); + + Quaternionf qPitch = new Quaternionf().rotationX((float) Math.toRadians(pitch)); + Quaternionf qYaw = new Quaternionf().rotationY((float) Math.toRadians(yaw)); + return qYaw.mul(qPitch); + } + + @Override + public void render(PoseStack matrices, int color, float tickDelta, int age) { + float x = (float) MathUtils.lerp(prevX, this.x, tickDelta); + float y = (float) MathUtils.lerp(prevY, this.y, tickDelta); + float z = (float) MathUtils.lerp(prevZ, this.z, tickDelta); + + float cx = minX + x + (maxX - minX) / 2; + float cy = minY + y + (maxY - minY) / 2; + float cz = minZ + z + (maxZ - minZ) / 2; + + matrices.pushPose(); + matrices.rotateAround(getRotation(tickDelta), cx, cy, cz); + matrices.translate(x, y, z); + + RenderUtils3d.fillRectPrism(matrices, minX, minY, minZ, maxX, maxY, maxZ, color, true); + + int alpha = Math.min((color >> 24 & 0xFF) * 5, 0xFF); + int outline = (alpha << 24) | (color & 0x00FFFFFF); + RenderUtils3d.drawRectPrism(matrices, minX, minY, minZ, maxX, maxY, maxZ, outline, true); + + matrices.popPose(); + } +} diff --git a/src/main/java/io/github/itzispyder/clickcrystals/modules/settings/SettingBuilder.java b/src/main/java/io/github/itzispyder/clickcrystals/modules/settings/SettingBuilder.java index 9bb75058..ff17f7d0 100644 --- a/src/main/java/io/github/itzispyder/clickcrystals/modules/settings/SettingBuilder.java +++ b/src/main/java/io/github/itzispyder/clickcrystals/modules/settings/SettingBuilder.java @@ -2,9 +2,12 @@ import io.github.itzispyder.clickcrystals.modules.ModuleSetting; +import java.util.function.Supplier; + public abstract class SettingBuilder, S extends ModuleSetting> { protected SettingChangeCallback changeAction; + protected Supplier visibility; protected String name, description; protected T def, val; @@ -12,6 +15,7 @@ public SettingBuilder() { name = description = ""; def = val = null; changeAction = setting -> {}; + visibility = () -> true; } protected T getOrDef(T val, T def) { @@ -43,11 +47,18 @@ public B onSettingChange(SettingChangeCallback changeAction) { return (B)this; } + // Only show this setting in GUIs while the condition holds. + public B visibleWhen(Supplier visibility) { + this.visibility = visibility; + return (B)this; + } + protected abstract S buildSetting(); public final S build() { S setting = buildSetting(); setting.setChangeAction((SettingChangeCallback>)changeAction); + setting.setVisibility(visibility); return setting; } } diff --git a/src/main/java/io/github/itzispyder/clickcrystals/scripting/ClickScript.java b/src/main/java/io/github/itzispyder/clickcrystals/scripting/ClickScript.java index 50cc33d6..474b2d24 100644 --- a/src/main/java/io/github/itzispyder/clickcrystals/scripting/ClickScript.java +++ b/src/main/java/io/github/itzispyder/clickcrystals/scripting/ClickScript.java @@ -1,5 +1,6 @@ package io.github.itzispyder.clickcrystals.scripting; +import io.github.itzispyder.clickcrystals.ClickCrystals; import io.github.itzispyder.clickcrystals.Global; import io.github.itzispyder.clickcrystals.client.commands.Command; import io.github.itzispyder.clickcrystals.client.system.Config; @@ -127,7 +128,7 @@ private synchronized void executeLine(String line) { public void printErrorDetails(Exception ex, String cmd) { String error = getErrorDetails(ex, cmd); - if (PlayerUtils.invalid()) { + if (PlayerUtils.invalid() || ClickCrystals.config.isDisableScriptChatErrors()) { system.printErr(error); } else { diff --git a/src/main/java/io/github/itzispyder/clickcrystals/scripting/syntax/macros/inventory/CraftCmd.java b/src/main/java/io/github/itzispyder/clickcrystals/scripting/syntax/macros/inventory/CraftCmd.java index 3f91352f..7ca8ed0f 100644 --- a/src/main/java/io/github/itzispyder/clickcrystals/scripting/syntax/macros/inventory/CraftCmd.java +++ b/src/main/java/io/github/itzispyder/clickcrystals/scripting/syntax/macros/inventory/CraftCmd.java @@ -77,14 +77,10 @@ public void onCommand(ScriptCommand command, String line, ScriptArgs args) { // One fill-then-take cycle; reschedules itself until done, materials run out, or the inventory fills up. private void craftCycle(AbstractCraftingMenu menu, RecipeDisplayId recipe, boolean all, int cyclesLeft, long delay) { - system.scheduler.runDelayedTask(() -> { - if (aborted(menu)) - return; + step(menu, delay, () -> { mc.gameMode.handlePlaceRecipe(menu.containerId, recipe, all); - system.scheduler.runDelayedTask(() -> { - if (aborted(menu)) - return; + step(menu, delay, () -> { Slot result = menu.getResultSlot(); if (!result.hasItem() || noRoomFor(result.getItem())) { crafting = false; @@ -95,21 +91,25 @@ private void craftCycle(AbstractCraftingMenu menu, RecipeDisplayId recipe, boole int left = all ? cyclesLeft : cyclesLeft - 1; if (left > 0) craftCycle(menu, recipe, all, left, delay); else crafting = false; - }, stepDelay(delay)); - }, stepDelay(delay)); - } - - // Stops the craft loop if the player left the menu or became invalid. - private boolean aborted(AbstractCraftingMenu menu) { - if (PlayerUtils.invalid() || mc.player.containerMenu != menu) { - crafting = false; - return true; - } - return false; + }); + }); } - private static long stepDelay(long base) { - return base + MIN_DELAY + (long) (Math.random() * RAND_DELAY); + // Runs a craft step on the main thread after a human-like delay. Aborts if the menu closed and + // always clears the busy flag on failure, so a dropped/throwing step can never wedge the command. + private void step(AbstractCraftingMenu menu, long delay, Runnable body) { + system.scheduler.runDelayedTask(() -> mc.execute(() -> { + try { + if (PlayerUtils.invalid() || mc.player.containerMenu != menu) { + crafting = false; + return; + } + body.run(); + } + catch (Exception e) { + crafting = false; + } + }), delay + MIN_DELAY + (long) (Math.random() * RAND_DELAY)); } // Scans the recipe book for a craftable crafting recipe whose result matches, tracking why a match may be unusable.