Skip to content

Kyonru/feel.lua

Repository files navigation

feel.lua

feel.lua is a tiny LOVE2D-first feedback sequencing library for making actions feel good.

What It Does

  • Defines reusable named feedback sequences with feel.define.
  • Plays named or inline sequences with feel.play.
  • Animates lightweight target values.
  • Emits host-owned events for particles, camera shake, flashes, sounds, haptics, shaders, and more.
  • Runs steps in order, including waits, nested sequences, repeats, random branches, and parallel groups.
  • Optionally groups adapter events into named feedback stacks with feel.feedbacks.
feel.lua.mp4

Install

Install with Feather:

feather package install feel

Feather installs the package under lib/feel:

local feel = require("lib.feel")

Quick Start

local feel = require("lib.feel")

local button = feel.target({
  label = "PRESS ME",
  x = 320,
  y = 240,
  w = 180,
  h = 54,
  values = { scale = 1, y = 0, glow = 0 },
})

feel.define("button.press", {
  { kind = "emit", event = "sound", payload = { cue = "click" } },
  { kind = "animate", duration = 0.06, to = { scale = 0.92, y = 3 }, ease = "quadout" },
  { kind = "parallel", steps = {
    {
      { kind = "animate", duration = 0.16, to = { scale = 1, y = 0 }, ease = "backout" },
    },
    {
      { kind = "animate", duration = 0.08, to = { glow = 1 }, ease = "quadout" },
      { kind = "animate", duration = 0.22, to = { glow = 0 }, ease = "quadout" },
    },
  } },
})

local function insideButton(x, y)
  return x >= button.x - button.w / 2
    and x <= button.x + button.w / 2
    and y >= button.y - button.h / 2
    and y <= button.y + button.h / 2
end

function love.update(dt)
  feel.update(dt)
end

function love.mousepressed(x, y)
  if insideButton(x, y) then
    feel.play("button.press", button, {
      restart = true,
      key = "button.press",
      emit = function(event)
        print(event.kind, event.payload and event.payload.cue)
      end,
    })
  end
end

function love.draw()
  local v = button.values
  local x = button.x
  local y = button.y + v.y

  love.graphics.clear(0.08, 0.09, 0.11)
  love.graphics.push()
  love.graphics.translate(x, y)
  love.graphics.scale(v.scale)

  love.graphics.setColor(0.2, 0.8, 1, 0.18 * v.glow)
  love.graphics.rectangle("fill", -button.w / 2 - 14, -button.h / 2 - 14, button.w + 28, button.h + 28, 12)

  love.graphics.setColor(0.12, 0.14, 0.18)
  love.graphics.rectangle("fill", -button.w / 2, -button.h / 2, button.w, button.h, 8)

  love.graphics.setColor(0.2, 0.8, 1)
  love.graphics.rectangle("line", -button.w / 2, -button.h / 2, button.w, button.h, 8)

  love.graphics.setColor(1, 1, 1)
  love.graphics.printf(button.label, -button.w / 2, -7, button.w, "center")
  love.graphics.pop()
end

Docs

Optional Feedback Authoring

feel.feedbacks lets gameplay call one named feedback while a feedback module owns the actual camera, post, sound, time, and 3D adapter events:

local Feedbacks = require("lib.feel.feedbacks").new({ love = fx, g3d = g3dfx })

Feedbacks.define("hit.heavy", {
  { kind = "time.freeze", duration = 0.04 },
  { kind = "screen.flash", amount = 0.3, duration = 0.08 },
  { kind = "g3d.camera.shake", amount = 0.14, duration = 0.16 },
})

Feedbacks.play("hit.heavy", { x = enemy.x, y = enemy.y, z = enemy.z })

Optional g3d Helpers

feel.g3d can bind animated target values to app-owned g3d models and cameras:

local feel = require("lib.feel")
local feelG3d = require("lib.feel.g3d")
local g3d = require("g3d")

local g3dfx = feelG3d.new(g3d)
local shipModel = g3d.newModel("ship.obj", "ship.png")
local ship = g3dfx:model("ship", shipModel, {
  values = { x = 0, y = 0, z = 0, rz = 0, scale = 1 },
})

feel.define("ship.hit", {
  { kind = "animate", to = { scale = 1.2, rz = 0.15 }, duration = 0.06 },
  { kind = "animate", to = { scale = 1, rz = 0 }, duration = 0.22, ease = "backout" },
})

function love.update(dt)
  feel.update(dt)
  g3dfx:update()
end

Optional Menori Helpers

feel.menori can bind animated target values to app-owned Menori nodes, cameras, glTF animations, and uniforms:

local feel = require("lib.feel")
local feelMenori = require("lib.feel.menori")
local menori = require("menori")

local menorifx = feelMenori.new(menori, { environment = environment })
local ship = menorifx:node("ship", shipNode, {
  values = { x = 0, y = 0, z = 0, rz = 0, scale = 1 },
})

feel.define("ship.hit", {
  { kind = "emit", event = "menori.node.scalePunch", payload = { name = "ship", amount = 0.2, duration = 0.06 } },
  { kind = "emit", event = "menori.camera.shake", payload = { amount = 0.06, duration = 0.14 } },
})

feel.play("ship.hit", ship, menorifx:handlers())

function love.update(dt)
  feel.update(dt)
  menorifx:update(dt)
end

How does it work?

It wraps a vendored copy of flux by rxi so you can describe game feel as small Lua recipes: animation, timing, emitted effects, audio cues, callbacks, random choices, loops, and grouped steps.

The core stays small and table-driven. LOVE-specific work lives in optional adapters or user callbacks.

Tests

busted spec

About

feedback recipe runner for making game actions feel better.

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors