@ganziliang/desktop-pet 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,120 @@
1
+ # drop-probe.ps1 -- does the pet stay where you drop it, or fall to the bottom?
2
+ #
3
+ # Presses on the character, drags it UP, releases, then waits and re-reads the
4
+ # window rect. With gravity off it must stay where it was released; with gravity
5
+ # on it must end up on the bottom edge of the work area.
6
+ #
7
+ # NOTE: keep this file pure ASCII (PowerShell 5.1 reads .ps1 as ANSI).
8
+
9
+ param(
10
+ [string]$Hwnd,
11
+ [int]$RisePx = 260,
12
+ [int]$SettleMs = 1500,
13
+ [ValidateSet('stay','fall')][string]$Expect = 'stay'
14
+ )
15
+ $ErrorActionPreference = 'Stop'
16
+ $root = Split-Path -Parent $PSScriptRoot
17
+ $report = Join-Path $root 'drop-probe-report.txt'
18
+ $out = New-Object System.Collections.Generic.List[string]
19
+ function Say($t) { $out.Add($t) | Out-Null; Write-Host $t }
20
+
21
+ Add-Type @"
22
+ using System;
23
+ using System.Runtime.InteropServices;
24
+ public class PetDrop {
25
+ [DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
26
+ [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT r);
27
+ [DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
28
+ [DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, IntPtr dwExtraInfo);
29
+ [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
30
+ }
31
+ "@
32
+ [void][PetDrop]::SetProcessDPIAware()
33
+
34
+ $MOVE = 0x0001; $LDOWN = 0x0002; $LUP = 0x0004
35
+
36
+ $health = Invoke-RestMethod -Uri 'http://127.0.0.1:8520/health' -TimeoutSec 3
37
+ if (-not $Hwnd -and $health.hwnd) { $Hwnd = [string]$health.hwnd }
38
+ if (-not $Hwnd) { Say 'FAIL: no hwnd'; $out | Set-Content -Encoding ASCII $report; exit 1 }
39
+
40
+ # take manual control so autonomous roaming does not fight the measurement, but remember
41
+ # the previous value: the setting is persisted, so leaving it off would silently turn the
42
+ # pet's autonomous behaviour off for good.
43
+ $autoWas = $health.autoRoam
44
+ if ($null -eq $autoWas) { $autoWas = $true }
45
+ Invoke-RestMethod -Uri 'http://127.0.0.1:8520/auto' -Method Post -ContentType 'application/json' -Body '{"enabled":false}' | Out-Null
46
+ Start-Sleep -Milliseconds 400
47
+
48
+ $h = [IntPtr][long]$Hwnd
49
+ $r1 = New-Object PetDrop+RECT
50
+ [void][PetDrop]::GetWindowRect($h, [ref]$r1)
51
+ $w = $r1.Right - $r1.Left
52
+ $hgt = $r1.Bottom - $r1.Top
53
+ Say ("before : ({0},{1}) size {2}x{3}" -f $r1.Left, $r1.Top, $w, $hgt)
54
+
55
+ $px = [int]($r1.Left + $w * 0.5)
56
+ $py = [int]($r1.Top + $hgt * 0.62)
57
+ [void][PetDrop]::SetCursorPos($px, $py)
58
+ Start-Sleep -Milliseconds 250
59
+ [void][PetDrop]::mouse_event($LDOWN, 0, 0, 0, [IntPtr]::Zero)
60
+ Start-Sleep -Milliseconds 180
61
+
62
+ $steps = 10
63
+ for ($i = 1; $i -le $steps; $i++) {
64
+ [void][PetDrop]::SetCursorPos($px, $py - [int]($RisePx * $i / $steps))
65
+ Start-Sleep -Milliseconds 35
66
+ }
67
+ [void][PetDrop]::mouse_event($LUP, 0, 0, 0, [IntPtr]::Zero)
68
+ Say ("dropped {0}px above the start position" -f $RisePx)
69
+
70
+ Start-Sleep -Milliseconds $SettleMs
71
+ $r2 = New-Object PetDrop+RECT
72
+ [void][PetDrop]::GetWindowRect($h, [ref]$r2)
73
+ $rise = $r1.Top - $r2.Top # positive = ended up above the start position
74
+ Say ("after : ({0},{1}) rise={2}px above start" -f $r2.Left, $r2.Top, $rise)
75
+
76
+ # The drag is a burst of synthetic mouse events and may drop a frame or two, so do not
77
+ # require an exact landing point. Two things are worth asserting:
78
+ # stay -> it ended up well above where it started (never got pulled down)
79
+ # fall -> its bottom edge is sitting on the bottom of the work area
80
+ # Work area comes from /health in DIP; the window rect is physical, hence scaleFactor.
81
+ $cx = ($r2.Left + $r2.Right) / 2.0
82
+ $cy = ($r2.Top + $r2.Bottom) / 2.0
83
+ $disp = $null
84
+ foreach ($d in $health.displays) {
85
+ $sf = $d.scaleFactor
86
+ $dx = $cx / $sf; $dy = $cy / $sf
87
+ if ($dx -ge $d.bounds.x -and $dx -le ($d.bounds.x + $d.bounds.width) -and
88
+ $dy -ge $d.bounds.y -and $dy -le ($d.bounds.y + $d.bounds.height)) { $disp = $d; break }
89
+ }
90
+ $floorBottom = $null
91
+ if ($disp) {
92
+ $floorBottom = ($disp.workArea.y + $disp.workArea.height) * $disp.scaleFactor
93
+ Say ("work area bottom (physical) = {0}, window bottom = {1}" -f $floorBottom, $r2.Bottom)
94
+ }
95
+
96
+ $ok = $true
97
+ if ($Expect -eq 'stay') {
98
+ if ($rise -gt [int]($RisePx * 0.6)) {
99
+ Say 'RESULT: PASS - pet stayed where it was dropped'
100
+ } else {
101
+ Say ("RESULT: FAIL - pet did not stay up (rise={0}, expected about {1})" -f $rise, $RisePx)
102
+ $ok = $false
103
+ }
104
+ } else {
105
+ if ($null -ne $floorBottom -and [Math]::Abs($r2.Bottom - $floorBottom) -le 20) {
106
+ Say 'RESULT: PASS - pet fell back to the ground line'
107
+ } else {
108
+ Say 'RESULT: FAIL - pet is not sitting on the bottom of the work area'
109
+ $ok = $false
110
+ }
111
+ }
112
+
113
+ # always put the autonomous setting back, even when the check failed
114
+ if ($autoWas) {
115
+ Invoke-RestMethod -Uri 'http://127.0.0.1:8520/auto' -Method Post -ContentType 'application/json' -Body '{"enabled":true}' | Out-Null
116
+ Say 'restored autoRoam=true'
117
+ }
118
+
119
+ $out | Set-Content -Encoding ASCII $report
120
+ if (-not $ok) { exit 1 }
@@ -0,0 +1,86 @@
1
+ # Hold-probe: press and HOLD the left button on the pet without moving the cursor,
2
+ # sampling the window rect every 200ms. Tells us whether the WINDOW grows or the
3
+ # rendered content is being zoomed. ASCII only (PS 5.1 reads .ps1 as ANSI).
4
+ param(
5
+ [string]$Hwnd,
6
+ [int]$HoldMs = 4000,
7
+ [int]$StepMs = 200,
8
+ [int]$JitterPx = 0,
9
+ [switch]$NoPress
10
+ )
11
+
12
+ $ErrorActionPreference = 'Stop'
13
+ $root = Split-Path -Parent $PSScriptRoot
14
+ $report = Join-Path $root 'hold-probe-report.txt'
15
+ $out = New-Object System.Collections.Generic.List[string]
16
+ function Say($t) { $out.Add($t) | Out-Null; Write-Host $t }
17
+
18
+ Add-Type @"
19
+ using System;
20
+ using System.Runtime.InteropServices;
21
+ public class HoldWin {
22
+ [DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
23
+ [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT r);
24
+ [DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
25
+ [DllImport("user32.dll")] public static extern bool GetCursorPos(out RECTpt p);
26
+ [DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, IntPtr dwExtraInfo);
27
+ [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
28
+ [StructLayout(LayoutKind.Sequential)] public struct RECTpt { public int X; public int Y; }
29
+ }
30
+ "@
31
+
32
+ [void][HoldWin]::SetProcessDPIAware()
33
+
34
+ $LDOWN = 0x0002
35
+ $LUP = 0x0004
36
+
37
+ if (-not $Hwnd) {
38
+ try {
39
+ $health = Invoke-RestMethod -Uri 'http://127.0.0.1:8520/health' -TimeoutSec 3
40
+ if ($health.hwnd) { $Hwnd = [string]$health.hwnd }
41
+ } catch { }
42
+ }
43
+ if (-not $Hwnd) { Say 'FAIL: no hwnd (is the app running?)'; $out | Set-Content -Encoding ASCII $report; exit 1 }
44
+
45
+ $h = [IntPtr][long]$Hwnd
46
+ $r = New-Object HoldWin+RECT
47
+ [void][HoldWin]::GetWindowRect($h, [ref]$r)
48
+ Say ("HWND={0}" -f $Hwnd)
49
+ Say ("initial: ({0},{1})-({2},{3}) size {4}x{5}" -f $r.Left, $r.Top, $r.Right, $r.Bottom, ($r.Right - $r.Left), ($r.Bottom - $r.Top))
50
+
51
+ $px = [int]($r.Left + ($r.Right - $r.Left) * 0.50)
52
+ $py = [int]($r.Top + ($r.Bottom - $r.Top) * 0.58)
53
+ Say ("press point: ({0},{1})" -f $px, $py)
54
+
55
+ [void][HoldWin]::SetCursorPos($px, $py)
56
+ Start-Sleep -Milliseconds 400
57
+ $c = New-Object HoldWin+RECTpt
58
+ [void][HoldWin]::GetCursorPos([ref]$c)
59
+ Say ("cursor: {0},{1}" -f $c.X, $c.Y)
60
+
61
+ if (-not $NoPress) { [HoldWin]::mouse_event($LDOWN, 0, 0, 0, [IntPtr]::Zero) }
62
+
63
+ $n = [int]($HoldMs / $StepMs)
64
+ for ($i = 1; $i -le $n; $i++) {
65
+ if ($JitterPx -gt 0) {
66
+ $d = if ($i % 2 -eq 0) { $JitterPx } else { -$JitterPx }
67
+ [HoldWin]::mouse_event(0x0001, $d, 0, 0, [IntPtr]::Zero)
68
+ }
69
+ Start-Sleep -Milliseconds $StepMs
70
+ $s = New-Object HoldWin+RECT
71
+ [void][HoldWin]::GetWindowRect($h, [ref]$s)
72
+ [void][HoldWin]::GetCursorPos([ref]$c)
73
+ Say ("t={0,5}ms rect=({1},{2})-({3},{4}) size {5}x{6} cursor=({7},{8})" -f `
74
+ ($i * $StepMs), $s.Left, $s.Top, $s.Right, $s.Bottom, ($s.Right - $s.Left), ($s.Bottom - $s.Top), $c.X, $c.Y)
75
+ }
76
+
77
+ if (-not $NoPress) {
78
+ [HoldWin]::mouse_event($LUP, 0, 0, 0, [IntPtr]::Zero)
79
+ Start-Sleep -Milliseconds 600
80
+ $e = New-Object HoldWin+RECT
81
+ [void][HoldWin]::GetWindowRect($h, [ref]$e)
82
+ Say ("after release: size {0}x{1}" -f ($e.Right - $e.Left), ($e.Bottom - $e.Top))
83
+ }
84
+
85
+ $out | Set-Content -Encoding ASCII $report
86
+ Write-Host ("report: {0}" -f $report)
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env python
2
+ """绿幕抠图:把纯绿背景的动漫立绘变成带 alpha 的通明 PNG。
3
+
4
+ 用法:
5
+ python scripts/knockout.py assets/character_green.png assets/character.png --height 640
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ from pathlib import Path
12
+
13
+ from PIL import Image
14
+
15
+ try: # numpy 有就快很多
16
+ import numpy as np
17
+ except ImportError: # pragma: no cover
18
+ np = None
19
+
20
+
21
+ def key_out(img: Image.Image, lo: int, hi: int) -> Image.Image:
22
+ """greenness = G - max(R, B),越大越绿。lo 以下保留,hi 以上全透明,中间做软过渡。"""
23
+ img = img.convert("RGBA")
24
+
25
+ if np is not None:
26
+ arr = np.asarray(img).astype(np.int16)
27
+ r, g, b, a = arr[..., 0], arr[..., 1], arr[..., 2], arr[..., 3]
28
+ greenness = g - np.maximum(r, b)
29
+ ramp = np.clip((hi - greenness) / float(hi - lo), 0.0, 1.0)
30
+ alpha = (a * ramp).astype(np.uint8)
31
+ # 边缘去绿溢色:半透明像素里把 G 压到不超过 max(R, B)
32
+ spill = (alpha < 250) & (g > np.maximum(r, b))
33
+ g = np.where(spill, np.maximum(r, b), g)
34
+ out = np.stack([r, g, b, alpha.astype(np.int16)], axis=-1).astype(np.uint8)
35
+ return Image.fromarray(out, "RGBA")
36
+
37
+ px = img.load()
38
+ out = Image.new("RGBA", img.size)
39
+ op = out.load()
40
+ for y in range(img.height):
41
+ for x in range(img.width):
42
+ r, g, b, a = px[x, y]
43
+ greenness = g - max(r, b)
44
+ if greenness <= lo:
45
+ alpha = a
46
+ elif greenness >= hi:
47
+ alpha = 0
48
+ else:
49
+ alpha = int(a * (hi - greenness) / float(hi - lo))
50
+ if 0 < alpha < 250 and g > max(r, b):
51
+ g = max(r, b)
52
+ op[x, y] = (r, g, b, alpha)
53
+ return out
54
+
55
+
56
+ def trim(img: Image.Image, pad: int = 6) -> Image.Image:
57
+ bbox = img.getchannel("A").point(lambda v: 255 if v > 8 else 0).getbbox()
58
+ if not bbox:
59
+ return img
60
+ left, top, right, bottom = bbox
61
+ return img.crop(
62
+ (
63
+ max(0, left - pad),
64
+ max(0, top - pad),
65
+ min(img.width, right + pad),
66
+ min(img.height, bottom + pad),
67
+ )
68
+ )
69
+
70
+
71
+ def main() -> None:
72
+ ap = argparse.ArgumentParser()
73
+ ap.add_argument("src")
74
+ ap.add_argument("dst")
75
+ ap.add_argument("--height", type=int, default=640, help="输出高度(等比缩放)")
76
+ ap.add_argument("--lo", type=int, default=30, help="绿度低于此值完全保留")
77
+ ap.add_argument("--hi", type=int, default=72, help="绿度高于此值完全透明")
78
+ args = ap.parse_args()
79
+
80
+ img = Image.open(args.src)
81
+ cut = key_out(img, args.lo, args.hi)
82
+ cut = trim(cut)
83
+
84
+ if args.height and cut.height > args.height:
85
+ w = round(cut.width * args.height / cut.height)
86
+ cut = cut.resize((w, args.height), Image.LANCZOS)
87
+
88
+ dst = Path(args.dst)
89
+ dst.parent.mkdir(parents=True, exist_ok=True)
90
+ cut.save(dst, "PNG", optimize=True)
91
+
92
+ opaque = cut.getchannel("A").point(lambda v: 255 if v > 200 else 0)
93
+ coverage = sum(opaque.histogram()[255:]) / float(cut.width * cut.height)
94
+ print(f"[knockout] {dst} {cut.width}x{cut.height} 角色像素占比 {coverage:.1%}")
95
+
96
+
97
+ if __name__ == "__main__":
98
+ main()
@@ -0,0 +1,95 @@
1
+ # roam-probe.ps1 -- end-to-end check for the autonomous locomotion feature.
2
+ #
3
+ # It triggers a roam over the local HTTP interface and samples the real window
4
+ # rectangle from /health while the pet is moving, so we can prove:
5
+ # 1) the window actually travels a meaningful distance
6
+ # 2) the window size never drifts (the mixed-DPI ratchet regression)
7
+ # 3) the pet ends up back on the "ground line" (bottom of the work area)
8
+ #
9
+ # NOTE: keep this file pure ASCII. PowerShell 5.1 reads .ps1 as ANSI, so any
10
+ # non-ASCII comment breaks the parser (see README "known pitfalls").
11
+
12
+ param(
13
+ [string]$Mode = 'run',
14
+ [int]$Samples = 14,
15
+ [int]$IntervalMs = 200,
16
+ [int]$MinTravel = 90,
17
+ [int]$SizeSlack = 4
18
+ )
19
+
20
+ $ErrorActionPreference = 'Stop'
21
+ $base = 'http://127.0.0.1:8520'
22
+
23
+ function Get-Health {
24
+ $r = Invoke-RestMethod -Uri "$base/health" -TimeoutSec 5
25
+ return $r
26
+ }
27
+
28
+ function Post-Json([string]$path, [string]$body) {
29
+ return Invoke-RestMethod -Uri "$base$path" -Method Post -ContentType 'application/json' -Body $body -TimeoutSec 5
30
+ }
31
+
32
+ Write-Host "== desktop-pet roam probe =="
33
+
34
+ try {
35
+ $h = Get-Health
36
+ } catch {
37
+ Write-Host "RESULT: FAIL - HTTP interface not reachable at $base (is the pet running?)"
38
+ exit 1
39
+ }
40
+
41
+ Write-Host ("hwnd={0} scale={1}" -f $h.hwnd, $h.scale)
42
+
43
+ # Take manual control: the autonomous scheduler would fight us otherwise.
44
+ Post-Json '/auto' '{"enabled":false}' | Out-Null
45
+ Post-Json '/halt' '{}' | Out-Null
46
+ Start-Sleep -Milliseconds 600
47
+
48
+ $start = Get-Health
49
+ $sw = $start.bounds.width
50
+ $sh = $start.bounds.height
51
+ $sx = $start.bounds.x
52
+ $sy = $start.bounds.y
53
+ Write-Host ("start : x={0} y={1} size={2}x{3}" -f $sx, $sy, $sw, $sh)
54
+
55
+ Post-Json '/roam' ("{""mode"":""$Mode""}") | Out-Null
56
+
57
+ $xs = @()
58
+ $sizes = @()
59
+ for ($i = 0; $i -lt $Samples; $i++) {
60
+ Start-Sleep -Milliseconds $IntervalMs
61
+ try {
62
+ $b = (Get-Health).bounds
63
+ } catch {
64
+ continue
65
+ }
66
+ $xs += $b.x
67
+ $sizes += ("{0}x{1}" -f $b.width, $b.height)
68
+ }
69
+
70
+ $end = (Get-Health).bounds
71
+ $minX = ($xs | Measure-Object -Minimum).Minimum
72
+ $maxX = ($xs | Measure-Object -Maximum).Maximum
73
+ $travel = [Math]::Abs($end.x - $sx)
74
+ $span = $maxX - $minX
75
+ $badSize = @($sizes | Where-Object { $_ -ne "$sw`x$sh" })
76
+ $maxW = ($sizes | ForEach-Object { [int]($_ -split 'x')[0] } | Measure-Object -Maximum).Maximum
77
+ $minW = ($sizes | ForEach-Object { [int]($_ -split 'x')[0] } | Measure-Object -Minimum).Minimum
78
+ $maxH = ($sizes | ForEach-Object { [int]($_ -split 'x')[1] } | Measure-Object -Maximum).Maximum
79
+ $minH = ($sizes | ForEach-Object { [int]($_ -split 'x')[1] } | Measure-Object -Minimum).Minimum
80
+ $drift = [Math]::Max($maxW - $sw, [Math]::Max($sw - $minW, [Math]::Max($maxH - $sh, $sh - $minH)))
81
+
82
+ Write-Host ("end : x={0} y={1} size={2}x{3}" -f $end.x, $end.y, $end.width, $end.height)
83
+ Write-Host ("travel: {0} px (samples moved across {1} px, min={2} max={3})" -f $travel, $span, $minX, $maxX)
84
+ Write-Host ("size : drift={0} px (slack {1}), distinct={2}" -f $drift, $SizeSlack, (($sizes | Select-Object -Unique) -join ','))
85
+
86
+ if ($drift -gt $SizeSlack) {
87
+ Write-Host "RESULT: FAIL - window size drifted while roaming"
88
+ exit 1
89
+ }
90
+ if ($travel -lt $MinTravel) {
91
+ Write-Host "RESULT: FAIL - window did not move (travel=$travel, expected >= $MinTravel)"
92
+ exit 1
93
+ }
94
+
95
+ Write-Host "RESULT: PASS - roaming works"
@@ -0,0 +1,107 @@
1
+ # End-to-end drag test: locate the pet window by the HWND the app itself reports,
2
+ # press on the character body, drag, and compare window rect before/after.
3
+ # NOTE: ASCII only - Windows PowerShell 5.1 reads .ps1 as ANSI, CJK literals break parsing.
4
+ param([string]$Hwnd)
5
+
6
+ $ErrorActionPreference = 'Stop'
7
+ $root = Split-Path -Parent $PSScriptRoot
8
+ $report = Join-Path $root 'sim-drag-report.txt'
9
+ $out = New-Object System.Collections.Generic.List[string]
10
+ function Say($t) { $out.Add($t) | Out-Null; Write-Host $t }
11
+ function Cursor() { $p = New-Object PetWin+RECTpt; [void][PetWin]::GetCursorPos([ref]$p); return "$($p.X),$($p.Y)" }
12
+
13
+ Add-Type @"
14
+ using System;
15
+ using System.Runtime.InteropServices;
16
+ public class PetWin {
17
+ [DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
18
+ [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT r);
19
+ [DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
20
+ [DllImport("user32.dll")] public static extern bool GetCursorPos(out RECTpt p);
21
+ [DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, IntPtr dwExtraInfo);
22
+ [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
23
+ [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
24
+ [StructLayout(LayoutKind.Sequential)] public struct RECTpt { public int X; public int Y; }
25
+ }
26
+ "@
27
+
28
+ # PowerShell 默认 DPI-unaware,坐标会和 Chromium 错位,必须先声明 DPI 感知
29
+ [void][PetWin]::SetProcessDPIAware()
30
+
31
+ $MOVE = 0x0001
32
+ $LDOWN = 0x0002
33
+ $LUP = 0x0004
34
+
35
+ if (-not $Hwnd) {
36
+ # Live source of truth first: the running app reports its own handle. The debug
37
+ # log can be stale (it survives restarts), so it is only a fallback.
38
+ try {
39
+ $health = Invoke-RestMethod -Uri 'http://127.0.0.1:8520/health' -TimeoutSec 3
40
+ if ($health.hwnd) { $Hwnd = [string]$health.hwnd }
41
+ } catch { }
42
+ }
43
+
44
+ if (-not $Hwnd) {
45
+ $log = Join-Path $root 'pet-debug.log'
46
+ if (Test-Path $log) {
47
+ $m = Select-String -Path $log -Pattern 'hwnd=(\d+)' | Select-Object -Last 1
48
+ if ($m) { $Hwnd = $m.Matches[0].Groups[1].Value }
49
+ }
50
+ }
51
+
52
+ if (-not $Hwnd) { Say 'FAIL: no hwnd (start the app, or pass -Hwnd)'; $out | Set-Content -Encoding ASCII $report; exit 1 }
53
+
54
+ $h = [IntPtr][long]$Hwnd
55
+ Say ("HWND={0} visible={1}" -f $Hwnd, [PetWin]::IsWindowVisible($h))
56
+
57
+ $r1 = New-Object PetWin+RECT
58
+ [bool]$okRect = [PetWin]::GetWindowRect($h, [ref]$r1)
59
+ Say ("GetWindowRect ok={0}" -f $okRect)
60
+ Say ("before: ({0},{1})-({2},{3}) size {4}x{5}" -f $r1.Left, $r1.Top, $r1.Right, $r1.Bottom, ($r1.Right - $r1.Left), ($r1.Bottom - $r1.Top))
61
+
62
+ # target = character body (hoodie), 50% / 58% inside the window
63
+ $startX = [int]($r1.Left + ($r1.Right - $r1.Left) * 0.50)
64
+ $startY = [int]($r1.Top + ($r1.Bottom - $r1.Top) * 0.58)
65
+ Say ("press point: ({0},{1})" -f $startX, $startY)
66
+
67
+ [void][PetWin]::SetCursorPos(30, 600)
68
+ Start-Sleep -Milliseconds 250
69
+ [void][PetWin]::SetCursorPos($startX, $startY)
70
+ Start-Sleep -Milliseconds 300
71
+ Say ("cursor after positioning: {0}" -f (Cursor))
72
+ [PetWin]::mouse_event($LDOWN, 0, 0, 0, [IntPtr]::Zero)
73
+ Start-Sleep -Milliseconds 150
74
+ for ($i = 1; $i -le 12; $i++) {
75
+ [PetWin]::mouse_event($MOVE, -12, 7, 0, [IntPtr]::Zero)
76
+ Start-Sleep -Milliseconds 30
77
+ }
78
+ Say ("cursor after drag moves: {0} (dragged left/down on purpose)" -f (Cursor))
79
+ Start-Sleep -Milliseconds 200
80
+ [PetWin]::mouse_event($LUP, 0, 0, 0, [IntPtr]::Zero)
81
+ Start-Sleep -Milliseconds 500
82
+
83
+ $r2 = New-Object PetWin+RECT
84
+ [void][PetWin]::GetWindowRect($h, [ref]$r2)
85
+ $dx = $r2.Left - $r1.Left
86
+ $dy = $r2.Top - $r1.Top
87
+ Say ("after : ({0},{1}) delta=({2},{3})" -f $r2.Left, $r2.Top, $dx, $dy)
88
+
89
+ # extra: single click, to check whether pointer events reach the renderer at all
90
+ # recompute from the CURRENT rect - the drag moved the window
91
+ $r3 = New-Object PetWin+RECT
92
+ [void][PetWin]::GetWindowRect($h, [ref]$r3)
93
+ $clickX = [int]($r3.Left + ($r3.Right - $r3.Left) * 0.50)
94
+ $clickY = [int]($r3.Top + ($r3.Bottom - $r3.Top) * 0.58)
95
+ [void][PetWin]::SetCursorPos(30, 600)
96
+ Start-Sleep -Milliseconds 250
97
+ [void][PetWin]::SetCursorPos($clickX, $clickY)
98
+ Start-Sleep -Milliseconds 250
99
+ [PetWin]::mouse_event($LDOWN, 0, 0, 0, [IntPtr]::Zero)
100
+ Start-Sleep -Milliseconds 60
101
+ [PetWin]::mouse_event($LUP, 0, 0, 0, [IntPtr]::Zero)
102
+ Start-Sleep -Milliseconds 400
103
+ Say 'click test done'
104
+
105
+ if ([Math]::Abs($dx) + [Math]::Abs($dy) -lt 20) { Say 'RESULT: FAIL - window did not move' } else { Say 'RESULT: PASS - drag works' }
106
+
107
+ $out | Set-Content -Encoding ASCII $report