2PlayerGame/detective-game/casegen/devices.js

87 lines
6.6 KiB
JavaScript
Raw Normal View History

// 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 };