diff --git a/apps/game/assets/chars/hero_down_2.png b/apps/game/assets/chars/hero_down_2.png index 286d79e..e7b22d3 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 d3343fd..c01294a 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_2.png b/apps/game/assets/chars/hero_side_2.png index 579aed5..b29df09 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_2.png b/apps/game/assets/chars/hero_up_2.png index 8cfd256..395d62e 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/LocationScene.ts b/apps/game/src/scenes/LocationScene.ts index fc93ca4..8c0060c 100644 --- a/apps/game/src/scenes/LocationScene.ts +++ b/apps/game/src/scenes/LocationScene.ts @@ -391,9 +391,10 @@ // ---------- остальное ---------- private updateCameraFollow(): void { - const tile = this.player.currentTile(); - const c = this.tileCenter(tile.x, tile.y); - this.camera.follow(c.x, c.y); + // Непрерывное следование за ногами героя: в покое совпадает с центром тайла, + // в движении карта едет плавно, без рывка на границе тайлов. + const p = this.player.position; + this.camera.follow(p.x, p.y); } /** Текстуры тайлов из загруженных ассетов (id -> Texture). */ diff --git a/tools/cam-probe.mjs b/tools/cam-probe.mjs new file mode 100644 index 0000000..e84d7f5 --- /dev/null +++ b/tools/cam-probe.mjs @@ -0,0 +1,52 @@ +/** + * Проба плавности камеры: отправляем героя на запад и сэмплируем + * worldRoot.position каждые 100 мс — шаги должны быть монотонными и плавными. + * Запуск: node tools/cam-probe.mjs [url] + */ +import puppeteer from 'puppeteer-core'; + +const url = process.argv[2] ?? 'http://localhost:5201/'; +const browser = await puppeteer.launch({ + executablePath: '/usr/bin/chromium', + headless: true, + args: ['--no-sandbox', '--enable-unsafe-swiftshader', '--use-angle=swiftshader', + '--autoplay-policy=no-user-gesture-required', '--window-size=960,540'] +}); +const page = await browser.newPage(); +await page.setViewport({ width: 960, height: 540 }); +page.on('pageerror', (err) => console.log(`[ошибка страницы] ${err.message}`)); + +const booted = new Promise((resolve) => { + page.on('console', (msg) => { + if (msg.text().includes('[location]')) resolve(); + }); +}); + +await page.goto(url, { waitUntil: 'networkidle2', timeout: 20000 }); +await new Promise((r) => setTimeout(r, 2000)); +await page.mouse.click(480, 283); +await booted; +await new Promise((r) => setTimeout(r, 1500)); + +await page.evaluate(() => { + const sc = window.__game.scenes.current; + const t = sc.player.currentTile(); + const p = sc.tileCenter(t.x - 8, t.y); + const canvas = document.querySelector('canvas'); + const rect = canvas.getBoundingClientRect(); + const wr = window.__game.renderer.worldRoot.position; + window.__pt = { x: rect.left + ((p.x + wr.x) / 480) * rect.width, + y: rect.top + ((p.y + wr.y) / 270) * rect.height }; +}); +await page.mouse.click(await page.evaluate(() => window.__pt.x), + await page.evaluate(() => window.__pt.y)); + +for (let i = 0; i < 12; i++) { + const p = await page.evaluate(() => { + const wr = window.__game.renderer.worldRoot.position; + return `${wr.x.toFixed(1)},${wr.y.toFixed(1)}`; + }); + console.log(`${String(i * 100).padStart(4)}мс мир: ${p}`); + await new Promise((r) => setTimeout(r, 100)); +} +await browser.close(); \ No newline at end of file diff --git a/tools/pixelart/gen.mjs b/tools/pixelart/gen.mjs index 8a2c30c..0431b1d 100644 --- a/tools/pixelart/gen.mjs +++ b/tools/pixelart/gen.mjs @@ -206,9 +206,10 @@ '................' ], HERO_MAP); -// Второй кадр ходьбы: ноги сошлись (средняя фаза). +// Второй кадр ходьбы: ноги сошлись (passing), туловище на пиксель ниже — боб походки. const heroDown2 = fromAscii([ '................', + '................', '.....oooooo.....', '....oTTTTTTo....', '...oTttttttTo...', @@ -230,12 +231,11 @@ '....oPo.oPo.....', '....oPPoPPo.....', '.....oPPPo......', - '......ooo.......', - '................' + '......ooo.......' ], HERO_MAP); // Спина: швы капюшона под шляпой, лампа на поясе (единственный тёплый пиксель). -const heroUp1 = fromAscii([ +const heroUpRows = [ '................', '.....oooooo.....', '....oTTTTTTo....', @@ -260,9 +260,12 @@ '...oPPo..oPPo...', '....oo....oo....', '................' -], { ...HERO_MAP, f: 'F0' }); +]; -const heroUp2 = heroUp1; // для спины ходьба читается слабо — оставляем один кадр +const heroUp1 = fromAscii(heroUpRows, { ...HERO_MAP, f: 'F0' }); + +// Второй кадр спины: туловище на пиксель ниже — боб походки. +const heroUp2 = fromAscii(['................', ...heroUpRows.slice(0, -1)], { ...HERO_MAP, f: 'F0' }); const heroSide1 = fromAscii([ '................', @@ -291,9 +294,10 @@ '................' ], HERO_MAP); -// Шаг в бок: ноги шире (фаза strides). +// Шаг в бок: ноги шире (stride), туловище на пиксель ниже — боб походки. const heroSide2 = fromAscii([ '................', + '................', '.....oooooo.....', '....oTTTTTTo....', '...oTttttttTo...', @@ -315,8 +319,7 @@ '...oPo...oPo....', '..oPPo...oPPo...', '..oPPo...oPPo...', - '...oo.....oo....', - '................' + '...oo.....oo....' ], HERO_MAP); const elderIrwin = fromAscii([ diff --git a/tools/walk-probe.mjs b/tools/walk-probe.mjs new file mode 100644 index 0000000..8ec0158 --- /dev/null +++ b/tools/walk-probe.mjs @@ -0,0 +1,52 @@ +/** + * Проба анимации ходьбы героя: запускаем игру, отправляем героя на запад, + * снимаем серию скриншотов во время движения. + * Запуск: node tools/walk-probe.mjs [url] [префикс] + */ +import puppeteer from 'puppeteer-core'; + +const url = process.argv[2] ?? 'http://localhost:5201/'; +const prefix = process.argv[3] ?? '/tmp/walk'; + +const browser = await puppeteer.launch({ + executablePath: '/usr/bin/chromium', + headless: true, + args: ['--no-sandbox', '--enable-unsafe-swiftshader', '--use-angle=swiftshader', + '--autoplay-policy=no-user-gesture-required', '--window-size=960,540'] +}); +const page = await browser.newPage(); +await page.setViewport({ width: 960, height: 540 }); +page.on('pageerror', (err) => console.log(`[ошибка страницы] ${err.message}`)); + +const booted = new Promise((resolve) => { + page.on('console', (msg) => { + if (msg.text().includes('[location]')) resolve(); + }); +}); + +await page.goto(url, { waitUntil: 'networkidle2', timeout: 20000 }); +await new Promise((r) => setTimeout(r, 2000)); +await page.mouse.click(480, 283); // «Новая игра» +await booted; +await new Promise((r) => setTimeout(r, 1500)); + +// Клик по мировому тайлу слева от героя (запад) — через window.__game. +await page.evaluate(() => { + const g = window.__game; + const sc = g.scenes.current; + const t = sc.player.currentTile(); + const p = sc.tileCenter(t.x - 6, t.y); + const canvas = document.querySelector('canvas'); + const rect = canvas.getBoundingClientRect(); + const wr = g.renderer.worldRoot.position; + return { x: rect.left + ((p.x + wr.x) / 480) * rect.width, + y: rect.top + ((p.y + wr.y) / 270) * rect.height }; +}).then((pt) => page.mouse.click(pt.x, pt.y)); + +// Серия кадров во время ходьбы +for (let i = 0; i < 6; i++) { + await page.screenshot({ path: `${prefix}_${i}.png` }); + await new Promise((r) => setTimeout(r, 130)); +} +console.log('Готово'); +await browser.close(); \ No newline at end of file