Разрушай роботов
by SparkDragon183366 lines53.0 KB
<!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.