refactor: separate js code
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
/.mimocode/
|
||||||
+1
-19
@@ -20,24 +20,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script src="js/index.js"></script>
|
||||||
const input = document.getElementById("channel-input");
|
|
||||||
const copyBtn = document.getElementById("copy-btn");
|
|
||||||
const linkEl = document.getElementById("game-link");
|
|
||||||
|
|
||||||
function copyLink() {
|
|
||||||
const name = input.value.trim().toLowerCase() || "Ku6ep_XBOCTuK";
|
|
||||||
const url = `${location.origin}${location.pathname.replace(/\/[^/]*$/, "/")}game.html?channel=${encodeURIComponent(name)}`;
|
|
||||||
navigator.clipboard.writeText(url).then(() => {
|
|
||||||
linkEl.textContent = "СКОПИРОВАНО!";
|
|
||||||
linkEl.className = "copied";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
copyBtn.addEventListener("click", copyLink);
|
|
||||||
input.addEventListener("keydown", (e) => {
|
|
||||||
if (e.key === "Enter") copyLink();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+204
@@ -0,0 +1,204 @@
|
|||||||
|
const CONFIG = {
|
||||||
|
commands: { play: "!play" },
|
||||||
|
texts: {
|
||||||
|
startTitle: "SHOOTING WORD",
|
||||||
|
startSubtitle: "Введи !play чтобы играть",
|
||||||
|
gameOverTitle: "ЧАТ, ВЫ ПРОИГРАЛИ!",
|
||||||
|
gameOverSubtitle: "Введи !play чтобы играть",
|
||||||
|
scoreLabel: "SCORE:",
|
||||||
|
waveLabel: "WAVE:",
|
||||||
|
livesLabel: "LIVES:",
|
||||||
|
finalScoreLabel: "Final Score:",
|
||||||
|
armorLabel: "ARMOR",
|
||||||
|
},
|
||||||
|
game: {
|
||||||
|
startLives: 10,
|
||||||
|
startWave: 1,
|
||||||
|
startScore: 0,
|
||||||
|
spawnIntervalBase: 240,
|
||||||
|
spawnIntervalMin: 60,
|
||||||
|
spawnIntervalDecayPerWave: 12,
|
||||||
|
waveInterval: 900,
|
||||||
|
heavyEnemyChanceBase: 0.25,
|
||||||
|
heavyEnemyChancePerWave: 0.02,
|
||||||
|
scoreSimple: 10,
|
||||||
|
scoreHeavy: 50,
|
||||||
|
},
|
||||||
|
speed: {
|
||||||
|
simpleEnemyCrossTime: 60,
|
||||||
|
simpleEnemyCrossTimeRand: 120,
|
||||||
|
simpleEnemyCrossTimeDecay: 4000,
|
||||||
|
heavyEnemyCrossTime: 120,
|
||||||
|
heavyEnemyCrossTimeRand: 240,
|
||||||
|
projectileCrossTime: 1.3,
|
||||||
|
gridSpeed: 0.5,
|
||||||
|
},
|
||||||
|
visual: {
|
||||||
|
gridSize: 80,
|
||||||
|
enemyPaddingX: 60,
|
||||||
|
enemyAlienOffset: 16,
|
||||||
|
armorBarPad: 20,
|
||||||
|
armorBarFont: 20,
|
||||||
|
missTargetPadX: 60,
|
||||||
|
missTargetPadY: 100,
|
||||||
|
inputAreaHeight: 105,
|
||||||
|
removeZone: 60,
|
||||||
|
playerY: 32,
|
||||||
|
playerRadius: 16,
|
||||||
|
playerGlow: 30,
|
||||||
|
enemySimpleHeight: 30,
|
||||||
|
enemyHeavyHeight: 42,
|
||||||
|
enemySimpleFont: 28,
|
||||||
|
enemyHeavyFont: 30,
|
||||||
|
enemyArmorBarHeight: 6,
|
||||||
|
enemyArmorBarOffset: 11,
|
||||||
|
enemyArmorLabelOffset: 15,
|
||||||
|
enemyGlow: 10,
|
||||||
|
enemyFlashGlow: 50,
|
||||||
|
enemyLineWidth: 3,
|
||||||
|
projectileGlow: 30,
|
||||||
|
projectileHitRadius: 30,
|
||||||
|
projectileMissRadius: 30,
|
||||||
|
projectileFont: 28,
|
||||||
|
projectileCircleRadius: 30,
|
||||||
|
shakeHit: 8,
|
||||||
|
shakeMiss: 6,
|
||||||
|
shakeDamage: 16,
|
||||||
|
particleVel: 12,
|
||||||
|
particleSizeMin: 4,
|
||||||
|
particleSizeMax: 10,
|
||||||
|
particleGlow: 16,
|
||||||
|
trailSizeBase: 4,
|
||||||
|
trailSizeInc: 1,
|
||||||
|
trailGlow: 20,
|
||||||
|
shooterOffset: 50,
|
||||||
|
},
|
||||||
|
visualRatio: {
|
||||||
|
missTargetYRange: 0.5,
|
||||||
|
projectileTrailLength: 8,
|
||||||
|
projectileTrailDecay: 0.15,
|
||||||
|
projectileTrailAlpha: 0.6,
|
||||||
|
projectileCircleAlpha: 0.5,
|
||||||
|
enemyPulseRate: 0.05,
|
||||||
|
enemyAlphaBase: 0.7,
|
||||||
|
enemyAlphaRange: 0.3,
|
||||||
|
enemyGlowPulse: 5,
|
||||||
|
particleDecayBase: 0.02,
|
||||||
|
particleDecayRange: 0.03,
|
||||||
|
particleDamping: 0.98,
|
||||||
|
},
|
||||||
|
counts: {
|
||||||
|
projectileTrailLength: 8,
|
||||||
|
particleHit: 15,
|
||||||
|
particleMiss: 20,
|
||||||
|
particleDestroySimple: 15,
|
||||||
|
particleDestroyHeavy: 30,
|
||||||
|
particleArmorBreak: 15,
|
||||||
|
flashDuration: 10,
|
||||||
|
shakeHitDuration: 6,
|
||||||
|
shakeMissDuration: 5,
|
||||||
|
shakeDamageDuration: 10,
|
||||||
|
trailMaxLength: 10,
|
||||||
|
trailDecay: 0.12,
|
||||||
|
trailAlpha: 0.5,
|
||||||
|
},
|
||||||
|
timing: {
|
||||||
|
inputShootDelay: 150,
|
||||||
|
glitchDuration: 300,
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: "#00ff41",
|
||||||
|
primaryDim: "#00cc33",
|
||||||
|
primaryGlow: "rgba(0,255,65,0.2)",
|
||||||
|
primaryGrid: "rgba(0,255,65,0.08)",
|
||||||
|
heavy: "#ffaa00",
|
||||||
|
heavyDim: "#cc8800",
|
||||||
|
miss: "#ff0044",
|
||||||
|
white: "#ffffff",
|
||||||
|
red: "#ff0000",
|
||||||
|
armorBarBg: "rgba(255,0,0,0.3)",
|
||||||
|
armorBarFill: "#ffaa00",
|
||||||
|
playerBase: "rgba(0,255,65,0.15)",
|
||||||
|
scanline: "rgba(0,255,65,0.03)",
|
||||||
|
},
|
||||||
|
intro: {
|
||||||
|
fontSize: 44,
|
||||||
|
textYR: 0.35,
|
||||||
|
subtitleYOffset: 60,
|
||||||
|
fadeInFrames: 40,
|
||||||
|
shot1Frame: 45,
|
||||||
|
shot2Frame: 70,
|
||||||
|
shot3Frame: 95,
|
||||||
|
shotSpeed: 0.045,
|
||||||
|
particleCountPerHit: 18,
|
||||||
|
slideStartFrame: 122,
|
||||||
|
slideDuration: 20,
|
||||||
|
totalFrames: 180,
|
||||||
|
startDelay: 300,
|
||||||
|
glow: 50,
|
||||||
|
trailGlow: 20,
|
||||||
|
projectileGlow: 30,
|
||||||
|
projectileRadius: 8,
|
||||||
|
trailRect: 8,
|
||||||
|
trailMax: 10,
|
||||||
|
trailDecay: 0.12,
|
||||||
|
trailAlpha: 0.5,
|
||||||
|
subtitleFont: 32,
|
||||||
|
subtitleGlow: 20,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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",
|
||||||
|
],
|
||||||
|
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"],
|
||||||
|
],
|
||||||
|
|
||||||
|
generateWord(isHeavy) {
|
||||||
|
if (isHeavy) {
|
||||||
|
const pair = this.heavyWords[Math.floor(Math.random() * this.heavyWords.length)];
|
||||||
|
return pair;
|
||||||
|
}
|
||||||
|
return this.simpleWords[Math.floor(Math.random() * this.simpleWords.length)];
|
||||||
|
},
|
||||||
|
};
|
||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
class Enemy {
|
||||||
|
constructor() {
|
||||||
|
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.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;
|
||||||
|
this.x = this._calculateSpawnX();
|
||||||
|
}
|
||||||
|
|
||||||
|
_calculateSpawnX() {
|
||||||
|
const padding = S.enemyPaddingX;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.font = `${this.isHeavy ? "bold " : ""}${this.isHeavy ? S.enemyHeavyFont : S.enemySimpleFont}px 'Courier New', monospace`;
|
||||||
|
|
||||||
|
let maxTextWidth = 0;
|
||||||
|
for (let word of this.words) {
|
||||||
|
const w = ctx.measureText(word).width;
|
||||||
|
if (w > maxTextWidth) maxTextWidth = w;
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
this.width = maxTextWidth + 24;
|
||||||
|
const halfWidth = this.width / 2;
|
||||||
|
|
||||||
|
const minX = padding + halfWidth;
|
||||||
|
const maxX = W - padding - halfWidth;
|
||||||
|
|
||||||
|
return minX + Math.random() * (maxX - minX);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextWord() {
|
||||||
|
this.currentWordIndex++;
|
||||||
|
this.flashTimer = S.flashDuration;
|
||||||
|
if (this.currentWordIndex >= this.words.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
this.text = this.words[this.currentWordIndex];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.y += this.speed;
|
||||||
|
this.pulse += 0.05;
|
||||||
|
if (this.flashTimer > 0) this.flashTimer--;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
const alpha = S.enemyAlphaBase + Math.sin(this.pulse) * S.enemyAlphaRange;
|
||||||
|
const flash = this.flashTimer > 0;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.font = `${this.isHeavy ? "bold " : ""}${this.isHeavy ? S.enemyHeavyFont : S.enemySimpleFont}px 'Courier New', monospace`;
|
||||||
|
|
||||||
|
const boxW = this.width;
|
||||||
|
const boxH = this.height;
|
||||||
|
|
||||||
|
ctx.shadowColor = flash ? CONFIG.colors.white : this.isHeavy ? CONFIG.colors.heavy : CONFIG.colors.primary;
|
||||||
|
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})`;
|
||||||
|
ctx.lineWidth = flash ? S.enemyLineWidth * 2 : S.enemyLineWidth;
|
||||||
|
|
||||||
|
if (this.isHeavy) {
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
const barW = boxW - S.armorBarPad;
|
||||||
|
const barH = S.enemyArmorBarHeight;
|
||||||
|
const totalLayers = this.words.length;
|
||||||
|
const remainingLayers = totalLayers - this.currentWordIndex;
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.font = `${S.armorBarFont}px "Courier New", monospace`;
|
||||||
|
ctx.fillStyle = flash ? CONFIG.colors.white : CONFIG.colors.heavy;
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.fillText(
|
||||||
|
`${CONFIG.texts.armorLabel} ${remainingLayers}/${totalLayers}`,
|
||||||
|
this.x,
|
||||||
|
this.y - boxH / 2 - S.enemyArmorLabelOffset,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ctx.strokeRect(this.x - boxW / 2, this.y - boxH / 2, boxW, boxH);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
getBounds() {
|
||||||
|
return {
|
||||||
|
left: this.x - this.width / 2,
|
||||||
|
right: this.x + this.width / 2,
|
||||||
|
top: this.y - this.height / 2,
|
||||||
|
bottom: this.y + this.height / 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+272
@@ -0,0 +1,272 @@
|
|||||||
|
const canvas = document.getElementById("game-canvas");
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
let W, H;
|
||||||
|
|
||||||
|
let gameState = "start";
|
||||||
|
let score = 0,
|
||||||
|
wave = 1,
|
||||||
|
lives = 3;
|
||||||
|
let enemies = [];
|
||||||
|
let particles = [];
|
||||||
|
let projectiles = [];
|
||||||
|
let spawnTimer = 0;
|
||||||
|
let spawnInterval = CONFIG.game.spawnIntervalBase;
|
||||||
|
let waveTimer = 0;
|
||||||
|
let shakeTimer = 0;
|
||||||
|
let shakeAmount = 0;
|
||||||
|
let bgOffset = 0;
|
||||||
|
let gameLoopStarted = false;
|
||||||
|
let introPlaying = false;
|
||||||
|
|
||||||
|
let playerStats = {};
|
||||||
|
|
||||||
|
const game = {
|
||||||
|
launchProjectile(word, targetEnemy, isHit, username) {
|
||||||
|
projectiles.push(new Projectile(word, targetEnemy, isHit, username));
|
||||||
|
},
|
||||||
|
|
||||||
|
onProjectileHit(projectile, enemy) {
|
||||||
|
const dead = enemy.nextWord();
|
||||||
|
|
||||||
|
if (dead) {
|
||||||
|
score += 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 };
|
||||||
|
playerStats[projectile.username].kills++;
|
||||||
|
}
|
||||||
|
this.destroyEnemy(enemy);
|
||||||
|
} else {
|
||||||
|
spawnParticles(enemy.x, enemy.y, CONFIG.colors.heavy, S.particleArmorBreak);
|
||||||
|
triggerShake(S.shakeHit, S.shakeHitDuration);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onProjectileMiss(projectile) {
|
||||||
|
spawnParticles(projectile.targetX, projectile.targetY, CONFIG.colors.miss, S.particleMiss);
|
||||||
|
triggerShake(S.shakeMiss, S.shakeMissDuration);
|
||||||
|
if (projectile.username) {
|
||||||
|
if (!playerStats[projectile.username]) playerStats[projectile.username] = { kills: 0, misses: 0 };
|
||||||
|
playerStats[projectile.username].misses++;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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;
|
||||||
|
spawnParticles(enemy.x, enemy.y, color, count);
|
||||||
|
enemies.splice(idx, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function spawnEnemy() {
|
||||||
|
enemies.push(new Enemy());
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeDamage() {
|
||||||
|
lives--;
|
||||||
|
document.getElementById("lives").textContent = lives;
|
||||||
|
triggerShake(S.shakeDamage, S.shakeDamageDuration);
|
||||||
|
document.getElementById("game-container").classList.add("glitch");
|
||||||
|
setTimeout(
|
||||||
|
() => document.getElementById("game-container").classList.remove("glitch"),
|
||||||
|
S.glitchDuration,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (lives <= 0) {
|
||||||
|
gameOver();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let gameOverTimeout = null;
|
||||||
|
|
||||||
|
function renderPlayerStats() {
|
||||||
|
const container = document.getElementById("player-stats");
|
||||||
|
container.innerHTML = "";
|
||||||
|
|
||||||
|
const names = Object.keys(playerStats);
|
||||||
|
if (names.length === 0) return;
|
||||||
|
|
||||||
|
const byKills = names.slice().sort((a, b) => playerStats[b].kills - playerStats[a].kills);
|
||||||
|
const top3 = byKills.slice(0, 3);
|
||||||
|
|
||||||
|
const sectionTitle = document.createElement("div");
|
||||||
|
sectionTitle.className = "stats-section-title";
|
||||||
|
sectionTitle.textContent = "УБИЙЦЫ";
|
||||||
|
container.appendChild(sectionTitle);
|
||||||
|
|
||||||
|
top3.forEach((name, i) => {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "stats-row" + (i === 0 ? " stats-top1" : i === 1 ? " stats-top2" : " stats-top3");
|
||||||
|
const place = i === 0 ? "1" : i === 1 ? "2" : "3";
|
||||||
|
row.innerHTML = `<span class="stats-place">#${place}</span> <span class="stats-name">${name}</span> <span class="stats-kills">${playerStats[name].kills}</span>`;
|
||||||
|
container.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
let maxMisses = 0;
|
||||||
|
let mazilaName = "";
|
||||||
|
for (const name of names) {
|
||||||
|
if (playerStats[name].misses > maxMisses) {
|
||||||
|
maxMisses = playerStats[name].misses;
|
||||||
|
mazilaName = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mazilaName && maxMisses > 0) {
|
||||||
|
const mazila = document.createElement("div");
|
||||||
|
mazila.className = "stats-mazila";
|
||||||
|
mazila.innerHTML = `<span class="mazila-title">Мазила:</span> ${mazilaName} <span class="mazila-count">(${maxMisses})</span>`;
|
||||||
|
container.appendChild(mazila);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function gameOver() {
|
||||||
|
gameState = "gameover";
|
||||||
|
document.getElementById("final-score").textContent = score;
|
||||||
|
renderPlayerStats();
|
||||||
|
document.getElementById("gameover-screen").classList.remove("hidden");
|
||||||
|
document.getElementById("gameover-subtitle").classList.add("hidden");
|
||||||
|
|
||||||
|
if (gameOverTimeout) clearTimeout(gameOverTimeout);
|
||||||
|
gameOverTimeout = setTimeout(() => {
|
||||||
|
gameOverTimeout = null;
|
||||||
|
document.getElementById("gameover-screen").classList.add("hidden");
|
||||||
|
document.getElementById("ui-overlay").classList.add("hidden");
|
||||||
|
document.getElementById("input-display").classList.add("hidden");
|
||||||
|
gameLoopStarted = false;
|
||||||
|
recalc();
|
||||||
|
runIntro();
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startGame() {
|
||||||
|
if (gameOverTimeout) {
|
||||||
|
clearTimeout(gameOverTimeout);
|
||||||
|
gameOverTimeout = null;
|
||||||
|
}
|
||||||
|
gameState = "playing";
|
||||||
|
score = CONFIG.game.startScore;
|
||||||
|
wave = CONFIG.game.startWave;
|
||||||
|
lives = CONFIG.game.startLives;
|
||||||
|
enemies = [];
|
||||||
|
particles = [];
|
||||||
|
projectiles = [];
|
||||||
|
playerStats = {};
|
||||||
|
spawnTimer = CONFIG.game.spawnIntervalBase;
|
||||||
|
spawnInterval = CONFIG.game.spawnIntervalBase;
|
||||||
|
waveTimer = 0;
|
||||||
|
InputModule.clear();
|
||||||
|
|
||||||
|
document.getElementById("score").textContent = score;
|
||||||
|
document.getElementById("wave").textContent = wave;
|
||||||
|
document.getElementById("lives").textContent = lives;
|
||||||
|
document.getElementById("start-screen").classList.add("hidden");
|
||||||
|
document.getElementById("gameover-screen").classList.add("hidden");
|
||||||
|
document.getElementById("ui-overlay").classList.remove("hidden");
|
||||||
|
document.getElementById("input-display").classList.remove("hidden");
|
||||||
|
|
||||||
|
if (!gameLoopStarted) {
|
||||||
|
gameLoopStarted = true;
|
||||||
|
loop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
waveTimer++;
|
||||||
|
if (waveTimer >= CONFIG.game.waveInterval) {
|
||||||
|
wave++;
|
||||||
|
waveTimer = 0;
|
||||||
|
document.getElementById("wave").textContent = wave;
|
||||||
|
spawnInterval = Math.max(
|
||||||
|
CONFIG.game.spawnIntervalMin,
|
||||||
|
CONFIG.game.spawnIntervalBase - wave * CONFIG.game.spawnIntervalDecayPerWave,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = enemies.length - 1; i >= 0; i--) {
|
||||||
|
const e = enemies[i];
|
||||||
|
e.update();
|
||||||
|
|
||||||
|
if (e.y > H - S.removeZone) {
|
||||||
|
takeDamage();
|
||||||
|
enemies.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = particles.length - 1; i >= 0; i--) {
|
||||||
|
particles[i].update();
|
||||||
|
if (particles[i].life <= 0) particles.splice(i, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = projectiles.length - 1; i >= 0; i--) {
|
||||||
|
const alive = projectiles[i].update();
|
||||||
|
if (!alive) {
|
||||||
|
projectiles.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shakeTimer > 0) {
|
||||||
|
shakeTimer--;
|
||||||
|
shakeAmount *= 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
ctx.clearRect(0, 0, W, H);
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
if (shakeTimer > 0) {
|
||||||
|
ctx.translate((Math.random() - 0.5) * shakeAmount, (Math.random() - 0.5) * shakeAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawGrid();
|
||||||
|
|
||||||
|
for (let p of projectiles) p.draw();
|
||||||
|
for (let p of particles) p.draw();
|
||||||
|
for (let e of enemies) e.draw();
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = CONFIG.colors.playerBase;
|
||||||
|
ctx.fillRect(0, H - S.inputAreaHeight, W, S.inputAreaHeight);
|
||||||
|
ctx.strokeStyle = CONFIG.colors.primary;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, H - S.inputAreaHeight);
|
||||||
|
ctx.lineTo(W, H - S.inputAreaHeight);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.fillStyle = CONFIG.colors.primary;
|
||||||
|
ctx.shadowColor = CONFIG.colors.primary;
|
||||||
|
ctx.shadowBlur = S.playerGlow;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(W / 2, H - S.playerY, S.playerRadius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loop() {
|
||||||
|
update();
|
||||||
|
draw();
|
||||||
|
if (gameLoopStarted) {
|
||||||
|
requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
const input = document.getElementById("channel-input");
|
||||||
|
const copyBtn = document.getElementById("copy-btn");
|
||||||
|
const linkEl = document.getElementById("game-link");
|
||||||
|
|
||||||
|
function copyLink() {
|
||||||
|
const name = input.value.trim().toLowerCase() || "Ku6ep_XBOCTuK";
|
||||||
|
const url = `${location.origin}${location.pathname.replace(/\/[^/]*$/, "/")}game.html?channel=${encodeURIComponent(name)}`;
|
||||||
|
navigator.clipboard.writeText(url).then(() => {
|
||||||
|
linkEl.textContent = "СКОПИРОВАНО!";
|
||||||
|
linkEl.className = "copied";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
copyBtn.addEventListener("click", copyLink);
|
||||||
|
input.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter") copyLink();
|
||||||
|
});
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
const InputModule = {
|
||||||
|
currentLine: "",
|
||||||
|
|
||||||
|
submitLine(text, username) {
|
||||||
|
const line = text.trim().toLowerCase();
|
||||||
|
if (!line) return;
|
||||||
|
if (introPlaying) return;
|
||||||
|
|
||||||
|
if (line === CONFIG.commands.play) {
|
||||||
|
if (gameState === "start" || gameState === "gameover") {
|
||||||
|
startGame();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gameState !== "playing") return;
|
||||||
|
|
||||||
|
this.currentLine = line;
|
||||||
|
this.updateDisplay(line);
|
||||||
|
|
||||||
|
const enemy = this.findEnemy(line);
|
||||||
|
|
||||||
|
this.animateShoot(line, enemy, username);
|
||||||
|
},
|
||||||
|
|
||||||
|
animateShoot(line, enemy, username) {
|
||||||
|
const el = document.getElementById("typed-text");
|
||||||
|
|
||||||
|
el.classList.add("shooting");
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
el.classList.remove("shooting");
|
||||||
|
this.currentLine = "";
|
||||||
|
this.updateDisplay("");
|
||||||
|
|
||||||
|
if (enemy) {
|
||||||
|
game.launchProjectile(line, enemy, true, username);
|
||||||
|
} else {
|
||||||
|
game.launchProjectile(line, null, false, username);
|
||||||
|
}
|
||||||
|
}, S.inputShootDelay);
|
||||||
|
},
|
||||||
|
|
||||||
|
findEnemy(word) {
|
||||||
|
for (let e of enemies) {
|
||||||
|
if (e.text === word) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateDisplay(text) {
|
||||||
|
const el = document.getElementById("typed-text");
|
||||||
|
el.innerHTML = text;
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.currentLine = "";
|
||||||
|
this.updateDisplay("");
|
||||||
|
},
|
||||||
|
};
|
||||||
+153
@@ -0,0 +1,153 @@
|
|||||||
|
function runIntro() {
|
||||||
|
introPlaying = true;
|
||||||
|
const I = CONFIG.intro;
|
||||||
|
const ih = H / 1000;
|
||||||
|
const IS = {
|
||||||
|
fontSize: Math.round(I.fontSize * ih),
|
||||||
|
glow: Math.round(I.glow * ih),
|
||||||
|
trailGlow: Math.round(I.trailGlow * ih),
|
||||||
|
projectileGlow: Math.round(I.projectileGlow * ih),
|
||||||
|
projectileRadius: Math.round(I.projectileRadius * ih),
|
||||||
|
trailRect: Math.round(I.trailRect * ih),
|
||||||
|
subtitleFont: Math.round(I.subtitleFont * ih),
|
||||||
|
subtitleGlow: Math.round(I.subtitleGlow * ih),
|
||||||
|
subtitleYOffset: Math.round(I.subtitleYOffset * ih),
|
||||||
|
};
|
||||||
|
const TEXT_Y = Math.round(H * I.textYR);
|
||||||
|
ctx.save();
|
||||||
|
ctx.font = `bold ${IS.fontSize}px "Courier New", monospace`;
|
||||||
|
const totalW = ctx.measureText("SHOOTING WORLD").width;
|
||||||
|
const worW = ctx.measureText("SHOOTING WOR").width;
|
||||||
|
const lW = ctx.measureText("L").width;
|
||||||
|
const dW = ctx.measureText("D").width;
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
const cx = W / 2;
|
||||||
|
const lTargetX = cx - totalW / 2 + worW + lW / 2;
|
||||||
|
const lTargetY = TEXT_Y;
|
||||||
|
const shooterX = cx;
|
||||||
|
const shooterY = H - S.shooterOffset;
|
||||||
|
|
||||||
|
let frame = 0;
|
||||||
|
let textAlpha = 0;
|
||||||
|
let p1 = [];
|
||||||
|
let shots = [];
|
||||||
|
|
||||||
|
function drawPartialWorld(alpha, dSlide) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.font = `bold ${IS.fontSize}px "Courier New", monospace`;
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
|
||||||
|
ctx.globalAlpha = alpha;
|
||||||
|
ctx.fillStyle = CONFIG.colors.primary;
|
||||||
|
ctx.shadowColor = CONFIG.colors.primaryGlow;
|
||||||
|
ctx.shadowBlur = IS.glow;
|
||||||
|
ctx.fillText("SHOOTING WOR", cx - totalW / 2 + worW / 2, TEXT_Y);
|
||||||
|
|
||||||
|
const lFade = Math.max(0, 1 - dSlide);
|
||||||
|
if (lFade > 0) {
|
||||||
|
ctx.globalAlpha = alpha * lFade;
|
||||||
|
ctx.fillText("L", cx - totalW / 2 + worW + lW / 2, TEXT_Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dOffset = lW * dSlide;
|
||||||
|
ctx.globalAlpha = alpha;
|
||||||
|
ctx.fillText("D", cx - totalW / 2 + worW + lW + dW / 2 - dOffset, TEXT_Y);
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawShotProjectile(s) {
|
||||||
|
const x = shooterX + (lTargetX - shooterX) * s.progress;
|
||||||
|
const y = shooterY + (lTargetY - shooterY) * s.progress;
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
for (let t of s.trail) {
|
||||||
|
ctx.globalAlpha = t.life * I.trailAlpha;
|
||||||
|
ctx.fillStyle = CONFIG.colors.primary;
|
||||||
|
ctx.shadowColor = CONFIG.colors.primary;
|
||||||
|
ctx.shadowBlur = IS.trailGlow;
|
||||||
|
ctx.fillRect(t.x - IS.trailRect / 2, t.y - IS.trailRect / 2, IS.trailRect, IS.trailRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!s.done) {
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
ctx.fillStyle = CONFIG.colors.primary;
|
||||||
|
ctx.shadowColor = CONFIG.colors.primary;
|
||||||
|
ctx.shadowBlur = IS.projectileGlow;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, IS.projectileRadius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function introLoop() {
|
||||||
|
frame++;
|
||||||
|
|
||||||
|
if (frame <= I.fadeInFrames) {
|
||||||
|
textAlpha = Math.min(1, frame / I.fadeInFrames);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame === I.shot1Frame) shots.push({ progress: 0, trail: [], done: false });
|
||||||
|
if (frame === I.shot2Frame) shots.push({ progress: 0, trail: [], done: false });
|
||||||
|
if (frame === I.shot3Frame) shots.push({ progress: 0, trail: [], done: false });
|
||||||
|
|
||||||
|
for (let s of shots) {
|
||||||
|
if (s.done) continue;
|
||||||
|
s.progress = Math.min(1, s.progress + I.shotSpeed);
|
||||||
|
|
||||||
|
const x = shooterX + (lTargetX - shooterX) * s.progress;
|
||||||
|
const y = shooterY + (lTargetY - shooterY) * s.progress;
|
||||||
|
|
||||||
|
s.trail.push({ x, y, life: 1 });
|
||||||
|
if (s.trail.length > I.trailMax) s.trail.shift();
|
||||||
|
for (let t of s.trail) t.life -= I.trailDecay;
|
||||||
|
s.trail = s.trail.filter((t) => t.life > 0);
|
||||||
|
|
||||||
|
if (s.progress >= 1) {
|
||||||
|
s.done = true;
|
||||||
|
for (let i = 0; i < I.particleCountPerHit; i++) {
|
||||||
|
p1.push(new Particle(lTargetX, lTargetY, CONFIG.colors.primary));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dSlide = 0;
|
||||||
|
if (frame >= I.slideStartFrame) {
|
||||||
|
dSlide = Math.min(1, (frame - I.slideStartFrame) / I.slideDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let p of p1) p.update();
|
||||||
|
p1 = p1.filter((p) => p.life > 0);
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, W, H);
|
||||||
|
drawGrid();
|
||||||
|
|
||||||
|
drawPartialWorld(textAlpha, dSlide);
|
||||||
|
for (let s of shots) drawShotProjectile(s);
|
||||||
|
for (let p of p1) p.draw();
|
||||||
|
|
||||||
|
if (frame < I.totalFrames) {
|
||||||
|
requestAnimationFrame(introLoop);
|
||||||
|
} else {
|
||||||
|
p1 = [];
|
||||||
|
introPlaying = false;
|
||||||
|
ctx.clearRect(0, 0, W, H);
|
||||||
|
drawGrid();
|
||||||
|
drawPartialWorld(1, 1);
|
||||||
|
ctx.save();
|
||||||
|
ctx.font = `${IS.subtitleFont}px "Courier New", monospace`;
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.fillStyle = CONFIG.colors.primaryDim;
|
||||||
|
ctx.shadowColor = CONFIG.colors.primary;
|
||||||
|
ctx.shadowBlur = IS.subtitleGlow;
|
||||||
|
ctx.fillText(CONFIG.texts.startSubtitle, cx, TEXT_Y + IS.subtitleYOffset);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(introLoop, I.startDelay);
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
(function initUI() {
|
||||||
|
document.getElementById("start-subtitle").textContent = CONFIG.texts.startSubtitle;
|
||||||
|
document.getElementById("gameover-title").textContent = CONFIG.texts.gameOverTitle;
|
||||||
|
document.getElementById("gameover-subtitle").textContent = CONFIG.texts.gameOverSubtitle;
|
||||||
|
|
||||||
|
document.querySelector("#ui-overlay .ui-text:nth-child(1)").innerHTML =
|
||||||
|
`${CONFIG.texts.scoreLabel} <span id="score">0</span>`;
|
||||||
|
document.querySelector("#ui-overlay .ui-text:nth-child(2)").innerHTML =
|
||||||
|
`${CONFIG.texts.waveLabel} <span id="wave">1</span>`;
|
||||||
|
document.querySelector("#ui-overlay .ui-text:nth-child(3)").innerHTML =
|
||||||
|
`${CONFIG.texts.livesLabel} <span id="lives">3</span>`;
|
||||||
|
|
||||||
|
document.querySelector("#gameover-screen .screen-subtitle").innerHTML =
|
||||||
|
`${CONFIG.texts.finalScoreLabel} <span id="final-score">0</span>`;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const channel = params.get("channel") || "ku6ep_xboctuk";
|
||||||
|
const client = new tmi.Client({ channels: [channel] });
|
||||||
|
client.connect();
|
||||||
|
|
||||||
|
client.on("message", (channel, tags, message, self) => {
|
||||||
|
const username = tags["display-name"] || tags.username || "anonymous";
|
||||||
|
InputModule.submitLine(message, username);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("resize", recalc);
|
||||||
|
recalc();
|
||||||
|
runIntro();
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
class Particle {
|
||||||
|
constructor(x, y, color) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.vx = (Math.random() - 0.5) * S.particleVel;
|
||||||
|
this.vy = (Math.random() - 0.5) * S.particleVel;
|
||||||
|
this.life = 1;
|
||||||
|
this.decay = S.particleDecayBase + Math.random() * S.particleDecayRange;
|
||||||
|
this.color = color;
|
||||||
|
this.size = S.particleSizeMin + Math.random() * (S.particleSizeMax - S.particleSizeMin);
|
||||||
|
}
|
||||||
|
update() {
|
||||||
|
this.x += this.vx;
|
||||||
|
this.y += this.vy;
|
||||||
|
this.life -= this.decay;
|
||||||
|
this.vx *= S.particleDamping;
|
||||||
|
this.vy *= S.particleDamping;
|
||||||
|
}
|
||||||
|
draw() {
|
||||||
|
ctx.save();
|
||||||
|
ctx.globalAlpha = this.life;
|
||||||
|
ctx.fillStyle = this.color;
|
||||||
|
ctx.shadowColor = this.color;
|
||||||
|
ctx.shadowBlur = S.particleGlow;
|
||||||
|
ctx.fillRect(this.x - this.size / 2, this.y - this.size / 2, this.size, this.size);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
class Projectile {
|
||||||
|
constructor(word, targetEnemy, isHit, username) {
|
||||||
|
this.word = word;
|
||||||
|
this.isHit = isHit;
|
||||||
|
this.username = username;
|
||||||
|
this.x = W / 2;
|
||||||
|
this.y = H - S.shooterOffset;
|
||||||
|
this.speed = S.projectileSpeed;
|
||||||
|
this.life = 1;
|
||||||
|
this.trail = [];
|
||||||
|
this.hit = false;
|
||||||
|
|
||||||
|
if (isHit && targetEnemy) {
|
||||||
|
this.target = targetEnemy;
|
||||||
|
this.targetX = targetEnemy.x;
|
||||||
|
this.targetY = targetEnemy.y;
|
||||||
|
const dx = targetEnemy.x - this.x;
|
||||||
|
const dy = targetEnemy.y - this.y;
|
||||||
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
this.vx = (dx / dist) * this.speed;
|
||||||
|
this.vy = (dy / dist) * this.speed;
|
||||||
|
} else {
|
||||||
|
this.target = null;
|
||||||
|
this.targetX = S.missTargetPadX + Math.random() * (W - 2 * S.missTargetPadX);
|
||||||
|
this.targetY = S.missTargetPadY + Math.random() * (H * S.missTargetYRange);
|
||||||
|
const dx = this.targetX - this.x;
|
||||||
|
const dy = this.targetY - this.y;
|
||||||
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
this.vx = (dx / dist) * this.speed;
|
||||||
|
this.vy = (dy / dist) * this.speed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.trail.push({ x: this.x, y: this.y, life: 1 });
|
||||||
|
if (this.trail.length > S.projectileTrailLength) this.trail.shift();
|
||||||
|
|
||||||
|
for (let t of this.trail) {
|
||||||
|
t.life -= S.projectileTrailDecay;
|
||||||
|
}
|
||||||
|
this.trail = this.trail.filter((t) => t.life > 0);
|
||||||
|
|
||||||
|
this.x += this.vx;
|
||||||
|
this.y += this.vy;
|
||||||
|
|
||||||
|
if (this.isHit && this.target && !this.hit) {
|
||||||
|
const dx = this.target.x - this.x;
|
||||||
|
const dy = this.target.y - this.y;
|
||||||
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (dist < S.projectileHitRadius) {
|
||||||
|
this.hit = true;
|
||||||
|
game.onProjectileHit(this, this.target);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enemies.includes(this.target)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isHit && !this.hit) {
|
||||||
|
const dx = this.targetX - this.x;
|
||||||
|
const dy = this.targetY - this.y;
|
||||||
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (dist < S.projectileMissRadius) {
|
||||||
|
this.hit = true;
|
||||||
|
game.onProjectileMiss(this);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const off = S.shooterOffset;
|
||||||
|
if (this.x < -off || this.x > W + off || this.y < -off || this.y > H + off) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
const color = this.isHit ? CONFIG.colors.primary : CONFIG.colors.miss;
|
||||||
|
const glowColor = this.isHit ? CONFIG.colors.primary : CONFIG.colors.miss;
|
||||||
|
|
||||||
|
for (let i = 0; i < this.trail.length; i++) {
|
||||||
|
const t = this.trail[i];
|
||||||
|
ctx.globalAlpha = t.life * S.projectileTrailAlpha;
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.shadowColor = glowColor;
|
||||||
|
ctx.shadowBlur = S.trailGlow;
|
||||||
|
const size = S.trailSizeBase + i * S.trailSizeInc;
|
||||||
|
ctx.fillRect(t.x - size / 2, t.y - size / 2, size, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
ctx.font = `bold ${S.projectileFont}px "Courier New", monospace`;
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.shadowColor = glowColor;
|
||||||
|
ctx.shadowBlur = S.projectileGlow;
|
||||||
|
ctx.fillText(this.word, this.x, this.y);
|
||||||
|
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.globalAlpha = S.projectileCircleAlpha;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(this.x, this.y, S.projectileCircleRadius, 0, Math.PI * 2);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
+142
@@ -0,0 +1,142 @@
|
|||||||
|
let S = {};
|
||||||
|
|
||||||
|
function recalc() {
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
W = window.innerWidth;
|
||||||
|
H = window.innerHeight;
|
||||||
|
canvas.width = W * dpr;
|
||||||
|
canvas.height = H * dpr;
|
||||||
|
canvas.style.width = W + "px";
|
||||||
|
canvas.style.height = H + "px";
|
||||||
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
S = {};
|
||||||
|
|
||||||
|
const V = CONFIG.visual;
|
||||||
|
const C = CONFIG.counts;
|
||||||
|
const R = CONFIG.visualRatio;
|
||||||
|
const T = CONFIG.timing;
|
||||||
|
|
||||||
|
S.gridSize = Math.round(V.gridSize * W / 1000);
|
||||||
|
S.enemyPaddingX = Math.round(V.enemyPaddingX * W / 1000);
|
||||||
|
S.enemyAlienOffset = Math.round(V.enemyAlienOffset * W / 1000);
|
||||||
|
S.armorBarPad = Math.round(V.armorBarPad * W / 1000);
|
||||||
|
S.armorBarFont = Math.round(V.armorBarFont * W / 1000);
|
||||||
|
S.missTargetPadX = Math.round(V.missTargetPadX * W / 1000);
|
||||||
|
S.missTargetPadY = Math.round(V.missTargetPadY * H / 1000);
|
||||||
|
S.inputAreaHeight = Math.round(V.inputAreaHeight * H / 1000);
|
||||||
|
S.removeZone = Math.round(V.removeZone * H / 1000);
|
||||||
|
|
||||||
|
for (const k of ["playerY","playerRadius","playerGlow","enemySimpleHeight","enemyHeavyHeight",
|
||||||
|
"enemySimpleFont","enemyHeavyFont","enemyArmorBarHeight","enemyArmorBarOffset",
|
||||||
|
"enemyArmorLabelOffset","enemyGlow","enemyFlashGlow","enemyLineWidth",
|
||||||
|
"projectileGlow","projectileHitRadius","projectileMissRadius","projectileFont",
|
||||||
|
"projectileCircleRadius","shakeHit","shakeMiss","shakeDamage","particleVel",
|
||||||
|
"particleSizeMin","particleSizeMax","particleGlow","trailSizeBase","trailSizeInc",
|
||||||
|
"trailGlow","shooterOffset"]) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
for (const k of ["projectileTrailLength","particleHit","particleMiss",
|
||||||
|
"particleDestroySimple","particleDestroyHeavy","particleArmorBreak",
|
||||||
|
"flashDuration","shakeHitDuration","shakeMissDuration","shakeDamageDuration",
|
||||||
|
"trailMaxLength","trailDecay","trailAlpha"]) {
|
||||||
|
S[k] = C[k];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const k of ["missTargetYRange","projectileTrailLength","projectileTrailDecay",
|
||||||
|
"projectileTrailAlpha","projectileCircleAlpha","enemyPulseRate","enemyAlphaBase",
|
||||||
|
"enemyAlphaRange","enemyGlowPulse","particleDecayBase","particleDecayRange",
|
||||||
|
"particleDamping"]) {
|
||||||
|
S[k] = R[k];
|
||||||
|
}
|
||||||
|
|
||||||
|
S.inputShootDelay = T.inputShootDelay;
|
||||||
|
S.glitchDuration = T.glitchDuration;
|
||||||
|
|
||||||
|
applyOverlayScale(W, H);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyOverlayScale(W, H) {
|
||||||
|
const title = document.querySelector(".screen-title");
|
||||||
|
if (title) title.style.fontSize = Math.round(40 * H / 1000) + "px";
|
||||||
|
const subs = document.querySelectorAll(".screen-subtitle");
|
||||||
|
for (let el of subs) el.style.fontSize = Math.round(18 * H / 1000) + "px";
|
||||||
|
|
||||||
|
const overlay = document.getElementById("ui-overlay");
|
||||||
|
if (overlay) {
|
||||||
|
overlay.style.padding = Math.round(12 * H / 1000) + "px " + Math.round(16 * W / 1000) + "px";
|
||||||
|
overlay.style.fontSize = Math.round(14 * H / 1000) + "px";
|
||||||
|
}
|
||||||
|
const inputDisplay = document.getElementById("input-display");
|
||||||
|
if (inputDisplay) {
|
||||||
|
inputDisplay.style.padding = Math.round(14 * H / 1000) + "px " + Math.round(16 * W / 1000) + "px";
|
||||||
|
}
|
||||||
|
const typed = document.getElementById("typed-text");
|
||||||
|
if (typed) {
|
||||||
|
typed.style.fontSize = Math.round(20 * H / 1000) + "px";
|
||||||
|
typed.style.minHeight = Math.round(28 * H / 1000) + "px";
|
||||||
|
}
|
||||||
|
const chForm = document.getElementById("channel-form");
|
||||||
|
if (chForm) {
|
||||||
|
chForm.querySelector(".screen-title").style.fontSize = Math.round(80 * H / 1000) + "px";
|
||||||
|
const inp = chForm.querySelector("input");
|
||||||
|
if (inp) {
|
||||||
|
inp.style.fontSize = Math.round(22 * H / 1000) + "px";
|
||||||
|
inp.style.padding = Math.round(12 * H / 1000) + "px " + Math.round(20 * W / 1000) + "px";
|
||||||
|
inp.style.width = Math.round(300 * W / 1000) + "px";
|
||||||
|
}
|
||||||
|
const btn = chForm.querySelector("button");
|
||||||
|
if (btn) {
|
||||||
|
btn.style.fontSize = Math.round(18 * H / 1000) + "px";
|
||||||
|
btn.style.padding = Math.round(10 * H / 1000) + "px " + Math.round(40 * W / 1000) + "px";
|
||||||
|
btn.style.minWidth = Math.round(260 * W / 1000) + "px";
|
||||||
|
}
|
||||||
|
const link = document.getElementById("game-link");
|
||||||
|
if (link) link.style.fontSize = Math.round(16 * H / 1000) + "px";
|
||||||
|
const note = chForm.querySelector(".obs-note");
|
||||||
|
if (note) note.style.fontSize = Math.round(16 * H / 1000) + "px";
|
||||||
|
}
|
||||||
|
const goTitle = document.getElementById("gameover-title");
|
||||||
|
if (goTitle) goTitle.style.fontSize = Math.round(32 * H / 1000) + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawGrid() {
|
||||||
|
ctx.strokeStyle = CONFIG.colors.primaryGrid;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
const gridSize = S.gridSize;
|
||||||
|
bgOffset = (bgOffset + S.gridSpeed) % gridSize;
|
||||||
|
for (let x = 0; x <= W; x += gridSize) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, 0);
|
||||||
|
ctx.lineTo(x, H);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let y = bgOffset; y <= H; y += gridSize) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, y);
|
||||||
|
ctx.lineTo(W, y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function spawnParticles(x, y, color, count) {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
particles.push(new Particle(x, y, color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerShake(amount, duration) {
|
||||||
|
shakeAmount = amount;
|
||||||
|
shakeTimer = duration;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user