Skip to content

REALMWTH/Discord-The-Last-Meadow-Auto-Script

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

31 Commits
 
 
 
 

Repository files navigation

Note

Grass Toucher is defeated, event is over ;) Thanks everyone for participation! See you next time!

Discord The Last Meadow Auto Script

Automation script for The Last Meadow mini-games inside Discord.

Features

  • Auto-click Grass Toucher
  • Auto-click available Activity buttons when no cooldown is active
  • Auto-click Continue button
  • Auto-click Go Back when the message Looks like we're out of resources :(. Once someone collects some more, try again! appears
  • Ranger mode: auto-detects targets and clicks them quickly
  • Paladin mode: auto-detects battle, tracks shield in real time, supports dual-projectile prioritization, blocks real mouse input inside Paladin arena
  • Priest mode: auto-detects 3x3 glyph board and solves triplets by grouping matching glyph signatures
  • Craft mode: reads and executes key sequences automatically
  • Global stop command: stopBot()

Requirements

  • Browser with Tampermonkey (or alternative) support (discord.com/app)

OR

  • Discord client (PC only, Console Paste method only)

Installation Methods

Method 1: Tampermonkey (or any alternative) (Recommended - Works on Desktop & Android) ⭐

Pros: Works on both desktop and mobile (Android), automatic updates, no need to paste on page reload Cons: Supports only browsers

  1. Install Tampermonkey first if you don't have it:

  2. Install Script:

    OR

    • Download/copy last-meadow-auto.user.js from GitHub
    • Open Tampermonkey dashboard
    • Select "Create New Script"
    • Paste entire script content (including // ==UserScript== header)
    • Save (Ctrl+S)

Method 2: Console Paste (Quick Test, Works on Desktop Only)

Pros: No browser extensions needed, works in Discord client Cons: Need to paste after each page reload, desktop only

  1. Open Discord and navigate to The Last Meadow.
  2. Open Developer Tools. Windows/Linux: Ctrl + Shift + I. macOS: Cmd + Option + I.
  3. Open the Console tab. If paste is blocked, type allow pasting and press Enter button.
  4. Paste the script below.
  5. Press Enter.
Script
(function () {
  "use strict";

  // Stop previous instance if present.
  try {
      if (typeof window.stopBot === "function") window.stopBot();
  } catch {}

  const RANGER_HASH = "16fb25536f00a7996cbdf5bfff2ef0d09459f580af9e67d380263f5ead43055e";
  const GO_BACK_BTN_SEL = ".button__65fca.buttonWhite__65fca.clickable__5c90e";
  const PRIEST_MATCHED_CLASS = "matched__0dcd3";
  const OUT_OF_RESOURCES_RE = /out of resources/i;

  const SELECTORS = Object.freeze({
      target: ".targetContainer_b6b008",
      clickable: ".clickable__5c90e",
      seq: ".sequences__34527",
      char: ".character__34527 img[alt]",
      cont: ".continueButtonWrapper__24749 .clickable__5c90e",
      activity: ".activityButton__8af73",
      cooldown: ".countdown__8af73",

      // Paladin
      projectile: ".projectile_cce732",
      shield: ".shield_cce732",
      palRoot: ".container__24749, .game__24749, .shaker_cce732",

      // Priest
      priestGame: ".game__5c62c",
      priestGrid: ".grid__0dcd3",
      priestItem: ".gridItem__0dcd3",
      priestGlyph: ".gridAssetGlyph__0dcd3",

      // Modal
      modalResourceText: ".text_a2a25a, .text-lg\\/normal_cf4812, [data-text-variant='text-lg/normal']"
  });

  const CONFIG = Object.freeze({
      dragonMs: 50,
      activityMs: 50,
      pollMs: 25,
      settleMs: 20,
      keyDelayMs: 70,
      craftRetrySameSeqMs: 700,
      craftPostKeySettleMs: 30,
      craftStepAckTimeoutMs: 280,
      craftStepPollMs: 20,
      craftRetryWholeMax: 3,
      craftRetryBackoffMs: 140,

      // Paladin
      palSmooth: 1,
      palAimY: 0.93,
      palTopDelta: 120,
      palDualCoverRatio: 1.08,
      palDefaultShieldW: 138,
      palDefaultProjW: 115,
      palMinShieldW: 96,
      blockRealMouse: true,

      // Priest
      priestClickDelayMs: 28,
      priestTripletDelayMs: 120,

      // Go Back modal
      goBackScanMs: 60,
      goBackCooldownMs: 250
  });

  const KEY_MAP = Object.freeze({
      ArrowLeft: Object.freeze({ key: "ArrowLeft", code: "ArrowLeft", keyCode: 37, which: 37 }),
      ArrowRight: Object.freeze({ key: "ArrowRight", code: "ArrowRight", keyCode: 39, which: 39 }),
      ArrowUp: Object.freeze({ key: "ArrowUp", code: "ArrowUp", keyCode: 38, which: 38 }),
      ArrowDown: Object.freeze({ key: "ArrowDown", code: "ArrowDown", keyCode: 40, which: 40 }),
      " ": Object.freeze({ key: " ", code: "Space", keyCode: 32, which: 32 }),
      Space: Object.freeze({ key: " ", code: "Space", keyCode: 32, which: 32 })
  });

  const MOUSE_LOCK_EVENTS = Object.freeze([
      "pointermove",
      "mousemove",
      "dragstart",
      "pointerdown",
      "mousedown",
      "pointerup",
      "mouseup"
  ]);

  const state = {
      mode: null,

      // Paladin
      palRaf: 0,
      palDrag: false,
      palRoot: null,
      mouseLockHandler: null,
      projectileMeta: new WeakMap(),
      liveProjectileSprites: new Set(),

      // Priest
      priestRaf: 0,
      priestBusy: false,

      // Shared
      craftBusy: false,
      lastSeqKey: "",
      lastSeqSentAt: 0,
      clickedContinue: new WeakSet(),
      lastGoBackClickAt: 0,

      // Intervals
      dragonTimer: 0,
      activityTimer: 0,
      goBackTimer: 0,
      pollTimer: 0,

      observer: null
  };

  const queryOne = (selector, root = document) => root.querySelector(selector);
  const queryAll = (selector, root = document) => Array.from(root.querySelectorAll(selector));
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

  function toNum(value) {
      const n = parseFloat(value);
      return Number.isFinite(n) ? n : null;
  }

  function isVisible(el) {
      if (!(el instanceof Element)) return false;
      if (!document.contains(el)) return false;

      const rect = el.getBoundingClientRect();
      if (rect.width < 2 || rect.height < 2) return false;

      const style = getComputedStyle(el);
      if (style.display === "none" || style.visibility === "hidden") return false;

      const opacity = parseFloat(style.opacity || "1");
      if (Number.isFinite(opacity) && opacity < 0.02) return false;

      return true;
  }

  function normalizeKeyName(name) {
      if (!name) return null;
      const trimmed = String(name).trim();
      if (trimmed === "Space" || trimmed === "Spacebar") return " ";
      return trimmed;
  }

  function resolveEventCtor(target, ctorName) {
      const targetWin =
          target?.ownerDocument?.defaultView ||
          target?.defaultView ||
          (target?.window === target ? target : window);

      return targetWin?.[ctorName] || window?.[ctorName] || globalThis?.[ctorName] || null;
  }

  function dispatchSafe(target, ctorName, type, options) {
      if (!target || typeof target.dispatchEvent !== "function") return false;

      const EventCtor = resolveEventCtor(target, ctorName);
      if (typeof EventCtor !== "function") return false;

      try {
          target.dispatchEvent(new EventCtor(type, options));
          return true;
      } catch {
          return false;
      }
  }

  function emitPointer(target, type, options) {
      dispatchSafe(target, "PointerEvent", type, options);
  }

  function emitMouse(target, type, options) {
      dispatchSafe(target, "MouseEvent", type, options);
  }

  function sendKey(target, keyName) {
      if (!target) return 0;

      const key = normalizeKeyName(keyName);
      if (!key) return 0;

      const definition = KEY_MAP[key];
      if (!definition) return 0;

      const options = {
          bubbles: true,
          cancelable: true,
          location: 0,
          repeat: false,
          altKey: false,
          ctrlKey: false,
          shiftKey: false,
          metaKey: false,
          isComposing: false,
          ...definition
      };

      let sent = 0;
      sent += dispatchSafe(target, "KeyboardEvent", "keydown", options) ? 1 : 0;
      sent += dispatchSafe(target, "KeyboardEvent", "keypress", options) ? 1 : 0;
      sent += dispatchSafe(target, "KeyboardEvent", "keyup", options) ? 1 : 0;
      return sent;
  }

  function hardClick(el) {
      if (!el) return false;

      try {
          if (typeof el.focus === "function") el.focus({ preventScroll: true });
      } catch {}

      const rect = el.getBoundingClientRect();
      if (rect.width < 2 || rect.height < 2) return false;

      const clientX = rect.left + rect.width / 2;
      const clientY = rect.top + rect.height / 2;

      const under = document.elementFromPoint(clientX, clientY);
      const targets = [el, under].filter(Boolean);

      const base = {
          bubbles: true,
          cancelable: true,
          composed: true,
          view: window,
          clientX,
          clientY
      };

      const mouseDown = { ...base, button: 0, buttons: 1 };
      const mouseUp = { ...base, button: 0, buttons: 0 };
      const pointerDown = { ...mouseDown, pointerId: 1, pointerType: "mouse", isPrimary: true };
      const pointerUp = { ...mouseUp, pointerId: 1, pointerType: "mouse", isPrimary: true };

      for (const target of targets) {
          emitPointer(target, "pointerdown", pointerDown);
          emitMouse(target, "mousedown", mouseDown);
      }

      for (const target of targets) {
          emitPointer(target, "pointerup", pointerUp);
          emitMouse(target, "mouseup", mouseUp);
          emitMouse(target, "click", mouseUp);
      }

      try {
          el.click();
      } catch {}

      dispatchSafe(el, "KeyboardEvent", "keydown", {
          key: "Enter",
          code: "Enter",
          keyCode: 13,
          which: 13,
          bubbles: true,
          cancelable: true
      });

      dispatchSafe(el, "KeyboardEvent", "keyup", {
          key: "Enter",
          code: "Enter",
          keyCode: 13,
          which: 13,
          bubbles: true,
          cancelable: true
      });

      return true;
  }

  function getBattleType() {
      if (getPaladinContext()) return "paladin";
      if (hasPriestBoard()) return "priest";
      if (queryAll(SELECTORS.target).some(isVisible)) return "ranger";

      // Ranger fallback by class asset before battle starts.
      for (const wrapper of queryAll(SELECTORS.activity)) {
          const img = wrapper.querySelector("img.activityButtonAsset__8af73, img.asset__65fca");
          if (img && img.src && img.src.includes(RANGER_HASH)) return "ranger";
      }

      return null;
  }

  function tryClickGoBackModal() {
      const now = Date.now();
      if (now - state.lastGoBackClickAt < CONFIG.goBackCooldownMs) return false;

      const warningNode = queryAll(SELECTORS.modalResourceText).find((el) => {
          if (!isVisible(el)) return false;
          const text = (el.textContent || "").trim();
          return OUT_OF_RESOURCES_RE.test(text);
      });

      if (!warningNode) return false;

      const modalRoot =
          warningNode.closest("[role='dialog']") ||
          warningNode.closest("[class*='modal']") ||
          warningNode.closest("[class*='layer']") ||
          warningNode.parentElement ||
          document;

      let button = queryOne(GO_BACK_BTN_SEL, modalRoot);
      if (button && !isVisible(button)) button = null;

      if (!button) {
          button = queryAll(GO_BACK_BTN_SEL).find((candidate) => isVisible(candidate)) || null;
      }

      if (!button) return false;

      state.lastGoBackClickAt = now;

      hardClick(button);
      setTimeout(() => hardClick(button), 60);

      console.log("%c[Modal] Go Back clicked", "color:#ffcc66;font-weight:bold");
      return true;
  }

  // ---------- Battle detection ----------
  function getPaladinContext() {
      let best = null;

      for (const root of queryAll(SELECTORS.palRoot)) {
          if (!isVisible(root)) continue;

          const shield = queryOne(SELECTORS.shield, root);
          if (!shield || !isVisible(shield)) continue;

          const rect = root.getBoundingClientRect();
          if (rect.width < 80 || rect.height < 80) continue;

          const projectiles = queryAll(SELECTORS.projectile, root).filter(isVisible);
          const score = projectiles.length * 1000000 + rect.width * rect.height;

          if (!best || score > best.score) {
              best = { root, shield, rect, projectiles, score };
          }
      }

      return best;
  }

  function hasPriestBoard() {
      const grid = queryOne(SELECTORS.priestGrid) || queryOne(SELECTORS.priestGame);
      if (!grid) return false;

      const items = queryAll(SELECTORS.priestItem).filter(isVisible);
      return items.length >= 3;
  }

  // ---------- Grass Toucher ----------
  function runDragonTick() {
      const el = queryOne(".dragonClickable__8e80e") || queryOne('img[alt="Grass Toucher"]');
      if (el) el.click();
  }

  // ---------- Activity ----------
  function runActivityTick() {
      for (const wrapper of queryAll(SELECTORS.activity)) {
          if (queryOne(SELECTORS.cooldown, wrapper)) continue;
          queryOne(SELECTORS.clickable, wrapper)?.click();
      }
  }

  // ---------- Continue ----------
  function tryContinue() {
      const btn = queryOne(SELECTORS.cont);
      if (!btn || state.clickedContinue.has(btn)) return;

      state.clickedContinue.add(btn);
      btn.click();

      console.log("%c[Continue] Clicked", "color:#aaffff;font-weight:bold");
  }

  // ---------- Ranger ----------
  const hitTargets = new WeakSet();

  function fireTarget(el) {
      if (!document.contains(el)) return;

      const btn = queryOne(SELECTORS.clickable, el) || el;
      try {
          btn.focus({ preventScroll: true });
      } catch {}

      btn.click();

      sendKey(btn, " ");
      sendKey(document.body, " ");
  }

  function tryTarget(el) {
      if (hitTargets.has(el)) return;

      try {
          if (!el.matches(SELECTORS.target)) return;
      } catch {
          return;
      }

      hitTargets.add(el);
      setTimeout(() => fireTarget(el), CONFIG.settleMs);
  }

  // ---------- Craft ----------
  function keysEqual(a, b) {
      if (!Array.isArray(a) || !Array.isArray(b)) return false;
      if (a.length !== b.length) return false;
      for (let i = 0; i < a.length; i++) {
          if (a[i] !== b[i]) return false;
      }
      return true;
  }

  function isCraftArrowSuccess(img) {
      const classes = Array.from(img?.classList || []);
      return classes.some((cls) => cls === "arrowSuccess__34527" || cls.startsWith("arrowSuccess__"));
  }

  function getLiveSequenceElement(seqHint) {
      if (seqHint instanceof Element && document.contains(seqHint)) {
          if (seqHint.matches?.(SELECTORS.seq)) return seqHint;

          const closest = seqHint.closest?.(SELECTORS.seq);
          if (closest && document.contains(closest)) return closest;
      }

      return queryOne(SELECTORS.seq);
  }

  function readPendingSequenceKeys(seqHint) {
      const seq = getLiveSequenceElement(seqHint);
      if (!seq) return [];

      return queryAll(SELECTORS.char, seq)
          .filter((img) => !isCraftArrowSuccess(img))
          .map((img) => normalizeKeyName(img.getAttribute("alt")))
          .filter((name) => KEY_MAP[name]);
  }

  async function waitForSequenceChange(beforeKeys, seqHint) {
      const startedAt = performance.now();

      while (performance.now() - startedAt <= CONFIG.craftStepAckTimeoutMs) {
          const nowKeys = readPendingSequenceKeys(seqHint);
          if (!keysEqual(nowKeys, beforeKeys)) {
              return { changed: true, keys: nowKeys };
          }
          await delay(CONFIG.craftStepPollMs);
      }

      return { changed: false, keys: readPendingSequenceKeys(seqHint) };
  }

  function sendCraftKeyEverywhere(key) {
      let targetsDelivered = 0;

      try {
          if (sendKey(document, key) > 0) targetsDelivered++;
      } catch (e) {
          console.warn("%c[Craft] Failed to send to document:", "color:#ffaa00", e.message);
      }

      try {
          if (sendKey(document.body, key) > 0) targetsDelivered++;
      } catch (e) {
          console.warn("%c[Craft] Failed to send to body:", "color:#ffaa00", e.message);
      }

      const active = document.activeElement;
      if (active && active !== document.body && document.contains(active)) {
          try {
              if (sendKey(active, key) > 0) targetsDelivered++;
          } catch (e) {
              console.warn("%c[Craft] Failed to send to activeElement:", "color:#ffaa00", e.message);
          }
      }

      return targetsDelivered;
  }

  async function runCraftAttempt(keys, attempt, seqHint) {
      console.log(
          `%c[Craft] Attempt ${attempt}/${CONFIG.craftRetryWholeMax}:`,
          "color:#66ffcc;font-weight:bold",
          keys.join(" -> ")
      );

      for (let i = 0; i < keys.length; i++) {
          const expectedKey = keys[i];
          const beforeKeys = readPendingSequenceKeys(seqHint);

          if (!beforeKeys.length) {
              return { ok: true, reason: "sequence already resolved" };
          }

          if (beforeKeys[0] !== expectedKey) {
              return {
                  ok: false,
                  reason: `desync before step ${i + 1}: expected ${expectedKey}, got ${beforeKeys[0] || "<none>"}`
              };
          }

          const delivered = sendCraftKeyEverywhere(expectedKey);
          if (delivered === 0) {
              return { ok: false, reason: `no target accepted key ${expectedKey}` };
          }

          await delay(CONFIG.craftPostKeySettleMs);

          const ack = await waitForSequenceChange(beforeKeys, seqHint);
          if (!ack.changed) {
              return {
                  ok: false,
                  reason: `no sequence change after key ${expectedKey}`
              };
          }

          await delay(CONFIG.keyDelayMs);
      }

      return { ok: true, reason: "attempt completed" };
  }

  async function doSequence(seqEl) {
      if (state.craftBusy) return;

      const initialKeys = readPendingSequenceKeys(seqEl);
      if (!initialKeys.length) return;

      const seqKey = initialKeys.join(",");
      const now = Date.now();
      const sameSeqCooldownLeft = CONFIG.craftRetrySameSeqMs - (now - state.lastSeqSentAt);

      if (seqKey === state.lastSeqKey && sameSeqCooldownLeft > 0) return;

      state.craftBusy = true;
      state.lastSeqKey = seqKey;
      state.lastSeqSentAt = now;

      try {
          let solved = false;

          for (let attempt = 1; attempt <= CONFIG.craftRetryWholeMax; attempt++) {
              const keys = readPendingSequenceKeys(seqEl);
              if (!keys.length) {
                  solved = true;
                  break;
              }

              const result = await runCraftAttempt(keys, attempt, seqEl);
              if (result.ok) {
                  solved = true;
                  break;
              }

              console.warn("%c[Craft] Attempt failed:", "color:#ffaa00;font-weight:bold", result.reason);

              if (attempt < CONFIG.craftRetryWholeMax) {
                  await delay(CONFIG.craftRetryBackoffMs);
              }
          }

          if (!solved) {
              console.warn("%c[Craft] Full sequence retries exhausted", "color:#ff6600;font-weight:bold");
              state.lastSeqKey = "";
              state.lastSeqSentAt = 0;
          }
      } finally {
          state.craftBusy = false;
      }
  }

  // ---------- Paladin ----------
  function resetPaladinProjectileCache() {
      state.projectileMeta = new WeakMap();
      state.liveProjectileSprites = new Set();
  }

  function isResolvedProjectile(el, topMetric, src, now) {
      let meta = state.projectileMeta.get(el);

      if (!meta) {
          meta = {
              top: topMetric,
              ts: now,
              stableFrames: 0,
              moved: false,
              src: src || ""
          };
          state.projectileMeta.set(el, meta);
          return false;
      }

      const dy = Math.abs(topMetric - meta.top);
      const dt = now - meta.ts;

      if (dy > 0.7) {
          meta.moved = true;
          meta.stableFrames = 0;

          // Learn "alive" sprite while projectile is moving.
          if (src) state.liveProjectileSprites.add(src);
      } else if (dt >= 20) {
          meta.stableFrames += 1;
      }

      const looksLikeImpactSprite =
          meta.moved && !!src && state.liveProjectileSprites.size > 0 && !state.liveProjectileSprites.has(src);
      const frozenAfterMove = meta.moved && meta.stableFrames >= 2;

      // Do not resolve on generic src changes: many flight sprites are animated and swap frames.
      const resolved = looksLikeImpactSprite || frozenAfterMove;

      meta.top = topMetric;
      meta.ts = now;
      meta.src = src || meta.src;

      return resolved;
  }

  function getShieldWidthLogical(shield) {
      return Math.max(
          CONFIG.palMinShieldW,
          toNum(shield.style.width) ?? toNum(getComputedStyle(shield).width) ?? CONFIG.palDefaultShieldW
      );
  }

  function getProjectileThreats(root) {
      const list = [];
      const now = performance.now();

      for (const el of queryAll(SELECTORS.projectile, root)) {
          if (!isVisible(el)) continue;

          const rect = el.getBoundingClientRect();
          const topMetric = toNum(el.style.top) ?? rect.bottom;
          const src = el.getAttribute("src") || "";

          // Skip projectiles already in impact animation.
          if (isResolvedProjectile(el, topMetric, src, now)) continue;

          const leftLogical = toNum(el.style.left);
          const widthLogical = toNum(el.style.width) ?? CONFIG.palDefaultProjW;
          const logicalCenter = leftLogical === null ? null : leftLogical + widthLogical / 2;
          const clientCenter = rect.left + rect.width / 2;

          list.push({
              el,
              topMetric,
              logicalCenter,
              clientCenter
          });
      }

      list.sort((a, b) => b.topMetric - a.topMetric);
      return list;
  }

  function choosePaladinTarget(threats, shieldW) {
      if (!threats.length) return null;

      const first = threats[0];
      const second = threats[1];

      if (!second) {
          return {
              logicalCenter: first.logicalCenter,
              clientCenter: first.clientCenter
          };
      }

      const closeInY = first.topMetric - second.topMetric <= CONFIG.palTopDelta;

      if (
          closeInY &&
          first.logicalCenter !== null &&
          second.logicalCenter !== null &&
          Math.abs(first.logicalCenter - second.logicalCenter) <= shieldW * CONFIG.palDualCoverRatio
      ) {
          // Dual-threat mode: check if 3+ close projectiles should be included
          let avgLogical = first.logicalCenter + second.logicalCenter;
          let avgClient = first.clientCenter + second.clientCenter;
          let count = 2;

          // Include nearby threats beyond the second one
          for (let i = 2; i < threats.length; i++) {
              const threat = threats[i];
              if (first.topMetric - threat.topMetric > CONFIG.palTopDelta) break; // Too far down
              if (threat.logicalCenter === null) continue;
              if (Math.abs(first.logicalCenter - threat.logicalCenter) > shieldW * CONFIG.palDualCoverRatio) continue;
              
              avgLogical += threat.logicalCenter;
              avgClient += threat.clientCenter;
              count++;
          }

          return {
              logicalCenter: avgLogical / count,
              clientCenter: avgClient / count
          };
      }

      return {
          logicalCenter: first.logicalCenter,
          clientCenter: first.clientCenter
      };
  }

  function getPaladinInputTargets(ctx) {
      return [ctx.shield, ctx.root, document, document.body, window];
  }

  function paladinPointerDown(ctx, x, y) {
      const base = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y };
      const mouse = { ...base, button: 0, buttons: 1 };
      const pointer = { ...mouse, pointerId: 1, pointerType: "mouse", isPrimary: true };

      for (const target of getPaladinInputTargets(ctx)) {
          emitPointer(target, "pointerdown", pointer);
          emitMouse(target, "mousedown", mouse);
      }

      state.palDrag = true;
  }

  function paladinPointerMove(ctx, x, y) {
      const base = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y, buttons: 1 };
      const pointer = { ...base, pointerId: 1, pointerType: "mouse", isPrimary: true };

      for (const target of getPaladinInputTargets(ctx)) {
          emitPointer(target, "pointermove", pointer);
          emitMouse(target, "mousemove", base);
      }
  }

  function paladinPointerUp() {
      if (!state.palDrag) return;

      const root = state.palRoot;
      const rect = root?.getBoundingClientRect();

      const x = rect ? rect.left + rect.width / 2 : 0;
      const y = rect ? rect.top + rect.height * CONFIG.palAimY : 0;

      const base = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y };
      const mouse = { ...base, button: 0, buttons: 0 };
      const pointer = { ...mouse, pointerId: 1, pointerType: "mouse", isPrimary: true };

      for (const target of [root, document, document.body, window]) {
          emitPointer(target, "pointerup", pointer);
          emitMouse(target, "mouseup", mouse);
      }

      state.palDrag = false;
  }

  function mouseEventInsidePaladin(e) {
      const root = state.palRoot;
      if (!root) return true;
      if (!("clientX" in e) || !("clientY" in e)) return true;

      const rect = root.getBoundingClientRect();
      return (
          e.clientX >= rect.left &&
          e.clientX <= rect.right &&
          e.clientY >= rect.top &&
          e.clientY <= rect.bottom
      );
  }

  function enableMouseLock() {
      if (!CONFIG.blockRealMouse || state.mouseLockHandler) return;

      state.mouseLockHandler = (e) => {
          if (state.mode !== "paladin") return;
          if (!e.isTrusted) return;
          if (!mouseEventInsidePaladin(e)) return;

          e.preventDefault();
          e.stopPropagation();
          e.stopImmediatePropagation();
      };

      for (const ev of MOUSE_LOCK_EVENTS) {
          window.addEventListener(ev, state.mouseLockHandler, true);
          document.addEventListener(ev, state.mouseLockHandler, true);
      }
  }

  function disableMouseLock() {
      if (!state.mouseLockHandler) return;

      for (const ev of MOUSE_LOCK_EVENTS) {
          window.removeEventListener(ev, state.mouseLockHandler, true);
          document.removeEventListener(ev, state.mouseLockHandler, true);
      }

      state.mouseLockHandler = null;
  }

  function paladinTick() {
      const ctx = getPaladinContext();
      if (!ctx) return;

      state.palRoot = ctx.root;

      const threats = getProjectileThreats(ctx.root);
      if (!threats.length) return;

      const shieldW = getShieldWidthLogical(ctx.shield);
      const target = choosePaladinTarget(threats, shieldW);
      if (!target) return;

      if (target.logicalCenter !== null) {
          const currentLeft = toNum(ctx.shield.style.left);
          const desiredLeft = target.logicalCenter - shieldW / 2;
          const nextLeft =
              currentLeft === null ? desiredLeft : currentLeft + (desiredLeft - currentLeft) * CONFIG.palSmooth;

          ctx.shield.style.setProperty("left", `${nextLeft}px`, "important");
          ctx.shield.style.setProperty("transform", "none", "important");
      }

      // Guard against null clientCenter
      if (target.clientCenter === null || target.clientCenter === undefined) {
          return;
      }

      const x = target.clientCenter;
      const y = ctx.rect.top + ctx.rect.height * CONFIG.palAimY;

      if (!state.palDrag) paladinPointerDown(ctx, x, y);
      paladinPointerMove(ctx, x, y);
  }

  function paladinLoop() {
      paladinTick();
      state.palRaf = requestAnimationFrame(paladinLoop);
  }

  function startPaladinBot() {
      if (state.palRaf) return;

      resetPaladinProjectileCache();
      enableMouseLock();

      state.palRaf = requestAnimationFrame(paladinLoop);
      console.log("%c[Paladin] Bot started", "color:#88aaff;font-weight:bold");
  }

  function stopPaladinBot() {
      if (state.palRaf) {
          cancelAnimationFrame(state.palRaf);
          state.palRaf = 0;
      }

      paladinPointerUp();
      disableMouseLock();
      resetPaladinProjectileCache();

      state.palRoot = null;
  }

  // ---------- Priest ----------
  function getPriestGlyphSignature(tile) {
      const glyphRoot =
          queryOne(SELECTORS.priestGlyph, tile) || queryOne(".gridAssetFront__0dcd3 svg", tile) || queryOne("svg", tile);
      if (!glyphRoot) return null;

      const paths = queryAll("path", glyphRoot)
          .map((p) => p.getAttribute("d") || "")
          .join("|");
      if (paths && paths.length > 12) return paths;

      const html = (glyphRoot.innerHTML || "").replace(/\s+/g, "");
      return html || null;
  }

  function buildPriestGroups() {
      const items = queryAll(SELECTORS.priestItem).filter(
          (el) => isVisible(el) && !el.classList.contains(PRIEST_MATCHED_CLASS)
      );

      const groupsBySignature = new Map();

      for (const item of items) {
          const signature = getPriestGlyphSignature(item);
          if (!signature) continue;

          if (!groupsBySignature.has(signature)) groupsBySignature.set(signature, []);
          groupsBySignature.get(signature).push(item);
      }

      return Array.from(groupsBySignature.values())
          .filter((group) => group.length >= 3)
          .map((group) => group.slice(0, 3));
  }

  async function solvePriestBoardOnce() {
      if (state.priestBusy || state.mode !== "priest") return;
      state.priestBusy = true;

      try {
          const groups = buildPriestGroups();
          if (!groups.length) return;

          for (const group of groups) {
              if (state.mode !== "priest") break;

              const live = group.filter(
                  (el) => document.contains(el) && isVisible(el) && !el.classList.contains(PRIEST_MATCHED_CLASS)
              );
              if (live.length < 3) continue;

              for (const tile of live) {
                  if (state.mode !== "priest") break;
                  hardClick(tile);
                  await delay(CONFIG.priestClickDelayMs);
              }

              await delay(CONFIG.priestTripletDelayMs);
          }
      } finally {
          state.priestBusy = false;
      }
  }

  function priestTick() {
      if (state.mode !== "priest") return;
      if (!hasPriestBoard()) return;

      solvePriestBoardOnce();
  }

  function priestLoop() {
      priestTick();
      state.priestRaf = requestAnimationFrame(priestLoop);
  }

  function startPriestBot() {
      if (state.priestRaf) return;

      state.priestBusy = false;
      state.priestRaf = requestAnimationFrame(priestLoop);

      console.log("%c[Priest] Bot started", "color:#a3ffcc;font-weight:bold");
  }

  function stopPriestBot() {
      if (state.priestRaf) {
          cancelAnimationFrame(state.priestRaf);
          state.priestRaf = 0;
      }

      state.priestBusy = false;
  }

  // ---------- Mode switch ----------
  function checkBattleMode() {
      const mode = getBattleType();
      if (mode === state.mode) return;

      state.mode = mode;

      if (mode === "paladin") {
          stopPriestBot();
          startPaladinBot();
          console.log("%c[Battle] PALADIN mode", "color:#88aaff;font-weight:bold");
          return;
      }

      if (mode === "priest") {
          stopPaladinBot();
          startPriestBot();
          console.log("%c[Battle] PRIEST mode", "color:#a3ffcc;font-weight:bold");
          return;
      }

      stopPaladinBot();
      stopPriestBot();

      if (mode === "ranger") {
          console.log("%c[Battle] RANGER mode", "color:#00ff88;font-weight:bold");
      }
  }

  // ---------- Observer ----------
  function handleAddedNode(node) {
      if (!(node instanceof Element)) return;

      if (node.matches?.(SELECTORS.target)) tryTarget(node);
      node.querySelectorAll?.(SELECTORS.target).forEach(tryTarget);

      const seq = node.matches?.(SELECTORS.seq) ? node : node.querySelector?.(SELECTORS.seq);
      if (seq) doSequence(seq);

      if (node.matches?.(SELECTORS.cont) || node.querySelector?.(SELECTORS.cont)) tryContinue();

      // Fast reaction Paladin.
      if (state.mode === "paladin") {
          if (node.matches?.(SELECTORS.projectile) || node.querySelector?.(SELECTORS.projectile)) {
              paladinTick();
          }
      }

      // Fast reaction Priest.
      if (state.mode === "priest") {
          if (
              node.matches?.(SELECTORS.priestItem) ||
              node.matches?.(SELECTORS.priestGrid) ||
              node.querySelector?.(SELECTORS.priestItem)
          ) {
              priestTick();
          }
      }
  }

  function handleMutation(mutation) {
      for (const node of mutation.addedNodes) {
          handleAddedNode(node);
      }

      if (mutation.type === "childList" && mutation.target instanceof Element) {
          if (mutation.target.closest?.(SELECTORS.seq) || mutation.target.matches?.(SELECTORS.seq)) {
              doSequence(mutation.target.closest(SELECTORS.seq) || mutation.target);
          }
      }
  }

  state.observer = new MutationObserver((mutations) => {
      tryClickGoBackModal();
      checkBattleMode();

      for (const mutation of mutations) {
          handleMutation(mutation);
      }
  });

  // ---------- Poll ----------
  function runPollTick() {
      tryClickGoBackModal();
      checkBattleMode();

      queryAll(SELECTORS.target).forEach(tryTarget);

      const seq = queryOne(SELECTORS.seq);
      if (seq) doSequence(seq);
      else {
          state.lastSeqKey = "";
          state.lastSeqSentAt = 0;
      }

      if (state.mode === "priest") priestTick();

      tryContinue();
  }

  // ---------- Stop ----------
  window.stopBot = () => {
      clearInterval(state.dragonTimer);
      clearInterval(state.activityTimer);
      clearInterval(state.goBackTimer);
      clearInterval(state.pollTimer);

      stopPaladinBot();
      stopPriestBot();

      if (state.observer) {
          state.observer.disconnect();
          state.observer = null;
      }

      console.log("%c[BOT] Stopped", "color:red;font-weight:bold");
  };

  // ---------- Init ----------
  state.dragonTimer = setInterval(runDragonTick, CONFIG.dragonMs);
  state.activityTimer = setInterval(runActivityTick, CONFIG.activityMs);
  state.goBackTimer = setInterval(tryClickGoBackModal, CONFIG.goBackScanMs);
  state.pollTimer = setInterval(runPollTick, CONFIG.pollMs);

  state.observer.observe(document.body, { childList: true, subtree: true });

  checkBattleMode();

  const initSeq = queryOne(SELECTORS.seq);
  if (initSeq) doSequence(initSeq);

  tryClickGoBackModal();
  tryContinue();

  console.log("%c[The Last Meadow Auto Script] v2.0 (final)", "color:#00ff00;font-weight:bold;font-size:14px");
  console.log("%cStop command: stopBot()", "color:#ff9900");
})();

Stopping the Script

Tampermonkey Method

  • Run in browser console: stopBot()
  • Or disable/delete script in Tampermonkey dashboard

Console Paste Method

  • Run in browser console: stopBot()

Notes

  • The script relies on current Discord CSS class names.
  • If Discord updates class names, selectors may need to be updated.
  • Re-run the script after a page refresh or Discord update.

Known issues

  • None

Disclaimer

Use at your own risk. This project is for educational and personal automation purposes.

About

Automation script for The Last Meadow mini-games inside Discord.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors