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

101 lines
11 KiB
JavaScript
Raw Normal View History

// casegen/junk.js — фейкові сліди, дрібниці, плітки
const { pick, shuffle, chance } = require('./rng');
const N = require('./names');
function setupJunk(ctx) {
const { r, citizens, victim, killer, herring, caseType, district, add, E, g, witnessFrom, dna2 } = ctx;
const nb = () => witnessFrom(citizens);
const dogOwner = citizens.find(c => c.dog && c.id !== victim.id);
const junkTexts = shuffle(r, [
{ t: 'Чек із бару', d: 'Два еспресо і коньяк, 21:47. На звороті олівцем — номер телефону без імені. Половина цифр затерта.' },
{ t: 'Стара запальничка', d: `Бензинова, з гравіюванням «${pick(r, ['С.', 'М.', 'В.', 'Т.'])}». Давно не заправлялась.` },
{ t: 'Флаєр', d: '«Ремонт взуття за годину». Усі відривні номери відірвані — популярний швець.' },
{ t: 'Порожня пляшка', d: 'З-під дешевого вина, ще липка. Поруч — два пластикові стаканчики.' },
{ t: 'Список покупок', d: 'Молоко, батарейки, свічки, скотч. Почерк квапливий.' },
{ t: 'Одна рукавичка', d: 'Шкіряна, права, добротна. Ліва, вочевидь, досі гріє чиюсь кишеню.' },
{ t: 'Прострочена квитанція', d: `Комуналка на ім'я «${nb().name}», третє нагадування. Хтось не любить платити вчасно.` },
{ t: 'Записник із адресами', d: `Половина сторінок вирвана. Серед уцілілих записів — «${nb().name}» і закреслений номер.` },
{ t: 'Білети в кіно', d: 'Два білети двомісячної давності, сеанс 21:30, місця поруч. І обгортка від шоколаду.' },
{ t: 'Іржавий ключ', d: 'Не підходить до жодних дверей поблизу. Хтось носив його «про всяк випадок» роками.' },
{ t: 'Оголошення про пса', d: dogOwner ? `«Загубився пес на кличку ${dogOwner.dog}. Винагорода.» Судячи з дати — пес давно вдома.` : '«Загубився пес. Винагорода.» Без прикмет — оптимістично.' },
{ t: 'Стос старих газет', d: 'В одній обведено оголошення про оренду квартири. Олівець аж продавив папір.' },
{ t: 'Візитка таксі', d: 'Від руки написано час: «23:15». Ні дати, ні імені.' },
{ t: 'Розбитий парасоль', d: 'І чек на новий — того ж дня, з маркету. Практична людина.' },
{ t: 'Гребінець із монограмою', d: 'Кістяний, старий, доглянутий. Літера затерта до нечитабельності.' },
{ t: 'Театральна програмка', d: 'Гастролі обласного театру, третій ряд. На полях — сердечко олівцем.' },
{ t: 'Ключниця з трьома ключами', d: 'Жоден не підписаний. Хтось досі відчиняє двері навпомацки.' },
{ t: 'Обгортка від валідолу', d: 'Свіжа. Комусь тут нещодавно було недобре — або дуже нервово.' },
{ t: 'Дитячий малюнок', d: 'Будинок, сонце і три постаті. Складений учетверо і затертий на згинах — його носили з собою.' },
{ t: 'Лотерейний білет', d: 'Не виграшний. Закреслений хрест-навхрест із таким натиском, що порвався папір.' },
{ t: 'Окуляри з тріснутим склом', d: 'Сильні діоптрії. Власник без них як без рук — але по них не повернувся.' },
{ t: 'Розклад електричок', d: 'Обведено останній нічний рейс. Двічі.' },
{ t: 'Порожній футляр від скрипки', d: 'Оксамит витертий. Або інструмент продали, або носять у ньому щось інше.' },
{ t: 'Пакетик насіння', d: 'Чорнобривці. Строк придатності вийшов три роки тому. Хтось усе не наважиться посадити.' },
{ t: 'Квитанція з ломбарду', d: `Здано «годинник чол. позолоч.». Викуп — до кінця місяця. Сума немала.` },
{ t: 'Зламана рація', d: 'Дитяча, з наліпкою блискавки. Друга з пари десь чекає на звʼязок.' },
{ t: 'Листівка без марки', d: '«Вітаю з ювілеєм!» — написано, підписано, не надіслано. Вже роки як.' },
{ t: 'Шматок пакувального шпагату', d: 'Свіжий зріз, гострим ножем. Таким перевʼязують коробки. Або щось інше.' },
{ t: 'Кишеньковий календарик', d: 'Позаминулий рік. Пʼятниці обведені. Усі до однієї.' },
{ t: 'Металева фляжка', d: 'Порожня, з увігнутим боком. Пахне не чаєм.' }
]);
const junkSpots = [];
for (const b of district.buildings) for (const sp of (b.spots || [])) junkSpots.push({ type: 'spot', building: b.id, spot: sp });
const chosen = shuffle(r, junkSpots).slice(0, 8);
const usedSlots = new Set(Object.values(E).filter(e => e.at.type === 'unit').map(e => e.at.unit + '|' + e.at.slot));
for (const [unit, n] of [[victim.home, 2], [killer.home, 1], [herring.home, 1]]) {
let placed = 0;
for (const slot of shuffle(r, district.homeSlots)) {
if (placed >= n) break;
if (usedSlots.has(unit + '|' + slot)) continue;
usedSlots.add(unit + '|' + slot); chosen.push({ type: 'unit', unit, slot }); placed++;
}
}
chosen.forEach((at, i) => {
const j = junkTexts[i % junkTexts.length];
add({ id: 'junk_' + i, side: 'field', at, title: j.t, tag: 'дрібниця', detail: j.d });
});
// Плітки
const GOSSIP = [
() => `Кажуть, ${nb().first} ${nb().last} і ${nb().first} не вітаються після сварки за паркування.`,
() => `${nb().name} нібито у великих боргах за комуналку. Але то таке, люди всяке кажуть.`,
() => `Подейкують, ${nb().name} шукає нову роботу — на старій щось не склалося.`,
() => `${nb().name} щовечора в барі. Щовечора!`,
() => `Бачили, як ${nb().name} кричить у телефон просто посеред вулиці. Аж луна йшла.`,
() => `У «Каштані» хтось заливає сусідів знизу, і всі чомусь думають на ${nb().name}.`,
() => 'На складі маркету вночі інколи світиться. Сторож каже — миші. Ага, миші з ліхтариком.',
() => `${nb().name} нібито бачили на набережній о третій ночі. Безсоння, кажуть.`,
() => 'У гуртожитку знову міняли замки — хтось утретє загубив ключі.',
() => `Подейкують, ${nb().name} збирає гроші на авто. Або на переїзд. Або на весілля. Версії різняться.`,
() => `${nb().name} виносить сміття о п'ятій ранку. Щодня. Хто взагалі стільки смітить?`,
() => { const p = nb(); return `Кажуть, ${p.name} писа${g(p, 'в', 'ла')} скарги на пів району. Пише красиво, з посиланнями на закони.`; },
() => `У ${nb().first} з ${nb().first} щось було. Або є. Або буде. Двір слідкує.`,
() => `${nb().name} тримає на балконі голубів. Сусіди знизу мають окрему думку про це.`,
() => 'На набережній вечорами хтось грає на трубі. Гарно грає. Сумно.',
() => { const p = nb(); return `${p.name} нібито ви${g(p, 'грав', 'грала')} у лотерею і мовчить. Мовчить підозріло добре.`; },
() => { const p = nb(); return `Бачили, як ${p.name} годує всіх котів району. Клички ${g(p, 'дав', 'дала')} кожному. Веде список.`; },
() => 'У клініці кажуть, що нічний охоронець спить. Охоронець каже, що медитує.',
() => { const p = nb(); return `${p.name} посвари${g(p, 'вся', 'лася')} з усім будинком через антену на даху. Війна триває.`; },
() => `Подейкують, ${nb().name} ходить до бару, але не п'є — сидить і слухає чужі розмови.`,
() => 'В автомайстерні беруть «за терміновість» удвічі. Терміново тепер усе.',
() => `${nb().name} щонеділі кудись їздить першим автобусом. Питати незручно, а цікаво.`,
() => 'Хтось ночами переставляє лавку в парку. Комунальники здалися.'
];
for (const c of citizens) {
if (c.id === victim.id) { c.gossip = []; continue; }
c.gossip = [pick(r, GOSSIP)()];
if (chance(r, 0.4)) c.gossip.push(pick(r, GOSSIP)());
}
const wit = nb();
wit.gossip.push(caseType === 'kidnap'
? `Пізно того вечора хтось у ${killer.coat} тягнув щось важке в бік проїзду. ${g(wit, 'Подумав', 'Подумала')} — килим. А тепер думаю — не килим.`
: `Пізно того вечора хтось у ${killer.coat} швидко йшов від місця, де потім знайшли тіло. Може, здалося.`);
nb().gossip.push(`У квартирі ${killer.home} останнім часом світло горить до третьої ночі. Комусь не спиться.`);
if (dna2 && dna2.why === 'brawl')
nb().gossip.push(`Удень перед тим бачили, як ${dna2.owner.name} ${g(dna2.owner, 'зчепився', 'зчепилася')} з ${victim.first} ${victim.last} у барі${dna2.reason}. Розборонили швидко, обоє розійшлися своїми ногами.`);
}
module.exports = { setupJunk };