RKL Suite

Gear · Forge · Crafting · Magic · Seasonal

RKL Suite — Developer Docs

Connected FiveM resources for RPG servers: wearable gear with stats and durability, a forge for upgrades, enchants and sockets, an XP-based crafting system, and a full spell & skill layer. Everything runs on ESX Legacy, QBCore and Qbox through auto-detecting bridges.

rklequipment — gear, stats, combat rklforge — upgrade · enchant · socket rklcrafting — recipes & XP spell system — 5 magic resources effects & FX — status & particles rklinterface — HUD & class build rklhalloween — Hallowed Night seasonal event

Overview

Each resource is independent but they share data. The forge reads and writes the equipment metadata, so equipment is the core the others build on.

ResourceDoesOwns
rklequipmentEquip gear, apply stats, durability, combat combosplayer_equipment
rklforgeUpgrade / enchant / socket equipped gearedits equipment metadata
craftingCraft items, levels & XPslv_crafting_data
Load order matters

The forge edits equipment metadata directly, so it must start after equipment. Full order is in the next section.

Install & server.cfg

Required on every framework

ox_lib, ox_inventory, and oxmysql. No es_extended is required for QBCore/Qbox — the bridge detects the framework automatically (qbx_core → Qbox, qb-core → QBCore, es_extended → ESX Legacy).

Ensure order

server.cfg
ensure oxmysql ensure ox_lib ensure ox_inventory # your framework (one of) ensure es_extended # or qb-core / qbx_core ensure rklequipment # core — start first ensure rklforge # AFTER equipment ensure rklcrafting ensure rklskilltree # class XP / skill nodes ensure rklbinder # card binder (uses forge_card) ensure rklrepair # durability repair NPC # effects (needed for combat visuals & damage numbers) ensure rklparticleseffect ensure rklstatuseffect # spell system ensure rklgrimoire ensure rklskillsystem ensure rklspellmanage ensure rklspellbook ensure rklskillcheck # HUD (reads skilltree + forge) ensure rklinterface

Currency — pick what players pay with

Costs that the scripts call "awaken coin" are not hard-wired to an item. Set Config.RKLCurrency in the resource's config.lua (an open file, editable under escrow) and the bridge routes every charge and balance check for you — same behaviour on ESX Legacy, QBCore and Qbox.

config.lua
Config.RKLCurrency = { type = 'item', item = 'awaken_coin' } -- ox_inventory item -- Config.RKLCurrency = { type = 'money' } -- cash -- Config.RKLCurrency = { type = 'bank' } -- bank balance -- Config.RKLCurrency = { type = 'account', account = 'black_money' }
typeESX LegacyQBCore / Qbox
itemox_inventory itemox_inventory item
moneygetMoney()money.cash
bankgetAccount('bank')money.bank
accountnamed ESX accountnamed money type
Leaving it out is safe

If Config.RKLCurrency isn't defined, nothing changes: an ESX server's own getAwakenCoin() is used when it exists, otherwise the awaken_coin ox_inventory item. Defining it always wins, so your choice is honoured on every framework.

Per-shop currency in rklequipment

Equipment shops already choose their own currency with currencyType in Config.Shops — set it to money, bank, any ox_inventory item name, or awaken_coin to follow the Config.RKLCurrency setting above.

Resource names

Exports and cross-resource events use fixed names, and a FiveM export only resolves if the folder name matches the caller. The whole suite is branded rkl* — keep these folder names exactly:

Folder nameUsed by
rklequipmentexports['rklequipment'] · rklequipment:* events
rklforgeexports['rklforge'] · rklforge:* events
rklcraftingrklcrafting:* events & callbacks
All references are rkl

Every export, event, callback and cross-resource sync uses the rkl names above, matching the folders. NUI uses GetParentResourceName(), so the UI follows the folder name automatically. The database tables (player_equipment, slv_crafting_data) keep their original names so existing data is untouched.

How they connect

The resources are independent but share three things: exports (direct calls), events (one-way messages), and database tables. This is the full dependency picture.

Direct export calls

CallerCallsForRequired?
rklequipmentrklskilltree:GetJobLevelLevel gate on gearOptional
rklinterfacerklforge:GetPlayerStatTotalCooldown / mana / skill-damage bonusesOptional
rklforgerklequipment:GetDefense, ApplyMagicDefenseReading gear statsRequired
rklbinder— (shares the forge_card item)Storing forge cards in an albumStandalone
rklrepair— (shares the equipment item)Restoring durabilityStandalone
Optional means optional

Where a call is marked optional, the caller checks the resource is running and falls back to 0 if it isn't — so you can ship a partial suite without console errors.

Event flow

FromEventToMeaning
rklforgerklequipment:updateCacherklequipmentGear changed — re-read metadata & re-apply stats
rklskilltreerklinterface:updatelvlxprklinterfaceClass level / XP changed — refresh HUD
rklskilltreerklinterface:refreshskillsrklinterfaceSkills changed — recalculate the build
rklskilltreerklinterface:resetstatrklinterfaceClear cached stat icons
rklinterfacerklequipment:addedstatsrklequipmentPush HUD-side stat changes
rklequipmentrkldamageped:victimrklparticleseffectDamage an NPC & show the number
rklequipmentrklstatuseffect:applyStatusrklstatuseffectApply burn / poison / etc. on hit

Shared database tables

TableWritten byRead by
player_equipmentrklequipment, rklforgerklequipment, rklforge
slv_player_job_pointsrklskilltreerklskilltree, rklinterface
slv_player_skillsrklskilltree, rklinterfacerklskilltree, rklinterface
slv_skills / slv_skill_treesrklskilltreerklskilltree, rklinterface
slv_crafting_datarklcraftingrklcrafting

Optional integrations

Three resources outside this suite are used when present: rklzones (safe zones), rklcompanion (pet detection) and rklspawnmonster (monster lookup). rklskilltree and rklforge are optional to each other in the same way.

Every one of these calls is wrapped, so a missing resource can never throw. You can also switch them off explicitly:

config.lua
Config.Integrations = { ['rklzones'] = true, -- fallback: never in a safe zone ['rklcompanion'] = true, -- fallback: not a companion ['rklspawnmonster'] = true, -- fallback: no monster ['rklskilltree'] = true, -- fallback: level 0 }
ValueBehaviour
trueUse it, but only when the resource is actually started
falseNever call it — the fallback value is used
omittedSame as true, so older configs keep working

Shared state bag

LocalPlayer.state.playerClass (and playerSubclass) is how the player's class travels between resources. rklskilltree sets it; rklinterface and rklequipment read it. It is framework-independent, so it behaves the same on ESX, QBCore and Qbox.

Start order matters

rklforge writes to equipment metadata and rklinterface reads forge stats, so start rklequipment first, then rklforge, then the rest. The full order is in Install & server.cfg.

Database & SQL

Import rkl_suite.sql once on your server database. Every statement is IF NOT EXISTS, so re-running it is safe and never deletes data.

TableHoldsKey
player_equipmentEquipped gear + full item JSONidentifier + slot
slv_crafting_dataCrafting level and XPidentifier
slv_player_job_pointsClass XP, level, skill pointsidentifier + job_name
slv_skill_treesOne tree per class + saved layoutid
slv_skillsEvery skill node and its unlock rulesid
slv_player_skillsUnlocked skills, rank, HUD slotidentifier + skill_id
slv_admin_xp_logsAudit trail for admin XP grantsid
sl_binderCard binder contents (JSON per player)identifier

Character class columns

The skill tree stores the class, sub-classes and rank on the character row. Run the block that matches your framework — the SQL file has both, with the QBCore/Qbox one commented out.

ESX Legacy
ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `class` VARCHAR(64) DEFAULT NULL, ADD COLUMN IF NOT EXISTS `sub_classes` TEXT DEFAULT NULL, ADD COLUMN IF NOT EXISTS `rank` VARCHAR(8) DEFAULT 'E';
QBCore / Qbox
ALTER TABLE `players` ADD COLUMN IF NOT EXISTS `class` VARCHAR(64) DEFAULT NULL, ADD COLUMN IF NOT EXISTS `sub_classes` TEXT DEFAULT NULL, ADD COLUMN IF NOT EXISTS `rank` VARCHAR(8) DEFAULT 'E';
The identifier column

Scripts store whatever the bridge returns — the ESX identifier on ESX Legacy, the citizenid on QBCore/Qbox. Both fit VARCHAR(64), so no schema change is needed per framework. Moving frameworks on the same database means old rows won't match, which is normal for any migration.

rklequipment client exports

The core. Manages equipped gear, the stats they grant (defense, magic defense, set bonuses), durability, and the melee combo system. All of its exports run on the client.

Exports

IsCombatMode()Client

Returns whether the local player is mid-attack (an attack/combo animation is playing). Use it to block actions that shouldn't interrupt combat.

ReturnsTypeNotes
isAttackingbooleantrue while an attack is in progress
client
if exports['rklequipment']:IsCombatMode() then return -- player is swinging, skip end
GetDefense()Client

Current total physical defense from all equipped gear (base + upgrades + sockets). Use it to reduce incoming physical damage.

ReturnsTypeNotes
defensenumber0 when nothing is equipped
client
local def = exports['rklequipment']:GetDefense() local final = math.max(1, incomingDamage - def)
ApplyMagicDefense()Client

Current total magic defense from equipped gear. Subtract it the same way you would physical defense for spell / elemental damage.

ReturnsTypeNotes
magicDefensenumbertakes no arguments; returns the value
client
local mdef = exports['rklequipment']:ApplyMagicDefense() local final = math.max(1, spellDamage - mdef)
GetSetBonuses()Client

Returns the full set-bonus config table (every set and the stats each grants). Handy for showing set info in a UI.

ReturnsTypeNotes
setBonusestablemirror of Config.SetBonuses
client
local sets = exports['rklequipment']:GetSetBonuses() for setId, set in pairs(sets) do print(setId, set.label) end
GetItemSet(itemName)Client

Finds which set an item belongs to. Returns the set id and set table, or nil if the item isn't part of any set.

ParameterTypeNotes
itemNamestringox_inventory item name
ReturnsTypeNotes
setIdstring | nilid of the matching set
settable | nilthe set definition
client
local setId, set = exports['rklequipment']:GetItemSet('dragon_helm') if setId then print('Part of set:', set.label) end

Open files — combos & language

Two files ship outside the escrow so server owners can edit them with the resource still encrypted. Both are listed in escrow_ignore.

language.lua

Every player-facing string lives in a Lang table. Translate values freely; keep any %s placeholder — it's filled at runtime (for example the item name in a "broke" message).

language.lua
['armor_broken_desc'] = 'Your %s broke! Its bonus stats are gone.',

The helper is called _L('key') (not _U) so it never clashes with ESX's built-in translator.

combos.lua

Each weapon type's attack animation sequence is data only — change the dictionary, clip, timing or number of stages without touching logic.

combos.lua
Config.Combos.Katana = { [0] = {dict = 'kp_wm_gs_slash_01', anim = 'kp_wm_gs_slash_01_clip', wait = 700, next = 1}, [1] = {dict = 'kp_wm_gs_slash_02', anim = 'kp_wm_gs_slash_02_clip', wait = 700, next = 2}, -- last stage loops back with next = 0 }

Built-in types: BigSword, Staff, Unarmed, DualDagger, Katana, DualKatana.

Add a brand-new weapon type

The combo dispatch is data-driven: any weapon type that isn't built in is played by the generic runner — no escrowed edits needed.

  1. Give the weapon item w_type = "yourtype" in config.lua.
  2. Define Config.Combos.yourtype in combos.lua.
combos.lua — custom type
Config.Combos.spear = { [0] = {dict = 'melee@large_wpn@streamed_core', anim = 'plyr_takedown_front_attacker', wait = 900, flag = 1, next = 1, sound = 'hitflesh', damageRange = 2.5}, [1] = {dict = 'melee@large_wpn@streamed_core', anim = 'plyr_takedown_rear_attacker', wait = 1100, flag = 1, next = 0, freeze = true, vfx = {dict = 'scr_sum2_hal', part = 'scr_sum2_hal_rider_weak_green', attachType = 'weapon_tip', loop = true, color = {r = 0.2, g = 0.6, b = 1.0}, scale = 0.5, duration = 700, off = {x = 0.0, y = 1.0, z = 0.0}}}, }

Optional per-stage fields

flag, freeze, blendIn, blendOut, wait, next, sound, damageRange, and vfx (a particle table — add weapon-tip effects straight from the open file).

Optional per-type tuning

combos.lua
Config.ComboSettings = { spear = { speedFactor = 0.03, speedCap = 0.30, minWait = 300, comboTimer = 5, slot = 'right_hand' }, }
Built-ins are untouched

The six built-in combos keep their own hand-tuned behavior (Staff casts priest/mage effects, DualDagger freezes, BigSword has VFX + attack speed). The generic runner is used for custom types only.

Commands & notable events

CommandAccessDoes
/managegeargroup.adminOpen the custom-equipment creator
/toggleweaponplayerHide / show the equipped weapon
/testanimsdebugCombat animation tester menu
/debugdetectdebugPrint detection / state info

Useful events

EventSideDoes
rklequipment:updateCacheclientRe-read equipped metadata & re-apply stats (forge fires this after editing gear)
rklequipment:openequipmentclientOpen the equipment UI

Most other events are internal (durability, damage sync, UI refresh) and aren't meant to be called from outside.

rklforge server exports

Upgrade, enchant and socket equipped gear. It reads and writes equipment metadata, then tells equipment to refresh. All of its exports run on the server.

Exports

GiveForgeReq(src, reqType, count, label, imageurl, description)Server

Gives a player a forge requirement item (the Config.ForgeReqItem) carrying the metadata you pass. Use it to hand out forge materials from loot, shops or quests.

ParameterTypeNotes
srcnumberplayer server id
reqTypestringrequirement id (e.g. protect_scroll)
countnumberamount — defaults to 1
labelstring?display name; defaults to reqType
imageurlstring?optional image URL
descriptionstring?optional tooltip text
ReturnsTypeNotes
okbooleanfalse if src or reqType is missing
server
exports['rklforge']:GiveForgeReq(src, 'protect_scroll', 1, 'Protection Scroll')
GiveForgeCard(src, cardType, level, imageurl, description)Server

Gives a player a forge card item (the Config.ForgeCardItem). The cardType must exist in Config.Cards.types.

ParameterTypeNotes
srcnumberplayer server id
cardTypestringkey in Config.Cards.types
levelnumbercard level — defaults to 1
imageurlstring?optional image URL
descriptionstring?optional; defaults to the card's desc
ReturnsTypeNotes
okbooleanfalse if cardType is unknown
server
exports['rklforge']:GiveForgeCard(src, 'attack_card', 3)
GetPlayerStatTotal(src, statKey)Server

Sums one stat across all equipped gear — base value plus upgrades plus sockets. Broken items (durability ≤ 0) are skipped.

ParameterTypeNotes
srcnumberplayer server id
statKeystringstat name, e.g. attack, luck, defense
ReturnsTypeNotes
totalnumber0 if none / invalid
server
local atk = exports['rklforge']:GetPlayerStatTotal(src, 'attack')
GetPlayerLuck(src)Server

Convenience wrapper for the total luck stat — the same as GetPlayerStatTotal(src, 'luck'). Useful for loot-chance boosts.

ParameterTypeNotes
srcnumberplayer server id
ReturnsTypeNotes
lucknumbertotal luck across equipped gear
server
local luck = exports['rklforge']:GetPlayerLuck(src) local chance = baseChance + (luck * 0.5)

Currency & items

ConfigPurpose
Config.CurrencyWhat upgrades / enchants cost (upgrade, enchant)
Config.ForgeReqItemItem name used by GiveForgeReq
Config.ForgeCardItemItem name used by GiveForgeCard
Config.Cards.typesDefined card types & their labels

Ready-made export examples also live, commented, inside rklforge/ox_inventory_items.lua.

Commands & notable events

CommandAccessDoes
/forgeadmingroup.adminOpen the forge admin menu
/giveforgereqadminGive forge requirement items
/giveforgecardadminGive forge cards
/forgedebugreqdebugInspect forge requirement metadata

Useful events

EventSideDoes
rklforge:openMenuclientOpen the forge menu for the player

rklcrafting events: rklcrafting:*

An XP-based crafting station. Players craft items from recipes, gaining levels and XP stored in slv_crafting_data. Internal events use the rklcrafting: prefix.

Export & callback

craft_boost_potion(data, slot)Client

An ox_inventory item-use handler for a craft-boost potion. Reads boostChance from the item metadata and applies a temporary boost to crafting success. Wire it as the item's use export in your ox_inventory items file.

ParameterTypeNotes
datatableox_inventory item-use payload
slotnumberinventory slot
ox_inventory/data/items — client.export
['craft_boost'] = { label = 'Crafting Boost Potion', client = { export = 'rklcrafting.craft_boost_potion' }, }
lib.callback 'rklcrafting:server:getHunterData'Server

Server callback returning the player's crafting level / XP record. Fetch it from the client with lib.callback.await.

client
local data = lib.callback.await('rklcrafting:server:getHunterData', false) print(data.level, data.xp)

Commands & data

CommandAccessDoes
/craftboostadminApply / set a crafting boost

Config & database

KeyPurpose
Config.CraftPlaceCrafting station location(s)
Config.XPTable / GetRequiredXPLevel curve & XP per level
slv_crafting_dataDB table: identifier/citizenid, level, xp

Spell System

Five connected resources that add a magic / skill layer: a grimoire with spell XP and a hotbar, the skill engine that casts spells and tracks mana, spellbook items with unlocked-skill storage, a spell menu, and a cast-time skillcheck minigame. Each ships with its own framework bridge (ESX Legacy / QBCore / Qbox auto-detected) and most cast text lives in an open language.lua.

ResourceDoesExports
rklgrimoireSpell XP, hotbar, spell upgradesAddGrimoireXP · GetHotbarData
rklskillsystemCast skills, mana, damage, UI toggles4 toggle exports
rklspellmanageSpellbook items & unlocked-skill storage (+ shop)usebook · getUnlockedSkills
rklspellbookSpell menu / permitted spellscallbacks & events
rklskillcheckCast-time skillcheck minigameevents
Optional integrations

Can read from an external skill tree via Config.DevhubSkillTree (the devhub_skillTree resource / dh_skilltree table). rklskillsystem can also gate access by job, by op-crime orgs (Config.EnableOPCrime), or by Discord roles.

rklgrimoire XP · hotbar · upgrades

Tracks each player's spell XP and the spell hotbar, and processes spell upgrades.

Exports

AddGrimoireXP(target, amount)Server

Grants spell (grimoire) XP to a player. Use it to reward casting, quests or events.

ParameterTypeNotes
targetnumberplayer server id
amountnumberXP to add
server
exports['rklgrimoire']:AddGrimoireXP(src, 50)
GetHotbarData()Client

Returns the local player's current spell hotbar data — the slots and the spells bound to them.

ReturnsTypeNotes
hotbartablecurrent hotbar slots
client
local hotbar = exports['rklgrimoire']:GetHotbarData()

Callbacks & events

NameKindDoes
grimoire:getPlayerDatacallbackFetch the player's grimoire record
grimoire:getLearnedSpellscallbackList the player's learned spells
rklgrimoire:serverUpgradeSpellcallbackUpgrade a spell (server-validated)
rklgrimoire:hotbarUpdatedeventFires when the hotbar changes (UI listens)
rklgrimoire:showMagicNotifyeventShow a themed magic notification

Command

CommandAccessDoes
/addxp <id> <amount>ace command.addxpAdd grimoire XP (console always allowed; ace set via Config.AddXPAce)

rklskillsystem cast · mana · UI

The skill engine — casting, mana, damage indicators and the spell hotbar UI. Its exports run on the client and let other scripts disable casting or hide the UI (cutscenes, downed states, safe zones).

Exports

SetDisableSkills(status)Client

Enable or disable the player's ability to cast skills. Pass true to block casting, false to allow it again.

ParameterTypeNotes
statusbooleantrue = casting disabled
client
exports['rklskillsystem']:SetDisableSkills(true) -- during a cutscene exports['rklskillsystem']:SetDisableSkills(false) -- restore
IsSkillsDisabled()Client

Returns whether casting is currently disabled.

ReturnsTypeNotes
disabledboolean
client
if exports['rklskillsystem']:IsSkillsDisabled() then return end
SetUIHidden(status)Client

Force-hide or show the spell hotbar UI. Pass true to hide it, false to bring it back.

ParameterTypeNotes
statusbooleantrue = UI hidden
client
exports['rklskillsystem']:SetUIHidden(true)
IsUIHidden()Client

Returns whether the spell hotbar UI is currently force-hidden.

ReturnsTypeNotes
hiddenboolean
client
local hidden = exports['rklskillsystem']:IsUIHidden()

Callback, command & events

NameKindDoes
rklskillsystem:getPlayerDatacallbackFetch the player's skill data
/skillsystemcommandOpen the skill manager UI (blocked while downed)
rklskillsystem:notifyeventShow a skill notification
rklskillsystem:showdamageeventShow a damage indicator
rklskillsystem:enableusingskills / disableusingskillseventEvent-based toggle (same effect as the exports)

Config highlights

Config.Mana, Config.Skills, Config.Hotbar, Config.RequireWeapon, Config.DamageIndicator, plus access gates Config.AllowedJob / Config.AllowedOrgs / Config.EnableJobLock / Config.EnableOPCrime.

rklspellmanage books · unlocks · shop

Handles spellbook items and stores which skills a player has unlocked. Includes an optional ox_inventory shop for skillbooks.

Exports

getUnlockedSkills(source)Server

Returns the list of skills a player has unlocked. With Config.DevhubSkillTree enabled it reads from the dh_skilltree table; otherwise from this resource's own storage.

ParameterTypeNotes
sourcenumberplayer server id
ReturnsTypeNotes
skillstablelist of { uid, categoryName }; empty if none
server
local skills = exports['rklspellmanage']:getUnlockedSkills(src) for _, s in ipairs(skills) do print(s.categoryName, s.uid) end
usebook(data, slot)Client

ox_inventory item-use handler for a spellbook. It runs the standard item use, then checks the player's race/job against the book's metadata (framework auto-detected) before unlocking. Wire it as the item's use export.

ParameterTypeNotes
datatableox_inventory item-use payload
slotnumberinventory slot
ox_inventory/data/items — client.export
['spellbook_fire'] = { label = 'Fire Spellbook', client = { export = 'rklspellmanage.usebook' }, }

Config highlights

Config.Abilities, Config.Command, Config.DevhubSkillTree, Config.EnableOxShopforSkillbook, Config.usingSkillbook.

rklspellbook spell menu

The spell menu — shows the spells a player is permitted to use and supports targeting other players. Driven by callbacks and an open event; it has no exports.

NameKindDoes
rkl_spellbook:client:openSpellbookeventOpen the spell menu for the player
rkl_spellmenu:getPermittedSpellscallbackList spells the player may cast
rkl_spellmenu:getPlayerscallbackNearby players for targeting
client — open the menu
TriggerEvent('rkl_spellbook:client:openSpellbook')

Open command is configurable via Config.SpellMenuCommand.

rklskillcheck cast minigame

A cast-time skillcheck minigame. Other spell resources start it during a cast; the result decides whether the spell succeeds. No exports — it's event-driven.

EventSideDoes
rklskillcheck:startSpellMinigameclientBegin the skillcheck for a cast
rklskillcheck:finishclientFired with the result (pass / fail)
Shared spell events

Across the suite, VFX use rklskill:spawnParticles / rklskill:playParticles / rklskill:stopParticles, and level syncing uses rklspells:updateLevels.

Effects & FX

Two shared layers used across combat and magic: a status-effect system (poison, burn, slow, blind, stun, freeze) with screen and world VFX, and a particle / damage-number layer that other resources fire to show hits and effects.

Shared by the suite

The FX layer handles rkldamageped:victim — the same event the equipment combos fire to damage NPCs and show floating numbers. Keep it running or combat visuals won't appear.

rklstatuseffect poison · burn · stun…

Applies timed status effects with optional damage-over-time plus screen and world particles. Built-in statuses: burn, poison, slow, blind, stun, freeze — add your own in Config.Statuses.

Server exports

GiveStatus(target, name, duration, damage)Server

Applies a status to a player from the server. duration is in seconds (default 5). Use it for traps, spells, poison weapons, etc.

ParameterTypeNotes
targetnumberplayer server id
namestringstatus key in Config.Statuses
durationnumberseconds (default 5)
damagenumber | falseper-tick damage; omit for config default, false for none
server
-- burn for 8 seconds, 5 damage per tick exports['rklstatuseffect']:GiveStatus(src, 'burn', 8, 5)
TakeStatus(target, name)Server

Removes a status from a player.

ParameterTypeNotes
targetnumberplayer server id
namestringstatus to remove
server
exports['rklstatuseffect']:TakeStatus(src, 'burn')

Client exports

ApplyStatus(name, duration, damage)Client

Applies a status to the local player. Same fallbacks as the event: omit duration / damage to use the config defaults; pass false for no damage.

ParameterTypeNotes
namestringstatus key
durationnumber?seconds; nil = config default
damagenumber | false?per-tick; nil = config default
client
exports['rklstatuseffect']:ApplyStatus('poison', 10, 3)
HasStatus(name)Client

Returns whether the local player currently has a status.

ReturnsTypeNotes
activeboolean
client
if exports['rklstatuseffect']:HasStatus('freeze') then return end
RemoveStatus(name)Client

Removes a status from the local player.

ParameterTypeNotes
namestringstatus to remove
client
exports['rklstatuseffect']:RemoveStatus('poison')

Event & config

You can also apply a status client-side via event — handy from another client script:

client
TriggerEvent('rklstatuseffect:status', 'slow', 6) -- name + duration, config damage TriggerEvent('rklstatuseffect:status', 'blind') -- name only, all config defaults

Add or tune statuses in Config.Statuses (each entry has duration, damage, screen FX and optional worldPtfx). Other config: Config.RenderDistance, Config.WaitStatus, Config.IdleWait, and a built-in test command via Config.CommandTest.

rklparticleseffect particles · damage numbers

The shared visual layer: plays attached particle effects on peds and shows floating damage numbers. It's event-driven (no exports) — other resources fire its events. Events use the rklcharfx: and rkldamgeshow: prefixes, plus the ped-damage handler.

EventSideDoes
rkldamageped:victimserverApply & show damage on a ped (NPC); fired by combat scripts
rklcharfx:syncserverPlay a particle on the caster, synced to nearby players
rklcharfx:playLocalclientPlay a particle locally on a target ped
rkldamgeshow:startclientShow a floating damage number above a target

Dealing ped damage (same call the combos use)

client → server event
TriggerServerEvent('rkldamageped:victim', 'rklequipment:victim', hitCoords, range, enemyNetId, damage)
No exports — just ensure it

Because it's event-driven, the folder name doesn't affect its API; just keep the resource started so its event handlers are live. It's a dependency for the equipment combat visuals and the spell VFX.

rklinterface HUD · class build · skill bar

The player HUD: health, mana and stamina bars, the class/subclass build (max HP, MP, sprint speed), the equipped-skill bar, and a drag-to-move edit mode. Class comes from the playerClass state bag, and skills are read from the skill-tree tables.

Export

isSpellActive()Client

Returns whether the local player is currently casting. Use it to block actions that shouldn't interrupt a cast.

ReturnsTypeNotes
castingbooleantrue while a cast is running
client
if exports['rklinterface']:isSpellActive() then return end

Callbacks

CallbackReturnsDoes
rklinterface:getJobDatatable | nilClass XP and level (falls back to the playerClass state bag)
rklinterface:getUnlockedSkillstableUnlocked active skills for the player's class
rklinterface:getUnlockedPassivestableUnlocked passive skills and their rank stats
client
local jobData = lib.callback.await('rklinterface:getJobData', false) print(jobData.level, jobData.currentClass)

Events

EventArgsDoes
rklinterface:updatexpcurrentXP, nextXPNeededUpdate the XP bar
rklinterface:updatelvlcurrentLevelUpdate the level display
rklinterface:updatelvlxplevel, xp, nextXPUpdate level and XP together
rklinterface:refreshskills—Recalculate the character build
rklinterface:forceRefreshHUD—Redraw the HUD icons
rklinterface:updatespell—Refresh the spell/skill bar
rklinterface:resetstat—Clear the cached stat icons
rklinterface:equipSkillslot, skillIdServer — equip a skill into a hotbar slot
rklinterface:gethpback—Server — restore saved HP / mana on spawn
server → client
TriggerClientEvent('rklinterface:updatelvlxp', src, 12, 340, 500)

Commands & config

CommandDoes
/hideuiToggle the HUD on / off
/interfaceOpen edit mode to drag HUD elements

Class stats live in Config.BaseStatus (base health, HP per level, and per-class modifiers for mage, priest, ranger, and more) and Config.Abilities. Database tables: slv_player_job_points, slv_player_skills, slv_skills, slv_skill_trees.

Hunger & thirst are framework-aware

On ESX the bars are driven by esx_status. On QBCore and Qbox those events never fire, so the HUD reads the same values from player metadata instead — via hud:client:UpdateNeeds, on player load, and with a slow poll as a catch-all. Starvation damage is left to the framework so it isn't applied twice.

Class comes from a state bag

The HUD reads LocalPlayer.state.playerClass (and playerSubclass). Whatever sets the class — a class-select resource, for example — should set that state bag so the HUD picks it up.

rklskilltree class XP · skill nodes

Class progression and the skill tree itself: XP and levels per class, skill points, node unlocking with rank-ups, sub-classes, and an in-game tree editor for admins. It feeds the HUD (rklinterface) and gates gear in rklequipment.

Server exports

AddJobXP(source, amount)Server

Grants class XP, handling level-ups and the HUD refresh automatically. Use it to reward quests, kills or events.

ParameterTypeNotes
sourcenumberplayer server id
amountnumberXP to add
server
exports['rklskilltree']:AddJobXP(src, 250)
GetJobLevel(source)Server

The player's current level in their active class. This is what rklequipment uses to gate gear by required_lvl.

ReturnsTypeNotes
levelnumber1 if there is no record yet
server
local lvl = exports['rklskilltree']:GetJobLevel(src) or 0 if lvl < item.required_lvl then return end
GetJobStats(source)Server

Full progression snapshot for the active class — level, XP, unspent skill points, and the XP needed for the next level.

ReturnsTypeNotes
levelnumbercurrent class level
xpnumbercurrent XP
pointsnumberunspent skill points
nextXpnumberXP required for the next level
server
local s = exports['rklskilltree']:GetJobStats(src) print(s.level, s.xp .. '/' .. s.nextXp, s.points)
AddJobPoints(source, amount)Server

Grants skill points directly, without XP. Handy for rewards that should let a player unlock a node straight away.

ParameterTypeNotes
sourcenumberplayer server id
amountnumberpoints to add
ReturnsTypeNotes
okbooleanfalse if the player isn't found
server
exports['rklskilltree']:AddJobPoints(src, 3)

Callbacks & events

NameKindDoes
slv_skilltree:getJobTreecallbackFetch the tree for the player's class, with unlock state
slv_skilltree:purchaseSkillcallbackSpend points to unlock or rank up a node (server-validated)
slv_skilltree:assignSkillSlotcallbackPut a skill into a HUD hotbar slot
slv_skilltree:saveAdminTreecallbackSave the tree from the admin editor
slv_skilltree:resetSystemcallbackReset a player's unlocked skills
slv_skilltree:server:addXPeventAdd XP to a target player
slv_skilltree:server:setBaseClasseventSet the player's base class
slv_skilltree:server:addSubClass / removeSubClasseventManage sub-classes

Commands

CommandAccessDoes
/skilltreeplayerOpen the skill tree UI
/setclassadminSet a player's base class
/addsubclass · /removesubclassadminManage sub-classes
/givejobxpadminGrant class XP (logged to slv_admin_xp_logs)
/giveawakeningstoneadminGive an awakening stone
/copytreeadminCopy a tree between classes

Config & data

Config.XPTable sets the XP curve per level; Config.RankLimits caps level by character rank (E, D, C…). Tables: slv_player_job_points, slv_skill_trees, slv_skills, slv_player_skills, plus the class columns on users / players.

Character table is auto-detected

Class, sub-classes and rank live on the character row — users.identifier on ESX Legacy, players.citizenid on QBCore/Qbox. The bridge resolves which to use at runtime, so the same code runs on all three. See Database & SQL for the columns to add.

rklbinder card album

A full-screen card binder. Players drag forge_card items out of their inventory into album pages, and drag them back out again. It reuses the forge's card item, so there's no extra item to register. Custom NUI, no ox_lib.

Exports

GetBinderCount(src)Server

How many cards the player currently has stored in their binder.

ReturnsTypeNotes
countnumber0 if the player isn't found
server
local n = exports['rklbinder']:GetBinderCount(src)
GetBinder(src)Server

The full binder contents as a raw table, keyed by slot index. Useful for collection rewards or "complete the album" checks.

ReturnsTypeNotes
bindertableempty table if nothing stored
server
local binder = exports['rklbinder']:GetBinder(src) for slot, card in pairs(binder) do print(slot, card.cardType, card.level) end

Events

EventArgsDoes
rklbinder:requestData—Open the binder (fired by the client on /binder)
rklbinder:storeinvSlot, binderIndexBag → binder
rklbinder:retrievebinderIndexBinder → bag
rklbinder:movefromIndex, toIndexMove or swap inside the binder
rklbinder:reload—Request fresh data

All of them are server-validated — slot range, card presence and free bag space are checked before anything is written.

Config

KeyPurpose
Config.Command / Config.OpenKeyOpen command (default /binder) and optional keybind
Config.CardItemItem used as the card — default forge_card, same as the forge
Config.LayoutBinder size: pages (even), cols, rows
Config.Rarity / Config.CardTypesFrame colour by level, and card visuals + stat info
Config.BinderTableDatabase table name
Shares the forge's card item

Cards come from rklforge — /giveforgecard or exports['rklforge']:GiveForgeCard(src, 'critical', 3). The binder is otherwise standalone: nothing else calls it, and it works without the forge if you hand out forge_card items another way.

Table name stays sl_binder

The default Config.BinderTable is still sl_binder on purpose — it holds live player data, and renaming it would orphan every existing binder. On a fresh install you can rename the table in the database and update the setting to match; do both, or neither.

rklrepair durability NPC

Repair NPCs that restore durability on equipment items. Each NPC accepts certain categories and only lists gear below its damage threshold. No database, no exports — it works directly on ox_inventory metadata.

How it meets rklequipment

Both resources use the same ox_inventory item, equipment, and the same durability fields. Repairing writes metadata.stats.durability (the equipment system's source of truth) and mirrors it to metadata.durability so the ox_inventory bar agrees.

Unequip before repairing

Equipping moves an item out of ox_inventory and into the player_equipment table, so equipped gear isn't in the bag at all. The repair list therefore only shows unequipped items: unequip → repair → equip again. The restored durability carries over on re-equip, so no refresh event is needed.

Callbacks

NameSideDoes
rklrepair:getAwakenCoinsserverCurrent balance, for the UI
rklrepair:processRepairserverRecheck cost, take payment, set durability to 100

Both are server-validated — the item is re-read server-side and the cost recomputed from config, so a tampered UI can't repair for free.

Config

KeyPurpose
Config.NPCsLocations, models, accepted categories, and minDurabilityToRepair
Config.WeaponListPer-weapon repair cost (materials + currency)
Config.DefaultRepairFallback cost for anything unlisted
Config.RKLCurrencyWhat the cost is paid with — item, money, bank or account

rklhalloween seasonal event

Hallowed Night is a free fantasy Halloween event pack in one resource: a soul-lantern hunt, trick-or-treat doors with curses, a witch's cauldron with ingredient gathering, and a scheduled Haunted Night boss event. Everything uses a custom Halloween UI (prompt, progress ring, toasts, Grimoire menu, cauldron, boss bar). It's standalone: no other RKL resource is needed.

FeatureWhat players doConfig
Soul Lantern HuntFind glowing lanterns around the map, press E to collect Souls + loot. Some lanterns are cursed: more Souls, plus a random curse.config/lanterns.lua
Trick-or-TreatKnock on doors. A costumed NPC answers with a Treat (candy, ingredients, cash) or a Trick (curse).config/trickortreat.lua
Curses8 fantasy curses with an on-screen timer. Lifted by death (optional) or the Purifying Elixir.config/curses.lua
Witch's CauldronGather Mandrake, Moonpetal and Bone Dust, then brew 7 potions with a stirring skill-check.config/cauldron.lua · gathering.lua · potions.lua
Haunted NightFog and blood moon over a zone, 3 waves of spirits, then The Hollow King boss with Soul Nova and summons.config/haunted.lua
Grimoire/halloween (F7): profile, leaderboard, milestone rewards, recipe book, curse list, built-in satchel.config/config.lua
Runs on every framework

Auto-detects Qbox, QBCore, ESX Legacy or standalone, plus ox_inventory, qb-inventory (and the ps/lj forks), the ESX default inventory, or a built-in Halloween Satchel when there's no inventory at all. Interaction works with the custom prompt, ox_target or qb-target. Progress is stored in server KVP, so no SQL is needed.

Quick start

1) Drop the rklhalloween folder into your resources. 2) Add ensure rklhalloween to server.cfg after your framework and inventory. 3) Paste the items from install/items into your inventory and copy the images from install/images. 4) Restart the server. Players open the menu with /halloween or F7; admins start the event with /hnhaunt start. The full walkthrough is in Quick start.

Hallowed Night — Quick start

From download to your first in-game test in about five minutes. This is the short path: Install has the full steps for every framework and inventory, and Troubleshooting covers anything that doesn't show up.

Before you start

  • OneSync must be enabled. It is the only hard requirement (dependencies { '/onesync' } in fxmanifest.lua).
  • No database, no ox_lib and no oxmysql. Souls, stats, milestones and the built-in satchel are saved in server KVP.
  • A framework, an inventory and a target script are all optional. The pack detects your framework and inventory when it starts. Target scripts are only used if you change Config.Interaction (the default is the pack's own 'prompt').

1. Drop the folder in

Unzip the download (from your keymaster Granted Assets if you claimed it on Tebex) and put the folder in your server's resources directory. The folder must be named exactly rklhalloween. If it extracted as something like rklhalloween-1.0.0, rename it.

2. Add it to server.cfg

Start it after your framework, inventory and target script, then give your admins the hallowed.admin ace:

server.cfg
# framework (pick yours, or nothing for standalone) ensure qbx_core # or qb-core / es_extended # inventory (or nothing: the built-in Halloween Satchel is used) ensure ox_inventory # or qb-inventory / ps-inventory / lj-inventory # target (optional, only used if you change Config.Interaction) ensure ox_target # or qb-target # Hallowed Night: always after the lines above ensure rklhalloween # admin commands: /hnhaunt, /hnsouls, /hnlanterns add_ace group.admin hallowed.admin allow

3. Register the items and copy the images

Your setupItemsImages (install/images/*.png)
Qbox / QBCore / ESX + ox_inventoryPaste install/items/ox_inventory.lua into ox_inventory/data/items.luaox_inventory/web/images/
QBCore + qb-inventoryPaste install/items/qb-core.lua into qb-core/shared/items.luaqb-inventory/html/images/ (ps/lj: that inventory's html/images/)
ESX default inventoryRun install/items/esx.sql on your databaseOnly if your inventory UI shows item images
StandaloneNothing. Items go to the built-in satchelNothing. The pack's UI ships its own icons

4. Turn the season on (only if you test outside the dates)

By default the event runs from 15 October to 3 November on the server's local date. Inside those dates you can skip this step. Outside them, lanterns, trick-or-treat doors, cauldrons and gathering nodes don't appear, and the season chip in the Grimoire reads "The veil is closed". For testing, set Config.Season.AlwaysOn = true in config/config.lua:

config/config.lua
Config.Season = { AlwaysOn = true, -- TESTING: ignore the dates below Start = { month = 10, day = 15 }, -- server local date Finish = { month = 11, day = 3 }, Id = '2026', -- change every year to start a fresh leaderboard/progress }

Two optional testing helpers:

  • Config.Lanterns.Blips.Enabled = true (config/lanterns.lua) puts a small blip on every active lantern so you can find one quickly.
  • Config.Debug = true (config/config.lua) prints every rewarded action in the server console (through Hooks.Log) and draws the trick-or-treat door zones when you use ox_target or qb-target.

5. Restart the server

Do a full server restart the first time, so your inventory picks up the new items and the custom props in stream/ (with hn_props.ytyp) load. When the resource starts, the console prints the setup it detected:

server console
[rklhalloween] framework: qbx | inventory: ox_inventory

Your values will match your server (qbx, qb, esx or standalone, and ox_inventory, qb-inventory, esx or internal). If it says standalone or internal while you run a framework or inventory, rklhalloween started too early: move its ensure line further down. After later config edits, restart rklhalloween is enough, because the config files load when the resource starts.

6. Test in game

Command / actionWhat it testsWhat you should get
/halloween or F7Grimoire and UIThe Grimoire opens on Profile. The season chip reads "The veil is thin". Tabs: Profile, Leaderboard, Milestones, Recipes, Curses (plus Satchel on the built-in satchel)
/hnlanterns respawnAdmin ace, lanternsToast "All lanterns respawned." The global lanterns are re-rolled: Config.Lanterns.ActiveCount (20) of the 32 locations
/hnsouls [your id] 25Souls, milestones, item + money rewardsToast "Changed <name> souls by 25 (now 25)." Then Grimoire → Milestones → claim Wandering Spirit: 5 Hallowed Candy + 500 cash (on standalone the cash is only paid if you connected Hooks.StandaloneAddMoney)
Use a Hallowed CandyUsable itemsEating animation, "Sugar rush!", +5 health and a 10-second sprint boost. On standalone, use it from Grimoire → Satchel
/hnhaunt start 1Haunted NightBanner "The Veil Thins" / "Spirits gather at Pacific Bluffs Cemetery in 2m 00s", a flashing blip and radius on the map, and a toast for you: "Haunted Night started at Pacific Bluffs Cemetery."
/hnhaunt stopCleanup"Haunted Night stopped." Every spirit is deleted
/hncoordsConfig helper"Coordinates copied to clipboard." Your position is copied as vec4(x, y, z, heading) and printed in F8
Best test spot: Pacific Bluffs Cemetery

With the default config, the cemetery has Witch's Cauldron #1 with its witch NPC (vec4(-1745.00, -205.00, 57.00, 140.0)), two Mandrake nodes, two Bone Piles, and Haunted Night zone 1, all within a short walk. After /hnhaunt start 1, stand inside the zone when the 120-second announce (Config.Haunted.AnnounceSeconds) ends: waves only spawn while a player is in or near the zone (within its radius plus Config.Haunted.ParticipantRadius, 30 m), and the TimeLimit clock (15 minutes) starts as soon as the event goes live.

7. Before you go live

  • Set Config.Season.AlwaysOn back to false (unless you want the event all year), and turn Config.Lanterns.Blips.Enabled and Config.Debug off again.
  • Take back test Souls with a negative amount: /hnsouls [id] -25. Souls never go below 0. Milestones you already claimed stay claimed for that character (that license on standalone) until Config.Season.Id changes.
  • The included trick-or-treat doors are samples. Walk to the doors you want and add them with /hncoords to Config.TrickOrTreat.Doors.
  • For a completely fresh start (everyone's Souls, stats, milestones and satchels), change Config.Season.Id.

What you should see

  • The [rklhalloween] framework: … | inventory: … line in the server console, and no red [rklhalloween] errors.
  • Three Witch's Cauldron blips on the map: Pacific Bluffs cemetery, Grapeseed and Paleto Bay.
  • At a cauldron: the custom cauldron with a green glow, a witch NPC next to it, and the prompt "Use Witch's Cauldron" (or a target option with a flask icon).
  • Soul lanterns that spawn once you are within 80 m (Config.Lanterns.RenderDistance) and glow orange (cursed ones purple, with an aura). "Collect Soul Lantern" → hold for 2.5 seconds (CollectTime = 2500) → a reward popup with +1 Soul (+3 for a cursed lantern, followed by a curse).
  • Gathering nodes with "Gather Mandrake" / "Gather Bone Pile". Moonpetal only shows between 20:00 and 06:00 in-game time.
  • Trick-or-treat doors (for example on Grove Street or Forum Drive) with a "Trick or Treat" prompt, and a costumed NPC answering after the knock.
  • Custom models, not plain pumpkins, barrels or bin bags. If you see those, the stream didn't load and the vanilla fallbacks are showing (see Custom props).
  • Rewards landing in your inventory with their images, or in the Grimoire's Satchel tab on standalone.
  • During Haunted Night: the announce banner for every player, then inside the zone fog, the HALLOWEEN weather, decorative tombstones and the wave counter.
Something didn't show up?

"The veil is closed" means the season is off (step 4). /hnlanterns respawn stays silent without permission, while /hnhaunt answers "You do not have permission.", so check the add_ace line and that your account is in that group. /halloween answering "You can't do that right now." means the pack couldn't get an identifier for you yet (no character loaded). Potions that do nothing on ox_inventory almost always mean a missing server = { export = 'rklhalloween.useItem' } or a renamed folder.

Hallowed Night — Install

The full setup for every supported framework and inventory. Pick your setup below and follow its steps, then choose how players interact (prompt or target). The pack detects your framework and inventory by itself, so in most setups you only paste the items and copy the images. A target script is only used if you set Config.Interaction.

Requirements

RequirementNeeded?Notes
OneSyncRequiredListed in dependencies in fxmanifest.lua. Haunted Night spirits and the boss are created by the server
FrameworkOptionalQbox (qbx_core), QBCore (qb-core), ESX Legacy (es_extended), or none (standalone)
InventoryOptionalox_inventory, qb-inventory (and the ps/lj forks), the ESX default inventory, or none: the built-in Halloween Satchel
TargetOptionalox_target or qb-target. The default is the pack's own Halloween prompt
DatabaseNot neededProgress is stored in server KVP: no SQL tables (the only SQL file registers items for the ESX default inventory)
ox_lib / oxmysqlNot neededThe pack doesn't use them

What's in the folder

PathContentsAfter escrow
config/All settings, one file per moduleOpen
locales/en.lua, tl.luaOpen
bridge/server.lua (framework, money, inventory, usable items) and client.lua (death check, player loaded, target)Open
editable/Server and client hooksOpen
install/items/ox_inventory.lua, qb-core.lua, esx.sqlOpen
install/images/16 item PNGs for your inventoryOpen
install/STREAM.mdProp collision and LOD notes, how to re-export a propOpen
stream/9 custom props (hn_*.ydr) and hn_props.ytypProtected
html/The Halloween UI, with its own item icons in html/assets/items/Open (not encrypted)
client/ server/ shared/Gameplay codeEncrypted

How the pack detects your setup

Config.Framework and Config.Inventory both default to 'auto' in config/config.lua. Config.Interaction is not auto-detected unless you set it to 'auto': by default it is 'prompt' (see Interaction below).

config/config.lua
-- 'auto' detects the running resource. Force one if you run more than one by accident. Config.Framework = 'auto' -- 'auto' | 'qbx' | 'qb' | 'esx' | 'standalone' Config.Inventory = 'auto' -- 'auto' | 'ox_inventory' | 'qb-inventory' | 'esx' | 'internal' -- 'internal' = built-in Halloween Satchel (no inventory needed, shown in the Grimoire) Config.Interaction = 'prompt' -- 'prompt' (custom Halloween prompt + key) | 'ox_target' | 'qb-target' | 'auto'
SettingAuto-detection order (first match wins)
Config.Frameworkqbx_core running → qbx · qb-core → qb · es_extended → esx · otherwise standalone
Config.Inventoryox_inventory running → ox_inventory (on any framework, standalone included) · Qbox/QBCore → qb-inventory · ESX → esx · otherwise internal (built-in satchel)

A resource counts as running when its state is started or starting. The check happens once, when rklhalloween starts, and the result is printed in the server console:

server console
[rklhalloween] framework: esx | inventory: ox_inventory
Start order matters

Because detection runs when the resource starts, your framework, inventory and target script must be ensured before rklhalloween. Started too early, the pack falls back to standalone / internal (items go to the built-in satchel), and with Config.Interaction = 'auto' to the prompt. If the console line shows the wrong values, fix the order or force the value in Config.Framework / Config.Inventory.

Custom framework?

Everything the pack needs from your server (identifier, character name, money, item count/add/remove, carry check, usable items) goes through bridge/server.lua, and the client side (dead/downed check, player loaded, ox_target/qb-target) through bridge/client.lua. Both stay open after escrow, so a custom framework only needs those two files edited.

server.cfg

server.cfg
# OneSync is required (skip this line if your host or txAdmin already turns it on) set onesync on # 1. framework (one of these, or none for standalone) ensure qbx_core # Qbox # ensure qb-core # QBCore # ensure es_extended # ESX Legacy # 2. inventory (one of these, or none: the built-in satchel is used) ensure ox_inventory # ensure qb-inventory # or ps-inventory / lj-inventory # 3. target (optional, only if Config.Interaction uses it) ensure ox_target # ensure qb-target # 4. Hallowed Night, after everything above ensure rklhalloween # 5. admin permission for /hnhaunt, /hnsouls and /hnlanterns add_ace group.admin hallowed.admin allow # if your account isn't in group.admin yet: # add_principal identifier.license:YOUR_LICENSE group.admin

The ace name comes from Config.Admin.Ace (default 'hallowed.admin'). If you change it, change the add_ace line too. Admin commands also work from the server console. /halloween and /hncoords are open to everyone.

Keep the folder name rklhalloween

Three things depend on the exact name: the ox_inventory items call the export rklhalloween.useItem when a potion or candy is used, other scripts reach the pack through exports['rklhalloween'], and saved progress lives in the resource's KVP store, which FiveM keeps per resource name. A renamed folder breaks potion use on ox_inventory and starts everyone from zero. (The UI itself follows the folder name automatically.)

Choose your setup

SetupDetected asItems fileImages toUsable items via
Qboxqbx + ox_inventoryinstall/items/ox_inventory.luaox_inventory/web/images/ox export rklhalloween.useItem
QBCore + qb-inventoryqb + qb-inventoryinstall/items/qb-core.luaqb-inventory/html/images/QBCore.Functions.CreateUseableItem
QBCore + ox_inventoryqb + ox_inventoryinstall/items/ox_inventory.luaox_inventory/web/images/ox export rklhalloween.useItem
ESX default inventoryesx + esxinstall/items/esx.sqlOnly if your inventory UI shows imagesESX.RegisterUsableItem
ESX + ox_inventoryesx + ox_inventoryinstall/items/ox_inventory.luaox_inventory/web/images/ox export rklhalloween.useItem
Standalonestandalone + internalNoneNoneGrimoire → Satchel → Use

Qbox (ox_inventory)

  1. Open install/items/ox_inventory.lua and copy the 16 item lines (everything below the two comment lines).
  2. Paste them inside the return { ... } table in ox_inventory/data/items.lua, before its closing brace.
  3. Copy every PNG from install/images/ into ox_inventory/web/images/.
  4. In server.cfg: ensure qbx_core, ensure ox_inventory, optionally ensure ox_target, then ensure rklhalloween.
  5. Restart the server and check for framework: qbx | inventory: ox_inventory in the console.
ox_inventory/data/items.lua
return { -- ... your existing items ... ["hn_mandrake_root"] = { label = "Mandrake Root", weight = 50, stack = true, close = false, description = "A gnarled root that screams when pulled. Hums with earthy magic." }, ["hn_potion_seer"] = { label = "Seer's Draught", weight = 150, stack = true, close = true, server = { export = 'rklhalloween.useItem' }, description = "Reveals nearby soul lanterns on your map." }, -- ... plus the other 14 lines from install/items/ox_inventory.lua }

Usable items: the 9 usable items (Hallowed Candy, the 7 potions and Cauldron Sludge) carry server = { export = 'rklhalloween.useItem' }. When a player uses one, ox_inventory asks the pack first (it only accepts items listed in Config.Potions), removes one item itself, and the pack then plays the animation and runs the effect without removing a second one. Nothing is registered through qbx_core. Progress is saved per character (citizenid), and money rewards go to the player's cash or bank.

QBCore + qb-inventory (ps-inventory, lj-inventory)

  1. Open install/items/qb-core.lua and copy the 16 item lines.
  2. Paste them inside the QBShared.Items = { ... } table in qb-core/shared/items.lua, before its closing brace.
  3. Copy every PNG from install/images/ into qb-inventory/html/images/. On a fork, use that inventory's html/images/ folder (ps-inventory/html/images/, lj-inventory/html/images/).
  4. In server.cfg: ensure qb-core, your inventory, optionally ensure qb-target, then ensure rklhalloween.
  5. Restart the server and check for framework: qb | inventory: qb-inventory.
qb-core/shared/items.lua
QBShared.Items = { -- ... your existing items ... hn_mandrake_root = { name = 'hn_mandrake_root', label = "Mandrake Root", weight = 50, type = 'item', image = 'hn_mandrake_root.png', unique = false, useable = false, shouldClose = true, description = "A gnarled root that screams when pulled. Hums with earthy magic." }, hn_potion_seer = { name = 'hn_potion_seer', label = "Seer's Draught", weight = 150, type = 'item', image = 'hn_potion_seer.png', unique = false, useable = true, shouldClose = true, description = "Reveals nearby soul lanterns on your map." }, -- ... plus the other 14 lines from install/items/qb-core.lua }

Usable items: the pack registers every item in Config.Potions with QBCore.Functions.CreateUseableItem (those items have useable = true in the install file). On use, the pack removes one item itself, then runs the effect.

Forks: ps-inventory and lj-inventory are handled as qb-inventory, because the pack works through the QBCore player functions (AddItem, RemoveItem, GetItemsByName) rather than the inventory resource. The carry check uses exports['qb-inventory']:CanAddItem on qb-inventory 2.x and falls back to a weight check against QBCore.Config.Player.MaxWeight on older versions and forks. The item box popup is sent with both qb-inventory:client:ItemBox and inventory:client:ItemBox, so forks show it too. Removing items walks split stacks and is all-or-nothing.

QBCore + ox_inventory

  1. Paste the 16 lines of install/items/ox_inventory.lua into ox_inventory/data/items.lua (same as Qbox above).
  2. Copy install/images/*.png into ox_inventory/web/images/.
  3. In server.cfg: ensure qb-core, ensure ox_inventory, optionally a target script, then ensure rklhalloween.
  4. Restart and check for framework: qb | inventory: ox_inventory.

ox_inventory wins the detection whenever it is running, so the pack adds, removes and counts items only through ox_inventory here. install/items/qb-core.lua isn't needed. Usable items work through the ox export exactly like on Qbox, and the pack skips CreateUseableItem. Progress is saved per character (citizenid).

ESX Legacy (default inventory)

  1. Run install/items/esx.sql on your ESX database (HeidiSQL, phpMyAdmin or similar). It uses INSERT IGNORE, so running it twice is safe.
  2. Images: if your inventory UI shows item images, copy install/images/*.png into its image folder. The pack's own UI already has its icons.
  3. In server.cfg: ensure es_extended, then ensure rklhalloween.
  4. Restart the server, so ESX loads the new items, and check for framework: esx | inventory: esx.
install/items/esx.sql
-- ESX Legacy (default inventory). Skip this if you use ox_inventory. INSERT IGNORE INTO `items` (`name`, `label`, `weight`, `rare`, `can_remove`) VALUES ('hn_mandrake_root', 'Mandrake Root', 1, 0, 1), ('hn_moonpetal', 'Moonpetal', 1, 0, 1), ('hn_bone_dust', 'Bone Dust', 1, 0, 1), ('hn_ghost_essence', 'Ghost Essence', 1, 0, 1), ('hn_pumpkin_seeds', 'Pumpkin Seeds', 1, 0, 1), ('hn_raven_feather', 'Raven Feather', 1, 0, 1), ('hn_candy', 'Hallowed Candy', 1, 0, 1), ('hn_candy_bucket', 'Candy Bucket', 6, 0, 1), ('hn_potion_owl', 'Owl''s Eye Elixir', 3, 0, 1), ('hn_potion_swift', 'Swiftfoot Draught', 3, 0, 1), ('hn_potion_feather', 'Featherfall Tonic', 3, 0, 1), ('hn_potion_phantom', 'Phantom Veil', 3, 0, 1), ('hn_potion_vigor', 'Elixir of Vigor', 3, 0, 1), ('hn_potion_purify', 'Purifying Elixir', 3, 0, 1), ('hn_potion_seer', 'Seer''s Draught', 3, 0, 1), ('hn_sludge', 'Cauldron Sludge', 3, 0, 1);

Usable items: registered with ESX.RegisterUsableItem for every item in Config.Potions. On use, the pack removes one item with removeInventoryItem and runs the effect. Rewards respect xPlayer.canCarryItem. Cash rewards go to the ESX money account and bank rewards to bank. Progress is saved per character (xPlayer.identifier) and written when a character logs out (esx:playerLogout), so multicharacter works. The client reads death from esx:onPlayerDeath / esx:onPlayerSpawn.

ESX Legacy + ox_inventory

  1. Paste the 16 lines of install/items/ox_inventory.lua into ox_inventory/data/items.lua.
  2. Copy install/images/*.png into ox_inventory/web/images/.
  3. Skip install/items/esx.sql (its own header says so).
  4. In server.cfg: ensure es_extended, ensure ox_inventory, optionally ensure ox_target, then ensure rklhalloween.
  5. Restart and check for framework: esx | inventory: ox_inventory.

Items go through ox_inventory and usable items through the rklhalloween.useItem export, so ESX.RegisterUsableItem isn't used. Identifier, money and death detection still come from ESX, as in the section above.

Standalone (built-in satchel)

  1. Nothing to paste and no images to copy. The pack's UI already ships its item icons in html/assets/items/.
  2. Add ensure rklhalloween to server.cfg (plus a target script if you want one).
  3. Restart and check for framework: standalone | inventory: internal.

The satchel: every item reward goes into a built-in Halloween Satchel saved with the player's progress in KVP. Players open it from the Grimoire (/halloween or F7) → Satchel. The tab only appears when the inventory resolves to internal. Usable items (everything in Config.Potions) get a Use button that removes one and runs the effect. The satchel has no weight limit.

Identifier: progress is saved per Rockstar license (license: identifier), so it's per account, not per character. The client detects death with the native dead / fatally-injured checks only, and the "player loaded" moment is the playerSpawned event.

Money: standalone has no money system, so cash and bank rewards (lantern loot, treats, milestones, the boss) go through Hooks.StandaloneAddMoney in editable/server/hooks.lua. By default it returns false and the money is simply not given. Connect your own economy there and return true only when you actually paid:

editable/server/hooks.lua (example comments added)
---Standalone servers have no money system. Return true if you gave the money yourself. function Hooks.StandaloneAddMoney(src, account, amount) -- account is 'cash' or 'bank' -- example: call your own economy script here, then -- return true return false end
The satchel resets with the season

Satchel contents are saved with the rest of the season's progress, under Config.Season.Id. Changing the id for a new season starts every player with an empty satchel, as well as fresh Souls, stats and milestones.

Want the satchel on a framework server?

Set Config.Inventory = 'internal' to keep event items out of your real inventory. Items then go to the satchel and are used from the Grimoire, while identifier and money still come from your framework.

How usable items work

The 9 usable items are the keys of Config.Potions in config/potions.lua. Here is how each setup connects them:

InventoryRegistered withWho removes the item
ox_inventoryserver = { export = 'rklhalloween.useItem' } in each itemox_inventory (the pack then runs the effect only)
qb-inventory (QBCore)QBCore.Functions.CreateUseableItemThe pack
qb-inventory (Qbox)exports.qbx_core:CreateUseableItemThe pack
ESX defaultESX.RegisterUsableItemThe pack
internalGrimoire → Satchel → UseThe pack
bridge/server.lua
---Register an item as usable. `cb(src)` runs when the player uses it. ---ox_inventory calls exports.rklhalloween:useItem instead (see install/items/ox_inventory.lua). function Bridge.RegisterUsable(item, cb) local inv = Bridge.Inventory if inv == 'ox_inventory' or inv == 'internal' then return end if Bridge.Framework == 'qb' then QBCore.Functions.CreateUseableItem(item, function(source) cb(source) end) elseif Bridge.Framework == 'qbx' then exports.qbx_core:CreateUseableItem(item, function(source) cb(source) end) elseif Bridge.Framework == 'esx' then ESX.RegisterUsableItem(item, function(source) cb(source) end) end end

Interaction: prompt or target

config/config.lua
Config.Interaction = 'prompt' -- 'prompt' (custom Halloween prompt + key) | 'ox_target' | 'qb-target' | 'auto' Config.InteractKey = { control = 38, label = 'E' } -- used by the prompt mode Config.CancelKey = { control = 73, label = 'X' } -- cancels hold actions
ValueWhat players get
'prompt'Default. The custom Halloween prompt appears for the nearest point in range, and players press Config.InteractKey (E). No target script needed, and a running ox_target or qb-target is ignored
'ox_target'Options on the props through addLocalEntity, and sphere zones (addSphereZone) for the doors
'qb-target'Options through AddTargetEntity and AddCircleZone. When the resource stops, the pack removes its own options (in both target modes), since qb-target keeps options of stopped resources
'auto'ox_target if it's running, otherwise qb-target, otherwise the prompt
Interaction pointTarget typeIconRange
Soul lanternsEntityfa-solid fa-ghostConfig.Lanterns.InteractDistance (2.0)
Gathering nodesEntityfa-solid fa-seedlingConfig.Gathering.InteractDistance (1.8)
Witch's cauldronsEntityfa-solid fa-flaskConfig.Cauldron.InteractDistance (2.2)
Trick-or-treat doorsSphere zone, radius 1.2fa-solid fa-candy-caneConfig.TrickOrTreat.InteractDistance (1.8)

In every mode, ClientHooks.CanInteract() in editable/client/hooks.lua decides whether an option is shown. By default it blocks players who are dead or downed (when Config.DisableWhileDead is on) and players in a vehicle. Add your own checks there (cuffed, in jail, on a call). Hold actions such as collecting or gathering show the progress ring and can be cancelled with Config.CancelKey (X) in every mode. Target option names start with rklhalloween:.

Start the target script first

'auto' is resolved once, when the client script loads, so ensure your target resource before rklhalloween. A forced 'ox_target' / 'qb-target' also needs that resource running, because the pack calls its exports. With Config.Debug = true, the door zones are drawn (ox_target debug, qb-target debugPoly).

Item list

All 16 items, with the weights from the install files. ox_inventory and qb-core use the same numbers. Every item is stackable (stack = true on ox, unique = false on qb).

ItemLabelWeight ox / qbWeight ESXUsableComes from (default config)
hn_mandrake_rootMandrake Root501NoMandrake gathering nodes
hn_moonpetalMoonpetal501NoMoonpetal nodes (night only), rare treat
hn_bone_dustBone Dust501NoBone Pile nodes, cursed lanterns
hn_ghost_essenceGhost Essence501NoCursed lanterns, Haunted Night spirits and boss, the Specter curse
hn_pumpkin_seedsPumpkin Seeds501NoLanterns, treats
hn_raven_featherRaven Feather501NoLanterns, treats
hn_candyHallowed Candy201YesLanterns, treats, boss, first milestone. +5 health and a 10 s sprint boost
hn_candy_bucketCandy Bucket3006NoHollow Sovereign milestone. Needed to knock only if Config.TrickOrTreat.RequireItem = true
hn_potion_owlOwl's Eye Elixir1503YesCauldron. Night vision, 90 s
hn_potion_swiftSwiftfoot Draught1503YesCauldron, milestone. Sprint ×1.25, 60 s
hn_potion_featherFeatherfall Tonic1503YesCauldron. Slow fall, 120 s
hn_potion_phantomPhantom Veil1503YesCauldron, milestone, boss. Ghost form, 30 s
hn_potion_vigorElixir of Vigor1503YesCauldron. +60 health, +25 armour
hn_potion_purifyPurifying Elixir1503YesCauldron, milestone. Lifts curses, 180 s immunity
hn_potion_seerSeer's Draught1503YesCauldron, milestone. Reveals lanterns within 400 m for 90 s
hn_sludgeCauldron Sludge1503YesFailed brew (Config.Cauldron.Fail.Item). Deals 10 damage

On ox_inventory the usable items have close = true (the inventory closes on use) and the rest close = false. On qb-core every item has shouldClose = true and image = '<item>.png'. On ESX every row has rare = 0 and can_remove = 1. Effects and durations are set in config/potions.lua.

Labels in the Halloween UI

The Grimoire, cauldron and reward popups take labels and descriptions from config/items.lua (Config.Items), not from your inventory. If you rename an item's label in your inventory, change it there too. Icons come from html/assets/items/<item>.png unless you set the optional image field.

Hallowed Night — Config reference

Every setting lives in config/. The files load as shared scripts (server and client) in the order below, and they stay readable and editable after escrow (escrow_ignore 'config/*.lua'). Restart the resource after you change a value.

FileTablesCovers
config.luaConfig.Locale … Config.EffectsLanguage, debug, framework / inventory / interaction mode, keys, season dates, Grimoire command, Souls label, leaderboard, milestones, admin ace and command names, death rules, particle effects
items.luaConfig.ItemsLabel, description and optional picture of every item shown in the Halloween UI
lanterns.luaConfig.LanternsSoul Lantern Hunt: mode, active count, respawn, cursed chance, loot, lights, blips, locations
trickortreat.luaConfig.TrickOrTreatDoors, cooldown, daily limit, trick chance, treats, door NPC
curses.luaConfig.Curses · Config.CurseImmunityAfterCurseThe 8 curses and the immunity window after a curse
cauldron.luaConfig.CauldronCauldron prop, witch NPC, blip, locations, stirring minigame, fail result, recipes
gathering.luaConfig.GatheringMandrake, Moonpetal and Bone Pile nodes
potions.luaConfig.Potions · Config.PotionAnim · Config.CandyAnimWhat every usable item does, plus the drink and eat animations
haunted.luaConfig.HauntedHaunted Night schedule, zones, visuals, spirits, waves, boss, rewards

This page goes key by key through config.lua, items.lua, curses.lua and potions.lua. Lantern, trick-or-treat, gathering, cauldron and Haunted Night settings are explained in Features & balancing, and the full item and recipe lists are in Items, potions & recipes. Text that isn't in a config (prompts, toasts, Grimoire labels) lives in locales/en.lua and locales/tl.lua.

config/config.lua

General

KeyDefaultWhat it does
Config.Locale'en'Language for every text: 'en' or 'tl'. Message keys missing from the chosen locale fall back to English. The ui table (Grimoire and cauldron labels) only falls back as a whole, if the locale has no ui table at all. To add a language, create a file in locales/ with the same layout as en.lua and add it to shared_scripts in fxmanifest.lua.
Config.Debugfalsetrue prints every logged action (Hooks.Log) in the server console and draws the debug outline of the ox_target / qb-target zones (the trick-or-treat doors).
Config.Framework'auto''auto' | 'qbx' | 'qb' | 'esx' | 'standalone'. Auto checks qbx_core, then qb-core, then es_extended, and uses standalone if none is running. The detected framework and inventory are printed in the server console on start.
Config.Inventory'auto''auto' | 'ox_inventory' | 'qb-inventory' | 'esx' | 'internal'. Auto uses ox_inventory when it's started, otherwise qb-inventory on QBCore/Qbox, the default inventory on ESX, and the built-in Halloween Satchel ('internal', shown as a tab in the Grimoire) on standalone.
Config.Interaction'prompt''prompt' = the custom Halloween prompt with a key press. 'ox_target' or 'qb-target' = target eye options. 'auto' picks ox_target, then qb-target, then the prompt.
Config.InteractKey{ control = 38, label = 'E' }Key used in prompt mode. control is the GTA control index, label is the key shown on the prompt.
Config.CancelKey{ control = 73, label = 'X' }Cancels a hold action (collecting, knocking, gathering). A hold is also cancelled if the player dies, ragdolls or gets into a vehicle.

Season

KeyDefaultWhat it does
Config.Season.AlwaysOnfalsetrue ignores the dates and keeps the event open all year. Handy for testing.
Config.Season.Start{ month = 10, day = 15 }First day of the event, using the server's local date.
Config.Season.Finish{ month = 11, day = 3 }Last day of the event (included). If Start comes later in the year than Finish, the season wraps over New Year.
Config.Season.Id'2026'Save slot for player progress. Souls, stats, claimed milestones, personal-mode lanterns, the satchel and the daily knock count are stored in server KVP under hn:<Id>:p:<identifier>. Change it to start a fresh season and leaderboard. Old data stays under the old id.
How the season gate works

The server checks the date every 5 minutes and publishes the result as GlobalState.hnSeason. Outside the season, lanterns, doors, gathering nodes and cauldrons are removed, and rewarded actions answer "The veil is closed". The Grimoire still opens, so players can check the leaderboard and claim milestones they already earned. Scheduled Haunted Nights only start during the season, but /hnhaunt start works at any time.

Grimoire, Souls & leaderboard

KeyDefaultWhat it does
Config.Menu.Command'halloween'Chat command that opens the Grimoire: profile, leaderboard, milestones, recipe book, curse list, and the satchel tab when the internal inventory is used.
Config.Menu.Key'F7'Default key, registered with RegisterKeyMapping as "Hallowed Night - Grimoire", so players can rebind it in Settings → Key Bindings → FiveM. '' = no key.
Config.Souls.Label'Souls'Name of the event currency in the UI and in messages. It only changes the name. The amounts are set per activity (see the next table).
Config.Leaderboard.Size10How many players the Grimoire leaderboard lists. Players outside that list still see their own rank in their profile once they have Souls.
Config.Leaderboard.CacheSeconds60The leaderboard is rebuilt from KVP (a scan of every saved player this season) at most once per this many seconds.

Where Souls come from

KeyDefaultPaid for
Config.Lanterns.Souls.normal1Collecting a soul lantern
Config.Lanterns.Souls.cursed3Collecting a cursed lantern
Config.TrickOrTreat.Souls.treat1A treat at a door
Config.TrickOrTreat.Souls.trick2A trick at a door (paid on top of the curse)
Config.Cauldron.Souls2A successful brew (a failed brew pays nothing)
Config.Gathering.Nodes.<node>.souls0Gathering a node (0 on all three nodes by default)
Config.Curses.specter.reward.souls2Banishing the Haunting Specter
Config.Haunted.Rewards.Kill.souls2Each Haunted Night spirit killed
Config.Haunted.Rewards.Boss.souls25Defeating The Hollow King (every participant still in or near the zone)

Admins can add or remove Souls with /hnsouls, and other scripts with the AddSouls export. A total never goes below 0.

Milestones

Rewards unlocked by a player's total Souls and claimed from the Grimoire. Each entry has a Souls threshold, a label, and a list of rewards. A reward is either an item or money ('cash' or 'bank').

config/config.lua
Config.Milestones = { { souls = 25, label = 'Wandering Spirit', rewards = { { type = 'item', name = 'hn_candy', amount = 5 }, { type = 'money', account = 'cash', amount = 500 }, } }, -- { souls = ..., label = '...', rewards = { ... } }, }
SoulsLabelDefault rewards
25Wandering Spirit5× hn_candy · $500 cash
75Lantern Keeper2× hn_potion_swift · $1,500 cash
150Hex Weaver2× hn_potion_purify · 2× hn_potion_seer
300Veil Walker2× hn_potion_phantom · $5,000 bank
500Hollow Sovereign1× hn_candy_bucket · $15,000 bank
  • A milestone can be claimed once per season (Config.Season.Id), as soon as the player's total reaches souls.
  • Before paying, the server checks that the player can carry every item reward. If an item still fails to fit, the items already given are taken back and the claim is undone, so nothing is lost.
  • Items are given first, then money. On ESX, 'cash' goes into the money account. On standalone, money goes through Hooks.StandaloneAddMoney, which pays nothing until you edit it.
Add new milestones at the end

Claimed milestones are saved by their position in the list (1st, 2nd, 3rd…). During a running season, only add new entries at the end. Reordering or removing entries changes which milestones count as claimed.

Admin

KeyDefaultWhat it does
Config.Admin.Ace'hallowed.admin'Ace permission for the admin commands. Grant it with add_ace group.admin hallowed.admin allow. The server console always counts as admin.
Config.Admin.Commands.haunt'hnhaunt'Admin: /hnhaunt start [zone] or /hnhaunt stop. start without a zone number picks a random zone. Without arguments it shows the usage.
Config.Admin.Commands.souls'hnsouls'Admin: /hnsouls [playerId] [amount]. A negative amount removes Souls. The player must be online.
Config.Admin.Commands.coords'hncoords'Anyone: copies your position as vec4(x, y, z, heading) to the clipboard and prints it in F8.
Config.Admin.Commands.lanterns'hnlanterns'Admin: /hnlanterns respawn clears every lantern and rolls a new set.

Death rules

KeyDefaultWhat it does
Config.ClearCursesOnDeathtrueLifts every active curse when the player dies or is downed (checked every second). "Dead" comes from the bridge: GTA death, QBCore isdead / inlaststand metadata, Qbox isDead / inLastStand state bags (or the QBCore metadata, if a qb-core resource is also started), or the ESX death event.
Config.DisableWhileDeadtrueHides the prompt and target options while the player is dead or downed. It's checked in ClientHooks.CanInteract (editable/client/hooks.lua), which also blocks interaction from inside a vehicle.

Particle effects

Each entry is { dict, name, scale }. Where a use below shows a scale in brackets, that fixed scale replaces scale.

KeyDefault (dict · name · scale)Used for
Config.Effects.collectscr_rcbarry1 · scr_alien_disintegrate · 0.6Lantern collected · gathering node picked (0.4)
Config.Effects.ghostInscr_rcbarry2 · scr_clown_appears · 1.2Cursed lantern collected · Specter appears · trick-or-treat NPC appears (0.6) · Haunted Night spirit appears (0.7)
Config.Effects.ghostOutscr_rcbarry2 · scr_clown_appears · 1.0Specter vanishes · trick-or-treat NPC leaves (0.8) · failed brew
Config.Effects.cursedAurascr_rcbarry1 · scr_alien_charging · 0.35Looping aura on cursed lanterns (only when Config.Lanterns.CursedLooksDifferent = true)
Config.Effects.bossNovascr_rcbarry1 · scr_alien_disintegrate · 3.0The Hollow King's Soul Nova
Config.Effects.brewscr_rcbarry1 · scr_alien_teleport · 0.8Successful brew · drinking the Purifying Elixir

config/items.lua

Config.Items holds what the Halloween UI shows for each item: the Grimoire, cauldron, satchel, reward popups and toasts. Items without an entry show their raw name. It's display only. Your inventory still needs the items from install/items/ and the pictures from install/images/.

config/items.lua
Config.Items = { hn_moonpetal = { label = 'Moonpetal', description = 'A pale flower that only blooms under moonlight.' }, -- optional: your own picture instead of html/assets/items/hn_candy.png hn_candy = { label = 'Hallowed Candy', description = 'Sweet, sticky and slightly enchanted. A small sugar rush.', image = 'assets/items/my_candy.png', }, }
FieldDefaultWhat it does
labelper itemName shown everywhere in the Halloween UI and in messages.
descriptionper itemFlavour text on the potion cards in the Grimoire recipe book and the cauldron.
imagenot setOptional picture. Without it the UI uses html/assets/items/<item>.png. The value is used as the image source, so a path relative to html/ or a full URL both work. If the picture fails to load, a potion icon is shown instead.
Custom pictures must be PNG

fxmanifest.lua only ships html/assets/items/*.png, so put your own pictures in that folder as .png files, or the client won't download them.

config/curses.lua

Curses come from tricks (1.8 seconds after the knock), cursed lanterns (1.2 seconds after collecting) and the ApplyCurse export. Every curse shares these keys:

KeyDefaultWhat it does
enabledtrue (all)false removes the curse from the random pick and from the Grimoire's curse list.
weightper curseChance relative to the other curses that can land right now (weighted pick).
durationper curseSeconds the curse lasts. The server also blocks new curses for duration + Config.CurseImmunityAfterCurse.
vehiclefalse (only pumpkin_head is true)Whether the curse can be picked while the player is in a vehicle. The server checks this.
labelper curseName in the curse toast, the on-screen timer and the Grimoire.
descriptionper curseLine in the Grimoire's Known Curses list.
iconper curseUI icon name. The built-in set: pumpkin lantern cursed ghost skull eye feather spiral bone imp moon shield potion candy souls door cauldron leaf crown star bag coin lock check close. Unknown names show the souls icon.

Default values per curse

CurseLabelweightdurationvehicleiconChance on foot
levitateHex of the Floating Dead1012falsefeather11.1%
haunted_sightHaunted Sight1225falseeye13.3%
drunken_hexDrunken Hex1230falsespiral13.3%
specterHaunting Specter1040falseghost11.1%
pumpkin_headPumpkin Head1460truepumpkin15.6%
heavy_bonesHeavy Bones1230falsebone13.3%
clumsyTrickster's Trip1025falseimp11.1%
blackoutShroud of Night1020falsemoon11.1%

Chance on foot = weight ÷ 90 (the sum of all default weights). In a vehicle only pumpkin_head can land, because it's the only curse with vehicle = true.

Extra keys

KeyDefaultWhat it does
levitate.height2.4Metres the player is lifted. They rise over 2 seconds, bob and slowly turn while frozen in place, then sink back down when the curse ends. Nothing happens if they're in a vehicle.
haunted_sight.screenEffect'DrugsTrevorClownsFight'Screen effect (an AnimpostfxPlay name), shown together with a light camera shake and a spectral vignette.
drunken_hexno extra keysDrunk walk (move_m@drunk@verydrunk), the drunk ped flag and a strong camera shake.
specter.model'u_m_y_zombie_01'Ghost ped model. It spawns 6 m behind the player, semi-transparent, and chases them. It's a local ped, so only the cursed player sees it. It disappears when the curse ends.
specter.health150Ghost health. The ped is spawned with health + 100.
specter.damage8Damage per hit. The ghost hits when it's within 1.6 m, at most once every 1.5 seconds.
specter.reward.item'hn_ghost_essence'Item for banishing the ghost before the curse runs out.
specter.reward.amount1How many of that item.
specter.reward.souls2Souls for the kill. When it's above 0, the kill also counts as a spirit in the player's stats. The server only pays out with a one-time token issued when the curse started.
pumpkin_head.model'hn_pumpkin_head'Prop attached to the head.
pumpkin_head.fallbackModel'prop_veg_crop_03_pump'Used if the custom model can't load.
pumpkin_head.bone31086Ped bone to attach to (SKEL_Head).
pumpkin_head.offset{ x = -0.075, y = 0.02, z = 0.0, rx = 0.0, ry = 90.0, rz = 0.0 }Position and rotation on the bone. Tweak x between -0.06 and -0.09 to raise or lower the pumpkin. The curse sets the hnPumpkin player state bag, and every nearby client attaches its own local prop.
heavy_bones.moveRate0.65Movement speed multiplier. Sprint and jump are disabled for the whole curse.
clumsy.interval{ 4000, 8000 }Random time in ms between trips. Each trip ragdolls the player (skipped while in a vehicle or already ragdolled).
blackoutno extra keysA dark overlay closes in on the screen.

Immunity

KeyDefaultWhat it does
Config.CurseImmunityAfterCurse20Seconds after a curse ends during which no new curse can land. This stops curse chains.
  • Only one curse lands at a time. While a curse plus its immunity window is running, a new curse is resisted and the player sees the "curse resisted" toast ("Your purification repelled a curse.").
  • The block is based on the curse's full duration. A curse lifted early by death still keeps new curses away until duration + CurseImmunityAfterCurse has passed.
  • The Purifying Elixir lifts all curses and replaces the block with its own Config.Potions.hn_potion_purify.immunity (180 seconds).
  • If no enabled curse is allowed (for example, every curse is disabled), no curse lands. A trick still pays its Souls.

config/potions.lua

Config.Potions decides what happens when a player uses an item. Every key in this table becomes usable: the pack registers it as a usable item on QBCore, Qbox and ESX, the internal satchel shows a Use button for it, and on ox_inventory the rklhalloween.useItem export accepts it (the item still needs server = { export = 'rklhalloween.useItem' } in ox_inventory/data/items.lua). Durations are in seconds.

ItemeffectParameters (defaults)
hn_potion_owl'nightvision'duration = 90
hn_potion_swift'swift'duration = 60 · multiplier = 1.25: sprint speed. The code caps it at 1.49.
hn_potion_feather'featherfall'duration = 120 · maxFallSpeed = 4.0: the fastest the player can fall, in m/s.
hn_potion_phantom'phantom'duration = 30 · alpha = 110: player transparency, 0–255 (255 = solid) · disableWeapons = true: blocks shooting, aiming and melee.
hn_potion_vigor'vigor'heal = 60: health added · armor = 25: armour added (0 = none).
hn_potion_purify'purify'immunity = 180: seconds without curses after cleansing.
hn_potion_seer'seer'duration = 90: seconds the lantern blips stay · radius = 400.0: search radius in metres.
hn_candy'candy'heal = 5: health added · sugarRush = 10: seconds of sprint ×1.15 (0 = no rush).
hn_sludge'sludge'damage = 10: damage dealt.

The effect names are fixed: nightvision, swift, featherfall, phantom, vigor, purify, seer, candy, sludge. What each one does in game is described in Items, potions & recipes.

Check the effect name

The server takes the item before the client runs the effect. If effect is misspelled, the item is used up and nothing happens.

Use animations

KeyDefaultWhat it does
Config.PotionAnim.dict'mp_player_intdrink'Animation dictionary for drinking.
Config.PotionAnim.clip'loop_bottle'Animation clip.
Config.PotionAnim.time2200Milliseconds the animation plays before the effect starts.
Config.CandyAnim.dict'mp_player_inteat@burger'Animation dictionary for eating.
Config.CandyAnim.clip'mp_player_int_eat_burger'Animation clip.
Config.CandyAnim.time1800Milliseconds before the effect starts.

Config.CandyAnim plays for items with the candy effect, and Config.PotionAnim plays for every other effect except sludge, which has its own vomit animation. Both play on the upper body only, so the player can keep moving. No animation plays while the player is in a vehicle.

Add your own consumable

Any item can reuse an existing effect with its own numbers:

config/potions.lua
Config.Potions.my_herbal_tea = { effect = 'vigor', heal = 25, armor = 0 }
  1. Register my_herbal_tea in your inventory. On ox_inventory, give it server = { export = 'rklhalloween.useItem' }. On QBCore, Qbox and ESX the pack registers it as usable on start.
  2. Optional: add a Config.Items entry so the Halloween UI shows a proper name.

Timed effects are tracked per effect name, so two items with the same timed effect share one timer.

Hallowed Night — Module config

A key-by-key reference for the five activity files in config/: lanterns.lua, trickortreat.lua, cauldron.lua, gathering.lua and haunted.lua. Every default below is the value that ships in the file. All of them stay editable after escrow.

Before you edit

  • The config files load as shared scripts (server and client) when the resource starts, so restart the resource after you change them.
  • Each module has its own Enabled switch. Setting it to false turns that module off on both server and client.
  • Lanterns, doors, gathering nodes and cauldrons only exist while the season is active (Config.Season in config/config.lua). Scheduled Haunted Nights also wait for the season. An admin start (/hnhaunt start or the StartHaunt export) works at any time.
  • Every timed action (collect, knock, gather) is checked on the server: the player must start it there, stay close, and let the progress ring run for the full time (500 ms tolerance). One press of the interact key starts it; the key doesn't have to be held. The server also allows a little extra distance on top of each InteractDistance, because client-side ground snapping can shift positions slightly.

Units

UnitKeys
millisecondsLanterns.CollectTime, TrickOrTreat.KnockTime, TrickOrTreat.Npc.Duration, Cauldron.BrewTime, gathering node time
secondsLanterns.RespawnSeconds, TrickOrTreat.DoorCooldown, gathering node respawn, Haunted.AnnounceSeconds, Haunted.TimeLimit, Haunted.WaveDelay, Haunted.Boss.Nova.interval
0.0 – 1.0Lanterns.CursedChance, TrickOrTreat.TrickChance, Haunted.Boss.Summon.atHealth
0 – 100 (%)chance in every loot entry
metresevery RenderDistance, InteractDistance and radius, plus Haunted.ParticipantRadius

Loot tables vs. weighted treats

Two reward formats are used across the modules, and they behave differently:

FormatUsed byHow it is rolled
Loot (chance)Lanterns.Loot, Lanterns.CursedLoot, Haunted.Rewards.Kill.items, Haunted.Rewards.Boss.itemsEvery entry is rolled on its own with its chance in %. One action can give nothing, one entry, or all of them. A missing chance counts as 100.
Treats (weight)TrickOrTreat.TreatsExactly one entry is picked per Treat. Its odds are weight ÷ total weight. A missing weight counts as 1.
entry format
-- Loot entry: rolled on its own { chance = 45, item = 'hn_pumpkin_seeds', min = 1, max = 2 }, -- 45%: 1-2 seeds { chance = 10, money = 'cash', min = 50, max = 150 }, -- 10%: $50-150 cash -- Treat entry: one of the list is picked by weight { weight = 50, item = 'hn_candy', min = 1, max = 3 }, { weight = 10, money = 'cash', min = 25, max = 120 },
  • item gives an inventory item. money pays an account: 'cash' or 'bank' (on ESX, cash maps to the money account).
  • The amount is random between min and max. You can write amount = 3 instead for a fixed amount.
  • If an item doesn't fit in the inventory, it isn't given and the player sees a "can't carry" notice.

Coordinates

/hncoords always copies vec4(x, y, z, heading). Lists that use vec3 (Lanterns.Locations, gathering points, Haunted center) need the 4th value removed. Lists that use vec4 (TrickOrTreat.Doors, Cauldron.Locations) keep it. Lantern, node, cauldron and tombstone props snap to the ground, so the exact z is not critical.

config/lanterns.lua — Soul Lantern Hunt

Glowing lanterns spawn around the map. Pressing E on one starts a short collect (the progress ring fills by itself) and gives Souls and loot. Cursed lanterns give more Souls and extra loot, but also put a random curse on the player.

KeyDefaultWhat it does
EnabledtrueTurns the whole lantern hunt on or off.
Mode'global''global': shared lanterns. The first player to collect one wins it, and a replacement spawns later. 'personal': every location is a collectible that each player can claim once per season.
ActiveCount20Global mode: how many lanterns exist at the same time. Capped at the number of Locations (32 by default).
RespawnSeconds240Global mode: the delay before a replacement lantern appears at a random free location. The spot it was taken from stays empty until then.
CursedChance0.150.0 – 1.0. Global mode rolls it every time a lantern spawns. Personal mode uses a fixed set of locations that are always cursed (picked from the location number), roughly this share of them. With the 32 default locations and 0.15, those are locations 5, 10, 15, 26 and 31.
CursedLooksDifferenttruetrue: cursed lanterns use CursedModel, the Light.cursed colour, a looping aura effect and the "Collect Cursed Lantern" prompt. false: every lantern looks normal, so players can't tell which ones are cursed.
CollectTime2500Collect duration in ms (the progress ring). The server only pays out if the action really lasted this long.
RenderDistance80.0The lantern prop spawns when the player is closer than this, and despawns once they are 15 m beyond it.
InteractDistance2.0Prompt / target distance. The server accepts up to 2.5 m more.
Model'hn_soul_lantern'Normal lantern prop.
CursedModel'hn_cursed_lantern'Cursed lantern prop (only used while CursedLooksDifferent = true).
FallbackModel'prop_veg_crop_03_pump'Vanilla prop used when the custom model can't load.
Light.normal{ 255, 140, 40 }RGB glow of a normal lantern (orange).
Light.cursed{ 150, 60, 255 }RGB glow of a visible cursed lantern (purple).
Light.range4.0Light radius. The light is only drawn while the player is within 45 m.
Light.intensity6.0Light brightness.
Souls.normal1Souls for a normal lantern.
Souls.cursed3Souls for a cursed lantern.
Loot4 entriesRolled for every lantern, normal or cursed. Each entry rolls on its own.
CursedLoot2 entriesRolled on top of Loot when the lantern was cursed.
Blips.Enabledfalsetrue puts a short-range blip on every active lantern (easy mode).
Blips.Sprite484Blip icon.
Blips.Color47Blip colour. Cursed lanterns that look different use colour 27.
Blips.Scale0.6Blip size.
Locations32 × vec3Every possible lantern spot. z snaps to the ground.

Default lantern loot

RewardTableChanceAmount
hn_pumpkin_seedsLoot45%1 – 2
hn_raven_featherLoot25%1
hn_candyLoot30%1 – 2
cashLoot10%$50 – $150
hn_ghost_essenceCursedLoot70%1
hn_bone_dustCursedLoot40%1 – 2
  • Cursed lantern: Souls.cursed + Loot + CursedLoot, then a random curse about 1.2 s later. Curse immunity and the vehicle rules from config/curses.lua still apply. It also counts toward the cursed stat.
  • Personal mode: collected spots are saved in each player's progress for the current Config.Season.Id. ActiveCount and RespawnSeconds are not used. Every location the player hasn't collected yet is shown.
  • Admin: /hnlanterns respawn clears every global lantern and its cooldown, then rolls a fresh set.
config/lanterns.lua — adding a spot
Locations = { vec3(195.17, -933.77, 30.69), -- Legion Square -- /hncoords gave vec4(412.10, -1001.30, 29.30, 90.0) -> drop the heading: vec3(412.10, -1001.30, 29.30), -- your new spot },

config/trickortreat.lua — Trick-or-Treat

Players knock on configured doors. A costumed NPC opens up and hands out a Treat (one weighted pick) or plays a Trick (a random curse).

KeyDefaultWhat it does
EnabledtrueTurns trick-or-treat on or off.
RequireItemfalsetrue: players must carry RequiredItem to knock. The item is only checked, never removed.
RequiredItem'hn_candy_bucket'Item needed when RequireItem = true (the 500-Soul milestone gives one by default).
KnockTime2200Knock duration in ms, checked by the server.
InteractDistance1.8Prompt / target distance from the door point. The server accepts up to 2.0 m more.
DoorCooldown1800Seconds before the same player can knock on the same door again. It is kept in server memory per player identifier, so a resource restart clears it.
DailyLimit40Max knocks per player per real day, based on the server's date. 0 = unlimited. The count is saved with the player's progress.
TrickChance0.300.0 – 1.0 chance that a knock is a Trick instead of a Treat.
Souls.treat1Souls for a Treat, plus one pick from Treats. Counts toward the treats stat.
Souls.trick2Souls for a Trick, plus a random curse about 1.8 s later (immunity and vehicle rules apply). Counts toward the tricks stat.
Npc.Enabledtruefalse = no NPC; the result shows right away.
Npc.Models5 ped modelsa_f_y_hipster_01, a_m_y_hipster_01, a_f_m_bevhills_01, a_m_m_bevhills_02, a_f_y_vinewood_02. One is picked at random for each knock.
Npc.Duration3500How long the NPC stays, in ms, before it vanishes.
Npc.HoldBuckettrueThe NPC holds the hn_candy_bucket prop. If that model can't load, the NPC simply has no bucket (there is no fallback).
Npc.BucketOffset{ x = 0.10, y = 0.0, z = 0.0, rx = 0.0, ry = 280.0, rz = 53.0 }Position and rotation of the bucket on the NPC's right hand bone (SKEL_R_Hand). Tweak this if you swap the bucket model.
Treats5 entriesWeighted pick, one entry per Treat. See the odds below.
Blips.Enabledfalsetrue puts a short-range "Trick or Treat" blip on every door.
Blips.Sprite40Blip icon.
Blips.Color17Blip colour.
Blips.Scale0.5Blip size.
Doors15 × vec4Sample residential doors (Forum Drive, Grove Street, Mirror Park, Vespucci Canals, Rockford Hills, Richman, Sandy Shores, Paleto Bay). Replace or add your own.

Default treat odds

TreatWeightOddsAmount
hn_candy5050%1 – 3
hn_pumpkin_seeds2020%1 – 2
hn_raven_feather1515%1
hn_moonpetal55%1
cash1010%$25 – $120

The weights add up to 100 here, so each weight reads as a percentage. They don't have to: with weights 2 and 1, the odds are 2/3 and 1/3.

What the four vec4 values mean

ValueMeaning
x, y, zThe knock point. The prompt or target sphere sits here.
w (heading)The direction the NPC faces when the door opens, looking out of the house. While knocking, the player is turned to w + 180 (toward the door), and the NPC appears 1.15 m in front of them, facing back.
Getting the heading right

Stand on the doorstep with your back to the door, looking out at the street, and type /hncoords. The copied vec4 can go straight into Doors. If you took it while facing the door, add or subtract 180 from the heading.

config/cauldron.lua — Witch's Cauldron

Cauldrons turn gathered ingredients into potions. Players pick a recipe in the cauldron UI, pass a stirring skill-check, and get the potion (or sludge).

KeyDefaultWhat it does
EnabledtrueTurns the cauldrons on or off. Gathering has its own switch in gathering.lua.
Model'hn_cauldron'Cauldron prop.
FallbackModel'prop_barrel_01a'Vanilla prop used when the custom model can't load.
InteractDistance2.2Prompt / target distance. Opening the recipe list and brewing are both re-checked by the server against every cauldron location (with up to 2.5 m extra).
RenderDistance120.0The prop (and its witch) spawn within this distance.
Light.color{ 80, 255, 120 }RGB glow drawn 1 m above the cauldron (green).
Light.range5.0Light radius. Drawn while the player is within 45 m.
Light.intensity8.0Light brightness.
Witch.EnabledtrueSpawns a witch NPC next to each cauldron, on each player's client. She is frozen, invincible and doesn't react to anything around her.
Witch.Model'a_f_o_soucent_02'Witch ped model.
Witch.Offsetvec3(-1.2, 0.6, 0.0)Position relative to the cauldron, so it turns with the cauldron's heading. Snapped to the ground, and she always faces the cauldron.
Witch.Scenario'WORLD_HUMAN_STAND_IMPATIENT'Idle scenario the witch plays.
Blip.EnabledtrueA short-range blip on every cauldron.
Blip.Sprite499Blip icon.
Blip.Color27Blip colour.
Blip.Scale0.8Blip size.
Blip.Label"Witch's Cauldron"Blip name.
Locations3 × vec4x, y, z plus the heading of the cauldron prop. Defaults: Pacific Bluffs cemetery, Grapeseed, Paleto Bay.
BrewTime3500Length of the stirring animation after the minigame, in ms. The result shows when it ends. The server also allows only one brew per player per BrewTime.
Souls2Souls per successful brew (counts toward the brews stat). Failed brews give no Souls.
Minigame.Enabledtruefalse = no skill-check, and every brew succeeds.
Minigame.Rounds3Successful stirs needed to pass.
Minigame.Speed1.0Marker speed multiplier. The base speed is 150° per second around the ring, and it gets 14% faster after every good stir.
Minigame.ZoneSize0.18Size of the glowing zone as a share of the ring (0.18 ≈ 65°). Clamped to 0.05 – 0.5. Bigger = easier.
Minigame.MaxMisses2Misses allowed. The next miss fails the brew, so by default the 3rd miss fails.
Fail.LoseIngredientstruetrue: a failed brew uses up the ingredients. false: players keep them.
Fail.Item'hn_sludge'Item given on a failed brew. false = nothing.
Fail.Amount1How many Fail.Item to give (1 if left out).
Recipes7 recipesKeyed by recipe id. Format below.
RecipeOrder{ 'owl', 'swift', 'feather', 'vigor', 'purify', 'seer', 'phantom' }The order recipes appear in the cauldron and in the Grimoire recipe book. A recipe that isn't listed here is not shown.

Recipe format

config/cauldron.lua
Recipes = { -- id = { result item, amount, ingredients } owl = { item = 'hn_potion_owl', -- item given on success amount = 1, -- how many color = '#f5b942', -- potion accent colour in the cauldron UI and recipe book ingredients = { { item = 'hn_moonpetal', amount = 2 }, { item = 'hn_raven_feather', amount = 1 }, }, }, }, RecipeOrder = { 'owl', 'swift', 'feather', 'vigor', 'purify', 'seer', 'phantom' },
FieldWhat it does
itemThe result item. Its label and description in the UI come from Config.Items (config/items.lua). Without an entry there, the item name is shown. The image is html/assets/items/<item>.png.
amountHow many result items one successful brew gives.
colorA CSS colour that tints the potion in the cauldron UI and the recipe book.
ingredientsA list of { item, amount }. The Brew button only unlocks when the player has all of them, and the server checks them again before brewing.

Default recipes

IdResultIngredientsColour
owlhn_potion_owl ×12 Moonpetal · 1 Raven Feather#f5b942
swifthn_potion_swift ×12 Pumpkin Seeds · 1 Raven Feather#3fe0ff
featherhn_potion_feather ×12 Raven Feather · 1 Moonpetal#d9c8ff
vigorhn_potion_vigor ×12 Mandrake Root · 1 Pumpkin Seeds#ff3b4e
purifyhn_potion_purify ×11 Moonpetal · 1 Mandrake Root · 1 Ghost Essence#cfe8ff
seerhn_potion_seer ×11 Ghost Essence · 1 Bone Dust · 1 Moonpetal#9b4dff
phantomhn_potion_phantom ×12 Ghost Essence · 1 Bone Dust#9ffff0

How a brew resolves

  1. Opening the cauldron asks the server for the recipe list. The server checks the season, Hooks.CanParticipate and that the player is at a cauldron, then sends each recipe with the player's ingredient counts.
  2. With Minigame.Enabled, the stir ring opens. Players press SPACE (or click anywhere in the stir window) while the marker is inside the glowing zone. Pressing Esc during the stir cancels it, and nothing is brewed or used up.
  3. The server checks the ingredients again. Success: the ingredients are removed, the player gets the result item and Souls. Fail: the player gets Fail.Item, and the ingredients are removed only if Fail.LoseIngredients is on.
  4. If the result item doesn't fit in the inventory, every removed ingredient is given back.
Adding your own recipe

Add it to Recipes and to RecipeOrder. Register the result item in your inventory (see install/items) and in Config.Items, and add its PNG. To make it drinkable, give it an entry in Config.Potions (config/potions.lua) with one of the existing effects: nightvision, swift, featherfall, phantom, vigor, purify, seer, candy or sludge.

config/gathering.lua — Ingredient gathering

Gathering nodes are shared. Once someone gathers a point, it disappears for everyone until its respawn time has passed. Ghost Essence doesn't come from nodes: it drops from Haunted Night spirits (and the boss reward), the Specter curse and cursed lanterns. Pumpkin Seeds and Raven Feathers come from lanterns and trick-or-treat.

KeyDefaultWhat it does
EnabledtrueTurns gathering on or off. This is separate from Config.Cauldron.Enabled.
RenderDistance70.0Node props spawn within this distance.
InteractDistance1.8Prompt / target distance. The server accepts up to 2.5 m more.
Nodesmandrake · moonpetal · bonesOne entry per node type, keyed by any unique id. The fields are below.

Node fields

FieldExample (mandrake)What it does
label'Mandrake'Shown in the prompt ("Gather Mandrake") and the progress ring ("Gathering Mandrake...").
model'hn_mandrake'Node prop.
fallbackModel'prop_plant_01a'Vanilla prop used when model can't load.
item'hn_mandrake_root'Item the node gives.
min1Minimum amount per gather.
max2Maximum amount per gather (random between min and max).
time4000Gather duration in ms, checked by the server.
respawn600Seconds the point stays gathered (hidden) for everyone.
nightOnlyfalsetrue: the node only shows while the in-game clock is between 20:00 and 06:00.
anim{ dict = 'amb@world_human_gardener_plant@male@base', clip = 'base' }Animation played while gathering.
souls0Souls per gather (0 = none). Every gather counts toward the gathered stat either way.
lightmoonpetal only: { 120, 180, 255 }Optional RGB glow (range 2.5, intensity 4.0). Leave it out for no light.
points6 × vec3Every spot where this node spawns. z snaps to the ground.

Default nodes

NodeItemAmountTimeRespawnNight onlyPoints
mandrakehn_mandrake_root1 – 24000 ms600 sno6
moonpetalhn_moonpetal13000 ms480 syes5
boneshn_bone_dust1 – 23500 ms600 sno5

Mandrake and Moonpetal use the gardening animation, and Bone Pile uses a kneel animation. If the player's inventory is full, the gather fails and the node stays up for someone else. Gathered points are tracked in server memory, so every node comes back after a resource restart.

config/gathering.lua — example extra node type
Nodes = { -- ...mandrake, moonpetal, bones... grave_bones = { label = 'Grave Bones', model = 'hn_bone_pile', fallbackModel = 'prop_rub_binbag_01', item = 'hn_bone_dust', min = 1, max = 1, time = 3500, respawn = 900, nightOnly = true, anim = { dict = 'amb@medic@standing@kneel@base', clip = 'base' }, souls = 1, -- also pays 1 Soul per gather light = { 150, 60, 255 }, -- optional glow points = { vec3(-1740.00, -230.00, 55.50), }, }, },

config/haunted.lua — Haunted Night event

The veil thins over one zone: fog and blood-moon visuals, waves of spirits, and then The Hollow King. The spirits are created by the server. The player nearest the zone centre (the "host") only supplies ground positions, and whoever owns each spirit runs its AI.

Event & schedule

KeyDefaultWhat it does
EnabledtrueTurns the event on or off, including the admin command and exports.
Schedule{ hour = 20, minute = 0 }, { hour = 23, minute = 30 }Start times in real server time (24 h, the server's local clock), checked every 20 s. Each entry fires at most once a day, in a random zone, and only when the season is active, no event is running and at least MinPlayers players are online. Schedule = {} means the event only starts by command or export.
MinPlayers1Online players needed for a scheduled start. It doesn't apply to admin starts.
AnnounceSeconds120Warning phase before the fight: a banner for everyone with the zone name and the time until it starts, plus a flashing blip and radius on the map. Players inside the zone see the live countdown in the event HUD.
TimeLimit15 * 60Seconds (900) to clear every wave and the boss, counted from the end of the announce phase. When time runs out, the spirits retreat and the event fails.
ParticipantRadius30.0Extra metres outside the zone radius that still count. Players inside radius + ParticipantRadius become participants, and the host is picked from that area. At victory, participants within radius + 2 × ParticipantRadius are paid.

Visuals

KeyDefaultWhat it does
Visuals.Weather'HALLOWEEN'Client-side weather override while the player is inside the zone. '' = off.
Visuals.PauseWeatherSynctruePauses qb-weathersync / Renewed-Weathersync while the player is inside the zone, so the fog isn't reset (ClientHooks.OnHauntZone in editable/client/hooks.lua). The in-game time is kept when qb-weathersync is paused.
Visuals.Timecycle'nervousRON_fog'Timecycle modifier while inside the zone. '' = off.
Visuals.TimecycleStrength0.55Timecycle strength.
Visuals.TombstonestruePlaces decorative tombstones in a ring around the zone, from the announce phase until the event resets.
Visuals.TombstoneModel'hn_tombstone'Tombstone prop.
Visuals.TombstoneFallback'prop_gravestones_01a'Vanilla prop used when the custom model can't load.
Visuals.TombstoneCount10Number of tombstones, spread between 35% and 85% of the zone radius.

A player counts as inside once they're within the zone radius, and leaves only after moving 25 m past it, so the visuals don't flicker at the edge. The event HUD (wave, spirits left, timer, boss bar) is shown only to players inside the zone.

Spirits & waves

KeyDefaultWhat it does
Spirits.Models{ 'u_m_y_zombie_01' }Ped models for normal spirits. One is picked at random for each spirit.
Spirits.Health250HP of each spirit. The owning client sets its max health to this value + 100 (GTA's base health).
Spirits.Armor0Armour of normal spirits. The boss always gets 100.
Spirits.Alpha170Transparency of normal spirits (255 = solid, lower = more ghostly). The boss is always solid.
Spirits.Accuracy25Combat accuracy of spirits and the boss.
Spirits.MoveClipset'move_m@drunk@verydrunk'Walk style of normal spirits (not used on the boss).
Waves{ count = 6 }, { count = 9 }, { count = 12 }One entry per wave, with the number of spirits in it. The number of entries is the number of waves ("Wave 1 / 3").
WaveDelay10Seconds between the last spirit of a wave dying and the next wave (or the boss) starting.
MaxAlive14Safety cap on living spirits, boss summons included. Bigger waves spawn in batches as spirits die, with a new attempt every 3 s.

Boss

KeyDefaultWhat it does
Boss.Label'The Hollow King'Name used in the boss banner, boss bar and victory banner. The Soul Nova warning text is a locale string (haunt_nova in locales/*.lua), so edit it too if you rename him.
Boss.Model'u_m_y_zombie_01'Boss ped model.
Boss.Health4000Base boss HP.
Boss.HealthPerPlayer1200Extra HP for every participant above 1. The total is capped at 8000.
Boss.Weapon'WEAPON_BATTLEAXE'Weapon given to the boss.
Boss.HeadModel'hn_pumpkin_head'Pumpkin head attached to the boss, with the same offset as the Pumpkin Head curse (Config.Curses.pumpkin_head.offset).
Boss.HeadFallback'prop_veg_crop_03_pump'Vanilla prop used when the custom head can't load.
Boss.Nova.interval18Seconds between Soul Novas while the boss is alive.
Boss.Nova.radius7.0Living players within this many metres of the boss are knocked down and pushed away. Players within 250 m see the effect and feel a camera shake.
Boss.Nova.damage15Damage dealt to every player the Nova hits.
Boss.Summon.atHealth{ 0.66, 0.33 }Boss HP fractions. When his HP drops to each one, he summons once (66% and 33% by default).
Boss.Summon.count4Spirits per summon, spawned within 10 m of the boss. MaxAlive still applies.
Boss.Scale1.0In the file, but the current code doesn't read it, so changing it has no effect.

Boss HP by participants (defaults)

ParticipantsBoss HP
14000
25200
36400
47600
5 or more8000 (cap)
Why the 8000 HP cap

GTA syncs ped health in 13 bits (max 8191), and the boss gets the extra 100 base health on top. The server caps the boss at 8000 HP no matter how high you set Boss.Health or Boss.HealthPerPlayer. Participants are counted when the boss spawns: everyone who has been within radius + ParticipantRadius since the fight started.

The boss doesn't take critical hits and can't be ragdolled. If his entity disappears (for example, when nobody is close enough to own him), the server spawns him again.

Rewards

KeyDefaultWhat it does
Rewards.Kill.souls2Souls for the player who lands the killing blow on a spirit (the driver, if it was run over). Wave spirits and boss summons both count; the boss himself doesn't. Counts toward the spirits stat.
Rewards.Kill.items{ chance = 35, item = 'hn_ghost_essence', min = 1, max = 1 }Loot rolled for every spirit kill.
Rewards.Boss.souls25Souls for every eligible participant when the boss falls. Counts toward the bosses stat.
Rewards.Boss.items3 entriesLoot rolled separately for each eligible participant: hn_ghost_essence 100% (2 – 3), hn_potion_phantom 60% (1), hn_candy 50% (3 – 6).
Rewards.Boss.money{ account = 'cash', min = 1500, max = 3500 }Money paid to each eligible participant. Remove the key to pay nothing.

To be eligible for the boss reward, a player must be a participant, still be online, be within radius + 2 × ParticipantRadius at the moment of victory, and pass Hooks.CanParticipate. The final list is passed to Hooks.OnBossDefeated(list).

Zones & blip

KeyDefaultWhat it does
Zones3 zonesEach one is { label, center = vec3, radius }. /hnhaunt start 2 uses the 2nd zone. Without a number, a random zone is picked.
Blip.Sprite484Centre blip icon, named "Haunted Night - <zone label>". It flashes during the announce phase.
Blip.Color27Centre blip colour.
Blip.Scale1.1Centre blip size.
Blip.RadiusColor27Colour of the zone radius circle.
Blip.RadiusAlpha90Opacity of the radius circle.
Zone #LabelCenterRadius
1Pacific Bluffs Cemeteryvec3(-1735.00, -220.00, 56.00)80.0
2Grapeseed Fieldsvec3(2010.00, 4890.00, 42.80)90.0
3Sandy Shores Outskirtsvec3(1600.00, 3600.00, 35.00)90.0
config/haunted.lua — adding a zone
Zones = { -- ...the three default zones... { label = 'My Graveyard', center = vec3(0.0, 0.0, 0.0), -- /hncoords without the heading, on the ground radius = 80.0, }, },
Picking a good zone

Spirits spawn within 80% of the radius around center, on ground no more than 25 m above or below the centre's height, never on water, and 5 – 90 m away from the host player. Choose open, dry, fairly flat ground and keep center.z at ground level. Waves only spawn while at least one player is within radius + ParticipantRadius.

Event phases

StatusWhat happensKeys involved
announceA banner for every player, a flashing blip and radius, and tombstones placed.AnnounceSeconds, Blip.*, Visuals.Tombstone*
activeWaves spawn one after another. The timer is running.Waves, WaveDelay, MaxAlive, Spirits.*, TimeLimit, Rewards.Kill
bossThe Hollow King spawns with the boss bar, Soul Novas and summons.Boss.*, TimeLimit
endedVictory (pays Rewards.Boss) or retreat. Spirits are removed 6 s later.Rewards.Boss, ParticipantRadius
idleThe event resets 12 s after it ends, and the next one can start.Schedule, MinPlayers

The status is published in GlobalState.hnHaunt and returned by exports['rklhalloween']:GetHauntState(). The announce banner reaches every player. The start, boss, victory and defeat banners only reach players within the zone radius + 600 m, and wave notices within the zone radius + 400 m.

Hallowed Night — Items, potions & recipes

The pack uses 16 items: 6 ingredients, a treat, a tool, 7 potions and the sludge from a failed brew. Item definitions for each inventory are in install/items/ (ox_inventory.lua, qb-core.lua, esx.sql) and the pictures are in install/images/. The Halloween UI has its own copy in html/assets/items/ and takes its labels from Config.Items.

All items

ItemLabelTypeWhere it comes from (defaults)Weight
hn_mandrake_rootMandrake RootingredientMandrake gathering nodes: 1–2 per pick (6 spots, respawn 600 s)50
hn_moonpetalMoonpetalingredientMoonpetal nodes, only visible 20:00–06:00 in-game: 1 per pick (5 spots, respawn 480 s) · trick-or-treat treat, weight 5: 150
hn_bone_dustBone DustingredientBone Pile nodes: 1–2 per pick (5 spots, respawn 600 s) · cursed lanterns: 40% for 1–250
hn_ghost_essenceGhost EssenceingredientCursed lanterns: 70% for 1 · Haunted Night spirit kill: 35% for 1 · banishing the Specter curse: 1 · Hollow King victory: 100% for 2–350
hn_pumpkin_seedsPumpkin SeedsingredientEvery lantern (normal and cursed): 45% for 1–2 · treat, weight 20: 1–250
hn_raven_featherRaven FeatheringredientEvery lantern: 25% for 1 · treat, weight 15: 150
hn_candyHallowed Candytreat · usableEvery lantern: 30% for 1–2 · treat, weight 50: 1–3 · Hollow King victory: 50% for 3–6 · milestone Wandering Spirit (25 Souls): 520
hn_candy_bucketCandy BuckettoolMilestone Hollow Sovereign (500 Souls): 1. With Config.TrickOrTreat.RequireItem = true, players need one in their inventory to knock (it isn't used up).300
hn_potion_owlOwl's Eye Elixirpotion · usableCauldron recipe owl150
hn_potion_swiftSwiftfoot Draughtpotion · usableRecipe swift · milestone Lantern Keeper (75 Souls): 2150
hn_potion_featherFeatherfall Tonicpotion · usableRecipe feather150
hn_potion_phantomPhantom Veilpotion · usableRecipe phantom · Hollow King victory: 60% for 1 · milestone Veil Walker (300 Souls): 2150
hn_potion_vigorElixir of Vigorpotion · usableRecipe vigor150
hn_potion_purifyPurifying Elixirpotion · usableRecipe purify · milestone Hex Weaver (150 Souls): 2150
hn_potion_seerSeer's Draughtpotion · usableRecipe seer · milestone Hex Weaver (150 Souls): 2150
hn_sludgeCauldron Sludgefailed potion · usableAny failed brew: 1 (Config.Cauldron.Fail.Item / Fail.Amount)150

Weight is the value in install/items/ox_inventory.lua and qb-core.lua. esx.sql uses 1 for ingredients and candy, 6 for the bucket and 3 for potions and sludge.

  • Lantern loot (Config.Lanterns.Loot): every entry rolls on its own, so one lantern can give several items. Lanterns can also give $50–150 cash (10%). A cursed lantern rolls Loot first, then CursedLoot as well.
  • Treats (Config.TrickOrTreat.Treats): one weighted pick per treat. The default weights add up to 100, so each weight is also the percent chance. Cash ($25–120) has weight 10.
  • Full inventory: loot that doesn't fit is skipped with a "You can't carry that." toast. A gathering node stays up when its item doesn't fit, so the player can come back.

Potion effects

What each effect does in game (client/modules/potions.lua), with the default numbers from Config.Potions:

ItemEffectKindWhat happens
hn_potion_owlnightvisiontimed, 90 sTurns on GTA night vision, and turns it off when the time runs out.
hn_potion_swiftswifttimed, 60 sSprint speed ×1.25 (SetRunSprintMultiplierForPlayer). If the candy sugar rush is active too, the higher multiplier wins (they don't add up), and it's always capped at 1.49.
hn_potion_featherfeatherfalltimed, 120 sWhile the player is falling or in parachute free fall, their downward speed is held at maxFallSpeed (4.0 m/s), so they drift down instead of plummeting.
hn_potion_phantomphantomtimed, 30 sSets the player's alpha to 110, turns on GTA ghost mode (SetLocalPlayerAsGhost) and shows a phantom screen overlay. With disableWeapons, firing, aiming and melee are blocked. Everything is reset when it ends.
hn_potion_vigorvigorinstant+60 health (never above max health) and +25 armour, with a heal flash on screen.
hn_potion_purifypurifyinstantLifts every active curse, then gives 180 s of curse immunity, enforced on both the client and the server. A pending Specter reward is cancelled. Plays the brew particle effect.
hn_potion_seerseerinstant, blips last 90 sPuts a flashing blip on every active lantern within 400 m for 90 s and says how many it found ("reveals nothing nearby" if none). Lanterns a player already collected in personal mode aren't shown.
hn_candycandyinstant + timed 10 s+5 health, then a 10 s sugar rush with sprint ×1.15 (this multiplier is fixed in the code). Uses Config.CandyAnim.
hn_sludgesludgeinstantVomit animation, 10 damage and a drunk camera shake for 6 seconds. No drink animation.
Drinking a potion that's already active

Using a timed potion again while it's active restarts its timer at the full duration (for example, back to 60 s for Swiftfoot). The time doesn't stack. Timed effects show in the effects HUD with a countdown, and a toast says when one wears off. Instant effects apply in full every time. A second Seer's Draught places a fresh set of blips.

How using an item works

  1. The player uses the item. On ox_inventory the item calls the rklhalloween.useItem export, and ox removes the item itself. On QBCore, Qbox and ESX, the pack registers every Config.Potions item as usable. With the internal satchel, the player presses Use in the Grimoire.
  2. The server removes one item (unless ox already did), applies the server side of the Purifying Elixir, logs the use with Hooks.Log(src, 'consume', ...) and tells the client to run the effect.
  3. The client plays the use animation (see Config.PotionAnim / Config.CandyAnim), shows a toast, then runs the effect.

Cauldron recipes

From Config.Cauldron.Recipes, in the order of Config.Cauldron.RecipeOrder (the order the Grimoire recipe book and the cauldron use). Every recipe makes 1 potion.

Recipe idResultIngredientsColour
owlhn_potion_owl · Owl's Eye Elixir2× Moonpetal · 1× Raven Feather#f5b942
swifthn_potion_swift · Swiftfoot Draught2× Pumpkin Seeds · 1× Raven Feather#3fe0ff
featherhn_potion_feather · Featherfall Tonic2× Raven Feather · 1× Moonpetal#d9c8ff
vigorhn_potion_vigor · Elixir of Vigor2× Mandrake Root · 1× Pumpkin Seeds#ff3b4e
purifyhn_potion_purify · Purifying Elixir1× Moonpetal · 1× Mandrake Root · 1× Ghost Essence#cfe8ff
seerhn_potion_seer · Seer's Draught1× Ghost Essence · 1× Bone Dust · 1× Moonpetal#9b4dff
phantomhn_potion_phantom · Phantom Veil2× Ghost Essence · 1× Bone Dust#9ffff0
  • The player needs every ingredient. A successful brew (stirring minigame passed, or Minigame.Enabled = false) takes the ingredients, gives the potion and pays Config.Cauldron.Souls (2).
  • A failed brew gives Fail.Item (hn_sludge ×1). It also takes the ingredients while Fail.LoseIngredients = true (the default). Set Fail.Item = false to give nothing.
  • If the result doesn't fit in the inventory, the ingredients are given back.
  • The server allows one brew per player every Config.Cauldron.BrewTime ms (3500).
  • color is the accent of the recipe card in the UI (#rgb or #rrggbb).

Add a recipe

config/cauldron.lua (inside Config.Cauldron)
Recipes = { -- ...the 7 default recipes... big_vigor = { item = 'hn_potion_vigor', amount = 2, color = '#ff3b4e', ingredients = { { item = 'hn_mandrake_root', amount = 3 }, { item = 'hn_bone_dust', amount = 1 }, } }, }, -- a recipe only shows up if its id is in this list RecipeOrder = { 'owl', 'swift', 'feather', 'vigor', 'big_vigor', 'purify', 'seer', 'phantom' },

The result can be any item your inventory knows. If you brew a new item, add it to Config.Items for its label and description, and to Config.Potions if it should be usable.

Hallowed Night — Features & balancing

Soul Lantern Hunt

Mode = 'global' keeps ActiveCount shared lanterns on the map. The first player to collect one wins it, and a new one appears elsewhere after RespawnSeconds. Mode = 'personal' turns every location into a collectible that each player can claim once per season.

KeyDefaultNotes
ActiveCount20Global mode only
RespawnSeconds240Delay before a collected lantern reappears somewhere else
CursedChance0.15Cursed = more Souls + CursedLoot + a random curse
CursedLooksDifferenttruefalse hides which lanterns are cursed
Loot / CursedLoot—Each entry rolls separately: { chance, item | money, min, max }
Blips.EnabledfalseShows every lantern on the map (easy mode)

Anti-exploit: the server starts a timed action when the player presses E and only pays out if the full collect time has passed, the player is still close, and the lantern still exists.

Trick-or-Treat

Each door is a vec4, and the heading is the direction the NPC faces. The included doors are samples, so add your own with /hncoords. Per-door cooldown and a daily limit stop farming.

KeyDefaultNotes
TrickChance0.30Trick = curse + Souls.trick
DoorCooldown1800Seconds, per player per door
DailyLimit40Knocks per real day per player (0 = unlimited)
RequireItemfalsetrue = players need hn_candy_bucket
Treats—Weighted pick: { weight, item | money, min, max }

Curses

CurseEffectTime
levitateLifts the player into the air, then lowers them gently12s
haunted_sightClown-vision screen effect + spectral vignette25s
drunken_hexDrunk walk + camera sway30s
specterA ghost hunts the player. Kill it for Ghost Essence (server-verified token)40s
pumpkin_headPumpkin helmet that everyone nearby can see (synced through a player state bag, so it works with entity lockdown)60s
heavy_bonesSlow walk, no sprint or jump30s
clumsyRandom trips (ragdoll)25s
blackoutDarkness closes in on the screen20s

Only one curse at a time, followed by Config.CurseImmunityAfterCurse seconds of immunity. Curses flagged vehicle = false never land on players who are driving. The Purifying Elixir lifts all curses and grants immunity.

Witch's Cauldron & gathering

PotionIngredientsEffect
hn_potion_owl2 Moonpetal · 1 Raven FeatherNight vision, 90s
hn_potion_swift2 Pumpkin Seeds · 1 Raven FeatherSprint ×1.25, 60s
hn_potion_feather2 Raven Feather · 1 MoonpetalSlow fall, no fall damage, 120s
hn_potion_vigor2 Mandrake Root · 1 Pumpkin Seeds+60 health, +25 armour
hn_potion_purifyMoonpetal · Mandrake Root · Ghost EssenceLift curses + 180s immunity
hn_potion_seerGhost Essence · Bone Dust · MoonpetalReveals lanterns within 400m for 90s
hn_potion_phantom2 Ghost Essence · 1 Bone DustGhost form (translucent, no collision, weapons disabled), 30s

Brewing opens the cauldron UI. With Minigame.Enabled, players stir by pressing SPACE when the marker is in the glowing zone. Rounds, Speed, ZoneSize and MaxMisses set the difficulty. A failed brew gives Fail.Item (sludge) and, if Fail.LoseIngredients is on, uses up the ingredients.

Gathering nodes are shared: once someone gathers one it disappears for everyone until respawn seconds pass. Moonpetal is nightOnly (20:00–06:00 in-game).

Haunted Night

  1. Announce: a banner for everyone, plus a flashing blip and radius on the map for AnnounceSeconds.
  2. Waves: spirits spawn inside the zone. Each kill pays Souls and has a chance at Ghost Essence. The next wave starts WaveDelay seconds after the last spirit falls.
  3. Boss: The Hollow King (pumpkin head, battle axe) with a boss bar. A Soul Nova knocks back nearby players every Nova.interval seconds, and he summons spirits at 66% and 33% health. His HP grows with each extra participant, up to 8000 (GTA syncs ped health in 13 bits, so higher values would break).
  4. Victory: every participant still near the zone gets Rewards.Boss. If TimeLimit runs out first, the spirits retreat.

Start it on a schedule (Schedule, real server time) or with /hnhaunt start [zone]. Inside the zone, players get a client-side HALLOWEEN weather override, a fog timecycle and decorative tombstones.

Works with entity lockdown

Spirits are created by the server. One player inside the zone (the "host") only sends back ground positions so nothing spawns underground, and whoever owns each spirit runs its AI. Waves wait until at least one player is inside the zone.

Hallowed Night — Player guide

What a player sees and does in every activity, step by step. It's written so you can copy it into your own server guide or Discord. All numbers are the defaults from the config files, so if you change a value, update the matching line here too.

Getting started

  • When: the event runs during the Halloween season, by default from 15 October to 3 November (server date). Outside the season the lanterns, doors, gathering spots and cauldrons are gone, and actions answer with "The veil is closed. Come back during the Halloween season."
  • Your menu: type /halloween or press F7 to open the Grimoire (profile, leaderboard, rewards, recipes, curses).
  • Souls are the event score, and every activity gives some. Each time your Souls change, a small pill in the lower right of the screen shows your new total and how many you just gained.
  • Interacting: walk up to a lantern, door, plant or cauldron. A glowing prompt appears at the bottom of the screen (for example E Collect Soul Lantern). Press E. On servers that use a target eye (ox_target or qb-target), pick the option from the target menu instead.
  • Timed actions: collecting, knocking and gathering show a flaming progress ring that fills on its own. You stand still while it runs. Press X to cancel. Dying, getting ragdolled or entering a vehicle also cancels it.
  • You can't interact from inside a vehicle or while you're dead.

Where Souls come from

ActivitySoulsConfig key
Soul Lantern1Config.Lanterns.Souls.normal
Cursed Lantern3Config.Lanterns.Souls.cursed
Treat1Config.TrickOrTreat.Souls.treat
Trick2Config.TrickOrTreat.Souls.trick
Successful brew2Config.Cauldron.Souls
Specter banished2Config.Curses.specter.reward.souls
Haunted Night spirit2Config.Haunted.Rewards.Kill.souls
The Hollow King25Config.Haunted.Rewards.Boss.souls
Gathering0Config.Gathering.Nodes.<node>.souls

1. The Soul Lantern Hunt

  1. Find a lantern. Glowing soul lanterns are spread across the whole map: the city, the beaches, Sandy Shores, Grapeseed, Paleto Bay, even Mount Chiliad. By default 20 are out at the same time. They appear when you're within 80 m and give off a warm orange glow.
  2. Spot the cursed ones. A lantern with a purple glow and a crackling aura is cursed, and its prompt reads Collect Cursed Lantern. It gives more Souls and extra loot, but it also curses you. (Some servers hide which lanterns are cursed, so any lantern could be one.)
  3. Collect it. Get within 2 m and press E. Your character kneels while the ring fills for 2.5 seconds ("Drawing out the lantern's soul..."). Press X to stop.
  4. Get your reward. The lantern bursts into light and a reward card pops up in the middle of the screen with your Souls, items and cash.
  5. Cursed? About a second later the curse lands. See Curses below.
  6. Then it's gone. The lantern disappears for everyone, and a new one appears at a random spot about 4 minutes later. If another player finishes first, you get "Someone else claimed this lantern first."
LootChanceAmount
hn_pumpkin_seeds45%1–2 Pumpkin Seeds
hn_raven_feather25%1 Raven Feather
hn_candy30%1–2 Hallowed Candy
cash10%$50–150
hn_ghost_essence70% (cursed only)1 Ghost Essence
hn_bone_dust40% (cursed only)1–2 Bone Dust

Every line is rolled on its own, so one lantern can give several items, or none at all. A cursed lantern rolls the normal loot and the cursed loot.

Lantern tips

A Seer's Draught marks every lantern within 400 m on your map with a flashing blip for 90 seconds. On servers that run the hunt in personal mode, every lantern spot can be collected once per player per season: there's no race, and the lanterns you already took stay hidden for you only.

2. Trick-or-Treat

  1. Find a door. Doors are set by the server owner and have no marker by default. The sample doors are houses on Forum Drive, Grove Street, Mirror Park, the Vespucci Canals, Rockford Hills, Richman, Sandy Shores and Paleto Bay. Stand within about 2 m of the door and the Trick or Treat prompt appears.
  2. Knock. Press E. Your character turns to the door and knocks for 2.2 seconds ("Knocking...").
  3. Someone answers. A costumed neighbour appears in a puff of smoke, holding a candy bucket.
  4. Treat (70%): the neighbour hands something over and a "Treat!" card shows +1 Soul and one treat.
  5. Trick (30%): the neighbour laughs at you, a "Trick!" message appears, you get +2 Souls, and a moment later a curse lands.
  6. The neighbour vanishes again after 3.5 seconds.
TreatChanceAmount
hn_candy50%1–3 Hallowed Candy
hn_pumpkin_seeds20%1–2 Pumpkin Seeds
hn_raven_feather15%1 Raven Feather
cash10%$25–120
hn_moonpetal5%1 Moonpetal

Limits

  • Same door: 30 minutes between knocks. Too early and you get "Nobody answers... try again in 12m 30s."
  • Per day: 40 knocks per real day (server date). After that: "You've trick-or-treated enough for today."
  • Candy Bucket: some servers require a Candy Bucket to knock ("You need a Candy Bucket."). The bucket is the 500 Souls milestone reward.

3. Curses

Curses come from tricks and cursed lanterns. When one lands you get a "Cursed!" message with its name, and it appears in the effects list on the left side of the screen with its icon, a ring timer and the seconds left. Active potion buffs show up in the same list.

CurseWhat happensLasts
Hex of the Floating DeadUnseen hands lift you about 2.4 m into the air. You bob and slowly spin, then get lowered back down.12s
Haunted SightYour vision turns strange (clown-vision screen effect), the camera wobbles and a purple vignette that slowly shifts colour darkens the edges.25s
Drunken HexYou stagger as if very drunk and the camera sways.30s
Haunting SpecterA see-through ghost appears behind you and chases you. It hits for 8 damage when it reaches you. Kill it before the timer ends for 1 Ghost Essence and +2 Souls. It only exists on your screen, so nobody else can banish it for you.40s
Pumpkin HeadYour head becomes a pumpkin that everyone nearby can see. Harmless, and the only curse that can land while you're in a vehicle.60s
Heavy BonesYou move at 65% speed and can't sprint or jump.30s
Trickster's TripAn invisible imp trips you every 4–8 seconds. You'll hear it giggle.25s
Shroud of NightDarkness closes in from every side, leaving a small, pulsing circle of vision.20s

How a curse ends

  • The timer runs out: you get "<curse> has faded."
  • You die: dying lifts every curse (when Config.ClearCursesOnDeath is on, the default).
  • Purifying Elixir: drink it to lift every curse at once ("All curses have been lifted.") and resist new ones for 3 minutes.

You can only carry one curse at a time. After a curse lands, new ones are resisted until its full time plus 20 seconds has passed, even if it ended early. A resisted curse shows "Your purification repelled a curse." Tricks and cursed lanterns still give their Souls when the curse is resisted.

4. Gathering ingredients

  1. Find a plant or bone pile. Walk within about 2 m and the prompt reads Gather Mandrake, Gather Moonpetal or Gather Bone Pile.
  2. Press E. Your character kneels or plants while the ring fills ("Gathering Mandrake...").
  3. A "Gathered" message shows what you got, for example 2x Mandrake Root.
  4. The spot disappears for everyone until it grows back. If someone else got there first: "This has already been gathered."
SpotGivesTimeGrows backDefault spots
Mandrake1–2 Mandrake Root4s10 minGrapeseed fields, Pacific Bluffs cemetery, Marlowe Vineyard
Moonpetal1 Moonpetal3s8 minNight only (20:00–06:00 in-game), with a soft blue glow. Near the Galileo Observatory, the Alamo Sea dock and the Paleto Forest lumber yard, plus a few wilder spots
Bone Pile1–2 Bone Dust3.5s10 minPacific Bluffs cemetery, the Sandy Shores outskirts and a few countryside spots

Where every ingredient comes from

IngredientSources (default config)
Mandrake RootMandrake spots only
MoonpetalMoonpetal spots at night, rare treat
Bone DustBone piles, cursed lanterns
Ghost EssenceCursed lanterns, Haunting Specter, Haunted Night spirits and boss
Pumpkin SeedsLanterns, treats
Raven FeatherLanterns, treats

5. Brewing at the Witch's Cauldron

  1. Find a cauldron. Look for the Witch's Cauldron blip on the map. Each one glows green and has a witch waiting beside it. The default cauldrons are at the Pacific Bluffs cemetery, in Grapeseed and in Paleto Bay.
  2. Open it. Press E on Use Witch's Cauldron.
  3. Pick a recipe. The list on the left shows every potion. A green dot means you have everything for it right now. The right side shows the potion, its description and each ingredient as have / need. Missing ones are marked in red.
  4. Brew. Click Brew (if you're missing something, the button reads Missing ingredients instead).
  5. Stir the brew. A white marker runs around a ring with a glowing green zone on it. Press SPACE (or click on the stirring window) while the marker is inside the zone. You need 3 perfect stirs. Every perfect stir moves the zone and speeds the marker up. Your third miss ruins the brew. Press Escape to stop stirring without brewing: nothing is used up.
  6. Wait. A bubbling pot shows "Brewing..." for 3.5 seconds while your character stirs.
  7. Result. Success gives the potion and +2 Souls, with a burst of magic from the cauldron. A failed brew gives Cauldron Sludge, and the ingredients are lost.

The ingredient counts refresh after every brew, so you can brew again straight away. Close the window with the X button or Escape (not while a brew is in progress).

Recipes

PotionIngredientsWhen you drink it
Owl's Eye Elixir2 Moonpetal · 1 Raven FeatherNight vision for 90s
Swiftfoot Draught2 Pumpkin Seeds · 1 Raven FeatherYou sprint 25% faster for 60s
Featherfall Tonic2 Raven Feather · 1 MoonpetalFor 120s your fall speed is capped, so you drift down instead of dropping
Elixir of Vigor2 Mandrake Root · 1 Pumpkin Seeds+60 health and +25 armour, instantly
Purifying Elixir1 Moonpetal · 1 Mandrake Root · 1 Ghost EssenceLifts every curse and resists new ones for 180s
Seer's Draught1 Ghost Essence · 1 Bone Dust · 1 MoonpetalFlashing map blips on every lantern within 400 m for 90s
Phantom Veil2 Ghost Essence · 1 Bone DustFor 30s you turn see-through and ghosted to other players, and you can't shoot, aim or punch

Other usable items

ItemWhen you use it
Hallowed Candy+5 health and a 10-second sugar rush (sprint ×1.15)
Cauldron SludgeYou throw up, take 10 damage and the screen shakes. "Why would you drink that?"

Drinking potions

  • Use the potion from your inventory. On servers without an inventory, open the Grimoire's Satchel tab and click Use.
  • Your character drinks for about 2 seconds (no animation inside a vehicle), then "You drank the <potion>." appears.
  • Timed potions show up in the effects list on the left with a ring timer. When one runs out: "<potion> wore off."
  • Drinking the same potion again while it's active restarts its timer. The effects don't stack.

6. Haunted Night

A server-wide event. By default it starts at 20:00 and 23:30 server time during the season (when at least one player is online), or whenever an admin starts it. The Grimoire's profile tab shows when the next one is due.

  1. The warning. Everyone gets a banner, "The Veil Thins: Spirits gather at Pacific Bluffs Cemetery in 2m 00s", with a deep bell. A flashing blip and a coloured circle mark the zone on the map.
  2. Head to the zone. Inside the circle the weather turns to Halloween with heavy fog, a low drone plays and tombstones rise around the area. A panel on the right side of the screen shows the zone name, Wave, Spirits left and Time left (during the warning, that's the countdown to the start).
  3. It begins. Banner: "Haunted Night: Defend <zone> from the restless dead!" The blip stops flashing and you have 15 minutes. (Only the warning banner goes to everyone. This one and the later banners show for players within about 600 m of the zone, and the wave messages within about 400 m.)
  4. Three waves of spirits. A "Wave 1 / 3" message, then 6, 9 and 12 ghostly, staggering spirits attack. Whoever lands the killing blow gets "Spirit banished", +2 Souls and a 35% chance of Ghost Essence. The next wave starts 10 seconds after the last spirit falls. Spirits only spawn while at least one player is in or near the zone, but the clock keeps running.
  5. The Hollow King. Banner: "The Hollow King Rises: Banish him before the veil tears!" A boss bar with his name and health appears at the top of the screen for players inside the zone. He has a pumpkin head, a battle axe and armour, and he doesn't ragdoll. His health grows with the crowd: 4000, plus 1200 for every participant after the first, up to 8000.
    • Soul Nova: every 18 seconds he releases a shockwave. Anyone within 7 m is knocked over and takes 15 damage. Within 60 m you get a warning, "The Hollow King unleashes a Soul Nova!" Fight from range or keep your distance after you strike.
    • Summons: at 66% and at 33% health he calls 4 more spirits around himself.
  6. Victory. Banner: "The Veil Is Sealed: The Hollow King has fallen." Every eligible participant (see the box below) gets a reward card.
  7. Defeat. If the 15 minutes run out first: "The Spirits Retreat: The veil closes... for now." There are no boss rewards, but you keep what you earned from spirit kills.
Boss rewardChanceAmount
Souls100%25
hn_ghost_essence100%2–3 Ghost Essence
hn_potion_phantom60%1 Phantom Veil
hn_candy50%3–6 Hallowed Candy
cash100%$1,500–3,500
Who counts as a participant?

Anyone who was inside the zone (or within 30 m of its edge) at any moment after the fight started, and who is still online and within 60 m of the zone's edge when the Hollow King falls. Leave early and you miss the boss rewards.

7. The Grimoire

Open it with /halloween or F7. Your Souls total is always shown in the header. Close it with the X button or Escape.

TabWhat's inside
ProfileYour name, whether the season is on ("The veil is thin" or "The veil is closed"), your Souls, your rank among everyone who has Souls this season, eight stat cards (Lanterns, Cursed Lanterns, Treats, Tricks, Potions Brewed, Ingredients Gathered, Spirits Banished, Kings Slain) and the Haunted Night card: next start time, Spirits gathering at during the warning, or Haunted Night is LIVE at while it runs
LeaderboardThe top 10 players by Souls this season. The top 3 get gold, silver and bronze badges, and your own row is highlighted. It refreshes at most once a minute
MilestonesA progress bar towards the last milestone and one card per milestone with its rewards. The button reads Claim when you have enough Souls, Claimed afterwards, or Locked
RecipesEvery cauldron recipe with its ingredients, handy while you're out gathering
CursesKnown Curses: every curse that is enabled on the server, what it does and how long it lasts
SatchelOnly on servers without an inventory. Your Halloween items with their amounts, and a Use button on potions, candy and sludge

Milestones

SoulsTitleRewards
25Wandering Spirit5 Hallowed Candy · $500 cash
75Lantern Keeper2 Swiftfoot Draught · $1,500 cash
150Hex Weaver2 Purifying Elixir · 2 Seer's Draught
300Veil Walker2 Phantom Veil · $5,000 bank
500Hollow Sovereign1 Candy Bucket · $15,000 bank

Each milestone can be claimed once per season, as soon as your Souls total reaches it. If your inventory can't hold the items you get "You can't carry that." and nothing is given, so make room and claim again. A "Milestone Claimed" card shows what you received.

The screen at a glance

ElementWhereShows
PromptBottom centreThe key to press and the action
Progress ringBottom centreA timed action, with the cancel key (X)
MessagesTop rightShort notifications, up to 5 stacked
Souls pillLower rightYour Souls total and the latest gain, for a few seconds
Effects listLeft sideActive curses and potion buffs with ring timers
Reward cardCentreSouls, items and cash you just earned (several rewards play one after another)
BannerTop centreHaunted Night announcements
Haunted Night panelRight sideZone, wave, spirits left, time left (only inside the zone)
Boss barTop centreThe Hollow King's name and health

Player FAQ

  • I can't find anything, and the Grimoire says "The veil is closed". The Halloween season isn't active on the server right now.
  • The prompt doesn't show up. Get out of your vehicle, and make sure you aren't dead or already doing something. Prompts are hidden while a menu is open.
  • Moonpetal is nowhere to be found. It only blooms between 20:00 and 06:00 in-game. Come back at night.
  • "You can't carry that." Your inventory is full or too heavy. Make room and try again.
  • The door won't open. You knocked there in the last 30 minutes, or you reached today's 40 knocks. The message tells you which.
  • I didn't get Haunted Night rewards. You have to be near the zone when the Hollow King falls, and you must have been inside it during the fight.

Hallowed Night — Customization tutorials

Practical recipes for the most common changes. Everything here happens in files that stay open after escrow: config/, locales/, editable/, bridge/ and html/ (tutorial 8 also adds one line to fxmanifest.lua). Config files are read when the resource starts, so run restart rklhalloween (or restart the server) after every change. New or changed streamed files need a full server restart.

1. Add your own lantern, door, node and cauldron spots

  1. Go to the spot in game. Stand on the ground there (don't save a spot while flying or in noclip).
  2. Type /hncoords. It copies vec4(x, y, z, heading) to your clipboard, prints the same line in the F8 console and shows "Coordinates copied to clipboard."
  3. Paste it into the right list below, in the format that list uses.
WhatListFormatNotes
LanternConfig.Lanterns.Locationsvec3Drop the heading. The lantern's rotation is set automatically and its height snaps to the ground
DoorConfig.TrickOrTreat.Doorsvec4The heading is the direction the neighbour faces (see below)
Gathering nodeConfig.Gathering.Nodes.<node>.pointsvec3Drop the heading. Height snaps to the ground
CauldronConfig.Cauldron.Locationsvec4The heading rotates the cauldron, and the witch stands at Witch.Offset relative to it, so the heading also decides her side
Haunted zoneConfig.Haunted.Zones[n].centervec3Plus a radius. See tutorial 6
config/lanterns.lua
Locations = { vec3(195.17, -933.77, 30.69), -- Legion Square -- ... the existing spots ... -- /hncoords gave vec4(-1150.25, -1525.10, 4.35, 212.4): keep x, y, z only vec3(-1150.25, -1525.10, 4.35), -- my new spot },

Ground snapping

Lanterns, gathering nodes, cauldrons and tombstones are props. The pack looks for the ground just above the saved height (2 m, then 10 m, then 50 m higher) and settles the prop on it, so standing on the spot is enough. The server still checks the player's distance against the saved position (it allows the interact distance plus a little extra sideways, and roughly 15 m more than that up or down), so a spot saved far above or below where players stand ends in "You are too far away." Doors and zone centres aren't props: they use the height exactly as copied.

Door heading

When a player knocks, they're turned to face heading + 180 (towards the door), and the neighbour appears 1.15 m in front of them, facing back at them. The easy way: stand on the doorstep with your back to the door, facing where the neighbour should look, and run /hncoords. If you copied it while facing the door, add 180 to the heading (subtract 360 if the result is above 360).

config/trickortreat.lua
Doors = { vec4(-14.30, -1441.20, 31.10, 180.0), -- Forum Drive -- ... the existing doors ... vec4(-1150.25, -1525.10, 4.35, 212.4), -- copied with my back to the door },

A whole new gathering node type

Every entry in Config.Gathering.Nodes becomes a node type, so you can add your own from the config alone. It needs the same keys as the built-in ones. Any vanilla prop works as the model.

config/gathering.lua — inside Nodes
pumpkins = { label = 'Pumpkin Patch', -- prompt: "Gather Pumpkin Patch" model = 'prop_veg_crop_03_pump', fallbackModel = 'prop_veg_crop_03_pump', item = 'hn_pumpkin_seeds', min = 2, max = 3, time = 3000, -- ms respawn = 300, -- seconds nightOnly = false, anim = { dict = 'amb@world_human_gardener_plant@male@base', clip = 'base' }, souls = 0, -- light = { 255, 140, 40 }, -- optional glow colour points = { vec3(-1150.25, -1525.10, 4.35), }, },
Add new spots at the end of a list

Spots are remembered by their position in the list. In personal lantern mode, each player's collected lanterns are saved by that number, so removing or reordering lanterns in the middle of a season changes which spots players have already collected. Adding to the end is always safe.

2. Change rewards and loot tables

There are two kinds of loot table:

  • Rolled (chance): every line is rolled on its own, chance is a percentage from 0 to 100 (100 if you leave it out). Used by Config.Lanterns.Loot, Config.Lanterns.CursedLoot and the Haunted Night items lists.
  • Weighted (weight): exactly one line is picked, with odds equal to its weight divided by the total. Used by Config.TrickOrTreat.Treats.

Each line gives either an item or money ('cash' or 'bank') with a random amount from min to max. A fixed amount works too.

config/lanterns.lua
Loot = { { chance = 45, item = 'hn_pumpkin_seeds', min = 1, max = 2 }, { chance = 25, item = 'hn_raven_feather', min = 1, max = 1 }, { chance = 30, item = 'hn_candy', min = 1, max = 2 }, { chance = 10, money = 'cash', min = 50, max = 150 }, { chance = 5, item = 'hn_mandrake_root', min = 1, max = 1 }, -- new: rare Mandrake }, CursedLoot = { { chance = 70, item = 'hn_ghost_essence', min = 1, max = 1 }, { chance = 40, item = 'hn_bone_dust', min = 1, max = 2 }, { chance = 15, money = 'bank', min = 250, max = 500 }, -- new },
config/trickortreat.lua
Treats = { { weight = 50, item = 'hn_candy', min = 1, max = 3 }, { weight = 20, item = 'hn_pumpkin_seeds', min = 1, max = 2 }, { weight = 15, item = 'hn_raven_feather', min = 1, max = 1 }, { weight = 5, item = 'hn_moonpetal', min = 1, max = 1 }, { weight = 10, money = 'cash', min = 25, max = 120 }, { weight = 2, item = 'hn_potion_swift', amount = 1 }, -- new: about 2 in every 102 treats },
config/haunted.lua
Rewards = { Kill = { souls = 2, items = { { chance = 35, item = 'hn_ghost_essence', min = 1, max = 1 } } }, Boss = { souls = 25, items = { { chance = 100, item = 'hn_ghost_essence', min = 2, max = 3 }, { chance = 60, item = 'hn_potion_phantom', min = 1, max = 1 }, { chance = 50, item = 'hn_candy', min = 3, max = 6 }, }, money = { account = 'cash', min = 1500, max = 3500 }, -- remove the line for no money }, },

Every reward key

KeyDefaultGives
Config.Lanterns.Souls{ normal = 1, cursed = 3 }Souls per lantern
Config.Lanterns.Loot / CursedLootsee aboveRolled lantern loot
Config.TrickOrTreat.Souls{ treat = 1, trick = 2 }Souls per knock
Config.TrickOrTreat.Treatssee aboveOne weighted treat per Treat
Config.Cauldron.Souls2Souls per successful brew
Config.Gathering.Nodes.*.souls0Souls per gather (0 = none)
Config.Curses.specter.reward{ item = 'hn_ghost_essence', amount = 1, souls = 2 }Banishing the Specter
Config.Haunted.Rewards.Killsee aboveEach spirit, for the killer
Config.Haunted.Rewards.Bosssee aboveEvery participant on victory
Config.Milestones5 tiersSee tutorial 3
Items and money

Any item you hand out must exist in your inventory (and in Config.Items if you want a proper label and image in the Halloween UI). On ESX, 'cash' is paid into the money account. On standalone servers there's no money system: money is only paid if you implement Hooks.StandaloneAddMoney in editable/server/hooks.lua and return true.

3. Add or edit a milestone

Milestones live in Config.Milestones (config/config.lua). Each one has the Souls needed, a title and a list of rewards: { type = 'item', name = 'x', amount = 1 } or { type = 'money', account = 'cash' | 'bank', amount = 100 }.

config/config.lua
Config.Milestones = { { souls = 25, label = 'Wandering Spirit', rewards = { { type = 'item', name = 'hn_candy', amount = 5 }, { type = 'money', account = 'cash', amount = 500 } } }, { souls = 75, label = 'Lantern Keeper', rewards = { { type = 'item', name = 'hn_potion_swift', amount = 2 }, { type = 'money', account = 'cash', amount = 1500 } } }, { souls = 150, label = 'Hex Weaver', rewards = { { type = 'item', name = 'hn_potion_purify', amount = 2 }, { type = 'item', name = 'hn_potion_seer', amount = 2 } } }, { souls = 300, label = 'Veil Walker', rewards = { { type = 'item', name = 'hn_potion_phantom', amount = 2 }, { type = 'money', account = 'bank', amount = 5000 } } }, { souls = 500, label = 'Hollow Sovereign', rewards = { { type = 'item', name = 'hn_candy_bucket', amount = 1 }, { type = 'money', account = 'bank', amount = 15000 } } }, -- new 6th tier { souls = 750, label = 'Lord of the Veil', rewards = { { type = 'item', name = 'hn_potion_purify', amount = 5 }, { type = 'item', name = 'hn_potion_vigor', amount = 5 }, { type = 'money', account = 'bank', amount = 25000 }, } }, }
  • Keep them in ascending order. The Grimoire's progress bar runs up to the last milestone's Souls.
  • Add new tiers at the end. Claims are saved by position (1st, 2nd, ...). Inserting a tier in the middle of a season shifts every claim after it, so players could claim a tier twice or lose one.
  • Items are checked first. If any item doesn't fit, the player gets "You can't carry that." and nothing is given (items already given are taken back). Money is paid after the items.
  • Milestones use the player's current Souls total. Removing Souls with /hnsouls doesn't undo claims that were already made.

4. Add a new potion recipe

A new potion needs four things: a UI label, an inventory item, an effect and a recipe. The effect must use one of the existing effect types in Config.Potions. New effect types need code in the escrowed client, so they can't be added from the config.

Effect typeKeysDoes
nightvisiondurationNight vision
swiftduration, multiplierFaster sprint (the multiplier is capped at 1.49)
featherfallduration, maxFallSpeedCaps downward speed while falling
phantomduration, alpha, disableWeaponsTranslucent, ghosted player, optional no-weapons
vigorheal, armorInstant health (up to max) and armour
purifyimmunityLifts curses, then immunity for N seconds
seerduration, radiusMap blips on lanterns within the radius
candyheal, sugarRushHeal plus a sugarRush-second sprint ×1.15
sludgedamageVomit animation and damage

Example: a Greater Vigor potion that heals more than the normal one.

Step 1: label and description for the Halloween UI

config/items.lua — inside Config.Items
hn_potion_greater_vigor = { label = 'Greater Vigor', description = 'A thick red draught that knits bones in seconds.' },

Step 2: register the item in your inventory

ox_inventory/data/items.lua
["hn_potion_greater_vigor"] = { label = "Greater Vigor", weight = 150, stack = true, close = true, server = { export = 'rklhalloween.useItem' }, description = "A thick red draught that knits bones in seconds." },
qb-core/shared/items.lua
hn_potion_greater_vigor = { name = 'hn_potion_greater_vigor', label = "Greater Vigor", weight = 150, type = 'item', image = 'hn_potion_greater_vigor.png', unique = false, useable = true, shouldClose = true, description = "A thick red draught that knits bones in seconds." },
ESX — SQL
INSERT IGNORE INTO `items` (`name`, `label`, `weight`, `rare`, `can_remove`) VALUES ('hn_potion_greater_vigor', 'Greater Vigor', 3, 0, 1);

With the built-in satchel (Config.Inventory = 'internal') there's nothing to register. Add an image named hn_potion_greater_vigor.png to html/assets/items/ for the Halloween UI (or set image in Config.Items), and to your inventory's image folder.

Step 3: the effect

config/potions.lua — inside Config.Potions
hn_potion_greater_vigor = { effect = 'vigor', heal = 100, armor = 50 },

With qb-inventory (on QBCore or Qbox) or the ESX default inventory, every key in Config.Potions is registered as a usable item automatically through the framework. ox_inventory calls the pack through the server = { export = 'rklhalloween.useItem' } line from step 2, and the satchel shows a Use button for it.

Step 4: the recipe

config/cauldron.lua
Recipes = { -- ... the existing recipes ... greatervigor = { item = 'hn_potion_greater_vigor', amount = 1, color = '#ff7a1a', ingredients = { { item = 'hn_mandrake_root', amount = 2 }, { item = 'hn_ghost_essence', amount = 1 }, { item = 'hn_bone_dust', amount = 1 }, } }, }, RecipeOrder = { 'owl', 'swift', 'feather', 'vigor', 'greatervigor', 'purify', 'seer', 'phantom' },

Only recipes listed in RecipeOrder show up in the cauldron and in the Grimoire's Recipes tab. color tints the recipe card and the potion's glow in the UI. Timed buffs are tracked per effect type, so two potions with the same effect share one timer: drinking one while the other is active only restarts the timer.

5. Disable or tune curses

Each curse in Config.Curses (config/curses.lua) has the same base keys:

KeyDoes
enabledfalse = never picked, and hidden from the Grimoire's Curses tab
weightOdds compared to the other enabled curses (defaults 10 to 14)
durationSeconds
vehicleCan it land while the player is in a vehicle? Only pumpkin_head is true by default
label / description / iconWhat the HUD and Grimoire show
CurseExtra keys
levitateheight = 2.4 (metres lifted)
haunted_sightscreenEffect = 'DrugsTrevorClownsFight'
spectermodel, health = 150, damage = 8 per hit, reward = { item, amount, souls }
pumpkin_headmodel, fallbackModel, bone, offset (tweak x between -0.06 and -0.09 to raise or lower the pumpkin)
heavy_bonesmoveRate = 0.65
clumsyinterval = { 4000, 8000 } ms between trips
config/curses.lua
levitate = { enabled = false, -- never picked weight = 10, duration = 12, vehicle = false, -- ... keep the other keys ... }, blackout = { enabled = true, weight = 4, duration = 10, vehicle = false, -- rarer and shorter label = 'Shroud of Night', description = 'Darkness closes in from every side.', icon = 'moon', }, -- below Config.Curses Config.CurseImmunityAfterCurse = 60 -- default 20: longer break between curses
  • No curses at all: set every curse to enabled = false. Tricks still pay their Souls, and cursed lanterns their Souls and loot. For no tricks at all, set Config.TrickOrTreat.TrickChance = 0.0; for no cursed lanterns, Config.Lanterns.CursedChance = 0.0.
  • Keep curses after death: Config.ClearCursesOnDeath = false in config/config.lua.
  • New curse types can't be added from the config: each curse name needs its own client code. A new name in Config.Curses would be picked by the server but do nothing in game.

6. Add a Haunted Night zone and change the schedule

Zones are a list in Config.Haunted.Zones. The first zone is 1, so a fourth zone starts with /hnhaunt start 4. Scheduled starts pick a random zone.

config/haunted.lua
Schedule = { { hour = 21, minute = 0 }, -- real server time, 24h }, MinPlayers = 4, -- online players needed for a scheduled start AnnounceSeconds = 180, -- warning before it starts TimeLimit = 20 * 60, -- seconds before the spirits retreat Zones = { { label = 'Pacific Bluffs Cemetery', center = vec3(-1735.00, -220.00, 56.00), radius = 80.0 }, { label = 'Grapeseed Fields', center = vec3(2010.00, 4890.00, 42.80), radius = 90.0 }, { label = 'Sandy Shores Outskirts', center = vec3(1600.00, 3600.00, 35.00), radius = 90.0 }, -- zone 4: x, y, z from /hncoords (drop the heading). Test it with /hnhaunt start 4 { label = 'Marlowe Vineyard', center = vec3(-1890.00, 2070.00, 140.50), radius = 80.0 }, },
  • Admin-only: Schedule = {}. Then only /hnhaunt start [zone] or the StartHaunt export starts it.
  • Scheduled starts only happen during the season, when nothing else is running and at least MinPlayers are online. Admin starts work any time, even out of season.
  • Pick open, flat ground on land. Spirits spawn on ground points inside 80% of the radius, 5 to 90 m from the host player (the player nearest the centre), within 25 m of the centre's height and never in water. The zone check uses the centre's height too, so save it standing at ground level.

Waves, spirits and boss

KeyDefaultNotes
Waves{ { count = 6 }, { count = 9 }, { count = 12 } }Add or remove entries. The HUD shows Wave n / total
WaveDelay10Seconds between waves
MaxAlive14Safety cap on spirits alive at once
ParticipantRadius30.0Extra metres outside the zone that still count as taking part
SpiritsHealth = 250, Alpha = 170Also Models (random pick), Armor, Accuracy, MoveClipset
Boss.Health / HealthPerPlayer4000 / 1200Capped at 8000 (GTA syncs ped health in 13 bits)
Boss.Nova{ interval = 18, radius = 7.0, damage = 15 }Soul Nova timing and reach
Boss.Summon{ atHealth = { 0.66, 0.33 }, count = 4 }Health fractions that trigger a summon
Boss.Label / Model / Weapon'The Hollow King'Also HeadModel and HeadFallback
VisualsWeather = 'HALLOWEEN'Timecycle, TimecycleStrength, Tombstones, TombstoneCount. '' turns weather or fog off
Blipsprite 484, colour 27Map blip and radius colour/alpha
Renaming the boss

The Soul Nova warning is a locale string with the name written in: haunt_nova = 'The Hollow King unleashes a Soul Nova!'. Update it in your locale file when you change Boss.Label.

your event script — server
-- start zone 4 from your own scheduler local ok, msg = exports['rklhalloween']:StartHaunt(4) if not ok then print(msg) end -- e.g. 'A Haunted Night is already running.'

7. Run the lantern hunt in personal (collectible) mode

In 'personal' mode every location in Locations is a collectible: each player can claim each spot once per season, at their own pace, with no race. Lanterns a player already took stay hidden for that player only.

config/lanterns.lua
Config.Lanterns = { Enabled = true, Mode = 'personal', -- ActiveCount and RespawnSeconds are ignored in personal mode CursedChance = 0.15, CursedLooksDifferent = true, Blips = { Enabled = true, -- optional: a map blip on every lantern the player still needs Sprite = 484, Color = 47, Scale = 0.6, }, -- ... the rest of the file unchanged ... }
  • Which lanterns are cursed is fixed per location in personal mode (worked out from the spot's number and CursedChance), so the same spots are cursed for everyone.
  • Progress is saved with the player's season data. A new Config.Season.Id resets the collection.
  • /hnlanterns respawn is meant for global mode. In personal mode every location is always listed anyway.
  • Add plenty of locations: with 32 spots, a dedicated player finishes quickly. Remember to add new spots at the end of the list (see tutorial 1).

8. Add a new language

  1. Copy locales/en.lua to a new file, for example locales/de.lua.
  2. Change the first table name from Locales['en'] to Locales['de'], and translate every value. Keep the keys and the %s / %d placeholders, in the same order.
  3. Add the file to shared_scripts in fxmanifest.lua. Locale files are listed one by one there, not with a wildcard.
  4. Set Config.Locale = 'de' in config/config.lua.
locales/de.lua
Locales = Locales or {} Locales['de'] = { too_far = 'Du bist zu weit weg.', -- was: 'You are too far away.' door_cooldown = 'Niemand macht auf... versuche es in %s erneut.', -- ... every other key ... ui = { grimoire_title = 'Grimoire der Geweihten Nacht', -- ... every key of the ui block ... }, }
fxmanifest.lua
shared_scripts { -- ... the config files ... 'locales/en.lua', 'locales/tl.lua', 'locales/de.lua', -- new 'shared/_init.lua', }
Translate the whole ui block

Keys you leave out of the main list fall back to English. The ui block is used as a whole though: if your file has a ui table, a key missing from it is not filled in from en.lua (the UI then shows the key name, or a built-in English default in a few places).

Some player-facing text lives in the config files instead of the locale: item labels and descriptions (Config.Items), curse labels and descriptions (Config.Curses), milestone titles, gathering node labels, zone labels, Config.Haunted.Boss.Label, Config.Cauldron.Blip.Label and Config.Souls.Label. Translate those too.

9. Restyle the UI

The UI is plain HTML, CSS and JavaScript in html/, never encrypted. Most colours come from CSS variables at the top of html/style.css:

VariableDefaultUsed for
--gold / --gold-2#f2b84b / #ffdf9aMain accent: titles, key caps, borders, progress icon
--ember / --ember-2#ff7a1a / #ffa24dSouls icons, prompt icon, lantern and treat cards, boss-name icons
--violet / --violet-2#9b5cff / #c9a5ffCurses, cursed and ghost cards, info messages
--ghost#8ff5e6Potion buffs in the effects list
--blood#ff3b4eErrors, missing ingredients, boss banner, live event
--moss#5dff8fSuccess, stir zone, brew results, season "on"
--text / --muted#f3e9d7 / #a893bfMain and secondary text
--line / --line-stronggold, 22% / 55%Thin borders
--title / --heading / --bodyCinzel Decorative / Cinzel / Crimson ProFonts (files in html/assets/fonts/)
--radius / --shadow14px / soft drop shadowCorner radius and shadows
html/style.css
:root { --gold: #7fe0ff; /* icy blue instead of gold */ --gold-2: #c9f3ff; --ember: #3fa9ff; --ember-2: #7cc6ff; --radius: 8px; } /* move the notifications down, e.g. below your own HUD */ #toasts { top: 120px; }
  • Panel frame and background: the Grimoire and cauldron use gradients written directly in the .panel and .panel::before rules. --night, --night-2 and --night-3 are defined but the current rules don't use them.
  • Positions: #prompt and #progress (bottom centre), #toasts (top right), #souls-pill (lower right), #effects (left), #banner and #boss (top centre), #haunt (right), #reward (centre).
  • Progress ring colours: the <linearGradient id="flame"> in html/index.html.
  • Item images: html/assets/items/<item>.png, or set image on the item in Config.Items.
  • Potion colours: color on each recipe in Config.Cauldron.Recipes.

10. Change commands, keys and the admin permission

config/config.lua
Config.InteractKey = { control = 38, label = 'E' } -- prompt mode only; label is what the prompt shows Config.CancelKey = { control = 73, label = 'X' } -- cancels timed actions Config.Menu = { Command = 'grimoire', -- /grimoire instead of /halloween Key = 'F6', -- default keybind, '' = no keybind } Config.Admin = { Ace = 'hallowed.admin', Commands = { haunt = 'hnhaunt', -- /hnhaunt start [zone] | /hnhaunt stop souls = 'hnsouls', -- /hnsouls [playerId] [amount] coords = 'hncoords', -- copies your position as vec4(...) lanterns = 'hnlanterns', -- /hnlanterns respawn }, }
  • control is a GTA control index and label is only the text on screen, so change both together.
  • Players can rebind the Grimoire key in Settings → Key Bindings → FiveM (Hallowed Night - Grimoire). FiveM keeps a player's binding once it exists, so a new default Key mainly reaches players who never had one.
  • Admin commands need the ace in Config.Admin.Ace (the server console is always allowed). If you rename the ace, rename it in server.cfg too. /hncoords is a client command and works for everyone.
  • Stirring always uses SPACE (or a click). That key lives in html/app.js, and its hint text is the stir_hint locale string.
server.cfg
add_ace group.admin hallowed.admin allow # or give it to a single moderator group add_ace group.mod hallowed.admin allow

11. Switch to ox_target or qb-target

config/config.lua
Config.Interaction = 'ox_target' -- 'prompt' | 'ox_target' | 'qb-target' | 'auto'

'auto' uses ox_target if it's running, then qb-target, and otherwise the Halloween prompt. Detection happens when the resource starts, so ensure your target resource before rklhalloween in server.cfg.

PointTarget typeIconDistance key
LanternEntity (the prop)fa-solid fa-ghostConfig.Lanterns.InteractDistance (2.0)
DoorSphere, radius 1.2 at the doorfa-solid fa-candy-caneConfig.TrickOrTreat.InteractDistance (1.8)
Gathering nodeEntity (the prop)fa-solid fa-seedlingConfig.Gathering.InteractDistance (1.8)
CauldronEntity (the prop)fa-solid fa-flaskConfig.Cauldron.InteractDistance (2.2)

Target options use the same labels as the prompt (from the locale) and the same checks: nothing shows while the player is busy, dead or in a vehicle, or while ClientHooks.CanInteract() returns false. The progress ring and the X cancel key still work. Set Config.Debug = true to see the door spheres while you test.

12. Start a fresh season

config/config.lua
Config.Season = { AlwaysOn = false, Start = { month = 10, day = 15 }, -- server local date Finish = { month = 11, day = 3 }, Id = '2027', -- was '2026': new leaderboard and progress }

All progress is stored in server KVP under the season id: hn:<Id>:p:<identifier> (the player record) and hn:<Id>:s:<identifier> (the leaderboard summary). A new Id starts everyone from zero. The old data stays where it is, so switching back to the old id brings it back.

DataWith a new Id
Souls, stats, rankStart from zero (new leaderboard)
Milestone claimsEvery milestone can be claimed again
Personal lanternsEvery spot can be collected again
Daily knock counterReset
Halloween SatchelEmpty (see below)
Inventory items, moneyKept: they live in your framework, not in the pack
Old season dataKept in KVP, and back if you switch to the old Id
  • Satchel warning: with Config.Inventory = 'internal' the satchel is part of the player record, so a new season id also empties it.
  • The dates use the server's local date. A season can cross New Year (for example December to January). The season state is re-checked every 5 minutes.
  • AlwaysOn = true ignores the dates, handy for testing.
  • Progress is saved per character on Qbox, QBCore and ESX, and per license on standalone.

13. Make brewing easier or harder

config/cauldron.lua
BrewTime = 3500, -- ms animation after the minigame Souls = 2, -- souls per successful brew Minigame = { Enabled = true, -- false = no minigame, every brew succeeds Rounds = 3, -- perfect stirs needed Speed = 1.0, -- marker speed multiplier ZoneSize = 0.18, -- share of the ring that counts (0.05 - 0.5) MaxMisses = 2, -- the brew fails on miss number MaxMisses + 1 }, Fail = { LoseIngredients = true, -- false = keep the ingredients on a failed brew Item = 'hn_sludge', Amount = 1, -- Item = false gives nothing },
  • The marker goes round at 150 degrees per second times Speed, and each perfect stir makes it 14% faster and moves the zone.
  • ZoneSize = 0.18 is about 65 degrees of the ring. The UI clamps it between 18 and 180 degrees.
  • With LoseIngredients = false the player keeps the ingredients and still gets Fail.Item, so set Item = false too if you don't want free sludge.

14. Turn modules and helpers on or off

KeyDefaultTurns off
Config.Lanterns.EnabledtrueThe lantern hunt
Config.TrickOrTreat.EnabledtrueTrick-or-treat doors
Config.Gathering.EnabledtrueGathering nodes
Config.Cauldron.EnabledtrueCauldrons and brewing
Config.Haunted.EnabledtrueHaunted Night
Config.TrickOrTreat.Npc.EnabledtrueThe neighbour who opens the door (results still work)
Config.Cauldron.Witch.EnabledtrueThe witch next to each cauldron
Config.Cauldron.Blip.EnabledtrueCauldron map blips
Config.Lanterns.Blips.Enabledfalsetrue = a blip on every lantern (easy mode)
Config.TrickOrTreat.Blips.Enabledfalsetrue = a blip on every door
Config.TrickOrTreat.RequireItemfalsetrue = players need RequiredItem (hn_candy_bucket) to knock
Keep every ingredient obtainable

With the default config, Mandrake Root comes only from gathering nodes. If you turn gathering off, add hn_mandrake_root to a loot table (tutorial 2), or the Elixir of Vigor and Purifying Elixir can't be brewed. Ghost Essence comes from cursed lanterns, the Specter curse and Haunted Night, so turning all of those off blocks the Purifying Elixir, Seer's Draught and Phantom Veil.

15. Plug the pack into your server with hooks

The hook files stay open after escrow. A few common uses:

editable/server/hooks.lua
-- Discord logs for every rewarded action ('' = off) Hooks.Webhook = 'YOUR_DISCORD_WEBHOOK_URL' -- Block players from every Halloween activity function Hooks.CanParticipate(src) if Player(src).state.jailed then return false end -- example: a state bag your jail script sets return true end -- Bonus for everyone who helped kill the Hollow King function Hooks.OnBossDefeated(participants) for _, src in ipairs(participants) do Bridge.AddItem(src, 'hn_potion_purify', 1) end end -- Mirror Souls into another system function Hooks.OnSoulsChanged(src, identifier, total, delta, reason) -- reason: 'lantern', 'treat', 'trick', 'brew', 'gather', 'specter', 'spirit', 'boss', 'admin', or the reason given to the AddSouls export ('export' if none) TriggerEvent('my_battlepass:addPoints', src, delta) -- example: your own event end
editable/client/hooks.lua
-- Block interactions in your own states function ClientHooks.CanInteract() if Config.DisableWhileDead and Bridge.IsDead() then return false end if IsPedInAnyVehicle(PlayerPedId(), false) then return false end if LocalPlayer.state.handcuffed then return false end -- example: your own state bag return true end -- Send the pack's messages to your own notify script function ClientHooks.Notify(data) -- data: { type, title, message, icon, item, souls, items } -- type: 'success' | 'error' | 'info' | 'curse' | 'souls' | 'haunt' | 'item' exports['your_notify']:Show(data.title or 'Hallowed Night', data.message, data.type) -- replace with your script's export end

ClientHooks.Notify replaces the small messages (only the "Cancelled." toast always uses the Halloween UI). Reward cards, banners, the effects list and the Haunted Night HUD always use the Halloween UI. Hooks.Log(src, action, details) receives these actions: lantern, trick_or_treat, curse, specter_killed, consume, gather, brew, boss_defeated, milestone and admin_souls.

Hallowed Night — Commands & keybinds

CommandWhoDoes
/halloweeneveryoneOpen the Grimoire. Default key F7 (players can rebind it in Settings → Key Bindings → FiveM)
/hnhaunt start [zone]adminStart Haunted Night in zone N (random if empty)
/hnhaunt stopadminStop it and delete all spirits
/hnsouls [id] [amount]adminGive (or remove, if negative) Souls
/hnlanterns respawnadminRespawn every lantern
/hncoordseveryoneCopy your position as vec4(...)

Admin = add_ace group.admin hallowed.admin allow. Every command name can be changed in Config.Admin.Commands. All admin commands also work from the server console.

Hallowed Night — Exports, state & hooks

You can connect Hallowed Night to the rest of your server in three ways. Exports can be called from any other resource. State bags let any script read the live season and event state. Hooks and the bridge are small open Lua files inside rklhalloween that stay editable after escrow. You edit them to plug the pack into your framework, logs, notifications and weather script. Always call exports as exports['rklhalloween'], which is why the folder name must stay rklhalloween.

Haunted Night exports need the module enabled

StartHaunt, StopHaunt and GetHauntState are registered by the Haunted Night module. If you set Config.Haunted.Enabled = false, they don't exist and GlobalState.hnHaunt is never set. Calling a missing export throws an error, so wrap the call in pcall if your script might run with the module turned off. Every other export is always available.

Server exports

AddSouls(src, amount, reason)Server

Gives Souls to a player from another script, or removes them if the amount is negative. Returns the new total.

ParameterTypeNotes
srcnumberplayer server id
amountnumberrounded down with math.floor. A value that can't be converted to a number counts as 0
reasonstring?passed on to Hooks.OnSoulsChanged. Defaults to 'export'
ReturnsTypeNotes
totalnumberthe new Souls total, which never goes below 0. Returns 0 if the player has no identifier yet (see Bridge.GetIdentifier)

This export doesn't check the season or Hooks.CanParticipate, and it doesn't change any stat. The Souls pill pops up on the player's screen with the new total, Hooks.OnSoulsChanged fires, and the change is written to disk in the next save cycle. Souls are also the leaderboard score and the milestone progress, so removing Souls lowers both. Milestones that were already claimed stay claimed.

server
exports['rklhalloween']:AddSouls(src, 5, 'quest_complete') -- spending: check the balance first, because removing more than the player has just stops at 0 if exports['rklhalloween']:GetSouls(src) >= 50 then exports['rklhalloween']:AddSouls(src, -50, 'souls_shop') end
GetSouls(src) · GetStats(src)Server

Reads a player's current Souls and activity stats. GetSouls returns a number (0 if the player has no data). GetStats returns the stats table, or nil if the player has no identifier. That table is the pack's live copy, so read from it but never change it.

Stat keyGoes up when
lanternsany Soul Lantern is collected (cursed ones too)
curseda cursed lantern is collected
treatsa door knock ends in a Treat
tricksa door knock ends in a Trick
brewsa brew succeeds (sludge doesn't count)
gathereda gathering node is harvested
spiritsa Haunted Night spirit is killed, or a Haunting Specter is banished (when its reward.souls is above 0)
bossesthe player is rewarded for a Hollow King victory
server
local souls = exports['rklhalloween']:GetSouls(src) local stats = exports['rklhalloween']:GetStats(src) if stats and stats.bosses > 0 then -- this character has helped defeat the Hollow King this season end
ApplyCurse(src)Server

Puts a random enabled curse on a player, using the curse weight values. Returns the curse name, or nil if no curse landed.

ReturnsTypeNotes
namestring?e.g. 'pumpkin_head'. Returns nil while the player is still protected from curses, or when no enabled curse can be applied
  • After each curse, the server blocks new curses for the curse's duration plus Config.CurseImmunityAfterCurse (20 s by default). The Purifying Elixir blocks them for its immunity time. A blocked player sees the "repelled a curse" toast and the export returns nil.
  • If the player is in a vehicle, only curses with vehicle = true can be picked (by default only pumpkin_head).
  • The Specter reward token is created automatically, so a specter you apply this way can still be banished for Ghost Essence.
  • This export doesn't check the season or Hooks.CanParticipate. It logs a curse action through Hooks.Log.
server
local curse = exports['rklhalloween']:ApplyCurse(src) if curse then print(('%s was cursed with %s'):format(GetPlayerName(src), curse)) end
StartHaunt(zone) · StopHaunt() · GetHauntState()Server

Controls Haunted Night from your own event scheduler. StartHaunt and StopHaunt return ok, message. GetHauntState returns the live server-side state table, with the same fields that are published as GlobalState.hnHaunt (see the fields below). It can be a moment ahead of the published copy. Read it, but don't change it.

ParameterTypeNotes
zonenumber?index into Config.Haunted.Zones (1, 2, 3…). Leave it out for a random zone. Pass a number, not a string
CallResult
StartHaunttrue, 'Haunted Night started at <zone>.', or false with 'A Haunted Night is already running.' / 'Unknown zone. Valid zones: 1 - N'
StopHaunttrue, 'Haunted Night stopped.' (deletes every spirit, gives no rewards), or false, 'No Haunted Night is running.'

The messages come from the active locale. StartHaunt doesn't check the season or MinPlayers. Only the built-in Config.Haunted.Schedule checks those (season active, nothing running, at least MinPlayers online).

server
local ok, msg = exports['rklhalloween']:StartHaunt(2) print(msg) print(exports['rklhalloween']:GetHauntState().status) -- idle | announce | active | boss | ended
useItem(event, item, inventory)Server

Used only by ox_inventory, and you never call it yourself. ox_inventory calls it for every item whose definition contains server = { export = 'rklhalloween.useItem' }, which is every usable item in install/items/ox_inventory.lua.

EventWhat the pack does
usingItemReturns true only if the item has an entry in Config.Potions. For any other item it returns false, so ox_inventory cancels the use and the player keeps the item.
usedItemox_inventory has already removed the item, so the pack only applies the effect to inventory.id (the player). For the Purifying Elixir, the server also sets the curse block to the elixir's immunity time. Then the client plays the effect, and Hooks.Log receives consume.

Other inventories don't use this export. On qb-inventory and ESX, the pack registers every Config.Potions item as usable through Bridge.RegisterUsable and removes one item itself before running the effect. With the built-in satchel, the Use button in the Grimoire does the same thing.

ox_inventory/data/items.lua
["hn_potion_vigor"] = { label = "Elixir of Vigor", weight = 150, stack = true, close = true, server = { export = 'rklhalloween.useItem' }, description = "Mends wounds and hardens the skin.", },
Reuse an effect for your own item

Any key in Config.Potions becomes usable. You can add your own item that reuses one of the built-in effects (nightvision, swift, featherfall, phantom, vigor, purify, seer, candy, sludge), for example Config.Potions.my_pumpkin_pie = { effect = 'vigor', heal = 30, armor = 0 }. Add the item to your inventory too, with the server.export line on ox_inventory, and give it a label in config/items.lua.

Client exports

OpenGrimoire() · GetActiveCurses()Client

Opens the Grimoire from a radial menu or phone app, or reads the names of the active curses (for example, to block your phone while the player is cursed).

ExportBehaviour
OpenGrimoire()Asks the server for the player's menu data and opens the Grimoire. It returns nothing. It does nothing while another Halloween panel has focus, and shows "You can't do that right now." if the server has no data for the player (no identifier yet). It works outside the season too, and the Grimoire shows that the veil is closed.
GetActiveCurses()Returns an array of active curse names, such as { 'pumpkin_head' }, or an empty table if there are none.
client
exports['rklhalloween']:OpenGrimoire() local curses = exports['rklhalloween']:GetActiveCurses() -- { 'pumpkin_head', ... }

State bags

The pack publishes its shared state in state bags, so any resource on the server or client can read it without an export. Treat these as read-only: the pack overwrites them, and writing to them yourself breaks the sync.

KeyScopeValue
GlobalState.hnSeasonglobaltrue while the season is active. Checked at start and then every 5 minutes against Config.Season (server local date) or AlwaysOn
GlobalState.hnHauntglobalthe Haunted Night state table (fields below). { status = 'idle' } when nothing is running
GlobalState.hnLanternsglobalactive lanterns as { ['<index>'] = 0 | 1 }, where 1 means the lantern is cursed and shown as cursed. With CursedLooksDifferent = false every lantern is 0, so players can't tell. In personal mode every location is listed, and each client hides the ones it already collected
GlobalState.hnDepletedglobalgathering nodes that are currently harvested, as { ['<node>:<index>'] = true }
Player(src).state.hnPumpkinplayertrue while that player has the Pumpkin Head curse. The cursed player's client sets it (replicated), and every client attaches its own local pumpkin prop, so it works with entity lockdown
Entity(ped).state.hnSpiritentity{ kind = 'minion' | 'boss', hp } on every Haunted Night spirit (set by the server)
Entity(ped).state.hnInitentitytrue once the owning client has applied the spirit's health

GlobalState.hnHaunt fields

FieldTypeMeaning
statusstringidle → announce → active → boss → ended → back to idle 12 s after the end. If time runs out before the boss phase, it goes from active straight to ended. StopHaunt goes straight to idle
zonenumberindex in Config.Haunted.Zones
labelstringzone label, e.g. Pacific Bluffs Cemetery
centervector3zone centre
radiusnumberzone radius in metres
wavenumbercurrent wave (0 during the announcement)
wavesnumbernumber of waves (#Config.Haunted.Waves)
bossLabelstringConfig.Haunted.Boss.Label
startsAtnumberUnix time (server os.time()) when the waves start: now + AnnounceSeconds
endsAtnumber?set when the event goes active: start + TimeLimit. If time runs out, the spirits retreat
bossDeadboolean?false during the boss phase, set to true when he dies
bossNetnumber?network id of the Hollow King once he has spawned
victoryboolean?set together with status = 'ended': true if the boss fell, false if time ran out
any resource (server or client)
if GlobalState.hnSeason then -- Halloween content is live end local haunt = GlobalState.hnHaunt if haunt and haunt.status ~= 'idle' then print(('Haunted Night at %s, wave %d/%d'):format(haunt.label, haunt.wave or 0, haunt.waves or 0)) end -- fires on every update (each new wave too), so compare with the last status local last = 'idle' AddStateBagChangeHandler('hnHaunt', 'global', function(_, _, value) local status = value and value.status or 'idle' if status ~= last then last = status print('Haunted Night is now ' .. status) end end)

Client events (listen only)

The server sends these net events to the player's client. Another client resource can listen to them with RegisterNetEvent to react, for example to update your own HUD. Never trigger them yourself. The callback events (rklhalloween:cb:*, rklhalloween:ccb:*) are internal.

EventArgumentsSent when
rklhalloween:client:soulstotal, deltathe player's Souls changed
rklhalloween:client:cursename, tokena curse landed (token is only set for specter)
rklhalloween:client:consumeitema potion, candy or sludge was used and its effect starts
rklhalloween:client:rewarddatareward popup { title, subtitle, souls, items, money, style }
rklhalloween:client:notifydataa toast from the server (goes through ClientHooks.Notify)
rklhalloween:client:bannerdataHaunted Night banners (announce, start, boss, victory, defeat)
rklhalloween:client:hauntTicktick{ alive, boss } when the number of living spirits or the boss health ratio (0–1) changes
rklhalloween:client:novacoords, radius, damagethe Hollow King cast Soul Nova
rklhalloween:client:lanternCollectedindexpersonal mode: the player collected that lantern
rklhalloween:client:satchelsatchelbuilt-in satchel contents changed ({ [item] = amount })

Server hooks

editable/server/hooks.lua is the first server script to load (right after the shared scripts: the config files, the locales and shared/_init.lua) and stays open after escrow. Hooks are plain functions that run inside rklhalloween, so edit the file itself (another resource can't replace them). Inside a hook you can use Config, Bridge and any other resource's exports.

Hooks.Log(src, action, details)Server

Called for every rewarded action, every curse and every /hnsouls use. Use it for logs or your own anticheat rules. By default it prints to the console when Config.Debug = true, and posts a Discord embed (username Hallowed Night, the action as the title, the player name, id and details as JSON) when Hooks.Webhook is not ''.

ActiondetailsFires when
lantern{ index, cursed }a lantern is collected
trick_or_treat{ door, result }a knock finishes (result = 'treat' or 'trick')
curse{ curse }a curse is applied
specter_killed{}a Haunting Specter is banished and rewarded
gather{ node, index, amount }a gathering node is harvested
brew{ recipe, success }a brew finishes (potion or sludge)
consume{ item }a potion, candy or sludge is used
milestone{ index, label }a milestone reward is claimed
boss_defeated{}once for each rewarded player after a Hollow King victory
admin_souls{ target, amount }/hnsouls was used (src is 0 from the console)

Haunted Night spirit kills aren't logged here. They show up in Hooks.OnSoulsChanged with the reason 'spirit'.

editable/server/hooks.lua
-- Discord webhook for logs ('' = off) Hooks.Webhook = 'https://discord.com/api/webhooks/your-id/your-token'
Hooks.CanParticipate(src)Server

Return false to block a player from all Halloween activities (jail, admin duty and so on). The default returns true.

  • Season-gated actions (lanterns, door knocks, gathering, opening the cauldron and brewing) check it at the start and again when the action finishes. Here any falsy value blocks, so always return true or false, never nil.
  • Event rewards (spirit kills, Hollow King rewards, specter rewards) and milestone claims check it without the season gate, so admin-started events work out of season too. Here only an explicit false blocks.
  • It isn't checked for opening the Grimoire, using consumables, the exports or admin commands.

Blocked players get "You can't do that right now." for actions. Event rewards are skipped and milestone claims fail without a message.

Hooks.OnSoulsChanged(src, identifier, total, delta, reason)Server

Fires every time a player's Souls change. Use it to mirror Souls into your own systems. total is the new total (never below 0). delta is the amount that was requested, so removing 50 from a player with 20 gives total = 0, delta = -50.

reasonSource
lanternSoul Lantern (normal or cursed)
treat · tricktrick-or-treat result
brewsuccessful brew
gathergathering node, only if that node's souls is above 0 (all default nodes use 0)
spiritHaunted Night spirit kill
bossHollow King victory
specterbanished Haunting Specter
admin/hnsouls
anything elsethe reason passed to the AddSouls export (default 'export')
Don't loop

Never call AddSouls from inside OnSoulsChanged without a guard. That call fires the hook again.

Hooks.OnBossDefeated(participants)Server

Fires once when the Hollow King is defeated, after the boss rewards have been given. participants is an array of server ids: the players who were tracked inside the zone (radius + ParticipantRadius) during the event, are still online, are within radius + 2 × ParticipantRadius at the moment of victory, and pass CanParticipate. The list can be empty. It doesn't fire when time runs out or when an admin stops the event.

Hooks.StandaloneAddMoney(src, account, amount)Server

Called by Bridge.AddMoney when there's no framework player object, which is always the case on standalone. account is the value from your config ('cash' or 'bank'), and amount is always above 0. Return true if you paid the money yourself. With the default false, the money entry is skipped and doesn't appear in the reward popup.

Client hooks

editable/client/hooks.lua loads right after bridge/client.lua and stays open after escrow.

ClientHooks.CanInteract()Client

Return false to hide the Halloween prompt or the ox_target / qb-target option. By default it blocks players who are dead or downed (when Config.DisableWhileDead is on) and players in a vehicle. It runs every frame while the player stands at a prompt point, and inside the target canInteract callback.

It only affects the UI. For a real block, also return false from the server's Hooks.CanParticipate.

ClientHooks.Notify(data)Client

The pack's toasts go through this function (exceptions below). By default it shows the Halloween toast (HN.UI.Notify(data)). Replace the body to send notifications to your own notify script.

FieldTypeNotes
typestringsuccess, error, info, curse, souls, haunt or item
messagestringmain text, already translated
titlestring?optional heading
iconstring?Halloween UI icon name (shield, door, skull, potion, eye…)
itemstring?item name. The toast shows that item's image
itemstable?items gained, as { { name, label, amount }, ... }
soulsnumber?Souls gained

Reward popups, Haunted Night banners and the "Cancelled." text of the progress ring go straight to the Halloween UI, not through this hook. Wave messages (type = 'haunt') only reach players within 400 m of the zone edge (zone radius + 400 m from the centre).

ClientHooks.OnCurseStart(name) · ClientHooks.OnCurseEnd(name)Client

Fire on the cursed player's client. OnCurseStart runs right after the curse toast. OnCurseEnd runs when the curse finishes: when its time is up (or earlier, for example when the specter is killed), when it is cleared by death (Config.ClearCursesOnDeath), or when the Purifying Elixir lifts it. Neither fires for a curse the client repelled. name is one of levitate, haunted_sight, drunken_hex, specter, pumpkin_head, heavy_bones, clumsy, blackout. Both are empty by default.

ClientHooks.OnHauntZone(entered)Client

Fires with true when the player enters the zone of a running Haunted Night (from the announcement on), and with false when they leave it (they count as inside until they are 25 m past the radius), when the event resets to idle while they're inside, or when the resource stops. Calls always come in balanced enter/leave pairs. The client applies the Visuals.Weather override and the fog timecycle right after the true call, and clears them right before the false call.

By default, when Config.Haunted.Visuals.PauseWeatherSync is on, it pauses qb-weathersync (DisableSync/EnableSync, keeping the current in-game time) and Renewed-Weathersync (the syncWeather player state). Add your own weather script here (see the examples below).

Bridge: custom frameworks & inventories

Everything the pack needs from your server goes through bridge/server.lua and bridge/client.lua. Both stay open after escrow. On a framework or inventory that isn't supported, these are the only files you need to touch. Replace the function bodies, but keep the names, arguments and return types. At start, the server console prints which modes were picked: [rklhalloween] framework: … | inventory: ….

bridge/server.lua

FunctionReturnsUsed for
Bridge.GetIdentifier(src)string | nilThe save key (see Data, persistence & security) and the "can this player act" check. It must be unique and stable, per character if you have multicharacter. nil = the player can't take part yet
Bridge.GetName(src)stringName shown in the Grimoire profile and on the leaderboard (read when the player's progress loads)
Bridge.AddMoney(src, account, amount)booleanCash and bank rewards (account = 'cash' or 'bank'). Falls back to Hooks.StandaloneAddMoney when there's no player object
Bridge.GetItemCount(src, item)numberRecipe ingredient checks, the RequireItem door check
Bridge.CanCarry(src, item, amount)booleanMilestone pre-check before anything is given
Bridge.AddItem(src, item, amount)booleanEvery item reward. Return false if the item didn't fit, so the pack can refund or roll back
Bridge.RemoveItem(src, item, amount)booleanBrewing, using consumables, milestone rollback. It must be all-or-nothing
Bridge.RegisterUsable(item, cb)—Registers each Config.Potions item as usable and calls cb(src) when one is used. Skipped on ox_inventory (which uses the useItem export) and on the internal satchel
Bridge.InventoryConsumesOnUse()booleanHelper (true on ox_inventory). The pack itself doesn't call it
Bridge.Frameworkstringqbx, qb, esx or standalone (from Config.Framework, or auto-detected)
Bridge.Inventorystringox_inventory, qb-inventory, esx or internal. internal turns on the Satchel tab and its Use button

bridge/client.lua

FunctionReturnsUsed for
Bridge.IsDead()booleanDead or downed check: default CanInteract, cancelling hold actions, clearing curses on death. Specter hits and Soul Nova skip dead players
Bridge.OnPlayerLoaded(cb)—Reloads personal-mode lantern progress after the character loads
Target.Modestringprompt, ox_target or qb-target (resolved from Config.Interaction)
Target.AddEntity(entity, opt)—Lanterns, gathering nodes, cauldrons
Target.RemoveEntity(entity, opt)—Removes the option when the prop despawns
Target.AddSphere(coords, radius, opt)handleTrick-or-treat doors (radius 1.2)
Target.RemoveSphere(handle)—Removes a door zone

opt is { name, label, icon?, distance, onSelect, canInteract? }. In prompt mode the Target functions are never called.

bridge/server.lua: custom framework skeleton ('my-framework' is a placeholder)
function Bridge.GetIdentifier(src) local player = exports['my-framework']:GetPlayer(src) return player and player.characterId or nil -- unique + stable per character end function Bridge.GetName(src) local player = exports['my-framework']:GetPlayer(src) return player and player.fullName or GetPlayerName(src) or ('Player %s'):format(src) end function Bridge.AddMoney(src, account, amount) -- account = 'cash' | 'bank' if amount <= 0 then return true end return exports['my-framework']:AddMoney(src, account, amount) == true end
No supported inventory?

Set Config.Inventory = 'internal'. Items then go to the built-in Halloween Satchel (saved with the player's progress and shown in the Grimoire), and you don't need to write any inventory functions.

Integration examples

Copy-ready snippets for common setups. Names such as my-framework, my-weather or my-battlepass are placeholders for your own resources.

1. Give Souls from a quest

your quest resource (server)
-- call this from your own server-side quest logic, never from a client event local function rewardHalloweenQuest(src, questId) if not GlobalState.hnSeason then return end -- AddSouls itself ignores the season local total = exports['rklhalloween']:AddSouls(src, 10, 'quest:' .. questId) print(('[quests] %s finished %s and now has %d souls'):format(GetPlayerName(src), questId, total)) end

2. Start Haunted Night from your own scheduler or txAdmin

Admin commands work from the server console, so you can type hnhaunt start 2 or hnhaunt stop in the txAdmin Live Console. To use only your own schedule, set Config.Haunted.Schedule = {} and start the event with the export:

your scheduler resource (server)
-- Fridays and Saturdays at 21:00 (server time), only during the season CreateThread(function() local lastRun while true do Wait(30000) local now = os.date('*t') if (now.wday == 6 or now.wday == 7) and now.hour == 21 and now.min == 0 and GlobalState.hnSeason then local key = now.year .. '-' .. now.yday if key ~= lastRun then lastRun = key local ok, msg = exports['rklhalloween']:StartHaunt() -- random zone print('[scheduler] ' .. msg) end end end end) -- end a running event before a txAdmin scheduled restart AddEventHandler('txAdmin:events:scheduledRestart', function(eventData) if eventData.secondsRemaining <= 300 and exports['rklhalloween']:GetHauntState().status ~= 'idle' then exports['rklhalloween']:StopHaunt() end end)

3. Block jailed or on-duty players

editable/server/hooks.lua
local QBCore function Hooks.CanParticipate(src) local player if Bridge.Framework == 'qbx' then player = exports.qbx_core:GetPlayer(src) elseif Bridge.Framework == 'qb' then QBCore = QBCore or exports['qb-core']:GetCoreObject() player = QBCore.Functions.GetPlayer(src) end local data = player and player.PlayerData if data then if (data.metadata.injail or 0) > 0 then return false end -- serving jail time if data.job.name == 'police' and data.job.onduty then return false end -- on duty end return true -- always return a boolean end

4. Discord logging

Setting Hooks.Webhook is enough to log every action. To send only the important ones, replace Hooks.Log with a filtered version:

editable/server/hooks.lua
Hooks.Webhook = 'https://discord.com/api/webhooks/your-id/your-token' local SEND = { milestone = true, boss_defeated = true, admin_souls = true, specter_killed = true } function Hooks.Log(src, action, details) if Config.Debug then print(('[rklhalloween] %s (%s) %s: %s'):format(GetPlayerName(src) or '?', src, action, json.encode(details or {}))) end if Hooks.Webhook == '' or not SEND[action] then return end PerformHttpRequest(Hooks.Webhook, function() end, 'POST', json.encode({ username = 'Hallowed Night', embeds = { { title = action, description = ('**%s** (%s)\n```%s```'):format(GetPlayerName(src) or 'console', src, json.encode(details or {})), color = 16744448, } }, }), { ['Content-Type'] = 'application/json' }) end

The webhook URL is only in a server file, so it's never sent to players.

5. Send notifications to ox_lib (or your notify script)

editable/client/hooks.lua
local OX_TYPES = { success = 'success', error = 'error', curse = 'warning', haunt = 'warning' } function ClientHooks.Notify(data) if GetResourceState('ox_lib') ~= 'started' then return HN.UI.Notify(data) -- fall back to the Halloween toasts end local text = data.message or '' if data.items and #data.items > 0 then local parts = {} for _, item in ipairs(data.items) do parts[#parts + 1] = ('+%d %s'):format(item.amount, item.label) end text = text .. ' (' .. table.concat(parts, ', ') .. ')' end if data.souls then text = ('%s +%d %s'):format(text, data.souls, Config.Souls.Label) end exports.ox_lib:notify({ title = data.title or 'Hallowed Night', description = text, type = OX_TYPES[data.type] or 'inform' }) -- QBCore: TriggerEvent('QBCore:Notify', text, data.type == 'error' and 'error' or 'success') -- ESX: TriggerEvent('esx:showNotification', text) end

6. Pause another weather script inside the zone

editable/client/hooks.lua
function ClientHooks.OnHauntZone(entered) if not Config.Haunted.Visuals.PauseWeatherSync then return end -- built-in handling (keep it) if GetResourceState('qb-weathersync') == 'started' then local h, m, s = GetClockHours(), GetClockMinutes(), GetClockSeconds() TriggerEvent(entered and 'qb-weathersync:client:DisableSync' or 'qb-weathersync:client:EnableSync') if entered then NetworkOverrideClockTime(h, m, s) end end if GetResourceState('Renewed-Weathersync') == 'started' then LocalPlayer.state:set('syncWeather', not entered, false) end -- your own weather script: use its real pause/resume event or export here if GetResourceState('my-weather') == 'started' then TriggerEvent(entered and 'my-weather:client:pause' or 'my-weather:client:resume') end end

7. Open the Grimoire from a radial menu

your radial resource (client, with ox_lib loaded)
-- show the entry only while the season is active local function setGrimoireItem(active) if active then lib.addRadialItem({ id = 'hn_grimoire', icon = 'ghost', label = 'Grimoire', onSelect = function() exports['rklhalloween']:OpenGrimoire() end, }) else lib.removeRadialItem('hn_grimoire') end end AddStateBagChangeHandler('hnSeason', 'global', function(_, _, value) setGrimoireItem(value == true) end) CreateThread(function() setGrimoireItem(GlobalState.hnSeason == true) end)

With qb-radialmenu or a phone app, point the button at a client event in your own resource that calls exports['rklhalloween']:OpenGrimoire(), or run ExecuteCommand('halloween') (the value of Config.Menu.Command).

8. React to curses

editable/client/hooks.lua
-- take the hat off while the pumpkin is on, then put it back local savedHat function ClientHooks.OnCurseStart(name) if name == 'pumpkin_head' then local ped = PlayerPedId() savedHat = { GetPedPropIndex(ped, 0), GetPedPropTextureIndex(ped, 0) } ClearPedProp(ped, 0) end end function ClientHooks.OnCurseEnd(name) if name == 'pumpkin_head' and savedHat then if savedHat[1] ~= -1 then SetPedPropIndex(PlayerPedId(), 0, savedHat[1], savedHat[2], true) end savedHat = nil end end
your phone resource (client)
-- no phone while the screen is cursed local function canOpenPhone() for _, curse in ipairs(exports['rklhalloween']:GetActiveCurses()) do if curse == 'blackout' or curse == 'haunted_sight' then return false end end return true end

9. Mirror Souls into your own system

editable/server/hooks.lua
function Hooks.OnSoulsChanged(src, identifier, total, delta, reason) if delta > 0 and reason ~= 'admin' then exports['my-battlepass']:AddXP(src, delta * 10) -- your own resource end end

10. Announce the boss kill and give a bonus

editable/server/hooks.lua
function Hooks.OnBossDefeated(participants) if #participants == 0 then return end local names = {} for _, src in ipairs(participants) do names[#names + 1] = GetPlayerName(src) or ('#' .. src) exports['rklhalloween']:AddSouls(src, 10, 'boss_bonus') -- on top of Config.Haunted.Rewards.Boss end TriggerClientEvent('chat:addMessage', -1, { args = { 'Hallowed Night', ('%s has fallen! Heroes: %s'):format(Config.Haunted.Boss.Label, table.concat(names, ', ')) }, }) end

11. Hide prompts while handcuffed

editable/client/hooks.lua
function ClientHooks.CanInteract() local ped = PlayerPedId() if Config.DisableWhileDead and Bridge.IsDead() then return false end if IsPedInAnyVehicle(ped, false) then return false end if IsPedCuffed(ped) then return false end return true end

12. Keep ped cleanup scripts away from the spirits

your cleanup resource (server)
for _, ped in ipairs(GetAllPeds()) do if not IsPedAPlayer(ped) and not Entity(ped).state.hnSpirit then DeleteEntity(ped) -- Haunted Night spirits and the Hollow King are skipped end end

Hallowed Night — Data, persistence & security

Hallowed Night needs no SQL. Player progress is stored in the resource KVP (key-value storage) of rklhalloween, which only the server can read or write. This page explains what is stored, when it is saved, how to start a fresh season, and everything the server checks before it hands out a reward.

Where progress lives

Each player has two keys per season:

KeyContent
hn:<season>:p:<id>Full progress as JSON (all fields below)
hn:<season>:s:<id>Small summary {"n": name, "s": souls}, so the leaderboard can be built without reading every full record

<season> is tostring(Config.Season.Id) (default '2026'). <id> is whatever Bridge.GetIdentifier returns:

FrameworkIdentifierExample key
Qbox / QBCorePlayerData.citizenid, so progress is per characterhn:2026:p:ABC12345
ESX LegacyxPlayer.identifier, as ESX gives it (per character on multicharacter)hn:2026:p:<esx identifier>
Standalonethe player's license: identifier, so progress is per accounthn:2026:p:license:1a2b…
No identifier, no progress

If the bridge returns nil (for example, the player is still on the character selection screen), the player can't do any Halloween activity and the Grimoire shows "You can't do that right now." Progress loads the first time the server needs it, for example on the player's first action or when they open the Grimoire.

What is saved

FieldTypeHolds
namestringfrom Bridge.GetName, refreshed every time the progress loads
soulsnumbercurrent Souls (never below 0)
statstablelanterns, cursed, treats, tricks, brews, spirits, bosses, gathered
claimedtableclaimed milestones by index, { ["1"] = true, ... }
collectedtablepersonal-mode lanterns this character collected, by location index
satcheltablebuilt-in satchel contents { [item] = amount } (internal inventory only)
dailytabletrick-or-treat counter { day = 'YYYY-MM-DD', count } (server date)
example value of hn:2026:p:ABC12345
{ "name": "Mara Vex", "souls": 132, "stats": { "lanterns": 41, "cursed": 6, "treats": 18, "tricks": 7, "brews": 5, "spirits": 22, "bosses": 1, "gathered": 12 }, "claimed": { "1": true, "2": true }, "collected": { "3": true, "17": true }, "satchel": { "hn_candy": 4 }, "daily": { "day": "2026-10-27", "count": 12 } }

Every field is type-checked when it loads. Broken or missing values fall back to defaults, and claims for milestone indexes that no longer exist are dropped.

What is not saved

These live only in server memory and reset when the resource restarts:

  • Per-door trick-or-treat cooldowns. They're keyed by identifier, so reconnecting doesn't reset them, but a resource restart does. The daily counter is saved.
  • Curse blocks and immunity, and specter reward tokens.
  • Which global lanterns are active, and the spots that are cooling down. They're picked again at random on start.
  • Harvested gathering nodes.
  • A running Haunted Night. A restart ends it and deletes its spirits.
  • The leaderboard cache.

When it saves

MomentWhat happens
Every 30 sAll changed (dirty) players are written and the KVP is flushed to disk
Milestone claimSaved and flushed immediately after a successful claim
Player dropThe player is saved and flushed if anything changed, then removed from the cache
Character logoutSaved on QBCore:Server:OnPlayerUnload and esx:playerLogout, so on a character switch the old character is saved before the new one loads
Resource stopEvery pending change is written and flushed
Leaderboard refreshPending changes are written first, so online players rank with their current Souls
Crashes

A clean stop or restart rklhalloween saves everything. If the server crashes or the process is killed, changes made since the last 30-second save can be lost. Milestone claims are never lost this way because they're written immediately.

Leaderboard

The leaderboard is built when someone opens or refreshes the Grimoire. The server scans every hn:<season>:s: key, keeps players with more than 0 Souls (offline players included), sorts them by Souls and caches the result for Config.Leaderboard.CacheSeconds. The top Config.Leaderboard.Size entries are sent as name + Souls only, so identifiers never reach players. Each player's own rank and the total player count come from the same cache, so a rank can be up to CacheSeconds old. The Souls number in the profile is always live.

config/config.lua
Config.Leaderboard = { Size = 10, -- entries shown in the Grimoire CacheSeconds = 60, -- how long one scan is reused }

New season & wipes

  1. Change Config.Season.Id (for example to '2027') and update Start / Finish if needed.
  2. Restart the resource. The season id is read once at start.

Everyone starts at 0 Souls with a fresh leaderboard, fresh milestones and fresh personal lanterns. Nothing is deleted: the old season's keys stay in the KVP under the old id, so setting the id back brings the old progress back.

Changing lists mid-season

Claimed milestones and personal-mode lanterns are stored by index. During a running season, add new entries to the end of Config.Milestones and Config.Lanterns.Locations, and don't reorder or remove existing ones. Otherwise saved claims point at the wrong milestone or spot. Door cooldowns are keyed by door index too.

  • Reset one player's Souls: /hnsouls <id> -<amount> (Souls stop at 0). Stats and claimed milestones stay.
  • Switching frameworks changes the identifiers (citizenid vs ESX identifier vs license), so players start empty. Their old keys are still stored.
  • Removing one player completely has no built-in command. If you need it, add a console command to editable/server/hooks.lua. It runs inside rklhalloween, so it can reach the pack's KVP:
editable/server/hooks.lua: optional console command (not built in)
-- usage (server console): hnwipe <identifier> e.g. hnwipe ABC12345 -- only use it while that player is OFFLINE: an online player's cached progress can be written back RegisterCommand('hnwipe', function(src, args) if src ~= 0 then return end -- console only local id = args[1] if not id then return print('usage: hnwipe <identifier>') end local season = tostring(Config.Season.Id) print(GetResourceKvpString(('hn:%s:p:%s'):format(season, id)) or 'no data') -- what is being removed DeleteResourceKvp(('hn:%s:p:%s'):format(season, id)) DeleteResourceKvp(('hn:%s:s:%s'):format(season, id)) print(('[rklhalloween] wiped %s for season %s'):format(id, season)) end, false)

Backups & moving servers

  • The FiveM server (FXServer) stores resource KVP in its own database, the db folder in the directory the server runs from (normally your server data folder). Back that folder up along with the rest of your server data. The best time is while the server is stopped, or right after a clean resource stop, which flushes every pending save.
  • Restoring the folder restores everyone's Souls, stats, claims, satchels and daily counters for every season id.
  • KVP belongs to the resource name, so a renamed folder starts with empty storage. That's one more reason to keep the folder name rklhalloween.
  • To move to a new host, copy the db folder together with the resource and keep the same framework, so the identifiers still match.

Security: what the server checks

Clients only send which lantern, door, node, recipe or milestone they mean, never an amount (the only other thing a client reports is the cauldron minigame result, see the note below). Every Souls value, loot roll, cooldown and reward is decided on the server from the config files.

CheckApplies toHow it works
Flood guardevery client callbackAt most 25 requests per player per second. Extra requests are dropped. Unknown callback names and bad request ids are ignored. A callback that errors is caught (pcall), logged in the console, and answered with an empty reply.
Season + participationlanterns, door knocks, gathering, cauldronGlobalState.hnSeason must be true, Bridge.GetIdentifier must return an id, and Hooks.CanParticipate must return true. Checked when the action starts and again when it finishes.
Participation onlyspirit kills, boss rewards, specter reward, milestone claimsIdentifier + Hooks.CanParticipate (not false), with no season gate, so admin-started events still pay out.
Timed actionslanterns, door knocks, gatheringThe server records a "begin" (action kind, key and time) and only accepts the matching "finish" when the full hold time has passed, minus 500 ms for latency. Hold times: Config.Lanterns.CollectTime (2500 ms), Config.TrickOrTreat.KnockTime (2200 ms), each node's time. Each begin can be used once, a player can only have one pending action, and it's cleared when they drop. This stops instant-collect injectors.
Distancelanterns, doors, nodes, cauldronsChecked at start and finish. 2D distance up to InteractDistance + 2.5 m (doors + 2.0 m, cauldrons: any cauldron location), with a height tolerance of that distance + 15 m because props snap to the ground.
Index normalisationlantern, door, node and milestone indexesHN.ToIndex turns '01', ' 1' and 1.0 into 1 and rejects anything that isn't a whole number, so one spot can't be claimed under several keys. Node names, recipe ids and satchel items must be strings that exist in the config.
Availabilitylanterns, nodesThe lantern must still be active (global) or not yet collected by you (personal), and the node must not be harvested. Re-checked on finish, so the first player to finish wins. A node is only marked harvested after the item was added.
Cooldowns & limitsdoors, cauldron, cursesDoorCooldown (1800 s) per identifier per door. DailyLimit (40) knocks per server day, saved. One brew per Config.Cauldron.BrewTime (3500 ms). After a curse, no new curse for its duration + Config.CurseImmunityAfterCurse.
Required itemdoorsWith RequireItem = true, the server counts RequiredItem in the inventory before every knock.
Safe brewingcauldronIngredients are counted on the server and removed one by one. If a removal fails, or the result doesn't fit, everything taken so far is refunded.
Milestone claimsGrimoireThe index must be valid, the milestone not yet claimed, the Souls high enough, and every item must pass Bridge.CanCarry. The claim is marked first (so it can't be claimed twice), then the items are given. If one doesn't fit, the given items are removed and the claim is undone. Money is paid only after all items succeed, then the save is flushed.
Specter tokensspecter curseEach specter curse gets a random 16-character token that expires after the curse duration + 30 s. The banish reward needs that exact token, works once, and the token is cleared by the Purifying Elixir or on drop.
Host pointsHaunted Night spawningThe server creates every spirit itself. The host (the closest player in the zone) only suggests ground points, and the server keeps only numeric, non-NaN points within radius + 5 m and 30 m height of the requested area, up to the requested count (and MaxAlive). It only accepts the reply from the player it asked, within 6 s, and spawning runs in pcall so a bad reply can't break the event.
Kill creditHaunted Night spiritsDeaths are detected on the server from the ped's health and GetPedSourceOfDeath (the driver, if it was a vehicle). No client message can claim a kill. Only minions pay kill rewards.
Boss rewardsHollow King victoryOnly players the server tracked in the zone, still online, within radius + 2 × ParticipantRadius at the moment of victory, and passing CanParticipate. Boss HP is capped at 8000 on the server.
Item usepotions, candy, sludgeox_inventory: usingItem only accepts Config.Potions items. Other inventories: the server removes the item before the effect runs. Satchel use only works in internal mode.
Admin commands/hnhaunt, /hnsouls, /hnlanternsIsPlayerAceAllowed(src, Config.Admin.Ace), or the server console. /hnsouls also checks that the target is online and has progress.
StorageKVPOnly the server reads and writes the KVP. Clients receive their own profile and the top list (names + Souls) and nothing else.
The stirring minigame

The cauldron skill-check runs in the UI, and the client reports pass or fail. The server still requires the real ingredients, a nearby cauldron and the brew cooldown, so a faked "pass" can only skip the skill-check. It can't create potions from nothing. A successful brew pays Config.Cauldron.Souls (2 by default), and every brew is logged as brew in Hooks.Log if you want to watch it.

Your own anticheat rules

Hooks.Log receives every rewarded action, so you can add your own rules on top of the built-in checks. Example: flag anyone who collects lanterns unusually fast.

editable/server/hooks.lua (add below the default Hooks.Log)
local recent = {} local defaultLog = Hooks.Log function Hooks.Log(src, action, details) defaultLog(src, action, details) if action ~= 'lantern' then return end local now, list = os.time(), recent[src] or {} list[#list + 1] = now while list[1] and now - list[1] > 60 do table.remove(list, 1) end recent[src] = list if #list > 8 then print(('[rklhalloween] %s (%s) collected %d lanterns in 60 s'):format(GetPlayerName(src) or '?', src, #list)) end end AddEventHandler('playerDropped', function() recent[source] = nil end)

Hallowed Night — Custom props

Nine custom models built in Blender (5k–30k vertices each), converted with Sollumz and CodeWalker, and shipped ready to use in stream/. Textures are embedded in each .ydr, so there is no separate .ytd. stream/hn_props.ytyp registers the archetypes through the data_file 'DLC_ITYP_REQUEST' line in fxmanifest.lua.

ModelUsed byVertices / trisCollisionLODFallback
hn_soul_lanternLantern hunt18,100 / 35,972box150prop_veg_crop_03_pump
hn_cursed_lanternCursed lanterns22,620 / 44,980box150prop_veg_crop_03_pump
hn_pumpkin_headPumpkin Head curse, Hollow King11,538 / 23,076none (attached to peds)100prop_veg_crop_03_pump
hn_cauldronWitch's Cauldron23,529 / 46,842custom mesh (metal)200prop_barrel_01a
hn_candy_bucketTrick-or-treat NPC17,099 / 33,938none (held by the NPC)80no bucket
hn_tombstoneHaunted Night decoration14,966 / 27,681custom mesh (stone)200prop_gravestones_01a
hn_mandrakeMandrake node17,170 / 34,144box120prop_plant_01a
hn_moonpetalMoonpetal node19,951 / 39,224box120prop_plant_01b
hn_bone_pileBone node25,142 / 50,096box120prop_rub_binbag_01

Shaders & textures

Every prop uses GTA's default.sps shader. The glowing parts use emissive.sps: the carved faces of the lanterns and the pumpkin head, the cauldron liquid and the moonpetal flowers. Textures are DXT1 with mipmaps, at 512 or 1024 px.

Collision and targeting

ox_target and qb-target need collision to hit an entity, so every ground prop has a collision box or mesh. The pumpkin head and candy bucket have none because they are attached to peds. The default prompt interaction mode is distance-based and works without collision.

Fallbacks keep the script working

If a custom model can't load, the pack automatically spawns the vanilla fallback set in each config (FallbackModel / fallbackModel), so nothing breaks while you investigate.

Editing a prop

  1. Open its .blend from the props source pack in Blender with the Sollumz add-on.
  2. Edit the model, keeping the object name (for example hn_cauldron), the origin and the materials.
  3. Export it as a Drawable. On Windows, Sollumz with PyMateria exports binary .ydr directly; otherwise export CodeWalker XML and import it in CodeWalker.
  4. Replace the file in stream/ and restart the server. Streamed files need a full restart.

Hallowed Night — Tebex & escrow

The manifest is escrow-ready (lua54 'yes'). These paths stay readable and editable for buyers:

fxmanifest.lua
escrow_ignore { 'config/*.lua', 'locales/*.lua', 'bridge/*.lua', 'editable/**/*.lua', 'install/**/*', }

The gameplay code (client/, server/, shared/) is encrypted. HTML/CSS/JS are never encrypted by asset escrow, so the UI is always editable. Streamed models are protected by escrow automatically.

  1. Zip the rklhalloween folder.
  2. Upload it at keymaster.fivem.net → Asset Escrow → Upload.
  3. Create a Tebex package (price 0 for a free release) and link the escrowed asset.
  4. Players claim it on Tebex and download it from their keymaster Granted Assets.

Hallowed Night — FAQ

Questions server owners ask most, answered from the resource itself. Every config key below is a real path in config/, and every default is the shipped value.

Licensing & setup

Is it free? What is escrowed and what can I edit?

Yes. Hallowed Night is a free release delivered through FiveM Asset Escrow: a Tebex package at price 0 that players claim into their keymaster Granted Assets (see Tebex & escrow). Only the gameplay code in client/, server/ and shared/ is encrypted. Everything you need to customise stays open:

PathWhat you can change
config/*.luaEvery setting: season, rewards, locations, curses, recipes, potions, Haunted Night
locales/*.luaAll messages and UI text (en, tl)
bridge/*.luaFramework, inventory, death check and target code (for custom frameworks)
editable/**/*.luaServer and client hooks (logs, money, participation, weather sync, notifications)
install/**/*Item files for ox / qb / ESX, item images, STREAM.md
html/*The whole Halloween UI. Asset escrow never encrypts HTML/CSS/JS

Does it need SQL?

No. Progress lives in server KVP, so there is no database table to import. Each player has two keys: hn:<Season.Id>:p:<identifier> (Souls, stats, claimed milestones, collected lanterns, satchel contents, daily knock count) and hn:<Season.Id>:s:<identifier> (name and Souls, used by the leaderboard). Changed data is written every 30 seconds, when a player leaves or switches character, right after a milestone claim, and when the resource stops. install/items/esx.sql only registers the items for the ESX default inventory. It has nothing to do with progress.

Which frameworks and inventories are supported?

Everything is auto-detected when the resource starts. The first match wins:

SettingDetection order ('auto')Force it with
Frameworkqbx_core → qb-core → es_extended → standaloneConfig.Framework
Inventoryox_inventory → qb-inventory (on QBCore/Qbox, includes the ps/lj forks) → ESX default → internal satchelConfig.Inventory
Interaction'auto': ox_target → qb-target → custom prompt (the shipped default is 'prompt')Config.Interaction

OneSync is required: the manifest lists '/onesync' as a dependency, because Haunted Night spirits are created by the server. The server console prints what was detected on every start, for example [rklhalloween] framework: qb | inventory: qb-inventory.

Is progress saved per character?

Yes, on frameworks. The identifier is the citizenid on Qbox/QBCore and the ESX identifier on ESX, so every character has its own Souls, milestones and satchel. Character switches are handled through QBCore:Server:OnPlayerUnload and esx:playerLogout. On standalone the player's license is used, so there is one profile per license.

Can I rename the resource folder?

Keep it rklhalloween. The UI finds its own name automatically, but two things are written with the fixed name: the ox_inventory item lines (server = { export = 'rklhalloween.useItem' }) and any exports['rklhalloween'] calls in your other scripts. If you rename the folder anyway, update both or potions stop working on ox_inventory.

Season & progression

Can I run it outside October?

Yes. Set Config.Season.AlwaysOn = true to ignore the dates, or move Start / Finish. Dates are { month, day } in the server's local date, and a season may wrap over the new year (for example Start in December, Finish in January). The season state (GlobalState.hnSeason) is checked when the resource starts and again every 5 minutes.

config/config.lua
Config.Season = { AlwaysOn = false, -- true = always open, dates ignored Start = { month = 10, day = 15 }, -- server local date Finish = { month = 11, day = 3 }, Id = '2026', -- new id = fresh leaderboard & progress }
FeatureOut of season?
Lanterns, doors, nodes, cauldronsHidden. Every rewarded action answers "The veil is closed..."
Scheduled Haunted NightDoes not fire
/hnhaunt startStill works. Admin starts and the StartHaunt export ignore the season, and kill/boss rewards still pay
Grimoire & milestonesOpens, and earned milestones can still be claimed
Potions & candyStill usable

How do players earn Souls?

Souls are the event score. Every source and its default amount:

ActivitySoulsKey
Soul lantern1Config.Lanterns.Souls.normal
Cursed lantern3 (plus a curse)Config.Lanterns.Souls.cursed
Treat1Config.TrickOrTreat.Souls.treat
Trick2 (plus a curse)Config.TrickOrTreat.Souls.trick
Specter banished2 + 1 Ghost EssenceConfig.Curses.specter.reward
Successful brew2 (failed brews give 0)Config.Cauldron.Souls
Gathering0 for every nodeConfig.Gathering.Nodes.<node>.souls
Spirit kill2 to the killer, plus a 35% chance of Ghost EssenceConfig.Haunted.Rewards.Kill
Hollow King defeated25 to every participant still near the zoneConfig.Haunted.Rewards.Boss.souls
Admin / scriptsAny amount/hnsouls [id] [amount] · AddSouls export

Souls unlock the milestones in Config.Milestones (25, 75, 150, 300 and 500 by default), which players claim from the Grimoire. Souls never go below 0.

Can I give money rewards on a standalone server?

Yes, through Hooks.StandaloneAddMoney(src, account, amount) in editable/server/hooks.lua. Standalone has no money system, so by default the hook returns false and money rewards (lantern cash, cash treats, milestone money, boss money) are skipped and not shown in the reward popup. Pay the money with your own economy script and return true. The same hook is used on a framework server when the player object can't be found.

editable/server/hooks.lua
---Standalone servers have no money system. Return true if you gave the money yourself. function Hooks.StandaloneAddMoney(src, account, amount) -- account is 'cash' or 'bank', amount is a positive number -- call your own economy script here, then: return true end

If you'd rather not pay money at all, remove the money entries from Config.Lanterns.Loot, Config.TrickOrTreat.Treats, Config.Milestones and Config.Haunted.Rewards.Boss.

How do I reset the leaderboard?

Change Config.Season.Id (for example '2026' → '2027') and restart the resource. All KVP keys include the season id, so everyone starts from zero: Souls, stats, milestones, collected lanterns, daily knocks and the built-in satchel (used when Config.Inventory resolves to 'internal', which is what auto-detection picks on standalone without ox_inventory). The old season's data stays in KVP under the old id, so switching back to it brings it back.

To take one player off the board, use /hnsouls [id] -[amount] while they are online: Souls stop at 0, and players with 0 Souls are not listed.

Gameplay & balancing

Can I change the boss model or HP?

Yes, everything about The Hollow King is in Config.Haunted.Boss. His HP is Health + HealthPerPlayer × (participants − 1), counted when he spawns, and capped at 8000.

config/haunted.lua (excerpt)
Boss = { Label = 'The Hollow King', -- banner and boss bar name Model = 'u_m_y_zombie_01', -- any ped model Health = 4000, -- HP with one participant HealthPerPlayer = 1200, -- added for every extra participant Weapon = 'WEAPON_BATTLEAXE', HeadModel = 'hn_pumpkin_head', -- set this and HeadFallback to false for no pumpkin head HeadFallback = 'prop_veg_crop_03_pump', Nova = { interval = 18, radius = 7.0, damage = 15 }, Summon = { atHealth = { 0.66, 0.33 }, count = 4 }, -- ... },

Two things to know: the boss's pumpkin head uses the same Config.Curses.pumpkin_head.offset as the curse, so a ped with a different skeleton may need that offset adjusted. And the Soul Nova message in the locale files (haunt_nova) names "The Hollow King" directly, so update it if you rename him.

Why is the boss HP capped at 8000?

GTA syncs ped health in 13 bits, so the highest value every client agrees on is 8191. The server caps the boss at 8000 (a fixed limit in the escrowed server code, not a config key), and the owning client applies HP + 100, also clamped to 8191. With the default values, the cap is reached at 5 participants (4 players give 7600). To make him harder for big groups, use the other settings: a shorter Boss.Nova.interval, a bigger Nova.damage or Nova.radius, more Summon.atHealth thresholds or a higher Summon.count, or a shorter Config.Haunted.TimeLimit.

Can I add more doors, lanterns and other locations?

Yes. The code has no limit on any list. Stand at the spot and type /hncoords: it copies vec4(x, y, z, heading) to your clipboard and prints it in F8. Then paste it into the right list and restart the resource.

KeyAddsFormat
Config.Lanterns.LocationsLantern spotsvec3 (keep the first three numbers). Height snaps to the ground
Config.TrickOrTreat.DoorsTrick-or-treat doorsvec4. Heading = the way the NPC faces. Height is used exactly as written
Config.Gathering.Nodes.<node>.pointsMandrake, Moonpetal, Bone Pile nodesvec3. Height snaps to the ground
Config.Cauldron.LocationsCauldrons (each gets a witch and a blip by default)vec4. Heading rotates the cauldron
Config.Haunted.ZonesHaunted Night arenas{ label, center = vec3, radius }. Keep center at ground height
Personal lantern mode: add new spots at the end

In Mode = 'personal', collected lanterns are saved by their position in the list. Add new locations at the end. Removing or reordering entries changes which spots players have already collected this season.

Can I remove curses from trick-or-treat?

  • No tricks at all: Config.TrickOrTreat.TrickChance = 0. Every knock is a treat.
  • Only some curses: set enabled = false on any curse in Config.Curses, or lower its weight. Disabled curses also disappear from the Grimoire's curse list.
  • No cursed lanterns: Config.Lanterns.CursedChance = 0.

If every curse is disabled but TrickChance is above 0, tricks still happen: the player gets the Trick message and Souls.trick, but no curse lands. Use TrickChance = 0 if you don't want tricks at all.

Can every player collect every lantern?

Yes: set Config.Lanterns.Mode = 'personal'. Every location becomes a collectible that each player can claim once per season, and ActiveCount / RespawnSeconds no longer apply. The default 'global' keeps ActiveCount (20) shared lanterns on the map, which the first player to finish collecting takes.

Compatibility

Does it work with entity lockdown?

Yes. Clients never need to create networked entities:

ThingHow it's createdWho sees it
Spirits & Hollow KingServer-created peds. A player inside the zone only sends ground positions, and each spirit's owner runs its AIEveryone
Pumpkin Head cursePlayer state bag hnPumpkin. Every client attaches its own local propEveryone nearby
Boss pumpkin headLocal prop on each clientEveryone nearby
Lanterns, nodes, cauldrons, tombstonesLocal props, spawned by each client within render distanceEveryone (same spots)
Witch NPCLocal ped on each clientEveryone (own copy)
Door NPC + candy bucketLocal, on the knocking player's clientOnly the knocking player
Haunting SpecterLocal ped on the cursed player's client. The kill reward is checked with a one-time server tokenOnly the cursed player

Does it support ox_target?

Yes, and qb-target too. The shipped default is the custom Halloween prompt (Config.Interaction = 'prompt'), so set it to 'ox_target', 'qb-target' or 'auto'. Either way, ensure ox_target before rklhalloween: 'auto' only checks which target resource is running when rklhalloween starts. Lanterns, gathering nodes and cauldrons become entity targets. Doors are small sphere zones (radius 1.2) at the door position. The custom prompt is turned off in target modes.

Does it support other languages?

English ('en') and Tagalog ('tl') are included. Switch with Config.Locale. To add one, copy locales/en.lua, rename the table key and translate it:

locales/de.lua
Locales = Locales or {} Locales['de'] = { season_inactive = '...', -- every key from locales/en.lua, including the whole ui = { ... } table }

Then load it: either list the new file in fxmanifest.lua under shared_scripts next to the other locales, or paste the Locales['de'] table at the bottom of an existing locale file (both are already loaded). Finally set Config.Locale = 'de'. A missing message key falls back to English, but the ui table is taken as a whole, so copy all of it. Names that live in the config files are not in the locales: curse labels (config/curses.lua), item labels (config/items.lua), milestone labels, zone labels, Boss.Label, Config.Souls.Label, gathering node labels and the cauldron blip label.

Do the custom props have collision?

From install/STREAM.md:

ModelCollisionLOD distance
hn_soul_lantern / hn_cursed_lanternBox150
hn_pumpkin_headNone (attached to peds)100
hn_cauldronCustom mesh (metal)200
hn_candy_bucketNone (held by the NPC)80
hn_tombstoneCustom mesh (stone)200
hn_mandrake / hn_moonpetal / hn_bone_pileBox120

All placed props are frozen and can't be damaged. The two props without collision are never interaction targets, so target scripts are not affected.

Hallowed Night — Troubleshooting

Common problems, each with the cause and the fix. Start with the quick checks: they solve most problems on their own.

Quick checks

  1. Read the startup line. Every start, the server console prints the detected framework and inventory. If it says standalone / internal on a framework server, see Wrong framework or inventory detected.
  2. Look for [rklhalloween] errors. Callback failures and spirit spawn failures print in the server console, and curse errors print in F8, even with debug off.
  3. Turn on Config.Debug to log every rewarded action (see Debug mode at the bottom).
server console
[rklhalloween] framework: qbx | inventory: ox_inventory

Season & startup

"The veil is closed" / nothing spawns

The season is off, so no lanterns, doors, nodes or cauldrons are created, and every rewarded action replies with this message. Config.Season is compared with the server's local date, so a host in another time zone can open or close a day off. The state is checked on start and every 5 minutes. For testing, set Config.Season.AlwaysOn = true and restart the resource.

Wrong framework or inventory detected

Detection runs once, when rklhalloween starts. If your framework starts after it, the pack runs as standalone: progress is saved per license instead of per character, money goes through Hooks.StandaloneAddMoney, and potions aren't registered as usable with your framework. Inventory detection works the same way: if ox_inventory starts later, the pack falls back to the framework's inventory (qb-inventory or ESX), or to the built-in satchel when no framework was found either. Move ensure rklhalloween below your framework, inventory and target resources, or force the choice with Config.Framework / Config.Inventory.

"You can't do that right now."

The server couldn't get an identifier for the player (the character isn't loaded yet), or Hooks.CanParticipate(src) returned false. The Grimoire shows the same message when there's no identifier. Check the character is fully loaded and review any rules you added to that hook.

Props & items

Props show as vanilla fallbacks

The custom models didn't load, so each config's fallback is used (plain pumpkins, a barrel, plants, bin bags, gravestones). Check that:

  • stream/ contains the nine hn_*.ydr files and hn_props.ytyp.
  • fxmanifest.lua still has data_file 'DLC_ITYP_REQUEST' 'stream/hn_props.ytyp' and 'stream/hn_props.ytyp' in files.
  • You did a full server restart after adding or replacing streamed files. restart rklhalloween is not enough.

The candy bucket has no fallback: if it can't load, the NPC simply holds nothing.

Potions do nothing on ox_inventory

Each usable item (the seven potions, hn_candy and hn_sludge) needs server = { export = 'rklhalloween.useItem' }, as in install/items/ox_inventory.lua. Without it, ox_inventory has nothing to call. The export also only accepts items that have an entry in Config.Potions, so an item you removed there can't be used. If you renamed the folder, the export string must use the new name. Restart the server after editing ox_inventory items.

Potions do nothing on QBCore, Qbox (without ox) or ESX

On start, the pack registers every item in Config.Potions as usable with the framework (QBCore.Functions.CreateUseableItem, exports.qbx_core:CreateUseableItem or ESX.RegisterUsableItem). Registration is skipped when the framework wasn't detected (start order), or when the inventory is ox_inventory or internal. On QBCore, the item must also have useable = true, which install/items/qb-core.lua already sets.

Items broken in qb-core (paste format)

install/items/qb-core.lua uses the hn_item = { name = 'hn_item', ... }, format. Paste every line inside the QBShared.Items = { ... } table in qb-core/shared/items.lua, before its closing brace, and keep the comma at the end of each line. Don't paste the ox_inventory file into qb-core (or the other way round), and don't wrap the lines in a second table. A Lua syntax error stops that file from loading. Typical symptoms are "You can't carry that." on every reward and missing icons. For icons, copy install/images/*.png to qb-inventory/html/images/.

qb-core/shared/items.lua
QBShared.Items = { -- ...your existing items... hn_mandrake_root = { name = 'hn_mandrake_root', label = "Mandrake Root", weight = 50, type = 'item', image = 'hn_mandrake_root.png', unique = false, useable = false, shouldClose = true, description = "A gnarled root that screams when pulled. Hums with earthy magic." }, -- ...the rest of install/items/qb-core.lua... }

"You can't carry that." (qb-inventory and others)

The inventory refused the item: it's full or over weight, or the item isn't registered. On qb-inventory, rewards are added with Player.Functions.AddItem, and this message shows when it returns false. Milestone claims check for space first, with qb-inventory 2.x CanAddItem (on older qb-core and the forks: the player's total weight against QBCore.Config.Player.MaxWeight). On ox_inventory the check is CanCarryItem, and on ESX canCarryItem. What happens next depends on the activity:

ActivityWhen an item doesn't fit
Lanterns · treats · spirit kills · bossSouls are still added. The item that didn't fit is skipped and the message shows
GatheringNothing is given and the node stays, so the player can try again
CauldronThe ingredients are given back and no potion is made
MilestonesChecked before claiming. If one item fails, the items already given are taken back and the milestone stays claimable

Interaction & UI

The E prompt doesn't show

  • Wrong mode: the prompt only runs when Config.Interaction = 'prompt', or 'auto' with no target resource running.
  • Blocked by ClientHooks.CanInteract(): by default, players in any vehicle can't interact, and neither can dead or downed players while Config.DisableWhileDead = true (it checks QBCore isdead/inlaststand metadata, Qbox isDead/inLastStand state and the ESX death event). Any rule you add to that hook applies too.
  • Busy: another hold action is running, or a Halloween panel is open.
  • Too far: players must be within InteractDistance (lanterns 2.0, doors 1.8, nodes 1.8, cauldrons 2.2). Doors are not snapped to the ground, so the exact z counts. Record doors with /hncoords while standing where the player should stand.
  • Nothing to interact with: the season is closed, the node was already gathered (it comes back after respawn seconds), Moonpetal only shows from 20:00 to 06:00 in-game, or someone else took the lantern.

The keys are Config.InteractKey (control 38, E) and Config.CancelKey (control 73, X).

ox_target / qb-target option is missing

  • The default is the custom prompt. Set Config.Interaction = 'ox_target' (or 'qb-target' / 'auto'), and start the target resource before rklhalloween ('auto' only looks once, at start).
  • Lanterns, nodes and cauldrons are entity targets, added only once the prop has spawned (within its RenderDistance). They all have collision (box or custom mesh), so the target raycast can hit them. Doors are sphere zones (radius 1.2) at the door's vec4, so aim at that exact spot.
  • hn_pumpkin_head and hn_candy_bucket have no collision. That's expected: they're never targets.
  • The same ClientHooks.CanInteract() rules apply: no options in a vehicle, while dead, or during another action. The option distance is the feature's InteractDistance.
  • Config.Debug = true draws the door zones so you can see where they are.

The UI is stuck or the cursor won't go away

Press ESC: it closes the Grimoire and the cauldron. During the stirring minigame, the first ESC cancels the stir (nothing is used up) and the second one closes the window. While a brew is finishing (Config.Cauldron.BrewTime, 3500 ms), ESC is ignored until the result shows. If it's still stuck, run restart rklhalloween in the server console: stopping the resource releases the UI focus.

The Grimoire doesn't open

It's /halloween (Config.Menu.Command), with F7 as the default key (Config.Menu.Key, '' = no key). Changing the key only changes the default, and players who rebound it in FiveM's key settings keep their own key. It won't open while another Halloween panel is open, and it shows "You can't do that right now." if the server has no identifier for the player yet.

Haunted Night

Spirits never appear

  • OneSync must be on. The resource depends on /onesync.
  • Someone must be in the zone. Waves wait until a player is within the zone radius plus Config.Haunted.ParticipantRadius (30 m). That player's client finds the ground positions.
  • Custom zones: the host's client only picks ground points within 25 m of the zone center height, not on water, and 5 to 90 m (flat distance) from the host player, so keep center.z at ground level (use /hncoords).
  • Cap: at most MaxAlive (14) spirits are alive at once. The rest arrive as others die.
  • Check the server console for [rklhalloween] spawn failed.

"A Haunted Night is already running." / the event looks stuck

Only one event can run at a time. It counts as running while it is announcing, active or in the boss phase, and for about 12 seconds after it ends. A wave only ends when all its spirits are dead. If players leave, the spirits retreat on their own once Config.Haunted.TimeLimit (900 seconds) runs out. To end it right away, use /hnhaunt stop (or hnhaunt stop in the server console, or the StopHaunt export). It deletes every spirit and resets the state.

It doesn't start on schedule

Config.Haunted.Schedule uses the real server clock (24h), checked every 20 seconds. A scheduled start also needs an active season, at least MinPlayers (1) online, and no event already running. An empty Schedule means admin-only. /hnhaunt start [zone] works any time, even out of season.

The boss HP stops growing with more players

That's the 8000 HP cap: GTA syncs ped health in 13 bits. With the defaults, it's reached at 5 participants. See the FAQ for other ways to make the fight harder.

Weather & time

Weather flickers inside the zone

A weather sync script is fighting the HALLOWEEN override. With Config.Haunted.Visuals.PauseWeatherSync = true, ClientHooks.OnHauntZone pauses qb-weathersync and Renewed-Weathersync while the player is inside, and resumes them when they leave. For any other sync script, add its own pause/resume call to that hook, or set Visuals.Weather = '' and keep only the fog timecycle (Visuals.Timecycle).

The clock jumps when entering the zone

Pausing qb-weathersync also sets the clock to 18:00, so the hook saves the current time and puts it back right away. When the player leaves, the sync script takes over again and the clock follows the server time. If another script still changes the time inside the zone, it isn't covered by the hook: add it to ClientHooks.OnHauntZone, or turn off PauseWeatherSync and set Visuals.Weather = ''.

Curses & effects

"Your purification repelled a curse." / curses don't land

After every curse, the player is immune for the curse's duration plus Config.CurseImmunityAfterCurse (20 seconds). The Purifying Elixir grants Config.Potions.hn_potion_purify.immunity (180 seconds). Both show this same message. Only enabled curses are picked, and in a vehicle only curses with vehicle = true can land (by default only pumpkin_head). If no curse fits, nothing lands.

The pumpkin head is misaligned

Adjust Config.Curses.pumpkin_head.offset. The config suggests changing x between -0.06 and -0.09 (default -0.075) to raise or lower it. It's attached to bone 31086 (SKEL_Head). The Hollow King's head uses the same offset, so check both after a change. If the stream didn't load, the fallback pumpkin uses this offset too and will look off, so fix the stream first.

config/curses.lua
offset = { x = -0.075, y = 0.02, z = 0.0, rx = 0.0, ry = 90.0, rz = 0.0 },

Anticheat flags Swiftfoot, candy or a curse

These features change the player's movement or visibility on the client. Whitelist rklhalloween in your anticheat, or tone them down:

FeatureWhat it doesSetting
hn_potion_swiftSetRunSprintMultiplierForPlayer ×1.25 for 60s (capped at 1.49)Config.Potions.hn_potion_swift.multiplier
hn_candySprint ×1.15 for sugarRush seconds (10)Config.Potions.hn_candy.sugarRush = 0 turns it off
hn_potion_featherCaps fall speed with SetEntityVelocityConfig.Potions.hn_potion_feather.maxFallSpeed
hn_potion_phantomSetEntityAlpha + SetLocalPlayerAsGhostRemove hn_potion_phantom from Config.Potions so it can't be used, and drop the phantom recipe (Config.Cauldron.Recipes + RecipeOrder) and its rewards (Config.Milestones, Config.Haunted.Rewards.Boss)
levitateFreezes the player and moves them up and down every frameConfig.Curses.levitate.enabled = false

Leaderboard & progress

The leaderboard isn't updating

It's rebuilt at most once every Config.Leaderboard.CacheSeconds (60), when someone opens the Grimoire (claiming a milestone or using a satchel item reloads it too), and the rank on the Profile tab comes from the same cache. Players with 0 Souls are not listed, and Config.Leaderboard.Size (10) sets how many rows show. Close the Grimoire and reopen it after a minute.

The leaderboard shows last season's players

Change Config.Season.Id and restart. Old progress stays in KVP under the old id. If you use the built-in satchel ('internal' inventory), its contents start empty too.

Door cooldowns or lanterns reset after a restart

Expected. Door cooldowns, the global lantern layout and gathered nodes are kept in memory, so a resource restart clears them. Saved progress (Souls, stats, milestones, personal-mode lanterns, satchel and the daily knock count for DailyLimit) is in KVP and stays. In global mode, /hnlanterns respawn reshuffles the lanterns at any time.

Milestone or lantern money isn't paid on standalone

Standalone has no money system. Fill in Hooks.StandaloneAddMoney in editable/server/hooks.lua and return true (see the FAQ), or remove the money rewards.

Language

Tagalog or another locale isn't applied

  • Config.Locale must match the table key exactly ('tl' for Locales['tl']). Restart the resource after changing it.
  • A new locale file must be loaded, either listed in fxmanifest.lua shared_scripts or pasted into an existing locale file.
  • Missing keys fall back to English one by one, but the ui = { ... } table is used as a whole. A locale without it shows the English UI.
  • Curse names, item names, milestone, zone and boss labels and the Souls label come from config/, not from the locale files. Translate them there.

Debug mode

config/config.lua
Config.Debug = true -- turn it off again on a live server

With debug on:

  • Hooks.Log prints every logged action to the server console: lantern, trick_or_treat, gather, brew, curse, consume, specter_killed, milestone, boss_defeated and admin_souls.
  • Target zones are drawn in the world (debug on ox_target, debugPoly on qb-target), so you can see the door spheres.
server console
[rklhalloween] John (3) lantern: {"cursed":false,"index":12} [rklhalloween] John (3) trick_or_treat: {"door":4,"result":"treat"} [rklhalloween] John (3) brew: {"recipe":"swift","success":true}
Logs without debug

Set Hooks.Webhook in editable/server/hooks.lua to a Discord webhook URL, and the same actions are posted there whether debug is on or off.

Hallowed Night — Changelog

v1.0.0 — 27 Oct 2026

  • Initial release: Soul Lantern Hunt (global and personal modes), Trick-or-Treat with 8 curses, Witch's Cauldron with gathering and 7 potions, Haunted Night event with waves and The Hollow King boss.
  • Grimoire menu: profile, leaderboard, milestone rewards, recipes, curse compendium and built-in satchel.
  • Custom Halloween UI with procedural sound effects.
  • 9 custom streamed props and 16 item icons.
  • Bridges for Qbox, QBCore, ESX Legacy and standalone, plus ox_inventory, qb-inventory, the ESX inventory and the internal satchel. Interaction works with the custom prompt, ox_target or qb-target.
  • English and Tagalog locales. Escrow-ready, with editable config, locales, bridge, hooks and install files.

Integration examples

Common ways other scripts plug into the suite.

Loot luck from equipped gear

Boost a drop chance using the player's total luck (server side):

your loot script — server
local function rollDrop(src, baseChance) local luck = exports['rklforge']:GetPlayerLuck(src) or 0 local chance = math.min(100, baseChance + luck * 0.5) return math.random(100) <= chance end

Reduce custom magic damage by gear

your spell script — client
local mdef = exports['rklequipment']:ApplyMagicDefense() or 0 local final = math.max(1, spellDamage - mdef)

Don't interrupt an active combo

your script — client
if exports['rklequipment']:IsCombatMode() then lib.notify({ description = 'Finish your attack first', type = 'error' }) return end

Reward forge materials from a quest

your quest script — server
-- on quest complete exports['rklforge']:GiveForgeReq(src, 'upgrade_stone', 5, 'Upgrade Stone') exports['rklforge']:GiveForgeCard(src, 'attack_card', 2)

Award spell XP on a cast

your spell hook — server
exports['rklgrimoire']:AddGrimoireXP(src, 25)

Pause casting during a cutscene

your cutscene script — client
exports['rklskillsystem']:SetDisableSkills(true) exports['rklskillsystem']:SetUIHidden(true) -- ...play cutscene... exports['rklskillsystem']:SetUIHidden(false) exports['rklskillsystem']:SetDisableSkills(false)

Gate a feature by unlocked skill

your script — server
local skills = exports['rklspellmanage']:getUnlockedSkills(src) local hasFireball = false for _, s in ipairs(skills) do if s.uid == 'fireball' then hasFireball = true break end end

Hosting these docs

This whole site is one index.html file — no build step. Drop it on any free static host:

HostHow
GitHub PagesPut index.html in a repo → Settings → Pages → deploy from branch
Cloudflare PagesConnect the repo, or drag-and-drop the file — free & fast
NetlifyDrag the file onto the dashboard for an instant URL

To edit, open index.html in any editor. Each export is a <div class="api"> block — copy one to add a new entry.

Troubleshooting

An export returns nil

The folder name must match the export call — see Resource names. exports['rklequipment'] only works if the folder is rklequipment.

Forge doesn't apply stats to gear

Check the ensure order — the forge must start after equipment, and both need oxmysql, ox_lib, ox_inventory running first.

Currency isn't deducted on QBCore / Qbox

Create an awaken_coin ox_inventory item, or change the currency in config to money / bank / another item.

A combo plays no animation

The animation dictionary/clip must exist (a streamed .ycd or a base-game dict). A missing dict means the clip won't play even though the combo logic runs.