🎮ArcadeLab

Penguin Dash: Overdrive

by GlitchGlider51
3437 lines71.7 KB
▶ Play
<!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.