Violet

Local Player

On this page

The Player object represents the local player character. Provides access to stats, buffs, position, and other player data.

Always Validate

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.

lua
local player = core.object_manager.get_local_player()
if not player or not player:is_valid() then
    return  -- Exit early
end

Basic 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.

FieldDescription
strTotal STR
dexTotal DEX
intTotal INT
lukTotal LUK
apUnspent 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.

FieldDescription
damageDamage %
boss_damageBoss Damage %
final_damageFinal Damage %
ignore_defenseIgnore Defense %
critical_rateCritical Rate % that applies: capped at 100, even when the window shows more
critical_damageCritical Damage %
star_forceStar Force
arcane_powerArcane Power
sacred_powerSacred Power
exp_bonusAdditional 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.

lua
local stats = player:get_detailed_stats()
if stats and stats.critical_rate < 100 then
    core.log("Critical Rate is " .. (100 - stats.critical_rate) .. "% short")
end

player: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.

lua
for i, pet in ipairs(player:get_pets()) do
    if pet.repleteness < 30 then
        core.log("pet " .. i .. " is hungry")
    end
end

player: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.

FieldDescription
idUnique ID of this summon on the map
skill_idSkill that created it
skill_levelLevel of that skill
position{x, y} where the summon currently is
time_remainingMilliseconds until the summon's time runs out. Absent for a summon without a timer, and once its time is up
lua
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
end

player:get_job() -> number

Available on GMS and CW

Returns the job ID.

Common Job IDs
Job IDClass
0Beginner
100Warrior
200Magician
300Bowman
400Thief
500Pirate

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.

HP/MP Percentage
lua
local hp_percent = (player:get_health() / player:get_max_health()) * 100
if hp_percent < 50 then
    core.input.use_item(2000001)  -- Use HP potion
end

Currency & 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.

FieldDescription
runtime_secSeconds since tracking started
meso_gainedGross meso earned this session
meso_per_hourMeso per hour
exp_gainedGross EXP earned this session
exp_per_hourEXP per hour
exp_percent_per_hourLevel progress per hour, in percent

Rates are per hour, matching the web dashboard. For a per-minute figure, divide by 60:

lua
local stats = player:get_session_stats()
if stats then
    core.log(string.format("%.2fm meso/min", stats.meso_per_hour / 60 / 1e6))
end
A rate of `0` means 'no rate available'

The 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.

lua
local pos = player:get_position()
-- pos.x, pos.y

player: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.

lua
local sp = player:get_server_position()
-- sp.x, sp.y

player: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.

lua
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
end

player: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.

lua
if player:is_left() then
    core.log("Facing left")
else
    core.log("Facing right")
end

Hyperstats

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

PropertyTypeDescription
presetnumberPreset these values belong to (0–2)
availablenumber?Points left to spend on this preset
usednumber?Points spent on this preset
totalnumber?Points earned up to the current level
statstable<hyper_stat>The stats the Hyper Stat window lists, in ascending ID order

Hyper Stat Structure

PropertyTypeDescription
idnumberHyper stat ID
namestringStat name
levelnumberCurrent level on this preset
max_levelnumberHighest level available now; it rises after the 5th job advancement
next_costnumber?Points needed for the next level; omitted at max_level
can_level_upbooleantrue 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.

lua
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.

lua
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
end

player: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:

ReasonMeaning
"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.

lua
local ok, why = player:level_up_hyper_stat(80000400, 2)
if not ok then
    core.log("Hyper stat not raised: " .. why)
end

Hyper 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

PropertyTypeDescription
passivetable?{ total, used, available } points for passive hyper skills
activetable?{ total, used, available } points for active hyper skills
skillstable<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

PropertyTypeDescription
idnumberSkill ID
namestringSkill name
typestring"passive" or "active"
levelnumberCurrent level (0 = not learned)
max_levelnumberMaximum level
required_levelnumberCharacter level needed to learn it
can_learnbooleantrue 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.

lua
local hyper = player:get_hyper_skills()
if hyper and hyper.active then
    core.log("Active hyper skill points left: " .. hyper.active.available)
end

Skill 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.

  • tier 0 = beginner skills (separate novice-SP pool)
  • tier 1+ = the job-advancement tabs in order, 1st job first (Dual Blade has six; for Zero, 1 and 2 are its two skill columns)

To find which tier a skill spends from, use core.skill_book.get_skill_level_up_info.

lua
local sp = player:get_skill_sp(4)  -- 4th job SP available

player: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.

lua
-- 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)
end

Presets

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).

lua
-- 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
end

Symbols

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

PropertyTypeDescription
indexnumberRegion slot 0-5 (region order); Grand Sacred symbols continue at 6-8
positionnumberEquip position
levelnumberCurrent symbol level
expnumberGrowth EXP toward the next level
max_levelnumberLevel cap (arcane 20, sacred 11)
exp_to_nextnumberEXP required to reach the next level
can_level_upbooleantrue when exp >= exp_to_next and below the cap
is_maxbooleantrue 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).

lua
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
end

Buff Management

Buff Object Structure

PropertyTypeDescription
idnumberUnique buff identifier
typenumberBuff type identifier
namestringReadable buff name
time_remainingnumberMilliseconds remaining
stacksnumber?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.

ClassFieldDescription
Zeroform"alpha" or "beta"
Arkform"flora" in Flora State, "specter" in Specter State
ArkspectraSpectra, 0–1000
Hoyoungtalisman_energyTalisman Energy, 0–100
Hoyoungscroll_energyScroll Energy, 0–900
Hoyoungheaven, earth, humanitytrue for each attribute lit in the current attribute combo
Sia AstellestellagramMarks recorded on the Stellagram, oldest first: "ray", "boom", "pole", "link" or "shine". Empty when nothing is recorded
lua
local state = player:get_class_state()
if state and state.form == "specter" and state.spectra < 200 then
    core.log("Spectra is running low")
end

Example

lua
-- 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