Search documentation

Find pages, sections, and content across all docs.

WavedashDocs

Love2D

Run Love2D games on the web with 2dengine's love.js player and host them on Wavedash.

View example project on GitHub Playtest the example project

Love2D reaches the browser through 2dengine's love.js player — a standalone player that runs .love files in the browser via an Emscripten build of LÖVE 11.5. No compile step: just package your game as a .love and drop the player's player.js, love.js, and love.wasm alongside.

Build for web

  1. Download the 2dengine/love.js repo and extract it — call the extracted dir LOVEJS_DIST. It must contain player.js, style.css, 11.5/love.js, 11.5/love.wasm, and lua/normalize1.lua + lua/normalize2.lua.
  2. Package your game as a .love (zip conf.lua, main.lua, and any other modules).
  3. Copy player.js, style.css, the 11.5/ folder, and the lua/ folder alongside your .love and an index.html that loads player.js?g=your-game.love&v=11.5.

The example's build.sh does all this for you — see example-love2d/build.sh.

SDK integration

2dengine's love.js ships a JS-interop trick rather than a true FFI: Lua can synchronously evaluate JavaScript by calling os.execute("javascript:<code>"). Internally that hops love.system.openURLwindow.open (intercepted by player.js) → eval. The result is written to window._output, which io.read() can read back via player.js's window.prompt override. normalize1.lua glues all of that together, so os.execute returns the evaluated string and no JS shim is needed.

-- wavedash.lua
local M = {}

function M.init()
  os.execute([[javascript:
    window.WavedashJS && window.WavedashJS.init({ debug: true })
  ]])
end

function M.update_load_progress(fraction)
  local clamped = math.max(0, math.min(1, fraction or 0))
  os.execute(string.format([[javascript:
    window.WavedashJS && window.WavedashJS.updateLoadProgressZeroToOne(%f)
  ]], clamped))
end

return M

Call it from the game

At the end of love.load:

local wavedash = require("wavedash")

function love.load()
  -- ...game setup...
  wavedash.update_load_progress(1)
  wavedash.init()
end

Adding more SDK methods

Embed the arguments directly into the JS literal — %q quotes strings safely for Lua-into-JS string contexts:

function M.submit_score(name, score)
  os.execute(string.format([[javascript:
    window.WavedashJS && window.WavedashJS.getLeaderboard(%q).then((lb) => {
      if (lb.success) window.WavedashJS.uploadLeaderboardScore(lb.data.id, %d, true);
    })
  ]], name, score))
end

Then call from Lua: wavedash.submit_score("level-1", 3500).

For methods that need to return data back to Lua, append a return to the JS string and read it with io.read():

function M.get_username()
  os.execute([[javascript:'return ' + (window.WavedashJS ? window.WavedashJS.getUsername() : '')]])
  return io.read() or ""
end

Async (Promise-returning) SDK methods are the ceiling of this approach — you'd need a JS-side dispatcher that resolves into window._output before Lua reads it.

wavedash.toml

game_id = "YOUR_GAME_ID_HERE"
upload_dir = "./build"

love.js 11.5 bundles the entire LÖVE runtime (~10-15MB). Compress images and prefer OGG for audio to keep the initial payload small.