Skip to content

Core Runner

The core runner stores named feedback recipes, plays named or inline sequences, advances active work through feel.update(dt), and leaves rendering or side effects to your app.

Targets

Use feel.target(meta) when a sequence needs animated values.

local button = feel.target({
  label = "Launch",
  values = { x = 0, y = 0, scale = 1, opacity = 1, glow = 0 },
})

The default transform fields are opacity, x, y, scale, scaleX, scaleY, and rotation. You can also add any numeric field your game wants to animate, such as glow, shake, teleportGlow, or charge.

Metadata that is not inside values stays available on the target table, but it is not tweened:

local ship = feel.target({
  kind = "player",
  values = { scale = 1, teleportGlow = 0 },
})

Named Sequences

Use feel.define(name, sequence) for reusable recipes.

feel.define("button.press", {
  { kind = "audio", cue = "press" },
  { kind = "animate", duration = 0.06, to = { scale = 0.92 }, ease = "quadout" },
  { kind = "animate", duration = 0.16, to = { scale = 1 }, ease = "backout" },
})

feel.get(name) returns the normalized sequence.

Use feel.validate(sequence) while authoring or loading recipes to catch common shape problems before playback:

local ok, err = feel.validate("button.press")
if not ok then
  print(err)
end

Playback

feel.play(nameOrSequence, target, opts) accepts a named sequence or inline sequence.

feel.play("button.press", button, {
  trigger = "click",
  restart = true,
  key = "button.press",
  emit = function(event, ctx) end,
  audio = function(event, ctx) end,
  markDirty = function(ctx) end,
})

target is optional for event-only recipes. If an animation step runs without a target, the runner creates an internal target.

Use restart = true when a new play should cancel the previous active play in the same target/key slot. Named sequences can omit key; inline sequences should pass a stable string key.

Without restart, repeated plays stack:

feel.play("button.press", button)
feel.play("button.press", button)

With restart, the second play replaces the first active run for that target/key:

feel.play("button.press", button, { restart = true })
feel.play("button.press", button, { restart = true })

Feedback Channels

Use feel.channel() when gameplay modules should announce feedback intents without importing the sequence module that handles them.

local feedback = feel.channel()

feedback:on("ship.explode", function(event)
  feel.play("ship.explode", event.target, { restart = true, key = "ship.explode" })
end)

feedback:emit("ship.explode", { target = ship.target })

Channels are local objects. Create them where they make module boundaries cleaner, and keep gameplay state changes direct.

Update And Clear

Call feel.update(dt) once per frame. It advances tweens, waits, nested sequences, repeat loops, and parallel branches.

Use feel.active() and feel.isPlaying(target, key) as tiny debug helpers when restart slots, long waits, or nested sequences are hard to reason about:

for _, run in ipairs(feel.active()) do
  print(run.source, run.key, run.index, run.count, run.remaining)
end

if feel.isPlaying(ship.target, "ship.teleport") then
  print("teleport feedback is still active")
end

Use feel.clear() to clear all named sequences and active work. Use feel.clear(target) to stop active tweens and active sequences for one target.

  • Sequence Steps describes every step shape.
  • API lists function signatures and option fields.
  • LOVE Adapter shows how emitted events become LOVE side effects.