1253 lines
46 KiB
JavaScript
1253 lines
46 KiB
JavaScript
// ========================================
|
||
// RPG.JS - Полная RPG система
|
||
// ========================================
|
||
|
||
const RPG = {
|
||
// Типы предметов
|
||
ItemType: {
|
||
WEAPON: 'weapon',
|
||
ARMOR: 'armor',
|
||
POTION: 'potion',
|
||
SCROLL: 'scroll',
|
||
QUEST: 'quest',
|
||
MATERIAL: 'material',
|
||
GOLD: 'gold',
|
||
FOOD: 'food',
|
||
GEM: 'gem',
|
||
KEY: 'key'
|
||
},
|
||
|
||
// Слоты экипировки
|
||
EquipmentSlot: {
|
||
HEAD: 'head',
|
||
CHEST: 'chest',
|
||
LEGS: 'legs',
|
||
FEET: 'feet',
|
||
HANDS: 'hands',
|
||
MAIN_HAND: 'main_hand',
|
||
OFF_HAND: 'off_hand',
|
||
ACCESSORY1: 'accessory1',
|
||
ACCESSORY2: 'accessory2'
|
||
},
|
||
|
||
// Типы оружия
|
||
WeaponType: {
|
||
SWORD: 'sword',
|
||
AXE: 'axe',
|
||
MACE: 'mace',
|
||
DAGGER: 'dagger',
|
||
BOW: 'bow',
|
||
STAFF: 'staff',
|
||
WAND: 'wand'
|
||
},
|
||
|
||
// Типы брони
|
||
ArmorType: {
|
||
HELMET: 'helmet',
|
||
CHESTPLATE: 'chestplate',
|
||
LEGGINGS: 'leggings',
|
||
BOOTS: 'boots',
|
||
SHIELD: 'shield',
|
||
CLOAK: 'cloak'
|
||
},
|
||
|
||
// Редкость предметов
|
||
Rarity: {
|
||
COMMON: 'common',
|
||
UNCOMMON: 'uncommon',
|
||
RARE: 'rare',
|
||
EPIC: 'epic',
|
||
LEGENDARY: 'legendary',
|
||
MYTHIC: 'mythic'
|
||
},
|
||
|
||
// Цвета редкости
|
||
RarityColors: {
|
||
common: '#b0b0b0',
|
||
uncommon: '#44ff44',
|
||
rare: '#4444ff',
|
||
epic: '#aa44ff',
|
||
legendary: '#ffaa00',
|
||
mythic: '#ff44ff'
|
||
},
|
||
|
||
// ========================================
|
||
// СОЗДАНИЕ ПЕРСОНАЖА
|
||
// ========================================
|
||
|
||
createCharacter(name, gender = 'male', classType = 'warrior') {
|
||
const classStats = {
|
||
warrior: {
|
||
hp: 120, mp: 30, str: 12, def: 10, mag: 5, spd: 8,
|
||
description: 'Сильный воин с высокой защитой'
|
||
},
|
||
mage: {
|
||
hp: 70, mp: 100, str: 4, def: 4, mag: 15, spd: 6,
|
||
description: 'Мастер магии с мощными заклинаниями'
|
||
},
|
||
archer: {
|
||
hp: 90, mp: 50, str: 10, def: 6, mag: 6, spd: 12,
|
||
description: 'Быстрый стрелок с высокой ловкостью'
|
||
},
|
||
thief: {
|
||
hp: 85, mp: 45, str: 8, def: 5, mag: 8, spd: 14,
|
||
description: 'Ловкий плут, мастер скрытности'
|
||
},
|
||
paladin: {
|
||
hp: 110, mp: 60, str: 10, def: 12, mag: 8, spd: 6,
|
||
description: 'Святой воин с лечащей магией'
|
||
},
|
||
necromancer: {
|
||
hp: 80, mp: 90, str: 6, def: 6, mag: 14, spd: 7,
|
||
description: 'Мастер темной магии и призыва'
|
||
}
|
||
};
|
||
|
||
const stats = classStats[classType] || classStats.warrior;
|
||
|
||
return {
|
||
id: 'player_' + Date.now(),
|
||
name: name,
|
||
gender: gender,
|
||
class: classType,
|
||
description: classStats[classType].description,
|
||
level: 1,
|
||
exp: 0,
|
||
expToNextLevel: 100,
|
||
hp: stats.hp,
|
||
maxHp: stats.hp,
|
||
mp: stats.mp,
|
||
maxMp: stats.mp,
|
||
baseStr: stats.str,
|
||
baseDef: stats.def,
|
||
baseMag: stats.mag,
|
||
baseSpd: stats.spd,
|
||
str: stats.str,
|
||
def: stats.def,
|
||
mag: stats.mag,
|
||
spd: stats.spd,
|
||
gold: 50,
|
||
hairColor: '#4a3520',
|
||
// Позиция
|
||
x: 5,
|
||
y: 5,
|
||
targetX: 5,
|
||
targetY: 5,
|
||
isMoving: false,
|
||
moveProgress: 0,
|
||
// Инвентарь и экипировка
|
||
inventory: [],
|
||
equipment: {
|
||
head: null,
|
||
chest: null,
|
||
legs: null,
|
||
feet: null,
|
||
main_hand: null,
|
||
off_hand: null,
|
||
accessory1: null,
|
||
accessory2: null
|
||
},
|
||
// Навыки и заклинания
|
||
skills: [],
|
||
spells: [],
|
||
learnedSpells: [],
|
||
// Квесты
|
||
quests: [],
|
||
completedQuests: [],
|
||
// Достижения
|
||
achievements: [],
|
||
// Боевые флаги
|
||
inCombat: false,
|
||
currentEnemy: null,
|
||
combatStats: {
|
||
attacks: 0,
|
||
damageDealt: 0,
|
||
damageTaken: 0,
|
||
enemiesKilled: 0
|
||
}
|
||
};
|
||
},
|
||
|
||
// ========================================
|
||
// СОЗДАНИЕ ПРЕДМЕТОВ
|
||
// ========================================
|
||
|
||
createItem(id, type, name, options = {}) {
|
||
return {
|
||
id: id || 'item_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
|
||
type: type,
|
||
name: name,
|
||
description: options.description || '',
|
||
value: options.value || 0,
|
||
rarity: options.rarity || 'common',
|
||
// Для оружия
|
||
damage: options.damage || 0,
|
||
damageType: options.damageType || 'physical', // physical, magic, fire, ice, lightning
|
||
weaponType: options.weaponType || null,
|
||
// Для брони
|
||
defense: options.defense || 0,
|
||
armorType: options.armorType || null,
|
||
// Для зелий
|
||
healAmount: options.healAmount || 0,
|
||
restoreMp: options.restoreMp || 0,
|
||
// Для еды
|
||
hungerRestore: options.hungerRestore || 0,
|
||
// Для свитков/книг
|
||
spell: options.spell || null,
|
||
spellLevel: options.spellLevel || 1,
|
||
// Требования
|
||
requiredLevel: options.requiredLevel || 1,
|
||
requiredClass: options.requiredClass || null,
|
||
// Особые свойства
|
||
special: options.special || null, // { name: 'something', value: x }
|
||
// Визуал
|
||
icon: options.icon || '?',
|
||
color: options.color || null,
|
||
// Остальное
|
||
stackable: options.stackable || false,
|
||
quantity: options.quantity || 1,
|
||
subtype: options.subtype || type,
|
||
// Статы
|
||
bonusStr: options.bonusStr || 0,
|
||
bonusDef: options.bonusDef || 0,
|
||
bonusMag: options.bonusMag || 0,
|
||
bonusSpd: options.bonusSpd || 0,
|
||
bonusHp: options.bonusHp || 0,
|
||
bonusMp: options.bonusMp || 0
|
||
};
|
||
},
|
||
|
||
// ========================================
|
||
// СОЗДАНИЕ ВРАГОВ
|
||
// ========================================
|
||
|
||
createEnemy(type, level, x, y) {
|
||
const enemyTemplates = {
|
||
// Обычные враги
|
||
goblin: {
|
||
name: 'Гоблин',
|
||
baseHp: 30, baseDmg: 8, baseDef: 2,
|
||
exp: 20, gold: 10,
|
||
lootTable: ['goblin_ear', 'herb'],
|
||
behavior: 'aggressive'
|
||
},
|
||
orc: {
|
||
name: 'Орк',
|
||
baseHp: 50, baseDmg: 12, baseDef: 4,
|
||
exp: 40, gold: 25,
|
||
lootTable: ['orc_tusk', 'meat'],
|
||
behavior: 'aggressive'
|
||
},
|
||
skeleton: {
|
||
name: 'Скелет',
|
||
baseHp: 40, baseDmg: 10, baseDef: 6,
|
||
exp: 30, gold: 15,
|
||
lootTable: ['bone', 'bone_dagger'],
|
||
behavior: 'aggressive'
|
||
},
|
||
slime: {
|
||
name: 'Слизень',
|
||
baseHp: 20, baseDmg: 5, baseDef: 0,
|
||
exp: 10, gold: 5,
|
||
lootTable: ['slime_gel'],
|
||
behavior: 'passive'
|
||
},
|
||
bandit: {
|
||
name: 'Разбойник',
|
||
baseHp: 35, baseDmg: 9, baseDef: 3,
|
||
exp: 25, gold: 20,
|
||
lootTable: ['money_pouch', 'dagger'],
|
||
behavior: 'aggressive'
|
||
},
|
||
// Маги
|
||
mage: {
|
||
name: 'Маг',
|
||
baseHp: 25, baseDmg: 20, baseDef: 2, baseMp: 50,
|
||
exp: 50, gold: 30,
|
||
lootTable: ['mana_potion', 'scroll'],
|
||
behavior: 'ranged'
|
||
},
|
||
// Элитные
|
||
troll: {
|
||
name: 'Тролль',
|
||
baseHp: 100, baseDmg: 18, baseDef: 8,
|
||
exp: 80, gold: 60,
|
||
lootTable: ['troll_heart', 'club'],
|
||
behavior: 'aggressive'
|
||
},
|
||
// Боссы
|
||
dragon: {
|
||
name: 'Дракон',
|
||
baseHp: 200, baseDmg: 30, baseDef: 15,
|
||
exp: 200, gold: 200,
|
||
lootTable: ['dragon_scale', 'dragon_heart', 'dragon_egg'],
|
||
behavior: 'boss',
|
||
isBoss: true
|
||
},
|
||
demon: {
|
||
name: 'Демон',
|
||
baseHp: 150, baseDmg: 25, baseDef: 10,
|
||
exp: 150, gold: 150,
|
||
lootTable: ['demon_heart', 'infernal_orb'],
|
||
behavior: 'boss',
|
||
isBoss: true
|
||
},
|
||
lich: {
|
||
name: 'Лич',
|
||
baseHp: 100, baseDmg: 35, baseDef: 8, baseMp: 100,
|
||
exp: 180, gold: 120,
|
||
lootTable: ['necronomicon', 'skull_staff'],
|
||
behavior: 'boss',
|
||
isBoss: true
|
||
}
|
||
};
|
||
|
||
const template = enemyTemplates[type] || enemyTemplates.goblin;
|
||
const levelMultiplier = 1 + (level - 1) * 0.2;
|
||
|
||
return {
|
||
id: 'enemy_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
|
||
type: type,
|
||
name: template.name,
|
||
level: level,
|
||
hp: Math.floor(template.baseHp * levelMultiplier),
|
||
maxHp: Math.floor(template.baseHp * levelMultiplier),
|
||
mp: (template.baseMp || 0) * levelMultiplier,
|
||
maxMp: (template.baseMp || 0) * levelMultiplier,
|
||
damage: Math.floor(template.baseDmg * levelMultiplier),
|
||
defense: Math.floor((template.baseDef || 0) * levelMultiplier),
|
||
exp: Math.floor(template.exp * levelMultiplier),
|
||
gold: Math.floor(template.gold * levelMultiplier),
|
||
lootTable: template.lootTable,
|
||
behavior: template.behavior,
|
||
isBoss: template.isBoss || false,
|
||
x: x,
|
||
y: y,
|
||
height: type === 'slime' ? 25 : 45,
|
||
isAttacking: false,
|
||
attackCooldown: 0
|
||
};
|
||
},
|
||
|
||
// ========================================
|
||
// СОЗДАНИЕ NPC
|
||
// ========================================
|
||
|
||
createNPC(name, x, y, options = {}) {
|
||
return {
|
||
id: 'npc_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
|
||
name: name,
|
||
x: x,
|
||
y: y,
|
||
color: options.color || '#8b6914',
|
||
// Диалоги
|
||
dialog: options.dialog || 'Приветствую, путник!',
|
||
dialogOptions: options.dialogOptions || [],
|
||
// Торговля
|
||
shop: options.shop || null,
|
||
shopName: options.shopName || 'Магазин',
|
||
// Квесты
|
||
quests: options.quests || [],
|
||
// Услуги
|
||
services: options.services || [], // heal, teleport, identify
|
||
// Внешность
|
||
sprite: options.sprite || 'villager',
|
||
// Отношения
|
||
faction: options.faction || 'neutral',
|
||
attitude: options.attitude || 0
|
||
};
|
||
},
|
||
|
||
// ========================================
|
||
// КВЕСТОВАЯ СИСТЕМА
|
||
// ========================================
|
||
|
||
QuestStatus: {
|
||
NOT_STARTED: 'not_started',
|
||
ACTIVE: 'active',
|
||
COMPLETED: 'completed',
|
||
FAILED: 'failed'
|
||
},
|
||
|
||
createQuest(id, name, description, options = {}) {
|
||
return {
|
||
id: id,
|
||
name: name,
|
||
description: description,
|
||
// Тип квеста
|
||
type: options.type || 'kill', // kill, collect, talk, explore, deliver
|
||
// Цели
|
||
objectives: options.objectives || [],
|
||
// Награды
|
||
rewards: options.rewards || { exp: 0, gold: 0, items: [] },
|
||
// Требования
|
||
requiredLevel: options.requiredLevel || 1,
|
||
requiredQuest: options.requiredQuest || null,
|
||
// Класс
|
||
requiredClass: options.requiredClass || null,
|
||
// Цепочка
|
||
nextQuest: options.nextQuest || null,
|
||
// Отслеживание
|
||
progress: {},
|
||
status: 'not_started'
|
||
};
|
||
},
|
||
|
||
// Добавить квест персонажу
|
||
addQuest(character, quest) {
|
||
if (character.completedQuests.includes(quest.id)) return false;
|
||
if (character.quests.find(q => q.id === quest.id)) return false;
|
||
|
||
const newQuest = JSON.parse(JSON.stringify(quest));
|
||
newQuest.status = 'active';
|
||
|
||
// Инициализация прогресса
|
||
newQuest.objectives.forEach(obj => {
|
||
newQuest.progress[obj.id] = { current: 0, target: obj.target, completed: false };
|
||
});
|
||
|
||
character.quests.push(newQuest);
|
||
return true;
|
||
},
|
||
|
||
// Обновить прогресс квеста
|
||
updateQuestProgress(character, objectiveType, targetId, amount = 1) {
|
||
character.quests.forEach(quest => {
|
||
if (quest.status !== 'active') return;
|
||
|
||
quest.objectives.forEach(obj => {
|
||
if (obj.type === objectiveType && obj.target === targetId) {
|
||
quest.progress[obj.id].current += amount;
|
||
if (quest.progress[obj.id].current >= obj.target) {
|
||
quest.progress[obj.id].completed = true;
|
||
}
|
||
}
|
||
});
|
||
});
|
||
},
|
||
|
||
// Проверить завершение квеста
|
||
checkQuestCompletion(character, questId) {
|
||
const quest = character.quests.find(q => q.id === questId);
|
||
if (!quest || quest.status !== 'active') return false;
|
||
|
||
const allCompleted = quest.objectives.every(obj => quest.progress[obj.id].completed);
|
||
if (allCompleted) {
|
||
quest.status = 'completed';
|
||
return true;
|
||
}
|
||
return false;
|
||
},
|
||
|
||
// Получить награду квеста
|
||
completeQuest(character, questId) {
|
||
const questIndex = character.quests.findIndex(q => q.id === questId);
|
||
if (questIndex === -1) return null;
|
||
|
||
const quest = character.quests[questIndex];
|
||
if (quest.status !== 'completed') return null;
|
||
|
||
// Удаляем из активных и добавляем в завершённые
|
||
character.quests.splice(questIndex, 1);
|
||
character.completedQuests.push(questId);
|
||
|
||
// Выдаём награды
|
||
character.exp += quest.rewards.exp;
|
||
character.gold += quest.rewards.gold;
|
||
|
||
const items = [];
|
||
if (quest.rewards.items) {
|
||
quest.rewards.items.forEach(itemTemplate => {
|
||
const item = this.createItem(itemTemplate.id, itemTemplate.type, itemTemplate.name, itemTemplate);
|
||
this.addItemToInventory(character, item);
|
||
items.push(item);
|
||
});
|
||
}
|
||
|
||
return { quest, items };
|
||
},
|
||
|
||
// ========================================
|
||
// МАГИЧЕСКАЯ СИСТЕМА
|
||
// ========================================
|
||
|
||
SpellType: {
|
||
FIRE: 'fire',
|
||
ICE: 'ice',
|
||
LIGHTNING: 'lightning',
|
||
HEALING: 'healing',
|
||
BUFF: 'buff',
|
||
DEBUFF: 'debuff',
|
||
SUMMON: 'summon',
|
||
DARK: 'dark',
|
||
HOLY: 'holy'
|
||
},
|
||
|
||
SpellSchool: {
|
||
ELEMENTAL: 'elemental',
|
||
MYSTIC: 'mystic',
|
||
DARK: 'dark',
|
||
HOLY: 'holy'
|
||
},
|
||
|
||
// База заклинаний
|
||
SpellDatabase: {
|
||
// Огненная магия
|
||
fireball: {
|
||
name: 'Огненный шар',
|
||
type: 'fire',
|
||
school: 'elemental',
|
||
mpCost: 15,
|
||
damage: 25,
|
||
damageType: 'fire',
|
||
range: 5,
|
||
cooldown: 3000,
|
||
description: 'Наносит урон огнём'
|
||
},
|
||
fireblast: {
|
||
name: 'Огненный взрыв',
|
||
type: 'fire',
|
||
school: 'elemental',
|
||
mpCost: 25,
|
||
damage: 40,
|
||
damageType: 'fire',
|
||
range: 3,
|
||
cooldown: 5000,
|
||
description: 'Мощный взрыв огня вокруг'
|
||
},
|
||
// Ледяная магия
|
||
frostbolt: {
|
||
name: 'Ледяная стрела',
|
||
type: 'ice',
|
||
school: 'elemental',
|
||
mpCost: 12,
|
||
damage: 20,
|
||
damageType: 'ice',
|
||
range: 6,
|
||
cooldown: 2000,
|
||
description: 'Наносит урон льдом и замедляет'
|
||
},
|
||
blizzard: {
|
||
name: 'Метель',
|
||
type: 'ice',
|
||
school: 'elemental',
|
||
mpCost: 35,
|
||
damage: 50,
|
||
damageType: 'ice',
|
||
range: 4,
|
||
cooldown: 8000,
|
||
description: 'Атакует всех врагов вокруг'
|
||
},
|
||
// Молния
|
||
lightning: {
|
||
name: 'Молния',
|
||
type: 'lightning',
|
||
school: 'elemental',
|
||
mpCost: 20,
|
||
damage: 35,
|
||
damageType: 'lightning',
|
||
range: 6,
|
||
cooldown: 3000,
|
||
description: 'Быстрая атака молнией'
|
||
},
|
||
// Лечение
|
||
heal: {
|
||
name: 'Исцеление',
|
||
type: 'healing',
|
||
school: 'mystic',
|
||
mpCost: 10,
|
||
heal: 30,
|
||
range: 4,
|
||
cooldown: 4000,
|
||
description: 'Восстанавливает здоровье'
|
||
},
|
||
greater_heal: {
|
||
name: 'Greater Heal',
|
||
type: 'healing',
|
||
school: 'mystic',
|
||
mpCost: 25,
|
||
heal: 70,
|
||
range: 4,
|
||
cooldown: 6000,
|
||
description: 'Мощное исцеление'
|
||
},
|
||
// Баффы
|
||
power: {
|
||
name: 'Усиление',
|
||
type: 'buff',
|
||
school: 'mystic',
|
||
mpCost: 15,
|
||
buff: { stat: 'damage', value: 1.5, duration: 10000 },
|
||
cooldown: 15000,
|
||
description: 'Увеличивает урон на 50%'
|
||
},
|
||
shield: {
|
||
name: 'Щит',
|
||
type: 'buff',
|
||
school: 'mystic',
|
||
mpCost: 20,
|
||
buff: { stat: 'defense', value: 2, duration: 15000 },
|
||
cooldown: 20000,
|
||
description: 'Удваивает защиту'
|
||
},
|
||
// Тёмная магия
|
||
life_drain: {
|
||
name: 'Похищение жизни',
|
||
type: 'dark',
|
||
school: 'dark',
|
||
mpCost: 18,
|
||
damage: 15,
|
||
heal: 10,
|
||
range: 4,
|
||
cooldown: 5000,
|
||
description: 'Наносит урон и восстанавливает HP'
|
||
},
|
||
curse: {
|
||
name: 'Проклятие',
|
||
type: 'debuff',
|
||
school: 'dark',
|
||
mpCost: 15,
|
||
debuff: { stat: 'damage', value: 0.7, duration: 8000 },
|
||
range: 5,
|
||
cooldown: 10000,
|
||
description: 'Снижает урон врага на 30%'
|
||
},
|
||
// Святая магия
|
||
holy_fire: {
|
||
name: 'Священный огонь',
|
||
type: 'holy',
|
||
school: 'holy',
|
||
mpCost: 22,
|
||
damage: 30,
|
||
damageType: 'holy',
|
||
range: 5,
|
||
cooldown: 4000,
|
||
description: 'Атакует нечисть'
|
||
},
|
||
resurrect: {
|
||
name: 'Воскрешение',
|
||
type: 'holy',
|
||
school: 'holy',
|
||
mpCost: 50,
|
||
resurrect: true,
|
||
cooldown: 60000,
|
||
description: 'Воскрешает союзника в бою'
|
||
}
|
||
},
|
||
|
||
// Изучить заклинание
|
||
learnSpell(character, spellId) {
|
||
const spell = this.SpellDatabase[spellId];
|
||
if (!spell) return false;
|
||
if (character.learnedSpells.includes(spellId)) return false;
|
||
|
||
// Проверка требований (уровень, класс)
|
||
if (spell.requiredLevel && character.level < spell.requiredLevel) return false;
|
||
if (spell.requiredClass && character.class !== spell.requiredClass) return false;
|
||
|
||
character.learnedSpells.push(spellId);
|
||
return true;
|
||
},
|
||
|
||
// Использовать заклинание
|
||
castSpell(character, spellId, target = null) {
|
||
const spell = this.SpellDatabase[spellId];
|
||
if (!spell) return { success: false, message: 'Заклинание не найдено!' };
|
||
if (!character.learnedSpells.includes(spellId)) return { success: false, message: 'Вы не изучили это заклинание!' };
|
||
|
||
// Проверка MP
|
||
if (character.mp < spell.mpCost) return { success: false, message: 'Недостаточно маны!' };
|
||
|
||
// Проверка кулдауна
|
||
const lastCast = character.combatStats.lastSpellCast || {};
|
||
const now = Date.now();
|
||
if (lastCast[spellId] && now - lastCast[spellId] < spell.cooldown) {
|
||
return { success: false, message: 'Заклинание на перезарядке!' };
|
||
}
|
||
|
||
// Списываем ману
|
||
character.mp -= spell.mpCost;
|
||
|
||
// Записываем кулдаун
|
||
character.combatStats.lastSpellCast = character.combatStats.lastSpellCast || {};
|
||
character.combatStats.lastSpellCast[spellId] = now;
|
||
|
||
return {
|
||
success: true,
|
||
spell: spell,
|
||
target: target,
|
||
message: `Прочитано: ${spell.name}!`
|
||
};
|
||
},
|
||
|
||
// ========================================
|
||
// ИНВЕНТАРЬ И ЭКИПИРОВКА
|
||
// ========================================
|
||
|
||
addItemToInventory(character, item) {
|
||
// Проверка стекируемости
|
||
if (item.stackable) {
|
||
const existing = character.inventory.find(i =>
|
||
i.id === item.id && i.stackable
|
||
);
|
||
if (existing) {
|
||
existing.quantity += item.quantity;
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// Проверка лимита инвентаря
|
||
if (character.inventory.length >= 25) {
|
||
return false;
|
||
}
|
||
|
||
character.inventory.push(item);
|
||
return true;
|
||
},
|
||
|
||
removeItemFromInventory(character, itemId, quantity = 1) {
|
||
const index = character.inventory.findIndex(i => i.id === itemId);
|
||
if (index === -1) return false;
|
||
|
||
const item = character.inventory[index];
|
||
|
||
if (item.stackable) {
|
||
if (item.quantity >= quantity) {
|
||
item.quantity -= quantity;
|
||
if (item.quantity <= 0) {
|
||
character.inventory.splice(index, 1);
|
||
}
|
||
return true;
|
||
}
|
||
} else {
|
||
character.inventory.splice(index, 1);
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
},
|
||
|
||
equipItem(character, item) {
|
||
if (item.type !== this.ItemType.WEAPON && item.type !== this.ItemType.ARMOR) {
|
||
return { success: false, message: 'Этот предмет нельзя экипировать!' };
|
||
}
|
||
|
||
// Проверка требований
|
||
if (item.requiredLevel && character.level < item.requiredLevel) {
|
||
return { success: false, message: `Требуется уровень ${item.requiredLevel}!` };
|
||
}
|
||
if (item.requiredClass && character.class !== item.requiredClass) {
|
||
return { success: false, message: `Только для класса ${item.requiredClass}!` };
|
||
}
|
||
|
||
let slot;
|
||
if (item.type === this.ItemType.WEAPON) {
|
||
slot = this.EquipmentSlot.MAIN_HAND;
|
||
} else if (item.type === this.ItemType.ARMOR) {
|
||
switch(item.armorType) {
|
||
case this.ArmorType.HELMET: slot = this.EquipmentSlot.HEAD; break;
|
||
case this.ArmorType.CHESTPLATE: slot = this.EquipmentSlot.CHEST; break;
|
||
case this.ArmorType.LEGGINGS: slot = this.EquipmentSlot.LEGS; break;
|
||
case this.ArmorType.BOOTS: slot = this.EquipmentSlot.FEET; break;
|
||
case this.ArmorType.SHIELD: slot = this.EquipmentSlot.OFF_HAND; break;
|
||
case this.ArmorType.CLOAK: slot = this.EquipmentSlot.CHEST; break;
|
||
default: slot = this.EquipmentSlot.CHEST;
|
||
}
|
||
}
|
||
|
||
// Снятие текущего предмета
|
||
if (character.equipment[slot]) {
|
||
this.unequipItem(character, slot);
|
||
}
|
||
|
||
// Экипировка
|
||
character.equipment[slot] = item;
|
||
|
||
// Удаляем из инвентаря
|
||
const invIndex = character.inventory.findIndex(i => i.id === item.id);
|
||
if (invIndex >= 0) {
|
||
character.inventory.splice(invIndex, 1);
|
||
}
|
||
|
||
return { success: true, message: `Экипировано: ${item.name}` };
|
||
},
|
||
|
||
unequipItem(character, slot) {
|
||
const item = character.equipment[slot];
|
||
if (!item) return false;
|
||
|
||
this.addItemToInventory(character, item);
|
||
character.equipment[slot] = null;
|
||
return true;
|
||
},
|
||
|
||
// Получение бонусов от экипировки
|
||
getEquipmentBonuses(character) {
|
||
const bonuses = {
|
||
damage: 0,
|
||
defense: 0,
|
||
hp: 0,
|
||
mp: 0,
|
||
str: 0,
|
||
def: 0,
|
||
mag: 0,
|
||
spd: 0
|
||
};
|
||
|
||
Object.values(character.equipment).forEach(item => {
|
||
if (!item) return;
|
||
bonuses.damage += item.damage || 0;
|
||
bonuses.defense += item.defense || 0;
|
||
bonuses.hp += item.bonusHp || 0;
|
||
bonuses.mp += item.bonusMp || 0;
|
||
bonuses.str += item.bonusStr || 0;
|
||
bonuses.def += item.bonusDef || 0;
|
||
bonuses.mag += item.bonusMag || 0;
|
||
bonuses.spd += item.bonusSpd || 0;
|
||
});
|
||
|
||
return bonuses;
|
||
},
|
||
|
||
// Полный расчет статов
|
||
getTotalStats(character) {
|
||
const bonuses = this.getEquipmentBonuses(character);
|
||
|
||
return {
|
||
hp: (character.maxHp || 100) + bonuses.hp,
|
||
mp: (character.maxMp || 50) + bonuses.mp,
|
||
damage: (character.str || 10) + bonuses.damage,
|
||
defense: (character.def || 5) + bonuses.defense,
|
||
magic: (character.mag || 5) + bonuses.mag,
|
||
speed: (character.spd || 5) + bonuses.spd
|
||
};
|
||
},
|
||
|
||
// Использование предмета
|
||
useItem(character, item) {
|
||
if (item.type === this.ItemType.POTION) {
|
||
const oldHp = character.hp;
|
||
const oldMp = character.mp;
|
||
|
||
character.hp = Math.min(character.hp + (item.healAmount || 0), character.maxHp);
|
||
character.mp = Math.min(character.mp + (item.restoreMp || 0), character.maxMp);
|
||
|
||
const healed = character.hp - oldHp;
|
||
const restored = character.mp - oldMp;
|
||
|
||
if (item.stackable) {
|
||
this.removeItemFromInventory(character, item.id, 1);
|
||
} else {
|
||
this.removeItemFromInventory(character, item.id);
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
message: healed > 0 ? `HP: +${healed}` : `MP: +${restored}`,
|
||
used: true
|
||
};
|
||
}
|
||
|
||
if (item.type === this.ItemType.FOOD) {
|
||
character.hp = Math.min(character.hp + (item.healAmount || 20), character.maxHp);
|
||
|
||
if (item.stackable) {
|
||
this.removeItemFromInventory(character, item.id, 1);
|
||
} else {
|
||
this.removeItemFromInventory(character, item.id);
|
||
}
|
||
|
||
return { success: true, message: `Съедено: ${item.name}` };
|
||
}
|
||
|
||
if (item.type === this.ItemType.SCROLL && item.spell) {
|
||
return {
|
||
success: true,
|
||
message: `Вы изучили заклинание: ${item.spell}!`,
|
||
scrollUsed: true,
|
||
spellLearned: item.spell
|
||
};
|
||
}
|
||
|
||
return { success: false, message: 'Этот предмет нельзя использовать!' };
|
||
},
|
||
|
||
// ========================================
|
||
// БОЕВАЯ СИСТЕМА
|
||
// ========================================
|
||
|
||
attack(attacker, defender) {
|
||
const attackerStats = attacker.isPlayer ? this.getTotalStats(attacker) : {
|
||
damage: attacker.damage,
|
||
defense: attacker.defense || 0,
|
||
speed: attacker.level * 2
|
||
};
|
||
|
||
const defenderStats = defender.isPlayer ? this.getTotalStats(defender) : {
|
||
damage: defender.damage || 0,
|
||
defense: defender.defense || 0,
|
||
speed: defender.level * 2
|
||
};
|
||
|
||
// Базовый урон
|
||
let damage = Math.max(1, attackerStats.damage - defenderStats.defense);
|
||
|
||
// Критический удар (10% базовый + бонус от скорости)
|
||
const critChance = 0.1 + (attackerStats.speed || 0) * 0.01;
|
||
const isCrit = Math.random() < critChance;
|
||
if (isCrit) {
|
||
damage *= 1.5 + (Math.random() * 0.5);
|
||
}
|
||
|
||
// Элементальная уязвимость
|
||
if (attacker.damageType && defender.weakness === attacker.damageType) {
|
||
damage *= 1.5;
|
||
}
|
||
|
||
// Случайное отклонение (±15%)
|
||
const variance = 0.85 + Math.random() * 0.3;
|
||
damage = Math.floor(damage * variance);
|
||
|
||
defender.hp = Math.max(0, defender.hp - damage);
|
||
|
||
// Статистика
|
||
if (attacker.isPlayer) {
|
||
attacker.combatStats.damageDealt += damage;
|
||
attacker.combatStats.attacks++;
|
||
}
|
||
if (defender.isPlayer) {
|
||
defender.combatStats.damageTaken += damage;
|
||
}
|
||
|
||
return {
|
||
damage: damage,
|
||
isCrit: isCrit,
|
||
killed: defender.hp <= 0
|
||
};
|
||
},
|
||
|
||
// Атака врага (AI)
|
||
enemyAttack(enemy, player) {
|
||
const stats = RPG.getTotalStats(player);
|
||
let damage = Math.max(1, enemy.damage - stats.defense);
|
||
|
||
const isCrit = Math.random() < 0.1;
|
||
if (isCrit) damage *= 1.5;
|
||
|
||
damage = Math.floor(damage * (0.9 + Math.random() * 0.2));
|
||
|
||
player.hp = Math.max(0, player.hp - damage);
|
||
player.combatStats.damageTaken += damage;
|
||
|
||
return { damage, isCrit, killed: player.hp <= 0 };
|
||
},
|
||
|
||
// ========================================
|
||
// СИСТЕМА УРОВНЕЙ
|
||
// ========================================
|
||
|
||
checkLevelUp(character) {
|
||
let leveledUp = false;
|
||
|
||
while (character.exp >= character.expToNextLevel) {
|
||
character.exp -= character.expToNextLevel;
|
||
character.level++;
|
||
character.expToNextLevel = Math.floor(100 * Math.pow(1.5, character.level - 1));
|
||
|
||
// Бонус к статам по классу
|
||
const classBonuses = {
|
||
warrior: { hp: 15, mp: 3, str: 3, def: 2, mag: 1, spd: 1 },
|
||
mage: { hp: 5, mp: 15, str: 1, def: 1, mag: 3, spd: 1 },
|
||
archer: { hp: 8, mp: 5, str: 2, def: 1, mag: 1, spd: 2 },
|
||
thief: { hp: 7, mp: 4, str: 2, def: 1, mag: 2, spd: 3 },
|
||
paladin: { hp: 12, mp: 8, str: 2, def: 3, mag: 2, spd: 1 },
|
||
necromancer: { hp: 6, mp: 12, str: 1, def: 1, mag: 4, spd: 1 }
|
||
};
|
||
|
||
const bonus = classBonuses[character.class] || classBonuses.warrior;
|
||
character.maxHp += bonus.hp;
|
||
character.maxMp += bonus.mp;
|
||
character.baseStr += bonus.str;
|
||
character.baseDef += bonus.def;
|
||
character.baseMag += bonus.mag;
|
||
character.baseSpd += bonus.spd;
|
||
|
||
// Пересчитываем текущие статы
|
||
character.str = character.baseStr;
|
||
character.def = character.baseDef;
|
||
character.mag = character.baseMag;
|
||
character.spd = character.baseSpd;
|
||
|
||
// Восстановление при левелапе
|
||
character.hp = character.maxHp;
|
||
character.mp = character.maxMp;
|
||
|
||
leveledUp = true;
|
||
}
|
||
|
||
return leveledUp;
|
||
},
|
||
|
||
// ========================================
|
||
// ГЕНЕРАЦИЯ ЛУТА
|
||
// ========================================
|
||
|
||
generateLoot(enemy) {
|
||
const loot = [];
|
||
|
||
// Всегда даёт золото
|
||
loot.push(this.createItem(
|
||
'gold',
|
||
this.ItemType.GOLD,
|
||
'Золото',
|
||
{ value: enemy.gold, stackable: true, quantity: enemy.gold, rarity: 'common' }
|
||
));
|
||
|
||
// Шанс выпадения из таблицы
|
||
if (enemy.lootTable) {
|
||
enemy.lootTable.forEach(lootId => {
|
||
if (Math.random() < 0.2) { // 20% шанс на каждый предмет
|
||
loot.push(this.createLootItem(lootId, enemy.level));
|
||
}
|
||
});
|
||
}
|
||
|
||
// Шанс случайного предмета (30%)
|
||
if (Math.random() < 0.3) {
|
||
const randomItems = [
|
||
{ type: 'potion', healAmount: 30 },
|
||
{ type: 'potion', healAmount: 50 },
|
||
{ type: 'potion', restoreMp: 20 },
|
||
{ type: 'food', healAmount: 15 },
|
||
{ type: 'gold', value: Math.floor(enemy.gold * 0.5) }
|
||
];
|
||
const template = randomItems[Math.floor(Math.random() * randomItems.length)];
|
||
loot.push(this.createItem(
|
||
'loot_' + Date.now(),
|
||
template.type,
|
||
template.type === 'gold' ? 'Золото' : 'Зелье',
|
||
template
|
||
));
|
||
}
|
||
|
||
return loot;
|
||
},
|
||
|
||
createLootItem(lootId, level) {
|
||
const lootDatabase = {
|
||
// Материалы
|
||
goblin_ear: { name: 'Ухо гоблина', value: 5, type: 'material' },
|
||
orc_tusk: { name: 'Клык орка', value: 10, type: 'material' },
|
||
bone: { name: 'Кость', value: 3, type: 'material' },
|
||
herb: { name: 'Трава', value: 8, type: 'material' },
|
||
slime_gel: { name: 'Слизь', value: 5, type: 'material' },
|
||
troll_heart: { name: 'Сердце тролля', value: 50, type: 'material', rarity: 'rare' },
|
||
dragon_scale: { name: 'Чешуя дракона', value: 100, type: 'material', rarity: 'epic' },
|
||
demon_heart: { name: 'Сердце демона', value: 80, type: 'material', rarity: 'epic' },
|
||
// Оружие
|
||
bone_dagger: { name: 'Костяной кинжал', value: 25, type: 'weapon', damage: 8, rarity: 'uncommon' },
|
||
club: { name: 'Дубина', value: 15, type: 'weapon', damage: 6 },
|
||
skull_staff: { name: 'Посох черепа', value: 80, type: 'weapon', damage: 15, rarity: 'rare' },
|
||
// Щиты
|
||
shield_wood: { name: 'Деревянный щит', value: 20, type: 'armor', defense: 3 },
|
||
// Зелья
|
||
health_potion: { name: 'Зелье HP', value: 25, type: 'potion', healAmount: 40, rarity: 'uncommon' },
|
||
mana_potion: { name: 'Зелье MP', value: 30, type: 'potion', restoreMp: 30, rarity: 'uncommon' },
|
||
// Еда
|
||
meat: { name: 'Мясо', value: 10, type: 'food', healAmount: 20 },
|
||
// Книги
|
||
scroll: { name: 'Свиток', value: 50, type: 'scroll', spell: 'fireball' },
|
||
necronomicon: { name: 'Некрономикон', value: 200, type: 'scroll', spell: 'life_drain', rarity: 'rare' },
|
||
// Разное
|
||
money_pouch: { name: 'Кошелёк', value: 15, type: 'gold' },
|
||
dragon_heart: { name: 'Сердце дракона', value: 300, type: 'material', rarity: 'legendary' },
|
||
dragon_egg: { name: 'Яйцо дракона', value: 500, type: 'quest', rarity: 'legendary' },
|
||
infernal_orb: { name: 'Инфернальная сфера', value: 150, type: 'material', rarity: 'epic' }
|
||
};
|
||
|
||
const template = lootDatabase[lootId] || { name: 'Предмет', value: 10, type: 'material' };
|
||
return this.createItem(
|
||
lootId + '_' + Date.now(),
|
||
template.type,
|
||
template.name,
|
||
{
|
||
value: template.value,
|
||
damage: template.damage,
|
||
defense: template.defense,
|
||
healAmount: template.healAmount,
|
||
restoreMp: template.restoreMp,
|
||
spell: template.spell,
|
||
rarity: template.rarity || 'common'
|
||
}
|
||
);
|
||
},
|
||
|
||
// ========================================
|
||
// ПРОВЕРКА ПРОХОДИМОСТИ
|
||
// ========================================
|
||
|
||
isTilePassable(map, x, y) {
|
||
if (x < 0 || y < 0 || x >= map[0].length || y >= map.length) {
|
||
return false;
|
||
}
|
||
|
||
const tile = map[y][x];
|
||
return tile !== 1 && tile !== 4 && tile !== 6; // вода, стена, лава
|
||
},
|
||
|
||
getAdjacentTiles(x, y, map) {
|
||
const directions = [
|
||
{ dx: 0, dy: -1 },
|
||
{ dx: 1, dy: 0 },
|
||
{ dx: 0, dy: 1 },
|
||
{ dx: -1, dy: 0 }
|
||
];
|
||
|
||
return directions
|
||
.map(d => ({ x: x + d.dx, y: y + d.dy }))
|
||
.filter(pos => this.isTilePassable(map, pos.x, pos.y));
|
||
},
|
||
|
||
// ========================================
|
||
// КОЛЛИЗИИ
|
||
// ========================================
|
||
|
||
checkEnemyCollision(character, enemies) {
|
||
const charX = Math.round(character.x);
|
||
const charY = Math.round(character.y);
|
||
|
||
for (let enemy of enemies) {
|
||
const enemyX = Math.round(enemy.x);
|
||
const enemyY = Math.round(enemy.y);
|
||
|
||
if (charX === enemyX && charY === enemyY) {
|
||
return enemy;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
},
|
||
|
||
checkItemCollision(character, items) {
|
||
const charX = Math.round(character.x);
|
||
const charY = Math.round(character.y);
|
||
|
||
for (let i = 0; i < items.length; i++) {
|
||
const item = items[i];
|
||
if (!item.collected && item.x === charX && item.y === charY) {
|
||
return i;
|
||
}
|
||
}
|
||
|
||
return -1;
|
||
},
|
||
|
||
checkNPCCollision(character, npcs) {
|
||
const charX = Math.round(character.x);
|
||
const charY = Math.round(character.y);
|
||
|
||
for (let npc of npcs) {
|
||
if (npc.x === charX && npc.y === charY) {
|
||
return npc;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
},
|
||
|
||
// ========================================
|
||
// СОХРАНЕНИЕ И ЗАГРУЗКА
|
||
// ========================================
|
||
|
||
saveGame(character, filename = 'rpg_save') {
|
||
const saveData = {
|
||
version: '1.0',
|
||
timestamp: Date.now(),
|
||
character: character
|
||
};
|
||
|
||
try {
|
||
localStorage.setItem(filename, JSON.stringify(saveData));
|
||
return { success: true, message: 'Игра сохранена!' };
|
||
} catch (e) {
|
||
return { success: false, message: 'Ошибка сохранения!' };
|
||
}
|
||
},
|
||
|
||
loadGame(filename = 'rpg_save') {
|
||
try {
|
||
const saveData = localStorage.getItem(filename);
|
||
if (!saveData) {
|
||
return { success: false, message: 'Нет сохранённой игры!' };
|
||
}
|
||
|
||
const data = JSON.parse(saveData);
|
||
return { success: true, character: data.character, message: 'Игра загружена!' };
|
||
} catch (e) {
|
||
return { success: false, message: 'Ошибка загрузки!' };
|
||
}
|
||
},
|
||
|
||
deleteSave(filename = 'rpg_save') {
|
||
try {
|
||
localStorage.removeItem(filename);
|
||
return { success: true, message: 'Сохранение удалено!' };
|
||
} catch (e) {
|
||
return { success: false, message: 'Ошибка удаления!' };
|
||
}
|
||
},
|
||
|
||
hasSave(filename = 'rpg_save') {
|
||
return localStorage.getItem(filename) !== null;
|
||
},
|
||
|
||
// ========================================
|
||
// БАЗЫ ДАННЫХ
|
||
// ========================================
|
||
|
||
ItemDatabase: {
|
||
weapons: [
|
||
{ id: 'sword_1', name: 'Железный меч', damage: 5, value: 50 },
|
||
{ id: 'sword_2', name: 'Стальной меч', damage: 10, value: 150 },
|
||
{ id: 'sword_3', name: 'Меч рыцаря', damage: 18, value: 400 },
|
||
{ id: 'sword_legend', name: 'Экскалибур', damage: 35, value: 2000, rarity: 'legendary' },
|
||
{ id: 'axe_1', name: 'Боевой топор', damage: 8, value: 80 },
|
||
{ id: 'axe_2', name: 'Секира варвара', damage: 15, value: 300 },
|
||
{ id: 'staff_1', name: 'Посох ученика', damage: 3, magic: 8, value: 60 },
|
||
{ id: 'staff_2', name: 'Посох мага', damage: 5, magic: 15, value: 200 },
|
||
{ id: 'dagger_1', name: 'Кинжал', damage: 4, value: 30 },
|
||
{ id: 'bow_1', name: 'Лук', damage: 7, value: 70 }
|
||
],
|
||
|
||
armor: [
|
||
{ id: 'helmet_1', name: 'Кожаный шлем', defense: 2, value: 40 },
|
||
{ id: 'helmet_2', name: 'Железный шлем', defense: 5, value: 120 },
|
||
{ id: 'chest_1', name: 'Кожаная куртка', defense: 3, value: 50 },
|
||
{ id: 'chest_2', name: 'Кольчуга', defense: 8, value: 200 },
|
||
{ id: 'chest_3', name: 'Латы рыцаря', defense: 15, value: 500 },
|
||
{ id: 'chest_epic', name: 'Доспех дракона', defense: 25, value: 1500, rarity: 'epic' },
|
||
{ id: 'legs_1', name: 'Кожаные поножи', defense: 2, value: 35 },
|
||
{ id: 'boots_1', name: 'Кожаные сапоги', defense: 1, value: 25 },
|
||
{ id: 'boots_2', name: 'Железные сапоги', defense: 3, value: 80 },
|
||
{ id: 'shield_1', name: 'Деревянный щит', defense: 3, value: 40 },
|
||
{ id: 'shield_2', name: 'Железный щит', defense: 6, value: 150 },
|
||
{ id: 'shield_epic', name: 'Щит небожителя', defense: 12, value: 800, rarity: 'epic' }
|
||
],
|
||
|
||
potions: [
|
||
{ id: 'health_potion_1', name: 'Малое зелье HP', healAmount: 30, value: 20 },
|
||
{ id: 'health_potion_2', name: 'Среднее зелье HP', healAmount: 60, value: 50 },
|
||
{ id: 'health_potion_3', name: 'Большое зелье HP', healAmount: 100, value: 100 },
|
||
{ id: 'mana_potion_1', name: 'Малое зелье MP', restoreMp: 20, value: 25 },
|
||
{ id: 'mana_potion_2', name: 'Среднее зелье MP', restoreMp: 40, value: 60 },
|
||
{ id: 'mana_potion_3', name: 'Большое зелье MP', restoreMp: 70, value: 120 },
|
||
{ id: 'elixir_1', name: 'Эликсир жизни', healAmount: 200, value: 300, rarity: 'rare' }
|
||
]
|
||
}
|
||
};
|
||
|
||
// Экспорт
|
||
if (typeof module !== 'undefined' && module.exports) {
|
||
module.exports = RPG;
|
||
}
|