NEON SHOOTER
by PrismPilot233647 lines144.6 KB
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>NEON SHOOTER</title>
<script src="/sdk.js"></script>
<style>
html, body {
margin: 0; padding: 0; width: 100%; height: 100%;
background: #05050a; overflow: hidden;
font-family: Consolas, 'Courier New', monospace;
touch-action: none; overscroll-behavior: none;
-webkit-user-select: none; user-select: none;
-webkit-touch-callout: none;
}
canvas {
display: block; position: fixed; top: 0; left: 0;
background: #07070d; cursor: crosshair;
touch-action: none; -webkit-tap-highlight-color: transparent;
}
#cheatInput {
position: fixed; display: none;
background: rgba(14,20,40,0.98);
border: 2px solid #7df9ff; color: #7df9ff;
font-family: Consolas, 'Courier New', monospace;
letter-spacing: 2px; outline: none; border-radius: 4px;
box-shadow: 0 0 22px rgba(125,249,255,0.6);
text-align: center; text-transform: lowercase;
z-index: 100; box-sizing: border-box;
}
#cheatInput::placeholder { color: rgba(125,249,255,0.35); letter-spacing: 1px; }
#cheatInput:focus { box-shadow: 0 0 30px rgba(125,249,255,0.9); border-color: #ffffff; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<input id="cheatInput" type="text" placeholder="введи код..." autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" maxlength="24">
<script>
"use strict";
/* =========================================================================
YANDEX GAMES SDK
========================================================================= */
let ysdk = null;
let sdkPlayer = null;
let sdkReady = false;
async function initYandexSDK() {
try {
ysdk = await YaGames.init();
try {
sdkPlayer = await ysdk.getPlayer({ scopes: false });
} catch (e) { sdkPlayer = null; }
ysdk.on('game_api_pause', () => {
if (game.state === 'playing') {
game.state = 'paused';
game.moveStick.end(); game.aimStick.end();
}
sound.muteForPause();
setGameplayActive(false);
});
ysdk.on('game_api_resume', () => {
sound.unmuteAfterPause();
});
sdkReady = true;
} catch (e) { sdkReady = false; }
}
async function savePlayerProgress(data) {
if (!sdkPlayer) return;
try { await sdkPlayer.setData(data, false); } catch (e) {}
}
async function loadPlayerProgress() {
if (!sdkPlayer) return null;
try { return await sdkPlayer.getData(['highScore', 'totalKills', 'totalRooms']); }
catch (e) { return null; }
}
function notifyGameReady() {
if (ysdk && ysdk.features && ysdk.features.LoadingAPI) {
try { ysdk.features.LoadingAPI.ready(); } catch (e) {}
}
}
function setGameplayActive(active) {
if (ysdk && ysdk.features && ysdk.features.GameplayAPI) {
try {
if (active) ysdk.features.GameplayAPI.start();
else ysdk.features.GameplayAPI.stop();
} catch (e) {}
}
}
function showFullscreenAd(onClose) {
if (!ysdk || !ysdk.adv) { if (onClose) onClose(); return; }
try {
ysdk.adv.showFullscreenAdv({
callbacks: {
onClose: () => { if (onClose) onClose(); },
onError: () => { if (onClose) onClose(); }
}
});
} catch (e) { if (onClose) onClose(); }
}
/* =========================================================================
КОНФИГУРАЦИЯ
========================================================================= */
const CONFIG = {
W: 1280, H: 720,
WORLD_W: 2600, WORLD_H: 1900,
FIXED_DT: 1 / 60, GRID: 80,
PLAYER: {
radius: 15, speed: 167, sprintMult: 1.75, hp: 100,
magSize: 30, reloadTime: 1.45, fireRate: 0.085,
bulletSpeed: 980, damage: 17, spread: 0.035,
spreadPerShot: 0.022, maxSpread: 0.16, spreadRecover: 0.42,
dash: { speed: 900, duration: 0.15, cooldown: 1.5 },
rage: { duration: 6.0, cooldown: 20.0, damageMult: 1.5, fireRateMult: 0.7, speedMult: 1.25 }
},
BOT_TYPES: {
normal: { key:'normal', name:'Обычный', color:'#ff3b6b', radius:15, hp:60,
speed:138, vision:560, fireRate:0.5, damage:9, bulletSpeed:640,
spread:0.085, score:100, magSize:10, reloadTime:2.0, preferred:260 },
heavy: { key:'heavy', name:'Тяжёлый', color:'#ff9f1c', radius:22, hp:195,
speed:88, vision:470, fireRate:1.1, damage:21, bulletSpeed:500,
spread:0.14, score:250, magSize:6, reloadTime:3.0, preferred:190 },
sniper: { key:'sniper', name:'Снайпер', color:'#b06bff', radius:14, hp:45,
speed:122, vision:980, fireRate:1.7, damage:33, bulletSpeed:1180,
spread:0.018, score:200, magSize:3, reloadTime:3.2, preferred:440 },
sapper: { key:'sapper', name:'Подрывник', color:'#ffcc00', radius:16,
hp:38, speed:215, vision:1200, fireRate:0, damage:0, bulletSpeed:0,
spread:0, score:180, magSize:0, reloadTime:0, preferred:0,
explodeRadius: 135, explodeDmgPlayer: 40, explodeDmgBots: 60 }
},
HEAL_ON_BREAK: 0.35,
ROOM_HP_SCALE: 0.15, ROOM_DMG_SCALE: 0.10, ROOM_SPEED_SCALE: 0.03,
BOSS_EVERY: 5
};
const DIFFICULTIES = {
easy: { key:'easy', name:'ЛЕГКО', color:'#00e5a0',
playerHpMult:1.5, botHpMult:0.8, botDmgMult:0.7,
botCount:5, botGrowth:2, respawns:99,
desc:'Старт 5 · +2 за комнату · ∞ возрождений' },
medium: { key:'medium', name:'СРЕДНЕ', color:'#ffcc00',
playerHpMult:1.0, botHpMult:1.0, botDmgMult:1.0,
botCount:5, botGrowth:3, respawns:3,
desc:'Старт 5 · +3 за комнату · 3 возрождения' },
hard: { key:'hard', name:'СЛОЖНО', color:'#ff9f1c',
playerHpMult:0.85, botHpMult:1.3, botDmgMult:1.3,
botCount:10, botGrowth:4, respawns:1,
desc:'Старт 10 · +4 за комнату · 1 возрождение' },
hardcore: { key:'hardcore', name:'ХАРДКОР', color:'#ff3b6b',
playerHpMult:1.0, botHpMult:1.5, botDmgMult:1.5,
botCount:20, botGrowth:6, respawns:0,
desc:'Старт 20 · +6 за комнату · Без возрождений' }
};
const DIFFICULTY_ORDER = ['easy','medium','hard','hardcore'];
/* =========================================================================
МАТЕМАТИКА
========================================================================= */
const TAU = Math.PI * 2;
const clamp = (v,a,b)=> v<a?a:v>b?b:v;
const rand = (a,b)=> a + Math.random()*(b-a);
const lerp = (a,b,t)=> a + (b-a)*t;
const dist2 = (ax,ay,bx,by)=> Math.hypot(ax-bx, ay-by);
function normAngle(a){ a%=TAU; if(a>Math.PI)a-=TAU; if(a<-Math.PI)a+=TAU; return a; }
function angleDiff(a,b){ return normAngle(b-a); }
function turnToward(a,b,step){
const d = angleDiff(a,b);
if (Math.abs(d) <= step) return b;
return a + Math.sign(d)*step;
}
function pointSegDist(px,py,x1,y1,x2,y2){
const dx=x2-x1, dy=y2-y1, l2=dx*dx+dy*dy;
let t = l2>1e-6 ? ((px-x1)*dx+(py-y1)*dy)/l2 : 0;
t = clamp(t,0,1);
return Math.hypot(px-(x1+t*dx), py-(y1+t*dy));
}
function segIntersectsRect(x1,y1,x2,y2,r){
let t0=0,t1=1;
const dx=x2-x1, dy=y2-y1;
const p=[-dx,dx,-dy,dy];
const q=[x1-r.x, r.x+r.w-x1, y1-r.y, r.y+r.h-y1];
for(let i=0;i<4;i++){
if(p[i]===0){ if(q[i]<0) return false; }
else {
const t=q[i]/p[i];
if(p[i]<0){ if(t>t1) return false; if(t>t0) t0=t; }
else { if(t<t0) return false; if(t<t1) t1=t; }
}
}
return true;
}
function rectsOverlap(a,b,pad=0){
return !(a.x+a.w+pad<b.x || b.x+b.w+pad<a.x ||
a.y+a.h+pad<b.y || b.y+b.h+pad<a.y);
}
/* =========================================================================
ЗВУК
========================================================================= */
class Sound {
constructor(){
this.ctx = null; this.master = null;
this.enabled = true; this.volume = 0.55;
this._lastPlayed = {};
}
init(){
if(this.ctx) return;
try {
const AC = window.AudioContext || window.webkitAudioContext;
if(!AC) { this.enabled = false; return; }
this.ctx = new AC();
this.master = this.ctx.createGain();
this.master.gain.value = this.volume;
this.master.connect(this.ctx.destination);
} catch(e){ this.enabled = false; }
}
resume(){ if(this.ctx && this.ctx.state === 'suspended') this.ctx.resume(); }
toggle(){
this.enabled = !this.enabled;
if(this.master) this.master.gain.value = this.enabled ? this.volume : 0;
return this.enabled;
}
muteForPause(){ if(this.master) this.master.gain.value = 0; }
unmuteAfterPause(){ if(this.master && this.enabled) this.master.gain.value = this.volume; }
setVolume(v){
this.volume = clamp(v, 0, 1);
if(this.master && this.enabled) this.master.gain.value = this.volume;
}
_throttle(key, ms=40){
const now = performance.now();
if(this._lastPlayed[key] && now - this._lastPlayed[key] < ms) return false;
this._lastPlayed[key] = now;
return true;
}
spatialVolume(x, y, camX, camY, maxDist = 900){
const d = Math.hypot(x - camX, y - camY);
if(d >= maxDist) return 0;
return 1 - (d / maxDist);
}
_tone(freq, duration, type='square', vol=0.2, freqEnd=null, delay=0, spatial=1){
if(!this.ctx || !this.enabled || spatial <= 0.01) return;
const v = vol * spatial;
if(v <= 0.001) return;
const t = this.ctx.currentTime + delay;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(Math.max(1, freq), t);
if(freqEnd !== null && freqEnd > 0){
osc.frequency.exponentialRampToValueAtTime(Math.max(1, freqEnd), t + duration);
}
gain.gain.setValueAtTime(0.0001, t);
gain.gain.exponentialRampToValueAtTime(v, t + 0.005);
gain.gain.exponentialRampToValueAtTime(0.0001, t + duration);
osc.connect(gain); gain.connect(this.master);
osc.start(t); osc.stop(t + duration + 0.02);
}
_noise(duration, vol=0.3, filterStart=3000, filterEnd=200, delay=0, spatial=1){
if(!this.ctx || !this.enabled || spatial <= 0.01) return;
const v = vol * spatial;
if(v <= 0.001) return;
const t = this.ctx.currentTime + delay;
const sr = this.ctx.sampleRate;
const bufSize = Math.max(1, Math.floor(sr * duration));
const buffer = this.ctx.createBuffer(1, bufSize, sr);
const data = buffer.getChannelData(0);
for(let i = 0; i < bufSize; i++) data[i] = Math.random() * 2 - 1;
const src = this.ctx.createBufferSource();
src.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(filterStart, t);
filter.frequency.exponentialRampToValueAtTime(Math.max(20, filterEnd), t + duration);
filter.Q.value = 1.2;
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(v, t);
gain.gain.exponentialRampToValueAtTime(0.0001, t + duration);
src.connect(filter); filter.connect(gain); gain.connect(this.master);
src.start(t); src.stop(t + duration + 0.02);
}
shoot(spatial=1){ if(!this._throttle('shoot',30)) return;
this._tone(950, 0.075, 'square', 0.11, 200, 0, spatial);
this._noise(0.05, 0.06, 3500, 900, 0, spatial); }
botShoot(spatial=1){ if(!this._throttle('botShoot',25)) return;
this._tone(420, 0.10, 'sawtooth', 0.055, 90, 0, spatial);
this._noise(0.06, 0.045, 1800, 400, 0, spatial); }
sniperShoot(spatial=1){ if(!this._throttle('sniperShoot',60)) return;
this._tone(1400, 0.18, 'sawtooth', 0.09, 180, 0, spatial);
this._noise(0.12, 0.05, 4200, 500, 0, spatial); }
heavyShoot(spatial=1){ if(!this._throttle('heavyShoot',60)) return;
this._tone(180, 0.20, 'square', 0.10, 60, 0, spatial);
this._noise(0.16, 0.08, 900, 100, 0, spatial); }
droneShoot(spatial=1){ if(!this._throttle('droneShoot',40)) return;
this._tone(1650, 0.045, 'square', 0.045, 800, 0, spatial); }
hit(spatial=1){ if(!this._throttle('hit',20)) return;
this._noise(0.05, 0.09, 2200, 300, 0, spatial);
this._tone(320, 0.045, 'square', 0.05, 120, 0, spatial); }
playerHit(){ if(!this._throttle('playerHit',80)) return;
this._tone(500, 0.16, 'sawtooth', 0.16, 90);
this._noise(0.10, 0.13, 1500, 200); }
explosion(spatial=1){
this._noise(0.42, 0.20, 1200, 60, 0, spatial);
this._tone(90, 0.35, 'sine', 0.12, 30, 0, spatial);
this._tone(220, 0.18, 'square', 0.06, 60, 0.01, spatial); }
bigExplosion(){
this._noise(0.9, 0.28, 1600, 40);
this._tone(70, 0.8, 'sine', 0.20, 25);
this._tone(180, 0.5, 'sawtooth', 0.12, 45, 0.02);
this._tone(400, 0.3, 'square', 0.08, 80, 0.05); }
reloadStart(){
this._noise(0.04, 0.10, 1800, 400);
this._tone(220, 0.05, 'square', 0.06, 110, 0.03); }
reloadDone(){
this._tone(880, 0.06, 'square', 0.08, 1200, 0);
this._tone(1400, 0.05, 'square', 0.05, 900, 0.05); }
upgrade(){
if(!this.ctx || !this.enabled) return;
this._tone(660, 0.10, 'triangle', 0.10, 880, 0);
this._tone(880, 0.10, 'triangle', 0.10, 1100, 0.07);
this._tone(1320, 0.16, 'triangle', 0.10, 1650, 0.14); }
deny(){ this._tone(200, 0.14, 'square', 0.10, 120); }
roomStart(){
if(!this.ctx || !this.enabled) return;
this._tone(80, 0.6, 'sine', 0.14, 40);
this._tone(400, 0.4, 'triangle', 0.09, 200, 0.05);
this._tone(600, 0.4, 'triangle', 0.08, 300, 0.10);
this._tone(800, 0.4, 'triangle', 0.07, 400, 0.15); }
roomClear(){
if(!this.ctx || !this.enabled) return;
this._tone(523, 0.35, 'triangle', 0.11, null, 0);
this._tone(659, 0.35, 'triangle', 0.11, null, 0.08);
this._tone(784, 0.55, 'triangle', 0.12, null, 0.16); }
respawn(){ this._tone(200, 0.5, 'sine', 0.14, 900);
this._tone(400, 0.5, 'triangle', 0.09, 1200, 0.02); }
gameOver(){
if(!this.ctx || !this.enabled) return;
this._tone(440, 1.2, 'sawtooth', 0.16, 60);
this._tone(220, 1.4, 'sine', 0.12, 40, 0.15);
this._noise(1.0, 0.10, 800, 50, 0.1); }
click(){ this._tone(1200, 0.03, 'square', 0.06, 700); }
startGame(){
if(!this.ctx || !this.enabled) return;
this._tone(220, 0.35, 'triangle', 0.14, 440, 0);
this._tone(330, 0.35, 'triangle', 0.12, 550, 0.05);
this._tone(440, 0.5, 'triangle', 0.12, 660, 0.1); }
tickLowHp(){ if(!this._throttle('lowHp',700)) return;
this._tone(180, 0.15, 'sine', 0.10, 120); }
dash(){
if(!this.ctx || !this.enabled) return;
this._tone(180, 0.18, 'sawtooth', 0.14, 1400);
this._noise(0.15, 0.10, 4000, 800); }
rage(){
if(!this.ctx || !this.enabled) return;
this._tone(120, 0.5, 'sawtooth', 0.16, 180);
this._tone(240, 0.5, 'square', 0.10, 320, 0.03);
this._tone(360, 0.5, 'triangle', 0.08, 480, 0.06);
this._noise(0.4, 0.10, 500, 200, 0); }
rageEnd(){ this._tone(400, 0.25, 'sine', 0.08, 200); }
bossAppear(){
if(!this.ctx || !this.enabled) return;
this._tone(60, 1.4, 'sawtooth', 0.18, 35);
this._tone(90, 1.2, 'square', 0.10, 50, 0.1);
this._tone(150, 1.0, 'sawtooth', 0.08, 70, 0.2);
this._noise(1.2, 0.16, 400, 80, 0);
this._noise(0.8, 0.10, 1200, 200, 0.3); }
bossHiss(){
if(!this._throttle('bossHiss', 400)) return;
this._noise(0.35, 0.10, 3000, 400);
this._tone(500, 0.25, 'sawtooth', 0.06, 200); }
bossDefeated(){
if(!this.ctx || !this.enabled) return;
this._noise(1.6, 0.30, 1600, 40);
this._tone(60, 1.6, 'sine', 0.22, 20);
this._tone(200, 1.0, 'sawtooth', 0.12, 40, 0.1);
this._tone(523, 0.6, 'triangle', 0.14, null, 0.8);
this._tone(659, 0.6, 'triangle', 0.14, null, 1.0);
this._tone(784, 0.9, 'triangle', 0.15, null, 1.2); }
sapperAlarm(spatial=1, fast=false){
if(!this._throttle('sapperAlarm', fast ? 90 : 220)) return;
this._tone(fast ? 1400 : 1100, 0.05, 'square', 0.07, 800, 0, spatial); }
sapperExplode(spatial=1){
this._noise(0.6, 0.28, 2000, 50, 0, spatial);
this._tone(80, 0.5, 'sine', 0.18, 25, 0, spatial);
this._tone(180, 0.3, 'sawtooth', 0.10, 40, 0.02, spatial);
this._tone(420, 0.2, 'square', 0.08, 90, 0.05, spatial); }
cheatOn(){
if(!this.ctx || !this.enabled) return;
this._tone(880, 0.08, 'triangle', 0.14, 1320, 0);
this._tone(1320, 0.10, 'triangle', 0.12, 1760, 0.06);
this._tone(1760, 0.14, 'square', 0.09, 2200, 0.12); }
cheatOff(){
if(!this.ctx || !this.enabled) return;
this._tone(880, 0.10, 'sawtooth', 0.12, 440);
this._tone(440, 0.15, 'square', 0.10, 220, 0.06); }
megaUpgrade(){
if(!this.ctx || !this.enabled) return;
for(let i = 0; i < 6; i++){
this._tone(660 + i*220, 0.14, 'triangle', 0.11, 880 + i*220, i*0.07);
}
this._noise(0.6, 0.10, 3000, 400, 0.3);
}
}
const sound = new Sound();
/* =========================================================================
VIRTUAL JOYSTICK
========================================================================= */
class VirtualJoystick {
constructor(radius=58){
this.radius=radius; this.stickRadius=24;
this.active=false; this.touchId=null;
this.baseX=0; this.baseY=0;
this.stickX=0; this.stickY=0;
this.dx=0; this.dy=0;
}
start(id,x,y){ this.active=true; this.touchId=id;
this.baseX=x; this.baseY=y; this.stickX=x; this.stickY=y;
this.dx=0; this.dy=0; }
move(x,y){
let dx=x-this.baseX, dy=y-this.baseY;
const d=Math.hypot(dx,dy);
if(d>this.radius){ dx=dx/d*this.radius; dy=dy/d*this.radius; }
this.stickX=this.baseX+dx; this.stickY=this.baseY+dy;
this.dx=dx/this.radius; this.dy=dy/this.radius;
}
end(){ this.active=false; this.touchId=null; this.dx=0; this.dy=0; }
getMag(){ return Math.hypot(this.dx,this.dy); }
draw(ctx){
if(!this.active) return;
ctx.globalAlpha=0.35;
ctx.fillStyle='rgba(10,20,45,0.55)';
ctx.beginPath(); ctx.arc(this.baseX,this.baseY,this.radius,0,TAU); ctx.fill();
ctx.globalAlpha=0.55; ctx.strokeStyle='#7df9ff'; ctx.lineWidth=3;
ctx.beginPath(); ctx.arc(this.baseX,this.baseY,this.radius,0,TAU); ctx.stroke();
ctx.globalAlpha=0.85; ctx.fillStyle='#22e6ff';
ctx.shadowColor='#22e6ff'; ctx.shadowBlur=14;
ctx.beginPath(); ctx.arc(this.stickX,this.stickY,this.stickRadius,0,TAU); ctx.fill();
ctx.shadowBlur=0; ctx.globalAlpha=1;
}
}
/* =========================================================================
WALL / PARTICLE / RING / BULLET
========================================================================= */
class Wall {
constructor(x,y,w,h){ this.x=x; this.y=y; this.w=w; this.h=h; }
circleCollide(cx,cy,r){
const nx=clamp(cx,this.x,this.x+this.w);
const ny=clamp(cy,this.y,this.y+this.h);
const dx=cx-nx, dy=cy-ny, d2=dx*dx+dy*dy;
if(d2>r*r) return null;
if(d2>1e-4){
const d=Math.sqrt(d2);
return { nx:dx/d, ny:dy/d, depth:r-d };
}
const L=cx-this.x, R=this.x+this.w-cx;
const T=cy-this.y, B=this.y+this.h-cy;
const m=Math.min(L,R,T,B);
if(m===L) return {nx:-1,ny:0,depth:r+L};
if(m===R) return {nx:1,ny:0,depth:r+R};
if(m===T) return {nx:0,ny:-1,depth:r+T};
return {nx:0,ny:1,depth:r+B};
}
draw(ctx){
ctx.fillStyle='#12122a';
ctx.fillRect(this.x,this.y,this.w,this.h);
ctx.strokeStyle='#3b6bff'; ctx.lineWidth=2;
ctx.shadowColor='#3b6bff'; ctx.shadowBlur=14;
ctx.strokeRect(this.x+1,this.y+1,this.w-2,this.h-2);
ctx.shadowBlur=0;
}
}
class Particle {
constructor(x,y,vx,vy,life,color,size,type='spark'){
this.x=x; this.y=y; this.vx=vx; this.vy=vy;
this.life=life; this.maxLife=life;
this.color=color; this.size=size; this.type=type;
this.dead=false;
}
update(dt){
this.x+=this.vx*dt; this.y+=this.vy*dt;
const f=Math.pow(0.04,dt);
this.vx*=f; this.vy*=f;
this.life-=dt;
if(this.life<=0) this.dead=true;
}
draw(ctx){
const a=Math.max(0,this.life/this.maxLife);
ctx.globalAlpha=a;
ctx.fillStyle=this.color;
ctx.beginPath();
ctx.arc(this.x,this.y,this.size*(this.type==='spark'?a:1),0,TAU);
ctx.fill();
ctx.globalAlpha=1;
}
}
class Ring {
constructor(x,y,color,maxR,life){
this.x=x; this.y=y; this.color=color;
this.maxR=maxR; this.life=life; this.maxLife=life;
this.dead=false;
}
update(dt){ this.life-=dt; if(this.life<=0) this.dead=true; }
draw(ctx){
const t=1-this.life/this.maxLife;
ctx.globalAlpha=(1-t)*0.9;
ctx.strokeStyle=this.color;
ctx.lineWidth=4*(1-t)+1;
ctx.beginPath(); ctx.arc(this.x,this.y,this.maxR*t,0,TAU); ctx.stroke();
ctx.globalAlpha=1;
}
}
class Bullet {
constructor(x,y,angle,speed,damage,owner,color,radius=3){
this.x=x; this.y=y; this.px=x; this.py=y;
this.vx=Math.cos(angle)*speed;
this.vy=Math.sin(angle)*speed;
this.damage=damage; this.owner=owner;
this.color=color; this.r=radius;
this.life=2.4; this.dead=false; this.fromDrone=false; this.isBossBullet=false;
}
update(dt,game){
this.px=this.x; this.py=this.y;
this.x+=this.vx*dt; this.y+=this.vy*dt;
this.life-=dt;
if(this.life<=0){ this.dead=true; return; }
for(const w of game.walls){
if(this.x>w.x && this.x<w.x+w.w && this.y>w.y && this.y<w.y+w.h){
this.dead=true;
game.spawnSparks(this.x,this.y,this.color,5);
return;
}
}
const targets=[];
if(game.player && game.player.alive && game.player!==this.owner) targets.push(game.player);
for(const b of game.bots) if(b.alive && b!==this.owner) targets.push(b);
for(const t of targets){
const d=pointSegDist(t.x,t.y,this.px,this.py,this.x,this.y);
if(d < t.r + this.r){
game.damageEntity(t,this.damage,this.owner);
game.spawnBlood(this.x,this.y,this.color,5);
this.dead=true;
return;
}
}
if(game.boss && game.boss.alive && game.boss !== this.owner){
const parts = [{x: game.boss.x, y: game.boss.y, r: game.boss.r}].concat(game.boss.segments);
for(const part of parts){
const d = pointSegDist(part.x, part.y, this.px, this.py, this.x, this.y);
if(d < part.r + this.r){
game.damageEntity(game.boss, this.damage, this.owner);
game.spawnBlood(this.x, this.y, this.color, 6);
this.dead = true;
return;
}
}
}
}
draw(ctx){
const a=Math.atan2(this.vy,this.vx);
const len=this.fromDrone?14:(this.isBossBullet?30:22);
ctx.strokeStyle=this.color; ctx.lineWidth=this.r*1.1; ctx.lineCap='round';
ctx.shadowColor=this.color; ctx.shadowBlur=this.isBossBullet?18:10;
ctx.beginPath();
ctx.moveTo(this.x-Math.cos(a)*len, this.y-Math.sin(a)*len);
ctx.lineTo(this.x,this.y);
ctx.stroke(); ctx.shadowBlur=0;
ctx.fillStyle='#ffffff';
ctx.beginPath(); ctx.arc(this.x,this.y,this.r*0.6,0,TAU); ctx.fill();
}
}
/* =========================================================================
PLAYER — с поддержкой русской раскладки (ЦФЫВ)
========================================================================= */
class Player {
constructor(x,y){
const C=CONFIG.PLAYER;
this.x=x; this.y=y; this.vx=0; this.vy=0;
this.r=C.radius;
this.hp=C.hp; this.maxHp=C.hp;
this.angle=0; this.mag=C.magSize;
this.reloading=false; this.reloadTimer=0;
this.fireTimer=0; this.spread=0;
this.hitFlash=0; this.alive=true;
this.color='#22e6ff'; this.isPlayer=true;
this.damageMult=1; this.fireRateMult=1;
this.reloadMult=1; this.speedMult=1;
this.magBonus=0;
this.dashTimer=0; this.dashCooldownTimer=0; this.dashAngle=0;
this.rageActive=false; this.rageTimer=0; this.rageCooldownTimer=0;
this.invulnerable=false;
}
get maxMag(){ return CONFIG.PLAYER.magSize + this.magBonus; }
get dashReady(){ return this.dashTimer <= 0 && this.dashCooldownTimer <= 0; }
get rageReady(){ return !this.rageActive && this.rageCooldownTimer <= 0; }
startReload(){
if(this.reloading || this.mag===this.maxMag) return;
this.reloading=true;
this.reloadTimer=CONFIG.PLAYER.reloadTime*this.reloadMult;
sound.reloadStart();
}
startDash(game){
if(!this.alive || !this.dashReady) return false;
let mx=0, my=0;
const k=game.keys;
if(k['w']||k['ц']||k['arrowup']) my-=1;
if(k['s']||k['ы']||k['arrowdown']) my+=1;
if(k['a']||k['ф']||k['arrowleft']) mx-=1;
if(k['d']||k['в']||k['arrowright']) mx+=1;
if(game.moveStick.active){ mx=game.moveStick.dx; my=game.moveStick.dy; }
if(Math.hypot(mx,my) > 0.1) this.dashAngle = Math.atan2(my, mx);
else this.dashAngle = this.angle;
this.dashTimer = CONFIG.PLAYER.dash.duration;
this.dashCooldownTimer = CONFIG.PLAYER.dash.cooldown + CONFIG.PLAYER.dash.duration;
this.invulnerable = true;
game.rings.push(new Ring(this.x, this.y, '#7df9ff', 60, 0.35));
for(let i=0;i<10;i++){
const a = this.dashAngle + Math.PI + rand(-0.6, 0.6);
const s = rand(120, 320);
game.particles.push(new Particle(this.x, this.y, Math.cos(a)*s, Math.sin(a)*s, rand(0.2, 0.4), '#7df9ff', rand(2, 4)));
}
game.shake(3);
sound.dash();
return true;
}
startRage(game){
if(!this.alive || !this.rageReady) return false;
this.rageActive = true;
this.rageTimer = CONFIG.PLAYER.rage.duration;
game.rings.push(new Ring(this.x, this.y, '#ff3b6b', 90, 0.6));
for(let i=0;i<24;i++){
const a = rand(0, TAU);
const s = rand(100, 400);
game.particles.push(new Particle(this.x, this.y, Math.cos(a)*s, Math.sin(a)*s, rand(0.4, 0.8), '#ff5577', rand(2, 5)));
}
game.shake(8);
sound.rage();
return true;
}
update(dt,game){
if(!this.alive) return;
const C=CONFIG.PLAYER;
this.hitFlash=Math.max(0,this.hitFlash-dt*4);
if(this.dashTimer > 0){
this.dashTimer -= dt;
if(this.dashTimer <= 0){ this.dashTimer = 0; this.invulnerable = false; }
}
if(this.dashCooldownTimer > 0) this.dashCooldownTimer -= dt;
if(this.rageActive){
this.rageTimer -= dt;
if(this.rageTimer <= 0){
this.rageActive = false; this.rageTimer = 0;
this.rageCooldownTimer = CONFIG.PLAYER.rage.cooldown;
sound.rageEnd();
}
}
if(this.rageCooldownTimer > 0 && !this.rageActive) this.rageCooldownTimer -= dt;
const k=game.keys;
let mx=0, my=0;
if(k['w']||k['ц']||k['arrowup']) my-=1;
if(k['s']||k['ы']||k['arrowdown']) my+=1;
if(k['a']||k['ф']||k['arrowleft']) mx-=1;
if(k['d']||k['в']||k['arrowright']) mx+=1;
const lk=Math.hypot(mx,my);
if(lk>0){ mx/=lk; my/=lk; }
let sprinting=!!k['shift'];
if(game.moveStick.active){
mx=game.moveStick.dx; my=game.moveStick.dy;
if(game.moveStick.getMag()>0.88) sprinting=true;
}
if(this.dashTimer > 0){
this.vx = Math.cos(this.dashAngle) * C.dash.speed;
this.vy = Math.sin(this.dashAngle) * C.dash.speed;
if(Math.random() < 0.9){
game.particles.push(new Particle(this.x, this.y, rand(-30,30), rand(-30,30), rand(0.18, 0.32), '#7df9ff', rand(2, 3.5)));
}
} else {
const rageSpeed = this.rageActive ? C.rage.speedMult : 1;
const cheatSpd = game.cheatSpeedMult || 1;
const spd = C.speed * this.speedMult * rageSpeed * cheatSpd * (sprinting ? C.sprintMult : 1);
this.vx=lerp(this.vx,mx*spd,0.28);
this.vy=lerp(this.vy,my*spd,0.28);
}
game.moveEntity(this,this.vx*dt,this.vy*dt);
if(game.aimMode==='touch'){
if(game.aimStick.active && game.aimStick.getMag()>0.2){
this.angle=Math.atan2(game.aimStick.dy,game.aimStick.dx);
}
} else {
const wm=game.worldMouse();
this.angle=Math.atan2(wm.y-this.y, wm.x-this.x);
}
if(this.reloading){
this.reloadTimer-=dt;
if(this.reloadTimer<=0){ this.reloading=false; this.mag=this.maxMag; sound.reloadDone(); }
}
this.spread=Math.max(0,this.spread - C.spreadRecover*dt);
this.fireTimer-=dt;
let wantShoot=game.mouse.down;
if(game.aimStick.active && game.aimStick.getMag()>0.35) wantShoot=true;
if(wantShoot && !this.reloading && this.fireTimer<=0){
if(this.mag>0 || game.infiniteAmmo) this.shoot(game);
else this.startReload();
}
}
shoot(game){
const C=CONFIG.PLAYER;
const rageDmg = this.rageActive ? C.rage.damageMult : 1;
const rageRate = this.rageActive ? C.rage.fireRateMult : 1;
const cheatDmg = game.cheatDamageMult || 1;
const ang=this.angle + rand(-1,1)*(C.spread+this.spread);
const mx=this.x+Math.cos(this.angle)*(this.r+8);
const my=this.y+Math.sin(this.angle)*(this.r+8);
game.bullets.push(new Bullet(mx,my,ang,C.bulletSpeed, C.damage*this.damageMult*rageDmg*cheatDmg,this,'#7df9ff',3.2));
if(game.infiniteAmmo) this.mag = this.maxMag;
else this.mag--;
this.fireTimer=C.fireRate*this.fireRateMult*rageRate;
this.spread=Math.min(this.spread+C.spreadPerShot, C.maxSpread);
game.spawnMuzzle(mx,my,ang,'#7df9ff');
game.shake(1.8);
sound.shoot();
if(this.mag<=0 && !game.infiniteAmmo) this.startReload();
}
draw(ctx){
if(!this.alive) return;
const r=this.r;
drawShadow(ctx,this.x,this.y,r);
if(this.rageActive){
const t = performance.now() / 100;
const pulse = 0.5 + Math.sin(t) * 0.15;
ctx.save();
ctx.globalAlpha = 0.35 + Math.sin(t*0.7) * 0.1;
const grd = ctx.createRadialGradient(this.x, this.y, r, this.x, this.y, r + 22);
grd.addColorStop(0, 'rgba(255, 60, 100, ' + pulse + ')');
grd.addColorStop(1, 'rgba(255, 60, 100, 0)');
ctx.fillStyle = grd;
ctx.beginPath(); ctx.arc(this.x, this.y, r + 22, 0, TAU); ctx.fill();
ctx.restore();
ctx.save();
ctx.globalAlpha = 0.55;
ctx.strokeStyle = '#ff5577'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(this.x, this.y, r + 8 + Math.sin(t*1.5)*3, 0, TAU); ctx.stroke();
ctx.restore();
}
if(this.invulnerable){
ctx.save();
ctx.globalAlpha = 0.5;
ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 3;
ctx.beginPath(); ctx.arc(this.x, this.y, r + 6, 0, TAU); ctx.stroke();
ctx.restore();
}
if(this.hitFlash>0){
ctx.globalAlpha=this.hitFlash*0.6;
ctx.fillStyle='#ff2244';
ctx.beginPath(); ctx.arc(this.x,this.y,r+8,0,TAU); ctx.fill();
ctx.globalAlpha=1;
}
ctx.save();
ctx.translate(this.x,this.y);
ctx.rotate(this.angle);
const bodyColor = this.rageActive ? '#ff5577' : '#22e6ff';
ctx.strokeStyle = bodyColor; ctx.lineWidth=5; ctx.lineCap='round';
ctx.shadowColor = bodyColor; ctx.shadowBlur = this.rageActive ? 22 : 14;
ctx.beginPath(); ctx.moveTo(r*0.4,0); ctx.lineTo(r+11,0); ctx.stroke();
ctx.fillStyle='#0b2230';
ctx.strokeStyle=bodyColor; ctx.lineWidth=3;
ctx.beginPath(); ctx.arc(0,0,r,0,TAU); ctx.fill(); ctx.stroke();
ctx.fillStyle= this.hitFlash>0 ? '#ff5566' : (this.rageActive ? '#ffcc66' : '#a8f7ff');
ctx.beginPath(); ctx.arc(0,0,r*0.4,0,TAU); ctx.fill();
ctx.restore();
ctx.shadowBlur=0;
drawHealthBar(ctx,this.x,this.y-r-16,54,this.hp,this.maxHp,'#22e6ff');
}
}
/* =========================================================================
ОБЩИЕ ФУНКЦИИ
========================================================================= */
function drawShadow(ctx,x,y,r){
ctx.fillStyle='rgba(0,0,0,0.45)';
ctx.beginPath();
ctx.ellipse(x,y+r*0.75,r*0.95,r*0.42,0,0,TAU);
ctx.fill();
}
function drawHealthBar(ctx,x,y,w,hp,maxHp,color){
const pct=clamp(hp/maxHp,0,1);
const h=5;
const bx=x-w/2, by=y;
ctx.fillStyle='rgba(0,0,0,0.65)';
ctx.fillRect(bx-1,by-1,w+2,h+2);
ctx.fillStyle='#1b1b2e';
ctx.fillRect(bx,by,w,h);
const r=Math.floor(lerp(255,60,pct));
const g=Math.floor(lerp(60,255,pct));
ctx.fillStyle= pct>0.3 ? `rgb(${r},${g},90)` : '#ff3355';
ctx.fillRect(bx,by,w*pct,h);
ctx.strokeStyle='rgba(255,255,255,0.25)';
ctx.lineWidth=1;
ctx.strokeRect(bx+0.5,by+0.5,w-1,h-1);
if(color){
ctx.strokeStyle=color; ctx.globalAlpha=0.5;
ctx.strokeRect(bx-1.5,by-1.5,w+3,h+3);
ctx.globalAlpha=1;
}
}
/* =========================================================================
BOT
========================================================================= */
class Bot {
constructor(x,y,type,game){
const cfg=CONFIG.BOT_TYPES[type];
this.cfg=cfg; this.type=type;
this.x=x; this.y=y; this.vx=0; this.vy=0;
this.r=cfg.radius;
const roomHpScale = 1 + (game.room - 1) * CONFIG.ROOM_HP_SCALE;
const roomDmgScale = 1 + (game.room - 1) * CONFIG.ROOM_DMG_SCALE;
const roomSpdScale = 1 + (game.room - 1) * CONFIG.ROOM_SPEED_SCALE;
this.maxHp = Math.round(cfg.hp * roomHpScale * game.difficulty.botHpMult);
this.hp = this.maxHp;
this.damage = (cfg.damage||0) * roomDmgScale * game.difficulty.botDmgMult;
this.speed = cfg.speed * roomSpdScale;
this.angle=rand(0,TAU);
this.mag=cfg.magSize;
this.reloading=false; this.reloadTimer=0;
this.fireTimer=rand(0,0.8);
this.hitFlash=0; this.alive=true; this.dead=false;
this.state='patrol';
this.target=null;
this.patrolPoint=null; this.patrolTimer=0;
this.strafeDir = Math.random()<0.5 ? -1 : 1;
this.strafeTimer=rand(0.4,1.4);
this.reactionTimer=0;
this.color=cfg.color;
this.isSapper = (type === 'sapper');
this.arming = false; this.armTimer = 0;
this.alarmTimer = 0;
this.bobPhase = Math.random() * TAU;
}
startReload(){
if(this.reloading) return;
this.reloading=true;
this.reloadTimer=this.cfg.reloadTime;
}
hasLOS(target,game){
for(const w of game.walls){
if(segIntersectsRect(this.x,this.y,target.x,target.y,w)) return false;
}
return true;
}
findTarget(game){
let best=null, bestScore=Infinity;
const cands=[];
if(game.player && game.player.alive) cands.push(game.player);
for(const b of game.bots) if(b!==this && b.alive && !b.isSapper) cands.push(b);
for(const c of cands){
const d=dist2(this.x,this.y,c.x,c.y);
if(d>this.cfg.vision) continue;
if(c!==game.player && d>420) continue;
if(!this.hasLOS(c,game)) continue;
const score = (c===game.player) ? d*0.55 : d;
if(score<bestScore){ bestScore=score; best=c; }
}
return best;
}
dodgeVector(game){
let dx=0, dy=0;
for(const b of game.bullets){
if(b.owner===this) continue;
const rx=this.x-b.x, ry=this.y-b.y;
const d=Math.hypot(rx,ry);
if(d>190 || d<1e-3) continue;
const bl=Math.hypot(b.vx,b.vy);
if(bl<1) continue;
const ux=b.vx/bl, uy=b.vy/bl;
const dot=(rx/d)*ux+(ry/d)*uy;
if(dot<0.86) continue;
const strength=1-d/190;
const proj=rx*ux+ry*uy;
const perpX=rx-proj*ux, perpY=ry-proj*uy;
const pl=Math.hypot(perpX,perpY);
if(pl<0.5){
dx += -uy*this.strafeDir*strength;
dy += ux*this.strafeDir*strength;
} else {
dx += (perpX/pl)*strength;
dy += (perpY/pl)*strength;
}
}
return {x:dx, y:dy};
}
shoot(target,game){
const cfg=this.cfg;
const d=dist2(this.x,this.y,target.x,target.y);
const t=d/cfg.bulletSpeed;
const px=target.x+(target.vx||0)*t;
const py=target.y+(target.vy||0)*t;
let ang=Math.atan2(py-this.y, px-this.x);
ang+=rand(-1,1)*cfg.spread;
const mx=this.x+Math.cos(ang)*(this.r+7);
const my=this.y+Math.sin(ang)*(this.r+7);
game.bullets.push(new Bullet(mx,my,ang,cfg.bulletSpeed,this.damage,this,cfg.color,3));
this.mag--;
this.fireTimer=cfg.fireRate*rand(0.9,1.25);
game.spawnMuzzle(mx,my,ang,cfg.color);
const sv = sound.spatialVolume(this.x, this.y, game.cam.x, game.cam.y, 1000);
if(sv > 0.05){
if(this.type === 'sniper') sound.sniperShoot(sv);
else if(this.type === 'heavy') sound.heavyShoot(sv);
else sound.botShoot(sv);
}
if(this.mag<=0) this.startReload();
}
updateSapper(dt, game){
this.hitFlash = Math.max(0, this.hitFlash - dt*4);
this.bobPhase += dt * 12;
const p = game.player;
if(p && p.alive){
const d = dist2(this.x, this.y, p.x, p.y);
const ang = Math.atan2(p.y - this.y, p.x - this.x);
this.angle = turnToward(this.angle, ang, dt * 9);
const dodge = this.dodgeVector(game);
let tx = Math.cos(this.angle) + dodge.x * 0.9;
let ty = Math.sin(this.angle) + dodge.y * 0.9;
const l = Math.hypot(tx, ty);
if(l > 1e-3){ tx /= l; ty /= l; }
this.vx = lerp(this.vx, tx * this.speed, 0.22);
this.vy = lerp(this.vy, ty * this.speed, 0.22);
if(!this.arming && d < 110){
this.arming = true;
this.armTimer = 1.6;
}
if(d < this.r + p.r + 4){ this.explode(game); return; }
} else { this.vx *= 0.9; this.vy *= 0.9; }
if(this.arming){
this.armTimer -= dt;
const fast = this.armTimer < 0.6;
const sv = sound.spatialVolume(this.x, this.y, game.cam.x, game.cam.y, 700);
if(sv > 0.03) sound.sapperAlarm(sv, fast);
if(Math.random() < (fast ? 0.5 : 0.15)){
game.particles.push(new Particle(
this.x + rand(-8,8), this.y - this.r - 4 + rand(-4,4),
rand(-40,40), rand(-80,-20),
rand(0.2,0.4), '#ff2244', rand(1.5,2.5)
));
}
if(this.armTimer <= 0){ this.explode(game); return; }
}
game.moveEntity(this, this.vx * dt, this.vy * dt);
}
explode(game){
if(!this.alive) return;
const cfg = this.cfg;
const R = cfg.explodeRadius || 130;
const roomDmgScale = 1 + (game.room - 1) * CONFIG.ROOM_DMG_SCALE;
const dmgPlayer = (cfg.explodeDmgPlayer || 40) * roomDmgScale * game.difficulty.botDmgMult;
const dmgBots = (cfg.explodeDmgBots || 60) * roomDmgScale;
if(game.player && game.player.alive){
const d = dist2(this.x, this.y, game.player.x, game.player.y);
if(d < R + game.player.r){
const k = clamp(1 - d / (R + game.player.r), 0.4, 1);
game.damageEntity(game.player, dmgPlayer * k, this);
}
}
for(const b of game.bots){
if(b === this || !b.alive) continue;
const d = dist2(this.x, this.y, b.x, b.y);
if(d < R + b.r){
const k = clamp(1 - d / (R + b.r), 0.3, 1);
game.damageEntity(b, dmgBots * k, this);
}
}
if(game.boss && game.boss.alive){
const parts = [{x: game.boss.x, y: game.boss.y, r: game.boss.r}].concat(game.boss.segments);
for(const part of parts){
const d = dist2(this.x, this.y, part.x, part.y);
if(d < R + part.r){
game.damageEntity(game.boss, 45, game.player);
break;
}
}
}
game.rings.push(new Ring(this.x, this.y, '#ffcc00', R * 1.05, 0.5));
game.rings.push(new Ring(this.x, this.y, '#ff2244', R * 0.75, 0.35));
for(let i = 0; i < 36; i++){
const a = rand(0, TAU), s = rand(120, 620);
game.particles.push(new Particle(this.x, this.y,
Math.cos(a)*s, Math.sin(a)*s,
rand(0.3, 0.9), i % 2 ? '#ffcc00' : '#ff5577', rand(2, 4.5)));
}
game.shake(14);
const sv = sound.spatialVolume(this.x, this.y, game.cam.x, game.cam.y, 1200);
if(sv > 0.05){
sound.explosion(sv);
if(sv > 0.4) sound.sapperExplode(sv);
}
game.kills++;
game.score += cfg.score;
this.alive = false;
this.dead = true;
}
update(dt,game){
if(!this.alive) return;
if(this.isSapper){ this.updateSapper(dt, game); return; }
const cfg=this.cfg;
this.hitFlash=Math.max(0,this.hitFlash-dt*4);
this.fireTimer-=dt;
this.reactionTimer-=dt;
this.strafeTimer-=dt;
if(this.strafeTimer<=0){
this.strafeTimer=rand(0.55,1.6);
this.strafeDir*=-1;
}
if(this.reloading){
this.reloadTimer-=dt;
if(this.reloadTimer<=0){ this.reloading=false; this.mag=cfg.magSize; }
}
const target=this.findTarget(game);
this.target=target;
let moveX=0, moveY=0;
if(target){
const d=dist2(this.x,this.y,target.x,target.y);
const ang=Math.atan2(target.y-this.y, target.x-this.x);
this.angle=turnToward(this.angle,ang,dt*7.5);
const lowHp=this.hp<this.maxHp*0.35;
if(lowHp && d<cfg.vision*0.85){
this.state='retreat';
moveX=-Math.cos(ang);
moveY=-Math.sin(ang);
moveX+=Math.cos(ang+Math.PI/2)*this.strafeDir*0.8;
moveY+=Math.sin(ang+Math.PI/2)*this.strafeDir*0.8;
} else {
this.state='attack';
const pref=cfg.preferred;
let dir=0;
if(d>pref*1.18) dir=1;
else if(d<pref*0.72) dir=-1;
moveX=Math.cos(ang)*dir;
moveY=Math.sin(ang)*dir;
moveX+=Math.cos(ang+Math.PI/2)*this.strafeDir*0.95;
moveY+=Math.sin(ang+Math.PI/2)*this.strafeDir*0.95;
}
if(!this.reloading && this.fireTimer<=0 && this.hasLOS(target,game)){
if(this.reactionTimer<=0){
this.shoot(target,game);
this.reactionTimer=rand(0.05,0.2);
}
}
if(this.mag<=0 && !this.reloading) this.startReload();
} else {
this.state='patrol';
this.patrolTimer-=dt;
if(!this.patrolPoint || this.patrolTimer<=0 ||
dist2(this.x,this.y,this.patrolPoint.x,this.patrolPoint.y)<45){
this.patrolPoint=game.randomFreePoint();
this.patrolTimer=rand(4,8);
}
const ang=Math.atan2(this.patrolPoint.y-this.y, this.patrolPoint.x-this.x);
this.angle=turnToward(this.angle,ang,dt*5);
moveX=Math.cos(ang);
moveY=Math.sin(ang);
if(this.mag<cfg.magSize && !this.reloading) this.startReload();
}
const dodge=this.dodgeVector(game);
moveX+=dodge.x*1.7;
moveY+=dodge.y*1.7;
for(const o of game.bots){
if(o===this || !o.alive || o.isSapper) continue;
const dx=this.x-o.x, dy=this.y-o.y;
const d=Math.hypot(dx,dy);
const minD=this.r+o.r+8;
if(d<minD && d>1e-3){
const f=(1-d/minD)*1.6;
moveX+=(dx/d)*f;
moveY+=(dy/d)*f;
}
}
const len=Math.hypot(moveX,moveY);
if(len>1e-3){ moveX/=len; moveY/=len; }
const spd=this.speed*(this.state==='retreat'?1.18:1);
this.vx=lerp(this.vx, moveX*spd, 0.18);
this.vy=lerp(this.vy, moveY*spd, 0.18);
game.moveEntity(this,this.vx*dt,this.vy*dt);
}
draw(ctx){
if(!this.alive) return;
if(this.isSapper){ this.drawSapper(ctx); return; }
const r=this.r;
const col=this.cfg.color;
drawShadow(ctx,this.x,this.y,r);
if(this.hitFlash>0){
ctx.globalAlpha=this.hitFlash*0.65;
ctx.fillStyle='#ffffff';
ctx.beginPath(); ctx.arc(this.x,this.y,r+7,0,TAU); ctx.fill();
ctx.globalAlpha=1;
}
if(this.state==='retreat'){
ctx.globalAlpha=0.25+Math.sin(performance.now()/120)*0.12;
ctx.strokeStyle='#ffcc00'; ctx.lineWidth=2;
ctx.beginPath(); ctx.arc(this.x,this.y,r+9,0,TAU); ctx.stroke();
ctx.globalAlpha=1;
}
ctx.save();
ctx.translate(this.x,this.y);
ctx.rotate(this.angle);
ctx.strokeStyle=col; ctx.lineWidth=this.type==='heavy'?6:4; ctx.lineCap='round';
ctx.shadowColor=col; ctx.shadowBlur=12;
ctx.beginPath();
ctx.moveTo(r*0.4,0);
ctx.lineTo(r+(this.type==='sniper'?16:10),0);
ctx.stroke();
ctx.fillStyle='#150c1a';
ctx.strokeStyle=col; ctx.lineWidth=3;
if(this.type==='heavy'){
ctx.beginPath(); ctx.rect(-r*0.8,-r*0.8,r*1.6,r*1.6); ctx.fill(); ctx.stroke();
} else if(this.type==='sniper'){
ctx.beginPath();
ctx.moveTo(r,0);
ctx.lineTo(-r*0.75,-r*0.85);
ctx.lineTo(-r*0.75, r*0.85);
ctx.closePath();
ctx.fill(); ctx.stroke();
} else {
ctx.beginPath(); ctx.arc(0,0,r,0,TAU); ctx.fill(); ctx.stroke();
}
ctx.fillStyle=col;
ctx.beginPath(); ctx.arc(0,0,r*0.33,0,TAU); ctx.fill();
ctx.restore();
ctx.shadowBlur=0;
drawHealthBar(ctx,this.x,this.y-r-15,46,this.hp,this.maxHp,col);
}
drawSapper(ctx){
const r = this.r;
const col = '#ffcc00';
ctx.fillStyle = 'rgba(0,0,0,0.45)';
ctx.beginPath();
ctx.ellipse(this.x, this.y + r * 0.75, r * 0.95, r * 0.42, 0, 0, TAU);
ctx.fill();
if(this.arming){
const blinkRate = this.armTimer < 0.6 ? 40 : 110;
const pulse = 0.5 + Math.sin(performance.now() / blinkRate) * 0.5;
ctx.save();
ctx.globalAlpha = 0.35 + pulse * 0.35;
const grd = ctx.createRadialGradient(this.x, this.y, r*0.5, this.x, this.y, r + 26);
grd.addColorStop(0, 'rgba(255, 40, 80, 0.9)');
grd.addColorStop(1, 'rgba(255, 40, 80, 0)');
ctx.fillStyle = grd;
ctx.beginPath(); ctx.arc(this.x, this.y, r + 26, 0, TAU); ctx.fill();
ctx.restore();
}
if(this.hitFlash > 0){
ctx.globalAlpha = this.hitFlash * 0.65;
ctx.fillStyle = '#ffffff';
ctx.beginPath(); ctx.arc(this.x, this.y, r + 6, 0, TAU); ctx.fill();
ctx.globalAlpha = 1;
}
const bodyStroke = this.arming ? '#ff2244' : col;
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(performance.now() * 0.0035 + this.bobPhase * 0.1);
ctx.fillStyle = '#2a1a05';
ctx.strokeStyle = bodyStroke;
ctx.lineWidth = 3;
ctx.shadowColor = bodyStroke;
ctx.shadowBlur = this.arming ? 22 : 12;
ctx.beginPath();
const sides = 6;
for(let i = 0; i < sides; i++){
const a = (i / sides) * TAU;
const px = Math.cos(a) * r;
const py = Math.sin(a) * r;
if(i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fill(); ctx.stroke();
ctx.shadowBlur = 0;
ctx.strokeStyle = bodyStroke;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(-r*0.45, 0); ctx.lineTo(r*0.45, 0);
ctx.moveTo(0, -r*0.45); ctx.lineTo(0, r*0.45);
ctx.stroke();
const blink = this.arming
? (Math.sin(performance.now() / (this.armTimer < 0.6 ? 40 : 90)) > 0 ? 1 : 0.15)
: 1;
ctx.globalAlpha = blink;
ctx.fillStyle = this.arming ? '#ff2244' : '#ffee66';
ctx.shadowColor = this.arming ? '#ff2244' : '#ffcc00';
ctx.shadowBlur = 16;
ctx.beginPath(); ctx.arc(0, 0, r * 0.28, 0, TAU); ctx.fill();
ctx.shadowBlur = 0;
ctx.globalAlpha = 1;
ctx.restore();
if(this.arming){
const t = 1 - this.armTimer / 1.6;
const yOff = -r - 22 - Math.sin(performance.now() / 80) * 3;
ctx.save();
ctx.globalAlpha = 0.85 + Math.sin(performance.now() / 60) * 0.15;
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.font = 'bold ' + (18 + t * 10) + 'px Consolas, monospace';
ctx.fillStyle = '#ff2244';
ctx.shadowColor = '#ff2244'; ctx.shadowBlur = 18;
ctx.fillText('!', this.x, yOff);
ctx.restore();
}
drawHealthBar(ctx, this.x, this.y - r - 15, 46, this.hp, this.maxHp, col);
}
}
/* =========================================================================
BOSS
========================================================================= */
class BossSnake {
constructor(x, y, game){
this.x = x; this.y = y;
this.vx = 0; this.vy = 0;
this.r = 30;
this.angle = rand(0, TAU);
this.isBoss = true; this.isPlayer = false;
this.alive = true; this.dead = false;
this.hitFlash = 0;
const roomIdx = Math.max(1, Math.floor(game.room / CONFIG.BOSS_EVERY));
const baseHp = 2400;
const roomScale = 1 + (roomIdx - 1) * 0.6;
this.maxHp = Math.round(baseHp * roomScale * game.difficulty.botHpMult);
this.hp = this.maxHp;
this.speed = 160;
this.turnSpeed = 3.0;
this.segmentCount = 7;
this.segmentSpacing = 30;
this.segments = [];
for(let i = 0; i < this.segmentCount; i++){
this.segments.push({
x: x - (i + 1) * this.segmentSpacing,
y: y, r: this.r * 0.82, angle: 0
});
}
this.fireTimer = 2.0;
this.contactDamage = 22 * game.difficulty.botDmgMult;
this.bulletDamage = 12 * game.difficulty.botDmgMult;
this.contactCooldown = 0;
this.hissTimer = 0;
}
update(dt, game){
if(!this.alive) return;
this.hitFlash = Math.max(0, this.hitFlash - dt * 3);
let speedMult = 1;
if(game.player && game.player.alive){
const d = dist2(this.x, this.y, game.player.x, game.player.y);
if(d < 320) speedMult = 1.45;
else if(d > 750) speedMult = 1.30;
const targetAng = Math.atan2(game.player.y - this.y, game.player.x - this.x);
this.angle = turnToward(this.angle, targetAng, dt * this.turnSpeed);
}
const spd = this.speed * speedMult;
this.vx = Math.cos(this.angle) * spd;
this.vy = Math.sin(this.angle) * spd;
game.moveEntity(this, this.vx * dt, this.vy * dt);
let prevX = this.x, prevY = this.y;
for(let i = 0; i < this.segments.length; i++){
const seg = this.segments[i];
const dx = prevX - seg.x;
const dy = prevY - seg.y;
const d = Math.hypot(dx, dy);
if(d > this.segmentSpacing){
const nx = dx / d, ny = dy / d;
seg.x = prevX - nx * this.segmentSpacing;
seg.y = prevY - ny * this.segmentSpacing;
}
seg.angle = Math.atan2(prevY - seg.y, prevX - seg.x);
prevX = seg.x; prevY = seg.y;
}
this.contactCooldown -= dt;
if(this.contactCooldown <= 0 && game.player && game.player.alive){
const parts = [{x: this.x, y: this.y, r: this.r}].concat(this.segments);
for(const part of parts){
const d = dist2(part.x, part.y, game.player.x, game.player.y);
if(d < part.r + game.player.r){
game.damageEntity(game.player, this.contactDamage, this);
this.contactCooldown = 0.55;
break;
}
}
}
this.fireTimer -= dt;
if(this.fireTimer <= 0 && game.player && game.player.alive){
this.fireBurst(game);
this.fireTimer = rand(1.6, 2.4);
}
this.hissTimer -= dt;
if(this.hissTimer <= 0){
const sv = sound.spatialVolume(this.x, this.y, game.cam.x, game.cam.y, 1200);
if(sv > 0.05) sound.bossHiss();
this.hissTimer = rand(3.5, 6.5);
}
}
fireBurst(game){
if(!game.player || !game.player.alive) return;
const baseAng = Math.atan2(game.player.y - this.y, game.player.x - this.x);
const count = 5;
for(let i = 0; i < count; i++){
const offset = (i - (count - 1) / 2) * 0.16;
const ang = baseAng + offset + rand(-0.05, 0.05);
const mx = this.x + Math.cos(ang) * (this.r + 6);
const my = this.y + Math.sin(ang) * (this.r + 6);
const b = new Bullet(mx, my, ang, 540, this.bulletDamage, this, '#ff5577', 4);
b.isBossBullet = true;
game.bullets.push(b);
}
game.spawnMuzzle(this.x + Math.cos(baseAng)*(this.r+4), this.y + Math.sin(baseAng)*(this.r+4), baseAng, '#ff5577');
sound.heavyShoot(1);
game.shake(4);
}
draw(ctx){
for(let i = this.segments.length - 1; i >= 0; i--){
this.drawCube(ctx, this.segments[i], i, false);
}
this.drawCube(ctx, {x: this.x, y: this.y, r: this.r, angle: this.angle}, -1, true);
}
drawCube(ctx, part, idx, isHead){
const r = part.r;
const flash = this.hitFlash > 0;
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.beginPath();
ctx.ellipse(part.x, part.y + r * 0.75, r * 0.95, r * 0.4, 0, 0, TAU);
ctx.fill();
ctx.save();
ctx.translate(part.x, part.y);
ctx.rotate((part.angle || 0) + performance.now() * 0.0015 * (isHead ? 2 : 1));
const size = r * 1.7;
ctx.fillStyle = flash ? '#ffffff' : (isHead ? '#1a0a2a' : '#160822');
ctx.fillRect(-size/2, -size/2, size, size);
const borderCol = flash ? '#ffffff' : (isHead ? '#ff3b6b' : '#b06bff');
ctx.strokeStyle = borderCol;
ctx.lineWidth = 3;
ctx.shadowColor = borderCol;
ctx.shadowBlur = isHead ? 20 : 12;
ctx.strokeRect(-size/2, -size/2, size, size);
ctx.shadowBlur = 0;
ctx.strokeStyle = flash ? '#ffffff' : 'rgba(255,150,200,0.55)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(-size/2 + 4, -size/2 + 4);
ctx.lineTo(size/2 - 4, size/2 - 4);
ctx.moveTo(size/2 - 4, -size/2 + 4);
ctx.lineTo(-size/2 + 4, size/2 - 4);
ctx.stroke();
if(isHead){
ctx.fillStyle = '#ff5577';
ctx.shadowColor = '#ff3b6b';
ctx.shadowBlur = 22;
ctx.beginPath();
ctx.arc(0, 0, r * 0.4, 0, TAU);
ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(r * 0.55, -r * 0.32, 2.8, 0, TAU);
ctx.arc(r * 0.55, r * 0.32, 2.8, 0, TAU);
ctx.fill();
} else {
ctx.fillStyle = '#b06bff';
ctx.shadowColor = '#b06bff';
ctx.shadowBlur = 12;
ctx.beginPath();
ctx.arc(0, 0, r * 0.24, 0, TAU);
ctx.fill();
ctx.shadowBlur = 0;
}
ctx.restore();
}
}
/* =========================================================================
DRONE
========================================================================= */
class Drone {
constructor(owner,index){
this.owner=owner;
const ring = Math.floor(index / 8);
const posInRing = index % 8;
this.orbitR = 58 + ring * 20;
this.angle = posInRing * (TAU / 8) + ring * 0.4;
this.x = owner.x + Math.cos(this.angle) * this.orbitR;
this.y = owner.y + Math.sin(this.angle) * this.orbitR;
this.r = 8;
this.fireTimer = rand(0, 0.4);
this.target = null;
this.aimAngle = 0;
this.pulse = Math.random() * TAU;
this.orbitSpeed = 1.0 + ring * 0.15;
}
update(dt,game){
this.angle += dt * this.orbitSpeed;
this.pulse += dt * 4;
const tx = this.owner.x + Math.cos(this.angle) * this.orbitR;
const ty = this.owner.y + Math.sin(this.angle) * this.orbitR;
const f = Math.min(1, dt * 14);
this.x = lerp(this.x, tx, f);
this.y = lerp(this.y, ty, f);
let best=null, bestD=520;
for(const b of game.bots){
if(!b.alive) continue;
const d=dist2(this.x,this.y,b.x,b.y);
if(d<bestD){ bestD=d; best=b; }
}
if(game.boss && game.boss.alive){
const d = dist2(this.x, this.y, game.boss.x, game.boss.y);
if(d < bestD){ bestD = d; best = game.boss; }
}
this.target=best;
if(best){
const targetAng=Math.atan2(best.y-this.y, best.x-this.x);
this.aimAngle=turnToward(this.aimAngle,targetAng,dt*9);
this.fireTimer-=dt;
if(this.fireTimer<=0){
this.shoot(game);
this.fireTimer=0.36*game.droneFireRateMult;
}
}
}
shoot(game){
const dmg = 2.33 * game.droneDamageMult;
const ang=this.aimAngle+rand(-1,1)*0.07;
const mx=this.x+Math.cos(ang)*(this.r+3);
const my=this.y+Math.sin(ang)*(this.r+3);
const b=new Bullet(mx,my,ang,880,dmg,this.owner,'#7df9ff',2.2);
b.fromDrone=true;
game.bullets.push(b);
game.spawnMuzzle(mx,my,ang,'#aaffff');
sound.droneShoot();
}
draw(ctx){
const t=this.pulse;
ctx.save();
ctx.globalAlpha=0.35+Math.sin(t)*0.1;
ctx.fillStyle='rgba(125,249,255,0.35)';
ctx.beginPath(); ctx.arc(this.x,this.y,this.r+4,0,TAU); ctx.fill();
ctx.restore();
ctx.save();
ctx.shadowColor='#7df9ff'; ctx.shadowBlur=10;
ctx.fillStyle='#0b2230';
ctx.beginPath(); ctx.arc(this.x,this.y,this.r,0,TAU); ctx.fill();
ctx.strokeStyle='#7df9ff'; ctx.lineWidth=2; ctx.stroke();
ctx.shadowBlur=0;
ctx.restore();
ctx.fillStyle='#aaffff';
ctx.beginPath(); ctx.arc(this.x,this.y,this.r*0.38,0,TAU); ctx.fill();
if(this.target){
ctx.save();
ctx.globalAlpha=0.55;
ctx.strokeStyle='#7df9ff'; ctx.lineWidth=1.5;
ctx.beginPath();
ctx.moveTo(this.x,this.y);
ctx.lineTo(this.x+Math.cos(this.aimAngle)*14, this.y+Math.sin(this.aimAngle)*14);
ctx.stroke();
ctx.restore();
}
}
}
/* =========================================================================
УЛУЧШЕНИЯ
========================================================================= */
const UPGRADE_DEFS = [
{ id:'heal', name:'Аптечка', desc:'+40 HP', cost:80, costMul:1.10,
apply:g=>{ g.player.hp=Math.min(g.player.maxHp, g.player.hp+40); } },
{ id:'maxhp', name:'Броня', desc:'+25 макс. HP', cost:200, costMul:1.30,
apply:g=>{ g.player.maxHp+=25; g.player.hp+=25; } },
{ id:'damage', name:'Урон', desc:'+15% урона', cost:300, costMul:1.35,
apply:g=>{ g.player.damageMult*=1.15; } },
{ id:'firerate', name:'Скорострельность', desc:'+15% скорости огня', cost:300, costMul:1.35,
apply:g=>{ g.player.fireRateMult*=0.87; } },
{ id:'reload', name:'Быстрая перезарядка', desc:'-15% времени перезарядки', cost:250, costMul:1.30,
apply:g=>{ g.player.reloadMult*=0.85; } },
{ id:'speed', name:'Ускорение', desc:'+12% скорости', cost:200, costMul:1.30,
apply:g=>{ g.player.speedMult*=1.12; } },
{ id:'mag', name:'Большой магазин', desc:'+8 патронов', cost:250, costMul:1.35,
apply:g=>{ g.player.magBonus+=8; } },
{ id:'drone', name:'Дрон', desc:'+1 дрон-помощник', cost:500, costMul:1.55,
apply:g=>{ g.addDrone(); } },
{ id:'dronedmg', name:'Урон дронов', desc:'+25% урона дронов', cost:280, costMul:1.40,
apply:g=>{ g.droneDamageMult*=1.25; } },
{ id:'dronefirerate', name:'Скорость дронов', desc:'+20% скорости огня дронов', cost:280, costMul:1.40,
apply:g=>{ g.droneFireRateMult*=0.83; } }
];
/* =========================================================================
ЧИТ-КОДЫ
========================================================================= */
const CHEAT_CODES = [
{ code: '100xdrones', action: 'drones100', desc: '+100 дронов' },
{ code: '50xdrones', action: 'drones50', desc: '+50 дронов' },
{ code: '10xdrones', action: 'drones10', desc: '+10 дронов' },
{ code: '100upgrades', action: 'upgrades100', desc: 'Все улучшения до 100 ур.' },
{ code: 'maxupgrades', action: 'upgrades100', desc: 'Все улучшения до 100 ур.' },
{ code: 'godmode', action: 'godmode', desc: 'Бессмертие (toggle)' },
{ code: 'infiniteammo', action: 'infiniteammo', desc: 'Бесконечные патроны' },
{ code: 'superdamage', action: 'superdamage', desc: '×10 урон (toggle)' },
{ code: 'superspeed', action: 'superspeed', desc: '×3 скорость (toggle)' },
{ code: 'killall', action: 'killall', desc: 'Убить всех врагов' },
{ code: 'getcoins', action: 'getcoins', desc: '+10000 очков' },
{ code: 'spawnboss', action: 'spawnboss', desc: 'Вызвать босса' },
{ code: 'healme', action: 'healme', desc: 'Полное HP' },
{ code: 'resetcd', action: 'resetcd', desc: 'Сбросить кулдауны' },
{ code: 'nuke', action: 'nuke', desc: 'Взрыв всего экрана' },
{ code: 'longerage', action: 'longerage', desc: 'Ярость 60 сек' }
];
/* =========================================================================
GAME
========================================================================= */
class Game {
constructor(canvas){
this.canvas=canvas;
this.ctx=canvas.getContext('2d');
this.isTouch=('ontouchstart' in window)||(navigator.maxTouchPoints>0);
this.dpr=1;
this.state='menu';
this.keys={};
this.mouse={x:0,y:0,down:false};
this.aimMode='mouse';
this.selectedDifficulty = 'medium';
this.cam={x:0,y:0};
this.shakeAmount=0;
this.shakeX=0; this.shakeY=0;
this.damageFlash=0;
this.walls=[];
this.bullets=[];
this.particles=[];
this.rings=[];
this.bots=[];
this.drones=[];
this.player=null;
this.boss = null;
this.bossDefeatedTimer = 0;
this.room=0;
this.score=0;
this.kills=0;
this.toSpawn=0;
this.spawnTimer=0;
this.roomActive=false;
this.message={text:'',sub:'',t:0,max:1};
this.elapsed=0;
this.respawnsLeft=0;
this.deathTimer=0;
this.difficulty = DIFFICULTIES.medium;
this.upgradeCounts={};
this.availableUpgrades=UPGRADE_DEFS.slice();
this.upgradeCards=[];
this.droneDamageMult=1;
this.droneFireRateMult=1;
this._lowHpWarnTimer = 0;
this._adCooldown = 0;
this.cheatBuffer = '';
this.cheatBufferTime = 0;
this.cheatBufferTimeout = 2.5;
this.cheatMaxLen = 20;
this.cheatHistory = [];
this.godMode = false;
this.infiniteAmmo = false;
this.cheatDamageMult = 1;
this.cheatSpeedMult = 1;
this.pending = { droneCount:0, heal:false, resetCD:false, longRage:false, spawnBoss:false, maxUpgrades:false };
this.showCheatList = false;
this.settingsMessage = '';
this.settingsMessageColor = '#7df9ff';
this.settingsMessageUntil = 0;
this.settingsReturnState = 'menu';
this.highScore = 0;
this.totalKills = 0;
this.totalRooms = 0;
this.moveStick=new VirtualJoystick(58);
this.aimStick=new VirtualJoystick(58);
this.startBtn={x:0,y:0,w:0,h:0};
this.restartBtn={x:0,y:0,w:0,h:0};
this.resumeBtn={x:0,y:0,w:0,h:0};
this.pauseMenuBtn={x:0,y:0,w:0,h:0};
this.pauseSoundBtn={x:0,y:0,w:0,h:0};
this.pauseSettingsBtn={x:0,y:0,w:0,h:0};
this.reloadBtn={x:0,y:0,w:0,h:0};
this.dashBtn={x:0,y:0,w:0,h:0};
this.rageBtn={x:0,y:0,w:0,h:0};
this.pauseBtn={x:0,y:0,w:0,h:0};
this.fullscreenBtn={x:0,y:0,w:0,h:0};
this.soundBtn={x:0,y:0,w:0,h:0};
this.settingsBtn={x:0,y:0,w:0,h:0};
this.upgradeContinueBtn={x:0,y:0,w:0,h:0};
this.difficultyBtns=[];
this.settingsBackBtn={x:0,y:0,w:0,h:0};
this.settingsSoundBtn={x:0,y:0,w:0,h:0};
this.settingsVolumeDownBtn={x:0,y:0,w:0,h:0};
this.settingsVolumeUpBtn={x:0,y:0,w:0,h:0};
this.settingsCheatInputRect={x:0,y:0,w:0,h:0};
this.settingsCheatActivateBtn={x:0,y:0,w:0,h:0};
this.settingsShowCodesBtn={x:0,y:0,w:0,h:0};
this.settingsResetCheatsBtn={x:0,y:0,w:0,h:0};
this.inputEl = document.getElementById('cheatInput');
this.layoutUI();
this.setupInput();
this.loadLocalProgress();
}
loadLocalProgress(){
try {
this.highScore = parseInt(localStorage.getItem('neon_highScore') || '0', 10);
this.totalKills = parseInt(localStorage.getItem('neon_totalKills') || '0', 10);
this.totalRooms = parseInt(localStorage.getItem('neon_totalRooms') || '0', 10);
} catch(e) {}
}
saveLocalProgress(){
try {
localStorage.setItem('neon_highScore', String(this.highScore));
localStorage.setItem('neon_totalKills', String(this.totalKills));
localStorage.setItem('neon_totalRooms', String(this.totalRooms));
} catch(e) {}
}
layoutUI(){
const W=CONFIG.W, H=CONFIG.H;
const cols = W >= 820 ? 4 : 2;
const rows = Math.ceil(4 / cols);
const gapX = 12, gapY = 12;
const bw = Math.min(210, (W - 60 - (cols-1)*gapX) / cols);
const bh = 72;
const totalW = cols*bw + (cols-1)*gapX;
const startX = (W - totalW) / 2;
const startY = H * 0.40;
this.difficultyBtns = DIFFICULTY_ORDER.map((k, i) => ({
key: k,
x: startX + (i % cols) * (bw + gapX),
y: startY + Math.floor(i / cols) * (bh + gapY),
w: bw, h: bh
}));
const startBtnY = startY + rows * (bh + gapY) + 18;
this.startBtn = { x: W/2 - 160, y: startBtnY, w: 260, h: 56 };
this.fullscreenBtn = { x: W/2 + 110, y: startBtnY + 2, w: 52, h: 52 };
this.soundBtn = { x: W/2 + 170, y: startBtnY + 2, w: 52, h: 52 };
const settingsBtnY = startBtnY + 56 + 12;
this.settingsBtn = { x: W/2 - 120, y: settingsBtnY, w: 240, h: 42 };
const pauseBtnW = 260;
const pauseBtnH = 56;
const pauseCx = W/2 - pauseBtnW/2;
const pauseStartY = H*0.38;
this.resumeBtn = { x: pauseCx, y: pauseStartY, w: pauseBtnW, h: pauseBtnH };
const rowY = pauseStartY + pauseBtnH + 14;
const smallGap = 8;
const smallW = 148;
const smallTotal = smallW * 3 + smallGap * 2;
const smallStart = W/2 - smallTotal/2;
this.pauseSoundBtn = { x: smallStart, y: rowY, w: smallW, h: 48 };
this.pauseSettingsBtn = { x: smallStart + smallW + smallGap, y: rowY, w: smallW, h: 48 };
this.pauseMenuBtn = { x: smallStart + (smallW + smallGap) * 2, y: rowY, w: smallW, h: 48 };
this.restartBtn = { x: W/2 - 130, y: H*0.62, w: 260, h: 64 };
const btnSize = 66;
const btnX = W - 24 - btnSize;
this.reloadBtn = { x: btnX, y: H - 105, w: btnSize, h: btnSize };
this.dashBtn = { x: btnX, y: H - 105 - (btnSize + 10), w: btnSize, h: btnSize };
this.rageBtn = { x: btnX, y: H - 105 - (btnSize + 10) * 2, w: btnSize, h: btnSize };
this.pauseBtn = { x: btnX, y: H - 105 - (btnSize + 10) * 3, w: btnSize, h: btnSize };
this.upgradeContinueBtn = { x: W/2 - 140, y: H - 88, w: 280, h: 58 };
const cx = W/2;
this.settingsSoundBtn = { x: cx - 110, y: H*0.20, w: 220, h: 50 };
this.settingsVolumeDownBtn = { x: cx - 220, y: H*0.30, w: 52, h: 52 };
this.settingsVolumeUpBtn = { x: cx + 168, y: H*0.30, w: 52, h: 52 };
this.settingsCheatInputRect = { x: cx - 240, y: H*0.46, w: 480, h: 52 };
this.settingsCheatActivateBtn = { x: cx - 110, y: H*0.46 + 62, w: 220, h: 44 };
this.settingsShowCodesBtn = { x: cx - 190, y: H*0.64, w: 180, h: 40 };
this.settingsResetCheatsBtn = { x: cx + 10, y: H*0.64, w: 180, h: 40 };
this.settingsBackBtn = { x: cx - 130, y: H - 80, w: 260, h: 56 };
if(this.state==='upgrade') this.layoutUpgradeCards();
}
layoutUpgradeCards(){
const W=CONFIG.W, H=CONFIG.H;
const list=this.availableUpgrades;
let cols=5;
if(W<1200) cols=4;
if(W<900) cols=3;
if(W<640) cols=2;
const rows=Math.ceil(list.length/cols);
const gap=14, padding=40;
const cardW=Math.min(240, (W - padding*2 - gap*(cols-1))/cols);
const cardH=Math.min(150, (H*0.52 - gap*(rows-1))/rows);
const totalW=cols*cardW+gap*(cols-1);
const startX=(W-totalW)/2;
const startY=Math.max(H*0.26, (H - cardH*rows - gap*(rows-1))/2 + 20);
this.upgradeCards=list.map((up,i)=>({
upgrade:up,
x:startX + (i%cols)*(cardW+gap),
y:startY + Math.floor(i/cols)*(cardH+gap),
w:cardW, h:cardH
}));
}
getUpgradeCost(up){
const c=this.upgradeCounts[up.id]||0;
return Math.round(up.cost*Math.pow(up.costMul,c));
}
/* ======================== ЧИТ-СИСТЕМА ======================== */
handleCheatInput(key){
if(key.length !== 1) return;
if(key === ' ') return;
const now = performance.now() / 1000;
if(now - this.cheatBufferTime > this.cheatBufferTimeout) this.cheatBuffer = '';
this.cheatBufferTime = now;
this.cheatBuffer += key.toLowerCase();
if(this.cheatBuffer.length > this.cheatMaxLen) this.cheatBuffer = this.cheatBuffer.slice(-this.cheatMaxLen);
for(const c of CHEAT_CODES){
if(this.cheatBuffer.endsWith(c.code)){
this.activateCheat(c);
this.cheatBuffer = '';
return;
}
}
}
tryActivateCheatFromInput(){
const raw = (this.inputEl.value || '').trim().toLowerCase();
if(!raw){ this.showSettingsMessage('Введи код', '#ffcc00'); return; }
const found = CHEAT_CODES.find(c => c.code === raw);
if(found){
this.activateCheat(found);
this.inputEl.value = '';
this.inputEl.blur();
} else {
this.showSettingsMessage('Неизвестный код: "' + raw + '"', '#ff5577');
sound.deny();
}
}
activateCheat(cheat){
sound.cheatOn();
this.cheatHistory.push({ desc: cheat.desc, time: performance.now() });
if(this.cheatHistory.length > 5) this.cheatHistory.shift();
this.executeCheat(cheat.action);
this.showSettingsMessage('✓ ' + cheat.desc, '#7df9ff');
}
showSettingsMessage(text, color){
this.settingsMessage = text;
this.settingsMessageColor = color || '#7df9ff';
this.settingsMessageUntil = performance.now() + 2800;
}
executeCheat(action){
const hasPlayer = !!this.player;
switch(action){
case 'drones100': this.cheatAddDrones(100, hasPlayer); break;
case 'drones50': this.cheatAddDrones(50, hasPlayer); break;
case 'drones10': this.cheatAddDrones(10, hasPlayer); break;
case 'upgrades100': this.cheatMaxUpgrades(hasPlayer); break;
case 'godmode': this.cheatToggleGod(); break;
case 'infiniteammo': this.cheatToggleInfiniteAmmo(); break;
case 'superdamage': this.cheatToggleDamage(); break;
case 'superspeed': this.cheatToggleSpeed(); break;
case 'killall': this.cheatKillAll(); break;
case 'getcoins': this.cheatAddScore(10000); break;
case 'spawnboss': this.cheatSpawnBoss(hasPlayer); break;
case 'healme': this.cheatHeal(hasPlayer); break;
case 'resetcd': this.cheatResetCooldowns(hasPlayer); break;
case 'nuke': this.cheatNuke(); break;
case 'longerage': this.cheatLongRage(hasPlayer); break;
}
}
cheatAddDrones(n, hasPlayer){
if(!hasPlayer){
this.pending.droneCount += n;
this.showSettingsMessage('Дроны (+' + n + ') будут добавлены при старте', '#ffcc00');
return;
}
const canAdd = Math.max(0, 200 - this.drones.length);
const toAdd = Math.min(n, canAdd);
for(let i = 0; i < toAdd; i++){
this.drones.push(new Drone(this.player, this.drones.length));
}
this.rings.push(new Ring(this.player.x, this.player.y, '#7df9ff', 140, 0.8));
for(let i = 0; i < 40; i++){
const a = rand(0, TAU), s = rand(100, 500);
this.particles.push(new Particle(this.player.x, this.player.y,
Math.cos(a)*s, Math.sin(a)*s, rand(0.3, 0.8), '#7df9ff', rand(2, 4)));
}
this.showMessage('ЧИТ АКТИВИРОВАН', '+' + toAdd + ' дронов (всего: ' + this.drones.length + ')', 2.5);
}
/** Прокачивает все доступные улучшения до 100 уровня, КРОМЕ скорости (кап 3 ур.) */
cheatMaxUpgrades(hasPlayer){
if(!hasPlayer){
this.pending.maxUpgrades = true;
this.showSettingsMessage('Все улучшения до 100 ур. применятся при старте (кроме скорости)', '#ffcc00');
return;
}
const targetLevel = 100;
const SPEED_CAP = 3; // скорость капаем на 3 уровнях, иначе игрок летает
let totalLevelsApplied = 0;
for(const up of this.availableUpgrades){
const current = this.upgradeCounts[up.id] || 0;
// Специальное исключение для скорости — кап 3
const target = (up.id === 'speed') ? SPEED_CAP : targetLevel;
const diff = target - current;
if(diff <= 0) continue;
for(let i = 0; i < diff; i++){
try { up.apply(this); } catch(e) {}
}
this.upgradeCounts[up.id] = target;
totalLevelsApplied += diff;
}
// Ограничение HP разумным максимумом
if(this.player.maxHp > 9999){
this.player.maxHp = 9999;
this.player.hp = Math.min(this.player.hp, this.player.maxHp);
}
// Ограничение дронов
if(this.drones.length > 200){
this.drones.length = 200;
}
// Эффекты
if(this.player){
this.rings.push(new Ring(this.player.x, this.player.y, '#ffd166', 180, 1.2));
this.rings.push(new Ring(this.player.x, this.player.y, '#a06bff', 130, 1.0));
for(let i = 0; i < 60; i++){
const a = rand(0, TAU), s = rand(150, 700);
this.particles.push(new Particle(this.player.x, this.player.y,
Math.cos(a)*s, Math.sin(a)*s, rand(0.5, 1.2),
i % 2 ? '#ffd166' : '#a06bff', rand(2, 5)));
}
}
sound.megaUpgrade();
this.shake(15);
this.damageFlash = 0.4;
this.showMessage('⚡ МЕГА-ПРОКАЧКА ⚡',
'Улучшения на ур. 100 · Скорость — кап 3 ур.', 3.5);
}
cheatToggleGod(){
this.godMode = !this.godMode;
if(this.godMode) sound.cheatOn(); else sound.cheatOff();
this.showMessage('ЧИТ: GODMODE', this.godMode ? 'Бессмертие ВКЛ' : 'Бессмертие ВЫКЛ', 2.0);
}
cheatToggleInfiniteAmmo(){
this.infiniteAmmo = !this.infiniteAmmo;
if(this.infiniteAmmo && this.player) this.player.mag = this.player.maxMag;
if(this.infiniteAmmo) sound.cheatOn(); else sound.cheatOff();
this.showMessage('ЧИТ: INFINITE AMMO', this.infiniteAmmo ? 'Патроны бесконечны' : 'Обычный режим', 2.0);
}
cheatToggleDamage(){
if(this.cheatDamageMult === 1){ this.cheatDamageMult = 10; sound.cheatOn();
this.showMessage('ЧИТ: SUPER DAMAGE', 'Урон ×10', 2.0);
} else { this.cheatDamageMult = 1; sound.cheatOff();
this.showMessage('ЧИТ: SUPER DAMAGE', 'Урон обычный', 2.0);
}
}
cheatToggleSpeed(){
if(this.cheatSpeedMult === 1){ this.cheatSpeedMult = 3; sound.cheatOn();
this.showMessage('ЧИТ: SUPER SPEED', 'Скорость ×3', 2.0);
} else { this.cheatSpeedMult = 1; sound.cheatOff();
this.showMessage('ЧИТ: SUPER SPEED', 'Скорость обычная', 2.0);
}
}
cheatKillAll(){
let count = 0;
for(const b of this.bots){
if(b.alive){ b.alive = false; b.dead = true;
this.spawnExplosion(b.x, b.y, b.isSapper ? '#ffcc00' : b.cfg.color);
count++;
}
}
if(this.boss && this.boss.alive){ this.killEntity(this.boss, this.player); count++; }
this.shake(15);
this.showMessage('ЧИТ: KILL ALL', 'Уничтожено: ' + count, 2.0);
}
cheatAddScore(n){ this.score += n; sound.cheatOn(); this.showMessage('ЧИТ: COINS', '+' + n + ' очков', 2.0); }
cheatSpawnBoss(hasPlayer){
if(!hasPlayer){ this.pending.spawnBoss = true;
this.showSettingsMessage('Босс появится при старте игры', '#ffcc00'); return; }
if(this.boss && this.boss.alive){ this.boss.alive = false; this.boss.dead = true;
this.spawnExplosion(this.boss.x, this.boss.y, '#ff3b6b'); }
const p = this.findBossSpawnPoint();
this.boss = new BossSnake(p.x, p.y, this);
this.bossDefeatedTimer = 0;
if(this.toSpawn === 0 && this.bots.length === 0 && !this.isBossRoom()) this.roomActive = true;
this.rings.push(new Ring(p.x, p.y, '#ff3b6b', 180, 1.2));
this.spawnSparks(p.x, p.y, '#ff3b6b', 40);
sound.bossAppear();
this.shake(20);
this.showMessage('ЧИТ: SPAWN BOSS', 'Кубическая змея появилась!', 2.5);
}
cheatHeal(hasPlayer){
if(!hasPlayer){ this.pending.heal = true;
this.showSettingsMessage('HP восстановится при старте', '#ffcc00'); return; }
this.player.hp = this.player.maxHp;
sound.cheatOn();
this.showMessage('ЧИТ: HEAL', 'HP полностью восстановлено', 1.8);
}
cheatResetCooldowns(hasPlayer){
if(!hasPlayer){ this.pending.resetCD = true;
this.showSettingsMessage('Кулдауны сбросятся при старте', '#ffcc00'); return; }
this.player.dashCooldownTimer = 0;
this.player.rageCooldownTimer = 0;
this.player.dashTimer = 0;
sound.cheatOn();
this.showMessage('ЧИТ: RESET CD', 'Кулдауны сброшены', 1.8);
}
cheatNuke(){
for(const b of this.bots){
if(b.alive){ b.alive = false; b.dead = true; this.spawnExplosion(b.x, b.y, '#ff5577'); }
}
if(this.boss && this.boss.alive) this.damageEntity(this.boss, 1500, this.player);
const px = this.player ? this.player.x : CONFIG.WORLD_W/2;
const py = this.player ? this.player.y : CONFIG.WORLD_H/2;
for(let i = 0; i < 20; i++){
const a = rand(0, TAU), d = rand(100, 700);
this.spawnExplosion(px + Math.cos(a) * d, py + Math.sin(a) * d, '#ff5577');
}
this.shake(22); this.damageFlash = 1;
this.showMessage('ЧИТ: NUKE', 'Всё снесено!', 2.0);
}
cheatLongRage(hasPlayer){
if(!hasPlayer){ this.pending.longRage = true;
this.showSettingsMessage('Ярость 60 сек включится при старте', '#ffcc00'); return; }
this.player.rageActive = true;
this.player.rageTimer = 60.0;
this.player.rageCooldownTimer = 0;
sound.rage();
this.showMessage('ЧИТ: LONG RAGE', 'Ярость на 60 сек!', 2.5);
}
applyPendingCheats(){
if(!this.player) return;
if(this.pending.droneCount > 0){
const canAdd = Math.max(0, 200 - this.drones.length);
const toAdd = Math.min(this.pending.droneCount, canAdd);
for(let i = 0; i < toAdd; i++) this.drones.push(new Drone(this.player, this.drones.length));
if(toAdd > 0){
this.rings.push(new Ring(this.player.x, this.player.y, '#7df9ff', 140, 0.8));
this.showMessage('ЧИТ: DRONES', '+' + toAdd + ' дронов', 2.0);
}
this.pending.droneCount = 0;
}
if(this.pending.maxUpgrades){
this.cheatMaxUpgrades(true);
this.pending.maxUpgrades = false;
}
if(this.pending.heal){ this.player.hp = this.player.maxHp; this.pending.heal = false; }
if(this.pending.resetCD){
this.player.dashCooldownTimer = 0;
this.player.rageCooldownTimer = 0;
this.pending.resetCD = false;
}
if(this.pending.longRage){
this.player.rageActive = true;
this.player.rageTimer = 60.0;
this.player.rageCooldownTimer = 0;
this.pending.longRage = false;
}
if(this.pending.spawnBoss){ this.cheatSpawnBoss(true); this.pending.spawnBoss = false; }
}
resetCheats(){
this.godMode = false;
this.infiniteAmmo = false;
this.cheatDamageMult = 1;
this.cheatSpeedMult = 1;
this.cheatHistory = [];
this.pending = { droneCount:0, heal:false, resetCD:false, longRage:false, spawnBoss:false, maxUpgrades:false };
this.showSettingsMessage('Все читы сброшены', '#ffcc00');
sound.cheatOff();
}
/* =================== КОНЕЦ ЧИТ-СИСТЕМЫ =================== */
positionCheatInput(){
if(!this.inputEl) return;
if(this.state !== 'settings' || this.showCheatList){
if(this.inputEl.style.display !== 'none') this.inputEl.style.display = 'none';
return;
}
const r = this.canvas.getBoundingClientRect();
const scale = r.width / CONFIG.W;
const rect = this.settingsCheatInputRect;
this.inputEl.style.display = 'block';
this.inputEl.style.left = (r.left + rect.x * scale) + 'px';
this.inputEl.style.top = (r.top + rect.y * scale) + 'px';
this.inputEl.style.width = (rect.w * scale) + 'px';
this.inputEl.style.height = (rect.h * scale) + 'px';
this.inputEl.style.fontSize = Math.max(14, Math.round(20 * scale)) + 'px';
this.inputEl.style.padding = Math.round(8*scale) + 'px ' + Math.round(14*scale) + 'px';
}
returnToMenu(){
sound.click();
this.saveProgressOnGameOver();
this.state = 'menu';
this.player = null;
this.boss = null;
this.bossDefeatedTimer = 0;
this.walls = []; this.bullets = []; this.particles = []; this.rings = [];
this.bots = []; this.drones = [];
this.message = {text:'', sub:'', t:0, max:1};
this.moveStick.end(); this.aimStick.end();
this.mouse.down = false;
this.keys = {};
this.godMode = false;
this.infiniteAmmo = false;
this.cheatDamageMult = 1;
this.cheatSpeedMult = 1;
this.cheatBuffer = '';
this.cheatHistory = [];
this.pending = { droneCount:0, heal:false, resetCD:false, longRage:false, spawnBoss:false, maxUpgrades:false };
if(this.inputEl) { this.inputEl.value = ''; this.inputEl.blur(); }
this.positionCheatInput();
setGameplayActive(false);
}
saveProgressOnGameOver(){
if(this.score > this.highScore) this.highScore = this.score;
this.totalKills += this.kills;
if(this.room > 0) this.totalRooms += Math.max(0, this.room - 1);
this.saveLocalProgress();
savePlayerProgress({
highScore: this.highScore,
totalKills: this.totalKills,
totalRooms: this.totalRooms
});
}
openSettings(returnState){
sound.click();
this.settingsReturnState = returnState || 'menu';
this.state = 'settings';
this.showCheatList = false;
this.settingsMessage = '';
this.settingsMessageUntil = 0;
this.moveStick.end(); this.aimStick.end();
this.positionCheatInput();
}
closeSettings(){
sound.click();
if(this.inputEl) { this.inputEl.value = ''; this.inputEl.blur(); }
this.showCheatList = false;
this.state = this.settingsReturnState || 'menu';
this.positionCheatInput();
}
setupInput(){
const c=this.canvas;
window.addEventListener('contextmenu', e => e.preventDefault());
document.addEventListener('selectstart', e => e.preventDefault());
document.addEventListener('dragstart', e => e.preventDefault());
document.addEventListener('touchstart', e => { if(e.target === c) e.preventDefault(); }, { passive: false });
window.addEventListener('keydown', e=>{
const k=e.key.toLowerCase();
if(document.activeElement === this.inputEl){
if(e.key === 'Enter'){ e.preventDefault(); this.tryActivateCheatFromInput(); }
else if(e.key === 'Escape'){ e.preventDefault(); this.inputEl.blur(); }
return;
}
this.keys[k]=true;
if(k==='escape'){
if(this.state==='playing'){ this.state='paused'; this.moveStick.end(); this.aimStick.end(); setGameplayActive(false); }
else if(this.state==='paused'){ this.state='playing'; setGameplayActive(true); }
else if(this.state==='settings'){ this.closeSettings(); }
}
if(k==='f') this.toggleFullscreen();
if(k==='m'){ const on = sound.toggle(); this.showMessage('', on ? 'Звук ВКЛ' : 'Звук ВЫКЛ', 1.0); }
if(this.state==='playing' && this.player){
if(k==='r' || k==='к') this.player.startReload();
if(k===' ' && !e.repeat) this.player.startDash(this);
if(k==='q' || k==='й'){ if(!e.repeat) this.player.startRage(this); }
}
if(this.state==='menu'){
if(k==='1'){ this.selectedDifficulty='easy'; sound.click(); }
if(k==='2'){ this.selectedDifficulty='medium'; sound.click(); }
if(k==='3'){ this.selectedDifficulty='hard'; sound.click(); }
if(k==='4'){ this.selectedDifficulty='hardcore'; sound.click(); }
}
if((k==='enter'||k===' ') && this.state==='menu'){ e.preventDefault(); this.startGame(); }
if((k==='enter'||k===' ') && this.state==='gameover'){ e.preventDefault(); this.startGame(); }
if(k==='enter' && this.state==='upgrade'){ e.preventDefault(); this.finishUpgradeAndContinue(); }
if(['arrowup','arrowdown','arrowleft','arrowright',' '].includes(k)) e.preventDefault();
if(this.state === 'playing' || this.state === 'paused'){
if(!e.repeat && k.length === 1) this.handleCheatInput(k);
}
});
window.addEventListener('keyup', e=>{ this.keys[e.key.toLowerCase()]=false; });
c.addEventListener('mousemove', e=>{
const m=this.toCanvasCoords(e);
this.mouse.x=m.x; this.mouse.y=m.y;
if(this.state!=='paused') this.aimMode='mouse';
});
c.addEventListener('mousedown', e=>{
const m=this.toCanvasCoords(e);
this.mouse.x=m.x; this.mouse.y=m.y;
sound.init(); sound.resume();
if(document.activeElement === this.inputEl) this.inputEl.blur();
if(this.state==='menu'){
for(const btn of this.difficultyBtns){
if(this.inRect(m,btn)){ this.selectedDifficulty = btn.key; sound.click(); return; }
}
if(this.inRect(m,this.startBtn)){ this.startGame(); return; }
if(this.inRect(m,this.fullscreenBtn)){ sound.click(); this.toggleFullscreen(); return; }
if(this.inRect(m,this.soundBtn)){ const on=sound.toggle(); this.showMessage('', on?'Звук ВКЛ':'Звук ВЫКЛ', 1.0); return; }
if(this.inRect(m,this.settingsBtn)){ this.openSettings('menu'); return; }
return;
}
if(this.state==='settings'){
if(this.showCheatList){
if(this.inRect(m,this.settingsBackBtn)){ sound.click(); this.showCheatList = false; this.positionCheatInput(); return; }
return;
}
if(this.inRect(m,this.settingsBackBtn)){ this.closeSettings(); return; }
if(this.inRect(m,this.settingsSoundBtn)){ sound.init(); sound.resume(); const on = sound.toggle(); this.showSettingsMessage(on?'Звук ВКЛ':'Звук ВЫКЛ', '#7df9ff'); return; }
if(this.inRect(m,this.settingsVolumeDownBtn)){ sound.setVolume(sound.volume - 0.1); this.showSettingsMessage('Громкость: ' + Math.round(sound.volume*100) + '%', '#7df9ff'); return; }
if(this.inRect(m,this.settingsVolumeUpBtn)){ sound.setVolume(sound.volume + 0.1); this.showSettingsMessage('Громкость: ' + Math.round(sound.volume*100) + '%', '#7df9ff'); return; }
if(this.inRect(m,this.settingsCheatActivateBtn)){ this.tryActivateCheatFromInput(); return; }
if(this.inRect(m,this.settingsShowCodesBtn)){ sound.click(); this.showCheatList = true; this.positionCheatInput(); return; }
if(this.inRect(m,this.settingsResetCheatsBtn)){ this.resetCheats(); return; }
return;
}
if(this.state==='gameover'){
if(this.inRect(m,this.restartBtn)){ this.startGame(); return; }
return;
}
if(this.state==='paused'){
if(this.inRect(m,this.resumeBtn)){ sound.click(); this.state='playing'; setGameplayActive(true); return; }
if(this.inRect(m,this.pauseSoundBtn)){
const on = sound.toggle();
this.showMessage('', on ? 'Звук ВКЛ' : 'Звук ВЫКЛ', 1.0);
return;
}
if(this.inRect(m,this.pauseSettingsBtn)){ this.openSettings('paused'); return; }
if(this.inRect(m,this.pauseMenuBtn)){ this.returnToMenu(); return; }
return;
}
if(this.state==='upgrade'){ this.handleUpgradeClick(m); return; }
if(this.state==='playing' && this.player){
if(this.inRect(m,this.reloadBtn)){ this.player.startReload(); return; }
if(this.inRect(m,this.dashBtn)){ this.player.startDash(this); return; }
if(this.inRect(m,this.rageBtn)){ this.player.startRage(this); return; }
if(this.inRect(m,this.pauseBtn)){ this.state='paused'; this.moveStick.end(); this.aimStick.end(); setGameplayActive(false); sound.click(); return; }
}
if(e.button===0) this.mouse.down=true;
});
window.addEventListener('mouseup', e=>{
if(e.button===0) this.mouse.down=false;
});
window.addEventListener('blur', ()=>{
this.keys={}; this.mouse.down=false;
this.moveStick.end(); this.aimStick.end();
if(this.state === 'playing'){
this.state = 'paused';
sound.muteForPause();
setGameplayActive(false);
}
});
window.addEventListener('focus', ()=>{
sound.unmuteAfterPause();
});
c.addEventListener('touchstart', e=>{
e.preventDefault();
this.aimMode='touch';
this.mouse.down=false;
sound.init(); sound.resume();
for(const t of e.changedTouches){
const p=this.toCanvasCoords(t);
if(this.state==='menu'){
for(const btn of this.difficultyBtns){
if(this.inRect(p,btn)){ this.selectedDifficulty = btn.key; sound.click(); return; }
}
if(this.inRect(p,this.startBtn)){ this.startGame(); return; }
if(this.inRect(p,this.fullscreenBtn)){ sound.click(); this.toggleFullscreen(); return; }
if(this.inRect(p,this.soundBtn)){ const on=sound.toggle(); this.showMessage('', on?'Звук ВКЛ':'Звук ВЫКЛ', 1.0); return; }
if(this.inRect(p,this.settingsBtn)){ this.openSettings('menu'); return; }
continue;
}
if(this.state==='settings'){
if(this.showCheatList){
if(this.inRect(p,this.settingsBackBtn)){ sound.click(); this.showCheatList = false; this.positionCheatInput(); return; }
continue;
}
if(this.inRect(p, this.settingsCheatInputRect)){ this.inputEl.focus(); continue; }
if(this.inRect(p,this.settingsBackBtn)){ this.closeSettings(); return; }
if(this.inRect(p,this.settingsSoundBtn)){ const on = sound.toggle(); this.showSettingsMessage(on?'Звук ВКЛ':'Звук ВЫКЛ', '#7df9ff'); continue; }
if(this.inRect(p,this.settingsVolumeDownBtn)){ sound.setVolume(sound.volume - 0.1); this.showSettingsMessage('Громкость: ' + Math.round(sound.volume*100) + '%', '#7df9ff'); continue; }
if(this.inRect(p,this.settingsVolumeUpBtn)){ sound.setVolume(sound.volume + 0.1); this.showSettingsMessage('Громкость: ' + Math.round(sound.volume*100) + '%', '#7df9ff'); continue; }
if(this.inRect(p,this.settingsCheatActivateBtn)){ this.tryActivateCheatFromInput(); continue; }
if(this.inRect(p,this.settingsShowCodesBtn)){ sound.click(); this.showCheatList = true; this.positionCheatInput(); continue; }
if(this.inRect(p,this.settingsResetCheatsBtn)){ this.resetCheats(); continue; }
continue;
}
if(this.state==='gameover'){
if(this.inRect(p,this.restartBtn)){ this.startGame(); return; }
continue;
}
if(this.state==='paused'){
if(this.inRect(p,this.resumeBtn)){ sound.click(); this.state='playing'; setGameplayActive(true); return; }
if(this.inRect(p,this.pauseSoundBtn)){
const on = sound.toggle();
this.showMessage('', on ? 'Звук ВКЛ' : 'Звук ВЫКЛ', 1.0);
continue;
}
if(this.inRect(p,this.pauseSettingsBtn)){ this.openSettings('paused'); return; }
if(this.inRect(p,this.pauseMenuBtn)){ this.returnToMenu(); return; }
continue;
}
if(this.state==='upgrade'){ this.handleUpgradeClick(p); continue; }
if(this.state!=='playing') continue;
if(this.inRect(p,this.reloadBtn)){ if(this.player) this.player.startReload(); continue; }
if(this.inRect(p,this.dashBtn)){ if(this.player) this.player.startDash(this); continue; }
if(this.inRect(p,this.rageBtn)){ if(this.player) this.player.startRage(this); continue; }
if(this.inRect(p,this.pauseBtn)){
this.state='paused';
this.moveStick.end(); this.aimStick.end();
setGameplayActive(false);
sound.click();
continue;
}
if(p.x<CONFIG.W/2){
if(!this.moveStick.active) this.moveStick.start(t.identifier,p.x,p.y);
} else {
if(!this.aimStick.active) this.aimStick.start(t.identifier,p.x,p.y);
}
}
}, {passive:false});
c.addEventListener('touchmove', e=>{
e.preventDefault();
for(const t of e.changedTouches){
const p=this.toCanvasCoords(t);
if(this.moveStick.active && this.moveStick.touchId===t.identifier)
this.moveStick.move(p.x,p.y);
if(this.aimStick.active && this.aimStick.touchId===t.identifier)
this.aimStick.move(p.x,p.y);
}
}, {passive:false});
const endTouch=e=>{
e.preventDefault();
for(const t of e.changedTouches){
if(this.moveStick.active && this.moveStick.touchId===t.identifier) this.moveStick.end();
if(this.aimStick.active && this.aimStick.touchId===t.identifier) this.aimStick.end();
}
};
c.addEventListener('touchend', endTouch, {passive:false});
c.addEventListener('touchcancel', endTouch, {passive:false});
}
toCanvasCoords(e){
const r=this.canvas.getBoundingClientRect();
const sx = r.width > 0 ? CONFIG.W / r.width : 1;
const sy = r.height > 0 ? CONFIG.H / r.height : 1;
return { x: (e.clientX - r.left) * sx, y: (e.clientY - r.top ) * sy };
}
inRect(p,r){
return p.x>=r.x && p.x<=r.x+r.w && p.y>=r.y && p.y<=r.y+r.h;
}
worldMouse(){ return { x:this.mouse.x+this.cam.x, y:this.mouse.y+this.cam.y }; }
toggleFullscreen(){
const doc=document, el=doc.documentElement;
if(!doc.fullscreenElement){
if(el.requestFullscreen) el.requestFullscreen().catch(()=>{});
else if(el.webkitRequestFullscreen) el.webkitRequestFullscreen();
} else {
if(doc.exitFullscreen) doc.exitFullscreen();
else if(doc.webkitExitFullscreen) doc.webkitExitFullscreen();
}
}
startGame(){
sound.init(); sound.resume();
sound.startGame();
this.reset();
this.state='playing';
this.mouse.down=false;
this.moveStick.end(); this.aimStick.end();
this.applyPendingCheats();
setGameplayActive(true);
this._adCooldown = 90;
}
reset(){
this.walls=[]; this.bullets=[]; this.particles=[];
this.rings=[]; this.bots=[]; this.drones=[];
this.boss = null; this.bossDefeatedTimer = 0;
this.difficulty = DIFFICULTIES[this.selectedDifficulty];
this.respawnsLeft = this.difficulty.respawns;
this.deathTimer = 0;
this.room=0; this.score=0; this.kills=0;
this.toSpawn=0; this.spawnTimer=0;
this.roomActive=false; this.elapsed=0;
this.shakeAmount=0; this.damageFlash=0;
this.message={text:'',sub:'',t:0,max:1};
this.upgradeCounts={};
this.droneDamageMult=1;
this.droneFireRateMult=1;
this._lowHpWarnTimer = 0;
this.cheatBuffer = '';
this.cheatBufferTime = 0;
this.cheatHistory = [];
this.generateMap();
this.player=new Player(CONFIG.WORLD_W/2, CONFIG.WORLD_H/2);
this.player.maxHp = Math.round(this.player.maxHp * this.difficulty.playerHpMult);
this.player.hp = this.player.maxHp;
this.recenterCamera();
this.startRoom();
}
recenterCamera(){
if(!this.player) return;
const maxX=Math.max(0,CONFIG.WORLD_W-CONFIG.W);
const maxY=Math.max(0,CONFIG.WORLD_H-CONFIG.H);
this.cam.x=clamp(this.player.x-CONFIG.W/2, 0, maxX);
this.cam.y=clamp(this.player.y-CONFIG.H/2, 0, maxY);
}
generateMap(){
const t=50;
const WW=CONFIG.WORLD_W, WH=CONFIG.WORLD_H;
this.walls.push(new Wall(-t,-t,WW+t*2,t));
this.walls.push(new Wall(-t,WH,WW+t*2,t));
this.walls.push(new Wall(-t,-t,t,WH+t*2));
this.walls.push(new Wall(WW,-t,t,WH+t*2));
const spawnZone={x:WW/2-200,y:WH/2-200,w:400,h:400};
let attempts=0, placed=0;
while(placed<30 && attempts<900){
attempts++;
const w=rand(70,230), h=rand(70,230);
const x=rand(90,WW-90-w), y=rand(90,WH-90-h);
const rect={x,y,w,h};
if(rectsOverlap(rect,spawnZone,90)) continue;
let ok=true;
for(const wl of this.walls){
if(rectsOverlap(rect,wl,72)){ ok=false; break; }
}
if(!ok) continue;
this.walls.push(new Wall(x,y,w,h));
placed++;
}
}
randomFreePoint(){
for(let i=0;i<250;i++){
const x=rand(90,CONFIG.WORLD_W-90);
const y=rand(90,CONFIG.WORLD_H-90);
if(this.pointFree(x,y,42)) return {x,y};
}
return {x:CONFIG.WORLD_W/2, y:CONFIG.WORLD_H/2};
}
pointFree(x,y,r){
for(const w of this.walls){
if(x>w.x-r && x<w.x+w.w+r && y>w.y-r && y<w.y+w.h+r) return false;
}
return true;
}
findSpawnPoint(){
for(let i=0;i<300;i++){
const x=rand(110,CONFIG.WORLD_W-110);
const y=rand(110,CONFIG.WORLD_H-110);
if(!this.pointFree(x,y,50)) continue;
if(this.player && dist2(x,y,this.player.x,this.player.y)<620) continue;
return {x,y};
}
return this.randomFreePoint();
}
findBossSpawnPoint(){
for(let i=0;i<300;i++){
const x=rand(200,CONFIG.WORLD_W-200);
const y=rand(200,CONFIG.WORLD_H-200);
if(!this.pointFree(x,y,80)) continue;
if(this.player && dist2(x,y,this.player.x,this.player.y)<800) continue;
return {x,y};
}
return {x: 300, y: 300};
}
getRoomBotCount(){
return this.difficulty.botCount + (this.room - 1) * this.difficulty.botGrowth;
}
isBossRoom(room = this.room){
return room % CONFIG.BOSS_EVERY === 0;
}
startRoom(){
this.room++;
this.roomActive = true;
this.boss = null;
this.bossDefeatedTimer = 0;
if(this.isBossRoom()){
this.toSpawn = 0;
this.spawnTimer = 999;
this.spawnBoss();
sound.bossAppear();
this.shake(20);
this.showMessage('КОМНАТА ' + this.room, '⚠ БОСС: КУБИЧЕСКАЯ ЗМЕЯ ⚠', 3.0);
return;
}
this.toSpawn = this.getRoomBotCount();
this.spawnTimer = 0;
sound.roomStart();
const hpMul = (1 + (this.room-1)*CONFIG.ROOM_HP_SCALE) * this.difficulty.botHpMult;
const dmgMul = (1 + (this.room-1)*CONFIG.ROOM_DMG_SCALE) * this.difficulty.botDmgMult;
if(this.room === 1){
this.showMessage('КОМНАТА 1', this.difficulty.name + ' · ' + this.toSpawn + ' врагов', 2.5);
} else {
const nextBoss = CONFIG.BOSS_EVERY - (this.room % CONFIG.BOSS_EVERY);
const suffix = nextBoss <= 2 ? ' · босс через ' + nextBoss : '';
this.showMessage('КОМНАТА ' + this.room,
this.toSpawn + ' врагов · HP ×' + hpMul.toFixed(2) + ' · Урон ×' + dmgMul.toFixed(2) + suffix, 2.5);
}
}
spawnBoss(){
const p = this.findBossSpawnPoint();
this.boss = new BossSnake(p.x, p.y, this);
this.rings.push(new Ring(p.x, p.y, '#ff3b6b', 180, 1.2));
this.rings.push(new Ring(p.x, p.y, '#b06bff', 120, 0.9));
this.spawnSparks(p.x, p.y, '#ff3b6b', 40);
this.spawnSparks(p.x, p.y, '#b06bff', 30);
}
updateRooms(dt){
if(!this.roomActive) return;
if(this.bossDefeatedTimer > 0){
this.bossDefeatedTimer -= dt;
if(this.bossDefeatedTimer <= 0){
this.boss = null;
const heal = Math.round(this.player.maxHp * CONFIG.HEAL_ON_BREAK);
this.player.hp = Math.min(this.player.maxHp, this.player.hp + heal);
const bossRoomNum = Math.floor(this.room / CONFIG.BOSS_EVERY);
const bonus = 1500 * Math.max(1, bossRoomNum);
this.score += bonus;
this.showMessage('БОСС ПОВЕРЖЕН', '+' + bonus + ' очков · +' + heal + ' HP', 3.5);
this.enterUpgradeState();
}
return;
}
if(this.isBossRoom()){
if(this.boss && !this.boss.alive) this.bossDefeatedTimer = 2.2;
return;
}
const maxAlive = Math.max(20, Math.floor(this.getRoomBotCount() * 0.6));
if(this.toSpawn > 0){
this.spawnTimer -= dt;
if(this.spawnTimer <= 0 && this.bots.length < maxAlive){
this.spawnBot();
this.toSpawn--;
this.spawnTimer = rand(0.20, 0.55);
}
}
if(this.toSpawn === 0 && this.bots.length === 0){
if(this.boss && this.boss.alive) return;
this.roomActive = false;
const heal = Math.round(this.player.maxHp * CONFIG.HEAL_ON_BREAK);
this.player.hp = Math.min(this.player.maxHp, this.player.hp + heal);
const bonus = 100 * this.room;
this.score += bonus;
sound.roomClear();
this.enterUpgradeState();
}
}
pickBotType(){
const r = this.room;
const rnd = Math.random();
if(r <= 2) {
if(r === 1) return 'normal';
return rnd < 0.75 ? 'normal' : 'heavy';
}
if(r <= 4){
if(rnd < 0.5) return 'normal';
if(rnd < 0.75) return 'heavy';
if(rnd < 0.9) return 'sniper';
return 'sapper';
}
if(rnd < 0.35) return 'normal';
if(rnd < 0.55) return 'heavy';
if(rnd < 0.75) return 'sniper';
return 'sapper';
}
spawnBot(){
const p=this.findSpawnPoint();
const type=this.pickBotType();
this.bots.push(new Bot(p.x,p.y,type,this));
this.rings.push(new Ring(p.x,p.y,CONFIG.BOT_TYPES[type].color,55,0.5));
this.spawnSparks(p.x,p.y,CONFIG.BOT_TYPES[type].color,12);
}
restartRoom(){
this.bots = []; this.bullets = []; this.particles = []; this.rings = [];
this.boss = null; this.bossDefeatedTimer = 0;
if(this.isBossRoom()){
this.toSpawn = 0; this.spawnTimer = 999; this.spawnBoss();
} else {
this.toSpawn = this.getRoomBotCount(); this.spawnTimer = 0;
}
this.roomActive = true;
for(const d of this.drones){
d.x = this.player.x + Math.cos(d.angle) * d.orbitR;
d.y = this.player.y + Math.sin(d.angle) * d.orbitR;
}
}
respawnPlayer(){
const p = this.player;
p.alive = true;
p.hp = p.maxHp;
p.x = CONFIG.WORLD_W/2; p.y = CONFIG.WORLD_H/2;
p.vx = 0; p.vy = 0;
p.mag = p.maxMag;
p.reloading = false; p.reloadTimer = 0;
p.spread = 0; p.hitFlash = 0;
p.dashTimer = 0; p.dashCooldownTimer = 0;
p.rageActive = false; p.rageTimer = 0; p.rageCooldownTimer = 0;
p.invulnerable = false;
this.restartRoom();
this.recenterCamera();
this.state = 'playing';
sound.respawn();
const msg = this.difficulty.respawns >= 99
? 'Комната ' + this.room + ' перезапущена'
: 'Комната ' + this.room + ' перезапущена · Возрождений: ' + this.respawnsLeft;
this.showMessage('ВОЗРОЖДЕНИЕ', msg, 2.5);
}
enterUpgradeState(){
this.state='upgrade';
this.moveStick.end(); this.aimStick.end();
this.mouse.down=false;
this.layoutUpgradeCards();
setGameplayActive(false);
this._adCooldown -= 1;
if(this._adCooldown <= 0 && this.room > 2 && this.room % 3 === 0){
this._adCooldown = 3;
showFullscreenAd(() => {});
}
}
handleUpgradeClick(p){
if(this.inRect(p,this.upgradeContinueBtn)){ sound.click(); this.finishUpgradeAndContinue(); return; }
for(const card of this.upgradeCards){
if(this.inRect(p,card)){ this.buyUpgrade(card.upgrade); return; }
}
}
buyUpgrade(up){
const cost=this.getUpgradeCost(up);
if(this.score<cost){ sound.deny(); this.showMessage('','Недостаточно очков',0.9); return false; }
this.score-=cost;
this.upgradeCounts[up.id]=(this.upgradeCounts[up.id]||0)+1;
up.apply(this);
this.spawnSparks(this.player.x,this.player.y,'#7df9ff',18);
this.rings.push(new Ring(this.player.x,this.player.y,'#7df9ff',60,0.5));
sound.upgrade();
this.showMessage('',up.name+' улучшено',0.9);
return true;
}
finishUpgradeAndContinue(){ this.state='playing'; setGameplayActive(true); this.startRoom(); }
addDrone(){
if(!this.player) return;
const d=new Drone(this.player,this.drones.length);
this.drones.push(d);
this.rings.push(new Ring(d.x,d.y,'#7df9ff',40,0.5));
this.spawnSparks(d.x,d.y,'#7df9ff',12);
}
shake(v){ this.shakeAmount=Math.min(this.shakeAmount+v,22); }
showMessage(text,sub,t){
if(!text && this.message.t>t) return;
this.message={text, sub:sub||'', t, max:t};
}
spawnMuzzle(x,y,ang,color){
for(let i=0;i<4;i++){
const a=ang+rand(-0.4,0.4);
const s=rand(90,300);
this.particles.push(new Particle(x,y,Math.cos(a)*s,Math.sin(a)*s, rand(0.07,0.17),color,rand(1.5,3)));
}
}
spawnSparks(x,y,color,n){
for(let i=0;i<n;i++){
const a=rand(0,TAU), s=rand(60,320);
this.particles.push(new Particle(x,y,Math.cos(a)*s,Math.sin(a)*s, rand(0.12,0.4),color,rand(1,2.6)));
}
}
spawnBlood(x,y,color,n){
for(let i=0;i<n;i++){
const a=rand(0,TAU), s=rand(40,260);
this.particles.push(new Particle(x,y,Math.cos(a)*s,Math.sin(a)*s, rand(0.25,0.6),color,rand(1.5,3.5),'blood'));
}
}
spawnExplosion(x,y,color){
for(let i=0;i<28;i++){
const a=rand(0,TAU), s=rand(80,540);
this.particles.push(new Particle(x,y,Math.cos(a)*s,Math.sin(a)*s, rand(0.3,0.85),color,rand(1.5,4.2)));
}
this.rings.push(new Ring(x,y,color,85,0.45));
}
moveEntity(ent,dx,dy){
ent.x+=dx;
for(const w of this.walls){
const c=w.circleCollide(ent.x,ent.y,ent.r);
if(c){ ent.x+=c.nx*c.depth; ent.y+=c.ny*c.depth; }
}
ent.y+=dy;
for(const w of this.walls){
const c=w.circleCollide(ent.x,ent.y,ent.r);
if(c){ ent.x+=c.nx*c.depth; ent.y+=c.ny*c.depth; }
}
ent.x=clamp(ent.x,ent.r,CONFIG.WORLD_W-ent.r);
ent.y=clamp(ent.y,ent.r,CONFIG.WORLD_H-ent.r);
}
damageEntity(ent,dmg,source){
if(!ent.alive) return;
if(ent.isPlayer && ent.invulnerable) return;
if(ent.isPlayer && this.godMode) return;
ent.hp-=dmg;
ent.hitFlash=1;
let col;
if(ent.isPlayer) col = '#ff3355';
else if(ent.isBoss) col = '#ff3b6b';
else col = ent.cfg.color;
this.spawnBlood(ent.x, ent.y, col, ent.isBoss ? 10 : 6);
if(ent.isPlayer){ this.shake(7); this.damageFlash=1; sound.playerHit(); }
else {
const sv = sound.spatialVolume(ent.x, ent.y, this.cam.x, this.cam.y, 1000);
if(sv > 0.05) sound.hit(sv);
}
if(ent.hp<=0){ ent.hp=0; this.killEntity(ent,source); }
}
killEntity(ent,source){
if(ent.isPlayer){
if(this.godMode){ ent.hp = ent.maxHp; return; }
ent.alive=false;
this.spawnExplosion(ent.x,ent.y,'#22e6ff');
this.shake(18);
sound.bigExplosion();
this.state = 'dying';
this.deathTimer = 1.6;
return;
}
if(ent.isBoss){
ent.alive=false; ent.dead=true;
const parts = [{x: ent.x, y: ent.y}].concat(ent.segments);
for(const p of parts) this.spawnExplosion(p.x, p.y, '#ff3b6b');
this.shake(22);
sound.bossDefeated();
if(source===this.player){ this.kills++; this.score += 2000; }
return;
}
ent.alive=false; ent.dead=true;
this.spawnExplosion(ent.x,ent.y, ent.isSapper ? '#ffcc00' : ent.cfg.color);
this.shake(ent.isSapper ? 6 : 4);
const sv = sound.spatialVolume(ent.x, ent.y, this.cam.x, this.cam.y, 1000);
if(sv > 0.05) sound.explosion(sv);
if(source===this.player){ this.kills++; this.score+=ent.cfg.score; }
}
update(dt){
if(this.state === 'dying'){
this.deathTimer -= dt;
this.shakeAmount *= Math.pow(0.0015,dt);
if(this.shakeAmount<0.05) this.shakeAmount=0;
this.damageFlash = Math.max(0, this.damageFlash - dt*2.2);
for(const p of this.particles) p.update(dt);
for(const r of this.rings) r.update(dt);
for(const b of this.bullets) b.update(dt,this);
this.particles = this.particles.filter(p=>!p.dead);
this.rings = this.rings.filter(r=>!r.dead);
this.bullets = this.bullets.filter(b=>!b.dead);
if(this.message.t>0) this.message.t -= dt;
if(this.deathTimer <= 0){
if(this.respawnsLeft > 0){
this.respawnsLeft--;
this.respawnPlayer();
} else {
this.state = 'gameover';
this.mouse.down = false;
this.moveStick.end(); this.aimStick.end();
sound.gameOver();
setGameplayActive(false);
this.saveProgressOnGameOver();
}
}
return;
}
if(this.state!=='playing') return;
this.elapsed+=dt;
const maxCamX=Math.max(0,CONFIG.WORLD_W-CONFIG.W);
const maxCamY=Math.max(0,CONFIG.WORLD_H-CONFIG.H);
const tcx=clamp(this.player.x-CONFIG.W/2, 0, maxCamX);
const tcy=clamp(this.player.y-CONFIG.H/2, 0, maxCamY);
const k=1-Math.pow(0.0015,dt);
this.cam.x=lerp(this.cam.x,tcx,k);
this.cam.y=lerp(this.cam.y,tcy,k);
this.shakeAmount*=Math.pow(0.0015,dt);
if(this.shakeAmount<0.05) this.shakeAmount=0;
this.damageFlash=Math.max(0,this.damageFlash-dt*2.2);
if(this.player.alive && this.player.hp < this.player.maxHp*0.3 && !this.godMode){
this._lowHpWarnTimer -= dt;
if(this._lowHpWarnTimer <= 0){ sound.tickLowHp(); this._lowHpWarnTimer = 0.7; }
}
this.player.update(dt,this);
for(const b of this.bots) b.update(dt,this);
if(this.boss && this.boss.alive) this.boss.update(dt, this);
for(const d of this.drones) d.update(dt,this);
for(const b of this.bullets) b.update(dt,this);
for(const p of this.particles) p.update(dt);
for(const r of this.rings) r.update(dt);
this.bullets=this.bullets.filter(b=>!b.dead);
this.particles=this.particles.filter(p=>!p.dead);
this.rings=this.rings.filter(r=>!r.dead);
this.bots=this.bots.filter(b=>!b.dead);
this.updateRooms(dt);
if(this.message.t>0) this.message.t-=dt;
const now = performance.now();
this.cheatHistory = this.cheatHistory.filter(h => now - h.time < 4000);
}
render(){
const ctx=this.ctx;
ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
ctx.clearRect(0,0,CONFIG.W,CONFIG.H);
ctx.fillStyle='#07070d';
ctx.fillRect(0,0,CONFIG.W,CONFIG.H);
if(this.state === 'settings' && !this.showCheatList){
if(this.inputEl.style.display !== 'block') this.positionCheatInput();
} else {
if(this.inputEl && this.inputEl.style.display !== 'none') this.inputEl.style.display = 'none';
}
if(this.state==='menu'){ this.drawBackgroundGrid(ctx); this.drawMenu(ctx); return; }
if(this.state==='settings'){ this.drawBackgroundGrid(ctx); this.drawSettings(ctx); return; }
this.shakeX=rand(-1,1)*this.shakeAmount;
this.shakeY=rand(-1,1)*this.shakeAmount;
ctx.save();
ctx.translate(-this.cam.x+this.shakeX, -this.cam.y+this.shakeY);
this.drawWorldGrid(ctx);
for(const w of this.walls) w.draw(ctx);
for(const r of this.rings) r.draw(ctx);
for(const p of this.particles) p.draw(ctx);
for(const b of this.bots) b.draw(ctx);
if(this.boss && this.boss.alive) this.boss.draw(ctx);
for(const d of this.drones) d.draw(ctx);
if(this.player) this.player.draw(ctx);
for(const b of this.bullets) b.draw(ctx);
ctx.restore();
if(this.damageFlash>0){
const g=ctx.createRadialGradient(CONFIG.W/2,CONFIG.H/2,CONFIG.H*0.25,CONFIG.W/2,CONFIG.H/2,CONFIG.H*0.8);
g.addColorStop(0,'rgba(255,0,40,0)');
g.addColorStop(1,'rgba(255,0,40,'+(this.damageFlash*0.55)+')');
ctx.fillStyle=g; ctx.fillRect(0,0,CONFIG.W,CONFIG.H);
}
if(this.state==='playing' && this.player && this.player.rageActive){
ctx.save(); ctx.globalAlpha = 0.10 + Math.sin(performance.now()/150) * 0.03;
ctx.fillStyle = '#ff2244'; ctx.fillRect(0,0,CONFIG.W,CONFIG.H);
ctx.restore();
}
if(this.state!=='upgrade'){
this.drawHUD(ctx);
this.drawMinimap(ctx);
this.drawTouchControls(ctx);
this.drawBossBar(ctx);
this.drawMessage(ctx);
this.drawCheatIndicators(ctx);
this.drawCheatHistory(ctx);
}
if(this.state==='dying'){
ctx.save();
ctx.fillStyle='rgba(60,0,20,0.35)';
ctx.fillRect(0,0,CONFIG.W,CONFIG.H);
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.font='bold 52px Consolas, monospace';
const canRespawn = this.respawnsLeft > 0;
ctx.fillStyle = canRespawn ? '#7df9ff' : '#ff3355';
ctx.shadowColor = ctx.fillStyle; ctx.shadowBlur = 30;
ctx.fillText(canRespawn ? 'ВОЗРОЖДЕНИЕ...' : 'СМЕРТЬ', CONFIG.W/2, CONFIG.H/2 - 30);
if(canRespawn){
ctx.font='bold 20px Consolas, monospace';
ctx.fillStyle='#ffd166'; ctx.shadowColor='#ff9f1c';
ctx.fillText('Комната ' + this.room + ' будет перезапущена · Осталось: ' + (this.respawnsLeft-1), CONFIG.W/2, CONFIG.H/2 + 30);
}
ctx.restore();
}
if(this.bossDefeatedTimer > 0){
ctx.save();
const a = Math.min(1, (2.2 - this.bossDefeatedTimer) * 2);
ctx.globalAlpha = a;
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.font='bold 68px Consolas, monospace';
ctx.fillStyle='#ffd166'; ctx.shadowColor='#ff9f1c'; ctx.shadowBlur=40;
ctx.fillText('БОСС ПОВЕРЖЕН', CONFIG.W/2, CONFIG.H*0.45);
ctx.restore();
}
if(this.state==='paused') this.drawPause(ctx);
if(this.state==='gameover') this.drawGameOver(ctx);
if(this.state==='upgrade') this.drawUpgradeScreen(ctx);
}
drawCheatIndicators(ctx){
const active = [];
if(this.godMode) active.push('GOD');
if(this.infiniteAmmo) active.push('AMMO ∞');
if(this.cheatDamageMult > 1) active.push('DMG×' + this.cheatDamageMult);
if(this.cheatSpeedMult > 1) active.push('SPD×' + this.cheatSpeedMult);
if(this.upgradeCounts && Object.values(this.upgradeCounts).some(v => v >= 100)) active.push('MAX UPGR');
if(active.length === 0) return;
ctx.save();
ctx.textAlign='right'; ctx.textBaseline='top';
ctx.font='bold 14px Consolas, monospace';
const t = performance.now() / 300;
const pulse = 0.75 + Math.sin(t) * 0.25;
ctx.fillStyle = 'rgba(255,80,170,' + pulse + ')';
ctx.shadowColor = '#ff44aa'; ctx.shadowBlur = 12;
ctx.fillText('⚡ ' + active.join(' · ') + ' ⚡', CONFIG.W - 20, 170);
ctx.restore();
}
drawCheatHistory(ctx){
if(!this.cheatHistory || this.cheatHistory.length === 0) return;
ctx.save();
ctx.textAlign='left'; ctx.textBaseline='bottom';
ctx.font='bold 13px Consolas, monospace';
const now = performance.now();
const baseY = CONFIG.H - 200;
for(let i = 0; i < this.cheatHistory.length; i++){
const h = this.cheatHistory[i];
const age = (now - h.time) / 4000;
const a = clamp(1 - age, 0, 1);
ctx.globalAlpha = a * 0.9;
ctx.fillStyle = '#ff44aa'; ctx.shadowColor = '#ff44aa'; ctx.shadowBlur = 8;
ctx.fillText('✓ ЧИТ: ' + h.desc, 24, baseY + i * 20);
}
ctx.restore();
}
drawWorldGrid(ctx){
const g=CONFIG.GRID;
const x0=Math.floor(this.cam.x/g)*g;
const y0=Math.floor(this.cam.y/g)*g;
ctx.strokeStyle='rgba(60,90,160,0.13)';
ctx.lineWidth=1;
ctx.beginPath();
for(let x=x0; x<this.cam.x+CONFIG.W+g; x+=g){
ctx.moveTo(x,this.cam.y-g); ctx.lineTo(x,this.cam.y+CONFIG.H+g);
}
for(let y=y0; y<this.cam.y+CONFIG.H+g; y+=g){
ctx.moveTo(this.cam.x-g,y); ctx.lineTo(this.cam.x+CONFIG.W+g,y);
}
ctx.stroke();
}
drawBackgroundGrid(ctx){
const g=60;
ctx.strokeStyle='rgba(60,90,160,0.10)';
ctx.lineWidth=1;
ctx.beginPath();
for(let x=0;x<=CONFIG.W;x+=g){ ctx.moveTo(x,0); ctx.lineTo(x,CONFIG.H); }
for(let y=0;y<=CONFIG.H;y+=g){ ctx.moveTo(0,y); ctx.lineTo(CONFIG.W,y); }
ctx.stroke();
}
drawBossBar(ctx){
if(!this.boss || !this.boss.alive) return;
const W = CONFIG.W;
const barW = Math.min(720, W * 0.72);
const barH = 22;
const x = (W - barW) / 2;
const y = 92;
ctx.save();
ctx.fillStyle = 'rgba(10,12,24,0.88)';
ctx.fillRect(x - 8, y - 8, barW + 16, barH + 42);
ctx.strokeStyle = '#ff3b6b'; ctx.lineWidth = 2;
ctx.shadowColor = '#ff3b6b'; ctx.shadowBlur = 18;
ctx.strokeRect(x - 8, y - 8, barW + 16, barH + 42);
ctx.shadowBlur = 0;
ctx.textAlign='center'; ctx.textBaseline='bottom';
ctx.font='bold 15px Consolas, monospace';
ctx.fillStyle='#ff5577'; ctx.shadowColor='#ff3b6b'; ctx.shadowBlur=12;
ctx.fillText('☠ КУБИЧЕСКАЯ ЗМЕЯ ☠', W/2, y + 2);
ctx.shadowBlur=0;
ctx.fillStyle='#151a30';
ctx.fillRect(x, y + 10, barW, barH);
const pct = clamp(this.boss.hp / this.boss.maxHp, 0, 1);
const grad = ctx.createLinearGradient(x, 0, x + barW, 0);
grad.addColorStop(0, '#ff3b6b');
grad.addColorStop(0.5, '#ff5577');
grad.addColorStop(1, '#b06bff');
ctx.fillStyle = grad;
ctx.shadowColor = '#ff3b6b'; ctx.shadowBlur = 14;
ctx.fillRect(x, y + 10, barW * pct, barH);
ctx.shadowBlur = 0;
ctx.strokeStyle = 'rgba(255,255,255,0.25)';
ctx.lineWidth = 1;
ctx.strokeRect(x + 0.5, y + 10.5, barW - 1, barH - 1);
ctx.fillStyle = '#ffffff';
ctx.font='bold 14px Consolas, monospace';
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText(Math.ceil(this.boss.hp) + ' / ' + this.boss.maxHp, W/2, y + 10 + barH/2 + 1);
ctx.restore();
}
drawHUD(ctx){
const p=this.player;
if(!p) return;
ctx.save();
ctx.textBaseline='middle';
const infoW = 240;
ctx.font='bold 17px Consolas, monospace';
ctx.textAlign='left';
ctx.fillStyle='rgba(10,12,24,0.72)';
ctx.fillRect(18,18,infoW,124);
ctx.strokeStyle=this.difficulty.color;
ctx.globalAlpha = 0.6; ctx.lineWidth=1.5;
ctx.strokeRect(18,18,infoW,124);
ctx.globalAlpha = 1;
ctx.fillStyle= this.isBossRoom() ? '#ff5577' : '#7df9ff';
ctx.fillText('КОМНАТА ' + this.room + (this.isBossRoom() ? ' ☠' : ''), 32, 42);
ctx.fillStyle='#ff5577';
ctx.fillText('УБИЙСТВА ' + this.kills, 32, 66);
ctx.fillStyle='#ffd166';
ctx.fillText('ОЧКИ ' + this.score, 32, 90);
ctx.font='bold 14px Consolas, monospace';
ctx.fillStyle = this.difficulty.color;
ctx.fillText(this.difficulty.name, 32, 116);
ctx.fillStyle = 'rgba(180,200,240,0.85)';
const respText = this.difficulty.respawns >= 99 ? '∞ возрождений' : (this.respawnsLeft + ' возр.');
ctx.fillText('· ' + respText, 32 + 88, 116);
this.drawAbilityIndicator(ctx, 42, CONFIG.H - 100, 'D', 'РЫВОК',
p.dashReady ? 1 : (1 - p.dashCooldownTimer / (CONFIG.PLAYER.dash.cooldown + CONFIG.PLAYER.dash.duration)),
p.dashReady ? '#7df9ff' : 'rgba(120,150,200,0.6)');
this.drawAbilityIndicator(ctx, 92, CONFIG.H - 100, 'R', 'ЯРОСТЬ',
p.rageActive ? 1 : (p.rageCooldownTimer > 0 ? 1 - p.rageCooldownTimer / CONFIG.PLAYER.rage.cooldown : 1),
p.rageActive ? '#ff5577' : (p.rageReady ? '#ffcc66' : 'rgba(180,120,140,0.6)'),
p.rageActive);
const hbW = 300, hbH = 26;
const hbX = 24, hbY = CONFIG.H-62;
ctx.fillStyle='rgba(10,12,24,0.75)';
ctx.fillRect(hbX-6,hbY-6,hbW+12,hbH+12);
ctx.strokeStyle='rgba(80,140,255,0.5)';
ctx.strokeRect(hbX-6,hbY-6,hbW+12,hbH+12);
const hpPct=clamp(p.hp/p.maxHp,0,1);
ctx.fillStyle='#151a30';
ctx.fillRect(hbX,hbY,hbW,hbH);
const grad=ctx.createLinearGradient(hbX,0,hbX+hbW,0);
if(hpPct>0.5){ grad.addColorStop(0,'#00e5a0'); grad.addColorStop(1,'#7df9ff'); }
else if(hpPct>0.25){ grad.addColorStop(0,'#ffcc00'); grad.addColorStop(1,'#ff9f1c'); }
else { grad.addColorStop(0,'#ff3355'); grad.addColorStop(1,'#ff6688'); }
ctx.fillStyle=grad;
ctx.shadowColor=hpPct>0.25?'#00e5a0':'#ff3355';
ctx.shadowBlur=12;
ctx.fillRect(hbX,hbY,hbW*hpPct,hbH);
ctx.shadowBlur=0;
ctx.fillStyle='#ffffff';
ctx.font='bold 16px Consolas, monospace';
ctx.textAlign='center';
ctx.fillText('HP '+Math.ceil(p.hp)+' / '+p.maxHp, hbX+hbW/2, hbY+hbH/2+1);
const amW=220, amH=62;
const amX=CONFIG.W-24, amY=CONFIG.H-62;
ctx.fillStyle='rgba(10,12,24,0.75)';
ctx.fillRect(amX-amW, amY-6, amW, amH);
ctx.strokeStyle='rgba(80,140,255,0.5)';
ctx.lineWidth=1.5;
ctx.strokeRect(amX-amW, amY-6, amW, amH);
ctx.textAlign='right';
if(p.reloading){
ctx.fillStyle='#ffcc00';
ctx.font='bold 18px Consolas, monospace';
ctx.fillText('ПЕРЕЗАРЯДКА', amX-14, amY+16);
const rp=1-p.reloadTimer/(CONFIG.PLAYER.reloadTime*p.reloadMult);
ctx.fillStyle='#22283d';
ctx.fillRect(amX-amW+12, amY+32, amW-24, 12);
ctx.fillStyle='#ffcc00';
ctx.shadowColor='#ffcc00'; ctx.shadowBlur=10;
ctx.fillRect(amX-amW+12, amY+32, (amW-24)*rp, 12);
ctx.shadowBlur=0;
} else {
const low = !this.infiniteAmmo && p.mag<=6;
ctx.fillStyle= this.infiniteAmmo ? '#00e5a0' : (low?'#ff3355':'#7df9ff');
ctx.font='bold 32px Consolas, monospace';
ctx.fillText(this.infiniteAmmo ? '∞' : p.mag, amX-82, amY+26);
ctx.fillStyle='rgba(180,200,255,0.55)';
ctx.font='bold 18px Consolas, monospace';
ctx.fillText('/ '+p.maxMag, amX-14, amY+28);
if(!this.isTouch){
ctx.fillStyle='rgba(160,180,220,0.7)';
ctx.font='12px Consolas, monospace';
ctx.fillText('R — перезарядка · Space — рывок · Q — ярость', amX-14, amY+50);
}
}
if(this.drones.length>0){
ctx.textAlign='left';
ctx.fillStyle='rgba(125,249,255,0.9)';
ctx.font='bold 14px Consolas, monospace';
ctx.fillText('DRONES ×'+this.drones.length, 24, CONFIG.H-130);
}
if(this.state==='playing' && this.roomActive){
ctx.textAlign='right';
ctx.font='bold 16px Consolas, monospace';
if(this.isBossRoom() && this.boss && this.boss.alive){
ctx.fillStyle='rgba(255,120,150,'+(0.75+Math.sin(performance.now()/180)*0.25)+')';
ctx.fillText('БОСС АКТИВЕН', CONFIG.W-24, CONFIG.H-100);
} else if(!this.isBossRoom()){
ctx.fillStyle='rgba(255,120,150,'+(0.75+Math.sin(performance.now()/180)*0.25)+')';
ctx.fillText('ВРАГИ '+this.bots.length+' / '+this.toSpawn, CONFIG.W-24, CONFIG.H-100);
}
}
ctx.restore();
}
drawAbilityIndicator(ctx, cx, cy, letter, label, progress, color, active=false){
const r = 18;
ctx.save();
ctx.fillStyle = 'rgba(10,12,24,0.85)';
ctx.beginPath(); ctx.arc(cx, cy, r, 0, TAU); ctx.fill();
ctx.strokeStyle = color; ctx.lineWidth = 2;
ctx.shadowColor = active ? '#ff5577' : color;
ctx.shadowBlur = active ? 16 : 6;
ctx.beginPath(); ctx.arc(cx, cy, r, 0, TAU); ctx.stroke();
ctx.shadowBlur = 0;
if(progress < 1){
ctx.strokeStyle = 'rgba(80,100,140,0.9)';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(cx, cy, r - 3, -Math.PI/2, -Math.PI/2 + TAU * progress);
ctx.stroke();
}
ctx.fillStyle = color;
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.font = 'bold 15px Consolas, monospace';
ctx.fillText(letter, cx, cy + 1);
ctx.fillStyle = 'rgba(170,190,230,0.75)';
ctx.font = 'bold 9px Consolas, monospace';
ctx.fillText(label, cx, cy + r + 8);
ctx.restore();
}
drawMinimap(ctx){
const mw = 180, mh = 130;
const mx = CONFIG.W - mw - 20, my = 20;
const sx = mw/CONFIG.WORLD_W, sy = mh/CONFIG.WORLD_H;
ctx.save();
ctx.fillStyle='rgba(8,10,22,0.82)';
ctx.fillRect(mx,my,mw,mh);
ctx.strokeStyle='rgba(80,140,255,0.6)';
ctx.lineWidth=1.5;
ctx.shadowColor='rgba(80,140,255,0.7)'; ctx.shadowBlur=12;
ctx.strokeRect(mx,my,mw,mh);
ctx.shadowBlur=0;
ctx.fillStyle='rgba(59,107,255,0.45)';
for(const w of this.walls){
ctx.fillRect(mx+w.x*sx, my+w.y*sy, Math.max(1,w.w*sx), Math.max(1,w.h*sy));
}
for(const b of this.bots){
ctx.fillStyle = b.isSapper ? '#ffcc00' : b.cfg.color;
ctx.beginPath(); ctx.arc(mx+b.x*sx, my+b.y*sy, b.isSapper ? 3.5 : 2.5, 0, TAU); ctx.fill();
}
if(this.boss && this.boss.alive){
ctx.fillStyle='#ff3b6b';
ctx.shadowColor='#ff3b6b'; ctx.shadowBlur=10;
ctx.beginPath(); ctx.arc(mx+this.boss.x*sx, my+this.boss.y*sy, 5, 0, TAU); ctx.fill();
for(const seg of this.boss.segments){
ctx.fillStyle='#b06bff';
ctx.beginPath(); ctx.arc(mx+seg.x*sx, my+seg.y*sy, 2.5, 0, TAU); ctx.fill();
}
ctx.shadowBlur=0;
}
if(this.player && this.player.alive){
ctx.fillStyle='#22e6ff';
ctx.shadowColor='#22e6ff'; ctx.shadowBlur=8;
ctx.beginPath(); ctx.arc(mx+this.player.x*sx, my+this.player.y*sy, 3.5, 0, TAU); ctx.fill();
ctx.shadowBlur=0;
}
ctx.fillStyle='rgba(150,180,230,0.75)';
ctx.font='10px Consolas, monospace';
ctx.textAlign='left'; ctx.textBaseline='top';
ctx.fillText('КАРТА', mx+5, my+4);
ctx.restore();
}
drawTouchControls(ctx){
if(!this.isTouch) return;
if(this.state==='playing'){
if(!this.moveStick.active)
this.drawJoystickHint(ctx, 150, CONFIG.H-140, 'ДВИЖЕНИЕ', '#7df9ff');
if(!this.aimStick.active)
this.drawJoystickHint(ctx, CONFIG.W-150, CONFIG.H-140, 'ПРИЦЕЛ / ОГОНЬ', '#ff3b6b');
this.drawActionButton(ctx, this.reloadBtn, 'RELOAD', '#7df9ff',
this.player && this.player.reloading,
this.player && this.player.reloading ? this.player.reloadTimer/(CONFIG.PLAYER.reloadTime*this.player.reloadMult) : 0);
this.drawActionButton(ctx, this.dashBtn, 'DASH', '#7df9ff',
!this.player || !this.player.dashReady,
this.player && this.player.dashCooldownTimer > 0 ? this.player.dashCooldownTimer/(CONFIG.PLAYER.dash.cooldown+CONFIG.PLAYER.dash.duration) : 0);
this.drawActionButton(ctx, this.rageBtn, 'RAGE', '#ff5577',
!this.player || !this.player.rageReady,
this.player && this.player.rageCooldownTimer > 0 ? this.player.rageCooldownTimer/CONFIG.PLAYER.rage.cooldown : 0,
this.player && this.player.rageActive);
this.drawActionButton(ctx, this.pauseBtn, 'PAUSE', '#a06bff', false, 0, false);
}
this.moveStick.draw(ctx);
this.aimStick.draw(ctx);
}
drawJoystickHint(ctx,cx,cy,label,color){
ctx.save();
ctx.globalAlpha=0.16;
ctx.strokeStyle=color; ctx.lineWidth=2;
ctx.beginPath(); ctx.arc(cx,cy,48,0,TAU); ctx.stroke();
ctx.globalAlpha=0.32;
ctx.fillStyle=color;
ctx.font='bold 11px Consolas, monospace';
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText(label, cx, cy+66);
ctx.restore();
}
drawActionButton(ctx, r, label, color, onCooldown, cooldownPct, active=false){
const cx = r.x + r.w/2;
const cy = r.y + r.h/2;
const rad = Math.min(r.w, r.h)/2;
ctx.save();
ctx.fillStyle = active ? 'rgba(60,15,30,0.9)' : (onCooldown ? 'rgba(20,22,35,0.85)' : 'rgba(14,20,40,0.85)');
ctx.beginPath(); ctx.arc(cx,cy,rad,0,TAU); ctx.fill();
const drawColor = active ? '#ff5577' : (onCooldown ? 'rgba(120,140,180,0.6)' : color);
ctx.strokeStyle = drawColor;
ctx.lineWidth = 2.5;
ctx.shadowColor = drawColor;
ctx.shadowBlur = active ? 22 : (onCooldown ? 4 : 10);
ctx.beginPath(); ctx.arc(cx,cy,rad,0,TAU); ctx.stroke();
ctx.shadowBlur = 0;
if(onCooldown && cooldownPct > 0){
ctx.strokeStyle = 'rgba(200,220,255,0.75)';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(cx,cy,rad-4, -Math.PI/2, -Math.PI/2 + TAU * (1 - cooldownPct));
ctx.stroke();
}
ctx.fillStyle = drawColor;
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.font = 'bold 12px Consolas, monospace';
ctx.fillText(label, cx, cy);
ctx.restore();
}
drawMessage(ctx){
if(this.message.t<=0) return;
const m=this.message;
const p=m.t/m.max;
const alpha = p>0.75 ? (1-p)/0.25 : Math.min(1, p/0.35);
ctx.save();
ctx.globalAlpha=clamp(alpha,0,1);
ctx.textAlign='center'; ctx.textBaseline='middle';
if(m.text){
ctx.font='bold 54px Consolas, monospace';
ctx.fillStyle='#7df9ff';
ctx.shadowColor='#22e6ff'; ctx.shadowBlur=26;
ctx.fillText(m.text, CONFIG.W/2, CONFIG.H*0.30);
}
if(m.sub){
ctx.font='bold 20px Consolas, monospace';
ctx.fillStyle='#ffd166';
ctx.shadowColor='#ff9f1c'; ctx.shadowBlur=16;
ctx.fillText(m.sub, CONFIG.W/2, CONFIG.H*0.30 + (m.text?52:0));
}
ctx.restore();
}
drawMenu(ctx){
const cx=CONFIG.W/2;
ctx.save();
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.font='bold ' + Math.min(76, CONFIG.W*0.06) + 'px Consolas, monospace';
ctx.fillStyle='#7df9ff';
ctx.shadowColor='#22e6ff'; ctx.shadowBlur=40;
ctx.fillText('NEON SHOOTER', cx, CONFIG.H*0.10);
ctx.font='bold 18px Consolas, monospace';
ctx.fillStyle='#ff3b6b';
ctx.shadowColor='#ff3b6b'; ctx.shadowBlur=20;
ctx.fillText('БОСС КАЖДЫЕ 5 КОМНАТ · ПОДРЫВНИКИ · РЫВОК · ЯРОСТЬ', cx, CONFIG.H*0.17);
ctx.shadowBlur=0;
if(this.highScore > 0){
ctx.font='bold 16px Consolas, monospace';
ctx.fillStyle='#ffd166';
ctx.fillText('🏆 Рекорд: ' + this.highScore + ' · Всего убийств: ' + this.totalKills, cx, CONFIG.H*0.215);
}
ctx.font='bold 20px Consolas, monospace';
ctx.fillStyle='rgba(180,210,255,0.95)';
ctx.fillText('ВЫБЕРИ СЛОЖНОСТЬ', cx, CONFIG.H*0.28);
ctx.font='15px Consolas, monospace';
ctx.fillStyle='rgba(160,185,225,0.8)';
const controls=this.isTouch?[
'Левый джойстик — движение · Правый — прицел и огонь',
'Кнопки DASH, RAGE, RELOAD, PAUSE — справа снизу'
]:[
'WASD (или ЦФЫВ) / Стрелки — движение · Мышь + ЛКМ — огонь',
'R — перезарядка · Space — РЫВОК · Q — ЯРОСТЬ · Shift — спринт',
'Клавиши 1–4 — сложность · M — звук · F — полный экран'
];
controls.forEach((line,i)=>{
ctx.fillText(line, cx, CONFIG.H*0.32 + i*22);
});
ctx.restore();
for(const btn of this.difficultyBtns){
const diff = DIFFICULTIES[btn.key];
const selected = this.selectedDifficulty === btn.key;
this.drawDifficultyButton(ctx, btn, diff, selected);
}
this.drawButton(ctx, this.startBtn, 'START', '#22e6ff');
this.drawButton(ctx, this.fullscreenBtn, '⛶', '#7df9ff');
this.drawButton(ctx, this.soundBtn, sound.enabled ? '🔊' : '🔇', sound.enabled ? '#7df9ff' : '#555');
this.drawButtonCustom(ctx, this.settingsBtn, '⚙ НАСТРОЙКИ', '#a06bff', 20);
ctx.save();
ctx.textAlign='center';
ctx.fillStyle='rgba(140,170,220,0.55)';
ctx.font='13px Consolas, monospace';
ctx.fillText(this.isTouch?'Нажми на сложность, затем START':'или нажми ENTER · 1-4 — сложность · M — звук',
cx, this.settingsBtn.y + this.settingsBtn.h + 16);
ctx.restore();
}
drawDifficultyButton(ctx, r, diff, selected){
ctx.save();
ctx.fillStyle = selected ? 'rgba(20,40,80,0.95)' : 'rgba(12,16,32,0.85)';
ctx.fillRect(r.x, r.y, r.w, r.h);
ctx.strokeStyle = diff.color;
ctx.lineWidth = selected ? 3 : 1.8;
ctx.shadowColor = diff.color;
ctx.shadowBlur = selected ? 22 : 8;
ctx.strokeRect(r.x, r.y, r.w, r.h);
ctx.shadowBlur = 0;
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.font = 'bold ' + Math.min(22, r.w*0.14) + 'px Consolas, monospace';
ctx.fillStyle = diff.color;
ctx.shadowColor = diff.color;
ctx.shadowBlur = selected ? 16 : 6;
ctx.fillText(diff.name, r.x + r.w/2, r.y + r.h*0.30);
ctx.shadowBlur = 0;
ctx.font = Math.min(10, r.w*0.065) + 'px Consolas, monospace';
ctx.fillStyle = 'rgba(200,215,240,0.85)';
ctx.fillText(diff.desc, r.x + r.w/2, r.y + r.h*0.68);
if(selected){
ctx.fillStyle = diff.color;
ctx.beginPath(); ctx.arc(r.x + r.w - 12, r.y + 12, 5, 0, TAU); ctx.fill();
}
ctx.restore();
}
drawButton(ctx,r,label,color){
const m=this.mouse;
const hover=!this.isTouch && this.inRect(m,r);
ctx.save();
ctx.fillStyle=hover?'rgba(30,50,90,0.92)':'rgba(14,20,40,0.88)';
ctx.fillRect(r.x,r.y,r.w,r.h);
ctx.strokeStyle=color;
ctx.lineWidth=hover?3:2;
ctx.shadowColor=color;
ctx.shadowBlur=hover?26:14;
ctx.strokeRect(r.x,r.y,r.w,r.h);
ctx.shadowBlur=0;
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.font='bold 24px Consolas, monospace';
ctx.fillStyle=color;
ctx.shadowColor=color;
ctx.shadowBlur=hover?18:8;
ctx.fillText(label, r.x+r.w/2, r.y+r.h/2+2);
ctx.restore();
}
drawButtonCustom(ctx, r, label, color, fontSize){
const m=this.mouse;
const hover=!this.isTouch && this.inRect(m,r);
ctx.save();
ctx.fillStyle=hover?'rgba(30,50,90,0.92)':'rgba(14,20,40,0.88)';
ctx.fillRect(r.x,r.y,r.w,r.h);
ctx.strokeStyle=color;
ctx.lineWidth=hover?2.5:1.6;
ctx.shadowColor=color;
ctx.shadowBlur=hover?20:10;
ctx.strokeRect(r.x,r.y,r.w,r.h);
ctx.shadowBlur=0;
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.font='bold ' + fontSize + 'px Consolas, monospace';
ctx.fillStyle=color;
ctx.shadowColor=color;
ctx.shadowBlur=hover?14:6;
ctx.fillText(label, r.x+r.w/2, r.y+r.h/2+1);
ctx.restore();
}
drawSettings(ctx){
const W = CONFIG.W, H = CONFIG.H;
ctx.save();
ctx.fillStyle='rgba(4,6,18,0.94)';
ctx.fillRect(0,0,W,H);
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.font='bold '+Math.min(52, W*0.04)+'px Consolas, monospace';
ctx.fillStyle='#a06bff';
ctx.shadowColor='#a06bff'; ctx.shadowBlur=30;
ctx.fillText('НАСТРОЙКИ', W/2, H*0.10);
ctx.shadowBlur=0;
ctx.restore();
if(this.showCheatList){ this.drawCheatListScreen(ctx); return; }
ctx.save();
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.font='13px Consolas, monospace';
ctx.fillStyle='rgba(160,180,220,0.6)';
ctx.fillText(this.settingsReturnState === 'paused' ? '(вернёшься в паузу)' : '(вернёшься в меню)', W/2, H*0.135);
ctx.restore();
ctx.save();
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.font='bold 16px Consolas, monospace';
ctx.fillStyle='rgba(180,200,240,0.85)';
ctx.fillText('ЗВУК', W/2, this.settingsSoundBtn.y - 20);
ctx.restore();
const soundLabel = sound.enabled ? '🔊 ВКЛ' : '🔇 ВЫКЛ';
const soundColor = sound.enabled ? '#7df9ff' : '#777';
this.drawButtonCustom(ctx, this.settingsSoundBtn, soundLabel, soundColor, 22);
ctx.save();
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.font='bold 16px Consolas, monospace';
ctx.fillStyle='rgba(180,200,240,0.85)';
ctx.fillText('ГРОМКОСТЬ: ' + Math.round(sound.volume * 100) + '%', W/2, this.settingsVolumeDownBtn.y - 18);
ctx.restore();
this.drawButtonCustom(ctx, this.settingsVolumeDownBtn, '−', '#7df9ff', 32);
this.drawButtonCustom(ctx, this.settingsVolumeUpBtn, '+', '#7df9ff', 32);
const barX = this.settingsVolumeDownBtn.x + this.settingsVolumeDownBtn.w + 14;
const barY = this.settingsVolumeDownBtn.y + 16;
const barW = this.settingsVolumeUpBtn.x - barX - 14;
const barH = this.settingsVolumeDownBtn.h - 32;
ctx.save();
ctx.fillStyle='rgba(10,14,28,0.9)';
ctx.fillRect(barX, barY, barW, barH);
ctx.strokeStyle='rgba(125,249,255,0.6)';
ctx.lineWidth=1.5;
ctx.strokeRect(barX+0.5, barY+0.5, barW-1, barH-1);
ctx.fillStyle='#7df9ff';
ctx.shadowColor='#7df9ff'; ctx.shadowBlur=12;
ctx.fillRect(barX+1, barY+1, (barW-2) * sound.volume, barH-2);
ctx.shadowBlur=0;
for(let i=1;i<10;i++){
const x = barX + barW * (i/10);
ctx.strokeStyle='rgba(0,0,0,0.35)';
ctx.lineWidth=1;
ctx.beginPath(); ctx.moveTo(x, barY); ctx.lineTo(x, barY+barH); ctx.stroke();
}
ctx.restore();
ctx.save();
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.font='bold 16px Consolas, monospace';
ctx.fillStyle='#ffcc00';
ctx.fillText('ЧИТ-КОД', W/2, this.settingsCheatInputRect.y - 18);
ctx.restore();
ctx.save();
ctx.strokeStyle = (document.activeElement === this.inputEl) ? '#ffffff' : 'rgba(125,249,255,0.6)';
ctx.lineWidth=2;
ctx.shadowColor='#7df9ff';
ctx.shadowBlur=(document.activeElement === this.inputEl) ? 22 : 8;
ctx.strokeRect(this.settingsCheatInputRect.x, this.settingsCheatInputRect.y,
this.settingsCheatInputRect.w, this.settingsCheatInputRect.h);
ctx.shadowBlur=0;
ctx.restore();
this.drawButtonCustom(ctx, this.settingsCheatActivateBtn, 'АКТИВИРОВАТЬ', '#00e5a0', 18);
if(this.settingsMessage && performance.now() < this.settingsMessageUntil){
const remaining = (this.settingsMessageUntil - performance.now()) / 2800;
const alpha = Math.min(1, remaining * 3);
ctx.save();
ctx.globalAlpha = alpha;
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.font='bold 17px Consolas, monospace';
ctx.fillStyle = this.settingsMessageColor;
ctx.shadowColor = this.settingsMessageColor;
ctx.shadowBlur=14;
ctx.fillText(this.settingsMessage, W/2, this.settingsCheatActivateBtn.y + this.settingsCheatActivateBtn.h + 24);
ctx.restore();
}
this.drawButtonCustom(ctx, this.settingsShowCodesBtn, 'СПИСОК КОДОВ', '#ff44aa', 14);
this.drawButtonCustom(ctx, this.settingsResetCheatsBtn, 'СБРОС ЧИТОВ', '#ff9f1c', 14);
const active = [];
if(this.godMode) active.push('GOD');
if(this.infiniteAmmo) active.push('AMMO∞');
if(this.cheatDamageMult > 1) active.push('DMG×' + this.cheatDamageMult);
if(this.cheatSpeedMult > 1) active.push('SPD×' + this.cheatSpeedMult);
if(this.pending.droneCount > 0) active.push('DRONES+' + this.pending.droneCount);
if(this.pending.maxUpgrades) active.push('MAXUPG');
if(active.length){
ctx.save();
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.font='bold 14px Consolas, monospace';
ctx.fillStyle='rgba(255,80,170,0.95)';
ctx.shadowColor='#ff44aa'; ctx.shadowBlur=12;
ctx.fillText('Активны: ' + active.join(' · '), W/2, this.settingsResetCheatsBtn.y + this.settingsResetCheatsBtn.h + 26);
ctx.restore();
}
this.drawButtonCustom(ctx, this.settingsBackBtn, '← НАЗАД', '#7df9ff', 24);
}
drawCheatListScreen(ctx){
const W = CONFIG.W, H = CONFIG.H;
ctx.save();
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.font='bold '+Math.min(38, W*0.03)+'px Consolas, monospace';
ctx.fillStyle='#ff44aa';
ctx.shadowColor='#ff44aa'; ctx.shadowBlur=24;
ctx.fillText('СПИСОК КОДОВ', W/2, H*0.10);
ctx.shadowBlur=0;
ctx.restore();
const total = CHEAT_CODES.length;
const colCount = W >= 700 ? 2 : 1;
const perCol = Math.ceil(total / colCount);
const colW = Math.min(420, (W - 120) / colCount);
const startX = W/2 - (colCount * colW) / 2;
const startY = H * 0.16;
const rowH = Math.min(38, (H * 0.62) / perCol);
ctx.save();
ctx.textBaseline='middle';
for(let i = 0; i < total; i++){
const col = Math.floor(i / perCol);
const row = i % perCol;
const x = startX + col * colW;
const y = startY + row * rowH;
const c = CHEAT_CODES[i];
ctx.fillStyle = (i % 2 === 0) ? 'rgba(20,30,55,0.55)' : 'rgba(14,20,40,0.55)';
ctx.fillRect(x + 10, y - rowH*0.45, colW - 20, rowH*0.9);
ctx.textAlign='left';
ctx.font='bold 14px Consolas, monospace';
ctx.fillStyle='#7df9ff';
ctx.shadowColor='#7df9ff';
ctx.shadowBlur=6;
ctx.fillText(c.code, x + 20, y);
ctx.textAlign='right';
ctx.font='13px Consolas, monospace';
ctx.fillStyle='rgba(200,220,255,0.85)';
ctx.shadowBlur=0;
ctx.fillText(c.desc, x + colW - 20, y);
}
ctx.restore();
this.drawButtonCustom(ctx, this.settingsBackBtn, '← НАЗАД', '#7df9ff', 24);
}
drawPause(ctx){
ctx.save();
ctx.fillStyle='rgba(4,6,14,0.82)';
ctx.fillRect(0,0,CONFIG.W,CONFIG.H);
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.font='bold 68px Consolas, monospace';
ctx.fillStyle='#7df9ff';
ctx.shadowColor='#22e6ff'; ctx.shadowBlur=30;
ctx.fillText('ПАУЗА', CONFIG.W/2, CONFIG.H*0.26);
ctx.shadowBlur=0;
ctx.font='18px Consolas, monospace';
ctx.fillStyle='rgba(170,195,240,0.75)';
ctx.fillText(this.isTouch ? 'Управление приостановлено' : 'Esc — продолжить игру', CONFIG.W/2, CONFIG.H*0.33);
ctx.restore();
this.drawButtonCustom(ctx, this.resumeBtn, 'ПРОДОЛЖИТЬ', '#22e6ff', 24);
const soundLabel = sound.enabled ? '🔊 ВКЛ' : '🔇 ВЫКЛ';
const soundColor = sound.enabled ? '#7df9ff' : '#777';
this.drawButtonCustom(ctx, this.pauseSoundBtn, soundLabel, soundColor, 16);
this.drawButtonCustom(ctx, this.pauseSettingsBtn, '⚙ НАСТРОЙКИ', '#a06bff', 16);
this.drawButtonCustom(ctx, this.pauseMenuBtn, 'В МЕНЮ', '#ff5577', 16);
ctx.save();
ctx.textAlign='center'; ctx.textBaseline='top';
ctx.font='13px Consolas, monospace';
ctx.fillStyle='rgba(140,170,220,0.55)';
ctx.fillText(this.isTouch ? 'Нажми кнопку, чтобы продолжить' : 'M — звук · Esc — вернуться в игру',
CONFIG.W/2, this.pauseMenuBtn.y + this.pauseMenuBtn.h + 22);
ctx.restore();
}
drawGameOver(ctx){
ctx.save();
ctx.fillStyle='rgba(12,2,8,0.88)';
ctx.fillRect(0,0,CONFIG.W,CONFIG.H);
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.font='bold 76px Consolas, monospace';
ctx.fillStyle='#ff3355';
ctx.shadowColor='#ff3355'; ctx.shadowBlur=36;
ctx.fillText('GAME OVER', CONFIG.W/2, CONFIG.H*0.18);
ctx.shadowBlur=0;
ctx.font='bold 20px Consolas, monospace';
ctx.fillStyle = this.difficulty.color;
ctx.fillText('СЛОЖНОСТЬ: ' + this.difficulty.name, CONFIG.W/2, CONFIG.H*0.28);
ctx.font='bold 24px Consolas, monospace';
ctx.fillStyle='#ffd166';
ctx.fillText('ИТОГОВАЯ СТАТИСТИКА', CONFIG.W/2, CONFIG.H*0.35);
const stats=[
['Комнат пройдено', this.room > 0 ? (this.room - 1) : 0],
['Убийств', this.kills],
['Очков', this.score],
['Время', this.elapsed.toFixed(1)+' сек'],
['🏆 Рекорд', this.highScore]
];
ctx.font='20px Consolas, monospace';
stats.forEach((s,i)=>{
const y=CONFIG.H*0.41 + i*30;
ctx.textAlign='right';
ctx.fillStyle='rgba(170,195,240,0.85)';
ctx.fillText(s[0], CONFIG.W/2-20, y);
ctx.textAlign='left';
ctx.fillStyle='#7df9ff';
ctx.fillText(''+s[1], CONFIG.W/2+20, y);
});
ctx.restore();
this.drawButton(ctx, this.restartBtn, 'В МЕНЮ', '#ff3b6b');
ctx.save();
ctx.textAlign='center';
ctx.fillStyle='rgba(140,170,220,0.55)';
ctx.font='13px Consolas, monospace';
ctx.fillText(this.isTouch?'или нажми кнопку':'или нажми ENTER (вернёт в меню)',
CONFIG.W/2, this.restartBtn.y + this.restartBtn.h + 24);
ctx.restore();
}
drawUpgradeScreen(ctx){
const W=CONFIG.W, H=CONFIG.H;
ctx.save();
ctx.fillStyle='rgba(4,6,18,0.90)';
ctx.fillRect(0,0,W,H);
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.font='bold '+Math.min(54, W*0.042)+'px Consolas, monospace';
ctx.fillStyle='#7df9ff';
ctx.shadowColor='#22e6ff'; ctx.shadowBlur=30;
ctx.fillText('УЛУЧШЕНИЯ', W/2, H*0.10);
ctx.shadowBlur=0;
ctx.font='bold 20px Consolas, monospace';
ctx.fillStyle='#ffd166';
ctx.fillText('Комната ' + this.room + ' зачищена • Очки: ' + this.score, W/2, H*0.165);
const nextRoom = this.room + 1;
const isBossNext = nextRoom % CONFIG.BOSS_EVERY === 0;
const nextBotCount = this.difficulty.botCount + (nextRoom - 1) * this.difficulty.botGrowth;
const hpMul = (1 + (nextRoom-1)*CONFIG.ROOM_HP_SCALE) * this.difficulty.botHpMult;
const dmgMul = (1 + (nextRoom-1)*CONFIG.ROOM_DMG_SCALE) * this.difficulty.botDmgMult;
ctx.font='bold 15px Consolas, monospace';
if(isBossNext){
ctx.fillStyle='rgba(255,80,120,'+(0.8+Math.sin(performance.now()/180)*0.2)+')';
ctx.shadowColor='#ff3b6b';
ctx.shadowBlur=18;
ctx.fillText('⚠ СЛЕДУЮЩАЯ КОМНАТА — БОСС: КУБИЧЕСКАЯ ЗМЕЯ ⚠', W/2, H*0.205);
ctx.shadowBlur=0;
} else {
ctx.fillStyle='rgba(255,140,160,'+(0.8+Math.sin(performance.now()/200)*0.15)+')';
ctx.fillText('Следующая комната: '+nextBotCount+' врагов · HP ×'+hpMul.toFixed(2)+' · Урон ×'+dmgMul.toFixed(2),
W/2, H*0.205);
}
ctx.restore();
for(const card of this.upgradeCards){
const up=card.upgrade;
const cost=this.getUpgradeCost(up);
const canBuy=this.score>=cost;
const lvl=this.upgradeCounts[up.id]||0;
ctx.save();
ctx.fillStyle = canBuy ? 'rgba(16,26,50,0.94)' : 'rgba(14,14,22,0.75)';
ctx.fillRect(card.x, card.y, card.w, card.h);
const borderCol = lvl >= 100 ? '#ffd166' : (canBuy ? '#7df9ff' : 'rgba(90,100,130,0.55)');
ctx.strokeStyle = borderCol;
ctx.lineWidth = lvl >= 100 ? 3 : 2;
ctx.shadowColor = lvl >= 100 ? '#ffd166' : (canBuy ? '#22e6ff' : 'transparent');
ctx.shadowBlur = lvl >= 100 ? 20 : (canBuy ? 12 : 0);
ctx.strokeRect(card.x, card.y, card.w, card.h);
ctx.shadowBlur = 0;
if(lvl>0){
ctx.fillStyle = lvl >= 100 ? '#ffd166' : (canBuy ? '#ffd166' : 'rgba(150,140,100,0.7)');
ctx.font = 'bold 12px Consolas, monospace';
ctx.textAlign = 'right';
ctx.textBaseline = 'top';
ctx.fillText('УР. '+lvl + (lvl >= 100 ? ' ★' : ''), card.x+card.w-8, card.y+7);
}
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.font = 'bold 18px Consolas, monospace';
ctx.fillStyle = canBuy ? '#ffffff' : 'rgba(200,200,200,0.7)';
ctx.fillText(up.name, card.x + card.w/2, card.y + 16);
ctx.font = '14px Consolas, monospace';
ctx.fillStyle = canBuy ? 'rgba(180,210,255,0.95)' : 'rgba(150,160,180,0.7)';
ctx.fillText(up.desc, card.x + card.w/2, card.y + 44);
ctx.font = 'bold 16px Consolas, monospace';
ctx.fillStyle = canBuy ? '#ffd166' : '#885555';
ctx.textBaseline = 'bottom';
ctx.fillText(cost + ' очк.', card.x + card.w/2, card.y + card.h - 10);
ctx.restore();
}
this.drawButton(ctx, this.upgradeContinueBtn, 'СЛЕДУЮЩАЯ КОМНАТА →', '#22e6ff');
ctx.save();
ctx.textAlign='center'; ctx.textBaseline='top';
ctx.font='13px Consolas, monospace';
ctx.fillStyle='rgba(140,170,220,0.55)';
ctx.fillText(this.isTouch ? 'Нажми карточку, чтобы купить улучшение'
: 'Кликни карточку для покупки · ENTER — продолжить',
W/2, H - 22);
ctx.restore();
}
}
/* =========================================================================
ЗАПУСК
========================================================================= */
const canvas = document.getElementById('game');
const game = new Game(canvas);
function resizeCanvas(){
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const screenW = window.innerWidth;
const screenH = window.innerHeight;
const LOGICAL_H = 720;
const LOGICAL_W = Math.max(640, Math.floor(LOGICAL_H * (screenW / screenH)));
CONFIG.W = LOGICAL_W;
CONFIG.H = LOGICAL_H;
canvas.width = Math.floor(LOGICAL_W * dpr);
canvas.height = Math.floor(LOGICAL_H * dpr);
canvas.style.width = screenW + 'px';
canvas.style.height = screenH + 'px';
game.dpr = dpr;
game.layoutUI();
if(game.player) game.recenterCamera();
game.positionCheatInput();
}
window.addEventListener('resize', resizeCanvas);
window.addEventListener('orientationchange', ()=>setTimeout(resizeCanvas,150));
resizeCanvas();
const cheatInputEl = document.getElementById('cheatInput');
cheatInputEl.addEventListener('focus', ()=>{ sound.init(); sound.resume(); });
cheatInputEl.addEventListener('keydown', (e)=>{
if(e.key === 'Escape') cheatInputEl.blur();
});
(async function boot(){
await initYandexSDK();
if(sdkPlayer){
const cloudData = await loadPlayerProgress();
if(cloudData){
if(cloudData.highScore && cloudData.highScore > game.highScore) game.highScore = cloudData.highScore;
if(cloudData.totalKills) game.totalKills = Math.max(game.totalKills, cloudData.totalKills);
if(cloudData.totalRooms) game.totalRooms = Math.max(game.totalRooms, cloudData.totalRooms);
game.saveLocalProgress();
}
}
notifyGameReady();
let lastTime = performance.now();
let accumulator = 0;
function loop(now){
requestAnimationFrame(loop);
let frameTime = (now - lastTime) / 1000;
lastTime = now;
if(frameTime > 0.25) frameTime = 0.25;
accumulator += frameTime;
let steps = 0;
while(accumulator >= CONFIG.FIXED_DT && steps < 5){
game.update(CONFIG.FIXED_DT);
accumulator -= CONFIG.FIXED_DT;
steps++;
}
game.render();
}
requestAnimationFrame(loop);
})();
</script>
</body>
</html>Game Source: NEON SHOOTER
Creator: PrismPilot23
Libraries: none
Complexity: complex (3647 lines, 144.6 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: neon-shooter-prismpilot23" to link back to the original. Then publish at arcadelab.ai/publish.