From 5489c0f8d5a7ac90e603b210fdc2832a330341b7 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Thu, 25 Jun 2026 09:05:44 +0500 Subject: [PATCH] feat: update waves, enemies --- js/config.js | 293 +++++++++++++++++++++++++++++++++++++++-------- js/enemy.js | 218 ++++++++++++++++++++++++++++------- js/game.js | 85 ++++++++++---- js/input.js | 11 +- js/projectile.js | 5 + js/utils.js | 30 +++-- 6 files changed, 518 insertions(+), 124 deletions(-) diff --git a/js/config.js b/js/config.js index fdac298..ae0bdb0 100644 --- a/js/config.js +++ b/js/config.js @@ -15,22 +15,27 @@ const CONFIG = { startLives: 10, startWave: 1, startScore: 0, - spawnIntervalBase: 240, - spawnIntervalMin: 60, - spawnIntervalDecayPerWave: 12, + enemiesPerWaveBase: 5, + enemiesPerWaveInc: 1, waveInterval: 900, heavyEnemyChanceBase: 0.25, heavyEnemyChancePerWave: 0.02, scoreSimple: 10, scoreHeavy: 50, + scoreBoss: 200, + bossEveryNthWave: 10, + bossLayers: 3, + bossWordsPerLayer: 3, gameOverDelay: 10000, }, speed: { - simpleEnemyCrossTime: 60, - simpleEnemyCrossTimeRand: 120, - simpleEnemyCrossTimeDecay: 4000, - heavyEnemyCrossTime: 120, - heavyEnemyCrossTimeRand: 240, + slowEnemyCrossTimeStart: 120, + slowEnemyCrossTimeCap: 20, + slowEnemyCrossTimeCapWave: 50, + fastEnemyCrossTimeStart: 60, + fastEnemyCrossTimeCap: 10, + fastEnemyCrossTimeCapWave: 50, + bossCrossTime: 60, projectileCrossTime: 1.3, gridSpeed: 0.5, }, @@ -114,6 +119,8 @@ const CONFIG = { primaryGrid: "rgba(0,255,65,0.08)", heavy: "#ffaa00", heavyDim: "#cc8800", + boss: "#ff4444", + bossDim: "#cc2222", miss: "#ff0044", white: "#ffffff", red: "#ff0000", @@ -151,48 +158,238 @@ const CONFIG = { const WordGenerator = { simpleWords: [ - "hack", "code", "data", "node", "link", "byte", "bit", "net", "web", "sys", - "run", "log", "bug", "fix", "api", "cpu", "ram", "ssd", "gpu", "dns", - "ip", "url", "tag", "css", "js", "sql", "git", "cli", "env", "dev", - "app", "bot", "ai", "io", "os", "ui", "ux", "vm", "vpn", "lan", - "wan", "ftp", "ssh", "http", "json", "xml", "yaml", "md", "py", "go", - "rust", "java", "php", "cpp", "ts", "rb", "kt", "swift", "dart", "lua", - "perl", "bash", "zsh", "fish", "vim", "nano", "emacs", "sed", "awk", "grep", - "find", "cat", "ls", "cd", "mv", "cp", "rm", "mkdir", "chmod", "chown", - "ping", "curl", "wget", "nc", "nmap", "tcp", "udp", "ip4", "ip6", "mac", - "hex", "bin", "dec", "oct", "xor", "and", "or", "not", "shl", "shr", - "key", "lock", "door", "gate", "wall", "fire", "ice", "wind", "storm", "rain", - "sun", "moon", "star", "void", "null", "zero", "one", "two", "ten", "max", + "hack", + "code", + "data", + "node", + "link", + "byte", + "bit", + "net", + "web", + "sys", + "run", + "log", + "bug", + "fix", + "api", + "cpu", + "ram", + "ssd", + "gpu", + "dns", + "ip", + "url", + "tag", + "css", + "js", + "sql", + "git", + "cli", + "env", + "dev", + "app", + "bot", + "ai", + "io", + "os", + "ui", + "ux", + "vm", + "vpn", + "lan", + "wan", + "ftp", + "ssh", + "http", + "json", + "xml", + "yaml", + "md", + "py", + "go", + "rust", + "java", + "php", + "cpp", + "ts", + "rb", + "kt", + "swift", + "dart", + "lua", + "perl", + "bash", + "zsh", + "fish", + "vim", + "nano", + "emacs", + "sed", + "awk", + "grep", + "find", + "cat", + "ls", + "cd", + "mv", + "cp", + "rm", + "mkdir", + "chmod", + "chown", + "ping", + "curl", + "wget", + "nc", + "nmap", + "tcp", + "udp", + "ip4", + "ip6", + "mac", + "hex", + "bin", + "dec", + "oct", + "xor", + "and", + "or", + "not", + "shl", + "shr", + "key", + "lock", + "door", + "gate", + "wall", + "fire", + "ice", + "wind", + "storm", + "rain", + "sun", + "moon", + "star", + "void", + "null", + "zero", + "one", + "two", + "ten", + "max", ], heavyWords: [ - ["root", "access"], ["data", "breach"], ["fire", "wall"], ["deep", "web"], - ["dark", "net"], ["cloud", "base"], ["neural", "net"], ["quantum", "bit"], - ["block", "chain"], ["smart", "contract"], ["machine", "learn"], ["deep", "fake"], - ["zero", "day"], ["back", "door"], ["side", "channel"], ["man", "middle"], - ["denial", "service"], ["brute", "force"], ["social", "engineer"], ["phishing", "attack"], - ["sql", "inject"], ["cross", "site"], ["buffer", "over"], ["heap", "spray"], - ["stack", "pivot"], ["return", "orient"], ["format", "string"], ["race", "condition"], - ["time", "check"], ["use", "after"], ["double", "free"], ["null", "pointer"], - ["integer", "over"], ["type", "confus"], ["memory", "leak"], ["dead", "lock"], - ["live", "lock"], ["starvation", "mode"], ["priority", "invert"], ["cache", "miss"], - ["branch", "mispred"], ["speculative", "exec"], ["meltdown", "flaw"], ["spectre", "bug"], - ["row", "hammer"], ["cold", "boot"], ["evil", "maid"], ["supply", "chain"], - ["hardware", "troj"], ["firmware", "root"], ["bios", "implant"], ["uefi", "shell"], - ["smm", "exploit"], ["ring", "zero"], ["kernel", "panic"], ["blue", "screen"], - ["kernel", "oops"], ["seg", "fault"], ["bus", "error"], ["illegal", "op"], - ["trap", "divide"], ["trap", "debug"], ["trap", "nmi"], ["trap", "break"], - ["trap", "overflow"], ["trap", "bound"], ["trap", "invalid"], ["trap", "device"], - ["trap", "double"], ["trap", "copro"], ["trap", "tss"], ["trap", "segment"], - ["trap", "stack"], ["trap", "general"], ["trap", "page"], ["trap", "x87"], - ["trap", "align"], ["trap", "machine"], ["trap", "simd"], ["trap", "virtual"], - ["security", "check"], ["stack", "cookie"], ["aslr", "bypass"], ["dep", "bypass"], - ["cfg", "bypass"], ["cet", "bypass"], ["shadow", "stack"], ["control", "flow"], - ["indirect", "call"], ["jump", "oriented"], ["call", "oriented"], ["data", "oriented"], - ["counterfeit", "obj"], ["use", "after"], ["heap", "feng"], ["house", "spirit"], - ["house", "lore"], ["house", "force"], ["fast", "bin"], ["tcache", "poison"], - ["unsorted", "bin"], ["large", "bin"], ["small", "bin"], ["mmap", "chunk"], - ["top", "chunk"], ["wilderness", "area"], ["arena", "corrupt"], ["thread", "cache"], - ["per", "thread"], ["main", "arena"], + ["root", "access"], + ["data", "breach"], + ["fire", "wall"], + ["deep", "web"], + ["dark", "net"], + ["cloud", "base"], + ["neural", "net"], + ["quantum", "bit"], + ["block", "chain"], + ["smart", "contract"], + ["machine", "learn"], + ["deep", "fake"], + ["zero", "day"], + ["back", "door"], + ["side", "channel"], + ["man", "middle"], + ["denial", "service"], + ["brute", "force"], + ["social", "engineer"], + ["phishing", "attack"], + ["sql", "inject"], + ["cross", "site"], + ["buffer", "over"], + ["heap", "spray"], + ["stack", "pivot"], + ["return", "orient"], + ["format", "string"], + ["race", "condition"], + ["time", "check"], + ["use", "after"], + ["double", "free"], + ["null", "pointer"], + ["integer", "over"], + ["type", "confus"], + ["memory", "leak"], + ["dead", "lock"], + ["live", "lock"], + ["starvation", "mode"], + ["priority", "invert"], + ["cache", "miss"], + ["branch", "mispred"], + ["speculative", "exec"], + ["meltdown", "flaw"], + ["spectre", "bug"], + ["row", "hammer"], + ["cold", "boot"], + ["evil", "maid"], + ["supply", "chain"], + ["hardware", "troj"], + ["firmware", "root"], + ["bios", "implant"], + ["uefi", "shell"], + ["smm", "exploit"], + ["ring", "zero"], + ["kernel", "panic"], + ["blue", "screen"], + ["kernel", "oops"], + ["seg", "fault"], + ["bus", "error"], + ["illegal", "op"], + ["trap", "divide"], + ["trap", "debug"], + ["trap", "nmi"], + ["trap", "break"], + ["trap", "overflow"], + ["trap", "bound"], + ["trap", "invalid"], + ["trap", "device"], + ["trap", "double"], + ["trap", "copro"], + ["trap", "tss"], + ["trap", "segment"], + ["trap", "stack"], + ["trap", "general"], + ["trap", "page"], + ["trap", "x87"], + ["trap", "align"], + ["trap", "machine"], + ["trap", "simd"], + ["trap", "virtual"], + ["security", "check"], + ["stack", "cookie"], + ["aslr", "bypass"], + ["dep", "bypass"], + ["cfg", "bypass"], + ["cet", "bypass"], + ["shadow", "stack"], + ["control", "flow"], + ["indirect", "call"], + ["jump", "oriented"], + ["call", "oriented"], + ["data", "oriented"], + ["counterfeit", "obj"], + ["use", "after"], + ["heap", "feng"], + ["house", "spirit"], + ["house", "lore"], + ["house", "force"], + ["fast", "bin"], + ["tcache", "poison"], + ["unsorted", "bin"], + ["large", "bin"], + ["small", "bin"], + ["mmap", "chunk"], + ["top", "chunk"], + ["wilderness", "area"], + ["arena", "corrupt"], + ["thread", "cache"], + ["per", "thread"], + ["main", "arena"], ], generateWord(isHeavy) { diff --git a/js/enemy.js b/js/enemy.js index a4a95a6..46b0bc4 100644 --- a/js/enemy.js +++ b/js/enemy.js @@ -1,29 +1,46 @@ class Enemy { - constructor() { - const isHeavy = Math.random() < CONFIG.game.heavyEnemyChanceBase + wave * CONFIG.game.heavyEnemyChancePerWave; - this.isHeavy = isHeavy; - const wordData = WordGenerator.generateWord(isHeavy); + constructor(boss) { + this.isBoss = !!boss; + this.isHeavy = false; - if (isHeavy) { - this.words = wordData; - this.currentWordIndex = 0; - this.text = this.words[0]; + if (this.isBoss) { + this.layers = []; + this.currentLayer = 0; + this.killedInLayer = new Set(); + for (let i = 0; i < CONFIG.game.bossLayers; i++) { + const layerWords = []; + for (let j = 0; j < CONFIG.game.bossWordsPerLayer; j++) { + layerWords.push(WordGenerator.generateWord(false)); + } + this.layers.push(layerWords); + } + this.height = S.enemyHeavyHeight * 1.5; + this.speed = crossTimeToSpeed(getEnemyCrossTime("boss")); } else { - this.words = [wordData]; - this.currentWordIndex = 0; - this.text = wordData; + const isHeavy = Math.random() < CONFIG.game.heavyEnemyChanceBase + wave * CONFIG.game.heavyEnemyChancePerWave; + this.isHeavy = isHeavy; + const wordData = WordGenerator.generateWord(isHeavy); + + if (isHeavy) { + this.words = wordData; + this.currentWordIndex = 0; + this.text = this.words[0]; + } else { + this.words = [wordData]; + this.currentWordIndex = 0; + this.text = wordData; + } + + this.height = isHeavy ? S.enemyHeavyHeight : S.enemySimpleHeight; + + if (isHeavy) { + this.speed = crossTimeToSpeed(getEnemyCrossTime("slow")); + } else { + this.speed = crossTimeToSpeed(getEnemyCrossTime("fast")); + } } this.y = 0; - this.speed = isHeavy - ? S.heavyEnemySpeedBase + Math.random() * S.heavyEnemySpeedRandom - : Math.min( - S.simpleEnemySpeedBase + - Math.random() * S.simpleEnemySpeedRandom + - wave * S.simpleEnemySpeedPerWave, - S.simpleEnemyMaxSpeed, - ); - this.height = isHeavy ? S.enemyHeavyHeight : S.enemySimpleHeight; this.pulse = 0; this.flashTimer = 0; this.width = 0; @@ -32,14 +49,29 @@ class Enemy { _calculateSpawnX() { const padding = S.enemyPaddingX; + const fontSize = this.isBoss + ? S.enemyHeavyFont * 1.2 + : this.isHeavy ? S.enemyHeavyFont : S.enemySimpleFont; + const fontWeight = (this.isBoss || this.isHeavy) ? "bold " : ""; ctx.save(); - ctx.font = `${this.isHeavy ? "bold " : ""}${this.isHeavy ? S.enemyHeavyFont : S.enemySimpleFont}px 'Courier New', monospace`; + ctx.font = `${fontWeight}${fontSize}px 'Courier New', monospace`; let maxTextWidth = 0; - for (let word of this.words) { - const w = ctx.measureText(word).width; - if (w > maxTextWidth) maxTextWidth = w; + if (this.isBoss) { + for (const layer of this.layers) { + let w = 0; + for (const word of layer) { + w += ctx.measureText(word).width; + } + w += (layer.length - 1) * 24; + if (w > maxTextWidth) maxTextWidth = w; + } + } else { + for (let word of this.words) { + const w = ctx.measureText(word).width; + if (w > maxTextWidth) maxTextWidth = w; + } } ctx.restore(); @@ -52,7 +84,24 @@ class Enemy { return minX + Math.random() * (maxX - minX); } - nextWord() { + hitWord(word) { + if (this.isBoss) { + this.killedInLayer.add(word); + this.flashTimer = S.flashDuration; + + const layer = this.layers[this.currentLayer]; + const allKilled = layer.every(w => this.killedInLayer.has(w)); + + if (allKilled) { + this.currentLayer++; + this.killedInLayer.clear(); + if (this.currentLayer >= this.layers.length) { + return true; + } + } + return false; + } + this.currentWordIndex++; this.flashTimer = S.flashDuration; if (this.currentWordIndex >= this.words.length) { @@ -73,22 +122,103 @@ class Enemy { const flash = this.flashTimer > 0; ctx.save(); - ctx.font = `${this.isHeavy ? "bold " : ""}${this.isHeavy ? S.enemyHeavyFont : S.enemySimpleFont}px 'Courier New', monospace`; + + const fontSize = this.isBoss + ? S.enemyHeavyFont * 1.2 + : this.isHeavy ? S.enemyHeavyFont : S.enemySimpleFont; + const fontWeight = (this.isBoss || this.isHeavy) ? "bold " : ""; const boxW = this.width; const boxH = this.height; - ctx.shadowColor = flash ? CONFIG.colors.white : this.isHeavy ? CONFIG.colors.heavy : CONFIG.colors.primary; + const color = this.isBoss ? CONFIG.colors.boss : this.isHeavy ? CONFIG.colors.heavy : CONFIG.colors.primary; + ctx.shadowColor = flash ? CONFIG.colors.white : color; ctx.shadowBlur = flash ? S.enemyFlashGlow : S.enemyGlow + Math.sin(this.pulse * 2) * S.enemyGlowPulse; - ctx.strokeStyle = flash - ? CONFIG.colors.white - : this.isHeavy - ? `rgba(255,170,0,${alpha})` - : `rgba(0,255,65,${alpha})`; + const rgb = this.isBoss ? "255,68,68" : this.isHeavy ? "255,170,0" : "0,255,65"; + ctx.strokeStyle = flash ? CONFIG.colors.white : `rgba(${rgb},${alpha})`; ctx.lineWidth = flash ? S.enemyLineWidth * 2 : S.enemyLineWidth; - if (this.isHeavy) { + if (this.isBoss) { + const off = S.enemyAlienOffset; + ctx.beginPath(); + ctx.moveTo(this.x - boxW / 2 + off, this.y - boxH / 2); + ctx.lineTo(this.x + boxW / 2 - off, this.y - boxH / 2); + ctx.lineTo(this.x + boxW / 2, this.y - boxH / 2 + off); + ctx.lineTo(this.x + boxW / 2, this.y + boxH / 2 - off); + ctx.lineTo(this.x + boxW / 2 - off, this.y + boxH / 2); + ctx.lineTo(this.x - boxW / 2 + off, this.y + boxH / 2); + ctx.lineTo(this.x - boxW / 2, this.y + boxH / 2 - off); + ctx.lineTo(this.x - boxW / 2, this.y - boxH / 2 + off); + ctx.closePath(); + ctx.stroke(); + + const barW = boxW - S.armorBarPad; + const barH = S.enemyArmorBarHeight; + const totalLayers = this.layers.length; + const remainingLayers = totalLayers - this.currentLayer; + ctx.fillStyle = CONFIG.colors.armorBarBg; + ctx.fillRect(this.x - barW / 2, this.y - boxH / 2 - S.enemyArmorBarOffset, barW, barH); + ctx.fillStyle = flash ? CONFIG.colors.white : CONFIG.colors.armorBarFill; + ctx.fillRect( + this.x - barW / 2, + this.y - boxH / 2 - S.enemyArmorBarOffset, + barW * (remainingLayers / totalLayers), + barH, + ); + + if (wave <= 5) { + ctx.font = `${S.armorBarFont}px "Courier New", monospace`; + ctx.fillStyle = flash ? CONFIG.colors.white : CONFIG.colors.boss; + ctx.textAlign = "center"; + ctx.fillText( + `BOSS ${remainingLayers}/${totalLayers}`, + this.x, + this.y - boxH / 2 - S.enemyArmorLabelOffset, + ); + } + + ctx.shadowBlur = 0; + ctx.font = `${fontWeight}${fontSize}px 'Courier New', monospace`; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + + const layer = this.layers[this.currentLayer]; + const wordGap = 24; + + let totalTextW = 0; + const wordWidths = []; + for (const word of layer) { + const w = ctx.measureText(word).width; + wordWidths.push(w); + totalTextW += w; + } + totalTextW += (layer.length - 1) * wordGap; + + let drawX = this.x - totalTextW / 2; + + for (let i = 0; i < layer.length; i++) { + const word = layer[i]; + const w = wordWidths[i]; + const wordX = drawX + w / 2; + + if (this.killedInLayer.has(word)) { + ctx.fillStyle = flash ? CONFIG.colors.white : `rgba(${rgb},0.3)`; + ctx.fillText(word, wordX, this.y); + ctx.beginPath(); + ctx.moveTo(wordX - w / 2, this.y); + ctx.lineTo(wordX + w / 2, this.y); + ctx.strokeStyle = flash ? CONFIG.colors.white : CONFIG.colors.boss; + ctx.lineWidth = 2; + ctx.stroke(); + } else { + ctx.fillStyle = flash ? CONFIG.colors.white : CONFIG.colors.boss; + ctx.fillText(word, wordX, this.y); + } + + drawX += w + wordGap; + } + } else if (this.isHeavy) { const off = S.enemyAlienOffset; ctx.beginPath(); ctx.moveTo(this.x - boxW / 2 + off, this.y - boxH / 2); @@ -124,18 +254,24 @@ class Enemy { this.y - boxH / 2 - S.enemyArmorLabelOffset, ); } + + ctx.shadowBlur = 0; + ctx.font = `${fontWeight}${fontSize}px 'Courier New', monospace`; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillStyle = flash ? CONFIG.colors.white : CONFIG.colors.heavy; + ctx.fillText(this.text, this.x, this.y); } else { ctx.strokeRect(this.x - boxW / 2, this.y - boxH / 2, boxW, boxH); + + ctx.shadowBlur = 0; + ctx.font = `${fontWeight}${fontSize}px 'Courier New', monospace`; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillStyle = flash ? CONFIG.colors.white : CONFIG.colors.primary; + ctx.fillText(this.text, this.x, this.y); } - ctx.shadowBlur = 0; - ctx.font = `${this.isHeavy ? "bold " : ""}${this.isHeavy ? S.enemyHeavyFont : S.enemySimpleFont}px 'Courier New', monospace`; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - - ctx.fillStyle = flash ? CONFIG.colors.white : this.isHeavy ? CONFIG.colors.heavy : CONFIG.colors.primary; - ctx.fillText(this.text, this.x, this.y); - ctx.restore(); } diff --git a/js/game.js b/js/game.js index cb70e2f..2960d5f 100644 --- a/js/game.js +++ b/js/game.js @@ -10,8 +10,11 @@ let enemies = []; let particles = []; let projectiles = []; let spawnTimer = 0; -let spawnInterval = CONFIG.game.spawnIntervalBase; +let waveEnemiesLeft = 0; let waveTimer = 0; +let wavePauseTimer = 0; +let wavePauseText = ""; +let wavePauseActive = false; let shakeTimer = 0; let shakeAmount = 0; let bgOffset = 0; @@ -55,10 +58,10 @@ const game = { }, onProjectileHit(projectile, enemy) { - const dead = enemy.nextWord(); + const dead = enemy.hitWord(projectile.word); if (dead) { - score += enemy.isHeavy ? CONFIG.game.scoreHeavy : CONFIG.game.scoreSimple; + score += enemy.isBoss ? CONFIG.game.scoreBoss : enemy.isHeavy ? CONFIG.game.scoreHeavy : CONFIG.game.scoreSimple; document.getElementById("score").textContent = score; if (projectile.username) { if (!playerStats[projectile.username]) playerStats[projectile.username] = { kills: 0, misses: 0 }; @@ -66,7 +69,8 @@ const game = { } this.destroyEnemy(enemy); } else { - spawnParticles(enemy.x, enemy.y, CONFIG.colors.heavy, S.particleArmorBreak); + const color = enemy.isBoss ? "#ff4444" : CONFIG.colors.heavy; + spawnParticles(enemy.x, enemy.y, color, S.particleArmorBreak); triggerShake(S.shakeHit, S.shakeHitDuration); } }, @@ -83,10 +87,12 @@ const game = { destroyEnemy(enemy) { const idx = enemies.indexOf(enemy); if (idx > -1) { - const color = enemy.isHeavy ? CONFIG.colors.heavy : CONFIG.colors.primary; - const count = enemy.isHeavy - ? S.particleDestroyHeavy - : S.particleDestroySimple; + const color = enemy.isBoss ? "#ff4444" : enemy.isHeavy ? CONFIG.colors.heavy : CONFIG.colors.primary; + const count = enemy.isBoss + ? S.particleDestroyHeavy * 2 + : enemy.isHeavy + ? S.particleDestroyHeavy + : S.particleDestroySimple; spawnParticles(enemy.x, enemy.y, color, count); enemies.splice(idx, 1); } @@ -97,6 +103,14 @@ function spawnEnemy() { enemies.push(new Enemy()); } +function spawnBoss() { + enemies.push(new Enemy(true)); +} + +function getEnemiesPerWave() { + return CONFIG.game.enemiesPerWaveBase + (wave - 1) * CONFIG.game.enemiesPerWaveInc; +} + function takeDamage() { lives--; document.getElementById("lives").textContent = lives; @@ -190,9 +204,10 @@ function startGame() { particles = []; projectiles = []; playerStats = {}; - spawnTimer = CONFIG.game.spawnIntervalBase; - spawnInterval = CONFIG.game.spawnIntervalBase; + spawnTimer = 0; + waveEnemiesLeft = getEnemiesPerWave(); waveTimer = 0; + wavePauseActive = false; InputModule.clear(); document.getElementById("score").textContent = score; @@ -212,25 +227,35 @@ function startGame() { function update() { if (gameState !== "playing") return; - spawnTimer++; - if (spawnTimer >= spawnInterval) { - spawnEnemy(); - spawnTimer = 0; - spawnInterval = Math.max( - CONFIG.game.spawnIntervalMin, - CONFIG.game.spawnIntervalBase - wave * CONFIG.game.spawnIntervalDecayPerWave, - ); + if (wavePauseActive) { + wavePauseTimer--; + if (wavePauseTimer <= 0) { + wavePauseActive = false; + waveEnemiesLeft = getEnemiesPerWave(); + if (wave % CONFIG.game.bossEveryNthWave === 0) { + spawnBoss(); + } + } + } else if (waveEnemiesLeft > 0) { + spawnTimer++; + const spawnInterval = CONFIG.game.waveInterval / getEnemiesPerWave(); + if (spawnTimer >= spawnInterval) { + spawnEnemy(); + waveEnemiesLeft--; + spawnTimer = 0; + } } waveTimer++; - if (waveTimer >= CONFIG.game.waveInterval) { + if (waveTimer >= CONFIG.game.waveInterval && !wavePauseActive && waveEnemiesLeft <= 0) { wave++; waveTimer = 0; document.getElementById("wave").textContent = wave; - spawnInterval = Math.max( - CONFIG.game.spawnIntervalMin, - CONFIG.game.spawnIntervalBase - wave * CONFIG.game.spawnIntervalDecayPerWave, - ); + wavePauseActive = true; + wavePauseTimer = 120; + wavePauseText = wave % CONFIG.game.bossEveryNthWave === 0 + ? `BOSS INCOMING` + : `WAVE ${wave} INCOMING`; } for (let i = enemies.length - 1; i >= 0; i--) { @@ -275,6 +300,20 @@ function draw() { for (let p of particles) p.draw(); for (let e of enemies) e.draw(); + if (wavePauseActive) { + const alpha = wavePauseTimer > 30 ? 1 : wavePauseTimer / 30; + ctx.save(); + ctx.globalAlpha = alpha; + ctx.font = `bold ${Math.round(36 * H / 1000)}px "Courier New", monospace`; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillStyle = wavePauseText === "BOSS INCOMING" ? "#ff4444" : CONFIG.colors.primary; + ctx.shadowColor = ctx.fillStyle; + ctx.shadowBlur = 20; + ctx.fillText(wavePauseText, W / 2, S.inputAreaHeight + 40); + ctx.restore(); + } + ctx.save(); ctx.fillStyle = CONFIG.colors.playerBase; ctx.fillRect(0, H - S.inputAreaHeight, W, S.inputAreaHeight); diff --git a/js/input.js b/js/input.js index ca7a75f..aded464 100644 --- a/js/input.js +++ b/js/input.js @@ -76,8 +76,15 @@ const InputModule = { findEnemy(word) { for (let e of enemies) { - if (e.text === word) { - return e; + if (e.isBoss) { + const layer = e.layers[e.currentLayer]; + if (layer && layer.includes(word) && !e.killedInLayer.has(word)) { + return e; + } + } else { + if (e.text === word) { + return e; + } } } return null; diff --git a/js/projectile.js b/js/projectile.js index b50ad19..09ca387 100644 --- a/js/projectile.js +++ b/js/projectile.js @@ -48,6 +48,11 @@ class Projectile { const dy = this.target.y - this.y; const dist = Math.sqrt(dx * dx + dy * dy); + if (dist > 0) { + this.vx = (dx / dist) * this.speed; + this.vy = (dy / dist) * this.speed; + } + if (dist < S.projectileHitRadius) { this.hit = true; game.onProjectileHit(this, this.target); diff --git a/js/utils.js b/js/utils.js index 095ed8a..4b20f34 100644 --- a/js/utils.js +++ b/js/utils.js @@ -1,3 +1,21 @@ +function crossTimeToSpeed(crossTimeSec) { + const playH = H - S.removeZone; + return playH / (crossTimeSec * 60); +} + +function getEnemyCrossTime(type) { + const SP = CONFIG.speed; + if (type === "slow") { + const t = Math.min(wave / SP.slowEnemyCrossTimeCapWave, 1); + return SP.slowEnemyCrossTimeStart - (SP.slowEnemyCrossTimeStart - SP.slowEnemyCrossTimeCap) * t; + } + if (type === "fast") { + const t = Math.min(wave / SP.fastEnemyCrossTimeCapWave, 1); + return SP.fastEnemyCrossTimeStart - (SP.fastEnemyCrossTimeStart - SP.fastEnemyCrossTimeCap) * t; + } + return SP.bossCrossTime; +} + let S = {}; function recalc() { @@ -36,16 +54,8 @@ function recalc() { S[k] = Math.round(V[k] * H / 1000); } - const SP = CONFIG.speed; - const playH = H - S.removeZone; - S.simpleEnemySpeedBase = playH / (SP.simpleEnemyCrossTime * 60); - S.simpleEnemySpeedRandom = playH / (SP.simpleEnemyCrossTimeRand * 60); - S.simpleEnemySpeedPerWave = playH / (SP.simpleEnemyCrossTimeDecay * 60); - S.simpleEnemyMaxSpeed = playH / (15 * 60); - S.heavyEnemySpeedBase = playH / (SP.heavyEnemyCrossTime * 60); - S.heavyEnemySpeedRandom = playH / (SP.heavyEnemyCrossTimeRand * 60); - S.projectileSpeed = H / (SP.projectileCrossTime * 60); - S.gridSpeed = SP.gridSpeed * H / 1000; + S.projectileSpeed = H / (CONFIG.speed.projectileCrossTime * 60); + S.gridSpeed = CONFIG.speed.gridSpeed * H / 1000; for (const k of ["projectileTrailLength","particleHit","particleMiss", "particleDestroySimple","particleDestroyHeavy","particleArmorBreak",