This page as Markdown: https://bloxd.io/docs/how-to-interactable-npcs.md. Every page of these docs, for AI agents: https://bloxd.io/docs/llms.txt
Interactable NPCs
An interactable entity is one the player aims at and interacts with rather than attacks. Interacting fires onPlayerAltAction with the entity as the target, and the crosshair tells the player which key to press.
interactablemakes the entity pickable on its own, so leavecanAttackoff unless being hit is meant to do something. Aim still lands on the entity; swinging or shooting at it does nothing.- The alt action is consumed, so a held block is not placed through the NPC and a held throwable is not spent.
interactionPromptreplaces the label beside the key. Leave it unset for the defaultInteract. Describe what interacting does rather than which button to press, since the engine draws the key itself.- It takes plain text or
CustomTextStyling, so use atranslationKeyif your world is translated. - Works on mobs as well as mesh entities.
- Nothing is drawn on touchscreens, where a tap needs no prompt. The interaction itself still works there.
A shopkeeper
let shopkeeperId = null
tick = () => {
if (shopkeeperId !== null) {
return
}
if (api.getBlock(8, 4, 8) === "Unloaded") {
// Wait for the chunk to load before placing the NPC
return
}
shopkeeperId = api.attemptCreateMeshEntity("Person", {
size: 1,
pose: "standing",
textures: { head: "trader_black" },
}, "Blacksmith")
if (shopkeeperId === null) {
// The mesh entity limit was reached
return
}
api.setPosition(shopkeeperId, 8, 5, 8)
// interactable is what keeps the NPC pickable, so canAttack can stay off
api.setTargetedPlayerSettingForEveryone(shopkeeperId, "canAttack", false)
api.setTargetedPlayerSettingForEveryone(shopkeeperId, "interactable", true)
api.setTargetedPlayerSettingForEveryone(shopkeeperId, "interactionPrompt", "Buy Gear")
}
onPlayerAltAction = (playerId, x, y, z, block, targetEId) => {
if (targetEId === shopkeeperId) {
// Toggle off, or holding the key would flicker the shop open and closed
api.openShop(playerId, false)
}
}
Players now see the interact key and Buy Gear at the crosshair whenever they look at the Blacksmith, and pressing it opens the shop. Hitting the NPC does nothing at all.
Guarding a held key
onPlayerAltAction keeps firing while the interact key is held down, roughly every 50-150ms. Reopening an already open shop does nothing, so the example above needs no guard. An interaction that grants an item, charges a currency or advances a conversation is not idempotent, and has to swallow the repeats itself:
const INTERACT_COOLDOWN_MS = 800
const lastInteractAt = {}
onPlayerAltAction = (playerId, x, y, z, block, targetEId) => {
if (targetEId === questGiverId) {
const now = api.now()
if (now - (lastInteractAt[playerId] ?? 0) >= INTERACT_COOLDOWN_MS) {
lastInteractAt[playerId] = now
sayNextLine(playerId)
}
}
}
onPlayerLeave = (playerId) => {
delete lastInteractAt[playerId]
}
800ms is long enough to swallow a held key and short enough that two deliberate presses still count twice. A quest giver fills in sayNextLine.