BixBall wiki
Caratteristiche, regole di gioco, file replay e API headless per stanze scriptate con bot — nello stesso spirito di haxball.com e node-haxball, con motore proprio.
Panoramica
BixBall è un calcio 2D in browser: nickname senza registrazione, stanze pubbliche/private, mappe .hbs, fisica a 60 Hz, registrazione .bit e replay.
- Client web su
http://localhost:3847 - Lobby WebSocket solo per lista stanze e signaling WebRTC (come haxball.com)
- La fisica gira sull’host: tab browser o processo headless. Se l’host chiude, la stanza muore
- Motore in
engine/bit/(leggibile: collisioni, calci, parse HBS, tick) - Nessun limite Haxball su chat, dimensione stadium, numero giocatori
- Modulo headless: lo script tiene la simulazione in locale e si registra in lista
Avvio
cd BixBall npm install npm start # http://localhost:3847
Test rapidi:
npm test node src/headlesstest.js node examples/headless-bot.js # esempio base (avatar K / !avatars) node examples/headless-promo-bot.js # tabellone custom + welcome + ads Discord # oppure: npm run headless:promo
Lo script del bot non apre un secondo sito: usa la stessa porta del server già avviato con npm start.
Client e stanze
Flusso
- Nickname (e avatar opzionale) — nessuna account
- Lista stanze pubbliche, filtro, crea stanza (nome, password, max, mappa, pubblica)
- In campo: squadre, start/stop/pause, chat, HBS custom, REC
Controlli
| Tasto | Azione |
|---|---|
| Frecce / WASD | Movimento |
| X, Spazio, Ctrl | Calcio |
| K | UI stanza (se lo script la registra, es. avatar) |
| Invio | Chat |
| Esc | Menu stanza (squadre, host, extrapolation) |
HUD
Scoreboard in alto. Chat overlay in basso al centro. Card NET (ping, FPS, loss) a sinistra. Menu stanza a comparsa con Esc.
Comando client /extrapolation <ms> (o /ext): come su haxball.com. Il client clona lo snapshot (posizioni, velocità e kickUsed) e fa girare la fisica a 60 Hz per età dello snapshot + valore: muri, damping, collisioni e un solo calcio per pressione. Il fake shot funziona come in Haxball: tieni kick in avvicinamento e l’extra mostra il tiro previsto; rilascia prima del contatto e la palla non parte. Non modifica il server. Senza argomenti mostra il valore attuale. /extrapolation off = 0 (resta solo il catch-up fino a “adesso”).
Motore fisico
Non usa più il blob engine/api.js di Haxball. La simulazione è in:
| File | Ruolo |
|---|---|
engine/bit/hbs.js | Parse JSON5 HBS, traits, colori, flag, ballPhysics |
engine/bit/physics.js | Integrazione, collisioni disco/segmento/piano/arco, calci, joint |
engine/bit/room.js | Stato partita, kickoff, gol, pause, sandbox callbacks |
engine/bit/clock.js | Clock bloccato a 60 Hz |
Player: acc 0.1, damp 0.96, kickStrength 5 (override da HBS). Velocità: speed += dir * acc, poi pos += speed, poi speed *= damping.
Mappe HBS
Formato stadium Haxball (JSON5) e .bitmap BixBall. Caricamento: select default o file custom in stanza (solo a gioco fermo, admin). Accetta .hbs e .bitmap.
Default in maps/: Classic, Easy, Small, Big, Rounded, Hockey, Big Hockey, Big Easy, Big Rounded, Huge.
ballPhysicsdefinisce la palla (raggio, colore, cGroup).false= nessuna palla autoplayerPhysicsoverride acc/raggio/kickkickOffReset: "full"resetta anche i giocatori dopo un gol- Trait
KO/redKO/blueKOper le barriere di centrocampo segments[].width= spessore del muro (capsula di collisione, non solo linea)bg.imageediscs[].image= percorsoimages/file.extinassets/images
discs a (0,0), BixBall non ne aggiunge una seconda.Editor mappe · sandbox live
Pagina /map-editor: editor in tempo reale. Mentre costruisci puoi già muoverti (WASD) e calciare (X). Non serve copiare il file in una stanza per provarlo.
- Strumenti: vertici, segmenti, muri spessi, dischi, box, cerchi, poligoni, piani, porte, immagini
- Esporta
.bitmap(formato BixBall). In partita: Esc → Host → carica.bitmapo.hbs - Immagini: importale dal PC nell’editor (anteprima subito). La mappa salva solo
images/nome.ext. Per vederle in partita il creatore deve avere lo stesso file inassets/images(l’editor lo copia sul server locale se può)
Kickoff e regole
- All’inizio e dopo un gol: stato
BeforeKickOff. I segmenti/piani KO collidono con red/blue. - Il primo tocco di qualsiasi giocatore sulla palla avvia il kickoff (non serve calciare).
- In quel momento le
cMaskdelle barriere KO della mappa vanno a 0 e i player perdono redKO/blueKO. - Gol: la palla attraversa il segmento goal; 150 tick poi reset.
- Limiti default: 3 gol, 3 minuti (0 = illimitato).
- Pause: 120 tick di freeze, poi countdown in ripresa.
Registrazione .bit
Pulsante REC in alto a destra sul campo (come Haxball). Secondo click oppure Stop partita: download automatico .bit.
Il file contiene tutti i tick a 60 Hz (posizioni dischi, input, score) e gli eventi (chat, gol, kickoff, start/stop, stadium).
Replay analyzer
Pagina /replay-analyzer: carica un .bit, play/pause, velocità, scrub, chat e gol sincronizzati al frame.
Headless — iniziare npm
Per hostare una stanza da una VPS senza clonare il sito: pacchetto bixball-sdk + token da /headlesstoken.
1. Progetto nuovo
mkdir my-bixball-bot cd my-bixball-bot npm init -y npm install bixball-sdk
Pacchetto pubblico su npm: bixball-sdk (non bixball). Se la VPS usa un mirror e dà 404, prova npm install bixball-sdk --registry https://registry.npmjs.org/.
2. Token (1 ora)
Apri /headlesstoken → Nuovo token → copia thr_….
3. bot.js
const { HBInit, Team } = require("bixball-sdk");
const room = HBInit({
roomName: "My BixBall Room",
maxPlayers: 16,
public: true,
stadium: "Classic",
token: process.env.BIXBALL_HEADLESS_TOKEN || "thr_INCOLLA_QUI"
});
room.onRoomLink = (link) => {
if (link) console.log("Stanza in lista:", link);
};
room.onPlayerJoin = (player) => {
if (player.conn === "host") return;
room.setPlayerTeam(player.id, Team.BLUE);
};
room.connect(); // default = lobby pubblica BixBall
4. Avvio
# opzionale: file .env con BIXBALL_HEADLESS_TOKEN=thr_… node bot.js # oppure in package.json: "start": "node bot.js" → npm start
Guida completa nel repo: docs/HEADLESS_GETTING_STARTED.md. API dettagliata sotto.
API headless modulo
I creator require("bixball-sdk") (o ./headless nel repo) e creano una stanza sulla lobby. La fisica resta nel processo dello script; i giocatori del sito si collegano in WebRTC. NAT restrittivi possono richiedere un TURN.
const { HBInit, Team, Input, CollisionFlags } = require("bixball-sdk");
const room = HBInit({
roomName: "My Room",
maxPlayers: 16,
public: true,
noPlayer: false,
hostName: "Host",
stadium: "Classic",
scoreLimit: 3,
timeLimit: 0,
token: process.env.BIXBALL_HEADLESS_TOKEN
});
room.onRoomLink = (link) => {
console.log("Stanza in lista su", link);
};
room.onPlayerJoin = (player) => {
if (player.conn === "bot") return;
room.setPlayerAdmin(player.id, true);
room.sendAnnouncement("Ciao " + player.name);
};
room.connect(); // lobby pubblica (override: BIXBALL_WS o connect(url))
// room.connect("local"); // → ws://127.0.0.1:3847
Esempio base: node examples/headless-bot.js — nella stanza premi K o scrivi !avatars.
Esempio avanzato (annunci colorati, tabellone, ads Discord): node examples/headless-promo-bot.js.
Opzioni HBInit
| Campo | Descrizione |
|---|---|
roomName | Nome stanza |
maxPlayers | 0 = illimitati |
password | Opzionale |
public | Visibile in lista |
noPlayer | Default true: lo script non entra come giocatore. false: entra l’host (non un bot) |
hostName | Nome dell’host se noPlayer: false (alias: playerName) |
stadium | Nome mappa default |
hbs | Testo stadium custom |
scoreLimit / timeLimit | Limiti partita |
afkTimeout | Secondi senza input in campo prima di onAfk (default 12, 0 = off) |
minPlayersPerTeam | Soglia onEnoughPlayers (default 1, 0 = off) |
collisionEvents | Abilita eventi collisione (opt-in, costosi) |
extrapolation | Limite client. Assente o false = nessun limite. Numero = valore fisso (ms). Oggetto { min, max } (o { value }) = intervallo. All’ingresso i giocatori vengono portati al minimo (di solito 0 = off). |
Eventi
Assegna funzioni sulla room. Stessi nomi di Haxball dove possibile.
| Evento | Argomenti | Quando |
|---|---|---|
onPlayerJoin | player | Join (umano o bot) |
onPlayerLeave | player, reason | Uscita spontanea / disconnect (non kick) |
onPlayerKicked | player, reason, ban, byPlayer | Kick o ban |
onPlayerChat | player, message | Return false per non mostrare. Non parte se c’è onCommand su un ! |
onCommand | player, cmd, args | !bb foo → "bb", ["foo"]. Default: nasconde. Return true per mostrare in chat |
onGameStart | byPlayer | null | Start |
onGameStop | byPlayer | null | Stop |
onGameTick | — | Ogni tick 60 Hz (partita attiva) |
onKickOff | player | Primo tocco; barriere KO off |
onPlayerBallTouch | player | Inizio contatto palla (non ogni tick) |
onPlayerBallKick | player | Calcio (tasto kick) |
onGoal | { team, scores, scorer, assist, ownGoal, lastTouch } | Gol con stats |
onTeamGoal | team, goal | Stesso gol (compat Haxball) |
onGameEnd / onTeamVictory | winningTeam, scores, reason | Fine partita. reason: score | time |
onTimeLimitReached | scores | Scaduto il tempo |
onScoreLimitReached | team, scores | Raggiunto il limite gol |
onGamePause | paused, byPlayer | Pausa on/off |
onGameUnpause | byPlayer | Ripresa dopo countdown |
onPlayerActivity | player | Cambio input |
onAfk | player, seconds | Niente input in campo per afkTimeout |
onEnoughPlayers | red, blue | Entrambe le squadre ≥ soglia |
onNotEnoughPlayers | red, blue | Si scende sotto soglia |
onKickRateLimitSet | min, rate, burst, byPlayer | Rate limit calcio |
onRoomClose | reason | Stanza distrutta / host caduto |
onCollisionDiscVsDisc | discA, discB | Opt-in collisionEvents |
onCollisionDiscVsSegment | disc, segmentIndex | Opt-in |
onCollisionDiscVsPlane | disc, planeIndex | Opt-in |
onPlayerAvatarChange | id, avatar, image | Cambio testo e/o immagine disco |
onPlayerUi | player, action, data | Azione dalla UI overlay (es. equipAvatar) |
onPlayerTeamChange | player, byPlayer | Cambio squadra |
onPlayerAdminChange | player, byPlayer | Admin |
onStadiumChange | name | Mappa |
onPositionsReset | — | Reset dopo gol |
onRoomLink | url | Dopo connect() (URL del sito hostato) |
Oggetto player
{ id, name, team, admin, avatar, avatarImg, flag, ping, conn, auth, position: {x,y} | null }
flag è il codice ISO (es. "IT") scelto al login; ping è aggiornato dal client (ms).
Annunci — sendAnnouncement
Due forme: classica (stile Haxball) e oggetto (testo multi-colore, HTML/CSS, suoni, link).
1. Forma classica
room.sendAnnouncement(msg, targetId, color, style, sound);
// Tutti
room.sendAnnouncement("Ciao a tutti");
room.sendAnnouncement("Goal!", null, 0xe56e56, 2, 1);
// Solo a un giocatore
room.sendAnnouncement("Sei admin", player.id, 0xd8b45a, 1, 0);
| Argomento | Tipo | Descrizione |
|---|---|---|
msg | string | Testo. Gli URL (https://…, discord.gg/…) diventano link cliccabili nel client |
targetId | number | null | null = broadcast; altrimenti solo quel player |
color | number | Colore RGB intero, es. 0xff0000 o 0xffffff |
style | number | Stile legacy (0 normal, 1 bold, 2 italic… — come Haxball) |
sound | number | 0 silenzio, 1/2 beep client |
2. Forma oggetto (avanzata)
room.sendAnnouncement({
msg: "Testo semplice", // oppure text
targetId: null, // o player.id
color: 0xffffff,
style: 0,
sound: 0, // beep numerico
soundFile: "sounds/goal.mp3", // file in assets/sounds
css: "font-weight:800", // stile sul contenitore .ann
html: '<span style="color:#f00">HTML</span>',
segments: [ /* pezzi colorati */ ],
align: "center" // "left" | "center" | "right"
});
| Campo | Descrizione |
|---|---|
msg / text | Fallback testo piano (con auto-link URL) |
html | HTML sanitizzato (niente script). Usa <a href>, gradienti, span colorati |
segments | Array di pezzi: vedi tabella sotto. Ha priorità su html/msg |
css | CSS inline sul blocco messaggio |
align | "left" · "center" · "right" (anche textAlign / alignment) |
soundFile | Chiave asset, es. sounds/welcome.mp3 (anche assets://sounds/…) |
targetId | Come sopra; welcome privati tipici |
Segmenti
{
text: "Discord",
color: 0x5865f2, // numero RGB o stringa "#5865F2"
css: "font-weight:900", // stile sul pezzo
className: "promo", // classe CSS opzionale
href: "https://discord.gg/…" // rende il pezzo un link <a>
}
// Welcome colorato + link
room.onPlayerJoin = (player) => {
if (player.conn === "host" || player.conn === "bot") return;
room.sendAnnouncement({
targetId: player.id,
align: "center",
segments: [
{ text: "Benvenuto ", color: 0xffffff },
{ text: player.name, color: 0x57f287, css: "font-weight:800" },
{ text: " — unisciti: ", color: 0xdcddde },
{ text: "Discord", href: "https://discord.gg/bixball", color: 0x5865f2,
css: "text-decoration:underline;font-weight:800" }
]
});
};
// HTML con gradiente
room.sendAnnouncement({
align: "center",
html: '<span style="background:linear-gradient(90deg,#ff6b6b,#5865f2);' +
'-webkit-background-clip:text;background-clip:text;color:transparent;font-weight:900">' +
'PROMO</span> <a href="https://discord.gg/bixball" target="_blank" ' +
'rel="noopener">entra nel server</a>'
});
// Suono custom (dopo putAsset / loadAssets)
room.sendAnnouncement({
msg: "Goal!",
color: 0xe56e56,
soundFile: "sounds/goal.mp3"
});
3. Immagini in chat — sendImageToChat
// Link esterno (centrata, size medium di default)
room.sendImageToChat("https://example.com/banner.png");
room.sendImageToChat({
url: "https://example.com/logo.png", // oppure src / image
// oppure asset locale: image: "images/promo.png" (dopo loadAssets/putAsset)
size: "lg", // xs|sm|md|lg|xl|full oppure numero px
width: 240, // opzionale, px o "50%"
height: "auto",
maxWidth: "80%",
align: "center", // left|center|right
href: "https://discord.gg/bixball", // click sull'immagine
caption: "Entra su Discord",
alt: "Discord",
targetId: null // o player.id
});
// Solo a un player
room.sendImageToChat({ url: "https://…/welcome.gif", size: "sm", align: "left" }, player.id);
Equivalente via announcement: room.sendAnnouncement({ url: "…", size: "md", align: "center", caption: "…" }).
Nota timing: al join il client svuota la chat; gli annunci ricevuti durante l’ingresso restano in coda e vengono mostrati subito dopo. Per welcome “sicuri” puoi anche fare setTimeout(() => room.sendAnnouncement(…), 500).
Tabellone score e lista player
Personalizza HUD partita senza overlay HTML. Le modifiche arrivano a tutti i client (e a chi join dopo).
setScoreboardStyle(opts)
room.setScoreboardStyle({
css: "#scoreboard{border-color:rgba(88,101,242,.5)}" +
"#scoreboard .score--red{color:#ff8b8b!important}" +
"#scoreboard .score--blue{color:#8ab4ff!important}",
redColor: "#ff8b8b", // colore numeri rossi
blueColor: "#8ab4ff",
sepColor: "#b5a0ff", // ":"
redIcon: "icons/red.png", // al posto del quadratino (asset icons/)
blueIcon: "icons/blue.png",
hideTeamIcons: false,
hideTime: false // nasconde il timer HUD
});
Selettori utili nel css: #scoreboard, .scoreBoard, .score--red, .score--blue, .score-sep, .gameTime, .teamicon.red, .teamicon.blue.
setPlayerListStyle(opts)
Controlla ordine e campi nella lista Red / Spec / Blue del menu Esc. Può essere globale per tutti e/o con override per singolo player.
room.setPlayerListStyle({
// Globale (tutti)
layout: ["flag", "admin", "name", "ping"],
// Token: flag|bandiera, admin|star|badge, name|playername|nick,
// ping, icon|avatar, icon2|aftericon
showPing: true,
showFlag: true,
showAdmin: true,
nameColor: "#f3f4f6",
icon: "icons/vip.png",
afterNameIcon: "icons/star.png",
css: ".player-row:hover{background:rgba(88,101,242,.12)}" +
".player-admin{color:#fee75c!important}" +
".player-row.vip{outline:1px solid rgba(254,231,92,.35)}" +
".player-row.host-row .player-name{color:#8ab4ff!important}",
// Override per utente (opzionale). Match: id | name | conn | auth
// (tutti i campi presenti devono combaciare; più regole si fondono in ordine)
players: [
{ id: 3, layout: ["icon", "name", "ping"], icon: "icons/crown.png",
nameColor: "#fee75c", className: "vip", badge: "VIP" },
{ name: "Moderator", nameIgnoreCase: true, afterNameIcon: "icons/star.png",
layout: ["flag", "admin", "name", "icon2", "ping"] },
{ conn: "host", className: "host-row", nameColor: "#8ab4ff" },
{ auth: "guest", showPing: false }
]
});
Sinonimi di players: overrides, perPlayer. Se ometti players in una chiamata successiva, gli override già impostati restano.
API dedicate per singolo utente
// Match: number (id), string (nome), oppure { id } | { name } | { conn } | { auth }
room.setPlayerInfoStyle(player.id, {
layout: ["icon", "name", "ping"],
icon: "icons/vip.png",
nameColor: "#fee75c",
className: "vip",
badge: "VIP",
style: "background:rgba(254,231,92,.08)" // inline sulla riga
});
room.setPlayerInfoStyle({ name: player.name }, { nameColor: "#57f287" });
room.setPlayerInfoStyle({ conn: player.conn }, { className: "trusted" });
room.setPlayerInfoStyle({ auth: player.auth }, { showFlag: false });
// Rimuovi uno / tutti
room.clearPlayerInfoStyle(player.id);
room.clearPlayerInfoStyle({ name: "Moderator" });
room.clearPlayerInfoStyle(); // svuota tutti gli override
Default layout globale: bandiera → ★ admin → nome → ping. Esempio: examples/headless-promo-bot.js.
Assets e UI custom
All’avvio di HBInit vengono create (se mancano) le cartelle:
headless/assets/{avatars,images,icons,sounds}
<cartella-dello-script>/assets/{avatars,images,icons,sounds}
Metti PNG/SVG/WebP in assets/avatars (o images/icons). Audio in assets/sounds (mp3, ogg, wav, m4a). Al connect() i file restano sull’host e arrivano ai client nel payload UI (data URL), non più dal server lobby.
Per le immagini delle mappe (sfondo e dischi): nell’editor importale dal PC. Nello stadium resta il percorso images/file.ext. In partita il file deve stare in assets/images (stesso nome) e viene servito da /assets/images/….
Overlay HTML/CSS/JS
room.onRoomLink = () => {
room.setRoomUI({
id: "shop",
title: "Shop",
key: "KeyK", // tasto nel client
command: "shop", // !shop
html: '<img src="assets://avatars/fox.svg">',
css: "img{width:48px}",
js: 'bit.send({ a: "equipAvatar", image: "avatars/fox.svg", text: "FX" });'
});
};
room.onCommand = (player, cmd) => {
if (cmd === "shop") room.openUI(player.id, "shop");
};
room.setPlayerAvatar(player.id, { text: "FX", image: "avatars/fox.svg" });
Nella UI, bit.send({ a, image, text }) arriva come onPlayerUi. L’azione equipAvatar applica l’immagine al disco se il file è tra gli asset della stanza. L’immagine è disegnata al centro del disco per tutti i client.
Scorciatoia: room.setAvatarPickerUI() (usata da examples/headless-bot.js).
Metodi room
| Metodo | Descrizione |
|---|---|
getPlayer(id) | Player o null |
getPlayerList() | Array player |
getBallPosition() | {x,y} o null |
getDiscPosition(index) | Disco per indice |
getScores() | red, blue, time, limiti |
sendAnnouncement(…) | Vedi Annunci: classica/oggetto, align, segments, html, soundFile |
sendImageToChat(url | opts, targetId?) | Immagine in chat (link o asset), size, align, caption, href |
setScoreboardStyle(opts) | Vedi Tabellone: CSS, colori, icone, hideTime |
setPlayerListStyle(opts) | Vedi Lista player: globale + players[] per id/name/conn/auth |
setPlayerInfoStyle(match, style) | Override riga player (match: id, name, conn, auth) |
clearPlayerInfoStyle(match?) | Rimuove uno o tutti gli override per-player |
sendChat(msg) | Chat da script |
setPlayerTeam(id, team) | 0 spec, 1 red, 2 blue |
setPlayerAdmin(id, bool) | Admin |
setPlayerAvatar(id, text | { text, image }) | Testo sul disco e/o immagine avatars/file.svg |
kickPlayer(id, reason, ban) | Kick/ban |
addBot(opts) / removeBot(id) | Bot in fisica |
startGame() / stopGame() | Partita |
pauseGame(bool) | Pausa |
setScoreLimit(n) / setTimeLimit(n) | Limiti |
setTeamsLock(bool) | Blocca cambio squadra |
setKickRateLimit(min, rate, burst) | Rate limit calcio |
setTeamColors(team, angle, ...colors) | Colori maglia |
setDefaultStadium(name) | Mappa built-in |
setCustomStadium(hbsText) | HBS |
setPlayerDiscProperties(id, props) | x, y, xspeed, yspeed, radius… |
setDiscProperties(index, props) | Disco per indice |
setPlayerInput(id, bits) | Input bot |
setAfkTimeout(seconds) | 0 = disattiva onAfk |
setMinPlayersPerTeam(n) | Soglia auto-start |
setCollisionEvents(bool) | Collisioni verso lo script |
ensureAssets() | Crea assets/avatars|images|icons|sounds |
loadAssets() | Carica i file sul server della stanza |
putAsset(kind, name, data) | kind: avatars, images, icons, sounds |
listAssets(kind?) | Manifest file caricati |
setRoomUI({ id, html, css, js, key, command }) | Overlay HTML/CSS/JS per i client |
setAvatarPickerUI() | UI avatar pronta (K / !avatars) |
openUI(playerId, id) | Apre l’overlay a un giocatore |
connect(url?) | Si registra in lobby (default ws://127.0.0.1:3847); i guest arrivano in WebRTC |
listen(port) | Opzionale: sito standalone (non serve se usi la lobby) |
destroy() | Chiude sandbox e server |
Bot
const bot = room.addBot({ name: "NPC", team: Team.RED, admin: false, avatar: "B" });
room.onGameTick = () => {
const ball = room.getBallPosition();
const p = room.getPlayer(bot.id);
if (!ball || !p || !p.position) return;
let k = 0;
if (ball.y < p.position.y) k |= Input.UP;
if (ball.y > p.position.y) k |= Input.DOWN;
room.setPlayerInput(bot.id, k);
};
room.removeBot(bot.id);
I bot sono player normali nella fisica (disco, collisioni, calci). Non occupano un datachannel.
Costanti
Team.SPEC = 0 Team.RED = 1 Team.BLUE = 2 Input.UP = 1 DOWN = 2 LEFT = 4 RIGHT = 8 KICK = 16 CollisionFlags.ball, red, blue, redKO, blueKO, wall, kick, score, c0–c3, all GamePlayState.BeforeKickOff, Playing, AfterGoal, Ending
Formato .bit
Magic BIT1 (4 byte) + gzip di un JSON:
{
"magic": "BIT1",
"version": 1,
"tickRate": 60,
"meta": { "room", "stadium", "startedAt", "endedAt", "frames", "events" },
"stadium": { /* serializeStadium */ },
"players": [ { id, name, team, ... } ],
"frames": [ [frameNo, gameTuple, discs, playersSlim], ... ],
"events": [ { f, t, ... } ]
}
Modulo: public/js/utoReplay.js (encode/decode, anche da Node con pako).
Struttura progetto
BixBall/ engine/bit/ motore (hbs, physics, room, clock) examples/ headless-bot.js, headless-promo-bot.js headless/ HBInit, assets/avatars|images|icons|sounds maps/ stadium .hbs public/ client, replay-analyzer, documentation public/vendor/flag-icons/ bandiere SVG (flag-icons) src/server.js lobby + signaling WebRTC src/lobby.js directory stanze (niente fisica) src/roomHost.js host runtime (browser bundle / headless) src/utoApi.js parse HBS, default maps
Porta default 3847 (sito + lobby/signaling). Lo script headless si collega a quella porta e hosta la partita in locale.