import { createCanvas, loadImage, GlobalFonts } from "@napi-rs/canvas"; import { GenerateBattlerOptions, GenerateBattleImageOptions, PlayerActionOptions, Opponents, CustomActionOptions, CommunityOpponents } from "./types"; import { generateBattler, drawText, applyText, validOpponents, validPlayerActions } from "./utils"; import { createServer } from "node:http"; import GenerateParty from "./party" GlobalFonts .registerFromPath(import.meta.dirname + "/LilitaOne.ttf", "Lilita One"); const opponents = { [Opponents.BattleBeginner]: { text: "Battle Beginner", colour: ["#007500", "#FFFFFF"] }, [Opponents.BattleBeginner2024]: { text: "Battle Beginner", colour: ["#007500", "#FFFFFF"] }, [Opponents.BattleCasual]: { text: "Battle Casual", colour: ["#757500", "#FFFFFF"] }, [Opponents.BattleMaster]: { text: "Battle Master", colour: ["#750000", "#FFFFFF"] }, [Opponents.BattlePro]: { text: "Battle Pro", colour: ["#753000", "#FFFFFF"] }, [Opponents.Battler]: { text: "Battler", colour: ["#FFFFFF", "#000000"] }, [Opponents.BattlerElite]: { text: "Battler Elite", colour: ["#FFFFFF", "#FF0000"] }, [Opponents.None]: { text: "", colour: ["#FFFFFF", "#FFFFFF"] }, [Opponents.RapStar]: { text: "Rap Star", colour: ["#FF0000", "#FFFFFF"] }, [Opponents.ArtMaster]: { text: "Art Master", colour: ["#000000", "#FFFFFF"] }, [Opponents.TheInterrogator]: { text: "The Interrogator", colour: ["#FFFFFF", "#000000"] }, [Opponents.CommonOverseer]: { text: "Common Overseer", colour: ["#000000", "#32CD32"] }, [Opponents.Moonlite]: { text: "Moonlite", colour: ["#000000", "#FFFFFF"] }, [Opponents.Dicey]: { text: "Dicey", colour: ["#000000", "#FFFFFF"] }, [Opponents.Kacey]: { text: "Kacey", colour: ["#000000", "#FFFFFF"] }, [Opponents.Lexa]: { text: "Lexa", colour: ["#FFFFFF", "#FF00FF"] }, [Opponents.Delta]: { text: "Delta", colour: ["#000000", "#FFFFFF"] }, [Opponents.DoodlePro]: { text: "Doodle Pro", colour: ["#000000", "#FFFFFF"] }, [Opponents.GuitarHero]: { text: "Guitar Hero", colour: ["#FFFFFF", "#FF8000"] }, [Opponents.SmallKeyMaster]: { text: "Small KeyMaster", colour: ["#FFFFFF", "#32CD32"] }, [Opponents.RareOverseer]: { text: "Rare Overseer", colour: ["#000000", "#00FFFF"] }, [Opponents.DEMUL]: { text: "DEMUL", colour: ["#FFFFFF", "#000000"] }, [Opponents.AgentZ]: { text: "Agent Z", colour: ["#FFFFFF", "#000000"] }, [Opponents.MediumKeyMaster]: { text: "Medium KeyMaster", colour: ["#000000", "#00FFFF"] }, [Opponents.IncomingSword]: { text: "Incoming Sword", colour: ["#FF0000", "#FFFFFF"] }, [Opponents.Eda]: { text: "Eda", colour: ["#9000FF", "#FFFFFF"] }, [Opponents.TheFirewall]: { text: "The Firewall", colour: ["#FFFFFF", "#FFAA00"] }, [Opponents.HouseannorSupport]: { text: "Houseannor Support", colour: ["#FF00FF", "#FFFFFF"] }, [Opponents.Dicey2024]: { text: "Dicey", colour: ["#000000", "#FFFFFF"] }, [Opponents.CakeDay]: { text: "Cake Day", colour: ["#FA61FF", "#FFFFFF"] }, [Opponents.MasterOG]: { text: "Master OG", colour: ["#FF0000", "#FFFFFF"] } } const communityopponents = { [CommunityOpponents.Stik]: { text: "Stik", colour: ["#FF0000", "#000000"] } } const server = createServer(async (req, res) => { if (req.method === "OPTIONS") { res.setHeader("access-control-allow-origin", "*") return res.end(); } const url = new URL(req.url!, "https://loc.al"); if (req.method == "GET" && url.pathname == "/status") { return res.end("OK") } if (req.method == "GET" && url.pathname == "/battler.png") { console.log(`Generating battler`) const opts = Object.fromEntries( Array.from(url.searchParams.entries()) .map(([key, value]) => [key, value.replaceAll('+', ' ')]), ) as GenerateBattlerOptions; const battler = await generateBattler(opts); res.setHeader('content-type', 'image/png'); return res.end(battler.toBuffer("image/png"), 'binary') } if (req.method == "GET" && url.pathname == "/profilebattler.png") { const opts = Object.fromEntries( Array.from(url.searchParams.entries()) .map(([key, value]) => [key, value.replaceAll('+', ' ')]), ) as GenerateBattlerOptions & { username: string }; console.log(`Generating profile battler for ${opts.username}`) const canvas = createCanvas(1280, 1280); const ctx = canvas.getContext("2d"); const battler = await generateBattler(opts) ctx.drawImage(battler, 0, 0); ctx.textAlign = "center" ctx.font = applyText(canvas, opts.username, canvas.width - 20) drawText([640, 1100], opts.username, opts.username == "homeannor" ? "#AA00FF" : "#000000", { colour: "#FFFFFF", width: 20 }, ctx) res.setHeader('content-type', 'image/png'); return res.end(canvas.toBuffer("image/png"), 'binary') } if ( req.method == "GET" && (url.pathname == "/battlesquadfight.png" || url.pathname == "/battlestart.png") ) { const opts = Object.fromEntries( Array.from(url.searchParams.entries()) .map(([key, value]) => [key, value.replaceAll('+', ' ')]), ) as GenerateBattlerOptions & GenerateBattleImageOptions; if (!opts.opponent) { res.writeHead(400, { 'content-type': "application/json" }) return res.end( JSON.stringify({ error: '"opponent" query parameter not specified', }) ); } console.log(`Generating battle start for ${opts.username} and ${opts.opponent}`) // Image dimensions const canvas = createCanvas(1920, 1080); const context = canvas.getContext("2d"); if (!validOpponents.includes(opts.opponent)) opts.opponent = Opponents.None const SquadBackground = await loadImage( `./assets/battlebgs/${opts.opponent}.png`, ); context.drawImage(SquadBackground, 0, 0, canvas.width, canvas.height); const battler = await generateBattler(opts); context.drawImage(battler, -140, -100, battler.width, battler.height); context.textAlign = "center" context.font = applyText(canvas, opponents[opts.opponent].text, 875) drawText([1450, 1000], opponents[opts.opponent].text, opponents[opts.opponent].colour[1], { colour: opponents[opts.opponent].colour[0], width: 20 }, context); context.font = applyText(canvas, opts.username, 675) drawText([470, 1000], opts.username, opts.username == "homeannor" ? "#AA00FF" : "#000000", { colour: "#FFFFFF", width: 20 }, context) res.setHeader('content-type', 'image/png'); return res.end(canvas.toBuffer("image/png"), 'binary') } if ( req.method == "GET" && url.pathname == "communityvs/.png" ) { const opts = Object.fromEntries( Array.from(url.searchParams.entries()) .map(([key, value]) => [key, value.replaceAll('+', ' ')]), ) as GenerateBattlerOptions & GenerateBattleImageOptions; if (!opts.opponent) { res.writeHead(400, { 'content-type': "application/json" }) return res.end( JSON.stringify({ error: '"opponent" query parameter not specified', }) ); } console.log(`Generating battle start for ${opts.username} and ${opts.opponent}`) // Image dimensions const canvas = createCanvas(1920, 1080); const context = canvas.getContext("2d"); if (!validOpponents.includes(opts.opponent)) opts.opponent = CommunityOpponents.None const SquadBackground = await loadImage( `./assets/communityvs/${opts.opponent}.png`, ); context.drawImage(SquadBackground, 0, 0, canvas.width, canvas.height); const battler = await generateBattler(opts); context.drawImage(battler, -140, -100, battler.width, battler.height); context.textAlign = "center" context.font = applyText(canvas, opponents[opts.opponent].text, 875) drawText([1450, 1000], opponents[opts.opponent].text, opponents[opts.opponent].colour[1], { colour: opponents[opts.opponent].colour[0], width: 20 }, context); context.font = applyText(canvas, opts.username, 675) drawText([470, 1000], opts.username, opts.username == "homeannor" ? "#AA00FF" : "#000000", { colour: "#FFFFFF", width: 20 }, context) res.setHeader('content-type', 'image/png'); return res.end(canvas.toBuffer("image/png"), 'binary') } if ( req.method == "GET" && url.pathname == "/playeraction.png" ) { const opts = Object.fromEntries( Array.from(url.searchParams.entries()) .map(([key, value]) => [key, value.replaceAll('+', ' ')]), ) as PlayerActionOptions; if (!opts.action) { res.writeHead(400, { 'content-type': "application/json" }) return res.end( JSON.stringify({ error: '"action" query parameter not specified', }) ); } if (!validPlayerActions.includes(opts.action)) { res.writeHead(400, { 'content-type': "application/json" }) return res.end( JSON.stringify({ error: '"action" query parameter not formatted correctly. Got "' + opts.action + '".', }) ); } console.log(`Generating ${opts.action} for ${opts.username}`) // Image dimensions const canvas = createCanvas(1920, 1080); const context = canvas.getContext("2d"); const actionBackground = await loadImage( `./assets/actionbgs/${opts.action}.png`, ); context.drawImage(actionBackground, 0, 0, canvas.width, canvas.height); const pUrl = new URL(encodeURI(opts.player)) const player = Object.fromEntries( Array.from(pUrl.searchParams.entries()) .map(([key, value]) => [key, value.replaceAll('+', ' ')]), ) as GenerateBattlerOptions if (opts.action.endsWith("Left")) { const battler = await generateBattler(player); context.drawImage(battler, -50, -100, battler.width, battler.height); } else if (opts.action.endsWith("Right")) { const battler = await generateBattler({ ...player, direction: "left" }); context.drawImage(battler, (canvas.width / 2) - 300, -100, battler.width, battler.height); } res.setHeader('content-type', 'image/png'); return res.end(canvas.toBuffer("image/png"), 'binary') } if ( req.method == "GET" && url.pathname == "/customaction.png" ) { const opts = Object.fromEntries( Array.from(url.searchParams.entries()) .map(([key, value]) => [key, value.replaceAll('+', ' ')]), ) as CustomActionOptions; if (!opts.background) { res.writeHead(400, { 'content-type': "application/json" }) return res.end( JSON.stringify({ error: '"background" query parameter not found', }) ); } console.log(`Generating custom action (${opts.background})`) // Image dimensions const canvas = createCanvas(1920, 1080); const context = canvas.getContext("2d"); const actionBackground = await loadImage( opts.background, ); context.drawImage(actionBackground, 0, 0, canvas.width, canvas.height); const pUrl = new URL(encodeURI(opts.player)) const player = Object.fromEntries( Array.from(pUrl.searchParams.entries()) .map(([key, value]) => [key, value.replaceAll('+', ' ')]), ) as GenerateBattlerOptions const battler = await generateBattler(player); if (opts.position == "middle") { context.drawImage(battler, (1920 / 2) - 640, -100, battler.width, battler.height); } else { context.drawImage(battler, -50, -100, battler.width, battler.height); } res.setHeader('content-type', 'image/png'); return res.end(canvas.toBuffer("image/png"), 'binary') } if ( req.method == "GET" && url.pathname == "/playermap.png" ) { const opts = Object.fromEntries( Array.from(url.searchParams.entries()) .map(([key, value]) => [key, value.replaceAll('+', ' ')]), ) as { player1: string, player2: string, map: string }; if (!opts.player1 || !opts.player2 || !opts.map) { res.writeHead(400, { 'content-type': "application/json" }) return res.end( JSON.stringify({ error: 'Did not pass all parameters', }) ); } console.log(`Generating player map (${opts.map})`) const p1URL = new URL(encodeURI(opts.player1)) const p2URL = new URL(encodeURI(opts.player2)) const p1Opts = Object.fromEntries( Array.from(p1URL.searchParams.entries()) .map(([key, value]) => [key, value.replaceAll('+', ' ')]), ) as GenerateBattlerOptions const p2Opts = Object.fromEntries( Array.from(p2URL.searchParams.entries()) .map(([key, value]) => [key, value.replaceAll('+', ' ')]), ) as GenerateBattlerOptions const p1 = await generateBattler(p1Opts); const p2 = await generateBattler({ ...p2Opts, direction: "left" }) // Image dimensions const canvas = createCanvas(1920, 1080); const context = canvas.getContext("2d"); const Background = await loadImage( `./assets/playerlocations/${opts.map}.png`, ); context.drawImage(Background, 0, 0, canvas.width, canvas.height); context.drawImage(p1, 140, 313, p1.width / 2, p1.height / 2); context.drawImage(p2, (canvas.width / 2) + 177, 313, p2.width / 2, p2.height / 2); res.setHeader('content-type', 'image/png'); return res.end(canvas.toBuffer("image/png"), 'binary') } if ( req.method == "GET" && url.pathname == "/map.png" ) { const opts = Object.fromEntries( Array.from(url.searchParams.entries()) .map(([key, value]) => [key, value.replaceAll('+', ' ')]), ) as { player: string, map: string }; if (!opts.player || !opts.map) { res.writeHead(400, { 'content-type': "application/json" }) return res.end( JSON.stringify({ error: 'Did not pass all parameters', }) ); } console.log(`Generating map (${opts.map})`) const p1URL = new URL(encodeURI(opts.player)) const p1Opts = Object.fromEntries( Array.from(p1URL.searchParams.entries()) .map(([key, value]) => [key, value.replaceAll('+', ' ')]), ) as GenerateBattlerOptions const p1 = await generateBattler(p1Opts); // Image dimensions const canvas = createCanvas(1920, 1080); const context = canvas.getContext("2d"); const Background = await loadImage( `./assets/locations/${opts.map}.png`, ); context.drawImage(Background, 0, 0, canvas.width, canvas.height); context.drawImage(p1, 140, 313, p1.width / 2, p1.height / 2); res.setHeader('content-type', 'image/png'); return res.end(canvas.toBuffer("image/png"), 'binary') } if ( req.method == "GET" && url.pathname == "/communitymap.png" ) { const opts = Object.fromEntries( Array.from(url.searchParams.entries()) .map(([key, value]) => [key, value.replaceAll('+', ' ')]), ) as { player: string, map: string }; if (!opts.player || !opts.map) { res.writeHead(400, { 'content-type': "application/json" }) return res.end( JSON.stringify({ error: 'Did not pass all parameters', }) ); } console.log(`Generating map (${opts.map})`) const p1URL = new URL(encodeURI(opts.player)) const p1Opts = Object.fromEntries( Array.from(p1URL.searchParams.entries()) .map(([key, value]) => [key, value.replaceAll('+', ' ')]), ) as GenerateBattlerOptions const p1 = await generateBattler(p1Opts); // Image dimensions const canvas = createCanvas(1920, 1080); const context = canvas.getContext("2d"); const Background = await loadImage( `./assets/communitymaps/${opts.map}.png`, ); context.drawImage(Background, 0, 0, canvas.width, canvas.height); context.drawImage(p1, 140, 313, p1.width / 2, p1.height / 2); res.setHeader('content-type', 'image/png'); return res.end(canvas.toBuffer("image/png"), 'binary') } if ( req.method == "GET" && url.pathname == "/pvp.png" ) { const opts = Object.fromEntries( Array.from(url.searchParams.entries()) .map(([key, value]) => [key, value.replaceAll('+', ' ')]), ) as { player1: string, player2: string, username1: string, username2: string }; if (!opts.player1 || !opts.player2 || !opts.username1 || !opts.username2) { res.writeHead(400, { 'content-type': "application/json" }) return res.end( JSON.stringify({ error: 'Did not pass all parameters', }) ); } console.log(`Generating PvP image for ${opts.username1} and ${opts.username2}`) const p1URL = new URL(encodeURI(opts.player1)) const p2URL = new URL(encodeURI(opts.player2)) const p1Opts = Object.fromEntries( Array.from(p1URL.searchParams.entries()) .map(([key, value]) => [key, value.replaceAll('+', ' ')]), ) as GenerateBattlerOptions const p2Opts = Object.fromEntries( Array.from(p2URL.searchParams.entries()) .map(([key, value]) => [key, value.replaceAll('+', ' ')]), ) as GenerateBattlerOptions const p1 = await generateBattler(p1Opts); const p2 = await generateBattler({ ...p2Opts, direction: "left" }) // Image dimensions const canvas = createCanvas(1920, 1080); const context = canvas.getContext("2d"); const Background = await loadImage( `./assets/battlebgs/PvP.png`, ); context.drawImage(Background, 0, 0, canvas.width, canvas.height); context.drawImage(p1, -140, -100, p1.width, p1.height); context.drawImage(p2, (canvas.width / 2) - 180, -100, p2.width, p2.height); context.textAlign = "center" context.font = applyText(canvas, opts.username2, 675) drawText([1450, 1000], opts.username2, opts.username2 == "homeannor" ? "#AA00FF" : "#000000", { colour: "#FFFFFF", width: 20 }, context) context.font = applyText(canvas, opts.username1, 675) drawText([470, 1000], opts.username1, opts.username1 == "homeannor" ? "#AA00FF" : "#000000", { colour: "#FFFFFF", width: 20 }, context) res.setHeader('content-type', 'image/png'); return res.end(canvas.toBuffer("image/png"), 'binary') } console.log(`Failed request (${req.method}, ${url.pathname})`) res.writeHead(404) return res.end("404 Not Found") }) server.listen(60125, () => { console.log(`Listening on localhost:60125`) })