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.

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

  1. Nickname (e avatar opzionale) — nessuna account
  2. Lista stanze pubbliche, filtro, crea stanza (nome, password, max, mappa, pubblica)
  3. In campo: squadre, start/stop/pause, chat, HBS custom, REC

Controlli

TastoAzione
Frecce / WASDMovimento
X, Spazio, CtrlCalcio
KUI stanza (se lo script la registra, es. avatar)
InvioChat
EscMenu 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:

FileRuolo
engine/bit/hbs.jsParse JSON5 HBS, traits, colori, flag, ballPhysics
engine/bit/physics.jsIntegrazione, collisioni disco/segmento/piano/arco, calci, joint
engine/bit/room.jsStato partita, kickoff, gol, pause, sandbox callbacks
engine/bit/clock.jsClock 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.

Se la mappa mette già un disco-palla in 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.

Kickoff e regole

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 /headlesstokenNuovo 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

CampoDescrizione
roomNameNome stanza
maxPlayers0 = illimitati
passwordOpzionale
publicVisibile in lista
noPlayerDefault true: lo script non entra come giocatore. false: entra l’host (non un bot)
hostNameNome dell’host se noPlayer: false (alias: playerName)
stadiumNome mappa default
hbsTesto stadium custom
scoreLimit / timeLimitLimiti partita
afkTimeoutSecondi senza input in campo prima di onAfk (default 12, 0 = off)
minPlayersPerTeamSoglia onEnoughPlayers (default 1, 0 = off)
collisionEventsAbilita eventi collisione (opt-in, costosi)
extrapolationLimite 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.

EventoArgomentiQuando
onPlayerJoinplayerJoin (umano o bot)
onPlayerLeaveplayer, reasonUscita spontanea / disconnect (non kick)
onPlayerKickedplayer, reason, ban, byPlayerKick o ban
onPlayerChatplayer, messageReturn false per non mostrare. Non parte se c’è onCommand su un !
onCommandplayer, cmd, args!bb foo"bb", ["foo"]. Default: nasconde. Return true per mostrare in chat
onGameStartbyPlayer | nullStart
onGameStopbyPlayer | nullStop
onGameTickOgni tick 60 Hz (partita attiva)
onKickOffplayerPrimo tocco; barriere KO off
onPlayerBallTouchplayerInizio contatto palla (non ogni tick)
onPlayerBallKickplayerCalcio (tasto kick)
onGoal{ team, scores, scorer, assist, ownGoal, lastTouch }Gol con stats
onTeamGoalteam, goalStesso gol (compat Haxball)
onGameEnd / onTeamVictorywinningTeam, scores, reasonFine partita. reason: score | time
onTimeLimitReachedscoresScaduto il tempo
onScoreLimitReachedteam, scoresRaggiunto il limite gol
onGamePausepaused, byPlayerPausa on/off
onGameUnpausebyPlayerRipresa dopo countdown
onPlayerActivityplayerCambio input
onAfkplayer, secondsNiente input in campo per afkTimeout
onEnoughPlayersred, blueEntrambe le squadre ≥ soglia
onNotEnoughPlayersred, blueSi scende sotto soglia
onKickRateLimitSetmin, rate, burst, byPlayerRate limit calcio
onRoomClosereasonStanza distrutta / host caduto
onCollisionDiscVsDiscdiscA, discBOpt-in collisionEvents
onCollisionDiscVsSegmentdisc, segmentIndexOpt-in
onCollisionDiscVsPlanedisc, planeIndexOpt-in
onPlayerAvatarChangeid, avatar, imageCambio testo e/o immagine disco
onPlayerUiplayer, action, dataAzione dalla UI overlay (es. equipAvatar)
onPlayerTeamChangeplayer, byPlayerCambio squadra
onPlayerAdminChangeplayer, byPlayerAdmin
onStadiumChangenameMappa
onPositionsResetReset dopo gol
onRoomLinkurlDopo 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);
ArgomentoTipoDescrizione
msgstringTesto. Gli URL (https://…, discord.gg/…) diventano link cliccabili nel client
targetIdnumber | nullnull = broadcast; altrimenti solo quel player
colornumberColore RGB intero, es. 0xff0000 o 0xffffff
stylenumberStile legacy (0 normal, 1 bold, 2 italic… — come Haxball)
soundnumber0 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"
});
CampoDescrizione
msg / textFallback testo piano (con auto-link URL)
htmlHTML sanitizzato (niente script). Usa <a href>, gradienti, span colorati
segmentsArray di pezzi: vedi tabella sotto. Ha priorità su html/msg
cssCSS inline sul blocco messaggio
align"left" · "center" · "right" (anche textAlign / alignment)
soundFileChiave asset, es. sounds/welcome.mp3 (anche assets://sounds/…)
targetIdCome 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

MetodoDescrizione
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.