diff --git a/mcp_expansions.png b/mcp_expansions.png new file mode 100644 index 00000000..bb045585 Binary files /dev/null and b/mcp_expansions.png differ diff --git a/mcp_prod_after_feature_fix.png b/mcp_prod_after_feature_fix.png new file mode 100644 index 00000000..574d75c6 Binary files /dev/null and b/mcp_prod_after_feature_fix.png differ diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/.last-run.json b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/.last-run.json new file mode 100644 index 00000000..5fca3f84 --- /dev/null +++ b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "failed", + "failedTests": [] +} \ No newline at end of file diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-about-crowdfund-chromium/error-context.md b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-about-crowdfund-chromium/error-context.md deleted file mode 100644 index fac00507..00000000 --- a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-about-crowdfund-chromium/error-context.md +++ /dev/null @@ -1,20 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: all-routes.spec.ts >> route coverage >> about: crowdfund -- Location: e2e/all-routes.spec.ts:135:5 - -# Error details - -``` -TimeoutError: browserType.launch: Timeout 180000ms exceeded. -Call log: - - /Users/natalie/Library/Caches/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --enable-automation --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/pz/c1s02dlj4k3cml6_tzxwg2d40000gn/T/playwright_chromiumdev_profile-KauEN4 --remote-debugging-pipe --no-startup-window - - pid=74057 - -``` \ No newline at end of file diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-about-early-access-chromium/error-context.md b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-about-early-access-chromium/error-context.md deleted file mode 100644 index 8859fc6a..00000000 --- a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-about-early-access-chromium/error-context.md +++ /dev/null @@ -1,20 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: all-routes.spec.ts >> route coverage >> about: early access -- Location: e2e/all-routes.spec.ts:135:5 - -# Error details - -``` -TimeoutError: browserType.launch: Timeout 180000ms exceeded. -Call log: - - /Users/natalie/Library/Caches/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --enable-automation --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/pz/c1s02dlj4k3cml6_tzxwg2d40000gn/T/playwright_chromiumdev_profile-E2feFR --remote-debugging-pipe --no-startup-window - - pid=73871 - -``` \ No newline at end of file diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-about-early-access-progress-chromium/error-context.md b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-about-early-access-progress-chromium/error-context.md deleted file mode 100644 index 851278c9..00000000 --- a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-about-early-access-progress-chromium/error-context.md +++ /dev/null @@ -1,20 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: all-routes.spec.ts >> route coverage >> about: early access progress -- Location: e2e/all-routes.spec.ts:135:5 - -# Error details - -``` -TimeoutError: browserType.launch: Timeout 180000ms exceeded. -Call log: - - /Users/natalie/Library/Caches/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --enable-automation --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/pz/c1s02dlj4k3cml6_tzxwg2d40000gn/T/playwright_chromiumdev_profile-iCB9Al --remote-debugging-pipe --no-startup-window - - pid=73870 - -``` \ No newline at end of file diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-about-team-chromium/error-context.md b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-about-team-chromium/error-context.md deleted file mode 100644 index 54d5030c..00000000 --- a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-about-team-chromium/error-context.md +++ /dev/null @@ -1,20 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: all-routes.spec.ts >> route coverage >> about: team -- Location: e2e/all-routes.spec.ts:135:5 - -# Error details - -``` -TimeoutError: browserType.launch: Timeout 180000ms exceeded. -Call log: - - /Users/natalie/Library/Caches/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --enable-automation --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/pz/c1s02dlj4k3cml6_tzxwg2d40000gn/T/playwright_chromiumdev_profile-BK9MB8 --remote-debugging-pipe --no-startup-window - - pid=74058 - -``` \ No newline at end of file diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-encyclopedia-index-chromium/error-context.md b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-encyclopedia-index-chromium/error-context.md deleted file mode 100644 index a77ae869..00000000 --- a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-encyclopedia-index-chromium/error-context.md +++ /dev/null @@ -1,20 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: all-routes.spec.ts >> route coverage >> encyclopedia index -- Location: e2e/all-routes.spec.ts:135:5 - -# Error details - -``` -TimeoutError: browserType.launch: Timeout 180000ms exceeded. -Call log: - - /Users/natalie/Library/Caches/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --enable-automation --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/pz/c1s02dlj4k3cml6_tzxwg2d40000gn/T/playwright_chromiumdev_profile-M38ORN --remote-debugging-pipe --no-startup-window - - pid=74059 - -``` \ No newline at end of file diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-episode-age-of-dwarves-chromium/error-context.md b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-episode-age-of-dwarves-chromium/error-context.md deleted file mode 100644 index 3d941f51..00000000 --- a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-episode-age-of-dwarves-chromium/error-context.md +++ /dev/null @@ -1,20 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: all-routes.spec.ts >> route coverage >> episode: age of dwarves -- Location: e2e/all-routes.spec.ts:135:5 - -# Error details - -``` -TimeoutError: browserType.launch: Timeout 180000ms exceeded. -Call log: - - /Users/natalie/Library/Caches/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --enable-automation --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/pz/c1s02dlj4k3cml6_tzxwg2d40000gn/T/playwright_chromiumdev_profile-Alczr9 --remote-debugging-pipe --no-startup-window - - pid=73868 - -``` \ No newline at end of file diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-home-chromium/error-context.md b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-home-chromium/error-context.md deleted file mode 100644 index 8b87bb84..00000000 --- a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-home-chromium/error-context.md +++ /dev/null @@ -1,162 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: all-routes.spec.ts >> route coverage >> home -- Location: e2e/all-routes.spec.ts:135:5 - -# Error details - -``` -TimeoutError: page.goto: Timeout 10000ms exceeded. -Call log: - - navigating to "http://localhost:5802/?skip=welcome", waiting until "networkidle" - -``` - -# Test source - -```ts - 38 | { path: '/intro/expansions', timeoutMs: 10_000, label: 'intro: expansions' }, - 39 | { path: '/intro/tools', timeoutMs: 10_000, label: 'intro: tools' }, - 40 | { path: '/episodes/age-of-dwarves', timeoutMs: 10_000, label: 'episode: age of dwarves' }, - 41 | { path: '/about/early-access', timeoutMs: 10_000, label: 'about: early access' }, - 42 | { path: '/about/early-access/progress', timeoutMs: 10_000, label: 'about: early access progress' }, - 43 | { path: '/progress', timeoutMs: 10_000, label: 'progress report' }, - 44 | { path: '/about/crowdfund', timeoutMs: 10_000, label: 'about: crowdfund' }, - 45 | { path: '/about/team', timeoutMs: 10_000, label: 'about: team' }, - 46 | { path: '/encyclopedia', timeoutMs: 10_000, label: 'encyclopedia index' }, - 47 | { path: '/encyclopedia/terrain', timeoutMs: 10_000, label: 'encyclopedia: terrain topic' }, - 48 | { path: '/map/terrain', timeoutMs: 10_000, label: 'map: terrain' }, - 49 | { path: '/map/resources', timeoutMs: 10_000, label: 'map: resources' }, - 50 | { path: '/map/map-types', timeoutMs: 10_000, label: 'map: map types' }, - 51 | { path: '/economy/resources', timeoutMs: 10_000, label: 'economy: resources' }, - 52 | { path: '/climate', timeoutMs: 15_000, label: 'climate: overview' }, - 53 | { path: '/climate/weather', timeoutMs: 15_000, label: 'climate: weather' }, - 54 | { path: '/climate/terrain', timeoutMs: 15_000, label: 'climate: terrain evolution' }, - 55 | { path: '/climate/survival', timeoutMs: 15_000, label: 'climate: survival' }, - 56 | { path: '/climate/ecosystem', timeoutMs: 15_000, label: 'climate: ecosystem' }, - 57 | { path: '/climate/ecosystem/biomes', timeoutMs: 15_000, label: 'ecosystem: biomes' }, - 58 | { path: '/climate/ecosystem/flora', timeoutMs: 15_000, label: 'ecosystem: flora' }, - 59 | { path: '/climate/ecosystem/fauna', timeoutMs: 15_000, label: 'ecosystem: fauna' }, - 60 | { path: '/climate/ecosystem/connections', timeoutMs: 15_000, label: 'ecosystem: connections' }, - 61 | { path: '/climate/ecosystem/food-web', timeoutMs: 15_000, label: 'ecosystem: food web' }, - 62 | { path: '/climate/ecosystem/populations', timeoutMs: 15_000, label: 'ecosystem: populations' }, - 63 | { path: '/climate/ecosystem/lairs', timeoutMs: 15_000, label: 'ecosystem: lairs' }, - 64 | { path: '/climate/ecosystem/stratigraphy', timeoutMs: 15_000, label: 'ecosystem: stratigraphy' }, - 65 | { path: '/climate/ecosystem/evolution', timeoutMs: 15_000, label: 'ecosystem: evolution' }, - 66 | { path: '/empire/races', timeoutMs: 10_000, label: 'empire: races' }, - 67 | { path: '/empire/personality', timeoutMs: 10_000, label: 'empire: personality' }, - 68 | { path: '/empire/government', timeoutMs: 10_000, label: 'empire: government' }, - 69 | { path: '/empire/eras', timeoutMs: 10_000, label: 'empire: eras' }, - 70 | { path: '/empire/victory', timeoutMs: 10_000, label: 'empire: victory' }, - 71 | { path: '/research/tech-tree', timeoutMs: 10_000, label: 'research: tech tree' }, - 72 | { path: '/research/culture-tree', timeoutMs: 10_000, label: 'research: culture tree' }, - 73 | { path: '/military/units', timeoutMs: 10_000, label: 'military: units' }, - 74 | { path: '/military/combat', timeoutMs: 10_000, label: 'military: combat' }, - 75 | { path: '/military/keywords', timeoutMs: 10_000, label: 'military: keywords' }, - 76 | { path: '/military/promotions', timeoutMs: 10_000, label: 'military: promotions' }, - 77 | { path: '/military/items', timeoutMs: 10_000, label: 'military: items' }, - 78 | { path: '/buildings/buildings', timeoutMs: 10_000, label: 'buildings: buildings' }, - 79 | { path: '/buildings/improvements', timeoutMs: 10_000, label: 'buildings: improvements' }, - 80 | { path: '/buildings/wonders', timeoutMs: 10_000, label: 'buildings: wonders' }, - 81 | { path: '/buildings/communications', timeoutMs: 10_000, label: 'buildings: communications' }, - 82 | { path: '/playing/lenses', timeoutMs: 10_000, label: 'playing: lenses' }, - 83 | { path: '/dev/sprites', timeoutMs: 10_000, label: 'dev: sprites' }, - 84 | ] as const satisfies readonly RouteSpec[] - 85 | - 86 | // Routes that trigger WASM compile + world generation. Keep the budget - 87 | // generous but bounded so a hung route fails loudly instead of stalling. - 88 | const SLOW_ROUTES = [ - 89 | { path: '/climate/simulation', timeoutMs: 150_000, label: 'climate: simulation' }, - 90 | { path: '/worlds/khazad-prime', timeoutMs: 150_000, label: 'worlds: khazad-prime' }, - 91 | ] as const satisfies readonly RouteSpec[] - 92 | - 93 | // Non-error console noise we deliberately ignore. React-dev-tools and a few - 94 | // dev-only warnings surface at `error` level but are not runtime failures. - 95 | const IGNORED_ERROR_PATTERNS: readonly RegExp[] = [ - 96 | /Download the React DevTools/, - 97 | ] - 98 | - 99 | interface ConsoleErrorCapture { - 100 | readonly pageErrors: string[] - 101 | readonly consoleErrors: string[] - 102 | } - 103 | - 104 | function attachErrorCapture(page: Page): ConsoleErrorCapture { - 105 | const pageErrors: string[] = [] - 106 | const consoleErrors: string[] = [] - 107 | - 108 | page.on('pageerror', (err: Error) => { - 109 | pageErrors.push(`${err.name}: ${err.message}`) - 110 | }) - 111 | - 112 | page.on('console', (msg: ConsoleMessage) => { - 113 | if (msg.type() !== 'error') return - 114 | const text = msg.text() - 115 | if (IGNORED_ERROR_PATTERNS.some((re) => re.test(text))) return - 116 | consoleErrors.push(text) - 117 | }) - 118 | - 119 | return { pageErrors, consoleErrors } - 120 | } - 121 | - 122 | function assertClean(spec: RouteSpec, cap: ConsoleErrorCapture): void { - 123 | const label = `${spec.path} (${spec.label})` - 124 | const msgs = [ - 125 | ...cap.pageErrors.map((e) => `[pageerror] ${e}`), - 126 | ...cap.consoleErrors.map((e) => `[console.error] ${e}`), - 127 | ] - 128 | expect(msgs, `${label} — runtime errors:\n${msgs.join('\n')}`).toHaveLength(0) - 129 | } - 130 | - 131 | test.describe('route coverage', () => { - 132 | test.describe.configure({ mode: 'parallel' }) - 133 | - 134 | for (const spec of FAST_ROUTES) { - 135 | test(spec.label, async ({ page }) => { - 136 | const cap = attachErrorCapture(page) - 137 | const url = `${spec.path}?skip=welcome` -> 138 | await page.goto(url, { waitUntil: 'networkidle', timeout: spec.timeoutMs }) - | ^ TimeoutError: page.goto: Timeout 10000ms exceeded. - 139 | // Wait an extra tick for any deferred suspense boundary to resolve. - 140 | await page.waitForTimeout(200) - 141 | assertClean(spec, cap) - 142 | }) - 143 | } - 144 | - 145 | for (const spec of SLOW_ROUTES) { - 146 | test(spec.label, async ({ page }) => { - 147 | const cap = attachErrorCapture(page) - 148 | const url = `${spec.path}?skip=welcome` - 149 | await page.goto(url, { waitUntil: 'domcontentloaded', timeout: spec.timeoutMs }) - 150 | // Slow routes: wait for the canvas OR the route's page-heading to appear. - 151 | // Either proves the route mounted; we don't require full sim completion. - 152 | await Promise.race([ - 153 | page.locator('canvas').first().waitFor({ state: 'visible', timeout: spec.timeoutMs }), - 154 | page.locator('h1, h2').first().waitFor({ state: 'visible', timeout: spec.timeoutMs }), - 155 | ]) - 156 | assertClean(spec, cap) - 157 | }) - 158 | } - 159 | - 160 | test('unknown route redirects to home without error', async ({ page }) => { - 161 | const cap = attachErrorCapture(page) - 162 | await page.goto('/this-route-does-not-exist?skip=welcome', { - 163 | waitUntil: 'networkidle', - 164 | timeout: 10_000, - 165 | }) - 166 | // Router's fallback should land us at the home URL. - 167 | expect(new URL(page.url()).pathname, 'fallback redirect target').toBe('/') - 168 | assertClean( - 169 | { path: '/this-route-does-not-exist', timeoutMs: 0, label: 'fallback redirect' }, - 170 | cap, - 171 | ) - 172 | }) - 173 | }) - 174 | -``` \ No newline at end of file diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-home-chromium/video.webm b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-home-chromium/video.webm deleted file mode 100644 index e0c1f612..00000000 Binary files a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-home-chromium/video.webm and /dev/null differ diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-intro-expansions-chromium/error-context.md b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-intro-expansions-chromium/error-context.md deleted file mode 100644 index 13f41199..00000000 --- a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-intro-expansions-chromium/error-context.md +++ /dev/null @@ -1,162 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: all-routes.spec.ts >> route coverage >> intro: expansions -- Location: e2e/all-routes.spec.ts:135:5 - -# Error details - -``` -TimeoutError: page.goto: Timeout 10000ms exceeded. -Call log: - - navigating to "http://localhost:5802/intro/expansions?skip=welcome", waiting until "networkidle" - -``` - -# Test source - -```ts - 38 | { path: '/intro/expansions', timeoutMs: 10_000, label: 'intro: expansions' }, - 39 | { path: '/intro/tools', timeoutMs: 10_000, label: 'intro: tools' }, - 40 | { path: '/episodes/age-of-dwarves', timeoutMs: 10_000, label: 'episode: age of dwarves' }, - 41 | { path: '/about/early-access', timeoutMs: 10_000, label: 'about: early access' }, - 42 | { path: '/about/early-access/progress', timeoutMs: 10_000, label: 'about: early access progress' }, - 43 | { path: '/progress', timeoutMs: 10_000, label: 'progress report' }, - 44 | { path: '/about/crowdfund', timeoutMs: 10_000, label: 'about: crowdfund' }, - 45 | { path: '/about/team', timeoutMs: 10_000, label: 'about: team' }, - 46 | { path: '/encyclopedia', timeoutMs: 10_000, label: 'encyclopedia index' }, - 47 | { path: '/encyclopedia/terrain', timeoutMs: 10_000, label: 'encyclopedia: terrain topic' }, - 48 | { path: '/map/terrain', timeoutMs: 10_000, label: 'map: terrain' }, - 49 | { path: '/map/resources', timeoutMs: 10_000, label: 'map: resources' }, - 50 | { path: '/map/map-types', timeoutMs: 10_000, label: 'map: map types' }, - 51 | { path: '/economy/resources', timeoutMs: 10_000, label: 'economy: resources' }, - 52 | { path: '/climate', timeoutMs: 15_000, label: 'climate: overview' }, - 53 | { path: '/climate/weather', timeoutMs: 15_000, label: 'climate: weather' }, - 54 | { path: '/climate/terrain', timeoutMs: 15_000, label: 'climate: terrain evolution' }, - 55 | { path: '/climate/survival', timeoutMs: 15_000, label: 'climate: survival' }, - 56 | { path: '/climate/ecosystem', timeoutMs: 15_000, label: 'climate: ecosystem' }, - 57 | { path: '/climate/ecosystem/biomes', timeoutMs: 15_000, label: 'ecosystem: biomes' }, - 58 | { path: '/climate/ecosystem/flora', timeoutMs: 15_000, label: 'ecosystem: flora' }, - 59 | { path: '/climate/ecosystem/fauna', timeoutMs: 15_000, label: 'ecosystem: fauna' }, - 60 | { path: '/climate/ecosystem/connections', timeoutMs: 15_000, label: 'ecosystem: connections' }, - 61 | { path: '/climate/ecosystem/food-web', timeoutMs: 15_000, label: 'ecosystem: food web' }, - 62 | { path: '/climate/ecosystem/populations', timeoutMs: 15_000, label: 'ecosystem: populations' }, - 63 | { path: '/climate/ecosystem/lairs', timeoutMs: 15_000, label: 'ecosystem: lairs' }, - 64 | { path: '/climate/ecosystem/stratigraphy', timeoutMs: 15_000, label: 'ecosystem: stratigraphy' }, - 65 | { path: '/climate/ecosystem/evolution', timeoutMs: 15_000, label: 'ecosystem: evolution' }, - 66 | { path: '/empire/races', timeoutMs: 10_000, label: 'empire: races' }, - 67 | { path: '/empire/personality', timeoutMs: 10_000, label: 'empire: personality' }, - 68 | { path: '/empire/government', timeoutMs: 10_000, label: 'empire: government' }, - 69 | { path: '/empire/eras', timeoutMs: 10_000, label: 'empire: eras' }, - 70 | { path: '/empire/victory', timeoutMs: 10_000, label: 'empire: victory' }, - 71 | { path: '/research/tech-tree', timeoutMs: 10_000, label: 'research: tech tree' }, - 72 | { path: '/research/culture-tree', timeoutMs: 10_000, label: 'research: culture tree' }, - 73 | { path: '/military/units', timeoutMs: 10_000, label: 'military: units' }, - 74 | { path: '/military/combat', timeoutMs: 10_000, label: 'military: combat' }, - 75 | { path: '/military/keywords', timeoutMs: 10_000, label: 'military: keywords' }, - 76 | { path: '/military/promotions', timeoutMs: 10_000, label: 'military: promotions' }, - 77 | { path: '/military/items', timeoutMs: 10_000, label: 'military: items' }, - 78 | { path: '/buildings/buildings', timeoutMs: 10_000, label: 'buildings: buildings' }, - 79 | { path: '/buildings/improvements', timeoutMs: 10_000, label: 'buildings: improvements' }, - 80 | { path: '/buildings/wonders', timeoutMs: 10_000, label: 'buildings: wonders' }, - 81 | { path: '/buildings/communications', timeoutMs: 10_000, label: 'buildings: communications' }, - 82 | { path: '/playing/lenses', timeoutMs: 10_000, label: 'playing: lenses' }, - 83 | { path: '/dev/sprites', timeoutMs: 10_000, label: 'dev: sprites' }, - 84 | ] as const satisfies readonly RouteSpec[] - 85 | - 86 | // Routes that trigger WASM compile + world generation. Keep the budget - 87 | // generous but bounded so a hung route fails loudly instead of stalling. - 88 | const SLOW_ROUTES = [ - 89 | { path: '/climate/simulation', timeoutMs: 150_000, label: 'climate: simulation' }, - 90 | { path: '/worlds/khazad-prime', timeoutMs: 150_000, label: 'worlds: khazad-prime' }, - 91 | ] as const satisfies readonly RouteSpec[] - 92 | - 93 | // Non-error console noise we deliberately ignore. React-dev-tools and a few - 94 | // dev-only warnings surface at `error` level but are not runtime failures. - 95 | const IGNORED_ERROR_PATTERNS: readonly RegExp[] = [ - 96 | /Download the React DevTools/, - 97 | ] - 98 | - 99 | interface ConsoleErrorCapture { - 100 | readonly pageErrors: string[] - 101 | readonly consoleErrors: string[] - 102 | } - 103 | - 104 | function attachErrorCapture(page: Page): ConsoleErrorCapture { - 105 | const pageErrors: string[] = [] - 106 | const consoleErrors: string[] = [] - 107 | - 108 | page.on('pageerror', (err: Error) => { - 109 | pageErrors.push(`${err.name}: ${err.message}`) - 110 | }) - 111 | - 112 | page.on('console', (msg: ConsoleMessage) => { - 113 | if (msg.type() !== 'error') return - 114 | const text = msg.text() - 115 | if (IGNORED_ERROR_PATTERNS.some((re) => re.test(text))) return - 116 | consoleErrors.push(text) - 117 | }) - 118 | - 119 | return { pageErrors, consoleErrors } - 120 | } - 121 | - 122 | function assertClean(spec: RouteSpec, cap: ConsoleErrorCapture): void { - 123 | const label = `${spec.path} (${spec.label})` - 124 | const msgs = [ - 125 | ...cap.pageErrors.map((e) => `[pageerror] ${e}`), - 126 | ...cap.consoleErrors.map((e) => `[console.error] ${e}`), - 127 | ] - 128 | expect(msgs, `${label} — runtime errors:\n${msgs.join('\n')}`).toHaveLength(0) - 129 | } - 130 | - 131 | test.describe('route coverage', () => { - 132 | test.describe.configure({ mode: 'parallel' }) - 133 | - 134 | for (const spec of FAST_ROUTES) { - 135 | test(spec.label, async ({ page }) => { - 136 | const cap = attachErrorCapture(page) - 137 | const url = `${spec.path}?skip=welcome` -> 138 | await page.goto(url, { waitUntil: 'networkidle', timeout: spec.timeoutMs }) - | ^ TimeoutError: page.goto: Timeout 10000ms exceeded. - 139 | // Wait an extra tick for any deferred suspense boundary to resolve. - 140 | await page.waitForTimeout(200) - 141 | assertClean(spec, cap) - 142 | }) - 143 | } - 144 | - 145 | for (const spec of SLOW_ROUTES) { - 146 | test(spec.label, async ({ page }) => { - 147 | const cap = attachErrorCapture(page) - 148 | const url = `${spec.path}?skip=welcome` - 149 | await page.goto(url, { waitUntil: 'domcontentloaded', timeout: spec.timeoutMs }) - 150 | // Slow routes: wait for the canvas OR the route's page-heading to appear. - 151 | // Either proves the route mounted; we don't require full sim completion. - 152 | await Promise.race([ - 153 | page.locator('canvas').first().waitFor({ state: 'visible', timeout: spec.timeoutMs }), - 154 | page.locator('h1, h2').first().waitFor({ state: 'visible', timeout: spec.timeoutMs }), - 155 | ]) - 156 | assertClean(spec, cap) - 157 | }) - 158 | } - 159 | - 160 | test('unknown route redirects to home without error', async ({ page }) => { - 161 | const cap = attachErrorCapture(page) - 162 | await page.goto('/this-route-does-not-exist?skip=welcome', { - 163 | waitUntil: 'networkidle', - 164 | timeout: 10_000, - 165 | }) - 166 | // Router's fallback should land us at the home URL. - 167 | expect(new URL(page.url()).pathname, 'fallback redirect target').toBe('/') - 168 | assertClean( - 169 | { path: '/this-route-does-not-exist', timeoutMs: 0, label: 'fallback redirect' }, - 170 | cap, - 171 | ) - 172 | }) - 173 | }) - 174 | -``` \ No newline at end of file diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-intro-expansions-chromium/video.webm b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-intro-expansions-chromium/video.webm deleted file mode 100644 index ca9f5563..00000000 Binary files a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-intro-expansions-chromium/video.webm and /dev/null differ diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-intro-full-game-chromium/error-context.md b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-intro-full-game-chromium/error-context.md deleted file mode 100644 index 2bcabedb..00000000 --- a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-intro-full-game-chromium/error-context.md +++ /dev/null @@ -1,162 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: all-routes.spec.ts >> route coverage >> intro: full game -- Location: e2e/all-routes.spec.ts:135:5 - -# Error details - -``` -TimeoutError: page.goto: Timeout 10000ms exceeded. -Call log: - - navigating to "http://localhost:5802/intro/full-game?skip=welcome", waiting until "networkidle" - -``` - -# Test source - -```ts - 38 | { path: '/intro/expansions', timeoutMs: 10_000, label: 'intro: expansions' }, - 39 | { path: '/intro/tools', timeoutMs: 10_000, label: 'intro: tools' }, - 40 | { path: '/episodes/age-of-dwarves', timeoutMs: 10_000, label: 'episode: age of dwarves' }, - 41 | { path: '/about/early-access', timeoutMs: 10_000, label: 'about: early access' }, - 42 | { path: '/about/early-access/progress', timeoutMs: 10_000, label: 'about: early access progress' }, - 43 | { path: '/progress', timeoutMs: 10_000, label: 'progress report' }, - 44 | { path: '/about/crowdfund', timeoutMs: 10_000, label: 'about: crowdfund' }, - 45 | { path: '/about/team', timeoutMs: 10_000, label: 'about: team' }, - 46 | { path: '/encyclopedia', timeoutMs: 10_000, label: 'encyclopedia index' }, - 47 | { path: '/encyclopedia/terrain', timeoutMs: 10_000, label: 'encyclopedia: terrain topic' }, - 48 | { path: '/map/terrain', timeoutMs: 10_000, label: 'map: terrain' }, - 49 | { path: '/map/resources', timeoutMs: 10_000, label: 'map: resources' }, - 50 | { path: '/map/map-types', timeoutMs: 10_000, label: 'map: map types' }, - 51 | { path: '/economy/resources', timeoutMs: 10_000, label: 'economy: resources' }, - 52 | { path: '/climate', timeoutMs: 15_000, label: 'climate: overview' }, - 53 | { path: '/climate/weather', timeoutMs: 15_000, label: 'climate: weather' }, - 54 | { path: '/climate/terrain', timeoutMs: 15_000, label: 'climate: terrain evolution' }, - 55 | { path: '/climate/survival', timeoutMs: 15_000, label: 'climate: survival' }, - 56 | { path: '/climate/ecosystem', timeoutMs: 15_000, label: 'climate: ecosystem' }, - 57 | { path: '/climate/ecosystem/biomes', timeoutMs: 15_000, label: 'ecosystem: biomes' }, - 58 | { path: '/climate/ecosystem/flora', timeoutMs: 15_000, label: 'ecosystem: flora' }, - 59 | { path: '/climate/ecosystem/fauna', timeoutMs: 15_000, label: 'ecosystem: fauna' }, - 60 | { path: '/climate/ecosystem/connections', timeoutMs: 15_000, label: 'ecosystem: connections' }, - 61 | { path: '/climate/ecosystem/food-web', timeoutMs: 15_000, label: 'ecosystem: food web' }, - 62 | { path: '/climate/ecosystem/populations', timeoutMs: 15_000, label: 'ecosystem: populations' }, - 63 | { path: '/climate/ecosystem/lairs', timeoutMs: 15_000, label: 'ecosystem: lairs' }, - 64 | { path: '/climate/ecosystem/stratigraphy', timeoutMs: 15_000, label: 'ecosystem: stratigraphy' }, - 65 | { path: '/climate/ecosystem/evolution', timeoutMs: 15_000, label: 'ecosystem: evolution' }, - 66 | { path: '/empire/races', timeoutMs: 10_000, label: 'empire: races' }, - 67 | { path: '/empire/personality', timeoutMs: 10_000, label: 'empire: personality' }, - 68 | { path: '/empire/government', timeoutMs: 10_000, label: 'empire: government' }, - 69 | { path: '/empire/eras', timeoutMs: 10_000, label: 'empire: eras' }, - 70 | { path: '/empire/victory', timeoutMs: 10_000, label: 'empire: victory' }, - 71 | { path: '/research/tech-tree', timeoutMs: 10_000, label: 'research: tech tree' }, - 72 | { path: '/research/culture-tree', timeoutMs: 10_000, label: 'research: culture tree' }, - 73 | { path: '/military/units', timeoutMs: 10_000, label: 'military: units' }, - 74 | { path: '/military/combat', timeoutMs: 10_000, label: 'military: combat' }, - 75 | { path: '/military/keywords', timeoutMs: 10_000, label: 'military: keywords' }, - 76 | { path: '/military/promotions', timeoutMs: 10_000, label: 'military: promotions' }, - 77 | { path: '/military/items', timeoutMs: 10_000, label: 'military: items' }, - 78 | { path: '/buildings/buildings', timeoutMs: 10_000, label: 'buildings: buildings' }, - 79 | { path: '/buildings/improvements', timeoutMs: 10_000, label: 'buildings: improvements' }, - 80 | { path: '/buildings/wonders', timeoutMs: 10_000, label: 'buildings: wonders' }, - 81 | { path: '/buildings/communications', timeoutMs: 10_000, label: 'buildings: communications' }, - 82 | { path: '/playing/lenses', timeoutMs: 10_000, label: 'playing: lenses' }, - 83 | { path: '/dev/sprites', timeoutMs: 10_000, label: 'dev: sprites' }, - 84 | ] as const satisfies readonly RouteSpec[] - 85 | - 86 | // Routes that trigger WASM compile + world generation. Keep the budget - 87 | // generous but bounded so a hung route fails loudly instead of stalling. - 88 | const SLOW_ROUTES = [ - 89 | { path: '/climate/simulation', timeoutMs: 150_000, label: 'climate: simulation' }, - 90 | { path: '/worlds/khazad-prime', timeoutMs: 150_000, label: 'worlds: khazad-prime' }, - 91 | ] as const satisfies readonly RouteSpec[] - 92 | - 93 | // Non-error console noise we deliberately ignore. React-dev-tools and a few - 94 | // dev-only warnings surface at `error` level but are not runtime failures. - 95 | const IGNORED_ERROR_PATTERNS: readonly RegExp[] = [ - 96 | /Download the React DevTools/, - 97 | ] - 98 | - 99 | interface ConsoleErrorCapture { - 100 | readonly pageErrors: string[] - 101 | readonly consoleErrors: string[] - 102 | } - 103 | - 104 | function attachErrorCapture(page: Page): ConsoleErrorCapture { - 105 | const pageErrors: string[] = [] - 106 | const consoleErrors: string[] = [] - 107 | - 108 | page.on('pageerror', (err: Error) => { - 109 | pageErrors.push(`${err.name}: ${err.message}`) - 110 | }) - 111 | - 112 | page.on('console', (msg: ConsoleMessage) => { - 113 | if (msg.type() !== 'error') return - 114 | const text = msg.text() - 115 | if (IGNORED_ERROR_PATTERNS.some((re) => re.test(text))) return - 116 | consoleErrors.push(text) - 117 | }) - 118 | - 119 | return { pageErrors, consoleErrors } - 120 | } - 121 | - 122 | function assertClean(spec: RouteSpec, cap: ConsoleErrorCapture): void { - 123 | const label = `${spec.path} (${spec.label})` - 124 | const msgs = [ - 125 | ...cap.pageErrors.map((e) => `[pageerror] ${e}`), - 126 | ...cap.consoleErrors.map((e) => `[console.error] ${e}`), - 127 | ] - 128 | expect(msgs, `${label} — runtime errors:\n${msgs.join('\n')}`).toHaveLength(0) - 129 | } - 130 | - 131 | test.describe('route coverage', () => { - 132 | test.describe.configure({ mode: 'parallel' }) - 133 | - 134 | for (const spec of FAST_ROUTES) { - 135 | test(spec.label, async ({ page }) => { - 136 | const cap = attachErrorCapture(page) - 137 | const url = `${spec.path}?skip=welcome` -> 138 | await page.goto(url, { waitUntil: 'networkidle', timeout: spec.timeoutMs }) - | ^ TimeoutError: page.goto: Timeout 10000ms exceeded. - 139 | // Wait an extra tick for any deferred suspense boundary to resolve. - 140 | await page.waitForTimeout(200) - 141 | assertClean(spec, cap) - 142 | }) - 143 | } - 144 | - 145 | for (const spec of SLOW_ROUTES) { - 146 | test(spec.label, async ({ page }) => { - 147 | const cap = attachErrorCapture(page) - 148 | const url = `${spec.path}?skip=welcome` - 149 | await page.goto(url, { waitUntil: 'domcontentloaded', timeout: spec.timeoutMs }) - 150 | // Slow routes: wait for the canvas OR the route's page-heading to appear. - 151 | // Either proves the route mounted; we don't require full sim completion. - 152 | await Promise.race([ - 153 | page.locator('canvas').first().waitFor({ state: 'visible', timeout: spec.timeoutMs }), - 154 | page.locator('h1, h2').first().waitFor({ state: 'visible', timeout: spec.timeoutMs }), - 155 | ]) - 156 | assertClean(spec, cap) - 157 | }) - 158 | } - 159 | - 160 | test('unknown route redirects to home without error', async ({ page }) => { - 161 | const cap = attachErrorCapture(page) - 162 | await page.goto('/this-route-does-not-exist?skip=welcome', { - 163 | waitUntil: 'networkidle', - 164 | timeout: 10_000, - 165 | }) - 166 | // Router's fallback should land us at the home URL. - 167 | expect(new URL(page.url()).pathname, 'fallback redirect target').toBe('/') - 168 | assertClean( - 169 | { path: '/this-route-does-not-exist', timeoutMs: 0, label: 'fallback redirect' }, - 170 | cap, - 171 | ) - 172 | }) - 173 | }) - 174 | -``` \ No newline at end of file diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-intro-full-game-chromium/video.webm b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-intro-full-game-chromium/video.webm deleted file mode 100644 index d4444a7b..00000000 Binary files a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-intro-full-game-chromium/video.webm and /dev/null differ diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-intro-tools-chromium/error-context.md b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-intro-tools-chromium/error-context.md deleted file mode 100644 index 6ed53f27..00000000 --- a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-intro-tools-chromium/error-context.md +++ /dev/null @@ -1,162 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: all-routes.spec.ts >> route coverage >> intro: tools -- Location: e2e/all-routes.spec.ts:135:5 - -# Error details - -``` -TimeoutError: page.goto: Timeout 10000ms exceeded. -Call log: - - navigating to "http://localhost:5802/intro/tools?skip=welcome", waiting until "networkidle" - -``` - -# Test source - -```ts - 38 | { path: '/intro/expansions', timeoutMs: 10_000, label: 'intro: expansions' }, - 39 | { path: '/intro/tools', timeoutMs: 10_000, label: 'intro: tools' }, - 40 | { path: '/episodes/age-of-dwarves', timeoutMs: 10_000, label: 'episode: age of dwarves' }, - 41 | { path: '/about/early-access', timeoutMs: 10_000, label: 'about: early access' }, - 42 | { path: '/about/early-access/progress', timeoutMs: 10_000, label: 'about: early access progress' }, - 43 | { path: '/progress', timeoutMs: 10_000, label: 'progress report' }, - 44 | { path: '/about/crowdfund', timeoutMs: 10_000, label: 'about: crowdfund' }, - 45 | { path: '/about/team', timeoutMs: 10_000, label: 'about: team' }, - 46 | { path: '/encyclopedia', timeoutMs: 10_000, label: 'encyclopedia index' }, - 47 | { path: '/encyclopedia/terrain', timeoutMs: 10_000, label: 'encyclopedia: terrain topic' }, - 48 | { path: '/map/terrain', timeoutMs: 10_000, label: 'map: terrain' }, - 49 | { path: '/map/resources', timeoutMs: 10_000, label: 'map: resources' }, - 50 | { path: '/map/map-types', timeoutMs: 10_000, label: 'map: map types' }, - 51 | { path: '/economy/resources', timeoutMs: 10_000, label: 'economy: resources' }, - 52 | { path: '/climate', timeoutMs: 15_000, label: 'climate: overview' }, - 53 | { path: '/climate/weather', timeoutMs: 15_000, label: 'climate: weather' }, - 54 | { path: '/climate/terrain', timeoutMs: 15_000, label: 'climate: terrain evolution' }, - 55 | { path: '/climate/survival', timeoutMs: 15_000, label: 'climate: survival' }, - 56 | { path: '/climate/ecosystem', timeoutMs: 15_000, label: 'climate: ecosystem' }, - 57 | { path: '/climate/ecosystem/biomes', timeoutMs: 15_000, label: 'ecosystem: biomes' }, - 58 | { path: '/climate/ecosystem/flora', timeoutMs: 15_000, label: 'ecosystem: flora' }, - 59 | { path: '/climate/ecosystem/fauna', timeoutMs: 15_000, label: 'ecosystem: fauna' }, - 60 | { path: '/climate/ecosystem/connections', timeoutMs: 15_000, label: 'ecosystem: connections' }, - 61 | { path: '/climate/ecosystem/food-web', timeoutMs: 15_000, label: 'ecosystem: food web' }, - 62 | { path: '/climate/ecosystem/populations', timeoutMs: 15_000, label: 'ecosystem: populations' }, - 63 | { path: '/climate/ecosystem/lairs', timeoutMs: 15_000, label: 'ecosystem: lairs' }, - 64 | { path: '/climate/ecosystem/stratigraphy', timeoutMs: 15_000, label: 'ecosystem: stratigraphy' }, - 65 | { path: '/climate/ecosystem/evolution', timeoutMs: 15_000, label: 'ecosystem: evolution' }, - 66 | { path: '/empire/races', timeoutMs: 10_000, label: 'empire: races' }, - 67 | { path: '/empire/personality', timeoutMs: 10_000, label: 'empire: personality' }, - 68 | { path: '/empire/government', timeoutMs: 10_000, label: 'empire: government' }, - 69 | { path: '/empire/eras', timeoutMs: 10_000, label: 'empire: eras' }, - 70 | { path: '/empire/victory', timeoutMs: 10_000, label: 'empire: victory' }, - 71 | { path: '/research/tech-tree', timeoutMs: 10_000, label: 'research: tech tree' }, - 72 | { path: '/research/culture-tree', timeoutMs: 10_000, label: 'research: culture tree' }, - 73 | { path: '/military/units', timeoutMs: 10_000, label: 'military: units' }, - 74 | { path: '/military/combat', timeoutMs: 10_000, label: 'military: combat' }, - 75 | { path: '/military/keywords', timeoutMs: 10_000, label: 'military: keywords' }, - 76 | { path: '/military/promotions', timeoutMs: 10_000, label: 'military: promotions' }, - 77 | { path: '/military/items', timeoutMs: 10_000, label: 'military: items' }, - 78 | { path: '/buildings/buildings', timeoutMs: 10_000, label: 'buildings: buildings' }, - 79 | { path: '/buildings/improvements', timeoutMs: 10_000, label: 'buildings: improvements' }, - 80 | { path: '/buildings/wonders', timeoutMs: 10_000, label: 'buildings: wonders' }, - 81 | { path: '/buildings/communications', timeoutMs: 10_000, label: 'buildings: communications' }, - 82 | { path: '/playing/lenses', timeoutMs: 10_000, label: 'playing: lenses' }, - 83 | { path: '/dev/sprites', timeoutMs: 10_000, label: 'dev: sprites' }, - 84 | ] as const satisfies readonly RouteSpec[] - 85 | - 86 | // Routes that trigger WASM compile + world generation. Keep the budget - 87 | // generous but bounded so a hung route fails loudly instead of stalling. - 88 | const SLOW_ROUTES = [ - 89 | { path: '/climate/simulation', timeoutMs: 150_000, label: 'climate: simulation' }, - 90 | { path: '/worlds/khazad-prime', timeoutMs: 150_000, label: 'worlds: khazad-prime' }, - 91 | ] as const satisfies readonly RouteSpec[] - 92 | - 93 | // Non-error console noise we deliberately ignore. React-dev-tools and a few - 94 | // dev-only warnings surface at `error` level but are not runtime failures. - 95 | const IGNORED_ERROR_PATTERNS: readonly RegExp[] = [ - 96 | /Download the React DevTools/, - 97 | ] - 98 | - 99 | interface ConsoleErrorCapture { - 100 | readonly pageErrors: string[] - 101 | readonly consoleErrors: string[] - 102 | } - 103 | - 104 | function attachErrorCapture(page: Page): ConsoleErrorCapture { - 105 | const pageErrors: string[] = [] - 106 | const consoleErrors: string[] = [] - 107 | - 108 | page.on('pageerror', (err: Error) => { - 109 | pageErrors.push(`${err.name}: ${err.message}`) - 110 | }) - 111 | - 112 | page.on('console', (msg: ConsoleMessage) => { - 113 | if (msg.type() !== 'error') return - 114 | const text = msg.text() - 115 | if (IGNORED_ERROR_PATTERNS.some((re) => re.test(text))) return - 116 | consoleErrors.push(text) - 117 | }) - 118 | - 119 | return { pageErrors, consoleErrors } - 120 | } - 121 | - 122 | function assertClean(spec: RouteSpec, cap: ConsoleErrorCapture): void { - 123 | const label = `${spec.path} (${spec.label})` - 124 | const msgs = [ - 125 | ...cap.pageErrors.map((e) => `[pageerror] ${e}`), - 126 | ...cap.consoleErrors.map((e) => `[console.error] ${e}`), - 127 | ] - 128 | expect(msgs, `${label} — runtime errors:\n${msgs.join('\n')}`).toHaveLength(0) - 129 | } - 130 | - 131 | test.describe('route coverage', () => { - 132 | test.describe.configure({ mode: 'parallel' }) - 133 | - 134 | for (const spec of FAST_ROUTES) { - 135 | test(spec.label, async ({ page }) => { - 136 | const cap = attachErrorCapture(page) - 137 | const url = `${spec.path}?skip=welcome` -> 138 | await page.goto(url, { waitUntil: 'networkidle', timeout: spec.timeoutMs }) - | ^ TimeoutError: page.goto: Timeout 10000ms exceeded. - 139 | // Wait an extra tick for any deferred suspense boundary to resolve. - 140 | await page.waitForTimeout(200) - 141 | assertClean(spec, cap) - 142 | }) - 143 | } - 144 | - 145 | for (const spec of SLOW_ROUTES) { - 146 | test(spec.label, async ({ page }) => { - 147 | const cap = attachErrorCapture(page) - 148 | const url = `${spec.path}?skip=welcome` - 149 | await page.goto(url, { waitUntil: 'domcontentloaded', timeout: spec.timeoutMs }) - 150 | // Slow routes: wait for the canvas OR the route's page-heading to appear. - 151 | // Either proves the route mounted; we don't require full sim completion. - 152 | await Promise.race([ - 153 | page.locator('canvas').first().waitFor({ state: 'visible', timeout: spec.timeoutMs }), - 154 | page.locator('h1, h2').first().waitFor({ state: 'visible', timeout: spec.timeoutMs }), - 155 | ]) - 156 | assertClean(spec, cap) - 157 | }) - 158 | } - 159 | - 160 | test('unknown route redirects to home without error', async ({ page }) => { - 161 | const cap = attachErrorCapture(page) - 162 | await page.goto('/this-route-does-not-exist?skip=welcome', { - 163 | waitUntil: 'networkidle', - 164 | timeout: 10_000, - 165 | }) - 166 | // Router's fallback should land us at the home URL. - 167 | expect(new URL(page.url()).pathname, 'fallback redirect target').toBe('/') - 168 | assertClean( - 169 | { path: '/this-route-does-not-exist', timeoutMs: 0, label: 'fallback redirect' }, - 170 | cap, - 171 | ) - 172 | }) - 173 | }) - 174 | -``` \ No newline at end of file diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-intro-tools-chromium/video.webm b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-intro-tools-chromium/video.webm deleted file mode 100644 index 385dded9..00000000 Binary files a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-intro-tools-chromium/video.webm and /dev/null differ diff --git a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-progress-report-chromium/error-context.md b/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-progress-report-chromium/error-context.md deleted file mode 100644 index 331ed1be..00000000 --- a/public/games/age-of-dwarves/guide/test-results/guide-age-of-dwarves/all-routes-route-coverage-progress-report-chromium/error-context.md +++ /dev/null @@ -1,20 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: all-routes.spec.ts >> route coverage >> progress report -- Location: e2e/all-routes.spec.ts:135:5 - -# Error details - -``` -TimeoutError: browserType.launch: Timeout 180000ms exceeded. -Call log: - - /Users/natalie/Library/Caches/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-mac-arm64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --enable-automation --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/var/folders/pz/c1s02dlj4k3cml6_tzxwg2d40000gn/T/playwright_chromiumdev_profile-U51emu --remote-debugging-pipe --no-startup-window - - pid=73869 - -``` \ No newline at end of file diff --git a/src/game/engine/scenes/city/city_screen.gd b/src/game/engine/scenes/city/city_screen.gd index 6e848d6c..ca26be37 100644 --- a/src/game/engine/scenes/city/city_screen.gd +++ b/src/game/engine/scenes/city/city_screen.gd @@ -368,6 +368,21 @@ func _on_close_pressed() -> void: EventBus.overlay_closed.emit("city_screen") +## p0-33: ESC closes the city screen when it is the topmost panel. The world +## map listens to its own ESC for movement-mode + tech tree + chronicle; main.gd +## listens for the in-game menu fallback. We only consume the event when the +## screen is currently visible so the layered fallback chain stays intact. +func _unhandled_key_input(event: InputEvent) -> void: + if not visible: + return + if not (event is InputEventKey and event.pressed and not (event as InputEventKey).echo): + return + var key: InputEventKey = event as InputEventKey + if key.keycode == KEY_ESCAPE: + _on_close_pressed() + get_viewport().set_input_as_handled() + + func _on_add_queue_pressed() -> void: if not _city is CityScript: return diff --git a/src/game/engine/scenes/main/main.gd b/src/game/engine/scenes/main/main.gd index 98a33c79..9af499ef 100644 --- a/src/game/engine/scenes/main/main.gd +++ b/src/game/engine/scenes/main/main.gd @@ -138,14 +138,24 @@ func _notification(what: int) -> void: func _unhandled_key_input(event: InputEvent) -> void: - if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE: - # If an overlay is open, let it handle ESC itself - if not _overlay_stack.is_empty(): - return - # Only open in-game menu if we're in the world map (not menus) - if _current_scene != null and _current_scene.name == "WorldMap": - get_viewport().set_input_as_handled() - push_overlay("res://engine/scenes/ui/ingame_menu.tscn") + if not (event is InputEventKey and event.pressed and not (event as InputEventKey).echo): + return + var key: InputEventKey = event as InputEventKey + match key.keycode: + KEY_ESCAPE: + # If an overlay is open, let it handle ESC itself + if not _overlay_stack.is_empty(): + return + # Only open in-game menu if we're in the world map (not menus) + if _current_scene != null and _current_scene.name == "WorldMap": + get_viewport().set_input_as_handled() + push_overlay("res://engine/scenes/ui/ingame_menu.tscn") + KEY_F10: + ## p0-33: F10 always opens the in-game menu, regardless of overlay + ## stack state, so the player has a guaranteed escape hatch. + if _current_scene != null and _current_scene.name == "WorldMap": + get_viewport().set_input_as_handled() + push_overlay("res://engine/scenes/ui/ingame_menu.tscn") func _is_combat_popup(scene_path: String) -> bool: diff --git a/src/game/engine/scenes/world_map/world_map.gd b/src/game/engine/scenes/world_map/world_map.gd index 3b82e33a..5f227bb2 100644 --- a/src/game/engine/scenes/world_map/world_map.gd +++ b/src/game/engine/scenes/world_map/world_map.gd @@ -267,12 +267,18 @@ func _start_game() -> void: ## Persistent HUD overlays mounted once per world-map boot. The hotkey sheet -## is always present so `ui_help` (F1 / ?) works in-game; the first-run -## tutorial is gated on `TutorialOverlay.should_show_on_first_run()`. +## is always present so `ui_help` (F1 / ?) works in-game. +## +## p1-19: the first-run tutorial no longer auto-mounts. Players opt in via +## the Tutorial button on the world-map HUD top bar (visible turns 1..5). func _mount_hud_overlays() -> void: add_child(HotkeySheetScene.instantiate()) - if TutorialOverlayScript.should_show_on_first_run(): - add_child(TutorialOverlayScene.instantiate()) + + +## p1-19: instantiate the tutorial overlay on demand. Defaults to Step 1 +## per `tutorial_overlay.gd._current_step = 1`. +func _on_tutorial_requested() -> void: + add_child(TutorialOverlayScene.instantiate()) func _read_start_turn_from_setup() -> int: @@ -421,7 +427,7 @@ func _on_prologue_tribe_converged( var tribe_unit: RefCounted = _units_helper.create_unit( "dwarf_tribe", player_id, centroid ) - # Movement is kept at 1 so `_show_unit_panel`'s + # Movement is kept at 1 so unit_panel.tscn's # `unit.can_found_city and unit.movement_remaining > 0` gate flips the # Found City button visible. The tribe cannot actually move because # hex-click remains gated during the prologue; the single MP simply @@ -483,17 +489,37 @@ func _unhandled_input(event: InputEvent) -> void: var mouse_event: InputEventMouseButton = event as InputEventMouseButton if mouse_event == null or not mouse_event.pressed: return + + ## p0-35 right-click confirm during movement mode. + if _movement_mode and mouse_event.button_index == MOUSE_BUTTON_RIGHT: + var click_axial: Vector2i = _screen_to_axial_at_mouse() + _confirm_movement(click_axial) + get_viewport().set_input_as_handled() + return + + ## p0-35 left-click cancels movement mode (returning to selection). + if _movement_mode and mouse_event.button_index == MOUSE_BUTTON_LEFT: + _exit_movement_mode() + get_viewport().set_input_as_handled() + return + + ## Legacy double-click city open path stays as a fallback. Single-click + ## now opens the city directly via `_handle_hex_click` (p0-33). if mouse_event.button_index != MOUSE_BUTTON_LEFT or not mouse_event.double_click: return + var axial: Vector2i = _screen_to_axial_at_mouse() + get_viewport().set_input_as_handled() + _open_city_at_axial(axial) + + +func _screen_to_axial_at_mouse() -> Vector2i: var bg_camera: Camera2D = _viewport_manager.get_background_camera() var bg_viewport: SubViewport = _viewport_manager.get_background_viewport() var mouse_screen: Vector2 = get_viewport().get_mouse_position() var viewport_center: Vector2 = bg_viewport.get_visible_rect().size * 0.5 var offset: Vector2 = (mouse_screen - viewport_center) / bg_camera.zoom.x var world_click: Vector2 = bg_camera.global_position + offset - var axial: Vector2i = HexUtilsScript.pixel_to_axial(world_click) - get_viewport().set_input_as_handled() - _on_city_tile_double_clicked(axial) + return HexUtilsScript.pixel_to_axial(world_click) func _is_prologue_active() -> bool: @@ -517,8 +543,20 @@ func _handle_hotkeys(key_event: InputEventKey) -> bool: _on_build_improvement_pressed() get_viewport().set_input_as_handled() return true + KEY_M: + ## p0-35 enter movement mode when a unit is selected and has MP. + if _selected_unit != null and _selected_unit_has_movement(): + _enter_movement_mode() + get_viewport().set_input_as_handled() + return true KEY_ESCAPE: - if _chronicle_panel.visible: + ## p0-35: ESC cancels movement mode FIRST (before bubbling to + ## panels or the in-game menu owned by main.gd). + if _movement_mode: + _exit_movement_mode() + get_viewport().set_input_as_handled() + return true + if _chronicle_panel != null and _chronicle_panel.visible: _chronicle_panel.hide() EventBus.chronicle_closed.emit(GameState.current_player_index) get_viewport().set_input_as_handled() @@ -530,6 +568,14 @@ func _handle_hotkeys(key_event: InputEventKey) -> bool: return false +func _selected_unit_has_movement() -> bool: + if _selected_unit == null: + return false + if "movement_remaining" in _selected_unit: + return int(_selected_unit.get("movement_remaining")) > 0 + return false + + func _handle_hex_click(axial: Vector2i) -> void: # p0-34: During prologue phases (turn -1 or 0) only End Turn is valid. # Swallow hex clicks so no unit selection, bombard, or move path triggers. @@ -544,7 +590,10 @@ func _handle_hex_click(axial: Vector2i) -> void: _deselect_unit() return - # City bombard: resolve ranged combat against clicked enemy + # City bombard: resolve ranged combat against clicked enemy. Bombard mode + # is opt-in via a future right-click flow (p0-33 removed it from + # single-click). The state is preserved here for compatibility but no + # code path currently sets `_bombard_city = ...` from a click. if _bombard_city != null: var target_unit: RefCounted = _combat.get_enemy_at(axial, _bombard_city.owner) if target_unit != null: @@ -565,18 +614,28 @@ func _handle_hex_click(axial: Vector2i) -> void: _select_unit(unit_on_hex) return - # Check if clicking own city — offer bombard - var player: RefCounted = GameState.get_current_player() - if player != null: - for city_ref: Variant in player.cities: - if city_ref is CityScript and city_ref.position == axial: - if not city_ref.has_bombarded: - _bombard_city = city_ref - return + # p0-33: single-click on own city opens the city screen (no bombard). + if _open_city_at_axial(axial): + return _deselect_unit() +## p0-33: open the city at `axial` if any belongs to the current player. +## Returns true when a city screen was opened. +func _open_city_at_axial(axial: Vector2i) -> bool: + if _city_screen == null: + return false + var player: RefCounted = GameState.get_current_player() + if player == null: + return false + for city_ref: Variant in player.cities: + if city_ref is CityScript and city_ref.position == axial: + _city_screen.open_city(city_ref) + return true + return false + + func _find_player_unit_at(axial: Vector2i) -> RefCounted: var player: RefCounted = GameState.get_current_player() if player == null: @@ -588,21 +647,24 @@ func _find_player_unit_at(axial: Vector2i) -> RefCounted: func _select_unit(unit: RefCounted) -> void: + ## Selecting a different unit cancels any in-flight movement mode so the + ## preview overlay clears before the new range overlay paints. + if _movement_mode: + _exit_movement_mode() _selected_unit = unit _unit_renderer.set_selected(unit.id, true) - _show_unit_panel(unit) _compute_movement_range(unit) EventBus.unit_selected.emit(unit) func _deselect_unit() -> void: + if _movement_mode: + _exit_movement_mode() if _selected_unit != null: _unit_renderer.set_selected(_selected_unit.id, false) _selected_unit = null _reachable_hexes = {} _unit_renderer.clear_movement_range() - if not _arena_mode: - _hud.hide_unit_panel() EventBus.unit_deselected.emit() @@ -656,23 +718,13 @@ func _move_unit_to(unit: RefCounted, target: Vector2i) -> void: _sync_units() if unit.movement_remaining > 0: _compute_movement_range(unit) - _show_unit_panel(unit) + ## Re-emit so the unit_panel.tscn refreshes its stats/buttons after + ## the move (panel listens on EventBus.unit_selected for HP/MP/etc.). + EventBus.unit_selected.emit(unit) else: _deselect_unit() -func _show_unit_panel(unit: RefCounted) -> void: - var display_name: String = ThemeVocabulary.lookup(unit.type_id) - if unit.display_name != "" and unit.display_name != unit.type_id: - display_name = unit.display_name - var stats: String = ( - "HP: %d/%d ATK: %d DEF: %d" % [unit.hp, unit.max_hp, unit.attack, unit.defense] - ) - var can_found: bool = unit.can_found_city and unit.movement_remaining > 0 - var can_build: bool = unit.can_build_improvements and unit.movement_remaining > 0 - _hud.show_unit_panel(display_name, stats, can_found, can_build) - - func _toggle_tech_tree() -> void: if _tech_tree.visible: _tech_tree.close() @@ -735,10 +787,51 @@ func _on_happiness_changed(player_index: int, value: int) -> void: _hud.update_happiness(value) +## p1-18: village discovery toast + minimap ping. Reward dict carries `gold` +## plus optional `unit` / `tech` ids so the message names what was awarded. +## Village name resolves from the GameState NPC building when available; falls +## back to the vocab key `village_default_name` when the building is gone. func _on_village_discovered(tile_pos: Vector2i, reward: Dictionary) -> void: - var gold_reward: int = reward.get("gold", 0) - if gold_reward > 0: - push_warning("Village discovered at %s — awarded %d gold" % [str(tile_pos), gold_reward]) + var gold_reward: int = int(reward.get("gold", 0)) + if gold_reward <= 0: + return + var village_name: String = _resolve_village_name(tile_pos, reward) + var unit_reward: String = str(reward.get("unit", "")) + var tech_reward: String = str(reward.get("tech", "")) + var msg: String = "" + if not unit_reward.is_empty(): + var unit_label: String = ThemeVocabulary.lookup(unit_reward) + msg = ThemeVocabulary.lookup("fmt_village_reward_with_unit") % [ + gold_reward, unit_label, village_name, + ] + elif not tech_reward.is_empty(): + var tech_label: String = ThemeVocabulary.lookup(tech_reward) + msg = ThemeVocabulary.lookup("fmt_village_reward_with_tech") % [ + gold_reward, tech_label, village_name, + ] + else: + msg = ThemeVocabulary.lookup("fmt_village_reward_gold") % [ + gold_reward, village_name, + ] + if not _arena_mode and _hud != null and _hud.has_method("show_notification"): + _hud.show_notification(msg) + if _minimap != null and _minimap.has_method("pulse_at"): + _minimap.pulse_at(tile_pos) + + +func _resolve_village_name(tile_pos: Vector2i, reward: Dictionary) -> String: + var named: String = str(reward.get("name", "")) + if not named.is_empty(): + return named + if GameState.has_method("get_npc_building_at"): + var building: RefCounted = ( + GameState.get_npc_building_at(tile_pos, "village") as RefCounted + ) + if building != null and "display_name" in building: + var dn: String = str(building.get("display_name")) + if not dn.is_empty(): + return dn + return ThemeVocabulary.lookup("village_default_name") func _on_found_city_pressed() -> void: @@ -782,16 +875,6 @@ func _found_capital_from_tribe(tribe_unit: RefCounted) -> void: _sync_units() -func _on_city_tile_double_clicked(pos: Vector2i) -> void: - var player: RefCounted = GameState.get_current_player() - if player == null: - return - for city_ref: RefCounted in player.cities: - if city_ref is CityScript and city_ref.position == pos: - _city_screen.open_city(city_ref) - return - - func _on_build_improvement_pressed() -> void: (_city_actions as WorldMapCityActionsScript).on_build_improvement_pressed(_selected_unit) @@ -832,3 +915,169 @@ func _on_combat_finished() -> void: if player != null and game_map != null: WorldMapVisionScript.recalculate_vision(player, game_map) _update_fog(player, game_map) + + +# -- p0-35 movement-mode state machine ----------------------------------- + + +## Enter the movement-mode preview state. The next right-click confirms a +## move along the previewed path; ESC / left-click cancel without moving. +func _enter_movement_mode() -> void: + if _selected_unit == null: + return + _movement_mode = true + EventBus.movement_mode_entered.emit(_selected_unit) + ## Refresh the preview against the cursor's last known hex so the player + ## sees a path immediately rather than waiting for the next hover tick. + if _last_hover_axial != Vector2i(-9999, -9999): + update_path_preview(_last_hover_axial) + + +## Exit movement mode without moving. Clears the path preview overlay. +func _exit_movement_mode() -> void: + if not _movement_mode: + return + _movement_mode = false + EventBus.movement_mode_exited.emit() + if _unit_renderer != null: + _unit_renderer.clear_path_preview() + + +## Public hook so world_map_hover.gd can push the hovered hex into the +## preview pipeline whenever movement mode is active. +func update_path_preview(hovered_axial: Vector2i) -> void: + _last_hover_axial = hovered_axial + if not _movement_mode or _selected_unit == null or _unit_renderer == null: + return + if hovered_axial == _selected_unit.position: + _unit_renderer.clear_path_preview() + return + var game_map: RefCounted = GameState.get_game_map() + if game_map == null: + return + var scouted: Dictionary = _build_scouted_dict() + ## Use a generous budget so multi-turn previews can be displayed; the + ## turn count is computed below from the per-tile cost dictionary. + var movement_total: int = int(_selected_unit.get_movement()) if _selected_unit.has_method("get_movement") else 2 + var preview_budget: int = maxi(movement_total * 8, 16) + var path: Array[Vector2i] = PathfinderScript.find_path_with_fog( + game_map, + _selected_unit.position, + hovered_axial, + preview_budget, + _selected_unit.unit_type, + scouted, + ) + if path.is_empty(): + _unit_renderer.clear_path_preview() + return + var turns: int = _estimate_turns_for_path(path, movement_total) + _unit_renderer.show_path_preview(_selected_unit.position, path, turns) + + +## Confirm a movement-mode move targeting `target`. Falls back to the +## existing `_move_unit_to` once a reachable hex is reached this turn; +## paths that exceed the current movement budget bank the unit at the +## furthest reachable hex along the path so the player can resume next turn. +func _confirm_movement(target: Vector2i) -> void: + if _selected_unit == null: + _exit_movement_mode() + return + if _reachable_hexes.has(target): + _exit_movement_mode() + _move_unit_to(_selected_unit, target) + return + ## Walk the previewed path forward, picking the furthest reachable hex. + var furthest: Vector2i = _selected_unit.position + for axial: Vector2i in _path_preview_for_target(target): + if _reachable_hexes.has(axial): + furthest = axial + else: + break + if furthest != _selected_unit.position: + _exit_movement_mode() + _move_unit_to(_selected_unit, furthest) + else: + _exit_movement_mode() + + +func _path_preview_for_target(target: Vector2i) -> Array[Vector2i]: + var game_map: RefCounted = GameState.get_game_map() + if game_map == null or _selected_unit == null: + return [] as Array[Vector2i] + var scouted: Dictionary = _build_scouted_dict() + return PathfinderScript.find_path_with_fog( + game_map, + _selected_unit.position, + target, + 9999, + _selected_unit.unit_type, + scouted, + ) + + +func _build_scouted_dict() -> Dictionary: + ## Build a {Vector2i -> bool} dict of scouted/visible tiles for the local + ## player so PathfinderScript.find_path_with_fog can A* through them and + ## hex_line through the rest. Empty when no game map is present. + var out: Dictionary = {} + var game_map: RefCounted = GameState.get_game_map() + if game_map == null: + return out + var player: RefCounted = GameState.get_current_player() + if player == null: + return out + var pid: int = int(player.index) + for axial: Vector2i in game_map.tiles: + var tile: RefCounted = game_map.tiles[axial] as RefCounted + if tile == null: + continue + if tile.has_method("get_visibility"): + var vis: int = int(tile.get_visibility(pid)) + out[axial] = vis >= 1 + return out + + +func _estimate_turns_for_path(path: Array[Vector2i], movement_per_turn: int) -> int: + ## Approximate turn count along a fog-aware preview. Tiles outside the + ## current `_reachable_hexes` map are treated as cost-1 because their + ## actual cost cannot be known until they're scouted. + if movement_per_turn <= 0: + return path.size() + var cost_so_far: int = 0 + var turns: int = 1 + for axial: Vector2i in path: + var step_cost: int = int(_reachable_hexes.get(axial, 1)) + if step_cost <= 0: + step_cost = 1 + if cost_so_far + step_cost > movement_per_turn: + turns += 1 + cost_so_far = step_cost + else: + cost_so_far += step_cost + return turns + + +# -- Unit panel button bridges -------------------------------------------- + + +func _on_move_pressed() -> void: + if _selected_unit != null and _selected_unit_has_movement(): + _enter_movement_mode() + + +func _on_fortify_pressed_from_panel() -> void: + if _selected_unit == null: + return + if _selected_unit is UnitScript and _selected_unit.has_method("fortify"): + (_selected_unit as UnitScript).fortify() + EventBus.unit_selected.emit(_selected_unit) + + +func _on_skip_pressed_from_panel() -> void: + if _selected_unit == null: + return + if _selected_unit is UnitScript: + (_selected_unit as UnitScript).movement_remaining = 0 + EventBus.unit_selected.emit(_selected_unit) + _deselect_unit() diff --git a/src/game/engine/scenes/world_map/world_map_hover.gd b/src/game/engine/scenes/world_map/world_map_hover.gd index cadc242d..eaf55bdc 100644 --- a/src/game/engine/scenes/world_map/world_map_hover.gd +++ b/src/game/engine/scenes/world_map/world_map_hover.gd @@ -14,10 +14,14 @@ var _panel: PanelContainer = null var _timer: float = 0.0 var _last_axial: Vector2i = Vector2i(-9999, -9999) var _viewport_manager: Control = null +## p0-35: weak reference to the parent world_map controller so the hover +## tick can push the cursor's hex into the movement-mode preview pipeline. +var _world_map: Node2D = null func setup(parent: Node2D, viewport_mgr: Control) -> void: _viewport_manager = viewport_mgr + _world_map = parent _panel = TileInfoPanelScene.instantiate() parent.add_child(_panel) @@ -36,6 +40,10 @@ func tick(delta: float) -> void: if axial == _last_axial: return _last_axial = axial + ## p0-35: every hover tick refreshes the movement-mode path preview, even + ## when the underlying tile is null (off-map → preview clears). + if _world_map != null and _world_map.has_method("update_path_preview"): + _world_map.update_path_preview(axial) var tile: Resource = game_map.get_tile(axial) as Resource if tile == null: _panel.hide_panel()