86 lines
6.6 KiB
JavaScript
86 lines
6.6 KiB
JavaScript
// 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 };
|