Криза
Завантаження…
Наслідки рішення
Інтерактивна гра · ~10 хвилин
15 днів. 15 криз. Тільки ваші рішення між скандалом і легендою. Чи витримаєте ви тиск киян, журналістів і бюджету?
Усі сценарії — на основі реальних подій 2023–2025 років.
/*
script.js — Гра «Мер на тиждень»
Vanilla JS, без імпортів. Усе огорнуто IIFE через лендинг-систему.
*/
(function () {
'use strict';
const root = document.getElementById('mayorGameRoot');
if (!root) return;
const STORAGE_KEY = 'mayorGameWP_v2';
// ───────────────────────────────────────────────────────────
// SCENARIOS
// ───────────────────────────────────────────────────────────
const scenarios = [
{
title: 'Скандал із Протасовим Яром',
tag: 'Забудова',
text: 'Забудовник заводить техніку попри судову заборону. Активісти мітингують, соцмережі вибухають.',
choices: [
{ emoji: String.fromCodePoint(0x1F6D1), text: 'Зупинити забудову та провести громадські слухання', effects: { trust: 10, media: 5, budget: -5 }, result: 'Ви отримали похвалу від мешканців, хоча забудовник подає до суду.' },
{ emoji: '⚖️', text: 'Зіграти нейтралітет, не втручаючись напряму', effects: { trust: -10, media: -5 }, result: 'Активісти звинувачують вас у бездіяльності.' },
{ emoji: String.fromCodePoint(0x1F4F0), text: 'Випустити пресреліз про повагу до права', effects: { media: 5, trust: -5 }, result: 'Медіа визнали дипломатичність, але люди чекають дій.' }
]
},
{
title: 'Фейк про отруєну воду',
tag: 'Інформаційна війна',
text: 'У TikTok шириться відео, що вода в Києві нібито отруєна. Люди панікують, офіційні джерела мовчать.',
choices: [
{ emoji: String.fromCodePoint(0x1F399, 0xFE0F), text: 'Екстрений брифінг із СЕС та доказами безпеки', effects: { trust: 15, media: 10 }, result: 'Паніка спадає, вас хвалять за відкритість.' },
{ emoji: String.fromCodePoint(0x1F604), text: 'Запустити контрвідео з мемами й фактами', effects: { media: 15 }, result: 'Мем-атака працює! Люди сміються й заспокоюються.' },
{ emoji: String.fromCodePoint(0x1F910), text: 'Не реагувати, хай експерти розберуться самі', effects: { trust: -15 }, result: 'Містяни розчаровані вашою мовчанкою.' }
]
},
{
title: 'Потоп у центрі Києва',
tag: 'Інфраструктура',
text: 'Злива перетворює центр на річку. Машини плавають, метро підтоплено, соцмережі заливає відео.',
choices: [
{ emoji: String.fromCodePoint(0x1F527), text: 'Терміново почати ремонт каналізації', effects: { trust: 10, budget: -15, media: 8 }, result: 'Ремонт розпочато, містяни задоволені оперативністю.' },
{ emoji: String.fromCodePoint(0x1F4B0), text: 'Запропонувати компенсації постраждалим', effects: { trust: 5, budget: -10 }, result: 'Компенсації знизили напругу, але проблему не вирішили.' },
{ emoji: String.fromCodePoint(0x1F30D), text: 'Звинувачувати кліматичні зміни', effects: { trust: -5, media: -7 }, result: 'Містяни вважають це відмовкою, критикують владу.' }
]
},
{
title: 'Земельний дерибан',
tag: 'Корупція',
text: 'Журналісти публікують розслідування багаторічних схем з виділенням землі. Прізвища не називають, але суспільство кипить.',
choices: [
{ emoji: String.fromCodePoint(0x1F575, 0xFE0F), text: 'Почати розслідування і обіцяти прозорість', effects: { trust: 12, media: 10, budget: -5 }, result: 'Громада підтримала чесність влади.' },
{ emoji: String.fromCodePoint(0x1F648), text: 'Ігнорувати звинувачення', effects: { trust: -15, media: -10 }, result: 'Негатив розповсюдився, довіра впала.' },
{ emoji: String.fromCodePoint(0x1F4DC), text: 'Запропонувати новий закон про землю', effects: { trust: 8, budget: -10 }, result: 'Ініціатива отримала підтримку, але коштує дорого.' }
]
},
{
title: 'Святкування під час жалоби',
tag: 'Етика',
text: 'У день офіційної жалоби чиновники влаштовують вечірку на комунальній території. Суспільство шоковане.',
choices: [
{ emoji: String.fromCodePoint(0x1F64F), text: 'Публічно вибачитися та звільнити організаторів', effects: { trust: 7, media: 8 }, result: 'Громада оцінила вашу відповідальність.' },
{ emoji: String.fromCodePoint(0x1F937), text: 'Ігнорувати інцидент', effects: { trust: -10, media: -12 }, result: 'Скандал триває, критикують мерію.' },
{ emoji: String.fromCodePoint(0x1F440), text: 'Звинувачувати опозицію у провокації', effects: { trust: -7, media: -5 }, result: 'Довіра падає через політичні ігри.' }
]
},
{
title: 'Нічний демонтаж МАФів',
tag: 'Бізнес',
text: 'МАФи демонтують серед ночі без попередження. Підприємці обурені, мітингують під КМДА.',
choices: [
{ emoji: String.fromCodePoint(0x1F91D), text: 'Провести консультації з підприємцями', effects: { trust: 8, media: 5 }, result: 'Конфлікт вщух, довіра зростає.' },
{ emoji: '⚡', text: 'Продовжити демонтаж без змін', effects: { trust: -10, media: -8 }, result: 'Обурення зростає, скандали у ЗМІ.' },
{ emoji: String.fromCodePoint(0x1F4B8), text: 'Запропонувати компенсації', effects: { trust: 4, budget: -8 }, result: 'Частина підприємців задоволена, але не всі.' }
]
},
{
title: 'Підтоплення синьої гілки метро',
tag: 'Транспорт',
text: 'Вода на платформах, станції закриті, відео масово розходиться в мережі. Хаос і нерозуміння.',
choices: [
{ emoji: String.fromCodePoint(0x1F68C), text: 'Оголосити ремонт і організувати транспорт', effects: { trust: 8, budget: -12, media: 7 }, result: 'Громада сприйняла заходи позитивно.' },
{ emoji: String.fromCodePoint(0x1F910), text: 'Ігнорувати, мовчати', effects: { trust: -12, media: -10 }, result: 'Обурення зростає, соцмережі киплять.' },
{ emoji: String.fromCodePoint(0x1F327, 0xFE0F), text: 'Пояснити, що це форс-мажор', effects: { trust: -5, media: -4 }, result: 'Вибачення не заспокоїли людей.' }
]
},
{
title: "Вирубка дерев у Лисогір'ї",
tag: 'Екологія',
text: 'Приватний забудовник прорубує дорогу через зелений масив. Активісти блокують техніку.',
choices: [
{ emoji: String.fromCodePoint(0x1F333), text: 'Призупинити проєкт і провести аудит', effects: { trust: 10, media: 6 }, result: 'Суспільство підтримало відповідальність влади.' },
{ emoji: String.fromCodePoint(0x1F3D7, 0xFE0F), text: 'Дозволити будівництво', effects: { trust: -10, media: -8 }, result: 'Активісти критикують владу.' },
{ emoji: String.fromCodePoint(0x1F331), text: 'Запропонувати компенсацію зелених насаджень', effects: { trust: 3, budget: -5 }, result: 'Частина громадськості задоволена, але є сумніви.' }
]
},
{
title: "Обвал гойдалки в Солом'янці",
tag: 'Безпека',
text: 'На дитячому майданчику травмується дитина. Шок, сюжети в новинах, батьки вимагають перевірок.',
choices: [
{ emoji: String.fromCodePoint(0x1F504), text: 'Терміново замінити обладнання', effects: { trust: 10, budget: -12 }, result: 'Ваша реакція сприйнята позитивно.' },
{ emoji: String.fromCodePoint(0x1F50D), text: 'Провести розслідування', effects: { trust: 5, media: 5 }, result: 'Громада чекає результатів.' },
{ emoji: String.fromCodePoint(0x1F937), text: 'Ігнорувати інцидент', effects: { trust: -15, media: -10 }, result: 'Критика зростає, довіра падає.' }
]
},
{
title: 'Клуб у комендантську',
tag: 'Громадський порядок',
text: 'Столичний нічний клуб працює під час комендантської години. Люди обурені, мерія виправдовується.',
choices: [
{ emoji: String.fromCodePoint(0x1F6AB), text: 'Закрити клуб і покарати власників', effects: { trust: 8, media: 7 }, result: 'Влада отримала підтримку.' },
{ emoji: String.fromCodePoint(0x1F648), text: 'Ігнорувати ситуацію', effects: { trust: -10, media: -8 }, result: 'Обурення населення росте.' },
{ emoji: String.fromCodePoint(0x1F4CB), text: 'Пояснити, що клуб працює законно', effects: { trust: -5, media: -5 }, result: 'Пояснення не задовольнили людей.' }
]
},
{
title: 'Тарифний скандал',
tag: 'Економіка',
text: 'Люди отримують платіжки з космічними сумами. Мітинги, обурення, запити до КМДА.',
choices: [
{ emoji: String.fromCodePoint(0x1F50D), text: 'Розпочати перевірку тарифів', effects: { trust: 8, budget: -5 }, result: 'Громада сприйняла це позитивно.' },
{ emoji: String.fromCodePoint(0x1F4B0), text: 'Запровадити тимчасове зниження тарифів', effects: { trust: 10, budget: -10 }, result: 'Містяни задоволені.' },
{ emoji: String.fromCodePoint(0x1F910), text: 'Ігнорувати скандал', effects: { trust: -15, media: -10 }, result: 'Обурення і протести зростають.' }
]
},
{
title: 'Інсценування побиття в школі',
tag: 'Освіта',
text: 'У шкільній сценці зображають насильство щодо військового. Батьки шоковані, соцмережі вибухають.',
choices: [
{ emoji: String.fromCodePoint(0x1F64F), text: 'Вибачитись і провести виховні заходи', effects: { trust: 7, media: 5 }, result: 'Громада прийняла вибачення.' },
{ emoji: String.fromCodePoint(0x1F910), text: 'Ігнорувати подію', effects: { trust: -12, media: -8 }, result: 'Негативна реакція суспільства.' },
{ emoji: String.fromCodePoint(0x1F3AD), text: 'Пояснити, що це лише вистава', effects: { trust: -5, media: -4 }, result: 'Пояснення не задовольнили всіх.' }
]
},
{
title: 'Мітинг про гроші для ЗСУ',
tag: 'Прозорість',
text: 'Під КМДА мітингувальники вимагають звітності за витрати на армію. Депутати не виходять.',
choices: [
{ emoji: String.fromCodePoint(0x1F4CA), text: 'Надати повний звіт та прозорість', effects: { trust: 12, media: 10 }, result: 'Громада оцінила чесність.' },
{ emoji: String.fromCodePoint(0x1F910), text: 'Ігнорувати запити', effects: { trust: -15, media: -10 }, result: 'Негативна реакція активістів.' },
{ emoji: String.fromCodePoint(0x1F50D), text: 'Обіцяти розслідування', effects: { trust: 5, media: 5 }, result: 'Часткова підтримка суспільства.' }
]
},
{
title: 'Квест із генератором',
tag: 'Енергетика',
text: 'Частина шкіл залишилась без генераторів після відключень. Батьки обурені, чиновники мовчать.',
choices: [
{ emoji: '⚡', text: 'Негайно забезпечити генераторами', effects: { trust: 10, budget: -15 }, result: 'Батьки задоволені оперативністю.' },
{ emoji: '⏳', text: 'Пообіцяти вирішення проблеми згодом', effects: { trust: -5 }, result: 'Незадоволення зберігається.' },
{ emoji: String.fromCodePoint(0x1F910), text: 'Ігнорувати скарги', effects: { trust: -15, media: -10 }, result: 'Скандал набирає обертів.' }
]
},
{
title: 'Чи підете на вибори?',
tag: 'Політика',
text: 'У фінальний день вас питають, чи будете балотуватися. Ви пояснюєте: вибори можливі лише після перемоги у війні.',
choices: [
{ emoji: String.fromCodePoint(0x1F5F3, 0xFE0F), text: 'Оголосити участь після перемоги', effects: { trust: 5, media: 5 }, result: 'Громада підтримала вашу позицію.' },
{ emoji: '⏳', text: 'Відкласти рішення', effects: { trust: 0 }, result: 'Люди чекають подальших дій.' },
{ emoji: String.fromCodePoint(0x1F910), text: 'Уникати відповіді', effects: { trust: -5, media: -5 }, result: 'Невизначеність викликає сумніви.' }
]
}
];
const achievements = [
{ id: 'diplomat', name: 'Дипломат', desc: 'Вирішили 3+ конфлікти мирно', test: (s) => s.history.filter(function (h) { return h.effects.trust > 5; }).length >= 3 },
{ id: 'economist', name: 'Економіст', desc: 'Зберегли бюджет вище 70', test: (s) => s.budget >= 70 && s.day >= 10 },
{ id: 'people_choice', name: 'Народний улюбленець', desc: 'Довіра вище 80', test: (s) => s.trust >= 80 },
{ id: 'media_master', name: 'Медіа-магнат', desc: 'Підтримка ЗМІ вище 80', test: (s) => s.media >= 80 },
{ id: 'crisis_manager', name: 'Антикризовий менеджер', desc: 'Пережили кризу довіри', test: (s) => s.trust < 40 && s.day >= 5 },
{ id: 'budget_master', name: 'Майстер бюджету', desc: 'Бюджет 100+ млн', test: (s) => s.budget >= 100 },
{ id: 'survivor', name: 'Виживальник', desc: 'Пройшли всі 15 днів', test: (s) => s.day >= scenarios.length }
];
const initialState = function () {
return { day: 0, trust: 60, budget: 80, media: 50, achievements: [], history: [] };
};
let state = initialState();
// ───────────────────────────────────────────────────────────
// DOM HELPERS
// ───────────────────────────────────────────────────────────
const $ = (sel) => root.querySelector(sel);
const $$ = (sel) => root.querySelectorAll(sel);
const screens = {
welcome: $('[data-screen="welcome"]'),
game: $('[data-screen="game"]'),
final: $('[data-screen="final"]')
};
function show(screenName, opts) {
opts = opts || {};
Object.keys(screens).forEach(function (key) {
const el = screens[key];
if (!el) return;
if (key === screenName) {
el.hidden = false;
el.classList.add('is-active');
} else {
el.hidden = true;
el.classList.remove('is-active');
}
});
if (opts.scroll) {
try { root.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (e) { /* noop */ }
}
}
// ───────────────────────────────────────────────────────────
// STORAGE
// ───────────────────────────────────────────────────────────
function saveGame() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
showSaveToast();
} catch (e) { /* ignore quota errors */ }
}
function loadGame() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return false;
const data = JSON.parse(raw);
if (!data || typeof data.day !== 'number') return false;
state = data;
return true;
} catch (e) { return false; }
}
function clearGame() {
try { localStorage.removeItem(STORAGE_KEY); } catch (e) { /* ignore */ }
}
function hasResumableGame() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return false;
const data = JSON.parse(raw);
return data && typeof data.day === 'number' && data.day < scenarios.length && data.day > 0;
} catch (e) { return false; }
}
function showSaveToast() {
const el = $('#mgSave');
if (!el) return;
el.hidden = false;
el.style.animation = 'none';
/* trigger reflow */
void el.offsetWidth;
el.style.animation = '';
setTimeout(function () { el.hidden = true; }, 1600);
}
// ───────────────────────────────────────────────────────────
// RENDER — TIMELINE
// ───────────────────────────────────────────────────────────
function renderTimeline() {
const tl = $('#mgTimeline');
if (!tl) return;
tl.innerHTML = '';
for (let i = 0; i < scenarios.length; i++) {
const cell = document.createElement('span');
cell.className = 'mg-timeline__cell';
if (i < state.day) cell.classList.add('is-done');
else if (i === state.day) cell.classList.add('is-current');
tl.appendChild(cell);
}
}
// ───────────────────────────────────────────────────────────
// RENDER — STATS
// ───────────────────────────────────────────────────────────
function tierForPercent(v) {
if (v >= 70) return 'is-good';
if (v >= 45) return '';
if (v >= 30) return 'is-warn';
return 'is-danger';
}
function tierForBudget(v) {
if (v >= 60) return 'is-good';
if (v >= 20) return '';
if (v >= 0) return 'is-warn';
return 'is-danger';
}
function fillPercent(v) { return Math.max(0, Math.min(100, v)); }
function fillBudget(v) { return Math.max(0, Math.min(100, ((v + 50) / 200) * 100)); }
function renderStats(prev) {
prev = prev || state;
const map = [
{ key: 'trust', selector: '[data-stat="trust"]', tier: tierForPercent(state.trust), fill: fillPercent(state.trust), value: state.trust },
{ key: 'budget', selector: '[data-stat="budget"]', tier: tierForBudget(state.budget), fill: fillBudget(state.budget), value: state.budget },
{ key: 'media', selector: '[data-stat="media"]', tier: tierForPercent(state.media), fill: fillPercent(state.media), value: state.media }
];
map.forEach(function (def) {
const el = $(def.selector);
if (!el) return;
el.classList.remove('is-good', 'is-warn', 'is-danger');
if (def.tier) el.classList.add(def.tier);
const valEl = el.querySelector('[data-val]');
if (valEl) {
const from = parseFloat(valEl.textContent) || 0;
animateNumber(valEl, from, def.value, 600);
}
const fill = el.querySelector('.mg-stat__fill');
if (fill) fill.style.width = def.fill + '%';
});
}
function animateNumber(el, from, to, duration) {
const start = performance.now();
const delta = to - from;
function tick(now) {
const t = Math.min(1, (now - start) / duration);
const eased = 1 - Math.pow(1 - t, 3);
const val = Math.round(from + delta * eased);
el.textContent = val;
if (t < 1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
}
function flashDelta(statKey, value) {
if (!value) return;
const card = $('[data-stat="' + statKey + '"]');
if (!card) return;
const delta = card.querySelector('.mg-stat__delta');
if (!delta) return;
delta.classList.remove('is-pos', 'is-neg', 'is-show');
void delta.offsetWidth;
delta.textContent = (value > 0 ? '+' : '') + value;
delta.classList.add(value > 0 ? 'is-pos' : 'is-neg', 'is-show');
setTimeout(function () { delta.classList.remove('is-show'); }, 1400);
}
// ───────────────────────────────────────────────────────────
// RENDER — SCENARIO
// ───────────────────────────────────────────────────────────
function renderScenario() {
const sc = scenarios[state.day];
if (!sc) return;
$('#mgDayNum').textContent = String(state.day + 1);
const chip = $('#mgScenarioChip');
if (chip) {
const label = chip.querySelector('span:last-child');
if (label) label.textContent = sc.tag || 'Криза';
}
$('#mgScenarioTitle').textContent = sc.title;
$('#mgScenarioText').textContent = sc.text;
const list = $('#mgChoices');
list.innerHTML = '';
sc.choices.forEach(function (choice, idx) {
const li = document.createElement('li');
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'mg-choice';
btn.setAttribute('data-idx', String(idx));
btn.innerHTML =
'<span class="mg-choice__key">' + String.fromCharCode(65 + idx) + '</span>' +
'<span class="mg-choice__text"><span class="mg-choice__text-emoji">' + choice.emoji + '</span>' +
escapeHtml(choice.text) + '</span>';
btn.addEventListener('click', function () { handleChoice(idx); });
li.appendChild(btn);
list.appendChild(li);
});
const result = $('#mgResult');
if (result) result.hidden = true;
const scenarioCard = $('#mgScenario');
if (scenarioCard) {
scenarioCard.style.animation = 'none';
void scenarioCard.offsetWidth;
scenarioCard.style.animation = '';
}
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// ───────────────────────────────────────────────────────────
// GAMEPLAY
// ───────────────────────────────────────────────────────────
function handleChoice(idx) {
const sc = scenarios[state.day];
const choice = sc.choices[idx];
if (!choice) return;
/* disable other buttons */
const buttons = $$('#mgChoices .mg-choice');
buttons.forEach(function (b, i) {
b.disabled = true;
if (i === idx) b.style.borderColor = 'var(--mg-blue)';
});
const prev = { trust: state.trust, budget: state.budget, media: state.media };
const eff = choice.effects || {};
state.trust = clamp(state.trust + (eff.trust || 0), 0, 100);
state.budget = clamp(state.budget + (eff.budget || 0), -50, 999);
state.media = clamp(state.media + (eff.media || 0), 0, 100);
state.history.push({ day: state.day, choice: idx, effects: eff });
renderStats(prev);
flashDelta('trust', eff.trust || 0);
flashDelta('budget', eff.budget || 0);
flashDelta('media', eff.media || 0);
checkAchievements();
/* result card */
const resultText = $('#mgResultText');
const effectsBox = $('#mgResultEffects');
if (resultText) resultText.textContent = choice.result;
if (effectsBox) effectsBox.innerHTML = renderEffects(eff);
const resultCard = $('#mgResult');
if (resultCard) resultCard.hidden = false;
saveGame();
}
function renderEffects(eff) {
const items = [
{ label: 'Довіра', val: eff.trust || 0, suffix: '%' },
{ label: 'Бюджет', val: eff.budget || 0, suffix: ' млн ₴' },
{ label: 'ЗМІ', val: eff.media || 0, suffix: '%' }
];
return items.map(function (it) {
let cls = 'is-zero', arrow = '·';
if (it.val > 0) { cls = 'is-pos'; arrow = '↑'; }
else if (it.val < 0) { cls = 'is-neg'; arrow = '↓'; }
const sign = it.val > 0 ? '+' : '';
return '<span class="mg-effect ' + cls + '">' +
'<span class="mg-effect__arrow">' + arrow + '</span>' +
it.label + ' ' + sign + it.val + it.suffix +
'</span>';
}).join('');
}
function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
function nextDay() {
/* game-over checks */
if (state.trust < 30 || state.budget < -50) {
state.day = scenarios.length;
saveGame();
renderFinal();
show('final', { scroll: true });
return;
}
state.day++;
renderTimeline();
if (state.day >= scenarios.length) {
saveGame();
renderFinal();
show('final', { scroll: true });
return;
}
renderScenario();
saveGame();
}
// ───────────────────────────────────────────────────────────
// ACHIEVEMENTS
// ───────────────────────────────────────────────────────────
function checkAchievements() {
let added = false;
achievements.forEach(function (a) {
if (state.achievements.indexOf(a.id) !== -1) return;
if (a.test(state)) {
state.achievements.push(a.id);
added = true;
}
});
if (added) renderAchievements();
}
function renderAchievements() {
const drawer = $('#mgAchievements');
const list = $('#mgAchievementsList');
if (!drawer || !list) return;
if (!state.achievements.length) { drawer.hidden = true; return; }
drawer.hidden = false;
list.innerHTML = '';
state.achievements.forEach(function (id) {
const a = achievements.find(function (x) { return x.id === id; });
if (!a) return;
const badge = document.createElement('span');
badge.className = 'mg-badge';
badge.textContent = a.name;
badge.title = a.desc;
list.appendChild(badge);
});
}
// ───────────────────────────────────────────────────────────
// FINAL SCREEN
// ───────────────────────────────────────────────────────────
function finalVerdict() {
if (state.budget < -50) {
return { icon: String.fromCodePoint(0x1F4B8), title: 'Банкрутство', message: 'Місто збанкрутіло через ваші рішення. Каденція достроково завершена.' };
}
if (state.trust < 30) {
return { icon: '⚖️', title: 'Народний суд', message: 'Мешканці втратили довіру. Час піти у відставку.' };
}
if (state.trust >= 80) {
return { icon: String.fromCodePoint(0x1F3C6), title: 'Легенда Києва', message: 'Ви стали справжнім героєм для киян. Ваше імʼя увійде в історію.' };
}
if (state.trust >= 65) {
return { icon: String.fromCodePoint(0x1F451), title: 'Герой міста', message: 'Відмінна робота. Мешканці щиро вдячні за вашу службу.' };
}
if (state.trust >= 50) {
return { icon: String.fromCodePoint(0x1F44D), title: 'Гідна каденція', message: "Ви впорались з обов'язками мера на достатньому рівні." };
}
return { icon: String.fromCodePoint(0x1F614), title: 'Складна каденція', message: 'Було важко, але ви дотрималися до кінця. Є над чим працювати.' };
}
function renderFinal() {
const v = finalVerdict();
$('#mgFinalIcon').textContent = v.icon;
$('#mgFinalTitle').textContent = v.title;
$('#mgFinalMessage').textContent = v.message;
const dateEl = $('#mgFinalDate');
if (dateEl) {
const d = new Date();
dateEl.textContent = d.toLocaleDateString('uk-UA', { day: '2-digit', month: 'long', year: 'numeric' });
}
const stats = [
{ label: 'Довіра', num: state.trust + '%', tier: tierForPercent(state.trust) },
{ label: 'Бюджет', num: state.budget + ' млн', tier: tierForBudget(state.budget) },
{ label: 'ЗМІ', num: state.media + '%', tier: tierForPercent(state.media) }
];
const statsBox = $('#mgFinalStats');
if (statsBox) {
statsBox.innerHTML = stats.map(function (s) {
return '<div class="mg-finalstat">' +
'<span class="mg-finalstat__num ' + (s.tier || '') + '">' + s.num + '</span>' +
'<span class="mg-finalstat__label">' + s.label + '</span>' +
'</div>';
}).join('');
}
const achBox = $('#mgFinalAchievements');
if (achBox) {
if (state.achievements.length) {
achBox.innerHTML =
'<h3>Здобуті досягнення</h3>' +
'<div class="mg-newspaper__achievements-list">' +
state.achievements.map(function (id) {
const a = achievements.find(function (x) { return x.id === id; });
return a ? '<span class="mg-badge" title="' + escapeHtml(a.desc) + '">' + escapeHtml(a.name) + '</span>' : '';
}).join('') +
'</div>';
} else {
achBox.innerHTML = '';
}
}
}
function shareTo(platform) {
const v = finalVerdict();
const text = 'Я завершив гру «Мер на тиждень» на my-kiev.com.\n' +
'Вердикт: ' + v.title + '\n' +
'Довіра ' + state.trust + '%, бюджет ' + state.budget + ' млн ₴, ЗМІ ' + state.media + '%.\n' +
'Спробуй і ти:';
const url = (location && location.href) ? location.href : 'https://my-kiev.com/';
let target = '';
if (platform === 'tg') {
target = 'https://t.me/share/url?url=' + encodeURIComponent(url) + '&text=' + encodeURIComponent(text);
} else if (platform === 'fb') {
target = 'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(url) + '"e=' + encodeURIComponent(text);
} else if (platform === 'tw') {
target = 'https://twitter.com/intent/tweet?text=' + encodeURIComponent(text) + '&url=' + encodeURIComponent(url);
}
if (target) window.open(target, '_blank', 'width=620,height=460,noopener');
}
// ───────────────────────────────────────────────────────────
// FLOW
// ───────────────────────────────────────────────────────────
function startNew() {
state = initialState();
saveGame();
renderTimeline();
renderStats(state);
renderAchievements();
renderScenario();
show('game', { scroll: true });
}
function resume() {
if (state.day >= scenarios.length) {
renderFinal();
show('final', { scroll: true });
return;
}
renderTimeline();
renderStats(state);
renderAchievements();
renderScenario();
show('game', { scroll: true });
}
function restart() {
clearGame();
state = initialState();
refreshContinueBtn();
show('welcome');
}
function refreshContinueBtn() {
const btn = $('#mgContinueBtn');
if (!btn) return;
if (hasResumableGame()) btn.classList.remove('mg-is-hidden');
else btn.classList.add('mg-is-hidden');
}
// ───────────────────────────────────────────────────────────
// BINDINGS
// ───────────────────────────────────────────────────────────
function bind() {
const startBtn = $('#mgStartBtn');
if (startBtn) startBtn.addEventListener('click', startNew);
const contBtn = $('#mgContinueBtn');
if (contBtn) contBtn.addEventListener('click', function () {
if (loadGame()) resume();
else startNew();
});
const nextBtn = $('#mgNextBtn');
if (nextBtn) nextBtn.addEventListener('click', nextDay);
const restartBtn = $('#mgRestartBtn');
if (restartBtn) restartBtn.addEventListener('click', function () {
if (confirm('Почати каденцію заново? Поточний прогрес буде втрачено.')) restart();
});
const restart2 = $('#mgRestart2Btn');
if (restart2) restart2.addEventListener('click', restart);
const shareTg = $('#mgShareTg');
const shareFb = $('#mgShareFb');
const shareTw = $('#mgShareTw');
if (shareTg) shareTg.addEventListener('click', function () { shareTo('tg'); });
if (shareFb) shareFb.addEventListener('click', function () { shareTo('fb'); });
if (shareTw) shareTw.addEventListener('click', function () { shareTo('tw'); });
/* keyboard shortcuts: A/B/C choose, Space advances */
document.addEventListener('keydown', function (e) {
if (root.querySelector('[data-screen="game"]') && !screens.game.hidden) {
const result = $('#mgResult');
const isResult = result && !result.hidden;
if (isResult && (e.code === 'Space' || e.code === 'Enter')) {
e.preventDefault();
nextDay();
return;
}
if (!isResult) {
const k = e.key.toUpperCase();
if (k === 'A' || k === 'B' || k === 'C') {
const idx = k.charCodeAt(0) - 65;
const btn = root.querySelector('#mgChoices .mg-choice[data-idx="' + idx + '"]');
if (btn && !btn.disabled) { e.preventDefault(); btn.click(); }
}
}
}
});
}
// ───────────────────────────────────────────────────────────
// INIT
// ───────────────────────────────────────────────────────────
function init() {
bind();
refreshContinueBtn();
show('welcome');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();