diff --git a/apps/game/assets/chars/hero_down_1.png b/apps/game/assets/chars/hero_down_1.png index 1290c5a..996100a 100644 --- a/apps/game/assets/chars/hero_down_1.png +++ b/apps/game/assets/chars/hero_down_1.png Binary files differ diff --git a/apps/game/assets/chars/hero_down_2.png b/apps/game/assets/chars/hero_down_2.png index b4e2a8e..286d79e 100644 --- a/apps/game/assets/chars/hero_down_2.png +++ b/apps/game/assets/chars/hero_down_2.png Binary files differ diff --git a/apps/game/assets/chars/hero_sheet.png b/apps/game/assets/chars/hero_sheet.png index 93591b3..d3343fd 100644 --- a/apps/game/assets/chars/hero_sheet.png +++ b/apps/game/assets/chars/hero_sheet.png Binary files differ diff --git a/apps/game/assets/chars/hero_side_1.png b/apps/game/assets/chars/hero_side_1.png index 1b96208..e2ae35b 100644 --- a/apps/game/assets/chars/hero_side_1.png +++ b/apps/game/assets/chars/hero_side_1.png Binary files differ diff --git a/apps/game/assets/chars/hero_side_2.png b/apps/game/assets/chars/hero_side_2.png index cc4d0fb..579aed5 100644 --- a/apps/game/assets/chars/hero_side_2.png +++ b/apps/game/assets/chars/hero_side_2.png Binary files differ diff --git a/apps/game/assets/chars/hero_up_1.png b/apps/game/assets/chars/hero_up_1.png index 149083a..8cfd256 100644 --- a/apps/game/assets/chars/hero_up_1.png +++ b/apps/game/assets/chars/hero_up_1.png Binary files differ diff --git a/apps/game/assets/chars/hero_up_2.png b/apps/game/assets/chars/hero_up_2.png index 149083a..8cfd256 100644 --- a/apps/game/assets/chars/hero_up_2.png +++ b/apps/game/assets/chars/hero_up_2.png Binary files differ diff --git a/apps/game/src/scenes/BootScene.ts b/apps/game/src/scenes/BootScene.ts index 09877ed..38b3624 100644 --- a/apps/game/src/scenes/BootScene.ts +++ b/apps/game/src/scenes/BootScene.ts @@ -15,7 +15,7 @@ constructor(private game: Game) { const label = new PixelText({ text: 'пепел оседает...', - size: 10, + size: 11, color: 0x8a8a9a }); label.anchor.set(0.5); diff --git a/apps/game/src/scenes/InventoryScene.ts b/apps/game/src/scenes/InventoryScene.ts index 62652f8..274ad72 100644 --- a/apps/game/src/scenes/InventoryScene.ts +++ b/apps/game/src/scenes/InventoryScene.ts @@ -19,7 +19,7 @@ panel.addChild(title); const addLine = (text: string, color: number, x: number, y: number): void => { - const t = new PixelText({ text, size: 9, color }); + const t = new PixelText({ text, size: 10, color }); t.position.set(x, y); panel.addChild(t); }; @@ -29,19 +29,19 @@ let y = 50; for (const item of inventoryItems(this.game.state)) { addLine(item, 0xd8c79a, 24, y); - y += 12; + y += 13; } // Журнал квестов addLine('Журнал', 0x999988, 16, y + 6); - y += 20; + y += 21; for (const entry of questLog(this.game.state)) { const mark = entry.done ? 'x' : '·'; addLine(`[${mark}] ${entry.text}`, entry.done ? 0xd8c79a : 0xaaaaaa, 24, y); - y += 12; + y += 13; } - const hint = new PixelText({ text: 'Esc — назад', size: 8, color: 0x777788 }); + const hint = new PixelText({ text: 'Esc — назад', size: 9, color: 0x777788 }); hint.anchor.set(0.5); hint.position.set(140, 168); panel.addChild(hint); diff --git a/apps/game/src/scenes/LocationScene.ts b/apps/game/src/scenes/LocationScene.ts index 483f7ca..fc93ca4 100644 --- a/apps/game/src/scenes/LocationScene.ts +++ b/apps/game/src/scenes/LocationScene.ts @@ -164,7 +164,7 @@ this.game.renderer.worldRoot.addChild(this.ash); // Название локации в правом верхнем углу. - const name = new PixelText({ text: location.name, size: 10, color: 0x999988 }); + const name = new PixelText({ text: location.name, size: 11, color: 0x999988 }); name.anchor.set(1, 0); name.position.set(474, 4); this.game.renderer.uiRoot.addChild(name); @@ -183,11 +183,12 @@ this.hint = new Container(); const text = new PixelText({ - text: 'клик — идти · клик по сгустку — атаковать · Space — удар (удержать — резонанс) · I — сумка · Esc — меню', - size: 8, + text: 'клик — идти/атаковать · Space — удар (удержать — резонанс) · I — сумка · Esc — меню', + size: 10, color: 0x999988 }); - text.position.set(6, 4); + // Под сердечками (HealthBar занимает y≈13..18). + text.position.set(6, 23); this.hint.addChild(text); this.game.renderer.uiRoot.addChild(this.hint); @@ -537,7 +538,7 @@ /** Всплывающая подсказка: появляется и растворяется над UI. */ private showToast(text: string): void { - const toast = new PixelText({ text, size: 9, color: 0xd8c79a }); + const toast = new PixelText({ text, size: 11, color: 0xd8c79a }); toast.anchor.set(0.5); toast.position.set(240, 40); toast.alpha = 0; diff --git a/apps/game/src/scenes/MenuScene.ts b/apps/game/src/scenes/MenuScene.ts index 7a436b2..1b43ef2 100644 --- a/apps/game/src/scenes/MenuScene.ts +++ b/apps/game/src/scenes/MenuScene.ts @@ -27,7 +27,7 @@ const subtitle = new PixelText({ text: 'пепел всё ещё дышит', - size: 10, + size: 11, color: 0x8a8a9a }); subtitle.anchor.set(0.5); @@ -85,10 +85,10 @@ const hint = new PixelText({ text: 'клик по тайлу — идти · клик по NPC — говорить\nEsc в игре — меню с сохранением', - size: 8, + size: 9, color: 0x666677, align: 'center', - lineHeight: 12, + lineHeight: 13, wordWrapWidth: 400 }); hint.anchor.set(0.5); diff --git a/apps/game/src/scenes/SaveSlotsScene.ts b/apps/game/src/scenes/SaveSlotsScene.ts index d6b0371..fc7d4eb 100644 --- a/apps/game/src/scenes/SaveSlotsScene.ts +++ b/apps/game/src/scenes/SaveSlotsScene.ts @@ -23,7 +23,7 @@ title.position.set(120, 14); panel.addChild(title); - this.menu = new MenuList({ width: 200, height: 13, gap: 3, size: 9 }); + this.menu = new MenuList({ width: 200, height: 14, gap: 3, size: 10 }); this.menu.position.set(20, 36); panel.addChild(this.menu); diff --git a/apps/game/src/scenes/SettingsScene.ts b/apps/game/src/scenes/SettingsScene.ts index 64c48b9..04a1a3d 100644 --- a/apps/game/src/scenes/SettingsScene.ts +++ b/apps/game/src/scenes/SettingsScene.ts @@ -29,7 +29,7 @@ title.position.set(110, 14); panel.addChild(title); - this.menu = new MenuList({ width: 180, height: 14, gap: 4, size: 9 }); + this.menu = new MenuList({ width: 180, height: 16, gap: 4, size: 10 }); this.menu.position.set(20, 36); panel.addChild(this.menu); diff --git a/packages/engine/src/ui/DialogueBox.ts b/packages/engine/src/ui/DialogueBox.ts index 88abf66..0d48c6b 100644 --- a/packages/engine/src/ui/DialogueBox.ts +++ b/packages/engine/src/ui/DialogueBox.ts @@ -47,7 +47,8 @@ this.width = options.width; this.height = options.height; this.margin = options.margin; - this.size = options.size ?? 9; + // 8–9px у VT323 нечитаемы (тонкие штрихи размазываются) — минимум 10. + this.size = Math.max(options.size ?? 10, 10); this.onChoice = options.onChoice ?? null; this.view = new Container(); @@ -73,10 +74,10 @@ const w = this.width; const h = this.height; const m = this.margin; - this.panel.rect(m, h - m - 48, w - m * 2, 48).fill({ color: 0x101018, alpha: 0.92 }); - this.panel.rect(m, h - m - 48, w - m * 2, 48).stroke({ color: 0x8899aa, width: 1 }); - this.nameText.position.set(m + 5, h - m - 46); - this.bodyText.position.set(m + 5, h - m - 36); + this.panel.rect(m, h - m - 54, w - m * 2, 54).fill({ color: 0x101018, alpha: 0.92 }); + this.panel.rect(m, h - m - 54, w - m * 2, 54).stroke({ color: 0x8899aa, width: 1 }); + this.nameText.position.set(m + 5, h - m - 52); + this.bodyText.position.set(m + 5, h - m - 42); this.hintText.position.set(w - m - 42, h - m - 10); } @@ -95,7 +96,7 @@ this.choiceViews = []; if (line.choices && line.choices.length > 0) { const m = this.margin; - let y = this.height - m - 36; + let y = this.height - m - 42; for (const item of line.choices) { const label = new PixelText({ text: `▸ ${item.text}`, diff --git a/tools/font-probe.mjs b/tools/font-probe.mjs new file mode 100644 index 0000000..3a173c1 --- /dev/null +++ b/tools/font-probe.mjs @@ -0,0 +1,54 @@ +/** + * Проба читаемости VT323 c НАСТОЯЩИМ конвейером игры: текст растеризуется + * в канвас 480×270 (как текстура Pixi при resolution 1), затем растягивается + * целым множителем с pixelated. Сравнение кеглей и resolution-подхода. + * Запуск: node tools/font-probe.mjs [скриншот] + */ +import puppeteer from 'puppeteer-core'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const ttf = readFileSync(path.join(root, 'packages/engine/assets/fonts/VT323-Regular.ttf')).toString('base64'); +const shot = process.argv[2] ?? '/tmp/rpg_font_probe.png'; + +const html = ` + +`; + +const browser = await puppeteer.launch({ + executablePath: '/usr/bin/chromium', + headless: true, + args: ['--no-sandbox', '--enable-unsafe-swiftshader', '--use-angle=swiftshader', '--window-size=960,540'] +}); +const page = await browser.newPage(); +await page.setViewport({ width: 960, height: 540 }); +await page.setContent(html, { waitUntil: 'networkidle0' }); +await page.evaluate(() => document.fonts.ready); +await page.evaluate(() => window.start()); +await new Promise((r) => setTimeout(r, 300)); +await page.screenshot({ path: shot }); +console.log(`Скриншот: ${shot}`); +await browser.close(); \ No newline at end of file diff --git a/tools/pixelart/gen.mjs b/tools/pixelart/gen.mjs index 49f527a..71c52e5 100644 --- a/tools/pixelart/gen.mjs +++ b/tools/pixelart/gen.mjs @@ -184,20 +184,20 @@ '.....oooooo.....', '....oTTTTTTo....', '...oTttttttTo...', - '...otttttttto...', - '....ooSSSSoo....', - '....oSoPSoSo....', - '....oSsSsSso....', + '..oTttttttttTo..', + '..oooooooooooo..', + '....oSSSSSSo....', + '....oSoSSoSo....', + '....oSSSsSSo....', '.....oSSSSo.....', - '.....oCCCCo.....', - '...oCcCccCcCo...', - '...oCcCccCcCo...', - '...oCccCcCccCo..', - '...oCcCccCcCo...', - '...obBccccBbo...', - '...oCcCccCcCo...', - '...oCccCcCccCo..', - '....oCooCoo.....', + '....oCCCCCCo....', + '...oCCCCCCCCo...', + '..oCCCCCCCCCCo..', + '..oCCCCCCCCCCo..', + '..obbBBBBBBbbo..', + '..oCCCCCCCCCCo..', + '..oCCCCCCCCCCo..', + '...oCCo..oCCo...', '....oPo..oPo....', '....oPo..oPo....', '...oPPo..oPPo...', @@ -212,53 +212,53 @@ '.....oooooo.....', '....oTTTTTTo....', '...oTttttttTo...', - '...otttttttto...', - '....ooSSSSoo....', - '....oSoPSoSo....', - '....oSsSsSso....', + '..oTttttttttTo..', + '..oooooooooooo..', + '....oSSSSSSo....', + '....oSoSSoSo....', + '....oSSSsSSo....', '.....oSSSSo.....', - '.....oCCCCo.....', - '...oCcCccCcCo...', - '...oCcCccCcCo...', - '...oCccCcCccCo..', - '...oCcCccCcCo...', - '...obBccccBbo...', - '...oCcCccCcCo...', - '...oCccCcCccCo..', - '....oCo.oCo.....', + '....oCCCCCCo....', + '...oCCCCCCCCo...', + '..oCCCCCCCCCCo..', + '..oCCCCCCCCCCo..', + '..obbBBBBBBbbo..', + '..oCCCCCCCCCCo..', + '..oCCCCCCCCCCo..', + '....oCCoCCo.....', '....oPo.oPo.....', '....oPo.oPo.....', - '....oPPooPPo....', - '.....oPPPPo.....', - '......oooo......', + '....oPPoPPo.....', + '.....oPPPo......', + '......ooo.......', '................' ], HERO_MAP); -// Спина: капюшон, узел плаща, лампа на поясе (F1 — единственный тёплый пиксель). +// Спина: швы капюшона под шляпой, лампа на поясе (единственный тёплый пиксель). const heroUp1 = fromAscii([ '................', '.....oooooo.....', '....oTTTTTTo....', '...oTttttttTo...', - '...otttttttto...', + '..oTttttttttTo..', + '..oooooooooooo..', '.....oTttTo.....', - '....ooCooCoo....', + '....oCCCCCCo....', '...oCCCCCCCCo...', - '...oCcCccCcCo...', - '...oCccCcCccCo..', - '...oCcCccCcCo...', - '...oCcCcccCcCo..', - '...oCccCcCccCo..', - '...oCcccccccCo..', - '...oCffCCffCo...', - '...oCcCccCcCo...', - '....oCooCoo.....', + '..oCCCCCCCCCCo..', + '..oCCCCCCCCCCo..', + '..oCcCCCCCCcCo..', + '..oCCCCCCCCCCo..', + '..oCCCCCCCCCCo..', + '..obbBBffBBbbo..', + '..oCCCCCCCCCCo..', + '..oCCCCCCCCCCo..', + '...oCCo..oCCo...', '....oPo..oPo....', '....oPo..oPo....', '...oPPo..oPPo...', '...oPPo..oPPo...', '....oo....oo....', - '................', '................' ], { ...HERO_MAP, f: 'F0' }); @@ -266,56 +266,56 @@ const heroSide1 = fromAscii([ '................', - '.....ooooo......', - '....oTTTTTo.....', - '...oTtttttto....', - '...ottttttto....', - '.....oSSSSo.....', - '.....oSSoPo.....', - '.....oSsSo......', - '.....oSSSo......', - '.....oCCCCo.....', - '....oCcCccCo....', - '....oCccCcCo....', - '....oCcCccCo....', - '....oCcccBbo....', - '....oCcCccCo....', - '....oCccCcCo....', - '.....oCoCco.....', - '.....oPo.Po.....', - '.....oPo.Po.....', - '....oPPo.PPo....', - '....oPPo.PPo....', - '.....oo...oo....', - '................', + '.....oooooo.....', + '....oTTTTTTo....', + '...oTttttttTo...', + '..oTttttttttTo..', + '..oooooooooooo..', + '.....oSSSSSSo...', + '.....oSSoSSo....', + '.....oSSSSSs....', + '......oSSSSo....', + '.....oCCCCCo....', + '....oCCCCCCCo...', + '...oCCCCCCCCCo..', + '...oCCCCCCCCCo..', + '...obbBBBBBBbo..', + '...oCCCCCCCCCo..', + '...oCCCCCCCCCo..', + '....oCCoCCo.....', + '....oPo.oPo.....', + '....oPo.oPo.....', + '...oPPo.oPPo....', + '...oPPo.oPPo....', + '....oo...oo.....', '................' ], HERO_MAP); -// Шаг в бок: передняя нога вытянута. +// Шаг в бок: ноги шире (фаза strides). const heroSide2 = fromAscii([ '................', - '.....ooooo......', - '....oTTTTTo.....', - '...oTtttttto....', - '...ottttttto....', - '.....oSSSSo.....', - '.....oSSoPo.....', - '.....oSsSo......', - '.....oSSSo......', - '.....oCCCCo.....', - '....oCcCccCo....', - '....oCccCcCo....', - '....oCcCccCo....', - '....oCcccBbo....', - '....oCcCccCo....', - '....oCccCcCo....', - '.....oCoCco.....', - '....oPo..Po.....', - '...oPPo..oPo....', - '...oPPo...PPo...', - '....oo....PPo...', - '..........oo....', - '................', + '.....oooooo.....', + '....oTTTTTTo....', + '...oTttttttTo...', + '..oTttttttttTo..', + '..oooooooooooo..', + '.....oSSSSSSo...', + '.....oSSoSSo....', + '.....oSSSSSs....', + '......oSSSSo....', + '.....oCCCCCo....', + '....oCCCCCCCo...', + '...oCCCCCCCCCo..', + '...oCCCCCCCCCo..', + '...obbBBBBBBbo..', + '...oCCCCCCCCCo..', + '...oCCCCCCCCCo..', + '....oCCoCCo.....', + '...oPo...oPo....', + '...oPo...oPo....', + '..oPPo...oPPo...', + '..oPPo...oPPo...', + '...oo.....oo....', '................' ], HERO_MAP);