🎮ArcadeLab

Pertarungan Abad Pertengahan

by CosmicOtter50
501 lines16.6 KB
▶ Play
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>Pertarungan Abad Pertengahan</title>
    <style>
        * {
            box-sizing: border-box;
            user-select: none;
            -webkit-user-select: none;
        }
        body {
            margin: 0;
            padding: 0;
            background-color: #1a1a1a;
            color: #ffffff;
            font-family: Arial, sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: flex-start;
            min-height: 100vh;
            overflow: hidden;
        }
        #game-container {
            width: 100%;
            max-width: 600px;
            height: 100vh;
            display: flex;
            flex-direction: column;
            background: #2b2b2b;
            border-left: 2px solid #444;
            border-right: 2px solid #444;
        }
        /* Panel Atas (Info) */
        #header {
            background: #1f1f1f;
            padding: 10px;
            display: flex;
            justify-content: space-around;
            align-items: center;
            border-bottom: 2px solid #444;
            font-size: 14px;
        }
        .stat-box {
            background: #333;
            padding: 5px 10px;
            border-radius: 5px;
            border: 1px solid #555;
        }
        /* Arena Pertarungan */
        #battlefield {
            flex: 1;
            position: relative;
            background: linear-gradient(to bottom, #4a773d 0%, #355e2b 100%);
            overflow: hidden;
            border-bottom: 2px solid #444;
        }
        .base {
            position: absolute;
            bottom: 20px;
            width: 60px;
            height: 120px;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: flex-end;
            font-weight: bold;
            font-size: 12px;
        }
        #player-base { left: 15px; color: #6bc5ff; }
        #enemy-base { right: 15px; color: #ff6b6b; }
        
        .base-tower {
            width: 50px;
            height: 90px;
            background: #555;
            border: 3px solid #333;
            border-radius: 5px 5px 0 0;
            display: flex;
            align-items: center;
            justify-content: center;
            text-align: center;
        }

        /* Unit Karakter */
        .unit {
            position: absolute;
            bottom: 30px;
            width: 35px;
            height: 45px;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            font-size: 10px;
            font-weight: bold;
            border-radius: 4px;
            transition: left 0.05s linear;
        }
        .player-unit { background: #3498db; border: 2px solid #2980b9; }
        .enemy-unit { background: #e74c3c; border: 2px solid #c0392b; }
        .miner { background: #f1c40f; border: 2px solid #d4ac0d; color: #000; }

        /* Panel Kontrol Bawah */
        #controls {
            background: #1f1f1f;
            padding: 10px;
            display: grid;
            grid-template-columns: repeat(4, 1fr);
            gap: 8px;
            border-top: 2px solid #444;
        }
        .btn {
            background: #444;
            color: white;
            border: none;
            padding: 10px 5px;
            border-radius: 6px;
            font-size: 11px;
            font-weight: bold;
            cursor: pointer;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            transition: 0.1s;
        }
        .btn:active {
            transform: scale(0.95);
        }
        .btn:disabled {
            background: #2a2a2a;
            color: #666;
            cursor: not-allowed;
        }
        .btn-miner { background: #b7950b; }
        .btn-swordsman { background: #2471a3; }
        .btn-knight { background: #117864; }
        .btn-archer { background: #7d3c98; }

        /* Layar Pesan / Game Over */
        #message-overlay {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.8);
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            z-index: 10;
            display: none;
        }
        #message-overlay h1 {
            margin-bottom: 20px;
            font-size: 24px;
            text-align: center;
        }
        .restart-btn {
            padding: 12px 24px;
            background: #27ae60;
            color: white;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
        }
    </style>
</head>
<body>

<div id="game-container">
    <!-- Header Informasi -->
    <div id="header">
        <div class="stat-box">💰 Emas: <span id="gold-display">50</span></div>
        <div class="stat-box">⛏️ Penambang: <span id="miner-count">0</span></div>
        <div class="stat-box">🏰 HP Markas: <span id="base-hp">100</span></div>
    </div>

    <!-- Arena Pertarungan -->
    <div id="battlefield">
        <div id="player-base" class="base">
            <div class="base-tower" id="p-hp-bar">100</div>
            <span>Kita</span>
        </div>
        <div id="enemy-base" class="base">
            <div class="base-tower" id="e-hp-bar">100</div>
            <span>Musuh</span>
        </div>
    </div>

    <!-- Panel Kontrol Tombol -->
    <div id="controls">
        <button class="btn btn-miner" onclick="spawnMiner()">
            <span>⛏️ Penambang</span>
            <span>(25 🪙)</span>
        </button>
        <button class="btn btn-swordsman" onclick="spawnUnit('swordsman', 20)">
            <span>⚔️ Pejuang</span>
            <span>(20 🪙)</span>
        </button>
        <button class="btn btn-archer" onclick="spawnUnit('archer', 30)">
            <span>🏹 Pemanah</span>
            <span>(30 🪙)</span>
        </button>
        <button class="btn btn-knight" onclick="spawnUnit('knight', 50)">
            <span>🛡️ Ksatria</span>
            <span>(50 🪙)</span>
        </button>
    </div>
</div>

<!-- Layar Selesai Game -->
<div id="message-overlay">
    <h1 id="game-message">KEMENANGAN!</h1>
    <button class="restart-btn" onclick="resetGame()">Main Lagi</button>
</div>

<script>
    // --- Web Audio API (Efek Suara Tanpa File Eksternal) ---
    const audioCtx = new (window.AudioContext || window.webkitAudioContext)();

    function playSound(type) {
        if (audioCtx.state === 'suspended') {
            audioCtx.resume();
        }
        const osc = audioCtx.createOscillator();
        const gainNode = audioCtx.createGain();
        osc.connect(gainNode);
        gainNode.connect(audioCtx.destination);

        let now = audioCtx.currentTime;

        if (type === 'gold') {
            osc.type = 'sine';
            osc.frequency.setValueAtTime(600, now);
            osc.frequency.exponentialRampToValueAtTime(900, now + 0.1);
            gainNode.gain.setValueAtTime(0.1, now);
            gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
            osc.start(now);
            osc.stop(now + 0.1);
        } else if (type === 'spawn') {
            osc.type = 'triangle';
            osc.frequency.setValueAtTime(150, now);
            osc.frequency.linearRampToValueAtTime(300, now + 0.15);
            gainNode.gain.setValueAtTime(0.15, now);
            gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
            osc.start(now);
            osc.stop(now + 0.15);
        } else if (type === 'hit') {
            osc.type = 'sawtooth';
            osc.frequency.setValueAtTime(100, now);
            osc.frequency.linearRampToValueAtTime(40, now + 0.1);
            gainNode.gain.setValueAtTime(0.2, now);
            gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
            osc.start(now);
            osc.stop(now + 0.1);
        } else if (type === 'win') {
            osc.type = 'sine';
            osc.frequency.setValueAtTime(300, now);
            osc.frequency.setValueAtTime(450, now + 0.1);
            osc.frequency.setValueAtTime(600, now + 0.2);
            gainNode.gain.setValueAtTime(0.2, now);
            gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.4);
            osc.start(now);
            osc.stop(now + 0.4);
        } else if (type === 'lose') {
            osc.type = 'sawtooth';
            osc.frequency.setValueAtTime(200, now);
            osc.frequency.linearRampToValueAtTime(80, now + 0.4);
            gainNode.gain.setValueAtTime(0.2, now);
            gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.4);
            osc.start(now);
            osc.stop(now + 0.4);
        }
    }

    // --- Variabel Game ---
    let gold = 50;
    let baseHp = 100;
    let enemyBaseHp = 100;
    let units = [];
    let isGameOver = false;
    let gameInterval;
    let enemySpawnTimer = 0;

    const unitTypes = {
        miner: { hp: 30, atk: 2, spd: 0.8, range: 20, cost: 25, label: '⛏️' },
        swordsman: { hp: 50, atk: 8, spd: 1.2, range: 25, cost: 20, label: '⚔️' },
        archer: { hp: 35, atk: 10, spd: 1.0, range: 120, cost: 30, label: '🏹' },
        knight: { hp: 120, atk: 15, spd: 0.7, range: 30, cost: 50, label: '🛡️' }
    };

    // --- Fungsi Utama Game ---
    function spawnMiner() {
        if (gold >= 25 && !isGameOver) {
            gold -= 25;
            createUnit('miner', 'player');
            playSound('spawn');
            updateUI();
        }
    }

    function spawnUnit(type, cost) {
        if (gold >= cost && !isGameOver) {
            gold -= cost;
            createUnit(type, 'player');
            playSound('spawn');
            updateUI();
        }
    }

    function createUnit(type, team) {
        let stats = unitTypes[type];
        let startX = team === 'player' ? 60 : window.innerWidth > 600 ? 500 : window.innerHeight > window.innerWidth ? window.innerWidth - 80 : 500;
        
        // Batasi kustomisasi posisi awal jika layar kecil
        let battlefieldWidth = document.getElementById('battlefield').clientWidth;
        if (team === 'enemy') startX = battlefieldWidth - 80;
        if (team === 'player') startX = 60;

        let unitObj = {
            type: type,
            team: team,
            hp: stats.hp,
            maxHp: stats.hp,
            atk: stats.atk,
            spd: stats.spd,
            range: stats.range,
            label: stats.label,
            x: startX,
            element: document.createElement('div')
        };

        unitObj.element.className = `unit ${team === 'player' ? (type === 'miner' ? 'miner' : 'player-unit') : 'enemy-unit'}`;
        unitObj.element.innerHTML = `${unitObj.label}<span style="font-size:8px">${unitObj.hp}</span>`;
        unitObj.element.style.left = unitObj.x + 'px';
        
        document.getElementById('battlefield').appendChild(unitObj.element);
        units.push(unitObj);
    }

    // AI Musuh membuat unit secara otomatis seiring waktu
    function handleEnemyAI(dt) {
        enemySpawnTimer += dt;
        if (enemySpawnTimer > 4) { // Musuh spawn setiap 4 detik
            enemySpawnTimer = 0;
            let choices = ['swordsman', 'archer', 'knight'];
            let randomChoice = choices[Math.floor(Math.random() * choices.length)];
            createUnit(randomChoice, 'enemy');
        }
    }

    // Loop Utama Game (Dipanggil setiap frame / ~60fps)
    let lastTime = performance.now();
    function gameLoop(time) {
        if (isGameOver) return;
        let dt = (time - lastTime) / 1000;
        lastTime = time;

        handleEnemyAI(dt);

        let battlefieldWidth = document.getElementById('battlefield').clientWidth;

        // Logika pergerakan & pertarungan unit
        for (let i = units.length - 1; i >= 0; i--) {
            let u = units[i];

            // Penambang khusus: Jika milik player dan berada di dekat markas, menghasilkan emas
            if (u.team === 'player' && u.type === 'miner' && u.x < 100) {
                if (Math.random() < 0.02) { // Mendapatkan emas secara berkala
                    gold += 2;
                    playSound('gold');
                    updateUI();
                }
            }

            // Cari target terdekat
            let target = null;
            let minDst = 9999;

            for (let j = 0; j < units.length; j++) {
                let other = units[j];
                if (u.team !== other.team) {
                    let dst = Math.abs(u.x - other.x);
                    if (dst < minDst) {
                        minDst = dst;
                        target = other;
                    }
                }
            }

            // Cek jarak dengan markas musuh/lawan
            let enemyBaseX = battlefieldWidth - 70;
            let playerBaseX = 50;
            let baseTargetX = u.team === 'player' ? enemyBaseX : playerBaseX;
            let baseDst = Math.abs(u.x - baseTargetX);

            // Pergerakan atau Pertarungan
            if (target && minDst <= u.range) {
                // Serang unit lawan
                if (Math.random() < 0.05) { // Kecepatan serang
                    target.hp -= u.atk;
                    playSound('hit');
                    if (target.hp <= 0) {
                        target.element.remove();
                        units = units.filter(item => item !== target);
                    }
                }
            } else if (baseDst <= u.range) {
                // Serang markas lawan
                if (Math.random() < 0.05) {
                    if (u.team === 'player') {
                        enemyBaseHp -= u.atk;
                        if (enemyBaseHp < 0) enemyBaseHp = 0;
                    } else {
                        baseHp -= u.atk;
                        if (baseHp < 0) baseHp = 0;
                    }
                    playSound('hit');
                    updateUI();
                    checkWinCondition();
                }
            } else {
                // Bergerak maju
                if (u.team === 'player') {
                    u.x += u.spd;
                    // Cegah menumpuk terlalu dekat dengan unit di depannya
                    let blocked = units.some(o => o.team === 'player' && o !== u && o.x > u.x && o.x - u.x < 30);
                    if (blocked) u.x -= u.spd;
                } else {
                    u.x -= u.spd;
                    let blocked = units.some(o => o.team === 'enemy' && o !== u && o.x < u.x && u.x - o.x < 30);
                    if (blocked) u.x += u.spd;
                }
            }

            // Update posisi elemen HTML
            u.element.style.left = u.x + 'px';
            u.element.querySelector('span').innerText = Math.max(0, Math.floor(u.hp));
        }

        // Pasif Emas seiring waktu untuk player
        if (Math.random() < 0.015) {
            gold += 1;
            updateUI();
        }

        requestAnimationFrame(gameLoop);
    }

    function updateUI() {
        document.getElementById('gold-display').innerText = gold;
        document.getElementById('base-hp').innerText = baseHp;
        document.getElementById('p-hp-bar').innerText = baseHp;
        document.getElementById('e-hp-bar').innerText = enemyBaseHp;
        
        let minerCount = units.filter(u => u.team === 'player' && u.type === 'miner').length;
        document.getElementById('miner-count').innerText = minerCount;
    }

    function checkWinCondition() {
        if (baseHp <= 0) {
            isGameOver = true;
            playSound('lose');
            document.getElementById('game-message').innerText = "KAMU KALAH!\nMarkas hancur.";
            document.getElementById('message-overlay').style.display = 'flex';
        } else if (enemyBaseHp <= 0) {
            isGameOver = true;
            playSound('win');
            document.getElementById('game-message').innerText = "KEMENANGAN!\nMusuh dikalahkan.";
            document.getElementById('message-overlay').style.display = 'flex';
        }
    }

    function resetGame() {
        units.forEach(u => u.element.remove());
        units = [];
        gold = 50;
        baseHp = 100;
        enemyBaseHp = 100;
        isGameOver = false;
        enemySpawnTimer = 0;
        document.getElementById('message-overlay').style.display = 'none';
        updateUI();
        lastTime = performance.now();
        requestAnimationFrame(gameLoop);
    }

    // Inisialisasi awal
    updateUI();
    requestAnimationFrame(gameLoop);
</script>

</body>
</html>

Game Source: Pertarungan Abad Pertengahan

Creator: CosmicOtter50

Libraries: none

Complexity: complex (501 lines, 16.6 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: pertarungan-abad-pertengahan-cosmicotter50" to link back to the original. Then publish at arcadelab.ai/publish.