UNPKG

node-haxball

Version:

The most powerful and lightweight API that allows you to develop your original Haxball(www.haxball.com) host, client, and standalone applications both on node.js and browser environments and also includes every possible hack and functionality that you can

1,116 lines (1,047 loc) 40.8 kB
module.exports = function(API, params){ const { OperationType, VariableType, ConnectionState, AllowFlags, Direction, CollisionFlags, CameraFollow, BackgroundType, GamePlayState, BanEntryType, Callback, Utils, Room, Replay, Query, Library, RoomConfig, Plugin, Renderer, Errors, Language, EventFactory, Impl } = API; Object.setPrototypeOf(this, Renderer.prototype); Renderer.call(this, { // Every renderer should have a unique name. name: "default", version: "1.9", author: "basro & abc", description: `This is a much more improved version of the default renderer currently used in Haxball with bug-fixes, aimbot and new features. Use +, - keys for zoom in-out. Disable followMode to zoom using mouse wheel.` }); // parameters are exported so that they can be edited outside this class. this.defineVariable({ name: "extrapolation", description: "The desired extrapolation value in milliseconds", type: VariableType.Integer, value: 0, range: { min: -1000, max: 10000, step: 5 } }); this.defineVariable({ // team_colors name: "showTeamColors", description: "Show team colors?", type: VariableType.Boolean, value: true }); this.defineVariable({ // show_avatars name: "showAvatars", description: "Show player avatars?", type: VariableType.Boolean, value: true }); this.defineVariable({ name: "showPlayerIds", description: "Show player ids?", type: VariableType.Boolean, value: false }); this.defineVariable({ name: "zoomCoeff", description: "Zoom Coefficient", type: VariableType.Number, value: 1.0, range: { min: 0, max: Infinity, step: 0.01 } }); this.defineVariable({ name: "wheelZoomCoeff", description: "Defines how fast you zoom in/out with mouse wheel", type: VariableType.Number, value: 1.2, range: { min: 1, max: 10, step: 0.01 } }); this.defineVariable({ // resolution_scale name: "resolutionScale", description: "Resolution Scale", type: VariableType.Number, value: 1, range: { min: 0, max: Infinity, step: 0.01 } }); this.defineVariable({ // show_indicators name: "showChatIndicators", description: "Show Chat Indicators?", type: VariableType.Boolean, value: true }); this.defineVariable({ name: "restrictCameraOrigin", description: "Restrict camera origin to view bounds?", type: VariableType.Boolean, value: true }); this.defineVariable({ name: "followMode", description: "Follow camera enabled?", type: VariableType.Boolean, value: true }); this.defineVariable({ name: "followPlayerId", description: "Id of the player that the camera will follow", type: VariableType.Integer, value: null }); this.defineVariable({ name: "drawBackground", description: "Draw Background?", type: VariableType.Boolean, value: true }); this.defineVariable({ name: "squarePlayers", description: "Draw Players as squares?", type: VariableType.Boolean, value: false }); this.defineVariable({ name: "currentPlayerDistinction", description: "Hide current player's name and draw halo around current player?", type: VariableType.Boolean, value: true }); this.defineVariable({ name: "showInvisibleSegments", description: "Show invisible segments?", type: VariableType.Boolean, value: false }); this.defineVariable({ name: "transparentDiscBugFix", description: "Hide transparent discs?", type: VariableType.Boolean, value: true }); this.defineVariable({ name: "generalLineWidth", description: "The line width of everything except discs and texts on screen.", type: VariableType.Number, value: 3, range: { min: 0, max: 100, step: 0.01 } }); this.defineVariable({ name: "discLineWidth", description: "The line width of discs.", type: VariableType.Number, value: 2, range: { min: 0, max: 100, step: 0.01 } }); this.defineVariable({ name: "textLineWidth", description: "The line width of texts.", type: VariableType.Number, value: 3, range: { min: 0, max: 100, step: 0.01 } }); this.defineVariable({ name: "lowLatency", description: "Desynchronized context for low-latency rendering.", type: VariableType.Boolean, value: true }); var thisRenderer = this, { Point, Team, TeamColors } = Impl.Core, roomLibrariesMap = null; var defaultTeamColors = [ new TeamColors(), new TeamColors(), new TeamColors() ]; defaultTeamColors[1].inner.push(15035990); defaultTeamColors[2].inner.push(5671397); // language-related stuff const LanguageData = { "GB": [ "Time is", "Up!", "Red is", "Victorious!", "Red", "Scores!", "Blue is", "Victorious!", "Blue", "Scores!", "Game", "Paused" ], "TR": [ "Süre", "Doldu!", "Kırmızı Takım", "Kazandı!", "Kırmızı Takım", "Gol Attı!", "Mavi Takım", "Kazandı!", "Mavi Takım", "Gol Attı!", "Oyun", "Durduruldu" ], "PT": [ "O tempo", "Acabou!", "O vermelho é", "Vitorioso!", "O vermelho", "Marca!", "O azul é", "Vitorioso!", "Pontuações", "Azuis!", "Jogo em", "Pausa" ], "ES": [ "¡El tiempo ha", "Terminado!", "¡El red ha", "Ganado!", "¡Punto para el", "Red!", "¡El azul ha", "Ganado!", "¡Punto para el", "Blue!", "Juego en", "Pausa" ] }; // start of basro's renderer logic function Animator(values){ // Ib this.values = values.slice(); // Yb } Animator.prototype = { eval: function(x){ var idx = this.values.length-1; if (x<=this.values[0]) return this.values[1]; if (x>=this.values[idx]) return this.values[idx-2]; var min = 0, max = (max/5)|0; do { var cur = (max+min)>>>1; if (x>this.values[5*cur]) min = cur+1; else max = cur-1; } while (min<=max); var idx2 = 5*max, idx3 = this.values[idx2]; var X = (x-idx3)/(this.values[idx2+5]-idx3), sqrX = X*X, cubeX = sqrX*X; return (2*cubeX-3*sqrX+1)*this.values[idx2+1]+(cubeX-2*sqrX+X)*this.values[idx2+2]+(-2*cubeX+3*sqrX)*this.values[idx2+3]+(cubeX-sqrX)*this.values[idx2+4]; } }; function CanvasText(lines, color){ // R var arr = []; for (var i=0;i<lines.length;i++) arr.push(this.createTextCanvas(lines[i], color)); this.canvasArray = arr; // We } CanvasText.alphaAnimator = new Animator([0, 0, 2, 1, 0, 0.35, 1, 0, 1, 0, 0.7, 1, 0, 0, 0, 1]); // jn CanvasText.coordAnimator = new Animator([0, -1, 3, 0, 0, 0.35, 0, 0, 0, 0, 0.65, 0, 0, 1, 3, 1]); // kn CanvasText.prototype = { calculateTime: function(){ // zo return 2.31+0.1155*(this.canvasArray.length-1); }, render: function (ctx, coeff){ // Kc var coeff1 = coeff / 2.31; ctx.imageSmoothingEnabled = true; for (var i=0;i<this.canvasArray.length;i++){ var canvas = this.canvasArray[i]; var coeff2 = coeff1-0.05*i, width = ((0!=(i&1)) ? -1 : 1)*180*CanvasText.coordAnimator.eval(coeff2); ctx.globalAlpha = CanvasText.alphaAnimator.eval(coeff2); ctx.drawImage(canvas, width-0.5*canvas.width, 35*(1-this.canvasArray.length)+70*i-0.5*canvas.height); ctx.globalAlpha = 1; } ctx.imageSmoothingEnabled = false; }, renderStatic: function(ctx){ // Tq ctx.imageSmoothingEnabled = true; for (var i=0;i<this.canvasArray.length;i++){ var canvas = this.canvasArray[i]; ctx.drawImage(canvas, -0.5*canvas.width, 35*(1-this.canvasArray.length)+70*i-0.5*canvas.height); } ctx.imageSmoothingEnabled = false; }, createTextCanvas: function(text, color){ // sp var canvas = window.document.createElement("canvas"); var ctx = canvas.getContext("2d", null); ctx.font = "900 70px Arial Black,Arial Bold,Gadget,sans-serif"; canvas.width = Math.ceil(ctx.measureText(text).width)+7; canvas.height = 90; ctx.font = "900 70px Arial Black,Arial Bold,Gadget,sans-serif"; ctx.textAlign = "left"; ctx.textBaseline = "middle"; ctx.fillStyle = "black"; ctx.fillText(text, 7, 52); ctx.fillStyle = Utils.numberToColor(color); ctx.fillText(text, 0, 45); return canvas; } }; function CanvasTextRenderer(){ // Sb const TextMap = LanguageData[Language.current?.abbr||"GB"]; this.time = 0; // xc this.textQueue = []; // ab this.timeUp = new CanvasText([TextMap[0], TextMap[1]], 16777215); // Ar // ["Time is", "Up!"] this.redVictory = new CanvasText([TextMap[2], TextMap[3]], 15035990); // Gq // ["Red is", "Victorious!"] this.redScore = new CanvasText([TextMap[4], TextMap[5]], 15035990); // Fq // ["Red", "Scores!"] this.blueVictory = new CanvasText([TextMap[6], TextMap[7]], 625603); // Cn // ["Blue is", "Victorious!"] this.blueScore = new CanvasText([TextMap[8], TextMap[9]], 625603); // Bn // ["Blue", "Scores!"] this.gamePause = new CanvasText([TextMap[10], TextMap[11]], 16777215); // eq // ["Game", "Paused"] } CanvasTextRenderer.prototype = { addText: function(textObj){ // Pa this.textQueue.push(textObj); }, reset: function(){ // Nn this.textQueue = []; this.time = 0; }, update: function(deltaTime){ // C if (this.textQueue.length==0) return; this.time += deltaTime; if (this.time<=this.textQueue[0].calculateTime()) return; this.time = 0; this.textQueue.shift(); }, render: function(a){ // Kc if (this.textQueue.length==0) return; this.textQueue[0].render(a, this.time); } }; function PlayerDecorator(){ // Ea this.chatIndicatorActive = false; // Xf this.name = ""; // w this.avatarNumber = 0; // uh this.avatarText = ""; // Jf this.teamColors = new TeamColors(); // kb var canvas = window.document.createElement("canvas"); canvas.width = 64; canvas.height = 64; this.ctx = canvas.getContext("2d", null); // rb this.pattern = this.ctx.createPattern(this.ctx.canvas, "no-repeat"); // Ij this.initialize(); } var /*PlayerDecorator.*/compareTeamColors = function(c1, c2){ if (c1.angle!=c2.angle || c1.text!=c2.text) return false; var a1 = c1.inner, a2 = c2.inner; if (a1==a2 || a1.length!=a2.length) return false; for (var i=0;i<a1.length;i++) if (a1[i]!=a2[i]) return false; return true; }; var /*PlayerDecorator.*/copyTeamColors = function(to, from){ // ao to.angle = from.angle; to.text = from.text; to.inner = from.inner?.slice(0); }; PlayerDecorator.prototype = { initialize: function(){ var canvas = window.document.createElement("canvas"); canvas.width = 160; canvas.height = 34; this.ctx2 = canvas.getContext("2d", null); }, repaintPlayerName: function(){ this.ctx2.resetTransform(); this.ctx2.clearRect(0, 0, 160, 34); this.ctx2.font = "26px sans-serif"; this.ctx2.fillStyle = "white"; if (this.ctx2.measureText(this.name).width>160){ this.ctx2.textAlign = "left"; this.ctx2.translate(2, 29); } else{ this.ctx2.textAlign = "center"; this.ctx2.translate(80, 29); } this.ctx2.fillText(this.name, 0, 0); }, drawToCanvas: function(ctx, x, y){ // so ctx.drawImage(this.ctx2.canvas, 0, 0, 160, 34, x-40, y-34, 80, 17); }, update: function(playerObj, roomState){ // C if (playerObj.disc) { var teamColors = thisRenderer.showTeamColors/*localStorageObj.xm.L()*/ ? roomState.teamColors[playerObj.team.id] : defaultTeamColors[playerObj.team.id]; // "team_colors" var avatarText = (playerObj.headlessAvatar!=null) ? playerObj.headlessAvatar : playerObj.avatar; var showAvatar = thisRenderer.showAvatars/*localStorageObj.lm.L()*/ && (avatarText!=null); // "show_avatars" if (!/*PlayerDecorator.*/compareTeamColors(this.teamColors, teamColors) || (!showAvatar && (playerObj.avatarNumber!=this.avatarNumber)) || (showAvatar && (this.avatarText!=avatarText))){ /*PlayerDecorator.*/copyTeamColors(this.teamColors, teamColors); if (showAvatar){ this.avatarText = avatarText; this.avatarNumber = -1; } else{ this.avatarText = "" + playerObj.avatarNumber; this.avatarNumber = playerObj.avatarNumber; } this.createInnerFillPattern(/*this.avatarText*/); } } this.strokeStyle = (roomState.gameState.pauseGameTickCounter>0 || !playerObj.isKicking) ? "black" : ((playerObj.isKicking && playerObj.kickRateMinTickCounter<=0 && playerObj.kickRateMaxTickCounter>=0) ? "white" : "black"); var name = thisRenderer.showPlayerIds?("["+playerObj.id+"] "+playerObj.name):playerObj.name; if (name!=this.name){ this.name = name; this.repaintPlayerName(); } }, createInnerFillPattern: function(/*avatarText*/){ var colorArray = this.teamColors.inner; if (!colorArray || colorArray.length==0) return; // fill the colors this.ctx.save(); this.ctx.translate(32, 32); // set midpoint of the canvas as origin this.ctx.rotate((3.141592653589793*this.teamColors.angle)/128); // team colors rotation by provided angle var stepWidth = 64/colorArray.length, x=-32; // here, 64 is the width of the canvas for (var i=0;i<colorArray.length;i++){ this.ctx.fillStyle = Utils.numberToColor(colorArray[i]); this.ctx.fillRect(x, -32, stepWidth+4, 64); x += stepWidth; } this.ctx.restore(); // origin and rotation returns back to normal // draw the avatar text this.ctx.fillStyle = Utils.numberToColor(this.teamColors.text); this.ctx.textAlign = "center"; this.ctx.textBaseline = "alphabetic"; this.ctx.font = "900 34px 'Arial Black','Arial Bold',Gadget,sans-serif"; this.ctx.fillText(this.avatarText, 32, 44); // convert this drawing into a pattern this.pattern = this.ctx.createPattern(this.ctx.canvas, "no-repeat"); } }; function HaxballRenderer(){ // N this.actualZoomCoeff = thisRenderer.zoomCoeff; this.lastRenderTime = window.performance.now(); // $c this.decoratorsByObject = new Map(); // Jg this.decoratorsById = new Map(); // dd this.origin = new Point(0, 0); // Ya this.gamePaused = false; // Dk this.textRenderer = new CanvasTextRenderer(); // td this.canvas = params.canvas; // sa this.canvas.mozOpaque = true; this.canvas.style.filter = ""; this.ctx = this.canvas.getContext("2d", { alpha: false, desynchronized: thisRenderer.lowLatency }); this.grassPattern = this.ctx.createPattern(/*n.Ko*/params.images?.grass, null); // Lo this.concretePattern = this.ctx.createPattern(/*n.Vn*/params.images?.concrete, null); // Wn this.concrete2Pattern = this.ctx.createPattern(/*n.Tn*/params.images?.concrete2, null); // Un } HaxballRenderer.setSmoothingEnabled = function(ctx, enabled){ // Gi ctx.imageSmoothingEnabled = enabled; ctx.mozImageSmoothingEnabled = enabled; }; HaxballRenderer.prototype = { updateChatIndicator: function(id, value){ // Po var decorator = this.decoratorsById.get(id); if (decorator) decorator.chatIndicatorActive = value; }, resizeCanvas: function(){ // Pr if (!this.canvas.parentElement) return; var coeff = window.devicePixelRatio*thisRenderer.resolutionScale, rect = this.canvas.getBoundingClientRect(); var w = Math.round(coeff*rect.width), h = Math.round(coeff*rect.height); if (this.canvas.width!=w || this.canvas.height!=h) { this.canvas.width = w; this.canvas.height = h; } }, transformPixelCoordToMapCoord: function(x, y){ return { x: (x-this.canvas.width/2)/this.actualZoomCoeff+this.origin.x, y: (y-this.canvas.height/2)/this.actualZoomCoeff+this.origin.y }; }, transformMapCoordToPixelCoord: function(x, y){ return { x: this.actualZoomCoeff*(x-this.origin.x)+this.canvas.width/2, y: this.actualZoomCoeff*(y-this.origin.y)+this.canvas.height/2 }; }, transformPixelDistanceToMapDistance: function(dist){ return dist/this.actualZoomCoeff; }, transformMapDistanceToPixelDistance: function(dist){ return dist*this.actualZoomCoeff; }, render: function(roomState){ // Kc var time = window.performance.now(), deltaTime = (time-this.lastRenderTime)/1000; this.spf = deltaTime; this.lastRenderTime = time; this.decoratorsByObject.clear(); this.resizeCanvas(); HaxballRenderer.setSmoothingEnabled(this.ctx, true); this.ctx.resetTransform(); if (!roomState.gameState) return; var gameState = roomState.gameState, mapObjects = gameState.physicsState, followPlayer = roomState.getPlayer(thisRenderer.followPlayerId), followDisc = followPlayer?.disc; var zoomCoeff = thisRenderer.zoomCoeff*window.devicePixelRatio*thisRenderer.resolutionScale; var maxViewWidth = gameState.stadium.maxViewWidth, viewWidth = this.canvas.width/zoomCoeff; if (maxViewWidth>0 && maxViewWidth<viewWidth){ viewWidth = maxViewWidth; zoomCoeff = this.canvas.width/maxViewWidth; } var viewHeight = this.canvas.height/zoomCoeff; this.updateCameraOrigin(gameState, followDisc, viewWidth, viewHeight, deltaTime); var playerObjects = roomState.players, playerObject, i; for (i=0;i<playerObjects.length;i++){ playerObject = playerObjects[i]; if (!playerObject.disc) continue; var playerDecorator = this.decoratorsById.get(playerObject.id); if (!playerDecorator){ playerDecorator = new PlayerDecorator(); this.decoratorsById.set(playerObject.id, playerDecorator); } playerDecorator.update(playerObject, roomState); this.decoratorsByObject.set(playerObject.disc, playerDecorator); } this.actualZoomCoeff = zoomCoeff; this.ctx.translate(this.canvas.width/2, this.canvas.height/2); this.ctx.scale(zoomCoeff, zoomCoeff); this.ctx.translate(-this.origin.x, -this.origin.y); this.ctx.lineWidth = thisRenderer.generalLineWidth; this.drawBackground(gameState.stadium); this.drawAllSegments(gameState.stadium); var discs = mapObjects.discs, joints = mapObjects.joints; for (i=0;i<joints.length;i++) this.drawJoint(joints[i], discs); this.indicateAllLocations(roomState, viewWidth, viewHeight); this.drawPlayerDecoratorsAndChatIndicators(roomState, followPlayer); if (thisRenderer.currentPlayerDistinction && followDisc) this.drawHalo(followDisc.pos); this.ctx.lineWidth = thisRenderer.discLineWidth; for (i=0;i<playerObjects.length;i++){ playerObject = playerObjects[i]; var playerDisc = playerObject.disc; if (!playerDisc) continue; this.drawDisc(playerDisc, this.decoratorsById.get(playerObject.id)); } for (i=0;i<discs.length;i++){ var disc = discs[i]; if (this.decoratorsByObject.get(disc)) continue; this.drawDisc(disc, null); } roomLibrariesMap?.aimbot?.calculateAndDraw(followDisc, gameState, this.ctx); this.ctx.lineWidth = thisRenderer.textLineWidth; this.ctx.resetTransform(); this.ctx.translate(this.canvas.width/2, this.canvas.height/2); this.updateGamePaused(gameState); if (gameState.pauseGameTickCounter<=0){ this.textRenderer.update(deltaTime); this.textRenderer.render(this.ctx); } this.decoratorsByObject.clear(); this.cleanUpDecoratorsById(roomState); }, cleanUpDecoratorsById: function(roomState){ // Kq var players = roomState.players; var playerIds = new Set(); for (var i=0;i<players.length;i++) playerIds.add(players[i].id); var decoratorPlayerIds = this.decoratorsById.keys(); for (var it=decoratorPlayerIds.next();!it.done;it=decoratorPlayerIds.next()){ var playerId = it.value; if (!playerIds.has(playerId)) this.decoratorsById.delete(playerId); } }, updateCameraOrigin: function(gameState, followDisc, viewWidth, viewHeight, deltaTime){ var stadium = gameState.stadium; if (thisRenderer.followMode){ var x, y, pos; if (followDisc && stadium.cameraFollow==1){ pos = followDisc.pos; // player's position x = pos.x; y = pos.y; } else{ pos = gameState.physicsState.discs[0].pos; // ball's position x = pos.x; y = pos.y; if (followDisc){ var playerPos = followDisc.pos; x = 0.5*(x+playerPos.x); y = 0.5*(y+playerPos.y); var w = 0.5*viewWidth; var h = 0.5*viewHeight; var minX = playerPos.x-w+50; var minY = playerPos.y-h+50; var maxX = playerPos.x+w-50; var maxY = playerPos.y+h-50; x = (x>maxX) ? maxX : ((x<minX) ? minX : x); y = (y>maxY) ? maxY : ((y<minY) ? minY : y); } } var t = 60*deltaTime; if (t>1) t = 1; t *= 0.04; var origin = this.origin; var x0 = origin.x; var y0 = origin.y; origin.x = x0+(x-x0)*t; origin.y = y0+(y-y0)*t; } if (thisRenderer.restrictCameraOrigin){ if (viewWidth>2*stadium.width) this.origin.x = 0; else if (this.origin.x+0.5*viewWidth>stadium.width) this.origin.x = stadium.width-0.5*viewWidth; else if (this.origin.x-0.5*viewWidth<-stadium.width) this.origin.x = -stadium.width+0.5*viewWidth; if (viewHeight>2*stadium.height) this.origin.y = 0; else if (this.origin.y+0.5*viewHeight>stadium.height) this.origin.y = stadium.height-0.5*viewHeight; else if (this.origin.y-0.5*viewHeight<-stadium.height) this.origin.y = -stadium.height+0.5*viewHeight; } // fix all possible camera bugs if (this.origin.x*0 != 0) this.origin.x = 0; if (this.origin.y*0 != 0) this.origin.y = 0; }, drawHalo: function(pos){ // Pq this.ctx.beginPath(); this.ctx.strokeStyle = "white"; this.ctx.globalAlpha = 0.3; if (thisRenderer.squarePlayers) this.ctx.rect(pos.x-25, pos.y-25, 50, 50); else this.ctx.arc(pos.x, pos.y, 25, 0, 2*Math.PI, false); this.ctx.stroke(); this.ctx.globalAlpha = 1; }, updateGamePaused: function(gameState){ // Oq var paused = (gameState.pauseGameTickCounter>0); this.setGamePaused(paused); if (!paused) return; if (gameState.pauseGameTickCounter!=120){ var width = (gameState.pauseGameTickCounter/120)*200; this.ctx.fillStyle = "white"; this.ctx.fillRect(-0.5*width, 100, width, 20); } this.textRenderer.gamePause.renderStatic(this.ctx); }, setGamePaused: function(gamePaused){ // lr if (gamePaused==this.gamePaused) return; this.canvas.style.filter = (gamePaused ? "grayscale(70%)" : ""); this.gamePaused = gamePaused; }, drawRoundedRect: function(ctx, x, y, width, height, cornerRadius){ var x2 = x+width; var y2 = y+height; ctx.beginPath(); ctx.moveTo(x2-cornerRadius, y); ctx.arcTo(x2, y, x2, y+cornerRadius, cornerRadius); ctx.lineTo(x2, y2-cornerRadius); ctx.arcTo(x2, y2, x2-cornerRadius, y2, cornerRadius); ctx.lineTo(x+cornerRadius, y2); ctx.arcTo(x, y2, x, y2-cornerRadius, cornerRadius); ctx.lineTo(x, y+cornerRadius); ctx.arcTo(x, y, x+cornerRadius, y, cornerRadius); ctx.closePath(); }, drawBackground: function(stadium){ // Sq HaxballRenderer.setSmoothingEnabled(this.ctx, false); var width = stadium.bgWidth, height = stadium.bgHeight; if (stadium.bgType==1) { this.ctx.save(); this.ctx.resetTransform(); this.ctx.fillStyle = Utils.numberToColor(stadium.bgColor); this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.restore(); if (thisRenderer.drawBackground){ this.ctx.strokeStyle = "#C7E6BD"; this.ctx.fillStyle = this.grassPattern; this.drawRoundedRect(this.ctx, -width, -height, 2*width, 2*height, stadium.bgCornerRadius); this.ctx.save(); this.ctx.scale(2, 2); this.ctx.fill(); this.ctx.restore(); this.ctx.moveTo(0, -height); this.ctx.lineTo(0, height); this.ctx.stroke(); this.ctx.beginPath(); this.ctx.arc(0, 0, stadium.bgKickOffRadius, 0, 2*Math.PI); this.ctx.stroke(); } } else if (stadium.bgType==2){ this.ctx.strokeStyle = "#E9CC6E"; this.ctx.save(); this.ctx.beginPath(); this.ctx.rect(this.origin.x-10000, this.origin.y-10000, 20000, 20000); this.ctx.scale(2, 2); this.ctx.fillStyle = this.concrete2Pattern; this.ctx.fill(); this.ctx.restore(); if (thisRenderer.drawBackground){ this.ctx.save(); this.drawRoundedRect(this.ctx, -width, -height, 2*width, 2*height, stadium.bgCornerRadius); this.ctx.scale(2, 2); this.ctx.fillStyle = this.concretePattern; this.ctx.fill(); this.ctx.restore(); this.ctx.stroke(); this.ctx.beginPath(); this.ctx.moveTo(0, -height); this.ctx.setLineDash([15, 15]); this.ctx.lineTo(0, height); this.ctx.stroke(); this.ctx.setLineDash([]); var goalLine = stadium.bgGoalLine, delta = width-goalLine; if (goalLine<stadium.bgCornerRadius) delta = 0; var that = this; var drawKickOff = function(color, x, ccw){ that.ctx.beginPath(); that.ctx.strokeStyle = color; that.ctx.arc(0, 0, stadium.bgKickOffRadius, -1.5707963267948966, 1.5707963267948966, ccw); if (x!=0){ that.ctx.moveTo(x, -height); that.ctx.lineTo(x, height); } that.ctx.stroke(); }; drawKickOff("#85ACF3", delta, false); drawKickOff("#E18977", -delta, true); } } else { this.ctx.save(); this.ctx.resetTransform(); this.ctx.fillStyle = Utils.numberToColor(stadium.bgColor); this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.restore(); } HaxballRenderer.setSmoothingEnabled(this.ctx, true); }, drawPlayerDecoratorsAndChatIndicators: function(roomState, followPlayer){ // Nq var showIndicators = thisRenderer.showChatIndicators/*localStorageObj.Ak.L()*/, players = roomState.players; // "show_indicators" for (var i=0;i<players.length;i++){ var player = players[i]; var disc = player.disc; if (!disc) continue; var pos = disc.pos, decorator = this.decoratorsById.get(player.id); if (showIndicators && decorator.chatIndicatorActive && /*n.Dm*/params.images?.typing) this.ctx.drawImage(params.images.typing, pos.x-0.5*params.images.typing.width, pos.y-35); if (!thisRenderer.currentPlayerDistinction || player!=followPlayer) decorator.drawToCanvas(this.ctx, pos.x, pos.y+50); } }, drawDisc: function(disc, playerDecorator){ // Ll var transparent; this.ctx.beginPath(); if (playerDecorator){ this.ctx.fillStyle = playerDecorator.pattern; this.ctx.strokeStyle = playerDecorator.strokeStyle; } else{ transparent = (disc.color|0)==-1; if (thisRenderer.transparentDiscBugFix || !transparent) this.ctx.fillStyle = Utils.numberToColor(disc.color); this.ctx.strokeStyle = "black"; } this.ctx.beginPath(); if (playerDecorator){ if (thisRenderer.squarePlayers) this.ctx.rect(disc.pos.x-disc.radius, disc.pos.y-disc.radius, 2*disc.radius, 2*disc.radius); else this.ctx.arc(disc.pos.x, disc.pos.y, disc.radius, 0, 2*Math.PI, false); this.ctx.save(); var c = disc.radius/32; this.ctx.translate(disc.pos.x, disc.pos.y); this.ctx.scale(c, c); this.ctx.translate(-32, -32); this.ctx.fill(); this.ctx.restore(); } else{ this.ctx.arc(disc.pos.x, disc.pos.y, disc.radius, 0, 2*Math.PI, false); if (!thisRenderer.transparentDiscBugFix || !transparent) this.ctx.fill(); } this.ctx.stroke(); }, drawAllSegments: function(stadium){ // Rq if (!stadium) return; var segments = stadium.segments; for (var i=0;i<segments.length;i++) this.drawSegment(segments[i]); }, drawJoint: function(joint, discs){ // Mq if (!thisRenderer.showInvisibleJoints && joint.color<0) return; this.ctx.beginPath(); this.ctx.strokeStyle = joint.color<0 ? "#006060" : Utils.numberToColor(joint.color); var disc1 = discs[joint.d0], disc2 = discs[joint.d1]; if (!disc1 || !disc2) return; var pos1 = disc1.pos; var pos2 = disc2.pos; this.ctx.moveTo(pos1.x, pos1.y); this.ctx.lineTo(pos2.x, pos2.y); this.ctx.stroke(); }, drawSegment: function(segment){ // Qq if (!thisRenderer.showInvisibleSegments && !segment.vis) return; this.ctx.beginPath(); this.ctx.strokeStyle = Utils.numberToColor(segment.color); var pos1 = segment.v0.pos, pos2 = segment.v1.pos; if (0*segment.curveF!=0){ // line this.ctx.moveTo(pos1.x, pos1.y); this.ctx.lineTo(pos2.x, pos2.y); } else{ // arc var center = segment.arcCenter, deltaX = pos1.x-center.x, deltaY = pos1.y-center.y; this.ctx.arc(center.x, center.y, Math.sqrt(deltaX*deltaX+deltaY*deltaY), Math.atan2(deltaY, deltaX), Math.atan2(pos2.y-center.y, pos2.x-center.x)); } this.ctx.stroke(); }, indicateAllLocations: function(roomState, viewWidth, viewHeight){ // Lq var gameState = roomState.gameState; if (!gameState) return; var ballDisc = gameState.physicsState.discs[0]; this.indicateLocation(ballDisc.pos, ballDisc.color, viewWidth, viewHeight); var players = roomState.players; for (var i=0;i<players.length;i++){ var player = players[i], playerDisc = player.disc; if (!playerDisc) continue; this.indicateLocation(playerDisc.pos, player.team.color, viewWidth, viewHeight); } }, indicateLocation: function(pos, color, viewWidth, viewHeight){ // nk viewWidth = 0.5*viewWidth-25; viewHeight = 0.5*viewHeight-25; var origin = this.origin; var deltaX = pos.x-origin.x; var deltaY = pos.y-origin.y; var x = origin.x+((deltaX>viewWidth) ? viewWidth : ((deltaX<-viewWidth) ? -viewWidth : deltaX)); var y = origin.y+((deltaY>viewHeight) ? viewHeight : ((deltaY<-viewHeight) ? -viewHeight : deltaY)); deltaX = pos.x-x; deltaY = pos.y-y; if (deltaX*deltaX+deltaY*deltaY<=900) return; // draw location indicator's shadow this.ctx.fillStyle = "rgba(0,0,0,0.5)"; this.drawLocationIndicator(x+2, y+2, Math.atan2(deltaY, deltaX)); // draw location indicator this.ctx.fillStyle = Utils.numberToColor(color); this.drawLocationIndicator(x-2, y-2, Math.atan2(deltaY, deltaX)); }, drawLocationIndicator: function(x, y, angle){ // pk this.ctx.save(); this.ctx.translate(x, y); this.ctx.rotate(angle); this.ctx.beginPath(); this.ctx.moveTo(15, 0); this.ctx.lineTo(0, 7); this.ctx.lineTo(0, -7); this.ctx.closePath(); this.ctx.fill(); this.ctx.restore(); }, resetChatIndicators: function(){ // Xq // This function is only used while viewing replays, so it might be deleted. var a = this.decoratorsById.values(), b = a.next(); while (!b.done){ b.value.chatIndicatorActive = false; b = a.next(); } } }; // end of basro's renderer logic var rendererObj = null; // Eb this.initialize = function(){ thisRenderer.followPlayerId = thisRenderer.room.currentPlayerId; roomLibrariesMap = thisRenderer.room.librariesMap; rendererObj = new HaxballRenderer(); }; this.finalize = function(){ roomLibrariesMap = null; rendererObj = null; }; this.render = function(){ // render logic here. called inside requestAnimationFrame callback var extrapolatedRoomState = thisRenderer.room.extrapolate(thisRenderer.extrapolation, true); if (!params.paintGame || !extrapolatedRoomState.gameState) return; rendererObj.render(extrapolatedRoomState); params.onRequestAnimationFrame?.(extrapolatedRoomState); }; this.fps = function(){ return 1/rendererObj.spf; }; // you can keep track of changes using these callbacks, and apply them in your render logic: this.onPlayerChatIndicatorChange = function(id, value, customData){ // wl (a, b) rendererObj.updateChatIndicator(id, value); }; this.onTeamGoal = function(teamId, goalId, goal, ballDiscId, ballDisc, customData){ // Ni (a) var tr = rendererObj.textRenderer; // "Red Scores!", "Blue Scores!" tr.addText((teamId==Team.red.id) ? tr.redScore : tr.blueScore); }; this.onGameStart = function(byId, customData){ // Ki (a) rendererObj.textRenderer.reset(); }; this.onGameEnd = function(winningTeamId, customData){ // Oi (a) var tr = rendererObj.textRenderer; // "Red is Victorious!", "Blue is Victorious!" tr.addText((winningTeamId==Team.red.id) ? tr.redVictory : tr.blueVictory); }; this.onTimeIsUp = function(customData){ // Pi () var tr = rendererObj.textRenderer; // "Time is Up!" tr.addText(tr.timeUp); }; this.onLanguageChange = function(abbr, customData){ rendererObj.textRenderer = new CanvasTextRenderer(); // td }; this.onKeyDown = function(e){ switch(e.keyCode){ case 107:{ // Numpad '+' key thisRenderer.zoomCoeff += 0.1; break; } case 109:{ // Numpad '-' key thisRenderer.zoomCoeff -= 0.1; if (thisRenderer.zoomCoeff<=0) thisRenderer.zoomCoeff = 0.01; break; } } }; this.transformPixelCoordToMapCoord = function(x, y){ return rendererObj.transformPixelCoordToMapCoord(x, y); }; this.transformMapCoordToPixelCoord = function(x, y){ return rendererObj.transformMapCoordToPixelCoord(x, y); }; this.transformPixelDistanceToMapDistance = function(dist){ return rendererObj.transformPixelDistanceToMapDistance(dist); }; this.transformMapDistanceToPixelDistance = function(dist){ return rendererObj.transformMapDistanceToPixelDistance(dist); }; this.getOrigin = function(){ return rendererObj.origin; }; this.getActualZoomCoefficient = function(){ return rendererObj.actualZoomCoeff; }; this.setOrigin = function(origin){ rendererObj.origin.x = origin.x; rendererObj.origin.y = origin.y; }; /* w = this.canvas.width, h = this.canvas.height, z = this.actualZoomCoeff, zc = zoomCoeff, Ox = this.origin.x, Oy = this.origin.y, p_to_m(x, y): [(x-w/2)/z+Ox, (y-h/2)/z+Oy], m_to_p(x, y): [z*(x-Ox)+w/2, z*(y-Oy)+h/2], e_x_p = pixelCoordX, e_y_p = pixelCoordY, Origin Calculation: ------------------- old map coords of event point: [(e_x_p-w/2)/z+Ox, (e_y_p-h/2)/z+Oy] new map coords of event point: [(e_x_p-w/2)/z_new+Ox_new, (e_y_p-h/2)/z_new+Oy_new] we want them to be equal, so; (e_x_p-w/2)/z_new+Ox_new = (e_x_p-w/2)/z+Ox (e_y_p-h/2)/z_new+Oy_new = (e_y_p-h/2)/z+Oy Ox_new = (e_x_p-w/2)*(1/z-1/z_new)+Ox Oy_new = (e_y_p-h/2)*(1/z-1/z_new)+Oy */ this.zoomIn = function(pixelCoordX, pixelCoordY, zoomCoeff){ var { origin, canvas } = rendererObj, k = (1-1/zoomCoeff)/thisRenderer.zoomCoeff; origin.x += k*(pixelCoordX-canvas.width/2); origin.y += k*(pixelCoordY-canvas.height/2); thisRenderer.zoomCoeff *= zoomCoeff; }; this.zoomOut = function(pixelCoordX, pixelCoordY, zoomCoeff){ var { origin, canvas } = rendererObj, k = (1-zoomCoeff)/thisRenderer.zoomCoeff; origin.x += k*(pixelCoordX-canvas.width/2); origin.y += k*(pixelCoordY-canvas.height/2); thisRenderer.zoomCoeff /= zoomCoeff; }; this.onWheel = function(event){ if (event.deltaY<0) thisRenderer.zoomIn(event.offsetX, event.offsetY, thisRenderer.wheelZoomCoeff); else thisRenderer.zoomOut(event.offsetX, event.offsetY, thisRenderer.wheelZoomCoeff); }; // snapshot support this.takeSnapshot = function(){ var { extrapolation, showTeamColors, showAvatars, showPlayerIds, zoomCoeff, wheelZoomCoeff, resolutionScale, showChatIndicators, restrictCameraOrigin, followMode, followPlayerId, drawBackground, squarePlayers, currentPlayerDistinction, showInvisibleSegments, transparentDiscBugFix, generalLineWidth, discLineWidth, textLineWidth, lowLatency } = thisRenderer; return { origin: { x: rendererObj.origin.x, y: rendererObj.origin.y }, actualZoomCoeff: rendererObj.actualZoomCoeff, lastRenderTime: rendererObj.lastRenderTime, gamePaused: rendererObj.gamePaused, extrapolation, showTeamColors, showAvatars, showPlayerIds, zoomCoeff, wheelZoomCoeff, resolutionScale, showChatIndicators, restrictCameraOrigin, followMode, followPlayerId, drawBackground, squarePlayers, currentPlayerDistinction, showInvisibleSegments, transparentDiscBugFix, generalLineWidth, discLineWidth, textLineWidth, lowLatency }; }; this.useSnapshot = function(snapshot){ var { extrapolation, showTeamColors, showAvatars, showPlayerIds, zoomCoeff, wheelZoomCoeff, resolutionScale, showChatIndicators, restrictCameraOrigin, followMode, followPlayerId, drawBackground, squarePlayers, currentPlayerDistinction, showInvisibleSegments, transparentDiscBugFix, generalLineWidth, discLineWidth, textLineWidth, lowLatency } = snapshot; Object.assign(thisRenderer, { extrapolation, showTeamColors, showAvatars, showPlayerIds, zoomCoeff, wheelZoomCoeff, resolutionScale, showChatIndicators, restrictCameraOrigin, followMode, followPlayerId, drawBackground, squarePlayers, currentPlayerDistinction, showInvisibleSegments, transparentDiscBugFix, generalLineWidth, discLineWidth, textLineWidth, lowLatency }); thisRenderer.setOrigin(snapshot.origin); rendererObj.actualZoomCoeff = snapshot.actualZoomCoeff; rendererObj.lastRenderTime = snapshot.lastRenderTime; rendererObj.gamePaused = snapshot.gamePaused; }; };