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