El SDK expone tres APIs para scores:
submitScore— sube un score al leaderboard global del juego.getTopScores— lee top N rankings (con cache).persistRun— guarda un snapshot de la run para una URL compartible (jugafy.com/r/<id>).
submitScore(game, name, score, durationMs?, extras?)
Sube un score asociado al game y al name (nickname del player). El backend dedupea por name lower-case — sólo el highscore por player queda visible en el leaderboard.
import { ensurePlayerName, submitScore } from "/sdk/v0.0.1-beta/jugafy.js";
async function onGameOver(finalScore, runDurationMs) {
const name = await ensurePlayerName();
try {
const result = await submitScore(
window.NEON_GAME, // "tu-slug"
name, // "ARCADE_KID"
finalScore, // 12450
runDurationMs, // 23400 (opcional, ms)
{ level: 7 } // extras opcional — level alcanzado en la run
);
console.log("submitted:", result); // { id, name, score, ... }
} catch (err) {
if (err.code === "name_required") {
// user no completó el modal — re-llamar ensurePlayerName?
}
console.error("score error:", err);
}
}
Args:
game(string, required) — slug. Usáwindow.NEON_GAME.name(string, required) — nickname. Sanitizado server-side. Si vacío post-sanitize → throwname_required.score(number, required) — entero ≥ 0. Floats se truncan.durationMs(number, optional) — duración de la run en ms. Truncado a entero. Útil para tie-breakers visuales.extras.level(number, optional) — nivel alcanzado dentro de la run. Backward compat: rows existentes tienenlevel = NULL.
Retorna: Promise<row> con { id, name, score, level, duration_ms, created_at, ... }.
Throws:
name_required— name vacío post-sanitize.http_<code>— error del backend (red, validación, rate limit). Tiene.statusy.code.
Side effect: persiste el name actual en localStorage del browser (key na_player_name) — equivalente a llamar setPlayerName(name).
Side effect: invalida el cache local de getTopScores para ese game. La próxima llamada va a fetcher fresh.
getTopScores(game, limit?, opts?)
Lee top N scores ordenados por score DESC. Dedupea por name (1 row por player, mejor score).
import { getTopScores } from "/sdk/v0.0.1-beta/jugafy.js";
const top10 = await getTopScores(window.NEON_GAME, 10);
console.log(top10);
// [
// { id, name: "ARCADE_KID", score: 12450, duration_ms: 23400, level: 7, created_at },
// ...
// ]
// Top de hoy (medianoche local del cliente, no UTC)
const todayTop = await getTopScores(window.NEON_GAME, 10, { scope: "today" });
// Forzar refresh ignorando cache
const fresh = await getTopScores(window.NEON_GAME, 10, { force: true });
Args:
game(string, required) — slug.limit(number, default 10) — top N.opts.scope("all" | "today", default"all") — filtro temporal."today"filtra porcreated_at >= startOfToday(local timezone).opts.force(boolean, default false) — bypass del cache.
Cache:
- En memoria (
Map) — TTL 60s. - En
localStorage(na_top_<game>_<limit>_<scope>) — TTL 60s, sobrevive reload.
Cuando hay cache fresh, devuelve sync del cache. Cuando hay cache stale (en localStorage), devuelve el stale + dispara fetch en background (stale-while-revalidate). Cuando no hay cache, fetch sync.
Caveat — orden por score, tie-breaker por created_at: si dos players tienen el mismo score, gana el que lo subió antes. No hay tie-breaker por duration_ms. Si querés un leaderboard ordenado por “score y luego más rápido”, tendrías que post-procesar el array client-side:
const top = await getTopScores(slug, 50);
top.sort((a, b) => b.score - a.score || (a.duration_ms ?? Infinity) - (b.duration_ms ?? Infinity));
Caveat — el dedupe es client-side, basado en lower-case del name. Si el backend recibe "PEPE", "pepe", " Pepe ", los considera el mismo player. El SDK fetchea limit * 5 rows del backend para tener buffer post-dedupe.
persistRun(game, opts) (Gate #2a)
Guarda un snapshot de la run para que el player pueda compartir la URL jugafy.com/r/<id>. La página renderea OG image dinámica para Twitter/WhatsApp/Discord previews.
import { persistRun } from "/sdk/v0.0.1-beta/jugafy.js";
async function onGameOver(score, durationMs, snapshot) {
// Llamar después de submitScore.
try {
const { id, url } = await persistRun(window.NEON_GAME, {
score,
durationMs,
playerName: getPlayerName(),
snapshot: {
// libre por juego — el endpoint OG lo lee según el `game`
max_combo: 12,
weapons: ["bow", "shield"],
boss_killed: "dragon",
},
});
console.log("share:", url); // "https://jugafy.com/r/abc123"
} catch (err) {
// Fire-and-forget conceptual: si falla, la run terminó bien igual.
// Solo se pierde la capacidad de compartir esa run en particular.
console.warn("persistRun failed:", err);
}
}
Args:
game(string, required).opts.score(number, required).opts.durationMs(number, optional).opts.playerName(string, optional) — si no, el row queda sin nombre.opts.snapshot(object, optional) — JSON arbitrario por juego. Cualquier shape, max ~16KB JSON serializado.
Retorna: Promise<{ id, url }>.
Side effect: si hay sesión Supabase activa (visitor logueado), el run queda asociado al user_id (server stampea via access_token).
Cuándo llamarlo:
- En el game over — DESPUÉS de
submitScore(paralelo es OK pero secuencial te asegura que el name ya está guardado). - NO en cada level / boss kill / etc. — sólo el snapshot final.
- NO esperes el await para mostrar “compartí tu run” — show del botón Share + dispatch sin await + cuando llegue el id, hidratá la URL.
Caveats:
- El backend tiene rate limiting por IP. Si el mismo IP hace 100 persistRun en 1 min, vas a empezar a recibir 429. No esperes que llegue siempre.
- El
snapshotes JSON inseguro per-game — el endpoint OG renderiza basado en él, así que no metas data sensible (un atacante podría cargar la URL para ver el shape). Solo cosas que el player ve en su propia UI. - URLs
jugafy.com/r/<id>son públicas (no requieren login para ver). Si querés runs privadas, no uses persistRun — sólosubmitScore.
Errores comunes
”Mi score no aparece en el leaderboard”
- Confirmá que
submitScoreresolvió OK (no rechazó). Mirá el log/network. - El leaderboard tiene cache de 60s. Recargá con
getTopScores(slug, 10, { force: true }). - El dedupe es por nickname lower-case. Si tu nick “ARCADE_KID” tiene otro score más alto que el de ahora, el row visible es el más alto, no el último.
”Quiero ver MIS scores históricos”
getTopScores muestra solo el highscore por nickname. Para ver tu histórico (todas tus runs):
const supa = window.NeonArcade?.__supabase ?? null; // no exportado oficialmente
// O usar el client directo:
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const sb = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
const { data } = await sb
.from("scores")
.select("*")
.eq("game", "tu-slug")
.eq("name", getPlayerName())
.order("created_at", { ascending: false })
.limit(20);
Esto NO está en la API pública — usalo si necesitás. Si pesa, lo agregamos al SDK.
”Quiero leaderboard por amigos / por país / por nivel”
Hoy NO hay segmentación por amigos ni geolocalización. Por nivel, podés filtrar client-side:
const top = await getTopScores(slug, 100);
const topLevel7 = top.filter((r) => r.level === 7).slice(0, 10);
Roadmap del SDK: getTopScores(slug, 10, { scope: "today" | "week" | "all", level?: number }) para que el filtro vaya server-side.