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:
| Type | Fields | Description |
|---|---|---|
hasFlag | flag, value | World flag is set |
questStatus | questId, status | Quest is 'available', 'active', 'complete', 'turned_in' |
hasItem | itemId, count | Player has at least count of item |
hasGold | amount | Player has at least amount gold |
playerLevel | minLevel | Player level >= minLevel |
Available effects:
| Type | Fields | Description |
|---|---|---|
setFlag | flag, value | Set a world flag |
startQuest | questId | Register and activate a quest |
turnInQuest | questId | Complete a quest, give rewards |
equipItem | itemId, slot | Give + equip an item directly (Armorer pattern) |
spendCurrency | currency, amount | Deduct gold (use with equipItem for a purchase) |
multi | effects: [] | 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 shownloot_window— opens a popup listing items; player clicks to collectchoice— 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:
- Idle — wanders or stays still
- Chase — player enters detection radius → moves toward player
- 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).