Football Stars Deluxe
by BlazeKoala994708 lines61.8 KB
<!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.