Penguin Dash: Overdrive
by GlitchGlider513437 lines71.7 KB
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<meta name="theme-color" content="#06111f">
<title>Penguin Dash: Overdrive</title>
<style>
:root{
--cyan:#22d3ee;
--blue:#3b82f6;
--purple:#8b5cf6;
--green:#34d399;
--yellow:#facc15;
--red:#fb7185;
--bg:#020617;
--panel:rgba(8,18,35,.78);
--border:rgba(148,163,184,.16);
--text:#f8fafc;
--muted:#94a3b8;
}
*{
box-sizing:border-box;
-webkit-tap-highlight-color:transparent;
}
html,body{
width:100%;
height:100%;
margin:0;
overflow:hidden;
background:#020617;
color:var(--text);
font-family:
Inter,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
user-select:none;
}
button{
font:inherit;
}
.hidden{
opacity:0!important;
pointer-events:none!important;
visibility:hidden;
}
.screen{
position:fixed;
inset:0;
z-index:100;
display:flex;
justify-content:center;
align-items:center;
overflow:auto;
padding:20px;
background:
radial-gradient(circle at 20% 20%,rgba(34,211,238,.12),transparent 28%),
radial-gradient(circle at 80% 70%,rgba(139,92,246,.14),transparent 30%),
rgba(2,6,23,.88);
backdrop-filter:blur(18px);
transition:.35s ease;
}
.screen::before{
content:"";
position:absolute;
inset:0;
pointer-events:none;
opacity:.28;
background-image:
linear-gradient(rgba(255,255,255,.025) 1px,transparent 1px),
linear-gradient(90deg,rgba(255,255,255,.025) 1px,transparent 1px);
background-size:34px 34px;
}
.panel{
position:relative;
z-index:2;
width:min(94vw,560px);
max-height:92vh;
overflow:auto;
padding:26px;
border:1px solid var(--border);
border-radius:30px;
background:
linear-gradient(145deg,rgba(15,30,52,.92),rgba(4,11,24,.86));
box-shadow:
0 30px 90px rgba(0,0,0,.55),
inset 0 1px rgba(255,255,255,.06),
0 0 60px rgba(34,211,238,.05);
}
.logo{
margin:0;
text-align:center;
font-size:clamp(2.4rem,9vw,4.8rem);
line-height:.9;
font-weight:1000;
letter-spacing:-3px;
text-transform:uppercase;
text-shadow:
0 0 20px rgba(34,211,238,.4),
0 0 55px rgba(34,211,238,.15);
}
.logo span{
color:var(--cyan);
}
.subtitle{
text-align:center;
color:var(--muted);
margin:12px 0 24px;
letter-spacing:3px;
text-transform:uppercase;
font-size:.72rem;
font-weight:900;
}
.stats{
display:grid;
grid-template-columns:repeat(3,1fr);
gap:9px;
margin-bottom:16px;
}
.stat{
padding:12px 8px;
text-align:center;
border:1px solid var(--border);
border-radius:16px;
background:rgba(255,255,255,.035);
}
.stat b{
display:block;
color:var(--cyan);
font-size:1.2rem;
}
.stat small{
color:var(--muted);
font-size:.62rem;
text-transform:uppercase;
letter-spacing:1px;
}
.btn{
width:100%;
border:0;
border-radius:16px;
padding:14px 16px;
margin-top:9px;
color:#fff;
font-weight:900;
text-transform:uppercase;
letter-spacing:.8px;
cursor:pointer;
transition:.16s ease;
box-shadow:0 8px 22px rgba(0,0,0,.2);
}
.btn:hover{
transform:translateY(-2px);
filter:brightness(1.08);
}
.btn:active{
transform:scale(.97);
}
.btn:disabled{
opacity:.45;
cursor:not-allowed;
transform:none;
}
.primary{
background:linear-gradient(135deg,#06b6d4,#2563eb);
}
.secondary{
background:rgba(255,255,255,.07);
border:1px solid var(--border);
}
.gold{
background:linear-gradient(135deg,#eab308,#f97316);
}
.danger{
background:linear-gradient(135deg,#ef4444,#be123c);
}
.green{
background:linear-gradient(135deg,#10b981,#059669);
}
.menu-grid{
display:grid;
grid-template-columns:1fr 1fr;
gap:9px;
margin-top:9px;
}
.menu-grid .btn{
margin:0;
}
.section-title{
display:flex;
align-items:center;
justify-content:space-between;
margin-bottom:15px;
}
.section-title h2{
margin:0;
font-size:1.45rem;
}
.close{
width:40px;
height:40px;
border-radius:12px;
border:1px solid var(--border);
color:white;
background:rgba(255,255,255,.06);
cursor:pointer;
font-size:1.2rem;
}
/* LOADING */
#loadingScreen{
z-index:999;
overflow:hidden;
background:
radial-gradient(circle at 50% 42%,rgba(34,211,238,.18),transparent 25%),
radial-gradient(circle at 15% 80%,rgba(59,130,246,.13),transparent 28%),
#020617;
}
.loading{
position:relative;
z-index:2;
width:min(88vw,720px);
text-align:center;
}
.loading-badge{
display:inline-flex;
align-items:center;
gap:8px;
padding:7px 14px;
border-radius:999px;
border:1px solid rgba(34,211,238,.25);
color:#a5f3fc;
background:rgba(8,47,73,.5);
font-size:.68rem;
letter-spacing:2px;
font-weight:900;
}
.loading-badge i{
width:7px;
height:7px;
border-radius:50%;
background:var(--cyan);
box-shadow:0 0 14px var(--cyan);
animation:pulse 1s infinite;
}
.loading h1{
margin:25px 0 8px;
font-size:clamp(3rem,13vw,7.5rem);
line-height:.82;
letter-spacing:-5px;
font-weight:1000;
}
.loading h1 span{
color:var(--cyan);
}
.loading p{
color:var(--muted);
text-transform:uppercase;
letter-spacing:4px;
font-size:.78rem;
font-weight:800;
}
.loader-box{
margin-top:40px;
}
.loader{
height:30px;
padding:4px;
border-radius:999px;
background:#020617;
border:1px solid rgba(148,163,184,.22);
box-shadow:inset 0 0 15px #000;
overflow:hidden;
}
#loaderBar{
height:100%;
width:0%;
border-radius:999px;
background:linear-gradient(
90deg,
#0891b2,
#22d3ee,
#a5f3fc,
#fff
);
box-shadow:
0 0 15px rgba(34,211,238,.9),
0 0 35px rgba(34,211,238,.45);
position:relative;
}
#loaderBar::after{
content:"";
position:absolute;
top:0;
right:-100px;
width:100px;
height:100%;
background:linear-gradient(
90deg,
transparent,
rgba(255,255,255,.9),
transparent
);
animation:shine .7s linear infinite;
}
.loader-info{
display:flex;
justify-content:space-between;
align-items:center;
margin-top:12px;
}
#loaderStatus{
color:var(--cyan);
font-size:.75rem;
font-weight:900;
text-transform:uppercase;
letter-spacing:1px;
}
#loaderPercent{
font-size:1.8rem;
font-weight:1000;
}
.loading-penguin{
font-size:4rem;
margin-top:22px;
animation:float 1.5s ease-in-out infinite;
filter:drop-shadow(0 0 20px rgba(34,211,238,.5));
}
/* GAME */
#game{
position:fixed;
inset:0;
overflow:hidden;
background:
linear-gradient(
to bottom,
#06152b 0%,
#102a4c 52%,
#13283b 100%
);
}
#gameCanvas{
position:absolute;
inset:0;
width:100%;
height:100%;
touch-action:none;
}
#hud{
position:absolute;
z-index:20;
top:max(12px,env(safe-area-inset-top));
left:12px;
right:12px;
display:flex;
justify-content:space-between;
align-items:flex-start;
pointer-events:none;
}
.hud-left,
.hud-right{
display:flex;
gap:7px;
}
.hud-card{
min-width:75px;
padding:8px 11px;
border-radius:13px;
border:1px solid rgba(255,255,255,.1);
background:rgba(2,6,23,.62);
backdrop-filter:blur(10px);
text-align:center;
box-shadow:0 7px 20px rgba(0,0,0,.18);
}
.hud-card small{
display:block;
color:#94a3b8;
font-size:.58rem;
font-weight:900;
text-transform:uppercase;
}
.hud-card b{
display:block;
margin-top:2px;
color:#fff;
font-size:1rem;
}
#comboBox{
position:absolute;
z-index:21;
top:95px;
left:50%;
transform:translateX(-50%) scale(.8);
opacity:0;
pointer-events:none;
text-align:center;
transition:.2s ease;
}
#comboBox.show{
opacity:1;
transform:translateX(-50%) scale(1);
}
#comboNumber{
font-size:2.7rem;
font-weight:1000;
color:#facc15;
text-shadow:0 0 25px rgba(250,204,21,.55);
}
#comboText{
color:#fff;
font-size:.65rem;
letter-spacing:3px;
font-weight:900;
}
#powerupStatus{
position:absolute;
z-index:21;
left:50%;
bottom:28px;
transform:translateX(-50%);
display:flex;
gap:8px;
pointer-events:none;
}
.power-indicator{
display:none;
padding:8px 12px;
border-radius:999px;
background:rgba(2,6,23,.72);
border:1px solid rgba(255,255,255,.12);
font-weight:900;
font-size:.72rem;
backdrop-filter:blur(10px);
}
#pauseButton{
position:absolute;
z-index:30;
right:13px;
top:90px;
width:42px;
height:42px;
border-radius:13px;
border:1px solid rgba(255,255,255,.12);
color:#fff;
background:rgba(2,6,23,.6);
cursor:pointer;
}
/* SHOP */
.shop-tabs{
display:grid;
grid-template-columns:repeat(3,1fr);
gap:6px;
margin-bottom:13px;
}
.tab{
padding:10px 5px;
border:0;
border-radius:11px;
background:rgba(255,255,255,.05);
color:var(--muted);
font-weight:900;
cursor:pointer;
}
.tab.active{
background:var(--cyan);
color:#042f3a;
}
.shop-grid{
display:grid;
grid-template-columns:repeat(2,1fr);
gap:10px;
max-height:52vh;
overflow:auto;
padding-right:3px;
}
.item{
position:relative;
min-height:150px;
padding:13px;
border-radius:18px;
border:1px solid var(--border);
background:rgba(255,255,255,.035);
display:flex;
flex-direction:column;
justify-content:space-between;
text-align:center;
}
.item-icon{
font-size:2.8rem;
filter:drop-shadow(0 5px 10px rgba(0,0,0,.3));
}
.item-name{
font-weight:900;
font-size:.78rem;
}
.item-rarity{
font-size:.55rem;
color:var(--muted);
text-transform:uppercase;
letter-spacing:1px;
}
.item button{
border:0;
border-radius:9px;
padding:8px;
color:white;
background:linear-gradient(135deg,#06b6d4,#2563eb);
font-size:.68rem;
font-weight:900;
cursor:pointer;
}
.item button:disabled{
opacity:.45;
cursor:not-allowed;
}
/* QUESTS */
.quest-list{
display:flex;
flex-direction:column;
gap:8px;
max-height:58vh;
overflow:auto;
}
.quest{
padding:12px;
border-radius:15px;
border:1px solid var(--border);
background:rgba(255,255,255,.035);
}
.quest-top{
display:flex;
justify-content:space-between;
gap:10px;
}
.quest-title{
font-size:.78rem;
font-weight:900;
}
.quest-reward{
color:#facc15;
font-weight:1000;
white-space:nowrap;
}
.quest-bar{
height:6px;
margin-top:8px;
border-radius:99px;
background:#020617;
overflow:hidden;
}
.quest-progress{
height:100%;
background:linear-gradient(90deg,#06b6d4,#8b5cf6);
}
.quest.ready{
border-color:rgba(250,204,21,.5);
}
/* DAILY */
.daily-grid{
display:grid;
grid-template-columns:repeat(4,1fr);
gap:8px;
margin:15px 0;
}
.daily{
padding:10px 4px;
border-radius:12px;
border:1px solid var(--border);
background:rgba(255,255,255,.035);
text-align:center;
font-size:.7rem;
}
.daily.active{
border-color:var(--cyan);
box-shadow:0 0 18px rgba(34,211,238,.15);
}
.daily.claimed{
opacity:.45;
}
.reward-big{
text-align:center;
font-size:4rem;
margin:10px 0;
}
/* GAME OVER */
.result-score{
text-align:center;
font-size:4rem;
line-height:1;
font-weight:1000;
color:var(--cyan);
text-shadow:0 0 30px rgba(34,211,238,.3);
}
.result-label{
text-align:center;
color:var(--muted);
text-transform:uppercase;
letter-spacing:3px;
font-size:.65rem;
font-weight:900;
}
.result-grid{
display:grid;
grid-template-columns:repeat(3,1fr);
gap:8px;
margin:20px 0;
}
/* TOAST */
#toast{
position:fixed;
z-index:1000;
left:50%;
bottom:25px;
transform:translate(-50%,80px);
opacity:0;
padding:11px 17px;
border-radius:13px;
background:rgba(2,6,23,.92);
border:1px solid rgba(34,211,238,.3);
box-shadow:0 12px 35px rgba(0,0,0,.4);
font-size:.78rem;
font-weight:900;
transition:.3s ease;
pointer-events:none;
}
#toast.show{
transform:translate(-50%,0);
opacity:1;
}
/* PAUSE */
.pause-content{
text-align:center;
}
.pause-icon{
font-size:4rem;
margin-bottom:8px;
}
/* ANIMATIONS */
@keyframes pulse{
50%{
opacity:.4;
transform:scale(.75)
}
}
@keyframes shine{
from{
transform:translateX(-20px)
}
to{
transform:translateX(120px)
}
}
@keyframes float{
0%,100%{
transform:translateY(0) rotate(-2deg)
}
50%{
transform:translateY(-10px) rotate(2deg)
}
}
@keyframes pop{
0%{
transform:scale(.5);
opacity:0
}
70%{
transform:scale(1.12);
opacity:1
}
100%{
transform:scale(1);
opacity:1
}
}
.pop{
animation:pop .25s ease;
}
@media(max-width:500px){
.panel{
padding:19px;
border-radius:24px;
}
.menu-grid{
grid-template-columns:1fr;
}
.stats{
grid-template-columns:repeat(3,1fr);
}
.hud-card{
min-width:64px;
padding:7px 8px;
}
.hud-card b{
font-size:.85rem;
}
.shop-grid{
grid-template-columns:repeat(2,1fr);
}
.loading h1{
letter-spacing:-3px;
}
}
</style>
</head>
<body>
<div id="loadingScreen" class="screen">
<div class="loading">
<div class="loading-badge">
<i></i>
OVERDRIVE SYSTEM ONLINE
</div>
<h1>
PENGUIN <span>DASH</span>
</h1>
<p>
Overdrive Edition
</p>
<div class="loader-box">
<div class="loader">
<div id="loaderBar"></div>
</div>
<div class="loader-info">
<span id="loaderStatus">
Wird geladen...
</span>
<span id="loaderPercent">
0%
</span>
</div>
</div>
<div class="loading-penguin">
🐧
</div>
</div>
</div>
<div id="menuScreen" class="screen hidden">
<div class="panel">
<h1 class="logo">
PENGUIN <span>DASH</span>
</h1>
<div class="subtitle">
Overdrive Edition
</div>
<div class="stats">
<div class="stat">
<b id="menuScore">0</b>
<small>Highscore</small>
</div>
<div class="stat">
<b id="menuCoins">0</b>
<small>Coins</small>
</div>
<div class="stat">
<b id="menuLevel">1</b>
<small>Level</small>
</div>
</div>
<button class="btn primary" onclick="startGame()">
🐧 Spiel starten
</button>
<div class="menu-grid">
<button class="btn secondary" onclick="openPanel('shopScreen')">
🛍️ Shop
</button>
<button class="btn secondary" onclick="openQuests()">
🏆 Quests
</button>
<button class="btn secondary" onclick="openDaily()">
🎁 Daily
</button>
<button class="btn secondary" onclick="showControls()">
🎮 Steuerung
</button>
</div>
<div style="
margin-top:18px;
padding:12px;
border-radius:15px;
background:rgba(34,211,238,.05);
border:1px solid rgba(34,211,238,.1);
text-align:center;
color:#94a3b8;
font-size:.68rem;
">
SPACE / ↑ / TIPPEN = Springen<br>
Zweimal springen = Doppelsprung
</div>
</div>
</div>
<div id="shopScreen" class="screen hidden">
<div class="panel">
<div class="section-title">
<h2>🛍️ Shop</h2>
<button class="close" onclick="closePanel('shopScreen')">
×
</button>
</div>
<div style="
text-align:center;
margin-bottom:14px;
color:#facc15;
font-weight:1000;
">
🪙 <span id="shopCoins">0</span>
</div>
<div class="shop-tabs">
<button
id="tabSkins"
class="tab active"
onclick="shopTab('skins')">
Skins
</button>
<button
id="tabThemes"
class="tab"
onclick="shopTab('themes')">
Welten
</button>
<button
id="tabUpgrades"
class="tab"
onclick="shopTab('upgrades')">
Upgrades
</button>
</div>
<div id="shopGrid" class="shop-grid"></div>
</div>
</div>
<div id="questScreen" class="screen hidden">
<div class="panel">
<div class="section-title">
<h2>🏆 Tages-Quests</h2>
<button class="close" onclick="closePanel('questScreen')">
×
</button>
</div>
<div id="questList" class="quest-list"></div>
</div>
</div>
<div id="dailyScreen" class="screen hidden">
<div class="panel">
<div class="section-title">
<h2>🎁 Daily Reward</h2>
<button class="close" onclick="closePanel('dailyScreen')">
×
</button>
</div>
<div class="reward-big">
🎁
</div>
<div style="
text-align:center;
color:#94a3b8;
font-size:.78rem;
">
Komm jeden Tag zurück und erhöhe deine Belohnung!
</div>
<div id="dailyGrid" class="daily-grid"></div>
<button
id="dailyButton"
class="btn gold"
onclick="claimDaily()">
Belohnung abholen
</button>
</div>
</div>
<div id="game">
<canvas id="gameCanvas"></canvas>
<div id="hud">
<div class="hud-left">
<div class="hud-card">
<small>Score</small>
<b id="hudScore">0</b>
</div>
<div class="hud-card">
<small>Coins</small>
<b id="hudCoins">0</b>
</div>
</div>
<div class="hud-right">
<div class="hud-card">
<small>Level</small>
<b id="hudLevel">1</b>
</div>
</div>
</div>
<div id="comboBox">
<div id="comboNumber">
x2
</div>
<div id="comboText">
COMBO
</div>
</div>
<div id="powerupStatus">
<div id="shieldPower" class="power-indicator">
🛡️ Schild
</div>
<div id="magnetPower" class="power-indicator">
🧲 Magnet
</div>
<div id="slowPower" class="power-indicator">
❄️ Zeitlupe
</div>
</div>
<button id="pauseButton" onclick="togglePause()">
⏸
</button>
</div>
<div id="pauseScreen" class="screen hidden">
<div class="panel pause-content">
<div class="pause-icon">
⏸️
</div>
<h2>Pause</h2>
<p style="color:#94a3b8">
Dein Run wartet auf dich.
</p>
<button class="btn primary" onclick="togglePause()">
▶ Weiter
</button>
<button class="btn secondary" onclick="quitGame()">
Menü
</button>
</div>
</div>
<div id="gameOverScreen" class="screen hidden">
<div class="panel">
<div class="result-label">
RUN BEENDET
</div>
<div id="finalScore" class="result-score">
0
</div>
<div class="result-label">
Punkte
</div>
<div class="result-grid">
<div class="stat">
<b id="finalCoins">0</b>
<small>Coins</small>
</div>
<div class="stat">
<b id="finalCombo">0</b>
<small>Combo</small>
</div>
<div class="stat">
<b id="finalLevel">1</b>
<small>Level</small>
</div>
</div>
<div id="newRecord" style="
display:none;
text-align:center;
color:#facc15;
font-weight:1000;
margin:12px;
">
🏆 NEUER HIGHSCORE!
</div>
<button class="btn primary" onclick="startGame()">
🔄 Nochmal
</button>
<button class="btn secondary" onclick="goMenu()">
🏠 Hauptmenü
</button>
</div>
</div>
<div id="toast"></div>
<script>
"use strict";
/* =====================================================
DATA
===================================================== */
const SKINS = [
{id:"🐧",name:"Classic",cost:0,rarity:"Common"},
{id:"🕶️",name:"Cool Penguin",cost:60,rarity:"Rare"},
{id:"🥷",name:"Ninja",cost:120,rarity:"Rare"},
{id:"👑",name:"Penguin King",cost:250,rarity:"Epic"},
{id:"🚀",name:"Astronaut",cost:400,rarity:"Epic"},
{id:"🤖",name:"Cyber Penguin",cost:650,rarity:"Legendary"},
{id:"🔥",name:"Inferno",cost:900,rarity:"Legendary"},
{id:"💎",name:"Diamond",cost:1400,rarity:"Legendary"},
{id:"👻",name:"Ghost",cost:1800,rarity:"Mythic"},
{id:"🐉",name:"Dragon",cost:2500,rarity:"Mythic"}
];
const THEMES = [
{
id:"arctic",
name:"Arctic",
cost:0,
sky1:"#06152b",
sky2:"#17456a",
ground:"#173044"
},
{
id:"neon",
name:"Neon City",
cost:500,
sky1:"#16062b",
sky2:"#43115f",
ground:"#180e2c"
},
{
id:"sunset",
name:"Sunset",
cost:800,
sky1:"#43111b",
sky2:"#b84d24",
ground:"#35191c"
},
{
id:"void",
name:"Void",
cost:1500,
sky1:"#02020c",
sky2:"#20134d",
ground:"#0c0a1d"
}
];
const QUESTS = [
{
text:"Erreiche 100 Punkte",
type:"score",
goal:100,
reward:50
},
{
text:"Sammle 15 Coins",
type:"coins",
goal:15,
reward:70
},
{
text:"Erreiche einen x5 Combo",
type:"combo",
goal:5,
reward:80
},
{
text:"Springe 25 Mal",
type:"jumps",
goal:25,
reward:60
},
{
text:"Erreiche 250 Punkte",
type:"score",
goal:250,
reward:150
},
{
text:"Sammle 35 Coins",
type:"coins",
goal:35,
reward:180
}
];
const DAILY_REWARDS=[
25,40,60,90,130,180,300
];
/* =====================================================
SAVE SYSTEM
===================================================== */
const SAVE_KEY="penguinDashSave";
const DEFAULT_SAVE={
version:2,
coins:0,
highscore:0,
level:1,
xp:0,
skin:"🐧",
ownedSkins:["🐧"],
theme:"arctic",
ownedThemes:["arctic"],
coinUpgrade:1,
jumpUpgrade:1,
dailyDay:0,
lastDaily:"",
questDay:"",
quests:[]
};
function cloneDefaultSave(){
return JSON.parse(
JSON.stringify(DEFAULT_SAVE)
);
}
function loadSave(){
try{
const raw=
localStorage.getItem(SAVE_KEY);
if(!raw){
return cloneDefaultSave();
}
const parsed=
JSON.parse(raw);
if(
!parsed ||
typeof parsed!=="object"
){
return cloneDefaultSave();
}
/*
* Alte Versionen werden automatisch
* mit den neuen Standardwerten ergänzt.
*/
const result={
...cloneDefaultSave(),
...parsed
};
if(
!Array.isArray(
result.ownedSkins
)
){
result.ownedSkins=["🐧"];
}
if(
!result.ownedSkins.includes("🐧")
){
result.ownedSkins.unshift("🐧");
}
if(
!Array.isArray(
result.ownedThemes
)
){
result.ownedThemes=["arctic"];
}
if(
!result.ownedThemes.includes("arctic")
){
result.ownedThemes.unshift("arctic");
}
if(
!Array.isArray(result.quests)
){
result.quests=[];
}
if(
!Number.isFinite(result.coins)
){
result.coins=0;
}
if(
!Number.isFinite(result.highscore)
){
result.highscore=0;
}
if(
!Number.isFinite(result.level) ||
result.level<1
){
result.level=1;
}
if(
!Number.isFinite(result.xp) ||
result.xp<0
){
result.xp=0;
}
if(
!Number.isFinite(result.coinUpgrade) ||
result.coinUpgrade<1
){
result.coinUpgrade=1;
}
if(
!Number.isFinite(result.jumpUpgrade) ||
result.jumpUpgrade<1
){
result.jumpUpgrade=1;
}
if(
!Number.isFinite(result.dailyDay) ||
result.dailyDay<0
){
result.dailyDay=0;
}
if(
!result.skin ||
!result.ownedSkins.includes(result.skin)
){
result.skin="🐧";
}
if(
!result.theme ||
!result.ownedThemes.includes(result.theme)
){
result.theme="arctic";
}
result.version=2;
return result;
}catch(error){
console.error(
"Fehler beim Laden des Spielstands:",
error
);
return cloneDefaultSave();
}
}
let save=loadSave();
function persist(){
try{
localStorage.setItem(
SAVE_KEY,
JSON.stringify(save)
);
return true;
}catch(error){
console.error(
"Speichern fehlgeschlagen:",
error
);
showToast(
"⚠️ Speichern fehlgeschlagen!"
);
return false;
}
}
/*
* Sofort nach dem Laden einmal speichern.
* Dadurch werden alte Spielstände automatisch
* auf die neue Struktur aktualisiert.
*/
persist();
/* =====================================================
AUTO SAVE
===================================================== */
let lastAutoSave=performance.now();
const AUTO_SAVE_INTERVAL=2000;
function autoSave(){
const now=performance.now();
if(
now-lastAutoSave>=AUTO_SAVE_INTERVAL
){
persist();
lastAutoSave=now;
}
}
function saveBeforeLeaving(){
/*
* Run-Coins werden dabei ebenfalls übernommen.
* Damit gehen gesammelte Coins beim Reload
* nicht verloren.
*/
if(
gameRunning &&
runCoins>0
){
save.coins+=runCoins;
runCoins=0;
}
persist();
}
window.addEventListener(
"beforeunload",
saveBeforeLeaving
);
document.addEventListener(
"visibilitychange",
()=>{
if(
document.visibilityState==="hidden"
){
saveBeforeLeaving();
}
}
);
/* =====================================================
DOM
===================================================== */
const loadingScreen=
document.getElementById("loadingScreen");
const menuScreen=
document.getElementById("menuScreen");
const gameOverScreen=
document.getElementById("gameOverScreen");
const canvas=
document.getElementById("gameCanvas");
const ctx=
canvas.getContext("2d");
const game=
document.getElementById("game");
/* =====================================================
LOADING
===================================================== */
const loaderBar=
document.getElementById("loaderBar");
const loaderPercent=
document.getElementById("loaderPercent");
const loaderStatus=
document.getElementById("loaderStatus");
const loaderStart=
performance.now();
const FILL_TIME=2500;
const HOLD_99=3000;
const HOLD_100=1500;
function loadingLoop(now){
const elapsed=
now-loaderStart;
if(elapsed<FILL_TIME){
const p=
elapsed/FILL_TIME;
const percent=
Math.floor(p*99);
loaderBar.style.width=
percent+"%";
loaderPercent.textContent=
percent+"%";
loaderStatus.textContent=
"Wird geladen...";
requestAnimationFrame(
loadingLoop
);
return;
}
if(
elapsed<
FILL_TIME+HOLD_99
){
loaderBar.style.width="99%";
loaderPercent.textContent="99%";
loaderStatus.textContent=
"Overdrive wird initialisiert...";
requestAnimationFrame(
loadingLoop
);
return;
}
loaderBar.style.width="100%";
loaderPercent.textContent="100%";
loaderStatus.textContent=
"Fertig!";
if(
elapsed<
FILL_TIME+
HOLD_99+
HOLD_100
){
requestAnimationFrame(
loadingLoop
);
return;
}
loadingScreen.classList.add(
"hidden"
);
menuScreen.classList.remove(
"hidden"
);
updateMenu();
prepareQuests();
applyTheme();
}
requestAnimationFrame(
loadingLoop
);
/* =====================================================
MENU
===================================================== */
function updateMenu(){
document.getElementById(
"menuScore"
).textContent=
Math.floor(save.highscore);
document.getElementById(
"menuCoins"
).textContent=
save.coins;
document.getElementById(
"menuLevel"
).textContent=
save.level;
}
function openPanel(id){
document
.getElementById(id)
.classList.remove("hidden");
if(id==="shopScreen"){
shopTab("skins");
document.getElementById(
"shopCoins"
).textContent=
save.coins;
}
if(id==="questScreen"){
prepareQuests();
renderQuests();
}
if(id==="dailyScreen"){
renderDaily();
}
}
function closePanel(id){
document
.getElementById(id)
.classList.add("hidden");
}
function goMenu(){
/*
* Alles sichern, bevor der Run verlassen wird.
*/
saveBeforeLeaving();
gameRunning=false;
paused=false;
gameOverScreen.classList.add(
"hidden"
);
document
.getElementById("pauseScreen")
.classList.add("hidden");
menuScreen.classList.remove(
"hidden"
);
updateMenu();
}
/* =====================================================
CONTROLS
===================================================== */
function showControls(){
showToast(
"Tippen / SPACE / ↑ = Sprung • zweimal = Doppelsprung"
);
}
/* =====================================================
GAME VARIABLES
===================================================== */
let W=innerWidth;
let H=innerHeight;
let gameRunning=false;
let paused=false;
let animationId=0;
let player;
let obstacles=[];
let coins=[];
let powerups=[];
let particles=[];
let snow=[];
let mountains=[];
let score=0;
let runCoins=0;
let jumps=0;
let combo=0;
let maxCombo=0;
let comboTimer=0;
let speed=5;
let level=1;
let spawnTimer=0;
let coinTimer=0;
let powerTimer=0;
let shake=0;
let powerState={
shield:0,
magnet:0,
slow:0
};
/* =====================================================
RESIZE
===================================================== */
function resize(){
W=innerWidth;
H=innerHeight;
const dpr=
Math.min(
devicePixelRatio||1,
2
);
canvas.width=W*dpr;
canvas.height=H*dpr;
canvas.style.width=
W+"px";
canvas.style.height=
H+"px";
ctx.setTransform(
dpr,
0,
0,
dpr,
0,
0
);
createBackground();
}
window.addEventListener(
"resize",
resize
);
/* =====================================================
BACKGROUND
===================================================== */
function createBackground(){
snow=[];
for(let i=0;i<90;i++){
snow.push({
x:Math.random()*W,
y:Math.random()*H,
size:
Math.random()*2.5+.5,
speed:
Math.random()*1.4+.3,
drift:
Math.random()*1.2-.6
});
}
mountains=[];
for(let i=0;i<8;i++){
mountains.push({
x:i*(W/5),
width:
W/3+
Math.random()*W/5,
height:
80+
Math.random()*150
});
}
}
/* =====================================================
THEME
===================================================== */
function getTheme(){
return THEMES.find(
t=>t.id===save.theme
)||THEMES[0];
}
function applyTheme(){
const t=getTheme();
game.style.background=
`linear-gradient(to bottom,${t.sky1},${t.sky2} 60%,${t.ground})`;
}
/* =====================================================
START GAME
===================================================== */
function startGame(){
/*
* Eventuelle Run-Coins aus einem vorherigen
* unterbrochenen Run werden gesichert.
*/
if(runCoins>0){
save.coins+=runCoins;
runCoins=0;
persist();
}
menuScreen.classList.add(
"hidden"
);
gameOverScreen.classList.add(
"hidden"
);
score=0;
runCoins=0;
jumps=0;
combo=0;
maxCombo=0;
comboTimer=0;
level=1;
speed=5;
spawnTimer=0;
coinTimer=0;
powerTimer=0;
shake=0;
obstacles=[];
coins=[];
powerups=[];
particles=[];
powerState={
shield:0,
magnet:0,
slow:0
};
player={
x:W*.18,
y:H*.68,
width:48,
height:48,
vy:0,
gravity:.75,
jumpPower:-13.5,
jumps:0,
rotation:0
};
gameRunning=true;
paused=false;
lastTime=performance.now();
lastAutoSave=performance.now();
applyTheme();
updateHUD();
cancelAnimationFrame(
animationId
);
animationId=
requestAnimationFrame(
gameLoop
);
}
/* =====================================================
PLAYER JUMP
===================================================== */
function jump(){
if(
!gameRunning||
paused
){
return;
}
if(player.jumps<2){
player.vy=
player.jumpPower-
(save.jumpUpgrade-1)*.7;
player.jumps++;
jumps++;
burst(
player.x+20,
player.y+42,
"#67e8f9",
8
);
shake=2;
}
}
window.addEventListener(
"keydown",
e=>{
if(
e.code==="Space"||
e.code==="ArrowUp"||
e.code==="KeyW"
){
e.preventDefault();
jump();
}
if(
e.code==="Escape"
){
togglePause();
}
}
);
canvas.addEventListener(
"pointerdown",
e=>{
if(
gameRunning&&
!paused
){
e.preventDefault();
jump();
}
}
);
/* =====================================================
PAUSE
===================================================== */
function togglePause(){
if(!gameRunning){
return;
}
paused=!paused;
document
.getElementById(
"pauseScreen"
)
.classList.toggle(
"hidden",
!paused
);
/*
* Auch beim Pausieren speichern.
*/
persist();
if(!paused){
lastTime=performance.now();
animationId=
requestAnimationFrame(
gameLoop
);
}
}
/* =====================================================
QUIT
===================================================== */
function quitGame(){
saveBeforeLeaving();
gameRunning=false;
paused=false;
document
.getElementById(
"pauseScreen"
)
.classList.add("hidden");
menuScreen.classList.remove(
"hidden"
);
updateMenu();
}
/* =====================================================
GAME LOOP
===================================================== */
let lastTime=performance.now();
function gameLoop(now){
if(
!gameRunning||
paused
){
return;
}
const dt=
Math.min(
(now-lastTime)/16.67,
2
);
lastTime=now;
update(dt);
draw();
/*
* Automatisches Speichern.
*/
autoSave();
animationId=
requestAnimationFrame(
gameLoop
);
}
/* =====================================================
UPDATE
===================================================== */
function update(dt){
const slowFactor=
powerState.slow>0
?.58
:1;
score+=
.11*
dt*
speed*
slowFactor;
const newLevel=
Math.floor(score/100)+1;
if(newLevel>level){
level=newLevel;
burst(
player.x,
player.y,
"#a78bfa",
25
);
showToast(
"LEVEL "+level+"! ⚡"
);
}
speed=
Math.min(
13,
5+
score*.006
);
player.vy+=
player.gravity*dt;
player.y+=
player.vy*dt;
player.rotation=
Math.max(
-.25,
Math.min(
.25,
player.vy*.025
)
);
const ground=
H*.80;
if(
player.y+
player.height>=ground
){
player.y=
ground-
player.height;
player.vy=0;
player.jumps=0;
}
spawnTimer+=dt;
coinTimer+=dt;
powerTimer+=dt;
const spawnRate=
Math.max(
27,
70-
speed*3.2
);
if(
spawnTimer>
spawnRate
){
spawnTimer=0;
spawnObstacle();
}
if(
coinTimer>34
){
coinTimer=0;
spawnCoinPattern();
}
if(
powerTimer>600
){
powerTimer=0;
if(Math.random()<.7){
spawnPowerup();
}
}
updateObstacles(
dt,
slowFactor
);
updateCoins(
dt,
slowFactor
);
updatePowerups(
dt,
slowFactor
);
updateParticles(dt);
for(const key in powerState){
if(powerState[key]>0){
powerState[key]-=dt;
}
if(powerState[key]<0){
powerState[key]=0;
}
}
updatePowerUI();
if(combo>0){
comboTimer-=dt;
if(comboTimer<=0){
combo=0;
updateCombo();
}
}
shake*=.88;
updateHUD();
checkQuestProgress();
/*
* Quest-Fortschritt wird ebenfalls
* automatisch gesichert.
*/
if(
save.quests.length &&
Math.floor(nowSafe())%10===0
){
persist();
}
}
function nowSafe(){
return performance.now()/1000;
}
/* =====================================================
OBSTACLES
===================================================== */
function spawnObstacle(){
const types=[
"ice",
"rock",
"spike",
"crate"
];
const type=
types[
Math.floor(
Math.random()*types.length
)
];
const h=
type==="spike"
?45
:38+
Math.random()*25;
obstacles.push({
x:W+50,
y:H*.80-h,
width:
type==="spike"
?42
:42+
Math.random()*18,
height:h,
type,
passed:false
});
if(
score>180&&
Math.random()<.16
){
obstacles.push({
x:W+150,
y:H*.80-42,
width:42,
height:42,
type:"rock",
passed:false
});
}
}
function updateObstacles(
dt,
slow
){
for(
let i=obstacles.length-1;
i>=0;
i--
){
const o=
obstacles[i];
o.x-=
speed*
slow*
dt;
if(
!o.passed&&
o.x+o.width<
player.x
){
o.passed=true;
combo++;
maxCombo=
Math.max(
maxCombo,
combo
);
comboTimer=110;
updateCombo();
score+=
1+
Math.min(
10,
combo*.15
);
if(
combo%5===0
){
burst(
player.x+20,
player.y,
"#facc15",
15
);
showToast(
"COMBO x"+
combo+
"! 🔥"
);
}
}
if(
collides(
player,
o
)
){
if(
powerState.shield>0
){
powerState.shield=0;
burst(
player.x+20,
player.y+20,
"#38bdf8",
30
);
shake=9;
obstacles.splice(
i,
1
);
showToast(
"Schild gerettet! 🛡️"
);
continue;
}
gameOver();
return;
}
if(
o.x+o.width<
-80
){
obstacles.splice(
i,
1
);
}
}
}
/* =====================================================
DRAW OBSTACLE
===================================================== */
function drawObstacle(o){
ctx.save();
if(o.type==="spike"){
ctx.fillStyle=
"#fb7185";
ctx.beginPath();
ctx.moveTo(
o.x,
o.y+o.height
);
ctx.lineTo(
o.x+o.width/2,
o.y
);
ctx.lineTo(
o.x+o.width,
o.y+o.height
);
ctx.closePath();
ctx.fill();
ctx.shadowColor=
"#fb7185";
ctx.shadowBlur=18;
ctx.fill();
}else{
const colors={
ice:"#67e8f9",
rock:"#64748b",
crate:"#d97706"
};
ctx.fillStyle=
colors[o.type]||
"#64748b";
roundRect(
ctx,
o.x,
o.y,
o.width,
o.height,
8
);
ctx.fill();
if(
o.type==="ice"
){
ctx.strokeStyle=
"rgba(255,255,255,.5)";
ctx.lineWidth=2;
ctx.beginPath();
ctx.moveTo(
o.x+8,
o.y+8
);
ctx.lineTo(
o.x+o.width-8,
o.y+o.height-8
);
ctx.stroke();
}
}
ctx.restore();
}
/* =====================================================
COINS
===================================================== */
function spawnCoinPattern(){
const count=
3+
Math.floor(
Math.random()*4
);
const baseY=
H*.80-
80-
Math.random()*110;
for(
let i=0;
i<count;
i++
){
coins.push({
x:
W+
40+
i*38,
y:
baseY+
Math.sin(i*.8)*18,
size:24,
rotation:
Math.random()*6
});
}
}
function updateCoins(
dt,
slow
){
for(
let i=coins.length-1;
i>=0;
i--
){
const c=coins[i];
if(
powerState.magnet>0
){
const dx=
player.x+
24-
c.x;
const dy=
player.y+
24-
c.y;
const dist=
Math.hypot(
dx,
dy
);
if(
dist<220
){
c.x+=
dx*
.07*
dt;
c.y+=
dy*
.07*
dt;
}
}
c.x-=
speed*
slow*
dt;
c.rotation+=
.08*
dt;
if(
collidesCircle(
player,
c
)
){
collectCoin(c);
coins.splice(
i,
1
);
continue;
}
if(
c.x<-50
){
coins.splice(
i,
1
);
}
}
}
function collectCoin(c){
const multiplier=
1+
Math.floor(
(save.coinUpgrade-1)*
.5
);
runCoins+=
multiplier;
combo=
Math.max(
combo+1,
1
);
maxCombo=
Math.max(
maxCombo,
combo
);
comboTimer=100;
burst(
c.x,
c.y,
"#facc15",
12
);
updateCombo();
}
/* =====================================================
POWERUPS
===================================================== */
function spawnPowerup(){
const types=[
"shield",
"magnet",
"slow"
];
const type=
types[
Math.floor(
Math.random()*
types.length
)
];
powerups.push({
x:W+40,
y:
H*.55+
Math.random()*100,
size:31,
type,
rotation:0
});
}
function updatePowerups(
dt,
slow
){
for(
let i=powerups.length-1;
i>=0;
i--
){
const p=
powerups[i];
p.x-=
speed*
slow*
dt;
p.rotation+=
.05*
dt;
if(
collidesCircle(
player,
p
)
){
powerState[p.type]=
p.type==="shield"
?480
:360;
burst(
p.x,
p.y,
p.type==="shield"
?"#38bdf8"
:p.type==="magnet"
?"#f472b6"
:"#a78bfa",
20
);
showToast(
p.type==="shield"
?"🛡️ Schild aktiviert!"
:p.type==="magnet"
?"🧲 Magnet aktiviert!"
:"❄️ Zeitlupe aktiviert!"
);
powerups.splice(
i,
1
);
continue;
}
if(
p.x<-60
){
powerups.splice(
i,
1
);
}
}
}
/* =====================================================
PARTICLES
===================================================== */
function burst(
x,
y,
color,
count
){
for(
let i=0;
i<count;
i++
){
particles.push({
x,
y,
vx:
(Math.random()-.5)*7,
vy:
(Math.random()-.5)*7,
life:1,
size:
2+
Math.random()*4,
color
});
}
}
function updateParticles(dt){
for(
let i=particles.length-1;
i>=0;
i--
){
const p=
particles[i];
p.x+=
p.vx*
dt;
p.y+=
p.vy*
dt;
p.vy+=
.12*
dt;
p.life-=
.035*
dt;
if(
p.life<=0
){
particles.splice(
i,
1
);
}
}
}
/* =====================================================
DRAW
===================================================== */
function draw(){
const t=getTheme();
ctx.save();
if(shake>0){
ctx.translate(
(Math.random()-.5)*
shake,
(Math.random()-.5)*
shake
);
}
const gradient=
ctx.createLinearGradient(
0,
0,
0,
H
);
gradient.addColorStop(
0,
t.sky1
);
gradient.addColorStop(
.65,
t.sky2
);
gradient.addColorStop(
1,
t.ground
);
ctx.fillStyle=gradient;
ctx.fillRect(
0,
0,
W,
H
);
drawMoon();
drawMountains();
drawSnow();
const ground=
H*.80;
ctx.fillStyle=
t.ground;
ctx.fillRect(
0,
ground,
W,
H-ground
);
ctx.strokeStyle=
"rgba(103,232,249,.8)";
ctx.lineWidth=3;
ctx.beginPath();
ctx.moveTo(
0,
ground
);
ctx.lineTo(
W,
ground
);
ctx.stroke();
for(
const o of obstacles
){
drawObstacle(o);
}
for(
const c of coins
){
drawCoin(c);
}
for(
const p of powerups
){
drawPowerup(p);
}
drawPlayer();
drawParticles();
ctx.restore();
}
/* =====================================================
BACKGROUND DRAW
===================================================== */
function drawMoon(){
ctx.save();
ctx.fillStyle=
"rgba(186,230,253,.14)";
ctx.shadowColor=
"rgba(186,230,253,.3)";
ctx.shadowBlur=35;
ctx.beginPath();
ctx.arc(
W*.78,
H*.19,
Math.min(W,H)*.055,
0,
Math.PI*2
);
ctx.fill();
ctx.restore();
}
function drawMountains(){
ctx.save();
ctx.fillStyle=
"rgba(2,6,23,.25)";
for(
const m of mountains
){
ctx.beginPath();
ctx.moveTo(
m.x,
H*.80
);
ctx.lineTo(
m.x+
m.width*.5,
H*.80-
m.height
);
ctx.lineTo(
m.x+
m.width,
H*.80
);
ctx.closePath();
ctx.fill();
}
ctx.restore();
}
function drawSnow(){
ctx.fillStyle=
"rgba(255,255,255,.55)";
for(
const s of snow
){
s.x+=
s.drift*.15;
s.y+=
s.speed;
if(s.y>H){
s.y=-5;
s.x=
Math.random()*W;
}
if(s.x>W){
s.x=0;
}
if(s.x<0){
s.x=W;
}
ctx.beginPath();
ctx.arc(
s.x,
s.y,
s.size,
0,
Math.PI*2
);
ctx.fill();
}
}
/* =====================================================
PLAYER DRAW
===================================================== */
function drawPlayer(){
ctx.save();
ctx.translate(
player.x+24,
player.y+24
);
ctx.rotate(
player.rotation
);
if(
powerState.shield>0
){
ctx.beginPath();
ctx.arc(
0,
0,
37+
Math.sin(
performance.now()*.008
)*3,
0,
Math.PI*2
);
ctx.strokeStyle=
"rgba(56,189,248,.85)";
ctx.lineWidth=3;
ctx.shadowColor=
"#38bdf8";
ctx.shadowBlur=20;
ctx.stroke();
}
ctx.font=
"46px sans-serif";
ctx.textAlign="center";
ctx.textBaseline="middle";
ctx.fillText(
save.skin,
0,
1
);
ctx.restore();
}
function drawCoin(c){
ctx.save();
ctx.translate(
c.x,
c.y
);
ctx.rotate(
Math.sin(c.rotation)
);
ctx.font=
`${c.size}px sans-serif`;
ctx.textAlign="center";
ctx.textBaseline="middle";
ctx.shadowColor=
"#facc15";
ctx.shadowBlur=14;
ctx.fillText(
"🪙",
0,
0
);
ctx.restore();
}
function drawPowerup(p){
const icons={
shield:"🛡️",
magnet:"🧲",
slow:"❄️"
};
ctx.save();
ctx.translate(
p.x,
p.y
);
ctx.rotate(
Math.sin(p.rotation)*
.12
);
ctx.font=
`${p.size}px sans-serif`;
ctx.textAlign="center";
ctx.textBaseline="middle";
ctx.shadowColor=
p.type==="shield"
?"#38bdf8"
:p.type==="magnet"
?"#f472b6"
:"#a78bfa";
ctx.shadowBlur=18;
ctx.fillText(
icons[p.type],
0,
0
);
ctx.restore();
}
function drawParticles(){
for(
const p of particles
){
ctx.save();
ctx.globalAlpha=
Math.max(
0,
p.life
);
ctx.fillStyle=
p.color;
ctx.beginPath();
ctx.arc(
p.x,
p.y,
p.size,
0,
Math.PI*2
);
ctx.fill();
ctx.restore();
}
}
/* =====================================================
COLLISION
===================================================== */
function collides(a,b){
const pad=9;
return (
a.x+pad<
b.x+b.width&&
a.x+a.width-pad>
b.x&&
a.y+pad<
b.y+b.height&&
a.y+a.height-pad>
b.y
);
}
function collidesCircle(a,b){
const ax=
a.x+
a.width/2;
const ay=
a.y+
a.height/2;
const dx=
ax-b.x;
const dy=
ay-b.y;
const r=
b.size*.75;
return (
dx*dx+
dy*dy
<
r*r+
22*22
);
}
/* =====================================================
HUD
===================================================== */
function updateHUD(){
document.getElementById(
"hudScore"
).textContent=
Math.floor(score);
document.getElementById(
"hudCoins"
).textContent=
runCoins;
document.getElementById(
"hudLevel"
).textContent=
level;
}
function updateCombo(){
const box=
document.getElementById(
"comboBox"
);
if(combo>=2){
document.getElementById(
"comboNumber"
).textContent=
"x"+combo;
box.classList.add(
"show"
);
box.classList.remove(
"pop"
);
void box.offsetWidth;
box.classList.add(
"pop"
);
}else{
box.classList.remove(
"show"
);
}
}
function updatePowerUI(){
const powers={
shield:
document.getElementById(
"shieldPower"
),
magnet:
document.getElementById(
"magnetPower"
),
slow:
document.getElementById(
"slowPower"
)
};
for(
const key in powers
){
powers[key].style.display=
powerState[key]>0
?"block"
:"none";
}
}
/* =====================================================
GAME OVER
===================================================== */
function gameOver(){
if(!gameRunning){
return;
}
gameRunning=false;
cancelAnimationFrame(
animationId
);
const finalScore=
Math.floor(score);
const earnedXP=
Math.max(
5,
Math.floor(
finalScore/4+
runCoins+
maxCombo*2
)
);
/*
* Run-Coins werden dauerhaft
* zum Spielstand hinzugefügt.
*/
save.coins+=
runCoins;
runCoins=0;
save.xp+=
earnedXP;
while(
save.xp>=
levelXP(save.level)
){
save.xp-=
levelXP(save.level);
save.level++;
save.coins+=
50+
save.level*10;
}
if(
finalScore>
save.highscore
){
save.highscore=
finalScore;
document.getElementById(
"newRecord"
).style.display=
"block";
}else{
document.getElementById(
"newRecord"
).style.display=
"none";
}
checkQuestProgress();
/*
* WICHTIG:
* Jetzt wird der komplette Fortschritt
* sofort gespeichert.
*/
persist();
document.getElementById(
"finalScore"
).textContent=
finalScore;
document.getElementById(
"finalCoins"
).textContent=
Math.floor(
save.coins
);
document.getElementById(
"finalCombo"
).textContent=
maxCombo;
document.getElementById(
"finalLevel"
).textContent=
save.level;
gameOverScreen.classList.remove(
"hidden"
);
updateMenu();
}
/* =====================================================
XP
===================================================== */
function levelXP(lvl){
return 100+
(lvl-1)*55;
}
/* =====================================================
SHOP
===================================================== */
let activeShopTab="skins";
function shopTab(tab){
activeShopTab=tab;
document
.getElementById("tabSkins")
.classList.toggle(
"active",
tab==="skins"
);
document
.getElementById("tabThemes")
.classList.toggle(
"active",
tab==="themes"
);
document
.getElementById("tabUpgrades")
.classList.toggle(
"active",
tab==="upgrades"
);
const grid=
document.getElementById(
"shopGrid"
);
grid.innerHTML="";
document.getElementById(
"shopCoins"
).textContent=
save.coins;
if(tab==="skins"){
SKINS.forEach(s=>{
const owned=
save.ownedSkins.includes(
s.id
);
const active=
save.skin===s.id;
grid.innerHTML+=`
<div class="item">
<div class="item-icon">
${s.id}
</div>
<div class="item-name">
${s.name}
</div>
<div class="item-rarity">
${s.rarity}
</div>
<button
onclick="buySkin('${s.id}',${s.cost})">
${
active
?"AKTIV"
:owned
?"BENUTZEN"
:s.cost+
" 🪙"
}
</button>
</div>
`;
});
}else if(tab==="themes"){
THEMES.forEach(t=>{
const owned=
save.ownedThemes.includes(
t.id
);
const active=
save.theme===t.id;
grid.innerHTML+=`
<div class="item">
<div class="item-icon">
🌌
</div>
<div class="item-name">
${t.name}
</div>
<div class="item-rarity">
${t.cost===0
?"FREE"
:"SPECIAL"}
</div>
<button
onclick="buyTheme('${t.id}',${t.cost})">
${
active
?"AKTIV"
:owned
?"BENUTZEN"
:t.cost+
" 🪙"
}
</button>
</div>
`;
});
}else{
const upgrades=[
{
key:"coinUpgrade",
icon:"🪙",
name:"Coin Multiplier",
max:6,
base:80
},
{
key:"jumpUpgrade",
icon:"🦘",
name:"Jump Power",
max:6,
base:100
}
];
upgrades.forEach(u=>{
const lvl=
save[u.key];
const cost=
lvl*u.base;
grid.innerHTML+=`
<div class="item">
<div class="item-icon">
${u.icon}
</div>
<div class="item-name">
${u.name}
</div>
<div class="item-rarity">
LEVEL ${lvl}/${u.max}
</div>
<button
onclick="buyUpgrade('${u.key}',${cost},${u.max})"
${lvl>=u.max
?"disabled"
:""}>
${
lvl>=u.max
?"MAX"
:cost+
" 🪙"
}
</button>
</div>
`;
});
}
}
function buySkin(
id,
cost
){
if(
save.ownedSkins.includes(id)
){
save.skin=id;
}else{
if(
save.coins<cost
){
showToast(
"Nicht genug Coins!"
);
return;
}
save.coins-=cost;
save.ownedSkins.push(
id
);
save.skin=id;
}
/*
* Kauf + Ausrüstung sofort speichern.
*/
persist();
shopTab("skins");
updateMenu();
showToast(
"Skin ausgerüstet! 🐧"
);
}
function buyTheme(
id,
cost
){
if(
save.ownedThemes.includes(id)
){
save.theme=id;
}else{
if(
save.coins<cost
){
showToast(
"Nicht genug Coins!"
);
return;
}
save.coins-=cost;
save.ownedThemes.push(
id
);
save.theme=id;
}
persist();
applyTheme();
shopTab("themes");
updateMenu();
showToast(
"Welt ausgerüstet! 🌌"
);
}
function buyUpgrade(
key,
cost,
max
){
if(
save[key]>=max
){
return;
}
if(
save.coins<cost
){
showToast(
"Nicht genug Coins!"
);
return;
}
save.coins-=cost;
save[key]++;
persist();
shopTab("upgrades");
updateMenu();
showToast(
"Upgrade verbessert! ⚡"
);
}
/* =====================================================
QUESTS
===================================================== */
function todayKey(){
const d=
new Date();
/*
* Lokales Datum statt UTC.
* Dadurch wechseln Daily/Quests
* nicht mitten am Abend unerwartet.
*/
const year=
d.getFullYear();
const month=
String(
d.getMonth()+1
).padStart(2,"0");
const day=
String(
d.getDate()
).padStart(2,"0");
return (
year+
"-"+
month+
"-"+
day
);
}
function prepareQuests(){
if(
save.questDay===
todayKey()&&
save.quests.length
){
return;
}
save.questDay=
todayKey();
const shuffled=
[...QUESTS].sort(
()=>Math.random()-.5
);
save.quests=
shuffled
.slice(0,4)
.map(q=>({
...q,
current:0,
claimed:false
}));
persist();
}
function openQuests(){
prepareQuests();
openPanel(
"questScreen"
);
renderQuests();
}
function questValue(q){
if(
q.type==="score"
){
return Math.floor(
score
);
}
if(
q.type==="coins"
){
return runCoins;
}
if(
q.type==="combo"
){
return maxCombo;
}
if(
q.type==="jumps"
){
return jumps;
}
return 0;
}
function checkQuestProgress(){
if(
!save.quests.length
){
return;
}
let changed=false;
for(
const q of save.quests
){
if(q.claimed){
continue;
}
const next=
Math.min(
q.goal,
questValue(q)
);
if(
q.current!==next
){
q.current=next;
changed=true;
}
}
if(changed){
persist();
}
}
function renderQuests(){
const list=
document.getElementById(
"questList"
);
list.innerHTML="";
save.quests.forEach(
(q,i)=>{
const current=
Math.min(
q.goal,
q.current||0
);
const percent=
Math.min(
100,
current/
q.goal*
100
);
const ready=
current>=q.goal;
list.innerHTML+=`
<div class="quest ${
ready&&!q.claimed
?"ready"
:""
}">
<div class="quest-top">
<div class="quest-title">
${q.text}
</div>
<div class="quest-reward">
+${q.reward} 🪙
</div>
</div>
<div style="
margin-top:6px;
color:#94a3b8;
font-size:.65rem;
">
${current} / ${q.goal}
</div>
<div class="quest-bar">
<div
class="quest-progress"
style="
width:${percent}%
">
</div>
</div>
${
q.claimed
?`
<div style="
margin-top:8px;
color:#34d399;
font-size:.65rem;
font-weight:900;
">
✓ ABGEHOLT
</div>
`
:ready
?`
<button
class="btn gold"
style="
margin-top:8px;
padding:9px
"
onclick="claimQuest(${i})">
Belohnung abholen
</button>
`
:""
}
</div>
`;
}
);
}
function claimQuest(index){
const q=
save.quests[index];
if(
!q||
q.claimed||
q.current<q.goal
){
return;
}
q.claimed=true;
save.coins+=
q.reward;
persist();
renderQuests();
updateMenu();
showToast(
"+"+
q.reward+
" Coins! 🏆"
);
}
/* =====================================================
DAILY
===================================================== */
function renderDaily(){
const grid=
document.getElementById(
"dailyGrid"
);
const today=
todayKey();
const claimed=
save.lastDaily===
today;
grid.innerHTML="";
DAILY_REWARDS.forEach(
(reward,i)=>{
const active=
!claimed&&
i===save.dailyDay;
const done=
i<save.dailyDay||
(
claimed&&
i===save.dailyDay
);
grid.innerHTML+=`
<div class="daily
${active
?"active"
:""}
${done
?"claimed"
:""}">
<strong>
Tag ${i+1}
</strong>
<br>
${
done
?"✓"
:reward+
" 🪙"
}
</div>
`;
}
);
const button=
document.getElementById(
"dailyButton"
);
button.disabled=
claimed;
button.textContent=
claimed
?"Heute bereits abgeholt"
:"Belohnung abholen";
}
function claimDaily(){
const today=
todayKey();
if(
save.lastDaily===
today
){
return;
}
const reward=
DAILY_REWARDS[
save.dailyDay
];
save.coins+=
reward;
save.lastDaily=
today;
save.dailyDay=
(
save.dailyDay+
1
)%
DAILY_REWARDS.length;
persist();
renderDaily();
updateMenu();
showToast(
"+"+
reward+
" Daily Coins! 🎁"
);
}
function openDaily(){
openPanel(
"dailyScreen"
);
renderDaily();
}
/* =====================================================
UTILITY
===================================================== */
function roundRect(
context,
x,
y,
width,
height,
radius
){
context.beginPath();
context.roundRect(
x,
y,
width,
height,
radius
);
}
let toastTimer=0;
function showToast(message){
const toast=
document.getElementById(
"toast"
);
toast.textContent=
message;
toast.classList.add(
"show"
);
clearTimeout(
toastTimer
);
toastTimer=
setTimeout(
()=>{
toast.classList.remove(
"show"
);
},
1800
);
}
/* =====================================================
INITIALIZE
===================================================== */
resize();
prepareQuests();
updateMenu();
applyTheme();
</script>
</body>
</html>Game Source: Penguin Dash: Overdrive
Creator: GlitchGlider51
Libraries: none
Complexity: complex (3437 lines, 71.7 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: penguin-dash-overdrive-glitchglider51" to link back to the original. Then publish at arcadelab.ai/publish.