Spaces:
Running
Running
Upload 3 files
Browse files- index.html +79 -573
- script.js +1034 -0
- style.css +274 -28
index.html
CHANGED
@@ -1,574 +1,80 @@
|
|
1 |
-
<!DOCTYPE html>
|
2 |
-
<html lang="
|
3 |
-
<head>
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
}
|
81 |
-
|
82 |
-
#ui {
|
83 |
-
position: absolute;
|
84 |
-
top: 20px;
|
85 |
-
left: 20px;
|
86 |
-
color: white;
|
87 |
-
font-size: 18px;
|
88 |
-
z-index: 100;
|
89 |
-
}
|
90 |
-
|
91 |
-
#health-bar {
|
92 |
-
width: 200px;
|
93 |
-
height: 20px;
|
94 |
-
background-color: #333;
|
95 |
-
border: 2px solid #555;
|
96 |
-
margin-top: 10px;
|
97 |
-
}
|
98 |
-
|
99 |
-
#health-fill {
|
100 |
-
height: 100%;
|
101 |
-
width: 100%;
|
102 |
-
background-color: #2ecc71;
|
103 |
-
transition: width 0.3s;
|
104 |
-
}
|
105 |
-
|
106 |
-
#game-over {
|
107 |
-
position: absolute;
|
108 |
-
top: 0;
|
109 |
-
left: 0;
|
110 |
-
width: 100%;
|
111 |
-
height: 100%;
|
112 |
-
background-color: rgba(0,0,0,0.8);
|
113 |
-
display: flex;
|
114 |
-
flex-direction: column;
|
115 |
-
justify-content: center;
|
116 |
-
align-items: center;
|
117 |
-
color: white;
|
118 |
-
font-size: 36px;
|
119 |
-
z-index: 200;
|
120 |
-
display: none;
|
121 |
-
}
|
122 |
-
|
123 |
-
#restart-btn {
|
124 |
-
margin-top: 20px;
|
125 |
-
padding: 10px 30px;
|
126 |
-
background-color: #3498db;
|
127 |
-
border: none;
|
128 |
-
color: white;
|
129 |
-
font-size: 18px;
|
130 |
-
cursor: pointer;
|
131 |
-
border-radius: 5px;
|
132 |
-
}
|
133 |
-
|
134 |
-
#restart-btn:hover {
|
135 |
-
background-color: #2980b9;
|
136 |
-
}
|
137 |
-
</style>
|
138 |
-
</head>
|
139 |
-
<body>
|
140 |
-
<div id="game-container">
|
141 |
-
<div id="game-board"></div>
|
142 |
-
<div id="ui">
|
143 |
-
<div>Очки: <span id="score">0</span></div>
|
144 |
-
<div>Жизни: <span id="health">100</span></div>
|
145 |
-
<div id="health-bar">
|
146 |
-
<div id="health-fill"></div>
|
147 |
-
</div>
|
148 |
-
<div>Патроны: <span id="ammo">30</span></div>
|
149 |
-
</div>
|
150 |
-
<div id="game-over">
|
151 |
-
<h1>Игра окончена!</h1>
|
152 |
-
<p>Ваш счет: <span id="final-score">0</span></p>
|
153 |
-
<button id="restart-btn">Играть снова</button>
|
154 |
-
</div>
|
155 |
-
</div>
|
156 |
-
|
157 |
-
<script>
|
158 |
-
document.addEventListener('DOMContentLoaded', () => {
|
159 |
-
// Конфигурация игры
|
160 |
-
const config = {
|
161 |
-
boardSize: 800,
|
162 |
-
tileSize: 40,
|
163 |
-
playerSpeed: 5,
|
164 |
-
enemySpeed: 2,
|
165 |
-
enemySpawnRate: 2000,
|
166 |
-
bulletSpeed: 10,
|
167 |
-
maxAmmo: 30,
|
168 |
-
reloadTime: 2000
|
169 |
-
};
|
170 |
-
|
171 |
-
// Состояние игры
|
172 |
-
const state = {
|
173 |
-
player: {
|
174 |
-
x: 400,
|
175 |
-
y: 400,
|
176 |
-
health: 100,
|
177 |
-
maxHealth: 100,
|
178 |
-
ammo: config.maxAmmo,
|
179 |
-
isReloading: false
|
180 |
-
},
|
181 |
-
enemies: [],
|
182 |
-
bullets: [],
|
183 |
-
score: 0,
|
184 |
-
gameOver: false,
|
185 |
-
lastEnemySpawn: 0,
|
186 |
-
keys: {
|
187 |
-
w: false,
|
188 |
-
a: false,
|
189 |
-
s: false,
|
190 |
-
d: false
|
191 |
-
},
|
192 |
-
mouse: {
|
193 |
-
x: 0,
|
194 |
-
y: 0
|
195 |
-
}
|
196 |
-
};
|
197 |
-
|
198 |
-
// Элементы DOM
|
199 |
-
const gameBoard = document.getElementById('game-board');
|
200 |
-
const scoreElement = document.getElementById('score');
|
201 |
-
const healthElement = document.getElementById('health');
|
202 |
-
const healthFill = document.getElementById('health-fill');
|
203 |
-
const ammoElement = document.getElementById('ammo');
|
204 |
-
const gameOverScreen = document.getElementById('game-over');
|
205 |
-
const finalScoreElement = document.getElementById('final-score');
|
206 |
-
const restartBtn = document.getElementById('restart-btn');
|
207 |
-
|
208 |
-
// Инициализация игрового поля
|
209 |
-
function initGameBoard() {
|
210 |
-
gameBoard.style.width = `${config.boardSize}px`;
|
211 |
-
gameBoard.style.height = `${config.boardSize}px`;
|
212 |
-
|
213 |
-
// Создание плиток
|
214 |
-
const tilesX = config.boardSize / config.tileSize;
|
215 |
-
const tilesY = config.boardSize / config.tileSize;
|
216 |
-
|
217 |
-
for (let y = 0; y < tilesY; y++) {
|
218 |
-
for (let x = 0; x < tilesX; x++) {
|
219 |
-
const tile = document.createElement('div');
|
220 |
-
tile.className = 'tile';
|
221 |
-
tile.style.left = `${x * config.tileSize}px`;
|
222 |
-
tile.style.top = `${y * config.tileSize}px`;
|
223 |
-
|
224 |
-
// Случайные стены
|
225 |
-
if (Math.random() < 0.1 && !(x > tilesX/2 - 3 && x < tilesX/2 + 3 && y > tilesY/2 - 3 && y < tilesY/2 + 3)) {
|
226 |
-
tile.classList.add('wall');
|
227 |
-
}
|
228 |
-
|
229 |
-
gameBoard.appendChild(tile);
|
230 |
-
}
|
231 |
-
}
|
232 |
-
|
233 |
-
// Создание игрока
|
234 |
-
const player = document.createElement('div');
|
235 |
-
player.className = 'player';
|
236 |
-
player.id = 'player';
|
237 |
-
player.style.left = `${state.player.x}px`;
|
238 |
-
player.style.top = `${state.player.y}px`;
|
239 |
-
gameBoard.appendChild(player);
|
240 |
-
}
|
241 |
-
|
242 |
-
// Обработчики событий
|
243 |
-
function setupEventListeners() {
|
244 |
-
// Клавиатура
|
245 |
-
document.addEventListener('keydown', (e) => {
|
246 |
-
if (e.key.toLowerCase() === 'w') state.keys.w = true;
|
247 |
-
if (e.key.toLowerCase() === 'a') state.keys.a = true;
|
248 |
-
if (e.key.toLowerCase() === 's') state.keys.s = true;
|
249 |
-
if (e.key.toLowerCase() === 'd') state.keys.d = true;
|
250 |
-
if (e.key === 'r' && !state.player.isReloading && state.player.ammo < config.maxAmmo) {
|
251 |
-
reload();
|
252 |
-
}
|
253 |
-
});
|
254 |
-
|
255 |
-
document.addEventListener('keyup', (e) => {
|
256 |
-
if (e.key.toLowerCase() === 'w') state.keys.w = false;
|
257 |
-
if (e.key.toLowerCase() === 'a') state.keys.a = false;
|
258 |
-
if (e.key.toLowerCase() === 's') state.keys.s = false;
|
259 |
-
if (e.key.toLowerCase() === 'd') state.keys.d = false;
|
260 |
-
});
|
261 |
-
|
262 |
-
// Мышь
|
263 |
-
gameBoard.addEventListener('mousemove', (e) => {
|
264 |
-
const rect = gameBoard.getBoundingClientRect();
|
265 |
-
state.mouse.x = e.clientX - rect.left;
|
266 |
-
state.mouse.y = e.clientY - rect.top;
|
267 |
-
|
268 |
-
// Поворот игрока к курсору
|
269 |
-
const player = document.getElementById('player');
|
270 |
-
const angle = Math.atan2(state.mouse.y - state.player.y, state.mouse.x - state.player.x) * 180 / Math.PI;
|
271 |
-
player.style.transform = `translate(-50%, -50%) rotate(${angle}deg)`;
|
272 |
-
});
|
273 |
-
|
274 |
-
gameBoard.addEventListener('click', (e) => {
|
275 |
-
if (state.gameOver) return;
|
276 |
-
if (state.player.ammo > 0 && !state.player.isReloading) {
|
277 |
-
shoot();
|
278 |
-
} else if (!state.player.isReloading) {
|
279 |
-
reload();
|
280 |
-
}
|
281 |
-
});
|
282 |
-
|
283 |
-
// Кнопка перезапуска
|
284 |
-
restartBtn.addEventListener('click', restartGame);
|
285 |
-
}
|
286 |
-
|
287 |
-
// Перезарядка
|
288 |
-
function reload() {
|
289 |
-
state.player.isReloading = true;
|
290 |
-
ammoElement.textContent = 'Перезарядка...';
|
291 |
-
|
292 |
-
setTimeout(() => {
|
293 |
-
state.player.ammo = config.maxAmmo;
|
294 |
-
ammoElement.textContent = state.player.ammo;
|
295 |
-
state.player.isReloading = false;
|
296 |
-
}, config.reloadTime);
|
297 |
-
}
|
298 |
-
|
299 |
-
// Выстрел
|
300 |
-
function shoot() {
|
301 |
-
if (state.player.ammo <= 0 || state.player.isReloading) return;
|
302 |
-
|
303 |
-
state.player.ammo--;
|
304 |
-
ammoElement.textContent = state.player.ammo;
|
305 |
-
|
306 |
-
const angle = Math.atan2(state.mouse.y - state.player.y, state.mouse.x - state.player.x);
|
307 |
-
|
308 |
-
const bullet = {
|
309 |
-
x: state.player.x,
|
310 |
-
y: state.player.y,
|
311 |
-
dx: Math.cos(angle) * config.bulletSpeed,
|
312 |
-
dy: Math.sin(angle) * config.bulletSpeed
|
313 |
-
};
|
314 |
-
|
315 |
-
state.bullets.push(bullet);
|
316 |
-
|
317 |
-
// Создание элемента пули
|
318 |
-
const bulletElement = document.createElement('div');
|
319 |
-
bulletElement.className = 'bullet';
|
320 |
-
bulletElement.style.left = `${bullet.x}px`;
|
321 |
-
bulletElement.style.top = `${bullet.y}px`;
|
322 |
-
bulletElement.id = `bullet-${state.bullets.length - 1}`;
|
323 |
-
gameBoard.appendChild(bulletElement);
|
324 |
-
|
325 |
-
// Звук выстрела
|
326 |
-
const shootSound = new Audio('data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YU...');
|
327 |
-
shootSound.volume = 0.2;
|
328 |
-
shootSound.play().catch(e => console.log('Audio error:', e));
|
329 |
-
}
|
330 |
-
|
331 |
-
// Спавн врагов
|
332 |
-
function spawnEnemy() {
|
333 |
-
const side = Math.floor(Math.random() * 4);
|
334 |
-
let x, y;
|
335 |
-
|
336 |
-
switch (side) {
|
337 |
-
case 0: // верх
|
338 |
-
x = Math.random() * config.boardSize;
|
339 |
-
y = -30;
|
340 |
-
break;
|
341 |
-
case 1: // право
|
342 |
-
x = config.boardSize + 30;
|
343 |
-
y = Math.random() * config.boardSize;
|
344 |
-
break;
|
345 |
-
case 2: // низ
|
346 |
-
x = Math.random() * config.boardSize;
|
347 |
-
y = config.boardSize + 30;
|
348 |
-
break;
|
349 |
-
case 3: // лево
|
350 |
-
x = -30;
|
351 |
-
y = Math.random() * config.boardSize;
|
352 |
-
break;
|
353 |
-
}
|
354 |
-
|
355 |
-
const enemy = {
|
356 |
-
x,
|
357 |
-
y,
|
358 |
-
id: Date.now()
|
359 |
-
};
|
360 |
-
|
361 |
-
state.enemies.push(enemy);
|
362 |
-
|
363 |
-
// Создание элемента врага
|
364 |
-
const enemyElement = document.createElement('div');
|
365 |
-
enemyElement.className = 'enemy';
|
366 |
-
enemyElement.style.left = `${enemy.x}px`;
|
367 |
-
enemyElement.style.top = `${enemy.y}px`;
|
368 |
-
enemyElement.id = `enemy-${enemy.id}`;
|
369 |
-
gameBoard.appendChild(enemyElement);
|
370 |
-
}
|
371 |
-
|
372 |
-
// Обновление игры
|
373 |
-
function update() {
|
374 |
-
if (state.gameOver) return;
|
375 |
-
|
376 |
-
// Движение игрока
|
377 |
-
let dx = 0, dy = 0;
|
378 |
-
if (state.keys.w) dy -= config.playerSpeed;
|
379 |
-
if (state.keys.s) dy += config.playerSpeed;
|
380 |
-
if (state.keys.a) dx -= config.playerSpeed;
|
381 |
-
if (state.keys.d) dx += config.playerSpeed;
|
382 |
-
|
383 |
-
// Нормализация диагонального движения
|
384 |
-
if (dx !== 0 && dy !== 0) {
|
385 |
-
dx *= 0.7071;
|
386 |
-
dy *= 0.7071;
|
387 |
-
}
|
388 |
-
|
389 |
-
// Проверка границ
|
390 |
-
const newX = state.player.x + dx;
|
391 |
-
const newY = state.player.y + dy;
|
392 |
-
|
393 |
-
if (newX > 0 && newX < config.boardSize) state.player.x = newX;
|
394 |
-
if (newY > 0 && newY < config.boardSize) state.player.y = newY;
|
395 |
-
|
396 |
-
// Обновление позиции игрока
|
397 |
-
const player = document.getElementById('player');
|
398 |
-
player.style.left = `${state.player.x}px`;
|
399 |
-
player.style.top = `${state.player.y}px`;
|
400 |
-
|
401 |
-
// Спавн врагов
|
402 |
-
const now = Date.now();
|
403 |
-
if (now - state.lastEnemySpawn > config.enemySpawnRate) {
|
404 |
-
spawnEnemy();
|
405 |
-
state.lastEnemySpawn = now;
|
406 |
-
|
407 |
-
// Увеличиваем сложность
|
408 |
-
if (state.score > 0 && state.score % 10 === 0) {
|
409 |
-
config.enemySpawnRate = Math.max(500, config.enemySpawnRate - 100);
|
410 |
-
}
|
411 |
-
}
|
412 |
-
|
413 |
-
// Движение врагов
|
414 |
-
state.enemies.forEach(enemy => {
|
415 |
-
// Движение к игроку
|
416 |
-
const angle = Math.atan2(state.player.y - enemy.y, state.player.x - enemy.x);
|
417 |
-
enemy.x += Math.cos(angle) * config.enemySpeed;
|
418 |
-
enemy.y += Math.sin(angle) * config.enemySpeed;
|
419 |
-
|
420 |
-
// Обновление позиции
|
421 |
-
const enemyElement = document.getElementById(`enemy-${enemy.id}`);
|
422 |
-
if (enemyElement) {
|
423 |
-
enemyElement.style.left = `${enemy.x}px`;
|
424 |
-
enemyElement.style.top = `${enemy.y}px`;
|
425 |
-
}
|
426 |
-
});
|
427 |
-
|
428 |
-
// Движение пуль
|
429 |
-
state.bullets.forEach((bullet, index) => {
|
430 |
-
bullet.x += bullet.dx;
|
431 |
-
bullet.y += bullet.dy;
|
432 |
-
|
433 |
-
// Обновление позиции
|
434 |
-
const bulletElement = document.getElementById(`bullet-${index}`);
|
435 |
-
if (bulletElement) {
|
436 |
-
bulletElement.style.left = `${bullet.x}px`;
|
437 |
-
bulletElement.style.top = `${bullet.y}px`;
|
438 |
-
}
|
439 |
-
|
440 |
-
// Проверка выхода за границы
|
441 |
-
if (bullet.x < 0 || bullet.x > config.boardSize || bullet.y < 0 || bullet.y > config.boardSize) {
|
442 |
-
if (bulletElement) bulletElement.remove();
|
443 |
-
state.bullets.splice(index, 1);
|
444 |
-
}
|
445 |
-
});
|
446 |
-
|
447 |
-
// Проверка столкновений
|
448 |
-
checkCollisions();
|
449 |
-
|
450 |
-
// Обновление UI
|
451 |
-
updateUI();
|
452 |
-
|
453 |
-
// Рекурсивный вызов
|
454 |
-
requestAnimationFrame(update);
|
455 |
-
}
|
456 |
-
|
457 |
-
// Проверка столкновений
|
458 |
-
function checkCollisions() {
|
459 |
-
// Пули с врагами
|
460 |
-
state.bullets.forEach((bullet, bulletIndex) => {
|
461 |
-
state.enemies.forEach((enemy, enemyIndex) => {
|
462 |
-
const distance = Math.sqrt(
|
463 |
-
Math.pow(bullet.x - enemy.x, 2) +
|
464 |
-
Math.pow(bullet.y - enemy.y, 2)
|
465 |
-
);
|
466 |
-
|
467 |
-
if (distance < 20) { // Столкновение
|
468 |
-
// Удаление врага
|
469 |
-
const enemyElement = document.getElementById(`enemy-${enemy.id}`);
|
470 |
-
if (enemyElement) enemyElement.remove();
|
471 |
-
state.enemies.splice(enemyIndex, 1);
|
472 |
-
|
473 |
-
// Удаление пули
|
474 |
-
const bulletElement = document.getElementById(`bullet-${bulletIndex}`);
|
475 |
-
if (bulletElement) bulletElement.remove();
|
476 |
-
state.bullets.splice(bulletIndex, 1);
|
477 |
-
|
478 |
-
// Увеличение счета
|
479 |
-
state.score += 1;
|
480 |
-
|
481 |
-
// Звук попадания
|
482 |
-
const hitSound = new Audio('data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YU...');
|
483 |
-
hitSound.volume = 0.3;
|
484 |
-
hitSound.play().catch(e => console.log('Audio error:', e));
|
485 |
-
}
|
486 |
-
});
|
487 |
-
});
|
488 |
-
|
489 |
-
// Игрок с врагами
|
490 |
-
state.enemies.forEach(enemy => {
|
491 |
-
const distance = Math.sqrt(
|
492 |
-
Math.pow(state.player.x - enemy.x, 2) +
|
493 |
-
Math.pow(state.player.y - enemy.y, 2)
|
494 |
-
);
|
495 |
-
|
496 |
-
if (distance < 25) { // Столкновение
|
497 |
-
state.player.health -= 10;
|
498 |
-
|
499 |
-
if (state.player.health <= 0) {
|
500 |
-
state.player.health = 0;
|
501 |
-
gameOver();
|
502 |
-
}
|
503 |
-
|
504 |
-
// Эффект ��олучения урона
|
505 |
-
const player = document.getElementById('player');
|
506 |
-
player.style.backgroundColor = '#ff0000';
|
507 |
-
setTimeout(() => {
|
508 |
-
player.style.backgroundColor = '#3498db';
|
509 |
-
}, 100);
|
510 |
-
}
|
511 |
-
});
|
512 |
-
}
|
513 |
-
|
514 |
-
// Обновление UI
|
515 |
-
function updateUI() {
|
516 |
-
scoreElement.textContent = state.score;
|
517 |
-
healthElement.textContent = state.player.health;
|
518 |
-
healthFill.style.width = `${(state.player.health / state.player.maxHealth) * 100}%`;
|
519 |
-
|
520 |
-
if (!state.player.isReloading) {
|
521 |
-
ammoElement.textContent = state.player.ammo;
|
522 |
-
}
|
523 |
-
}
|
524 |
-
|
525 |
-
// Конец игры
|
526 |
-
function gameOver() {
|
527 |
-
state.gameOver = true;
|
528 |
-
finalScoreElement.textContent = state.score;
|
529 |
-
gameOverScreen.style.display = 'flex';
|
530 |
-
}
|
531 |
-
|
532 |
-
// Перезапуск игры
|
533 |
-
function restartGame() {
|
534 |
-
// Очистка
|
535 |
-
gameBoard.innerHTML = '';
|
536 |
-
state.enemies = [];
|
537 |
-
state.bullets = [];
|
538 |
-
|
539 |
-
// Сброс состояния
|
540 |
-
state.player = {
|
541 |
-
x: 400,
|
542 |
-
y: 400,
|
543 |
-
health: 100,
|
544 |
-
maxHealth: 100,
|
545 |
-
ammo: config.maxAmmo,
|
546 |
-
isReloading: false
|
547 |
-
};
|
548 |
-
state.score = 0;
|
549 |
-
state.gameOver = false;
|
550 |
-
state.lastEnemySpawn = 0;
|
551 |
-
|
552 |
-
// Сброс сложности
|
553 |
-
config.enemySpawnRate = 2000;
|
554 |
-
|
555 |
-
// Скрыть экран окончания
|
556 |
-
gameOverScreen.style.display = 'none';
|
557 |
-
|
558 |
-
// Переинициализация
|
559 |
-
initGameBoard();
|
560 |
-
update();
|
561 |
-
}
|
562 |
-
|
563 |
-
// Запуск игры
|
564 |
-
function startGame() {
|
565 |
-
initGameBoard();
|
566 |
-
setupEventListeners();
|
567 |
-
update();
|
568 |
-
}
|
569 |
-
|
570 |
-
startGame();
|
571 |
-
});
|
572 |
-
</script>
|
573 |
-
</body>
|
574 |
</html>
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="ru">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Ковбои Удачи</title>
|
7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
9 |
+
<link href="https://fonts.googleapis.com/css2?family=Merriweather:ital,wght@0,400;0,700;1,400&family=Rye&display=swap" rel="stylesheet">
|
10 |
+
<link rel="stylesheet" href="style.css">
|
11 |
+
</head>
|
12 |
+
<body>
|
13 |
+
<div id="game-container">
|
14 |
+
<h1>Ковбои Удачи</h1>
|
15 |
+
|
16 |
+
<!-- Постоянно видимая информация -->
|
17 |
+
<div id="status-bar">
|
18 |
+
<div>Деньги: $<span id="money">100</span></div>
|
19 |
+
<div>
|
20 |
+
Общие Характеристики Отряда:
|
21 |
+
<span id="squad-stats">Сила: 0, Ловкость: 0, Меткость: 0, Харизма: 0</span>
|
22 |
+
</div>
|
23 |
+
<div>Снаряжение: <span id="equipment-summary">Ничего</span></div>
|
24 |
+
</div>
|
25 |
+
|
26 |
+
<!-- Основная игровая область -->
|
27 |
+
<div id="main-content">
|
28 |
+
<!-- Сюда будет загружаться контент разных фаз игры -->
|
29 |
+
<div id="phase-hire">
|
30 |
+
<h2>Нанять Ковбоев</h2>
|
31 |
+
<ul id="hire-list"></ul>
|
32 |
+
<button id="go-to-equipment-btn">Перейти к Снаряжению</button>
|
33 |
+
</div>
|
34 |
+
|
35 |
+
<div id="phase-equip" style="display: none;">
|
36 |
+
<h2>Купить Снаряжение</h2>
|
37 |
+
<ul id="shop-list"></ul>
|
38 |
+
<p>Ваш Отряд:</p>
|
39 |
+
<ul id="squad-manage-list"></ul>
|
40 |
+
<button id="go-to-plan-btn">Выбрать План</button>
|
41 |
+
</div>
|
42 |
+
|
43 |
+
<div id="phase-plan" style="display: none;">
|
44 |
+
<h2>Выбрать План Ограбления</h2>
|
45 |
+
<ul id="plan-list"></ul>
|
46 |
+
</div>
|
47 |
+
|
48 |
+
<div id="phase-robbery" style="display: none;">
|
49 |
+
<h2>Ограбление!</h2>
|
50 |
+
<div id="squad-health-display">
|
51 |
+
<!-- Здоровье отряда будет здесь -->
|
52 |
+
</div>
|
53 |
+
<div id="robbery-log">
|
54 |
+
<p id="event-description">Описание события...</p>
|
55 |
+
</div>
|
56 |
+
<div id="choices">
|
57 |
+
<!-- Варианты действий -->
|
58 |
+
</div>
|
59 |
+
<p id="roll-result-display" style="margin-top: 15px; font-weight: bold;"></p>
|
60 |
+
</div>
|
61 |
+
|
62 |
+
<div id="phase-results" style="display: none;">
|
63 |
+
<h2>Результаты Ограбления</h2>
|
64 |
+
<p id="result-message"></p>
|
65 |
+
<p>Получено опыта: <span id="xp-gained">0</span></p>
|
66 |
+
<button id="continue-game-btn">Продолжить с этим отрядом</button>
|
67 |
+
<button id="new-game-btn">Начать Новую Игру</button>
|
68 |
+
</div>
|
69 |
+
</div>
|
70 |
+
|
71 |
+
<div id="game-over" style="display: none; color: red; text-align: center;">
|
72 |
+
<h2>ИГРА ОКОНЧЕНА</h2>
|
73 |
+
<p>Весь ваш отряд погиб или был пойман.</p>
|
74 |
+
<button id="restart-game-btn">Начать Сначала</button>
|
75 |
+
</div>
|
76 |
+
</div>
|
77 |
+
|
78 |
+
<script src="script.js"></script>
|
79 |
+
</body>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
80 |
</html>
|
script.js
ADDED
@@ -0,0 +1,1034 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
document.addEventListener('DOMContentLoaded', () => {
|
2 |
+
// --- Глобальные переменные состояния игры ---
|
3 |
+
let playerMoney = 150; // Start with slightly more money
|
4 |
+
let squad = []; // Массив объектов ковбоев в отряде
|
5 |
+
let availableCowboys = []; // Ковбои, доступные для найма
|
6 |
+
let shopItems = []; // Предметы в магазине
|
7 |
+
let availablePlans = []; // Доступные планы ограблений
|
8 |
+
let currentPlan = null; // Выбранный план
|
9 |
+
let currentRobberyEvent = null; // Текущее событие в ограблении
|
10 |
+
let currentRobberyProgress = 0; // Условный прогресс (можно расширить)
|
11 |
+
let gameState = 'hire'; // Текущая фаза: hire, equip, plan, robbery, results, gameover
|
12 |
+
|
13 |
+
// --- Элементы DOM ---
|
14 |
+
const moneyEl = document.getElementById('money');
|
15 |
+
const squadStatsEl = document.getElementById('squad-stats');
|
16 |
+
const equipmentSummaryEl = document.getElementById('equipment-summary');
|
17 |
+
const mainContentEl = document.getElementById('main-content');
|
18 |
+
const hireListEl = document.getElementById('hire-list');
|
19 |
+
const shopListEl = document.getElementById('shop-list');
|
20 |
+
const squadManageListEl = document.getElementById('squad-manage-list');
|
21 |
+
const planListEl = document.getElementById('plan-list');
|
22 |
+
const squadHealthDisplayEl = document.getElementById('squad-health-display');
|
23 |
+
const eventDescriptionEl = document.getElementById('event-description');
|
24 |
+
const choicesEl = document.getElementById('choices');
|
25 |
+
const rollResultDisplayEl = document.getElementById('roll-result-display');
|
26 |
+
const resultMessageEl = document.getElementById('result-message');
|
27 |
+
const xpGainedEl = document.getElementById('xp-gained');
|
28 |
+
const gameOverEl = document.getElementById('game-over');
|
29 |
+
|
30 |
+
// --- Кнопки навигации/действий ---
|
31 |
+
const goToEquipmentBtn = document.getElementById('go-to-equipment-btn');
|
32 |
+
const goToPlanBtn = document.getElementById('go-to-plan-btn');
|
33 |
+
const continueGameBtn = document.getElementById('continue-game-btn');
|
34 |
+
const newGameBtn = document.getElementById('new-game-btn');
|
35 |
+
const restartGameBtn = document.getElementById('restart-game-btn'); // Из GameOver
|
36 |
+
|
37 |
+
// --- Игровые Константы и Настройки ---
|
38 |
+
const cowboyNames = ["Джед", "Билли", "Сэм", "Клэй", "Дасти", "Хосе", "Уайетт", "Док", "Барт", "Коул"]; // Added more names
|
39 |
+
const cowboyStats = ["strength", "agility", "marksmanship", "charisma"]; // Сила, Ловкость, Меткость, Харизма
|
40 |
+
const itemNames = ["Хороший Револьвер", "Винтовка", "Динамит", "Аптечка", "Отмычки", "Бронежилет", "Шляпа Удачи"]; // Added another item
|
41 |
+
const planNames = ["Ограбление Почтового Вагона", "Нападение на Мосту", "Засада в Каньоне", "Тихое Проникновение"]; // Added another plan
|
42 |
+
const MAX_LEVEL = 10;
|
43 |
+
const XP_PER_LEVEL = 100;
|
44 |
+
const DICE_SIDES = 10; // Используем D10 для проверок
|
45 |
+
|
46 |
+
// --- Основные Функции Игры ---
|
47 |
+
|
48 |
+
function initGame() {
|
49 |
+
playerMoney = 300; // Начальные деньги
|
50 |
+
squad = [];
|
51 |
+
availableCowboys = [];
|
52 |
+
shopItems = [];
|
53 |
+
availablePlans = [];
|
54 |
+
currentPlan = null;
|
55 |
+
currentRobberyEvent = null;
|
56 |
+
currentRobberyProgress = 0;
|
57 |
+
gameState = 'hire';
|
58 |
+
gameOverEl.style.display = 'none';
|
59 |
+
mainContentEl.style.display = 'block';
|
60 |
+
|
61 |
+
generateInitialData();
|
62 |
+
updateStatusBar();
|
63 |
+
switchPhase('hire'); // Ensure starting phase is rendered correctly
|
64 |
+
}
|
65 |
+
|
66 |
+
function generateInitialData() {
|
67 |
+
// Генерируем ковбоев для найма
|
68 |
+
availableCowboys = [];
|
69 |
+
for (let i = 0; i < 5; i++) {
|
70 |
+
availableCowboys.push(generateCowboy());
|
71 |
+
}
|
72 |
+
|
73 |
+
// Генерируем предметы для магазина
|
74 |
+
shopItems = [];
|
75 |
+
for (let i = 0; i < 4; i++) {
|
76 |
+
// Ensure variety by removing chosen names temporarily
|
77 |
+
let tempItemNames = [...itemNames];
|
78 |
+
const itemName = tempItemNames.splice(Math.floor(Math.random() * tempItemNames.length), 1)[0];
|
79 |
+
shopItems.push(generateItem(itemName));
|
80 |
+
}
|
81 |
+
// Add specific items if desired
|
82 |
+
if (!shopItems.some(item => item.name === "Аптечка")) {
|
83 |
+
shopItems.push(generateItem("Аптечка"));
|
84 |
+
}
|
85 |
+
if (!shopItems.some(item => item.name === "Динамит")) {
|
86 |
+
shopItems.push(generateItem("Динамит"));
|
87 |
+
}
|
88 |
+
|
89 |
+
|
90 |
+
// Генери��уем планы
|
91 |
+
availablePlans = [];
|
92 |
+
let tempPlanNames = [...planNames];
|
93 |
+
for (let i = 0; i < 3; i++) {
|
94 |
+
if (tempPlanNames.length === 0) break; // Avoid errors if not enough names
|
95 |
+
const planName = tempPlanNames.splice(Math.floor(Math.random() * tempPlanNames.length), 1)[0];
|
96 |
+
availablePlans.push(generatePlan(planName, i));
|
97 |
+
}
|
98 |
+
}
|
99 |
+
|
100 |
+
// --- Генерация Случайных Данных ---
|
101 |
+
|
102 |
+
function generateCowboy() {
|
103 |
+
// Generate a 3-character alphanumeric suffix
|
104 |
+
const randomSuffix = Math.random().toString(36).substring(2, 5);
|
105 |
+
const name = cowboyNames[Math.floor(Math.random() * cowboyNames.length)] + " " + randomSuffix; // Example: "Уайетт 5xs"
|
106 |
+
|
107 |
+
const stats = {};
|
108 |
+
let totalStatPoints = 10 + Math.floor(Math.random() * 12); // Slightly wider range
|
109 |
+
cowboyStats.forEach(stat => stats[stat] = 1);
|
110 |
+
totalStatPoints -= cowboyStats.length;
|
111 |
+
while (totalStatPoints > 0) {
|
112 |
+
stats[cowboyStats[Math.floor(Math.random() * cowboyStats.length)]]++;
|
113 |
+
totalStatPoints--;
|
114 |
+
}
|
115 |
+
|
116 |
+
const level = 1;
|
117 |
+
const xp = 0;
|
118 |
+
const maxHealth = 50 + stats.strength * 5 + Math.floor(Math.random() * 15); // Slightly higher health potential
|
119 |
+
// Adjusted cost calculation
|
120 |
+
const cost = 20 + Object.values(stats).reduce((a, b) => a + b, 0) * 3 + Math.floor(maxHealth / 8);
|
121 |
+
|
122 |
+
return {
|
123 |
+
id: Date.now() + Math.random(),
|
124 |
+
name: name,
|
125 |
+
stats: stats,
|
126 |
+
health: maxHealth,
|
127 |
+
maxHealth: maxHealth,
|
128 |
+
level: level,
|
129 |
+
xp: xp,
|
130 |
+
cost: cost,
|
131 |
+
weapon: null,
|
132 |
+
equipment: []
|
133 |
+
};
|
134 |
+
}
|
135 |
+
|
136 |
+
function generateItem(baseItemName = null) {
|
137 |
+
// If no name provided, pick one randomly
|
138 |
+
const baseItem = baseItemName || itemNames[Math.floor(Math.random() * itemNames.length)];
|
139 |
+
let cost = 15 + Math.floor(Math.random() * 50);
|
140 |
+
let type = "equipment";
|
141 |
+
let effect = {};
|
142 |
+
|
143 |
+
switch (baseItem) {
|
144 |
+
case "Хороший Револьвер":
|
145 |
+
effect = { marksmanship_bonus: 2 + Math.floor(Math.random() * 3) }; // 2-4
|
146 |
+
type = "weapon";
|
147 |
+
cost += 25;
|
148 |
+
break;
|
149 |
+
case "Винтовка":
|
150 |
+
effect = { marksmanship_bonus: 4 + Math.floor(Math.random() * 4) }; // 4-7
|
151 |
+
type = "weapon";
|
152 |
+
cost += 45;
|
153 |
+
break;
|
154 |
+
case "Динамит":
|
155 |
+
effect = { demolition_chance: 0.25 }; // +25%
|
156 |
+
type = "consumable";
|
157 |
+
cost += 20;
|
158 |
+
break;
|
159 |
+
case "Аптечка":
|
160 |
+
effect = { health: 25 + Math.floor(Math.random() * 16) }; // 25-40 health
|
161 |
+
type = "consumable";
|
162 |
+
cost += 15;
|
163 |
+
break;
|
164 |
+
case "Отмычки":
|
165 |
+
effect = { lockpick_chance: 0.20 }; // +20%
|
166 |
+
type = "equipment";
|
167 |
+
cost += 30;
|
168 |
+
break;
|
169 |
+
case "Бронежилет":
|
170 |
+
effect = { damage_reduction: 0.15 }; // 15% reduction
|
171 |
+
type = "equipment";
|
172 |
+
cost += 40;
|
173 |
+
break;
|
174 |
+
case "Шляпа Удачи":
|
175 |
+
effect = { charisma_bonus: 1, luck_bonus: 0.05 }; // +1 Charisma, +5% general luck (need to implement luck)
|
176 |
+
type = "equipment";
|
177 |
+
cost += 25;
|
178 |
+
break;
|
179 |
+
}
|
180 |
+
|
181 |
+
return {
|
182 |
+
id: Date.now() + Math.random(),
|
183 |
+
name: baseItem,
|
184 |
+
type: type,
|
185 |
+
effect: effect,
|
186 |
+
cost: cost
|
187 |
+
};
|
188 |
+
}
|
189 |
+
|
190 |
+
function generatePlan(planName, index) {
|
191 |
+
const difficulty = 40 + Math.floor(Math.random() * 60) + index * 15; // Base difficulty + random + index scaling
|
192 |
+
const potentialReward = 80 + Math.floor(Math.random() * 120) + difficulty * 2.5; // More varied reward
|
193 |
+
|
194 |
+
// Simplified event structure - can be vastly expanded
|
195 |
+
const events = [
|
196 |
+
{
|
197 |
+
description: `Подход к поезду (${planName}). Охрана патрулирует. Ваши действия?`,
|
198 |
+
choices: [
|
199 |
+
{ text: "Прокрасться мимо (Ловкость)", requiredStat: "agility", difficulty: difficulty * 0.9 },
|
200 |
+
{ text: "Создать отвлекающий шум (Харизма)", requiredStat: "charisma", difficulty: difficulty * 1.0 },
|
201 |
+
{ text: "Устранить охранника тихо (Меткость?)", requiredStat: "marksmanship", difficulty: difficulty * 1.1 }, // Risky use of Marksmanship
|
202 |
+
],
|
203 |
+
successReward: { progress: 1 },
|
204 |
+
failurePenalty: { health: 10 }
|
205 |
+
},
|
206 |
+
{
|
207 |
+
description: "Нужно проникнуть в вагон с ценностями.",
|
208 |
+
choices: [
|
209 |
+
{ text: "Взломать замок (Ловкость + Отмычки?)", requiredStat: "agility", difficulty: difficulty * 1.0 },
|
210 |
+
{ text: "Выбить дверь (Сила)", requiredStat: "strength", difficulty: difficulty * 0.9 },
|
211 |
+
{ text: "Использовать Динамит? (Сила + Динамит?)", requiredStat: "strength", difficulty: difficulty * 0.7 } // Easier but noisy & uses item
|
212 |
+
],
|
213 |
+
successReward: { progress: 1, money: potentialReward * 0.1 }, // Small initial reward
|
214 |
+
failurePenalty: { health: 15, money: -15 }
|
215 |
+
},
|
216 |
+
{
|
217 |
+
description: "Внутри! Забрать добычу и быстро уходить!",
|
218 |
+
choices: [
|
219 |
+
{ text: "Хватать все и бежать (Ловкость)", requiredStat: "agility", difficulty: difficulty * 1.1 },
|
220 |
+
{ text: "Прикрывать отход (Меткость)", requiredStat: "marksmanship", difficulty: difficulty * 1.0 },
|
221 |
+
{ text: "Забаррикадировать дверь (Сила)", requiredStat: "strength", difficulty: difficulty * 1.0 }
|
222 |
+
],
|
223 |
+
successReward: { money: potentialReward * 0.9, xp: 50 + difficulty / 2, progress: 1 }, // Main reward + scaled XP
|
224 |
+
failurePenalty: { health: 25, money: -potentialReward * 0.3 }
|
225 |
+
}
|
226 |
+
];
|
227 |
+
|
228 |
+
return {
|
229 |
+
id: Date.now() + Math.random(),
|
230 |
+
name: planName,
|
231 |
+
description: `Сложность: ~${difficulty}, Награда: ~$${Math.round(potentialReward)}`,
|
232 |
+
baseDifficulty: difficulty,
|
233 |
+
potentialReward: Math.round(potentialReward),
|
234 |
+
events: events
|
235 |
+
};
|
236 |
+
}
|
237 |
+
|
238 |
+
function getRobberyEvent() {
|
239 |
+
if (currentPlan && currentPlan.events.length > currentRobberyProgress) {
|
240 |
+
return currentPlan.events[currentRobberyProgress];
|
241 |
+
}
|
242 |
+
return null;
|
243 |
+
}
|
244 |
+
|
245 |
+
|
246 |
+
// --- Обновление Интерфейса ---
|
247 |
+
|
248 |
+
function updateStatusBar() {
|
249 |
+
moneyEl.textContent = playerMoney;
|
250 |
+
|
251 |
+
const totalStats = { strength: 0, agility: 0, marksmanship: 0, charisma: 0 };
|
252 |
+
let equipmentText = [];
|
253 |
+
let consumableText = []; // Separate consumables
|
254 |
+
|
255 |
+
squad.forEach(cowboy => {
|
256 |
+
// Base stats
|
257 |
+
Object.keys(totalStats).forEach(stat => {
|
258 |
+
totalStats[stat] += cowboy.stats[stat] || 0; // Ensure stat exists
|
259 |
+
});
|
260 |
+
|
261 |
+
// Bonuses from weapon
|
262 |
+
if (cowboy.weapon) {
|
263 |
+
equipmentText.push(cowboy.weapon.name);
|
264 |
+
Object.keys(totalStats).forEach(stat => {
|
265 |
+
if (cowboy.weapon.effect[`${stat}_bonus`]) {
|
266 |
+
totalStats[stat] += cowboy.weapon.effect[`${stat}_bonus`];
|
267 |
+
}
|
268 |
+
});
|
269 |
+
}
|
270 |
+
|
271 |
+
// Bonuses from equipment and list consumables
|
272 |
+
cowboy.equipment.forEach(item => {
|
273 |
+
if (item.type === 'consumable') {
|
274 |
+
// Count consumables instead of just listing names once
|
275 |
+
let existing = consumableText.find(c => c.name === item.name);
|
276 |
+
if (existing) {
|
277 |
+
existing.count++;
|
278 |
+
} else {
|
279 |
+
consumableText.push({ name: item.name, count: 1 });
|
280 |
+
}
|
281 |
+
} else {
|
282 |
+
equipmentText.push(item.name); // Add other equipment names
|
283 |
+
}
|
284 |
+
// Apply stat bonuses from all equipment/consumables (if any)
|
285 |
+
Object.keys(totalStats).forEach(stat => {
|
286 |
+
if (item.effect && item.effect[`${stat}_bonus`]) {
|
287 |
+
totalStats[stat] += item.effect[`${stat}_bonus`];
|
288 |
+
}
|
289 |
+
});
|
290 |
+
});
|
291 |
+
});
|
292 |
+
|
293 |
+
// --- Updated line with emojis for total stats ---
|
294 |
+
squadStatsEl.innerHTML = `💪С ${totalStats.strength} ✨Л ${totalStats.agility} 🎯М ${totalStats.marksmanship} 😊Х ${totalStats.charisma}`;
|
295 |
+
// --- End of updated line --- (Used for spacing)
|
296 |
+
|
297 |
+
// Combine and display equipment/consumables summary
|
298 |
+
const uniqueEquipment = [...new Set(equipmentText)];
|
299 |
+
let summary = uniqueEquipment.slice(0, 2).join(', '); // Show max 2 permanent items
|
300 |
+
if (uniqueEquipment.length > 2) summary += '...';
|
301 |
+
|
302 |
+
if (consumableText.length > 0) {
|
303 |
+
// Format consumables as "Name(count)"
|
304 |
+
let consumableSummary = consumableText
|
305 |
+
.map(c => `${c.name}(${c.count})`)
|
306 |
+
.slice(0, 2) // Show max 2 types of consumables
|
307 |
+
.join(', ');
|
308 |
+
if (consumableText.length > 2) consumableSummary += '...';
|
309 |
+
summary += (summary ? ' / ' : '') + 'Расх: ' + consumableSummary;
|
310 |
+
}
|
311 |
+
|
312 |
+
equipmentSummaryEl.textContent = summary || "Ничего";
|
313 |
+
}
|
314 |
+
|
315 |
+
function updateSquadHealthDisplay() {
|
316 |
+
if (gameState !== 'robbery') {
|
317 |
+
squadHealthDisplayEl.innerHTML = '';
|
318 |
+
squadHealthDisplayEl.style.display = 'none'; // Hide if not in robbery
|
319 |
+
return;
|
320 |
+
}
|
321 |
+
squadHealthDisplayEl.style.display = 'block'; // Show if in robbery
|
322 |
+
squadHealthDisplayEl.innerHTML = "Здоровье отряда: ";
|
323 |
+
if (squad.length === 0) {
|
324 |
+
squadHealthDisplayEl.innerHTML += "Отряд пуст!";
|
325 |
+
return;
|
326 |
+
}
|
327 |
+
squad.forEach(cowboy => {
|
328 |
+
const healthSpan = document.createElement('span');
|
329 |
+
healthSpan.classList.add('cowboy-health');
|
330 |
+
// Calculate percentage for styling, handle division by zero
|
331 |
+
const healthPercent = cowboy.maxHealth > 0 ? (cowboy.health / cowboy.maxHealth) * 100 : 0;
|
332 |
+
healthSpan.textContent = `${cowboy.name}: ${cowboy.health}/${cowboy.maxHealth} HP`;
|
333 |
+
|
334 |
+
healthSpan.classList.remove('low-health', 'critical-health'); // Reset classes
|
335 |
+
if (healthPercent <= 20) {
|
336 |
+
healthSpan.classList.add('critical-health');
|
337 |
+
} else if (healthPercent <= 50) {
|
338 |
+
healthSpan.classList.add('low-health');
|
339 |
+
}
|
340 |
+
squadHealthDisplayEl.appendChild(healthSpan);
|
341 |
+
});
|
342 |
+
}
|
343 |
+
|
344 |
+
|
345 |
+
// --- Рендеринг Фаз Игры ---
|
346 |
+
|
347 |
+
function switchPhase(newPhase) {
|
348 |
+
console.log("Switching phase to:", newPhase);
|
349 |
+
// Hide all phases
|
350 |
+
document.querySelectorAll('#main-content > div[id^="phase-"]').forEach(div => div.style.display = 'none');
|
351 |
+
gameOverEl.style.display = 'none';
|
352 |
+
mainContentEl.style.display = 'block'; // Ensure main content is visible unless game over
|
353 |
+
|
354 |
+
gameState = newPhase;
|
355 |
+
|
356 |
+
// Show the target phase
|
357 |
+
const phaseEl = document.getElementById(`phase-${newPhase}`);
|
358 |
+
if (phaseEl) {
|
359 |
+
phaseEl.style.display = 'block';
|
360 |
+
// Call the corresponding render function
|
361 |
+
switch (newPhase) {
|
362 |
+
case 'hire': renderHirePhase(); break;
|
363 |
+
case 'equip': renderEquipPhase(); break;
|
364 |
+
case 'plan': renderPlanPhase(); break;
|
365 |
+
case 'robbery': renderRobberyPhase(); break;
|
366 |
+
case 'results': /* Handled by endRobbery calling renderResultsPhase */ break;
|
367 |
+
}
|
368 |
+
} else if (newPhase === 'gameover') {
|
369 |
+
gameOverEl.style.display = 'block';
|
370 |
+
mainContentEl.style.display = 'none'; // Hide main content on game over
|
371 |
+
} else {
|
372 |
+
console.error("Unknown phase:", newPhase);
|
373 |
+
}
|
374 |
+
updateStatusBar(); // Update status bar on every phase switch
|
375 |
+
updateSquadHealthDisplay(); // Update health display visibility
|
376 |
+
}
|
377 |
+
|
378 |
+
function renderHirePhase() {
|
379 |
+
hireListEl.innerHTML = ''; // Clear list
|
380 |
+
if (availableCowboys.length === 0) {
|
381 |
+
hireListEl.innerHTML = '<li>Нет доступных ковбоев для найма.</li>';
|
382 |
+
} else {
|
383 |
+
availableCowboys.forEach(cowboy => {
|
384 |
+
const li = document.createElement('li');
|
385 |
+
// --- Updated line with emojis ---
|
386 |
+
li.innerHTML = `
|
387 |
+
<div>
|
388 |
+
<b>${cowboy.name}</b> (Ур: ${cowboy.level}, Зд: ${cowboy.health}/${cowboy.maxHealth})<br>
|
389 |
+
Характеристики: 💪С ${cowboy.stats.strength} ✨Л ${cowboy.stats.agility} 🎯М ${cowboy.stats.marksmanship} 😊Х ${cowboy.stats.charisma}<br>
|
390 |
+
Цена: $${cowboy.cost}
|
391 |
+
</div>
|
392 |
+
<button data-cowboy-id="${cowboy.id}" ${playerMoney < cowboy.cost ? 'disabled' : ''}>Нанять</button>
|
393 |
+
`;
|
394 |
+
// --- End of updated line ---
|
395 |
+
li.querySelector('button').addEventListener('click', () => handleHire(cowboy.id));
|
396 |
+
hireListEl.appendChild(li);
|
397 |
+
});
|
398 |
+
}
|
399 |
+
// Disable "Go to Equipment" if no squad members
|
400 |
+
goToEquipmentBtn.disabled = squad.length === 0;
|
401 |
+
}
|
402 |
+
|
403 |
+
|
404 |
+
function renderEquipPhase() {
|
405 |
+
shopListEl.innerHTML = ''; // Clear shop
|
406 |
+
if (shopItems.length === 0) {
|
407 |
+
shopListEl.innerHTML = '<li>Магазин пуст.</li>';
|
408 |
+
} else {
|
409 |
+
shopItems.forEach(item => {
|
410 |
+
const li = document.createElement('li');
|
411 |
+
let effectDesc = Object.entries(item.effect)
|
412 |
+
.map(([key, value]) => `${key.replace('_bonus', '').replace('_chance', ' шанс').replace('health','здровье').replace('damage_reduction','сниж. урона')}: ${value * 100 % 1 === 0 && value < 1 && value > 0 ? (value*100)+'%' : value}`) // Format effects nicely
|
413 |
+
.join(', ');
|
414 |
+
li.innerHTML = `
|
415 |
+
<div>
|
416 |
+
<b>${item.name}</b> (${item.type === 'consumable' ? 'Расходуемый' : item.type === 'weapon' ? 'Оружие' : 'Снаряжение'})<br>
|
417 |
+
Эффект: ${effectDesc || 'Нет'}<br>
|
418 |
+
Цена: $${item.cost}
|
419 |
+
</div>
|
420 |
+
<button data-item-id="${item.id}" ${playerMoney < item.cost || squad.length === 0 ? 'disabled' : ''}>Купить</button>
|
421 |
+
`;
|
422 |
+
li.querySelector('button').addEventListener('click', () => handleBuy(item.id));
|
423 |
+
shopListEl.appendChild(li);
|
424 |
+
});
|
425 |
+
}
|
426 |
+
|
427 |
+
// Display squad for equipment management
|
428 |
+
squadManageListEl.innerHTML = '';
|
429 |
+
if (squad.length === 0) {
|
430 |
+
squadManageListEl.innerHTML = '<li>Ваш отряд пуст.</li>';
|
431 |
+
} else {
|
432 |
+
squad.forEach(cowboy => {
|
433 |
+
const li = document.createElement('li');
|
434 |
+
const equipmentList = cowboy.equipment.map(e => e.name).join(', ') || 'Нет';
|
435 |
+
li.innerHTML = `
|
436 |
+
<span><b>${cowboy.name}</b> (Зд: ${cowboy.health}/${cowboy.maxHealth}) Оружие: ${cowboy.weapon ? cowboy.weapon.name : 'Нет'}, Снаряжение: ${equipmentList}</span>
|
437 |
+
<!-- Basic 'Use Medkit' button -->
|
438 |
+
${cowboy.equipment.some(e => e.name === 'Аптечка' && cowboy.health < cowboy.maxHealth) ?
|
439 |
+
`<button class="use-item-btn" data-cowboy-id="${cowboy.id}" data-item-name="Аптечка">Исп. Аптечку</button>` : ''}
|
440 |
+
`;
|
441 |
+
// Add event listener for using items if button exists
|
442 |
+
const useButton = li.querySelector('.use-item-btn');
|
443 |
+
if (useButton) {
|
444 |
+
useButton.addEventListener('click', (e) => {
|
445 |
+
const cowboyId = e.target.getAttribute('data-cowboy-id');
|
446 |
+
const itemName = e.target.getAttribute('data-item-name');
|
447 |
+
handleUseItem(cowboyId, itemName);
|
448 |
+
});
|
449 |
+
}
|
450 |
+
|
451 |
+
squadManageListEl.appendChild(li);
|
452 |
+
});
|
453 |
+
}
|
454 |
+
// Disable "Go to Plan" if no squad members
|
455 |
+
goToPlanBtn.disabled = squad.length === 0;
|
456 |
+
}
|
457 |
+
|
458 |
+
function handleUseItem(cowboyId, itemName) {
|
459 |
+
const cowboy = squad.find(c => c.id == cowboyId); // Use == for type flexibility if needed, or === if strict
|
460 |
+
if (!cowboy) return;
|
461 |
+
|
462 |
+
const itemIndex = cowboy.equipment.findIndex(item => item.name === itemName && item.type === 'consumable');
|
463 |
+
if (itemIndex > -1) {
|
464 |
+
const item = cowboy.equipment[itemIndex];
|
465 |
+
|
466 |
+
let used = false;
|
467 |
+
if (item.name === "Аптечка" && cowboy.health < cowboy.maxHealth) {
|
468 |
+
const healAmount = item.effect.health || 25;
|
469 |
+
const actualHeal = Math.min(healAmount, cowboy.maxHealth - cowboy.health); // Don't overheal
|
470 |
+
cowboy.health += actualHeal;
|
471 |
+
used = true;
|
472 |
+
console.log(`${cowboy.name} использовал ${item.name}, восстановлено ${actualHeal} здоровья.`);
|
473 |
+
}
|
474 |
+
// Add other usable items here (e.g., Dynamite outside of combat?)
|
475 |
+
|
476 |
+
if (used) {
|
477 |
+
cowboy.equipment.splice(itemIndex, 1); // Remove used consumable
|
478 |
+
updateStatusBar();
|
479 |
+
renderEquipPhase(); // Re-render the equipment phase to show changes
|
480 |
+
}
|
481 |
+
}
|
482 |
+
}
|
483 |
+
|
484 |
+
|
485 |
+
function renderPlanPhase() {
|
486 |
+
planListEl.innerHTML = ''; // Clear plan list
|
487 |
+
if (availablePlans.length === 0) {
|
488 |
+
planListEl.innerHTML = '<li>Нет доступных планов для ограбления.</li>';
|
489 |
+
} else {
|
490 |
+
availablePlans.forEach(plan => {
|
491 |
+
const li = document.createElement('li');
|
492 |
+
li.innerHTML = `
|
493 |
+
<div>
|
494 |
+
<b>${plan.name}</b><br>
|
495 |
+
Описание: ${plan.description}<br>
|
496 |
+
</div>
|
497 |
+
<button data-plan-id="${plan.id}">Выбрать</button>
|
498 |
+
`;
|
499 |
+
li.querySelector('button').addEventListener('click', () => handleChoosePlan(plan.id));
|
500 |
+
planListEl.appendChild(li);
|
501 |
+
});
|
502 |
+
}
|
503 |
+
}
|
504 |
+
|
505 |
+
function renderRobberyPhase() {
|
506 |
+
currentRobberyEvent = getRobberyEvent();
|
507 |
+
updateSquadHealthDisplay();
|
508 |
+
rollResultDisplayEl.textContent = '';
|
509 |
+
|
510 |
+
if (squad.length === 0 && gameState === 'robbery') {
|
511 |
+
console.log("Game over triggered from renderRobberyPhase - squad empty");
|
512 |
+
setTimeout(() => switchPhase('gameover'), 500); // Delay slightly
|
513 |
+
return;
|
514 |
+
}
|
515 |
+
|
516 |
+
if (!currentRobberyEvent) {
|
517 |
+
console.log("No more events for this plan.");
|
518 |
+
// Check if we successfully completed the required progress
|
519 |
+
if (currentPlan && currentRobberyProgress >= currentPlan.events.length) {
|
520 |
+
endRobbery(true, `Ограбление "${currentPlan.name}" успешно завершено!`);
|
521 |
+
} else {
|
522 |
+
// Didn't finish all steps, might be considered a partial success or failure depending on logic
|
523 |
+
endRobbery(false, "Ограбление прервано или не завершено.");
|
524 |
+
}
|
525 |
+
return;
|
526 |
+
}
|
527 |
+
|
528 |
+
eventDescriptionEl.textContent = currentRobberyEvent.description;
|
529 |
+
choicesEl.innerHTML = ''; // Clear old choices
|
530 |
+
|
531 |
+
currentRobberyEvent.choices.forEach(choice => {
|
532 |
+
const button = document.createElement('button');
|
533 |
+
let bonusChanceText = '';
|
534 |
+
// Check for relevant items
|
535 |
+
if (choice.text.toLowerCase().includes('динамит') && squadHasItemType('consumable', 'Динамит')) {
|
536 |
+
bonusChanceText = ' (+Динамит)';
|
537 |
+
} else if (choice.text.toLowerCase().includes('взломать') && squadHasItemType('equipment', 'Отмычки')) {
|
538 |
+
bonusChanceText = ' (+Отмычки)';
|
539 |
+
} else if (choice.requiredStat === 'strength' && squadHasItemType('consumable', 'Динамит') && !bonusChanceText) {
|
540 |
+
// Generic dynamite bonus for strength if not explicitly mentioned
|
541 |
+
// bonusChanceText = ' (?+Динамит)'; // Maybe don't show if not obvious
|
542 |
+
}
|
543 |
+
|
544 |
+
|
545 |
+
button.innerHTML = `
|
546 |
+
${choice.text}
|
547 |
+
<span class="stat-requirement">(Проверка: ${choice.requiredStat}, Сложность: ${choice.difficulty})${bonusChanceText}</span>
|
548 |
+
`;
|
549 |
+
button.addEventListener('click', () => handleChoice(choice.requiredStat, choice.difficulty, choice));
|
550 |
+
choicesEl.appendChild(button);
|
551 |
+
});
|
552 |
+
}
|
553 |
+
|
554 |
+
function renderResultsPhase(success, message, xp) {
|
555 |
+
resultMessageEl.textContent = message;
|
556 |
+
xpGainedEl.textContent = xp;
|
557 |
+
// Ensure buttons are displayed correctly based on success AND if squad survived
|
558 |
+
const canContinue = success && squad.length > 0;
|
559 |
+
continueGameBtn.style.display = canContinue ? 'block' : 'none';
|
560 |
+
newGameBtn.style.display = 'block'; // Always allow starting a new game from results
|
561 |
+
// Make sure continue button is enabled/disabled correctly
|
562 |
+
continueGameBtn.disabled = !canContinue;
|
563 |
+
|
564 |
+
}
|
565 |
+
|
566 |
+
|
567 |
+
// --- Обработчики Действий ---
|
568 |
+
|
569 |
+
function handleHire(cowboyId) {
|
570 |
+
const cowboy = availableCowboys.find(c => c.id == cowboyId); // Use == for potential string/number mismatch
|
571 |
+
if (cowboy && playerMoney >= cowboy.cost) {
|
572 |
+
playerMoney -= cowboy.cost;
|
573 |
+
squad.push(cowboy);
|
574 |
+
availableCowboys = availableCowboys.filter(c => c.id != cowboyId);
|
575 |
+
console.log(`Нанят ${cowboy.name}`);
|
576 |
+
updateStatusBar();
|
577 |
+
renderHirePhase(); // Update hire list and buttons
|
578 |
+
renderEquipPhase(); // Also update squad list in equip phase if visible
|
579 |
+
} else if (cowboy) {
|
580 |
+
alert("Недостаточно денег!");
|
581 |
+
}
|
582 |
+
}
|
583 |
+
|
584 |
+
function handleBuy(itemId) {
|
585 |
+
const item = shopItems.find(i => i.id == itemId);
|
586 |
+
if (!item) return;
|
587 |
+
|
588 |
+
if (playerMoney < item.cost) {
|
589 |
+
alert("Недостаточно денег!");
|
590 |
+
return;
|
591 |
+
}
|
592 |
+
if (squad.length === 0) {
|
593 |
+
alert("Сначала наймите ковбоев, чтобы дать им снаряжение!");
|
594 |
+
return;
|
595 |
+
}
|
596 |
+
|
597 |
+
// --- More robust item assignment ---
|
598 |
+
let assigned = false;
|
599 |
+
if (item.type === 'weapon') {
|
600 |
+
// Try to give to someone without a weapon first
|
601 |
+
let targetCowboy = squad.find(c => !c.weapon);
|
602 |
+
if (targetCowboy) {
|
603 |
+
targetCowboy.weapon = item;
|
604 |
+
assigned = true;
|
605 |
+
console.log(`${item.name} выдан ${targetCowboy.name}`);
|
606 |
+
} else {
|
607 |
+
// If everyone has a weapon, replace the first cowboy's (simple logic)
|
608 |
+
// In a real game, you'd let the player choose or compare stats
|
609 |
+
squad[0].weapon = item;
|
610 |
+
assigned = true;
|
611 |
+
console.log(`${item.name} выдан ${squad[0].name} (заменил старое)`);
|
612 |
+
}
|
613 |
+
} else if (item.type === 'equipment' || item.type === 'consumable') {
|
614 |
+
// Find cowboy with fewest equipment items to distribute somewhat evenly
|
615 |
+
let targetCowboy = squad.reduce((prev, curr) => {
|
616 |
+
return (curr.equipment.length < prev.equipment.length) ? curr : prev;
|
617 |
+
});
|
618 |
+
targetCowboy.equipment.push(item);
|
619 |
+
assigned = true;
|
620 |
+
console.log(`${item.name} добавлен в снаряжение ${targetCowboy.name}`);
|
621 |
+
}
|
622 |
+
|
623 |
+
if (assigned) {
|
624 |
+
playerMoney -= item.cost;
|
625 |
+
// Remove *one* instance of the bought item from the shop
|
626 |
+
const itemIndexInShop = shopItems.findIndex(i => i.id == itemId);
|
627 |
+
if(itemIndexInShop > -1) {
|
628 |
+
shopItems.splice(itemIndexInShop, 1);
|
629 |
+
}
|
630 |
+
console.log(`Куплен ${item.name}`);
|
631 |
+
updateStatusBar();
|
632 |
+
renderEquipPhase(); // Update shop and squad display
|
633 |
+
} else {
|
634 |
+
alert("Не удалось назначить предмет."); // Should not happen with current logic if squad exists
|
635 |
+
}
|
636 |
+
}
|
637 |
+
|
638 |
+
function handleChoosePlan(planId) {
|
639 |
+
currentPlan = availablePlans.find(p => p.id == planId);
|
640 |
+
if (currentPlan) {
|
641 |
+
console.log(`Выбран план: ${currentPlan.name}`);
|
642 |
+
currentRobberyProgress = 0;
|
643 |
+
switchPhase('robbery');
|
644 |
+
}
|
645 |
+
}
|
646 |
+
|
647 |
+
function handleChoice(stat, difficulty, choiceData) {
|
648 |
+
console.log(`Выбрано действие: ${choiceData.text}, Проверка: ${stat}, Сложность: ${difficulty}`);
|
649 |
+
performCheck(stat, difficulty, choiceData);
|
650 |
+
}
|
651 |
+
|
652 |
+
function performCheck(stat, difficulty, choiceData) {
|
653 |
+
if (squad.length === 0) {
|
654 |
+
rollResultDisplayEl.textContent = "Нет отряда для выполнения действия!";
|
655 |
+
// Treat as failure?
|
656 |
+
handleFailure(choiceData, "Отряд пуст!");
|
657 |
+
return;
|
658 |
+
}
|
659 |
+
|
660 |
+
// Sum the required stat across the squad, including bonuses
|
661 |
+
let totalStatValue = squad.reduce((sum, cowboy) => {
|
662 |
+
let effectiveStat = cowboy.stats[stat] || 0;
|
663 |
+
if (cowboy.weapon && cowboy.weapon.effect && cowboy.weapon.effect[`${stat}_bonus`]) {
|
664 |
+
effectiveStat += cowboy.weapon.effect[`${stat}_bonus`];
|
665 |
+
}
|
666 |
+
cowboy.equipment.forEach(item => {
|
667 |
+
if (item.effect && item.effect[`${stat}_bonus`]) {
|
668 |
+
effectiveStat += item.effect[`${stat}_bonus`];
|
669 |
+
}
|
670 |
+
});
|
671 |
+
return sum + effectiveStat;
|
672 |
+
}, 0);
|
673 |
+
|
674 |
+
// Check for item-specific bonuses/effects mentioned in the choice
|
675 |
+
let bonusChance = 0; // Represents a multiplier bonus, e.g., 0.2 for +20%
|
676 |
+
let itemUsed = null; // Track if a consumable is used
|
677 |
+
|
678 |
+
if (choiceData.text.toLowerCase().includes('динамит') && squadHasItemType('consumable', 'Динамит')) {
|
679 |
+
const dynamite = findItemInSquad('consumable', 'Динамит'); // Find the item to get its effect
|
680 |
+
if(dynamite && dynamite.effect.demolition_chance) {
|
681 |
+
bonusChance += dynamite.effect.demolition_chance;
|
682 |
+
itemUsed = { type: 'consumable', name: 'Динамит' }; // Mark dynamite for removal
|
683 |
+
console.log("Используется Динамит! Бонус шанса: +", bonusChance * 100, "%");
|
684 |
+
}
|
685 |
+
} else if (choiceData.text.toLowerCase().includes('взломать') && squadHasItemType('equipment', 'Отмычки')) {
|
686 |
+
const lockpicks = findItemInSquad('equipment', 'Отмычки'); // Find the item
|
687 |
+
if(lockpicks && lockpicks.effect.lockpick_chance) {
|
688 |
+
bonusChance += lockpicks.effect.lockpick_chance; // Permanent bonus, item not used up
|
689 |
+
console.log("Используются Отмычки! Бонус шанса: +", bonusChance * 100, "%");
|
690 |
+
}
|
691 |
+
}
|
692 |
+
// Add luck bonus from items like "Шляпа Удачи" (needs implementation)
|
693 |
+
// bonusChance += squad.reduce((luck, c) => luck + (c.equipment.find(e => e.name === "Шляпа Удачи")?.effect.luck_bonus || 0), 0);
|
694 |
+
|
695 |
+
|
696 |
+
// Dice roll (D10)
|
697 |
+
const diceRoll = Math.floor(Math.random() * DICE_SIDES) + 1;
|
698 |
+
// Core check calculation: (Dice * Stat) compared to Difficulty
|
699 |
+
// Apply bonus chance multiplicatively to the *score* or additively to the *roll* - let's try multiplying score
|
700 |
+
const baseScore = diceRoll * totalStatValue;
|
701 |
+
const finalScore = baseScore * (1 + bonusChance); // Apply bonus multiplier
|
702 |
+
|
703 |
+
console.log(`Бросок D${DICE_SIDES}: ${diceRoll}, Суммарная характеристика (${stat}): ${totalStatValue}, Базовый результат: ${baseScore}, Модификатор шанса: ${(1 + bonusChance).toFixed(2)}x, Финальный результат: ${finalScore.toFixed(2)}, Сложность: ${difficulty}`);
|
704 |
+
rollResultDisplayEl.textContent = `Бросок: ${diceRoll} × ${totalStatValue} (${stat}) × ${(1 + bonusChance).toFixed(2)} (бонус) = ${finalScore.toFixed(2)} / Требуется: ${difficulty}`;
|
705 |
+
|
706 |
+
// Consume the item if it was marked
|
707 |
+
if (itemUsed) {
|
708 |
+
removeItemFromSquad(itemUsed.type, itemUsed.name); // Remove one instance
|
709 |
+
}
|
710 |
+
|
711 |
+
if (finalScore >= difficulty) {
|
712 |
+
console.log("Успех!");
|
713 |
+
handleSuccess(choiceData);
|
714 |
+
} else {
|
715 |
+
console.log("Провал!");
|
716 |
+
handleFailure(choiceData);
|
717 |
+
}
|
718 |
+
}
|
719 |
+
|
720 |
+
// Helper to check if *any* cowboy has an item
|
721 |
+
function squadHasItemType(type, name = null) {
|
722 |
+
return squad.some(cowboy =>
|
723 |
+
(cowboy.weapon && cowboy.weapon.type === type && (!name || cowboy.weapon.name === name)) ||
|
724 |
+
cowboy.equipment.some(item => item.type === type && (!name || item.name === name))
|
725 |
+
);
|
726 |
+
}
|
727 |
+
|
728 |
+
// Helper to find the first instance of an item in the squad (to get its effect details)
|
729 |
+
function findItemInSquad(type, name) {
|
730 |
+
for (let cowboy of squad) {
|
731 |
+
if (cowboy.weapon && cowboy.weapon.type === type && cowboy.weapon.name === name) {
|
732 |
+
return cowboy.weapon;
|
733 |
+
}
|
734 |
+
const item = cowboy.equipment.find(item => item.type === type && item.name === name);
|
735 |
+
if (item) {
|
736 |
+
return item;
|
737 |
+
}
|
738 |
+
}
|
739 |
+
return null;
|
740 |
+
}
|
741 |
+
|
742 |
+
|
743 |
+
// Helper to remove the *first* instance of a consumable item found in the squad
|
744 |
+
function removeItemFromSquad(type, name) {
|
745 |
+
for (let cowboy of squad) {
|
746 |
+
const itemIndex = cowboy.equipment.findIndex(item => item.type === type && item.name === name);
|
747 |
+
if (itemIndex > -1) {
|
748 |
+
cowboy.equipment.splice(itemIndex, 1);
|
749 |
+
console.log(`Предмет ${name} использован и удален у ${cowboy.name}`);
|
750 |
+
updateStatusBar(); // Update equipment summary
|
751 |
+
return true; // Item found and removed
|
752 |
+
}
|
753 |
+
}
|
754 |
+
console.log(`Предмет ${name} не найден в отряде для удаления.`);
|
755 |
+
return false; // Item not found
|
756 |
+
}
|
757 |
+
|
758 |
+
|
759 |
+
function handleSuccess(choiceData) {
|
760 |
+
const eventData = currentRobberyEvent;
|
761 |
+
if (!eventData) return; // Should not happen if called correctly
|
762 |
+
|
763 |
+
let message = "Успех! ";
|
764 |
+
let xpEarned = 0;
|
765 |
+
|
766 |
+
// Apply rewards
|
767 |
+
if (eventData.successReward) {
|
768 |
+
if (eventData.successReward.money) {
|
769 |
+
playerMoney += eventData.successReward.money;
|
770 |
+
message += `Получено $${eventData.successReward.money}. `;
|
771 |
+
}
|
772 |
+
if (eventData.successReward.xp) {
|
773 |
+
xpEarned = eventData.successReward.xp; // Track XP for awarding later
|
774 |
+
message += `Получено ${xpEarned} опыта. `;
|
775 |
+
}
|
776 |
+
if (eventData.successReward.progress) {
|
777 |
+
currentRobberyProgress += eventData.successReward.progress;
|
778 |
+
message += `Продвижение по плану. `;
|
779 |
+
}
|
780 |
+
// TODO: Add item rewards here
|
781 |
+
}
|
782 |
+
|
783 |
+
// Try to use a medkit automatically if someone is injured (optional QoL)
|
784 |
+
const injuredCowboy = squad.find(c => c.health < c.maxHealth);
|
785 |
+
if (injuredCowboy && squadHasItemType('consumable', 'Аптечка')) {
|
786 |
+
const medkit = findItemInSquad('consumable', 'Аптечка');
|
787 |
+
if (medkit && removeItemFromSquad('consumable', 'Аптечка')) { // Find and remove it
|
788 |
+
const healAmount = medkit.effect.health || 25;
|
789 |
+
const actualHeal = Math.min(healAmount, injuredCowboy.maxHealth - injuredCowboy.health);
|
790 |
+
injuredCowboy.health += actualHeal;
|
791 |
+
message += `${injuredCowboy.name} использовал аптечку (+${actualHeal} ЗД). `;
|
792 |
+
console.log(`${injuredCowboy.name} подлечился.`);
|
793 |
+
updateSquadHealthDisplay(); // Update health display immediately
|
794 |
+
}
|
795 |
+
}
|
796 |
+
|
797 |
+
awardXP(xpEarned); // Award XP gained from this step
|
798 |
+
updateStatusBar();
|
799 |
+
|
800 |
+
// Move to the next event or end the robbery
|
801 |
+
const nextEvent = getRobberyEvent();
|
802 |
+
rollResultDisplayEl.textContent += ` | ${message}`; // Append outcome to roll result
|
803 |
+
|
804 |
+
if (nextEvent && currentRobberyProgress < currentPlan.events.length) { // Check progress hasn't exceeded plan length
|
805 |
+
setTimeout(() => renderRobberyPhase(), 1500); // Pause before next step
|
806 |
+
} else {
|
807 |
+
// If progress >= length, it means the last step was successful
|
808 |
+
console.log("Reached end of plan events successfully.");
|
809 |
+
setTimeout(() => endRobbery(true, message + " Ограбление завершено!"), 1500);
|
810 |
+
}
|
811 |
+
}
|
812 |
+
|
813 |
+
function handleFailure(choiceData, failureReason = "Провал проверки!") {
|
814 |
+
const eventData = currentRobberyEvent;
|
815 |
+
if (!eventData) return;
|
816 |
+
|
817 |
+
let message = "Провал! " + failureReason + " ";
|
818 |
+
|
819 |
+
// Apply penalties
|
820 |
+
if (eventData.failurePenalty) {
|
821 |
+
if (eventData.failurePenalty.money) {
|
822 |
+
const moneyLost = Math.min(playerMoney, Math.abs(eventData.failurePenalty.money));
|
823 |
+
playerMoney -= moneyLost;
|
824 |
+
message += `Потеряно $${moneyLost}. `;
|
825 |
+
}
|
826 |
+
if (eventData.failurePenalty.health) {
|
827 |
+
const damage = eventData.failurePenalty.health;
|
828 |
+
message += `Отряд ранен! `;
|
829 |
+
let casualties = 0;
|
830 |
+
// Apply damage, check for deaths immediately
|
831 |
+
const remainingSquad = [];
|
832 |
+
squad.forEach(cowboy => {
|
833 |
+
if (applyDamage(cowboy, Math.ceil(damage / Math.max(1, squad.length)) + Math.floor(Math.random() * 5))) {
|
834 |
+
remainingSquad.push(cowboy); // Cowboy survived
|
835 |
+
} else {
|
836 |
+
casualties++; // Cowboy died
|
837 |
+
message += `${cowboy.name} погиб! `;
|
838 |
+
}
|
839 |
+
});
|
840 |
+
squad = remainingSquad; // Update squad with survivors
|
841 |
+
|
842 |
+
if (squad.length === 0) {
|
843 |
+
// Game Over scenario
|
844 |
+
rollResultDisplayEl.textContent += ` | ${message} Весь отряд погиб!`;
|
845 |
+
console.log("Game over triggered from handleFailure - squad wiped out.");
|
846 |
+
setTimeout(() => switchPhase('gameover'), 1500);
|
847 |
+
return; // Stop further processing
|
848 |
+
}
|
849 |
+
}
|
850 |
+
if (eventData.failurePenalty.progress && eventData.failurePenalty.progress < 0) {
|
851 |
+
currentRobberyProgress = Math.max(0, currentRobberyProgress + eventData.failurePenalty.progress); // Setback
|
852 |
+
message += `Продвижение отброшено назад. `;
|
853 |
+
}
|
854 |
+
// TODO: Add item loss penalty
|
855 |
+
}
|
856 |
+
|
857 |
+
updateStatusBar();
|
858 |
+
updateSquadHealthDisplay(); // Update health after damage/deaths
|
859 |
+
|
860 |
+
// Decide whether to continue or fail the robbery
|
861 |
+
// Simple: continue to next step even on failure, unless squad wiped out
|
862 |
+
const nextEvent = getRobberyEvent();
|
863 |
+
rollResultDisplayEl.textContent += ` | ${message}`;
|
864 |
+
|
865 |
+
if (nextEvent && currentRobberyProgress < currentPlan.events.length) { // Check if there are still events left
|
866 |
+
setTimeout(() => renderRobberyPhase(), 1500);
|
867 |
+
} else {
|
868 |
+
// Reached end of events after a failure, or no more events possible
|
869 |
+
console.log("Reached end of plan events after failure or squad wipeout.");
|
870 |
+
// Consider this a failed robbery outcome
|
871 |
+
setTimeout(() => endRobbery(false, message + " Ограбление провалено!"), 1500);
|
872 |
+
}
|
873 |
+
}
|
874 |
+
|
875 |
+
// Returns true if cowboy survives, false if they die
|
876 |
+
function applyDamage(cowboy, amount) {
|
877 |
+
if (!cowboy || amount <= 0) return true; // No damage or invalid cowboy
|
878 |
+
|
879 |
+
let damageTaken = amount;
|
880 |
+
const armor = cowboy.equipment.find(item => item.effect && item.effect.damage_reduction);
|
881 |
+
if (armor) {
|
882 |
+
damageTaken = Math.max(1, Math.round(amount * (1 - armor.effect.damage_reduction)));
|
883 |
+
console.log(`${cowboy.name} получил ${damageTaken} урона (снижено броней с ${amount})`);
|
884 |
+
} else {
|
885 |
+
console.log(`${cowboy.name} получил ${damageTaken} урона`);
|
886 |
+
}
|
887 |
+
|
888 |
+
cowboy.health -= damageTaken;
|
889 |
+
|
890 |
+
if (cowboy.health <= 0) {
|
891 |
+
console.log(`${cowboy.name} погиб!`);
|
892 |
+
return false; // Cowboy died
|
893 |
+
}
|
894 |
+
return true; // Cowboy survived
|
895 |
+
}
|
896 |
+
|
897 |
+
|
898 |
+
function awardXP(amount) {
|
899 |
+
if (squad.length === 0 || amount <= 0) return;
|
900 |
+
|
901 |
+
const xpPerCowboy = Math.floor(amount / squad.length);
|
902 |
+
if (xpPerCowboy <= 0) return; // Avoid awarding 0 XP
|
903 |
+
|
904 |
+
squad.forEach(cowboy => {
|
905 |
+
if (cowboy.level < MAX_LEVEL) {
|
906 |
+
cowboy.xp += xpPerCowboy;
|
907 |
+
console.log(`${cowboy.name} получил ${xpPerCowboy} опыта (Всего: ${cowboy.xp}).`);
|
908 |
+
// Check for level up
|
909 |
+
while (cowboy.xp >= XP_PER_LEVEL && cowboy.level < MAX_LEVEL) {
|
910 |
+
levelUp(cowboy);
|
911 |
+
}
|
912 |
+
}
|
913 |
+
});
|
914 |
+
// No status bar update needed here unless level up changes stats shown there directly
|
915 |
+
}
|
916 |
+
|
917 |
+
function levelUp(cowboy) {
|
918 |
+
cowboy.level++;
|
919 |
+
cowboy.xp -= XP_PER_LEVEL;
|
920 |
+
console.log(`%c${cowboy.name} достиг Уровня ${cowboy.level}!`, "color: green; font-weight: bold;");
|
921 |
+
|
922 |
+
// Improve stats: +1 random mandatory, +1 random optional based on luck/class?
|
923 |
+
const randomStat1 = cowboyStats[Math.floor(Math.random() * cowboyStats.length)];
|
924 |
+
cowboy.stats[randomStat1]++;
|
925 |
+
let levelUpMessage = `+1 ${randomStat1}`;
|
926 |
+
|
927 |
+
// Increase health
|
928 |
+
const healthIncrease = 8 + Math.floor(Math.random() * 8) + Math.floor(cowboy.stats.strength / 2); // 8-15 + strength bonus
|
929 |
+
cowboy.maxHealth += healthIncrease;
|
930 |
+
// Heal proportional to level up health gain
|
931 |
+
cowboy.health = Math.min(cowboy.maxHealth, cowboy.health + healthIncrease);
|
932 |
+
|
933 |
+
levelUpMessage += `, +${healthIncrease} Макс. Здоровья`;
|
934 |
+
console.log(` ${levelUpMessage}`);
|
935 |
+
updateStatusBar(); // Update stats if they changed
|
936 |
+
}
|
937 |
+
|
938 |
+
|
939 |
+
function endRobbery(success, finalMessage = "") {
|
940 |
+
let totalXP = 0;
|
941 |
+
let finalReward = 0; // Track monetary reward given *at the end*
|
942 |
+
|
943 |
+
if (!currentPlan) {
|
944 |
+
console.error("Ending robbery without a current plan!");
|
945 |
+
success = false; // Cannot succeed without a plan
|
946 |
+
finalMessage = finalMessage || "Ошибка: План ограбления не найден.";
|
947 |
+
} else if (success && currentRobberyProgress >= currentPlan.events.length) {
|
948 |
+
// Successfully completed ALL events
|
949 |
+
totalXP = currentPlan.baseDifficulty + Math.floor(currentPlan.potentialReward / 10); // XP for success + reward bonus
|
950 |
+
finalReward = currentPlan.potentialReward; // Assume full reward if not given incrementally
|
951 |
+
// Check if reward was already given on last step
|
952 |
+
const lastEvent = currentPlan.events[currentPlan.events.length - 1];
|
953 |
+
if(lastEvent && lastEvent.successReward && lastEvent.successReward.money && lastEvent.successReward.money >= finalReward * 0.8) {
|
954 |
+
// If last step gave most of the reward, don't add it again
|
955 |
+
console.log("Final reward likely given on last step, not adding full amount again.");
|
956 |
+
finalReward = 0; // Reset final reward
|
957 |
+
} else {
|
958 |
+
playerMoney += finalReward;
|
959 |
+
finalMessage = finalMessage || `Ограбление "${currentPlan.name}" успешно! Добыча: $${finalReward}`;
|
960 |
+
}
|
961 |
+
finalMessage += ` (Опыт: ${totalXP})`;
|
962 |
+
|
963 |
+
} else if (success) {
|
964 |
+
// Escaped early but successfully (partial success)
|
965 |
+
totalXP = Math.floor((currentPlan.baseDifficulty * currentRobberyProgress / currentPlan.events.length) * 0.8); // XP proportional to progress, slight penalty for leaving early
|
966 |
+
finalMessage = finalMessage || `Удалось уйти с частью добычи после ${currentRobberyProgress} этапов.`;
|
967 |
+
finalMessage += ` (Опыт: ${totalXP})`;
|
968 |
+
} else {
|
969 |
+
// Failure
|
970 |
+
totalXP = Math.floor((currentPlan.baseDifficulty * currentRobberyProgress / currentPlan.events.length) * 0.1); // Minimal XP for failure based on progress
|
971 |
+
finalMessage = finalMessage || "Ограбление провалено!";
|
972 |
+
finalMessage += ` (Опыт: ${totalXP})`;
|
973 |
+
}
|
974 |
+
|
975 |
+
// Ensure squad exists before awarding XP
|
976 |
+
if (squad.length > 0) {
|
977 |
+
awardXP(totalXP);
|
978 |
+
} else {
|
979 |
+
// If squad wiped out, ensure success is false
|
980 |
+
success = false;
|
981 |
+
finalMessage += " Отряд не выжил.";
|
982 |
+
}
|
983 |
+
|
984 |
+
|
985 |
+
console.log("Ограбление завершено. Итог:", success ? "Успех" : "Провал", "| Сообщение:", finalMessage, "| Опыт:", totalXP);
|
986 |
+
currentPlan = null; // Clear current plan after it's finished
|
987 |
+
currentRobberyProgress = 0;
|
988 |
+
|
989 |
+
switchPhase('results');
|
990 |
+
renderResultsPhase(success, finalMessage, totalXP);
|
991 |
+
}
|
992 |
+
|
993 |
+
|
994 |
+
// --- Навигация и Перезапуск ---
|
995 |
+
|
996 |
+
goToEquipmentBtn.addEventListener('click', () => {
|
997 |
+
if (squad.length > 0) {
|
998 |
+
switchPhase('equip');
|
999 |
+
} else {
|
1000 |
+
alert("Сначала наймите хотя бы одного ковбоя!");
|
1001 |
+
}
|
1002 |
+
});
|
1003 |
+
|
1004 |
+
goToPlanBtn.addEventListener('click', () => {
|
1005 |
+
if (squad.length > 0) {
|
1006 |
+
switchPhase('plan');
|
1007 |
+
} else {
|
1008 |
+
alert("Нужно нанять хотя бы одного ковбоя!");
|
1009 |
+
}
|
1010 |
+
});
|
1011 |
+
|
1012 |
+
continueGameBtn.addEventListener('click', () => {
|
1013 |
+
if (squad.length === 0) {
|
1014 |
+
alert("Нельзя продолжить - отряд пуст! Начните новую игру.");
|
1015 |
+
return;
|
1016 |
+
}
|
1017 |
+
// Heal surviving squad members partially
|
1018 |
+
squad.forEach(cowboy => {
|
1019 |
+
const healAmount = Math.max(10, Math.round(cowboy.maxHealth * 0.4)); // Heal 40% or 10 HP, whichever is more
|
1020 |
+
cowboy.health = Math.min(cowboy.maxHealth, cowboy.health + healAmount);
|
1021 |
+
});
|
1022 |
+
|
1023 |
+
// Generate new shop items and plans
|
1024 |
+
generateInitialData(); // This regenerates *everything* except the squad
|
1025 |
+
|
1026 |
+
switchPhase('equip'); // Go back to the equipment phase
|
1027 |
+
});
|
1028 |
+
|
1029 |
+
newGameBtn.addEventListener('click', initGame);
|
1030 |
+
restartGameBtn.addEventListener('click', initGame); // Button from Game Over screen
|
1031 |
+
|
1032 |
+
// --- Запуск Игры ---
|
1033 |
+
initGame();
|
1034 |
+
});
|
style.css
CHANGED
@@ -1,28 +1,274 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
}
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
}
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* Apply box-sizing globally for easier layout */
|
2 |
+
* {
|
3 |
+
box-sizing: border-box;
|
4 |
+
}
|
5 |
+
|
6 |
+
body {
|
7 |
+
/* Using Merriweather as the base font */
|
8 |
+
font-family: 'Merriweather', serif;
|
9 |
+
/* Dark brown text for readability */
|
10 |
+
color: #4d3a2a;
|
11 |
+
/* Subtle repeating background texture (replace URL if needed) */
|
12 |
+
background-color: #f4e8c1; /* Fallback color */
|
13 |
+
background-image: url('https://www.transparenttextures.com/patterns/wood-pattern.png'); /* Example wood texture */
|
14 |
+
margin: 0;
|
15 |
+
padding: 20px;
|
16 |
+
}
|
17 |
+
|
18 |
+
#game-container {
|
19 |
+
max-width: 850px; /* Slightly wider */
|
20 |
+
margin: 20px auto;
|
21 |
+
/* Aged paper background */
|
22 |
+
background-color: #fffaf0; /* Off-white like parchment */
|
23 |
+
/* Thick border suggesting a wooden frame */
|
24 |
+
border: 8px solid #8b4513; /* Darker, thicker brown border */
|
25 |
+
border-image: url('https://www.transparenttextures.com/patterns/lined-paper.png') 10 round; /* Example paper edge, adjust if needed */
|
26 |
+
padding: 25px;
|
27 |
+
/* More pronounced shadow */
|
28 |
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
|
29 |
+
border-radius: 5px; /* Optional: slight rounding */
|
30 |
+
}
|
31 |
+
|
32 |
+
h1 {
|
33 |
+
/* Using the stylized Rye font for the main title */
|
34 |
+
font-family: 'Rye', cursive;
|
35 |
+
text-align: center;
|
36 |
+
color: #5a3d2b; /* Darker, richer brown */
|
37 |
+
font-size: 2.8em; /* Larger title */
|
38 |
+
margin-bottom: 25px;
|
39 |
+
padding-bottom: 15px;
|
40 |
+
/* Simple underline instead of dashed */
|
41 |
+
border-bottom: 2px solid #a0522d; /* Sienna color */
|
42 |
+
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
|
43 |
+
}
|
44 |
+
|
45 |
+
h2 {
|
46 |
+
font-family: 'Merriweather', serif;
|
47 |
+
font-weight: 700; /* Bold */
|
48 |
+
text-align: center;
|
49 |
+
color: #8b4513; /* Same as border */
|
50 |
+
border-bottom: 1px solid #d2b48c; /* Lighter brown */
|
51 |
+
padding-bottom: 10px;
|
52 |
+
margin-top: 30px;
|
53 |
+
margin-bottom: 20px;
|
54 |
+
}
|
55 |
+
|
56 |
+
/* --- Status Bar --- */
|
57 |
+
#status-bar {
|
58 |
+
background-color: #d2b48c; /* Tan color, like worn leather */
|
59 |
+
border: 2px solid #a0522d; /* Sienna border */
|
60 |
+
padding: 12px 15px;
|
61 |
+
margin-bottom: 25px;
|
62 |
+
display: flex;
|
63 |
+
justify-content: space-around;
|
64 |
+
flex-wrap: wrap;
|
65 |
+
border-radius: 4px;
|
66 |
+
font-size: 0.95em;
|
67 |
+
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.1);
|
68 |
+
color: #4d3a2a; /* Ensure good contrast */
|
69 |
+
}
|
70 |
+
|
71 |
+
#status-bar div {
|
72 |
+
margin: 5px 10px;
|
73 |
+
font-weight: bold; /* Make status text bolder */
|
74 |
+
}
|
75 |
+
#status-bar span {
|
76 |
+
font-weight: normal; /* Normal weight for values */
|
77 |
+
color: #000; /* Black for money/stats values */
|
78 |
+
margin-left: 5px;
|
79 |
+
}
|
80 |
+
|
81 |
+
/* --- Main Content Area --- */
|
82 |
+
#main-content ul {
|
83 |
+
list-style: none;
|
84 |
+
padding: 0;
|
85 |
+
}
|
86 |
+
|
87 |
+
/* Styling list items like cards or entries */
|
88 |
+
#main-content li {
|
89 |
+
background-color: #fdf5e6; /* Lighter parchment/linen */
|
90 |
+
border: 1px solid #c1a97a; /* Subtle border */
|
91 |
+
margin-bottom: 15px;
|
92 |
+
padding: 15px 20px;
|
93 |
+
border-radius: 4px;
|
94 |
+
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
|
95 |
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
96 |
+
}
|
97 |
+
#main-content li:hover {
|
98 |
+
transform: translateY(-2px); /* Slight lift on hover */
|
99 |
+
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.15);
|
100 |
+
}
|
101 |
+
|
102 |
+
|
103 |
+
/* --- Buttons --- */
|
104 |
+
/* General button style */
|
105 |
+
button {
|
106 |
+
font-family: 'Merriweather', serif; /* Consistent font */
|
107 |
+
background-color: #8b4513; /* Dark brown */
|
108 |
+
color: #fdf5e6; /* Light parchment text */
|
109 |
+
border: 1px solid #5a3d2b; /* Darker border */
|
110 |
+
padding: 10px 18px;
|
111 |
+
cursor: pointer;
|
112 |
+
border-radius: 4px;
|
113 |
+
font-size: 0.9em;
|
114 |
+
font-weight: bold;
|
115 |
+
text-transform: uppercase; /* Make text uppercase */
|
116 |
+
letter-spacing: 0.5px;
|
117 |
+
transition: background-color 0.2s ease, transform 0.1s ease;
|
118 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
119 |
+
margin-left: 10px; /* Default margin */
|
120 |
+
}
|
121 |
+
|
122 |
+
button:hover {
|
123 |
+
background-color: #a0522d; /* Sienna on hover */
|
124 |
+
transform: translateY(-1px); /* Slight lift */
|
125 |
+
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3);
|
126 |
+
}
|
127 |
+
|
128 |
+
button:active {
|
129 |
+
transform: translateY(0px); /* Press down effect */
|
130 |
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
131 |
+
}
|
132 |
+
|
133 |
+
/* Buttons directly within main content (like phase navigation) */
|
134 |
+
#main-content > button {
|
135 |
+
display: block;
|
136 |
+
margin: 25px auto 10px; /* Centered with more margin */
|
137 |
+
padding: 12px 30px; /* Larger padding */
|
138 |
+
font-size: 1em;
|
139 |
+
}
|
140 |
+
|
141 |
+
/* Buttons inside list items (like Hire, Buy, Choose Plan) */
|
142 |
+
#main-content li button {
|
143 |
+
float: right; /* Align to the right */
|
144 |
+
margin-top: -5px; /* Adjust vertical alignment */
|
145 |
+
margin-left: 15px;
|
146 |
+
padding: 8px 15px; /* Slightly smaller */
|
147 |
+
font-size: 0.85em;
|
148 |
+
}
|
149 |
+
|
150 |
+
/* --- Robbery Phase Specific Styling --- */
|
151 |
+
#robbery-log {
|
152 |
+
background-color: #f5f5dc; /* Beige like telegram paper */
|
153 |
+
border: 1px dashed #aaa;
|
154 |
+
padding: 20px;
|
155 |
+
margin-bottom: 20px;
|
156 |
+
min-height: 100px;
|
157 |
+
border-radius: 3px;
|
158 |
+
white-space: pre-wrap;
|
159 |
+
font-style: italic; /* Italicize descriptions */
|
160 |
+
box-shadow: inset 1px 1px 4px rgba(0, 0, 0, 0.1);
|
161 |
+
}
|
162 |
+
|
163 |
+
/* Choice buttons */
|
164 |
+
#choices button {
|
165 |
+
display: block;
|
166 |
+
width: 100%;
|
167 |
+
margin-bottom: 10px;
|
168 |
+
text-align: left;
|
169 |
+
padding: 12px 15px;
|
170 |
+
background-color: #cd853f; /* Peru - slightly lighter brown */
|
171 |
+
border-color: #8b4513;
|
172 |
+
color: #fff; /* White text for contrast */
|
173 |
+
text-transform: none; /* Normal case for choices */
|
174 |
+
font-weight: normal;
|
175 |
+
font-size: 1em;
|
176 |
+
}
|
177 |
+
#choices button:hover {
|
178 |
+
background-color: #d2a679; /* Lighter on hover */
|
179 |
+
color: #4d3a2a;
|
180 |
+
}
|
181 |
+
|
182 |
+
.stat-requirement {
|
183 |
+
font-size: 0.85em;
|
184 |
+
color: rgba(255, 255, 255, 0.8); /* Lighter text for requirement */
|
185 |
+
margin-left: 15px;
|
186 |
+
font-style: italic;
|
187 |
+
}
|
188 |
+
#choices button:hover .stat-requirement {
|
189 |
+
color: rgba(77, 58, 42, 0.8); /* Darker on hover */
|
190 |
+
}
|
191 |
+
|
192 |
+
/* Health Display */
|
193 |
+
#squad-health-display {
|
194 |
+
margin-bottom: 20px;
|
195 |
+
padding: 10px 15px;
|
196 |
+
background-color: rgba(139, 69, 19, 0.05); /* Very light brown tint */
|
197 |
+
border: 1px solid #e0d6b3; /* Light border */
|
198 |
+
border-radius: 3px;
|
199 |
+
font-size: 0.9em;
|
200 |
+
}
|
201 |
+
|
202 |
+
.cowboy-health {
|
203 |
+
margin-right: 15px;
|
204 |
+
display: inline-block;
|
205 |
+
border: none; /* Remove border */
|
206 |
+
padding: 2px 0;
|
207 |
+
}
|
208 |
+
|
209 |
+
.low-health {
|
210 |
+
color: #d98000; /* Orange */
|
211 |
+
font-weight: bold;
|
212 |
+
}
|
213 |
+
|
214 |
+
.critical-health {
|
215 |
+
color: #c00; /* Red */
|
216 |
+
font-weight: bold;
|
217 |
+
}
|
218 |
+
|
219 |
+
#roll-result-display {
|
220 |
+
margin-top: 20px;
|
221 |
+
padding: 10px;
|
222 |
+
background-color: #e8e1ca;
|
223 |
+
border-radius: 3px;
|
224 |
+
font-weight: bold;
|
225 |
+
text-align: center;
|
226 |
+
border: 1px solid #d2b48c;
|
227 |
+
}
|
228 |
+
|
229 |
+
|
230 |
+
/* --- Equipment Phase Squad List --- */
|
231 |
+
#squad-manage-list li {
|
232 |
+
display: flex;
|
233 |
+
justify-content: space-between;
|
234 |
+
align-items: center;
|
235 |
+
background-color: #faf0e6; /* Slightly different bg */
|
236 |
+
padding: 10px 15px;
|
237 |
+
}
|
238 |
+
|
239 |
+
#squad-manage-list select {
|
240 |
+
margin-left: 10px;
|
241 |
+
padding: 5px 8px;
|
242 |
+
font-family: inherit;
|
243 |
+
background-color: #fff;
|
244 |
+
border: 1px solid #c1a97a;
|
245 |
+
border-radius: 3px;
|
246 |
+
}
|
247 |
+
|
248 |
+
/* --- Result/Game Over Phases --- */
|
249 |
+
#phase-results, #game-over {
|
250 |
+
text-align: center;
|
251 |
+
padding: 30px;
|
252 |
+
}
|
253 |
+
|
254 |
+
#game-over {
|
255 |
+
color: #a00; /* Darker red */
|
256 |
+
background-color: rgba(255, 0, 0, 0.05);
|
257 |
+
border: 2px solid #a00;
|
258 |
+
border-radius: 5px;
|
259 |
+
}
|
260 |
+
|
261 |
+
#game-over h2 {
|
262 |
+
color: #a00;
|
263 |
+
border: none;
|
264 |
+
}
|
265 |
+
|
266 |
+
#result-message {
|
267 |
+
font-size: 1.1em;
|
268 |
+
margin-bottom: 15px;
|
269 |
+
font-weight: bold;
|
270 |
+
}
|
271 |
+
|
272 |
+
#phase-results button, #game-over button {
|
273 |
+
margin: 10px; /* Space out buttons */
|
274 |
+
}
|