Classes and Attributes¶
Player classes port cleanly. Attributes — one of Helix's more distinctive subsystems — have no direct Parallax equivalent and need to be rebuilt with a few existing pieces.
Table of Contents¶
- Classes
- Field Mapping
- Class Validation
- A Full Worked Example
- Ranks
- Attributes
- What Helix Attributes Did
- The Port Pattern
- Full Attributes Module
- Consuming Attribute Values
- Pitfalls
Classes¶
Classes in both frameworks are subdivisions of factions. A player belongs to exactly one faction, and optionally a class within it. The file layout and registration flow are almost identical.
Field Mapping¶
| Helix | Parallax | Notes |
|---|---|---|
CLASS.name |
CLASS.name |
Same. |
CLASS.description |
CLASS.description |
Same. |
CLASS.faction |
CLASS.faction |
Faction ID or index; same. |
CLASS.color |
CLASS.color |
Same. |
CLASS.isDefault |
CLASS.isDefault |
Same — class players start in. |
CLASS.weapons |
(use PlayerLoadout hook) |
No first-class loadout field. |
CLASS.models |
CLASS.models |
Same. |
CLASS.limit |
(use CanBecome) |
Merge the limit check into the validation function. |
CLASS:CanSwitchTo(client) |
CLASS:CanBecome(client) |
Rename. |
CLASS:OnSet(client) |
Use OnPlayerBecameClass hook. |
|
CLASS:OnLeave(client) |
Use OnPlayerBecameClass hook (check old class). |
|
CLASS:OnSpawn(client) |
Use PlayerLoadout hook filtered by char:GetClass(). |
|
Global CLASS_TCP etc. |
Global CLASS_TCP etc. |
Same convention. |
Library call mapping:
| Helix | Parallax |
|---|---|
ix.class.Get(identifier) |
ax.class:Get(identifier) |
ix.class.GetPlayers(classID) |
Iterate players and check char:GetClass(). |
ix.class.CanSwitchTo(client, classID) |
ax.class:CanBecome(identifier, client) |
ix.class.LoadFromDir(dir) |
ax.class:Include(dir, timeFilter) |
Class Validation¶
The dispatch model:
CanPlayerBecomeClasshook fires (framework-level, schema, modules).- Class's own
CanBecomemethod runs (if defined). - First
falsereturn blocks the change.
-- Helix
function CLASS:CanSwitchTo(client)
if ( client:GetCharacter():GetAttribute("strength") < 10 ) then
return false
end
return true
end
-- Parallax
function CLASS:CanBecome(client)
local char = client:GetCharacter()
if ( ax.character:GetVar(char, "attributes", "strength", 0) < 10 ) then
return false, "You need 10 strength to join this class."
end
return true
end
Parallax supports returning a reason string as the second value; it's surfaced to the player via the notification system.
A Full Worked Example¶
Helix (schema/classes/sh_tcp.lua):
CLASS.name = "Transhuman Arm"
CLASS.description = "The elite of the Overwatch."
CLASS.faction = FACTION_MPF
CLASS.isDefault = false
CLASS.weapons = {
"weapon_ar2",
"weapon_frag",
}
function CLASS:CanSwitchTo(client)
local char = client:GetCharacter()
return char:GetData("canBecomeTCP", false)
end
function CLASS:OnSet(client)
client:SetHealth(150)
end
CLASS_TCP = CLASS.index
Parallax (<your-schema>/gamemode/schema/classes/sh_tcp.lua):
CLASS.name = "Transhuman Arm"
CLASS.description = "The elite of the Overwatch."
CLASS.faction = FACTION_MPF
CLASS.isDefault = false
function CLASS:CanBecome(client)
local char = client:GetCharacter()
if ( !ax.character:GetVar(char, "data", "canBecomeTCP", false) ) then
return false, "You are not authorized to join the Transhuman Arm."
end
return true
end
CLASS_TCP = CLASS.index
And then in a shared PlayerLoadout hook:
-- <your-schema>/gamemode/schema/hooks/sv_hooks.lua
function SCHEMA:PlayerLoadout(client)
local char = client:GetCharacter()
if ( !char ) then return end
if ( char:GetClass() == CLASS_TCP ) then
client:Give("weapon_ar2")
client:Give("weapon_frag")
client:SetHealth(150)
end
end
Loadout centralization matches the faction pattern — one function deciding loadout for everyone is easier to reason about than per-class methods scattered across files.
Ranks¶
Parallax has a third tier below classes: ax.rank. Helix has no equivalent — rank-like concepts were usually expressed either as character flags or as a custom character var.
Ranks are positional titles within a class (e.g. within the "MPF" faction's "i3" class, ranks could be 1E, 2E, 3E, etc.). They register in schema/ranks/ just like factions and classes:
-- <your-schema>/gamemode/schema/ranks/sh_mpf_1e.lua
RANK.name = "MPF-1E"
RANK.description = "Standard Metropolice rank."
RANK.class = CLASS_MPF_I3
RANK.color = Color(50, 50, 120)
RANK.isDefault = true
If your Helix schema had a rank-like char var, consider whether promoting it to a rank would be cleaner. The ranks UI surface (scoreboard badges, chat prefixes, faction-internal HUD) gets you structure Helix never had.
Attributes¶
Helix's ix.attributes subsystem is a per-character bag of named stat values — strength, agility, endurance, etc. Values are capped by the schema, spent at character creation, optionally trained up over time, and queried throughout the schema for gameplay gating.
Parallax has no built-in attributes system. The port requires you to build it on top of existing primitives.
What Helix Attributes Did¶
A Helix attribute file:
-- schema/attributes/sh_strength.lua
ATTRIBUTE.name = "Strength"
ATTRIBUTE.description = "Affects how much weight you can carry."
ATTRIBUTE.default = 0
ATTRIBUTE.max = 100
The ix.attributes.LoadFromDir loader collected these into a shared registry. Characters stored their per-attribute values in character.vars.attributes, a table indexed by attribute unique ID. Methods like character:GetAttribute("strength") returned the current value; character:UpdateAttrib("strength", 5) added to it with clamping.
The subsystem also hooked into character creation: a certain number of "points" were budgeted per character, distributed across attributes in the creation UI, and validated on submit.
The Port Pattern¶
In Parallax, the idiomatic build is:
- A character var named
attributesoffieldType = ax.type.data, holding{ strength = N, agility = N, ... }. - A small library table (
SCHEMA.attributesor a module-local) registering each attribute's metadata. - Helper methods on the character meta:
GetAttribute,SetAttribute,BoostAttribute. - A character-creation hook that budgets points and validates the distribution.
Full Attributes Module¶
Create a module for the subsystem to keep it self-contained and optional.
<your-schema>/gamemode/modules/attributes/
├── boot.lua
├── libraries/
│ └── sh_attributes.lua
└── attributes/
├── sh_strength.lua
├── sh_agility.lua
└── sh_endurance.lua
boot.lua:
MODULE.name = "Attributes"
MODULE.description = "Character stat system with per-schema attributes and training."
MODULE.author = "Your Name"
-- Register the backing character var.
ax.character:RegisterVar("attributes", {
fieldType = ax.type.data,
default = {},
})
function MODULE:OnLoaded()
-- The attributes/ subdir has already been ignored by autoload because
-- it's not in the pre-loaded list; walk it manually.
local dir = "<your-schema>/gamemode/modules/attributes/attributes"
-- Replace with your actual schema path or derive from MODULE.folder.
-- Load each sh_<name>.lua into MODULE.stored.
-- (Implementation elided; call ax.util:Include per file.)
end
return MODULE
libraries/sh_attributes.lua:
MODULE = MODULE or ax.module:Get("attributes")
MODULE.stored = MODULE.stored or {}
-- Register a specific attribute definition.
function MODULE:Register(uniqueID, data)
data.uniqueID = uniqueID
data.name = data.name or uniqueID
data.default = tonumber(data.default) or 0
data.max = tonumber(data.max) or 100
data.description = data.description or ""
self.stored[uniqueID] = data
end
-- Character-meta helpers.
function ax.character.meta:GetAttribute(id, default)
local attrs = ax.character:GetVar(self, "attributes", id)
if ( attrs == nil ) then return default or 0 end
return attrs
end
function ax.character.meta:SetAttribute(id, value, bNoSync)
local mod = ax.module:Get("attributes")
local def = mod and mod.stored[id]
if ( !def ) then return false end
value = math.Clamp(tonumber(value) or 0, 0, def.max)
ax.character:SetVar(self, "attributes", id, {
dataValue = value,
bNoNetworking = bNoSync,
})
return true
end
function ax.character.meta:BoostAttribute(id, delta)
return self:SetAttribute(id, self:GetAttribute(id, 0) + delta)
end
attributes/sh_strength.lua:
local mod = ax.module:Get("attributes")
mod:Register("strength", {
name = "Strength",
description = "Affects how much weight you can carry.",
default = 0,
max = 100,
})
Consuming Attribute Values¶
Once ported, consumers look the same as in Helix:
-- Helix
if ( char:GetAttribute("strength") >= 10 ) then ... end
-- Parallax (with the module loaded)
if ( char:GetAttribute("strength") >= 10 ) then ... end
Inventory weight scaling is a common dependent subsystem in Helix. In Parallax, adjust inventory.maxWeight as a function of strength in a PlayerLoadout hook or whenever the strength value changes:
hook.Add("OnCharacterVarChanged", "axWeightByStrength", function(char, name, value)
if ( name != "attributes" ) then return end
local inv = ax.inventory.instances[char:GetInventoryID()]
if ( !inv ) then return end
local strength = ax.character:GetVar(char, "attributes", "strength", 0)
inv.maxWeight = 20 + strength * 0.5 -- base 20 + bonus
end)
Character Creation Points¶
Helix's attributes integrated into the character creation panel with a point-budget UI. Parallax's character creation flow is theme-driven and doesn't auto-inject per-attribute panels. Your options:
- Skip point distribution at creation. All characters start at the attribute defaults; training is the only way to level up. Simplest port.
- Custom creation panel. Add a step to your character creation UI that shows attribute sliders summing to a budget. This is real UI work — see
08-UI_THEME_GUIDELINES.mdin the main docs. - Post-creation dialogue. Players pick their distribution in an in-game menu after spawning. Avoids touching the creation flow.
If you don't need the creation-time budget and training is your only progression mechanic, the simpler defaults-plus-training approach is dramatically less porting work.
Pitfalls¶
CanSwitchTovsCanBecome. Grep for both — they're the same concept but old Helix code sometimes uses either name.- Class loadouts scattered across files.
CLASS:OnSpawnin every class file is readable but hard to audit. Centralize inPlayerLoadout; leave a comment pointing to the hook from each class file. - Attribute precision loss. Helix stored integers. If you use fractional values, make sure your
math.Clampandmath.Roundcalls line up with the UI's resolution (e.g. don't show "Strength: 9.833"). - Forgetting to register the
attributesvar. If you write to it before it's registered, Parallax logs an error and the write is lost. Register inboot.luabefore any attribute sub-file loads. - Attribute file discovery. Parallax's module loader auto-walks known content subdirs but
attributes/is not in the preloaded list; load it manually fromboot.luaor rename the directory to something auto-loaded (likelibraries/) and accept less semantic clarity. ranks/vsclasses/. Clarify the distinction in your schema docs; players often confuse the two. Classes are "what role within the faction", ranks are "what seniority within the class".
You're Done¶
That's the complete Helix → Parallax porting guide as it stands. For anything not covered here, the main framework docs (../../README.md) are the next stop, and the Parallax source itself is usually the shortest path to a definitive answer. When in doubt, grep both codebases side-by-side for the concept you're porting — the similarities are usually closer than the documentation makes them look.
Back to: ../README.md