🎮ArcadeLab

Football Stars Deluxe

by BlazeKoala99
4708 lines61.8 KB
▶ Play
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport"
      content="width=device-width, initial-scale=1.0,
               maximum-scale=1.0, user-scalable=no">

<title>Football Stars Deluxe</title>

<style>
*{
  box-sizing:border-box;
}

html,body{
  margin:0;
  width:100%;
  height:100%;
  overflow:hidden;
  background:#010408;
  font-family:'Segoe UI',Arial,sans-serif;
  color:#fff;
  user-select:none;
  touch-action:none;
}

button,
select{
  font:inherit;
}

#gameCanvas{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  z-index:1;
}

/* =========================================================
   SCOREBOARD
========================================================= */

#score-board{
  position:absolute;
  top:15px;
  left:20px;

  min-width:310px;

  background:
    linear-gradient(
      135deg,
      rgba(10,25,60,.96),
      rgba(5,12,30,.98)
    );

  border:1px solid rgba(255,204,0,.4);
  border-left:6px solid #ffcc00;

  padding:8px 15px;

  border-radius:0 9px 9px 0;

  font-weight:900;

  display:flex;
  justify-content:space-between;
  align-items:center;

  gap:12px;

  z-index:5;

  box-shadow:
    0 10px 30px rgba(0,0,0,.6);
}

.score-display{
  background:#020612;
  padding:4px 15px;
  border-radius:5px;

  font-size:22px;
  color:#ffcc00;

  font-family:monospace;

  border:1px solid rgba(255,255,255,.1);

  white-space:nowrap;
}

#match-time{
  color:#8a99ad;
  font-size:13px;
  font-weight:800;
  white-space:nowrap;
}

#tournament-stage-text{
  position:absolute;
  top:75px;
  left:20px;

  font-size:11px;
  font-weight:900;

  background:rgba(0,0,0,.72);

  border:1px solid rgba(255,255,255,.15);

  padding:5px 12px;

  border-radius:4px;

  letter-spacing:1px;

  color:#ffcc00;

  z-index:5;
}

/* =========================================================
   MOBILE CONTROLS
========================================================= */

.control-zone{
  position:absolute;
  bottom:22px;
  left:0;

  width:100%;

  display:flex;
  justify-content:space-between;
  align-items:flex-end;

  padding:0 28px;

  z-index:8;

  pointer-events:none;
}

.joystick-container{
  width:135px;
  height:135px;

  background:
    radial-gradient(
      circle,
      rgba(10,25,50,.85),
      rgba(2,5,15,.94)
    );

  border:
    2px solid rgba(255,204,0,.45);

  border-radius:50%;

  position:relative;

  pointer-events:auto;

  box-shadow:
    0 10px 30px rgba(0,0,0,.65),
    inset 0 0 20px rgba(255,204,0,.05);
}

#joystick-knob{
  width:57px;
  height:57px;

  position:absolute;

  left:38px;
  top:38px;

  border-radius:50%;

  background:
    radial-gradient(
      circle at 35% 30%,
      #fff,
      #c8d8e8 45%,
      #66809f
    );

  border:1px solid rgba(255,255,255,.7);

  box-shadow:
    0 6px 18px rgba(0,0,0,.8);

  transition:transform .04s linear;
}

.action-buttons{
  display:flex;
  align-items:flex-end;
  gap:13px;

  pointer-events:auto;
}

.btn-fifa{
  border-radius:50%;

  color:#fff;

  font-weight:900;

  display:flex;
  align-items:center;
  justify-content:center;

  flex-direction:column;

  text-align:center;

  font-size:10px;

  border:2px solid rgba(255,255,255,.35);

  text-transform:uppercase;

  cursor:pointer;

  box-shadow:
    0 8px 22px rgba(0,0,0,.65);

  transition:
    transform .08s,
    filter .08s;
}

.btn-fifa:active{
  transform:scale(.9);
  filter:brightness(1.25);
}

.btn-shoot{
  width:80px;
  height:80px;

  background:
    linear-gradient(
      135deg,
      #ffb300,
      #d52d00
    );
}

.btn-pass{
  width:68px;
  height:68px;

  background:
    linear-gradient(
      135deg,
      #00a3ff,
      #0033cc
    );
}

.btn-tackle{
  width:68px;
  height:68px;

  background:
    linear-gradient(
      135deg,
      #2ecc71,
      #16733e
    );
}

/* =========================================================
   MENUS
========================================================= */

.screen{
  position:absolute;
  inset:0;

  background:
    radial-gradient(
      circle at center,
      #0c224a,
      #02050c 80%
    );

  display:flex;

  flex-direction:column;

  align-items:center;
  justify-content:center;

  z-index:20;

  overflow-y:auto;

  padding:20px;
}

.menu-title{
  font-size:38px;

  font-weight:1000;

  margin-bottom:25px;

  letter-spacing:3px;

  background:
    linear-gradient(
      90deg,
      #fff,
      #ffcc00,
      #fff
    );

  -webkit-background-clip:text;
  -webkit-text-fill-color:transparent;

  text-transform:uppercase;

  text-align:center;
}

.selection-box{
  background:
    rgba(4,12,30,.96);

  border:
    2px solid rgba(255,204,0,.5);

  padding:30px;

  border-radius:16px;

  text-align:center;

  box-shadow:
    0 25px 60px rgba(0,0,0,.85);
}

select{
  background:#061026;

  color:#fff;

  border:
    1px solid #ffcc00;

  padding:12px 30px;

  font-size:16px;

  border-radius:8px;

  outline:none;

  font-weight:bold;
}

.btn-gold{
  background:
    linear-gradient(
      90deg,
      #ffcc00,
      #ff9900
    );

  border:none;

  color:#050b14;

  font-weight:1000;

  padding:16px 50px;

  font-size:16px;

  border-radius:8px;

  box-shadow:
    0 10px 30px rgba(255,204,0,.4);

  cursor:pointer;

  text-transform:uppercase;

  margin-top:20px;

  transition:
    transform .1s,
    filter .1s;
}

.btn-gold:active{
  transform:scale(.96);
}

.btn-gold:hover{
  filter:brightness(1.08);
}

table{
  border-collapse:collapse;

  margin:15px 0;

  width:360px;

  text-align:left;

  background:
    rgba(255,255,255,.05);

  border-radius:8px;

  overflow:hidden;
}

th,td{
  padding:10px 12px;

  border-bottom:
    1px solid rgba(255,255,255,.1);

  font-size:14px;
}

th{
  background:
    rgba(255,204,0,.25);

  color:#ffcc00;
}

.highlight-row{
  background:
    rgba(0,229,255,.2);

  font-weight:bold;
}

/* =========================================================
   GOAL
========================================================= */

#goal-overlay{
  position:absolute;
  inset:0;

  background:
    radial-gradient(
      circle,
      rgba(255,204,0,.42) 0%,
      rgba(0,0,0,.82) 80%
    );

  display:flex;

  align-items:center;
  justify-content:center;

  z-index:15;

  opacity:0;

  pointer-events:none;

  transition:
    opacity .25s;
}

.goal-text{
  font-size:75px;

  font-weight:1000;

  text-shadow:
    0 0 40px #ffcc00,
    0 0 80px #ff9900;

  font-style:italic;

  letter-spacing:4px;

  text-align:center;
}

/* =========================================================
   LOADING
========================================================= */

#loading-screen{
  position:absolute;
  inset:0;

  background:
    radial-gradient(
      circle at center,
      #163566 0%,
      #02050c 80%
    );

  display:flex;

  flex-direction:column;

  align-items:center;
  justify-content:center;

  z-index:99;

  transition:
    opacity .45s;
}

.loading-title{
  font-size:44px;

  font-weight:1000;

  letter-spacing:6px;

  text-transform:uppercase;

  text-align:center;
}

.loading-title span{
  color:#ffcc00;
}

.loading-sub{
  margin-top:6px;

  color:#9fb0c5;

  font-size:13px;

  letter-spacing:4px;

  text-transform:uppercase;
}

.bar-container{
  width:75%;

  max-width:440px;

  height:16px;

  background:
    rgba(255,255,255,.1);

  border:
    1px solid rgba(255,204,0,.3);

  border-radius:99px;

  overflow:hidden;

  margin-top:30px;

  box-shadow:
    inset 0 2px 5px rgba(0,0,0,.6);
}

#progress-bar{
  width:0%;

  height:100%;

  background:
    linear-gradient(
      90deg,
      #ffcc00,
      #ff9900
    );

  box-shadow:
    0 0 20px #ffcc00;
}

#progress-text{
  font-size:20px;

  font-weight:900;

  color:#ffcc00;

  margin-top:14px;

  font-family:monospace;
}

#loading-info{
  font-size:11px;

  color:#8a99ad;

  margin-top:6px;

  text-transform:uppercase;

  letter-spacing:1px;
}

.loading-ball{
  font-size:52px;

  margin-top:25px;

  animation:
    spin 1s linear infinite;

  filter:
    drop-shadow(
      0 0 10px
      rgba(255,204,0,.5)
    );
}

@keyframes spin{
  to{
    transform:rotate(360deg);
  }
}

/* =========================================================
   RESPONSIVE
========================================================= */

@media(max-width:600px){

  #score-board{
    top:8px;
    left:8px;
    min-width:0;
    width:auto;
    gap:7px;
    padding:6px 9px;
  }

  .score-display{
    font-size:17px;
    padding:3px 8px;
  }

  #match-time{
    font-size:11px;
  }

  .menu-title{
    font-size:29px;
  }

  .selection-box{
    padding:22px;
  }

  table{
    width:310px;
  }

  .control-zone{
    padding:0 15px;
    bottom:14px;
  }

  .joystick-container{
    width:115px;
    height:115px;
  }

  #joystick-knob{
    left:29px;
    top:29px;
  }

  .btn-shoot{
    width:70px;
    height:70px;
  }

  .btn-pass,
  .btn-tackle{
    width:58px;
    height:58px;
  }

  .action-buttons{
    gap:8px;
  }

  .goal-text{
    font-size:48px;
  }
}
</style>
</head>

<body>

<!-- =======================================================
     LOADING
======================================================== -->

<div id="loading-screen">

  <div class="loading-title">
    FOOTBALL <span>STARS</span>
  </div>

  <div class="loading-sub">
    Deluxe Edition
  </div>

  <div class="bar-container">
    <div id="progress-bar"></div>
  </div>

  <div id="progress-text">
    0%
  </div>

  <div id="loading-info">
    Stadion wird aufgebaut...
  </div>

  <div class="loading-ball">
    ⚽
  </div>

</div>

<!-- =======================================================
     TEAM MENU
======================================================== -->

<div id="menu-team"
     class="screen"
     style="display:none">

  <div style="
    color:#ffcc00;
    font-weight:900;
    letter-spacing:4px;
    margin-bottom:5px;
  ">
    FOOTBALL STARS DELUXE
  </div>

  <div class="menu-title">
    Wähle dein Land
  </div>

  <div class="selection-box">

    <select id="select-country"></select>

    <br>

    <button class="btn-gold"
            onclick="setupTournament()">
      Turnier starten
    </button>

  </div>

</div>

<!-- =======================================================
     TOURNAMENT HUB
======================================================== -->

<div id="menu-hub"
     class="screen"
     style="display:none">

  <div class="menu-title"
       id="hub-stage-title">
    Gruppenphase
  </div>

  <div class="selection-box"
       style="min-width:380px">

    <div id="hub-content"></div>

    <button class="btn-gold"
            id="btn-next-match"
            onclick="triggerLoadingScreen()">
      Nächstes Match spielen
    </button>

  </div>

</div>

<!-- =======================================================
     PENALTY
======================================================== -->

<div id="menu-penalty"
     class="screen"
     style="display:none">

  <div class="menu-title">
    Elfmeterschiessen
  </div>

  <div class="selection-box">

    <h3 id="penalty-teams"
        style="
          color:#ffcc00;
          font-size:24px;
        ">
      0 : 0
    </h3>

    <p id="penalty-info">
      Nutze den Joystick zum Zielen
      und drücke SCHUSS!
    </p>

    <button class="btn-gold"
            onclick="executePenaltyRound()">
      Schießen / Halten
    </button>

  </div>

</div>

<!-- =======================================================
     GOAL OVERLAY
======================================================== -->

<div id="goal-overlay">
  <div class="goal-text" id="gt">
    TOR!
  </div>
</div>

<!-- =======================================================
     SCORE
======================================================== -->

<div id="score-board"
     style="display:none">

  <span id="name-p">
    GER
  </span>

  <span class="score-display">
    <span id="p-score">0</span>
    :
    <span id="e-score">0</span>
  </span>

  <span id="name-e">
    BRA
  </span>

  <span id="match-time">
    00:00
  </span>

</div>

<div id="tournament-stage-text"
     style="display:none">
  GRUPPENPHASE
</div>

<!-- =======================================================
     CANVAS
======================================================== -->

<canvas id="gameCanvas"></canvas>

<!-- =======================================================
     CONTROLS
======================================================== -->

<div class="control-zone"
     id="controls"
     style="display:none">

  <div class="joystick-container"
       id="joystick">

    <div id="joystick-knob"></div>

  </div>

  <div class="action-buttons">

    <div class="btn-fifa btn-tackle"
         id="tackleBtn">
      Abnehmen
    </div>

    <div class="btn-fifa btn-pass"
         id="passBtn">
      Pass
    </div>

    <div class="btn-fifa btn-shoot"
         id="shootBtn">
      Schuss
    </div>

  </div>

</div>

<script>
/* =========================================================
   CANVAS
========================================================= */

const canvas =
  document.getElementById("gameCanvas");

const ctx =
  canvas.getContext("2d");

let W=innerWidth;
let H=innerHeight;

let fieldX=0;
let fieldY=0;
let fieldW=0;
let fieldH=0;

const WORLD_W=60;
const WORLD_H=40;

const HALF_W=30;
const HALF_H=20;

let gameRunning=false;
let penaltyMode=false;

let userTeamKey="";
let tournamentStage="GROUP";
let currentOpponent="";

let groupStageData=[];

let scorePlayer=0;
let scoreEnemy=0;

let matchMinutes=0;
let matchSeconds=0;

let frameCounter=0;
let aiTick=0;

let kickoffTeamIsPlayer=true;

let lastTime=performance.now();

/* =========================================================
   TEAMS
========================================================= */

const TEAMS={

  GER:{
    short:"GER",
    flag:"🇩🇪",
    name:"Deutschland",
    color:"#f5f5f5",
    accent:"#000"
  },

  BRA:{
    short:"BRA",
    flag:"🇧🇷",
    name:"Brasilien",
    color:"#f1c40f",
    accent:"#27ae60"
  },

  FRA:{
    short:"FRA",
    flag:"🇫🇷",
    name:"Frankreich",
    color:"#155ab6",
    accent:"#c0392b"
  },

  ARG:{
    short:"ARG",
    flag:"🇦🇷",
    name:"Argentinien",
    color:"#74acdf",
    accent:"#fff"
  },

  ESP:{
    short:"ESP",
    flag:"🇪🇸",
    name:"Spanien",
    color:"#c0392b",
    accent:"#f1c40f"
  },

  POR:{
    short:"POR",
    flag:"🇵🇹",
    name:"Portugal",
    color:"#b31919",
    accent:"#27ae60"
  },

  ENG:{
    short:"ENG",
    flag:"🏴",
    name:"England",
    color:"#fff",
    accent:"#c0392b"
  },

  ITA:{
    short:"ITA",
    flag:"🇮🇹",
    name:"Italien",
    color:"#006699",
    accent:"#fff"
  },

  NED:{
    short:"NED",
    flag:"🇳🇱",
    name:"Niederlande",
    color:"#ff6600",
    accent:"#fff"
  },

  BEL:{
    short:"BEL",
    flag:"🇧🇪",
    name:"Belgien",
    color:"#cc0000",
    accent:"#f1c40f"
  },

  CRO:{
    short:"CRO",
    flag:"🇭🇷",
    name:"Kroatien",
    color:"#fff",
    accent:"#c0392b"
  },

  JPN:{
    short:"JPN",
    flag:"🇯🇵",
    name:"Japan",
    color:"#000066",
    accent:"#fff"
  },

  USA:{
    short:"USA",
    flag:"🇺🇸",
    name:"USA",
    color:"#002b49",
    accent:"#c0392b"
  },

  MAR:{
    short:"MAR",
    flag:"🇲🇦",
    name:"Marokko",
    color:"#c1272d",
    accent:"#006633"
  }
};

/* =========================================================
   TEAM SELECT
========================================================= */

const selectC =
  document.getElementById("select-country");

Object.keys(TEAMS).forEach(key=>{

  const option =
    document.createElement("option");

  option.value=key;

  option.textContent=
    `${TEAMS[key].flag} ${TEAMS[key].name}`;

  selectC.appendChild(option);
});

/* =========================================================
   HELPERS
========================================================= */

function clamp(value,min,max){
  return Math.max(
    min,
    Math.min(max,value)
  );
}

function distance(a,b){
  return Math.hypot(
    a.x-b.x,
    a.z-b.z
  );
}

function normalize(x,z){

  const d=Math.hypot(x,z);

  if(d<0.0001){
    return {x:0,z:0};
  }

  return {
    x:x/d,
    z:z/d
  };
}

function randomRange(a,b){
  return a+
    Math.random()*(b-a);
}

function worldToScreen(x,z){

  return {
    x:
      fieldX+
      (x+HALF_W)/
      WORLD_W*
      fieldW,

    y:
      fieldY+
      (z+HALF_H)/
      WORLD_H*
      fieldH
  };
}

/* =========================================================
   RESIZE
========================================================= */

function resizeCanvas(){

  W=innerWidth;
  H=innerHeight;

  canvas.width=
    W*devicePixelRatio;

  canvas.height=
    H*devicePixelRatio;

  canvas.style.width=
    W+"px";

  canvas.style.height=
    H+"px";

  ctx.setTransform(
    devicePixelRatio,
    0,
    0,
    devicePixelRatio,
    0,
    0
  );

  const maxW=
    Math.min(W-30,960);

  const maxH=
    Math.min(H-80,620);

  fieldW=maxW;
  fieldH=maxH;

  fieldX=
    (W-fieldW)/2;

  fieldY=
    (H-fieldH)/2;
}

addEventListener(
  "resize",
  resizeCanvas
);

resizeCanvas();

/* =========================================================
   CREATE PLAYERS
========================================================= */

function createTeamPlayers(isPlayerTeam){

  const dir=
    isPlayerTeam ? 1 : -1;

  const formation=[

    {role:"GK",x:27,z:0},

    {role:"DEF",x:21,z:-12},
    {role:"DEF",x:20,z:-4},
    {role:"DEF",x:20,z:4},
    {role:"DEF",x:21,z:12},

    {role:"MID",x:11,z:-10},
    {role:"MID",x:9,z:-3},
    {role:"MID",x:9,z:3},
    {role:"MID",x:11,z:10},

    {role:"ST",x:2,z:-5},
    {role:"ST",x:2,z:5}
  ];

  return formation.map((p,index)=>{

    const speed=
      p.role==="GK"
        ? .14
        : p.role==="DEF"
          ? .21
          : p.role==="MID"
            ? .235
            : .255;

    return {

      index,

      role:p.role,

      x:p.x*dir,
      z:p.z,

      homeX:p.x*dir,
      homeZ:p.z,

      vx:0,
      vz:0,

      speed,

      maxSpeed:
        speed*1.12,

      radius:.8,

      stamina:100,

      cooldown:0,

      isGK:
        p.role==="GK",

      isPlayer:false,

      aiState:"POSITION",

      targetX:p.x*dir,
      targetZ:p.z,

      facing:dir
    };
  });
}

/* =========================================================
   OBJECTS
========================================================= */

let playerTeam=[];
let enemyTeam=[];

let controlledPlayerIndex=9;

const referee={
  x:0,
  z:-6,
  speed:.17,
  radius:.7
};

const ball={
  x:0,
  y:.4,
  z:0,

  vx:0,
  vz:0,

  radius:.4,

  owner:null,

  friction:.965
};

/* =========================================================
   FIELD DRAW
========================================================= */

function drawField(){

  ctx.clearRect(
    0,
    0,
    W,
    H
  );

  ctx.fillStyle="#040b16";

  ctx.fillRect(
    0,
    0,
    W,
    H
  );

  /*
     Rasenrand
  */

  ctx.fillStyle="#10381d";

  ctx.fillRect(
    fieldX-15,
    fieldY-15,
    fieldW+30,
    fieldH+30
  );

  /*
     Rasenstreifen
  */

  const stripeWidth=
    fieldW/12;

  for(let i=0;i<12;i++){

    ctx.fillStyle=
      i%2===0
        ? "#186b30"
        : "#1f7d39";

    ctx.fillRect(
      fieldX+
      i*stripeWidth,

      fieldY,

      stripeWidth,
      fieldH
    );
  }

  /*
     Mittellinie
  */

  ctx.strokeStyle=
    "rgba(255,255,255,.9)";

  ctx.lineWidth=3;

  ctx.strokeRect(
    fieldX,
    fieldY,
    fieldW,
    fieldH
  );

  ctx.beginPath();

  ctx.moveTo(
    fieldX+fieldW/2,
    fieldY
  );

  ctx.lineTo(
    fieldX+fieldW/2,
    fieldY+fieldH
  );

  ctx.stroke();

  /*
     Mittelkreis
  */

  ctx.beginPath();

  ctx.arc(
    fieldX+fieldW/2,
    fieldY+fieldH/2,
    fieldH*.13,
    0,
    Math.PI*2
  );

  ctx.stroke();

  ctx.beginPath();

  ctx.fillStyle="#fff";

  ctx.arc(
    fieldX+fieldW/2,
    fieldY+fieldH/2,
    4,
    0,
    Math.PI*2
  );

  ctx.fill();

  /*
     Strafräume
  */

  ctx.strokeRect(
    fieldX,
    fieldY+fieldH*.32,
    fieldW*.14,
    fieldH*.36
  );

  ctx.strokeRect(
    fieldX+fieldW*.86,
    fieldY+fieldH*.32,
    fieldW*.14,
    fieldH*.36
  );

  /*
     Torpfosten
  */

  ctx.fillStyle="#fff";

  ctx.fillRect(
    fieldX-4,
    fieldY+fieldH*.4,
    6,
    fieldH*.2
  );

  ctx.fillRect(
    fieldX+fieldW-2,
    fieldY+fieldH*.4,
    6,
    fieldH*.2
  );

  /*
     Tornetze
  */

  ctx.strokeStyle=
    "rgba(255,255,255,.18)";

  ctx.lineWidth=1;

  ctx.strokeRect(
    fieldX-18,
    fieldY+fieldH*.4,
    18,
    fieldH*.2
  );

  ctx.strokeRect(
    fieldX+fieldW,
    fieldY+fieldH*.4,
    18,
    fieldH*.2
  );
}

/* =========================================================
   DRAW PLAYER
========================================================= */

function drawPlayer(
  player,
  team,
  label,
  controlled
){

  const p=
    worldToScreen(
      player.x,
      player.z
    );

  const r=
    Math.max(
      9,
      fieldW/72
    );

  /*
     Schatten
  */

  ctx.beginPath();

  ctx.fillStyle=
    "rgba(0,0,0,.45)";

  ctx.ellipse(
    p.x,
    p.y+r*.85,
    r*1.1,
    r*.4,
    0,
    0,
    Math.PI*2
  );

  ctx.fill();

  /*
     Kontrollring
  */

  if(controlled){

    ctx.strokeStyle="#ffcc00";

    ctx.lineWidth=3.5;

    ctx.beginPath();

    ctx.arc(
      p.x,
      p.y,
      r+5,
      0,
      Math.PI*2
    );

    ctx.stroke();

    ctx.strokeStyle=
      "rgba(255,204,0,.3)";

    ctx.lineWidth=6;

    ctx.beginPath();

    ctx.arc(
      p.x,
      p.y,
      r+9,
      0,
      Math.PI*2
    );

    ctx.stroke();
  }

  /*
     Ballbesitzring
  */

  if(ball.owner===player){

    ctx.strokeStyle="#00e5ff";

    ctx.lineWidth=2;

    ctx.beginPath();

    ctx.arc(
      p.x,
      p.y,
      r+12,
      0,
      Math.PI*2
    );

    ctx.stroke();
  }

  /*
     Körper
  */

  ctx.beginPath();

  ctx.fillStyle=
    team.color;

  ctx.arc(
    p.x,
    p.y,
    r,
    0,
    Math.PI*2
  );

  ctx.fill();

  ctx.strokeStyle=
    team.accent;

  ctx.lineWidth=2;

  ctx.stroke();

  /*
     Kopf
  */

  ctx.beginPath();

  ctx.fillStyle="#f3c19a";

  ctx.arc(
    p.x,
    p.y-r*.7,
    r*.4,
    0,
    Math.PI*2
  );

  ctx.fill();

  /*
     Rolle
  */

  if(label){

    ctx.fillStyle="#fff";

    ctx.font=
      "bold 10px Segoe UI";

    ctx.textAlign="center";

    ctx.shadowColor=
      "rgba(0,0,0,.8)";

    ctx.shadowBlur=4;

    ctx.fillText(
      label,
      p.x,
      p.y+r*1.9
    );

    ctx.shadowBlur=0;
  }
}

/* =========================================================
   DRAW GAME
========================================================= */

function drawGame(){

  drawField();

  const playerTeamObj=
    TEAMS[userTeamKey] ||
    {
      color:"#fff",
      accent:"#000"
    };

  const enemyTeamObj=
    TEAMS[currentOpponent] ||
    {
      color:"#c33",
      accent:"#fff"
    };

  playerTeam.forEach(
    (player,index)=>{

      drawPlayer(
        player,
        playerTeamObj,
        index===0
          ? "TW"
          : "",
        index===
        controlledPlayerIndex
      );
    }
  );

  enemyTeam.forEach(
    (player,index)=>{

      drawPlayer(
        player,
        enemyTeamObj,
        index===0
          ? "TW"
          : "",
        false
      );
    }
  );

  /*
     Schiedsrichter
  */

  drawPlayer(
    referee,
    {
      color:"#d4ac0d",
      accent:"#111"
    },
    "REF",
    false
  );

  /*
     Ball
  */

  const b=
    worldToScreen(
      ball.x,
      ball.z
    );

  const br=
    Math.max(
      6,
      fieldW/95
    );

  /*
     Ball shadow
  */

  ctx.beginPath();

  ctx.fillStyle=
    "rgba(0,0,0,.5)";

  ctx.ellipse(
    b.x,
    b.y+br,
    br*1.2,
    br*.5,
    0,
    0,
    Math.PI*2
  );

  ctx.fill();

  /*
     Ball
  */

  ctx.beginPath();

  ctx.fillStyle="#fff";

  ctx.arc(
    b.x,
    b.y,
    br,
    0,
    Math.PI*2
  );

  ctx.fill();

  ctx.strokeStyle="#222";

  ctx.lineWidth=1.5;

  ctx.stroke();

  ctx.beginPath();

  ctx.fillStyle="#111";

  ctx.arc(
    b.x,
    b.y,
    br*.35,
    0,
    Math.PI*2
  );

  ctx.fill();
}

/* =========================================================
   LOADING
========================================================= */

function triggerLoadingScreen(
  isInitialStart=false
){

  if(!isInitialStart){

    document
      .getElementById("menu-hub")
      .style.display="none";
  }

  const screen=
    document.getElementById(
      "loading-screen"
    );

  const bar=
    document.getElementById(
      "progress-bar"
    );

  const text=
    document.getElementById(
      "progress-text"
    );

  screen.style.display="flex";
  screen.style.opacity="1";

  bar.style.width="0%";
  text.textContent="0%";

  const duration=1000;

  const start=Date.now();

  function step(){

    const elapsed=
      Date.now()-start;

    const percent=
      Math.min(
        100,
        Math.floor(
          elapsed/duration*100
        )
      );

    bar.style.width=
      percent+"%";

    text.textContent=
      percent+"%";

    if(percent<100){

      requestAnimationFrame(step);

    }else{

      setTimeout(()=>{

        screen.style.opacity="0";

        setTimeout(()=>{

          screen.style.display="none";

          if(isInitialStart){

            document
              .getElementById("menu-team")
              .style.display="flex";

          }else{

            playNextMatch();
          }

        },450);

      },150);
    }
  }

  step();
}

/* =========================================================
   TOURNAMENT
========================================================= */

function setupTournament(){

  userTeamKey=
    selectC.value;

  tournamentStage=
    "GROUP";

  let opponents=
    Object.keys(TEAMS)
      .filter(
        key=>key!==userTeamKey
      );

  opponents.sort(
    ()=>Math.random()-.5
  );

  const selected=[
    userTeamKey,
    opponents[0],
    opponents[1],
    opponents[2]
  ];

  groupStageData=
    selected.map(key=>({

      key,

      name:
        TEAMS[key].name,

      flag:
        TEAMS[key].flag,

      pts:0,

      gd:0,

      matchesPlayed:0
    }));

  document
    .getElementById("menu-team")
    .style.display="none";

  updateTournamentHub();

  drawGame();
}

/* =========================================================
   HUB
========================================================= */

function updateTournamentHub(){

  document
    .getElementById("menu-hub")
    .style.display="flex";

  const content=
    document.getElementById(
      "hub-content"
    );

  const button=
    document.getElementById(
      "btn-next-match"
    );

  if(tournamentStage==="GROUP"){

    document
      .getElementById("hub-stage-title")
      .textContent=
      "🏆 WM - Gruppenphase";

    groupStageData.sort(
      (a,b)=>
        b.pts-a.pts ||
        b.gd-a.gd
    );

    let html=`
      <table>
        <tr>
          <th>Pos</th>
          <th>Team</th>
          <th>Sp</th>
          <th>Diff</th>
          <th>Pkt</th>
        </tr>
    `;

    groupStageData.forEach(
      (team,index)=>{

        const highlight=
          team.key===userTeamKey
            ? "class='highlight-row'"
            : "";

        html+=`
          <tr ${highlight}>
            <td>${index+1}</td>
            <td>
              ${team.flag} ${team.key}
            </td>
            <td>
              ${team.matchesPlayed}
            </td>
            <td>
              ${team.gd}
            </td>
            <td>
              ${team.pts}
            </td>
          </tr>
        `;
      }
    );

    html+="</table>";

    const me=
      groupStageData.find(
        t=>t.key===userTeamKey
      );

    /*
       Gruppenphase beendet
    */

    if(me.matchesPlayed>=3){

      tournamentStage=
        "HALBFINALE";

      const candidates=
        groupStageData
          .filter(
            t=>t.key!==userTeamKey
          )
          .sort(
            (a,b)=>
              b.pts-a.pts ||
              b.gd-a.gd
          );

      currentOpponent=
        candidates[0].key;

      content.innerHTML=
        html+
        "<p><b>Gruppenphase beendet!</b></p>";

      button.textContent=
        "Zum Halbfinale";

      button.onclick=
        triggerLoadingScreen;

      return;
    }

    /*
       Nächster Gegner
    */

    const myIndex=
      groupStageData.findIndex(
        t=>t.key===userTeamKey
      );

    currentOpponent=
      groupStageData[
        (
          myIndex+
          me.matchesPlayed+
          1
        )%
        groupStageData.length
      ].key;

    html+=`
      <p>
        Nächstes Spiel:
        <b>
          ${TEAMS[userTeamKey].flag}
          ${userTeamKey}
        </b>
        vs
        <b>
          ${TEAMS[currentOpponent].flag}
          ${currentOpponent}
        </b>
      </p>
    `;

    content.innerHTML=html;

    button.textContent=
      "Nächstes Match spielen";

    button.onclick=
      triggerLoadingScreen;

  }

  else if(
    tournamentStage==="HALBFINALE" ||
    tournamentStage==="FINALE"
  ){

    document
      .getElementById("hub-stage-title")
      .textContent=
        `🏁 WM - ${tournamentStage}`;

    content.innerHTML=`
      <h3>K.o.-Runde</h3>

      <p>
        ${TEAMS[userTeamKey].flag}
        ${userTeamKey}

        vs

        ${TEAMS[currentOpponent].flag}
        ${currentOpponent}
      </p>
    `;

    button.textContent=
      "Match starten";

    button.onclick=
      triggerLoadingScreen;

  }

  else{

    document
      .getElementById("hub-stage-title")
      .textContent=
      "🎉 WELTMEISTER!";

    content.innerHTML=`
      <h2>
        🏆 CHAMPION! 🏆
      </h2>

      <p>
        Pokal gewonnen!
      </p>
    `;

    button.textContent=
      "Neues Turnier";

    button.onclick=()=>{
      location.reload();
    };
  }
}

/* =========================================================
   START MATCH
========================================================= */

function playNextMatch(){

  document
    .getElementById("score-board")
    .style.display="flex";

  document
    .getElementById("tournament-stage-text")
    .style.display="block";

  document
    .getElementById("controls")
    .style.display="flex";

  document
    .getElementById("name-p")
    .textContent=userTeamKey;

  document
    .getElementById("name-e")
    .textContent=currentOpponent;

  document
    .getElementById("tournament-stage-text")
    .textContent=
      tournamentStage==="GROUP"
        ? "GRUPPENPHASE"
        : tournamentStage;

  scorePlayer=0;
  scoreEnemy=0;

  matchMinutes=0;
  matchSeconds=0;

  document
    .getElementById("p-score")
    .textContent="0";

  document
    .getElementById("e-score")
    .textContent="0";

  playerTeam=
    createTeamPlayers(true);

  enemyTeam=
    createTeamPlayers(false);

  controlledPlayerIndex=9;

  kickoffTeamIsPlayer=true;

  resetPositions();

  gameRunning=true;
}

/* =========================================================
   RESET
========================================================= */

function resetPositions(){

  playerTeam.forEach(player=>{

    player.x=player.homeX;
    player.z=player.homeZ;

    player.vx=0;
    player.vz=0;

    player.cooldown=0;
    player.isPlayer=false;
  });

  enemyTeam.forEach(player=>{

    player.x=player.homeX;
    player.z=player.homeZ;

    player.vx=0;
    player.vz=0;

    player.cooldown=0;
    player.isPlayer=false;
  });

  referee.x=0;
  referee.z=-6;

  ball.x=0;
  ball.z=0;

  ball.vx=0;
  ball.vz=0;

  if(kickoffTeamIsPlayer){

    ball.owner=
      playerTeam[9];

    ball.owner.isPlayer=true;

    controlledPlayerIndex=9;

  }else{

    ball.owner=
      enemyTeam[9];

    ball.owner.isPlayer=false;
  }
}

/* =========================================================
   PLAYER SWITCH
========================================================= */

function switchToBestPlayer(){

  /*
     Wenn wir Ballbesitz haben:
     Ballführer kontrollieren.
  */

  if(
    ball.owner &&
    ball.owner.isPlayer
  ){

    const index=
      playerTeam.indexOf(
        ball.owner
      );

    if(index>=0)
      controlledPlayerIndex=index;

    return;
  }

  let bestIndex=0;
  let bestScore=Infinity;

  playerTeam.forEach(
    (player,index)=>{

      if(
        player.isGK &&
        Math.abs(ball.x)>18
      ){
        return;
      }

      const d=
        distance(player,ball);

      let score=d;

      /*
         Bei gegnerischem Ball
         Verteidiger bevorzugen.
      */

      if(
        ball.owner &&
        !ball.owner.isPlayer
      ){

        if(player.role==="DEF")
          score-=4;

        if(player.role==="MID")
          score-=2;
      }

      /*
         Bei freiem Ball:
         Mittelfeldspieler und Stürmer
         dürfen früher attackieren.
      */

      if(!ball.owner){

        if(player.role==="MID")
          score-=2;

        if(player.role==="ST")
          score-=1;
      }

      if(score<bestScore){

        bestScore=score;
        bestIndex=index;
      }
    }
  );

  controlledPlayerIndex=
    bestIndex;
}

/* =========================================================
   JOYSTICK
========================================================= */

let joystickActive=false;

let joyStartX=0;
let joyStartY=0;

let moveX=0;
let moveZ=0;

const joystick=
  document.getElementById(
    "joystick"
  );

const knob=
  document.getElementById(
    "joystick-knob"
  );

function joystickStart(event){

  if(
    !gameRunning &&
    !penaltyMode
  ){
    return;
  }

  joystickActive=true;

  const rect=
    joystick.getBoundingClientRect();

  joyStartX=
    rect.left+
    rect.width/2;

  joyStartY=
    rect.top+
    rect.height/2;

  if(event.touches)
    event.preventDefault();
}

function joystickMove(event){

  if(!joystickActive)
    return;

  const pointer=
    event.touches
      ? event.touches[0]
      : event;

  const dx=
    pointer.clientX-
    joyStartX;

  const dy=
    pointer.clientY-
    joyStartY;

  const max=43;

  const length=
    Math.hypot(dx,dy);

  const scale=
    length>max
      ? max/length
      : 1;

  const x=
    dx*scale;

  const y=
    dy*scale;

  knob.style.transform=
    `translate(${x}px,${y}px)`;

  moveX=
    x/max;

  moveZ=
    y/max;

  if(event.touches)
    event.preventDefault();
}

function joystickEnd(){

  joystickActive=false;

  knob.style.transform=
    "translate(0,0)";

  moveX=0;
  moveZ=0;
}

joystick.addEventListener(
  "touchstart",
  joystickStart,
  {passive:false}
);

joystick.addEventListener(
  "mousedown",
  joystickStart
);

addEventListener(
  "touchmove",
  joystickMove,
  {passive:false}
);

addEventListener(
  "mousemove",
  joystickMove
);

addEventListener(
  "touchend",
  joystickEnd
);

addEventListener(
  "mouseup",
  joystickEnd
);

/* =========================================================
   HUMAN MOVEMENT
========================================================= */

function moveHumanPlayer(
  player,
  dt
){

  const magnitude=
    Math.min(
      1,
      Math.hypot(
        moveX,
        moveZ
      )
    );

  const sprint=
    magnitude>.72
      ? 1.08
      : .92;

  const targetVX=
    moveX*
    player.maxSpeed*
    sprint;

  const targetVZ=
    moveZ*
    player.maxSpeed*
    sprint;

  const acceleration=
    Math.min(
      1,
      9*dt
    );

  player.vx+=
    (targetVX-player.vx)*
    acceleration;

  player.vz+=
    (targetVZ-player.vz)*
    acceleration;

  player.x+=player.vx;
  player.z+=player.vz;

  player.x=
    clamp(
      player.x,
      -29,
      29
    );

  player.z=
    clamp(
      player.z,
      -19,
      19
    );

  if(
    Math.abs(player.vx)+
    Math.abs(player.vz)>.01
  ){

    player.facing=
      player.vx>=0
        ? 1
        : -1;
  }
}

/* =========================================================
   TACTICAL POSITION
========================================================= */

function tacticalPosition(
  player,
  isPlayerTeam
){

  const dir=
    isPlayerTeam
      ? 1
      : -1;

  const ballProgress=
    ball.x*dir;

  let x=
    player.homeX;

  let z=
    player.homeZ;

  /*
     Ganze Mannschaft verschiebt
     sich zur Ballseite.
  */

  const shift=
    clamp(
      ballProgress*.18,
      -5,
      5
    );

  x+=
    shift*dir;

  /*
     Torwart
  */

  if(player.role==="GK"){

    x=27*dir;

    z=
      clamp(
        ball.z,
        -5.5,
        5.5
      );
  }

  /*
     Verteidiger
  */

  else if(player.role==="DEF"){

    if(ballProgress>8)
      x-=3*dir;

    if(ballProgress<-5)
      x+=2*dir;
  }

  /*
     Mittelfeld
  */

  else if(player.role==="MID"){

    x+=
      ballProgress*
      .12*
      dir;

    z+=
      Math.sin(
        performance.now()/900+
        player.index
      )*.7;
  }

  /*
     Stürmer
  */

  else if(player.role==="ST"){

    x+=
      clamp(
        ballProgress*.2,
        -2,
        7
      )*
      dir;

    z+=
      Math.sin(
        performance.now()/700+
        player.index
      )*1.2;
  }

  return {
    x:clamp(x,-28,28),
    z:clamp(z,-18,18)
  };
}

/* =========================================================
   BEST PASS TARGET
========================================================= */

function findBestPassTarget(
  carrier,
  team,
  isPlayerTeam
){

  const dir=
    isPlayerTeam
      ? 1
      : -1;

  const opponents=
    isPlayerTeam
      ? enemyTeam
      : playerTeam;

  let best=null;
  let bestScore=-Infinity;

  team.forEach(player=>{

    if(
      player===carrier ||
      player.isGK
    ){
      return;
    }

    const dx=
      player.x-carrier.x;

    const dz=
      player.z-carrier.z;

    const d=
      Math.hypot(dx,dz);

    if(
      d<3 ||
      d>18
    ){
      return;
    }

    const forward=
      dx*dir;

    let nearestEnemy=999;

    opponents.forEach(enemy=>{

      nearestEnemy=
        Math.min(
          nearestEnemy,
          distance(player,enemy)
        );
    });

    const score=
      forward*1.8+
      nearestEnemy*1.5-
      d*.5+
      Math.random()*1.5;

    if(score>bestScore){

      bestScore=score;
      best=player;
    }
  });

  return best;
}

/* =========================================================
   AI SHOOT
========================================================= */

function aiShoot(
  player,
  isPlayerTeam
){

  const dir=
    isPlayerTeam
      ? 1
      : -1;

  const goalX=
    30*dir;

  const distanceToGoal=
    Math.abs(
      goalX-player.x
    );

  const accuracy=
    clamp(
      1-
      distanceToGoal/35,
      .45,
      .95
    );

  let targetZ=
    clamp(
      -player.z*.25+
      randomRange(-2.5,2.5),
      -3.8,
      3.8
    );

  if(Math.random()>accuracy){

    targetZ+=
      randomRange(-3,3);
  }

  const angle=
    Math.atan2(
      targetZ-ball.z,
      goalX-ball.x
    );

  const power=
    clamp(
      2+
      (35-distanceToGoal)/35,
      1.8,
      3.2
    );

  ball.owner=null;

  ball.vx=
    Math.cos(angle)*
    power;

  ball.vz=
    Math.sin(angle)*
    power;
}

/* =========================================================
   AI PASS
========================================================= */

function aiPass(
  player,
  target,
  isPlayerTeam
){

  if(!target)
    return false;

  const dx=
    target.x-player.x;

  const dz=
    target.z-player.z;

  const d=
    Math.hypot(dx,dz);

  if(d<1)
    return false;

  const direction=
    normalize(dx,dz);

  ball.owner=null;

  ball.vx=
    direction.x*
    1.45+
    target.vx*.15;

  ball.vz=
    direction.z*
    1.45+
    target.vz*.15;

  return true;
}

/* =========================================================
   AI CARRIER
========================================================= */

function updateAICarrier(
  player,
  team,
  opponents,
  isPlayerTeam,
  dt
){

  const dir=
    isPlayerTeam
      ? 1
      : -1;

  const goalX=
    30*dir;

  const goalDistance=
    Math.abs(
      goalX-player.x
    );

  let nearestOpponent=null;

  let pressure=999;

  opponents
    .filter(
      o=>!o.isGK
    )
    .forEach(opponent=>{

      const d=
        distance(
          player,
          opponent
        );

      if(d<pressure){

        pressure=d;
        nearestOpponent=
          opponent;
      }
    });

  /*
     SCHUSS
  */

  if(
    goalDistance<17 &&
    Math.abs(player.z)<10 &&
    player.cooldown<=0
  ){

    const chance=
      goalDistance<10
        ? .65
        : .25;

    if(Math.random()<chance){

      aiShoot(
        player,
        isPlayerTeam
      );

      player.cooldown=35;

      return;
    }
  }

  /*
     PASS
  */

  if(
    player.cooldown<=0 &&
    pressure<5 &&
    Math.random()<.07
  ){

    const target=
      findBestPassTarget(
        player,
        team,
        isPlayerTeam
      );

    if(target){

      aiPass(
        player,
        target,
        isPlayerTeam
      );

      player.cooldown=30;

      return;
    }
  }

  /*
     DRIBBLING
  */

  let targetX=
    player.x+
    dir*8;

  let targetZ=
    player.z;

  /*
     Gegner ausweichen
  */

  if(
    nearestOpponent &&
    pressure<7
  ){

    const away=
      normalize(
        player.x-
        nearestOpponent.x,

        player.z-
        nearestOpponent.z
      );

    targetZ+=
      away.z*5;

    targetX+=
      away.x*2;
  }

  /*
     Leichte unregelmäßige
     Bewegung
  */

  targetZ+=
    Math.sin(
      performance.now()/500+
      player.index
    )*1.2;

  const direction=
    normalize(
      targetX-player.x,
      targetZ-player.z
    );

  player.vx+=
    (
      direction.x*
      player.maxSpeed-
      player.vx
    )*
    Math.min(1,5*dt);

  player.vz+=
    (
      direction.z*
      player.maxSpeed-
      player.vz
    )*
    Math.min(1,5*dt);

  player.x+=player.vx;
  player.z+=player.vz;

  player.x=
    clamp(
      player.x,
      -29,
      29
    );

  player.z=
    clamp(
      player.z,
      -19,
      19
    );
}

/* =========================================================
   AI DEFENSIVE TARGET
========================================================= */

function getDefensiveTarget(
  player,
  team,
  opponents,
  isPlayerTeam
){

  const tactical=
    tacticalPosition(
      player,
      isPlayerTeam
    );

  const dir=
    isPlayerTeam
      ? 1
      : -1;

  /*
     Gegner besitzt Ball
  */

  if(
    ball.owner &&
    ball.owner.isPlayer !==
    isPlayerTeam
  ){

    const carrier=
      ball.owner;

    /*
       Verteidiger
    */

    if(player.role==="DEF"){

      const candidates=
        opponents
          .filter(
            opponent=>
              !opponent.isGK
          )
          .sort(
            (a,b)=>
              distance(
                player,
                a
              )-
              distance(
                player,
                b
              )
          );

      const dangerous=
        candidates[0];

      /*
         Einer presst,
         die anderen sichern.
      */

      const closest=
        team
          .filter(
            p=>p.role==="DEF"
          )
          .sort(
            (a,b)=>
              distance(a,ball)-
              distance(b,ball)
          )[0];

      if(
        closest===player
      ){

        return {
          x:
            carrier.x-
            dir*1.5,

          z:
            carrier.z
        };

      }

      if(dangerous){

        return {
          x:
            dangerous.x-
            dir*1.5,

          z:
            dangerous.z
        };
      }
    }

    /*
       Mittelfeld schiebt zum Ball
    */

    if(player.role==="MID"){

      return {

        x:
          player.homeX+
          clamp(
            (
              ball.x-
              player.homeX
            )*.25,
            -4,
            4
          ),

        z:
          player.homeZ+
          clamp(
            (
              ball.z-
              player.homeZ
            )*.35,
            -5,
            5
          )
      };
    }
  }

  return tactical;
}

/* =========================================================
   AI PLAYER UPDATE
========================================================= */

function updateAIPlayer(
  player,
  team,
  opponents,
  isPlayerTeam,
  dt
){

  if(player.cooldown>0)
    player.cooldown--;

  /*
     Ballbesitzer
  */

  if(ball.owner===player){

    updateAICarrier(
      player,
      team,
      opponents,
      isPlayerTeam,
      dt
    );

    return;
  }

  /*
     Freier Ball
  */

  if(!ball.owner){

    const closest=
      team
        .filter(
          p=>!p.isGK
        )
        .sort(
          (a,b)=>
            distance(a,ball)-
            distance(b,ball)
        )[0];

    if(
      closest===player &&
      distance(player,ball)<13
    ){

      const direction=
        normalize(
          ball.x-player.x,
          ball.z-player.z
        );

      player.vx+=
        (
          direction.x*
          player.maxSpeed-
          player.vx
        )*
        Math.min(1,7*dt);

      player.vz+=
        (
          direction.z*
          player.maxSpeed-
          player.vz
        )*
        Math.min(1,7*dt);

      player.x+=player.vx;
      player.z+=player.vz;

      return;
    }
  }

  /*
     Zielposition
  */

  const defensive=
    getDefensiveTarget(
      player,
      team,
      opponents,
      isPlayerTeam
    );

  let targetX=
    defensive.x;

  let targetZ=
    defensive.z;

  /*
     Eigenes Team hat Ball:
     Mitspieler bieten sich an.
  */

  if(
    ball.owner &&
    ball.owner.isPlayer===
      isPlayerTeam
  ){

    const carrier=
      ball.owner;

    if(player!==carrier){

      const dir=
        isPlayerTeam
          ? 1
          : -1;

      /*
         Stürmer starten Tiefenläufe
      */

      if(player.role==="ST"){

        targetX=
          carrier.x+
          dir*
          randomRange(5,10);

        targetZ=
          carrier.z+
          randomRange(-6,6);
      }

      /*
         Mittelfeld bildet Dreiecke
      */

      else if(player.role==="MID"){

        targetX=
          carrier.x+
          randomRange(-4,5);

        targetZ=
          carrier.z+
          randomRange(-8,8);
      }

      /*
         Verteidiger sichern ab
      */

      else if(player.role==="DEF"){

        targetX=
          carrier.x-
          dir*5;

        targetZ=
          player.homeZ+
          (
            ball.z-
            player.homeZ
          )*.3;
      }
    }
  }

  /*
     Spielerabstand
  */

  let avoidX=0;
  let avoidZ=0;

  team.forEach(other=>{

    if(other===player)
      return;

    const dx=
      player.x-other.x;

    const dz=
      player.z-other.z;

    const d=
      Math.hypot(dx,dz);

    if(
      d<2.2 &&
      d>.01
    ){

      const strength=
        (2.2-d)/2.2;

      avoidX+=
        dx/d*
        strength;

      avoidZ+=
        dz/d*
        strength;
    }
  });

  targetX+=
    avoidX*2;

  targetZ+=
    avoidZ*2;

  /*
     Bewegung
  */

  const direction=
    normalize(
      targetX-player.x,
      targetZ-player.z
    );

  const targetVX=
    direction.x*
    player.speed;

  const targetVZ=
    direction.z*
    player.speed;

  player.vx+=
    (
      targetVX-
      player.vx
    )*
    Math.min(1,5*dt);

  player.vz+=
    (
      targetVZ-
      player.vz
    )*
    Math.min(1,5*dt);

  player.x+=player.vx;
  player.z+=player.vz;

  player.x=
    clamp(
      player.x,
      -29,
      29
    );

  player.z=
    clamp(
      player.z,
      -19,
      19
    );
}

/* =========================================================
   AI TACKLE
========================================================= */

function updateAITackles(){

  if(!ball.owner)
    return;

  const carrier=
    ball.owner;

  const defenders=
    carrier.isPlayer
      ? enemyTeam
      : playerTeam;

  let closest=null;
  let closestDistance=999;

  defenders.forEach(
    defender=>{

      if(defender.isGK)
        return;

      const d=
        distance(
          defender,
          carrier
        );

      if(d<closestDistance){

        closestDistance=d;
        closest=defender;
      }
    }
  );

  if(
    closest &&
    closestDistance<1.7 &&
    Math.random()<.08
  ){

    ball.owner=
      closest;

    closest.isPlayer=
      defenders===playerTeam;

    if(
      defenders===playerTeam
    ){

      controlledPlayerIndex=
        playerTeam.indexOf(
          closest
        );
    }
  }
}

/* =========================================================
   GOALKEEPER
========================================================= */

function updateGoalkeeper(
  goalkeeper,
  isPlayerTeam,
  dt
){

  if(!goalkeeper)
    return;

  const goalX=
    isPlayerTeam
      ? -27
      : 27;

  let targetX=goalX;

  let targetZ=
    clamp(
      ball.z,
      -5.5,
      5.5
    );

  /*
     Bei Angriff auf Tor
     Torwart kommt leicht heraus.
  */

  const danger=
    isPlayerTeam
      ? ball.x<-20
      : ball.x>20;

  if(
    danger &&
    !ball.owner
  ){

    targetX=
      isPlayerTeam
        ? -24
        : 24;
  }

  const dx=
    targetX-
    goalkeeper.x;

  const dz=
    targetZ-
    goalkeeper.z;

  goalkeeper.vx+=
    (
      dx*.05-
      goalkeeper.vx
    )*
    Math.min(1,5*dt);

  goalkeeper.vz+=
    (
      dz*.05-
      goalkeeper.vz
    )*
    Math.min(1,5*dt);

  goalkeeper.x+=
    goalkeeper.vx;

  goalkeeper.z+=
    goalkeeper.vz;

  if(isPlayerTeam){

    goalkeeper.x=
      clamp(
        goalkeeper.x,
        -28,
        -22
      );

  }else{

    goalkeeper.x=
      clamp(
        goalkeeper.x,
        22,
        28
      );
  }

  goalkeeper.z=
    clamp(
      goalkeeper.z,
      -6,
      6
    );

  /*
     Ball aufnehmen
  */

  if(
    !ball.owner &&
    distance(
      goalkeeper,
      ball
    )<2
  ){

    ball.owner=
      goalkeeper;

    goalkeeper.isPlayer=
      isPlayerTeam;

    if(isPlayerTeam){

      controlledPlayerIndex=0;
    }
  }
}

/* =========================================================
   HUMAN PASS
========================================================= */

function triggerPass(){

  if(
    !ball.owner ||
    !ball.owner.isPlayer
  ){
    return;
  }

  const owner=
    ball.owner;

  const target=
    findBestPassTarget(
      owner,
      playerTeam,
      true
    );

  if(!target)
    return;

  const direction=
    normalize(
      target.x-owner.x,
      target.z-owner.z
    );

  ball.owner=null;

  ball.vx=
    direction.x*
    1.65+
    target.vx*.2;

  ball.vz=
    direction.z*
    1.65+
    target.vz*.2;
}

/* =========================================================
   HUMAN SHOOT
========================================================= */

function triggerShoot(){

  if(
    !ball.owner ||
    !ball.owner.isPlayer
  ){
    return;
  }

  const owner=
    ball.owner;

  /*
     Ziel abhängig vom Joystick
  */

  let targetZ=
    clamp(
      owner.z-
      moveZ*3,
      -4,
      4
    );

  if(
    Math.abs(moveZ)>.15
  ){

    targetZ+=
      moveZ*2;
  }

  const angle=
    Math.atan2(
      targetZ-ball.z,
      30-ball.x
    );

  const distanceToGoal=
    Math.abs(
      30-owner.x
    );

  const power=
    clamp(
      2.2+
      (25-distanceToGoal)/20,
      2,
      3.3
    );

  ball.owner=null;

  ball.vx=
    Math.cos(angle)*
    power;

  ball.vz=
    Math.sin(angle)*
    power;
}

/* =========================================================
   HUMAN TACKLE
========================================================= */

function triggerTackle(){

  const active=
    playerTeam[
      controlledPlayerIndex
    ];

  if(!active)
    return;

  if(
    ball.owner &&
    !ball.owner.isPlayer
  ){

    const opponent=
      ball.owner;

    const d=
      distance(
        active,
        opponent
      );

    if(d<3){

      /*
         Tackling ist nicht
         immer automatisch erfolgreich.
      */

      const success=
        Math.random()<.82;

      if(success){

        ball.owner=
          active;

        active.isPlayer=true;

        controlledPlayerIndex=
          playerTeam.indexOf(
            active
          );

      }else{

        active.vx*=.3;
        active.vz*=.3;
      }
    }
  }
}

document
  .getElementById("passBtn")
  .addEventListener(
    "click",
    triggerPass
  );

document
  .getElementById("shootBtn")
  .addEventListener(
    "click",
    triggerShoot
  );

document
  .getElementById("tackleBtn")
  .addEventListener(
    "click",
    triggerTackle
  );

/* =========================================================
   BALL PHYSICS
========================================================= */

function updateBall(){

  /*
     Ball am Spieler
  */

  if(ball.owner){

    const owner=
      ball.owner;

    const facing=
      owner.vx!==0
        ? Math.sign(owner.vx)
        : owner.facing||1;

    ball.x=
      owner.x+
      facing*.95;

    ball.z=
      owner.z;

    return;
  }

  /*
     Freier Ball
  */

  ball.x+=ball.vx;
  ball.z+=ball.vz;

  ball.vx*=
    ball.friction;

  ball.vz*=
    ball.friction;

  /*
     Spieler kann Ball aufnehmen
  */

  let nearest=null;
  let nearestDistance=1.55;

  playerTeam.forEach(
    (player,index)=>{

      const d=
        distance(
          player,
          ball
        );

      if(d<nearestDistance){

        nearestDistance=d;
        nearest={
          player,
          index,
          human:true
        };
      }
    }
  );

  enemyTeam.forEach(
    (player,index)=>{

      const d=
        distance(
          player,
          ball
        );

      if(d<nearestDistance){

        nearestDistance=d;

        nearest={
          player,
          index,
          human:false
        };
      }
    }
  );

  if(nearest){

    ball.owner=
      nearest.player;

    nearest.player.isPlayer=
      nearest.human;

    if(nearest.human){

      controlledPlayerIndex=
        nearest.index;
    }
  }

  /*
     Seitenlinie
  */

  if(
    Math.abs(ball.z)>19.5
  ){

    ball.z=
      Math.sign(ball.z)*
      19.5;

    ball.vz*=-.65;
  }

  /*
     TOR LINKS
  */

  if(
    ball.x<-30 &&
    Math.abs(ball.z)<4
  ){

    scoreEnemy++;

    document
      .getElementById("e-score")
      .textContent=
      scoreEnemy;

    kickoffTeamIsPlayer=true;

    showGoal(
      "TOR FÜR GEGNER!"
    );

    resetPositions();

    return;
  }

  /*
     TOR RECHTS
  */

  if(
    ball.x>30 &&
    Math.abs(ball.z)<4
  ){

    scorePlayer++;

    document
      .getElementById("p-score")
      .textContent=
      scorePlayer;

    kickoffTeamIsPlayer=false;

    showGoal(
      "TOR FÜR DICH!"
    );

    resetPositions();

    return;
  }

  /*
     Ball zurück ins Feld
  */

  if(
    Math.abs(ball.x)>30
  ){

    ball.x=
      clamp(
        ball.x,
        -30,
        30
      );

    ball.vx*=-.5;
  }
}

/* =========================================================
   GOAL OVERLAY
========================================================= */

function showGoal(text){

  const overlay=
    document.getElementById(
      "goal-overlay"
    );

  document
    .getElementById("gt")
    .textContent=text;

  overlay.style.opacity="1";

  setTimeout(()=>{

    overlay.style.opacity="0";

  },1200);
}

/* =========================================================
   PHYSICS
========================================================= */

function updatePhysics(){

  if(!gameRunning)
    return;

  const now=
    performance.now();

  const dt=
    Math.min(
      .033,
      (now-lastTime)/1000
    );

  lastTime=now;

  frameCounter++;

  /*
     Spielzeit
  */

  if(
    frameCounter%6===0
  ){

    matchSeconds++;

    if(matchSeconds>=60){

      matchSeconds=0;
      matchMinutes++;
    }

    document
      .getElementById("match-time")
      .textContent=
      `${String(
        matchMinutes
      ).padStart(2,"0")}:`+
      `${String(
        matchSeconds
      ).padStart(2,"0")}`;
  }

  /*
     3 Minuten Match
  */

  if(matchMinutes>=3){

    finishMatch();

    return;
  }

  /*
     Spieler auswählen
  */

  if(
    !ball.owner ||
    !ball.owner.isPlayer
  ){

    switchToBestPlayer();

  }else{

    controlledPlayerIndex=
      playerTeam.indexOf(
        ball.owner
      );
  }

  /*
     Human Player
  */

  const active=
    playerTeam[
      controlledPlayerIndex
    ];

  if(active){

    moveHumanPlayer(
      active,
      dt
    );
  }

  /*
     Eigene KI-Spieler
  */

  playerTeam.forEach(
    (player,index)=>{

      if(
        index===
        controlledPlayerIndex
      ){
        return;
      }

      updateAIPlayer(
        player,
        playerTeam,
        enemyTeam,
        true,
        dt
      );
    }
  );

  /*
     Gegner-KI
  */

  enemyTeam.forEach(
    player=>{

      updateAIPlayer(
        player,
        enemyTeam,
        playerTeam,
        false,
        dt
      );
    }
  );

  /*
     Torwart
  */

  updateGoalkeeper(
    playerTeam[0],
    true,
    dt
  );

  updateGoalkeeper(
    enemyTeam[0],
    false,
    dt
  );

  /*
     Tackles
  */

  if(aiTick++%3===0){

    updateAITackles();
  }

  /*
     Schiedsrichter
  */

  const dx=
    ball.x-referee.x;

  const dz=
    ball.z-referee.z;

  const d=
    Math.hypot(dx,dz);

  if(d>4){

    referee.x+=
      dx/d*
      referee.speed;

    referee.z+=
      dz/d*
      referee.speed;
  }

  /*
     Ball
  */

  updateBall();
}

/* =========================================================
   MATCH FINISH
========================================================= */

function finishMatch(){

  gameRunning=false;

  document
    .getElementById("score-board")
    .style.display="none";

  document
    .getElementById("tournament-stage-text")
    .style.display="none";

  document
    .getElementById("controls")
    .style.display="none";

  /*
     Gruppenphase
  */

  if(
    tournamentStage==="GROUP"
  ){

    const me=
      groupStageData.find(
        t=>t.key===userTeamKey
      );

    const opponent=
      groupStageData.find(
        t=>t.key===currentOpponent
      );

    me.matchesPlayed++;
    opponent.matchesPlayed++;

    me.gd+=
      scorePlayer-scoreEnemy;

    opponent.gd+=
      scoreEnemy-scorePlayer;

    if(scorePlayer>scoreEnemy){

      me.pts+=3;

    }else if(
      scorePlayer<scoreEnemy
    ){

      opponent.pts+=3;

    }else{

      me.pts++;
      opponent.pts++;
    }

    /*
       Simulation der anderen
       Gruppenspiele
    */

    groupStageData.forEach(team=>{

      if(
        team.key!==userTeamKey &&
        team.key!==currentOpponent
      ){

        team.matchesPlayed++;

        team.pts+=
          Math.random()>.5
            ? 3
            : 1;
      }
    });

    updateTournamentHub();

    return;
  }

  /*
     KO
  */

  if(
    scorePlayer===
    scoreEnemy
  ){

    startPenaltyShootout();

    return;
  }

  if(scorePlayer>scoreEnemy){

    if(
      tournamentStage===
      "HALBFINALE"
    ){

      tournamentStage=
        "FINALE";

      const candidates=
        Object.keys(TEAMS)
          .filter(
            key=>
              key!==userTeamKey &&
              key!==currentOpponent
          );

      currentOpponent=
        candidates[
          Math.floor(
            Math.random()*
            candidates.length
          )
        ];

    }else{

      tournamentStage=
        "FINISHED";
    }

    updateTournamentHub();

  }else{

    alert(
      "Ausgeschieden!"
    );

    location.reload();
  }
}

/* =========================================================
   PENALTY SHOOTOUT
========================================================= */

let penaltyState={
  pScores:0,
  eScores:0,
  round:0,
  turn:true
};

function startPenaltyShootout(){

  penaltyMode=true;
  gameRunning=false;

  document
    .getElementById("menu-penalty")
    .style.display="flex";

  penaltyState={
    pScores:0,
    eScores:0,
    round:0,
    turn:true
  };

  updatePenaltyScreen();
}

function updatePenaltyScreen(){

  document
    .getElementById("penalty-teams")
    .textContent=
      `${userTeamKey} `+
      `${penaltyState.pScores}`+
      ` : `+
      `${penaltyState.eScores} `+
      `${currentOpponent}`;

  document
    .getElementById("penalty-info")
    .textContent=
      penaltyState.turn
        ? "Joystick zielen & SCHUSS!"
        : "Gegner schießt!";
}

function executePenaltyRound(){

  const target=
    moveZ<-.2
      ? "left"
      : moveZ>.2
        ? "right"
        : "center";

  if(penaltyState.turn){

    const dive=
      Math.random()<.45
        ? "left"
        : Math.random()<.9
          ? "right"
          : "center";

    if(target!==dive){

      penaltyState.pScores++;

      alert("TOR!");

    }else{

      alert("GEHALTEN!");
    }

  }else{

    const shoot=
      Math.random()<.45
        ? "left"
        : Math.random()<.9
          ? "right"
          : "center";

    const dive=
      Math.random()<.5
        ? "left"
        : Math.random()<.5
          ? "right"
          : "center";

    if(shoot!==dive){

      penaltyState.eScores++;

      alert(
        "Gegner trifft."
      );

    }else{

      alert("PARADE!");
    }
  }

  penaltyState.round++;

  penaltyState.turn=
    !penaltyState.turn;

  updatePenaltyScreen();

  /*
     Mindestens 5 Schützen
  */

  if(
    penaltyState.round>=10 ||
    (
      penaltyState.round>=6 &&
      Math.abs(
        penaltyState.pScores-
        penaltyState.eScores
      )>1
    )
  ){

    document
      .getElementById("menu-penalty")
      .style.display="none";

    penaltyMode=false;

    if(
      penaltyState.pScores>
      penaltyState.eScores
    ){

      /*
         Sieg im Elfmeterschießen
      */

      if(
        tournamentStage===
        "HALBFINALE"
      ){

        tournamentStage=
          "FINALE";

        const candidates=
          Object.keys(TEAMS)
            .filter(
              key=>
                key!==userTeamKey &&
                key!==currentOpponent
            );

        currentOpponent=
          candidates[
            Math.floor(
              Math.random()*
              candidates.length
            )
          ];

      }else{

        tournamentStage=
          "FINISHED";
      }

      updateTournamentHub();

    }else{

      alert(
        "Im Elfmeterschießen verloren!"
      );

      location.reload();
    }
  }
}

/* =========================================================
   MAIN LOOP
========================================================= */

function animate(now){

  updatePhysics();

  drawGame();

  requestAnimationFrame(
    animate
  );
}

/* =========================================================
   START
========================================================= */

function startGame(){

  lastTime=
    performance.now();

  triggerLoadingScreen(true);

  requestAnimationFrame(
    animate
  );
}

if(
  document.readyState===
  "loading"
){

  document.addEventListener(
    "DOMContentLoaded",
    startGame,
    {once:true}
  );

}else{

  startGame();
}
</script>

</body>
</html>

Game Source: Football Stars Deluxe

Creator: BlazeKoala99

Libraries: none

Complexity: complex (4708 lines, 61.8 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: football-stars-deluxe-blazekoala99" to link back to the original. Then publish at arcadelab.ai/publish.