npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

d2r-affix-lib

v0.2.4

Published

Bundled D2R affix data (ranges, tiers, contributors) for community item tools.

Readme

d2r-affix-lib

Bundled D2R affix data for community item tools (item valuators, triage helpers, card-assemblers). Provides authoritative roll ranges and per-affix contributors so consumers don't have to hand-compile this data from magicprefix.txt / magicsuffix.txt.

Part of the D2R Tools project — a constellation of community tools for Diablo II: Resurrected.

Status: v0.2.4. Coverage: ten item types — ring, scharm, lcharm, gcharm, circlet, boots, amulet, gloves, belt, jewel. Crafts are first-class on rings, boots, amulets, gloves, and belts (Quality.CRAFT_BLOOD / CRAFT_CASTER / CRAFT_HITPOWER / CRAFT_SAFETY). Both forward-compat craft patterns are now live: cardRangeGap populated by caster-amulet FCR (v0.2.1, discontinuous {5..20} with gap {11..14}), and craft-only stats populated by glove crushing_blow (v0.2.2, empty rare pool with a populated craftBonuses sidecar). The full design rationale, response shapes, card-range derivation rules, and open questions live in docs/SPEC.md; craft semantics get their own deep-dive in docs/CRAFTS-DESIGN.md.

Install

npm install d2r-affix-lib

Quick start

import {
  getAbilityForItem,
  Abilities,
  ItemTypes,
  Quality,
  SkillTabs,
} from "d2r-affix-lib";

// Non-parametric — get the rolled-up range plus every contributing affix.
getAbilityForItem(ItemTypes.SCHARM, Abilities.LIFE);
// → {
//     parametric: false,
//     cardRange: { min: 5, max: 20 },
//     contributors: [
//       { affixId: 349, name: "of Life",        kind: "suffix", group: 26, alvl: 1,  levelreq: 14, min: 5,  max: 10, primary: true,  frequency: 4 },
//       { affixId: 350, name: "of Substinence", kind: "suffix", group: 26, alvl: 23, levelreq: 17, min: 11, max: 15, primary: true,  frequency: 4 },
//       { affixId: 351, name: "of Vita",        kind: "suffix", group: 26, alvl: 47, levelreq: 39, min: 16, max: 20, primary: true,  frequency: 4 },
//     ],
//   }

// Parametric — fetch all 20 skill-tab variants on a circlet.
const tabs = getAbilityForItem(ItemTypes.CIRCLET, Abilities.PLUS_SKILL_TAB);
// → { parametric: true, paramKind: "skilltab", variants: [...20...] }

// Filter to a specific tab + ask for magic-quality (the +3 magic-only tier
// rolls only on magic circlets).
getAbilityForItem(ItemTypes.CIRCLET, Abilities.PLUS_SKILL_TAB, {
  filter: { kind: "tab", tab: SkillTabs.AMA_PASSIVE_AND_MAGIC },
  quality: Quality.MAGIC,
});
// → { parametric: true, paramKind: "skilltab", variants: [
//      { param: { tab: "ama_passive_and_magic" },
//        cardRange: { min: 1, max: 3 },        // includes Athlete's (+3 magic-only)
//        contributors: [Acrobat's, Gymnast's, Athlete's] }
//    ] }

// Same query with Quality.RARE — Athlete's is filtered out.
getAbilityForItem(ItemTypes.CIRCLET, Abilities.PLUS_SKILL_TAB, {
  filter: { kind: "tab", tab: SkillTabs.AMA_PASSIVE_AND_MAGIC },
  quality: Quality.RARE,
});
// → cardRange.max === 2

// Crafts (new in v0.2.0) — RARE surfaces craftBonuses sidecar:
getAbilityForItem(ItemTypes.RING, Abilities.LIFE_LEECH);
// → {
//     parametric: false,
//     cardRange: { min: 3, max: 8 },              // rare-pool envelope
//     contributors: [...the three rare LL tiers...],
//     craftBonuses: [{ craft: "blood", min: 1, max: 3 }],   // ← new
//   }

// Same ability under Quality.CRAFT_BLOOD returns the projected view:
getAbilityForItem(ItemTypes.RING, Abilities.LIFE_LEECH, {
  quality: Quality.CRAFT_BLOOD,
});
// → cardRange = { min: 1, max: 11 }   ← composed: craft.min .. rare.max + craft.max

// Returns null when the ability cannot roll on the given item type, when
// a filter doesn't match any variant, or when a CRAFT_* mode doesn't apply.
getAbilityForItem(ItemTypes.SCHARM, Abilities.PLUS_CLASS_SKILLS);
// → null
getAbilityForItem(ItemTypes.SCHARM, Abilities.LIFE, {
  quality: Quality.CRAFT_BLOOD,
});
// → null  (charms can't be crafted)

Coverage (v0.2.4)

| Item type | Slug | In v0.2.4? | Crafts? | | --------------------- | --------- | -------------------- | ------- | | Rings | ring | ✓ | ✓ | | Small charms (1×1) | scharm | ✓ | — | | Large charms (1×2) | lcharm | ✓ | — | | Grand charms (1×3) | gcharm | ✓ | — | | Circlets | circlet | ✓ | — | | Boots | boots | ✓ | ✓ | | Amulets | amulet | ✓ | ✓ | | Gloves | gloves | ✓ | ✓ | | Belts | belt | ✓ | ✓ | | Jewels | jewel | ✓ | — | | All other slots | — | not yet | |

Charms and jewels are magic + rare quality (charms are magic-only by game rules, jewels are dual-quality), and neither can be crafted; circlets aren't a Horadric Cube craft input. All three correctly return null for any CRAFT_* query.

Quality

options.quality controls which roll-stack the cardRange and contributor list reflect:

| Value | What you get | |---|---| | Quality.RARE (default) | Rare-quality items: up to 3 prefixes + 3 suffixes (one tier per group, summed). Magic-only tiers are filtered out. When one or more crafts contribute to the queried ability, a craftBonuses? sidecar lists each applicable craft's min/max addendum. | | Quality.MAGIC | Magic-quality items: 1 prefix + 1 suffix max, full pool (including magic-only tiers). No craftBonuses (magic items can't be crafted). | | Quality.CRAFT_BLOOD / CRAFT_CASTER / CRAFT_HITPOWER / CRAFT_SAFETY | Crafted-rare items. Returns the projected view — the rare-pool envelope composed with the craft's fixed bonus. Discontinuous projections (when present) carry a cardRangeGap? field marking unreachable values inside the outer envelope. |

Charms are magic-only by game rules. Quality.RARE on a charm collapses to Quality.MAGIC for ergonomic forgiveness; Quality.CRAFT_* on a charm returns null.

The full craft-mode design — sidecar vs. projection rationale, worked examples (ring LL contiguous, amulet FCR discontinuous), and migration notes — lives in docs/CRAFTS-DESIGN.md.

Integration patterns

Crafts give consumers two integration paths. Pick based on whether you already know the craft mode of the item you're reasoning about.

Path A — "What's possible?" (don't know the craft, or showing a generic range)

Use Quality.RARE (the default) and read both cardRange and the craftBonuses sidecar. This is the right path for triage UIs, OCR pipelines that haven't yet identified the craft, and any "show me the legal range" display.

// User entered "+6% Life Leech" on a ring. Is that valid?
const r = getAbilityForItem(ItemTypes.RING, Abilities.LIFE_LEECH);
// → cardRange:    { min: 3, max: 8 }
//   craftBonuses: [{ craft: "blood", min: 1, max: 3 }]

const value = 6;

// Reachable on a non-crafted rare:
const reachableOnRare = value >= r.cardRange.min && value <= r.cardRange.max;
// → true (3 ≤ 6 ≤ 8)

// Reachable on a blood-crafted rare (rare stack + craft addendum):
const blood = r.craftBonuses?.find((cb) => cb.craft === "blood");
const reachableOnBloodCraft =
  blood !== undefined &&
  value >= blood.min &&                              // craft is always present
  value <= r.cardRange.max + blood.max;              // stacked ceiling
// → true (1 ≤ 6 ≤ 11)

If you only need the rare-pool answer (no crafting in your domain), ignore craftBonuses and you're done. The sidecar is informationally additive; nothing about the primary cardRange or contributors shifts because of it.

Craft-only stats — some abilities (e.g. REGENERATE_MANA on rings) only roll via crafts. Under default RARE the response shape is:

const r = getAbilityForItem(ItemTypes.RING, Abilities.REGENERATE_MANA);
// → cardRange:    { min: 0, max: 0 }      ← no rare-pool contribution
//   contributors: []                       ← none
//   craftBonuses: [{ craft: "caster", min: 4, max: 10 }]

So the rule is simple: the answer is wherever it has range. If cardRange is {0,0} and contributors is empty, look in craftBonuses (or use Quality.CRAFT_* to promote the craft into the cardRange directly — see Path B).

Path B — "Is this specific craft valid?" (the craft mode is known)

Use Quality.CRAFT_BLOOD / CRAFT_CASTER / CRAFT_HITPOWER / CRAFT_SAFETY. The lib does the projection internally — cardRange is the union envelope of "craft alone" and "rare + craft" outcomes — and surfaces cardRangeGap when there's a hole inside the envelope. One call yields a correct-by-construction validator.

// User confirmed: this is a blood-crafted rare ring with +6% LL.
const r = getAbilityForItem(ItemTypes.RING, Abilities.LIFE_LEECH, {
  quality: Quality.CRAFT_BLOOD,
});
// → cardRange:    { min: 1, max: 11 }     ← projected envelope
//   cardRangeGap: undefined               ← contiguous, no hole

const value = 6;
const inGap =
  r.cardRangeGap !== undefined &&
  value >= r.cardRangeGap.min &&
  value <= r.cardRangeGap.max;

const valid =
  !inGap && value >= r.cardRange.min && value <= r.cardRange.max;
// → true. Exhaustive by construction — no "did I remember to add craft?"

Always check cardRangeGap even if you "expect" the projection to be contiguous on your data — it's the explicit signal for discontinuous ranges, and forgetting it on (e.g.) a caster amulet would silently accept unreachable values:

// Live in v0.2.1: caster amulet FCR is the canonical discontinuous case.
//   of the Apprentice (rare):          +10
//   craft addendum (caster):           +5..10
//   stacked (rare + craft):            +15..20
//   union:                             {5..10} ∪ {15..20}
//   ↳ values 11-14 are unreachable
const r = getAbilityForItem(ItemTypes.AMULET, Abilities.FASTER_CAST_RATE, {
  quality: Quality.CRAFT_CASTER,
});
// → cardRange:    { min: 5,  max: 20 }
//   cardRangeGap: { min: 11, max: 14 }    ← always check this

A craft mode that doesn't apply to the queried (itype, ability) returns null rather than a zeroed shape — call it on Quality.CRAFT_HITPOWER for ring MANA and you get null directly. Charms and circlets (which can't be crafted) return null for any CRAFT_* query, with no per-itype branching needed on your side.

Choosing between the two paths

| Use Path A (RARE default + sidecar) when… | Use Path B (CRAFT_* projection) when… | |---|---| | You're showing a generic "valid range" or contributor list. | You're scoring a specific item whose craft mode is known. | | You haven't yet identified whether the item is crafted (e.g. early in an OCR pipeline). | You want a single call that returns a correct-by-construction validator. | | You want to display "rare ceiling: 8, blood adds +1..3" in the UI. | You want one cardRange plus an optional cardRangeGap. | | Your domain has no concept of crafted items. | You've already filtered down to "this is a blood ring." |

Both paths share the same underlying data — the difference is whether the lib does the composition for you (Path B) or hands you the primitives so you can compose conditionally (Path A).

Contributor fields

Each Contributor returned in a non-parametric result (or inside a parametric variant) carries:

| Field | Meaning | | ----------- | -------------------------------------------------------------------------------------------------------------------- | | affixId | 1-based line number in magicprefix.txt / magicsuffix.txt. Stable id; the canonical reference for this contributor. | | name | Display name from the txt row (e.g. "Bronze", "of the Sun"). | | kind | "prefix" or "suffix". | | group | The group column from the txt — affixes in the same group are mutually exclusive on a single item. | | alvl | Affix level — gates which item ilvl can roll this tier. | | levelreq | Character level required to wear an item carrying this affix. | | min/max | This contributor's min/max contribution to the queried ability. | | primary | true when the ability is a user-controlled axis on the affix; false for side-effect contributions (e.g. the +15 AR riding along on of Light). | | frequency | Spawn weight inside the contributor's group. Useful for odds math. |

Versioning

Bundled JSON means semver here governs the data layer plus the API surface:

| Bump | Trigger | |---|---| | patch | Data corrections, new item types, new ability ids exposed via existing methods. Anything additive at the data layer that doesn't change the API surface. | | minor | New methods (getAffix()), new options on existing methods (options.craft), new public exports. Things that expand what consumers can call. | | major | Breaking changes (id renames, response-shape changes, removed item types). |

Pinning ^0.1.0 is safe and gets you every itype expansion automatically. You only need to widen your range when the API actually changes shape (a minor or major bump).

License

PolyForm Noncommercial License 1.0.0 — copyright © 2026 Alex Beverage.

Free to use, modify, and redistribute for any noncommercial purpose — community tools, hobby projects, research, personal use, and use by educational/charitable/government organizations are all explicitly permitted, regardless of whether the consuming application itself is open- or closed-source.

For commercial use (including monetized products, paid services, or any use intended for commercial advantage), a separate commercial license is required. Open an issue on the repository or reach out on the #the-hellforge channel of the Diablo II: Resurrected Discord.