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