Local Player
On this page
The Player object represents the local player character. Provides access to stats, buffs, position, and other player data.
Call player:is_valid() before using any other methods. Player objects can become invalid at any time.
Functions
Validation
player:is_valid() -> boolean
Available on GMS and CW
Validates the object exists in the game world. Always call this first.
local player = core.object_manager.get_local_player()
if not player or not player:is_valid() then
return -- Exit early
endBasic Information
player:get_id() -> number
Available on GMS and CW
Returns the character ID.
player:get_account_id() -> number
Available on GMS and CW
Returns the account ID (the same for every character on the account).
player:get_name() -> string
Available on GMS and CW
Returns the character name.
player:get_level() -> number
Available on GMS and CW
Returns the character level.
player:get_exp() -> number
Available on GMS and CW
Returns the experience points earned within the current level.
player:get_exp_percent() -> number
Available on GMS and CW
Returns progress through the current level as a percentage (0–100), derived from get_exp() and the current level. Returns 0 at max level.
player:get_stats() -> table | nil
Available on GMS and CW
Returns a snapshot of the primary stats, or nil if the character data is not loaded yet.
| Field | Description |
|---|---|
str | Total STR |
dex | Total DEX |
int | Total INT |
luk | Total LUK |
ap | Unspent ability points |
Totals are what the stat window shows — base plus gear and buffs.
player:get_detailed_stats() -> table | nil
Available on GMS and CW
Returns the detailed stats shown in the Character Info window, or nil when the character is not in a map.
| Field | Description |
|---|---|
damage | Damage % |
boss_damage | Boss Damage % |
final_damage | Final Damage % |
ignore_defense | Ignore Defense % |
critical_rate | Critical Rate % that applies: capped at 100, even when the window shows more |
critical_damage | Critical Damage % |
star_force | Star Force |
arcane_power | Arcane Power |
sacred_power | Sacred Power |
exp_bonus | Additional EXP Obtained % |
Values include active buffs, and percentages can have decimals. exp_bonus does not include the reduction from daily remote play restrictions. Each call recalculates every stat, so keep the result instead of calling it every tick.
local stats = player:get_detailed_stats()
if stats and stats.critical_rate < 100 then
core.log("Critical Rate is " .. (100 - stats.critical_rate) .. "% short")
endplayer:is_alive() -> boolean | nil
Available on GMS and CW
Returns false while dead (HP 0, tombstone up), or nil if character data is unavailable.
player:get_pets() -> table<pet>
Available on GMS and CW
Returns the summoned pets. Each entry has repleteness and is_active.
repleteness is the fullness meter (0–100). It decays over time and the pet despawns at 0, so a long-running script should watch it and feed.
for i, pet in ipairs(player:get_pets()) do
if pet.repleteness < 30 then
core.log("pet " .. i .. " is hungry")
end
endplayer:get_summons() -> table<summon>
Available on GMS and CW
Returns your active summons. Each entry is a snapshot, so call it again for fresh values.
| Field | Description |
|---|---|
id | Unique ID of this summon on the map |
skill_id | Skill that created it |
skill_level | Level of that skill |
position | {x, y} where the summon currently is |
time_remaining | Milliseconds until the summon's time runs out. Absent for a summon without a timer, and once its time is up |
local KISHIN = 42111003
local function find_summon(skill_id)
for _, summon in ipairs(player:get_summons()) do
if summon.skill_id == skill_id then
return summon
end
end
end
local kishin = find_summon(KISHIN)
if kishin then
core.log("Kishin at " .. kishin.position.x .. ", " .. kishin.position.y)
if kishin.time_remaining and kishin.time_remaining < 5000 then
core.log("Kishin has under 5 seconds left")
end
endplayer:get_job() -> number
Available on GMS and CW
Returns the job ID.
Common Job IDs
| Job ID | Class |
|---|---|
0 | Beginner |
100 | Warrior |
200 | Magician |
300 | Bowman |
400 | Thief |
500 | Pirate |
Health & Mana
player:get_health() -> number
Available on GMS and CW
Returns current HP.
player:get_max_health() -> number
Available on GMS and CW
Returns maximum HP.
player:get_mana() -> number
Available on GMS and CW
Returns current MP.
player:get_max_mana() -> number
Available on GMS and CW
Returns maximum MP.
local hp_percent = (player:get_health() / player:get_max_health()) * 100
if hp_percent < 50 then
core.input.use_item(2000001) -- Use HP potion
endCurrency & Position
player:get_meso() -> number
Available on GMS and CW
Returns the amount of meso (currency).
player:get_sol_erda() -> number
Available on GMS and CW
Returns the amount of Sol Erda (the HEXA Matrix currency). Returns 0 if unavailable.
player:get_session_stats() -> table | nil
Available on GMS and CW
Returns what this play session has earned so far, and the rate it is earning at. Returns nil until the first sample lands.
| Field | Description |
|---|---|
runtime_sec | Seconds since tracking started |
meso_gained | Gross meso earned this session |
meso_per_hour | Meso per hour |
exp_gained | Gross EXP earned this session |
exp_per_hour | EXP per hour |
exp_percent_per_hour | Level progress per hour, in percent |
Rates are per hour, matching the web dashboard. For a per-minute figure, divide by 60:
local stats = player:get_session_stats()
if stats then
core.log(string.format("%.2fm meso/min", stats.meso_per_hour / 60 / 1e6))
endThe totals are live from the first sample, but the rates are read off a rolling five-minute window and stay 0 until that window spans at least a minute. They also drop back to 0 whenever sampling stops — see below.
So a script acting on the rate should treat 0 as "not known yet", never as "earning nothing".
Gross totals, and what a park does
"Gross" means only positive changes count. Spending meso does not reduce meso_gained, so the figure stays a record of what was farmed rather than what is left over. Levelling up does not reset exp_gained either.
Samples are only taken while in a field. Parking in the cash shop or auction house freezes the totals rather than resetting them — they resume where they left off.
The rates behave differently: once the newest sample is more than a minute old they go to 0, rather than continuing to quote a figure from before the break. After returning to a field they need about a minute of fresh samples before reading non-zero again.
player:get_position() -> table
Available on GMS and CW
Returns position as a table with x and y fields.
local pos = player:get_position()
-- pos.x, pos.yplayer:get_server_position() -> table
Available on GMS and CW
Returns the position the server believes the character is at, as a table with x and y. Useful when client-side movement and the server's view can drift apart (e.g. during teleport/rush automation) — checks against this position survive a desync where get_position() would lie.
local sp = player:get_server_position()
-- sp.x, sp.yplayer:anchor() -> number | nil
Available on GMS and CW
Returns an anchor handle for core.render, or nil if the player's position is unavailable. Pass it as the anchor draw option so shapes drawn on the player follow the sprite between logic steps instead of trailing it by a tick.
The handle is only valid for the on_render() call it was created in, so call anchor() again every frame. Pass the number itself, not the player object.
function on_render()
local player = core.object_manager.get_local_player()
if not player or not player:is_valid() then return end
local pos, a = player:get_position(), player:anchor()
if pos and a then
core.render.circle(pos.x, pos.y - 30, 40, { anchor = a, glow = 6 })
end
endplayer:get_render_position() -> table | nil
Available on GMS and CW
Returns the player's position as it was drawn in the latest rendered frame, as a table with x and y, or nil if no frame has captured it yet. Unlike get_position(), this includes between-tick interpolation, so it matches where the sprite is on screen.
The first call usually returns nil: it registers interest, and the position becomes available on the following frames. Prefer an anchor when you only need a shape to follow the player; use this when your logic needs the coordinate itself.
player:get_move_action() -> number
Available on GMS and CW
Returns the raw move action value for the character.
player:is_left() -> boolean
Available on GMS and CW
Returns true when the character is currently facing left.
if player:is_left() then
core.log("Facing left")
else
core.log("Facing right")
endHyperstats
Hyper stats unlock at level 140. There are 3 hyper stat presets (0–2), and only the active one can be raised.
The first read in a session loads the point totals and level costs. Until they arrive a moment later, available, used, total and next_cost are left out and get_hyperstat_sp() returns nil. If they never arrive, they stay left out until the Hyper Stat window is opened.
Hyper Stat Sheet Structure
| Property | Type | Description |
|---|---|---|
preset | number | Preset these values belong to (0–2) |
available | number? | Points left to spend on this preset |
used | number? | Points spent on this preset |
total | number? | Points earned up to the current level |
stats | table<hyper_stat> | The stats the Hyper Stat window lists, in ascending ID order |
Hyper Stat Structure
| Property | Type | Description |
|---|---|---|
id | number | Hyper stat ID |
name | string | Stat name |
level | number | Current level on this preset |
max_level | number | Highest level available now; it rises after the 5th job advancement |
next_cost | number? | Points needed for the next level; omitted at max_level |
can_level_up | boolean | true when the stat can be raised right now |
can_level_up is true only on the active preset, below max_level, on a map that allows raising hyper stats, and with at least next_cost points available. Stat 80000421 stays locked until the 5th job advancement, so until then it can show a next_cost while can_level_up is false.
player:get_hyperstat_sp() -> number | nil
Available on GMS
Returns the hyper stat points available on the active preset, the same number as get_hyper_stats().available. Returns nil if no character data is available or the points haven't loaded yet.
local sp = player:get_hyperstat_sp()player:get_hyper_stats(preset?: number) -> table | nil
Available on GMS
Returns the hyper stats of preset, or of the active preset when omitted. Returns nil if no character data is available or preset is not 0–2.
local sheet = player:get_hyper_stats()
if sheet then
for _, stat in ipairs(sheet.stats) do
if stat.can_level_up then
core.log(string.format("%s %d/%d: next level costs %d",
stat.name, stat.level, stat.max_level, stat.next_cost))
end
end
endplayer:level_up_hyper_stat(stat: number | string, count?: number) -> boolean, string?
Available on GMS
Spends hyper stat points to raise stat on the active preset by count levels (default 1, up to 15). stat is a hyper stat ID or its name as get_hyper_stats() lists it, in any case.
Returns true when the level-up was sent; the new level shows in get_hyper_stats() a moment later. Otherwise returns false and a reason, and nothing is sent:
| Reason | Meaning |
|---|---|
"no_character" | No character data is available |
"below_level" | The character is below level 140 |
"unknown_stat" | stat is not one of the stats get_hyper_stats() lists |
"locked" | Stat 80000421 before the 5th job advancement |
"map_blocked" | Hyper stats can't be raised on this map or in the character's current state |
"max_level" | The stat would go past max_level |
"not_loaded" | The points or level costs haven't loaded yet; try again shortly |
"not_enough_points" | Fewer points are available than the levels cost together |
"busy" | Another request was sent moments ago; try again shortly |
"invalid_count" | count is below 1 or above 15 |
A one-level call succeeds exactly when the stat's can_level_up is true, apart from "busy". Raising several levels at once costs the sum of each level's cost.
local ok, why = player:level_up_hyper_stat(80000400, 2)
if not ok then
core.log("Hyper stat not raised: " .. why)
endHyper Skills
Hyper skills unlock at level 140. Passive and active hyper skills earn points separately, and learning one costs a point of its type.
Hyper Skill Sheet Structure
| Property | Type | Description |
|---|---|---|
passive | table? | { total, used, available } points for passive hyper skills |
active | table? | { total, used, available } points for active hyper skills |
skills | table<hyper_skill> | The job's hyper skills, in skill window order |
passive and active are left out until the points have loaded, which normally happens right after login. Until then, can_learn is false for every hyper skill.
Hyper Skill Structure
| Property | Type | Description |
|---|---|---|
id | number | Skill ID |
name | string | Skill name |
type | string | "passive" or "active" |
level | number | Current level (0 = not learned) |
max_level | number | Maximum level |
required_level | number | Character level needed to learn it |
can_learn | boolean | true when it can be learned right now |
can_learn is true when the character meets required_level and any prerequisite skills, the skill isn't learned yet, a point of its type is available, and the map allows learning skills. A few class-specific summons and transformations still make the game refuse while they are active.
player:get_hyper_skills() -> table | nil
Available on GMS
Returns the character's hyper skills and hyper skill points. Returns nil if no character data is available.
local hyper = player:get_hyper_skills()
if hyper and hyper.active then
core.log("Active hyper skill points left: " .. hyper.active.available)
endSkill Points
player:get_skill_sp(tier: number) -> number
Available on GMS and CW
Returns the skill points available to spend on the given job-advancement tier (the skill window's tab). Most modern jobs use per-tier Extended SP — each job advancement has its own pool — so a single SP value isn't enough; pass the tier you want.
tier0= beginner skills (separate novice-SP pool)tier1+ = the job-advancement tabs in order, 1st job first (Dual Blade has six; for Zero,1and2are its two skill columns)
To find which tier a skill spends from, use core.skill_book.get_skill_level_up_info.
local sp = player:get_skill_sp(4) -- 4th job SP availableplayer:level_up_skill(skill_id: number, count: number)
Available on GMS and CW
Sends a skill-up request, spending SP to raise skill_id by count levels. Invalid requests are rejected. Hyper skills spend hyper SP and rise one level; count is ignored for them. core.skill_book.can_level_up tells you whether a point can go in first.
-- Auto-distribute a tier's available SP into one skill
local sp = player:get_skill_sp(4)
if sp > 0 then
player:level_up_skill(target_skill_id, sp)
endPresets
The Character Preset window (PRESET1–5) bundles one preset per category: equip, hyper_stat, ability, union, link_skill, shortcut, familiar. All preset indices are 0-based.
player:get_character_presets() -> table<character_preset> | nil
Available on GMS and CW
Returns the 5 Character Preset bundles. Each is { index, name, <category> = preset }; a category the bundle leaves unchanged has no field. nil if character data is unavailable.
player:get_active_presets() -> table | nil
Available on GMS and CW
Returns the preset currently active in each category, keyed the same way (equip, hyper_stat, …). A category that can't be read right now has no field.
player:apply_character_preset(index: number) -> boolean
Available on GMS and CW
Applies Character Preset bundle index without showing the swap prompt. Like the in-game window, it leaves out parts the game won't change right now, such as Hyper Stat below level 140 or a part the current map blocks. Returns false when the slot is empty, nothing would change, or the game would refuse the swap right now (dead, in battle, or another request still pending). true only means the request was sent; poll get_active_presets() to see it land.
player:change_equip_preset(index: number) -> boolean, number
Available on GMS and CW
Switches the equipment window's preset tab (0–2). Returns true, 0 when sent, otherwise false and a reason code (15 = already active, 12 = dead, 1 = blocked).
-- Swap to the second bundle and wait for the equip preset to flip
local bundle = player:get_character_presets()[2]
if bundle and bundle.equip and player:apply_character_preset(bundle.index) then
swapping_to = bundle.equip
end
function on_tick()
if swapping_to and player:get_active_presets().equip == swapping_to then
swapping_to = nil
core.log("preset swap landed")
end
endSymbols
Arcane and Sacred symbols are individual equipped items, each with its own level and growth EXP. These return one entry per equipped symbol — unequipped region slots are skipped.
Symbol Object Structure
| Property | Type | Description |
|---|---|---|
index | number | Region slot 0-5 (region order); Grand Sacred symbols continue at 6-8 |
position | number | Equip position |
level | number | Current symbol level |
exp | number | Growth EXP toward the next level |
max_level | number | Level cap (arcane 20, sacred 11) |
exp_to_next | number | EXP required to reach the next level |
can_level_up | boolean | true when exp >= exp_to_next and below the cap |
is_max | boolean | true when at the level cap |
player:get_arcane_symbols() -> table<symbol>
Available on GMS
Returns an array of the player's equipped Arcane symbols, in Arcane River region order.
player:get_sacred_symbols() -> table<symbol>
Available on GMS
Returns an array of the player's equipped Sacred (Authentic) symbols, in Grandis region order, followed by the equipped Grand Sacred symbols (Tallahart, Geardock).
for _, sym in ipairs(player:get_arcane_symbols()) do
if sym.can_level_up then
core.log(("arcane #%d ready: lvl %d (%d/%d)"):format(
sym.index, sym.level, sym.exp, sym.exp_to_next))
end
endBuff Management
Buff Object Structure
| Property | Type | Description |
|---|---|---|
id | number | Unique buff identifier |
type | number | Buff type identifier |
name | string | Readable buff name |
time_remaining | number | Milliseconds remaining |
stacks | number? | Stack count shown on the buff icon, such as Mo Xuan's Divine Fist. Only some stacking buffs report it; absent otherwise |
player:has_buff(buff_id: number) -> boolean
Available on GMS and CW
Returns true if the buff is currently active. More efficient than iterating get_buffs().
player:get_buff(buff_id: number) -> buff | nil
Available on GMS and CW
Returns a specific buff object if active, or nil if not found.
player:get_buffs() -> table<buff>
Available on GMS and CW
Returns all active buffs as an array.
Class State
player:get_class_state() -> table | nil
Available on GMS
Returns the resources and forms a class keeps outside its buffs, or nil if character data is not loaded. The fields depend on the class; any other class gets an empty table.
| Class | Field | Description |
|---|---|---|
| Zero | form | "alpha" or "beta" |
| Ark | form | "flora" in Flora State, "specter" in Specter State |
| Ark | spectra | Spectra, 0–1000 |
| Hoyoung | talisman_energy | Talisman Energy, 0–100 |
| Hoyoung | scroll_energy | Scroll Energy, 0–900 |
| Hoyoung | heaven, earth, humanity | true for each attribute lit in the current attribute combo |
| Sia Astelle | stellagram | Marks recorded on the Stellagram, oldest first: "ray", "boom", "pole", "link" or "shine". Empty when nothing is recorded |
local state = player:get_class_state()
if state and state.form == "specter" and state.spectra < 200 then
core.log("Spectra is running low")
endExample
-- Maintain buff and monitor HP
local MAGIC_GUARD = 2001002
function on_tick(stage)
if stage ~= core.stage.FIELD then return end
local player = core.object_manager.get_local_player()
if not player or not player:is_valid() then return end
-- HP monitoring
local hp_percent = (player:get_health() / player:get_max_health()) * 100
if hp_percent < 30 then
core.input.use_item(2000001) -- Red Potion
end
-- Buff maintenance
if not player:has_buff(MAGIC_GUARD) then
if not core.skill_book.is_skill_on_cooldown(MAGIC_GUARD) then
core.input.use_skill(MAGIC_GUARD)
end
end
end