2PlayerGame/detective-game/casegen/devices.js
2026-08-15 14:07:39 +03:00

86 lines
6.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// casegen/devices.js — телефони та ноутбуки фігурантів, PIN/паролі, вміст
const { chance, pick, shuffle, ri, md5 } = require('./rng');
const { genPassword } = require('./rng');
function setupDevices(ctx) {
const { r, citizens, killer, herring, victim, accomplice, caseType, templateId, district, add, addPuzzle, byId, E, method, fm } = ctx;
const HIST_COMMON = ['погода на тиждень', 'розклад маршруток', 'рецепт як у їдальні', 'дешеві авіаквитки',
'чому болить спина зранку', 'серіали за вечір список', 'як позбутися мурах', 'гороскоп терези'];
const CHAT_COMMON = ['«Купи хліба» — «Вже йду»', '«Ти де?» — «Затримуюсь, не чекайте»',
'«Завтра зміна з восьмої» — «Прийду»', '«Мама дзвонила?» — «Подзвоню ввечері»'];
const histFor = who => {
if (who === killer) return caseType === 'kidnap'
? pick(r, [['навісні замки які надійніші', 'скільки людина може без їжі'], ['чи пишуть камери вночі', 'зняти готівку без банкомата']])
: method.id === 'poison'
? [['барбітурати дозування', 'через скільки діє снодійне'], ['алкоголь і снодійне сумісність', 'чи виявляє розтин отруту']][ri(r, 0, 1)]
: pick(r, [['чи бачать камери вночі', 'як поводитись на допиті'], ['квитки в один бік дешево', 'строк давності тяжких злочинів']]);
if (who === herring) return {
'Борг': ['мікропозика без відмови', 'як швидко заробити'],
'Ревнощі': ['як повернути кохану людину', 'чому мене не помічають'],
'Сусідська війна': ['зразок скарги на сусіда', 'куди скаржитись на шум'],
'Розбите авто': ['вартість ремонту крила', 'як стягнути збитки з винуватця']
}[fm.label];
if (accomplice && who === accomplice) return ['чи можна відкликати свідчення', 'адвокат недорого консультація'];
return {
jealousy: ['готелі подобово поруч'], debt: ['шаблон розписки про борг'], blackmail: ['анонімна пошта створити'],
witness: ['куди повідомити про крадіжку'], stalker: ['як зафіксувати переслідування'], inherit: ['зразок заповіту'],
oldcrime: ['строк давності злочинів'], mistaken: ['як змінити номер телефону'], business: ['митні правила дрібні партії'],
serial: ['самооборона курси'], ransom: ['страхування життя умови'], k_ransom: ['страхування життя умови'],
k_obsession: ['як зафіксувати переслідування'], k_debt: ['шаблон розписки про борг']
}[templateId] || [];
};
const usedDevSlots = new Set();
const devFigures = [[killer, 'k'], [herring, 'h']];
if (accomplice) devFigures.push([accomplice, 'a']);
if (caseType === 'murder' && !['jealousy', 'witness', 'blackmail'].includes(templateId)) devFigures.push([victim, 'v']);
for (const [who, suf] of devFigures) {
const isPhone = chance(r, 0.5);
const taken = sl => usedDevSlots.has(who.home + '|' + sl) ||
Object.values(E).some(e => e.at.type === 'unit' && e.at.unit === who.home && e.at.slot === sl);
const slot = shuffle(r, district.homeSlots).find(sl => !taken(sl)) || 'комора';
usedDevSlots.add(who.home + '|' + slot);
let pz;
if (isPhone) {
const pin = String(who.birthYear);
pz = addPuzzle({
id: 'pz_dev_' + suf, kind: 'pin', hash: md5(pin), answer: pin,
prompt: `Вилучений телефон (${who.name}): заблоковано 4-значним PIN.`,
hint: 'Люди ставлять те, що не забудуть. Рік народження власника є в реєстрі мешканців.'
});
} else {
const pw = genPassword(r, who);
pz = addPuzzle({
id: 'pz_dev_' + suf, kind: 'md5', hash: md5(pw.answer), answer: pw.answer, style: pw.style,
prompt: `Вилучений ноутбук (${who.name}): вхід за паролем. Хеш (md5): ${md5(pw.answer)}.`,
hint: (pw.personal ? 'Особистий пароль — щось про власника, латиницею (див. реєстр): ' : 'Пароль випадковий — лише словниковий перебір: ') + pw.hint
});
}
add({
id: 'dev_' + suf, side: 'field', at: { type: 'unit', unit: who.home, slot },
title: `${isPhone ? 'Телефон' : 'Ноутбук'}: ${who.name}`, tag: 'слід',
detail: `${isPhone ? 'Телефон' : 'Ноутбук'} у житлі ${who.name}. Вилучено для лабораторії: ${isPhone ? 'заблокований PIN-кодом' : 'вхід за паролем'} — далі справа аналітика.`
});
const parts = [];
if (chance(r, 0.3)) {
const lines = shuffle(r, [...histFor(who), ...shuffle(r, HIST_COMMON).slice(0, 3)]).slice(0, 4);
parts.push('Історія браузера: ' + lines.map(l => `«${l}»`).join(', ') + '.');
}
if (chance(r, 0.3)) {
const mateC = who.spouse ? byId(who.spouse) : pick(r, citizens.filter(o => o !== who && o.id !== victim.id));
parts.push(`Останні чати: побут із ${mateC.name}. ` + pick(r, CHAT_COMMON) +
(who === killer ? ` І одне непрочитане того вечора: «Подзвони, як будеш удома» — без відповіді.` : ''));
}
add({
id: 'dev_' + suf + '_data', side: 'analyst', at: { type: 'lab' }, requires: ['dev_' + suf], puzzle: pz,
title: `Вміст пристрою: ${who.name}`, tag: 'слід',
detail: parts.length ? parts.join(' ')
: 'Чисто: банк, погода, пасьянс. Або власник обережний, або йому справді нічого ховати.'
});
}
}
module.exports = { setupDevices };