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.
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.
| Resource | Does | Owns |
|---|---|---|
| rklequipment | Equip gear, apply stats, durability, combat combos | player_equipment |
| rklforge | Upgrade / enchant / socket equipped gear | edits equipment metadata |
| crafting | Craft items, levels & XP | slv_crafting_data |
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
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.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' }
| type | ESX Legacy | QBCore / Qbox |
|---|---|---|
| item | ox_inventory item | ox_inventory item |
| money | getMoney() | money.cash |
| bank | getAccount('bank') | money.bank |
| account | named ESX account | named money type |
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.
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 name | Used by |
|---|---|
| rklequipment | exports['rklequipment'] · rklequipment:* events |
| rklforge | exports['rklforge'] · rklforge:* events |
| rklcrafting | rklcrafting:* events & callbacks |
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
| Caller | Calls | For | Required? |
|---|---|---|---|
| rklequipment | rklskilltree:GetJobLevel | Level gate on gear | Optional |
| rklinterface | rklforge:GetPlayerStatTotal | Cooldown / mana / skill-damage bonuses | Optional |
| rklforge | rklequipment:GetDefense, ApplyMagicDefense | Reading gear stats | Required |
| rklbinder | — (shares the forge_card item) | Storing forge cards in an album | Standalone |
| rklrepair | — (shares the equipment item) | Restoring durability | Standalone |
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
| From | Event | To | Meaning |
|---|---|---|---|
| rklforge | rklequipment:updateCache | rklequipment | Gear changed — re-read metadata & re-apply stats |
| rklskilltree | rklinterface:updatelvlxp | rklinterface | Class level / XP changed — refresh HUD |
| rklskilltree | rklinterface:refreshskills | rklinterface | Skills changed — recalculate the build |
| rklskilltree | rklinterface:resetstat | rklinterface | Clear cached stat icons |
| rklinterface | rklequipment:addedstats | rklequipment | Push HUD-side stat changes |
| rklequipment | rkldamageped:victim | rklparticleseffect | Damage an NPC & show the number |
| rklequipment | rklstatuseffect:applyStatus | rklstatuseffect | Apply burn / poison / etc. on hit |
Shared database tables
| Table | Written by | Read by |
|---|---|---|
| player_equipment | rklequipment, rklforge | rklequipment, rklforge |
| slv_player_job_points | rklskilltree | rklskilltree, rklinterface |
| slv_player_skills | rklskilltree, rklinterface | rklskilltree, rklinterface |
| slv_skills / slv_skill_trees | rklskilltree | rklskilltree, rklinterface |
| slv_crafting_data | rklcrafting | rklcrafting |
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.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
}
| Value | Behaviour |
|---|---|
| true | Use it, but only when the resource is actually started |
| false | Never call it — the fallback value is used |
| omitted | Same 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.
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.
| Table | Holds | Key |
|---|---|---|
| player_equipment | Equipped gear + full item JSON | identifier + slot |
| slv_crafting_data | Crafting level and XP | identifier |
| slv_player_job_points | Class XP, level, skill points | identifier + job_name |
| slv_skill_trees | One tree per class + saved layout | id |
| slv_skills | Every skill node and its unlock rules | id |
| slv_player_skills | Unlocked skills, rank, HUD slot | identifier + skill_id |
| slv_admin_xp_logs | Audit trail for admin XP grants | id |
| sl_binder | Card 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.
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';
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';
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
Returns whether the local player is mid-attack (an attack/combo animation is playing). Use it to block actions that shouldn't interrupt combat.
| Returns | Type | Notes |
|---|---|---|
| isAttacking | boolean | true while an attack is in progress |
if exports['rklequipment']:IsCombatMode() then
return -- player is swinging, skip
end
Current total physical defense from all equipped gear (base + upgrades + sockets). Use it to reduce incoming physical damage.
| Returns | Type | Notes |
|---|---|---|
| defense | number | 0 when nothing is equipped |
local def = exports['rklequipment']:GetDefense()
local final = math.max(1, incomingDamage - def)
Current total magic defense from equipped gear. Subtract it the same way you would physical defense for spell / elemental damage.
| Returns | Type | Notes |
|---|---|---|
| magicDefense | number | takes no arguments; returns the value |
local mdef = exports['rklequipment']:ApplyMagicDefense()
local final = math.max(1, spellDamage - mdef)
Returns the full set-bonus config table (every set and the stats each grants). Handy for showing set info in a UI.
| Returns | Type | Notes |
|---|---|---|
| setBonuses | table | mirror of Config.SetBonuses |
local sets = exports['rklequipment']:GetSetBonuses()
for setId, set in pairs(sets) do
print(setId, set.label)
end
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.
| Parameter | Type | Notes |
|---|---|---|
| itemName | string | ox_inventory item name |
| Returns | Type | Notes |
|---|---|---|
| setId | string | nil | id of the matching set |
| set | table | nil | the set definition |
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).
['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.
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.
- Give the weapon item
w_type = "yourtype"inconfig.lua. - Define
Config.Combos.yourtypeincombos.lua.
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
Config.ComboSettings = {
spear = { speedFactor = 0.03, speedCap = 0.30, minWait = 300, comboTimer = 5, slot = 'right_hand' },
}
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
| Command | Access | Does |
|---|---|---|
| /managegear | group.admin | Open the custom-equipment creator |
| /toggleweapon | player | Hide / show the equipped weapon |
| /testanims | debug | Combat animation tester menu |
| /debugdetect | debug | Print detection / state info |
Useful events
| Event | Side | Does |
|---|---|---|
| rklequipment:updateCache | client | Re-read equipped metadata & re-apply stats (forge fires this after editing gear) |
| rklequipment:openequipment | client | Open 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
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.
| Parameter | Type | Notes |
|---|---|---|
| src | number | player server id |
| reqType | string | requirement id (e.g. protect_scroll) |
| count | number | amount — defaults to 1 |
| label | string? | display name; defaults to reqType |
| imageurl | string? | optional image URL |
| description | string? | optional tooltip text |
| Returns | Type | Notes |
|---|---|---|
| ok | boolean | false if src or reqType is missing |
exports['rklforge']:GiveForgeReq(src, 'protect_scroll', 1, 'Protection Scroll')
Gives a player a forge card item (the Config.ForgeCardItem). The cardType must exist in Config.Cards.types.
| Parameter | Type | Notes |
|---|---|---|
| src | number | player server id |
| cardType | string | key in Config.Cards.types |
| level | number | card level — defaults to 1 |
| imageurl | string? | optional image URL |
| description | string? | optional; defaults to the card's desc |
| Returns | Type | Notes |
|---|---|---|
| ok | boolean | false if cardType is unknown |
exports['rklforge']:GiveForgeCard(src, 'attack_card', 3)
Sums one stat across all equipped gear — base value plus upgrades plus sockets. Broken items (durability ≤ 0) are skipped.
| Parameter | Type | Notes |
|---|---|---|
| src | number | player server id |
| statKey | string | stat name, e.g. attack, luck, defense |
| Returns | Type | Notes |
|---|---|---|
| total | number | 0 if none / invalid |
local atk = exports['rklforge']:GetPlayerStatTotal(src, 'attack')
Convenience wrapper for the total luck stat — the same as GetPlayerStatTotal(src, 'luck'). Useful for loot-chance boosts.
| Parameter | Type | Notes |
|---|---|---|
| src | number | player server id |
| Returns | Type | Notes |
|---|---|---|
| luck | number | total luck across equipped gear |
local luck = exports['rklforge']:GetPlayerLuck(src)
local chance = baseChance + (luck * 0.5)
Currency & items
| Config | Purpose |
|---|---|
| Config.Currency | What upgrades / enchants cost (upgrade, enchant) |
| Config.ForgeReqItem | Item name used by GiveForgeReq |
| Config.ForgeCardItem | Item name used by GiveForgeCard |
| Config.Cards.types | Defined card types & their labels |
Ready-made export examples also live, commented, inside rklforge/ox_inventory_items.lua.
Commands & notable events
| Command | Access | Does |
|---|---|---|
| /forgeadmin | group.admin | Open the forge admin menu |
| /giveforgereq | admin | Give forge requirement items |
| /giveforgecard | admin | Give forge cards |
| /forgedebugreq | debug | Inspect forge requirement metadata |
Useful events
| Event | Side | Does |
|---|---|---|
| rklforge:openMenu | client | Open 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
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.
| Parameter | Type | Notes |
|---|---|---|
| data | table | ox_inventory item-use payload |
| slot | number | inventory slot |
['craft_boost'] = {
label = 'Crafting Boost Potion',
client = { export = 'rklcrafting.craft_boost_potion' },
}
Server callback returning the player's crafting level / XP record. Fetch it from the client with lib.callback.await.
local data = lib.callback.await('rklcrafting:server:getHunterData', false)
print(data.level, data.xp)
Commands & data
| Command | Access | Does |
|---|---|---|
| /craftboost | admin | Apply / set a crafting boost |
Config & database
| Key | Purpose |
|---|---|
| Config.CraftPlace | Crafting station location(s) |
| Config.XPTable / GetRequiredXP | Level curve & XP per level |
| slv_crafting_data | DB 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.
| Resource | Does | Exports |
|---|---|---|
| rklgrimoire | Spell XP, hotbar, spell upgrades | AddGrimoireXP · GetHotbarData |
| rklskillsystem | Cast skills, mana, damage, UI toggles | 4 toggle exports |
| rklspellmanage | Spellbook items & unlocked-skill storage (+ shop) | usebook · getUnlockedSkills |
| rklspellbook | Spell menu / permitted spells | callbacks & events |
| rklskillcheck | Cast-time skillcheck minigame | events |
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
Grants spell (grimoire) XP to a player. Use it to reward casting, quests or events.
| Parameter | Type | Notes |
|---|---|---|
| target | number | player server id |
| amount | number | XP to add |
exports['rklgrimoire']:AddGrimoireXP(src, 50)
Returns the local player's current spell hotbar data — the slots and the spells bound to them.
| Returns | Type | Notes |
|---|---|---|
| hotbar | table | current hotbar slots |
local hotbar = exports['rklgrimoire']:GetHotbarData()
Callbacks & events
| Name | Kind | Does |
|---|---|---|
| grimoire:getPlayerData | callback | Fetch the player's grimoire record |
| grimoire:getLearnedSpells | callback | List the player's learned spells |
| rklgrimoire:serverUpgradeSpell | callback | Upgrade a spell (server-validated) |
| rklgrimoire:hotbarUpdated | event | Fires when the hotbar changes (UI listens) |
| rklgrimoire:showMagicNotify | event | Show a themed magic notification |
Command
| Command | Access | Does |
|---|---|---|
| /addxp <id> <amount> | ace command.addxp | Add 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
Enable or disable the player's ability to cast skills. Pass true to block casting, false to allow it again.
| Parameter | Type | Notes |
|---|---|---|
| status | boolean | true = casting disabled |
exports['rklskillsystem']:SetDisableSkills(true) -- during a cutscene
exports['rklskillsystem']:SetDisableSkills(false) -- restore
Returns whether casting is currently disabled.
| Returns | Type | Notes |
|---|---|---|
| disabled | boolean |
if exports['rklskillsystem']:IsSkillsDisabled() then return end
Force-hide or show the spell hotbar UI. Pass true to hide it, false to bring it back.
| Parameter | Type | Notes |
|---|---|---|
| status | boolean | true = UI hidden |
exports['rklskillsystem']:SetUIHidden(true)
Returns whether the spell hotbar UI is currently force-hidden.
| Returns | Type | Notes |
|---|---|---|
| hidden | boolean |
local hidden = exports['rklskillsystem']:IsUIHidden()
Callback, command & events
| Name | Kind | Does |
|---|---|---|
| rklskillsystem:getPlayerData | callback | Fetch the player's skill data |
| /skillsystem | command | Open the skill manager UI (blocked while downed) |
| rklskillsystem:notify | event | Show a skill notification |
| rklskillsystem:showdamage | event | Show a damage indicator |
| rklskillsystem:enableusingskills / disableusingskills | event | Event-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
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.
| Parameter | Type | Notes |
|---|---|---|
| source | number | player server id |
| Returns | Type | Notes |
|---|---|---|
| skills | table | list of { uid, categoryName }; empty if none |
local skills = exports['rklspellmanage']:getUnlockedSkills(src)
for _, s in ipairs(skills) do print(s.categoryName, s.uid) end
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.
| Parameter | Type | Notes |
|---|---|---|
| data | table | ox_inventory item-use payload |
| slot | number | inventory slot |
['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.
| Name | Kind | Does |
|---|---|---|
| rkl_spellbook:client:openSpellbook | event | Open the spell menu for the player |
| rkl_spellmenu:getPermittedSpells | callback | List spells the player may cast |
| rkl_spellmenu:getPlayers | callback | Nearby players for targeting |
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.
| Event | Side | Does |
|---|---|---|
| rklskillcheck:startSpellMinigame | client | Begin the skillcheck for a cast |
| rklskillcheck:finish | client | Fired with the result (pass / fail) |
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.
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
Applies a status to a player from the server. duration is in seconds (default 5). Use it for traps, spells, poison weapons, etc.
| Parameter | Type | Notes |
|---|---|---|
| target | number | player server id |
| name | string | status key in Config.Statuses |
| duration | number | seconds (default 5) |
| damage | number | false | per-tick damage; omit for config default, false for none |
-- burn for 8 seconds, 5 damage per tick
exports['rklstatuseffect']:GiveStatus(src, 'burn', 8, 5)
Removes a status from a player.
| Parameter | Type | Notes |
|---|---|---|
| target | number | player server id |
| name | string | status to remove |
exports['rklstatuseffect']:TakeStatus(src, 'burn')
Client exports
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.
| Parameter | Type | Notes |
|---|---|---|
| name | string | status key |
| duration | number? | seconds; nil = config default |
| damage | number | false? | per-tick; nil = config default |
exports['rklstatuseffect']:ApplyStatus('poison', 10, 3)
Returns whether the local player currently has a status.
| Returns | Type | Notes |
|---|---|---|
| active | boolean |
if exports['rklstatuseffect']:HasStatus('freeze') then return end
Removes a status from the local player.
| Parameter | Type | Notes |
|---|---|---|
| name | string | status to remove |
exports['rklstatuseffect']:RemoveStatus('poison')
Event & config
You can also apply a status client-side via event — handy from another client script:
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.
| Event | Side | Does |
|---|---|---|
| rkldamageped:victim | server | Apply & show damage on a ped (NPC); fired by combat scripts |
| rklcharfx:sync | server | Play a particle on the caster, synced to nearby players |
| rklcharfx:playLocal | client | Play a particle locally on a target ped |
| rkldamgeshow:start | client | Show a floating damage number above a target |
Dealing ped damage (same call the combos use)
TriggerServerEvent('rkldamageped:victim', 'rklequipment:victim',
hitCoords, range, enemyNetId, damage)
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
Returns whether the local player is currently casting. Use it to block actions that shouldn't interrupt a cast.
| Returns | Type | Notes |
|---|---|---|
| casting | boolean | true while a cast is running |
if exports['rklinterface']:isSpellActive() then return end
Callbacks
| Callback | Returns | Does |
|---|---|---|
| rklinterface:getJobData | table | nil | Class XP and level (falls back to the playerClass state bag) |
| rklinterface:getUnlockedSkills | table | Unlocked active skills for the player's class |
| rklinterface:getUnlockedPassives | table | Unlocked passive skills and their rank stats |
local jobData = lib.callback.await('rklinterface:getJobData', false)
print(jobData.level, jobData.currentClass)
Events
| Event | Args | Does |
|---|---|---|
| rklinterface:updatexp | currentXP, nextXPNeeded | Update the XP bar |
| rklinterface:updatelvl | currentLevel | Update the level display |
| rklinterface:updatelvlxp | level, xp, nextXP | Update 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:equipSkill | slot, skillId | Server — equip a skill into a hotbar slot |
| rklinterface:gethpback | — | Server — restore saved HP / mana on spawn |
TriggerClientEvent('rklinterface:updatelvlxp', src, 12, 340, 500)
Commands & config
| Command | Does |
|---|---|
| /hideui | Toggle the HUD on / off |
| /interface | Open 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.
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.
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
Grants class XP, handling level-ups and the HUD refresh automatically. Use it to reward quests, kills or events.
| Parameter | Type | Notes |
|---|---|---|
| source | number | player server id |
| amount | number | XP to add |
exports['rklskilltree']:AddJobXP(src, 250)
The player's current level in their active class. This is what rklequipment uses to gate gear by required_lvl.
| Returns | Type | Notes |
|---|---|---|
| level | number | 1 if there is no record yet |
local lvl = exports['rklskilltree']:GetJobLevel(src) or 0
if lvl < item.required_lvl then return end
Full progression snapshot for the active class — level, XP, unspent skill points, and the XP needed for the next level.
| Returns | Type | Notes |
|---|---|---|
| level | number | current class level |
| xp | number | current XP |
| points | number | unspent skill points |
| nextXp | number | XP required for the next level |
local s = exports['rklskilltree']:GetJobStats(src)
print(s.level, s.xp .. '/' .. s.nextXp, s.points)
Grants skill points directly, without XP. Handy for rewards that should let a player unlock a node straight away.
| Parameter | Type | Notes |
|---|---|---|
| source | number | player server id |
| amount | number | points to add |
| Returns | Type | Notes |
|---|---|---|
| ok | boolean | false if the player isn't found |
exports['rklskilltree']:AddJobPoints(src, 3)
Callbacks & events
| Name | Kind | Does |
|---|---|---|
| slv_skilltree:getJobTree | callback | Fetch the tree for the player's class, with unlock state |
| slv_skilltree:purchaseSkill | callback | Spend points to unlock or rank up a node (server-validated) |
| slv_skilltree:assignSkillSlot | callback | Put a skill into a HUD hotbar slot |
| slv_skilltree:saveAdminTree | callback | Save the tree from the admin editor |
| slv_skilltree:resetSystem | callback | Reset a player's unlocked skills |
| slv_skilltree:server:addXP | event | Add XP to a target player |
| slv_skilltree:server:setBaseClass | event | Set the player's base class |
| slv_skilltree:server:addSubClass / removeSubClass | event | Manage sub-classes |
Commands
| Command | Access | Does |
|---|---|---|
| /skilltree | player | Open the skill tree UI |
| /setclass | admin | Set a player's base class |
| /addsubclass · /removesubclass | admin | Manage sub-classes |
| /givejobxp | admin | Grant class XP (logged to slv_admin_xp_logs) |
| /giveawakeningstone | admin | Give an awakening stone |
| /copytree | admin | Copy 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.
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
How many cards the player currently has stored in their binder.
| Returns | Type | Notes |
|---|---|---|
| count | number | 0 if the player isn't found |
local n = exports['rklbinder']:GetBinderCount(src)
The full binder contents as a raw table, keyed by slot index. Useful for collection rewards or "complete the album" checks.
| Returns | Type | Notes |
|---|---|---|
| binder | table | empty table if nothing stored |
local binder = exports['rklbinder']:GetBinder(src)
for slot, card in pairs(binder) do
print(slot, card.cardType, card.level)
end
Events
| Event | Args | Does |
|---|---|---|
| rklbinder:requestData | — | Open the binder (fired by the client on /binder) |
| rklbinder:store | invSlot, binderIndex | Bag → binder |
| rklbinder:retrieve | binderIndex | Binder → bag |
| rklbinder:move | fromIndex, toIndex | Move 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
| Key | Purpose |
|---|---|
| Config.Command / Config.OpenKey | Open command (default /binder) and optional keybind |
| Config.CardItem | Item used as the card — default forge_card, same as the forge |
| Config.Layout | Binder size: pages (even), cols, rows |
| Config.Rarity / Config.CardTypes | Frame colour by level, and card visuals + stat info |
| Config.BinderTable | Database table name |
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.
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.
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
| Name | Side | Does |
|---|---|---|
| rklrepair:getAwakenCoins | server | Current balance, for the UI |
| rklrepair:processRepair | server | Recheck 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
| Key | Purpose |
|---|---|
| Config.NPCs | Locations, models, accepted categories, and minDurabilityToRepair |
| Config.WeaponList | Per-weapon repair cost (materials + currency) |
| Config.DefaultRepair | Fallback cost for anything unlisted |
| Config.RKLCurrency | What 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.
| Feature | What players do | Config |
|---|---|---|
| Soul Lantern Hunt | Find 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-Treat | Knock on doors. A costumed NPC answers with a Treat (candy, ingredients, cash) or a Trick (curse). | config/trickortreat.lua |
| Curses | 8 fantasy curses with an on-screen timer. Lifted by death (optional) or the Purifying Elixir. | config/curses.lua |
| Witch's Cauldron | Gather Mandrake, Moonpetal and Bone Dust, then brew 7 potions with a stirring skill-check. | config/cauldron.lua · gathering.lua · potions.lua |
| Haunted Night | Fog 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 |
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.
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' }infxmanifest.lua). - No database, no
ox_liband nooxmysql. 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:
# 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 setup | Items | Images (install/images/*.png) |
|---|---|---|
| Qbox / QBCore / ESX + ox_inventory | Paste install/items/ox_inventory.lua into ox_inventory/data/items.lua | ox_inventory/web/images/ |
| QBCore + qb-inventory | Paste install/items/qb-core.lua into qb-core/shared/items.lua | qb-inventory/html/images/ (ps/lj: that inventory's html/images/) |
| ESX default inventory | Run install/items/esx.sql on your database | Only if your inventory UI shows item images |
| Standalone | Nothing. Items go to the built-in satchel | Nothing. 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.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 (throughHooks.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:
[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 / action | What it tests | What you should get |
|---|---|---|
| /halloween or F7 | Grimoire and UI | The 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 respawn | Admin ace, lanterns | Toast "All lanterns respawned." The global lanterns are re-rolled: Config.Lanterns.ActiveCount (20) of the 32 locations |
| /hnsouls [your id] 25 | Souls, milestones, item + money rewards | Toast "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 Candy | Usable items | Eating animation, "Sugar rush!", +5 health and a 10-second sprint boost. On standalone, use it from Grimoire → Satchel |
| /hnhaunt start 1 | Haunted Night | Banner "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 stop | Cleanup | "Haunted Night stopped." Every spirit is deleted |
| /hncoords | Config helper | "Coordinates copied to clipboard." Your position is copied as vec4(x, y, z, heading) and printed in F8 |
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.AlwaysOnback tofalse(unless you want the event all year), and turnConfig.Lanterns.Blips.EnabledandConfig.Debugoff 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) untilConfig.Season.Idchanges. - The included trick-or-treat doors are samples. Walk to the doors you want and add them with
/hncoordstoConfig.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
HALLOWEENweather, decorative tombstones and the wave counter.
"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
| Requirement | Needed? | Notes |
|---|---|---|
| OneSync | Required | Listed in dependencies in fxmanifest.lua. Haunted Night spirits and the boss are created by the server |
| Framework | Optional | Qbox (qbx_core), QBCore (qb-core), ESX Legacy (es_extended), or none (standalone) |
| Inventory | Optional | ox_inventory, qb-inventory (and the ps/lj forks), the ESX default inventory, or none: the built-in Halloween Satchel |
| Target | Optional | ox_target or qb-target. The default is the pack's own Halloween prompt |
| Database | Not needed | Progress is stored in server KVP: no SQL tables (the only SQL file registers items for the ESX default inventory) |
| ox_lib / oxmysql | Not needed | The pack doesn't use them |
What's in the folder
| Path | Contents | After escrow |
|---|---|---|
| config/ | All settings, one file per module | Open |
| locales/ | en.lua, tl.lua | Open |
| bridge/ | server.lua (framework, money, inventory, usable items) and client.lua (death check, player loaded, target) | Open |
| editable/ | Server and client hooks | Open |
| install/items/ | ox_inventory.lua, qb-core.lua, esx.sql | Open |
| install/images/ | 16 item PNGs for your inventory | Open |
| install/STREAM.md | Prop collision and LOD notes, how to re-export a prop | Open |
| stream/ | 9 custom props (hn_*.ydr) and hn_props.ytyp | Protected |
| html/ | The Halloween UI, with its own item icons in html/assets/items/ | Open (not encrypted) |
| client/ server/ shared/ | Gameplay code | Encrypted |
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).
-- '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'
| Setting | Auto-detection order (first match wins) |
|---|---|
| Config.Framework | qbx_core running → qbx · qb-core → qb · es_extended → esx · otherwise standalone |
| Config.Inventory | ox_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:
[rklhalloween] framework: esx | inventory: ox_inventory
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.
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
# 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.
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
| Setup | Detected as | Items file | Images to | Usable items via |
|---|---|---|---|---|
| Qbox | qbx + ox_inventory | install/items/ox_inventory.lua | ox_inventory/web/images/ | ox export rklhalloween.useItem |
| QBCore + qb-inventory | qb + qb-inventory | install/items/qb-core.lua | qb-inventory/html/images/ | QBCore.Functions.CreateUseableItem |
| QBCore + ox_inventory | qb + ox_inventory | install/items/ox_inventory.lua | ox_inventory/web/images/ | ox export rklhalloween.useItem |
| ESX default inventory | esx + esx | install/items/esx.sql | Only if your inventory UI shows images | ESX.RegisterUsableItem |
| ESX + ox_inventory | esx + ox_inventory | install/items/ox_inventory.lua | ox_inventory/web/images/ | ox export rklhalloween.useItem |
| Standalone | standalone + internal | None | None | Grimoire → Satchel → Use |
Qbox (ox_inventory)
- Open
install/items/ox_inventory.luaand copy the 16 item lines (everything below the two comment lines). - Paste them inside the
return { ... }table inox_inventory/data/items.lua, before its closing brace. - Copy every PNG from
install/images/intoox_inventory/web/images/. - In
server.cfg:ensure qbx_core,ensure ox_inventory, optionallyensure ox_target, thenensure rklhalloween. - Restart the server and check for
framework: qbx | inventory: ox_inventoryin the console.
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)
- Open
install/items/qb-core.luaand copy the 16 item lines. - Paste them inside the
QBShared.Items = { ... }table inqb-core/shared/items.lua, before its closing brace. - Copy every PNG from
install/images/intoqb-inventory/html/images/. On a fork, use that inventory'shtml/images/folder (ps-inventory/html/images/,lj-inventory/html/images/). - In
server.cfg:ensure qb-core, your inventory, optionallyensure qb-target, thenensure rklhalloween. - Restart the server and check for
framework: qb | inventory: qb-inventory.
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
- Paste the 16 lines of
install/items/ox_inventory.luaintoox_inventory/data/items.lua(same as Qbox above). - Copy
install/images/*.pngintoox_inventory/web/images/. - In
server.cfg:ensure qb-core,ensure ox_inventory, optionally a target script, thenensure rklhalloween. - 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)
- Run
install/items/esx.sqlon your ESX database (HeidiSQL, phpMyAdmin or similar). It usesINSERT IGNORE, so running it twice is safe. - Images: if your inventory UI shows item images, copy
install/images/*.pnginto its image folder. The pack's own UI already has its icons. - In
server.cfg:ensure es_extended, thenensure rklhalloween. - Restart the server, so ESX loads the new items, and check for
framework: esx | inventory: esx.
-- 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
- Paste the 16 lines of
install/items/ox_inventory.luaintoox_inventory/data/items.lua. - Copy
install/images/*.pngintoox_inventory/web/images/. - Skip
install/items/esx.sql(its own header says so). - In
server.cfg:ensure es_extended,ensure ox_inventory, optionallyensure ox_target, thenensure rklhalloween. - 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)
- Nothing to paste and no images to copy. The pack's UI already ships its item icons in
html/assets/items/. - Add
ensure rklhalloweentoserver.cfg(plus a target script if you want one). - 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:
---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
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.
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:
| Inventory | Registered with | Who removes the item |
|---|---|---|
| ox_inventory | server = { export = 'rklhalloween.useItem' } in each item | ox_inventory (the pack then runs the effect only) |
| qb-inventory (QBCore) | QBCore.Functions.CreateUseableItem | The pack |
| qb-inventory (Qbox) | exports.qbx_core:CreateUseableItem | The pack |
| ESX default | ESX.RegisterUsableItem | The pack |
| internal | Grimoire → Satchel → Use | The pack |
---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.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
| Value | What 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 point | Target type | Icon | Range |
|---|---|---|---|
| Soul lanterns | Entity | fa-solid fa-ghost | Config.Lanterns.InteractDistance (2.0) |
| Gathering nodes | Entity | fa-solid fa-seedling | Config.Gathering.InteractDistance (1.8) |
| Witch's cauldrons | Entity | fa-solid fa-flask | Config.Cauldron.InteractDistance (2.2) |
| Trick-or-treat doors | Sphere zone, radius 1.2 | fa-solid fa-candy-cane | Config.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:.
'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).
| Item | Label | Weight ox / qb | Weight ESX | Usable | Comes from (default config) |
|---|---|---|---|---|---|
| hn_mandrake_root | Mandrake Root | 50 | 1 | No | Mandrake gathering nodes |
| hn_moonpetal | Moonpetal | 50 | 1 | No | Moonpetal nodes (night only), rare treat |
| hn_bone_dust | Bone Dust | 50 | 1 | No | Bone Pile nodes, cursed lanterns |
| hn_ghost_essence | Ghost Essence | 50 | 1 | No | Cursed lanterns, Haunted Night spirits and boss, the Specter curse |
| hn_pumpkin_seeds | Pumpkin Seeds | 50 | 1 | No | Lanterns, treats |
| hn_raven_feather | Raven Feather | 50 | 1 | No | Lanterns, treats |
| hn_candy | Hallowed Candy | 20 | 1 | Yes | Lanterns, treats, boss, first milestone. +5 health and a 10 s sprint boost |
| hn_candy_bucket | Candy Bucket | 300 | 6 | No | Hollow Sovereign milestone. Needed to knock only if Config.TrickOrTreat.RequireItem = true |
| hn_potion_owl | Owl's Eye Elixir | 150 | 3 | Yes | Cauldron. Night vision, 90 s |
| hn_potion_swift | Swiftfoot Draught | 150 | 3 | Yes | Cauldron, milestone. Sprint ×1.25, 60 s |
| hn_potion_feather | Featherfall Tonic | 150 | 3 | Yes | Cauldron. Slow fall, 120 s |
| hn_potion_phantom | Phantom Veil | 150 | 3 | Yes | Cauldron, milestone, boss. Ghost form, 30 s |
| hn_potion_vigor | Elixir of Vigor | 150 | 3 | Yes | Cauldron. +60 health, +25 armour |
| hn_potion_purify | Purifying Elixir | 150 | 3 | Yes | Cauldron, milestone. Lifts curses, 180 s immunity |
| hn_potion_seer | Seer's Draught | 150 | 3 | Yes | Cauldron, milestone. Reveals lanterns within 400 m for 90 s |
| hn_sludge | Cauldron Sludge | 150 | 3 | Yes | Failed 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.
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.
| File | Tables | Covers |
|---|---|---|
| config.lua | Config.Locale … Config.Effects | Language, debug, framework / inventory / interaction mode, keys, season dates, Grimoire command, Souls label, leaderboard, milestones, admin ace and command names, death rules, particle effects |
| items.lua | Config.Items | Label, description and optional picture of every item shown in the Halloween UI |
| lanterns.lua | Config.Lanterns | Soul Lantern Hunt: mode, active count, respawn, cursed chance, loot, lights, blips, locations |
| trickortreat.lua | Config.TrickOrTreat | Doors, cooldown, daily limit, trick chance, treats, door NPC |
| curses.lua | Config.Curses · Config.CurseImmunityAfterCurse | The 8 curses and the immunity window after a curse |
| cauldron.lua | Config.Cauldron | Cauldron prop, witch NPC, blip, locations, stirring minigame, fail result, recipes |
| gathering.lua | Config.Gathering | Mandrake, Moonpetal and Bone Pile nodes |
| potions.lua | Config.Potions · Config.PotionAnim · Config.CandyAnim | What every usable item does, plus the drink and eat animations |
| haunted.lua | Config.Haunted | Haunted 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
| Key | Default | What 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.Debug | false | true 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
| Key | Default | What it does |
|---|---|---|
| Config.Season.AlwaysOn | false | true 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. |
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
| Key | Default | What 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.Size | 10 | How many players the Grimoire leaderboard lists. Players outside that list still see their own rank in their profile once they have Souls. |
| Config.Leaderboard.CacheSeconds | 60 | The leaderboard is rebuilt from KVP (a scan of every saved player this season) at most once per this many seconds. |
Where Souls come from
| Key | Default | Paid for |
|---|---|---|
| Config.Lanterns.Souls.normal | 1 | Collecting a soul lantern |
| Config.Lanterns.Souls.cursed | 3 | Collecting a cursed lantern |
| Config.TrickOrTreat.Souls.treat | 1 | A treat at a door |
| Config.TrickOrTreat.Souls.trick | 2 | A trick at a door (paid on top of the curse) |
| Config.Cauldron.Souls | 2 | A successful brew (a failed brew pays nothing) |
| Config.Gathering.Nodes.<node>.souls | 0 | Gathering a node (0 on all three nodes by default) |
| Config.Curses.specter.reward.souls | 2 | Banishing the Haunting Specter |
| Config.Haunted.Rewards.Kill.souls | 2 | Each Haunted Night spirit killed |
| Config.Haunted.Rewards.Boss.souls | 25 | Defeating 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.Milestones = {
{ souls = 25, label = 'Wandering Spirit', rewards = {
{ type = 'item', name = 'hn_candy', amount = 5 },
{ type = 'money', account = 'cash', amount = 500 },
} },
-- { souls = ..., label = '...', rewards = { ... } },
}
| Souls | Label | Default rewards |
|---|---|---|
| 25 | Wandering Spirit | 5× hn_candy · $500 cash |
| 75 | Lantern Keeper | 2× hn_potion_swift · $1,500 cash |
| 150 | Hex Weaver | 2× hn_potion_purify · 2× hn_potion_seer |
| 300 | Veil Walker | 2× hn_potion_phantom · $5,000 bank |
| 500 | Hollow Sovereign | 1× hn_candy_bucket · $15,000 bank |
- A milestone can be claimed once per season (
Config.Season.Id), as soon as the player's total reachessouls. - 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 themoneyaccount. On standalone, money goes throughHooks.StandaloneAddMoney, which pays nothing until you edit it.
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
| Key | Default | What 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
| Key | Default | What it does |
|---|---|---|
| Config.ClearCursesOnDeath | true | Lifts 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.DisableWhileDead | true | Hides 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.
| Key | Default (dict · name · scale) | Used for |
|---|---|---|
| Config.Effects.collect | scr_rcbarry1 · scr_alien_disintegrate · 0.6 | Lantern collected · gathering node picked (0.4) |
| Config.Effects.ghostIn | scr_rcbarry2 · scr_clown_appears · 1.2 | Cursed lantern collected · Specter appears · trick-or-treat NPC appears (0.6) · Haunted Night spirit appears (0.7) |
| Config.Effects.ghostOut | scr_rcbarry2 · scr_clown_appears · 1.0 | Specter vanishes · trick-or-treat NPC leaves (0.8) · failed brew |
| Config.Effects.cursedAura | scr_rcbarry1 · scr_alien_charging · 0.35 | Looping aura on cursed lanterns (only when Config.Lanterns.CursedLooksDifferent = true) |
| Config.Effects.bossNova | scr_rcbarry1 · scr_alien_disintegrate · 3.0 | The Hollow King's Soul Nova |
| Config.Effects.brew | scr_rcbarry1 · scr_alien_teleport · 0.8 | Successful 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 = {
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',
},
}
| Field | Default | What it does |
|---|---|---|
| label | per item | Name shown everywhere in the Halloween UI and in messages. |
| description | per item | Flavour text on the potion cards in the Grimoire recipe book and the cauldron. |
| image | not set | Optional 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. |
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:
| Key | Default | What it does |
|---|---|---|
| enabled | true (all) | false removes the curse from the random pick and from the Grimoire's curse list. |
| weight | per curse | Chance relative to the other curses that can land right now (weighted pick). |
| duration | per curse | Seconds the curse lasts. The server also blocks new curses for duration + Config.CurseImmunityAfterCurse. |
| vehicle | false (only pumpkin_head is true) | Whether the curse can be picked while the player is in a vehicle. The server checks this. |
| label | per curse | Name in the curse toast, the on-screen timer and the Grimoire. |
| description | per curse | Line in the Grimoire's Known Curses list. |
| icon | per curse | UI 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
| Curse | Label | weight | duration | vehicle | icon | Chance on foot |
|---|---|---|---|---|---|---|
| levitate | Hex of the Floating Dead | 10 | 12 | false | feather | 11.1% |
| haunted_sight | Haunted Sight | 12 | 25 | false | eye | 13.3% |
| drunken_hex | Drunken Hex | 12 | 30 | false | spiral | 13.3% |
| specter | Haunting Specter | 10 | 40 | false | ghost | 11.1% |
| pumpkin_head | Pumpkin Head | 14 | 60 | true | pumpkin | 15.6% |
| heavy_bones | Heavy Bones | 12 | 30 | false | bone | 13.3% |
| clumsy | Trickster's Trip | 10 | 25 | false | imp | 11.1% |
| blackout | Shroud of Night | 10 | 20 | false | moon | 11.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
| Key | Default | What it does |
|---|---|---|
| levitate.height | 2.4 | Metres 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_hex | no extra keys | Drunk 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.health | 150 | Ghost health. The ped is spawned with health + 100. |
| specter.damage | 8 | Damage 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.amount | 1 | How many of that item. |
| specter.reward.souls | 2 | Souls 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.bone | 31086 | Ped 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.moveRate | 0.65 | Movement 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). |
| blackout | no extra keys | A dark overlay closes in on the screen. |
Immunity
| Key | Default | What it does |
|---|---|---|
| Config.CurseImmunityAfterCurse | 20 | Seconds 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 + CurseImmunityAfterCursehas 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.
| Item | effect | Parameters (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.
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
| Key | Default | What it does |
|---|---|---|
| Config.PotionAnim.dict | 'mp_player_intdrink' | Animation dictionary for drinking. |
| Config.PotionAnim.clip | 'loop_bottle' | Animation clip. |
| Config.PotionAnim.time | 2200 | Milliseconds 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.time | 1800 | Milliseconds 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.my_herbal_tea = { effect = 'vigor', heal = 25, armor = 0 }
- Register
my_herbal_teain your inventory. On ox_inventory, give itserver = { export = 'rklhalloween.useItem' }. On QBCore, Qbox and ESX the pack registers it as usable on start. - Optional: add a
Config.Itemsentry 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
Enabledswitch. Setting it tofalseturns that module off on both server and client. - Lanterns, doors, gathering nodes and cauldrons only exist while the season is active (
Config.Seasoninconfig/config.lua). Scheduled Haunted Nights also wait for the season. An admin start (/hnhaunt startor theStartHauntexport) 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
| Unit | Keys |
|---|---|
| milliseconds | Lanterns.CollectTime, TrickOrTreat.KnockTime, TrickOrTreat.Npc.Duration, Cauldron.BrewTime, gathering node time |
| seconds | Lanterns.RespawnSeconds, TrickOrTreat.DoorCooldown, gathering node respawn, Haunted.AnnounceSeconds, Haunted.TimeLimit, Haunted.WaveDelay, Haunted.Boss.Nova.interval |
| 0.0 – 1.0 | Lanterns.CursedChance, TrickOrTreat.TrickChance, Haunted.Boss.Summon.atHealth |
| 0 – 100 (%) | chance in every loot entry |
| metres | every RenderDistance, InteractDistance and radius, plus Haunted.ParticipantRadius |
Loot tables vs. weighted treats
Two reward formats are used across the modules, and they behave differently:
| Format | Used by | How it is rolled |
|---|---|---|
| Loot (chance) | Lanterns.Loot, Lanterns.CursedLoot, Haunted.Rewards.Kill.items, Haunted.Rewards.Boss.items | Every 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.Treats | Exactly one entry is picked per Treat. Its odds are weight ÷ total weight. A missing weight counts as 1. |
-- 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 },
itemgives an inventory item.moneypays an account:'cash'or'bank'(on ESX,cashmaps to themoneyaccount).- The amount is random between
minandmax. You can writeamount = 3instead 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.
| Key | Default | What it does |
|---|---|---|
| Enabled | true | Turns 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. |
| ActiveCount | 20 | Global mode: how many lanterns exist at the same time. Capped at the number of Locations (32 by default). |
| RespawnSeconds | 240 | Global mode: the delay before a replacement lantern appears at a random free location. The spot it was taken from stays empty until then. |
| CursedChance | 0.15 | 0.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. |
| CursedLooksDifferent | true | true: 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. |
| CollectTime | 2500 | Collect duration in ms (the progress ring). The server only pays out if the action really lasted this long. |
| RenderDistance | 80.0 | The lantern prop spawns when the player is closer than this, and despawns once they are 15 m beyond it. |
| InteractDistance | 2.0 | Prompt / 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.range | 4.0 | Light radius. The light is only drawn while the player is within 45 m. |
| Light.intensity | 6.0 | Light brightness. |
| Souls.normal | 1 | Souls for a normal lantern. |
| Souls.cursed | 3 | Souls for a cursed lantern. |
| Loot | 4 entries | Rolled for every lantern, normal or cursed. Each entry rolls on its own. |
| CursedLoot | 2 entries | Rolled on top of Loot when the lantern was cursed. |
| Blips.Enabled | false | true puts a short-range blip on every active lantern (easy mode). |
| Blips.Sprite | 484 | Blip icon. |
| Blips.Color | 47 | Blip colour. Cursed lanterns that look different use colour 27. |
| Blips.Scale | 0.6 | Blip size. |
| Locations | 32 × vec3 | Every possible lantern spot. z snaps to the ground. |
Default lantern loot
| Reward | Table | Chance | Amount |
|---|---|---|---|
| hn_pumpkin_seeds | Loot | 45% | 1 – 2 |
| hn_raven_feather | Loot | 25% | 1 |
| hn_candy | Loot | 30% | 1 – 2 |
| cash | Loot | 10% | $50 – $150 |
| hn_ghost_essence | CursedLoot | 70% | 1 |
| hn_bone_dust | CursedLoot | 40% | 1 – 2 |
- Cursed lantern:
Souls.cursed+Loot+CursedLoot, then a random curse about 1.2 s later. Curse immunity and the vehicle rules fromconfig/curses.luastill apply. It also counts toward thecursedstat. - Personal mode: collected spots are saved in each player's progress for the current
Config.Season.Id.ActiveCountandRespawnSecondsare not used. Every location the player hasn't collected yet is shown. - Admin:
/hnlanterns respawnclears every global lantern and its cooldown, then rolls a fresh set.
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).
| Key | Default | What it does |
|---|---|---|
| Enabled | true | Turns trick-or-treat on or off. |
| RequireItem | false | true: 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). |
| KnockTime | 2200 | Knock duration in ms, checked by the server. |
| InteractDistance | 1.8 | Prompt / target distance from the door point. The server accepts up to 2.0 m more. |
| DoorCooldown | 1800 | Seconds 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. |
| DailyLimit | 40 | Max knocks per player per real day, based on the server's date. 0 = unlimited. The count is saved with the player's progress. |
| TrickChance | 0.30 | 0.0 – 1.0 chance that a knock is a Trick instead of a Treat. |
| Souls.treat | 1 | Souls for a Treat, plus one pick from Treats. Counts toward the treats stat. |
| Souls.trick | 2 | Souls for a Trick, plus a random curse about 1.8 s later (immunity and vehicle rules apply). Counts toward the tricks stat. |
| Npc.Enabled | true | false = no NPC; the result shows right away. |
| Npc.Models | 5 ped models | a_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.Duration | 3500 | How long the NPC stays, in ms, before it vanishes. |
| Npc.HoldBucket | true | The 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. |
| Treats | 5 entries | Weighted pick, one entry per Treat. See the odds below. |
| Blips.Enabled | false | true puts a short-range "Trick or Treat" blip on every door. |
| Blips.Sprite | 40 | Blip icon. |
| Blips.Color | 17 | Blip colour. |
| Blips.Scale | 0.5 | Blip size. |
| Doors | 15 × vec4 | Sample 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
| Treat | Weight | Odds | Amount |
|---|---|---|---|
| hn_candy | 50 | 50% | 1 – 3 |
| hn_pumpkin_seeds | 20 | 20% | 1 – 2 |
| hn_raven_feather | 15 | 15% | 1 |
| hn_moonpetal | 5 | 5% | 1 |
| cash | 10 | 10% | $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
| Value | Meaning |
|---|---|
| x, y, z | The 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. |
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).
| Key | Default | What it does |
|---|---|---|
| Enabled | true | Turns 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. |
| InteractDistance | 2.2 | Prompt / 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). |
| RenderDistance | 120.0 | The prop (and its witch) spawn within this distance. |
| Light.color | { 80, 255, 120 } | RGB glow drawn 1 m above the cauldron (green). |
| Light.range | 5.0 | Light radius. Drawn while the player is within 45 m. |
| Light.intensity | 8.0 | Light brightness. |
| Witch.Enabled | true | Spawns 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.Offset | vec3(-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.Enabled | true | A short-range blip on every cauldron. |
| Blip.Sprite | 499 | Blip icon. |
| Blip.Color | 27 | Blip colour. |
| Blip.Scale | 0.8 | Blip size. |
| Blip.Label | "Witch's Cauldron" | Blip name. |
| Locations | 3 × vec4 | x, y, z plus the heading of the cauldron prop. Defaults: Pacific Bluffs cemetery, Grapeseed, Paleto Bay. |
| BrewTime | 3500 | Length 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. |
| Souls | 2 | Souls per successful brew (counts toward the brews stat). Failed brews give no Souls. |
| Minigame.Enabled | true | false = no skill-check, and every brew succeeds. |
| Minigame.Rounds | 3 | Successful stirs needed to pass. |
| Minigame.Speed | 1.0 | Marker speed multiplier. The base speed is 150° per second around the ring, and it gets 14% faster after every good stir. |
| Minigame.ZoneSize | 0.18 | Size of the glowing zone as a share of the ring (0.18 ≈ 65°). Clamped to 0.05 – 0.5. Bigger = easier. |
| Minigame.MaxMisses | 2 | Misses allowed. The next miss fails the brew, so by default the 3rd miss fails. |
| Fail.LoseIngredients | true | true: a failed brew uses up the ingredients. false: players keep them. |
| Fail.Item | 'hn_sludge' | Item given on a failed brew. false = nothing. |
| Fail.Amount | 1 | How many Fail.Item to give (1 if left out). |
| Recipes | 7 recipes | Keyed 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
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' },
| Field | What it does |
|---|---|
| item | The 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. |
| amount | How many result items one successful brew gives. |
| color | A CSS colour that tints the potion in the cauldron UI and the recipe book. |
| ingredients | A 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
| Id | Result | Ingredients | Colour |
|---|---|---|---|
| owl | hn_potion_owl ×1 | 2 Moonpetal · 1 Raven Feather | #f5b942 |
| swift | hn_potion_swift ×1 | 2 Pumpkin Seeds · 1 Raven Feather | #3fe0ff |
| feather | hn_potion_feather ×1 | 2 Raven Feather · 1 Moonpetal | #d9c8ff |
| vigor | hn_potion_vigor ×1 | 2 Mandrake Root · 1 Pumpkin Seeds | #ff3b4e |
| purify | hn_potion_purify ×1 | 1 Moonpetal · 1 Mandrake Root · 1 Ghost Essence | #cfe8ff |
| seer | hn_potion_seer ×1 | 1 Ghost Essence · 1 Bone Dust · 1 Moonpetal | #9b4dff |
| phantom | hn_potion_phantom ×1 | 2 Ghost Essence · 1 Bone Dust | #9ffff0 |
How a brew resolves
- Opening the cauldron asks the server for the recipe list. The server checks the season,
Hooks.CanParticipateand that the player is at a cauldron, then sends each recipe with the player's ingredient counts. - 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. - The server checks the ingredients again. Success: the ingredients are removed, the player gets the result item and
Souls. Fail: the player getsFail.Item, and the ingredients are removed only ifFail.LoseIngredientsis on. - If the result item doesn't fit in the inventory, every removed ingredient is given back.
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.
| Key | Default | What it does |
|---|---|---|
| Enabled | true | Turns gathering on or off. This is separate from Config.Cauldron.Enabled. |
| RenderDistance | 70.0 | Node props spawn within this distance. |
| InteractDistance | 1.8 | Prompt / target distance. The server accepts up to 2.5 m more. |
| Nodes | mandrake · moonpetal · bones | One entry per node type, keyed by any unique id. The fields are below. |
Node fields
| Field | Example (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. |
| min | 1 | Minimum amount per gather. |
| max | 2 | Maximum amount per gather (random between min and max). |
| time | 4000 | Gather duration in ms, checked by the server. |
| respawn | 600 | Seconds the point stays gathered (hidden) for everyone. |
| nightOnly | false | true: 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. |
| souls | 0 | Souls per gather (0 = none). Every gather counts toward the gathered stat either way. |
| light | moonpetal only: { 120, 180, 255 } | Optional RGB glow (range 2.5, intensity 4.0). Leave it out for no light. |
| points | 6 × vec3 | Every spot where this node spawns. z snaps to the ground. |
Default nodes
| Node | Item | Amount | Time | Respawn | Night only | Points |
|---|---|---|---|---|---|---|
| mandrake | hn_mandrake_root | 1 – 2 | 4000 ms | 600 s | no | 6 |
| moonpetal | hn_moonpetal | 1 | 3000 ms | 480 s | yes | 5 |
| bones | hn_bone_dust | 1 – 2 | 3500 ms | 600 s | no | 5 |
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.
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
| Key | Default | What it does |
|---|---|---|
| Enabled | true | Turns 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. |
| MinPlayers | 1 | Online players needed for a scheduled start. It doesn't apply to admin starts. |
| AnnounceSeconds | 120 | Warning 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. |
| TimeLimit | 15 * 60 | Seconds (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. |
| ParticipantRadius | 30.0 | Extra 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
| Key | Default | What it does |
|---|---|---|
| Visuals.Weather | 'HALLOWEEN' | Client-side weather override while the player is inside the zone. '' = off. |
| Visuals.PauseWeatherSync | true | Pauses 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.TimecycleStrength | 0.55 | Timecycle strength. |
| Visuals.Tombstones | true | Places 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.TombstoneCount | 10 | Number 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
| Key | Default | What it does |
|---|---|---|
| Spirits.Models | { 'u_m_y_zombie_01' } | Ped models for normal spirits. One is picked at random for each spirit. |
| Spirits.Health | 250 | HP of each spirit. The owning client sets its max health to this value + 100 (GTA's base health). |
| Spirits.Armor | 0 | Armour of normal spirits. The boss always gets 100. |
| Spirits.Alpha | 170 | Transparency of normal spirits (255 = solid, lower = more ghostly). The boss is always solid. |
| Spirits.Accuracy | 25 | Combat 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"). |
| WaveDelay | 10 | Seconds between the last spirit of a wave dying and the next wave (or the boss) starting. |
| MaxAlive | 14 | Safety cap on living spirits, boss summons included. Bigger waves spawn in batches as spirits die, with a new attempt every 3 s. |
Boss
| Key | Default | What 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.Health | 4000 | Base boss HP. |
| Boss.HealthPerPlayer | 1200 | Extra 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.interval | 18 | Seconds between Soul Novas while the boss is alive. |
| Boss.Nova.radius | 7.0 | Living 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.damage | 15 | Damage 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.count | 4 | Spirits per summon, spawned within 10 m of the boss. MaxAlive still applies. |
| Boss.Scale | 1.0 | In the file, but the current code doesn't read it, so changing it has no effect. |
Boss HP by participants (defaults)
| Participants | Boss HP |
|---|---|
| 1 | 4000 |
| 2 | 5200 |
| 3 | 6400 |
| 4 | 7600 |
| 5 or more | 8000 (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
| Key | Default | What it does |
|---|---|---|
| Rewards.Kill.souls | 2 | Souls 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.souls | 25 | Souls for every eligible participant when the boss falls. Counts toward the bosses stat. |
| Rewards.Boss.items | 3 entries | Loot 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
| Key | Default | What it does |
|---|---|---|
| Zones | 3 zones | Each one is { label, center = vec3, radius }. /hnhaunt start 2 uses the 2nd zone. Without a number, a random zone is picked. |
| Blip.Sprite | 484 | Centre blip icon, named "Haunted Night - <zone label>". It flashes during the announce phase. |
| Blip.Color | 27 | Centre blip colour. |
| Blip.Scale | 1.1 | Centre blip size. |
| Blip.RadiusColor | 27 | Colour of the zone radius circle. |
| Blip.RadiusAlpha | 90 | Opacity of the radius circle. |
| Zone # | Label | Center | Radius |
|---|---|---|---|
| 1 | Pacific Bluffs Cemetery | vec3(-1735.00, -220.00, 56.00) | 80.0 |
| 2 | Grapeseed Fields | vec3(2010.00, 4890.00, 42.80) | 90.0 |
| 3 | Sandy Shores Outskirts | vec3(1600.00, 3600.00, 35.00) | 90.0 |
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,
},
},
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
| Status | What happens | Keys involved |
|---|---|---|
| announce | A banner for every player, a flashing blip and radius, and tombstones placed. | AnnounceSeconds, Blip.*, Visuals.Tombstone* |
| active | Waves spawn one after another. The timer is running. | Waves, WaveDelay, MaxAlive, Spirits.*, TimeLimit, Rewards.Kill |
| boss | The Hollow King spawns with the boss bar, Soul Novas and summons. | Boss.*, TimeLimit |
| ended | Victory (pays Rewards.Boss) or retreat. Spirits are removed 6 s later. | Rewards.Boss, ParticipantRadius |
| idle | The 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
| Item | Label | Type | Where it comes from (defaults) | Weight |
|---|---|---|---|---|
| hn_mandrake_root | Mandrake Root | ingredient | Mandrake gathering nodes: 1–2 per pick (6 spots, respawn 600 s) | 50 |
| hn_moonpetal | Moonpetal | ingredient | Moonpetal nodes, only visible 20:00–06:00 in-game: 1 per pick (5 spots, respawn 480 s) · trick-or-treat treat, weight 5: 1 | 50 |
| hn_bone_dust | Bone Dust | ingredient | Bone Pile nodes: 1–2 per pick (5 spots, respawn 600 s) · cursed lanterns: 40% for 1–2 | 50 |
| hn_ghost_essence | Ghost Essence | ingredient | Cursed lanterns: 70% for 1 · Haunted Night spirit kill: 35% for 1 · banishing the Specter curse: 1 · Hollow King victory: 100% for 2–3 | 50 |
| hn_pumpkin_seeds | Pumpkin Seeds | ingredient | Every lantern (normal and cursed): 45% for 1–2 · treat, weight 20: 1–2 | 50 |
| hn_raven_feather | Raven Feather | ingredient | Every lantern: 25% for 1 · treat, weight 15: 1 | 50 |
| hn_candy | Hallowed Candy | treat · usable | Every lantern: 30% for 1–2 · treat, weight 50: 1–3 · Hollow King victory: 50% for 3–6 · milestone Wandering Spirit (25 Souls): 5 | 20 |
| hn_candy_bucket | Candy Bucket | tool | Milestone 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_owl | Owl's Eye Elixir | potion · usable | Cauldron recipe owl | 150 |
| hn_potion_swift | Swiftfoot Draught | potion · usable | Recipe swift · milestone Lantern Keeper (75 Souls): 2 | 150 |
| hn_potion_feather | Featherfall Tonic | potion · usable | Recipe feather | 150 |
| hn_potion_phantom | Phantom Veil | potion · usable | Recipe phantom · Hollow King victory: 60% for 1 · milestone Veil Walker (300 Souls): 2 | 150 |
| hn_potion_vigor | Elixir of Vigor | potion · usable | Recipe vigor | 150 |
| hn_potion_purify | Purifying Elixir | potion · usable | Recipe purify · milestone Hex Weaver (150 Souls): 2 | 150 |
| hn_potion_seer | Seer's Draught | potion · usable | Recipe seer · milestone Hex Weaver (150 Souls): 2 | 150 |
| hn_sludge | Cauldron Sludge | failed potion · usable | Any 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 rollsLootfirst, thenCursedLootas 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:
| Item | Effect | Kind | What happens |
|---|---|---|---|
| hn_potion_owl | nightvision | timed, 90 s | Turns on GTA night vision, and turns it off when the time runs out. |
| hn_potion_swift | swift | timed, 60 s | Sprint 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_feather | featherfall | timed, 120 s | While 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_phantom | phantom | timed, 30 s | Sets 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_vigor | vigor | instant | +60 health (never above max health) and +25 armour, with a heal flash on screen. |
| hn_potion_purify | purify | instant | Lifts 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_seer | seer | instant, blips last 90 s | Puts 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_candy | candy | instant + 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_sludge | sludge | instant | Vomit animation, 10 damage and a drunk camera shake for 6 seconds. No drink animation. |
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
- The player uses the item. On ox_inventory the item calls the
rklhalloween.useItemexport, and ox removes the item itself. On QBCore, Qbox and ESX, the pack registers everyConfig.Potionsitem as usable. With the internal satchel, the player presses Use in the Grimoire. - 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. - 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 id | Result | Ingredients | Colour |
|---|---|---|---|
| owl | hn_potion_owl · Owl's Eye Elixir | 2× Moonpetal · 1× Raven Feather | #f5b942 |
| swift | hn_potion_swift · Swiftfoot Draught | 2× Pumpkin Seeds · 1× Raven Feather | #3fe0ff |
| feather | hn_potion_feather · Featherfall Tonic | 2× Raven Feather · 1× Moonpetal | #d9c8ff |
| vigor | hn_potion_vigor · Elixir of Vigor | 2× Mandrake Root · 1× Pumpkin Seeds | #ff3b4e |
| purify | hn_potion_purify · Purifying Elixir | 1× Moonpetal · 1× Mandrake Root · 1× Ghost Essence | #cfe8ff |
| seer | hn_potion_seer · Seer's Draught | 1× Ghost Essence · 1× Bone Dust · 1× Moonpetal | #9b4dff |
| phantom | hn_potion_phantom · Phantom Veil | 2× 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 paysConfig.Cauldron.Souls(2). - A failed brew gives
Fail.Item(hn_sludge×1). It also takes the ingredients whileFail.LoseIngredients = true(the default). SetFail.Item = falseto 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.BrewTimems (3500). coloris the accent of the recipe card in the UI (#rgbor#rrggbb).
Add a recipe
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.
| Key | Default | Notes |
|---|---|---|
| ActiveCount | 20 | Global mode only |
| RespawnSeconds | 240 | Delay before a collected lantern reappears somewhere else |
| CursedChance | 0.15 | Cursed = more Souls + CursedLoot + a random curse |
| CursedLooksDifferent | true | false hides which lanterns are cursed |
| Loot / CursedLoot | — | Each entry rolls separately: { chance, item | money, min, max } |
| Blips.Enabled | false | Shows 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.
| Key | Default | Notes |
|---|---|---|
| TrickChance | 0.30 | Trick = curse + Souls.trick |
| DoorCooldown | 1800 | Seconds, per player per door |
| DailyLimit | 40 | Knocks per real day per player (0 = unlimited) |
| RequireItem | false | true = players need hn_candy_bucket |
| Treats | — | Weighted pick: { weight, item | money, min, max } |
Curses
| Curse | Effect | Time |
|---|---|---|
| levitate | Lifts the player into the air, then lowers them gently | 12s |
| haunted_sight | Clown-vision screen effect + spectral vignette | 25s |
| drunken_hex | Drunk walk + camera sway | 30s |
| specter | A ghost hunts the player. Kill it for Ghost Essence (server-verified token) | 40s |
| pumpkin_head | Pumpkin helmet that everyone nearby can see (synced through a player state bag, so it works with entity lockdown) | 60s |
| heavy_bones | Slow walk, no sprint or jump | 30s |
| clumsy | Random trips (ragdoll) | 25s |
| blackout | Darkness closes in on the screen | 20s |
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
| Potion | Ingredients | Effect |
|---|---|---|
| hn_potion_owl | 2 Moonpetal · 1 Raven Feather | Night vision, 90s |
| hn_potion_swift | 2 Pumpkin Seeds · 1 Raven Feather | Sprint ×1.25, 60s |
| hn_potion_feather | 2 Raven Feather · 1 Moonpetal | Slow fall, no fall damage, 120s |
| hn_potion_vigor | 2 Mandrake Root · 1 Pumpkin Seeds | +60 health, +25 armour |
| hn_potion_purify | Moonpetal · Mandrake Root · Ghost Essence | Lift curses + 180s immunity |
| hn_potion_seer | Ghost Essence · Bone Dust · Moonpetal | Reveals lanterns within 400m for 90s |
| hn_potion_phantom | 2 Ghost Essence · 1 Bone Dust | Ghost 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
- Announce: a banner for everyone, plus a flashing blip and radius on the map for
AnnounceSeconds. - Waves: spirits spawn inside the zone. Each kill pays Souls and has a chance at Ghost Essence. The next wave starts
WaveDelayseconds after the last spirit falls. - Boss: The Hollow King (pumpkin head, battle axe) with a boss bar. A Soul Nova knocks back nearby players every
Nova.intervalseconds, 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). - Victory: every participant still near the zone gets
Rewards.Boss. IfTimeLimitruns 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.
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
/halloweenor 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
| Activity | Souls | Config key |
|---|---|---|
| Soul Lantern | 1 | Config.Lanterns.Souls.normal |
| Cursed Lantern | 3 | Config.Lanterns.Souls.cursed |
| Treat | 1 | Config.TrickOrTreat.Souls.treat |
| Trick | 2 | Config.TrickOrTreat.Souls.trick |
| Successful brew | 2 | Config.Cauldron.Souls |
| Specter banished | 2 | Config.Curses.specter.reward.souls |
| Haunted Night spirit | 2 | Config.Haunted.Rewards.Kill.souls |
| The Hollow King | 25 | Config.Haunted.Rewards.Boss.souls |
| Gathering | 0 | Config.Gathering.Nodes.<node>.souls |
1. The Soul Lantern Hunt
- 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.
- 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.)
- 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.
- 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.
- Cursed? About a second later the curse lands. See Curses below.
- 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."
| Loot | Chance | Amount |
|---|---|---|
| hn_pumpkin_seeds | 45% | 1–2 Pumpkin Seeds |
| hn_raven_feather | 25% | 1 Raven Feather |
| hn_candy | 30% | 1–2 Hallowed Candy |
| cash | 10% | $50–150 |
| hn_ghost_essence | 70% (cursed only) | 1 Ghost Essence |
| hn_bone_dust | 40% (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.
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
- 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.
- Knock. Press E. Your character turns to the door and knocks for 2.2 seconds ("Knocking...").
- Someone answers. A costumed neighbour appears in a puff of smoke, holding a candy bucket.
- Treat (70%): the neighbour hands something over and a "Treat!" card shows +1 Soul and one treat.
- Trick (30%): the neighbour laughs at you, a "Trick!" message appears, you get +2 Souls, and a moment later a curse lands.
- The neighbour vanishes again after 3.5 seconds.
| Treat | Chance | Amount |
|---|---|---|
| hn_candy | 50% | 1–3 Hallowed Candy |
| hn_pumpkin_seeds | 20% | 1–2 Pumpkin Seeds |
| hn_raven_feather | 15% | 1 Raven Feather |
| cash | 10% | $25–120 |
| hn_moonpetal | 5% | 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.
| Curse | What happens | Lasts |
|---|---|---|
| Hex of the Floating Dead | Unseen hands lift you about 2.4 m into the air. You bob and slowly spin, then get lowered back down. | 12s |
| Haunted Sight | Your vision turns strange (clown-vision screen effect), the camera wobbles and a purple vignette that slowly shifts colour darkens the edges. | 25s |
| Drunken Hex | You stagger as if very drunk and the camera sways. | 30s |
| Haunting Specter | A 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 Head | Your 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 Bones | You move at 65% speed and can't sprint or jump. | 30s |
| Trickster's Trip | An invisible imp trips you every 4–8 seconds. You'll hear it giggle. | 25s |
| Shroud of Night | Darkness 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.ClearCursesOnDeathis 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
- Find a plant or bone pile. Walk within about 2 m and the prompt reads Gather Mandrake, Gather Moonpetal or Gather Bone Pile.
- Press E. Your character kneels or plants while the ring fills ("Gathering Mandrake...").
- A "Gathered" message shows what you got, for example 2x Mandrake Root.
- The spot disappears for everyone until it grows back. If someone else got there first: "This has already been gathered."
| Spot | Gives | Time | Grows back | Default spots |
|---|---|---|---|---|
| Mandrake | 1–2 Mandrake Root | 4s | 10 min | Grapeseed fields, Pacific Bluffs cemetery, Marlowe Vineyard |
| Moonpetal | 1 Moonpetal | 3s | 8 min | Night 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 Pile | 1–2 Bone Dust | 3.5s | 10 min | Pacific Bluffs cemetery, the Sandy Shores outskirts and a few countryside spots |
Where every ingredient comes from
| Ingredient | Sources (default config) |
|---|---|
| Mandrake Root | Mandrake spots only |
| Moonpetal | Moonpetal spots at night, rare treat |
| Bone Dust | Bone piles, cursed lanterns |
| Ghost Essence | Cursed lanterns, Haunting Specter, Haunted Night spirits and boss |
| Pumpkin Seeds | Lanterns, treats |
| Raven Feather | Lanterns, treats |
5. Brewing at the Witch's Cauldron
- 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.
- Open it. Press E on Use Witch's Cauldron.
- 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.
- Brew. Click Brew (if you're missing something, the button reads Missing ingredients instead).
- 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.
- Wait. A bubbling pot shows "Brewing..." for 3.5 seconds while your character stirs.
- 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
| Potion | Ingredients | When you drink it |
|---|---|---|
| Owl's Eye Elixir | 2 Moonpetal · 1 Raven Feather | Night vision for 90s |
| Swiftfoot Draught | 2 Pumpkin Seeds · 1 Raven Feather | You sprint 25% faster for 60s |
| Featherfall Tonic | 2 Raven Feather · 1 Moonpetal | For 120s your fall speed is capped, so you drift down instead of dropping |
| Elixir of Vigor | 2 Mandrake Root · 1 Pumpkin Seeds | +60 health and +25 armour, instantly |
| Purifying Elixir | 1 Moonpetal · 1 Mandrake Root · 1 Ghost Essence | Lifts every curse and resists new ones for 180s |
| Seer's Draught | 1 Ghost Essence · 1 Bone Dust · 1 Moonpetal | Flashing map blips on every lantern within 400 m for 90s |
| Phantom Veil | 2 Ghost Essence · 1 Bone Dust | For 30s you turn see-through and ghosted to other players, and you can't shoot, aim or punch |
Other usable items
| Item | When you use it |
|---|---|
| Hallowed Candy | +5 health and a 10-second sugar rush (sprint ×1.15) |
| Cauldron Sludge | You 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.
- 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.
- 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).
- 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.)
- 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.
- 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.
- Victory. Banner: "The Veil Is Sealed: The Hollow King has fallen." Every eligible participant (see the box below) gets a reward card.
- 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 reward | Chance | Amount |
|---|---|---|
| Souls | 100% | 25 |
| hn_ghost_essence | 100% | 2–3 Ghost Essence |
| hn_potion_phantom | 60% | 1 Phantom Veil |
| hn_candy | 50% | 3–6 Hallowed Candy |
| cash | 100% | $1,500–3,500 |
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.
| Tab | What's inside |
|---|---|
| Profile | Your 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 |
| Leaderboard | The 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 |
| Milestones | A 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 |
| Recipes | Every cauldron recipe with its ingredients, handy while you're out gathering |
| Curses | Known Curses: every curse that is enabled on the server, what it does and how long it lasts |
| Satchel | Only on servers without an inventory. Your Halloween items with their amounts, and a Use button on potions, candy and sludge |
Milestones
| Souls | Title | Rewards |
|---|---|---|
| 25 | Wandering Spirit | 5 Hallowed Candy · $500 cash |
| 75 | Lantern Keeper | 2 Swiftfoot Draught · $1,500 cash |
| 150 | Hex Weaver | 2 Purifying Elixir · 2 Seer's Draught |
| 300 | Veil Walker | 2 Phantom Veil · $5,000 bank |
| 500 | Hollow Sovereign | 1 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
| Element | Where | Shows |
|---|---|---|
| Prompt | Bottom centre | The key to press and the action |
| Progress ring | Bottom centre | A timed action, with the cancel key (X) |
| Messages | Top right | Short notifications, up to 5 stacked |
| Souls pill | Lower right | Your Souls total and the latest gain, for a few seconds |
| Effects list | Left side | Active curses and potion buffs with ring timers |
| Reward card | Centre | Souls, items and cash you just earned (several rewards play one after another) |
| Banner | Top centre | Haunted Night announcements |
| Haunted Night panel | Right side | Zone, wave, spirits left, time left (only inside the zone) |
| Boss bar | Top centre | The 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
- Go to the spot in game. Stand on the ground there (don't save a spot while flying or in noclip).
- Type
/hncoords. It copiesvec4(x, y, z, heading)to your clipboard, prints the same line in the F8 console and shows "Coordinates copied to clipboard." - Paste it into the right list below, in the format that list uses.
| What | List | Format | Notes |
|---|---|---|---|
| Lantern | Config.Lanterns.Locations | vec3 | Drop the heading. The lantern's rotation is set automatically and its height snaps to the ground |
| Door | Config.TrickOrTreat.Doors | vec4 | The heading is the direction the neighbour faces (see below) |
| Gathering node | Config.Gathering.Nodes.<node>.points | vec3 | Drop the heading. Height snaps to the ground |
| Cauldron | Config.Cauldron.Locations | vec4 | The heading rotates the cauldron, and the witch stands at Witch.Offset relative to it, so the heading also decides her side |
| Haunted zone | Config.Haunted.Zones[n].center | vec3 | Plus a radius. See tutorial 6 |
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).
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.
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),
},
},
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,chanceis a percentage from 0 to 100 (100 if you leave it out). Used byConfig.Lanterns.Loot,Config.Lanterns.CursedLootand the Haunted Nightitemslists. - Weighted (
weight): exactly one line is picked, with odds equal to its weight divided by the total. Used byConfig.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.
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
},
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
},
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
| Key | Default | Gives |
|---|---|---|
| Config.Lanterns.Souls | { normal = 1, cursed = 3 } | Souls per lantern |
| Config.Lanterns.Loot / CursedLoot | see above | Rolled lantern loot |
| Config.TrickOrTreat.Souls | { treat = 1, trick = 2 } | Souls per knock |
| Config.TrickOrTreat.Treats | see above | One weighted treat per Treat |
| Config.Cauldron.Souls | 2 | Souls per successful brew |
| Config.Gathering.Nodes.*.souls | 0 | Souls per gather (0 = none) |
| Config.Curses.specter.reward | { item = 'hn_ghost_essence', amount = 1, souls = 2 } | Banishing the Specter |
| Config.Haunted.Rewards.Kill | see above | Each spirit, for the killer |
| Config.Haunted.Rewards.Boss | see above | Every participant on victory |
| Config.Milestones | 5 tiers | See tutorial 3 |
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.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
/hnsoulsdoesn'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 type | Keys | Does |
|---|---|---|
| nightvision | duration | Night vision |
| swift | duration, multiplier | Faster sprint (the multiplier is capped at 1.49) |
| featherfall | duration, maxFallSpeed | Caps downward speed while falling |
| phantom | duration, alpha, disableWeapons | Translucent, ghosted player, optional no-weapons |
| vigor | heal, armor | Instant health (up to max) and armour |
| purify | immunity | Lifts curses, then immunity for N seconds |
| seer | duration, radius | Map blips on lanterns within the radius |
| candy | heal, sugarRush | Heal plus a sugarRush-second sprint ×1.15 |
| sludge | damage | Vomit animation and damage |
Example: a Greater Vigor potion that heals more than the normal one.
Step 1: label and description for the Halloween UI
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
["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." },
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." },
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
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
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:
| Key | Does |
|---|---|
| enabled | false = never picked, and hidden from the Grimoire's Curses tab |
| weight | Odds compared to the other enabled curses (defaults 10 to 14) |
| duration | Seconds |
| vehicle | Can it land while the player is in a vehicle? Only pumpkin_head is true by default |
| label / description / icon | What the HUD and Grimoire show |
| Curse | Extra keys |
|---|---|
| levitate | height = 2.4 (metres lifted) |
| haunted_sight | screenEffect = 'DrugsTrevorClownsFight' |
| specter | model, health = 150, damage = 8 per hit, reward = { item, amount, souls } |
| pumpkin_head | model, fallbackModel, bone, offset (tweak x between -0.06 and -0.09 to raise or lower the pumpkin) |
| heavy_bones | moveRate = 0.65 |
| clumsy | interval = { 4000, 8000 } ms between trips |
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, setConfig.TrickOrTreat.TrickChance = 0.0; for no cursed lanterns,Config.Lanterns.CursedChance = 0.0. - Keep curses after death:
Config.ClearCursesOnDeath = falsein 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.Curseswould 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.
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 theStartHauntexport starts it. - Scheduled starts only happen during the season, when nothing else is running and at least
MinPlayersare 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
| Key | Default | Notes |
|---|---|---|
| Waves | { { count = 6 }, { count = 9 }, { count = 12 } } | Add or remove entries. The HUD shows Wave n / total |
| WaveDelay | 10 | Seconds between waves |
| MaxAlive | 14 | Safety cap on spirits alive at once |
| ParticipantRadius | 30.0 | Extra metres outside the zone that still count as taking part |
| Spirits | Health = 250, Alpha = 170 | Also Models (random pick), Armor, Accuracy, MoveClipset |
| Boss.Health / HealthPerPlayer | 4000 / 1200 | Capped 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 |
| Visuals | Weather = 'HALLOWEEN' | Timecycle, TimecycleStrength, Tombstones, TombstoneCount. '' turns weather or fog off |
| Blip | sprite 484, colour 27 | Map blip and radius colour/alpha |
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.
-- 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 = {
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.Idresets the collection. /hnlanterns respawnis 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
- Copy
locales/en.luato a new file, for examplelocales/de.lua. - Change the first table name from
Locales['en']toLocales['de'], and translate every value. Keep the keys and the%s/%dplaceholders, in the same order. - Add the file to
shared_scriptsinfxmanifest.lua. Locale files are listed one by one there, not with a wildcard. - Set
Config.Locale = 'de'in config/config.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 ...
},
}
shared_scripts {
-- ... the config files ...
'locales/en.lua',
'locales/tl.lua',
'locales/de.lua', -- new
'shared/_init.lua',
}
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:
| Variable | Default | Used for |
|---|---|---|
| --gold / --gold-2 | #f2b84b / #ffdf9a | Main accent: titles, key caps, borders, progress icon |
| --ember / --ember-2 | #ff7a1a / #ffa24d | Souls icons, prompt icon, lantern and treat cards, boss-name icons |
| --violet / --violet-2 | #9b5cff / #c9a5ff | Curses, cursed and ghost cards, info messages |
| --ghost | #8ff5e6 | Potion buffs in the effects list |
| --blood | #ff3b4e | Errors, missing ingredients, boss banner, live event |
| --moss | #5dff8f | Success, stir zone, brew results, season "on" |
| --text / --muted | #f3e9d7 / #a893bf | Main and secondary text |
| --line / --line-strong | gold, 22% / 55% | Thin borders |
| --title / --heading / --body | Cinzel Decorative / Cinzel / Crimson Pro | Fonts (files in html/assets/fonts/) |
| --radius / --shadow | 14px / soft drop shadow | Corner radius and shadows |
: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
.paneland.panel::beforerules.--night,--night-2and--night-3are defined but the current rules don't use them. - Positions:
#promptand#progress(bottom centre),#toasts(top right),#souls-pill(lower right),#effects(left),#bannerand#boss(top centre),#haunt(right),#reward(centre). - Progress ring colours: the
<linearGradient id="flame">inhtml/index.html. - Item images:
html/assets/items/<item>.png, or setimageon the item inConfig.Items. - Potion colours:
coloron each recipe inConfig.Cauldron.Recipes.
10. Change commands, keys and the admin permission
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
},
}
controlis a GTA control index andlabelis 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
Keymainly 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./hncoordsis 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 thestir_hintlocale string.
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.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.
| Point | Target type | Icon | Distance key |
|---|---|---|---|
| Lantern | Entity (the prop) | fa-solid fa-ghost | Config.Lanterns.InteractDistance (2.0) |
| Door | Sphere, radius 1.2 at the door | fa-solid fa-candy-cane | Config.TrickOrTreat.InteractDistance (1.8) |
| Gathering node | Entity (the prop) | fa-solid fa-seedling | Config.Gathering.InteractDistance (1.8) |
| Cauldron | Entity (the prop) | fa-solid fa-flask | Config.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.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.
| Data | With a new Id |
|---|---|
| Souls, stats, rank | Start from zero (new leaderboard) |
| Milestone claims | Every milestone can be claimed again |
| Personal lanterns | Every spot can be collected again |
| Daily knock counter | Reset |
| Halloween Satchel | Empty (see below) |
| Inventory items, money | Kept: they live in your framework, not in the pack |
| Old season data | Kept 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 = trueignores 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
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.18is about 65 degrees of the ring. The UI clamps it between 18 and 180 degrees.- With
LoseIngredients = falsethe player keeps the ingredients and still getsFail.Item, so setItem = falsetoo if you don't want free sludge.
14. Turn modules and helpers on or off
| Key | Default | Turns off |
|---|---|---|
| Config.Lanterns.Enabled | true | The lantern hunt |
| Config.TrickOrTreat.Enabled | true | Trick-or-treat doors |
| Config.Gathering.Enabled | true | Gathering nodes |
| Config.Cauldron.Enabled | true | Cauldrons and brewing |
| Config.Haunted.Enabled | true | Haunted Night |
| Config.TrickOrTreat.Npc.Enabled | true | The neighbour who opens the door (results still work) |
| Config.Cauldron.Witch.Enabled | true | The witch next to each cauldron |
| Config.Cauldron.Blip.Enabled | true | Cauldron map blips |
| Config.Lanterns.Blips.Enabled | false | true = a blip on every lantern (easy mode) |
| Config.TrickOrTreat.Blips.Enabled | false | true = a blip on every door |
| Config.TrickOrTreat.RequireItem | false | true = players need RequiredItem (hn_candy_bucket) to knock |
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:
-- 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
-- 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
| Command | Who | Does |
|---|---|---|
| /halloween | everyone | Open the Grimoire. Default key F7 (players can rebind it in Settings → Key Bindings → FiveM) |
| /hnhaunt start [zone] | admin | Start Haunted Night in zone N (random if empty) |
| /hnhaunt stop | admin | Stop it and delete all spirits |
| /hnsouls [id] [amount] | admin | Give (or remove, if negative) Souls |
| /hnlanterns respawn | admin | Respawn every lantern |
| /hncoords | everyone | Copy 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.
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
Gives Souls to a player from another script, or removes them if the amount is negative. Returns the new total.
| Parameter | Type | Notes |
|---|---|---|
| src | number | player server id |
| amount | number | rounded down with math.floor. A value that can't be converted to a number counts as 0 |
| reason | string? | passed on to Hooks.OnSoulsChanged. Defaults to 'export' |
| Returns | Type | Notes |
|---|---|---|
| total | number | the 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.
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
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 key | Goes up when |
|---|---|
| lanterns | any Soul Lantern is collected (cursed ones too) |
| cursed | a cursed lantern is collected |
| treats | a door knock ends in a Treat |
| tricks | a door knock ends in a Trick |
| brews | a brew succeeds (sludge doesn't count) |
| gathered | a gathering node is harvested |
| spirits | a Haunted Night spirit is killed, or a Haunting Specter is banished (when its reward.souls is above 0) |
| bosses | the player is rewarded for a Hollow King victory |
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
Puts a random enabled curse on a player, using the curse weight values. Returns the curse name, or nil if no curse landed.
| Returns | Type | Notes |
|---|---|---|
| name | string? | 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
durationplusConfig.CurseImmunityAfterCurse(20 s by default). The Purifying Elixir blocks them for itsimmunitytime. A blocked player sees the "repelled a curse" toast and the export returnsnil. - If the player is in a vehicle, only curses with
vehicle = truecan be picked (by default onlypumpkin_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 acurseaction throughHooks.Log.
local curse = exports['rklhalloween']:ApplyCurse(src)
if curse then
print(('%s was cursed with %s'):format(GetPlayerName(src), curse))
end
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.
| Parameter | Type | Notes |
|---|---|---|
| zone | number? | index into Config.Haunted.Zones (1, 2, 3…). Leave it out for a random zone. Pass a number, not a string |
| Call | Result |
|---|---|
| StartHaunt | true, 'Haunted Night started at <zone>.', or false with 'A Haunted Night is already running.' / 'Unknown zone. Valid zones: 1 - N' |
| StopHaunt | true, '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).
local ok, msg = exports['rklhalloween']:StartHaunt(2)
print(msg)
print(exports['rklhalloween']:GetHauntState().status) -- idle | announce | active | boss | ended
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.
| Event | What the pack does |
|---|---|
| usingItem | Returns 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. |
| usedItem | ox_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.
["hn_potion_vigor"] = {
label = "Elixir of Vigor", weight = 150, stack = true, close = true,
server = { export = 'rklhalloween.useItem' },
description = "Mends wounds and hardens the skin.",
},
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
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).
| Export | Behaviour |
|---|---|
| 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. |
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.
| Key | Scope | Value |
|---|---|---|
| GlobalState.hnSeason | global | true while the season is active. Checked at start and then every 5 minutes against Config.Season (server local date) or AlwaysOn |
| GlobalState.hnHaunt | global | the Haunted Night state table (fields below). { status = 'idle' } when nothing is running |
| GlobalState.hnLanterns | global | active 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.hnDepleted | global | gathering nodes that are currently harvested, as { ['<node>:<index>'] = true } |
| Player(src).state.hnPumpkin | player | true 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.hnSpirit | entity | { kind = 'minion' | 'boss', hp } on every Haunted Night spirit (set by the server) |
| Entity(ped).state.hnInit | entity | true once the owning client has applied the spirit's health |
GlobalState.hnHaunt fields
| Field | Type | Meaning |
|---|---|---|
| status | string | idle → 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 |
| zone | number | index in Config.Haunted.Zones |
| label | string | zone label, e.g. Pacific Bluffs Cemetery |
| center | vector3 | zone centre |
| radius | number | zone radius in metres |
| wave | number | current wave (0 during the announcement) |
| waves | number | number of waves (#Config.Haunted.Waves) |
| bossLabel | string | Config.Haunted.Boss.Label |
| startsAt | number | Unix time (server os.time()) when the waves start: now + AnnounceSeconds |
| endsAt | number? | set when the event goes active: start + TimeLimit. If time runs out, the spirits retreat |
| bossDead | boolean? | false during the boss phase, set to true when he dies |
| bossNet | number? | network id of the Hollow King once he has spawned |
| victory | boolean? | set together with status = 'ended': true if the boss fell, false if time ran out |
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.
| Event | Arguments | Sent when |
|---|---|---|
| rklhalloween:client:souls | total, delta | the player's Souls changed |
| rklhalloween:client:curse | name, token | a curse landed (token is only set for specter) |
| rklhalloween:client:consume | item | a potion, candy or sludge was used and its effect starts |
| rklhalloween:client:reward | data | reward popup { title, subtitle, souls, items, money, style } |
| rklhalloween:client:notify | data | a toast from the server (goes through ClientHooks.Notify) |
| rklhalloween:client:banner | data | Haunted Night banners (announce, start, boss, victory, defeat) |
| rklhalloween:client:hauntTick | tick | { alive, boss } when the number of living spirits or the boss health ratio (0–1) changes |
| rklhalloween:client:nova | coords, radius, damage | the Hollow King cast Soul Nova |
| rklhalloween:client:lanternCollected | index | personal mode: the player collected that lantern |
| rklhalloween:client:satchel | satchel | built-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.
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 ''.
| Action | details | Fires 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'.
-- Discord webhook for logs ('' = off)
Hooks.Webhook = 'https://discord.com/api/webhooks/your-id/your-token'
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
trueorfalse, nevernil. - 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
falseblocks. - 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.
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.
| reason | Source |
|---|---|
| lantern | Soul Lantern (normal or cursed) |
| treat · trick | trick-or-treat result |
| brew | successful brew |
| gather | gathering node, only if that node's souls is above 0 (all default nodes use 0) |
| spirit | Haunted Night spirit kill |
| boss | Hollow King victory |
| specter | banished Haunting Specter |
| admin | /hnsouls |
| anything else | the reason passed to the AddSouls export (default 'export') |
Never call AddSouls from inside OnSoulsChanged without a guard. That call fires the hook again.
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.
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.
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.
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.
| Field | Type | Notes |
|---|---|---|
| type | string | success, error, info, curse, souls, haunt or item |
| message | string | main text, already translated |
| title | string? | optional heading |
| icon | string? | Halloween UI icon name (shield, door, skull, potion, eye…) |
| item | string? | item name. The toast shows that item's image |
| items | table? | items gained, as { { name, label, amount }, ... } |
| souls | number? | 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).
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.
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
| Function | Returns | Used for |
|---|---|---|
| Bridge.GetIdentifier(src) | string | nil | The 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) | string | Name shown in the Grimoire profile and on the leaderboard (read when the player's progress loads) |
| Bridge.AddMoney(src, account, amount) | boolean | Cash and bank rewards (account = 'cash' or 'bank'). Falls back to Hooks.StandaloneAddMoney when there's no player object |
| Bridge.GetItemCount(src, item) | number | Recipe ingredient checks, the RequireItem door check |
| Bridge.CanCarry(src, item, amount) | boolean | Milestone pre-check before anything is given |
| Bridge.AddItem(src, item, amount) | boolean | Every item reward. Return false if the item didn't fit, so the pack can refund or roll back |
| Bridge.RemoveItem(src, item, amount) | boolean | Brewing, 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() | boolean | Helper (true on ox_inventory). The pack itself doesn't call it |
| Bridge.Framework | string | qbx, qb, esx or standalone (from Config.Framework, or auto-detected) |
| Bridge.Inventory | string | ox_inventory, qb-inventory, esx or internal. internal turns on the Satchel tab and its Use button |
bridge/client.lua
| Function | Returns | Used for |
|---|---|---|
| Bridge.IsDead() | boolean | Dead 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.Mode | string | prompt, 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) | handle | Trick-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.
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
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
-- 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:
-- 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
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:
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)
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
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
-- 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
-- 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
-- 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
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
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
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
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:
| Key | Content |
|---|---|
| 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:
| Framework | Identifier | Example key |
|---|---|---|
| Qbox / QBCore | PlayerData.citizenid, so progress is per character | hn:2026:p:ABC12345 |
| ESX Legacy | xPlayer.identifier, as ESX gives it (per character on multicharacter) | hn:2026:p:<esx identifier> |
| Standalone | the player's license: identifier, so progress is per account | hn:2026:p:license:1a2b… |
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
| Field | Type | Holds |
|---|---|---|
| name | string | from Bridge.GetName, refreshed every time the progress loads |
| souls | number | current Souls (never below 0) |
| stats | table | lanterns, cursed, treats, tricks, brews, spirits, bosses, gathered |
| claimed | table | claimed milestones by index, { ["1"] = true, ... } |
| collected | table | personal-mode lanterns this character collected, by location index |
| satchel | table | built-in satchel contents { [item] = amount } (internal inventory only) |
| daily | table | trick-or-treat counter { day = 'YYYY-MM-DD', count } (server date) |
{
"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
| Moment | What happens |
|---|---|
| Every 30 s | All changed (dirty) players are written and the KVP is flushed to disk |
| Milestone claim | Saved and flushed immediately after a successful claim |
| Player drop | The player is saved and flushed if anything changed, then removed from the cache |
| Character logout | Saved on QBCore:Server:OnPlayerUnload and esx:playerLogout, so on a character switch the old character is saved before the new one loads |
| Resource stop | Every pending change is written and flushed |
| Leaderboard refresh | Pending changes are written first, so online players rank with their current Souls |
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.Leaderboard = {
Size = 10, -- entries shown in the Grimoire
CacheSeconds = 60, -- how long one scan is reused
}
New season & wipes
- Change
Config.Season.Id(for example to'2027') and updateStart/Finishif needed. - 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.
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 insiderklhalloween, so it can reach the pack's KVP:
-- 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
dbfolder 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
dbfolder 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.
| Check | Applies to | How it works |
|---|---|---|
| Flood guard | every client callback | At 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 + participation | lanterns, door knocks, gathering, cauldron | GlobalState.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 only | spirit kills, boss rewards, specter reward, milestone claims | Identifier + Hooks.CanParticipate (not false), with no season gate, so admin-started events still pay out. |
| Timed actions | lanterns, door knocks, gathering | The 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. |
| Distance | lanterns, doors, nodes, cauldrons | Checked 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 normalisation | lantern, door, node and milestone indexes | HN.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. |
| Availability | lanterns, nodes | The 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 & limits | doors, cauldron, curses | DoorCooldown (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 item | doors | With RequireItem = true, the server counts RequiredItem in the inventory before every knock. |
| Safe brewing | cauldron | Ingredients 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 claims | Grimoire | The 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 tokens | specter curse | Each 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 points | Haunted Night spawning | The 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 credit | Haunted Night spirits | Deaths 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 rewards | Hollow King victory | Only 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 use | potions, candy, sludge | ox_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, /hnlanterns | IsPlayerAceAllowed(src, Config.Admin.Ace), or the server console. /hnsouls also checks that the target is online and has progress. |
| Storage | KVP | Only the server reads and writes the KVP. Clients receive their own profile and the top list (names + Souls) and nothing else. |
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.
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.
| Model | Used by | Vertices / tris | Collision | LOD | Fallback |
|---|---|---|---|---|---|
| hn_soul_lantern | Lantern hunt | 18,100 / 35,972 | box | 150 | prop_veg_crop_03_pump |
| hn_cursed_lantern | Cursed lanterns | 22,620 / 44,980 | box | 150 | prop_veg_crop_03_pump |
| hn_pumpkin_head | Pumpkin Head curse, Hollow King | 11,538 / 23,076 | none (attached to peds) | 100 | prop_veg_crop_03_pump |
| hn_cauldron | Witch's Cauldron | 23,529 / 46,842 | custom mesh (metal) | 200 | prop_barrel_01a |
| hn_candy_bucket | Trick-or-treat NPC | 17,099 / 33,938 | none (held by the NPC) | 80 | no bucket |
| hn_tombstone | Haunted Night decoration | 14,966 / 27,681 | custom mesh (stone) | 200 | prop_gravestones_01a |
| hn_mandrake | Mandrake node | 17,170 / 34,144 | box | 120 | prop_plant_01a |
| hn_moonpetal | Moonpetal node | 19,951 / 39,224 | box | 120 | prop_plant_01b |
| hn_bone_pile | Bone node | 25,142 / 50,096 | box | 120 | prop_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.
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.
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
- Open its
.blendfrom the props source pack in Blender with the Sollumz add-on. - Edit the model, keeping the object name (for example
hn_cauldron), the origin and the materials. - Export it as a Drawable. On Windows, Sollumz with PyMateria exports binary
.ydrdirectly; otherwise export CodeWalker XML and import it in CodeWalker. - 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:
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.
- Zip the
rklhalloweenfolder. - Upload it at keymaster.fivem.net → Asset Escrow → Upload.
- Create a Tebex package (price 0 for a free release) and link the escrowed asset.
- 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:
| Path | What you can change |
|---|---|
| config/*.lua | Every setting: season, rewards, locations, curses, recipes, potions, Haunted Night |
| locales/*.lua | All messages and UI text (en, tl) |
| bridge/*.lua | Framework, inventory, death check and target code (for custom frameworks) |
| editable/**/*.lua | Server 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:
| Setting | Detection order ('auto') | Force it with |
|---|---|---|
| Framework | qbx_core → qb-core → es_extended → standalone | Config.Framework |
| Inventory | ox_inventory → qb-inventory (on QBCore/Qbox, includes the ps/lj forks) → ESX default → internal satchel | Config.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.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
}
| Feature | Out of season? |
|---|---|
| Lanterns, doors, nodes, cauldrons | Hidden. Every rewarded action answers "The veil is closed..." |
| Scheduled Haunted Night | Does not fire |
| /hnhaunt start | Still works. Admin starts and the StartHaunt export ignore the season, and kill/boss rewards still pay |
| Grimoire & milestones | Opens, and earned milestones can still be claimed |
| Potions & candy | Still usable |
How do players earn Souls?
Souls are the event score. Every source and its default amount:
| Activity | Souls | Key |
|---|---|---|
| Soul lantern | 1 | Config.Lanterns.Souls.normal |
| Cursed lantern | 3 (plus a curse) | Config.Lanterns.Souls.cursed |
| Treat | 1 | Config.TrickOrTreat.Souls.treat |
| Trick | 2 (plus a curse) | Config.TrickOrTreat.Souls.trick |
| Specter banished | 2 + 1 Ghost Essence | Config.Curses.specter.reward |
| Successful brew | 2 (failed brews give 0) | Config.Cauldron.Souls |
| Gathering | 0 for every node | Config.Gathering.Nodes.<node>.souls |
| Spirit kill | 2 to the killer, plus a 35% chance of Ghost Essence | Config.Haunted.Rewards.Kill |
| Hollow King defeated | 25 to every participant still near the zone | Config.Haunted.Rewards.Boss.souls |
| Admin / scripts | Any 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.
---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.
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.
| Key | Adds | Format |
|---|---|---|
| Config.Lanterns.Locations | Lantern spots | vec3 (keep the first three numbers). Height snaps to the ground |
| Config.TrickOrTreat.Doors | Trick-or-treat doors | vec4. Heading = the way the NPC faces. Height is used exactly as written |
| Config.Gathering.Nodes.<node>.points | Mandrake, Moonpetal, Bone Pile nodes | vec3. Height snaps to the ground |
| Config.Cauldron.Locations | Cauldrons (each gets a witch and a blip by default) | vec4. Heading rotates the cauldron |
| Config.Haunted.Zones | Haunted Night arenas | { label, center = vec3, radius }. Keep center at ground height |
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 = falseon any curse inConfig.Curses, or lower itsweight. 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:
| Thing | How it's created | Who sees it |
|---|---|---|
| Spirits & Hollow King | Server-created peds. A player inside the zone only sends ground positions, and each spirit's owner runs its AI | Everyone |
| Pumpkin Head curse | Player state bag hnPumpkin. Every client attaches its own local prop | Everyone nearby |
| Boss pumpkin head | Local prop on each client | Everyone nearby |
| Lanterns, nodes, cauldrons, tombstones | Local props, spawned by each client within render distance | Everyone (same spots) |
| Witch NPC | Local ped on each client | Everyone (own copy) |
| Door NPC + candy bucket | Local, on the knocking player's client | Only the knocking player |
| Haunting Specter | Local ped on the cursed player's client. The kill reward is checked with a one-time server token | Only 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 = 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:
| Model | Collision | LOD distance |
|---|---|---|
| hn_soul_lantern / hn_cursed_lantern | Box | 150 |
| hn_pumpkin_head | None (attached to peds) | 100 |
| hn_cauldron | Custom mesh (metal) | 200 |
| hn_candy_bucket | None (held by the NPC) | 80 |
| hn_tombstone | Custom mesh (stone) | 200 |
| hn_mandrake / hn_moonpetal / hn_bone_pile | Box | 120 |
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
- Read the startup line. Every start, the server console prints the detected framework and inventory. If it says
standalone/internalon a framework server, see Wrong framework or inventory detected. - 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. - Turn on
Config.Debugto log every rewarded action (see Debug mode at the bottom).
[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 ninehn_*.ydrfiles andhn_props.ytyp.fxmanifest.luastill hasdata_file 'DLC_ITYP_REQUEST' 'stream/hn_props.ytyp'and'stream/hn_props.ytyp'infiles.- You did a full server restart after adding or replacing streamed files.
restart rklhalloweenis 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/.
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:
| Activity | When an item doesn't fit |
|---|---|
| Lanterns · treats · spirit kills · boss | Souls are still added. The item that didn't fit is skipped and the message shows |
| Gathering | Nothing is given and the node stays, so the player can try again |
| Cauldron | The ingredients are given back and no potion is made |
| Milestones | Checked 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 whileConfig.DisableWhileDead = true(it checks QBCoreisdead/inlaststandmetadata, QboxisDead/inLastStandstate 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 exactzcounts. Record doors with/hncoordswhile standing where the player should stand. - Nothing to interact with: the season is closed, the node was already gathered (it comes back after
respawnseconds), 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'svec4, so aim at that exact spot. hn_pumpkin_headandhn_candy_buckethave 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'sInteractDistance. Config.Debug = truedraws 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
centerheight, not on water, and 5 to 90 m (flat distance) from the host player, so keepcenter.zat 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.
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:
| Feature | What it does | Setting |
|---|---|---|
| hn_potion_swift | SetRunSprintMultiplierForPlayer ×1.25 for 60s (capped at 1.49) | Config.Potions.hn_potion_swift.multiplier |
| hn_candy | Sprint ×1.15 for sugarRush seconds (10) | Config.Potions.hn_candy.sugarRush = 0 turns it off |
| hn_potion_feather | Caps fall speed with SetEntityVelocity | Config.Potions.hn_potion_feather.maxFallSpeed |
| hn_potion_phantom | SetEntityAlpha + SetLocalPlayerAsGhost | Remove 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) |
| levitate | Freezes the player and moves them up and down every frame | Config.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.Localemust match the table key exactly ('tl'forLocales['tl']). Restart the resource after changing it.- A new locale file must be loaded, either listed in
fxmanifest.luashared_scriptsor 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.Debug = true -- turn it off again on a live server
With debug on:
Hooks.Logprints every logged action to the server console:lantern,trick_or_treat,gather,brew,curse,consume,specter_killed,milestone,boss_defeatedandadmin_souls.- Target zones are drawn in the world (
debugon ox_target,debugPolyon qb-target), so you can see the door spheres.
[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}
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):
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
local mdef = exports['rklequipment']:ApplyMagicDefense() or 0
local final = math.max(1, spellDamage - mdef)
Don't interrupt an active combo
if exports['rklequipment']:IsCombatMode() then
lib.notify({ description = 'Finish your attack first', type = 'error' })
return
end
Reward forge materials from a quest
-- 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
exports['rklgrimoire']:AddGrimoireXP(src, 25)
Pause casting during a cutscene
exports['rklskillsystem']:SetDisableSkills(true)
exports['rklskillsystem']:SetUIHidden(true)
-- ...play cutscene...
exports['rklskillsystem']:SetUIHidden(false)
exports['rklskillsystem']:SetDisableSkills(false)
Gate a feature by unlocked skill
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:
| Host | How |
|---|---|
| GitHub Pages | Put index.html in a repo → Settings → Pages → deploy from branch |
| Cloudflare Pages | Connect the repo, or drag-and-drop the file — free & fast |
| Netlify | Drag 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.