KubeJS
KubeJS scripts can drive EmbersTextAPI directly. Send and update immersive HUD messages, run sequenced queues, and apply effects to item names and tooltips, all from JavaScript, with no Java mod to compile.
The integration exposes a single global, EmbersText, in every script type. It wraps the same send pipeline and effect registry the Java API and /eta commands use, so anything you can do from a command you can do from a script.
EmbersTextAPI treats KubeJS as an optional dependency. Nothing extra to configure.
- Install KubeJS (NeoForge 1.21.1) and its dependencies alongside the matching EmbersTextAPI build.
- Launch once so KubeJS generates its
kubejs/script folders. - Put your scripts in
kubejs/startup_scripts/,kubejs/server_scripts/, orkubejs/client_scripts/depending on what they do (see below).
If KubeJS is absent, ETA loads and runs normally; the binding simply isn’t registered.
Which script type to use
Section titled “Which script type to use”| Script folder | Runs on | Use EmbersText for |
|---|---|---|
startup_scripts/ | both, at load | itemName rules |
server_scripts/ | server | send, hud, queue, and everything that targets players |
client_scripts/ | client | tooltip styling via ItemEvents.dynamicTooltips |
Messages, HUD elements, and queues are sent from the server (they target ServerPlayer). Item-name rules register at startup. Tooltip styling is client-side because tooltips render on the client.
Sending a message
Section titled “Sending a message”EmbersText.markup(duration, text) builds a message from ETA markup; EmbersText.message(duration, text) builds one from plain text. Both take a duration in ticks (20 ticks = 1 second) and return a builder you can position and style with fluent calls. Pass the result to send:
ServerEvents.commandRegistry(event => { const cmd = event.commands event.register(cmd.literal('eta_hello').executes(ctx => { EmbersText.send(ctx.source.player, EmbersText.markup(60, '<wave>Hello from KubeJS</wave>')) return 1 }))})Send to everyone online with sendToAll(server, message):
EmbersText.sendToAll(event.server, EmbersText.markup(80, '<rainbow>Server restarting soon</rainbow>'))Positioning and styling
Section titled “Positioning and styling”The builder returned by message/markup carries the same layout options as the Java ImmersiveMessage. Anchors are passed as lowercase strings:
EmbersText.markup(0, '<bold>Wave incoming</bold>') .anchor('top_center') .scale(1.5) .fadeInTicks(10) .fadeOutTicks(10) .offset(0, 20)Common builder methods: anchor(string), align(string), offset(x, y), scale(size), color(hexOrName), shadow(bool), background(bool), bgColor(color), bgAlpha(0..1), fadeInTicks(int), fadeOutTicks(int), effect(tag), wrap(maxWidthPx). See Layout & Positioning for what each one does.
Live-updating HUD elements
Section titled “Live-updating HUD elements”hud(player, id, message) opens a persistent HUD element identified by id (any string). Call it again with the same id to swap the contents in place, with no flicker and no fade restart. Unlike send, a hud element ignores the client’s maximum-duration cap, so a duration of 0 stays on screen until you close it. This is what you want for timers, counters, and stat readouts.
// Rhino re-runs tick callbacks repeatedly. Declare per-call locals with `var`,// not `const`/`let`, or you'll hit "redeclaration of var".const CLOCK_ID = 'eta_clock'var clockOn = falsevar clockSeconds = 0
ServerEvents.commandRegistry(event => { const cmd = event.commands event.register(cmd.literal('eta_clock').executes(ctx => { clockOn = !clockOn clockSeconds = 0 if (!clockOn) { ctx.source.server.playerList.players.forEach(p => EmbersText.closeHud(p, CLOCK_ID)) } return 1 }))})
ServerEvents.tick(event => { if (!clockOn) return if (event.server.tickCount % 20 !== 0) return clockSeconds++ EmbersText.hudToAll(event.server, CLOCK_ID, EmbersText.markup(0, '<bold>Uptime: ' + clockSeconds + 's</bold>') .anchor('top_center') .scale(1.5))})hudToAll(server, id, message) does the same for every online player. closeHud(player, id) removes one element; closeAllMessages(player) clears everything on a player at once.
Queues
Section titled “Queues”A queue plays a sequence of steps on a named channel. Each step is a list of messages that display together, and steps advance in order. Build it as a list of lists:
const steps = [ [EmbersText.markup(40, '<type>Step one...</type>')], [EmbersText.markup(40, '<type>Step two...</type>')]]EmbersText.queue(ctx.source.player, 'demo', steps)Control queues by channel name: stopQueue(player, channel) halts the active one and drops what’s pending; clearQueue(player, channel) drops the pending steps but lets the current message finish; clearAllQueues(player) and stopAllQueues(player) apply to every channel. See Queues for the timing model.
Styling item names
Section titled “Styling item names”itemName(matcher, markup) decorates an item’s default name wherever it appears (inventory, tooltips, hover), with no language-file edits. Register the rules at startup. The matcher selects items; the markup is normal ETA markup, so chain tags to stack effects:
EmbersText.itemName('minecraft:diamond_sword', '<rainbow>')EmbersText.itemName('#minecraft:planks', '<wave>')EmbersText.itemName('/_ingot$/', '<glow color=gold>')EmbersText.itemName('minecraft:netherite_sword', '<glow color=gold><wave>')Matchers are tested against the item id:
| Matcher form | Matches | Example |
|---|---|---|
| Exact id | one item | minecraft:diamond_sword |
#tag | every item in the tag | #minecraft:planks |
namespace:* | every item from a mod | create:* |
/regex/ | ids matching the pattern | /_ingot$/ |
A player rename (anvil) overrides the rule for that stack.
Styling tooltips
Section titled “Styling tooltips”For tooltip lines that depend on the stack (durability, a name match, NBT), use KubeJS’s own ItemEvents.dynamicTooltips and turn effect specs into styled components with EmbersText.style or EmbersText.styled. These run in client_scripts.
style(component, ...effectSpecs) returns a copy of a Component with effects attached; styled(text, ...effectSpecs) does the same starting from a literal string. Effect specs are tag bodies without angle brackets, one per argument: 'rainbow', 'glow color=gold', 'shake speed=2'.
// Rainbow the name line of any sword.ItemEvents.dynamicTooltips('emberstextapi:swords', event => { if (!Ingredient.of('#minecraft:swords').test(event.item)) return event.lines.set(0, EmbersText.style(event.lines.get(0), 'rainbow'))})
// Shake and pulse red when a tool is near breaking.ItemEvents.dynamicTooltips('emberstextapi:lowdurability', event => { const stack = event.item if (!stack.isDamageableItem()) return const remaining = (stack.getMaxDamage() - stack.getDamageValue()) / stack.getMaxDamage() if (remaining > 0.15) return event.lines.set(0, EmbersText.style(event.lines.get(0), 'shake speed=2', 'pulse color=red'))})EmbersText reference
Section titled “EmbersText reference”| Method | Returns | Description |
|---|---|---|
message(duration, text) | builder | Build a message from plain text. |
markup(duration, markup) | builder | Build a message from ETA markup. |
send(player, message) | void | Send to one player. |
sendToAll(server, message) | void | Send to every online player. |
closeAllMessages(player) | void | Close all of a player’s messages and HUD elements. |
| Method | Returns | Description |
|---|---|---|
hud(player, id, message) | void | Open or update a persistent HUD element by id. |
hudToAll(server, id, message) | void | Same, for every online player. |
closeHud(player, id) | void | Close one HUD element by id. |
| Method | Returns | Description |
|---|---|---|
queue(player, channel, steps) | void | Play a sequence of steps on a channel. steps is a list of lists of messages. |
clearQueue(player, channel) | void | Drop pending steps; let the current message finish. |
stopQueue(player, channel) | void | Stop the active queue and drop everything pending. |
clearAllQueues(player) | void | clearQueue for every channel. |
stopAllQueues(player) | void | stopQueue for every channel. |
| Method | Returns | Description |
|---|---|---|
style(component, ...specs) | Component | Copy a component with effects attached. |
styled(text, ...specs) | Component | Build a literal component and attach effects. |
itemName(matcher, markup) | void | Register a default-name effect rule (startup). |
Not yet supported
Section titled “Not yet supported”A few things are intentionally out of scope for the scripting binding:
- Chat-word styling. Effects don’t survive the vanilla
Stylenetwork codec yet, so styling individual chat words from a script is deferred to a later phase. - Script-defined effects. You can use any built-in effect from JS, but defining brand-new per-glyph effects in a script is not supported (it’s a render hot path). Write a custom effect in Java instead.
- GUI hover-name styling outside tooltips. There’s no KubeJS event for it. Use
itemNamerules, which cover the name everywhere it renders.