Files
fishing-game/tools/embed-fish.mjs
T
andrey 482bdec145 Этап 56: Которыбка — болотный босс переименован и перерисован из целаканта (идея «кошачьей рыбы» — милая вымышленная рыба-кот, не окаменелость). (1) Ренейм coelacanth→meowfish «Которыбка»/Meowfish: SPECIES[37] (hue 28, glow #ff9ecb — розовая аура вместо сине-стальной), NIGHT_WEIGHTS, SWAMP_IDS, SWAMP_DAY/NIGHT_WEIGHTS, BOSS_BY_LOCATION[4], ачивка swamp_boss («Победа над Которыбкой»/Meowfish Tamer, check=bosses.meowfish; id/без coinPct — пин 0.18 жив). Числа (цена 50🪙/кг, 20–80 кг, ролл 4, статы) не менялись — баланс Этапа 55 сохранён 1:1 (Болото 302.9/342.3). Сейв: bosses — динамический словарь, миграция не нужна (целакант не выходил в релизе), выданная ach не отзывается. (2) Портрет: новый fish-meowfish.svg — пухлое рыжее тельце с кремовым брюшком и полосками «табби», ушки с розовым внутренним, большой зелёный глаз, носик-треугольничек, ротик-«бантик», усики, щёчка, лапки, пушистый полосатый хвост-плавник; fish-coelacanth.svg удалён; embed-fish IDS обновлён, спрайт перегенерирован (38 symbol). (3) Тесты 234/234 (счётчик тот же): §9k2 Болото → Которыбка (карточка «Которыбка-БОСС», bosses.meowfish), lc1 (mwf='Которыбка,4,50,20,80', swampOrder …,meowfish), lc2/lc10, тексты. (4) Проверки: check.mjs OK; test.js 234/234 exit 0; balance.mjs — 302.9/342.3 без изменений, целостность зелёная; emoji --check OK; minify 6/6 (583→386 КБ); pack 5/5; pack-smoke 6/6. (5) Доки: AGENTS.md/README — Которыбка/meowfish, DEVLOG (Этап 56).
2026-10-30 20:32:25 +03:00

139 lines
7.4 KiB
JavaScript

#!/usr/bin/env node
// tools/embed-fish.mjs — встраивает 38 SVG-портретов рыб (assets/fish/fish-<id>.svg)
// в index.html как СКРЫТЫЙ SVG-спрайт из <symbol> (id="fish-<id>", viewBox 0 0 160 100).
//
// Запуск: node tools/embed-fish.mjs (из каталога репо или откуда угодно — пути от репо)
// - идемпотентен: если маркеры FISH-SVG:BEGIN/END уже есть — заменяет ВСЁ между ними;
// если нет — вставляет блок сразу ПОСЛЕ открывающего тега <body> (в начале файла).
// - после записи — самопроверка: 38 вхождений id="fish-, оба маркера, затем node tools/check.mjs.
// Чистый Node, без npm-зависимостей. Детерминирован: одинаковый ввод → одинаковый index.html.
//
// ВАЖНО: это НЕ подключение UI. Дневник/карточка/JS-логику НЕ трогаем —
// <use href="#fish-<id>"> подключит следующий этап.
import { readFileSync, writeFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const INDEX = join(ROOT, 'index.html');
const ASSETS = join(ROOT, 'assets', 'fish');
// Порядок = порядок SPECIES в index.html (карасс → кит → дух болота → которыбка).
const IDS = [
'carass', 'perch', 'bream', 'carp', 'pike', 'catfish', 'sturge', 'gold',
'zander', 'chuch', 'crayfish', 'bleak', 'roundel', 'eelpout', 'tench', 'navaga',
'rudd', 'cisco', 'herring', 'mackerel', 'flounder', 'tuna', 'swordfish',
'moray', 'grouper', 'turtle', 'angler', 'octopus', 'manta', 'shark', 'squid', 'whale',
'glowfin', 'moonfish', 'emberfin', 'violetlure', 'spiritfin', 'meowfish',
];
const BEGIN = '<!-- FISH-SVG:BEGIN (генерируется tools/embed-fish.mjs — не править руками) -->';
const END = '<!-- FISH-SVG:END -->';
const VIEWBOX = 'viewBox="0 0 160 100"';
function fail(msg) {
console.error(`embed-fish: ОШИБКА: ${msg}`);
process.exit(1);
}
// Чтение и валидация одного fish-<id>.svg → ВНУТРЕННЕЕ содержимое (между '>' корневого
// <svg ...> и '</svg>'). Корневой тег (width/height) в symbol не нужен — только viewBox.
function loadInner(id) {
const file = `fish-${id}.svg`;
let s;
try {
s = readFileSync(join(ASSETS, file), 'utf8');
} catch (e) {
fail(`${file} (вид ${id}): не удалось прочитать: ${e.message}`);
}
// BOM допустим
if (s.charCodeAt(0) === 0xFEFF) s = s.slice(1);
// Перед <svg допустимы пробельные символы и XML-комментарии (в файлах — поясняющий комментарий вида).
let i = 0;
for (;;) {
const ws = /^[ \t\r\n]+/.exec(s.slice(i));
if (ws) { i += ws[0].length; continue; }
const cm = /^<!--[\s\S]*?-->/.exec(s.slice(i));
if (cm) { i += cm[0].length; continue; }
break;
}
// Корневой тег: <svg, за ним пробел (атрибуты) или '>'
if (!/^<svg([ >])/.test(s.slice(i))) {
fail(`${file} (вид ${id}): файл не начинается с <svg (после BOM/пробелов/комментариев)`);
}
if (!s.includes(VIEWBOX)) {
fail(`${file} (вид ${id}): не найдено ${VIEWBOX}`);
}
if (!s.trimEnd().endsWith('</svg>')) {
fail(`${file} (вид ${id}): файл не заканчивается на </svg>`);
}
const openEnd = s.indexOf('>', i); // закрывающая '>' корневого <svg ...>
const closeStart = s.lastIndexOf('</svg>'); // финальное </svg>
if (openEnd === -1 || closeStart === -1 || closeStart < openEnd) {
fail(`${file} (вид ${id}): нераспознана структура (корневой <svg> / </svg>)`);
}
return s.slice(openEnd + 1, closeStart);
}
// Сборка блока спрайта (строка на symbol; внутреннее содержимое — как в исходнике).
function buildBlock() {
const lines = [BEGIN, '<svg id="fishSprites" width="0" height="0" style="position:absolute" aria-hidden="true">'];
for (const id of IDS) {
lines.push(`<symbol id="fish-${id}" ${VIEWBOX}>${loadInner(id)}</symbol>`);
}
lines.push('</svg>', END);
return lines.join('\n');
}
// --- основная логика ---
let html;
try {
html = readFileSync(INDEX, 'utf8');
} catch (e) {
fail(`не удалось прочитать ${INDEX}: ${e.message}`);
}
const block = buildBlock();
const b = html.indexOf(BEGIN);
const e = html.indexOf(END);
if (b !== -1 || e !== -1) {
// Маркеры есть (хотя бы один) — заменяем ВСЁ между ними (идемпотентность повторного запуска).
if (b === -1 || e === -1 || e < b) fail('маркеры FISH-SVG:BEGIN/END найдены, но парно/порядком — index.html повреждён');
html = html.slice(0, b) + block + html.slice(e + END.length);
console.log('embed-fish: маркеры уже есть — блок заменён (повторный запуск)');
} else {
// Маркеров нет — вставляем сразу ПОСЛЕ открывающего тега <body> (с атрибутами, первый в файле).
const m = /<body(\s[^>]*)?>/.exec(html);
if (!m) fail('в index.html не найден открывающий тег <body>');
const pos = m.index + m[0].length;
html = html.slice(0, pos) + '\n' + block + html.slice(pos);
console.log(`embed-fish: блок вставлен после <body> (строка ${html.slice(0, pos).split('\n').length})`);
}
writeFileSync(INDEX, html);
// --- самопроверка: перечитываем файл и убеждаемся, что всё на месте ---
const check = readFileSync(INDEX, 'utf8');
const nSymbols = (check.match(/id="fish-/g) || []).length;
if (nSymbols !== IDS.length) fail(`самопроверка: ожидалось ${IDS.length} вхождений id="fish-, найдено ${nSymbols}`);
if (check.split(BEGIN).length - 1 !== 1) fail('самопроверка: маркер FISH-SVG:BEGIN не найден (или дублируется)');
if (check.split(END).length - 1 !== 1) fail('самопроверка: маркер FISH-SVG:END не найден (или дублируется)');
// Синтаксис JS в index.html (node --check на вынутых блоках) — как п.1 «Проверки» из AGENTS.md.
let out;
try {
out = execFileSync(process.execPath, [join(ROOT, 'tools', 'check.mjs')], { encoding: 'utf8', cwd: ROOT });
} catch (err) {
process.stdout.write(err.stdout || '');
process.stderr.write(err.stderr || String(err));
fail('node tools/check.mjs завершился с ошибкой');
}
process.stdout.write(out);
const blockBytes = Buffer.byteLength(block, 'utf8');
const blockLines = block.split('\n').length;
console.log(`embed-fish: OK — ${IDS.length} символов, блок ${blockBytes} байт / ${blockLines} строк, маркеры на месте, check.mjs пройден`);