diff --git a/play.html b/play.html index d4becc6..c9ee233 100644 --- a/play.html +++ b/play.html @@ -231,6 +231,12 @@ background: #000; box-shadow: 0 0 42px rgba(0, 0, 0, 0.95), 0 0 0 1px #2e251b; overflow: hidden; + cursor: none; + } + + #play-stage, + #play-stage * { + cursor: none !important; } #bnet-shell { @@ -241,6 +247,7 @@ align-items: center; justify-content: center; background: #000; + cursor: none; } #bnet-canvas { @@ -251,6 +258,7 @@ aspect-ratio: 4 / 3; display: block; image-rendering: pixelated; + cursor: none; } #view { @@ -262,6 +270,7 @@ background: #080706; z-index: 5; image-rendering: pixelated; + cursor: none; } #d2-hud-canvas { @@ -273,6 +282,7 @@ z-index: 10; pointer-events: none; image-rendering: pixelated; + cursor: none; } #play-disconnect-banner { diff --git a/src/client/frontend/frontend-renderer.ts b/src/client/frontend/frontend-renderer.ts index 5ecb60a..d688eb4 100644 --- a/src/client/frontend/frontend-renderer.ts +++ b/src/client/frontend/frontend-renderer.ts @@ -321,6 +321,9 @@ export class FrontendRenderer { constructor(canvas: HTMLCanvasElement, state: BnetFlowState) { this.canvas = canvas + if (this.canvas && this.canvas.style) { + this.canvas.style.cursor = 'none' + } const ctx = canvas.getContext('2d') if (!ctx) throw new Error('Failed to acquire 2D context for bnet-canvas') ctx.imageSmoothingEnabled = false diff --git a/src/client/main.ts b/src/client/main.ts index dea2fb8..411da41 100644 --- a/src/client/main.ts +++ b/src/client/main.ts @@ -148,6 +148,12 @@ export async function startPlayApp(options?: { throw new Error('Missing required DOM elements in play.html') } + for (const el of [stageEl, bnetShell, bnetCanvas, viewCanvas, hudCanvas]) { + if (el.style) { + el.style.cursor = 'none' + } + } + const bootProgress = new BootProgressTracker({ stageEl }).install() const settingsStore = getGlobalSettingsStore() @@ -350,7 +356,7 @@ export async function startPlayApp(options?: { chatInputEl.placeholder = 'Say...' chatInputEl.hidden = true chatInputEl.style.cssText = - 'position:absolute;left:24px;bottom:64px;width:360px;max-width:calc(100% - 48px);box-sizing:border-box;z-index:25;background:rgba(10,8,6,0.92);border:1px solid #c8a858;color:#f2e6c8;padding:4px 8px;font:13px "D2Formal436",serif;outline:none;' + 'position:absolute;left:24px;bottom:64px;width:360px;max-width:calc(100% - 48px);box-sizing:border-box;z-index:25;background:rgba(10,8,6,0.92);border:1px solid #c8a858;color:#f2e6c8;padding:4px 8px;font:13px "D2Formal436",serif;outline:none;cursor:none;' stageEl.appendChild(chatInputEl) const toggleInGameChatInput = (): void => { diff --git a/tests/cursor.test.ts b/tests/cursor.test.ts index df5dd84..869e530 100644 --- a/tests/cursor.test.ts +++ b/tests/cursor.test.ts @@ -32,6 +32,8 @@ import { } from '../src/common/items/cursor-meta.ts' import { BAKED_UI_MANIFEST } from '../src/common/items/baked-ui-meta.ts' import { HudManager, type HudCommandSink } from '../src/client/ui/hud-manager.ts' +import { FrontendRenderer } from '../src/client/frontend/frontend-renderer.ts' +import { createInitialFlowState } from '../src/client/frontend/flow-state.ts' import { openGeneratedVendorShop } from './helpers/vendor-shop-fixture.ts' import { VENDOR_BUTTON_TABLE_X } from '../src/client/ui/world-panels.ts' import { seedDemoHud } from './fixtures/hud-demo-state.ts' @@ -522,6 +524,33 @@ describe('Diablo II v1.13c Dynamic Cursor System (Issue #148 & #491)', () => { expect(hud.state.cursorTradeMode).toBeNull() expect(hud.state.cursorAnimation).toBe('protate') }) + + it('hides the native OS triangle cursor across #play-stage, #view, #bnet-canvas, and #d2-hud-canvas per D2gfx.dll #10029 (0x6fa88290) (Issue #718)', () => { + // 1. FrontendRenderer hides native OS cursor on #bnet-canvas (D2Win.dll 0x6f8f7800 -> D2gfx.#10029) + const bnetCanvas = { + width: 800, + height: 600, + style: { cursor: 'default' }, + getContext: () => ({ imageSmoothingEnabled: true }), + } as unknown as HTMLCanvasElement + const fe = new FrontendRenderer(bnetCanvas, createInitialFlowState()) + expect(fe.canvas.style.cursor).toBe('none') + + // 2. HudManager hides native OS cursor on #d2-hud-canvas (D2Client.dll 0x6fac63d0 -> D2gfx.#10029) + const { mockCanvas: hudCanvas } = createTestHud() + expect(hudCanvas.style.cursor).toBe('none') + + // 3. play.html enforces cursor: none on #play-stage and all child canvases (#bnet-shell, #bnet-canvas, #view, #d2-hud-canvas) + // so pointer-events: none on #d2-hud-canvas over the game world never exposes the OS cursor on #view + const playHtml = readFileSync(join(process.cwd(), 'play.html'), 'utf8') + expect(playHtml).toMatch(/#play-stage\s*\{[^}]*cursor:\s*none;/s) + expect(playHtml).toMatch(/#play-stage,\s*#play-stage\s*\*\s*\{[^}]*cursor:\s*none\s*!important;/s) + expect(playHtml).toMatch(/#bnet-shell\s*\{[^}]*cursor:\s*none;/s) + expect(playHtml).toMatch(/#bnet-canvas\s*\{[^}]*cursor:\s*none;/s) + expect(playHtml).toMatch(/#view\s*\{[^}]*cursor:\s*none;/s) + expect(playHtml).toMatch(/#d2-hud-canvas\s*\{[^}]*cursor:\s*none;/s) + }) }) }) +