Equip Legality (weapons, armor, techniques)

The key words MUST / MUST NOT / SHOULD are used as in RFC 2119. This page defines which gear a class can use — weapons (by type), armor (by body type: frame / armor / robe), and which techniques a class can learn (and to what level) — and the one ✕ "cannot use/equip" marker that surfaces it. It is the normative contract behind every screen that shows that marker or gates equipping: the Equip menu, the 3D field weapon picker, storage, and all of the shops.

The invariant: the ✕ marker and the actual equip action MUST agree everywhere. If a row shows no ✕ it MUST equip; if it shows a ✕ it MUST NOT equip. Both sides route through one predicate (EquipmentUtils.item_fits_slot), so they cannot drift — see UI surfaces for the full map.

Contract

Class × weapon-type matrix

Current allowed_weapon_types per class. Columns: Sab·Swd·Dgr·Claw·DSab(Double Saber)·Spr(Spear)·Slc(Slicer)·Hgn(Handgun)·Mgn(Mechgun)·Rfl(Rifle)·Bzk(Bazooka)·Rod·Wnd. (Gun Blade, Shield, and Laser Cannon are omitted — no class allows them; see Issues.)

ClassSabSwdDgrClawDSabSprSlcHgnMgnRflBzkRodWnd
HUmar········
HUmarl·······
HUnewm·······
HUnewearl·······
HUcast········
HUcaseal·······
RAmar·······
RAmarl·······
RAcast·······
RAcaseal·······
FOmar········
FOmarl········
FOnewm·········
FOnewearl·········

Broad strokes hold (Hunters = melee + handgun, Rangers = guns + saber, Forces = rod/wand + handgun), and every class can use a Saber + Handgun.

Armor (frame · armor · robe)

Armor comes in three body types. Frames are universal; the heavy/light split is by race + role, not class alone:

Body typeEquippable byusable_by
Frame (universal)Every class(empty)
Armor (heavy)Human & Cast Hunters and RangersHunter Human, Hunter Cast, Ranger Human, Ranger Cast
Robe (light / caster)Newman Hunters and all ForcesHunter Newman, Force Human, Force Newman

The low-DEF body types — Newmen and Forces — wear robes; Human and Cast Hunters/Rangers wear armor; everyone wears frames. (Rangers exist only as Human or Cast — there is no Newman Ranger — and Forces only as Human or Newman, so every class lands in exactly one of armor/robe.)

ClassFrameArmorRobe
HUmar·
HUmarl·
HUnewm·
HUnewearl·
HUcast·
HUcaseal·
RAmar·
RAmarl·
RAcast·
RAcaseal·
FOmar·
FOmarl·
FOnewm·
FOnewearl·

Mechanism. Each armor carries ArmorData.usable_by (a list of "Type Race" strings) — the only source of armor legality (there is no ClassData armor analog), evaluated through EquipmentUtils.item_fits_slot(id, "frame") + DebugConfig.equip_all. The three body types above are the broad rule; an individual piece can narrow it further via its own list (e.g. a cast-only armor, or a robe that also admits Ranger Humans) — that per-item list is the override mechanism. 27 of 50 armors carry a restriction; the rest are open frames.

Technique disks (learnability)

A technique disk (disk_<technique>_<level>) teaches a technique when used. Its ✕ marker means the class can never learn that technique — TechniqueManager.class_can_learn:

Class × technique max level

What each class can learn, and the cap (max disk level) per group. = the class can never learn that group (permanent ✕ on any such disk). The cap lives in ClassData.technique_limits, keyed by group. Columns:

ClassAtkGrantsMegidSupportBuffDebuff
HUmar101010
HUmarl101010
HUnewm101010
HUnewearl101010
HUcast
HUcaseal
RAmar101010
RAmarl101010
RAcast
RAcaseal
FOmar151515151515
FOmarl151515151515
FOnewm151515151515
FOnewearl151515151515

Reading it: Forces learn everything to Lv.15. Hunters get attack + Resta + Shifta/Deband to Lv.10 (no Grants/Megid, no Jellen/Zalure). Rangers get attack + Resta + Jellen/Zalure to Lv.10 (no Shifta/Deband). CASTs (all four) learn no techniques — they use traps instead (ClassData.trap_limits, a separate system). So every disk carries the ✕ for a CAST.

Two further gates on top of the class cap (both temporary — grey, no ✕): the player must meet the disk's required character level (get_disk_required_level: Lv.1–5 disks need that level; higher disks need level×2−5), and a disk at or below the technique's current level is rejected as a downgrade / already-known.

UI surfaces — one predicate, everywhere

Every surface below routes its ✕ marker and its equip/use action through the same gate, so marker == action by construction. Weapons/armor go through item_fits_slot (markers via the permanent-only wrapper ShopNav.sell_cannot_use); disks go through class_can_learn.

SurfaceShows ✕ forPredicate
Weapon shop — buy (weapons, armor)weapon, armor_check_equippabilityitem_fits_slot
Weapon shop — sellweapon, armor, diskShopNav.sell_cannot_use
Item shop — sellweapon, armor, diskShopNav.sell_cannot_use
Item shop — buy disksdiskclass_can_learn
Crafting / synthesisweapon (output)_output_unequippableitem_fits_slot
Storageweapon, armor, diskstorage._item_cannot_usesell_cannot_use
3D field start menu — items listweapon, armor, diskstart_menu_renderer._item_cannot_usesell_cannot_use
3D start menu — equip (action)weapon, armor_slot_key_for_inventory_item / _get_equip_candidatesitem_fits_slot
2D equipment screen (action)weapon, armoritem_fits_slot
In-field weapon palette swap (action)weapon_QuickWeaponMenu._build_weapon_listitem_fits_slot

Units, mags, materials, and consumables carry no permanent class block, so they never get a ✕ from this gate. (Consumables have their own ConsumableData.usable_by — a separate, legitimate source used only by the item shop's consumable rows.)

The grey tier routes through one predicate too. The temporary "grey, no ✕" disk block — a disk for a technique already known at that level or higher, or below the required player level — MUST mute the row on every item-list surface, not just the buy tab. All of them route it through the single companion predicate ShopNav.sell_disabled (the can_learn-but-not-class_can_learn slice), exactly as the ✕ tier routes through sell_cannot_use: the item and weapon shop SELL lists, the storage deposit/withdraw lists, and the field start-menu inventory. (The item shop's buy-disks tab computes the same slice inline from can_learnclass_can_learn.) So owning five Lv.1 Foie disks and learning Foie Lv.1 greys the other four everywhere they appear — they are still sellable/depositable, just no longer useful to learn.

The grey tier gates the use action too — marker == action holds for grey, not just ✕. Wherever a disk row is greyed by sell_disabled, the surface's "use" affordance MUST also be withdrawn, exactly as a ✕ row withdraws its equip action. Concretely, the 3D field start-menu item modal builds its Use choice from the row's usable flag, and a disk is usable only when it can be learned right now — not sell_cannot_use(id) and not sell_disabled(id). So an already-known (or too-low-level / class-illegal) disk offers no Use, only Drop / Sort: the greyed row and the missing action can never disagree. The disabled row style MUST be clearly distinct from a normal row (a faded text colour and a dimmed icon), so "you can't use this" is unmissable without a ✕.

Beyond the tables: per-item exceptions (planned)

The tables above are the base layer and the right default, but legality won't always reduce to "class × type." Some gear needs a finer, per-item rule a class-wide table can't express:

The intended shape is tables + a per-item override: the class×type / class×technique-cap / armor-body-type tables decide the default, and an optional per-item allow/deny list (keyed by Type / Race / Gender) narrows or widens a single piece. Armor already works this wayArmorData.usable_by is the live per-item override layered on the body-type rule. Weapons would gain the same: a cleaned per-item field on top of allowed_weapon_types. The shared predicate (EquipmentUtils.item_fits_slot) stays the single gate — the override is just one more input it folds in, so every UI surface inherits exceptions for free and marker == action still holds.

Techniques are different — the table is already fine-grained. The technique matrix is per-class-per-group, so balance exclusions are just a cap of 0 in ClassData.technique_limits, no new mechanism. RAmar/RAmarl already have shiftaDeband = 0 (no buffs). Note Hunters currently have shiftaDeband = 10 (they can buff) — if Hunters shouldn't, that's a one-value data change, not an exception system. Per-disk exceptions aren't anticipated; the class cap is the lever.

Issues to reconcile

The current data predates this contract and needs a pass (tracked with the legality refactor):

Testing

Two layers (the project's frame/UI rule):