diff --git a/gradle.properties b/gradle.properties index 0f5855c..9483d5e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ loader_version=0.19.3 loom_version=1.17-SNAPSHOT # Mod Properties -mod_version=2.1.1-beta.1 +mod_version=2.1.2-beta.0 maven_group=com.kingjayan # Dependencies diff --git a/src/main/java/com/kingjayan/keybinds/core/action/ActionTypes.java b/src/main/java/com/kingjayan/keybinds/core/action/ActionTypes.java index 4053b3d..48d52f9 100644 --- a/src/main/java/com/kingjayan/keybinds/core/action/ActionTypes.java +++ b/src/main/java/com/kingjayan/keybinds/core/action/ActionTypes.java @@ -46,14 +46,31 @@ public final class ActionTypes { GsonHelper.getAsInt(json, "useDelay", 3), GsonHelper.getAsInt(json, "swapBackDelay", 2)))); + public static final ActionType EQUIP_ARMOR = ActionRegistry.register(new ActionType("equip_armor", + json -> new EquipArmorAction( + GsonHelper.getAsString(json, "item", ""), + GsonHelper.getAsString(json, "hotbarGate", UseItemAction.GATE_ANY), + GsonHelper.getAsBoolean(json, "vanillaSwap", false), + GsonHelper.getAsInt(json, "swapToDelay", 2), + GsonHelper.getAsInt(json, "useDelay", 3), + GsonHelper.getAsInt(json, "swapBackDelay", 2)))); + public static final ActionType SHIELD_TOTEM_SWAP = ActionRegistry.register(new ActionType("shield_totem_swap", json -> new ShieldTotemSwapAction())); public static final ActionType LOOK = ActionRegistry.register(new ActionType("look", - json -> new LookAction(GsonHelper.getAsString(json, "direction", "north")))); + json -> new LookAction( + GsonHelper.getAsString(json, "direction", "north"), + GsonHelper.getAsString(json, "motion", CameraMotion.MOTION_INSTANT), + GsonHelper.getAsString(json, "easing", "linear"), + GsonHelper.getAsInt(json, "ticks", 10)))); public static final ActionType TURN = ActionRegistry.register(new ActionType("turn", - json -> new TurnAction(GsonHelper.getAsFloat(json, "degrees", 0.0F)))); + json -> new TurnAction( + GsonHelper.getAsFloat(json, "degrees", 0.0F), + GsonHelper.getAsString(json, "motion", CameraMotion.MOTION_INSTANT), + GsonHelper.getAsString(json, "easing", "linear"), + GsonHelper.getAsInt(json, "ticks", 10)))); public static final ActionType SPRINT = ActionRegistry.register(new ActionType("sprint", json -> new SprintAction(GsonHelper.getAsString(json, "mode", "on")))); diff --git a/src/main/java/com/kingjayan/keybinds/core/action/CameraMotion.java b/src/main/java/com/kingjayan/keybinds/core/action/CameraMotion.java new file mode 100644 index 0000000..253399c --- /dev/null +++ b/src/main/java/com/kingjayan/keybinds/core/action/CameraMotion.java @@ -0,0 +1,81 @@ +package com.kingjayan.keybinds.core.action; + +import com.kingjayan.keybinds.core.schedule.TickScheduler; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.util.Mth; + +import java.util.concurrent.ThreadLocalRandom; + +final class CameraMotion { + + static final String MOTION_INSTANT = "instant"; + static final String MOTION_LERP = "lerp"; + static final String MOTION_JITTER = "jitter"; + static final String MOTION_RANDOM = "random"; + + private static final float JITTER_DEGREES = 2.0F; + + private CameraMotion() { + } + + static int animate(LocalPlayer player, TickScheduler scheduler, float yawDelta, float pitchDelta, + String motion, String easing, int ticks) { + if (motion == null || motion.equals(MOTION_INSTANT) || ticks <= 0) { + apply(player, player.getYRot() + yawDelta, player.getXRot() + pitchDelta); + return 0; + } + float startYaw = player.getYRot(); + float startPitch = player.getXRot(); + float[] progress = progressCurve(motion, easing, ticks); + boolean jitter = motion.equals(MOTION_JITTER); + ThreadLocalRandom random = ThreadLocalRandom.current(); + for (int step = 1; step <= ticks; step++) { + float p = progress[step - 1]; + float noise = jitter && step < ticks + ? (random.nextFloat() * 2.0F - 1.0F) * JITTER_DEGREES * (1.0F - p) : 0.0F; + float yaw = startYaw + yawDelta * p + noise; + float pitch = startPitch + pitchDelta * p + (pitchDelta != 0.0F ? noise : 0.0F); + scheduler.schedule(step, () -> apply(player, yaw, pitch)); + } + return ticks; + } + + private static void apply(LocalPlayer player, float yaw, float pitch) { + player.setYRot(Mth.wrapDegrees(yaw)); + player.setXRot(Mth.clamp(pitch, -90.0F, 90.0F)); + } + + private static float[] progressCurve(String motion, String easing, int ticks) { + float[] progress = new float[ticks]; + if (motion.equals(MOTION_RANDOM)) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + float total = 0.0F; + for (int i = 0; i < ticks; i++) { + total += random.nextFloat() + 0.1F; + progress[i] = total; + } + for (int i = 0; i < ticks; i++) { + progress[i] = ease(easing, progress[i] / total); + } + } else { + for (int i = 0; i < ticks; i++) { + progress[i] = ease(easing, (i + 1.0F) / ticks); + } + } + progress[ticks - 1] = 1.0F; + return progress; + } + + private static float ease(String easing, float p) { + return switch (easing == null ? "linear" : easing) { + case "cubic" -> p < 0.5F + ? 4.0F * p * p * p + : 1.0F - (float) Math.pow(-2.0F * p + 2.0F, 3) / 2.0F; + case "quartic" -> p < 0.5F + ? 8.0F * p * p * p * p + : 1.0F - (float) Math.pow(-2.0F * p + 2.0F, 4) / 2.0F; + case "bezier" -> p * p * (3.0F - 2.0F * p); + default -> p; + }; + } +} diff --git a/src/main/java/com/kingjayan/keybinds/core/action/EquipArmorAction.java b/src/main/java/com/kingjayan/keybinds/core/action/EquipArmorAction.java new file mode 100644 index 0000000..56cc0bb --- /dev/null +++ b/src/main/java/com/kingjayan/keybinds/core/action/EquipArmorAction.java @@ -0,0 +1,28 @@ +package com.kingjayan.keybinds.core.action; + +import com.google.gson.JsonObject; + +public record EquipArmorAction(String item, String hotbarGate, boolean vanillaSwap, + int swapToDelay, int useDelay, int swapBackDelay) implements Action { + + @Override + public ActionType type() { + return ActionTypes.EQUIP_ARMOR; + } + + @Override + public void write(JsonObject out) { + out.addProperty("item", item); + out.addProperty("hotbarGate", hotbarGate); + out.addProperty("vanillaSwap", vanillaSwap); + out.addProperty("swapToDelay", swapToDelay); + out.addProperty("useDelay", useDelay); + out.addProperty("swapBackDelay", swapBackDelay); + } + + @Override + public ActionResult execute(ActionContext ctx) { + return new UseItemAction(item, hotbarGate, vanillaSwap, + swapToDelay, useDelay, swapBackDelay).execute(ctx); + } +} diff --git a/src/main/java/com/kingjayan/keybinds/core/action/LookAction.java b/src/main/java/com/kingjayan/keybinds/core/action/LookAction.java index afaf220..16c214e 100644 --- a/src/main/java/com/kingjayan/keybinds/core/action/LookAction.java +++ b/src/main/java/com/kingjayan/keybinds/core/action/LookAction.java @@ -4,7 +4,7 @@ import net.minecraft.client.player.LocalPlayer; import net.minecraft.util.Mth; -public record LookAction(String direction) implements Action { +public record LookAction(String direction, String motion, String easing, int ticks) implements Action { @Override public ActionType type() { @@ -14,6 +14,9 @@ public ActionType type() { @Override public void write(JsonObject out) { out.addProperty("direction", direction); + out.addProperty("motion", motion); + out.addProperty("easing", easing); + out.addProperty("ticks", ticks); } @Override @@ -22,18 +25,22 @@ public ActionResult execute(ActionContext ctx) { if (player == null) { return ActionResult.proceed(); } + float yawDelta = 0.0F; + float pitchDelta = 0.0F; switch (direction) { - case "north" -> player.setYRot(180.0F); - case "south" -> player.setYRot(0.0F); - case "east" -> player.setYRot(-90.0F); - case "west" -> player.setYRot(90.0F); - case "up" -> player.setXRot(-90.0F); - case "down" -> player.setXRot(90.0F); - case "left" -> player.setYRot(Mth.wrapDegrees(player.getYRot() - 90.0F)); - case "right" -> player.setYRot(Mth.wrapDegrees(player.getYRot() + 90.0F)); + case "north" -> yawDelta = Mth.wrapDegrees(180.0F - player.getYRot()); + case "south" -> yawDelta = Mth.wrapDegrees(0.0F - player.getYRot()); + case "east" -> yawDelta = Mth.wrapDegrees(-90.0F - player.getYRot()); + case "west" -> yawDelta = Mth.wrapDegrees(90.0F - player.getYRot()); + case "up" -> pitchDelta = -90.0F - player.getXRot(); + case "down" -> pitchDelta = 90.0F - player.getXRot(); + case "left" -> yawDelta = -90.0F; + case "right" -> yawDelta = 90.0F; default -> { + return ActionResult.proceed(); } } - return ActionResult.proceed(); + int delay = CameraMotion.animate(player, ctx.scheduler(), yawDelta, pitchDelta, motion, easing, ticks); + return ActionResult.delay(delay); } } diff --git a/src/main/java/com/kingjayan/keybinds/core/action/TurnAction.java b/src/main/java/com/kingjayan/keybinds/core/action/TurnAction.java index 47c92ec..0d6a781 100644 --- a/src/main/java/com/kingjayan/keybinds/core/action/TurnAction.java +++ b/src/main/java/com/kingjayan/keybinds/core/action/TurnAction.java @@ -4,7 +4,7 @@ import net.minecraft.client.player.LocalPlayer; import net.minecraft.util.Mth; -public record TurnAction(float degrees) implements Action { +public record TurnAction(float degrees, String motion, String easing, int ticks) implements Action { @Override public ActionType type() { @@ -14,14 +14,19 @@ public ActionType type() { @Override public void write(JsonObject out) { out.addProperty("degrees", degrees); + out.addProperty("motion", motion); + out.addProperty("easing", easing); + out.addProperty("ticks", ticks); } @Override public ActionResult execute(ActionContext ctx) { LocalPlayer player = ctx.client() != null ? ctx.client().player : null; - if (player != null) { - player.setYRot(Mth.wrapDegrees(degrees)); + if (player == null) { + return ActionResult.proceed(); } - return ActionResult.proceed(); + float yawDelta = Mth.wrapDegrees(degrees - player.getYRot()); + int delay = CameraMotion.animate(player, ctx.scheduler(), yawDelta, 0.0F, motion, easing, ticks); + return ActionResult.delay(delay); } } diff --git a/src/main/java/com/kingjayan/keybinds/core/trigger/EquippedTrigger.java b/src/main/java/com/kingjayan/keybinds/core/trigger/EquippedTrigger.java new file mode 100644 index 0000000..ba20368 --- /dev/null +++ b/src/main/java/com/kingjayan/keybinds/core/trigger/EquippedTrigger.java @@ -0,0 +1,29 @@ +package com.kingjayan.keybinds.core.trigger; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import java.util.List; + +public record EquippedTrigger(List items, String enchant) implements Trigger { + + @Override + public TriggerType type() { + return TriggerTypes.EQUIPPED; + } + + @Override + public void write(JsonObject out) { + JsonArray array = new JsonArray(); + for (String item : items) { + array.add(item); + } + out.add("items", array); + out.addProperty("enchant", enchant); + } + + @Override + public TriggerResult evaluate(TriggerContext ctx) { + return new TriggerResult(ctx.equippedAny(items, enchant), false); + } +} diff --git a/src/main/java/com/kingjayan/keybinds/core/trigger/TriggerContext.java b/src/main/java/com/kingjayan/keybinds/core/trigger/TriggerContext.java index 9c1d06c..b20c67a 100644 --- a/src/main/java/com/kingjayan/keybinds/core/trigger/TriggerContext.java +++ b/src/main/java/com/kingjayan/keybinds/core/trigger/TriggerContext.java @@ -77,6 +77,10 @@ public boolean holdingAny(List items) { return ItemLookup.matchesAny(client.player.getMainHandItem(), items); } + public boolean equippedAny(List items, String enchant) { + return client != null && ItemLookup.equippedAny(client.player, items, enchant); + } + public boolean edge(Trigger node, boolean active) { current.put(node, active); boolean was = previous.getOrDefault(node, false); diff --git a/src/main/java/com/kingjayan/keybinds/core/trigger/TriggerTypes.java b/src/main/java/com/kingjayan/keybinds/core/trigger/TriggerTypes.java index 45c55a4..eaa4a17 100644 --- a/src/main/java/com/kingjayan/keybinds/core/trigger/TriggerTypes.java +++ b/src/main/java/com/kingjayan/keybinds/core/trigger/TriggerTypes.java @@ -36,6 +36,9 @@ public final class TriggerTypes { public static final TriggerType HOLDING = TriggerRegistry.register(new TriggerType("holding", json -> new HoldingTrigger(readStrings(json, "items")))); + public static final TriggerType EQUIPPED = TriggerRegistry.register(new TriggerType("equipped", + json -> new EquippedTrigger(readStrings(json, "items"), GsonHelper.getAsString(json, "enchant", "")))); + public static final TriggerType KEY_COMBO = TriggerRegistry.register(new TriggerType("key_combo", json -> new KeyComboTrigger(readStrings(json, "keys")))); diff --git a/src/main/java/com/kingjayan/keybinds/dsl/ScriptCompiler.java b/src/main/java/com/kingjayan/keybinds/dsl/ScriptCompiler.java index 27c6f97..04947a7 100644 --- a/src/main/java/com/kingjayan/keybinds/dsl/ScriptCompiler.java +++ b/src/main/java/com/kingjayan/keybinds/dsl/ScriptCompiler.java @@ -221,6 +221,8 @@ private static Condition leafCondition(CallExpr call) { case "condition" -> new ScriptProgram.PlayerCondition(json.get("condition").getAsString()); case "holding" -> new ScriptProgram.HoldingCondition( EditorSpecs.readStringList(json, "items")); + case "equipped" -> new ScriptProgram.EquippedCondition( + EditorSpecs.readStringList(json, "items"), json.get("enchant").getAsString()); case "key_combo" -> new ScriptProgram.ComboCondition( EditorSpecs.readStringList(json, "keys")); case "riding" -> new ScriptProgram.RidingCondition(); diff --git a/src/main/java/com/kingjayan/keybinds/dsl/ScriptDocs.java b/src/main/java/com/kingjayan/keybinds/dsl/ScriptDocs.java index bff3972..1df5573 100644 --- a/src/main/java/com/kingjayan/keybinds/dsl/ScriptDocs.java +++ b/src/main/java/com/kingjayan/keybinds/dsl/ScriptDocs.java @@ -30,7 +30,7 @@ static Entry text(String description) { ENTRIES.add(Entry.text("A script starts with one 'when ' line, followed by actions, one per line. '#' starts a comment.")); ENTRIES.add(Entry.text("Triggers combine with and / or / not and parentheses: when key(r) and not riding")); ENTRIES.add(Entry.text("Arguments are positional or named: use_item(item=\"minecraft:ender_pearl\", useDelay=3). Lists use [\"a\", \"b\"].")); - ENTRIES.add(Entry.text("if { ... } else { ... } branches while the macro runs. Checks are live state: holding, riding, key (held down), mouse, slot_selected, held_durability, and any player condition.")); + ENTRIES.add(Entry.text("if { ... } else if { ... } else { ... } branches while the macro runs. Checks combine with and / or / not and parentheses. Checks are live state: holding, equipped, riding, key (held down), mouse, slot_selected, held_durability, and any player condition.")); ENTRIES.add(Entry.text("Key names: a-z, 0-9, f1-f25, space, left_shift, ... or a raw GLFW code. Mouse: left, right, middle, button4+.")); ENTRIES.add(Entry.text("random(min, max) can replace any number argument and rolls a new value on every run: use_item(useDelay=random(0, 2)).")); @@ -41,6 +41,7 @@ static Entry text(String description) { trigger("mouse_release", "mouse_release(left)", "Fires when the mouse button is released."); trigger("combo", "combo(ctrl, c)", "Fires when the modifiers (ctrl, shift, alt) plus at most one normal key are held together; modifiers match either side. Inside if: true while all held."); trigger("holding", "holding(\"minecraft:mace\")", "Fires when you start holding one of the items; empty matches anything."); + trigger("equipped", "equipped(\"minecraft:elytra\", enchant=\"minecraft:unbreaking\")", "Fires when an armor piece or elytra is equipped; items and enchant are optional filters, multiple items use [\"a\", \"b\"]."); trigger("slot_selected", "slot_selected(0)", "Fires when the hotbar slot (0-8) is selected."); trigger("riding", "riding", "Fires when you mount a vehicle or mob."); trigger("held_durability", "held_durability(10)", "Fires when the held item's remaining durability drops to the value or less."); @@ -58,8 +59,9 @@ static Entry text(String description) { action("use", "use", "Uses the held item once."); action("use_item", "use_item(item=\"minecraft:ender_pearl\", hotbarGate=any, vanillaSwap=false, swapToDelay=2, useDelay=3, swapBackDelay=2)", "Swaps to an item, uses it, and swaps back with configurable tick delays."); action("shield_totem_swap", "shield_totem_swap", "Swaps shield and totem in the offhand."); - action("look", "look(north)", "Snaps the camera toward north, south, east, west, up, down, or 90° left/right."); - action("turn", "turn(90)", "Sets the camera yaw to the given degrees (0 south, 90 west, 180 north, -90 east)."); + action("equip_armor", "equip_armor(item=\"minecraft:elytra\", useDelay=3)", "Equips an armor piece or elytra by using it; same arguments and swap behavior as use_item."); + action("look", "look(north, motion=lerp, easing=cubic, ticks=10)", "Moves the camera toward north, south, east, west, up, down, or 90° left/right; motion is instant, lerp, jitter, or random with linear, cubic, quartic, or bezier easing over ticks."); + action("turn", "turn(90, motion=jitter, ticks=8)", "Turns the camera to the given yaw (0 south, 90 west, 180 north, -90 east); motion is instant, lerp, jitter, or random with linear, cubic, quartic, or bezier easing over ticks."); action("sprint", "sprint(on)", "Turns sprinting on, off, or toggle."); action("sneak", "sneak(toggle)", "Turns sneaking on, off, or toggle."); action("jump", "jump", "Taps the jump key once."); diff --git a/src/main/java/com/kingjayan/keybinds/dsl/ScriptParser.java b/src/main/java/com/kingjayan/keybinds/dsl/ScriptParser.java index 571fccb..2d551aa 100644 --- a/src/main/java/com/kingjayan/keybinds/dsl/ScriptParser.java +++ b/src/main/java/com/kingjayan/keybinds/dsl/ScriptParser.java @@ -81,9 +81,13 @@ private IfStmt parseIf() { List elseBody = List.of(); if (peek().type() == TokenType.IDENT && peek().text().equals("else")) { advance(); - expect(TokenType.LBRACE, "'{' after else"); - elseBody = parseStatements(TokenType.RBRACE); - expect(TokenType.RBRACE, "'}' to close else block"); + if (peek().type() == TokenType.IDENT && peek().text().equals("if")) { + elseBody = List.of(parseIf()); + } else { + expect(TokenType.LBRACE, "'{' after else"); + elseBody = parseStatements(TokenType.RBRACE); + expect(TokenType.RBRACE, "'}' to close else block"); + } } return new IfStmt(condition, thenBody, elseBody, line); } diff --git a/src/main/java/com/kingjayan/keybinds/dsl/ScriptPrinter.java b/src/main/java/com/kingjayan/keybinds/dsl/ScriptPrinter.java index e1936e7..177459f 100644 --- a/src/main/java/com/kingjayan/keybinds/dsl/ScriptPrinter.java +++ b/src/main/java/com/kingjayan/keybinds/dsl/ScriptPrinter.java @@ -53,6 +53,13 @@ private static String expr(JsonObject json, int depth) { case "mouse_release" -> "mouse_release(" + keyArg(json, "button", false) + ")"; case "condition" -> GsonHelper.getAsString(json, "condition", "low_health"); case "holding" -> "holding(" + quotedList(json, "items") + ")"; + case "equipped" -> { + String enchant = GsonHelper.getAsString(json, "enchant", ""); + String items = quotedList(json, "items"); + yield "equipped(" + items + + (enchant.isEmpty() ? "" : (items.isEmpty() ? "" : ", ") + "enchant=\"" + enchant + "\"") + + ")"; + } case "key_combo" -> "combo(" + String.join(", ", EditorSpecs.readStringList(json, "keys")) + ")"; case "slot_selected" -> "slot_selected(" + GsonHelper.getAsInt(json, "slot", 0) + ")"; case "riding" -> "riding"; diff --git a/src/main/java/com/kingjayan/keybinds/dsl/ScriptProgram.java b/src/main/java/com/kingjayan/keybinds/dsl/ScriptProgram.java index 1efb8a2..3cf9522 100644 --- a/src/main/java/com/kingjayan/keybinds/dsl/ScriptProgram.java +++ b/src/main/java/com/kingjayan/keybinds/dsl/ScriptProgram.java @@ -46,7 +46,7 @@ public static Instruction jumpIfFalse(Condition condition) { public sealed interface Condition permits AllCondition, AnyCondition, NotCondition, KeyDownCondition, MouseDownCondition, PlayerCondition, HoldingCondition, RidingCondition, SlotCondition, - DurabilityCondition, ComboCondition { + DurabilityCondition, ComboCondition, EquippedCondition { boolean test(Minecraft client); } @@ -130,6 +130,14 @@ public boolean test(Minecraft client) { } } + public record EquippedCondition(List items, String enchant) implements Condition { + + @Override + public boolean test(Minecraft client) { + return client != null && ItemLookup.equippedAny(client.player, items, enchant); + } + } + public record RidingCondition() implements Condition { @Override diff --git a/src/main/java/com/kingjayan/keybinds/game/ItemLookup.java b/src/main/java/com/kingjayan/keybinds/game/ItemLookup.java index e95515a..6ea21dc 100644 --- a/src/main/java/com/kingjayan/keybinds/game/ItemLookup.java +++ b/src/main/java/com/kingjayan/keybinds/game/ItemLookup.java @@ -4,7 +4,9 @@ import net.minecraft.core.registries.Registries; import net.minecraft.resources.Identifier; import net.minecraft.tags.TagKey; +import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.enchantment.ItemEnchantments; @@ -65,6 +67,29 @@ public static int enchantLevel(ItemStack stack, String enchantId) { return 0; } + private static final EquipmentSlot[] ARMOR_SLOTS = { + EquipmentSlot.HEAD, EquipmentSlot.CHEST, EquipmentSlot.LEGS, EquipmentSlot.FEET + }; + + public static boolean equippedAny(Player player, List items, String enchant) { + if (player == null) { + return false; + } + boolean anyItem = items == null || items.isEmpty(); + boolean anyEnchant = enchant == null || enchant.isBlank(); + for (EquipmentSlot slot : ARMOR_SLOTS) { + ItemStack stack = player.getItemBySlot(slot); + if (stack.isEmpty()) { + continue; + } + if ((anyItem || matchesAny(stack, items)) + && (anyEnchant || enchantLevel(stack, enchant) > 0)) { + return true; + } + } + return false; + } + public static int findHotbarSlot(Inventory inventory, List items, String tag) { TagKey tagKey = resolveTag(tag); for (int slot = 0; slot < Inventory.SELECTION_SIZE; slot++) { diff --git a/src/main/java/com/kingjayan/keybinds/ui/editor/EditorSpecs.java b/src/main/java/com/kingjayan/keybinds/ui/editor/EditorSpecs.java index c2f4ae0..57c0ed1 100644 --- a/src/main/java/com/kingjayan/keybinds/ui/editor/EditorSpecs.java +++ b/src/main/java/com/kingjayan/keybinds/ui/editor/EditorSpecs.java @@ -65,11 +65,24 @@ public record TypeSpec(String id, String label, Nesting nesting, List ParamSpec.of("useDelay", "Use ticks", Kind.INT, "3"), ParamSpec.of("swapBackDelay", "Swap-back ticks", Kind.INT, "2")); action("shield_totem_swap", "Shield/totem offhand swap"); + action("equip_armor", "Equip armor", + ParamSpec.of("item", "Item id", Kind.STRING, ""), + ParamSpec.choice("hotbarGate", "Only fire when", "any", List.of("any", "in_hotbar", "not_in_hotbar")), + ParamSpec.of("vanillaSwap", "Vanilla swap", Kind.BOOL, "false"), + ParamSpec.of("swapToDelay", "Swap-to ticks", Kind.INT, "2"), + ParamSpec.of("useDelay", "Use ticks", Kind.INT, "3"), + ParamSpec.of("swapBackDelay", "Swap-back ticks", Kind.INT, "2")); action("look", "Look toward", ParamSpec.choice("direction", "Direction", "north", - List.of("north", "south", "east", "west", "up", "down", "left", "right"))); + List.of("north", "south", "east", "west", "up", "down", "left", "right")), + ParamSpec.choice("motion", "Motion", "instant", List.of("instant", "lerp", "jitter", "random")), + ParamSpec.choice("easing", "Easing", "linear", List.of("linear", "cubic", "quartic", "bezier")), + ParamSpec.of("ticks", "Motion ticks", Kind.INT, "10")); action("turn", "Turn to yaw", - ParamSpec.of("degrees", "Yaw degrees", Kind.FLOAT, "0")); + ParamSpec.of("degrees", "Yaw degrees", Kind.FLOAT, "0"), + ParamSpec.choice("motion", "Motion", "instant", List.of("instant", "lerp", "jitter", "random")), + ParamSpec.choice("easing", "Easing", "linear", List.of("linear", "cubic", "quartic", "bezier")), + ParamSpec.of("ticks", "Motion ticks", Kind.INT, "10")); action("sprint", "Sprint", ParamSpec.choice("mode", "Mode", "on", List.of("on", "off", "toggle"))); action("sneak", "Sneak", @@ -95,6 +108,9 @@ public record TypeSpec(String id, String label, Nesting nesting, List List.of(ParamSpec.choice("condition", "Condition", "low_health", PlayerConditions.ids()))); trigger("holding", "Holding item", Nesting.NONE, List.of(ParamSpec.of("items", "Item ids", Kind.STRING_LIST, ""))); + trigger("equipped", "Armor equipped", Nesting.NONE, + List.of(ParamSpec.of("items", "Item ids", Kind.STRING_LIST, ""), + ParamSpec.of("enchant", "Required enchant", Kind.STRING, ""))); trigger("key_combo", "Key combo", Nesting.NONE, List.of(ParamSpec.of("keys", "Keys (ctrl, c)", Kind.STRING_LIST, ""))); trigger("slot_selected", "Hotbar slot selected", Nesting.NONE, @@ -166,8 +182,11 @@ public static String describeAction(JsonObject json) { case "use" -> "Use held item"; case "use_item" -> "Use " + orAny(GsonHelper.getAsString(json, "item", "")); case "shield_totem_swap" -> "Shield/totem offhand swap"; - case "look" -> "Look " + GsonHelper.getAsString(json, "direction", "north"); - case "turn" -> "Turn to " + trimFloat(GsonHelper.getAsFloat(json, "degrees", 0.0F)) + "°"; + case "equip_armor" -> "Equip " + orAny(GsonHelper.getAsString(json, "item", "")); + case "look" -> "Look " + GsonHelper.getAsString(json, "direction", "north") + + motionSuffix(json); + case "turn" -> "Turn to " + trimFloat(GsonHelper.getAsFloat(json, "degrees", 0.0F)) + "°" + + motionSuffix(json); case "sprint" -> "Sprint " + GsonHelper.getAsString(json, "mode", "on"); case "sneak" -> "Sneak " + GsonHelper.getAsString(json, "mode", "on"); case "jump" -> "Jump"; @@ -189,6 +208,9 @@ public static String describeTrigger(JsonObject json) { case "mouse_release" -> "Mouse release: " + buttonName(GsonHelper.getAsInt(json, "button", -1)); case "condition" -> "Condition: " + GsonHelper.getAsString(json, "condition", ""); case "holding" -> "Holding " + orAny(joinList(json, "items")); + case "equipped" -> "Equipped " + orAny(joinList(json, "items")) + + (GsonHelper.getAsString(json, "enchant", "").isEmpty() + ? "" : " with " + GsonHelper.getAsString(json, "enchant", "")); case "key_combo" -> "Combo: " + orAny(String.join(" + ", readStringList(json, "keys"))); case "slot_selected" -> "Slot " + (GsonHelper.getAsInt(json, "slot", 0) + 1) + " selected"; case "riding" -> "Riding a mount"; @@ -219,6 +241,11 @@ public static String buttonName(int button) { }; } + private static String motionSuffix(JsonObject json) { + String motion = GsonHelper.getAsString(json, "motion", "instant"); + return motion.equals("instant") ? "" : " (" + motion + ")"; + } + private static String itemsOrTag(JsonObject json) { String items = joinList(json, "items"); if (!items.isEmpty()) { diff --git a/src/test/java/com/kingjayan/keybinds/ScriptDslTest.java b/src/test/java/com/kingjayan/keybinds/ScriptDslTest.java index 4376d61..7d73146 100644 --- a/src/test/java/com/kingjayan/keybinds/ScriptDslTest.java +++ b/src/test/java/com/kingjayan/keybinds/ScriptDslTest.java @@ -7,6 +7,7 @@ import com.kingjayan.keybinds.core.action.RandomWaitAction; import com.kingjayan.keybinds.core.macro.Macro; import com.kingjayan.keybinds.core.schedule.TickScheduler; +import com.kingjayan.keybinds.core.trigger.EquippedTrigger; import com.kingjayan.keybinds.core.trigger.KeyComboTrigger; import com.kingjayan.keybinds.core.trigger.TriggerRegistry; import com.kingjayan.keybinds.core.trigger.TriggerTypes; @@ -250,6 +251,73 @@ void combosRequireModifiersPlusOneKey() { new KeyComboTrigger(List.of("ctrl", "c", "r")).keys()); } + @Test + void elseIfChainsAndLogicGatesCompile() { + ScriptProgram program = ScriptCompiler.compile(""" + when key(g) + if riding and (sneaking or holding("minecraft:mace")) and not key(r) { + attack + } else if sneaking { + use + } else { + jump + } + """); + List instructions = program.instructions(); + assertEquals(7, instructions.size()); + assertEquals(ScriptProgram.Instruction.JUMP_IF_FALSE, instructions.get(0).op); + assertEquals(3, instructions.get(0).target); + assertEquals(ScriptProgram.Instruction.JUMP, instructions.get(2).op); + assertEquals(7, instructions.get(2).target); + assertEquals(ScriptProgram.Instruction.JUMP_IF_FALSE, instructions.get(3).op); + assertEquals(6, instructions.get(3).target); + assertEquals(ScriptProgram.Instruction.JUMP, instructions.get(5).op); + assertEquals(7, instructions.get(5).target); + } + + @Test + void equippedTriggerAndEquipArmorCompile() { + ScriptProgram program = ScriptCompiler.compile(""" + when equipped("minecraft:elytra", enchant="minecraft:unbreaking") + equip_armor(item="minecraft:netherite_chestplate", useDelay=5) + """); + EquippedTrigger trigger = (EquippedTrigger) program.trigger(); + assertEquals(List.of("minecraft:elytra"), trigger.items()); + assertEquals("minecraft:unbreaking", trigger.enchant()); + JsonObject json = TriggerRegistry.toJson(program.trigger()); + assertEquals(json, TriggerRegistry.toJson(TriggerRegistry.fromJson(json))); + JsonObject action = ActionRegistry.toJson(program.instructions().get(0).action); + assertEquals("equip_armor", action.get("type").getAsString()); + assertEquals(5, action.get("useDelay").getAsInt()); + assertEquals("any", action.get("hotbarGate").getAsString()); + ScriptProgram conditional = ScriptCompiler.compile(""" + when key(g) + if equipped("minecraft:elytra") { attack } + """); + assertEquals(2, conditional.instructions().size()); + } + + @Test + void cameraMotionArgumentsCompile() { + ScriptProgram program = ScriptCompiler.compile(""" + when key(g) + turn(90, motion=lerp, easing=cubic, ticks=8) + look(north, motion=jitter) + look(up) + """); + JsonObject turn = ActionRegistry.toJson(program.instructions().get(0).action); + assertEquals("lerp", turn.get("motion").getAsString()); + assertEquals("cubic", turn.get("easing").getAsString()); + assertEquals(8, turn.get("ticks").getAsInt()); + JsonObject look = ActionRegistry.toJson(program.instructions().get(1).action); + assertEquals("jitter", look.get("motion").getAsString()); + assertEquals("linear", look.get("easing").getAsString()); + JsonObject snap = ActionRegistry.toJson(program.instructions().get(2).action); + assertEquals("instant", snap.get("motion").getAsString()); + assertThrows(ScriptException.class, + () -> ScriptCompiler.compile("when key(g)\nturn(90, motion=warp)")); + } + @Test void reportsErrorsWithLineNumbers() { assertEquals(1, assertThrows(ScriptException.class,