isoshooter / script.js
murapolo's picture
Upload 3 files
8062c1f verified
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} &nbsp; ✨Л ${totalStats.agility} &nbsp; 🎯М ${totalStats.marksmanship} &nbsp; 😊Х ${totalStats.charisma}`;
// --- End of updated line --- (Used &nbsp; 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} &nbsp; ✨Л ${cowboy.stats.agility} &nbsp; 🎯М ${cowboy.stats.marksmanship} &nbsp; 😊Х ${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();
});