murapolo commited on
Commit
8062c1f
·
verified ·
1 Parent(s): 0968450

Upload 3 files

Browse files
Files changed (3) hide show
  1. index.html +79 -573
  2. script.js +1034 -0
  3. style.css +274 -28
index.html CHANGED
@@ -1,574 +1,80 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Изометрический шутер</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
8
- <style>
9
- body {
10
- margin: 0;
11
- overflow: hidden;
12
- background-color: #111;
13
- font-family: 'Arial', sans-serif;
14
- }
15
-
16
- #game-container {
17
- position: relative;
18
- width: 100vw;
19
- height: 100vh;
20
- perspective: 1000px;
21
- }
22
-
23
- #game-board {
24
- position: absolute;
25
- top: 50%;
26
- left: 50%;
27
- transform: translate(-50%, -50%) rotateX(60deg) rotateZ(45deg);
28
- transform-style: preserve-3d;
29
- width: 800px;
30
- height: 800px;
31
- background-color: #333;
32
- border: 2px solid #555;
33
- overflow: hidden;
34
- }
35
-
36
- .tile {
37
- position: absolute;
38
- width: 40px;
39
- height: 40px;
40
- background-color: #444;
41
- transform-style: preserve-3d;
42
- box-shadow: inset 0 0 10px rgba(0,0,0,0.5);
43
- }
44
-
45
- .wall {
46
- background-color: #666;
47
- box-shadow: none;
48
- }
49
-
50
- .player {
51
- position: absolute;
52
- width: 30px;
53
- height: 30px;
54
- background-color: #3498db;
55
- border-radius: 50%;
56
- transform: translate(-50%, -50%);
57
- z-index: 10;
58
- transition: all 0.1s ease;
59
- }
60
-
61
- .enemy {
62
- position: absolute;
63
- width: 30px;
64
- height: 30px;
65
- background-color: #e74c3c;
66
- border-radius: 50%;
67
- transform: translate(-50%, -50%);
68
- z-index: 5;
69
- transition: all 0.2s ease;
70
- }
71
-
72
- .bullet {
73
- position: absolute;
74
- width: 8px;
75
- height: 8px;
76
- background-color: #f1c40f;
77
- border-radius: 50%;
78
- transform: translate(-50%, -50%);
79
- z-index: 8;
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} &nbsp; ✨Л ${totalStats.agility} &nbsp; 🎯М ${totalStats.marksmanship} &nbsp; 😊Х ${totalStats.charisma}`;
295
+ // --- End of updated line --- (Used &nbsp; 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} &nbsp; ✨Л ${cowboy.stats.agility} &nbsp; 🎯М ${cowboy.stats.marksmanship} &nbsp; 😊Х ${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
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
-
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
17
-
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
- }
25
-
26
- .card p:last-child {
27
- margin-bottom: 0;
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
+ }