🎮ArcadeLab

Разрушай роботов

by SparkDragon18
3366 lines53.0 KB
▶ Play
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>Разрушай роботов</title>

<style>
html,body{
    margin:0;
    width:100%;
    height:100%;
    overflow:hidden;
    background:#080b10;
    touch-action:none;
}

canvas{
    position:fixed;
    inset:0;
    width:100%;
    height:100%;
    display:block;
}

#hud{
    position:fixed;
    left:14px;
    top:12px;
    z-index:5;
    color:white;
    font-family:Arial,sans-serif;
    pointer-events:none;
    text-shadow:0 2px 8px #000;
}

#title{
    font-size:20px;
    font-weight:900;
}

#stats{
    margin-top:5px;
    color:#62eaff;
    font-size:14px;
    font-weight:bold;
}

#tip{
    position:fixed;
    left:50%;
    top:90px;
    transform:translateX(-50%);
    z-index:5;
    color:#dce6eb;
    background:#0009;
    padding:7px 12px;
    border-radius:9px;
    font:12px Arial;
    pointer-events:none;
    text-align:center;
}

#tools{
    position:fixed;
    left:50%;
    bottom:12px;
    transform:translateX(-50%);
    z-index:10;
    display:flex;
    gap:6px;
    width:96%;
    overflow-x:auto;
    justify-content:center;
    padding:3px;
}

button{
    flex:none;
    border:2px solid #35434d;
    border-radius:10px;
    background:#121920;
    color:white;
    padding:10px 11px;
    font-size:11px;
    font-weight:bold;
}

button.active{
    border-color:#52eaff;
    background:#19333c;
    box-shadow:0 0 15px #52eaff66;
}
</style>
</head>

<body>

<canvas id="game"></canvas>

<div id="hud">
    <div id="title">РАЗРУШАЙ РОБОТОВ</div>
    <div id="stats">Очки: 0 • Уничтожено: 0</div>
</div>

<div id="tip">
    Тяни робота или предмет пальцем
</div>

<div id="tools">
    <button id="experiment" class="active">ЭКСПЕРИМЕНТ</button>
    <button id="destroy">УДАР</button>
    <button id="hammer">МОЛОТОК</button>
    <button id="ball">ШАР</button>
    <button id="box">ЯЩИК</button>
    <button id="electric">РАЗРЯД</button>
    <button id="compress">СЖАТЬ</button>
</div>

<script>
"use strict";


/* =========================================================
   CANVAS
========================================================= */

const canvas=document.getElementById("game");
const ctx=canvas.getContext("2d");

let W=innerWidth;
let H=innerHeight;

let DPR=Math.min(
    devicePixelRatio||1,
    2
);

function resize(){

    W=innerWidth;
    H=innerHeight;

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

    updatePlatform();
}

addEventListener(
    "resize",
    resize
);


/* =========================================================
   ИЗОБРАЖЕНИЯ
========================================================= */

const GEAR_URL=
"https://png.pngtree.com/png-clipart/20240208/original/pngtree-small-gear-on-a-white-background-watch-photo-png-image_14259145.png";

const WIRE_URL=
"https://img.magnific.com/premium-vector/broken-electrical-copper-wire-cord-damaged-electric-power-cable-torn-wire-with-bare-wire-danger-electric-problem-color-electricity-cable-vector-illustration-isolated-white-background_93083-3844.jpg";

const gearImage=new Image();
gearImage.src=GEAR_URL;

const wireImage=new Image();
wireImage.src=WIRE_URL;

let gearLoaded=false;
let wireLoaded=false;

gearImage.onload=()=>{
    gearLoaded=true;
};

wireImage.onload=()=>{
    wireLoaded=true;
};


/* =========================================================
   ПЛАТФОРМА
========================================================= */

const platform={
    x:0,
    y:0,
    w:0,
    h:65
};

function updatePlatform(){

    platform.x=0;
    platform.w=W;

    platform.y=Math.max(
        300,
        H-125
    );
}


/* =========================================================
   МАССИВЫ
========================================================= */

const robots=[];
const objects=[];
const particles=[];
const debris=[];
const effects=[];

let score=0;
let destroyed=0;
let mode="experiment";


/* =========================================================
   КАМЕРА
========================================================= */

const camera={
    zoom:1,
    targetZoom:1,
    shake:0,
    shakeX:0,
    shakeY:0,
    rotation:0,
    targetRotation:0,
    flash:0
};

function cameraImpact(power=1){

    camera.shake=Math.max(
        camera.shake,
        10*power
    );

    camera.targetZoom=Math.max(
        camera.targetZoom,
        1.04+0.04*power
    );

    camera.targetRotation+=
        (Math.random()-.5)*
        .025*
        power;
}

function updateCamera(){

    camera.zoom+=
        (camera.targetZoom-camera.zoom)*.12;

    camera.targetZoom+=
        (1-camera.targetZoom)*.08;

    camera.rotation+=
        (camera.targetRotation-camera.rotation)*.15;

    camera.targetRotation*=.88;

    if(camera.shake>0){

        camera.shake*=.82;

        camera.shakeX=
            (Math.random()-.5)*
            camera.shake;

        camera.shakeY=
            (Math.random()-.5)*
            camera.shake;

    }else{

        camera.shakeX=0;
        camera.shakeY=0;
    }
}


/* =========================================================
   ROUND RECT
========================================================= */

function roundRect(
    x,y,w,h,r
){

    ctx.beginPath();

    ctx.moveTo(
        x+r,y
    );

    ctx.lineTo(
        x+w-r,y
    );

    ctx.quadraticCurveTo(
        x+w,y,
        x+w,y+r
    );

    ctx.lineTo(
        x+w,y+h-r
    );

    ctx.quadraticCurveTo(
        x+w,y+h,
        x+w-r,y+h
    );

    ctx.lineTo(
        x+r,y+h
    );

    ctx.quadraticCurveTo(
        x,y+h,
        x,y+h-r
    );

    ctx.lineTo(
        x,y+r
    );

    ctx.quadraticCurveTo(
        x,y,
        x+r,y
    );

    ctx.closePath();
    ctx.fill();
}


/* =========================================================
   РОБОТ
========================================================= */

class Robot{

    constructor(x){

        this.x=x;
        this.y=platform.y-65;

        this.vx=0;
        this.vy=0;

        this.rotation=0;
        this.vr=0;

        this.hp=10;

        /* СЖАТИЕ */

        this.compressLevel=0;

        this.bodySquash=0;
        this.bodyWide=1;

        this.leftArmSquash=0;
        this.rightArmSquash=0;

        this.leftLegSquash=0;
        this.rightLegSquash=0;

        this.smokeTimer=0;
        this.electricTimer=0;

        this.parts={

            leftArm:{
                hp:3,
                detached:false
            },

            rightArm:{
                hp:3,
                detached:false
            },

            leftLeg:{
                hp:4,
                detached:false
            },

            rightLeg:{
                hp:4,
                detached:false
            }
        };

        this.dead=false;
        this.flash=0;
        this.shake=0;

        const colors=[
            "#367dff",
            "#9255e8",
            "#14aa9a",
            "#df5a38",
            "#bd4fe2"
        ];

        this.color=
            colors[
                Math.floor(
                    Math.random()*
                    colors.length
                )
            ];
    }


    hit(power){

        if(this.dead)return;

        this.hp-=power;

        this.flash=1;
        this.shake=7;

        hitEffect(
            this.x,
            this.y
        );

        playSound(
            "metal-hit",
            .45
        );

        /* повреждённый робот начинает искрить */

        if(this.hp<=7){

            this.electricTimer=
                Math.max(
                    this.electricTimer,
                    20
                );

            createElectricSparks(
                this.x,
                this.y
            );
        }

        if(this.hp<=5){

            this.smokeTimer=
                Math.max(
                    this.smokeTimer,
                    30
                );
        }

        if(Math.random()<.45){

            const available=
                Object.keys(this.parts)
                .filter(
                    k=>
                    !this.parts[k].detached
                );

            if(available.length){

                damagePart(
                    this,
                    available[
                        Math.floor(
                            Math.random()*
                            available.length
                        )
                    ]
                );
            }
        }

        if(this.hp<=0){
            destroyRobot(this);
        }
    }


    compress(){

        if(this.dead)return;

        this.compressLevel++;

        this.flash=.8;
        this.shake=
            5+
            this.compressLevel*2;

        cameraImpact(
            .55+
            this.compressLevel*.1
        );

        score+=15;

        /* =================================================
           ТЕЛО
        ================================================= */

        this.bodySquash=
            Math.min(
                4,
                this.compressLevel
            );

        this.bodyWide=
            1+
            this.compressLevel*.045;


        /* =================================================
           РУКИ
        ================================================= */

        this.leftArmSquash=
            Math.min(
                4,
                this.compressLevel
            );

        this.rightArmSquash=
            Math.min(
                4,
                this.compressLevel
            );


        /* =================================================
           НОГИ
        ================================================= */

        this.leftLegSquash=
            Math.min(
                4,
                this.compressLevel
            );

        this.rightLegSquash=
            Math.min(
                4,
                this.compressLevel
            );


        /* =================================================
           ИСКРЫ
        ================================================= */

        createElectricSparks(
            this.x,
            this.y
        );

        hitEffect(
            this.x,
            this.y
        );

        /* =================================================
           ДЫМ
        ================================================= */

        this.smokeTimer=35;

        createSmoke(
            this.x,
            this.y-20,
            5+
            this.compressLevel
        );

        playSound(
            "compress",
            .7
        );


        /* =================================================
           ЧТО-ТО МОЖЕТ ОТВАЛИТЬСЯ
        ================================================= */

        const available=
            Object.keys(this.parts)
            .filter(
                k=>
                !this.parts[k].detached
            );

        if(
            available.length &&
            (
                Math.random()<.35+
                this.compressLevel*.05
            )
        ){

            const key=
                available[
                    Math.floor(
                        Math.random()*
                        available.length
                    )
                ];

            detachPart(
                this,
                key
            );
        }


        /* =================================================
           ПЯТОЕ НАЖАТИЕ
        ================================================= */

        if(
            this.compressLevel>=5
        ){

            this.compressLevel=5;

            this.hp=0;

            setTimeout(
                ()=>{
                    if(!this.dead){
                        destroyRobot(this);
                    }
                },
                180
            );
        }
    }


    update(){

        if(this.dead)return;

        this.x+=this.vx;
        this.y+=this.vy;

        this.vy+=.22;

        this.vx*=.985;
        this.vr*=.985;

        this.rotation+=this.vr;

        if(
            this.y+65>=
            platform.y
        ){

            this.y=
                platform.y-65;

            if(
                Math.abs(this.vy)>2
            ){

                this.vy*=-.3;

            }else{

                this.vy=0;
            }

            this.vx*=.9;
        }

        if(this.x<60){

            this.x=60;
            this.vx*=-.45;
        }

        if(this.x>W-60){

            this.x=W-60;
            this.vx*=-.45;
        }

        if(this.flash>0){
            this.flash-=.08;
        }

        if(this.shake>0){
            this.shake*=.75;
        }

        if(this.electricTimer>0){

            this.electricTimer--;

            if(
                Math.random()<.45
            ){

                createElectricSparks(
                    this.x+
                    (Math.random()-.5)*50,
                    this.y+
                    (Math.random()-.5)*70
                );
            }
        }

        if(this.smokeTimer>0){

            this.smokeTimer--;

            if(
                Math.random()<.22
            ){

                createSmoke(
                    this.x+
                    (Math.random()-.5)*30,
                    this.y-40,
                    1
                );
            }
        }
    }


    draw(){

        if(this.dead)return;

        const sx=
            (Math.random()-.5)*
            this.shake;

        const sy=
            (Math.random()-.5)*
            this.shake;

        ctx.save();

        ctx.translate(
            this.x+sx,
            this.y+sy
        );

        ctx.rotate(
            this.rotation
        );

        /*
         * Чем сильнее сжатие,
         * тем ниже тело.
         */

        const squash=
            Math.min(
                .88,
                this.bodySquash*.08
            );

        const scaleY=
            1-squash;

        const scaleX=
            this.bodyWide;

        ctx.scale(
            scaleX,
            scaleY
        );

        ctx.fillStyle=
            "rgba(0,0,0,.5)";

        ctx.beginPath();

        ctx.ellipse(
            0,
            70/
            scaleY,
            48,
            9,
            0,
            0,
            Math.PI*2
        );

        ctx.fill();


        drawLeg(
            this.parts.leftLeg,
            -24,
            this.leftLegSquash,
            -1
        );

        drawLeg(
            this.parts.rightLeg,
            24,
            this.rightLegSquash,
            1
        );

        drawArm(
            this.parts.leftArm,
            -1,
            this.leftArmSquash
        );

        drawArm(
            this.parts.rightArm,
            1,
            this.rightArmSquash
        );


        /* ТЕЛО */

        ctx.fillStyle=this.color;

        roundRect(
            -40,
            -5,
            80,
            65,
            10
        );

        ctx.fillStyle="#11171c";

        roundRect(
            -27,
            8,
            54,
            31,
            6
        );

        ctx.fillStyle="#42eaff";

        ctx.fillRect(
            -18,
            17,
            8,
            8
        );

        ctx.fillStyle="#ffd34d";

        ctx.fillRect(
            -4,
            17,
            8,
            8
        );

        ctx.fillStyle="#ff5268";

        ctx.fillRect(
            10,
            17,
            8,
            8
        );


        /* ГОЛОВА */

        ctx.fillStyle="#b2bcc2";

        roundRect(
            -34,
            -58,
            68,
            53,
            11
        );

        ctx.fillStyle="#10151a";

        roundRect(
            -24,
            -43,
            48,
            28,
            7
        );

        ctx.fillStyle="#53eaff";

        ctx.shadowColor="#53eaff";
        ctx.shadowBlur=12;

        ctx.beginPath();

        ctx.arc(
            -12,
            -29,
            5,
            0,
            Math.PI*2
        );

        ctx.fill();

        ctx.beginPath();

        ctx.arc(
            12,
            -29,
            5,
            0,
            Math.PI*2
        );

        ctx.fill();

        ctx.shadowBlur=0;

        ctx.strokeStyle="#303a42";
        ctx.lineWidth=3;

        ctx.beginPath();

        ctx.moveTo(
            -10,
            -14
        );

        ctx.lineTo(
            10,
            -14
        );

        ctx.stroke();


        /* ПОЛОСА HP */

        ctx.fillStyle="#080a0c";

        roundRect(
            -40,
            -76,
            80,
            6,
            3
        );

        ctx.fillStyle="#48e66b";

        ctx.fillRect(
            -40,
            -76,
            80*
            Math.max(
                0,
                this.hp/10
            ),
            6
        );


        if(this.flash>0){

            ctx.globalAlpha=
                Math.max(
                    0,
                    this.flash
                );

            ctx.fillStyle="#fff";

            ctx.beginPath();

            ctx.arc(
                0,
                -10,
                24,
                0,
                Math.PI*2
            );

            ctx.fill();

            ctx.globalAlpha=1;
        }


        /*
         * Электрические линии
         */

        if(
            this.hp<=7 ||
            this.compressLevel>0
        ){

            drawRobotElectricity(
                this.compressLevel
            );
        }

        ctx.restore();
    }
}


/* =========================================================
   РУКИ
========================================================= */

function drawArm(
    part,
    side,
    squash
){

    if(part.detached)return;

    ctx.save();

    const curve=
        side*
        (
            24+
            squash*5
        );

    const down=
        squash*5;

    ctx.translate(
        side*39,
        7
    );

    ctx.strokeStyle="#303940";

    ctx.lineWidth=
        Math.max(
            8,
            14-
            squash*.7
        );

    ctx.lineCap="round";

    ctx.beginPath();

    ctx.moveTo(
        0,
        0
    );

    ctx.quadraticCurveTo(
        side*12,
        18+down,
        curve,
        37+down
    );

    ctx.stroke();

    ctx.fillStyle="#89949d";

    ctx.beginPath();

    ctx.arc(
        curve,
        40+down,
        9,
        0,
        Math.PI*2
    );

    ctx.fill();

    ctx.restore();
}


/* =========================================================
   НОГИ
========================================================= */

function drawLeg(
    part,
    x,
    squash,
    side
){

    if(part.detached)return;

    ctx.save();

    ctx.translate(
        x,
        50
    );

    /*
     * Нога всё больше изгибается
     * и становится короче.
     */

    const bend=
        side*
        squash*
        3.5;

    const length=
        48-
        squash*
        4;

    ctx.strokeStyle="#303940";

    ctx.lineWidth=20;

    ctx.lineCap="round";

    ctx.beginPath();

    ctx.moveTo(
        0,
        4
    );

    ctx.quadraticCurveTo(
        bend,
        length*.45,
        bend*1.4,
        length
    );

    ctx.stroke();

    ctx.fillStyle="#1d2329";

    roundRect(
        bend*1.4-16,
        length-4,
        32,
        13,
        5
    );

    ctx.restore();
}


/* =========================================================
   ЭЛЕКТРИЧЕСТВО НА РОБОТЕ
========================================================= */

function drawRobotElectricity(
    level
){

    const amount=
        1+
        Math.min(
            3,
            level
        );

    ctx.save();

    ctx.strokeStyle="#64f4ff";
    ctx.shadowColor="#64f4ff";
    ctx.shadowBlur=12;

    ctx.lineWidth=2;

    for(
        let i=0;
        i<amount;
        i++
    ){

        const side=
            Math.random()<.5
            ?-1
            :1;

        const startX=
            side*
            (
                25+
                Math.random()*12
            );

        const startY=
            -20+
            Math.random()*65;

        ctx.beginPath();

        ctx.moveTo(
            startX,
            startY
        );

        let x=startX;
        let y=startY;

        for(
            let j=0;
            j<4;
            j++
        ){

            x+=
                (
                    Math.random()-.5
                )*15;

            y+=
                8+
                Math.random()*10;

            ctx.lineTo(
                x,
                y
            );
        }

        ctx.stroke();
    }

    ctx.restore();
}


/* =========================================================
   ИСКРЫ
========================================================= */

function createElectricSparks(
    x,
    y
){

    for(
        let i=0;
        i<7;
        i++
    ){

        particles.push({

            x:
                x+
                (Math.random()-.5)*50,

            y:
                y+
                (Math.random()-.5)*65,

            vx:
                (Math.random()-.5)*7,

            vy:
                (Math.random()-.5)*7,

            size:
                1+
                Math.random()*3,

            life:.65,

            type:"electric"
        });
    }
}


/* =========================================================
   ДЫМ
========================================================= */

function createSmoke(
    x,
    y,
    amount
){

    for(
        let i=0;
        i<amount;
        i++
    ){

        particles.push({

            x:
                x+
                (Math.random()-.5)*15,

            y:
                y+
                (Math.random()-.5)*15,

            vx:
                (Math.random()-.5)*1.5,

            vy:
                -1-
                Math.random()*2,

            size:
                5+
                Math.random()*8,

            life:
                .8+
                Math.random()*.5,

            type:"smoke"
        });
    }
}


/* =========================================================
   ДЕТАЛИ
========================================================= */

function damagePart(
    robot,
    key
){

    const part=
        robot.parts[key];

    if(
        !part||
        part.detached
    )return;

    part.hp--;

    hitEffect(
        robot.x,
        robot.y
    );

    if(part.hp<=0){

        detachPart(
            robot,
            key
        );
    }
}

function detachPart(
    robot,
    key
){

    const part=
        robot.parts[key];

    if(
        !part||
        part.detached
    )return;

    part.detached=true;

    const arm=
        key==="leftArm"||
        key==="rightArm";

    const side=
        key.includes("left")
        ?-1
        :1;

    debris.push({

        type:
            arm
            ?"arm"
            :"leg",

        x:
            robot.x+
            (
                arm
                ?side*52
                :side*24
            ),

        y:
            robot.y+
            (
                arm
                ?25
                :78
            ),

        vx:
            side*
            (
                2+
                Math.random()*3
            ),

        vy:
            -3-
            Math.random()*4,

        rotation:
            Math.random()*
            Math.PI*2,

        vr:
            (Math.random()-.5)*.3,

        life:1
    });

    playSound(
        "part-break",
        .55
    );
}


/* =========================================================
   УНИЧТОЖЕНИЕ
========================================================= */

function destroyRobot(
    robot
){

    if(
        !robot||
        robot.dead
    )return;

    robot.dead=true;

    destroyed++;

    score+=100;

    playSound(
        "robot-break",
        .8
    );

    cameraImpact(1.5);

    createSmoke(
        robot.x,
        robot.y,
        15
    );

    for(
        const key in robot.parts
    ){

        if(
            !robot.parts[key].detached
        ){

            detachPart(
                robot,
                key
            );
        }
    }

    for(let i=0;i<9;i++){

        debris.push({

            type:"gear",

            x:
                robot.x+
                (Math.random()-.5)*50,

            y:
                robot.y+
                (Math.random()-.5)*60,

            vx:
                (Math.random()-.5)*8,

            vy:
                -3-
                Math.random()*7,

            rotation:
                Math.random()*
                Math.PI*2,

            vr:
                (Math.random()-.5)*.4,

            size:
                8+
                Math.random()*8,

            life:1
        });
    }

    for(let i=0;i<7;i++){

        debris.push({

            type:"wire",

            x:
                robot.x+
                (Math.random()-.5)*50,

            y:
                robot.y+
                (Math.random()-.5)*60,

            vx:
                (Math.random()-.5)*9,

            vy:
                -3-
                Math.random()*7,

            rotation:
                Math.random()*
                Math.PI*2,

            vr:
                (Math.random()-.5)*.4,

            size:
                13+
                Math.random()*14,

            life:1
        });
    }

    for(let i=0;i<35;i++){

        particles.push({

            x:robot.x,
            y:robot.y,

            vx:
                (Math.random()-.5)*13,

            vy:
                (Math.random()-.5)*13,

            size:
                2+
                Math.random()*5,

            life:1,

            type:
                Math.random()<.7
                ?"spark"
                :"smoke"
        });
    }

    effects.push({

        type:"bigRing",

        x:robot.x,
        y:robot.y,

        size:8,
        life:1
    });

    setTimeout(
        spawnRobot,
        800
    );
}


/* =========================================================
   ОБЪЕКТЫ
========================================================= */

class GameObject{

    constructor(
        type,
        x,
        y
    ){

        this.type=type;

        this.x=x;
        this.y=y;

        this.vx=0;
        this.vy=0;

        this.rotation=0;
        this.vr=0;

        this.held=false;

        this.size=
            type==="ball"
            ?22
            :
            type==="hammer"
            ?18
            :
            type==="box"
            ?30
            :23;
    }

    update(){

        if(this.held)return;

        this.x+=this.vx;
        this.y+=this.vy;

        this.vy+=.22;

        this.vx*=.985;
        this.vr*=.98;

        this.rotation+=this.vr;

        if(
            this.y+
            this.size>=
            platform.y
        ){

            this.y=
                platform.y-
                this.size;

            if(
                Math.abs(this.vy)>2
            ){

                this.vy*=-.45;

            }else{

                this.vy=0;
            }

            this.vx*=.86;
        }

        if(
            this.x-this.size<0
        ){

            this.x=this.size;
            this.vx*=-.5;
        }

        if(
            this.x+this.size>W
        ){

            this.x=W-this.size;
            this.vx*=-.5;
        }
    }

    draw(){

        ctx.save();

        ctx.translate(
            this.x,
            this.y
        );

        ctx.rotate(
            this.rotation
        );

        if(
            this.type==="ball"
        ){

            ctx.fillStyle="#68757e";
            ctx.strokeStyle="#b9c3ca";
            ctx.lineWidth=3;

            ctx.beginPath();

            ctx.arc(
                0,
                0,
                this.size,
                0,
                Math.PI*2
            );

            ctx.fill();
            ctx.stroke();

        }

        else if(
            this.type==="box"
        ){

            ctx.fillStyle="#704321";

            ctx.fillRect(
                -this.size,
                -this.size,
                this.size*2,
                this.size*2
            );

            ctx.fillStyle="#8e5b31";

            ctx.fillRect(
                -this.size+4,
                -this.size+4,
                this.size*2-8,
                this.size*2-8
            );

            ctx.strokeStyle="#432716";
            ctx.lineWidth=4;

            ctx.strokeRect(
                -this.size,
                -this.size,
                this.size*2,
                this.size*2
            );

            ctx.strokeStyle="#b77a42";
            ctx.lineWidth=2;

            ctx.beginPath();

            ctx.moveTo(
                -this.size+3,
                -this.size+8
            );

            ctx.quadraticCurveTo(
                0,
                -this.size-4,
                this.size-3,
                -this.size+8
            );

            ctx.moveTo(
                -this.size+3,
                0
            );

            ctx.quadraticCurveTo(
                0,
                -7,
                this.size-3,
                0
            );

            ctx.moveTo(
                -this.size+3,
                this.size-7
            );

            ctx.quadraticCurveTo(
                0,
                this.size+5,
                this.size-3,
                this.size-7
            );

            ctx.stroke();

        }

        else if(
            this.type==="hammer"
        ){

            ctx.fillStyle="#6d4930";

            ctx.fillRect(
                -4,
                -34,
                8,
                60
            );

            ctx.fillStyle="#7f8990";

            roundRect(
                -27,
                -42,
                54,
                18,
                5
            );
        }

        ctx.restore();
    }
}


/* =========================================================
   ЯЩИК
========================================================= */

function breakBox(box){

    if(
        !box||
        box.type!=="box"
    )return;

    const index=
        objects.indexOf(box);

    if(index!==-1){

        objects.splice(
            index,
            1
        );
    }

    playSound(
        "wood-break",
        .9
    );

    cameraImpact(1);

    for(let i=0;i<14;i++){

        const angle=
            Math.random()*
            Math.PI*2;

        const force=
            7+
            Math.random()*9;

        debris.push({

            type:"woodBoard",

            x:
                box.x+
                (Math.random()-.5)*22,

            y:
                box.y+
                (Math.random()-.5)*22,

            vx:
                Math.cos(angle)*force,

            vy:
                Math.sin(angle)*force-5,

            rotation:
                Math.random()*
                Math.PI*2,

            vr:
                (Math.random()-.5)*.8,

            width:
                55+
                Math.random()*60,

            height:
                10+
                Math.random()*10,

            life:1,

            bounce:.42
        });
    }

    score+=25;
}


/* =========================================================
   ЭФФЕКТ УДАРА
========================================================= */

function hitEffect(
    x,
    y
){

    for(let i=0;i<10;i++){

        particles.push({

            x:x,
            y:y,

            vx:
                (Math.random()-.5)*8,

            vy:
                (Math.random()-.5)*8,

            size:
                2+
                Math.random()*4,

            life:1,

            type:"spark"
        });
    }

    effects.push({

        type:"ring",

        x:x,
        y:y,

        size:4,
        life:1
    });

    cameraImpact(.5);
}


/* =========================================================
   ЗВУК
========================================================= */

const sounds={

    "metal-hit":
        "sounds/metal-hit.mp3",

    "robot-break":
        "sounds/robot-break.mp3",

    "wood-hit":
        "sounds/wood-hit.mp3",

    "wood-break":
        "sounds/wood-break.mp3",

    "electric":
        "sounds/electric.mp3",

    "compress":
        "sounds/mechanical-click.mp3",

    "part-break":
        "sounds/metal-hit.mp3"
};

function playSound(
    name,
    volume=1
){

    const src=
        sounds[name];

    if(!src)return;

    const audio=
        new Audio(src);

    audio.preload="auto";

    audio.volume=
        Math.max(
            0,
            Math.min(
                1,
                volume
            )
        );

    audio.play()
    .then(()=>{})
    .catch(
        err=>{
            console.warn(
                "Не удалось воспроизвести звук:",
                src,
                err
            );
        }
    );
}


/* =========================================================
   ОБЛОМКИ
========================================================= */

function drawDebris(d){

    ctx.save();

    ctx.translate(
        d.x,
        d.y
    );

    ctx.rotate(
        d.rotation
    );

    if(
        d.type==="gear"
    ){

        if(gearLoaded){

            ctx.drawImage(
                gearImage,
                -d.size,
                -d.size,
                d.size*2,
                d.size*2
            );

        }else{

            ctx.fillStyle="#b5c0c6";

            ctx.beginPath();

            for(
                let i=0;
                i<16;
                i++
            ){

                const a=
                    i*Math.PI/8;

                const r=
                    i%2===0
                    ?d.size
                    :d.size*.68;

                const x=
                    Math.cos(a)*r;

                const y=
                    Math.sin(a)*r;

                if(i===0)
                    ctx.moveTo(x,y);
                else
                    ctx.lineTo(x,y);
            }

            ctx.closePath();
            ctx.fill();
        }
    }

    else if(
        d.type==="wire"
    ){

        if(wireLoaded){

            const sx=
                wireImage.naturalWidth*.08;

            const sy=
                wireImage.naturalHeight*.15;

            const sw=
                wireImage.naturalWidth*.84;

            const sh=
                wireImage.naturalHeight*.70;

            ctx.drawImage(
                wireImage,
                sx,
                sy,
                sw,
                sh,
                -d.size,
                -d.size*.6,
                d.size*2,
                d.size*1.2
            );

        }else{

            ctx.strokeStyle="#c87b35";
            ctx.lineWidth=4;

            ctx.beginPath();

            ctx.moveTo(
                -d.size,
                0
            );

            ctx.quadraticCurveTo(
                0,
                -10,
                d.size,
                0
            );

            ctx.stroke();
        }
    }

    else if(
        d.type==="arm"
    ){

        ctx.strokeStyle="#303940";
        ctx.lineWidth=13;
        ctx.lineCap="round";

        ctx.beginPath();

        ctx.moveTo(
            -22,
            -20
        );

        ctx.lineTo(
            22,
            22
        );

        ctx.stroke();

    }

    else if(
        d.type==="leg"
    ){

        ctx.fillStyle="#303940";

        roundRect(
            -10,
            -28,
            20,
            48,
            5
        );

    }

    else if(
        d.type==="woodBoard"
    ){

        ctx.fillStyle="#754622";

        ctx.fillRect(
            -d.width/2,
            -d.height/2,
            d.width,
            d.height
        );

        ctx.strokeStyle="#3e2517";
        ctx.lineWidth=2;

        ctx.strokeRect(
            -d.width/2,
            -d.height/2,
            d.width,
            d.height
        );
    }

    ctx.restore();
}


/* =========================================================
   ОБЛОМКИ — ФИЗИКА
========================================================= */

function updateDebris(){

    for(
        let i=debris.length-1;
        i>=0;
        i--
    ){

        const d=debris[i];

        d.x+=d.vx;
        d.y+=d.vy;

        d.vy+=.25;

        d.vx*=.985;
        d.vr*=.99;

        d.rotation+=d.vr;

        const size=
            d.type==="woodBoard"
            ?
            Math.max(
                d.width,
                d.height
            )/2
            :
            d.size||18;

        if(
            d.y+size>=
            platform.y
        ){

            d.y=
                platform.y-size;

            if(
                Math.abs(d.vy)>1.5
            ){

                d.vy*=
                    -(d.bounce||.35);

            }else{

                d.vy=0;
            }

            d.vx*=.8;
            d.vr*=.8;
        }

        if(
            d.x-size<0
        ){

            d.x=size;
            d.vx*=-.5;
        }

        if(
            d.x+size>W
        ){

            d.x=W-size;
            d.vx*=-.5;
        }

        d.life-=.008;

        if(d.life<=0){

            debris.splice(
                i,
                1
            );
        }
    }
}


/* =========================================================
   ЧАСТИЦЫ
========================================================= */

function updateParticles(){

    for(
        let i=particles.length-1;
        i>=0;
        i--
    ){

        const p=
            particles[i];

        p.x+=p.vx;
        p.y+=p.vy;

        p.vy+=
            p.type==="smoke"
            ?.01
            :.12;

        p.vx*=.97;

        p.life-=
            p.type==="smoke"
            ?.012
            :.025;

        if(p.life<=0){

            particles.splice(
                i,
                1
            );
        }
    }
}

function drawParticles(){

    for(
        const p of particles
    ){

        ctx.save();

        ctx.globalAlpha=
            Math.max(
                0,
                p.life
            );

        if(
            p.type==="smoke"
        ){

            ctx.fillStyle="#737e84";

        }else if(
            p.type==="electric"
        ){

            ctx.fillStyle="#65f6ff";

            ctx.shadowColor="#65f6ff";
            ctx.shadowBlur=12;

        }else if(
            p.type==="woodDust"
        ){

            ctx.fillStyle="#a8794e";

        }else{

            ctx.fillStyle="#63eaff";
        }

        ctx.beginPath();

        ctx.arc(
            p.x,
            p.y,
            p.size,
            0,
            Math.PI*2
        );

        ctx.fill();

        ctx.restore();
    }
}


/* =========================================================
   ЭФФЕКТЫ
========================================================= */

function updateEffects(){

    for(
        let i=effects.length-1;
        i>=0;
        i--
    ){

        const e=
            effects[i];

        e.size+=
            e.type==="bigRing"
            ?7
            :4;

        e.life-=.045;

        if(e.life<=0){

            effects.splice(
                i,
                1
            );
        }
    }
}

function drawEffects(){

    for(
        const e of effects
    ){

        ctx.save();

        ctx.globalAlpha=
            Math.max(
                0,
                e.life
            );

        ctx.strokeStyle=
            e.type==="bigRing"
            ?"rgba(82,234,255,.9)"
            :
            "rgba(255,210,80,.9)";

        ctx.lineWidth=
            e.type==="bigRing"
            ?5
            :3;

        ctx.beginPath();

        ctx.arc(
            e.x,
            e.y,
            e.size,
            0,
            Math.PI*2
        );

        ctx.stroke();

        ctx.restore();
    }
}


/* =========================================================
   СПАВН
========================================================= */

function spawnRobot(){

    robots.push(
        new Robot(
            100+
            Math.random()*
            Math.max(
                100,
                W-200
            )
        )
    );
}


/* =========================================================
   ИНСТРУМЕНТЫ
========================================================= */

function setMode(m){

    mode=m;

    document
    .querySelectorAll(
        "#tools button"
    )
    .forEach(
        b=>
        b.classList.remove(
            "active"
        )
    );

    const id=
        m==="experiment"
        ?"experiment"
        :
        m==="destroy"
        ?"destroy"
        :
        m==="hammer"
        ?"hammer"
        :
        m==="ball"
        ?"ball"
        :
        m==="box"
        ?"box"
        :
        m==="electric"
        ?"electric"
        :"compress";

    document
    .getElementById(id)
    .classList.add("active");
}

document
.getElementById("experiment")
.onclick=()=>{
    setMode("experiment");
};

document
.getElementById("destroy")
.onclick=()=>{
    setMode("destroy");
};

document
.getElementById("hammer")
.onclick=()=>{
    setMode("hammer");
};

document
.getElementById("ball")
.onclick=()=>{
    setMode("ball");
};

document
.getElementById("box")
.onclick=()=>{
    setMode("box");
};

document
.getElementById("electric")
.onclick=()=>{
    setMode("electric");
};

document
.getElementById("compress")
.onclick=()=>{
    setMode("compress");
};


/* =========================================================
   ПОИСК
========================================================= */

function findTarget(
    x,
    y
){

    for(
        let i=objects.length-1;
        i>=0;
        i--
    ){

        const o=objects[i];

        const dx=x-o.x;
        const dy=y-o.y;

        if(
            dx*dx+
            dy*dy<
            (o.size+20)*
            (o.size+20)
        ){

            return o;
        }
    }

    for(
        let i=robots.length-1;
        i>=0;
        i--
    ){

        const r=robots[i];

        if(
            !r.dead&&
            Math.abs(x-r.x)<55&&
            Math.abs(y-r.y)<95
        ){

            return r;
        }
    }

    return null;
}


/* =========================================================
   TOUCH
========================================================= */

let selected=null;
let pointerId=null;

let lastX=0;
let lastY=0;
let lastTime=0;

function getPoint(e){

    const r=
        canvas.getBoundingClientRect();

    return {

        x:
            e.clientX-r.left,

        y:
            e.clientY-r.top
    };
}

canvas.addEventListener(
    "pointerdown",
    e=>{

        e.preventDefault();

        const p=
            getPoint(e);

        /*
         * СЖАТИЕ:
         * один тап = одно сжатие
         */

        if(
            mode==="compress"
        ){

            const target=
                findTarget(
                    p.x,
                    p.y
                );

            if(
                target instanceof Robot
            ){

                target.compress();
            }

            return;
        }

        pointerId=e.pointerId;

        canvas.setPointerCapture(
            pointerId
        );

        lastX=p.x;
        lastY=p.y;

        lastTime=
            performance.now();

        selected=
            findTarget(
                p.x,
                p.y
            );

        if(!selected){

            if(mode==="ball"){

                selected=
                    new GameObject(
                        "ball",
                        p.x,
                        p.y
                    );

                selected.vy=-5;

                objects.push(
                    selected
                );

            }else if(
                mode==="box"
            ){

                selected=
                    new GameObject(
                        "box",
                        p.x,
                        p.y
                    );

                objects.push(
                    selected
                );

            }else if(
                mode==="hammer"
            ){

                selected=
                    new GameObject(
                        "hammer",
                        p.x,
                        p.y
                    );

                objects.push(
                    selected
                );
            }
        }
    },
    {
        passive:false
    }
);

canvas.addEventListener(
    "pointermove",
    e=>{

        e.preventDefault();

        if(
            pointerId!==e.pointerId||
            !selected
        )return;

        const p=
            getPoint(e);

        const now=
            performance.now();

        const dt=
            Math.max(
                1,
                now-lastTime
            );

        const vx=
            (p.x-lastX)/
            dt*
            16;

        const vy=
            (p.y-lastY)/
            dt*
            16;

        selected.x=p.x;
        selected.y=p.y;

        selected.vx=vx;
        selected.vy=vy;

        if(
            selected instanceof Robot
        ){

            selected.rotation+=
                vx*.015;

        }else{

            selected.vr=
                vx*.03;
        }

        lastX=p.x;
        lastY=p.y;
        lastTime=now;
    },
    {
        passive:false
    }
);

canvas.addEventListener(
    "pointerup",
    e=>{

        e.preventDefault();

        if(
            pointerId!==e.pointerId
        )return;

        if(selected){

            const speed=
                Math.sqrt(
                    selected.vx*
                    selected.vx+
                    selected.vy*
                    selected.vy
                );

            if(
                selected instanceof Robot
            ){

                if(
                    mode==="destroy"
                ){

                    selected.hit(
                        2+
                        Math.min(
                            5,
                            speed
                        )
                    );

                }else if(
                    mode==="hammer"
                ){

                    selected.hit(
                        3+
                        Math.min(
                            7,
                            speed
                        )
                    );

                }else if(
                    mode==="electric"
                ){

                    selected.hit(4);

                    playSound(
                        "electric",
                        .7
                    );

                }else{

                    if(speed>2){

                        selected.hit(2);
                    }
                }

            }else{

                if(
                    selected.type==="box"&&
                    speed>5
                ){

                    breakBox(
                        selected
                    );

                    selected=null;
                    pointerId=null;

                    return;
                }

                if(speed>4){

                    selected.vx*=1.5;
                    selected.vy*=1.5;

                    objectHit(
                        selected,
                        Math.min(
                            3,
                            speed/4
                        )
                    );
                }
            }
        }

        selected=null;
        pointerId=null;
    },
    {
        passive:false
    }
);


/* =========================================================
   УДАР ПРЕДМЕТОМ
========================================================= */

function objectHit(
    obj,
    power
){

    if(
        obj.type==="box"
    ){

        playSound(
            "wood-hit",
            .6
        );

    }else{

        playSound(
            "metal-hit",
            .5
        );
    }

    obj.vx+=
        (Math.random()-.5)*
        power*8;

    obj.vy-=
        power*
        (
            3+
            Math.random()*4
        );

    obj.vr+=
        (Math.random()-.5)*
        .5;

    hitEffect(
        obj.x,
        obj.y
    );
}


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

function update(){

    for(
        const r of robots
    ){
        r.update();
    }

    for(
        const o of objects
    ){
        o.update();
    }

    updateDebris();
    updateParticles();
    updateEffects();
    updateCamera();

    for(
        const r of robots
    ){

        if(r.dead)continue;

        for(
            const o of objects
        ){

            const dx=r.x-o.x;
            const dy=r.y-o.y;

            const distance=
                Math.sqrt(
                    dx*dx+
                    dy*dy
                );

            if(
                distance<
                65+o.size
            ){

                if(
                    Math.abs(o.vx)+
                    Math.abs(o.vy)>3
                ){

                    r.hit(
                        Math.min(
                            3,
                            (
                                Math.abs(o.vx)+
                                Math.abs(o.vy)
                            )/5
                        )
                    );

                    r.vx+=
                        o.vx*.3;

                    r.vy-=2;

                    o.vx*=-.45;
                    o.vy*=-.4;
                }
            }
        }
    }

    updateStats();
}

function updateStats(){

    document.getElementById(
        "stats"
    ).textContent=
        "Очки: "+
        score+
        " • Уничтожено: "+
        destroyed;
}


/* =========================================================
   ФОН
========================================================= */

function drawBackground(){

    const g=
        ctx.createLinearGradient(
            0,0,0,H
        );

    g.addColorStop(
        0,
        "#080b10"
    );

    g.addColorStop(
        1,
        "#101820"
    );

    ctx.fillStyle=g;

    ctx.fillRect(
        0,0,W,H
    );

    ctx.strokeStyle="#1a2932";
    ctx.lineWidth=1;

    for(
        let x=0;
        x<W;
        x+=50
    ){

        ctx.beginPath();

        ctx.moveTo(
            x,
            0
        );

        ctx.lineTo(
            x,
            platform.y
        );

        ctx.stroke();
    }

    for(
        let y=50;
        y<platform.y;
        y+=50
    ){

        ctx.beginPath();

        ctx.moveTo(
            0,
            y
        );

        ctx.lineTo(
            W,
            y
        );

        ctx.stroke();
    }
}


/* =========================================================
   ПЛАТФОРМА
========================================================= */

function drawPlatform(){

    ctx.fillStyle="#182229";

    ctx.fillRect(
        0,
        platform.y,
        W,
        platform.h
    );

    ctx.fillStyle="#52eaff";

    ctx.fillRect(
        0,
        platform.y,
        W,
        3
    );

    ctx.strokeStyle="#31454f";
    ctx.lineWidth=2;

    for(
        let x=0;
        x<W;
        x+=70
    ){

        ctx.beginPath();

        ctx.moveTo(
            x,
            platform.y+15
        );

        ctx.lineTo(
            x+35,
            platform.y+50
        );

        ctx.stroke();
    }
}


/* =========================================================
   DRAW
========================================================= */

function draw(){

    ctx.setTransform(
        DPR,0,0,DPR,0,0
    );

    ctx.clearRect(
        0,0,W,H
    );

    drawBackground();

    ctx.save();

    ctx.translate(
        W/2+camera.shakeX,
        H/2+camera.shakeY
    );

    ctx.rotate(
        camera.rotation
    );

    ctx.scale(
        camera.zoom,
        camera.zoom
    );

    ctx.translate(
        -W/2,
        -H/2
    );

    drawPlatform();

    for(
        const o of objects
    ){
        o.draw();
    }

    for(
        const r of robots
    ){
        r.draw();
    }

    for(
        const d of debris
    ){
        drawDebris(d);
    }

    drawParticles();
    drawEffects();

    ctx.restore();

    if(camera.flash>0){

        ctx.save();

        ctx.globalAlpha=
            camera.flash;

        ctx.fillStyle="#fff";

        ctx.fillRect(
            0,
            0,
            W,
            H
        );

        ctx.restore();

        camera.flash-=.04;
    }
}


/* =========================================================
   LOOP
========================================================= */

function loop(){

    update();
    draw();

    requestAnimationFrame(
        loop
    );
}


/* =========================================================
   СТАРТ
========================================================= */

resize();

spawnRobot();
spawnRobot();

objects.push(
    new GameObject(
        "box",
        W*.25,
        platform.y-30
    )
);

objects.push(
    new GameObject(
        "ball",
        W*.7,
        platform.y-22
    )
);

objects.push(
    new GameObject(
        "hammer",
        W*.5,
        platform.y-18
    )
);

loop();

</script>

</body>
</html>

Game Source: Разрушай роботов

Creator: SparkDragon18

Libraries: none

Complexity: complex (3366 lines, 53.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: game-sparkdragon18-mu4jjkwo" to link back to the original. Then publish at arcadelab.ai/publish.