184 lines
11 KiB
JavaScript
184 lines
11 KiB
JavaScript
// Campaign Worlds & 25 Levels Definition System
|
||
window.CampaignSystem = (function() {
|
||
const WORLDS = [
|
||
{ id: 1, name: 'Солнечный День', icon: '☀️', biome: 'day', range: '1-1 .. 1-5' },
|
||
{ id: 2, name: 'Ночной Двор', icon: '🌙', biome: 'night', range: '2-1 .. 2-5' },
|
||
{ id: 3, name: 'Бассейн', icon: '🏊', biome: 'pool', range: '3-1 .. 3-5' },
|
||
{ id: 4, name: 'Ночной Туман', icon: '🌫️', biome: 'fog', range: '4-1 .. 4-5' },
|
||
{ id: 5, name: 'Крыша Дома', icon: '🏠', biome: 'roof', range: '5-1 .. 5-5' }
|
||
];
|
||
|
||
const DAVE_DIALOGUES = {
|
||
'2-1': {
|
||
title: '🌙 Безумный Дэйв предупреждает:',
|
||
text: 'Эй, сосед! Ночью солнце с неба не падает! Обязательно возьми Солнцегриб!',
|
||
btnText: 'ДАЛЕЕ / В БОЙ ⚔️'
|
||
},
|
||
'3-1': {
|
||
title: '🏊 Безумный Дэйв спешит на помощь:',
|
||
text: 'Там бассейн! На воде растения тонут — сначала сажай Кувшинки (Lily Pad)!',
|
||
btnText: 'ДАЛЕЕ / В БОЙ ⚔️'
|
||
},
|
||
'4-1': {
|
||
title: '🌫️ Безумный Дэйв щурится во мгле:',
|
||
text: 'Ого, какой густой туман! Зомби крадутся в темноте, используй Плантерн и Вздуватель!',
|
||
btnText: 'ДАЛЕЕ / В БОЙ ⚔️'
|
||
},
|
||
'5-1': {
|
||
title: '🏠 Безумный Дэйв лезет на скат:',
|
||
text: 'Мы на крыше! Скаты мешают прямой стрельбе — используй Горшки и Катапульты!',
|
||
btnText: 'ДАЛЕЕ / В БОЙ ⚔️'
|
||
},
|
||
'5-5': {
|
||
title: '🤖 БЕЗУМИЕ ПОЛНОГО МАСШТАБА!',
|
||
text: 'БЕЗУМИЕ! Доктор Зомбосс выкатил своего гигантского робота! Держи оборону!',
|
||
btnText: 'ДАЛЕЕ / В БОЙ ⚔️'
|
||
}
|
||
};
|
||
|
||
const LEVELS = [
|
||
// World 1: Day (1-1 .. 1-5)
|
||
{ level: 1, world: 1, stage: 1, code: '1-1', title: 'Первые шаги', desc: 'Знакомство с садом и простыми зомби', waves: 3, biome: 'day', reward: 100 },
|
||
{ level: 2, world: 1, stage: 2, code: '1-2', title: 'Опасные конусы', desc: 'Появление стойких зомби с дорожными конусами', waves: 3, biome: 'day', reward: 120 },
|
||
{ level: 3, world: 1, stage: 3, code: '1-3', title: 'Картофельный рубеж', desc: 'Тактическая оборона с картофельными минами', waves: 4, biome: 'day', reward: 140 },
|
||
{ level: 4, world: 1, stage: 4, code: '1-4', title: 'Прыгуны с шестом', desc: 'Быстрые зомби легко перепрыгивают первые растения', waves: 4, biome: 'day', reward: 160 },
|
||
{ level: 5, world: 1, stage: 5, code: '1-5', title: 'Штурм Дня', desc: 'Финальная массивная волна зомби на солнечном газоне', waves: 4, biome: 'day', reward: 200 },
|
||
|
||
// World 2: Night (2-1 .. 2-5)
|
||
{ level: 6, world: 2, stage: 1, code: '2-1', title: 'Сгущаются сумерки', desc: 'Ночь без падающего солнца: солнцегрибы и надгробия', waves: 3, biome: 'night', reward: 150 },
|
||
{ level: 7, world: 2, stage: 2, code: '2-2', title: 'Ярость газетчика', desc: 'Разъярённые зомби с газетами ускоряются в бешенстве', waves: 4, biome: 'night', reward: 170 },
|
||
{ level: 8, world: 2, stage: 3, code: '2-3', title: 'Споровый шквал', desc: 'Дымогрибы пробивают плотные ряды врагов', waves: 4, biome: 'night', reward: 190 },
|
||
{ level: 9, world: 2, stage: 4, code: '2-4', title: 'Бронекаски', desc: 'Зомби с вёдрами штурмуют тёмный двор', waves: 4, biome: 'night', reward: 210 },
|
||
{ level: 10, world: 2, stage: 5, code: '2-5', title: 'Полночный кошмар', desc: 'Зомби восстают прямо из могил в финальной волне', waves: 5, biome: 'night', reward: 250 },
|
||
|
||
// World 3: Pool (3-1 .. 3-5)
|
||
{ level: 11, world: 3, stage: 1, code: '3-1', title: 'Водная гладь', desc: '6 дорожек газона, кувшинки и зомби на утках', waves: 3, biome: 'pool', reward: 200 },
|
||
{ level: 12, world: 3, stage: 2, code: '3-2', title: 'Заплыв с шестом', desc: 'Прыгуны на суше и быстрые пловцы на воде', waves: 4, biome: 'pool', reward: 220 },
|
||
{ level: 13, world: 3, stage: 3, code: '3-3', title: 'Зубастики у воды', desc: 'Хищные зубастики против бронированных уток', waves: 4, biome: 'pool', reward: 240 },
|
||
{ level: 14, world: 3, stage: 4, code: '3-4', title: 'Футбольный матч', desc: 'Мощные футболисты прорывают береговую линию', waves: 4, biome: 'pool', reward: 260 },
|
||
{ level: 15, world: 3, stage: 5, code: '3-5', title: 'Водный шквал', desc: 'Штурм всех 6 дорожек с очистителями бассейна', waves: 5, biome: 'pool', reward: 300 },
|
||
|
||
// World 4: Fog (4-1 .. 4-5)
|
||
{ level: 16, world: 4, stage: 1, code: '4-1', title: 'Плотная мгла', desc: 'Густой туман скрывает правую половину поля', waves: 4, biome: 'fog', reward: 250 },
|
||
{ level: 17, world: 4, stage: 2, code: '4-2', title: 'Невидимая угроза', desc: 'Внезапные появления врагов прямо из белой пелены', waves: 4, biome: 'fog', reward: 270 },
|
||
{ level: 18, world: 4, stage: 3, code: '4-3', title: 'Туманный бассейн', desc: 'Водные линии скрыты во мраке ночного тумана', waves: 4, biome: 'fog', reward: 290 },
|
||
{ level: 19, world: 4, stage: 4, code: '4-4', title: 'Спринт сквозь мглу', desc: 'Футболисты и прыгуны выскакивают из тумана', waves: 5, biome: 'fog', reward: 310 },
|
||
{ level: 20, world: 4, stage: 5, code: '4-5', title: 'Штормовой туман', desc: 'Финальный прорыв орды через непроглядную пелену', waves: 5, biome: 'fog', reward: 350 },
|
||
|
||
// World 5: Roof (5-1 .. 5-5)
|
||
{ level: 21, world: 5, stage: 1, code: '5-1', title: 'Черепичный скат', desc: 'Крыша дома, цветочные горшки и капустопульты', waves: 4, biome: 'roof', reward: 300 },
|
||
{ level: 22, world: 5, stage: 2, code: '5-2', title: 'Арбузный гром', desc: 'Тяжёлые арбузопульты со сплэш-уроном по площади', waves: 4, biome: 'roof', reward: 320 },
|
||
{ level: 23, world: 5, stage: 3, code: '5-3', title: 'Опасные верхолазы', desc: 'Бронезомби карабкаются по скользкому скату крыши', waves: 4, biome: 'roof', reward: 350 },
|
||
{ level: 24, world: 5, stage: 4, code: '5-4', title: 'Осада дымохода', desc: 'Генеральный штурм перед чердаком дома', waves: 5, biome: 'roof', reward: 400 },
|
||
{ level: 25, world: 5, stage: 5, code: '5-5', title: 'Доктор Зомбосс!', desc: 'Финальная битва против шагающего робота Zombot!', waves: 1, biome: 'roof', isBoss: true, reward: 1000 }
|
||
];
|
||
|
||
let _activeLevels = [...LEVELS];
|
||
let _serverLoaded = false;
|
||
|
||
async function loadLevelsFromServer() {
|
||
try {
|
||
const resp = await fetch('/api/campaign/levels');
|
||
if (!resp.ok) return _activeLevels;
|
||
const data = await resp.json();
|
||
if (data && data.success && Array.isArray(data.levels) && data.levels.length > 0) {
|
||
_activeLevels = data.levels.map(lvl => {
|
||
const num = parseInt(lvl.level, 10) || 1;
|
||
const defaultLvl = LEVELS.find(d => d.level === num) || {};
|
||
|
||
let wavesCount = 4;
|
||
if (lvl.waves && typeof lvl.waves === 'object' && Array.isArray(lvl.waves.list)) {
|
||
wavesCount = lvl.waves.list.length;
|
||
} else if (Array.isArray(lvl.waves)) {
|
||
wavesCount = lvl.waves.length;
|
||
} else if (typeof lvl.waves === 'number') {
|
||
wavesCount = lvl.waves;
|
||
} else if (defaultLvl.waves) {
|
||
wavesCount = defaultLvl.waves;
|
||
}
|
||
|
||
const modifiers = (lvl.modifiers && typeof lvl.modifiers === 'object') ? lvl.modifiers : {};
|
||
const wavesConfig = lvl.wavesConfig || lvl.waves || defaultLvl.wavesConfig || null;
|
||
|
||
return {
|
||
...defaultLvl,
|
||
...lvl,
|
||
waves: wavesCount,
|
||
modifiers: {
|
||
speedMultiplier: modifiers.speedMultiplier || lvl.speedMultiplier || 1.0,
|
||
hpMultiplier: modifiers.hpMultiplier || lvl.hpMultiplier || 1.0,
|
||
startingSun: modifiers.startingSun !== undefined ? modifiers.startingSun : (lvl.startingSun !== undefined ? lvl.startingSun : null)
|
||
},
|
||
wavesConfig,
|
||
isBoss: Boolean(lvl.isBoss || lvl.is_boss || num === 25 || defaultLvl.isBoss)
|
||
};
|
||
});
|
||
_serverLoaded = true;
|
||
return _activeLevels;
|
||
}
|
||
} catch (e) {
|
||
console.warn('Campaign server load error, using offline standard levels:', e);
|
||
}
|
||
return _activeLevels;
|
||
}
|
||
|
||
// Load levels on initialization
|
||
if (typeof window !== 'undefined') {
|
||
setTimeout(() => {
|
||
loadLevelsFromServer();
|
||
}, 50);
|
||
}
|
||
|
||
return {
|
||
WORLDS,
|
||
LEVELS,
|
||
DAVE_DIALOGUES,
|
||
loadLevelsFromServer,
|
||
isServerLoaded() {
|
||
return _serverLoaded;
|
||
},
|
||
getAllLevels() {
|
||
return _activeLevels;
|
||
},
|
||
getDaveDialogue(levelNum, mode = 'adventure') {
|
||
if (mode === 'boss' || mode === 'bossrush') {
|
||
return DAVE_DIALOGUES['5-5'];
|
||
}
|
||
const num = parseInt(levelNum, 10) || 1;
|
||
const levelObj = this.getLevel(num);
|
||
if (levelObj) {
|
||
if (levelObj.dialogue && typeof levelObj.dialogue === 'object' && levelObj.dialogue.text) {
|
||
return levelObj.dialogue;
|
||
}
|
||
if (levelObj.daveDialogue && typeof levelObj.daveDialogue === 'object' && levelObj.daveDialogue.text) {
|
||
return levelObj.daveDialogue;
|
||
}
|
||
if (DAVE_DIALOGUES[levelObj.code]) {
|
||
return DAVE_DIALOGUES[levelObj.code];
|
||
}
|
||
}
|
||
return null;
|
||
},
|
||
getLevel(levelNum) {
|
||
const num = parseInt(levelNum, 10) || 1;
|
||
return _activeLevels.find(l => l.level === num) || LEVELS.find(l => l.level === num) || LEVELS[0];
|
||
},
|
||
getWorld(worldId) {
|
||
return WORLDS.find(w => w.id === worldId) || WORLDS[0];
|
||
},
|
||
getLevelsByWorld(worldId) {
|
||
return _activeLevels.filter(l => l.world === worldId);
|
||
},
|
||
formatLevel(levelNum) {
|
||
const item = this.getLevel(levelNum);
|
||
const world = this.getWorld(item.world);
|
||
return {
|
||
...item,
|
||
worldName: world.name,
|
||
worldIcon: world.icon,
|
||
fullLabel: `${item.code} (${world.icon} ${world.name})`
|
||
};
|
||
}
|
||
};
|
||
})();
|