169 lines
5.7 KiB
TypeScript
169 lines
5.7 KiB
TypeScript
/**
|
|
* Adversarial Challenger Stress Testing Suite for Milestone 2 Left-Click & Shift Controls
|
|
* (Solution Stress Testing Playbook: Differential Fuzzing, Boundary Values, Invariant Checks)
|
|
*/
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
import { HudManager } from '../src/client/ui/hud-manager.ts'
|
|
import { GameEngine, type GameEngineOptions, type WorldMapProvider } from '../src/server/engine/engine.ts'
|
|
import { WaypointNetwork } from '../src/common/world/portal.ts'
|
|
import { screenToWorld, facingOf } from '../src/common/world/scene-links-nav.ts'
|
|
import { getSkillRange, getSkillMana } from '../src/common/skills/weapon-combat-meta.ts'
|
|
|
|
class MockWindow extends EventTarget {
|
|
innerWidth = 1920
|
|
innerHeight = 1080
|
|
devicePixelRatio = 1
|
|
}
|
|
|
|
class MockCanvas extends EventTarget {
|
|
clientWidth = 1920
|
|
clientHeight = 1080
|
|
width = 1920
|
|
height = 1080
|
|
style = { pointerEvents: 'none' }
|
|
rect = { left: 0, top: 0, width: 1920, height: 1080 }
|
|
getBoundingClientRect() {
|
|
return this.rect
|
|
}
|
|
getContext() {
|
|
return null
|
|
}
|
|
}
|
|
|
|
class SyntheticKeyboardEvent extends Event {
|
|
readonly code: string
|
|
readonly key: string
|
|
|
|
constructor(type: string, init: { code: string; key: string }) {
|
|
super(type, { cancelable: true })
|
|
this.code = init.code
|
|
this.key = init.key
|
|
}
|
|
}
|
|
|
|
class SyntheticPointerEvent extends Event {
|
|
readonly clientX: number
|
|
readonly clientY: number
|
|
readonly button: number
|
|
readonly buttons: number
|
|
readonly shiftKey: boolean
|
|
private _defaultPrevented = false
|
|
|
|
constructor(type: string, init: { clientX: number; clientY: number; button?: number; buttons?: number; shiftKey?: boolean; cancelable?: boolean }) {
|
|
super(type, { cancelable: init.cancelable ?? true })
|
|
this.clientX = init.clientX
|
|
this.clientY = init.clientY
|
|
this.button = init.button ?? 0
|
|
this.buttons = init.buttons !== undefined ? init.buttons : (1 << (init.button ?? 0))
|
|
this.shiftKey = init.shiftKey ?? false
|
|
}
|
|
|
|
override get defaultPrevented(): boolean {
|
|
return this._defaultPrevented
|
|
}
|
|
|
|
override preventDefault() {
|
|
super.preventDefault()
|
|
this._defaultPrevented = true
|
|
}
|
|
}
|
|
|
|
const mockWin = new MockWindow()
|
|
// @ts-ignore
|
|
globalThis.window = mockWin
|
|
// @ts-ignore
|
|
globalThis.HTMLInputElement = class HTMLInputElement {}
|
|
// @ts-ignore
|
|
globalThis.HTMLSelectElement = class HTMLSelectElement {}
|
|
|
|
describe('Challenger Stress Tests: Milestone 2 Left-Click & Shift Controls', () => {
|
|
describe('1. screenToWorld projection with varying camera zoom levels and DPRs', () => {
|
|
it('exact round-trip invertibility across zoom levels [0.4x..8.0x] and DPRs [1.0..4.0]', () => {
|
|
const zooms = [0.4, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0]
|
|
const dprs = [1.0, 1.25, 1.333, 1.5, 2.0, 2.5, 3.0, 4.0]
|
|
const viewports = [
|
|
{ width: 800, height: 600, left: 0, top: 0 },
|
|
{ width: 1280, height: 720, left: 10, top: 20 },
|
|
{ width: 1920, height: 1080, left: 100, top: 50 },
|
|
{ width: 2560, height: 1440, left: 0, top: 0 },
|
|
{ width: 3440, height: 1440, left: 50, top: 0 }, // ultrawide
|
|
]
|
|
|
|
let testIterations = 0
|
|
for (const zoom of zooms) {
|
|
for (const dpr of dprs) {
|
|
for (const vp of viewports) {
|
|
const rect = { left: vp.left, top: vp.top, width: vp.width, height: vp.height }
|
|
const canvas = {
|
|
width: Math.round(vp.width * dpr),
|
|
height: Math.round(vp.height * dpr),
|
|
}
|
|
const camX = 1500
|
|
const camY = 1200
|
|
|
|
// Test 20 random points per configuration
|
|
for (let i = 0; i < 20; i++) {
|
|
testIterations++
|
|
const originalWorldX = 500 + (i * 97) % 2000
|
|
const originalWorldY = 400 + (i * 113) % 2000
|
|
|
|
// Forward projection to screen client coordinates:
|
|
const scaleX = canvas.width > 0 ? rect.width / canvas.width : 1
|
|
const scaleY = canvas.height > 0 ? rect.height / canvas.height : 1
|
|
const clientX = rect.left + rect.width * 0.5 + (originalWorldX - camX) * zoom * scaleX
|
|
const clientY = rect.top + rect.height * 0.5 + (originalWorldY - camY) * zoom * scaleY
|
|
|
|
// Invert back via screenToWorld:
|
|
const unprojected = screenToWorld({
|
|
clientX,
|
|
clientY,
|
|
rect,
|
|
canvas,
|
|
camX,
|
|
camY,
|
|
cameraZoom: zoom,
|
|
})
|
|
|
|
expect(Math.abs(unprojected.x - originalWorldX)).toBeLessThan(1e-4)
|
|
expect(Math.abs(unprojected.y - originalWorldY)).toBeLessThan(1e-4)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
expect(testIterations).toBe(zooms.length * dprs.length * viewports.length * 20)
|
|
})
|
|
|
|
it('center of viewport strictly projects to camera focal position regardless of zoom or DPR', () => {
|
|
const dprs = [1.0, 1.5, 2.0, 3.0]
|
|
const zooms = [0.4, 1.0, 2.5, 8.0]
|
|
|
|
for (const dpr of dprs) {
|
|
for (const zoom of zooms) {
|
|
const rect = { left: 80, top: 40, width: 1600, height: 900 }
|
|
const canvas = { width: Math.round(1600 * dpr), height: Math.round(900 * dpr) }
|
|
const camX = 777.5
|
|
const camY = 888.25
|
|
|
|
const centerClientX = rect.left + rect.width * 0.5 // 80 + 800 = 880
|
|
const centerClientY = rect.top + rect.height * 0.5 // 40 + 450 = 490
|
|
|
|
const world = screenToWorld({
|
|
clientX: centerClientX,
|
|
clientY: centerClientY,
|
|
rect,
|
|
canvas,
|
|
camX,
|
|
camY,
|
|
cameraZoom: zoom,
|
|
})
|
|
|
|
expect(world.x).toBeCloseTo(camX, 5)
|
|
expect(world.y).toBeCloseTo(camY, 5)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
})
|