class RiskGame {
constructor() {
this.canvas = null;
this.ctx = null;
this.gameState = 'idle'; // idle, playing, gameover
this.score = 0;
this.lives = 3;
this.level = 1;
this.asteroids = [];
this.projectiles = [];
this.tools = [];
this.lastSpawn = 0;
this.spawnRate = 2000; // ms
this.gameSpeed = 2;
this.riskTypes = [
{
name: 'Срыв дедлайна',
color: '#ff9d00',
speed: 1.5,
health: 1,
points: 100
},
{
name: 'Уход ключевого разработчика',
color: '#ff0000',
speed: 2,
health: 2,
points: 200
},
{
name: 'Изменение требований',
color: '#ff00ff',
speed: 1,
health: 3,
points: 300
},
{
name: 'Бюджетные ограничения',
color: '#9d00ff',
speed: 1.2,
health: 2,
points: 250
},
{
name: 'Технический долг',
color: '#00f3ff',
speed: 0.8,
health: 4,
points: 400
}
];
this.toolTypes = {
mitigation: {
name: 'Митeйшн',
color: '#00ff9d',
cooldown: 1000,
lastUsed: 0
},
transfer: {
name: 'Трансфер',
color: '#009dff',
cooldown: 1500,
lastUsed: 0
},
acceptance: {
name: 'Акцептанс',
color: '#ff9d00',
cooldown: 2000,
lastUsed: 0
},
avoidance: {
name: 'Избежание',
color: '#ff0000',
cooldown: 2500,
lastUsed: 0
}
};
this.init();
}
init() {
this.canvas = document.getElementById('riskCanvas');
if (!this.canvas) return;
this.ctx = this.canvas.getContext('2d');
this.setupEventListeners();
this.setupTools();
this.gameLoop();
}
setupEventListeners() {
// Start game button
document.getElementById('startGame')?.addEventListener('click', () => {
this.start();
});
// Tool buttons
document.querySelectorAll('.tool-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const tool = e.target.dataset.tool;
this.useTool(tool);
});
});
// Keyboard controls
document.addEventListener('keydown', (e) => {
if (this.gameState !== 'playing') return;
switch(e.key) {
case '1':
case 'q':
this.useTool('mitigation');
break;
case '2':
case 'w':
this.useTool('transfer');
break;
case '3':
case 'e':
this.useTool('acceptance');
break;
case '4':
case 'r':
this.useTool('avoidance');
break;
case ' ':
this.shootProjectile();
break;
}
});
// Mouse controls for shooting
this.canvas.addEventListener('click', (e) => {
if (this.gameState !== 'playing') return;
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
this.shootProjectile(x, y);
});
// Canvas resize
window.addEventListener('resize', () => {
this.resizeCanvas();
});
}
setupTools() {
this.tools = Object.keys(this.toolTypes).map(key => ({
type: key,
...this.toolTypes[key],
active: false,
progress: 0
}));
}
resizeCanvas() {
const container = this.canvas.parentElement;
this.canvas.width = container.clientWidth;
this.canvas.height = Math.min(500, container.clientWidth * 0.6);
}
start() {
this.gameState = 'playing';
this.score = 0;
this.lives = 3;
this.level = 1;
this.asteroids = [];
this.projectiles = [];
this.lastSpawn = Date.now();
this.gameSpeed = 2;
this.spawnRate = 2000;
this.updateUI();
// Show game instructions
this.showMessage('Уничтожайте риски! Используйте инструменты для защиты.');
}
gameLoop() {
this.clearCanvas();
if (this.gameState === 'playing') {
this.update();
this.spawnAsteroids();
}
this.draw();
requestAnimationFrame(() => this.gameLoop());
}
clearCanvas() {
this.ctx.fillStyle = '#0a0e17';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Draw starfield
this.ctx.fillStyle = '#ffffff';
for (let i = 0; i < 100; i++) {
const x = Math.random() * this.canvas.width;
const y = Math.random() * this.canvas.height;
const size = Math.random() * 2;
this.ctx.fillRect(x, y, size, size);
}
}
update() {
const currentTime = Date.now();
// Update asteroids
this.asteroids = this.asteroids.filter(asteroid => {
asteroid.y += asteroid.speed * this.gameSpeed;
// Check if asteroid reached bottom
if (asteroid.y > this.canvas.height) {
this.loseLife();
return false;
}
// Check collision with projectiles
for (let i = this.projectiles.length - 1; i >= 0; i--) {
const projectile = this.projectiles[i];
const dx = asteroid.x - projectile.x;
const dy = asteroid.y - projectile.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < asteroid.radius + projectile.radius) {
asteroid.health--;
// Remove projectile
this.projectiles.splice(i, 1);
if (asteroid.health <= 0) {
this.score += asteroid.points;
this.showFloatingText(`+${asteroid.points}`, asteroid.x, asteroid.y, asteroid.color);
// Chance to drop power-up
if (Math.random() < 0.1) {
this.createPowerUp(asteroid.x, asteroid.y);
}
return false;
}
}
}
// Apply active tool effects
this.tools.forEach(tool => {
if (tool.active) {
const effect = this.getToolEffect(tool.type);
if (effect) {
effect(asteroid);
}
}
});
return true;
});
// Update projectiles
this.projectiles = this.projectiles.filter(projectile => {
projectile.y -= projectile.speed;
projectile.lifetime--;
return projectile.y > 0 && projectile.lifetime > 0;
});
// Update tools cooldown
this.tools.forEach(tool => {
if (tool.active) {
tool.progress = (currentTime - tool.lastUsed) / tool.cooldown;
if (tool.progress >= 1) {
tool.active = false;
tool.progress = 0;
}
}
});
// Update power-ups
this.powerUps = (this.powerUps || []).filter(powerUp => {
powerUp.y += 1;
// Check collection by player (bottom of screen)
if (powerUp.y > this.canvas.height - 50) {
this.collectPowerUp(powerUp);
return false;
}
return true;
});
// Update level
if (this.score >= this.level * 1000) {
this.level++;
this.gameSpeed *= 1.1;
this.spawnRate = Math.max(500, this.spawnRate * 0.9);
this.showMessage(`Уровень ${this.level}! Риски ускоряются.`);
}
// Update UI
this.updateUI();
// Check game over
if (this.lives <= 0) {
this.gameOver();
}
}
spawnAsteroids() {
const currentTime = Date.now();
if (currentTime - this.lastSpawn > this.spawnRate) {
const riskType = this.riskTypes[Math.floor(Math.random() * this.riskTypes.length)];
this.asteroids.push({
x: Math.random() * (this.canvas.width - 100) + 50,
y: -50,
radius: 20 + Math.random() * 15,
speed: riskType.speed + Math.random() * 0.5,
health: riskType.health,
maxHealth: riskType.health,
color: riskType.color,
name: riskType.name,
points: riskType.points,
rotation: Math.random() * Math.PI * 2,
rotationSpeed: (Math.random() - 0.5) * 0.1
});
this.lastSpawn = currentTime;
// Occasionally spawn multiple asteroids
if (Math.random() < 0.3) {
setTimeout(() => this.spawnAsteroids(), 500);
}
}
}
createPowerUp(x, y) {
if (!this.powerUps) this.powerUps = [];
const types = ['life', 'score', 'slow', 'rapid'];
const type = types[Math.floor(Math.random() * types.length)];
let color, value;
switch(type) {
case 'life':
color = '#00ff9d';
value = 1;
break;
case 'score':
color = '#ff9d00';
value = 500;
break;
case 'slow':
color = '#009dff';
value = 0.5;
break;
case 'rapid':
color = '#ff00ff';
value = 2;
break;
}
this.powerUps.push({
x,
y,
radius: 10,
type,
color,
value
});
}
collectPowerUp(powerUp) {
switch(powerUp.type) {
case 'life':
this.lives = Math.min(5, this.lives + powerUp.value);
this.showFloatingText('+1 жизнь', powerUp.x, this.canvas.height - 50, powerUp.color);
break;
case 'score':
this.score += powerUp.value;
this.showFloatingText(`+${powerUp.value}`, powerUp.x, this.canvas.height - 50, powerUp.color);
break;
case 'slow':
this.gameSpeed *= powerUp.value;
setTimeout(() => {
this.gameSpeed /= powerUp.value;
}, 5000);
this.showFloatingText('Замедление!', powerUp.x, this.canvas.height - 50, powerUp.color);
break;
case 'rapid':
// Rapid fire for 5 seconds
const originalSpawnRate = this.spawnRate;
this.spawnRate = 300;
setTimeout(() => {
this.spawnRate = originalSpawnRate;
}, 5000);
this.showFloatingText('Рапид огонь!', powerUp.x, this.canvas.height - 50, powerUp.color);
break;
}
}
shootProjectile(x = this.canvas.width / 2, y = this.canvas.height) {
if (this.gameState !== 'playing') return;
this.projectiles.push({
x: x || this.canvas.width / 2,
y: y || this.canvas.height,
radius: 5,
speed: 8,
color: '#00f3ff',
lifetime: 100
});
// Visual feedback
this.showParticles(x, y, 5, '#00f3ff');
}
useTool(toolType) {
if (this.gameState !== 'playing') return;
const tool = this.tools.find(t => t.type === toolType);
if (!tool || tool.active) return;
const currentTime = Date.now();
if (currentTime - tool.lastUsed < tool.cooldown) return;
tool.active = true;
tool.lastUsed = currentTime;
tool.progress = 0;
// Apply tool effect
switch(toolType) {
case 'mitigation':
this.showMessage('Митeйшн: Снижена скорость рисков на 5 секунд');
break;
case 'transfer':
this.showMessage('Трансфер: Риски заморожены на 3 секунды');
break;
case 'acceptance':
this.showMessage('Акцептанс: Восстановлена 1 жизнь');
this.lives = Math.min(5, this.lives + 1);
break;
case 'avoidance':
this.showMessage('Избежание: Уничтожены все мелкие риски');
this.asteroids = this.asteroids.filter(a => a.radius > 25);
break;
}
this.updateUI();
}
getToolEffect(toolType) {
switch(toolType) {
case 'mitigation':
return (asteroid) => {
asteroid.speed *= 0.5;
};
case 'transfer':
return (asteroid) => {
asteroid.speed = 0;
};
default:
return null;
}
}
loseLife() {
this.lives--;
this.showMessage(`Потеряна жизнь! Осталось: ${this.lives}`);
this.updateUI();
// Screen shake effect
this.shakeScreen();
}
shakeScreen() {
const originalX = this.canvas.style.marginLeft;
const originalY = this.canvas.style.marginTop;
let shake = 10;
const shakeInterval = setInterval(() => {
this.canvas.style.marginLeft = `${(Math.random() - 0.5) * shake}px`;
this.canvas.style.marginTop = `${(Math.random() - 0.5) * shake}px`;
shake *= 0.9;
if (shake < 1) {
clearInterval(shakeInterval);
this.canvas.style.marginLeft = originalX;
this.canvas.style.marginTop = originalY;
}
}, 30);
}
showFloatingText(text, x, y, color) {
const floatingText = {
text,
x,
y,
color,
alpha: 1,
velocity: -2
};
if (!this.floatingTexts) this.floatingTexts = [];
this.floatingTexts.push(floatingText);
// Remove after animation
setTimeout(() => {
this.floatingTexts = this.floatingTexts.filter(t => t !== floatingText);
}, 1000);
}
showParticles(x, y, count, color) {
for (let i = 0; i < count; i++) {
const particle = {
x,
y,
vx: (Math.random() - 0.5) * 10,
vy: (Math.random() - 0.5) * 10,
radius: Math.random() * 3 + 1,
color,
life: 1
};
if (!this.particles) this.particles = [];
this.particles.push(particle);
}
}
showMessage(text) {
this.message = {
text,
time: Date.now()
};
setTimeout(() => {
this.message = null;
}, 3000);
}
draw() {
// Draw player base (bottom of screen)
this.ctx.fillStyle = '#00f3ff';
this.ctx.fillRect(this.canvas.width / 2 - 50, this.canvas.height - 20, 100, 20);
// Draw player turret
this.ctx.fillStyle = '#ffffff';
this.ctx.beginPath();
this.ctx.arc(this.canvas.width / 2, this.canvas.height - 20, 15, 0, Math.PI * 2);
this.ctx.fill();
// Draw asteroids
this.asteroids.forEach(asteroid => {
// Asteroid body
this.ctx.save();
this.ctx.translate(asteroid.x, asteroid.y);
this.ctx.rotate(asteroid.rotation);
this.ctx.fillStyle = asteroid.color;
this.ctx.beginPath();
this.ctx.arc(0, 0, asteroid.radius, 0, Math.PI * 2);
this.ctx.fill();
// Health bar
const healthPercent = asteroid.health / asteroid.maxHealth;
this.ctx.fillStyle = '#00ff9d';
this.ctx.fillRect(-asteroid.radius, -asteroid.radius - 10, asteroid.radius * 2 * healthPercent, 5);
// Asteroid name
this.ctx.fillStyle = '#ffffff';
this.ctx.font = '12px Arial';
this.ctx.textAlign = 'center';
this.ctx.fillText(asteroid.name, 0, -asteroid.radius - 15);
this.ctx.restore();
// Update rotation
asteroid.rotation += asteroid.rotationSpeed;
});
// Draw projectiles
this.projectiles.forEach(projectile => {
this.ctx.fillStyle = projectile.color;
this.ctx.beginPath();
this.ctx.arc(projectile.x, projectile.y, projectile.radius, 0, Math.PI * 2);
this.ctx.fill();
});
// Draw power-ups
(this.powerUps || []).forEach(powerUp => {
this.ctx.fillStyle = powerUp.color;
this.ctx.beginPath();
this.ctx.arc(powerUp.x, powerUp.y, powerUp.radius, 0, Math.PI * 2);
this.ctx.fill();
// Icon based on type
this.ctx.fillStyle = '#ffffff';
this.ctx.font = '10px Arial';
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
let icon = '?';
switch(powerUp.type) {
case 'life': icon = '♥'; break;
case 'score': icon = '★'; break;
case 'slow': icon = '⏱'; break;
case 'rapid': icon = '⚡'; break;
}
this.ctx.fillText(icon, powerUp.x, powerUp.y);
});
// Draw particles
(this.particles || []).forEach((particle, index) => {
particle.x += particle.vx;
particle.y += particle.vy;
particle.life -= 0.02;
this.ctx.globalAlpha = particle.life;
this.ctx.fillStyle = particle.color;
this.ctx.beginPath();
this.ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2);
this.ctx.fill();
if (particle.life <= 0) {
this.particles.splice(index, 1);
}
});
this.ctx.globalAlpha = 1;
// Draw floating text
(this.floatingTexts || []).forEach(text => {
text.y += text.velocity;
text.alpha -= 0.01;
this.ctx.globalAlpha = text.alpha;
this.ctx.fillStyle = text.color;
this.ctx.font = 'bold 16px Arial';
this.ctx.textAlign = 'center';
this.ctx.fillText(text.text, text.x, text.y);
});
this.ctx.globalAlpha = 1;
// Draw message
if (this.message) {
const age = Date.now() - this.message.time;
const alpha = age < 2500 ? 1 : 1 - ((age - 2500) / 500);
this.ctx.globalAlpha = alpha;
this.ctx.fillStyle = '#ffffff';
this.ctx.font = 'bold 20px Arial';
this.ctx.textAlign = 'center';
this.ctx.fillText(this.message.text, this.canvas.width / 2, 50);
this.ctx.globalAlpha = 1;
}
// Draw tools cooldown
this.drawTools();
// Draw game state
if (this.gameState === 'gameover') {
this.drawGameOver();
} else if (this.gameState === 'idle') {
this.drawInstructions();
}
}
drawTools() {
const toolWidth = 80;
const startX = (this.canvas.width - (this.tools.length * toolWidth)) / 2;
this.tools.forEach((tool, index) => {
const x = startX + index * toolWidth;
const y = this.canvas.height - 80;
// Tool background
this.ctx.fillStyle = tool.active ? tool.color : '#2a3a5a';
this.ctx.fillRect(x, y, toolWidth - 10, 40);
// Cooldown overlay
if (tool.progress < 1) {
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
this.ctx.fillRect(x, y + (1 - tool.progress) * 40, toolWidth - 10, tool.progress * 40);
// Cooldown percentage
this.ctx.fillStyle = '#ffffff';
this.ctx.font = '12px Arial';
this.ctx.textAlign = 'center';
this.ctx.fillText(
`${Math.round((1 - tool.progress) * 100)}%`,
x + (toolWidth - 10) / 2,
y + 25
);
}
// Tool name
this.ctx.fillStyle = '#ffffff';
this.ctx.font = '10px Arial';
this.ctx.textAlign = 'center';
this.ctx.fillText(tool.name, x + (toolWidth - 10) / 2, y + 15);
// Hotkey
this.ctx.fillStyle = '#00f3ff';
this.ctx.font = 'bold 12px Arial';
this.ctx.fillText(index + 1, x + 10, y + 35);
});
}
drawInstructions() {
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.fillStyle = '#00f3ff';
this.ctx.font = 'bold 24px Arial';
this.ctx.textAlign = 'center';
this.ctx.fillText('СИМУЛЯТОР РИСКОВ', this.canvas.width / 2, 100);
this.ctx.fillStyle = '#ffffff';
this.ctx.font = '16px Arial';
this.ctx.fillText('Защитите проект от падающих рисков!', this.canvas.width / 2, 150);
const instructions = [
'• Кликайте по рискам для уничтожения',
'• Используйте инструменты (1-4) для защиты',
'• Собирайте бонусы для усиления',
'• Не пропустите более 3 рисков'
];
instructions.forEach((text, index) => {
this.ctx.fillText(text, this.canvas.width / 2, 200 + index * 30);
});
this.ctx.fillStyle = '#ff9d00';
this.ctx.font = 'bold 20px Arial';
this.ctx.fillText('Нажмите "Начать тренировку"', this.canvas.width / 2, 350);
}
drawGameOver() {
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.fillStyle = '#ff0000';
this.ctx.font = 'bold 36px Arial';
this.ctx.textAlign = 'center';
this.ctx.fillText('МИССИЯ ПРОВАЛЕНА', this.canvas.width / 2, 150);
this.ctx.fillStyle = '#ffffff';
this.ctx.font = '24px Arial';
this.ctx.fillText(`Счет: ${this.score}`, this.canvas.width / 2, 200);
this.ctx.fillText(`Уровень: ${this.level}`, this.canvas.width / 2, 240);
this.ctx.fillStyle = '#00f3ff';
this.ctx.font = '20px Arial';
this.ctx.fillText('Нажмите "Начать тренировку"', this.canvas.width / 2, 300);
this.ctx.fillText('для новой попытки', this.canvas.width / 2, 330);
// Draw risk management tips based on performance
const tips = [
'Совет: Используйте митейшн для частых рисков',
'Совет: Трансфер эффективен против крупных рисков',
'Совет: Собирайте бонусы для усиления',
'Совет: Чередуйте инструменты для максимального эффекта'
];
this.ctx.fillStyle = '#ff9d00';
this.ctx.font = '16px Arial';
this.ctx.fillText(tips[Math.floor(Math.random() * tips.length)], this.canvas.width / 2, 380);
}
updateUI() {
document.getElementById('score').textContent = this.score;
document.getElementById('lives').textContent = this.lives;
document.getElementById('level').textContent = this.level;
// Update tool buttons
this.tools.forEach(tool => {
const btn = document.querySelector(`.tool-btn[data-tool="${tool.type}"]`);
if (btn) {
btn.disabled = tool.active;
btn.style.opacity = tool.active ? '0.5' : '1';
}
});
}
gameOver() {
this.gameState = 'gameover';
// Show final score
setTimeout(() => {
alert(`Игра окончена!\n\nВаш счет: ${this.score}\nДостигнутый уровень: ${this.level}\n\nПопробуйте снова!`);
}, 500);
}
}
// Initialize game
window.addEventListener('DOMContentLoaded', () => {
window.riskGame = new RiskGame();
});