// Name: Generate Haxball Headless Script // Description: Creates a ready-to-use Haxball Headless host script with basic admin and chat commands. // Author: htzzsdcs9m-lang // GitHub: htzzsdcs9m-lang import "@johnlindquist/kit" const [roomName, maxPlayersStr, publicStr, roomPassword, adminPassword] = await fields( [ { label: "Room Name", placeholder: "My Haxball Room", value: "My Haxball Room", required: true }, { label: "Max Players", type: "number", value: "8", min: 2, max: 30, step: 1, required: true }, { label: "Public? (yes/no)", value: "yes", required: true }, { label: "Password (optional)", value: "" }, { label: "Admin Password (optional)", value: "" }, ], [ { name: "Generate", shortcut: "return", }, ] ) const maxPlayers = Math.max(2, Math.min(30, parseInt(maxPlayersStr || "8", 10) || 8)) const isPublic = /^y(es)?$/i.test(publicStr || "yes") const pwd = (roomPassword || "").trim() const adminPwd = (adminPassword || "").trim() const js = `/* Haxball Headless Host Script - Room: ${roomName} - Max Players: ${maxPlayers} - Public: ${isPublic} - Password: ${pwd ? "(set)" : "(none)"} - Admin Password: ${adminPwd ? "(set)" : "(none)"} Paste this into https://www.haxball.com/headless (or your headless runtime). */ const room = HBInit({ roomName: ${JSON.stringify(roomName)}, maxPlayers: ${maxPlayers}, public: ${isPublic}, password: ${pwd ? JSON.stringify(pwd) : "undefined"}, noPlayer: true, }); const CONFIG = { adminPassword: ${adminPwd ? JSON.stringify(adminPwd) : "undefined"}, scoreLimit: 5, timeLimit: 3, stadium: "Classic", autoAdminIfNoPassword: ${adminPwd ? "false" : "true"}, }; room.setDefaultStadium(CONFIG.stadium); room.setScoreLimit(CONFIG.scoreLimit); room.setTimeLimit(CONFIG.timeLimit); const colors = { info: 0x95a5a6, ok: 0x2ecc71, warn: 0xf39c12, err: 0xe74c3c, sys: 0x3498db, }; function send(msg, color = colors.info) { room.sendAnnouncement(msg, null, color, "bold"); } function getAdmins() { return room.getPlayerList().filter(p => p.admin); } function isAdmin(player) { return !!player.admin; } function findPlayerByIdOrName(query) { const id = parseInt(query, 10); if (!isNaN(id)) return room.getPlayerList().find(p => p.id === id) || null; const q = String(query).toLowerCase(); return room.getPlayerList().find(p => p.name.toLowerCase().includes(q)) || null; } function kickIfAdmin(issuer, target, reason = "Kicked by admin") { if (!isAdmin(issuer)) { send(\`❌ You must be admin to use this command.\`, colors.err); return; } if (!target) { send(\`❌ Player not found.\`, colors.err); return; } if (target.id === issuer.id) { send(\`❌ You cannot kick yourself.\`, colors.err); return; } room.kickPlayer(target.id, reason, false); } function showHelp(player) { const cmds = [ "!help - Show this help", CONFIG.adminPassword ? "!admin <password> - Become admin" : "(admin password not set; first join may be admin)", "!red / !blue / !spec - Move to team", "!rr - Restart match (admin)", "!start - Start match (admin)", "!stop - Stop match (admin)", "!kick <id|name> - Kick player (admin)", "!players - List players", ]; cmds.forEach(c => room.sendAnnouncement("• " + c, player?.id || null, colors.sys, "small")); } room.onRoomLink = link => { send("Room link: " + link, colors.sys); }; room.onPlayerJoin = player => { send(\`👋 \${player.name} joined (id:\${player.id})\`, colors.ok); // Auto-admin first player if no admin password set if (CONFIG.autoAdminIfNoPassword && getAdmins().length === 0) { room.setPlayerAdmin(player.id, true); room.sendAnnouncement("You are admin (auto). Type !help", player.id, colors.ok, "normal"); } else if (CONFIG.adminPassword) { room.sendAnnouncement("Type !admin <password> to become admin.", player.id, colors.info, "normal"); } // Balance teams a bit const reds = room.getPlayerList().filter(p => p.team === 1).length; const blues = room.getPlayerList().filter(p => p.team === 2).length; if (reds > blues) room.setPlayerTeam(player.id, 2); else room.setPlayerTeam(player.id, 1); }; room.onPlayerLeave = player => { send(\`👋 \${player.name} left\`, colors.warn); }; room.onPlayerKicked = (kickedPlayer, reason, ban, byPlayer) => { if (byPlayer) { send(\`🚫 \${kickedPlayer.name} was kicked by \${byPlayer.name} (\${reason || "no reason"})\`, colors.warn); } else { send(\`🚫 \${kickedPlayer.name} was kicked (\${reason || "no reason"})\`, colors.warn); } }; room.onTeamGoal = team => { const teamName = team === 1 ? "Red" : "Blue"; send(\`🥅 Goal for \${teamName}!\`, colors.ok); }; room.onGameStart = byPlayer => { if (byPlayer) send(\`▶️ Match started by \${byPlayer.name}\`, colors.sys); }; room.onGameStop = byPlayer => { if (byPlayer) send(\`⏹ Match stopped by \${byPlayer.name}\`, colors.sys); }; room.onPlayerChat = (player, message) => { const msg = (message || "").trim(); if (!msg.startsWith("!")) return false; const [rawCmd, ...rest] = msg.slice(1).split(/\s+/); const cmd = rawCmd.toLowerCase(); const arg = rest.join(" "); switch (cmd) { case "help": showHelp(player); break; case "players": { const list = room.getPlayerList(); if (list.length === 0) { room.sendAnnouncement("No players.", player.id, colors.info, "normal"); } else { list.forEach(p => { const t = p.team === 1 ? "RED" : p.team === 2 ? "BLUE" : "SPEC"; const tag = p.admin ? " (admin)" : ""; room.sendAnnouncement(\`#\${p.id} \${p.name} [\${t}]\${tag}\`, player.id, colors.info, "small"); }); } break; } case "admin": { if (!CONFIG.adminPassword) { room.sendAnnouncement("Admin password is not set.", player.id, colors.err, "normal"); break; } if (!arg) { room.sendAnnouncement("Usage: !admin <password>", player.id, colors.info, "normal"); break; } if (arg === CONFIG.adminPassword) { room.setPlayerAdmin(player.id, true); room.sendAnnouncement("✅ You are now admin.", player.id, colors.ok, "normal"); } else { room.sendAnnouncement("❌ Incorrect password.", player.id, colors.err, "normal"); } break; } case "red": room.setPlayerTeam(player.id, 1); break; case "blue": room.setPlayerTeam(player.id, 2); break; case "spec": room.setPlayerTeam(player.id, 0); break; case "rr": // restart if (!isAdmin(player)) { room.sendAnnouncement("❌ Admin only.", player.id, colors.err, "normal"); break; } room.stopGame(); room.startGame(); break; case "start": if (!isAdmin(player)) { room.sendAnnouncement("❌ Admin only.", player.id, colors.err, "normal"); break; } room.startGame(); break; case "stop": if (!isAdmin(player)) { room.sendAnnouncement("❌ Admin only.", player.id, colors.err, "normal"); break; } room.stopGame(); break; case "kick": { if (!isAdmin(player)) { room.sendAnnouncement("❌ Admin only.", player.id, colors.err, "normal"); break; } if (!arg) { room.sendAnnouncement("Usage: !kick <id|name>", player.id, colors.info, "normal"); break; } const target = findPlayerByIdOrName(arg); kickIfAdmin(player, target, "Kicked via !kick"); break; } default: room.sendAnnouncement(\`Unknown command: !\${cmd}. Type !help\`, player.id, colors.warn, "normal"); break; } return false; // prevent echo }; // Optional: auto-start if enough players function maybeAutoStart() { const fieldPlayers = room.getPlayerList().filter(p => p.team === 1 || p.team === 2); if (!room.getScores() && fieldPlayers.length >= 2) { room.startGame(); } } room.onPlayerTeamChange = () => maybeAutoStart(); ` await copy(js) await notify("Haxball script copied to clipboard") await editor({ value: js, hint: "Save as haxball-room.js and paste into the Haxball Headless console.", shortcuts: [ { name: "Copy", key: `${cmd}+c`, onPress: async (input: string) => { await copy(input) await toast("Copied!") }, bar: "right", }, ], })// Name: Haxball Aim Helper // Description: Compute aiming angles from positions; does not modify or automate the game. // Author: htzzsdcs9m-lang // GitHub: htzzsdcs9m-lang import "@johnlindquist/kit" function toNumber(s: string, fallback = 0) { const n = parseFloat((s || "").trim()) return Number.isFinite(n) ? n : fallback } function deg(rad: number) { let d = (rad * 180) / Math.PI if (d < 0) d += 360 return d } function dist(ax: number, ay: number, bx: number, by: number) { const dx = bx - ax const dy = by - ay return Math.hypot(dx, dy) } while (true) { const [pxs, pys, bxs, bys, txs, tys] = await fields( [ { label: "Player X", placeholder: "e.g. 0" }, { label: "Player Y", placeholder: "e.g. 0" }, { label: "Ball X", placeholder: "e.g. 10" }, { label: "Ball Y", placeholder: "e.g. 0" }, { label: "Target X (goal/teammate)", placeholder: "e.g. 30" }, { label: "Target Y (goal/teammate)", placeholder: "e.g. 0" }, ], [ { name: "Reset", onAction: async () => { submit(["", "", "", "", "", ""]) }, flag: "reset", }, ] ) const px = toNumber(pxs) const py = toNumber(pys) const bx = toNumber(bxs) const by = toNumber(bys) const tx = toNumber(txs) const ty = toNumber(tys) const anglePlayerToBall = deg(Math.atan2(by - py, bx - px)) const angleBallToTarget = deg(Math.atan2(ty - by, tx - bx)) const anglePlayerToTarget = deg(Math.atan2(ty - py, tx - px)) const dPlayerBall = dist(px, py, bx, by) const dBallTarget = dist(bx, by, tx, ty) const dPlayerTarget = dist(px, py, tx, ty) const html = md(` # Haxball Aim Helper (Training) - Face the ball: ${anglePlayerToBall.toFixed(1)}° - Desired ball travel toward target: ${angleBallToTarget.toFixed(1)}° - Direct line from you to target: ${anglePlayerToTarget.toFixed(1)}° Distances: - Player → Ball: ${dPlayerBall.toFixed(2)} - Ball → Target: ${dBallTarget.toFixed(2)} - Player → Target: ${dPlayerTarget.toFixed(2)} Notes: - Use these angles as guidance to line up your player and the ball. - This helper does not inject, automate, or modify the game. `) await div(html) const action = await arg("Next action", ["Recalculate", "Copy Angles", "Exit"]) if (action === "Copy Angles") { const text = [ `Player→Ball: ${anglePlayerToBall.toFixed(1)}°`, `Ball→Target: ${angleBallToTarget.toFixed(1)}°`, `Player→Target: ${anglePlayerToTarget.toFixed(1)}°`, ].join("\n") await copy(text) await toast("Angles copied to clipboard") } else if (action === "Exit") { exit() } }