diff --git a/AGENTS.md b/AGENTS.md index edcfc29..2069a01 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,7 @@ - Do not copy secrets from that `.env` file into tracked files, logs, or responses. - For this project, whenever source changes are made, commit the changes and push them to the Gitea remote before finishing the task. - The game targets normal operation on PC/desktop browsers; do not prioritize mobile layout or mobile-specific work. +- All browser, screenshot, and visual QA must explicitly use a 1920x1080 CSS viewport at 100% browser zoom. Never rely on an in-app browser's ambient/default viewport. Additional desktop resolutions are secondary checks and do not replace the 1920x1080 baseline. - 이미지는 KOEI 삼국지 스타일을 활용하되, 실제 저작권 원본 이미지나 로고를 복제하지 않고 프로젝트 고유 자산으로 제작한다. - 캠페인 큰 줄기는 유비의 삼국지 서사를 따른다: 황건적 토벌 → 반동탁 흐름 → 공손찬 의탁 → 도겸에게 서주 인수 → 여포에게 서주 상실 → 조조 의탁 → 조조 이탈 → 원소 의탁 → 유표 의탁 → 제갈량 영입 → 적벽대전 → 형주·익주 확보 → 촉한 건국. - 게임이 진행되며 무장이 추가될수록 출전 전에 함께할 무장을 선택하는 편성 재미가 드러나도록 구현한다. diff --git a/docs/battle-map-art-direction-first-sample.md b/docs/battle-map-art-direction-first-sample.md index 43d8b25..3466872 100644 --- a/docs/battle-map-art-direction-first-sample.md +++ b/docs/battle-map-art-direction-first-sample.md @@ -8,7 +8,7 @@ Raise the first battle map from flat placeholder-like terrain to an original han - Unit sprites must read before the map. - Terrain contrast stays one step below characters, combat UI, and hover/selection feedback. -- Texture detail should add depth without turning into visual noise at PC browser 100% scale. +- Texture detail should add depth without turning into visual noise at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1. - Terrain identity must remain clear: road, forest, hill, river, village, camp, fort. ## Terrain Palette @@ -35,4 +35,4 @@ Raise the first battle map from flat placeholder-like terrain to an original han - First battle lower-left camp and starting road should guide the eye toward the central route. - Central village and river should be readable without stealing focus. - Enemy fort area in the upper-right must imply an enemy line without becoming a large UI-like block. -- At 100% PC browser scale, Liu Bei, Guan Yu, Zhang Fei, and enemy units must remain the strongest silhouettes. +- At an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1, Liu Bei, Guan Yu, Zhang Fei, and enemy units must remain the strongest silhouettes. Follow `docs/desktop-browser-qa-standard.md` for capture verification. diff --git a/docs/battle-map-terrain-atlas-pipeline.md b/docs/battle-map-terrain-atlas-pipeline.md index a13cd9b..98d6c49 100644 --- a/docs/battle-map-terrain-atlas-pipeline.md +++ b/docs/battle-map-terrain-atlas-pipeline.md @@ -12,7 +12,7 @@ The first battle uses `src/assets/images/battle/first-battle-map-v2.webp` as the ## Art Direction -- Units must read before the terrain at 100% desktop browser scale. +- Units must read before the terrain at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1. - Terrain uses muted greens, ochre roads, dark blue-green water, and compact structure markers. - Roads, forests, rivers, camps, villages, cliffs, and forts should be recognizable by silhouette and texture, not by high saturation. - The grid remains subtle and embedded in the paint grade, so it helps targeting without becoming the main visual layer. @@ -314,4 +314,4 @@ python scripts/generate-handpaint-map-sample.py --map thirtieth --tile-size 192 - `docs/sixty-sixth-battle-map-handpaint-before-after.png` - `docs/sixty-sixth-battle-map-handpaint-preview.png` -Browser screenshots are saved per verification run after build/deploy. +Browser screenshots are saved per verification run after build/deploy at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1, following `docs/desktop-browser-qa-standard.md`. Larger 2560-class checks are supplemental and do not replace the FHD evidence. diff --git a/docs/battle-preview-ui-rework-report.md b/docs/battle-preview-ui-rework-report.md index 8ce1c10..61945a6 100644 --- a/docs/battle-preview-ui-rework-report.md +++ b/docs/battle-preview-ui-rework-report.md @@ -11,7 +11,8 @@ ## 로컬 검증 - 빌드: `pnpm build` 성공 -- 브라우저: PC 1280x720 인앱 브라우저, `debugBattleSetup=attack-preview` +- 기존 캡처: PC 1280x720 인앱 브라우저에서 생성된 PNG는 당시 세션의 증적으로 유지하며 FHD 캡처로 재표기하거나 확대하지 않는다. `debugBattleSetup=attack-preview` +- 현재 기준 재검증(2026-07-17): 명시적 1920x1080 CSS viewport, 브라우저 확대 100%, DPR 1에서 공격·책략 예측 화면이 통과했고 콘솔 오류가 없었다. - 공격 예측: `docs/battle-preview-ui-rework-local-combat-preview.png` - 책략 예측: `docs/battle-preview-ui-rework-local-strategy-preview.png` - 콘솔 에러: 없음 diff --git a/docs/battle-ui-icons-hq-batch1-report.md b/docs/battle-ui-icons-hq-batch1-report.md index b83463f..1aa8c25 100644 --- a/docs/battle-ui-icons-hq-batch1-report.md +++ b/docs/battle-ui-icons-hq-batch1-report.md @@ -52,8 +52,9 @@ The atlas remains a 128px-frame spritesheet with the existing 5x6 layout. New al - Build: `pnpm build` passed. - Local browser: `http://127.0.0.1:4190/heros_web/?debug&debugBattleSetup=attack-preview&v=hq-icons-batch1-local` -- Checked unit detail, strategy menu, command menu, and combat preview at desktop 1280x720. -- Console errors: none observed during local browser check. +- Historical capture note: the linked local and deployed PNGs preserve the original desktop 1280x720 session and have not been relabeled or upscaled. +- Current-standard revalidation (2026-07-17): unit detail, strategy menu, command menu, and combat preview passed at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1. +- Console errors: none observed during the historical capture or current-standard revalidation. - Deploy: `pnpm deploy:nas` passed and deployed to `/volume1/web/heros_web`. - Deployed browser: `https://comtropy.synology.me/heros_web/?debug&debugBattleSetup=attack-preview&v=hq-icons-batch1-prod` - Deployed console errors: none observed during combat preview check. diff --git a/docs/battle-ui-icons-v2-action-item-report.md b/docs/battle-ui-icons-v2-action-item-report.md index 6177df0..5ee2be9 100644 --- a/docs/battle-ui-icons-v2-action-item-report.md +++ b/docs/battle-ui-icons-v2-action-item-report.md @@ -54,6 +54,6 @@ The existing 5x6 atlas contract is preserved. Pixel comparison against the previ - `pnpm run verify:static-data` passed. - `pnpm run build` passed. - `pnpm run verify:release` passed, including new-game, reload/continue, defeat retry, victory, and camp-return flows. -- Desktop browser checks passed at 1280x720 for the command menu, battle item menu, target selection, combat preview, sortie supply assignment, camp supply rows, and merchant rows. +- Desktop browser checks passed on 2026-07-17 at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1 for the command menu, battle item menu, target selection, combat preview, sortie supply assignment, camp supply rows, and merchant rows. - Both atlases loaded successfully and no browser console errors were observed. - Independent read-only review found no remaining blockers. diff --git a/docs/desktop-browser-qa-standard.md b/docs/desktop-browser-qa-standard.md new file mode 100644 index 0000000..60657c1 --- /dev/null +++ b/docs/desktop-browser-qa-standard.md @@ -0,0 +1,52 @@ +# Desktop Browser QA Standard + +## Primary Baseline + +All routine desktop browser QA, visual review, screenshots, and release evidence use these explicit settings: + +- CSS viewport: `1920x1080`. +- Browser zoom: `100%`. +- Device pixel ratio: `1`. +- Desktop layout only; a different viewport is permitted only for a separately identified responsive or compatibility test. + +Do not rely on the browser window's default size. Set the CSS viewport before opening the target flow, then confirm the values below before accepting a screenshot or pass result. + +## Required Runtime Checks + +Record or assert the following values for the primary pass: + +- `window.innerWidth === 1920`. +- `window.innerHeight === 1080`. +- `window.devicePixelRatio === 1`. +- The game canvas backing surface is `canvas.width === 1920` and `canvas.height === 1080`. +- At the primary viewport, `canvas.getBoundingClientRect()` is approximately `1920x1080` and the canvas is centered without clipping. +- The active Phaser game size and scene bounds remain `1920x1080`. +- Browser console warning/error count is recorded, with unexpected errors treated as a failed check. + +Capture the URL, renderer mode, checked flow, and date in the report. A report should state `1920x1080 CSS viewport, 100% browser zoom, DPR 1` rather than only saying `desktop` or `PC browser`. + +## Supplemental 2560 Checks + +Use `2560x1440` or `2560x1600` only as supplemental fit, centering, and large-monitor coverage checks. These checks verify that the fixed FHD game surface remains correctly contained and centered; they do not replace the primary `1920x1080` evidence. + +## Legacy Authored Coordinates + +Battle and camp UI still contain an authored legacy `1280x720` coordinate system that is scaled by `1.5` onto the FHD game surface. Constants and comments such as `battleReferenceWidth`, `battleReferenceHeight`, `campLegacyCanvasWidth`, `campLegacyCanvasHeight`, and legacy click-coordinate scaling describe this internal layout basis. + +Do not change those values to `1920x1080` as part of viewport QA standardization. Doing so would double-scale or displace existing UI. Browser viewport settings and authored scene coordinates are separate concerns. + +## Historical Evidence + +Existing `1280x720` screenshots remain valid evidence of the session in which they were captured. Do not relabel, resize, or present them as FHD captures. When a historical report needs current validation: + +1. Keep the original screenshot and identify it as historical evidence. +2. Run a separate current-standard pass at `1920x1080` CSS viewport, `100%` browser zoom, and DPR `1`. +3. Record the new date, checked surfaces, and console result without rewriting what the old image represents. + +This policy applies to local, deployed, Canvas, and WebGL browser checks. + +## Automation Contract + +The shared automation source is `scripts/desktop-browser-viewport.mjs`. Browser-based QA scripts must use its context options rather than embedding another primary viewport. + +Run `pnpm run verify:desktop-viewport` to confirm the FHD viewport, DPR 1 context, Phaser game size, project policy, and browser-check imports. This gate is also included in `pnpm run verify:static-data` so an omitted or changed baseline fails routine verification. diff --git a/docs/equipment-icon-accessory-pass.md b/docs/equipment-icon-accessory-pass.md index 3b3c15d..e1fd294 100644 --- a/docs/equipment-icon-accessory-pass.md +++ b/docs/equipment-icon-accessory-pass.md @@ -29,6 +29,7 @@ Generated after the accessory icon rework in `src/game/scenes/BootScene.ts`. ![Sortie prep accessory icon QA](./equipment-icon-accessory-sortie-prep.png) - URL checked: `http://127.0.0.1:4200/heros_web/?debug&debugSortiePrep&renderer=canvas` -- Result: sortie prep opened on a desktop 1280x720 canvas and rendered the generated accessory textures in roster, slot, and detail surfaces. -- Browser console warnings/errors: none observed. +- Historical capture result: the PNG above preserves the original desktop 1280x720 session and has not been relabeled or upscaled. +- Current-standard revalidation (2026-07-17): sortie prep passed at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1, with the generated accessory textures visible in the roster, slot, and detail surfaces. +- Browser console warnings/errors: none observed during the historical capture or current-standard revalidation. - Follow-up target from the updated audit: remaining low-detail equipment icons. diff --git a/docs/equipment-icon-armor-pass.md b/docs/equipment-icon-armor-pass.md index 333efa8..e746275 100644 --- a/docs/equipment-icon-armor-pass.md +++ b/docs/equipment-icon-armor-pass.md @@ -25,6 +25,7 @@ Generated after the armor icon rework in `src/game/scenes/BootScene.ts`. ![Sortie prep armor icon QA](./equipment-icon-armor-sortie-prep.png) - URL checked: `http://127.0.0.1:4198/heros_web/?debug&debugSortiePrep&renderer=canvas` -- Result: sortie prep opened on a desktop 1280x720 canvas and rendered the generated armor textures in roster, slot, and detail surfaces. -- Browser console warnings/errors: none observed. +- Historical capture result: the PNG above preserves the original desktop 1280x720 session and has not been relabeled or upscaled. +- Current-standard revalidation (2026-07-17): sortie prep passed at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1, with the generated armor textures visible in the roster, slot, and detail surfaces. +- Browser console warnings/errors: none observed during the historical capture or current-standard revalidation. - Follow-up target from the updated audit: accessory charms and tokens. diff --git a/docs/equipment-icon-quality-audit.md b/docs/equipment-icon-quality-audit.md index dcf2bd5..daaf773 100644 --- a/docs/equipment-icon-quality-audit.md +++ b/docs/equipment-icon-quality-audit.md @@ -11,8 +11,9 @@ Generated by `scripts/audit-equipment-icon-quality.mjs`. - Texture size: 64px - Drawing grid: 28 units scaled 2.29x into the texture - Runtime item icon scene: `src/game/scenes/CampScene.ts` -- Observed runtime display sizes: `14px`, `20px`, `21px`, `22px`, `30px` +- Observed runtime display sizes: `14px` - Direct item texture render sites found: 5 +- Browser visual QA baseline: `1920x1080` CSS viewport at 100% zoom ## Main Findings diff --git a/docs/equipment-icon-remaining-weapons-pass.md b/docs/equipment-icon-remaining-weapons-pass.md index 85edab6..d9179c0 100644 --- a/docs/equipment-icon-remaining-weapons-pass.md +++ b/docs/equipment-icon-remaining-weapons-pass.md @@ -21,6 +21,7 @@ Generated after the final low-detail weapon icon rework in `src/game/scenes/Boot ![Sortie prep remaining weapon icon QA](./equipment-icon-remaining-weapons-sortie-prep.png) - URL checked: `http://127.0.0.1:4201/heros_web/?debug&debugSortiePrep&renderer=canvas` -- Result: sortie prep opened on a desktop 1280x720 canvas and rendered the generated weapon textures in roster, slot, and detail surfaces. -- Browser console warnings/errors: none observed. +- Historical capture result: the PNG above preserves the original desktop 1280x720 session and has not been relabeled or upscaled. +- Current-standard revalidation (2026-07-17): sortie prep passed at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1, with the generated weapon textures visible in the roster, slot, and detail surfaces. +- Browser console warnings/errors: none observed during the historical capture or current-standard revalidation. - Follow-up target from the updated audit: no generated equipment icon batch is currently flagged; select the next art pass from runtime screenshots. diff --git a/docs/equipment-icon-spear-polearm-pass.md b/docs/equipment-icon-spear-polearm-pass.md index 82d6773..f5e22fe 100644 --- a/docs/equipment-icon-spear-polearm-pass.md +++ b/docs/equipment-icon-spear-polearm-pass.md @@ -25,5 +25,6 @@ Generated after the spear/polearm icon rework in `src/game/scenes/BootScene.ts`. ![Sortie prep equipment icon QA](./equipment-icon-spear-polearm-sortie-prep.png) - URL checked: `http://127.0.0.1:4197/heros_web/?debug&debugSortiePrep&renderer=canvas` -- Result: sortie prep opened on a desktop 1280x720 canvas and rendered the generated equipment textures in the roster, slot, and detail surfaces. +- Historical capture result: the PNG above preserves the original desktop 1280x720 session and has not been relabeled or upscaled. +- Current-standard revalidation (2026-07-17): sortie prep passed at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1, with the generated equipment textures visible in the roster, slot, and detail surfaces and no browser console warnings or errors. - Follow-up target from the updated audit: armor silhouettes. diff --git a/docs/result-feedback-report.md b/docs/result-feedback-report.md index 16380ed..7b3a207 100644 --- a/docs/result-feedback-report.md +++ b/docs/result-feedback-report.md @@ -14,8 +14,9 @@ Date: 2026-07-03 ## Browser Checks - Local: `http://127.0.0.1:4195/heros_web/?debug&debugBattleSetup=attack-preview&v=result-feedback-local-card` - Deployed: `https://comtropy.synology.me/heros_web/?debug&debugBattleSetup=attack-preview&v=result-feedback-20260703` -- PC viewport: 1280x720 -- Console errors: 0 local, 0 deployed +- Historical capture viewport: 1280x720. The six PNGs below preserve the original local and deployed sessions and have not been relabeled or upscaled. +- Current-standard revalidation (2026-07-17): the equivalent current-code target preview, map popup, and post-attack card flows passed locally at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1. The older deployed PNGs remain historical evidence rather than current FHD captures. +- Console errors: 0 during the historical local/deployed captures and current-standard revalidation. ## Captures - Local target preview: `docs/result-feedback-local-target-preview-card.png` diff --git a/docs/strategist-character-direction.md b/docs/strategist-character-direction.md index ae22848..1e6e7d6 100644 --- a/docs/strategist-character-direction.md +++ b/docs/strategist-character-direction.md @@ -47,4 +47,4 @@ Completed representative pass: - `unit-sima-yi`: strengthened from generic dark strategist to named Wei court conspirator with tall black court crown, black lacquer command tablet, sealed military order, dark decree scroll, bronze tally baton, and narrow command rod. - `unit-huang-quan`: strengthened from generic quartermaster official to Shu logistics quartermaster with supply ledger, ration register, supply-route map board, bamboo tally slips, bronze counting rods, sealed requisition scroll, and wooden document case. -Every representative fix should include contact sheet, animation GIF, battle preview, before/after, alpha audit, build, and browser check. +Every representative fix should include contact sheet, animation GIF, battle preview, before/after, alpha audit, build, and a browser check at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1, following `docs/desktop-browser-qa-standard.md`. diff --git a/docs/tactical-unit-sprite-art-direction.md b/docs/tactical-unit-sprite-art-direction.md index c9ad817..3681d9a 100644 --- a/docs/tactical-unit-sprite-art-direction.md +++ b/docs/tactical-unit-sprite-art-direction.md @@ -47,7 +47,7 @@ Only Liu Bei may be produced in this sample phase. Do not expand to Guan Yu, Zha Required sample outputs: - 313x313 contact sheet for Liu Bei. - Animated GIF or browser capture for idle/walk/action. -- 100% desktop browser screenshot from the battle map. +- Battle-map browser screenshot captured at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1, following `docs/desktop-browser-qa-standard.md`. - Before/after comparison against the current checked-in Liu Bei sprite. No deployment, commit, or push is allowed before sample approval. diff --git a/docs/unit-sprite-batch-output-checklist.md b/docs/unit-sprite-batch-output-checklist.md index 102a50a..41f7f3c 100644 --- a/docs/unit-sprite-batch-output-checklist.md +++ b/docs/unit-sprite-batch-output-checklist.md @@ -62,8 +62,8 @@ Review state manifest: `unit-sprite-rework-manifest.json`. - `python scripts/check-unit-sprite-workflow-consistency.py` - Review the relevant `docs/unit-sprite-review-.png` by eye. - `pnpm build` -- Local PC browser check. -- For pending samples, local PC browser check with `?debug&debugSpritePreview=`. +- Local PC browser check at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1, following `docs/desktop-browser-qa-standard.md`. +- For pending samples, run the same 1920x1080 CSS viewport, 100% zoom, DPR 1 check with `?debug&debugSpritePreview=`. - Console errors: `0`. - Approval received for the batch before commit/push/deploy. @@ -76,7 +76,7 @@ Do not commit, push, or deploy when: - Any newly edited unit is still marked `pending-approval`. - Any untouched unit is accidentally counted as approved because it is only technically `ok`. - The review board shows the new unit is not visibly better than the old placeholder. -- The browser check has not been run. +- The browser check has not been run at the required 1920x1080 CSS viewport, 100% browser zoom, and DPR 1. - Console errors are present. After approval and successful verification: diff --git a/docs/visual-asset-sixty-fourth-map-runtime-qa.md b/docs/visual-asset-sixty-fourth-map-runtime-qa.md index 5a97b68..4d2687a 100644 --- a/docs/visual-asset-sixty-fourth-map-runtime-qa.md +++ b/docs/visual-asset-sixty-fourth-map-runtime-qa.md @@ -5,7 +5,8 @@ - Map key: `battle-map-sixty-fourth` - Source asset: `src/assets/images/battle/sixty-fourth-battle-map.webp` - Audit trigger: lowest battle-map source density bucket, `128x110` grid at `112.0` source pixels per tile -- Browser target: local Vite, `1280x720`, `http://127.0.0.1:4202/heros_web/?debug&debugBattle=sixty-fourth-battle-weishui-camps&v=visual-qa-sixty-fourth` +- Historical browser target: local Vite at `1280x720`, `http://127.0.0.1:4202/heros_web/?debug&debugBattle=sixty-fourth-battle-weishui-camps&v=visual-qa-sixty-fourth`. The three linked PNGs preserve that original session and have not been relabeled or upscaled. +- Current-standard revalidation target (2026-07-17): the same battle flow at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1. ## Captures @@ -35,6 +36,7 @@ Recommended repaint constraints: ## Verification -- Local runtime: passed at `1280x720`. +- Historical local runtime: passed at `1280x720`; the three saved screenshots remain historical evidence. +- Current-standard revalidation (2026-07-17): passed at an explicit 1920x1080 CSS viewport, 100% browser zoom, and DPR 1 for deployment, battle start, tactical readability, and minimap rendering. - Screenshots saved: 3. -- Browser warning/error logs during runtime pass: none. +- Browser warning/error logs during the historical runtime pass or current-standard revalidation: none. diff --git a/package.json b/package.json index 4a72646..bc9b41f 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "verify:unit-sprites": "node scripts/verify-unit-sprite-asset-data.mjs", "verify:battle-map-assets": "node scripts/verify-battle-map-asset-data.mjs", "verify:audio-assets": "node scripts/verify-audio-asset-data.mjs", + "verify:desktop-viewport": "node scripts/verify-desktop-browser-viewport.mjs", "verify:static-data": "node scripts/verify-static-data.mjs", "verify:flow": "node scripts/verify-flow.mjs", "verify:save-flow": "node scripts/verify-save-retry-flow.mjs", diff --git a/scripts/audit-equipment-icon-quality.mjs b/scripts/audit-equipment-icon-quality.mjs index a45d6e1..5be2115 100644 --- a/scripts/audit-equipment-icon-quality.mjs +++ b/scripts/audit-equipment-icon-quality.mjs @@ -1,5 +1,6 @@ import { readFileSync, writeFileSync } from 'node:fs'; import { createServer } from 'vite'; +import { desktopBrowserViewportLabel } from './desktop-browser-viewport.mjs'; const bootScenePath = 'src/game/scenes/BootScene.ts'; const campScenePath = 'src/game/scenes/CampScene.ts'; @@ -428,6 +429,7 @@ function renderReport({ iconSourceSize, iconTextureSize, itemRows, groupedRows, `- Runtime item icon scene: \`${sceneUsage.scene}\``, `- Observed runtime display sizes: ${sceneUsage.displaySizes.map((size) => `\`${size}px\``).join(', ') || 'none found'}`, `- Direct item texture render sites found: ${sceneUsage.directItemImageCount}`, + `- Browser visual QA baseline: \`${desktopBrowserViewportLabel}\` CSS viewport at 100% zoom`, '', '## Main Findings', '', @@ -499,7 +501,7 @@ function renderRecommendedNextBatch(recommendations) { `- Item ids: ${formatItemIds(recommendations.firstTargetIds)}`, `- Success criteria: ${recommendations.firstTargetSuccessCriteria}`, '- Keep the existing 64px texture size unless a UI-wide equipment icon pass is planned.', - '- Add before/after contact sheets and verify the sortie equipment list and equipment swap panel in a desktop browser.', + `- Add before/after contact sheets and verify the sortie equipment list and equipment swap panel in a desktop browser at the ${desktopBrowserViewportLabel} CSS viewport and 100% zoom.`, '' ]; } diff --git a/scripts/audit-visual-asset-quality.mjs b/scripts/audit-visual-asset-quality.mjs index f3f11b3..a4ae3b6 100644 --- a/scripts/audit-visual-asset-quality.mjs +++ b/scripts/audit-visual-asset-quality.mjs @@ -1,12 +1,13 @@ import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'; import { join, sep } from 'node:path'; import { createServer } from 'vite'; +import { desktopBrowserViewport } from './desktop-browser-viewport.mjs'; const battleMapDir = join('src', 'assets', 'images', 'battle'); const storyDir = join('src', 'assets', 'images', 'story'); const titleImagePath = join('src', 'assets', 'images', 'taoyuan-oath-title.png'); const reportPath = join('docs', 'visual-asset-quality-audit.md'); -const targetViewport = { width: 1920, height: 1080 }; +const targetViewport = desktopBrowserViewport; const expectedStorySize = { width: 1672, height: 941 }; const server = await createServer({ diff --git a/scripts/desktop-browser-viewport.mjs b/scripts/desktop-browser-viewport.mjs new file mode 100644 index 0000000..96064ac --- /dev/null +++ b/scripts/desktop-browser-viewport.mjs @@ -0,0 +1,19 @@ +export const desktopBrowserViewport = Object.freeze({ + width: 1920, + height: 1080 +}); + +export const desktopBrowserDeviceScaleFactor = 1; + +export const desktopBrowserContextOptions = Object.freeze({ + viewport: desktopBrowserViewport, + deviceScaleFactor: desktopBrowserDeviceScaleFactor +}); + +export const desktopBrowserViewportBounds = Object.freeze({ + x: 0, + y: 0, + ...desktopBrowserViewport +}); + +export const desktopBrowserViewportLabel = `${desktopBrowserViewport.width}x${desktopBrowserViewport.height}`; diff --git a/scripts/measure-performance.mjs b/scripts/measure-performance.mjs index 8fadb24..be88c5a 100644 --- a/scripts/measure-performance.mjs +++ b/scripts/measure-performance.mjs @@ -2,10 +2,11 @@ import { spawn } from 'node:child_process'; import { existsSync, readdirSync, statSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { chromium } from 'playwright'; +import { desktopBrowserContextOptions, desktopBrowserViewport } from './desktop-browser-viewport.mjs'; const targetUrl = withDebugParam(process.env.MEASURE_URL ?? 'http://127.0.0.1:4173/'); const outputPath = process.env.MEASURE_OUT ?? 'dist/performance-report.json'; -const baselineViewport = { width: 1920, height: 1080 }; +const baselineViewport = desktopBrowserViewport; const legacyUiScale = 1.5; const budgets = { titleReadyMs: readBudget('PERF_BUDGET_TITLE_READY_MS', 30000), @@ -50,7 +51,7 @@ let browser; try { serverProcess = await ensurePreviewServer(targetUrl); browser = await chromium.launch({ headless: true }); - const context = await browser.newContext({ viewport: baselineViewport }); + const context = await browser.newContext(desktopBrowserContextOptions); await context.addInitScript(() => window.localStorage.clear()); const page = await context.newPage(); @@ -90,6 +91,8 @@ try { const report = { targetUrl, measuredAt: new Date().toISOString(), + viewport: { ...baselineViewport }, + deviceScaleFactor: desktopBrowserContextOptions.deviceScaleFactor, timingsMs: { titleReady: titleMs, storyReadyFromNewGame: storyMs, diff --git a/scripts/qa-representative-battles.mjs b/scripts/qa-representative-battles.mjs index 0d2db8b..e674626 100644 --- a/scripts/qa-representative-battles.mjs +++ b/scripts/qa-representative-battles.mjs @@ -3,6 +3,7 @@ import { existsSync } from 'node:fs'; import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { dirname } from 'node:path'; import { chromium } from 'playwright'; +import { desktopBrowserContextOptions, desktopBrowserViewport } from './desktop-browser-viewport.mjs'; const cliOptions = parseCliOptions(process.argv.slice(2)); const defaultQaPort = process.env.QA_PORT ?? '41737'; @@ -16,7 +17,7 @@ const campaignStorageKey = 'heros-web:campaign-state'; const campaignSlotStorageKey = 'heros-web:campaign-state:slot-1'; const campaignSnapshotPath = process.env.QA_CAMPAIGN_SNAPSHOT_PATH; const resumeCampaignSnapshot = process.env.QA_RESUME_CAMPAIGN_SNAPSHOT === '1'; -const baselineViewport = { width: 1920, height: 1080 }; +const baselineViewport = desktopBrowserViewport; function withRenderer(url, renderer) { if (!['canvas', 'webgl'].includes(renderer)) { @@ -1181,7 +1182,7 @@ try { const browser = await chromium.launch({ headless }); try { - const page = await browser.newPage({ viewport: baselineViewport }); + const page = await browser.newPage(desktopBrowserContextOptions); const results = []; for (const battle of representativeBattles) { @@ -3186,6 +3187,8 @@ function buildQaReport(results, failed) { generatedAt: new Date().toISOString(), set: qaSetName, targetUrl, + viewport: { ...baselineViewport }, + deviceScaleFactor: desktopBrowserContextOptions.deviceScaleFactor, headless, maxRounds, cumulativeMode, diff --git a/scripts/verify-desktop-browser-viewport.mjs b/scripts/verify-desktop-browser-viewport.mjs new file mode 100644 index 0000000..be3b368 --- /dev/null +++ b/scripts/verify-desktop-browser-viewport.mjs @@ -0,0 +1,71 @@ +import { readdirSync, readFileSync } from 'node:fs'; +import { + desktopBrowserContextOptions, + desktopBrowserDeviceScaleFactor, + desktopBrowserViewport, + desktopBrowserViewportBounds, + desktopBrowserViewportLabel +} from './desktop-browser-viewport.mjs'; + +const expectedViewport = { width: 1920, height: 1080 }; +const expectedBounds = { x: 0, y: 0, ...expectedViewport }; +const errors = []; + +assertJsonEqual(desktopBrowserViewport, expectedViewport, 'central desktop browser viewport'); +assertJsonEqual(desktopBrowserViewportBounds, expectedBounds, 'central desktop browser viewport bounds'); +assertJsonEqual( + desktopBrowserContextOptions, + { viewport: expectedViewport, deviceScaleFactor: 1 }, + 'central desktop browser context options' +); +assert(desktopBrowserDeviceScaleFactor === 1, 'desktop browser device scale factor must be 1'); +assert(desktopBrowserViewportLabel === '1920x1080', 'desktop browser viewport label must be 1920x1080'); + +const mainSource = readFileSync('src/main.ts', 'utf8'); +assert(/width:\s*1920\s*,[\s\S]*?height:\s*1080\s*,/.test(mainSource), 'Phaser game config must remain 1920x1080'); + +const policySource = readFileSync('AGENTS.md', 'utf8'); +assert(policySource.includes('1920x1080 CSS viewport at 100% browser zoom'), 'AGENTS.md must define the 1920x1080 browser QA baseline'); +assert(policySource.includes("Never rely on an in-app browser's ambient/default viewport"), 'AGENTS.md must prohibit ambient browser viewport use'); + +const automatedBrowserChecks = readdirSync('scripts', { withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name.endsWith('.mjs')) + .map((entry) => `scripts/${entry.name}`) + .filter((scriptPath) => { + const source = readFileSync(scriptPath, 'utf8'); + return source.includes("from 'playwright'") && /\b(?:newPage|newContext)\s*\(/.test(source); + }) + .sort(); + +assert(automatedBrowserChecks.length > 0, 'at least one automated browser check must be discovered'); + +for (const scriptPath of automatedBrowserChecks) { + const source = readFileSync(scriptPath, 'utf8'); + assert(source.includes("from './desktop-browser-viewport.mjs'"), `${scriptPath} must import the shared browser viewport`); + assert( + /\b(?:newPage|newContext)\(desktopBrowserContextOptions\)/.test(source), + `${scriptPath} must explicitly create its primary browser page or context with the shared FHD context options` + ); + assert( + !/viewport:\s*\{\s*width:\s*1920\s*,\s*height:\s*1080\s*\}/.test(source), + `${scriptPath} must not duplicate the primary 1920x1080 viewport inline` + ); +} + +if (errors.length > 0) { + throw new Error(`Desktop browser viewport verification failed:\n- ${errors.join('\n- ')}`); +} + +console.log( + `Verified ${desktopBrowserViewportLabel} desktop browser baseline at DPR ${desktopBrowserDeviceScaleFactor} across ${automatedBrowserChecks.length} browser checks.` +); + +function assert(condition, message) { + if (!condition) { + errors.push(message); + } +} + +function assertJsonEqual(actual, expected, label) { + assert(JSON.stringify(actual) === JSON.stringify(expected), `${label} must be ${JSON.stringify(expected)}; received ${JSON.stringify(actual)}`); +} diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index 443242a..6984163 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -1,10 +1,10 @@ import { spawn } from 'node:child_process'; import { readFileSync } from 'node:fs'; import { chromium } from 'playwright'; +import { desktopBrowserContextOptions } from './desktop-browser-viewport.mjs'; const targetUrl = withCanvasRenderer(process.env.VERIFY_URL ?? 'http://localhost:5173/'); const rewardDataFiles = ['src/game/data/battles.ts', 'src/game/scenes/CampScene.ts']; -const baselineViewport = { width: 1920, height: 1080 }; const legacyUiScale = 1.5; const legacyUiClickPoints = new Set([ '962,240', @@ -238,7 +238,8 @@ async function moveBattlePointerToRightEdge(page) { function installLegacyUiClickScaling(page) { const click = page.mouse.click.bind(page.mouse); page.mouse.click = (x, y, options) => { - // Only authored 1280x720 controls are scaled; runtime map/minimap coordinates pass through unchanged. + // Only controls authored in the legacy reference coordinate system are scaled 1.5x to FHD; + // runtime map/minimap coordinates pass through unchanged. const scale = legacyUiClickPoints.has(`${x},${y}`) ? legacyUiScale : 1; return click(x * scale, y * scale, options); }; @@ -253,7 +254,7 @@ try { serverProcess = await ensureLocalServer(targetUrl); browser = await chromium.launch({ headless: true }); - const page = await browser.newPage({ viewport: baselineViewport }); + const page = await browser.newPage(desktopBrowserContextOptions); installLegacyUiClickScaling(page); await page.goto(targetUrl, { waitUntil: 'domcontentloaded' }); diff --git a/scripts/verify-public-deploy.mjs b/scripts/verify-public-deploy.mjs index a5fc1b1..f2b3143 100644 --- a/scripts/verify-public-deploy.mjs +++ b/scripts/verify-public-deploy.mjs @@ -2,6 +2,7 @@ import { mkdirSync } from 'node:fs'; import { execFileSync } from 'node:child_process'; import { dirname } from 'node:path'; import { chromium } from 'playwright'; +import { desktopBrowserContextOptions } from './desktop-browser-viewport.mjs'; const targetUrl = withQaParams(process.env.PUBLIC_QA_URL ?? 'https://comtropy.synology.me/heros_web/'); const screenshotPath = process.env.PUBLIC_QA_SCREENSHOT ?? 'dist/public-deploy-qa.png'; @@ -15,7 +16,7 @@ mkdirSync(dirname(screenshotPath), { recursive: true }); const browser = await chromium.launch({ headless: true }); try { - const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } }); + const page = await browser.newPage(desktopBrowserContextOptions); const consoleMessages = []; const pageErrors = []; const requestFailures = []; diff --git a/scripts/verify-release-candidate.mjs b/scripts/verify-release-candidate.mjs index 2d5e283..aa0c8a8 100644 --- a/scripts/verify-release-candidate.mjs +++ b/scripts/verify-release-candidate.mjs @@ -1,10 +1,16 @@ import { spawn, spawnSync } from 'node:child_process'; import { mkdirSync, writeFileSync } from 'node:fs'; import { chromium } from 'playwright'; +import { + desktopBrowserContextOptions, + desktopBrowserDeviceScaleFactor, + desktopBrowserViewport, + desktopBrowserViewportBounds +} from './desktop-browser-viewport.mjs'; const targetUrl = withDebugParam(process.env.RELEASE_QA_URL ?? 'http://127.0.0.1:4173/'); const screenshotDir = 'dist'; -const baselineViewport = { width: 1920, height: 1080 }; +const baselineViewport = desktopBrowserViewport; const phaserWebglRendererType = 2; const legacyUiScale = 1.5; const expectedTitle = '\uC0BC\uAD6D\uC9C0: \uC138 \uD615\uC81C\uC758 \uB9F9\uC138'; @@ -49,7 +55,7 @@ try { runCommand(process.execPath, ['scripts/verify-save-retry-flow.mjs'], { VERIFY_URL: targetUrl }); browser = await chromium.launch({ headless: true }); - const page = await browser.newPage({ viewport: baselineViewport }); + const page = await browser.newPage(desktopBrowserContextOptions); const consoleMessages = []; const pageErrors = []; const requestFailures = []; @@ -2710,7 +2716,7 @@ try { } ]; const firstSortiePursuit = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()?.sortiePursuitPreview); - const logicalViewportBounds = { x: 0, y: 0, width: 1920, height: 1080 }; + const logicalViewportBounds = desktopBrowserViewportBounds; assert( sameJsonValue(firstSortiePursuit?.activePairs, expectedFirstSortiePursuitPairs) && firstSortiePursuit?.selectableOpportunities?.length === 0 && @@ -5268,7 +5274,7 @@ try { const midEvaluationSave = await readCampaignSave(page); const mobileResultCard = midEvaluationProbe.evaluation?.presetCards?.find((card) => card.id === 'mobile'); const openedRecommendationFeedback = midEvaluationProbe.evaluation?.recommendationFeedback; - const fhdViewportBounds = { x: 0, y: 0, width: 1920, height: 1080 }; + const fhdViewportBounds = desktopBrowserViewportBounds; assert( midEvaluationProbe.texts.includes('이번 편성 평가') && midEvaluationProbe.texts.some((text) => text.includes('추천안 검증') && text.includes(midRecommendationLaunchFixture.selection.label)) && @@ -5767,8 +5773,8 @@ async function assertReleaseShellReady(page) { async function assertHdPlusCanvasFit(page) { const cases = [ { - viewport: { width: 1920, height: 1080 }, - expectedBounds: { x: 0, y: 0, width: 1920, height: 1080 } + viewport: desktopBrowserViewport, + expectedBounds: desktopBrowserViewportBounds }, { viewport: { width: 2560, height: 1440 }, @@ -5850,7 +5856,7 @@ function boundsNear(actual, expected, tolerance = 1) { } async function assertHdPlusInputMapping(browser, url) { - const page = await browser.newPage({ viewport: { width: 2560, height: 1600 } }); + const page = await browser.newPage({ viewport: { width: 2560, height: 1600 }, deviceScaleFactor: desktopBrowserDeviceScaleFactor }); try { await page.goto(url, { waitUntil: 'domcontentloaded' }); await page.evaluate(() => window.localStorage.clear()); @@ -5896,7 +5902,7 @@ async function assertLargeRosterHud(browser, url) { ]; const battleUrl = new URL(url); battleUrl.searchParams.set('debugBattle', 'sixty-sixth-battle-wuzhang-final'); - const page = await browser.newPage({ viewport: baselineViewport }); + const page = await browser.newPage(desktopBrowserContextOptions); try { await page.goto(battleUrl.toString(), { waitUntil: 'domcontentloaded' }); await page.waitForFunction(() => { @@ -6048,7 +6054,7 @@ async function assertWebglBattleQuickTabSmoke(browser, url, battleId) { const battleUrl = new URL(url); battleUrl.searchParams.set('renderer', 'webgl'); battleUrl.searchParams.set('debugBattle', battleId); - const page = await browser.newPage({ viewport: baselineViewport }); + const page = await browser.newPage(desktopBrowserContextOptions); const pageErrors = []; page.on('pageerror', (error) => pageErrors.push(error.message)); @@ -7786,7 +7792,7 @@ function assertCampRosterPageLayout(roster, expectedPage, pageCount, expectedSel const listBounds = roster.listBounds; assert(isPositiveBounds(listBounds), `Expected final-camp roster list bounds on page ${expectedPage + 1}: ${JSON.stringify(roster)}`); assert( - isBoundsInside(listBounds, { x: 0, y: 0, width: 1920, height: 1080 }), + isBoundsInside(listBounds, desktopBrowserViewportBounds), `Expected final-camp roster list inside the 1920x1080 canvas: ${JSON.stringify(roster)}` ); assert( @@ -7794,7 +7800,7 @@ function assertCampRosterPageLayout(roster, expectedPage, pageCount, expectedSel isPositiveBounds(roster.navigationBounds?.previous) && isPositiveBounds(roster.navigationBounds?.label) && isPositiveBounds(roster.navigationBounds?.next) && - isBoundsInside(roster.navigationBounds.container, { x: 0, y: 0, width: 1920, height: 1080 }), + isBoundsInside(roster.navigationBounds.container, desktopBrowserViewportBounds), `Expected complete final-camp roster navigation bounds inside the FHD canvas: ${JSON.stringify(roster)}` ); assert( @@ -7810,7 +7816,7 @@ function assertCampRosterPageLayout(roster, expectedPage, pageCount, expectedSel row.unitId === roster.visibleUnitIds[rowIndex] && isPositiveBounds(row) && isBoundsInside(row, listBounds) && - isBoundsInside(row, { x: 0, y: 0, width: 1920, height: 1080 }) && + isBoundsInside(row, desktopBrowserViewportBounds) && row.y + row.height <= roster.navigationBounds.container.y, `Expected final-camp roster row ${rowIndex + 1} to match its visible officer and remain inside the list and FHD canvas: ${JSON.stringify({ row, @@ -8628,7 +8634,7 @@ async function readSortiePresetControlPoint(page, control, presetId) { function assertSortiePortraitRosterPage(view, expectedPage, expectedVisibleUnitIds) { const expectedStart = expectedVisibleUnitIds.length > 0 ? expectedPage * 6 + 1 : 0; const expectedEnd = expectedPage * 6 + expectedVisibleUnitIds.length; - const hdBounds = { x: 0, y: 0, width: 1920, height: 1080 }; + const hdBounds = desktopBrowserViewportBounds; assert( view?.enabled === true && view.total === 23 && diff --git a/scripts/verify-save-retry-flow.mjs b/scripts/verify-save-retry-flow.mjs index ea83aac..f3e3379 100644 --- a/scripts/verify-save-retry-flow.mjs +++ b/scripts/verify-save-retry-flow.mjs @@ -1,8 +1,9 @@ import { spawn } from 'node:child_process'; import { chromium } from 'playwright'; +import { desktopBrowserContextOptions, desktopBrowserViewport } from './desktop-browser-viewport.mjs'; const targetUrl = withCanvasRenderer(process.env.VERIFY_URL ?? 'http://localhost:5173/'); -const baselineViewport = { width: 1920, height: 1080 }; +const baselineViewport = desktopBrowserViewport; const legacyUiScale = 1.5; function withCanvasRenderer(url) { @@ -18,7 +19,7 @@ try { serverProcess = await ensureLocalServer(targetUrl); browser = await chromium.launch({ headless: true }); - const page = await browser.newPage({ viewport: baselineViewport }); + const page = await browser.newPage(desktopBrowserContextOptions); await page.goto(targetUrl, { waitUntil: 'domcontentloaded' }); await page.evaluate(() => window.localStorage.clear()); diff --git a/scripts/verify-static-data.mjs b/scripts/verify-static-data.mjs index 8ac2f44..191901f 100644 --- a/scripts/verify-static-data.mjs +++ b/scripts/verify-static-data.mjs @@ -1,6 +1,7 @@ import { spawnSync } from 'node:child_process'; const checks = [ + 'scripts/verify-desktop-browser-viewport.mjs', 'scripts/verify-campaign-flow-data.mjs', 'scripts/verify-campaign-save-normalization.mjs', 'scripts/verify-battle-save-normalization.mjs',