๐ŸŽฎArcadeLab

Penguin Dash: Overdrive

by WildGecko80
3533 lines61.8 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);
}

.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;
}

/* 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>

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

<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>

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

<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>

<!-- =====================================================
     SHOP
===================================================== -->

<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>

<!-- =====================================================
     QUESTS
===================================================== -->

<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>

<!-- =====================================================
     DAILY
===================================================== -->

<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>

<!-- =====================================================
     GAME
===================================================== -->

<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>

<!-- =====================================================
     PAUSE
===================================================== -->

<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>

<!-- =====================================================
     GAME OVER
===================================================== -->

<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>

<!-- =====================================================
     TOAST
===================================================== -->

<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 DATA
===================================================== */

let save = JSON.parse(
    localStorage.getItem("penguinDashSave") || "null"
);

if(!save){
    save={
        coins:0,
        highscore:0,
        level:1,
        xp:0,
        skin:"๐Ÿง",
        ownedSkins:["๐Ÿง"],
        theme:"arctic",
        ownedThemes:["arctic"],
        coinUpgrade:1,
        jumpUpgrade:1,
        dailyDay:0,
        lastDaily:"",
        questDay:"",
        quests:[]
    };
}

function persist(){
    localStorage.setItem(
        "penguinDashSave",
        JSON.stringify(save)
    );
}

/* =====================================================
   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
   0 -> 99 in 2.5 sec
   99 hold 3 sec
   100 hold 1.5 sec
===================================================== */

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(){

    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(){

    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;

    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
        );

    if(!paused){
        animationId=
            requestAnimationFrame(gameLoop);
    }
}

/* =====================================================
   QUIT
===================================================== */

function quitGame(){

    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();

    animationId=
        requestAnimationFrame(gameLoop);
}

/* =====================================================
   UPDATE
===================================================== */

function update(dt){

    const slowFactor=
        powerState.slow>0?.58:1;

    /* SCORE */

    score+=.11*dt*speed*slowFactor;

    /* LEVEL */

    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 */

    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;
    }

    /* SPAWN */

    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();
        }
    }

    /* OBJECTS */

    updateObstacles(dt,slowFactor);
    updateCoins(dt,slowFactor);
    updatePowerups(dt,slowFactor);
    updateParticles(dt);

    /* POWERS */

    for(const key in powerState){

        if(powerState[key]>0){
            powerState[key]-=dt;
        }

        if(powerState[key]<0){
            powerState[key]=0;
        }
    }

    updatePowerUI();

    /* COMBO */

    if(combo>0){

        comboTimer-=dt;

        if(comboTimer<=0){

            combo=0;
            updateCombo();
        }
    }

    /* SHAKE */

    shake*=.88;

    updateHUD();

    checkQuestProgress();
}

/* =====================================================
   OBSTACLES
===================================================== */

function spawnObstacle(){

    const types=[
        "ice",
        "rock",
        "spike",
        "crate"
    ];

    let type=
        types[
            Math.floor(
                Math.random()*types.length
            )
        ];

    let 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
    });

    /* sometimes double obstacle */

    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
        );
    }

    /* SKY */

    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;

    /* GROUND */

    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();

    /* OBJECTS */

    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 oldHigh=
        save.highscore;

    const earnedXP=
        Math.max(
            5,
            Math.floor(
                finalScore/4+
                runCoins+
                maxCombo*2
            )
        );

    save.coins+=runCoins;

    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";
    }

    persist();

    document.getElementById(
        "finalScore"
    ).textContent=
        finalScore;

    document.getElementById(
        "finalCoins"
    ).textContent=
        runCoins;

    document.getElementById(
        "finalCombo"
    ).textContent=
        maxCombo;

    document.getElementById(
        "finalLevel"
    ).textContent=
        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;
    }

    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();

    return d.toISOString().slice(0,10);
}

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;

    for(const q of save.quests){

        if(q.claimed)continue;

        q.current=
            Math.min(
                q.goal,
                questValue(q)
            );
    }
}

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: WildGecko80

Libraries: none

Complexity: complex (3533 lines, 61.8 KB)

The full source code is displayed above on this page.

Remix Instructions

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