ready-to-use scripts — copy, paste, tweak the numbers at the top
← back to POE Assistant full POEMEMORY API reference →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).
no setup — safe to run anywhere; they never touch your character
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
endToast + 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
endDraw 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
endBox 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
endA 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
endNudge 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
endToast 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
endno setup — read what's on you: auras, flasks, charge stacks
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("----")
endIds look like: power_charge, frenzy_charge, endurance_charge, ghost_dance_stacks, mana_leech. Matching ignores capitals.
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
endWarns 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
endShouts 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
endRage 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
endOn 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.
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("----")
endWhat 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.
no setup — read your belt slot by slot. Slot 1 is the leftmost cell, the key you press.
.Name when it matters. PoE1 has no charms — there it is flasks only.
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("----")
endWhat 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".
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")
endWhy 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.
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
endPlaying in another language? Match on f.BaseId instead — that string never changes with your game language.
these send input — read the box, then flip input enabled on
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.
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.
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])
endWhy 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.
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.
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
endFind your MELEE number: temporarily add notify("d "..math.floor(m.Distance)) and read it while standing at swing range.