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,395 lines (1,332 loc) • 51.5 kB
JavaScript
module.exports = function(API){
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, Plugin.prototype);
Plugin.call(this, "CMD_realSoccer", true, { // "CMD_realSoccer" is plugin's name, "true" means "activated just after initialization". Every plugin should have a unique name.
version: "0.3",
author: "abc",
description: "This plugin sets up a real soccer game.",
allowFlags: AllowFlags.CreateRoom // We allow this plugin to be activated on CreateRoom only.
});
this.defineVariable({
name: "timeLimit",
description: "Time limit of the game (in seconds)",
type: VariableType.Integer,
value: 180,
range: {
min: 0,
max: Infinity,
step: 1
}
});
this.defineVariable({
name: "checkThrowIns",
description: "Whether to detect throw-ins or not",
type: VariableType.Boolean,
value: true
});
this.defineVariable({
name: "checkOuts",
description: "Whether to detect outs or not (If disabled, corners also do not work)",
type: VariableType.Boolean,
value: true
});
this.defineVariable({
name: "checkCorners",
description: "Whether to detect corners or not",
type: VariableType.Boolean,
value: true
});
this.defineVariable({
name: "checkOffsides",
description: "Whether to detect offsides or not",
type: VariableType.Boolean,
value: true
});
this.defineVariable({
name: "showDirectionIndicator",
description: "Whether to show ball's direction on throw-ins/outs/corners",
type: VariableType.Boolean,
value: true
});
this.defineVariable({
name: "directionIndicatorThreshold",
description: "Maximum ball distance to a player to show the direction indicator",
type: VariableType.Number,
value: 45,
range: {
min: 0,
max: 1000,
step: 0.1
}
});
this.defineVariable({
name: "directionIndicatorLengthCoeff",
description: "Coefficient about the length of the direction indicator",
type: VariableType.Number,
value: 50,
range: {
min: 0,
max: 1000,
step: 0.01
}
});
this.defineVariable({
name: "speedCoeff",
description: "Coefficient for the speed of ball on throw-ins/outs/corners/freekicks",
type: VariableType.Number,
value: 0.0007,
range: {
min: 0,
max: 0.005,
step: 0.0001
}
});
this.defineVariable({
name: "announceTime",
description: "Announces the remaining time",
type: VariableType.Boolean,
value: true
});
this.defineVariable({
name: "penaltiesEnabled",
description: "Whether to shoot penalties after the game ends in a draw",
type: VariableType.Boolean,
value: true
});
this.defineVariable({
name: "penaltiesSaveTimeout",
description: "Maximum waiting time before a save is recorded, in miliseconds",
type: VariableType.Integer,
value: 2000,
range: {
min: 0,
max: 20000,
step: 50
}
});
this.defineVariable({
name: "penaltiesTurnCount",
description: "Number of penalties to shoot before tie breaks start",
type: VariableType.Integer,
value: 5,
range: {
min: 1,
max: 100,
step: 1
}
});
this.defineVariable({
name: "playerArrangementEnabled",
description: "Whether to arrange the players each time before starting the actual game",
type: VariableType.Boolean,
value: true
});
this.defineVariable({
name: "waitForFullFormation",
description: "Whether to wait until there is no empty spot in both team formations",
type: VariableType.Boolean,
value: false
});
this.defineVariable({
name: "redFormation",
description: "Formation of the red team",
type: VariableType.String,
value: "4-3-3",
range: {
min: 1,
max: 20
}
});
this.defineVariable({
name: "blueFormation",
description: "Formation of the blue team",
type: VariableType.String,
value: "4-1-3-2",
range: {
min: 1,
max: 20
}
});
this.defineVariable({
name: "reset",
description: "Resets the game",
type: VariableType.Void,
value: completeReset
});
this.defineVariable({
name: "width",
description: "Stadium width",
type: VariableType.Number,
value: 1000,//1600,
range: {
min: 500,
max: 5000,
step: 10
}
});
this.defineVariable({
name: "height",
description: "Stadium height",
type: VariableType.Number,
value: 670,//850,
range: {
min: 300,
max: 5000,
step: 10
}
});
this.defineVariable({
name: "ballRadius",
description: "Ball's radius",
type: VariableType.Number,
value: 7,//8,
range: {
min: 0,
max: 10000,
step: 0.1
}
});
this.defineVariable({
name: "kickOffRadiusCoeff",
description: "Coefficient for kick-off radius",
type: VariableType.Number,
value: 0.22,
range: {
min: 0,
max: 0.5,
step: 0.000001
}
});
this.defineVariable({
name: "goalSizeCoeff",
description: "Coefficient for the size of goals",
type: VariableType.Number,
value: 0.16,
range: {
min: 0.01,
max: 0.5,
step: 0.0001
}
});
this.defineVariable({
name: "kickStrengthCoeff",
description: "Coefficient for the kicking strength of players",
type: VariableType.Number,
value: 0.0009,
range: {
min: 0,
max: 0.005,
step: 0.0001
}
});
var stadiumWidth, stadiumHeight, ballRadius;
var throwInSpeed, cornerSpeed, outSpeed, freeKickSpeed, penaltySpeed;
var throwInObstacleRadius, cornerObstacleRadius, outObstacleRadius, freeKickObstacleRadius;
var blockChanges, goalY;
var permissionCtx, permissionIds, lastTouchedPlayers = null;
var throwInType = null, throwInTeam = null, state = 0;
var pMap, pStats, tStats;
var playerArrangements, formations, formationsPlayerCoords, playerArrangementRedCount, playerArrangementBlueCount;
var penaltiesTurn, penaltyShotCount, penaltyShot, penaltySaveTimeout, cps;
var currentInGameTicks, targetInGameTicks;
var that = this;
function reset(nextTick=true, ltp=true){
function f(){
that.room.setDiscProperties(5, {
"x": NaN,
"y": NaN
});
that.room.setDiscProperties(6, {
"x": NaN,
"y": NaN
});
that.room.setDiscProperties(7, {
"x": NaN,
"y": NaN
});
throwInTeam = null;
throwInType = null;
if (ltp)
lastTouchedPlayers = [];
}
if (nextTick)
Utils.runAfterGameTick(f);
else
f();
}
function createPlayerStats(){
return {
goals: 0,
assists: 0,
ownGoals: 0,
passes: 0,
accuratePasses: 0
};
}
function createTeamStats(){
return {
throwIns: 0,
corners: 0,
outs: 0,
offsides: 0,
freeKicks: 0,
passes: 0,
accuratePasses: 0,
ballTicks: 0, // takımın topa yüzde kaç oranla sahip olduğu
ballInHalfTicks: 0 // topun yüzde kaç oranla hangi yarısahada bulunduğu
};
}
function percentage(a,b){
return ((b<=0)?0:(100*a/b)).toFixed(2)+"%";
}
function format(x, n){
var y = ""+x;
var nn = n*2.8-y.length*0.9;
var k = (nn-y.length)/2;
for (var i=0;i<k;i++)
y="·"+y;
while(y.length<nn)
y=y+"·";
return y;
}
function showPlayerStatistics(playerId){
var p = pStats.get(playerId);
that.room.librariesMap.commands?.announceInfo("Goals: "+p.goals+" OwnGoals: "+p.ownGoals+" Assists: "+p.goals+" Pass Accuracy: "+p.accuratePasses+"/"+p.passes+" ("+percentage(p.accuratePasses, p.passes)+")", playerId);
}
function showAllTeamStatistics(){
var t1=tStats[1], t2=tStats[2];
that.room.librariesMap.commands?.announceInfo("┌─────────────────────────────────────────────────────────────────────────┐");
that.room.librariesMap.commands?.announceInfo("│"+format("Team Statistics", 73)+"│");
that.room.librariesMap.commands?.announceInfo("├─────────────────┬───────────────────────────┬───────────────────────────┤");
that.room.librariesMap.commands?.announceInfo("│"+format("", 17)+"│"+format("Red", 27)+"│"+format("Blue", 27)+"│");
that.room.librariesMap.commands?.announceInfo("├─────────────────┼───────────────────────────┼───────────────────────────┤");
that.room.librariesMap.commands?.announceInfo("│"+format("ThrowIns", 17)+"│"+format(t1.throwIns, 27)+"│"+format(t2.throwIns, 27)+"│");
that.room.librariesMap.commands?.announceInfo("│"+format("Corners", 17)+"│"+format(t1.corners, 27)+"│"+format(t2.corners, 27)+"│");
that.room.librariesMap.commands?.announceInfo("│"+format("Outs", 17)+"│"+format(t1.outs, 27)+"│"+format(t2.outs, 27)+"│");
that.room.librariesMap.commands?.announceInfo("│"+format("Offsides", 17)+"│"+format(t1.offsides, 27)+"│"+format(t2.offsides, 27)+"│");
that.room.librariesMap.commands?.announceInfo("│"+format("FreeKicks", 17)+"│"+format(t1.freeKicks, 27)+"│"+format(t2.freeKicks, 27)+"│");
that.room.librariesMap.commands?.announceInfo("│"+format("PassAccuracy", 17)+"│"+format(t1.accuratePasses+"/"+t1.passes+" ("+percentage(t1.accuratePasses, t1.passes)+")", 27)+"│"+format(t2.accuratePasses+"/"+t2.passes+" ("+percentage(t2.accuratePasses, t2.passes)+")", 27)+"│");
that.room.librariesMap.commands?.announceInfo("│"+format("Ball Possession", 17)+"│"+format(percentage(t1.ballTicks, currentInGameTicks), 27)+"│"+format(percentage(t2.ballTicks, currentInGameTicks), 27)+"│");
that.room.librariesMap.commands?.announceInfo("│"+format("Ball In Own Half", 17)+"│"+format(percentage(t1.ballInHalfTicks, currentInGameTicks), 27)+"│"+format(percentage(t2.ballInHalfTicks, currentInGameTicks), 27)+"│");
that.room.librariesMap.commands?.announceInfo("└─────────────────┴───────────────────────────┴───────────────────────────┘");
}
function checkFormation(){
var fr = that.redFormation.split("-").map((x)=>parseInt(x.trim())).filter((x)=>!isNaN(x));
var fb = that.blueFormation.split("-").map((x)=>parseInt(x.trim())).filter((x)=>!isNaN(x));
formations = [null, fr, fb];
playerArrangementRedCount = 1+fr.reduce((p,x)=>(p+x), 0);
playerArrangementBlueCount = 1+fb.reduce((p,x)=>(p+x), 0);
formationsPlayerCoords = [null, [], []];
for (var t=0;t<2;t++){
var f=formations[t+1], coeff=2*t-1;
var x=stadiumWidth-20;
formationsPlayerCoords[t+1].push({x: coeff*x, y: 0});
var xStep=(stadiumWidth-20)/(1+f.length);
x-=xStep;
for (var i=0;i<f.length;i++){
var yStep=2*stadiumHeight/(f[i]+1);
for (var j=0,y=-stadiumHeight+yStep;j<f[i];j++,y+=yStep)
formationsPlayerCoords[t+1].push({x: coeff*x, y: y});
x-=xStep;
}
}
}
function resetAll(autoStart){
if (state==0){
if (that.playerArrangementEnabled)
state=1;
else
state=2;
}
ballRadius = parseFloat(that.ballRadius);
var w = parseFloat(that.width), h = parseFloat(that.height);
pMap = new Map();
pStats = new Map();
var diagonal = Math.sqrt(w*w+h*h);
var kickOffRadius = Math.min(w, h)*that.kickOffRadiusCoeff;
goalY = h*that.goalSizeCoeff;
var sc = that.speedCoeff*diagonal;
throwInSpeed = 7*sc-0.5;
freeKickSpeed = 9*sc-0.5;
penaltySpeed = 9*sc-0.5;
cornerSpeed = 10*sc-0.5;
outSpeed = 12*sc-0.5;
throwInObstacleRadius = 100*sc;
freeKickObstacleRadius = 200*sc;
cornerObstacleRadius = 300*sc;
outObstacleRadius = 500*sc;
var shackX1 = 0.03125*w, shackY1 = h-20, shackX2 = 0.25*w, shackY2 = h-90;
lastTouchedPlayers = [];
stadiumWidth = w-200+ballRadius;
stadiumHeight = h-190+ballRadius;
var mapObj = {
"name" : "abc Real Soccer (w="+w+", h="+h+", br="+ballRadius+", kr="+that.kickOffRadiusCoeff+", gs="+that.goalSizeCoeff+", ks="+that.kickStrengthCoeff+")",
"width" : w,
"height" : h,
"spawnDistance" : w/2-30,
"bg" : { "type" : "grass", "width" : w-200, "height" : h-190, "kickOffRadius" : kickOffRadius, "cornerRadius" : 0 },
"playerPhysics" : {
"bCoef" : 0.5,
"invMass" : 0.5,
"damping" : 0.96101,
"acceleration" : 0.1201,
"kickingAcceleration" : 0.07,
"kickingDamping" : 0.9605,
"kickStrength" : 4.5+that.kickStrengthCoeff*diagonal
},
"ballPhysics" : {
"radius" : ballRadius,
"bCoef" : 0.5,
"invMass" : 1,
"damping" : 0.9902,
"color" : "FFFFFF",
"cMask" : ["all"],
"cGroup" : ["ball"]
},
"vertexes" : [
/* 0 */ { "x" : 0, "y" : (h-100), "trait" : "kickOffBarrier" },
/* 1 */ { "x" : 0, "y" : kickOffRadius, "trait" : "kickOffBarrier" },
/* 2 */ { "x" : 0, "y" : -kickOffRadius, "trait" : "kickOffBarrier" },
/* 3 */ { "x" : 0, "y" : -(h-100), "trait" : "kickOffBarrier" },
/* 4 */ { "x" : -(w-200), "y" : goalY },
/* 5 */ { "x" : -(w-140), "y" : goalY },
/* 6 */ { "x" : -(w-200), "y" : -goalY },
/* 7 */ { "x" : -(w-140), "y" : -goalY },
/* 8 */ { "x" : w-200, "y" : goalY },
/* 9 */ { "x" : w-140, "y" : goalY },
/* 10 */ { "x" : w-200, "y" : -goalY },
/* 11 */ { "x" : w-140, "y" : -goalY },
/* 12 */ { "x" : -shackX1, "y" : shackY1, "trait" : "ballArea" },
/* 13 */ { "x" : -shackX1, "y" : shackY2, "trait" : "ballArea" },
/* 14 */ { "x" : -shackX2, "y" : shackY1, "trait" : "ballArea" },
/* 15 */ { "x" : -shackX2, "y" : shackY2, "trait" : "ballArea" },
/* 16 */ { "x" : shackX2, "y" : shackY1, "trait" : "ballArea" },
/* 17 */ { "x" : shackX2, "y" : shackY2, "trait" : "ballArea" },
/* 18 */ { "x" : shackX1, "y" : shackY1, "trait" : "ballArea" },
/* 19 */ { "x" : shackX1, "y" : shackY2, "trait" : "ballArea" }
],
"segments" : [
{ "v0" : 5, "v1" : 7, "vis" : true, "bCoef" : 0.1, "cMask" : ["ball","red","blue" ], "curve" : 10, "color" : "C7E6BD" },
{ "v0" : 9, "v1" : 11, "vis" : true, "bCoef" : 0.1, "cMask" : ["ball","red","blue" ], "curve" : -10, "color" : "C7E6BD" },
{ "v0" : 4, "v1" : 5, "curve" : 5, "color" : "ffffff", "trait" : "sidegoalNet" },
{ "v0" : 6, "v1" : 7, "curve" : -5, "color" : "ffffff", "trait" : "sidegoalNet" },
{ "v0" : 8, "v1" : 9, "curve" : -5, "color" : "ffffff", "trait" : "sidegoalNet" },
{ "v0" : 10, "v1" : 11, "curve" : 5, "color" : "ffffff", "trait" : "sidegoalNet" },
{ "v0" : 15, "v1" : 14, "curve" : 0, "vis" : true, "color" : "C7E6BD", "bCoef" : 1, "cMask" : ["red","blue" ], "trait" : "ballArea" },
{ "v0" : 15, "v1" : 13, "curve" : 0, "vis" : true, "color" : "C7E6BD", "bCoef" : 1, "cMask" : ["red","blue" ], "trait" : "ballArea" },
{ "v0" : 13, "v1" : 12, "curve" : 0, "vis" : true, "color" : "C7E6BD", "bCoef" : 1, "cMask" : ["red","blue" ], "trait" : "ballArea" },
{ "v0" : 18, "v1" : 19, "curve" : 0, "vis" : true, "color" : "C7E6BD", "bCoef" : 1, "cMask" : ["red","blue" ], "trait" : "ballArea" },
{ "v0" : 19, "v1" : 17, "curve" : 0, "vis" : true, "color" : "C7E6BD", "bCoef" : 1, "cMask" : ["red","blue" ], "trait" : "ballArea" },
{ "v0" : 17, "v1" : 16, "curve" : 0, "vis" : true, "color" : "C7E6BD", "bCoef" : 1, "cMask" : ["red","blue" ], "trait" : "ballArea" },
{ "v0" : 2, "v1" : 1, "curve" : -180, "cGroup" : ["blueKO" ], "trait" : "kickOffBarrier" },
{ "v0" : 2, "v1" : 1, "curve" : 180, "cGroup" : ["redKO" ], "trait" : "kickOffBarrier" },
{ "v0" : 0, "v1" : 1, "trait" : "kickOffBarrier" },
{ "v0" : 2, "v1" : 3, "trait" : "kickOffBarrier" }
],
"goals" : [
{ "p0" : [-(w-200),goalY], "p1" : [-(w-200),-goalY], "team" : "red" },
{ "p0" : [w-200,goalY], "p1" : [w-200,-goalY], "team" : "blue" }
],
"discs" : [
{ "pos" : [-(w-200),goalY], "trait" : "goalPost" },
{ "pos" : [-(w-200),-goalY], "trait" : "goalPost" },
{ "pos" : [w-200,goalY], "trait" : "goalPost" },
{ "pos" : [w-200,-goalY], "trait" : "goalPost" },
{ "pos" : [0,0], "trait" : "obstacle" },
{ "pos" : [0,0], "trait" : "directionIndicator" },
{ "pos" : [0,0], "trait" : "directionIndicator" }
],
"planes" : [
{ "normal" : [0,1], "dist" : -(h-150), "bCoef" : 0, "cMask" : ["ball"] },
{ "normal" : [0,-1], "dist" : -(h-150), "bCoef" : 0, "cMask" : ["ball"] },
{ "normal" : [0,1], "dist" : -(h-100), "bCoef" : 0 },
{ "normal" : [0,-1], "dist" : -(h-100), "bCoef" : 0 },
{ "normal" : [1,0], "dist" : -w, "bCoef" : 0 },
{ "normal" : [-1,0], "dist" : -w, "bCoef" : 0 },
{ "normal" : [1,0], "dist" : -(w-140), "bCoef" : 0, "cMask" : ["ball"] },
{ "normal" : [-1,0], "dist" : -(w-140), "bCoef" : 0, "cMask" : ["ball"] }
],
"joints" : [
{ "d0" : 6, "d1" : 7, "color": "000000", "strength": "rigid", length: [0, 10000] },
],
"traits" : {
"ballArea" : { "vis" : false, "bCoef" : 0, "cMask" : ["ball"] },
"goalPost" : { "radius" : 5, "invMass" : 0, "bCoef" : 2, "cMask" : ["ball", "red", "blue"], "cGroup" : ["wall"] },
"sidegoalNet" : { "bCoef" : 1, "cMask" : ["ball","red","blue"], "color" : "C7E6BD" },
"kickOffBarrier" : { "vis" : false, "bCoef" : 0.1, "cGroup" : ["redKO","blueKO"], "cMask" : ["red","blue"] },
"obstacle" : { "radius" : 15, "cMask" : ["red","blue" ], "cGroup" : ["none"], "invMass" : 0, "color" : "transparent" },
"directionIndicator" : { "radius" : 0, "vis" : false, "cMask" : ["none"], "cGroup" : ["none"] }
},
"redSpawnPoints": [[-(shackX1+shackX2)/2, shackY1]],
"blueSpawnPoints": [[(shackX1+shackX2)/2, shackY1]],
"cameraFollow": "ball"
};
checkFormation();
switch(state){
case 1:{ // player arrangement
playerArrangements = [null, [], []];
mapObj.traits["redSpot"] = { "color": "ff0000", "radius": 15, "invMass" : 0, "cMask" : ["red"] };
mapObj.traits["blueSpot"] = { "color": "0000ff", "radius": 15, "invMass" : 0, "cMask" : ["blue"] };
for (var t=1;t<3;t++){
var trait = (t==1) ? "redSpot" : "blueSpot";
formationsPlayerCoords[t].forEach((coord, idx)=>{
mapObj.discs.push({ "pos" : [coord.x, coord.y], "trait" : trait });
});
}
mapObj.cameraFollow = "player";
break;
}
case 2: // play mode
case 3:{ // penalties mode
if (!playerArrangements)
playerArrangements = [null, [], []];
if (playerArrangements[1].length==0 && playerArrangements[2].length==0){
that.room.players.forEach((p)=>{
var arr = playerArrangements[p.team.id];
if (!arr)
return;
arr.push(p.id);
});
}
if (state==2)
break;
mapObj.playerPhysics.kickingAcceleration = 0.27;
mapObj.traits["playerBarrier"] = { "bCoef": 0, "cMask": ["red", "blue"], vis: false };
mapObj.vertexes.push({ "x" : -(stadiumWidth-120), "y" : -h, "trait" : "playerBarrier" });
mapObj.vertexes.push({ "x" : -(stadiumWidth-120), "y" : h, "trait" : "playerBarrier" });
mapObj.vertexes.push({ "x" : -(stadiumWidth-20-ballRadius), "y" : -goalY, "trait" : "playerBarrier" });
mapObj.vertexes.push({ "x" : -(stadiumWidth-20-ballRadius), "y" : goalY, "trait" : "playerBarrier" });
mapObj.vertexes.push({ "x" : -(stadiumWidth+20-ballRadius), "y" : -goalY, "trait" : "playerBarrier" });
mapObj.vertexes.push({ "x" : -(stadiumWidth+20-ballRadius), "y" : goalY, "trait" : "playerBarrier" });
mapObj.vertexes.push({ "x" : stadiumWidth-120, "y" : -h, "trait" : "playerBarrier" });
mapObj.vertexes.push({ "x" : stadiumWidth-120, "y" : h, "trait" : "playerBarrier" });
mapObj.vertexes.push({ "x" : stadiumWidth-20-ballRadius, "y" : -goalY, "trait" : "playerBarrier" });
mapObj.vertexes.push({ "x" : stadiumWidth-20-ballRadius, "y" : goalY, "trait" : "playerBarrier" });
mapObj.vertexes.push({ "x" : stadiumWidth+20-ballRadius, "y" : -goalY, "trait" : "playerBarrier" });
mapObj.vertexes.push({ "x" : stadiumWidth+20-ballRadius, "y" : goalY, "trait" : "playerBarrier" });
mapObj.segments.push({ "v0" : 20, "v1" : 21, "trait" : "playerBarrier" });
mapObj.segments.push({ "v0" : 22, "v1" : 23, "trait" : "playerBarrier" });
mapObj.segments.push({ "v0" : 24, "v1" : 25, "trait" : "playerBarrier" });
mapObj.segments.push({ "v0" : 26, "v1" : 27, "trait" : "playerBarrier" });
mapObj.segments.push({ "v0" : 28, "v1" : 29, "trait" : "playerBarrier" });
mapObj.segments.push({ "v0" : 30, "v1" : 31, "trait" : "playerBarrier" });
penaltiesTurn = 0;
break;
}
}
setMode(state);
Utils.runAfterGameTick(()=>{
that.room.players.forEach((p)=>{
pMap.set(p.id, p);
pStats.set(p.id, createPlayerStats());
});
blockChanges = false;
that.room.stopGame();
that.room.setCurrentStadium(Utils.parseStadium(JSON.stringify(mapObj)));
that.room.setScoreLimit(0);
that.room.setTimeLimit(0);
if (autoStart)
that.room.startGame();
blockChanges = true;
if (state==1)
playerArrangements = [null, [], []];
else if (state==2)
arrangePlayers();
else if (state==3){
penaltyShotCount = -1;
cps = [];
penaltiesSetupNextTurn();
}
});
}
function penaltiesSetupNextTurn(){
if (penaltySaveTimeout)
clearTimeout(penaltySaveTimeout);
penaltySaveTimeout = null;
penaltiesTurn++;
var teamId = 1+penaltiesTurn%2, coeff = (penaltiesTurn%2)*2-1;
var a1 = playerArrangements[teamId].filter((x)=>x!=null), a2 = playerArrangements[3-teamId].filter((x)=>x!=null);
var gk1 = a1[0], gk2 = a2[0];
var ps1 = a2[(1+(penaltiesTurn/2)|0)%a2.length];
var ps2 = a1[(1+(penaltiesTurn/2)|0)%a1.length];
if (gk1==null || ps1==null || gk2==null || ps2==null || (penaltyShotCount>=2*that.penaltiesTurnCount && penaltyShotCount%2==0 && that.room.gameState.redScore!=that.room.gameState.blueScore)){
that.room.librariesMap.commands?.announceAction("Game over.");
that.room.stopGame();
cps = null;
return;
}
cps[0] = gk2;
cps[1] = ps2;
that.room.pluginsMap.powerShot?.stopPowerShot();
that.room.setDiscProperties(0, {
"x": coeff*(stadiumWidth-120),
"y": 0,
"xspeed": 0,
"yspeed": 0,
"invMass": 0
});
that.room.players.filter((x)=>x.team.id!=0).forEach(({id})=>{
that.room.setPlayerDiscProperties(id, {
"x": (id==gk2) ? coeff*(stadiumWidth-ballRadius) : ((id==ps2) ? coeff*(stadiumWidth-200) : NaN),
"y": (id==gk2 || id==ps2) ? 0 : NaN,
"xspeed": 0,
"yspeed": 0
});
});
penaltyShot = false;
}
function arrangePlayers(){
playerArrangements.forEach((arr, teamId)=>{
if (teamId==0)
return;
arr.forEach((playerId, formationIndex)=>{
if (playerId==null)
return;
var pos = formationsPlayerCoords[teamId][formationIndex];
if (!pos)
return;
that.room.setPlayerDiscProperties(playerId, {
"x": pos.x,
"y": pos.y,
"xspeed": 0,
"yspeed": 0
});
});
});
}
function addToLastTouchedPlayers(playerId){
var last = lastTouchedPlayers[lastTouchedPlayers.length-1];
if (last?.id==playerId)
return;
lastTouchedPlayers.push({id: playerId, teamId: pMap.get(playerId)?.team.id});
if (lastTouchedPlayers.length>2)
lastTouchedPlayers.splice(0, 1);
}
function fCheckOffside(player, ballX, ballXDirection){
if (!that.checkOffsides)
return false;
var t = player.team.id, tt = t*2-3;
if (tt*ballXDirection>=0)
return false;
var a = [null, that.room.players.filter((x)=>x.team.id==1).sort((a,b)=>a.disc.ext.pos.x-b.disc.ext.pos.x), that.room.players.filter((x)=>x.team.id==2).sort((a,b)=>b.disc.ext.pos.x-a.disc.ext.pos.x)];
var at = a[t];
if (a[3-t].filter((p)=>(tt*(ballX-p.disc.ext.pos.x)<0)).length>0)
return false;
if (tt*(at[at.length-1]?.disc.ext.pos.x-a[3-t][1]?.disc.ext.pos.x)<0)
return at[at.length-1].disc.ext.pos;
return false;
}
function fFreeKick(coord, teamId){
throwInTeam = teamId;
throwInType = 4;
tStats[throwInTeam].freeKicks++;
that.room.pluginsMap.powerShot?.stopPowerShot();
Utils.runAfterGameTick(()=>{
that.room.setDiscProperties(0, {
"x": coord.x,
"y": coord.y,
"xspeed": 0,
"yspeed": 0,
"invMass": 0
});
that.room.setDiscProperties(5, {
"x": coord.x,
"y": coord.y,
"radius": freeKickObstacleRadius,
"cMask": 1<<(3-teamId),
"cGroup": 1<<(3-teamId)
});
that.room.setDiscProperties(6, {
"x": coord.x,
"y": coord.y
});
that.room.setDiscProperties(7, {
"x": coord.x,
"y": coord.y
});
});
}
function fOffside(player, ball, ballXDirection){
if (Math.sign(ball.pos.x)*(player.team.id*2-3)>=0)
return;
var result = fCheckOffside(player, ball.pos.x, ballXDirection);
if (result){
tStats[player.team.id].offsides++;
that.room.librariesMap.commands?.announceAction("offside");
fFreeKick(result, 3-player.team.id);
}
return result;
}
function checkPass(player){
var last = lastTouchedPlayers[lastTouchedPlayers.length-2];
if (!last)
return;
if (typeof player=="number")
player = pMap.get(player);
var ps = pStats.get(last.id), teamId = player?.team?.id, ts = tStats[teamId];
ps && (ps.passes++);
ts && (ts.passes++);
if (last.teamId==teamId){
ps && (ps.accuratePasses++);
ts && (ts.accuratePasses++);
}
}
function sqrDist(x1,y1,x2,y2){
return (x2-x1)*(x2-x1)+(y2-y1)*(y2-y1);
}
function completeReset(){
reset(false, false);
state = 0;
resetAll(true);
}
function formation_IndexToDisc(teamId, formationIndex){
return {
id: formationIndex+8+((teamId==2)?playerArrangementRedCount:0),
pos: formationsPlayerCoords[teamId][formationIndex]
};
}
function formation_DiscToIndex(teamId, discId){
var n = discId-8-((teamId==2)?playerArrangementRedCount:0);
return {
idx: n,
pos: formationsPlayerCoords[teamId][n]
};
}
function removePlayerFromFormation(playerObj){
if (!playerObj)
return;
var _id = playerObj.id, formationIndex, teamId;
for (teamId=1;teamId<3;teamId++){
formationIndex = playerArrangements[teamId].findIndex((playerId)=>(playerId==_id))
if (formationIndex>=0)
break;
}
if (formationIndex<0)
return;
playerArrangements[teamId][formationIndex] = null;
return {
teamId,
formationIndex
};
}
this.initialize = function(){
state = 0;
resetAll(true);
permissionCtx = that.room.librariesMap.permissions?.createContext("CMD_realSoccer");
if (permissionCtx)
permissionIds = {
reset: permissionCtx.addPermission("reset"),
teamStats: permissionCtx.addPermission("teamStats"),
setMapParams: permissionCtx.addPermission("setMapParams")
};
var mapProperties = ["width", "height", "ballRadius", "kickOffRadiusCoeff", "goalSizeCoeff", "kickStrengthCoeff"];
that.room.librariesMap.commands?.add({
name: "setMapProperty",
parameters: [{
name: "propertyIndex",
type: VariableType.Integer,
range: {
min: 0,
max: mapProperties.length-1
}
}, {
name: "value",
type: VariableType.Number
}],
minParameterCount: 2,
helpText: "Overrides current map parameters and resets the game. propertyIndex can be one of the following: ("+mapProperties.map((x,i)=>(i+": "+x)).join(", ")+")",
callback: ({propertyIndex, value}, byId) => {
if (byId!=0 && !permissionCtx?.checkPlayerPermission(byId, permissionIds.setMapParams)){
that.room.librariesMap.commands?.announcePermissionDenied(byId);
return;
}
var prop = mapProperties[propertyIndex];
if (!prop){
that.room.librariesMap.commands?.announceError("Invalid property index: "+propertyIndex+".", byId);
return;
}
that[prop] = value;
completeReset();
}
});
that.room.librariesMap.commands?.add({
name: "reset",
parameters: [],
minParameterCount: 0,
helpText: "Resets the game.",
callback: ({}, byId) => {
if (byId!=0 && !permissionCtx?.checkPlayerPermission(byId, permissionIds.reset)){
that.room.librariesMap.commands?.announcePermissionDenied(byId);
return;
}
completeReset();
}
});
that.room.librariesMap.commands?.add({
name: "stats",
parameters: [],
minParameterCount: 0,
helpText: "Shows your current player statistics.",
callback: ({}, byId) => {
showPlayerStatistics(byId);
}
});
that.room.librariesMap.commands?.add({
name: "teamStats",
parameters: [],
minParameterCount: 0,
helpText: "Shows all current team statistics.",
callback: ({}, byId) => {
if (byId!=0 && !permissionCtx?.checkPlayerPermission(byId, permissionIds.teamStats)){
that.room.librariesMap.commands?.announcePermissionDenied(byId);
return;
}
showAllTeamStatistics();
}
});
that.room.librariesMap.commands?.add({
name: "change",
parameters: [],
minParameterCount: 0,
helpText: "Changes your current position. Only works while arranging players.",
callback: ({}, byId) => {
if (state!=1)
return;
var ret = removePlayerFromFormation(pMap.get(byId));
if (!ret)
return;
var { id, pos } = formation_IndexToDisc(ret.teamId, ret.formationIndex);
Utils.runAfterGameTick(()=>{
that.room.setDiscProperties(id, pos);
that.room.setPlayerDiscProperties(byId, {
"x": pos.x,
"y": pos.y+30,
"xspeed": 0,
"yspeed": 0,
"invMass": 1
});
});
}
});
};
this.finalize = function(){
if (penaltySaveTimeout){
clearTimeout(penaltySaveTimeout);
penaltySaveTimeout = null;
}
try{
reset(false, false);
}catch(ex){}
that.room.librariesMap.commands?.remove("setMapParams");
that.room.librariesMap.commands?.remove("reset");
that.room.librariesMap.commands?.remove("stats");
that.room.librariesMap.commands?.remove("teamStats");
that.room.librariesMap.commands?.remove("change");
that.room.librariesMap.permissions?.removeContext(permissionCtx);
permissionCtx = null;
permissionIds = null;
lastTouchedPlayers = null;
pMap = null;
pStats = null;
tStats = null;
};
function onOperationReceivedCommon(type, msg){
if (blockChanges && (type==OperationType.SetGamePlayLimit || type==OperationType.SetStadium))
return false;
if (type==OperationType.AutoTeams){
var p0 = that.room.players.filter((x)=>(x.team.id==0));
if (p0.length==0)
return false;
var p1 = that.room.players.filter((x)=>(x.team.id==1));
var p2 = that.room.players.filter((x)=>(x.team.id==2));
if (p1.length<p2.length)
that.room.setPlayerTeam(p0[p0.length-1].id, 1);
else if (p2.length<p1.length)
that.room.setPlayerTeam(p0[p0.length-1].id, 2);
else if (p0.length>1){
that.room.setPlayerTeam(p0[p0.length-2].id, 1);
that.room.setPlayerTeam(p0[p0.length-1].id, 2);
}
return false;
}
return true;
};
var modes = [{
onPlayerJoin: function(playerObj){
pMap.set(playerObj.id, playerObj);
},
onPlayerLeave: function(playerObj, reason, isBanned, byId, customData){
pMap.delete(playerObj.id);
},
onOperationReceived: onOperationReceivedCommon
},
// arrangement mode
{
onPlayerJoin: function(playerObj){
pMap.set(playerObj.id, playerObj);
},
onPlayerLeave: function(playerObj, reason, isBanned, byId, customData){
pMap.delete(playerObj.id);
var ret = removePlayerFromFormation(playerObj);
if (!ret)
return;
var { id, pos } = formation_IndexToDisc(ret.teamId, ret.formationIndex);
Utils.runAfterGameTick(()=>{
that.room.setDiscProperties(id, pos);
});
},
onGameStart: function(byId){
reset(false, false);
that.room.setDiscProperties(0, {
"x": NaN,
"y": NaN
});
playerArrangements = [null, [], []];
},
onGameStop: null,
onPlayerTeamChange: function(_id, teamId, byId){
var ret = removePlayerFromFormation(pMap.get(_id));
if (!ret)
return;
var { id, pos } = formation_IndexToDisc(ret.teamId, ret.formationIndex);
Utils.runAfterGameTick(()=>{
that.room.setDiscProperties(id, pos);
});
},
onTeamGoal: null,
onPlayerBallKick: null,
onCollisionDiscVsPlane: null,
onCollisionDiscVsSegment: null,
onCollisionDiscVsDisc: function(discId1, discPlayerId1, discId2, discPlayerId2){
if (discPlayerId1==null && (discPlayerId2==null || discId1<5))
return;
if (discPlayerId1==null){
var p = pMap.get(discPlayerId2);
if (!p)
return;
var teamId = p.team.id;
var { idx, pos } = formation_DiscToIndex(teamId, discId1);
playerArrangements[teamId][idx] = discPlayerId2;
that.room.fakeSendPlayerInput(0, discPlayerId2);
Utils.runAfterGameTick(()=>{
that.room.setDiscProperties(discId1, {
"x": NaN,
"y": NaN
});
that.room.setDiscProperties(discId2, {
"x": pos.x,
"y": pos.y,
"xspeed": 0,
"yspeed": 0,
"invMass": 0
});
});
setTimeout(()=>{
that.room.setDiscProperties(discId2, {
"x": pos.x,
"y": pos.y,
"xspeed": 0,
"yspeed": 0,
"invMass": 0
});
p = pMap.get(discPlayerId2);
if (!p)
return;
var c1 = that.waitForFullFormation ? playerArrangementRedCount : that.room.players.reduce((o,x)=>(o+((x.team.id==1)?1:0)),0);
var c2 = that.waitForFullFormation ? playerArrangementBlueCount : that.room.players.reduce((o,x)=>(o+((x.team.id==2)?1:0)),0);
if ((playerArrangements[1].reduce((o,x)=>(o+((x!=null)?1:0)),0)==c1) && (playerArrangements[2].reduce((o,x)=>(o+((x!=null)?1:0)),0)==c2)){
state = 2;
resetAll(true);
}
}, 120);
}
},
onGameTick: null,
onPositionsReset: null,
onOperationReceived: function(type, msg, globalFrameNo, clientFrameNo){
if (!onOperationReceivedCommon(type))
return false;
if (type==OperationType.SetPlayerTeam){
var currentPlayerCount = that.room.players.filter((x)=>x.team.id==msg.team.id).length;
if (msg.team.id==1 && currentPlayerCount>=playerArrangementRedCount)
return false;
if (msg.team.id==2 && currentPlayerCount>=playerArrangementBlueCount)
return false;
return true;
}
if (type!=OperationType.SendInput)
return true;
if (playerArrangements[pMap.get(msg.byId)?.team.id]?.findIndex((playerId)=>(playerId==msg.byId))>=0)
msg.input = 0;
else
msg.input = msg.input&(~16);
return true;
}
},
// play mode
{
onPlayerJoin: function(playerObj){
pMap.set(playerObj.id, playerObj);
pStats.set(playerObj.id, createPlayerStats());
},
onPlayerLeave: function(playerObj, reason, isBanned, byId, customData){
removePlayerFromFormation(playerObj);
pMap.delete(playerObj.id);
pStats.delete(playerObj.id);
},
onGameStart: function(byId){
tStats = [null, createTeamStats(), createTeamStats()];
currentInGameTicks = 0;
targetInGameTicks = Math.round(that.timeLimit/0.01666666666666667);
reset();
Utils.runAfterGameTick(()=>{
arrangePlayers();
});
},
onGameStop: function(byId){
if (!blockChanges)
return;
state = 0;
resetAll(false);
},
onPlayerTeamChange: function(id, teamId, byId){
removePlayerFromFormation(pMap.get(id));
if (teamId!=0){
var idx = playerArrangements[teamId].findIndex((x)=>x==null);
if (idx>-1)
playerArrangements[teamId][idx] = id;
else
playerArrangements[teamId].push(id);
}
/*
lastTouchedPlayers.forEach((x)=>{
if (x.id==id)
x.teamId = teamId;
});
*/
},
onTeamGoal: function(teamId, goalId, goal, ballDiscId, ballDisc){
var last = lastTouchedPlayers[lastTouchedPlayers.length-1];
if (!last)
return;
var s = pStats.get(last.id);
if (teamId==last.teamId){
s && (s.goals++);
var p = lastTouchedPlayers[lastTouchedPlayers.length-2];
if (p?.teamId==teamId){
s = pStats.get(p.id);
s && (s.assists++);
}
}
else
s && (s.ownGoals++);
},
onPlayerBallKick: function(playerId){
addToLastTouchedPlayers(playerId);
var p = pMap.get(playerId);
if (!p)
return;
checkPass(p);
var ball = that.room.getBall();
if (throwInTeam!=null && throwInType!=null){
if (p.team.id!=throwInTeam)
return;
var { pos } = p.disc.ext;
if ((throwInType==1 && Math.sign(ball.pos.y)*Math.sign(ball.pos.y-pos.y)>=0) ||
(throwInType==2 && (Math.sign(ball.pos.y)*Math.sign(ball.pos.y-pos.y)>=0 || Math.sign(ball.pos.x)*Math.sign(ball.pos.x-pos.x)>=0)) ||
(throwInType==3 && Math.sign(ball.pos.x)*Math.sign(ball.pos.x-pos.x)>=0))
return;
if (fOffside(p, ball, Math.sign(ball.pos.x-pos.x)))
return;
var d = Math.sqrt(sqrDist(ball.pos.x, ball.pos.y, pos.x, pos.y));
var speed = (throwInType==1) ? throwInSpeed : ((throwInType==2) ? cornerSpeed : ((throwInType==3) ? outSpeed : freeKickSpeed));
reset(true, false);
Utils.runAfterGameTick(()=>{
that.room.setDiscProperties(0, {
"xspeed": speed*(ball.pos.x-pos.x)/d,
"yspeed": speed*(ball.pos.y-pos.y)/d,
"invMass": 1
});
});
return;
}
if (fOffside(p, ball, Math.sign(ball.speed.x)))
return;
},
onCollisionDiscVsPlane: null,
onCollisionDiscVsSegment: null,
onCollisionDiscVsDisc: function(discId1, discPlayerId1, discId2, discPlayerId2){
if (discId1!=0 || discPlayerId2==null)
return;
addToLastTouchedPlayers(discPlayerId2);
checkPass(discPlayerId2);
},
onGameTick: function(){
var ball = that.room.getBall();
if (!ball || that.room.gameState.state!=GamePlayState.Playing)
return;
if (throwInTeam!=null){
if (that.showDirectionIndicator)
Utils.runAfterGameTick(()=>{
var bx = ball.pos.x, by = ball.pos.y;
var best = that.room.players.reduce((best, p)=>{
if (p.team.id==throwInTeam && p.disc.ext){
var { pos } = p.disc.ext, value = sqrDist(bx, by, pos.x, pos.y);
var t = that.directionIndicatorThreshold;
if (value<t*t && value<best.value){
best.value = value;
best.pos = pos;
}
}
return best;
}, {pos: {x: NaN, y: NaN}, value: Infinity});
var a = Math.sqrt(best.value);
var k = that.directionIndicatorLengthCoeff*((throwInType==1) ? throwInSpeed : ((throwInType==2) ? cornerSpeed : ((throwInType==3) ? outSpeed : freeKickSpeed)));
that.room.setDiscProperties(7, {
x: bx+k*(bx-best.pos.x)/a,
y: by+k*(by-best.pos.y)/a
});
});
return;
}
currentInGameTicks++;
tStats[ball.pos.x>0?2:1].ballInHalfTicks++;
var lastTouchedTeamId = lastTouchedPlayers[lastTouchedPlayers.length-1]?.teamId;
if (lastTouchedTeamId)
tStats[lastTouchedTeamId].ballTicks++;
if (currentInGameTicks==targetInGameTicks){
Utils.runAfterGameTick(()=>{
if (that.room.gameState.redScore==that.room.gameState.blueScore && that.penaltiesEnabled){
that.room.librariesMap.commands?.announceAction("Penalties.");
state = 3;
resetAll(true);
return;
}
that.room.librariesMap.commands?.announceAction("Time is up.");
that.room.stopGame();
});
return;
}
if (that.announceTime){
var rt = targetInGameTicks-currentInGameTicks;
if (rt%3600==0 || rt==1800 || rt==1200 || rt==600 || rt==300)
that.room.librariesMap.commands?.announceInfo("Remaining time: "+((rt/60)|0)+" seconds.");
}
if (that.checkOuts && Math.abs(ball.pos.x)>stadiumWidth && Math.abs(ball.pos.y)>goalY){
if (!lastTouchedTeamId)
lastTouchedTeamId = 1;
throwInTeam = 3-lastTouchedTeamId;
var signedTeamId = (throwInTeam*2-3), x, y, r, signBallPosX = Math.sign(ball.pos.x);
if (that.checkCorners && signBallPosX*signedTeamId<0){
x = signBallPosX*stadiumWidth;
y = Math.sign(ball.pos.y)*stadiumHeight;
r = cornerObstacleRadius;
throwInType = 2;
tStats[throwInTeam].corners++;
}
else{
x = signBallPosX*(stadiumWidth-135);
y = Math.sign(ball.pos.y)*goalY;
r = outObstacleRadius;
throwInType = 3;
tStats[throwInTeam].outs++;
Utils.runAfterGameTick(()=>{
that.room.players.forEach((p)=>{
if (p.team.id!=3-throwInTeam)
return;
var pos = p.disc?.ext?.pos;
if (!pos)
return;
var diff = Math.sign(pos.x-x);
if (diff*signedTeamId>0)
that.room.setPlayerDiscProperties(p.id, {
"x": x-diff*40
});
});
});
}
that.room.pluginsMap.powerShot?.stopPowerShot();
Utils.runAfterGameTick(()=>{
that.room.setDiscProperties(0, {
"x": x,
"y": y,
"xspeed": 0,
"yspeed": 0,
"invMass": 0
});
that.room.setDiscProperties(5, {
"x": x,
"y": y,
"radius": r,
"cMask": 1<<(3-throwInTeam),
"cGroup": 1<<(3-throwInTeam)
});
that.room.setDiscProperties(6, {
"x": x,
"y": y
});
that.room.setDiscProperties(7, {
"x": x,
"y": y
});
});
return;
}
if (that.checkThrowIns && Math.abs(ball.pos.y)>stadiumHeight){
var x = ball.pos.x, y = Math.sign(ball.pos.y)*stadiumHeight;
if (!lastTouchedTeamId)
lastTouchedTeamId = 1;
throwInTeam = 3-lastTouchedTeamId;
throwInType = 1;
tStats[throwInTeam].throwIns++;
that.room.pluginsMap.powerShot?.stopPowerShot();
Utils.runAfterGameTick(()=>{
that.room.setDiscProperties(0, {
"x": x,
"y": y,
"xspeed": 0,
"yspeed": 0,
"invMass": 0
});
that.room.setDiscProperties(5, {
"x": x,
"y": y,
"radius": throwInObstacleRadius,
"cMask": 1<<(3-throwInTeam),
"cGroup": 1<<(3-throwInTeam)
});
that.room.setDiscProperties(6, {
"x": x,
"y": y
});
that.room.setDiscProperties(7, {
"x": x,
"y": y
});
});
return;
}
},
onPositionsReset: function(){
Utils.runAfterGameTick(()=>{
arrangePlayers();
});
},
onOperationReceived: onOperationReceivedCommon
},
// penalties mode
{
onPlayerJoin: function(playerObj){
pMap.set(playerObj.id, playerObj);
},
onPlayerLeave: function(playerObj, reason, isBanned, byId, customData){
pMap.delete(playerObj.id);
removePlayerFromFormation(playerObj);
if (cps[0]==playerObj.id || cps[1]==playerObj.id)
Utils.runAfterGameTick(()=>{
penaltiesTurn--;
penaltiesSetupNextTurn();
});
},
onGameStart: function(byId){
reset();
},
onGameStop: function(byId){
if (!blockChanges)
return;
state = 0;
resetAll(false);
},
onPlayerTeamChange: function(id, newTeamId, byId){
removePlayerFromFormation(pMap.get(id));
if (newTeamId!=0){
var idx = playerArrangements[newTeamId].findIndex((x)=>x==null);
if (idx>-1)
playerArrangements[newTeamId][idx] = id;
else
playerArrangements[newTeamId].push(id);
Utils.runAfterGameTick(()=>{
that.room.setPlayerDiscProperties(id, {
"x": NaN,
"y": NaN
});
});
}
if (cps[0]==id || cps[1]==id)
Utils.runAfterGameTick(()=>{
penaltiesTurn--;
penaltiesSetupNextTurn();
});
},
onTeamGoal: function(teamId, goalId, goal, ballDiscId, ballDisc){
if (!penaltySaveTimeout)
return;
clearTimeout(penaltySaveTimeout);
penaltySaveTimeout = null;
penaltyShot = false;
},
onPlayerBallKick: function(playerId){
if (penaltyShot){
if (penaltySaveTimeout){
clearTimeout(penaltySaveTimeout);
penaltySaveTimeout = null;
Utils.runAfterGameTick(()=>{
penaltiesSetupNextTurn();
});
return;
}
penaltySaveTimeout = setTimeout(()=>{
Utils.runAfterGameTick(()=>{
penaltiesSetupNextTurn();
});
}, that.penaltiesSaveTimeout);
return;
}
Utils.runAfterGameTick(()=>{
var ball = that.room.getBall();
var p = pMap.get(playerId);
var { pos } = p.disc;
var d = Math.sqrt(sqrDist(ball.pos.x, ball.pos.y, pos.x, pos.y));
that.room.setDiscProperties(0, {
"xspeed": penaltySpeed*(ball.pos.x-pos.x)/d,
"yspeed": penaltySpeed*(ball.pos.y-pos.y)/d,
"invMass": 1
});
penaltyShot = true;
penaltyShotCount++;
});
},
onCollisionDiscVsPlane: function(discId, discPlayerId, planeId){
if (!penaltyShot || discId!=0 || that.room.gameState.state==GamePlayState.AfterGoal)
return;
Utils.runAfterGameTick(()=>{
penaltiesSetupNextTurn();
});
},
onCollisionDiscVsSegment: function(discId, discPlayerId, segmentId){
if (!penaltyShot || discId!=0 || that.room.gameState.state==GamePlayState.AfterGoal)
return;
Utils.runAfterGameTick(()=>{
penaltiesSetupNextTurn();
});
},
onCollisionDiscVsDisc: function(discId1, discPlayerId1, discId2, discPlayerId2){
if (penaltySaveTimeout || !penaltyShot || (discId1!=0 && discId2!=0))
return;
penaltySaveTimeout = setTimeout(()=>{
Utils.runAfterGameTick(()=>{
penaltiesSetupNextTurn();
});
}, that.penaltiesSaveTimeout);
},
onGameTick: null,
onPositionsReset: ()=>{
Utils.runAfterGameTick(()=>{
penaltiesSetupNextTurn();
});
},
onOperationReceived: function(type, msg, globalFrameNo, clientFrameNo){
if (!onOperationReceivedCommon(type))
return false;
if (type!=OperationType.SendInput)
return true;