Gear Factory
by SparkDragon181925 lines26.5 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>Gear Factory</title>
<style>
*{
box-sizing:border-box;
-webkit-tap-highlight-color:transparent;
}
html,body{
margin:0;
width:100%;
height:100%;
overflow:hidden;
background:#101216;
color:#eee;
font-family:Arial,sans-serif;
touch-action:none;
}
body{
background:
radial-gradient(circle at 50% 35%,#30343a 0%,#181b20 45%,#090a0d 100%);
}
/* =========================
ВЕРХНЯЯ ПАНЕЛЬ
========================= */
#top{
position:absolute;
top:0;
left:0;
width:100%;
height:76px;
z-index:20;
display:flex;
align-items:center;
justify-content:space-between;
padding:8px 14px;
background:rgba(9,10,13,.94);
border-bottom:1px solid #3a3e45;
box-shadow:0 5px 20px #0008;
}
/* =========================
НОВЫЙ ЛОГОТИП
========================= */
.logo{
display:flex;
align-items:center;
gap:10px;
height:58px;
user-select:none;
}
.logoImage{
width:54px;
height:54px;
object-fit:contain;
filter:
drop-shadow(0 3px 4px #000)
drop-shadow(0 0 7px #8c969f);
mix-blend-mode:screen;
}
.logoText{
font-weight:900;
font-size:20px;
letter-spacing:1px;
color:#e8edf2;
text-shadow:
0 2px 4px #000,
0 0 8px #777;
}
.logoSub{
font-size:8px;
letter-spacing:3px;
color:#7f8993;
margin-top:2px;
}
/* =========================
СТАТИСТИКА
========================= */
.stats{
display:flex;
gap:8px;
}
.stat{
min-width:82px;
padding:8px 11px;
border:1px solid #444951;
border-radius:10px;
background:#191c21;
text-align:center;
font-weight:bold;
}
.stat small{
display:block;
color:#858b94;
font-size:10px;
}
/* =========================
ИГРОВАЯ ОБЛАСТЬ
========================= */
#factory{
position:absolute;
left:0;
right:0;
top:76px;
bottom:100px;
overflow:hidden;
background:
linear-gradient(#ffffff05 1px,transparent 1px),
linear-gradient(90deg,#ffffff05 1px,transparent 1px);
background-size:40px 40px;
}
/* =========================
МАШИНА
========================= */
#machine{
position:absolute;
left:50%;
top:50%;
width:900px;
height:620px;
transform:translate(-50%,-50%);
}
#base{
position:absolute;
left:50%;
top:50%;
width:720px;
height:460px;
transform:translate(-50%,-50%);
border-radius:24px;
border:2px solid #454a52;
background:
linear-gradient(145deg,#292d33,#15181c);
box-shadow:
inset 0 0 0 8px #0b0d10,
inset 0 0 35px #000,
0 20px 50px #000b;
}
#base:before,
#base:after{
content:"";
position:absolute;
width:18px;
height:18px;
border-radius:50%;
background:#08090b;
border:3px solid #555b63;
}
#base:before{
left:25px;
top:25px;
}
#base:after{
right:25px;
bottom:25px;
}
/* =========================
ШЕСТЕРЁНКИ
========================= */
.gear{
position:absolute;
width:140px;
height:140px;
transform:translate(-50%,-50%);
cursor:pointer;
z-index:5;
user-select:none;
touch-action:none;
}
.gear img{
position:absolute;
width:100%;
height:100%;
object-fit:contain;
pointer-events:none;
user-select:none;
mix-blend-mode:screen;
filter:
drop-shadow(0 5px 4px #000)
drop-shadow(0 0 4px #777);
}
.gear .core{
position:absolute;
left:50%;
top:50%;
width:37px;
height:37px;
transform:translate(-50%,-50%);
border-radius:50%;
border:7px solid #70757c;
background:#17191c;
box-shadow:
inset 0 0 7px #000,
0 0 4px #000;
pointer-events:none;
}
.gear .bolt{
position:absolute;
left:50%;
top:50%;
width:12px;
height:12px;
transform:translate(-50%,-50%);
border-radius:50%;
background:#969ba1;
box-shadow:inset 0 2px 2px #fff5;
pointer-events:none;
}
/* =========================
ВРАЩЕНИЕ
========================= */
.gear.cw{
animation:rotateCW var(--speed,3s) linear infinite;
}
.gear.ccw{
animation:rotateCCW var(--speed,3s) linear infinite;
}
.gear.dragging{
animation-play-state:paused !important;
z-index:50;
filter:
drop-shadow(0 0 10px #fff)
drop-shadow(0 0 20px #8fa8c5);
}
@keyframes rotateCW{
from{
transform:
translate(-50%,-50%)
rotate(0deg);
}
to{
transform:
translate(-50%,-50%)
rotate(360deg);
}
}
@keyframes rotateCCW{
from{
transform:
translate(-50%,-50%)
rotate(360deg);
}
to{
transform:
translate(-50%,-50%)
rotate(0deg);
}
}
/* =========================
ВЫБРАННАЯ ШЕСТЕРЁНКА
========================= */
.gear.selected{
filter:
drop-shadow(0 0 10px #9eb5ca)
drop-shadow(0 0 18px #7891a8);
}
.gear.selected:after{
content:"";
position:absolute;
inset:-7px;
border:2px solid #aabed3;
border-radius:50%;
pointer-events:none;
}
/* =========================
ПРОЧНОСТЬ
========================= */
.health{
position:absolute;
left:50%;
top:calc(100% + 3px);
transform:translateX(-50%);
width:95px;
height:7px;
background:#090a0c;
border:1px solid #555;
border-radius:8px;
overflow:hidden;
}
.healthFill{
width:100%;
height:100%;
background:#8fa37b;
transition:.25s;
}
/* =========================
ИНФОРМАЦИЯ
========================= */
#info{
position:absolute;
left:12px;
top:88px;
z-index:25;
width:205px;
padding:12px;
border-radius:15px;
border:1px solid #454a51;
background:#101318ef;
box-shadow:0 10px 30px #000b;
display:none;
}
#info.show{
display:block;
}
.infoTitle{
font-size:18px;
font-weight:900;
margin-bottom:7px;
}
.infoRow{
display:flex;
justify-content:space-between;
padding:4px 0;
color:#b8bec6;
}
.infoRow b{
color:#fff;
}
/* =========================
МАГАЗИН
========================= */
#shop{
position:absolute;
right:12px;
top:88px;
z-index:25;
width:175px;
padding:12px;
border:1px solid #4a5058;
border-radius:15px;
background:#111419ee;
box-shadow:0 10px 30px #000b;
display:none;
}
#shop.show{
display:block;
}
.shopTitle{
font-weight:900;
margin-bottom:8px;
}
.shopGear{
width:100%;
height:100px;
object-fit:contain;
mix-blend-mode:screen;
filter:drop-shadow(0 5px 5px #000);
}
.price{
color:#c4cbd3;
margin-bottom:7px;
}
#buy{
width:100%;
}
/* =========================
НИЖНЯЯ ПАНЕЛЬ
========================= */
#bottom{
position:absolute;
bottom:0;
left:0;
width:100%;
height:100px;
z-index:30;
display:flex;
align-items:center;
justify-content:center;
gap:8px;
padding:8px;
background:#0b0d10ee;
border-top:1px solid #40454c;
box-shadow:0 -5px 25px #000a;
}
button{
border:1px solid #555c65;
background:
linear-gradient(#30353c,#1a1d22);
color:#fff;
border-radius:12px;
min-height:64px;
padding:7px 12px;
font-weight:bold;
font-size:12px;
box-shadow:
inset 0 1px 0 #ffffff12,
0 4px 12px #0008;
transition:
transform .08s,
background .15s,
box-shadow .15s;
}
button:active{
transform:scale(.94);
background:#414750;
box-shadow:
inset 0 0 15px #0008,
0 2px 6px #000;
}
button.active{
background:
linear-gradient(#515b66,#252b31);
border-color:#aebbc7;
box-shadow:
0 0 12px #8294a5,
inset 0 0 10px #fff1;
}
.actionIcon{
width:30px;
height:30px;
object-fit:contain;
display:block;
margin:0 auto 3px;
filter:
brightness(0)
invert(1)
drop-shadow(0 0 3px #fff5);
}
button span{
display:block;
margin-top:3px;
}
/* =========================
СООБЩЕНИЯ
========================= */
#toast{
position:absolute;
left:50%;
top:90px;
transform:
translateX(-50%)
translateY(-20px);
z-index:100;
padding:11px 17px;
border-radius:10px;
background:#15191e;
border:1px solid #656c75;
box-shadow:0 8px 25px #000;
opacity:0;
pointer-events:none;
transition:.25s;
}
#toast.show{
opacity:1;
transform:
translateX(-50%)
translateY(0);
}
/* =========================
МОБИЛЬНЫЙ РЕЖИМ
========================= */
@media(max-width:700px){
#top{
height:64px;
padding:6px 8px;
}
.logo{
gap:6px;
height:52px;
}
.logoImage{
width:43px;
height:43px;
}
.logoText{
font-size:13px;
letter-spacing:.5px;
}
.logoSub{
font-size:6px;
letter-spacing:2px;
}
.stats{
gap:3px;
}
.stat{
min-width:59px;
padding:6px 3px;
font-size:10px;
}
.stat small{
font-size:7px;
}
#factory{
top:64px;
bottom:94px;
}
#machine{
width:700px;
height:500px;
}
#base{
width:590px;
height:370px;
}
#bottom{
height:94px;
gap:4px;
padding:5px;
overflow-x:auto;
justify-content:flex-start;
scrollbar-width:none;
}
#bottom::-webkit-scrollbar{
display:none;
}
button{
flex:0 0 auto;
min-width:64px;
min-height:67px;
padding:5px 3px;
font-size:8px;
}
.actionIcon{
width:28px;
height:28px;
}
.gear{
width:115px;
height:115px;
}
#info{
transform:scale(.85);
transform-origin:top left;
}
#shop{
transform:scale(.85);
transform-origin:top right;
}
}
</style>
</head>
<body>
<!-- =========================
ВЕРХ
========================= -->
<div id="top">
<div class="logo">
<img
class="logoImage"
src="https://img.pikbest.com/element_our/20230502/bg/184c55385d6c5.png!f305cw"
alt="Gear Factory logo"
>
<div>
<div class="logoText">
GEAR FACTORY
</div>
<div class="logoSub">
MECHANICAL SYSTEM
</div>
</div>
</div>
<div class="stats">
<div class="stat">
<span id="money">250</span> $
<small>ДЕНЬГИ</small>
</div>
<div class="stat">
<span id="power">0</span>
<small>МОЩНОСТЬ</small>
</div>
<div class="stat">
<span id="count">0</span>
<small>ДЕТАЛЕЙ</small>
</div>
</div>
</div>
<!-- =========================
ИГРА
========================= -->
<div id="factory">
<div id="machine">
<div id="base"></div>
</div>
</div>
<!-- =========================
ИНФОРМАЦИЯ
========================= -->
<div id="info">
<div class="infoTitle">
Шестерёнка
</div>
<div class="infoRow">
<span>Уровень</span>
<b id="level">1</b>
</div>
<div class="infoRow">
<span>Прочность</span>
<b id="durability">100</b>
</div>
<div class="infoRow">
<span>Скорость</span>
<b id="speedText">1.0x</b>
</div>
<div class="infoRow">
<span>Мощность</span>
<b id="powerText">10</b>
</div>
</div>
<!-- =========================
МАГАЗИН
========================= -->
<div id="shop">
<div class="shopTitle">
МАГАЗИН
</div>
<img
class="shopGear"
src="https://png.pngtree.com/png-clipart/20240208/original/pngtree-small-gear-on-a-white-background-watch-photo-png-image_14259145.png"
>
<div class="price">
Новая шестерёнка — 75 $
</div>
<button id="buy">
КУПИТЬ
</button>
</div>
<div id="toast"></div>
<!-- =========================
КНОПКИ
========================= -->
<div id="bottom">
<button id="repair">
<img
class="actionIcon"
src="https://png.pngtree.com/png-vector/20260224/ourmid/pngtree-industrial-gear-and-wrench-tool-outline-vector-for-repair-service-icon-png-image_18801855.webp"
>
<span>ПОЧИНИТЬ</span>
</button>
<button id="upgrade">
<img
class="actionIcon"
src="https://png.pngtree.com/png-vector/20260224/ourmid/pngtree-industrial-gear-and-wrench-tool-outline-vector-for-repair-service-icon-png-image_18801855.webp"
>
<span>УЛУЧШИТЬ</span>
</button>
<button id="add">
<img
class="actionIcon"
src="https://cdn-icons-png.flaticon.com/512/416/416255.png"
>
<span>ПОСТАВИТЬ</span>
</button>
<button id="shopBtn">
<img
class="actionIcon"
src="https://cdn-icons-png.flaticon.com/512/126/126122.png"
>
<span>МАГАЗИН</span>
</button>
<button id="remove">
<img
class="actionIcon"
src="https://cdn-icons-png.flaticon.com/512/64/64022.png"
>
<span>УБРАТЬ</span>
</button>
<button id="dragButton">
<img
class="actionIcon"
src="https://images.icon-icons.com/4260/PNG/512/gear_cogwheel_arrows_circular_process_icon_265392.png"
>
<span>ПЕРЕТАСКИВАТЬ</span>
</button>
</div>
<script>
/* =========================================
ОСНОВА
========================================= */
const machine=
document.getElementById("machine");
const factory=
document.getElementById("factory");
let money=250;
let selected=null;
let gearID=0;
let dragMode=false;
let draggingGear=null;
let dragPointerId=null;
const gears=[];
/* =========================================
СТАРТОВЫЕ ШЕСТЕРЁНКИ
========================================= */
addGear(330,250,145);
addGear(465,250,110);
addGear(560,250,155);
/* =========================================
СОЗДАНИЕ
========================================= */
function addGear(x,y,size=130){
const data={
id:gearID++,
x:x,
y:y,
size:size,
level:1,
hp:100,
maxHp:100,
speed:1,
power:10,
direction:
gears.length%2===0
? "cw"
: "ccw",
element:null
};
gears.push(data);
createGearElement(data);
updateStats();
return data;
}
/* =========================================
ЭЛЕМЕНТ
========================================= */
function createGearElement(data){
const el=
document.createElement("div");
el.className=
"gear "+data.direction;
el.dataset.id=data.id;
el.style.left=
data.x+"px";
el.style.top=
data.y+"px";
el.style.width=
data.size+"px";
el.style.height=
data.size+"px";
el.style.setProperty(
"--speed",
Math.max(
.45,
3.2/data.speed
)+"s"
);
el.innerHTML=`
<img
src="https://png.pngtree.com/png-clipart/20240208/original/pngtree-small-gear-on-a-white-background-watch-photo-png-image_14259145.png"
>
<div class="core"></div>
<div class="bolt"></div>
<div class="health">
<div class="healthFill"></div>
</div>
`;
machine.appendChild(el);
data.element=el;
updateHealth(data);
el.addEventListener(
"pointerdown",
gearPointerDown
);
}
/* =========================================
НАЖАТИЕ
========================================= */
function gearPointerDown(e){
e.stopPropagation();
const id=
Number(this.dataset.id);
const data=
gears.find(
g=>g.id===id
);
if(!data)return;
selectGear(data);
if(!dragMode)return;
startDragging(
e,
data
);
}
/* =========================================
ВЫБОР
========================================= */
function selectGear(data){
selected=data;
document
.querySelectorAll(".gear")
.forEach(g=>{
g.classList.remove("selected");
});
data.element
.classList.add("selected");
document
.getElementById("info")
.classList.add("show");
updateInfo();
}
/* =========================================
РЕЖИМ ПЕРЕТАСКИВАНИЯ
========================================= */
document
.getElementById("dragButton")
.addEventListener(
"click",
()=>{
dragMode=!dragMode;
document
.getElementById("dragButton")
.classList.toggle(
"active",
dragMode
);
toast(
dragMode
? "Режим перетаскивания включён"
: "Режим перетаскивания выключен"
);
}
);
/* =========================================
НАЧАЛО ПЕРЕТАСКИВАНИЯ
========================================= */
function startDragging(e,data){
draggingGear=data;
dragPointerId=
e.pointerId;
data.element
.classList
.add("dragging");
try{
data.element
.setPointerCapture(
e.pointerId
);
}catch(err){}
updateGearPosition(
e,
data
);
data.element.addEventListener(
"pointermove",
draggingMove
);
data.element.addEventListener(
"pointerup",
draggingEnd
);
data.element.addEventListener(
"pointercancel",
draggingEnd
);
}
/* =========================================
ДВИЖЕНИЕ
========================================= */
function draggingMove(e){
if(!draggingGear)return;
if(
e.pointerId!==dragPointerId
)return;
updateGearPosition(
e,
draggingGear
);
}
/* =========================================
ПОЗИЦИЯ
========================================= */
function updateGearPosition(e,data){
const rect=
machine.getBoundingClientRect();
const scaleX=
machine.offsetWidth/
rect.width;
const scaleY=
machine.offsetHeight/
rect.height;
let x=
(e.clientX-rect.left)*
scaleX;
let y=
(e.clientY-rect.top)*
scaleY;
const margin=
data.size*.35;
x=Math.max(
margin,
Math.min(
machine.offsetWidth-margin,
x
)
);
y=Math.max(
margin,
Math.min(
machine.offsetHeight-margin,
y
)
);
data.x=x;
data.y=y;
data.element.style.left=
x+"px";
data.element.style.top=
y+"px";
}
/* =========================================
КОНЕЦ ПЕРЕТАСКИВАНИЯ
========================================= */
function draggingEnd(e){
if(!draggingGear)return;
if(
e.pointerId!==dragPointerId
)return;
const gear=
draggingGear;
gear.element
.classList
.remove("dragging");
gear.element
.removeEventListener(
"pointermove",
draggingMove
);
gear.element
.removeEventListener(
"pointerup",
draggingEnd
);
gear.element
.removeEventListener(
"pointercancel",
draggingEnd
);
try{
gear.element
.releasePointerCapture(
e.pointerId
);
}catch(err){}
draggingGear=null;
dragPointerId=null;
updateConnections();
toast(
"Шестерёнка перемещена"
);
}
/* =========================================
ИНФОРМАЦИЯ
========================================= */
function updateInfo(){
if(!selected)return;
document.getElementById("level")
.textContent=
selected.level;
document.getElementById("durability")
.textContent=
Math.round(
selected.hp
);
document.getElementById("speedText")
.textContent=
selected.speed.toFixed(1)
+"x";
document.getElementById("powerText")
.textContent=
selected.power;
}
/* =========================================
ПРОЧНОСТЬ
========================================= */
function updateHealth(data){
if(!data.element)return;
const fill=
data.element
.querySelector(
".healthFill"
);
fill.style.width=
Math.max(
0,
data.hp/data.maxHp*100
)+"%";
}
/* =========================================
ДЕНЬГИ
========================================= */
function spend(amount){
if(money<amount){
toast(
"Недостаточно денег"
);
return false;
}
money-=amount;
updateStats();
return true;
}
/* =========================================
ПОЧИНИТЬ
========================================= */
document
.getElementById("repair")
.addEventListener(
"click",
()=>{
if(!selected){
toast(
"Сначала выбери шестерёнку"
);
return;
}
if(
selected.hp>=
selected.maxHp
){
toast(
"Она уже полностью исправна"
);
return;
}
const missing=
selected.maxHp-
selected.hp;
const price=
Math.max(
5,
Math.ceil(
missing*.45
)
);
if(!spend(price))
return;
selected.hp=
selected.maxHp;
updateHealth(selected);
updateInfo();
toast(
"Шестерёнка починена"
);
}
);
/* =========================================
УЛУЧШИТЬ
========================================= */
document
.getElementById("upgrade")
.addEventListener(
"click",
()=>{
if(!selected){
toast(
"Сначала выбери шестерёнку"
);
return;
}
const price=
50*selected.level;
if(!spend(price))
return;
selected.level++;
selected.maxHp+=35;
selected.hp=
selected.maxHp;
selected.speed+=.35;
selected.power+=12;
selected.element
.style.setProperty(
"--speed",
Math.max(
.35,
3.2/
selected.speed
)+"s"
);
updateHealth(selected);
updateInfo();
updateStats();
toast(
"Уровень повышен до "+
selected.level
);
}
);
/* =========================================
ПОКУПКА
========================================= */
document
.getElementById("buy")
.addEventListener(
"click",
()=>{
if(!spend(75))
return;
const angle=
Math.random()*
Math.PI*2;
const radius=160;
const x=
450+
Math.cos(angle)*
radius;
const y=
310+
Math.sin(angle)*
radius;
const g=
addGear(
x,
y,
95+
Math.random()*65
);
selectGear(g);
toast(
"Новая шестерёнка установлена"
);
}
);
/* =========================================
ПОСТАВИТЬ
========================================= */
document
.getElementById("add")
.addEventListener(
"click",
()=>{
const g=
addGear(
300+
Math.random()*300,
170+
Math.random()*250,
100+
Math.random()*60
);
selectGear(g);
toast(
"Шестерёнка добавлена"
);
}
);
/* =========================================
УБРАТЬ
========================================= */
document
.getElementById("remove")
.addEventListener(
"click",
()=>{
if(!selected){
toast(
"Выбери шестерёнку"
);
return;
}
selected.element.remove();
const index=
gears.indexOf(
selected
);
if(index!==-1){
gears.splice(
index,
1
);
}
selected=null;
document
.getElementById("info")
.classList
.remove("show");
updateStats();
toast(
"Шестерёнка убрана"
);
}
);
/* =========================================
МАГАЗИН
========================================= */
document
.getElementById("shopBtn")
.addEventListener(
"click",
()=>{
document
.getElementById("shop")
.classList.toggle(
"show"
);
}
);
/* =========================================
СВЯЗИ ШЕСТЕРЁНОК
========================================= */
function updateConnections(){
for(
let i=0;
i<gears.length;
i++
){
const a=gears[i];
if(!a.element)
continue;
for(
let j=i+1;
j<gears.length;
j++
){
const b=gears[j];
const dx=
a.x-b.x;
const dy=
a.y-b.y;
const distance=
Math.sqrt(
dx*dx+
dy*dy
);
const contact=
(a.size+b.size)*.46;
if(
distance<contact
){
if(
a.direction===
b.direction
){
b.direction=
b.direction==="cw"
? "ccw"
: "cw";
b.element
.classList
.remove(
"cw",
"ccw"
);
b.element
.classList
.add(
b.direction
);
}
}
}
}
}
/* =========================================
СТАТИСТИКА
========================================= */
function updateStats(){
document
.getElementById("money")
.textContent=
money;
document
.getElementById("count")
.textContent=
gears.length;
let total=0;
gears.forEach(g=>{
total+=
g.power*
(g.hp/g.maxHp);
});
document
.getElementById("power")
.textContent=
Math.round(total);
}
/* =========================================
СООБЩЕНИЯ
========================================= */
let toastTimer;
function toast(text){
const box=
document.getElementById(
"toast"
);
box.textContent=text;
box.classList.add("show");
clearTimeout(
toastTimer
);
toastTimer=
setTimeout(
()=>{
box.classList.remove(
"show"
);
},
1600
);
}
/* =========================================
ИЗНОС
========================================= */
setInterval(
()=>{
gears.forEach(g=>{
g.hp-=
.8*g.speed;
if(g.hp<0)
g.hp=0;
updateHealth(g);
});
updateStats();
if(selected)
updateInfo();
},
2500
);
/* =========================================
ПРОВЕРКА СОЕДИНЕНИЙ
========================================= */
setInterval(
()=>{
updateConnections();
},
700
);
/* =========================================
КЛИК ПО ПУСТОМУ МЕСТУ
========================================= */
factory.addEventListener(
"pointerdown",
e=>{
if(
e.target.closest(".gear")
){
return;
}
if(selected){
selected.element
.classList
.remove("selected");
selected=null;
document
.getElementById("info")
.classList
.remove("show");
}
}
);
/* =========================================
СТАРТ
========================================= */
updateStats();
</script>
</body>
</html>Game Source: Gear Factory
Creator: SparkDragon18
Libraries: none
Complexity: complex (1925 lines, 26.5 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: gear-factory-sparkdragon18" to link back to the original. Then publish at arcadelab.ai/publish.