Spaces:
Running
Running
document.addEventListener('DOMContentLoaded', () => { | |
// --- Глобальные переменные состояния игры --- | |
let playerMoney = 150; // Start with slightly more money | |
let squad = []; // Массив объектов ковбоев в отряде | |
let availableCowboys = []; // Ковбои, доступные для найма | |
let shopItems = []; // Предметы в магазине | |
let availablePlans = []; // Доступные планы ограблений | |
let currentPlan = null; // Выбранный план | |
let currentRobberyEvent = null; // Текущее событие в ограблении | |
let currentRobberyProgress = 0; // Условный прогресс (можно расширить) | |
let gameState = 'hire'; // Текущая фаза: hire, equip, plan, robbery, results, gameover | |
// --- Элементы DOM --- | |
const moneyEl = document.getElementById('money'); | |
const squadStatsEl = document.getElementById('squad-stats'); | |
const equipmentSummaryEl = document.getElementById('equipment-summary'); | |
const mainContentEl = document.getElementById('main-content'); | |
const hireListEl = document.getElementById('hire-list'); | |
const shopListEl = document.getElementById('shop-list'); | |
const squadManageListEl = document.getElementById('squad-manage-list'); | |
const planListEl = document.getElementById('plan-list'); | |
const squadHealthDisplayEl = document.getElementById('squad-health-display'); | |
const eventDescriptionEl = document.getElementById('event-description'); | |
const choicesEl = document.getElementById('choices'); | |
const rollResultDisplayEl = document.getElementById('roll-result-display'); | |
const resultMessageEl = document.getElementById('result-message'); | |
const xpGainedEl = document.getElementById('xp-gained'); | |
const gameOverEl = document.getElementById('game-over'); | |
// --- Кнопки навигации/действий --- | |
const goToEquipmentBtn = document.getElementById('go-to-equipment-btn'); | |
const goToPlanBtn = document.getElementById('go-to-plan-btn'); | |
const continueGameBtn = document.getElementById('continue-game-btn'); | |
const newGameBtn = document.getElementById('new-game-btn'); | |
const restartGameBtn = document.getElementById('restart-game-btn'); // Из GameOver | |
// --- Игровые Константы и Настройки --- | |
const cowboyNames = ["Джед", "Билли", "Сэм", "Клэй", "Дасти", "Хосе", "Уайетт", "Док", "Барт", "Коул"]; // Added more names | |
const cowboyStats = ["strength", "agility", "marksmanship", "charisma"]; // Сила, Ловкость, Меткость, Харизма | |
const itemNames = ["Хороший Револьвер", "Винтовка", "Динамит", "Аптечка", "Отмычки", "Бронежилет", "Шляпа Удачи"]; // Added another item | |
const planNames = ["Ограбление Почтового Вагона", "Нападение на Мосту", "Засада в Каньоне", "Тихое Проникновение"]; // Added another plan | |
const MAX_LEVEL = 10; | |
const XP_PER_LEVEL = 100; | |
const DICE_SIDES = 10; // Используем D10 для проверок | |
// --- Основные Функции Игры --- | |
function initGame() { | |
playerMoney = 300; // Начальные деньги | |
squad = []; | |
availableCowboys = []; | |
shopItems = []; | |
availablePlans = []; | |
currentPlan = null; | |
currentRobberyEvent = null; | |
currentRobberyProgress = 0; | |
gameState = 'hire'; | |
gameOverEl.style.display = 'none'; | |
mainContentEl.style.display = 'block'; | |
generateInitialData(); | |
updateStatusBar(); | |
switchPhase('hire'); // Ensure starting phase is rendered correctly | |
} | |
function generateInitialData() { | |
// Генерируем ковбоев для найма | |
availableCowboys = []; | |
for (let i = 0; i < 5; i++) { | |
availableCowboys.push(generateCowboy()); | |
} | |
// Генерируем предметы для магазина | |
shopItems = []; | |
for (let i = 0; i < 4; i++) { | |
// Ensure variety by removing chosen names temporarily | |
let tempItemNames = [...itemNames]; | |
const itemName = tempItemNames.splice(Math.floor(Math.random() * tempItemNames.length), 1)[0]; | |
shopItems.push(generateItem(itemName)); | |
} | |
// Add specific items if desired | |
if (!shopItems.some(item => item.name === "Аптечка")) { | |
shopItems.push(generateItem("Аптечка")); | |
} | |
if (!shopItems.some(item => item.name === "Динамит")) { | |
shopItems.push(generateItem("Динамит")); | |
} | |
// Генерируем планы | |
availablePlans = []; | |
let tempPlanNames = [...planNames]; | |
for (let i = 0; i < 3; i++) { | |
if (tempPlanNames.length === 0) break; // Avoid errors if not enough names | |
const planName = tempPlanNames.splice(Math.floor(Math.random() * tempPlanNames.length), 1)[0]; | |
availablePlans.push(generatePlan(planName, i)); | |
} | |
} | |
// --- Генерация Случайных Данных --- | |
function generateCowboy() { | |
// Generate a 3-character alphanumeric suffix | |
const randomSuffix = Math.random().toString(36).substring(2, 5); | |
const name = cowboyNames[Math.floor(Math.random() * cowboyNames.length)] + " " + randomSuffix; // Example: "Уайетт 5xs" | |
const stats = {}; | |
let totalStatPoints = 10 + Math.floor(Math.random() * 12); // Slightly wider range | |
cowboyStats.forEach(stat => stats[stat] = 1); | |
totalStatPoints -= cowboyStats.length; | |
while (totalStatPoints > 0) { | |
stats[cowboyStats[Math.floor(Math.random() * cowboyStats.length)]]++; | |
totalStatPoints--; | |
} | |
const level = 1; | |
const xp = 0; | |
const maxHealth = 50 + stats.strength * 5 + Math.floor(Math.random() * 15); // Slightly higher health potential | |
// Adjusted cost calculation | |
const cost = 20 + Object.values(stats).reduce((a, b) => a + b, 0) * 3 + Math.floor(maxHealth / 8); | |
return { | |
id: Date.now() + Math.random(), | |
name: name, | |
stats: stats, | |
health: maxHealth, | |
maxHealth: maxHealth, | |
level: level, | |
xp: xp, | |
cost: cost, | |
weapon: null, | |
equipment: [] | |
}; | |
} | |
function generateItem(baseItemName = null) { | |
// If no name provided, pick one randomly | |
const baseItem = baseItemName || itemNames[Math.floor(Math.random() * itemNames.length)]; | |
let cost = 15 + Math.floor(Math.random() * 50); | |
let type = "equipment"; | |
let effect = {}; | |
switch (baseItem) { | |
case "Хороший Револьвер": | |
effect = { marksmanship_bonus: 2 + Math.floor(Math.random() * 3) }; // 2-4 | |
type = "weapon"; | |
cost += 25; | |
break; | |
case "Винтовка": | |
effect = { marksmanship_bonus: 4 + Math.floor(Math.random() * 4) }; // 4-7 | |
type = "weapon"; | |
cost += 45; | |
break; | |
case "Динамит": | |
effect = { demolition_chance: 0.25 }; // +25% | |
type = "consumable"; | |
cost += 20; | |
break; | |
case "Аптечка": | |
effect = { health: 25 + Math.floor(Math.random() * 16) }; // 25-40 health | |
type = "consumable"; | |
cost += 15; | |
break; | |
case "Отмычки": | |
effect = { lockpick_chance: 0.20 }; // +20% | |
type = "equipment"; | |
cost += 30; | |
break; | |
case "Бронежилет": | |
effect = { damage_reduction: 0.15 }; // 15% reduction | |
type = "equipment"; | |
cost += 40; | |
break; | |
case "Шляпа Удачи": | |
effect = { charisma_bonus: 1, luck_bonus: 0.05 }; // +1 Charisma, +5% general luck (need to implement luck) | |
type = "equipment"; | |
cost += 25; | |
break; | |
} | |
return { | |
id: Date.now() + Math.random(), | |
name: baseItem, | |
type: type, | |
effect: effect, | |
cost: cost | |
}; | |
} | |
function generatePlan(planName, index) { | |
const difficulty = 40 + Math.floor(Math.random() * 60) + index * 15; // Base difficulty + random + index scaling | |
const potentialReward = 80 + Math.floor(Math.random() * 120) + difficulty * 2.5; // More varied reward | |
// Simplified event structure - can be vastly expanded | |
const events = [ | |
{ | |
description: `Подход к поезду (${planName}). Охрана патрулирует. Ваши действия?`, | |
choices: [ | |
{ text: "Прокрасться мимо (Ловкость)", requiredStat: "agility", difficulty: difficulty * 0.9 }, | |
{ text: "Создать отвлекающий шум (Харизма)", requiredStat: "charisma", difficulty: difficulty * 1.0 }, | |
{ text: "Устранить охранника тихо (Меткость?)", requiredStat: "marksmanship", difficulty: difficulty * 1.1 }, // Risky use of Marksmanship | |
], | |
successReward: { progress: 1 }, | |
failurePenalty: { health: 10 } | |
}, | |
{ | |
description: "Нужно проникнуть в вагон с ценностями.", | |
choices: [ | |
{ text: "Взломать замок (Ловкость + Отмычки?)", requiredStat: "agility", difficulty: difficulty * 1.0 }, | |
{ text: "Выбить дверь (Сила)", requiredStat: "strength", difficulty: difficulty * 0.9 }, | |
{ text: "Использовать Динамит? (Сила + Динамит?)", requiredStat: "strength", difficulty: difficulty * 0.7 } // Easier but noisy & uses item | |
], | |
successReward: { progress: 1, money: potentialReward * 0.1 }, // Small initial reward | |
failurePenalty: { health: 15, money: -15 } | |
}, | |
{ | |
description: "Внутри! Забрать добычу и быстро уходить!", | |
choices: [ | |
{ text: "Хватать все и бежать (Ловкость)", requiredStat: "agility", difficulty: difficulty * 1.1 }, | |
{ text: "Прикрывать отход (Меткость)", requiredStat: "marksmanship", difficulty: difficulty * 1.0 }, | |
{ text: "Забаррикадировать дверь (Сила)", requiredStat: "strength", difficulty: difficulty * 1.0 } | |
], | |
successReward: { money: potentialReward * 0.9, xp: 50 + difficulty / 2, progress: 1 }, // Main reward + scaled XP | |
failurePenalty: { health: 25, money: -potentialReward * 0.3 } | |
} | |
]; | |
return { | |
id: Date.now() + Math.random(), | |
name: planName, | |
description: `Сложность: ~${difficulty}, Награда: ~$${Math.round(potentialReward)}`, | |
baseDifficulty: difficulty, | |
potentialReward: Math.round(potentialReward), | |
events: events | |
}; | |
} | |
function getRobberyEvent() { | |
if (currentPlan && currentPlan.events.length > currentRobberyProgress) { | |
return currentPlan.events[currentRobberyProgress]; | |
} | |
return null; | |
} | |
// --- Обновление Интерфейса --- | |
function updateStatusBar() { | |
moneyEl.textContent = playerMoney; | |
const totalStats = { strength: 0, agility: 0, marksmanship: 0, charisma: 0 }; | |
let equipmentText = []; | |
let consumableText = []; // Separate consumables | |
squad.forEach(cowboy => { | |
// Base stats | |
Object.keys(totalStats).forEach(stat => { | |
totalStats[stat] += cowboy.stats[stat] || 0; // Ensure stat exists | |
}); | |
// Bonuses from weapon | |
if (cowboy.weapon) { | |
equipmentText.push(cowboy.weapon.name); | |
Object.keys(totalStats).forEach(stat => { | |
if (cowboy.weapon.effect[`${stat}_bonus`]) { | |
totalStats[stat] += cowboy.weapon.effect[`${stat}_bonus`]; | |
} | |
}); | |
} | |
// Bonuses from equipment and list consumables | |
cowboy.equipment.forEach(item => { | |
if (item.type === 'consumable') { | |
// Count consumables instead of just listing names once | |
let existing = consumableText.find(c => c.name === item.name); | |
if (existing) { | |
existing.count++; | |
} else { | |
consumableText.push({ name: item.name, count: 1 }); | |
} | |
} else { | |
equipmentText.push(item.name); // Add other equipment names | |
} | |
// Apply stat bonuses from all equipment/consumables (if any) | |
Object.keys(totalStats).forEach(stat => { | |
if (item.effect && item.effect[`${stat}_bonus`]) { | |
totalStats[stat] += item.effect[`${stat}_bonus`]; | |
} | |
}); | |
}); | |
}); | |
// --- Updated line with emojis for total stats --- | |
squadStatsEl.innerHTML = `💪С ${totalStats.strength} ✨Л ${totalStats.agility} 🎯М ${totalStats.marksmanship} 😊Х ${totalStats.charisma}`; | |
// --- End of updated line --- (Used for spacing) | |
// Combine and display equipment/consumables summary | |
const uniqueEquipment = [...new Set(equipmentText)]; | |
let summary = uniqueEquipment.slice(0, 2).join(', '); // Show max 2 permanent items | |
if (uniqueEquipment.length > 2) summary += '...'; | |
if (consumableText.length > 0) { | |
// Format consumables as "Name(count)" | |
let consumableSummary = consumableText | |
.map(c => `${c.name}(${c.count})`) | |
.slice(0, 2) // Show max 2 types of consumables | |
.join(', '); | |
if (consumableText.length > 2) consumableSummary += '...'; | |
summary += (summary ? ' / ' : '') + 'Расх: ' + consumableSummary; | |
} | |
equipmentSummaryEl.textContent = summary || "Ничего"; | |
} | |
function updateSquadHealthDisplay() { | |
if (gameState !== 'robbery') { | |
squadHealthDisplayEl.innerHTML = ''; | |
squadHealthDisplayEl.style.display = 'none'; // Hide if not in robbery | |
return; | |
} | |
squadHealthDisplayEl.style.display = 'block'; // Show if in robbery | |
squadHealthDisplayEl.innerHTML = "Здоровье отряда: "; | |
if (squad.length === 0) { | |
squadHealthDisplayEl.innerHTML += "Отряд пуст!"; | |
return; | |
} | |
squad.forEach(cowboy => { | |
const healthSpan = document.createElement('span'); | |
healthSpan.classList.add('cowboy-health'); | |
// Calculate percentage for styling, handle division by zero | |
const healthPercent = cowboy.maxHealth > 0 ? (cowboy.health / cowboy.maxHealth) * 100 : 0; | |
healthSpan.textContent = `${cowboy.name}: ${cowboy.health}/${cowboy.maxHealth} HP`; | |
healthSpan.classList.remove('low-health', 'critical-health'); // Reset classes | |
if (healthPercent <= 20) { | |
healthSpan.classList.add('critical-health'); | |
} else if (healthPercent <= 50) { | |
healthSpan.classList.add('low-health'); | |
} | |
squadHealthDisplayEl.appendChild(healthSpan); | |
}); | |
} | |
// --- Рендеринг Фаз Игры --- | |
function switchPhase(newPhase) { | |
console.log("Switching phase to:", newPhase); | |
// Hide all phases | |
document.querySelectorAll('#main-content > div[id^="phase-"]').forEach(div => div.style.display = 'none'); | |
gameOverEl.style.display = 'none'; | |
mainContentEl.style.display = 'block'; // Ensure main content is visible unless game over | |
gameState = newPhase; | |
// Show the target phase | |
const phaseEl = document.getElementById(`phase-${newPhase}`); | |
if (phaseEl) { | |
phaseEl.style.display = 'block'; | |
// Call the corresponding render function | |
switch (newPhase) { | |
case 'hire': renderHirePhase(); break; | |
case 'equip': renderEquipPhase(); break; | |
case 'plan': renderPlanPhase(); break; | |
case 'robbery': renderRobberyPhase(); break; | |
case 'results': /* Handled by endRobbery calling renderResultsPhase */ break; | |
} | |
} else if (newPhase === 'gameover') { | |
gameOverEl.style.display = 'block'; | |
mainContentEl.style.display = 'none'; // Hide main content on game over | |
} else { | |
console.error("Unknown phase:", newPhase); | |
} | |
updateStatusBar(); // Update status bar on every phase switch | |
updateSquadHealthDisplay(); // Update health display visibility | |
} | |
function renderHirePhase() { | |
hireListEl.innerHTML = ''; // Clear list | |
if (availableCowboys.length === 0) { | |
hireListEl.innerHTML = '<li>Нет доступных ковбоев для найма.</li>'; | |
} else { | |
availableCowboys.forEach(cowboy => { | |
const li = document.createElement('li'); | |
// --- Updated line with emojis --- | |
li.innerHTML = ` | |
<div> | |
<b>${cowboy.name}</b> (Ур: ${cowboy.level}, Зд: ${cowboy.health}/${cowboy.maxHealth})<br> | |
Характеристики: 💪С ${cowboy.stats.strength} ✨Л ${cowboy.stats.agility} 🎯М ${cowboy.stats.marksmanship} 😊Х ${cowboy.stats.charisma}<br> | |
Цена: $${cowboy.cost} | |
</div> | |
<button data-cowboy-id="${cowboy.id}" ${playerMoney < cowboy.cost ? 'disabled' : ''}>Нанять</button> | |
`; | |
// --- End of updated line --- | |
li.querySelector('button').addEventListener('click', () => handleHire(cowboy.id)); | |
hireListEl.appendChild(li); | |
}); | |
} | |
// Disable "Go to Equipment" if no squad members | |
goToEquipmentBtn.disabled = squad.length === 0; | |
} | |
function renderEquipPhase() { | |
shopListEl.innerHTML = ''; // Clear shop | |
if (shopItems.length === 0) { | |
shopListEl.innerHTML = '<li>Магазин пуст.</li>'; | |
} else { | |
shopItems.forEach(item => { | |
const li = document.createElement('li'); | |
let effectDesc = Object.entries(item.effect) | |
.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 | |
.join(', '); | |
li.innerHTML = ` | |
<div> | |
<b>${item.name}</b> (${item.type === 'consumable' ? 'Расходуемый' : item.type === 'weapon' ? 'Оружие' : 'Снаряжение'})<br> | |
Эффект: ${effectDesc || 'Нет'}<br> | |
Цена: $${item.cost} | |
</div> | |
<button data-item-id="${item.id}" ${playerMoney < item.cost || squad.length === 0 ? 'disabled' : ''}>Купить</button> | |
`; | |
li.querySelector('button').addEventListener('click', () => handleBuy(item.id)); | |
shopListEl.appendChild(li); | |
}); | |
} | |
// Display squad for equipment management | |
squadManageListEl.innerHTML = ''; | |
if (squad.length === 0) { | |
squadManageListEl.innerHTML = '<li>Ваш отряд пуст.</li>'; | |
} else { | |
squad.forEach(cowboy => { | |
const li = document.createElement('li'); | |
const equipmentList = cowboy.equipment.map(e => e.name).join(', ') || 'Нет'; | |
li.innerHTML = ` | |
<span><b>${cowboy.name}</b> (Зд: ${cowboy.health}/${cowboy.maxHealth}) Оружие: ${cowboy.weapon ? cowboy.weapon.name : 'Нет'}, Снаряжение: ${equipmentList}</span> | |
<!-- Basic 'Use Medkit' button --> | |
${cowboy.equipment.some(e => e.name === 'Аптечка' && cowboy.health < cowboy.maxHealth) ? | |
`<button class="use-item-btn" data-cowboy-id="${cowboy.id}" data-item-name="Аптечка">Исп. Аптечку</button>` : ''} | |
`; | |
// Add event listener for using items if button exists | |
const useButton = li.querySelector('.use-item-btn'); | |
if (useButton) { | |
useButton.addEventListener('click', (e) => { | |
const cowboyId = e.target.getAttribute('data-cowboy-id'); | |
const itemName = e.target.getAttribute('data-item-name'); | |
handleUseItem(cowboyId, itemName); | |
}); | |
} | |
squadManageListEl.appendChild(li); | |
}); | |
} | |
// Disable "Go to Plan" if no squad members | |
goToPlanBtn.disabled = squad.length === 0; | |
} | |
function handleUseItem(cowboyId, itemName) { | |
const cowboy = squad.find(c => c.id == cowboyId); // Use == for type flexibility if needed, or === if strict | |
if (!cowboy) return; | |
const itemIndex = cowboy.equipment.findIndex(item => item.name === itemName && item.type === 'consumable'); | |
if (itemIndex > -1) { | |
const item = cowboy.equipment[itemIndex]; | |
let used = false; | |
if (item.name === "Аптечка" && cowboy.health < cowboy.maxHealth) { | |
const healAmount = item.effect.health || 25; | |
const actualHeal = Math.min(healAmount, cowboy.maxHealth - cowboy.health); // Don't overheal | |
cowboy.health += actualHeal; | |
used = true; | |
console.log(`${cowboy.name} использовал ${item.name}, восстановлено ${actualHeal} здоровья.`); | |
} | |
// Add other usable items here (e.g., Dynamite outside of combat?) | |
if (used) { | |
cowboy.equipment.splice(itemIndex, 1); // Remove used consumable | |
updateStatusBar(); | |
renderEquipPhase(); // Re-render the equipment phase to show changes | |
} | |
} | |
} | |
function renderPlanPhase() { | |
planListEl.innerHTML = ''; // Clear plan list | |
if (availablePlans.length === 0) { | |
planListEl.innerHTML = '<li>Нет доступных планов для ограбления.</li>'; | |
} else { | |
availablePlans.forEach(plan => { | |
const li = document.createElement('li'); | |
li.innerHTML = ` | |
<div> | |
<b>${plan.name}</b><br> | |
Описание: ${plan.description}<br> | |
</div> | |
<button data-plan-id="${plan.id}">Выбрать</button> | |
`; | |
li.querySelector('button').addEventListener('click', () => handleChoosePlan(plan.id)); | |
planListEl.appendChild(li); | |
}); | |
} | |
} | |
function renderRobberyPhase() { | |
currentRobberyEvent = getRobberyEvent(); | |
updateSquadHealthDisplay(); | |
rollResultDisplayEl.textContent = ''; | |
if (squad.length === 0 && gameState === 'robbery') { | |
console.log("Game over triggered from renderRobberyPhase - squad empty"); | |
setTimeout(() => switchPhase('gameover'), 500); // Delay slightly | |
return; | |
} | |
if (!currentRobberyEvent) { | |
console.log("No more events for this plan."); | |
// Check if we successfully completed the required progress | |
if (currentPlan && currentRobberyProgress >= currentPlan.events.length) { | |
endRobbery(true, `Ограбление "${currentPlan.name}" успешно завершено!`); | |
} else { | |
// Didn't finish all steps, might be considered a partial success or failure depending on logic | |
endRobbery(false, "Ограбление прервано или не завершено."); | |
} | |
return; | |
} | |
eventDescriptionEl.textContent = currentRobberyEvent.description; | |
choicesEl.innerHTML = ''; // Clear old choices | |
currentRobberyEvent.choices.forEach(choice => { | |
const button = document.createElement('button'); | |
let bonusChanceText = ''; | |
// Check for relevant items | |
if (choice.text.toLowerCase().includes('динамит') && squadHasItemType('consumable', 'Динамит')) { | |
bonusChanceText = ' (+Динамит)'; | |
} else if (choice.text.toLowerCase().includes('взломать') && squadHasItemType('equipment', 'Отмычки')) { | |
bonusChanceText = ' (+Отмычки)'; | |
} else if (choice.requiredStat === 'strength' && squadHasItemType('consumable', 'Динамит') && !bonusChanceText) { | |
// Generic dynamite bonus for strength if not explicitly mentioned | |
// bonusChanceText = ' (?+Динамит)'; // Maybe don't show if not obvious | |
} | |
button.innerHTML = ` | |
${choice.text} | |
<span class="stat-requirement">(Проверка: ${choice.requiredStat}, Сложность: ${choice.difficulty})${bonusChanceText}</span> | |
`; | |
button.addEventListener('click', () => handleChoice(choice.requiredStat, choice.difficulty, choice)); | |
choicesEl.appendChild(button); | |
}); | |
} | |
function renderResultsPhase(success, message, xp) { | |
resultMessageEl.textContent = message; | |
xpGainedEl.textContent = xp; | |
// Ensure buttons are displayed correctly based on success AND if squad survived | |
const canContinue = success && squad.length > 0; | |
continueGameBtn.style.display = canContinue ? 'block' : 'none'; | |
newGameBtn.style.display = 'block'; // Always allow starting a new game from results | |
// Make sure continue button is enabled/disabled correctly | |
continueGameBtn.disabled = !canContinue; | |
} | |
// --- Обработчики Действий --- | |
function handleHire(cowboyId) { | |
const cowboy = availableCowboys.find(c => c.id == cowboyId); // Use == for potential string/number mismatch | |
if (cowboy && playerMoney >= cowboy.cost) { | |
playerMoney -= cowboy.cost; | |
squad.push(cowboy); | |
availableCowboys = availableCowboys.filter(c => c.id != cowboyId); | |
console.log(`Нанят ${cowboy.name}`); | |
updateStatusBar(); | |
renderHirePhase(); // Update hire list and buttons | |
renderEquipPhase(); // Also update squad list in equip phase if visible | |
} else if (cowboy) { | |
alert("Недостаточно денег!"); | |
} | |
} | |
function handleBuy(itemId) { | |
const item = shopItems.find(i => i.id == itemId); | |
if (!item) return; | |
if (playerMoney < item.cost) { | |
alert("Недостаточно денег!"); | |
return; | |
} | |
if (squad.length === 0) { | |
alert("Сначала наймите ковбоев, чтобы дать им снаряжение!"); | |
return; | |
} | |
// --- More robust item assignment --- | |
let assigned = false; | |
if (item.type === 'weapon') { | |
// Try to give to someone without a weapon first | |
let targetCowboy = squad.find(c => !c.weapon); | |
if (targetCowboy) { | |
targetCowboy.weapon = item; | |
assigned = true; | |
console.log(`${item.name} выдан ${targetCowboy.name}`); | |
} else { | |
// If everyone has a weapon, replace the first cowboy's (simple logic) | |
// In a real game, you'd let the player choose or compare stats | |
squad[0].weapon = item; | |
assigned = true; | |
console.log(`${item.name} выдан ${squad[0].name} (заменил старое)`); | |
} | |
} else if (item.type === 'equipment' || item.type === 'consumable') { | |
// Find cowboy with fewest equipment items to distribute somewhat evenly | |
let targetCowboy = squad.reduce((prev, curr) => { | |
return (curr.equipment.length < prev.equipment.length) ? curr : prev; | |
}); | |
targetCowboy.equipment.push(item); | |
assigned = true; | |
console.log(`${item.name} добавлен в снаряжение ${targetCowboy.name}`); | |
} | |
if (assigned) { | |
playerMoney -= item.cost; | |
// Remove *one* instance of the bought item from the shop | |
const itemIndexInShop = shopItems.findIndex(i => i.id == itemId); | |
if(itemIndexInShop > -1) { | |
shopItems.splice(itemIndexInShop, 1); | |
} | |
console.log(`Куплен ${item.name}`); | |
updateStatusBar(); | |
renderEquipPhase(); // Update shop and squad display | |
} else { | |
alert("Не удалось назначить предмет."); // Should not happen with current logic if squad exists | |
} | |
} | |
function handleChoosePlan(planId) { | |
currentPlan = availablePlans.find(p => p.id == planId); | |
if (currentPlan) { | |
console.log(`Выбран план: ${currentPlan.name}`); | |
currentRobberyProgress = 0; | |
switchPhase('robbery'); | |
} | |
} | |
function handleChoice(stat, difficulty, choiceData) { | |
console.log(`Выбрано действие: ${choiceData.text}, Проверка: ${stat}, Сложность: ${difficulty}`); | |
performCheck(stat, difficulty, choiceData); | |
} | |
function performCheck(stat, difficulty, choiceData) { | |
if (squad.length === 0) { | |
rollResultDisplayEl.textContent = "Нет отряда для выполнения действия!"; | |
// Treat as failure? | |
handleFailure(choiceData, "Отряд пуст!"); | |
return; | |
} | |
// Sum the required stat across the squad, including bonuses | |
let totalStatValue = squad.reduce((sum, cowboy) => { | |
let effectiveStat = cowboy.stats[stat] || 0; | |
if (cowboy.weapon && cowboy.weapon.effect && cowboy.weapon.effect[`${stat}_bonus`]) { | |
effectiveStat += cowboy.weapon.effect[`${stat}_bonus`]; | |
} | |
cowboy.equipment.forEach(item => { | |
if (item.effect && item.effect[`${stat}_bonus`]) { | |
effectiveStat += item.effect[`${stat}_bonus`]; | |
} | |
}); | |
return sum + effectiveStat; | |
}, 0); | |
// Check for item-specific bonuses/effects mentioned in the choice | |
let bonusChance = 0; // Represents a multiplier bonus, e.g., 0.2 for +20% | |
let itemUsed = null; // Track if a consumable is used | |
if (choiceData.text.toLowerCase().includes('динамит') && squadHasItemType('consumable', 'Динамит')) { | |
const dynamite = findItemInSquad('consumable', 'Динамит'); // Find the item to get its effect | |
if(dynamite && dynamite.effect.demolition_chance) { | |
bonusChance += dynamite.effect.demolition_chance; | |
itemUsed = { type: 'consumable', name: 'Динамит' }; // Mark dynamite for removal | |
console.log("Используется Динамит! Бонус шанса: +", bonusChance * 100, "%"); | |
} | |
} else if (choiceData.text.toLowerCase().includes('взломать') && squadHasItemType('equipment', 'Отмычки')) { | |
const lockpicks = findItemInSquad('equipment', 'Отмычки'); // Find the item | |
if(lockpicks && lockpicks.effect.lockpick_chance) { | |
bonusChance += lockpicks.effect.lockpick_chance; // Permanent bonus, item not used up | |
console.log("Используются Отмычки! Бонус шанса: +", bonusChance * 100, "%"); | |
} | |
} | |
// Add luck bonus from items like "Шляпа Удачи" (needs implementation) | |
// bonusChance += squad.reduce((luck, c) => luck + (c.equipment.find(e => e.name === "Шляпа Удачи")?.effect.luck_bonus || 0), 0); | |
// Dice roll (D10) | |
const diceRoll = Math.floor(Math.random() * DICE_SIDES) + 1; | |
// Core check calculation: (Dice * Stat) compared to Difficulty | |
// Apply bonus chance multiplicatively to the *score* or additively to the *roll* - let's try multiplying score | |
const baseScore = diceRoll * totalStatValue; | |
const finalScore = baseScore * (1 + bonusChance); // Apply bonus multiplier | |
console.log(`Бросок D${DICE_SIDES}: ${diceRoll}, Суммарная характеристика (${stat}): ${totalStatValue}, Базовый результат: ${baseScore}, Модификатор шанса: ${(1 + bonusChance).toFixed(2)}x, Финальный результат: ${finalScore.toFixed(2)}, Сложность: ${difficulty}`); | |
rollResultDisplayEl.textContent = `Бросок: ${diceRoll} × ${totalStatValue} (${stat}) × ${(1 + bonusChance).toFixed(2)} (бонус) = ${finalScore.toFixed(2)} / Требуется: ${difficulty}`; | |
// Consume the item if it was marked | |
if (itemUsed) { | |
removeItemFromSquad(itemUsed.type, itemUsed.name); // Remove one instance | |
} | |
if (finalScore >= difficulty) { | |
console.log("Успех!"); | |
handleSuccess(choiceData); | |
} else { | |
console.log("Провал!"); | |
handleFailure(choiceData); | |
} | |
} | |
// Helper to check if *any* cowboy has an item | |
function squadHasItemType(type, name = null) { | |
return squad.some(cowboy => | |
(cowboy.weapon && cowboy.weapon.type === type && (!name || cowboy.weapon.name === name)) || | |
cowboy.equipment.some(item => item.type === type && (!name || item.name === name)) | |
); | |
} | |
// Helper to find the first instance of an item in the squad (to get its effect details) | |
function findItemInSquad(type, name) { | |
for (let cowboy of squad) { | |
if (cowboy.weapon && cowboy.weapon.type === type && cowboy.weapon.name === name) { | |
return cowboy.weapon; | |
} | |
const item = cowboy.equipment.find(item => item.type === type && item.name === name); | |
if (item) { | |
return item; | |
} | |
} | |
return null; | |
} | |
// Helper to remove the *first* instance of a consumable item found in the squad | |
function removeItemFromSquad(type, name) { | |
for (let cowboy of squad) { | |
const itemIndex = cowboy.equipment.findIndex(item => item.type === type && item.name === name); | |
if (itemIndex > -1) { | |
cowboy.equipment.splice(itemIndex, 1); | |
console.log(`Предмет ${name} использован и удален у ${cowboy.name}`); | |
updateStatusBar(); // Update equipment summary | |
return true; // Item found and removed | |
} | |
} | |
console.log(`Предмет ${name} не найден в отряде для удаления.`); | |
return false; // Item not found | |
} | |
function handleSuccess(choiceData) { | |
const eventData = currentRobberyEvent; | |
if (!eventData) return; // Should not happen if called correctly | |
let message = "Успех! "; | |
let xpEarned = 0; | |
// Apply rewards | |
if (eventData.successReward) { | |
if (eventData.successReward.money) { | |
playerMoney += eventData.successReward.money; | |
message += `Получено $${eventData.successReward.money}. `; | |
} | |
if (eventData.successReward.xp) { | |
xpEarned = eventData.successReward.xp; // Track XP for awarding later | |
message += `Получено ${xpEarned} опыта. `; | |
} | |
if (eventData.successReward.progress) { | |
currentRobberyProgress += eventData.successReward.progress; | |
message += `Продвижение по плану. `; | |
} | |
// TODO: Add item rewards here | |
} | |
// Try to use a medkit automatically if someone is injured (optional QoL) | |
const injuredCowboy = squad.find(c => c.health < c.maxHealth); | |
if (injuredCowboy && squadHasItemType('consumable', 'Аптечка')) { | |
const medkit = findItemInSquad('consumable', 'Аптечка'); | |
if (medkit && removeItemFromSquad('consumable', 'Аптечка')) { // Find and remove it | |
const healAmount = medkit.effect.health || 25; | |
const actualHeal = Math.min(healAmount, injuredCowboy.maxHealth - injuredCowboy.health); | |
injuredCowboy.health += actualHeal; | |
message += `${injuredCowboy.name} использовал аптечку (+${actualHeal} ЗД). `; | |
console.log(`${injuredCowboy.name} подлечился.`); | |
updateSquadHealthDisplay(); // Update health display immediately | |
} | |
} | |
awardXP(xpEarned); // Award XP gained from this step | |
updateStatusBar(); | |
// Move to the next event or end the robbery | |
const nextEvent = getRobberyEvent(); | |
rollResultDisplayEl.textContent += ` | ${message}`; // Append outcome to roll result | |
if (nextEvent && currentRobberyProgress < currentPlan.events.length) { // Check progress hasn't exceeded plan length | |
setTimeout(() => renderRobberyPhase(), 1500); // Pause before next step | |
} else { | |
// If progress >= length, it means the last step was successful | |
console.log("Reached end of plan events successfully."); | |
setTimeout(() => endRobbery(true, message + " Ограбление завершено!"), 1500); | |
} | |
} | |
function handleFailure(choiceData, failureReason = "Провал проверки!") { | |
const eventData = currentRobberyEvent; | |
if (!eventData) return; | |
let message = "Провал! " + failureReason + " "; | |
// Apply penalties | |
if (eventData.failurePenalty) { | |
if (eventData.failurePenalty.money) { | |
const moneyLost = Math.min(playerMoney, Math.abs(eventData.failurePenalty.money)); | |
playerMoney -= moneyLost; | |
message += `Потеряно $${moneyLost}. `; | |
} | |
if (eventData.failurePenalty.health) { | |
const damage = eventData.failurePenalty.health; | |
message += `Отряд ранен! `; | |
let casualties = 0; | |
// Apply damage, check for deaths immediately | |
const remainingSquad = []; | |
squad.forEach(cowboy => { | |
if (applyDamage(cowboy, Math.ceil(damage / Math.max(1, squad.length)) + Math.floor(Math.random() * 5))) { | |
remainingSquad.push(cowboy); // Cowboy survived | |
} else { | |
casualties++; // Cowboy died | |
message += `${cowboy.name} погиб! `; | |
} | |
}); | |
squad = remainingSquad; // Update squad with survivors | |
if (squad.length === 0) { | |
// Game Over scenario | |
rollResultDisplayEl.textContent += ` | ${message} Весь отряд погиб!`; | |
console.log("Game over triggered from handleFailure - squad wiped out."); | |
setTimeout(() => switchPhase('gameover'), 1500); | |
return; // Stop further processing | |
} | |
} | |
if (eventData.failurePenalty.progress && eventData.failurePenalty.progress < 0) { | |
currentRobberyProgress = Math.max(0, currentRobberyProgress + eventData.failurePenalty.progress); // Setback | |
message += `Продвижение отброшено назад. `; | |
} | |
// TODO: Add item loss penalty | |
} | |
updateStatusBar(); | |
updateSquadHealthDisplay(); // Update health after damage/deaths | |
// Decide whether to continue or fail the robbery | |
// Simple: continue to next step even on failure, unless squad wiped out | |
const nextEvent = getRobberyEvent(); | |
rollResultDisplayEl.textContent += ` | ${message}`; | |
if (nextEvent && currentRobberyProgress < currentPlan.events.length) { // Check if there are still events left | |
setTimeout(() => renderRobberyPhase(), 1500); | |
} else { | |
// Reached end of events after a failure, or no more events possible | |
console.log("Reached end of plan events after failure or squad wipeout."); | |
// Consider this a failed robbery outcome | |
setTimeout(() => endRobbery(false, message + " Ограбление провалено!"), 1500); | |
} | |
} | |
// Returns true if cowboy survives, false if they die | |
function applyDamage(cowboy, amount) { | |
if (!cowboy || amount <= 0) return true; // No damage or invalid cowboy | |
let damageTaken = amount; | |
const armor = cowboy.equipment.find(item => item.effect && item.effect.damage_reduction); | |
if (armor) { | |
damageTaken = Math.max(1, Math.round(amount * (1 - armor.effect.damage_reduction))); | |
console.log(`${cowboy.name} получил ${damageTaken} урона (снижено броней с ${amount})`); | |
} else { | |
console.log(`${cowboy.name} получил ${damageTaken} урона`); | |
} | |
cowboy.health -= damageTaken; | |
if (cowboy.health <= 0) { | |
console.log(`${cowboy.name} погиб!`); | |
return false; // Cowboy died | |
} | |
return true; // Cowboy survived | |
} | |
function awardXP(amount) { | |
if (squad.length === 0 || amount <= 0) return; | |
const xpPerCowboy = Math.floor(amount / squad.length); | |
if (xpPerCowboy <= 0) return; // Avoid awarding 0 XP | |
squad.forEach(cowboy => { | |
if (cowboy.level < MAX_LEVEL) { | |
cowboy.xp += xpPerCowboy; | |
console.log(`${cowboy.name} получил ${xpPerCowboy} опыта (Всего: ${cowboy.xp}).`); | |
// Check for level up | |
while (cowboy.xp >= XP_PER_LEVEL && cowboy.level < MAX_LEVEL) { | |
levelUp(cowboy); | |
} | |
} | |
}); | |
// No status bar update needed here unless level up changes stats shown there directly | |
} | |
function levelUp(cowboy) { | |
cowboy.level++; | |
cowboy.xp -= XP_PER_LEVEL; | |
console.log(`%c${cowboy.name} достиг Уровня ${cowboy.level}!`, "color: green; font-weight: bold;"); | |
// Improve stats: +1 random mandatory, +1 random optional based on luck/class? | |
const randomStat1 = cowboyStats[Math.floor(Math.random() * cowboyStats.length)]; | |
cowboy.stats[randomStat1]++; | |
let levelUpMessage = `+1 ${randomStat1}`; | |
// Increase health | |
const healthIncrease = 8 + Math.floor(Math.random() * 8) + Math.floor(cowboy.stats.strength / 2); // 8-15 + strength bonus | |
cowboy.maxHealth += healthIncrease; | |
// Heal proportional to level up health gain | |
cowboy.health = Math.min(cowboy.maxHealth, cowboy.health + healthIncrease); | |
levelUpMessage += `, +${healthIncrease} Макс. Здоровья`; | |
console.log(` ${levelUpMessage}`); | |
updateStatusBar(); // Update stats if they changed | |
} | |
function endRobbery(success, finalMessage = "") { | |
let totalXP = 0; | |
let finalReward = 0; // Track monetary reward given *at the end* | |
if (!currentPlan) { | |
console.error("Ending robbery without a current plan!"); | |
success = false; // Cannot succeed without a plan | |
finalMessage = finalMessage || "Ошибка: План ограбления не найден."; | |
} else if (success && currentRobberyProgress >= currentPlan.events.length) { | |
// Successfully completed ALL events | |
totalXP = currentPlan.baseDifficulty + Math.floor(currentPlan.potentialReward / 10); // XP for success + reward bonus | |
finalReward = currentPlan.potentialReward; // Assume full reward if not given incrementally | |
// Check if reward was already given on last step | |
const lastEvent = currentPlan.events[currentPlan.events.length - 1]; | |
if(lastEvent && lastEvent.successReward && lastEvent.successReward.money && lastEvent.successReward.money >= finalReward * 0.8) { | |
// If last step gave most of the reward, don't add it again | |
console.log("Final reward likely given on last step, not adding full amount again."); | |
finalReward = 0; // Reset final reward | |
} else { | |
playerMoney += finalReward; | |
finalMessage = finalMessage || `Ограбление "${currentPlan.name}" успешно! Добыча: $${finalReward}`; | |
} | |
finalMessage += ` (Опыт: ${totalXP})`; | |
} else if (success) { | |
// Escaped early but successfully (partial success) | |
totalXP = Math.floor((currentPlan.baseDifficulty * currentRobberyProgress / currentPlan.events.length) * 0.8); // XP proportional to progress, slight penalty for leaving early | |
finalMessage = finalMessage || `Удалось уйти с частью добычи после ${currentRobberyProgress} этапов.`; | |
finalMessage += ` (Опыт: ${totalXP})`; | |
} else { | |
// Failure | |
totalXP = Math.floor((currentPlan.baseDifficulty * currentRobberyProgress / currentPlan.events.length) * 0.1); // Minimal XP for failure based on progress | |
finalMessage = finalMessage || "Ограбление провалено!"; | |
finalMessage += ` (Опыт: ${totalXP})`; | |
} | |
// Ensure squad exists before awarding XP | |
if (squad.length > 0) { | |
awardXP(totalXP); | |
} else { | |
// If squad wiped out, ensure success is false | |
success = false; | |
finalMessage += " Отряд не выжил."; | |
} | |
console.log("Ограбление завершено. Итог:", success ? "Успех" : "Провал", "| Сообщение:", finalMessage, "| Опыт:", totalXP); | |
currentPlan = null; // Clear current plan after it's finished | |
currentRobberyProgress = 0; | |
switchPhase('results'); | |
renderResultsPhase(success, finalMessage, totalXP); | |
} | |
// --- Навигация и Перезапуск --- | |
goToEquipmentBtn.addEventListener('click', () => { | |
if (squad.length > 0) { | |
switchPhase('equip'); | |
} else { | |
alert("Сначала наймите хотя бы одного ковбоя!"); | |
} | |
}); | |
goToPlanBtn.addEventListener('click', () => { | |
if (squad.length > 0) { | |
switchPhase('plan'); | |
} else { | |
alert("Нужно нанять хотя бы одного ковбоя!"); | |
} | |
}); | |
continueGameBtn.addEventListener('click', () => { | |
if (squad.length === 0) { | |
alert("Нельзя продолжить - отряд пуст! Начните новую игру."); | |
return; | |
} | |
// Heal surviving squad members partially | |
squad.forEach(cowboy => { | |
const healAmount = Math.max(10, Math.round(cowboy.maxHealth * 0.4)); // Heal 40% or 10 HP, whichever is more | |
cowboy.health = Math.min(cowboy.maxHealth, cowboy.health + healAmount); | |
}); | |
// Generate new shop items and plans | |
generateInitialData(); // This regenerates *everything* except the squad | |
switchPhase('equip'); // Go back to the equipment phase | |
}); | |
newGameBtn.addEventListener('click', initGame); | |
restartGameBtn.addEventListener('click', initGame); // Button from Game Over screen | |
// --- Запуск Игры --- | |
initGame(); | |
}); |