POE Assistant

Overlay POE — Lua Examples

ready-to-use scripts — copy, paste, tweak the numbers at the top

← back to POE Assistant full POEMEMORY API reference →
1 SCRIPTING tab → + new 2 paste & save 3 toggle the script on 4 tweak the values at the top

Scripts marked no setup just observe and alert. Scripts marked needs input send keys/clicks — first turn on input enabled in the tab and bind a panic key (HOTKEYS tab).

Alerts & marks

no setup — safe to run anywhere; they never touch your character

Swarm alertno setup

Toast when a lot of monsters are close — packs, breach, delirium, ritual.

Tune: RANGE (grid units), COUNT (how many triggers it).

local RANGE = 35
local COUNT = 10

function tick()
  local n = POEMEMORY.World.Monsters:CountWithin(RANGE)
  if n >= COUNT then
    notify("swarm: " .. n, "ff5555")
  end
end
Rare / unique nearbyno setup

Toast + chime + box when a rare or unique comes into range. Throttled so it won't spam.

Tune: RANGE. Colors are hex strings.

local RANGE = 60
local last = 0

function tick()
  if now() - last < 1500 then return end
  local m = POEMEMORY.World.Monsters:Nearest()
  if m and (m.Rarity == "Rare" or m.Rarity == "Unique") and m.Distance <= RANGE then
    last = now()
    local col = "ffcc00"
    if m.Rarity == "Unique" then col = "ff8000" end
    notify(m.Rarity .. " nearby", col)
    sound(1)
    m:Mark(col)
  end
end
Box every rare & uniqueno setup

Draw a hollow box on each rare (yellow) and unique (orange) around you — pure visibility.

Tune: RANGE, the two colors.

local RANGE = 70

function tick()
  for _, m in ipairs(POEMEMORY.World.Monsters:Within(RANGE)) do
    if m.Rarity == "Unique" then
      m:Mark("ff8000")
    elseif m.Rarity == "Rare" then
      m:Mark("ffd000")
    end
  end
end
Highlight a dangerous modno setup

Box any monster carrying a specific mod (a damaging aura, on-death effect, etc.). Great for "don't stand in that."

Tune: put a fragment of the mod id in MOD — find ids via the F11 dump or the MONSTER WATCHER catalog.

local MOD = "Volatile"     -- substring, case-insensitive
local RANGE = 80

function tick()
  for _, m in ipairs(POEMEMORY.World.Monsters:Within(RANGE)) do
    if m:HasMod(MOD) then
      m:Mark("ff3030")
    end
  end
end
Low-life alarmno setup

A loud warning when life gets dangerous. This only alerts — it does not touch your character.

Tune: THRESH (percent).

local THRESH = 35
local last = 0

function tick()
  if POEMEMORY.Player.Life.Pct <= THRESH and now() - last >= 800 then
    last = now()
    notify("LOW LIFE", "ff0000")
    sound(3)
  end
end
Idle detectorno setup

Nudge you if you haven't moved in a while — handy if you tab out mid-map. Shows off Player.X/Y + timing.

Tune: the 15000 ms idle window.

local lastX, lastY, stillSince = 0, 0, 0

function tick()
  local p = POEMEMORY.Player
  if math.abs(p.X - lastX) > 2 or math.abs(p.Y - lastY) > 2 then
    lastX, lastY, stillSince = p.X, p.Y, now()   -- moved: reset
  elseif now() - stillSince > 15000 then
    notify("idle 15s", "aa88ff")
    stillSince = now()
  end
end
Zone-change announcerno setup

Toast the zone whenever you enter a new area — fires once per transition via Area.ChangeCount.

Tune: nothing needed.

local seen = -1

function tick()
  local c = POEMEMORY.Area.ChangeCount
  if c ~= seen then
    seen = c
    local name = POEMEMORY.Area.Name
    if name == "" then name = POEMEMORY.Area.Code end
    notify("zone: " .. name)
  end
end
Buffs & charges

no setup — read what's on you: auras, flasks, charge stacks

What am I actually buffed with?no setup

Start here. Prints every buff on you, with its stack count and time left, once a second. Run it, make the thing happen in game, read the log, then copy the id into any script below.

Tune: nothing. Turn it off once you have the id you wanted.

local last = 0

function tick()
  if now() - last < 1000 then return end
  last = now()
  for _, b in ipairs(POEMEMORY.Player:Buffs()) do
    log(b.Id .. "  x" .. b.Charges .. "  " ..
        (b.IsPermanent and "permanent" or string.format("%.1fs", b.TimeLeft)))
  end
  log("----")
end

Ids look like: power_charge, frenzy_charge, endurance_charge, ghost_dance_stacks, mana_leech. Matching ignores capitals.

Charge counterno setup

Tells you when you hit full charges, and when you drop to none. Only speaks when the number actually changes, so it won't spam.

Tune: CHARGE (which one), FULL (your maximum).

local CHARGE = "power_charge"
local FULL   = 3
local seen = -1

function tick()
  local n = POEMEMORY.Player:Charges(CHARGE)
  if n == seen then return end        -- nothing changed
  seen = n
  if n >= FULL then
    notify(CHARGE .. " FULL (" .. n .. ")", "c792ea")
  elseif n == 0 then
    notify(CHARGE .. " empty", "888888")
  end
end
Buff about to run outno setup

Warns you a couple of seconds before a buff expires, so you can refresh it. Skips permanent buffs, which never expire.

Tune: BUFF (the id), WARN (seconds of notice).

local BUFF = "ghost_dance_stacks"
local WARN = 2.0
local warned = false

function tick()
  local b = POEMEMORY.Player:Buff(BUFF)
  if not b or b.IsPermanent then warned = false; return end
  if b.TimeLeft <= WARN then
    if not warned then
      warned = true
      notify(BUFF .. " " .. string.format("%.1fs", b.TimeLeft), "ffb26b")
    end
  else
    warned = false                    -- it got refreshed; arm again
  end
end
Lost my chargesno setup

Shouts when charges drop sharply — you spent them, or you stopped generating. Good for builds that care about staying at maximum.

Tune: CHARGE, DROP (how big a fall is worth a shout).

local CHARGE = "frenzy_charge"
local DROP   = 2
local prev = 0

function tick()
  local n = POEMEMORY.Player:Charges(CHARGE)
  if prev - n >= DROP then
    notify("lost " .. (prev - n) .. " " .. CHARGE, "ff5e6c")
  end
  prev = n
end
Rage watcher (works on both games)no setup

Rage is kept in two completely different places: on PoE2 it is a pool like mana, on PoE1 it is a buff with a stack count. You read it the same way either time. Note the script uses .Current, not .Pct — PoE1 exposes no maximum, so percent is 0 there.

Tune: LOW (shout below this), GOOD (shout once you reach this).

local LOW  = 5
local GOOD = 20
local said = ""

function tick()
  local r = POEMEMORY.Player.Rage
  if not r.Present then return end          -- this build has no rage at all

  local n = r.Current
  local state = (n <= LOW and "low") or (n >= GOOD and "good") or "mid"
  if state == said then return end          -- only speak when it changes
  said = state

  if state == "low"  then notify("rage low ("  .. n .. ")", "ff8a5c") end
  if state == "good" then notify("rage ready (" .. n .. ")", "6bd98b") end
end

On PoE2 you can also use r.Max and r.Pct — a real pool, so both work. On PoE1 they read 0 by design, because the game has no rage maximum to report.

Stacks vs entries — reading the right numberno setup

Buffs stack in two different ways, and picking the wrong reader gives you a quietly wrong number. A charge is one entry counting up, so ask :Charges. Leech, recoup and reservation effects instead repeat — one entry per application, each counting 1 — so ask :BuffCount. This prints both side by side so you can see which kind a buff is.

Tune: the WATCH list — put your own ids in it.

local WATCH = { "power_charge", "frenzy_charge", "endurance_charge", "life_leech" }
local last = 0

function tick()
  if now() - last < 1000 then return end
  last = now()

  for _, id in ipairs(WATCH) do
    local entries = POEMEMORY.Player:BuffCount(id)
    if entries > 0 then
      log(id .. "  entries=" .. entries .. "  charges=" .. POEMEMORY.Player:Charges(id))
    end
  end
  log("----")
end

What you should see: a charge reads entries=1 with a rising charges number. life_leech does the opposite — many entries, each charges=1. On a real PoE1 character it showed up as 15 entries at once.

Flasks & charms

no setup — read your belt slot by slot. Slot 1 is the leftmost cell, the key you press.

On PoE2 the belt also holds charms PoE2 keeps flasks and charms in the same belt, so slot 3 may well be a charm. Check .Name when it matters. PoE1 has no charms — there it is flasks only.
What is in my belt?no setup

Print every belt slot with its name and charges. Run it once, read the log, then write your real script against the slot numbers you saw.

Faster way: the SCRIPTING tab's show character current values button lists the same thing in a popup, no script needed.

local last = 0

function tick()
  if now() - last < 1000 then return end
  last = now()
  for _, f in ipairs(POEMEMORY.Player:Flasks()) do
    log("[" .. f.Slot .. "] " .. f.Name .. "  " .. f.Charges .. "/" .. f.BaseMax .. "  (" .. f.BaseId .. ")")
  end
  log("----")
end

What you should see: one line per filled slot, in order. The number after the slash is the base type's maximum — your own charges can sit above it if your flask has "+X to maximum charges".

Flask running lowno setup

Warn when a slot drops under a charge count you choose. Useful for the flask you rely on most.

Tune: SLOT (1 = leftmost), LOW (charges), the 3000 ms throttle.

local SLOT = 1
local LOW  = 12
local last = 0

function tick()
  local c = POEMEMORY.Player:FlaskCharges(SLOT)
  if c >= LOW then return end
  if now() - last < 3000 then return end
  last = now()
  notify("flask " .. SLOT .. " low: " .. c, "ff5555")
end

Why a raw number and not a percent: .Pct is measured against the base maximum, which your mods change. A charge count you read off your own flask is exact.

Find a flask by nameno setup

Don't hard-code a slot. Search the belt for the flask you mean, so the script still works after you rearrange it.

Tune: the word inside find, and LOW.

local WANT = "Quicksilver"
local LOW  = 10
local last = 0

function tick()
  if now() - last < 3000 then return end
  for _, f in ipairs(POEMEMORY.Player:Flasks()) do
    if f.Name:find(WANT) and f.Charges < LOW then
      last = now()
      notify(f.Name .. " low (slot " .. f.Slot .. ")", "ffcc00")
      return
    end
  end
end

Playing in another language? Match on f.BaseId instead — that string never changes with your game language.

Automation

these send input — read the box, then flip input enabled on

⚠ Before running any of these Turn on input enabled in the SCRIPTING tab and bind a panic key in the HOTKEYS tab (your instant kill switch, works even alt-tabbed). Input only fires while the game is focused and is rate-limited to a handful of actions per second.
Auto life-flaskneeds input

Press your life-flask key when life drops below a threshold. It also checks whether the flask is already working, so it doesn't burn a second charge on top of the first.

Tune: FLASK (your slot key), SLOT (the same slot as a number), THRESH (percent), BUFF (see below), the 1200 ms floor.

local FLASK  = "1"
local SLOT   = 1      -- the same flask, as a belt slot number (1 = leftmost)
local THRESH = 65
local COST   = 10     -- charges one sip costs on YOUR flask; 0 to skip this check
local BUFF   = ""     -- your life-flask buff id; leave "" to skip this check
local last = 0

function tick()
  if POEMEMORY.Player.Life.Pct > THRESH then return end

  -- no charges? pressing does nothing but hide the problem
  if COST > 0 and POEMEMORY.Player:FlaskCharges(SLOT) < COST then return end

  -- already drinking? don't waste a second charge
  if BUFF ~= "" and POEMEMORY.Player:HasBuff(BUFF) then return end

  if now() - last < 1200 then return end
  last = now()
  Input.Press(FLASK)
end

Why the BUFF line: a timer alone is a guess. It re-presses whether or not the flask is actually up, which wastes charges when it fires early and leaves you dry when it fires late. Checking the buff asks the game instead of guessing. Find your id with "What am I actually buffed with?" above; leave BUFF empty and the script behaves exactly as it did before.

Why the COST line: an empty flask presses just as happily as a full one, and you only notice when you die. Reading the real charge count stops that. Set COST to what one sip costs on your flask — watch the number drop with the belt-printing script above, since flask mods change the cost.

Auto mana-flaskneeds input

Same idea for mana — fires when mana runs low so you don't stop attacking, and skips the press while the flask is still running.

Tune: FLASK, THRESH, BUFF.

local FLASK  = "2"
local SLOT   = 2
local THRESH = 25
local COST   = 10                    -- charges one sip costs on YOUR flask; 0 to skip
local BUFF   = "flask_effect_mana"   -- verify yours with the buff-list script
local last = 0

function tick()
  if POEMEMORY.Player.Mana.Pct > THRESH then return end
  if COST > 0 and POEMEMORY.Player:FlaskCharges(SLOT) < COST then return end
  if BUFF ~= "" and POEMEMORY.Player:HasBuff(BUFF) then return end
  if now() - last < 1500 then return end
  last = now()
  Input.Press(FLASK)
end

Check the id first. flask_effect_mana is what a mana flask reported on our test character — yours can differ by flask type and by game. Run the buff-list script, drink the flask, and read the log.

Quicksilver rotation — PoE1needs input

The classic PoE1 setup: several Quicksilver flasks in the belt, taking turns. Each press goes to a different one, so they refill while the others carry you — and a flask is only pressed when it actually has the charges for a sip.

Tune: WANT (the flask name to rotate), KEYS (your key per belt slot), COST (charges one sip costs on your flask), BUFF, GAP.

local WANT = "Quicksilver"                -- matched against the flask name
local KEYS = { "1", "2", "3", "4", "5" }  -- your key for belt slot 1..5
local COST = 9                            -- charges one sip costs on YOUR flask
local BUFF = "flask_utility_sprint"       -- the quicksilver buff; "" to skip this check
local GAP  = 400                          -- ms floor between presses

local last = 0
local lastSlot = 0        -- the slot we pressed last, so the next press moves on

function tick()
  if POEMEMORY.Area.InTown then return end
  if now() - last < GAP then return end

  -- already running? then nothing needs pressing
  if BUFF ~= "" and POEMEMORY.Player:HasBuff(BUFF) then return end

  -- walk the belt in slot order and find the next usable one after lastSlot
  local pick, first = nil, nil
  for _, f in ipairs(POEMEMORY.Player:Flasks()) do
    if f.Name:find(WANT) and f.Charges >= COST then
      if first == nil then first = f.Slot end
      if f.Slot > lastSlot then
        pick = f.Slot
        break
      end
    end
  end

  if pick == nil then pick = first end   -- past the last one: wrap to the first
  if pick == nil then return end         -- none of them has enough charges

  lastSlot = pick
  last = now()
  Input.Press(KEYS[pick])
end

Why it remembers a slot and not a list position: the list of usable flasks changes between ticks as they drain and refill. Remembering "the second usable one" would jump around; remembering the slot number is stable, so the rotation keeps its order no matter how many are ready.

Set COST from your own flask. Run the belt-printing script, sip once, and watch how far the number drops. Flask mods change the cost, so a number you measured beats any number a script can guess.

Not sure of the buff id? Open the SCRIPTING tab, press show character current values while the flask is running, and copy the id from the popup. Leave BUFF empty and the script just rotates on the timer instead.

Keep a utility flask up — PoE1needs input

For a Basalt, Granite, Jade or any other defensive flask you want running all the time. It presses only when the buff has dropped and the flask has charges — never on a timer, and never into an empty flask.

Tune: SLOT + KEY (same flask), BUFF, COST.

local SLOT = 3
local KEY  = "3"
local BUFF = "flask_utility_stone"   -- basalt; verify yours in the popup
local COST = 10
local last = 0

function tick()
  if POEMEMORY.Area.InTown then return end
  if POEMEMORY.Player:HasBuff(BUFF) then return end            -- still up
  if POEMEMORY.Player:FlaskCharges(SLOT) < COST then return end -- nothing to spend
  if now() - last < 600 then return end
  last = now()
  Input.Press(KEY)
end

The two guards do different jobs. The buff check stops you re-pressing a flask that is already working. The charge check stops you pressing one that has nothing left — which otherwise fails silently, since a dead keypress looks exactly like a working one.

Melee auto-attack nearestneeds input

Right-click the closest on-screen monster within melee range, once per second. The cursor jumps to the target to aim.

Tune: MELEE (grid range — measure it, see the note below), the 1000 ms rate, and the button ("right").

local MELEE = 20
local last = 0

function tick()
  if now() - last < 1000 then return end
  last = now()
  local m = POEMEMORY.World.Monsters:Nearest()
  if m and m.OnScreen and m.Distance <= MELEE then
    Mouse.ClickAt(m.ScreenX, m.ScreenY, "right")
  end
end

Find your MELEE number: temporarily add notify("d "..math.floor(m.Distance)) and read it while standing at swing range.