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

@alife-sdk/economy

v0.4.1

Published

Trade, inventory, and quest systems for A-Life SDK

Downloads

365

Readme

@alife-sdk/economy

Trade, inventory, and quest systems for A-Life SDK.

Engine-agnostic. Depends on @alife-sdk/core.

npm install @alife-sdk/economy

What this package does

@alife-sdk/economy provides the full player-facing economy layer:

  • Inventory — slot-based item container with stack limits and overflow handling
  • Trade — player ↔ NPC buy/sell, item gifting, pricing with faction discounts
  • Offline NPC-NPC trade — background trader-to-trader exchanges via co-location
  • Quests — lifecycle FSM with objective tracking, prerequisites, typed events, and terrain effects

The package is engine-agnostic — it never touches the renderer, UI, or event bus. All integration goes through optional ports — typed adapters that are optional registration keys decoupling the economy plugin from your game's terrain, simulation, and item systems. Register them via kernel.portRegistry.provide() before kernel.init().


Quick start

EconomyPlugin works standalone or inside ALifeKernel. The example below uses the kernel. For standalone usage skip kernel.use() and call methods directly.

import { ALifeKernel }              from '@alife-sdk/core';
import { SeededRandom }             from '@alife-sdk/core/ports';
import { EconomyPlugin }            from '@alife-sdk/economy/plugin';
import { TradeResult }              from '@alife-sdk/economy/trade';

// 1. Create and install the plugin
const random = new SeededRandom(42);
const econ   = new EconomyPlugin(random);
kernel.use(econ);
await kernel.init();

// 2. Stock a trader
econ.traders.register('sid', 'loner', 5000);
econ.traders.addStock('sid', 'medkit',   10);
econ.traders.addStock('sid', 'ammo_9mm', 200);

// 3. Register quests
econ.quests.registerQuest({
  id: 'q_clear_village',
  name: 'Clear the Village',
  objectives: [
    { id: 'obj_kill', type: 'kill', target: 'bandit', description: 'Kill 5 bandits', count: 5, current: 0, completed: false },
  ],
});

// 4. Subscribe to quest events
econ.quests.on('quest:completed', ({ questId }) => {
  econ.playerInventory.add('reward_artifact', 1);
});

// 5. Player buys a medkit from the trader
const outcome = executeBuy({
  playerInventory: econ.playerInventory,
  playerMoney:     2000,
  traders:         econ.traders,
  traderId:        'sid',
  itemId:          'medkit',
  basePrice:       500,
  factionRelation: 40,
  config:          econ.config.trade,
});
// outcome.receipt.result === TradeResult.SUCCESS

// 6. Quest progress — call per kill event
econ.quests.startQuest('q_clear_village');
econ.quests.updateObjectiveProgress('q_clear_village', 'obj_kill'); // +1 per kill

// 7. Save / load
const save = econ.serialize();
econ.restore(save);

Sub-path imports

Each module has its own import path for optimal tree-shaking:

| Import path | What's inside | Module docs | |-------------|--------------|-------------| | @alife-sdk/economy | Full re-export of all sub-modules | src/ | | @alife-sdk/economy/plugin | EconomyPlugin — kernel plugin entry point | plugin/ | | @alife-sdk/economy/inventory | Inventory — slot-based item container | inventory/ | | @alife-sdk/economy/trade | TraderInventory, executeBuy, executeSell, executeGift, OfflineTradeScheduler | trade/ | | @alife-sdk/economy/quest | QuestEngine, QuestStatus, QuestEventMap | quest/ | | @alife-sdk/economy/ports | EconomyPorts — optional integration tokens | ports/ | | @alife-sdk/economy/types | All interfaces and config types | types/ |


Architecture

                ┌────────────────────────────────────────┐
                │              ALifeKernel               │
                │   (from @alife-sdk/core)               │
                └──────────────┬─────────────────────────┘
                               │ kernel.use(econ)
                ┌──────────────▼─────────────────────────┐
                │            EconomyPlugin               │
                │                                        │
                │  playerInventory  ── Inventory         │
                │  traders  ────────── TraderInventory   │
                │  quests   ────────── QuestEngine       │
                │  config   ────────── IEconomyConfig    │
                └──┬─────────────────────────────────────┘
                   │ optional ports (register before init)
       ┌───────────┼──────────────────┐
       │           │                  │
  TerrainLock  CoLocation        ItemCatalogue
  (quest lock)  (NPC-NPC trade)  (offline prices)

Trade functions (stateless, use anywhere):
  executeBuy / executeSell     player ↔ trader
  executeGift                  any inventory ↔ any inventory (no money)
  OfflineTradeScheduler        background NPC-NPC trading

Key concepts

Inventory — slot-based container

Inventory stores items as (itemId → quantity) slots with configurable capacity and per-item stack limits. add() returns overflow — items that didn't fit. remove() is all-or-nothing.

const inv = new Inventory({ maxSlots: 30, defaultMaxStack: 99 });
const overflow = inv.add('medkit', 5, 10); // → 0 (all fit, maxStack 10)
inv.remove('medkit', 2);                   // → true
inv.has('medkit', 3);                      // → true

See inventory/README.md.

Trade — buy, sell, gift

Three pure functions handle all item movement:

  • executeBuy — player buys 1 item from a trader; validates relation, stock, money, inventory space; returns ITradeOutcome with a result code
  • executeSell — player sells 1 item to a trader; validates relation, player stock, trader funds
  • executeGift — moves items between any two Inventory instances with no money involved; supports partial overflow; use for quest rewards, NPC handoffs, loot containers

All three are stateless — no state is mutated on failure.

See trade/README.md.

QuestEngine — lifecycle FSM with events

QuestEngine tracks quest state through AVAILABLE → ACTIVE → COMPLETED / FAILED. Key features:

  • Prerequisitesrequires: ['q_prev'] gates startQuest() until prior quests complete
  • Typed events — subscribe with engine.on('quest:completed', cb) instead of polling
  • Terrain effects — declarative lock/unlock actions on on_start, on_complete, on_fail
  • Open objective types — any string valid; engine drives all via completeObjective() / updateObjectiveProgress()

startQuest() returns false if prerequisites are unmet or the quest is already active (not in AVAILABLE status).

engine.on('quest:completed', ({ questId }) => giveReward(questId));
engine.on('objective:progress', ({ current, total }) => ui.setProgress(current / total));

See quest/README.md.

Optional ports

Register adapters before kernel.init() to unlock optional features:

| Port | Activates | |------|-----------| | EconomyPorts.TerrainLock | Quest-driven terrain lock/unlock | | EconomyPorts.CoLocationSource | Offline NPC-NPC trading | | EconomyPorts.ItemCatalogue | Offline trade price lookup |

All ports are optional — EconomyPlugin works without them. See ports/README.md.


Lifecycle

kernel.use(econ)       ← install plugin
  ↓
kernel.init()          ← validate config, wire optional ports
  ↓
boot time:
  traders.register()   ← register NPC traders
  traders.addStock()   ← set initial stock (also sets restock baseline)
  quests.registerQuest() ← define all quests
  ↓
game loop:
  traders.restock(now) ← call periodically (every in-game hour or timer)
  quests events fire automatically on state transitions
  ↓
save / load:
  econ.serialize()     ← inventory + traders + quests
  econ.restore(state)  ← re-register quests first, then restore
  ↓
kernel.destroy()       ← clears inventory + traders, releases kernel ref

Important: Call quests.registerQuest() for ALL quest definitions BEFORE calling econ.restore(). The plugin merges saved progress into registered definitions — quests not registered before restore are silently skipped.


Testing

The package has 228 tests (vitest). Run them:

pnpm test --filter @alife-sdk/economy

All subsystems are pure — no kernel needed for unit tests:

import { Inventory }                from '@alife-sdk/economy/inventory';
import { executeBuy, TradeResult }  from '@alife-sdk/economy/trade';
import { QuestEngine }              from '@alife-sdk/economy/quest';
import { createDefaultEconomyConfig } from '@alife-sdk/economy/types';
import { SeededRandom }             from '@alife-sdk/core/ports';

const config  = createDefaultEconomyConfig();
const traders = new TraderInventory(config.trade, new SeededRandom(0));
traders.register('t1', 'loner', 9999);
traders.addStock('t1', 'medkit', 5);

const outcome = executeBuy({
  playerInventory: new Inventory(config.inventory),
  playerMoney: 0,
  traders, traderId: 't1', itemId: 'medkit',
  basePrice: 100, factionRelation: 0, config: config.trade,
});
expect(outcome.receipt.result).toBe(TradeResult.INSUFFICIENT_MONEY);

Module map

src/
├── plugin/     EconomyPlugin (IALifePlugin — owns inventory + traders + quests)
├── inventory/  Inventory (slot container, stack limits, serialize/restore)
├── trade/      TraderInventory, executeBuy, executeSell, executeGift,
│               PricingEngine, OfflineTradeScheduler
├── quest/      QuestEngine (FSM, events, prerequisites, terrain effects)
├── ports/      EconomyPorts tokens (TerrainLock / CoLocationSource / ItemCatalogue)
└── types/      IEconomyConfig, ITradeConfig, IInventoryConfig,
                IQuestDefinition, IQuestObjective, QuestStatus, ObjectiveType, …