gameplay/behaviors.js

Gameplay — Behaviors

Complete reference for all interaction and world behavior classes — config fields, how they work, and which popup types they use.

Gameplay — Behaviors

All behavior classes live in behaviors.js. Attach them to entity defs via matching property names (e.g., an entity with a seller property gets a SellerBehavior). The AreaLoader reads the entity def and instantiates the matching behavior class automatically.

Interaction Behaviors

These are triggered by the player (press E, click). They show a prompt above the entity when in range, and open a popup if needed.


MissionGiverBehavior

NPC that offers and tracks quests.

missionGiver: {
  npcName: 'Elder Bjorn',
  quests: ['goblin_bounty', 'lost_sword'],   // quest IDs registered in QuestManager
}

Popup tab: Missions. Shows available, active, and completable quests. A completable quest (all objectives met) shows a “Turn In” button.


SellerBehavior + BuyerBehavior

A shop NPC. Usually both behaviors on the same entity — one for buying from them, one for selling to them.

seller: {
  items: [
    { itemId: 'fishing_rod', price: 30, currency: 'gold' },
    { itemId: 'bait',        price: 5,  currency: 'gold' },
  ]
},
buyer: {
  items: [
    { itemId: 'fish',        price: 12, currency: 'gold', priceMode: 'dynamic' },
    { itemId: 'wheat',       price: 8,  currency: 'gold' },
  ]
}

priceMode: 'dynamic' defers to MarketManager.getPriceFor(itemId). priceMode: 'static' (default) uses the listed price.

Popup tabs: Buy (from SellerBehavior), Sell (from BuyerBehavior). Both appear when both behaviors are present.


CrafterBehavior

Converts input items into output items via a recipe list.

crafter: {
  npcName: 'Workbench',
  recipes: [
    {
      id:      'craft_sword',
      name:    'Iron Sword',
      inputs:  [{ itemId: 'iron_bar', count: 2 }],
      output:  { itemId: 'iron_sword', count: 1 },
    },
    {
      id:      'craft_shield',
      name:    'Iron Shield',
      inputs:  [{ itemId: 'iron_bar', count: 3 }],
      output:  { itemId: 'iron_shield', count: 1 },
    },
  ]
}

Popup tab: Craft. Shows all recipes; grays out those the player can’t afford. Crafting consumes inputs and adds the output to inventory immediately.


RefinerBehavior

Converts a primary input + a fuel into an output (e.g., ore + coal → metal bar).

refiner: {
  npcName: 'Smelter',
  recipes: [
    {
      id:     'smelt_iron',
      name:   'Smelt Iron',
      input:  { itemId: 'iron_ore', count: 1 },
      fuel:   { itemId: 'coal',     count: 1 },
      output: { itemId: 'iron_bar', count: 1 },
    },
  ]
}

Popup tab: Refine. Separate from Craft — shows the input + fuel cost clearly.


DialogueBehavior

Branching dialogue tree with conditions and side effects.

dialogue: {
  npcName: 'Elder Bjorn',
  tree: [
    {
      id:   'start',
      text: 'Goblins have been raiding the village. Will you help?',
      choices: [
        {
          label:  'Yes, I will deal with them.',
          effect: { type: 'startQuest', questId: 'goblin_bounty' },
          next:   'accepted',
        },
        {
          label:  'Not now.',
          next:   null,   // closes dialogue
        },
      ],
    },
    {
      id:   'accepted',
      text: 'Thank you. Kill 3 goblins to the south.',
      choices: [],   // no choices = just an OK button, closes after
    },
  ]
}

Conditions on choices (hide a choice until a condition is true):

{
  label:     'I have returned with proof.',
  condition: { type: 'questStatus', questId: 'goblin_bounty', status: 'complete' },
  effect:    { type: 'turnInQuest', questId: 'goblin_bounty' },
  next:      'reward_given',
}

Available conditions:

TypeFieldsDescription
hasFlagflag, valueWorld flag is set
questStatusquestId, statusQuest is 'available', 'active', 'complete', 'turned_in'
hasItemitemId, countPlayer has at least count of item
hasGoldamountPlayer has at least amount gold
playerLevelminLevelPlayer level >= minLevel

Available effects:

TypeFieldsDescription
setFlagflag, valueSet a world flag
startQuestquestIdRegister and activate a quest
turnInQuestquestIdComplete a quest, give rewards
equipItemitemId, slotGive + equip an item directly (Armorer pattern)
spendCurrencycurrency, amountDeduct gold (use with equipItem for a purchase)
multieffects: []Apply multiple effects in sequence

Popup tab: Talk.


LootBehavior

One-time container — chest, bag, dropped item.

loot: {
  mode:    'loot_window',   // 'auto' | 'loot_window' | 'choice'
  oneTime: true,
  dropData: {
    type:  'set',           // 'set' | 'randomCount' | 'weightedLootTable'
    items: [
      { itemId: 'gold_coin',  count: 10 },
      { itemId: 'iron_sword', count: 1  },
    ]
  }
}
  • auto — items added to inventory immediately, float notification shown
  • loot_window — opens a popup listing items; player clicks to collect
  • choice — player picks one item from the list

oneTime: true marks the entity as looted after the first interaction; it won’t open again.


FarmPlotBehavior

Plant → grow → harvest cycle.

farmPlot: {
  seedItemId:    'wheat_seeds',
  harvestItemId: 'wheat',
  harvestCount:  3,
  growthTimeMs:  30000,   // 30 seconds
}

States: empty (shows “Plant” prompt) → growing (shows progress bar) → ready (shows “Harvest” prompt). Growth uses Date.now() — survives area unload/reload.


FishingSpotBehavior

Cast → wait → catch cycle.

fishing: {
  fishItems: [
    { itemId: 'fish',    weight: 70 },
    { itemId: 'rare_fish', weight: 30 },
  ],
  catchTimeMs: { min: 2000, max: 6000 },
  baitItemId:  'bait',           // optional — consumed on catch
}

Popup: Shows a casting animation, then a “Reel In!” button when the catch is ready.


SimpleGiverBehavior + ExamineBehavior

  • SimpleGiver — gives one item immediately, no popup. Use for pickup collectibles.
  • Examine / notice_board — opens a notice board popup with static text. No interaction required beyond reading.
simpleGiver: { itemId: 'gold_coin', count: 5 }
examine:     { text: 'A weathered signpost. "Danger ahead."' }

World Behaviors (Autonomous)

These run per-frame without player interaction.


MovementBehavior

movement: {
  mode:   'wander',   // 'wander' | 'patrol'
  speed:  1.5,        // tiles per second
  radius: 4,          // wander radius from spawn point (wander mode)
  path: [             // waypoints in order (patrol mode)
    { x: 5, y: 3 },
    { x: 8, y: 3 },
    { x: 8, y: 7 },
  ],
  pauseMs: 1000,      // pause duration at each waypoint
}

HealthBehavior

HP pool, faction system, death, loot drops, respawn.

health: {
  hp:        40,
  maxHp:     40,
  faction:   'goblin',      // entities attack other factions, not their own
  xpReward:  25,            // XP given to player on kill
  loot: [
    { itemId: 'gold_coin', count: 5 },
  ],
  respawn:   true,
  respawnMs: 15000,         // respawn delay after death
}

Works with EnemyAIBehavior — both must be present for enemy combat.


EnemyAIBehavior

Three-state AI: idle → chase → attack.

// (usually defined alongside health — same entity)
// No separate config needed — uses health.faction and entity.movement for positioning.
// Attack damage comes from the entity's base stats.

State machine:

  1. Idle — wanders or stays still
  2. Chase — player enters detection radius → moves toward player
  3. Attack — player in melee range → calls gameMgr.takeDamage() on cooldown

TriggerZoneBehavior

Fires events when the player enters, exits, or stays inside an area.

triggerZone: {
  width:  4, height: 4,     // trigger box size in tiles
  onEnter:    { eventName: 'entered_village' },
  onExit:     { eventName: 'left_village' },
  whileInside: {
    eventName:  'in_danger_zone',
    intervalMs: 1000,       // fires every 1s while inside
  },
  onTransport: {            // transport the player on enter
    destination: 'village_interior',
    spawnX: 5, spawnY: 10,
  },
}

DamageZoneBehavior

Hazard area that deals damage over time (lava, poison floor, void).

damageZone: {
  width:  4, height: 4,
  damagePerTick: 8,
  tickIntervalMs: 1500,
  label: '🔥 Lava!',        // shown in float notification
}

Shield absorbs damage zone ticks before HP is hit. Use debug: true to show a visible colored box during development (remove before release).