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)
+ })
})
})
+