🎮ArcadeLab

V12 Engine Simulator – тахометр, троение, тряска двигателя

by PrismBolt10
568 lines22.0 KB
▶ Play
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>V12 Engine Simulator – тахометр, троение, тряска двигателя</title>
    <style>
        * {
            user-select: none;
            -webkit-tap-highlight-color: transparent;
        }
        body {
            background: radial-gradient(circle at 20% 30%, #0a0f1a, #03060c);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: 'Orbitron', 'Courier New', monospace;
            padding: 16px;
            margin: 0;
        }
        /* приборная панель */
        .dashboard {
            max-width: 800px;
            width: 100%;
            background: #0b0e16dd;
            backdrop-filter: blur(4px);
            border-radius: 64px;
            padding: 24px 20px 30px;
            box-shadow: 0 20px 40px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.08);
            border: 1px solid #2a3a5a;
        }
        h1 {
            text-align: center;
            font-size: 1.5rem;
            letter-spacing: 4px;
            color: #ffd966;
            text-shadow: 0 0 6px #ff9900;
            margin-bottom: 10px;
        }
        .engine-badge {
            text-align: center;
            font-size: 0.7rem;
            color: #7f8fa4;
            margin-bottom: 20px;
        }
        /* тахометр (canvas) */
        .tacho-container {
            background: #000000aa;
            border-radius: 60px;
            padding: 10px;
            margin-bottom: 20px;
        }
        canvas#tachometer {
            display: block;
            width: 100%;
            background: #10141f;
            border-radius: 40px;
            box-shadow: inset 0 0 10px #00000055, 0 5px 12px black;
        }
        .rpm-value {
            text-align: center;
            font-size: 2.1rem;
            font-weight: bold;
            margin-top: 8px;
            color: #ffaa44;
            background: #00000077;
            border-radius: 36px;
            padding: 6px;
            letter-spacing: 3px;
        }
        /* визуальный блок двигателя (тряска) */
        .engine-visual {
            background: #1a1f2c;
            border-radius: 48px;
            padding: 16px;
            margin-bottom: 24px;
            transition: transform 0.02s linear;
            box-shadow: 0 5px 15px black;
            border-bottom: 3px solid #ff8800;
        }
        .engine-title {
            display: flex;
            justify-content: space-between;
            font-size: 0.85rem;
            color: #bbccff;
            margin-bottom: 12px;
        }
        .v12-block {
            display: flex;
            gap: 8px;
            flex-wrap: wrap;
            justify-content: center;
        }
        .cylinder {
            width: 22px;
            height: 44px;
            background: linear-gradient(145deg, #3a445e, #1f2538);
            border-radius: 12px;
            box-shadow: inset -1px -1px 0 #0f111c, inset 1px 1px 0 #5b6d96;
            transition: all 0.03s linear;
        }
        .cylinder.firing {
            background: #ffaa33;
            box-shadow: 0 0 10px #ff7700;
        }
        .status-lights {
            display: flex;
            gap: 14px;
            margin-top: 12px;
            font-size: 0.7rem;
            justify-content: center;
        }
        .light {
            padding: 4px 12px;
            border-radius: 50px;
            background: #202433;
            color: #8b98b5;
        }
        .light.warning {
            background: #441111;
            color: #ff8888;
            box-shadow: 0 0 4px red;
        }
        /* элементы управления */
        .controls {
            display: flex;
            flex-direction: column;
            gap: 20px;
        }
        .gas-panel {
            background: #0c0f18;
            border-radius: 80px;
            padding: 12px 18px;
        }
        .gas-slider {
            display: flex;
            align-items: center;
            gap: 16px;
            flex-wrap: wrap;
        }
        input#throttleSlider {
            flex: 3;
            height: 8px;
            border-radius: 10px;
            background: #2a2f3e;
        }
        .gas-value {
            background: #00000088;
            padding: 4px 12px;
            border-radius: 20px;
            font-weight: bold;
            width: 65px;
            text-align: center;
            color: #ffb347;
        }
        .button-group {
            display: flex;
            gap: 16px;
            justify-content: space-between;
            flex-wrap: wrap;
        }
        button {
            background: #1f2538;
            border: none;
            padding: 12px 24px;
            border-radius: 60px;
            font-family: inherit;
            font-weight: bold;
            color: #eef5ff;
            font-size: 1rem;
            cursor: pointer;
            flex: 1;
            transition: 0.05s linear;
            box-shadow: 0 3px 0 #0b0e16;
        }
        button:active {
            transform: translateY(2px);
            box-shadow: none;
        }
        .ignite-btn {
            background: #2c6e2c;
            color: #e0ffc0;
        }
        .break-btn {
            background: #8b3c2c;
        }
        .fix-btn {
            background: #2a5f6e;
        }
        .misfire-indicator {
            font-size: 0.9rem;
            text-align: center;
            margin-top: 16px;
            background: #00000066;
            padding: 6px;
            border-radius: 30px;
        }
        @keyframes shake {
            0% { transform: translate(1px, 0px);}
            25% { transform: translate(-1px, 1px);}
            50% { transform: translate(1px, -1px);}
            75% { transform: translate(-1px, 0px);}
            100% { transform: translate(0, 1px);}
        }
        .shake-engine {
            animation: shake 0.06s infinite;
        }
        footer {
            font-size: 0.6rem;
            text-align: center;
            margin-top: 20px;
            color: #5f6b87;
        }
    </style>
</head>
<body>
<div class="dashboard">
    <h1>⚡ V12 BITURBO ⚡</h1>
    <div class="engine-badge">симулятор двигателя | тахометр | троение</div>

    <div class="tacho-container">
        <canvas id="tachometer" width="600" height="180" style="width:100%; height:auto; max-width:600px; margin:0 auto; display:block;"></canvas>
        <div class="rpm-value" id="rpmDisplay">0 RPM</div>
    </div>

    <!-- двигатель (визуальный блок с тряской) -->
    <div class="engine-visual" id="engineBlock">
        <div class="engine-title">
            <span>🔧 V12 - 48 клапанов</span>
            <span id="engineStateLabel">⛔ ЗАГЛУШЕН</span>
        </div>
        <div class="v12-block" id="cylinderBank">
            <!-- 12 цилиндров добавим js -->
        </div>
        <div class="status-lights">
            <div class="light" id="ignitionLight">🔌 ЗАЖИГАНИЕ</div>
            <div class="light" id="misfireLight">⚠️ ТРОИТ</div>
            <div class="light" id="checkEngine">🧰 CHECK</div>
        </div>
    </div>

    <div class="controls">
        <div class="gas-panel">
            <div class="gas-slider">
                <span style="color:#ccc;">🎛️ ГАЗ (0-100%)</span>
                <input type="range" id="throttleSlider" min="0" max="100" value="0" step="1" disabled>
                <span class="gas-value" id="throttlePercent">0%</span>
            </div>
        </div>
        <div class="button-group">
            <button id="startStopBtn" class="ignite-btn">🔑 ЗАВЕСТИ / ЗАГЛУШИТЬ</button>
            <button id="breakBtn" class="break-btn">⚠️ ВЫЗВАТЬ ТРОЕНИЕ</button>
            <button id="fixBtn" class="fix-btn">🔧 ПОЧИНИТЬ</button>
        </div>
        <div class="misfire-indicator" id="misfireMsg">⚙️ Двигатель работает ровно</div>
    </div>
    <footer>нажмите газ (ползунок) — двигатель V12 оживает, троение вызывает пропуски и тряску</footer>
</div>

<script>
    (function(){
        // ---------- состояние двигателя ----------
        let engineRunning = false;   // заведён ли
        let throttle = 0;            // 0..100
        let rpm = 0;                 // текущие обороты
        let isBroken = false;        // режим "троит" (пропуски воспламенения)
        let misfireSeverity = 0;     // 0..1 (интенсивность тряски/потери мощности)
        
        // физические параметры
        let idleRPM = 850;
        let maxRPM = 7800;
        let healthFactor = 1.0;      // при троении снижается максимальная тяга
        
        // для анимации цилиндров и тряски
        let cylinders = 12;
        let cylinderElements = [];
        let lastTimestamp = 0;
        let animationId = null;
        
        // случайные пропуски зажигания (для визуального эффекта)
        let misfireRandom = 0;
        
        // DOM элементы
        const tachoCanvas = document.getElementById('tachometer');
        const ctx = tachoCanvas.getContext('2d');
        const rpmSpan = document.getElementById('rpmDisplay');
        const engineBlockDiv = document.getElementById('engineBlock');
        const throttleSlider = document.getElementById('throttleSlider');
        const throttlePercentSpan = document.getElementById('throttlePercent');
        const startStopBtn = document.getElementById('startStopBtn');
        const breakBtn = document.getElementById('breakBtn');
        const fixBtn = document.getElementById('fixBtn');
        const misfireMsgSpan = document.getElementById('misfireMsg');
        const engineStateLabel = document.getElementById('engineStateLabel');
        const misfireLight = document.getElementById('misfireLight');
        const checkEngineLight = document.getElementById('checkEngine');
        const ignitionLight = document.getElementById('ignitionLight');
        
        // создаём 12 цилиндров
        const cylinderContainer = document.getElementById('cylinderBank');
        for(let i=0; i<cylinders; i++) {
            let div = document.createElement('div');
            div.classList.add('cylinder');
            cylinderContainer.appendChild(div);
            cylinderElements.push(div);
        }
        
        // отрисовка тахометра (стрелка, шкала)
        function drawTachometer(rpmVal) {
            const w = tachoCanvas.width = 600;
            const h = tachoCanvas.height = 180;
            ctx.clearRect(0, 0, w, h);
            // фон
            ctx.fillStyle = "#0c1020";
            ctx.fillRect(0, 0, w, h);
            // градиентная шкала
            let percent = Math.min(1, Math.max(0, rpmVal / maxRPM));
            let angle = -Math.PI/2 + percent * Math.PI; // 0..180 градусов
            let centerX = w/2, centerY = h - 22;
            let radius = 70;
            // дуга тахометра
            ctx.beginPath();
            ctx.arc(centerX, centerY, radius, -Math.PI/2, Math.PI/2, false);
            ctx.lineWidth = 18;
            ctx.strokeStyle = "#2e3b4e";
            ctx.stroke();
            // цветная дуга (обороты)
            let grad = ctx.createLinearGradient(0,0,w,0);
            grad.addColorStop(0, "#22ff88");
            grad.addColorStop(0.7, "#ffcc33");
            grad.addColorStop(1, "#ff5533");
            ctx.beginPath();
            ctx.arc(centerX, centerY, radius, -Math.PI/2, -Math.PI/2 + (percent * Math.PI), false);
            ctx.lineWidth = 18;
            ctx.strokeStyle = grad;
            ctx.stroke();
            // рисование рисок
            for(let i=0; i<=10; i++) {
                let p = i/10;
                let rAngle = -Math.PI/2 + p * Math.PI;
                let x1 = centerX + (radius-12)*Math.cos(rAngle);
                let y1 = centerY + (radius-12)*Math.sin(rAngle);
                let x2 = centerX + (radius+2)*Math.cos(rAngle);
                let y2 = centerY + (radius+2)*Math.sin(rAngle);
                ctx.beginPath();
                ctx.moveTo(x1,y1);
                ctx.lineTo(x2,y2);
                ctx.strokeStyle = "#bbddff";
                ctx.lineWidth = 2;
                ctx.stroke();
            }
            // стрелка
            let arrowLen = radius+6;
            let arrowX = centerX + arrowLen * Math.cos(angle);
            let arrowY = centerY + arrowLen * Math.sin(angle);
            ctx.beginPath();
            ctx.moveTo(centerX, centerY);
            ctx.lineTo(arrowX, arrowY);
            ctx.lineWidth = 4;
            ctx.strokeStyle = "#ffaa44";
            ctx.stroke();
            ctx.fillStyle = "#ffaa44";
            ctx.beginPath();
            ctx.arc(centerX, centerY, 8, 0, Math.PI*2);
            ctx.fill();
            // текст оборотов
            ctx.font = "bold 24px 'Orbitron'";
            ctx.fillStyle = "#ffe5a3";
            ctx.shadowBlur = 0;
            ctx.fillText(Math.floor(rpmVal), centerX-45, centerY-35);
            ctx.font = "12px monospace";
            ctx.fillStyle = "#bfbfdf";
            ctx.fillText("x100 RPM", centerX+20, centerY-28);
        }
        
        // обновление модели двигателя (физика оборотов)
        function updateEnginePhysics(deltaSec) {
            if(!engineRunning) {
                // заглушен -> обороты падают к 0
                rpm = Math.max(0, rpm - 600 * deltaSec);
                if(rpm < 10) rpm = 0;
                return;
            }
            
            let targetRPM = idleRPM;
            let effectiveThrottle = throttle;
            // если троит, снижаем мощность
            let powerMulti = isBroken ? 0.45 + (Math.random() * 0.2) : 1.0;
            if(isBroken) {
                // хаотичность оборотов при троении
                effectiveThrottle = throttle * (0.5 + Math.random() * 0.3);
            }
            // зависимость целевых оборотов от газа
            let rpmFromGas = idleRPM + (maxRPM - idleRPM) * (effectiveThrottle / 100) * powerMulti;
            targetRPM = Math.min(maxRPM, Math.max(idleRPM, rpmFromGas));
            
            // инерция двигателя
            let diff = targetRPM - rpm;
            let acceleration = diff * 2.8 * deltaSec;
            rpm += acceleration;
            if(rpm < 0) rpm = 0;
            if(rpm > maxRPM) rpm = maxRPM;
            
            // эффект троения – случайные провалы оборотов если isBroken
            if(isBroken && engineRunning && rpm > 1000) {
                if(Math.random() < 0.08) {
                    rpm = Math.max(idleRPM, rpm - (Math.random() * 800));
                }
            }
        }
        
        // анимация цилиндров (вспышки) и тряска двигателя
        let lastSpark = 0;
        function animateCylindersAndShake(nowSec) {
            if(!engineRunning) {
                cylinderElements.forEach(c => c.classList.remove('firing'));
                engineBlockDiv.classList.remove('shake-engine');
                return;
            }
            // скорость вспышек зависит от оборотов и троения
            let flashesPerSec = Math.min(30, 10 + (rpm / 800));
            if(isBroken) flashesPerSec *= 0.6; // часть пропусков
            let threshold = 1/flashesPerSec;
            if(nowSec - lastSpark > threshold) {
                lastSpark = nowSec;
                // случайные цилиндры
                let numFiring = isBroken ? (Math.random() > 0.6 ? 1 : Math.floor(Math.random() * 3) + 1) : 2;
                cylinderElements.forEach(c => c.classList.remove('firing'));
                for(let i=0; i<numFiring; i++) {
                    let idx = Math.floor(Math.random() * cylinders);
                    cylinderElements[idx].classList.add('firing');
                    setTimeout(() => {
                        if(cylinderElements[idx]) cylinderElements[idx].classList.remove('firing');
                    }, 70);
                }
            }
            // тряска двигателя: зависит от оборотов и особенно от троения
            let shakeIntensity = 0;
            if(engineRunning) {
                let rpmFactor = Math.min(1, rpm / 5000);
                shakeIntensity = rpmFactor * 0.8;
                if(isBroken) shakeIntensity += 0.7;
                if(rpm > 2000 && isBroken) shakeIntensity += 0.5;
            }
            if(shakeIntensity > 0.15) {
                engineBlockDiv.classList.add('shake-engine');
                let intensityPx = Math.min(3, shakeIntensity * 2);
                engineBlockDiv.style.animation = `shake ${0.04 + (shakeIntensity*0.02)}s infinite`;
            } else {
                engineBlockDiv.classList.remove('shake-engine');
                engineBlockDiv.style.animation = '';
            }
        }
        
        // обновление UI (тахометр, индикаторы, текст)
        function updateUI() {
            drawTachometer(rpm);
            rpmSpan.innerText = Math.floor(rpm) + " RPM";
            throttlePercentSpan.innerText = throttle + "%";
            throttleSlider.value = throttle;
            if(engineRunning) {
                engineStateLabel.innerHTML = "🔥 V12 РАБОТАЕТ 🔥";
                ignitionLight.style.background = "#2c9e2c";
                ignitionLight.style.color = "white";
                ignitionLight.style.boxShadow = "0 0 6px #00ff66";
            } else {
                engineStateLabel.innerHTML = "⛔ ЗАГЛУШЕН";
                ignitionLight.style.background = "#202433";
                ignitionLight.style.color = "#8b98b5";
                ignitionLight.style.boxShadow = "none";
            }
            if(isBroken) {
                misfireLight.classList.add('warning');
                checkEngineLight.classList.add('warning');
                misfireMsgSpan.innerHTML = "⚠️ ДВИГАТЕЛЬ ТРОИТ! ПРОПУСКИ ВОСПЛАМЕНЕНИЯ, ТРЯСКА ⚠️";
            } else {
                misfireLight.classList.remove('warning');
                checkEngineLight.classList.remove('warning');
                misfireMsgSpan.innerHTML = "✅ Двигатель работает ровно, V12 звучит мощно.";
            }
        }
        
        // ---------- обработчики управления ----------
        function setThrottle(value) {
            if(!engineRunning) return;
            throttle = Math.min(100, Math.max(0, value));
            throttlePercentSpan.innerText = throttle + "%";
            throttleSlider.value = throttle;
        }
        
        function toggleEngine() {
            if(engineRunning) {
                engineRunning = false;
                throttle = 0;
                setThrottle(0);
                throttleSlider.disabled = true;
                // снимаем тряску
                engineBlockDiv.classList.remove('shake-engine');
            } else {
                engineRunning = true;
                throttleSlider.disabled = false;
                rpm = idleRPM;
                // если была поломка, оставляем её, но мотор заведётся с троением
                if(isBroken) {
                    // двигатель заведётся но будет троить
                }
            }
            updateUI();
        }
        
        function causeMisfire() {
            if(!engineRunning) {
                // можно сломать и после запуска будет троить
            }
            isBroken = true;
            updateUI();
        }
        
        function fixEngine() {
            isBroken = false;
            updateUI();
        }
        
        // привязка событий
        startStopBtn.addEventListener('click', toggleEngine);
        breakBtn.addEventListener('click', causeMisfire);
        fixBtn.addEventListener('click', fixEngine);
        throttleSlider.addEventListener('input', (e) => {
            setThrottle(parseInt(e.target.value));
        });
        
        // первоначальное состояние
        throttleSlider.disabled = true;
        engineRunning = false;
        rpm = 0;
        isBroken = false;
        updateUI();
        
        // главный цикл симуляции с delta time
        let lastFrameTime = 0;
        let nowSeconds = 0;
        function simulationLoop(nowMs) {
            requestAnimationFrame(simulationLoop);
            if(!lastFrameTime) { lastFrameTime = nowMs; return; }
            let delta = Math.min(0.033, (nowMs - lastFrameTime) / 1000);
            if(delta < 0.005) { lastFrameTime = nowMs; return; }
            updateEnginePhysics(delta);
            updateUI();
            let nowSec = nowMs / 1000;
            animateCylindersAndShake(nowSec);
            lastFrameTime = nowMs;
        }
        
        // запуск цикла
        simulationLoop(performance.now());
        
        // дополнительный эффект: если поломка и газ высокий — показать сообщение
        setInterval(() => {
            if(engineRunning && isBroken && throttle > 40) {
                misfireMsgSpan.style.background = "#3a1a1a";
                setTimeout(() => { if(misfireMsgSpan) misfireMsgSpan.style.background = ""; }, 300);
            }
        }, 800);
    })();
</script>
</body>
</html>

Game Source: V12 Engine Simulator – тахометр, троение, тряска двигателя

Creator: PrismBolt10

Libraries: none

Complexity: complex (568 lines, 22.0 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: v12-engine-simulator-prismbolt10" to link back to the original. Then publish at arcadelab.ai/publish.