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

love.js has no Lua↔JS FFI: love.system.openURL isn't wired in the 11.5 build, and love.js.eval referenced in the 2dengine docs isn't in the distributed wasm. The only channel that works reliably is stdout — LÖVE's print() is piped through Emscripten's Module.printconsole.log. The example uses that channel.

Lua side — emit prefixed commands

-- wavedash.lua
local M = {}
local PREFIX = "[WAVEDASH_BRIDGE]"

function M.init()
  print(PREFIX .. "init")
end

function M.update_load_progress(fraction)
  local clamped = math.max(0, math.min(1, fraction or 0))
  print(string.format("%sprogress:%.6f", PREFIX, clamped))
end

return M

JS side — sniff console.log, dispatch to the SDK

Ship this as wavedash-bridge.js loaded from index.html before player.js. The script wraps console.log up front; when love.js later does Module.print = console.log.bind(console), it captures the already-wrapped function, so every LÖVE print() flows through the sniffer.

(function () {
  const PREFIX = "[WAVEDASH_BRIDGE]";
  const Wavedash = window.Wavedash;

  const handlers = {
    init() {
      Wavedash.init({ debug: true });
    },
    progress(raw) {
      const value = Math.max(0, Math.min(1, Number(raw) || 0));
      Wavedash.updateLoadProgressZeroToOne(value);
    },
  };

  const realLog = console.log.bind(console);
  console.log = function (...args) {
    if (args.length === 1 && typeof args[0] === "string" && args[0].startsWith(PREFIX)) {
      const [name, ...rest] = args[0].slice(PREFIX.length).split(":");
      handlers[name]?.(rest.join(":"));
      return;
    }
    realLog(...args);
  };
})();

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

Add another handler in the JS bridge for each SDK method you need, and a matching Lua wrapper that emits the prefixed line. For example, score submission — uploadLeaderboardScore takes a leaderboard ID, so the handler resolves the name to an ID via Wavedash.getLeaderboard("name") first:

submitScore(rest) {
  const [name, score] = rest.split(":");
  Wavedash.getLeaderboard(name).then((lb) => {
    if (lb.success) Wavedash.uploadLeaderboardScore(lb.data.id, Number(score), true);
  });
},
function M.submit_score(name, score)
  print(string.format("%ssubmitScore:%s:%d", PREFIX, name, score))
end

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

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.